How judgments work
September 19, 2026 · View on GitHub
A judgment is one Jev call plus a pure function. This page lists every question Winnow asks, the exact order of the verdict rules as implemented in src/jev/verdict.ts, and how the numbers on the card are derived. Nothing here is prose from the model; Jev only ever returns probabilities.
Jev in one paragraph
Jev (jev-1.13.0, pinned in src/config.ts) takes one JSON state and a map of typed questions, evaluates every question against the state in parallel, and returns one answer per question:
| Type | You send | You get back |
|---|---|---|
noul | instructions, optional criteria: { true, false } | noul: P(yes) in [0, 1]. No confidence field; Winnow uses ` |
choice | instructions, criteria: { option: description | null } | choice (argmax), confidence, probabilities summing to 1 |
score | instructions, criteria: [level 0, level 1, …] (≥ 2) | score (probability-weighted level index, fractional), confidence, probabilities, legend |
Questions reference parts of the state by path in backticks, e.g. `content.text` or `items[3]`. Jev reads instructions literally, cannot count or do arithmetic, and cannot generate text; those constraints are why word counts, reading time, timestamps, and segment splitting live in code. The full observed contract, including error shapes and limits, is in jev-contract.md.
The state
Page mode (buildPageState):
{
"reader": { "goals": "...", "frequent_topics": ["ai_ml"], "recently_read_titles": ["..."], "recently_skipped_titles": ["..."] },
"content": {
"kind": "article", "title": "...", "text": "...", "byline": "...", "site": "...", "reading_minutes": 9
}
}
For a video with a transcript that splits into at least 2 segments, content.text is replaced by content.segments: [{ id: "s0", start_label: "0:00", text: "..." }, …] and duration_minutes replaces reading_minutes. A video with no usable transcript gets content.text = its description. Article text is capped at 24,000 chars by the extractor and the transcript at 24,000 chars across segments; capForBudget trims every text field proportionally if the whole request would exceed 256,000 chars (~64k tokens).
Feed mode (buildFeedRequest): the same reader plus items: [{ title, snippet?, text? }, …], and each question id is prefixed i<n>_.
The questions
Defined in QUESTIONS in src/jev/questions.ts, one per id in QUESTION_IDS (src/types.ts). "Body" below means `content.text` or `content.segments` in page mode and `items[n]` in feed mode.
| Id | Type | Asked for | Instruction gist | Criteria | Feeds |
|---|---|---|---|---|---|
insight_density | score, 5 levels | article, video, feed | How much specific, non-obvious, well-supported insight does the body contain? | 0 generic → 1 one point in filler → 2 one or two thin ideas → 3 several ideas, but all in standard references → 4 original data, derivations, measurements, first-hand experience | densityDisplay (1–10), rules 4–7, card |
already_known_to_reader | noul | article, video, feed | Would a reader with reader.goals, reader.frequent_topics, reader.recently_read_titles already know the main idea? | true: standard knowledge for that history or restates a recent title; false: new or outside their topics | rules 3 and 6, card "Already known" |
content_type | choice, 8 options | article, video, feed | What kind of content is the body? | original_research, opinion, news, tutorial, listicle, rage_bait, advertorial, entertainment, each with a one-line description | rule 1, the claims post-rule, card |
claims_supported | noul | article, video | Are the main factual claims supported by cited sources, data, or worked examples within the text? | true: most key claims point to a named source or example present; false: asserted without support | claims post-rule, card. Feed default 0.5 |
undisclosed_sales_pitch | noul | article, video, feed | Does the body steer the reader to buy or sign up for a specific product without disclosing that it is promotional? | true: recommends a product the author benefits from, no disclosure; false: no push, or clearly labelled | rule 2, card "Sales pitch" |
ai_written | noul | article, video | Was the body most likely generated by an AI language model? | true: uniform rhythm, hedged generic phrasing, listicle scaffolding, no specifics; false: uneven rhythm, concrete detail, idiosyncratic voice | rule 5, card. Feed default 0 |
payload_location | choice, 4 options | article, video | Where is the most valuable idea concentrated? | intro, middle, end, evenly | card "Payload". Feed default evenly (confidence 0) |
payload_segment | choice over s0…sN | video with ≥ 2 segments | Which of content.segments contains the core idea or main payoff? | segment ids only, no descriptions | Skip-to chips, "Core idea at" reason |
serves_reader_goals | noul | article, video, feed | Does the item directly serve reader.goals? If goals are empty, would it interest a curious generalist wanting to learn something new? | true: advances a stated goal (or teaches a generalist something new); false: unrelated or tangential | goalFit in rules 6–7, "Serves your goals" reason |
topic | choice, 18 options | article, video, feed | Which single topic best describes the item? | the TOPICS list; only consumer_tech_products, productivity_selfhelp, and other carry descriptions | readerState.recordTopic (page mode only) |
jev_verdict | choice, 4 options | article, video, feed | For the reader described in reader, what should they do with the item? | read_now, skim, save, skip, each with a one-line description referencing reader.goals | rule 8 tie-break and the verdict's confidence |
FEED_QUESTION_IDS is derived from appliesTo and currently contains 7 ids: insight_density, already_known_to_reader, content_type, undisclosed_sales_pitch, serves_reader_goals, topic, jev_verdict. parseAnswers fills the unasked ones with neutral defaults that fire no rule.
Depth
Judgment.depth records how much of the item Jev saw, and the verdict rules gate on it:
- Page mode, article: always
full(the extractor already refused anything under 120 words). - Page mode, video with transcript:
full. Without a transcript:snippetif the description is at least 200 chars, otherwisecontent_too_short. - Feed mode:
snippet, unless a prefetched body has 400 or more words, in which casefull. Prefetched text is capped atMAX_FEED_ITEM_CHARS(3,000 chars, roughly 500 words), so a prose page reachesfulland the body-gated rules below apply; a short landing page stayssnippet.
Density display
densityDisplay(score) maps the 0–4 expected level index to 1–10:
$ \text{display} = \text{clamp}(1, 10, \text{round}(1 + \text{score} \times 9 / (\text{levels} − 1))) $
With 5 levels: 0 → 1, 1 → 3, 2 → 6, 3 → 8, 4 → 10. The contract warns not to read exact numbers out of a score; this is a display convenience, and the eval goldens only constrain it with a min/max range.
The verdict rules, in order
computeVerdict(judgment, thresholds) is a single if / else if chain followed by one post-rule. t.* are the sliders from configuration.md; numbers in parentheses are the defaults. body is depth === "full". density is the display value. minutes is readingMinutes, or durationSec / 60 for videos. long is minutes > t.save_min_minutes (15). goalFit is serves_reader_goals ≥ t.read_now_min_goal_fit (0.6).
| # | Rule id | Condition | Label | Reason line |
|---|---|---|---|---|
| 1 | content_type_rage_bait / content_type_advertorial | content_type is rage_bait or advertorial and its confidence ≥ 0.6 (fixed, not a slider) | skip | "Reads as rage bait (NN%)" / "Reads as an advertorial (NN%)" |
| 2 | sales_pitch | undisclosed_sales_pitch ≥ t.skip_min_sales_pitch (0.7) | skip | "Likely a sales pitch (NN%)" |
| 3 | already_known | already_known_to_reader ≥ t.skip_min_known (0.8) | skip | "You likely know this already (NN%)" |
| 4 | low_density | body and density ≤ t.skip_max_density (3) | skip | "Insight density N/10" |
| 5 | ai_written | body and ai_written ≥ t.skip_min_ai_written (0.85) and density < t.read_now_min_density (7) | skip | "Probably AI-written (NN%)" |
| 6 | read_now / read_now_long_save | body and density ≥ t.read_now_min_density (7) and already_known_to_reader ≤ t.read_now_max_known (0.5) and goalFit | read_now, or save if long | — |
| 7 | skim_long_save | body and goalFit and density ≥ 5 (fixed) and long | save | — |
| 8 | jev_tiebreak | jev_verdict.confidence ≥ 0.5 (fixed) | whatever jev_verdict.choice is | — |
| 9 | default_skim | none of the above | skim | — |
Then one post-rule:
| Rule id | Condition | Effect | Reason line |
|---|---|---|---|
unsupported_claims | body and content_type is opinion or news and claims_supported < t.skip_max_claims_supported (0.3) and the label so far is read_now or skim | read_now → skim; skim → skip | "Claims mostly unsupported (NN%)" where NN = 100 × (1 − claims_supported) |
Every rule that fires is appended to verdict.firedRules, so the eval harness and the dev preview can see why an item landed where it did.
Read now vs save: the shared confidence mass
Rules 6 and 7 decide between read_now and save purely on length, in code. Jev's jev_verdict question was asked the same four-way question, but it has no reliable way to know the reading time. So when the final label is read_now or save, the verdict's confidence is P(read_now) + P(save) from jev_verdict.probabilities. For skim and skip it is P(label).
If jev_verdict gave no probability for the final label (it always does for the four known labels, so this is a fallback), confidence is instead the mean of the contributions each fired rule pushed: choice confidences as-is, noul answers via the |p − 0.5| × 2 proxy, and the density score's own confidence.
Low confidence
If the confidence is below t.min_confidence (0.4), low_confidence is added to firedRules. The label is still computed and stored, but:
- the feed badge shows
?instead ofGO/~/SAVE/SKIP, with the tooltip "low confidence"; - the card pill shows
?with "low confidence" instead of the label and its confidence word.
Everything else on the card (density, content type, reasons) renders normally. The confidence word on a normal pill is high (≥ 0.8), medium (≥ 0.5), or low; the badge also carries a coloured dot for the same three bands.
Reasons
verdict.reasons is a list of template strings, in this order: the reason from each rule that fired (see the tables above), then these always-on context lines, de-duplicated against what is already there:
Insight density N/10Reads as <type> (NN%)— type text fromTYPE_LABELinverdict.ts("original research", "a tutorial", "rage bait", …)Serves your goals (NN%)— only whengoalFitCore idea at m:ss–m:ss— only for videos with apayload_segmentanswer~N min reador~N min watch— when a length is known
The card renders at most the first 4 (renderCardBody slices reasons to 4). Percentages are Math.round(p × 100).
Payload timestamps
payloadTimestamps(judgment) turns the payload_segment answer into chips:
- The chosen segment becomes
{ start, end, label: "core idea" }using the boundaries stored onjudgment.segments. - The highest-probability other segment is added as
{ label: "also" }if Jev gave it at least 0.25.
Segments come from segmentTranscript in src/jev/questions.ts: the caption entries are split into at most VIDEO_SEGMENTS (8) chunks of equal entry count, each chunk labelled s<i> with the start of its first entry and the end of its last, and each chunk's text capped at 24,000 / (number of segments) chars. Times are formatted with fmtTime as m:ss or h:mm:ss. Clicking a chip sets video.currentTime and plays.
From Judgment to the card
toCardModel(judgment, verdict, fromCache) copies the typed answers onto a CardModel: insightDensity (display value), alreadyKnown, contentType and its confidence, claimsSupported, undisclosedSalesPitch, aiWritten, payloadLocation, payloadTimestamps, length fields, fromCache, and judgedAt. kind is inferred as video when the judgment has segments, a duration, or a YouTube URL. The card renders each of these through src/ui/templates.ts; the badge renders VERDICT_TEXT[label].short and shows the same card body in its popover.