@kontourai/survey 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +4 -0
  2. package/dist/example-data/public-directory-review-resource.d.ts +3 -3
  3. package/dist/examples/calibrated-auto-accept.d.ts +22 -15
  4. package/dist/examples/calibrated-auto-accept.js +40 -36
  5. package/dist/src/calibration.d.ts +48 -21
  6. package/dist/src/calibration.js +72 -33
  7. package/dist/src/console/review-console-server.d.ts +3 -1
  8. package/dist/src/console/review-console-server.js +203 -50
  9. package/dist/src/extraction-envelope.d.ts +22 -0
  10. package/dist/src/extraction-envelope.js +25 -4
  11. package/dist/src/index.d.ts +8 -7
  12. package/dist/src/index.js +2 -2
  13. package/dist/src/inquiry-mapping.d.ts +15 -1
  14. package/dist/src/inquiry-mapping.js +10 -2
  15. package/dist/src/mcp/review-mcp.js +70 -65
  16. package/dist/src/producer-profile.d.ts +41 -2
  17. package/dist/src/producer-profile.js +29 -2
  18. package/dist/src/review-session-file.d.ts +64 -0
  19. package/dist/src/review-session-file.js +320 -0
  20. package/dist/src/review-workbench/edited-value.d.ts +70 -0
  21. package/dist/src/review-workbench/edited-value.js +147 -0
  22. package/dist/src/review-workbench/review-presentation.d.ts +44 -0
  23. package/dist/src/review-workbench/review-presentation.js +49 -0
  24. package/dist/src/review-workbench/review-queue-session.js +8 -1
  25. package/dist/src/review-workbench/review-session-replay.d.ts +35 -1
  26. package/dist/src/review-workbench/review-session-replay.js +77 -0
  27. package/dist/src/review-workbench/review-workbench.d.ts +8 -4
  28. package/dist/src/review-workbench/review-workbench.js +19 -7
  29. package/dist/src/review-workbench/server-review-session.d.ts +3 -1
  30. package/dist/src/review-workbench/server-review-session.js +1 -0
  31. package/dist/src/reviewed-candidate-resolution.js +13 -7
  32. package/dist/src/schema-mapping.d.ts +23 -0
  33. package/dist/src/schema-mapping.js +30 -20
  34. package/dist/src/to-surface.d.ts +30 -6
  35. package/dist/src/to-surface.js +196 -18
  36. package/dist/src/types.d.ts +39 -0
  37. package/package.json +5 -4
@@ -2,15 +2,34 @@ import { createHash } from "node:crypto";
2
2
  import { buildReviewProofAnchor } from "./review-proof.js";
3
3
  import { assertReviewOutcomeDiscipline } from "./producer-discipline.js";
4
4
  import { deriveCalibration } from "./calibration.js";
5
+ import { AUTO_ACCEPT_ACTOR } from "./producer-profile.js";
6
+ import { canonicalJson } from "./review-workbench/canonical.js";
5
7
  /** Minimum labeled samples a calibration group needs before its empirical
6
8
  * accuracy is emitted as a `conclusionConfidence.value`. */
7
9
  const DEFAULT_CALIBRATION_MIN_SAMPLES = 20;
