Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/design/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,25 @@ Declined:

The measurement, and how to redo it. `_RECORDED_DIFFS` reads 45 / 5 / 1 / 0 = 51 rows at 1.4.0 / 2.0.0 / 2.1.0 / 2.2.0 and `_WATCHED_DIFFS` 36 / 31 / 30 / 5 = 102 over the same 50 distinct names — five names moved rosters at 1.4.0 and stayed watched at both 2.x baselines, where each has one admitter and no contest to own, so only the 1.4.0 row count fell and the population did not move. 32 of the 45 rows at 1.4.0 sit on radar-tier names and 13 have a single admitter at their recorded shape; none of the fourteen joins those 13, every one having two or more admitters at its recorded shape by the definition of the sweep that found it. THE POPULATION FIGURES, recomputed: drive `main()` at each baseline with `classify()` wrapped, keep the calls whose issue is not None, and for each list the rules satisfying `compare._entry_matches` at that call's measured shape and order — fewer than two admitters is no contest; for each admitter after the first, the winner's `fields` a strict subset of the loser's is narrow-first and justified, the loser's a strict subset of the winner's WITH the loser's issue in `compare._declared_over(winner)` is declared and justified, and anything else is owed a pin unless the name is a key of `_RECORDED_DIFFS` for that ledger. That gives 47 / 3 / 1 / 0 contested, 18 / 3 / 1 / 0 pinned and 14 / 0 / 0 / 0 owed on the tree this arc opened on, and 0 owed at every baseline since. Do NOT replay the corpus load by hand: the `(name, order)` dedup, the baseline-minimum skip and the tier stamp all happen inside `main()`, so a row's tier is read as `main()` reads it — the default-order entry's where there is one, the first-loaded entry's otherwise — by wrapping `_run_worker` for the entries it was handed. THE VERDICTS' values come off that same run — the old side from the worker's rows for the entries `main()` was handed, the new side from `HumanName` and `_tree_parse` as `main()` computes them — and only the facade is compared at a pre-2.0 baseline, `_surfaces_for` returning `{"facade"}` below 2.0.0, so every old and new value in the bullets above is `HumanName`'s. THE "WHAT THE PIN BUYS" FIGURES are measured the same way and their recipe sits beside the rows in tests/v2/test_ledger_guards.py: move the loser above the winner in a COPY of the run's own sorted rule list and re-classify the run's own 352 classified calls, counting the labels that change; the `fields` readings are that recipe with one role dropped from the winner instead of a rule moved, and nothing on disk moves for either. MUTATION CONTROL, in memory around `main()` at 1.4.0 with the worktree untouched: `'Smith, Ph. D. Jr.'` deleted from `_RECORDED_DIFFS['expected_since_1.4.0.toml']` — the contract-tier row of the six-name equal-`fields` pair — makes the run exit 1 and print `UNPINNED CONTEST expected_since_1.4.0.toml: 1 contested pair(s), one per name and losing rule, that no rule outranks by nesting or declaration and no row pins`, naming the name under its winner as `'Smith, Ph. D. Jr.' [contract] ['suffix', 'title']` with the reason "equal `fields`, file order the whole decision". Unmutated, no `UNPINNED CONTEST` block prints at any of the four baselines; a corruption that changed nothing would have meant an inert check, and this one did not. The arc moved no classification: 352 / 247 / 155 / 14 intentional diffs, 0 unexplained and 0 radar unclassified, exit 0, summary lines byte-identical at every commit of the branch — a pin moves no classification, and neither does a check that fires on nothing. RECOMPUTE the row counts with `{k: len(v) for k, v in compare._RECORDED_DIFFS.items()}` and the same over `_WATCHED_DIFFS`; the radar and one-runner splits by taking each row's tier as above and classifying it at its recorded shape against `_sorted_rules`.

### differential-ledger, a pin names one rule (2026-09-05, #508)

The eleventh ledger arc, and a small one: it changes how a `_CROSS_RULE_WINNERS` pin is read, not what any pin says. The winner guard compared `classify()`'s answer to the pin with `startswith`, chosen without a recorded reason, so a pin was satisfied by any rule whose issue extended it. The #498 completeness check, earlier the same day, had borrowed that guard's meaning outright — a `_RECORDED_DIFFS` key counts as "a winner is pinned" because the two rosters' keys are equal — and its review found the case where the borrowed meaning was hollow. Mechanics are owned by `_pinned_rule`'s docstring in tests/v2/test_ledger_guards.py; these are the decisions. Every figure was measured 2026-09-05 on master at `449a7bc`.

Decisions that landed:

- 2026-09-05 #508 — **a pin names exactly one rule.** The guard resolves each pin to one rule's issue — exact where a rule's issue equals the pin, else the single rule whose issue starts with it — and refuses a pin that resolves to none or to several; the winner comparison is then EQUALITY against the resolved issue. Prefix pins stay: a family prefix that reaches one rule names it as well as the full string does, and 17 of the 51 rows are written that way. Measured over the 51 rows before the change: 31 full issue strings reaching only themselves; 17 short prefixes reaching one rule; 2 full strings that are also a strict prefix of a sibling's — `'Bob Jones, author'` and `'Bob Jones, compositeur'`, pinned to the all-titles rule whose issue `... keeps the given/family split, the C1 example` extends; 1 short prefix reaching three rules — `'Andrews, M.D.'` on the bare `fix(comma-family)`. All three ambiguous rows are in `expected_since_1.4.0.toml`, the 2.x rosters have none, and each was latent: `classify()` returned the intended rule. `'Andrews, M.D.'` is spelled out to the lone-post-comma rule that wins it; the Bob Jones rows are unchanged, exact comparison being what their comment already rested on. A second guard, `test_every_pinned_winner_names_exactly_one_rule`, refuses an ambiguous pin on its own at pytest speed, so the failure names the pin rather than a classification. RECOMPUTE: for each ledger and each pin, count the `[[change]]` issues that `startswith` the pin and whether one equals it (the loop is in the #508 issue body).

