@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 +1 -1
- package/dist/src/agent-utterance.js +4 -5
- package/dist/src/canonical-reviewed-trust-input.d.ts +31 -0
- package/dist/src/canonical-reviewed-trust-input.js +265 -0
- package/dist/src/extraction-envelope.d.ts +2 -0
- package/dist/src/extraction-envelope.js +11 -3
- package/dist/src/extraction-improvement-proposal.d.ts +17 -0
- package/dist/src/extraction-improvement-proposal.js +33 -0
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.js +3 -1
- package/dist/src/pdf-layout.d.ts +53 -0
- package/dist/src/pdf-layout.js +201 -0
- package/dist/src/review-workbench/extraction-inspector.d.ts +4 -0
- package/dist/src/review-workbench/extraction-inspector.js +43 -8
- package/package.json +5 -13
- package/dist/src/anthropic.d.ts +0 -104
- package/dist/src/anthropic.js +0 -383
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
|
-
|
|
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
|
-
*
|
|
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
|
|
31
|
-
*
|
|
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
|
|
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,
|
|
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);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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;
|