@kontourai/survey 0.4.2 → 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 (34) hide show
  1. package/README.md +247 -35
  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 +10 -6
  21. package/dist/src/index.js +5 -3
  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/source-of-authority-observation.d.ts +20 -0
  31. package/dist/src/source-of-authority-observation.js +68 -0
  32. package/dist/src/to-surface.js +226 -13
  33. package/dist/src/types.d.ts +17 -1
  34. package/package.json +10 -4
@@ -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";
@@ -1,5 +1,5 @@
1
1
  import type { ConfidenceBasis, DerivationEdge, EvidenceMethod, EvidenceType, ImpactLevel, TrustStatus } from "@kontourai/surface";
2
- export type RawSourceKind = "uploaded-document" | "web-page" | "api-record" | "manual-entry";
2
+ export type RawSourceKind = "uploaded-document" | "web-page" | "api-record" | "manual-entry" | "policy-standard";
3
3
  export type LocatorScheme = "pdf" | "text" | "html" | "structured-field";
4
4
  export interface RawSource {
5
5
  id: string;
@@ -9,6 +9,9 @@ export interface RawSource {
9
9
  fetchedAt?: string;
10
10
  checksum?: string;
11
11
  locatorScheme: LocatorScheme;
12
+ inlineText?: string;
13
+ standardVersion?: string;
14
+ paragraphRef?: string;
12
15
  metadata?: Record<string, unknown>;
13
16
  }
14
17
  export interface Extraction {
@@ -31,6 +34,7 @@ export interface Candidate {
31
34
  value: unknown;
32
35
  confidence?: number;
33
36
  sourceRank?: number;
37
+ rejectionReason?: string;
34
38
  metadata?: Record<string, unknown>;
35
39
  }
36
40
  export interface CandidateSet {
@@ -67,6 +71,17 @@ export interface EscalationRecord {
67
71
  resolvedBy?: string;
68
72
  metadata?: Record<string, unknown>;
69
73
  }
74
+ export interface Interpretation {
75
+ id: string;
76
+ appliesToTarget?: string;
77
+ appliesToClaimId?: string;
78
+ anchorsToSourceId: string;
79
+ ruleLocator: string;
80
+ reading: string;
81
+ actor: string;
82
+ recordedAt: string;
83
+ metadata?: Record<string, unknown>;
84
+ }
70
85
  export interface ClaimTarget {
71
86
  id: string;
72
87
  candidateSetId: string;
@@ -100,4 +115,5 @@ export interface SurveyInput {
100
115
  reviewOutcomes: ReviewOutcome[];
101
116
  claims: ClaimTarget[];
102
117
  escalations?: EscalationRecord[];
118
+ interpretations?: Interpretation[];
103
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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",
@@ -19,7 +19,9 @@
19
19
  "exports": {
20
20
  ".": "./dist/src/index.js",
21
21
  "./fixtures/public-field-review": "./dist/fixtures/public-field-review.js",
22
- "./fixtures/corrected-document-candidates": "./dist/fixtures/corrected-document-candidates.js"
22
+ "./fixtures/corrected-document-candidates": "./dist/fixtures/corrected-document-candidates.js",
23
+ "./fixtures/public-directory-review-resource": "./dist/fixtures/public-directory-review-resource.js",
24
+ "./fixtures/regulated-document-review-resource": "./dist/fixtures/regulated-document-review-resource.js"
23
25
  },
24
26
  "files": [
25
27
  "dist/examples/",
@@ -31,8 +33,12 @@
31
33
  "build": "rm -rf dist && tsc",
32
34
  "typecheck": "tsc --noEmit",
33
35
  "test": "npm run build && node --test dist/tests/*.test.js",
34
- "verify": "node scripts/check-content-boundary.cjs && npm run typecheck && npm test",
35
- "check:content-boundary": "node scripts/check-content-boundary.cjs"
36
+ "verify": "node scripts/check-content-boundary.cjs && npm run typecheck && npm test && npm run check:review-workbench",
37
+ "check:content-boundary": "node scripts/check-content-boundary.cjs",
38
+ "check:review-workbench": "npm run check:review-workbench:static",
39
+ "check:review-workbench:static": "npm run build && node scripts/check-review-workbench.cjs",
40
+ "setup:repo-hooks": "node scripts/setup-repo-hooks.cjs",
41
+ "validate:repo-hooks": "node scripts/validate-repo-hooks.cjs"
36
42
  },
37
43
  "dependencies": {
38
44
  "@kontourai/surface": "^0.5.1"