@kontourai/survey 0.4.0 → 0.4.1

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/README.md CHANGED
@@ -183,6 +183,94 @@ preserved. Producers still own scalar semantics, validation, candidate ranking,
183
183
  review policy, and whether a value should be verified, proposed, rejected, or
184
184
  assumed.
185
185
 
186
+ ## Source-of-authority observations
187
+
188
+ Use `sourceOfAuthorityObservation` when a producer treats the raw source as
189
+ authoritative for the extracted target: an official publication, registration
190
+ platform page, policy document, contract record, or system-of-record response.
191
+ The helper does not decide whether the source is truly authoritative. It
192
+ enforces record discipline around the producer's declared source posture.
193
+
194
+ Verified or assumed source-of-authority observations require:
195
+
196
+ - source reference
197
+ - source locator
198
+ - source-authority class
199
+ - source-authority scope
200
+ - review actor
201
+ - reviewed time
202
+
203
+ Source-authority metadata projects through Surface Evidence metadata under
204
+ `sourceAuthority`. It does not project to Surface `authorityTrace`, which is
205
+ reserved for actor, credential, role, organization, policy, or system authority.
206
+
207
+ ```ts
208
+ import {
209
+ buildSurveyTrustInput,
210
+ sourceOfAuthorityObservation,
211
+ SurveyInputBuilder,
212
+ uploadedDocumentSource,
213
+ } from "@kontourai/survey";
214
+
215
+ const observedAt = new Date().toISOString();
216
+ const rawSource = uploadedDocumentSource({
217
+ sourceRef: "https://rules.example.test/standard-deduction.pdf",
218
+ observedAt,
219
+ checksum: "abc123",
220
+ locatorScheme: "pdf",
221
+ });
222
+
223
+ const surveyInput = new SurveyInputBuilder({
224
+ source: "rule-producer:run-1",
225
+ })
226
+ .addObservation(sourceOfAuthorityObservation({
227
+ id: "rule.standard-deduction.mfj.2026",
228
+ field: "federal.standardDeduction.mfj.2026",
229
+ value: 30000,
230
+ sourceAuthority: {
231
+ authorityClass: "official_publication",
232
+ scope: {
233
+ jurisdiction: "federal",
234
+ productArea: "regulated-rule",
235
+ taxYear: 2026,
236
+ },
237
+ sourceVersion: "2026",
238
+ declaredBy: "rule-producer",
239
+ },
240
+ rawSource,
241
+ extraction: {
242
+ confidence: 0.94,
243
+ locator: "pdf:page=12;table=standard-deduction;row=mfj",
244
+ extractor: "rule-producer",
245
+ extractedAt: observedAt,
246
+ },
247
+ reviewOutcome: {
248
+ status: "verified",
249
+ actor: "rule-reviewer",
250
+ reviewedAt: new Date().toISOString(),
251
+ },
252
+ claim: {
253
+ subjectType: "regulated-rule",
254
+ subjectId: "federal:standard-deduction:mfj:2026",
255
+ surface: "regulated.rules",
256
+ claimType: "regulated.rule-value",
257
+ status: "verified",
258
+ impactLevel: "high",
259
+ evidenceType: "policy_rule",
260
+ evidenceMethod: "extraction",
261
+ collectedBy: "rule-producer",
262
+ },
263
+ }))
264
+ .build();
265
+
266
+ const trustInput = buildSurveyTrustInput(surveyInput);
267
+ ```
268
+
269
+ Contextual claims such as "this return position is compliant" or "this camp is
270
+ eligible for an 8-year-old in June" are not source-of-authority observations.
271
+ They are Surface claims with Claim Dependencies on source-of-authority claims
272
+ and other product facts. The vertical product owns that domain logic.
273
+
186
274
  ## Reviewed candidate resolutions
187
275
 
188
276
  Use `reviewedCandidateResolution` when a producer has multiple candidate
@@ -384,6 +472,53 @@ Conflict before review has resolved which candidate should win. When no review
384
472
  outcome overrides it, `buildSurveyTrustInput` projects the claim to Surface
385
473
  status `"disputed"` and records a `"candidate-conflict"` verification event.
386
474
 
