@atcn/subledger 1.4.1 → 1.5.1

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/dist/types.js CHANGED
@@ -1,15 +1,64 @@
1
- import { ExecutionDescriptorSchema } from "@atcn/schema";
1
+ import { ExecutionDescriptorSchema, PricingSchema, RefundTermsSchema, SkillRefSchema, UsageSummarySchema } from "@atcn/schema";
2
2
  import { z } from "zod";
3
3
  /** Agent Work Subledger record shapes (PRD v1.2 §6-§7). Amounts are integer minor units; currencies are ISO 4217. */
4
4
  export const Currency = z.string().regex(/^[A-Z]{3}$/, "ISO 4217 currency code");
5
5
  export const Minor = z.number().int().refine(Number.isSafeInteger, "amount must be a safe integer");
6
6
  const Iso = z.iso.datetime();
7
7
  const Digest = z.string().regex(/^sha256:[0-9a-f]{64}$/);
8
- export const FINANCIAL_EVENT_TYPES = ["quote", "invoice", "charge", "payment_reported", "refund", "reversal", "fee", "credit", "adjustment", "fx_rate"];
9
- /** Normalized status vocabulary; the provider's original status is always kept beside it. */
10
- export const NORMALIZED_STATUSES = ["quoted", "issued", "pending", "reported_paid", "failed", "refunded", "reversed", "void", "unknown"];
8
+ /** "estimate" and "hold" (schema 1.5) record what was expected before work ran; they are never costs. */
9
+ export const FINANCIAL_EVENT_TYPES = ["quote", "invoice", "charge", "payment_reported", "refund", "reversal", "fee", "credit", "adjustment", "fx_rate", "estimate", "hold"];
10
+ /**
11
+ * Normalized status vocabulary; the provider's original status is always kept beside it. "pending_finality" (schema
12
+ * 1.5) is a payment the rail reports as settled that can still be reversed until the rail's finality window ends.
13
+ */
14
+ export const NORMALIZED_STATUSES = ["quoted", "issued", "pending", "reported_paid", "pending_finality", "failed", "refunded", "reversed", "void", "unknown"];
15
+ /** Financial event types that bill for work and so may name the skill billed (schema 1.5). */
16
+ export const SKILL_BILLED_EVENT_TYPES = ["quote", "invoice", "charge"];
17
+ /**
18
+ * Estimates and holds (schema 1.5): what the agent, a budget gateway or the operator expected a piece of work to cost
19
+ * before it ran. ATCN records them and reports the gap against actual cost; it never enforces, blocks or reserves.
20
+ */
21
+ export const EXPECTATION_EVENT_TYPES = ["estimate", "hold"];
22
+ export const EXPECTATION_ISSUERS = ["agent", "gateway", "operator"];
23
+ export const HOLD_STATUSES = ["open", "captured", "released", "expired"];
24
+ /**
25
+ * A signature by a key bound to a registered provider (an agent's provider or a gateway), over the statement of an
26
+ * estimate, a hold, or a terminal delivery claim (schema 1.5).
27
+ */
28
+ export const KeySignerSchema = z.strictObject({
29
+ provider_id: z.string().min(1),
30
+ binding_id: z.string().min(1),
31
+ key_id: z.string().min(1),
32
+ value: z.string().min(1),
33
+ });
34
+ export const ExpectationSchema = z.strictObject({
35
+ /** Who made the estimate or hold. */
36
+ issued_by: z.enum(EXPECTATION_ISSUERS),
37
+ /** The issuer's own reference, such as a gateway request ID. */
38
+ source_ref: z.string().max(200).nullable(),
39
+ /** How the amount was worked out, for example "max tokens x rate". */
40
+ basis: z.string().max(500).nullable(),
41
+ expires_at: Iso.nullable(),
42
+ /** The source_event_id (same source) of the earlier estimate, or earlier record of the same hold, this one replaces. */
43
+ supersedes: z.string().max(200).nullable(),
44
+ /** Holds only: the hold's status as of this record. */
45
+ hold_status: z.enum(HOLD_STATUSES).optional(),
46
+ /** Absent when the operator recorded it; the record is then buyer_recorded. */
47
+ signer: KeySignerSchema.optional(),
48
+ });
49
+ /**
50
+ * A payment rail's own record of a payment or refund (schema 1.5), for example an A2A-SE escrow attestation with its
51
+ * Merkle inclusion proof. `scheme` names the format; `record` is the rail's record exactly as received.
52
+ */
53
+ export const RailAttestationSchema = z.strictObject({
54
+ scheme: z.string().min(1).max(200),
55
+ record: z.record(z.string(), z.unknown()),
56
+ });
57
+ export const RAIL_ATTESTED_EVENT_TYPES = ["payment_reported", "refund"];
11
58
  export const PAYERS = ["buyer", "provider", "other"];
