@kontourai/survey 0.4.17 → 0.4.20
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 +17 -5
- package/dist/examples/review-workbench/facility-credential-consumer.d.ts +5 -2
- package/dist/examples/review-workbench/facility-credential-consumer.js +16 -5
- package/dist/examples/review-workbench/server-apply-consumer.d.ts +30 -0
- package/dist/examples/review-workbench/server-apply-consumer.js +47 -0
- package/dist/src/review-workbench/review-session-replay.d.ts +12 -0
- package/dist/src/review-workbench/review-session-replay.js +136 -0
- package/dist/src/review-workbench/review-workbench.d.ts +32 -8
- package/dist/src/review-workbench/review-workbench.js +55 -43
- package/dist/src/review-workbench/server-review-session.d.ts +56 -0
- package/dist/src/review-workbench/server-review-session.js +120 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -101,12 +101,24 @@ For the full consumer path from `ReviewItem` construction through persisted
|
|
|
101
101
|
review events, exported results, and optional Surface projection, see
|
|
102
102
|
[`docs/consumer-integration-guide.md`](docs/consumer-integration-guide.md).
|
|
103
103
|
That guide also covers the server-side apply boundary: producers should derive
|
|
104
|
-
write results from
|
|
104
|
+
write results from pre-decision review snapshots plus persisted events, not from
|
|
105
105
|
browser-computed decisions or exported result payloads.
|
|
106
|
-
Use `persistReviewSessionEvents` when server code needs to save review events
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
Use `persistReviewSessionEvents` when server code needs to save review events,
|
|
107
|
+
then pass the persisted event set to `deriveReviewSessionApplyResultForSnapshot`
|
|
108
|
+
before applying product policy. Survey derives selected review results and
|
|
109
|
+
structured replay/completion issues; the producer still owns current-record
|
|
110
|
+
validation and writes.
|
|
111
|
+
For browser-backed queues, server code can import
|
|
112
|
+
`@kontourai/survey/review-workbench/server-review-session` and use
|
|
113
|
+
`createServerReviewSessionRecord`, `hashReviewSessionSnapshot`,
|
|
114
|
+
`assertServerReviewSessionFreshness`, and `assertServerReviewSessionEvents` to
|
|
115
|
+
keep the review snapshot server-owned while accepting browser-submitted
|
|
116
|
+
`ReviewSessionEvent` resources.
|
|
117
|
+
For generic, test-covered consumer examples, see
|
|
118
|
+
[`examples/review-workbench/facility-credential-consumer.ts`](examples/review-workbench/facility-credential-consumer.ts)
|
|
119
|
+
for presentation and event persistence, and
|
|
120
|
+
[`examples/review-workbench/server-apply-consumer.ts`](examples/review-workbench/server-apply-consumer.ts)
|
|
121
|
+
for a compact server-side apply boundary.
|
|
110
122
|
For the current decision on why Survey is not adding a generic review adapter
|
|
111
123
|
builder yet, see
|
|
112
124
|
[`docs/consumer-adapter-abstraction-assessment.md`](docs/consumer-adapter-abstraction-assessment.md).
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { facilityCredentialReviewItemFixture } from "../../src/review-workbench/review-workbench-data.js";
|
|
2
|
-
import { buildReviewItemPresentation, buildReviewResultPresentation,
|
|
2
|
+
import { buildReviewItemPresentation, buildReviewResultPresentation, buildSurfaceProjectionPreview, deriveReviewSessionApplyResultForSnapshot, initialReviewQueueSessionState, type ReviewPresentationAdapter } from "../../src/review-workbench/review-workbench.js";
|
|
3
3
|
import type { ReviewSessionEvent } from "../../src/review-resource.js";
|
|
4
4
|
export declare const facilityCredentialPresentationAdapter: ReviewPresentationAdapter;
|
|
5
5
|
export declare function buildFacilityCredentialConsumerExample(): Promise<FacilityCredentialConsumerExample>;
|
|
6
6
|
export declare const facilityCredentialConsumerExample: FacilityCredentialConsumerExample;
|
|
7
7
|
export interface FacilityCredentialConsumerExample {
|
|
8
8
|
readonly reviewItem: typeof facilityCredentialReviewItemFixture;
|
|
9
|
+
readonly reviewSessionSnapshot: ReturnType<typeof initialReviewQueueSessionState>;
|
|
9
10
|
readonly reviewedSnapshot: ReturnType<typeof initialReviewQueueSessionState>;
|
|
10
11
|
readonly eventsToPersist: readonly ReviewSessionEvent[];
|
|
11
12
|
readonly persistedEvents: readonly ReviewSessionEvent[];
|
|
12
13
|
readonly persistedEventCount: number;
|
|
13
|
-
readonly
|
|
14
|
+
readonly applyResult: Extract<ReturnType<typeof deriveReviewSessionApplyResultForSnapshot>, {
|
|
15
|
+
readonly ok: true;
|
|
16
|
+
}>;
|
|
14
17
|
readonly itemPresentation: ReturnType<typeof buildReviewItemPresentation>;
|
|
15
18
|
readonly resultPresentation: ReturnType<typeof buildReviewResultPresentation>;
|
|
16
19
|
readonly surfaceProjectionPreview: NonNullable<ReturnType<typeof buildSurfaceProjectionPreview>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { facilityCredentialReviewItemFixture } from "../../src/review-workbench/review-workbench-data.js";
|
|
2
|
-
import { buildReviewItemPresentation, buildReviewResultPresentation, buildReviewSessionEvents,
|
|
2
|
+
import { buildReviewItemPresentation, buildReviewResultPresentation, buildReviewSessionEvents, buildSurfaceProjectionPreview, deriveReviewSessionApplyResultForSnapshot, initialReviewQueueSessionState, persistReviewSessionEvents, } from "../../src/review-workbench/review-workbench.js";
|
|
3
3
|
export const facilityCredentialPresentationAdapter = {
|
|
4
4
|
labelForTarget: (target) => target === "operatingLicenseCredential"
|
|
5
5
|
? "Operating license credential"
|
|
@@ -45,10 +45,13 @@ export const facilityCredentialPresentationAdapter = {
|
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
export async function buildFacilityCredentialConsumerExample() {
|
|
48
|
-
const
|
|
48
|
+
const reviewSessionSnapshot = {
|
|
49
49
|
...initialReviewQueueSessionState([facilityCredentialReviewItemFixture]),
|
|
50
50
|
actorId: "review-operator@example.test",
|
|
51
51
|
reviewedAt: "2026-01-17T16:15:00.000Z",
|
|
52
|
+
};
|
|
53
|
+
const reviewedSnapshot = {
|
|
54
|
+
...reviewSessionSnapshot,
|
|
52
55
|
decisionsByItemName: {
|
|
53
56
|
[facilityCredentialReviewItemFixture.metadata.name]: "accept-proposed",
|
|
54
57
|
},
|
|
@@ -70,8 +73,15 @@ export async function buildFacilityCredentialConsumerExample() {
|
|
|
70
73
|
return { eventCount: persistedEvents.length };
|
|
71
74
|
},
|
|
72
75
|
});
|
|
73
|
-
const
|
|
74
|
-
|
|
76
|
+
const applyResult = deriveReviewSessionApplyResultForSnapshot({
|
|
77
|
+
snapshot: reviewSessionSnapshot,
|
|
78
|
+
events: persisted.events,
|
|
79
|
+
requiredResolvedItems: "all",
|
|
80
|
+
});
|
|
81
|
+
if (!applyResult.ok) {
|
|
82
|
+
throw new Error(`Expected persisted credential events to replay before apply: ${applyResult.issues.map((issue) => issue.message).join(" ")}`);
|
|
83
|
+
}
|
|
84
|
+
const [result] = applyResult.results;
|
|
75
85
|
if (!result) {
|
|
76
86
|
throw new Error("Expected the reviewed credential snapshot to produce one review result.");
|
|
77
87
|
}
|
|
@@ -83,11 +93,12 @@ export async function buildFacilityCredentialConsumerExample() {
|
|
|
83
93
|
}
|
|
84
94
|
return {
|
|
85
95
|
reviewItem: facilityCredentialReviewItemFixture,
|
|
96
|
+
reviewSessionSnapshot,
|
|
86
97
|
reviewedSnapshot,
|
|
87
98
|
eventsToPersist,
|
|
88
99
|
persistedEvents: persisted.events,
|
|
89
100
|
persistedEventCount: persisted.eventCount,
|
|
90
|
-
|
|
101
|
+
applyResult,
|
|
91
102
|
itemPresentation,
|
|
92
103
|
resultPresentation,
|
|
93
104
|
surfaceProjectionPreview,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReviewQueueSessionState, type ReviewWorkbenchResult } from "../../src/review-workbench/review-workbench.js";
|
|
2
|
+
import type { ReviewSessionEvent } from "../../src/review-resource.js";
|
|
3
|
+
export interface FacilityCredentialRecord {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly credential: unknown;
|
|
6
|
+
readonly appliedReviewItemNames: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export type FacilityCredentialApplyPreparation = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly mutation: {
|
|
11
|
+
readonly recordId: string;
|
|
12
|
+
readonly credential: unknown;
|
|
13
|
+
readonly reviewItemName: string;
|
|
14
|
+
readonly selectedCandidateId: string;
|
|
15
|
+
readonly actorId: string;
|
|
16
|
+
readonly appliedAt: string;
|
|
17
|
+
};
|
|
18
|
+
readonly result: ReviewWorkbenchResult;
|
|
19
|
+
} | {
|
|
20
|
+
readonly ok: false;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function prepareFacilityCredentialServerApply(input: {
|
|
24
|
+
readonly currentRecord: FacilityCredentialRecord;
|
|
25
|
+
readonly reviewSessionSnapshot: ReviewQueueSessionState;
|
|
26
|
+
readonly events: readonly ReviewSessionEvent[];
|
|
27
|
+
readonly actorId: string;
|
|
28
|
+
readonly appliedAt: string;
|
|
29
|
+
}): FacilityCredentialApplyPreparation;
|
|
30
|
+
export declare const facilityCredentialCurrentRecordFixture: FacilityCredentialRecord;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { facilityCredentialReviewItemFixture } from "../../src/review-workbench/review-workbench-data.js";
|
|
2
|
+
import { deriveReviewSessionApplyResultForSnapshot, } from "../../src/review-workbench/review-workbench.js";
|
|
3
|
+
export function prepareFacilityCredentialServerApply(input) {
|
|
4
|
+
const applyResult = deriveReviewSessionApplyResultForSnapshot({
|
|
5
|
+
snapshot: input.reviewSessionSnapshot,
|
|
6
|
+
events: input.events,
|
|
7
|
+
requiredResolvedItems: "all",
|
|
8
|
+
});
|
|
9
|
+
if (!applyResult.ok) {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
message: applyResult.issues.map((issue) => issue.message).join(" "),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const [result] = applyResult.results;
|
|
16
|
+
if (!result || applyResult.results.length !== 1) {
|
|
17
|
+
return { ok: false, message: "Expected exactly one reviewed credential result." };
|
|
18
|
+
}
|
|
19
|
+
const item = input.reviewSessionSnapshot.items.find((candidate) => candidate.metadata.name === result.reviewItemName);
|
|
20
|
+
if (!item || item.spec.target !== "operatingLicenseCredential") {
|
|
21
|
+
return { ok: false, message: "Review result does not target the credential field." };
|
|
22
|
+
}
|
|
23
|
+
const currentCandidate = item.spec.candidates.find((candidate) => candidate.role === "current");
|
|
24
|
+
if (!currentCandidate || JSON.stringify(currentCandidate.value) !== JSON.stringify(input.currentRecord.credential)) {
|
|
25
|
+
return { ok: false, message: "Current credential no longer matches the review session snapshot." };
|
|
26
|
+
}
|
|
27
|
+
if (input.currentRecord.appliedReviewItemNames.includes(result.reviewItemName)) {
|
|
28
|
+
return { ok: false, message: "Review result was already applied." };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
result,
|
|
33
|
+
mutation: {
|
|
34
|
+
recordId: input.currentRecord.id,
|
|
35
|
+
credential: result.selectedValue,
|
|
36
|
+
reviewItemName: result.reviewItemName,
|
|
37
|
+
selectedCandidateId: result.selectedCandidateId,
|
|
38
|
+
actorId: input.actorId,
|
|
39
|
+
appliedAt: input.appliedAt,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export const facilityCredentialCurrentRecordFixture = {
|
|
44
|
+
id: "facility-credential-record-1",
|
|
45
|
+
credential: facilityCredentialReviewItemFixture.spec.candidates.find((candidate) => candidate.role === "current")?.value,
|
|
46
|
+
appliedReviewItemNames: [],
|
|
47
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReviewSessionEvent } from "../review-resource.js";
|
|
2
|
+
import { type ReviewQueueSessionState } from "./review-queue-session.js";
|
|
3
|
+
export type ReviewSessionReplayIssueCode = "invalid-sequence" | "duplicate-sequence" | "non-contiguous-sequence" | "unknown-active-item" | "unknown-review-item" | "unknown-candidate" | "missing-review-item" | "invalid-workbench-decision" | "decision-candidate-mismatch" | "decision-status-mismatch";
|
|
4
|
+
export interface ReviewSessionReplayIssue {
|
|
5
|
+
readonly code: ReviewSessionReplayIssueCode;
|
|
6
|
+
readonly eventName: string;
|
|
7
|
+
readonly sequence: number;
|
|
8
|
+
readonly reviewItemName?: string;
|
|
9
|
+
readonly candidateId?: string;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function validateReviewSessionEventsForSnapshot(snapshot: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): ReviewSessionReplayIssue[];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { candidateForDecision, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
2
|
+
export function validateReviewSessionEventsForSnapshot(snapshot, events) {
|
|
3
|
+
const itemsByName = new Map(snapshot.items.map((item) => [item.metadata.name, item]));
|
|
4
|
+
const sequenceIssues = validateEventSequence(events);
|
|
5
|
+
const replayIssues = events.flatMap((event) => {
|
|
6
|
+
const issues = [];
|
|
7
|
+
const activeItemName = event.spec.activeItemName;
|
|
8
|
+
const reviewItemName = event.spec.reviewItemName;
|
|
9
|
+
const itemName = reviewItemName ?? activeItemName;
|
|
10
|
+
const eventRef = {
|
|
11
|
+
eventName: event.metadata.name,
|
|
12
|
+
sequence: event.spec.sequence,
|
|
13
|
+
};
|
|
14
|
+
if (activeItemName && !itemsByName.has(activeItemName)) {
|
|
15
|
+
issues.push({
|
|
16
|
+
...eventRef,
|
|
17
|
+
code: "unknown-active-item",
|
|
18
|
+
reviewItemName: activeItemName,
|
|
19
|
+
message: `ReviewSessionEvent ${event.metadata.name} references active item ${activeItemName}, but the supplied session snapshot does not contain that ReviewItem.`,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (reviewItemName && !itemsByName.has(reviewItemName)) {
|
|
23
|
+
issues.push({
|
|
24
|
+
...eventRef,
|
|
25
|
+
code: "unknown-review-item",
|
|
26
|
+
reviewItemName,
|
|
27
|
+
message: `ReviewSessionEvent ${event.metadata.name} references review item ${reviewItemName}, but the supplied session snapshot does not contain that ReviewItem.`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if ((event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted")
|
|
31
|
+
&& !reviewItemName) {
|
|
32
|
+
issues.push({
|
|
33
|
+
...eventRef,
|
|
34
|
+
code: "missing-review-item",
|
|
35
|
+
message: `ReviewSessionEvent ${event.metadata.name} is a decision event but does not reference a ReviewItem.`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted") {
|
|
39
|
+
const decision = replayableWorkbenchDecision(event.spec.data?.workbenchDecision);
|
|
40
|
+
if (!decision) {
|
|
41
|
+
issues.push({
|
|
42
|
+
...eventRef,
|
|
43
|
+
code: "invalid-workbench-decision",
|
|
44
|
+
reviewItemName,
|
|
45
|
+
candidateId: event.spec.candidateId,
|
|
46
|
+
message: `ReviewSessionEvent ${event.metadata.name} is a decision event but does not include a replayable workbench decision.`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else if (itemName && itemsByName.has(itemName)) {
|
|
50
|
+
const item = itemsByName.get(itemName);
|
|
51
|
+
const expectedCandidate = item ? candidateForDecision(item, decision) : undefined;
|
|
52
|
+
const expectedStatus = workbenchDecisionDefinitions[decision].status;
|
|
53
|
+
const referencedCandidateExists = event.spec.candidateId
|
|
54
|
+
? item?.spec.candidates.some((candidate) => candidate.id === event.spec.candidateId)
|
|
55
|
+
: false;
|
|
56
|
+
if (expectedCandidate && (!event.spec.candidateId || referencedCandidateExists) && event.spec.candidateId !== expectedCandidate.id) {
|
|
57
|
+
issues.push({
|
|
58
|
+
...eventRef,
|
|
59
|
+
code: "decision-candidate-mismatch",
|
|
60
|
+
reviewItemName: itemName,
|
|
61
|
+
candidateId: event.spec.candidateId,
|
|
62
|
+
message: `ReviewSessionEvent ${event.metadata.name} decision ${decision} expects candidate ${expectedCandidate.id}, but references ${event.spec.candidateId ?? "no candidate"}.`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (event.spec.status !== expectedStatus) {
|
|
66
|
+
issues.push({
|
|
67
|
+
...eventRef,
|
|
68
|
+
code: "decision-status-mismatch",
|
|
69
|
+
reviewItemName: itemName,
|
|
70
|
+
candidateId: event.spec.candidateId,
|
|
71
|
+
message: `ReviewSessionEvent ${event.metadata.name} decision ${decision} expects status ${expectedStatus}, but references ${event.spec.status ?? "no status"}.`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (event.spec.candidateId && itemName && itemsByName.has(itemName)) {
|
|
77
|
+
const item = itemsByName.get(itemName);
|
|
78
|
+
const hasCandidate = item?.spec.candidates.some((candidate) => candidate.id === event.spec.candidateId);
|
|
79
|
+
if (!hasCandidate) {
|
|
80
|
+
issues.push({
|
|
81
|
+
...eventRef,
|
|
82
|
+
code: "unknown-candidate",
|
|
83
|
+
reviewItemName: itemName,
|
|
84
|
+
candidateId: event.spec.candidateId,
|
|
85
|
+
message: `ReviewSessionEvent ${event.metadata.name} references candidate ${event.spec.candidateId}, but ReviewItem ${itemName} in the supplied session snapshot does not contain that candidate.`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return issues;
|
|
90
|
+
});
|
|
91
|
+
return [...sequenceIssues, ...replayIssues];
|
|
92
|
+
}
|
|
93
|
+
function replayableWorkbenchDecision(value) {
|
|
94
|
+
return typeof value === "string" && value in workbenchDecisionDefinitions
|
|
95
|
+
? value
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
|
98
|
+
function validateEventSequence(events) {
|
|
99
|
+
const issues = [];
|
|
100
|
+
const seen = new Map();
|
|
101
|
+
events.forEach((event, index) => {
|
|
102
|
+
const sequence = event.spec.sequence;
|
|
103
|
+
const eventRef = {
|
|
104
|
+
eventName: event.metadata.name,
|
|
105
|
+
sequence,
|
|
106
|
+
};
|
|
107
|
+
if (!Number.isSafeInteger(sequence) || sequence < 1) {
|
|
108
|
+
issues.push({
|
|
109
|
+
...eventRef,
|
|
110
|
+
code: "invalid-sequence",
|
|
111
|
+
message: `ReviewSessionEvent ${event.metadata.name} has invalid sequence ${String(sequence)}. Sequences must be positive safe integers.`,
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const duplicateOf = seen.get(sequence);
|
|
116
|
+
if (duplicateOf) {
|
|
117
|
+
issues.push({
|
|
118
|
+
...eventRef,
|
|
119
|
+
code: "duplicate-sequence",
|
|
120
|
+
message: `ReviewSessionEvent ${event.metadata.name} reuses sequence ${sequence} from ${duplicateOf}.`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
seen.set(sequence, event.metadata.name);
|
|
125
|
+
}
|
|
126
|
+
const expected = index + 1;
|
|
127
|
+
if (sequence !== expected) {
|
|
128
|
+
issues.push({
|
|
129
|
+
...eventRef,
|
|
130
|
+
code: "non-contiguous-sequence",
|
|
131
|
+
message: `ReviewSessionEvent ${event.metadata.name} has sequence ${sequence}, but canonical event streams must be ordered and contiguous from 1; expected ${expected}.`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return issues;
|
|
136
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type ReviewQueueSessionState, type ReviewWorkbenchDecision, type ReviewWorkbenchState } from "./review-queue-session.js";
|
|
2
|
+
import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
|
|
2
3
|
import { type ReviewPresentationAdapter } from "./review-presentation.js";
|
|
3
4
|
import { type ReviewCandidate, type ReviewDecision, type ReviewSession, type ReviewSessionEvent } from "../review-resource.js";
|
|
4
5
|
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
|
|
5
6
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, type ReviewCandidatePresentation, type ReviewCandidatePresentationContext, type ReviewItemPresentation, type ReviewItemPresentationContext, type ReviewPresentationAdapter, type ReviewPresentationLink, type ReviewResultPresentation, type ReviewTracePresentationContext, type ReviewTraceRef, type ReviewValuePresentationContext, } from "./review-presentation.js";
|
|
6
7
|
export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
8
|
+
export { validateReviewSessionEventsForSnapshot, type ReviewSessionReplayIssue, type ReviewSessionReplayIssueCode, } from "./review-session-replay.js";
|
|
7
9
|
export declare function buildReviewDecision(state: ReviewWorkbenchState): ReviewDecision | undefined;
|
|
8
10
|
export interface ReviewWorkbenchSessionExport {
|
|
9
11
|
readonly session: ReviewSession;
|
|
@@ -11,15 +13,37 @@ export interface ReviewWorkbenchSessionExport {
|
|
|
11
13
|
readonly decisions: readonly ReviewDecision[];
|
|
12
14
|
readonly results: readonly ReviewWorkbenchResult[];
|
|
13
15
|
}
|
|
14
|
-
export type
|
|
15
|
-
export
|
|
16
|
-
readonly code:
|
|
17
|
-
readonly
|
|
18
|
-
readonly
|
|
19
|
-
|
|
20
|
-
readonly
|
|
16
|
+
export type ReviewSessionApplyResolutionRequirement = "all" | "any" | "none";
|
|
17
|
+
export type ReviewSessionApplyIssue = ReviewSessionReplayIssue | {
|
|
18
|
+
readonly code: "unresolved-review-item";
|
|
19
|
+
readonly reviewItemName: string;
|
|
20
|
+
readonly message: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly code: "no-resolved-review-items";
|
|
21
23
|
readonly message: string;
|
|
24
|
+
};
|
|
25
|
+
export interface DeriveReviewSessionApplyResultForSnapshotOptions {
|
|
26
|
+
readonly snapshot: ReviewQueueSessionState;
|
|
27
|
+
readonly events: readonly ReviewSessionEvent[];
|
|
28
|
+
readonly requiredResolvedItems?: ReviewSessionApplyResolutionRequirement;
|
|
22
29
|
}
|
|
30
|
+
export type DeriveReviewSessionApplyResultForSnapshotResult = {
|
|
31
|
+
readonly ok: true;
|
|
32
|
+
readonly issues: readonly [];
|
|
33
|
+
readonly unresolvedItemNames: readonly string[];
|
|
34
|
+
readonly replayedSession: ReviewQueueSessionState;
|
|
35
|
+
readonly sessionExport: ReviewWorkbenchSessionExport;
|
|
36
|
+
readonly decisions: readonly ReviewDecision[];
|
|
37
|
+
readonly results: readonly ReviewWorkbenchResult[];
|
|
38
|
+
} | {
|
|
39
|
+
readonly ok: false;
|
|
40
|
+
readonly issues: readonly ReviewSessionApplyIssue[];
|
|
41
|
+
readonly unresolvedItemNames: readonly string[];
|
|
42
|
+
readonly replayedSession?: ReviewQueueSessionState;
|
|
43
|
+
readonly sessionExport?: ReviewWorkbenchSessionExport;
|
|
44
|
+
readonly decisions: readonly ReviewDecision[];
|
|
45
|
+
readonly results: readonly ReviewWorkbenchResult[];
|
|
46
|
+
};
|
|
23
47
|
export interface ReviewSessionEventStore {
|
|
24
48
|
load(session: ReviewQueueSessionState): readonly ReviewSessionEvent[] | undefined;
|
|
25
49
|
save(session: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): void;
|
|
@@ -77,9 +101,9 @@ export interface ReviewWorkbenchResult {
|
|
|
77
101
|
export declare function buildReviewDecisionsFromSession(session: ReviewQueueSessionState): ReviewDecision[];
|
|
78
102
|
export declare function buildReviewWorkbenchResultsFromSession(session: ReviewQueueSessionState): ReviewWorkbenchResult[];
|
|
79
103
|
export declare function buildReviewWorkbenchSessionExport(session: ReviewQueueSessionState, events?: readonly ReviewSessionEvent[]): ReviewWorkbenchSessionExport;
|
|
80
|
-
export declare function validateReviewSessionEventsForSnapshot(snapshot: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): ReviewSessionReplayIssue[];
|
|
81
104
|
export declare function replayReviewSessionEventsForSnapshot(snapshot: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): ReviewQueueSessionState;
|
|
82
105
|
export declare function buildReviewWorkbenchSessionExportForSnapshot(snapshot: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): ReviewWorkbenchSessionExport;
|
|
106
|
+
export declare function deriveReviewSessionApplyResultForSnapshot(options: DeriveReviewSessionApplyResultForSnapshotOptions): DeriveReviewSessionApplyResultForSnapshotResult;
|
|
83
107
|
export declare function createInMemoryReviewSessionEventStore(initialEvents?: readonly ReviewSessionEvent[]): ReviewSessionEventStore & {
|
|
84
108
|
events(): readonly ReviewSessionEvent[];
|
|
85
109
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { candidateForDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
2
|
+
import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
2
3
|
import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
|
|
3
4
|
import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "./review-presentation.js";
|
|
4
5
|
import { reviewResourceApiVersion, } from "../review-resource.js";
|
|
5
6
|
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
6
7
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
7
8
|
export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
9
|
+
export { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
8
10
|
export function buildReviewDecision(state) {
|
|
9
11
|
if (!state.decision) {
|
|
10
12
|
return undefined;
|
|
@@ -91,49 +93,6 @@ export function buildReviewWorkbenchSessionExport(session, events = buildReviewS
|
|
|
91
93
|
results: buildReviewWorkbenchResultsFromSession(session),
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
|
-
export function validateReviewSessionEventsForSnapshot(snapshot, events) {
|
|
95
|
-
const itemsByName = new Map(snapshot.items.map((item) => [item.metadata.name, item]));
|
|
96
|
-
return events.flatMap((event) => {
|
|
97
|
-
const issues = [];
|
|
98
|
-
const activeItemName = event.spec.activeItemName;
|
|
99
|
-
const reviewItemName = event.spec.reviewItemName;
|
|
100
|
-
const itemName = reviewItemName ?? activeItemName;
|
|
101
|
-
const eventRef = {
|
|
102
|
-
eventName: event.metadata.name,
|
|
103
|
-
sequence: event.spec.sequence,
|
|
104
|
-
};
|
|
105
|
-
if (activeItemName && !itemsByName.has(activeItemName)) {
|
|
106
|
-
issues.push({
|
|
107
|
-
...eventRef,
|
|
108
|
-
code: "unknown-active-item",
|
|
109
|
-
reviewItemName: activeItemName,
|
|
110
|
-
message: `ReviewSessionEvent ${event.metadata.name} references active item ${activeItemName}, but the supplied session snapshot does not contain that ReviewItem.`,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
if (reviewItemName && !itemsByName.has(reviewItemName)) {
|
|
114
|
-
issues.push({
|
|
115
|
-
...eventRef,
|
|
116
|
-
code: "unknown-review-item",
|
|
117
|
-
reviewItemName,
|
|
118
|
-
message: `ReviewSessionEvent ${event.metadata.name} references review item ${reviewItemName}, but the supplied session snapshot does not contain that ReviewItem.`,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
if (event.spec.candidateId && itemName && itemsByName.has(itemName)) {
|
|
122
|
-
const item = itemsByName.get(itemName);
|
|
123
|
-
const hasCandidate = item?.spec.candidates.some((candidate) => candidate.id === event.spec.candidateId);
|
|
124
|
-
if (!hasCandidate) {
|
|
125
|
-
issues.push({
|
|
126
|
-
...eventRef,
|
|
127
|
-
code: "unknown-candidate",
|
|
128
|
-
reviewItemName: itemName,
|
|
129
|
-
candidateId: event.spec.candidateId,
|
|
130
|
-
message: `ReviewSessionEvent ${event.metadata.name} references candidate ${event.spec.candidateId}, but ReviewItem ${itemName} in the supplied session snapshot does not contain that candidate.`,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return issues;
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
96
|
export function replayReviewSessionEventsForSnapshot(snapshot, events) {
|
|
138
97
|
const issues = validateReviewSessionEventsForSnapshot(snapshot, events);
|
|
139
98
|
if (issues.length > 0) {
|
|
@@ -145,6 +104,59 @@ export function buildReviewWorkbenchSessionExportForSnapshot(snapshot, events) {
|
|
|
145
104
|
const replayedSession = replayReviewSessionEventsForSnapshot(snapshot, events);
|
|
146
105
|
return buildReviewWorkbenchSessionExport(replayedSession, events);
|
|
147
106
|
}
|
|
107
|
+
export function deriveReviewSessionApplyResultForSnapshot(options) {
|
|
108
|
+
const requiredResolvedItems = options.requiredResolvedItems ?? "none";
|
|
109
|
+
const replayIssues = validateReviewSessionEventsForSnapshot(options.snapshot, options.events);
|
|
110
|
+
if (replayIssues.length > 0) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
issues: replayIssues,
|
|
114
|
+
unresolvedItemNames: options.snapshot.items.map((item) => item.metadata.name),
|
|
115
|
+
decisions: [],
|
|
116
|
+
results: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const replayedSession = replayReviewSessionEvents(options.snapshot, options.events);
|
|
120
|
+
const sessionExport = buildReviewWorkbenchSessionExport(replayedSession, options.events);
|
|
121
|
+
const resolvedItemNames = new Set(sessionExport.results.map((result) => result.reviewItemName));
|
|
122
|
+
const unresolvedItemNames = options.snapshot.items
|
|
123
|
+
.map((item) => item.metadata.name)
|
|
124
|
+
.filter((itemName) => !resolvedItemNames.has(itemName));
|
|
125
|
+
const issues = [];
|
|
126
|
+
if (requiredResolvedItems === "all") {
|
|
127
|
+
issues.push(...unresolvedItemNames.map((reviewItemName) => ({
|
|
128
|
+
code: "unresolved-review-item",
|
|
129
|
+
reviewItemName,
|
|
130
|
+
message: `Review item ${reviewItemName} has no resolved review decision.`,
|
|
131
|
+
})));
|
|
132
|
+
}
|
|
133
|
+
if (requiredResolvedItems === "any" && sessionExport.results.length === 0) {
|
|
134
|
+
issues.push({
|
|
135
|
+
code: "no-resolved-review-items",
|
|
136
|
+
message: "Review session has no resolved review decisions.",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (issues.length > 0) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
issues,
|
|
143
|
+
unresolvedItemNames,
|
|
144
|
+
replayedSession,
|
|
145
|
+
sessionExport,
|
|
146
|
+
decisions: sessionExport.decisions,
|
|
147
|
+
results: sessionExport.results,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
issues: [],
|
|
153
|
+
unresolvedItemNames,
|
|
154
|
+
replayedSession,
|
|
155
|
+
sessionExport,
|
|
156
|
+
decisions: sessionExport.decisions,
|
|
157
|
+
results: sessionExport.results,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
148
160
|
export function createInMemoryReviewSessionEventStore(initialEvents = []) {
|
|
149
161
|
let savedEvents = [...initialEvents];
|
|
150
162
|
return {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
|
|
2
|
+
import type { ReviewSessionEvent } from "../review-resource.js";
|
|
3
|
+
import type { ReviewQueueSessionState } from "./review-queue-session.js";
|
|
4
|
+
export interface ServerReviewSessionRecord {
|
|
5
|
+
readonly sessionName: string;
|
|
6
|
+
readonly snapshot: ReviewQueueSessionState;
|
|
7
|
+
readonly snapshotHash: string;
|
|
8
|
+
readonly eventCount?: number;
|
|
9
|
+
readonly updatedAt: string;
|
|
10
|
+
}
|
|
11
|
+
export type ServerReviewSessionFreshnessStatus = "current" | "stale";
|
|
12
|
+
export interface ServerReviewSessionFreshnessComparison {
|
|
13
|
+
readonly status: ServerReviewSessionFreshnessStatus;
|
|
14
|
+
readonly expectedSnapshotHash: string;
|
|
15
|
+
readonly actualSnapshotHash: string;
|
|
16
|
+
readonly expectedEventCount?: number;
|
|
17
|
+
readonly actualEventCount?: number;
|
|
18
|
+
}
|
|
19
|
+
export type ServerReviewSessionStaleIssueCode = "snapshot-hash-mismatch" | "event-count-mismatch";
|
|
20
|
+
export interface ServerReviewSessionStaleIssue {
|
|
21
|
+
readonly code: ServerReviewSessionStaleIssueCode;
|
|
22
|
+
readonly message: string;
|
|
23
|
+
readonly expected: string | number;
|
|
24
|
+
readonly actual: string | number;
|
|
25
|
+
}
|
|
26
|
+
export declare class StaleServerReviewSessionError extends Error {
|
|
27
|
+
readonly name = "StaleServerReviewSessionError";
|
|
28
|
+
readonly issues: readonly ServerReviewSessionStaleIssue[];
|
|
29
|
+
readonly comparison: ServerReviewSessionFreshnessComparison;
|
|
30
|
+
constructor(comparison: ServerReviewSessionFreshnessComparison);
|
|
31
|
+
}
|
|
32
|
+
export type ServerReviewSessionEventValidationIssue = ReviewSessionReplayIssue | {
|
|
33
|
+
readonly code: "session-name-mismatch";
|
|
34
|
+
readonly eventName: string;
|
|
35
|
+
readonly sequence: number;
|
|
36
|
+
readonly expectedSessionName: string;
|
|
37
|
+
readonly actualSessionName: string;
|
|
38
|
+
readonly message: string;
|
|
39
|
+
};
|
|
40
|
+
export declare class ServerReviewSessionEventValidationError extends Error {
|
|
41
|
+
readonly name = "ServerReviewSessionEventValidationError";
|
|
42
|
+
readonly issues: readonly ServerReviewSessionEventValidationIssue[];
|
|
43
|
+
constructor(issues: readonly ServerReviewSessionEventValidationIssue[]);
|
|
44
|
+
}
|
|
45
|
+
export interface CreateServerReviewSessionRecordOptions {
|
|
46
|
+
readonly sessionName: string;
|
|
47
|
+
readonly snapshot: ReviewQueueSessionState;
|
|
48
|
+
readonly eventCount?: number;
|
|
49
|
+
readonly updatedAt?: string | Date;
|
|
50
|
+
}
|
|
51
|
+
export declare function createServerReviewSessionRecord(options: CreateServerReviewSessionRecordOptions): ServerReviewSessionRecord;
|
|
52
|
+
export declare function hashReviewSessionSnapshot(snapshot: ReviewQueueSessionState): string;
|
|
53
|
+
export declare function compareServerReviewSessionFreshness(record: ServerReviewSessionRecord, snapshot: ReviewQueueSessionState, eventCount?: number): ServerReviewSessionFreshnessComparison;
|
|
54
|
+
export declare function assertServerReviewSessionFreshness(record: ServerReviewSessionRecord, snapshot: ReviewQueueSessionState, eventCount?: number): void;
|
|
55
|
+
export declare function validateServerReviewSessionEvents(record: ServerReviewSessionRecord, events: readonly ReviewSessionEvent[]): ServerReviewSessionEventValidationIssue[];
|
|
56
|
+
export declare function assertServerReviewSessionEvents(record: ServerReviewSessionRecord, events: readonly ReviewSessionEvent[]): void;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
3
|
+
export class StaleServerReviewSessionError extends Error {
|
|
4
|
+
name = "StaleServerReviewSessionError";
|
|
5
|
+
issues;
|
|
6
|
+
comparison;
|
|
7
|
+
constructor(comparison) {
|
|
8
|
+
const issues = staleIssuesForComparison(comparison);
|
|
9
|
+
super(`Review session is stale: ${issues.map((issue) => issue.message).join(" ")}`);
|
|
10
|
+
this.issues = issues;
|
|
11
|
+
this.comparison = comparison;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class ServerReviewSessionEventValidationError extends Error {
|
|
15
|
+
name = "ServerReviewSessionEventValidationError";
|
|
16
|
+
issues;
|
|
17
|
+
constructor(issues) {
|
|
18
|
+
super(`Review session events are invalid: ${issues.map((issue) => issue.message).join(" ")}`);
|
|
19
|
+
this.issues = issues;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function createServerReviewSessionRecord(options) {
|
|
23
|
+
return {
|
|
24
|
+
sessionName: options.sessionName,
|
|
25
|
+
snapshot: options.snapshot,
|
|
26
|
+
snapshotHash: hashReviewSessionSnapshot(options.snapshot),
|
|
27
|
+
eventCount: options.eventCount,
|
|
28
|
+
updatedAt: isoTimestamp(options.updatedAt ?? new Date()),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function hashReviewSessionSnapshot(snapshot) {
|
|
32
|
+
return sha256(canonicalJson(snapshot));
|
|
33
|
+
}
|
|
34
|
+
export function compareServerReviewSessionFreshness(record, snapshot, eventCount) {
|
|
35
|
+
const actualSnapshotHash = hashReviewSessionSnapshot(snapshot);
|
|
36
|
+
const eventCountMatches = record.eventCount === undefined || eventCount === undefined || record.eventCount === eventCount;
|
|
37
|
+
const current = record.snapshotHash === actualSnapshotHash && eventCountMatches;
|
|
38
|
+
return {
|
|
39
|
+
status: current ? "current" : "stale",
|
|
40
|
+
expectedSnapshotHash: record.snapshotHash,
|
|
41
|
+
actualSnapshotHash,
|
|
42
|
+
expectedEventCount: record.eventCount,
|
|
43
|
+
actualEventCount: eventCount,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function assertServerReviewSessionFreshness(record, snapshot, eventCount) {
|
|
47
|
+
const comparison = compareServerReviewSessionFreshness(record, snapshot, eventCount);
|
|
48
|
+
if (comparison.status === "stale") {
|
|
49
|
+
throw new StaleServerReviewSessionError(comparison);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function validateServerReviewSessionEvents(record, events) {
|
|
53
|
+
const replayIssues = validateReviewSessionEventsForSnapshot(record.snapshot, events);
|
|
54
|
+
const sessionIssues = events.flatMap((event) => {
|
|
55
|
+
if (event.spec.sessionName === record.sessionName) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return [{
|
|
59
|
+
code: "session-name-mismatch",
|
|
60
|
+
eventName: event.metadata.name,
|
|
61
|
+
sequence: event.spec.sequence,
|
|
62
|
+
expectedSessionName: record.sessionName,
|
|
63
|
+
actualSessionName: event.spec.sessionName,
|
|
64
|
+
message: `ReviewSessionEvent ${event.metadata.name} references session ${event.spec.sessionName}, but the server session is ${record.sessionName}.`,
|
|
65
|
+
}];
|
|
66
|
+
});
|
|
67
|
+
return [...sessionIssues, ...replayIssues];
|
|
68
|
+
}
|
|
69
|
+
export function assertServerReviewSessionEvents(record, events) {
|
|
70
|
+
const issues = validateServerReviewSessionEvents(record, events);
|
|
71
|
+
if (issues.length > 0) {
|
|
72
|
+
throw new ServerReviewSessionEventValidationError(issues);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function staleIssuesForComparison(comparison) {
|
|
76
|
+
const issues = [];
|
|
77
|
+
if (comparison.expectedSnapshotHash !== comparison.actualSnapshotHash) {
|
|
78
|
+
issues.push({
|
|
79
|
+
code: "snapshot-hash-mismatch",
|
|
80
|
+
expected: comparison.expectedSnapshotHash,
|
|
81
|
+
actual: comparison.actualSnapshotHash,
|
|
82
|
+
message: `Expected snapshot hash ${comparison.expectedSnapshotHash}, received ${comparison.actualSnapshotHash}.`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (comparison.expectedEventCount !== undefined
|
|
86
|
+
&& comparison.actualEventCount !== undefined
|
|
87
|
+
&& comparison.expectedEventCount !== comparison.actualEventCount) {
|
|
88
|
+
issues.push({
|
|
89
|
+
code: "event-count-mismatch",
|
|
90
|
+
expected: comparison.expectedEventCount,
|
|
91
|
+
actual: comparison.actualEventCount,
|
|
92
|
+
message: `Expected ${comparison.expectedEventCount} events, received ${comparison.actualEventCount}.`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return issues;
|
|
96
|
+
}
|
|
97
|
+
function canonicalJson(value) {
|
|
98
|
+
return JSON.stringify(canonicalize(value));
|
|
99
|
+
}
|
|
100
|
+
function canonicalize(value) {
|
|
101
|
+
if (value instanceof Date) {
|
|
102
|
+
return value.toISOString();
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
return value.map((entry) => canonicalize(entry));
|
|
106
|
+
}
|
|
107
|
+
if (value && typeof value === "object") {
|
|
108
|
+
const entries = Object.entries(value)
|
|
109
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
110
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
111
|
+
return Object.fromEntries(entries.map(([key, entryValue]) => [key, canonicalize(entryValue)]));
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function sha256(value) {
|
|
116
|
+
return createHash("sha256").update(value).digest("hex");
|
|
117
|
+
}
|
|
118
|
+
function isoTimestamp(value) {
|
|
119
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/survey",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.20",
|
|
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",
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"types": "./dist/src/review-workbench/review-workbench.d.ts",
|
|
23
23
|
"default": "./dist/src/review-workbench/review-workbench.js"
|
|
24
24
|
},
|
|
25
|
+
"./review-workbench/server-review-session": {
|
|
26
|
+
"types": "./dist/src/review-workbench/server-review-session.d.ts",
|
|
27
|
+
"default": "./dist/src/review-workbench/server-review-session.js"
|
|
28
|
+
},
|
|
25
29
|
"./review-workbench.css": "./dist/src/review-workbench/review-workbench.css",
|
|
26
30
|
"./review-workbench/standalone.css": "./dist/src/review-workbench/review-workbench.standalone.css",
|
|
27
31
|
"./fixtures/public-field-review": "./dist/fixtures/public-field-review.js",
|