Skip to content

Do the CJK comma ledger rules still earn their place now that nothing demands them? (fix(cjk-comma-compound) reaches 23 radar names and 0 contract) #495

Description

@derek73

Correction (2026-09-02): two figures below were measured with a hand-built
file→tier map that wrongly marked corpus.jsonl as contract; it is radar. The
split is 326 contract / 790 radar, not 786/330, and the jr rule's reach is
2 contract / 3 radar, not 3/2. The argument and the five radar-only pairs are
unaffected — see the correction comment for the recompute.

Rationale

#488 demoted every composed comma/Latin-wrapper form around a CJK name to
tolerated input and split the projection: corpus_cjk_tolerated.jsonl is
radar tier, where an unmatched diff is "reported, never fatal" and
"cannot fail the run or demand a rule" (_CORPUS_TIERS, #468).

The two fix(cjk-comma-*) ledger families were kept — #488's verification
records them "still classify 11 + 7, now radar-sourced". That is a
measurement, not an argued decision: decisions.md#cjk-comma-demotion
records the format-purity line and the C1→W3 move, and says nothing about
whether the rules that classify the demoted names should stay.

Measurement (2026-09-02, c1a98a9)

Of the 1116 distinct corpus texts, 786 are contract tier and 330 radar.
Scanning expected_since_1.4.0.toml for same-tier rule pairs whose fields
are strictly nested and whose regexes reach a common corpus name gives 11
pairs; 5 of them are contested only over radar names:

earlier (wide) → later (narrow) co-matched
fix(#296) credential-only comma → fix(comma-family) lone post-comma 2
fix(#296) credential-only comma → fix(comma-precomma-family) 2
fix(cjk-comma-compound) → fix(cjk-glued-honorific-peel) 17
fix(cjk-glued-honorific-peel) → fix(suffix-routing) numeral 1
fix(cjk-glued-honorific-peel) → fix(suffix-routing) jr 1

Sharper, and the number that makes this actionable: three of the five rules
can never classify a contract name at all
, because their name_regex
reaches none. Regex reach is an upper bound on what a rule explains — a rule
cannot explain a name it does not match — so this is sound without a run:

rule contract reach radar reach
fix(#296) a credential-only comma string... 0 2
fix(cjk-comma-compound) 0 23
fix(suffix-routing) roman numeral 0 4
fix(cjk-glued-honorific-peel) 16 21
fix(suffix-routing) jr 3 2

Recompute: load the ledger with tomllib, take each rule's regex reach over
the distinct name values in corpus*.jsonl, and split by _CORPUS_TIERS
(a text held by both tiers reads contract, matching main()'s dedup).

The question

A rule nothing demands is not automatically a rule to delete — it still
classifies a diff rather than leaving it merely reported, and the radar tier
exists so those diffs stay watched. But nothing has re-argued the case since
the demotion, and the rules carry ongoing cost: _CORPUS_CLAIMS entries,
_CROSS_RULE_WINNERS rows, and — once #382 lands — a precedes_narrower
exemption each, whose why today says only "radar-only since #488, kept so
the diff stays explained".

Options

  1. Keep them, and say so. A decisions.md#cjk-comma-demotion bullet
    arguing that a radar diff is worth classifying rather than merely
    reporting. Cost: none, beyond writing the argument.
  2. Delete the three rules with zero contract reach. They are the clean
    case: nothing they could ever classify can fail a run. Cost: each deletion
    moves a _CORPUS_CLAIMS entry and any _CROSS_RULE_WINNERS rows, the
    dormancy check has to be re-reasoned for whatever survives, and the
    demoted diffs start reporting as UNCLASSIFIED on every run — visible
    noise that a reader has to learn to skip.
  3. Case by case, which the reach table argues for. fix(cjk-comma-compound)
    has 23 radar names and no contract reach, and is the classifier-of-record
    for shapes _CROSS_RULE_WINNERS pins — the strongest keep-or-delete
    argument either way. fix(cjk-glued-honorific-peel) and
    fix(suffix-routing) jr still reach contract names and are not candidates
    for deletion at all; they appear here only as the narrower half of a
    contested pair.

Not in scope

Rule ORDER. #382 settles that separately: all five pairs get a
precedes_narrower exemption there, so this issue is only about whether the
rules should exist, not about which one wins while they do.

Blocked by

#382 — its exemptions are what make the current state legible, and its
_ORDER_EXEMPTION_EFFECT roster is what would have to be re-recorded here.

Activity

  1. added this to the v2.3 milestone on Sep 2, 2026
  2. self-assigned this
    on Sep 2, 2026
  3. derek73 commented on Sep 3, 2026

    @derek73
    OwnerAuthor

    Correction — two figures in the issue body were measured wrong. The argument
    is unaffected; the digits are not.

    What was wrong

    The body says "Of the 1116 distinct corpus texts, 786 are contract tier and 330
    radar." Measured against compare._CORPUS_TIERS, it is 326 contract and 790
    radar
    .

    Root cause: the measuring script hand-wrote the file→tier map instead of reading
    _CORPUS_TIERS, and marked corpus.jsonl as contract. It is radar —
    compare.py's roster says so, and decisions.md's #468 bullet records why
    ("corpus.jsonl (scraped from v1's test banks) and corpus_issues.jsonl
    (harvested from the tracker) became RADAR"). It is also the largest corpus at 486
    distinct names, which is the intuition that misleads: the biggest file reads like
    the contract one and is the demoted scrape.

    That is docs/design/AGENTS.md axis 2 — "your detector is a second, unreviewed
    implementation; it must read the same vocabulary and boundaries the rule reads."

    The reach table also has one wrong row. Recomputed with the real roster:

    rule contract reach radar reach
    fix(#296) a credential-only comma string... 0 2
    fix(cjk-comma-compound) 0 23
    fix(suffix-routing) roman numeral 0 4
    fix(cjk-glued-honorific-peel) 16 21
    fix(suffix-routing) jr 2 3

    Only the last row moved (was 3 / 2). Recompute:

    tier = {}   # read the roster, do not rebuild it
    for c in sorted(_TOOLS.glob("corpus*.jsonl"),
                    key=lambda p: (compare._CORPUS_TIERS[p.name] != "contract", p.name)):
        for line in c.read_text().splitlines():
            if line.strip():
                row = json.loads(line)
                tier.setdefault(row["name"] if isinstance(row, dict) else row,
                                compare._CORPUS_TIERS[c.name])

    What is unaffected

    • The issue's central claim stands: three of the five rules have zero
      contract-tier regex reach
      , so they can never classify a name that could fail a
      run. Same three rules, same zeros.
    • The five radar-only pairs are still the same five. Recomputed per pair with
      the real roster: pairs 1, 4, 5, 6, 7, 8 are contract-backed and 2, 3, 9, 10, 11
      are radar-only — identical to what the body lists.
    • fix(cjk-glued-honorific-peel) and fix(suffix-routing) jr still reach contract
      names and are still not deletion candidates.

    Filed under #497 as well

    This is a fourth instance of that issue's class, and the first where the wrong
    value reached a tracker rather than a comment. Worth noting for whoever picks
    #497 up: the sibling roster comment in tests/v2/test_ledger_guards.py
    (_ORDER_EXEMPTION_EFFECT) states the same division as an ARGUMENT with no
    digits — "some reach contract-tier names, the rest only radar" — and is therefore
    still correct. That is the convention AGENTS.md prescribes, working.

  4. derek73 commented on Sep 3, 2026

    @derek73
    OwnerAuthor

    Closing as no — the radar-only rules earn their place, and "nothing demands
    it" is not a deletion criterion.

    The measurement that settles it, and why this issue asked the wrong question

    This issue named five rules, reached from the five radar-only contest pairs of
    #382. That was the wrong population. Measured over expected_since_1.4.0.toml
    at baseline 1.4.0, 19 of the 72 explaining rules explain only radar names —
    48 of the 352 intentional diffs:

    names rule
    11 fix(cjk-comma-compound) comma routing compounds with the CJK order flip
    7 fix(cjk-comma-honorific-peel) glued honorific peels off a post-comma given name
    4 fix(#296) dr is not postnominal vocabulary, so a trailing Dr. is a name word
    3 feat(#273) typographic nickname delimiters recognized by default
    3 fix(#367) a title no longer displaces a leading particle out of the leading run
    2 feat(#269) Arabic بن prefix chains onto family (non-Latin new-recognition)
    2 fix(#296) a credential-only comma string reads a name and its postnominal
    2 fix(#296) a dropped prenominal takes the name position it occupies
    2 fix(comma-family) a comma followed only by titles keeps the given/family split
    2 fix(suffix-routing) a two-token name ending in a credential acronym
    2 fix(suffix-routing) a two-token name ending in a roman numeral
    1 each fix(#325), fix(#342), fix(#360), fix(#367) inferred-title, fix(#397), fix(#424), fix(credential-pair-order), fix(suffix-routing) M.A.

    Applied as a deletion criterion, "nothing demands it" removes 26% of the rules
    that explain anything — including:

    • fix(#342) and fix(#397), the two NOT WANTED rules. Those exist to
      classify a reading nobody wants so the gate stays green while the bug is open,
      and they carry a delete-when-fixed instruction the dormancy check enforces once
      the fix lands (decisions.md#differential-ledger, the fields-only arc). Deleting
      them would make two open bugs report as UNEXPLAINED on a radar tier that cannot
      fail, i.e. silently.
    • feat(#269) and feat(#273), records of new recognition behavior.
    • fix(#360), the record of ste moving into the never-given particles.

    Why the tier does not carry the argument the issue assumed

    The corpus-tier arc states what radar is for: "the tier split gave the
    differential somewhere to WATCH a name without promising it"

    (decisions.md#differential-ledger, the corpus-tier arc). A rule explaining a
    radar name is what watching looks like. Delete it and the diff does not
    disappear — it reports as UNCLASSIFIED, which is watched-but-unexplained, strictly
    less information for strictly no gain.

    The vocabulary already distinguishes the case this issue was reaching for.
    dormant marks a rule that explains nothing, and requires a written reason
    precisely because an exemption nobody can justify should be deleted instead. A
    rule explaining eleven radar names is not dormant; it is doing its job on names
    the contract does not answer for.

    So the criterion that would justify deleting a ledger rule is "it explains
    nothing" — which dormant already covers and the dormancy check already
    enforces — not "the names it explains cannot fail the gate".

    What this leaves standing from #382

    Nothing changes for the five precedes_narrower exemptions that cite this issue.
    Their claim was always about the NAMES being radar, never about the rules being
    deletable; #496 was closed on the same measurement and its exemption text says so.
    The reach figures in the correction comment above are unaffected.

    Recompute

    uv run python tools/differential/compare.py --baseline 1.4.0
    

    Read the ## <issue> (N) headings and their name lists, then take each name's
    tier from compare._CORPUS_TIERS (read the roster; do not rebuild the file→tier
    map — corpus.jsonl is the largest corpus and it is radar). Note the report
    truncates each rule's list at ten names
    (names[:10], with no "and N more"
    line), so a rule explaining more than ten cannot be tiered from the printed
    output — that truncation is what produced a wrong count of 20 on the first pass
    here. Take the full lists from by_issue, or raise the slice in a scratch copy.

    Recorded in docs/design/decisions.md so the next reader does not re-derive it.

  5. added a commit that references this issue on Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions