@kontourai/survey 0.4.6 → 0.4.8
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 +23 -0
- package/dist/examples/review-workbench/review-workbench.d.ts +1 -7
- package/dist/examples/review-workbench/review-workbench.js +5 -487
- package/dist/src/index.d.ts +1 -1
- package/dist/src/review-resource.d.ts +33 -2
- package/dist/{examples → src}/review-workbench/review-queue-session.d.ts +7 -1
- package/dist/src/review-workbench/review-queue-session.js +275 -0
- package/dist/{examples → src}/review-workbench/review-surface-preview.d.ts +1 -1
- package/dist/{examples → src}/review-workbench/review-surface-preview.js +18 -1
- package/dist/{examples → src}/review-workbench/review-workbench-data.d.ts +1 -1
- package/dist/{examples → src}/review-workbench/review-workbench-data.js +1 -1
- package/dist/src/review-workbench/review-workbench.css +1499 -0
- package/dist/src/review-workbench/review-workbench.d.ts +71 -0
- package/dist/src/review-workbench/review-workbench.js +892 -0
- package/dist/src/review-workbench/review-workbench.standalone.css +1341 -0
- package/dist/src/review-workbench/vendor/console-kit/tokens/fonts.css +1 -0
- package/dist/src/review-workbench/vendor/console-kit/tokens/index.css +2 -0
- package/dist/src/review-workbench/vendor/console-kit/tokens/themes.css +67 -0
- package/dist/src/review-workbench/vendor/console-kit/tokens/tokens.css +79 -0
- package/package.json +15 -3
- package/dist/examples/review-workbench/review-queue-session.js +0 -122
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { publicDirectoryReviewItemFixture, reviewWorkbenchQueueFixtures } from "./review-workbench-data.js";
|
|
2
|
+
import { reviewResourceApiVersion, } from "../../src/review-resource.js";
|
|
3
|
+
export const reviewWorkbenchSessionStorageKey = "kontourai.survey.review-workbench.session-events.v1";
|
|
4
|
+
export const defaultReviewSessionName = "review-workbench-session";
|
|
5
|
+
export const workbenchDecisionDefinitions = {
|
|
6
|
+
"accept-proposed": {
|
|
7
|
+
label: "Accept proposed",
|
|
8
|
+
effect: "Proposed value becomes the verified review outcome.",
|
|
9
|
+
candidateRole: "proposed",
|
|
10
|
+
status: "verified",
|
|
11
|
+
},
|
|
12
|
+
"keep-current": {
|
|
13
|
+
label: "Keep current",
|
|
14
|
+
effect: "Current value remains the verified review outcome.",
|
|
15
|
+
candidateRole: "current",
|
|
16
|
+
status: "verified",
|
|
17
|
+
},
|
|
18
|
+
"reject-proposed": {
|
|
19
|
+
label: "Reject proposed",
|
|
20
|
+
effect: "Proposed value is rejected and the current value remains unmodified.",
|
|
21
|
+
candidateRole: "proposed",
|
|
22
|
+
status: "rejected",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export function initialReviewWorkbenchState(item = publicDirectoryReviewItemFixture) {
|
|
26
|
+
return {
|
|
27
|
+
item,
|
|
28
|
+
note: "",
|
|
29
|
+
decision: undefined,
|
|
30
|
+
reviewedAt: "2026-06-04T00:00:00.000Z",
|
|
31
|
+
actorId: "review-workbench-operator",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function initialReviewQueueSessionState(items = reviewWorkbenchQueueFixtures) {
|
|
35
|
+
return {
|
|
36
|
+
items,
|
|
37
|
+
activeItemName: items[0]?.metadata.name ?? "",
|
|
38
|
+
notesByItemName: {},
|
|
39
|
+
decisionsByItemName: {},
|
|
40
|
+
reviewedAt: "2026-06-04T00:00:00.000Z",
|
|
41
|
+
actorId: "review-workbench-operator",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function currentReviewWorkbenchState(session) {
|
|
45
|
+
const item = currentReviewItem(session);
|
|
46
|
+
return {
|
|
47
|
+
item,
|
|
48
|
+
note: session.notesByItemName[item.metadata.name] ?? "",
|
|
49
|
+
decision: session.decisionsByItemName[item.metadata.name],
|
|
50
|
+
reviewedAt: session.reviewedAt,
|
|
51
|
+
actorId: session.actorId,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function currentReviewItem(session) {
|
|
55
|
+
const item = session.items.find((entry) => entry.metadata.name === session.activeItemName) ?? session.items[0];
|
|
56
|
+
if (!item) {
|
|
57
|
+
throw new Error("Review queue session has no ReviewItems.");
|
|
58
|
+
}
|
|
59
|
+
return item;
|
|
60
|
+
}
|
|
61
|
+
export function deriveQueueRowStatus(item, session) {
|
|
62
|
+
const decision = session.decisionsByItemName[item.metadata.name];
|
|
63
|
+
if (decision === "reject-proposed") {
|
|
64
|
+
return "rejected";
|
|
65
|
+
}
|
|
66
|
+
if (decision === "accept-proposed" || decision === "keep-current" || item.spec.candidateSetStatus === "resolved") {
|
|
67
|
+
return "resolved";
|
|
68
|
+
}
|
|
69
|
+
if (item.spec.candidateSetStatus === "escalated") {
|
|
70
|
+
return "escalated";
|
|
71
|
+
}
|
|
72
|
+
if (item.metadata.name === session.activeItemName) {
|
|
73
|
+
return "in-review";
|
|
74
|
+
}
|
|
75
|
+
return "pending";
|
|
76
|
+
}
|
|
77
|
+
export function nextUnresolvedItemName(session) {
|
|
78
|
+
const activeIndex = session.items.findIndex((item) => item.metadata.name === session.activeItemName);
|
|
79
|
+
const orderedItems = [
|
|
80
|
+
...session.items.slice(Math.max(activeIndex, 0) + 1),
|
|
81
|
+
...session.items.slice(0, Math.max(activeIndex, 0) + 1),
|
|
82
|
+
];
|
|
83
|
+
return orderedItems.find((item) => deriveQueueRowStatus(item, session) === "pending")?.metadata.name;
|
|
84
|
+
}
|
|
85
|
+
export function reviewSessionSummary(session) {
|
|
86
|
+
return session.items.reduce((summary, item) => {
|
|
87
|
+
const decision = session.decisionsByItemName[item.metadata.name];
|
|
88
|
+
if (decision === "accept-proposed") {
|
|
89
|
+
return { ...summary, accepted: summary.accepted + 1 };
|
|
90
|
+
}
|
|
91
|
+
if (decision === "keep-current") {
|
|
92
|
+
return { ...summary, keptCurrent: summary.keptCurrent + 1 };
|
|
93
|
+
}
|
|
94
|
+
if (decision === "reject-proposed") {
|
|
95
|
+
return { ...summary, rejected: summary.rejected + 1 };
|
|
96
|
+
}
|
|
97
|
+
if (item.spec.candidateSetStatus === "escalated") {
|
|
98
|
+
return { ...summary, escalated: summary.escalated + 1 };
|
|
99
|
+
}
|
|
100
|
+
if (item.spec.candidateSetStatus === "resolved") {
|
|
101
|
+
return { ...summary, accepted: summary.accepted + 1 };
|
|
102
|
+
}
|
|
103
|
+
return { ...summary, unresolved: summary.unresolved + 1 };
|
|
104
|
+
}, {
|
|
105
|
+
accepted: 0,
|
|
106
|
+
keptCurrent: 0,
|
|
107
|
+
rejected: 0,
|
|
108
|
+
escalated: 0,
|
|
109
|
+
unresolved: 0,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export function candidateForDecision(item, decision) {
|
|
113
|
+
const definition = workbenchDecisionDefinitions[decision];
|
|
114
|
+
const candidate = item.spec.candidates.find((entry) => entry.role === definition.candidateRole);
|
|
115
|
+
if (!candidate) {
|
|
116
|
+
throw new Error(`ReviewItem ${item.metadata.name} has no ${definition.candidateRole} candidate.`);
|
|
117
|
+
}
|
|
118
|
+
return candidate;
|
|
119
|
+
}
|
|
120
|
+
export function selectedCandidateRole(state) {
|
|
121
|
+
if (!state.decision) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return workbenchDecisionDefinitions[state.decision].candidateRole;
|
|
125
|
+
}
|
|
126
|
+
export function buildReviewSessionResource(session, events = [], sessionName = defaultReviewSessionName) {
|
|
127
|
+
const summary = reviewSessionSummary(session);
|
|
128
|
+
const completedAt = summary.unresolved === 0 ? session.reviewedAt : undefined;
|
|
129
|
+
return {
|
|
130
|
+
apiVersion: reviewResourceApiVersion,
|
|
131
|
+
kind: "ReviewSession",
|
|
132
|
+
metadata: {
|
|
133
|
+
name: sessionName,
|
|
134
|
+
},
|
|
135
|
+
spec: {
|
|
136
|
+
reviewItemNames: session.items.map((item) => item.metadata.name),
|
|
137
|
+
actor: {
|
|
138
|
+
id: session.actorId,
|
|
139
|
+
},
|
|
140
|
+
startedAt: session.reviewedAt,
|
|
141
|
+
completedAt,
|
|
142
|
+
},
|
|
143
|
+
status: {
|
|
144
|
+
activeItemName: session.activeItemName,
|
|
145
|
+
eventCount: events.length,
|
|
146
|
+
decisionCount: Object.keys(session.decisionsByItemName).length,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function buildReviewSessionEvents(session, sessionName = defaultReviewSessionName) {
|
|
151
|
+
const events = [
|
|
152
|
+
buildReviewSessionEvent(session, {
|
|
153
|
+
sessionName,
|
|
154
|
+
sequence: 1,
|
|
155
|
+
eventType: "session-started",
|
|
156
|
+
occurredAt: session.reviewedAt,
|
|
157
|
+
}),
|
|
158
|
+
buildReviewSessionEvent(session, {
|
|
159
|
+
sessionName,
|
|
160
|
+
sequence: 2,
|
|
161
|
+
eventType: "item-selected",
|
|
162
|
+
occurredAt: session.reviewedAt,
|
|
163
|
+
activeItemName: session.activeItemName,
|
|
164
|
+
reviewItemName: session.activeItemName,
|
|
165
|
+
}),
|
|
166
|
+
];
|
|
167
|
+
for (const item of session.items) {
|
|
168
|
+
const note = session.notesByItemName[item.metadata.name];
|
|
169
|
+
if (note) {
|
|
170
|
+
events.push(buildReviewSessionEvent(session, {
|
|
171
|
+
sessionName,
|
|
172
|
+
sequence: events.length + 1,
|
|
173
|
+
eventType: "note-changed",
|
|
174
|
+
occurredAt: session.reviewedAt,
|
|
175
|
+
reviewItemName: item.metadata.name,
|
|
176
|
+
rationale: note,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
const decision = session.decisionsByItemName[item.metadata.name];
|
|
180
|
+
if (!decision) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const candidate = candidateForDecision(item, decision);
|
|
184
|
+
const definition = workbenchDecisionDefinitions[decision];
|
|
185
|
+
const reviewDecisionName = `${item.metadata.name}-${decision}`;
|
|
186
|
+
const data = { workbenchDecision: decision };
|
|
187
|
+
events.push(buildReviewSessionEvent(session, {
|
|
188
|
+
sessionName,
|
|
189
|
+
sequence: events.length + 1,
|
|
190
|
+
eventType: "decision-changed",
|
|
191
|
+
occurredAt: session.reviewedAt,
|
|
192
|
+
reviewItemName: item.metadata.name,
|
|
193
|
+
reviewDecisionName,
|
|
194
|
+
candidateId: candidate.id,
|
|
195
|
+
status: definition.status,
|
|
196
|
+
data,
|
|
197
|
+
}));
|
|
198
|
+
events.push(buildReviewSessionEvent(session, {
|
|
199
|
+
sessionName,
|
|
200
|
+
sequence: events.length + 1,
|
|
201
|
+
eventType: "decision-submitted",
|
|
202
|
+
occurredAt: session.reviewedAt,
|
|
203
|
+
reviewItemName: item.metadata.name,
|
|
204
|
+
reviewDecisionName,
|
|
205
|
+
candidateId: candidate.id,
|
|
206
|
+
status: definition.status,
|
|
207
|
+
rationale: note,
|
|
208
|
+
data,
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
if (reviewSessionSummary(session).unresolved === 0) {
|
|
212
|
+
events.push(buildReviewSessionEvent(session, {
|
|
213
|
+
sessionName,
|
|
214
|
+
sequence: events.length + 1,
|
|
215
|
+
eventType: "session-completed",
|
|
216
|
+
occurredAt: session.reviewedAt,
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
return events;
|
|
220
|
+
}
|
|
221
|
+
export function replayReviewSessionEvents(startState, events) {
|
|
222
|
+
const sortedEvents = [...events].sort((left, right) => left.spec.sequence - right.spec.sequence);
|
|
223
|
+
return sortedEvents.reduce((session, event) => {
|
|
224
|
+
if (event.spec.eventType === "item-selected") {
|
|
225
|
+
const activeItemName = event.spec.activeItemName ?? event.spec.reviewItemName;
|
|
226
|
+
return activeItemName && session.items.some((item) => item.metadata.name === activeItemName)
|
|
227
|
+
? { ...session, activeItemName }
|
|
228
|
+
: session;
|
|
229
|
+
}
|
|
230
|
+
if (event.spec.eventType === "note-changed" && event.spec.reviewItemName) {
|
|
231
|
+
return {
|
|
232
|
+
...session,
|
|
233
|
+
notesByItemName: {
|
|
234
|
+
...session.notesByItemName,
|
|
235
|
+
[event.spec.reviewItemName]: event.spec.rationale ?? "",
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if ((event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted")
|
|
240
|
+
&& event.spec.reviewItemName) {
|
|
241
|
+
const decision = workbenchDecisionFromEvent(event);
|
|
242
|
+
return decision
|
|
243
|
+
? {
|
|
244
|
+
...session,
|
|
245
|
+
decisionsByItemName: {
|
|
246
|
+
...session.decisionsByItemName,
|
|
247
|
+
[event.spec.reviewItemName]: decision,
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
: session;
|
|
251
|
+
}
|
|
252
|
+
return session;
|
|
253
|
+
}, startState);
|
|
254
|
+
}
|
|
255
|
+
export function buildReviewSessionEvent(session, spec) {
|
|
256
|
+
return {
|
|
257
|
+
apiVersion: reviewResourceApiVersion,
|
|
258
|
+
kind: "ReviewSessionEvent",
|
|
259
|
+
metadata: {
|
|
260
|
+
name: `${spec.sessionName}-${String(spec.sequence).padStart(4, "0")}-${spec.eventType}`,
|
|
261
|
+
},
|
|
262
|
+
spec: {
|
|
263
|
+
...spec,
|
|
264
|
+
actor: {
|
|
265
|
+
id: session.actorId,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function workbenchDecisionFromEvent(event) {
|
|
271
|
+
const decision = event.spec.data?.workbenchDecision;
|
|
272
|
+
return typeof decision === "string" && decision in workbenchDecisionDefinitions
|
|
273
|
+
? decision
|
|
274
|
+
: undefined;
|
|
275
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReviewDecision, type ReviewItem } from "
|
|
1
|
+
import { type ReviewDecision, type ReviewItem } from "../review-resource.js";
|
|
2
2
|
export interface SurfaceProjectionPreview {
|
|
3
3
|
readonly canonicalClaim: PreviewClaim;
|
|
4
4
|
readonly candidateHistory: PreviewCandidateHistory[];
|
|
@@ -15,7 +15,24 @@ export function buildSurfaceProjectionPreview(item, decision) {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
export function formatValue(value) {
|
|
18
|
-
|
|
18
|
+
if (typeof value === "string") {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
if (value === undefined) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
if (value === null) {
|
|
25
|
+
return "null";
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return JSON.stringify(value, null, 2);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return String(value);
|
|
35
|
+
}
|
|
19
36
|
}
|
|
20
37
|
function selectedPreviewCandidate(item, decision) {
|
|
21
38
|
if (!decision?.spec.candidateId) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { reviewResourceApiVersion } from "
|
|
1
|
+
import { reviewResourceApiVersion } from "../review-resource.js";
|
|
2
2
|
// Browser-safe mirror of fixtures/public-directory-review-resource.ts.
|
|
3
3
|
// scripts/check-review-workbench.cjs fails if this data drifts from the canonical fixture.
|
|
4
4
|
export const publicDirectoryReviewItemFixture = {
|