12
59
  export const DELIVERY_EVENT_TYPES = ["acceptance", "completion", "partial_completion", "cancellation", "provider_failure", "terms_update", "correction"];
60
+ /** Outcome claims the provider may sign with its bound key (schema 1.5): how its work ended. */
61
+ export const SIGNED_CLAIM_TYPES = ["completion", "partial_completion", "cancellation", "provider_failure"];
13
62
  /**
14
63
  * Who made a delivery statement. Callers record "buyer" or "provider" (relayed by the buyer). The other values are
15
64
  * recorded only from clearing-network events: "clearing_policy" for a decision computed by the agreed policy from
@@ -29,6 +78,28 @@ export const EXCEPTION_KINDS = [
29
78
  "refund_after_close",
30
79
  "allocation_changed_after_close",
31
80
  "incomplete_lineage",
81
+ "usage_unpriced",
82
+ "usage_cost_mismatch",
83
+ /** A refund exceeds the agreed post-settlement cap or window, or a refund the terms require was not made. */
84
+ "refund_terms_breach",
85
+ /** A quote, invoice or charge names a different skill from the one delegated. */
86
+ "skill_price_mismatch",
87
+ /** Fewer independent witnesses attested to the run than the delegation requires. */
88
+ "witness_quorum_not_met",
89
+ /** Signed provider statements bound to the same receipt contradict each other. */
90
+ "conflicting_statements",
91
+ /** An estimate or hold matched no task or delegation, or several. */
92
+ "unmatched_estimate",
93
+ /** Actual cost on a node exceeds its estimate (beyond the task's tolerance). Recorded, not prevented. */
94
+ "actual_exceeds_estimate",
95
+ /** Actual cost on a node exceeds what is held for it. Recorded, not prevented. */
96
+ "actual_exceeds_hold",
97
+ /** A hold is still open past its expiry, or at close with nothing charged. */
98
+ "hold_not_released",
99
+ /** An estimate was issued after the first charge on its node; it is kept but not used as the estimate. */
100
+ "estimate_after_charge",
101
+ /** A cancelled or failed delegation still has billed cost that no refund, credit or reversal nets to zero. */
102
+ "charge_after_cancellation",
32
103
  ];
33
104
  /** Trust labels kept separate on every screen and export (PRD §16 "Identity assurance"). Never collapsed into "verified". */
