@kontourai/survey 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,164 @@
1
+ import type { RawSource } from "./types.js";
2
+ import type { ClaimTargetHint, ReviewItem, ReviewValueType } from "./review-resource.js";
3
+ /** The upstream-owned portable extraction-result wire identifiers accepted by this adapter. */
4
+ export declare const portableExtractionResultFormat = "traverse-extraction-result";
5
+ export declare const portableExtractionResultVersion = 1;
6
+ export declare const extractionEnvelopeImportApiVersion = "survey.kontourai.io/v1alpha1";
7
+ export interface PortableExtractionOccurrence {
8
+ resolverVersion: "exact-occurrence-v1";
9
+ count: number;
10
+ selected: {
11
+ index: number;
12
+ start: number;
13
+ end: number;
14
+ };
15
+ selection: "source-order" | "occurrence-hint";
16
+ hintUsed: boolean;
17
+ ambiguous: boolean;
18
+ }
19
+ export interface PortableExtractionProposal {
20
+ fieldPath: string;
21
+ candidateValue: unknown;
22
+ confidence: number;
23
+ provenance: {
24
+ excerpt: string;
25
+ locator: string;
26
+ occurrence: PortableExtractionOccurrence;
27
+ };
28
+ extractor: string;
29
+ pathIndices?: number[];
30
+ inferenceType?: "explicit" | "inferred";
31
+ valueType?: ReviewValueType;
32
+ enumValues?: string[];
33
+ }
34
+ export type PortablePreparedArtifactState = {
35
+ status: "available" | "unavailable" | "storage-error";
36
+ requestedRef: string;
37
+ canonicalRef: string;
38
+ } | {
39
+ status: "identity-mismatch";
40
+ requestedRef: string;
41
+ canonicalRef: string;
42
+ } | {
43
+ status: "invalid-artifact";
44
+ reason: string;
45
+ canonicalRef: string;
46
+ } | {
47
+ status: "digest-mismatch";
48
+ requestedRef: string;
49
+ canonicalRef: string;
50
+ actualDigest: string;
51
+ actualContentLength: number;
52
+ };
53
+ export interface PortableExtractionResultEnvelope {
54
+ format: typeof portableExtractionResultFormat;
55
+ version: typeof portableExtractionResultVersion;
56
+ source: {
57
+ ref: string;
58
+ snapshotRef?: string;
59
+ };
60
+ result: {
61
+ proposals: PortableExtractionProposal[];
62
+ provider: string;
63
+ model?: string;
64
+ runId: string;
65
+ raw: {
66
+ tokensUsed?: number;
67
+ };
68
+ outcome: {
69
+ status: "success";
70
+ } | {
71
+ status: "partial";
72
+ reason: "cancelled" | "max-provider-calls" | "max-total-tokens";
73
+ } | {
74
+ status: "failure";
75
+ category: "invalid-config" | "invalid-task" | "preparation" | "provider" | "unexpected";
76
+ code: string;
77
+ };
78
+ warningClassifications?: Array<{
79
+ category: "provider" | "normalization" | "preparation" | "limit" | "storage" | "content" | "other";
80
+ code: string;
81
+ }>;
82
+ extractedAt: string;
83
+ providerCalls: number;
84
+ totalTokensUsed: number;
85
+ partial?: {
86
+ reason: "cancelled" | "max-provider-calls" | "max-total-tokens";
87
+ completedChunks: number;
88
+ remainingChunks: number;
89
+ tokenOvershoot?: number;
90
+ };
91
+ providerFailures?: Array<{
92
+ provider: string;
93
+ kind: "authentication" | "rate-limit" | "timeout" | "invalid-request" | "unavailable" | "unknown";
94
+ retryable: boolean;
95
+ }>;
96
+ taskDigest?: string;
97
+ exampleDigests?: string[];
98
+ pdfPageOffsets?: number[];
99
+ ocrDerived?: true;
100
+ preparedArtifact?: {
101
+ format: "traverse-prepared-artifact";
102
+ version: 1;
103
+ digest: string;
104
+ ref: string;
105
+ preparationMode: string;
106
+ preparationVersion: string;
107
+ contentLength: number;
108
+ sourceSnapshotRef?: string;
109
+ };
110
+ preparedArtifactState?: PortablePreparedArtifactState;
111
+ };
112
+ }
113
+ export interface ExtractionEnvelopeImportOptions {
114
+ /** Stable Survey import identity. Defaults to the upstream run id. */
115
+ importName?: string;
116
+ /** Producer namespace used to avoid cross-producer identity collisions. */
117
+ producerNamespace?: string;
118
+ sourceKind: RawSource["kind"];
119
+ /** Survey meaning is supplied at the boundary; it is not added to the upstream wire contract. */
120
+ claimTarget: (proposal: PortableExtractionProposal, index: number) => ClaimTargetHint;
121
+ }
122
+ export type ExtractionEnvelopeImportDiagnostic = {
123
+ kind: "artifact-unavailable";
124
+ status: "unavailable" | "storage-error" | "identity-mismatch" | "invalid-artifact";
125
+ artifactRef?: string;
126
+ message: string;
127
+ } | {
128
+ kind: "digest-mismatch";
129
+ artifactRef: string;
130
+ expectedDigest: string;
131
+ actualDigest: string;
132
+ message: string;
133
+ };
134
+ export interface ExtractionEnvelopeImport {
135
+ apiVersion: typeof extractionEnvelopeImportApiVersion;
136
+ kind: "ExtractionEnvelopeImport";
137
+ metadata: {
138
+ name: string;
139
+ producerNamespace: string;
140
+ };
141
+ spec: {
142
+ envelope: PortableExtractionResultEnvelope;
143
+ sourceKind: RawSource["kind"];
144
+ claimTargets: ClaimTargetHint[];
145
+ };
146
+ status: {
147
+ state: "grounded" | "unresolved";
148
+ diagnostics: ExtractionEnvelopeImportDiagnostic[];
149
+ };
150
+ }
151
+ export interface ExtractionEnvelopeImportResult {
152
+ record: ExtractionEnvelopeImport;
153
+ reviewItems: ReviewItem[];
154
+ }
155
+ export interface ExtractionEnvelopeResolutionIdentity {
156
+ evidenceId: string;
157
+ eventId: string;
158
+ }
159
+ /** Parse an untrusted upstream document and create Survey's durable import projection. */
160
+ export declare function importExtractionEnvelope(serialized: string | PortableExtractionResultEnvelope, options: ExtractionEnvelopeImportOptions): ExtractionEnvelopeImportResult;
161
+ export declare function buildReviewItemsFromExtractionEnvelopeImport(record: ExtractionEnvelopeImport): ReviewItem[];
162
+ export declare function exportExtractionEnvelopeImport(record: ExtractionEnvelopeImport): string;
163
+ export declare function reimportExtractionEnvelope(serialized: string): ExtractionEnvelopeImport;
164
+ export declare function createExtractionEnvelopeResolutionIdentity(record: ExtractionEnvelopeImport, proposalIndex: number): ExtractionEnvelopeResolutionIdentity;
@@ -0,0 +1,427 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { reviewResourceApiVersion } from "./review-resource.js";
3
+ import { canonicalJson } from "./review-workbench/canonical.js";
4
+ /** The upstream-owned portable extraction-result wire identifiers accepted by this adapter. */
5
+ export const portableExtractionResultFormat = "traverse-extraction-result";
6
+ export const portableExtractionResultVersion = 1;
7
+ export const extractionEnvelopeImportApiVersion = "survey.kontourai.io/v1alpha1";
8
+ /** Parse an untrusted upstream document and create Survey's durable import projection. */
9
+ export function importExtractionEnvelope(serialized, options) {
10
+ let parsed;
11
+ if (typeof serialized === "string") {
12
+ try {
13
+ parsed = JSON.parse(serialized);
14
+ }
15
+ catch {
16
+ throw new Error("Portable extraction envelope is not valid JSON.");
17
+ }
18
+ }
19
+ else
20
+ parsed = serialized;
21
+ const envelope = validateEnvelope(parsed);
22
+ const claimTargets = envelope.result.proposals.map(options.claimTarget);
23
+ claimTargets.forEach(validateClaimTarget);
24
+ const name = options.importName ?? envelope.result.runId;
25
+ const producerNamespace = options.producerNamespace ?? envelope.result.provider;
26
+ nonEmpty(name, "Extraction envelope import name");
27
+ nonEmpty(producerNamespace, "Extraction envelope producer namespace");
28
+ const diagnostics = diagnosticsFor(envelope);
29
+ const record = {
30
+ apiVersion: extractionEnvelopeImportApiVersion,
31
+ kind: "ExtractionEnvelopeImport",
32
+ metadata: { name, producerNamespace },
33
+ spec: { envelope, sourceKind: options.sourceKind, claimTargets: cloneJson(claimTargets) },
34
+ status: { state: diagnostics.length ? "unresolved" : "grounded", diagnostics },
35
+ };
36
+ return { record, reviewItems: buildReviewItemsFromExtractionEnvelopeImport(record) };
37
+ }
38
+ export function buildReviewItemsFromExtractionEnvelopeImport(record) {
39
+ validateImport(record);
40
+ if (record.status.state !== "grounded")
41
+ return [];
42
+ return record.spec.envelope.result.proposals.map((proposal, index) => buildReviewItem(record, proposal, index));
43
+ }
44
+ export function exportExtractionEnvelopeImport(record) {
45
+ validateImport(record);
46
+ return canonicalJson(record);
47
+ }
48
+ export function reimportExtractionEnvelope(serialized) {
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(serialized);
52
+ }
53
+ catch {
54
+ throw new Error("Extraction envelope import is not valid JSON.");
55
+ }
56
+ validateImport(parsed);
57
+ return cloneJson(parsed);
58
+ }
59
+ export function createExtractionEnvelopeResolutionIdentity(record, proposalIndex) {
60
+ validateImport(record);
61
+ if (!Number.isSafeInteger(proposalIndex) || proposalIndex < 0 || proposalIndex >= record.spec.envelope.result.proposals.length) {
62
+ throw new Error(`Extraction envelope import ${record.metadata.name} has no proposal at index ${proposalIndex}.`);
63
+ }
64
+ const base = identityHash(identityInputs(record, record.spec.envelope.result.proposals[proposalIndex], proposalIndex));
65
+ const nonce = randomUUID();
66
+ return { evidenceId: `survey.extraction.${base}.resolution-evidence.${nonce}`, eventId: `survey.extraction.${base}.resolution-event.${nonce}` };
67
+ }
68
+ function buildReviewItem(record, proposal, index) {
69
+ const envelope = record.spec.envelope;
70
+ const identity = identityHash(identityInputs(record, proposal, index));
71
+ const evidence = identityHash(evidenceInputs(record, proposal));
72
+ const target = record.spec.claimTargets[index];
73
+ const valueType = proposal.valueType ?? inferValueType(proposal.candidateValue);
74
+ const candidate = {
75
+ id: `extraction-envelope.${identity}.proposed`, role: "proposed", value: proposal.candidateValue,
76
+ confidence: proposal.confidence,
77
+ source: {
78
+ sourceRef: envelope.source.ref,
79
+ sourceId: envelope.source.snapshotRef ?? envelope.source.ref,
80
+ kind: record.spec.sourceKind,
81
+ observedAt: envelope.result.extractedAt,
82
+ ...(envelope.result.preparedArtifact?.digest ? { checksum: envelope.result.preparedArtifact.digest } : {}),
83
+ locatorScheme: "text-span",
84
+ },
85
+ locator: { scheme: "text-span", locator: proposal.provenance.locator, excerpt: proposal.provenance.excerpt },
86
+ extraction: {
87
+ extractionId: `extraction-envelope.${identity}`,
88
+ target: proposal.fieldPath,
89
+ confidence: proposal.confidence,
90
+ extractor: proposal.extractor,
91
+ ...(envelope.result.model ? { model: envelope.result.model } : {}),
92
+ },
93
+ claimTarget: target,
94
+ producer: { "survey.kontourai.io/extraction-envelope": {
95
+ importName: record.metadata.name, proposalIndex: index,
96
+ evidenceId: `survey.extraction.${evidence}.source-evidence`,
97
+ runId: envelope.result.runId, provider: envelope.result.provider,
98
+ ...(envelope.result.model ? { model: envelope.result.model } : {}),
99
+ ...(envelope.result.taskDigest ? { taskDigest: envelope.result.taskDigest } : {}),
100
+ ...(envelope.result.exampleDigests ? { exampleDigests: envelope.result.exampleDigests } : {}),
101
+ valueType: { type: valueType, origin: proposal.inferenceType ?? "inferred" },
102
+ occurrence: proposal.provenance.occurrence,
103
+ attempt: { id: envelope.result.runId, providerCalls: envelope.result.providerCalls },
104
+ ...(envelope.result.warningClassifications ? { warnings: envelope.result.warningClassifications } : {}),
105
+ outcome: envelope.result.outcome,
106
+ } },
107
+ };
108
+ return {
109
+ apiVersion: reviewResourceApiVersion, kind: "ReviewItem",
110
+ metadata: { name: `extraction-envelope.${identity}`, producer: { "survey.kontourai.io/extraction-envelope": {
111
+ importName: record.metadata.name,
112
+ evidenceId: `survey.extraction.${evidence}.source-evidence`,
113
+ source: envelope.source,
114
+ ...(envelope.result.preparedArtifact ? { preparedArtifact: envelope.result.preparedArtifact } : {}),
115
+ } } },
116
+ spec: { target: proposal.fieldPath, candidates: [candidate], candidateSetStatus: "needs-review", valueDescriptor: { type: valueType }, editable: false },
117
+ status: { observedCandidateCount: 1 },
118
+ };
119
+ }
120
+ function identityInputs(record, proposal, index) {
121
+ return { producerNamespace: record.metadata.producerNamespace, importName: record.metadata.name, source: record.spec.envelope.source,
122
+ preparedArtifact: record.spec.envelope.result.preparedArtifact, runId: record.spec.envelope.result.runId,
123
+ proposalIndex: index, proposal, claimTarget: record.spec.claimTargets[index] };
124
+ }
125
+ function evidenceInputs(record, proposal) {
126
+ return { producerNamespace: record.metadata.producerNamespace, importName: record.metadata.name, source: record.spec.envelope.source,
127
+ sourceKind: record.spec.sourceKind, preparedArtifact: record.spec.envelope.result.preparedArtifact,
128
+ extractedAt: record.spec.envelope.result.extractedAt,
129
+ provenance: proposal.provenance };
130
+ }
131
+ function diagnosticsFor(envelope) {
132
+ const state = envelope.result.preparedArtifactState;
133
+ if (!state || state.status === "available")
134
+ return [];
135
+ if (state.status === "digest-mismatch")
136
+ return [{ kind: "digest-mismatch", artifactRef: state.canonicalRef,
137
+ expectedDigest: envelope.result.preparedArtifact.digest, actualDigest: state.actualDigest,
138
+ message: "Prepared artifact digest does not match the expected extraction artifact." }];
139
+ return [{ kind: "artifact-unavailable", status: state.status,
140
+ ...(state.status === "invalid-artifact" ? { artifactRef: state.canonicalRef } : { artifactRef: state.requestedRef }),
141
+ message: `Prepared artifact resolution is ${state.status}.` }];
142
+ }
143
+ function validateImport(value) {
144
+ jsonSafe(value, "Extraction envelope import");
145
+ const record = obj(value, "Extraction envelope import");
146
+ exact(record, ["apiVersion", "kind", "metadata", "spec", "status"], "Extraction envelope import");
147
+ if (record.apiVersion !== extractionEnvelopeImportApiVersion || record.kind !== "ExtractionEnvelopeImport")
148
+ throw new Error("Invalid extraction envelope import resource identity.");
149
+ const metadata = obj(record.metadata, "metadata");
150
+ exact(metadata, ["name", "producerNamespace"], "metadata");
151
+ nonEmpty(metadata.name, "metadata.name");
152
+ nonEmpty(metadata.producerNamespace, "metadata.producerNamespace");
153
+ const spec = obj(record.spec, "spec");
154
+ exact(spec, ["envelope", "sourceKind", "claimTargets"], "spec");
155
+ const envelope = validateEnvelope(spec.envelope);
156
+ if (!RAW_SOURCE_KINDS.has(spec.sourceKind))
157
+ throw new Error("spec.sourceKind is invalid.");
158
+ const targets = array(spec.claimTargets, "spec.claimTargets");
159
+ targets.forEach(validateClaimTarget);
160
+ if (targets.length !== envelope.result.proposals.length)
161
+ throw new Error("spec.claimTargets must align with proposals.");
162
+ const status = obj(record.status, "status");
163
+ exact(status, ["state", "diagnostics"], "status");
164
+ const expected = diagnosticsFor(envelope);
165
+ if (canonicalJson(status.diagnostics) !== canonicalJson(expected) || status.state !== (expected.length ? "unresolved" : "grounded"))
166
+ throw new Error("Import status does not match envelope state.");
167
+ }
168
+ function validateEnvelope(input) {
169
+ jsonSafe(input, "Portable extraction envelope");
170
+ const e = obj(input, "envelope");
171
+ exact(e, ["format", "version", "source", "result"], "envelope");
172
+ if (e.format !== portableExtractionResultFormat || e.version !== portableExtractionResultVersion)
173
+ throw new Error("Unsupported portable extraction envelope format or version.");
174
+ const source = obj(e.source, "source");
175
+ exact(source, ["ref"], "source", ["snapshotRef"]);
176
+ safeReference(source.ref, "source.ref");
177
+ if (source.snapshotRef !== undefined)
178
+ safeReference(source.snapshotRef, "source.snapshotRef");
179
+ const r = obj(e.result, "result");
180
+ exact(r, ["proposals", "provider", "runId", "raw", "outcome", "extractedAt", "providerCalls", "totalTokensUsed"], "result", ["model", "warningClassifications", "partial", "providerFailures", "taskDigest", "exampleDigests", "pdfPageOffsets", "ocrDerived", "preparedArtifact", "preparedArtifactState"]);
181
+ stableIdentity(r.provider, "result.provider");
182
+ if (r.model !== undefined)
183
+ stableIdentity(r.model, "result.model");
184
+ if (typeof r.runId !== "string" || !RUN_ID.test(r.runId))
185
+ throw new Error("result.runId is invalid.");
186
+ wireNonEmpty(r.extractedAt, "result.extractedAt");
187
+ integer(r.providerCalls, "result.providerCalls");
188
+ integer(r.totalTokensUsed, "result.totalTokensUsed");
189
+ const raw = obj(r.raw, "result.raw");
190
+ exact(raw, [], "result.raw", ["tokensUsed"]);
191
+ if (raw.tokensUsed !== undefined)
192
+ integer(raw.tokensUsed, "result.raw.tokensUsed");
193
+ validateOutcome(r.outcome, r.partial);
194
+ if (r.warningClassifications !== undefined)
195
+ array(r.warningClassifications, "warnings").forEach(validateWarning);
196
+ if (r.providerFailures !== undefined)
197
+ array(r.providerFailures, "providerFailures").forEach(validateFailure);
198
+ optionalDigest(r.taskDigest, "result.taskDigest");
199
+ if (r.exampleDigests !== undefined)
200
+ array(r.exampleDigests, "exampleDigests").forEach((v) => digest(v, "exampleDigest"));
201
+ if (r.pdfPageOffsets !== undefined) {
202
+ const offsets = array(r.pdfPageOffsets, "pdfPageOffsets");
203
+ offsets.forEach((v) => integer(v, "pageOffset"));
204
+ if (offsets.some((v, i) => i > 0 && v <= offsets[i - 1]))
205
+ throw new Error("pdfPageOffsets must ascend strictly.");
206
+ }
207
+ if (r.ocrDerived !== undefined && r.ocrDerived !== true)
208
+ throw new Error("result.ocrDerived must be true.");
209
+ const artifact = r.preparedArtifact === undefined ? undefined : validateArtifact(r.preparedArtifact);
210
+ const state = r.preparedArtifactState === undefined ? undefined : validateArtifactState(r.preparedArtifactState, artifact);
211
+ if (source.snapshotRef !== undefined && artifact?.sourceSnapshotRef !== undefined && source.snapshotRef !== artifact.sourceSnapshotRef)
212
+ throw new Error("Source snapshot identity mismatch.");
213
+ const proposals = array(r.proposals, "result.proposals").map((p, i) => validateProposal(p, i, artifact?.contentLength));
214
+ return cloneJson({ ...e, source, result: { ...r, proposals, ...(artifact ? { preparedArtifact: artifact } : {}), ...(state ? { preparedArtifactState: state } : {}) } });
215
+ }
216
+ function validateProposal(input, index, contentLength) {
217
+ const p = obj(input, `proposal[${index}]`);
218
+ exact(p, ["fieldPath", "candidateValue", "confidence", "provenance", "extractor"], `proposal[${index}]`, ["pathIndices", "inferenceType", "valueType", "enumValues"]);
219
+ wireNonEmpty(p.fieldPath, "proposal.fieldPath");
220
+ stableIdentity(p.extractor, "proposal.extractor");
221
+ finite(p.confidence, "proposal.confidence", 0, 1);
222
+ const provenance = obj(p.provenance, "proposal.provenance");
223
+ exact(provenance, ["excerpt", "locator", "occurrence"], "proposal.provenance");
224
+ wireNonEmpty(provenance.excerpt, "proposal.provenance.excerpt");
225
+ if (typeof provenance.locator !== "string")
226
+ throw new Error("proposal locator must be chars:start-end.");
227
+ const match = /^chars:(0|[1-9]\d*)-(0|[1-9]\d*)$/.exec(provenance.locator);
228
+ if (!match)
229
+ throw new Error("proposal locator must be chars:start-end.");
230
+ const start = Number(match[1]), end = Number(match[2]);
231
+ if (end < start || end - start !== provenance.excerpt.length || (contentLength !== undefined && end > contentLength))
232
+ throw new Error("proposal locator/excerpt span is incoherent.");
233
+ validateOccurrence(provenance.occurrence, start, end);
234
+ if (p.pathIndices !== undefined)
235
+ array(p.pathIndices, "pathIndices").forEach((v) => integer(v, "pathIndex"));
236
+ if (p.inferenceType !== undefined && p.inferenceType !== "explicit" && p.inferenceType !== "inferred")
237
+ throw new Error("proposal inferenceType is invalid.");
238
+ if (p.valueType !== undefined && !VALUE_TYPES.has(p.valueType))
239
+ throw new Error("proposal valueType is invalid.");
240
+ if (p.enumValues !== undefined)
241
+ array(p.enumValues, "enumValues").forEach((v) => wellFormedString(v, "enumValue"));
242
+ return cloneJson(p);
243
+ }
244
+ function validateOccurrence(input, start, end) { const o = obj(input, "occurrence"); exact(o, ["resolverVersion", "count", "selected", "selection", "hintUsed", "ambiguous"], "occurrence"); if (o.resolverVersion !== "exact-occurrence-v1")
245
+ throw new Error("occurrence resolver version is invalid."); integer(o.count, "occurrence.count"); const s = obj(o.selected, "occurrence.selected"); exact(s, ["index", "start", "end"], "occurrence.selected"); integer(s.index, "selected.index"); integer(s.start, "selected.start"); integer(s.end, "selected.end"); if (s.start !== start || s.end !== end || s.index >= o.count || o.count < 1)
246
+ throw new Error("occurrence selection is incoherent."); if (o.selection !== "source-order" && o.selection !== "occurrence-hint")
247
+ throw new Error("occurrence selection is invalid."); if (typeof o.hintUsed !== "boolean" || typeof o.ambiguous !== "boolean")
248
+ throw new Error("occurrence flags must be booleans."); if (o.hintUsed !== (o.selection === "occurrence-hint"))
249
+ throw new Error("occurrence hintUsed does not match selection."); if (o.ambiguous !== (o.count > 1))
250
+ throw new Error("occurrence ambiguous does not match count."); }
251
+ function validateArtifact(input) { const a = obj(input, "preparedArtifact"); exact(a, ["format", "version", "digest", "ref", "preparationMode", "preparationVersion", "contentLength"], "preparedArtifact", ["sourceSnapshotRef"]); if (a.format !== "traverse-prepared-artifact" || a.version !== 1)
252
+ throw new Error("prepared artifact format is invalid."); digest(a.digest, "artifact.digest", false); if (!PREPARATION_MODES.has(a.preparationMode))
253
+ throw new Error("artifact.preparationMode is invalid."); nonEmpty(a.preparationVersion, "artifact.preparationVersion"); integer(a.contentLength, "artifact.contentLength"); if (a.sourceSnapshotRef !== undefined)
254
+ wireNonEmpty(a.sourceSnapshotRef, "artifact.sourceSnapshotRef"); const binding = JSON.stringify({ format: a.format, version: a.version, digest: a.digest, preparationMode: a.preparationMode, preparationVersion: a.preparationVersion, contentLength: a.contentLength, sourceSnapshotRef: a.sourceSnapshotRef ?? null }); const expectedRef = `traverse-prepared-artifact:v1:sha256:${createHash("sha256").update(binding).digest("hex")}`; if (a.ref !== expectedRef)
255
+ throw new Error("prepared artifact ref does not match its identity binding."); return cloneJson(a); }
256
+ function validateArtifactState(input, artifact) { if (!artifact)
257
+ throw new Error("prepared artifact state requires prepared artifact."); const s = obj(input, "preparedArtifactState"); const status = s.status; if (status === "digest-mismatch") {
258
+ exact(s, ["status", "requestedRef", "canonicalRef", "actualDigest", "actualContentLength"], "preparedArtifactState");
259
+ digest(s.actualDigest, "actualDigest", false);
260
+ integer(s.actualContentLength, "actualContentLength");
261
+ }
262
+ else if (status === "invalid-artifact") {
263
+ exact(s, ["status", "reason", "canonicalRef"], "preparedArtifactState");
264
+ if (!ARTIFACT_INVALID_REASONS.has(s.reason))
265
+ throw new Error("prepared artifact invalid reason is invalid.");
266
+ }
267
+ else if (["available", "unavailable", "storage-error", "identity-mismatch"].includes(status))
268
+ exact(s, ["status", "requestedRef", "canonicalRef"], "preparedArtifactState");
269
+ else
270
+ throw new Error("prepared artifact state is invalid."); preparedReference(s.canonicalRef, "canonicalRef"); if (s.canonicalRef !== artifact.ref)
271
+ throw new Error("prepared artifact canonical ref mismatch."); if (s.requestedRef !== undefined) {
272
+ safeReference(s.requestedRef, "requestedRef");
273
+ if (status !== "identity-mismatch")
274
+ preparedReference(s.requestedRef, "requestedRef");
275
+ if (status === "identity-mismatch" ? s.requestedRef === s.canonicalRef : s.requestedRef !== s.canonicalRef)
276
+ throw new Error(`prepared artifact ${status} requestedRef relationship is invalid.`);
277
+ } return cloneJson(s); }
278
+ function validateOutcome(input, partial) { const o = obj(input, "outcome"); if (o.status === "success")
279
+ exact(o, ["status"], "outcome");
280
+ else if (o.status === "partial") {
281
+ exact(o, ["status", "reason"], "outcome");
282
+ if (!PARTIAL.has(o.reason))
283
+ throw new Error("partial reason is invalid.");
284
+ const p = obj(partial, "partial");
285
+ exact(p, ["reason", "completedChunks", "remainingChunks"], "partial", ["tokenOvershoot"]);
286
+ if (p.reason !== o.reason)
287
+ throw new Error("partial reason mismatch.");
288
+ integer(p.completedChunks, "completedChunks");
289
+ integer(p.remainingChunks, "remainingChunks");
290
+ if (p.tokenOvershoot !== undefined) {
291
+ integer(p.tokenOvershoot, "tokenOvershoot");
292
+ if (p.tokenOvershoot === 0)
293
+ throw new Error("tokenOvershoot must be positive.");
294
+ }
295
+ }
296
+ else if (o.status === "failure") {
297
+ exact(o, ["status", "category", "code"], "outcome");
298
+ if (!FAILURE_CATEGORIES.has(o.category))
299
+ throw new Error("failure category is invalid.");
300
+ stableIdentity(o.code, "failure code");
301
+ }
302
+ else
303
+ throw new Error("outcome status is invalid."); if (o.status !== "partial" && partial !== undefined)
304
+ throw new Error("partial requires partial outcome."); }
305
+ function validateWarning(v) { const w = obj(v, "warning"); exact(w, ["category", "code"], "warning"); if (!WARNING_CATEGORIES.has(w.category))
306
+ throw new Error("warning category invalid."); stableIdentity(w.code, "warning.code"); }
307
+ function validateFailure(v) { const f = obj(v, "providerFailure"); exact(f, ["provider", "kind", "retryable"], "providerFailure"); stableIdentity(f.provider, "failure.provider"); if (!FAILURE_KINDS.has(f.kind) || typeof f.retryable !== "boolean")
308
+ throw new Error("provider failure invalid."); }
309
+ function validateClaimTarget(v) { const t = obj(v, "claimTarget"); exact(t, ["subjectType", "subjectId", "facet", "claimType", "fieldOrBehavior", "impactLevel"], "claimTarget", ["claimId", "evidenceType", "evidenceMethod", "collectedBy", "derivedFrom"]); for (const key of ["subjectType", "subjectId", "facet", "claimType", "fieldOrBehavior"])
310
+ nonEmpty(t[key], `claimTarget.${key}`); if (!["low", "medium", "high", "critical"].includes(t.impactLevel))
311
+ throw new Error("claimTarget.impactLevel invalid."); for (const key of ["claimId", "evidenceType", "evidenceMethod", "collectedBy"])
312
+ optionalString(t[key], `claimTarget.${key}`); if (t.derivedFrom !== undefined)
313
+ array(t.derivedFrom, "claimTarget.derivedFrom").forEach((entry) => nonEmpty(entry, "claimTarget.derivedFrom entry")); }
314
+ function inferValueType(v) { if (Array.isArray(v))
315
+ return "array"; if (v === null || typeof v === "object")
316
+ return "object"; if (["string", "number", "boolean"].includes(typeof v))
317
+ return typeof v; return "string"; }
318
+ function obj(v, subject) { if (!v || typeof v !== "object" || Array.isArray(v))
319
+ throw new Error(`${subject} must be an object.`); return v; }
320
+ function array(v, subject) { if (!Array.isArray(v))
321
+ throw new Error(`${subject} must be an array.`); return v; }
322
+ function exact(v, required, subject, optional = []) { for (const k of required)
323
+ if (!Object.hasOwn(v, k))
324
+ throw new Error(`${subject}.${k} is required.`); for (const k of Object.keys(v))
325
+ if (![...required, ...optional].includes(k))
326
+ throw new Error(`${subject}.${k} is unexpected.`); }
327
+ function nonEmpty(v, subject) { if (typeof v !== "string" || !v.trim())
328
+ throw new Error(`${subject} must be a non-empty string.`); }
329
+ function wireNonEmpty(v, subject) { wellFormedString(v, subject); if (v.length === 0)
330
+ throw new Error(`${subject} must be non-empty.`); }
331
+ function wellFormedString(v, subject) { if (typeof v !== "string" || !isWellFormedUnicode(v))
332
+ throw new Error(`${subject} must be a well-formed string.`); }
333
+ function optionalString(v, subject) { if (v !== undefined)
334
+ nonEmpty(v, subject); }
335
+ function finite(v, subject, min, max) { if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max)
336
+ throw new Error(`${subject} is invalid.`); }
337
+ function integer(v, subject) { if (!Number.isSafeInteger(v) || v < 0)
338
+ throw new Error(`${subject} must be a non-negative safe integer.`); }
339
+ function digest(v, subject, prefixed = true) { if (typeof v !== "string" || !(prefixed ? /^sha256:[a-f0-9]{64}$/ : /^[a-f0-9]{64}$/).test(v))
340
+ throw new Error(`${subject} is invalid.`); }
341
+ function optionalDigest(v, subject) { if (v !== undefined)
342
+ digest(v, subject); }
343
+ function safeReference(v, subject) { wireNonEmpty(v, subject); if (referenceContainsAuthorization(v))
344
+ throw new Error(`${subject} contains authorization material.`); }
345
+ function referenceContainsAuthorization(value, depth = 0) { if (depth > 2 || /authorization\s*[:=]|bearer\s+[a-z0-9._~-]+/i.test(value))
346
+ return true; let parsed; try {
347
+ parsed = new URL(value);
348
+ }
349
+ catch {
350
+ return false;
351
+ } if (parsed.username || parsed.password)
352
+ return true; for (const [key, nested] of parsed.searchParams) {
353
+ if (/(?:^|[-_])(token|secret|password|passwd|api[-_]?key|authorization|signature|credential)(?:$|[-_])/i.test(key) || referenceContainsAuthorization(nested, depth + 1))
354
+ return true;
355
+ } return false; }
356
+ function stableIdentity(v, subject) { wireNonEmpty(v, subject); if (!STABLE_IDENTITY.test(v) || CREDENTIAL_IDENTITY.test(v) || referenceContainsAuthorization(v))
357
+ throw new Error(`${subject} must be a credential-free stable identity.`); }
358
+ function preparedReference(v, subject) { safeReference(v, subject); if (!/^traverse-prepared-artifact:v1:sha256:[a-f0-9]{64}$/.test(v))
359
+ throw new Error(`${subject} must be a prepared-artifact reference.`); }
360
+ function cloneJson(v) { return JSON.parse(JSON.stringify(v)); }
361
+ function identityHash(v) { return createHash("sha256").update(canonicalJson(v)).digest("hex").slice(0, 20); }
362
+ function jsonSafe(v, subject, seen = new Set()) { if (v === null || typeof v === "boolean")
363
+ return; if (typeof v === "string") {
364
+ if (!isWellFormedUnicode(v))
365
+ throw new Error(`${subject} contains ill-formed Unicode.`);
366
+ return;
367
+ } if (typeof v === "number") {
368
+ if (!Number.isFinite(v) || Object.is(v, -0))
369
+ throw new Error(`${subject} contains a non-lossless number.`);
370
+ return;
371
+ } if (Array.isArray(v)) {
372
+ if (seen.has(v))
373
+ throw new Error(`${subject} contains a cycle.`);
374
+ const keys = Reflect.ownKeys(v);
375
+ const expected = new Set(["length", ...Array.from({ length: v.length }, (_, index) => String(index))]);
376
+ for (const key of keys) {
377
+ if (typeof key === "symbol" || !expected.has(key))
378
+ throw new Error(`${subject} is sparse or has unexpected array properties.`);
379
+ const descriptor = Object.getOwnPropertyDescriptor(v, key);
380
+ if (!descriptor || !("value" in descriptor))
381
+ throw new Error(`${subject} has an array accessor property.`);
382
+ if (key !== "length" && !descriptor.enumerable)
383
+ throw new Error(`${subject} has a non-enumerable array item.`);
384
+ }
385
+ if (keys.length !== expected.size)
386
+ throw new Error(`${subject} contains a sparse array slot.`);
387
+ seen.add(v);
388
+ for (let i = 0; i < v.length; i++) {
389
+ const descriptor = Object.getOwnPropertyDescriptor(v, String(i));
390
+ jsonSafe(descriptor.value, `${subject}[${i}]`, seen);
391
+ }
392
+ seen.delete(v);
393
+ return;
394
+ } if (typeof v !== "object" || Object.getPrototypeOf(v) !== Object.prototype)
395
+ throw new Error(`${subject} contains a non-JSON value.`); if (seen.has(v))
396
+ throw new Error(`${subject} contains a cycle.`); seen.add(v); for (const key of Reflect.ownKeys(v)) {
397
+ if (typeof key !== "string")
398
+ throw new Error(`${subject} contains a symbol key.`);
399
+ const d = Object.getOwnPropertyDescriptor(v, key);
400
+ if (!d?.enumerable || !("value" in d) || !isWellFormedUnicode(key))
401
+ throw new Error(`${subject}.${key} is not a lossless JSON property.`);
402
+ jsonSafe(d.value, `${subject}.${key}`, seen);
403
+ } seen.delete(v); }
404
+ function isWellFormedUnicode(value) { for (let index = 0; index < value.length; index += 1) {
405
+ const unit = value.charCodeAt(index);
406
+ if (unit >= 0xd800 && unit <= 0xdbff) {
407
+ if (index + 1 >= value.length)
408
+ return false;
409
+ const next = value.charCodeAt(index + 1);
410
+ if (next < 0xdc00 || next > 0xdfff)
411
+ return false;
412
+ index += 1;
413
+ }
414
+ else if (unit >= 0xdc00 && unit <= 0xdfff)
415
+ return false;
416
+ } return true; }
417
+ const RAW_SOURCE_KINDS = new Set(["uploaded-document", "web-page", "api-record", "manual-entry", "policy-standard", "inquiry-question", "agent-utterance", "system-schema"]);
418
+ const VALUE_TYPES = new Set(["string", "number", "boolean", "date", "enum", "array", "object"]);
419
+ const PARTIAL = new Set(["cancelled", "max-provider-calls", "max-total-tokens"]);
420
+ const FAILURE_CATEGORIES = new Set(["invalid-config", "invalid-task", "preparation", "provider", "unexpected"]);
421
+ const WARNING_CATEGORIES = new Set(["provider", "normalization", "preparation", "limit", "storage", "content", "other"]);
422
+ const FAILURE_KINDS = new Set(["authentication", "rate-limit", "timeout", "invalid-request", "unavailable", "unknown"]);
423
+ const PREPARATION_MODES = new Set(["text", "markdown", "transcript", "pdf-text", "image-ocr"]);
424
+ const ARTIFACT_INVALID_REASONS = new Set(["not-an-object", "invalid-format", "invalid-version", "invalid-digest", "invalid-ref", "invalid-preparation-mode", "invalid-preparation-version", "invalid-content-length", "invalid-source-snapshot-ref", "ill-formed-unicode", "invalid-resolved-text"]);
425
+ const STABLE_IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:@/+~-]{0,255}$/;
426
+ const CREDENTIAL_IDENTITY = /^(?:gh[pousr]_|sk-[A-Za-z0-9]|AKIA[A-Z0-9]|ASIA[A-Z0-9]|eyJ[A-Za-z0-9_-]+\.eyJ)/;
427
+ const RUN_ID = /^traverse-extraction-run:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
@@ -1,6 +1,8 @@
1
1
  export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, EscalationDimension, EscalationRecord, Extraction, Interpretation, LocatorScheme, ProvenanceResolution, RawSource, RawSourceKind, ReviewAuthorizing, ReviewAuthorizingAuthorizedAction, ReviewAuthorizingExchange, ReviewAuthorizingExplicitStatement, ReviewAuthorizingKind, ReviewOutcome, ReviewResolution, ReviewStatus, SurveyInput, } from "./types.js";
2
2
  export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
3
3
  export { reviewResourceApiVersion } from "./review-resource.js";
4
+ export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeResolutionIdentity, exportExtractionEnvelopeImport, extractionEnvelopeImportApiVersion, importExtractionEnvelope, portableExtractionResultFormat, portableExtractionResultVersion, reimportExtractionEnvelope, } from "./extraction-envelope.js";
5
+ export type { ExtractionEnvelopeImport, ExtractionEnvelopeImportDiagnostic, ExtractionEnvelopeImportOptions, ExtractionEnvelopeImportResult, ExtractionEnvelopeResolutionIdentity, PortableExtractionOccurrence, PortableExtractionProposal, PortableExtractionResultEnvelope, PortablePreparedArtifactState, } from "./extraction-envelope.js";
4
6
  export type { CandidateRole, ClaimTargetHint, ProducerPolicy, ExtractionReference, ResourceEnvelope, ResourceMetadata, ReviewActor, ReviewCandidate, ReviewDecision, ReviewDecisionMode, ReviewDecisionSpec, ReviewDecisionStatus, ReviewItem, ReviewItemSpec, ReviewItemStatus, ReviewLocator, ReviewResource, ReviewResourceApiVersion, ReviewResourceKind, ReviewSession, ReviewSessionEvent, ReviewSessionEventSpec, ReviewSessionEventStatus, ReviewSessionEventType, ReviewSessionSpec, ReviewSessionStatus, ReviewValueDescriptor, ReviewValueType, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
5
7
  export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
6
8
  export type { CandidateReviewRecordInput, SurveyClaimRecord, SurveyInputBuilderArgs, SurveyObservationInput, } from "./builder.js";
package/dist/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
2
2
  export { reviewResourceApiVersion } from "./review-resource.js";
3
+ export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeResolutionIdentity, exportExtractionEnvelopeImport, extractionEnvelopeImportApiVersion, importExtractionEnvelope, portableExtractionResultFormat, portableExtractionResultVersion, reimportExtractionEnvelope, } from "./extraction-envelope.js";
3
4
  export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
4
5
  export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
5
6
  export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Producer-side source, extraction, candidate, and review contracts for projecting verified claims into Surface.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -87,6 +87,7 @@
87
87
  },
88
88
  "devDependencies": {
89
89
  "@anthropic-ai/sdk": "^0.54.0",
90
+ "@kontourai/traverse": "^0.19.0",
90
91
  "@kontourai/ui": "^1.1.0",
91
92
  "@playwright/test": "^1.60.0",
92
93
  "@types/node": "^25.6.0",