@kontourai/survey 0.4.4 → 0.4.5

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
@@ -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.5",
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",