@kontourai/survey 0.4.14 → 0.4.16

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
  }
@@ -267,6 +267,164 @@ export declare const regulatedRuleConflictReviewItemFixture: {
267
267
  selectedCandidateId: string;
268
268
  };
269
269
  };
270
+ export declare const facilityCredentialReviewItemFixture: {
271
+ apiVersion: "survey.kontourai.io/v1alpha1";
272
+ kind: "ReviewItem";
273
+ metadata: {
274
+ name: string;
275
+ labels: {
276
+ domain: string;
277
+ workflow: string;
278
+ };
279
+ producer: {
280
+ displayName: string;
281
+ slug: string;
282
+ };
283
+ };
284
+ spec: {
285
+ target: string;
286
+ selectedCandidateId: string;
287
+ candidateSetStatus: "needs-review";
288
+ rationale: string;
289
+ projection: {
290
+ candidateSetId: string;
291
+ };
292
+ producerPolicy: {
293
+ decisionMode: string;
294
+ sourceAuthorityProjection: string;
295
+ feedbackTags: string[];
296
+ };
297
+ candidates: ({
298
+ id: string;
299
+ role: "current";
300
+ value: {
301
+ licenseNumber: string;
302
+ status: string;
303
+ issuedAt: string;
304
+ expiresAt: string;
305
+ permittedServices: string[];
306
+ inspections: {
307
+ date: string;
308
+ outcome: string;
309
+ findingCount: number;
310
+ }[];
311
+ };
312
+ confidence: number;
313
+ source: {
314
+ sourceId: string;
315
+ sourceRef: string;
316
+ kind: "manual-entry";
317
+ observedAt: string;
318
+ locatorScheme: "structured-field";
319
+ fetchedAt?: undefined;
320
+ };
321
+ locator: {
322
+ scheme: "structured-field";
323
+ locator: string;
324
+ excerpt: string;
325
+ };
326
+ extraction: {
327
+ extractionId: string;
328
+ target: string;
329
+ confidence: number;
330
+ extractor: string;
331
+ extractedAt: string;
332
+ };
333
+ claimTarget: {
334
+ claimId: string;
335
+ subjectType: string;
336
+ subjectId: string;
337
+ surface: string;
338
+ claimType: string;
339
+ fieldOrBehavior: string;
340
+ impactLevel: "high";
341
+ evidenceType: "human_attestation";
342
+ evidenceMethod: "attestation";
343
+ collectedBy: string;
344
+ };
345
+ projection: {
346
+ rawSourceId: string;
347
+ extractionId: string;
348
+ candidateSetId: string;
349
+ candidateId: string;
350
+ claimId: string;
351
+ };
352
+ producer: {
353
+ credentialSystem: string;
354
+ sourceAuthority?: undefined;
355
+ };
356
+ sourceRank?: undefined;
357
+ } | {
358
+ id: string;
359
+ role: "proposed";
360
+ value: {
361
+ licenseNumber: string;
362
+ status: string;
363
+ issuedAt: string;
364
+ expiresAt: string;
365
+ permittedServices: string[];
366
+ inspections: {
367
+ date: string;
368
+ outcome: string;
369
+ findingCount: number;
370
+ }[];
371
+ };
372
+ confidence: number;
373
+ sourceRank: number;
374
+ source: {
375
+ sourceId: string;
376
+ sourceRef: string;
377
+ kind: "api-record";
378
+ observedAt: string;
379
+ fetchedAt: string;
380
+ locatorScheme: "structured-field";
381
+ };
382
+ locator: {
383
+ scheme: "structured-field";
384
+ locator: string;
385
+ excerpt: string;
386
+ };
387
+ extraction: {
388
+ extractionId: string;
389
+ target: string;
390
+ confidence: number;
391
+ extractor: string;
392
+ extractedAt: string;
393
+ };
394
+ claimTarget: {
395
+ claimId: string;
396
+ subjectType: string;
397
+ subjectId: string;
398
+ surface: string;
399
+ claimType: string;
400
+ fieldOrBehavior: string;
401
+ impactLevel: "high";
402
+ evidenceType: "source_excerpt";
403
+ evidenceMethod: "extraction";
404
+ collectedBy: string;
405
+ };
406
+ projection: {
407
+ rawSourceId: string;
408
+ extractionId: string;
409
+ candidateSetId: string;
410
+ candidateId: string;
411
+ claimId: string;
412
+ };
413
+ producer: {
414
+ credentialSystem: string;
415
+ sourceAuthority: {
416
+ authorityClass: string;
417
+ declaredBy: string;
418
+ scope: string;
419
+ };
420
+ };
421
+ })[];
422
+ };
423
+ status: {
424
+ observedCandidateCount: number;
425
+ selectedCandidateId: string;
426
+ };
427
+ };
270
428
  export declare const reviewWorkbenchQueueFixtures: (ReviewItem | {
271
429
  apiVersion: "survey.kontourai.io/v1alpha1";
272
430
  kind: "ReviewItem";
@@ -267,6 +267,161 @@ export const regulatedRuleConflictReviewItemFixture = {
267
267
  selectedCandidateId: "regulated-rule-conflict-standard-threshold:candidate:current",
268
268
  },
269
269
  };
270
+ export const facilityCredentialReviewItemFixture = {
271
+ apiVersion: reviewResourceApiVersion,
272
+ kind: "ReviewItem",
273
+ metadata: {
274
+ name: "facility-credential-review-operating-license",
275
+ labels: {
276
+ domain: "facility-credential",
277
+ workflow: "credential-review",
278
+ },
279
+ producer: {
280
+ displayName: "Facility Credential Review",
281
+ slug: "facility-credential-review",
282
+ },
283
+ },
284
+ spec: {
285
+ target: "operatingLicenseCredential",
286
+ selectedCandidateId: "facility-credential-review-operating-license:candidate:current",
287
+ candidateSetStatus: "needs-review",
288
+ rationale: "A source-of-authority registry update may supersede the currently managed facility credential snapshot.",
289
+ projection: {
290
+ candidateSetId: "facility-credential-review-operating-license:candidate-set",
291
+ },
292
+ producerPolicy: {
293
+ decisionMode: "current-proposed",
294
+ sourceAuthorityProjection: "only-for-selected-source-backed-value",
295
+ feedbackTags: ["credential-review", "authority-backed-registry", "nested-value"],
296
+ },
297
+ candidates: [
298
+ {
299
+ id: "facility-credential-review-operating-license:candidate:current",
300
+ role: "current",
301
+ value: {
302
+ licenseNumber: "FAC-2025-1042",
303
+ status: "active",
304
+ issuedAt: "2025-01-15",
305
+ expiresAt: "2026-01-15",
306
+ permittedServices: ["day-program", "after-school-care"],
307
+ inspections: [
308
+ { date: "2025-11-20", outcome: "passed", findingCount: 0 },
309
+ ],
310
+ },
311
+ confidence: 0.98,
312
+ source: {
313
+ sourceId: "facility-credential-review-operating-license:source:managed-record",
314
+ sourceRef: "survey-example://facility-credentials/facility-42/current",
315
+ kind: "manual-entry",
316
+ observedAt: "2026-01-02T16:00:00.000Z",
317
+ locatorScheme: "structured-field",
318
+ },
319
+ locator: {
320
+ scheme: "structured-field",
321
+ locator: "facilityCredentials[path=operatingLicense]",
322
+ excerpt: "Managed facility credential FAC-2025-1042 is active through 2026-01-15.",
323
+ },
324
+ extraction: {
325
+ extractionId: "facility-credential-review-operating-license:extraction:current",
326
+ target: "operatingLicenseCredential",
327
+ confidence: 0.98,
328
+ extractor: "credential-record-manager",
329
+ extractedAt: "2026-01-02T16:00:00.000Z",
330
+ },
331
+ claimTarget: {
332
+ claimId: "facility-credential.facility-42.operating-license.current",
333
+ subjectType: "facility",
334
+ subjectId: "facility-42",
335
+ surface: "facility.credential-profile",
336
+ claimType: "facility.credential",
337
+ fieldOrBehavior: "operatingLicenseCredential",
338
+ impactLevel: "high",
339
+ evidenceType: "human_attestation",
340
+ evidenceMethod: "attestation",
341
+ collectedBy: "credential-record-manager",
342
+ },
343
+ projection: {
344
+ rawSourceId: "facility-credential-review-operating-license:source:managed-record",
345
+ extractionId: "facility-credential-review-operating-license:extraction:current",
346
+ candidateSetId: "facility-credential-review-operating-license:candidate-set",
347
+ candidateId: "facility-credential-review-operating-license:projection:current",
348
+ claimId: "facility-credential.facility-42.operating-license.current",
349
+ },
350
+ producer: {
351
+ credentialSystem: "managed-record",
352
+ },
353
+ },
354
+ {
355
+ id: "facility-credential-review-operating-license:candidate:proposed",
356
+ role: "proposed",
357
+ value: {
358
+ licenseNumber: "FAC-2026-1042",
359
+ status: "active",
360
+ issuedAt: "2026-01-16",
361
+ expiresAt: "2027-01-15",
362
+ permittedServices: ["day-program", "after-school-care", "summer-session"],
363
+ inspections: [
364
+ { date: "2026-01-10", outcome: "passed", findingCount: 1 },
365
+ { date: "2026-01-12", outcome: "corrected", findingCount: 0 },
366
+ ],
367
+ },
368
+ confidence: 0.93,
369
+ sourceRank: 1,
370
+ source: {
371
+ sourceId: "facility-credential-review-operating-license:source:registry",
372
+ sourceRef: "https://example.test/facility-registry/facility-42/license",
373
+ kind: "api-record",
374
+ observedAt: "2026-01-17T14:45:00.000Z",
375
+ fetchedAt: "2026-01-17T14:45:00.000Z",
376
+ locatorScheme: "structured-field",
377
+ },
378
+ locator: {
379
+ scheme: "structured-field",
380
+ locator: "/facilityCredentials/operatingLicense",
381
+ excerpt: "License FAC-2026-1042 active through 2027-01-15; permitted services include summer-session.",
382
+ },
383
+ extraction: {
384
+ extractionId: "facility-credential-review-operating-license:extraction:registry",
385
+ target: "operatingLicenseCredential",
386
+ confidence: 0.93,
387
+ extractor: "credential-registry-sync",
388
+ extractedAt: "2026-01-17T14:45:00.000Z",
389
+ },
390
+ claimTarget: {
391
+ claimId: "facility-credential.facility-42.operating-license.registry",
392
+ subjectType: "facility",
393
+ subjectId: "facility-42",
394
+ surface: "facility.credential-profile",
395
+ claimType: "facility.credential-candidate",
396
+ fieldOrBehavior: "operatingLicenseCredential",
397
+ impactLevel: "high",
398
+ evidenceType: "source_excerpt",
399
+ evidenceMethod: "extraction",
400
+ collectedBy: "credential-registry-sync",
401
+ },
402
+ projection: {
403
+ rawSourceId: "facility-credential-review-operating-license:source:registry",
404
+ extractionId: "facility-credential-review-operating-license:extraction:registry",
405
+ candidateSetId: "facility-credential-review-operating-license:candidate-set",
406
+ candidateId: "facility-credential-review-operating-license:projection:registry",
407
+ claimId: "facility-credential.facility-42.operating-license.registry",
408
+ },
409
+ producer: {
410
+ credentialSystem: "source-of-authority-registry",
411
+ sourceAuthority: {
412
+ authorityClass: "facility-credential-registry",
413
+ declaredBy: "Example Facility Registry",
414
+ scope: "Operating license credential for facility-42",
415
+ },
416
+ },
417
+ },
418
+ ],
419
+ },
420
+ status: {
421
+ observedCandidateCount: 2,
422
+ selectedCandidateId: "facility-credential-review-operating-license:candidate:current",
423
+ },
424
+ };
270
425
  function queueFixture(name, target, currentValue, proposedValue, candidateSetStatus, feedbackTags) {
271
426
  return {
272
427
  ...publicDirectoryReviewItemFixture,
@@ -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() {
@@ -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(titleCase(candidate.role ?? "candidate"))}</h2>
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">Value</span>
834
- <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>
835
846
  </div>
836
847
  <dl class="field-stack">
837
- ${fieldItem("Source URL", candidate.source.sourceRef)}
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("Candidate ID", candidate.id)}
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("&", "&amp;")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
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",