Skip to content

fix(gateway): the legacy decomposer never matches on invented terms (board N23) - #439

Merged
LamaSu merged 1 commit into
masterfrom
fix/decomposer-no-default-terms
Sep 30, 2026
Merged

LamaSu merged 1 commit into
masterfrom
fix/decomposer-no-default-terms

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 29, 2026

Copy link
Copy Markdown
Owner

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:

  • tiers to [0, 1];
  • currency to "USDC";
  • price to 0, through 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.
    • Tiers must be a non-empty set drawn from 0..3, and are kept as a sorted set.
    • Pricing must name a non-empty currency and a plain decimal headline that is non-zero. The headline is baseCost, else minimum: the same one capPrice reads.
    • Each failure has a reason: no-declared-tiers, invalid-tiers, no-declared-pricing, invalid-pricing or zero-price.
  • createMatcher skips any capability that fails the gate. Which matchable candidate wins is unchanged.
  • toMatched takes only the validated terms, with no fallbacks.
  • Unchanged:
    • capPrice, which the stacked R10 imports for digest parity;
    • matched-capability-digest.ts;
    • the matching and digests of every capability that declares its terms.

Tests

Suite Result
new agentic-decomposer-no-defaults.test.ts 31 passed
agentic-decomposer-digest.test.ts (unchanged; its fixtures already declare their terms) passed (35 with the new file)
full gateway 2958 passed, 6 skipped, 0 failed

The capture and capture-3d suites fail to load in a worktree without a built @pcc/verifier dist. They are untouched, and CI builds first. No existing fixture needed a change.

Mutations: 7 of 9 killed, each read from its counts. Killed:

  • restoring the tier default;
  • dropping the matcher's skip;
  • dropping the tier range;
  • dropping the currency check;
  • dropping the zero check;
  • dropping the headline-format check;
  • dropping the no-declared-pricing check.

The 2 survivors are an equivalent pair: they restore the price or the currency default inside toMatched. toMatched is reached only with terms already validated for that same capability, so re-deriving capPrice(cap) or cap.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

  • gateway (the decomposer's historical owner) on resume.
  • A small cross-family pack follows, if the steward wants one.

🤖 Generated with Claude Code

…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
LamaSu marked this pull request as ready for review September 30, 2026 19:16
@LamaSu
LamaSu merged commit 25d2d16 into master Sep 30, 2026
5 checks passed
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
LamaSu added a commit that referenced this pull request Oct 3, 2026
fix(gateway): close four matchableTerms gaps from astra's #439 review (#439 follow-up)
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.

1 participant