owlrunkit 0.13.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +674 -0
  3. package/NOTICE.md +137 -0
  4. package/README.md +44 -0
  5. package/SOURCE.md +10 -0
  6. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_1.md +160 -0
  7. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +459 -0
  8. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +408 -0
  9. package/integrations/codex/skills/owlcoda-runkit/agents/openai.yaml +4 -0
  10. package/integrations/codex/skills/owlcoda-runkit/assets/templates/agents-snippet.md +7 -0
  11. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +10 -0
  12. package/integrations/codex/skills/owlcoda-runkit/assets/templates/coverage-adopt-request.json +26 -0
  13. package/integrations/codex/skills/owlcoda-runkit/assets/templates/execution-delivery-packet.json +13 -0
  14. package/integrations/codex/skills/owlcoda-runkit/assets/templates/finalize-request.json +18 -0
  15. package/integrations/codex/skills/owlcoda-runkit/assets/templates/goal-contract.json +11 -0
  16. package/integrations/codex/skills/owlcoda-runkit/assets/templates/profiles.json +9 -0
  17. package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +11 -0
  18. package/integrations/codex/skills/owlcoda-runkit/assets/templates/resource-preflight-request.json +56 -0
  19. package/integrations/codex/skills/owlcoda-runkit/assets/templates/resume-request.json +10 -0
  20. package/integrations/codex/skills/owlcoda-runkit/assets/templates/snapshot-request.json +23 -0
  21. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-context.json +15 -0
  22. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-receipt.json +11 -0
  23. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verify-plan-request.json +31 -0
  24. package/integrations/codex/skills/owlcoda-runkit/assets/templates/visual-smoke-request.json +32 -0
  25. package/integrations/codex/skills/owlcoda-runkit/assets/templates/worker-lease.json +7 -0
  26. package/package.json +24 -0
  27. package/packages/attest/LICENSE +674 -0
  28. package/packages/attest/README.md +63 -0
  29. package/packages/attest/cli/owlcoda-attest.mjs +155 -0
  30. package/packages/attest/consumer-surface-v1.json +255 -0
  31. package/packages/attest/distribution-manifest-v1.json +104 -0
  32. package/packages/attest/fixtures/bad/accepted-authority-leak.json +7 -0
  33. package/packages/attest/fixtures/bad/accepted-failed-receipt.json +7 -0
  34. package/packages/attest/fixtures/bad/active-context-binding-mismatch.json +7 -0
  35. package/packages/attest/fixtures/bad/active-source-binding-mismatch.json +7 -0
  36. package/packages/attest/fixtures/bad/context-fingerprint-mismatch.json +7 -0
  37. package/packages/attest/fixtures/bad/duplicate-object-key.json +8 -0
  38. package/packages/attest/fixtures/bad/multiple-active-leaves.json +10 -0
  39. package/packages/attest/fixtures/bad/orphan-replacement.json +7 -0
  40. package/packages/attest/fixtures/bad/root-shape-extra-property.json +8 -0
  41. package/packages/attest/fixtures/bad/source-fingerprint-mismatch.json +7 -0
  42. package/packages/attest/fixtures/good/accepted-replacement-chain.json +59 -0
  43. package/packages/attest/fixtures/good/blocked-single-leaf.json +9 -0
  44. package/packages/attest/fixtures/index.json +90 -0
  45. package/packages/attest/sbom.spdx.json +33 -0
  46. package/packages/attest/schemas/attestation-ref-v1.schema.json +44 -0
  47. package/packages/attest/schemas/attestation-result-v1.schema.json +163 -0
  48. package/packages/attest/schemas/offline-attestation-bundle-v1.schema.json +30 -0
  49. package/packages/attest/schemas/public-verification-bundle-v1.schema.json +74 -0
  50. package/packages/attest/schemas/quick-verification-receipt-v1.schema.json +219 -0
  51. package/packages/attest/schemas/workspace-snapshot-v1.schema.json +135 -0
  52. package/packages/attest/src/canonical.mjs +12 -0
  53. package/packages/attest/src/formal.mjs +762 -0
  54. package/packages/attest/src/index.mjs +113 -0
  55. package/packages/attest/src/offline-bundle.mjs +108 -0
  56. package/packages/attest/src/quick-receipt-contract.mjs +156 -0
  57. package/packages/attest/src/quick.mjs +274 -0
  58. package/packages/attest/src/reference-contract.mjs +64 -0
  59. package/packages/attest/src/reference.mjs +131 -0
  60. package/packages/attest/src/workspace-snapshot.mjs +171 -0
  61. package/registry-release-policy-v1.json +34 -0
  62. package/sbom.spdx.json +39 -0
  63. package/scripts/runkit-contract/acceptance-evidence.mjs +103 -0
  64. package/scripts/runkit-contract/core-contract.mjs +392 -0
  65. package/scripts/runkit-contract/coverage-adoption.mjs +414 -0
  66. package/scripts/runkit-contract/delivery-create.mjs +252 -0
  67. package/scripts/runkit-contract/delivery-selection.mjs +31 -0
  68. package/scripts/runkit-contract/finalize.mjs +790 -0
  69. package/scripts/runkit-contract/inspect-presentation.mjs +183 -0
  70. package/scripts/runkit-contract/install-codex-skill.mjs +239 -0
  71. package/scripts/runkit-contract/lease-lifecycle.mjs +324 -0
  72. package/scripts/runkit-contract/lifecycle-orchestration.mjs +415 -0
  73. package/scripts/runkit-contract/offline-store.mjs +248 -0
  74. package/scripts/runkit-contract/profile-impact.mjs +223 -0
  75. package/scripts/runkit-contract/provenance-common.mjs +153 -0
  76. package/scripts/runkit-contract/quick-attest.mjs +4 -0
  77. package/scripts/runkit-contract/quick-canonical.mjs +41 -0
  78. package/scripts/runkit-contract/quick-metrics.mjs +86 -0
  79. package/scripts/runkit-contract/quick-receipt.mjs +139 -0
  80. package/scripts/runkit-contract/quick-verify.mjs +201 -0
  81. package/scripts/runkit-contract/quick-workspace-snapshot.mjs +3 -0
  82. package/scripts/runkit-contract/ready-for-commit.mjs +188 -0
  83. package/scripts/runkit-contract/receipt-lineage.mjs +332 -0
  84. package/scripts/runkit-contract/registry-adoption-gate.mjs +117 -0
  85. package/scripts/runkit-contract/repair-execution.mjs +997 -0
  86. package/scripts/runkit-contract/resource-preflight.mjs +829 -0
  87. package/scripts/runkit-contract/resume-execution.mjs +569 -0
  88. package/scripts/runkit-contract/runkit-cli.mjs +1214 -0
  89. package/scripts/runkit-contract/schemas/closeout-receipt-v2.schema.json +42 -0
  90. package/scripts/runkit-contract/schemas/core-artifact-v2.schema.json +23 -0
  91. package/scripts/runkit-contract/schemas/core-artifact.schema.json +23 -0
  92. package/scripts/runkit-contract/schemas/coverage-adopt-request-v1.schema.json +66 -0
  93. package/scripts/runkit-contract/schemas/engine-pin-v2.schema.json +14 -0
  94. package/scripts/runkit-contract/schemas/engine-pin.schema.json +14 -0
  95. package/scripts/runkit-contract/schemas/evidence-coverage-index-v1.schema.json +131 -0
  96. package/scripts/runkit-contract/schemas/finalize-request-v1.schema.json +131 -0
  97. package/scripts/runkit-contract/schemas/profiles.schema.json +47 -0
  98. package/scripts/runkit-contract/schemas/project-config-v2.schema.json +13 -0
  99. package/scripts/runkit-contract/schemas/project-config.schema.json +13 -0
  100. package/scripts/runkit-contract/schemas/ready-for-commit-v1.schema.json +26 -0
  101. package/scripts/runkit-contract/schemas/replayable-evidence-v1.schema.json +82 -0
  102. package/scripts/runkit-contract/schemas/resource-preflight-request-v1.schema.json +178 -0
  103. package/scripts/runkit-contract/schemas/resource-preflight-v1.schema.json +153 -0
  104. package/scripts/runkit-contract/schemas/resume-attempt-v1.schema.json +80 -0
  105. package/scripts/runkit-contract/schemas/resume-request-v1.schema.json +77 -0
  106. package/scripts/runkit-contract/schemas/snapshot-v1.schema.json +71 -0
  107. package/scripts/runkit-contract/schemas/verification-context-v1.schema.json +92 -0
  108. package/scripts/runkit-contract/schemas/verification-plan-v1.schema.json +150 -0
  109. package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +118 -0
  110. package/scripts/runkit-contract/schemas/verify-plan-request-v1.schema.json +56 -0
  111. package/scripts/runkit-contract/schemas/visual-smoke-request-v1.schema.json +64 -0
  112. package/scripts/runkit-contract/schemas/visual-smoke-result-v1.schema.json +68 -0
  113. package/scripts/runkit-contract/snapshot.mjs +340 -0
  114. package/scripts/runkit-contract/source-fingerprint.mjs +404 -0
  115. package/scripts/runkit-contract/templates/coverage-adopt-request.json +26 -0
  116. package/scripts/runkit-contract/templates/finalize-request.json +18 -0
  117. package/scripts/runkit-contract/templates/goal-contract.json +11 -0
  118. package/scripts/runkit-contract/templates/ready-for-commit-request.json +11 -0
  119. package/scripts/runkit-contract/templates/resource-preflight-request.json +56 -0
  120. package/scripts/runkit-contract/templates/resume-request.json +10 -0
  121. package/scripts/runkit-contract/templates/snapshot-request.json +23 -0
  122. package/scripts/runkit-contract/templates/verify-plan-request.json +31 -0
  123. package/scripts/runkit-contract/templates/visual-smoke-request.json +32 -0
  124. package/scripts/runkit-contract/templates/worker-lease.json +7 -0
  125. package/scripts/runkit-contract/verification-context.mjs +141 -0
  126. package/scripts/runkit-contract/verification-plan.mjs +608 -0
  127. package/scripts/runkit-contract/verification-receipt-gate.mjs +566 -0
  128. package/scripts/runkit-contract/visual-smoke.mjs +410 -0
