@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.
Files changed (45) hide show
  1. package/dist/src/calibration.d.ts +15 -0
  2. package/dist/src/calibration.js +11 -0
  3. package/dist/src/canonical-reviewed-trust-input.d.ts +10 -0
  4. package/dist/src/canonical-reviewed-trust-input.js +45 -1
  5. package/dist/src/console/review-console-server.js +22 -4
  6. package/dist/src/decision-carry-forward.d.ts +157 -0
  7. package/dist/src/decision-carry-forward.js +295 -0
  8. package/dist/src/extraction-envelope.d.ts +4 -3
  9. package/dist/src/extraction-envelope.js +3 -3
  10. package/dist/src/field-states.d.ts +100 -0
  11. package/dist/src/field-states.js +169 -0
  12. package/dist/src/index.d.ts +6 -4
  13. package/dist/src/index.js +3 -1
  14. package/dist/src/mcp/review-mcp.js +173 -45
  15. package/dist/src/review-resource.d.ts +36 -0
  16. package/dist/src/review-session-file.d.ts +7 -0
  17. package/dist/src/review-workbench/audit-rows.d.ts +1 -1
  18. package/dist/src/review-workbench/audit-rows.js +1 -0
  19. package/dist/src/review-workbench/audit-sample.d.ts +32 -0
  20. package/dist/src/review-workbench/audit-sample.js +53 -0
  21. package/dist/src/review-workbench/extraction-inspector.d.ts +1 -1
  22. package/dist/src/review-workbench/extraction-inspector.js +1 -1
  23. package/dist/src/review-workbench/field-state-view.d.ts +15 -0
  24. package/dist/src/review-workbench/field-state-view.js +84 -0
  25. package/dist/src/review-workbench/queue-binding.d.ts +45 -2
  26. package/dist/src/review-workbench/queue-binding.js +45 -0
  27. package/dist/src/review-workbench/review-presentation.d.ts +23 -0
  28. package/dist/src/review-workbench/review-presentation.js +51 -10
  29. package/dist/src/review-workbench/review-queue-session.d.ts +93 -5
  30. package/dist/src/review-workbench/review-queue-session.js +147 -11
  31. package/dist/src/review-workbench/review-session-replay.d.ts +11 -1
  32. package/dist/src/review-workbench/review-session-replay.js +49 -1
  33. package/dist/src/review-workbench/review-workbench-css.generated.js +126 -0
  34. package/dist/src/review-workbench/review-workbench-element.d.ts +10 -0
  35. package/dist/src/review-workbench/review-workbench-element.js +21 -3
  36. package/dist/src/review-workbench/review-workbench.css +126 -0
  37. package/dist/src/review-workbench/review-workbench.d.ts +40 -8
  38. package/dist/src/review-workbench/review-workbench.js +192 -71
  39. package/dist/src/review-workbench/review-workbench.standalone.css +126 -0
  40. package/dist/src/review-workbench/server-review-session.d.ts +18 -0
  41. package/dist/src/review-workbench/server-review-session.js +20 -2
  42. package/dist/src/surface-reviewed-extraction.d.ts +23 -0
  43. package/dist/src/surface-reviewed-extraction.js +64 -1
  44. package/dist/src/to-surface.js +32 -3
  45. 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 {
@@ -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) throw new Error("Session fetch failed: " + res.status);
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;