@kontourai/survey 6.1.0 → 7.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 (31) hide show
  1. package/dist/example-data/public-directory-review-resource.d.ts +3 -3
  2. package/dist/src/canonical-reviewed-trust-input.js +30 -1
  3. package/dist/src/console/review-console-server.js +22 -4
  4. package/dist/src/index.d.ts +3 -3
  5. package/dist/src/index.js +1 -1
  6. package/dist/src/mcp/review-mcp.js +139 -36
  7. package/dist/src/review-resource.d.ts +7 -0
  8. package/dist/src/review-session-file.d.ts +7 -0
  9. package/dist/src/review-workbench/audit-rows.d.ts +1 -1
  10. package/dist/src/review-workbench/audit-rows.js +1 -0
  11. package/dist/src/review-workbench/queue-binding.d.ts +45 -2
  12. package/dist/src/review-workbench/queue-binding.js +45 -0
  13. package/dist/src/review-workbench/review-presentation.d.ts +23 -0
  14. package/dist/src/review-workbench/review-presentation.js +51 -10
  15. package/dist/src/review-workbench/review-queue-session.d.ts +47 -4
  16. package/dist/src/review-workbench/review-queue-session.js +73 -8
  17. package/dist/src/review-workbench/review-session-replay.d.ts +11 -1
  18. package/dist/src/review-workbench/review-session-replay.js +35 -1
  19. package/dist/src/review-workbench/review-workbench-css.generated.js +94 -0
  20. package/dist/src/review-workbench/review-workbench-element.d.ts +10 -0
  21. package/dist/src/review-workbench/review-workbench-element.js +21 -3
  22. package/dist/src/review-workbench/review-workbench.css +94 -0
  23. package/dist/src/review-workbench/review-workbench.d.ts +28 -8
  24. package/dist/src/review-workbench/review-workbench.js +157 -62
  25. package/dist/src/review-workbench/review-workbench.standalone.css +94 -0
  26. package/dist/src/review-workbench/server-review-session.d.ts +18 -0
  27. package/dist/src/review-workbench/server-review-session.js +20 -2
  28. package/dist/src/surface-reviewed-extraction.d.ts +23 -0
  29. package/dist/src/surface-reviewed-extraction.js +64 -1
  30. package/dist/src/to-surface.js +32 -3
  31. package/package.json +1 -1
@@ -3,7 +3,9 @@ import { type ReviewDecisionModeIssue } from "./producer-decision-mode.js";
3
3
  import { type ReviewSessionReplayIssue, type ReviewSessionReplayWarning } from "./review-session-replay.js";
4
4
  import type { ReviewDecision, ReviewSessionEvent } from "../review-resource.js";
5
5
  import { type ReviewQueueBinding } from "./queue-binding.js";
6
+ import type { ExtractionEnvelopeImport, ExtractionEnvelopeImportResult } from "../extraction-envelope.js";
6
7
  import { type ReviewQueueSessionState } from "./review-queue-session.js";
8
+ import type { ReviewPresentationAdapter } from "./review-presentation.js";
7
9
  export interface ServerReviewSessionRecord {
8
10
  readonly sessionName: string;
9
11
  readonly snapshot: ReviewQueueSessionState;
@@ -57,6 +59,8 @@ export interface DeriveServerReviewSessionApplyResultOptions {
57
59
  readonly currentSnapshot?: ReviewQueueSessionState;
58
60
  readonly currentEventCount?: number;
59
61
  readonly requiredResolvedItems?: ReviewSessionApplyResolutionRequirement;
62
+ /** The adapter the reviewer's card used; the recorded decision prompts render the target label and values with it. */
63
+ readonly presentationAdapter?: ReviewPresentationAdapter;
60
64
  /**
61
65
  * The queue binding taken when this session opened (see
62
66
  * ./queue-binding.ts). When present, the apply derivation refuses unless the
@@ -70,6 +74,15 @@ export interface DeriveServerReviewSessionApplyResultOptions {
70
74
  * consumer's storage, not be recomputed at call time.
71
75
  */
72
76
  readonly binding?: ReviewQueueBinding;
77
+ /**
78
+ * The extraction import record the snapshot's items were built from, as
79
+ * stored beside the session. When present, the derivation refuses (throws
80
+ * {@link UnattestedExtractionQueueError}) unless the record's snapshot items
81
+ * match it (see `validateReviewQueueAgainstExtractionImport`). When absent and
82
+ * the snapshot's items carry the extraction-envelope binding, the result
83
+ * carries an `unverified-extraction-queue` warning: nothing checked them.
84
+ */
85
+ readonly extractionImport?: ExtractionEnvelopeImport | ExtractionEnvelopeImportResult;
73
86
  }
74
87
  export declare function createServerReviewSessionRecord(options: CreateServerReviewSessionRecordOptions): ServerReviewSessionRecord;
75
88
  export declare function hashReviewSessionSnapshot(snapshot: ReviewQueueSessionState): string;
@@ -104,6 +117,8 @@ export interface ApplyReviewSessionOptions<TAction = never> {
104
117
  readonly currentSnapshot?: ReviewQueueSessionState;
105
118
  readonly currentEventCount?: number;
106
119
  readonly requiredResolvedItems?: ReviewSessionApplyResolutionRequirement;
120
+ /** See {@link DeriveServerReviewSessionApplyResultOptions.extractionImport}. */
121
+ readonly extractionImport?: ExtractionEnvelopeImport | ExtractionEnvelopeImportResult;
107
122
  /** When true, each result is checked against its item's producerPolicy.decisionMode. */
108
123
  readonly enforceProducerPolicy?: boolean;
109
124
  /** When provided, results are mapped to product apply actions in the same call. */
@@ -115,6 +130,9 @@ export type ApplyReviewSessionIssue = ReviewSessionApplyIssue | {
115
130
  } | {
116
131
  readonly code: "invalid-events";
117
132
  readonly message: string;
133
+ } | {
134
+ readonly code: "unattested-extraction-queue";
135
+ readonly message: string;
118
136
  } | {
119
137
  readonly code: "decision-mode-violation";
120
138
  readonly reviewItemName: string;
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { deriveReviewSessionApplyResultForSnapshot, mapReviewWorkbenchResultsToApplyActions, ReviewApplyActionMappingError, } from "./review-workbench.js";
3
3
  import { validateReviewDecisionMode, } from "./producer-decision-mode.js";
4
4
  import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
5
- import { assertReviewQueueBinding } from "./queue-binding.js";
5
+ import { assertReviewQueueBinding, attestReviewQueueExtraction, UnattestedExtractionQueueError, } from "./queue-binding.js";
6
6
  import { replayReviewSessionEvents } from "./review-queue-session.js";
7
7
  import { canonicalJson } from "./canonical.js";
8
8
  export class StaleServerReviewSessionError extends Error {
@@ -104,11 +104,25 @@ export function deriveServerReviewSessionApplyResult(options) {
104
104
  assertServerReviewSessionFreshness(options.record, options.currentSnapshot, options.currentEventCount);
105
105
  }
106
106
  assertServerReviewSessionEvents(options.record, options.events);
107
- return deriveReviewSessionApplyResultForSnapshot({
107
+ const attestation = attestReviewQueueExtraction(options.record.snapshot.items, options.extractionImport);
108
+ if (attestation.state === "diverges") {
109
+ throw new UnattestedExtractionQueueError(attestation.issues);
110
+ }
111
+ const derived = deriveReviewSessionApplyResultForSnapshot({
108
112
  snapshot: options.record.snapshot,
109
113
  events: options.events,
110
114
  requiredResolvedItems: options.requiredResolvedItems,
115
+ presentationAdapter: options.presentationAdapter,
111
116
  });
117
+ return attestation.state === "unverified"
118
+ ? {
119
+ ...derived,
120
+ warnings: [
121
+ ...(derived.warnings ?? []),
122
+ { code: "unverified-extraction-queue", itemNames: attestation.itemNames, message: attestation.message },
123
+ ],
124
+ }
125
+ : derived;
112
126
  }
113
127
  function staleIssuesForComparison(comparison) {
114
128
  const issues = [];
@@ -164,9 +178,13 @@ export function applyReviewSession(options) {
164
178
  currentSnapshot: options.currentSnapshot,
165
179
  currentEventCount: options.currentEventCount,
166
180
  requiredResolvedItems: options.requiredResolvedItems,
181
+ extractionImport: options.extractionImport,
167
182
  });
168
183
  }
169
184
  catch (error) {
185
+ if (error instanceof UnattestedExtractionQueueError) {
186
+ return { ok: false, issues: [{ code: "unattested-extraction-queue", message: error.message }], decisions: [], results: [], actions: [] };
187
+ }
170
188
  if (error instanceof StaleServerReviewSessionError) {
171
189
  return { ok: false, issues: [{ code: "stale-session", message: error.message }], decisions: [], results: [], actions: [] };
172
190
  }
@@ -1,6 +1,29 @@
1
1
  import type { SurveyExtractionEnvelopeImport, SurveyExtractionReviewDecision, SurveyExtractionReviewItem } from "@kontourai/surface";
2
2
  import type { ExtractionEnvelopeImport } from "./extraction-envelope.js";
3
3
  import type { ReviewDecision, ReviewItem } from "./review-resource.js";
4
+ /**
5
+ * The version of the `@kontourai/surface` package whose entry point
6
+ * `resolveEntry` returns. Surface does not export its package.json, so this
7
+ * walks up from the entry to the nearest package.json named
8
+ * `@kontourai/surface`; other package.json files on the way (a nested
9
+ * `{ "type": "module" }`, say) are skipped. Throws a Survey error when the
10
+ * entry cannot be resolved, as in a bundle without `import.meta.resolve`, or
11
+ * no such package.json exists.
12
+ */
13
+ export declare function surfaceVersionFromEntry(resolveEntry: () => string): string;
14
+ /** Whether a Surface version accepts proposals without confidence. An unparseable version does not. */
15
+ export declare function surfaceAcceptsAbsentConfidence(version: string): boolean;
16
+ /**
17
+ * The record, typed for Surface's reviewed-extraction profile. A proposal
18
+ * without confidence passes through without one; no number is substituted.
19
+ * On a Surface older than 4.1.0, which rejects such a proposal, the record is
20
+ * refused by name instead.
21
+ *
22
+ * The version checked is that of the Surface copy Survey itself resolves. A
23
+ * consumer that projects with a different copy (for example Surface 4.0.0 of
24
+ * its own next to Survey's 4.2.0) gets Surface's own refusal instead of this
25
+ * one; dedupe Surface so that one copy is installed.
26
+ */
4
27
  export declare function toSurfaceReviewedExtractionImport(record: ExtractionEnvelopeImport): SurveyExtractionEnvelopeImport;
5
28
  export declare function toSurfaceReviewedExtractionItem(item: ReviewItem): SurveyExtractionReviewItem;
6
29
  export declare function toSurfaceReviewedExtractionDecision(decision: ReviewDecision): SurveyExtractionReviewDecision;
@@ -1,7 +1,70 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** The first Surface release whose reviewed-extraction profile accepts a proposal without confidence. */
5
+ const ABSENT_CONFIDENCE_SINCE = [4, 1];
6
+ let resolvedSurfaceVersion;
7
+ const UNKNOWN_SURFACE_VERSION = "Cannot determine the installed @kontourai/surface version; refusing to export proposals without confidence.";
8
+ /**
9
+ * The version of the `@kontourai/surface` package whose entry point
10
+ * `resolveEntry` returns. Surface does not export its package.json, so this
11
+ * walks up from the entry to the nearest package.json named
12
+ * `@kontourai/surface`; other package.json files on the way (a nested
13
+ * `{ "type": "module" }`, say) are skipped. Throws a Survey error when the
14
+ * entry cannot be resolved, as in a bundle without `import.meta.resolve`, or
15
+ * no such package.json exists.
16
+ */
17
+ export function surfaceVersionFromEntry(resolveEntry) {
18
+ let dir;
19
+ try {
20
+ dir = dirname(fileURLToPath(resolveEntry()));
21
+ }
22
+ catch (error) {
23
+ throw new Error(UNKNOWN_SURFACE_VERSION, { cause: error });
24
+ }
25
+ for (;;) {
26
+ try {
27
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
28
+ if (pkg.name === "@kontourai/surface" && typeof pkg.version === "string")
29
+ return pkg.version;
30
+ }
31
+ catch { /* no readable package.json here; keep walking up */ }
32
+ const parent = dirname(dir);
33
+ if (parent === dir)
34
+ throw new Error(UNKNOWN_SURFACE_VERSION);
35
+ dir = parent;
36
+ }
37
+ }
38
+ /** The version of the `@kontourai/surface` copy this module resolves, cached once found. */
39
+ function installedSurfaceVersion() {
40
+ return resolvedSurfaceVersion ??= surfaceVersionFromEntry(() => import.meta.resolve("@kontourai/surface"));
41
+ }
42
+ /** Whether a Surface version accepts proposals without confidence. An unparseable version does not. */
43
+ export function surfaceAcceptsAbsentConfidence(version) {
44
+ const match = /^(\d+)\.(\d+)\.\d+/.exec(version);
45
+ if (!match)
46
+ return false;
47
+ const [major, minor] = [Number(match[1]), Number(match[2])];
48
+ return major > ABSENT_CONFIDENCE_SINCE[0] || (major === ABSENT_CONFIDENCE_SINCE[0] && minor >= ABSENT_CONFIDENCE_SINCE[1]);
49
+ }
50
+ /**
51
+ * The record, typed for Surface's reviewed-extraction profile. A proposal
52
+ * without confidence passes through without one; no number is substituted.
53
+ * On a Surface older than 4.1.0, which rejects such a proposal, the record is
54
+ * refused by name instead.
55
+ *
56
+ * The version checked is that of the Surface copy Survey itself resolves. A
57
+ * consumer that projects with a different copy (for example Surface 4.0.0 of
58
+ * its own next to Survey's 4.2.0) gets Surface's own refusal instead of this
59
+ * one; dedupe Surface so that one copy is installed.
60
+ */
1
61
  export function toSurfaceReviewedExtractionImport(record) {
2
62
  const unreported = record.spec.envelope.result.proposals.findIndex((proposal) => proposal.confidence === undefined);
3
63
  if (unreported !== -1) {
4
- throw new Error(`Surface's reviewed-extraction profile requires a proposer confidence on every proposal; proposal ${unreported} of ${record.metadata.name} reports none.`);
64
+ const version = installedSurfaceVersion();
65
+ if (!surfaceAcceptsAbsentConfidence(version)) {
66
+ throw new Error(`@kontourai/surface ${version} requires a proposer confidence on every proposal (4.1.0 and later do not); proposal ${unreported} of ${record.metadata.name} reports none.`);
67
+ }
5
68
  }
6
69
  return record;
7
70
  }
@@ -7,6 +7,16 @@ import { canonicalJson } from "./review-workbench/canonical.js";
7
7
  /** Minimum labeled samples a calibration group needs before its empirical
8
8
  * accuracy is emitted as a `conclusionConfidence.value`. */
9
9
  const DEFAULT_CALIBRATION_MIN_SAMPLES = 20;
10
+ /**
11
+ * The evidence's own `confidence` replaces any a metadata spread carried. When
12
+ * the extraction reported none, the key is removed rather than set to
13
+ * undefined, so absence is absence in memory as well as on the wire.
14
+ */
15
+ function withoutUndefinedConfidence(metadata) {
16
+ if (metadata.confidence === undefined)
17
+ delete metadata.confidence;
18
+ return metadata;
19
+ }
10
20
  /**
11
21
  * Thrown when a claim's trusted status or value does not agree with the review
12
22
  * outcome it cites, or when the governing review cannot be chosen.
@@ -80,7 +90,8 @@ export function buildSurveyTrustBundle(input, options = {}) {
80
90
  derivationEdges: projection.derivationEdges,
81
91
  confidenceBasis: {
82
92
  sourceQuality: "moderate",
83
- extractionConfidence: candidate.confidence ?? extraction.confidence,
93
+ // An absent confidence stays absent; no key, no default.
94
+ ...((candidate.confidence ?? extraction.confidence) !== undefined ? { extractionConfidence: candidate.confidence ?? extraction.confidence } : {}),
84
95
  // An auto-accept policy checked nothing but the proposer's own
85
96
  // confidence, so its claims carry system authority and weak evidence;
86
97
  // only a human review earns operator authority.
@@ -168,7 +179,7 @@ export function buildSurveyTrustBundle(input, options = {}) {
168
179
  observedAt: rawSource.observedAt,
169
180
  collectedBy: projection.collectedBy,
170
181
  integrityRef: rawSource.checksum,
171
- metadata: {
182
+ metadata: withoutUndefinedConfidence({
172
183
  ...rawSource.metadata,
173
184
  ...extraction.metadata,
174
185
  ...candidate.metadata,
@@ -177,7 +188,7 @@ export function buildSurveyTrustBundle(input, options = {}) {
177
188
  locatorScheme: rawSource.locatorScheme,
178
189
  ...(rawSource.resolution ? { provenanceResolution: rawSource.resolution } : {}),
179
190
  confidence: candidate.confidence ?? extraction.confidence,
180
- },
191
+ }),
181
192
  });
182
193
  const rationale = projectionReview?.rationale ?? candidateSet.rationale;
183
194
  events.push({
@@ -606,8 +617,26 @@ function buildSurveyMetadata(input) {
606
617
  },
607
618
  }
608
619
  : {}),
620
+ // A claim whose value was chosen from conflicting proposed values lists
621
+ // every value and marks the chosen one, so the claim never reads as if its
622
+ // value had been the only one proposed.
623
+ ...(proposedRoleCount(input.candidateSet) > 1
624
+ ? {
625
+ candidateSetStatus: input.candidateSet.status,
626
+ candidates: input.candidateSet.candidates.map((candidate) => ({
627
+ candidateId: candidate.id,
628
+ value: candidate.value,
629
+ ...(candidate.id === input.candidate.id ? { selected: true } : {}),
630
+ ...(candidate.rejectionReason !== undefined ? { rejectionReason: candidate.rejectionReason } : {}),
631
+ })),
632
+ }
633
+ : {}),
609
634
  };
610
635
  }
636
+ /** How many of a set's candidates carry the `proposed` role (Survey writes it to `metadata.role`). */
637
+ function proposedRoleCount(candidateSet) {
638
+ return candidateSet.candidates.filter((candidate) => candidate.metadata?.role === "proposed").length;
639
+ }
611
640
  function statusFor(input) {
612
641
  if (input.review)
613
642
  return input.review.status;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "6.1.0",
3
+ "version": "7.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",