@kontourai/survey 2.2.3 → 2.3.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.
@@ -5,7 +5,7 @@ export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeR
5
5
  export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
6
6
  export { resolvePortablePdfRegion } from "./pdf-layout.js";
7
7
  export type { PortablePdfBoundingBox, PortablePdfLayout, PortablePdfPageGeometry, PortablePdfRegionContext, PortablePdfTable, PortablePdfTableCell, PortablePdfTextElement, PortablePdfTextRange, } from "./pdf-layout.js";
8
- export type { ExtractionAlignmentState, ArtifactUnavailableCode, ExtractionInspectorCandidate, ExtractionInspectorEntry, ExtractionInspectorExportOptions, ExtractionInspectorFilters, ExtractionInspectorInput, ExtractionInspectorModel, ExtractionInspectorSource, ResolvedExtractionArtifact, } from "./review-workbench/extraction-inspector.js";
8
+ export type { ExtractionAlignmentState, ArtifactUnavailableCode, BuiltExtractionInspectorCandidate, BuiltExtractionInspectorModel, ExtractionInspectorCandidate, ExtractionInspectorEntry, ExtractionInspectorExportOptions, ExtractionInspectorFilters, ExtractionInspectorInput, ExtractionInspectorModel, ExtractionInspectorSource, ResolvedExtractionArtifact, } from "./review-workbench/extraction-inspector.js";
9
9
  export type { ExtractionEnvelopeImport, ExtractionEnvelopeImportDiagnostic, ExtractionEnvelopeImportOptions, ExtractionEnvelopeImportResult, ExtractionEnvelopeResolutionIdentity, PortableExtractionOccurrence, PortableExtractionProposal, PortableExtractionResultEnvelope, PortablePreparedArtifactState, } from "./extraction-envelope.js";
10
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";
11
11
  export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
@@ -231,3 +231,16 @@ export interface ReviewSessionEventStatus {
231
231
  export type ReviewSession = ResourceEnvelope<"ReviewSession", ReviewSessionSpec, ReviewSessionStatus>;
232
232
  export type ReviewSessionEvent = ResourceEnvelope<"ReviewSessionEvent", ReviewSessionEventSpec, ReviewSessionEventStatus>;
233
233
  export type ReviewResource = ReviewItem | ReviewDecision | ReviewSession | ReviewSessionEvent;
234
+ /**
235
+ * The one candidate carrying `candidateId`, or `undefined` if the item has none.
236
+ *
237
+ * Fails closed when two carry it. A `ReviewDecision` references its candidate by
238
+ * id and nothing else, so unlike a DOM binding there is no second handle to
239
+ * route on: if the id is ambiguous, the decision itself is undecidable, and
240
+ * picking the first match would project one candidate's value under another's
241
+ * decision. `ReviewItem` has never validated candidate-id uniqueness — the
242
+ * Surface record builder does (`assertUniqueCandidateIds`), but a
243
+ * caller-authored ReviewItem reaches the workbench without passing through it.
244
+ */
245
+ export declare function assertSoleCandidateId(item: ReviewItem, candidateId: string): void;
246
+ export declare function findSoleCandidateById(item: ReviewItem, candidateId: string): ReviewCandidate | undefined;
@@ -1 +1,22 @@
1
1
  export const reviewResourceApiVersion = "survey.kontourai.io/v1alpha1";
2
+ /**
3
+ * The one candidate carrying `candidateId`, or `undefined` if the item has none.
4
+ *
5
+ * Fails closed when two carry it. A `ReviewDecision` references its candidate by
6
+ * id and nothing else, so unlike a DOM binding there is no second handle to
7
+ * route on: if the id is ambiguous, the decision itself is undecidable, and
8
+ * picking the first match would project one candidate's value under another's
9
+ * decision. `ReviewItem` has never validated candidate-id uniqueness — the
10
+ * Surface record builder does (`assertUniqueCandidateIds`), but a
11
+ * caller-authored ReviewItem reaches the workbench without passing through it.
12
+ */
13
+ export function assertSoleCandidateId(item, candidateId) {
14
+ const matches = item.spec.candidates.filter((candidate) => candidate.id === candidateId);
15
+ if (matches.length > 1) {
16
+ throw new Error(`ReviewItem ${item.metadata.name} has ${matches.length} candidates with id ${candidateId}; candidate ids must be unique.`);
17
+ }
18
+ }
19
+ export function findSoleCandidateById(item, candidateId) {
20
+ assertSoleCandidateId(item, candidateId);
21
+ return item.spec.candidates.find((candidate) => candidate.id === candidateId);
22
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The AUDIT DETAILS row contract.
3
+ *
4
+ * Two separate concerns live here, and they are not the same thing:
5
+ *
6
+ * 1. **Naming.** Every row inside a field card's AUDIT DETAILS carries
7
+ * `data-audit-row="<key>"` — a stable machine name a host can select on.
8
+ * Row *labels* are display copy and change; these keys do not. A host that
9
+ * wants to restyle, reorder, or suppress a row on its own surface selects on
10
+ * the key. Deriving a selector from label text is not a supported way to
11
+ * address a row, and this attribute exists so nobody has to.
12
+ *
13
+ * 2. **Not printing the same fact twice.** Survey renders a card's identifiers
14
+ * and provenance in more than one place (the ID stack, the Raw Source
15
+ * section, each section's "IDs and trace links"). Where those placements
16
+ * carry the *same fact*, the card printed one value two or three times under
17
+ * different labels. {@link AuditFactTrace} keeps the first placement and
18
+ * drops the later ones. That is Survey's job, not a consumer's — see
19
+ * docs/consumer-integration-guide.md.
20
+ */
21
+ /**
22
+ * PUBLIC CONTRACT — every `data-audit-row` key Survey emits.
23
+ *
24
+ * Additive by policy: a key may be added, and a row may stop being emitted when
25
+ * it is a duplicate placement or a constant, but a key is never renamed or
26
+ * repointed at a different fact. Hosts may hold selectors against these values.
27
+ */
28
+ export declare const reviewAuditRowKeys: readonly ["current-candidate-id", "proposed-candidate-id", "claim-id", "raw-source-id", "locator", "model", "extractor", "extracted-at", "history-value", "candidate-id", "source-reference", "excerpt", "observed", "source-authority-class", "declared-by", "authority-scope", "extraction-id", "actor", "reviewed-at", "status", "rationale", "outcome", "checksum", "candidate-set-id", "authority-trace-status", "authority-trace-detail"];
29
+ /** One of {@link reviewAuditRowKeys}. */
30
+ export type ReviewAuditRowKey = (typeof reviewAuditRowKeys)[number];
31
+ /**
32
+ * The identity of a *fact*, not of a string: which record a value came from and
33
+ * which property of it. Two placements share an identity only when they are the
34
+ * same property of the same record — which is exactly when printing both says
35
+ * nothing the first one did not.
36
+ *
37
+ * Matching on the rendered value instead conflates facts that merely coincide.
38
+ * A candidate whose `extraction.extractedAt` equals its `source.observedAt` —
39
+ * the stock fixture's shape, and a common one — would lose its `Observed` row
40
+ * entirely, and an auditor reading the card could not tell that field had ever
41
+ * existed. The labels differing is the signal that the facts differ.
42
+ */
43
+ export interface AuditFactId {
44
+ /** The record the value belongs to: a candidate id, or the review item. */
45
+ readonly of: string;
46
+ /** The property path within that record. */
47
+ readonly property: string;
48
+ }
49
+ /** Per-card record of which facts have already been printed, and as what. */
50
+ export interface AuditFactTrace {
51
+ /**
52
+ * Records `fact` and reports whether this card has already printed it with
53
+ * this same value. Placeholder and empty values are never recorded and never
54
+ * suppressed.
55
+ */
56
+ isRepeatPlacement(fact: AuditFactId, value: unknown): boolean;
57
+ }
58
+ /**
59
+ * Suppression requires BOTH conditions, and each rules out a different mistake.
60
+ *
61
+ * Identity alone would be too eager: placements that nominally report the same
62
+ * property can resolve through different fallback chains (a projection override
63
+ * versus the candidate's own field), so a matching identity does not guarantee
64
+ * a matching value. Value alone would be far too eager — that is what conflates
65
+ * `extracted-at` with `observed`.
66
+ *
67
+ * @param alreadyOnCard facts the card face shows outside AUDIT DETAILS (the
68
+ * quoted excerpt, for one), so the audit surface does not reprint them.
69
+ */
70
+ export declare function createAuditFactTrace(alreadyOnCard?: readonly (AuditFactId & {
71
+ readonly value: unknown;
72
+ })[]): AuditFactTrace;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The AUDIT DETAILS row contract.
3
+ *
4
+ * Two separate concerns live here, and they are not the same thing:
5
+ *
6
+ * 1. **Naming.** Every row inside a field card's AUDIT DETAILS carries
7
+ * `data-audit-row="<key>"` — a stable machine name a host can select on.
8
+ * Row *labels* are display copy and change; these keys do not. A host that
9
+ * wants to restyle, reorder, or suppress a row on its own surface selects on
10
+ * the key. Deriving a selector from label text is not a supported way to
11
+ * address a row, and this attribute exists so nobody has to.
12
+ *
13
+ * 2. **Not printing the same fact twice.** Survey renders a card's identifiers
14
+ * and provenance in more than one place (the ID stack, the Raw Source
15
+ * section, each section's "IDs and trace links"). Where those placements
16
+ * carry the *same fact*, the card printed one value two or three times under
17
+ * different labels. {@link AuditFactTrace} keeps the first placement and
18
+ * drops the later ones. That is Survey's job, not a consumer's — see
19
+ * docs/consumer-integration-guide.md.
20
+ */
21
+ /**
22
+ * PUBLIC CONTRACT — every `data-audit-row` key Survey emits.
23
+ *
24
+ * Additive by policy: a key may be added, and a row may stop being emitted when
25
+ * it is a duplicate placement or a constant, but a key is never renamed or
26
+ * repointed at a different fact. Hosts may hold selectors against these values.
27
+ */
28
+ export const reviewAuditRowKeys = [
29
+ // Card-level identity
30
+ "current-candidate-id",
31
+ "proposed-candidate-id",
32
+ "claim-id",
33
+ "raw-source-id",
34
+ "locator",
35
+ "model",
36
+ "extractor",
37
+ "extracted-at",
38
+ // Unselected candidate history
39
+ "history-value",
40
+ "candidate-id",
41
+ // Raw Source
42
+ "source-reference",
43
+ "excerpt",
44
+ "observed",
45
+ "source-authority-class",
46
+ "declared-by",
47
+ "authority-scope",
48
+ "extraction-id",
49
+ // Review event
50
+ "actor",
51
+ "reviewed-at",
52
+ "status",
53
+ "rationale",
54
+ "outcome",
55
+ // Integrity posture
56
+ "checksum",
57
+ "candidate-set-id",
58
+ // Authority trace
59
+ "authority-trace-status",
60
+ "authority-trace-detail",
61
+ ];
62
+ /**
63
+ * Values that report a fact as missing rather than carrying one. Two placements
64
+ * both reading "not provided" are not one fact printed twice, so a placeholder
65
+ * never suppresses a later placement and is never suppressed by an earlier one.
66
+ *
67
+ * Deliberately only the literals the workbench itself substitutes for an absent
68
+ * field — every one of them, and nothing else. Producer data is not ours to
69
+ * interpret: an extractor genuinely named "none" would be misread as an absence,
70
+ * so a general vocabulary of nullish-looking words does not belong here.
71
+ * "unknown" is the one string on both sides of that line: Survey emits it for a
72
+ * missing extractor or timestamp, and a producer could conceivably use it as a
73
+ * real name. The cost of getting that wrong is a duplicate row, never a dropped
74
+ * one, so it stays.
75
+ *
76
+ * Keep this in step with the `?? "…"` substitutions in review-surface-preview.ts
77
+ * and the ID-stack rows in review-workbench.ts. The last two entries are not
78
+ * reachable through today's traced placements; they are here so the set means
79
+ * what it says rather than what it currently needs to.
80
+ */
81
+ const ABSENCE_VALUES = new Set([
82
+ "not provided",
83
+ "not recorded",
84
+ "unknown",
85
+ "no source excerpt provided.",
86
+ "no reviewer rationale provided.",
87
+ ]);
88
+ /**
89
+ * Suppression requires BOTH conditions, and each rules out a different mistake.
90
+ *
91
+ * Identity alone would be too eager: placements that nominally report the same
92
+ * property can resolve through different fallback chains (a projection override
93
+ * versus the candidate's own field), so a matching identity does not guarantee
94
+ * a matching value. Value alone would be far too eager — that is what conflates
95
+ * `extracted-at` with `observed`.
96
+ *
97
+ * @param alreadyOnCard facts the card face shows outside AUDIT DETAILS (the
98
+ * quoted excerpt, for one), so the audit surface does not reprint them.
99
+ */
100
+ export function createAuditFactTrace(alreadyOnCard = []) {
101
+ const printed = new Map();
102
+ for (const fact of alreadyOnCard) {
103
+ if (carriesAFact(fact.value)) {
104
+ printed.set(factKey(fact), String(fact.value));
105
+ }
106
+ }
107
+ return {
108
+ isRepeatPlacement(fact, value) {
109
+ if (!carriesAFact(value)) {
110
+ return false;
111
+ }
112
+ const key = factKey(fact);
113
+ const already = printed.get(key);
114
+ if (already !== undefined) {
115
+ // Same identity but a different value is a divergence worth showing,
116
+ // not a repeat. Keep the first reading on record.
117
+ return already === String(value);
118
+ }
119
+ printed.set(key, String(value));
120
+ return false;
121
+ },
122
+ };
123
+ }
124
+ function factKey(fact) {
125
+ return `${fact.of}::${fact.property}`;
126
+ }
127
+ function carriesAFact(value) {
128
+ if (value === undefined || value === null) {
129
+ return false;
130
+ }
131
+ const text = String(value).trim();
132
+ return text.length > 0 && !ABSENCE_VALUES.has(text.toLocaleLowerCase());
133
+ }
@@ -25,6 +25,44 @@ export type ExtractionInspectorInput = ExtractionInspectorEntry | {
25
25
  };
26
26
  export interface ExtractionInspectorCandidate {
27
27
  id: string;
28
+ /**
29
+ * PUBLIC CONTRACT — the element id of the source-highlight anchor that
30
+ * {@link mountExtractionInspector} renders for this candidate.
31
+ *
32
+ * A host that wants to link a fact back to the sentence it came from uses this
33
+ * value directly (`href="#" + highlightElementId`, or
34
+ * `document.getElementById(highlightElementId)`); it must never re-derive an id
35
+ * from {@link ExtractionInspectorCandidate.id}. The renderer reads this same
36
+ * field, so the value a host reads and the id in the DOM cannot drift apart.
37
+ *
38
+ * Optional **here** only because this type is also the shape a caller may hand
39
+ * to {@link mountExtractionInspector} directly; a model that came from
40
+ * {@link buildExtractionInspectorModel} is a
41
+ * {@link BuiltExtractionInspectorModel}, where it is always present. Take the
42
+ * builder's type if you want to link, and the guarantees below are yours.
43
+ * Mount fills in an id for any candidate that arrives without one, so a
44
+ * hand-authored model still renders — it just has nothing published to link to.
45
+ *
46
+ * Resolvable for **every** candidate in the model for as long as the inspector
47
+ * is mounted, in every posture. Anchors are deliberately exempt from the
48
+ * candidate list's paging and filtering (see
49
+ * {@link ExtractionInspectorMountOptions.pageSize}), and are rendered even when
50
+ * the prepared artifact is unavailable, digest-mismatched, or excerpt-
51
+ * mismatched — there is no highlighted span to land on then, so the anchor sits
52
+ * with the non-grounded posture message that explains why. A link that dies
53
+ * when a reviewer pages, filters, or opens a source that failed verification is
54
+ * the same broken promise as an id that drifted.
55
+ *
56
+ * Guaranteed unique across every candidate in one
57
+ * {@link BuiltExtractionInspectorModel}, and a valid CSS/HTML identifier.
58
+ *
59
+ * The reverse lookup — DOM node to candidate — is the equally public
60
+ * `data-highlight-candidate-id="<candidate.id>"` attribute on the same anchor.
61
+ *
62
+ * Deliberately omitted from {@link exportExtractionInspector}: the canonical
63
+ * export records extraction evidence, and a DOM binding is not evidence.
64
+ */
65
+ highlightElementId?: string;
28
66
  sourceKey: string;
29
67
  reviewItemName: string;
30
68
  proposalIndex: number;
@@ -57,6 +95,23 @@ export interface ExtractionInspectorModel {
57
95
  sources: ExtractionInspectorSource[];
58
96
  candidates: ExtractionInspectorCandidate[];
59
97
  }
98
+ /**
99
+ * A candidate from {@link buildExtractionInspectorModel}, where the DOM binding
100
+ * is resolved rather than optional.
101
+ *
102
+ * The split exists so the linking guarantee can be unconditional without making
103
+ * the *input* shape unbuildable by hand. `ExtractionInspectorModel` stays what a
104
+ * caller may author and pass to mount; this is what the builder hands back, and
105
+ * only the builder can promise the id is present, unique, and the one the
106
+ * renderer will use.
107
+ */
108
+ export interface BuiltExtractionInspectorCandidate extends ExtractionInspectorCandidate {
109
+ highlightElementId: string;
110
+ }
111
+ /** The model {@link buildExtractionInspectorModel} returns. Assignable anywhere an {@link ExtractionInspectorModel} is accepted. */
112
+ export interface BuiltExtractionInspectorModel extends ExtractionInspectorModel {
113
+ candidates: BuiltExtractionInspectorCandidate[];
114
+ }
60
115
  export interface ExtractionInspectorFilters {
61
116
  field?: string;
62
117
  provider?: string;
@@ -72,7 +127,17 @@ export interface ExtractionInspectorExportOptions {
72
127
  includeExcerpts?: boolean;
73
128
  }
74
129
  export interface ExtractionInspectorMountOptions {
75
- /** Maximum candidate rows and source highlights mounted at once. Defaults to 100 and is capped at 500. */
130
+ /**
131
+ * Maximum candidate rows and painted source highlights mounted at once.
132
+ * Defaults to 100 and is capped at 500.
133
+ *
134
+ * This governs the candidate list and the `<mark>` painting only. The
135
+ * highlight *anchors* — the elements
136
+ * {@link ExtractionInspectorCandidate.highlightElementId} names — are mounted
137
+ * for every candidate in the model regardless of page or filter, because a
138
+ * host's link to a span must not go dead when a reviewer types in the filter
139
+ * box. Anchors are empty and inert; the per-candidate cost is one element.
140
+ */
76
141
  pageSize?: number;
77
142
  }
78
143
  /**
@@ -80,7 +145,7 @@ export interface ExtractionInspectorMountOptions {
80
145
  * public import boundary. The function rechecks the result/ReviewItem binding
81
146
  * and fails closed if mutable caller data has drifted since import.
82
147
  */
83
- export declare function buildExtractionInspectorModel(input: ExtractionInspectorInput): ExtractionInspectorModel;
148
+ export declare function buildExtractionInspectorModel(input: ExtractionInspectorInput): BuiltExtractionInspectorModel;
84
149
  export declare function filterExtractionInspectorCandidates(model: ExtractionInspectorModel, filters: ExtractionInspectorFilters): ExtractionInspectorCandidate[];
85
150
  export declare function exportExtractionInspector(model: ExtractionInspectorModel, options?: ExtractionInspectorExportOptions): string;
86
151
  export declare function mountExtractionInspector(container: HTMLElement, model: ExtractionInspectorModel, options?: ExtractionInspectorMountOptions): () => void;