Skip to content

SIPP income, assets, pension and SSI readers (D2) - #581

Draft
MaxGhenis wants to merge 22 commits into
masterfrom
ir/d2-20261024
Draft

MaxGhenis wants to merge 22 commits into
masterfrom
ir/d2-20261024

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

WIP: round 7 approved 695798ed with four low findings and four notes, answered in 6e96c64d and merged with master at 085a33b9; the round-8 delta review is next. This is milestone D2 of the income-and-resources design (#559, the D2 row of the milestone table). It adds readers for the SIPP releases of 2018 through 2025 (reference years 2017–2024). The tier-count update waits for the coordinator's slot.

What it adds

  • src/populace_dynamics/data/sipp_income.py: read_sipp_income_months, a person-month reader.
    • Concepts are selected explicitly, and pipe-delimited files are read with usecols.
    • Quotes are literal characters (quoting=csv.QUOTE_NONE), so the reader sees the same physical records as the layout validator.
    • Identifiers stay strings, and value domains are strict.
    • It can attach the calendar-year replicate weights.
  • src/populace_dynamics/data/sipp_income_registry.py: canonical concepts and each release's Census definitions: labels, source and storage types, codes, ranges, universes, ownership level, period, corrections and user notes. It needs no network access at run time.
  • data/external/sipp_income/: the evidence and its tooling.
    • Evidence: the captured Census metadata, 107.5 MB in 228 tracked files, none over 4.2 MB.
    • Tooling: the dictionary parser, the registry generator, the concept selection, the cross-release crosswalk, label and domain checks, refusals and source rulings.
    • CRLF files are marked -text, so git stores the bytes the registry hashes.
  • Tests: tests/income_resources/test_sipp_income.py, test_sipp_income_evidence.py and test_sipp_income_refusal_properties.py.

Concepts: 561 are selected, covering:

  • SSI and Social Security receipt and amounts;
  • assets, debts and net worth, with each item's ownership level;
  • retirement accounts and pensions, with employer offer and participation from 2021;
  • disability;
  • citizenship, nativity and year of entry;
  • living arrangement;
  • the design and linkage variables (panel, wave, month, person and household identifiers, the monthly weight, the variance stratum and half sample, and 241 calendar-year replicate weights).

Verification sources: the Census variable API verifies the 2022–2025 releases. Its 2018–2021 endpoints return 404, so each of those releases is verified against its own dictionary and storage schema.

Every disagreement between sources is either refused or carries a named ruling:

  • source_rulings.json names 13 domain rulings covering all 64 release-and-variable disagreements in domain_checks.json, with both sources' locations and intervals;
  • the registry generator refuses a disagreement that lacks an exact, cited, settled ruling.

User notes: the inventory lists 355 Census user notes and indexes. 101 are stored (93 notes and 8 indexes). Eight notes quoted individual survey cases, so they are kept as exclusions: their URL, title, release and variables stay, but not their bodies. The capture script refuses those URLs before any request. A guard, record_bearing_note, refuses to store any new page holding a case record in any form it lists:

  • an identifier column, or a cell that has a cell of digits alone below it and begins with an identifier name or, in a first-row or header cell, names one anywhere;
  • an identifier name followed by a digit with only spaces or simple marks between, or by a run of three or more digits that is not a year, with at most 40 characters and no period between;
  • a table row with a cell that begins with an identifier name, or in a first-row or header cell names one anywhere, and, in any other cell, even one that names an identifier too, a run of three or more digits that is not a year, even in a variable-definition table;
  • a table row, or a preformatted line, of three or more numbers;
  • a run of nine or more digits in the text a reader sees, or of 14 or more, an SSUID's length, anywhere in the page;
  • a preformatted block with an identifier and a digit.

The two rules that follow an identifier name read three things, all with entities decoded: the visible text with HTML comments; the page source with its tags, attributes and scripts; and each quoted attribute value other than href and src, and each quoted string in a script, one at a time. In the text and comments a tag counts as a space. In the source, the 40 characters also hold no angle bracket, quote or equals sign, and href and src values, whose slugs name identifiers without values, are removed from every tag outside scripts and styles and from their opening tags. A tag opens with a letter, ! or /. Script and style contents, up to their end tag or the end of the page, are read as they stand, so no < in script code hides a src assignment. The table and preformatted rules read each cell's or block's text without its tags; the row-header exemption also reads a header cell's scope attribute. The nine-digit rule skips comments and noscript blocks, whose tracking numbers are not page content.

These forms are out of scope: a value before its identifier name; <br>-separated CSV text without an identifier; div grids; an identifier name and its value in different attribute values or script strings, or one of them in a string and the other in script code; a script string after an unpaired quote mark, as in a comment such as // it's; an identifier compared with a value in script code, as in PNUM < 101; an attribute value that holds a raw >, which ends its tag; a value in an href or src value, unless it is a run of 14 or more digits; and a cell outside a table's first row and header cells that names an identifier after other words. The table rules in turn refuse some definition and index tables that name an identifier in a heading cell with digits below it, or beside a run of three or more digits that is not a year in its row; such a page stops the capture loudly.

Invariants

  • Only a verified release, concept, source type and value domain may enter. Every verified source must admit a value, apart from the API's documented missing sentinels.
  • A domain on which sources disagree enters only under an exact, cited, settled ruling.
  • One input person-month yields one output row, in input order. A quote never joins two physical records, and a carriage return never splits one: the reader yields one row per LF-delimited data line, or refuses. A malformed layout is refused.
  • Person identifiers are strings, stable within a panel across months, waves and releases.
  • Source codes and weights pass through unchanged. There is no household or joint allocation and no annualization.
  • The corrected 2018–2021 TSSSAMT requires a file downloaded on or after Census's 2025-07-24 correction.
  • Replicate joins need verified, unique, complete keys and matching base weights.
  • No metadata is borrowed from another release. The frozen tables reproduce the hashed evidence without network access.
  • No stored page shows a form record_bearing_note refuses.
  • Each guard rule, and each limit on what the identifier rules read, has a minimal invented page and a twin that differs only in what it reads (33 pairs): disabling the rule admits its page, and dropping the limit refuses its twin. The exemptions and the bounds on link-target removal are pinned by invented record and definition pages instead.
  • Each ruling member's PDF citations, with a page or without, are exactly one {year}_dictionary.pdf#page={page} per page its dictionary entry holds.

Each invariant has a Hypothesis property or an example test. The domain rule and Census's own crosswalks give differential tests.

Open questions

Each of these refuses the affected concepts rather than guessing; refused_concepts.json and source_rulings.json list every case.

  • Early monthly Social Security receipt (ESSSMNYN, 2018–2020): it has no verified codes. Annual receipt and amounts remain.
  • ESSI_BRSN, 2021–2022: the category order conflicts.
  • The 2020 joint asset and debt families: they disagree with Census's correction note.
  • Disability definitions: 2020 TDIS6AMT and EDIS8MNYN conflict.
  • Missing codes or ranges: 2021 TSSI_ERSN and EBOATOWNER3 lack value codes, and the 2021 interview address identifier has an inverted printed range.
  • SSI0 inputs not yet verified: no RMD identifier or retirement-fund access input has been found.

Tests

Counted by collection over the three test modules:

Tier Tests
unit 183
artifact 216

All 399 pass locally with two workers at 085a33b9, and so do 12 of the 13 birth-evidence seal tests. The other, the reachability test's track-U assertion, needs modules this sparse clone leaves out. CI runs that test and the U2 isolation tests.

Review

  • Round 1 was stopped before it finished, for a reason unrelated to the code.

  • Round 2 (fresh, at 1f51b60a): REVISE, with one high and two medium findings.

    • High: eight stored user notes quoted individual survey cases.
    • Medium: a quote in an unselected field could join two physical records, silently dropping person-months.
    • Medium: 64 admitted domain disagreements had no named rulings.
  • Fix (eb77fc19):

    • removes the eight bodies and adds the exclusions and guard;
    • sets QUOTE_NONE, with a minimized regression and a quote property;
    • adds the 13 rulings and the generator's refusal;
    • adds properties for the vintage gate, malformed layouts, replicate keys, source conflicts, ownership, periods and evidence pins.

    The regenerated concept, definition, release, replicate, correction and variable-to-note tables are identical to 1f51b60a's. The note table keeps all 101 entries: the eight exclusions have an empty body path and the status excluded_record_bearing. The evidence pins lose the eight bodies and change only for source_rulings.json and user_notes_manifest.json.

  • Round 3 (delta, at eb77fc19): REVISE, with two medium and two low findings.

    • Medium: the capture guard missed record forms such as long-format value tables, row headers, prose with words between an identifier and its value, preformatted blocks and plain-text CSV.
    • Medium: a lone carriage return in an unselected field split one physical record into two person-months for pandas while the layout check saw one.
    • Low: the THEQ_BUS ruling misdescribed the dictionary's printed range.
    • Low: the generator did not check a ruling's PDF page citations.
  • Fix (7521c6d8):

    • the guard refuses the forms then listed under User notes. Tests add 16 invented record pages (the round-3 guard missed 13), 4 invented definition pages it still admits, and three properties. Every stored note still passes;
    • _check_layout strips one LF, then one CR, and refuses any other carriage return. Tests add the review's minimized regression, a CRLF file and a carriage return in the quote property's alphabet;
    • the THEQ_BUS reason now says the printed upper bound is malformed and the lower bound differs from the API's. The pin of source_rulings.json is regenerated, and no other table changes;
    • the generator checks PDF citations against the dictionary pages, and the damage property adds a wrong page and another release's PDF.
  • Round 4 (delta, at 7521c6d8): APPROVE, with seven low findings and four notes.

    • Low: three guard rules had no test that failed without them, because every invented record page also tripped the digit-run rules.
    • Low: the identifier rules skipped scripts and attributes, which are stored but not shown.
    • Low: a year beside an identifier name refused definition pages and index links.
    • Low: the three-number rule missed Census's negative-dollar form and preformatted lines. (Round 4 also listed <br> CSV text and div grids; those stay admitted and out of scope.)
    • Low: the notes, the docstring and the round-3 commit message overstated three rules.
    • Low: the PDF citation check ignored citations without a page.
    • Low: no test pinned the 40-character window.
  • Fix (04a2b89a):

    • each guard rule now has a minimal refused page and an admitted twin (eight pairs);
    • the identifier rules read the page source, and a 14-digit run refuses anywhere;
    • years from 1900 to 2099 are exempt from the 40-character and same-row rules, with admitted year pages and a property;
    • numeric cells admit the signed-dollar, Unicode-minus and bracketed forms, and preformatted lines of three numbers refuse;
    • every .pdf citation is checked, and each damage case must fail at its own check;
    • a test pins the window at 40 characters;
    • the THEQ_BUS reason, the reader's docstring and the notes are corrected. Only the source_rulings.json pin changes in the registry.

    17 of the 129 evidence tests fail against 7521c6d8's guard, and 16 mutants are all caught. Every stored note still passes. (Round 5 corrected four claims in this fix's message: the eight twins covered five rules and three readings, not every rule; the 16 mutants had two for the window width and none for the digit-below rule or the row-header and definition-table exemptions; only pageless_pdf passes generation under the parent check, while pageless_other_release_pdf stops on a file missing from the invented folder; and 14 of the 16 invented record pages, not all, carried the invented SSUID. The heading correction also needs "that is not a year".)

  • Merge (ffc4d485): origin/master at 1ec57a5d (D1a, PSID income and wealth readers and field registry (D1a) #575). The birth-evidence check keeps D1a's exclusions and reachability assertion, then D2's.

  • Round 5 (delta, at ffc4d485): APPROVE, with five low findings and five notes.

    • Low: reading the page source refused pages that only name an identifier, and index links: the 40-character window ran across attribute boundaries, and the adjacency rule fired on link slugs.
    • Low: mutants of the new rules survived every test, because the source twins were each refused by both source rules.
    • Low: four claims about the tests overstated them (listed above).
    • Low: the notes and this body overstated which rules read the source, and the same-row rule's condition.
    • Low: some record forms were still admitted; the review gave a fix for an identifier inside a heading.
  • Fix (c1a3c7b6):

    • in the source, the window holds no quote or equals sign, and href and src values are removed from tags before the identifier rules read it (the 14-digit rule still reads them). The review's pages that only name an identifier are now admitted definition pages; a 14-digit run in a link target is still refused;
    • twins now cover every rule and each source-reading limit (17 pairs), with record pages for the year exemption's lookahead and its absence from the adjacency rule, a row header outside a definition table, two numbers per preformatted line, and a property over preformatted lines;
    • a first-row or header cell that names an identifier anywhere, with a cell of digits alone below it, is refused;
    • the notes, the docstring and the reader's carriage-return sentence are corrected;
    • found by this fix's own mutants: the visible-text window stopped at an angle bracket, though the text holds no tags by then, so a decoded &lt; or &gt; between an identifier name and a value admitted the page. The window there now stops only at a period, as the round-3 rule meant ("within 40 characters of the same sentence"); the source reading still stops at tags. A twin pins it, 18 pairs in all.

    With ffc4d485's guard, 9 of the 153 evidence tests fail, the same 9 in each of three Hypothesis seeds. All 32 guard mutants are caught in seeds 0 and 7, each of the 18 twins isolates its rule or limit, and every stored note still passes. (Round 6 corrected two claims in this fix: the 18 twins did not cover the heading-cell rule, each mark that stops the source window alone, unquoted href and src link targets, or adjacency in the text alone; and the other L5 forms were out of scope only in this body, not in the docstring. Two of the "now admitted" definition pages, tracking_number_in_link_target and row_header_outside_definition_table, were already admitted by the parent.)

  • Merge (60b67f82): origin/master at d8302233 (SM1, SM1: differential mortality by lifetime earnings, education and race (normalized within age-sex-year cells) #570). It has no conflicts; git auto-merged the birth-evidence check and its script, which both branches changed.

  • Round 6 (delta, at 60b67f82): APPROVE, with three low findings and four notes.

    • Low: the new source limits admit an identifier and its value in different attribute values or script strings, or split by a quote or equals sign within one, and values in link targets; the docstring's "stay inside one attribute value or string" overstated the window.
    • Low: nine of the review's ten mutants of the changed rules survived, so the twin claims overstated the tests.
    • Low: a header cell that names an identifier after other words, with a value elsewhere in its row, was admitted.
    • Notes: the heading-cell rule refuses some definition and index tables; an angle bracket in a script comparison hid a src assignment from the source reading; four wording corrections; PNUM &gt; 100 is now refused.
  • Fix (c8dfe4f1):

    • the docstring, the notes and this body list the forms the source limits admit as out of scope, with the review's other residual forms;
    • twins pin each mark that stops the source window alone, unquoted href and src link targets, adjacency in the text reading alone, and both heading-cell rules, 26 pairs in all. Record pages pin the heading-names rule's two cell conditions, and a definition page pins its limit to those cells;
    • a first-row or header cell that names an identifier anywhere now also counts for the same-row rule, as the review proposed;
    • a tag opens with a letter, ! or /, so a comparison in a script no longer hides a src assignment;
    • the notes, the docstring and the test comments are corrected.

    With c1a3c7b6's guard, 2 of the 165 evidence tests fail, the same 2 in each of three Hypothesis seeds: the two that pin this fix's code changes. All 44 guard mutants are caught in seeds 0 and 7, each of the 26 twins isolates its rule or limit, and every stored note still passes. (Round 7 corrected two claims in this fix: a minified comparison, a CDATA marker or a</b in a script still hid a src assignment, and the bounds on link-target removal were pinned only against a < followed by a space or digit.)

  • Merge (695798ed): origin/master at 0f69bbb2 (OP1, OP1: open-population harness around the sealed loop #566, and its report fix, Fix review r10's report findings for OP1 #589). It has no conflicts; git auto-merged the birth-evidence check and its script, which both branches changed.

  • Round 7 (delta, at 695798ed): APPROVE, with four low findings and four notes.

    • Low: a < followed by a letter, / or ! in a script still opened a "tag" whose href and src values were removed, so a minified comparison, a CDATA marker or a</b hid a src assignment.
    • Low: the source window stops at any angle bracket, raw or decoded, which the lists did not say, and the out-of-scope list left out angle-bracket splits and script comparisons.
    • Low: the same-row rule skipped any other cell that began with an identifier name, which no list stated.
    • Low: the review's mutants of the tag pattern and of the same-row rule's cell conditions survived.
    • Notes: the heading-cell rules also refuse some definition and index rows that hold a value in their row; the quote twins' comma is load-bearing; a condition was always true; round 5's L4b page is now refused.
  • Fix (6e96c64d):

    • the source reading splits out script and style elements, keeps their contents as they stand, and removes href and src values only from their opening tags;
    • the identifier rules also read each quoted attribute value other than href and src, and each quoted string in a script, one at a time, decoded and joined by periods;
    • the same-row rule reads every other cell, even one that begins with an identifier name;
    • record and definition pages pin each of the review's surviving mutants, and seven new twins pin the new rules and limits, 33 pairs in all. Two older twins now use forms that only the source reading refuses;
    • the docstring, the notes and this body name the angle-bracket limit, the new out-of-scope forms and the rows the table rules refuse.

    With 695798ed's guard, 26 of the 201 evidence tests fail, the same 26 in each of three Hypothesis seeds: the ones that pin this fix's code changes. All 67 guard mutants are caught in seeds 0 and 7; this fix's own checks found four of them unpinned, and record pages now pin them. Each of the 33 twins isolates its rule or limit, no page the parent guard refuses is admitted, and every stored note still passes.

  • Merge (085a33b9): origin/master at 172f690f (Held-out data registry: round 7's follow-ups (unset event path, record ids, zero push boundary) #580 and the commits before it). It has no conflicts and changes no D2 path.

  • Round 8 (delta, at 085a33b9): next.

The eight removed bodies remain in this branch's earlier commits. A squash merge keeps them out of master's history.

Status

  • The build ran in three attempts; two stopped on network errors. Commits 0482dc31 and 5bdd595b hold the later attempts' edits. 1f51b60a lists the two reader modules among the birth-evidence check's post-review exclusions; dac0d5f9, ffc4d485, 60b67f82, 695798ed and 085a33b9 merge master.
  • One CI failure is expected: the tier-count test, until the slot.

Data

No SIPP microdata was downloaded, opened or used to compute a statistic, because a SIPP release will be held out for the SSI gate. Tests use invented records and committed metadata.

axiom: n/a: survey-file readers and metadata registry for the microsim; no PolicyEngine country-model or RuleSpec encoding changed

🤖 Generated with Claude Code

MaxGhenis and others added 9 commits October 24, 2026 09:31
Pins the documentation the SIPP income-and-resources registry is verified
against, with no microdata requested or opened:

- the Census SIPP variable API for 2022-2025 (2018-2021 return HTTP 404;
  the 404 bodies are kept);
- each release's data dictionary PDF, its pdftotext layout and the parsed
  per-variable metadata, the public-use and replicate storage schemas,
  the replicate dictionaries and Census's Python input examples;
- the locator pages they were found from.

capture_manifest.{txt,json} records every URL, UTC retrieval time, byte
size and SHA-256; derived_manifest.json pins the derived files and the
parser. The parser now refuses an inverted range (min above max) instead
of admitting an empty domain: the 2021 dictionary prints EHRESIDENCID as
"Minimum 100001, Maximum 40999". A folder .gitattributes keeps the CRLF
pages and input examples byte-identical to their pinned hashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sipp_income_registry.py embeds, per release 2018-2025, each admitted
concept's Census name, label, type, value codes and ranges, generated by
data/external/sipp_income/build_registry.py from the committed metadata
only (API for 2022-2025, the release dictionary and storage schema for
2018-2021). Unknown releases, concepts and codes raise; a concept absent,
suppressed or without a verified domain in a release is refused there,
with the reason in refused_concepts.json.

sipp_income.py reads a pipe-delimited pu{year} file with usecols, every
column as a string first, then validates each token against that
release's exact domain before conversion. SSUID stays a string; the
person key is panel:SSUID:PNUM; replicate weights attach many-to-one on
SSUID, PNUM and MONTHCODE only when the sidecar's keys are unique and
complete and REPWGT0 equals WPFINWGT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Invented pipe-delimited files only (INVENTED_* names); no staging
directory is read. Covers column selection, dtypes, refusal of
unverified releases, concepts and codes, release-specific sentinels,
the entry-year and roster crosswalks, replicate linkage, and Hypothesis
properties: row count and order preserved, person ids stable across
months and waves, weights carried unchanged, unverified codes refused.
The evidence tests replay the registry from the committed metadata.

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

Verification now uses every Census source a release has and records each
disagreement:

- 2018-2021 labels come from the release dictionary (the verifying
  source); the storage-schema label is kept and checked against it. Four
  conflicts: 2018 EJSOWNRE/EJSOWNRP resolve to the dictionary on Census's
  user note; 2020 TDIS6AMT (state vs local) and EDIS8MNYN (military vs
  railroad) stay open and are refused.
- 2022-2025 values must satisfy both the variable API and the release
  dictionary (API missing codes exempt). This admits GHLFSAM and GVARSTR,
  whose API entries list only a missing code, and narrows owner pointers
  to Census's documented codes (domain_checks.json lists all 72 cases).
- source_rulings.json holds every hand ruling with its evidence; the
  generator stops on an unruled label conflict. Open: ESSI_BRSN in 2021
  and 2022 (code order flips against Census's 2018 correction note) and
  70 joint-asset concept-years in 2020 whose metadata predate Census's
  2020 joint-assets note.
- TSSSAMT for 2018-2021 is gated on the download date: Census re-released
  those files on 2025-07-24 with corrected amounts under the same names.
- Captured the release notes, Census's 2018-2022 crosswalks, the 2018
  metadata and topic dictionaries, and screened all 355 user-note pages;
  the 109 naming an admitted variable are stored and attached per concept.
- release_changes.json lists every change in name, label, universe,
  codes, domain, missing codes or admission across releases.
- Census's input examples are stored as .py.txt (bytes unchanged) so the
  repository-wide black and ruff checks skip them.

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

Unit tier (107, invented pipe-delimited files only):
- design codes in 2022-2025 come from the dictionary when the API lists
  only a missing code; a value one source admits and the other does not
  refuses (owner pointers 70 versus 80);
- the TSSSAMT gate refuses 2018-2021 reads without, or before, a
  2025-07-24 download date, before any file is opened;
- ESSI_BRSN, the 2020 joint families and the 2020 disability sources are
  refused where their sources conflict; universes travel per release;
- Hypothesis: row count and order preserved, person ids stable across
  months, waves and releases, weights and replicate weights carried
  unchanged, an unverified code always refuses, and a differential test
  that the reader admits exactly what an independent restatement of the
  domain rule admits (400 examples over every numeric concept).

Artifact tier (72): the registry replays from each release's own
metadata; schema labels agree with the dictionary or carry a ruling;
every refusal has a cause in the evidence; each ruling's claim appears in
the Census note it cites; the crosswalk agrees with Census's 2018-2022
crosswalk workbooks except one recorded Census anomaly; the change ledger
holds the documented universe and label changes; user notes, captures,
derived files and the regenerated tables match their pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The variable API documents a missing code for WPFINWGT in 2022-2025
(-99900000000 in 2022, -999.00000000 in 2023-2025). The reader admitted
it as a value and never checked the weight, so a file carrying it would
have entered with a weight of -999. Every structural key and the weight
must now be present and differ from each of that release's documented
missing codes, compared as decimals. A parametrized test covers all 32
documented key and weight missing codes; on the previous reader the four
weight cases loaded without error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
metadata_notes.md now records the captures, the per-release verification
rules, every ruling and open question, and the Census anomalies found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two adversarial reviews (code, and claims against the Census evidence)
found problems; each fixed one was reproduced first.

Reader (sipp_income.py):
- Count fields on every line before parsing. With usecols pandas stops
  checking, so a short row's missing fields read as structural blanks
  and an extra field shifted values between columns. Repeated header
  names and corrupt or truncated gzip files also refuse.
- Tokens must be plain ASCII decimals, and string-stored identifiers the
  dictionary's fixed-width digits (SSUID 14, ERESIDENCEID 6, SHHADID 3,
  GVARSTR 3, GHLFSAM 2). Decimal() accepted "+11", "1.2345E4", "0_1"
  and full-width digits, which split one key into several or put the
  record value into a pandas error message.
- downloaded_on accepts a datetime.

Registry and generator:
- Blanks are refused for items whose universe is all persons or all
  households (citizenship, nativity, household status, design codes).
- 2022-2025 API labels are checked against the dictionary too (no
  conflicts); a variable one source suppresses and another documents is
  a ruled conflict (2018 EDIS8MNYN, open).
- 2018 EJSOWNRE/EJSOWNRP take label and universe verbatim from Census's
  correcting user note; the dictionary's EJSOWNRP universe read "or".
- Misprinted universes are cleared under cited rulings: 2021 ERETTYPnYN
  (empty as printed) and 2024-2025 TRET1AMT-TRET7AMT (all name source 8).
- The dictionary parser reads comma-grouped codes (TSCNTAMT1/2's
  9,999,999 special code now verifies and is labelled) and marks 2018-2020
  entries whose value list runs onto an unjoined page as unverified
  (30, 33 and 33 entries; none selected).
- Census record levels: SSI spell items and child Social Security flags
  are person-month (period monthly); ETENURE and TLIVQTR household;
  SSUID, SPANEL, SWAVE design keys; EHRESIDENCID person.
- The registry embeds its tables as one JSON string literal, so
  regeneration needs no formatter pass over megabytes of tables.
- Text corrections: 101 user notes are stored (plus 8 index pages; the
  earlier commit said 109 notes), the 2020 joint-debt treatment, the
  roster slot 15 years, the ERETANY and TSSAGESS changes, and the
  correction ruling no longer claims unchanged file names.

Tests: the evidence suite passes (77) with an independent boundary probe
of every API-dictionary difference and a record-level check; reader tests
gain layout, spelling, universe-blank, datetime and string-identifier
differential cases (full reader run pending on a loaded host).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Attempt a2 stopped on a network error while rerunning the reader tests;
these edits have not yet passed that run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 24, 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 26, 2026 12:02pm UTC

Request Review

MaxGhenis and others added 3 commits October 24, 2026 16:07
Eight design-code cases now supply zero-padded tokens and check that an
unpadded token is refused; the ninth expects the recorded Census
correction note as the label source. The notes' count of concept-years
where the API and dictionary domains differ is 64, not 72.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The birth-evidence identity check seals every source under
src/populace_dynamics except listed opt-in modules. List the two new
reader modules as post-review exclusions, in the test's expected tuple,
and in the reachability test, which asserts the historical reducer
cannot import them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Record-bearing user notes (high). Eight stored Census user notes carried
individual survey identifiers with case values. Their whole-page captures
are removed. The note inventory keeps each one's URL, title, release and
variables, marked excluded with no stored body. The capture script now
refuses those URLs before any request and refuses any note page whose
tables or text carry identifier columns or identifiers followed by
values. A test checks every stored capture with that guard, and its
failure message names only the file. The evidence pins drop the eight
bodies, and metadata_notes.md corrects its claim and its counts (93
stored notes).

CSV quoting (medium). The reader's pandas call now passes
csv.QUOTE_NONE, so its record boundaries agree with the physical-line
layout validator. A quote in an unselected field can no longer absorb
the next person-month. Adds the minimized invented regression and a
property with quotes in selected and unselected fields.

Domain rulings (medium). source_rulings.json gains 13 named
domain-conflict rulings covering all 64 API/dictionary disagreements,
each listing its members and citing both sources' locations. The
generator now refuses a disagreement without an exact, cited, settled
ruling. Every admitted domain is unchanged: only the ruling references
and their pins move.

Also adds Hypothesis properties for the vintage gate, malformed layouts,
replicate keys and the registry's source-conflict, ownership and
evidence-pin invariants, on invented inputs through the production code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
M1, the capture guard. record_bearing_note now refuses a page holding any
of these forms, and its docstring lists them:
- a column headed by an identifier name, including a heading that opens
  with one, such as "SSUID (sample unit)";
- a table row with an identifier name and, in another cell, three or
  more digits, even in a variable-definition table;
- a table row of three or more numeric cells;
- an identifier name followed by digits with only punctuation between,
  or by three or more digits within 40 characters of the same sentence,
  in the text or inside an HTML comment, which is stored though not
  shown;
- a run of nine or more digits in the text a reader sees;
- a preformatted block naming an identifier and holding a digit.
Variable-definition rows and prose may still name an identifier without
a value. Tests add 16 invented record pages, which the guard refuses
(the parent guard missed 13 of them), and 4 invented definition pages,
which it admits. Three properties cover identifier-and-value sentences,
long digit runs where rendered, and rows of three numbers. Every stored
user note still passes the guard. metadata_notes.md now states the
guard as these forms.

M2, physical records. _check_layout strips one LF and then one CR from
each line, and refuses any other carriage return in a record. The line
check and pandas therefore see the same records: QUOTE_NONE already kept
quotes from joining lines, and a lone CR can no longer split one. Tests
add the review's minimized regression (two invented rows joined by a CR
in an unselected field, which gave two person-months before), a CRLF
file that keeps every record, and a carriage return in the quote
property's alphabet. The property expects one row per LF-delimited data
line, or a refusal.

L1. The THEQ_BUS ruling's reason now describes the printed range as it
is: its upper bound is malformed, and its lower bound differs from the
API's. The registry pin of source_rulings.json is regenerated.

L2. build_registry checks that a ruling member's PDF citations name its
own release's dictionary at exactly the pages its dictionary entry
holds. The invented ruling cites its page, and the damage property adds
a wrong page and another release's PDF.

Invariants:
- No stored page shows a form record_bearing_note refuses.
- The reader yields one person-month per LF-delimited data line, or
  refuses.
- Every ruling member's PDF citations equal its dictionary pages.

Tests (invented data only): test_sipp_income 183 passed;
test_sipp_income_evidence 106 passed; test_sipp_income_refusal_properties
6 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 25, 2026 07:58
Round 4 approved 7521c6d with seven low findings and four notes. This
commit answers each.

L1, rules without a failing test. Every invented record page carried the
15-digit invented SSUID, so the digit-run rules refused each page before
the rule it was meant to test. INVENTED_RULE_TWINS pairs a refused page
with a twin that differs only in what one rule reads, and the test
requires the rule to refuse the page and admit the twin. There are
eight pairs: an identifier beside a three-digit value in a definition
row, an exact identifier heading, a preformatted identifier with a
digit, a preformatted line of three numbers, a table row of three
numbers, and short values in an attribute, a script and a comment with
markup inside it.

L2, stored content not shown. The identifier rules now also read the
page source with its tags, attributes and scripts, entities decoded, and
a run of 14 or more digits, an SSUID's length, refuses the page wherever
it appears. The nine-digit rule still skips comments, noscript blocks,
scripts, styles and tags, where tracking numbers sit; the tracking
definition pages now put a 12-digit number in comment text after a ">",
in a noscript block and in an attribute, each admitted.

L3, years beside identifier names. The 40-character and same-row rules
no longer count a year from 1900 to 2099. Admitted pages add a universe
footer without a period, a panel year, an index link titled with a year
and a definition row with a year, and a property draws identifier
sentences ending in any such year, in text, a comment or an attribute.
An identifier followed directly by digits, such as "SSUID 2022", still
refuses.

L4, numeric forms. A numeric cell now admits Census's negative-dollar
forms (-$250, $-250, the Unicode minus) and bracketed amounts, and a
preformatted line of three or more such numbers refuses. The property's
numeric text draws these forms too.

L5, claims. The docstring, metadata_notes.md and the module header now
state the rules as the code applies them. A correction to 7521c6d's
message: it said a column headed by a heading that opens with an
identifier, "such as SSUID (sample unit)", refuses. Only a cell that is
exactly an identifier name refuses as a heading; a cell that opens with
one refuses only when a cell of digits alone sits below it or a value of
three or more digits sits in its row.

L6, PDF citations. Every citation containing ".pdf", in any case, now
counts: a member's PDF citations must equal one
"{year}_dictionary.pdf#page={page}" per page of its dictionary entry.
The damage property adds a pageless own-release PDF, a pageless
other-release PDF and a release-notes PDF with a page. Each damage now
runs as its own case and must fail at its own check, by message. With
the parent check, the two pageless damages pass generation.

L7, the window. A test pins the 40-character window: 40 characters
between an identifier and a three-digit value refuse, 41 admit.

Notes. The THEQ_BUS reason now says only the malformed upper bound makes
the entry unverified, and that its lower bound also differs from the
API's; the regenerated registry changes only the source_rulings.json
pin. The reader's docstring now says quotes are literal characters, so
the line check and pandas split the same records, and only a stray
carriage return refuses.

Invariants:
- No stored page shows a form record_bearing_note refuses (all 101
  stored captures pass).
- Each guard rule alone refuses its minimal page and admits its twin.
- Every ruling member's PDF citations equal its dictionary pages, one
  citation per page.

Results:
- 17 of the 129 evidence tests fail against 7521c6d's guard.
- 16 mutants, one per guard rule or exemption, are all caught.
- Tests (invented data only): test_sipp_income 183 passed;
  test_sipp_income_evidence 129 passed;
  test_sipp_income_refusal_properties 15 passed. Tiers: unit 183,
  artifact 144.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	scripts/first_estimates_birth_evidence.py
#	tests/estimates/test_birth_evidence_artifact.py
MaxGhenis and others added 2 commits October 25, 2026 15:18
The round-5 delta review (an Opus 5.5 reviewer, at ffc4d48) approved, with
five low findings and some notes.

- L1: reading the page source refused pages that only name an identifier,
  and index links, because the 40-character window ran across attribute
  boundaries and the adjacency rule fired on link slugs such as
  "ssuid-2024". In the source, the window now holds no quote or equals
  sign as well as no period or tag, so it stays inside one attribute value
  or string, and href and src values are removed from tags before the two
  identifier rules read it. The 14-digit rule still reads them. The
  review's four pages that only name an identifier, and an anchor whose
  title is an identifier name, are now admitted definition pages, as are
  a tracking number in a link target outside noscript and a row header
  outside a definition table. A 14-digit run in a link target, such as a
  timestamp, is still refused.
- L2: the mutants that survived now fail tests. Twins pin the window and
  the adjacency rule each alone in the source, a tag and a quote as
  window limits, and the removal of link targets; record pages pin the
  year exemption's lookahead and its absence from the adjacency rule; a
  definition page with two numbers per preformatted line and a property
  over preformatted lines pin that rule's threshold and line split; and a
  row header outside a definition table pins that exemption.
- L3: twins now cover the digit-below, adjacency, nine-digit and 14-digit
  rules too, so every rule, and each limit on what the identifier rules
  read, has a refused page and an admitted twin (17 pairs). The
  40-character window in visible text is pinned by the width test, which
  has letters between.
- L4: the notes and the docstring now say which rules read the source
  ("the two rules that follow an identifier name"), that the same-row rule
  needs a cell that begins with an identifier name, and that the window
  allows no period between, and in the source no tag either.
- L5a: a first-row or header cell that names an identifier anywhere, as
  in "Person number (PNUM)", with a cell of digits alone below it, is now
  refused. The other forms the review lists (a value before its
  identifier, <br> CSV text with no identifier, div grids) stay out of
  scope, as the docstring states.
- Notes: the reader's carriage-return sentence now says a CR is allowed at
  a line's end, before its LF or last in a final line without one. The
  other notes need no change: the top-code row is the three-number rule's
  accepted cost, and the year exemption admits a 1900-2099 value after an
  identifier with words between.

Found by this fix's own mutants: the visible-text window stopped at an
angle bracket, though the text holds no tags by then, so a decoded &lt; or
&gt; between an identifier name and a value admitted the page. The window
there now stops only at a period, as the round-3 rule meant ("within 40
characters of the same sentence"); in the text and comments a tag counts
as a space, and the source reading still stops at tags. A twin pins it,
18 pairs in all, and mutant G23 puts the old limit back.

Corrections to 04a2b89's message:
- "Each guard rule alone refuses its minimal page and admits its twin"
  held for the eight pairs then present, which covered five rules and
  three readings; the digit-below, adjacency, nine-digit and 14-digit
  rules had no twin until now.
- "16 mutants, one per guard rule or exemption": the 16 had two for the
  window width and none for the digit-below rule or the row-header and
  definition-table exemptions.
- "With the parent check, the two pageless damages pass generation":
  pageless_pdf does, but pageless_other_release_pdf stops earlier on a
  file missing from the invented folder.
- "Every invented record page carried the 15-digit invented SSUID": 14 of
  the 16 did; header_after_title_row_with_dashed_values and
  person_months_without_identifier don't.
- The heading correction's "a value of three or more digits sits in its
  row" omits "that is not a year".

Checks:
- Each of the 18 twins isolates its rule or limit: disabling the rule
  admits its page, and dropping the limit refuses its twin, with the head
  refusing each page and admitting each twin.
- All 32 guard mutants fail at least one test, in Hypothesis seeds 0
  and 7, and the guard is restored after each.
- With ffc4d48's guard, 9 of the 153 tests in
  test_sipp_income_evidence.py fail, the same 9 in each of three
  Hypothesis seeds.
- Every stored capture still passes the guard (the stored-capture test).
- The three D2 test modules through the wrapper: 351 passed. By
  collection, unit 183 and artifact 168, 24 artifact tests more than at
  ffc4d48.
- black and ruff are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Master brings SM1 (#570). The merge has no conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 25, 2026 22:23
Round 6 reviewed c1a3c7b and its merge 60b67f8 and returned APPROVE,
with three low findings and four notes. This commit answers each.

- Low 1: the source window's limits admit an identifier and its value
  that sit in different attribute values or script strings, or that a
  quote or equals sign splits within one, and a value in an href or src
  value other than a 14-digit run. The docstring said the window stays
  "inside one attribute value or string", which overstated it. This
  commit takes the review's first option: the docstring, metadata_notes.md
  and the PR body now list these forms as out of scope, with a value
  before its identifier, CSV text, div grids and a body cell that names
  an identifier after other words. The review's second option, reading
  each attribute value and script string alone, would close three of the
  seven forms; the script forms would need a rule of their own, and the
  link-target form would stay.
- Low 2: nine of the review's ten mutants of the changed rules survived,
  so "every rule and each limit has a twin" overstated the tests. New
  twins pin each mark that stops the source window alone (equals sign,
  single quote, double quote), unquoted href values and src values,
  adjacency in the text reading alone, and both heading-cell rules: 26
  twins in all. Two record pages pin the heading-names rule's cell
  conditions, a header cell after the first row and a first-row cell that
  is not a header cell, and a definition page pins its limit to those
  cells.
- Low 3: a first-row or header cell that names an identifier after other
  words now also counts for the same-row rule, so a run of three or more
  digits that is not a year elsewhere in its row is refused (the review's
  K2). A twin pins it. A body cell that names an identifier after other
  words stays out of scope; the review's K1 would also refuse such
  definition tables.
- Note 1: the heading-cell rule refuses some definition and index tables.
  The docstring and the notes now say so; no stored capture is refused.
- Note 2: a tag now opens with a letter, "!" or "/", so a comparison such
  as "1 < 2" in a script no longer hides a src assignment from the source
  reading. A record page pins it, and the test comment says so.
- Note 3, corrections to c1a3c7b's message and to the notes:
  - "the other L5 forms stay out of scope, as the docstring states": only
    the PR body stated it; the docstring does from this commit;
  - "are now admitted definition pages": the parent already admitted
    tracking_number_in_link_target and row_header_outside_definition_table;
    they are new test pages, not new admissions;
  - "The 40-character window in visible text is pinned by the width test":
    the width page is also refused by the source reading, so the test
    pins the width in both readings; comment_text and
    text_window_crosses_decoded_brackets pin the text reading alone;
  - the notes said the table rules read cells without their attributes,
    but the row-header exemption reads scope. The notes now say so, and
    their unreflowed line is reflowed.
- Note 4: "PNUM &gt; 100" is now refused, as "PNUM greater than 100"
  already was. No change.

Checks:
- each of the 26 twins isolates its rule or limit
  (twin_isolation_r7.py);
- all 44 guard mutants fail a test in Hypothesis seeds 0 and 7: round 6's
  32, and 12 new ones, for the review's R1-R7, R9 and R10, the double
  quote, the same-row rule's heading cells and the tag pattern
  (mutate_guard_r7.py);
- with c1a3c7b's guard, 2 of the 165 evidence tests fail, the same 2 in
  seeds 0, 1 and 2: the script-comparison record page and the twin for
  the same-row rule's heading cells, which pin this commit's two code
  changes. The other new tests pin limits the parent already kept;
- the guard refuses none of the 101 stored captures (counts only), and
  the stored-capture test passes;
- 375 passed and 1 failed over D2's three modules and the birth-evidence
  check: 363 from D2's modules and 12 birth-evidence tests. The failure
  is the track-U assertion, which needs modules this sparse clone leaves
  out; CI runs it;
- tiers by collection: unit 183, artifact 180 (12 more artifact tests);
- black -l 79 --check, ruff check and git diff --check are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 26, 2026 04:58
Round 7 reviewed c8dfe4f and its merge 695798e and returned APPROVE,
with four low findings and four notes. This commit answers each, taking
the review's code options for Low 1 to Low 3.

- Low 1: a "<" followed by a letter, "/" or "!" in a script still opened
  a "tag" whose href and src values were removed, so a minified loop
  (i<n), a CDATA marker or "a</b" hid a src assignment from the source
  reading. The source reading now splits out script and style elements,
  up to their end tag or the end of the page. Their contents are kept as
  they stand, and only their opening tags lose href and src values. Six
  record pages pin it: a minified comparison, CDATA, an end-tag-like
  span, an unquoted src assignment, a script without an end tag and a
  style block. A twin pins reading the contents as they stand, and a
  definition page and a twin pin the removal from opening tags.
- Low 2: the source window stops at any angle bracket, raw or decoded,
  which the docstring did not say, and the out-of-scope list left out
  angle-bracket splits and script comparisons. The identifier rules now
  also read a third thing: each quoted attribute value other than href
  and src, and each quoted string in a script, one at a time. Values are
  decoded, read with the text's window and joined by periods, so the
  window never spans two. This refuses the review's A1 and A3, S7 and
  S8, and round 6's Q3, Q4 and Q9. Eight record pages pin it, and four
  twins pin reading each value alone, the period join, the link-target
  exclusion and script strings. The docstring now says "no angle bracket
  (a tag, or a < or > decoded from an entity)", and the out-of-scope
  list now names an identifier and its value with one in a string and
  the other in script code, a script string after an unpaired quote
  mark, an identifier compared with a value in script code, and an
  attribute value that holds a raw ">".
- Low 3: the same-row rule skipped any other cell that began with an
  identifier name, which no list stated. The exclusion is dropped, so
  the review's K5 and K5b are refused. Two record pages and a twin pin
  it.
- Low 4: the review's V1 to V6, mutants of the tag pattern and of the
  same-row rule's cell conditions, survived. Pages from the review's
  fix_pages pin each: definition pages for an upper-case tag, CDATA and
  DOCTYPE tags, and an end tag, each with a link target; a record page
  for "<=" before a src outside scripts; and record pages for a
  first-row cell and a later header cell that name an identifier beside
  a value.
- Note 1: the table rules also refuse some definition and index rows
  that name an identifier in a heading cell beside a run of three or
  more digits that is not a year in its row (K12, K13). The docstring,
  the notes and the PR body now say so. No stored capture is refused.
- Note 2: the quote twins' comma is load-bearing; isolation holds, since
  G35 and G36 refuse the twins. No change.
- Note 3: "(begins or names)" was always true after the continue above
  it. With the exclusion gone, the condition is dropped.
- Note 4: the L4b page is now refused, as the new wording says. No
  change.

Found by this commit's own checks:
- the new reading also refused the attribute_source and script_source
  twins, so removing the source reading no longer admitted them. Their
  pages now use an unquoted attribute value and script code without a
  string, so that only the source reading refuses them;
- four new mutants changed no page, so four record pages pin them: an
  unquoted data-src value, decoded entities in a quoted value, values in
  an upper-case tag and strings in a script without an end tag;
- a script string with a backslash escape was not read whole. Strings
  are now read past their escapes, and a record page pins it.

Checks:
- 144 invented pages behave as expected, and no page the parent guard
  refuses is admitted at the head: over the test pages, the review's
  probes and round-6 forms, and 2 x 100,000 random invented pages
  (prescreen_r8.py);
- each of the 33 twins isolates its rule or limit
  (twin_isolation_r8.py);
- all 67 guard mutants fail a test in Hypothesis seeds 0 and 7: round
  7's 44, with G33, G42 and G44 ported to the moved code, and 23 new
  ones, for the split, the quoted-value reading, the same-row rule and
  the review's V1-V6 and V8 (mutate_guard_r8.py; V7 is now the head's
  own condition);
- with 695798e's guard, 26 of the 201 evidence tests fail, the same 26
  in seeds 0, 1 and 2: 20 of the new record pages and 6 of the new
  twins, which pin this commit's code changes. The other 10 new tests
  pin limits the parent already kept (parent_guard_r8.sh);
- on invented pages of about 1 MB (prose, script code and strings,
  unpaired quotes, escapes, attributes, a table) the guard takes 0.07 to
  0.24 s, against the parent's 0.03 to 0.22 s. A page of 0.5 MB of
  unclosed "<a" tags takes 126 s against the parent's 101 s, and 0.25 MB
  of "<script>" start tags 35 s against 34 s;
- the guard refuses none of the 101 stored captures (counts only), and
  the stored-capture test passes;
- 411 passed and 1 failed over D2's three modules and the birth-evidence
  check: 399 from D2's modules and 12 birth-evidence tests. The failure
  is the track-U assertion, which needs modules this sparse clone leaves
  out; CI runs it;
- tiers by collection: unit 183, artifact 216 (36 more artifact tests);
- black -l 79 --check, ruff check and git diff --check are clean.

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

This branch was successfully deployed

1 active deployment
Preview — 085a33b9 Deployed Oct 26, 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