Field States, Carry-Forward and Score-Blind Audit#

Decision#

Field states are derived, never stored. deriveFieldStates reads import records, decisions, and optionally verifier records, carry-forward records and supersessions, and returns a content state and a lifecycle state per claim slot. A state appears only when the input that produces it is present.

  • Content: value, conflicting (two or more distinct values), unsupported (every candidate has a contradicted or not-addressed verifier record for its current value and none supported; precedence over the other two), excluded (proposals were made for the field but the import left every one out because its excerpt was not at its span: the field was read, so it is never not_covered), not_covered (no proposal at all, and the run was partial or failed).
  • Lifecycle: pending, accepted, rejected, could_not_confirm, superseded (a supersession names the decision).
  • Signals, not states: incompleteRun, excludedProposals, unresolvedImport.

not_found, stale, held, stated_none and not_applicable are not shipped: no producer computes them. A field with no candidate in a complete run has no content state, because chunk coverage does not show that one field's proposal was not dropped. A value from a partial run stays value but carries incompleteRun, so a run that lost content never reads as complete.

Unit. One import record is one source extraction; states are keyed by (import, claim slot), where the slot is the importer's grouping identity (subject, facet, claim type, field, claim id, path indices). A field with no proposal appears only when the caller passes expectedFields. Merging slots across imports of different sources is not done.

Surface projection. buildCanonicalReviewedTrustInput({ fieldStates }) derives the states from the imports and the results' own decisions and writes them under claim metadata key survey.kontourai.io/field-state ({ schemaVersion: 1, content?, lifecycle?, decisionBasis?, signals? }). Claim status is unchanged.

Carry-forward. splitRoundForCarryForward takes the new round's items with producer slotId and per-candidate version ids (Survey validates presence and never invents them) and the prior round's decisions. An item carries forward only when exactly one live prior decision names its slot, the version-id sets are equal, and each candidate's value, full locator (scheme, locator, excerpt), source ref, source checksum and full claim target (subject, field, impact and the rest) are unchanged. The last check means a decision never carries across changed content even if a producer reuses a version id. By default only accepted carries; rejected and could_not_confirm carry only under an explicit policy.carry. The output is a content-addressed DecisionCarryForward record naming the prior decision by name and digest. buildDecisionSupersession records candidate-changed or re-reviewed. The vocabulary is carried-forward versus affirmed, matching the round receipts producers already keep. Field states expose it as decisionBasis.

Score-blind audit. A session's presentation: { scoreBlind } and sampling sit in the snapshot, and every decision event is stamped with them (data.sessionConditions). Replay refuses an event whose stamp differs from the snapshot's, so sighted events cannot be replayed over a blind snapshot and a snapshot's presentation cannot be flipped after decisions were recorded. Full tamper evidence needs a ReviewQueueBinding: without one (the MCP and console file sessions), an edit that changes the snapshot and every event stamp together is not detectable. The session validates sampling (rate in (0, 1], seed a stable identity). Calibration takes outcome metadata at its word, so it should only read outcomes from Survey's apply path. In a score-blind session the workbench, the MCP item text, data and card, and the recorded decision prompt show no confidence and no verifier result, before or after a decision. Excerpt-verification notes and excluded proposals stay visible: they are about the evidence, not a verdict on the value. Decisions copy both fields into ReviewDecision.spec and the canonical projection copies them into ReviewOutcome.metadata. drawRandomAuditSample uses a per-item SHA-256 draw, so the same seed gives the same sample in any input order. deriveCalibration({ auditSamplesOnly: true }) keeps only score-blind random-audit outcomes. It is off by default.

Provenance. status.provenance is required on import records (breaking).

Left open#

  • Field-slot and version identities from the extractor (kontourai/traverse#172); until then callers compute them.
  • not_found: needs per-field chunk coverage and a dropped-proposal count.
  • Carried-forward decisions are records, not ReviewDecisions, so they do not yet project to Surface claims. A carried accept that had an edit points at the prior decision for the edited value.
  • The MCP server does not show field states. The workbench panel lists every field, including unread ones, when the host passes fieldStates.
  • A score-blind browser page still holds the item JSON in memory. The blind mode controls what is rendered, not what a determined reviewer could inspect.

Source: docs/decisions/field-states-and-carry-forward.md