@kontourai/survey 6.0.0 → 7.0.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/dist/src/candidate-verification.d.ts +175 -0
- package/dist/src/candidate-verification.js +334 -0
- package/dist/src/canonical-reviewed-trust-input.js +30 -1
- package/dist/src/console/review-console-server.js +22 -4
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.js +3 -2
- package/dist/src/mcp/review-mcp.js +161 -44
- package/dist/src/review-resource.d.ts +16 -0
- package/dist/src/review-session-file.d.ts +7 -0
- package/dist/src/review-workbench/audit-rows.d.ts +1 -1
- package/dist/src/review-workbench/audit-rows.js +1 -0
- package/dist/src/review-workbench/queue-binding.d.ts +45 -2
- package/dist/src/review-workbench/queue-binding.js +45 -0
- package/dist/src/review-workbench/review-presentation.d.ts +60 -0
- package/dist/src/review-workbench/review-presentation.js +107 -10
- package/dist/src/review-workbench/review-queue-session.d.ts +47 -4
- package/dist/src/review-workbench/review-queue-session.js +73 -8
- package/dist/src/review-workbench/review-session-replay.d.ts +11 -1
- package/dist/src/review-workbench/review-session-replay.js +35 -1
- package/dist/src/review-workbench/review-workbench-css.generated.js +96 -1
- package/dist/src/review-workbench/review-workbench-element.d.ts +10 -0
- package/dist/src/review-workbench/review-workbench-element.js +21 -3
- package/dist/src/review-workbench/review-workbench.css +96 -1
- package/dist/src/review-workbench/review-workbench.d.ts +28 -8
- package/dist/src/review-workbench/review-workbench.js +174 -63
- package/dist/src/review-workbench/review-workbench.standalone.css +96 -1
- package/dist/src/review-workbench/server-review-session.d.ts +18 -0
- package/dist/src/review-workbench/server-review-session.js +20 -2
- package/dist/src/surface-reviewed-extraction.d.ts +23 -0
- package/dist/src/surface-reviewed-extraction.js +64 -1
- package/dist/src/to-surface.js +32 -3
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReviewQueueSessionState } from "./review-queue-session.js";
|
|
2
2
|
import { reviewResourceApiVersion, type ReviewItem } from "../review-resource.js";
|
|
3
|
-
import { type ExtractionEnvelopeImportResult } from "../extraction-envelope.js";
|
|
3
|
+
import { type ExtractionEnvelopeImport, type ExtractionEnvelopeImportResult } from "../extraction-envelope.js";
|
|
4
4
|
/**
|
|
5
5
|
* The durable attestation record. Serializable; a consumer stores it beside the
|
|
6
6
|
* queue when the round opens and presents it, unchanged, at every later
|
|
@@ -78,7 +78,7 @@ export declare function bindReviewQueue(snapshot: ReviewQueueSessionState, optio
|
|
|
78
78
|
*/
|
|
79
79
|
export declare function validateReviewQueueBinding(binding: ReviewQueueBinding, snapshot: ReviewQueueSessionState, options?: ValidateReviewQueueBindingOptions): ReviewQueueBindingIssue[];
|
|
80
80
|
export declare function assertReviewQueueBinding(binding: ReviewQueueBinding, snapshot: ReviewQueueSessionState, options?: ValidateReviewQueueBindingOptions): void;
|
|
81
|
-
export type ReviewQueueExtractionIssueCode = "import-not-grounded" | "empty-queue" | "item-missing-from-queue" | "item-not-in-extraction" | "item-diverges-from-extraction";
|
|
81
|
+
export type ReviewQueueExtractionIssueCode = "import-invalid" | "import-not-grounded" | "empty-queue" | "item-missing-from-queue" | "item-not-in-extraction" | "item-diverges-from-extraction";
|
|
82
82
|
export interface ReviewQueueExtractionIssue {
|
|
83
83
|
readonly code: ReviewQueueExtractionIssueCode;
|
|
84
84
|
readonly message: string;
|
|
@@ -124,4 +124,47 @@ export declare class UnattestedExtractionQueueError extends Error {
|
|
|
124
124
|
* cross-check the label.
|
|
125
125
|
*/
|
|
126
126
|
export declare function validateReviewQueueAgainstExtractionImport(items: readonly ReviewItem[], importResult: ExtractionEnvelopeImportResult): ReviewQueueExtractionIssue[];
|
|
127
|
+
/**
|
|
128
|
+
* What a reload path can say about a stored queue and the extraction import it
|
|
129
|
+
* came from:
|
|
130
|
+
*
|
|
131
|
+
* - `attested`: an import record was supplied and the queue matches it (see
|
|
132
|
+
* {@link validateReviewQueueAgainstExtractionImport}).
|
|
133
|
+
* - `diverges`: an import record was supplied and the queue does not match it,
|
|
134
|
+
* or the record itself is invalid. The queue must not be reviewed.
|
|
135
|
+
* - `unverified`: items carry the extraction-envelope binding but no import
|
|
136
|
+
* record was supplied, so nothing checked them. A rival value deleted from
|
|
137
|
+
* the stored queue would not show; every surface says so.
|
|
138
|
+
* - `not-extraction`: no item carries the binding and no record was supplied;
|
|
139
|
+
* there is no import to check against.
|
|
140
|
+
*
|
|
141
|
+
* Detection reads the binding the import writes on every item and candidate.
|
|
142
|
+
* A writer that strips it from every item also strips every other extraction
|
|
143
|
+
* fact from the queue; keeping the stored import record beside the queue is
|
|
144
|
+
* what makes a reload path able to check it.
|
|
145
|
+
*/
|
|
146
|
+
export type ReviewQueueExtractionAttestation = {
|
|
147
|
+
readonly state: "attested";
|
|
148
|
+
} | {
|
|
149
|
+
readonly state: "not-extraction";
|
|
150
|
+
} | {
|
|
151
|
+
readonly state: "unverified";
|
|
152
|
+
readonly itemNames: readonly string[];
|
|
153
|
+
readonly message: string;
|
|
154
|
+
} | {
|
|
155
|
+
readonly state: "diverges";
|
|
156
|
+
readonly issues: readonly ReviewQueueExtractionIssue[];
|
|
157
|
+
readonly message: string;
|
|
158
|
+
};
|
|
159
|
+
/** Whether a ReviewItem, or any of its candidates, carries the extraction-envelope binding. */
|
|
160
|
+
export declare function reviewItemCarriesExtractionBinding(item: ReviewItem): boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Check a queue a reload path is about to present against its extraction
|
|
163
|
+
* import. Pass the import record (or import result) stored beside the queue;
|
|
164
|
+
* pass `undefined` when the path has none, and the result says whether that
|
|
165
|
+
* leaves the queue unverified.
|
|
166
|
+
*/
|
|
167
|
+
export declare function attestReviewQueueExtraction(items: readonly ReviewItem[], extractionImport: ExtractionEnvelopeImport | ExtractionEnvelopeImportResult | undefined): ReviewQueueExtractionAttestation;
|
|
168
|
+
/** The notice every surface shows for an `unverified` queue. */
|
|
169
|
+
export declare function unverifiedExtractionQueueSentence(itemCount: number): string;
|
|
127
170
|
export declare function assertReviewQueueAgainstExtractionImport(items: readonly ReviewItem[], importResult: ExtractionEnvelopeImportResult): void;
|
|
@@ -276,6 +276,51 @@ export function validateReviewQueueAgainstExtractionImport(items, importResult)
|
|
|
276
276
|
}
|
|
277
277
|
return issues;
|
|
278
278
|
}
|
|
279
|
+
const EXTRACTION_ENVELOPE_PRODUCER = "survey.kontourai.io/extraction-envelope";
|
|
280
|
+
/** Whether a ReviewItem, or any of its candidates, carries the extraction-envelope binding. */
|
|
281
|
+
export function reviewItemCarriesExtractionBinding(item) {
|
|
282
|
+
const bound = (producer) => producer !== undefined && producer !== null && typeof producer === "object" && Object.hasOwn(producer, EXTRACTION_ENVELOPE_PRODUCER);
|
|
283
|
+
return bound(item.metadata?.producer) || (Array.isArray(item.spec?.candidates) && item.spec.candidates.some((candidate) => bound(candidate?.producer)));
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Check a queue a reload path is about to present against its extraction
|
|
287
|
+
* import. Pass the import record (or import result) stored beside the queue;
|
|
288
|
+
* pass `undefined` when the path has none, and the result says whether that
|
|
289
|
+
* leaves the queue unverified.
|
|
290
|
+
*/
|
|
291
|
+
export function attestReviewQueueExtraction(items, extractionImport) {
|
|
292
|
+
if (extractionImport === undefined || extractionImport === null) {
|
|
293
|
+
const itemNames = items.filter(reviewItemCarriesExtractionBinding).map((item) => item.metadata.name);
|
|
294
|
+
return itemNames.length === 0
|
|
295
|
+
? { state: "not-extraction" }
|
|
296
|
+
: { state: "unverified", itemNames, message: unverifiedExtractionQueueSentence(itemNames.length) };
|
|
297
|
+
}
|
|
298
|
+
const record = isImportResult(extractionImport) ? extractionImport.record : extractionImport;
|
|
299
|
+
let issues;
|
|
300
|
+
try {
|
|
301
|
+
issues = validateReviewQueueAgainstExtractionImport(items, { record, reviewItems: [] });
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
issues = [{
|
|
305
|
+
code: "import-invalid",
|
|
306
|
+
message: `The stored extraction import is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
307
|
+
}];
|
|
308
|
+
}
|
|
309
|
+
return issues.length === 0
|
|
310
|
+
? { state: "attested" }
|
|
311
|
+
: {
|
|
312
|
+
state: "diverges",
|
|
313
|
+
issues,
|
|
314
|
+
message: `This review queue does not match the extraction import stored with it, so it cannot be reviewed: ${issues.map((issue) => issue.message).join(" ")}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** The notice every surface shows for an `unverified` queue. */
|
|
318
|
+
export function unverifiedExtractionQueueSentence(itemCount) {
|
|
319
|
+
return `Unverified queue: ${itemCount === 1 ? "1 item came" : `${itemCount} items came`} from an extraction import, but no import record was stored with this queue, so its items were not checked against it. A rival value removed from the stored queue would not be shown.`;
|
|
320
|
+
}
|
|
321
|
+
function isImportResult(value) {
|
|
322
|
+
return typeof value === "object" && value !== null && "record" in value && !("kind" in value);
|
|
323
|
+
}
|
|
279
324
|
export function assertReviewQueueAgainstExtractionImport(items, importResult) {
|
|
280
325
|
const issues = validateReviewQueueAgainstExtractionImport(items, importResult);
|
|
281
326
|
if (issues.length > 0) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CandidateVerificationStatus, type SupportAbstainReason, type SupportVerificationMethod, type SupportVerificationResult } from "../candidate-verification.js";
|
|
1
2
|
import { type ReviewCandidate, type ReviewItem } from "../review-resource.js";
|
|
2
3
|
import type { InterpretationAnswerImpact, InterpretationReadingKind } from "../types.js";
|
|
3
4
|
import { type ReviewWorkbenchResult } from "./review-workbench.js";
|
|
@@ -56,6 +57,14 @@ export interface ReviewItemPresentation {
|
|
|
56
57
|
* disproven: every decision surface shows them. Empty for other items.
|
|
57
58
|
*/
|
|
58
59
|
readonly excludedProposals: readonly ExcludedProposalPresentation[];
|
|
60
|
+
/**
|
|
61
|
+
* Stored `excludedProposals` entries that {@link excludedProposals} cannot
|
|
62
|
+
* show: malformed entries, or every entry of an item whose envelope binding
|
|
63
|
+
* is not intact. Hiding a rival is the unsafe direction, so every surface
|
|
64
|
+
* that shows excluded proposals shows this notice too. Absent when every
|
|
65
|
+
* stored entry is shown.
|
|
66
|
+
*/
|
|
67
|
+
readonly excludedProposalsUnreadable?: ExcludedProposalsUnreadable;
|
|
59
68
|
/**
|
|
60
69
|
* For an envelope-imported item, whether its import checked excerpts
|
|
61
70
|
* against the prepared artifact. Absent for other items, and for an item
|
|
@@ -70,6 +79,16 @@ export interface ExcludedProposalPresentation {
|
|
|
70
79
|
readonly locator: string;
|
|
71
80
|
readonly excerpt: string;
|
|
72
81
|
}
|
|
82
|
+
export interface ExcludedProposalsUnreadable {
|
|
83
|
+
/**
|
|
84
|
+
* `malformed-entries`: some stored entries are not well-formed.
|
|
85
|
+
* `binding-broken`: the item's extraction binding is not intact, so none of
|
|
86
|
+
* its stored entries can be vouched for.
|
|
87
|
+
*/
|
|
88
|
+
readonly reason: "malformed-entries" | "binding-broken";
|
|
89
|
+
/** How many stored entries are not shown; absent when the stored field is not a list. */
|
|
90
|
+
readonly count?: number;
|
|
91
|
+
}
|
|
73
92
|
export interface ReviewResultPresentation {
|
|
74
93
|
readonly result: ReviewWorkbenchResult;
|
|
75
94
|
readonly item?: ReviewItem;
|
|
@@ -130,6 +149,47 @@ export declare function buildInterpretationReadingPresentation(source: Interpret
|
|
|
130
149
|
* text (the MCP item, the recorded decision prompt). Undefined when none.
|
|
131
150
|
*/
|
|
132
151
|
export declare function excludedProposalsSentence(excluded: readonly ExcludedProposalPresentation[]): string | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* One sentence saying that stored excluded proposals cannot be shown, for
|
|
154
|
+
* every surface that shows excluded proposals. Undefined when none are hidden.
|
|
155
|
+
*/
|
|
156
|
+
export declare function excludedProposalsUnreadableSentence(unreadable: ExcludedProposalsUnreadable | undefined): string | undefined;
|
|
157
|
+
export interface CandidateVerificationRecordPresentation {
|
|
158
|
+
readonly recordId: string;
|
|
159
|
+
readonly verifierId: string;
|
|
160
|
+
readonly verifierVersion: string;
|
|
161
|
+
readonly method: SupportVerificationMethod;
|
|
162
|
+
readonly result: SupportVerificationResult;
|
|
163
|
+
readonly abstainReason?: SupportAbstainReason;
|
|
164
|
+
readonly createdAt: string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* What the verifier records on one candidate say about the value under
|
|
168
|
+
* review. Only candidates that carry `verifications` get a note, so a
|
|
169
|
+
* candidate no verifier looked at shows nothing rather than a verdict.
|
|
170
|
+
*/
|
|
171
|
+
export interface CandidateVerificationNote {
|
|
172
|
+
/** Position in `item.spec.candidates`; ids are not assumed unique. */
|
|
173
|
+
readonly candidateIndex: number;
|
|
174
|
+
readonly candidateId: string;
|
|
175
|
+
readonly subjectLabel: string;
|
|
176
|
+
/** As {@link foldCandidateVerifications}: `not-evaluated`, `abstained` (only abstentions apply) or `evaluated`. */
|
|
177
|
+
readonly status: CandidateVerificationStatus;
|
|
178
|
+
readonly records: readonly CandidateVerificationRecordPresentation[];
|
|
179
|
+
/** Valid records bound to another value, candidate or input. */
|
|
180
|
+
readonly inapplicableCount: number;
|
|
181
|
+
/** Records that failed validation (edited, inconsistent or malformed); never shown as verdicts. */
|
|
182
|
+
readonly rejectedCount: number;
|
|
183
|
+
readonly sentence: string;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Verifier notes for every candidate that carries records, read against the
|
|
187
|
+
* value each surface is deciding on: the candidate's own value, or the
|
|
188
|
+
* reviewer's edit when an accept carries one (records on the proposed value do
|
|
189
|
+
* not apply to an edited value). Records are validated here, so an inconsistent
|
|
190
|
+
* record is counted and ignored on every surface alike.
|
|
191
|
+
*/
|
|
192
|
+
export declare function candidateVerificationNotes(item: ReviewItem, editedValue?: unknown): CandidateVerificationNote[];
|
|
133
193
|
export declare function buildReviewItemPresentation(item: ReviewItem, adapter?: ReviewPresentationAdapter): ReviewItemPresentation;
|
|
134
194
|
export declare function buildReviewCandidatePresentation(item: ReviewItem, candidate: ReviewCandidate, adapter?: ReviewPresentationAdapter, targetLabel?: string): ReviewCandidatePresentation;
|
|
135
195
|
export declare function buildReviewResultPresentation(result: ReviewWorkbenchResult, item: ReviewItem | undefined, adapter?: ReviewPresentationAdapter): ReviewResultPresentation;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { foldCandidateVerifications } from "../candidate-verification.js";
|
|
1
2
|
import { findSoleCandidateById } from "../review-resource.js";
|
|
2
3
|
import { formatValue } from "./review-surface-preview.js";
|
|
3
4
|
const INTERPRETATION_KIND_LABELS = {
|
|
@@ -68,20 +69,39 @@ function extractionEnvelopeBinding(item) {
|
|
|
68
69
|
});
|
|
69
70
|
return bound ? binding : undefined;
|
|
70
71
|
}
|
|
71
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* The well-formed `excludedProposals` entries of a bound envelope item, and
|
|
74
|
+
* what the item stores but cannot show: malformed entries, or every entry
|
|
75
|
+
* when the binding is not intact. Nothing stored is dropped silently.
|
|
76
|
+
*/
|
|
72
77
|
function excludedProposalsOf(item, binding, adapter) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
const metadata = item.metadata?.producer?.[EXTRACTION_ENVELOPE_PRODUCER];
|
|
79
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
80
|
+
// Candidates that still carry the envelope binding came from an import,
|
|
81
|
+
// whose item metadata is gone or replaced: what it stored is unknown.
|
|
82
|
+
const candidatesBound = item.spec.candidates.some((candidate) => candidate.producer?.[EXTRACTION_ENVELOPE_PRODUCER] !== undefined);
|
|
83
|
+
return candidatesBound ? { excludedProposals: [], excludedProposalsUnreadable: { reason: "binding-broken" } } : { excludedProposals: [] };
|
|
84
|
+
}
|
|
85
|
+
const stored = metadata.excludedProposals;
|
|
86
|
+
if (stored === undefined)
|
|
87
|
+
return { excludedProposals: [] };
|
|
88
|
+
const count = Array.isArray(stored) ? stored.length : undefined;
|
|
89
|
+
if (!binding)
|
|
90
|
+
return count === 0 ? { excludedProposals: [] } : { excludedProposals: [], excludedProposalsUnreadable: { reason: "binding-broken", ...(count !== undefined ? { count } : {}) } };
|
|
91
|
+
if (!Array.isArray(stored))
|
|
92
|
+
return { excludedProposals: [], excludedProposalsUnreadable: { reason: "malformed-entries" } };
|
|
93
|
+
const proposed = item.spec.candidates.find((candidate) => candidate.role === "proposed");
|
|
94
|
+
const excludedProposals = stored.flatMap((entry) => {
|
|
76
95
|
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
77
96
|
return [];
|
|
78
97
|
const e = entry;
|
|
79
98
|
if (!Number.isSafeInteger(e.proposalIndex) || typeof e.locator !== "string" || typeof e.excerpt !== "string" || !("value" in e))
|
|
80
99
|
return [];
|
|
81
|
-
const proposed = item.spec.candidates.find((candidate) => candidate.role === "proposed");
|
|
82
100
|
const valueText = (proposed ? adapter.summarizeValue?.(e.value, { item, candidate: proposed, value: e.value }) : undefined) ?? formatValue(e.value);
|
|
83
101
|
return [{ proposalIndex: e.proposalIndex, value: e.value, valueText, locator: e.locator, excerpt: e.excerpt }];
|
|
84
102
|
});
|
|
103
|
+
const unreadable = stored.length - excludedProposals.length;
|
|
104
|
+
return { excludedProposals, ...(unreadable > 0 ? { excludedProposalsUnreadable: { reason: "malformed-entries", count: unreadable } } : {}) };
|
|
85
105
|
}
|
|
86
106
|
/**
|
|
87
107
|
* One sentence naming every excluded proposal, for surfaces that speak in
|
|
@@ -93,6 +113,75 @@ export function excludedProposalsSentence(excluded) {
|
|
|
93
113
|
const named = excluded.map((entry) => `${entry.valueText} (proposal ${entry.proposalIndex}, ${entry.locator})`).join(", ");
|
|
94
114
|
return `${excluded.length === 1 ? "Another proposed value was" : `${excluded.length} other proposed values were`} excluded at import because the source text at the cited span is not the excerpt: ${named}. Unverifiable is not disproven.`;
|
|
95
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* One sentence saying that stored excluded proposals cannot be shown, for
|
|
118
|
+
* every surface that shows excluded proposals. Undefined when none are hidden.
|
|
119
|
+
*/
|
|
120
|
+
export function excludedProposalsUnreadableSentence(unreadable) {
|
|
121
|
+
if (!unreadable)
|
|
122
|
+
return undefined;
|
|
123
|
+
const { count } = unreadable;
|
|
124
|
+
const entries = count === undefined ? "Stored excluded proposals are" : `${count} stored excluded ${count === 1 ? "entry is" : "entries are"}`;
|
|
125
|
+
const why = unreadable.reason === "binding-broken"
|
|
126
|
+
? "not shown because this item's extraction binding is broken"
|
|
127
|
+
: "unreadable and not shown";
|
|
128
|
+
return `${entries} ${why}. Another value may have been proposed for this field.`;
|
|
129
|
+
}
|
|
130
|
+
const VERIFICATION_RESULT_TEXT = {
|
|
131
|
+
supported: "supported",
|
|
132
|
+
contradicted: "contradicted",
|
|
133
|
+
"not-addressed": "not addressed by the evidence",
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Verifier notes for every candidate that carries records, read against the
|
|
137
|
+
* value each surface is deciding on: the candidate's own value, or the
|
|
138
|
+
* reviewer's edit when an accept carries one (records on the proposed value do
|
|
139
|
+
* not apply to an edited value). Records are validated here, so an inconsistent
|
|
140
|
+
* record is counted and ignored on every surface alike.
|
|
141
|
+
*/
|
|
142
|
+
export function candidateVerificationNotes(item, editedValue) {
|
|
143
|
+
const proposed = item.spec.candidates.filter((candidate) => candidate.role === "proposed");
|
|
144
|
+
return item.spec.candidates.flatMap((candidate, candidateIndex) => {
|
|
145
|
+
if (candidate.verifications === undefined)
|
|
146
|
+
return [];
|
|
147
|
+
const edited = editedValue !== undefined && proposed.length === 1 && candidate === proposed[0];
|
|
148
|
+
const subjectLabel = edited
|
|
149
|
+
? "the edited value"
|
|
150
|
+
: candidate.role === "current"
|
|
151
|
+
? "the current value"
|
|
152
|
+
: candidate.role === "proposed"
|
|
153
|
+
? proposed.length > 1 ? `proposed value ${proposed.indexOf(candidate) + 1} of ${proposed.length}` : "the proposed value"
|
|
154
|
+
: `candidate ${candidate.id}`;
|
|
155
|
+
const records = Array.isArray(candidate.verifications) ? candidate.verifications : [candidate.verifications];
|
|
156
|
+
const fold = foldCandidateVerifications({ candidateId: candidate.id, value: edited ? editedValue : candidate.value }, records);
|
|
157
|
+
const presented = fold.applicable.map((record) => ({
|
|
158
|
+
recordId: record.id,
|
|
159
|
+
verifierId: record.verifier.id,
|
|
160
|
+
verifierVersion: record.verifier.version,
|
|
161
|
+
method: record.method,
|
|
162
|
+
result: record.result,
|
|
163
|
+
...(record.abstainReason ? { abstainReason: record.abstainReason } : {}),
|
|
164
|
+
createdAt: record.createdAt,
|
|
165
|
+
}));
|
|
166
|
+
const said = presented.map((record) => `${record.verifierId} ${record.verifierVersion} (${record.method}) ${record.result === "abstain" ? `abstained: ${record.abstainReason}` : `said ${VERIFICATION_RESULT_TEXT[record.result]}`}`);
|
|
167
|
+
const plural = (count, one, many) => `${count} ${count === 1 ? one : many}`;
|
|
168
|
+
const sentence = [
|
|
169
|
+
said.length ? `Verifier records for ${subjectLabel} (what a verifier said: not proof, not a review decision): ${said.join("; ")}.` : `No verifier record applies to ${subjectLabel}.`,
|
|
170
|
+
...(fold.inapplicable.length ? [`${plural(fold.inapplicable.length, "record was", "records were")} made for a different value or evidence and ${fold.inapplicable.length === 1 ? "does" : "do"} not apply.`] : []),
|
|
171
|
+
...(fold.rejected.length ? [`${plural(fold.rejected.length, "record", "records")} failed validation and ${fold.rejected.length === 1 ? "was" : "were"} ignored.`] : []),
|
|
172
|
+
].join(" ");
|
|
173
|
+
return [{
|
|
174
|
+
candidateIndex,
|
|
175
|
+
candidateId: candidate.id,
|
|
176
|
+
subjectLabel,
|
|
177
|
+
status: fold.status,
|
|
178
|
+
records: presented,
|
|
179
|
+
inapplicableCount: fold.inapplicable.length,
|
|
180
|
+
rejectedCount: fold.rejected.length,
|
|
181
|
+
sentence,
|
|
182
|
+
}];
|
|
183
|
+
});
|
|
184
|
+
}
|
|
96
185
|
export function buildReviewItemPresentation(item, adapter = {}) {
|
|
97
186
|
const context = { item };
|
|
98
187
|
const targetLabel = adapter.labelForTarget?.(item.spec.target, context) ?? humanizeIdentifier(item.spec.target);
|
|
@@ -111,7 +200,7 @@ export function buildReviewItemPresentation(item, adapter = {}) {
|
|
|
111
200
|
function extractionPresentation(item, adapter) {
|
|
112
201
|
const binding = extractionEnvelopeBinding(item);
|
|
113
202
|
return {
|
|
114
|
-
|
|
203
|
+
...excludedProposalsOf(item, binding, adapter),
|
|
115
204
|
...(binding ? { excerptVerification: binding.excerptVerification === "verified" ? "verified" : "unverified" } : {}),
|
|
116
205
|
};
|
|
117
206
|
}
|
|
@@ -168,12 +257,20 @@ export function buildReviewResultPresentation(result, item, adapter = {}) {
|
|
|
168
257
|
selectedValueText: selectedCandidate && item
|
|
169
258
|
? buildReviewCandidatePresentation(item, selectedCandidate, adapter, targetLabel).valueText
|
|
170
259
|
: result.selectedDisplayValue,
|
|
171
|
-
applyMeaning: result.
|
|
172
|
-
? "Saved decision applies proposed
|
|
173
|
-
:
|
|
260
|
+
applyMeaning: result.decision === "select-proposed"
|
|
261
|
+
? "Saved decision applies the chosen value; the other proposed values were seen and not chosen"
|
|
262
|
+
: result.selectedCandidateRole === "proposed"
|
|
263
|
+
? "Saved decision applies proposed value"
|
|
264
|
+
: "Saved decision keeps current value",
|
|
174
265
|
reviewItemLink: item && itemContext ? adapter.linkForReviewItem?.(item, itemContext) : undefined,
|
|
175
266
|
traceRefs: item
|
|
176
|
-
?
|
|
267
|
+
? [
|
|
268
|
+
...traceRefsForResult(item, result, selectedCandidate, adapter),
|
|
269
|
+
// A choice between conflicting values names the values it passed over.
|
|
270
|
+
...(result.decision === "select-proposed"
|
|
271
|
+
? result.unselectedCandidates.filter((candidate) => candidate.role === "proposed").flatMap((candidate) => withTraceLinks([{ label: "Not chosen candidate", value: candidate.id, kind: "candidate" }], { item, candidate }, adapter))
|
|
272
|
+
: []),
|
|
273
|
+
]
|
|
177
274
|
: [{ label: "Survey ReviewItem", value: result.reviewItemName, kind: "review-item" }],
|
|
178
275
|
};
|
|
179
276
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { type ReviewCandidate, type ReviewItem, type ReviewSession, type ReviewSessionEvent, type ReviewSessionEventSpec } from "../../src/review-resource.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The decisions a reviewer can record on a ReviewItem.
|
|
4
|
+
*
|
|
5
|
+
* `select-proposed` chooses one value of a conflict (two or more `proposed`
|
|
6
|
+
* candidates) by candidate id, which the state carries beside the decision
|
|
7
|
+
* (`selectedCandidateId` / `selectedCandidateIdsByItemName`). It is its own
|
|
8
|
+
* kind rather than `accept-proposed` with an id so that the record always says
|
|
9
|
+
* a rival was seen and not chosen, and `accept-proposed` keeps its meaning:
|
|
10
|
+
* accept the one proposed value, refused on a conflict.
|
|
11
|
+
*/
|
|
12
|
+
export type ReviewWorkbenchDecision = "accept-proposed" | "select-proposed" | "keep-current" | "reject-proposed" | "could-not-confirm";
|
|
3
13
|
export type ReviewQueueRowStatus = "pending" | "in-review" | "resolved" | "rejected" | "could-not-confirm" | "escalated";
|
|
4
14
|
export declare const reviewWorkbenchSessionStorageKey = "kontourai.survey.review-workbench.session-events.v1";
|
|
5
15
|
export declare const defaultReviewSessionName = "review-workbench-session";
|
|
@@ -16,6 +26,11 @@ export interface ReviewWorkbenchState {
|
|
|
16
26
|
*/
|
|
17
27
|
readonly editedValue?: unknown;
|
|
18
28
|
readonly attemptEvidenceIds?: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* The candidate a `select-proposed` decision chose. Required for that
|
|
31
|
+
* decision and ignored for every other one.
|
|
32
|
+
*/
|
|
33
|
+
readonly selectedCandidateId?: string;
|
|
19
34
|
}
|
|
20
35
|
export interface ReviewQueueSessionState {
|
|
21
36
|
readonly items: readonly ReviewItem[];
|
|
@@ -32,6 +47,12 @@ export interface ReviewQueueSessionState {
|
|
|
32
47
|
*/
|
|
33
48
|
readonly editedValuesByItemName?: Readonly<Record<string, unknown>>;
|
|
34
49
|
readonly attemptEvidenceIdsByItemName?: Readonly<Record<string, readonly string[]>>;
|
|
50
|
+
/**
|
|
51
|
+
* The candidate each `select-proposed` decision chose, keyed by ReviewItem
|
|
52
|
+
* name. Additive/optional: sessions stored before this field existed have no
|
|
53
|
+
* `select-proposed` decision and never read it.
|
|
54
|
+
*/
|
|
55
|
+
readonly selectedCandidateIdsByItemName?: Readonly<Record<string, string>>;
|
|
35
56
|
}
|
|
36
57
|
export interface ReviewSessionSummary {
|
|
37
58
|
readonly accepted: number;
|
|
@@ -48,6 +69,12 @@ export declare const workbenchDecisionDefinitions: {
|
|
|
48
69
|
candidateRole: "proposed";
|
|
49
70
|
status: "verified";
|
|
50
71
|
};
|
|
72
|
+
"select-proposed": {
|
|
73
|
+
label: string;
|
|
74
|
+
effect: string;
|
|
75
|
+
candidateRole: "proposed";
|
|
76
|
+
status: "verified";
|
|
77
|
+
};
|
|
51
78
|
"keep-current": {
|
|
52
79
|
label: string;
|
|
53
80
|
effect: string;
|
|
@@ -109,14 +136,30 @@ export declare function keepActionDecision(item: ReviewItem, flaggedWrong: boole
|
|
|
109
136
|
* {@link decisionCandidateId} is `undefined` for it, so the decision, its
|
|
110
137
|
* session events and the canonical projection name no candidate.
|
|
111
138
|
*/
|
|
112
|
-
export declare function candidateForDecision(item: ReviewItem, decision: ReviewWorkbenchDecision): ReviewCandidate;
|
|
139
|
+
export declare function candidateForDecision(item: ReviewItem, decision: ReviewWorkbenchDecision, selectedCandidateId?: string): ReviewCandidate;
|
|
140
|
+
/**
|
|
141
|
+
* Why a `select-proposed` decision naming `candidateId` cannot be recorded on
|
|
142
|
+
* this item, or `undefined` when it can.
|
|
143
|
+
*
|
|
144
|
+
* The choice must name, by a unique id, one of the item's `proposed`
|
|
145
|
+
* candidates, and the item must hold at least two of them: choosing "over a
|
|
146
|
+
* rival" on an item with no rival would record a conflict that never existed
|
|
147
|
+
* (use `accept-proposed` there). Proposals excluded at import are not
|
|
148
|
+
* candidates, so they can never be chosen.
|
|
149
|
+
*/
|
|
150
|
+
export declare function conflictSelectionIssue(item: ReviewItem, candidateId: string | undefined): string | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* The proposed candidates a `select-proposed` decision saw and did not choose,
|
|
153
|
+
* in item order. Empty for every other decision.
|
|
154
|
+
*/
|
|
155
|
+
export declare function unchosenProposedCandidates(item: ReviewItem, decision: ReviewWorkbenchDecision, selectedCandidateId: string | undefined): ReviewCandidate[];
|
|
113
156
|
/** Whether a decision on this item selects no candidate at all (see {@link candidateForDecision}). */
|
|
114
157
|
export declare function decisionSelectsNoCandidate(item: ReviewItem, decision: ReviewWorkbenchDecision): boolean;
|
|
115
158
|
/**
|
|
116
159
|
* The candidate id a decision records: the selected candidate's, or
|
|
117
160
|
* `undefined` when the decision selects no candidate.
|
|
118
161
|
*/
|
|
119
|
-
export declare function decisionCandidateId(item: ReviewItem, decision: ReviewWorkbenchDecision): string | undefined;
|
|
162
|
+
export declare function decisionCandidateId(item: ReviewItem, decision: ReviewWorkbenchDecision, selectedCandidateId?: string): string | undefined;
|
|
120
163
|
/**
|
|
121
164
|
* The value that should actually be applied for a decision: the reviewer's inline
|
|
122
165
|
* edit when one was made for an accept-proposed decision, otherwise the selected
|
|
@@ -125,7 +168,7 @@ export declare function decisionCandidateId(item: ReviewItem, decision: ReviewWo
|
|
|
125
168
|
* this rule; this helper exists for callers deriving the value from raw session
|
|
126
169
|
* state directly.
|
|
127
170
|
*/
|
|
128
|
-
export declare function effectiveValueForDecision(item: ReviewItem, decision: ReviewWorkbenchDecision, editedValue?: unknown): unknown;
|
|
171
|
+
export declare function effectiveValueForDecision(item: ReviewItem, decision: ReviewWorkbenchDecision, editedValue?: unknown, selectedCandidateId?: string): unknown;
|
|
129
172
|
export declare function selectedCandidateRole(state: ReviewWorkbenchState): ReviewCandidate["role"] | undefined;
|
|
130
173
|
export declare function buildReviewSessionResource(session: ReviewQueueSessionState, events?: readonly ReviewSessionEvent[], sessionName?: string): ReviewSession;
|
|
131
174
|
export declare function buildReviewSessionEvents(session: ReviewQueueSessionState, sessionName?: string): ReviewSessionEvent[];
|
|
@@ -11,6 +11,12 @@ export const workbenchDecisionDefinitions = {
|
|
|
11
11
|
candidateRole: "proposed",
|
|
12
12
|
status: "verified",
|
|
13
13
|
},
|
|
14
|
+
"select-proposed": {
|
|
15
|
+
label: "Use this value",
|
|
16
|
+
effect: "The chosen value becomes the verified review outcome; the other proposed values were seen and not chosen.",
|
|
17
|
+
candidateRole: "proposed",
|
|
18
|
+
status: "verified",
|
|
19
|
+
},
|
|
14
20
|
"keep-current": {
|
|
15
21
|
label: "Keep current",
|
|
16
22
|
effect: "Current value remains the verified review outcome.",
|
|
@@ -58,6 +64,7 @@ export function currentReviewWorkbenchState(session) {
|
|
|
58
64
|
decision: session.decisionsByItemName[item.metadata.name],
|
|
59
65
|
editedValue: session.editedValuesByItemName?.[item.metadata.name],
|
|
60
66
|
attemptEvidenceIds: session.attemptEvidenceIdsByItemName?.[item.metadata.name],
|
|
67
|
+
selectedCandidateId: session.selectedCandidateIdsByItemName?.[item.metadata.name],
|
|
61
68
|
reviewedAt: session.reviewedAt,
|
|
62
69
|
actorId: session.actorId,
|
|
63
70
|
};
|
|
@@ -77,7 +84,7 @@ export function deriveQueueRowStatus(item, session) {
|
|
|
77
84
|
if (decision === "could-not-confirm") {
|
|
78
85
|
return "could-not-confirm";
|
|
79
86
|
}
|
|
80
|
-
if (decision === "accept-proposed" || decision === "keep-current" || item.spec.candidateSetStatus === "resolved") {
|
|
87
|
+
if (decision === "accept-proposed" || decision === "select-proposed" || decision === "keep-current" || item.spec.candidateSetStatus === "resolved") {
|
|
81
88
|
return "resolved";
|
|
82
89
|
}
|
|
83
90
|
if (item.spec.candidateSetStatus === "escalated") {
|
|
@@ -99,7 +106,7 @@ export function nextUnresolvedItemName(session) {
|
|
|
99
106
|
export function reviewSessionSummary(session) {
|
|
100
107
|
return session.items.reduce((summary, item) => {
|
|
101
108
|
const decision = session.decisionsByItemName[item.metadata.name];
|
|
102
|
-
if (decision === "accept-proposed") {
|
|
109
|
+
if (decision === "accept-proposed" || decision === "select-proposed") {
|
|
103
110
|
return { ...summary, accepted: summary.accepted + 1 };
|
|
104
111
|
}
|
|
105
112
|
if (decision === "keep-current") {
|
|
@@ -176,7 +183,14 @@ const VALUE_NEUTRAL_DECISIONS = new Set(["reject-proposed", "could-not-confirm"]
|
|
|
176
183
|
* {@link decisionCandidateId} is `undefined` for it, so the decision, its
|
|
177
184
|
* session events and the canonical projection name no candidate.
|
|
178
185
|
*/
|
|
179
|
-
export function candidateForDecision(item, decision) {
|
|
186
|
+
export function candidateForDecision(item, decision, selectedCandidateId) {
|
|
187
|
+
if (decision === "select-proposed") {
|
|
188
|
+
const issue = conflictSelectionIssue(item, selectedCandidateId);
|
|
189
|
+
if (issue) {
|
|
190
|
+
throw new Error(issue);
|
|
191
|
+
}
|
|
192
|
+
return item.spec.candidates.find((entry) => entry.id === selectedCandidateId);
|
|
193
|
+
}
|
|
180
194
|
const definition = workbenchDecisionDefinitions[decision];
|
|
181
195
|
const matches = item.spec.candidates.filter((entry) => entry.role === definition.candidateRole);
|
|
182
196
|
const candidate = matches[0];
|
|
@@ -189,6 +203,45 @@ export function candidateForDecision(item, decision) {
|
|
|
189
203
|
assertSoleCandidateId(item, candidate.id);
|
|
190
204
|
return candidate;
|
|
191
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Why a `select-proposed` decision naming `candidateId` cannot be recorded on
|
|
208
|
+
* this item, or `undefined` when it can.
|
|
209
|
+
*
|
|
210
|
+
* The choice must name, by a unique id, one of the item's `proposed`
|
|
211
|
+
* candidates, and the item must hold at least two of them: choosing "over a
|
|
212
|
+
* rival" on an item with no rival would record a conflict that never existed
|
|
213
|
+
* (use `accept-proposed` there). Proposals excluded at import are not
|
|
214
|
+
* candidates, so they can never be chosen.
|
|
215
|
+
*/
|
|
216
|
+
export function conflictSelectionIssue(item, candidateId) {
|
|
217
|
+
const proposed = item.spec.candidates.filter((entry) => entry.role === "proposed");
|
|
218
|
+
if (proposed.length < 2) {
|
|
219
|
+
return `ReviewItem ${item.metadata.name} has ${proposed.length} proposed candidate${proposed.length === 1 ? "" : "s"}; select-proposed chooses between conflicting proposed values, so it needs at least two.`;
|
|
220
|
+
}
|
|
221
|
+
if (!candidateId) {
|
|
222
|
+
return `ReviewItem ${item.metadata.name}: select-proposed must name the chosen candidate id.`;
|
|
223
|
+
}
|
|
224
|
+
const matches = item.spec.candidates.filter((entry) => entry.id === candidateId);
|
|
225
|
+
if (matches.length === 0) {
|
|
226
|
+
return `ReviewItem ${item.metadata.name} has no candidate ${candidateId}; select-proposed can only choose one of its proposed candidates.`;
|
|
227
|
+
}
|
|
228
|
+
if (matches.length > 1) {
|
|
229
|
+
return `ReviewItem ${item.metadata.name} has ${matches.length} candidates with id ${candidateId}; candidate ids must be unique.`;
|
|
230
|
+
}
|
|
231
|
+
if (matches[0].role !== "proposed") {
|
|
232
|
+
return `ReviewItem ${item.metadata.name} candidate ${candidateId} has role ${matches[0].role ?? "none"}; select-proposed can only choose a proposed candidate.`;
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The proposed candidates a `select-proposed` decision saw and did not choose,
|
|
238
|
+
* in item order. Empty for every other decision.
|
|
239
|
+
*/
|
|
240
|
+
export function unchosenProposedCandidates(item, decision, selectedCandidateId) {
|
|
241
|
+
return decision === "select-proposed"
|
|
242
|
+
? item.spec.candidates.filter((entry) => entry.role === "proposed" && entry.id !== selectedCandidateId)
|
|
243
|
+
: [];
|
|
244
|
+
}
|
|
192
245
|
/** Whether a decision on this item selects no candidate at all (see {@link candidateForDecision}). */
|
|
193
246
|
export function decisionSelectsNoCandidate(item, decision) {
|
|
194
247
|
const role = workbenchDecisionDefinitions[decision].candidateRole;
|
|
@@ -198,8 +251,8 @@ export function decisionSelectsNoCandidate(item, decision) {
|
|
|
198
251
|
* The candidate id a decision records: the selected candidate's, or
|
|
199
252
|
* `undefined` when the decision selects no candidate.
|
|
200
253
|
*/
|
|
201
|
-
export function decisionCandidateId(item, decision) {
|
|
202
|
-
const candidate = candidateForDecision(item, decision);
|
|
254
|
+
export function decisionCandidateId(item, decision, selectedCandidateId) {
|
|
255
|
+
const candidate = candidateForDecision(item, decision, selectedCandidateId);
|
|
203
256
|
return decisionSelectsNoCandidate(item, decision) ? undefined : candidate.id;
|
|
204
257
|
}
|
|
205
258
|
/**
|
|
@@ -210,8 +263,8 @@ export function decisionCandidateId(item, decision) {
|
|
|
210
263
|
* this rule; this helper exists for callers deriving the value from raw session
|
|
211
264
|
* state directly.
|
|
212
265
|
*/
|
|
213
|
-
export function effectiveValueForDecision(item, decision, editedValue) {
|
|
214
|
-
const candidate = candidateForDecision(item, decision);
|
|
266
|
+
export function effectiveValueForDecision(item, decision, editedValue, selectedCandidateId) {
|
|
267
|
+
const candidate = candidateForDecision(item, decision, selectedCandidateId);
|
|
215
268
|
return decision === "accept-proposed" && editedValue !== undefined ? editedValue : candidate.value;
|
|
216
269
|
}
|
|
217
270
|
export function selectedCandidateRole(state) {
|
|
@@ -287,7 +340,7 @@ export function buildReviewSessionEvents(session, sessionName = defaultReviewSes
|
|
|
287
340
|
if (decision === "could-not-confirm" && !note?.trim()) {
|
|
288
341
|
throw new Error(`ReviewItem ${item.metadata.name} could not confirm requires a non-empty reason.`);
|
|
289
342
|
}
|
|
290
|
-
const candidateId = decisionCandidateId(item, decision);
|
|
343
|
+
const candidateId = decisionCandidateId(item, decision, session.selectedCandidateIdsByItemName?.[item.metadata.name]);
|
|
291
344
|
const definition = workbenchDecisionDefinitions[decision];
|
|
292
345
|
const reviewDecisionName = `${item.metadata.name}-${decision}`;
|
|
293
346
|
// Carry the reviewer's inline edit in the event itself (accept-proposed
|
|
@@ -383,11 +436,13 @@ export function replayReviewSessionEvents(startState, events) {
|
|
|
383
436
|
const { [itemName]: _removedDecision, ...remainingDecisions } = session.decisionsByItemName;
|
|
384
437
|
const { [itemName]: _removedEdit, ...remainingEdits } = session.editedValuesByItemName ?? {};
|
|
385
438
|
const { [itemName]: _removedAttempts, ...remainingAttempts } = session.attemptEvidenceIdsByItemName ?? {};
|
|
439
|
+
const { [itemName]: _removedSelection, ...remainingSelections } = session.selectedCandidateIdsByItemName ?? {};
|
|
386
440
|
return {
|
|
387
441
|
...session,
|
|
388
442
|
decisionsByItemName: remainingDecisions,
|
|
389
443
|
editedValuesByItemName: remainingEdits,
|
|
390
444
|
attemptEvidenceIdsByItemName: remainingAttempts,
|
|
445
|
+
...(session.selectedCandidateIdsByItemName ? { selectedCandidateIdsByItemName: remainingSelections } : {}),
|
|
391
446
|
};
|
|
392
447
|
}
|
|
393
448
|
const decision = workbenchDecisionFromEvent(event);
|
|
@@ -419,6 +474,15 @@ export function replayReviewSessionEvents(startState, events) {
|
|
|
419
474
|
else {
|
|
420
475
|
delete attemptEvidenceIdsByItemName[itemName];
|
|
421
476
|
}
|
|
477
|
+
// A choice between conflicting values is the event's candidateId: that
|
|
478
|
+
// id is the authority for select-proposed (validated replay checks it).
|
|
479
|
+
const selectedCandidateIdsByItemName = { ...session.selectedCandidateIdsByItemName };
|
|
480
|
+
if (decision === "select-proposed" && event.spec.candidateId) {
|
|
481
|
+
selectedCandidateIdsByItemName[itemName] = event.spec.candidateId;
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
delete selectedCandidateIdsByItemName[itemName];
|
|
485
|
+
}
|
|
422
486
|
return {
|
|
423
487
|
...session,
|
|
424
488
|
decisionsByItemName: {
|
|
@@ -427,6 +491,7 @@ export function replayReviewSessionEvents(startState, events) {
|
|
|
427
491
|
},
|
|
428
492
|
editedValuesByItemName,
|
|
429
493
|
attemptEvidenceIdsByItemName,
|
|
494
|
+
...(session.selectedCandidateIdsByItemName || decision === "select-proposed" ? { selectedCandidateIdsByItemName } : {}),
|
|
430
495
|
};
|
|
431
496
|
}
|
|
432
497
|
return session;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReviewSessionEvent } from "../review-resource.js";
|
|
2
2
|
import { type ReviewQueueSessionState } from "./review-queue-session.js";
|
|
3
|
-
export type ReviewSessionReplayIssueCode = "invalid-sequence" | "duplicate-sequence" | "non-contiguous-sequence" | "unknown-active-item" | "unknown-review-item" | "unknown-candidate" | "missing-review-item" | "invalid-workbench-decision" | "decision-candidate-mismatch" | "decision-status-mismatch" | "decision-resolution-mismatch" | "missing-resolution-reason" | "edited-value-not-editable" | "edited-value-type-mismatch";
|
|
3
|
+
export type ReviewSessionReplayIssueCode = "invalid-sequence" | "duplicate-sequence" | "non-contiguous-sequence" | "unknown-active-item" | "unknown-review-item" | "unknown-candidate" | "missing-review-item" | "invalid-workbench-decision" | "decision-candidate-mismatch" | "invalid-conflict-selection" | "decision-status-mismatch" | "decision-resolution-mismatch" | "missing-resolution-reason" | "edited-value-not-editable" | "edited-value-type-mismatch";
|
|
4
4
|
export interface ReviewSessionReplayIssue {
|
|
5
5
|
readonly code: ReviewSessionReplayIssueCode;
|
|
6
6
|
readonly eventName: string;
|
|
@@ -37,6 +37,16 @@ export type ReviewSessionReplayWarning = {
|
|
|
37
37
|
readonly refusedCode: "edited-value-not-editable" | "edited-value-type-mismatch";
|
|
38
38
|
readonly editedValue: unknown;
|
|
39
39
|
readonly message: string;
|
|
40
|
+
} | {
|
|
41
|
+
/**
|
|
42
|
+
* The session's items came from an extraction import, but the apply
|
|
43
|
+
* boundary was not given the import record to check them against (see
|
|
44
|
+
* `DeriveServerReviewSessionApplyResultOptions.extractionImport`). Emitted
|
|
45
|
+
* by the server apply boundary, never by replay itself.
|
|
46
|
+
*/
|
|
47
|
+
readonly code: "unverified-extraction-queue";
|
|
48
|
+
readonly itemNames: readonly string[];
|
|
49
|
+
readonly message: string;
|
|
40
50
|
};
|
|
41
51
|
/**
|
|
42
52
|
* Lists the legacy text edits replay converts to typed values, and refused
|