Data Model — refactor-stable-gate-substrate-01KWK3FY

The entities are gate keys, inventory rows, and the theater contract.

GateAllowlistKey (converted, IC-01)

FieldBeforeAfterRole
rel_path— (absent)str (NEW)Disambiguates qualname collisions (implement/review ×2)
enclosing_qualnamestrstrFunction scope of the allowlisted site
token_lineint (raw AST lineno — THE fragility)str (frozen code_tokens_by_line token)The authoritative content comparand

YAML entry shape (after):

- file: specify_cli/cli/commands/agent/workflow.py   # NEW — part of the key
  qualname: implement                                # part of the key
  token: "feature_dir = primary_feature_dir_for_mission ( repo_root , handle )"  # FROZEN comparand
  line: 453        # NON-authoritative locator (diagnostics/jump-to only)
  reason: "..."    # unchanged

membership. node.lineno feeds NO key anywhere (SC-001 grep evidence).

occurrences of that token in that function — if the gate must distinguish occurrences, the entry carries an explicit count: (default 1) and the scanner compares occurrence counts (mirrors the reference implementation's two-doctor-sites handling).

the locator is refreshed opportunistically, never compared.

  • The scanner emits (rel_path, qualname, token) from live source; comparison is set
  • Collision rule (within one function, identical tokens): an entry covers all
  • Violation message format keeps {file}:{line-locator} ({qualname}) for ergonomics;

Audit row identity (converted, IC-02/IC-03)

Row typeBefore keyAfter key
SinkRow (untrusted)f"{rel_path}:{line}"(rel_path, enclosing_qualname, token) via composite_key_from_file
ResolutionRow (surface)samesame
SelectionRow (surface)samesame

Inventory markdown rows keep their human columns; the line column becomes a locator. Tagged rows: a row may carry [inventory-only] in its notes column to be exempt from the overcount guard (narrow, documented use: retained documentation of an intentionally-removed sink — each such row must reference the removing change).

Tripwire directions (per audit, after IC-02/03)

1. Undercount (exists today, re-keyed): every DISCOVERED sink must match an inventory row by composite identity → RED naming the missing row otherwise. 2. Overcount/ghost (NEW): every inventory row (minus [inventory-only]) must match a live discovered sink → RED naming the ghost row otherwise. 3. Check-3 (KNOWN_CANDIDATE_FILES) — path-level, already drift-immune, untouched.

Theater triad (per converted gate — the acceptance instrument)

LegDrivesMust
Drift-immunitytop-level check_*_gate / audit main()-equivalent with synthetic source shifted +1 line, allowlist/inventory untouchedstay GREEN
Content-detectionsame entry point with an allowlisted/documented site's token editedgo RED (staleness/mismatch)
Non-vacuitysame entry point with a synthetic NEW offending sitego RED with actionable message

All legs drive the exact entry points CI runs (T005 model) — helper-only theater is a review reject.

Quarantine set (IC-05)

  • 31 marked nodes → 16 after CT9 (15 markers removed across ~5 files).
  • Stay-behind reason format: @pytest.mark.quarantine # <honest diagnosis> — <issue ref>.

Doctrine styleguide delta (IC-04)

examples; graph.yaml regenerated (byte-freshness).

  • +6 principles (research.md D7 outline), +patterns/anti_patterns with PR #2308-cited

Invariants

1. Frozen-comparand invariant: no stored/compared gate or audit key contains a raw line number after this mission (locators excepted, never compared). 2. Tool-derivation invariant: every frozen token was produced by composite_key/composite_key_from_file — hand-typed tokens are a review reject. 3. Zero-production invariant (NFR-002): no diff under src/specify_cli/ or src/runtime/. 4. Reference-implementation invariant: test_no_worktree_name_guess.py keys are NOT modified (FR-005 is documentation only).