@kontourai/survey 0.4.3 → 0.4.4

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 (32) hide show
  1. package/README.md +211 -9
  2. package/dist/examples/review-workbench/downstream-public-directory-adapter.d.ts +3 -0
  3. package/dist/examples/review-workbench/downstream-public-directory-adapter.js +262 -0
  4. package/dist/examples/review-workbench/review-queue-session.d.ts +54 -0
  5. package/dist/examples/review-workbench/review-queue-session.js +122 -0
  6. package/dist/examples/review-workbench/review-surface-preview.d.ts +55 -0
  7. package/dist/examples/review-workbench/review-surface-preview.js +104 -0
  8. package/dist/examples/review-workbench/review-workbench-data.d.ts +261 -0
  9. package/dist/examples/review-workbench/review-workbench-data.js +199 -0
  10. package/dist/examples/review-workbench/review-workbench.d.ts +7 -0
  11. package/dist/examples/review-workbench/review-workbench.js +488 -0
  12. package/dist/fixtures/downstream-public-directory-proposal.d.ts +100 -0
  13. package/dist/fixtures/downstream-public-directory-proposal.js +59 -0
  14. package/dist/fixtures/public-directory-review-resource.d.ts +156 -0
  15. package/dist/fixtures/public-directory-review-resource.js +157 -0
  16. package/dist/fixtures/regulated-document-review-resource.d.ts +121 -0
  17. package/dist/fixtures/regulated-document-review-resource.js +131 -0
  18. package/dist/src/builder.d.ts +4 -1
  19. package/dist/src/builder.js +7 -0
  20. package/dist/src/index.d.ts +8 -4
  21. package/dist/src/index.js +4 -2
  22. package/dist/src/learning-projections.d.ts +19 -0
  23. package/dist/src/learning-projections.js +77 -0
  24. package/dist/src/raw-source.d.ts +15 -0
  25. package/dist/src/raw-source.js +22 -0
  26. package/dist/src/review-proof.d.ts +28 -0
  27. package/dist/src/review-proof.js +58 -1
  28. package/dist/src/review-resource.d.ts +111 -0
  29. package/dist/src/review-resource.js +1 -0
  30. package/dist/src/to-surface.js +226 -13
  31. package/dist/src/types.d.ts +17 -1
  32. package/package.json +10 -4
