Skip to content

Track B: held-out data registry for every Track B and open-population gate - #560

Merged
MaxGhenis merged 12 commits into
masterfrom
trackb-holdout-registry-20261021
Oct 24, 2026
Merged

MaxGhenis merged 12 commits into
masterfrom
trackb-holdout-registry-20261021

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

The dynamics coordinator made one registry of held-out data the record for every Track B gate and every open-population gate (2026-10-21). Several lanes will cite it, so it lands on master on its own, outside B0.2's draft (#554).

What it adds

  • docs/design/track_b_holdout_registry.json, schema track_b_holdout_registry.v1.
    • Five historical imports, each verified at a line of gates.yaml, b2.py, disability.py or epuf.py, with scored evidence identified by gate:
      • H001 gate_m6: PSID waves 2015–2023;
      • H002 B2: PSID waves 2013 and 2015;
      • H003 gate_epuf_fill: EPUF 2006 TEST;
      • H004 gate_m4: the PSID employment-status panel and DI ASR 2023;
      • H005 gate_w1: Supplement 2023 §6.B, DI ASR 2023 table 19, Census HH-4 and AD-3 2023, the 2024 frame and the Mermin–Smith anchors.
    • Six disclosed pairs: four shared units (H001–H002, H001–H004, H002–H004, H004–H005) and two unresolved relations (H003–H004 and H003–H005: whether the 2023 DI ASR and Supplement tables report data years EPUF 2006 also covers).
    • complete: false until B0.2's exposure inventory (docs/design/track_b_b0_2_exposure_inventory.json) exists.
  • src/populace_dynamics/track_b/holdout_registry.py: the rules and four controlled lists, each changed only by a reviewed code change:
    • the source vocabulary (SOURCES);
    • the registrations outside gates.yaml (REGISTRATIONS_OUTSIDE_GATES_YAML);
    • the scored runs (SCORED_RUNS), each pinned to its gate and the SHA-256 of its bytes;
    • the authorized historical imports (HISTORICAL_IMPORTS), each binding one entry id to its gate, unit keys and scored runs.
  • Tests:
    • tests/test_track_b_holdout_registry.py, unit tier, 109 tests: invented registries and temporary git repositories, with an independently specified family map, overlap rule and transition table.
    • tests/test_track_b_holdout_registry_committed.py, artifact tier, 10 tests: the committed registry against the committed tree and its pinned runs/ artifacts, its base commit, a pull request's own commits, and, on pushes, every first-parent commit since the base.
  • scripts/first_estimates_birth_evidence.py: the birth-evidence seal excludes the new module, as it does b2.py. Nothing in the historical projection imports it.

