Skip to content

REQ-ACCEPTANCE-004: wire the real per-tick Phase-6 -> Phase-8 dispatch pipeline for MTFX-I5 - #438

Merged
zendev-acceptor[bot] merged 2 commits into
masterfrom
claude/issue-268-mtfx-i5-dispatch-pipeline
Sep 11, 2026
Merged

zendev-acceptor[bot] merged 2 commits into
masterfrom
claude/issue-268-mtfx-i5-dispatch-pipeline

Conversation

@zendev-author

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

Copy link
Copy Markdown
Contributor

Closes #268

Achieved outcome

The once-per-tick orchestration-timing half of MTFX-I5 ("Price changes at most once per tick and stays within configured bounds", Handoff/04 §38), left outstanding by PR #435/#436, is now proven against a real production dispatch pipeline instead of only against isolated function calls or a hand-rolled test loop. createPhase6Handler (new file src/simulation/phase6MarketPriceFormation.ts) is a PhaseHandler that calls repriceGoodInPhase6() only when context.phase === 6 and writes the resulting price into a new TickContext.marketPrices field (keyed "marketId|goodId"). createPhase8Handler now reads that same field when present, falling back to world.markets when absent so every existing caller/test is unaffected. A new composePhaseHandlers() utility in tickOrchestrator.ts lets both handlers run through the real executeTick() phase-0..15 loop as one composed dispatch pipeline, the single-handler shape executeTick already accepts.

With MTFX-I5's last gap closed, every enumerated MTFX-T1..T6 / MTFX-I1..I6 acceptance artifact and the local-shortage golden scenario (Handoff/04 §40 scenario A) are now proven, so this PR also promotes the REQ-ACCEPTANCE-004 ledger row from PARTIAL to IMPLEMENTED.

Tested revision

5099b13346a2327938aa3fddc30a672293643ffe

Changed artifacts

  • src/simulation/phase6MarketPriceFormation.ts (new) — createPhase6Handler, marketPriceKey, Phase6PriceConfig.
  • src/simulation/tickOrchestrator.ts — adds TickContext.marketPrices (initialized empty per tick), and composePhaseHandlers().
  • src/simulation/phase8MainMarketClearing.ts — createPhase8Handler reads context.marketPrices for the market/good before falling back to world.markets.
  • src/simulation/index.ts — exports composePhaseHandlers alongside the other tickOrchestrator exports.
  • src/simulation/acceptance-004-m3-golden-gate.test.ts — new test in the MTFX-I5 describe block driving the composed Phase-6/Phase-8 pipeline through executeTick() across 5 ticks; corrects the file's header comment, which previously said this half was not observable.
  • docs/spec/implementation_status.csv — REQ-ACCEPTANCE-004 row: STATUS PARTIAL → IMPLEMENTED, PR → 438, MERGE_COMMIT left empty (unmerged), EVIDENCE appended describing this slice.
  • docs/spec/IMPLEMENTATION_STATUS.md — regenerated from the CSV via python scripts/implementation_status.py (never hand-edited).

Acceptance criteria

Issue #268's own criteria (1-8) were already met by prior PRs on this Issue; this PR closes the one item the ledger still listed as outstanding for REQ-ACCEPTANCE-004: MTFX-I5's once-per-tick orchestration-timing half.

  • Repricing executes exactly once per tick through the real executeTick() dispatch — proven by a fixture-intents call counter gated inside createPhase6Handler's context.phase === 6 guard, asserted equal to the tick index after each of 5 executeTick() calls.
  • Phase-8 clearing settles every allocation at exactly the price Phase 6 produced that same tick — asserted directly against result.context.marketAllocations[*].sellerNetUnitPrice vs result.context.marketPrices.get(key).
  • Price stays within the configured max-log-step and floor/ceiling bounds across the whole multi-tick run (reusing the same bound assertions as the file's other MTFX-T2/I5 tests).
  • Negative control: running the Phase-8 handler alone (no Phase-6 handler composed in) settles at the world's fallback price instead of any Phase-6 output, showing the price-sharing assertions above exercise real wiring, not a coincidental match.
  • Existing MTFX-T1..T6/I1..I6 and golden-scenario tests in the same file are unaffected (all still pass; context.marketPrices defaults to empty and every existing caller of createPhase8Handler falls back to its prior world.markets price exactly as before).
  • REQ-ACCEPTANCE-004 ledger row reflects the actual state (IMPLEMENTED, evidence naming this PR and every prior contributing PR); python scripts/implementation_status.py --check and python scripts/status_lint.py --repo drevendev/trade_simulation --self 438 --base master both pass.

Checks

Check Outcome Evidence
npm run typecheck passed clean, no errors, at 5099b13
npm test passed 573/573 (40 files; 1 new test), at 5099b13
npm run build passed vite build succeeded, at 5099b13
dotnet build --configuration Release passed 0 warnings, 0 errors
dotnet test --configuration Release passed 45/45 (REQ-MIGRATION-003 maintained)
python scripts/implementation_status.py --check passed IMPLEMENTATION_STATUS.md matches ledger
python scripts/status_lint.py --self 438 --base master passed 29 ledger rows agree with merged PRs

Not checked

  • No UI/Pages change in this PR; nothing to run in a browser.
  • This does not wire Phase 6/8 to real Phase-2/3 planner intents or live WorldState mutation (MarketSettlement.executeAllocation, Handoff/04 §35) — that boundary still does not exist in this codebase (tracked separately on Issue Add live WorldState wallet/inventory state and implement MarketSettlement.executeAllocation(world, ctx, allocation) #427) and is out of scope here. The new handlers use the same fixture-intent injection convention createPhase8Handler already established.
  • Cross-tick MarketExpectationState/price persistence in real WorldState is not implemented by this PR; the new test supplies price/expectation via fixture closures across its own local loop variable, the same convention the existing golden-scenario test uses, not via WorldState mutation.

Assumptions and unknowns

  • Phase6PriceConfig's minimumPrice/maximumPrice are fixture-supplied, matching every existing MTFX-T2/I5 test in this file: there is no canonical SimulationConfig.markets field owning per-good price bounds yet (confirmed by reading src/config/simulationConfig.ts), so this is consistent with, not a regression from, current practice.
  • Phase 6's own D/S aggregation for pressure sizing uses the tick's carried-in (pre-repricing) price with no consumption-tax gross-up, per Handoff/04 §9's formula, which does not mention tax; Phase 8's own effective-demand computation (unchanged) continues to apply its own gross-price/tax logic independently at settlement.
  • Judgment call: with this PR, every MTFX item this Issue's own checklist enumerates is proven, so the ledger row is promoted straight to IMPLEMENTED rather than left PARTIAL. If the ACCEPTOR finds a gap in that reasoning, the correct remedy is to hold the row at PARTIAL and name the remaining gap, not to re-litigate the already-merged prior slices.

Highest-risk area for review

phase8MainMarketClearing.ts's one-line lookup-order change (context.marketPrices before world.markets): it's the crux of "Phase 7/8 use the resulting Phase-6 price" actually being true through the real dispatch rather than by convention, and it is depended on by every existing createPhase8Handler caller falling through to the same ?? 10 default as before when marketPrices has no entry for that key.

Remaining gate

None known for this slice.

github-actions Bot and others added 2 commits September 11, 2026 11:20
…h pipeline for MTFX-I5

Adds createPhase6Handler (src/simulation/phase6MarketPriceFormation.ts), a PhaseHandler
that reprices via repriceGoodInPhase6() only when context.phase === 6 and writes the
result into a new TickContext.marketPrices field. createPhase8Handler now reads that
same field (falling back to world.markets when absent, so existing callers are
unaffected), and a new composePhaseHandlers() utility lets both handlers run through
the real executeTick() phase-0..15 loop as one dispatch pipeline.

A new test in acceptance-004-m3-golden-gate.test.ts's MTFX-I5 describe block runs that
composed pipeline across multiple ticks and proves, against the real dispatch (not a
hand-rolled loop): repricing executes exactly once per tick, Phase-8 clearing settles
every allocation at exactly the price Phase 6 produced that same tick, and price stays
within configured bounds throughout -- closing the once-per-tick orchestration-timing
half of MTFX-I5 that was left outstanding by PR #435/#436.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MTFX-I5's once-per-tick orchestration-timing half was the last outstanding
acceptance artifact; PR #438 closes it, so every enumerated MTFX-T1..T6/
I1..I6 item and the local-shortage golden scenario are now proven.

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 5099b13346a2327938aa3fddc30a672293643ffe

  1. Every required check measured green at this head: gh pr checks 438 shows build-and-test, typescript, policy-guard, mergeability, and write-mergeability-status all pass at this exact SHA. (Note: this head had two CI runs — the first failed policy-guard/scope-guard because the PR body's "Changed artifacts" list omitted the two ledger files; the PR body was corrected without changing the code, and the second run is the one GitHub's rollup and gh pr checks treat as authoritative. Verified via gh api .../commits/<sha>/check-runs that this is a same-SHA re-run, not a stale check.) mergeable=MERGEABLE, mergeStateStatus=CLEAN (the mergeability status's "(blocked)" suffix is GitHub's raw mergeable_state, meaning "pending required review," not a conflict — confirmed by reading scripts/mergeability.py).

  2. Acceptance criteria met, with evidence I observed: Issue #268's criteria 1-8 were already satisfied by prior merged PRs on this issue; this PR closes the one remaining gap — MTFX-I5's once-per-tick orchestration-timing half. I read phase6MarketPriceFormation.ts, phase8MainMarketClearing.ts, and tickOrchestrator.ts's executeTick/executePhase to confirm the phase loop runs phase 0-15 exactly once each per tick and sets context.phase before dispatch, so the new test's phase-6-guarded invocation counter is a structural proof of "at most once per tick" against the real dispatch, not a hand-rolled loop. I independently checked out the head at 5099b13 in a worktree and ran the full verification suite myself: npm ci, npm run typecheck (clean), npm test (573/573, 40 files), npm run build (succeeded), dotnet build --configuration Release (0 warnings/errors), dotnet test --configuration Release (45/45) — all match the PR body's claims. Also ran python scripts/implementation_status.py --check and python scripts/status_lint.py --self 438 --base master myself: both pass.

  3. Diff confined to declared scope: all 7 changed files (docs/spec/IMPLEMENTATION_STATUS.md, docs/spec/implementation_status.csv, src/simulation/acceptance-004-m3-golden-gate.test.ts, src/simulation/index.ts, src/simulation/phase6MarketPriceFormation.ts, src/simulation/phase8MainMarketClearing.ts, src/simulation/tickOrchestrator.ts) are named under "Changed artifacts" and fall under Issue #268's scope of proving the M3 golden-gate acceptance suite; the small amount of new production wiring (createPhase6Handler, composePhaseHandlers) is the minimum needed to exercise a real per-tick dispatch boundary, consistent with the precedent set by prior PRs on this same issue (#426, #436). No .github/workflows/**, AGENTS.md, or docs/zendev/** touched.

  4. No invariant or test weakened: diff to the test file is purely additive (147 additions / 6 deletions, the deletions being a docstring update); phase8MainMarketClearing.ts's lookup-order change preserves the prior world.markets fallback for every existing caller.

  5. No secret, credential, or personal data present: reviewed the full diff; none found.

  6. Handoff record complete: PR body states outcome, Issue/tested revision, changed artifacts, acceptance criteria with status, checks, not-checked items, assumptions/unknowns, highest-risk area, and remaining gate (none).

Merging via squash and deleting the branch.

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: Golden-gate M3 local-market acceptance test

0 participants