@kontourai/survey 3.0.0 → 5.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.
Files changed (46) hide show
  1. package/README.md +4 -0
  2. package/dist/examples/calibrated-auto-accept.d.ts +22 -15
  3. package/dist/examples/calibrated-auto-accept.js +40 -36
  4. package/dist/examples/review-workbench/server-apply-consumer.js +5 -0
  5. package/dist/src/calibration.d.ts +48 -21
  6. package/dist/src/calibration.js +72 -33
  7. package/dist/src/canonical-reviewed-trust-input.js +73 -34
  8. package/dist/src/console/review-console-server.d.ts +3 -1
  9. package/dist/src/console/review-console-server.js +203 -50
  10. package/dist/src/extraction-envelope.d.ts +81 -3
  11. package/dist/src/extraction-envelope.js +183 -24
  12. package/dist/src/index.d.ts +9 -8
  13. package/dist/src/index.js +3 -3
  14. package/dist/src/inquiry-mapping.d.ts +15 -1
  15. package/dist/src/inquiry-mapping.js +10 -2
  16. package/dist/src/mcp/review-mcp.js +112 -90
  17. package/dist/src/producer-profile.d.ts +41 -2
  18. package/dist/src/producer-profile.js +29 -2
  19. package/dist/src/review-session-file.d.ts +64 -0
  20. package/dist/src/review-session-file.js +320 -0
  21. package/dist/src/review-workbench/edited-value.d.ts +70 -0
  22. package/dist/src/review-workbench/edited-value.js +147 -0
  23. package/dist/src/review-workbench/extraction-inspector.d.ts +15 -1
  24. package/dist/src/review-workbench/extraction-inspector.js +55 -19
  25. package/dist/src/review-workbench/queue-binding.js +1 -1
  26. package/dist/src/review-workbench/review-presentation.d.ts +46 -1
  27. package/dist/src/review-workbench/review-presentation.js +72 -1
  28. package/dist/src/review-workbench/review-queue-session.d.ts +19 -5
  29. package/dist/src/review-workbench/review-queue-session.js +54 -14
  30. package/dist/src/review-workbench/review-session-replay.d.ts +35 -1
  31. package/dist/src/review-workbench/review-session-replay.js +82 -3
  32. package/dist/src/review-workbench/review-workbench-css.generated.js +2 -0
  33. package/dist/src/review-workbench/review-workbench.css +2 -0
  34. package/dist/src/review-workbench/review-workbench.d.ts +22 -11
  35. package/dist/src/review-workbench/review-workbench.js +91 -26
  36. package/dist/src/review-workbench/review-workbench.standalone.css +2 -0
  37. package/dist/src/review-workbench/server-review-session.d.ts +3 -1
  38. package/dist/src/review-workbench/server-review-session.js +1 -0
  39. package/dist/src/reviewed-candidate-resolution.js +13 -7
  40. package/dist/src/schema-mapping.d.ts +23 -0
  41. package/dist/src/schema-mapping.js +30 -20
  42. package/dist/src/surface-reviewed-extraction.js +4 -0
  43. package/dist/src/to-surface.d.ts +30 -6
  44. package/dist/src/to-surface.js +312 -18
  45. package/dist/src/types.d.ts +44 -1
  46. package/package.json +5 -4
@@ -17,10 +17,33 @@ export interface PortableExtractionOccurrence {
17
17
  hintUsed: boolean;
18
18
  ambiguous: boolean;
19
19
  }
