@aiwg/cli 2026.9.19 → 2026.9.21

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 (272) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +58 -6
  2. package/agentic/code/providers/model-capabilities.v1.json +34 -0
  3. package/agentic/code/providers/model-catalog.v1.json +55 -13
  4. package/bin/aiwg.mjs +2 -0
  5. package/dist/src/api/index.d.ts +1 -0
  6. package/dist/src/api/index.js +1 -0
  7. package/dist/src/artifacts/enrichment/cli.js +1 -1
  8. package/dist/src/artifacts/views/cli.js +1 -1
  9. package/dist/src/audit/operator-decision.js +10 -0
  10. package/dist/src/auth/credential-store.js +127 -27
  11. package/dist/src/catalog/builtin-models.json +272 -40
  12. package/dist/src/catalog/cli.js +1 -1
  13. package/dist/src/catalog/cli.mjs +1 -1
  14. package/dist/src/cli/handlers/effect.js +872 -0
  15. package/dist/src/cli/handlers/help.js +2 -0
  16. package/dist/src/cli/handlers/index.js +4 -2
  17. package/dist/src/cli/handlers/models.js +41 -10
  18. package/dist/src/cli/handlers/ralph.js +1 -1
  19. package/dist/src/cli/handlers/sessions.js +113 -37
  20. package/dist/src/cli/handlers/subcommands.js +31 -0
  21. package/dist/src/cli/handlers/use.js +46 -9
  22. package/dist/src/cli/handlers/utilities.js +10 -0
  23. package/dist/src/cli/handlers/workspace.js +3 -1
  24. package/dist/src/cli/scope-resolver.js +112 -9
  25. package/dist/src/cli/services/deployment-verification.js +5 -0
  26. package/dist/src/config/aiwg-config.js +3 -0
  27. package/dist/src/decision/acceptance-rollout.js +93 -0
  28. package/dist/src/decision/acceptance.js +175 -0
  29. package/dist/src/decision/adapters/jev.js +234 -13
  30. package/dist/src/decision/adapters/llm-subagent.js +60 -12
  31. package/dist/src/decision/admission.js +550 -0
  32. package/dist/src/decision/batch-benchmark.js +47 -0
  33. package/dist/src/decision/batch-receipts/accounting.js +75 -0
  34. package/dist/src/decision/batch-receipts/export.js +40 -0
  35. package/dist/src/decision/batch-receipts/index.js +9 -0
  36. package/dist/src/decision/batch-receipts/protection.js +40 -0
  37. package/dist/src/decision/batch-receipts/receipt.js +44 -0
  38. package/dist/src/decision/batch-receipts/result-store.js +341 -0
  39. package/dist/src/decision/batch-receipts/store.js +362 -0
  40. package/dist/src/decision/batch-receipts/types.js +2 -0
  41. package/dist/src/decision/batch-receipts/validate.js +127 -0
  42. package/dist/src/decision/batch.js +68 -0
  43. package/dist/src/decision/calibration/index.js +4 -0
  44. package/dist/src/decision/calibration/receipts.js +130 -0
  45. package/dist/src/decision/calibration/registry.js +258 -0
  46. package/dist/src/decision/calibration/types.js +2 -0
  47. package/dist/src/decision/compile-cache/benchmark.js +62 -0
  48. package/dist/src/decision/compile-cache/compatibility.js +65 -0
  49. package/dist/src/decision/compile-cache/file-store.js +280 -0
  50. package/dist/src/decision/compile-cache/identity.js +26 -0
  51. package/dist/src/decision/compile-cache/index.js +9 -0
  52. package/dist/src/decision/compile-cache/prefix.js +21 -0
  53. package/dist/src/decision/compile-cache/records.js +86 -0
  54. package/dist/src/decision/compile-cache/runtime.js +68 -0
  55. package/dist/src/decision/compile-cache/store.js +108 -0
  56. package/dist/src/decision/compile-cache/types.js +2 -0
  57. package/dist/src/decision/context-plan.js +234 -0
  58. package/dist/src/decision/context-qualification.js +60 -0
  59. package/dist/src/decision/ensemble/aggregate.js +215 -0
  60. package/dist/src/decision/ensemble/contract.js +316 -0
  61. package/dist/src/decision/ensemble/drift.js +51 -0
  62. package/dist/src/decision/ensemble/index.js +6 -0
  63. package/dist/src/decision/ensemble/report.js +71 -0
  64. package/dist/src/decision/ensemble/types.js +2 -0
  65. package/dist/src/decision/evaluate.js +1024 -78
  66. package/dist/src/decision/file-lifecycle-store.js +168 -0
  67. package/dist/src/decision/graph-beam.js +40 -0
  68. package/dist/src/decision/graph-budget.js +110 -0
  69. package/dist/src/decision/graph-decision-bridge.js +93 -0
  70. package/dist/src/decision/graph-evidence.js +123 -0
  71. package/dist/src/decision/graph-flow-adapter.js +56 -0
  72. package/dist/src/decision/graph-flow.js +105 -0
  73. package/dist/src/decision/graph-parallel.js +79 -0
  74. package/dist/src/decision/graph-patterns.js +20 -0
  75. package/dist/src/decision/graph-run-store.js +66 -0
  76. package/dist/src/decision/graph-run.js +166 -0
  77. package/dist/src/decision/graph-skill-bridge.js +91 -0
  78. package/dist/src/decision/graph-templates.js +56 -0
  79. package/dist/src/decision/graph.js +115 -0
  80. package/dist/src/decision/index.js +35 -0
  81. package/dist/src/decision/job-accounting.js +93 -0
  82. package/dist/src/decision/job-contract.js +186 -0
  83. package/dist/src/decision/job-effects.js +95 -0
  84. package/dist/src/decision/job-evaluate.js +80 -0
  85. package/dist/src/decision/job-gateway.js +121 -0
  86. package/dist/src/decision/job-http.js +194 -0
  87. package/dist/src/decision/job-payload-store.js +258 -0
  88. package/dist/src/decision/job-poll-limiter.js +92 -0
  89. package/dist/src/decision/job-quota.js +158 -0
  90. package/dist/src/decision/job-runtime.js +240 -0
  91. package/dist/src/decision/job-scheduler.js +26 -0
  92. package/dist/src/decision/job-service.js +117 -0
  93. package/dist/src/decision/job-store.js +264 -0
  94. package/dist/src/decision/job-worker.js +141 -0
  95. package/dist/src/decision/lifecycle.js +148 -0
  96. package/dist/src/decision/patterns/artifacts.js +173 -0
  97. package/dist/src/decision/patterns/catalog.js +108 -0
  98. package/dist/src/decision/patterns/durable-review.js +237 -0
  99. package/dist/src/decision/patterns/index.js +8 -0
  100. package/dist/src/decision/patterns/playground.js +401 -0
  101. package/dist/src/decision/patterns/recorded.js +43 -0
  102. package/dist/src/decision/patterns/specs.js +174 -0
  103. package/dist/src/decision/patterns/types.js +2 -0
  104. package/dist/src/decision/portable-secrets.js +53 -0
  105. package/dist/src/decision/projection.js +210 -0
  106. package/dist/src/decision/qualification/capture.js +67 -0
  107. package/dist/src/decision/qualification/drift.js +68 -0
  108. package/dist/src/decision/qualification/gate-evidence.js +172 -0
  109. package/dist/src/decision/qualification/gates.js +83 -0
  110. package/dist/src/decision/qualification/index.js +12 -0
  111. package/dist/src/decision/qualification/load.js +275 -0
  112. package/dist/src/decision/qualification/manifest.js +37 -0
  113. package/dist/src/decision/qualification/privacy.js +41 -0
  114. package/dist/src/decision/qualification/quality.js +213 -0
  115. package/dist/src/decision/qualification/release.js +116 -0
  116. package/dist/src/decision/qualification/runner.js +408 -0
  117. package/dist/src/decision/qualification/types.js +2 -0
  118. package/dist/src/decision/receipts.js +30 -7
  119. package/dist/src/decision/result-cache/index.js +6 -0
  120. package/dist/src/decision/result-cache/integrity.js +28 -0
  121. package/dist/src/decision/result-cache/key.js +28 -0
  122. package/dist/src/decision/result-cache/service.js +92 -0
  123. package/dist/src/decision/result-cache/store.js +327 -0
  124. package/dist/src/decision/result-cache/types.js +3 -0
  125. package/dist/src/decision/review/authorization.js +110 -0
  126. package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
  127. package/dist/src/decision/review/index.js +12 -0
  128. package/dist/src/decision/review/migrate.js +34 -0
  129. package/dist/src/decision/review/operator-audit.js +59 -0
  130. package/dist/src/decision/review/recovery-adapters.js +252 -0
  131. package/dist/src/decision/review/recovery.js +33 -0
  132. package/dist/src/decision/review/service.js +498 -0
  133. package/dist/src/decision/review/store.js +219 -0
  134. package/dist/src/decision/review/types.js +2 -0
  135. package/dist/src/decision/review/validate.js +205 -0
  136. package/dist/src/decision/scheduler.js +115 -0
  137. package/dist/src/decision/telemetry/context.js +42 -0
  138. package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
  139. package/dist/src/decision/telemetry/exporter.js +77 -0
  140. package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
  141. package/dist/src/decision/telemetry/index.js +13 -0
  142. package/dist/src/decision/telemetry/mapping.js +146 -0
  143. package/dist/src/decision/telemetry/metrics.js +164 -0
  144. package/dist/src/decision/telemetry/otlp-http.js +183 -0
  145. package/dist/src/decision/telemetry/redaction.js +93 -0
  146. package/dist/src/decision/telemetry/retention.js +109 -0
  147. package/dist/src/decision/telemetry/runtime.js +310 -0
  148. package/dist/src/decision/telemetry/trace.js +158 -0
  149. package/dist/src/decision/telemetry/types.js +3 -0
  150. package/dist/src/decision/validate.js +135 -5
  151. package/dist/src/effects/checkpoint-sinks.js +94 -0
  152. package/dist/src/effects/checkpoint.js +76 -0
  153. package/dist/src/effects/decision-links.js +54 -0
  154. package/dist/src/effects/errors.js +48 -0
  155. package/dist/src/effects/identity.js +178 -0
  156. package/dist/src/effects/index.js +25 -0
  157. package/dist/src/effects/keyring.js +160 -0
  158. package/dist/src/effects/keys.js +130 -0
  159. package/dist/src/effects/ledger.js +435 -0
  160. package/dist/src/effects/lifecycle.js +110 -0
  161. package/dist/src/effects/lock-recovery.js +269 -0
  162. package/dist/src/effects/reader.js +103 -0
  163. package/dist/src/effects/records.js +114 -0
  164. package/dist/src/effects/redaction.js +51 -0
  165. package/dist/src/effects/schema.js +73 -0
  166. package/dist/src/effects/store.js +239 -0
  167. package/dist/src/effects/types.js +49 -0
  168. package/dist/src/effects/verifiers/decision.js +168 -0
  169. package/dist/src/effects/verifiers/file.js +106 -0
  170. package/dist/src/effects/verifiers/git.js +206 -0
  171. package/dist/src/effects/verifiers/index.js +90 -0
  172. package/dist/src/effects/verifiers/registry.js +148 -0
  173. package/dist/src/effects/verifiers/review.js +123 -0
  174. package/dist/src/effects/verifiers/tracker.js +299 -0
  175. package/dist/src/effects/verifiers/types.js +21 -0
  176. package/dist/src/effects/verify.js +295 -0
  177. package/dist/src/evidence/bundle.js +2 -0
  178. package/dist/src/extensions/commands/definitions.js +28 -0
  179. package/dist/src/extensions/manifest.js +1 -0
  180. package/dist/src/marketplace/provenance.js +25 -27
  181. package/dist/src/mcp/cli.mjs +11 -1
  182. package/dist/src/mcp/grok-build-config.mjs +189 -0
  183. package/dist/src/mcp/registry.js +21 -1
  184. package/dist/src/mcp/registry.mjs +20 -1
  185. package/dist/src/models/grok-build-models.js +356 -0
  186. package/dist/src/models/index.js +1 -0
  187. package/dist/src/models/model-capabilities.v1.json +34 -0
  188. package/dist/src/models/model-catalog.v1.json +55 -13
  189. package/dist/src/models/model-discovery.js +41 -5
  190. package/dist/src/models/provider-policy.js +3 -2
  191. package/dist/src/plugin/skill-command-translator.js +1 -0
  192. package/dist/src/providers/capability-matrix.yaml +58 -6
  193. package/dist/src/providers/grok-build-acp.js +192 -0
  194. package/dist/src/providers/grok-build-dispatch.js +251 -0
  195. package/dist/src/providers/grok-build-headless.js +170 -0
  196. package/dist/src/providers/muse-paths.js +105 -0
  197. package/dist/src/providers/provider-definitions.js +93 -7
  198. package/dist/src/providers/provider-definitions.mjs +21 -1
  199. package/dist/src/providers/provider-inventory.js +3 -0
  200. package/dist/src/security/artifact-attestation.js +4 -8
  201. package/dist/src/security/signing.js +119 -0
  202. package/dist/src/sessions/adapters/grok-build.js +187 -0
  203. package/dist/src/sessions/adapters/muse.js +412 -0
  204. package/dist/src/sessions/contracts.js +1 -1
  205. package/dist/src/sessions/grok-build-cli.js +196 -0
  206. package/dist/src/sessions/index.js +3 -0
  207. package/dist/src/sessions/workspace-discovery.js +9 -5
  208. package/dist/src/skills/deployer.js +33 -5
  209. package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
  210. package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
  211. package/dist/src/smiths/context-pipeline/finalization.js +1 -32
  212. package/dist/src/smiths/context-pipeline/generator.js +12 -0
  213. package/dist/src/smiths/context-pipeline/index.js +1 -1
  214. package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
  215. package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
  216. package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
  217. package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
  218. package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
  219. package/dist/src/smiths/hook-bridge/index.js +3 -0
  220. package/dist/src/smiths/hook-bridge/shim.js +2 -0
  221. package/dist/src/smiths/hook-bridge/types.js +5 -0
  222. package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
  223. package/dist/src/storage/protected-files.js +102 -0
  224. package/dist/src/tracker/forge-http.js +371 -0
  225. package/dist/src/tracker/project-inputs.js +41 -0
  226. package/package.json +2 -1
  227. package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
  228. package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
  229. package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
  230. package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
  231. package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
  232. package/schemas/decision/DecisionBinding.schema.json +309 -0
  233. package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
  234. package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
  235. package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
  236. package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
  237. package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
  238. package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
  239. package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
  240. package/schemas/decision/DecisionDefinition.schema.json +159 -0
  241. package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
  242. package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
  243. package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
  244. package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
  245. package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
  246. package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
  247. package/schemas/decision/DecisionJob.v1.schema.json +71 -0
  248. package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
  249. package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
  250. package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
  251. package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
  252. package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
  253. package/schemas/decision/DecisionResult.schema.json +524 -0
  254. package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
  255. package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
  256. package/schemas/decision/DecisionReview.schema.json +29 -0
  257. package/schemas/decision/DecisionRuleset.schema.json +313 -0
  258. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
  259. package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
  260. package/schemas/decision/RulesetResult.schema.json +721 -0
  261. package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
  262. package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
  263. package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
  264. package/tools/agents/deploy-agents.mjs +21 -6
  265. package/tools/agents/providers/base.mjs +7 -4
  266. package/tools/agents/providers/codex.mjs +1 -1
  267. package/tools/agents/providers/grok-build.d.mts +7 -0
  268. package/tools/agents/providers/grok-build.mjs +249 -9
  269. package/tools/agents/providers/model-role.mjs +6 -3
  270. package/tools/agents/providers/muse-hooks.mjs +785 -0
  271. package/tools/agents/providers/muse-paths.mjs +112 -0
  272. package/tools/agents/providers/muse.mjs +336 -0
