feat: JobExecutionDTO read model; job detail stops reading mock data (PX-6) - #353
Merged
Merged
Conversation
…o refund-as-payment)
statusClass() used a greedy substring regex that rendered any status containing
settl/releas/complet/done/paid/funded/success/approved/active as a green "settled"
pill. So a REFUND ("refunded" contains "funded"), an UNRELEASED/INCOMPLETE/UNSUCCESSFUL/
NOT_APPROVED/INACTIVE/UNDERFUNDED, and every *_ALLOCATED (decided, not final) rendered as
a completed PAYMENT. A refund shown as a payment is the read-route contract's forbidden-
asserter CRITICAL, live in the shipped on-ramp kit.
Replace it with an EXACT normalized-status map keyed off the sec-A conformance table
(V-next finalState/unitState, the 10-state machine) plus the legacy EscrowSummaryDTO enum:
- SETTLED_RELEASED / COMPLETED -> st-settled (operator distribution discharged)
- SETTLED_REFUNDED / REFUNDED -> new non-green st-refunded ("payer refunded - operator NOT paid")
- RELEASE_ALLOCATED / REFUND_ALLOCATED / in-flight (0-5) / FUNDED / CREATED -> st-waiting
- unmapped / unknown -> new neutral st-unknown, NEVER green (fail closed)
No substring inference for money state. Add honest sec-A direction labels on the settlement
rail, and drop the dishonest rail fallback that inferred 'settled' from releasedCount
(contract rule 12: never key settlement off count/receipt existence).
Adversarial conformance test extracts the shipped <status-map v1> region verbatim (tests the
real bytes, not a copy) and asserts the sec-A table, the never-green invariant, normalization
(casing/whitespace/separators), generic-state tones, and that the old regex is gone. 7/7 green.
Refs: genui-read-route-contract sec-A + rules 1/12; genui conformance matrix v1.4.
Cross-family (sol/GPT-5.6) verdict: the one-line /refund/ guard is insufficient; this is the
exact-map it prescribed. gen-UI owns the fix (defect is in gen-UI's shipped kit + contract).
…d/dispute as green) EscrowPage.tsx rendered the escrow status GlowBadge with `... : "green"` as the ternary DEFAULT, so any status other than active/funded -- refunded, disputed, created, unknown -- rendered GREEN: a refunded or disputed escrow shown as a completed payment. Same false-green money-display bug as the ui-kit statusClass fix, in a parallel formatter (the blast radius sol predicted: "fixing one helper doesn't help if dashboards independently infer status"). Replace with an exact map defaulting to gray: completed -> green, disputed -> red, active -> gold, everything else (funded/created/refunded/unknown) -> gray. Matches the already-honest MilestoneTimeline statusColors + read-route contract rule 1. Type-correct by inspection (all branches return valid GlowBadge colors: green|gold|red|gray); a full dashboard typecheck/build is Spark-offload territory and was NOT run here.
Bring PR #313 current with master before extending it (dry-run merge-tree was clean; no conflicts). No force-push: the PR history is preserved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
…state table
Master had five competing escrow-status vocabularies (EscrowStatus,
Escrow.status, the dashboard DTO, the V-next UnitState, the context-pack
summary) and every surface re-inferred them; the shipped kit's substring regex
rendered a REFUND as a green completed payment (read-route contract rule 1).
MONEY_STATUS_MAP is the ONE exact table: normalized key -> semantic tone +
honest, direction-explicit label. Green ('settled') is reserved for the three
documented FINAL releases to the operator (SETTLED_RELEASED, COMPLETED,
RELEASED). Refunds are 'refunded' (final, operator NOT paid); allocated-not-
final states are 'waiting'; unknown fails closed. Bare SETTLED is deliberately
unmapped: SettlementResultDTO uses it for operator-paid but the V-next phase
vocabulary uses it for BOTH released and refunded.
Frozen at every level; compile-time coverage records make tsc fail if
EscrowStatus / Escrow.status gains a value with no entry. Browser-safe.
8 tests (sec-A table, never-green invariant, unknown fail-closed,
normalization, full vocabulary coverage, immutability).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
…ifier - <status-map v2> mirrors @pcc/spec MONEY_STATUS_MAP verbatim (the vanilla kit cannot import it) and now covers every documented escrow vocabulary, so canonical states like released/slashed/releasing/expired no longer render 'unknown'. - New moneyStatusClass for MONEY surfaces (the receipt window): money table only. An off-schema 'success'/'done'/'ok' on a money response is NOT a settlement state and never renders paid (it still tones a generic run/list surface via statusClass). - Conformance test moved into CI: #313's node --test file lived under apps/dashboard/public/ (served publicly) and no CI job ran it. The new vitest file (a) extracts the shipped region verbatim and proves it equals the spec map key for key and agrees with classifyMoneyStatus over an adversarial battery, and (b) boots the WHOLE kit in jsdom and asserts the rendered receipt pill: refund / allocation / off-schema success / missing status (rule 12) are never settled-green. 19 tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
EscrowPage's escrow badge had its own inline ternary (a sixth formatter). It now calls moneyBadgeColor(), which takes the semantic tone from @pcc/spec classifyMoneyStatus and only maps tone -> GlowBadge color: green is reserved for a documented FINAL release; refunded / allocated / unknown are never green. GlowBadge itself defaults to green, so money badges must pass an explicit color. A compile-time record lists every value of the dashboard's EscrowStatus type (tsc fails if the type gains one) and the test asserts each is a KNOWN state in the canonical map, so no real escrow status renders 'unknown'. 3 tests; dashboard tsc --noEmit clean; full dashboard suite 223/223. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
… phase table Product pack section 7 / PX-6. A product surface needs to PROJECT a job, not infer it. The DTO has four independent axes, each with an explicit enum, the source it was read from, and that source's own timestamps: - execution: what the executor reported. ONE exact table maps every job-row status PCC writes or documents (gateway JOB_STATUSES, KernelJobStatus, StepStatus, and the paid-job pipeline's active / evidence_stored / evidence_submitted / settled) to a phase. Anything else is `unknown`, never a success phase. `settled` in a job row is an execution fact only: mock settlement writes it with no money moving. - evidence: received, counted, and fabricated-by-design events counted with the canonical isFabricated. Received is never verified. - verification: outcome verification is `not_available` (no public verdict read exists); capture checks are counted as capture checks. - settlement: the linked escrow record, classified by the canonical money map (#313 classifyMoneyStatus); payout is paid / refunded / not_paid / simulated / unknown, never derived from completion. asOf is READ time (render-provenance contract, #2222/#2232). Browser-safe. Compile-time coverage of KernelJobStatus and StepStatus; 8 tests (exact table, fail-closed input including prototype keys, frozen table, terminal set). agent: pcc-readmodels (c255d7dc)
…s not settled
PX-6: the live Jobs list drilled into a mock detail page, and the legacy job
DTO inferred payment from completion. This adds the product read model and
stops the inference.
- New GET /api/jobs/:jobId/execution -> JobExecutionDTO (@pcc/spec). One
synchronous read pass over the store; each source (capability, kernel,
evidence + events, capture verdicts, settlement) is read separately, and a
failed read becomes `unavailable` with a generic error (details go to the
log), never an empty list or a default. Same gate as GET /api/jobs/:jobId;
under TENANT_ENFORCE a job of another tenant is a 404. cache-control:
no-store.
- Settlement link follows the paid-job flow (negotiation session -> cwmId ->
escrow), then the job's own cwmId, then the session's escrow address; more
than one candidate is `ambiguous` and nothing is chosen. The payout reads the
job's OWN milestone (by stepId). The whole-escrow status is used only when
the escrow records no milestones; when milestones exist but none is this
step, payout is unknown, so a sibling step's release never pays this job. A
mock-settlement escrow (mock-escrow-*) is `simulated`, never paid.
- Legacy JobDetailDTO timeline: status `completed` now emits a `completed`
event instead of `settled` (completed is not payment). A row that itself
says `settled` is still echoed.
- JobDTO rows carry executionPhase (server-read, exact table), so no surface
re-derives it from status.
- GET /api/jobs keeps { jobs } and adds collection-v1 `items` (the closed
render IR list shape, #2231), total (ALL matching jobs, not the page
length), offset/limit/hasMore and asOf (read time, #2222).
Tests: 34 in readmodels/job-execution.test.ts (builder, loader, route, list
envelope), including negatives: completed without a record is not paid; a
job row saying settled is not paid; funded+completed is not_paid; refund is
never paid; an unknown money status is never paid; a mock escrow is never
paid; sibling releases never pay this job; ambiguous links/milestones choose
nothing; evidence is never verification; failed reads are unavailable with
no internal detail; missing timestamps stay null; tenant isolation. Plus 2
populator tests. Full gateway suite: 3016 passed, 6 skipped, 0 failed.
agent: pcc-readmodels (c255d7dc)
…no mock data
PX-6, the continuity break: JobsPage (live) opened JobDetailPage, which read
only mockJobs/jobMeta/mockEscrows (emptied, so every real job said "Job not
found"), a hard-coded mockEvidence array, hard-coded DID / IPFS CID / Base and
Solana tx hashes, and a "Load demo trace" fixture.
JobDetailPage now renders GET /api/jobs/:jobId/execution only:
- four independent panels (Work, Payment, Evidence, Verification), each
naming its source. "Work reported complete" is never payment; evidence is
"received, not verified"; capture checks are explained as capture checks.
- Payment: only a gateway-classified `paid` is green (GlowBadge defaults to
green, so every badge passes an explicit color); recorded amounts are
labeled recorded, never paid; not-linked / ambiguous / unavailable read as
what they are.
- 404 ("Job not found") is told apart from unavailable ("Job details are
unavailable right now", with retry). A failed refresh keeps the last read
and says it is stale. Polls every 15s until the phase is terminal.
- Notices from the DTO (job row says settled, fabricated evidence, simulated
escrow, unknown status) are shown as-is.
- An "Inspect raw record" section keeps ids, raw statuses and the contract
address available without forcing them into the main view.
Support: gateway.ts ApiError keeps the HTTP status (message unchanged);
getJobExecution; useJobExecution (no retry on 404); useJobs now throws on a
response without a jobs array instead of returning [] (absence is not
evidence); dto.ts JobDTO.executionPhase and the `completed` timeline event.
Presentation rules live in lib/job-execution-view.ts (exhaustive over the spec
unions). JobsPage is the shell lane's (PX-3) and is untouched here.
Tests: 10 view tests + 5 page-source tests (no fixture imports, no hard-coded
hashes/CIDs/DIDs, 404 vs unavailable, stale marker). Dashboard suite 238/238;
tsc -b && vite build clean.
agent: pcc-readmodels (c255d7dc)
…ate escrows Two escrow records under the job's cwm must read `ambiguous` with no record and payout unknown; picking the first would show one escrow's money state for a job that may belong to the other. Mutation-checked: making the loader take the first candidate turns this test red (16/16 mutations of the PX-6 rules are caught by the suite). agent: pcc-readmodels (c255d7dc)
… confirmation Response to the cross-family review of #353 (DO-NOT-SHIP, coord-watch #2477). The types now carry what the gateway must prove before claiming any money: - SettlementLink gains `conflicting`: the job's recorded identifiers point at different escrow records, or the record found contradicts one of them. - SettlementAxis.linkMatches lists every identifier that points at the linked record; payoutBasis is only ever this job's own milestone (no whole-escrow fallback); payoutConfirmation `record_only` qualifies a paid/refunded claim that no chain receipt confirms. - SettlementRecordView.statusObservedAt: null. The escrow record stores no time for its statuses, so a fresh asOf dates the read, not the settlement. - PayoutState documents the reconciliation: paid needs this job's milestone to say released AND the escrow not to contradict it; any disagreement is unknown. no_milestones is unknown. - New notices: settlement_records_conflict, settlement_link_conflict. agent: pcc-readmodels (c255d7dc)
…ze the reader Addresses the cross-family review of #353 (coord-watch #2477, astra), which showed the read model could report a job as PAID with the wrong money. P1-1 link: resolveSettlement now looks up EVERY recorded identifier in full (the session's escrow address and CWM, the job's CWM), with row cardinality checked (the address lookup no longer hides duplicates). One identifier matching several escrows is ambiguous; identifiers matching different escrows are conflicting; the one escrow found must match every identifier the session recorded; a session that records no escrow cannot be linked through the job's CWM. The job's CWM alone links only jobs with no negotiation session. P1-2: an escrow with no milestone rows no longer yields a payout (several jobs can share an escrow): unknown. P1-3: the payout reads this job's milestone reconciled with the escrow's own status (reconcilePayout); released + refunded/disputed/slashed/expired/ unrecognized escrow, and unreleased/refunded + an all-released escrow, are conflicts -> unknown with a notice. paid/refunded are marked record_only. P1-5: GET /api/jobs/:jobId/execution is object-authorized before any axis is read (authorizeJobRead), whatever TENANT_ENFORCE says: a valid X-Admin-Key (constant-time, no dev bypass when PCC_ADMIN_KEY is unset), the job's kernel operator (kernel.operatorAddress), or its recorded buyer (the negotiation session's userAgentId). Anonymous -> 401; anyone else -> 404 (no existence oracle). Stated limits: the principal is the API key's operatorId (proven identity is board N2, gateway); the buyer is only recorded as the session's userAgentId. Tests: 56 in readmodels/job-execution.test.ts, including the reviewer's counterexample (unrelated completed escrow on the job's CWM), address duplicates, session address vs job CWM naming different escrows, a session address with no row, a session CWM contradicting the found escrow, zero milestones for every escrow status, the full milestone x escrow table, and the authorization matrix (anonymous, stranger, operator, buyer, admin, wrong admin key) under both TENANT_ENFORCE settings. Mutation-checked: 17/17 red. agent: pcc-readmodels (c255d7dc)
…b row The job row can say the work finished; it cannot prove payment. Mock settlement writes `settled` into the row with no money moving, so every "work finished" row status (completed, settled, evidence_stored, evidence_submitted, verified, awaiting_verification) now becomes a `completed` timeline event and the legacy JobDetailDTO timeline never emits `settled`. Payment lives on the escrow record (JobExecutionDTO settlement axis). Raised by the #353 cross-family review as a remaining legacy lie. agent: pcc-readmodels (c255d7dc)
…shing Addresses the #353 cross-family review (P1-4, P2) and the design lane's note (#2510). - P1-4: the recorded escrow and milestone statuses are neutral record text (recordStatusBadge: always gray, labeled from the DTO's own classification, prefixed "Simulated:" on a mock escrow). The page no longer re-classifies raw money strings, so a simulated or conflicting record can never show a green "payment sent to operator" beside a payout that says otherwise. - P2: the job keeps refreshing after the work finishes (60s instead of 15s), because finishing the work never makes the money final; a read older than two intervals is marked stale by age, as is a failed refresh. - Design #2510: no work phase uses a success or animated-online chip; only "executing" (amber) and "failed" (red) carry color. - Wording for the new states: link conflicts, records that disagree, no milestones, and "Not confirmed on chain" on a record-only paid claim. A 401 says to sign in instead of "not found". Tests: 18 view tests (every canonical money status x simulated renders neutral; freshness; new wording) and 7 page-source tests (no raw money re-classification, polling never turns off). Dashboard 248/248; tsc -b && vite build clean. agent: pcc-readmodels (c255d7dc)
…id; route data surfaces by data First part of the #313 review fixes (astra via coord-watch #2465, product-qa #2594, reviewer-bravo): - Normalization rejects rather than erases: only a plain status string ([A-Za-z0-9 _-]) is classified. "RELEASED?", "❌RELEASED", "releaſed", ["RELEASED"] and objects whose toString returns a green word are now unknown. Spec and kit mirror are changed together. - COMPLETED is no longer green. It ends many non-money DTOs (jobs, A2A, steps, batch claims where paid != completed), so it now reads "completed - settlement not confirmed". - List rows and run status pick their table from the DATA: money unless the binding is a known non-money read and the row carries no money field. So an escrow row's "success" or "done" is never green, while a completed job still is. Generic surfaces check generic states first. - The EscrowPage milestone badge now comes from the canonical map (no dead "fulfilled" green). Tests: money-status 35/35 (+8); full spec 832/832; dashboard 223/223; the 7 gateway kit suites 95/95; tsc (spec, dashboard) clean. More #313 fixes follow (source-schema receipt adapter). agent: pcc-genui (4df1e691) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
…el; no bare-word green Second part of the #313 review fixes: steward rulings #2490/#2688, astra (coord-watch #2465), escrow #2580, product-qa #2594. - Classify by SOURCE SCHEMA: classifySettlementRecord (spec) / settlementRecordClass (kit mirror): - V-next /lifecycle: unitState is an ordinal 1..9, pinned to VNextSettlementLib.sol's `enum UnitState`; 0 is a read error. finalState, isAllocated and isTerminal must agree, and a final state needs all three present. - V-next /receipt: finalState counts only when terminal with isAllocated true. `null` + allocated reads "decided, not yet paid out". - A legacy escrow record goes through the flat table on its status. - Anything else (a job, an A2A task) is "not a settlement record". - The flat word table has NO green entry: bare SETTLED_RELEASED, RELEASED and COMPLETED are not settlement reads. The only green is a consistent V-next state 8. AWAITING_FUNDING is unknown. Labels are fixed per escrow F4-F6: "not final", "payer not yet refunded", "payout distribution discharged", "payees NOT paid". - Receipt: state by schema; nothing invented (no default "USDC", "payer"/"payee" or "escrow-milestone"; missing values read "not reported"). - Run window: a read model by its schema; a full snapshot with no status reads unknown (an earlier green is never kept). List/run money rows that are read models use the schema. - The kit tables are frozen. Compile-time coverage now constrains the real map (`satisfies`). - EscrowPage: the "Total Locked" and selected-escrow panels no longer glow green. Tests: money-status 18 + conformance 37 = 55. They cover wire fixtures for states 0-9, every cross-check disagreement, missing corroboration, receipt shapes, job/A2A records, the kit==spec adapter over the battery, the ordinal table re-read from the Solidity enum, frozen tables, no-invention, run null snapshot, and money rows. Full spec 852/852; dashboard 223/223 (after a spec build); gateway kit suites 95/95; tsc spec + dashboard clean. agent: pcc-genui (4df1e691) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
…rrency Pins the no-invention rule for the currency line (the earlier test used a receipt with no amount, so the currency branch never ran; mutation M8 survived). M8 is now red. agent: pcc-genui (4df1e691) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
…els-job-execution Brings PX-1's fixed money-status map into PX-6 (steward #2688, genui #2743): bare status words are never green, 'completed' is never paid, and settlement tone comes only from classifySettlementRecord. The merge applies cleanly. Under the new map a milestone record's 'released' is no longer a settled tone, so the next commit adapts the payout axis (5 settlement-axis tests fail at this merge commit alone). agent: pcc-readmodels (c255d7dc)
…_released, never paid #313's fixed map (merged in the previous commit) makes every bare status word a non-green tone, following steward ruling #2490: settlement tone comes only from authoritative money state. A milestone record's "released" is now a waiting tone, so reconciling by tone would have reported a recorded release as "not_paid": a false statement. The payout now reads the gateway's escrow words exactly (their source schema is the escrow_milestones and escrows tables): - a released milestone (released, settled_released) that the escrow record does not contradict is the new payout state reported_released, with payoutConfirmation record_only. It is gray and says it is not confirmed. On a V-next escrow "released" can mean the outcome was allocated, not paid out. - "paid" is reserved for an authoritative settlement read model (a V-next lifecycle or receipt that classifySettlementRecord shows as released). The gateway's escrow records never produce it; a test sweeps every word pair of the money map to prove that. - a milestone saying "completed" is ambiguous (completion is not release): unknown. - conflicts are unchanged: released vs a refunded, disputed or slashed escrow, and refunded or unreleased vs an escrow saying everything was released, are unknown. This also answers coord-watch's open question on #353 (can payout be "paid" when this job's money was not released?): no gateway record can make it "paid" now. Gateway 3042 passed / 6 skipped / 0 failed; spec 861; dashboard 249, tsc -b clean. Mutation check on the new rule: 5/5 killed. agent: pcc-readmodels (c255d7dc)
This was referenced Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…sign #3013) A job row can literally say "settled" with nothing paid (#353's notice job_row_reports_settled), so the Run card could read "Status: settled", and a job list badge or a status metric likewise. Colour was already neutral; the text was not. Any value read from a field named `status` (status, job.status, kernel.status) whose word is a money state now carries PCC's fixed qualifier "- reported by the record, not confirmed by a settlement read" (the #313 wording), in every sink: metric (bindScalar), run card (bindSchemaCard), list badge and list meta (bindListRows). The money-state word list is narrower than the prose CLAIM_RE ("verified"/"approved" are not money states); lookalikes, zero-width, fullwidth and camel/snake/kebab joins are folded first. The kit is rebuilt (check:ir-kit). Tests: 4 new, including the shipped kit end to end (review344 19/19). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
…ocated/phase semantics #313's classifier assumed isAllocated was true for every allocated-or-final state (6-9). The read routes say otherwise: gateway unit-state-mapper isAllocatedState is true for 6/7 ONLY ("outcome decided, money NOT fully moved"), so a settled /lifecycle or /receipt body carries isAllocated:false. A genuinely settled unit therefore rendered "settlement fields disagree" and was never shown as settled -- fail-closed, but wrong, and the hand-written test fixtures encoded the same assumption (one even pinned the real settled receipt as unknown). Now, in the spec classifier and the kit mirror alike: - lifecycle: allocated = state 6 or 7; `phase` must match VNEXT_PHASE (the mapper's PHASE_BY_STATE) when present, and a final state needs finalState, isAllocated, isTerminal AND phase; - receipt (finalState, phase, isAllocated): final only with isAllocated:false and phase:"settled" (both present); isTerminal, if present, must agree; "allocated" and "in flight" cross-check phase too. A new gateway suite takes its fixtures from the routes themselves (Fastify inject over a fake reader) for states 1-9 and classifies every body with the spec AND the shipped kit: only state 8 is green, from either route, and each tampered field is unknown. Tests: money-status 18 + conformance 39; spec 854/854; dashboard 223/223; new gateway suite 3/3. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
…lay sum
The receipt window fell back to economics.amount and formatted it with fmtUsd. On the
real /receipt route that field is a raw integer in the token's BASE units (read-surface
contract rule 14) and the route sends no tokenDecimals, so a 1 USDC settlement
("1000000") would have rendered as 1,000,000.00.
Now economics.amount becomes a display amount only with the record's own tokenDecimals
(exact string arithmetic, no float); otherwise it is shown as "N base units (decimals
not reported)", with no invented currency. A malformed value is "amount not reported".
Top-level amount/totalAmount (legacy escrow records, already display units) are unchanged.
Tests: 3 new (conformance 42/42) using the route's exact EconomicsRecord shape.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
Genui #3113 / #3151. 7061730 classifies settlement reads by the routes' own isAllocated / phase semantics (isAllocated is true for states 6/7 only, so a settled unit no longer reads as "fields disagree"), and de3a973 stops the ui-kit receipt window formatting economics.amount (base units) as a sum. No conflicts. JobExecutionDTO shows no economics.amount: its milestone.amount and escrowTotal.amount are the escrow record's decimal strings, labelled as recorded. agent: pcc-readmodels (c255d7dc)
… it (escrow ruling #3163) Escrow ruled that master's settlement routes are the target and asked gateway to ADD the staticcall-authoritative unitState to /receipt (additive), so a receipt consumer can tell 6 (release decided) from 7 (refund decided); genui keys that direction off unitState, never off finalState. A receipt with unitState enters the unitState branch, which required isTerminal for a final state, and /receipt carries no isTerminal, so settled receipts would have classified "incomplete" the day the field lands. A final state now needs unitState, finalState, isAllocated and phase (every present field still cross-checked, isTerminal included when present). Spec and kit alike. Tests: the route-derived suite classifies each state's real /receipt plus the lifecycle's unitState (states 1-9: same tones as /lifecycle, green only at 8, 6 vs 7 named) and four tampers of it (4/4); money-status 60/60. Requiring isTerminal again turns it red. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7daTDp5o3mAwxYNYyRCVy
Genui #3217: per escrow ruling #3163 the gateway will add unitState to /receipt; classifySettlementRecord accepts that shape and names state 6 vs 7 from unitState. Today's bodies classify as before. No conflicts. agent: pcc-readmodels (c255d7dc)
… unwritten tenant column Found by scout-alpha's evidence map: no writer sets evidence_bundles.tenant_id (the kernel path, PUT /complete, the operator relay and the setup test job all leave it null), so under TENANT_ENFORCE the loader's tenant-filtered findByJob matched no rows and the evidence axis said state "none" for a job that has evidence. The route already refuses a job outside the caller's tenant before any axis is read, so the job's evidence is read by job id alone. Test: a tenant-a job with one bundle and one event, written the way production writes them, read by a tenant-a operator under TENANT_ENFORCE counts 1 bundle and 1 event (on the previous loader: state "none"). agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
#353 now carries #313 @8f946499 (settlement-read classification by the routes' isAllocated/phase semantics; base-unit receipts; unitState on /receipt) and scopes JobExecutionDTO's evidence through the job instead of the never-written evidence_bundles.tenant_id. The one adaptation: loadLegacySettlement stops passing the removed `tenant` load option (it still refuses a job outside the caller's tenant before any read). agent: pcc-readmodels (c255d7dc)
…stone is not this job's Cross-family review r3 of #353 (23-px6-353-r3, astra, DO-NOT-SHIP). P1-5, authorization: - The operator and buyer checks now compare only a wallet proven by a SIWE signature: req.provenWallet, which WP-A's API gate (#326) sets for a SIWE session or an API key minted from one. - An operatorId or email is never used, since self-service provisioning lets anyone claim one (the verdict's "single weakest link"). - Identity is checked BEFORE the job is read (precheckJobRead): no credential is 401, and a credential without a proven wallet is 403 identity_unverified. So no refusal depends on whether the job exists; anonymous callers used to get 404 or 401 by existence. - A proven non-party gets the same 404 as a missing job. - Recorded operators and buyers must be addresses; a label or an email never matches. - Before WP-A merges, no caller has a proven wallet, so only an admin reads: this fails closed. P1-1, attribution: - A milestone records no job, so one milestone for this job's step is this job's only when no other job could claim it. - Claimants are jobs with the escrow's CWM and the same step, plus jobs whose negotiation session names the escrow (countStepClaimants). - More than one claimant is milestoneMatch shared_by_jobs: no milestone attributed, payout unknown, notice milestone_shared_by_jobs. - Different steps on one CWM stay exact. P1-3 and exact words: - reconcilePayout decides by exact word sets only. The canonical map is used only to recognize a word, and a test checks that every word it reads is known to the map. - An unrecognized status now says so (settlement_status_unrecognized; it used to give unknown with no notice). - The DTO names why a linked payout is unknown (payoutUnknownReason: records_conflict, status_unrecognized, status_ambiguous, milestone_shared, no_single_milestone). - No branch returns paid. Tests: gateway job-execution suite 71, and the full gateway suite 3058/6/0 with tsc clean; spec 865. Mutation check: 14 gateway mutants for these fixes, all killed. agent: pcc-readmodels (c255d7dc)
… is never colored Cross-family review r3 of #353, section 5. - A 401, 403 or 404 on the LATEST read replaces the page, cached data included (jobPageState). Before, the page checked only when it had no data, so a signed-out or no-longer-authorized caller kept seeing the money under a stale warning. 403 identity_unverified gets its own text: sign in with a wallet. - Freshness is computed once per page. A stale read (failed refresh, or too old) never colors the payout (payoutBadge): the color is a claim about now. - The query cache is cleared whenever the signed-in identity changes (auth-store onIdentityChange: a key, wallet or SIWE session signed in, changed or signed out), so the next identity never sees the previous one's reads. A 403 is final, like 401 and 404 (no retry). - The new notices and the payout's unknown reasons have plain wording. Tests: dashboard 258 (13 files), including the auth-store identity test; tsc -b clean. Mutation check: 6 dashboard mutants for these fixes, all killed. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
#353 now requires a proven wallet for job reads, checked before the job is read. It no longer attributes a milestone another job could claim, and names why a linked payout is unknown. #382's legacy settlement reads project the same settlement axis. Conflict in readmodels/job-execution.ts: #382's job-row conflict (a job row saying settled over a milestone record that is not released) becomes payoutUnknownReason "job_row_conflict", which drives the same settlement_row_conflict notice. The spec union gains that reason. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…nknown After #353's review-r3 fixes, the legacy settlement claim (GET /api/settlement/:jobId, GET /api/jobs/:jobId/settlement) carries payoutUnknownReason and the two new money notices (settlement_status_unrecognized, milestone_shared_by_jobs). The dashboard names a job-row conflict as one, and a test pins the reason. Tests: gateway 3081/6/0, dashboard 258, spec 865; tsc clean. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…#403 #353's review-r3 fixes change the predicate this family's gate is built on, so the merge adapts the gate. The merged tree does not build without it. - gateJobRead: identity first (precheckJobRead). No credential is 401 and a credential without a PROVEN wallet (WP-A's req.provenWallet, SIWE) is 403 identity_unverified, both before the job row is read. Then the job, TENANT_ENFORCE, and authorizeJobRead(job, provenWallet): kernel operator or recorded buyer, compared as addresses. An operatorId or email is never trusted as an identity. - refuseJobRead and the two legacy settlement routes answer 401 and 403 with the shared JOB_READ_REFUSAL bodies, the same for every job id. - Conflict in routes/jobs.ts: the execution route keeps #403's gate call. - Tests: the stand-in gate sets a proven wallet for a wallet principal (helpers/job-read-party provenWalletFor), the F3 buyer is a wallet, and every route in the family pins the new 403 for a key that claims the operator's or buyer's id without proof, identical for a missing job. Effect: until WP-A (#326) merges, no caller has a proven wallet, so the whole job read family is admin-only (fail closed). After it, email-provisioned and legacy keys cannot read job records. That is the operator's call (decision posted with the #353 triage). Tests: gateway 3131/6/0, dashboard 258; tsc clean. Gate mutation check: 5/5 killed. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…ntity rule) into #441 agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…d id is 403 After #403 took #353's review-r3 identity rule, the job read gate requires a proven wallet (WP-A's req.provenWallet). The test's stand-in gate sets one for a wallet principal. A key that only claims the operator's id, without proof, is pinned as 403 identity_unverified. Tests: gateway 3144/6/0, dashboard 258; tsc clean. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
Conflict in readmodels/job-execution.ts: #389's buildSettlementAxis now returns buildSettlement's axis directly. The axis carries payoutUnknownReason, and the old recordsConflict side channel is gone. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…wallet #353's review r3 (P1-5) applies here too: /api/operator/work and /api/operator/income scoped the caller's kernels by the API key's operatorId, which self-service provisioning lets anyone claim. So a key claiming a public operatorAddress read that operator's work and income. - The operator is now the caller's PROVEN wallet (SIWE: WP-A's req.provenWallet, via jobReadCallerOf). - No credential is 401. A credential without a proven wallet (an email, a self-declared id) is 403 identity_unverified. - Kernels match only as addresses, in any letter case: a SIWE session address is checksummed. Tests: - a claimed id and an email are 403; - a key claiming kernel-nyc's operator but proven as another wallet sees none of its work; - a kernel recorded under a checksummed address belongs to its lowercase proven wallet. Gateway 3090/6/0. Mutation check: 3/3 killed. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…#409 agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 30, 2026
…d gate (F1) Astra round 1 on #382 (a6bcfb7), CRITICAL; reproduced first. GET /api/jobs/:jobId/settlement and GET /api/settlement/:jobId read the job before any identity check and applied only the optional tenant filter. An unrelated or anonymous caller read another job's settlement, evidence metadata, quote and escrow details. The new tests failed at a6bcfb7: anonymous, a claimed id, a stranger's proven wallet and a wrong X-Admin-Key all got 200 on both routes. loadLegacySettlement now applies the same identity-first object authorization as /execution (#353's precheckJobRead and authorizeJobRead): - no credential: 401; - a credential without a proven wallet: 403 identity_unverified. Both come before the job is read, for every job id alike. - then the tenant check; - then only X-Admin-Key, or a proven wallet that is the job's kernel operator or recorded buyer, reads it. Anyone else gets each route's own 404, byte-identical to a missing job's. - a failed authorization read: 503. The integration tests now read as the seeded kernel operator's proven wallet. Also astra's LOW: the route comments no longer say a recorded release makes a job settled (it is reported_released, settled: false). agent: pcc-readmodels (c255d7dc)
LamaSu
marked this pull request as ready for review
September 30, 2026 19:18
LamaSu
added a commit
that referenced
this pull request
Sep 30, 2026
…job-read-family-auth #382's round-1 fixes come forward: - milestones[].forThisJob from the attributed milestone, plus association; - the corrected route comments; - the job-party test helper. It was already byte-identical here. Conflicts resolved to this branch's family gate: gateJobRead and its unauthenticated, identity_unverified, not_found and unavailable kinds replace #382's inline precheck and authorize. Both implement #353's rule, and the gate is the shared one. The legacy-settlement integration tests keep #382's explicit stand-in gate (identity only from headers), so its 401 and 403 tests hold. Their reads name the kernel-nyc operator explicitly. agent: pcc-readmodels (c255d7dc)
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.
PX-6 (product pack §7, gateway read-model lane). The live Jobs list drilled into a job detail page built from mock data, and the gateway's own job DTO reported completed as settled. This PR adds a typed read model for one job and makes the detail page a projection of it.
What was wrong (at
ac86a404)JobDetailPageread onlymockJobs,jobMetaandmockEscrows, which are empty since mock-data was emptied, so every real job showed "Job not found". Its only content was a hard-coded evidence array, hard-coded DID, IPFS CID and Base/Solana tx hashes, and a "Load demo trace" fixture.job.populator.tsbuildTimelineemitted asettledevent for anycompletedjob, so paid was inferred from completion on the server.settledis written by mock settlement with no money moving, andescrowshas nojobId, so the job↔escrow link has to be resolved.GET /api/jobs/:jobId/execution→JobExecutionDTO(@pcc/spec,pcc.job-execution/v1)The DTO has four independent axes. Each has an explicit enum, names its source, and carries the source's own timestamps. Axes never borrow meaning from each other.
pending, queued, dispatched, running, awaiting_handoff, paused, completed, failed, timed_out, cancelled, unknown, from one exact table covering every documented job vocabulary. An undocumented value isunknown, never success.none / received / unavailable, with bundle and event counts and fabricated-by-design events counted by the canonicalisFabricated. Received is not verified.not_available(no public verdict read exists). Capture checks are counted as capture checks only.linked / not_linked / ambiguous / unavailable; money states via #313's map; payoutpaid / refunded / not_paid / simulated / unknownPayout rules:
mock-escrow-*escrow issimulated, never paid.ambiguous, and nothing is chosen.unavailablewith a generic error. It is never an empty list or a default.asOfis the READ time (render-provenance contract #2222/#2232).GET /api/jobs/:jobId. UnderTENANT_ENFORCE, a job belonging to another tenant returns 404.Also
completedforcompleted. A row that itself sayssettledis still echoed.JobDTOrows carryexecutionPhase, read on the server, so the list never re-derives it.GET /api/jobskeeps{ jobs }and adds collection-v1items(for the closed IR, #2231),total(all matching jobs, not the page length),offset,limit,hasMoreandasOf.JobDetailPagerenders only the DTO.paid, and recorded amounts are labeled "recorded".useJobsnow throws on an off-schema response instead of returning[].ApiErrorkeeps the HTTP status.JobsPagebelongs to the shell lane (PX-3) and is not changed here.Tests
@pcc/spec: 832/832 (8 new).tsc -b && vite buildis clean.settledis not paid;not_paid;unavailableand leaks no internal detail;paidpayout green and never imports fixtures.Not in this PR (tracked)
GET /api/settlement/:jobId(settled = completed) andGET /api/jobs/:jobId/settlement(completed → "settled", plus apaidAmountthat holds the escrow total or quote). These are money-semantics routes, so the fix goes on a separate branch for cross-family review.KernelDetailPagestill reads mock data, the same continuity break for kernels.BIND_POLICYfollows feat(genui-b): closed render IR + promotion - modern-port of #272 and the 6 orphaned promotion commits #344/feat(genui-b): derived render-state provenance - source-assigned class, freshness, no regression (PX-4; depends on #344) #348.Revision 2 (
6c88956b..7e6071b6): answers the cross-family review (coord-watch #2477, astra: DO-NOT-SHIP)ambiguous. Identifiers naming different escrows, an escrow contradicting a session identifier, or a session that records no escrow →conflicting. The job CWM links alone only when the job has no session.unknown. There is no whole-escrow fallback at all.[]milestones → unknownreconcilePayout(milestone, escrow). Released + refunded/disputed/slashed/expired/unrecognized →unknown; unreleased or refunded + all-released →unknown. Both carry thesettlement_records_conflictnotice. Paid/refunded carrypayoutConfirmation: "record_only".statusObservedAt: nullbecause the record stores no status time.authorizeJobReadruns before any axis is read, whateverTENANT_ENFORCEsays. It allows a validX-Admin-Key(constant-time compare, no dev bypass), the kernel operator, or the recorded buyer. Anonymous → 401; anyone else → 404.TENANT_ENFORCEsettingsJobDetailDTOtimeline no longer emitssettledfrom a job row (mock settlement writes it).Authorization limits (stated in the code). The principal is the API key's
operatorId; a proven identity is board N2 (gateway lane). The only record of the buyer is the negotiation session'suserAgentId. Jobs submitted without a session record no buyer, so for those only the operator and admins can read.Tests at
7e6071b6:tsc -b && vite buildcleanMutation check: 17/17 red. Each of the rules above was broken on purpose, and the suite failed every time. The initial run left two guards uncaught; I added tests that isolate them. Secret scan: 0 findings.
Revision 3 (
7e6071b6..513b933a): carries #313's fix, and "paid" is no longer reachable from gateway records7ec16196: brings in fix(ui-kit,dashboard,spec): one canonical money-status map - no refund/allocated/unknown as settled-green #313's fixed heada813c013(steward #2688, genui #2743). The merge applied cleanly. feat: JobExecutionDTO read model; job detail stops reading mock data (PX-6) #353 no longer carries the old506f437ecode on its own.513b933a: adapts the payout axis to fix(ui-kit,dashboard,spec): one canonical money-status map - no refund/allocated/unknown as settled-green #313's map, following steward ruling #2490 (settlement tone comes only from authoritative money state).not_paid, which is false. So the payout reads the gateway's escrow words exactly.reported_releasedis a new payout state: this job's milestone record says released, and the escrow record does not contradict it. It carriespayoutConfirmation: "record_only", is gray, and says it is not confirmed. On a V-next escrow, "released" can mean allocated, not paid out.paidis reserved for an authoritative settlement read model: a V-next lifecycle or receipt thatclassifySettlementRecordshows as released. No gateway escrow record produces it. A test sweeps every word pair of the money map to check this. That answers the open review question ("can payout be paid when this job's money was not released?"): no.completedis ambiguous, so it isunknown. Conflicts are unchanged.tsc -bclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01A2ZvsqsAb7jC7AC8Viisqn