Reviewed Extraction Evidence#

Surface projects reviewed extraction provenance through standard portable Evidence rather than defining a competing evidence schema. sourceRef, sourceLocator, and integrityRef remain the generic anchors. The open Evidence.metadata.reviewedExtraction namespace carries a versioned, reversible profile for the complete reviewed record.

The profile consumes the landed Survey import, ReviewItem, and ReviewDecision resource shapes. It keeps candidate confidence, reviewer disposition, collector identity, and structural validation state separate. Artifact unavailability, digest mismatch, unsupported inference vocabulary, explicitly dropped provenance, unsafe structure, and non-accepted review are typed in-band gaps. Unsafe evidence is cited, non-passing, and blocking; only accepted, verified, validated, gap-free evidence entails its claim. The native Survey accept-proposed decision is verified with no explicit resolution, and the projection preserves that shape rather than inventing one.

The golden fixture round-trips the source and prepared-artifact references, exact locator plus format coordinates, field/value and type origin, provider/model, task/example digests, attempt, and review outcome, including all prepared-artifact resolution fields. A canonical profile digest binds the full input, gaps, and every portable evidence field. Adversarial tests reject field tampering, credentials, forged artifact identity, incoherent spans, and JSON collapse; a policy regression proves rejected or invalid evidence cannot verify. The resulting evidence validates against the upstream schema. This proves the current upstream Evidence structure is sufficient, so no schema change is needed.

Reference profile (v2)#

The v1 profile embeds the whole import record, so a bundle with one evidence item per reviewed proposal of an N-proposal run carries N copies of every proposal. surface.reviewed-extraction-evidence/v2 is an opt-in alternative (projectReviewedExtractionEvidence(input, { profile })); v1 stays the default and keeps restoring unchanged, so existing bundles and the pinned v1 profile digest are unaffected.

A v2 item carries the cited proposal and importRecordDigest, the canonical JSON SHA-256 of the import record, in place of the record. The profile digest covers that input, so it binds the record by digest.

Self-verification. A v2 bundle can verify from the bundle alone, as a v1 bundle does, when it carries each of its import records: one v2 item per record holds it as an optional importRecord sidecar in its reviewedExtraction metadata, outside the digested profile input, accepted only when it hashes to that item's importRecordDigest. This is a producer precondition, not a default. Projection leaves the sidecar off (includeImportRecord defaults to false, since a sidecar on every item re-bloats the bundle past v1). The producer calls attachImportRecords(evidence, records), which gives each digest exactly one carrier (the first v2 item citing it, unless a matching carrier already exists elsewhere, which stays where it is), and can assert findUncarriedImportRecordDigests(bundle) is empty before publishing. That check covers carrier completeness and consistency (a digest with any mismatching carrier is reported), not full integrity: consistently tampered evidence still fails at restore. A reader then passes resolverFromBundle(bundle) to any restore or policy call. A bundle that misses the precondition is not accepted on trust: its uncarried items fail closed as unresolved. A bundle-level map was rejected because TrustBundle is closed (additionalProperties: false) and merge drops proof; a separate carrier Evidence item was rejected because it needs a claimId and would enter claim evaluation. Evidence metadata is open, so the sidecar needs no schema change.

Restore stays fail-closed. It resolves through the synchronous resolveImportRecord(digest) option (a caller may also back it with its own record store), refuses a record whose digest differs and an embedded proposal that is not proposals[proposalIndex], then runs the v1 validation and re-projection on the rebuilt input, so every v1 check applies. With no record, as in a sliced bundle that lost its carrier, it throws a typed ReviewedExtractionImportRecordUnresolvedError; the grounding policy reports an import-record-unresolved gap, and the other readers that restore (source-state builders, answer assessment, the Basis reviewed-source adapter) take the same optional resolver and fail closed without it. A sidecar that does not match its digest is refused: restoring its own item fails, and resolverFromBundle refuses every lookup of that digest rather than falling back to another copy. Two sidecars claiming one digest with different content therefore refuse every item of that record, since at least one of them cannot hash to the digest. A supplied resolver is consulted before an item's own sidecar, so a refused digest stays refused for the carrier too.

Known denial of service: merge keeps one copy per evidence id, choosing by content order. A tampered copy of the carrier with the same id can therefore replace the genuine carrier in a merged bundle. Every item of that record is then refused, and the refusal names the offending evidence id, so this fails closed. It is equivalent to deleting the carrier, and mergeBundlesDetailed reports the collision.

The v2 profile does not change the review shape; a review item with more than one candidate needs the v3 profile below.

