@kontourai/survey 1.4.0 → 1.5.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.
@@ -2,7 +2,7 @@ import { type DeriveReviewSessionApplyResultForSnapshotResult, type MapReviewWor
2
2
  import { type ReviewDecisionModeIssue } from "./producer-decision-mode.js";
3
3
  import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
4
4
  import type { ReviewDecision, ReviewSessionEvent } from "../review-resource.js";
5
- import type { ReviewQueueSessionState } from "./review-queue-session.js";
5
+ import { type ReviewQueueSessionState } from "./review-queue-session.js";
6
6
  export interface ServerReviewSessionRecord {
7
7
  readonly sessionName: string;
8
8
  readonly snapshot: ReviewQueueSessionState;
@@ -59,6 +59,19 @@ export interface DeriveServerReviewSessionApplyResultOptions {
59
59
  }
60
60
  export declare function createServerReviewSessionRecord(options: CreateServerReviewSessionRecordOptions): ServerReviewSessionRecord;
61
61
  export declare function hashReviewSessionSnapshot(snapshot: ReviewQueueSessionState): string;
62
+ /**
63
+ * Project the current review session state from a snapshot plus its
64
+ * append-only event log: replay events over the snapshot when any exist,
65
+ * otherwise return the snapshot itself unchanged (same object reference —
66
+ * no eager replay, no copy). Collapses the
67
+ * `events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot`
68
+ * conditional that was duplicated across the MCP and console review
69
+ * adapters (src/mcp/review-mcp.ts, src/console/review-console-server.ts) —
70
+ * same shape of fix as canonicalJson in ./canonical.ts, which was
71
+ * extracted after two near-identical copies of a derivation desynced
72
+ * (audit 2026-06-28, ops#24).
73
+ */
74
+ export declare function currentSessionState(snapshot: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): ReviewQueueSessionState;
62
75
  export declare function compareServerReviewSessionFreshness(record: ServerReviewSessionRecord, snapshot: ReviewQueueSessionState, eventCount?: number): ServerReviewSessionFreshnessComparison;
63
76
  export declare function assertServerReviewSessionFreshness(record: ServerReviewSessionRecord, snapshot: ReviewQueueSessionState, eventCount?: number): void;
64
77
  export declare function validateServerReviewSessionEvents(record: ServerReviewSessionRecord, events: readonly ReviewSessionEvent[]): ServerReviewSessionEventValidationIssue[];
@@ -2,6 +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 { replayReviewSessionEvents } from "./review-queue-session.js";
5
6
  import { canonicalJson } from "./canonical.js";
6
7
  export class StaleServerReviewSessionError extends Error {
7
8
  name = "StaleServerReviewSessionError";
@@ -34,6 +35,21 @@ export function createServerReviewSessionRecord(options) {
34
35
  export function hashReviewSessionSnapshot(snapshot) {
35
36
  return sha256(canonicalJson(snapshot));
36
37
  }
38
+ /**
39
+ * Project the current review session state from a snapshot plus its
40
+ * append-only event log: replay events over the snapshot when any exist,
41
+ * otherwise return the snapshot itself unchanged (same object reference —
42
+ * no eager replay, no copy). Collapses the
43
+ * `events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot`
44
+ * conditional that was duplicated across the MCP and console review
45
+ * adapters (src/mcp/review-mcp.ts, src/console/review-console-server.ts) —
46
+ * same shape of fix as canonicalJson in ./canonical.ts, which was
47
+ * extracted after two near-identical copies of a derivation desynced
48
+ * (audit 2026-06-28, ops#24).
49
+ */
50
+ export function currentSessionState(snapshot, events) {
51
+ return events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot;
52
+ }
37
53
  export function compareServerReviewSessionFreshness(record, snapshot, eventCount) {
38
54
  const actualSnapshotHash = hashReviewSessionSnapshot(snapshot);
39
55
  const eventCountMatches = record.eventCount === undefined || eventCount === undefined || record.eventCount === eventCount;
@@ -20,6 +20,7 @@
20
20
  * so that resolveInquiry can resolve across systems with weakest-link
21
21
  * capping.
22
22
  */
23
+ import { AUTO_ACCEPT_ACTOR, AUTO_ACCEPT_WITHIN_COMFORT_ZONE, getProducerProposal, meetsAutoAcceptThreshold, projectProposalsToCandidateSet, } from "./producer-profile.js";
23
24
  import { buildSurveyTrustBundle } from "./to-surface.js";
24
25
  // ---------------------------------------------------------------------------
25
26
  // Canonical pair key
@@ -84,10 +85,7 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
84
85
  const subjectId = mappingSubjectId(first.sourceField, first.targetField);
85
86
  const candidateSetId = `schema-mapping.candidate-set.${pairKey}`;
86
87
  const claimId = `schema-mapping.claim.${pairKey}`;
87
- // Detect conflict: proposals disagree on relation
88
- const relations = new Set(pairProposals.map((p) => p.relation));
89
- const status = relations.size > 1 ? "conflict" : "needs-review";
90
- const candidates = pairProposals.map((proposal) => {
88
+ const candidateSetProposals = pairProposals.map((proposal) => {
91
89
  // Use the source system's RawSource for this extraction
92
90
  const rawSource = sourceById.get(proposal.sourceField.system) ?? rawSources[0];
93
91
  const extractionId = `schema-mapping.extraction.${proposal.id}`;
@@ -120,7 +118,7 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
120
118
  };
121
119
  extractions.push(extraction);
122
120
  return {
123
- id: `schema-mapping.candidate.${proposal.id}`,
121
+ candidateId: `schema-mapping.candidate.${proposal.id}`,
124
122
  extractionId,
125
123
  value: {
126
124
  relation: proposal.relation,
@@ -128,45 +126,41 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
128
126
  conversion: proposal.conversion,
129
127
  },
130
128
  confidence: proposal.confidence,
129
+ equivalenceKey: proposal.relation,
131
130
  metadata: {
132
- schemaMappingProposal: {
133
- proposalId: proposal.id,
134
- sourceField: proposal.sourceField,
135
- targetField: proposal.targetField,
136
- relation: proposal.relation,
137
- conversion: proposal.conversion,
138
- evidence: proposal.evidence,
139
- confidence: proposal.confidence,
140
- rationale: proposal.rationale,
141
- proposedBy: proposal.proposedBy,
142
- proposedAt: proposal.proposedAt,
143
- },
131
+ proposalId: proposal.id,
132
+ sourceField: proposal.sourceField,
133
+ targetField: proposal.targetField,
134
+ relation: proposal.relation,
135
+ conversion: proposal.conversion,
136
+ evidence: proposal.evidence,
137
+ confidence: proposal.confidence,
138
+ rationale: proposal.rationale,
139
+ proposedBy: proposal.proposedBy,
140
+ proposedAt: proposal.proposedAt,
144
141
  },
145
142
  };
146
143
  });
147
- const candidateSet = {
148
- id: candidateSetId,
149
- target: `schema-mapping:${pairKey}`,
150
- candidates,
151
- selectedCandidateId: status !== "conflict" ? candidates[0]?.id : undefined,
152
- status,
153
- rationale: status === "conflict"
154
- ? `Proposals disagree on relation for pair ${pairKey}: ${[...relations].join(", ")}`
155
- : `${candidates.length} proposal(s) agree on relation "${first.relation}" for pair ${pairKey}.`,
156
- metadata: {
144
+ const { candidateSet, candidates } = projectProposalsToCandidateSet(`schema-mapping:${pairKey}`, candidateSetProposals, {
145
+ candidateSetId,
146
+ candidateSetMetadata: {
157
147
  schemaMapping: {
158
148
  pairKey,
159
149
  sourceField: first.sourceField,
160
150
  targetField: first.targetField,
161
151
  },
162
152
  },
163
- };
153
+ candidateSetRationale: (status, proposals) => status === "conflict"
154
+ ? `Proposals disagree on relation for pair ${pairKey}: ${[...new Set(proposals.map((p) => p.equivalenceKey))].join(", ")}`
155
+ : `${proposals.length} proposal(s) agree on relation "${first.relation}" for pair ${pairKey}.`,
156
+ });
157
+ candidateSet.selectedCandidateId = candidateSet.status !== "conflict" ? candidates[0]?.id : undefined;
164
158
  candidateSets.push(candidateSet);
165
159
  // Auto-accept policy: non-conflicting proposals above threshold → assumed
166
160
  let autoReviewStatus;
167
- if (status !== "conflict" && options.autoAcceptMinConfidence !== undefined) {
161
+ if (candidateSet.status !== "conflict" && options.autoAcceptMinConfidence !== undefined) {
168
162
  const topConfidence = Math.max(...pairProposals.map((p) => p.confidence));
169
- if (topConfidence >= options.autoAcceptMinConfidence) {
163
+ if (meetsAutoAcceptThreshold(topConfidence, options.autoAcceptMinConfidence)) {
170
164
  autoReviewStatus = "assumed";
171
165
  }
172
166
  }
@@ -180,10 +174,10 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
180
174
  candidateSetId,
181
175
  candidateId: selectedCandidate.id,
182
176
  status: autoReviewStatus,
183
- actor: "auto-accept-policy",
177
+ actor: AUTO_ACCEPT_ACTOR,
184
178
  reviewedAt: generatedAt,
185
179
  rationale: `Auto-accepted: confidence ${selectedCandidate.confidence} >= threshold ${options.autoAcceptMinConfidence}`,
186
- withinComfortZone: true,
180
+ withinComfortZone: AUTO_ACCEPT_WITHIN_COMFORT_ZONE,
187
181
  });
188
182
  }
189
183
  // Project to ClaimTarget (subjectType "system-field", fieldOrBehavior "maps-to")
@@ -191,7 +185,7 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
191
185
  const review = reviewOutcomes.find((r) => r.candidateSetId === candidateSetId);
192
186
  const claimStatus = review
193
187
  ? review.status
194
- : (status === "conflict" ? "disputed" : undefined);
188
+ : (candidateSet.status === "conflict" ? "disputed" : undefined);
195
189
  const claimTarget = {
196
190
  id: claimId,
197
191
  candidateSetId,
@@ -266,7 +260,7 @@ export function mappingReviewToSurface(reviewedMappings, options = {}) {
266
260
  const rawSources = [];
267
261
  const seenSources = new Set();
268
262
  for (const rm of accepted) {
269
- const meta = rm.selectedCandidate.metadata?.schemaMappingProposal;
263
+ const meta = getProducerProposal(rm.selectedCandidate);
270
264
  const sourceField = meta?.sourceField ?? rm.proposal.sourceField;
271
265
  const sourceId = `schema-mapping.source.${sourceField.system}`;
272
266
  if (!seenSources.has(sourceId)) {
@@ -290,7 +284,7 @@ export function mappingReviewToSurface(reviewedMappings, options = {}) {
290
284
  const reviewOutcomes = [];
291
285
  const claims = [];
292
286
  for (const rm of accepted) {
293
- const meta = rm.selectedCandidate.metadata?.schemaMappingProposal;
287
+ const meta = getProducerProposal(rm.selectedCandidate);
294
288
  const sourceField = meta?.sourceField ?? rm.proposal.sourceField;
295
289
  const targetField = meta?.targetField ?? rm.proposal.targetField;
296
290
  const relation = (meta?.relation ?? rm.proposal.relation);
@@ -379,7 +373,7 @@ export function mappingReviewToSurface(reviewedMappings, options = {}) {
379
373
  // subject and back-references the mapping claim via mappingClaimId.
380
374
  const identityLinks = [];
381
375
  for (const rm of accepted) {
382
- const meta = rm.selectedCandidate.metadata?.schemaMappingProposal;
376
+ const meta = getProducerProposal(rm.selectedCandidate);
383
377
  const sourceField = meta?.sourceField ?? rm.proposal.sourceField;
384
378
  const targetField = meta?.targetField ?? rm.proposal.targetField;
385
379
  const relation = (meta?.relation ?? rm.proposal.relation);
@@ -1,4 +1,5 @@
1
1
  import { buildObservation } from "./observation-helper.js";
2
+ import { assertReviewOutcomeDiscipline } from "./producer-discipline.js";
2
3
  export class SourceOfAuthorityObservationBuilder {
3
4
  state;
4
5
  constructor(args) {
@@ -115,15 +116,11 @@ function assertVerifiedPosture(input) {
115
116
  if (!input.extraction.locator) {
116
117
  throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without a source locator`);
117
118
  }
118
- if (!input.reviewOutcome) {
119
- throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without a review outcome`);
120
- }
121
- if (!input.reviewOutcome.actor) {
122
- throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without review actor authority`);
123
- }
124
- if (!input.reviewOutcome.reviewedAt) {
125
- throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without reviewedAt`);
126
- }
119
+ assertReviewOutcomeDiscipline({
120
+ subject: `Source-of-authority observation ${input.id}`,
121
+ status,
122
+ review: input.reviewOutcome,
123
+ });
127
124
  }
128
125
  function claimStatus(claimStatusValue, reviewStatus) {
129
126
  return claimStatusValue ?? reviewStatus;
@@ -1,4 +1,5 @@
1
1
  import { buildReviewProofAnchor } from "./review-proof.js";
2
+ import { assertReviewOutcomeDiscipline } from "./producer-discipline.js";
2
3
  export function buildSurveyTrustBundle(input, options = {}) {
3
4
  const rawSources = indexById(input.rawSources, "raw source");
4
5
  const extractions = indexById(input.extractions, "extraction");
@@ -334,15 +335,11 @@ function statusFor(input) {
334
335
  return "proposed";
335
336
  }
336
337
  function assertProducerDiscipline(input) {
337
- if ((input.status === "verified" || input.status === "assumed") && !input.review) {
338
- throw new Error(`Claim ${input.projection.id} cannot be ${input.status} without a review outcome`);
339
- }
340
- if ((input.status === "verified" || input.status === "assumed") && !input.review?.actor) {
341
- throw new Error(`Claim ${input.projection.id} cannot be ${input.status} without review actor authority`);
342
- }
343
- if ((input.status === "verified" || input.status === "assumed") && !input.review?.reviewedAt) {
344
- throw new Error(`Claim ${input.projection.id} cannot be ${input.status} without reviewedAt`);
345
- }
338
+ assertReviewOutcomeDiscipline({
339
+ subject: `Claim ${input.projection.id}`,
340
+ status: input.status,
341
+ review: input.review,
342
+ });
346
343
  if (input.rawSource.kind !== "manual-entry" && !input.extraction.locator) {
347
344
  throw new Error(`Claim ${input.projection.id} needs a source locator for ${input.rawSource.kind}`);
348
345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "1.4.0",
3
+ "version": "1.5.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",
@@ -55,8 +55,11 @@
55
55
  "typecheck": "tsc --noEmit",
56
56
  "test": "npm run build && node --test dist/tests/*.test.js",
57
57
  "test:browser": "playwright test",
58
- "verify": "node scripts/check-content-boundary.cjs && npm run typecheck && npm test && npm run check:review-workbench-assets && npm run check:review-workbench && npm run test:browser",
58
+ "verify": "node scripts/check-content-boundary.cjs && npm run check:decisions && npm run typecheck && npm test && npm run check:review-workbench-assets && npm run check:review-workbench && npm run test:browser",
59
59
  "check:content-boundary": "node scripts/check-content-boundary.cjs",
60
+ "check:decisions": "node scripts/check-decisions.cjs check",
61
+ "gen:decisions-index": "node scripts/check-decisions.cjs gen-index",
62
+ "freeze:adrs": "node scripts/freeze-adrs.mjs",
60
63
  "sync:review-workbench-assets": "node scripts/sync-review-workbench-assets.cjs",
61
64
  "check:review-workbench-assets": "node scripts/sync-review-workbench-assets.cjs --check",
62
65
  "check:review-workbench": "npm run check:review-workbench:static && npm run check:generated-css",