Source: cases/CASE-DSL.md
# CASE DSL v1 — a case as an INSTANCE of a law
A `.case` file says: *this opinion cites law L in this ROLE; for each element of L it touches,
here is what kind of ruling it made (SCOPE), the facts it relied on (verbatim, with offsets, and
which way each fact cuts), what it held, and which known variant of the element this is.*
Line-oriented, not JSON, diff-friendly; every quote is machine-verified.
One file per (case, law): `neov2/cases/<law>/<reporter>-<vol>-<page>-<slug>.case`.
`<law>` is the directory name and the id of `neov2/law/<law>.law`.
v1 (2026-11-10) answers the QA1/QA2 reviews of v0: a wrong label must not be ingestible just
because a `misfit` sentence explains it. Changes from v0 are marked **[v1]**; see `NOTES.md`.
## The source-text contract (HARD RULE, unchanged)
* Canonical text of a case = the `text` field of its IR record
(`neochartdocroot/docroot/ir/caselaw/<reporter>_<vol>.jsonl`, record `id` = case path).
* Offsets are Python `str` indices into that text: Unicode code points, 0-based, end-exclusive —
the same convention as the IR's own `cite_spans`. `text[start:end] == quote`, exactly.
* The header pins the text version: `source … sha1=<IR source_sha1> chars=<len(text)>`.
* Quotes are copied byte-for-byte **including OCR damage**. Never "fix" a quote. A quote never
crosses a paragraph break (`\n`).
* The IR text is capped at ~60 000 chars for long opinions.
* `casedsl.py quotes-equal <git-rev> <files>` proves that a revision of the annotations changed
no quote (kind, offsets, text). **[v1]**
## Grammar
```
casedsl 1 [v1] version line; absent = v0 rules
# comment
case <case_path> the IR id, e.g. /us/533/0158-01
name <short caption>
cite <reporter cite (year)>
court <court> the deciding court (annotator-corrected if the IR field is wrong)
date <YYYY-MM-DD> must equal IR decision_date
url <docroot-relative url> must equal IR address
source ir/caselaw/<file>.jsonl sha1=<source_sha1> chars=<n>
law <law-id> [<subsection(s) in issue>]
role applied | comparator | rule-announced | background [v1] REQUIRED
posture <free text>
result claimant-won | claimant-lost | mixed | remanded | not-decided
element <id>[.<sub>] : <holding> [<basis>]
role <as above> [v1] optional override of the case-level role for this block
scope merits | procedural | hypothetical | alternative | dissent | lower-court | superseded [v1] default merits
party <who this block is about> [v1] typed qualifiers replace v0 `for`
count <count / claim / incident> [v1]
theory <legal theory / route> [v1]
opinion <panel | en-banc | majority | ...> [v1] which opinion inside a multi-opinion IR record
variant <genus>/<species>
fact @<start>..<end> [supports|cuts-against [absent]] | <verbatim quote> [v1] polarity REQUIRED
rule @<start>..<end> [quoted] | <verbatim: the test in the court's words> [v1] optional tag
held @<start>..<end> | <verbatim: the sentence deciding this element>
gloss <annotator's words — never the source's>
defense <id> : <holding> [<basis>] same body; "satisfied" = the defense succeeded
misfit <free text> what the notation still cannot express
```
### Citation role **[v1]** — what the citation to the statute is doing
| role | meaning | counts as an instance? |
|---|---|---|
| `applied` | the court applies the statute to this case's own facts | yes (if scope = merits and holding ≠ not-reached) |
| `rule-announced` | the court states/chooses the test (or the governing mode of analysis) but does not apply it — remand, reserved, or conceded-if-proved. Holding MUST be `not-reached` and the block MUST carry a `rule` quote | no |
| `comparator` | the statute is a yardstick or analogy for something else (categorical approach; sibling statute construed by contrast). The case's own facts are not facts of this statute | no |
| `background` | cited as context, predicate or procedural hook; no element discussed | no |
The role on the `law` section is the default for every block; a block may override it (an
`applied` case usually contains some `rule-announced` blocks; a `comparator` case may have none
that are `applied`). In a `rule-announced` case, bare `not-reached` blocks (no quotes) simply
inherit the role; any block with content must satisfy the rule-announced constraints or override.
### Ruling scope **[v1]** — what kind of ruling the block records
`merits` (default: this court decides the element for this case) · `procedural` (venue,
jurisdiction, waiver, preclusion, double jeopardy, limitations mechanics — the element is only the
occasion) · `hypothetical` (a claim nobody brought, analysed to decide something else, e.g. the
disparate-impact claim inside Ricci's defense) · `alternative` ("even assuming…": a second,
independent ground) · `dissent` (a non-controlling opinion) · `lower-court` (a ruling of the court
below that this court reports but does not review) · `superseded` (a ruling inside the same IR
record that a later opinion in that record displaces, e.g. a panel opinion followed by the en banc
opinion; a lower-court finding the reviewing court vacates; since wave 2 also a ruling displaced by a
later opinion in ANOTHER IR record, e.g. a panel opinion reheard en banc — name that record in the
`opinion` qualifier or a gloss).
Only `role applied` + `scope merits` + a decided holding is an **instance**; `parse()` exposes it
as `block['instance']`, and `VARIANTS.md` counts only instances (everything else is tabulated
separately). Consumers must filter on it.
### Holding and basis
`<holding>`: `satisfied` | `not-satisfied` | `not-reached`. A `not-reached` block has no basis.
`<basis>` — epistemic status (rules fixed after QA1):
| basis | use when |
|---|---|
| `pleaded` | allegations taken as true (12(b)(6), indictment on its face) |
| `triable` | claimant's evidence survives summary judgment (only with `satisfied`) |
| `no-evidence` | summary judgment against the claimant, or verdict reversed for insufficiency (only with `not-satisfied`) |
| `proved` | a trier of fact (jury or bench) found it after weighing evidence, or sufficiency was upheld |
| `conceded` | undisputed between the parties **as to this case's facts** (conceding which legal standard would govern is `role rule-announced`, not `satisfied [conceded]`) |
| `assumed` | arguendo |
| `law` | pure question of law, independent of evidentiary sufficiency |
| `waived` | abandoned / not preserved (only with `not-satisfied`) |
### Quote lines
* `fact` = what happened in the world, or was alleged. **Polarity [v1]** is relative to the
ELEMENT (or defense) of the block, not to who won: `[supports]` = tends to show the element is
met; `[cuts-against]` = tends to show it is not. Add `absent` when the "fact" is the court
noting what is NOT alleged or proved (`[cuts-against absent]`). A satisfied block may contain
cuts-against facts the court discounted, and vice versa — that is the point.
* `rule` = the legal test in the court's words. Tag `[quoted]` when the words are another
court's, quoted or block-quoted with approval by this court.
* `held` = the sentence in which THIS court decides the block. (Under scope `dissent` /
`lower-court`, the dissent's / lower court's conclusion as reported in the text.)
### Ids, qualifiers, variants
* `<id>` must be declared in `neov2/law/<law>.law` (checked; kind mismatches are warnings while
the law side is still moving). `<law>/ELEMENTS` is the historical v0 vocabulary and the
fallback when no law file exists.
* The same element may appear several times, distinguished by typed qualifiers. Use the most
specific type that is true: `party` (a defendant/plaintiff/pairing), `count` (a count, claim or
incident), `theory` (a legal route to the same element), `opinion` (panel vs en banc…).
* Variant `genus/species`: genus = the doctrinal or evidentiary route, species = the recurring
fact pattern; the genus never encodes success or failure — the holding does.
* No escaping anywhere: a quote is everything after `| ` up to end of line.
## Workflow / tools (`neov2/tools/casedsl.py`)
```
casedsl.py new <case_path> <law> header skeleton (v1) with sha1/chars pin
casedsl.py text <case_path> [start end] source text, paragraph per line with [offset]
casedsl.py grep <case_path> <regex> paragraphs matching a regex
casedsl.py fill <file.case> author quotes as `fact @? [supports] | …`; fill resolves offsets
casedsl.py check <file|dir> verbatim diff + law ids + role/scope/polarity rules; exit 1 on error
casedsl.py variants <law-dir> regenerate <law>/VARIANTS.md
casedsl.py quotes-equal <rev> <file|dir> prove no quote changed since a git revision
```
`parse()` is the loader contract. v0 keys are unchanged (`case, header, law, claim, lawmeta,
blocks[kind,id,holding,basis,variant,for,gloss[],quotes[kind,start,end,quote,line]], misfits[]`);
v1 adds `version`, `role`, and per block `role`, `role_explicit`, `scope`, `party`, `count`,
`theory`, `opinion`, `instance`, and per quote `tags`, `polarity`. For v1 files `for` is
synthesized from the typed qualifiers (`party=…; count=…`) so v0 consumers keep working.