Skip to content

REQ-ACCEPTANCE-004: persist Phase-6 price and post-MAIN expectation into WorldState - #441

Merged
zendev-acceptor[bot] merged 3 commits into
masterfrom
claude/issue-440-market-state-persistence
Sep 11, 2026
Merged

zendev-acceptor[bot] merged 3 commits into
masterfrom
claude/issue-440-market-state-persistence

Conversation

@zendev-author

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

Copy link
Copy Markdown
Contributor

Closes #440

Achieved outcome

The Phase-6 -> Phase-8 dispatch pipeline now persists its per-tick output into the next tick's authoritative WorldState.markets instead of relying on test-owned cross-tick memory. applyMarketStateTransition(world, context) is the explicit production boundary a caller applies between executeTick() calls to carry forward the tick's Phase-6 price and the post-Phase-8-MAIN MarketExpectationState observation (Handoff/04 sections 9 and 11). Phase-8's MAIN-pass effectiveDemand/offered/cleared aggregates are now computed unconditionally in TickContext.marketClearingAggregates, no longer gated behind the collectTelemetry toggle, so this authoritative transition never depends on the non-authoritative telemetry flag (REQ-MARKET-005 neutrality).

Tested revision

28af61e07628a9ce220ae2712b59e09daed0e001

Changed artifacts

  • src/simulation/marketStateTransition.ts (new): applyMarketStateTransition(world, context) — updates world.markets (priceByGood/expectationsByGood) for every market/good touched this tick; throws rather than inventing a market/good endpoint that doesn't already exist on WorldState.
  • src/simulation/tickOrchestrator.ts: adds TickContext.marketClearingAggregates (new MarketClearingAggregate type) and initializes it empty per tick.
  • src/simulation/phase8MainMarketClearing.ts: hoists the MAIN-pass effectiveDemand/offered/cleared aggregate computation out from behind the collectTelemetry gate so it always populates context.marketClearingAggregates; telemetry building itself is unchanged.
  • src/simulation/phase6MarketPriceFormation.ts: exports ZERO_EXPECTATION and a new parseMarketPriceKey() (inverse of marketPriceKey()) for reuse by the transition boundary.
  • src/simulation/acceptance-004-m3-golden-gate.test.ts: rewrites the MTFX-I5 multi-tick dispatch test to seed a market into WorldState and persist state via applyMarketStateTransition() between real executeTick() calls instead of getFixtureCurrentPrice/getFixtureExpectation closures; adjusts the trailing Phase-8-only negative control to run against the pre-loop initialWorldState (since the final worldState now legitimately carries the persisted price); adds a new negative-control test proving that skipping applyMarketStateTransition() between ticks leaves price and expectation observably stuck.
  • docs/spec/implementation_status.csv / docs/spec/IMPLEMENTATION_STATUS.md: updates the REQ-ACCEPTANCE-004 ledger row's PR field to this PR and appends evidence for this fix (see acceptance criterion 6). A first pass of this edit accidentally converted the whole CSV from LF to CRLF line endings (Python csv.writer's default lineterminator); a follow-up commit on this branch rewrote it with an explicit \n terminator, so the cumulative diff against master is confined to the one updated row.

Acceptance criteria

  • 1. A Phase-6 price produced in tick N becomes the authoritative LocalMarket price carried into tick N+1; the test does not provide that price through getFixtureCurrentPrice. Verified: the closure is removed; the market's starting price is seeded once into WorldState.markets, and applyMarketStateTransition() carries each tick's Phase-6 output forward.
  • 2. The informative Phase-8 MAIN observation in tick N updates canonical MarketExpectationState, and Phase 6 in tick N+1 reads that persisted lagged state without a test-owned expectation callback. Verified: getFixtureExpectation is removed; the test asserts worldState.markets.get(marketId)!.expectationsByGood.get(goodId)!.observationCount increments by exactly 1 every tick.
  • 3. Phase 8 in each tested tick settles/clears at exactly that tick's Phase-6 price. Preserved unchanged from the prior revision.
  • 4. Repricing occurs at most once per market/good per tick and remains within the configured bounds already required by MTFX-I5. Preserved unchanged (phase6InvocationCount structural proof, bounds assertions).
  • 5. A controlled negative variant with the persistence transition removed or bypassed makes the focused multi-tick regression fail. New test: "without applying the production persistence boundary between ticks, Phase 6 repeats the same stale price/expectation instead of progressing" — proves price and expectationsByGood stay stuck when applyMarketStateTransition() is never called.
  • 6. REQ-ACCEPTANCE-004 ledger status/evidence matches what is actually proven. Ledger row updated in this PR: stays IMPLEMENTED, evidence appended naming this PR and what it closes.
  • 7. Required TypeScript and retained .NET checks pass on the repairing revision. See Checks below.