@@ -0,0 +1,19 @@
1
+ import type { SurveyInput } from "./types.js";
2
+ export type LearningProjectionKind = "learning.comfort-zone" | "learning.escalation";
3
+ export type LearningProjectionSignal = "comfort-zone.outside" | "escalation.unresolved";
4
+ export type LearningProjectionSeverity = "info" | "attention";
5
+ export interface LearningProjection {
6
+ id: string;
7
+ kind: LearningProjectionKind;
8
+ source: string;
9
+ createdAt: string;
10
+ target?: string;
11
+ claimId?: string;
12
+ reviewOutcomeId?: string;
13
+ escalationId?: string;
14
+ signal: LearningProjectionSignal;
15
+ severity?: LearningProjectionSeverity;
16
+ summary: string;
17
+ metadata?: Record<string, unknown>;
18
+ }
19
+ export declare function buildSurveyLearningProjections(input: SurveyInput): LearningProjection[];
@@ -0,0 +1,77 @@
1
+ export function buildSurveyLearningProjections(input) {
2
+ const claimsByCandidateSet = groupBy(input.claims, (claim) => claim.candidateSetId);
3
+ const candidateSetTargets = new Map(input.candidateSets.map((candidateSet) => [candidateSet.id, candidateSet.target]));
4
+ const projections = [];
5
+ for (const reviewOutcome of input.reviewOutcomes) {
6
+ if (reviewOutcome.withinComfortZone !== false)
7
+ continue;
8
+ const target = candidateSetTargets.get(reviewOutcome.candidateSetId);
9
+ const claim = findClaimForReview(claimsByCandidateSet.get(reviewOutcome.candidateSetId) ?? [], reviewOutcome);
10
+ const note = reviewOutcome.comfortZoneNote;
11
+ projections.push({
12
+ id: `${reviewOutcome.id}.learning.comfort-zone`,
13
+ kind: "learning.comfort-zone",
14
+ source: input.source,
15
+ createdAt: reviewOutcome.reviewedAt ?? input.generatedAt,
16
+ target,
17
+ claimId: claim?.id,
18
+ reviewOutcomeId: reviewOutcome.id,
19
+ signal: "comfort-zone.outside",
20
+ severity: "attention",
21
+ summary: note ? `Review outcome is outside comfort zone: ${note}` : "Review outcome is outside comfort zone.",
22
+ metadata: {
23
+ comfortZone: {
24
+ withinComfortZone: false,
25
+ ...(note ? { note } : {}),
26
+ },
27
+ },
28
+ });
29
+ }
30
+ for (const escalation of input.escalations ?? []) {
31
+ if (escalation.resolvedBy)
32
+ continue;
33
+ projections.push({
34
+ id: `${escalation.id}.learning.escalation`,
35
+ kind: "learning.escalation",
36
+ source: input.source,
37
+ createdAt: escalation.raisedAt,
38
+ target: escalation.target,
39
+ claimId: escalation.attachToClaimId,
40
+ escalationId: escalation.id,
41
+ signal: "escalation.unresolved",
42
+ severity: "attention",
43
+ summary: `Unresolved ${escalation.dimension} escalation: ${escalation.reason}`,
44
+ metadata: {
45
+ escalation: unresolvedEscalationMetadata(escalation),
46
+ },
47
+ });
48
+ }
49
+ return projections;
50
+ }
51
+ function findClaimForReview(claims, reviewOutcome) {
52
+ if (reviewOutcome.candidateId) {
53
+ return claims.find((claim) => claim.candidateId === reviewOutcome.candidateId);
54
+ }
55
+ return claims[0];
56
+ }
57
+ function unresolvedEscalationMetadata(escalation) {
58
+ return {
59
+ id: escalation.id,
60
+ target: escalation.target,
61
+ dimension: escalation.dimension,
62
+ reason: escalation.reason,
63
+ raisedBy: escalation.raisedBy,
64
+ raisedAt: escalation.raisedAt,
65
+ ...(escalation.attachToClaimId ? { attachToClaimId: escalation.attachToClaimId } : {}),
66
+ resolved: false,
67
+ ...(escalation.metadata ? { metadata: escalation.metadata } : {}),
68
+ };
69
+ }
70
+ function groupBy(items, getKey) {
71
+ const map = new Map();
72
+ for (const item of items) {
73
+ const key = getKey(item);
74
+ map.set(key, [...(map.get(key) ?? []), item]);
75
+ }
76
+ return map;
77
+ }
@@ -25,7 +25,22 @@ export interface ManualEntrySourceInput extends Omit<RawSourceInput, "checksum"
25
25
  checksum?: ChecksumInput;
26
26
  locatorScheme?: LocatorScheme;
27
27
  }
28
+ export interface PolicyStandardMetadata {
29
+ inlineText: string;
30
+ standardVersion: string;
31
+ paragraphRef?: string;
32
+ reference?: string;
33
+ }
34
+ export interface PolicyStandardSourceInput extends Omit<RawSourceInput, "locatorScheme" | "metadata"> {
35
+ inlineText: string;
36
+ standardVersion: string;
37
+ paragraphRef?: string;
38
+ reference?: string;
39
+ locatorScheme?: LocatorScheme;
40
+ metadata?: Record<string, unknown>;
41
+ }
28
42
  export declare function uploadedDocumentSource(input: UploadedDocumentSourceInput): RawSource;
29
43
  export declare function apiRecordSource(input: ApiRecordSourceInput): RawSource;
30
44
  export declare function webPageSource(input: WebPageSourceInput): RawSource;
31
45
  export declare function manualEntrySource(input: ManualEntrySourceInput): RawSource;
46
+ export declare function policyStandardSource(input: PolicyStandardSourceInput): RawSource;
@@ -19,6 +19,28 @@ export function manualEntrySource(input) {
19
19
  ...input,
20
20
  });
21
21
  }
