Adversarial Passes And Learning Projections#
Survey records adversarial review rounds and projects what reviewers actually corrected into learning signals. This is the producer-side half of the adversarial-review pattern: Kontour Flow owns the orchestration — route-back budgets and transition accounting — while Survey owns the per-round review evidence those gates consume.
Adversarial passes#
Producers that run a second adversarial pass — whether an LLM judge, a rules
engine, or a second human reviewer — emit their output into Survey as a normal
producer pass with a distinct extractor id. Survey does not know or care that
a second pass ran; it sees two producers disagreeing on the same target, which
is exactly what conflict and escalation records are for.
Two patterns cover the adversary's output:
Conflicting candidate. The adversary disagrees with the first-pass extraction
value. Add the adversary's extraction as a second candidate to the same candidate
set using candidateReviewRecord with status: "conflict". Survey projects the
conflict to a disputed claim in Surface.
import { candidateReviewRecord, fieldObservation, SurveyInputBuilder } from "@kontourai/survey";
const records = candidateReviewRecord({
id: "candidate-set.entity-1.registration-status",
target: "registrationStatus",
status: "conflict",
rationale: "First pass and adversary disagree; human review required.",
observations: [
fieldObservation({
id: "observation.entity-1.status.first-pass",
field: "registrationStatus",
value: "ACTIVE",
rawSource: {
kind: "api-record",
sourceRef: "records://entity-1/registry",
observedAt: new Date().toISOString(),
locatorScheme: "structured-field",
},
extraction: {
confidence: 0.91,
locator: "json:$.registrationStatus",
extractor: "agent-v1",
extractedAt: new Date().toISOString(),
},
candidate: { id: "candidate.first-pass", confidence: 0.91 },
claim: {
subjectType: "public-record.entity",
subjectId: "entity-1",
surface: "public-record.profile",
claimType: "public-data.field",
impactLevel: "high",
collectedBy: "agent-v1",
},
}),
fieldObservation({
id: "observation.entity-1.status.adversary",
field: "registrationStatus",
value: "INACTIVE",
rawSource: {
kind: "api-record",
sourceRef: "records://entity-1/registry",
observedAt: new Date().toISOString(),
locatorScheme: "structured-field",
},
extraction: {
confidence: 0.84,
locator: "json:$.registrationStatus",
extractor: "adversary-v1",
extractedAt: new Date().toISOString(),
},
candidate: { id: "candidate.adversary", confidence: 0.84 },
claim: {
subjectType: "public-record.entity",
subjectId: "entity-1",
surface: "public-record.profile",
claimType: "public-data.field",
impactLevel: "high",
collectedBy: "adversary-v1",
},
}),
],
});
Framing challenge. The adversary identifies a target that was not addressed
at all — a missed standard, an unconsidered alternative, or a misframed question.
Use addEscalation to record the challenge. Attach it to the closest relevant
claim with attachToClaimId; Survey projects it as an additional disputed
verification event on that claim so the reviewer sees it prominently.
import { SurveyInputBuilder, fieldObservation } from "@kontourai/survey";
const builder = new SurveyInputBuilder({ source: "example-producer:run-2" });
// First-pass observation
builder.addObservation(fieldObservation({ /* ... */ }));
// Adversary raises a framing challenge
builder.addEscalation({
id: "escalation.entity-1.fair-value.completeness",
target: "fairValue",
dimension: "completeness",
reason: "Measurement standard Level 3 inputs were not documented; sensitivity range and unobservable input assumptions are missing.",
raisedBy: "adversary-v1",
raisedAt: new Date().toISOString(),
attachToClaimId: "claim.entity-1.fair-value",
});
If a subsequent first-pass or human-review pass resolves the challenge, set
resolvedBy to the id of the observation that closes it. Survey will not project
a disputed event for resolved escalations.
Escalation dimensions follow the adversary's attack surface: framing (wrong
question framed), completeness (missing standards, alternatives, or evidence),
conclusion (reasoning would not survive challenge), and citation (cited
sources do not support the claims attached to them).
Framing challenges without an attachToClaimId are carried in SurveyInput
for producer tooling but are not projected to Surface. If the adversary cannot
identify a target claim to attach a framing challenge to, emit a candidate set
with status: "escalated" for the affected target — that projects to disputed
in Surface with a candidate-escalation event.
Learning projections#
Use buildSurveyLearningProjections(input) when producer or review tooling needs
workflow/evaluation signals without changing Surface TrustBundle.
import {
buildSurveyLearningProjections,
buildSurveyTrustBundle,
} from "@kontourai/survey";
const learning = buildSurveyLearningProjections(surveyInput);
const trustBundle = buildSurveyTrustBundle(surveyInput);
Learning projections are product-neutral learning.* records. Survey emits
learning.rejected-candidate from structured candidate rejection data such as
non-empty Candidate.rejectionReason values or a candidate-specific
ReviewOutcome.status === "rejected" outcome with rationale. When both exist,
Survey emits one rejected-candidate projection enriched with candidate and
review outcome context.
Ordinary rejected candidates do not emit learning.comfort-zone.
Survey also emits learning.comfort-zone from structured
ReviewOutcome.withinComfortZone === false data and learning.escalation from
unresolved EscalationRecords, including unattached records that producer
tooling can route but Surface cannot attach to a claim event.
These projections are producer/review workflow and evaluation signals. They are
not claims about truth or veracity, not Surface claim status, not evidence, and
not verification events. Calling buildSurveyLearningProjections does not alter
buildSurveyTrustBundle, trust status derivation, or escalation event projection.