Checks

Check Outcome Evidence
npm run typecheck passed clean
npm test passed 574/574 (40 files; 1 new test)
npm run build passed vite build succeeded
dotnet build --configuration Release --no-restore passed 0 warnings, 0 errors
dotnet test --configuration Release --no-build passed 45/45
python scripts/implementation_status.py --check passed after ledger update commit

Not checked

Nothing deliberately skipped for this Issue's scope.

Assumptions and unknowns

  • The MTFX-I5-adjacent trailing negative control ("Phase-8 alone falls back to the world's price") previously depended on world.markets never being updated; now that persistence is real, that assertion had to move to run against the pre-loop initialWorldState rather than the post-loop worldState (which now legitimately carries the persisted final price). This is a necessary consequence of the fix, not a weakening — the new dedicated negative-control test covers the "persistence removed" case directly.
  • This PR only wires the market/good pairs the M3 fixtures actually drive through context.marketPrices/context.marketClearingAggregates; it does not add a scheduled step that calls applyMarketStateTransition() automatically inside executeTick() or a higher-level run loop, because no such run loop exists yet in this codebase (M3 fixtures call executeTick() directly, tick by tick, from tests). Wiring an automatic per-tick caller is future run-loop work, not part of this Issue's scope.

Highest-risk area for review

applyMarketStateTransition()'s decision to throw when a touched marketId is not already present in world.markets, rather than inventing an entry. This matches the "fail rather than invent an endpoint" pattern used elsewhere in this codebase (e.g. Issue #427's settlement design), but means every market/good a Phase-6/Phase-8 fixture touches must be seeded into WorldState.markets ahead of time — worth confirming this is the right boundary before production Phase-2/3 planning replaces these fixtures.

Remaining gate

None identified for this Issue's scope.

github-actions Bot and others added 3 commits September 11, 2026 12:19
…nto WorldState

Adds applyMarketStateTransition(), the explicit production boundary a caller
applies between executeTick() calls to carry the tick's Phase-6 price and the
post-Phase-8-MAIN MarketExpectationState observation forward into the next
tick's authoritative WorldState.markets, per Handoff/04 sections 9 and 11.

Phase-8's MAIN-pass effectiveDemand/offered/cleared aggregates are now
computed unconditionally (previously gated behind collectTelemetry), stored
in a new authoritative TickContext.marketClearingAggregates field, so the
expectation transition never depends on the non-authoritative telemetry
toggle (REQ-MARKET-005).

Rewrites the MTFX-I5 multi-tick dispatch test to seed a market into
WorldState and persist state via applyMarketStateTransition() between real
executeTick() calls instead of test-owned getFixtureCurrentPrice/
getFixtureExpectation closures, and adds a negative-control test proving
that skipping the persistence step leaves price and expectation stuck.

Closes #440.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records PR #441's production persistence boundary (applyMarketStateTransition)
as the evidence closing the final MTFX-I5 orchestration gap. PR field updated
to 441; MERGE_COMMIT left empty until it merges.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The prior commit's CSV rewrite used Python csv.writer's default '\r\n'
line terminator, converting every line in implementation_status.csv from
LF to CRLF. Rewrites with an explicit '\n' terminator so the diff is
confined to the one updated REQ-ACCEPTANCE-004 row.

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

