@kontourai/survey 0.4.16 → 0.4.17

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.
package/README.md CHANGED
@@ -70,10 +70,20 @@ Survey also exposes a framework-neutral review workbench for downstream
70
70
  products that already produce `ReviewItem` queues.
71
71
 
72
72
  ```ts
73
- import { mountReviewWorkbench } from "@kontourai/survey/review-workbench";
73
+ import {
74
+ mountReviewWorkbench,
75
+ type ReviewPresentationAdapter,
76
+ } from "@kontourai/survey/review-workbench";
74
77
  import "@kontourai/survey/review-workbench.css";
75
78
 
76
- mountReviewWorkbench(element, reviewQueueSession);
79
+ const presentationAdapter = {
80
+ labelForTarget: (target) => target === "registrationStatus"
81
+ ? "Registration status"
82
+ : undefined,
83
+ linkForReviewItem: (item) => ({ href: `/review/${item.metadata.name}` }),
84
+ } satisfies ReviewPresentationAdapter;
85
+
86
+ mountReviewWorkbench(element, reviewQueueSession, { presentationAdapter });
77
87
  ```
78
88
 
79
89
  The default stylesheet is scoped to `.survey-workbench-embed` and bundles the
@@ -93,6 +103,10 @@ review events, exported results, and optional Surface projection, see
93
103
  That guide also covers the server-side apply boundary: producers should derive
94
104
  write results from reviewed snapshots plus persisted events, not from
95
105
  browser-computed decisions or exported result payloads.
106
+ Use `persistReviewSessionEvents` when server code needs to save review events
107
+ and then replay exactly the persisted event set before applying product policy.
108
+ For a generic, test-covered example of the consumer adapter contract, see
109
+ [`examples/review-workbench/facility-credential-consumer.ts`](examples/review-workbench/facility-credential-consumer.ts).
96
110
  For the current decision on why Survey is not adding a generic review adapter
97
111
  builder yet, see
98
112
  [`docs/consumer-adapter-abstraction-assessment.md`](docs/consumer-adapter-abstraction-assessment.md).