Found rather than decided:

- **The #498 arc's "a key in `_RECORDED_DIFFS` means a winner is pinned" was true of 48 rows and hollow for 3.** Nothing was wrong today, because `classify()` happened to return the intended rule for all three. What the prefix comparison could not do was FAIL when it stopped: the `'Bob Jones, compositeur'` roster comment rested its pin on the C1 rule's regex being unable to reach the string, an argument no test executed — measured 2026-09-05, forcing `classify()` to answer the C1 string for that name passed the old guard and fails the new one.

Declined:

- Exact match everywhere (2026-09-05) — rewriting the 18 short pins to full strings and ending the family-pin idiom. The idiom is harmless where the prefix reaches one rule, which is 17 of the 18, and the resolver makes the eighteenth loud; a reworded rule breaking its pins is the same outcome under either reading.
- Prefix plus an allowlist (2026-09-05, #508's option 2) — a second list to keep honest, for three rows the resolver handles without one.
- A word-boundary prefix (2026-09-05, #508's option 3) — answers the two Bob Jones rows and not `'Andrews, M.D.'`, and adds a tokenising rule to a string comparison.
- Refusing two rules in one ledger from sharing a prefix (2026-09-05) — a property of the ledger, not of a pin; the ledger's rule families are deliberate (`fix(comma-family)`, `fix(#296)`), and the resolver already makes the only case that matters loud.

### comma-suffix-arc — #291/#296/#316 (2026-07-26 → 2026-08-01)

#291 was filed 2026-07-26 out of the 2.0 vocabulary cleanup, with
Expand Down
2 changes: 1 addition & 1 deletion docs/design/mechanisms.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Problem shape. A test's input depends on two config sets intersecting (a word th

## CROSS-RULE-OUTCOME-PINS — pin who wins the contest

Problem shape. Every per-rule roster measures a rule alone and the gate total is per-corpus, but WHICH rule wins a contested name is neither — and it is exactly what a reorder or a narrowing changes. Contract statement. Contested outcomes are pinned as data: a roster records which rule classifies which contested name, so a change in the winner fails the suite even when every total is unchanged. How it works. A pure file reorder in the 1.4 ledger fails _CROSS_RULE_WINNERS and nothing else in the suite — the pin is the only guard at that granularity. A shape recorded ALONE pins no winner and lives in the other roster, `compare._WATCHED_DIFFS`, so a reader looking for who wins a name does not look there (#501). The entry criterion is that the boundary is ARGUED ANYWHERE and not that it is argued in the guard module, because a ledger comment's argument about which rule wins a name is exactly what no guard and no run can see — the #501 adjudication is the instance, three of its six boundaries having been argued in a ledger comment and one of those three wrongly, green everywhere for a month (decisions.md, the #501 adjudication). The roster's COMPLETENESS is checked at run time over the contests a run measures: `compare.unowned_contests` refuses a measured contest the winner takes by neither nesting nor a declaration and that no row pins, so "`_CROSS_RULE_WINNERS` stays the instrument" for non-nested pairs is now enforced rather than stated (#498). Lives in. tests/v2/test_ledger_guards.py (_CROSS_RULE_WINNERS). Reach for it when. Two rules can claim the same name and you are about to change either one, or their order.
Problem shape. Every per-rule roster measures a rule alone and the gate total is per-corpus, but WHICH rule wins a contested name is neither — and it is exactly what a reorder or a narrowing changes. Contract statement. Contested outcomes are pinned as data: a roster records which rule classifies which contested name, so a change in the winner fails the suite even when every total is unchanged. A pin names exactly one rule — the rule whose issue equals it, else the single rule whose issue starts with it — and the guard refuses a pin that names none or several before asking `classify()` anything (#508; a full issue string can be a strict prefix of a sibling's, so the comparison is equality against the resolved rule, not a prefix). How it works. A pure file reorder in the 1.4 ledger fails _CROSS_RULE_WINNERS and nothing else in the suite — the pin is the only guard at that granularity. A shape recorded ALONE pins no winner and lives in the other roster, `compare._WATCHED_DIFFS`, so a reader looking for who wins a name does not look there (#501). The entry criterion is that the boundary is ARGUED ANYWHERE and not that it is argued in the guard module, because a ledger comment's argument about which rule wins a name is exactly what no guard and no run can see — the #501 adjudication is the instance, three of its six boundaries having been argued in a ledger comment and one of those three wrongly, green everywhere for a month (decisions.md, the #501 adjudication). The roster's COMPLETENESS is checked at run time over the contests a run measures: `compare.unowned_contests` refuses a measured contest the winner takes by neither nesting nor a declaration and that no row pins, so "`_CROSS_RULE_WINNERS` stays the instrument" for non-nested pairs is now enforced rather than stated (#498). Lives in. tests/v2/test_ledger_guards.py (_CROSS_RULE_WINNERS). Reach for it when. Two rules can claim the same name and you are about to change either one, or their order.

## VOCABULARY-FEEDS-STRUCTURE — a wordlist edit can move the comma decision

Expand Down
Loading