@kontourai/survey 0.4.4 → 0.4.6

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
@@ -871,11 +871,18 @@ const learning = buildSurveyLearningProjections(surveyInput);
871
871
  const trustInput = buildSurveyTrustInput(surveyInput);
872
872
  ```
873
873
 
874
- Learning projections are product-neutral `learning.*` records. Survey currently
875
- emits `learning.comfort-zone` from structured `ReviewOutcome.withinComfortZone
876
- === false` data and `learning.escalation` from unresolved `EscalationRecord`s,
877
- including unattached records that producer tooling can route but Surface cannot
878
- attach to a claim event.
874
+ Learning projections are product-neutral `learning.*` records. Survey emits
875
+ `learning.rejected-candidate` from structured candidate rejection data such as
876
+ non-empty `Candidate.rejectionReason` values or a candidate-specific
877
+ `ReviewOutcome.status === "rejected"` outcome with rationale. When both exist,
878
+ Survey emits one rejected-candidate projection enriched with candidate and
879
+ review outcome context.
880
+ Ordinary rejected candidates do not emit `learning.comfort-zone`.
881
+
882
+ Survey also emits `learning.comfort-zone` from structured
883
+ `ReviewOutcome.withinComfortZone === false` data and `learning.escalation` from
884
+ unresolved `EscalationRecord`s, including unattached records that producer
885
+ tooling can route but Surface cannot attach to a claim event.
879
886
 
880
887
  These projections are producer/review workflow and evaluation signals. They are
881
888
  not claims about truth or veracity, not Surface claim status, not evidence, and
@@ -129,6 +129,144 @@ export declare const publicDirectoryReviewItemFixture: {
129
129
  reviewDecisionName: string;
130
130
  };
131
131
  };
132
+ export declare const regulatedRuleConflictReviewItemFixture: {
133
+ apiVersion: "survey.kontourai.io/v1alpha1";
134
+ kind: "ReviewItem";
135
+ metadata: {
136
+ name: string;
137
+ labels: {
138
+ domain: string;
139
+ };
140
+ producer: {
141
+ displayName: string;
142
+ jurisdiction: string;
143
+ reportingPeriod: string;
144
+ };
145
+ };
146
+ spec: {
147
+ target: string;
148
+ selectedCandidateId: string;
149
+ candidateSetStatus: "conflict";
150
+ rationale: string;
151
+ projection: {
152
+ candidateSetId: string;
153
+ };
154
+ producerPolicy: {
155
+ decisionMode: string;
156
+ sourceAuthorityProjection: string;
157
+ feedbackTags: string[];
158
+ };
159
+ candidates: ({
160
+ id: string;
161
+ role: "current";
162
+ value: number;
163
+ confidence: number;
164
+ source: {
165
+ sourceId: string;
166
+ sourceRef: string;
167
+ kind: "manual-entry";
168
+ observedAt: string;
169
+ locatorScheme: "structured-field";
170
+ };
171
+ locator: {
172
+ scheme: "structured-field";
173
+ locator: string;
174
+ excerpt: string;
175
+ };
176
+ extraction: {
177
+ extractionId: string;
178
+ target: string;
179
+ confidence: number;
180
+ extractor: string;
181
+ extractedAt: string;
182
+ };
183
+ claimTarget: {
184
+ claimId: string;
185
+ subjectType: string;
186
+ subjectId: string;
187
+ surface: string;
188
+ claimType: string;
189
+ fieldOrBehavior: string;
190
+ impactLevel: "high";
191
+ evidenceType: "human_attestation";
192
+ evidenceMethod: "attestation";
193
+ collectedBy: string;
194
+ };
195
+ projection: {
196
+ rawSourceId: string;
197
+ extractionId: string;
198
+ candidateSetId: string;
199
+ candidateId: string;
200
+ claimId: string;
201
+ };
202
+ producer: {
203
+ status: string;
204
+ sourceSection?: undefined;
205
+ sourcePage?: undefined;
206
+ sourceAuthority?: undefined;
207
+ };
208
+ sourceRank?: undefined;
209
+ } | {
210
+ id: string;
211
+ role: "proposed";
212
+ value: number;
213
+ confidence: number;
214
+ sourceRank: number;
215
+ source: {
216
+ sourceId: string;
217
+ sourceRef: string;
218
+ kind: "uploaded-document";
219
+ observedAt: string;
220
+ locatorScheme: "pdf";
221
+ };
222
+ locator: {
223
+ scheme: "pdf";
224
+ locator: string;
225
+ excerpt: string;
226
+ };
227
+ extraction: {
228
+ extractionId: string;
229
+ target: string;
230
+ confidence: number;
231
+ extractor: string;
232
+ extractedAt: string;
233
+ };
234
+ claimTarget: {
235
+ claimId: string;
236
+ subjectType: string;
237
+ subjectId: string;
238
+ surface: string;
239
+ claimType: string;
240
+ fieldOrBehavior: string;
241
+ impactLevel: "high";
242
+ evidenceType: "policy_rule";
243
+ evidenceMethod: "extraction";
244
+ collectedBy: string;
245
+ };
246
+ projection: {
247
+ rawSourceId: string;
248
+ extractionId: string;
249
+ candidateSetId: string;
250
+ candidateId: string;
251
+ claimId: string;
252
+ };
253
+ producer: {
254
+ sourceSection: string;
255
+ sourcePage: number;
256
+ sourceAuthority: {
257
+ authorityClass: string;
258
+ declaredBy: string;
259
+ scope: string;
260
+ };
261
+ status?: undefined;
262
+ };
263
+ })[];
264
+ };
265
+ status: {
266
+ observedCandidateCount: number;
267
+ selectedCandidateId: string;
268
+ };
269
+ };
132
270
  export declare const reviewWorkbenchQueueFixtures: (ReviewItem | {
133
271
  apiVersion: "survey.kontourai.io/v1alpha1";
134
272
  kind: "ReviewItem";
@@ -258,4 +396,141 @@ export declare const reviewWorkbenchQueueFixtures: (ReviewItem | {
258
396
  selectedCandidateId: string;
259
397
  reviewDecisionName: string;
260
398
  };
399
+ } | {
400
+ apiVersion: "survey.kontourai.io/v1alpha1";
401
+ kind: "ReviewItem";
402
+ metadata: {
403
+ name: string;
404
+ labels: {
405
+ domain: string;
406
+ };
407
+ producer: {
408
+ displayName: string;
409
+ jurisdiction: string;
410
+ reportingPeriod: string;
411
+ };
412
+ };
413
+ spec: {
414
+ target: string;
415
+ selectedCandidateId: string;
416
+ candidateSetStatus: "conflict";
417
+ rationale: string;
418
+ projection: {
419
+ candidateSetId: string;
420
+ };
421
+ producerPolicy: {
422
+ decisionMode: string;
423
+ sourceAuthorityProjection: string;
424
+ feedbackTags: string[];
425
+ };
426
+ candidates: ({
427
+ id: string;
428
+ role: "current";
429
+ value: number;
430
+ confidence: number;
431
+ source: {
432
+ sourceId: string;
433
+ sourceRef: string;
434
+ kind: "manual-entry";
435
+ observedAt: string;
436
+ locatorScheme: "structured-field";
437
+ };
438
+ locator: {
439
+ scheme: "structured-field";
440
+ locator: string;
441
+ excerpt: string;
442
+ };
443
+ extraction: {
444
+ extractionId: string;
445
+ target: string;
446
+ confidence: number;
447
+ extractor: string;
448
+ extractedAt: string;
449
+ };
450
+ claimTarget: {
451
+ claimId: string;
452
+ subjectType: string;
453
+ subjectId: string;
454
+ surface: string;
455
+ claimType: string;
456
+ fieldOrBehavior: string;
457
+ impactLevel: "high";
458
+ evidenceType: "human_attestation";
459
+ evidenceMethod: "attestation";
460
+ collectedBy: string;
461
+ };
462
+ projection: {
463
+ rawSourceId: string;
464
+ extractionId: string;
465
+ candidateSetId: string;
466
+ candidateId: string;
467
+ claimId: string;
468
+ };
469
+ producer: {
470
+ status: string;
471
+ sourceSection?: undefined;
472
+ sourcePage?: undefined;
473
+ sourceAuthority?: undefined;
474
+ };
475
+ sourceRank?: undefined;
476
+ } | {
477
+ id: string;
478
+ role: "proposed";
479
+ value: number;
480
+ confidence: number;
481
+ sourceRank: number;
482
+ source: {
483
+ sourceId: string;
484
+ sourceRef: string;
485
+ kind: "uploaded-document";
486
+ observedAt: string;
487
+ locatorScheme: "pdf";
488
+ };
489
+ locator: {
490
+ scheme: "pdf";
491
+ locator: string;
492
+ excerpt: string;
493
+ };
494
+ extraction: {
495
+ extractionId: string;
496
+ target: string;
497
+ confidence: number;
498
+ extractor: string;
499
+ extractedAt: string;
500
+ };
501
+ claimTarget: {
502
+ claimId: string;
503
+ subjectType: string;
504
+ subjectId: string;
505
+ surface: string;
506
+ claimType: string;
507
+ fieldOrBehavior: string;
508
+ impactLevel: "high";
509
+ evidenceType: "policy_rule";
510
+ evidenceMethod: "extraction";
511
+ collectedBy: string;
512
+ };
513
+ projection: {
514
+ rawSourceId: string;
515
+ extractionId: string;
516
+ candidateSetId: string;
517
+ candidateId: string;
518
+ claimId: string;
519
+ };
520
+ producer: {
521
+ sourceSection: string;
522
+ sourcePage: number;
523
+ sourceAuthority: {
524
+ authorityClass: string;
525
+ declaredBy: string;
526
+ scope: string;
527
+ };
528
+ status?: undefined;
529
+ };
530
+ })[];
531
+ };
532
+ status: {
533
+ observedCandidateCount: number;
534
+ selectedCandidateId: string;
535
+ };
261
536
  })[];
@@ -131,6 +131,142 @@ export const publicDirectoryReviewItemFixture = {
131
131
  reviewDecisionName: "public-directory-availability-operator-decision",
132
132
  },
133
133
  };
134
+ export const regulatedRuleConflictReviewItemFixture = {
135
+ apiVersion: reviewResourceApiVersion,
136
+ kind: "ReviewItem",
137
+ metadata: {
138
+ name: "regulated-rule-conflict-standard-threshold",
139
+ labels: {
140
+ domain: "regulated-rule-source",
141
+ },
142
+ producer: {
143
+ displayName: "Regulated Rule Review",
144
+ jurisdiction: "example-jurisdiction",
145
+ reportingPeriod: "2026",
146
+ },
147
+ },
148
+ spec: {
149
+ target: "standardThreshold",
150
+ selectedCandidateId: "regulated-rule-conflict-standard-threshold:candidate:current",
151
+ candidateSetStatus: "conflict",
152
+ rationale: "A newly extracted source candidate conflicts with the currently managed rule value.",
153
+ projection: {
154
+ candidateSetId: "regulated-rule-conflict-standard-threshold:candidate-set",
155
+ },
156
+ producerPolicy: {
157
+ decisionMode: "keep-current",
158
+ sourceAuthorityProjection: "only-for-selected-source-backed-value",
159
+ feedbackTags: ["rule-conflict", "source-extraction-review"],
160
+ },
161
+ candidates: [
162
+ {
163
+ id: "regulated-rule-conflict-standard-threshold:candidate:current",
164
+ role: "current",
165
+ value: 15000,
166
+ confidence: 1,
167
+ source: {
168
+ sourceId: "regulated-rule-conflict-standard-threshold:source:current",
169
+ sourceRef: "survey-example://rules/example-jurisdiction/2026/standardThreshold",
170
+ kind: "manual-entry",
171
+ observedAt: "2026-06-03T00:00:00.000Z",
172
+ locatorScheme: "structured-field",
173
+ },
174
+ locator: {
175
+ scheme: "structured-field",
176
+ locator: "managed-rules:path=standardThreshold",
177
+ excerpt: "Current managed rule value.",
178
+ },
179
+ extraction: {
180
+ extractionId: "regulated-rule-conflict-standard-threshold:extraction:current",
181
+ target: "standardThreshold",
182
+ confidence: 1,
183
+ extractor: "example-rule-manager",
184
+ extractedAt: "2026-06-03T00:00:00.000Z",
185
+ },
186
+ claimTarget: {
187
+ claimId: "regulated-rule.example-jurisdiction.2026.standard-threshold.current",
188
+ subjectType: "regulated-rule-source",
189
+ subjectId: "example-jurisdiction:2026:standardThreshold",
190
+ surface: "regulated.rules",
191
+ claimType: "regulated.rule-source-value",
192
+ fieldOrBehavior: "standardThreshold",
193
+ impactLevel: "high",
194
+ evidenceType: "human_attestation",
195
+ evidenceMethod: "attestation",
196
+ collectedBy: "example-rule-manager",
197
+ },
198
+ projection: {
199
+ rawSourceId: "regulated-rule-conflict-standard-threshold:source:current",
200
+ extractionId: "regulated-rule-conflict-standard-threshold:extraction:current",
201
+ candidateSetId: "regulated-rule-conflict-standard-threshold:candidate-set",
202
+ candidateId: "regulated-rule-conflict-standard-threshold:projection:current",
203
+ claimId: "regulated-rule.example-jurisdiction.2026.standard-threshold.current",
204
+ },
205
+ producer: {
206
+ status: "current-managed-value",
207
+ },
208
+ },
209
+ {
210
+ id: "regulated-rule-conflict-standard-threshold:candidate:proposed",
211
+ role: "proposed",
212
+ value: 16000,
213
+ confidence: 0.95,
214
+ sourceRank: 1,
215
+ source: {
216
+ sourceId: "regulated-rule-conflict-standard-threshold:source:proposed",
217
+ sourceRef: "https://example.test/regulatory-bulletins/2026-thresholds.pdf",
218
+ kind: "uploaded-document",
219
+ observedAt: "2026-06-03T00:30:00.000Z",
220
+ locatorScheme: "pdf",
221
+ },
222
+ locator: {
223
+ scheme: "pdf",
224
+ locator: "pdf:page=12;section=Standard%20Threshold;path=standardThreshold",
225
+ excerpt: "Example Individual Standard Threshold $16,000",
226
+ },
227
+ extraction: {
228
+ extractionId: "regulated-rule-conflict-standard-threshold:extraction:proposed",
229
+ target: "standardThreshold",
230
+ confidence: 0.95,
231
+ extractor: "example-rule-source-parser",
232
+ extractedAt: "2026-06-03T00:30:00.000Z",
233
+ },
234
+ claimTarget: {
235
+ claimId: "regulated-rule.example-jurisdiction.2026.standard-threshold.proposed",
236
+ subjectType: "regulated-rule-source",
237
+ subjectId: "example-jurisdiction:2026:standardThreshold",
238
+ surface: "regulated.rules",
239
+ claimType: "regulated.rule-source-value",
240
+ fieldOrBehavior: "standardThreshold",
241
+ impactLevel: "high",
242
+ evidenceType: "policy_rule",
243
+ evidenceMethod: "extraction",
244
+ collectedBy: "example-rule-source-parser",
245
+ },
246
+ projection: {
247
+ rawSourceId: "regulated-rule-conflict-standard-threshold:source:proposed",
248
+ extractionId: "regulated-rule-conflict-standard-threshold:extraction:proposed",
249
+ candidateSetId: "regulated-rule-conflict-standard-threshold:candidate-set",
250
+ candidateId: "regulated-rule-conflict-standard-threshold:projection:proposed",
251
+ claimId: "regulated-rule.example-jurisdiction.2026.standard-threshold.proposed",
252
+ },
253
+ producer: {
254
+ sourceSection: "Standard Threshold",
255
+ sourcePage: 12,
256
+ sourceAuthority: {
257
+ authorityClass: "official_publication",
258
+ declaredBy: "Example regulatory source registry",
259
+ scope: "standardThreshold rule value for example-jurisdiction 2026",
260
+ },
261
+ },
262
+ },
263
+ ],
264
+ },
265
+ status: {
266
+ observedCandidateCount: 2,
267
+ selectedCandidateId: "regulated-rule-conflict-standard-threshold:candidate:current",
268
+ },
269
+ };
134
270
  function queueFixture(name, target, currentValue, proposedValue, candidateSetStatus, feedbackTags) {
135
271
  return {
136
272
  ...publicDirectoryReviewItemFixture,
@@ -196,4 +332,5 @@ export const reviewWorkbenchQueueFixtures = [
196
332
  publicDirectoryReviewItemFixture,
197
333
  queueFixture("public-directory-address", "streetAddress", "100 Main Street", "102 Main Street", "needs-review", ["address-change", "producer-escalation-candidate"]),
198
334
  queueFixture("public-directory-license", "licenseStatus", "ACTIVE", "EXPIRED", "escalated", ["licensing", "manual-review-required"]),
335
+ regulatedRuleConflictReviewItemFixture,
199
336
  ];
@@ -1,6 +1,6 @@
1
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";
2
+ export type LearningProjectionKind = "learning.comfort-zone" | "learning.escalation" | "learning.rejected-candidate";
3
+ export type LearningProjectionSignal = "comfort-zone.outside" | "escalation.unresolved" | "rejected-candidate.reason";
4
4
  export type LearningProjectionSeverity = "info" | "attention";
5
5
  export interface LearningProjection {
6
6
  id: string;
@@ -1,7 +1,44 @@
1
1
  export function buildSurveyLearningProjections(input) {
2
2
  const claimsByCandidateSet = groupBy(input.claims, (claim) => claim.candidateSetId);
3
3
  const candidateSetTargets = new Map(input.candidateSets.map((candidateSet) => [candidateSet.id, candidateSet.target]));
4
+ const extractionsById = new Map(input.extractions.map((extraction) => [extraction.id, extraction]));
5
+ const rejectedReviewsByCandidate = groupBy(input.reviewOutcomes.filter((reviewOutcome) => reviewOutcome.status === "rejected" && reviewOutcome.candidateId), (reviewOutcome) => `${reviewOutcome.candidateSetId}:${reviewOutcome.candidateId}`);
4
6
  const projections = [];
7
+ for (const candidateSet of input.candidateSets) {
8
+ for (const candidate of candidateSet.candidates) {
9
+ const rejectedReviews = rejectedReviewsByCandidate.get(`${candidateSet.id}:${candidate.id}`) ?? [];
10
+ const candidateReason = normalizeText(candidate.rejectionReason);
11
+ const reviewOutcome = rejectedReviews[0];
12
+ const reviewRationale = normalizeText(reviewOutcome?.rationale);
13
+ const rejectionReason = candidateReason ?? reviewRationale;
14
+ if (!rejectionReason)
15
+ continue;
16
+ const claim = claimsByCandidateSet.get(candidateSet.id)?.find((candidateSetClaim) => candidateSetClaim.candidateId === candidate.id);
17
+ const extraction = extractionsById.get(candidate.extractionId);
18
+ projections.push({
19
+ id: `${candidateSet.id}.${candidate.id}.learning.rejected-candidate`,
20
+ kind: "learning.rejected-candidate",
21
+ source: input.source,
22
+ createdAt: reviewOutcome?.reviewedAt ?? extraction?.extractedAt ?? input.generatedAt,
23
+ target: candidateSet.target,
24
+ claimId: claim?.id,
25
+ reviewOutcomeId: reviewOutcome?.id,
26
+ signal: "rejected-candidate.reason",
27
+ severity: "info",
28
+ summary: `Rejected candidate reason: ${rejectionReason}`,
29
+ metadata: {
30
+ rejectedCandidate: rejectedCandidateMetadata({
31
+ candidateSet,
32
+ candidate,
33
+ extraction,
34
+ reviewOutcome,
35
+ claim,
36
+ rejectionReason,
37
+ }),
38
+ },
39
+ });
40
+ }
41
+ }
5
42
  for (const reviewOutcome of input.reviewOutcomes) {
6
43
  if (reviewOutcome.withinComfortZone !== false)
7
44
  continue;
@@ -48,6 +85,20 @@ export function buildSurveyLearningProjections(input) {
48
85
  }
49
86
  return projections;
50
87
  }
88
+ function rejectedCandidateMetadata(input) {
89
+ return {
90
+ candidateId: input.candidate.id,
91
+ candidateSetId: input.candidateSet.id,
92
+ target: input.candidateSet.target,
93
+ rejectionReason: input.rejectionReason,
94
+ ...(input.reviewOutcome ? { reviewOutcomeId: input.reviewOutcome.id, reviewStatus: input.reviewOutcome.status } : {}),
95
+ ...(input.claim ? { claimId: input.claim.id } : {}),
96
+ ...(input.extraction ? { extractionId: input.extraction.id } : {}),
97
+ ...(input.candidateSet.selectedCandidateId ? { selectedCandidateId: input.candidateSet.selectedCandidateId } : {}),
98
+ ...(input.candidate.rejectionReason ? { candidateRejectionReason: input.candidate.rejectionReason } : {}),
99
+ ...(input.reviewOutcome?.rationale ? { reviewRationale: input.reviewOutcome.rationale } : {}),
100
+ };
101
+ }
51
102
  function findClaimForReview(claims, reviewOutcome) {
52
103
  if (reviewOutcome.candidateId) {
53
104
  return claims.find((claim) => claim.candidateId === reviewOutcome.candidateId);
@@ -75,3 +126,7 @@ function groupBy(items, getKey) {
75
126
  }
76
127
  return map;
77
128
  }
129
+ function normalizeText(value) {
130
+ const trimmed = value?.trim();
131
+ return trimmed ? trimmed : undefined;
132
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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",