20
+ /** Which model produced one proposal, as the extractor recorded it for that proposal's own request. */
21
+ export interface PortableExtractionProducedBy {
22
+ model: string;
23
+ modelSource: "provider-reported" | "configured";
24
+ /** Content-free digest of the provider request (`sha256:<hex>`). */
25
+ requestDigest: string;
26
+ }
27
+ /**
28
+ * Deterministic, versioned facts about how a proposal's value relates to the
29
+ * declared schema and to its own excerpt. Annotations only: Survey carries
30
+ * them to review and does not route or block on them.
31
+ */
32
+ export interface PortableExtractionEvidenceMatch {
33
+ checkerVersion: string;
34
+ schema: "ok" | "type-mismatch" | "enum-mismatch" | "format-invalid";
35
+ valueInExcerpt: "match" | "mismatch" | "not-evaluated" | "not-applicable";
36
+ tokenBoundary?: boolean;
37
+ }
20
38
  export interface PortableExtractionProposal {
21
39
  fieldPath: string;
22
40
  candidateValue: unknown;
23
- confidence: number;
41
+ /**
42
+ * The proposer's own uncalibrated self-report in `0..1`, absent when the
43
+ * proposer reported none. Survey carries it only when present and never
44
+ * substitutes a default.
45
+ */
46
+ confidence?: number;
24
47
  provenance: {
25
48
  excerpt: string;
26
49
  locator: string;
@@ -31,6 +54,8 @@ export interface PortableExtractionProposal {
31
54
  inferenceType?: "explicit" | "inferred";
32
55
  valueType?: ReviewValueType;
33
56
  enumValues?: string[];
57
+ producedBy?: PortableExtractionProducedBy;
58
+ evidenceMatch?: PortableExtractionEvidenceMatch;
34
59
  }
35
60
  export type PortablePreparedArtifactState = {
36
61
  status: "available" | "unavailable" | "storage-error";
@@ -51,6 +76,25 @@ export type PortablePreparedArtifactState = {
51
76
  actualDigest: string;
52
77
  actualContentLength: number;
53
78
  };
79
+ /**
80
+ * Why a run stopped short. The first four are early stops; the last three mean
81
+ * a dispatched chunk was not (fully) read or answered, and such an envelope
82
+ * names the affected ranges in `result.coverage`.
83
+ */
84
+ export type PortableExtractionPartialReason = "cancelled" | "max-provider-calls" | "max-total-tokens" | "max-chunks" | "provider-failure" | "content-truncated" | "output-truncated";
85
+ /**
86
+ * One prepared-text range (UTF-16 offsets, the space of `chars:` locators) and
87
+ * whether it was read and answered. `reason` is present exactly when `status`
88
+ * is `unread`, and then covers exactly the unread span.
89
+ */
90
+ export interface PortableExtractionCoverageEntry {
91
+ /** 1-based chunk number. */
92
+ chunk: number;
93
+ start: number;
94
+ end: number;
95
+ status: "complete" | "unread" | "output-truncated";
96
+ reason?: "provider-failure" | "content-truncated" | "missing-tool-call" | "not-dispatched";
97
+ }
54
98
  export interface PortableExtractionResultEnvelope {
55
99
  format: typeof portableExtractionResultFormat;
56
100
  version: typeof portableExtractionResultVersion;
@@ -70,7 +114,7 @@ export interface PortableExtractionResultEnvelope {
70
114
  status: "success";
71
115
  } | {
72
116
  status: "partial";
73
- reason: "cancelled" | "max-provider-calls" | "max-total-tokens" | "max-chunks";
117
+ reason: PortableExtractionPartialReason;
74
118
  } | {
75
119
  status: "failure";
76
120
  category: "invalid-config" | "invalid-task" | "preparation" | "provider" | "unexpected";
@@ -84,15 +128,19 @@ export interface PortableExtractionResultEnvelope {
84
128
  providerCalls: number;
85
129
  totalTokensUsed: number;
86
130
  partial?: {
87
- reason: "cancelled" | "max-provider-calls" | "max-total-tokens" | "max-chunks";
131
+ reason: PortableExtractionPartialReason;
88
132
  completedChunks: number;
89
133
  remainingChunks: number;
90
134
  tokenOvershoot?: number;
91
135
  };
136
+ /** Per-chunk read coverage, ordered by `start`; ranges may overlap. Requires `preparedArtifact`. */
137
+ coverage?: PortableExtractionCoverageEntry[];
138
+ /** `code` is the upstream error code, informational only; `kind` stays authoritative. */
92
139
  providerFailures?: Array<{
93
140
  provider: string;
94
141
  kind: "authentication" | "rate-limit" | "timeout" | "invalid-request" | "unavailable" | "unknown";
95
142
  retryable: boolean;
143
+ code?: string;
96
144
  }>;
97
145
  taskDigest?: string;
98
146
  exampleDigests?: string[];
@@ -121,6 +169,13 @@ export interface ExtractionEnvelopeImportOptions {
121
169
  /** Survey meaning is supplied at the boundary; it is not added to the upstream wire contract. */
122
170
  claimTarget: (proposal: PortableExtractionProposal, index: number) => ClaimTargetHint;
123
171
  }
172
+ /**
173
+ * Why an import is `unresolved`: its prepared artifact did not resolve, or the
174
+ * extraction itself produced nothing reviewable. `extraction-failed` is a
175
+ * failure outcome; `extraction-incomplete` is a partial outcome that proposed
176
+ * nothing, so no candidate can carry the reason. Either way the import is not
177
+ * a complete run that found no values.
178
+ */
124
179
  export type ExtractionEnvelopeImportDiagnostic = {
125
180
  kind: "artifact-unavailable";
126
181
  status: "unavailable" | "storage-error" | "identity-mismatch" | "invalid-artifact";
@@ -132,6 +187,15 @@ export type ExtractionEnvelopeImportDiagnostic = {
132
187
  expectedDigest: string;
133
188
  actualDigest: string;
134
189
  message: string;
190
+ } | {
191
+ kind: "extraction-failed";
192
+ category: string;
193
+ code: string;
194
+ message: string;
195
+ } | {
196
+ kind: "extraction-incomplete";
197
+ reason: PortableExtractionPartialReason;
198
+ message: string;
135
199
  };
136
200
  export interface ExtractionEnvelopeImport {
137
201
  apiVersion: typeof extractionEnvelopeImportApiVersion;
@@ -160,6 +224,20 @@ export interface ExtractionEnvelopeResolutionIdentity {
160
224
  }
161
225
  /** Parse an untrusted upstream document and create Survey's durable import projection. */
162
226
  export declare function importExtractionEnvelope(serialized: string | PortableExtractionResultEnvelope, options: ExtractionEnvelopeImportOptions): ExtractionEnvelopeImportResult;
227
+ /**
228
+ * One ReviewItem per claim slot: every proposal whose claim target names the
229
+ * same claim (subject, facet, claim type, field or behavior, and claim id when
230
+ * one is set) at the same `pathIndices` is one candidate set, so two values for
231
+ * one claim can never be accepted as two separate verified claims. The slot is
232
+ * the claim the proposal would project to, not the upstream `fieldPath` (two
233
+ * field paths mapped to one claim are one slot) and not the value.
234
+ *
235
+ * Within a slot there is one candidate per distinct canonical value; further
236
+ * proposals of an already-seen value are recorded on that candidate as
237
+ * `sameValueProposals`. Two or more distinct values make the set `conflict`.
238
+ * Distinct `pathIndices` (array items) or distinct claim ids are separate slots,
239
+ * which is how a producer declares a multi-valued field.
240
+ */
163
241
  export declare function buildReviewItemsFromExtractionEnvelopeImport(record: ExtractionEnvelopeImport): ReviewItem[];
164
242
  export declare function exportExtractionEnvelopeImport(record: ExtractionEnvelopeImport): string;
165
243
  export declare function reimportExtractionEnvelope(serialized: string): ExtractionEnvelopeImport;
@@ -36,11 +36,48 @@ export function importExtractionEnvelope(serialized, options) {
36
36
  };
37
37
  return { record, reviewItems: buildReviewItemsFromExtractionEnvelopeImport(record) };
38
38
  }
39
+ /**
40
+ * One ReviewItem per claim slot: every proposal whose claim target names the
41
+ * same claim (subject, facet, claim type, field or behavior, and claim id when
42
+ * one is set) at the same `pathIndices` is one candidate set, so two values for
43
+ * one claim can never be accepted as two separate verified claims. The slot is
44
+ * the claim the proposal would project to, not the upstream `fieldPath` (two
45
+ * field paths mapped to one claim are one slot) and not the value.
46
+ *
47
+ * Within a slot there is one candidate per distinct canonical value; further
48
+ * proposals of an already-seen value are recorded on that candidate as
49
+ * `sameValueProposals`. Two or more distinct values make the set `conflict`.
50
+ * Distinct `pathIndices` (array items) or distinct claim ids are separate slots,
51
+ * which is how a producer declares a multi-valued field.
52
+ */
39
53
  export function buildReviewItemsFromExtractionEnvelopeImport(record) {
40
54
  validateImport(record);
41
55
  if (record.status.state !== "grounded")
42
56
  return [];
43
- return record.spec.envelope.result.proposals.map((proposal, index) => buildReviewItem(record, proposal, index));
57
+ return claimSlotGroups(record).map((group) => buildReviewItem(record, group));
58
+ }
59
+ function claimSlotGroups(record) {
60
+ const groups = new Map();
61
+ record.spec.envelope.result.proposals.forEach((proposal, index) => {
62
+ const target = record.spec.claimTargets[index];
63
+ const slot = {
64
+ subjectType: target.subjectType, subjectId: target.subjectId, facet: target.facet, claimType: target.claimType,
65
+ fieldOrBehavior: target.fieldOrBehavior, claimId: target.claimId ?? null, pathIndices: proposal.pathIndices ?? null,
66
+ };
67
+ const key = canonicalJson(slot);
68
+ const group = groups.get(key);
69
+ if (!group) {
70
+ groups.set(key, { slot, members: [{ proposal, index }] });
71
+ return;
72
+ }
73
+ const first = group.members[0].index;
74
+ // One claim cannot carry two impact levels or evidence descriptions.
75
+ if (canonicalJson(record.spec.claimTargets[first]) !== canonicalJson(target)) {
76
+ throw new Error(`Proposals ${first} and ${index} map to the same claim with different claim targets.`);
77
+ }
78
+ group.members.push({ proposal, index });
79
+ });
80
+ return [...groups.values()];
44
81
  }
45
82
  export function exportExtractionEnvelopeImport(record) {
46
83
  validateImport(record);
@@ -66,15 +103,50 @@ export function createExtractionEnvelopeResolutionIdentity(record, proposalIndex
66
103
  const nonce = globalThis.crypto.randomUUID();
67
104
  return { evidenceId: `survey.extraction.${base}.resolution-evidence.${nonce}`, eventId: `survey.extraction.${base}.resolution-event.${nonce}` };
68
105
  }
69
- function buildReviewItem(record, proposal, index) {
106
+ function buildReviewItem(record, group) {
107
+ const envelope = record.spec.envelope;
108
+ const byValue = new Map();
109
+ for (const member of group.members) {
110
+ const key = canonicalJson(member.proposal.candidateValue);
111
+ byValue.set(key, [...(byValue.get(key) ?? []), member]);
112
+ }
113
+ const candidates = [...byValue.values()].map((members) => buildCandidate(record, members));
114
+ const lead = group.members[0].proposal;
115
+ const valueType = lead.valueType ?? inferValueType(lead.candidateValue);
116
+ const identity = identityHash({ producerNamespace: record.metadata.producerNamespace, importName: record.metadata.name, source: envelope.source,
117
+ preparedArtifact: envelope.result.preparedArtifact, pdfLayout: envelope.result.pdfLayout, runId: envelope.result.runId, claimSlot: group.slot });
118
+ return {
119
+ apiVersion: reviewResourceApiVersion, kind: "ReviewItem",
120
+ metadata: { name: `extraction-envelope.${identity}`, producer: { "survey.kontourai.io/extraction-envelope": {
121
+ importName: record.metadata.name,
122
+ evidenceId: `survey.extraction.${identityHash(evidenceInputs(record, lead))}.source-evidence`,
123
+ proposalIndices: group.members.map((member) => member.index),
124
+ source: envelope.source,
125
+ ...(envelope.result.preparedArtifact ? { preparedArtifact: envelope.result.preparedArtifact } : {}),
126
+ } } },
127
+ spec: {
128
+ target: lead.fieldPath, candidates,
129
+ candidateSetStatus: candidates.length > 1 ? "conflict" : "needs-review",
130
+ valueDescriptor: { type: valueType }, editable: false,
131
+ },
132
+ status: { observedCandidateCount: candidates.length },
133
+ };
134
+ }
135
+ /** One candidate for one distinct value; `members` are that value's proposals in envelope order. */
136
+ function buildCandidate(record, members) {
70
137
  const envelope = record.spec.envelope;
71
- const identity = identityHash(identityInputs(record, proposal, index));
138
+ const { proposal, index } = members[0];
139
+ const others = members.slice(1);
140
+ // A candidate commits to every proposal it stands for; a single-proposal
141
+ // candidate keeps exactly the identity it had before grouping.
142
+ const identity = identityHash(others.length === 0 ? identityInputs(record, proposal, index)
143
+ : { ...identityInputs(record, proposal, index), sameValueProposals: others.map((other) => ({ proposalIndex: other.index, proposal: other.proposal })) });
72
144
  const evidence = identityHash(evidenceInputs(record, proposal));
73
145
  const target = record.spec.claimTargets[index];
74
146
  const valueType = proposal.valueType ?? inferValueType(proposal.candidateValue);
75
- const candidate = {
147
+ return {
76
148
  id: `extraction-envelope.${identity}.proposed`, role: "proposed", value: proposal.candidateValue,
77
- confidence: proposal.confidence,
149
+ ...(proposal.confidence !== undefined ? { confidence: proposal.confidence } : {}),
78
150
  source: {
79
151
  sourceRef: envelope.source.ref,
80
152
  sourceId: envelope.source.snapshotRef ?? envelope.source.ref,
@@ -87,9 +159,12 @@ function buildReviewItem(record, proposal, index) {
87
159
  extraction: {
88
160
  extractionId: `extraction-envelope.${identity}`,
89
161
  target: proposal.fieldPath,
90
- confidence: proposal.confidence,
162
+ ...(proposal.confidence !== undefined ? { confidence: proposal.confidence } : {}),
91
163
  extractor: proposal.extractor,
92
- ...(envelope.result.model ? { model: envelope.result.model } : {}),
164
+ extractedAt: envelope.result.extractedAt,
165
+ // The proposal's own served model when recorded; in a multi-chunk run
166
+ // `result.model` names only the last chunk's model.
167
+ ...(proposal.producedBy ? { model: proposal.producedBy.model } : envelope.result.model ? { model: envelope.result.model } : {}),
93
168
  },
94
169
  claimTarget: target,
95
170
  producer: { "survey.kontourai.io/extraction-envelope": {
@@ -100,23 +175,24 @@ function buildReviewItem(record, proposal, index) {
100
175
  ...(envelope.result.taskDigest ? { taskDigest: envelope.result.taskDigest } : {}),
101
176
  ...(envelope.result.exampleDigests ? { exampleDigests: envelope.result.exampleDigests } : {}),
102
177
  valueType: { type: valueType, origin: proposal.inferenceType ?? "inferred" },
178
+ ...(proposal.producedBy ? { producedBy: proposal.producedBy } : {}),
179
+ ...(proposal.evidenceMatch ? { evidenceMatch: proposal.evidenceMatch } : {}),
103
180
  occurrence: proposal.provenance.occurrence,
181
+ ...(others.length ? { sameValueProposals: others.map((other) => ({
182
+ proposalIndex: other.index,
183
+ evidenceId: `survey.extraction.${identityHash(evidenceInputs(record, other.proposal))}.source-evidence`,
184
+ locator: other.proposal.provenance.locator,
185
+ excerpt: other.proposal.provenance.excerpt,
186
+ })) } : {}),
104
187
  attempt: { id: envelope.result.runId, providerCalls: envelope.result.providerCalls },
105
188
  ...(envelope.result.warningClassifications ? { warnings: envelope.result.warningClassifications } : {}),
106
189
  outcome: envelope.result.outcome,
190
+ // A partial run may have left this field's other values unread: the
191
+ // reason and the per-chunk coverage travel with every candidate.
192
+ ...(envelope.result.partial ? { partial: envelope.result.partial } : {}),
193
+ ...(envelope.result.coverage ? { coverage: envelope.result.coverage } : {}),
107
194
  } },
108
195
  };
109
- return {
110
- apiVersion: reviewResourceApiVersion, kind: "ReviewItem",
111
- metadata: { name: `extraction-envelope.${identity}`, producer: { "survey.kontourai.io/extraction-envelope": {
112
- importName: record.metadata.name,
113
- evidenceId: `survey.extraction.${evidence}.source-evidence`,
114
- source: envelope.source,
115
- ...(envelope.result.preparedArtifact ? { preparedArtifact: envelope.result.preparedArtifact } : {}),
116
- } } },
117
- spec: { target: proposal.fieldPath, candidates: [candidate], candidateSetStatus: "needs-review", valueDescriptor: { type: valueType }, editable: false },
118
- status: { observedCandidateCount: 1 },
119
- };
120
196
  }
121
197
  function identityInputs(record, proposal, index) {
122
198
  return { producerNamespace: record.metadata.producerNamespace, importName: record.metadata.name, source: record.spec.envelope.source,
@@ -132,6 +208,9 @@ function evidenceInputs(record, proposal) {
132
208
  provenance: proposal.provenance };
133
209
  }
134
210
  function diagnosticsFor(envelope) {
211
+ return [...artifactDiagnostics(envelope), ...outcomeDiagnostics(envelope)];
212
+ }
213
+ function artifactDiagnostics(envelope) {
135
214
  const state = envelope.result.preparedArtifactState;
136
215
  if (!state || state.status === "available")
137
216
  return [];
@@ -143,6 +222,21 @@ function diagnosticsFor(envelope) {
143
222
  ...(state.status === "invalid-artifact" ? { artifactRef: state.canonicalRef } : { artifactRef: state.requestedRef }),
144
223
  message: `Prepared artifact resolution is ${state.status}.` }];
145
224
  }
225
+ /**
226
+ * A failed run, or a partial run with no proposal, must not look like a
227
+ * complete run that found nothing. A partial run with proposals stays
228
+ * grounded: its reason and coverage travel on every candidate.
229
+ */
230
+ function outcomeDiagnostics(envelope) {
231
+ const outcome = envelope.result.outcome;
232
+ if (outcome.status === "failure")
233
+ return [{ kind: "extraction-failed", category: outcome.category, code: outcome.code,
234
+ message: `Extraction failed (${outcome.category}/${outcome.code}); no usable answer was recorded for this source, so the import has no candidates.` }];
235
+ if (outcome.status === "partial" && envelope.result.proposals.length === 0)
236
+ return [{ kind: "extraction-incomplete", reason: outcome.reason,
237
+ message: `Extraction stopped short (${outcome.reason}) without proposing any value; unread text may hold values.` }];
238
+ return [];
239
+ }
146
240
  function validateImport(value) {
147
241
  jsonSafe(value, "Extraction envelope import");
148
242
  const record = obj(value, "Extraction envelope import");
@@ -185,7 +279,7 @@ function validateEnvelope(input) {
185
279
  if (source.snapshotRef !== undefined)
186
280
  safeReference(source.snapshotRef, "source.snapshotRef");
187
281
  const r = obj(e.result, "result");
188
- exact(r, ["proposals", "provider", "runId", "raw", "outcome", "extractedAt", "providerCalls", "totalTokensUsed"], "result", ["model", "warningClassifications", "partial", "providerFailures", "taskDigest", "exampleDigests", "pdfPageOffsets", "pdfLayout", "ocrDerived", "preparedArtifact", "preparedArtifactState"]);
282
+ exact(r, ["proposals", "provider", "runId", "raw", "outcome", "extractedAt", "providerCalls", "totalTokensUsed"], "result", ["model", "warningClassifications", "partial", "coverage", "providerFailures", "taskDigest", "exampleDigests", "pdfPageOffsets", "pdfLayout", "ocrDerived", "preparedArtifact", "preparedArtifactState"]);
189
283
  stableIdentity(r.provider, "result.provider");
190
284
  if (r.model !== undefined)
191
285
  stableIdentity(r.model, "result.model");
@@ -220,6 +314,9 @@ function validateEnvelope(input) {
220
314
  : artifact === undefined
221
315
  ? (() => { throw new Error("result.pdfLayout requires result.preparedArtifact."); })()
222
316
  : validatePortablePdfLayout(r.pdfLayout, artifact.contentLength);
317
+ if (r.coverage !== undefined)
318
+ validateCoverage(r.coverage, artifact);
319
+ validateCoverageAgreement(r.outcome, r.coverage);
223
320
  const state = r.preparedArtifactState === undefined ? undefined : validateArtifactState(r.preparedArtifactState, artifact);
224
321
  if (source.snapshotRef !== undefined && artifact?.sourceSnapshotRef !== undefined && source.snapshotRef !== artifact.sourceSnapshotRef)
225
322
  throw new Error("Source snapshot identity mismatch.");
@@ -228,10 +325,12 @@ function validateEnvelope(input) {
228
325
  }
229
326
  function validateProposal(input, index, contentLength) {
230
327
  const p = obj(input, `proposal[${index}]`);
231
- exact(p, ["fieldPath", "candidateValue", "confidence", "provenance", "extractor"], `proposal[${index}]`, ["pathIndices", "inferenceType", "valueType", "enumValues"]);
328
+ exact(p, ["fieldPath", "candidateValue", "provenance", "extractor"], `proposal[${index}]`, ["confidence", "pathIndices", "inferenceType", "valueType", "enumValues", "producedBy", "evidenceMatch"]);
232
329
  wireNonEmpty(p.fieldPath, "proposal.fieldPath");
233
330
  stableIdentity(p.extractor, "proposal.extractor");
234
- finite(p.confidence, "proposal.confidence", 0, 1);
331
+ // Absent means the proposer reported none; `null` is not absent and is rejected.
332
+ if (Object.hasOwn(p, "confidence"))
333
+ finite(p.confidence, "proposal.confidence", 0, 1);
235
334
  const provenance = obj(p.provenance, "proposal.provenance");
236
335
  exact(provenance, ["excerpt", "locator", "occurrence"], "proposal.provenance");
237
336
  wireNonEmpty(provenance.excerpt, "proposal.provenance.excerpt");
@@ -252,6 +351,10 @@ function validateProposal(input, index, contentLength) {
252
351
  throw new Error("proposal valueType is invalid.");
253
352
  if (p.enumValues !== undefined)
254
353
  array(p.enumValues, "enumValues").forEach((v) => wellFormedString(v, "enumValue"));
354
+ if (p.producedBy !== undefined)
355
+ validateProducedBy(p.producedBy);
356
+ if (p.evidenceMatch !== undefined)
357
+ validateEvidenceMatch(p.evidenceMatch);
255
358
  return cloneJson(p);
256
359
  }
257
360
  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")
@@ -315,10 +418,59 @@ else if (o.status === "failure") {
315
418
  else
316
419
  throw new Error("outcome status is invalid."); if (o.status !== "partial" && partial !== undefined)
317
420
  throw new Error("partial requires partial outcome."); }
421
+ function validateCoverage(input, artifact) {
422
+ if (!artifact)
423
+ throw new Error("result.coverage requires result.preparedArtifact.");
424
+ const entries = array(input, "result.coverage");
425
+ entries.forEach((value, index) => {
426
+ const subject = `result.coverage[${index}]`;
427
+ const c = obj(value, subject);
428
+ exact(c, ["chunk", "start", "end", "status"], subject, ["reason"]);
429
+ integer(c.chunk, `${subject}.chunk`);
430
+ if (c.chunk === 0)
431
+ throw new Error(`${subject}.chunk must be positive.`);
432
+ integer(c.start, `${subject}.start`);
433
+ integer(c.end, `${subject}.end`);
434
+ if (c.end <= c.start)
435
+ throw new Error(`${subject} must have start < end.`);
436
+ if (c.end > artifact.contentLength)
437
+ throw new Error(`${subject}.end exceeds the prepared artifact contentLength.`);
438
+ if (!COVERAGE_STATUSES.has(c.status))
439
+ throw new Error(`${subject}.status is invalid.`);
440
+ if ((c.status === "unread") !== (c.reason !== undefined))
441
+ throw new Error(`${subject}.reason is required exactly when status is unread.`);
442
+ if (c.reason !== undefined && !COVERAGE_REASONS.has(c.reason))
443
+ throw new Error(`${subject}.reason is invalid.`);
444
+ // Chunks overlap by design, so ranges may overlap; only the order is fixed.
445
+ if (index > 0 && c.start < (entries[index - 1].start))
446
+ throw new Error("result.coverage entries must be ordered by start.");
447
+ });
448
+ }
449
+ /** The outcome and the coverage must tell the same story about unread text. */
450
+ function validateCoverageAgreement(outcome, coverage) {
451
+ const lost = coverage?.some((entry) => entry.status !== "complete") ?? false;
452
+ if (outcome.status === "success" && lost)
453
+ throw new Error("result.coverage names unread or unanswered text, but the outcome is success.");
454
+ // A loss reason means a dispatched chunk lost text; a never-dispatched range
455
+ // is an early stop, not that loss.
456
+ const dispatchedLoss = coverage?.some((entry) => entry.status !== "complete" && entry.reason !== "not-dispatched") ?? false;
457
+ if (outcome.status === "partial" && LOSS_PARTIAL.has(outcome.reason) && !dispatchedLoss)
458
+ throw new Error(`partial reason ${outcome.reason} requires a result.coverage entry for a dispatched chunk that was not read or answered.`);
459
+ }
318
460
  function validateWarning(v) { const w = obj(v, "warning"); exact(w, ["category", "code"], "warning"); if (!WARNING_CATEGORIES.has(w.category))
319
461
  throw new Error("warning category invalid."); stableIdentity(w.code, "warning.code"); }
320
- 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")
321
- throw new Error("provider failure invalid."); }
462
+ 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")
463
+ throw new Error("provider failure invalid."); if (f.code !== undefined) {
464
+ stableIdentity(f.code, "failure.code");
465
+ if (f.code.length > 128)
466
+ throw new Error("failure.code must be at most 128 characters.");
467
+ } }
468
+ function validateProducedBy(v) { const b = obj(v, "proposal.producedBy"); exact(b, ["model", "modelSource", "requestDigest"], "proposal.producedBy"); stableIdentity(b.model, "proposal.producedBy.model"); if (!MODEL_SOURCES.has(b.modelSource))
469
+ throw new Error("proposal.producedBy.modelSource is invalid."); digest(b.requestDigest, "proposal.producedBy.requestDigest"); }
470
+ function validateEvidenceMatch(v) { const m = obj(v, "proposal.evidenceMatch"); exact(m, ["checkerVersion", "schema", "valueInExcerpt"], "proposal.evidenceMatch", ["tokenBoundary"]); stableIdentity(m.checkerVersion, "proposal.evidenceMatch.checkerVersion"); if (!SCHEMA_MATCHES.has(m.schema))
471
+ throw new Error("proposal.evidenceMatch.schema is invalid."); if (!VALUE_IN_EXCERPT_MATCHES.has(m.valueInExcerpt))
472
+ throw new Error("proposal.evidenceMatch.valueInExcerpt is invalid."); if (m.tokenBoundary !== undefined && typeof m.tokenBoundary !== "boolean")
473
+ throw new Error("proposal.evidenceMatch.tokenBoundary must be a boolean."); }
322
474
  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"])
323
475
  nonEmpty(t[key], `claimTarget.${key}`); if (!["low", "medium", "high", "critical"].includes(t.impactLevel))
324
476
  throw new Error("claimTarget.impactLevel invalid."); for (const key of ["claimId", "evidenceType", "evidenceMethod", "collectedBy"])
@@ -429,10 +581,17 @@ function isWellFormedUnicode(value) { for (let index = 0; index < value.length;
429
581
  } return true; }
430
582
  const RAW_SOURCE_KINDS = new Set(["uploaded-document", "web-page", "api-record", "manual-entry", "policy-standard", "inquiry-question", "agent-utterance", "system-schema"]);
431
583
  const VALUE_TYPES = new Set(["string", "number", "boolean", "date", "enum", "array", "object"]);
432
- const PARTIAL = new Set(["cancelled", "max-provider-calls", "max-total-tokens", "max-chunks"]);
584
+ /** Partial reasons meaning a dispatched chunk was not (fully) read or answered. */
585
+ const LOSS_PARTIAL = new Set(["provider-failure", "content-truncated", "output-truncated"]);
586
+ const PARTIAL = new Set(["cancelled", "max-provider-calls", "max-total-tokens", "max-chunks", ...LOSS_PARTIAL]);
587
+ const COVERAGE_STATUSES = new Set(["complete", "unread", "output-truncated"]);
588
+ const COVERAGE_REASONS = new Set(["provider-failure", "content-truncated", "missing-tool-call", "not-dispatched"]);
433
589
  const FAILURE_CATEGORIES = new Set(["invalid-config", "invalid-task", "preparation", "provider", "unexpected"]);
434
590
  const WARNING_CATEGORIES = new Set(["provider", "normalization", "preparation", "limit", "storage", "content", "other"]);
435
591
  const FAILURE_KINDS = new Set(["authentication", "rate-limit", "timeout", "invalid-request", "unavailable", "unknown"]);
592
+ const MODEL_SOURCES = new Set(["provider-reported", "configured"]);
593
+ const SCHEMA_MATCHES = new Set(["ok", "type-mismatch", "enum-mismatch", "format-invalid"]);
594
+ const VALUE_IN_EXCERPT_MATCHES = new Set(["match", "mismatch", "not-evaluated", "not-applicable"]);
436
595
  const PREPARATION_MODES = new Set(["text", "markdown", "transcript", "pdf-text", "image-ocr"]);
437
596
  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"]);
438
597
  const STABLE_IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:@/+~-]{0,255}$/;
@@ -1,14 +1,14 @@
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";
1
+ export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, EscalationDimension, EscalationRecord, Extraction, Interpretation, InterpretationAnswerImpact, InterpretationReadingKind, 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
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, PortableExtractionOccurrence, PortableExtractionProposal, PortableExtractionResultEnvelope, PortablePreparedArtifactState, } from "./extraction-envelope.js";
11
+ export type { ExtractionEnvelopeImport, ExtractionEnvelopeImportDiagnostic, ExtractionEnvelopeImportOptions, 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";
@@ -17,7 +17,7 @@ export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js"
17
17
  export type { ReviewedCandidateResolutionInput } from "./reviewed-candidate-resolution.js";
18
18
  export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
19
19
  export type { CurrentProposedCandidateRole, ReviewedCurrentProposedResolutionInput, } from "./reviewed-current-proposed-resolution.js";
20
- export { buildSurveyTrustBundle } from "./to-surface.js";
20
+ export { buildSurveyTrustBundle, ReviewAgreementError } from "./to-surface.js";
21
21
  export type { BuildSurveyTrustBundleOptions } from "./to-surface.js";
22
22
  export { buildCanonicalReviewedTrustInput } from "./canonical-reviewed-trust-input.js";
23
23
  export type { BuildCanonicalReviewedTrustInputOptions, CanonicalReviewedTrustInput, } from "./canonical-reviewed-trust-input.js";
@@ -35,16 +35,17 @@ export { repeatedObservation } from "./repeated-observation.js";
35
35
  export type { RepeatedObservationInput } from "./repeated-observation.js";
36
36
  export { sourceOfAuthorityObservation, sourceOfAuthorityObservationBuilder, SourceOfAuthorityObservationBuilder, } from "./source-of-authority-observation.js";
37
37
  export type { SourceAuthorityClass, SourceAuthorityMetadata, SourceOfAuthorityObservationBuilderArgs, SourceOfAuthorityObservationInput, } from "./source-of-authority-observation.js";
38
- export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-workbench/review-presentation.js";
39
- export type { ReviewCandidatePresentation, ReviewCandidatePresentationContext, ReviewItemPresentation, ReviewItemPresentationContext, ReviewPresentationAdapter, ReviewPresentationLink, ReviewResultPresentation, ReviewTracePresentationContext, ReviewTraceRef, ReviewValuePresentationContext, } from "./review-workbench/review-presentation.js";
38
+ export { buildInterpretationReadingPresentation, buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-workbench/review-presentation.js";
39
+ export type { InterpretationReadingPresentation, InterpretationReadingSource, ReviewCandidatePresentation, ReviewCandidatePresentationContext, ReviewItemPresentation, ReviewItemPresentationContext, ReviewPresentationAdapter, ReviewPresentationLink, ReviewResultPresentation, ReviewTracePresentationContext, ReviewTraceRef, ReviewValuePresentationContext, } from "./review-workbench/review-presentation.js";
40
40
  export { apiRecordSource, manualEntrySource, policyStandardSource, uploadedDocumentSource, webPageSource, } from "./raw-source.js";
41
41
  export type { ApiRecordSourceInput, ChecksumInput, ManualEntrySourceInput, PolicyStandardMetadata, PolicyStandardSourceInput, RawSourceInput, UploadedDocumentSourceInput, WebPageSourceInput, } from "./raw-source.js";
42
42
  export { applyAutoAcceptPolicy, applyMappingReview, buildMappingReviewItems, lookupMapping, lookupRejectedMapping, normalizeQuestion, proposalsToCandidateSet, referenceMappingProposer, resolveQuestion, } from "./inquiry-mapping.js";
43
- export type { AutoAcceptPolicy, InquiryMapping, MappingProposal, MappingProposer, } from "./inquiry-mapping.js";
43
+ export type { AutoAcceptWarning } from "./producer-profile.js";
44
+ export type { AutoAcceptPolicy, AutoAcceptPolicyOptions, InquiryMapping, MappingProposal, MappingProposer, } from "./inquiry-mapping.js";
44
45
  export { referenceUtteranceExtractor, surveyAgentUtterance, utteranceToSurveyInput, } from "./agent-utterance.js";
45
46
  export type { ExtractedStatement, LocatorResolution, StatementBadge, StatementValueComparison, UtteranceClaimExtractor, UtteranceStatement, UtteranceStatementRecords, UtteranceTrustReport, } from "./agent-utterance.js";
46
47
  export { mappingReviewToSurface, referenceSchemaExtractor, surveySchemaMapping, } from "./schema-mapping.js";
47
- export type { MappingProposalRecord, ReviewedMapping, SchemaMappingExtractor, SchemaMappingOptions, SystemFieldRef, } from "./schema-mapping.js";
48
+ export type { MappingProposalRecord, ReviewedMapping, SchemaMappingExtractor, SchemaMappingOptions, SchemaMappingValue, SystemFieldRef, } from "./schema-mapping.js";
48
49
  export { buildAuthorizedActionAuthorizing, buildPromptRef, isValidAuthorizing, validateAuthorizing } from "./review-authorizing.js";
49
50
  export type { BuildAuthorizedActionAuthorizingInput, BuildPromptRefInput, ReviewAuthorizingIssue, ReviewAuthorizingIssueCode, } from "./review-authorizing.js";
50
51
  export { confidenceBasisForReview, defineProductVocabulary, stableId } from "./vocabulary.js";
package/dist/src/index.js CHANGED
@@ -1,14 +1,14 @@
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";
8
8
  export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
9
9
  export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
10
10
  export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
11
- export { buildSurveyTrustBundle } from "./to-surface.js";
11
+ export { buildSurveyTrustBundle, ReviewAgreementError } from "./to-surface.js";
12
12
  export { buildCanonicalReviewedTrustInput } from "./canonical-reviewed-trust-input.js";
13
13
  export { buildSurveyLearningProjections } from "./learning-projections.js";
14
14
  export { buildReviewedLearningUpdateProposal } from "./learning-update-proposal.js";
@@ -17,7 +17,7 @@ export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalRevi
17
17
  export { fieldObservation } from "./field-observation.js";
18
18
  export { repeatedObservation } from "./repeated-observation.js";
19
19
  export { sourceOfAuthorityObservation, sourceOfAuthorityObservationBuilder, SourceOfAuthorityObservationBuilder, } from "./source-of-authority-observation.js";
20
- export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-workbench/review-presentation.js";
20
+ export { buildInterpretationReadingPresentation, buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-workbench/review-presentation.js";
21
21
  export { apiRecordSource, manualEntrySource, policyStandardSource, uploadedDocumentSource, webPageSource, } from "./raw-source.js";
22
22
  export { applyAutoAcceptPolicy, applyMappingReview, buildMappingReviewItems, lookupMapping, lookupRejectedMapping, normalizeQuestion, proposalsToCandidateSet, referenceMappingProposer, resolveQuestion, } from "./inquiry-mapping.js";
23
23
  export { referenceUtteranceExtractor, surveyAgentUtterance, utteranceToSurveyInput, } from "./agent-utterance.js";
@@ -20,6 +20,7 @@
20
20
  import type { DerivationRule, InquiryRecord, TrustBundle } from "@kontourai/surface";
21
21
  import type { CanonicalClaimTarget } from "@kontourai/surface";
22
22
  import type { Candidate, CandidateSet, ReviewOutcome } from "./types.js";
23
+ import type { AutoAcceptWarning } from "./producer-profile.js";
23
24
  import type { ReviewItem } from "./review-resource.js";
24
25
  /**
25
26
  * A single machine- or human-generated suggestion that a natural-language
@@ -140,18 +141,31 @@ export declare function applyMappingReview(candidateSet: CandidateSet, reviewOut
140
141
  export interface AutoAcceptPolicy {
141
142
  minConfidence: number;
142
143
  }
144
+ /** Optional hooks for {@link applyAutoAcceptPolicy}. */
145
+ export interface AutoAcceptPolicyOptions {
146
+ /**
147
+ * Called once for each proposal the policy refused because its confidence
148
+ * is not a finite number in [0, 1]. The proposal gets no mapping and stays
149
+ * in human review.
150
+ */
151
+ onWarning?: (warning: AutoAcceptWarning) => void;
152
+ }
143
153
  /**
144
154
  * Apply an auto-accept policy to a list of proposals, returning InquiryMappings.
145
155
  *
146
156
  * Proposals at or above minConfidence → status "assumed", withinComfortZone: true
147
157
  * Proposals below minConfidence → return a "needs-review" mapping (not yet durable)
158
+ * Proposals whose confidence is not a finite number in [0, 1] are never
159
+ * auto-accepted. Throws `RangeError` unless `policy.minConfidence` is a finite
160
+ * number in (0, 1]. Refused out-of-range proposals are reported through
161
+ * `options.onWarning`.
148
162
  *
149
163
  * Only non-conflicting proposals are auto-accepted. If proposals disagree, they
150
164
  * need human review regardless of confidence.
151
165
  *
152
166
  * Returns an array of InquiryMappings (only for accepted proposals).
153
167
  */
154
- export declare function applyAutoAcceptPolicy(proposals: MappingProposal[], policy: AutoAcceptPolicy): InquiryMapping[];
168
+ export declare function applyAutoAcceptPolicy(proposals: MappingProposal[], policy: AutoAcceptPolicy, options?: AutoAcceptPolicyOptions): InquiryMapping[];
155
169
  /**
156
170
  * Look up an InquiryMapping for a question by exact normalized-text match.
157
171
  *
@@ -18,7 +18,7 @@
18
18
  * and lives in the flow-agents repo.
19
19
  */
20
20
  import { resolveInquiry } from "@kontourai/surface";
21
- import { evaluateAutoAccept, getProducerProposal, hasCandidateConflict, projectProposalsToCandidateSet, } from "./producer-profile.js";
21
+ import { assertValidAutoAcceptThreshold, evaluateAutoAccept, getProducerProposal, hasCandidateConflict, projectProposalsToCandidateSet, } from "./producer-profile.js";
22
22
  import { reviewResourceApiVersion } from "./review-resource.js";
23
23
  // ---------------------------------------------------------------------------
24
24
  // Question normalization
@@ -140,13 +140,18 @@ export function applyMappingReview(candidateSet, reviewOutcome) {
140
140
  *
141
141
  * Proposals at or above minConfidence → status "assumed", withinComfortZone: true
142
142
  * Proposals below minConfidence → return a "needs-review" mapping (not yet durable)
143
+ * Proposals whose confidence is not a finite number in [0, 1] are never
144
+ * auto-accepted. Throws `RangeError` unless `policy.minConfidence` is a finite
145
+ * number in (0, 1]. Refused out-of-range proposals are reported through
146
+ * `options.onWarning`.
143
147
  *
144
148
  * Only non-conflicting proposals are auto-accepted. If proposals disagree, they
145
149
  * need human review regardless of confidence.
146
150
  *
147
151
  * Returns an array of InquiryMappings (only for accepted proposals).
148
152
  */
149
- export function applyAutoAcceptPolicy(proposals, policy) {
153
+ export function applyAutoAcceptPolicy(proposals, policy, options = {}) {
154
+ assertValidAutoAcceptThreshold(policy.minConfidence);
150
155
  if (proposals.length === 0)
151
156
  return [];
152
157
  // If proposals disagree, none can be auto-accepted
@@ -154,6 +159,9 @@ export function applyAutoAcceptPolicy(proposals, policy) {
154
159
  return [];
155
160
  return proposals.flatMap((proposal) => {
156
161
  const decision = evaluateAutoAccept({ confidence: proposal.confidence, rationale: proposal.rationale, proposedAt: proposal.proposedAt }, false, policy, proposal.proposedAt);
162
+ if (decision.warning) {
163
+ options.onWarning?.({ code: decision.warning, proposalId: proposal.id, confidence: decision.confidence });
164
+ }
157
165
  if (!decision.accepted)
158
166
  return [];
159
167
  return [