Choice profile (v3, #195)#

Survey 7 groups conflicting proposals for one claim into one review item with a candidate per distinct value, and its select-proposed decision names the chosen candidate and lists the others in unselectedCandidateIds. v1 and v2 restore by re-running a validation that accepts exactly one candidate, so a reader of the current release would refuse such evidence; widening them is not additive. surface.reviewed-extraction-evidence/v3 is a new opt-in profile (reviewedExtractionEvidenceChoiceProfile); v1 and v2 are unchanged, and a test pins their bytes and profile digests from 4.3.0.

v3 is the v2 reference shape (cited proposal, importRecordDigest, optional importRecord sidecar), so it is self-verifying the same way through attachImportRecords, findUncarriedImportRecordDigests, and resolverFromBundle, and fails closed without the record. It accepts only items with two or more candidates, so v3 evidence always records a choice and a single-candidate review stays v1 or v2.

Binding. Each candidate is bound through Survey's own per-candidate envelope binding (producer["survey.kontourai.io/extraction-envelope"], with importName naming the bound import and proposalIndex). A bound candidate must have role proposed, name a distinct proposal, and meet every v1 candidate rule against that proposal (value, confidence, source, locator, extraction, model). A separate candidate-to-proposal input map was rejected: it would duplicate, and could contradict, the item it describes, as with the review signals above. The cited proposal must be one candidate's.

Decision. The decision must name one candidate (candidateId is optional in v1 and v2). When that candidate is one of several proposed values, unselectedCandidateIds must list exactly the other proposed candidates in item order, which is Survey's own rule; otherwise it must be empty. Value-neutral decisions on a conflict (reject all, could not confirm) name no candidate and are refused; they choose nothing to ground.

Projection. Evidence citing the named candidate projects as before (entails when accepted, validated, and gap-free). Evidence citing another candidate gets a candidate-not-chosen gap and is cited, so one item per candidate can be exported and only the chosen one supports the claim. Every v3 item carries a derived choice block: every candidate with its role, bound proposal, and value digest; the cited and decision candidates; and chosenOver, the rivals the cited candidate was chosen over. It is derived from the digest-bound input, and restore's re-projection compares it, so it cannot be edited. A reader sees the rivals without restoring, and the profile name alone says the value was one of several.

Grounding. A chosen value is allowed on its own evidence, like any other; the rival's evidence is refused. requireAcceptedReview treats a decision that names a different candidate as not accepting the cited one. The dimension carries the choice. The opt-in refuseChosenOverRivals refuses a chosen value with any rival (chosen-over-rival-unresolved), in the style of refuseExcludedRivals: a rival that was seen and not chosen is not disproven, and nothing currently resolves one, so every rival counts.

Prior versus proposed. The recheck shape fits the same rules: a candidate without the envelope binding is a prior value, allowed only with role current and at most once. It has no proposal, so it is never cited; it appears in choice with no proposalIndex, and a keep-current decision leaves the proposal's evidence cited with candidate-not-chosen. Accepted gaps: the proposed candidate must carry Survey's envelope binding, and the item must be non-editable, as in v1. Survey's own binding rule wants every candidate to carry the binding, so on such an item the review signals read excerpt verification as unverified.

Hidden conflicts. Dropping the rivals from a chosen conflict and projecting the rest as v1 or v2 would otherwise read as an uncontested review. The import record still holds the rival values, so the review signals check it (all profiles, derived, no profile change): hiddenRivalProposalIndices lists non-excluded proposals of the cited claim slot with a different value that no candidate carries, and, on an item whose Survey binding is intact, droppedProposalIndices lists slot or proposalIndices members that no candidate carries. A candidate's sameValueProposals counts only where the named proposal has the candidate's value. Survey 5, 6, and 7 group every non-excluded proposal of a slot into one item and list them in proposalIndices, so no Survey release writes dropped proposals: the grounding policy refuses them by default with hidden-conflict. Survey 4 and earlier wrote one item per proposal with no proposalIndices, so a legitimate older bundle has hidden rivals whose own items may have been reviewed; those are shown on the dimension (hiddenConflict) and refused only under refuseExcludedRivals or refuseChosenOverRivals. Refusing them by default would flip existing allowed decisions for bundles Surface cannot tell apart from that older shape. Stripping proposalIndices to imitate it breaks the binding, so excerpt verification also reads as unverified. Survey 6 and later write status.provenance on every import record, and Survey 4.0.0 and 5.0.0 never do (their validators reject the field). An import record that carries it therefore is not a Survey 4 import, so on an item whose binding is broken its hidden rivals are listed as dropped and refused by default. That closes the stripping path for Survey 6+ imports; a Survey 5 import whose item binding is broken still reads as the older shape.

Basis. The reviewed-source adapter uses the same rule as the policy: a v3 item whose decision names another candidate reads review: "not-chosen" with no reviewedAt and a reviewed-source-review-not-chosen gap. Every v3 context carries choice (candidateCount, and on the accepted candidate the chosenOverCandidateIds); the parser checks that shape, and the Basis field budget grows from 12 to 13 for it. An older parser refuses such a context.

Verified end to end against @kontourai/survey 7.0.0 from npm: its import, buildReviewDecision with select-proposed, and toSurfaceReviewedExtractionImport (which passes the record through) project under v3, restore from the bundle alone, and ground the claim buildSurveyTrustBundle exports, whose metadata.survey.candidates lists the same candidates with the chosen one selected. The fixture in tests/fixtures/reviewed-extraction-choice.survey7.json is that output.

Review signals (#289)#

Survey's excerpt verification and excluded proposals are read from the import record and review item the profile already binds, not added as profile fields. Projection is unchanged, so the pinned profile digests hold and no profile version bump is needed: an older reader restores the same evidence and simply does not interpret the facts. Grounding decisions over bundles without the facts are unchanged; decisions over Survey 6 bundles that carry them gain the excerptVerification and excludedRivals dimension fields. The item's facts are read only when its Survey binding is intact, and the import record's excerpt-mismatch diagnostics are checked too, so a stripped item entry cannot hide a rival. A separate input field was rejected because it would duplicate, and could contradict, the item it came from. Absence reads as unverified and as no excluded proposals; the policy requirements that use them are opt-in.

Source: docs/decisions/reviewed-extraction-evidence.md