@kontourai/survey 4.0.0 → 6.0.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/dist/examples/review-workbench/server-apply-consumer.js +5 -0
- package/dist/src/canonical-reviewed-trust-input.js +73 -34
- package/dist/src/extraction-envelope.d.ts +125 -3
- package/dist/src/extraction-envelope.js +332 -25
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/mcp/review-mcp.js +62 -25
- package/dist/src/review-workbench/audit-rows.d.ts +1 -1
- package/dist/src/review-workbench/audit-rows.js +1 -0
- package/dist/src/review-workbench/extraction-inspector.d.ts +31 -13
- package/dist/src/review-workbench/extraction-inspector.js +77 -47
- package/dist/src/review-workbench/queue-binding.js +1 -1
- package/dist/src/review-workbench/review-presentation.d.ts +26 -1
- package/dist/src/review-workbench/review-presentation.js +75 -1
- package/dist/src/review-workbench/review-queue-session.d.ts +19 -5
- package/dist/src/review-workbench/review-queue-session.js +46 -13
- package/dist/src/review-workbench/review-session-replay.js +5 -3
- package/dist/src/review-workbench/review-workbench-css.generated.js +10 -1
- package/dist/src/review-workbench/review-workbench.css +10 -1
- package/dist/src/review-workbench/review-workbench.d.ts +14 -7
- package/dist/src/review-workbench/review-workbench.js +111 -20
- package/dist/src/review-workbench/review-workbench.standalone.css +10 -1
- package/dist/src/surface-reviewed-extraction.js +4 -0
- package/dist/src/to-surface.js +116 -0
- package/dist/src/types.d.ts +5 -1
- package/package.json +1 -1
|
@@ -26,21 +26,76 @@ export function importExtractionEnvelope(serialized, options) {
|
|
|
26
26
|
const producerNamespace = options.producerNamespace ?? envelope.result.provider;
|
|
27
27
|
nonEmpty(name, "Extraction envelope import name");
|
|
28
28
|
nonEmpty(producerNamespace, "Extraction envelope producer namespace");
|
|
29
|
-
const
|
|
29
|
+
const verification = options.artifact === undefined
|
|
30
|
+
? { provenance: "unverified", diagnostics: [] }
|
|
31
|
+
: verifyAgainstArtifact(envelope, options.artifact);
|
|
32
|
+
const diagnostics = [...diagnosticsFor(envelope), ...verification.diagnostics];
|
|
30
33
|
const record = {
|
|
31
34
|
apiVersion: extractionEnvelopeImportApiVersion,
|
|
32
35
|
kind: "ExtractionEnvelopeImport",
|
|
33
36
|
metadata: { name, producerNamespace },
|
|
34
37
|
spec: { envelope, sourceKind: options.sourceKind, claimTargets: cloneJson(claimTargets) },
|
|
35
|
-
status: { state: diagnostics.length
|
|
38
|
+
status: { state: stateFor(diagnostics, envelope.result.proposals.length), diagnostics, provenance: verification.provenance },
|
|
36
39
|
};
|
|
37
40
|
return { record, reviewItems: buildReviewItemsFromExtractionEnvelopeImport(record) };
|
|
38
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* One ReviewItem per claim slot: every proposal whose claim target names the
|
|
44
|
+
* same claim (subject, facet, claim type, field or behavior, and claim id when
|
|
45
|
+
* one is set) at the same `pathIndices` is one candidate set, so two values for
|
|
46
|
+
* one claim can never be accepted as two separate verified claims. The slot is
|
|
47
|
+
* the claim the proposal would project to, not the upstream `fieldPath` (two
|
|
48
|
+
* field paths mapped to one claim are one slot) and not the value.
|
|
49
|
+
*
|
|
50
|
+
* Within a slot there is one candidate per distinct canonical value; further
|
|
51
|
+
* proposals of an already-seen value are recorded on that candidate as
|
|
52
|
+
* `sameValueProposals`. Two or more distinct values make the set `conflict`.
|
|
53
|
+
* Distinct `pathIndices` (array items) or distinct claim ids are separate slots,
|
|
54
|
+
* which is how a producer declares a multi-valued field.
|
|
55
|
+
*/
|
|
39
56
|
export function buildReviewItemsFromExtractionEnvelopeImport(record) {
|
|
40
57
|
validateImport(record);
|
|
41
58
|
if (record.status.state !== "grounded")
|
|
42
59
|
return [];
|
|
43
|
-
return record.
|
|
60
|
+
return claimSlotGroups(record).map((group) => buildReviewItem(record, group));
|
|
61
|
+
}
|
|
62
|
+
function claimSlotGroups(record) {
|
|
63
|
+
const groups = new Map();
|
|
64
|
+
const excluded = excerptMismatchProposalIndices(record);
|
|
65
|
+
const slotOf = (proposal, index) => {
|
|
66
|
+
const target = record.spec.claimTargets[index];
|
|
67
|
+
return {
|
|
68
|
+
subjectType: target.subjectType, subjectId: target.subjectId, facet: target.facet, claimType: target.claimType,
|
|
69
|
+
fieldOrBehavior: target.fieldOrBehavior, claimId: target.claimId ?? null, pathIndices: proposal.pathIndices ?? null,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
record.spec.envelope.result.proposals.forEach((proposal, index) => {
|
|
73
|
+
if (excluded.has(index))
|
|
74
|
+
return;
|
|
75
|
+
const target = record.spec.claimTargets[index];
|
|
76
|
+
const slot = slotOf(proposal, index);
|
|
77
|
+
const key = canonicalJson(slot);
|
|
78
|
+
const group = groups.get(key);
|
|
79
|
+
if (!group) {
|
|
80
|
+
groups.set(key, { slot, members: [{ proposal, index }], excluded: [] });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const first = group.members[0].index;
|
|
84
|
+
// One claim cannot carry two impact levels or evidence descriptions.
|
|
85
|
+
if (canonicalJson(record.spec.claimTargets[first]) !== canonicalJson(target)) {
|
|
86
|
+
throw new Error(`Proposals ${first} and ${index} map to the same claim with different claim targets.`);
|
|
87
|
+
}
|
|
88
|
+
group.members.push({ proposal, index });
|
|
89
|
+
});
|
|
90
|
+
// An excluded proposal is unverifiable, not disproven: the slot it would have
|
|
91
|
+
// joined keeps a record of it, so a rival value cannot vanish without trace.
|
|
92
|
+
// A slot whose every proposal was excluded has no item; the import
|
|
93
|
+
// diagnostics are then the only record, as for any excluded proposal.
|
|
94
|
+
record.spec.envelope.result.proposals.forEach((proposal, index) => {
|
|
95
|
+
if (excluded.has(index))
|
|
96
|
+
groups.get(canonicalJson(slotOf(proposal, index)))?.excluded.push({ proposal, index });
|
|
97
|
+
});
|
|
98
|
+
return [...groups.values()];
|
|
44
99
|
}
|
|
45
100
|
export function exportExtractionEnvelopeImport(record) {
|
|
46
101
|
validateImport(record);
|
|
@@ -66,15 +121,57 @@ export function createExtractionEnvelopeResolutionIdentity(record, proposalIndex
|
|
|
66
121
|
const nonce = globalThis.crypto.randomUUID();
|
|
67
122
|
return { evidenceId: `survey.extraction.${base}.resolution-evidence.${nonce}`, eventId: `survey.extraction.${base}.resolution-event.${nonce}` };
|
|
68
123
|
}
|
|
69
|
-
function buildReviewItem(record,
|
|
124
|
+
function buildReviewItem(record, group) {
|
|
125
|
+
const envelope = record.spec.envelope;
|
|
126
|
+
const byValue = new Map();
|
|
127
|
+
for (const member of group.members) {
|
|
128
|
+
const key = canonicalJson(member.proposal.candidateValue);
|
|
129
|
+
byValue.set(key, [...(byValue.get(key) ?? []), member]);
|
|
130
|
+
}
|
|
131
|
+
const candidates = [...byValue.values()].map((members) => buildCandidate(record, members));
|
|
132
|
+
const lead = group.members[0].proposal;
|
|
133
|
+
const valueType = lead.valueType ?? inferValueType(lead.candidateValue);
|
|
134
|
+
const identity = identityHash({ producerNamespace: record.metadata.producerNamespace, importName: record.metadata.name, source: envelope.source,
|
|
135
|
+
preparedArtifact: envelope.result.preparedArtifact, pdfLayout: envelope.result.pdfLayout, runId: envelope.result.runId, claimSlot: group.slot });
|
|
136
|
+
return {
|
|
137
|
+
apiVersion: reviewResourceApiVersion, kind: "ReviewItem",
|
|
138
|
+
metadata: { name: `extraction-envelope.${identity}`, producer: { "survey.kontourai.io/extraction-envelope": {
|
|
139
|
+
importName: record.metadata.name,
|
|
140
|
+
evidenceId: `survey.extraction.${identityHash(evidenceInputs(record, lead))}.source-evidence`,
|
|
141
|
+
proposalIndices: group.members.map((member) => member.index),
|
|
142
|
+
source: envelope.source,
|
|
143
|
+
...(envelope.result.preparedArtifact ? { preparedArtifact: envelope.result.preparedArtifact } : {}),
|
|
144
|
+
// Present only on verified imports, so an unverified import's items stay
|
|
145
|
+
// byte-identical to the ones earlier releases produced.
|
|
146
|
+
...(record.status.provenance === "verified" ? { excerptVerification: "verified" } : {}),
|
|
147
|
+
...(group.excluded.length ? { excludedProposals: group.excluded.map(({ proposal, index }) => ({
|
|
148
|
+
proposalIndex: index, value: proposal.candidateValue, locator: proposal.provenance.locator,
|
|
149
|
+
excerpt: proposal.provenance.excerpt, reason: "excerpt-mismatch",
|
|
150
|
+
})) } : {}),
|
|
151
|
+
} } },
|
|
152
|
+
spec: {
|
|
153
|
+
target: lead.fieldPath, candidates,
|
|
154
|
+
candidateSetStatus: candidates.length > 1 ? "conflict" : "needs-review",
|
|
155
|
+
valueDescriptor: { type: valueType }, editable: false,
|
|
156
|
+
},
|
|
157
|
+
status: { observedCandidateCount: candidates.length },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** One candidate for one distinct value; `members` are that value's proposals in envelope order. */
|
|
161
|
+
function buildCandidate(record, members) {
|
|
70
162
|
const envelope = record.spec.envelope;
|
|
71
|
-
const
|
|
163
|
+
const { proposal, index } = members[0];
|
|
164
|
+
const others = members.slice(1);
|
|
165
|
+
// A candidate commits to every proposal it stands for; a single-proposal
|
|
166
|
+
// candidate keeps exactly the identity it had before grouping.
|
|
167
|
+
const identity = identityHash(others.length === 0 ? identityInputs(record, proposal, index)
|
|
168
|
+
: { ...identityInputs(record, proposal, index), sameValueProposals: others.map((other) => ({ proposalIndex: other.index, proposal: other.proposal })) });
|
|
72
169
|
const evidence = identityHash(evidenceInputs(record, proposal));
|
|
73
170
|
const target = record.spec.claimTargets[index];
|
|
74
171
|
const valueType = proposal.valueType ?? inferValueType(proposal.candidateValue);
|
|
75
|
-
|
|
172
|
+
return {
|
|
76
173
|
id: `extraction-envelope.${identity}.proposed`, role: "proposed", value: proposal.candidateValue,
|
|
77
|
-
confidence: proposal.confidence,
|
|
174
|
+
...(proposal.confidence !== undefined ? { confidence: proposal.confidence } : {}),
|
|
78
175
|
source: {
|
|
79
176
|
sourceRef: envelope.source.ref,
|
|
80
177
|
sourceId: envelope.source.snapshotRef ?? envelope.source.ref,
|
|
@@ -87,8 +184,9 @@ function buildReviewItem(record, proposal, index) {
|
|
|
87
184
|
extraction: {
|
|
88
185
|
extractionId: `extraction-envelope.${identity}`,
|
|
89
186
|
target: proposal.fieldPath,
|
|
90
|
-
confidence: proposal.confidence,
|
|
187
|
+
...(proposal.confidence !== undefined ? { confidence: proposal.confidence } : {}),
|
|
91
188
|
extractor: proposal.extractor,
|
|
189
|
+
extractedAt: envelope.result.extractedAt,
|
|
92
190
|
// The proposal's own served model when recorded; in a multi-chunk run
|
|
93
191
|
// `result.model` names only the last chunk's model.
|
|
94
192
|
...(proposal.producedBy ? { model: proposal.producedBy.model } : envelope.result.model ? { model: envelope.result.model } : {}),
|
|
@@ -105,22 +203,21 @@ function buildReviewItem(record, proposal, index) {
|
|
|
105
203
|
...(proposal.producedBy ? { producedBy: proposal.producedBy } : {}),
|
|
106
204
|
...(proposal.evidenceMatch ? { evidenceMatch: proposal.evidenceMatch } : {}),
|
|
107
205
|
occurrence: proposal.provenance.occurrence,
|
|
206
|
+
...(others.length ? { sameValueProposals: others.map((other) => ({
|
|
207
|
+
proposalIndex: other.index,
|
|
208
|
+
evidenceId: `survey.extraction.${identityHash(evidenceInputs(record, other.proposal))}.source-evidence`,
|
|
209
|
+
locator: other.proposal.provenance.locator,
|
|
210
|
+
excerpt: other.proposal.provenance.excerpt,
|
|
211
|
+
})) } : {}),
|
|
108
212
|
attempt: { id: envelope.result.runId, providerCalls: envelope.result.providerCalls },
|
|
109
213
|
...(envelope.result.warningClassifications ? { warnings: envelope.result.warningClassifications } : {}),
|
|
110
214
|
outcome: envelope.result.outcome,
|
|
215
|
+
// A partial run may have left this field's other values unread: the
|
|
216
|
+
// reason and the per-chunk coverage travel with every candidate.
|
|
217
|
+
...(envelope.result.partial ? { partial: envelope.result.partial } : {}),
|
|
218
|
+
...(envelope.result.coverage ? { coverage: envelope.result.coverage } : {}),
|
|
111
219
|
} },
|
|
112
220
|
};
|
|
113
|
-
return {
|
|
114
|
-
apiVersion: reviewResourceApiVersion, kind: "ReviewItem",
|
|
115
|
-
metadata: { name: `extraction-envelope.${identity}`, producer: { "survey.kontourai.io/extraction-envelope": {
|
|
116
|
-
importName: record.metadata.name,
|
|
117
|
-
evidenceId: `survey.extraction.${evidence}.source-evidence`,
|
|
118
|
-
source: envelope.source,
|
|
119
|
-
...(envelope.result.preparedArtifact ? { preparedArtifact: envelope.result.preparedArtifact } : {}),
|
|
120
|
-
} } },
|
|
121
|
-
spec: { target: proposal.fieldPath, candidates: [candidate], candidateSetStatus: "needs-review", valueDescriptor: { type: valueType }, editable: false },
|
|
122
|
-
status: { observedCandidateCount: 1 },
|
|
123
|
-
};
|
|
124
221
|
}
|
|
125
222
|
function identityInputs(record, proposal, index) {
|
|
126
223
|
return { producerNamespace: record.metadata.producerNamespace, importName: record.metadata.name, source: record.spec.envelope.source,
|
|
@@ -135,7 +232,150 @@ function evidenceInputs(record, proposal) {
|
|
|
135
232
|
extractedAt: record.spec.envelope.result.extractedAt,
|
|
136
233
|
provenance: proposal.provenance };
|
|
137
234
|
}
|
|
235
|
+
/** Proposals the import excluded because their span did not match their excerpt. */
|
|
236
|
+
export function excerptMismatchProposalIndices(record) {
|
|
237
|
+
return new Set(record.status.diagnostics.flatMap((diagnostic) => diagnostic.kind === "excerpt-mismatch" ? [diagnostic.proposalIndex] : []));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* An import is grounded when nothing but excerpt mismatches was diagnosed and
|
|
241
|
+
* at least one proposal survived them. Every proposal excluded must not read as
|
|
242
|
+
* a complete run that found nothing.
|
|
243
|
+
*/
|
|
244
|
+
function stateFor(diagnostics, proposalCount) {
|
|
245
|
+
const mismatches = diagnostics.filter((diagnostic) => diagnostic.kind === "excerpt-mismatch").length;
|
|
246
|
+
if (mismatches !== diagnostics.length)
|
|
247
|
+
return "unresolved";
|
|
248
|
+
return mismatches > 0 && mismatches === proposalCount ? "unresolved" : "grounded";
|
|
249
|
+
}
|
|
250
|
+
/** Validate the caller-resolved artifact shape; resolver detail beyond the code is refused. */
|
|
251
|
+
export function assertResolvedExtractionArtifact(artifact) {
|
|
252
|
+
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact))
|
|
253
|
+
throw new Error("Invalid resolved extraction artifact.");
|
|
254
|
+
const a = artifact;
|
|
255
|
+
const keys = Object.keys(a).sort();
|
|
256
|
+
if (a.status === "available") {
|
|
257
|
+
if (keys.join(",") !== "actualDigest,status,text" || typeof a.text !== "string" || typeof a.actualDigest !== "string" || !/^[a-f0-9]{64}$/.test(a.actualDigest))
|
|
258
|
+
throw new Error("Invalid available extraction artifact.");
|
|
259
|
+
}
|
|
260
|
+
else if (a.status === "digest-mismatch") {
|
|
261
|
+
if (keys.join(",") !== "actualDigest,status" || typeof a.actualDigest !== "string" || !/^[a-f0-9]{64}$/.test(a.actualDigest))
|
|
262
|
+
throw new Error("Invalid digest-mismatch extraction artifact.");
|
|
263
|
+
}
|
|
264
|
+
else if (a.status === "unavailable") {
|
|
265
|
+
if (keys.join(",") !== "code,status" || !ARTIFACT_UNAVAILABLE_CODES.has(a.code))
|
|
266
|
+
throw new Error("Invalid unavailable extraction artifact.");
|
|
267
|
+
}
|
|
268
|
+
else
|
|
269
|
+
throw new Error("Invalid resolved extraction artifact status.");
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Checks resolved text against the envelope's prepared-artifact identity: the
|
|
273
|
+
* caller's reported digest, the digest of the text itself, and the declared
|
|
274
|
+
* length. Shared by import-time verification and the source inspector.
|
|
275
|
+
*/
|
|
276
|
+
export function checkPreparedArtifact(prepared, artifact) {
|
|
277
|
+
if (artifact.status === "unavailable")
|
|
278
|
+
return { status: "unavailable", code: artifact.code };
|
|
279
|
+
if (artifact.status === "digest-mismatch")
|
|
280
|
+
return { status: "digest-mismatch", actualDigest: artifact.actualDigest };
|
|
281
|
+
const textDigest = sha256Hex(artifact.text);
|
|
282
|
+
if (!prepared || artifact.actualDigest !== prepared.digest || textDigest !== artifact.actualDigest)
|
|
283
|
+
return { status: "digest-mismatch", actualDigest: textDigest };
|
|
284
|
+
if (artifact.text.length !== prepared.contentLength)
|
|
285
|
+
return { status: "wrong-length" };
|
|
286
|
+
return { status: "aligned", text: artifact.text };
|
|
287
|
+
}
|
|
288
|
+
/** Whether the verified prepared text at a proposal's `chars:` span is exactly its excerpt. */
|
|
289
|
+
export function proposalSpanMatches(text, proposal) {
|
|
290
|
+
const match = /^chars:(\d+)-(\d+)$/.exec(proposal.provenance.locator);
|
|
291
|
+
if (!match)
|
|
292
|
+
return false;
|
|
293
|
+
return text.slice(Number(match[1]), Number(match[2])) === proposal.provenance.excerpt;
|
|
294
|
+
}
|
|
295
|
+
function verifyAgainstArtifact(envelope, artifact) {
|
|
296
|
+
assertResolvedExtractionArtifact(artifact);
|
|
297
|
+
const prepared = envelope.result.preparedArtifact;
|
|
298
|
+
if (!prepared)
|
|
299
|
+
throw new Error("The artifact option requires result.preparedArtifact to verify against.");
|
|
300
|
+
const check = checkPreparedArtifact(prepared, artifact);
|
|
301
|
+
if (check.status === "unavailable")
|
|
302
|
+
return { provenance: "unverified", diagnostics: [unreadableArtifactDiagnostic(prepared.ref, check.code)] };
|
|
303
|
+
if (check.status === "wrong-length")
|
|
304
|
+
return { provenance: "unverified", diagnostics: [wrongLengthDiagnostic(prepared.ref)] };
|
|
305
|
+
if (check.status === "digest-mismatch")
|
|
306
|
+
return { provenance: "unverified", diagnostics: [suppliedDigestMismatchDiagnostic(prepared, check.actualDigest)] };
|
|
307
|
+
const diagnostics = [];
|
|
308
|
+
envelope.result.proposals.forEach((proposal, index) => {
|
|
309
|
+
if (!proposalSpanMatches(check.text, proposal))
|
|
310
|
+
diagnostics.push(excerptMismatchDiagnostic(index, proposal));
|
|
311
|
+
});
|
|
312
|
+
return { provenance: "verified", diagnostics };
|
|
313
|
+
}
|
|
314
|
+
function unreadableArtifactDiagnostic(artifactRef, code) {
|
|
315
|
+
const status = code === "storage-error" ? "storage-error" : code === "invalid-artifact" ? "invalid-artifact" : "unavailable";
|
|
316
|
+
return { kind: "artifact-unavailable", status, artifactRef, code, message: `Prepared artifact could not be read to verify excerpts (${code}); no candidate is grounded.` };
|
|
317
|
+
}
|
|
318
|
+
function wrongLengthDiagnostic(artifactRef) {
|
|
319
|
+
return { kind: "artifact-unavailable", status: "invalid-artifact", artifactRef, message: "Prepared artifact text does not have the declared content length; no candidate is grounded." };
|
|
320
|
+
}
|
|
321
|
+
function suppliedDigestMismatchDiagnostic(prepared, actualDigest) {
|
|
322
|
+
return { kind: "digest-mismatch", artifactRef: prepared.ref, expectedDigest: prepared.digest, actualDigest,
|
|
323
|
+
message: "Prepared artifact text supplied at import does not match the extraction artifact digest; no candidate is grounded." };
|
|
324
|
+
}
|
|
325
|
+
function excerptMismatchDiagnostic(index, proposal) {
|
|
326
|
+
return { kind: "excerpt-mismatch", proposalIndex: index, locator: proposal.provenance.locator,
|
|
327
|
+
message: `Proposal ${index} (${proposal.fieldPath}): the prepared text at ${proposal.provenance.locator} is not its excerpt, so it produced no review item.` };
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* The import-time verification diagnostics a record may carry after the
|
|
331
|
+
* envelope-derived ones. They cannot be recomputed without the artifact text,
|
|
332
|
+
* so a reloaded record is checked for being one the import could have
|
|
333
|
+
* written: each diagnostic is rebuilt from its own fields and must match.
|
|
334
|
+
*/
|
|
335
|
+
function verificationDiagnosticsAreCoherent(envelope, provenance, diagnostics) {
|
|
336
|
+
if (provenance === undefined)
|
|
337
|
+
return diagnostics.length === 0;
|
|
338
|
+
const prepared = envelope.result.preparedArtifact;
|
|
339
|
+
const rebuild = (value) => {
|
|
340
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || !prepared)
|
|
341
|
+
return undefined;
|
|
342
|
+
const d = value;
|
|
343
|
+
if (provenance === "verified") {
|
|
344
|
+
const proposal = Number.isSafeInteger(d.proposalIndex) ? envelope.result.proposals[d.proposalIndex] : undefined;
|
|
345
|
+
return d.kind === "excerpt-mismatch" && proposal ? excerptMismatchDiagnostic(d.proposalIndex, proposal) : undefined;
|
|
346
|
+
}
|
|
347
|
+
if (d.kind === "artifact-unavailable") {
|
|
348
|
+
if (d.code === undefined)
|
|
349
|
+
return wrongLengthDiagnostic(prepared.ref);
|
|
350
|
+
return ARTIFACT_UNAVAILABLE_CODES.has(d.code) ? unreadableArtifactDiagnostic(prepared.ref, d.code) : undefined;
|
|
351
|
+
}
|
|
352
|
+
if (d.kind === "digest-mismatch" && typeof d.actualDigest === "string" && /^[a-f0-9]{64}$/.test(d.actualDigest))
|
|
353
|
+
return suppliedDigestMismatchDiagnostic(prepared, d.actualDigest);
|
|
354
|
+
return undefined;
|
|
355
|
+
};
|
|
356
|
+
if (provenance !== "verified" && provenance !== "unverified")
|
|
357
|
+
return false;
|
|
358
|
+
if (provenance === "verified" && !prepared)
|
|
359
|
+
return false;
|
|
360
|
+
if (provenance === "unverified" && diagnostics.length > 1)
|
|
361
|
+
return false;
|
|
362
|
+
let previous = -1;
|
|
363
|
+
for (const diagnostic of diagnostics) {
|
|
364
|
+
const expected = rebuild(diagnostic);
|
|
365
|
+
if (!expected || canonicalJson(expected) !== canonicalJson(diagnostic))
|
|
366
|
+
return false;
|
|
367
|
+
if (expected.kind === "excerpt-mismatch") {
|
|
368
|
+
if (expected.proposalIndex <= previous)
|
|
369
|
+
return false;
|
|
370
|
+
previous = expected.proposalIndex;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
138
375
|
function diagnosticsFor(envelope) {
|
|
376
|
+
return [...artifactDiagnostics(envelope), ...outcomeDiagnostics(envelope)];
|
|
377
|
+
}
|
|
378
|
+
function artifactDiagnostics(envelope) {
|
|
139
379
|
const state = envelope.result.preparedArtifactState;
|
|
140
380
|
if (!state || state.status === "available")
|
|
141
381
|
return [];
|
|
@@ -147,6 +387,21 @@ function diagnosticsFor(envelope) {
|
|
|
147
387
|
...(state.status === "invalid-artifact" ? { artifactRef: state.canonicalRef } : { artifactRef: state.requestedRef }),
|
|
148
388
|
message: `Prepared artifact resolution is ${state.status}.` }];
|
|
149
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* A failed run, or a partial run with no proposal, must not look like a
|
|
392
|
+
* complete run that found nothing. A partial run with proposals stays
|
|
393
|
+
* grounded: its reason and coverage travel on every candidate.
|
|
394
|
+
*/
|
|
395
|
+
function outcomeDiagnostics(envelope) {
|
|
396
|
+
const outcome = envelope.result.outcome;
|
|
397
|
+
if (outcome.status === "failure")
|
|
398
|
+
return [{ kind: "extraction-failed", category: outcome.category, code: outcome.code,
|
|
399
|
+
message: `Extraction failed (${outcome.category}/${outcome.code}); no usable answer was recorded for this source, so the import has no candidates.` }];
|
|
400
|
+
if (outcome.status === "partial" && envelope.result.proposals.length === 0)
|
|
401
|
+
return [{ kind: "extraction-incomplete", reason: outcome.reason,
|
|
402
|
+
message: `Extraction stopped short (${outcome.reason}) without proposing any value; unread text may hold values.` }];
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
150
405
|
function validateImport(value) {
|
|
151
406
|
jsonSafe(value, "Extraction envelope import");
|
|
152
407
|
const record = obj(value, "Extraction envelope import");
|
|
@@ -167,9 +422,12 @@ function validateImport(value) {
|
|
|
167
422
|
if (targets.length !== envelope.result.proposals.length)
|
|
168
423
|
throw new Error("spec.claimTargets must align with proposals.");
|
|
169
424
|
const status = obj(record.status, "status");
|
|
170
|
-
exact(status, ["state", "diagnostics"], "status");
|
|
425
|
+
exact(status, ["state", "diagnostics"], "status", ["provenance"]);
|
|
426
|
+
const diagnostics = array(status.diagnostics, "status.diagnostics");
|
|
171
427
|
const expected = diagnosticsFor(envelope);
|
|
172
|
-
if (canonicalJson(
|
|
428
|
+
if (canonicalJson(diagnostics.slice(0, expected.length)) !== canonicalJson(expected)
|
|
429
|
+
|| !verificationDiagnosticsAreCoherent(envelope, status.provenance, diagnostics.slice(expected.length))
|
|
430
|
+
|| status.state !== stateFor(diagnostics, envelope.result.proposals.length))
|
|
173
431
|
throw new Error("Import status does not match envelope state.");
|
|
174
432
|
}
|
|
175
433
|
/** Validate and defensively clone a persisted or browser-delivered import. */
|
|
@@ -189,7 +447,7 @@ function validateEnvelope(input) {
|
|
|
189
447
|
if (source.snapshotRef !== undefined)
|
|
190
448
|
safeReference(source.snapshotRef, "source.snapshotRef");
|
|
191
449
|
const r = obj(e.result, "result");
|
|
192
|
-
exact(r, ["proposals", "provider", "runId", "raw", "outcome", "extractedAt", "providerCalls", "totalTokensUsed"], "result", ["model", "warningClassifications", "partial", "providerFailures", "taskDigest", "exampleDigests", "pdfPageOffsets", "pdfLayout", "ocrDerived", "preparedArtifact", "preparedArtifactState"]);
|
|
450
|
+
exact(r, ["proposals", "provider", "runId", "raw", "outcome", "extractedAt", "providerCalls", "totalTokensUsed"], "result", ["model", "warningClassifications", "partial", "coverage", "providerFailures", "taskDigest", "exampleDigests", "pdfPageOffsets", "pdfLayout", "ocrDerived", "preparedArtifact", "preparedArtifactState"]);
|
|
193
451
|
stableIdentity(r.provider, "result.provider");
|
|
194
452
|
if (r.model !== undefined)
|
|
195
453
|
stableIdentity(r.model, "result.model");
|
|
@@ -224,6 +482,9 @@ function validateEnvelope(input) {
|
|
|
224
482
|
: artifact === undefined
|
|
225
483
|
? (() => { throw new Error("result.pdfLayout requires result.preparedArtifact."); })()
|
|
226
484
|
: validatePortablePdfLayout(r.pdfLayout, artifact.contentLength);
|
|
485
|
+
if (r.coverage !== undefined)
|
|
486
|
+
validateCoverage(r.coverage, artifact);
|
|
487
|
+
validateCoverageAgreement(r.outcome, r.coverage);
|
|
227
488
|
const state = r.preparedArtifactState === undefined ? undefined : validateArtifactState(r.preparedArtifactState, artifact);
|
|
228
489
|
if (source.snapshotRef !== undefined && artifact?.sourceSnapshotRef !== undefined && source.snapshotRef !== artifact.sourceSnapshotRef)
|
|
229
490
|
throw new Error("Source snapshot identity mismatch.");
|
|
@@ -232,10 +493,12 @@ function validateEnvelope(input) {
|
|
|
232
493
|
}
|
|
233
494
|
function validateProposal(input, index, contentLength) {
|
|
234
495
|
const p = obj(input, `proposal[${index}]`);
|
|
235
|
-
exact(p, ["fieldPath", "candidateValue", "
|
|
496
|
+
exact(p, ["fieldPath", "candidateValue", "provenance", "extractor"], `proposal[${index}]`, ["confidence", "pathIndices", "inferenceType", "valueType", "enumValues", "producedBy", "evidenceMatch"]);
|
|
236
497
|
wireNonEmpty(p.fieldPath, "proposal.fieldPath");
|
|
237
498
|
stableIdentity(p.extractor, "proposal.extractor");
|
|
238
|
-
|
|
499
|
+
// Absent means the proposer reported none; `null` is not absent and is rejected.
|
|
500
|
+
if (Object.hasOwn(p, "confidence"))
|
|
501
|
+
finite(p.confidence, "proposal.confidence", 0, 1);
|
|
239
502
|
const provenance = obj(p.provenance, "proposal.provenance");
|
|
240
503
|
exact(provenance, ["excerpt", "locator", "occurrence"], "proposal.provenance");
|
|
241
504
|
wireNonEmpty(provenance.excerpt, "proposal.provenance.excerpt");
|
|
@@ -323,6 +586,45 @@ else if (o.status === "failure") {
|
|
|
323
586
|
else
|
|
324
587
|
throw new Error("outcome status is invalid."); if (o.status !== "partial" && partial !== undefined)
|
|
325
588
|
throw new Error("partial requires partial outcome."); }
|
|
589
|
+
function validateCoverage(input, artifact) {
|
|
590
|
+
if (!artifact)
|
|
591
|
+
throw new Error("result.coverage requires result.preparedArtifact.");
|
|
592
|
+
const entries = array(input, "result.coverage");
|
|
593
|
+
entries.forEach((value, index) => {
|
|
594
|
+
const subject = `result.coverage[${index}]`;
|
|
595
|
+
const c = obj(value, subject);
|
|
596
|
+
exact(c, ["chunk", "start", "end", "status"], subject, ["reason"]);
|
|
597
|
+
integer(c.chunk, `${subject}.chunk`);
|
|
598
|
+
if (c.chunk === 0)
|
|
599
|
+
throw new Error(`${subject}.chunk must be positive.`);
|
|
600
|
+
integer(c.start, `${subject}.start`);
|
|
601
|
+
integer(c.end, `${subject}.end`);
|
|
602
|
+
if (c.end <= c.start)
|
|
603
|
+
throw new Error(`${subject} must have start < end.`);
|
|
604
|
+
if (c.end > artifact.contentLength)
|
|
605
|
+
throw new Error(`${subject}.end exceeds the prepared artifact contentLength.`);
|
|
606
|
+
if (!COVERAGE_STATUSES.has(c.status))
|
|
607
|
+
throw new Error(`${subject}.status is invalid.`);
|
|
608
|
+
if ((c.status === "unread") !== (c.reason !== undefined))
|
|
609
|
+
throw new Error(`${subject}.reason is required exactly when status is unread.`);
|
|
610
|
+
if (c.reason !== undefined && !COVERAGE_REASONS.has(c.reason))
|
|
611
|
+
throw new Error(`${subject}.reason is invalid.`);
|
|
612
|
+
// Chunks overlap by design, so ranges may overlap; only the order is fixed.
|
|
613
|
+
if (index > 0 && c.start < (entries[index - 1].start))
|
|
614
|
+
throw new Error("result.coverage entries must be ordered by start.");
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
/** The outcome and the coverage must tell the same story about unread text. */
|
|
618
|
+
function validateCoverageAgreement(outcome, coverage) {
|
|
619
|
+
const lost = coverage?.some((entry) => entry.status !== "complete") ?? false;
|
|
620
|
+
if (outcome.status === "success" && lost)
|
|
621
|
+
throw new Error("result.coverage names unread or unanswered text, but the outcome is success.");
|
|
622
|
+
// A loss reason means a dispatched chunk lost text; a never-dispatched range
|
|
623
|
+
// is an early stop, not that loss.
|
|
624
|
+
const dispatchedLoss = coverage?.some((entry) => entry.status !== "complete" && entry.reason !== "not-dispatched") ?? false;
|
|
625
|
+
if (outcome.status === "partial" && LOSS_PARTIAL.has(outcome.reason) && !dispatchedLoss)
|
|
626
|
+
throw new Error(`partial reason ${outcome.reason} requires a result.coverage entry for a dispatched chunk that was not read or answered.`);
|
|
627
|
+
}
|
|
326
628
|
function validateWarning(v) { const w = obj(v, "warning"); exact(w, ["category", "code"], "warning"); if (!WARNING_CATEGORIES.has(w.category))
|
|
327
629
|
throw new Error("warning category invalid."); stableIdentity(w.code, "warning.code"); }
|
|
328
630
|
function validateFailure(v) { const f = obj(v, "providerFailure"); exact(f, ["provider", "kind", "retryable"], "providerFailure", ["code"]); stableIdentity(f.provider, "failure.provider"); if (!FAILURE_KINDS.has(f.kind) || typeof f.retryable !== "boolean")
|
|
@@ -447,13 +749,18 @@ function isWellFormedUnicode(value) { for (let index = 0; index < value.length;
|
|
|
447
749
|
} return true; }
|
|
448
750
|
const RAW_SOURCE_KINDS = new Set(["uploaded-document", "web-page", "api-record", "manual-entry", "policy-standard", "inquiry-question", "agent-utterance", "system-schema"]);
|
|
449
751
|
const VALUE_TYPES = new Set(["string", "number", "boolean", "date", "enum", "array", "object"]);
|
|
450
|
-
|
|
752
|
+
/** Partial reasons meaning a dispatched chunk was not (fully) read or answered. */
|
|
753
|
+
const LOSS_PARTIAL = new Set(["provider-failure", "content-truncated", "output-truncated"]);
|
|
754
|
+
const PARTIAL = new Set(["cancelled", "max-provider-calls", "max-total-tokens", "max-chunks", ...LOSS_PARTIAL]);
|
|
755
|
+
const COVERAGE_STATUSES = new Set(["complete", "unread", "output-truncated"]);
|
|
756
|
+
const COVERAGE_REASONS = new Set(["provider-failure", "content-truncated", "missing-tool-call", "not-dispatched"]);
|
|
451
757
|
const FAILURE_CATEGORIES = new Set(["invalid-config", "invalid-task", "preparation", "provider", "unexpected"]);
|
|
452
758
|
const WARNING_CATEGORIES = new Set(["provider", "normalization", "preparation", "limit", "storage", "content", "other"]);
|
|
453
759
|
const FAILURE_KINDS = new Set(["authentication", "rate-limit", "timeout", "invalid-request", "unavailable", "unknown"]);
|
|
454
760
|
const MODEL_SOURCES = new Set(["provider-reported", "configured"]);
|
|
455
761
|
const SCHEMA_MATCHES = new Set(["ok", "type-mismatch", "enum-mismatch", "format-invalid"]);
|
|
456
762
|
const VALUE_IN_EXCERPT_MATCHES = new Set(["match", "mismatch", "not-evaluated", "not-applicable"]);
|
|
763
|
+
const ARTIFACT_UNAVAILABLE_CODES = new Set(["not-found", "storage-error", "access-denied", "invalid-artifact", "unknown"]);
|
|
457
764
|
const PREPARATION_MODES = new Set(["text", "markdown", "transcript", "pdf-text", "image-ocr"]);
|
|
458
765
|
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"]);
|
|
459
766
|
const STABLE_IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:@/+~-]{0,255}$/;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, Escalati
|
|
|
2
2
|
export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
|
|
3
3
|
export { reviewResourceApiVersion } from "./review-resource.js";
|
|
4
4
|
export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeResolutionIdentity, exportExtractionEnvelopeImport, extractionEnvelopeImportApiVersion, importExtractionEnvelope, portableExtractionResultFormat, portableExtractionResultVersion, reimportExtractionEnvelope, validateExtractionEnvelopeImport, } from "./extraction-envelope.js";
|
|
5
|
-
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
|
|
5
|
+
export { buildExtractionInspectorModel, exportExtractionInspector, inspectorSourcePosture, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
|
|
6
6
|
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, bindReviewQueue, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./review-workbench/queue-binding.js";
|
|
7
7
|
export type { BindReviewQueueOptions, ReviewQueueBinding, ReviewQueueBindingIssue, ReviewQueueBindingIssueCode, ReviewQueueExtractionIssue, ReviewQueueExtractionIssueCode, ValidateReviewQueueBindingOptions, } from "./review-workbench/queue-binding.js";
|
|
8
8
|
export { resolvePortablePdfRegion } from "./pdf-layout.js";
|
|
9
9
|
export type { PortablePdfBoundingBox, PortablePdfLayout, PortablePdfPageGeometry, PortablePdfRegionContext, PortablePdfTable, PortablePdfTableCell, PortablePdfTextElement, PortablePdfTextRange, } from "./pdf-layout.js";
|
|
10
10
|
export type { ExtractionAlignmentState, ArtifactUnavailableCode, BuiltExtractionInspectorCandidate, BuiltExtractionInspectorModel, ExtractionInspectorCandidate, ExtractionInspectorEntry, ExtractionInspectorExportOptions, ExtractionInspectorFilters, ExtractionInspectorInput, ExtractionInspectorModel, ExtractionInspectorSource, ResolvedExtractionArtifact, } from "./review-workbench/extraction-inspector.js";
|
|
11
|
-
export type { ExtractionEnvelopeImport, ExtractionEnvelopeImportDiagnostic, ExtractionEnvelopeImportOptions, ExtractionEnvelopeImportResult, ExtractionEnvelopeResolutionIdentity, PortableExtractionEvidenceMatch, PortableExtractionOccurrence, PortableExtractionProducedBy, PortableExtractionProposal, PortableExtractionResultEnvelope, PortablePreparedArtifactState, } from "./extraction-envelope.js";
|
|
11
|
+
export type { ExtractionEnvelopeImport, ExtractionEnvelopeImportDiagnostic, ExtractionEnvelopeImportOptions, ExtractionEnvelopeImportProvenance, ExtractionEnvelopeImportResult, ExtractionEnvelopeResolutionIdentity, PortableExtractionCoverageEntry, PortableExtractionEvidenceMatch, PortableExtractionPartialReason, PortableExtractionOccurrence, PortableExtractionProducedBy, PortableExtractionProposal, PortableExtractionResultEnvelope, PortablePreparedArtifactState, } from "./extraction-envelope.js";
|
|
12
12
|
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";
|
|
13
13
|
export { toSurfaceReviewedExtractionDecision, toSurfaceReviewedExtractionImport, toSurfaceReviewedExtractionItem, } from "./surface-reviewed-extraction.js";
|
|
14
14
|
export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
|
|
2
2
|
export { reviewResourceApiVersion } from "./review-resource.js";
|
|
3
3
|
export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeResolutionIdentity, exportExtractionEnvelopeImport, extractionEnvelopeImportApiVersion, importExtractionEnvelope, portableExtractionResultFormat, portableExtractionResultVersion, reimportExtractionEnvelope, validateExtractionEnvelopeImport, } from "./extraction-envelope.js";
|
|
4
|
-
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
|
|
4
|
+
export { buildExtractionInspectorModel, exportExtractionInspector, inspectorSourcePosture, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
|
|
5
5
|
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, bindReviewQueue, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./review-workbench/queue-binding.js";
|
|
6
6
|
export { resolvePortablePdfRegion } from "./pdf-layout.js";
|
|
7
7
|
export { toSurfaceReviewedExtractionDecision, toSurfaceReviewedExtractionImport, toSurfaceReviewedExtractionItem, } from "./surface-reviewed-extraction.js";
|