Invariants (hold for every registry and every commit)

  1. Schema and types. The schema is track_b_holdout_registry.v1. Every field has its type, and dates are real. Malformed input, including appended entries, is reported, never raised; this is property-tested and was fuzzed with 5,000 cases.

  2. Controlled vocabulary. The registry's sources equals the module's SOURCES, so adding a source or family is a reviewed code change. Catalog references are unique, which refuses aliases. Every unit key names a declared source. The key year means what the source declares: for PSID, the interview wave; for EPUF, the vintage in its title; for publications, the edition.

  3. Overlap. A key is source:years[:scope...]. Two keys share a unit when the source is the same and the years intersect; scope never narrows that.

  4. Disclosure. Live entries that share a unit list each other in disclosed_overlaps. Disclosures are symmetric, never self-referential, and name only known ids. An unresolved relation is also disclosed.

  5. Family review. An entry reviews every earlier live entry of the same family that it neither shares a unit with nor discloses. A review has outcome disjoint, a substantive reason (at least 40 characters, no placeholder words) and citations: path:line within a committed file, or an https URL.

  6. Provenance and status.

    • A registration starts as a lone candidate. Against the base, a new registration must arrive alone and move on only in a later commit. Its scored run's exact bytes must not have been committed, at any path and whether or not they were later moved or deleted, in the history of the commit that first records the entry on the first-parent chain (git log -m --find-object). When git cannot answer, or the clone is shallow so an empty search proves nothing, the check fails closed; replacement refs are ignored.
    • A historical import starts as used, and its id, gate, unit keys and scored runs must equal an authorization in HISTORICAL_IMPORTS. A second import under another id, or one claiming other units or runs, is refused. B0.2's inventory adds an import, or a correction to one, as a new authorized entry in a reviewed code change. Imports close once the base registry is complete.
    • Transitions run only candidate→frozen→used and candidate/frozen→withdrawn, and status is the history's last status.
  7. Used means scored. Each scored_evidence item is {path, field, equals}.

    • The path is a scored run listed in SCORED_RUNS for the entry's gate. Its bytes are read from HEAD's tree (not the working tree, not a symlink) and must match the pinned SHA-256. Floor builders, candidate lists and dry runs are not listed, so they cannot stand as scored evidence, and a pathname cannot be reused for other content.
    • The field is gate, gate.name or registration_id, and its value equals equals.
    • equals must identify the gate in gates.yaml (key, id or registration_id) or in REGISTRATIONS_OUTSIDE_GATES_YAML.
    • Each identifier belongs to one gate only.
  8. Append-only. This holds against the base commit; between a pull request's own consecutive commits; and, on pushes, between every pair of consecutive first-parent commits since the base, where each snapshot is also validated against its own commit's tree. A newly created registry is admitted against an empty one.

    • Base entries stay as a prefix, in order.
    • id, provenance, gate, lane, source, units and unit_keys never change.
    • gate_identifiers, evidence, scored_evidence, disclosed_overlaps, overlap_review and history only extend. Rules only extend.
    • Declared sources never change.
    • complete never goes back to false.

    The base is chosen as follows:

    • on a push, the event's before commit;
    • on a pull request, the merge base with origin/$GITHUB_BASE_REF;
    • otherwise, the merge base with master;
    • and when that merge base is HEAD, HEAD's first parent.

    In CI, an unresolvable base fails the test.

  9. Incomplete flag. complete: true requires the completion record to be committed.

Merge with a merge commit. This PR and every later registry PR must merge with a merge commit, never a rebase, so draft commits never enter master's first-parent history (registry rule 7).

Known incompleteness. Legacy gates gate_1, gate_2, gate_2b and gate_2c have committed scored outputs that are not yet imported. B0.2's exposure inventory imports each or records that it holds out nothing a Track B or open-population gate could use; the registry stays complete: false until then.

What the rules cannot catch mechanically. The timing rule sees scores committed to this repository, not scores computed elsewhere; gates.yaml preregistration and issue #42 cover that. The overlap rule cannot see two sources sharing underlying data, or two editions reporting the same data years; family reviews and disclosures cover that, and reviewers of each registry change must check them. Vocabulary changes are code changes, reviewed as such.

Verification

Review history:

  • Round 1 asked for changes (one high, five medium, one low), answered in c299a304.
  • Round 2 asked for changes (six medium, one low), answered in 9e3c6a4c.
  • Round 3 asked for changes (five medium, two low), answered in 296bf2bb.
  • Round 4 asked for changes (two medium, two low), answered in 5f7d995d. The reviewer's own counterexample scripts, replayed on that commit, are all refused.
  • Round 5 asked for changes (one medium, three low) at d5dfef9b, answered in the next commit: shallow clones fail closed, merge-resolution and replacement-ref topologies are tested, subprocess failures are reported, and SHA-256 ids are accepted.

No real data is read, and nothing in gates.yaml, engine/ or runs/ changes.

axiom: n/a: Track B audit infrastructure; no policy encoding.

🤖 Generated with Claude Code

… commit

The dynamics coordinator made one registry of held-out years, editions,
tables and partitions the record for every Track B gate and every
open-population gate (2026-10-21). This adds it at
docs/design/track_b_holdout_registry.json with five verified prior uses,
marked incomplete until B0.2's exposure inventory lands, and
populace_dynamics.track_b.holdout_registry, which enforces its rules:
schema v1, unique ids, symmetric disclosures, no silent shared unit,
legal status histories, scored evidence for used entries, the
incomplete flag, and append-only changes against the base branch.

