Skip to content

fix(gateway): credit composition reputation at completion, not submission-ack (corrects #96) - #208

Open
LamaSu wants to merge 6 commits into
masterfrom
feat/reputation-completion-credit
Open

fix(gateway): credit composition reputation at completion, not submission-ack (corrects #96)#208
LamaSu wants to merge 6 commits into
masterfrom
feat/reputation-completion-credit

Conversation

@LamaSu

@LamaSu LamaSu commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Problem

PR #96 wired composition execution to reputation but credits deltas at submission-ack: in executeComposition, recordStepOutcome + finalizeReputation fire per step right after effectiveRunStep — which only submits the job (physical completion is async via processEvidence). Completion never corrects it (reputation refs in settlement-service.ts/kernel-service.ts = 0). Net: a step that acks then physically fails later keeps its +10 — optimistic credit, no compensation.

Fix (real-mode-only, PCC_COMPOSE_EXECUTE_REAL=true)

  • Defer each step outcome to a new pending status (zero delta at ack).
  • Credit at completion via new settleStepReputationForJob(jobId, verdict), wired into the four completion decision points: kernel-service.ts local success / failure / catch, and paid-job-flow.ts external PUT /complete success + 422 oracle_verification_failed. Success → +10 (+one-time +5 composition bonus); fail → nothing (deferral ⇒ no optimistic credit to reverse; compensation is free).
  • Reuses finalizeReputation unchanged → idempotency + composition bonus preserved.
  • Step→job linkage rides the job's existing nullable parameters JSON (__pccStep) — no schema migration.

Secondary

Wires the dormant MachineAdapter.preflight() into KernelService.submitJob, requirements-gated (only fires when a job declares parameters.requirements) — additive; ships the @pcc/resource-tracker 2-phase reserve/commit/rollback package it calls.

Tests

  • 10 new: deferral (pending, zero ack deltas); ack-then-fail earns no positive reputation; credit-at-completion (+10, +5 bonus, idempotent); preflight() refusal (typed, spy-confirmed) + requirements-gated. Kernel suite 803/0. NOOP/test path untouched → existing ack-time reputation tests stay green.

Caveats (settlement code — flagged honestly)

  1. The two external settleStepReputationForJob calls in paid-job-flow.ts are type-checked + the seam is unit-tested, but paid-job-flow.test.ts is in a pre-existing vitest exclude, so those two call-sites have no live test. Local path fully tested.
  2. A local success with no in-memory bundle leaves its outcome pending (documented; sweep out of scope).
  3. Pre-existing V2/V3 failures (v2-settlement-wiring, gateway-evidence-v2 LIT label) are env-dependent (Story/IPFS/chain unavailable) and verified to predate this branch — not introduced here.

🤖 Generated with Claude Code

LamaSu and others added 6 commits July 6, 2026 17:51
… + 2PC ledger

implementer-r3: clean-room TS port of the PyLabRobot poka-yoke pattern
(pattern reference only, no dependency on pylabrobot). Offline, in-memory,
zero external deps: CapacityModel refuses out-of-bounds requests
independent of availability; ResourceTracker adds reserve()/commit()/
rollback() two-phase-commit semantics on top; five typed refuse-to-start
error classes carry structured details instead of string messages.

This is the resource layer for PCC's new preflight() gate (R3,
ROBOTICS-BUILD-SPEC.md); wired into MachineAdapter/job-handler in
follow-up commits. 19/19 unit tests green, typecheck clean.
…eference impl

implementer-r3: adds preflight(input): Promise<PreflightResult> as an
OPTIONAL method on MachineAdapter (packages/kernel/src/adapters/types.ts)
so none of the ~10 existing adapters (hamilton, ipp, octoprint, opcua,
sila, etc.) are affected — confirmed via full suite run, 803/803 tests
green, zero regressions.

MockFDMAdapter gets the reference implementation: a calibrated
filament/build-volume capacity model backed by @pcc/resource-tracker's
2-phase reserve/commit/rollback ledger, plus commitReservation()/
rollbackReservation() to finalize a preflight-approved hold.

Acceptance demonstrated in src/adapters/__tests__/mock-fdm.test.ts: an
under-resourced job (200g requested, 50g on hand) is refused with a typed
insufficient_resource error before execute() is ever reachable; a
sufficient job passes and returns a reservation token; commit permanently
consumes capacity, rollback releases it.
…dler

implementer-r3: preflight runs AFTER inbound auth verification (~L175)
and BEFORE the session key is minted / execute() is invoked (~L220) —
the exact PyLabRobot poka-yoke seam (tracker check before
backend.aspirate), applied to the third-party kernel-builder SDK path.

Kept kernel-sdk decoupled from @pcc/kernel and @pcc/resource-tracker on
purpose: kernel-sdk is published (private:false) and must not gain a
workspace dependency on either private package. `preflight` is a
structurally-typed optional callback (PreflightCheckResult), symmetric
to the existing `execute` callback — builders backed by a
MachineAdapter map its preflight() result into this shape when wiring
a handler. Refusal throws the new typed KernelPreflightError.

5/5 tests green (src/__tests__/job-handler.test.ts): under-resourced job
refused before execute() is ever called; typed reason/details on the
thrown error; sufficient job passes through and returns a normal
response; no-preflight kernels are unaffected (backward compatible);
preflight confirmed to run after — never before — auth verification.
…credit

Additive enum member (type union + Zod schema) so real-execution mode can
record a step outcome at ack without applying a reputation delta.
finalizeReputation already only settles success/failed, so pending is
naturally skipped and the composition bonus is withheld while any step is
still pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L95pzhZjDxuUFiZtJT62mY
…ion, not ack

Fixes the reputation-at-completion bug: real-execution mode credited each
step's reputation off the submission ack, and physical completion never
corrected it (ack-then-fail kept its +10).

- compose.ts executeComposition: gate on PCC_COMPOSE_EXECUTE_REAL. In real
  mode, ack-success records a "pending" outcome (no finalize, no
  short-circuit) and ack-throw records "failed" + finalizes that step;
  NOOP/test mode is unchanged (still finalizes at loop end). Thread
  {compositionId, stepIndex} onto the job via parameters.__pccStep
  (createProductionBinding + ExecutorBinding.runStep ctx arg) — no migration.
- reputation.ts: new settleStepReputationForJob(jobId, verdict, opts) seam —
  reads the job's __pccStep bridge, flips the pending outcome, and reuses
  finalizeReputation to apply +10/-15 and the one-time +5 bonus. Inert for
  non-composition jobs; idempotent.
- kernel-service.ts: call the seam at all local completion points (success
  after processEvidence, failure else-branch, .catch) on both the Sentry and
  fallback paths, best-effort. Secondary: wire MachineAdapter.preflight() into
  submitJob, gated on parameters.requirements (typed PreflightRefusedError).
- paid-job-flow.ts: call the seam on external verified success and before the
  422 oracle_verification_failed, best-effort.
- job.facade.ts: thread parameters.requirements into submitJob.

Additive: NOOP/test mode keeps ack-finalize, so every existing test stays
green. New tests: real-mode deferral (all pending, no deltas), credit at
completion (+10 then +5 bonus once, idempotent), ack-then-fail (-15, no +10),
single-job no-op, preflight refusal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L95pzhZjDxuUFiZtJT62mY
Close caveat #1 on PR #208: the two settleStepReputationForJob call-sites on
the EXTERNAL completion path (PUT /api/jobs/:jobId/complete in paid-job-flow.ts)
were type-checked and seam-unit-tested but had no runtime test, because the
original paid-job-flow.test.ts is in the vitest exclude (stale post-facade-rewrite
expectations). This adds a NEW focused file that drives the real /complete route
via app.inject and spies on settleStepReputationForJob to assert:
  (a) oracle-verified success -> settle(jobId, "success", {evidenceBundleId}), 200
  (b) 422 oracle_verification_failed -> settle(jobId, "failed")
  (c) inert for a job with no __pccStep bridge (still 200)

Only the real-I/O boundaries are mocked (evidence storage + verification oracle);
the reputation module is spied but delegates to the real impl, so the +10/+5
credit and -15 debit are asserted end-to-end against the in-memory ledger. No
escrow-client/on-chain mocks needed: MOCK_SETTLEMENT=true + a seeded job with no
negotiation session gates every V2/V3 branch off. The original excluded file is
left excluded (its scope-revocation + response-code expectations remain stale).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L95pzhZjDxuUFiZtJT62mY
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