@kontourai/survey 1.15.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.
- package/README.md +19 -1
- package/dist/src/calibration.d.ts +1 -0
- package/dist/src/calibration.js +5 -0
- package/dist/src/extraction-envelope.d.ts +164 -0
- package/dist/src/extraction-envelope.js +427 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +1 -0
- package/dist/src/learning-projections.d.ts +2 -2
- package/dist/src/learning-projections.js +25 -0
- package/dist/src/mcp/review-mcp.js +48 -10
- package/dist/src/producer-discipline.d.ts +13 -0
- package/dist/src/producer-discipline.js +41 -0
- package/dist/src/review-proof.d.ts +22 -8
- package/dist/src/review-proof.js +56 -5
- package/dist/src/review-resource.d.ts +6 -0
- package/dist/src/review-workbench/producer-decision-mode.js +2 -0
- package/dist/src/review-workbench/review-queue-session.d.ts +11 -2
- package/dist/src/review-workbench/review-queue-session.js +58 -2
- package/dist/src/review-workbench/review-session-replay.d.ts +1 -1
- package/dist/src/review-workbench/review-session-replay.js +19 -0
- package/dist/src/review-workbench/review-surface-preview.js +1 -1
- package/dist/src/review-workbench/review-workbench-css.generated.js +8 -0
- package/dist/src/review-workbench/review-workbench.css +8 -0
- package/dist/src/review-workbench/review-workbench.js +68 -3
- package/dist/src/review-workbench/review-workbench.standalone.css +8 -0
- package/dist/src/to-surface.js +31 -14
- package/dist/src/types.d.ts +8 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -105,6 +105,24 @@ Keep producer operational state outside Survey. Queue status, reviewer form stat
|
|
|
105
105
|
|
|
106
106
|
When you build an `authorized-action` authorizing block outside the workbench, pair `buildAuthorizedActionAuthorizing` with `buildPromptRef({ module, component, version?, scheme? })` — `buildPromptRef` formats a well-formed, versioned `promptRef` (bare `"review-workbench/decision-card@v1"` or scheme-prefixed `"survey://<module>/<component>@v1"`) that `buildAuthorizedActionAuthorizing` accepts directly, instead of hand-formatting the string.
|
|
107
107
|
|
|
108
|
+
## Review lifecycle
|
|
109
|
+
|
|
110
|
+
`ReviewOutcome.resolution` optionally records the terminal result of a review
|
|
111
|
+
round while preserving compatibility with outcomes that infer their result from
|
|
112
|
+
`status`. The explicit `could_not_confirm` resolution requires a non-empty
|
|
113
|
+
`resolutionReason`; `attemptEvidenceIds` may record evidence of what the reviewer
|
|
114
|
+
tried. It is valid only with an unchanged `proposed` or `assumed` status.
|
|
115
|
+
Other explicit resolutions must also agree with status: accepted is
|
|
116
|
+
verified/assumed, rejected is rejected, and held is any non-rejected retained
|
|
117
|
+
posture.
|
|
118
|
+
|
|
119
|
+
Could-not-confirm is terminal for the review round, but it is not rejection,
|
|
120
|
+
verification, or escalation. Surface projection stays quiet: the claim keeps its
|
|
121
|
+
pre-review status, no attempt evidence is projected, and no review-derived
|
|
122
|
+
freshness, validity, gap, or verification posture is added. The outcome remains
|
|
123
|
+
available in Survey review records, canonical review proof v3, and the distinct
|
|
124
|
+
`learning.could-not-confirm` producer-learning signal.
|
|
125
|
+
|
|
108
126
|
## Review Workbench embed
|
|
109
127
|
|
|
110
128
|
**Web component** (shadow DOM, no framework required):
|
|
@@ -181,7 +199,7 @@ Drive review-queue decisions from an MCP agent (Claude Desktop, Cursor, or any M
|
|
|
181
199
|
npx survey-review-mcp --session path/to/session.json
|
|
182
200
|
```
|
|
183
201
|
|
|
184
|
-
Three tools: `survey_review_queue` (queue state), `survey_review_item` (item detail), and `survey_review_decide` (record a decision). Each queue and item call includes an embedded, fully self-contained review card with Accept / Hold / Reject
|
|
202
|
+
Three tools: `survey_review_queue` (queue state), `survey_review_item` (item detail), and `survey_review_decide` (record a decision). Each queue and item call includes an embedded, fully self-contained review card with Accept / Hold / Reject / Could not confirm actions. See [docs/review-mcp.md](docs/review-mcp.md).
|
|
185
203
|
|
|
186
204
|
At viewports ≤ 980 px, the queue panel becomes a slide-in drawer with a compact progress bar. At narrow container widths, `cqi`-based type scaling keeps candidate values from overflowing at 360 px. CSS custom properties (`--k-*`) inherit through the shadow boundary so the host can theme either mode without forking styles.
|
|
187
205
|
|
|
@@ -151,6 +151,7 @@ export interface CalibrationMetrics {
|
|
|
151
151
|
* label. A sample is skipped when it carries no human label or no prediction:
|
|
152
152
|
*
|
|
153
153
|
* - status "proposed" (not yet reviewed);
|
|
154
|
+
* - resolution "could_not_confirm" (no human correctness label);
|
|
154
155
|
* - a machine auto-accept, unless `includeAutoAccepted` is set;
|
|
155
156
|
* - no `selectedCandidateId`, or the selected candidate / its confidence is
|
|
156
157
|
* missing or non-finite (no prediction to calibrate).
|
package/dist/src/calibration.js
CHANGED
|
@@ -37,6 +37,7 @@ const DEFAULT_MIN_BIN_SAMPLES = 1;
|
|
|
37
37
|
* label. A sample is skipped when it carries no human label or no prediction:
|
|
38
38
|
*
|
|
39
39
|
* - status "proposed" (not yet reviewed);
|
|
40
|
+
* - resolution "could_not_confirm" (no human correctness label);
|
|
40
41
|
* - a machine auto-accept, unless `includeAutoAccepted` is set;
|
|
41
42
|
* - no `selectedCandidateId`, or the selected candidate / its confidence is
|
|
42
43
|
* missing or non-finite (no prediction to calibrate).
|
|
@@ -66,6 +67,10 @@ export function deriveCalibration(input, options = {}) {
|
|
|
66
67
|
const samples = [];
|
|
67
68
|
let skippedCount = 0;
|
|
68
69
|
for (const outcome of input.reviewOutcomes) {
|
|
70
|
+
if (outcome.resolution === "could_not_confirm") {
|
|
71
|
+
skippedCount++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
69
74
|
if (cutoff !== undefined) {
|
|
70
75
|
const t = outcome.reviewedAt ? Date.parse(outcome.reviewedAt) : NaN;
|
|
71
76
|
if (isNaN(t) || t < cutoff) {
|
|
@@ -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}$/;
|