Skip to content

REQ-CONFIG-004: key cohort population genesis reconciliation by typed region identity - #471

Merged
zendev-acceptor[bot] merged 2 commits into
masterfrom
claude/issue-452-population-region-identity
Sep 14, 2026
Merged

zendev-acceptor[bot] merged 2 commits into
masterfrom
claude/issue-452-population-region-identity

Conversation

@zendev-author

@zendev-author zendev-author Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Closes #452

Achieved outcome

Cohort opening population is now reconciled by canonical cohort owner plus typed Region identity, so a POPULATION_ENDOWMENT record naming the wrong Region can no longer pass genesis reconciliation on an unchanged owner, source key and amount. Before this change the expected side keyed population on POP:<owner> alone and the actual side rebuilt the same owner-only key, so the Region the record carried typed and required was never read; the comparison proved who held the population but not where it was. sourceSeedKey remains provenance only, as Handoff/03 section 20 requires.

Tested revision

a8349336310508cbb6896d082893cb89f08923b4 (branch head; every check below was re-run at this revision after the ledger commit).

Changed artifacts

git diff --name-only origin/master...HEAD:

  • src/simulation/genesisReconciliation.ts — population key on both sides becomes POP:<owner>:<region>. The expected side reads the record's own typed regionId; the actual side resolves the cohort's Region through the existing regionIdByRegionKey registry map from CohortSeed.regionKey (the same canonical cohort/region relation and the same lookup the goods projection already uses), hoisted above the population block so both cohort projections share one resolution. Header contract comment updated to state the population key. No behavior outside population reconciliation changes.
  • src/simulation/genesisReconciliation.test.ts — one new negative control plus a totalPopulation helper used by it.
  • docs/spec/implementation_status.csv — REQ-CONFIG-004 row updated in place (one row per requirement): ISSUE 451 → 452, PR 469 → 471, MERGE_COMMIT cleared (unknowable before merge; scripts/backfill_merge_commits.py fills it), EVIDENCE extended to name this pull request alongside the prior contributing ones. STATUS stays PARTIAL.
  • docs/spec/IMPLEMENTATION_STATUS.md — regenerated by python scripts/implementation_status.py, never hand-edited.

Acceptance criteria

  • 1. POPULATION_ENDOWMENT reconciliation verifies canonical cohort owner and typed Region identity, not only sourceSeedKey. Both sides key on POP:${serializeOwner(...)}:${serializeRegion(...)}. Neither side reads sourceSeedKey (population reconciliation stopped doing so in PR REQ-CONFIG-004: attribute cohort opening money/goods/population to the cohort, not its Clan #459; this adds the location half).
  • 2. A wrong-region negative control fails while source key, owner, amount, and aggregate population remain unchanged. genesisReconciliation.test.ts → "fails when a cohort's population record names another region while owner, source key and total are unchanged". It asserts the unmodified genesis reconciles, asserts the relabelled record's owner, sourceSeedKey and amount are identical to the original, asserts total recorded population is unchanged, and only then asserts success === false with details.category === "POPULATION". Sensitivity was proved by stashing genesisReconciliation.ts back to its master content with the test in place: exactly that test fails (1 failed | 29 passed); restoring the repair returns 30/30.
  • 3. Correct baseline genesis continues to pass within canonical population/reconciliation tolerance. The new test's first assertion is that the unmodified baseline-multistate-v1 ledger reconciles true; the full suite is green (590/590), and no positive test regressed.
  • 4. REQ-CONFIG-004 / REQ-MARKET-005: cohort opening stocks are mis-owned by Clan in genesis ledger #448's cohort-owner repair and this Region-identity repair share one canonical population-stock key/model. REQ-CONFIG-004 / REQ-MARKET-005: cohort opening stocks are mis-owned by Clan in genesis ledger #448 landed as PR REQ-CONFIG-004: attribute cohort opening money/goods/population to the cohort, not its Clan #459 (11bd421), which made population owner-bound as POP:${serializeOwner(...)}. This change extends that same key with a region segment and reuses the same serializeOwner/serializeRegion helpers and the same region registry lookup the goods path uses — no parallel convention was introduced.
  • 5. docs/spec/implementation_status.csv does not claim CONFIG-004 is fully proved until both owner and Region identity gaps are repaired and evidenced. The row stays PARTIAL and states plainly that sibling defect REQ-CONFIG-004: preserve ProductionUnit GOOD_ENDOWMENT inventory-bucket identity #455 (ProductionUnit GOOD_ENDOWMENT INPUT/OUTPUT/INVESTMENT bucket identity) is still open, so the requirement is not yet provable at full typed-identity granularity.
  • 6. Required TypeScript and retained .NET gates remain green. See below.

Checks

Check Outcome Evidence
npm ci passed 0 vulnerabilities
npm run typecheck passed tsc --noEmit, no output
npm test passed 590 tests in 40 files (1 new)
npm run build passed dist/canonical.js built
dotnet restore passed both projects restored
dotnet build --configuration Release passed 0 Warning(s), 0 Error(s)
dotnet test --configuration Release passed 45 passed, 0 failed (REQ-MIGRATION-003 maintained)
python scripts/implementation_status.py --check passed generated table matches 29 ledger rows over 49 registry rows
python scripts/status_lint.py --self 471 --base master passed 29 ledger rows agree with the merged pull requests
python scripts/policy_guard.py --base master passed run before the ledger commit, over the 2 source files; no policy path and no product/policy bundling in this diff

Outcome is exactly one of passed, failed, not_run, unavailable.

Not checked

  • The negative control's sensitivity was proved by reverting genesisReconciliation.ts only; I did not separately neutralize the region segment alone, so the test is proved sensitive to this whole repair rather than to the region segment in isolation. Residual risk: low — the owner half of the key already existed on master, so the only behavior the test can be reacting to is the added region segment.
  • The forge-run required checks (typescript, build-and-test, policy-guard, mergeability) had not reported at the time this body was written; the outcomes above are from local runs at the tested revision.
  • No run of the docs/ viewer or Pages build: this diff touches neither.

Assumptions and unknowns

  • Fact: POPULATION_ENDOWMENT declares regionId: RegionId as required in src/domain/genesisLedger.ts, and buildInitialWorld step 8 is the only site that emits one, always from idMap.regionIds.get(cohortSeed.regionKey).
  • Fact: Handoff/03 section 20 declares regionId?: RegionId on GenesisRecord and states that sourceSeedKey is provenance only and must never substitute for typed stock identity.
  • Assumption: section 20 spells the "reconcile by owner + region" rule out explicitly for ProductionUnit-owned GOOD_ENDOWMENT (HANDOFF-REPAIR-017) but not separately for POPULATION_ENDOWMENT; I read the typed-identity principle as general, consistent with how PRs REQ-CONFIG-004: key good genesis reconciliation by region identity #467 and REQ-CONFIG-004: key resource genesis reconciliation by typed region/good identity #469 treated goods and resources. If the researcher intends population to be deliberately Region-blind, this repair is wrong and the correct remedy is a spec question, not a code revert.
  • Assumption: because both sides derive the Region from the same CohortSeed.regionKey, a production divergence is not reachable from seed data alone today; the defect this closes is that a ledger record carrying a wrong Region was unobservable. That is exactly what the Issue describes, and it is why the negative control mutates the record rather than the seed.
  • Unknown: whether a cohort whose regionKey does not resolve in the region registry is possible after validateScenarioContent (which rejects a cohort with invalid regionKey). If it were, the actual side keys it as NO_REGION and reconciliation fails loudly rather than silently — the safe direction.

Highest-risk area for review

src/simulation/genesisReconciliation.ts lines around the cohort loop: cohortRegionId was hoisted from the goods block to above the population block. Confirm the goods projection still resolves the identical value (it is the same expression, now computed once) and that no other consumer of that variable was reordered.

Remaining gate

No mandatory work remains for Issue #452. REQ-CONFIG-004 deliberately stays PARTIAL: sibling Issue #455 (ProductionUnit GOOD_ENDOWMENT INPUT/OUTPUT/INVESTMENT bucket identity) is open and out of this Issue's scope by its own Non-goals, and #455's own scope likewise excludes this repair. Merging is the ACCEPTOR's decision; this run neither approves nor merges.

🤖 Generated with Claude Code

github-actions Bot and others added 2 commits September 14, 2026 21:15
… region identity

POPULATION_ENDOWMENT records carry the cohort's Region typed, but genesis
reconciliation keyed cohort population by owner alone. A record naming the wrong
Region therefore still reconciled, because the comparison never read the typed
location it carried.

Key both sides of the population comparison by cohort owner plus Region: the
expected side from the record's own `regionId`, the actual side from the cohort's
Region resolved through the region registry, so `sourceSeedKey` stays provenance
only (Handoff/03 section 20). Add a negative control that changes only
`POPULATION_ENDOWMENT.regionId` while owner, source key, amount and aggregate
population are unchanged, and proves reconciliation fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… PR #471

Keeps REQ-CONFIG-004 PARTIAL: #455 (ProductionUnit GOOD_ENDOWMENT
INPUT/OUTPUT/INVESTMENT bucket identity) is still open, so the requirement is
not yet provable at full typed-identity granularity. MERGE_COMMIT stays empty;
scripts/backfill_merge_commits.py fills it after this pull request merges.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zendev-author

zendev-author Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

AUTHOR note: one superseded policy-guard failure in the rollup

The check rollup shows two policy-guard entries at head a8349336310508cbb6896d082893cb89f08923b4, one FAILURE and one SUCCESS. Recording what happened so the older entry is not read as an unresolved gate:

  • 21:16:38Z — policy-guard failed. Its scope-guard step refused the diff because this pull request was opened with a placeholder body while I obtained its number for the ledger row (AUTHOR_RUNBOOK.md §7: "Open the pull request first if you do not know the number, then add the row on the same branch"), so the body did not yet name docs/spec/implementation_status.csv or docs/spec/IMPLEMENTATION_STATUS.md.
  • 21:17:35Z — after the full handoff body was written naming every path in git diff --name-only origin/master...HEAD, policy-guard passed on the same head, with no new commit, which is exactly the remedy the runbook describes.

No commit changed between the two runs. Current state: mergeable: MERGEABLE, mergeStateStatus: CLEAN, and typescript, build-and-test, policy-guard and mergeability are all SUCCESS at this head. The failing entry is stale, not a standing gate — I am reporting it rather than omitting it, and I am not claiming it away.

@zendev-acceptor

Copy link
Copy Markdown
Contributor

ACCEPTOR verdict: ACCEPT

Judged head revision: a8349336310508cbb6896d082893cb89f08923b4 (a834933), branch claude/issue-452-population-region-identity, linked Issue #452.

1. Every required check is measured green at the head revision

Read from the checks tab at a834933, not from the pull request body:

Check State at a834933
mergeability success — "merges cleanly into the base branch", written 21:16:49Z, after master's current head c2f1fd8 (20:57:02Z). Independently confirmed: git rev-list --count pr471..origin/master = 0, so the branch is neither conflicting nor behind, and its green checks were measured against the base that would actually land.
typescript success
build-and-test success
policy-guard success

mergeable: MERGEABLE, mergeStateStatus: CLEAN, reviewDecision empty — no standing CHANGES_REQUESTED from any account, including this one.

On the two policy-guard entries at this head. The AUTHOR's note is accurate and I verified it rather than taking its word: the failure run started 21:16:38Z and completed 21:16:47Z; the success run started 21:17:35Z and completed 21:17:43Z. The success is the later run at the same head with no intervening commit, which is what the rollup, the combined status (state: success) and mergeStateStatus: CLEAN all reflect. A scope_guard refusal of a placeholder body, cured by writing the body and re-running, is the documented remedy, not a standing gate. Reporting it rather than omitting it was the right call.

2. Every acceptance criterion is met, with the evidence I observed

  1. Population reconciliation verifies cohort owner and typed Region. src/simulation/genesisReconciliation.ts:134 (expected side) and :261 (actual side) both key on POP:${serializeOwner(...)}:${serializeRegion(...)}. The expected side reads the record's own typed regionId; the actual side resolves the cohort's Region through regionIdByRegionKey.get(cohort.seed.regionKey) — the canonical cohort/region relation, not seed-key text. Neither side reads sourceSeedKey, so it stays provenance only as Handoff/03 §20 requires.

  2. Wrong-region negative control fails while source key, owner, amount and aggregate are unchanged. genesisReconciliation.test.ts:858 — "fails when a cohort's population record names another region while owner, source key and total are unchanged". It asserts the unmodified genesis reconciles true, asserts owner/sourceSeedKey/amount are identical to the original, asserts totalPopulation is unchanged, and only then asserts success === false with category === "POPULATION". It cannot pass on a quantity or owner change.

    I verified sensitivity myself, and to a tighter standard than the body claims. The "Not checked" section states the test was proved sensitive to the whole repair rather than to the region segment in isolation. I neutralized only the region segment — reverting both keys to POP:${serializeOwner(...)} and changing nothing else — and got exactly 1 failed | 29 passed in that file, the failure being this test at line 899. So the control is sensitive to the region segment alone, and to nothing else in the diff. The residual risk the AUTHOR flagged as low is in fact closed; the honest flagging is noted.

  3. Correct baseline still passes. The test's first assertion is that unmodified baseline-multistate-v1 reconciles true; the full suite is green at 590/590 with no positive test regressed (589 → 590, +1 new).

  4. One population-stock key shared with REQ-CONFIG-004 / REQ-MARKET-005: cohort opening stocks are mis-owned by Clan in genesis ledger #448/REQ-CONFIG-004: attribute cohort opening money/goods/population to the cohort, not its Clan #459's owner repair. The change extends the existing POP:${serializeOwner(...)} key with a region segment and reuses the same serializeOwner/serializeRegion helpers and the same regionIdByRegionKey lookup the goods path already uses. No parallel convention.

  5. The ledger does not overclaim. docs/spec/implementation_status.csv keeps REQ-CONFIG-004 at PARTIAL and states plainly that sibling defect REQ-CONFIG-004: preserve ProductionUnit GOOD_ENDOWMENT inventory-bucket identity #455 (ProductionUnit GOOD_ENDOWMENT INPUT/OUTPUT/INVESTMENT bucket identity) is still open. python scripts/implementation_status.py --check passes: the generated table matches 29 ledger rows over 49 registry rows, so IMPLEMENTATION_STATUS.md is generated, not hand-edited. The cleared MERGE_COMMIT is the established convention on this row (scripts/backfill_merge_commits.py fills it post-merge, as ledger: record the merge commits of requirements that landed #468 and ledger: record the merge commits of requirements that landed #470 did).

  6. Both runtimes green under my own execution — see below.

Independent re-execution at a834933 (the runbook's expensive gate, run because a diff can otherwise make its own check green):

npm ci             passed   0 vulnerabilities
npm run typecheck  passed   tsc --noEmit, no output
npm test           passed   590 passed (590), 40 files
npm run build      passed   dist/canonical.js built
dotnet build -c Release  passed   0 Warning(s), 0 Error(s)
dotnet test  -c Release  passed   45 passed, 0 failed  (REQ-MIGRATION-003 maintained)

The claimed requirement ID is implemented, not merely mentioned: REQ-CONFIG-004's population identity gap is closed in code and proved by a control that fails without it.

3. The diff is confined to the declared scope

Four files, every one named in the handoff and every one inside Issue #452's Scope:

  • src/simulation/genesisReconciliation.ts (+10/−5)
  • src/simulation/genesisReconciliation.test.ts (+56/−0)
  • docs/spec/implementation_status.csv (+1/−1)
  • docs/spec/IMPLEMENTATION_STATUS.md (+1/−1, generated)

No path under .github/workflows/**, AGENTS.md, docs/zendev/** or docs/spec/mirror/**. No product/policy bundling, and no policy change to hand to a human. The Issue's Non-goals are respected: no population amount, demographic mechanic, Clan derivation or phase order is touched.

The highest-risk area the body nominated — hoisting cohortRegionId above the population block — checks out. regionIdByRegionKey is built at line 164, well before the cohort loop at line 261; the goods projection at line 282 now reads the identical expression computed once at line 264 instead of at line 285. Same value, one evaluation, no other consumer reordered.

4. No invariant and no test was weakened

The test file is +56/−0: nothing removed, no it.skip, no assertion loosened. Discovered count rises 589 → 590. No money-, stock-, negative-stock- or negative-balance-conservation test is touched, and the change tightens reconciliation rather than relaxing it — a record that previously passed with the wrong Region now fails.

5. No secret, credential, personal data or local machine path

policy-guard owns this gate and is green; reading the diff myself surfaced nothing of that shape either.

6. The handoff record is complete

All nine required elements are present, and the evidence separates measured from assumed rather than blurring them: the "Not checked" section names the sensitivity limitation, the un-reported forge checks at writing time, and the un-run Pages build; the assumptions section marks the "population is not deliberately Region-blind" reading as an assumption and names the right remedy if it is wrong (a spec question, not a revert). That is the distinction this gate exists to judge, and it is drawn honestly.

Note on the open sibling

REQ-CONFIG-004 correctly remains PARTIAL. #455 is out of this Issue's scope by its own Non-goals and stays open; nothing here closes it, and nothing here claims to.

Merging at a8349336310508cbb6896d082893cb89f08923b4 and deleting the branch.

@zendev-acceptor
zendev-acceptor Bot merged commit d3c3541 into master Sep 14, 2026
8 of 9 checks passed
@zendev-acceptor
zendev-acceptor Bot deleted the claude/issue-452-population-region-identity branch September 14, 2026 21:24
zendev-author Bot pushed a commit that referenced this pull request Sep 14, 2026
Every typed-identity defect named by the row's own PARTIAL rationale is now
closed: cohort owner identity (#459), capital-good identity (#461),
GOOD_ENDOWMENT region identity (#467), RESOURCE_ENDOWMENT typed region/good
identity (#469), POPULATION_ENDOWMENT region identity (#471) and, with this
pull request, ProductionUnit inventory-bucket identity (#455). No further
REQ-CONFIG-004 Issue is open.

MERGE_COMMIT is left blank for scripts/backfill_merge_commits.py, which fills
it once the squash commit exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zendev-acceptor Bot pushed a commit that referenced this pull request Sep 14, 2026
…ucket (#473)

* REQ-CONFIG-004: key ProductionUnit genesis goods by typed inventory bucket

Handoff/03 section 20 (spec changelog HANDOFF-REPAIR-017) makes the
ProductionUnit inventory bucket part of canonical stock identity: opening
goods must reconcile by ProductionUnit + region + inventoryBucket + goodId,
not by a unit-wide aggregate, and sourceSeedKey is provenance only.

Before this change the three inventories were emitted as three GOOD_ENDOWMENT
records distinguished only by their sourceSeedKey text, and both sides of
reconciliation summed them into one owner+region+good bucket. A same-quantity
INPUT<->OUTPUT<->INVESTMENT relocation therefore reconciled cleanly. The
baseline scenario makes this reachable rather than theoretical: the iron mine
holds `good:iron` as both INPUT (80) and OUTPUT (300).

- genesisLedger.ts: add the InventoryBucket type and split GOOD_ENDOWMENT into
  a ProductionUnit-owned member that requires inventoryBucket and a member for
  every other owner that may not carry one. A Cohort holds one
  householdInventory and a State one publicInventory, neither of which is an
  INPUT/OUTPUT/INVESTMENT container, so the requirement and the prohibition are
  both structural rather than a runtime check.
- worldState.ts: emit INPUT / OUTPUT / INVESTMENT for the three ProductionUnit
  opening inventories.
- genesisReconciliation.ts: build one goodStockKey(owner, region, bucket, good)
  used by the expected and both actual projections. The reported diagnostic key
  now names the bucket, so a failing stock distinguishes INPUT from OUTPUT from
  INVESTMENT.

Proving tests (genesisReconciliation.test.ts): a cross-bucket split that holds
the owner+region+good aggregate exactly constant, asserted before the failure
assertion; a bucket relabel on the iron mine's INPUT iron, which the unit's
OUTPUT iron already covers; and a positive test that each opening inventory is
recorded under its own bucket, asserting all three buckets appear so it is not
vacuous. Confirmed sensitive by collapsing the bucket key segment: exactly the
two new negative controls fail (2 failed | 31 passed) and restoring returns
33/33.

Closes #455

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

* ledger: record REQ-CONFIG-004 as IMPLEMENTED via PR #473

Every typed-identity defect named by the row's own PARTIAL rationale is now
closed: cohort owner identity (#459), capital-good identity (#461),
GOOD_ENDOWMENT region identity (#467), RESOURCE_ENDOWMENT typed region/good
identity (#469), POPULATION_ENDOWMENT region identity (#471) and, with this
pull request, ProductionUnit inventory-bucket identity (#455). No further
REQ-CONFIG-004 Issue is open.

MERGE_COMMIT is left blank for scripts/backfill_merge_commits.py, which fills
it once the squash commit exists.

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

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

REQ-CONFIG-004: population genesis reconciliation drops typed region identity

0 participants