@kontourai/survey 6.1.0 → 8.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/calibration.d.ts +15 -0
- package/dist/src/calibration.js +11 -0
- package/dist/src/canonical-reviewed-trust-input.d.ts +10 -0
- package/dist/src/canonical-reviewed-trust-input.js +45 -1
- package/dist/src/console/review-console-server.js +22 -4
- package/dist/src/decision-carry-forward.d.ts +157 -0
- package/dist/src/decision-carry-forward.js +295 -0
- package/dist/src/extraction-envelope.d.ts +4 -3
- package/dist/src/extraction-envelope.js +3 -3
- package/dist/src/field-states.d.ts +100 -0
- package/dist/src/field-states.js +169 -0
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.js +3 -1
- package/dist/src/mcp/review-mcp.js +173 -45
- package/dist/src/review-resource.d.ts +36 -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/audit-sample.d.ts +32 -0
- package/dist/src/review-workbench/audit-sample.js +53 -0
- package/dist/src/review-workbench/extraction-inspector.d.ts +1 -1
- package/dist/src/review-workbench/extraction-inspector.js +1 -1
- package/dist/src/review-workbench/field-state-view.d.ts +15 -0
- package/dist/src/review-workbench/field-state-view.js +84 -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 +23 -0
- package/dist/src/review-workbench/review-presentation.js +51 -10
- package/dist/src/review-workbench/review-queue-session.d.ts +93 -5
- package/dist/src/review-workbench/review-queue-session.js +147 -11
- package/dist/src/review-workbench/review-session-replay.d.ts +11 -1
- package/dist/src/review-workbench/review-session-replay.js +49 -1
- package/dist/src/review-workbench/review-workbench-css.generated.js +126 -0
- 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 +126 -0
- package/dist/src/review-workbench/review-workbench.d.ts +40 -8
- package/dist/src/review-workbench/review-workbench.js +192 -71
- package/dist/src/review-workbench/review-workbench.standalone.css +126 -0
- 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
|
@@ -75,6 +75,21 @@ export interface DeriveCalibrationOptions {
|
|
|
75
75
|
* labeled samples. Default false — see the module note on circularity.
|
|
76
76
|
*/
|
|
77
77
|
readonly includeAutoAccepted?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Count only audit labels: outcomes whose metadata records a score-blind
|
|
80
|
+
* decision (`presentation.scoreBlind === true`) on a random audit sample
|
|
81
|
+
* (`sampling.kind === "random-audit"`), as the review workbench writes them
|
|
82
|
+
* for a session opened with `openRandomAuditSession`. Default false (every
|
|
83
|
+
* human outcome counts, as before). Other outcomes are skipped and counted
|
|
84
|
+
* in `skippedCount`, as windowed-out outcomes are.
|
|
85
|
+
*
|
|
86
|
+
* Calibration reads these fields as recorded on the outcome. The review
|
|
87
|
+
* session validates them (a `random-audit` rate in (0, 1] and a stable seed)
|
|
88
|
+
* and replay refuses events recorded under other conditions than the
|
|
89
|
+
* snapshot's, but an outcome assembled outside Survey's apply path is taken
|
|
90
|
+
* at its word: only feed outcomes from a store you trust.
|
|
91
|
+
*/
|
|
92
|
+
readonly auditSamplesOnly?: boolean;
|
|
78
93
|
}
|
|
79
94
|
/** One labeled calibration sample: a prediction paired with the human label. */
|
|
80
95
|
export interface CalibrationSample {
|
package/dist/src/calibration.js
CHANGED
|
@@ -80,6 +80,10 @@ export function deriveCalibration(input, options = {}) {
|
|
|
80
80
|
const samples = [];
|
|
81
81
|
let skippedCount = 0;
|
|
82
82
|
for (const outcome of input.reviewOutcomes) {
|
|
83
|
+
if (options.auditSamplesOnly && !isScoreBlindAuditOutcome(outcome)) {
|
|
84
|
+
skippedCount++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
83
87
|
if (outcome.resolution === "could_not_confirm") {
|
|
84
88
|
skippedCount++;
|
|
85
89
|
continue;
|
|
@@ -142,6 +146,13 @@ export function deriveCalibration(input, options = {}) {
|
|
|
142
146
|
// ---------------------------------------------------------------------------
|
|
143
147
|
// Internal: sample construction
|
|
144
148
|
// ---------------------------------------------------------------------------
|
|
149
|
+
/** A decision recorded score-blind on a random audit sample (see `openRandomAuditSession`). */
|
|
150
|
+
function isScoreBlindAuditOutcome(outcome) {
|
|
151
|
+
const presentation = outcome.metadata?.presentation;
|
|
152
|
+
const sampling = outcome.metadata?.sampling;
|
|
153
|
+
return typeof presentation === "object" && presentation !== null && presentation.scoreBlind === true
|
|
154
|
+
&& typeof sampling === "object" && sampling !== null && sampling.kind === "random-audit";
|
|
155
|
+
}
|
|
145
156
|
function toSample(outcome, candidateSetById, extractionById, includeAutoAccepted) {
|
|
146
157
|
// No human label yet.
|
|
147
158
|
if (outcome.status === "proposed")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SurveyInput } from "./types.js";
|
|
2
2
|
import type { ReviewItem } from "./review-resource.js";
|
|
3
3
|
import type { ReviewWorkbenchResult } from "./review-workbench/review-workbench.js";
|
|
4
|
+
import { type DeriveFieldStatesInput } from "./field-states.js";
|
|
4
5
|
export interface BuildCanonicalReviewedTrustInputOptions {
|
|
5
6
|
/** Producer identity for the resulting SurveyInput batch. */
|
|
6
7
|
readonly source: string;
|
|
@@ -12,6 +13,15 @@ export interface BuildCanonicalReviewedTrustInputOptions {
|
|
|
12
13
|
readonly items: readonly ReviewItem[];
|
|
13
14
|
/** Results derived by Survey's server apply boundary from snapshot + events. */
|
|
14
15
|
readonly results: readonly ReviewWorkbenchResult[];
|
|
16
|
+
/**
|
|
17
|
+
* The extraction imports the items came from (and optionally verifier
|
|
18
|
+
* records and supersessions). When present, each claim whose item one of
|
|
19
|
+
* these imports produced carries its derived content and lifecycle states
|
|
20
|
+
* under `metadata["survey.kontourai.io/field-state"]` (see
|
|
21
|
+
* `deriveFieldStates`). The states are derived here from the imports and the
|
|
22
|
+
* results' own decisions, never supplied as labels. Claim status is unchanged.
|
|
23
|
+
*/
|
|
24
|
+
readonly fieldStates?: Omit<DeriveFieldStatesInput, "decisions" | "carryForwards">;
|
|
15
25
|
}
|
|
16
26
|
export interface CanonicalReviewedTrustInput {
|
|
17
27
|
readonly surveyInput: SurveyInput;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
|
|
2
2
|
import { canonicalJson } from "./review-workbench/canonical.js";
|
|
3
3
|
import { decisionSelectsNoCandidate, workbenchDecisionDefinitions } from "./review-workbench/review-queue-session.js";
|
|
4
|
+
import { deriveFieldStates, fieldStateClaimMetadata, FIELD_STATE_METADATA_KEY } from "./field-states.js";
|
|
4
5
|
/**
|
|
5
6
|
* Projects server-applied review records into the complete SurveyInput consumed
|
|
6
7
|
* by buildSurveyTrustBundle. The ReviewItem and ReviewWorkbenchResult are the
|
|
@@ -22,6 +23,13 @@ export function buildCanonicalReviewedTrustInput(options) {
|
|
|
22
23
|
if (itemsByName.size !== resultsByName.size) {
|
|
23
24
|
throw new Error("Canonical review projection requires exactly one resolved result for every ReviewItem.");
|
|
24
25
|
}
|
|
26
|
+
const fieldStatesByItem = new Map();
|
|
27
|
+
if (options.fieldStates) {
|
|
28
|
+
const states = deriveFieldStates({ ...options.fieldStates, decisions: options.results.map((result) => result.reviewDecision) });
|
|
29
|
+
for (const state of states)
|
|
30
|
+
if (state.reviewItemName !== undefined)
|
|
31
|
+
fieldStatesByItem.set(state.reviewItemName, state);
|
|
32
|
+
}
|
|
25
33
|
const rawSources = new Map();
|
|
26
34
|
const extractions = new Map();
|
|
27
35
|
const candidateSets = new Map();
|
|
@@ -39,10 +47,17 @@ export function buildCanonicalReviewedTrustInput(options) {
|
|
|
39
47
|
const rejectsAll = selectsNone && result.decision === "reject-proposed";
|
|
40
48
|
const rejectedRole = workbenchDecisionDefinitions[result.decision].candidateRole;
|
|
41
49
|
const rejectAllReason = result.rationale?.trim() || "Every proposed value for this claim was rejected.";
|
|
50
|
+
// A choice of one conflicting value passes over the others: each keeps a
|
|
51
|
+
// record that it was seen and not chosen (and so its own rejected-candidate
|
|
52
|
+
// learning), never silently dropped.
|
|
53
|
+
const unchosenIds = new Set(result.decision === "select-proposed" ? result.reviewDecision.spec.unselectedCandidateIds ?? [] : []);
|
|
54
|
+
const notChosenReason = `Not chosen: the reviewer chose candidate ${recordIdFor(item, result.selectedCandidateId ?? "")} for this claim.`;
|
|
42
55
|
const candidates = item.spec.candidates.map((candidate) => {
|
|
43
56
|
const records = projectCandidate(item, rejectsAll && candidate.role === rejectedRole
|
|
44
57
|
? { ...candidate, rejectionReason: candidate.rejectionReason ?? rejectAllReason }
|
|
45
|
-
: candidate)
|
|
58
|
+
: unchosenIds.has(candidate.id)
|
|
59
|
+
? { ...candidate, rejectionReason: candidate.rejectionReason ?? notChosenReason }
|
|
60
|
+
: candidate);
|
|
46
61
|
addConsistent(rawSources, records.rawSource, "raw source");
|
|
47
62
|
addConsistent(extractions, records.extraction, "extraction");
|
|
48
63
|
return records.candidate;
|
|
@@ -96,6 +111,13 @@ export function buildCanonicalReviewedTrustInput(options) {
|
|
|
96
111
|
metadata: {
|
|
97
112
|
workbenchDecision: result.decision,
|
|
98
113
|
...(result.editedValue !== undefined ? { editedValue: result.editedValue } : {}),
|
|
114
|
+
...(decision.unselectedCandidateIds?.length
|
|
115
|
+
? { unselectedCandidateIds: decision.unselectedCandidateIds.map((id) => recordIdFor(item, id)) }
|
|
116
|
+
: {}),
|
|
117
|
+
// How the decision was made: whether scores were hidden, and how the
|
|
118
|
+
// item was sampled. Calibration reads these to select audit labels.
|
|
119
|
+
...(decision.presentation !== undefined ? { presentation: { scoreBlind: decision.presentation.scoreBlind } } : {}),
|
|
120
|
+
...(decision.sampling !== undefined ? { sampling: { ...decision.sampling } } : {}),
|
|
99
121
|
},
|
|
100
122
|
};
|
|
101
123
|
addConsistent(reviewOutcomes, reviewOutcome, "review outcome");
|
|
@@ -136,6 +158,9 @@ export function buildCanonicalReviewedTrustInput(options) {
|
|
|
136
158
|
...(hint.derivedFrom ? { derivedFrom: [...hint.derivedFrom] } : {}),
|
|
137
159
|
collectedBy: hint.collectedBy ?? sharedExtractor(selected ? [selected] : item.spec.candidates) ?? options.source,
|
|
138
160
|
...(decision.actor?.id ? { actor: decision.actor.id } : {}),
|
|
161
|
+
...(fieldStatesByItem.has(item.metadata.name)
|
|
162
|
+
? { metadata: { [FIELD_STATE_METADATA_KEY]: fieldStateClaimMetadata(fieldStatesByItem.get(item.metadata.name)) } }
|
|
163
|
+
: {}),
|
|
139
164
|
};
|
|
140
165
|
addConsistent(claims, claim, "claim target");
|
|
141
166
|
}
|
|
@@ -192,6 +217,20 @@ function assertCanonicalResult(item, result) {
|
|
|
192
217
|
|| canonicalJson(decision.editedValue) !== canonicalJson(result.editedValue)) {
|
|
193
218
|
throw new Error(`Review result ${result.reviewItemName} contradicts its canonical ReviewDecision.`);
|
|
194
219
|
}
|
|
220
|
+
// Accepting one value of a conflict would project it as if it had been the
|
|
221
|
+
// only value; a choice between conflicting values is select-proposed.
|
|
222
|
+
if (result.decision === "accept-proposed" && item.spec.candidates.filter((candidate) => candidate.role === "proposed").length > 1) {
|
|
223
|
+
throw new Error(`Review result ${result.reviewItemName} accepts one proposed value of a conflict; a choice between conflicting values must be a select-proposed decision that records the values it passed over.`);
|
|
224
|
+
}
|
|
225
|
+
// A choice names exactly the proposed values it passed over; no other
|
|
226
|
+
// decision names any.
|
|
227
|
+
const expectedUnselected = result.decision === "select-proposed" && selected
|
|
228
|
+
? item.spec.candidates.filter((candidate) => candidate.role === "proposed" && candidate.id !== selected.id).map((candidate) => candidate.id)
|
|
229
|
+
: [];
|
|
230
|
+
if (canonicalJson(decision.unselectedCandidateIds ?? []) !== canonicalJson(expectedUnselected)
|
|
231
|
+
|| (result.decision === "select-proposed" && expectedUnselected.length === 0)) {
|
|
232
|
+
throw new Error(`Review result ${result.reviewItemName} does not record the proposed values its ${result.decision} decision passed over.`);
|
|
233
|
+
}
|
|
195
234
|
if ((result.decision === "could-not-confirm") !== (decision.resolution === "could_not_confirm")) {
|
|
196
235
|
throw new Error(`Review result ${result.reviewItemName} contradicts its canonical review resolution.`);
|
|
197
236
|
}
|
|
@@ -210,6 +249,11 @@ function assertCanonicalResult(item, result) {
|
|
|
210
249
|
}
|
|
211
250
|
}
|
|
212
251
|
}
|
|
252
|
+
/** The projected Candidate record id of a ReviewItem candidate. */
|
|
253
|
+
function recordIdFor(item, candidateId) {
|
|
254
|
+
const candidate = item.spec.candidates.find((entry) => entry.id === candidateId);
|
|
255
|
+
return candidate?.projection?.candidateId ?? candidateId;
|
|
256
|
+
}
|
|
213
257
|
function claimTargetIdentity(target) {
|
|
214
258
|
const { claimId: _claimId, ...identity } = target;
|
|
215
259
|
return identity;
|
|
@@ -18,6 +18,7 @@ import { readFile as readFileAsync } from "node:fs/promises";
|
|
|
18
18
|
import { resolve, join, dirname, extname, normalize, sep } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { createServerReviewSessionRecord, currentSessionState, deriveServerReviewSessionApplyResult, } from "../review-workbench/server-review-session.js";
|
|
21
|
+
import { attestReviewQueueExtraction } from "../review-workbench/queue-binding.js";
|
|
21
22
|
import { appendReviewSessionEvents, readReviewSessionFile, reviewSessionRevision, storedReviewSessionName, updateReviewSessionFile, } from "../review-session-file.js";
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Constants
|
|
@@ -404,9 +405,15 @@ let activeItemName = null;
|
|
|
404
405
|
async function fetchAndMount() {
|
|
405
406
|
try {
|
|
406
407
|
const res = await fetch("/api/session");
|
|
407
|
-
if (!res.ok)
|
|
408
|
+
if (!res.ok) {
|
|
409
|
+
const body = await res.json().catch(() => ({}));
|
|
410
|
+
showSaveStatus(body.error || ("The session could not be loaded (HTTP " + res.status + ")."));
|
|
411
|
+
const root = document.getElementById("review-workbench");
|
|
412
|
+
if (root) root.innerHTML = "";
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
408
415
|
const data = await res.json();
|
|
409
|
-
const { snapshot, events, revision } = data;
|
|
416
|
+
const { snapshot, events, revision, extractionImport } = data;
|
|
410
417
|
const state = events && events.length > 0
|
|
411
418
|
? replayReviewSessionEvents(snapshot, events)
|
|
412
419
|
: snapshot;
|
|
@@ -419,7 +426,7 @@ async function fetchAndMount() {
|
|
|
419
426
|
const root = document.getElementById("review-workbench");
|
|
420
427
|
if (!root) return;
|
|
421
428
|
|
|
422
|
-
mountReviewWorkbench(root, state, { eventStore: createConsoleEventStore(revision) });
|
|
429
|
+
mountReviewWorkbench(root, state, { eventStore: createConsoleEventStore(revision), ...(extractionImport ? { extractionImport } : {}) });
|
|
423
430
|
} catch (err) {
|
|
424
431
|
console.error("[console] Mount error:", err);
|
|
425
432
|
}
|
|
@@ -602,12 +609,22 @@ export async function startReviewConsoleServer(options) {
|
|
|
602
609
|
// ---- Session read ----
|
|
603
610
|
if (pathname === "/api/session" && req.method === "GET") {
|
|
604
611
|
const content = await readSession(sessionPath);
|
|
612
|
+
// A queue that does not match the extraction import stored with it is
|
|
613
|
+
// not served for review; one without a stored import is served, and the
|
|
614
|
+
// workbench marks it unverified.
|
|
615
|
+
const attestation = attestReviewQueueExtraction(content.snapshot.items, content.extractionImport);
|
|
616
|
+
if (attestation.state === "diverges") {
|
|
617
|
+
sendJson(res, 409, { error: attestation.message, queueAttestation: attestation.state });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
605
620
|
sendJson(res, 200, {
|
|
606
621
|
session: content.session,
|
|
607
622
|
snapshot: content.snapshot,
|
|
608
623
|
events: content.events,
|
|
609
624
|
revision: reviewSessionRevision(content.events),
|
|
610
625
|
state: currentSessionState(content.snapshot, content.events),
|
|
626
|
+
queueAttestation: attestation.state,
|
|
627
|
+
...(content.extractionImport !== undefined ? { extractionImport: content.extractionImport } : {}),
|
|
611
628
|
});
|
|
612
629
|
return;
|
|
613
630
|
}
|
|
@@ -660,6 +677,7 @@ export async function startReviewConsoleServer(options) {
|
|
|
660
677
|
record,
|
|
661
678
|
events: nextEvents,
|
|
662
679
|
requiredResolvedItems: "none",
|
|
680
|
+
extractionImport: content.extractionImport,
|
|
663
681
|
});
|
|
664
682
|
issueMessages = applyResult.ok
|
|
665
683
|
? []
|
|
@@ -672,7 +690,7 @@ export async function startReviewConsoleServer(options) {
|
|
|
672
690
|
return { result: { status: 422, body: { error: `Validation failed: ${issueMessages.join("; ")}` } } };
|
|
673
691
|
}
|
|
674
692
|
return {
|
|
675
|
-
next: { session: content.session, snapshot, events: nextEvents },
|
|
693
|
+
next: { ...content, session: content.session, snapshot, events: nextEvents },
|
|
676
694
|
result: {
|
|
677
695
|
status: 200,
|
|
678
696
|
body: { ok: true, eventCount: nextEvents.length, revision: reviewSessionRevision(nextEvents) },
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ReviewDecision, ReviewItem } from "./review-resource.js";
|
|
2
|
+
/**
|
|
3
|
+
* Decisions across review rounds (#295).
|
|
4
|
+
*
|
|
5
|
+
* A new extraction of the same document gives every review item a new identity,
|
|
6
|
+
* so on its own every re-extraction sends every field back to review. Given the
|
|
7
|
+
* producer's stable slot identity and per-candidate version identity, a
|
|
8
|
+
* decision on a slot whose candidates did not change is carried forward to the
|
|
9
|
+
* new round, and a decision that a later one replaces is marked superseded.
|
|
10
|
+
*
|
|
11
|
+
* Vocabulary follows the round receipts producers already keep: a decision is
|
|
12
|
+
* `carried-forward` (made in an earlier round and still holding because its
|
|
13
|
+
* candidates are unchanged) or `affirmed` (made against this round's item).
|
|
14
|
+
*
|
|
15
|
+
* Survey does not invent slot or version identities: they come from the
|
|
16
|
+
* producer, and every item and candidate must carry one. Survey additionally
|
|
17
|
+
* compares each candidate's value, full locator, source evidence and full
|
|
18
|
+
* claim target, so a
|
|
19
|
+
* decision is never carried across a candidate whose content changed, even if a
|
|
20
|
+
* producer reused a version identity.
|
|
21
|
+
*
|
|
22
|
+
* Both records are content-addressed (`id` is a digest of every other field)
|
|
23
|
+
* and immutable: a record that changes is a new record.
|
|
24
|
+
*/
|
|
25
|
+
export declare const decisionCarryForwardKind: "survey.decision-carry-forward";
|
|
26
|
+
export declare const decisionSupersessionKind: "survey.decision-supersession";
|
|
27
|
+
/** How a decision reads in a round. */
|
|
28
|
+
export type DecisionBasis = "carried-forward" | "affirmed";
|
|
29
|
+
/** The terminal resolution a decision reached, as carry-forward reads it. */
|
|
30
|
+
export type CarriedResolution = "accepted" | "rejected" | "could_not_confirm";
|
|
31
|
+
/** One review item in a round, with the producer's slot and version identities. */
|
|
32
|
+
export interface RoundReviewItem {
|
|
33
|
+
readonly item: ReviewItem;
|
|
34
|
+
/** Producer slot identity: the same field of the same source lineage across rounds. */
|
|
35
|
+
readonly slotId: string;
|
|
36
|
+
/** Producer version identity of every candidate of `item`, keyed by candidate id. */
|
|
37
|
+
readonly candidateVersionIds: Readonly<Record<string, string>>;
|
|
38
|
+
}
|
|
39
|
+
/** A decision made in an earlier round, with the round item it was made on. */
|
|
40
|
+
export interface PriorRoundDecision extends RoundReviewItem {
|
|
41
|
+
readonly decision: ReviewDecision;
|
|
42
|
+
}
|
|
43
|
+
/** A decision named by content: its resource name and the digest of its canonical bytes. */
|
|
44
|
+
export interface DecisionReference {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly digest: string;
|
|
47
|
+
}
|
|
48
|
+
export interface DecisionCarryForward {
|
|
49
|
+
/** `sha256:` digest of every other field; recomputed on validation. */
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly kind: typeof decisionCarryForwardKind;
|
|
52
|
+
readonly schemaVersion: 1;
|
|
53
|
+
readonly priorDecision: DecisionReference;
|
|
54
|
+
readonly slotId: string;
|
|
55
|
+
/** Sorted, unique version ids of the slot's candidates, identical in both rounds. */
|
|
56
|
+
readonly versionIds: readonly string[];
|
|
57
|
+
readonly roundId: string;
|
|
58
|
+
/** The new round's review item the decision now applies to. */
|
|
59
|
+
readonly reviewItemName: string;
|
|
60
|
+
/** The new round's candidate the prior decision selected, when it selected one. */
|
|
61
|
+
readonly candidateId?: string;
|
|
62
|
+
readonly resolution: CarriedResolution;
|
|
63
|
+
readonly basis: "carried-forward";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Why a decision was superseded: its slot's candidates changed and the new
|
|
67
|
+
* version was decided (`candidate-changed`), or the unchanged slot was
|
|
68
|
+
* reviewed again and decided anew (`re-reviewed`).
|
|
69
|
+
*/
|
|
70
|
+
export type SupersessionReason = "candidate-changed" | "re-reviewed";
|
|
71
|
+
export interface DecisionSupersession {
|
|
72
|
+
/** `sha256:` digest of every other field; recomputed on validation. */
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly kind: typeof decisionSupersessionKind;
|
|
75
|
+
readonly schemaVersion: 1;
|
|
76
|
+
readonly priorDecision: DecisionReference;
|
|
77
|
+
readonly newDecision: DecisionReference;
|
|
78
|
+
readonly slotId: string;
|
|
79
|
+
readonly reason: SupersessionReason;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Which prior resolutions may be carried forward. Only `accepted` is carried
|
|
83
|
+
* unless a policy names others explicitly: a rejection or a could-not-confirm
|
|
84
|
+
* is a judgment about evidence a new round may have changed around it.
|
|
85
|
+
*/
|
|
86
|
+
export interface CarryForwardPolicy {
|
|
87
|
+
readonly carry: readonly CarriedResolution[];
|
|
88
|
+
}
|
|
89
|
+
export declare const defaultCarryForwardPolicy: CarryForwardPolicy;
|
|
90
|
+
export type NeedsReviewReason =
|
|
91
|
+
/** No prior decision was made on this slot. */
|
|
92
|
+
"no-prior-decision"
|
|
93
|
+
/** The slot's candidate versions differ from the ones the prior decision saw. */
|
|
94
|
+
| "candidate-changed"
|
|
95
|
+
/** A version id matched, but the candidate's value, locator, source or claim target did not. */
|
|
96
|
+
| "candidate-content-changed"
|
|
97
|
+
/** The prior decision's resolution is not one the policy carries. */
|
|
98
|
+
| "resolution-not-carried"
|
|
99
|
+
/** More than one live prior decision names this slot. */
|
|
100
|
+
| "ambiguous-prior-decisions";
|
|
101
|
+
export interface CarriedForwardItem {
|
|
102
|
+
readonly item: RoundReviewItem;
|
|
103
|
+
readonly carryForward: DecisionCarryForward;
|
|
104
|
+
}
|
|
105
|
+
export interface NeedsReviewItem {
|
|
106
|
+
readonly item: RoundReviewItem;
|
|
107
|
+
readonly reason: NeedsReviewReason;
|
|
108
|
+
/** The prior decision the item could not inherit, when there was one. */
|
|
109
|
+
readonly priorDecision?: DecisionReference;
|
|
110
|
+
}
|
|
111
|
+
export interface SplitRoundForCarryForwardInput {
|
|
112
|
+
/** Identity of the new round. */
|
|
113
|
+
readonly roundId: string;
|
|
114
|
+
readonly items: readonly RoundReviewItem[];
|
|
115
|
+
readonly priorDecisions: readonly PriorRoundDecision[];
|
|
116
|
+
/** Supersessions already recorded: a prior decision they name is no longer live. */
|
|
117
|
+
readonly supersessions?: readonly DecisionSupersession[];
|
|
118
|
+
readonly policy?: CarryForwardPolicy;
|
|
119
|
+
}
|
|
120
|
+
export interface SplitRoundForCarryForwardResult {
|
|
121
|
+
readonly carriedForward: readonly CarriedForwardItem[];
|
|
122
|
+
readonly needsReview: readonly NeedsReviewItem[];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Splits a new round's items into those that inherit a prior decision
|
|
126
|
+
* (with a {@link DecisionCarryForward} record naming it) and those that need
|
|
127
|
+
* review. Pure; performs no I/O. An item carries forward only when exactly one
|
|
128
|
+
* live prior decision names its slot, the slot's candidates have the same
|
|
129
|
+
* version ids in both rounds, each candidate's value, locator, excerpt and
|
|
130
|
+
* source are unchanged, and the policy carries the prior resolution.
|
|
131
|
+
*/
|
|
132
|
+
export declare function splitRoundForCarryForward(input: SplitRoundForCarryForwardInput): SplitRoundForCarryForwardResult;
|
|
133
|
+
export interface BuildDecisionSupersessionInput {
|
|
134
|
+
readonly prior: PriorRoundDecision;
|
|
135
|
+
/** The new round's item the new decision was made on. */
|
|
136
|
+
readonly item: RoundReviewItem;
|
|
137
|
+
readonly decision: ReviewDecision;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Records that `decision`, made on the new round's item for the same slot,
|
|
141
|
+
* replaces `prior`. The reason is derived: `candidate-changed` when the slot's
|
|
142
|
+
* candidate versions or content differ, `re-reviewed` when they do not.
|
|
143
|
+
*/
|
|
144
|
+
export declare function buildDecisionSupersession(input: BuildDecisionSupersessionInput): DecisionSupersession;
|
|
145
|
+
/** The name and canonical digest of a decision. */
|
|
146
|
+
export declare function decisionReference(decision: ReviewDecision): DecisionReference;
|
|
147
|
+
/**
|
|
148
|
+
* The terminal resolution a decision reached: `could_not_confirm`, `rejected`
|
|
149
|
+
* for a rejected status, `accepted` for a verified or assumed one. Undefined
|
|
150
|
+
* for a decision that reached none (a `proposed` status without
|
|
151
|
+
* could-not-confirm).
|
|
152
|
+
*/
|
|
153
|
+
export declare function decisionResolution(decision: ReviewDecision): CarriedResolution | undefined;
|
|
154
|
+
/** Validates a stored carry-forward record: its shape and its content-addressed id. */
|
|
155
|
+
export declare function validateDecisionCarryForward(value: unknown): DecisionCarryForward;
|
|
156
|
+
/** Validates a stored supersession record: its shape and its content-addressed id. */
|
|
157
|
+
export declare function validateDecisionSupersession(value: unknown): DecisionSupersession;
|