475
+ ## Review proofs
476
+
477
+ Use review proof helpers when a producer wants a Surface-compatible integrity
478
+ anchor for one reviewed Survey source -> extraction -> candidate -> review ->
479
+ claim path.
480
+
481
+ ```ts
482
+ import {
483
+ buildCanonicalReviewProofPayload,
484
+ buildReviewProofAnchor,
485
+ canonicalReviewProofJson,
486
+ hashCanonicalReviewProofPayload,
487
+ } from "@kontourai/survey";
488
+
489
+ const proofInput = {
490
+ rawSource,
491
+ extraction,
492
+ candidate,
493
+ candidateSet,
494
+ reviewOutcome,
495
+ claim,
496
+ };
497
+
498
+ const payload = buildCanonicalReviewProofPayload(proofInput);
499
+ const canonicalJson = canonicalReviewProofJson(payload);
500
+ const hash = hashCanonicalReviewProofPayload(payload);
501
+ const anchor = buildReviewProofAnchor(proofInput);
502
+ ```
503
+
504
+ `buildReviewProofAnchor` returns a hash-only Surface `IntegrityAnchor` for the
505
+ canonical payload. The lower-level payload, JSON, and hash helpers are exported
506
+ so producers can store or recompute the exact canonical proof material used for
507
+ the anchor. Producer metadata is not part of the canonical payload; any
508
+ non-portable context belongs outside the hash, such as anchor metadata.
509
+
510
+ When the Surface projection proof option is enabled, `buildSurveyTrustInput`
511
+ will attach the same kind of anchor to the projected reviewed claim:
512
+
513
+ ```ts
514
+ const trustInput = buildSurveyTrustInput(surveyInput, { reviewProofs: true });
515
+ ```
516
+
517
+ The proof provides hash-only tamper evidence for the Survey review/provenance
518
+ trail in the canonical payload. It does not authenticate an actor, sign the
519
+ payload, or prove the real-world truth of the claim. Non-goals include JWT/JWS
520
+ signing, key management, a transparency log, and any veracity guarantee.
521
+
387
522
  ## Computed values
388
523
 
389
524
  Computed values are normal `ClaimTarget` entries in `claims`. Producers should
@@ -395,6 +530,146 @@ Survey passes those fields through to Surface while keeping the same
395
530
  source -> extraction -> candidate -> review -> claim projection path. Surface
396
531
  owns dependency semantics such as recompute pressure and status ceilings.
397
532
 
533
+ ## Adversarial passes
534
+
535
+ Producers that run a second adversarial pass — whether an LLM judge, a rules
536
+ engine, or a second human reviewer — emit their output into Survey as a normal
537
+ producer pass with a distinct `extractor` id. Survey does not know or care that
538
+ a second pass ran; it sees two producers disagreeing on the same target, which
539
+ is exactly what `conflict` and escalation records are for.
540
+
541
+ Two patterns cover the adversary's output:
542
+
543
+ **Conflicting candidate.** The adversary disagrees with the first-pass extraction
544
+ value. Add the adversary's extraction as a second candidate to the same candidate
545
+ set using `candidateReviewRecord` with `status: "conflict"`. Survey projects the
546
+ conflict to a `disputed` claim in Surface.
547
+
548
+ ```ts
549
+ import { candidateReviewRecord, fieldObservation, SurveyInputBuilder } from "@kontourai/survey";
550
+
551
+ const records = candidateReviewRecord({
552
+ id: "candidate-set.entity-1.registration-status",
553
+ target: "registrationStatus",
554
+ status: "conflict",
555
+ rationale: "First pass and adversary disagree; human review required.",
556
+ observations: [
557
+ fieldObservation({
558
+ id: "observation.entity-1.status.first-pass",
559
+ field: "registrationStatus",
560
+ value: "ACTIVE",
561
+ rawSource: {
562
+ kind: "api-record",
563
+ sourceRef: "records://entity-1/registry",
564
+ observedAt: new Date().toISOString(),
565
+ locatorScheme: "structured-field",
566
+ },
567
+ extraction: {
568
+ confidence: 0.91,
569
+ locator: "json:$.registrationStatus",
570
+ extractor: "agent-v1",
571
+ extractedAt: new Date().toISOString(),
572
+ },
573
+ candidate: { id: "candidate.first-pass", confidence: 0.91 },
574
+ claim: {
575
+ subjectType: "public-record.entity",
576
+ subjectId: "entity-1",
577
+ surface: "public-record.profile",
578
+ claimType: "public-data.field",
579
+ impactLevel: "high",
580
+ collectedBy: "agent-v1",
581
+ },
582
+ }),
583
+ fieldObservation({
584
+ id: "observation.entity-1.status.adversary",
585
+ field: "registrationStatus",
586
+ value: "INACTIVE",
587
+ rawSource: {
588
+ kind: "api-record",
589
+ sourceRef: "records://entity-1/registry",
590
+ observedAt: new Date().toISOString(),
591
+ locatorScheme: "structured-field",
592
+ },
593
+ extraction: {
594
+ confidence: 0.84,
595
+ locator: "json:$.registrationStatus",
596
+ extractor: "adversary-v1",
597
+ extractedAt: new Date().toISOString(),
598
+ },
599
+ candidate: { id: "candidate.adversary", confidence: 0.84 },
600
+ claim: {
601
+ subjectType: "public-record.entity",
602
+ subjectId: "entity-1",
603
+ surface: "public-record.profile",
604
+ claimType: "public-data.field",
605
+ impactLevel: "high",
606
+ collectedBy: "adversary-v1",
607
+ },
608
+ }),
609
+ ],
610
+ });
611
+ ```
612
+
613
+ **Framing challenge.** The adversary identifies a target that was not addressed
614
+ at all — a missed standard, an unconsidered alternative, or a misframed question.
615
+ Use `addEscalation` to record the challenge. Attach it to the closest relevant
616
+ claim with `attachToClaimId`; Survey projects it as an additional `disputed`
617
+ verification event on that claim so the reviewer sees it prominently.
618
+
619
+ ```ts
620
+ import { SurveyInputBuilder, fieldObservation } from "@kontourai/survey";
621
+
622
+ const builder = new SurveyInputBuilder({ source: "example-producer:run-2" });
623
+
624
+ // First-pass observation
625
+ builder.addObservation(fieldObservation({ /* ... */ }));
626
+
627
+ // Adversary raises a framing challenge
628
+ builder.addEscalation({
629
+ id: "escalation.entity-1.fair-value.completeness",
630
+ target: "fairValue",
631
+ dimension: "completeness",
632
+ reason: "Measurement standard Level 3 inputs were not documented; sensitivity range and unobservable input assumptions are missing.",
633
+ raisedBy: "adversary-v1",
634
+ raisedAt: new Date().toISOString(),
635
+ attachToClaimId: "claim.entity-1.fair-value",
636
+ });
637
+ ```
638
+
639
+ If a subsequent first-pass or human-review pass resolves the challenge, set
640
+ `resolvedBy` to the id of the observation that closes it. Survey will not project
641
+ a `disputed` event for resolved escalations.
642
+
643
+ Escalation dimensions follow the adversary's attack surface: `framing` (wrong
644
+ question framed), `completeness` (missing standards, alternatives, or evidence),
645
+ `conclusion` (reasoning would not survive challenge), and `citation` (cited
646
+ sources do not support the claims attached to them).
647
+
648
+ Framing challenges without an `attachToClaimId` are carried in `SurveyInput`
649
+ for producer tooling but are not projected to Surface. If the adversary cannot
650
+ identify a target claim to attach a framing challenge to, emit a candidate set
651
+ with `status: "escalated"` for the affected target — that projects to `disputed`
652
+ in Surface with a `candidate-escalation` event.
653
+
654
+ ## Comfort zone flags
655
+
656
+ Use `withinComfortZone: false` on a `ReviewOutcome` when the reviewer is
657
+ recording a decision outside their domain expertise or is flagging that the
658
+ conclusion requires a different authority to confirm. The flag and optional
659
+ `comfortZoneNote` are carried forward to the Surface verification event `notes`
660
+ so the reviewer chain sees the signal without having to read into the rationale.
661
+
662
+ ```ts
663
+ reviewOutcome: {
664
+ status: "assumed",
665
+ actor: "records-operator",
666
+ reviewedAt: new Date().toISOString(),
667
+ rationale: "Assumed from registry source pending specialist review.",
668
+ withinComfortZone: false,
669
+ comfortZoneNote: "Renewal clause interpretation requires specialist counsel.",
670
+ },
671
+ ```
672
+
398
673
  ## Product Boundary