22
+ export function policyStandardSource(input) {
23
+ const policyStandard = {
24
+ inlineText: input.inlineText,
25
+ standardVersion: input.standardVersion,
26
+ paragraphRef: input.paragraphRef,
27
+ reference: input.reference,
28
+ };
29
+ const source = rawSource("policy-standard", {
30
+ locatorScheme: "text",
31
+ ...input,
32
+ metadata: {
33
+ ...input.metadata,
34
+ policyStandard,
35
+ },
36
+ });
37
+ return {
38
+ ...source,
39
+ inlineText: input.inlineText,
40
+ standardVersion: input.standardVersion,
41
+ paragraphRef: input.paragraphRef,
42
+ };
43
+ }
22
44
  function rawSource(kind, input) {
23
45
  return {
24
46
  id: input.id ?? sourceId(kind, input.sourceRef),
@@ -1,5 +1,9 @@
1
1
  import type { IntegrityAnchor } from "@kontourai/surface";
2
2
  import type { Candidate, CandidateSet, ClaimTarget, Extraction, RawSource, ReviewOutcome } from "./types.js";
3
+ export declare const REVIEW_PROOF_SCHEMA = "survey.review-proof";
4
+ export declare const REVIEW_PROOF_SCHEMA_VERSION = 1;
5
+ export declare const REVIEW_PROOF_PACKAGE_NAME = "@kontourai/survey";
6
+ export declare const REVIEW_PROOF_CONTRACT_VERSION = "1";
3
7
  export interface ReviewProofInput {
4
8
  rawSource: RawSource;
5
9
  extraction: Extraction;
@@ -13,6 +17,30 @@ export interface ReviewProofInput {
13
17
  }
14
18
  export interface CanonicalReviewProofPayload {
15
19
  schemaVersion: 1;
20
+ proof: {
21
+ schema: typeof REVIEW_PROOF_SCHEMA;
22
+ schemaVersion: typeof REVIEW_PROOF_SCHEMA_VERSION;
23
+ packageName: typeof REVIEW_PROOF_PACKAGE_NAME;
24
+ packageVersion: typeof REVIEW_PROOF_CONTRACT_VERSION;
25
+ issuer: string;
26
+ producer: string;
27
+ issuedAt: string;
28
+ subject: {
29
+ claimId: string;
30
+ candidateSetId: string;
31
+ candidateId?: string;
32
+ subjectType: string;
33
+ subjectId: string;
34
+ surface: string;
35
+ claimType: string;
36
+ fieldOrBehavior: string;
37
+ };
38
+ sourcePayload: {
39
+ id: string;
40
+ sourceRef: string;
41
+ checksum?: string;
42
+ };
43
+ };
16
44
  rawSource: {
17
45
  id: string;
18
46
  kind: RawSource["kind"];
@@ -1,7 +1,38 @@
1
1
  import { createHash } from "node:crypto";
2
+ export const REVIEW_PROOF_SCHEMA = "survey.review-proof";
3
+ export const REVIEW_PROOF_SCHEMA_VERSION = 1;
4
+ export const REVIEW_PROOF_PACKAGE_NAME = "@kontourai/survey";
5
+ // Version of the review proof contract emitted by this helper. This is intentionally
6
+ // independent from the npm package release version because it participates in hashes.
7
+ export const REVIEW_PROOF_CONTRACT_VERSION = "1";
2
8
  export function buildCanonicalReviewProofPayload(input) {
9
+ assertCandidateConsistency(input);
3
10
  return {
4
- schemaVersion: 1,
11
+ schemaVersion: REVIEW_PROOF_SCHEMA_VERSION,
12
+ proof: {
13
+ schema: REVIEW_PROOF_SCHEMA,
14
+ schemaVersion: REVIEW_PROOF_SCHEMA_VERSION,
15
+ packageName: REVIEW_PROOF_PACKAGE_NAME,
16
+ packageVersion: REVIEW_PROOF_CONTRACT_VERSION,
17
+ issuer: input.claim.collectedBy,
18
+ producer: input.extraction.extractor,
19
+ issuedAt: input.reviewOutcome?.reviewedAt ?? input.claim.updatedAt ?? input.extraction.extractedAt,
20
+ subject: {
21
+ claimId: input.claim.id,
22
+ candidateSetId: input.claim.candidateSetId,
23
+ candidateId: input.candidate.id,
24
+ subjectType: input.claim.subjectType,
25
+ subjectId: input.claim.subjectId,
26
+ surface: input.claim.surface,
27
+ claimType: input.claim.claimType,
28
+ fieldOrBehavior: input.claim.fieldOrBehavior,
29
+ },
30
+ sourcePayload: {
31
+ id: input.rawSource.id,
32
+ sourceRef: input.rawSource.sourceRef,
33
+ checksum: input.rawSource.checksum,
34
+ },
35
+ },
5
36
  rawSource: {
6
37
  id: input.rawSource.id,
7
38
  kind: input.rawSource.kind,
@@ -80,6 +111,32 @@ export function buildCanonicalReviewProofPayload(input) {
80
111
  },
81
112
  };
82
113
  }
114
+ function assertCandidateConsistency(input) {
115
+ if (input.extraction.sourceId !== input.rawSource.id) {
116
+ throw new Error(`Review proof extraction sourceId "${input.extraction.sourceId}" does not match raw source id "${input.rawSource.id}".`);
117
+ }
118
+ if (input.candidate.extractionId !== input.extraction.id) {
119
+ throw new Error(`Review proof candidate extractionId "${input.candidate.extractionId}" does not match extraction id "${input.extraction.id}".`);
120
+ }
121
+ if (!input.candidateSet.candidates.some((candidate) => candidate.id === input.candidate.id)) {
122
+ throw new Error(`Review proof candidate id "${input.candidate.id}" is not present in candidate set "${input.candidateSet.id}".`);
123
+ }
124
+ if (input.candidateSet.selectedCandidateId && input.candidateSet.selectedCandidateId !== input.candidate.id) {
125
+ throw new Error(`Review proof candidate set selectedCandidateId "${input.candidateSet.selectedCandidateId}" does not match reviewed candidate id "${input.candidate.id}".`);
126
+ }
127
+ if (input.claim.candidateSetId !== input.candidateSet.id) {
128
+ throw new Error(`Review proof claim candidateSetId "${input.claim.candidateSetId}" does not match candidate set id "${input.candidateSet.id}".`);
129
+ }
130
+ if (input.claim.candidateId && input.claim.candidateId !== input.candidate.id) {
131
+ throw new Error(`Review proof claim candidateId "${input.claim.candidateId}" does not match reviewed candidate id "${input.candidate.id}".`);
132
+ }
133
+ if (input.reviewOutcome && input.reviewOutcome.candidateSetId !== input.candidateSet.id) {
134
+ throw new Error(`Review proof review outcome candidateSetId "${input.reviewOutcome.candidateSetId}" does not match candidate set id "${input.candidateSet.id}".`);
135
+ }
136
+ if (input.reviewOutcome?.candidateId && input.reviewOutcome.candidateId !== input.candidate.id) {
137
+ throw new Error(`Review proof review outcome candidateId "${input.reviewOutcome.candidateId}" does not match reviewed candidate id "${input.candidate.id}".`);
138
+ }
139
+ }
83
140
  export function canonicalReviewProofJson(payload) {
84
141
  return JSON.stringify(canonicalize(payload));
85
142
  }
@@ -0,0 +1,111 @@
1
+ import type { CandidateSetStatus, ClaimTarget, Extraction, RawSource, ReviewOutcome } from "./types.js";
2
+ export declare const reviewResourceApiVersion = "survey.kontourai.io/v1alpha1";
3
+ export type ReviewResourceApiVersion = typeof reviewResourceApiVersion;
4
+ export type ReviewResourceKind = "ReviewItem" | "ReviewDecision";
5
+ export interface ResourceMetadata {
6
+ name: string;
7
+ uid?: string;
8
+ labels?: Record<string, string>;
9
+ annotations?: Record<string, string>;
10
+ producer?: Record<string, unknown>;
11
+ }
12
+ export interface ResourceEnvelope<K extends ReviewResourceKind, Spec, Status = Record<string, never>> {
13
+ apiVersion: ReviewResourceApiVersion;
14
+ kind: K;
15
+ metadata: ResourceMetadata;
16
+ spec: Spec;
17
+ status?: Status;
18
+ }
19
+ export type CandidateRole = "current" | "proposed" | "alternative" | "source-version" | "computed";
20
+ export interface SourceReference {
21
+ sourceRef: string;
22
+ sourceId?: string;
23
+ kind?: RawSource["kind"];
24
+ observedAt?: string;
25
+ fetchedAt?: string;
26
+ checksum?: string;
27
+ locatorScheme?: RawSource["locatorScheme"];
28
+ }
29
+ export interface ReviewLocator {
30
+ scheme: RawSource["locatorScheme"];
31
+ locator?: string;
32
+ excerpt?: string;
33
+ }
34
+ export interface ExtractionReference {
35
+ extractionId?: string;
36
+ target: string;
37
+ confidence?: number;
38
+ extractor?: string;
39
+ extractedAt?: string;
40
+ }
41
+ export interface ClaimTargetHint {
42
+ claimId?: string;
43
+ subjectType: string;
44
+ subjectId: string;
45
+ surface: string;
46
+ claimType: string;
47
+ fieldOrBehavior: string;
48
+ impactLevel: ClaimTarget["impactLevel"];
49
+ evidenceType?: ClaimTarget["evidenceType"];
50
+ evidenceMethod?: ClaimTarget["evidenceMethod"];
51
+ collectedBy?: string;
52
+ derivedFrom?: string[];
53
+ }
54
+ export interface SurveyRecordProjectionHint {
55
+ rawSourceId?: RawSource["id"];
56
+ extractionId?: Extraction["id"];
57
+ candidateSetId?: string;
58
+ candidateId?: string;
59
+ reviewOutcomeId?: ReviewOutcome["id"];
60
+ claimId?: ClaimTarget["id"];
61
+ }
62
+ export interface ReviewCandidate {
63
+ id: string;
64
+ role?: CandidateRole;
65
+ value: unknown;
66
+ confidence?: number;
67
+ sourceRank?: number;
68
+ rejectionReason?: string;
69
+ source: SourceReference;
70
+ locator?: ReviewLocator;
71
+ extraction: ExtractionReference;
72
+ claimTarget: ClaimTargetHint;
73
+ projection?: SurveyRecordProjectionHint;
74
+ producer?: Record<string, unknown>;
75
+ }
76
+ export interface ReviewItemSpec {
77
+ target: string;
78
+ candidates: ReviewCandidate[];
79
+ candidateSetStatus?: CandidateSetStatus;
80
+ selectedCandidateId?: string;
81
+ rationale?: string;
82
+ producerPolicy?: Record<string, unknown>;
83
+ projection?: SurveyRecordProjectionHint;
84
+ }
85
+ export interface ReviewItemStatus {
86
+ observedCandidateCount?: number;
87
+ selectedCandidateId?: string;
88
+ reviewDecisionName?: string;
89
+ }
90
+ export type ReviewItem = ResourceEnvelope<"ReviewItem", ReviewItemSpec, ReviewItemStatus>;
91
+ export interface ReviewActor {
92
+ id: string;
93
+ displayName?: string;
94
+ }
95
+ export interface ReviewDecisionSpec {
96
+ reviewItemName: string;
97
+ candidateId?: string;
98
+ status: ReviewOutcome["status"];
99
+ actor?: ReviewActor;
100
+ reviewedAt?: string;
101
+ rationale?: string;
102
+ evidenceIds?: string[];
103
+ withinComfortZone?: boolean;
104
+ comfortZoneNote?: string;
105
+ projection?: SurveyRecordProjectionHint;
106
+ }
107
+ export interface ReviewDecisionStatus {
108
+ appliedToClaimIds?: string[];
109
+ }
110
+ export type ReviewDecision = ResourceEnvelope<"ReviewDecision", ReviewDecisionSpec, ReviewDecisionStatus>;
111
+ export type ReviewResource = ReviewItem | ReviewDecision;
@@ -0,0 +1 @@
1
+ export const reviewResourceApiVersion = "survey.kontourai.io/v1alpha1";
@@ -43,14 +43,7 @@ export function buildSurveyTrustInput(input, options = {}) {
43
43
  },
44
44
  metadata: {
45
45
  ...projection.metadata,
46
- survey: {
47
- ...(isRecord(projection.metadata?.survey) ? projection.metadata.survey : {}),
48
- rawSourceId: rawSource.id,
49
- extractionId: extraction.id,
50
- candidateSetId: candidateSet.id,
51
- candidateId: candidate.id,
52
- reviewOutcomeId: review?.id,
53
- },
46
+ survey: buildSurveyMetadata({ projection, rawSource, extraction, candidateSet, candidate, review }),
54
47
  },
55
48
  };
56
49
  if (options.reviewProofs && review) {
@@ -68,6 +61,7 @@ export function buildSurveyTrustInput(input, options = {}) {
68
61
  });
69
62
  }
70
63
  claims.push(claim);
64
+ const policyStandard = policyStandardFields(rawSource);
71
65
  evidence.push({
72
66
  id: evidenceId,
73
67
  claimId: projection.id,
@@ -75,7 +69,7 @@ export function buildSurveyTrustInput(input, options = {}) {
75
69
  method: projection.evidenceMethod ?? "extraction",
76
70
  sourceRef: rawSource.sourceRef,
77
71
  sourceLocator: extraction.locator,
78
- excerptOrSummary: extraction.excerpt ?? `Extracted ${projection.fieldOrBehavior} from ${rawSource.kind}.`,
72
+ excerptOrSummary: evidenceExcerptOrSummary({ rawSource, extraction, projection, policyStandard }),
79
73
  observedAt: rawSource.observedAt,
80
74
  collectedBy: projection.collectedBy,
81
75
  integrityRef: rawSource.checksum,
@@ -83,15 +77,13 @@ export function buildSurveyTrustInput(input, options = {}) {
83
77
  ...rawSource.metadata,
84
78
  ...extraction.metadata,
85
79
  ...candidate.metadata,
80
+ ...(policyStandard ? { policyStandard } : {}),
86
81
  rawSourceKind: rawSource.kind,
87
82
  locatorScheme: rawSource.locatorScheme,
88
83
  confidence: candidate.confidence ?? extraction.confidence,
89
84
  },
90
85
  });
91
86
  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;
95
87
  events.push({
96
88
  id: `${projection.id}.event.${status}`,
97
89
  claimId: projection.id,
@@ -101,9 +93,10 @@ export function buildSurveyTrustInput(input, options = {}) {
101
93
  evidenceIds: review?.evidenceIds?.length ? review.evidenceIds : [evidenceId],
102
94
  createdAt: review?.reviewedAt ?? input.generatedAt,
103
95
  verifiedAt: status === "verified" || status === "assumed" ? review?.reviewedAt ?? input.generatedAt : undefined,
104
- notes: [rationale, comfortZoneNote].filter(Boolean).join(" | ") || undefined,
96
+ notes: rationale,
105
97
  });
106
98
  }
99
+ projectInterpretations({ input, rawSources, claims, evidence, events });
107
100
  if (input.escalations) {
108
101
  const claimIds = new Set(claims.map((c) => c.id));
109
102
  for (const escalation of input.escalations) {
@@ -135,6 +128,199 @@ export function buildSurveyTrustInput(input, options = {}) {
135
128
  events,
136
129
  };
137
130
  }
131
+ function projectInterpretations(input) {
132
+ const interpretations = input.input.interpretations
133
+ ? [...indexById(input.input.interpretations, "interpretation").values()]
134
+ : [];
135
+ if (interpretations.length === 0)
136
+ return;
137
+ const claimsById = new Map(input.claims.map((claim) => [claim.id, claim]));
138
+ for (const interpretation of interpretations) {
139
+ const projection = buildInterpretationProjection({
140
+ interpretation,
141
+ rawSources: input.rawSources,
142
+ claims: input.claims,
143
+ claimsById,
144
+ input: input.input,
145
+ });
146
+ input.evidence.push(projection.evidence);
147
+ input.events.push(projection.event);
148
+ attachInterpretationClaimMetadata({
149
+ claim: projection.claim,
150
+ interpretation,
151
+ anchorSource: projection.anchorSource,
152
+ evidenceId: projection.evidence.id,
153
+ });
154
+ }
155
+ }
156
+ function buildInterpretationProjection(input) {
157
+ const anchorSource = requirePolicyStandardAnchor(input.interpretation, input.rawSources);
158
+ const claim = resolveInterpretationClaim(input.interpretation, input.claims, input.input);
159
+ if (!input.claimsById.has(claim.id)) {
160
+ throw new Error(`Interpretation ${input.interpretation.id} resolved unknown claim ${claim.id}`);
161
+ }
162
+ const policyStandard = policyStandardFields(anchorSource);
163
+ const evidence = createInterpretationAnchorEvidence({
164
+ interpretation: input.interpretation,
165
+ claim,
166
+ anchorSource,
167
+ policyStandard,
168
+ });
169
+ return {
170
+ claim,
171
+ anchorSource,
172
+ evidence,
173
+ event: createInterpretationEvent({
174
+ interpretation: input.interpretation,
175
+ claim,
176
+ evidenceId: evidence.id,
177
+ }),
178
+ };
179
+ }
180
+ function requirePolicyStandardAnchor(interpretation, rawSources) {
181
+ const anchorSource = requireMapValue(rawSources, interpretation.anchorsToSourceId, "interpretation anchor raw source");
182
+ if (anchorSource.kind !== "policy-standard") {
183
+ throw new Error(`Interpretation ${interpretation.id} anchors to raw source ${anchorSource.id}, but expected policy-standard source`);
184
+ }
185
+ return anchorSource;
186
+ }
187
+ function createInterpretationAnchorEvidence(input) {
188
+ return {
189
+ id: `${input.interpretation.id}.evidence.anchor`,
190
+ claimId: input.claim.id,
191
+ evidenceType: "policy_rule",
192
+ method: "anchoring",
193
+ sourceRef: input.anchorSource.sourceRef,
194
+ sourceLocator: input.interpretation.ruleLocator,
195
+ excerptOrSummary: input.policyStandard?.inlineText ?? `Anchored policy-standard reading at ${input.interpretation.ruleLocator}.`,
196
+ observedAt: input.anchorSource.observedAt,
197
+ collectedBy: input.interpretation.actor,
198
+ integrityRef: input.anchorSource.checksum,
199
+ metadata: {
200
+ ...input.anchorSource.metadata,
201
+ ...(input.interpretation.metadata ? { interpretation: input.interpretation.metadata } : {}),
202
+ ...(input.policyStandard ? { policyStandard: input.policyStandard } : {}),
203
+ rawSourceKind: input.anchorSource.kind,
204
+ locatorScheme: input.anchorSource.locatorScheme,
205
+ anchorsToSourceId: input.anchorSource.id,
206
+ ruleLocator: input.interpretation.ruleLocator,
207
+ },
208
+ };
209
+ }
210
+ function createInterpretationEvent(input) {
211
+ return {
212
+ id: `${input.interpretation.id}.event`,
213
+ claimId: input.claim.id,
214
+ status: input.claim.status ?? "proposed",
215
+ actor: input.interpretation.actor,
216
+ method: "survey-interpretation",
217
+ evidenceIds: [input.evidenceId],
218
+ createdAt: input.interpretation.recordedAt,
219
+ verifiedAt: input.claim.status === "verified" || input.claim.status === "assumed" ? input.interpretation.recordedAt : undefined,
220
+ notes: input.interpretation.reading,
221
+ };
222
+ }
223
+ function attachInterpretationClaimMetadata(input) {
224
+ const claimMetadata = isRecord(input.claim.metadata) ? input.claim.metadata : {};
225
+ const surveyMetadata = isRecord(claimMetadata.survey) ? claimMetadata.survey : {};
226
+ const existingInterpretations = Array.isArray(surveyMetadata.interpretations) ? surveyMetadata.interpretations : [];
227
+ input.claim.metadata = {
228
+ ...claimMetadata,
229
+ survey: {
230
+ ...surveyMetadata,
231
+ interpretations: [
232
+ ...existingInterpretations,
233
+ {
234
+ interpretationId: input.interpretation.id,
235
+ ruleLocator: input.interpretation.ruleLocator,
236
+ reading: input.interpretation.reading,
237
+ actor: input.interpretation.actor,
238
+ recordedAt: input.interpretation.recordedAt,
239
+ ...(input.interpretation.metadata ? { metadata: input.interpretation.metadata } : {}),
240
+ edges: [
241
+ {
242
+ type: "appliesTo",
243
+ targetKind: "claim",
244
+ targetId: input.claim.id,
245
+ },
246
+ {
247
+ type: "anchorsTo",
248
+ targetKind: "rawSource",
249
+ targetId: input.anchorSource.id,
250
+ evidenceId: input.evidenceId,
251
+ ruleLocator: input.interpretation.ruleLocator,
252
+ },
253
+ ],
254
+ },
255
+ ],
256
+ },
257
+ };
258
+ }
259
+ function resolveInterpretationClaim(interpretation, claims, input) {
260
+ if (interpretation.appliesToClaimId) {
261
+ const claim = claims.find((item) => item.id === interpretation.appliesToClaimId);
262
+ if (!claim) {
263
+ throw new Error(`Interpretation ${interpretation.id} references unknown claim ${interpretation.appliesToClaimId}`);
264
+ }
265
+ if (interpretation.appliesToTarget) {
266
+ const targetClaim = resolveInterpretationTargetClaim(interpretation, claims, input);
267
+ if (targetClaim.id !== claim.id) {
268
+ throw new Error(`Interpretation ${interpretation.id} has conflicting appliesToClaimId ${claim.id} and appliesToTarget ${interpretation.appliesToTarget} resolved to ${targetClaim.id}`);
269
+ }
270
+ }
271
+ return claim;
272
+ }
273
+ if (!interpretation.appliesToTarget) {
274
+ throw new Error(`Interpretation ${interpretation.id} needs appliesToClaimId or appliesToTarget`);
275
+ }
276
+ return resolveInterpretationTargetClaim(interpretation, claims, input);
277
+ }
278
+ function resolveInterpretationTargetClaim(interpretation, claims, input) {
279
+ const candidateSetIdsByTarget = new Set(input.candidateSets
280
+ .filter((candidateSet) => candidateSet.target === interpretation.appliesToTarget)
281
+ .map((candidateSet) => candidateSet.id));
282
+ const matches = input.claims.filter((claim) => claim.fieldOrBehavior === interpretation.appliesToTarget ||
283
+ candidateSetIdsByTarget.has(claim.candidateSetId));
284
+ const uniqueClaimIds = [...new Set(matches.map((claim) => claim.id))];
285
+ if (uniqueClaimIds.length === 0) {
286
+ throw new Error(`Interpretation ${interpretation.id} target ${interpretation.appliesToTarget} did not match any claim`);
287
+ }
288
+ if (uniqueClaimIds.length > 1) {
289
+ throw new Error(`Interpretation ${interpretation.id} target ${interpretation.appliesToTarget} is ambiguous across claims: ${uniqueClaimIds.join(", ")}`);
290
+ }
291
+ const claim = claims.find((item) => item.id === uniqueClaimIds[0]);
292
+ if (!claim)
293
+ throw new Error(`Interpretation ${interpretation.id} resolved unknown claim ${uniqueClaimIds[0]}`);
294
+ return claim;
295
+ }
296
+ function buildSurveyMetadata(input) {
297
+ const producerSurveyMetadata = isRecord(input.projection.metadata?.survey) ? input.projection.metadata.survey : {};
298
+ const producerCandidateMetadata = isRecord(producerSurveyMetadata.candidate) ? producerSurveyMetadata.candidate : {};
299
+ return {
300
+ ...producerSurveyMetadata,
301
+ rawSourceId: input.rawSource.id,
302
+ extractionId: input.extraction.id,
303
+ candidateSetId: input.candidateSet.id,
304
+ candidateId: input.candidate.id,
305
+ reviewOutcomeId: input.review?.id,
306
+ ...(input.candidate.rejectionReason !== undefined
307
+ ? {
308
+ candidate: {
309
+ ...producerCandidateMetadata,
310
+ rejectionReason: input.candidate.rejectionReason,
311
+ },
312
+ }
313
+ : {}),
314
+ ...(input.review?.withinComfortZone === false
315
+ ? {
316
+ comfortZone: {
317
+ withinComfortZone: false,
318
+ ...(input.review.comfortZoneNote ? { note: input.review.comfortZoneNote } : {}),
319
+ },
320
+ }
321
+ : {}),
322
+ };
323
+ }
138
324
  function statusFor(input) {
139
325
  if (input.review)
140
326
  return input.review.status;
@@ -173,12 +359,39 @@ function selectReview(reviews, candidateId) {
173
359
  return reviews.find((review) => review.candidateId === candidateId) ?? reviews.find((review) => !review.candidateId);
174
360
  }
175
361
  function evidenceTypeFor(rawSource) {
362
+ if (rawSource.kind === "policy-standard")
363
+ return "policy_rule";
176
364
  if (rawSource.kind === "uploaded-document")
177
365
  return "document_citation";
178
366
  if (rawSource.kind === "web-page")
179
367
  return "crawl_observation";
180
368
  return "attestation";
181
369
  }
370
+ function policyStandardFields(rawSource) {
371
+ const metadata = rawSource.metadata?.policyStandard;
372
+ const metadataRecord = isRecord(metadata) ? metadata : {};
373
+ const inlineText = rawSource.inlineText ?? stringValue(metadataRecord.inlineText) ?? stringValue(metadataRecord.text);
374
+ const standardVersion = rawSource.standardVersion ?? stringValue(metadataRecord.standardVersion) ?? stringValue(metadataRecord.version);
375
+ const paragraphRef = rawSource.paragraphRef ?? stringValue(metadataRecord.paragraphRef);
376
+ const reference = stringValue(metadataRecord.reference);
377
+ if (!inlineText && !standardVersion && !paragraphRef && !reference)
378
+ return undefined;
379
+ return {
380
+ inlineText,
381
+ standardVersion,
382
+ paragraphRef,
383
+ reference,
384
+ };
385
+ }
386
+ function evidenceExcerptOrSummary(input) {
387
+ if (input.rawSource.kind === "policy-standard" && input.policyStandard?.inlineText) {
388
+ return input.policyStandard.inlineText;
389
+ }
390
+ return input.extraction.excerpt ?? `Extracted ${input.projection.fieldOrBehavior} from ${input.rawSource.kind}.`;
391
+ }
392
+ function stringValue(value) {
393
+ return typeof value === "string" ? value : undefined;
394
+ }
182
395
  function eventMethodFor(status, candidateSet) {
183
396
  if (status === "verified")
184
397
  return "survey-review";