@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.
- package/dist/agent-run-engine.d.ts +239 -4
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +2924 -85
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +51 -3
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-planner.d.ts +6 -0
- package/dist/agent-run-planner.d.ts.map +1 -1
- package/dist/agent-run-planner.js +2 -0
- package/dist/agent-run-planner.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +591 -11
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +9 -1
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +204 -4
- package/dist/agentic/analyst-loop.js.map +1 -1
- package/dist/agentic/assumptions.d.ts +1 -1
- package/dist/agentic/assumptions.d.ts.map +1 -1
- package/dist/agentic/assumptions.js.map +1 -1
- package/dist/agentic/ledger-tools.d.ts.map +1 -1
- package/dist/agentic/ledger-tools.js +8 -1
- package/dist/agentic/ledger-tools.js.map +1 -1
- package/dist/agentic/orchestrator-policy.d.ts +3 -1
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
- package/dist/agentic/orchestrator-policy.js +1 -1
- package/dist/agentic/orchestrator-policy.js.map +1 -1
- package/dist/agentic/research-agent.d.ts.map +1 -1
- package/dist/agentic/research-agent.js +10 -1
- package/dist/agentic/research-agent.js.map +1 -1
- package/dist/agentic/sql-authorization.d.ts +8 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -1
- package/dist/agentic/sql-authorization.js +3 -0
- package/dist/agentic/sql-authorization.js.map +1 -1
- package/dist/agentic/tool-loop.d.ts +13 -1
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +441 -32
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/analytical-compatibility.d.ts.map +1 -1
- package/dist/analytical-compatibility.js +8 -1
- package/dist/analytical-compatibility.js.map +1 -1
- package/dist/analytical-failure-repair.d.ts.map +1 -1
- package/dist/analytical-failure-repair.js +6 -2
- package/dist/analytical-failure-repair.js.map +1 -1
- package/dist/analytical-frame.d.ts +42 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +490 -26
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +1259 -9
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +970 -84
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +230 -10
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1182 -85
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +27 -0
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +55 -0
- package/dist/answer-shape.js.map +1 -1
- package/dist/ask-observability/index.d.ts +11 -0
- package/dist/ask-observability/index.d.ts.map +1 -0
- package/dist/ask-observability/index.js +7 -0
- package/dist/ask-observability/index.js.map +1 -0
- package/dist/ask-observability/instrumentation.d.ts +35 -0
- package/dist/ask-observability/instrumentation.d.ts.map +1 -0
- package/dist/ask-observability/instrumentation.js +523 -0
- package/dist/ask-observability/instrumentation.js.map +1 -0
- package/dist/ask-observability/observer.d.ts +62 -0
- package/dist/ask-observability/observer.d.ts.map +1 -0
- package/dist/ask-observability/observer.js +537 -0
- package/dist/ask-observability/observer.js.map +1 -0
- package/dist/ask-observability/portable.d.ts +98 -0
- package/dist/ask-observability/portable.d.ts.map +1 -0
- package/dist/ask-observability/portable.js +1089 -0
- package/dist/ask-observability/portable.js.map +1 -0
- package/dist/ask-observability/store.d.ts +130 -0
- package/dist/ask-observability/store.d.ts.map +1 -0
- package/dist/ask-observability/store.js +1114 -0
- package/dist/ask-observability/store.js.map +1 -0
- package/dist/ask-observability/types.d.ts +380 -0
- package/dist/ask-observability/types.d.ts.map +1 -0
- package/dist/ask-observability/types.js +2 -0
- package/dist/ask-observability/types.js.map +1 -0
- package/dist/ask-observability/utils.d.ts +21 -0
- package/dist/ask-observability/utils.d.ts.map +1 -0
- package/dist/ask-observability/utils.js +102 -0
- package/dist/ask-observability/utils.js.map +1 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
- package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
- package/dist/conversation/session-store.d.ts +31 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +38 -0
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts +4 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +85 -3
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +2 -0
- package/dist/conversation/turn-trust.js.map +1 -1
- package/dist/embeddings/provider.d.ts +1 -0
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +18 -0
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/generated-analytical-proposal.d.ts +37 -1
- package/dist/generated-analytical-proposal.d.ts.map +1 -1
- package/dist/generated-analytical-proposal.js +246 -3
- package/dist/generated-analytical-proposal.js.map +1 -1
- package/dist/governed-relational-compiler.d.ts +15 -0
- package/dist/governed-relational-compiler.d.ts.map +1 -1
- package/dist/governed-relational-compiler.js +99 -1
- package/dist/governed-relational-compiler.js.map +1 -1
- package/dist/index.d.ts +21 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -7
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +87 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +2 -2
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.js +32 -2
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +207 -6
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +439 -53
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +78 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts +14 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +29 -2
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +28 -2
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +373 -16
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/plan-execution-adapter.d.ts +9 -1
- package/dist/plan-execution-adapter.d.ts.map +1 -1
- package/dist/plan-execution-adapter.js +93 -13
- package/dist/plan-execution-adapter.js.map +1 -1
- package/dist/provider-egress.d.ts +28 -11
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +72 -22
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts +2 -2
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +201 -37
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/dispatch.d.ts +27 -1
- package/dist/providers/dispatch.d.ts.map +1 -1
- package/dist/providers/dispatch.js +63 -1
- package/dist/providers/dispatch.js.map +1 -1
- package/dist/providers/gemini.js +10 -10
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.js +15 -15
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +213 -43
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +131 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/relationship-proof.d.ts +81 -0
- package/dist/relationship-proof.d.ts.map +1 -1
- package/dist/relationship-proof.js +228 -4
- package/dist/relationship-proof.js.map +1 -1
- package/dist/requirement-clauses.d.ts +23 -0
- package/dist/requirement-clauses.d.ts.map +1 -0
- package/dist/requirement-clauses.js +70 -0
- package/dist/requirement-clauses.js.map +1 -0
- package/dist/resolved-analytical-plan.d.ts +18 -4
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +953 -50
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +118 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +2481 -232
- package/dist/router.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +23 -4
- package/dist/semantic-bridge/compose.js.map +1 -1
- 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
|