34
105
  export const ASSURANCE_LABELS = [
@@ -46,11 +117,28 @@ export const ASSURANCE_LABELS = [
46
117
  "expired",
47
118
  /** A response statement revoked by a later statement from the same provider (schema 1.4). */
48
119
  "revoked",
120
+ /** An estimate or hold signed by a budget gateway's bound key (schema 1.5). */
121
+ "gateway_signed",
122
+ /** A payment or refund backed by the rail's own attestation, verified offline (schema 1.5). Not operator-reported. */
123
+ "rail_attested",
49
124
  ];
50
125
  export const RESPONSE_TYPES = ["acknowledge_view", "acknowledge_delivery", "submit_evidence", "propose_correction", "signed_attestation"];
51
- export const SHARE_ACTIONS = ["view", "acknowledge_delivery", "submit_evidence", "propose_correction", "signed_attestation"];
126
+ /** "witness_attestation" (schema 1.5) lets an independent witness sign a statement that it observed the run. */
127
+ export const SHARE_ACTIONS = ["view", "acknowledge_delivery", "submit_evidence", "propose_correction", "signed_attestation", "witness_attestation"];
52
128
  /** Receipt fields a provider may attest to or contest. Attesting one never implies the others (acceptance 16). */
53
- export const ATTESTABLE_FIELDS = ["delivery.status", "delivery.evidence", "scope.terms_digest", "financial.amounts", "financial.status"];
129
+ export const ATTESTABLE_FIELDS = ["delivery.status", "delivery.evidence", "scope.terms_digest", "financial.amounts", "financial.status", "delivery.usage"];
130
+ /** Schema 1.5 added "delivery.usage". Documents of earlier versions disclose exactly these fields. */
131
+ const ATTESTABLE_FIELDS_BEFORE_1_5 = ["delivery.status", "delivery.evidence", "scope.terms_digest", "financial.amounts", "financial.status"];
132
+ export function attestableFieldsFor(schemaVersion) {
133
+ return ["1.2", "1.3", "1.4"].includes(schemaVersion) ? ATTESTABLE_FIELDS_BEFORE_1_5 : ATTESTABLE_FIELDS;
134
+ }
135
+ /** The usage of a run as recorded on a delivery claim: the trace's summary and the digest of the full trace, which stays with its holder. */
136
+ export const UsageRecordSchema = z.strictObject({
137
+ trace_digest: Digest,
138
+ summary: UsageSummarySchema,
139
+ });
140
+ /** Delivery claims that may carry usage. */
141
+ export const USAGE_CLAIM_TYPES = ["completion", "partial_completion"];
54
142
  /** Evidence stays in its source system; only references and digests are stored. Script-capable schemes are refused. */
55
143
  export const EvidenceRefSchema = z.object({
56
144
  uri: z
@@ -61,6 +149,13 @@ export const EvidenceRefSchema = z.object({
61
149
  digest: Digest.nullable(),
62
150
  evidence_type: z.string().min(1).max(100),
63
151
  });
152
+ /** Why part of a delegation chain was not captured. broken_edge: a reported sub-task arrived with no outcome. */
153
+ export const CAPTURE_GAP_KINDS = ["capture_failed", "queue_overflow", "provider_undisclosed", "manual_gap", "broken_edge"];
154
+ export const CaptureGapInputSchema = z.object({
155
+ delegation_id: z.string().nullable().default(null),
156
+ kind: z.enum(CAPTURE_GAP_KINDS),
157
+ detail: z.string().min(1).max(2000),
158
+ });
64
159
  export const TaskInputSchema = z.object({
65
160
  external_ref: z.string().min(1).max(200),
66
161
  currency: Currency,
@@ -72,12 +167,25 @@ export const TaskInputSchema = z.object({
72
167
  shared_description: z.string().max(1000).nullable().default(null),
73
168
  retrospective: z.boolean().default(false),
74
169
  occurred_at: Iso.nullable().default(null),
170
+ /** Schema 1.5: how far actual cost may exceed an estimate, in basis points, before actual_exceeds_estimate opens. */
171
+ estimate_tolerance_bps: z.number().int().min(0).max(100_000).optional(),
75
172
  });
76
173
  export const ProviderInputSchema = z.object({
77
174
  name: z.string().min(1).max(200),
78
175
  provider_own_id: z.string().max(200).nullable().default(null),
79
176
  domain: z.string().max(253).nullable().default(null),
80
177
  });
178
+ /**
179
+ * Independent witnesses a delegation requires (schema 1.5). A witness is a registered provider other than the
180
+ * delegation's own, signing with a domain-challenged key; it counts only if that registrable domain differs from the
181
+ * provider's, the buyer operator's and every other counted witness's. Too few raise witness_quorum_not_met.
182
+ */
183
+ export const SubledgerWitnessPolicySchema = z.strictObject({
184
+ min_independent_witnesses: z.number().int().min(1).max(10),
185
+ /** If set, only these providers may witness. */
186
+ witness_provider_ids: z.array(z.string().min(1)).min(1).max(20).optional(),
187
+ independence: z.literal("distinct_verified_domain"),
188
+ });
81
189
  export const DelegationInputSchema = z.object({
82
190
  parent_delegation_id: z.string().nullable().default(null),
83
191
  provider_id: z.string().nullable().default(null),
@@ -100,6 +208,12 @@ export const DelegationInputSchema = z.object({
100
208
  retrospective: z.boolean().default(false),
101
209
  /** The provider's run as the buyer recorded it (for A2A, from the task and agent card). Provider statements can cite it. */
102
210
  execution: ExecutionDescriptorSchema.optional(),
211
+ /** Agreed usage prices (schema 1.5). Billed cost is compared with usage priced at these rates. */
212
+ pricing: PricingSchema.optional(),
213
+ /** Agreed refund and failure terms (schema 1.5). Refunds that break them raise refund_terms_breach. */
214
+ refund_terms: RefundTermsSchema.optional(),
215
+ /** Independent witnesses required (schema 1.5). */
216
+ witness_policy: SubledgerWitnessPolicySchema.optional(),
103
217
  });
104
218
  export const DelegationEventInputSchema = z.object({
105
219
  type: z.enum(DELIVERY_EVENT_TYPES),
@@ -117,6 +231,12 @@ export const DelegationEventInputSchema = z.object({
117
231
  terms_digest: Digest.nullable().optional(),
118
232
  expected_delivery: Iso.nullable().optional(),
119
233
  downstream_visibility: z.enum(["unknown", "disclosed", "none"]).optional(),
234
+ /** Schema 1.5: replaces the delegation's pricing; null removes it. */
235
+ pricing: PricingSchema.nullable().optional(),
236
+ /** Schema 1.5: replaces the delegation's refund terms; null removes them. */
237
+ refund_terms: RefundTermsSchema.nullable().optional(),
238
+ /** Schema 1.5: replaces the delegation's witness policy; null removes it. */
239
+ witness_policy: SubledgerWitnessPolicySchema.nullable().optional(),
120
240
  })
121
241
  .nullable()
122
242
  .default(null),
@@ -125,7 +245,29 @@ export const DelegationEventInputSchema = z.object({
125
245
  reason: z.string().max(2000).nullable().default(null),
126
246
  retrospective: z.boolean().default(false),
127
247
  occurred_at: Iso.nullable().default(null),
248
+ /** completion and partial_completion only (schema 1.5): the run's usage, from its trace. */
249
+ usage: UsageRecordSchema.optional(),
250
+ /**
251
+ * Outcome claims asserted by the provider only (schema 1.5): the provider's signature over the outcome statement,
252
+ * which names the delegation's provider_job_ref (for A2A, the task id) and the claim's evidence.
253
+ */
254
+ signer: KeySignerSchema.optional(),
128
255
  });
256
+ /** Rules across fields of a delivery event that the schema cannot express. */
257
+ export function delegationEventProblems(input) {
258
+ const problems = [];
259
+ if (input.usage !== undefined && !USAGE_CLAIM_TYPES.includes(input.type))
260
+ problems.push(`usage is allowed only on ${USAGE_CLAIM_TYPES.join(" and ")} events`);
261
+ if (input.signer !== undefined) {
262
+ if (!SIGNED_CLAIM_TYPES.includes(input.type))
263
+ problems.push(`signer is allowed only on ${SIGNED_CLAIM_TYPES.join(", ")} events`);
264
+ if (input.asserted_by !== "provider")
265
+ problems.push("a signed claim must be asserted_by provider");
266
+ if (input.usage !== undefined)
267
+ problems.push("a signed claim cannot carry usage, which the outcome statement does not cover");
268
+ }
269
+ return problems;
270
+ }
129
271
  export const FxSchema = z.object({
130
272
  base_currency: Currency,
131
273
  quote_currency: Currency,
@@ -155,6 +297,12 @@ export const FinancialEventInputSchema = z
155
297
  settles_event_id: z.string().nullable().default(null),
156
298
  fx: FxSchema.nullable().default(null),
157
299
  reason: z.string().max(2000).nullable().default(null),
300
+ /** Quotes, invoices and charges only (schema 1.5): the skill billed, from the provider's billing line. */
301
+ skill: SkillRefSchema.optional(),
302
+ /** Estimates and holds only (schema 1.5): who issued it, why, and how it relates to earlier ones. */
303
+ expectation: ExpectationSchema.optional(),
304
+ /** Payments and refunds only (schema 1.5): the rail's own attestation of the payment, embedded so it verifies offline. */
305
+ rail_attestation: RailAttestationSchema.optional(),
158
306
  /** Stable references used for matching. Only a unique match is applied automatically. */
159
307
  match: z
160
308
  .object({
@@ -175,7 +323,32 @@ export const FinancialEventInputSchema = z
175
323
  issue.addIssue({ code: "custom", path: ["reverses_event_id"], message: "reversal needs reverses_event_id" });
176
324
  if (e.type === "reversal" && !e.reason)
177
325
  issue.addIssue({ code: "custom", path: ["reason"], message: "reversal needs a reason" });
326
+ if (e.skill && !SKILL_BILLED_EVENT_TYPES.includes(e.type))
327
+ issue.addIssue({ code: "custom", path: ["skill"], message: `skill is allowed only on ${SKILL_BILLED_EVENT_TYPES.join(", ")} events` });
328
+ for (const problem of expectationProblems(e))
329
+ issue.addIssue({ code: "custom", path: ["expectation"], message: problem });
330
+ if (e.rail_attestation && !RAIL_ATTESTED_EVENT_TYPES.includes(e.type)) {
331
+ issue.addIssue({ code: "custom", path: ["rail_attestation"], message: `rail_attestation is allowed only on ${RAIL_ATTESTED_EVENT_TYPES.join(" and ")} events` });
332
+ }
178
333
  });
334
+ /** Rules for estimates and holds that the schema cannot express. */
335
+ export function expectationProblems(e) {
336
+ const isExpectation = EXPECTATION_EVENT_TYPES.includes(e.type);
337
+ if (!isExpectation)
338
+ return e.expectation ? ["expectation is allowed only on estimate and hold events"] : [];
339
+ if (!e.expectation)
340
+ return [`${e.type} events need expectation`];
341
+ const problems = [];
342
+ if (e.amount_minor < 0)
343
+ problems.push(`${e.type} amount must not be negative`);
344
+ if (e.type === "hold" && !e.expectation.hold_status)
345
+ problems.push("hold events need expectation.hold_status");
346
+ if (e.type === "estimate" && e.expectation.hold_status)
347
+ problems.push("only hold events carry hold_status");
348
+ if (e.expectation.issued_by === "operator" && e.expectation.signer)
349
+ problems.push("operator estimates and holds are recorded by the operator, not signed by a provider key");
350
+ return problems;
351
+ }
179
352
  export const AllocationTargetSchema = z.object({
180
353
  type: z.enum(["task", "delegation", "cost_center", "unallocated"]),
181
354
  id: z.string().max(200).nullable(),
@@ -0,0 +1,41 @@
1
+ import { type Pricing } from "@atcn/schema";
2
+ import type { DeliveryClaim, ResponseRecord, UsageCheck } from "./documents.js";
3
+ import type { Rollup, Totals } from "./rollup.js";
4
+ /** What a delegation was billed: invoices, charges, fees and adjustments, less credits and refunds. */
5
+ export declare function billedMinor(totals: Totals | undefined): number;
6
+ /**
7
+ * The usage claims that count for one delegation: completion and partial_completion claims with usage that no later
8
+ * correction superseded. Each claim reports the usage of the work it covers, so claims are summed, but a trace
9
+ * recorded more than once (for example by both buyer and provider) counts once, from its latest claim.
10
+ */
11
+ export declare function countedUsageClaims(claims: DeliveryClaim[]): DeliveryClaim[];
12
+ export interface UsageCheckInput {
13
+ delegation_id: string;
14
+ currency: string;
15
+ pricing: Pricing | null | undefined;
16
+ /** This delegation's claims, in recorded order. */
17
+ claims: DeliveryClaim[];
18
+ billed_minor: number;
19
+ /** Whether a provider key-signed, unexpired, unrevoked statement attests delivery.usage on this delegation's receipt. */
20
+ provider_attested: boolean;
21
+ }
22
+ /**
23
+ * Prices a delegation's usage at its agreed rates and compares it with what was billed, in both directions:
24
+ * overbilling is the obvious risk, and underbilling often means a charge is missing or attributed elsewhere.
25
+ * Null when the delegation has no pricing or no counted usage.
26
+ */
27
+ export declare function usageCheckFor(input: UsageCheckInput): UsageCheck | null;
28
+ /** Delegations whose receipts carry an in-force provider key-signed attestation of delivery.usage. */
29
+ export declare function usageAttestedDelegations(responses: ResponseRecord[], receipts: {
30
+ receipt_id: string;
31
+ delegation_id: string;
32
+ }[]): Set<string>;
33
+ /** Usage checks for every delegation of a closure with pricing and counted usage, in delegation order. */
34
+ export declare function usageChecksFor(delegations: {
35
+ delegation_id: string;
36
+ currency: string;
37
+ pricing?: Pricing;
38
+ }[], claims: DeliveryClaim[], rollup: Rollup, responses: ResponseRecord[], receipts: {
39
+ receipt_id: string;
40
+ delegation_id: string;
41
+ }[]): UsageCheck[];
package/dist/usage.js ADDED
@@ -0,0 +1,86 @@
1
+ import { allowedDifference, expectedCostFromUsage } from "@atcn/schema";
2
+ import { ASSURANCE_LABELS, USAGE_CLAIM_TYPES } from "./types.js";
3
+ /** What a delegation was billed: invoices, charges, fees and adjustments, less credits and refunds. */
4
+ export function billedMinor(totals) {
5
+ return totals ? totals.invoiced + totals.charged + totals.fees + totals.adjustments - totals.credits - totals.refunded : 0;
6
+ }
7
+ /**
8
+ * The usage claims that count for one delegation: completion and partial_completion claims with usage that no later
9
+ * correction superseded. Each claim reports the usage of the work it covers, so claims are summed, but a trace
10
+ * recorded more than once (for example by both buyer and provider) counts once, from its latest claim.
11
+ */
12
+ export function countedUsageClaims(claims) {
13
+ const superseded = new Set(claims.map((c) => c.supersedes_event_id).filter((id) => id !== null));
14
+ const byTrace = new Map();
15
+ for (const c of claims) {
16
+ if (!c.usage || superseded.has(c.event_id) || !USAGE_CLAIM_TYPES.includes(c.type))
17
+ continue;
18
+ byTrace.set(c.usage.trace_digest, c);
19
+ }
20
+ return [...byTrace.values()];
21
+ }
22
+ function sortedLabels(labels) {
23
+ return ASSURANCE_LABELS.filter((l) => labels.has(l));
24
+ }
25
+ /**
26
+ * Prices a delegation's usage at its agreed rates and compares it with what was billed, in both directions:
27
+ * overbilling is the obvious risk, and underbilling often means a charge is missing or attributed elsewhere.
28
+ * Null when the delegation has no pricing or no counted usage.
29
+ */
30
+ export function usageCheckFor(input) {
31
+ if (!input.pricing)
32
+ return null;
33
+ const counted = countedUsageClaims(input.claims);
34
+ if (counted.length === 0)
35
+ return null;
36
+ const cost = expectedCostFromUsage(input.pricing, counted.map((c) => c.usage.summary));
37
+ const expected = cost.expected_minor;
38
+ const allowed = expected === null ? null : allowedDifference(expected, input.pricing.tolerance_bps);
39
+ const difference = expected === null ? null : input.billed_minor - expected;
40
+ const labels = new Set(counted.flatMap((c) => c.assurance).filter((l) => l !== "superseded"));
41
+ if (input.provider_attested)
42
+ labels.add("provider_key_signed");
43
+ return {
44
+ delegation_id: input.delegation_id,
45
+ currency: input.currency,
46
+ expected_minor: expected,
47
+ lines: cost.lines,
48
+ billed_minor: input.billed_minor,
49
+ difference_minor: difference,
50
+ allowed_difference_minor: allowed,
51
+ within_tolerance: difference === null || allowed === null ? null : Math.abs(difference) <= allowed,
52
+ unpriced: cost.unpriced,
53
+ trace_digests: counted.map((c) => c.usage.trace_digest).sort(),
54
+ assurance: sortedLabels(labels),
55
+ };
56
+ }
57
+ /** Delegations whose receipts carry an in-force provider key-signed attestation of delivery.usage. */
58
+ export function usageAttestedDelegations(responses, receipts) {
59
+ const delegationOf = new Map(receipts.map((r) => [r.receipt_id, r.delegation_id]));
60
+ const attested = new Set();
61
+ for (const r of responses) {
62
+ const inForce = r.assurance.includes("provider_key_signed") && !r.assurance.includes("expired") && !r.assurance.includes("revoked");
63
+ if (inForce && r.statement.response_type === "signed_attestation" && r.statement.fields.includes("delivery.usage")) {
64
+ const delegationId = delegationOf.get(r.receipt_id);
65
+ if (delegationId)
66
+ attested.add(delegationId);
67
+ }
68
+ }
69
+ return attested;
70
+ }
71
+ /** Usage checks for every delegation of a closure with pricing and counted usage, in delegation order. */
72
+ export function usageChecksFor(delegations, claims, rollup, responses, receipts) {
73
+ const attested = usageAttestedDelegations(responses, receipts);
74
+ return delegations.flatMap((d) => {
75
+ const node = rollup.nodes.find((n) => n.node_id === d.delegation_id);
76
+ const result = usageCheckFor({
77
+ delegation_id: d.delegation_id,
78
+ currency: d.currency,
79
+ pricing: d.pricing,
80
+ claims: claims.filter((c) => c.delegation_id === d.delegation_id),
81
+ billed_minor: billedMinor(node?.direct[d.currency]),
82
+ provider_attested: attested.has(d.delegation_id),
83
+ });
84
+ return result ? [result] : [];
85
+ });
86
+ }
package/dist/verify.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import { type PublicKeyRecord } from "@atcn/schema";
2
- import { type OperatorKeyRecord } from "./documents.js";
2
+ import { type OperatorKeyRecord, type SignedClosure } from "./documents.js";
3
+ import { type DerivedException } from "./exceptions.js";
3
4
  export interface CheckResult {
4
5
  name: string;
5
6
  ok: boolean;
6
7
  details: string[];
8
+ /** Set when the check had nothing it could inspect (for example, traces committed but not supplied). Not a pass. */
9
+ state?: "not_inspected";
7
10
  }
8
11
  export interface SubledgerVerificationReport {
9
12
  valid: boolean;
@@ -25,8 +28,12 @@ export interface SubledgerVerifyOptions {
25
28
  obligationPackages?: unknown[];
26
29
  /** Time to check a receipt's expires_at against (ISO 8601). Defaults to now. */
27
30
  at?: string;
31
+ /** Trace files (raw bytes) behind recorded usage, to recompute each usage summary from its trace. */
32
+ traces?: Uint8Array[];
28
33
  }
29
34
  /** Detects the document type and verifies it offline: no network calls, no service access (acceptance 9). */
30
35
  export declare function verifySubledgerDocument(input: unknown, options: SubledgerVerifyOptions): SubledgerVerificationReport;
31
36
  export declare function verifyReceipt(input: unknown, options: SubledgerVerifyOptions): SubledgerVerificationReport;
32
37
  export declare function verifyClosure(input: unknown, options: SubledgerVerifyOptions): SubledgerVerificationReport;
38
+ /** The derived exceptions a closure's own records imply at close, as of generated_at; the service must list each as open or resolved. */
39
+ export declare function closureDerivedExceptions(c: SignedClosure["payload"]): DerivedException[];