@kontourai/survey 1.11.0 → 1.13.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.
@@ -40,6 +40,7 @@ export declare const publicDirectoryReviewItemExample: {
40
40
  confidence: number;
41
41
  extractor: string;
42
42
  extractedAt: string;
43
+ model?: undefined;
43
44
  };
44
45
  claimTarget: {
45
46
  claimId: string;
@@ -91,6 +92,7 @@ export declare const publicDirectoryReviewItemExample: {
91
92
  target: string;
92
93
  confidence: number;
93
94
  extractor: string;
95
+ model: string;
94
96
  extractedAt: string;
95
97
  };
96
98
  claimTarget: {
@@ -92,6 +92,7 @@ export const publicDirectoryReviewItemExample = {
92
92
  target: "availabilityStatus",
93
93
  confidence: 0.82,
94
94
  extractor: "example-crawl",
95
+ model: "example-extraction-model-2026-05",
95
96
  extractedAt: "2026-05-31T15:00:00.000Z",
96
97
  },
97
98
  claimTarget: {
@@ -110,7 +110,8 @@ function proposedReviewCandidate(proposal, field, diff, selectedRole) {
110
110
  sourceRef: diff.sourceUrl ?? proposal.sourceUrl,
111
111
  observedAt: proposal.createdAt,
112
112
  excerpt: diff.excerpt ?? `Proposed ${field} value from downstream extraction.`,
113
- extractor: proposal.extractionModel,
113
+ extractor: "downstream-directory-extractor",
114
+ model: proposal.extractionModel,
114
115
  extractedAt: proposal.createdAt,
115
116
  confidence: diff.confidence,
116
117
  sourceRank: selectedRole === "proposed" ? 1 : 2,
@@ -193,6 +194,7 @@ function candidateExtraction(args, extractionId) {
193
194
  target: args.field,
194
195
  confidence: args.confidence,
195
196
  extractor: args.extractor,
197
+ ...(args.model ? { model: args.model } : {}),
196
198
  extractedAt: args.extractedAt,
197
199
  };
198
200
  }
@@ -1,7 +1,7 @@
1
1
  export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, EscalationDimension, EscalationRecord, Extraction, Interpretation, LocatorScheme, ProvenanceResolution, RawSource, RawSourceKind, ReviewAuthorizing, ReviewAuthorizingAuthorizedAction, ReviewAuthorizingExchange, ReviewAuthorizingExplicitStatement, ReviewAuthorizingKind, ReviewOutcome, ReviewStatus, SurveyInput, } from "./types.js";
2
2
  export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
3
3
  export { reviewResourceApiVersion } from "./review-resource.js";
4
- 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, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
4
+ 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";
5
5
  export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
6
6
  export type { CandidateReviewRecordInput, SurveyClaimRecord, SurveyInputBuilderArgs, SurveyObservationInput, } from "./builder.js";
7
7
  export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
@@ -35,7 +35,16 @@ export interface ExtractionReference {
35
35
  extractionId?: string;
36
36
  target: string;
37
37
  confidence?: number;
38
+ /** The extraction tool/pipeline that produced this candidate (e.g. a crawler or parser). */
38
39
  extractor?: string;
40
+ /**
41
+ * The model or model-version that generated this candidate, when the producer
42
+ * knows it (e.g. an LLM id or a dated extraction-model tag). Distinct from
43
+ * `extractor` (the tool): a reviewer wants to know *which model* proposed a
44
+ * value for trust/calibration. Producer-provided provenance; Survey only
45
+ * carries and displays it.
46
+ */
47
+ model?: string;
39
48
  extractedAt?: string;
40
49
  }
41
50
  export interface ClaimTargetHint {
@@ -96,6 +105,28 @@ export interface ReviewCandidate {
96
105
  projection?: SurveyRecordProjectionHint;
97
106
  producer?: Record<string, unknown>;
98
107
  }
108
+ /**
109
+ * Well-known neutral value-type vocabulary for {@link ReviewValueDescriptor}.
110
+ * Survey defines NO field-schema system of its own; this deliberately MIRRORS
111
+ * the shape an upstream field-schema owner already uses (e.g. traverse's
112
+ * `TargetFieldSchema.type` / `ExtractionProposal.valueType`), so a producer can
113
+ * carry a reviewed field's declared type down to the review UI WITHOUT Survey
114
+ * importing that owner's package (structural match, zero coupling).
115
+ */
116
+ export type ReviewValueType = "string" | "number" | "boolean" | "date" | "enum" | "array" | "object";
117
+ /**
118
+ * Optional, producer-supplied descriptor of a reviewed field's declared value
119
+ * shape. Purely descriptive: the workbench uses it ONLY to pick a typed editor
120
+ * (an enum `<select>`, a date/number input) and to validate a reviewer's edit
121
+ * before "Use proposed". Survey never re-derives, coerces, or overrides a
122
+ * candidate's value from it — a producer can still surface an out-of-shape
123
+ * candidate, which is exactly what a typed reviewer catches.
124
+ */
125
+ export interface ReviewValueDescriptor {
126
+ type: ReviewValueType;
127
+ /** Allowed values — meaningful with `type: "enum"`; ignored otherwise. */
128
+ enumValues?: string[];
129
+ }
99
130
  export interface ReviewItemSpec {
100
131
  target: string;
101
132
  candidates: ReviewCandidate[];
@@ -104,6 +135,13 @@ export interface ReviewItemSpec {
104
135
  rationale?: string;
105
136
  producerPolicy?: ProducerPolicy;
106
137
  projection?: SurveyRecordProjectionHint;
138
+ /**
139
+ * Optional neutral descriptor of the reviewed field's declared value type.
140
+ * When present, the workbench renders a typed editor and validates a
141
+ * reviewer's inline edit against it before accepting the proposed value.
142
+ * Absent → a plain text editor with no validation (today's behavior).
143
+ */
144
+ valueDescriptor?: ReviewValueDescriptor;
107
145
  }
108
146
  export interface ReviewItemStatus {
109
147
  observedCandidateCount?: number;
@@ -130,6 +168,15 @@ export interface ReviewDecisionSpec {
130
168
  * their own admissible block. */
131
169
  authorizing?: ReviewAuthorizing;
132
170
  projection?: SurveyRecordProjectionHint;
171
+ /**
172
+ * Reviewer-edited override for the proposed candidate's value, captured when the
173
+ * reviewer edits the inline proposed-value editor before choosing "Use proposed".
174
+ * Only meaningful when the decision selects the proposed candidate. Downstream
175
+ * consumers should read the effective value as `editedValue ?? <selected candidate value>`
176
+ * rather than assuming the candidate's original value was applied verbatim.
177
+ * Additive/optional: absent means the candidate's original value was used unchanged.
178
+ */
179
+ editedValue?: unknown;
133
180
  }
134
181
  export interface ReviewDecisionStatus {
135
182
  appliedToClaimIds?: string[];
@@ -9,6 +9,12 @@ export interface ReviewWorkbenchState {
9
9
  readonly decision?: ReviewWorkbenchDecision;
10
10
  readonly reviewedAt: string;
11
11
  readonly actorId: string;
12
+ /**
13
+ * Reviewer-edited override for the item's proposed value (inline edit in the
14
+ * field-diff card). Additive/optional: undefined means no edit was made and the
15
+ * proposed candidate's original value applies.
16
+ */
17
+ readonly editedValue?: unknown;
12
18
  }
13
19
  export interface ReviewQueueSessionState {
14
20
  readonly items: readonly ReviewItem[];
@@ -17,6 +23,13 @@ export interface ReviewQueueSessionState {
17
23
  readonly decisionsByItemName: Readonly<Record<string, ReviewWorkbenchDecision>>;
18
24
  readonly reviewedAt: string;
19
25
  readonly actorId: string;
26
+ /**
27
+ * Reviewer-edited overrides for proposed values, keyed by ReviewItem name.
28
+ * Additive/optional: a session built before this field existed behaves exactly
29
+ * as before (every lookup resolves to undefined, meaning "use the candidate's
30
+ * original value").
31
+ */
32
+ readonly editedValuesByItemName?: Readonly<Record<string, unknown>>;
20
33
  }
21
34
  export interface ReviewSessionSummary {
22
35
  readonly accepted: number;
@@ -53,8 +66,25 @@ export declare function deriveQueueRowStatus(item: ReviewItem, session: ReviewQu
53
66
  export declare function nextUnresolvedItemName(session: ReviewQueueSessionState): string | undefined;
54
67
  export declare function reviewSessionSummary(session: ReviewQueueSessionState): ReviewSessionSummary;
55
68
  export declare function candidateForDecision(item: ReviewItem, decision: ReviewWorkbenchDecision): ReviewCandidate;
69
+ /**
70
+ * The value that should actually be applied for a decision: the reviewer's inline
71
+ * edit when one was made for an accept-proposed decision, otherwise the selected
72
+ * candidate's original value. Consumers reading `ReviewWorkbenchResult` should
73
+ * prefer `effectiveValue`/`effectiveDisplayValue`, which are already computed with
74
+ * this rule; this helper exists for callers deriving the value from raw session
75
+ * state directly.
76
+ */
77
+ export declare function effectiveValueForDecision(item: ReviewItem, decision: ReviewWorkbenchDecision, editedValue?: unknown): unknown;
56
78
  export declare function selectedCandidateRole(state: ReviewWorkbenchState): ReviewCandidate["role"] | undefined;
57
79
  export declare function buildReviewSessionResource(session: ReviewQueueSessionState, events?: readonly ReviewSessionEvent[], sessionName?: string): ReviewSession;
58
80
  export declare function buildReviewSessionEvents(session: ReviewQueueSessionState, sessionName?: string): ReviewSessionEvent[];
59
81
  export declare function replayReviewSessionEvents(startState: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): ReviewQueueSessionState;
82
+ /**
83
+ * Detects the explicit "clear this ReviewItem's decision" replay signal (emitted
84
+ * by the workbench's "Change" / undo control): a decision event whose
85
+ * `data.workbenchDecision` is the literal `null` sentinel, as opposed to `undefined`
86
+ * (no decision info present — event is ignored by replay, same as before this
87
+ * feature existed).
88
+ */
89
+ export declare function isClearedWorkbenchDecisionEvent(event: ReviewSessionEvent): boolean;
60
90
  export declare function buildReviewSessionEvent(session: ReviewQueueSessionState, spec: Omit<ReviewSessionEventSpec, "actor">): ReviewSessionEvent;
@@ -37,6 +37,7 @@ export function initialReviewQueueSessionState(items = reviewWorkbenchQueueExamp
37
37
  activeItemName: items[0]?.metadata.name ?? "",
38
38
  notesByItemName: {},
39
39
  decisionsByItemName: {},
40
+ editedValuesByItemName: {},
40
41
  reviewedAt: "2026-06-04T00:00:00.000Z",
41
42
  actorId: "review-workbench-operator",
42
43
  };
@@ -47,6 +48,7 @@ export function currentReviewWorkbenchState(session) {
47
48
  item,
48
49
  note: session.notesByItemName[item.metadata.name] ?? "",
49
50
  decision: session.decisionsByItemName[item.metadata.name],
51
+ editedValue: session.editedValuesByItemName?.[item.metadata.name],
50
52
  reviewedAt: session.reviewedAt,
51
53
  actorId: session.actorId,
52
54
  };
@@ -117,6 +119,18 @@ export function candidateForDecision(item, decision) {
117
119
  }
118
120
  return candidate;
119
121
  }
122
+ /**
123
+ * The value that should actually be applied for a decision: the reviewer's inline
124
+ * edit when one was made for an accept-proposed decision, otherwise the selected
125
+ * candidate's original value. Consumers reading `ReviewWorkbenchResult` should
126
+ * prefer `effectiveValue`/`effectiveDisplayValue`, which are already computed with
127
+ * this rule; this helper exists for callers deriving the value from raw session
128
+ * state directly.
129
+ */
130
+ export function effectiveValueForDecision(item, decision, editedValue) {
131
+ const candidate = candidateForDecision(item, decision);
132
+ return decision === "accept-proposed" && editedValue !== undefined ? editedValue : candidate.value;
133
+ }
120
134
  export function selectedCandidateRole(state) {
121
135
  if (!state.decision) {
122
136
  return undefined;
@@ -238,6 +252,10 @@ export function replayReviewSessionEvents(startState, events) {
238
252
  }
239
253
  if ((event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted")
240
254
  && event.spec.reviewItemName) {
255
+ if (isClearedWorkbenchDecisionEvent(event)) {
256
+ const { [event.spec.reviewItemName]: _removed, ...remainingDecisions } = session.decisionsByItemName;
257
+ return { ...session, decisionsByItemName: remainingDecisions };
258
+ }
241
259
  const decision = workbenchDecisionFromEvent(event);
242
260
  return decision
243
261
  ? {
@@ -252,6 +270,18 @@ export function replayReviewSessionEvents(startState, events) {
252
270
  return session;
253
271
  }, startState);
254
272
  }
273
+ /**
274
+ * Detects the explicit "clear this ReviewItem's decision" replay signal (emitted
275
+ * by the workbench's "Change" / undo control): a decision event whose
276
+ * `data.workbenchDecision` is the literal `null` sentinel, as opposed to `undefined`
277
+ * (no decision info present — event is ignored by replay, same as before this
278
+ * feature existed).
279
+ */
280
+ export function isClearedWorkbenchDecisionEvent(event) {
281
+ return event.spec.data !== undefined
282
+ && "workbenchDecision" in event.spec.data
283
+ && event.spec.data.workbenchDecision === null;
284
+ }
255
285
  export function buildReviewSessionEvent(session, spec) {
256
286
  return {
257
287
  apiVersion: reviewResourceApiVersion,
@@ -1,4 +1,4 @@
1
- import { candidateForDecision, workbenchDecisionDefinitions, } from "./review-queue-session.js";
1
+ import { candidateForDecision, isClearedWorkbenchDecisionEvent, workbenchDecisionDefinitions, } from "./review-queue-session.js";
2
2
  export function validateReviewSessionEventsForSnapshot(snapshot, events) {
3
3
  const itemsByName = new Map(snapshot.items.map((item) => [item.metadata.name, item]));
4
4
  const sequenceIssues = validateEventSequence(events);
@@ -35,7 +35,12 @@ export function validateReviewSessionEventsForSnapshot(snapshot, events) {
35
35
  message: `ReviewSessionEvent ${event.metadata.name} is a decision event but does not reference a ReviewItem.`,
36
36
  });
37
37
  }
38
- if (event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted") {
38
+ if ((event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted")
39
+ && isClearedWorkbenchDecisionEvent(event)) {
40
+ // Explicit "clear this ReviewItem's decision" signal (undo). No candidate/status
41
+ // expectations apply — the event carries no selected candidate.
42
+ }
43
+ else if (event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted") {
39
44
  const decision = replayableWorkbenchDecision(event.spec.data?.workbenchDecision);
40
45
  if (!decision) {
41
46
  issues.push({