@kontourai/survey 0.4.6 → 0.4.7

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
@@ -64,6 +64,29 @@ const trustInput = validateTrustInput(buildSurveyTrustInput(surveyInput));
64
64
  const report = buildTrustReport(trustInput);
65
65
  ```
66
66
 
67
+ ## Review Workbench Embed
68
+
69
+ Survey also exposes a framework-neutral review workbench for downstream
70
+ products that already produce `ReviewItem` queues.
71
+
72
+ ```ts
73
+ import { mountReviewWorkbench } from "@kontourai/survey/review-workbench";
74
+ import "@kontourai/survey/review-workbench.css";
75
+
76
+ mountReviewWorkbench(element, reviewQueueSession);
77
+ ```
78
+
79
+ The default stylesheet is scoped to `.survey-workbench-embed` and bundles the
80
+ Console Kit tokens it needs, so importing it should not rewrite the host
81
+ application's `body` or `:root` styles. Hosts should mount into an element like:
82
+
83
+ ```html
84
+ <div class="survey-workbench-embed theme-survey"></div>
85
+ ```
86
+
87
+ The package also exposes `@kontourai/survey/review-workbench/standalone.css` for
88
+ the standalone demo page. Use that only when Survey owns the whole page.
89
+
67
90
  ## Contributor checks
68
91
 
69
92
  Install the repo-owned Git hooks once per clone:
@@ -1,7 +1 @@
1
- import { type ReviewQueueSessionState, type ReviewWorkbenchState } from "./review-queue-session.js";
2
- import { type ReviewDecision } from "../../src/review-resource.js";
3
- export { candidateForDecision, currentReviewItem, currentReviewWorkbenchState, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, reviewSessionSummary, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
4
- export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
5
- export declare function buildReviewDecision(state: ReviewWorkbenchState): ReviewDecision | undefined;
6
- export declare function renderReviewWorkbenchHtml(state: ReviewWorkbenchState | ReviewQueueSessionState): string;
7
- export declare function mountReviewWorkbench(root: HTMLElement, startState?: ReviewQueueSessionState | ReviewWorkbenchState): void;
1
+ export {};
@@ -1,488 +1,6 @@
1
- import { candidateForDecision, currentReviewItem, currentReviewWorkbenchState, deriveQueueRowStatus, initialReviewQueueSessionState, nextUnresolvedItemName, reviewSessionSummary, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
2
- import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
3
- import { reviewResourceApiVersion, } from "../../src/review-resource.js";
4
- export { candidateForDecision, currentReviewItem, currentReviewWorkbenchState, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, reviewSessionSummary, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
5
- export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
6
- export function buildReviewDecision(state) {
7
- if (!state.decision) {
8
- return undefined;
9
- }
10
- const definition = workbenchDecisionDefinitions[state.decision];
11
- const candidate = candidateForDecision(state.item, state.decision);
12
- const projection = {
13
- ...candidate.projection,
14
- reviewOutcomeId: candidate.projection?.reviewOutcomeId
15
- ?? `${state.item.metadata.name}:${state.decision}:review-outcome`,
16
- };
17
- return {
18
- apiVersion: reviewResourceApiVersion,
19
- kind: "ReviewDecision",
20
- metadata: {
21
- name: `${state.item.metadata.name}-${state.decision}`,
22
- labels: state.item.metadata.labels,
23
- producer: state.item.metadata.producer,
24
- },
25
- spec: {
26
- reviewItemName: state.item.metadata.name,
27
- candidateId: candidate.id,
28
- status: definition.status,
29
- actor: {
30
- id: state.actorId,
31
- },
32
- reviewedAt: state.reviewedAt,
33
- rationale: state.note,
34
- projection,
35
- },
36
- status: {
37
- appliedToClaimIds: candidate.projection?.claimId ? [candidate.projection.claimId] : undefined,
38
- },
39
- };
40
- }
41
- export function renderReviewWorkbenchHtml(state) {
42
- if ("items" in state) {
43
- return renderReviewQueueSessionHtml(state);
44
- }
45
- return `
46
- <section class="workbench-shell" aria-label="Survey review workbench">
47
- ${renderWorkbenchHeader(state)}
48
- <div class="content-grid">
49
- ${renderCandidateComparison(state)}
50
- ${renderDecisionColumn(state)}
51
- </div>
52
- </section>
53
- `;
54
- }
55
- function renderReviewQueueSessionHtml(session) {
56
- const state = currentReviewWorkbenchState(session);
57
- return `
58
- <section class="workbench-shell" aria-label="Survey review workbench">
59
- ${renderWorkbenchHeader(state)}
60
- <div class="queue-layout">
61
- <aside class="queue-panel" aria-label="Review queue">
62
- ${renderQueueRows(session)}
63
- ${renderSessionSummary(session)}
64
- </aside>
65
- <div class="content-grid">
66
- ${renderCandidateComparison(state)}
67
- ${renderDecisionColumn(state)}
68
- </div>
69
- </div>
70
- </section>
71
- `;
72
- }
73
- function renderQueueRows(session) {
74
- return `
75
- <section class="queue-list" data-testid="review-queue">
76
- <div class="queue-head">
77
- <h2>Review queue</h2>
78
- <button class="next-button" type="button" data-testid="next-unresolved">Next unresolved</button>
79
- </div>
80
- ${session.items.map((item) => renderQueueRow(item, session)).join("")}
81
- </section>
82
- `;
83
- }
84
- function renderQueueRow(item, session) {
85
- const status = deriveQueueRowStatus(item, session);
86
- const isActive = item.metadata.name === session.activeItemName;
87
- return `
88
- <button class="queue-row${isActive ? " is-active" : ""}" type="button" data-item-name="${escapeHtml(item.metadata.name)}" data-testid="queue-row" data-queue-status="${status}">
89
- <span class="queue-row-main">
90
- <span class="queue-row-title">${escapeHtml(item.metadata.name)}</span>
91
- <span class="queue-row-target">${escapeHtml(item.spec.target)}</span>
92
- </span>
93
- <span class="state-label">${escapeHtml(status)}</span>
94
- </button>
95
- `;
96
- }
97
- function renderSessionSummary(session) {
98
- const summary = reviewSessionSummary(session);
99
- return `
100
- <section class="session-summary" data-testid="session-summary">
101
- <h2>Session summary</h2>
102
- <dl class="summary-grid">
103
- ${metaItem("Accepted", String(summary.accepted))}
104
- ${metaItem("Kept current", String(summary.keptCurrent))}
105
- ${metaItem("Rejected", String(summary.rejected))}
106
- ${metaItem("Escalated", String(summary.escalated))}
107
- ${metaItem("Unresolved", String(summary.unresolved))}
108
- </dl>
109
- </section>
110
- `;
111
- }
112
- function renderWorkbenchHeader(state) {
113
- return `
114
- <header class="topbar">
115
- <div>
116
- <p class="eyebrow">${escapeHtml(String(state.item.metadata.producer?.displayName ?? "Survey"))}</p>
117
- <h1>${escapeHtml(state.item.metadata.name)}</h1>
118
- </div>
119
- <dl class="meta-grid">
120
- ${metaItem("Target", state.item.spec.target)}
121
- ${metaItem("Status", state.item.spec.candidateSetStatus ?? "unresolved")}
122
- ${metaItem("Selected", state.item.spec.selectedCandidateId ?? "none")}
123
- ${metaItem("Candidate count", String(state.item.status?.observedCandidateCount ?? state.item.spec.candidates.length))}
124
- </dl>
125
- </header>
126
- `;
127
- }
128
- function renderCandidateComparison(state) {
129
- const current = candidateByRole(state.item, "current");
130
- const proposed = candidateByRole(state.item, "proposed");
131
- return `
132
- <section class="candidate-grid" aria-label="Candidate comparison">
133
- ${renderCandidateCard(current, state)}
134
- ${renderCandidateCard(proposed, state)}
135
- </section>
136
- `;
137
- }
138
- function renderDecisionColumn(state) {
139
- return `
140
- <aside class="decision-column" aria-label="Review decision">
141
- ${renderDecisionControls(state)}
142
- ${renderSurfacePreview(state)}
143
- ${renderDecisionPayload(state)}
144
- </aside>
145
- `;
146
- }
147
- function renderDecisionControls(state) {
148
- const activeDefinition = state.decision ? workbenchDecisionDefinitions[state.decision] : undefined;
149
- return `
150
- <section class="decision-row">
151
- <label class="field">
152
- <span class="field-label">Reviewer note</span>
153
- <textarea id="reviewer-note" data-testid="reviewer-note">${escapeHtml(state.note)}</textarea>
154
- </label>
155
- <div class="decision-buttons">
156
- ${renderDecisionButtons(state)}
157
- </div>
158
- <div class="effect" data-testid="decision-effect">
159
- <span class="field-label">Decision effect</span>
160
- <span class="field-value">${escapeHtml(activeDefinition?.effect ?? "No decision selected.")}</span>
161
- </div>
162
- ${renderFeedbackTags(state)}
163
- </section>
164
- `;
165
- }
166
- function renderFeedbackTags(state) {
167
- const tags = producerFeedbackTags(state.item);
168
- return `
169
- <div class="feedback-tags" data-testid="producer-feedback-tags">
170
- <span class="field-label">Producer feedback tags</span>
171
- <div class="tag-row">
172
- ${tags.length === 0 ? "<span class=\"tag is-empty\">none</span>" : tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("")}
173
- </div>
174
- </div>
175
- `;
176
- }
177
- function renderDecisionButtons(state) {
178
- return Object.entries(workbenchDecisionDefinitions).map(([key, definition]) => `
179
- <button class="decision-button${state.decision === key ? " is-active" : ""}" type="button" data-decision="${key}">
180
- ${escapeHtml(definition.label)}
181
- </button>
182
- `).join("");
183
- }
184
- function renderDecisionPayload(state) {
185
- const decision = buildReviewDecision(state);
186
- return `
187
- <section class="payload-panel">
188
- <h2 class="field-label">ReviewDecision payload</h2>
189
- <pre data-testid="decision-payload">${escapeHtml(JSON.stringify(decision ?? null, null, 2))}</pre>
190
- </section>
191
- `;
192
- }
193
- function renderSurfacePreview(state) {
194
- const preview = buildSurfaceProjectionPreview(state.item, buildReviewDecision(state));
195
- return preview ? renderPopulatedSurfacePreview(preview) : renderPendingSurfacePreview();
196
- }
197
- function renderPendingSurfacePreview() {
198
- return `
199
- <section class="surface-preview" data-testid="surface-preview" aria-label="Surface preview">
200
- <div class="surface-head">
201
- <h2>Surface preview</h2>
202
- <span class="state-label">pending</span>
203
- </div>
204
- <p class="preview-disclaimer">Choose a review decision to preview the source and review posture that would project toward Surface. Survey does not validate real-world truth.</p>
205
- </section>
206
- `;
207
- }
208
- function renderPopulatedSurfacePreview(preview) {
209
- return `
210
- <section class="surface-preview" data-testid="surface-preview" aria-label="Surface preview">
211
- <div class="surface-head">
212
- <h2>Surface preview</h2>
213
- <span class="state-label">local preview</span>
214
- </div>
215
- <p class="preview-disclaimer" data-testid="surface-preview-disclaimer">${escapeHtml(preview.postureDisclaimer)}</p>
216
- <div class="preview-section-grid">
217
- ${renderSurfacePreviewSections(preview)}
218
- </div>
219
- </section>
220
- `;
221
- }
222
- function renderSurfacePreviewSections(preview) {
223
- return [
224
- renderCanonicalClaim(preview),
225
- renderCandidateHistory(preview),
226
- renderSourceEvidence(preview),
227
- renderReviewEvent(preview),
228
- renderIntegrityPosture(preview),
229
- renderAuthorityTrace(preview),
230
- ].join("");
231
- }
232
- function renderCanonicalClaim(preview) {
233
- return renderPreviewSection("Selected canonical claim", "surface-canonical-claim", [
234
- ["Candidate", preview.canonicalClaim.candidateId],
235
- ["Claim", preview.canonicalClaim.claimId],
236
- ["Value", preview.canonicalClaim.value],
237
- ["Review status", preview.canonicalClaim.status],
238
- ]);
239
- }
240
- function renderReviewEvent(preview) {
241
- return renderPreviewSection("Review event", "surface-review-event", [
242
- ["Actor", preview.reviewEvent?.actor ?? "unknown"],
243
- ["Reviewed at", preview.reviewEvent?.reviewedAt ?? "not recorded"],
244
- ["Status", preview.reviewEvent?.status ?? "pending"],
245
- ["Rationale", preview.reviewEvent?.rationale ?? "No reviewer rationale provided."],
246
- ["Outcome", preview.reviewEvent?.reviewOutcomeId ?? "not provided"],
247
- ]);
248
- }
249
- function renderIntegrityPosture(preview) {
250
- return renderPreviewSection("Integrity posture", "surface-integrity-posture", [
251
- ["Candidate set", preview.integrityPosture.candidateSetId],
252
- ["Raw source", preview.integrityPosture.rawSourceId],
253
- ["Extraction", preview.integrityPosture.extractionId],
254
- ["Checksum", preview.integrityPosture.checksum],
255
- ]);
256
- }
257
- function renderAuthorityTrace(preview) {
258
- return renderPreviewSection("Authority trace", "surface-authority-trace", [
259
- ["Status", preview.authorityTrace.label],
260
- ["Detail", preview.authorityTrace.detail],
261
- ], preview.authorityTrace.status === "empty" ? " is-neutral" : "");
262
- }
263
- function renderCandidateHistory(preview) {
264
- const rows = preview.candidateHistory.length === 0
265
- ? [["History", "No unselected candidates."]]
266
- : preview.candidateHistory.flatMap((candidate) => [
267
- ["History", candidate.historyLabel],
268
- ["Candidate", candidate.candidateId],
269
- ["Value", candidate.value],
270
- ]);
271
- return renderPreviewSection("Unselected candidate history", "surface-candidate-history", rows);
272
- }
273
- function renderSourceEvidence(preview) {
274
- const rows = [
275
- ["Source URL", preview.sourceEvidence.sourceRef],
276
- ["Source ref", preview.sourceEvidence.sourceId],
277
- ["Excerpt", preview.sourceEvidence.excerpt],
278
- ["Extraction", preview.sourceEvidence.extractionId],
279
- ["Extractor", preview.sourceEvidence.extractor],
280
- ["Observed", preview.sourceEvidence.observedAt],
281
- ];
282
- if (preview.sourceEvidence.sourceAuthority) {
283
- rows.push(["Source authority class", preview.sourceEvidence.sourceAuthority.authorityClass], ["Declared by", preview.sourceEvidence.sourceAuthority.declaredBy], ["Authority scope", preview.sourceEvidence.sourceAuthority.scope]);
284
- }
285
- return renderPreviewSection("Source evidence", "surface-source-evidence", rows);
286
- }
287
- function renderPreviewSection(title, testId, rows, extraClass = "") {
288
- return `
289
- <section class="preview-section${extraClass}" data-testid="${testId}">
290
- <h3>${escapeHtml(title)}</h3>
291
- <dl class="field-stack compact">
292
- ${rows.map(([label, value]) => fieldItem(label, value)).join("")}
293
- </dl>
294
- </section>
295
- `;
296
- }
297
- export function mountReviewWorkbench(root, startState = initialReviewQueueSessionState()) {
298
- const controller = createReviewWorkbenchController(root, startState);
299
- controller.renderCurrentState();
300
- }
301
- function createReviewWorkbenchController(root, startState) {
302
- let session = queueSessionFromStartState(startState);
303
- const applySessionUpdate = (update) => {
304
- session = update(session);
305
- };
306
- const selectDecision = (decision) => {
307
- applySessionUpdate((current) => ({
308
- ...current,
309
- decisionsByItemName: {
310
- ...current.decisionsByItemName,
311
- [currentReviewItem(current).metadata.name]: decision,
312
- },
313
- }));
314
- };
315
- const controller = {
316
- currentState: () => currentReviewWorkbenchState(session),
317
- goToNextUnresolved: () => applyNextUnresolvedSessionUpdate(applySessionUpdate, session),
318
- renderCurrentState: () => renderCurrentState(root, session, controller),
319
- selectDecision,
320
- selectQueueItem: (itemName) => applySessionUpdate((current) => ({ ...current, activeItemName: itemName })),
321
- updateReviewerNote: (note) => applyReviewerNoteSessionUpdate(applySessionUpdate, session, note),
322
- };
323
- return controller;
324
- }
325
- function applyReviewerNoteSessionUpdate(applySessionUpdate, session, note) {
326
- const itemName = currentReviewItem(session).metadata.name;
327
- applySessionUpdate((current) => ({
328
- ...current,
329
- notesByItemName: {
330
- ...current.notesByItemName,
331
- [itemName]: note,
332
- },
333
- }));
334
- }
335
- function applyNextUnresolvedSessionUpdate(applySessionUpdate, session) {
336
- const next = nextUnresolvedItemName(session);
337
- if (next) {
338
- applySessionUpdate((current) => ({ ...current, activeItemName: next }));
339
- }
340
- }
341
- function renderCurrentState(root, session, controller) {
342
- root.innerHTML = renderReviewWorkbenchHtml(session);
343
- bindReviewerNote(root, controller.updateReviewerNote, controller.currentState, controller.renderCurrentState);
344
- bindDecisionButtons(root, (decision) => {
345
- controller.selectDecision(decision);
346
- controller.renderCurrentState();
347
- });
348
- bindQueueRows(root, (itemName) => {
349
- controller.selectQueueItem(itemName);
350
- controller.renderCurrentState();
351
- });
352
- bindNextUnresolved(root, () => {
353
- controller.goToNextUnresolved();
354
- controller.renderCurrentState();
355
- });
356
- }
357
- function queueSessionFromStartState(startState) {
358
- if ("items" in startState) {
359
- return startState;
360
- }
361
- return {
362
- items: [startState.item],
363
- activeItemName: startState.item.metadata.name,
364
- notesByItemName: startState.note ? { [startState.item.metadata.name]: startState.note } : {},
365
- decisionsByItemName: startState.decision ? { [startState.item.metadata.name]: startState.decision } : {},
366
- reviewedAt: startState.reviewedAt,
367
- actorId: startState.actorId,
368
- };
369
- }
370
- function bindReviewerNote(root, updateReviewerNote, currentState, renderCurrentState) {
371
- root.querySelector("[data-testid='reviewer-note']")?.addEventListener("input", (event) => {
372
- updateReviewerNote(event.target.value);
373
- refreshDecisionOutputs(root, currentState(), renderCurrentState);
374
- });
375
- }
376
- function bindDecisionButtons(root, selectDecision) {
377
- root.querySelectorAll("[data-decision]").forEach((button) => {
378
- button.addEventListener("click", () => {
379
- selectDecision(button.dataset.decision);
380
- });
381
- });
382
- }
383
- function bindQueueRows(root, selectQueueItem) {
384
- root.querySelectorAll("[data-item-name]").forEach((button) => {
385
- button.addEventListener("click", () => {
386
- selectQueueItem(button.dataset.itemName ?? "");
387
- });
388
- });
389
- }
390
- function bindNextUnresolved(root, goToNextUnresolved) {
391
- root.querySelector("[data-testid='next-unresolved']")?.addEventListener("click", goToNextUnresolved);
392
- }
393
- function refreshDecisionOutputs(root, state, renderCurrentState) {
394
- const payload = root.querySelector("[data-testid='decision-payload']");
395
- if (payload) {
396
- payload.textContent = JSON.stringify(buildReviewDecision(state) ?? null, null, 2);
397
- }
398
- const preview = root.querySelector("[data-testid='surface-preview']");
399
- if (!preview) {
400
- return;
401
- }
402
- if (typeof document === "undefined") {
403
- renderCurrentState();
404
- return;
405
- }
406
- const wrapper = document.createElement("div");
407
- wrapper.innerHTML = renderSurfacePreview(state);
408
- preview.replaceWith(wrapper.firstElementChild);
409
- }
410
- function producerFeedbackTags(item) {
411
- const tags = item.spec.producerPolicy?.feedbackTags;
412
- return Array.isArray(tags) ? tags.map(String) : [];
413
- }
414
- function renderCandidateCard(candidate, state) {
415
- const selectedRole = selectedCandidateRole(state);
416
- const candidateState = state.decision
417
- ? candidate.role === selectedRole ? "selected" : "unselected"
418
- : "pending";
419
- const cssState = candidateState === "selected" ? " is-selected" : candidateState === "unselected" ? " is-unselected" : "";
420
- const confidence = candidate.extraction.confidence ?? candidate.confidence;
421
- return `
422
- <article class="candidate-card${cssState}" data-testid="candidate-${escapeHtml(candidate.role ?? candidate.id)}" data-outcome="${candidateState}">
423
- <div class="card-head">
424
- <div>
425
- <p class="eyebrow">${escapeHtml(candidate.id)}</p>
426
- <h2 class="role">${escapeHtml(titleCase(candidate.role ?? "candidate"))}</h2>
427
- </div>
428
- <span class="state-label">${escapeHtml(candidateState)}</span>
429
- </div>
430
- <div class="candidate-value">
431
- <span class="field-label">Value</span>
432
- <p class="field-value">${escapeHtml(formatValue(candidate.value))}</p>
433
- </div>
434
- <dl class="field-stack">
435
- ${fieldItem("Source URL", candidate.source.sourceRef)}
436
- ${fieldItem("Source ref", candidate.source.sourceId ?? candidate.source.sourceRef)}
437
- ${fieldItem("Locator", candidate.locator?.locator ?? candidate.locator?.scheme ?? "none")}
438
- ${fieldItem("Excerpt", candidate.locator?.excerpt ?? "none", "excerpt")}
439
- ${fieldItem("Extraction confidence", confidence === undefined ? "unknown" : formatConfidence(confidence))}
440
- ${fieldItem("Extractor", candidate.extraction.extractor ?? "unknown")}
441
- ${fieldItem("Claim", candidate.claimTarget.claimId ?? candidate.claimTarget.fieldOrBehavior)}
442
- </dl>
443
- </article>
444
- `;
445
- }
446
- function candidateByRole(item, role) {
447
- const candidate = item.spec.candidates.find((entry) => entry.role === role);
448
- if (!candidate) {
449
- throw new Error(`ReviewItem ${item.metadata.name} has no ${role} candidate.`);
450
- }
451
- return candidate;
452
- }
453
- function metaItem(label, value) {
454
- return `
455
- <div class="meta-item">
456
- <dt class="field-label">${escapeHtml(label)}</dt>
457
- <dd class="meta-value">${escapeHtml(value)}</dd>
458
- </div>
459
- `;
460
- }
461
- function fieldItem(label, value, extraClass = "") {
462
- return `
463
- <div class="field ${extraClass}">
464
- <dt class="field-label">${escapeHtml(label)}</dt>
465
- <dd class="field-value">${escapeHtml(value)}</dd>
466
- </div>
467
- `;
468
- }
469
- function formatConfidence(value) {
470
- return `${Math.round(value * 100)}% (${value.toFixed(2)})`;
471
- }
472
- function titleCase(value) {
473
- return value.slice(0, 1).toUpperCase() + value.slice(1);
474
- }
475
- function escapeHtml(value) {
476
- return value
477
- .replaceAll("&", "&amp;")
478
- .replaceAll("<", "&lt;")
479
- .replaceAll(">", "&gt;")
480
- .replaceAll("\"", "&quot;")
481
- .replaceAll("'", "&#39;");
482
- }
483
- if (typeof document !== "undefined") {
484
- const root = document.querySelector("#review-workbench");
485
- if (root) {
486
- mountReviewWorkbench(root);
487
- }
1
+ import { browserReviewWorkbenchStartState, mountReviewWorkbench, } from "../../src/review-workbench/review-workbench.js";
2
+ const root = document.querySelector("#review-workbench");
3
+ if (!root) {
4
+ throw new Error("Missing #review-workbench root.");
488
5
  }
6
+ mountReviewWorkbench(root, browserReviewWorkbenchStartState());
@@ -1,6 +1,6 @@
1
1
  export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, EscalationDimension, EscalationRecord, Extraction, Interpretation, LocatorScheme, RawSource, RawSourceKind, ReviewOutcome, ReviewStatus, SurveyInput, } from "./types.js";
2
2
  export { reviewResourceApiVersion } from "./review-resource.js";
3
- export type { CandidateRole, ClaimTargetHint, ExtractionReference, ResourceEnvelope, ResourceMetadata, ReviewActor, ReviewCandidate, ReviewDecision, ReviewDecisionSpec, ReviewDecisionStatus, ReviewItem, ReviewItemSpec, ReviewItemStatus, ReviewLocator, ReviewResource, ReviewResourceApiVersion, ReviewResourceKind, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
3
+ export type { CandidateRole, ClaimTargetHint, ExtractionReference, ResourceEnvelope, ResourceMetadata, ReviewActor, ReviewCandidate, ReviewDecision, ReviewDecisionSpec, ReviewDecisionStatus, ReviewItem, ReviewItemSpec, ReviewItemStatus, ReviewLocator, ReviewResource, ReviewResourceApiVersion, ReviewResourceKind, ReviewSession, ReviewSessionEvent, ReviewSessionEventSpec, ReviewSessionEventStatus, ReviewSessionEventType, ReviewSessionSpec, ReviewSessionStatus, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
4
4
  export { candidateReviewRecord, SurveyInputBuilder } from "./builder.js";
5
5
  export type { CandidateReviewRecordInput, SurveyClaimRecord, SurveyInputBuilderArgs, SurveyObservationInput, } from "./builder.js";
6
6
  export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
@@ -1,7 +1,7 @@
1
1
  import type { CandidateSetStatus, ClaimTarget, Extraction, RawSource, ReviewOutcome } from "./types.js";
2
2
  export declare const reviewResourceApiVersion = "survey.kontourai.io/v1alpha1";
3
3
  export type ReviewResourceApiVersion = typeof reviewResourceApiVersion;
4
- export type ReviewResourceKind = "ReviewItem" | "ReviewDecision";
4
+ export type ReviewResourceKind = "ReviewItem" | "ReviewDecision" | "ReviewSession" | "ReviewSessionEvent";
5
5
  export interface ResourceMetadata {
6
6
  name: string;
7
7
  uid?: string;
@@ -108,4 +108,35 @@ export interface ReviewDecisionStatus {
108
108
  appliedToClaimIds?: string[];
109
109
  }
110
110
  export type ReviewDecision = ResourceEnvelope<"ReviewDecision", ReviewDecisionSpec, ReviewDecisionStatus>;
111
- export type ReviewResource = ReviewItem | ReviewDecision;
111
+ export interface ReviewSessionSpec {
112
+ reviewItemNames: string[];
113
+ actor?: ReviewActor;
114
+ startedAt: string;
115
+ completedAt?: string;
116
+ }
117
+ export interface ReviewSessionStatus {
118
+ activeItemName?: string;
119
+ eventCount?: number;
120
+ decisionCount?: number;
121
+ }
122
+ export type ReviewSessionEventType = "session-started" | "item-selected" | "decision-changed" | "note-changed" | "decision-submitted" | "session-completed";
123
+ export interface ReviewSessionEventSpec {
124
+ sessionName: string;
125
+ sequence: number;
126
+ eventType: ReviewSessionEventType;
127
+ occurredAt: string;
128
+ actor?: ReviewActor;
129
+ reviewItemName?: string;
130
+ activeItemName?: string;
131
+ reviewDecisionName?: string;
132
+ candidateId?: string;
133
+ status?: ReviewOutcome["status"];
134
+ rationale?: string;
135
+ data?: Record<string, unknown>;
136
+ }
137
+ export interface ReviewSessionEventStatus {
138
+ replayed?: boolean;
139
+ }
140
+ export type ReviewSession = ResourceEnvelope<"ReviewSession", ReviewSessionSpec, ReviewSessionStatus>;
141
+ export type ReviewSessionEvent = ResourceEnvelope<"ReviewSessionEvent", ReviewSessionEventSpec, ReviewSessionEventStatus>;
142
+ export type ReviewResource = ReviewItem | ReviewDecision | ReviewSession | ReviewSessionEvent;
@@ -1,6 +1,8 @@
1
- import { type ReviewCandidate, type ReviewItem } from "../../src/review-resource.js";
1
+ import { type ReviewCandidate, type ReviewItem, type ReviewSession, type ReviewSessionEvent, type ReviewSessionEventSpec } from "../../src/review-resource.js";
2
2
  export type ReviewWorkbenchDecision = "accept-proposed" | "keep-current" | "reject-proposed";
3
3
  export type ReviewQueueRowStatus = "pending" | "in-review" | "resolved" | "rejected" | "escalated";
4
+ export declare const reviewWorkbenchSessionStorageKey = "kontourai.survey.review-workbench.session-events.v1";
5
+ export declare const defaultReviewSessionName = "review-workbench-session";
4
6
  export interface ReviewWorkbenchState {
5
7
  readonly item: ReviewItem;
6
8
  readonly note: string;
@@ -52,3 +54,7 @@ export declare function nextUnresolvedItemName(session: ReviewQueueSessionState)
52
54
  export declare function reviewSessionSummary(session: ReviewQueueSessionState): ReviewSessionSummary;
53
55
  export declare function candidateForDecision(item: ReviewItem, decision: ReviewWorkbenchDecision): ReviewCandidate;
54
56
  export declare function selectedCandidateRole(state: ReviewWorkbenchState): ReviewCandidate["role"] | undefined;
57
+ export declare function buildReviewSessionResource(session: ReviewQueueSessionState, events?: readonly ReviewSessionEvent[], sessionName?: string): ReviewSession;
58
+ export declare function buildReviewSessionEvents(session: ReviewQueueSessionState, sessionName?: string): ReviewSessionEvent[];
59
+ export declare function replayReviewSessionEvents(startState: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): ReviewQueueSessionState;
60
+ export declare function buildReviewSessionEvent(session: ReviewQueueSessionState, spec: Omit<ReviewSessionEventSpec, "actor">): ReviewSessionEvent;