Skip to content

fix(gateway): open-taxonomy buyer-path — ad-hoc capabilities buyable end-to-end - #212

Open
LamaSu wants to merge 3 commits into
masterfrom
fix/open-taxonomy-buyer-path
Open

fix(gateway): open-taxonomy buyer-path — ad-hoc capabilities buyable end-to-end#212
LamaSu wants to merge 3 commits into
masterfrom
fix/open-taxonomy-buyer-path

Conversation

@LamaSu

@LamaSu LamaSu commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Makes ad-hoc capabilities (pizza, rideshare) quotable/orderable, not just discoverable.

  • submit-from-discovery: derive real price from the capability's own pricing (was a hardcoded $10) — 2565701
  • onboard-kit: ad-hoc types warn, don't hard-reject — d8fb44e
  • NL inference of ad-hoc listings + re-decompose preserves routing + publish auto-assign — 4f61040

7 upstream buyer-path bugs were already merged (#148/#168/#169/#171/#172/#173/#170); this closes the residual gaps. No settlement/kernels.ts touched, no deploy. Worklist: ai/research/HANDOFF-open-taxonomy-buyer-path.md

🤖 Generated with Claude Code

LamaSu and others added 3 commits July 6, 2026 19:22
…n pricing for ad-hoc types

POST /api/jobs/submit-from-discovery computed its quote via
getTemplate(capabilityType)?.basePricingHints?.basePrice, falling back to a
hardcoded $10 for any type without a built-in @pcc/contract-builder template
-- silently mispricing every ad-hoc-capability escrow funded through this
fast-track endpoint. This was the last unclosed piece of the open-taxonomy
buyer-path fix: negotiate/session and build/options already derive ad-hoc
pricing via getCapabilityDescriptor() (coord 6be1ebbe/5e2b3aeb, PR #148) and
buyer-listing matching already keys off the capability's own type (coord
6d33a1a5, PR #169) -- this endpoint was the one quote-computation path the
prior sessions flagged as a residual gap (MATCHING-NOTES.md #1: "the order
path must consume [the listing's own price]").

Fix mirrors the existing build.ts/negotiation.ts pattern exactly:
getCapabilityDescriptor(capabilityType) resolves template vs ad-hoc vs
missing; ad-hoc derives basePrice/currency from the capability row's own
pricingModel; missing returns a clean 404 (capability_not_buildable) instead
of silently quoting $10. Removed the now-unused direct getTemplate import
(getCapabilityDescriptor calls it internally).

Verified: negotiate-ad-hoc.test.ts (14/14), request-matching.test.ts (12/12),
onboard-analyze.test.ts (11/11, one route-integration test needs a >10s hook
timeout on this cold from-source worktree -- confirmed a pre-existing
transform-timing artifact, not a logic issue, by re-running standalone with
--hookTimeout=90000), requests.test.ts all green -- confirming bugs 1-4 from
the open-taxonomy handoff were already fixed and merged to master
(PRs #148/#168/#169) before this session started.

New test file submit-from-discovery-adhoc-pricing.test.ts (3/3 passing)
covers this fix directly. Not added to paid-job-flow.test.ts because that
file is excluded from the default vitest run for pre-existing, unrelated
reasons (vitest.config.ts: "facade rewrite changed route behavior" --
confirmed by temporarily un-excluding it: 3 job-completion/scope assertions
fail there independent of this change, the other 15/18 including this fix's
behavior pass). vitest.config.ts and paid-job-flow.test.ts are unchanged
(diff-verified byte-for-byte reverted after the temporary investigation).

Scoped tsc --noEmit shows 500 pre-existing errors, all "Cannot find module
@pcc/*" (workspace dist/*.d.ts not built in this worktree, affects the whole
package uniformly, e.g. sse/producers.ts) plus pre-existing TS7006
implicit-any on lines untouched by this diff. Zero new errors introduced.

Did not touch: settlement contracts, kernels.ts, onboard-kit's separate
VALID_CAPABILITY_TYPES allow-list (already flagged in MATCHING-NOTES.md as
an adjacent, out-of-scope gap for a different owner), NL-inference of ad-hoc
types, or /requests/:id/decompose re-templating -- all explicitly out of
scope per the prior sessions' own gap analysis.

fixer-taxonomy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, don't reject)

VALID_CAPABILITY_TYPES was a hard allow-list: an operator onboarding a
well-formed but non-built-in capability (wood-fired-pizza, rideshare-driver,
pizza.order) failed config validation with a blocking error, even though PCC
is an open taxonomy — buyers reach any registered type by its own type string
+ pricing model (the whole point of the buyer-path fixes). Flagged as the
adjacent onboard-kit gap in MATCHING-NOTES.md #5.

Reframe the set as RECOGNIZED_CAPABILITY_TYPES (ships a built-in template) and
classify each type three ways:
  - recognized  → info (built-in)
  - ad-hoc      → warning, ALLOWED (well-formed slug, not built-in)
  - malformed   → error, REJECTED (empty / spaces / uppercase / illegal chars)
A well-formed slug is /^[a-z0-9]+([.-][a-z0-9]+)*$/ — matches built-ins,
hyphenated ad-hoc types, and dotted namespaced types (pizza.order). Same
three-way logic applied to profile.capabilityType.

The ad-hoc check is severity:"warning" with passed:false — this codebase
derives `warnings` from failed checks (checks.filter(c => !c.passed &&
severity==="warning")) while `valid` keys only off severity:"error", matching
the existing tier-2-camera warning convention. So an ad-hoc type is allowed
AND surfaced as a warning, never blocking.

Test contract updated to the open taxonomy: the old "invalid-type" (a
well-formed slug) is no longer an error — replaced with a genuinely malformed
type ("Not A Real Type!") + empty-string cases for the error path, plus new
tests that a well-formed ad-hoc type (wood-fired-pizza) and a dotted type
(pizza.order) validate with a warning, and that a built-in stays warning-free.

Verified: 14/14 validate.test.ts green (solo run, cache-cleared, exit 0);
cross-checked against the real source via a standalone esbuild bundle
(wood-fired-pizza → valid=true, errs=0, ad-hoc warning present; malformed →
error). onboard-kit is a leaf package not imported by the gateway, so this is
operator-onboarding-config validation only — zero buyer-path runtime change.

fixer-taxonomy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g on re-decompose, auto-assign on publish

Closes the three remaining open-taxonomy buyer-path gaps documented in
MATCHING-NOTES.md (#2, #3, #4) so a buyer request for an ad-hoc capability
flows all the way from free text to a funded, operator-targeted job.

2. NL inference of ad-hoc types (request-matcher.ts: inferListingFromText).
   A request with NO capabilityType used to always fall to the generic
   custom_product template — "I want a wood-fired pizza" never reached the
   pizzeria's listing. Now, on the no-LLM path (after agentic decompose
   declines), a deterministic pass infers ONE registered listing from the
   description, keyed off each capability's own type slug + name tokens (no
   allow-list). Conservative by construction: minimum distinctive-token
   score, must be strictly stronger than the runner-up of a different type
   (no ambiguous ties), skips stopwords, and is kill-switchable via
   PCC_NL_INFER_ADHOC_DISABLED=1. On any doubt it declines and the composite
   template path runs unchanged — it never hijacks a real multi-step request.
   An inferred match stays status="decomposed" (unpublished) because inference
   is a guess the buyer confirms; the response carries `inferredMatch` so the
   routing is transparent.

3. Re-decompose preserves direct-match routing (routes/requests.ts). A request
   routed to one listing carries a lone node with kernelId + capabilityId.
   POST /:id/decompose used to re-run the NL/template decomposer and drop that
   routing, stranding the buyer from their chosen operator. Now it detects the
   direct-match node, re-resolves the SAME listing via matchListings (picking
   up any price change), recovers the original quantity from the stored cost,
   and rebuilds the direct-match node. Only falls back to composite if the
   listing was removed. Composite requests re-decompose unchanged.

4. Publish auto-assign (routes/requests.ts). POST /:id/publish minted open
   bounties for every node and left assignment manual — but a direct-match
   node already knows its operator. Now direct-match nodes (those carrying
   kernelId) auto-assign straight to the operator (kernel.operatorAddress,
   falling back to kernelId), status→"assigned", skipping the bounty auction;
   generic template nodes still publish as open bounties. Response adds
   `autoAssigned` / `autoAssignedCount` alongside the existing bounty fields.

Tests: new request-adhoc-routing-extras.test.ts (8/8) covers all three —
inference routes pizza/rideshare, does NOT hijack the animatronic-robot
composite, honors the kill switch; re-decompose keeps kernel+capability+qty
(and re-templates composites unchanged); publish auto-assigns the operator and
still mints bounties for composite nodes. request-matching.test.ts unchanged
(12/12, no regression — the inference change leaves composite routing intact).
Agentic decompose forced off in the new suite so the deterministic path is
what's under test. 20/20 green (exit 0).

Verification limits: gateway `tsc --noEmit` not re-run (workspace @pcc/*
dist/*.d.ts absent in this worktree → ~500 pre-existing module-resolution
errors that swamp signal, same limitation the prior buyer-path commits noted);
runtime + behavior verified by the green vitest suites (esbuild transform
compiles the touched files). RoutedCapabilityNode + inferListingFromText are
properly exported. No settlement/contracts/kernels.ts touched; no deploy.

fixer-taxonomy

Co-Authored-By: Claude Opus 4.8 <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.

1 participant