@@ -0,0 +1,310 @@
1
+ import { extractTraceContext, transportTraceContext } from './context.js';
2
+ import { mapAdmissionEvidence, mapDecisionAttempt, mapDecisionResult, mapRulesetResult } from './mapping.js';
3
+ import { DecisionTraceBuilder, recordBatchReceiptTrace } from './trace.js';
4
+ import { recordDecisionSpanMetrics } from './metrics.js';
5
+ import { sanitizeAttributes } from './redaction.js';
6
+ // Keyed by the per-invocation telemetry object, which request snapshots copy by reference.
7
+ const TRACES = new WeakMap();
8
+ const USAGE_KEYS = ['gen_ai.usage.input_tokens', 'gen_ai.usage.output_tokens', 'aiwg.usage.cost_usd',
9
+ 'aiwg.usage.cost_provenance', 'aiwg.provider.request_id', 'aiwg.provider.request_id_source'];
10
+ /**
11
+ * Records metadata-only spans while one evaluator invocation runs. Spans carry
12
+ * live start/end times, are buffered in start order and flushed once after the
13
+ * terminal result. Every failure disables the trace; none reaches the decision.
14
+ */
15
+ export class DecisionRuntimeTrace {
16
+ telemetry;
17
+ now;
18
+ builder;
19
+ root;
20
+ validate;
21
+ batchSpans = new Map();
22
+ disabled = false;
23
+ constructor(telemetry, now) {
24
+ this.telemetry = telemetry;
25
+ this.now = now;
26
+ this.builder = new DecisionTraceBuilder(telemetry.ids, now);
27
+ this.root = this.builder.startSpan('decision.workflow', telemetry.parent ? { parent: telemetry.parent } : {});
28
+ }
29
+ /** Start the root span and bind the trace to a request copy. Invalid ID sources disable telemetry. */
30
+ static begin(request) {
31
+ if (!request.telemetry)
32
+ return { request, trace: null };
33
+ try {
34
+ const telemetry = { ...request.telemetry };
35
+ const trace = new DecisionRuntimeTrace(telemetry, request.now ?? Date.now);
36
+ TRACES.set(telemetry, trace);
37
+ return { request: { ...request, telemetry }, trace };
38
+ }
39
+ catch {
40
+ return { request, trace: null };
41
+ }
42
+ }
43
+ /** W3C traceparent of the workflow span, for durable receipts created by this invocation. */
44
+ get traceParent() {
45
+ return this.disabled ? undefined : transportTraceContext(this.root.context).traceparent;
46
+ }
47
+ validated(rejected) {
48
+ this.guard(() => {
49
+ if (this.validate)
50
+ return;
51
+ const outcome = rejected ? 'rejected' : 'accepted';
52
+ this.validate = this.builder.startSpan('decision.validate', { parent: this.root.context,
53
+ startTimeUnixMs: this.root.startTimeUnixMs,
54
+ attributes: { 'aiwg.validation.outcome': outcome }, provenance: { 'aiwg.validation.outcome': 'client-derived' } });
55
+ this.builder.endSpan(this.validate, rejected ? 'error' : 'ok');
56
+ });
57
+ }
58
+ startAttempt(alias, target, ordinal, fallback = false) {
59
+ return this.guard(() => {
60
+ // Known before dispatch, so an attempt whose outcome is never observed still names its route.
61
+ const attributes = { 'aiwg.decision.alias': alias, 'aiwg.adapter.id': target.adapter,
62
+ 'aiwg.adapter.version': target.adapterVersion, 'gen_ai.request.model': target.model,
63
+ 'aiwg.attempt.ordinal': ordinal, 'aiwg.route.fallback': fallback };
64
+ return this.open(this.builder.startSpan('decision.attempt', { parent: this.root.context, attributes,
65
+ provenance: Object.fromEntries(Object.keys(attributes).map(key => [key, 'client-derived'])) }), ordinal);
66
+ });
67
+ }
68
+ endAttempt(handle, attempt) {
69
+ this.guard(() => {
70
+ if (!handle)
71
+ return;
72
+ const mapped = mapDecisionAttempt(attempt);
73
+ this.builder.annotate(handle.span, mapped.attributes, mapped.provenance);
74
+ if (attempt.termination)
75
+ handle.span.events.push({ name: 'attempt.terminated', timeUnixMs: this.now(),
76
+ attributes: { 'aiwg.attempt.termination': attempt.termination } });
77
+ this.builder.endSpan(handle.span, attempt.status === 'success' ? 'ok' : 'error');
78
+ });
79
+ }
80
+ /**
81
+ * Open the live D05 `decision.admit` span for one admission decision. A granted
82
+ * lease stays open until release, so breaker transitions caused by the attempt
83
+ * land inside the span; a rejection or deferral closes it at once. Evidence is
84
+ * metadata-only by contract: principal and workspace IDs never reach the span.
85
+ */
86
+ startAdmission(alias, adapterId, startedAtMs, evidence) {
87
+ const handle = this.guard(() => {
88
+ const mapped = mapAdmissionEvidence(evidence);
89
+ return { closed: false, span: this.builder.startSpan('decision.admit', { parent: this.root.context, startTimeUnixMs: startedAtMs,
90
+ attributes: { ...mapped.attributes, 'aiwg.decision.alias': alias, 'aiwg.adapter.id': adapterId },
91
+ provenance: { ...mapped.provenance, 'aiwg.decision.alias': 'client-derived', 'aiwg.adapter.id': 'client-derived' } }) };
92
+ });
93
+ if (evidence.decision !== 'admit')
94
+ this.endAdmission(handle, evidence);
95
+ return handle;
96
+ }
97
+ /** Close an admission span once; later calls are ignored. */
98
+ endAdmission(handle, evidence) {
99
+ this.guard(() => {
100
+ if (!handle || handle.closed)
101
+ return;
102
+ handle.closed = true;
103
+ for (const change of evidence.breakerTransitions ?? []) {
104
+ handle.span.events.push({ name: 'breaker.transition', timeUnixMs: this.now(),
105
+ attributes: { 'aiwg.breaker.from': change.from, 'aiwg.breaker.to': change.to } });
106
+ }
107
+ this.builder.endSpan(handle.span, evidence.decision === 'admit' ? 'ok' : evidence.decision === 'reject' ? 'error' : 'unset');
108
+ });
109
+ }
110
+ /** Record one D10 projection step, under its attempt span when there is one. */
111
+ projected(alias, parent, startedAtMs, result) {
112
+ this.guard(() => {
113
+ const attributes = { 'aiwg.decision.alias': alias, 'aiwg.projection.mode': result.mode,
114
+ 'aiwg.projection.outcome': result.outcome };
115
+ if (result.reason)
116
+ attributes['aiwg.projection.reason'] = result.reason;
117
+ if (result.fieldCount !== undefined)
118
+ attributes['aiwg.projection.field_count'] = result.fieldCount;
119
+ if (result.incompleteContext !== undefined)
120
+ attributes['aiwg.projection.incomplete_context'] = result.incompleteContext;
121
+ if (result.automaticActionAllowed !== undefined) {
122
+ attributes['aiwg.projection.automatic_action_allowed'] = result.automaticActionAllowed;
123
+ }
124
+ const span = this.builder.startSpan('decision.project', { parent: (parent?.span ?? this.root).context,
125
+ startTimeUnixMs: startedAtMs, attributes,
126
+ provenance: Object.fromEntries(Object.keys(attributes).map(key => [key, 'client-derived'])) });
127
+ this.builder.endSpan(span, result.outcome === 'allowed' ? 'ok' : 'error');
128
+ });
129
+ }
130
+ retryScheduled(handle, delayMs) {
131
+ this.guard(() => {
132
+ if (!handle)
133
+ return;
134
+ this.builder.annotate(handle.span, { 'aiwg.retry.delay_ms': delayMs }, { 'aiwg.retry.delay_ms': 'client-derived' });
135
+ handle.span.events.push({ name: 'retry.scheduled', timeUnixMs: this.now(), attributes: { 'aiwg.retry.delay_ms': delayMs } });
136
+ });
137
+ }
138
+ /** Open the shared request span before dispatch; its context is what the transport receives. */
139
+ startBatch(aliases, groupId, ordinal) {
140
+ return this.guard(() => {
141
+ const span = this.builder.startSpan('decision.batch.request', { parent: this.root.context,
142
+ attributes: { 'aiwg.batch.id': groupId, 'aiwg.batch.mode': 'native', 'aiwg.batch.item_count': aliases.length,
143
+ 'aiwg.attempt.ordinal': ordinal },
144
+ provenance: { 'aiwg.batch.id': 'client-derived', 'aiwg.batch.mode': 'client-derived',
145
+ 'aiwg.batch.item_count': 'client-derived', 'aiwg.attempt.ordinal': 'client-derived' } });
146
+ for (const alias of aliases)
147
+ this.batchSpans.set(alias, [...this.batchSpans.get(alias) ?? [], span]);
148
+ return this.open(span, ordinal);
149
+ });
150
+ }
151
+ /**
152
+ * Close one batch plan. A durable receipt is the accounting authority and is
153
+ * bridged through `recordBatchReceiptTrace`; without one, shared usage comes
154
+ * from the single transport response. A replayed receipt dispatched nothing
155
+ * here, so it only links back to the trace that created it.
156
+ */
157
+ finishBatch(handles, receipt) {
158
+ this.guard(() => {
159
+ if (receipt && handles.length === 0) {
160
+ this.linkOrigin(receipt.traceParent, 'batch', { 'aiwg.batch.id': receipt.batchId });
161
+ return;
162
+ }
163
+ if (receipt) {
164
+ recordBatchReceiptTrace(this.builder, receipt, this.root.context, new Map(handles.map(handle => [handle.ordinal, handle.span])));
165
+ return;
166
+ }
167
+ for (const handle of handles) {
168
+ if (!handle.response || handle.span.status !== 'unset')
169
+ continue;
170
+ const answers = handle.response.answers.map(answer => answer.observation);
171
+ const requestIds = [...new Set(answers.map(answer => answer.requestId).filter((id) => id !== null))];
172
+ const models = [...new Set(answers.map(answer => answer.actualModel).filter((model) => model !== null))];
173
+ this.builder.recordBatchUsage(handle.span, String(handle.span.attributes['aiwg.batch.id']), handle.response.sharedUsage, { requestId: requestIds.length === 1 ? requestIds[0] : null, actualModel: models.length === 1 ? models[0] : null }, { accountingKey: handle.span.context.spanId });
174
+ this.builder.endSpan(handle.span, answers.every(answer => answer.status === 'success') ? 'ok' : 'error');
175
+ }
176
+ });
177
+ }
178
+ /** Link the workflow to the trace that created a durable receipt this invocation is replaying. */
179
+ linkOrigin(traceParent, relationship, attributes) {
180
+ this.guard(() => {
181
+ const origin = traceParent ? extractTraceContext({ traceparent: traceParent }) : null;
182
+ if (!origin || (origin.traceId === this.root.context.traceId && origin.spanId === this.root.context.spanId)
183
+ || this.root.links.some(link => link.traceId === origin.traceId && link.spanId === origin.spanId))
184
+ return;
185
+ this.root.links.push({ traceId: origin.traceId, spanId: origin.spanId, relationship,
186
+ ...(Object.keys(attributes).length ? { attributes: sanitizeAttributes(attributes) } : {}) });
187
+ });
188
+ }
189
+ /** Record normalize/accept for one finished evaluation, linked to any shared batch request. */
190
+ decided(result) {
191
+ this.guard(() => {
192
+ const batches = this.batchSpans.get(result.spec.alias) ?? [];
193
+ const links = batches.map((span) => ({ traceId: span.context.traceId, spanId: span.context.spanId,
194
+ relationship: 'batch' }));
195
+ result.spec.attempts.forEach((attempt, index) => {
196
+ const request = batches[index];
197
+ // Native answers without a request span from this invocation are replayed evidence, not new work.
198
+ if (attempt.batch?.mode !== 'native' || !request)
199
+ return;
200
+ const mapped = withoutUsage(mapDecisionAttempt(attempt));
201
+ const span = this.builder.startSpan('decision.attempt', { parent: this.root.context,
202
+ startTimeUnixMs: request.startTimeUnixMs, links: [links[index]],
203
+ attributes: { ...mapped.attributes, 'aiwg.decision.alias': result.spec.alias, 'aiwg.usage.scope': 'batch-answer' },
204
+ provenance: { ...mapped.provenance, 'aiwg.decision.alias': 'client-derived', 'aiwg.usage.scope': 'client-derived' } });
205
+ this.builder.endSpan(span, attempt.status === 'success' ? 'ok' : 'error', request.endTimeUnixMs);
206
+ });
207
+ const mapped = mapDecisionResult(result);
208
+ const normalize = this.builder.startSpan('decision.normalize', { parent: this.root.context, links,
209
+ attributes: mapped.attributes, provenance: mapped.provenance });
210
+ this.builder.endSpan(normalize, result.spec.reason === 'invalid-output' ? 'error' : 'ok');
211
+ const accept = this.builder.startSpan('decision.accept', { parent: this.root.context, links,
212
+ attributes: mapped.attributes, provenance: mapped.provenance });
213
+ this.builder.endSpan(accept, result.spec.status === 'error' || result.spec.status === 'cancelled' ? 'error' : 'ok');
214
+ });
215
+ }
216
+ /** Close the workflow and flush. Exporter and metric failures are isolated per span. */
217
+ async finish(result) {
218
+ if (this.disabled)
219
+ return;
220
+ let spans;
221
+ try {
222
+ const failed = result.spec.status === 'error' || result.spec.status === 'cancelled';
223
+ const mapped = mapRulesetResult(result);
224
+ this.builder.annotate(this.root, mapped.attributes, mapped.provenance);
225
+ if (result.spec.cache?.disposition === 'cache-hit') {
226
+ // Historical attempts are evidence, not attempts by this caller. Emitting
227
+ // their provider usage again would double-count tokens and fabricate work.
228
+ this.builder.annotate(this.root, { 'aiwg.invocation.id': result.spec.cache.callerInvocationId,
229
+ 'aiwg.cache.layer': 'result', 'aiwg.cache.result': 'hit' }, { 'aiwg.invocation.id': 'client-derived',
230
+ 'aiwg.cache.layer': 'client-derived', 'aiwg.cache.result': 'client-derived' });
231
+ const hit = this.builder.startSpan('decision.cache', { parent: this.root.context,
232
+ attributes: { 'aiwg.cache.layer': 'result', 'aiwg.cache.result': 'hit' },
233
+ provenance: { 'aiwg.cache.layer': 'client-derived', 'aiwg.cache.result': 'client-derived' } });
234
+ this.builder.endSpan(hit, 'ok');
235
+ }
236
+ else {
237
+ this.validated(result.spec.reason === 'invalid-input' || result.spec.reason === 'invalid-definition');
238
+ // A span still open here never observed its outcome, e.g. an uncertain remote dispatch.
239
+ for (const span of this.builder.openSpans()) {
240
+ if (span === this.root)
241
+ continue;
242
+ this.builder.annotate(span, { 'aiwg.decision.reason': result.spec.reason }, { 'aiwg.decision.reason': 'client-derived' });
243
+ this.builder.endSpan(span, 'error');
244
+ }
245
+ const compose = this.builder.startSpan('decision.compose', { parent: this.root.context,
246
+ attributes: mapped.attributes, provenance: mapped.provenance });
247
+ this.builder.endSpan(compose, failed ? 'error' : 'ok');
248
+ if (result.spec.status === 'review') {
249
+ const review = this.builder.startSpan('decision.review', { parent: this.root.context,
250
+ attributes: { 'aiwg.decision.reason': result.spec.reason }, provenance: { 'aiwg.decision.reason': 'client-derived' } });
251
+ this.builder.endSpan(review, 'ok');
252
+ }
253
+ const persist = this.builder.startSpan('decision.persist', { parent: this.root.context,
254
+ attributes: { 'aiwg.persistence.result': result.spec.reason === 'persistence-error' ? 'failed' : 'persisted' },
255
+ provenance: { 'aiwg.persistence.result': 'client-derived' } });
256
+ this.builder.endSpan(persist, result.spec.reason === 'persistence-error' ? 'error' : 'ok');
257
+ }
258
+ this.builder.endSpan(this.root, failed ? 'error' : 'ok');
259
+ spans = this.builder.build().spans;
260
+ }
261
+ catch {
262
+ // Observability must never alter, reject, or mask the authoritative result.
263
+ return;
264
+ }
265
+ finally {
266
+ this.disabled = true;
267
+ }
268
+ if (this.telemetry.metrics)
269
+ for (const span of spans) {
270
+ try {
271
+ recordDecisionSpanMetrics(span, this.telemetry.metrics);
272
+ }
273
+ catch { /* metrics cannot change a decision */ }
274
+ }
275
+ for (const span of spans) {
276
+ try {
277
+ await this.telemetry.hook.emit(span);
278
+ }
279
+ catch { /* isolate individual exporter failures */ }
280
+ }
281
+ }
282
+ open(span, ordinal) {
283
+ return { span, ordinal, traceContext: transportTraceContext(span.context) };
284
+ }
285
+ guard(record) {
286
+ if (this.disabled)
287
+ return undefined;
288
+ try {
289
+ return record();
290
+ }
291
+ catch {
292
+ this.disabled = true;
293
+ return undefined;
294
+ }
295
+ }
296
+ }
297
+ /** The live trace bound to this evaluator request, if telemetry is configured and healthy. */
298
+ export function runtimeTraceOf(request) {
299
+ return request.telemetry ? TRACES.get(request.telemetry) ?? null : null;
300
+ }
301
+ function withoutUsage(mapping) {
302
+ const attributes = { ...mapping.attributes };
303
+ const provenance = { ...mapping.provenance };
304
+ for (const key of USAGE_KEYS) {
305
+ delete attributes[key];
306
+ delete provenance[key];
307
+ }
308
+ return { attributes, provenance };
309
+ }
310
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,158 @@
1
+ import { createTelemetryContext } from './context.js';
2
+ import { sanitizeAttributes, sanitizeOpaqueValue } from './redaction.js';
3
+ import { DECISION_TELEMETRY_SCHEMA_VERSION } from './types.js';
4
+ export class DecisionTraceBuilder {
5
+ ids;
6
+ now;
7
+ spans = [];
8
+ batchUsage = new Set();
9
+ root;
10
+ constructor(ids, now = Date.now) {
11
+ this.ids = ids;
12
+ this.now = now;
13
+ }
14
+ startSpan(name, options = {}) {
15
+ const parent = options.parent ?? this.root;
16
+ const context = createTelemetryContext(this.ids, parent);
17
+ if (!this.root)
18
+ this.root = context;
19
+ const attributes = sanitizeAttributes(options.attributes ?? {});
20
+ const span = {
21
+ schemaVersion: DECISION_TELEMETRY_SCHEMA_VERSION, name, context,
22
+ parentSpanId: parent?.spanId ?? null,
23
+ startTimeUnixMs: options.startTimeUnixMs ?? this.now(), endTimeUnixMs: options.startTimeUnixMs ?? this.now(),
24
+ status: 'unset', attributes,
25
+ provenance: Object.fromEntries(Object.entries(options.provenance ?? {}).filter(([key]) => Object.hasOwn(attributes, key))),
26
+ links: structuredClone(options.links ?? []), events: [],
27
+ };
28
+ this.spans.push(span);
29
+ return span;
30
+ }
31
+ /** Merge allowlisted metadata into a recorded span; unknown or protected keys are dropped. */
32
+ annotate(span, attributes, provenance = {}) {
33
+ const safe = sanitizeAttributes(attributes);
34
+ Object.assign(span.attributes, safe);
35
+ for (const [key, value] of Object.entries(provenance))
36
+ if (Object.hasOwn(safe, key))
37
+ span.provenance[key] = value;
38
+ }
39
+ /** Spans that were started but not yet ended, in start order. */
40
+ openSpans() {
41
+ return this.spans.filter(span => span.status === 'unset');
42
+ }
43
+ endSpan(span, status = 'ok', endTimeUnixMs = this.now()) {
44
+ span.endTimeUnixMs = Math.max(span.startTimeUnixMs, endTimeUnixMs);
45
+ span.status = status;
46
+ }
47
+ /** Shared provider usage belongs to exactly one batch request span. */
48
+ recordBatchUsage(span, batchId, usage, provider = {}, options = {}) {
49
+ if (span.name !== 'decision.batch.request')
50
+ throw new Error('Batch usage can only be recorded on a batch request span');
51
+ const accountingKey = options.accountingKey ?? batchId;
52
+ if (this.batchUsage.has(accountingKey))
53
+ throw new Error(`Batch usage already recorded: ${accountingKey}`);
54
+ this.batchUsage.add(accountingKey);
55
+ span.attributes['aiwg.batch.id'] = batchId;
56
+ span.attributes['gen_ai.usage.input_tokens'] = usage.inputTokens;
57
+ span.attributes['gen_ai.usage.output_tokens'] = usage.outputTokens;
58
+ span.attributes['aiwg.usage.cost_usd'] = usage.costUsd;
59
+ span.attributes['aiwg.usage.scope'] = 'shared-request';
60
+ span.attributes['aiwg.provider.request_id'] = provider.requestId === null || provider.requestId === undefined
61
+ ? null : sanitizeOpaqueValue(provider.requestId, 128);
62
+ span.attributes['gen_ai.response.model'] = provider.actualModel === null || provider.actualModel === undefined
63
+ ? null : sanitizeOpaqueValue(provider.actualModel, 128);
64
+ for (const key of ['gen_ai.usage.input_tokens', 'gen_ai.usage.output_tokens']) {
65
+ span.provenance[key] = span.attributes[key] === null ? 'unknown' : 'provider-fact';
66
+ }
67
+ span.provenance['aiwg.usage.cost_usd'] = span.attributes['aiwg.usage.cost_usd'] === null
68
+ ? 'unknown' : (options.costProvenance ?? 'provider-fact');
69
+ span.provenance['aiwg.provider.request_id'] = provider.requestId === null || provider.requestId === undefined ? 'unknown' : 'provider-fact';
70
+ span.provenance['gen_ai.response.model'] = provider.actualModel === null || provider.actualModel === undefined ? 'unknown' : 'provider-fact';
71
+ }
72
+ build() {
73
+ if (!this.root)
74
+ throw new Error('Cannot build an empty decision trace');
75
+ return { schemaVersion: DECISION_TELEMETRY_SCHEMA_VERSION, traceId: this.root.traceId, spans: structuredClone(this.spans) };
76
+ }
77
+ }
78
+ /**
79
+ * Emit transport-attempt spans from the durable accounting authority. Each
80
+ * consumed provider attempt owns its usage exactly once, including failed
81
+ * retry/fallback attempts. Per-answer allocations remain off these spans.
82
+ *
83
+ * `started` holds live `decision.batch.request` spans keyed by attempt ordinal.
84
+ * The evaluator opens them before dispatch, so the propagated `traceparent`
85
+ * and the live start time belong to the span that later receives the receipt's
86
+ * accounting. Attempts without a live span are reconstructed from the receipt.
87
+ */
88
+ export function recordBatchReceiptTrace(builder, receipt, parent, started = new Map()) {
89
+ const spans = [];
90
+ for (const attempt of receipt.attempts) {
91
+ const live = started.get(attempt.ordinal);
92
+ if (attempt.status === 'not-sent') {
93
+ // Nothing crossed the transport, so no usage belongs to this span.
94
+ if (live) {
95
+ builder.annotate(live, { 'aiwg.decision.status': attempt.status }, { 'aiwg.decision.status': 'client-derived' });
96
+ builder.endSpan(live, 'error', attempt.completedAtEpochMs ?? receipt.updatedAtEpochMs);
97
+ }
98
+ continue;
99
+ }
100
+ const cost = batchCost(attempt.cost);
101
+ const attributes = {
102
+ 'aiwg.batch.id': receipt.batchId,
103
+ 'aiwg.batch.plan_digest': receipt.plan.planDigest,
104
+ 'aiwg.batch.partition_id': receipt.plan.partitionId,
105
+ 'aiwg.batch.item_count': receipt.questionIds.length,
106
+ 'aiwg.batch.result_count': receipt.answerReferences.length,
107
+ 'aiwg.attempt.ordinal': attempt.ordinal,
108
+ 'aiwg.adapter.id': attempt.adapterId,
109
+ 'aiwg.adapter.version': attempt.adapterVersion,
110
+ 'gen_ai.request.model': attempt.requestedModel,
111
+ 'aiwg.decision.status': attempt.status,
112
+ 'aiwg.route.fallback': attempt.fallbackFromAttemptOrdinal !== null,
113
+ };
114
+ const provenance = Object.fromEntries(Object.keys(attributes).map(key => [key, 'client-derived']));
115
+ const span = live ?? builder.startSpan('decision.batch.request', {
116
+ parent, startTimeUnixMs: attempt.dispatchedAtEpochMs ?? receipt.createdAtEpochMs,
117
+ });
118
+ builder.annotate(span, attributes, provenance);
119
+ builder.recordBatchUsage(span, receipt.batchId, {
120
+ inputTokens: attempt.usage.inputTokens,
121
+ outputTokens: attempt.usage.outputTokens,
122
+ costUsd: cost.amountUsd,
123
+ }, { requestId: attempt.providerRequestId, actualModel: attempt.actualModel }, {
124
+ accountingKey: `${receipt.batchId}:${attempt.ordinal}`,
125
+ costProvenance: cost.provenance,
126
+ });
127
+ builder.endSpan(span, attempt.status === 'succeeded' ? 'ok' : 'error', attempt.completedAtEpochMs ?? attempt.dispatchedAtEpochMs ?? receipt.updatedAtEpochMs);
128
+ spans.push(span);
129
+ }
130
+ return spans;
131
+ }
132
+ function batchCost(cost) {
133
+ switch (cost.kind) {
134
+ case 'provider-authoritative': return { amountUsd: cost.amountMicros / 1_000_000, provenance: 'provider-fact' };
135
+ case 'client-derived': return { amountUsd: cost.amountMicros / 1_000_000, provenance: 'client-derived' };
136
+ case 'bounded-unknown':
137
+ case 'unknown': return { amountUsd: null, provenance: 'unknown' };
138
+ }
139
+ }
140
+ export function estimatedUsageAllocation(total, weights, method, version) {
141
+ if (!Number.isSafeInteger(total) || total < 0 || weights.length === 0 || weights.some(weight => !Number.isFinite(weight) || weight < 0))
142
+ throw new Error('Invalid usage allocation input');
143
+ const sum = weights.reduce((left, right) => left + right, 0);
144
+ if (sum <= 0 || !method || !version)
145
+ throw new Error('Usage allocation requires positive weights and versioned method');
146
+ const raw = weights.map(weight => total * weight / sum);
147
+ const values = raw.map(Math.floor);
148
+ let remainder = total - values.reduce((left, right) => left + right, 0);
149
+ const order = raw.map((value, index) => ({ index, fraction: value - values[index] }))
150
+ .sort((left, right) => right.fraction - left.fraction || left.index - right.index);
151
+ for (let index = 0; index < remainder; index += 1)
152
+ values[order[index].index] += 1;
153
+ remainder = total - values.reduce((left, right) => left + right, 0);
154
+ if (remainder !== 0)
155
+ throw new Error('Usage allocation did not reconcile');
156
+ return values.map(value => ({ value, provenance: 'estimate', method, version }));
157
+ }
158
+ //# sourceMappingURL=trace.js.map
@@ -0,0 +1,3 @@
1
+ /** OpenTelemetry-compatible, dependency-free decision telemetry contract. */
2
+ export const DECISION_TELEMETRY_SCHEMA_VERSION = 'decision-telemetry/v1';
3
+ //# sourceMappingURL=types.js.map
@@ -41,6 +41,12 @@ function getValidators() {
41
41
  if (validators)
42
42
  return validators;
43
43
  const ajv = new Ajv2020({ strict: false, allErrors: true, validateFormats: false });
44
+ const admissionSchema = JSON.parse(readFileSync(resolve(schemaRoot(), 'DecisionAdmissionEvidence.v1.schema.json'), 'utf8'));
45
+ const contextEvidenceSchema = JSON.parse(readFileSync(resolve(schemaRoot(), 'DecisionContextEvidence.v1.schema.json'), 'utf8'));
46
+ const providerPrefixSchema = JSON.parse(readFileSync(resolve(schemaRoot(), 'DecisionProviderPrefixEvidence.v1.schema.json'), 'utf8'));
47
+ ajv.addSchema(admissionSchema);
48
+ ajv.addSchema(contextEvidenceSchema);
49
+ ajv.addSchema(providerPrefixSchema);
44
50
  validators = new Map();
45
51
  for (const [kind, filename] of Object.entries(schemaFiles)) {
46
52
  const schema = JSON.parse(readFileSync(resolve(schemaRoot(), filename), 'utf8'));
@@ -103,6 +109,83 @@ export function assertDecisionWriterVersion(value, semantic) {
103
109
  throw new DecisionValidationError(`${semantic} requires ${DECISION_API_VERSION_STRUCTURED}`);
104
110
  }
105
111
  }
112
+ /**
113
+ * Result evidence that exists only in the v1alpha2 DecisionResult and RulesetResult
114
+ * schemas, with the contract addition that owns each field. v1alpha1 results never
115
+ * carry these fields; writers must label any result that does as v1alpha2.
116
+ */
117
+ export const DECISION_RESULT_V1ALPHA2_FIELDS = [
118
+ { field: 'batchResult', scope: 'decision', owner: 'D07', semantic: 'batch-receipt' },
119
+ { field: 'batch', scope: 'attempt', owner: 'D04', semantic: 'batch-provenance' },
120
+ { field: 'admission', scope: 'attempt', owner: 'D05', semantic: 'admission-evidence' },
121
+ { field: 'context', scope: 'decision', owner: 'D06', semantic: 'context-evidence' },
122
+ { field: 'context', scope: 'ruleset', owner: 'D06', semantic: 'context-evidence' },
123
+ { field: 'contextFailure', scope: 'ruleset', owner: 'D06', semantic: 'context-evidence' },
124
+ { field: 'providerPrefix', scope: 'attempt', owner: 'D30', semantic: 'provider-prefix-evidence' },
125
+ { field: 'acceptance', scope: 'decision', owner: 'D08', semantic: 'acceptance-uncertainty' },
126
+ { field: 'calibrationCompatibility', scope: 'decision', owner: 'D09', semantic: 'calibration-pin' },
127
+ { field: 'cache', scope: 'ruleset', owner: 'D15', semantic: 'result-cache-receipt' },
128
+ { field: 'projection', scope: 'ruleset', owner: 'D10', semantic: 'projection-opt-out' },
129
+ ];
130
+ /** JSON paths of every v1alpha2-only field present in a DecisionResult or RulesetResult. */
131
+ export function decisionResultV1Alpha2Fields(value) {
132
+ const found = [];
133
+ const spec = (document) => {
134
+ const inner = document && typeof document === 'object' ? document.spec : undefined;
135
+ return inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : undefined;
136
+ };
137
+ const visitDecision = (document, path) => {
138
+ const decision = spec(document);
139
+ if (!decision)
140
+ return;
141
+ for (const entry of DECISION_RESULT_V1ALPHA2_FIELDS) {
142
+ if (entry.scope === 'decision' && Object.hasOwn(decision, entry.field))
143
+ found.push(`${path}.spec.${entry.field}`);
144
+ }
145
+ if (!Array.isArray(decision.attempts))
146
+ return;
147
+ decision.attempts.forEach((attempt, index) => {
148
+ if (!attempt || typeof attempt !== 'object')
149
+ return;
150
+ for (const entry of DECISION_RESULT_V1ALPHA2_FIELDS) {
151
+ if (entry.scope === 'attempt' && Object.hasOwn(attempt, entry.field))
152
+ found.push(`${path}.spec.attempts[${index}].${entry.field}`);
153
+ }
154
+ });
155
+ };
156
+ const kind = value && typeof value === 'object' ? value.kind : undefined;
157
+ if (kind === 'DecisionResult')
158
+ visitDecision(value, '$');
159
+ if (kind === 'RulesetResult') {
160
+ const ruleset = spec(value);
161
+ for (const entry of DECISION_RESULT_V1ALPHA2_FIELDS) {
162
+ if (entry.scope === 'ruleset' && ruleset && Object.hasOwn(ruleset, entry.field))
163
+ found.push(`$.spec.${entry.field}`);
164
+ }
165
+ const evaluations = ruleset?.evaluations;
166
+ if (evaluations && typeof evaluations === 'object' && !Array.isArray(evaluations)) {
167
+ for (const [alias, evaluation] of Object.entries(evaluations))
168
+ visitDecision(evaluation, `$.spec.evaluations.${alias}`);
169
+ }
170
+ }
171
+ return found;
172
+ }
173
+ /**
174
+ * Writer gate for every runtime DecisionResult/RulesetResult writer, including
175
+ * invocation receipt payloads. A result that carries v1alpha2-only evidence must be
176
+ * labelled v1alpha2; a v1alpha1 label on such a result is rejected before validation.
177
+ */
178
+ export function assertDecisionResultWriterVersion(value) {
179
+ const kind = value && typeof value === 'object' ? value.kind : undefined;
180
+ if (kind !== 'DecisionResult' && kind !== 'RulesetResult') {
181
+ throw new DecisionValidationError('Result writer accepts only DecisionResult or RulesetResult documents');
182
+ }
183
+ const fields = decisionResultV1Alpha2Fields(value);
184
+ if (fields.length && value.apiVersion !== DECISION_API_VERSION_STRUCTURED) {
185
+ throw new DecisionValidationError(`${kind} carrying ${fields.join(', ')} requires ${DECISION_API_VERSION_STRUCTURED}`);
186
+ }
187
+ validateDecisionDocument(value);
188
+ }
106
189
  /** A rollback reader may inspect v1alpha2, but cannot execute or rewrite it. */
107
190
  export function readDecisionDocumentForRollback(value, mode) {
108
191
  validateDecisionDocument(value);
@@ -121,12 +204,50 @@ function deepFreeze(value) {
121
204
  deepFreeze(child);
122
205
  Object.freeze(value);
123
206
  }
207
+ /**
208
+ * Compiled caller-schema validators, keyed by the sha256 of the schema's
209
+ * canonical JSON, so equal content shares a validator and different content
210
+ * never does. Each miss compiles a detached clone in a fresh Ajv instance, so
211
+ * later mutation of the caller's object or another schema's `$id` cannot leak
212
+ * into a cached entry. Bounded LRU; failed compiles are never cached.
213
+ */
214
+ const SCHEMA_CACHE_LIMIT = 256;
215
+ const strictSchemaDigests = new Map();
216
+ const permissiveValidators = new Map();
217
+ function schemaDigest(schema) {
218
+ try {
219
+ return createHash('sha256').update(canonicalJson(schema)).digest('hex');
220
+ }
221
+ catch {
222
+ return null;
223
+ } // Not canonical JSON (for example an undefined member): compile uncached.
224
+ }
225
+ /** Only a cacheable (canonical JSON) schema needs a private copy. */
226
+ function detached(schema, digest) {
227
+ return digest === null ? schema : structuredClone(schema);
228
+ }
229
+ function cached(cache, digest, build) {
230
+ if (digest !== null) {
231
+ const hit = cache.get(digest);
232
+ if (hit !== undefined) {
233
+ cache.delete(digest);
234
+ cache.set(digest, hit);
235
+ return hit;
236
+ }
237
+ }
238
+ const value = build();
239
+ if (digest !== null) {
240
+ cache.set(digest, value);
241
+ if (cache.size > SCHEMA_CACHE_LIMIT)
242
+ cache.delete(cache.keys().next().value);
243
+ }
244
+ return value;
245
+ }
124
246
  export function validateAgainstSchema(schema, value, label) {
125
- assertLocalSchema(schema, label);
126
- const ajv = new Ajv2020({ strict: false, allErrors: true, validateFormats: false });
247
+ const digest = assertLocalSchema(schema, label);
127
248
  let validate;
128
249
  try {
129
- validate = ajv.compile(schema);
250
+ validate = cached(permissiveValidators, digest, () => new Ajv2020({ strict: false, allErrors: true, validateFormats: false }).compile(detached(schema, digest)));
130
251
  }
131
252
  catch (error) {
132
253
  throw new DecisionValidationError(`${label} is not a supported local draft 2020-12 schema: ${errorMessage(error)}`);
@@ -182,6 +303,9 @@ export function validateBinding(binding, ruleset) {
182
303
  }
183
304
  for (const [alias, evaluation] of Object.entries(binding.spec.evaluations)) {
184
305
  for (const target of evaluation.targets) {
306
+ if (target.acceptance.mode === 'primitive-policy' && binding.apiVersion !== DECISION_API_VERSION_STRUCTURED) {
307
+ throw new DecisionValidationError(`${alias} primitive-aware acceptance requires ${DECISION_API_VERSION_STRUCTURED}`);
308
+ }
185
309
  if (target.retry.maxDelayMs < target.retry.initialDelayMs) {
186
310
  throw new DecisionValidationError(`${alias} retry maxDelayMs must be >= initialDelayMs`);
187
311
  }
@@ -267,13 +391,19 @@ function assertLocalSchema(schema, label) {
267
391
  stack.delete(value);
268
392
  };
269
393
  visit(schema, new Set(), 0);
394
+ // The structural walk above runs on every call; only the strict compile is cached.
395
+ const digest = schemaDigest(schema);
270
396
  try {
271
- new Ajv2020({ strictSchema: true, strictTypes: false, strictTuples: false,
272
- strictRequired: false, validateFormats: false }).compile(schema);
397
+ cached(strictSchemaDigests, digest, () => {
398
+ new Ajv2020({ strictSchema: true, strictTypes: false, strictTuples: false,
399
+ strictRequired: false, validateFormats: false }).compile(detached(schema, digest));
400
+ return true;
401
+ });
273
402
  }
274
403
  catch (error) {
275
404
  throw new DecisionValidationError(`${label} uses an unsupported schema construct: ${errorMessage(error)}`);
276
405
  }
406
+ return digest;
277
407
  }
278
408
  function assertPredicateAliases(predicate, aliases) {
279
409
  if ('all' in predicate)