Skip to content

Should a pinned winner match its rule by prefix? (Bob Jones, compositeur is pinned to a full issue string that is also the prefix of a sibling rule's, and the guard passes whichever of the two wins) #508

Description

@derek73

Rationale

test_the_recorded_rule_still_wins_each_contested_name feeds each _RECORDED_DIFFS shape to classify() and asserts the result startswith the pinned winner in _CROSS_RULE_WINNERS. A prefix was chosen so a row could name a rule family (fix(comma-family)) rather than one issue string, and so a pin survives a rule's issue being reworded past the prefix.

The cost is that a pin is satisfied by ANY rule whose issue extends it. Where two rules share a prefix, the guard cannot tell which one won, and that is the one thing the roster exists to say (mechanisms.md#CROSS-RULE-OUTCOME-PINS: "pin who wins the contest"). #507's completeness check borrows this guard's meaning outright — a key in _RECORDED_DIFFS counts as "a winner is pinned" because set(winners) == set(shapes) — so a pin that names two rules is a pin that owns nothing.

The measurement (2026-09-05, master at 449a7bc)

Over the 51 rows in _CROSS_RULE_WINNERS, three have a pin that is a prefix of more than one rule's issue in their ledger, all in expected_since_1.4.0.toml:

name pin rules whose issue starts with it what classify() returns today
Andrews, M.D. fix(comma-family) three: …a comma followed only by titles keeps the given/family split, …, the C1 example, …lone post-comma piece routes to suffix/title, not first the lone-post-comma rule
Bob Jones, author fix(comma-family) a comma followed only by titles keeps the given/family split two: itself, and …, the C1 example, whose issue extends it by a suffix the all-titles rule
Bob Jones, compositeur same same the all-titles rule

The first is a deliberate short pin naming a family; the other two are FULL issue strings that happen to be a strict prefix of a sibling's. Bob Jones, compositeur's roster comment (added by #507) already says so and rests the pin on the C1 rule's name_regex — anchored ^john\s+smith,\s*mr\.?… — being unable to reach the string. That argument is true and nothing executes it: widen that regex and hoist the rule, and classify() returns the C1 rule, startswith still passes, and the pin has changed meaning with every guard green.

In the three 2.x ledgers no pin is ambiguous (measured over their 6 rows).

RECOMPUTE:

import importlib, tomllib
from pathlib import Path
g = importlib.import_module("tests.v2.test_ledger_guards")
for ledger, winners in g._CROSS_RULE_WINNERS.items():
    issues = [r["issue"] for r in tomllib.loads(
        (Path("tools/differential") / ledger).read_text())["change"]]
    for name, pin in winners.items():
        hits = [i for i in issues if i.startswith(pin)]
        if len(hits) > 1:
            print(ledger, repr(name), repr(pin[:40]), len(hits))

Options

  1. Exact match, with the three rows spelled out. got == expected, and Andrews, M.D. pinned to the lone-post-comma rule's full string (which is what wins it). Simplest, and it ends the family-pin idiom: a rule reworded past its old issue string breaks its pins, which is arguably right — a reworded rule is a different claim. Cost: every existing prefix pin that is unambiguous today (most of the 51) must become the full string too, or the guard has two modes.
  2. Prefix match plus an ambiguity guard. Keep startswith, and add a guard that refuses any pin whose prefix matches more than one rule in its ledger unless the row is listed in an explicit allowlist with a reason. Today that list is the three rows above. Cheap, keeps the idiom, and the allowlist is exactly the roster of "pins that mean a family" — but an allowlist is a second thing to keep honest.
  3. Prefix match on a delimiter. Treat the pin as matching only at a word boundary, so …keeps the given/family split does not match …split, the C1 example. Fixes the two full-string rows and leaves Andrews, M.D.'s family pin as it is; but it does not answer whether a family pin should exist at all, and the comma in , the C1 example makes the boundary rule fiddly.

Option 1 is the honest one if the roster's job is "which rule wins"; option 2 is the cheap one if family pins are wanted. Either way Bob Jones, author and Bob Jones, compositeur deserve a guard that executes the argument their comment makes.

Where this is written down

mechanisms.md#CROSS-RULE-OUTCOME-PINS, the roster's docstring in tests/v2/test_ledger_guards.py, and decisions.md's #498 completeness check entry, which says a _RECORDED_DIFFS key means "a winner is pinned" on the strength of this guard.

Activity

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

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions