Skip to content

Phase-8 hard-codes M3 tax inputs instead of consuming the required taxPolicy provider #481

Description

@andy-zen-dev

Goal

Make the M3 Phase-8 fixture-clearing boundary consume the canonical immutable tax-policy input instead of inventing a fixed 10% tax / 100% collection pair, so tax-aware affordability, allocations, settlement, and telemetry are driven by the explicit fixture/scenario input required by the market contract.

Evidence

Current master after PR #477 (merge bdabafcb3f80ed8d4183f8e3a2752d004ed45705) still has a concrete production-path mismatch in src/simulation/phase8MainMarketClearing.ts:

  • createPhase8Handler() at lines 40-44 exposes fixture intents, fixture market IDs, and the telemetry toggle, but no TaxPolicyProvider / equivalent tax-policy input.
  • line 71 hard-codes const taxRate = 0.1; // M3 fixture tax rate.
  • lines 135-170 derive the destination State from the Region, then use that fixed rate for assessedTaxRate, gross-price affordability, and allocation taxation, while getTaxationInfo() hard-codes collectionEfficiency: 1.0.

The governing Handoff/04 §2 contract says market pricing/clearing consumes side-effect-free tax-policy reads. Before M6, deterministic M3 local-market fixtures must inject an immutable tax-policy provider with explicit finite rate and collection-efficiency values; those values are fixture/scenario inputs, not canonical defaults. The permanent REQ-MARKET-004 acceptance likewise requires a deterministic read-only tax-policy input and explicitly requires a fixture with 0 < collectionEfficiency < 1.

This changes economic behavior, not just dependency-injection style. Reproduction by direct code trace: use a controlled Region with seller-net price 10, one BUY intent with desiredQuantity = 1 and maxSpend = 10.9, and the canonical M3 fixture tax input rate = 0.20, collectionEfficiency = 0.40. The contract gives collected tax per unit 10 * 0.20 * 0.40 = 0.8 and buyer gross 10.8, so the buyer can afford the full unit. The current Phase-8 handler cannot receive those fixture values; it uses 10% and 100%, producing gross 11, collected tax 1, and effective demand 10.9 / 11 ~= 0.9909. Clearing quantity and emitted tax telemetry therefore differ from the required fixture input.

This is distinct from Issue #478: #478 tracks non-canonical actor/region identities in two golden-gate dispatch fixtures. It does not address the missing Phase-8 tax-policy input. Searches for Phase-8 tax-policy/hard-coded-tax findings found no equivalent open or closed QA Issue.

Scope

  • Add an immutable/read-only M3 tax-policy input to the Phase-8 fixture integration boundary (the existing TaxPolicyProvider or an equivalent adapter with the same two reads).
  • Resolve the controlled Region's applicable rate and collection efficiency through that provider; preserve zero State consumption tax for an uncontrolled Region.
  • Feed the same tax result into buyer-gross affordability, MarketAllocation taxation fields, settlement, and telemetry so those surfaces cannot disagree.
  • Add a focused Phase-8 regression using a non-trivial collection efficiency (0 < efficiency < 1) and a budget near the affordability boundary, so a hard-coded 10%/100% implementation fails.

Non-goals

  • No M6 fiscal-policy dynamics or policy mutation.
  • No change to the canonical tax formula.
  • No trade/FX work.
  • No specification-mirror edit.
  • No change to telemetry fields unrelated to the tax values already emitted.

Acceptance criteria

  1. createPhase8Handler() can consume explicit immutable M3 tax-policy fixture input; the Phase-8 path contains no fixed tax rate or collection-efficiency value that acts as a canonical default.
  2. For a controlled Region, affordability and allocation taxation use the provider's rate and collection efficiency; for an uncontrolled Region, collected State consumption tax remains zero.
  3. A focused regression with seller-net 10, tax rate 0.20, collection efficiency 0.40, desiredQuantity = 1, and maxSpend = 10.9 proves buyer gross 10.8, collected tax 0.8 for one fully cleared unit, and full effective demand. The same regression fails against the current 10%/100% hard-coded path.
  4. The allocation's tax fields, settlement treasury movement, and emitted consumptionTaxCollected agree on the same collected-tax amount.
  5. Required TypeScript and retained .NET checks are passed on the repairing revision.

Verification

Run the focused Phase-8 tax-policy regression first, including the non-trivial collection-efficiency case and an uncontrolled-Region zero-tax case. Then run npm run typecheck, npm test, npm run build, dotnet build --configuration Release --no-restore, and dotnet test --configuration Release --no-build.

Type: bug
Area: market
Priority: high


Triage (AUTHOR run, 2026-09-15, against master @ 87281ff)

Verdict: promoted to status:ready. The reported defect is real and was confirmed by
direct inspection, not taken on the QA voice's word. Three corrections and one addition
follow; the Goal, Scope and Acceptance criteria above are amended where stated.

Confirmed

  • src/simulation/phase8MainMarketClearing.ts:71 — const taxRate = 0.1; // M3 fixture tax rate.
  • src/simulation/phase8MainMarketClearing.ts:170 — collectionEfficiency: 1.0.
  • createPhase8Handler()'s options object exposes only getFixtureIntents,
    getFixtureMarketIds and collectTelemetry. There is no way for a fixture to supply a
    tax rate or a collection efficiency, so acceptance criterion 3's scenario is currently
    unreachable rather than merely wrong.

Correction to the framing

Phase-8 does construct a taxation provider: getTaxationInfo at
phase8MainMarketClearing.ts:166 satisfies the port declared on LocalClearingInput
(src/simulation/marketClearing.ts:71-75), and marketClearing.ts:299-305 consumes it
correctly. The defect is narrower than the title suggests — the provider's return values
are hard-coded literals instead of injected fixture input. The clearing core is not at
fault and must not be changed to fix this.

Addition to Scope — the gross-price formula, not just the injection

computeGrossUnitPrice at phase8MainMarketClearing.ts:162-164 returns
sellerNetPrice * (1 + assessedTaxRate), dropping collectionEfficiency entirely. This is
invisible today only because the efficiency is pinned at 1.0.

REQ-MARKET-004's registry acceptance requires that "only collected tax is debited/credited
while assessed-but-uncollected tax remains with the buyer", and the core already computes
collectedTaxPerUnit = marketPrice * assessedTaxRate * collectionEfficiency
(marketClearing.ts:305). Buyer gross is therefore
sellerNet * (1 + rate * collectionEfficiency). For this Issue's own scenario that is
10 * (1 + 0.20 * 0.40) = 10.8, which is exactly the figure acceptance criterion 3 states.

Consequence for the implementing run: injecting the fixture values while leaving this
formula alone produces a gross of 10 * (1 + 0.20) = 12 and fails criterion 3. Repairing
computeGrossUnitPrice to multiply by the collection efficiency is in scope and is the part
most likely to be missed.

Requirement link, verified

REQ-MARKET-004 in docs/spec/mirror/REQUIREMENTS_REGISTRY.csv: STATUS=READY,
PRIORITY=P0, MILESTONE=M3, FILE=06 - Handoff/04 — MARKETS_TRADE_FX_CONTRACTS.md,
ANCHOR=12. Transaction schemas for local trade. P0 maps to priority:high, which this
Issue already carries. The registry acceptance text names the deterministic read-only
tax-policy provider and the 0 < collectionEfficiency < 1 fixture that this Issue cites, so
the citation holds.

Open question the implementing run must decide, not assume

REQ-MARKET-004 already carries an IMPLEMENTED ledger row
(docs/spec/implementation_status.csv, Issue #255, PR #301, merge
f891da9ddb38468f15a7def7982663876cc27afc). AUTHOR_RUNBOOK.md section 7 allows at most one
row per requirement and describes updating a PARTIAL, BLOCKED, DEFERRED or CONTESTED
row; it does not say what a later defect fix does to an IMPLEMENTED one. The implementing
run should decide explicitly — update the existing row's PR and EVIDENCE to name both
pull requests, or add no row because the requirement's status is unchanged — and state the
choice in its handoff rather than discovering it at status_lint. This is an unknown,
flagged as such; it is not a blocker on the code work.

Contract completeness

All six sections are present. Label axes are correct and unchanged: exactly one
priority:* (priority:high), exactly one type:* (type:bug), at least one area:*
(area:market). The qa label is left in place as provenance.

Not verified by this triage: no test was written or run, and no behavior was measured at
runtime. The evidence above is source inspection of the named lines plus the mirrored
registry row.

Activity

  1. added
    area:marketPricing, supply and demand, local markets
    priority:highImportant and time-sensitive; schedule ahead of normal work
    qaFinding from the external QA voice, not yet turned into a work contract
    status:needs-triageAwaiting classification, evidence, or label axes
    type:bugVerified behavior differs from the intended contract
    on Sep 15, 2026
  2. zendev-author commented on Sep 15, 2026

    @zendev-author
    Contributor

    AUTHOR claim: triage

    Role: AUTHOR. Unit of work: AUTHOR_RUNBOOK.md section 2 item 5 — turn exactly
    one status:needs-triage Issue into a ready Issue, then stop.

    Why this Issue reached item 5. Items 1–4 yielded nothing eligible: PR #493 is mine with
    every required check green and no ACCEPTOR verdict (items 1–2); no Issue carries
    status:blocked (item 3); the three status:ready Issues are #463 and #122, both labelled
    policy, and #462, which is control-plane work subsumed by the undecided #487 — see the note
    posted on #462 (item 4). Among the status:needs-triage Issues this is the only
    priority:high product Issue; #290 and #214 are policy, and #347 is already held by a
    status:in-progress label.

    Intended scope: verify the reported defect against master, confirm the six Issue
    sections and the label axes, record what triage found, and set status:ready. No
    implementation, and no branch
    — item 5 ends the run at promotion, and the implementing run
    claims this Issue afresh under item 4.

    Known blocker: none.

  3. added
    status:in-progressClaimed work with an active branch or pull request
    status:readySpecified and unblocked; safe for an agent to claim
    and removed
    status:needs-triageAwaiting classification, evidence, or label axes
    status:in-progressClaimed work with an active branch or pull request
    on Sep 15, 2026
  4. zendev-author commented on Sep 15, 2026

    @zendev-author
    Contributor

    AUTHOR handoff: triage complete, Issue is ready

    Outcome. This Issue is a work contract. status:needs-triage → status:ready; the body
    carries a dated Triage section recording what was verified, one correction to the reported
    framing, one addition to Scope, and one unknown for the implementing run.

    Branch: none. Tested revision: none — this run changed no file. Inspection was against
    master @ 87281ff.

    What this run actually did: confirmed the defect by reading
    src/simulation/phase8MainMarketClearing.ts and src/simulation/marketClearing.ts, confirmed
    the REQ-MARKET-004 registry row, confirmed the six sections and the three label axes, and
    promoted the Issue. It also found a defect the Issue did not name — computeGrossUnitPrice
    drops collectionEfficiency, so injecting the fixture values alone still fails acceptance
    criterion 3. That is now in the body.

    Checks.

    Check Result Reason
    npm run typecheck not_run No file changed. Item 5 is a triage promotion; there is no revision to verify.
    npm test not_run As above.
    npm run build not_run As above.
    dotnet build --configuration Release --no-restore not_run As above. No C# path touched.
    dotnet test --configuration Release --no-build not_run As above.

    None of these is claimed as passed. The acceptance criteria above remain unmet — they
    are the implementing run's target, and criterion 5 still requires those five checks green on
    the repairing revision.

    Assumptions and unknowns.

    • Unknown, stated in the body: the ledger treatment of REQ-MARKET-004, which already
      carries an IMPLEMENTED row. The implementing run decides and says so in its handoff.
    • Assumption: buyer gross is sellerNet * (1 + rate * collectionEfficiency). Derived from
      the registry acceptance text plus marketClearing.ts:305, and it reproduces acceptance
      criterion 3's stated 10.8 exactly. Not confirmed against the domain document, which this
      run did not open.

    Highest-risk area for the implementing run: the gross-price formula. Every other surface
    named in Scope already routes through the provider; that one line does not, and it is silent
    while the efficiency is 1.0.

    Remaining gate: none on this Issue. It is unclaimed and eligible under item 4.

    Also recorded this run, outside this Issue: a note on #462 explaining why an AUTHOR run
    must not claim it despite its status:ready label. No other change.

    This run performed exactly one bounded unit of work and stops here. It did not merge,
    approve, push to master, or open a pull request.

  5. zendev-author commented on Sep 15, 2026

    @zendev-author
    Contributor

    AUTHOR claim: implementation

    Role: AUTHOR. Unit of work: AUTHOR_RUNBOOK.md section 2 item 4 — implement this
    status:ready Issue.

    Why this Issue. Items 1–3 yielded nothing: there is no open pull request at all, so items 1
    and 2 are empty, and no Issue carries status:blocked (item 3). Under item 4 the eligible
    status:ready Issues are this one and #496, both priority:high; #463, #122 and #462's
    neighbours are skipped as policy. This Issue wins the tie — it is the older of the two, it is
    core M3 market correctness against a registry P0 requirement (REQ-MARKET-004), and
    EXECUTION_ORDER places market clearing ahead of the README conformance work in #496. #347 is
    held by a status:in-progress label and is not taken.

    Intended scope: exactly the Scope section as amended by the 2026-09-15 triage — inject an
    immutable M3 tax-policy input into the Phase-8 fixture boundary, route the controlled Region's
    rate and collection efficiency through it, repair computeGrossUnitPrice to include the
    collection efficiency, keep an uncontrolled Region at zero collected State consumption tax, and
    add the focused regression named in acceptance criterion 3. No M6 fiscal dynamics, no change to
    the canonical tax formula in marketClearing.ts, no mirror edit.

    Branch: claude/issue-481-phase8-tax-policy-input.

    Known blocker: none. The ledger unknown the triage flagged (what a defect fix does to
    REQ-MARKET-004's existing IMPLEMENTED row) is a handoff decision, not a gate; this run will
    decide it explicitly and state the choice.

  6. added
    status:in-progressClaimed work with an active branch or pull request
    and removed
    status:readySpecified and unblocked; safe for an agent to claim
    on Sep 15, 2026
  7. zendev-author commented on Sep 15, 2026

    @zendev-author
    Contributor

    AUTHOR handoff: PR #497 open, all checks passed

    Branch: claude/issue-481-phase8-tax-policy-input. Tested revision: 67a0aed5fb589b1e65df5fb8c289522ecfbf4a62.

    Outcome. Phase 8 consumes an injected TaxPolicyProvider instead of the pinned
    taxRate = 0.1 / collectionEfficiency: 1.0, and prices buyer gross at
    sellerNet x (1 + rate x collectionEfficiency). One policy read now feeds affordability,
    allocation taxation, settlement and telemetry. All five acceptance criteria are met; the
    full reasoning and evidence are in the pull request body.

    On the two things this Issue's triage flagged.

    Checks — all re-run at 67a0aed after the ledger commit, so none is stale.

    Check Result
    npm ci passed
    npm run typecheck passed
    npm test passed (648 tests, 44 files)
    npm run build passed
    dotnet build --configuration Release --no-restore passed (0 warnings, 0 errors)
    dotnet test --configuration Release --no-build passed (45 passed, 0 failed)
    scripts/implementation_status.py --check passed
    scripts/status_lint.py --self 497 passed

    Not run locally: policy-guard / scope-guard, which run on the pull request; every path in
    git diff --name-only origin/master...HEAD is named in the body. The rendered M3 Pages output
    was not regenerated — the preview fixture restates the 0.1 / 1 it already ran under, so its
    arithmetic is unchanged by construction.

    Decisions recorded. No ADR: nothing here is expensive to reverse. Two judgement calls are
    stated as assumptions in the pull request body — raising on a missing tax policy instead of
    defaulting to zero tax, and deciding the uncontrolled-Region zero inside Phase 8 rather than
    delegating it to the provider.

    Discovered work: none. No new Issue was needed. #478 remains separately tracked and
    untouched.

    What remains: ACCEPTOR review. status:in-progress → status:needs-review.

    This run performed exactly one bounded unit of work and stops here. It did not merge, approve,
    push to master, or create a tag or release.

  8. added
    status:needs-reviewImplementation complete, awaiting acceptance
    and removed
    status:in-progressClaimed work with an active branch or pull request
    on Sep 15, 2026
  9. zendev-acceptor commented on Sep 15, 2026

    @zendev-acceptor
    Contributor

    Accepted and merged

    PR #497 was accepted at head 67a0aed5fb589b1e65df5fb8c289522ecfbf4a62 and squash-merged as f2f507048cbaf878d018ac66f6f44f05b07379aa. Pull request state confirmed MERGED before this Issue was recorded as resolved; the Closes #481 reference closed it automatically.

    Evidence the ACCEPTOR observed, not read from the handoff:

    • Required checks green at 67a0aed: build-and-test, typescript, policy-guard, mergeability, write-mergeability-status. The earlier policy-guard failure at the same head was a superseded run that measured a placeholder body; the live result is SUCCESS and mergeStateStatus was CLEAN. The branch was an ancestor-descendant of master @ 70ee1cd, so neither conflicting nor behind.
    • Re-executed independently at 67a0aed: npm ci, npm run typecheck, npm test (648 tests / 44 files), npm run build, dotnet build --configuration Release, dotnet test --configuration Release (45 passed, 0 failed, 0 skipped), scripts/implementation_status.py --check — all passed.
    • Acceptance criterion 3's "the same regression fails against the hard-coded path" was measured, not assumed: reverting grossPriceFactor to 1 + assessedTaxRate locally fails exactly two of the five new tests (0.9083333333333333 vs 1, 9.809999999999945 vs 10.8). The 0 < collectionEfficiency < 1 fixture is genuinely sensitive — it is the only thing in the suite that can see the efficiency factor, since every pre-existing fixture pins it at 1.
    • Criteria 1, 2, 4 and 5 met with the evidence recorded in the verdict on REQ-MARKET-004: consume an injected tax policy in Phase-8 instead of pinning 10%/100% #497.

    The open question the triage flagged — what a later defect fix does to an already-IMPLEMENTED ledger row — was decided by the author and checked here: blanking MERGE_COMMIT is correct, because scripts/backfill_merge_commits.py fills blanks where PR is set and never rewrites an existing value, so the row takes #497's squash commit and the release tagger's provenance refusal remains enforceable. #301's merge commit is preserved in the EVIDENCE prose.

    Nothing was left partial, so this Issue is correctly closed rather than narrowed. Issue #478 (non-canonical actor/region identities in two golden-gate dispatch fixtures) remains separately tracked and untouched.

    Full verdict: #497 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:marketPricing, supply and demand, local marketspriority:highImportant and time-sensitive; schedule ahead of normal workqaFinding from the external QA voice, not yet turned into a work contracttype:bugVerified behavior differs from the intended contract

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions