lack of evidence of AI use and misapplication of AI case law.
Both are about one case. Neither can ever match a second document. The catalog's entire
value is cross-document recall — a case-specific kind fills the coinage caps, defeats dedupe,
and rots the vocabulary into one-use labels. RFD-DOC-004 D26 AMENDMENT (human,
2026-07-12) rules the kind names the section's function in the genre ("prayer", "lack of
evidence"); the case's own subject matter (parties, technology, dates) belongs in
heading_path/summary. Blake's coinage prompt asks nicely (the pramaan-agents
sibling). This half is the lint that refuses.
Story-2276 already put a lint at the write door: label_is_instruction_shaped(), called by
coin_section_kind() in src/domains/section_kind/service.py. That is the ONLY
door — the broker's propose_section_kind tool is the sole caller, and segmentation
routes every proposed label through it. So generality goes in the same function, behind the same door.
There is no parallel validator and no new call site.
lint_section_kind_label(label, matter_vocabulary=...) -> LabelRefusal | None 1. instruction_shaped (Story-2276, unchanged) 2. too_many_content_tokens <- NEW 3. proper_noun_density <- NEW 4. matter_vocabulary_overlap <- NEW
| Rule | What it sees | Threshold (config) |
|---|---|---|
| content-token cap | A role name is short — "prayer", "lack of evidence". A sentence-length label is describing
one document's contents, not naming a role. Stopwords (of, to,
and) don't count, so a genuinely long role name like "list of dates and events"
(3 content words) survives. |
SECTION_KIND_LABEL_MAX_CONTENT_TOKENS = 4 |
| proper-noun density | Party names, product names, acronyms, possessives — "Addictive Learning", "AI",
"Learning's". These ARE the case's subject matter. Mid-label capitals count only when the label is in
running case, so Preliminary Submissions (Title Case) is not mistaken for a proper noun;
acronyms and possessives survive that suppression, so
Misapplication Of AI Case Law still refuses. |
SECTION_KIND_LABEL_MAX_PROPER_NOUN_RATIO = 0.0 |
| matter-vocabulary overlap | A label built from THIS matter's own distinctive terms can never match a second document. The
distinctive terms are read from the matter's identity strings (title, case ref, filename) — the
capitalised words, minus stopwords, minus the genre floor. This is the rule that catches an
all-lowercase, short, proper-noun-free label like addictive learning objections, which
the other two rules cannot see. |
SECTION_KIND_LABEL_MAX_MATTER_VOCABULARY_TOKENS = 0 |
CoinStatus = "rejected_label" —
the status Story-2276 already shipped. No row is written; the section keeps its byte-proved boundary and
lands on the nearest existing kind or the structural other. The only addition is
CoinOutcome.refusal, which names WHICH rule refused (D9: nothing is silently dropped). That
is diagnostics, surfaced as lint_rule in the tool response — not a state.
other is a last resort, never a bucket
The same human gate that ordered the lint also bounded it. An over-eager lint that refuses real role
labels degrades the whole catalog to other — which is a worse failure than
the case-specific labels it was built to stop, because now nothing has a kind at all. So calibration is
deliberately conservative: when in doubt, the label coins. Rejection is for
case-specific labels ONLY.
Two things enforce that bound. First, the thresholds are loose enough that every role-level kind in real
Indian filings passes. Second, the genre floor: a hand-listed set of role words
(prayer, synopsis, cause, title,
ground, verification, chronology, corporate forms…) that
never enter a matter's distinctive vocabulary. Without it, a firm acting for "Prayer Constructions Pvt
Ltd" could never coin prayer.
test_label_lint_keeps_role_vocabulary_coinable is the assertion of that breadth —
21 role-level kinds, each checked twice: bare, and again against a
deliberately hostile matter vocabulary ("Prayer Constructions Pvt Ltd v Addictive Learning Technology
Ltd", "Synopsis Title Ground Verification Cause Facts Index"). All must coin.
| Label | Verdict | Rule |
|---|---|---|
lack of evidence | ACCEPT | — |
prayer | ACCEPT | — |
preliminary submissions | ACCEPT | — |
lack of evidence of AI use | REJECT | proper_noun_density ("AI") |
misapplication of AI case law | REJECT | proper_noun_density ("AI") |
opposition to Addictive Learning's application | REJECT | proper_noun_density ("Addictive", "Learning's") |
All three golden rejects happen to trip the same rule, so the other two are proved red on their own
planted negatives in test_each_generality_rule_refuses_on_its_own: "detailed narration of
events leading to the dispute" (5 content words) trips the cap, and "addictive learning objections"
— short, lowercase, no proper noun — is invisible to both other rules and is caught only by
matter-vocabulary overlap.
SECTION_KIND_LABEL_MAX_PROPER_NOUN_RATIO=1.0,
SECTION_KIND_LABEL_MAX_MATTER_VOCABULARY_TOKENS=5) and the suite re-run: the golden reject
rows fail, and the end-to-end write-door test fails with
ToolOutcome.AUTO_APPLIED is not ToolOutcome.SKIPPED_UNRESOLVED — i.e. the
case-specific label coins again. That proves two things at once: the lint is what refuses the label, and
the thresholds are genuinely config rather than decoration.
test_case_specific_label_is_refused_at_the_write_door_and_lands_on_other runs against a real
Postgres: a two-section segmentation proposing prayer and
lack of evidence of AI use, plus a direct broker intent for
opposition to Addictive Learning's application. Afterwards — both sections persist
(2 rows: a refused label never un-sections a boundary the bytes proved),
prayer is in the catalog, other is in the catalog, and no kind name contains
"AI use" or "Addictive".
lack of evidence of AI use and
misapplication of AI case law were coined on 2026-07-12, before this lint existed. They are
rename/merge candidates for counsel via the existing dedupe/merge path (D26 guardrail 3:
near-duplicates merge, never fork; guardrail 6: counsel may delete any kind at will, and delete =
merge-into-nothing, with sections re-labelled to the nearest survivor or other). The
operator step is therefore: rename lack of evidence of AI use →
lack of evidence, at which point normalized-name dedupe merges it into the general kind if
one already exists; and rename misapplication of AI case law → the role it actually
plays (misapplication of case law, or merge into grounds). Both are counsel
acts through the catalog surface, not SQL. This lint guarantees no new casualties.
The matter-vocabulary read is scoped by firm_id explicitly as well as by RLS — belt to
the policy's braces. One matter's vocabulary can never leak into another firm's lint.
Instrumentation-exempt (ADR-124/ADR-104): the coinage path carries no
manual_span and no counter today — there are no registry-listed guardrail span
attributes to extend, and this Story does not add a second tracing surface to create some. The refusal is
already visible where it matters: named in the broker tool response (refusal +
lint_rule) and, when coinage is refused, in the section's landing on other.
| File | Change |
|---|---|
src/domains/section_kind/service.py | The lint, extended: three generality
rules, the genre floor, the matter-vocabulary read, CoinOutcome.refusal. |
src/shared/settings.py | Three thresholds. No literals in the rule. |
src/shared/registrar/executor.py | Surfaces lint_rule in the
refusal response. Same outcome, same state. |
tests/integration/test_section_kind_coinage.py | The golden table, the
role-vocabulary breadth floor, the per-rule planted negatives, the end-to-end degrade-to-other
proof. |