@duckcodeailabs/dql-agent 1.14.2 → 1.14.3-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/agent-run-engine.d.ts +63 -2
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +864 -29
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-planner.d.ts +6 -0
  6. package/dist/agent-run-planner.d.ts.map +1 -1
  7. package/dist/agent-run-planner.js +2 -0
  8. package/dist/agent-run-planner.js.map +1 -1
  9. package/dist/agent-run-store.d.ts.map +1 -1
  10. package/dist/agent-run-store.js +48 -3
  11. package/dist/agent-run-store.js.map +1 -1
  12. package/dist/agentic/analyst-loop.d.ts +7 -1
  13. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  14. package/dist/agentic/analyst-loop.js +163 -0
  15. package/dist/agentic/analyst-loop.js.map +1 -1
  16. package/dist/agentic/assumptions.d.ts +1 -1
  17. package/dist/agentic/assumptions.d.ts.map +1 -1
  18. package/dist/agentic/assumptions.js.map +1 -1
  19. package/dist/agentic/research-agent.d.ts.map +1 -1
  20. package/dist/agentic/research-agent.js +10 -1
  21. package/dist/agentic/research-agent.js.map +1 -1
  22. package/dist/agentic/sql-authorization.d.ts +8 -0
  23. package/dist/agentic/sql-authorization.d.ts.map +1 -1
  24. package/dist/agentic/sql-authorization.js +3 -0
  25. package/dist/agentic/sql-authorization.js.map +1 -1
  26. package/dist/analytical-compatibility.d.ts.map +1 -1
  27. package/dist/analytical-compatibility.js +8 -1
  28. package/dist/analytical-compatibility.js.map +1 -1
  29. package/dist/analytical-failure-repair.d.ts.map +1 -1
  30. package/dist/analytical-failure-repair.js +6 -2
  31. package/dist/analytical-failure-repair.js.map +1 -1
  32. package/dist/analytical-frame.d.ts +23 -1
  33. package/dist/analytical-frame.d.ts.map +1 -1
  34. package/dist/analytical-frame.js +322 -16
  35. package/dist/analytical-frame.js.map +1 -1
  36. package/dist/analytical-orchestration.d.ts +314 -3
  37. package/dist/analytical-orchestration.d.ts.map +1 -1
  38. package/dist/analytical-orchestration.js +486 -43
  39. package/dist/analytical-orchestration.js.map +1 -1
  40. package/dist/answer-loop.d.ts +64 -8
  41. package/dist/answer-loop.d.ts.map +1 -1
  42. package/dist/answer-loop.js +528 -67
  43. package/dist/answer-loop.js.map +1 -1
  44. package/dist/ask-observability/index.d.ts +11 -0
  45. package/dist/ask-observability/index.d.ts.map +1 -0
  46. package/dist/ask-observability/index.js +7 -0
  47. package/dist/ask-observability/index.js.map +1 -0
  48. package/dist/ask-observability/instrumentation.d.ts +35 -0
  49. package/dist/ask-observability/instrumentation.d.ts.map +1 -0
  50. package/dist/ask-observability/instrumentation.js +472 -0
  51. package/dist/ask-observability/instrumentation.js.map +1 -0
  52. package/dist/ask-observability/observer.d.ts +62 -0
  53. package/dist/ask-observability/observer.d.ts.map +1 -0
  54. package/dist/ask-observability/observer.js +537 -0
  55. package/dist/ask-observability/observer.js.map +1 -0
  56. package/dist/ask-observability/portable.d.ts +98 -0
  57. package/dist/ask-observability/portable.d.ts.map +1 -0
  58. package/dist/ask-observability/portable.js +527 -0
  59. package/dist/ask-observability/portable.js.map +1 -0
  60. package/dist/ask-observability/store.d.ts +130 -0
  61. package/dist/ask-observability/store.d.ts.map +1 -0
  62. package/dist/ask-observability/store.js +1114 -0
  63. package/dist/ask-observability/store.js.map +1 -0
  64. package/dist/ask-observability/types.d.ts +357 -0
  65. package/dist/ask-observability/types.d.ts.map +1 -0
  66. package/dist/ask-observability/types.js +2 -0
  67. package/dist/ask-observability/types.js.map +1 -0
  68. package/dist/ask-observability/utils.d.ts +21 -0
  69. package/dist/ask-observability/utils.d.ts.map +1 -0
  70. package/dist/ask-observability/utils.js +102 -0
  71. package/dist/ask-observability/utils.js.map +1 -0
  72. package/dist/generated-analytical-proposal.d.ts +37 -1
  73. package/dist/generated-analytical-proposal.d.ts.map +1 -1
  74. package/dist/generated-analytical-proposal.js +211 -3
  75. package/dist/generated-analytical-proposal.js.map +1 -1
  76. package/dist/governed-relational-compiler.d.ts +15 -0
  77. package/dist/governed-relational-compiler.d.ts.map +1 -1
  78. package/dist/governed-relational-compiler.js +99 -1
  79. package/dist/governed-relational-compiler.js.map +1 -1
  80. package/dist/index.d.ts +9 -7
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +6 -4
  83. package/dist/index.js.map +1 -1
  84. package/dist/intent-controller.d.ts +10 -0
  85. package/dist/intent-controller.d.ts.map +1 -1
  86. package/dist/intent-controller.js +1 -1
  87. package/dist/intent-controller.js.map +1 -1
  88. package/dist/meaning-resolution.d.ts +78 -6
  89. package/dist/meaning-resolution.d.ts.map +1 -1
  90. package/dist/meaning-resolution.js +331 -38
  91. package/dist/meaning-resolution.js.map +1 -1
  92. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  93. package/dist/metadata/analysis-planner.js +21 -1
  94. package/dist/metadata/analysis-planner.js.map +1 -1
  95. package/dist/metadata/catalog.d.ts.map +1 -1
  96. package/dist/metadata/catalog.js +1 -1
  97. package/dist/metadata/catalog.js.map +1 -1
  98. package/dist/metadata/meaning-evidence.d.ts +9 -2
  99. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  100. package/dist/metadata/meaning-evidence.js +261 -11
  101. package/dist/metadata/meaning-evidence.js.map +1 -1
  102. package/dist/plan-execution-adapter.d.ts +9 -1
  103. package/dist/plan-execution-adapter.d.ts.map +1 -1
  104. package/dist/plan-execution-adapter.js +93 -13
  105. package/dist/plan-execution-adapter.js.map +1 -1
  106. package/dist/provider-egress.d.ts +24 -10
  107. package/dist/provider-egress.d.ts.map +1 -1
  108. package/dist/provider-egress.js +22 -17
  109. package/dist/provider-egress.js.map +1 -1
  110. package/dist/providers/claude.d.ts.map +1 -1
  111. package/dist/providers/claude.js +10 -9
  112. package/dist/providers/claude.js.map +1 -1
  113. package/dist/providers/dispatch.d.ts +27 -1
  114. package/dist/providers/dispatch.d.ts.map +1 -1
  115. package/dist/providers/dispatch.js +63 -1
  116. package/dist/providers/dispatch.js.map +1 -1
  117. package/dist/providers/gemini.js +10 -10
  118. package/dist/providers/gemini.js.map +1 -1
  119. package/dist/providers/index.d.ts +2 -2
  120. package/dist/providers/index.d.ts.map +1 -1
  121. package/dist/providers/index.js +1 -1
  122. package/dist/providers/index.js.map +1 -1
  123. package/dist/providers/ollama.js +15 -15
  124. package/dist/providers/ollama.js.map +1 -1
  125. package/dist/providers/openai.js +22 -22
  126. package/dist/providers/openai.js.map +1 -1
  127. package/dist/providers/types.d.ts +52 -0
  128. package/dist/providers/types.d.ts.map +1 -1
  129. package/dist/providers/types.js.map +1 -1
  130. package/dist/relationship-proof.d.ts +81 -0
  131. package/dist/relationship-proof.d.ts.map +1 -1
  132. package/dist/relationship-proof.js +228 -4
  133. package/dist/relationship-proof.js.map +1 -1
  134. package/dist/resolved-analytical-plan.d.ts +18 -4
  135. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  136. package/dist/resolved-analytical-plan.js +764 -47
  137. package/dist/resolved-analytical-plan.js.map +1 -1
  138. package/dist/router.d.ts +10 -1
  139. package/dist/router.d.ts.map +1 -1
  140. package/dist/router.js +1785 -213
  141. package/dist/router.js.map +1 -1
  142. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  143. package/dist/semantic-bridge/compose.js +23 -4
  144. package/dist/semantic-bridge/compose.js.map +1 -1
  145. package/package.json +4 -4
