@duckcodeailabs/dql-agent 1.14.2 → 1.14.4

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 (196) hide show
  1. package/dist/agent-run-engine.d.ts +239 -4
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +2924 -85
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-gates.d.ts.map +1 -1
  6. package/dist/agent-run-gates.js +51 -3
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/agent-run-planner.d.ts +6 -0
  9. package/dist/agent-run-planner.d.ts.map +1 -1
  10. package/dist/agent-run-planner.js +2 -0
  11. package/dist/agent-run-planner.js.map +1 -1
  12. package/dist/agent-run-store.d.ts.map +1 -1
  13. package/dist/agent-run-store.js +591 -11
  14. package/dist/agent-run-store.js.map +1 -1
  15. package/dist/agentic/analyst-loop.d.ts +9 -1
  16. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  17. package/dist/agentic/analyst-loop.js +204 -4
  18. package/dist/agentic/analyst-loop.js.map +1 -1
  19. package/dist/agentic/assumptions.d.ts +1 -1
  20. package/dist/agentic/assumptions.d.ts.map +1 -1
  21. package/dist/agentic/assumptions.js.map +1 -1
  22. package/dist/agentic/ledger-tools.d.ts.map +1 -1
  23. package/dist/agentic/ledger-tools.js +8 -1
  24. package/dist/agentic/ledger-tools.js.map +1 -1
  25. package/dist/agentic/orchestrator-policy.d.ts +3 -1
  26. package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
  27. package/dist/agentic/orchestrator-policy.js +1 -1
  28. package/dist/agentic/orchestrator-policy.js.map +1 -1
  29. package/dist/agentic/research-agent.d.ts.map +1 -1
  30. package/dist/agentic/research-agent.js +10 -1
  31. package/dist/agentic/research-agent.js.map +1 -1
  32. package/dist/agentic/sql-authorization.d.ts +8 -0
  33. package/dist/agentic/sql-authorization.d.ts.map +1 -1
  34. package/dist/agentic/sql-authorization.js +3 -0
  35. package/dist/agentic/sql-authorization.js.map +1 -1
  36. package/dist/agentic/tool-loop.d.ts +13 -1
  37. package/dist/agentic/tool-loop.d.ts.map +1 -1
  38. package/dist/agentic/tool-loop.js +441 -32
  39. package/dist/agentic/tool-loop.js.map +1 -1
  40. package/dist/analytical-compatibility.d.ts.map +1 -1
  41. package/dist/analytical-compatibility.js +8 -1
  42. package/dist/analytical-compatibility.js.map +1 -1
  43. package/dist/analytical-failure-repair.d.ts.map +1 -1
  44. package/dist/analytical-failure-repair.js +6 -2
  45. package/dist/analytical-failure-repair.js.map +1 -1
  46. package/dist/analytical-frame.d.ts +42 -1
  47. package/dist/analytical-frame.d.ts.map +1 -1
  48. package/dist/analytical-frame.js +490 -26
  49. package/dist/analytical-frame.js.map +1 -1
  50. package/dist/analytical-orchestration.d.ts +1259 -9
  51. package/dist/analytical-orchestration.d.ts.map +1 -1
  52. package/dist/analytical-orchestration.js +970 -84
  53. package/dist/analytical-orchestration.js.map +1 -1
  54. package/dist/answer-loop.d.ts +230 -10
  55. package/dist/answer-loop.d.ts.map +1 -1
  56. package/dist/answer-loop.js +1182 -85
  57. package/dist/answer-loop.js.map +1 -1
  58. package/dist/answer-shape.d.ts +27 -0
  59. package/dist/answer-shape.d.ts.map +1 -1
  60. package/dist/answer-shape.js +55 -0
  61. package/dist/answer-shape.js.map +1 -1
  62. package/dist/ask-observability/index.d.ts +11 -0
  63. package/dist/ask-observability/index.d.ts.map +1 -0
  64. package/dist/ask-observability/index.js +7 -0
  65. package/dist/ask-observability/index.js.map +1 -0
  66. package/dist/ask-observability/instrumentation.d.ts +35 -0
  67. package/dist/ask-observability/instrumentation.d.ts.map +1 -0
  68. package/dist/ask-observability/instrumentation.js +523 -0
  69. package/dist/ask-observability/instrumentation.js.map +1 -0
  70. package/dist/ask-observability/observer.d.ts +62 -0
  71. package/dist/ask-observability/observer.d.ts.map +1 -0
  72. package/dist/ask-observability/observer.js +537 -0
  73. package/dist/ask-observability/observer.js.map +1 -0
  74. package/dist/ask-observability/portable.d.ts +98 -0
  75. package/dist/ask-observability/portable.d.ts.map +1 -0
  76. package/dist/ask-observability/portable.js +1089 -0
  77. package/dist/ask-observability/portable.js.map +1 -0
  78. package/dist/ask-observability/store.d.ts +130 -0
  79. package/dist/ask-observability/store.d.ts.map +1 -0
  80. package/dist/ask-observability/store.js +1114 -0
  81. package/dist/ask-observability/store.js.map +1 -0
  82. package/dist/ask-observability/types.d.ts +380 -0
  83. package/dist/ask-observability/types.d.ts.map +1 -0
  84. package/dist/ask-observability/types.js +2 -0
  85. package/dist/ask-observability/types.js.map +1 -0
  86. package/dist/ask-observability/utils.d.ts +21 -0
  87. package/dist/ask-observability/utils.d.ts.map +1 -0
  88. package/dist/ask-observability/utils.js +102 -0
  89. package/dist/ask-observability/utils.js.map +1 -0
  90. package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
  91. package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
  92. package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
  93. package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
  94. package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
  95. package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
  96. package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
  97. package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
  98. package/dist/conversation/session-store.d.ts +31 -0
  99. package/dist/conversation/session-store.d.ts.map +1 -1
  100. package/dist/conversation/session-store.js +38 -0
  101. package/dist/conversation/session-store.js.map +1 -1
  102. package/dist/conversation/snapshot.d.ts +4 -1
  103. package/dist/conversation/snapshot.d.ts.map +1 -1
  104. package/dist/conversation/snapshot.js +85 -3
  105. package/dist/conversation/snapshot.js.map +1 -1
  106. package/dist/conversation/turn-trust.d.ts.map +1 -1
  107. package/dist/conversation/turn-trust.js +2 -0
  108. package/dist/conversation/turn-trust.js.map +1 -1
  109. package/dist/embeddings/provider.d.ts +1 -0
  110. package/dist/embeddings/provider.d.ts.map +1 -1
  111. package/dist/embeddings/provider.js +18 -0
  112. package/dist/embeddings/provider.js.map +1 -1
  113. package/dist/generated-analytical-proposal.d.ts +37 -1
  114. package/dist/generated-analytical-proposal.d.ts.map +1 -1
  115. package/dist/generated-analytical-proposal.js +246 -3
  116. package/dist/generated-analytical-proposal.js.map +1 -1
  117. package/dist/governed-relational-compiler.d.ts +15 -0
  118. package/dist/governed-relational-compiler.d.ts.map +1 -1
  119. package/dist/governed-relational-compiler.js +99 -1
  120. package/dist/governed-relational-compiler.js.map +1 -1
  121. package/dist/index.d.ts +21 -12
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +14 -7
  124. package/dist/index.js.map +1 -1
  125. package/dist/intent-controller.d.ts +87 -1
  126. package/dist/intent-controller.d.ts.map +1 -1
  127. package/dist/intent-controller.js +2 -2
  128. package/dist/intent-controller.js.map +1 -1
  129. package/dist/kg/build.js +32 -2
  130. package/dist/kg/build.js.map +1 -1
  131. package/dist/meaning-resolution.d.ts +207 -6
  132. package/dist/meaning-resolution.d.ts.map +1 -1
  133. package/dist/meaning-resolution.js +439 -53
  134. package/dist/meaning-resolution.js.map +1 -1
  135. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  136. package/dist/metadata/analysis-planner.js +78 -7
  137. package/dist/metadata/analysis-planner.js.map +1 -1
  138. package/dist/metadata/catalog.d.ts +14 -0
  139. package/dist/metadata/catalog.d.ts.map +1 -1
  140. package/dist/metadata/catalog.js +29 -2
  141. package/dist/metadata/catalog.js.map +1 -1
  142. package/dist/metadata/meaning-evidence.d.ts +28 -2
  143. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  144. package/dist/metadata/meaning-evidence.js +373 -16
  145. package/dist/metadata/meaning-evidence.js.map +1 -1
  146. package/dist/plan-execution-adapter.d.ts +9 -1
  147. package/dist/plan-execution-adapter.d.ts.map +1 -1
  148. package/dist/plan-execution-adapter.js +93 -13
  149. package/dist/plan-execution-adapter.js.map +1 -1
  150. package/dist/provider-egress.d.ts +28 -11
  151. package/dist/provider-egress.d.ts.map +1 -1
  152. package/dist/provider-egress.js +72 -22
  153. package/dist/provider-egress.js.map +1 -1
  154. package/dist/providers/claude.d.ts +2 -2
  155. package/dist/providers/claude.d.ts.map +1 -1
  156. package/dist/providers/claude.js +201 -37
  157. package/dist/providers/claude.js.map +1 -1
  158. package/dist/providers/dispatch.d.ts +27 -1
  159. package/dist/providers/dispatch.d.ts.map +1 -1
  160. package/dist/providers/dispatch.js +63 -1
  161. package/dist/providers/dispatch.js.map +1 -1
  162. package/dist/providers/gemini.js +10 -10
  163. package/dist/providers/gemini.js.map +1 -1
  164. package/dist/providers/index.d.ts +2 -2
  165. package/dist/providers/index.d.ts.map +1 -1
  166. package/dist/providers/index.js +1 -1
  167. package/dist/providers/index.js.map +1 -1
  168. package/dist/providers/ollama.js +15 -15
  169. package/dist/providers/ollama.js.map +1 -1
  170. package/dist/providers/openai.d.ts +2 -2
  171. package/dist/providers/openai.d.ts.map +1 -1
  172. package/dist/providers/openai.js +213 -43
  173. package/dist/providers/openai.js.map +1 -1
  174. package/dist/providers/types.d.ts +131 -1
  175. package/dist/providers/types.d.ts.map +1 -1
  176. package/dist/providers/types.js.map +1 -1
  177. package/dist/relationship-proof.d.ts +81 -0
  178. package/dist/relationship-proof.d.ts.map +1 -1
  179. package/dist/relationship-proof.js +228 -4
  180. package/dist/relationship-proof.js.map +1 -1
  181. package/dist/requirement-clauses.d.ts +23 -0
  182. package/dist/requirement-clauses.d.ts.map +1 -0
  183. package/dist/requirement-clauses.js +70 -0
  184. package/dist/requirement-clauses.js.map +1 -0
  185. package/dist/resolved-analytical-plan.d.ts +18 -4
  186. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  187. package/dist/resolved-analytical-plan.js +953 -50
  188. package/dist/resolved-analytical-plan.js.map +1 -1
  189. package/dist/router.d.ts +118 -2
  190. package/dist/router.d.ts.map +1 -1
  191. package/dist/router.js +2481 -232
  192. package/dist/router.js.map +1 -1
  193. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  194. package/dist/semantic-bridge/compose.js +23 -4
  195. package/dist/semantic-bridge/compose.js.map +1 -1
  196. package/package.json +4 -4
@@ -0,0 +1,1089 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { basename, join, resolve } from 'node:path';
3
+ import { canonicalJson, fingerprint, pseudo, sha256 } from './utils.js';
4
+ /** Defense-in-depth forbidden content canary. Strict export must pass this. */
5
+ const PROHIBITED = [
6
+ /(?:authorization|cookie|x-api-key|api[_-]?key|password|secret|token)\s*[:=]/i,
7
+ /-----BEGIN [A-Z ]+-----/,
8
+ /\bselect\b[\s\S]{0,120}\bfrom\b/i,
9
+ /https?:\/\//i,
10
+ /(?:^|[\s"'])\/(?:Users|home|tmp|private|var)\//,
11
+ /\b(?:sk|pk|rk)_[A-Za-z0-9_-]{12,}\b/,
12
+ ];
13
+ export function createAskTracePortableBundleV1(trace, options) {
14
+ if (options.profile === 'support' && !options.confirmReviewedIdentifiers) {
15
+ throw Object.assign(new Error('Support export requires --confirm-reviewed-identifiers.'), { code: 'TRACE_EXPORT_REDACTION_FAILED' });
16
+ }
17
+ // Export-scoped pseudonyms and bundle content are deterministic for the same
18
+ // stored receipt. A support engineer can therefore validate/compare bundles
19
+ // without an export timestamp changing every identifier or checksum.
20
+ const createdAt = trace.envelope.completedAt ?? trace.envelope.startedAt;
21
+ const salt = fingerprint({ trace: trace.envelope.traceId, profile: options.profile, schema: 1 });
22
+ const portableTrace = redactTrace(trace, options.profile, salt);
23
+ const receipt = redactRunReceipt(options.runReceipt, trace, options.profile, salt);
24
+ const redactionReceipt = {
25
+ version: 1,
26
+ profile: options.profile,
27
+ identifiers: options.profile === 'strict' ? 'pseudonymized' : 'reviewed_only',
28
+ reviewedQuestionIncluded: options.profile === 'support' && Boolean(options.reviewedQuestion?.trim()),
29
+ generatedAt: createdAt,
30
+ };
31
+ if (options.profile === 'support' && options.reviewedQuestion?.trim()) {
32
+ // A review-provided question goes in one explicitly named safe field; it
33
+ // never flows from an AgentRun or trace store.
34
+ redactionReceipt.reviewedQuestion = options.reviewedQuestion.trim().slice(0, 2_000);
35
+ }
36
+ // Support-only reviewed prose is explicit human input, but it remains
37
+ // untrusted for secrets, SQL, paths, and other prohibited content.
38
+ const canary = scanForProhibited([portableTrace, receipt, redactionReceipt]);
39
+ if (!canary.passed) {
40
+ throw Object.assign(new Error(`Trace export redaction failed: ${canary.findings.join(', ')}`), { code: 'TRACE_EXPORT_REDACTION_FAILED' });
41
+ }
42
+ redactionReceipt.prohibitedContentScan = canary;
43
+ const traceJson = `${canonicalJson(portableTrace)}\n`;
44
+ const receiptJson = `${canonicalJson(receipt)}\n`;
45
+ const redactionJson = `${canonicalJson(redactionReceipt)}\n`;
46
+ const checksums = {
47
+ 'trace.json': sha256(traceJson),
48
+ 'run-receipt.json': sha256(receiptJson),
49
+ 'redaction-receipt.json': sha256(redactionJson),
50
+ };
51
+ const manifest = {
52
+ version: 1,
53
+ schema: 'dql.ask-trace-bundle.v1',
54
+ profile: options.profile,
55
+ createdAt,
56
+ provenance: options.provenance ?? 'unknown',
57
+ bundleFingerprint: fingerprint({ checksums, profile: options.profile, trace: portableTrace.envelope.traceFingerprint ?? portableTrace.envelope.traceId }),
58
+ checksums,
59
+ ...(portableTrace.envelope.traceFingerprint ? { traceFingerprint: portableTrace.envelope.traceFingerprint } : {}),
60
+ traceId: portableTrace.envelope.traceId,
61
+ };
62
+ return { manifest, trace: portableTrace, runReceipt: receipt, redactionReceipt };
63
+ }
64
+ export function exportAskTraceBundleV1(trace, options) {
65
+ const output = resolve(options.outputDirectory);
66
+ if (existsSync(output) && readdirSync(output).length > 0) {
67
+ throw Object.assign(new Error('Trace export directory must be empty; existing bundle files are never overwritten.'), { code: 'TRACE_EXPORT_REDACTION_FAILED' });
68
+ }
69
+ const bundle = createAskTracePortableBundleV1(trace, options);
70
+ mkdirSync(output, { recursive: true });
71
+ const traceJson = `${canonicalJson(bundle.trace)}\n`;
72
+ const receiptJson = `${canonicalJson(bundle.runReceipt)}\n`;
73
+ const redactionJson = `${canonicalJson(bundle.redactionReceipt)}\n`;
74
+ const manifestJson = `${canonicalJson(bundle.manifest)}\n`;
75
+ writeFileSync(join(output, 'trace.json'), traceJson, { encoding: 'utf8', flag: 'wx' });
76
+ writeFileSync(join(output, 'run-receipt.json'), receiptJson, { encoding: 'utf8', flag: 'wx' });
77
+ writeFileSync(join(output, 'redaction-receipt.json'), redactionJson, { encoding: 'utf8', flag: 'wx' });
78
+ writeFileSync(join(output, 'manifest.json'), manifestJson, { encoding: 'utf8', flag: 'wx' });
79
+ return {
80
+ version: 1,
81
+ profile: bundle.manifest.profile,
82
+ bundleFingerprint: bundle.manifest.bundleFingerprint,
83
+ exportedAt: bundle.manifest.createdAt,
84
+ checksums: { 'manifest.json': sha256(manifestJson), ...bundle.manifest.checksums },
85
+ canaryPassed: true,
86
+ ...(bundle.trace.envelope.traceFingerprint ? { traceFingerprint: bundle.trace.envelope.traceFingerprint } : {}),
87
+ };
88
+ }
89
+ export function validateAskTraceBundleV1(directory) {
90
+ const root = resolve(directory);
91
+ const errors = [];
92
+ const names = ['manifest.json', 'trace.json', 'run-receipt.json', 'redaction-receipt.json'];
93
+ for (const name of names)
94
+ if (!existsSync(join(root, name)))
95
+ errors.push(`Missing ${name}.`);
96
+ if (errors.length)
97
+ return { valid: false, errors };
98
+ const parsed = Object.fromEntries(names.map((name) => [name, parseJsonFile(join(root, name), errors)]));
99
+ const manifest = parsed['manifest.json'];
100
+ if (!manifest || manifest.version !== 1 || manifest.schema !== 'dql.ask-trace-bundle.v1')
101
+ errors.push('Unsupported trace bundle manifest.');
102
+ if (manifest) {
103
+ for (const name of ['trace.json', 'run-receipt.json', 'redaction-receipt.json']) {
104
+ const actual = sha256(readFileSync(join(root, name), 'utf8'));
105
+ if (actual !== manifest.checksums[name])
106
+ errors.push(`Checksum mismatch for ${name}.`);
107
+ }
108
+ const expectedBundle = fingerprint({ checksums: manifest.checksums, profile: manifest.profile, trace: manifest.traceFingerprint ?? manifest.traceId });
109
+ if (manifest.bundleFingerprint !== expectedBundle)
110
+ errors.push('Bundle fingerprint mismatch.');
111
+ }
112
+ const scan = scanForProhibited([parsed['trace.json'], parsed['run-receipt.json'], parsed['redaction-receipt.json']]);
113
+ if (!scan.passed)
114
+ errors.push(`Redaction canary failed: ${scan.findings.join(', ')}.`);
115
+ const trace = parsed['trace.json'];
116
+ if (trace)
117
+ errors.push(...validateTraceReceiptV1(trace));
118
+ errors.push(...validateRunReceiptTerminalGapV1(parsed['run-receipt.json'], trace));
119
+ return { valid: errors.length === 0, errors, ...(manifest ? { manifest } : {}) };
120
+ }
121
+ /** Offline-only receipt replay. It validates; it never imports providers/tools/connectors. */
122
+ export function replayAskTraceReceiptV1(directory) {
123
+ const validation = validateAskTraceBundleV1(directory);
124
+ if (!validation.valid)
125
+ return { valid: false, errors: validation.errors };
126
+ const trace = parseJsonFile(join(resolve(directory), 'trace.json'), []);
127
+ const errors = trace ? validateTraceReceiptV1(trace) : ['Trace payload is missing.'];
128
+ return { valid: errors.length === 0, errors, ...(trace ? { trace } : {}) };
129
+ }
130
+ export function compareAskTracesV1(left, right) {
131
+ const reasonCounts = (trace) => new Map(trace.candidateDecisions.reduce((all, candidate) => {
132
+ const key = `${candidate.role}\u0000${candidate.reasonCode}`;
133
+ all.set(key, (all.get(key) ?? 0) + 1);
134
+ return all;
135
+ }, new Map()));
136
+ const leftReasons = reasonCounts(left);
137
+ const rightReasons = reasonCounts(right);
138
+ const candidateReasonChanges = [...new Set([...leftReasons.keys(), ...rightReasons.keys()])]
139
+ .sort()
140
+ .flatMap((key) => {
141
+ const [role, reason] = key.split('\u0000');
142
+ const l = leftReasons.get(key) ?? 0;
143
+ const r = rightReasons.get(key) ?? 0;
144
+ return l === r ? [] : [{ role, reason, left: l, right: r }];
145
+ });
146
+ const coverage = (trace) => new Map(trace.spans.flatMap((span) => span.payload.kind === 'cascade'
147
+ ? span.payload.decision.sourceCoverage.map((entry) => [entry.source, entry.status])
148
+ : []));
149
+ const leftCoverage = coverage(left);
150
+ const rightCoverage = coverage(right);
151
+ const sourceCoverageChanges = [...new Set([...leftCoverage.keys(), ...rightCoverage.keys()])]
152
+ .sort()
153
+ .flatMap((source) => leftCoverage.get(source) === rightCoverage.get(source)
154
+ ? []
155
+ : [{ source, left: leftCoverage.get(source), right: rightCoverage.get(source) }]);
156
+ const count = (trace, name) => trace.spans.filter((span) => span.name === name).length;
157
+ const attempts = (trace) => trace.spans.flatMap((span) => span.payload.kind === 'cascade' ? span.payload.decision.attempts : []).length;
158
+ const compatibleIdentity = left.envelope.traceId === right.envelope.traceId
159
+ || Boolean(left.envelope.traceFingerprint && right.envelope.traceFingerprint && left.envelope.traceFingerprint === right.envelope.traceFingerprint);
160
+ return {
161
+ compatibleIdentity,
162
+ leftTraceId: left.envelope.traceId,
163
+ rightTraceId: right.envelope.traceId,
164
+ selectedTier: {
165
+ left: left.envelope.selectedTier,
166
+ right: right.envelope.selectedTier,
167
+ matches: left.envelope.selectedTier === right.envelope.selectedTier,
168
+ },
169
+ recordingStatus: {
170
+ left: left.envelope.recordingStatus,
171
+ right: right.envelope.recordingStatus,
172
+ matches: left.envelope.recordingStatus === right.envelope.recordingStatus,
173
+ },
174
+ sourceCoverageChanges,
175
+ candidateReasonChanges,
176
+ attempts: { left: attempts(left), right: attempts(right) },
177
+ providerAttempts: { left: count(left, 'provider.attempt'), right: count(right, 'provider.attempt') },
178
+ toolCalls: { left: count(left, 'tool.call'), right: count(right, 'tool.call') },
179
+ sqlAttempts: { left: count(left, 'sql.execute'), right: count(right, 'sql.execute') },
180
+ trustState: { left: left.envelope.trustState, right: right.envelope.trustState, matches: left.envelope.trustState === right.envelope.trustState },
181
+ ...(typeof left.envelope.durationMs === 'number' && typeof right.envelope.durationMs === 'number'
182
+ ? { durationDeltaMs: right.envelope.durationMs - left.envelope.durationMs }
183
+ : {}),
184
+ notes: compatibleIdentity
185
+ ? []
186
+ : ['Trace identities use export-scoped pseudonyms or differ; stage and role comparison remains available.'],
187
+ };
188
+ }
189
+ /** Portable, local JSON mapper — intentionally no OTLP network exporter. */
190
+ export function toOtlpOpenInferenceJsonV1(trace) {
191
+ const spanKind = (span) => {
192
+ if (span.name === 'ask.run' || span.name === 'research.run')
193
+ return 'AGENT';
194
+ if (span.stage === 'retrieval')
195
+ return 'RETRIEVER';
196
+ if (span.stage === 'provider')
197
+ return 'LLM';
198
+ if (span.stage === 'tool' || span.name === 'sql.execute')
199
+ return 'TOOL';
200
+ return 'CHAIN';
201
+ };
202
+ return {
203
+ resourceSpans: [{
204
+ resource: { attributes: [{ key: 'service.name', value: { stringValue: 'dql-ask-local' } }] },
205
+ scopeSpans: [{
206
+ scope: { name: 'dql.ask-observability', version: '1' },
207
+ spans: trace.spans.map((span) => ({
208
+ traceId: trace.envelope.traceId,
209
+ spanId: span.spanId,
210
+ ...(span.parentSpanId ? { parentSpanId: span.parentSpanId } : {}),
211
+ name: span.name,
212
+ startTimeUnixNano: String(Date.parse(span.startedAt) * 1_000_000),
213
+ ...(span.completedAt ? { endTimeUnixNano: String(Date.parse(span.completedAt) * 1_000_000) } : {}),
214
+ status: { code: span.outcome === 'ok' ? 1 : 2, message: span.reasonCode },
215
+ attributes: [
216
+ { key: 'openinference.span.kind', value: { stringValue: spanKind(span) } },
217
+ { key: 'dql.ask.stage', value: { stringValue: span.stage } },
218
+ { key: 'dql.ask.outcome', value: { stringValue: span.outcome } },
219
+ { key: 'dql.ask.reason_code', value: { stringValue: span.reasonCode } },
220
+ ],
221
+ })),
222
+ }],
223
+ }],
224
+ };
225
+ }
226
+ function redactTrace(trace, profile, salt) {
227
+ const mapId = (value, kind = 'id') => profile === 'strict' ? pseudo(value, salt, kind) : value;
228
+ const redactEnvelope = (envelope) => ({
229
+ ...envelope,
230
+ traceId: mapId(envelope.traceId, 'trace'),
231
+ rootSpanId: mapId(envelope.rootSpanId, 'span'),
232
+ runId: mapId(envelope.runId, 'run'),
233
+ ...(envelope.threadId ? { threadId: mapId(envelope.threadId, 'thread') } : {}),
234
+ ...(envelope.snapshotId ? { snapshotId: mapId(envelope.snapshotId, 'snapshot') } : {}),
235
+ ...(envelope.parentTraceId ? { parentTraceId: mapId(envelope.parentTraceId, 'trace') } : {}),
236
+ ...(envelope.parentRunId ? { parentRunId: mapId(envelope.parentRunId, 'run') } : {}),
237
+ ...(envelope.firstIssueSpanId ? { firstIssueSpanId: mapId(envelope.firstIssueSpanId, 'span') } : {}),
238
+ });
239
+ const mappedTraceId = mapId(trace.envelope.traceId, 'trace');
240
+ const spans = trace.spans.map((span) => redactSpan(span, profile, salt, mappedTraceId));
241
+ const candidates = trace.candidateDecisions.map((candidate) => redactCandidate(candidate, profile, salt, mappedTraceId));
242
+ const links = trace.links.map((link) => redactLink(link, profile, salt, mappedTraceId));
243
+ return { envelope: redactEnvelope(trace.envelope), spans, candidateDecisions: candidates, links };
244
+ }
245
+ function redactSpan(span, profile, salt, traceId) {
246
+ const map = (value, kind = 'id') => profile === 'strict' ? pseudo(value, salt, kind) : value;
247
+ const payload = redactPayload(span.payload, profile, salt);
248
+ return {
249
+ ...span,
250
+ traceId,
251
+ spanId: map(span.spanId, 'span'),
252
+ ...(span.parentSpanId ? { parentSpanId: map(span.parentSpanId, 'span') } : {}),
253
+ payload,
254
+ };
255
+ }
256
+ function redactCandidate(candidate, profile, salt, traceId) {
257
+ if (profile === 'support')
258
+ return { ...candidate, traceId };
259
+ return {
260
+ ...candidate,
261
+ traceId,
262
+ candidateId: pseudo(candidate.candidateId, salt, 'candidate'),
263
+ };
264
+ }
265
+ function redactLink(link, profile, salt, _traceId) {
266
+ const map = (value, kind) => profile === 'strict' ? pseudo(value, salt, kind) : value;
267
+ return {
268
+ ...link,
269
+ // A fetched trace can be either end of a relationship. Map each original
270
+ // endpoint independently so strict bundles retain the actual edge direction
271
+ // instead of rewriting every source into the selected trace.
272
+ sourceTraceId: map(link.sourceTraceId, 'trace'),
273
+ sourceRunId: map(link.sourceRunId, 'run'),
274
+ ...(link.targetTraceId ? { targetTraceId: map(link.targetTraceId, 'trace') } : {}),
275
+ ...(link.targetRunId ? { targetRunId: map(link.targetRunId, 'run') } : {}),
276
+ };
277
+ }
278
+ function redactPayload(payload, profile, salt) {
279
+ const map = (value, kind = 'id') => profile === 'strict' ? pseudo(value, salt, kind) : value;
280
+ switch (payload.kind) {
281
+ case 'snapshot': return { ...payload, ...(payload.snapshotId ? { snapshotId: map(payload.snapshotId, 'snapshot') } : {}) };
282
+ case 'meaning': return { ...payload, selectedCandidateIds: payload.selectedCandidateIds.map((id) => map(id, 'candidate')), rejectedCandidateIds: payload.rejectedCandidateIds.map((id) => map(id, 'candidate')) };
283
+ case 'cascade': return {
284
+ ...payload,
285
+ decision: {
286
+ ...payload.decision,
287
+ ...(payload.decision.terminalGap ? {
288
+ terminalGap: {
289
+ code: payload.decision.terminalGap.code,
290
+ requirement: payload.decision.terminalGap.requirement,
291
+ witnessCandidateIds: payload.decision.terminalGap.witnessCandidateIds.map((id) => map(id, 'candidate')),
292
+ },
293
+ } : {}),
294
+ sourceCoverage: payload.decision.sourceCoverage.map((coverage) => ({ ...coverage, candidateIds: coverage.candidateIds.map((id) => map(id, 'candidate')) })),
295
+ attempts: payload.decision.attempts.map((attempt) => ({ ...attempt, candidateIds: attempt.candidateIds.map((id) => map(id, 'candidate')) })),
296
+ },
297
+ };
298
+ case 'tool': return { ...payload, call: { ...payload.call, toolCallId: map(payload.call.toolCallId, 'tool') } };
299
+ case 'research': return { ...payload, ...(payload.branchId ? { branchId: map(payload.branchId, 'branch') } : {}) };
300
+ default: return payload;
301
+ }
302
+ }
303
+ function redactRunReceipt(value, trace, profile, salt) {
304
+ const record = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
305
+ const source = record.diagnosticReceiptV3 && typeof record.diagnosticReceiptV3 === 'object'
306
+ ? record.diagnosticReceiptV3
307
+ : {};
308
+ const map = (value, kind = 'id') => typeof value === 'string'
309
+ ? profile === 'strict' ? pseudo(value, salt, kind) : value
310
+ : undefined;
311
+ const terminalGap = portableTerminalRelationshipGapV1(source.terminalGap
312
+ ?? (source.cascade && typeof source.cascade === 'object'
313
+ ? source.cascade.terminalGap
314
+ : undefined), profile, salt);
315
+ const runtimeReceiptV5 = portableRuntimeReceiptV5(record.diagnosticReceiptV5, profile, salt);
316
+ const runtimeReceiptV6 = portableRuntimeReceiptV6(record.diagnosticReceiptV6, profile, salt);
317
+ const runtimeReceiptV7 = portableRuntimeReceiptV7(record.diagnosticReceiptV7, profile, salt);
318
+ const runtimeReceiptV8 = portableRuntimeReceiptV8(record.diagnosticReceiptV8, profile, salt);
319
+ return {
320
+ version: 1,
321
+ traceReference: {
322
+ traceId: profile === 'strict' ? pseudo(trace.envelope.traceId, salt, 'trace') : trace.envelope.traceId,
323
+ recordingStatus: trace.envelope.recordingStatus,
324
+ traceFingerprint: trace.envelope.traceFingerprint,
325
+ },
326
+ route: typeof record.route === 'string' ? record.route : undefined,
327
+ status: typeof record.status === 'string' ? record.status : trace.envelope.status,
328
+ trustState: typeof record.trustState === 'string' ? record.trustState : trace.envelope.trustState,
329
+ selectedTier: trace.envelope.selectedTier,
330
+ planFrozen: source.planFrozen === true,
331
+ finalStopReason: typeof source.finalStopReason === 'string' ? source.finalStopReason : undefined,
332
+ ...(terminalGap ? { terminalGap } : {}),
333
+ ...(runtimeReceiptV5 ? { runtimeReceiptV5 } : {}),
334
+ ...(runtimeReceiptV6 ? { runtimeReceiptV6 } : {}),
335
+ ...(runtimeReceiptV7 ? { runtimeReceiptV7 } : {}),
336
+ ...(runtimeReceiptV8 ? { runtimeReceiptV8 } : {}),
337
+ telemetry: sanitizeTelemetry(record.telemetry),
338
+ fingerprints: {
339
+ plan: map(findFingerprint(record, 'planFingerprint'), 'plan'),
340
+ sql: map(findFingerprint(record, 'sqlFingerprint'), 'sql'),
341
+ result: map(findFingerprint(record, 'resultFingerprint'), 'result'),
342
+ },
343
+ };
344
+ }
345
+ /**
346
+ * V8 is the bounded Ask-tool-runtime story. Strict exports retain its
347
+ * allowlisted enums, counts, timings, and pseudonymous identifiers only. In
348
+ * particular, this deliberately does not make generated DQL/SQL exportable:
349
+ * the local trace contract never retains query text, result rows, prompts,
350
+ * provider responses, credentials, or chain-of-thought.
351
+ */
352
+ function portableRuntimeReceiptV8(value, profile, salt) {
353
+ if (!value || typeof value !== 'object' || Array.isArray(value))
354
+ return undefined;
355
+ const receipt = value;
356
+ if (receipt.version !== 8)
357
+ return undefined;
358
+ const record = (candidate) => candidate && typeof candidate === 'object' && !Array.isArray(candidate) ? candidate : undefined;
359
+ const count = (candidate, maximum = 10_000) => typeof candidate === 'number' && Number.isFinite(candidate)
360
+ ? Math.max(0, Math.min(maximum, Math.floor(candidate)))
361
+ : undefined;
362
+ const identifier = (candidate, kind) => typeof candidate === 'string' && candidate.length > 0 && candidate.length <= 256
363
+ ? (profile === 'strict' ? pseudo(candidate, salt, kind) : candidate)
364
+ : undefined;
365
+ const safeCode = (candidate) => typeof candidate === 'string' && /^[A-Z][A-Z0-9_:-]{1,95}$/.test(candidate)
366
+ ? candidate
367
+ : 'UNRECOGNIZED_REASON';
368
+ const modes = new Set(['legacy_v1', 'shadow_v2', 'authoritative_v2']);
369
+ const turns = new Set(['analytics', 'definition', 'business_context', 'prior_result', 'general', 'clarification_response', 'research']);
370
+ const sources = new Set(['certified', 'semantic', 'governed_relational', 'dbt_manifest', 'runtime_schema', 'vector', 'conversation', 'business']);
371
+ const sourceStates = new Set(['available', 'empty', 'stale', 'unavailable', 'errored', 'skipped']);
372
+ const tools = new Set(['inspect_ask_context', 'inspect_conversation_result', 'inspect_business_context', 'inspect_certified_candidates', 'run_certified', 'inspect_semantic_candidates', 'compile_and_run_semantic', 'inspect_relational_context', 'compile_and_run_dql', 'validate_and_run_sql', 'search_values', 'request_clarification', 'finish_answer']);
373
+ const outcomes = new Set(['eligible', 'executed', 'ineligible', 'unavailable', 'ambiguous', 'needs_input', 'denied', 'error']);
374
+ const tiers = new Set(['certified', 'semantic', 'governed_relational', 'exploratory_sql']);
375
+ const origins = new Set(['retrieval', 'agent_control', 'tool', 'validation', 'freeze', 'execution', 'provider', 'narration']);
376
+ const terminalKinds = new Set(['finish_answer', 'clarification', 'gap', 'provider_failure', 'execution_failure', 'denied', 'budget_exhausted']);
377
+ const semanticEngines = new Set(['native', 'metricflow-cli', 'dbt-cloud']);
378
+ const providerPhases = new Set(['preflight', 'classification', 'meaning_resolution', 'planning', 'generation', 'repair', 'narration', 'agent_control', 'tool_followup', 'unknown']);
379
+ const providerCauses = new Set(['authentication', 'model_not_found', 'rate_limited', 'gateway', 'network', 'provider_timeout', 'run_deadline', 'admission_denied', 'dispatch_budget', 'cancelled', 'unknown']);
380
+ const narrations = new Set(['fact_bound', 'deterministic_fallback', 'not_retained', 'not_applicable']);
381
+ if (typeof receipt.mode !== 'string' || !modes.has(receipt.mode)
382
+ || typeof receipt.turnClass !== 'string' || !turns.has(receipt.turnClass))
383
+ return undefined;
384
+ const contextCoverage = Array.isArray(receipt.contextCoverage)
385
+ ? receipt.contextCoverage.flatMap((entry) => {
386
+ const item = record(entry);
387
+ if (!item || typeof item.source !== 'string' || !sources.has(item.source)
388
+ || typeof item.status !== 'string' || !sourceStates.has(item.status))
389
+ return [];
390
+ return [{
391
+ source: item.source,
392
+ status: item.status,
393
+ ...(count(item.admittedCandidateCount) !== undefined ? { admittedCandidateCount: count(item.admittedCandidateCount) } : {}),
394
+ ...(count(item.excludedCandidateCount) !== undefined ? { excludedCandidateCount: count(item.excludedCandidateCount) } : {}),
395
+ reasonCodes: Array.isArray(item.reasonCodes) ? item.reasonCodes.slice(0, 8).map(safeCode) : [],
396
+ }];
397
+ })
398
+ : [];
399
+ const observations = Array.isArray(receipt.observations)
400
+ ? receipt.observations.slice(0, 24).flatMap((entry) => {
401
+ const item = record(entry);
402
+ if (!item || typeof item.tool !== 'string' || !tools.has(item.tool)
403
+ || typeof item.outcome !== 'string' || !outcomes.has(item.outcome))
404
+ return [];
405
+ const provider = record(item.provider);
406
+ const safeProvider = provider && typeof provider.phase === 'string' && providerPhases.has(provider.phase)
407
+ && typeof provider.cause === 'string' && providerCauses.has(provider.cause)
408
+ && typeof provider.retryable === 'boolean'
409
+ ? { phase: provider.phase, cause: provider.cause, retryable: provider.retryable, safeAction: safeCode(provider.safeAction) }
410
+ : undefined;
411
+ return [{
412
+ tool: item.tool,
413
+ outcome: item.outcome,
414
+ ...(typeof item.tier === 'string' && tiers.has(item.tier) ? { tier: item.tier } : {}),
415
+ reasonCode: safeCode(item.reasonCode),
416
+ candidateIds: Array.isArray(item.candidateIds)
417
+ ? item.candidateIds.slice(0, 48).flatMap((id) => identifier(id, 'candidate') ? [identifier(id, 'candidate')] : [])
418
+ : [],
419
+ ...(identifier(item.planId, 'plan') ? { planId: identifier(item.planId, 'plan') } : {}),
420
+ ...(typeof item.frozen === 'boolean' ? { frozen: item.frozen } : {}),
421
+ ...(typeof item.retryable === 'boolean' ? { retryable: item.retryable } : {}),
422
+ ...(typeof item.safeAction === 'string' ? { safeAction: safeCode(item.safeAction) } : {}),
423
+ ...(count(item.durationMs, 86_400_000) !== undefined ? { durationMs: count(item.durationMs, 86_400_000) } : {}),
424
+ ...(identifier(item.inputFingerprint, 'input') ? { inputFingerprint: identifier(item.inputFingerprint, 'input') } : {}),
425
+ ...(identifier(item.outputFingerprint, 'output') ? { outputFingerprint: identifier(item.outputFingerprint, 'output') } : {}),
426
+ ...(typeof item.origin === 'string' && origins.has(item.origin) ? { origin: item.origin } : {}),
427
+ ...(safeProvider ? { provider: safeProvider } : {}),
428
+ }];
429
+ })
430
+ : [];
431
+ const tierAttempts = Array.isArray(receipt.tierAttempts)
432
+ ? receipt.tierAttempts.slice(0, 8).flatMap((entry) => {
433
+ const item = record(entry);
434
+ if (!item || typeof item.tier !== 'string' || !tiers.has(item.tier)
435
+ || typeof item.outcome !== 'string' || !outcomes.has(item.outcome)
436
+ || typeof item.frozen !== 'boolean')
437
+ return [];
438
+ return [{
439
+ tier: item.tier,
440
+ outcome: item.outcome,
441
+ reasonCode: safeCode(item.reasonCode),
442
+ frozen: item.frozen,
443
+ candidateIds: Array.isArray(item.candidateIds)
444
+ ? item.candidateIds.slice(0, 48).flatMap((id) => identifier(id, 'candidate') ? [identifier(id, 'candidate')] : [])
445
+ : [],
446
+ ...(count(item.durationMs, 86_400_000) !== undefined ? { durationMs: count(item.durationMs, 86_400_000) } : {}),
447
+ }];
448
+ })
449
+ : [];
450
+ const terminal = record(receipt.terminalOutcome);
451
+ const safeTerminal = terminal && typeof terminal.kind === 'string' && terminalKinds.has(terminal.kind)
452
+ && typeof terminal.origin === 'string' && origins.has(terminal.origin)
453
+ ? {
454
+ kind: terminal.kind,
455
+ origin: terminal.origin,
456
+ reasonCode: safeCode(terminal.reasonCode),
457
+ ...(typeof terminal.safeAction === 'string' ? { safeAction: safeCode(terminal.safeAction) } : {}),
458
+ }
459
+ : undefined;
460
+ const outcome = record(receipt.outcome);
461
+ const safeOutcome = outcome && typeof outcome.connectionAttempted === 'boolean'
462
+ && typeof outcome.narration === 'string' && narrations.has(outcome.narration)
463
+ ? {
464
+ connectionAttempted: outcome.connectionAttempted,
465
+ executionAttempts: count(outcome.executionAttempts) ?? 0,
466
+ factCount: count(outcome.factCount) ?? 0,
467
+ narration: outcome.narration,
468
+ }
469
+ : { connectionAttempted: false, executionAttempts: 0, factCount: 0, narration: 'not_retained' };
470
+ const semanticRuntime = record(receipt.semanticRuntime);
471
+ const safeSemanticRuntime = semanticRuntime
472
+ && semanticRuntime.version === 1
473
+ && typeof semanticRuntime.preference === 'string'
474
+ && (semanticRuntime.preference === 'auto' || semanticEngines.has(semanticRuntime.preference))
475
+ && typeof semanticRuntime.readiness === 'string'
476
+ && (semanticRuntime.readiness === 'ready' || semanticRuntime.readiness === 'unavailable')
477
+ && (semanticRuntime.selectedEngine === undefined
478
+ || (typeof semanticRuntime.selectedEngine === 'string' && semanticEngines.has(semanticRuntime.selectedEngine)))
479
+ ? {
480
+ version: 1,
481
+ preference: semanticRuntime.preference,
482
+ ...(typeof semanticRuntime.selectedEngine === 'string' ? { selectedEngine: semanticRuntime.selectedEngine } : {}),
483
+ readiness: semanticRuntime.readiness,
484
+ }
485
+ : undefined;
486
+ return {
487
+ version: 8,
488
+ mode: receipt.mode,
489
+ turnClass: receipt.turnClass,
490
+ ...(identifier(receipt.snapshotId, 'snapshot') ? { snapshotId: identifier(receipt.snapshotId, 'snapshot') } : {}),
491
+ retainedCandidateCount: count(receipt.retainedCandidateCount) ?? 0,
492
+ initialCandidateCount: count(receipt.initialCandidateCount) ?? 0,
493
+ expansionCount: count(receipt.expansionCount) ?? 0,
494
+ objective: typeof receipt.objective === 'string' && turns.has(receipt.objective) ? receipt.objective : receipt.turnClass,
495
+ contextCoverage,
496
+ excludedCandidateCount: count(receipt.excludedCandidateCount) ?? 0,
497
+ exclusionReasonCodes: Array.isArray(receipt.exclusionReasonCodes) ? receipt.exclusionReasonCodes.slice(0, 8).map(safeCode) : [],
498
+ observations,
499
+ tierAttempts,
500
+ ...(typeof receipt.controllerTier === 'string' && tiers.has(receipt.controllerTier)
501
+ ? { controllerTier: receipt.controllerTier }
502
+ : {}),
503
+ ...(safeSemanticRuntime ? { semanticRuntime: safeSemanticRuntime } : {}),
504
+ planFrozen: receipt.planFrozen === true,
505
+ ...(safeTerminal ? { terminalOutcome: safeTerminal } : {}),
506
+ outcome: safeOutcome,
507
+ toolDurationMs: count(receipt.toolDurationMs, 86_400_000) ?? 0,
508
+ finalStopReason: safeCode(receipt.finalStopReason),
509
+ };
510
+ }
511
+ /**
512
+ * V6 adds the short reader-facing decision path to V5. Keep the portable
513
+ * shape deliberately narrower than the persisted receipt: a support bundle
514
+ * needs counts, enum outcomes, and pseudonymous identity fingerprints, never
515
+ * raw requirements, member values, candidate lifecycle prose, or answer text.
516
+ */
517
+ function portableRuntimeReceiptV6(value, profile, salt) {
518
+ if (!value || typeof value !== 'object' || Array.isArray(value))
519
+ return undefined;
520
+ const receipt = value;
521
+ if (receipt.version !== 6 || typeof receipt.runId !== 'string')
522
+ return undefined;
523
+ const record = (candidate) => candidate && typeof candidate === 'object' && !Array.isArray(candidate) ? candidate : undefined;
524
+ // The V5 projection is an explicit allowlist for all inherited fields. Do
525
+ // not copy V6 state/workspace wholesale merely because it extends V5.
526
+ const base = portableRuntimeReceiptV5({ ...receipt, version: 5 }, profile, salt);
527
+ if (!base)
528
+ return undefined;
529
+ const planning = record(receipt.planning);
530
+ const allowedModes = new Set(['exact_fast_path', 'initial_planner', 'targeted_revision', 'deterministic_binding']);
531
+ const allowedVerification = new Set(['valid', 'needs_targeted_context', 'ambiguous', 'denied', 'invalid']);
532
+ const allowedStages = new Set(['retrieval', 'role_coverage', 'planner', 'verification', 'targeted_recovery', 'cascade', 'freeze', 'connection', 'execution', 'facts']);
533
+ const allowedStatuses = new Set(['completed', 'skipped', 'blocked', 'unavailable']);
534
+ const allowedRoles = new Set(['metric', 'entity_key', 'entity_label', 'categorical_dimension', 'time_dimension', 'member', 'relationship', 'context']);
535
+ const allowedTiers = new Set(['certified', 'semantic', 'governed_relational', 'exploratory_sql', 'clarify_or_gap']);
536
+ const allowedTierOutcomes = new Set(['executable', 'ineligible', 'unavailable', 'ambiguous', 'denied']);
537
+ const allowedBoundaries = new Set(['plan.compile', 'semantic.compile', 'sql.authorize', 'sql.execute', 'provider', 'cascade', 'run']);
538
+ const allowedOrigins = new Set(['internal_invariant', 'governance_gate', 'semantic_compiler', 'plan_compiler', 'provider', 'warehouse', 'unknown']);
539
+ const allowedImpacts = new Set(['execution_not_attempted', 'execution_failed', 'answer_not_produced', 'run_cancelled']);
540
+ const allowedActions = new Set(['export_redacted_trace', 'configure_connection', 'change_authorized_connection', 'inspect_failure', 'retry_same_plan', 'refresh_snapshot', 'edit_dql', 'open_sql_notebook', 'request_access', 'reapply_semantic_runtime', 'review_analytical_failure', 'inspect_research_failures', 'none']);
541
+ const finiteCount = (candidate) => typeof candidate === 'number' && Number.isFinite(candidate)
542
+ ? Math.max(0, Math.min(10_000, Math.floor(candidate)))
543
+ : undefined;
544
+ const pseudonym = (candidate, kind) => typeof candidate === 'string'
545
+ ? (profile === 'strict' ? pseudo(candidate, salt, kind) : candidate)
546
+ : undefined;
547
+ const verificationRecord = planning ? record(planning.verification) : undefined;
548
+ const safePlanning = planning
549
+ && planning.version === 1
550
+ && typeof planning.mode === 'string'
551
+ && allowedModes.has(planning.mode)
552
+ && verificationRecord
553
+ && verificationRecord.version === 1
554
+ && typeof verificationRecord.status === 'string'
555
+ && allowedVerification.has(verificationRecord.status)
556
+ ? {
557
+ version: 1,
558
+ mode: planning.mode,
559
+ ...(finiteCount(planning.plannerCalls) !== undefined ? { plannerCalls: finiteCount(planning.plannerCalls) } : {}),
560
+ ...(finiteCount(planning.revisionCalls) !== undefined ? { revisionCalls: finiteCount(planning.revisionCalls) } : {}),
561
+ verification: {
562
+ version: 1,
563
+ status: verificationRecord.status,
564
+ missingRoleCount: Array.isArray(verificationRecord.missingRoles)
565
+ ? verificationRecord.missingRoles.filter((role) => typeof role === 'string').slice(0, 8).length
566
+ : 0,
567
+ candidateIds: Array.isArray(verificationRecord.candidateIds)
568
+ ? verificationRecord.candidateIds
569
+ .filter((id) => typeof id === 'string')
570
+ .slice(0, 32)
571
+ .map((id) => pseudonym(id, 'candidate'))
572
+ : [],
573
+ reasonCode: typeof verificationRecord.reasonCode === 'string'
574
+ && /^[a-z0-9_:-]{1,96}$/.test(verificationRecord.reasonCode)
575
+ ? verificationRecord.reasonCode
576
+ : 'unrecognized_reason',
577
+ },
578
+ }
579
+ : undefined;
580
+ const story = Array.isArray(receipt.story)
581
+ ? receipt.story.flatMap((step) => {
582
+ const item = record(step);
583
+ if (!item || typeof item.stage !== 'string' || !allowedStages.has(item.stage)
584
+ || typeof item.status !== 'string' || !allowedStatuses.has(item.status))
585
+ return [];
586
+ return [{
587
+ stage: item.stage,
588
+ status: item.status,
589
+ reasonCode: typeof item.reasonCode === 'string' && /^[a-z0-9_:-]{1,96}$/.test(item.reasonCode)
590
+ ? item.reasonCode
591
+ : 'unrecognized_reason',
592
+ }];
593
+ })
594
+ : [];
595
+ const roleCoverage = Array.isArray(receipt.roleCoverage)
596
+ ? receipt.roleCoverage.flatMap((entry) => {
597
+ const item = record(entry);
598
+ if (!item || typeof item.role !== 'string' || !allowedRoles.has(item.role))
599
+ return [];
600
+ const candidateCount = finiteCount(item.candidateCount);
601
+ return candidateCount === undefined ? [] : [{
602
+ role: item.role,
603
+ candidateCount,
604
+ ...(item.state === 'alternatives' || item.state === 'proven' ? { state: item.state } : {}),
605
+ }];
606
+ })
607
+ : [];
608
+ const cascade = record(receipt.cascade);
609
+ const safeCascade = cascade && typeof cascade.planFrozen === 'boolean'
610
+ ? {
611
+ attempts: Array.isArray(cascade.attempts)
612
+ ? cascade.attempts.flatMap((entry) => {
613
+ const item = record(entry);
614
+ if (!item || typeof item.tier !== 'string' || !allowedTiers.has(item.tier)
615
+ || typeof item.outcome !== 'string' || !allowedTierOutcomes.has(item.outcome)
616
+ || typeof item.planFrozen !== 'boolean')
617
+ return [];
618
+ return [{ tier: item.tier, outcome: item.outcome, planFrozen: item.planFrozen }];
619
+ })
620
+ : [],
621
+ ...(typeof cascade.selectedTier === 'string' && allowedTiers.has(cascade.selectedTier) ? { selectedTier: cascade.selectedTier } : {}),
622
+ ...(typeof cascade.stopReason === 'string' && /^[a-z0-9_:-]{1,96}$/.test(cascade.stopReason) ? { stopReason: cascade.stopReason } : {}),
623
+ planFrozen: cascade.planFrozen,
624
+ }
625
+ : undefined;
626
+ const origin = record(receipt.origin);
627
+ const safeOrigin = origin
628
+ && typeof origin.boundary === 'string' && allowedBoundaries.has(origin.boundary)
629
+ && typeof origin.origin === 'string' && allowedOrigins.has(origin.origin)
630
+ && typeof origin.impact === 'string' && allowedImpacts.has(origin.impact)
631
+ ? { boundary: origin.boundary, origin: origin.origin, impact: origin.impact }
632
+ : undefined;
633
+ const connection = record(receipt.connection);
634
+ const execution = record(receipt.execution);
635
+ const facts = record(receipt.facts);
636
+ const safeConnection = connection && typeof connection.attempted === 'boolean'
637
+ ? { attempted: connection.attempted }
638
+ : undefined;
639
+ const executionAttempts = execution ? finiteCount(execution.attempts) : undefined;
640
+ const safeFacts = facts ? {
641
+ ...(finiteCount(facts.factCount) !== undefined ? { factCount: finiteCount(facts.factCount) } : {}),
642
+ ...(typeof facts.resultFingerprint === 'string' ? { resultFingerprint: pseudonym(facts.resultFingerprint, 'result') } : {}),
643
+ } : undefined;
644
+ const safeTelemetry = sanitizeTelemetry(receipt.telemetry);
645
+ return {
646
+ ...base,
647
+ version: 6,
648
+ ...(safePlanning ? { planning: safePlanning } : {}),
649
+ roleCoverage,
650
+ ...(safeCascade ? { cascade: safeCascade } : {}),
651
+ ...(safeOrigin ? { origin: safeOrigin } : {}),
652
+ ...(safeConnection ? { connection: safeConnection } : {}),
653
+ ...(executionAttempts !== undefined ? { execution: { attempts: executionAttempts } } : {}),
654
+ ...(safeTelemetry ? { telemetry: safeTelemetry } : {}),
655
+ ...(safeFacts ? { facts: safeFacts } : {}),
656
+ ...(typeof receipt.safeNextAction === 'string' && allowedActions.has(receipt.safeNextAction) ? { safeNextAction: receipt.safeNextAction } : {}),
657
+ story,
658
+ };
659
+ }
660
+ /**
661
+ * V7 is a count/enum-only first-read projection. Reuse the explicit V6
662
+ * allowlist for inherited fields, then independently allowlist the small
663
+ * inspector rather than copying its object wholesale into a support bundle.
664
+ */
665
+ function portableRuntimeReceiptV7(value, profile, salt) {
666
+ if (!value || typeof value !== 'object' || Array.isArray(value))
667
+ return undefined;
668
+ const receipt = value;
669
+ if (receipt.version !== 7 || typeof receipt.runId !== 'string')
670
+ return undefined;
671
+ const base = portableRuntimeReceiptV6({ ...receipt, version: 6 }, profile, salt);
672
+ if (!base)
673
+ return undefined;
674
+ const inspector = receipt.inspector && typeof receipt.inspector === 'object' && !Array.isArray(receipt.inspector)
675
+ ? receipt.inspector
676
+ : undefined;
677
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
678
+ const count = (value) => typeof value === 'number' && Number.isFinite(value)
679
+ ? Math.max(0, Math.min(10_000, Math.floor(value)))
680
+ : undefined;
681
+ const understood = record(inspector?.understood);
682
+ const evidence = record(inspector?.evidence);
683
+ const planning = record(inspector?.planning);
684
+ const route = record(inspector?.route);
685
+ const outcome = record(inspector?.outcome);
686
+ const allowedKinds = new Set(['conversation', 'definition', 'lookup', 'ranking', 'breakdown', 'comparison', 'drilldown', 'diagnosis', 'research', 'compound', 'clarification', 'aggregation']);
687
+ const allowedBindings = new Set(['none', 'structured_clarification', 'prior_result', 'task_dependency']);
688
+ const allowedModes = new Set(['exact_fast_path', 'initial_planner', 'targeted_revision', 'deterministic_binding']);
689
+ const allowedVerification = new Set(['valid', 'needs_targeted_context', 'ambiguous', 'denied', 'invalid']);
690
+ const allowedTiers = new Set(['certified', 'semantic', 'governed_relational', 'exploratory_sql', 'clarify_or_gap']);
691
+ const allowedNarration = new Set(['fact_bound', 'result_without_facts', 'not_applicable']);
692
+ const safeInspector = understood && evidence && planning && route && outcome
693
+ && typeof understood.questionKind === 'string' && allowedKinds.has(understood.questionKind)
694
+ && typeof understood.conversationBinding === 'string' && allowedBindings.has(understood.conversationBinding)
695
+ && typeof understood.entityRequested === 'boolean' && typeof understood.hasBoundFilter === 'boolean'
696
+ && typeof evidence.recoveryAttempted === 'boolean'
697
+ && typeof planning.mode === 'string' && allowedModes.has(planning.mode)
698
+ && typeof planning.verification === 'string' && allowedVerification.has(planning.verification)
699
+ && typeof route.planFrozen === 'boolean' && typeof route.reviewRequired === 'boolean'
700
+ && typeof outcome.connectionAttempted === 'boolean'
701
+ && typeof outcome.narration === 'string' && allowedNarration.has(outcome.narration)
702
+ ? {
703
+ understood: {
704
+ questionKind: understood.questionKind,
705
+ conversationBinding: understood.conversationBinding,
706
+ ...(count(understood.measureCount) !== undefined ? { measureCount: count(understood.measureCount) } : {}),
707
+ ...(count(understood.dimensionCount) !== undefined ? { dimensionCount: count(understood.dimensionCount) } : {}),
708
+ entityRequested: understood.entityRequested,
709
+ hasBoundFilter: understood.hasBoundFilter,
710
+ },
711
+ evidence: {
712
+ ...(count(evidence.admittedCandidateCount) !== undefined ? { admittedCandidateCount: count(evidence.admittedCandidateCount) } : {}),
713
+ ...(count(evidence.roleCount) !== undefined ? { roleCount: count(evidence.roleCount) } : {}),
714
+ recoveryAttempted: evidence.recoveryAttempted,
715
+ },
716
+ planning: {
717
+ mode: planning.mode,
718
+ ...(count(planning.plannerCalls) !== undefined ? { plannerCalls: count(planning.plannerCalls) } : {}),
719
+ verification: planning.verification,
720
+ },
721
+ route: {
722
+ ...(typeof route.selectedTier === 'string' && allowedTiers.has(route.selectedTier) ? { selectedTier: route.selectedTier } : {}),
723
+ ...(count(route.tierAttemptCount) !== undefined ? { tierAttemptCount: count(route.tierAttemptCount) } : {}),
724
+ planFrozen: route.planFrozen,
725
+ reviewRequired: route.reviewRequired,
726
+ },
727
+ outcome: {
728
+ connectionAttempted: outcome.connectionAttempted,
729
+ ...(count(outcome.executionAttempts) !== undefined ? { executionAttempts: count(outcome.executionAttempts) } : {}),
730
+ ...(count(outcome.factCount) !== undefined ? { factCount: count(outcome.factCount) } : {}),
731
+ narration: outcome.narration,
732
+ },
733
+ }
734
+ : undefined;
735
+ return {
736
+ ...base,
737
+ version: 7,
738
+ ...(safeInspector ? { inspector: safeInspector } : {}),
739
+ };
740
+ }
741
+ /**
742
+ * Keep portable V5 parity with the full local trace without exporting raw Ask
743
+ * state. The V5 receipt is already content-free; strict export additionally
744
+ * pseudonymizes fact/result/question identities while retaining its summary
745
+ * fingerprint so a support bundle can be matched to the on-device trace.
746
+ */
747
+ function portableRuntimeReceiptV5(value, profile, salt) {
748
+ if (!value || typeof value !== 'object' || Array.isArray(value))
749
+ return undefined;
750
+ const receipt = value;
751
+ if (receipt.version !== 5 || typeof receipt.runId !== 'string')
752
+ return undefined;
753
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
754
+ const summary = record(receipt.summary);
755
+ const state = record(receipt.state);
756
+ const businessAnswer = record(receipt.businessAnswer);
757
+ const provider = record(receipt.provider);
758
+ const pseudonym = (value, kind) => typeof value === 'string'
759
+ ? (profile === 'strict' ? pseudo(value, salt, kind) : value)
760
+ : undefined;
761
+ const safeSummary = summary ? {
762
+ version: summary.version,
763
+ summaryFingerprint: summary.summaryFingerprint,
764
+ runtimeMode: summary.runtimeMode,
765
+ selectedCompiler: summary.selectedCompiler,
766
+ programTaskCount: summary.programTaskCount,
767
+ admittedCandidateCount: summary.admittedCandidateCount,
768
+ toolCallCount: summary.toolCallCount,
769
+ executionAttempts: summary.executionAttempts,
770
+ } : undefined;
771
+ const safeState = state ? {
772
+ version: state.version,
773
+ mode: state.mode,
774
+ phase: state.phase,
775
+ questionFingerprint: pseudonym(state.questionFingerprint, 'question'),
776
+ kind: state.kind,
777
+ requirementCounts: state.requirementCounts,
778
+ mission: state.mission,
779
+ workspace: portableRuntimeWorkspaceV5(state.workspace, profile, salt),
780
+ program: (() => {
781
+ const program = record(state.program);
782
+ return program ? {
783
+ id: pseudonym(program.id, 'program'),
784
+ taskCount: program.taskCount,
785
+ candidateCount: program.candidateCount,
786
+ requiredRoles: program.requiredRoles,
787
+ outputAssertionCount: program.outputAssertionCount,
788
+ } : undefined;
789
+ })(),
790
+ resolvedPlan: (() => {
791
+ const plan = record(state.resolvedPlan);
792
+ return plan ? {
793
+ version: plan.version,
794
+ programId: pseudonym(plan.programId, 'program'),
795
+ compiler: plan.compiler,
796
+ selectedTier: plan.selectedTier,
797
+ planFrozen: plan.planFrozen,
798
+ reviewRequired: plan.reviewRequired,
799
+ planFingerprint: pseudonym(plan.planFingerprint, 'plan'),
800
+ } : undefined;
801
+ })(),
802
+ counters: state.counters,
803
+ } : undefined;
804
+ const safeBusinessAnswer = businessAnswer ? {
805
+ version: businessAnswer.version,
806
+ mode: businessAnswer.mode,
807
+ trustState: businessAnswer.trustState,
808
+ factIds: Array.isArray(businessAnswer.factIds)
809
+ ? businessAnswer.factIds.filter((id) => typeof id === 'string').map((id) => pseudonym(id, 'fact'))
810
+ : [],
811
+ resultFingerprint: pseudonym(businessAnswer.resultFingerprint, 'result'),
812
+ limitationCount: businessAnswer.limitationCount,
813
+ } : undefined;
814
+ const safeProvider = provider ? {
815
+ version: provider.version,
816
+ cause: provider.cause,
817
+ phase: provider.phase,
818
+ retryable: provider.retryable,
819
+ safeAction: provider.safeAction,
820
+ httpStatusClass: provider.httpStatusClass,
821
+ providerFingerprint: pseudonym(provider.providerFingerprint, 'provider'),
822
+ modelFingerprint: pseudonym(provider.modelFingerprint, 'model'),
823
+ baseOriginFingerprint: pseudonym(provider.baseOriginFingerprint, 'origin'),
824
+ } : undefined;
825
+ return {
826
+ version: 5,
827
+ runId: pseudonym(receipt.runId, 'run'),
828
+ ...(safeState ? { state: safeState } : {}),
829
+ ...(safeSummary ? { summary: safeSummary } : {}),
830
+ ...(safeBusinessAnswer ? { businessAnswer: safeBusinessAnswer } : {}),
831
+ ...(safeProvider ? { provider: safeProvider } : {}),
832
+ finalStopReason: receipt.finalStopReason,
833
+ };
834
+ }
835
+ function portableRuntimeWorkspaceV5(value, profile, salt) {
836
+ const workspace = value && typeof value === 'object' && !Array.isArray(value)
837
+ ? value
838
+ : undefined;
839
+ if (!workspace)
840
+ return undefined;
841
+ const pseudoIdentifier = (value, kind) => typeof value === 'string'
842
+ ? (profile === 'strict' ? pseudo(value, salt, kind) : value)
843
+ : undefined;
844
+ const sourceValues = new Set([
845
+ 'certified', 'semantic', 'governed_relational', 'exploratory', 'dbt_manifest', 'runtime_schema', 'vector', 'conversation', 'business_context',
846
+ ]);
847
+ const statuses = new Set(['available', 'empty', 'stale', 'unavailable', 'errored', 'skipped']);
848
+ const toolKinds = new Set(['retrieve_snapshot', 'candidate_extension', 'compiler_broker', 'provider_meaning', 'execute', 'repair']);
849
+ const toolStatuses = new Set(['completed', 'skipped', 'failed']);
850
+ const reasonCodes = new Set([
851
+ 'snapshot_acquired', 'broker_retrieval_required', 'snapshot_retrieval_failed', 'shadow_preserves_legacy_decision',
852
+ 'meaning_resolution_completed', 'canonical_or_structured_binding', 'meaning_resolution_failed', 'meaning_resolution_unavailable',
853
+ 'execution_completed', 'execution_failed', 'repair_completed', 'repair_exhausted', 'no_compiler_selected',
854
+ ]);
855
+ const finiteCount = (value) => typeof value === 'number' && Number.isFinite(value)
856
+ ? Math.max(0, Math.min(10_000, Math.floor(value)))
857
+ : undefined;
858
+ const sourceCoverage = Array.isArray(workspace.sourceCoverage)
859
+ ? workspace.sourceCoverage.flatMap((coverage) => {
860
+ if (!coverage || typeof coverage !== 'object' || Array.isArray(coverage))
861
+ return [];
862
+ const record = coverage;
863
+ if (typeof record.source !== 'string' || !sourceValues.has(record.source)
864
+ || typeof record.status !== 'string' || !statuses.has(record.status))
865
+ return [];
866
+ const candidateCount = finiteCount(record.candidateCount);
867
+ return [{ source: record.source, status: record.status, ...(candidateCount !== undefined ? { candidateCount } : {}) }];
868
+ })
869
+ : [];
870
+ const tools = Array.isArray(workspace.tools)
871
+ ? workspace.tools.flatMap((tool) => {
872
+ if (!tool || typeof tool !== 'object' || Array.isArray(tool))
873
+ return [];
874
+ const record = tool;
875
+ if (typeof record.id !== 'string' || typeof record.kind !== 'string' || !toolKinds.has(record.kind)
876
+ || typeof record.status !== 'string' || !toolStatuses.has(record.status))
877
+ return [];
878
+ return [{
879
+ id: pseudoIdentifier(record.id, 'tool'),
880
+ kind: record.kind,
881
+ status: record.status,
882
+ reasonCode: typeof record.reasonCode === 'string' && reasonCodes.has(record.reasonCode)
883
+ ? record.reasonCode
884
+ : 'unrecognized_reason',
885
+ }];
886
+ })
887
+ : [];
888
+ const admittedCandidateCount = finiteCount(workspace.admittedCandidateCount);
889
+ const excludedCandidateCount = finiteCount(workspace.excludedCandidateCount);
890
+ return {
891
+ ...(typeof workspace.snapshotId === 'string' ? { snapshotId: pseudoIdentifier(workspace.snapshotId, 'snapshot') } : {}),
892
+ ...(typeof workspace.sourceFingerprint === 'string' ? { sourceFingerprint: pseudoIdentifier(workspace.sourceFingerprint, 'source') } : {}),
893
+ ...(admittedCandidateCount !== undefined ? { admittedCandidateCount } : {}),
894
+ ...(excludedCandidateCount !== undefined ? { excludedCandidateCount } : {}),
895
+ sourceCoverage,
896
+ tools,
897
+ };
898
+ }
899
+ /**
900
+ * Receipt export is intentionally stricter than its source type: only the
901
+ * one enumerated relationship proof requirement is portable. This prevents a
902
+ * future V3 field or a malformed persisted record from putting router prose,
903
+ * question terms, or inferred SQL relationships into a support bundle.
904
+ */
905
+ function portableTerminalRelationshipGapV1(value, profile, salt) {
906
+ if (!value || typeof value !== 'object' || Array.isArray(value))
907
+ return undefined;
908
+ const record = value;
909
+ if (record.code !== 'MISSING_RELATIONSHIP' || record.requirement !== 'certified_relationship_or_allocation_proof')
910
+ return undefined;
911
+ const witnessCandidateIds = Array.isArray(record.witnessCandidateIds)
912
+ ? [...new Set(record.witnessCandidateIds.filter((id) => typeof id === 'string' && id.length > 0))]
913
+ .sort()
914
+ .slice(0, 32)
915
+ .map((id) => profile === 'strict' ? pseudo(id, salt, 'candidate') : id)
916
+ : [];
917
+ return {
918
+ code: 'MISSING_RELATIONSHIP',
919
+ requirement: 'certified_relationship_or_allocation_proof',
920
+ witnessCandidateIds,
921
+ };
922
+ }
923
+ function sanitizeTelemetry(value) {
924
+ if (!value || typeof value !== 'object' || Array.isArray(value))
925
+ return undefined;
926
+ const record = value;
927
+ const result = {};
928
+ for (const key of ['providerRoundTrips', 'toolCalls', 'sqlExecutions', 'repairs', 'egressReceipts']) {
929
+ if (typeof record[key] === 'number' && Number.isFinite(record[key]))
930
+ result[key] = record[key];
931
+ }
932
+ return Object.keys(result).length ? result : undefined;
933
+ }
934
+ function findFingerprint(record, key) {
935
+ if (typeof record[key] === 'string')
936
+ return record[key];
937
+ const nested = record.diagnosticReceiptV3;
938
+ if (nested && typeof nested === 'object' && typeof nested[key] === 'string')
939
+ return nested[key];
940
+ return undefined;
941
+ }
942
+ function scanForProhibited(values) {
943
+ const source = values.map((value) => canonicalJson(value)).join('\n');
944
+ const findings = PROHIBITED.flatMap((pattern) => pattern.test(source) ? [pattern.source] : []);
945
+ return { passed: findings.length === 0, findings };
946
+ }
947
+ function validateTraceReceiptV1(trace) {
948
+ const errors = [];
949
+ // Strict portable bundles use deterministic export-scoped pseudonyms. Live
950
+ // receipts retain W3C identifiers. Both shapes are structurally valid.
951
+ if (!(/^[0-9a-f]{32}$/.test(trace.envelope.traceId) || /^trace_[0-9a-f]{16}$/.test(trace.envelope.traceId)))
952
+ errors.push('Invalid trace ID.');
953
+ if (!(/^[0-9a-f]{16}$/.test(trace.envelope.rootSpanId) || /^span_[0-9a-f]{16}$/.test(trace.envelope.rootSpanId)))
954
+ errors.push('Invalid root span ID.');
955
+ const ids = new Set();
956
+ const ordinals = new Set();
957
+ for (const span of trace.spans) {
958
+ if (span.traceId !== trace.envelope.traceId)
959
+ errors.push(`Span ${span.spanId} has a mismatched trace ID.`);
960
+ if (ids.has(span.spanId))
961
+ errors.push(`Duplicate span ${span.spanId}.`);
962
+ ids.add(span.spanId);
963
+ if (ordinals.has(span.ordinal))
964
+ errors.push(`Duplicate span ordinal ${span.ordinal}.`);
965
+ ordinals.add(span.ordinal);
966
+ if (span.parentSpanId && !ids.has(span.parentSpanId) && !trace.spans.some((candidate) => candidate.spanId === span.parentSpanId))
967
+ errors.push(`Unknown parent span ${span.parentSpanId}.`);
968
+ }
969
+ if (!ids.has(trace.envelope.rootSpanId))
970
+ errors.push('Root span is missing from trace detail.');
971
+ if (trace.envelope.firstIssueSpanId && !ids.has(trace.envelope.firstIssueSpanId)) {
972
+ errors.push(`Unknown first issue span ${trace.envelope.firstIssueSpanId}.`);
973
+ }
974
+ for (const candidate of trace.candidateDecisions) {
975
+ if (candidate.traceId !== trace.envelope.traceId)
976
+ errors.push(`Candidate ${candidate.sequence} has a mismatched trace ID.`);
977
+ }
978
+ for (const link of trace.links) {
979
+ const sourceIsCurrent = link.sourceTraceId === trace.envelope.traceId;
980
+ const targetIsCurrent = link.targetTraceId === trace.envelope.traceId;
981
+ if (!sourceIsCurrent && !targetIsCurrent) {
982
+ errors.push(`Trace link ${link.kind} does not reference the exported trace.`);
983
+ continue;
984
+ }
985
+ if (sourceIsCurrent && link.sourceRunId !== trace.envelope.runId) {
986
+ errors.push(`Trace link ${link.kind} has a broken source run reference.`);
987
+ }
988
+ if (targetIsCurrent && link.targetRunId !== trace.envelope.runId) {
989
+ errors.push(`Trace link ${link.kind} has a broken target run reference.`);
990
+ }
991
+ }
992
+ const cascade = trace.spans.flatMap((span) => span.payload.kind === 'cascade' ? [{ span, decision: span.payload.decision }] : []);
993
+ for (const { span, decision } of cascade) {
994
+ const expected = ['certified', 'semantic', 'governed_relational', 'exploratory_sql', 'clarify_or_gap'];
995
+ let last = -1;
996
+ let frozen = false;
997
+ for (const attempt of decision.attempts) {
998
+ const position = expected.indexOf(attempt.tier);
999
+ if (position < last)
1000
+ errors.push('Cascade attempts are not authoritative order.');
1001
+ last = position;
1002
+ if (frozen)
1003
+ errors.push('Cascade continued after plan freeze.');
1004
+ frozen ||= attempt.planFrozen;
1005
+ }
1006
+ // Exploratory selection is allowed to appear before the physical plan has
1007
+ // frozen. The initial router receipt is authoritative evidence of that
1008
+ // pre-freeze selection; a later plan.freeze span proves the only valid
1009
+ // transition. Do not rewrite the original receipt merely to make a
1010
+ // portable bundle validate, and do not accept an unrelated later freeze.
1011
+ if (span.name === 'cascade.evaluate'
1012
+ && decision.selectedTier === 'exploratory_sql'
1013
+ && decision.planFrozen === false
1014
+ && !hasLaterMatchingExploratoryFreeze(trace, span)) {
1015
+ errors.push('Exploratory selection is not frozen.');
1016
+ }
1017
+ if (decision.terminalGap) {
1018
+ if (decision.terminalGap.code !== 'MISSING_RELATIONSHIP'
1019
+ || decision.terminalGap.requirement !== 'certified_relationship_or_allocation_proof') {
1020
+ errors.push('Unsupported terminal relationship gap receipt.');
1021
+ }
1022
+ if (!Array.isArray(decision.terminalGap.witnessCandidateIds)
1023
+ || decision.terminalGap.witnessCandidateIds.some((id) => typeof id !== 'string' || id.length === 0)) {
1024
+ errors.push('Invalid terminal relationship gap witnesses.');
1025
+ }
1026
+ }
1027
+ }
1028
+ return errors;
1029
+ }
1030
+ function hasLaterMatchingExploratoryFreeze(trace, selection) {
1031
+ if (selection.payload.kind !== 'cascade')
1032
+ return false;
1033
+ const selectedAttempt = selection.payload.decision.attempts.find((attempt) => attempt.tier === 'exploratory_sql');
1034
+ if (!selectedAttempt)
1035
+ return false;
1036
+ return trace.spans.some((span) => {
1037
+ if (span.ordinal <= selection.ordinal
1038
+ || span.name !== 'plan.freeze'
1039
+ || span.outcome !== 'ok'
1040
+ || span.reasonCode !== 'plan_frozen'
1041
+ || span.payload.kind !== 'cascade')
1042
+ return false;
1043
+ const frozen = span.payload.decision;
1044
+ const frozenAttempt = frozen.attempts.find((attempt) => attempt.tier === 'exploratory_sql');
1045
+ return frozen.selectedTier === 'exploratory_sql'
1046
+ && frozen.planFrozen === true
1047
+ && frozenAttempt?.planFrozen === true
1048
+ && sameCandidateIds(selectedAttempt.candidateIds, frozenAttempt.candidateIds);
1049
+ });
1050
+ }
1051
+ function sameCandidateIds(left, right) {
1052
+ const a = [...new Set(left)].sort();
1053
+ const b = [...new Set(right)].sort();
1054
+ return a.length === b.length && a.every((id, index) => id === b[index]);
1055
+ }
1056
+ function validateRunReceiptTerminalGapV1(receipt, trace) {
1057
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt))
1058
+ return [];
1059
+ const record = receipt;
1060
+ const gap = record.terminalGap;
1061
+ if (gap === undefined)
1062
+ return [];
1063
+ const portable = portableTerminalRelationshipGapV1(gap, 'support', 'validation');
1064
+ if (!portable)
1065
+ return ['Invalid run receipt terminal relationship gap.'];
1066
+ if (!trace)
1067
+ return ['Run receipt terminal relationship gap has no trace.'];
1068
+ const traceGap = trace.spans
1069
+ .flatMap((span) => span.payload.kind === 'cascade' ? [span.payload.decision.terminalGap] : [])
1070
+ .find((candidate) => candidate?.code === portable.code && candidate.requirement === portable.requirement);
1071
+ if (!traceGap)
1072
+ return ['Run receipt terminal relationship gap is not present in the trace.'];
1073
+ const expected = [...new Set(traceGap.witnessCandidateIds)].sort();
1074
+ const actual = [...new Set(portable.witnessCandidateIds)].sort();
1075
+ if (expected.length !== actual.length || expected.some((id, index) => id !== actual[index])) {
1076
+ return ['Run receipt terminal relationship gap witnesses do not match the trace.'];
1077
+ }
1078
+ return [];
1079
+ }
1080
+ function parseJsonFile(path, errors) {
1081
+ try {
1082
+ return JSON.parse(readFileSync(path, 'utf8'));
1083
+ }
1084
+ catch {
1085
+ errors.push(`Invalid JSON in ${basename(path)}.`);
1086
+ return undefined;
1087
+ }
1088
+ }
1089
+ //# sourceMappingURL=portable.js.map