@kontourai/survey 1.3.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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/example-data/corrected-document-candidates.js +2 -2
  3. package/dist/example-data/public-directory-review-resource.d.ts +2 -2
  4. package/dist/example-data/public-directory-review-resource.js +2 -2
  5. package/dist/example-data/public-field-review.js +2 -2
  6. package/dist/example-data/regulated-document-review-resource.d.ts +2 -2
  7. package/dist/example-data/regulated-document-review-resource.js +2 -2
  8. package/dist/examples/public-field-observation.js +1 -1
  9. package/dist/examples/review-workbench/downstream-public-directory-adapter.js +1 -1
  10. package/dist/src/agent-utterance.d.ts +25 -0
  11. package/dist/src/agent-utterance.js +207 -95
  12. package/dist/src/console/review-console-server.js +3 -7
  13. package/dist/src/field-observation.d.ts +2 -16
  14. package/dist/src/field-observation.js +2 -14
  15. package/dist/src/index.d.ts +1 -1
  16. package/dist/src/inquiry-mapping.d.ts +2 -46
  17. package/dist/src/inquiry-mapping.js +37 -39
  18. package/dist/src/mcp/review-mcp.js +12 -12
  19. package/dist/src/observation-helper.d.ts +47 -1
  20. package/dist/src/observation-helper.js +41 -2
  21. package/dist/src/oversight-metrics.d.ts +2 -2
  22. package/dist/src/oversight-metrics.js +1 -1
  23. package/dist/src/producer-discipline.d.ts +40 -0
  24. package/dist/src/producer-discipline.js +13 -0
  25. package/dist/src/producer-profile.d.ts +134 -0
  26. package/dist/src/producer-profile.js +124 -0
  27. package/dist/src/raw-source.d.ts +18 -0
  28. package/dist/src/raw-source.js +28 -13
  29. package/dist/src/repeated-observation.d.ts +2 -16
  30. package/dist/src/repeated-observation.js +2 -14
  31. package/dist/src/review-proof.d.ts +2 -2
  32. package/dist/src/review-proof.js +2 -2
  33. package/dist/src/review-resource.d.ts +5 -1
  34. package/dist/src/review-workbench/review-workbench-data.d.ts +10 -10
  35. package/dist/src/review-workbench/review-workbench-data.js +6 -6
  36. package/dist/src/review-workbench/review-workbench.js +1 -1
  37. package/dist/src/review-workbench/server-review-session.d.ts +14 -1
  38. package/dist/src/review-workbench/server-review-session.js +16 -0
  39. package/dist/src/schema-mapping.js +32 -38
  40. package/dist/src/source-of-authority-observation.js +6 -9
  41. package/dist/src/to-surface.js +8 -11
  42. package/dist/src/types.d.ts +6 -1
  43. package/dist/src/vocabulary.d.ts +46 -4
  44. package/dist/src/vocabulary.js +29 -3
  45. package/package.json +6 -3
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Producer Profile core — ADR 0003 §4, CONTEXT.md "Producer Profile".
3
+ *
4
+ * This module carries the shared scaffolding every Producer Profile
5
+ * (inquiry-mapping, schema-mapping, and — from Slice 4 — agent-utterance)
6
+ * needs to turn its own proposals into Survey's existing Candidate/Candidate
7
+ * Set records: a generic proposal -> Candidate Set projection grouped by
8
+ * target, the Candidate Conflict rule, and one canonical `Candidate.metadata`
9
+ * key with a typed accessor, replacing each profile's hand-rolled projection,
10
+ * conflict check, and `as`-cast metadata round-trip.
11
+ *
12
+ * This is a module-internal seam: its exports are consumed directly by
13
+ * profile modules via relative import and are NOT re-exported from
14
+ * `src/index.ts`.
15
+ *
16
+ * Hard constraint (ADR 0003 §4): this module never decides a review outcome
17
+ * or a claim status. It only shapes proposal-backed Candidate/Candidate Set
18
+ * records — every profile still routes its output through Survey's existing
19
+ * review -> claim machinery unchanged.
20
+ */
21
+ // ---------------------------------------------------------------------------
22
+ // Canonical proposal-metadata key
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * The one canonical `Candidate.metadata` key every Producer Profile uses to
26
+ * carry its profile-specific proposal payload. Replaces the per-profile keys
27
+ * (`mappingProposal`, `schemaMappingProposal`) each profile used before
28
+ * adopting this core module.
29
+ */
30
+ export const PRODUCER_PROPOSAL_METADATA_KEY = "producerProposal";
31
+ // ---------------------------------------------------------------------------
32
+ // Candidate Conflict rule
33
+ // ---------------------------------------------------------------------------
34
+ /**
35
+ * The shared Candidate Conflict rule: a group of proposals conflicts iff it
36
+ * carries more than one distinct `equivalenceKey`. A group of 0 or 1
37
+ * proposals can never conflict.
38
+ */
39
+ export function hasCandidateConflict(proposals) {
40
+ return new Set(proposals.map((p) => p.equivalenceKey)).size > 1;
41
+ }
42
+ /**
43
+ * Build one Candidate Set (and its Candidates) from one target's proposal
44
+ * group. Grouping proposals by target itself stays a caller concern — this
45
+ * function projects exactly one group per call; it never reaches across
46
+ * multiple targets on its own. `status` is `"conflict"` when
47
+ * {@link hasCandidateConflict} is true for `proposals`, otherwise
48
+ * `"needs-review"` (an empty `proposals` array yields `"needs-review"` with
49
+ * an empty `candidates` array). `selectedCandidateId` is left unset — both
50
+ * profiles compute it themselves, or not at all, per their own review flow.
51
+ */
52
+ export function projectProposalsToCandidateSet(target, proposals, options) {
53
+ const candidates = proposals.map((proposal) => ({
54
+ id: proposal.candidateId,
55
+ extractionId: proposal.extractionId,
56
+ value: proposal.value,
57
+ confidence: proposal.confidence,
58
+ metadata: {
59
+ [PRODUCER_PROPOSAL_METADATA_KEY]: proposal.metadata,
60
+ },
61
+ }));
62
+ const status = hasCandidateConflict(proposals) ? "conflict" : "needs-review";
63
+ const candidateSet = {
64
+ id: options.candidateSetId,
65
+ target,
66
+ candidates,
67
+ status,
68
+ rationale: options.candidateSetRationale?.(status, proposals),
69
+ metadata: options.candidateSetMetadata,
70
+ };
71
+ return { candidateSet, candidates };
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Typed proposal-metadata accessor
75
+ // ---------------------------------------------------------------------------
76
+ /**
77
+ * Typed read-back of the proposal payload a Candidate carries under
78
+ * {@link PRODUCER_PROPOSAL_METADATA_KEY}. Returns `undefined` if the
79
+ * Candidate, its `metadata`, or the key itself is absent — never throws.
80
+ *
81
+ * No fallback reads of any legacy per-profile metadata key are performed
82
+ * (Owner decision: no legacy support).
83
+ */
84
+ export function getProducerProposal(candidate) {
85
+ return candidate?.metadata?.[PRODUCER_PROPOSAL_METADATA_KEY];
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Shared auto-accept primitives
89
+ // ---------------------------------------------------------------------------
90
+ /**
91
+ * Actor identity every Producer Profile's auto-accept policy uses when it
92
+ * accepts a proposal without human review. Shared literal — see ADR 0003
93
+ * §4 (the core never decides "verified"; auto-accept only ever produces
94
+ * "assumed" + comfort-zone true).
95
+ */
96
+ export const AUTO_ACCEPT_ACTOR = "auto-accept-policy";
97
+ /**
98
+ * The comfort-zone posture every Producer Profile's auto-accept policy
99
+ * sets when it accepts a proposal: `withinComfortZone: true` always — an
100
+ * auto-accepted proposal is, by definition, one the policy's declared
101
+ * threshold covers, so there is nothing "outside comfort zone" about an
102
+ * auto-accept decision (ADR 0003 §4).
103
+ */
104
+ export const AUTO_ACCEPT_WITHIN_COMFORT_ZONE = true;
105
+ /**
106
+ * The one auto-accept threshold rule every Producer Profile applies: a
107
+ * confidence value clears an auto-accept policy iff it is at or above
108
+ * (inclusive) the policy's minimum confidence. This is the only piece of
109
+ * auto-accept *mechanics* that is identical across profiles today — each
110
+ * profile decides its own iteration granularity (per-proposal filter vs.
111
+ * per-group max-confidence gate) and output record shape around this call;
112
+ * the core does not decide that.
113
+ *
114
+ * These three exports are the ONLY auto-accept semantics the two profiles
115
+ * genuinely share today (see the Slice 3 plan's Part (a) field-by-field
116
+ * diff table). Everything else about auto-accept — iteration granularity
117
+ * (per-proposal vs. per-group), output record type and cardinality, id
118
+ * templates, timestamp source, and rationale string format — diverges
119
+ * between profiles and stays entirely per-profile; this module does not
120
+ * decide any of it.
121
+ */
122
+ export function meetsAutoAcceptThreshold(confidence, minConfidence) {
123
+ return confidence >= minConfidence;
124
+ }
@@ -39,6 +39,24 @@ export interface PolicyStandardSourceInput extends Omit<RawSourceInput, "locator
39
39
  locatorScheme?: LocatorScheme;
40
40
  metadata?: Record<string, unknown>;
41
41
  }
42
+ /**
43
+ * Raw Source kinds whose factories accept a caller-supplied `locatorScheme`
44
+ * override and otherwise fall back to a per-kind default. `uploaded-document`
45
+ * has no default (`undefined`) because `UploadedDocumentSourceInput.locatorScheme`
46
+ * is required, not optional — modeled here as a real, explicit table entry
47
+ * rather than an omission, so a caller-supplied value is the only source of
48
+ * truth for that kind.
49
+ *
50
+ * Module-internal seam: consumed by relative import from
51
+ * `rawSourceWithDefaultLocatorScheme` below and by
52
+ * `tests/raw-source-defaults.test.ts`, NOT re-exported from `src/index.ts`.
53
+ */
54
+ export declare const DEFAULT_LOCATOR_SCHEME: {
55
+ "uploaded-document": undefined;
56
+ "api-record": LocatorScheme;
57
+ "web-page": LocatorScheme;
58
+ "manual-entry": LocatorScheme;
59
+ };
42
60
  export declare function uploadedDocumentSource(input: UploadedDocumentSourceInput): RawSource;
43
61
  export declare function apiRecordSource(input: ApiRecordSourceInput): RawSource;
44
62
  export declare function webPageSource(input: WebPageSourceInput): RawSource;
@@ -1,23 +1,38 @@
1
+ /**
2
+ * Raw Source kinds whose factories accept a caller-supplied `locatorScheme`
3
+ * override and otherwise fall back to a per-kind default. `uploaded-document`
4
+ * has no default (`undefined`) because `UploadedDocumentSourceInput.locatorScheme`
5
+ * is required, not optional — modeled here as a real, explicit table entry
6
+ * rather than an omission, so a caller-supplied value is the only source of
7
+ * truth for that kind.
8
+ *
9
+ * Module-internal seam: consumed by relative import from
10
+ * `rawSourceWithDefaultLocatorScheme` below and by
11
+ * `tests/raw-source-defaults.test.ts`, NOT re-exported from `src/index.ts`.
12
+ */
13
+ export const DEFAULT_LOCATOR_SCHEME = {
14
+ "uploaded-document": undefined,
15
+ "api-record": "structured-field",
16
+ "web-page": "html",
17
+ "manual-entry": "structured-field",
18
+ };
19
+ function rawSourceWithDefaultLocatorScheme(kind, input) {
20
+ return rawSource(kind, {
21
+ locatorScheme: DEFAULT_LOCATOR_SCHEME[kind],
22
+ ...input,
23
+ });
24
+ }
1
25
  export function uploadedDocumentSource(input) {
2
- return rawSource("uploaded-document", input);
26
+ return rawSourceWithDefaultLocatorScheme("uploaded-document", input);
3
27
  }
4
28
  export function apiRecordSource(input) {
5
- return rawSource("api-record", {
6
- locatorScheme: "structured-field",
7
- ...input,
8
- });
29
+ return rawSourceWithDefaultLocatorScheme("api-record", input);
9
30
  }
10
31
  export function webPageSource(input) {
11
- return rawSource("web-page", {
12
- locatorScheme: "html",
13
- ...input,
14
- });
32
+ return rawSourceWithDefaultLocatorScheme("web-page", input);
15
33
  }
16
34
  export function manualEntrySource(input) {
17
- return rawSource("manual-entry", {
18
- locatorScheme: "structured-field",
19
- ...input,
20
- });
35
+ return rawSourceWithDefaultLocatorScheme("manual-entry", input);
21
36
  }
22
37
  export function policyStandardSource(input) {
23
38
  const policyStandard = {
@@ -1,20 +1,6 @@
1
1
  import type { SurveyObservationInput } from "./builder.js";
2
- export interface RepeatedObservationInput<TItem> {
3
- id: string;
4
- field: string;
5
- value: readonly TItem[];
6
- rawSource: SurveyObservationInput["rawSource"];
7
- extraction: Omit<SurveyObservationInput["extraction"], "target" | "value" | "excerpt"> & {
8
- target?: string;
9
- excerpt?: string | null;
10
- };
11
- reviewOutcome?: SurveyObservationInput["reviewOutcome"];
12
- claim: Omit<SurveyObservationInput["claim"], "fieldOrBehavior" | "value"> & {
13
- fieldOrBehavior?: string;
14
- };
15
- candidate?: SurveyObservationInput["candidate"];
16
- candidateSet?: SurveyObservationInput["candidateSet"];
2
+ import { type ObservationAuthoringInput } from "./observation-helper.js";
3
+ export interface RepeatedObservationInput<TItem> extends ObservationAuthoringInput<readonly TItem[]> {
17
4
  representation?: "aggregate-array";
18
- metadata?: Record<string, unknown>;
19
5
  }
20
6
  export declare function repeatedObservation<TItem>(input: RepeatedObservationInput<TItem>): SurveyObservationInput;
@@ -1,16 +1,4 @@
1
- import { buildObservation } from "./observation-helper.js";
1
+ import { buildRepeatedObservation } from "./observation-helper.js";
2
2
  export function repeatedObservation(input) {
3
- const representation = input.representation ?? "aggregate-array";
4
- const value = [...input.value];
5
- return buildObservation({
6
- ...input,
7
- value,
8
- surveyMetadata: {
9
- repeated: {
10
- representation,
11
- itemCount: value.length,
12
- },
13
- },
14
- defaultExcerpt: `${input.field}: ${value.length} item(s)`,
15
- });
3
+ return buildRepeatedObservation(input);
16
4
  }
@@ -31,7 +31,7 @@ export interface CanonicalReviewProofPayload {
31
31
  candidateId?: string;
32
32
  subjectType: string;
33
33
  subjectId: string;
34
- surface: string;
34
+ facet: string;
35
35
  claimType: string;
36
36
  fieldOrBehavior: string;
37
37
  };
@@ -92,7 +92,7 @@ export interface CanonicalReviewProofPayload {
92
92
  candidateId?: string;
93
93
  subjectType: string;
94
94
  subjectId: string;
95
- surface: string;
95
+ facet: string;
96
96
  claimType: string;
97
97
  fieldOrBehavior: string;
98
98
  value?: unknown;
@@ -23,7 +23,7 @@ export function buildCanonicalReviewProofPayload(input) {
23
23
  candidateId: input.candidate.id,
24
24
  subjectType: input.claim.subjectType,
25
25
  subjectId: input.claim.subjectId,
26
- surface: input.claim.surface,
26
+ facet: input.claim.facet,
27
27
  claimType: input.claim.claimType,
28
28
  fieldOrBehavior: input.claim.fieldOrBehavior,
29
29
  },
@@ -86,7 +86,7 @@ export function buildCanonicalReviewProofPayload(input) {
86
86
  candidateId: input.claim.candidateId,
87
87
  subjectType: input.claim.subjectType,
88
88
  subjectId: input.claim.subjectId,
89
- surface: input.claim.surface,
89
+ facet: input.claim.facet,
90
90
  claimType: input.claim.claimType,
91
91
  fieldOrBehavior: input.claim.fieldOrBehavior,
92
92
  value: input.claim.value,
@@ -42,7 +42,11 @@ export interface ClaimTargetHint {
42
42
  claimId?: string;
43
43
  subjectType: string;
44
44
  subjectId: string;
45
- surface: string;
45
+ /**
46
+ * Producer-defined grouping or namespace for this claim (Hachure schema 5,
47
+ * surface@2.0.0: Claim.surface -> Claim.facet).
48
+ */
49
+ facet: string;
46
50
  claimType: string;
47
51
  fieldOrBehavior: string;
48
52
  impactLevel: ClaimTarget["impactLevel"];
@@ -46,7 +46,7 @@ export declare const publicDirectoryReviewItemExample: {
46
46
  claimId: string;
47
47
  subjectType: string;
48
48
  subjectId: string;
49
- surface: string;
49
+ facet: string;
50
50
  claimType: string;
51
51
  fieldOrBehavior: string;
52
52
  impactLevel: "medium";
@@ -98,7 +98,7 @@ export declare const publicDirectoryReviewItemExample: {
98
98
  claimId: string;
99
99
  subjectType: string;
100
100
  subjectId: string;
101
- surface: string;
101
+ facet: string;
102
102
  claimType: string;
103
103
  fieldOrBehavior: string;
104
104
  impactLevel: "medium";
@@ -184,7 +184,7 @@ export declare const regulatedRuleConflictReviewItemExample: {
184
184
  claimId: string;
185
185
  subjectType: string;
186
186
  subjectId: string;
187
- surface: string;
187
+ facet: string;
188
188
  claimType: string;
189
189
  fieldOrBehavior: string;
190
190
  impactLevel: "high";
@@ -235,7 +235,7 @@ export declare const regulatedRuleConflictReviewItemExample: {
235
235
  claimId: string;
236
236
  subjectType: string;
237
237
  subjectId: string;
238
- surface: string;
238
+ facet: string;
239
239
  claimType: string;
240
240
  fieldOrBehavior: string;
241
241
  impactLevel: "high";
@@ -334,7 +334,7 @@ export declare const facilityCredentialReviewItemExample: {
334
334
  claimId: string;
335
335
  subjectType: string;
336
336
  subjectId: string;
337
- surface: string;
337
+ facet: string;
338
338
  claimType: string;
339
339
  fieldOrBehavior: string;
340
340
  impactLevel: "high";
@@ -395,7 +395,7 @@ export declare const facilityCredentialReviewItemExample: {
395
395
  claimId: string;
396
396
  subjectType: string;
397
397
  subjectId: string;
398
- surface: string;
398
+ facet: string;
399
399
  claimType: string;
400
400
  fieldOrBehavior: string;
401
401
  impactLevel: "high";
@@ -472,7 +472,7 @@ export declare const reviewWorkbenchQueueExamples: (ReviewItem | {
472
472
  claimId: string;
473
473
  subjectType: string;
474
474
  subjectId: string;
475
- surface: string;
475
+ facet: string;
476
476
  claimType: string;
477
477
  fieldOrBehavior: string;
478
478
  impactLevel: "medium";
@@ -524,7 +524,7 @@ export declare const reviewWorkbenchQueueExamples: (ReviewItem | {
524
524
  claimId: string;
525
525
  subjectType: string;
526
526
  subjectId: string;
527
- surface: string;
527
+ facet: string;
528
528
  claimType: string;
529
529
  fieldOrBehavior: string;
530
530
  impactLevel: "medium";
@@ -609,7 +609,7 @@ export declare const reviewWorkbenchQueueExamples: (ReviewItem | {
609
609
  claimId: string;
610
610
  subjectType: string;
611
611
  subjectId: string;
612
- surface: string;
612
+ facet: string;
613
613
  claimType: string;
614
614
  fieldOrBehavior: string;
615
615
  impactLevel: "high";
@@ -660,7 +660,7 @@ export declare const reviewWorkbenchQueueExamples: (ReviewItem | {
660
660
  claimId: string;
661
661
  subjectType: string;
662
662
  subjectId: string;
663
- surface: string;
663
+ facet: string;
664
664
  claimType: string;
665
665
  fieldOrBehavior: string;
666
666
  impactLevel: "high";
@@ -49,7 +49,7 @@ export const publicDirectoryReviewItemExample = {
49
49
  claimId: "public-field.entity-123.availability-status.current",
50
50
  subjectType: "public-record.entity",
51
51
  subjectId: "entity-123",
52
- surface: "public-record.profile",
52
+ facet: "public-record.profile",
53
53
  claimType: "public-data.field",
54
54
  fieldOrBehavior: "availabilityStatus",
55
55
  impactLevel: "medium",
@@ -100,7 +100,7 @@ export const publicDirectoryReviewItemExample = {
100
100
  claimId: "public-field.entity-123.availability-status.proposal-456",
101
101
  subjectType: "public-record.entity",
102
102
  subjectId: "entity-123",
103
- surface: "public-record.profile",
103
+ facet: "public-record.profile",
104
104
  claimType: "public-data.field-candidate",
105
105
  fieldOrBehavior: "availabilityStatus",
106
106
  impactLevel: "medium",
@@ -187,7 +187,7 @@ export const regulatedRuleConflictReviewItemExample = {
187
187
  claimId: "regulated-rule.example-jurisdiction.2026.standard-threshold.current",
188
188
  subjectType: "regulated-rule-source",
189
189
  subjectId: "example-jurisdiction:2026:standardThreshold",
190
- surface: "regulated.rules",
190
+ facet: "regulated.rules",
191
191
  claimType: "regulated.rule-source-value",
192
192
  fieldOrBehavior: "standardThreshold",
193
193
  impactLevel: "high",
@@ -235,7 +235,7 @@ export const regulatedRuleConflictReviewItemExample = {
235
235
  claimId: "regulated-rule.example-jurisdiction.2026.standard-threshold.proposed",
236
236
  subjectType: "regulated-rule-source",
237
237
  subjectId: "example-jurisdiction:2026:standardThreshold",
238
- surface: "regulated.rules",
238
+ facet: "regulated.rules",
239
239
  claimType: "regulated.rule-source-value",
240
240
  fieldOrBehavior: "standardThreshold",
241
241
  impactLevel: "high",
@@ -332,7 +332,7 @@ export const facilityCredentialReviewItemExample = {
332
332
  claimId: "facility-credential.facility-42.operating-license.current",
333
333
  subjectType: "facility",
334
334
  subjectId: "facility-42",
335
- surface: "facility.credential-profile",
335
+ facet: "facility.credential-profile",
336
336
  claimType: "facility.credential",
337
337
  fieldOrBehavior: "operatingLicenseCredential",
338
338
  impactLevel: "high",
@@ -391,7 +391,7 @@ export const facilityCredentialReviewItemExample = {
391
391
  claimId: "facility-credential.facility-42.operating-license.registry",
392
392
  subjectType: "facility",
393
393
  subjectId: "facility-42",
394
- surface: "facility.credential-profile",
394
+ facet: "facility.credential-profile",
395
395
  claimType: "facility.credential-candidate",
396
396
  fieldOrBehavior: "operatingLicenseCredential",
397
397
  impactLevel: "high",
@@ -1308,7 +1308,7 @@ function isReviewCandidate(value) {
1308
1308
  && isRecord(value.claimTarget)
1309
1309
  && typeof value.claimTarget.subjectType === "string"
1310
1310
  && typeof value.claimTarget.subjectId === "string"
1311
- && typeof value.claimTarget.surface === "string"
1311
+ && typeof value.claimTarget.facet === "string"
1312
1312
  && typeof value.claimTarget.claimType === "string"
1313
1313
  && typeof value.claimTarget.fieldOrBehavior === "string"
1314
1314
  && typeof value.claimTarget.impactLevel === "string";
@@ -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;