@kontourai/survey 1.14.0 → 1.16.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.
- package/README.md +45 -1
- package/dist/src/calibration.d.ts +1 -0
- package/dist/src/calibration.js +5 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/learning-projections.d.ts +2 -2
- package/dist/src/learning-projections.js +25 -0
- package/dist/src/mcp/review-mcp.js +48 -10
- package/dist/src/producer-discipline.d.ts +13 -0
- package/dist/src/producer-discipline.js +41 -0
- package/dist/src/review-proof.d.ts +22 -8
- package/dist/src/review-proof.js +56 -5
- package/dist/src/review-resource.d.ts +6 -0
- package/dist/src/review-workbench/producer-decision-mode.js +2 -0
- package/dist/src/review-workbench/review-queue-session.d.ts +11 -2
- package/dist/src/review-workbench/review-queue-session.js +58 -2
- package/dist/src/review-workbench/review-session-replay.d.ts +1 -1
- package/dist/src/review-workbench/review-session-replay.js +19 -0
- package/dist/src/review-workbench/review-surface-preview.js +1 -1
- package/dist/src/review-workbench/review-workbench-css.generated.js +98 -90
- package/dist/src/review-workbench/review-workbench.css +98 -90
- package/dist/src/review-workbench/review-workbench.js +68 -3
- package/dist/src/review-workbench/review-workbench.standalone.css +8 -0
- package/dist/src/to-surface.js +31 -14
- package/dist/src/types.d.ts +8 -0
- package/package.json +1 -1
|
@@ -3,149 +3,149 @@
|
|
|
3
3
|
color-scheme: dark;
|
|
4
4
|
|
|
5
5
|
/* structure */
|
|
6
|
-
--k-bg: #0a0e13;
|
|
7
|
-
--k-panel: #111824;
|
|
8
|
-
--k-panel-raised: #16202d;
|
|
9
|
-
--k-line: rgba(150, 180, 210, 0.12);
|
|
10
|
-
--k-line-strong: rgba(150, 180, 210, 0.22);
|
|
11
|
-
--k-shadow: 0 26px 60px -42px rgba(0, 0, 0, 0.95);
|
|
6
|
+
--k-bg: var(--k-bg, #0a0e13);
|
|
7
|
+
--k-panel: var(--k-panel, #111824);
|
|
8
|
+
--k-panel-raised: var(--k-panel-raised, #16202d);
|
|
9
|
+
--k-line: var(--k-line, rgba(150, 180, 210, 0.12));
|
|
10
|
+
--k-line-strong: var(--k-line-strong, rgba(150, 180, 210, 0.22));
|
|
11
|
+
--k-shadow: var(--k-shadow, 0 26px 60px -42px rgba(0, 0, 0, 0.95));
|
|
12
12
|
|
|
13
13
|
/* text */
|
|
14
|
-
--k-text: #eef3f8;
|
|
15
|
-
--k-text-muted: #aebccb;
|
|
16
|
-
--k-text-faint: #72869b;
|
|
14
|
+
--k-text: var(--k-text, #eef3f8);
|
|
15
|
+
--k-text-muted: var(--k-text-muted, #aebccb);
|
|
16
|
+
--k-text-faint: var(--k-text-faint, #72869b);
|
|
17
17
|
|
|
18
18
|
/* brand */
|
|
19
|
-
--k-brand: #5ce0c6;
|
|
20
|
-
--k-brand-contrast: #06080b;
|
|
19
|
+
--k-brand: var(--k-brand, #5ce0c6);
|
|
20
|
+
--k-brand-contrast: var(--k-brand-contrast, #06080b);
|
|
21
21
|
|
|
22
22
|
/* semantic status scale */
|
|
23
|
-
--k-positive: #34d399;
|
|
24
|
-
--k-caution: #f3b14b;
|
|
25
|
-
--k-negative: #ff6f6f;
|
|
26
|
-
--k-neutral: #6f8095;
|
|
27
|
-
--k-active: #7aa2ff;
|
|
23
|
+
--k-positive: var(--k-positive, #34d399);
|
|
24
|
+
--k-caution: var(--k-caution, #f3b14b);
|
|
25
|
+
--k-negative: var(--k-negative, #ff6f6f);
|
|
26
|
+
--k-neutral: var(--k-neutral, #6f8095);
|
|
27
|
+
--k-active: var(--k-active, #7aa2ff);
|
|
28
28
|
--k-positive-soft: color-mix(in oklab, var(--k-positive) 14%, transparent);
|
|
29
29
|
--k-caution-soft: color-mix(in oklab, var(--k-caution) 14%, transparent);
|
|
30
30
|
--k-negative-soft: color-mix(in oklab, var(--k-negative) 14%, transparent);
|
|
31
31
|
--k-active-soft: color-mix(in oklab, var(--k-active) 14%, transparent);
|
|
32
32
|
|
|
33
33
|
/* spacing */
|
|
34
|
-
--k-space-1: 4px;
|
|
35
|
-
--k-space-2: 8px;
|
|
36
|
-
--k-space-3: 12px;
|
|
37
|
-
--k-space-4: 16px;
|
|
38
|
-
--k-space-5: 24px;
|
|
39
|
-
--k-space-6: 32px;
|
|
34
|
+
--k-space-1: var(--k-space-1, 4px);
|
|
35
|
+
--k-space-2: var(--k-space-2, 8px);
|
|
36
|
+
--k-space-3: var(--k-space-3, 12px);
|
|
37
|
+
--k-space-4: var(--k-space-4, 16px);
|
|
38
|
+
--k-space-5: var(--k-space-5, 24px);
|
|
39
|
+
--k-space-6: var(--k-space-6, 32px);
|
|
40
40
|
|
|
41
41
|
/* radius */
|
|
42
|
-
--k-radius-sm: 9px;
|
|
43
|
-
--k-radius-md: 14px;
|
|
42
|
+
--k-radius-sm: var(--k-radius-sm, 9px);
|
|
43
|
+
--k-radius-md: var(--k-radius-md, 14px);
|
|
44
44
|
|
|
45
45
|
/* type */
|
|
46
|
-
--k-text-xs: 11px;
|
|
47
|
-
--k-text-sm: 12.5px;
|
|
48
|
-
--k-text-md: 14px;
|
|
49
|
-
--k-text-lg: 18px;
|
|
50
|
-
--k-text-xl: 22px;
|
|
51
|
-
--k-text-2xl: clamp(26px, 3.4vw, 38px);
|
|
46
|
+
--k-text-xs: var(--k-text-xs, 11px);
|
|
47
|
+
--k-text-sm: var(--k-text-sm, 12.5px);
|
|
48
|
+
--k-text-md: var(--k-text-md, 14px);
|
|
49
|
+
--k-text-lg: var(--k-text-lg, 18px);
|
|
50
|
+
--k-text-xl: var(--k-text-xl, 22px);
|
|
51
|
+
--k-text-2xl: var(--k-text-2xl, clamp(26px, 3.4vw, 38px));
|
|
52
52
|
|
|
53
53
|
/* fonts */
|
|
54
|
-
--k-font-display: "Fraunces", Georgia, "Times New Roman", serif;
|
|
55
|
-
--k-font-ui: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
56
|
-
--k-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
54
|
+
--k-font-display: var(--k-font-display, "Fraunces", Georgia, "Times New Roman", serif);
|
|
55
|
+
--k-font-ui: var(--k-font-ui, "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
|
|
56
|
+
--k-font-mono: var(--k-font-mono, "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
57
57
|
|
|
58
58
|
/* motion */
|
|
59
|
-
--k-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
60
|
-
--k-dur: 0.16s;
|
|
59
|
+
--k-ease: var(--k-ease, cubic-bezier(0.22, 1, 0.36, 1));
|
|
60
|
+
--k-dur: var(--k-dur, 0.16s);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
.survey-workbench-embed[data-theme="light"]{
|
|
64
64
|
color-scheme: light;
|
|
65
|
-
--k-bg: #f5f4ef;
|
|
66
|
-
--k-panel: #ffffff;
|
|
67
|
-
--k-panel-raised: #fbfaf7;
|
|
68
|
-
--k-line: rgba(36, 40, 46, 0.12);
|
|
69
|
-
--k-line-strong: rgba(36, 40, 46, 0.20);
|
|
70
|
-
--k-shadow: 0 18px 50px -30px rgba(0, 0, 0, 0.25);
|
|
71
|
-
--k-text: #202124;
|
|
72
|
-
--k-text-muted: #5b626b;
|
|
73
|
-
--k-text-faint: #707782;
|
|
74
|
-
--k-brand-contrast: #ffffff;
|
|
75
|
-
--k-positive: #168257;
|
|
76
|
-
--k-caution: #8a5a00;
|
|
77
|
-
--k-negative: #c83b3b;
|
|
78
|
-
--k-neutral: #5f6975;
|
|
79
|
-
--k-active: #3f6fd6;
|
|
65
|
+
--k-bg: var(--k-bg, #f5f4ef);
|
|
66
|
+
--k-panel: var(--k-panel, #ffffff);
|
|
67
|
+
--k-panel-raised: var(--k-panel-raised, #fbfaf7);
|
|
68
|
+
--k-line: var(--k-line, rgba(36, 40, 46, 0.12));
|
|
69
|
+
--k-line-strong: var(--k-line-strong, rgba(36, 40, 46, 0.20));
|
|
70
|
+
--k-shadow: var(--k-shadow, 0 18px 50px -30px rgba(0, 0, 0, 0.25));
|
|
71
|
+
--k-text: var(--k-text, #202124);
|
|
72
|
+
--k-text-muted: var(--k-text-muted, #5b626b);
|
|
73
|
+
--k-text-faint: var(--k-text-faint, #707782);
|
|
74
|
+
--k-brand-contrast: var(--k-brand-contrast, #ffffff);
|
|
75
|
+
--k-positive: var(--k-positive, #168257);
|
|
76
|
+
--k-caution: var(--k-caution, #8a5a00);
|
|
77
|
+
--k-negative: var(--k-negative, #c83b3b);
|
|
78
|
+
--k-neutral: var(--k-neutral, #5f6975);
|
|
79
|
+
--k-active: var(--k-active, #3f6fd6);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
.survey-workbench-embed.theme-survey{
|
|
84
|
-
--k-bg: #06080b;
|
|
85
|
-
--k-brand: #5ce0c6;
|
|
86
|
-
--k-text-faint: #6f8095;
|
|
84
|
+
--k-bg: var(--k-bg, #06080b);
|
|
85
|
+
--k-brand: var(--k-brand, #5ce0c6);
|
|
86
|
+
--k-text-faint: var(--k-text-faint, #6f8095);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
.survey-workbench-embed.theme-console{
|
|
90
|
-
--k-font-ui: "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif;
|
|
91
|
-
--k-font-display: "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif;
|
|
92
|
-
--k-radius-sm: 0;
|
|
93
|
-
--k-radius-md: 0;
|
|
94
|
-
--k-brand: #c9ff4a;
|
|
95
|
-
--k-brand-contrast: #11120f;
|
|
96
|
-
--k-bg: #11120f;
|
|
97
|
-
--k-panel: #191b16;
|
|
98
|
-
--k-panel-raised: #20231e;
|
|
99
|
-
--k-line: #3a4035;
|
|
100
|
-
--k-line-strong: #8ea36e;
|
|
101
|
-
--k-text: #f2f0e8;
|
|
102
|
-
--k-text-muted: #a6ab9c;
|
|
103
|
-
--k-text-faint: #747a6c;
|
|
104
|
-
--k-positive: #a6d37b;
|
|
105
|
-
--k-caution: #e8c15f;
|
|
106
|
-
--k-negative: #ee776f;
|
|
107
|
-
--k-active: #84d8c8;
|
|
90
|
+
--k-font-ui: var(--k-font-ui, "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif);
|
|
91
|
+
--k-font-display: var(--k-font-display, "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif);
|
|
92
|
+
--k-radius-sm: var(--k-radius-sm, 0);
|
|
93
|
+
--k-radius-md: var(--k-radius-md, 0);
|
|
94
|
+
--k-brand: var(--k-brand, #c9ff4a);
|
|
95
|
+
--k-brand-contrast: var(--k-brand-contrast, #11120f);
|
|
96
|
+
--k-bg: var(--k-bg, #11120f);
|
|
97
|
+
--k-panel: var(--k-panel, #191b16);
|
|
98
|
+
--k-panel-raised: var(--k-panel-raised, #20231e);
|
|
99
|
+
--k-line: var(--k-line, #3a4035);
|
|
100
|
+
--k-line-strong: var(--k-line-strong, #8ea36e);
|
|
101
|
+
--k-text: var(--k-text, #f2f0e8);
|
|
102
|
+
--k-text-muted: var(--k-text-muted, #a6ab9c);
|
|
103
|
+
--k-text-faint: var(--k-text-faint, #747a6c);
|
|
104
|
+
--k-positive: var(--k-positive, #a6d37b);
|
|
105
|
+
--k-caution: var(--k-caution, #e8c15f);
|
|
106
|
+
--k-negative: var(--k-negative, #ee776f);
|
|
107
|
+
--k-active: var(--k-active, #84d8c8);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
.survey-workbench-embed.theme-flow{
|
|
111
|
-
--k-brand: #2f88a6;
|
|
111
|
+
--k-brand: var(--k-brand, #2f88a6);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
.survey-workbench-embed.theme-surface{
|
|
115
|
-
--k-brand: #14a37a;
|
|
115
|
+
--k-brand: var(--k-brand, #14a37a);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
.survey-workbench-embed[data-theme="light"].theme-survey,
|
|
119
119
|
.survey-workbench-embed[data-theme="light"] .theme-survey{
|
|
120
|
-
--k-brand: #16806f;
|
|
120
|
+
--k-brand: var(--k-brand, #16806f);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
.survey-workbench-embed[data-theme="light"].theme-console,
|
|
124
124
|
.survey-workbench-embed[data-theme="light"] .theme-console{
|
|
125
|
-
--k-brand: #6c9400;
|
|
126
|
-
--k-brand-contrast: #ffffff;
|
|
127
|
-
--k-bg: #f3f5eb;
|
|
128
|
-
--k-panel: #fbfcf7;
|
|
129
|
-
--k-panel-raised: #eef2e6;
|
|
130
|
-
--k-line: #ccd5bf;
|
|
131
|
-
--k-line-strong: #8fa36f;
|
|
132
|
-
--k-text: #1e2319;
|
|
133
|
-
--k-text-muted: #596250;
|
|
134
|
-
--k-text-faint: #77816d;
|
|
135
|
-
--k-positive: #2f7d32;
|
|
136
|
-
--k-caution: #8a6500;
|
|
137
|
-
--k-negative: #b93a36;
|
|
138
|
-
--k-active: #247f75;
|
|
125
|
+
--k-brand: var(--k-brand, #6c9400);
|
|
126
|
+
--k-brand-contrast: var(--k-brand-contrast, #ffffff);
|
|
127
|
+
--k-bg: var(--k-bg, #f3f5eb);
|
|
128
|
+
--k-panel: var(--k-panel, #fbfcf7);
|
|
129
|
+
--k-panel-raised: var(--k-panel-raised, #eef2e6);
|
|
130
|
+
--k-line: var(--k-line, #ccd5bf);
|
|
131
|
+
--k-line-strong: var(--k-line-strong, #8fa36f);
|
|
132
|
+
--k-text: var(--k-text, #1e2319);
|
|
133
|
+
--k-text-muted: var(--k-text-muted, #596250);
|
|
134
|
+
--k-text-faint: var(--k-text-faint, #77816d);
|
|
135
|
+
--k-positive: var(--k-positive, #2f7d32);
|
|
136
|
+
--k-caution: var(--k-caution, #8a6500);
|
|
137
|
+
--k-negative: var(--k-negative, #b93a36);
|
|
138
|
+
--k-active: var(--k-active, #247f75);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
.survey-workbench-embed[data-theme="light"].theme-flow,
|
|
142
142
|
.survey-workbench-embed[data-theme="light"] .theme-flow{
|
|
143
|
-
--k-brand: #1f6f88;
|
|
143
|
+
--k-brand: var(--k-brand, #1f6f88);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
.survey-workbench-embed[data-theme="light"].theme-surface,
|
|
147
147
|
.survey-workbench-embed[data-theme="light"] .theme-surface{
|
|
148
|
-
--k-brand: #0f6b52;
|
|
148
|
+
--k-brand: var(--k-brand, #0f6b52);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
|
|
@@ -434,6 +434,7 @@
|
|
|
434
434
|
.survey-workbench-embed .field[data-state="accepted"] .stripe{ background: var(--k-positive); }
|
|
435
435
|
.survey-workbench-embed .field[data-state="kept"] .stripe{ background: var(--k-faint); }
|
|
436
436
|
.survey-workbench-embed .field[data-state="rejected"] .stripe{ background: var(--k-negative); }
|
|
437
|
+
.survey-workbench-embed .field[data-state="could-not-confirm"] .stripe{ background: var(--k-caution); }
|
|
437
438
|
.survey-workbench-embed .field[data-state="review"] .stripe{ background: var(--k-brand); }
|
|
438
439
|
|
|
439
440
|
.survey-workbench-embed .fbody{
|
|
@@ -488,6 +489,7 @@
|
|
|
488
489
|
.survey-workbench-embed .chip.accepted{ color: var(--k-positive); background: var(--k-positive-wash); }
|
|
489
490
|
.survey-workbench-embed .chip.kept{ color: var(--k-muted); background: var(--k-sunken); }
|
|
490
491
|
.survey-workbench-embed .chip.rejected{ color: var(--k-negative); background: var(--k-negative-wash); }
|
|
492
|
+
.survey-workbench-embed .chip.could-not-confirm{ color: var(--k-caution); background: var(--k-caution-wash); }
|
|
491
493
|
|
|
492
494
|
.survey-workbench-embed .frow1 .push{
|
|
493
495
|
margin-left: auto;
|
|
@@ -752,6 +754,12 @@
|
|
|
752
754
|
background: var(--k-panel-raised, var(--k-raised));
|
|
753
755
|
}
|
|
754
756
|
|
|
757
|
+
.survey-workbench-embed .btn.unconfirmed{
|
|
758
|
+
border-color: var(--k-caution);
|
|
759
|
+
color: var(--k-caution);
|
|
760
|
+
background: var(--k-caution-wash);
|
|
761
|
+
}
|
|
762
|
+
|
|
755
763
|
/* "Suggestion was wrong" — optional quiet calibration toggle on Keep current.
|
|
756
764
|
Ticking it turns a keep-current decision into the reject signal. Not a third action. */
|
|
757
765
|
.survey-workbench-embed .wrong{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canonicalJson } from "./canonical.js";
|
|
2
|
+
import { assertReviewResolutionConsistency } from "../producer-discipline.js";
|
|
2
3
|
import { candidateForDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewWorkbenchState, defaultReviewSessionName, effectiveValueForDecision, initialReviewQueueSessionState, replayReviewSessionEvents, reviewWorkbenchSessionStorageKey, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
3
4
|
import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
4
5
|
import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
|
|
@@ -16,13 +17,16 @@ export function buildReviewDecision(state) {
|
|
|
16
17
|
return undefined;
|
|
17
18
|
}
|
|
18
19
|
const definition = workbenchDecisionDefinitions[state.decision];
|
|
20
|
+
if (state.decision === "could-not-confirm" && !state.note.trim()) {
|
|
21
|
+
throw new Error("Could not confirm requires a non-empty reason.");
|
|
22
|
+
}
|
|
19
23
|
const candidate = candidateForDecision(state.item, state.decision);
|
|
20
24
|
const projection = {
|
|
21
25
|
...candidate.projection,
|
|
22
26
|
reviewOutcomeId: candidate.projection?.reviewOutcomeId
|
|
23
27
|
?? `${state.item.metadata.name}:${state.decision}:review-outcome`,
|
|
24
28
|
};
|
|
25
|
-
|
|
29
|
+
const reviewDecision = {
|
|
26
30
|
apiVersion: reviewResourceApiVersion,
|
|
27
31
|
kind: "ReviewDecision",
|
|
28
32
|
metadata: {
|
|
@@ -34,6 +38,13 @@ export function buildReviewDecision(state) {
|
|
|
34
38
|
reviewItemName: state.item.metadata.name,
|
|
35
39
|
candidateId: candidate.id,
|
|
36
40
|
status: definition.status,
|
|
41
|
+
...(state.decision === "could-not-confirm"
|
|
42
|
+
? {
|
|
43
|
+
resolution: "could_not_confirm",
|
|
44
|
+
resolutionReason: state.note.trim(),
|
|
45
|
+
...(state.attemptEvidenceIds?.length ? { attemptEvidenceIds: [...state.attemptEvidenceIds] } : {}),
|
|
46
|
+
}
|
|
47
|
+
: {}),
|
|
37
48
|
actor: {
|
|
38
49
|
id: state.actorId,
|
|
39
50
|
},
|
|
@@ -47,6 +58,15 @@ export function buildReviewDecision(state) {
|
|
|
47
58
|
appliedToClaimIds: candidate.projection?.claimId ? [candidate.projection.claimId] : undefined,
|
|
48
59
|
},
|
|
49
60
|
};
|
|
61
|
+
assertReviewResolutionConsistency(`ReviewDecision ${reviewDecision.metadata.name}`, {
|
|
62
|
+
status: reviewDecision.spec.status,
|
|
63
|
+
resolution: reviewDecision.spec.resolution,
|
|
64
|
+
resolutionReason: reviewDecision.spec.resolutionReason,
|
|
65
|
+
attemptEvidenceIds: reviewDecision.spec.attemptEvidenceIds,
|
|
66
|
+
actor: reviewDecision.spec.actor?.id,
|
|
67
|
+
reviewedAt: reviewDecision.spec.reviewedAt,
|
|
68
|
+
});
|
|
69
|
+
return reviewDecision;
|
|
50
70
|
}
|
|
51
71
|
/**
|
|
52
72
|
* Stable versioned prompt identifier for the workbench decision card control.
|
|
@@ -177,6 +197,9 @@ function duplicateTargetIssues(items) {
|
|
|
177
197
|
return issues;
|
|
178
198
|
}
|
|
179
199
|
function mapReviewApplyResultToActions(input) {
|
|
200
|
+
if (input.result.decision === "could-not-confirm") {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
180
203
|
const context = buildReviewApplyActionContext(input.result, input.itemByName, input.issues);
|
|
181
204
|
if (!context || input.options.skip?.(context)) {
|
|
182
205
|
return [];
|
|
@@ -442,6 +465,8 @@ function fieldCardState(item, decision) {
|
|
|
442
465
|
return "kept";
|
|
443
466
|
if (decision === "reject-proposed")
|
|
444
467
|
return "rejected";
|
|
468
|
+
if (decision === "could-not-confirm")
|
|
469
|
+
return "could-not-confirm";
|
|
445
470
|
if (item.spec.candidateSetStatus === "resolved")
|
|
446
471
|
return "kept";
|
|
447
472
|
return "review";
|
|
@@ -451,6 +476,7 @@ function chipLabel(state) {
|
|
|
451
476
|
case "accepted": return "Accepted";
|
|
452
477
|
case "kept": return "Kept current";
|
|
453
478
|
case "rejected": return "Kept — flagged wrong";
|
|
479
|
+
case "could-not-confirm": return "Could not confirm";
|
|
454
480
|
default: return "Needs review";
|
|
455
481
|
}
|
|
456
482
|
}
|
|
@@ -540,6 +566,7 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
540
566
|
<input type="checkbox" class="wrongbox" data-testid="wrong-toggle" data-item-name="${escapeHtml(item.metadata.name)}">
|
|
541
567
|
Suggestion was wrong
|
|
542
568
|
</label>
|
|
569
|
+
<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button>
|
|
543
570
|
</div>
|
|
544
571
|
<div class="decided">
|
|
545
572
|
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state)}</span>
|
|
@@ -813,7 +840,7 @@ function renderProducerFeedbackTags(item) {
|
|
|
813
840
|
`;
|
|
814
841
|
}
|
|
815
842
|
function renderFooterTally(session) {
|
|
816
|
-
const counts = { review: 0, accepted: 0, kept: 0, rejected: 0 };
|
|
843
|
+
const counts = { review: 0, accepted: 0, kept: 0, rejected: 0, "could-not-confirm": 0 };
|
|
817
844
|
for (const item of session.items) {
|
|
818
845
|
const state = fieldCardState(item, session.decisionsByItemName[item.metadata.name]);
|
|
819
846
|
counts[state] += 1;
|
|
@@ -824,6 +851,7 @@ function renderFooterTally(session) {
|
|
|
824
851
|
<span>Accepted <b data-testid="tally-accepted">${counts.accepted}</b></span>
|
|
825
852
|
<span>Kept <b data-testid="tally-kept">${counts.kept}</b></span>
|
|
826
853
|
<span>Flagged <b data-testid="tally-rejected">${counts.rejected}</b></span>
|
|
854
|
+
<span>Unconfirmed <b data-testid="tally-could-not-confirm">${counts["could-not-confirm"]}</b></span>
|
|
827
855
|
<span>Remaining <b data-testid="tally-review">${counts.review}</b></span>
|
|
828
856
|
</div>
|
|
829
857
|
</footer>
|
|
@@ -973,10 +1001,13 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
973
1001
|
// effectiveValue from snapshot + events alone. setDecision updates the
|
|
974
1002
|
// session before this runs, so the edit is already in state.
|
|
975
1003
|
const editedValue = itemName && decision === "accept-proposed" ? session.editedValuesByItemName?.[itemName] : undefined;
|
|
1004
|
+
const attemptEvidenceIds = itemName && decision === "could-not-confirm"
|
|
1005
|
+
? session.attemptEvidenceIdsByItemName?.[itemName]
|
|
1006
|
+
: undefined;
|
|
976
1007
|
const defaultData = decision
|
|
977
1008
|
? (editedValue !== undefined
|
|
978
1009
|
? { workbenchDecision: decision, workbenchEditedValue: editedValue }
|
|
979
|
-
: { workbenchDecision: decision })
|
|
1010
|
+
: { workbenchDecision: decision, ...(attemptEvidenceIds?.length ? { attemptEvidenceIds } : {}) })
|
|
980
1011
|
: undefined;
|
|
981
1012
|
events = [
|
|
982
1013
|
...events,
|
|
@@ -989,6 +1020,13 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
989
1020
|
reviewDecisionName: item && decision ? `${item.metadata.name}-${decision}` : undefined,
|
|
990
1021
|
candidateId: candidate?.id,
|
|
991
1022
|
status: definition?.status,
|
|
1023
|
+
...(decision === "could-not-confirm"
|
|
1024
|
+
? {
|
|
1025
|
+
resolution: "could_not_confirm",
|
|
1026
|
+
resolutionReason: note?.trim(),
|
|
1027
|
+
...(attemptEvidenceIds ? { attemptEvidenceIds: [...attemptEvidenceIds] } : {}),
|
|
1028
|
+
}
|
|
1029
|
+
: {}),
|
|
992
1030
|
rationale: note,
|
|
993
1031
|
data: dataOverride ?? defaultData,
|
|
994
1032
|
}),
|
|
@@ -998,6 +1036,9 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
998
1036
|
session = update(session);
|
|
999
1037
|
};
|
|
1000
1038
|
const setDecision = (itemName, decision, rawEditedValue) => {
|
|
1039
|
+
if (decision === "could-not-confirm" && !session.notesByItemName[itemName]?.trim()) {
|
|
1040
|
+
throw new Error("Could not confirm requires a non-empty reason.");
|
|
1041
|
+
}
|
|
1001
1042
|
applySessionUpdate((current) => {
|
|
1002
1043
|
const item = current.items.find((entry) => entry.metadata.name === itemName);
|
|
1003
1044
|
const proposed = item?.spec.candidates.find((candidate) => candidate.role === "proposed");
|
|
@@ -1025,11 +1066,14 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1025
1066
|
delete remainingDecisions[itemName];
|
|
1026
1067
|
const remainingEdits = { ...current.editedValuesByItemName };
|
|
1027
1068
|
delete remainingEdits[itemName];
|
|
1069
|
+
const remainingAttemptEvidenceIds = { ...current.attemptEvidenceIdsByItemName };
|
|
1070
|
+
delete remainingAttemptEvidenceIds[itemName];
|
|
1028
1071
|
return {
|
|
1029
1072
|
...current,
|
|
1030
1073
|
activeItemName: itemName,
|
|
1031
1074
|
decisionsByItemName: remainingDecisions,
|
|
1032
1075
|
editedValuesByItemName: remainingEdits,
|
|
1076
|
+
attemptEvidenceIdsByItemName: remainingAttemptEvidenceIds,
|
|
1033
1077
|
};
|
|
1034
1078
|
});
|
|
1035
1079
|
appendEvent("decision-changed", itemName, { workbenchDecision: null });
|
|
@@ -1082,6 +1126,9 @@ function queueSessionFromStartState(startState) {
|
|
|
1082
1126
|
editedValuesByItemName: startState.editedValue !== undefined
|
|
1083
1127
|
? { [startState.item.metadata.name]: startState.editedValue }
|
|
1084
1128
|
: {},
|
|
1129
|
+
...(startState.attemptEvidenceIds?.length
|
|
1130
|
+
? { attemptEvidenceIdsByItemName: { [startState.item.metadata.name]: [...startState.attemptEvidenceIds] } }
|
|
1131
|
+
: {}),
|
|
1085
1132
|
reviewedAt: startState.reviewedAt,
|
|
1086
1133
|
actorId: startState.actorId,
|
|
1087
1134
|
};
|
|
@@ -1125,6 +1172,23 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1125
1172
|
controller.renderCurrentState();
|
|
1126
1173
|
});
|
|
1127
1174
|
});
|
|
1175
|
+
root.querySelectorAll("[data-testid='could-not-confirm']").forEach((button) => {
|
|
1176
|
+
button.addEventListener("click", () => {
|
|
1177
|
+
const itemName = button.dataset.itemName ?? "";
|
|
1178
|
+
const note = controller.currentSession().notesByItemName[itemName]?.trim() ?? "";
|
|
1179
|
+
const field = button.closest("[data-testid='review-field']");
|
|
1180
|
+
const textarea = field?.querySelector("[data-testid='reviewer-note']");
|
|
1181
|
+
if (!note) {
|
|
1182
|
+
textarea?.setCustomValidity?.("A reason is required when you could not confirm.");
|
|
1183
|
+
textarea?.reportValidity?.();
|
|
1184
|
+
textarea?.focus();
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
textarea?.setCustomValidity?.("");
|
|
1188
|
+
controller.setDecision(itemName, "could-not-confirm");
|
|
1189
|
+
controller.renderCurrentState();
|
|
1190
|
+
});
|
|
1191
|
+
});
|
|
1128
1192
|
root.querySelectorAll("[data-testid='undo-decision']").forEach((button) => {
|
|
1129
1193
|
button.addEventListener("click", () => {
|
|
1130
1194
|
controller.clearDecision(button.dataset.itemName ?? "");
|
|
@@ -1133,6 +1197,7 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1133
1197
|
});
|
|
1134
1198
|
root.querySelectorAll("[data-testid='reviewer-note']").forEach((textarea) => {
|
|
1135
1199
|
textarea.addEventListener("input", () => {
|
|
1200
|
+
textarea.setCustomValidity?.("");
|
|
1136
1201
|
const itemName = textarea.dataset.itemName ?? "";
|
|
1137
1202
|
controller.updateReviewerNote(itemName, textarea.value);
|
|
1138
1203
|
refreshAuditPayloadForItem(textarea, controller, itemName);
|
|
@@ -283,6 +283,7 @@ h1, h2, h3, p {
|
|
|
283
283
|
.field[data-state="accepted"] .stripe { background: var(--k-positive); }
|
|
284
284
|
.field[data-state="kept"] .stripe { background: var(--k-faint); }
|
|
285
285
|
.field[data-state="rejected"] .stripe { background: var(--k-negative); }
|
|
286
|
+
.field[data-state="could-not-confirm"] .stripe { background: var(--k-caution); }
|
|
286
287
|
.field[data-state="review"] .stripe { background: var(--k-brand); }
|
|
287
288
|
|
|
288
289
|
.fbody {
|
|
@@ -337,6 +338,7 @@ h1, h2, h3, p {
|
|
|
337
338
|
.chip.accepted { color: var(--k-positive); background: var(--k-positive-wash); }
|
|
338
339
|
.chip.kept { color: var(--k-muted); background: var(--k-sunken); }
|
|
339
340
|
.chip.rejected { color: var(--k-negative); background: var(--k-negative-wash); }
|
|
341
|
+
.chip.could-not-confirm { color: var(--k-caution); background: var(--k-caution-wash); }
|
|
340
342
|
|
|
341
343
|
.frow1 .push {
|
|
342
344
|
margin-left: auto;
|
|
@@ -601,6 +603,12 @@ h1, h2, h3, p {
|
|
|
601
603
|
background: var(--k-panel-raised, var(--k-raised));
|
|
602
604
|
}
|
|
603
605
|
|
|
606
|
+
.btn.unconfirmed {
|
|
607
|
+
border-color: var(--k-caution);
|
|
608
|
+
color: var(--k-caution);
|
|
609
|
+
background: var(--k-caution-wash);
|
|
610
|
+
}
|
|
611
|
+
|
|
604
612
|
/* "Suggestion was wrong" — optional quiet calibration toggle on Keep current.
|
|
605
613
|
Ticking it turns a keep-current decision into the reject signal. Not a third action. */
|
|
606
614
|
.wrong {
|
package/dist/src/to-surface.js
CHANGED
|
@@ -27,11 +27,16 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
27
27
|
const extraction = requireMapValue(extractions, candidate.extractionId, "extraction");
|
|
28
28
|
const rawSource = requireMapValue(rawSources, extraction.sourceId, "raw source");
|
|
29
29
|
const review = selectReview(reviewsByCandidateSet.get(candidateSet.id) ?? [], candidate.id);
|
|
30
|
-
const
|
|
31
|
-
|
|
30
|
+
const projectionReview = review?.resolution === "could_not_confirm" ? undefined : review;
|
|
31
|
+
const unreviewedStatus = statusFor({ candidateSet, candidate });
|
|
32
|
+
const status = projection.status
|
|
33
|
+
?? (review?.resolution === "could_not_confirm"
|
|
34
|
+
? (unreviewedStatus === "disputed" ? unreviewedStatus : review.status)
|
|
35
|
+
: statusFor({ candidateSet, candidate, review }));
|
|
36
|
+
assertProducerDiscipline({ status, review, candidateSet, extraction, rawSource, projection });
|
|
32
37
|
const claimValue = projection.value ?? candidate.value;
|
|
33
38
|
const createdAt = projection.createdAt ?? extraction.extractedAt;
|
|
34
|
-
const updatedAt = projection.updatedAt ??
|
|
39
|
+
const updatedAt = projection.updatedAt ?? projectionReview?.reviewedAt ?? input.generatedAt;
|
|
35
40
|
const evidenceId = `${projection.id}.evidence.source`;
|
|
36
41
|
const claim = {
|
|
37
42
|
id: projection.id,
|
|
@@ -57,7 +62,15 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
57
62
|
},
|
|
58
63
|
metadata: {
|
|
59
64
|
...projection.metadata,
|
|
60
|
-
survey: buildSurveyMetadata({
|
|
65
|
+
survey: buildSurveyMetadata({
|
|
66
|
+
projection,
|
|
67
|
+
rawSource,
|
|
68
|
+
extraction,
|
|
69
|
+
candidateSet,
|
|
70
|
+
candidate,
|
|
71
|
+
review: projectionReview,
|
|
72
|
+
comfortZoneReview: review,
|
|
73
|
+
}),
|
|
61
74
|
},
|
|
62
75
|
};
|
|
63
76
|
// Promote the review's comfort-zone signal — and, when calibration is
|
|
@@ -82,7 +95,7 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
82
95
|
}
|
|
83
96
|
: undefined;
|
|
84
97
|
const affirmedConclusion = status === "verified" || status === "assumed";
|
|
85
|
-
const calibrated = calibrationMetrics &&
|
|
98
|
+
const calibrated = calibrationMetrics && projectionReview && affirmedConclusion
|
|
86
99
|
? lookupCalibratedValue(calibrationMetrics, extraction.extractor, extraction.target, calibrationMinSamples)
|
|
87
100
|
: undefined;
|
|
88
101
|
if (comfortZone || calibrated) {
|
|
@@ -91,13 +104,16 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
91
104
|
...(comfortZone ? { comfortZone } : {}),
|
|
92
105
|
};
|
|
93
106
|
}
|
|
94
|
-
|
|
107
|
+
// Could-not-confirm stays byte-quiet at the Surface boundary. In particular,
|
|
108
|
+
// do not attach an anchor whose public observedAt would disclose review time;
|
|
109
|
+
// callers can persist/recompute Survey's canonical v3 proof separately.
|
|
110
|
+
if (options.reviewProofs && projectionReview) {
|
|
95
111
|
claim.currentIntegrityAnchor = buildReviewProofAnchor({
|
|
96
112
|
rawSource,
|
|
97
113
|
extraction,
|
|
98
114
|
candidate,
|
|
99
115
|
candidateSet,
|
|
100
|
-
reviewOutcome:
|
|
116
|
+
reviewOutcome: projectionReview,
|
|
101
117
|
claim: {
|
|
102
118
|
...projection,
|
|
103
119
|
value: claimValue,
|
|
@@ -129,16 +145,16 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
129
145
|
confidence: candidate.confidence ?? extraction.confidence,
|
|
130
146
|
},
|
|
131
147
|
});
|
|
132
|
-
const rationale =
|
|
148
|
+
const rationale = projectionReview?.rationale ?? candidateSet.rationale;
|
|
133
149
|
events.push({
|
|
134
150
|
id: `${projection.id}.event.${status}`,
|
|
135
151
|
claimId: projection.id,
|
|
136
152
|
status,
|
|
137
|
-
actor: projection.actor ??
|
|
153
|
+
actor: projection.actor ?? projectionReview?.actor ?? projection.collectedBy,
|
|
138
154
|
method: projection.eventMethod ?? eventMethodFor(status, candidateSet),
|
|
139
|
-
evidenceIds:
|
|
140
|
-
createdAt:
|
|
141
|
-
verifiedAt: status === "verified" || status === "assumed" ?
|
|
155
|
+
evidenceIds: projectionReview?.evidenceIds?.length ? projectionReview.evidenceIds : [evidenceId],
|
|
156
|
+
createdAt: projectionReview?.reviewedAt ?? input.generatedAt,
|
|
157
|
+
verifiedAt: status === "verified" || status === "assumed" ? projectionReview?.reviewedAt ?? input.generatedAt : undefined,
|
|
142
158
|
notes: rationale,
|
|
143
159
|
});
|
|
144
160
|
}
|
|
@@ -357,11 +373,11 @@ function buildSurveyMetadata(input) {
|
|
|
357
373
|
},
|
|
358
374
|
}
|
|
359
375
|
: {}),
|
|
360
|
-
...(input.
|
|
376
|
+
...(input.comfortZoneReview?.withinComfortZone === false
|
|
361
377
|
? {
|
|
362
378
|
comfortZone: {
|
|
363
379
|
withinComfortZone: false,
|
|
364
|
-
...(input.
|
|
380
|
+
...(input.comfortZoneReview.comfortZoneNote ? { note: input.comfortZoneReview.comfortZoneNote } : {}),
|
|
365
381
|
},
|
|
366
382
|
}
|
|
367
383
|
: {}),
|
|
@@ -384,6 +400,7 @@ function assertProducerDiscipline(input) {
|
|
|
384
400
|
subject: `Claim ${input.projection.id}`,
|
|
385
401
|
status: input.status,
|
|
386
402
|
review: input.review,
|
|
403
|
+
candidateSetStatus: input.candidateSet.status,
|
|
387
404
|
});
|
|
388
405
|
if (input.rawSource.kind !== "manual-entry" && !input.extraction.locator) {
|
|
389
406
|
throw new Error(`Claim ${input.projection.id} needs a source locator for ${input.rawSource.kind}`);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export interface CandidateSet {
|
|
|
49
49
|
metadata?: Record<string, unknown>;
|
|
50
50
|
}
|
|
51
51
|
export type ReviewStatus = Extract<TrustStatus, "verified" | "assumed" | "rejected" | "proposed">;
|
|
52
|
+
export type ReviewResolution = "accepted" | "rejected" | "held" | "could_not_confirm";
|
|
52
53
|
export type ReviewAuthorizingKind = "explicit-statement" | "exchange" | "authorized-action";
|
|
53
54
|
export interface ReviewAuthorizingExplicitStatement {
|
|
54
55
|
kind: "explicit-statement";
|
|
@@ -74,6 +75,13 @@ export interface ReviewOutcome {
|
|
|
74
75
|
candidateSetId: string;
|
|
75
76
|
candidateId?: string;
|
|
76
77
|
status: ReviewStatus;
|
|
78
|
+
/** Explicit terminal resolution for this review round. Absent outcomes retain
|
|
79
|
+
* the legacy status-inference behavior. */
|
|
80
|
+
resolution?: ReviewResolution;
|
|
81
|
+
/** Required, non-empty explanation when resolution is `could_not_confirm`. */
|
|
82
|
+
resolutionReason?: string;
|
|
83
|
+
/** Evidence of attempts made before the reviewer concluded they could not confirm. */
|
|
84
|
+
attemptEvidenceIds?: string[];
|
|
77
85
|
actor?: string;
|
|
78
86
|
reviewedAt?: string;
|
|
79
87
|
rationale?: string;
|
package/package.json
CHANGED