Repository navigation
fix(gateway): the legacy decomposer never matches on invented terms (board N23) - #439
Merged
Merged
Conversation
…board N23) toMatched used to default any term a capability did not declare: tiers to [0, 1], currency to "USDC", and price to 0 (through capPrice's fallback). So the planner could propose, and show a buyer, terms no provider declared. matchableTerms is now the gate: - tiers must be a non-empty set drawn from 0..3, kept as a sorted set; - pricing must name a currency and a plain decimal headline (baseCost, else minimum, the same headline capPrice reads) that is non-zero. createMatcher skips any capability that fails the gate, and toMatched takes only the validated terms. capPrice and the v1 digest are unchanged, so a declared capability matches and digests exactly as before. A sonnet implementer wrote this to the lane's spec. The lane reviewed it and reran the evidence: - tests: 31 new; gateway suite 2958 passed, 6 skipped (the capture suites do not load without a built @pcc/verifier dist); - mutations: 7 of 9 killed. The 2 survivors are an equivalent pair: re-deriving price or currency inside toMatched from a cap the gate has already validated. Their real guard is the gate, whose mutants all die. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
marked this pull request as ready for review
September 30, 2026 19:16
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…ites exactly its value (#439 follow-up, astra 130) Astra 130 left 439-B open (MEDIUM) and found a regression in its guard. - 439-B: isSafeTwoDecimalHeadline checked only the integer part, so "9007199254740991.01" and "….02" both passed, both parsed to 9007199254740991, and both entered the digest as "9007199254740991.00". - The regression: leading zeros counted toward the digit limit, so "00000000000000001" (the value 1) was refused. The guard now states the property directly: the v1 digest writes Number(headline).toFixed(2), and a headline is accepted only when that is exactly the two-decimal value it spells (leading zeros stripped). This also refuses any other value a double cannot hold, such as "99999999999999.99", which the new property test found, and anything toFixed writes in exponential notation. Reproduced first: the 3 new tests fail at 3a7721e. Decomposer and digest suites: 64/64. Full gateway suite: 3022 passed, 6 skipped. tsc is clean. Mutants 3/3 killed: the equality dropped, zeros counted, and the integer-only check restored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Board row N23 (composition): the legacy decomposer's defaults. The steward routed this to composition in the reply to #3550.
The legacy decomposer's
toMatched(packages/gateway/src/services/agentic-decomposer.ts, lines 235-240 at master) defaulted every term a capability did not declare:[0, 1];"USDC";capPrice's fallback.So the planner could propose, and show a buyer, a price, a currency and evidence tiers that no provider declared. The accept path was never at risk: R10 (#355, on the stack) re-reads live rows and fails closed on unpriced or tierless rows. The falsehood was in what the planner displayed.
This is the sibling of #437. #437 stops the heartbeat from WRITING invented terms; this PR stops the planner from MATCHING on them.
The change
matchableTerms(cap)(exported) is the gate.baseCost, elseminimum: the same onecapPricereads.no-declared-tiers,invalid-tiers,no-declared-pricing,invalid-pricingorzero-price.createMatcherskips any capability that fails the gate. Which matchable candidate wins is unchanged.toMatchedtakes only the validated terms, with no fallbacks.capPrice, which the stacked R10 imports for digest parity;matched-capability-digest.ts;Tests
agentic-decomposer-no-defaults.test.tsagentic-decomposer-digest.test.ts(unchanged; its fixtures already declare their terms)The
captureandcapture-3dsuites fail to load in a worktree without a built@pcc/verifierdist. They are untouched, and CI builds first. No existing fixture needed a change.Mutations: 7 of 9 killed, each read from its counts. Killed:
The 2 survivors are an equivalent pair: they restore the price or the currency default inside
toMatched.toMatchedis reached only with terms already validated for that same capability, so re-derivingcapPrice(cap)orcap.pricing?.currency ?? "USDC"there yields identical values. Their real guard is the gate, and every mutant of the gate is killed.How it was built: a sonnet implementer wrote it to composition's spec. Composition reviewed it and reran the tests and the mutation set itself.
Review
🤖 Generated with Claude Code