@@ -0,0 +1,98 @@
1
+ import type { AskTraceDataV1, AskTraceExportReceiptV1 } from './types.js';
2
+ export type AskTraceExportProfileV1 = 'strict' | 'support';
3
+ export interface AskTraceExportOptionsV1 {
4
+ profile: AskTraceExportProfileV1;
5
+ outputDirectory: string;
6
+ /** Support exports require an explicit human confirmation from the CLI/API. */
7
+ confirmReviewedIdentifiers?: boolean;
8
+ /** Never sourced from a stored run: support prose is opt-in reviewed input. */
9
+ reviewedQuestion?: string;
10
+ runReceipt?: unknown;
11
+ provenance?: 'live' | 'recorded' | 'synthetic' | 'migrated' | 'unknown';
12
+ }
13
+ export type AskTracePortableBundleOptionsV1 = Omit<AskTraceExportOptionsV1, 'outputDirectory'>;
14
+ /** A redacted in-memory bundle for the local runtime download endpoint. */
15
+ export interface AskTracePortableBundleV1 {
16
+ manifest: AskTraceBundleManifestV1;
17
+ trace: AskTraceDataV1;
18
+ runReceipt: Record<string, unknown>;
19
+ redactionReceipt: Record<string, unknown>;
20
+ }
21
+ export interface AskTraceBundleValidationV1 {
22
+ valid: boolean;
23
+ errors: string[];
24
+ manifest?: AskTraceBundleManifestV1;
25
+ }
26
+ export interface AskTraceBundleManifestV1 {
27
+ version: 1;
28
+ schema: 'dql.ask-trace-bundle.v1';
29
+ profile: AskTraceExportProfileV1;
30
+ createdAt: string;
31
+ provenance: 'live' | 'recorded' | 'synthetic' | 'migrated' | 'unknown';
32
+ bundleFingerprint: string;
33
+ checksums: Record<'trace.json' | 'run-receipt.json' | 'redaction-receipt.json', string>;
34
+ traceFingerprint?: string;
35
+ traceId: string;
36
+ }
37
+ export interface AskTraceCompareResultV1 {
38
+ compatibleIdentity: boolean;
39
+ leftTraceId: string;
40
+ rightTraceId: string;
41
+ selectedTier: {
42
+ left?: string;
43
+ right?: string;
44
+ matches: boolean;
45
+ };
46
+ recordingStatus: {
47
+ left: string;
48
+ right: string;
49
+ matches: boolean;
50
+ };
51
+ sourceCoverageChanges: Array<{
52
+ source: string;
53
+ left?: string;
54
+ right?: string;
55
+ }>;
56
+ candidateReasonChanges: Array<{
57
+ role: string;
58
+ reason: string;
59
+ left: number;
60
+ right: number;
61
+ }>;
62
+ attempts: {
63
+ left: number;
64
+ right: number;
65
+ };
66
+ providerAttempts: {
67
+ left: number;
68
+ right: number;
69
+ };
70
+ toolCalls: {
71
+ left: number;
72
+ right: number;
73
+ };
74
+ sqlAttempts: {
75
+ left: number;
76
+ right: number;
77
+ };
78
+ trustState: {
79
+ left?: string;
80
+ right?: string;
81
+ matches: boolean;
82
+ };
83
+ durationDeltaMs?: number;
84
+ notes: string[];
85
+ }
86
+ export declare function createAskTracePortableBundleV1(trace: AskTraceDataV1, options: AskTracePortableBundleOptionsV1): AskTracePortableBundleV1;
87
+ export declare function exportAskTraceBundleV1(trace: AskTraceDataV1, options: AskTraceExportOptionsV1): AskTraceExportReceiptV1;
88
+ export declare function validateAskTraceBundleV1(directory: string): AskTraceBundleValidationV1;
89
+ /** Offline-only receipt replay. It validates; it never imports providers/tools/connectors. */
90
+ export declare function replayAskTraceReceiptV1(directory: string): {
91
+ valid: boolean;
92
+ errors: string[];
93
+ trace?: AskTraceDataV1;
94
+ };
95
+ export declare function compareAskTracesV1(left: AskTraceDataV1, right: AskTraceDataV1): AskTraceCompareResultV1;
96
+ /** Portable, local JSON mapper — intentionally no OTLP network exporter. */
97
+ export declare function toOtlpOpenInferenceJsonV1(trace: AskTraceDataV1): Record<string, unknown>;
98
+ //# sourceMappingURL=portable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portable.d.ts","sourceRoot":"","sources":["../../src/ask-observability/portable.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,527 @@
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
+ return {
316
+ version: 1,
317
+ traceReference: {
318
+ traceId: profile === 'strict' ? pseudo(trace.envelope.traceId, salt, 'trace') : trace.envelope.traceId,
319
+ recordingStatus: trace.envelope.recordingStatus,
320
+ traceFingerprint: trace.envelope.traceFingerprint,
321
+ },
322
+ route: typeof record.route === 'string' ? record.route : undefined,
323
+ status: typeof record.status === 'string' ? record.status : trace.envelope.status,
324
+ trustState: typeof record.trustState === 'string' ? record.trustState : trace.envelope.trustState,
325
+ selectedTier: trace.envelope.selectedTier,
326
+ planFrozen: source.planFrozen === true,
327
+ finalStopReason: typeof source.finalStopReason === 'string' ? source.finalStopReason : undefined,
328
+ ...(terminalGap ? { terminalGap } : {}),
329
+ telemetry: sanitizeTelemetry(record.telemetry),
330
+ fingerprints: {
331
+ plan: map(findFingerprint(record, 'planFingerprint'), 'plan'),
332
+ sql: map(findFingerprint(record, 'sqlFingerprint'), 'sql'),
333
+ result: map(findFingerprint(record, 'resultFingerprint'), 'result'),
334
+ },
335
+ };
336
+ }
337
+ /**
338
+ * Receipt export is intentionally stricter than its source type: only the
339
+ * one enumerated relationship proof requirement is portable. This prevents a
340
+ * future V3 field or a malformed persisted record from putting router prose,
341
+ * question terms, or inferred SQL relationships into a support bundle.
342
+ */
343
+ function portableTerminalRelationshipGapV1(value, profile, salt) {
344
+ if (!value || typeof value !== 'object' || Array.isArray(value))
345
+ return undefined;
346
+ const record = value;
347
+ if (record.code !== 'MISSING_RELATIONSHIP' || record.requirement !== 'certified_relationship_or_allocation_proof')
348
+ return undefined;
349
+ const witnessCandidateIds = Array.isArray(record.witnessCandidateIds)
350
+ ? [...new Set(record.witnessCandidateIds.filter((id) => typeof id === 'string' && id.length > 0))]
351
+ .sort()
352
+ .slice(0, 32)
353
+ .map((id) => profile === 'strict' ? pseudo(id, salt, 'candidate') : id)
354
+ : [];
355
+ return {
356
+ code: 'MISSING_RELATIONSHIP',
357
+ requirement: 'certified_relationship_or_allocation_proof',
358
+ witnessCandidateIds,
359
+ };
360
+ }
361
+ function sanitizeTelemetry(value) {
362
+ if (!value || typeof value !== 'object' || Array.isArray(value))
363
+ return undefined;
364
+ const record = value;
365
+ const result = {};
366
+ for (const key of ['providerRoundTrips', 'toolCalls', 'sqlExecutions', 'repairs', 'egressReceipts']) {
367
+ if (typeof record[key] === 'number' && Number.isFinite(record[key]))
368
+ result[key] = record[key];
369
+ }
370
+ return Object.keys(result).length ? result : undefined;
371
+ }
372
+ function findFingerprint(record, key) {
373
+ if (typeof record[key] === 'string')
374
+ return record[key];
375
+ const nested = record.diagnosticReceiptV3;
376
+ if (nested && typeof nested === 'object' && typeof nested[key] === 'string')
377
+ return nested[key];
378
+ return undefined;
379
+ }
380
+ function scanForProhibited(values) {
381
+ const source = values.map((value) => canonicalJson(value)).join('\n');
382
+ const findings = PROHIBITED.flatMap((pattern) => pattern.test(source) ? [pattern.source] : []);
383
+ return { passed: findings.length === 0, findings };
384
+ }
385
+ function validateTraceReceiptV1(trace) {
386
+ const errors = [];
387
+ // Strict portable bundles use deterministic export-scoped pseudonyms. Live
388
+ // receipts retain W3C identifiers. Both shapes are structurally valid.
389
+ if (!(/^[0-9a-f]{32}$/.test(trace.envelope.traceId) || /^trace_[0-9a-f]{16}$/.test(trace.envelope.traceId)))
390
+ errors.push('Invalid trace ID.');
391
+ if (!(/^[0-9a-f]{16}$/.test(trace.envelope.rootSpanId) || /^span_[0-9a-f]{16}$/.test(trace.envelope.rootSpanId)))
392
+ errors.push('Invalid root span ID.');
393
+ const ids = new Set();
394
+ const ordinals = new Set();
395
+ for (const span of trace.spans) {
396
+ if (span.traceId !== trace.envelope.traceId)
397
+ errors.push(`Span ${span.spanId} has a mismatched trace ID.`);
398
+ if (ids.has(span.spanId))
399
+ errors.push(`Duplicate span ${span.spanId}.`);
400
+ ids.add(span.spanId);
401
+ if (ordinals.has(span.ordinal))
402
+ errors.push(`Duplicate span ordinal ${span.ordinal}.`);
403
+ ordinals.add(span.ordinal);
404
+ if (span.parentSpanId && !ids.has(span.parentSpanId) && !trace.spans.some((candidate) => candidate.spanId === span.parentSpanId))
405
+ errors.push(`Unknown parent span ${span.parentSpanId}.`);
406
+ }
407
+ if (!ids.has(trace.envelope.rootSpanId))
408
+ errors.push('Root span is missing from trace detail.');
409
+ if (trace.envelope.firstIssueSpanId && !ids.has(trace.envelope.firstIssueSpanId)) {
410
+ errors.push(`Unknown first issue span ${trace.envelope.firstIssueSpanId}.`);
411
+ }
412
+ for (const candidate of trace.candidateDecisions) {
413
+ if (candidate.traceId !== trace.envelope.traceId)
414
+ errors.push(`Candidate ${candidate.sequence} has a mismatched trace ID.`);
415
+ }
416
+ for (const link of trace.links) {
417
+ const sourceIsCurrent = link.sourceTraceId === trace.envelope.traceId;
418
+ const targetIsCurrent = link.targetTraceId === trace.envelope.traceId;
419
+ if (!sourceIsCurrent && !targetIsCurrent) {
420
+ errors.push(`Trace link ${link.kind} does not reference the exported trace.`);
421
+ continue;
422
+ }
423
+ if (sourceIsCurrent && link.sourceRunId !== trace.envelope.runId) {
424
+ errors.push(`Trace link ${link.kind} has a broken source run reference.`);
425
+ }
426
+ if (targetIsCurrent && link.targetRunId !== trace.envelope.runId) {
427
+ errors.push(`Trace link ${link.kind} has a broken target run reference.`);
428
+ }
429
+ }
430
+ const cascade = trace.spans.flatMap((span) => span.payload.kind === 'cascade' ? [{ span, decision: span.payload.decision }] : []);
431
+ for (const { span, decision } of cascade) {
432
+ const expected = ['certified', 'semantic', 'governed_relational', 'exploratory_sql', 'clarify_or_gap'];
433
+ let last = -1;
434
+ let frozen = false;
435
+ for (const attempt of decision.attempts) {
436
+ const position = expected.indexOf(attempt.tier);
437
+ if (position < last)
438
+ errors.push('Cascade attempts are not authoritative order.');
439
+ last = position;
440
+ if (frozen)
441
+ errors.push('Cascade continued after plan freeze.');
442
+ frozen ||= attempt.planFrozen;
443
+ }
444
+ // Exploratory selection is allowed to appear before the physical plan has
445
+ // frozen. The initial router receipt is authoritative evidence of that
446
+ // pre-freeze selection; a later plan.freeze span proves the only valid
447
+ // transition. Do not rewrite the original receipt merely to make a
448
+ // portable bundle validate, and do not accept an unrelated later freeze.
449
+ if (span.name === 'cascade.evaluate'
450
+ && decision.selectedTier === 'exploratory_sql'
451
+ && decision.planFrozen === false
452
+ && !hasLaterMatchingExploratoryFreeze(trace, span)) {
453
+ errors.push('Exploratory selection is not frozen.');
454
+ }
455
+ if (decision.terminalGap) {
456
+ if (decision.terminalGap.code !== 'MISSING_RELATIONSHIP'
457
+ || decision.terminalGap.requirement !== 'certified_relationship_or_allocation_proof') {
458
+ errors.push('Unsupported terminal relationship gap receipt.');
459
+ }
460
+ if (!Array.isArray(decision.terminalGap.witnessCandidateIds)
461
+ || decision.terminalGap.witnessCandidateIds.some((id) => typeof id !== 'string' || id.length === 0)) {
462
+ errors.push('Invalid terminal relationship gap witnesses.');
463
+ }
464
+ }
465
+ }
466
+ return errors;
467
+ }
468
+ function hasLaterMatchingExploratoryFreeze(trace, selection) {
469
+ if (selection.payload.kind !== 'cascade')
470
+ return false;
471
+ const selectedAttempt = selection.payload.decision.attempts.find((attempt) => attempt.tier === 'exploratory_sql');
472
+ if (!selectedAttempt)
473
+ return false;
474
+ return trace.spans.some((span) => {
475
+ if (span.ordinal <= selection.ordinal
476
+ || span.name !== 'plan.freeze'
477
+ || span.outcome !== 'ok'
478
+ || span.reasonCode !== 'plan_frozen'
479
+ || span.payload.kind !== 'cascade')
480
+ return false;
481
+ const frozen = span.payload.decision;
482
+ const frozenAttempt = frozen.attempts.find((attempt) => attempt.tier === 'exploratory_sql');
483
+ return frozen.selectedTier === 'exploratory_sql'
484
+ && frozen.planFrozen === true
485
+ && frozenAttempt?.planFrozen === true
486
+ && sameCandidateIds(selectedAttempt.candidateIds, frozenAttempt.candidateIds);
487
+ });
488
+ }
489
+ function sameCandidateIds(left, right) {
490
+ const a = [...new Set(left)].sort();
491
+ const b = [...new Set(right)].sort();
492
+ return a.length === b.length && a.every((id, index) => id === b[index]);
493
+ }
494
+ function validateRunReceiptTerminalGapV1(receipt, trace) {
495
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt))
496
+ return [];
497
+ const record = receipt;
498
+ const gap = record.terminalGap;
499
+ if (gap === undefined)
500
+ return [];
501
+ const portable = portableTerminalRelationshipGapV1(gap, 'support', 'validation');
502
+ if (!portable)
503
+ return ['Invalid run receipt terminal relationship gap.'];
504
+ if (!trace)
505
+ return ['Run receipt terminal relationship gap has no trace.'];
506
+ const traceGap = trace.spans
507
+ .flatMap((span) => span.payload.kind === 'cascade' ? [span.payload.decision.terminalGap] : [])
508
+ .find((candidate) => candidate?.code === portable.code && candidate.requirement === portable.requirement);
509
+ if (!traceGap)
510
+ return ['Run receipt terminal relationship gap is not present in the trace.'];
511
+ const expected = [...new Set(traceGap.witnessCandidateIds)].sort();
512
+ const actual = [...new Set(portable.witnessCandidateIds)].sort();
513
+ if (expected.length !== actual.length || expected.some((id, index) => id !== actual[index])) {
514
+ return ['Run receipt terminal relationship gap witnesses do not match the trace.'];
515
+ }
516
+ return [];
517
+ }
518
+ function parseJsonFile(path, errors) {
519
+ try {
520
+ return JSON.parse(readFileSync(path, 'utf8'));
521
+ }
522
+ catch {
523
+ errors.push(`Invalid JSON in ${basename(path)}.`);
524
+ return undefined;
525
+ }
526
+ }
527
+ //# sourceMappingURL=portable.js.map