@zendev-acceptor zendev-acceptor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACCEPT — head revision 28af61e07628a9ce220ae2712b59e09daed0e001.

  1. Required checks green at head: build-and-test, typescript, policy-guard, mergeability all pass at this exact head (verified via gh pr checks 441); mergeStateStatus is CLEAN.
  2. Acceptance criteria (Issue #440), each verified against the diff:
    • AC1/AC2: applyMarketStateTransition() (new src/simulation/marketStateTransition.ts) persists Phase-6 price and post-Phase-8-MAIN MarketExpectationState into WorldState.markets; the rewritten multi-tick test seeds the market directly into WorldState and calls the real boundary between executeTick() calls — no getFixtureCurrentPrice/getFixtureExpectation closures remain.
    • AC3/AC4: same-tick price-sharing and phase6InvocationCount === tick + 1 once-per-tick assertions are unchanged in the diff (confirmed by direct read of the test file at this head).
    • AC5: new test "without applying the production persistence boundary between ticks..." runs 3 ticks without calling applyMarketStateTransition() and asserts price/expectation stay stuck — a real negative control.
    • AC6: ledger row updated, PR field points to #441, MERGE_COMMIT correctly left blank pending merge (consistent with this repo's established follow-up-PR pattern for recording merge commits).
    • AC7: see check reproduction below.
  3. Scope: diff confined to the 7 files declared in "Changed artifacts" (marketStateTransition.ts new, tickOrchestrator.ts, phase8MainMarketClearing.ts, phase6MarketPriceFormation.ts, acceptance-004-m3-golden-gate.test.ts, ledger CSV/MD) — matches Issue #440's Scope. No .github/workflows/**, AGENTS.md, or docs/zendev/** touched.
  4. No invariant/test weakening: the one relocated assertion (trailing Phase-8-alone negative control now runs against pre-loop initialWorldState) is a strengthening, not a weakening — it adds a concrete toBe(initialPrice) check alongside the original not.toBe(lastPrice) check, justified by persistence now legitimately advancing worldState.
  5. No secrets/credentials/personal data/local paths found in the diff.
  6. Handoff record: all 9 required sections present in the PR body (outcome, tested revision, changed artifacts, acceptance criteria, checks, not-checked, assumptions/unknowns, highest-risk area, remaining gate).

Independent re-execution at this head (checked out via worktree, not trusting the checks tab):

  • npm ci && npm run typecheck — clean
  • npm test — 574/574 passed (40 files)
  • npm run build — succeeded
  • dotnet restore && dotnet build --configuration Release --no-restore — 0 warnings, 0 errors
  • dotnet test --configuration Release --no-build — 45/45 passed
  • python3 scripts/implementation_status.py --check — matches

No prior verdict exists on this PR (reviews list empty) and no standing refusal from any account. Merging.

@zendev-acceptor
zendev-acceptor Bot merged commit e5197d3 into master Sep 11, 2026
6 of 9 checks passed
@zendev-acceptor
zendev-acceptor Bot deleted the claude/issue-440-market-state-persistence branch September 11, 2026 12:33
zendev-author Bot pushed a commit that referenced this pull request Sep 15, 2026
The requirement stays IMPLEMENTED -- this pull request repairs the identity of
two of its fixtures, it does not add acceptance coverage. ISSUE/PR now name the
work that last touched the row (#478 / #506); the prior pair (#268 / #441) and
every earlier contributing pull request stay named in EVIDENCE. MERGE_COMMIT is
left empty for scripts/backfill_merge_commits.py, which cannot run until this
pull request has a squash commit.

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

* REQ-ACCEPTANCE-004: trade the M3 dispatch fixtures between canonical stock owners

R235 (`ANSWERS_TO_IMPLEMENTER.md`, `CODE_RUNTIME_QA_M3_16`) calls a
`CLAN`+`GENERAL` M3 settlement endpoint implementation drift: a Clan owns a
treasury and no physical goods at all (Handoff/01 §§5.3/5.4/7), so
`resolveGoodsEndpoint()` refuses such an allocation rather than inventing a
container. PR #477 converted the seven Phase-8 fixtures in
`acceptance-req-market-005-phase8-integration.test.ts` and deliberately left the
two Phase-6/Phase-8 *dispatch* fixtures in the golden gate alone: they never
settle, so they did not throw.

They still named endpoints no production path could produce, on a synthetic
region that was not in `world.regions`. Both now draw their counterparties and
their region from a real `buildInitialWorld(baselineScenario, ...)` world via a
new `canonicalDispatchCounterparties()` helper: a `PRODUCTION_UNIT` seller
offering from the `OUTPUT` inventory that actually holds its food, against a
funded `COHORT` buyer whose single household inventory owns consumed food, in a
region whose `RegionState.controllerStateId` is non-null — so the settlement
currency and consumption-tax destination Phase 8 reads off the canonical
`RegionState` both resolve, as they would in production.

This is a fixture-identity change only. Every assertion about once-per-tick
dispatch, Phase-6/Phase-8 price sharing, bounded repricing and the persistence
boundary is unchanged in meaning and still passes.

Closes #478

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

* ledger: record PR #506 against REQ-ACCEPTANCE-004

The requirement stays IMPLEMENTED -- this pull request repairs the identity of
two of its fixtures, it does not add acceptance coverage. ISSUE/PR now name the
work that last touched the row (#478 / #506); the prior pair (#268 / #441) and
every earlier contributing pull request stay named in EVIDENCE. MERGE_COMMIT is
left empty for scripts/backfill_merge_commits.py, which cannot run until this
pull request has a squash commit.

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-ACCEPTANCE-004: Phase-6/8 dispatch proof bypasses persistent LocalMarket price/expectation state

0 participants