Repository navigation
feat(spec): evidence levels -- submitted / device_reported / inspected_output (must-close 5) - #345
Merged
Merged
Conversation
…kernel before settlement A signature over a bundle digest proves who signed which digest, not which job, node or output the digest is evidence for. The bundle-level jobId and kernelId are not inputs to hashBundle, and /complete never reopened the digest, so with the SEAM-2 gate open: - a genuine device bundle for job A, relayed again under job B, settled job B (same kernel, direct-signed); - a delegation scoped to several jobs let job A's bundle settle job B; - any other tagged digest the node key signs (a log-chain entryHash) passed the signature check as a bundle digest. @pcc/spec gains verifyEvidenceSubjectBinding (pcc.evidence.subject-binding.v1). It fails closed unless every event reproduces its own hash, the events reproduce the signed bundleHash, at least one event commits payload.jobId and every payload.jobId is the subject job, every source.kernelId and payload.kernelId is the kernel that accepted the job, and (when the subject names one) payload.outputHash matches. These are the fields kernel-sdk already hashes. resolveSettlementEvidence now requires a subject and the stored events for a device slot, checks the binding before the signature, and takes the delegation scope from the subject's job. /complete loads each device-signed row's events and tries every such row, so a replayed row stored first cannot hide the genuine one. The gate stays closed by default; the closed path is unchanged. Relayed rows only carry events once the operator relay stores them (#335, LO-GW-4b). Until then a relayed device row has no events and falls back to the gateway anchor. Tests: spec 843/843 (22 new), gateway full suite 2996 passed / 6 skipped (8 new resolver cases, 3 new /complete route cases with the gate forced open). Each binding leg was removed in turn and a test failed every time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…eported or inspected_output Must-close 5 keeps three kinds of evidence apart: the device TOOK the work (submitted), the device that did the work reports it finished (device_reported), and the output was observed or measured by something else (inspected_output). pcc-node now reports acceptance as execution_progress at level submitted (PR #343); this gives every consumer one classification so none of them can read an accepted-only job as a completed one. evidenceLevelOf / evidenceLevelOfBundle rule on every member of EVIDENCE_EVENT_TYPES, including the ones that prove no outcome level, and a test fails if a new vocabulary type arrives without a ruling. An inspection counts as inspected_output only when the events name the executing devices and the observer is not one of them; a device measuring its own output, or an observation whose independence cannot be shown, is device_reported. Fabricated events (isFabricated) and events with no device attribution prove no level, and a fabricated execution event cannot make an inspection look independent. printer_job_verified proves no level: it is a log-stream summary with no success field (evidence vocabulary ruling, 2026-09-07). A level is strength, not verdict: a failed inspection is still inspected_output. Failure and contradiction stay with the committed program and the measurement profile's policy. The level names match MeasurementProfile AcceptanceLevel (#336), which can alias EvidenceLevel. Tests: 20 new; spec 817/817; spec tsc clean. Six mutants (independence, the size guard, submitted membership, fabricated filter, attribution, fabricated executors) each turn at least one test red. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
This was referenced Sep 24, 2026
Draft
# Conflicts: # packages/gateway/src/__tests__/device-evidence-settlement.test.ts
…re-verified anchor Cross-family review of #341 (r1-341-binding-astra, DO-NOT-SHIP): the binding held in /complete, but /resume-settlement took the LAST evidence row for the job and drove the chain and the oracle with its digest, verifying nothing. Any row relayed after completion became the settlement evidence. - /resume-settlement settles only the evidence /complete pinned (job.evidenceBundleId) and re-verifies it before any settlement step (verifyPinnedSettlementEvidence): the row must belong to this job and its kernel; a device anchor must pass the subject binding and the registered-key signature again; a gateway anchor's bundleHash must recompute from its stored envelope (the bytes GET /api/evidence/:hash serves). Otherwise the claim is released and the route refuses with the reason. The chain receives the pinned digest as bytes32, as /complete's V3 path already does. - /complete no longer restates a device digest over the gateway's own events: the gateway record is stored under its own envelope hash, and when a verified device bundle anchors settlement the job pins that device row (its events open to its digest; its delegation is kept). The archive and the response report the anchor. - Recovery test fixtures pinned fake hashes ("sha256:trapped-evidence", a bare 0x); they now pin genuine gateway anchors. Merged #338 @897c7bae (R20 fixes) into this branch first. Tests: gateway full suite 3010 passed / 6 skipped / 0 failed (188 files; 12 new here); gateway tsc clean. Six mutants (latest row again, device re-check skipped, gateway recompute skipped, /complete restating the digest, job check removed, refusal ignored) each turn a test red. Still open from the review, with owners (not claimed here): delivered-output and assigned-device binding need authoritative execution state (gateway / composition); an acceptance nonce per execution (gateway); a persisted settlement intent for idempotent retries (escrow / gateway); comparing the pinned digest with evidence already on-chain (escrow read). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…anchor carries the canonical snapshots
Coord-watch's cross-cutting rule, from two independent verdicts: an unsigned
payload.jobId hidden as a non-enumerable property passed the job binding,
because hashing skipped it but the evaluator read it.
verifyEvidenceSubjectBinding now:
- hashes canonicalize({type, timestamp, source, payload}) itself;
- reads jobId, kernelId and outputHash from the parsed canonical text, as own
properties only, never from the live object;
- returns those snapshots (ok: true, events).
A non-enumerable, inherited or Object.prototype-polluted jobId, and a getter
that answers the hash with job A and the evaluator with job B, can no longer
bind a subject. Any event canonicalize cannot hash (a cycle today; more once
#359's strict tree lands) is malformed-event: the function still never
throws.
The gateway's resolveSettlementEvidence anchors on the binding's snapshots
instead of the caller's objects, so what is archived and evaluated
downstream is exactly what was hashed. The R1 pinned-evidence check uses the
same anchor verifier.
Tests:
- subject-binding 28/28: 5 new hashed-only cases plus snapshot return
- device-evidence-settlement 47/47: the anchor is the snapshot, independent
of later changes to the caller's copy
- spec 852/852
- gateway 3012 passed / 6 skipped, tsc clean; the one timeout in
completion-real-tier.test.ts under load passes 3/3 alone, three times
- 4 mutants killed: live objects evaluated, inherited reads,
canonicalize throw escaping, resolver returning caller objects
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…once, so one milestone's evidence cannot settle another The oracle's audit probe found that evidence signed for milestone 3 of a job settled milestone 4 of the same job. Kernel-signed events committed only payload.jobId; the unit and the challenge lived in the evidence block, which the producer assembles and the kernel never signs. - EvidenceSubject gains optional settlementUnitId and challengeNonce, each 0x + 64 lowercase hex and byte-equal to the settlement package's unitBinding.settlementUnitId and challengeBinding.nonce. They come from the unit record, never from the evidence. - When the subject names one, some event must commit it and every event that carries it must agree, the same rule as jobId and outputHash. The new reasons are unit-not-committed, unit-mismatch, challenge-not-committed and challenge-mismatch. A subject that names no unit behaves as before. - kernel-sdk's handler accepts settlementUnitId and challengeNonce on the job request (refusing either if malformed, 400) and commits them in the signed execution_started and execution_completed payloads. Jobs without them keep their bytes. - The gateway must forward both on dispatch, and pcc-node must commit them (adk). The oracle mirrors the check at /settle. Until then a unit-bound subject fails closed. Tests: - subject-binding 34/34 (6 new) - kernel-sdk 38/38 (3 new; the handler's evidence binds its unit and refuses another) - spec 858/858 - gateway 3013 passed / 6 skipped; gateway and kernel-sdk tsc clean - 6 mutants killed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…ernel-sdk names the job on every event The oracle enforces per-event job binding at /settle: every event's payload.jobId must equal the settling job. kernel-sdk's gcode_hash_verified and workflow_step_completed carried no jobId, so every kernel-sdk bundle authenticated and then failed the job binding. The kernel signs event by event, and a session delegated for several jobs could not attribute a jobless event, so the producer moves (oracle #3101; my answer #3137). LO-EV-9 moves with it, so public and private binding agree. - LO-EV-9 rule 5 changes from "some event commits payload.jobId" to "EVERY event commits payload.jobId equal to the subject" (job-not-committed now names the event). - settlementUnitId and challengeNonce, when the subject names them, likewise scope EVERY event. outputHash stays "some event", since it lives on the completion. - kernel-sdk puts jobId, and the unit fields when given, on every event: the input commitment, every step completion, started and completed. - Other producers (pcc-node for adk; sensors' adapters) must follow. Until they do, their bundles fall back and fail closed. Tests: - subject-binding 36/36 (new: only some events naming the job; a unit carried by only some events) - kernel-sdk 38/38 (every event, step completions included, names the job and unit) - spec 860/860 - gateway 3013 passed / 6 skipped; gateway and kernel-sdk tsc clean - 4 mutants killed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…el bundles bind (LO-EV-9 per-event) Charter item 3, incumbents conform. After per-event job binding (0a4836a), no bundle from packages/kernel could bind: - EvidenceEmitter.addEvent knew the job but never committed it. - The digital kernels' input and step events had no jobId. - The IPP adapter put the PRINTER's own job number in payload.jobId, the field LO-EV-9 reserves for the PCC job. That evidence could never bind, even under the old "every present jobId must match" rule. - EvidenceEmitter commits payload.jobId on every event before hashing. When registerStep is given a settlement unit (0x hex32 unitId + nonce), it commits both on every event too. An adapter may pre-fill those fields, but a different value is refused, never overwritten. - IPP adapter: the printer-local number is now payload.ippJobId, at every evidence emit site (mock and real). printer-job reads printerJobId from it. Status responses are unchanged. - Accounting and procurement-RFQ kernels thread jobId into every step event and the input commitment. Tests: - evidence-emitter-binding 4/4: stamping, the unit, refusing another job or a malformed unit, and a real mock IPP print whose bundle binds {jobId, kernelId} - procurement 16/16: the bundle binds - kernel 868/868; tsc clean - spec 860; gateway 3013 passed / 6 skipped - 6 mutants killed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…he oracle signs rejects on (J4)
The oracle signed reject verdicts on a producer-asserted contradiction flag
it never derived: the same class as signing an underived claim. Ruling
(#3240): it signs reject only for a contradiction it derives from
authenticated events, and an underived one is refused unsigned. The derivable
rule must be public, so the oracle and profile admission read one predicate.
- deriveContradictions(events) returns, in a fixed order:
- "completion-and-failure": a device-reported completion and an
execution_failed in the same set;
- "completion-and-failed-inspection": a completion and an inspection
reporting its own negative verdict.
- inspectionFailed(event): an INSPECTION_EVENT_TYPES event whose
payload.passed is present and not true. This is the rule sensors adopted
in profile admission (#363); it now lives next to the inspection types.
- A failure without a completion is a device failure, not a contradiction.
- Fabricated events prove no contradiction, just as they prove no level.
Tests: evidence-level 26/26 (6 new); spec 823/823; tsc clean. 5 mutants
killed: fabricated counted, completion gate, absent verdict, non-inspection
verdict, failure kind.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
Two gaps in the per-event unit binding, found while reviewing pcc-node's matching change (#420), where the same probes pass: - EvidenceEmitter kept a settlementUnitId or challengeNonce that an adapter pre-filled on a step registered without a unit, so a signed event could commit a unit the kernel was never given. Those fields are reserved for the binding; a unit-less step now refuses them. - kernel-sdk's job handler accepted a settlementUnitId without its challengeNonce, or the reverse. The oracle requires both on every event, so half a binding can never settle; it is now refused with 400 before anything executes. kernel-sdk 39/39, kernel 869/869, tsc clean for both. Four mutants (each guard dropped, the pair check narrowed to one direction, the nonce left unreserved) are each killed by one test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
LamaSu
added a commit
that referenced
this pull request
Sep 28, 2026
…vidence's failure and contradiction rules Coord-watch's cross-cutting rule (#2961), carried to #363 by evidence (#3079): evaluate only what you hashed. LO-EV-9 (#341 @799cea1d) now returns the verified canonical snapshots of each bundle's events. Admission reads those snapshots for every check (dedupe, simulation, levels, device, version, window, the inspection verdict), never the caller's objects, whose getters or non-enumerable fields could answer differently from the hashed bytes. inspectionFailed and the contradiction rule now come from evidence-level.ts (#345 @cb81284f): inspectionFailed replaces my private copy (same rule), and deriveContradictions is the one public contradiction rule the oracle signs rejects on, so admission and the oracle cannot drift. A failure with no completion is still a device failure under onDeviceFailure. Stack refreshed: #338 @92b4302b (R20 round 2), #341 @799cea1d, #345 @cb81284f, #336 @06a5a49b. Tests: a payload.passed getter that answers false while binding hashes it and true afterwards is rejected (with the old live-object read put back, that test fails); contradictions are named by kind. 42 admission tests; spec 979/979; tsc clean; test files type-check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PUT /api/jobs/:jobId/complete writes the gateway's own execution_completed and the caller's evidenceEvents, of any type, under source.deviceId "gateway". No device reported them. Until now evidenceLevelOf read that stamp as a device: a gateway completion counted as device_reported, and it named "gateway" as an executing device, so a printer inspecting its own output could read as inspected_output. GATEWAY_STAMPED_DEVICE_ID is not a device attribution, so those events prove no level and name no executor. deriveContradictions is unchanged (the oracle mirrors it, J4): types decide there, and a contradiction can only refuse. Found while checking readmodels' #441 against the evidence vocabulary; #441 will take evidenceLevel from evidenceLevelOfBundle. spec 826/826, tsc clean. Mutant (drop the clause) fails both new tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Oct 1, 2026
…ontradictions and the gateway-stamp rule #349 was cut from #345 before cb81284 (deriveContradictions, J4) and ba31ea6 (gateway-stamped events prove no level). Merged, not rebased, so nothing is force-pushed. No conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…main bundles with closed inspection verdicts (E5 F1-F4) Cross-family review E5 on #345 returned DO-NOT-SHIP. F1-F4 all reproduced against ba31ea6 and are fixed here. F5 is a documented scope rule, not code. - F1: replace the event-list API (executingDeviceIds, evidenceLevelOf, evidenceLevelOfBundle) with evidenceLevelOfBundles(bundles, context) and deriveContradictions(bundles). Independence is judged between AUTHENTICATED trust domains (operator principals), never declared source.deviceId strings. inspected_output needs a non-empty assigned executorTrustDomains, a known executor set, an inspector bundle with a trust domain, and that domain outside the executor set. Malformed trust domains throw the new EvidenceLevelInputError. The gateway stamp is compared after an ASCII trim and ASCII lowercase. - F2+F3: one closed verdict per inspection type (inspectionVerdict: pass, fail, none, malformed). Only pass and fail prove a level. In contradictions fail and malformed count as failed, none does not. inspectionFailed stays as a wrapper. Pinned fields: instrument_result `pass` (boolean), batch_sample_result `status` ("PASS"/"FAIL"), photo_comparison_result none (no producer anywhere). - F4: fabrication is bundle-wide (bundleHasFabricatedEvents). A fabricated bundle proves no level, is ignored by deriveContradictions, and still contributes its trust domain (or UNKNOWN) to the executor set. - F5: the per-settlement-unit scope rule is written into the module header and the deriveContradictions doc. OPEN, not guessed: cv_inspection_result is NOT pinned. The ruling pins `pass`, but the real producers emit `passed` (kernel photo-camera-adapter, mock-camera, onboard-kit camera templates) while types/dpp.ts reads `pass`. Until the contract owner rules, either spelling is malformed (no level, fails closed in contradictions). See the E5 triage file. The old evidence-level.test.ts exercises the removed API and is rewritten in the next commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…ndle API and pin E5 F1-F5
Rewrites evidence-level.test.ts for evidenceLevelOfBundles,
deriveContradictions and inspectionVerdict (the event-list API is gone).
The vocabulary partition test is kept.
- F1: declared-id attacks ("printer-1 ", "PRINTER-1", a second id, a decoy
executor) are not independent; the id is never consulted; an unknown
executor domain, an empty or absent executorTrustDomains, an inspector
bundle with no trust domain, and a fabricated execution bundle's domain
all block independence; malformed trust domains and bundle shapes throw
EvidenceLevelInputError; the gateway stamp folds ASCII case and
whitespace only (NBSP, U+2028, U+FEFF, full-width are real attributions).
- F2/F3: each pinned type has pass, fail, none and malformed rows
(instrument_result `pass`, batch_sample_result `status`, photo_comparison_result
unpinned); none and malformed prove no level; in contradictions
malformed fails closed and none does not; non-plain payloads, inherited
keys and accessors are malformed.
- F4: a bundle with any fabricated event proves no level and is ignored
by deriveContradictions.
- F5: one test pins the per-settlement-unit scope rule.
- cv_inspection_result: OPEN. Tests cover only what holds under any ruling
(empty payload is none, either spelling is malformed until pinned) and
an it.todo marks the decision.
114 single-site mutants of evidence-level.ts (trust-domain comparison,
unknown/empty executor blocks, bundle-wide fabrication, each pinned field,
malformed/none handling, the gateway ASCII fold, the principal pattern,
input validation, list membership) each turn at least one test red.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…the evidence-level contract header The header listed the verdict-pinning rule but not which types are pinned. Say it where a reader of the contract will see it: instrument_result and batch_sample_result are pinned, photo_comparison_result has no producer, and cv_inspection_result is NOT pinned yet (the producers emit `passed`, types/dpp.ts reads `pass`), so it proves no level. Comment-only change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…`passed` (E5 round 2, D1) Lane decision D1. Every real producer (kernel PhotoCameraAdapter and MockCameraAdapter, the onboard-kit camera templates) and every reader except types/dpp.ts (the baseline module, the oracle J4 mirror, sensors profile-admission) use `passed`. types/dpp.ts:462 reads `pass`, which no producer writes; that reader bug is routed to its owner and is not touched here. - cv_inspection_result is pinned to `passed`, a boolean: true is pass, false is fail, any other present value is malformed, and a payload that carries only `pass` is malformed (pass is not the pinned field). - The OPEN it.todo becomes real rows: pass, fail, none, malformed (string, number, null, undefined, array, object), the `pass` spelling, and a valid `passed` beside a stray `pass` (the pinned field decides). - cv now proves inspected_output through an independent trust domain, and a failed cv inspection is a "fail" in contradictions, not a malformed one. - The contract header and the PINNED_VERDICTS comment state the pin. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…per-event facts (E5 round 2, D2) Lane decision D2. prepareBundles validated events[j] but the fabrication check, the execution scan, the level pass and deriveContradictions each re-read the live array and each event's type/source/payload, so a Proxy array or an accessor that answers differently on each read could make two passes disagree about one event. - prepareBundles now reads bundles.length and events.length once, each element once, and each event's type, source and payload once, then source.deviceId, source.simulated and payload.mock once each, and builds ONE frozen facts record per event: type, deviceAttributed (with the gateway fold), fabricated (the canonical isFabricated predicate over the values already read) and verdict (the inspection verdict logic run on the payload already read). A bundle's fabricated flag is "any event fabricated"; holdsExecutionEvent comes from the facts. - Levels and deriveContradictions use ONLY the facts. - The verdict logic is now a function of a payload already read (verdictOfPayload): one prototype read, one snapshot of the own key names, one descriptor read of the pinned field, never a getter call. - The public inspectionVerdict(event) keeps reading type and payload once each. Lengths must be non-negative safe integers or the input is refused. - Tests: getters and Proxy arrays that answer differently on each read (the first read of type says execution_completed, later reads say execution_failed) cannot split the contradiction rule from the level; read counters prove one read per field; a Proxy payload shows one prototype, key and descriptor read; fabrication equals isFabricated. 8 of the 12 new tests fail against the previous module. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…vel the bundle API lacked (E5 round 2, D3)
Lane decision D3. sensors profile-admission.ts:650 needs the level of each
event and evidenceLevelOfBundle's replacement returned only the maximum.
- evidenceLevelsOfEvents(bundles, context?) returns a frozen array, in
input order, of frozen { bundleIndex, eventIndex, level } records, one
per event. It is computed from the same per-event facts and rules: every
event of a fabricated bundle is null, an inspection is inspected_output
only through an independent authenticated trust domain, and so on.
- evidenceLevelOfBundles is now the maximum over evidenceLevelsOfEvents, so
there is ONE implementation of the level rules and the two cannot
disagree.
- Exported through the evidence barrel (export *); the barrel comment names
it.
- Tests: record shape, frozenness, order and indices, each class of event,
fabricated bundles, per-bundle independence, empty input, input
validation and a single read, the barrel export, and the agreement
between the two functions over 1536 combinations of trust domains,
assignments, verdicts and fabrication.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… after an ASCII fold (E5 round 2, D4)
Lane decision D4. Verdict-key detection was an exact-spelling closed set, so
`Pass`, ` passed` or `Status` read as "no verdict" and a failure could hide
under a spelling the module did not recognise.
- Detection of verdict-like keys, and of the pinned field's presence, now
folds ASCII case and trims ASCII whitespace on the payload's OWN key names
(enumerable or not; symbols ignored). `Passed`, ` passed`, `PASS` and
`Status` all count as present.
- A folded match of the pinned field that is not the EXACT key is
malformed: `{Passed:true}` on cv_inspection_result is malformed, not a
pass, and so is `{passed:true, Passed:false}` (two readings). Another
verdict-looking key beside a valid exact pinned field is still ignored:
the pinned field decides.
- The fold is ASCII only (A-Z lowered, space/TAB/LF/VT/FF/CR trimmed), never
toLowerCase: a Unicode lookalike such as the Kelvin sign in `oK` (which
Unicode lowercasing turns into "ok") or a full-width `pass` does not match.
A key longer than any verdict name after the trim is skipped without
building a folded copy, so the fold stays linear.
- The gateway-stamp comparison now shares the same asciiFold helper (same
behaviour, one implementation).
- Still one read: a snapshot of the own key names, one prototype read, one
descriptor read of the exact pinned field.
- Tests: every verdict name in eight ASCII spellings, per-type pinned
spellings (with and without the exact key beside them), conflicts, stray
keys beside a valid pinned field, Unicode lookalikes, symbol and
non-enumerable keys, 200k-character padded keys, and the level and
contradiction consequences. 9 of the 10 new tests fail against the
previous module.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… after D2-D4 (E5 round 2) The round-2 mutation pass over the committed D4 head (171 mutants) left three survivors that were real test gaps: - G21: a fractional `length` was only refused because an index happened to be missing. The test now uses a Proxy that answers every index with a valid event, so only the length can refuse 0.5 and 1.5. - H16: the prototype-read count was only proved on the Object.prototype branch of the plain-object check. The trap test now also runs a null-prototype payload, which reaches the second branch, and asserts one getPrototypeOf either way. - H20: a lying length on `executorTrustDomains` was untested. A Proxy that answers length 1 then 0 must still yield the assigned executor, and the log must show one length read and one element read. Tests only; no source change. The remaining non-kills are the two mutants that cannot be observed through the public API (internal records not frozen; the redundant accessor check) and Reflect.ownKeys, whose symbol keys fold to the empty string and match nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…nt verdict-looking key beside a valid pinned field is ignored
D4 makes a non-exact spelling of the pinned field malformed (even beside
the exact key). The converse is part of the contract too and was only in
the verdictOfPayload comment: a DIFFERENT verdict-looking key beside a
valid exact pinned field (for example `{pass: true, Status: "FAIL"}` on
instrument_result) is ignored, because the pinned field decides. Comment-only
change; the mutation pass ran on c4c5e66.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…s malformed
Lane decision on fixer-juliet's round-2 interpretation: with the exact
pinned field valid, another verdict-looking key (folded) was ignored, so
cv {passed:true, pass:false} read as a pass here while types/dpp.ts,
which reads pass for cv, reads FAIL. Two readers, two answers from one
payload. Such a payload now carries two claims and fails closed: no
level, and a failed inspection in deriveContradictions. No real producer
emits a second verdict key (fixer-juliet's producer inventory: the
cameras emit passed plus confidence/findings/hashes/model).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…rier legs included (E5b) Cross-family round E5b on 9c86a0f, the one open HIGH: the courier events (pickup is submitted, delivery is device-reported) were missing from EXECUTION_EVENT_TYPES. So a courier operator's own instrument_result, signed in its domain beside its courier_delivery_confirmed, was inspected_output when it was not the assigned executor. Reproduced first at 9c86a0f: the reviewer's case and a pickup variant both returned inspected_output. - EXECUTION_EVENT_TYPES adds both courier events, plus the executing party's own records: workflow_step_completed, printer_log_captured, printer_job_verified, process_log_summary, log_hash_chain_entry. - New NON_EXECUTOR_EVENT_TYPES: the inspections, plus the telemetry, captures, custody, integrity and device-lifecycle records an independent observer may emit. The two lists partition the vocabulary, and a test fails if a new type is not ruled on. - The executor set is the union of EXECUTION_EVENT_TYPES with every submitted and device-reported type, so "took or finished the work" identifies an executor even if a list is edited carelessly. spec 922; tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…an executor (E5c) Cross-family round E5c on bb20068, the one open HIGH: custody_handoff_confirmed was ruled non-executor, yet its only producer is the print-and-mail driver doing the seal and drop-off (gateway print-and-mail-handoff.ts). So the driver's domain could self-inspect to inspected_output. Reproduced first: the reviewer's case returned inspected_output. Rather than move one type, every non-executor ruling was audited against its producers in this repo: - custody_handoff_confirmed and photo_captured have one producer, the driver handoff, so they identify an executor; - custody_sealed and custody_handoff_initiated have no producer, but sealing and handing off are a leg's work, so they identify an executor too (fail closed; a future inspector-receipt flow needs its own type); - camera_snapshot stays non-executor: independent inspection cameras emit it, and the driver flow always also emits custody_handoff_confirmed. The producer audit is written into the NON_EXECUTOR_EVENT_TYPES doc. Tests: the reviewer's case, the photo and the other custody events, and an independent camera_snapshot. spec 925; tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…ets master Brings in #459 (the relay binds a signed document to its job and kernel) and the rest of master, so the subject binding is tested against current code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…LO-EV-9 #459's test (adk #4322, rule 5) presented a session bundle signed for job A as job B with no events or subject. Merged with #341 (LO-EV-9), the slot is refused earlier as missing-subject, so the test no longer exercised rule 5 (found by #341's first full CI on master: build-and-test, 1 failure). The test now binds the events to job B, the job being settled, so the subject binding passes, and signs them with a session key whose delegation names only job A. The scope check refuses it: contract_not_allowed. The whole-bundle replay (job A's bundle for job B) stays covered by the kernel-sdk subject test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
… canonicalize (verbatim from 8dc6ef2) E11 (astra, DO-NOT-SHIP on #341 @51dbabd2) found that the subject binding reads caller objects live: an event.hash getter or Proxy answers the checks one way and the bundle hash another (F2), and a throwing getter or Proxy makes the verifier throw (F3). Sensors closed the same class on #336 with one plain-data copy at the boundary and intrinsics captured at load (astra packs 158-171, 171 SHIP). The steward suggested reusing it (#5232), and sensors reproduced realm-mutation forgeries against subject-binding.ts itself (#5381). These six files are byte-identical to #336 @8dc6ef2b, so #336's merge-up of #341 stays clean: - packages/spec/src/util/primordials.ts (new); - packages/spec/src/util/plain-data.ts (new): plainDataCopy, and isProxy from a static node:util import; - packages/spec/src/util/canonical.ts: canonicalize calls only captured intrinsics, with byte-identical output for JSON data; - apps/dashboard/src/lib/node-util-shim.ts (new) and the vite alias, so the dashboard build resolves node:util; - packages/spec/src/__tests__/plain-data-prototype.test.ts (new). The next commit routes verifyEvidenceSubjectBinding through them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…throws, and binds unit and challenge both ways (E11 F1-F3) astra's E11 on #341 @51dbabd2 was DO-NOT-SHIP. All three findings were reproduced at 51dbabd before any change: /mnt/sparkbulk/tmp/evidence-341-e11-repro-51dbabd2.txt has 3 of 3 failing. F1 (HIGH). A subject that names no settlement unit accepted evidence that commits one. The legacy /complete and /resume-settlement build exactly that subject ({jobId, kernelId}) and drive milestone 0, so evidence signed for U3/N3 could anchor them. Unit and challenge are now matched exactly in both directions: - named by the subject, every event commits the value; - not named, no event may commit one (unit-not-in-subject, challenge-not-in-subject). A consumer that cannot name the unit it settles therefore cannot accept unit-scoped evidence. The output stays one-way: it is content, not scope. F2 (HIGH). event.hash was read three times, so a getter or Proxy could answer the checks with B's hash and the bundle with A's. F3 (MEDIUM). Field reads sat outside the try, so a throwing getter or Proxy rejected the promise. For F2 and F3, every field is read once: - the input's three fields, the subject's five and each event's six through own data descriptors; - source and payload through #336's plainDataCopy. A Proxy, an accessor, a hole or non-JSON data is refused without being run. All checks and hashes then read the copies only, and the whole body is wrapped, so every input resolves to a refusal. Realm mutation (sensors' residual on this file, bus #5381). The checks call only intrinsics captured at load: - primordials; - canonicalize; - node:crypto's SHA-256, captured and synchronous, so nothing is awaited inside. There is no sort, iterator protocol, RegExp or JSON.parse. Results and copies have null prototypes and are frozen, so Object.prototype.then cannot rewrite an answer. Tests: - subject-binding.test.ts: the F1 unit test flips, plus 4 more F1 cases and 6 F2/F3 cases: astra's getter and Proxy reproductions, nested Proxy and accessor, throwing getters at every read, never-throws inputs, frozen null-prototype results. - subject-binding-realm.test.ts (new): 33 after-load patches x 9 cases leave every answer unchanged, and a source scan finds no ambient method, iterator, RegExp or await. - The scratch reproduction file is deleted. spec: 50 files, 1206 tests pass; tsc --noEmit is clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
… patch held until the answer settles Each recipe forged an ok out of 51dbabd's binding leg. They were reproduced before the fix (/mnt/sparkbulk/tmp/evidence-341-realm-repro-51dbabd2.txt): - a targeted Array.prototype.sort answers job A's signed hashes for re-hashed job B events; - JSON.parse answers a snapshot that commits job B; - an Object.prototype.then getter forges the refusal into ok; - SubtleCrypto.prototype.digest hashes B's content as A's; - a replaced array iterator skips the unit and challenge checks. At this head all five refuse. Run against 51dbabd's subject-binding.ts and canonical.ts, all five fail (/mnt/sparkbulk/tmp/evidence-341-r4-recipes-vs-51dbabd2.txt). The patches are targeted, so vitest's own use of each intrinsic keeps working while the patch is held. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
… anchor /complete or /resume-settlement
/complete and /resume-settlement verify device evidence against
{ jobId, kernelId } and drive milestone 0 of the job's per-job escrow. A
bundle whose events commit settlement unit U3 and challenge N3 used to
anchor them (astra E11 F1).
LO-EV-9 now matches unit and challenge exactly in both directions, so these
routes refuse such evidence and settle on the gateway fallback. Recovery
refuses a unit-scoped row pinned before the fix.
device-evidence-settlement.test.ts:
- resolveSettlementEvidence on the exact /complete subject falls back with
unit-not-in-subject;
- a U4/N4 consumer gets unit-mismatch;
- the U3/N3 consumer anchors on the device (positive control);
- verifyPinnedSettlementEvidence refuses a pinned U3/N3 row.
paid-job-flow-evidence-binding.test.ts:
- /complete does not anchor or pin a U3/N3 bundle, while a unit-less bundle
from the same node still anchors;
- /resume-settlement answers 409 unit-not-in-subject for a unit-scoped pinned
row, and the job stays at evidence_submitted.
Comments at both subject sites say why they name no unit. No code change in
the gateway.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…r master moved #345 (must-close 5, evidence levels) was SHIP at 8c5d68c. It went CONFLICTING once master merged #338's signing-preimage export, and it dropped off MERGE NOW. The only conflict was packages/spec/src/evidence/index.ts. Both exports are kept: master's `export * from "./signing-preimage.js"`, then #345's `export * from "./evidence-level.js"`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…erge-up after #341 merges The steward's merge-tree simulation (#5833): #345 conflicts with #341 in packages/spec/src/evidence/index.ts. Both add an `export *` at the same place. Resolution: both exports are kept. #341's subject-binding.js comes first, then #345's evidence-level.js. Spec: 51 files, 1339 tests pass; tsc is clean. This is a pre-stage branch. #345's own branch stays frozen at ad3d4a5 until #341 merges; then this merge becomes #345's merge-up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
… a criss-cross (steward #6415) A plain merge, no edits: its tree equals git merge-tree of d94da4d and master. #438 carried #345's pre-staged head, and #345 merged as a merge commit, which left two merge bases (cd9d877, 03d4e46). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
The one conflict was #345's comment in evidence/index.ts; master's comment is kept, with #363's profile-admission export. profile-admission.ts still calls #345's removed device-id API (evidenceLevelOf, evidenceLevelOfBundle, executingDeviceIds), so it does not compile at this commit. The next commit migrates it to the trust-domain rule (steward #6478).
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…ne rule (N24, steward #6478) #345 replaced the device-id independence rule that admission called (evidenceLevelOf, evidenceLevelOfBundle, executingDeviceIds) with one rule over AUTHENTICATED bundles: independence between trust domains, the rule the oracle signs on. Admission now uses it. It does not keep a private copy of the old rule. - New input executorTrustDomains: the operators the deal assigned, from the accepted deal (as subject comes from the job record). It must be a list of operator principal ids; absent or malformed rejects as input-unreadable. - The signature leg answers { trustDomain }: the verified signer's operator principal, or null when the registry names none. false, a throw, a bare true, a malformed principal or an accessor fails it (unauthenticated-bundle). - Levels come from evidenceLevelsOfEvents and contradictions from deriveContradictions, both over the authenticated bundles. An event present in several bundles counts at the LOWEST level any copy gets (steward #6478), and reached is the highest of those. - An inspection counts only with a pass under evidence's pinned verdict field (inspectionVerdict), not payload.passed: instrument_result is `pass`. Tests: the pilot world has two operators, the assigned executor (A) and an independent inspector (B). New cases cover the lowest level across copies, the leg's answer, the executor-domain checks, assignment of the inspector's own operator, and contradiction across domains. 116 of 116. Mutations: 10 of 11 killed; the survivor ("null is lowest") is equivalent here, because a null copy needs a fabricated bundle, which admission refuses first (simulationProhibited is always true). Spec suite: 2529 of 2529.
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…to the realm-hardened admission #363 moved profile admission to #345's one rule (steward #6478). The deal's executorTrustDomains are an input. The signature leg answers { trustDomain }. Levels and contradictions run over authenticated bundles, each event counts at its LOWEST level, and inspections pass by the pinned verdict field (inspectionVerdict). #519 had made admission hold under post-load realm mutation. This merge carries the migration into #519's hardened file, so both hold. Conflicts and how each was resolved: - evidence-level.ts: #577's file (426be44), the realm port of #345's final file. It already freezes EVIDENCE_LEVELS, #363's only change to that file, and master has not touched the file since #577 forked. E5's evidence-level.test.ts is #577's too, for the two expectations #577 changed. - profile-admission.ts: #519's hardened file with the migration applied in its style: - executorTrustDomains is read as data with the other fields (INPUT_FIELDS, DATA_FIELDS, plainDataCopy) and checked by isOperatorPrincipalId. That is a code-unit predicate equal to principal-id.ts parseOperatorPrincipalId, with no RegExp; it is exported, and a test holds the two equal on edge cases and 20,000 near-misses. - The signature leg's answer is read once from its own data (signerOf) into a frozen null-prototype record. A proxy, an own then, an accessor or a malformed domain fails it. - A native promise is followed by the then captured at load (followedPromise, new in util/primordials.ts), and its value is read inside the handler. - A promised answer must have no prototype (signedBy, exported). Resolving an ordinary object looks then up on Object.prototype, where code running after load could substitute the answer. A synchronous plain answer is still accepted, since nothing resolves it. - Levels and contradictions run over null-prototype AuthenticatedBundles. The lowest level per event hash is kept in a null-prototype record. reached is the highest of those. - Tests: - #363's two-operator world, with the auto-merged helper's duplicated executorTrustDomains removed. - #519's pack-187 inputs now carry executorTrustDomains, and the binding-lookup test uses the two-bundle pilot. - New cases cover the signature leg under a then getter planted on Object.prototype, signedBy, a promised ordinary record, a thenable or proxy answer, and the executors refused at the input boundary (no leg runs). - The realm harness: - It moves to the two-operator world (the printer in A's bundle, every other device in B's) and the new leg contract. - It gains cases for the new leg (a signedBy async leg, a promised ordinary record, a bare true, a null domain, the executor's own camera, no or malformed executors, a thenable). - Its evidence-level items are dropped: #577's harness (evidence-level-realm.ts) holds the levels under realm mutation. Spec 2676 of 2676, admission 249 of 249, tsc clean. Mutations: 19 of 20 killed. The survivor, "null copy not lowest", is equivalent, as in pack 271: copies of one event differ in level only when a bundle is fabricated, and admission refuses fabricated events first.
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…urrences, as #345 does (steward #6623, evidence #6559) The duplicates ruling is option (a): admission keeps no policy of its own, and takes #345's per-occurrence semantics. Each occurrence is levelled by its own bundle, and an event counts at the highest. This reverses the "lowest copy" rule of steward #6478. With one domain per signer, only a copy in a truly independent signer's bundle can lift an event. - reached is now #345's own evidenceLevelOfBundles over the authenticated bundles (the maximum over every occurrence). - The per-event level the observation loop counts is the max of evidenceLevelsOfEvents over the event's occurrences (higherLevel; null is the lowest). Tests: - The duplicate case flips: the camera's inspection in both the executor's and the independent inspector's bundle now admits at inspected_output. With only the executor's copy it is device_reported. - A parity test holds admission's reached equal to evidenceLevelOfBundles, and the counted level equal to the max of evidenceLevelsOfEvents, on the duplicate case.
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…ng witness grant is named (steward #6694) Two additions to round 13's grants: - A key that signs as the subject's executor is the executor's own, never an independent inspector. #345 builds its executor set from the deal's executorTrustDomains plus the operators of bundles holding execution events. So the kernel's own key, whose operator the deal left out, could sign an inspections-only bundle that counted as independent: #345 on the deal's executors alone says inspected_output (now a test). Admission now passes #345 the executor set completed by roles: every executor-role bundle's operator joins it, and an executor key whose operator the registry does not name leaves no inspection independent. A deal that names no executor still shows no independence, as before. - When the profile requires inspected_output and no pinned row grants a witness for the subject, the shortfall is no-witness-authorized (under onMissingData), not level-not-reached. No registry record assigns witnesses yet (N132), so inspected_output through a witness waits for it visibly (steward #6694). Admission 134/134, spec 2563/2563, tsc clean; 8 of 8 mutations for this change killed, and round 13's set re-run.
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…ned admission #363's pinned key registry (astra pack 275) and the steward's max-over-occurrences ruling (#6623), ported into #519's admission: - registryKeys and pinnedRegistryDigest are read as data at the input boundary and walked for code; rows go into null-prototype records, checked by isRegistryKey (a code-unit predicate equal to ^0x[0-9a-f]{64}$), unique by key and by signer id; - the registry digest is the captured SHA-256 over canonicalize({domain, keys}); - each bundle's Ed25519 signature is checked here, synchronously, through node:crypto's verify as captured at load, with its key options in a null-prototype record. No promise is on the signature path: Web Crypto resolves importKey with a CryptoKey object, and that resolution looks `then` up on Object.prototype (the realm harness reproduced a forged admit through an awaited helper first); - the signer leg, its snapshot, isSignerId and followedPromise are gone; - the per-event level is the max over occurrences, and reached is #345's. The realm harness moves to the registry (seeded keys, registry cases, node:crypto and options-pollution scenarios); the intrinsics test checks isRegistryKey.
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…ned admission #363's signer grants (astra packs 281, 285, 287; steward DECISIONS 00:26 and #6694), ported into #519's admission: - registry rows carry grants { role: executor | witness, kernelId, jobId? }, and session-key rows { publicKey, delegatedBy, authorization } are rooted in a registered row. They are read through own descriptors and checked with Reflect.ownKeys as captured at load, and become frozen null-prototype records. Grants are sorted by a JSON-quoted identity that orders as #363's does, so the v2 digest is byte-identical; - the delegation's root signature is checked synchronously through node:crypto's verify, captured at load, over the LO-EV-1 delegation preimage rebuilt here: - a fixed key order; - strings quoted by JSON.stringify as captured at load; - safe-integer numbers; - lists sorted by code unit; - lowercase key hex; - UTF-8 encoded code unit by code unit; - authorization after binding: a grant must name the subject, a witness signs inspections only, and a session key counts within its scope and window. The window uses Date.parse as captured at load and floors whole seconds by arithmetic; - the executor set for #345's levels is completed by executor-role operators, and no-witness-authorized names an inspected_output shortfall with no witness granted; - computeRegistryDigest reads a plain-data copy. The realm harness gets the round-13 cases with seeded session keys and new scenarios: grant-field pollution, plus JSON.stringify and Date.parse (IDENTICAL-strict), Math.floor and toLowerCase. New tests show the rebuilt delegation bytes equal LO-EV-1's: unicode, a lone surrogate, unsorted lists, either-case hex, a derivation path. Pack 287's LOW (a test comment that did not match its assertion) is reworded here as the tracked follow-up.
LamaSu
added a commit
that referenced
this pull request
Oct 6, 2026
fix(spec): the evidence levels hold under post-load realm mutation (#345's final file)
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.
What this closes (technical pack §3, must-close 5)
It keeps three kinds of evidence apart:
submitteddevice_reportedinspected_outputPR #343 makes pcc-node report acceptance as
execution_progressat levelsubmitted. This PR gives every consumer the one classification, so none of them can read an accepted-only job as a completed one.Rules
EVIDENCE_EVENT_TYPESis ruled on, including those that prove no outcome level. A test fails if a new vocabulary type arrives without a ruling.submitted:gcode_received,gcode_loaded,method_loaded,execution_progress,courier_pickup_confirmeddevice_reported:execution_completed,digital_task_completed,batch_session_completed,courier_delivery_confirmedcv_inspection_result,photo_comparison_result,instrument_result,batch_sample_resultinspected_outputonly when the events name the executing devices and the observer is not one of them.device_reported: a device measuring its own output, or an observation whose independence can't be shown.printer_job_verifiedproves no level. It is a log-stream summary with no success field (evidence vocabulary ruling, 2026-09-07).isFabricated) prove no level, and a fabricated execution event can't make an inspection look independent.GATEWAY_STAMPED_DEVICE_ID,"gateway", @ba31ea65).PUT /api/jobs/:jobId/completewrites its ownexecution_completedand the caller'sevidenceEvents, of any type, under that id; no device reported them. They also name no executing device, so the stamp can't make a device's inspection of its own output look independent.deriveContradictions(events)(@cb81284f) is the one public contradiction rule the oracle signs rejects on (J4):completion-and-failureandcompletion-and-failed-inspection, with fabricated events ignored. Event types decide there, so a gateway-stamped completion still counts: a contradiction can only refuse.inspected_output. Failure and contradiction stay with the committed program (execution_failedabsence) and the measurement profile's policy.Consumers
profileAdmitsBundle(sensors, stacked on feat(spec): MeasurementProfileV1 — capture parameters committed before execution (LO-SE-2) #336): imports this for theacceptanceLevelcheck.AcceptanceLevelinmeasurement-profile.tshas the same three names and can aliasEvidenceLevel.verifyEvidenceSubjectBinding(feat(evidence): LO-EV-9 bind device evidence to the accepted job and kernel before settlement #341).Tests
tscclean.ba31ea65: spec 826/826; spectscclean. Dropping the gateway clause fails both new level tests.Based on master (no dependency on #338 / #341).
🤖 Generated with Claude Code
https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS