@kontourai/survey 0.4.13 → 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.
@@ -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: formatValue(candidate.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: formatValue(candidate.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
  }
@@ -771,7 +771,7 @@
771
771
 
772
772
  .survey-workbench-embed .candidate-card{
773
773
  display: grid;
774
- grid-template-rows: auto auto minmax(0, 1fr);
774
+ grid-template-rows: auto auto minmax(0, 1fr) auto;
775
775
  gap: 11px;
776
776
  min-height: auto;
777
777
  padding: 14px;
@@ -817,7 +817,7 @@
817
817
  color: var(--paper-faint);
818
818
  font-size: 10px;
819
819
  letter-spacing: 0.1em;
820
- text-transform: none;
820
+ text-transform: uppercase;
821
821
  overflow-wrap: anywhere;
822
822
  word-break: break-word;
823
823
  }
@@ -1110,6 +1110,44 @@
1110
1110
  color: var(--paper-faint);
1111
1111
  }
1112
1112
 
1113
+ .survey-workbench-embed .reference-details{
1114
+ margin-top: 10px;
1115
+ border-top: 1px solid var(--line);
1116
+ padding-top: 8px;
1117
+ }
1118
+
1119
+ .survey-workbench-embed .reference-details summary{
1120
+ cursor: pointer;
1121
+ color: var(--paper-faint);
1122
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
1123
+ font-size: 10px;
1124
+ font-weight: 600;
1125
+ letter-spacing: 0.08em;
1126
+ text-transform: uppercase;
1127
+ }
1128
+
1129
+ .survey-workbench-embed .reference-details summary::-webkit-details-marker{
1130
+ display: none;
1131
+ }
1132
+
1133
+ .survey-workbench-embed .reference-details summary::before{
1134
+ content: "› ";
1135
+ color: var(--paper-faint);
1136
+ }
1137
+
1138
+ .survey-workbench-embed .reference-details[open] summary{
1139
+ color: var(--accent);
1140
+ }
1141
+
1142
+ .survey-workbench-embed .reference-details[open] summary::before{
1143
+ content: "⌄ ";
1144
+ color: var(--accent);
1145
+ }
1146
+
1147
+ .survey-workbench-embed .reference-details .field-stack{
1148
+ margin-top: 8px;
1149
+ }
1150
+
1113
1151
  /* ---- Decision payload: terminal readout, intentionally demoted -------- */
1114
1152
 
1115
1153
  .survey-workbench-embed .payload-panel{
@@ -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[]): string;
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(item.metadata.name)}</span>
330
- <span class="queue-row-target">${escapeHtml(item.spec.target)}</span>
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(state.item.metadata.name)}</strong>
360
- <span>${escapeHtml(state.item.spec.target)}</span>
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(state.item.spec.target)}</strong>, decide whether <strong>${escapeHtml(formatValue(proposed.value))}</strong> should replace <strong>${escapeHtml(formatValue(current.value))}</strong>.</p>
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", state.item.spec.candidateSetStatus ?? "unresolved")}
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(state.item.spec.candidateSetStatus ?? "unresolved")}</span>
413
+ <span class="state-label">${escapeHtml(presentation.statusLabel)}</span>
406
414
  </div>
407
415
  <div class="focus-values">
408
- ${focusValue("Current", current, currentConfidence, "current")}
409
- ${focusValue("Proposed", proposed, proposedConfidence, "proposed")}
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", state.item.spec.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(label, candidate, confidence, tone) {
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(label)}</span>
423
- <strong>${escapeHtml(formatValue(candidate.value))}</strong>
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() {
@@ -530,11 +539,12 @@ function renderSurfacePreviewSections(preview) {
530
539
  ].join("");
531
540
  }
