@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.
- package/README.md +211 -9
- package/dist/examples/review-workbench/downstream-public-directory-adapter.d.ts +3 -0
- package/dist/examples/review-workbench/downstream-public-directory-adapter.js +262 -0
- package/dist/examples/review-workbench/review-queue-session.d.ts +54 -0
- package/dist/examples/review-workbench/review-queue-session.js +122 -0
- package/dist/examples/review-workbench/review-surface-preview.d.ts +55 -0
- package/dist/examples/review-workbench/review-surface-preview.js +104 -0
- package/dist/examples/review-workbench/review-workbench-data.d.ts +261 -0
- package/dist/examples/review-workbench/review-workbench-data.js +199 -0
- package/dist/examples/review-workbench/review-workbench.d.ts +7 -0
- package/dist/examples/review-workbench/review-workbench.js +488 -0
- package/dist/fixtures/downstream-public-directory-proposal.d.ts +100 -0
- package/dist/fixtures/downstream-public-directory-proposal.js +59 -0
- package/dist/fixtures/public-directory-review-resource.d.ts +156 -0
- package/dist/fixtures/public-directory-review-resource.js +157 -0
- package/dist/fixtures/regulated-document-review-resource.d.ts +121 -0
- package/dist/fixtures/regulated-document-review-resource.js +131 -0
- package/dist/src/builder.d.ts +4 -1
- package/dist/src/builder.js +7 -0
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.js +4 -2
- package/dist/src/learning-projections.d.ts +19 -0
- package/dist/src/learning-projections.js +77 -0
- package/dist/src/raw-source.d.ts +15 -0
- package/dist/src/raw-source.js +22 -0
- package/dist/src/review-proof.d.ts +28 -0
- package/dist/src/review-proof.js +58 -1
- package/dist/src/review-resource.d.ts +111 -0
- package/dist/src/review-resource.js +1 -0
- package/dist/src/to-surface.js +226 -13
- package/dist/src/types.d.ts +17 -1
- 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
|
+
}
|
package/dist/src/raw-source.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/raw-source.js
CHANGED
|
@@ -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"];
|
package/dist/src/review-proof.js
CHANGED
|
@@ -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:
|
|
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";
|
package/dist/src/to-surface.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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";
|