Tests: 25 unit tests, with Hypothesis properties; 16 of 16 mutants
killed (docs/design/track_b_holdout_registry_mutations.json). Tiers:
unit 9,958 -> 9,983.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 21, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
social-security-model Ready Ready Preview Oct 24, 2026 3:54pm UTC

Request Review

- High: gate_m6's entry kept event years and reference years apart only
  in prose. Unit keys now name a declared source and its years; PSID is
  keyed by interview wave, and each entry's units state the gated and
  report-only reference and event years separately.
- Overlap: two keys share a unit when the source matches and the years
  intersect; scope segments never narrow it, so table_19 and table19
  overlap. Sources are a declared vocabulary whose catalog references
  must differ, which refuses alias names. Same-family relations the rule
  cannot see need an overlap_review entry with a reason. The coarser keys
  expose three more real shared units (gate_m6 with B2 and gate_m4, B2
  with gate_m4), now disclosed both ways.
- Used means scored: scored_evidence is structured (path, field, equals);
  the file must be committed, inside the repository and not a symlink,
  and its field must equal one of the entry's gate identifiers, each of
  which belongs to one gate only.
- Master pushes compare against the push's before commit, or the first
  parent when the merge base is HEAD itself.
- Registrations start as a lone candidate in a commit of their own;
  historical imports start as frozen or used and close when the registry
  is complete.
- The immutable and extending field lists are specified independently in
  the tests; every field is typed and dates are parsed, so malformed
  input is reported rather than raised (fuzzed).
- The birth-evidence seal excludes the new module, as it does b2.py.

Tests: 62 unit tests; 36 of 36 mutants killed. Tiers: unit 9,958 ->
10,020.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- EPUF's key year is the vintage in the file's title (the 2006 file,
  released in 2011); the seeded keys are pinned by an independently
  specified test.
- The source vocabulary lives in the module (SOURCES), so adding a
  source or family is a reviewed code change; the registry's copy must
  equal it. Family reviews need the outcome "disjoint", a substantive
  reason (no placeholders, at least 40 characters) and path:line or URL
  citations checked against the committed tree; an unresolved relation
  is disclosed instead. The two deferred EPUF reviews are now
  disclosures.
- Historical imports start as used, and their scored evidence must have
  been committed before the registry was.