10
+ /**
11
+ * Thrown when a claim's trusted status or value does not agree with the review
12
+ * outcome it cites, or when the governing review cannot be chosen.
13
+ * - `status-mismatch`: a `verified`/`assumed` claim cites a review whose status differs.
14
+ * - `value-mismatch`: a `verified`/`assumed` claim carries a value other than the reviewed value.
15
+ * - `ambiguous-review-order`: several reviews apply to one candidate and the latest
16
+ * cannot be determined from `reviewedAt` (missing, unparseable, or tied).
17
+ */
18
+ export class ReviewAgreementError extends Error {
19
+ name = "ReviewAgreementError";
20
+ code;
21
+ constructor(code, message) {
22
+ super(message);
23
+ this.code = code;
24
+ }
25
+ }
8
26
  export function buildSurveyTrustBundle(input, options = {}) {
9
27
  const projectionContextId = validateProjectionContextId(options.projectionContextId);
10
28
  const rawSources = indexById(input.rawSources, "raw source");
11
29
  const extractions = indexById(input.extractions, "extraction");
12
30
  const candidateSets = indexById(input.candidateSets, "candidate set");
13
31
  const reviewsByCandidateSet = groupBy(input.reviewOutcomes, (review) => review.candidateSetId);
32
+ // Only the explicit experimental opt-in produces a value (#279).
14
33
  const calibrationOptions = normalizeCalibrationOptions(options.calibration);
15
34
  const calibrationMetrics = calibrationOptions
16
35
  ? (calibrationOptions.metrics ?? deriveCalibration({
@@ -28,18 +47,19 @@ export function buildSurveyTrustBundle(input, options = {}) {
28
47
  const candidate = selectCandidate(candidateSet, projection.candidateId);
29
48
  const extraction = requireMapValue(extractions, candidate.extractionId, "extraction");
30
49
  const rawSource = requireMapValue(rawSources, extraction.sourceId, "raw source");
31
- const review = selectReview(reviewsByCandidateSet.get(candidateSet.id) ?? [], candidate.id);
50
+ const review = selectReview(reviewsByCandidateSet.get(candidateSet.id) ?? [], candidate.id, projection.id);
32
51
  const projectionReview = review?.resolution === "could_not_confirm" ? undefined : review;
33
52
  const unreviewedStatus = statusFor({ candidateSet, candidate });
34
53
  const status = projection.status
35
54
  ?? (review?.resolution === "could_not_confirm"
36
55
  ? (unreviewedStatus === "disputed" ? unreviewedStatus : review.status)
37
56
  : statusFor({ candidateSet, candidate, review }));
38
- assertProducerDiscipline({ status, review, candidateSet, extraction, rawSource, projection });
39
57
  const claimValue = projection.value ?? candidate.value;
58
+ assertProducerDiscipline({ status, review, candidateSet, candidate, extraction, rawSource, projection, claimValue });
40
59
  const createdAt = projection.createdAt ?? extraction.extractedAt;
41
60
  const updatedAt = projection.updatedAt ?? projectionReview?.reviewedAt ?? input.generatedAt;
42
61
  const evidenceId = projectionRecordId(projection.id, projectionContextId, "claim-evidence", "evidence.source");
62
+ const autoAccepted = projectionReview?.actor === AUTO_ACCEPT_ACTOR;
43
63
  const claim = {
44
64
  id: projection.id,
45
65
  subjectType: projection.subjectType,
@@ -57,8 +77,13 @@ export function buildSurveyTrustBundle(input, options = {}) {
57
77
  confidenceBasis: {
58
78
  sourceQuality: "moderate",
59
79
  extractionConfidence: candidate.confidence ?? extraction.confidence,
60
- reviewerAuthority: status === "verified" || status === "assumed" ? "operator" : "none",
61
- evidenceStrength: status === "verified" || status === "assumed" ? "moderate" : "weak",
80
+ // An auto-accept policy checked nothing but the proposer's own
81
+ // confidence, so its claims carry system authority and weak evidence;
82
+ // only a human review earns operator authority.
83
+ reviewerAuthority: status === "verified" || status === "assumed"
84
+ ? (autoAccepted ? "system" : "operator")
85
+ : "none",
86
+ evidenceStrength: (status === "verified" || status === "assumed") && !autoAccepted ? "moderate" : "weak",
62
87
  impactLevel: projection.impactLevel,
63
88
  ...projection.confidenceBasis,
64
89
  },
@@ -72,18 +97,21 @@ export function buildSurveyTrustBundle(input, options = {}) {
72
97
  candidate,
73
98
  review: projectionReview,
74
99
  comfortZoneReview: review,
100
+ valueEdit: (status === "verified" || status === "assumed") && review ? acceptedEdit(review) : undefined,
75
101
  }),
76
102
  },
77
103
  };
78
- // Promote the review's comfort-zone signal — and, when calibration is
79
- // enabled, an empirically-calibrated conclusion probability — into the
104
+ // Promote the review's comfort-zone signal — and, under the experimental
105
+ // calibration opt-in, the group affirmation rate — into the
80
106
  // first-class conclusionConfidence field (Surface 2.9 / Hachure 0.14) so the
81
107
  // signal is portable and comparable, not buried in producer metadata.
82
108
  //
83
- // comfortZone is CARRIED from the review. `value` is PRODUCED from empirical
84
- // review calibration (#114/#137): the affirmation rate of this extractor's
85
- // proposals at this confidence — a calibrated conclusion probability, distinct
86
- // from the extraction-confidence ingredient in confidenceBasis.
109
+ // comfortZone is CARRIED from the review. `value` is PRODUCED, only under the
110
+ // experimental opt-in, from empirical review calibration (#114/#137/#279):
111
+ // the affirmation rate of this claim's extractor/field GROUP. It is a group
112
+ // base rate shared by every affirmed claim in the group, not a per-claim
113
+ // probability, and distinct from the extraction-confidence ingredient in
114
+ // confidenceBasis.
87
115
  //
88
116
  // A value is produced only for an AFFIRMED conclusion (status verified/assumed)
89
117
  // that clears the sample floor. conclusionConfidence.value is "probability the
@@ -235,7 +263,8 @@ function projectInterpretations(input) {
235
263
  }
236
264
  }
237
265
  function buildInterpretationProjection(input) {
238
- const anchorSource = requirePolicyStandardAnchor(input.interpretation, input.rawSources);
266
+ assertInterpretationReadingShape(input.interpretation);
267
+ const anchorSource = requireInterpretationAnchor(input.interpretation, input.rawSources);
239
268
  const claim = resolveInterpretationClaim(input.interpretation, input.claims, input.input);
240
269
  if (!input.claimsById.has(claim.id)) {
241
270
  throw new Error(`Interpretation ${input.interpretation.id} resolved unknown claim ${claim.id}`);
@@ -260,6 +289,48 @@ function buildInterpretationProjection(input) {
260
289
  }),
261
290
  };
262
291
  }
292
+ const INTERPRETATION_READING_KINDS = ["policy-standard", "gleaned", "answerImpact"];
293
+ const INTERPRETATION_ANSWER_IMPACTS = ["supported", "narrowed", "accepted-risk"];
294
+ /** Absent readingKind means the original policy-standard reading (pre-#259 records). */
295
+ function interpretationReadingKind(interpretation) {
296
+ return interpretation.readingKind ?? "policy-standard";
297
+ }
298
+ /**
299
+ * Fail-closed shape validation for the reading dimension (#259). Records come
300
+ * from JSON as often as from typed callers, so unknown vocabulary throws
301
+ * instead of silently projecting under a label nothing derived.
302
+ */
303
+ function assertInterpretationReadingShape(interpretation) {
304
+ if (interpretation.readingKind !== undefined && !INTERPRETATION_READING_KINDS.includes(interpretation.readingKind)) {
305
+ throw new Error(`Interpretation ${interpretation.id} has unknown readingKind ${String(interpretation.readingKind)}; expected one of ${INTERPRETATION_READING_KINDS.join(", ")}`);
306
+ }
307
+ const kind = interpretationReadingKind(interpretation);
308
+ if (kind === "answerImpact") {
309
+ if (interpretation.answerImpact === undefined) {
310
+ throw new Error(`Interpretation ${interpretation.id} readingKind answerImpact requires an answerImpact value`);
311
+ }
312
+ if (!INTERPRETATION_ANSWER_IMPACTS.includes(interpretation.answerImpact)) {
313
+ throw new Error(`Interpretation ${interpretation.id} has unknown answerImpact ${String(interpretation.answerImpact)}; expected one of ${INTERPRETATION_ANSWER_IMPACTS.join(", ")}`);
314
+ }
315
+ return;
316
+ }
317
+ if (interpretation.answerImpact !== undefined) {
318
+ throw new Error(`Interpretation ${interpretation.id} sets answerImpact but readingKind is ${kind}; answerImpact is only valid on answerImpact readings`);
319
+ }
320
+ }
321
+ /**
322
+ * Resolves an interpretation's anchor raw source. Every reading kind requires
323
+ * a KNOWN raw source (referential integrity per #16 R3); only the original
324
+ * policy-standard reading additionally requires the source kind to be
325
+ * `policy-standard`. The gleaned / answerImpact readings (#259) anchor to the
326
+ * result source the producer read, whatever its kind.
327
+ */
328
+ function requireInterpretationAnchor(interpretation, rawSources) {
329
+ if (interpretationReadingKind(interpretation) === "policy-standard") {
330
+ return requirePolicyStandardAnchor(interpretation, rawSources);
331
+ }
332
+ return requireMapValue(rawSources, interpretation.anchorsToSourceId, "interpretation anchor raw source");
333
+ }
263
334
  function requirePolicyStandardAnchor(interpretation, rawSources) {
264
335
  const anchorSource = requireMapValue(rawSources, interpretation.anchorsToSourceId, "interpretation anchor raw source");
265
336
  if (anchorSource.kind !== "policy-standard") {
@@ -268,14 +339,20 @@ function requirePolicyStandardAnchor(interpretation, rawSources) {
268
339
  return anchorSource;
269
340
  }
270
341
  function createInterpretationAnchorEvidence(input) {
342
+ const readingKind = interpretationReadingKind(input.interpretation);
271
343
  return {
272
344
  id: projectionRecordId(input.interpretation.id, input.projectionContextId, "interpretation-evidence", "evidence.anchor"),
273
345
  claimId: input.claim.id,
274
- evidenceType: "policy_rule",
346
+ // The policy-standard reading keeps its exact prior projection; the new
347
+ // reading kinds (#259) derive the evidence type from the anchor source
348
+ // they actually read, via the same mapping extraction evidence uses.
349
+ evidenceType: readingKind === "policy-standard" ? "policy_rule" : evidenceTypeFor(input.anchorSource),
275
350
  method: "anchoring",
276
351
  sourceRef: input.anchorSource.sourceRef,
277
352
  sourceLocator: input.interpretation.ruleLocator,
278
- excerptOrSummary: input.policyStandard?.inlineText ?? `Anchored policy-standard reading at ${input.interpretation.ruleLocator}.`,
353
+ excerptOrSummary: readingKind === "policy-standard"
354
+ ? input.policyStandard?.inlineText ?? `Anchored policy-standard reading at ${input.interpretation.ruleLocator}.`
355
+ : input.anchorSource.inlineText ?? `Anchored ${readingKind} reading at ${input.interpretation.ruleLocator}.`,
279
356
  observedAt: input.anchorSource.observedAt,
280
357
  collectedBy: input.interpretation.actor,
281
358
  integrityRef: input.anchorSource.checksum,
@@ -287,6 +364,10 @@ function createInterpretationAnchorEvidence(input) {
287
364
  locatorScheme: input.anchorSource.locatorScheme,
288
365
  anchorsToSourceId: input.anchorSource.id,
289
366
  ruleLocator: input.interpretation.ruleLocator,
367
+ // Only explicitly-set reading kinds project, so legacy batches stay
368
+ // byte-identical. Authored-judgment provenance markers, not machine facts.
369
+ ...(input.interpretation.readingKind ? { readingKind: input.interpretation.readingKind } : {}),
370
+ ...(input.interpretation.answerImpact ? { answerImpact: input.interpretation.answerImpact } : {}),
290
371
  },
291
372
  };
292
373
  }
@@ -319,6 +400,10 @@ function attachInterpretationClaimMetadata(input) {
319
400
  reading: input.interpretation.reading,
320
401
  actor: input.interpretation.actor,
321
402
  recordedAt: input.interpretation.recordedAt,
403
+ // Explicit reading dimension only (#259): legacy entries keep their
404
+ // exact prior bytes; absent readingKind means policy-standard.
405
+ ...(input.interpretation.readingKind ? { readingKind: input.interpretation.readingKind } : {}),
406
+ ...(input.interpretation.answerImpact ? { answerImpact: input.interpretation.answerImpact } : {}),
322
407
  ...(input.interpretation.metadata ? { metadata: input.interpretation.metadata } : {}),
323
408
  edges: [
324
409
  {
@@ -394,6 +479,11 @@ function buildSurveyMetadata(input) {
394
479
  },
395
480
  }
396
481
  : {}),
482
+ // A trusted claim whose value is a reviewer's edit says so, and keeps the
483
+ // value the reviewer was shown.
484
+ ...(input.valueEdit
485
+ ? { valueEdit: { edited: true, originalValue: input.candidate.value, reviewOutcomeId: input.comfortZoneReview?.id } }
486
+ : {}),
397
487
  ...(input.comfortZoneReview?.withinComfortZone === false
398
488
  ? {
399
489
  comfortZone: {
@@ -423,6 +513,17 @@ function assertProducerDiscipline(input) {
423
513
  review: input.review,
424
514
  candidateSetStatus: input.candidateSet.status,
425
515
  });
516
+ if ((input.status === "verified" || input.status === "assumed") && input.review) {
517
+ // A trusted claim may only restate its review: same status, and the value
518
+ // the reviewer saw (the candidate value, or the edit the review records).
519
+ if (input.review.status !== input.status) {
520
+ throw new ReviewAgreementError("status-mismatch", `Claim ${input.projection.id} status ${input.status} disagrees with review outcome ${input.review.id} status ${input.review.status}`);
521
+ }
522
+ const reviewedValue = reviewedValueFor(input.review, input.candidate);
523
+ if (canonicalJson(input.claimValue) !== canonicalJson(reviewedValue)) {
524
+ throw new ReviewAgreementError("value-mismatch", `Claim ${input.projection.id} is ${input.status} but its value differs from the value reviewed in ${input.review.id}`);
525
+ }
526
+ }
426
527
  if (input.rawSource.kind !== "manual-entry" && !input.extraction.locator) {
427
528
  throw new Error(`Claim ${input.projection.id} needs a source locator for ${input.rawSource.kind}`);
428
529
  }
@@ -435,15 +536,92 @@ function selectCandidate(candidateSet, candidateId) {
435
536
  }
436
537
  return candidate;
437
538
  }
438
- function selectReview(reviews, candidateId) {
439
- return reviews.find((review) => review.candidateId === candidateId) ?? reviews.find((review) => !review.candidateId);
539
+ /** An accepted edit the review records. `buildCanonicalReviewedTrustInput`
540
+ * writes `metadata.editedValue` next to `metadata.workbenchDecision`; only an
541
+ * edit on an `accept-proposed` decision replaces the reviewed value. Any other
542
+ * `editedValue` (a bare one, or one on a reject or keep decision) is not an
543
+ * accepted edit and is ignored. */
544
+ function acceptedEdit(review) {
545
+ const metadata = review.metadata;
546
+ return metadata?.workbenchDecision === "accept-proposed" && Object.hasOwn(metadata, "editedValue")
547
+ ? { value: metadata.editedValue }
548
+ : undefined;
549
+ }
550
+ /** The value a review outcome attests: its accepted edit, else the candidate value that was reviewed. */
551
+ function reviewedValueFor(review, candidate) {
552
+ const edit = acceptedEdit(review);
553
+ return edit ? edit.value : candidate.value;
554
+ }
555
+ /** The latest applicable review governs. Exact candidate bindings take
556
+ * precedence over unbound (set-wide) reviews; within a tier, several reviews
557
+ * are ordered by `reviewedAt`, and an order that cannot be established is
558
+ * refused rather than guessed. */
559
+ function selectReview(reviews, candidateId, claimId) {
560
+ const exact = reviews.filter((review) => review.candidateId === candidateId);
561
+ return latestReview(exact.length ? exact : reviews.filter((review) => !review.candidateId), claimId);
562
+ }
563
+ function latestReview(reviews, claimId) {
564
+ if (reviews.length <= 1)
565
+ return reviews[0];
566
+ const timed = reviews.map((review) => ({ review, at: review.reviewedAt ? Date.parse(review.reviewedAt) : Number.NaN }));
567
+ const untimed = timed.find((entry) => Number.isNaN(entry.at));
568
+ if (untimed) {
569
+ throw new ReviewAgreementError("ambiguous-review-order", `Claim ${claimId} has ${reviews.length} applicable review outcomes but ${untimed.review.id} has no parseable reviewedAt`);
570
+ }
571
+ const latestAt = Math.max(...timed.map((entry) => entry.at));
572
+ const latest = timed.filter((entry) => entry.at === latestAt).map((entry) => entry.review);
573
+ // Reviews at the same instant (however the timestamp is spelled) that record
574
+ // the same decision are one review recorded twice; conflicting ones are refused.
575
+ if (new Set(latest.map(reviewDecisionKey)).size > 1) {
576
+ throw new ReviewAgreementError("ambiguous-review-order", `Claim ${claimId} has conflicting review outcomes ${latest.map((review) => review.id).join(", ")} at the latest reviewedAt`);
577
+ }
578
+ return [...latest].sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0))[0];
579
+ }
580
+ function reviewDecisionKey(review) {
581
+ const edit = acceptedEdit(review);
582
+ return canonicalJson({
583
+ candidateId: review.candidateId ?? null,
584
+ status: review.status,
585
+ resolution: review.resolution ?? null,
586
+ edit: edit ? { value: edit.value } : null,
587
+ // These reach the claim (comfort zone, testimony provenance), so reviews
588
+ // that differ in them are different decisions, not one recorded twice.
589
+ withinComfortZone: review.withinComfortZone ?? null,
590
+ comfortZoneNote: review.comfortZoneNote ?? null,
591
+ authorizing: review.authorizing ?? null,
592
+ });
440
593
  }
441
594
  function normalizeCalibrationOptions(calibration) {
442
595
  if (calibration === undefined || calibration === false)
443
596
  return undefined;
444
- if (calibration === true)
445
- return {};
446
- return calibration;
597
+ if (calibration === true || calibration.experimentalConclusionValue === undefined) {
598
+ // Callers written before #279 asked for a value this way; tell them it is
599
+ // now ignored instead of silently dropping it. An explicit `false` is a
600
+ // deliberate opt-out and stays quiet.
601
+ warnCalibrationOptInMissingOnce();
602
+ return undefined;
603
+ }
604
+ return calibration.experimentalConclusionValue === true ? calibration : undefined;
605
+ }
606
+ let warnedCalibrationOptInMissing = false;
607
+ /**
608
+ * Warn once per process, matching the `defineProductVocabulary` deprecation
609
+ * notice: `buildSurveyTrustBundle` has no diagnostics channel in its return
610
+ * value (it returns a TrustBundle), and a per-call warning would flood batch
611
+ * projections.
612
+ */
613
+ function warnCalibrationOptInMissingOnce() {
614
+ if (warnedCalibrationOptInMissing)
615
+ return;
616
+ warnedCalibrationOptInMissing = true;
617
+ console.warn("[@kontourai/survey] buildSurveyTrustBundle: the `calibration` option no longer sets " +
618
+ "conclusionConfidence.value without `calibration: { experimentalConclusionValue: true }` (#279). " +
619
+ "The value is an experimental extractor/field group base rate, not a per-claim probability. " +
620
+ "Pass the flag to keep it, or `experimentalConclusionValue: false` / omit `calibration` to silence this warning.");
621
+ }
622
+ /** Test-only: re-arm the once-per-process calibration opt-in warning. Not exported from the package index. */
623
+ export function resetCalibrationOptInWarningForTests() {
624
+ warnedCalibrationOptInMissing = false;
447
625
  }
448
626
  /**
449
627
  * Looks up the empirical affirmation rate for an extractor/field, preferring the
@@ -104,11 +104,50 @@ export interface EscalationRecord {
104
104
  resolvedBy?: string;
105
105
  metadata?: Record<string, unknown>;
106
106
  }
107
+ /**
108
+ * The reading dimension of an {@link Interpretation} (#259, generalizing #16):
109
+ *
110
+ * - `"policy-standard"` — the original reading of how a policy-standard
111
+ * paragraph applies to the claim. This kind keeps the hard requirement that
112
+ * the anchor raw source is `kind: "policy-standard"`.
113
+ * - `"gleaned"` — what this result taught: an authored reading of what the
114
+ * anchored source material showed for the claim.
115
+ * - `"answerImpact"` — how the reading moved the inquiry answer the claim
116
+ * feeds (see {@link InterpretationAnswerImpact}); requires `answerImpact`.
117
+ *
118
+ * Additive-optional: a record with no `readingKind` is a `"policy-standard"`
119
+ * reading, so existing batches keep their exact prior validation and
120
+ * projection behavior. Interpretations never enter canonical review-proof
121
+ * bytes regardless of kind.
122
+ */
123
+ export type InterpretationReadingKind = "policy-standard" | "gleaned" | "answerImpact";
124
+ /**
125
+ * How an `"answerImpact"` reading moved the inquiry answer its claim feeds:
126
+ * the result `supported` the answer, `narrowed` it, or was recorded as an
127
+ * `accepted-risk`. "Answer" means the inquiry answer (inquiry-mapping);
128
+ * downstream decisions stay with consumers.
129
+ */
130
+ export type InterpretationAnswerImpact = "supported" | "narrowed" | "accepted-risk";
107
131
  export interface Interpretation {
108
132
  id: string;
109
133
  appliesToTarget?: string;
110
134
  appliesToClaimId?: string;
135
+ /**
136
+ * Which reading dimension this record carries. Absent means
137
+ * `"policy-standard"` (the pre-#259 behavior, unchanged).
138
+ */
139
+ readingKind?: InterpretationReadingKind;
140
+ /**
141
+ * Required when `readingKind` is `"answerImpact"`; illegal for any other
142
+ * kind. The structured impact beside the free-text `reading`.
143
+ */
144
+ answerImpact?: InterpretationAnswerImpact;
111
145
  anchorsToSourceId: string;
146
+ /**
147
+ * Locator inside the anchor source: the rule paragraph for a
148
+ * `"policy-standard"` reading, or the result location the producer read for
149
+ * `"gleaned"` / `"answerImpact"` readings.
150
+ */
112
151
  ruleLocator: string;
113
152
  reading: string;
114
153
  actor: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Producer-side source, extraction, candidate, and review contracts for projecting verified claims into Surface.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -72,9 +72,9 @@
72
72
  "workflow:validate-artifacts": "flow-agents-validate-artifacts"
73
73
  },
74
74
  "dependencies": {
75
- "@kontourai/surface": "^2.13.0",
75
+ "@kontourai/surface": "^2.13.0 || ^3.0.0 || ^4.0.0",
76
76
  "@modelcontextprotocol/server": "2.0.0",
77
- "zod": "4.2.0"
77
+ "zod": "4.4.3"
78
78
  },
79
79
  "peerDependencies": {
80
80
  "typescript": ">=5.0.0"
@@ -85,7 +85,7 @@
85
85
  }
86
86
  },
87
87
  "devDependencies": {
88
- "@kontourai/flow-agents": "5.5.0",
88
+ "@kontourai/flow-agents": "5.10.0",
89
89
  "@kontourai/traverse": "^0.25.0",
90
90
  "@kontourai/ui": "^1.1.0",
91
91
  "@playwright/test": "^1.62.0",
@@ -96,6 +96,7 @@
96
96
  "engines": {
97
97
  "node": ">=22"
98
98
  },
99
+ "packageManager": "pnpm@11.25.0",
99
100
  "bin": {
100
101
  "survey-review-mcp": "./bin/survey-review-mcp.mjs",
101
102
  "survey-review-console": "./bin/survey-review-console.mjs"