@kontourai/survey 0.5.1 → 0.6.0

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.
@@ -3,6 +3,8 @@ import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay
3
3
  import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
4
4
  import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "./review-presentation.js";
5
5
  import { reviewResourceApiVersion, } from "../review-resource.js";
6
+ import { validateAuthorizing, buildAuthorizedActionAuthorizing } from "../review-authorizing.js";
7
+ import { humanizeIdentifier } from "./review-presentation.js";
6
8
  export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
7
9
  export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
8
10
  export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
@@ -35,6 +37,7 @@ export function buildReviewDecision(state) {
35
37
  },
36
38
  reviewedAt: state.reviewedAt,
37
39
  rationale: state.note,
40
+ authorizing: buildDecisionCardAuthorizing(state),
38
41
  projection,
39
42
  },
40
43
  status: {
@@ -42,6 +45,61 @@ export function buildReviewDecision(state) {
42
45
  },
43
46
  };
44
47
  }
48
+ /**
49
+ * Stable versioned prompt identifier for the workbench decision card control.
50
+ * Derived from the component naming convention: <module>/<component>@<version>.
51
+ */
52
+ const DECISION_CARD_PROMPT_REF = "review-workbench/decision-card@v1";
53
+ /**
54
+ * Constructs the `authorized-action` authorizing block for a workbench decision.
55
+ * On validation failure, emits a console warning and returns undefined so the
56
+ * outcome is recorded without authorizing (transparency-gap-not-blocker per ADR 0004).
57
+ */
58
+ function buildDecisionCardAuthorizing(state) {
59
+ if (!state.decision) {
60
+ return undefined;
61
+ }
62
+ const targetLabel = humanizeIdentifier(state.item.spec.target);
63
+ const renderedPrompt = decisionCardRenderedPrompt(state, targetLabel);
64
+ // Reviewer note present means the reviewer also typed a rationale — "typed".
65
+ // Control-only affirmation (no note) is "affirmed-control".
66
+ const action = state.note?.trim() ? "typed" : "affirmed-control";
67
+ const authorityRef = `actor:${state.actorId}`;
68
+ try {
69
+ const block = buildAuthorizedActionAuthorizing({
70
+ promptRef: DECISION_CARD_PROMPT_REF,
71
+ renderedPrompt,
72
+ action,
73
+ authorityRef,
74
+ });
75
+ const issues = validateAuthorizing(block);
76
+ if (issues.length > 0) {
77
+ console.warn("[survey] buildDecisionCardAuthorizing: authorizing block failed validation — recording outcome without authorizing.", issues);
78
+ return undefined;
79
+ }
80
+ return block;
81
+ }
82
+ catch (err) {
83
+ console.warn("[survey] buildDecisionCardAuthorizing: failed to construct authorizing block — recording outcome without authorizing.", err);
84
+ return undefined;
85
+ }
86
+ }
87
+ /**
88
+ * Derives the exact decision prompt rendered on the workbench decision card
89
+ * for a given review item and decision.
90
+ *
91
+ * Format mirrors the review question shown in the workbench header:
92
+ * "For {target}, decide whether {proposed} should replace {current}."
93
+ * followed by the selected decision label so the block is self-contained.
94
+ */
95
+ function decisionCardRenderedPrompt(state, targetLabel) {
96
+ const currentCandidate = state.item.spec.candidates.find((c) => c.role === "current");
97
+ const proposedCandidate = state.item.spec.candidates.find((c) => c.role === "proposed");
98
+ const currentValue = formatValue(currentCandidate?.value ?? "");
99
+ const proposedValue = formatValue(proposedCandidate?.value ?? "");
100
+ const decisionLabel = state.decision ? workbenchDecisionDefinitions[state.decision].label : "";
101
+ return `For ${targetLabel}, decide whether ${proposedValue} should replace ${currentValue}. Selected decision: ${decisionLabel}.`;
102
+ }
45
103
  export class ReviewApplyActionMappingError extends Error {
46
104
  name = "ReviewApplyActionMappingError";
47
105
  issues;
@@ -391,9 +449,12 @@ function renderReviewQueueSessionHtml(session, events, presentationAdapter) {
391
449
  return `
392
450
  <section class="workbench-shell" aria-label="Survey review workbench">
393
451
  ${renderWorkbenchHeader(state, presentationAdapter)}
452
+ ${renderMobileQueueBar(session, state, presentationAdapter)}
394
453
  ${renderActiveReviewStrip(session, state, presentationAdapter)}
395
454
  <div class="queue-layout">
396
- <aside class="queue-panel" aria-label="Review queue">
455
+ <div class="queue-drawer-backdrop" data-testid="queue-drawer-backdrop" aria-hidden="true"></div>
456
+ <aside class="queue-panel" aria-label="Review queue" role="dialog" aria-modal="false" id="queue-panel">
457
+ <button class="queue-drawer-close" type="button" data-testid="queue-drawer-close" aria-label="Close queue panel">&#x2715;</button>
397
458
  ${renderQueueRows(session, presentationAdapter)}
398
459
  ${renderSessionSummary(session)}
399
460
  ${renderSessionAudit(session, sessionExport)}
@@ -407,6 +468,28 @@ function renderReviewQueueSessionHtml(session, events, presentationAdapter) {
407
468
  </section>
408
469
  `;
409
470
  }
471
+ function renderMobileQueueBar(session, state, presentationAdapter) {
472
+ const activeIndex = session.items.findIndex((item) => item.metadata.name === session.activeItemName);
473
+ const position = activeIndex >= 0 ? activeIndex + 1 : 1;
474
+ const resolved = Object.keys(session.decisionsByItemName).length;
475
+ const presentation = buildReviewItemPresentation(state.item, presentationAdapter);
476
+ return `
477
+ <div class="mobile-queue-bar" data-testid="mobile-queue-bar" aria-label="Queue progress">
478
+ <div class="mobile-queue-progress">
479
+ <span class="field-label">Queue · ${resolved} of ${session.items.length} resolved</span>
480
+ <span class="mobile-queue-item-label">${escapeHtml(presentation.targetLabel)}</span>
481
+ </div>
482
+ <button
483
+ class="mobile-queue-open"
484
+ type="button"
485
+ data-testid="mobile-queue-open"
486
+ aria-controls="queue-panel"
487
+ aria-expanded="false"
488
+ aria-label="Open review queue"
489
+ >Queue</button>
490
+ </div>
491
+ `;
492
+ }
410
493
  function renderSessionAudit(session, sessionExport) {
411
494
  const lastEvents = sessionExport.events.slice(-6);
412
495
  const replayed = replayReviewSessionEvents({
@@ -527,17 +610,13 @@ function renderActiveReviewStrip(session, state, presentationAdapter) {
527
610
  `;
528
611
  }
529
612
  function renderWorkbenchHeader(state, presentationAdapter) {
530
- const current = candidateByRole(state.item, "current");
531
- const proposed = candidateByRole(state.item, "proposed");
532
613
  const presentation = buildReviewItemPresentation(state.item, presentationAdapter);
533
- const currentPresentation = buildReviewCandidatePresentation(state.item, current, presentationAdapter, presentation.targetLabel);
534
- const proposedPresentation = buildReviewCandidatePresentation(state.item, proposed, presentationAdapter, presentation.targetLabel);
535
614
  return `
536
615
  <header class="topbar">
537
616
  <div>
538
617
  <p class="eyebrow">${escapeHtml(String(state.item.metadata.producer?.displayName ?? "Survey"))}</p>
539
618
  <h1>Review candidate update</h1>
540
- <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>
619
+ <p class="review-target-label" data-testid="review-target-label"><strong>${escapeHtml(presentation.targetLabel)}</strong></p>
541
620
  </div>
542
621
  <dl class="meta-grid">
543
622
  ${metaItem("Item", state.item.metadata.name)}
@@ -661,32 +740,31 @@ function renderSurfacePreview(state, presentationAdapter) {
661
740
  }
662
741
  function renderPendingSurfacePreview() {
663
742
  return `
664
- <section class="surface-preview" data-testid="surface-preview" aria-label="Surface preview">
665
- <div class="surface-head">
666
- <h2>Surface preview</h2>
743
+ <details class="surface-preview" data-testid="surface-preview" aria-label="Surface preview">
744
+ <summary class="surface-summary">
745
+ <span class="surface-summary-label">Surface preview</span>
667
746
  <span class="state-label">pending</span>
668
- </div>
669
- <p class="preview-disclaimer">Choose a review decision to preview the source and review posture that would project toward Surface. Survey does not validate real-world truth.</p>
670
- </section>
747
+ </summary>
748
+ <p class="preview-disclaimer">Choose a review decision to preview the source and review posture that would project toward Surface.</p>
749
+ </details>
671
750
  `;
672
751
  }
673
752
  function renderPopulatedSurfacePreview(preview) {
674
753
  return `
675
- <section class="surface-preview" data-testid="surface-preview" aria-label="Surface preview">
676
- <div class="surface-head">
677
- <h2>Surface preview</h2>
754
+ <details class="surface-preview" data-testid="surface-preview" aria-label="Surface preview">
755
+ <summary class="surface-summary">
756
+ <span class="surface-summary-label">Surface preview</span>
678
757
  <span class="state-label">local preview</span>
679
- </div>
680
- <p class="preview-disclaimer" data-testid="surface-preview-disclaimer">${escapeHtml(preview.postureDisclaimer)}</p>
758
+ </summary>
681
759
  <div class="preview-section-grid">
682
760
  ${renderSurfacePreviewSections(preview)}
683
761
  </div>
684
- </section>
762
+ <p class="preview-disclaimer preview-disclaimer-footer" data-testid="surface-preview-disclaimer">${escapeHtml(preview.postureDisclaimer)}</p>
763
+ </details>
685
764
  `;
686
765
  }
687
766
  function renderSurfacePreviewSections(preview) {
688
767
  return [
689
- renderCanonicalClaim(preview),
690
768
  renderCandidateHistory(preview),
691
769
  renderSourceEvidence(preview),
692
770
  renderReviewEvent(preview),
@@ -884,11 +962,13 @@ function renderCurrentState(root, session, controller, presentationAdapter) {
884
962
  bindQueueRows(root, (itemName) => {
885
963
  controller.selectQueueItem(itemName);
886
964
  controller.renderCurrentState();
965
+ closeQueueDrawer(root);
887
966
  });
888
967
  bindNextUnresolved(root, () => {
889
968
  controller.goToNextUnresolved();
890
969
  controller.renderCurrentState();
891
970
  });
971
+ bindQueueDrawer(root);
892
972
  }
893
973
  function queueSessionFromStartState(startState) {
894
974
  if ("items" in startState) {
@@ -935,6 +1015,48 @@ function bindNextUnresolved(root, goToNextUnresolved) {
935
1015
  });
936
1016
  });
937
1017
  }
1018
+ function bindQueueDrawer(root) {
1019
+ const openButton = root.querySelector("[data-testid='mobile-queue-open']");
1020
+ const closeButton = root.querySelector("[data-testid='queue-drawer-close']");
1021
+ const backdrop = root.querySelector("[data-testid='queue-drawer-backdrop']");
1022
+ const panel = root.querySelector("#queue-panel");
1023
+ openButton?.addEventListener("click", () => openQueueDrawer(root));
1024
+ closeButton?.addEventListener("click", () => closeQueueDrawer(root));
1025
+ backdrop?.addEventListener("click", () => closeQueueDrawer(root));
1026
+ if (typeof root.addEventListener === "function") {
1027
+ root.addEventListener("keydown", (event) => {
1028
+ if (event.key === "Escape" && panel?.classList.contains("is-open")) {
1029
+ closeQueueDrawer(root);
1030
+ }
1031
+ });
1032
+ }
1033
+ }
1034
+ function openQueueDrawer(root) {
1035
+ const panel = root.querySelector("#queue-panel");
1036
+ const backdrop = root.querySelector("[data-testid='queue-drawer-backdrop']");
1037
+ const openButton = root.querySelector("[data-testid='mobile-queue-open']");
1038
+ if (!panel) {
1039
+ return;
1040
+ }
1041
+ panel.classList.add("is-open");
1042
+ panel.setAttribute("aria-modal", "true");
1043
+ backdrop?.classList.add("is-visible");
1044
+ openButton?.setAttribute("aria-expanded", "true");
1045
+ panel.querySelector("button, [tabindex]")?.focus();
1046
+ }
1047
+ function closeQueueDrawer(root) {
1048
+ const panel = root.querySelector("#queue-panel");
1049
+ const backdrop = root.querySelector("[data-testid='queue-drawer-backdrop']");
1050
+ const openButton = root.querySelector("[data-testid='mobile-queue-open']");
1051
+ if (!panel) {
1052
+ return;
1053
+ }
1054
+ panel.classList.remove("is-open");
1055
+ panel.setAttribute("aria-modal", "false");
1056
+ backdrop?.classList.remove("is-visible");
1057
+ openButton?.setAttribute("aria-expanded", "false");
1058
+ openButton?.focus();
1059
+ }
938
1060
  function scrollActiveReviewIntoView(root) {
939
1061
  if (typeof window === "undefined" || !window.matchMedia("(max-width: 980px)").matches) {
940
1062
  return;
@@ -169,6 +169,17 @@ h1 {
169
169
  font-size: 0.95em;
170
170
  }
171
171
 
172
+ .review-target-label {
173
+ margin-top: 4px;
174
+ }
175
+
176
+ .review-target-label strong {
177
+ color: var(--paper);
178
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
179
+ font-size: 13px;
180
+ font-weight: 600;
181
+ }
182
+
172
183
  .meta-grid {
173
184
  display: none;
174
185
  grid-template-columns: repeat(4, minmax(110px, 1fr));
@@ -223,6 +234,52 @@ h1 {
223
234
  display: none;
224
235
  }
225
236
 
237
+ /* ---- Mobile queue progress bar (mobile only) -------------------------- */
238
+
239
+ .mobile-queue-bar {
240
+ display: none;
241
+ }
242
+
243
+ /* ---- Queue drawer backdrop -------------------------------------------- */
244
+
245
+ .queue-drawer-backdrop {
246
+ display: none;
247
+ position: fixed;
248
+ inset: 0;
249
+ z-index: 40;
250
+ background: rgba(0, 0, 0, 0.55);
251
+ backdrop-filter: blur(2px);
252
+ opacity: 0;
253
+ transition: opacity 0.22s ease;
254
+ pointer-events: none;
255
+ }
256
+
257
+ .queue-drawer-backdrop.is-visible {
258
+ opacity: 1;
259
+ pointer-events: auto;
260
+ }
261
+
262
+ /* ---- Queue drawer close button (mobile only) -------------------------- */
263
+
264
+ .queue-drawer-close {
265
+ display: none;
266
+ position: absolute;
267
+ top: 12px;
268
+ right: 12px;
269
+ width: 30px;
270
+ height: 30px;
271
+ border: 1px solid var(--line-strong);
272
+ border-radius: 999px;
273
+ background: rgba(8, 12, 18, 0.8);
274
+ color: var(--paper-faint);
275
+ cursor: pointer;
276
+ font-size: 14px;
277
+ line-height: 1;
278
+ align-items: center;
279
+ justify-content: center;
280
+ z-index: 2;
281
+ }
282
+
226
283
  .queue-list,
227
284
  .session-summary,
228
285
  .session-audit {
@@ -894,6 +951,53 @@ textarea:focus {
894
951
  gap: 11px;
895
952
  }
896
953
 
954
+ /* When surface-preview is a <details> element */
955
+ details.surface-preview {
956
+ display: block;
957
+ }
958
+
959
+ details.surface-preview[open] {
960
+ display: grid;
961
+ gap: 11px;
962
+ }
963
+
964
+ .surface-summary {
965
+ display: flex;
966
+ gap: 12px;
967
+ justify-content: space-between;
968
+ align-items: center;
969
+ cursor: pointer;
970
+ list-style: none;
971
+ padding: 0;
972
+ }
973
+
974
+ .surface-summary::-webkit-details-marker {
975
+ display: none;
976
+ }
977
+
978
+ .surface-summary::before {
979
+ content: none;
980
+ }
981
+
982
+ .surface-summary-label {
983
+ font-family: "Fraunces", Georgia, serif;
984
+ font-weight: 600;
985
+ font-size: 16px;
986
+ letter-spacing: -0.01em;
987
+ color: var(--paper);
988
+ }
989
+
990
+ .surface-summary-label::before {
991
+ content: "› ";
992
+ color: var(--accent);
993
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
994
+ font-size: 13px;
995
+ }
996
+
997
+ details.surface-preview[open] .surface-summary-label::before {
998
+ content: "⌄ ";
999
+ }
1000
+
897
1001
  .surface-head {
898
1002
  display: flex;
899
1003
  gap: 12px;
@@ -916,6 +1020,13 @@ textarea:focus {
916
1020
  line-height: 1.5;
917
1021
  }
918
1022
 
1023
+ .preview-disclaimer-footer {
1024
+ margin-top: 4px;
1025
+ border-left-color: var(--line);
1026
+ font-size: 11.5px;
1027
+ color: var(--paper-faint);
1028
+ }
1029
+
919
1030
  .preview-section-grid {
920
1031
  display: grid;
921
1032
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1058,7 +1169,6 @@ pre {
1058
1169
 
1059
1170
  @media (max-width: 980px) {
1060
1171
  .topbar,
1061
- .queue-layout,
1062
1172
  .focus-evidence,
1063
1173
  .candidate-grid {
1064
1174
  grid-template-columns: 1fr;
@@ -1073,9 +1183,8 @@ pre {
1073
1183
  }
1074
1184
 
1075
1185
  .queue-layout {
1076
- grid-template-areas:
1077
- "content"
1078
- "queue";
1186
+ display: block;
1187
+ position: relative;
1079
1188
  }
1080
1189
 
1081
1190
  .active-review-strip {
@@ -1136,8 +1245,85 @@ pre {
1136
1245
  font-size: 11px;
1137
1246
  }
1138
1247
 
1139
- .queue-panel {
1140
- grid-area: queue;
1248
+ .queue-drawer-backdrop {
1249
+ display: block;
1250
+ }
1251
+
1252
+ .queue-drawer-close {
1253
+ display: flex;
1254
+ }
1255
+
1256
+ .mobile-queue-bar {
1257
+ display: grid;
1258
+ grid-template-columns: minmax(0, 1fr) auto;
1259
+ gap: 12px;
1260
+ align-items: center;
1261
+ border: 1px solid var(--line-strong);
1262
+ border-left: 3px solid var(--accent);
1263
+ border-radius: var(--radius);
1264
+ padding: 11px 14px;
1265
+ margin-bottom: 14px;
1266
+ background:
1267
+ linear-gradient(90deg, rgba(92, 224, 198, 0.08), transparent 46%),
1268
+ linear-gradient(180deg, var(--ink-800), var(--ink-850));
1269
+ animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
1270
+ }
1271
+
1272
+ .mobile-queue-progress {
1273
+ display: grid;
1274
+ gap: 3px;
1275
+ min-width: 0;
1276
+ }
1277
+
1278
+ .mobile-queue-item-label {
1279
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
1280
+ font-size: 13px;
1281
+ font-weight: 600;
1282
+ color: var(--paper);
1283
+ overflow-wrap: anywhere;
1284
+ word-break: break-word;
1285
+ }
1286
+
1287
+ .mobile-queue-open {
1288
+ flex-shrink: 0;
1289
+ min-height: 34px;
1290
+ padding: 0 14px;
1291
+ border: 1px solid var(--line-strong);
1292
+ border-radius: 999px;
1293
+ background: rgba(92, 224, 198, 0.08);
1294
+ color: var(--accent);
1295
+ cursor: pointer;
1296
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
1297
+ font-size: 11px;
1298
+ font-weight: 600;
1299
+ letter-spacing: 0.06em;
1300
+ transition: background 0.16s ease, border-color 0.16s ease;
1301
+ }
1302
+
1303
+ .mobile-queue-open:hover {
1304
+ background: rgba(92, 224, 198, 0.16);
1305
+ border-color: var(--accent);
1306
+ }
1307
+
1308
+ /* Higher specificity than the @container stacked-layout fallback below:
1309
+ on a narrow viewport the drawer must win even inside a narrow container. */
1310
+ .queue-layout .queue-panel {
1311
+ position: fixed;
1312
+ top: 0;
1313
+ left: 0;
1314
+ bottom: 0;
1315
+ width: min(320px, 90vw);
1316
+ z-index: 50;
1317
+ overflow-y: auto;
1318
+ overflow-x: hidden;
1319
+ transform: translateX(-105%);
1320
+ transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
1321
+ border-radius: 0 var(--radius) var(--radius) 0;
1322
+ padding-top: 50px;
1323
+ }
1324
+
1325
+ .queue-layout .queue-panel.is-open {
1326
+ transform: translateX(0);
1141
1327
  }
1142
1328
 
1143
1329
  .content-grid {
@@ -1148,10 +1334,6 @@ pre {
1148
1334
  display: none;
1149
1335
  }
1150
1336
 
1151
- .queue-panel {
1152
- position: static;
1153
- }
1154
-
1155
1337
  .content-grid,
1156
1338
  .review-main,
1157
1339
  .candidate-grid,
@@ -1331,11 +1513,11 @@ pre {
1331
1513
  }
1332
1514
 
1333
1515
  .focus-value strong {
1334
- font-size: clamp(22px, 10vw, 32px);
1516
+ font-size: clamp(16px, 5cqi, 22px);
1335
1517
  }
1336
1518
 
1337
1519
  .candidate-value .field-value {
1338
- font-size: clamp(21px, 9vw, 30px);
1520
+ font-size: clamp(15px, 4.5cqi, 20px);
1339
1521
  line-height: 1.12;
1340
1522
  }
1341
1523
 
@@ -1440,7 +1622,7 @@ pre {
1440
1622
 
1441
1623
  .focus-value strong {
1442
1624
  margin-top: 6px;
1443
- font-size: 24px;
1625
+ font-size: 18px;
1444
1626
  line-height: 1.05;
1445
1627
  }
1446
1628
 
@@ -1449,6 +1631,6 @@ pre {
1449
1631
  }
1450
1632
 
1451
1633
  .candidate-value .field-value {
1452
- font-size: 20px;
1634
+ font-size: 17px;
1453
1635
  }
1454
1636
  }
@@ -47,6 +47,26 @@ export interface CandidateSet {
47
47
  metadata?: Record<string, unknown>;
48
48
  }
49
49
  export type ReviewStatus = Extract<TrustStatus, "verified" | "assumed" | "rejected" | "proposed">;
50
+ export type ReviewAuthorizingKind = "explicit-statement" | "exchange" | "authorized-action";
51
+ export interface ReviewAuthorizingExplicitStatement {
52
+ kind: "explicit-statement";
53
+ statement: string;
54
+ source?: string;
55
+ }
56
+ export interface ReviewAuthorizingExchange {
57
+ kind: "exchange";
58
+ prompt: string;
59
+ response: string;
60
+ source?: string;
61
+ }
62
+ export interface ReviewAuthorizingAuthorizedAction {
63
+ kind: "authorized-action";
64
+ promptRef: string;
65
+ renderedPrompt: string;
66
+ action: "affirmed-control" | "typed";
67
+ authorityRef: string;
68
+ }
69
+ export type ReviewAuthorizing = ReviewAuthorizingExplicitStatement | ReviewAuthorizingExchange | ReviewAuthorizingAuthorizedAction;
50
70
  export interface ReviewOutcome {
51
71
  id: string;
52
72
  candidateSetId: string;
@@ -58,6 +78,9 @@ export interface ReviewOutcome {
58
78
  evidenceIds?: string[];
59
79
  withinComfortZone?: boolean;
60
80
  comfortZoneNote?: string;
81
+ /** Optional testimony provenance. When present, records how this decision
82
+ * was authorized — interpreted by downstream verifiers for admissibility. */
83
+ authorizing?: ReviewAuthorizing;
61
84
  metadata?: Record<string, unknown>;
62
85
  }
63
86
  export interface EscalationRecord {