@@ -0,0 +1,17 @@
1
+ import { facilityCredentialReviewItemFixture } from "../../src/review-workbench/review-workbench-data.js";
2
+ import { buildReviewItemPresentation, buildReviewResultPresentation, buildReviewWorkbenchSessionExportForSnapshot, buildSurfaceProjectionPreview, initialReviewQueueSessionState, type ReviewPresentationAdapter } from "../../src/review-workbench/review-workbench.js";
3
+ import type { ReviewSessionEvent } from "../../src/review-resource.js";
4
+ export declare const facilityCredentialPresentationAdapter: ReviewPresentationAdapter;
5
+ export declare function buildFacilityCredentialConsumerExample(): Promise<FacilityCredentialConsumerExample>;
6
+ export declare const facilityCredentialConsumerExample: FacilityCredentialConsumerExample;
7
+ export interface FacilityCredentialConsumerExample {
8
+ readonly reviewItem: typeof facilityCredentialReviewItemFixture;
9
+ readonly reviewedSnapshot: ReturnType<typeof initialReviewQueueSessionState>;
10
+ readonly eventsToPersist: readonly ReviewSessionEvent[];
11
+ readonly persistedEvents: readonly ReviewSessionEvent[];
12
+ readonly persistedEventCount: number;
13
+ readonly sessionExport: ReturnType<typeof buildReviewWorkbenchSessionExportForSnapshot>;
14
+ readonly itemPresentation: ReturnType<typeof buildReviewItemPresentation>;
15
+ readonly resultPresentation: ReturnType<typeof buildReviewResultPresentation>;
16
+ readonly surfaceProjectionPreview: NonNullable<ReturnType<typeof buildSurfaceProjectionPreview>>;
17
+ }
@@ -0,0 +1,109 @@
1
+ import { facilityCredentialReviewItemFixture } from "../../src/review-workbench/review-workbench-data.js";
2
+ import { buildReviewItemPresentation, buildReviewResultPresentation, buildReviewSessionEvents, buildReviewWorkbenchSessionExportForSnapshot, buildSurfaceProjectionPreview, initialReviewQueueSessionState, persistReviewSessionEvents, } from "../../src/review-workbench/review-workbench.js";
3
+ export const facilityCredentialPresentationAdapter = {
4
+ labelForTarget: (target) => target === "operatingLicenseCredential"
5
+ ? "Operating license credential"
6
+ : undefined,
7
+ labelForCandidateRole: (role) => role === "current"
8
+ ? "Current managed credential"
9
+ : role === "proposed"
10
+ ? "Registry candidate"
11
+ : undefined,
12
+ summarizeValue: (value) => {
13
+ if (!isCredentialValue(value)) {
14
+ return undefined;
15
+ }
16
+ const serviceSummary = value.permittedServices.length === 0
17
+ ? "no listed services"
18
+ : value.permittedServices.join(", ");
19
+ return `${value.licenseNumber} is ${value.status} through ${value.expiresAt}; services: ${serviceSummary}`;
20
+ },
21
+ linkForReviewItem: (item) => ({
22
+ label: typeof item.metadata.producer?.displayName === "string"
23
+ ? item.metadata.producer.displayName
24
+ : "Review item",
25
+ href: `/review/items/${encodeURIComponent(item.metadata.name)}`,
26
+ }),
27
+ linkForSource: (sourceRef, { candidate }) => ({
28
+ label: candidate.role === "current" ? "Managed record" : "Registry source",
29
+ href: sourceRef.startsWith("http") ? sourceRef : `/sources/${encodeURIComponent(sourceRef)}`,
30
+ }),
31
+ linkForTraceRef: (ref) => {
32
+ if (ref.kind === "claim") {
33
+ return {
34
+ label: "Claim target",
35
+ href: `/claims/${encodeURIComponent(ref.value)}`,
36
+ };
37
+ }
38
+ if (ref.kind === "candidate-set") {
39
+ return {
40
+ label: "Candidate set",
41
+ href: `/candidate-sets/${encodeURIComponent(ref.value)}`,
42
+ };
43
+ }
44
+ return undefined;
45
+ },
46
+ };
47
+ export async function buildFacilityCredentialConsumerExample() {
48
+ const reviewedSnapshot = {
49
+ ...initialReviewQueueSessionState([facilityCredentialReviewItemFixture]),
50
+ actorId: "review-operator@example.test",
51
+ reviewedAt: "2026-01-17T16:15:00.000Z",
52
+ decisionsByItemName: {
53
+ [facilityCredentialReviewItemFixture.metadata.name]: "accept-proposed",
54
+ },
55
+ notesByItemName: {
56
+ [facilityCredentialReviewItemFixture.metadata.name]: "Registry credential supersedes the managed snapshot.",
57
+ },
58
+ };
59
+ const eventsToPersist = buildReviewSessionEvents(reviewedSnapshot);
60
+ const persistedEvents = [];
61
+ const persisted = await persistReviewSessionEvents({
62
+ session: reviewedSnapshot,
63
+ events: eventsToPersist,
64
+ expectedEventCount: persistedEvents.length,
65
+ persist: async ({ events, expectedEventCount }) => {
66
+ if (expectedEventCount !== persistedEvents.length) {
67
+ throw new Error(`Expected ${expectedEventCount} persisted events, found ${persistedEvents.length}.`);
68
+ }
69
+ persistedEvents.splice(0, persistedEvents.length, ...events);
70
+ return { eventCount: persistedEvents.length };
71
+ },
72
+ });
73
+ const sessionExport = buildReviewWorkbenchSessionExportForSnapshot(reviewedSnapshot, persisted.events);
74
+ const [result] = sessionExport.results;
75
+ if (!result) {
76
+ throw new Error("Expected the reviewed credential snapshot to produce one review result.");
77
+ }
78
+ const itemPresentation = buildReviewItemPresentation(facilityCredentialReviewItemFixture, facilityCredentialPresentationAdapter);
79
+ const resultPresentation = buildReviewResultPresentation(result, facilityCredentialReviewItemFixture, facilityCredentialPresentationAdapter);
80
+ const surfaceProjectionPreview = buildSurfaceProjectionPreview(facilityCredentialReviewItemFixture, result.reviewDecision, facilityCredentialPresentationAdapter);
81
+ if (!surfaceProjectionPreview) {
82
+ throw new Error("Expected the reviewed credential result to produce a Surface projection preview.");
83
+ }
84
+ return {
85
+ reviewItem: facilityCredentialReviewItemFixture,
86
+ reviewedSnapshot,
87
+ eventsToPersist,
88
+ persistedEvents: persisted.events,
89
+ persistedEventCount: persisted.eventCount,
90
+ sessionExport,
91
+ itemPresentation,
92
+ resultPresentation,
93
+ surfaceProjectionPreview,
94
+ };
95
+ }
96
+ export const facilityCredentialConsumerExample = await buildFacilityCredentialConsumerExample();
97
+ function isCredentialValue(value) {
98
+ return typeof value === "object"
99
+ && value !== null
100
+ && "licenseNumber" in value
101
+ && typeof value.licenseNumber === "string"
102
+ && "status" in value
103
+ && typeof value.status === "string"
104
+ && "expiresAt" in value
105
+ && typeof value.expiresAt === "string"
106
+ && "permittedServices" in value
107
+ && Array.isArray(value.permittedServices)
108
+ && value.permittedServices.every((service) => typeof service === "string");
109
+ }
@@ -36,8 +36,19 @@ export interface ReviewSessionPersistenceRequest {
36
36
  readonly expectedEventCount: number;
37
37
  }
38
38
  export interface ReviewSessionPersistenceResult {
39
+ readonly events?: readonly ReviewSessionEvent[];
39
40
  readonly eventCount?: number;
40
41
  }
42
+ export interface PersistReviewSessionEventsOptions {
43
+ readonly session: ReviewQueueSessionState;
44
+ readonly events: readonly ReviewSessionEvent[];
45
+ readonly expectedEventCount?: number;
46
+ readonly persist: (request: ReviewSessionPersistenceRequest) => Promise<ReviewSessionPersistenceResult | void>;
47
+ }
48
+ export interface PersistReviewSessionEventsResult {
49
+ readonly events: readonly ReviewSessionEvent[];
50
+ readonly eventCount: number;
51
+ }
41
52
  export interface PersistentReviewSessionEventStoreOptions {
42
53
  readonly initialEvents?: readonly ReviewSessionEvent[];
43
54
  readonly persist: (request: ReviewSessionPersistenceRequest) => Promise<ReviewSessionPersistenceResult | void>;
@@ -73,6 +84,7 @@ export declare function createInMemoryReviewSessionEventStore(initialEvents?: re
73
84
  events(): readonly ReviewSessionEvent[];
74
85
  };
75
86
  export declare function createLocalStorageReviewSessionEventStore(storage: Pick<Storage, "getItem" | "setItem">, keyPrefix?: string): ReviewSessionEventStore;
87
+ export declare function persistReviewSessionEvents(options: PersistReviewSessionEventsOptions): Promise<PersistReviewSessionEventsResult>;
76
88
  export declare function createPersistentReviewSessionEventStore(options: PersistentReviewSessionEventStoreOptions): ReviewSessionEventStore & {
77
89
  events(): readonly ReviewSessionEvent[];
78
90
  };
@@ -175,6 +175,19 @@ export function createLocalStorageReviewSessionEventStore(storage, keyPrefix = r
175
175
  },
176
176
  };
177
177
  }
178
+ export async function persistReviewSessionEvents(options) {
179
+ const events = [...options.events];
180
+ const result = await options.persist({
181
+ session: options.session,
182
+ events,
183
+ expectedEventCount: options.expectedEventCount ?? 0,
184
+ });
185
+ const persistedEvents = result?.events ? [...result.events] : events;
186
+ return {
187
+ events: persistedEvents,
188
+ eventCount: result?.eventCount ?? persistedEvents.length,
189
+ };
190
+ }
178
191
  export function createPersistentReviewSessionEventStore(options) {
179
192
  let savedEvents = [...(options.initialEvents ?? [])];
180
193
  let lastPersistedSerialized = JSON.stringify(savedEvents);
@@ -198,15 +211,16 @@ export function createPersistentReviewSessionEventStore(options) {
198
211
  return;
199
212
  }
200
213
  emit({ status: "saving", events: normalizedEvents });
201
- const result = await options.persist({
214
+ const result = await persistReviewSessionEvents({
202
215
  session,
203
216
  events: normalizedEvents,
204
217
  expectedEventCount: lastPersistedEventCount,
218
+ persist: options.persist,
205
219
  });
206
- savedEvents = normalizedEvents;
207
- lastPersistedSerialized = serialized;
208
- lastPersistedEventCount = result?.eventCount ?? normalizedEvents.length;
209
- emit({ status: "saved", events: normalizedEvents });
220
+ savedEvents = result.events;
221
+ lastPersistedSerialized = JSON.stringify(result.events);
222
+ lastPersistedEventCount = result.eventCount;
223
+ emit({ status: "saved", events: result.events });
210
224
  })
211
225
  .catch((error) => {
212
226
  emit({ status: "error", events: normalizedEvents, error });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
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",