@integraledger/lcp-verify 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,277 @@
1
+ /**
2
+ * `verify` — the walk at the caller-specified `depth` (default `"structural"`). Pure over
3
+ * its supplied inputs (the fetched ATR bytes, the enumerated settlements, the ATA chain, the acceptance)
4
+ * — the imperative shell that fetches them via live ports is the buyer gate's. The report is honest about
5
+ * coverage: a step whose inputs are absent is `not-attempted(<why>)`.
6
+ *
7
+ * At STRUCTURAL depth `verified` is always `false` (a presence/absence readout → class, not a verdict).
8
+ * At MECHANICAL depth `verified` is an honest function of the walk via `computeVerified` — raised `true`
9
+ * iff every class-required step is `proved` and no step `failed`.
10
+ *
11
+ * `supportedClass` is an HONEST READOUT, never a target: the record is *shaped for* `claimedClass`, and
12
+ * the structural walk can only CONFIRM-OR-IMPEACH that shape — any `failed` step downgrades it to `TC-0`;
13
+ * the walk never raises it (raising requires the mechanical verification `computeVerified` supplies).
14
+ */
15
+ import type {
16
+ Bounds,
17
+ ChainWalkResult,
18
+ SignatureVerifier,
19
+ SignedAcceptance,
20
+ } from "@integraledger/lcp-authority";
21
+ import {
22
+ type CompositionInput,
23
+ discoveryIntegrityStep,
24
+ frcNonGatingStep,
25
+ offerBoundStep,
26
+ operationsStep,
27
+ proportionalityStep,
28
+ } from "./composition.js";
29
+ import type {
30
+ StepOutcome,
31
+ VerificationReport,
32
+ VerificationStep,
33
+ VerifyDepth,
34
+ } from "./report.js";
35
+ import {
36
+ computeVerified,
37
+ type StepName,
38
+ type TransactionClass,
39
+ } from "./required.js";
40
+ import {
41
+ type AuthorityLink,
42
+ acceptanceStep,
43
+ authorityStep,
44
+ authorityStepFromWalk,
45
+ commitmentStep,
46
+ fingerprintStep,
47
+ type PlacementInput,
48
+ type RecordIdentity,
49
+ recourseStep,
50
+ referencePlacementStep,
51
+ resolvePartyStep,
52
+ settlementStep,
53
+ } from "./steps.js";
54
+
55
+ export {
56
+ type CompositionInput,
57
+ type CompositionReadout,
58
+ discoveryIntegrityStep,
59
+ frcNonGatingStep,
60
+ offerBoundStep,
61
+ operationsStep,
62
+ proportionalityStep,
63
+ readCompositionSlots,
64
+ } from "./composition.js";
65
+ export {
66
+ jcsCanonicalize,
67
+ type StepOutcome,
68
+ serializeReport,
69
+ type VerificationReport,
70
+ type VerificationStep,
71
+ type VerifyDepth,
72
+ } from "./report.js";
73
+ export {
74
+ computeVerified,
75
+ REQUIRED_STEPS,
76
+ type StepName,
77
+ type TransactionClass,
78
+ } from "./required.js";
79
+ export {
80
+ type AuthorityLink,
81
+ acceptanceStep,
82
+ authorityStep,
83
+ authorityStepFromWalk,
84
+ commitmentStep,
85
+ fingerprintStep,
86
+ type PlacementInput,
87
+ RCS4_REQUIRED_ROLES,
88
+ type RecordIdentity,
89
+ recourseStep,
90
+ referencePlacementStep,
91
+ resolvePartyStep,
92
+ settlementStep,
93
+ } from "./steps.js";
94
+
95
+ /**
96
+ * Everything a verification walk is given. **The caller gathers the inputs; `verify()` opens no network
97
+ * connection of its own** — it takes `atrBytes`, not a reference to fetch, and takes `settlements` rather
98
+ * than enumerating them. That is what makes the walk pure and reproducible.
99
+ *
100
+ * Nearly every field is optional, and this is the type's central property: an absent input yields an
101
+ * `indeterminate` or `not-attempted` step, never a pass. `coverage` is how the report stays honest about
102
+ * that — it states which ports and bindings were actually supplied, so a thin walk cannot read as a
103
+ * thorough one.
104
+ */
105
+ export interface VerifyInput {
106
+ /** The settlement's chain-anchored time (every validity check is evaluated against it). */
107
+ asOf: string;
108
+ /** Which ports/bindings were supplied — coverage is honest depth. */
109
+ coverage: { ports: string[]; bindings: string[] };
110
+ /** The retrieved ATR bytes (absent ⇒ the fingerprint step is indeterminate). */
111
+ atrBytes?: Uint8Array;
112
+ /** The atrHash the settlement committed (nonce/salt) — the fingerprint is checked against it. */
113
+ settledAtrHash?: string;
114
+ /** Settlements found for the fingerprint across supplied forward-indexable bindings. */
115
+ settlements?: unknown[];
116
+ /** The buyer's presented signed acceptance over the fingerprint (TRM-6). */
117
+ acceptance?: SignedAcceptance;
118
+ /** The port that checks the acceptance's signature. Absent ⇒ the acceptance step cannot prove — a
119
+ * signature nobody checked is not evidence of a signature. The EVM impl is `binding-evm-common`'s. */
120
+ acceptanceVerifier?: SignatureVerifier;
121
+ /**
122
+ * The ATA chain to check for attenuation + as-of revocation/expiry, ALREADY FLATTENED.
123
+ *
124
+ * Whoever produced this array is a trusted oracle — every field on a link is a derived fact the step
125
+ * takes at face value. Prefer `authorityWalk`, which removes that trust. This door stays open because
126
+ * a foreign conformance subject may legitimately derive its links some other way.
127
+ */
128
+ authorityChain?: AuthorityLink[];
129
+ /**
130
+ * The custody walk's readout (`authority.walkChain`) — the PREFERRED authority input.
131
+ *
132
+ * `authorityChain` cannot express what the walk found: a spliced link, an issuer discontinuity, a root
133
+ * issued by someone other than the declared principal, a leaf granted to anyone but the acceptance
134
+ * signer. Flattening a `refused` walk throws those away and hands over links that then read as a clean
135
+ * `not-attempted`, so the report says the record simply did not carry a chain when in fact it carried
136
+ * a contradictory one. Supplied here, the refusal reaches the report WITH its halt class, and the
137
+ * walk's own gap depths pass through verbatim.
138
+ *
139
+ * Mutually exclusive with `authorityChain` — supplying both is a contradiction, not a precedence
140
+ * question, so it throws rather than silently picking one (the same ruling `makeProposalRecord` makes
141
+ * on its own exactly-one-of invariant).
142
+ */
143
+ authorityWalk?: ChainWalkResult;
144
+ /** The accepted commitment vs the leaf grant's bounds (ATA-4). */
145
+ commitment?: { commitment: Bounds; leafBounds: Bounds };
146
+ /** Both parties' resolutions, each at a stated assurance (IDN-1/IDN-3) — the record's `identity` slot. */
147
+ identity?: RecordIdentity;
148
+ /** The retained evidence package's manifest roles (RCS-4 completeness; `RCS4_REQUIRED_ROLES` is the floor). */
149
+ evidenceRoles?: readonly string[];
150
+ /** The class the record is shaped for (default TC-2). The taxonomy is closed (TC-0..TC-4); a caller
151
+ * at a trust boundary validates untyped input before calling (Zod there, not here). */
152
+ claimedClass?: TransactionClass;
153
+ /** Verification depth (default "structural"). Mechanical depth lets `verified` be raised honestly. */
154
+ depth?: VerifyDepth;
155
+ /** The TC-4 commerce-plane slots. Absent ⇒ the composition steps are NOT appended to the walk
156
+ * (TC-0..TC-3 reports are byte-identical). Present ⇒ the four required composition steps + the
157
+ * impeachment-only `frc-non-gating` step are appended and flow through the existing machinery. */
158
+ composition?: CompositionInput;
159
+ /** The reference recovered from a commerce protocol's own document. Absent ⇒ the
160
+ * `reference-placement` step is NOT appended and every existing report stays byte-identical. Present ⇒
161
+ * the step is appended and flows through the same machinery as every other: it impeaches when it FAILS
162
+ * and never blocks by its absence. REPORTED, never REQUIRED — no class lists it. */
163
+ placement?: PlacementInput;
164
+ }
165
+
166
+ /** Run the walk at the caller-specified `depth` (default `"structural"`) and emit the vector-anchored, JCS-serializable report. */
167
+ export async function verify(input: VerifyInput): Promise<VerificationReport> {
168
+ if (input.authorityWalk !== undefined && input.authorityChain !== undefined)
169
+ throw new Error(
170
+ "verify: authorityWalk and authorityChain are mutually exclusive — a walked chain is already the readout, and flattening it alongside would ask which of two answers about the same chain to believe",
171
+ );
172
+ const steps: VerificationStep[] = [
173
+ {
174
+ name: "atr-fingerprint",
175
+ outcome: await fingerprintStep(input.atrBytes, input.settledAtrHash),
176
+ },
177
+ {
178
+ name: "settlement-enumeration",
179
+ outcome: settlementStep(input.settlements),
180
+ },
181
+ {
182
+ name: "buyer-acceptance",
183
+ outcome: await acceptanceStep(
184
+ input.acceptance,
185
+ input.settledAtrHash,
186
+ input.acceptanceVerifier,
187
+ ),
188
+ },
189
+ {
190
+ name: "authority-attenuation",
191
+ // One step name either way: the report's shape does not change with how the caller obtained its
192
+ // chain, only how much the walk was able to say about it.
193
+ outcome:
194
+ input.authorityWalk !== undefined
195
+ ? authorityStepFromWalk(input.authorityWalk)
196
+ : authorityStep(input.authorityChain),
197
+ },
198
+ { name: "commitment-vs-leaf", outcome: commitmentStep(input.commitment) },
199
+ {
200
+ name: "recourse-elections",
201
+ outcome: recourseStep(input.atrBytes, input.evidenceRoles),
202
+ },
203
+ { name: "resolve-party", outcome: resolvePartyStep(input.identity) },
204
+ ];
205
+
206
+ // Append the TC-4 composition steps ONLY when a composition slot is supplied — the slot is additive:
207
+ // a TC-0..TC-3 report that supplies none is byte-identical to one produced without the slot existing.
208
+ // The appended steps flow through the same `anyFailed`/`computeVerified` machinery as every other.
209
+ if (input.composition !== undefined) {
210
+ steps.push(
211
+ { name: "offer-bound", outcome: offerBoundStep(input.composition) },
212
+ { name: "operations-bound", outcome: operationsStep(input.composition) },
213
+ {
214
+ name: "discovery-integrity",
215
+ outcome: discoveryIntegrityStep(input.composition),
216
+ },
217
+ {
218
+ name: "proportionality-declared",
219
+ outcome: proportionalityStep(input.composition),
220
+ },
221
+ {
222
+ name: "frc-non-gating",
223
+ outcome: frcNonGatingStep(input.composition),
224
+ },
225
+ );
226
+ }
227
+
228
+ // Append the reference-placement step ONLY when a placement slot is supplied — its own `if`, because a
229
+ // placement is independent of a composition and a record may carry either, both, or neither. Absent ⇒
230
+ // nothing is appended and existing reports stay byte-identical, which is what makes the slot additive.
231
+ // Supplied ⇒ it flows through the same `anyFailed`/`computeVerified` machinery as every other step, so
232
+ // a placement that CONTRADICTS the record impeaches `supportedClass` to TC-0 — the one power the step
233
+ // grants it. Without this block the step would appear in no report and impeach nothing.
234
+ if (input.placement !== undefined) {
235
+ steps.push({
236
+ name: "reference-placement",
237
+ outcome: referencePlacementStep(input.placement, input.settledAtrHash),
238
+ });
239
+ }
240
+
241
+ const anyFailed = steps.some((s) => s.outcome.status === "failed");
242
+ const found = input.settlements ?? [];
243
+ const depth = input.depth ?? "structural";
244
+ const claimed = input.claimedClass ?? "TC-2";
245
+ const stepsForVerified = steps.map((s) => ({
246
+ name: s.name as StepName,
247
+ outcome: s.outcome,
248
+ }));
249
+
250
+ return {
251
+ verified: computeVerified(stepsForVerified, claimed, depth),
252
+ // The BUYER's stated level: the report's assurance answers "at what assurance did the party who
253
+ // committed resolve?" — the seller's own level rides in the record's identity slot and the package.
254
+ // `buyer` is optional-chained on purpose: `RecordIdentity` requires it, but this readout must be TOTAL
255
+ // over the same half-shaped input `resolvePartyStep` is total over (an untyped caller, a foreign
256
+ // conformance subject). A walk that throws on a malformed record cannot report the malformation.
257
+ //
258
+ // The fallback is NOT a ladder value. `wallet-signature-only` would be conservative in
259
+ // direction, since it never inflates, but still a level the verifier elected for a record that stated
260
+ // none, and the floor of a ladder reads as a finding rather than as silence. `no-assurance-stated` is
261
+ // the SAME token `resolvePartyStep` emits for this condition (steps.ts), so absence has one spelling
262
+ // across the step and the report instead of contradicting itself inside a single run.
263
+ assurance: input.identity?.buyer?.assurance ?? "no-assurance-stated",
264
+ supportedClass: anyFailed ? "TC-0" : claimed,
265
+ asOf: input.asOf,
266
+ steps,
267
+ coverage: input.coverage,
268
+ settlements: { found, multiplySettled: found.length > 1 },
269
+ };
270
+ }
271
+
272
+ /** Discriminant helper — did any step fail (the honest impeachment signal)? */
273
+ export function anyStepFailed(report: VerificationReport): boolean {
274
+ return report.steps.some(
275
+ (s: VerificationStep) => (s.outcome as StepOutcome).status === "failed",
276
+ );
277
+ }
package/src/report.ts ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The `VerificationReport` and its **deterministic serialization — RFC 8785 (JCS) canonical JSON**.
3
+ * The protocol promises byte-identical reports across independent implementations: the report is RECOMPUTED by
4
+ * every verifier, and JCS is exactly the multi-producer-convergence case it exists for. This is NOT the
5
+ * ATR rule and the two must never be conflated — canonicalization is banned for the *ATR* because it has
6
+ * one producer and the exact received bytes are the fingerprint; the *report* has many producers.
7
+ *
8
+ * All monetary values in reports are decimal-integer base-unit STRINGS, so JCS's
9
+ * ECMAScript number serialization never touches money. JCS = recursively sort object keys by UTF-16 code
10
+ * units (JS default string sort), then serialize with JSON's escaping + ECMAScript number formatting
11
+ * (which already match RFC 8785 §3.2.2). Confirmed byte-for-byte against an independent JCS implementation.
12
+ */
13
+ import type { HaltClass } from "@integraledger/lcp-binding-core";
14
+
15
+ /** Verification depth: structural (presence/absence readout, verified always false) vs mechanical
16
+ * (inputs gathered over live ports; verified is an honest function of the walk). */
17
+ export type VerifyDepth = "structural" | "mechanical";
18
+
19
+ /** A step outcome — aligned to `report.schema.json`. */
20
+ export type StepOutcome =
21
+ | { status: "proved" }
22
+ | { status: "failed"; haltClass: HaltClass }
23
+ | { status: "indeterminate" } // unretrievable ATR — not a failure
24
+ | { status: "not-attempted"; depth: string };
25
+
26
+ /** One named step of the walk and how it came out. The `name` is a {@link StepName} in this
27
+ * implementation but is typed as `string` here so a foreign implementation's report is representable —
28
+ * a report type that cannot hold another engine's step cannot be used to compare against it. */
29
+ export interface VerificationStep {
30
+ name: string;
31
+ outcome: StepOutcome;
32
+ }
33
+
34
+ /** The output of a verification walk — the artifact a stranger reads instead of re-running it.
35
+ *
36
+ * Read `steps` before `verified`. `verified` is a summary that is FALSE at structural depth by
37
+ * construction, so a `false` may mean "impeached" or merely "not attempted mechanically", and only the
38
+ * step list distinguishes them. `supportedClass` is what the record honestly supports, not what the
39
+ * caller asked for. Several fields are `string` rather than a union on purpose: this type has to be able
40
+ * to hold another implementation's answer, including a wrong one. */
41
+ export interface VerificationReport {
42
+ /** Honest function of depth: `false` at structural (presence/absence → class); at mechanical, raised by
43
+ * `computeVerified` iff every class-required step is `proved` and none `failed`. */
44
+ verified: boolean;
45
+ /** The stated assurance level — never a bare boolean. One of the four ladder values
46
+ * (`wallet-signature-only` | `domain-controlled` | `attested` | `legal-party`), or
47
+ * `no-assurance-stated` where the record states none. Left as `string` rather than a union for the same
48
+ * reason `supportedClass` is: a foreign implementation's report must be representable before it can be
49
+ * compared, and a type that refuses to hold a wrong value cannot report one. */
50
+ assurance: string;
51
+ /** The transaction class the record honestly supports — an honest readout, never a target. */
52
+ supportedClass: string;
53
+ /** The settlement's chain-anchored time every validity check is evaluated against. */
54
+ asOf: string;
55
+ steps: VerificationStep[];
56
+ /** Supplied ports/bindings — coverage is part of honest depth. */
57
+ coverage: { ports: string[]; bindings: string[] };
58
+ settlements: { found: unknown[]; multiplySettled: boolean };
59
+ }
60
+
61
+ /**
62
+ * Emit the canonical text DIRECTLY from the sorted key list — never by re-assigning sorted keys onto an
63
+ * object and handing it to `JSON.stringify`. JS object property order puts integer-like keys FIRST, in
64
+ * ascending numeric order, whatever the insertion order (the same engine behaviour `kernel.assemble`
65
+ * refuses integer-like slot names over), so the object round-trip silently re-sorts `{"10":…,"9":…}` to
66
+ * `9,10` while RFC 8785 §3.2.3 requires UTF-16 code-unit order — `"1" (0x31) < "9" (0x39)`. Two conformant
67
+ * implementations would then disagree byte-for-byte on the same report, which is the one thing this buys.
68
+ * Reachable through `settlements.found` (caller-supplied objects). Pinned by `vectors/report/serialization.json`.
69
+ */
70
+ function serializeCanonical(v: unknown): string {
71
+ if (Array.isArray(v)) return `[${v.map(serializeCanonical).join(",")}]`;
72
+ if (v !== null && typeof v === "object") {
73
+ const obj = v as Record<string, unknown>;
74
+ // Default Array#sort compares by UTF-16 code units — exactly RFC 8785's ordering.
75
+ const parts = Object.keys(obj)
76
+ .sort()
77
+ .map((k) => `${JSON.stringify(k)}:${serializeCanonical(obj[k])}`);
78
+ return `{${parts.join(",")}}`;
79
+ }
80
+ // Primitives: JSON's own escaping + ECMAScript number formatting already match RFC 8785 §3.2.2.
81
+ return JSON.stringify(v) as string;
82
+ }
83
+
84
+ /** RFC 8785 JCS canonical JSON string for any JSON value (keys sorted by UTF-16 code units, no whitespace). */
85
+ export function jcsCanonicalize(value: unknown): string {
86
+ // Round-trip through JSON first so `toJSON`, dropped `undefined`/function members, and non-finite
87
+ // numbers resolve EXACTLY as JSON.stringify defines them; `serializeCanonical` then sees pure JSON
88
+ // and owns only the ordering rule.
89
+ const json = JSON.stringify(value);
90
+ if (json === undefined)
91
+ throw new Error(
92
+ "value is not JSON-serializable (JCS is defined only over JSON values)",
93
+ );
94
+ return serializeCanonical(JSON.parse(json));
95
+ }
96
+
97
+ /** The report's canonical bytes (UTF-8 of its JCS form) — the byte-for-byte target. */
98
+ export function serializeReport(report: VerificationReport): Uint8Array {
99
+ return new TextEncoder().encode(jcsCanonicalize(report));
100
+ }
@@ -0,0 +1,99 @@
1
+ import type { VerifyDepth } from "./report.js";
2
+
3
+ export type { VerifyDepth };
4
+ /** The transaction-class ladder. Higher classes require strictly more steps, so a record supporting TC-3
5
+ * supports every class below it. A class is a CLAIM about what a record can support — it is never a
6
+ * quality score, and nothing in this package tries to reach a higher one. */
7
+ export type TransactionClass = "TC-0" | "TC-1" | "TC-2" | "TC-3" | "TC-4";
8
+ /** Every step the walk can append, in one closed union. Closed because `REQUIRED_STEPS` is keyed by these
9
+ * names and a typo would silently make a required step unfindable — which `computeVerified` would read as
10
+ * "not proved", failing safe but for the wrong reason. */
11
+ export type StepName =
12
+ | "atr-fingerprint"
13
+ | "settlement-enumeration"
14
+ | "buyer-acceptance"
15
+ | "authority-attenuation"
16
+ | "commitment-vs-leaf"
17
+ | "recourse-elections"
18
+ | "resolve-party"
19
+ // The TC-4 composition steps:
20
+ | "offer-bound"
21
+ | "operations-bound"
22
+ | "discovery-integrity"
23
+ | "proportionality-declared"
24
+ | "frc-non-gating"
25
+ // The reference-placement step is REPORTED, NEVER REQUIRED. Deliberately absent from every entry in REQUIRED_STEPS below,
26
+ // for the same reason frc-non-gating is: it impeaches when it FAILS (the reference found in the
27
+ // protocol's native field contradicts the record) but its ABSENCE never blocks. A protocol that never
28
+ // settles has nothing to enumerate, and letting a placement stand in for a settlement rung would let a
29
+ // record that moved no money read as classed — the exact inflation the center lock forbids.
30
+ | "reference-placement";
31
+
32
+ /** PAY + IDN — the attribution rungs every class from TC-1 up carries. */
33
+ const TC1: readonly StepName[] = ["settlement-enumeration", "resolve-party"];
34
+ /** + TRM/WLD: the fingerprint step IS the weld check (recomputed hash vs the hash the settlement committed). */
35
+ const TC2: readonly StepName[] = [...TC1, "atr-fingerprint"];
36
+ /** + TRM-6 + ATA + RCS-1/2/4 — the definition of the class, verbatim. */
37
+ const TC3: readonly StepName[] = [
38
+ ...TC2,
39
+ "buyer-acceptance",
40
+ "authority-attenuation",
41
+ "commitment-vs-leaf",
42
+ "recourse-elections",
43
+ ];
44
+
45
+ /**
46
+ * What each class MECHANICALLY requires to be `proved` before `verified` can hold, derived row-for-row from
47
+ * The class requirements (see `vectors/verify/mechanical.json`, the authored-from-spec truth table):
48
+ *
49
+ * PAY (value settles) ................. `settlement-enumeration` TC-1 +
50
+ * IDN (identity, stated assurance) .... `resolve-party` TC-1 +
51
+ * TRM/WLD (fingerprint + weld) ........ `atr-fingerprint` TC-2 +
52
+ * TRM-6 (signed acceptance) ........... `buyer-acceptance` TC-3 +
53
+ * ATA (authority + delegation chain) .. `authority-attenuation`, `commitment-vs-leaf` TC-3 +
54
+ * RCS-1/2/4 (forum, law, package) ..... `recourse-elections` TC-3 +
55
+ * OFR/OPS/DSC (commerce plane) ........ the four composition steps TC-4
56
+ *
57
+ * The ladder is CUMULATIVE: a class requires every rung below it, which is why TC-4 cannot hold without the
58
+ * TC-3 acceptance. It is also EXACT: a class never requires a step it does not depend on — TRM is not a TC-1
59
+ * rung, so a TC-1 record with an unattempted fingerprint still verifies.
60
+ *
61
+ * DECLARED GAP — ASP (spend authority, verifiable bounds) is marked from TC-1 and has NO step here: the bounds
62
+ * live inside the rail's own settlement authorization (an EIP-3009 `value`, an escrow `PaymentInfo` cap) and
63
+ * `verify` is pure over supplied inputs with no rail decoder. RCS-4 reaches it indirectly — `recourse-elections`
64
+ * requires the `spend artifact` role in the retained package, so the artifact is kept and referenced even though
65
+ * its bounds are not re-derived here. Closing it properly is a rail-decoder work item, named rather than omitted.
66
+ *
67
+ * `frc-non-gating` is DELIBERATELY absent from TC-4: it impeaches when it FAILS (a signal that gated settlement)
68
+ * but its ABSENCE never blocks (FRC is a downstream consequence, not a v1 requirement).
69
+ */
70
+ export const REQUIRED_STEPS: Record<TransactionClass, readonly StepName[]> = {
71
+ "TC-0": [],
72
+ "TC-1": TC1,
73
+ "TC-2": TC2,
74
+ "TC-3": TC3,
75
+ "TC-4": [
76
+ ...TC3,
77
+ "offer-bound",
78
+ "operations-bound",
79
+ "discovery-integrity",
80
+ "proportionality-declared",
81
+ ],
82
+ };
83
+
84
+ /** Whether a walk affirms the claimed class. Three rules, in order: **structural depth is always `false`**
85
+ * (a presence/absence readout never affirms liveness), any `failed` step impeaches the whole walk, and
86
+ * otherwise every step required by the claimed class must be `proved`. A step that is absent, or
87
+ * `indeterminate`, is not `proved` — absent inputs never prove. */
88
+ export function computeVerified(
89
+ steps: readonly { name: StepName; outcome: { status: string } }[],
90
+ claimedClass: TransactionClass,
91
+ depth: VerifyDepth,
92
+ ): boolean {
93
+ if (depth === "structural") return false; // structural = presence/absence readout, never affirms liveness
94
+ if (steps.some((s) => s.outcome.status === "failed")) return false; // any failure impeaches
95
+ const required = REQUIRED_STEPS[claimedClass];
96
+ return required.every(
97
+ (name) => steps.find((s) => s.name === name)?.outcome.status === "proved",
98
+ );
99
+ }