@kontourai/survey 0.4.3 → 0.4.4
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 +211 -9
- package/dist/examples/review-workbench/downstream-public-directory-adapter.d.ts +3 -0
- package/dist/examples/review-workbench/downstream-public-directory-adapter.js +262 -0
- package/dist/examples/review-workbench/review-queue-session.d.ts +54 -0
- package/dist/examples/review-workbench/review-queue-session.js +122 -0
- package/dist/examples/review-workbench/review-surface-preview.d.ts +55 -0
- package/dist/examples/review-workbench/review-surface-preview.js +104 -0
- package/dist/examples/review-workbench/review-workbench-data.d.ts +261 -0
- package/dist/examples/review-workbench/review-workbench-data.js +199 -0
- package/dist/examples/review-workbench/review-workbench.d.ts +7 -0
- package/dist/examples/review-workbench/review-workbench.js +488 -0
- package/dist/fixtures/downstream-public-directory-proposal.d.ts +100 -0
- package/dist/fixtures/downstream-public-directory-proposal.js +59 -0
- package/dist/fixtures/public-directory-review-resource.d.ts +156 -0
- package/dist/fixtures/public-directory-review-resource.js +157 -0
- package/dist/fixtures/regulated-document-review-resource.d.ts +121 -0
- package/dist/fixtures/regulated-document-review-resource.js +131 -0
- package/dist/src/builder.d.ts +4 -1
- package/dist/src/builder.js +7 -0
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.js +4 -2
- package/dist/src/learning-projections.d.ts +19 -0
- package/dist/src/learning-projections.js +77 -0
- package/dist/src/raw-source.d.ts +15 -0
- package/dist/src/raw-source.js +22 -0
- package/dist/src/review-proof.d.ts +28 -0
- package/dist/src/review-proof.js +58 -1
- package/dist/src/review-resource.d.ts +111 -0
- package/dist/src/review-resource.js +1 -0
- package/dist/src/to-surface.js +226 -13
- package/dist/src/types.d.ts +17 -1
- package/package.json +10 -4
|
@@ -0,0 +1,488 @@
|
|
|
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("&", "&")
|
|
478
|
+
.replaceAll("<", "<")
|
|
479
|
+
.replaceAll(">", ">")
|
|
480
|
+
.replaceAll("\"", """)
|
|
481
|
+
.replaceAll("'", "'");
|
|
482
|
+
}
|
|
483
|
+
if (typeof document !== "undefined") {
|
|
484
|
+
const root = document.querySelector("#review-workbench");
|
|
485
|
+
if (root) {
|
|
486
|
+
mountReviewWorkbench(root);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export type DownstreamProposalStatus = "PENDING" | "APPROVED" | "REJECTED" | "SKIPPED";
|
|
2
|
+
export interface DownstreamFieldDiff {
|
|
3
|
+
old: unknown;
|
|
4
|
+
new: unknown;
|
|
5
|
+
confidence: number;
|
|
6
|
+
excerpt?: string;
|
|
7
|
+
sourceUrl?: string;
|
|
8
|
+
mode?: "update" | "populate" | "add_items";
|
|
9
|
+
}
|
|
10
|
+
export interface DownstreamPublicDirectoryProposal {
|
|
11
|
+
id: string;
|
|
12
|
+
publicRecordId: string;
|
|
13
|
+
crawlRunId: string | null;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
reviewedAt: string | null;
|
|
16
|
+
reviewedBy: string | null;
|
|
17
|
+
status: DownstreamProposalStatus;
|
|
18
|
+
sourceUrl: string;
|
|
19
|
+
rawExtraction: Record<string, unknown>;
|
|
20
|
+
proposedChanges: Record<string, DownstreamFieldDiff>;
|
|
21
|
+
overallConfidence: number;
|
|
22
|
+
extractionModel: string;
|
|
23
|
+
reviewerNotes: string | null;
|
|
24
|
+
feedbackTags: string[] | null;
|
|
25
|
+
priority: number;
|
|
26
|
+
appliedFields: string[];
|
|
27
|
+
recordName?: string;
|
|
28
|
+
recordSlug?: string;
|
|
29
|
+
communitySlug?: string;
|
|
30
|
+
providerId?: string | null;
|
|
31
|
+
lastVerifiedAt?: string | null;
|
|
32
|
+
recordData?: Record<string, unknown>;
|
|
33
|
+
fieldTimeline?: Record<string, {
|
|
34
|
+
lastUpdatedAt: string | null;
|
|
35
|
+
lastAttestedAt: string | null;
|
|
36
|
+
}>;
|
|
37
|
+
crawlStartedAt?: string;
|
|
38
|
+
crawlCompletedAt?: string | null;
|
|
39
|
+
crawlTrigger?: string;
|
|
40
|
+
crawlTriggeredBy?: string | null;
|
|
41
|
+
}
|
|
42
|
+
export declare const downstreamPublicDirectoryProposalFixture: {
|
|
43
|
+
id: string;
|
|
44
|
+
publicRecordId: string;
|
|
45
|
+
crawlRunId: string;
|
|
46
|
+
createdAt: string;
|
|
47
|
+
reviewedAt: string;
|
|
48
|
+
reviewedBy: string;
|
|
49
|
+
status: "REJECTED";
|
|
50
|
+
sourceUrl: string;
|
|
51
|
+
rawExtraction: {
|
|
52
|
+
registrationStatus: string;
|
|
53
|
+
confidence: {
|
|
54
|
+
registrationStatus: number;
|
|
55
|
+
};
|
|
56
|
+
sourceObservedAt: string;
|
|
57
|
+
};
|
|
58
|
+
proposedChanges: {
|
|
59
|
+
registrationStatus: {
|
|
60
|
+
old: string;
|
|
61
|
+
new: string;
|
|
62
|
+
confidence: number;
|
|
63
|
+
excerpt: string;
|
|
64
|
+
sourceUrl: string;
|
|
65
|
+
mode: "update";
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
overallConfidence: number;
|
|
69
|
+
extractionModel: string;
|
|
70
|
+
reviewerNotes: string;
|
|
71
|
+
feedbackTags: string[];
|
|
72
|
+
priority: number;
|
|
73
|
+
appliedFields: never[];
|
|
74
|
+
recordName: string;
|
|
75
|
+
recordSlug: string;
|
|
76
|
+
communitySlug: string;
|
|
77
|
+
providerId: string;
|
|
78
|
+
lastVerifiedAt: string;
|
|
79
|
+
recordData: {
|
|
80
|
+
registrationStatus: string;
|
|
81
|
+
websiteUrl: string;
|
|
82
|
+
fieldSources: {
|
|
83
|
+
registrationStatus: {
|
|
84
|
+
excerpt: string;
|
|
85
|
+
sourceUrl: string;
|
|
86
|
+
approvedAt: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
fieldTimeline: {
|
|
91
|
+
registrationStatus: {
|
|
92
|
+
lastUpdatedAt: string;
|
|
93
|
+
lastAttestedAt: string;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
crawlStartedAt: string;
|
|
97
|
+
crawlCompletedAt: string;
|
|
98
|
+
crawlTrigger: string;
|
|
99
|
+
crawlTriggeredBy: null;
|
|
100
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const downstreamPublicDirectoryProposalFixture = {
|
|
2
|
+
id: "proposal-public-456",
|
|
3
|
+
publicRecordId: "record-123",
|
|
4
|
+
crawlRunId: "crawl-run-2026-05-31",
|
|
5
|
+
createdAt: "2026-05-31T15:00:00.000Z",
|
|
6
|
+
reviewedAt: "2026-06-01T17:20:00.000Z",
|
|
7
|
+
reviewedBy: "review-operator-7",
|
|
8
|
+
status: "REJECTED",
|
|
9
|
+
sourceUrl: "https://example.test/listings/example-program",
|
|
10
|
+
rawExtraction: {
|
|
11
|
+
registrationStatus: "WAITLIST",
|
|
12
|
+
confidence: {
|
|
13
|
+
registrationStatus: 0.82,
|
|
14
|
+
},
|
|
15
|
+
sourceObservedAt: "2026-05-31T15:00:00.000Z",
|
|
16
|
+
},
|
|
17
|
+
proposedChanges: {
|
|
18
|
+
registrationStatus: {
|
|
19
|
+
old: "OPEN",
|
|
20
|
+
new: "WAITLIST",
|
|
21
|
+
confidence: 0.82,
|
|
22
|
+
excerpt: "Join the waitlist for this listing.",
|
|
23
|
+
sourceUrl: "https://example.test/listings/example-program",
|
|
24
|
+
mode: "update",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
overallConfidence: 0.82,
|
|
28
|
+
extractionModel: "example-directory-extractor-2026-05",
|
|
29
|
+
reviewerNotes: "Rejected proposed registration status; retained the current reviewed value.",
|
|
30
|
+
feedbackTags: ["source-text-ambiguous"],
|
|
31
|
+
priority: 0,
|
|
32
|
+
appliedFields: [],
|
|
33
|
+
recordName: "Example Program",
|
|
34
|
+
recordSlug: "example-program",
|
|
35
|
+
communitySlug: "example-community",
|
|
36
|
+
providerId: "provider-789",
|
|
37
|
+
lastVerifiedAt: "2026-05-30T18:00:00.000Z",
|
|
38
|
+
recordData: {
|
|
39
|
+
registrationStatus: "OPEN",
|
|
40
|
+
websiteUrl: "https://example.test/listings/example-program",
|
|
41
|
+
fieldSources: {
|
|
42
|
+
registrationStatus: {
|
|
43
|
+
excerpt: "Registration is open for the example program.",
|
|
44
|
+
sourceUrl: "https://example.test/listings/example-program",
|
|
45
|
+
approvedAt: "2026-05-30T18:00:00.000Z",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
fieldTimeline: {
|
|
50
|
+
registrationStatus: {
|
|
51
|
+
lastUpdatedAt: "2026-05-30T18:05:00.000Z",
|
|
52
|
+
lastAttestedAt: "2026-05-30T18:05:00.000Z",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
crawlStartedAt: "2026-05-31T14:58:00.000Z",
|
|
56
|
+
crawlCompletedAt: "2026-05-31T15:03:00.000Z",
|
|
57
|
+
crawlTrigger: "SCHEDULED",
|
|
58
|
+
crawlTriggeredBy: null,
|
|
59
|
+
};
|