- Scored evidence must be a runs/*.json artifact read from HEAD's tree
  (not the working tree, not a symlink), matched on gate, gate.name or
  registration_id, whose value names the gate in gates.yaml or in the
  module's REGISTRATIONS_OUTSIDE_GATES_YAML.
- On pushes, every pair of consecutive first-parent commits since the
  base must append only (history_problems), so a later commit cannot
  hide a change.
- Appended entries with malformed histories are reported, not raised.
- The family-review tests use an independently specified family map and
  overlap rule, with fixed same-family cases.
- Tests split by tier: invented registries and temporary repositories
  (unit, 74) and the committed registry, which reads committed run
  artifacts (artifact, 8). 47 of 47 mutants killed.

Tiers: unit 9,958 -> 10,032; artifact 4,253 -> 4,261.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. Preparation artifacts as scored evidence (medium). Scored runs are now a
   controlled list in code, SCORED_RUNS {path: (gate, sha256)}. Evidence must
   name a listed run that belongs to the entry's gate, and the committed
   blob must match the pinned SHA-256. Floor builders, candidate lists and
   dry runs are not listed, so they are rejected.
2. Import cutoff by pathname (medium). Historical imports are limited to
   HISTORICAL_IMPORT_GATES, a code-level list, and start as used. Content
   is pinned by hash, so renames, re-adds and merge-created registries
   cannot launder a future gate's evidence into an import.
3. Registration after scoring (medium). Committed.registered_at walks the
   registry's first-parent history; a registration must be on record at a
   commit where its scored run was not yet committed.
4. History checks (medium). history_problems validates each snapshot
   against its own commit's tree (evidence, citations, identifiers,
   completion record), admits a new registry against empty_like, and skips
   invalid snapshots as predecessors.
5. Rebase merging (medium). The committed module now checks every commit
   on the pull request branch (full=False), so a rebase cannot introduce
   an unchecked change and restoration.
6. Terminal-status mutant (low). The unit tests enumerate the permitted
   transitions independently (EXPECTED_TRANSITIONS) and check terminal
   statuses; used -> withdrawn is now killed.
7. Malformed inputs (low). Decoding, NUL and '..' paths, oversized line
   numbers, non-mapping registries and malformed predecessors return
   problems instead of raising.

Seeded entries: H005 cites gates.yaml:4311-4327 for its Census labels;
H003 cites the open-population design as PR #557 (draft).

Mutations: 53 of 53 killed (restored source sha256 aae22b1d...).
Tests: 98 passed, 2 skipped. Tier counts: unit 10,048 (+16), artifact
4,263 (+2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. Moving or removing a score bypassed preregistration (medium). The
   timing check now asks whether the scored run's exact bytes were
   committed, at any path, in the history of the commit that first
   records the entry on the first-parent chain (git hash-object, then
   git log -m --find-object). A score committed elsewhere and moved, or
   committed, deleted and restored, or committed on the base branch while
   the registration waited on a side branch, is refused. When git cannot
   answer, the check fails closed.
2. Historical-import permission could be reused for unscored units
   (medium). HISTORICAL_IMPORTS replaces the list of gates: each
   authorization binds one entry id to its gate, unit keys and scored
   runs, so an import under another id, with other units or with other
   runs is refused. A correction found by B0.2's inventory is a new
   authorized entry in a reviewed code change.
3. The first-parent registration rule had no regression test (low). The
   side-branch scenario now kills the --topo-order mutant.
4. Malformed inputs still raised (low). Entries without string ids are
   reported as malformed before any diagnostic reads them; JSON and YAML
   parser limits (a 5,000-digit integer) and NUL arguments to git return
   problems, or registry_at's documented ValueError and LookupError.

Rules 6-8 in the registry now describe these mechanisms, and rule 7 states
that a pull request touching the registry is merged with a merge commit,
never rebased (the review's nonblocking point 5).

Known and left to B0.2's inventory (nonblocking in the review): legacy
gates gate_1, gate_2, gate_2b and gate_2c have committed scored outputs
that are not yet imported; the registry stays incomplete until then.

The reviewer's own counterexample scripts, replayed on this code: every
import and timing bypass is refused, and the valid order still passes.

Mutations: 64 of 64 killed, including the reviewer's --topo-order
survivor and new checks on the authorization, the any-path timing
search, fail-closed timing, malformed ids, parser limits and NUL
arguments. Tests: 122 passed, 2 skipped (with the birth-evidence test).
Tier counts: unit 10,059 (+11), artifact 4,263.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/tier_counts.json and tests/README-tiers.md conflicted. Recounted on
the merged tree with pytest --collect-only -m <tier>: unit 18,189
(master's 18,088 + this PR's 101), artifact 4,263 (4,253 + 10),
integration_psid 1,450, reproduction_legacy 520, oracle_policyengine
308; total 24,730. README-tiers.md had kept round-2 numbers on this
branch; it now matches the manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 23, 2026
…scored run

#560's round-4 review found four legacy gates (gate_1, gate_2, gate_2b,
gate_2c) with committed scored outputs beyond the registry's five seeded
imports. The inventory must import each, or record that it holds out
nothing a Track B or open-population gate could use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. Shallow history made the timing check fail open (medium). An empty
   find-object search is now a negative only in a complete repository:
   when git reports a shallow clone, committed_by returns None and the
   fail-closed diagnostic rejects the entry. A positive find is still
   conclusive. The search also runs with --no-replace-objects, so a
   replacement ref that orphans a commit cannot hide a committed score
   (the review's nonblocking environment-trust point).
2. A score added only in a merge resolution had no regression test (low).
   That topology is now tested; dropping -m from the search fails it.
3. Subprocess failures escaped (low). _git and _git_bytes turn TypeError
   and OSError (an argument list too long, a missing git, a None
   argument) into git-style failures, so validate reports a problem and
   registry_at and history_problems raise their documented LookupError.
   validate also checks the shape of injected import authorizations.
4. SHA-256 object ids were ignored at event boundaries (low). SHA_RX
   accepts 40- and 64-digit ids, so a SHA-256 push's before commit and a
   SHA-256 pull request head are honoured.

Mutations: 70 of 70 killed, adding merge diffs not searched, shallow
history trusted, replacement refs honoured, SHA-256 ids rejected,
subprocess OSError escapes and authorization shapes unchecked.
Tests: 130 passed, 2 skipped (with the birth-evidence test).
Tier counts: unit 18,197 (+8 since round 4), artifact 4,263.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 24, 2026 07:41
…eview round 6)

Round 6 (Astra, at daf0242) found one medium and one low.

- Medium: a transient git failure while resolving a push's `before`
  commit made `base_commit` fall back to the merge base, which on master
  is HEAD^1, so the history check covered fewer commits and passed an
  edit-and-restore of immutable unit keys. A push now resolves its own
  `before` commit or raises LookupError; an unreadable event, or one
  naming no commit id, also raises. An all-zero `before` (a branch's
  first push) still falls through to the merge base.
- The same class on pull requests: `pull_request_head` returned None for
  an unreadable event, and the committed-registry test then skipped the
  pull request's own commits. On a pull_request event it now raises.
  One helper, `_event`, reads the payload for both.
- Low: `_git_bytes`'s TypeError and OSError handling had no test. A new
  test makes `hash-object` fail four ways and requires `committed_by`
  to return None and validation to report the timing as unanswerable.

Tests: 137 passed, 2 skipped (20 new) in the two registry modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stry-20261021

# Conflicts:
#	scripts/first_estimates_birth_evidence.py
#	tests/estimates/test_birth_evidence_artifact.py
Round 6 added 20 unit tests; master (2195258) adds none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All 75 checks again fail the registry tests with the mutation and pass
without it, against the merged source (a1fd1d99). The runner now records
parametrized test ids in full; the previous record cut them at the first
space, so some failing cases were missing from its lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 24, 2026 15:25
…recount tiers

Only the tier files conflicted; the seal lists merged cleanly and keep
the registry's and S1's entries. Recounted by collection on the merged
tree: unit 18,254 (master 18,125, +129), artifact 4,271 (master 4,261,
+10), integration_psid 1,450, reproduction_legacy 520,
oracle_policyengine 308; total 24,803. The registry and birth-evidence
tests: 150 passed, 2 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit 097d3cb into master Oct 24, 2026
12 checks passed
@MaxGhenis

MaxGhenis commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Merge audit (coordinator, 2026-10-24 17:08Z): merged at head 77b5bc31 with --match-head-commit, merge commit 097d3cb4.

  • Independent review: round 7 (GPT-6.1 Sol) returned APPROVE at 404a7ee6. Both round-6 findings are resolved in substance, with executed evidence. Two low findings remain, neither blocking. First, no regression test covers an unset GITHUB_EVENT_PATH: a mutant that drops TypeError handling at holdout_registry.py:1187 survives, though production raises LookupError correctly. Second, two pytest IDs in the mutation record (check 73) are truncated; the mutant itself is killed. The review also read the remerge diff and found it only removes conflict markers.
  • The delta since the review is the single merge commit 77b5bc31, whose parents are 404a7ee6 and master f8ffc7d9. The coordinator checked it mechanically:
  • CI: gh pr checks exited 0 at 77b5bc31 (11 passed, 1 skipped), the tier-count check included.
  • State: MERGEABLE, not a draft, no CHANGES_REQUESTED review.
  • Body and diff: a data-terms grep of the body and the diff f8ffc7d9...77b5bc31 found no matches.
  • Tier slot: released. The next queued PR recounts against master 097d3cb4.

This branch was successfully deployed

1 active deployment
Preview — 77b5bc31 Deployed Oct 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant