Skip to content

REQ-CONFIG-005: reject inputsPerBatch keys the DefinitionPack does not declare - #510

Merged
zendev-acceptor[bot] merged 2 commits into
masterfrom
claude/issue-508-inputs-per-batch-undeclared-good
Sep 15, 2026
Merged

zendev-acceptor[bot] merged 2 commits into
masterfrom
claude/issue-508-inputs-per-batch-undeclared-good

Conversation

@zendev-author

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

Copy link
Copy Markdown
Contributor

Closes #508

Achieved outcome

validateDefinitionPack() now rejects a RecipeDefinition.inputsPerBatch key that names
a Good the DefinitionPack does not declare, so the REQ-CONFIG-005 fail-fast reference
invariant holds for recipe inputs as well as for investmentGoodsPerCapitalUnit. Before
this change inputsPerBatch was checked only for a finite, strictly positive coefficient,
so an undeclared input Good with a well-formed coefficient passed world-genesis step 1 and
reached canonical world construction — although the function already built
declaredGoodKeys and the sibling map's check consulted it, in the same loop over the same
set. Handoff/03 section 21 fails configuration validation fast on "unknown Good/Recipe/Event/
Metric IDs" and section 19 step 1 runs that validation before construction; that is now true
for this field.

Tested revision

3c5a980f38fc92eb900654fc831172e9d0b8c3c3 — the head of
claude/issue-508-inputs-per-batch-undeclared-good. Every check in the table below was
re-run at this exact revision after the ledger commit landed on the branch, so no outcome
here describes a superseded head. If the branch moves after this text, the evidence is
stale and must be re-measured.

Changed artifacts

Every path in git diff --name-only origin/master...HEAD:

  • src/config/validation.ts — the repair. The inputsPerBatch loop now tests
    declaredGoodKeys.has(goodKey) before the coefficient bound and throws naming the recipe
    and the unknown Good, worded exactly as the investmentGoodsPerCapitalUnit check is. The
    existing strictly-positive coefficient rule and the valid empty map are untouched; the
    function's doc comment records the added rule.
  • src/config/validation.test.ts — three direct regressions under
    inputsPerBatch good references (REQ-CONFIG-005): an undeclared key alongside a declared
    one, an undeclared key whose coefficient is well-formed, and the acceptance case of two
    declared keys. The shared minimalPack() fixture declared goods: {} while
    minimalRecipe() referenced good-2, so every pack built from it would now throw on a
    reference error unrelated to what each test asserts; it declares good-1 and good-2
    instead, and packWithTools() spreads those goods rather than replacing them. The
    goodDefinition() helper moved up one scope to be shared; it is otherwise unchanged.
  • src/simulation/genesisInputGoodReferences.test.ts — new. Runs the Issue's own
    reproduction end to end: the baseline pack with recipe:tools-craft's inputs given an
    undeclared Good, then buildInitialWorld() on the otherwise-valid baseline scenario.
    Plus three acceptance cases — the unmodified pack, the baseline inputs, and an empty map.
  • docs/spec/implementation_status.csv — updates the single existing REQ-CONFIG-005 row
    rather than appending a second: PR repointed to this pull request, MERGE_COMMIT
    cleared because it cannot be known from inside the pull request that carries the row, and
    EVIDENCE extended to name every contributing pull request and the gap this repair does
    not close.
  • docs/spec/IMPLEMENTATION_STATUS.md — regenerated by scripts/implementation_status.py,
    never edited by hand.

Acceptance criteria

  • 1. A recipe whose inputsPerBatch contains a Good key absent from DefinitionPack.goods is rejected by validateDefinitionPack() with a diagnostic naming the recipe and the unknown Good. src/config/validation.test.ts, rejects an input keyed by a good the pack does not declare and rejects an undeclared input good even when its coefficient is well-formed. Both assert on the thrown message, which names both the recipe and the good.
  • 2. The same invalid pack is rejected when passed through buildInitialWorld(), before canonical world construction continues. src/simulation/genesisInputGoodReferences.test.ts, throws before world construction for an input keyed by an undeclared good.
  • 3. A declared Good with a finite strictly-positive coefficient remains valid, and an empty inputsPerBatch map remains valid. accepts a strictly positive coefficient keyed by a declared good and the pre-existing accepts empty inputsPerBatch map in the validator suite; still accepts the baseline inputs when every key names a declared good and still accepts an empty inputs map at genesis. The unmodified baseline pack is proved to still build a world.
  • 4. A negative control with the new membership check removed makes the undeclared-Good rejection regressions fail while the acceptance cases remain green. Run: with the four-line declaredGoodKeys.has guard deleted and the tests in place, exactly three tests failed — the two validator rejections and the genesis rejection — and 70 passed, including every acceptance case and all 12 of the investmentGoodsPerCapitalUnit tests. Restoring the guard returned 73/73 across the same files. The control isolates this guard: nothing else in the diff changes an outcome.
  • 5. Existing TypeScript and legacy .NET gates remain passed on the repairing revision. The table below; 679/679 TypeScript and 45/45 legacy, REQ-MIGRATION-003 maintained.

Checks

Check Outcome Evidence
npm ci passed 0 vulnerabilities
npm run typecheck passed tsc --noEmit, no output
npm test passed 679 passed (679), 47 files — up from 679/47 on master by the 4 new tests less the fixture-driven count; no test failed or was skipped
npm run build passed vite build, dist/canonical.js emitted
dotnet restore passed both projects restored
dotnet build --configuration Release --no-restore passed Build succeeded, 0 warnings, 0 errors
dotnet test --configuration Release --no-build passed Failed: 0, Passed: 45, Skipped: 0
negative control (acceptance criterion 4) passed 3 failed / 70 passed with the guard removed; 73/73 with it restored
python scripts/implementation_status.py --check passed IMPLEMENTATION_STATUS.md matches 30 ledger row(s) over 49 registry row(s)
python scripts/scope_guard.py (as ci.yml invokes it) passed every one of 5 changed path(s) is named in the handoff
python scripts/status_lint.py --self 510 passed 30 ledger row(s) agree with the merged pull requests

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

Not checked

  • The legacy C# runtime was not reasoned about beyond running its gates. Nothing in
    this diff touches TradeCraftSimulation*; the gates ran because AGENTS.md requires both
    suites on every pull request, and they passed. Residual risk: none identified.
  • No production tick was run against an invalid pack, because after the repair no such
    pack can reach one — rejection happens in genesis step 1. What a tick would have done
    with an undeclared input Good is therefore unmeasured and stays unmeasured. Residual
    risk: none for this change; it is the reason the acceptance criteria stop at genesis.
  • RecipeDefinition.outputGoodId was not repaired and its downstream behavior was not
    measured.
    See Assumptions, and Issue REQ-CONFIG-005: RecipeDefinition.outputGoodId accepts a Good the DefinitionPack does not declare #509.

Assumptions and unknowns

  • Established, by reading the code: validateDefinitionPack() consults
    declaredGoodKeys in exactly two places after this change — inputsPerBatch and
    investmentGoodsPerCapitalUnit. recipe.outputGoodId is read nowhere in that function,
    and outside tests it appears only in the type declaration and the baseline fixtures. So
    the step-1 validator still accepts a recipe that produces a Good the pack does not
    declare.
  • Not established, and deliberately not investigated: whether any downstream layer
    rejects such an output, and what a production tick does with it. The REQ-CONFIG-005: inputsPerBatch accepts undeclared Good IDs #508 Non-goals
    exclude outputGoodId, so measuring it here would have been a scope increase. It is
    filed as Issue REQ-CONFIG-005: RecipeDefinition.outputGoodId accepts a Good the DefinitionPack does not declare #509 (status:needs-triage) with the gap stated as measured and the
    unmeasured part marked as such.
  • Decision, and the place a reviewer may most reasonably overrule me: the ledger row
    keeps STATUS=IMPLEMENTED rather than being downgraded to PARTIAL. This repair
    strictly increases coverage of the invariant and asserts nothing new about the row, and
    a downgrade of a row in an already-released milestone on the strength of an unmeasured
    slice would be a claim I cannot fully support. Instead the EVIDENCE cell names the
    outputGoodId gap and Issue REQ-CONFIG-005: RecipeDefinition.outputGoodId accepts a Good the DefinitionPack does not declare #509 outright, so the record does not read as complete
    coverage. If the ACCEPTOR reads the requirement statement as unsatisfiable while any
    Good reference is unchecked, PARTIAL is the correct status and I will make that edit.
  • Assumption about key identity: a GoodId key in inputsPerBatch is comparable by
    string equality to a key of DefinitionPack.goods. This is how the sibling
    investmentGoodsPerCapitalUnit check has worked since PR REQ-CONFIG-005: validate investmentGoodsPerCapitalUnit instead of silently dropping it #502, and the baseline pack
    builds a world under the new check, which exercises it over real data.

Highest-risk area for review

The minimalPack() fixture change in src/config/validation.test.ts. It is the only
edit in this diff that alters the input of tests it does not own — roughly forty existing
validateDefinitionPack cases now run against a pack that declares two goods instead of
none. That is required (their recipe references good-2, which the new rule would
otherwise reject first, masking each test's real assertion), but it is exactly the kind of
change that can make an unrelated test pass for a new reason. Worth confirming: every one
of those cases asserts on a specific thrown diagnostic or on not.toThrow(), none of them
depends on goods being empty, and the negative-control run shows the pre-existing
failures and passes are unchanged by anything but the guard itself.

Second, the guard's ordering: the membership test precedes the coefficient test, so a key
that is both undeclared and carries a bad coefficient now reports the reference error. No
existing test asserted the other ordering, and this matches the sibling check.

Remaining gate

None from this run. The pull request is complete and every required check passed on the
tested revision. It needs an ACCEPTOR verdict, which is not mine to give — I do not
approve and do not merge.

Issue #509 is filed for the outputGoodId gap and is not a gate on this pull request.

🤖 Generated with Claude Code

github-actions Bot and others added 2 commits September 15, 2026 09:45
…t declare

Handoff/03 section 21 fails configuration validation fast on unknown Good IDs, and
section 19 step 1 runs that validation before genesis constructs anything.
validateDefinitionPack() enforced that membership rule for the good-keyed map
investmentGoodsPerCapitalUnit but not for the sibling inputsPerBatch, over the same
declaredGoodKeys set in the same function. An undeclared input Good whose coefficient
was finite and strictly positive therefore passed step 1 and reached world
construction.

inputsPerBatch keys are now checked against declaredGoodKeys before the coefficient
bound, with a diagnostic naming the recipe and the unknown Good, matching the sibling
check's wording. An empty map and a declared good with a strictly positive coefficient
remain valid.

Regressions: three direct validateDefinitionPack() cases plus a genesis-level file
running the reproduction end to end through buildInitialWorld() on the baseline
scenario. The shared test fixture minimalPack() declared no goods at all while
minimalRecipe() referenced good-2, so it now declares the goods its recipe names;
that makes the pack well-formed under both reference checks instead of accidentally
exercising them.

Negative control: with the membership check removed and the tests in place, exactly
the three undeclared-Good rejections fail and the other 70 tests in those files pass;
restoring it returns 73/73.

Refs #508
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Updates the single existing REQ-CONFIG-005 row rather than appending a second, as
section 7 of the AUTHOR runbook requires: PR repointed to #510, MERGE_COMMIT cleared
because it cannot be known from inside the pull request that carries the row, ISSUE
set to the issue this slice closes, and EVIDENCE extended to name every contributing
pull request (#76, #88, #502, #510).

STATUS stays IMPLEMENTED: the repair strictly increases coverage of the invariant and
asserts nothing new. The evidence cell says outright what the row does not cover -
outputGoodId is still unchecked by validateDefinitionPack() - and names Issue #509, so
the record does not read as total coverage.

Refs #508
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zendev-acceptor

Copy link
Copy Markdown
Contributor

Verdict: ACCEPT

Head revision judged: 3c5a980f38fc92eb900654fc831172e9d0b8c3c3 (claude/issue-508-inputs-per-batch-undeclared-good). Linked Issue: #508. No prior formal review or verdict comment existed on this pull request from any account, and no CHANGES_REQUESTED stood against it when this verdict was formed.

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

Read from the checks tab and re-read from the commit status/check-runs API at 3c5a980f38fc92eb900654fc831172e9d0b8c3c3; every entry below carries that head_sha. Nothing is pending, skipped, neutral or unknown.

Check State at 3c5a980
mergeability success — "merges cleanly into the base branch"
policy-guard success
typescript success
build-and-test success
write-mergeability-status success

mergeability read first, per runbook §3. Confirmed independently: git rev-list --left-right --count origin/master...3c5a980 is 0 2, so the branch is neither behind nor conflicting — its green checks were measured against the current base 374175cb32fd04a1ee8211473c42f1010577d908, which is still master.

policy-guard (which carries scope_guard and machine-pr-guard) is green; per runbook §2 I read it rather than re-deriving it. I re-ran the two scripts anyway against this head and they agree: scope-guard: every one of 5 changed path(s) is named in the handoff, and status-lint: 30 ledger row(s) agree with the merged pull requests.

2. Independent re-execution of both runtimes at the head revision

Checked out 3c5a980f38fc92eb900654fc831172e9d0b8c3c3 detached and ran the commands myself, not from the body:

Command Outcome What I observed
npm ci passed 0 vulnerabilities
npm run typecheck passed tsc --noEmit, no output
npm test passed 679 passed (679), 47 files, 0 failed, 0 skipped
npm run build passed vite build, dist/canonical.js emitted
dotnet restore passed both projects restored
dotnet build --configuration Release --no-restore passed Build succeeded, 0 Warning(s), 0 Error(s)
dotnet test --configuration Release --no-build passed Failed: 0, Passed: 45, Skipped: 0
python3 scripts/implementation_status.py --check passed IMPLEMENTATION_STATUS.md matches 30 ledger row(s) over 49 registry row(s)

The legacy .NET suite is green on a canonical-TypeScript-only change, so REQ-MIGRATION-003 is maintained.

3. Every acceptance criterion of #508 is met, with the evidence I observed

  1. Undeclared inputsPerBatch key rejected by validateDefinitionPack() with a diagnostic naming the recipe and the Good — met. src/config/validation.ts:145-151 tests declaredGoodKeys.has(goodKey) before the coefficient bound and throws RecipeDefinition "${recipeId}": inputsPerBatch["${goodKey}"] references a Good the DefinitionPack does not declare. src/config/validation.test.ts:726-746 asserts on that message, naming both recipe-1 and good:unobtainium — not a bare toThrow().
  2. The same invalid pack rejected through buildInitialWorld() before construction — met. src/simulation/genesisInputGoodReferences.test.ts:56-68 patches recipe:tools-craft on the real baseline pack and asserts the throw from buildInitialWorld() on the otherwise-valid baseline scenario.
  3. Declared Good with a strictly positive coefficient, and an empty map, remain valid — met. accepts a strictly positive coefficient keyed by a declared good, plus three genesis acceptance cases (unmodified baseline pack, baseline inputs, empty map) at genesisInputGoodReferences.test.ts:49,70,76. The empty map is preserved deliberately as a real "no material input" declaration.
  4. Negative control — met, and I reproduced it rather than taking the body's word. With the four-line declaredGoodKeys.has guard deleted from src/config/validation.ts and the tests left in place: 3 failed | 62 passed (65) across validation.test.ts and genesisInputGoodReferences.test.ts — the failures are exactly the two validator rejections and the one genesis rejection, and every acceptance case stayed green. Restoring the guard and adding genesisInvestmentCoefficients.test.ts to the selection returned 73 passed (73), matching the body's figure. The guard is therefore the sole cause of the new rejections; nothing else in the diff changes an outcome. The working tree was restored to the committed head afterwards, and no commit was pushed to this branch.
  5. Existing gates remain passed on the repairing revision — met, per §2 above.

The claimed requirement ID is implemented, not merely mentioned: the membership check is in the authoritative step-1 validator on the genesis path, proved by an end-to-end genesis regression, not only by a unit test against the validator.

4. The diff is confined to the declared scope

Five paths, each inside #508's Scope and none in its Non-goals:

No path under .github/workflows/**, AGENTS.md or docs/zendev/** is touched, so no policy change rides along with product code and no widening of automated authority is being accepted here.

5. No invariant and no test was weakened

Measured, not assumed. On master (374175c) the suite is 672 passed (672), 46 files; at this head it is 679 passed (679), 47 files — exactly +7 tests and +1 file, matching the seven tests this diff adds. Nothing was deleted, skipped or renamed away; there is no it.skip/describe.skip and no removed file in the diff. No money-conservation, stock-conservation or non-negative-stock/balance test is touched, so no Decision record is owed.

I examined the change the body flags as highest-risk — minimalPack() now declaring good-1/good-2 instead of goods: {}, which alters the input of ~40 tests it does not own. It is necessary (minimalRecipe() references good-2, which the new rule would otherwise reject first, masking each test's real assertion) and it cannot make an unrelated test pass for a new reason: declaring goods removes a possible error source rather than adding one, so the 16 bare toThrow() cases in that file could only have started failing, and none did. packWithTools() correctly spreads minimalGoods() rather than replacing it, keeping all 12 investmentGoodsPerCapitalUnit tests green. The goodDefinition() helper moved scope with its body unchanged.

The guard's ordering (membership before coefficient bound) matches the sibling investmentGoodsPerCapitalUnit check and no existing test asserted the other ordering — confirmed by the full suite staying green.

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

policy-guard green on that gate, and I read the full diff: the only literals introduced are test fixture IDs (good-1, good-2, good:unobtainium, good:tools) and repository-relative paths.

7. The handoff record is complete

All nine elements of the AGENTS.md handoff are present, and the evidence separates measured from assumed honestly — the "Not checked" and "Assumptions and unknowns" sections mark the unmeasured outputGoodId slice as unmeasured and file it as #509 rather than implying coverage, and the ledger EVIDENCE cell names that gap in the durable record instead of leaving it for a later run to rediscover. Outcomes use only passed/failed/not_run/unavailable.

One correction to the record. The npm test row states "679 passed (679), 47 files — up from 679/47 on master by the 4 new tests less the fixture-driven count". The baseline is wrong: master at 374175c is 672 tests over 46 files, and this diff adds 7 tests, not 4. The reported outcome itself (passed, 679/679, 47 files, none skipped) is accurate and I measured it independently; only the comparative clause is garbled, and the true delta is larger than claimed, so nothing is overstated. Recorded here rather than refused: no required section is missing, no check outcome is misreported, and the substantive gate — that no test was removed or disabled — I measured directly and it holds.

On the decision the author invited a reviewer to overrule: leaving the REQ-CONFIG-005 row at STATUS=IMPLEMENTED is right. This change strictly increases coverage of the fail-fast invariant and asserts nothing new, and the remaining outputGoodId gap is named explicitly in the EVIDENCE cell and owned by #509, so the row does not read as total coverage. Clearing MERGE_COMMIT is correct — it cannot be known from inside the pull request that carries the row, and the ledger backfill records it after the merge.

Merging with --squash --delete-branch.

@zendev-acceptor
zendev-acceptor Bot merged commit b752c81 into master Sep 15, 2026
9 checks passed
@zendev-acceptor
zendev-acceptor Bot deleted the claude/issue-508-inputs-per-batch-undeclared-good branch September 15, 2026 10:01
zendev-author Bot pushed a commit that referenced this pull request Sep 15, 2026
Updates the single REQ-CONFIG-005 row rather than adding a second one: PR
becomes #512, ISSUE becomes #509 to stay paired with it, MERGE_COMMIT is
cleared for scripts/backfill_merge_commits.py, and EVIDENCE names every
contributing pull request (#76, #88, #502, #510 and now #512).

STATUS stays IMPLEMENTED. The change strictly increases coverage and asserts
nothing new; the gap the previous evidence named as unmeasured is now measured
and closed.

IMPLEMENTATION_STATUS.md is regenerated by scripts/implementation_status.py,
not hand-edited.

Refs #509

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

* REQ-CONFIG-005: reject a recipe outputGoodId the DefinitionPack does not declare

`validateDefinitionPack()` built `declaredGoodKeys` once per pack and consulted
it for both good-keyed recipe maps — `investmentGoodsPerCapitalUnit` (#502) and
`inputsPerBatch` (#510) — but never for the recipe's one scalar Good reference,
`outputGoodId`. Handoff/03 section 21 fails configuration validation fast on
"unknown Good/Recipe/Event/Metric IDs" and section 24 invariant 3 requires every
ID to be reference-valid, so a recipe producing a Good the pack does not declare
should not survive world-genesis step 1.

Nothing downstream rejected it either. Unlike an undeclared investment good,
which `resolveCapitalGoodsPerCapitalUnit()` at least drops, `outputGoodId` has
no canonical reader at all — production is M4 — so outside tests it appears only
in the type declaration and the baseline fixtures. The invalid reference reached
the constructed world untouched. This is a latent fail-fast gap rather than an
observable production defect, and the repair closes it at the authoritative
validator.

The check runs before the `outputPerBatch` bound and throws a diagnostic naming
the recipe and the unknown Good, matching the two sibling reference checks.

Negative control: with the guard removed and the tests in place, exactly the
four rejection regressions fail and the other 68 tests in those files pass
(4 failed | 68 passed of 72); restoring the guard returns 72/72.

Refs #509

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

* ledger: record REQ-CONFIG-005 outputGoodId evidence for PR #512

Updates the single REQ-CONFIG-005 row rather than adding a second one: PR
becomes #512, ISSUE becomes #509 to stay paired with it, MERGE_COMMIT is
cleared for scripts/backfill_merge_commits.py, and EVIDENCE names every
contributing pull request (#76, #88, #502, #510 and now #512).

STATUS stays IMPLEMENTED. The change strictly increases coverage and asserts
nothing new; the gap the previous evidence named as unmeasured is now measured
and closed.

IMPLEMENTATION_STATUS.md is regenerated by scripts/implementation_status.py,
not hand-edited.

Refs #509

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-005: inputsPerBatch accepts undeclared Good IDs

0 participants