399
674
 
400
675
  Survey does not crawl pages, parse PDFs, rank candidates, decide review policy,
@@ -1,4 +1,4 @@
1
- import type { CandidateSet, ClaimTarget, Extraction, RawSource, ReviewOutcome, SurveyInput } from "./types.js";
1
+ import type { CandidateSet, ClaimTarget, EscalationRecord, Extraction, RawSource, ReviewOutcome, SurveyInput } from "./types.js";
2
2
  export interface SurveyInputBuilderArgs {
3
3
  source: string;
4
4
  generatedAt?: string;
@@ -58,12 +58,14 @@ export declare class SurveyInputBuilder {
58
58
  private readonly candidateSets;
59
59
  private readonly reviewOutcomes;
60
60
  private readonly claims;
61
+ private readonly escalations;
61
62
  constructor(args: SurveyInputBuilderArgs);
62
63
  addRawSource(rawSource: RawSource): this;
63
64
  addExtraction(extraction: Extraction): this;
64
65
  addCandidateSet(candidateSet: CandidateSet): this;
65
66
  addReviewOutcome(reviewOutcome: ReviewOutcome): this;
66
67
  addClaim(claim: ClaimTarget): this;
68
+ addEscalation(escalation: EscalationRecord): this;
67
69
  addClaimRecord(record: SurveyClaimRecord): this;
68
70
  addClaimRecords(records: SurveyClaimRecord[]): this;
69
71
  addObservation(observation: SurveyObservationInput): this;
@@ -6,6 +6,7 @@ export class SurveyInputBuilder {
6
6
  candidateSets = new Map();
7
7
  reviewOutcomes = new Map();
8
8
  claims = new Map();
9
+ escalations = new Map();
9
10
  constructor(args) {
10
11
  this.source = args.source;
11
12
  this.generatedAt = args.generatedAt ?? new Date().toISOString();
@@ -30,6 +31,10 @@ export class SurveyInputBuilder {
30
31
  addUnique(this.claims, claim, "claim target");
31
32
  return this;
32
33
  }
34
+ addEscalation(escalation) {
35
+ addUnique(this.escalations, escalation, "escalation");
36
+ return this;
37
+ }
33
38
  addClaimRecord(record) {
34
39
  this.addRecordRawSource(record.rawSource);
35
40
  this.addExtraction(record.extraction);
@@ -61,6 +66,7 @@ export class SurveyInputBuilder {
61
66
  candidateSets: [...this.candidateSets.values()],
62
67
  reviewOutcomes: [...this.reviewOutcomes.values()],
63
68
  claims: [...this.claims.values()],
69
+ escalations: this.escalations.size > 0 ? [...this.escalations.values()] : undefined,
64
70
  };
65
71
  }
66
72
  addRecordCandidateSet(candidateSet) {
@@ -1,4 +1,4 @@
1
- export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, Extraction, LocatorScheme, RawSource, RawSourceKind, ReviewOutcome, ReviewStatus, SurveyInput, } from "./types.js";
1
+ export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, EscalationDimension, EscalationRecord, Extraction, LocatorScheme, RawSource, RawSourceKind, ReviewOutcome, ReviewStatus, SurveyInput, } from "./types.js";
2
2
  export { candidateReviewRecord, SurveyInputBuilder } from "./builder.js";
3
3
  export type { CandidateReviewRecordInput, SurveyClaimRecord, SurveyInputBuilderArgs, SurveyObservationInput, } from "./builder.js";
4
4
  export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
@@ -6,9 +6,14 @@ export type { ReviewedCandidateResolutionInput } from "./reviewed-candidate-reso
6
6
  export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
7
7
  export type { CurrentProposedCandidateRole, ReviewedCurrentProposedResolutionInput, } from "./reviewed-current-proposed-resolution.js";
8
8
  export { buildSurveyTrustInput } from "./to-surface.js";
9
+ export type { BuildSurveyTrustInputOptions } from "./to-surface.js";
10
+ export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalReviewProofJson, hashCanonicalReviewProofPayload, } from "./review-proof.js";
11
+ export type { CanonicalReviewProofPayload, ReviewProofInput, } from "./review-proof.js";
9
12
  export { fieldObservation } from "./field-observation.js";
10
13
  export type { FieldObservationInput } from "./field-observation.js";
11
14
  export { repeatedObservation } from "./repeated-observation.js";
12
15
  export type { RepeatedObservationInput } from "./repeated-observation.js";
16
+ export { sourceOfAuthorityObservation } from "./source-of-authority-observation.js";
17
+ export type { SourceAuthorityClass, SourceAuthorityMetadata, SourceOfAuthorityObservationInput, } from "./source-of-authority-observation.js";
13
18
  export { apiRecordSource, manualEntrySource, uploadedDocumentSource, webPageSource, } from "./raw-source.js";
14
19
  export type { ApiRecordSourceInput, ChecksumInput, ManualEntrySourceInput, RawSourceInput, UploadedDocumentSourceInput, WebPageSourceInput, } from "./raw-source.js";
package/dist/src/index.js CHANGED
@@ -2,6 +2,8 @@ export { candidateReviewRecord, SurveyInputBuilder } from "./builder.js";
2
2
  export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
3
3
  export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
4
4
  export { buildSurveyTrustInput } from "./to-surface.js";
5
+ export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalReviewProofJson, hashCanonicalReviewProofPayload, } from "./review-proof.js";
5
6
  export { fieldObservation } from "./field-observation.js";
6
7
  export { repeatedObservation } from "./repeated-observation.js";
8
+ export { sourceOfAuthorityObservation } from "./source-of-authority-observation.js";
7
9
  export { apiRecordSource, manualEntrySource, uploadedDocumentSource, webPageSource, } from "./raw-source.js";
@@ -0,0 +1,88 @@
1
+ import type { IntegrityAnchor } from "@kontourai/surface";
2
+ import type { Candidate, CandidateSet, ClaimTarget, Extraction, RawSource, ReviewOutcome } from "./types.js";
3
+ export interface ReviewProofInput {
4
+ rawSource: RawSource;
5
+ extraction: Extraction;
6
+ candidate: Candidate;
7
+ candidateSet: CandidateSet;
8
+ reviewOutcome?: ReviewOutcome;
9
+ claim: ClaimTarget;
10
+ sourceRef?: string;
11
+ observedAt?: string;
12
+ metadata?: Record<string, unknown>;
13
+ }
14
+ export interface CanonicalReviewProofPayload {
15
+ schemaVersion: 1;
16
+ rawSource: {
17
+ id: string;
18
+ kind: RawSource["kind"];
19
+ sourceRef: string;
20
+ observedAt: string;
21
+ fetchedAt?: string;
22
+ checksum?: string;
23
+ locatorScheme: RawSource["locatorScheme"];
24
+ };
25
+ extraction: {
26
+ id: string;
27
+ sourceId: string;
28
+ target: string;
29
+ value: unknown;
30
+ confidence?: number;
31
+ locator?: string;
32
+ excerpt?: string;
33
+ extractor: string;
34
+ extractedAt: string;
35
+ };
36
+ candidate: {
37
+ id: string;
38
+ extractionId: string;
39
+ value: unknown;
40
+ confidence?: number;
41
+ sourceRank?: number;
42
+ };
43
+ candidateSet: {
44
+ id: string;
45
+ target: string;
46
+ candidateIds: string[];
47
+ selectedCandidateId?: string;
48
+ status: CandidateSet["status"];
49
+ rationale?: string;
50
+ };
51
+ reviewOutcome?: {
52
+ id: string;
53
+ candidateSetId: string;
54
+ candidateId?: string;
55
+ status: ReviewOutcome["status"];
56
+ actor?: string;
57
+ reviewedAt?: string;
58
+ rationale?: string;
59
+ evidenceIds?: string[];
60
+ };
61
+ claim: {
62
+ id: string;
63
+ candidateSetId: string;
64
+ candidateId?: string;
65
+ subjectType: string;
66
+ subjectId: string;
67
+ surface: string;
68
+ claimType: string;
69
+ fieldOrBehavior: string;
70
+ value?: unknown;
71
+ status?: ClaimTarget["status"];
72
+ impactLevel: ClaimTarget["impactLevel"];
73
+ createdAt?: string;
74
+ updatedAt?: string;
75
+ evidenceType?: ClaimTarget["evidenceType"];
76
+ evidenceMethod?: ClaimTarget["evidenceMethod"];
77
+ confidenceBasis?: ClaimTarget["confidenceBasis"];
78
+ derivedFrom?: string[];
79
+ derivationEdges?: Array<Omit<NonNullable<ClaimTarget["derivationEdges"]>[number], "metadata">>;
80
+ collectedBy: string;
81
+ actor?: string;
82
+ eventMethod?: string;
83
+ };
84
+ }
85
+ export declare function buildCanonicalReviewProofPayload(input: ReviewProofInput): CanonicalReviewProofPayload;
86
+ export declare function canonicalReviewProofJson(payload: CanonicalReviewProofPayload): string;
87
+ export declare function hashCanonicalReviewProofPayload(payload: CanonicalReviewProofPayload): string;
88
+ export declare function buildReviewProofAnchor(input: ReviewProofInput): IntegrityAnchor;
@@ -0,0 +1,124 @@
1
+ import { createHash } from "node:crypto";
2
+ export function buildCanonicalReviewProofPayload(input) {
3
+ return {
4
+ schemaVersion: 1,
5
+ rawSource: {
6
+ id: input.rawSource.id,
7
+ kind: input.rawSource.kind,
8
+ sourceRef: input.rawSource.sourceRef,
9
+ observedAt: input.rawSource.observedAt,
10
+ fetchedAt: input.rawSource.fetchedAt,
11
+ checksum: input.rawSource.checksum,
12
+ locatorScheme: input.rawSource.locatorScheme,
13
+ },
14
+ extraction: {
15
+ id: input.extraction.id,
16
+ sourceId: input.extraction.sourceId,
17
+ target: input.extraction.target,
18
+ value: input.extraction.value,
19
+ confidence: input.extraction.confidence,
20
+ locator: input.extraction.locator,
21
+ excerpt: input.extraction.excerpt,
22
+ extractor: input.extraction.extractor,
23
+ extractedAt: input.extraction.extractedAt,
24
+ },
25
+ candidate: {
26
+ id: input.candidate.id,
27
+ extractionId: input.candidate.extractionId,
28
+ value: input.candidate.value,
29
+ confidence: input.candidate.confidence,
30
+ sourceRank: input.candidate.sourceRank,
31
+ },
32
+ candidateSet: {
33
+ id: input.candidateSet.id,
34
+ target: input.candidateSet.target,
35
+ candidateIds: input.candidateSet.candidates.map((candidate) => candidate.id).sort(),
36
+ selectedCandidateId: input.candidateSet.selectedCandidateId,
37
+ status: input.candidateSet.status,
38
+ rationale: input.candidateSet.rationale,
39
+ },
40
+ reviewOutcome: input.reviewOutcome
41
+ ? {
42
+ id: input.reviewOutcome.id,
43
+ candidateSetId: input.reviewOutcome.candidateSetId,
44
+ candidateId: input.reviewOutcome.candidateId,
45
+ status: input.reviewOutcome.status,
46
+ actor: input.reviewOutcome.actor,
47
+ reviewedAt: input.reviewOutcome.reviewedAt,
48
+ rationale: input.reviewOutcome.rationale,
49
+ evidenceIds: input.reviewOutcome.evidenceIds ? [...input.reviewOutcome.evidenceIds].sort() : undefined,
50
+ }
51
+ : undefined,
52
+ claim: {
53
+ id: input.claim.id,
54
+ candidateSetId: input.claim.candidateSetId,
55
+ candidateId: input.claim.candidateId,
56
+ subjectType: input.claim.subjectType,
57
+ subjectId: input.claim.subjectId,
58
+ surface: input.claim.surface,
59
+ claimType: input.claim.claimType,
60
+ fieldOrBehavior: input.claim.fieldOrBehavior,
61
+ value: input.claim.value,
62
+ status: input.claim.status,
63
+ impactLevel: input.claim.impactLevel,
64
+ createdAt: input.claim.createdAt,
65
+ updatedAt: input.claim.updatedAt,
66
+ evidenceType: input.claim.evidenceType,
67
+ evidenceMethod: input.claim.evidenceMethod,
68
+ confidenceBasis: input.claim.confidenceBasis,
69
+ derivedFrom: input.claim.derivedFrom ? [...input.claim.derivedFrom].sort() : undefined,
70
+ derivationEdges: input.claim.derivationEdges?.map((edge) => ({
71
+ inputClaimId: edge.inputClaimId,
72
+ method: edge.method,
73
+ role: edge.role,
74
+ supportStrength: edge.supportStrength,
75
+ rationale: edge.rationale,
76
+ })),
77
+ collectedBy: input.claim.collectedBy,
78
+ actor: input.claim.actor,
79
+ eventMethod: input.claim.eventMethod,
80
+ },
81
+ };
82
+ }
83
+ export function canonicalReviewProofJson(payload) {
84
+ return JSON.stringify(canonicalize(payload));
85
+ }
86
+ export function hashCanonicalReviewProofPayload(payload) {
87
+ return createHash("sha256").update(canonicalReviewProofJson(payload)).digest("hex");
88
+ }
89
+ export function buildReviewProofAnchor(input) {
90
+ const payload = buildCanonicalReviewProofPayload(input);
91
+ const hash = hashCanonicalReviewProofPayload(payload);
92
+ return {
93
+ id: `review-proof.${input.claim.id}.${hash.slice(0, 16)}`,
94
+ kind: "hash",
95
+ algorithm: "sha256",
96
+ value: hash,
97
+ sourceRef: input.sourceRef ?? input.rawSource.sourceRef,
98
+ observedAt: input.observedAt ?? input.reviewOutcome?.reviewedAt ?? input.rawSource.observedAt,
99
+ verificationStatus: "unverified",
100
+ metadata: input.metadata,
101
+ };
102
+ }
103
+ function canonicalize(value) {
104
+ if (Array.isArray(value))
105
+ return value.map((item) => canonicalize(item));
106
+ if (!isRecord(value))
107
+ return value;
108
+ const canonical = Object.create(null);
109
+ for (const key of Object.keys(value).sort()) {
110
+ const item = value[key];
111
+ if (item !== undefined) {
112
+ Object.defineProperty(canonical, key, {
113
+ value: canonicalize(item),
114
+ enumerable: true,
115
+ configurable: true,
116
+ writable: true,
117
+ });
118
+ }
119
+ }
120
+ return canonical;
121
+ }
122
+ function isRecord(value) {
123
+ return typeof value === "object" && value !== null && !Array.isArray(value);
124
+ }
@@ -0,0 +1,31 @@
1
+ import type { SurveyObservationInput } from "./builder.js";
2
+ export type SourceAuthorityClass = "official_publication" | "system_of_record" | "publisher_owned_page" | "contract_record" | "policy_document" | "other";
3
+ export interface SourceAuthorityMetadata {
4
+ authorityClass: SourceAuthorityClass;
5
+ scope: Record<string, unknown>;
6
+ effectiveFrom?: string;
7
+ effectiveUntil?: string;
8
+ sourceVersion?: string;
9
+ sourceOwner?: string;
10
+ declaredBy: string;
11
+ metadata?: Record<string, unknown>;
12
+ }
13
+ export interface SourceOfAuthorityObservationInput<TValue> {
14
+ id: string;
15
+ field: string;
16
+ value: TValue;
17
+ sourceAuthority: SourceAuthorityMetadata;
18
+ rawSource: SurveyObservationInput["rawSource"];
19
+ extraction: Omit<SurveyObservationInput["extraction"], "target" | "value" | "excerpt"> & {
20
+ target?: string;
21
+ excerpt?: string | null;
22
+ };
23
+ reviewOutcome?: SurveyObservationInput["reviewOutcome"];
24
+ claim: Omit<SurveyObservationInput["claim"], "fieldOrBehavior" | "value"> & {
25
+ fieldOrBehavior?: string;
26
+ };
27
+ candidate?: SurveyObservationInput["candidate"];
28
+ candidateSet?: SurveyObservationInput["candidateSet"];
29
+ metadata?: Record<string, unknown>;
30
+ }
31
+ export declare function sourceOfAuthorityObservation<TValue>(input: SourceOfAuthorityObservationInput<TValue>): SurveyObservationInput;
@@ -0,0 +1,70 @@
1
+ import { buildObservation } from "./observation-helper.js";
2
+ export function sourceOfAuthorityObservation(input) {
3
+ assertSourceAuthority(input.sourceAuthority, input.id);
4
+ assertVerifiedPosture(input);
5
+ const sourceAuthority = {
6
+ ...input.sourceAuthority,
7
+ scope: { ...input.sourceAuthority.scope },
8
+ metadata: input.sourceAuthority.metadata
9
+ ? { ...input.sourceAuthority.metadata }
10
+ : undefined,
11
+ };
12
+ return buildObservation({
13
+ ...input,
14
+ extraction: {
15
+ ...input.extraction,
16
+ metadata: {
17
+ ...input.extraction.metadata,
18
+ sourceAuthority,
19
+ },
20
+ },
21
+ surveyMetadata: {
22
+ sourceOfAuthority: {
23
+ authorityClass: sourceAuthority.authorityClass,
24
+ },
25
+ },
26
+ defaultExcerpt: `${input.field}: ${valueSummary(input.value)}`,
27
+ });
28
+ }
29
+ function assertSourceAuthority(sourceAuthority, observationId) {
30
+ if (!sourceAuthority.authorityClass) {
31
+ throw new Error(`Source-of-authority observation ${observationId} needs sourceAuthority.authorityClass`);
32
+ }
33
+ if (!sourceAuthority.declaredBy) {
34
+ throw new Error(`Source-of-authority observation ${observationId} needs sourceAuthority.declaredBy`);
35
+ }
36
+ if (!isRecord(sourceAuthority.scope) || Object.keys(sourceAuthority.scope).length === 0) {
37
+ throw new Error(`Source-of-authority observation ${observationId} needs sourceAuthority.scope`);
38
+ }
39
+ }
40
+ function assertVerifiedPosture(input) {
41
+ const status = claimStatus(input.claim.status, input.reviewOutcome?.status);
42
+ if (status !== "verified" && status !== "assumed")
43
+ return;
44
+ if (!input.rawSource.sourceRef) {
45
+ throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without a source reference`);
46
+ }
47
+ if (!input.extraction.locator) {
48
+ throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without a source locator`);
49
+ }
50
+ if (!input.reviewOutcome) {
51
+ throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without a review outcome`);
52
+ }
53
+ if (!input.reviewOutcome.actor) {
54
+ throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without review actor authority`);
55
+ }
56
+ if (!input.reviewOutcome.reviewedAt) {
57
+ throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without reviewedAt`);
58
+ }
59
+ }
60
+ function claimStatus(claimStatusValue, reviewStatus) {
61
+ return claimStatusValue ?? reviewStatus;
62
+ }
63
+ function valueSummary(value) {
64
+ if (value === null || value === undefined)
65
+ return "<empty>";
66
+ return String(value);
67
+ }
68
+ function isRecord(value) {
69
+ return typeof value === "object" && value !== null && !Array.isArray(value);
70
+ }
@@ -1,3 +1,6 @@
1
1
  import type { TrustInput } from "@kontourai/surface";
2
2
  import type { SurveyInput } from "./types.js";
3
- export declare function buildSurveyTrustInput(input: SurveyInput): TrustInput;
3
+ export interface BuildSurveyTrustInputOptions {
4
+ reviewProofs?: boolean;
5
+ }
6
+ export declare function buildSurveyTrustInput(input: SurveyInput, options?: BuildSurveyTrustInputOptions): TrustInput;
@@ -1,4 +1,5 @@
1
- export function buildSurveyTrustInput(input) {
1
+ import { buildReviewProofAnchor } from "./review-proof.js";
2
+ export function buildSurveyTrustInput(input, options = {}) {
2
3
  const rawSources = indexById(input.rawSources, "raw source");
3
4
  const extractions = indexById(input.extractions, "extraction");
4
5
  const candidateSets = indexById(input.candidateSets, "candidate set");
@@ -18,7 +19,7 @@ export function buildSurveyTrustInput(input) {
18
19
  const createdAt = projection.createdAt ?? extraction.extractedAt;
19
20
  const updatedAt = projection.updatedAt ?? review?.reviewedAt ?? input.generatedAt;
20
21
  const evidenceId = `${projection.id}.evidence.source`;
21
- claims.push({
22
+ const claim = {
22
23
  id: projection.id,
23
24
  subjectType: projection.subjectType,
24
25
  subjectId: projection.subjectId,
@@ -51,7 +52,22 @@ export function buildSurveyTrustInput(input) {
51
52
  reviewOutcomeId: review?.id,
52
53
  },
53
54
  },
54
- });
55
+ };
56
+ if (options.reviewProofs && review) {
57
+ claim.currentIntegrityAnchor = buildReviewProofAnchor({
58
+ rawSource,
59
+ extraction,
60
+ candidate,
61
+ candidateSet,
62
+ reviewOutcome: review,
63
+ claim: {
64
+ ...projection,
65
+ value: claimValue,
66
+ status,
67
+ },
68
+ });
69
+ }
70
+ claims.push(claim);
55
71
  evidence.push({
56
72
  id: evidenceId,
57
73
  claimId: projection.id,
@@ -72,6 +88,10 @@ export function buildSurveyTrustInput(input) {
72
88
  confidence: candidate.confidence ?? extraction.confidence,
73
89
  },
74
90
  });
91
+ const rationale = review?.rationale ?? candidateSet.rationale;
92
+ const comfortZoneNote = review?.withinComfortZone === false
93
+ ? `[outside comfort zone] ${review.comfortZoneNote ?? "reviewer flagged this as outside their comfort zone"}`
94
+ : undefined;
75
95
  events.push({
76
96
  id: `${projection.id}.event.${status}`,
77
97
  claimId: projection.id,
@@ -81,9 +101,31 @@ export function buildSurveyTrustInput(input) {
81
101
  evidenceIds: review?.evidenceIds?.length ? review.evidenceIds : [evidenceId],
82
102
  createdAt: review?.reviewedAt ?? input.generatedAt,
83
103
  verifiedAt: status === "verified" || status === "assumed" ? review?.reviewedAt ?? input.generatedAt : undefined,
84
- notes: review?.rationale ?? candidateSet.rationale,
104
+ notes: [rationale, comfortZoneNote].filter(Boolean).join(" | ") || undefined,
85
105
  });
86
106
  }
107
+ if (input.escalations) {
108
+ const claimIds = new Set(claims.map((c) => c.id));
109
+ for (const escalation of input.escalations) {
110
+ if (escalation.resolvedBy)
111
+ continue;
112
+ if (!escalation.attachToClaimId)
113
+ continue;
114
+ if (!claimIds.has(escalation.attachToClaimId)) {
115
+ throw new Error(`Escalation ${escalation.id} references unknown claim ${escalation.attachToClaimId}`);
116
+ }
117
+ events.push({
118
+ id: `${escalation.id}.event`,
119
+ claimId: escalation.attachToClaimId,
120
+ status: "disputed",
121
+ actor: escalation.raisedBy,
122
+ method: "candidate-escalation",
123
+ evidenceIds: [],
124
+ createdAt: escalation.raisedAt,
125
+ notes: `[${escalation.dimension}] ${escalation.reason}`,
126
+ });
127
+ }
128
+ }
87
129
  return {
88
130
  schemaVersion: 3,
89
131
  source: input.source,
@@ -101,6 +143,8 @@ function statusFor(input) {
101
143
  }
102
144
  if (input.candidateSet.status === "conflict")
103
145
  return "disputed";
146
+ if (input.candidateSet.status === "escalated")
147
+ return "disputed";
104
148
  return "proposed";
105
149
  }
106
150
  function assertProducerDiscipline(input) {
@@ -144,6 +188,8 @@ function eventMethodFor(status, candidateSet) {
144
188
  return "survey-rejection";
145
189
  if (candidateSet.status === "conflict")
146
190
  return "candidate-conflict";
191
+ if (candidateSet.status === "escalated")
192
+ return "candidate-escalation";
147
193
  return "candidate-proposal";
148
194
  }
149
195
  function indexById(items, label) {
@@ -23,7 +23,8 @@ export interface Extraction {
23
23
  extractedAt: string;
24
24
  metadata?: Record<string, unknown>;
25
25
  }
26
- export type CandidateSetStatus = "resolved" | "needs-review" | "conflict";
26
+ export type CandidateSetStatus = "resolved" | "needs-review" | "conflict" | "escalated";
27
+ export type EscalationDimension = "framing" | "completeness" | "conclusion" | "citation";
27
28
  export interface Candidate {
28
29
  id: string;
29
30
  extractionId: string;
@@ -51,6 +52,19 @@ export interface ReviewOutcome {
51
52
  reviewedAt?: string;
52
53
  rationale?: string;
53
54
  evidenceIds?: string[];
55
+ withinComfortZone?: boolean;
56
+ comfortZoneNote?: string;
57
+ metadata?: Record<string, unknown>;
58
+ }
59
+ export interface EscalationRecord {
60
+ id: string;
61
+ target: string;
62
+ dimension: EscalationDimension;
63
+ reason: string;
64
+ raisedBy: string;
65
+ raisedAt: string;
66
+ attachToClaimId?: string;
67
+ resolvedBy?: string;
54
68
  metadata?: Record<string, unknown>;
55
69
  }
56
70
  export interface ClaimTarget {
@@ -85,4 +99,5 @@ export interface SurveyInput {
85
99
  candidateSets: CandidateSet[];
86
100
  reviewOutcomes: ReviewOutcome[];
87
101
  claims: ClaimTarget[];
102
+ escalations?: EscalationRecord[];
88
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Producer-side source, extraction, candidate, and review contracts for projecting verified claims into Surface.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "check:content-boundary": "node scripts/check-content-boundary.cjs"
36
36
  },
37
37
  "dependencies": {
38
- "@kontourai/surface": "^0.5.0"
38
+ "@kontourai/surface": "git+https://github.com/kontourai/surface.git#9f2a18fb2c7c2430e81879d730b2a9b37c92f017"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^25.6.0",