@@ -0,0 +1,829 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ assertAllowedKeys,
6
+ isRecord,
7
+ loadActiveExecution,
8
+ readJson,
9
+ relativeToWorkspace,
10
+ resolveExistingArtifact,
11
+ safeIdentifier,
12
+ safeRelativePath,
13
+ sha256,
14
+ writeJsonExclusiveAtomically,
15
+ } from "./provenance-common.mjs";
16
+
17
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
18
+ const REQUEST_KEYS = [
19
+ "schemaVersion",
20
+ "preflightId",
21
+ "verificationPlanPath",
22
+ "verificationPlanSha256",
23
+ "policy",
24
+ "observations",
25
+ "workloads",
26
+ ];
27
+ const POLICY_KEYS = ["gateMode", "maxObservationAgeMs", "unknownHandling", "limits"];
28
+ const UNKNOWN_HANDLING_KEYS = ["availability", "quota", "resetTime", "pricing"];
29
+ const LIMIT_KEYS = ["maxCalls", "maxTotalTokens", "maxElapsedMs", "maxCostUsd"];
30
+ const OBSERVATION_KEYS = ["providerId", "modelId", "adapter", "availability", "quota", "pricing"];
31
+ const ADAPTER_KEYS = ["id", "kind", "evidenceRef", "evidenceSha256", "observedAt"];
32
+ const AVAILABILITY_KEYS = ["status", "reason"];
33
+ const QUOTA_KEYS = ["remainingCalls", "remainingTokens", "resetAt"];
34
+ const TYPED_VALUE_KEYS = ["status", "value", "reason"];
35
+ const PRICING_KEYS = ["status", "currency", "inputPerMillion", "outputPerMillion", "reason"];
36
+ const WORKLOAD_KEYS = [
37
+ "id",
38
+ "providerId",
39
+ "modelId",
40
+ "calls",
41
+ "inputTokens",
42
+ "outputTokens",
43
+ "elapsedMs",
44
+ "coveredByReceiptId",
45
+ ];
46
+ const ARTIFACT_KEYS = [
47
+ "schemaVersion",
48
+ "runId",
49
+ "preflightId",
50
+ "sequence",
51
+ "evaluatedAt",
52
+ "validUntil",
53
+ "requestSha256",
54
+ "verificationPlan",
55
+ "policy",
56
+ "receiptReuse",
57
+ "estimate",
58
+ "resources",
59
+ "decision",
60
+ "authorizationGranted",
61
+ "gitAuthorization",
62
+ "releaseAuthorization",
63
+ ];
64
+
65
+ export function buildResourcePreflight({
66
+ runId,
67
+ request,
68
+ verificationPlan,
69
+ requestSha256,
70
+ sequence = 1,
71
+ evaluatedAt,
72
+ }) {
73
+ safeIdentifier(runId, "runId");
74
+ const normalizedSequence = positiveInteger(sequence, "resource preflight sequence");
75
+ const normalized = normalizeRequest(request);
76
+ const plan = normalizeVerificationPlan(verificationPlan, runId);
77
+ const normalizedRequestSha256 = requiredSha256(requestSha256, "requestSha256");
78
+ const reusableReceiptIds = plan.reusableReceiptIds;
79
+ const reusableSet = new Set(reusableReceiptIds);
80
+ const appliedReceiptIds = new Set();
81
+ const pending = [];
82
+ for (const workload of normalized.workloads) {
83
+ if (workload.coveredByReceiptId !== null) {
84
+ if (!reusableSet.has(workload.coveredByReceiptId)) {
85
+ throw new Error(`Workload ${workload.id} receipt is not reusable: ${workload.coveredByReceiptId}`);
86
+ }
87
+ appliedReceiptIds.add(workload.coveredByReceiptId);
88
+ continue;
89
+ }
90
+ pending.push(workload);
91
+ }
92
+ const effectiveEvaluatedAt = isoTimestamp(
93
+ evaluatedAt ?? latestObservedAt(normalized.observations),
94
+ "resource evaluatedAt",
95
+ );
96
+
97
+ const demandByResource = new Map();
98
+ for (const workload of pending) {
99
+ const key = resourceKey(workload.providerId, workload.modelId);
100
+ const current = demandByResource.get(key) ?? emptyDemand();
101
+ current.calls += workload.calls;
102
+ current.inputTokens += workload.inputTokens;
103
+ current.outputTokens += workload.outputTokens;
104
+ current.totalTokens += workload.inputTokens + workload.outputTokens;
105
+ current.elapsedMs += workload.elapsedMs;
106
+ demandByResource.set(key, current);
107
+ }
108
+
109
+ const blockers = [];
110
+ const warnings = [];
111
+ const resourceAssessments = normalized.observations.map(observation => {
112
+ const key = resourceKey(observation.providerId, observation.modelId);
113
+ const demand = demandByResource.get(key) ?? emptyDemand();
114
+ if (demand.calls > 0) {
115
+ assessObservation({
116
+ observation,
117
+ demand,
118
+ policy: normalized.policy,
119
+ evaluatedAt: effectiveEvaluatedAt,
120
+ blockers,
121
+ warnings,
122
+ });
123
+ }
124
+ return { ...observation, demand };
125
+ });
126
+
127
+ const totals = pending.reduce((result, workload) => {
128
+ result.calls += workload.calls;
129
+ result.inputTokens += workload.inputTokens;
130
+ result.outputTokens += workload.outputTokens;
131
+ result.totalTokens += workload.inputTokens + workload.outputTokens;
132
+ result.elapsedMs += workload.elapsedMs;
133
+ return result;
134
+ }, emptyDemand());
135
+ const cost = costEstimateFromResources(resourceAssessments);
136
+ assessLimits({ totals, cost, policy: normalized.policy, blockers });
137
+ const validUntil = pending.length === 0
138
+ ? null
139
+ : earliestResourceExpiry(resourceAssessments, normalized.policy.maxObservationAgeMs);
140
+ const decision = resourceDecision({
141
+ pendingCount: pending.length,
142
+ blockers,
143
+ warnings,
144
+ gateMode: normalized.policy.gateMode,
145
+ });
146
+
147
+ return {
148
+ schemaVersion: "OwlCodaRunKitResourcePreflightV1",
149
+ runId,
150
+ preflightId: normalized.preflightId,
151
+ sequence: normalizedSequence,
152
+ evaluatedAt: effectiveEvaluatedAt,
153
+ validUntil,
154
+ requestSha256: normalizedRequestSha256,
155
+ verificationPlan: {
156
+ path: normalized.verificationPlanPath,
157
+ sha256: normalized.verificationPlanSha256,
158
+ planId: plan.planId,
159
+ },
160
+ policy: normalized.policy,
161
+ receiptReuse: {
162
+ verificationPlanId: plan.planId,
163
+ reusableReceiptIds,
164
+ appliedReceiptIds: [...appliedReceiptIds].sort(),
165
+ },
166
+ estimate: {
167
+ plannedWorkloads: normalized.workloads.length,
168
+ reusedWorkloads: normalized.workloads.length - pending.length,
169
+ pendingWorkloads: pending.length,
170
+ ...totals,
171
+ cost,
172
+ },
173
+ resources: resourceAssessments,
174
+ decision,
175
+ authorizationGranted: false,
176
+ gitAuthorization: false,
177
+ releaseAuthorization: false,
178
+ };
179
+ }
180
+
181
+ export function runResourcePreflight({ workspaceRoot, runId, request, requestSha256 }) {
182
+ const normalized = normalizeRequest(request);
183
+ const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
184
+ if (pinGate.status !== "valid") return { ...pinGate, authorizationGranted: false };
185
+ const planPath = resolveExistingArtifact(
186
+ workspaceRoot,
187
+ normalized.verificationPlanPath,
188
+ "verificationPlanPath",
189
+ );
190
+ const expectedPlanRoot = path.join(executionRoot, "verification-plans");
191
+ const relativePlanPath = path.relative(expectedPlanRoot, planPath);
192
+ if (relativePlanPath === ""
193
+ || relativePlanPath === ".."
194
+ || relativePlanPath.startsWith(`..${path.sep}`)
195
+ || path.isAbsolute(relativePlanPath)
196
+ || realpathSync(planPath) !== path.resolve(workspaceRoot, normalized.verificationPlanPath)) {
197
+ throw new Error("verificationPlanPath must be a real file inside the active execution verification-plans directory.");
198
+ }
199
+ const planStat = lstatSync(planPath);
200
+ if (planStat.isSymbolicLink() || !planStat.isFile()) {
201
+ throw new Error("verificationPlanPath must be a regular file, not a symlink.");
202
+ }
203
+ const planBytes = readFileSync(planPath);
204
+ if (sha256(planBytes) !== normalized.verificationPlanSha256) {
205
+ throw new Error("Verification plan hash mismatch.");
206
+ }
207
+ const artifact = buildResourcePreflight({
208
+ runId,
209
+ request,
210
+ verificationPlan: JSON.parse(planBytes.toString("utf8")),
211
+ requestSha256,
212
+ sequence: nextResourceSequence(executionRoot, runId),
213
+ evaluatedAt: new Date().toISOString(),
214
+ });
215
+ const outputPath = path.join(executionRoot, "resource-preflights", `${normalized.preflightId}.json`);
216
+ writeJsonExclusiveAtomically(outputPath, artifact);
217
+ return {
218
+ status: "resource_preflight_written",
219
+ exitCode: 0,
220
+ runId,
221
+ preflightPath: relativeToWorkspace(workspaceRoot, outputPath),
222
+ decision: artifact.decision.status,
223
+ nextAllowedAction: artifact.decision.nextAllowedAction,
224
+ authorizationGranted: false,
225
+ };
226
+ }
227
+
228
+ export function summarizeResourcePreflight(value, expectedRunId) {
229
+ assertAllowedKeys(value, "Resource preflight artifact", ARTIFACT_KEYS);
230
+ if (value.schemaVersion !== "OwlCodaRunKitResourcePreflightV1"
231
+ || value.runId !== expectedRunId
232
+ || value.authorizationGranted !== false
233
+ || value.gitAuthorization !== false
234
+ || value.releaseAuthorization !== false) {
235
+ throw new Error("Resource preflight artifact identity or authorization boundary is invalid.");
236
+ }
237
+ const preflightId = safeIdentifier(value.preflightId, "resource preflightId");
238
+ const sequence = positiveInteger(value.sequence, "resource preflight sequence");
239
+ const evaluatedAt = isoTimestamp(value.evaluatedAt, "resource evaluatedAt");
240
+ const validUntil = value.validUntil === null ? null : isoTimestamp(value.validUntil, "resource validUntil");
241
+ requiredSha256(value.requestSha256, "resource requestSha256");
242
+ if (!isRecord(value.verificationPlan)) throw new Error("Resource verificationPlan is invalid.");
243
+ assertAllowedKeys(value.verificationPlan, "Resource verificationPlan", ["path", "sha256", "planId"]);
244
+ safeRelativePath(value.verificationPlan.path, "Resource verificationPlan path");
245
+ requiredSha256(value.verificationPlan.sha256, "Resource verificationPlan sha256");
246
+ safeIdentifier(value.verificationPlan.planId, "Resource verificationPlan planId");
247
+ const policy = normalizePolicy(value.policy);
248
+ if (!isRecord(value.receiptReuse)) throw new Error("Resource receiptReuse is invalid.");
249
+ assertAllowedKeys(value.receiptReuse, "Resource receiptReuse", [
250
+ "verificationPlanId",
251
+ "reusableReceiptIds",
252
+ "appliedReceiptIds",
253
+ ]);
254
+ const verificationPlanId = safeIdentifier(value.receiptReuse.verificationPlanId, "Resource verificationPlanId");
255
+ const reusableReceiptIds = sortedUniqueIdentifiers(value.receiptReuse.reusableReceiptIds, "Resource reusableReceiptIds");
256
+ const appliedReceiptIds = sortedUniqueIdentifiers(value.receiptReuse.appliedReceiptIds, "Resource appliedReceiptIds");
257
+ if (verificationPlanId !== value.verificationPlan.planId) {
258
+ throw new Error("Resource receipt reuse verification plan does not match the bound verification plan.");
259
+ }
260
+ if (appliedReceiptIds.some(receiptId => !reusableReceiptIds.includes(receiptId))) {
261
+ throw new Error("Resource applied receipt ids must be a subset of reusable receipt ids.");
262
+ }
263
+ if (!isRecord(value.estimate)) throw new Error("Resource estimate is invalid.");
264
+ assertAllowedKeys(value.estimate, "Resource estimate", [
265
+ "plannedWorkloads",
266
+ "reusedWorkloads",
267
+ "pendingWorkloads",
268
+ "calls",
269
+ "inputTokens",
270
+ "outputTokens",
271
+ "totalTokens",
272
+ "elapsedMs",
273
+ "cost",
274
+ ]);
275
+ for (const key of [
276
+ "plannedWorkloads",
277
+ "reusedWorkloads",
278
+ "pendingWorkloads",
279
+ "calls",
280
+ "inputTokens",
281
+ "outputTokens",
282
+ "totalTokens",
283
+ "elapsedMs",
284
+ ]) nonNegativeInteger(value.estimate[key], `Resource estimate ${key}`);
285
+ if (value.estimate.totalTokens !== value.estimate.inputTokens + value.estimate.outputTokens
286
+ || value.estimate.plannedWorkloads !== value.estimate.reusedWorkloads + value.estimate.pendingWorkloads) {
287
+ throw new Error("Resource estimate totals are inconsistent.");
288
+ }
289
+ const cost = normalizeCost(value.estimate.cost);
290
+ if ((value.estimate.pendingWorkloads === 0) !== (value.estimate.calls === 0)
291
+ || appliedReceiptIds.length > value.estimate.reusedWorkloads) {
292
+ throw new Error("Resource workload and receipt reuse counts are inconsistent.");
293
+ }
294
+ if (!Array.isArray(value.resources)) throw new Error("Resource assessments must be an array.");
295
+ const normalizedResources = value.resources.map(resource => {
296
+ if (!isRecord(resource)) throw new Error("Resource assessment must be an object.");
297
+ assertAllowedKeys(resource, "Resource assessment", [...OBSERVATION_KEYS, "demand"]);
298
+ const observation = normalizeObservation(Object.fromEntries(
299
+ OBSERVATION_KEYS.map(key => [key, resource[key]]),
300
+ ));
301
+ if (!isRecord(resource.demand)) throw new Error("Resource demand is invalid.");
302
+ assertAllowedKeys(resource.demand, "Resource demand", ["calls", "inputTokens", "outputTokens", "totalTokens", "elapsedMs"]);
303
+ for (const key of ["calls", "inputTokens", "outputTokens", "totalTokens", "elapsedMs"]) {
304
+ nonNegativeInteger(resource.demand[key], `Resource demand ${key}`);
305
+ }
306
+ if (resource.demand.totalTokens !== resource.demand.inputTokens + resource.demand.outputTokens) {
307
+ throw new Error("Resource demand token totals are inconsistent.");
308
+ }
309
+ return { ...observation, demand: { ...resource.demand } };
310
+ }).sort((left, right) => resourceKey(left.providerId, left.modelId).localeCompare(resourceKey(right.providerId, right.modelId)));
311
+ const resourceKeys = normalizedResources.map(resource => resourceKey(resource.providerId, resource.modelId));
312
+ if (new Set(resourceKeys).size !== resourceKeys.length) throw new Error("Resource assessments must be unique.");
313
+ const demandTotals = normalizedResources.reduce((total, resource) => {
314
+ for (const key of ["calls", "inputTokens", "outputTokens", "totalTokens", "elapsedMs"]) {
315
+ total[key] += resource.demand[key];
316
+ }
317
+ return total;
318
+ }, emptyDemand());
319
+ for (const key of ["calls", "inputTokens", "outputTokens", "totalTokens", "elapsedMs"]) {
320
+ if (demandTotals[key] !== value.estimate[key]) throw new Error(`Resource estimate ${key} does not match resource demand.`);
321
+ }
322
+ const recomputedCost = costEstimateFromResources(normalizedResources);
323
+ if (JSON.stringify(recomputedCost) !== JSON.stringify(cost)) {
324
+ throw new Error("Resource cost does not match the declared resource demand and pricing.");
325
+ }
326
+ const expectedValidUntil = value.estimate.pendingWorkloads === 0
327
+ ? null
328
+ : earliestResourceExpiry(normalizedResources, policy.maxObservationAgeMs);
329
+ if (validUntil !== expectedValidUntil) {
330
+ throw new Error("Resource validUntil does not match observation freshness policy.");
331
+ }
332
+ if (!isRecord(value.decision)) throw new Error("Resource decision is invalid.");
333
+ assertAllowedKeys(value.decision, "Resource decision", ["status", "blockers", "warnings", "nextAllowedAction"]);
334
+ const status = enumValue(value.decision.status, [
335
+ "ready_for_model_execution",
336
+ "ready_without_model_execution",
337
+ "blocked_by_resource",
338
+ ], "resource decision status");
339
+ const blockers = sortedUniqueStrings(value.decision.blockers, "resource blockers");
340
+ const warnings = sortedUniqueStrings(value.decision.warnings, "resource warnings");
341
+ const nextAllowedAction = enumValue(value.decision.nextAllowedAction, [
342
+ "begin_model_execution",
343
+ "continue_without_model_calls",
344
+ "pause_at_deterministic_stage",
345
+ ], "resource nextAllowedAction");
346
+ const recomputedBlockers = [];
347
+ const recomputedWarnings = [];
348
+ for (const resource of normalizedResources) {
349
+ if (resource.demand.calls > 0) {
350
+ assessObservation({
351
+ observation: resource,
352
+ demand: resource.demand,
353
+ policy,
354
+ evaluatedAt,
355
+ blockers: recomputedBlockers,
356
+ warnings: recomputedWarnings,
357
+ });
358
+ }
359
+ }
360
+ assessLimits({ totals: demandTotals, cost: recomputedCost, policy, blockers: recomputedBlockers });
361
+ const expectedDecision = resourceDecision({
362
+ pendingCount: value.estimate.pendingWorkloads,
363
+ blockers: recomputedBlockers,
364
+ warnings: recomputedWarnings,
365
+ gateMode: policy.gateMode,
366
+ });
367
+ if (status !== expectedDecision.status
368
+ || nextAllowedAction !== expectedDecision.nextAllowedAction
369
+ || JSON.stringify(blockers) !== JSON.stringify(expectedDecision.blockers)
370
+ || JSON.stringify(warnings) !== JSON.stringify(expectedDecision.warnings)) {
371
+ throw new Error("Resource decision does not match the declared evidence, demand, and policy.");
372
+ }
373
+ const resources = normalizedResources.map(resource => ({
374
+ providerId: resource.providerId,
375
+ modelId: resource.modelId,
376
+ availability: resource.availability,
377
+ quota: resource.quota,
378
+ demand: resource.demand,
379
+ }));
380
+ return {
381
+ preflightId,
382
+ sequence,
383
+ evaluatedAt,
384
+ validUntil,
385
+ status,
386
+ nextAllowedAction,
387
+ blockers,
388
+ warnings,
389
+ receiptReuse: {
390
+ reusableCount: reusableReceiptIds.length,
391
+ appliedCount: appliedReceiptIds.length,
392
+ },
393
+ estimate: {
394
+ calls: value.estimate.calls,
395
+ inputTokens: value.estimate.inputTokens,
396
+ outputTokens: value.estimate.outputTokens,
397
+ totalTokens: value.estimate.totalTokens,
398
+ elapsedMs: value.estimate.elapsedMs,
399
+ cost,
400
+ },
401
+ resources,
402
+ };
403
+ }
404
+
405
+ function nextResourceSequence(executionRoot, runId) {
406
+ const resourceRoot = path.join(executionRoot, "resource-preflights");
407
+ if (!existsSync(resourceRoot)) return 1;
408
+ const stat = lstatSync(resourceRoot);
409
+ if (stat.isSymbolicLink() || !stat.isDirectory() || realpathSync(resourceRoot) !== path.resolve(resourceRoot)) {
410
+ throw new Error("Resource preflight directory must be a real directory, not a symlink.");
411
+ }
412
+ const sequences = [];
413
+ for (const entry of readdirSync(resourceRoot, { withFileTypes: true })) {
414
+ if (entry.isSymbolicLink() || !entry.isFile() || !entry.name.endsWith(".json")) {
415
+ throw new Error(`Resource preflight must be a regular JSON file: ${entry.name}`);
416
+ }
417
+ const artifactPath = path.join(resourceRoot, entry.name);
418
+ sequences.push(summarizeResourcePreflight(readJson(artifactPath), runId).sequence);
419
+ }
420
+ if (new Set(sequences).size !== sequences.length) throw new Error("Resource preflight sequences must be unique.");
421
+ return sequences.length === 0 ? 1 : Math.max(...sequences) + 1;
422
+ }
423
+
424
+ function normalizeRequest(value) {
425
+ assertAllowedKeys(value, "Resource preflight request", REQUEST_KEYS);
426
+ if (value.schemaVersion !== "OwlCodaRunKitResourcePreflightRequestV1") {
427
+ throw new Error("Unsupported resource preflight request schemaVersion.");
428
+ }
429
+ const preflightId = safeIdentifier(value.preflightId, "preflightId");
430
+ const verificationPlanPath = safeRelativePath(value.verificationPlanPath, "verificationPlanPath");
431
+ const verificationPlanSha256 = requiredSha256(value.verificationPlanSha256, "verificationPlanSha256");
432
+ const policy = normalizePolicy(value.policy);
433
+ if (!Array.isArray(value.observations) || value.observations.length === 0) {
434
+ throw new Error("observations must contain at least one resource observation.");
435
+ }
436
+ const observationByKey = new Map();
437
+ const observations = value.observations.map(normalizeObservation)
438
+ .sort((left, right) => resourceKey(left.providerId, left.modelId).localeCompare(resourceKey(right.providerId, right.modelId)));
439
+ for (const observation of observations) {
440
+ const key = resourceKey(observation.providerId, observation.modelId);
441
+ if (observationByKey.has(key)) throw new Error(`Resource observations must be unique: ${key}`);
442
+ observationByKey.set(key, observation);
443
+ }
444
+ if (!Array.isArray(value.workloads)) throw new Error("workloads must be an array.");
445
+ const workloadIds = new Set();
446
+ const workloads = value.workloads.map(workload => {
447
+ const normalized = normalizeWorkload(workload);
448
+ if (workloadIds.has(normalized.id)) throw new Error(`Workload ids must be unique: ${normalized.id}`);
449
+ workloadIds.add(normalized.id);
450
+ const key = resourceKey(normalized.providerId, normalized.modelId);
451
+ if (!observationByKey.has(key)) throw new Error(`Workload has no resource observation: ${key}`);
452
+ return normalized;
453
+ }).sort((left, right) => left.id.localeCompare(right.id));
454
+ return {
455
+ schemaVersion: value.schemaVersion,
456
+ preflightId,
457
+ verificationPlanPath,
458
+ verificationPlanSha256,
459
+ policy,
460
+ observations,
461
+ observationByKey,
462
+ workloads,
463
+ };
464
+ }
465
+
466
+ function normalizePolicy(value) {
467
+ assertAllowedKeys(value, "Resource policy", POLICY_KEYS);
468
+ if (!new Set(["model_required", "model_optional"]).has(value.gateMode)) {
469
+ throw new Error("Resource policy gateMode must be model_required or model_optional.");
470
+ }
471
+ const maxObservationAgeMs = positiveInteger(value.maxObservationAgeMs, "Resource policy maxObservationAgeMs");
472
+ assertAllowedKeys(value.unknownHandling, "Resource unknownHandling", UNKNOWN_HANDLING_KEYS);
473
+ const unknownHandling = {};
474
+ for (const key of UNKNOWN_HANDLING_KEYS) {
475
+ if (!new Set(["fail_open", "fail_closed"]).has(value.unknownHandling[key])) {
476
+ throw new Error(`Resource unknownHandling ${key} must be fail_open or fail_closed.`);
477
+ }
478
+ unknownHandling[key] = value.unknownHandling[key];
479
+ }
480
+ assertAllowedKeys(value.limits, "Resource limits", LIMIT_KEYS);
481
+ const limits = {};
482
+ for (const key of LIMIT_KEYS) {
483
+ if (value.limits[key] === undefined) continue;
484
+ limits[key] = nonNegativeNumber(value.limits[key], `Resource limit ${key}`, key !== "maxCostUsd");
485
+ }
486
+ return { gateMode: value.gateMode, maxObservationAgeMs, unknownHandling, limits };
487
+ }
488
+
489
+ function normalizeObservation(value) {
490
+ assertAllowedKeys(value, "Resource observation", OBSERVATION_KEYS);
491
+ const providerId = safeIdentifier(value.providerId, "providerId");
492
+ const modelId = safeIdentifier(value.modelId, "modelId");
493
+ assertAllowedKeys(value.adapter, "Resource adapter", ADAPTER_KEYS);
494
+ const adapter = {
495
+ id: safeIdentifier(value.adapter.id, "adapter id"),
496
+ kind: enumValue(value.adapter.kind, ["owlcoda_runtime", "provider_api", "project_declared"], "adapter kind"),
497
+ evidenceRef: evidenceReference(value.adapter.evidenceRef),
498
+ evidenceSha256: requiredSha256(value.adapter.evidenceSha256, "adapter evidenceSha256"),
499
+ observedAt: isoTimestamp(value.adapter.observedAt, "adapter observedAt"),
500
+ };
501
+ assertAllowedKeys(value.availability, "Resource availability", AVAILABILITY_KEYS);
502
+ const availabilityStatus = enumValue(
503
+ value.availability.status,
504
+ ["available", "unavailable", "unknown"],
505
+ "availability status",
506
+ );
507
+ const availability = availabilityStatus === "available"
508
+ ? noUnexpectedReason(value.availability, "available availability")
509
+ : { status: availabilityStatus, reason: nonEmptyString(value.availability.reason, "availability reason") };
510
+ assertAllowedKeys(value.quota, "Resource quota", QUOTA_KEYS);
511
+ const quota = {
512
+ remainingCalls: typedNumber(value.quota.remainingCalls, "remainingCalls", true),
513
+ remainingTokens: typedNumber(value.quota.remainingTokens, "remainingTokens", true),
514
+ resetAt: typedTimestamp(value.quota.resetAt, "resetAt"),
515
+ };
516
+ const pricing = normalizePricing(value.pricing);
517
+ return { providerId, modelId, adapter, availability, quota, pricing };
518
+ }
519
+
520
+ function normalizePricing(value) {
521
+ assertAllowedKeys(value, "Resource pricing", PRICING_KEYS);
522
+ if (value.status === "unknown") {
523
+ return { status: "unknown", reason: nonEmptyString(value.reason, "pricing reason") };
524
+ }
525
+ if (value.status !== "known" || value.currency !== "USD") {
526
+ throw new Error("Known resource pricing must use USD.");
527
+ }
528
+ if (value.reason !== undefined) throw new Error("Known pricing must not include reason.");
529
+ return {
530
+ status: "known",
531
+ currency: "USD",
532
+ inputPerMillion: nonNegativeNumber(value.inputPerMillion, "inputPerMillion", false),
533
+ outputPerMillion: nonNegativeNumber(value.outputPerMillion, "outputPerMillion", false),
534
+ };
535
+ }
536
+
537
+ function normalizeWorkload(value) {
538
+ assertAllowedKeys(value, "Resource workload", WORKLOAD_KEYS);
539
+ return {
540
+ id: safeIdentifier(value.id, "workload id"),
541
+ providerId: safeIdentifier(value.providerId, "workload providerId"),
542
+ modelId: safeIdentifier(value.modelId, "workload modelId"),
543
+ calls: positiveInteger(value.calls, "workload calls"),
544
+ inputTokens: nonNegativeInteger(value.inputTokens, "workload inputTokens"),
545
+ outputTokens: nonNegativeInteger(value.outputTokens, "workload outputTokens"),
546
+ elapsedMs: nonNegativeInteger(value.elapsedMs, "workload elapsedMs"),
547
+ coveredByReceiptId: value.coveredByReceiptId === undefined
548
+ ? null
549
+ : safeIdentifier(value.coveredByReceiptId, "coveredByReceiptId"),
550
+ };
551
+ }
552
+
553
+ function normalizeVerificationPlan(value, runId) {
554
+ if (!isRecord(value)
555
+ || value.schemaVersion !== "OwlCodaRunKitVerificationPlanV1"
556
+ || value.runId !== runId
557
+ || typeof value.planId !== "string"
558
+ || !isRecord(value.evidence)
559
+ || !Array.isArray(value.evidence.reusableReceiptIds)
560
+ || value.authorizationGranted !== false) {
561
+ throw new Error("verificationPlan must be a valid OwlCodaRunKitVerificationPlanV1 for this execution.");
562
+ }
563
+ const planId = safeIdentifier(value.planId, "verification planId");
564
+ const reusableReceiptIds = sortedUniqueIdentifiers(value.evidence.reusableReceiptIds, "reusableReceiptIds");
565
+ return { planId, reusableReceiptIds };
566
+ }
567
+
568
+ function assessObservation({ observation, demand, policy, evaluatedAt, blockers, warnings }) {
569
+ const key = resourceKey(observation.providerId, observation.modelId);
570
+ const observedAtMs = new Date(observation.adapter.observedAt).getTime();
571
+ const evaluatedAtMs = new Date(evaluatedAt).getTime();
572
+ if (observedAtMs > evaluatedAtMs) throw new Error(`Resource observation is from the future: ${key}`);
573
+ if (evaluatedAtMs - observedAtMs > policy.maxObservationAgeMs) {
574
+ blockers.push(`observation_stale:${key}`);
575
+ return;
576
+ }
577
+ if (observation.availability.status === "unavailable") {
578
+ blockers.push(`model_unavailable:${key}`);
579
+ return;
580
+ }
581
+ unknownIssue({
582
+ unknown: observation.availability.status === "unknown",
583
+ policy: policy.unknownHandling.availability,
584
+ code: `availability_unknown:${key}`,
585
+ blockers,
586
+ warnings,
587
+ });
588
+ if (observation.quota.remainingCalls.status === "known") {
589
+ if (observation.quota.remainingCalls.value < demand.calls) blockers.push(`insufficient_call_quota:${key}`);
590
+ } else {
591
+ unknownIssue({ unknown: true, policy: policy.unknownHandling.quota, code: `quota_calls_unknown:${key}`, blockers, warnings });
592
+ }
593
+ if (observation.quota.remainingTokens.status === "known") {
594
+ if (observation.quota.remainingTokens.value < demand.totalTokens) blockers.push(`insufficient_token_quota:${key}`);
595
+ } else {
596
+ unknownIssue({ unknown: true, policy: policy.unknownHandling.quota, code: `quota_tokens_unknown:${key}`, blockers, warnings });
597
+ }
598
+ unknownIssue({
599
+ unknown: observation.quota.resetAt.status === "unknown",
600
+ policy: policy.unknownHandling.resetTime,
601
+ code: `reset_time_unknown:${key}`,
602
+ blockers,
603
+ warnings,
604
+ });
605
+ unknownIssue({
606
+ unknown: observation.pricing.status === "unknown",
607
+ policy: policy.unknownHandling.pricing,
608
+ code: `pricing_unknown:${key}`,
609
+ blockers,
610
+ warnings,
611
+ });
612
+ }
613
+
614
+ function assessLimits({ totals, cost, policy, blockers }) {
615
+ const limits = policy.limits;
616
+ if (limits.maxCalls !== undefined && totals.calls > limits.maxCalls) blockers.push("policy_limit_exceeded:calls");
617
+ if (limits.maxTotalTokens !== undefined && totals.totalTokens > limits.maxTotalTokens) {
618
+ blockers.push("policy_limit_exceeded:total_tokens");
619
+ }
620
+ if (limits.maxElapsedMs !== undefined && totals.elapsedMs > limits.maxElapsedMs) {
621
+ blockers.push("policy_limit_exceeded:elapsed_ms");
622
+ }
623
+ if (limits.maxCostUsd !== undefined && cost.status === "known" && cost.valueUsd > limits.maxCostUsd) {
624
+ blockers.push("policy_limit_exceeded:cost_usd");
625
+ }
626
+ }
627
+
628
+ function resourceDecision({ pendingCount, blockers, warnings, gateMode }) {
629
+ const uniqueBlockers = sortedUnique(blockers);
630
+ const uniqueWarnings = sortedUnique(warnings);
631
+ if (pendingCount === 0) {
632
+ return {
633
+ status: "ready_without_model_execution",
634
+ blockers: [],
635
+ warnings: uniqueWarnings,
636
+ nextAllowedAction: "continue_without_model_calls",
637
+ };
638
+ }
639
+ if (uniqueBlockers.length > 0 && gateMode === "model_optional") {
640
+ return {
641
+ status: "ready_without_model_execution",
642
+ blockers: [],
643
+ warnings: sortedUnique([...uniqueWarnings, ...uniqueBlockers]),
644
+ nextAllowedAction: "continue_without_model_calls",
645
+ };
646
+ }
647
+ if (uniqueBlockers.length > 0) {
648
+ return {
649
+ status: "blocked_by_resource",
650
+ blockers: uniqueBlockers,
651
+ warnings: uniqueWarnings,
652
+ nextAllowedAction: "pause_at_deterministic_stage",
653
+ };
654
+ }
655
+ return {
656
+ status: "ready_for_model_execution",
657
+ blockers: [],
658
+ warnings: uniqueWarnings,
659
+ nextAllowedAction: "begin_model_execution",
660
+ };
661
+ }
662
+
663
+ function costEstimateFromResources(resources) {
664
+ let knownSubtotalUsd = 0;
665
+ const unknownResources = new Set();
666
+ for (const resource of resources) {
667
+ if (resource.demand.calls === 0) continue;
668
+ const key = resourceKey(resource.providerId, resource.modelId);
669
+ const pricing = resource.pricing;
670
+ if (pricing.status === "unknown") {
671
+ unknownResources.add(key);
672
+ continue;
673
+ }
674
+ knownSubtotalUsd += (resource.demand.inputTokens / 1_000_000) * pricing.inputPerMillion;
675
+ knownSubtotalUsd += (resource.demand.outputTokens / 1_000_000) * pricing.outputPerMillion;
676
+ }
677
+ const subtotal = roundUsd(knownSubtotalUsd);
678
+ return unknownResources.size === 0
679
+ ? { status: "known", valueUsd: subtotal }
680
+ : { status: "unknown", knownSubtotalUsd: subtotal, unknownResources: [...unknownResources].sort() };
681
+ }
682
+
683
+ function normalizeCost(value) {
684
+ if (!isRecord(value)) throw new Error("Resource cost must be an object.");
685
+ if (value.status === "known") {
686
+ assertAllowedKeys(value, "Known resource cost", ["status", "valueUsd"]);
687
+ return { status: "known", valueUsd: nonNegativeNumber(value.valueUsd, "Resource cost valueUsd", false) };
688
+ }
689
+ if (value.status === "unknown") {
690
+ assertAllowedKeys(value, "Unknown resource cost", ["status", "knownSubtotalUsd", "unknownResources"]);
691
+ return {
692
+ status: "unknown",
693
+ knownSubtotalUsd: nonNegativeNumber(value.knownSubtotalUsd, "Resource cost knownSubtotalUsd", false),
694
+ unknownResources: sortedUniqueStrings(value.unknownResources, "Resource cost unknownResources"),
695
+ };
696
+ }
697
+ throw new Error("Resource cost status must be known or unknown.");
698
+ }
699
+
700
+ function typedNumber(value, label, integer) {
701
+ assertAllowedKeys(value, label, TYPED_VALUE_KEYS);
702
+ if (value.status === "unknown") return unknownValue(value, label);
703
+ if (value.status !== "known" || value.reason !== undefined) throw new Error(`${label} must be typed known or unknown.`);
704
+ return {
705
+ status: "known",
706
+ value: nonNegativeNumber(value.value, `${label} value`, integer),
707
+ };
708
+ }
709
+
710
+ function typedTimestamp(value, label) {
711
+ assertAllowedKeys(value, label, TYPED_VALUE_KEYS);
712
+ if (value.status === "unknown") return unknownValue(value, label);
713
+ if (value.status !== "known" || value.reason !== undefined) throw new Error(`${label} must be typed known or unknown.`);
714
+ return { status: "known", value: isoTimestamp(value.value, `${label} value`) };
715
+ }
716
+
717
+ function unknownValue(value, label) {
718
+ if (value.value !== undefined) throw new Error(`${label} unknown value must not include value.`);
719
+ return { status: "unknown", reason: nonEmptyString(value.reason, `${label} reason`) };
720
+ }
721
+
722
+ function noUnexpectedReason(value, label) {
723
+ if (value.reason !== undefined) throw new Error(`${label} must not include reason.`);
724
+ return { status: "available" };
725
+ }
726
+
727
+ function unknownIssue({ unknown, policy, code, blockers, warnings }) {
728
+ if (!unknown) return;
729
+ (policy === "fail_closed" ? blockers : warnings).push(code);
730
+ }
731
+
732
+ function emptyDemand() {
733
+ return { calls: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, elapsedMs: 0 };
734
+ }
735
+
736
+ function resourceKey(providerId, modelId) {
737
+ return `${providerId}/${modelId}`;
738
+ }
739
+
740
+ function latestObservedAt(observations) {
741
+ return observations.map(observation => observation.adapter.observedAt).sort().at(-1);
742
+ }
743
+
744
+ function earliestResourceExpiry(resources, maxObservationAgeMs) {
745
+ const expiries = resources
746
+ .filter(resource => resource.demand.calls > 0)
747
+ .map(resource => new Date(
748
+ new Date(resource.adapter.observedAt).getTime() + maxObservationAgeMs,
749
+ ).toISOString());
750
+ if (expiries.length === 0) throw new Error("Pending resource workloads require non-zero resource demand.");
751
+ return expiries.sort()[0];
752
+ }
753
+
754
+ function requiredSha256(value, label) {
755
+ if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
756
+ throw new Error(`${label} must be a lowercase SHA-256.`);
757
+ }
758
+ return value;
759
+ }
760
+
761
+ function nonNegativeNumber(value, label, integer) {
762
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || (integer && !Number.isInteger(value))) {
763
+ throw new Error(`${label} must be a non-negative ${integer ? "integer" : "number"}.`);
764
+ }
765
+ return value;
766
+ }
767
+
768
+ function nonNegativeInteger(value, label) {
769
+ return nonNegativeNumber(value, label, true);
770
+ }
771
+
772
+ function positiveInteger(value, label) {
773
+ const normalized = nonNegativeInteger(value, label);
774
+ if (normalized < 1) throw new Error(`${label} must be positive.`);
775
+ return normalized;
776
+ }
777
+
778
+ function enumValue(value, allowed, label) {
779
+ if (!allowed.includes(value)) throw new Error(`${label} is invalid.`);
780
+ return value;
781
+ }
782
+
783
+ function nonEmptyString(value, label) {
784
+ if (typeof value !== "string" || value.length === 0 || /[\u0000-\u001f\u007f]/.test(value)) {
785
+ throw new Error(`${label} must be a non-empty control-free string.`);
786
+ }
787
+ return value;
788
+ }
789
+
790
+ function evidenceReference(value) {
791
+ const reference = nonEmptyString(value, "adapter evidenceRef");
792
+ if (/[?&](?:api[_-]?key|token|secret|signature)=/i.test(reference)) {
793
+ throw new Error("adapter evidenceRef must not contain credentials.");
794
+ }
795
+ return reference;
796
+ }
797
+
798
+ function isoTimestamp(value, label) {
799
+ const timestamp = nonEmptyString(value, label);
800
+ const parsed = new Date(timestamp);
801
+ if (Number.isNaN(parsed.getTime()) || parsed.toISOString() !== timestamp) {
802
+ throw new Error(`${label} must be a canonical UTC ISO timestamp.`);
803
+ }
804
+ return timestamp;
805
+ }
806
+
807
+ function sortedUnique(values) {
808
+ return [...new Set(values)].sort();
809
+ }
810
+
811
+ function sortedUniqueIdentifiers(values, label) {
812
+ const normalized = values.map(value => safeIdentifier(value, label));
813
+ if (new Set(normalized).size !== normalized.length) throw new Error(`${label} must be unique.`);
814
+ return [...normalized].sort();
815
+ }
816
+
817
+ function sortedUniqueStrings(values, label) {
818
+ if (!Array.isArray(values) || values.some(value => typeof value !== "string" || value.length === 0)) {
819
+ throw new Error(`${label} must contain non-empty strings.`);
820
+ }
821
+ if (new Set(values).size !== values.length) throw new Error(`${label} must be unique.`);
822
+ const sorted = [...values].sort();
823
+ if (JSON.stringify(sorted) !== JSON.stringify(values)) throw new Error(`${label} must be sorted.`);
824
+ return sorted;
825
+ }
826
+
827
+ function roundUsd(value) {
828
+ return Number(value.toFixed(12));
829
+ }