@kontourai/survey 0.4.14 → 0.4.15
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/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/review-workbench/review-presentation.d.ts +67 -0
- package/dist/src/review-workbench/review-presentation.js +133 -0
- package/dist/src/review-workbench/review-surface-preview.d.ts +2 -1
- package/dist/src/review-workbench/review-surface-preview.js +9 -7
- package/dist/src/review-workbench/review-workbench.d.ts +6 -1
- package/dist/src/review-workbench/review-workbench.js +64 -59
- package/package.json +1 -1
package/dist/src/index.d.ts
CHANGED
|
@@ -19,5 +19,7 @@ export { repeatedObservation } from "./repeated-observation.js";
|
|
|
19
19
|
export type { RepeatedObservationInput } from "./repeated-observation.js";
|
|
20
20
|
export { sourceOfAuthorityObservation, sourceOfAuthorityObservationBuilder, SourceOfAuthorityObservationBuilder, } from "./source-of-authority-observation.js";
|
|
21
21
|
export type { SourceAuthorityClass, SourceAuthorityMetadata, SourceOfAuthorityObservationBuilderArgs, SourceOfAuthorityObservationInput, } from "./source-of-authority-observation.js";
|
|
22
|
+
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-workbench/review-presentation.js";
|
|
23
|
+
export type { ReviewCandidatePresentation, ReviewCandidatePresentationContext, ReviewItemPresentation, ReviewItemPresentationContext, ReviewPresentationAdapter, ReviewPresentationLink, ReviewResultPresentation, ReviewTracePresentationContext, ReviewTraceRef, ReviewValuePresentationContext, } from "./review-workbench/review-presentation.js";
|
|
22
24
|
export { apiRecordSource, manualEntrySource, policyStandardSource, uploadedDocumentSource, webPageSource, } from "./raw-source.js";
|
|
23
25
|
export type { ApiRecordSourceInput, ChecksumInput, ManualEntrySourceInput, PolicyStandardMetadata, PolicyStandardSourceInput, RawSourceInput, UploadedDocumentSourceInput, WebPageSourceInput, } from "./raw-source.js";
|
package/dist/src/index.js
CHANGED
|
@@ -8,4 +8,5 @@ export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalRevi
|
|
|
8
8
|
export { fieldObservation } from "./field-observation.js";
|
|
9
9
|
export { repeatedObservation } from "./repeated-observation.js";
|
|
10
10
|
export { sourceOfAuthorityObservation, sourceOfAuthorityObservationBuilder, SourceOfAuthorityObservationBuilder, } from "./source-of-authority-observation.js";
|
|
11
|
+
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-workbench/review-presentation.js";
|
|
11
12
|
export { apiRecordSource, manualEntrySource, policyStandardSource, uploadedDocumentSource, webPageSource, } from "./raw-source.js";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ReviewCandidate, type ReviewItem } from "../review-resource.js";
|
|
2
|
+
import { type ReviewWorkbenchResult } from "./review-workbench.js";
|
|
3
|
+
export interface ReviewPresentationAdapter {
|
|
4
|
+
readonly labelForTarget?: (target: string, context: ReviewItemPresentationContext) => string | undefined;
|
|
5
|
+
readonly labelForCandidateRole?: (role: ReviewCandidate["role"] | undefined, context: ReviewCandidatePresentationContext) => string | undefined;
|
|
6
|
+
readonly summarizeValue?: (value: unknown, context: ReviewValuePresentationContext) => string | undefined;
|
|
7
|
+
readonly linkForReviewItem?: (item: ReviewItem, context: ReviewItemPresentationContext) => ReviewPresentationLink | undefined;
|
|
8
|
+
readonly linkForSource?: (sourceRef: string, context: ReviewCandidatePresentationContext) => ReviewPresentationLink | undefined;
|
|
9
|
+
readonly linkForTraceRef?: (ref: ReviewTraceRef, context: ReviewTracePresentationContext) => ReviewPresentationLink | undefined;
|
|
10
|
+
readonly statusLabel?: (status: string, context: ReviewItemPresentationContext) => string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface ReviewItemPresentationContext {
|
|
13
|
+
readonly item: ReviewItem;
|
|
14
|
+
}
|
|
15
|
+
export interface ReviewCandidatePresentationContext extends ReviewItemPresentationContext {
|
|
16
|
+
readonly candidate: ReviewCandidate;
|
|
17
|
+
}
|
|
18
|
+
export interface ReviewValuePresentationContext extends ReviewCandidatePresentationContext {
|
|
19
|
+
readonly value: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface ReviewTracePresentationContext extends ReviewItemPresentationContext {
|
|
22
|
+
readonly candidate?: ReviewCandidate;
|
|
23
|
+
}
|
|
24
|
+
export interface ReviewPresentationLink {
|
|
25
|
+
readonly label?: string;
|
|
26
|
+
readonly href: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ReviewTraceRef {
|
|
29
|
+
readonly label: string;
|
|
30
|
+
readonly value: string;
|
|
31
|
+
readonly kind: "review-item" | "candidate" | "candidate-set" | "claim" | "source" | "locator" | "proposal" | "external-record";
|
|
32
|
+
readonly link?: ReviewPresentationLink;
|
|
33
|
+
}
|
|
34
|
+
export interface ReviewCandidatePresentation {
|
|
35
|
+
readonly candidate: ReviewCandidate;
|
|
36
|
+
readonly roleLabel: string;
|
|
37
|
+
readonly valueLabel: string;
|
|
38
|
+
readonly valueText: string;
|
|
39
|
+
readonly sourceLabel: string;
|
|
40
|
+
readonly sourceText: string;
|
|
41
|
+
readonly sourceLink?: ReviewPresentationLink;
|
|
42
|
+
readonly traceRefs: readonly ReviewTraceRef[];
|
|
43
|
+
}
|
|
44
|
+
export interface ReviewItemPresentation {
|
|
45
|
+
readonly item: ReviewItem;
|
|
46
|
+
readonly target: string;
|
|
47
|
+
readonly targetLabel: string;
|
|
48
|
+
readonly statusLabel: string;
|
|
49
|
+
readonly reviewItemLink?: ReviewPresentationLink;
|
|
50
|
+
readonly traceRefs: readonly ReviewTraceRef[];
|
|
51
|
+
readonly candidates: readonly ReviewCandidatePresentation[];
|
|
52
|
+
}
|
|
53
|
+
export interface ReviewResultPresentation {
|
|
54
|
+
readonly result: ReviewWorkbenchResult;
|
|
55
|
+
readonly item?: ReviewItem;
|
|
56
|
+
readonly target: string;
|
|
57
|
+
readonly targetLabel: string;
|
|
58
|
+
readonly decisionLabel: string;
|
|
59
|
+
readonly selectedValueText: string;
|
|
60
|
+
readonly applyMeaning: string;
|
|
61
|
+
readonly reviewItemLink?: ReviewPresentationLink;
|
|
62
|
+
readonly traceRefs: readonly ReviewTraceRef[];
|
|
63
|
+
}
|
|
64
|
+
export declare function buildReviewItemPresentation(item: ReviewItem, adapter?: ReviewPresentationAdapter): ReviewItemPresentation;
|
|
65
|
+
export declare function buildReviewCandidatePresentation(item: ReviewItem, candidate: ReviewCandidate, adapter?: ReviewPresentationAdapter, targetLabel?: string): ReviewCandidatePresentation;
|
|
66
|
+
export declare function buildReviewResultPresentation(result: ReviewWorkbenchResult, item: ReviewItem | undefined, adapter?: ReviewPresentationAdapter): ReviewResultPresentation;
|
|
67
|
+
export declare function humanizeIdentifier(value: string): string;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { formatValue } from "./review-surface-preview.js";
|
|
2
|
+
export function buildReviewItemPresentation(item, adapter = {}) {
|
|
3
|
+
const context = { item };
|
|
4
|
+
const targetLabel = adapter.labelForTarget?.(item.spec.target, context) ?? humanizeIdentifier(item.spec.target);
|
|
5
|
+
const status = item.spec.candidateSetStatus ?? "needs-review";
|
|
6
|
+
return {
|
|
7
|
+
item,
|
|
8
|
+
target: item.spec.target,
|
|
9
|
+
targetLabel,
|
|
10
|
+
statusLabel: adapter.statusLabel?.(status, context) ?? humanizeIdentifier(status),
|
|
11
|
+
reviewItemLink: adapter.linkForReviewItem?.(item, context),
|
|
12
|
+
traceRefs: traceRefsForReviewItem(item, adapter),
|
|
13
|
+
candidates: item.spec.candidates.map((candidate) => buildReviewCandidatePresentation(item, candidate, adapter, targetLabel)),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function buildReviewCandidatePresentation(item, candidate, adapter = {}, targetLabel = adapter.labelForTarget?.(item.spec.target, { item }) ?? humanizeIdentifier(item.spec.target)) {
|
|
17
|
+
const context = { item, candidate };
|
|
18
|
+
const sourceRef = candidate.source.sourceRef;
|
|
19
|
+
const sourceLink = adapter.linkForSource?.(sourceRef, context) ?? urlLink(sourceRef);
|
|
20
|
+
return {
|
|
21
|
+
candidate,
|
|
22
|
+
roleLabel: adapter.labelForCandidateRole?.(candidate.role, context) ?? defaultCandidateRoleLabel(candidate.role),
|
|
23
|
+
valueLabel: targetLabel,
|
|
24
|
+
valueText: adapter.summarizeValue?.(candidate.value, { ...context, value: candidate.value }) ?? formatValue(candidate.value),
|
|
25
|
+
sourceLabel: "Source",
|
|
26
|
+
sourceText: sourceLink?.label ?? sourceRef,
|
|
27
|
+
sourceLink,
|
|
28
|
+
traceRefs: traceRefsForCandidate(item, candidate, adapter),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function buildReviewResultPresentation(result, item, adapter = {}) {
|
|
32
|
+
const target = item?.spec.target ?? result.reviewItemName;
|
|
33
|
+
const itemContext = item ? { item } : undefined;
|
|
34
|
+
const targetLabel = item && itemContext
|
|
35
|
+
? adapter.labelForTarget?.(target, itemContext) ?? humanizeIdentifier(target)
|
|
36
|
+
: humanizeIdentifier(target);
|
|
37
|
+
const selectedCandidate = item?.spec.candidates.find((candidate) => candidate.role === result.selectedCandidateRole || candidate.id === result.selectedCandidateId);
|
|
38
|
+
return {
|
|
39
|
+
result,
|
|
40
|
+
item,
|
|
41
|
+
target,
|
|
42
|
+
targetLabel,
|
|
43
|
+
decisionLabel: humanizeIdentifier(result.decision),
|
|
44
|
+
selectedValueText: selectedCandidate && item
|
|
45
|
+
? buildReviewCandidatePresentation(item, selectedCandidate, adapter, targetLabel).valueText
|
|
46
|
+
: result.selectedDisplayValue,
|
|
47
|
+
applyMeaning: result.selectedCandidateRole === "proposed"
|
|
48
|
+
? "Saved decision applies proposed value"
|
|
49
|
+
: "Saved decision keeps current value",
|
|
50
|
+
reviewItemLink: item && itemContext ? adapter.linkForReviewItem?.(item, itemContext) : undefined,
|
|
51
|
+
traceRefs: item
|
|
52
|
+
? traceRefsForResult(item, result, selectedCandidate, adapter)
|
|
53
|
+
: [{ label: "Survey ReviewItem", value: result.reviewItemName, kind: "review-item" }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function humanizeIdentifier(value) {
|
|
57
|
+
return value
|
|
58
|
+
.replace(/[_-]+/g, " ")
|
|
59
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
60
|
+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
61
|
+
}
|
|
62
|
+
function traceRefsForReviewItem(item, adapter) {
|
|
63
|
+
const refs = [
|
|
64
|
+
{ label: "Survey ReviewItem", value: item.metadata.name, kind: "review-item" },
|
|
65
|
+
];
|
|
66
|
+
const candidateSetId = item.spec.projection?.candidateSetId;
|
|
67
|
+
if (candidateSetId) {
|
|
68
|
+
refs.push({ label: "Candidate set", value: candidateSetId, kind: "candidate-set" });
|
|
69
|
+
}
|
|
70
|
+
return withTraceLinks(refs, { item }, adapter);
|
|
71
|
+
}
|
|
72
|
+
function traceRefsForCandidate(item, candidate, adapter) {
|
|
73
|
+
const refs = [
|
|
74
|
+
{ label: "Candidate ID", value: candidate.id, kind: "candidate" },
|
|
75
|
+
{
|
|
76
|
+
label: "Claim ID",
|
|
77
|
+
value: candidate.claimTarget.claimId ?? candidate.claimTarget.fieldOrBehavior,
|
|
78
|
+
kind: "claim",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
label: "Source ID",
|
|
82
|
+
value: candidate.source.sourceId ?? candidate.source.sourceRef,
|
|
83
|
+
kind: "source",
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
const locator = candidate.locator?.locator ?? candidate.locator?.scheme;
|
|
87
|
+
if (locator) {
|
|
88
|
+
refs.push({ label: "Locator", value: locator, kind: "locator" });
|
|
89
|
+
}
|
|
90
|
+
return withTraceLinks(refs, { item, candidate }, adapter);
|
|
91
|
+
}
|
|
92
|
+
function traceRefsForResult(item, result, selectedCandidate, adapter) {
|
|
93
|
+
return withTraceLinks([
|
|
94
|
+
{ label: "Survey ReviewItem", value: result.reviewItemName, kind: "review-item" },
|
|
95
|
+
{ label: "Selected candidate", value: result.selectedCandidateId, kind: "candidate" },
|
|
96
|
+
{
|
|
97
|
+
label: "Selected claim",
|
|
98
|
+
value: selectedCandidate?.claimTarget.claimId ?? "not provided",
|
|
99
|
+
kind: "claim",
|
|
100
|
+
},
|
|
101
|
+
], { item, candidate: selectedCandidate }, adapter);
|
|
102
|
+
}
|
|
103
|
+
function withTraceLinks(refs, context, adapter) {
|
|
104
|
+
return refs.map((ref) => ({
|
|
105
|
+
...ref,
|
|
106
|
+
link: ref.link ?? adapter.linkForTraceRef?.(ref, context),
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
function defaultCandidateRoleLabel(role) {
|
|
110
|
+
if (role === "current")
|
|
111
|
+
return "Current value";
|
|
112
|
+
if (role === "proposed")
|
|
113
|
+
return "Proposed value";
|
|
114
|
+
return "Candidate";
|
|
115
|
+
}
|
|
116
|
+
function urlLink(value) {
|
|
117
|
+
if (!/^https?:\/\//.test(value)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
label: displayUrl(value),
|
|
122
|
+
href: value,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function displayUrl(value) {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = new URL(value);
|
|
128
|
+
return `${parsed.hostname}${parsed.pathname === "/" ? "" : parsed.pathname}`;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReviewDecision, type ReviewItem } from "../review-resource.js";
|
|
2
|
+
import { type ReviewPresentationAdapter } from "./review-presentation.js";
|
|
2
3
|
export interface SurfaceProjectionPreview {
|
|
3
4
|
readonly canonicalClaim: PreviewClaim;
|
|
4
5
|
readonly candidateHistory: PreviewCandidateHistory[];
|
|
@@ -51,5 +52,5 @@ export interface PreviewAuthorityTrace {
|
|
|
51
52
|
readonly label: string;
|
|
52
53
|
readonly detail: string;
|
|
53
54
|
}
|
|
54
|
-
export declare function buildSurfaceProjectionPreview(item: ReviewItem, decision: ReviewDecision | undefined): SurfaceProjectionPreview | undefined;
|
|
55
|
+
export declare function buildSurfaceProjectionPreview(item: ReviewItem, decision: ReviewDecision | undefined, presentationAdapter?: ReviewPresentationAdapter): SurfaceProjectionPreview | undefined;
|
|
55
56
|
export declare function formatValue(value: unknown): string;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
export function buildSurfaceProjectionPreview(item, decision) {
|
|
1
|
+
export function buildSurfaceProjectionPreview(item, decision, presentationAdapter = {}) {
|
|
2
2
|
const selectedCandidate = selectedPreviewCandidate(item, decision);
|
|
3
3
|
if (!selectedCandidate || !decision) {
|
|
4
4
|
return undefined;
|
|
5
5
|
}
|
|
6
6
|
const projection = decision.spec.projection ?? selectedCandidate.projection;
|
|
7
7
|
return {
|
|
8
|
-
canonicalClaim: buildPreviewClaim(selectedCandidate, decision, projection),
|
|
9
|
-
candidateHistory: buildCandidateHistory(item, selectedCandidate),
|
|
8
|
+
canonicalClaim: buildPreviewClaim(item, selectedCandidate, decision, projection, presentationAdapter),
|
|
9
|
+
candidateHistory: buildCandidateHistory(item, selectedCandidate, presentationAdapter),
|
|
10
10
|
sourceEvidence: buildSourceEvidence(selectedCandidate),
|
|
11
11
|
reviewEvent: buildReviewEvent(decision, projection),
|
|
12
12
|
integrityPosture: buildIntegrityPosture(item, selectedCandidate, projection),
|
|
@@ -44,20 +44,22 @@ function selectedPreviewCandidate(item, decision) {
|
|
|
44
44
|
}
|
|
45
45
|
return candidate;
|
|
46
46
|
}
|
|
47
|
-
function buildPreviewClaim(candidate, decision, projection) {
|
|
47
|
+
function buildPreviewClaim(item, candidate, decision, projection, presentationAdapter) {
|
|
48
48
|
return {
|
|
49
49
|
candidateId: candidate.id,
|
|
50
50
|
claimId: projection?.claimId ?? candidate.claimTarget.claimId ?? candidate.claimTarget.fieldOrBehavior,
|
|
51
|
-
value:
|
|
51
|
+
value: presentationAdapter.summarizeValue?.(candidate.value, { item, candidate, value: candidate.value })
|
|
52
|
+
?? formatValue(candidate.value),
|
|
52
53
|
status: decision.spec.status,
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
|
-
function buildCandidateHistory(item, selectedCandidate) {
|
|
56
|
+
function buildCandidateHistory(item, selectedCandidate, presentationAdapter) {
|
|
56
57
|
return item.spec.candidates
|
|
57
58
|
.filter((candidate) => candidate.id !== selectedCandidate.id)
|
|
58
59
|
.map((candidate) => ({
|
|
59
60
|
candidateId: candidate.id,
|
|
60
|
-
value:
|
|
61
|
+
value: presentationAdapter.summarizeValue?.(candidate.value, { item, candidate, value: candidate.value })
|
|
62
|
+
?? formatValue(candidate.value),
|
|
61
63
|
historyLabel: "Unselected candidate history",
|
|
62
64
|
}));
|
|
63
65
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ReviewQueueSessionState, type ReviewWorkbenchDecision, type ReviewWorkbenchState } from "./review-queue-session.js";
|
|
2
|
+
import { type ReviewPresentationAdapter } from "./review-presentation.js";
|
|
2
3
|
import { type ReviewCandidate, type ReviewDecision, type ReviewSession, type ReviewSessionEvent } from "../review-resource.js";
|
|
3
4
|
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
|
+
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";
|
|
4
6
|
export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
5
7
|
export declare function buildReviewDecision(state: ReviewWorkbenchState): ReviewDecision | undefined;
|
|
6
8
|
export interface ReviewWorkbenchSessionExport {
|
|
@@ -43,6 +45,7 @@ export interface PersistentReviewSessionEventStoreOptions {
|
|
|
43
45
|
}
|
|
44
46
|
export interface MountReviewWorkbenchOptions {
|
|
45
47
|
readonly eventStore?: ReviewSessionEventStore;
|
|
48
|
+
readonly presentationAdapter?: ReviewPresentationAdapter;
|
|
46
49
|
}
|
|
47
50
|
export interface BrowserReviewWorkbenchConfig {
|
|
48
51
|
readonly startState?: ReviewQueueSessionState | ReviewWorkbenchState;
|
|
@@ -73,7 +76,9 @@ export declare function createLocalStorageReviewSessionEventStore(storage: Pick<
|
|
|
73
76
|
export declare function createPersistentReviewSessionEventStore(options: PersistentReviewSessionEventStoreOptions): ReviewSessionEventStore & {
|
|
74
77
|
events(): readonly ReviewSessionEvent[];
|
|
75
78
|
};
|
|
76
|
-
export declare function renderReviewWorkbenchHtml(state: ReviewWorkbenchState | ReviewQueueSessionState, events?: readonly ReviewSessionEvent[]
|
|
79
|
+
export declare function renderReviewWorkbenchHtml(state: ReviewWorkbenchState | ReviewQueueSessionState, events?: readonly ReviewSessionEvent[], options?: {
|
|
80
|
+
readonly presentationAdapter?: ReviewPresentationAdapter;
|
|
81
|
+
}): string;
|
|
77
82
|
export declare function mountReviewWorkbench(root: HTMLElement, startState?: ReviewQueueSessionState | ReviewWorkbenchState, options?: MountReviewWorkbenchOptions): void;
|
|
78
83
|
export declare function browserReviewWorkbenchStartState(): ReviewQueueSessionState | ReviewWorkbenchState | undefined;
|
|
79
84
|
declare global {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { candidateForDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
2
2
|
import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
|
|
3
|
+
import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "./review-presentation.js";
|
|
3
4
|
import { reviewResourceApiVersion, } from "../review-resource.js";
|
|
4
5
|
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
6
|
+
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
5
7
|
export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
6
8
|
export function buildReviewDecision(state) {
|
|
7
9
|
if (!state.decision) {
|
|
@@ -212,38 +214,38 @@ export function createPersistentReviewSessionEventStore(options) {
|
|
|
212
214
|
},
|
|
213
215
|
};
|
|
214
216
|
}
|
|
215
|
-
export function renderReviewWorkbenchHtml(state, events) {
|
|
217
|
+
export function renderReviewWorkbenchHtml(state, events, options = {}) {
|
|
216
218
|
if ("items" in state) {
|
|
217
|
-
return renderReviewQueueSessionHtml(state, events);
|
|
219
|
+
return renderReviewQueueSessionHtml(state, events, options.presentationAdapter);
|
|
218
220
|
}
|
|
219
221
|
return `
|
|
220
222
|
<section class="workbench-shell" aria-label="Survey review workbench">
|
|
221
|
-
${renderWorkbenchHeader(state)}
|
|
223
|
+
${renderWorkbenchHeader(state, options.presentationAdapter)}
|
|
222
224
|
<div class="content-grid">
|
|
223
|
-
${renderReviewFocus(state)}
|
|
224
|
-
${renderDecisionColumn(state)}
|
|
225
|
-
${renderCandidateComparison(state)}
|
|
225
|
+
${renderReviewFocus(state, options.presentationAdapter)}
|
|
226
|
+
${renderDecisionColumn(state, options.presentationAdapter)}
|
|
227
|
+
${renderCandidateComparison(state, options.presentationAdapter)}
|
|
226
228
|
</div>
|
|
227
229
|
</section>
|
|
228
230
|
`;
|
|
229
231
|
}
|
|
230
|
-
function renderReviewQueueSessionHtml(session, events) {
|
|
232
|
+
function renderReviewQueueSessionHtml(session, events, presentationAdapter) {
|
|
231
233
|
const state = currentReviewWorkbenchState(session);
|
|
232
234
|
const sessionExport = buildReviewWorkbenchSessionExport(session, events);
|
|
233
235
|
return `
|
|
234
236
|
<section class="workbench-shell" aria-label="Survey review workbench">
|
|
235
|
-
${renderWorkbenchHeader(state)}
|
|
236
|
-
${renderActiveReviewStrip(session, state)}
|
|
237
|
+
${renderWorkbenchHeader(state, presentationAdapter)}
|
|
238
|
+
${renderActiveReviewStrip(session, state, presentationAdapter)}
|
|
237
239
|
<div class="queue-layout">
|
|
238
240
|
<aside class="queue-panel" aria-label="Review queue">
|
|
239
|
-
${renderQueueRows(session)}
|
|
241
|
+
${renderQueueRows(session, presentationAdapter)}
|
|
240
242
|
${renderSessionSummary(session)}
|
|
241
243
|
${renderSessionAudit(session, sessionExport)}
|
|
242
244
|
</aside>
|
|
243
245
|
<div class="content-grid">
|
|
244
|
-
${renderReviewFocus(state)}
|
|
245
|
-
${renderDecisionColumn(state)}
|
|
246
|
-
${renderCandidateComparison(state)}
|
|
246
|
+
${renderReviewFocus(state, presentationAdapter)}
|
|
247
|
+
${renderDecisionColumn(state, presentationAdapter)}
|
|
248
|
+
${renderCandidateComparison(state, presentationAdapter)}
|
|
247
249
|
</div>
|
|
248
250
|
</div>
|
|
249
251
|
</section>
|
|
@@ -309,25 +311,26 @@ function renderSessionEventRow(event) {
|
|
|
309
311
|
</li>
|
|
310
312
|
`;
|
|
311
313
|
}
|
|
312
|
-
function renderQueueRows(session) {
|
|
314
|
+
function renderQueueRows(session, presentationAdapter) {
|
|
313
315
|
return `
|
|
314
316
|
<section class="queue-list" data-testid="review-queue">
|
|
315
317
|
<div class="queue-head">
|
|
316
318
|
<h2>Review queue</h2>
|
|
317
319
|
<button class="next-button" type="button" data-testid="next-unresolved">Next unresolved</button>
|
|
318
320
|
</div>
|
|
319
|
-
${session.items.map((item) => renderQueueRow(item, session)).join("")}
|
|
321
|
+
${session.items.map((item) => renderQueueRow(item, session, presentationAdapter)).join("")}
|
|
320
322
|
</section>
|
|
321
323
|
`;
|
|
322
324
|
}
|
|
323
|
-
function renderQueueRow(item, session) {
|
|
325
|
+
function renderQueueRow(item, session, presentationAdapter) {
|
|
324
326
|
const status = deriveQueueRowStatus(item, session);
|
|
325
327
|
const isActive = item.metadata.name === session.activeItemName;
|
|
328
|
+
const presentation = buildReviewItemPresentation(item, presentationAdapter);
|
|
326
329
|
return `
|
|
327
330
|
<button class="queue-row${isActive ? " is-active" : ""}" type="button" data-item-name="${escapeHtml(item.metadata.name)}" data-testid="queue-row" data-queue-status="${status}">
|
|
328
331
|
<span class="queue-row-main">
|
|
329
|
-
<span class="queue-row-title">${escapeHtml(
|
|
330
|
-
<span class="queue-row-target">${escapeHtml(item.
|
|
332
|
+
<span class="queue-row-title">${escapeHtml(presentation.targetLabel)}</span>
|
|
333
|
+
<span class="queue-row-target">${escapeHtml(item.metadata.name)}</span>
|
|
331
334
|
</span>
|
|
332
335
|
<span class="state-label">${escapeHtml(status)}</span>
|
|
333
336
|
</button>
|
|
@@ -348,16 +351,17 @@ function renderSessionSummary(session) {
|
|
|
348
351
|
</section>
|
|
349
352
|
`;
|
|
350
353
|
}
|
|
351
|
-
function renderActiveReviewStrip(session, state) {
|
|
354
|
+
function renderActiveReviewStrip(session, state, presentationAdapter) {
|
|
352
355
|
const activeIndex = session.items.findIndex((item) => item.metadata.name === session.activeItemName);
|
|
353
356
|
const position = activeIndex >= 0 ? activeIndex + 1 : 1;
|
|
354
357
|
const status = deriveQueueRowStatus(state.item, session);
|
|
358
|
+
const presentation = buildReviewItemPresentation(state.item, presentationAdapter);
|
|
355
359
|
return `
|
|
356
360
|
<section class="active-review-strip" data-testid="active-review-strip" aria-label="Active review item">
|
|
357
361
|
<div class="active-review-copy">
|
|
358
362
|
<span class="field-label">Review ${position} of ${session.items.length}</span>
|
|
359
|
-
<strong>${escapeHtml(
|
|
360
|
-
<span>${escapeHtml(state.item.
|
|
363
|
+
<strong>${escapeHtml(presentation.targetLabel)}</strong>
|
|
364
|
+
<span>${escapeHtml(state.item.metadata.name)}</span>
|
|
361
365
|
</div>
|
|
362
366
|
<div class="active-review-actions">
|
|
363
367
|
<span class="state-label">${escapeHtml(status)}</span>
|
|
@@ -366,19 +370,22 @@ function renderActiveReviewStrip(session, state) {
|
|
|
366
370
|
</section>
|
|
367
371
|
`;
|
|
368
372
|
}
|
|
369
|
-
function renderWorkbenchHeader(state) {
|
|
373
|
+
function renderWorkbenchHeader(state, presentationAdapter) {
|
|
370
374
|
const current = candidateByRole(state.item, "current");
|
|
371
375
|
const proposed = candidateByRole(state.item, "proposed");
|
|
376
|
+
const presentation = buildReviewItemPresentation(state.item, presentationAdapter);
|
|
377
|
+
const currentPresentation = buildReviewCandidatePresentation(state.item, current, presentationAdapter, presentation.targetLabel);
|
|
378
|
+
const proposedPresentation = buildReviewCandidatePresentation(state.item, proposed, presentationAdapter, presentation.targetLabel);
|
|
372
379
|
return `
|
|
373
380
|
<header class="topbar">
|
|
374
381
|
<div>
|
|
375
382
|
<p class="eyebrow">${escapeHtml(String(state.item.metadata.producer?.displayName ?? "Survey"))}</p>
|
|
376
383
|
<h1>Review candidate update</h1>
|
|
377
|
-
<p class="review-question">For <strong>${escapeHtml(
|
|
384
|
+
<p class="review-question">For <strong>${escapeHtml(presentation.targetLabel)}</strong>, decide whether <strong>${escapeHtml(proposedPresentation.valueText)}</strong> should replace <strong>${escapeHtml(currentPresentation.valueText)}</strong>.</p>
|
|
378
385
|
</div>
|
|
379
386
|
<dl class="meta-grid">
|
|
380
387
|
${metaItem("Item", state.item.metadata.name)}
|
|
381
|
-
${metaItem("Status",
|
|
388
|
+
${metaItem("Status", presentation.statusLabel)}
|
|
382
389
|
${metaItem("Selected", state.item.spec.selectedCandidateId ?? "none")}
|
|
383
390
|
${metaItem("Candidate count", String(state.item.status?.observedCandidateCount ?? state.item.spec.candidates.length))}
|
|
384
391
|
</dl>
|
|
@@ -393,53 +400,55 @@ function renderReviewMain(state) {
|
|
|
393
400
|
</div>
|
|
394
401
|
`;
|
|
395
402
|
}
|
|
396
|
-
function renderReviewFocus(state) {
|
|
403
|
+
function renderReviewFocus(state, presentationAdapter) {
|
|
397
404
|
const current = candidateByRole(state.item, "current");
|
|
398
405
|
const proposed = candidateByRole(state.item, "proposed");
|
|
399
406
|
const proposedConfidence = proposed.extraction.confidence ?? proposed.confidence;
|
|
400
407
|
const currentConfidence = current.extraction.confidence ?? current.confidence;
|
|
408
|
+
const presentation = buildReviewItemPresentation(state.item, presentationAdapter);
|
|
401
409
|
return `
|
|
402
410
|
<section class="review-focus" data-testid="review-focus" aria-label="Active review focus">
|
|
403
411
|
<div class="focus-head">
|
|
404
412
|
<span class="field-label">Active review</span>
|
|
405
|
-
<span class="state-label">${escapeHtml(
|
|
413
|
+
<span class="state-label">${escapeHtml(presentation.statusLabel)}</span>
|
|
406
414
|
</div>
|
|
407
415
|
<div class="focus-values">
|
|
408
|
-
${focusValue(
|
|
409
|
-
${focusValue(
|
|
416
|
+
${focusValue(state.item, current, currentConfidence, "current", presentationAdapter, presentation.targetLabel)}
|
|
417
|
+
${focusValue(state.item, proposed, proposedConfidence, "proposed", presentationAdapter, presentation.targetLabel)}
|
|
410
418
|
</div>
|
|
411
419
|
<dl class="focus-evidence">
|
|
412
|
-
${fieldItem("Target",
|
|
420
|
+
${fieldItem("Target", presentation.targetLabel)}
|
|
413
421
|
${fieldItem("Proposed source", proposed.source.sourceRef)}
|
|
414
422
|
${fieldItem("Proposed excerpt", proposed.locator?.excerpt ?? "none", "excerpt")}
|
|
415
423
|
</dl>
|
|
416
424
|
</section>
|
|
417
425
|
`;
|
|
418
426
|
}
|
|
419
|
-
function focusValue(
|
|
427
|
+
function focusValue(item, candidate, confidence, tone, presentationAdapter, targetLabel) {
|
|
428
|
+
const presentation = buildReviewCandidatePresentation(item, candidate, presentationAdapter, targetLabel);
|
|
420
429
|
return `
|
|
421
430
|
<div class="focus-value is-${tone}">
|
|
422
|
-
<span class="field-label">${escapeHtml(
|
|
423
|
-
<strong>${escapeHtml(
|
|
431
|
+
<span class="field-label">${escapeHtml(presentation.roleLabel)}</span>
|
|
432
|
+
<strong>${escapeHtml(presentation.valueText)}</strong>
|
|
424
433
|
<span>${escapeHtml(confidence === undefined ? "confidence unknown" : formatConfidence(confidence))}</span>
|
|
425
434
|
</div>
|
|
426
435
|
`;
|
|
427
436
|
}
|
|
428
|
-
function renderCandidateComparison(state) {
|
|
437
|
+
function renderCandidateComparison(state, presentationAdapter) {
|
|
429
438
|
const current = candidateByRole(state.item, "current");
|
|
430
439
|
const proposed = candidateByRole(state.item, "proposed");
|
|
431
440
|
return `
|
|
432
441
|
<section class="candidate-grid" aria-label="Candidate comparison">
|
|
433
|
-
${renderCandidateCard(current, state)}
|
|
434
|
-
${renderCandidateCard(proposed, state)}
|
|
442
|
+
${renderCandidateCard(current, state, presentationAdapter)}
|
|
443
|
+
${renderCandidateCard(proposed, state, presentationAdapter)}
|
|
435
444
|
</section>
|
|
436
445
|
`;
|
|
437
446
|
}
|
|
438
|
-
function renderDecisionColumn(state) {
|
|
447
|
+
function renderDecisionColumn(state, presentationAdapter) {
|
|
439
448
|
return `
|
|
440
449
|
<aside class="decision-column" aria-label="Review decision">
|
|
441
450
|
${renderDecisionControls(state)}
|
|
442
|
-
${renderSurfacePreview(state)}
|
|
451
|
+
${renderSurfacePreview(state, presentationAdapter)}
|
|
443
452
|
${renderDecisionPayload(state)}
|
|
444
453
|
</aside>
|
|
445
454
|
`;
|
|
@@ -490,8 +499,8 @@ function renderDecisionPayload(state) {
|
|
|
490
499
|
</details>
|
|
491
500
|
`;
|
|
492
501
|
}
|
|
493
|
-
function renderSurfacePreview(state) {
|
|
494
|
-
const preview = buildSurfaceProjectionPreview(state.item, buildReviewDecision(state));
|
|
502
|
+
function renderSurfacePreview(state, presentationAdapter) {
|
|
503
|
+
const preview = buildSurfaceProjectionPreview(state.item, buildReviewDecision(state), presentationAdapter);
|
|
495
504
|
return preview ? renderPopulatedSurfacePreview(preview) : renderPendingSurfacePreview();
|
|
496
505
|
}
|
|
497
506
|
function renderPendingSurfacePreview() {
|
|
@@ -668,7 +677,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
668
677
|
appendEvent("item-selected");
|
|
669
678
|
persistEvents();
|
|
670
679
|
},
|
|
671
|
-
renderCurrentState: () => renderCurrentState(root, session, controller),
|
|
680
|
+
renderCurrentState: () => renderCurrentState(root, session, controller, options.presentationAdapter),
|
|
672
681
|
selectDecision,
|
|
673
682
|
selectQueueItem: (itemName) => {
|
|
674
683
|
applySessionUpdate((current) => ({ ...current, activeItemName: itemName }));
|
|
@@ -709,9 +718,9 @@ function reviewSessionStorageKey(session, keyPrefix) {
|
|
|
709
718
|
const itemNames = buildReviewSessionResource(session).spec.reviewItemNames.join(",");
|
|
710
719
|
return `${keyPrefix}:${itemNames}`;
|
|
711
720
|
}
|
|
712
|
-
function renderCurrentState(root, session, controller) {
|
|
713
|
-
root.innerHTML = renderReviewWorkbenchHtml(session, controller.currentSessionExport().events);
|
|
714
|
-
bindReviewerNote(root, controller.updateReviewerNote, controller.currentState, controller.currentSession, controller.currentSessionExport, controller.renderCurrentState);
|
|
721
|
+
function renderCurrentState(root, session, controller, presentationAdapter) {
|
|
722
|
+
root.innerHTML = renderReviewWorkbenchHtml(session, controller.currentSessionExport().events, { presentationAdapter });
|
|
723
|
+
bindReviewerNote(root, controller.updateReviewerNote, controller.currentState, controller.currentSession, controller.currentSessionExport, controller.renderCurrentState, presentationAdapter);
|
|
715
724
|
bindDecisionButtons(root, (decision) => {
|
|
716
725
|
controller.selectDecision(decision);
|
|
717
726
|
controller.renderCurrentState();
|
|
@@ -738,10 +747,10 @@ function queueSessionFromStartState(startState) {
|
|
|
738
747
|
actorId: startState.actorId,
|
|
739
748
|
};
|
|
740
749
|
}
|
|
741
|
-
function bindReviewerNote(root, updateReviewerNote, currentState, currentSession, currentSessionExport, renderCurrentState) {
|
|
750
|
+
function bindReviewerNote(root, updateReviewerNote, currentState, currentSession, currentSessionExport, renderCurrentState, presentationAdapter) {
|
|
742
751
|
root.querySelector("[data-testid='reviewer-note']")?.addEventListener("input", (event) => {
|
|
743
752
|
updateReviewerNote(event.target.value);
|
|
744
|
-
refreshDecisionOutputs(root, currentState(), renderCurrentState);
|
|
753
|
+
refreshDecisionOutputs(root, currentState(), renderCurrentState, presentationAdapter);
|
|
745
754
|
refreshSessionAudit(root, currentSession(), currentSessionExport(), renderCurrentState);
|
|
746
755
|
});
|
|
747
756
|
}
|
|
@@ -779,7 +788,7 @@ function scrollActiveReviewIntoView(root) {
|
|
|
779
788
|
behavior: "smooth",
|
|
780
789
|
});
|
|
781
790
|
}
|
|
782
|
-
function refreshDecisionOutputs(root, state, renderCurrentState) {
|
|
791
|
+
function refreshDecisionOutputs(root, state, renderCurrentState, presentationAdapter) {
|
|
783
792
|
const payload = root.querySelector("[data-testid='decision-payload']");
|
|
784
793
|
if (payload) {
|
|
785
794
|
payload.textContent = JSON.stringify(buildReviewDecision(state) ?? null, null, 2);
|
|
@@ -793,7 +802,7 @@ function refreshDecisionOutputs(root, state, renderCurrentState) {
|
|
|
793
802
|
return;
|
|
794
803
|
}
|
|
795
804
|
const wrapper = document.createElement("div");
|
|
796
|
-
wrapper.innerHTML = renderSurfacePreview(state);
|
|
805
|
+
wrapper.innerHTML = renderSurfacePreview(state, presentationAdapter);
|
|
797
806
|
preview.replaceWith(wrapper.firstElementChild);
|
|
798
807
|
}
|
|
799
808
|
function refreshSessionAudit(root, session, sessionExport, renderCurrentState) {
|
|
@@ -813,28 +822,30 @@ function producerFeedbackTags(item) {
|
|
|
813
822
|
const tags = item.spec.producerPolicy?.feedbackTags;
|
|
814
823
|
return Array.isArray(tags) ? tags.map(formatValue) : [];
|
|
815
824
|
}
|
|
816
|
-
function renderCandidateCard(candidate, state) {
|
|
825
|
+
function renderCandidateCard(candidate, state, presentationAdapter) {
|
|
817
826
|
const selectedRole = selectedCandidateRole(state);
|
|
818
827
|
const candidateState = state.decision
|
|
819
828
|
? candidate.role === selectedRole ? "selected" : "unselected"
|
|
820
829
|
: "pending";
|
|
821
830
|
const cssState = candidateState === "selected" ? " is-selected" : candidateState === "unselected" ? " is-unselected" : "";
|
|
822
831
|
const confidence = candidate.extraction.confidence ?? candidate.confidence;
|
|
832
|
+
const itemPresentation = buildReviewItemPresentation(state.item, presentationAdapter);
|
|
833
|
+
const presentation = buildReviewCandidatePresentation(state.item, candidate, presentationAdapter, itemPresentation.targetLabel);
|
|
823
834
|
return `
|
|
824
835
|
<article class="candidate-card${cssState}" data-testid="candidate-${escapeHtml(candidate.role ?? candidate.id)}" data-outcome="${candidateState}">
|
|
825
836
|
<div class="card-head">
|
|
826
837
|
<div>
|
|
827
838
|
<p class="eyebrow">${escapeHtml(candidate.role === "proposed" ? "incoming value" : candidate.role === "current" ? "existing value" : "candidate")}</p>
|
|
828
|
-
<h2 class="role">${escapeHtml(
|
|
839
|
+
<h2 class="role">${escapeHtml(presentation.roleLabel)}</h2>
|
|
829
840
|
</div>
|
|
830
841
|
<span class="state-label">${escapeHtml(candidateState)}</span>
|
|
831
842
|
</div>
|
|
832
843
|
<div class="candidate-value">
|
|
833
|
-
<span class="field-label"
|
|
834
|
-
<p class="field-value">${escapeHtml(
|
|
844
|
+
<span class="field-label">${escapeHtml(presentation.valueLabel)}</span>
|
|
845
|
+
<p class="field-value">${escapeHtml(presentation.valueText)}</p>
|
|
835
846
|
</div>
|
|
836
847
|
<dl class="field-stack">
|
|
837
|
-
${fieldItem("Source URL",
|
|
848
|
+
${fieldItem("Source URL", presentation.sourceText)}
|
|
838
849
|
${fieldItem("Locator", candidate.locator?.locator ?? candidate.locator?.scheme ?? "none")}
|
|
839
850
|
${fieldItem("Excerpt", candidate.locator?.excerpt ?? "none", "excerpt")}
|
|
840
851
|
${fieldItem("Extraction confidence", confidence === undefined ? "unknown" : formatConfidence(confidence))}
|
|
@@ -843,9 +854,7 @@ function renderCandidateCard(candidate, state) {
|
|
|
843
854
|
<details class="reference-details">
|
|
844
855
|
<summary>IDs and trace links</summary>
|
|
845
856
|
<dl class="field-stack compact">
|
|
846
|
-
${fieldItem(
|
|
847
|
-
${fieldItem("Source ID", candidate.source.sourceId ?? candidate.source.sourceRef)}
|
|
848
|
-
${fieldItem("Claim ID", candidate.claimTarget.claimId ?? candidate.claimTarget.fieldOrBehavior)}
|
|
857
|
+
${presentation.traceRefs.map((ref) => fieldItem(ref.label, ref.value)).join("")}
|
|
849
858
|
</dl>
|
|
850
859
|
</details>
|
|
851
860
|
</article>
|
|
@@ -877,10 +886,6 @@ function fieldItem(label, value, extraClass = "") {
|
|
|
877
886
|
function formatConfidence(value) {
|
|
878
887
|
return `${Math.round(value * 100)}% (${value.toFixed(2)})`;
|
|
879
888
|
}
|
|
880
|
-
function titleCase(value) {
|
|
881
|
-
const displayValue = formatValue(value);
|
|
882
|
-
return displayValue.slice(0, 1).toUpperCase() + displayValue.slice(1);
|
|
883
|
-
}
|
|
884
889
|
function escapeHtml(value) {
|
|
885
890
|
return formatValue(value)
|
|
886
891
|
.replaceAll("&", "&")
|
package/package.json
CHANGED