Skip to content

fix(gateway): N34, no route serves fixtures or random values as live data - #421

Draft
LamaSu wants to merge 41 commits into
fix/fiat-ramp-honestyfrom
fix/n34-no-fabricated-routes
Draft

LamaSu wants to merge 41 commits into
fix/fiat-ramp-honestyfrom
fix/n34-no-fabricated-routes

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

N34: no route serves fixtures or random values as live data

Board N34 is the server side of PX-3. Steward ruling #2498: one PCC_DEMO_ROUTES flag, and each family's owner decides later whether to wire it or retire it.

Stack: on #373 (fix/fiat-ramp-honesty @ d5be880). That PR carries isDemoRoutesOn(), which ignores the flag under NODE_ENV=production. CI suites run only on PRs based on master (#2474), so the counts below are from local runs.

What changes

With PCC_DEMO_ROUTES unset, 71 routes in 10 families answer 501 {error: "not_available", message, see}. A probe of every registered route in each family found these; the table lists them.

Family Refused Live, unchanged
logistics (retired, carrier #2922) 12/12 none. Its see pointer is GET /api/carrier/shipments/:jobId
spaces 3/3 (the random match score is drawn only in demo) none
agents conversations 2/2 none. Its see pointer is GET /api/agents/live/conversations
marketplace 11/13: classes, demand-supply, listings, orders POST /roi; GET /categories (a real taxonomy, with count: null + unavailable outside demo)
registry 2/7: attestations entities, reputation, leaderboard, summary
rewards (with certificates and treasury) 9/9 none
discover 2/3: scan, onboard (the gateway has no local scanner) generate-csd. Demo onboarding registers nothing (registered: false)
orchestrator 8/8 none
protocols (with protocol-runs, automation-status and transfer-agents) 21/21 none. The gate runs before the body is parsed
swf 1/27: epoch distribution (random contribution scores); refused before any read or write, with no demo path (economics #3315; the steward's SWF ruling, operator item 69; N46 money floor) the other 26

In demo mode, the refused routes (except SWF distribution) answer their old examples, marked mock: true, demo: true with the header x-pcc-demo: true.

Other changes:

  • Report hints. report_hint is not added to designed refusals (not_available, not_configured), so agents are no longer told to file a bug for them.
  • see pointers. Each one names a registered live route, as METHOD + path.
  • Demo batch. The demo HPLC batch (made-up samples, wallets and result hash) is seeded only in demo mode. It used to be seeded into every gateway's batch tracker at import.
  • ROI calculator. The answer names the defaulted inputs and its assumptions, and refuses a non-number with 400.
  • Agent context pack. The 44 refused rows it listed move to one "Not available on this gateway (demo only)" list, which is demoOnlyEndpoints in the JSON form.
  • Docs. CLAUDE.md and docs/AGENT_INTEGRATION.md mark each refused family and the MCP tools pcc_list_protocols, pcc_depin_stats, pcc_discover_scan and pcc_discover_onboard.

Ratchet

n34-routes-fabrication-ratchet.test.ts counts mock*/MOCK_*/fake*/FAKE_* data bindings and value Math.random() calls in routes/, with comments stripped.

  • The allowlist can only shrink.
  • Every entry names an owner and the reason it may stay. Most entries are demo-only data behind the gate.
  • A new fixture fails the test. So does an allowance above the actual count, so each gain is locked in.

Reviews (independent, per group)

  • Group a (logistics, spaces, agents), reviewer-n34-a: CHANGES_REQUIRED. routes.test.ts pinned the old 200s; fixed in 08c0f68. The report-hint finding is fixed in bea9c87, and the context pack in 967fd7f.
  • Group b (marketplace, registry, rewards), reviewer-n34-b: APPROVE_WITH_NITS. The attestation see pointer is fixed in cc78e3e, the ROI defaults in 4e649bc, and the MCP/agent-package flag is sent to aeo.
  • Group c (orchestrator, protocols, swf), reviewer-n34-c: APPROVE_WITH_NITS. It re-derived all 56 classes and killed 15/15 mutants. In the full createGateway() composition, every refused route answered 501 with zero fixture values, across path and method variants. Its wording nit is fixed in 7aaff8b, the batches finding in 8fa98ac, and the context pack in 967fd7f.

Evidence

  • gateway: pnpm test gives 3706 passed / 6 skipped / 0 failed (220 files, solo run at 799f2f8; the same at 4e649bc). An earlier run at a load average of 32 timed out one unrelated test, settlement-crank-wiring, which passed 3/3 when run alone.
  • New tests:
    • group a: 58
    • group b: rewards, marketplace and registry suites
    • group c: 104
    • discover: 13
    • batches: 3
    • context-pack availability: 4. It probes the families at test time and catches all 4 regression mutants tried.
    • ratchet
  • tsc is clean.
  • Real-gateway trace at 799f2f8: the gateway was built from this branch and booted with NODE_ENV=production, PCC_DEMO_ROUTES=true (ignored in production) and a fresh DB, with a key provisioned through POST /api/auth/provision. Results:
    • 10/10 sampled refused routes (logistics, spaces, marketplace listings, protocols, orchestrator workflows, rewards, registry attestations, agent conversations, discover scan, SWF distribute) answered 501 with a body of exactly error, message and see: no report_hint, no mock or demo mark.
    • The live routes answered: categories with count unavailable, ROI with defaulted, the registry summary with attestations unavailable, and batches [].
    • /agent-context-pack.json lists 44 demoOnlyEndpoints.
    • Transcript: returns/pcc-readmodels-work/n34-e2e-trace-20260924.txt.

For other owners (not changed here)

  • economics:
    • swfAccrue("settlement", jobId, 1000, ...) accrues a constant 1000 per released milestone (settlement.ts:204, settlement.facade.ts:692), so every SWF money figure derives from it.
    • GET /api/swf/summary reports lastDistributionAt = now when nothing was distributed, and labels the in-memory ledger as a chain balance.
    • feat(spec): economic agreements v1, an exact compiler to V-next per-unit payouts #360 rewrites the same distribute handler. Both are now an unconditional 501 (799f2f8), so whichever lands second resolves a wording-only conflict. This PR keeps the reviewed message (jobs and votes are recorded; the per-epoch score is not) and the METHOD-path see pointers.
  • refvertical: decide whether to wire or retire orchestrator and protocols. Demo mode keeps the old 200 {error: "not_found"} for unknown ids.
  • shell (PX-3): OrchestratorPage, ProtocolLibraryPage and three more pages read r.json() without checking r.ok, so a 501 shows as empty data.
  • aeo (R43): the per-tool availability flag. 42 of the 253 agent-package tools are refused outside demo, and the list has been sent on the bus.
  • operator routes: handled by fix(gateway): operator read routes stop fabricating earnings and fleet data #362 (N32), not by this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A2ZvsqsAb7jC7AC8Viisqn

Every /api/logistics/* route answered from hard-coded fixtures: providers,
shipments, space bookings, installation orders, a timeline, counts over them,
and a quote priced by a made-up formula under fixture providers' names. The
installation-step PATCH edited a fixture in memory.

One onRequest hook, encapsulated to the plugin, now answers 501 not_available
before the body is parsed unless PCC_DEMO_ROUTES=true, so nothing is read,
priced or changed. The refusal says what is not recorded and, for shipments,
points at the real carrier route. In demo mode the old answers come back
marked mock/demo, with the x-pcc-demo header.

Board N34, the server side of PX-3.

agent: implementer-alpha-n34 (pcc-readmodels c255d7dc)
…cores

/api/spaces and /api/spaces/:id answered from two hard-coded spaces (names,
street addresses, operator wallets, power, slots, ratings), and
POST /api/spaces/match scored them with Math.random(): a 70-95 matchScore with
no basis, different on every call.

One onRequest hook, encapsulated to the plugin, now answers 501 not_available
before the body is parsed unless PCC_DEMO_ROUTES=true. In demo mode the old
answers come back marked mock/demo, with the x-pcc-demo header.

Board N34, the server side of PX-3.

agent: implementer-alpha-n34 (pcc-readmodels c255d7dc)
… data

GET /api/agents/conversations and /:convId answered from two hard-coded
conversations (topics, participant agents, message counts, timestamps) and
read no agent bus.

One onRequest hook, encapsulated to the plugin, now answers 501 not_available
unless PCC_DEMO_ROUTES=true, and points at GET /api/agents/live/conversations,
which reads the gateway's agent bus. In demo mode the old answers come back
marked mock/demo, with the x-pcc-demo header. The heartbeat routes under
/api/agents/* are untouched.

routes.test.ts still asserts the fixture answers without the flag (4 tests);
it is outside this change's file scope and is left for the lane owner.

Board N34, the server side of PX-3.

agent: implementer-alpha-n34 (pcc-readmodels c255d7dc)
Board N34 (server side of PX-3), steward rulings #2498 and #2639: a
production surface never substitutes plausible mock data for real data.

Every /api/marketplace route but one answered from invented data:
equipment classes with made-up machine counts, utilization and job
values, a sine-wave price history and demand curve, supplier listings
and orders. Its writes pushed into those fixtures and wrote a telemetry
event and an audit entry for a listing or order that exists nowhere.

- SERVED-MOCK (11): GET /classes, /classes/:id, /demand-supply,
  /listings, /listings/:id, /orders, /orders/:id, and POST /listings,
  PUT /listings/:id, DELETE /listings/:id, POST /orders. Unless
  PCC_DEMO_ROUTES=true they answer 501 not_available before any fixture
  is read or changed (and before input validation, the telemetry event
  and the audit entry, so an unknown id cannot reveal which fixture ids
  exist). `see` names the real routes where they exist: capability
  types/templates/by-type, the kernel marketplace, jobs, kernels. In
  demo the old answers stay, each marked mock/demo (errors included).
- MIXED: GET /categories keeps the spec's category taxonomy; the
  per-category listing counts came from the fixture, so outside demo
  they are null and named in unavailable: ["categories[].count"].
- REAL: POST /roi is a calculator over the caller's own inputs;
  unchanged.

Tests: n34-marketplace-no-fabrication.test.ts, 30 cases (11 routes x
refused with no fixture value and x demo-marked with the old shape;
refusal before validation; unknown ids; refused writes change no
fixture and write no audit entry; the categories split; /roi identical
with the flag on and off). Mutation-checked 4/4 red: dropping the
GET /listings, POST /listings or POST /orders guard, or feeding fixture
counts into the non-demo categories answer.

agent: implementer-bravo-n34 (pcc-readmodels c255d7dc)
…real

Board N34 (server side of PX-3), steward rulings #2498 and #2639: a
production surface never substitutes plausible mock data for real data.

The ERC-8004 registry reads identity and reputation from chain, but the
attestation reads had no source: one invented attestation (validator
0xcccc..., claim hash 0xaaa111..., "FDM printing - PLA/PETG ...") was
served as registry content, in two lists, an entity's detail and the
summary counts.

- SERVED-MOCK: GET /api/registry/attestations and /attestations/:id
  answer 501 not_available unless PCC_DEMO_ROUTES=true (before the id
  lookup, so an unknown id reveals nothing). `see` names the registry
  reads that are real: /api/registry/entities/:entityId and
  /api/registry/reputation/:entityId. In demo the fixture answers stay,
  marked mock/demo (the 404 included).
- MIXED: GET /entities/:entityId keeps the chain-read entity and
  reputation; outside demo attestations is null and named in
  unavailable: ["attestations"]. GET /summary keeps the DB entity counts;
  outside demo totalAttestations and activeAttestations are null, named
  in `unavailable`, and source.attestations is null (it said "mock").
  In demo both keep the fixture values and are marked mock/demo. The 400
  and 404 answers come from real reads and stay unmarked.
- REAL, unchanged: GET /entities, /reputation/:entityId,
  /reputation/leaderboard.

Tests: n34-registry-no-fabrication.test.ts, 17 cases on a mocked chain
(no network): the attestation routes refused with no fixture value and
demo-marked; the entity detail and summary split with a seeded minted
agent; the real routes identical with the flag off and on, never
marked. routes.test.ts (summary/entities) still green. Mutation-checked
4/4 red: dropping either attestation guard, or serving the fixture
attestations in the non-demo entity detail or summary.

agent: implementer-bravo-n34 (pcc-readmodels c255d7dc)
…nt record

The 501 for GET /api/logistics/timeline listed no real route, although the
gateway does record logistics events: a carrier shipment's tracking events,
owner-scoped, at GET /api/carrier/shipments/:jobId. The shipment refusals
already pointed there; the timeline (four of its five fixture events were
shipment tracking) now does too. Summary, quote, providers, bookings and
installations keep an empty list: no real route holds their data.

Board N34, the server side of PX-3.

agent: implementer-alpha-n34 (pcc-readmodels c255d7dc)
…refuses

The spaces test showed the refusal body carries no matchScore. It did not
show the random score is never drawn. A Math.random spy now shows no draw on
the 501 path, and a draw on the demo path, so the spy is known to see one.

Board N34, the server side of PX-3.

agent: implementer-alpha-n34 (pcc-readmodels c255d7dc)
… demo

Board N34 (server side of PX-3), steward rulings #2498 and #2639: a
production surface never substitutes plausible mock data for real data.

Every route in rewards.ts answered from invented data: two reward
epochs with kernel scores and reward amounts, claims (one with a
made-up transaction hash), three soulbound capability certificates,
and a "broker-agent" treasury holding 50,000 USDC and 10.5 ETH.
POST /api/rewards/claims answered created:true and
POST /api/certificates/mint answered minted:true; neither recorded or
minted anything.

- SERVED-MOCK, all 9 routes: GET /api/rewards/epochs,
  /api/rewards/epochs/:epochId, /api/rewards/kernels/:kernelId,
  /api/rewards/claims/:claimId, /api/certificates,
  /api/certificates/:certId and /api/treasury/summary, and
  POST /api/rewards/claims and /api/certificates/mint.
- Because every route is SERVED-MOCK, the plugin is gated by one
  encapsulated onRequest hook: unless PCC_DEMO_ROUTES=true each route
  answers 501 not_available with its own "... not recorded on this
  gateway" message and see: [] (no real source exists here). The hook
  runs after the root API-key gate, so a caller without a key still
  gets 401, and before the body is parsed, so a refused write creates,
  mints and records nothing, and an unknown id reveals nothing.
- In demo the old answers stay, each marked mock/demo (the 400 and 404
  answers included).

Tests: n34-rewards-no-fabrication.test.ts, 25 cases: the 9 routes
refused with their own message and no fixture value, and in demo the
old status and shape, marked; unknown ids and filters refused;
malformed JSON on a write is 501, not 400; HEAD refused; demo 404/400
marked; the hook does not leak to the parent's routes or to sibling
plugins (marketplace's real POST /roi, a probe plugin) and an
unmatched path stays 404; with apiGate registered first, no key gets
401 and a key gets the refusal. Mutation-checked 7/7 red: gate
removed, writes ungated, reads ungated, gate moved to preHandler, gate
leaked via skip-override, demo treasury unmarked, and rewards
registered before apiGate (caught by the 401-first case).

agent: implementer-bravo-n34 (pcc-readmodels c255d7dc)
…o fix/n34-no-fabricated-routes

Group a of board row N34 (steward #2498; carrier #2922 retires logistics). All 17
handlers in logistics.ts, spaces.ts and agents.ts serve fixtures or random scores,
so each plugin answers 501 not_available unless PCC_DEMO_ROUTES, and demo answers
are marked. Implemented by implementer-alpha-n34 (5 commits); integrated here.

agent: pcc-readmodels (c255d7dc)
…) into fix/n34-no-fabricated-routes

Group b of board row N34. 29 handlers classified: SERVED-MOCK routes answer 501
not_available unless PCC_DEMO_ROUTES; mixed registry reads null their mock
fields and list them in unavailable; real reads are unchanged. Implemented by
implementer-bravo-n34 (3 commits); internal review APPROVE_WITH_NITS, nits
folded in the next commits.

agent: pcc-readmodels (c255d7dc)
#326's scoped keys

routes.test.ts expected 200 fixture conversations from /api/agents/conversations;
they now expect the honest 501 not_available (with the pointer to the recorded
conversations), and that a fixture id and an unknown id answer alike. The rewards
gate test mints its probe key with explicit scopes ([]), which #326 requires.

agent: pcc-readmodels (c255d7dc)
Every 5xx got a report_hint telling agents to report a bug (reviewer-bravo-n34).
A 501 not_available (a route with no real source, N34) and a 503
not_configured (an unconfigured provider, N48) are documented, intentional
answers, so they no longer carry it. Any other 5xx still does.

agent: pcc-readmodels (c255d7dc)
Group b's see entries were bare paths while group a's named the method. They are
now all 'GET /api/...'. The registry attestation refusal pointed at the entity
and reputation reads, which hold no attestations, so its see is empty
(reviewer-bravo-n34 nit).

agent: pcc-readmodels (c255d7dc)
…nters

bonjour-service is not a dependency, so on every deployed gateway the mDNS
import fails, and /api/discover/scan answered two example printers (Canon, HP)
as if found. /api/discover/onboard then REGISTERED one of them in the CSD
registry. Found by the N34 ratchet inventory; not in the census.

- Without mDNS: 501 not_available (discover on the operator's machine, pcc-node).
- With PCC_DEMO_ROUTES: the examples, marked mock/demo; demo onboarding returns
  the would-be device and CSD with registered:false and writes nothing.
- discover.test.ts updated from the mock expectations; 13 tests.

agent: pcc-readmodels (c255d7dc)
Steward #2498. For every file under src/routes it counts mock data declarations
(mock*/MOCK_*/fake*/FAKE_* bound to an array, object, Map or Set) and
value-producing Math.random() calls (not the toString(36) id idiom). A file may
have them only as the allowlist says, and each entry names an owner and why it
stays for now. Shrink-only: a count above its allowance fails, and so does an
allowance above the count. The detector has its own tests.

agent: pcc-readmodels (c255d7dc)
Every /api/orchestrator/{graphs,samples,claims,workflows} route answered
from one made-up lab kernel (kernel-biolab-01): a five-instrument transfer
graph, two samples with a movement history, a running workflow and its
resource claims. Nothing was read from the kernels, devices or jobs the
gateway records. GET /graphs/:kernelId answered {error:"not_found"} with
HTTP 200 for every real kernel, and POST /workflows said accepted:true and
recorded nothing.

All eight routes are SERVED-MOCK, so one encapsulated onRequest hook gates
the plugin: unless PCC_DEMO_ROUTES=true (never under NODE_ENV=production),
each answers 501 not_available before the body is parsed, with pointers to
the real routes where they exist (kernels and their devices, jobs, job
submission). In demo the old answers stay, each marked mock:true, demo:true
and x-pcc-demo: true. The hook does not reach the other /api/orchestrator/*
plugins (template directory, data-product sessions); the new test proves it,
with a skip-override control that shows the check can fail.

Board N34, the server side of PX-3.

agent: implementer-charlie-n34 (pcc-readmodels c255d7dc)
Every /api/protocols*, /api/protocol-runs*, /api/automation-status* and
/api/transfer-agents route answered from fixtures that nothing records: a
two-template "library" with invented authors and usage (runCount 7, rating
4.6), a "running" protocol run on a made-up kernel with evidence hashes and
robot transfer episodes, a fork, automation statuses and transfer agents.
The writes (create, update, publish, fork, start a run, pause, resume,
cancel, record an episode, advance a level) answered created, updated,
started or recorded and stored nothing. /validate checked templates against
a hard-coded capability list, not the kernel's.

All 21 routes are SERVED-MOCK, so one encapsulated onRequest hook gates the
plugin: unless PCC_DEMO_ROUTES=true (never under NODE_ENV=production), each
answers 501 not_available before the body is parsed, with its own message
and pointers to the real routes where they exist (jobs and job submission
for run state, a kernel's capabilities for /validate). In demo the old
answers stay, each marked mock:true, demo:true and x-pcc-demo: true. The new
test proves the hook reaches no other plugin (skip-override control
included) and that every registered route has its own refusal.

Board N34, the server side of PX-3.

agent: implementer-charlie-n34 (pcc-readmodels c255d7dc)
…de demo

POST /api/swf/epochs/:epochId/distribute scored every active participant
with Math.random() (job count, reputation, uptime, votes) and then
DISTRIBUTED the epoch on those scores: pending dividend claims with
real-looking amounts, and the epoch marked completed. Nothing records a
participant's per-epoch contribution, so the shares were set by chance.

Unless PCC_DEMO_ROUTES=true (never under NODE_ENV=production), the route
now answers 501 not_available BEFORE anything is read, scored or written,
says why (the shares would come from random numbers) and points at
GET /api/swf/epochs/:epochId; the epoch stays exactly as it was. In demo
the old distribution runs and its answers (200, and the 409 conflict) are
marked mock:true, demo:true.

It is the only route in swf.ts that draws random values or reads a
fixture; the rest read and write the SWF service's own records and are
unchanged. The new test proves that with the flag on and off.

Board N34, the server side of PX-3.

agent: implementer-charlie-n34 (pcc-readmodels c255d7dc)
…ly at live routes

Two checks the per-route tables could not make on their own:

- every route the orchestrator (8) and protocols (21) plugins register is
  refused with its own message, so a route added later, or a refusal key
  that no longer matches its route pattern, cannot fall back unnoticed;
- every `see` pointer in the refusals (orchestrator, protocols, swf)
  names a route the gateway actually registers (Fastify hasRoute on the
  kernels, jobs, job-submit, capabilities and swf plugins), so a refusal
  never sends a caller to a dead path.

Board N34, the server side of PX-3.

agent: implementer-charlie-n34 (pcc-readmodels c255d7dc)
… N34 integration

wip/n34-c @6ba90ef9 by implementer-charlie-n34, reviewed APPROVE_WITH_NITS
by reviewer-n34-c: 104 new tests, full gateway suite 3566/6/0 twice on
its base, 15/15 reviewer mutants killed. Outside demo, all 29 orchestrator
and protocol routes answer 501 not_available before the body is parsed.
SWF epoch distribution (random contribution scores) is refused per route
before any read, score or write; the other SWF routes are unchanged.

agent: pcc-readmodels (c255d7dc)
…easons)

reviewer-n34-c: the SWF distribute refusal said per-epoch contribution
DATA (jobs, reputation, activity, votes) is not recorded, but jobs and
votes are recorded; what is missing is a per-participant, per-epoch
contribution score. The message now says exactly that. The ratchet's
orchestrator, protocols and swf entries now state the demo-only reason,
and swf names economics as the owner (#360 replaces the handler).

agent: pcc-readmodels (c255d7dc)
reviewer-n34-c: services.ts seeded a demo HPLC batch (Sample Alpha, Beta
and Gamma, jobs job-010..012, made-up 0x wallets, a fake sha256 result)
into every gateway's batch tracker at import, so GET /api/batches* served
it as live data on production. It is now seeded only when
PCC_DEMO_ROUTES=true (never under NODE_ENV=production), and its runConfig
says demo: true. The batch routes are unchanged: they serve what the
tracker holds.

Tests: n34-batches-no-fabrication (3). Removing the gate fails both
negative tests.

agent: pcc-readmodels (c255d7dc)
reviewer-n34-a and reviewer-n34-c: /agent-context-pack(.json) still
described the routes that now answer 501 not_available as live
endpoints, so an agent following the pack walked into refusals. The 44
such rows (marketplace classes and demand-supply, discover scan and
onboard, protocols and protocol runs, rewards claims, certificate mint,
treasury summary, spaces, logistics, orchestrator, agent conversations)
move to one list, "Not available on this gateway (demo only)" in the
markdown and `demoOnlyEndpoints` in the JSON, rendered from a single
constant. Sections left empty are dropped.

context-pack-availability.test.ts asks the N34 route families which
routes refuse with PCC_DEMO_ROUTES unset, then checks that no available
row refuses, every demo-only row does, and both pack forms agree. It
fails on each of: a refused row put back in a table, a live route
listed as demo-only, a refused row put back in the JSON groups, and the
JSON demo list dropped (4/4).

agent: pcc-readmodels (c255d7dc)
The API reference described rewards, logistics, spaces, marketplace
listings and orders, discover scan and onboard, protocols and
orchestrator as live. Each row now says it answers 501 not_available
outside demo mode, names the live routes that remain (marketplace roi
and categories, discover generate-csd) and the live pointer where one
exists. SWF epoch distribution and the demo-only HPLC batch are noted.
The MCP tools that call only refused routes (pcc_list_protocols,
pcc_depin_stats, pcc_discover_scan, pcc_discover_onboard) are marked.

agent: pcc-readmodels (c255d7dc)
…on-numbers

reviewer-n34-b: POST /api/marketplace/roi silently filled missing inputs
with example values, so a body of {} returned a plausible 25-month
projection that read as the caller's own. The answer now carries
`inputs` (the values used), `defaulted` (which ones were examples) and
`assumptions` (30 days a month, 70% bookable, 3 months up front, a
24-month horizon). A numeric string is still used, as before. An input
that is not a number answers 400 invalid_input instead of a NaN
projection. The dashboard computes its ROI client-side and does not call
this route.

agent: pcc-readmodels (c255d7dc)
… demo path

Economics #3315 and the steward's SWF ruling (operator item 69): the fund
stays a design artifact and its money routes stay 501; distribute is on
N46's money-floor list. So POST /api/swf/epochs/:epochId/distribute
refuses whatever PCC_DEMO_ROUTES says, before any read, score or write
(the random-score demo distribution is removed, and with it swf.ts's
last Math.random calls and its ratchet entry). The refusal keeps the
reviewed wording and METHOD-path `see` pointers, now also pointing at
GET /api/swf/accruals. #360 makes the same handler an unconditional 501,
so whichever lands second resolves a wording-only conflict.

agent: pcc-readmodels (c255d7dc)
Brings the fiat-ramp round-3 fixes (one CDP snapshot, exact principals,
operation readiness, legacy routes retired, session modes, provision,
spend authority, no demo payment instructions) under #421.

agent: pcc-readmodels (c255d7dc)
LamaSu added a commit that referenced this pull request Sep 30, 2026
…bels simulation totals

Cross-family review EC2 (astra, SHIP-WITH-FIXES at 4e79258). Each finding was reproduced
at 4e79258 with a test that fails there, then fixed:

- F1 (HIGH): accepting terms, activating an equity position, recording revenue and
  distributing an epoch now answer 501 not_available before anything is read or written.
  At 4e79258 they answered [200, 409, 409, 409] and the portfolio showed deployed capital
  and earned revenue that no transfer backed. The distribute handler is #421's hunk verbatim.
- F2 (MEDIUM): lastDistributionAt is always null. A completed epoch is a simulated split whose
  endTime is its scheduled deadline, so the summary reported a future time as a distribution.
- F3 (MEDIUM): the summary says basis "simulation" and lists the three money totals as
  unavailable, so a zero reads as an empty simulation ledger, not a measured fund.
- F5 (MEDIUM): the ratchet now catches any spelling. swfAccrue may be named only at its
  definition (no import, alias or re-export), and .accrue( may appear only in routes/swf.ts.
  Behavioural tests show settlement release leaves the SWF ledger unchanged. Two comments
  that named the helper are reworded.

F4 (the dashboard's MOCK_SUMMARY page) is outside this diff; #394 retires that page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…code alone

Reviewer finding F-C (r1a): buildReportHint suppressed the report_hint for ANY
5xx carrying error "not_available" or "not_configured", regardless of status
code. An accidental `500 not_available` or `599 not_configured` — a real bug —
was silently treated as the designed N34/N48 refusal (501/503) and lost its
report_hint. Existing tests covered the intended pairs and lookalike error
strings, but not mismatched statuses.

Reproduced in report-hint.test.ts: buildReportHint({url:"/api/x", method:"GET",
statusCode:500, errorCode:"not_available"}) returned null at d3647b8. Added
assertions for all four mismatches (500/not_available, 599/not_configured,
501/not_configured, 503/not_available) — all returned null before the fix.

Fix: replaced the code-only INTENTIONAL_REFUSAL_CODES.has() check with an
INTENTIONAL_REFUSAL_STATUS map pairing each code to its one designed status
(not_available -> 501, not_configured -> 503); suppression now requires the
code AND the status to match. INTENTIONAL_REFUSAL_CODES is kept exported
(unused elsewhere, but preserves the public surface) since it wasn't the
gate itself, just documentation of which codes are involved.

agent: implementer-alpha for pcc-readmodels (c255d7dc)
…time state

Reviewer finding F-D (r1b): services.ts seeds one demo HPLC batch into the
shared BatchTracker singleton at import, only when PCC_DEMO_ROUTES was on.
The batch routes then served the tracker's contents unconditionally, so a
process that started in demo mode and later had PCC_DEMO_ROUTES turned off
kept serving the seeded fixture — the on-to-off transition was never
rechecked, only the state at seed time.

Reproduced in n34-batches-no-fabrication.test.ts: import batches.js with
demo on (seeds the fixture), confirm it's present, delete PCC_DEMO_ROUTES in
the SAME process (no re-import), then GET /api/batches and
/api/batches/by-job/job-010 — at d3647b8 both still returned the fixture
(HPLC batch, Sample Alpha/Beta/Gamma, job-010..012).

Fix: batches.ts now reads config/demo-routes.ts's isDemoRoutesOn() on every
read (GET /api/batches, /api/batches/:batchId, /api/batches/by-job/:jobId)
and filters out any batch whose runConfig.demo === true when demo mode is
currently off (BatchManifest.runConfig is always present per @pcc/spec, so
no optional-chaining edge case). A batch a caller actually creates has no
demo flag on its runConfig and stays visible regardless of demo mode — added
a test asserting this explicitly, so the fix cannot be "hide everything".
Batch writes (POST .../slots) are untouched, per the finding: they mutate
real in-memory state by design.

agent: implementer-alpha for pcc-readmodels (c255d7dc)
Reviewer finding F-B (r1a): countFabrication() only recognized bindings whose
NAME started with mock/MOCK_/fake/FAKE_ and whose INITIALIZER started with
`[`, `{`, `new Map(`, or `new Set(`. Reproduced the reviewer's three cheapest
cases directly: countFabrication("const demoRows = [{id: 1}]"),
countFabrication("const mockRows = Array.from({length: 2}, (_, i) => i)"),
and countFabrication('import rows from "./fixture.json"') all returned
{mockData: 0, random: 0} at d3647b8 — a "demo"-named binding, a
recognized-name binding with an unrecognized initializer shape (Array.from),
and a JSON import all evaded it, as did any allowlisted fixture swapped for
a differently-named one of the same count (the allowlist pinned by count
only).

Fix (n34-routes-fabrication-ratchet.test.ts):
- Binding-name matching now recognizes demo/mock/fake/sample/seed/fixture as
  a prefix OR suffix, with a case-sensitive camelCase/ALL_CAPS boundary check
  (hasFixturePrefix/hasFixtureSuffix) — deliberately NOT a bare case-insensitive
  substring match, which produced real false positives caught while iterating:
  "isDemoBatch" (a predicate, "Demo" mid-word, no prefix/suffix boundary),
  "sampler"/"currentSampler" (extra trailing letters after "Sample"),
  "downsamplePoints" ("sample" embedded with no case-transition boundary).
- Initializer matching now accepts ANY shape except a string/template literal
  (STRING_INITIALIZER_RE) — catches Array.from(...), Object.assign(...),
  structuredClone(...) — while still excluding derived-value cases the
  existing tests require (MOCK_NOTE = "a warning string", mockAddress =
  `0x${hex}`).
- Declarations are scoped to top-level (module-scope) only, via the
  ENCLOSING LINE's leading character rather than anchoring the regex match
  itself to `^` (isTopLevel) — this is what actually excludes handler-local
  variables derived from an already-counted fixture (`let samples =
  [...mockSamples]`, `const sample = mockSamples.find(...)` inside a route
  handler) without also rejecting a second same-line top-level declaration
  (`const url = "..."; const mockX = [];`, an existing test case).
- Added a JSON_IMPORT_RE pass: `import x from "*.json"` counts regardless of
  the imported binding's name.
- Allowance gained a `names: string[]` field; a new test asserts the
  ALLOWLIST's name SET matches each file's actual fixture-binding names
  exactly (not just the count), so swapping one allowed fixture for a
  new one of the same count now fails.

Verified against every real route file (not just the unit cases): re-running
the strengthened detector across src/routes/ produces IDENTICAL counts to
the existing ALLOWLIST for every file except context-pack.ts, which newly
trips on its own DEMO_ONLY_ENDPOINTS array (a real "demo"-prefixed,
array-shaped top-level binding) — pinned with an honest reason (it's the
context pack's own documentation of real refusals, not served business
data, per F-E in this same PR).

Kept countFabrication()'s {mockData, random} return shape unchanged so the
existing "detector" unit tests didn't need rewriting; added fixtureNames()
as a separate export for the allowlist's by-name comparison.

agent: implementer-alpha for pcc-readmodels (c255d7dc)
…uest)

Reviewer finding F-A (r1a): the marketplace gates in packages/gateway/src/
routes/marketplace.ts:400-405,481-486 (and their siblings) lived INSIDE each
route handler as `if (!isDemoRoutesOn())`. Fastify parses the JSON body
before invoking a handler, so a malformed payload on a gated write hit the
parser's own 400 before the intended unconditional 501 was ever reached.

Reproduced in n34-marketplace-no-fabrication.test.ts: POST
/api/marketplace/listings (and /orders, and PUT .../listings/:id) with
Content-Type: application/json and payload "{not json" returned 400 "Bad
Request" (Fastify's own parser error) at d3647b8, not the 501 not_available
every other malformed/empty request to these routes gets.

Fix: added an encapsulated onRequest hook to marketplaceRoutes (the
rewards.ts/logistics.ts pattern), keyed by "METHOD /route" via a
REFUSED_ROUTE_KEYS table carrying each route's exact existing message and
`see` array. The hook runs before preParsing, so a listed route now refuses
before ANY input is read — same guarantee logistics/orchestrator/protocols/
spaces/agents/rewards already have. GET /api/marketplace/categories and
POST /api/marketplace/roi are intentionally NOT in the table (mixed/live
routes per the existing MIXED and REAL test suites) — the hook no-ops for
them and they reach their handlers unchanged. Existing per-handler
`if (!isDemoRoutesOn())` checks are left in place as (now unreachable when
demo is off) dead code — cheapest correct diff; demo-on behavior is
untouched since the hook returns early when isDemoRoutesOn() is true.

Verified: full n34-marketplace-no-fabrication.test.ts (33/33), the N34
ratchet (marketplace.ts's fixture count unchanged at 4 — the new
REFUSED_ROUTE_KEYS binding doesn't match its demo/mock/fake/sample/seed/
fixture name patterns), context-pack-availability.test.ts and
n34-rewards-no-fabrication.test.ts (both also register marketplaceRoutes)
all still pass. tsc --noEmit clean.

agent: implementer-alpha for pcc-readmodels (c255d7dc)
…refuses

Reviewer finding F-E (r1b): DEMO_ONLY_ENDPOINTS omitted at least ten refused
routes named in the finding (logistics installation-step PATCH; orchestrator
sample/workflow collection GETs; protocol template/run collections,
automation-status routes, transfer-agent GET) plus more the finding didn't
enumerate. The existing context-pack-availability.test.ts only checked the
forward direction (documented rows really refuse), never the reverse
(every refused route is documented).

Reproduced: added a test that derives the refused set from the SAME route
registration context-pack-availability.test.ts's own beforeAll already
builds (agents/discover/logistics/marketplace/registry/rewards/spaces/
orchestrator/protocols/swf, demo off, every route probed), and asserts
refused subset-of documented. At d3647b8 this failed with exactly 27
missing routes.

Fix: completed DEMO_ONLY_ENDPOINTS in context-pack.ts with all 27 — 8 in
marketplace (listings/orders GET+POST+PUT+DELETE, left ungated on the
list until F-A's onRequest fix in this same PR), 6 in protocols
(GET /api/protocols and /api/protocol-runs collections, automation-status
GET/POST x2, GET /api/transfer-agents), 2 in orchestrator (samples and
workflows collection GETs), 2 in registry (GET /api/registry/attestations
and /:attestationId — a family with no prior representation in this list
at all), 1 in logistics (the installation-step PATCH), and
1 in rewards + certificates (GET epochs, GET epochs/:id, GET kernels/:id,
GET claims/:id, GET certificates, GET certificates/:id — 6 routes, all
already-live siblings of the 3 rewards routes that WERE documented). No
existing documented entry was removed.

One entry needs the lane's attention: POST /api/swf/epochs/:epochId/distribute
also surfaced as refused-but-undocumented (it answers 501 not_available
unconditionally, no demo path, per swf.ts's own comment "boards N34 and
N46 (money floor)"). Per this task's scope split, packages/gateway/src/
routes/swf.ts is lane-owned and I did not touch it — I only added a
documentation line for its EXISTING behavior to context-pack.ts (not an
excluded file). If the lane's own SWF work changes that route's refusal
shape, this one line in DEMO_ONLY_ENDPOINTS may need to move with it.

Verified: context-pack-availability.test.ts (5/5, including the new
reverse-direction test), the N34 ratchet (context-pack.ts's DEMO_ONLY_ENDPOINTS
binding count stays 1 regardless of array length — consistent with the F-B
allowlist entry added for it in this PR). tsc --noEmit clean.

agent: implementer-alpha for pcc-readmodels (c255d7dc)
#373 answered astra round 3 (A06a) in 552144f: a demo CDP wallet carries
a reference, never an address, and a mock onramp session builds no
coinbase.com URL. N34 stacks on #373, so those fixes come forward. The
merge is clean.

agent: pcc-readmodels (c255d7dc)
…teral (N34)

The #373 merge brings fiat-ramp.ts's DEMO_WALLET_REF_RE, the pattern a
demo walletRef must match. The ratchet's stronger detector (8162ce9)
counted it as fixture data because its name starts with DEMO_, so the
merged branch failed the ratchet.

An initializer that is exactly one regular-expression literal is a
pattern that checks input. It is not data, like the string literals the
detector already exempts. A regex that is called (/x/.exec(rows),
/,/[Symbol.split](text)) still counts. The tests pin both cases.

agent: pcc-readmodels (c255d7dc)
…s nothing into it (N34)

Astra round 1 on #421 (r1a): the HIGH, and the SWF half of the MEDIUM.
Reproduced first: 7 of the 8 tests in n34-swf-demo-only.test.ts fail at
4a28d7a, whose SWF and settlement code is d3647b8's.
- With demo off, GET /api/swf/summary served an epoch seeded at import,
  a "last distribution" of now, and a Base USDC balance that no chain
  read produced.
- A malformed JSON body to /api/swf/participants or to the distribute
  route got the parser's 400, not the refusal.
- Demo answers carried no mock or demo marker.
- A released milestone accrued an invented 1,000 USDC into the SWF,
  through both the release route and the settlement facade: 2 accruals
  where there should be 0.

Now one encapsulated onRequest gate covers every /api/swf route:
- Epoch distribution answers 501 not_available in every mode, before
  the body is parsed (operator item 69). Its see list is empty, because
  the routes it named are refused too.
- Outside demo mode, and always under NODE_ENV=production, every other
  route answers 501 not_available before its body is parsed. Nothing is
  read or written.
- In demo mode the simulation answers, with the x-pcc-demo header and
  mock: true, demo: true on object bodies (the orchestrator's pattern).
  Its first epoch opens on the first demo request, not at import.

Also:
- settlement.ts and settlement.facade.ts no longer accrue into the SWF.
  The release result carries no amount to accrue. swfAccrue and
  swfAutoRegister had no other callers and are removed.
- The agent context pack lists all 27 SWF routes as demo-only and none
  as available. CLAUDE.md says the same.

n34-swf-no-fabrication.test.ts keeps its distribution refusals, now
read through the service. Its "real routes in both modes" section
becomes demo-mode checks: the same answers, now marked, and an
allocation still invents no budget.

Mutation: removing the demo gate, the distribute pre-parse refusal or
the demo marking each fails the SWF tests.

agent: pcc-readmodels (c255d7dc)
#373 now names a demo CDP wallet by its reference in the dashboard onramp
(cbd436e) and syncs the integration guide (f7ab854). N34 stacks on
#373, so they come forward. The merge is clean.

agent: pcc-readmodels (c255d7dc)
…o a call on the next line still counts (N34)

Found while packing afeda4b for review: the exemption accepted a
regex literal followed by a newline. JavaScript continues an
expression across that newline when the next line starts with
.exec(...), [, (, an operator and so on. So `const mockMatch = /x/`
followed by `  .exec(rows)` on the next line was exempt although the
regex is called.

The literal must now end its statement: spaces or tabs, then `;`, the
end, or a newline whose next token does not continue the expression
(. [ ( ` ? or a binary operator, in, instanceof). New tests pin three
continued forms as counted, and a literal followed by a comment and a
new statement as exempt. With the old pattern, the new test fails.

The header comment now describes the detector as it is:
- names with a demo, mock, fake, sample, seed or fixture prefix or
  suffix, whatever the initializer, except a bare string or regex;
- JSON imports;
- the allowlist's per-name pins;
- what the ratchet cannot see (inline literals in a handler, an
  imported mock service) and which tests cover those.

agent: pcc-readmodels (c255d7dc)
Found while packing d957015 for review. The marketplace onRequest gate
keyed its refusal table on req.method, so HEAD on a gated GET missed
the table. It was refused only because each GET handler still checks
isDemoRoutesOn. Removing those checks, which d957015's message calls
dead, would have opened HEAD. The gate now maps HEAD to GET, as the
orchestrator and protocols gates do. A test asserts that HEAD on every
gated marketplace GET answers 501.

agent: pcc-readmodels (c255d7dc)
…outside demo mode (N34)

Found while packing #421. CLAUDE.md marked these tools, but the
descriptions agents actually read still advertised them as live: the
MCP server's tool descriptions, the dashboard's
.well-known/mcp/server-card.json and the CLI help. The tools are
pcc_list_protocols, pcc_depin_stats, pcc_discover_scan,
pcc_discover_onboard, pcc_swf_summary, pcc_swf_participant_dashboard
and pcc_swf_list_proposals. Each now adds: outside demo mode
(PCC_DEMO_ROUTES) the gateway answers 501 not_available. The tools
were found by matching each tool's gateway path against the context
pack's demoOnlyEndpoints. In the server card, the seven description
lines changed and nothing else.

agent: pcc-readmodels (c255d7dc)
…ent, so "a,b".split(",") counts (N34)

Found while packing 7e60f95, the same class as the regex hole closed
in 32f663c. The string exemption tested only the first character, so
any initializer that starts with a quote was exempt, a call or a join
included. The packer's grid found 998 of 1,484 TypeScript-valid inputs
that TypeScript reads as a larger expression.

Both exemptions now share one statement-end rule (STATEMENT_END). The
string exemption accepts string or template literals only, joined by
`+` (a note split over lines, like DEMO_ONLY_NOTE and
MOCK_WALLET_NOTE). Anything else joined in, called or continued still
counts. New tests cover a split call, a continued template, a literal
plus a variable, a literal plus a join, and a multi-line note. With
the old first-character rule, the new test fails.

agent: pcc-readmodels (c255d7dc)
…he docs carry the SWF and tool notes (N34)

Found while packing 7e60f95:
- 7e60f95's sentence was wider than the discover routes. They answer
  501 only where the gateway has no local network discovery (mDNS),
  which means any hosted gateway. With discovery they return real
  devices. The two discover tools now say exactly that, in the MCP
  server, the server card and the CLI help, and in CLAUDE.md's and the
  integration guide's rows. pcc_discover_scan no longer claims it
  "falls back to mock devices in dev mode": the examples are returned
  only in demo mode, marked.
- docs/AGENT_INTEGRATION.md's /api/swf/* row is now the same as
  CLAUDE.md's: 501 outside demo mode, distribution 501 in every mode.
  Its three pcc_swf_* tool rows carry the 501 note.
- packages/mcp-server/README.md lists the seven tools with the same
  notes.

agent: pcc-readmodels (c255d7dc)
LamaSu added a commit that referenced this pull request Oct 3, 2026
#360 was SHIP at c73d708 (EC1c) with full CI green, but master moved on 9/30–10/01 (#424,
#459 N80, #436, #338, #477, #483 and others), and three files conflicted. Resolutions:

- packages/spec/src/index.ts: both sides added exports at the same place. Both are kept: #360's
  `economics` namespace, and master's money-status and readmodels exports.
- packages/gateway/src/routes/swf.ts: both sides made POST /api/swf/epochs/:epochId/distribute
  answer 501 not_available before anything is read. Master's hunk (#421's, reviewed and merged
  with #424) is kept. The behavior is identical.
- packages/gateway/src/routes/rewards.ts: #360 made the reward, certificate and treasury routes
  answer 501 and deleted their fixtures. Master's N80 (#459, reviewed and merged) changed only
  the certificates: an empty list, 404 for one, and a 501 mint that claims nothing. It kept the
  reward and treasury fixtures. Kept: master's N80 certificates, and #360's 501s for rewards and
  treasury, with no fixture import. economics-no-fabrication.test.ts drops its three certificate
  rows (certificates-n80.test.ts pins N80), and the reward and treasury rows still assert 501.

On the merged tree: spec 1286/1286; contracts 267/267; mcp-server 76/76; gateway 3717 passed,
13 skipped, 0 failed; dashboard 353/353; tsc clean for contracts, mcp-server, gateway and
dashboard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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