532
541
  function renderCanonicalClaim(preview) {
533
- return renderPreviewSection("Selected canonical claim", "surface-canonical-claim", [
534
- ["Candidate", preview.canonicalClaim.candidateId],
535
- ["Claim", preview.canonicalClaim.claimId],
542
+ return renderPreviewSection("Selected claim", "surface-canonical-claim", [
536
543
  ["Value", preview.canonicalClaim.value],
537
544
  ["Review status", preview.canonicalClaim.status],
545
+ ], undefined, [
546
+ ["Candidate ID", preview.canonicalClaim.candidateId],
547
+ ["Claim ID", preview.canonicalClaim.claimId],
538
548
  ]);
539
549
  }
540
550
  function renderReviewEvent(preview) {
@@ -548,10 +558,11 @@ function renderReviewEvent(preview) {
548
558
  }
549
559
  function renderIntegrityPosture(preview) {
550
560
  return renderPreviewSection("Integrity posture", "surface-integrity-posture", [
551
- ["Candidate set", preview.integrityPosture.candidateSetId],
552
- ["Raw source", preview.integrityPosture.rawSourceId],
553
- ["Extraction", preview.integrityPosture.extractionId],
554
561
  ["Checksum", preview.integrityPosture.checksum],
562
+ ], undefined, [
563
+ ["Candidate set ID", preview.integrityPosture.candidateSetId],
564
+ ["Raw source ID", preview.integrityPosture.rawSourceId],
565
+ ["Extraction ID", preview.integrityPosture.extractionId],
555
566
  ]);
556
567
  }
557
568
  function renderAuthorityTrace(preview) {
@@ -565,35 +576,50 @@ function renderCandidateHistory(preview) {
565
576
  ? [["History", "No unselected candidates."]]
566
577
  : preview.candidateHistory.flatMap((candidate) => [
567
578
  ["History", candidate.historyLabel],
568
- ["Candidate", candidate.candidateId],
569
579
  ["Value", candidate.value],
570
580
  ]);
571
- return renderPreviewSection("Unselected candidate history", "surface-candidate-history", rows);
581
+ const references = preview.candidateHistory.flatMap((candidate) => [
582
+ ["Candidate ID", candidate.candidateId],
583
+ ]);
584
+ return renderPreviewSection("Unselected candidate history", "surface-candidate-history", rows, undefined, references);
572
585
  }
573
586
  function renderSourceEvidence(preview) {
574
587
  const rows = [
575
588
  ["Source URL", preview.sourceEvidence.sourceRef],
576
- ["Source ref", preview.sourceEvidence.sourceId],
577
589
  ["Excerpt", preview.sourceEvidence.excerpt],
578
- ["Extraction", preview.sourceEvidence.extractionId],
579
590
  ["Extractor", preview.sourceEvidence.extractor],
580
591
  ["Observed", preview.sourceEvidence.observedAt],
581
592
  ];
593
+ const references = [
594
+ ["Source ID", preview.sourceEvidence.sourceId],
595
+ ["Extraction ID", preview.sourceEvidence.extractionId],
596
+ ];
582
597
  if (preview.sourceEvidence.sourceAuthority) {
583
598
  rows.push(["Source authority class", preview.sourceEvidence.sourceAuthority.authorityClass], ["Declared by", preview.sourceEvidence.sourceAuthority.declaredBy], ["Authority scope", preview.sourceEvidence.sourceAuthority.scope]);
584
599
  }
585
600
  return renderPreviewSection("Source evidence", "surface-source-evidence", rows);
586
601
  }
587
- function renderPreviewSection(title, testId, rows, extraClass = "") {
602
+ function renderPreviewSection(title, testId, rows, extraClass = "", references = []) {
588
603
  return `
589
604
  <section class="preview-section${extraClass}" data-testid="${testId}">
590
605
  <h3>${escapeHtml(title)}</h3>
591
606
  <dl class="field-stack compact">
592
607
  ${rows.map(([label, value]) => fieldItem(label, value)).join("")}
593
608
  </dl>
609
+ ${references.length > 0 ? renderReferenceDetails(references) : ""}
594
610
  </section>
595
611
  `;
596
612
  }
613
+ function renderReferenceDetails(references) {
614
+ return `
615
+ <details class="reference-details">
616
+ <summary>IDs and trace links</summary>
617
+ <dl class="field-stack compact">
618
+ ${references.map(([label, value]) => fieldItem(label, value)).join("")}
619
+ </dl>
620
+ </details>
621
+ `;
622
+ }
597
623
  export function mountReviewWorkbench(root, startState = initialReviewQueueSessionState(), options = {}) {
598
624
  const controller = createReviewWorkbenchController(root, startState, options);
599
625
  controller.renderCurrentState();
@@ -651,7 +677,7 @@ function createReviewWorkbenchController(root, startState, options) {
651
677
  appendEvent("item-selected");
652
678
  persistEvents();
653
679
  },
654
- renderCurrentState: () => renderCurrentState(root, session, controller),
680
+ renderCurrentState: () => renderCurrentState(root, session, controller, options.presentationAdapter),
655
681
  selectDecision,
656
682
  selectQueueItem: (itemName) => {
657
683
  applySessionUpdate((current) => ({ ...current, activeItemName: itemName }));
@@ -692,9 +718,9 @@ function reviewSessionStorageKey(session, keyPrefix) {
692
718
  const itemNames = buildReviewSessionResource(session).spec.reviewItemNames.join(",");
693
719
  return `${keyPrefix}:${itemNames}`;
694
720
  }
695
- function renderCurrentState(root, session, controller) {
696
- root.innerHTML = renderReviewWorkbenchHtml(session, controller.currentSessionExport().events);
697
- 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);
698
724
  bindDecisionButtons(root, (decision) => {
699
725
  controller.selectDecision(decision);
700
726
  controller.renderCurrentState();
@@ -721,10 +747,10 @@ function queueSessionFromStartState(startState) {
721
747
  actorId: startState.actorId,
722
748
  };
723
749
  }
724
- function bindReviewerNote(root, updateReviewerNote, currentState, currentSession, currentSessionExport, renderCurrentState) {
750
+ function bindReviewerNote(root, updateReviewerNote, currentState, currentSession, currentSessionExport, renderCurrentState, presentationAdapter) {
725
751
  root.querySelector("[data-testid='reviewer-note']")?.addEventListener("input", (event) => {
726
752
  updateReviewerNote(event.target.value);
727
- refreshDecisionOutputs(root, currentState(), renderCurrentState);
753
+ refreshDecisionOutputs(root, currentState(), renderCurrentState, presentationAdapter);
728
754
  refreshSessionAudit(root, currentSession(), currentSessionExport(), renderCurrentState);
729
755
  });
730
756
  }
@@ -762,7 +788,7 @@ function scrollActiveReviewIntoView(root) {
762
788
  behavior: "smooth",
763
789
  });
764
790
  }
765
- function refreshDecisionOutputs(root, state, renderCurrentState) {
791
+ function refreshDecisionOutputs(root, state, renderCurrentState, presentationAdapter) {
766
792
  const payload = root.querySelector("[data-testid='decision-payload']");
767
793
  if (payload) {
768
794
  payload.textContent = JSON.stringify(buildReviewDecision(state) ?? null, null, 2);
@@ -776,7 +802,7 @@ function refreshDecisionOutputs(root, state, renderCurrentState) {
776
802
  return;
777
803
  }
778
804
  const wrapper = document.createElement("div");
779
- wrapper.innerHTML = renderSurfacePreview(state);
805
+ wrapper.innerHTML = renderSurfacePreview(state, presentationAdapter);
780
806
  preview.replaceWith(wrapper.firstElementChild);
781
807
  }
782
808
  function refreshSessionAudit(root, session, sessionExport, renderCurrentState) {
@@ -796,35 +822,41 @@ function producerFeedbackTags(item) {
796
822
  const tags = item.spec.producerPolicy?.feedbackTags;
797
823
  return Array.isArray(tags) ? tags.map(formatValue) : [];
798
824
  }
799
- function renderCandidateCard(candidate, state) {
825
+ function renderCandidateCard(candidate, state, presentationAdapter) {
800
826
  const selectedRole = selectedCandidateRole(state);
801
827
  const candidateState = state.decision
802
828
  ? candidate.role === selectedRole ? "selected" : "unselected"
803
829
  : "pending";
804
830
  const cssState = candidateState === "selected" ? " is-selected" : candidateState === "unselected" ? " is-unselected" : "";
805
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);
806
834
  return `
807
835
  <article class="candidate-card${cssState}" data-testid="candidate-${escapeHtml(candidate.role ?? candidate.id)}" data-outcome="${candidateState}">
808
836
  <div class="card-head">
809
837
  <div>
810
- <p class="eyebrow">${escapeHtml(candidate.id)}</p>
811
- <h2 class="role">${escapeHtml(titleCase(candidate.role ?? "candidate"))}</h2>
838
+ <p class="eyebrow">${escapeHtml(candidate.role === "proposed" ? "incoming value" : candidate.role === "current" ? "existing value" : "candidate")}</p>
839
+ <h2 class="role">${escapeHtml(presentation.roleLabel)}</h2>
812
840
  </div>
813
841
  <span class="state-label">${escapeHtml(candidateState)}</span>
814
842
  </div>
815
843
  <div class="candidate-value">
816
- <span class="field-label">Value</span>
817
- <p class="field-value">${escapeHtml(formatValue(candidate.value))}</p>
844
+ <span class="field-label">${escapeHtml(presentation.valueLabel)}</span>
845
+ <p class="field-value">${escapeHtml(presentation.valueText)}</p>
818
846
  </div>
819
847
  <dl class="field-stack">
820
- ${fieldItem("Source URL", candidate.source.sourceRef)}
821
- ${fieldItem("Source ref", candidate.source.sourceId ?? candidate.source.sourceRef)}
848
+ ${fieldItem("Source URL", presentation.sourceText)}
822
849
  ${fieldItem("Locator", candidate.locator?.locator ?? candidate.locator?.scheme ?? "none")}
823
850
  ${fieldItem("Excerpt", candidate.locator?.excerpt ?? "none", "excerpt")}
824
851
  ${fieldItem("Extraction confidence", confidence === undefined ? "unknown" : formatConfidence(confidence))}
825
852
  ${fieldItem("Extractor", candidate.extraction.extractor ?? "unknown")}
826
- ${fieldItem("Claim", candidate.claimTarget.claimId ?? candidate.claimTarget.fieldOrBehavior)}
827
853
  </dl>
854
+ <details class="reference-details">
855
+ <summary>IDs and trace links</summary>
856
+ <dl class="field-stack compact">
857
+ ${presentation.traceRefs.map((ref) => fieldItem(ref.label, ref.value)).join("")}
858
+ </dl>
859
+ </details>
828
860
  </article>
829
861
  `;
830
862
  }
@@ -854,10 +886,6 @@ function fieldItem(label, value, extraClass = "") {
854
886
  function formatConfidence(value) {
855
887
  return `${Math.round(value * 100)}% (${value.toFixed(2)})`;
856
888
  }
857
- function titleCase(value) {
858
- const displayValue = formatValue(value);
859
- return displayValue.slice(0, 1).toUpperCase() + displayValue.slice(1);
860
- }
861
889
  function escapeHtml(value) {
862
890
  return formatValue(value)
863
891
  .replaceAll("&", "&amp;")
@@ -621,7 +621,7 @@ h1 {
621
621
 
622
622
  .candidate-card {
623
623
  display: grid;
624
- grid-template-rows: auto auto minmax(0, 1fr);
624
+ grid-template-rows: auto auto minmax(0, 1fr) auto;
625
625
  gap: 11px;
626
626
  min-height: auto;
627
627
  padding: 14px;
@@ -667,7 +667,7 @@ h1 {
667
667
  color: var(--paper-faint);
668
668
  font-size: 10px;
669
669
  letter-spacing: 0.1em;
670
- text-transform: none;
670
+ text-transform: uppercase;
671
671
  overflow-wrap: anywhere;
672
672
  word-break: break-word;
673
673
  }
@@ -960,6 +960,44 @@ textarea:focus {
960
960
  color: var(--paper-faint);
961
961
  }
962
962
 
963
+ .reference-details {
964
+ margin-top: 10px;
965
+ border-top: 1px solid var(--line);
966
+ padding-top: 8px;
967
+ }
968
+
969
+ .reference-details summary {
970
+ cursor: pointer;
971
+ color: var(--paper-faint);
972
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
973
+ font-size: 10px;
974
+ font-weight: 600;
975
+ letter-spacing: 0.08em;
976
+ text-transform: uppercase;
977
+ }
978
+
979
+ .reference-details summary::-webkit-details-marker {
980
+ display: none;
981
+ }
982
+
983
+ .reference-details summary::before {
984
+ content: "› ";
985
+ color: var(--paper-faint);
986
+ }
987
+
988
+ .reference-details[open] summary {
989
+ color: var(--accent);
990
+ }
991
+
992
+ .reference-details[open] summary::before {
993
+ content: "⌄ ";
994
+ color: var(--accent);
995
+ }
996
+
997
+ .reference-details .field-stack {
998
+ margin-top: 8px;
999
+ }
1000
+
963
1001
  /* ---- Decision payload: terminal readout, intentionally demoted -------- */
964
1002
 
965
1003
  .payload-panel {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
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",