@kontourai/survey 1.19.0 → 2.1.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 CHANGED
@@ -103,7 +103,7 @@ One observation, one chain: the page it came from, what the extractor read, who
103
103
 
104
104
  Keep producer operational state outside Survey. Queue status, reviewer form state, retries, source caches, and product policy decisions belong in the producer's own data model. Survey carries only the portable evidence chain records needed by Surface.
105
105
 
106
- Need Anthropic-backed `MappingProposer`/`UtteranceClaimExtractor` implementations for step 1? `@kontourai/survey/anthropic` ships production adapters as proposers-only — every output still goes through the normal review path. It's a separate subpath (not re-exported from the package root) so consumers without `@anthropic-ai/sdk` installed pay nothing.
106
+ Model-backed producers implement `MappingProposer` or `UtteranceClaimExtractor` in the product that owns the prompt, parsing, runtime policy, and domain mapping. Survey accepts their normalized proposals through the same framework-neutral interfaces and review path; it does not acquire credentials or depend on an AI runtime.
107
107
 
108
108
  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.
109
109
 
@@ -22,17 +22,16 @@ import { projectProposalsToCandidateSet } from "./producer-profile.js";
22
22
  /**
23
23
  * The Candidate Conflict comparison key for an utterance-proposed value.
24
24
  *
25
- * Neither extractor (the reference extractor below, nor the Anthropic-backed
26
- * one in `./anthropic.js`) normalizes `value` before it reaches
25
+ * Extractors do not normalize `value` before it reaches
27
26
  * `ExtractedStatement` — case and internal formatting are preserved
28
27
  * verbatim. String values are the only case where "representation noise"
29
28
  * (leading/trailing whitespace from excerpt boundaries, incidental case
30
- * differences like "Healthy" vs "healthy") is plausible given the two
31
- * extractors' actual output, so this key trims + lowercases STRING values
29
+ * differences like "Healthy" vs "healthy") is plausible in producer output,
30
+ * so this key trims + lowercases STRING values
32
31
  * in the COMPARISON KEY ONLY — the stored `Candidate.value`/`Extraction.value`
33
32
  * stay byte-for-byte verbatim; this function only feeds `equivalenceKey`,
34
33
  * never `value`. Non-string values (number, boolean, null — the other types
35
- * the Anthropic tool schema permits) compare via exact canonical
34
+ * supported by the portable record contract) compare via exact canonical
36
35
  * `JSON.stringify`, so there is no cross-type coercion that could silently
37
36
  * equate e.g. "5" and 5, or lose a genuine numeric disagreement (5 vs 6 is
38
37
  * never noise). This mirrors the Producer Profile core's established
@@ -0,0 +1,31 @@
1
+ import type { SurveyInput } from "./types.js";
2
+ import type { ReviewItem } from "./review-resource.js";
3
+ import type { ReviewWorkbenchResult } from "./review-workbench/review-workbench.js";
4
+ export interface BuildCanonicalReviewedTrustInputOptions {
5
+ /** Producer identity for the resulting SurveyInput batch. */
6
+ readonly source: string;
7
+ /** Server-controlled projection time. */
8
+ readonly generatedAt: string;
9
+ /** Stable producer-owned identity for this review projection. */
10
+ readonly projectionContextId: string;
11
+ /** Canonical ReviewItems from the server-owned pre-decision snapshot. */
12
+ readonly items: readonly ReviewItem[];
13
+ /** Results derived by Survey's server apply boundary from snapshot + events. */
14
+ readonly results: readonly ReviewWorkbenchResult[];
15
+ }
16
+ export interface CanonicalReviewedTrustInput {
17
+ readonly surveyInput: SurveyInput;
18
+ /** Pass unchanged to buildSurveyTrustBundle's projectionContextId option. */
19
+ readonly projectionContextId: string;
20
+ }
21
+ /**
22
+ * Projects server-applied review records into the complete SurveyInput consumed
23
+ * by buildSurveyTrustBundle. The ReviewItem and ReviewWorkbenchResult are the
24
+ * authority: callers cannot override status, value, identity, or provenance.
25
+ *
26
+ * The helper deliberately returns the projection context beside SurveyInput
27
+ * because repeated append-only projections need that context at the Surface
28
+ * projection boundary, while SurveyInput's existing byte shape remains
29
+ * unchanged for compatibility.
30
+ */
31
+ export declare function buildCanonicalReviewedTrustInput(options: BuildCanonicalReviewedTrustInputOptions): CanonicalReviewedTrustInput;
@@ -0,0 +1,265 @@
1
+ import { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
2
+ import { canonicalJson } from "./review-workbench/canonical.js";
3
+ import { workbenchDecisionDefinitions } from "./review-workbench/review-queue-session.js";
4
+ /**
5
+ * Projects server-applied review records into the complete SurveyInput consumed
6
+ * by buildSurveyTrustBundle. The ReviewItem and ReviewWorkbenchResult are the
7
+ * authority: callers cannot override status, value, identity, or provenance.
8
+ *
9
+ * The helper deliberately returns the projection context beside SurveyInput
10
+ * because repeated append-only projections need that context at the Surface
11
+ * projection boundary, while SurveyInput's existing byte shape remains
12
+ * unchanged for compatibility.
13
+ */
14
+ export function buildCanonicalReviewedTrustInput(options) {
15
+ requireNonEmpty(options.source, "source");
16
+ requireTimestamp(options.generatedAt, "generatedAt");
17
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(options.projectionContextId)) {
18
+ throw new Error("projectionContextId must be a non-empty portable resource-name fragment.");
19
+ }
20
+ const itemsByName = uniqueBy(options.items, (item) => item.metadata.name, "ReviewItem");
21
+ const resultsByName = uniqueBy(options.results, (result) => result.reviewItemName, "ReviewWorkbenchResult");
22
+ if (itemsByName.size !== resultsByName.size) {
23
+ throw new Error("Canonical review projection requires exactly one resolved result for every ReviewItem.");
24
+ }
25
+ const rawSources = new Map();
26
+ const extractions = new Map();
27
+ const candidateSets = new Map();
28
+ const reviewOutcomes = new Map();
29
+ const claims = new Map();
30
+ for (const item of options.items) {
31
+ const result = resultsByName.get(item.metadata.name);
32
+ if (!result) {
33
+ throw new Error(`ReviewItem ${item.metadata.name} has no canonical server-applied result.`);
34
+ }
35
+ assertCanonicalResult(item, result);
36
+ const candidates = item.spec.candidates.map((candidate) => {
37
+ const records = projectCandidate(item, candidate);
38
+ addConsistent(rawSources, records.rawSource, "raw source");
39
+ addConsistent(extractions, records.extraction, "extraction");
40
+ return records.candidate;
41
+ });
42
+ const selected = item.spec.candidates.find((candidate) => candidate.id === result.selectedCandidateId);
43
+ const selectedRecordId = selected.projection?.candidateId ?? selected.id;
44
+ const candidateSetId = selected.projection?.candidateSetId
45
+ ?? item.spec.projection?.candidateSetId
46
+ ?? `${item.metadata.name}.candidates`;
47
+ if (candidates.some((candidate) => candidate.metadata?.candidateSetId !== candidateSetId)) {
48
+ throw new Error(`ReviewItem ${item.metadata.name} carries conflicting candidate-set projection ids.`);
49
+ }
50
+ const candidateSet = {
51
+ id: candidateSetId,
52
+ target: item.spec.target,
53
+ candidates: candidates.map(({ metadata, ...candidate }) => ({
54
+ ...candidate,
55
+ ...(metadata && Object.keys(metadata).length > 1
56
+ ? { metadata: Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== "candidateSetId")) }
57
+ : {}),
58
+ })),
59
+ selectedCandidateId: selectedRecordId,
60
+ status: result.decision === "could-not-confirm"
61
+ ? (item.spec.candidateSetStatus ?? "needs-review")
62
+ : "resolved",
63
+ ...(result.rationale ?? item.spec.rationale
64
+ ? { rationale: result.rationale ?? item.spec.rationale }
65
+ : {}),
66
+ };
67
+ addConsistent(candidateSets, candidateSet, "candidate set");
68
+ const decision = result.reviewDecision.spec;
69
+ const reviewOutcomeId = decision.projection?.reviewOutcomeId
70
+ ?? selected.projection?.reviewOutcomeId
71
+ ?? `${item.metadata.name}.${result.decision}.review-outcome`;
72
+ const reviewOutcome = {
73
+ id: reviewOutcomeId,
74
+ candidateSetId,
75
+ candidateId: selectedRecordId,
76
+ status: result.status,
77
+ ...(decision.resolution ? { resolution: decision.resolution } : {}),
78
+ ...(decision.resolutionReason ? { resolutionReason: decision.resolutionReason } : {}),
79
+ ...(decision.attemptEvidenceIds?.length ? { attemptEvidenceIds: [...decision.attemptEvidenceIds] } : {}),
80
+ ...(decision.actor?.id ? { actor: decision.actor.id } : {}),
81
+ ...(decision.reviewedAt ? { reviewedAt: decision.reviewedAt } : {}),
82
+ ...(decision.rationale ? { rationale: decision.rationale } : {}),
83
+ ...(decision.evidenceIds?.length ? { evidenceIds: [...decision.evidenceIds] } : {}),
84
+ ...(decision.withinComfortZone !== undefined ? { withinComfortZone: decision.withinComfortZone } : {}),
85
+ ...(decision.comfortZoneNote ? { comfortZoneNote: decision.comfortZoneNote } : {}),
86
+ ...(decision.authorizing ? { authorizing: decision.authorizing } : {}),
87
+ metadata: {
88
+ workbenchDecision: result.decision,
89
+ ...(result.editedValue !== undefined ? { editedValue: result.editedValue } : {}),
90
+ },
91
+ };
92
+ addConsistent(reviewOutcomes, reviewOutcome, "review outcome");
93
+ const hint = selected.claimTarget;
94
+ assertSingleProjectionId("claim", item.metadata.name, [
95
+ decision.projection?.claimId,
96
+ item.spec.projection?.claimId,
97
+ ...item.spec.candidates.flatMap((candidate) => [candidate.projection?.claimId, candidate.claimTarget.claimId]),
98
+ ]);
99
+ const claimId = decision.projection?.claimId
100
+ ?? selected.projection?.claimId
101
+ ?? item.spec.projection?.claimId
102
+ ?? hint.claimId
103
+ ?? `${item.metadata.name}.claim`;
104
+ const claim = {
105
+ id: claimId,
106
+ candidateSetId,
107
+ candidateId: selectedRecordId,
108
+ subjectType: hint.subjectType,
109
+ subjectId: hint.subjectId,
110
+ facet: hint.facet,
111
+ claimType: hint.claimType,
112
+ fieldOrBehavior: hint.fieldOrBehavior,
113
+ value: result.effectiveValue,
114
+ status: result.status,
115
+ impactLevel: hint.impactLevel,
116
+ updatedAt: decision.reviewedAt ?? options.generatedAt,
117
+ ...(hint.evidenceType ? { evidenceType: hint.evidenceType } : {}),
118
+ ...(hint.evidenceMethod ? { evidenceMethod: hint.evidenceMethod } : {}),
119
+ ...(hint.derivedFrom ? { derivedFrom: [...hint.derivedFrom] } : {}),
120
+ collectedBy: hint.collectedBy ?? selected.extraction.extractor ?? options.source,
121
+ ...(decision.actor?.id ? { actor: decision.actor.id } : {}),
122
+ };
123
+ addConsistent(claims, claim, "claim target");
124
+ }
125
+ return {
126
+ projectionContextId: options.projectionContextId,
127
+ surveyInput: {
128
+ contractVersion: SURVEY_INPUT_CONTRACT_VERSION,
129
+ source: options.source,
130
+ generatedAt: options.generatedAt,
131
+ rawSources: [...rawSources.values()],
132
+ extractions: [...extractions.values()],
133
+ candidateSets: [...candidateSets.values()],
134
+ reviewOutcomes: [...reviewOutcomes.values()],
135
+ claims: [...claims.values()],
136
+ },
137
+ };
138
+ }
139
+ function assertCanonicalResult(item, result) {
140
+ const selected = item.spec.candidates.find((candidate) => candidate.id === result.selectedCandidateId);
141
+ if (!selected) {
142
+ throw new Error(`Review result ${result.reviewItemName} selects an unknown candidate.`);
143
+ }
144
+ if (canonicalJson(selected) !== canonicalJson(result.selectedCandidate)) {
145
+ throw new Error(`Review result ${result.reviewItemName} selected candidate does not match its canonical ReviewItem.`);
146
+ }
147
+ const unselected = item.spec.candidates.filter((candidate) => candidate.id !== selected.id);
148
+ if (canonicalJson(unselected) !== canonicalJson(result.unselectedCandidates)) {
149
+ throw new Error(`Review result ${result.reviewItemName} unselected candidates do not match its canonical ReviewItem.`);
150
+ }
151
+ if (result.selectedCandidateRole !== selected.role || canonicalJson(result.selectedValue) !== canonicalJson(selected.value)) {
152
+ throw new Error(`Review result ${result.reviewItemName} selected identity does not match its canonical ReviewItem.`);
153
+ }
154
+ const decision = result.reviewDecision.spec;
155
+ const definition = workbenchDecisionDefinitions[result.decision];
156
+ if (decision.reviewItemName !== item.metadata.name
157
+ || decision.candidateId !== result.selectedCandidateId
158
+ || decision.status !== result.status
159
+ || decision.status !== definition.status
160
+ || decision.rationale !== result.rationale
161
+ || canonicalJson(decision.editedValue) !== canonicalJson(result.editedValue)) {
162
+ throw new Error(`Review result ${result.reviewItemName} contradicts its canonical ReviewDecision.`);
163
+ }
164
+ if ((result.decision === "could-not-confirm") !== (decision.resolution === "could_not_confirm")) {
165
+ throw new Error(`Review result ${result.reviewItemName} contradicts its canonical review resolution.`);
166
+ }
167
+ const expectedEffective = result.editedValue !== undefined && result.decision === "accept-proposed"
168
+ ? result.editedValue
169
+ : selected.value;
170
+ if (canonicalJson(expectedEffective) !== canonicalJson(result.effectiveValue)) {
171
+ throw new Error(`Review result ${result.reviewItemName} effective value is not canonical.`);
172
+ }
173
+ for (const candidate of item.spec.candidates) {
174
+ if (canonicalJson(claimTargetIdentity(candidate.claimTarget)) !== canonicalJson(claimTargetIdentity(selected.claimTarget))) {
175
+ throw new Error(`ReviewItem ${item.metadata.name} candidates carry conflicting claim targets.`);
176
+ }
177
+ }
178
+ }
179
+ function claimTargetIdentity(target) {
180
+ const { claimId: _claimId, ...identity } = target;
181
+ return identity;
182
+ }
183
+ function projectCandidate(item, input) {
184
+ const rawSourceId = input.projection?.rawSourceId ?? input.source.sourceId ?? `${item.metadata.name}.${input.id}.source`;
185
+ const extractionId = input.projection?.extractionId ?? input.extraction.extractionId ?? `${item.metadata.name}.${input.id}.extraction`;
186
+ const candidateSetId = input.projection?.candidateSetId ?? item.spec.projection?.candidateSetId ?? `${item.metadata.name}.candidates`;
187
+ const sourceKind = input.source.kind;
188
+ const observedAt = input.source.observedAt;
189
+ const locatorScheme = input.source.locatorScheme ?? input.locator?.scheme;
190
+ const extractor = input.extraction.extractor;
191
+ const extractedAt = input.extraction.extractedAt;
192
+ if (!sourceKind || !observedAt || !locatorScheme || !extractor || !extractedAt) {
193
+ throw new Error(`ReviewCandidate ${input.id} lacks source or extraction provenance required for TrustInput projection.`);
194
+ }
195
+ const rawSource = {
196
+ id: rawSourceId,
197
+ kind: sourceKind,
198
+ sourceRef: input.source.sourceRef,
199
+ observedAt,
200
+ ...(input.source.fetchedAt ? { fetchedAt: input.source.fetchedAt } : {}),
201
+ ...(input.source.checksum ? { checksum: input.source.checksum } : {}),
202
+ locatorScheme,
203
+ };
204
+ const extraction = {
205
+ id: extractionId,
206
+ sourceId: rawSourceId,
207
+ target: input.extraction.target,
208
+ value: input.value,
209
+ ...(input.extraction.confidence ?? input.confidence) !== undefined
210
+ ? { confidence: input.extraction.confidence ?? input.confidence }
211
+ : {},
212
+ ...(input.locator?.locator ? { locator: input.locator.locator } : {}),
213
+ ...(input.locator?.excerpt ? { excerpt: input.locator.excerpt } : {}),
214
+ extractor,
215
+ extractedAt,
216
+ ...(input.extraction.model ? { metadata: { model: input.extraction.model } } : {}),
217
+ };
218
+ const candidate = {
219
+ id: input.projection?.candidateId ?? input.id,
220
+ extractionId,
221
+ value: input.value,
222
+ ...(input.confidence !== undefined ? { confidence: input.confidence } : {}),
223
+ ...(input.sourceRank !== undefined ? { sourceRank: input.sourceRank } : {}),
224
+ ...(input.rejectionReason ? { rejectionReason: input.rejectionReason } : {}),
225
+ metadata: {
226
+ candidateSetId,
227
+ ...(input.role ? { role: input.role } : {}),
228
+ ...(input.producer ? { producer: input.producer } : {}),
229
+ },
230
+ };
231
+ return { rawSource, extraction, candidate };
232
+ }
233
+ function uniqueBy(values, id, label) {
234
+ const output = new Map();
235
+ for (const value of values) {
236
+ const key = requireNonEmpty(id(value), `${label} identity`);
237
+ if (output.has(key)) {
238
+ throw new Error(`Canonical review projection received duplicate ${label} ${key}.`);
239
+ }
240
+ output.set(key, value);
241
+ }
242
+ return output;
243
+ }
244
+ function addConsistent(records, value, label) {
245
+ const existing = records.get(value.id);
246
+ if (existing && canonicalJson(existing) !== canonicalJson(value)) {
247
+ throw new Error(`Canonical review projection found conflicting ${label} ${value.id}.`);
248
+ }
249
+ records.set(value.id, value);
250
+ }
251
+ function requireNonEmpty(value, label) {
252
+ if (!value.trim())
253
+ throw new Error(`${label} must be non-empty.`);
254
+ return value;
255
+ }
256
+ function requireTimestamp(value, label) {
257
+ if (!value.trim() || Number.isNaN(Date.parse(value)))
258
+ throw new Error(`${label} must be an ISO timestamp.`);
259
+ }
260
+ function assertSingleProjectionId(label, itemName, values) {
261
+ const ids = new Set(values.filter((value) => value !== undefined));
262
+ if (ids.size > 1) {
263
+ throw new Error(`ReviewItem ${itemName} carries conflicting ${label} projection ids.`);
264
+ }
265
+ }
@@ -1,5 +1,6 @@
1
1
  import type { RawSource } from "./types.js";
2
2
  import type { ClaimTargetHint, ReviewItem, ReviewValueType } from "./review-resource.js";
3
+ import { type PortablePdfLayout } from "./pdf-layout.js";
3
4
  /** The upstream-owned portable extraction-result wire identifiers accepted by this adapter. */
4
5
  export declare const portableExtractionResultFormat = "traverse-extraction-result";
5
6
  export declare const portableExtractionResultVersion = 1;
@@ -96,6 +97,7 @@ export interface PortableExtractionResultEnvelope {
96
97
  taskDigest?: string;
97
98
  exampleDigests?: string[];
98
99
  pdfPageOffsets?: number[];
100
+ pdfLayout?: PortablePdfLayout;
99
101
  ocrDerived?: true;
100
102
  preparedArtifact?: {
101
103
  format: "traverse-prepared-artifact";
@@ -1,6 +1,7 @@
1
1
  import { sha256Hex } from "./sha256.js";
2
2
  import { reviewResourceApiVersion } from "./review-resource.js";
3
3
  import { canonicalJson } from "./review-workbench/canonical.js";
4
+ import { validatePortablePdfLayout } from "./pdf-layout.js";
4
5
  /** The upstream-owned portable extraction-result wire identifiers accepted by this adapter. */
5
6
  export const portableExtractionResultFormat = "traverse-extraction-result";
6
7
  export const portableExtractionResultVersion = 1;
@@ -119,12 +120,14 @@ function buildReviewItem(record, proposal, index) {
119
120
  }
120
121
  function identityInputs(record, proposal, index) {
121
122
  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
+ preparedArtifact: record.spec.envelope.result.preparedArtifact, pdfLayout: record.spec.envelope.result.pdfLayout,
124
+ runId: record.spec.envelope.result.runId,
123
125
  proposalIndex: index, proposal, claimTarget: record.spec.claimTargets[index] };
124
126
  }
125
127
  function evidenceInputs(record, proposal) {
126
128
  return { producerNamespace: record.metadata.producerNamespace, importName: record.metadata.name, source: record.spec.envelope.source,
127
129
  sourceKind: record.spec.sourceKind, preparedArtifact: record.spec.envelope.result.preparedArtifact,
130
+ pdfLayout: record.spec.envelope.result.pdfLayout,
128
131
  extractedAt: record.spec.envelope.result.extractedAt,
129
132
  provenance: proposal.provenance };
130
133
  }
@@ -182,7 +185,7 @@ function validateEnvelope(input) {
182
185
  if (source.snapshotRef !== undefined)
183
186
  safeReference(source.snapshotRef, "source.snapshotRef");
184
187
  const r = obj(e.result, "result");
185
- exact(r, ["proposals", "provider", "runId", "raw", "outcome", "extractedAt", "providerCalls", "totalTokensUsed"], "result", ["model", "warningClassifications", "partial", "providerFailures", "taskDigest", "exampleDigests", "pdfPageOffsets", "ocrDerived", "preparedArtifact", "preparedArtifactState"]);
188
+ exact(r, ["proposals", "provider", "runId", "raw", "outcome", "extractedAt", "providerCalls", "totalTokensUsed"], "result", ["model", "warningClassifications", "partial", "providerFailures", "taskDigest", "exampleDigests", "pdfPageOffsets", "pdfLayout", "ocrDerived", "preparedArtifact", "preparedArtifactState"]);
186
189
  stableIdentity(r.provider, "result.provider");
187
190
  if (r.model !== undefined)
188
191
  stableIdentity(r.model, "result.model");
@@ -212,11 +215,16 @@ function validateEnvelope(input) {
212
215
  if (r.ocrDerived !== undefined && r.ocrDerived !== true)
213
216
  throw new Error("result.ocrDerived must be true.");
214
217
  const artifact = r.preparedArtifact === undefined ? undefined : validateArtifact(r.preparedArtifact);
218
+ const pdfLayout = r.pdfLayout === undefined
219
+ ? undefined
220
+ : artifact === undefined
221
+ ? (() => { throw new Error("result.pdfLayout requires result.preparedArtifact."); })()
222
+ : validatePortablePdfLayout(r.pdfLayout, artifact.contentLength);
215
223
  const state = r.preparedArtifactState === undefined ? undefined : validateArtifactState(r.preparedArtifactState, artifact);
216
224
  if (source.snapshotRef !== undefined && artifact?.sourceSnapshotRef !== undefined && source.snapshotRef !== artifact.sourceSnapshotRef)
217
225
  throw new Error("Source snapshot identity mismatch.");
218
226
  const proposals = array(r.proposals, "result.proposals").map((p, i) => validateProposal(p, i, artifact?.contentLength));
219
- return cloneJson({ ...e, source, result: { ...r, proposals, ...(artifact ? { preparedArtifact: artifact } : {}), ...(state ? { preparedArtifactState: state } : {}) } });
227
+ return cloneJson({ ...e, source, result: { ...r, proposals, ...(pdfLayout ? { pdfLayout } : {}), ...(artifact ? { preparedArtifact: artifact } : {}), ...(state ? { preparedArtifactState: state } : {}) } });
220
228
  }
221
229
  function validateProposal(input, index, contentLength) {
222
230
  const p = obj(input, `proposal[${index}]`);
@@ -123,6 +123,23 @@ export interface RejectExtractionImprovementProposalInput {
123
123
  }
124
124
  /** One producer/store disposition is allowed for each shared dispositionKey. */
125
125
  export type ExtractionImprovementDisposition = ExtractionImprovementActivationRequest | ExtractionImprovementRejection;
126
+ export interface ExtractionImprovementDispositionConflict {
127
+ kind: "survey.extraction-improvement-disposition-conflict";
128
+ dispositionKey: string;
129
+ /** Canonically ordered distinct records. The fold never selects a winner. */
130
+ dispositions: readonly ExtractionImprovementDisposition[];
131
+ }
132
+ export interface FoldExtractionImprovementDispositionsResult {
133
+ /** One canonically ordered record for each uncontested disposition key. */
134
+ dispositions: readonly ExtractionImprovementDisposition[];
135
+ /** Canonically ordered conflicts for keys with more than one distinct record. */
136
+ conflicts: readonly ExtractionImprovementDispositionConflict[];
137
+ }
138
+ /**
139
+ * Folds producer-owned disposition records without performing I/O or selecting
140
+ * between conflicting decisions. Byte-equivalent replay is idempotent.
141
+ */
142
+ export declare function foldExtractionImprovementDispositions(input: readonly ExtractionImprovementDisposition[]): FoldExtractionImprovementDispositionsResult;
126
143
  /** Builds a frozen draft from validated canonical records. Performs no I/O. */
127
144
  export declare function buildExtractionImprovementProposal(input: BuildExtractionImprovementProposalInput): ExtractionImprovementDraft;
128
145
  /** Emits a reversible activation request, never an activation. */
@@ -2,6 +2,39 @@ import { createHash } from "node:crypto";
2
2
  import { buildReviewItemsFromExtractionEnvelopeImport } from "./extraction-envelope.js";
3
3
  import { reviewResourceApiVersion } from "./review-resource.js";
4
4
  import { canonicalJson } from "./review-workbench/canonical.js";
5
+ /**
6
+ * Folds producer-owned disposition records without performing I/O or selecting
7
+ * between conflicting decisions. Byte-equivalent replay is idempotent.
8
+ */
9
+ export function foldExtractionImprovementDispositions(input) {
10
+ const byKey = new Map();
11
+ for (const disposition of input) {
12
+ const records = byKey.get(disposition.dispositionKey) ?? new Map();
13
+ records.set(canonicalJson(disposition), disposition);
14
+ byKey.set(disposition.dispositionKey, records);
15
+ }
16
+ const dispositions = [];
17
+ const conflicts = [];
18
+ for (const dispositionKey of [...byKey.keys()].sort()) {
19
+ const records = [...byKey.get(dispositionKey).entries()]
20
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
21
+ .map(([, disposition]) => disposition);
22
+ if (records.length === 1) {
23
+ dispositions.push(records[0]);
24
+ }
25
+ else {
26
+ conflicts.push(Object.freeze({
27
+ kind: "survey.extraction-improvement-disposition-conflict",
28
+ dispositionKey,
29
+ dispositions: Object.freeze(records),
30
+ }));
31
+ }
32
+ }
33
+ return Object.freeze({
34
+ dispositions: Object.freeze(dispositions),
35
+ conflicts: Object.freeze(conflicts),
36
+ });
37
+ }
5
38
  /** Builds a frozen draft from validated canonical records. Performs no I/O. */
6
39
  export function buildExtractionImprovementProposal(input) {
7
40
  const diagnosis = normalizeDiagnosis(input.diagnosis);
@@ -3,6 +3,8 @@ 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
5
  export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
6
+ export { resolvePortablePdfRegion } from "./pdf-layout.js";
7
+ export type { PortablePdfBoundingBox, PortablePdfLayout, PortablePdfPageGeometry, PortablePdfRegionContext, PortablePdfTable, PortablePdfTableCell, PortablePdfTextElement, PortablePdfTextRange, } from "./pdf-layout.js";
6
8
  export type { ExtractionAlignmentState, ArtifactUnavailableCode, ExtractionInspectorCandidate, ExtractionInspectorEntry, ExtractionInspectorExportOptions, ExtractionInspectorFilters, ExtractionInspectorInput, ExtractionInspectorModel, ExtractionInspectorSource, ResolvedExtractionArtifact, } from "./review-workbench/extraction-inspector.js";
7
9
  export type { ExtractionEnvelopeImport, ExtractionEnvelopeImportDiagnostic, ExtractionEnvelopeImportOptions, ExtractionEnvelopeImportResult, ExtractionEnvelopeResolutionIdentity, PortableExtractionOccurrence, PortableExtractionProposal, PortableExtractionResultEnvelope, PortablePreparedArtifactState, } from "./extraction-envelope.js";
8
10
  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";
@@ -14,12 +16,14 @@ export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-r
14
16
  export type { CurrentProposedCandidateRole, ReviewedCurrentProposedResolutionInput, } from "./reviewed-current-proposed-resolution.js";
15
17
  export { buildSurveyTrustBundle } from "./to-surface.js";
16
18
  export type { BuildSurveyTrustBundleOptions } from "./to-surface.js";
19
+ export { buildCanonicalReviewedTrustInput } from "./canonical-reviewed-trust-input.js";
20
+ export type { BuildCanonicalReviewedTrustInputOptions, CanonicalReviewedTrustInput, } from "./canonical-reviewed-trust-input.js";
17
21
  export { buildSurveyLearningProjections } from "./learning-projections.js";
18
22
  export type { LearningProjection, LearningProjectionKind, LearningProjectionSeverity, LearningProjectionSignal, } from "./learning-projections.js";
19
23
  export { buildReviewedLearningUpdateProposal } from "./learning-update-proposal.js";
20
24
  export type { LearningUpdateEvidenceReference, LearningUpdateProposal, OpaqueEvidenceReference, ProvenanceReference, ReviewedLearningUpdateProposalInput, ReviewProofReference, } from "./learning-update-proposal.js";
21
- export { approveExtractionImprovementProposal, buildExtractionImprovementProposal, rejectExtractionImprovementProposal, } from "./extraction-improvement-proposal.js";
22
- export type { ApproveExtractionImprovementProposalInput, AcceptedExtractionDiagnosis, BadExtractionDiagnosis, BuildExtractionImprovementProposalInput, ExtractionImprovementActivationRequest, ExtractionImprovementDiagnosis, ExtractionImprovementDisposition, ExtractionImprovementDraft, ExtractionImprovementLineage, ExtractionImprovementRejection, ExtractionImprovementRecordDigests, ExtractionImprovementReview, ExtractionTaskSpecReference, InsufficientSourceEvidenceDiagnosis, ProducerApproval, RejectExtractionImprovementProposalInput, } from "./extraction-improvement-proposal.js";
25
+ export { approveExtractionImprovementProposal, buildExtractionImprovementProposal, foldExtractionImprovementDispositions, rejectExtractionImprovementProposal, } from "./extraction-improvement-proposal.js";
26
+ export type { ApproveExtractionImprovementProposalInput, AcceptedExtractionDiagnosis, BadExtractionDiagnosis, BuildExtractionImprovementProposalInput, ExtractionImprovementActivationRequest, ExtractionImprovementDiagnosis, ExtractionImprovementDisposition, ExtractionImprovementDispositionConflict, ExtractionImprovementDraft, ExtractionImprovementLineage, ExtractionImprovementRejection, ExtractionImprovementRecordDigests, ExtractionImprovementReview, ExtractionTaskSpecReference, FoldExtractionImprovementDispositionsResult, InsufficientSourceEvidenceDiagnosis, ProducerApproval, RejectExtractionImprovementProposalInput, } from "./extraction-improvement-proposal.js";
23
27
  export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalReviewProofJson, hashCanonicalReviewProofPayload, verifyCanonicalReviewProofPayload, REVIEW_PROOF_CONTRACT_VERSION, REVIEW_PROOF_PACKAGE_NAME, REVIEW_PROOF_SCHEMA, REVIEW_PROOF_SCHEMA_VERSION, } from "./review-proof.js";
24
28
  export type { CanonicalReviewProofPayload, CanonicalReviewProofPayloadV1, CanonicalReviewProofPayloadV2, ReviewProofInput, } from "./review-proof.js";
25
29
  export { fieldObservation } from "./field-observation.js";
package/dist/src/index.js CHANGED
@@ -2,13 +2,15 @@ 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
4
  export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
5
+ export { resolvePortablePdfRegion } from "./pdf-layout.js";
5
6
  export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
6
7
  export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
7
8
  export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
8
9
  export { buildSurveyTrustBundle } from "./to-surface.js";
10
+ export { buildCanonicalReviewedTrustInput } from "./canonical-reviewed-trust-input.js";
9
11
  export { buildSurveyLearningProjections } from "./learning-projections.js";
10
12
  export { buildReviewedLearningUpdateProposal } from "./learning-update-proposal.js";
11
- export { approveExtractionImprovementProposal, buildExtractionImprovementProposal, rejectExtractionImprovementProposal, } from "./extraction-improvement-proposal.js";
13
+ export { approveExtractionImprovementProposal, buildExtractionImprovementProposal, foldExtractionImprovementDispositions, rejectExtractionImprovementProposal, } from "./extraction-improvement-proposal.js";
12
14
  export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalReviewProofJson, hashCanonicalReviewProofPayload, verifyCanonicalReviewProofPayload, REVIEW_PROOF_CONTRACT_VERSION, REVIEW_PROOF_PACKAGE_NAME, REVIEW_PROOF_SCHEMA, REVIEW_PROOF_SCHEMA_VERSION, } from "./review-proof.js";
13
15
  export { fieldObservation } from "./field-observation.js";
14
16
  export { repeatedObservation } from "./repeated-observation.js";
@@ -0,0 +1,53 @@
1
+ export interface PortablePdfTextRange {
2
+ start: number;
3
+ end: number;
4
+ }
5
+ export interface PortablePdfBoundingBox {
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ }
11
+ export interface PortablePdfPageGeometry {
12
+ pageNumber: number;
13
+ width: number;
14
+ height: number;
15
+ unit: "points" | "pixels" | "normalized";
16
+ rotation?: 0 | 90 | 180 | 270;
17
+ }
18
+ export interface PortablePdfTextElement {
19
+ kind: "heading" | "paragraph" | "list" | "table" | "table-cell" | "figure" | "other";
20
+ providerType?: string;
21
+ pageNumber: number;
22
+ range: PortablePdfTextRange;
23
+ bounds?: PortablePdfBoundingBox;
24
+ }
25
+ export interface PortablePdfTableCell {
26
+ rowIndex: number;
27
+ columnIndex: number;
28
+ rowSpan?: number;
29
+ columnSpan?: number;
30
+ range: PortablePdfTextRange;
31
+ bounds?: PortablePdfBoundingBox;
32
+ }
33
+ export interface PortablePdfTable {
34
+ pageNumber: number;
35
+ bounds?: PortablePdfBoundingBox;
36
+ cells: PortablePdfTableCell[];
37
+ }
38
+ export interface PortablePdfLayout {
39
+ pages?: PortablePdfPageGeometry[];
40
+ elements: PortablePdfTextElement[];
41
+ tables?: PortablePdfTable[];
42
+ }
43
+ export interface PortablePdfRegionContext {
44
+ pages: number[];
45
+ elements: PortablePdfTextElement[];
46
+ tableCells: Array<{
47
+ pageNumber: number;
48
+ tableIndex: number;
49
+ cell: PortablePdfTableCell;
50
+ }>;
51
+ }
52
+ export declare function validatePortablePdfLayout(value: unknown, textLength: number): PortablePdfLayout;
53
+ export declare function resolvePortablePdfRegion(layout: PortablePdfLayout, locator: string): PortablePdfRegionContext | undefined;