@kontourai/survey 1.19.0 → 2.0.0
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 +1 -1
- package/dist/src/agent-utterance.js +4 -5
- package/dist/src/canonical-reviewed-trust-input.d.ts +31 -0
- package/dist/src/canonical-reviewed-trust-input.js +265 -0
- package/dist/src/extraction-improvement-proposal.d.ts +17 -0
- package/dist/src/extraction-improvement-proposal.js +33 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/package.json +3 -11
- package/dist/src/anthropic.d.ts +0 -104
- package/dist/src/anthropic.js +0 -383
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ One observation, one chain: the page it came from, what the extractor read, who
|
|
|
103
103
|
|
|
104
104
|
Keep producer operational state outside Survey. Queue status, reviewer form state, retries, source caches, and product policy decisions belong in the producer's own data model. Survey carries only the portable evidence chain records needed by Surface.
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Model-backed producers implement `MappingProposer` or `UtteranceClaimExtractor` in the product that owns the prompt, parsing, runtime policy, and domain mapping. Survey accepts their normalized proposals through the same framework-neutral interfaces and review path; it does not acquire credentials or depend on an AI runtime.
|
|
107
107
|
|
|
108
108
|
When you build an `authorized-action` authorizing block outside the workbench, pair `buildAuthorizedActionAuthorizing` with `buildPromptRef({ module, component, version?, scheme? })` — `buildPromptRef` formats a well-formed, versioned `promptRef` (bare `"review-workbench/decision-card@v1"` or scheme-prefixed `"survey://<module>/<component>@v1"`) that `buildAuthorizedActionAuthorizing` accepts directly, instead of hand-formatting the string.
|
|
109
109
|
|
|
@@ -22,17 +22,16 @@ import { projectProposalsToCandidateSet } from "./producer-profile.js";
|
|
|
22
22
|
/**
|
|
23
23
|
* The Candidate Conflict comparison key for an utterance-proposed value.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
* one in `./anthropic.js`) normalizes `value` before it reaches
|
|
25
|
+
* Extractors do not normalize `value` before it reaches
|
|
27
26
|
* `ExtractedStatement` — case and internal formatting are preserved
|
|
28
27
|
* verbatim. String values are the only case where "representation noise"
|
|
29
28
|
* (leading/trailing whitespace from excerpt boundaries, incidental case
|
|
30
|
-
* differences like "Healthy" vs "healthy") is plausible
|
|
31
|
-
*
|
|
29
|
+
* differences like "Healthy" vs "healthy") is plausible in producer output,
|
|
30
|
+
* so this key trims + lowercases STRING values
|
|
32
31
|
* in the COMPARISON KEY ONLY — the stored `Candidate.value`/`Extraction.value`
|
|
33
32
|
* stay byte-for-byte verbatim; this function only feeds `equivalenceKey`,
|
|
34
33
|
* never `value`. Non-string values (number, boolean, null — the other types
|
|
35
|
-
* the
|
|
34
|
+
* supported by the portable record contract) compare via exact canonical
|
|
36
35
|
* `JSON.stringify`, so there is no cross-type coercion that could silently
|
|
37
36
|
* equate e.g. "5" and 5, or lose a genuine numeric disagreement (5 vs 6 is
|
|
38
37
|
* never noise). This mirrors the Producer Profile core's established
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SurveyInput } from "./types.js";
|
|
2
|
+
import type { ReviewItem } from "./review-resource.js";
|
|
3
|
+
import type { ReviewWorkbenchResult } from "./review-workbench/review-workbench.js";
|
|
4
|
+
export interface BuildCanonicalReviewedTrustInputOptions {
|
|
5
|
+
/** Producer identity for the resulting SurveyInput batch. */
|
|
6
|
+
readonly source: string;
|
|
7
|
+
/** Server-controlled projection time. */
|
|
8
|
+
readonly generatedAt: string;
|
|
9
|
+
/** Stable producer-owned identity for this review projection. */
|
|
10
|
+
readonly projectionContextId: string;
|
|
11
|
+
/** Canonical ReviewItems from the server-owned pre-decision snapshot. */
|
|
12
|
+
readonly items: readonly ReviewItem[];
|
|
13
|
+
/** Results derived by Survey's server apply boundary from snapshot + events. */
|
|
14
|
+
readonly results: readonly ReviewWorkbenchResult[];
|
|
15
|
+
}
|
|
16
|
+
export interface CanonicalReviewedTrustInput {
|
|
17
|
+
readonly surveyInput: SurveyInput;
|
|
18
|
+
/** Pass unchanged to buildSurveyTrustBundle's projectionContextId option. */
|
|
19
|
+
readonly projectionContextId: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Projects server-applied review records into the complete SurveyInput consumed
|
|
23
|
+
* by buildSurveyTrustBundle. The ReviewItem and ReviewWorkbenchResult are the
|
|
24
|
+
* authority: callers cannot override status, value, identity, or provenance.
|
|
25
|
+
*
|
|
26
|
+
* The helper deliberately returns the projection context beside SurveyInput
|
|
27
|
+
* because repeated append-only projections need that context at the Surface
|
|
28
|
+
* projection boundary, while SurveyInput's existing byte shape remains
|
|
29
|
+
* unchanged for compatibility.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildCanonicalReviewedTrustInput(options: BuildCanonicalReviewedTrustInputOptions): CanonicalReviewedTrustInput;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
|
|
2
|
+
import { canonicalJson } from "./review-workbench/canonical.js";
|
|
3
|
+
import { workbenchDecisionDefinitions } from "./review-workbench/review-queue-session.js";
|
|
4
|
+
/**
|
|
5
|
+
* Projects server-applied review records into the complete SurveyInput consumed
|
|
6
|
+
* by buildSurveyTrustBundle. The ReviewItem and ReviewWorkbenchResult are the
|
|
7
|
+
* authority: callers cannot override status, value, identity, or provenance.
|
|
8
|
+
*
|
|
9
|
+
* The helper deliberately returns the projection context beside SurveyInput
|
|
10
|
+
* because repeated append-only projections need that context at the Surface
|
|
11
|
+
* projection boundary, while SurveyInput's existing byte shape remains
|
|
12
|
+
* unchanged for compatibility.
|
|
13
|
+
*/
|
|
14
|
+
export function buildCanonicalReviewedTrustInput(options) {
|
|
15
|
+
requireNonEmpty(options.source, "source");
|
|
16
|
+
requireTimestamp(options.generatedAt, "generatedAt");
|
|
17
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(options.projectionContextId)) {
|
|
18
|
+
throw new Error("projectionContextId must be a non-empty portable resource-name fragment.");
|
|
19
|
+
}
|
|
20
|
+
const itemsByName = uniqueBy(options.items, (item) => item.metadata.name, "ReviewItem");
|
|
21
|
+
const resultsByName = uniqueBy(options.results, (result) => result.reviewItemName, "ReviewWorkbenchResult");
|
|
22
|
+
if (itemsByName.size !== resultsByName.size) {
|
|
23
|
+
throw new Error("Canonical review projection requires exactly one resolved result for every ReviewItem.");
|
|
24
|
+
}
|
|
25
|
+
const rawSources = new Map();
|
|
26
|
+
const extractions = new Map();
|
|
27
|
+
const candidateSets = new Map();
|
|
28
|
+
const reviewOutcomes = new Map();
|
|
29
|
+
const claims = new Map();
|
|
30
|
+
for (const item of options.items) {
|
|
31
|
+
const result = resultsByName.get(item.metadata.name);
|
|
32
|
+
if (!result) {
|
|
33
|
+
throw new Error(`ReviewItem ${item.metadata.name} has no canonical server-applied result.`);
|
|
34
|
+
}
|
|
35
|
+
assertCanonicalResult(item, result);
|
|
36
|
+
const candidates = item.spec.candidates.map((candidate) => {
|
|
37
|
+
const records = projectCandidate(item, candidate);
|
|
38
|
+
addConsistent(rawSources, records.rawSource, "raw source");
|
|
39
|
+
addConsistent(extractions, records.extraction, "extraction");
|
|
40
|
+
return records.candidate;
|
|
41
|
+
});
|
|
42
|
+
const selected = item.spec.candidates.find((candidate) => candidate.id === result.selectedCandidateId);
|
|
43
|
+
const selectedRecordId = selected.projection?.candidateId ?? selected.id;
|
|
44
|
+
const candidateSetId = selected.projection?.candidateSetId
|
|
45
|
+
?? item.spec.projection?.candidateSetId
|
|
46
|
+
?? `${item.metadata.name}.candidates`;
|
|
47
|
+
if (candidates.some((candidate) => candidate.metadata?.candidateSetId !== candidateSetId)) {
|
|
48
|
+
throw new Error(`ReviewItem ${item.metadata.name} carries conflicting candidate-set projection ids.`);
|
|
49
|
+
}
|
|
50
|
+
const candidateSet = {
|
|
51
|
+
id: candidateSetId,
|
|
52
|
+
target: item.spec.target,
|
|
53
|
+
candidates: candidates.map(({ metadata, ...candidate }) => ({
|
|
54
|
+
...candidate,
|
|
55
|
+
...(metadata && Object.keys(metadata).length > 1
|
|
56
|
+
? { metadata: Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== "candidateSetId")) }
|
|
57
|
+
: {}),
|
|
58
|
+
})),
|
|
59
|
+
selectedCandidateId: selectedRecordId,
|
|
60
|
+
status: result.decision === "could-not-confirm"
|
|
61
|
+
? (item.spec.candidateSetStatus ?? "needs-review")
|
|
62
|
+
: "resolved",
|
|
63
|
+
...(result.rationale ?? item.spec.rationale
|
|
64
|
+
? { rationale: result.rationale ?? item.spec.rationale }
|
|
65
|
+
: {}),
|
|
66
|
+
};
|
|
67
|
+
addConsistent(candidateSets, candidateSet, "candidate set");
|
|
68
|
+
const decision = result.reviewDecision.spec;
|
|
69
|
+
const reviewOutcomeId = decision.projection?.reviewOutcomeId
|
|
70
|
+
?? selected.projection?.reviewOutcomeId
|
|
71
|
+
?? `${item.metadata.name}.${result.decision}.review-outcome`;
|
|
72
|
+
const reviewOutcome = {
|
|
73
|
+
id: reviewOutcomeId,
|
|
74
|
+
candidateSetId,
|
|
75
|
+
candidateId: selectedRecordId,
|
|
76
|
+
status: result.status,
|
|
77
|
+
...(decision.resolution ? { resolution: decision.resolution } : {}),
|
|
78
|
+
...(decision.resolutionReason ? { resolutionReason: decision.resolutionReason } : {}),
|
|
79
|
+
...(decision.attemptEvidenceIds?.length ? { attemptEvidenceIds: [...decision.attemptEvidenceIds] } : {}),
|
|
80
|
+
...(decision.actor?.id ? { actor: decision.actor.id } : {}),
|
|
81
|
+
...(decision.reviewedAt ? { reviewedAt: decision.reviewedAt } : {}),
|
|
82
|
+
...(decision.rationale ? { rationale: decision.rationale } : {}),
|
|
83
|
+
...(decision.evidenceIds?.length ? { evidenceIds: [...decision.evidenceIds] } : {}),
|
|
84
|
+
...(decision.withinComfortZone !== undefined ? { withinComfortZone: decision.withinComfortZone } : {}),
|
|
85
|
+
...(decision.comfortZoneNote ? { comfortZoneNote: decision.comfortZoneNote } : {}),
|
|
86
|
+
...(decision.authorizing ? { authorizing: decision.authorizing } : {}),
|
|
87
|
+
metadata: {
|
|
88
|
+
workbenchDecision: result.decision,
|
|
89
|
+
...(result.editedValue !== undefined ? { editedValue: result.editedValue } : {}),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
addConsistent(reviewOutcomes, reviewOutcome, "review outcome");
|
|
93
|
+
const hint = selected.claimTarget;
|
|
94
|
+
assertSingleProjectionId("claim", item.metadata.name, [
|
|
95
|
+
decision.projection?.claimId,
|
|
96
|
+
item.spec.projection?.claimId,
|
|
97
|
+
...item.spec.candidates.flatMap((candidate) => [candidate.projection?.claimId, candidate.claimTarget.claimId]),
|
|
98
|
+
]);
|
|
99
|
+
const claimId = decision.projection?.claimId
|
|
100
|
+
?? selected.projection?.claimId
|
|
101
|
+
?? item.spec.projection?.claimId
|
|
102
|
+
?? hint.claimId
|
|
103
|
+
?? `${item.metadata.name}.claim`;
|
|
104
|
+
const claim = {
|
|
105
|
+
id: claimId,
|
|
106
|
+
candidateSetId,
|
|
107
|
+
candidateId: selectedRecordId,
|
|
108
|
+
subjectType: hint.subjectType,
|
|
109
|
+
subjectId: hint.subjectId,
|
|
110
|
+
facet: hint.facet,
|
|
111
|
+
claimType: hint.claimType,
|
|
112
|
+
fieldOrBehavior: hint.fieldOrBehavior,
|
|
113
|
+
value: result.effectiveValue,
|
|
114
|
+
status: result.status,
|
|
115
|
+
impactLevel: hint.impactLevel,
|
|
116
|
+
updatedAt: decision.reviewedAt ?? options.generatedAt,
|
|
117
|
+
...(hint.evidenceType ? { evidenceType: hint.evidenceType } : {}),
|
|
118
|
+
...(hint.evidenceMethod ? { evidenceMethod: hint.evidenceMethod } : {}),
|
|
119
|
+
...(hint.derivedFrom ? { derivedFrom: [...hint.derivedFrom] } : {}),
|
|
120
|
+
collectedBy: hint.collectedBy ?? selected.extraction.extractor ?? options.source,
|
|
121
|
+
...(decision.actor?.id ? { actor: decision.actor.id } : {}),
|
|
122
|
+
};
|
|
123
|
+
addConsistent(claims, claim, "claim target");
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
projectionContextId: options.projectionContextId,
|
|
127
|
+
surveyInput: {
|
|
128
|
+
contractVersion: SURVEY_INPUT_CONTRACT_VERSION,
|
|
129
|
+
source: options.source,
|
|
130
|
+
generatedAt: options.generatedAt,
|
|
131
|
+
rawSources: [...rawSources.values()],
|
|
132
|
+
extractions: [...extractions.values()],
|
|
133
|
+
candidateSets: [...candidateSets.values()],
|
|
134
|
+
reviewOutcomes: [...reviewOutcomes.values()],
|
|
135
|
+
claims: [...claims.values()],
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function assertCanonicalResult(item, result) {
|
|
140
|
+
const selected = item.spec.candidates.find((candidate) => candidate.id === result.selectedCandidateId);
|
|
141
|
+
if (!selected) {
|
|
142
|
+
throw new Error(`Review result ${result.reviewItemName} selects an unknown candidate.`);
|
|
143
|
+
}
|
|
144
|
+
if (canonicalJson(selected) !== canonicalJson(result.selectedCandidate)) {
|
|
145
|
+
throw new Error(`Review result ${result.reviewItemName} selected candidate does not match its canonical ReviewItem.`);
|
|
146
|
+
}
|
|
147
|
+
const unselected = item.spec.candidates.filter((candidate) => candidate.id !== selected.id);
|
|
148
|
+
if (canonicalJson(unselected) !== canonicalJson(result.unselectedCandidates)) {
|
|
149
|
+
throw new Error(`Review result ${result.reviewItemName} unselected candidates do not match its canonical ReviewItem.`);
|
|
150
|
+
}
|
|
151
|
+
if (result.selectedCandidateRole !== selected.role || canonicalJson(result.selectedValue) !== canonicalJson(selected.value)) {
|
|
152
|
+
throw new Error(`Review result ${result.reviewItemName} selected identity does not match its canonical ReviewItem.`);
|
|
153
|
+
}
|
|
154
|
+
const decision = result.reviewDecision.spec;
|
|
155
|
+
const definition = workbenchDecisionDefinitions[result.decision];
|
|
156
|
+
if (decision.reviewItemName !== item.metadata.name
|
|
157
|
+
|| decision.candidateId !== result.selectedCandidateId
|
|
158
|
+
|| decision.status !== result.status
|
|
159
|
+
|| decision.status !== definition.status
|
|
160
|
+
|| decision.rationale !== result.rationale
|
|
161
|
+
|| canonicalJson(decision.editedValue) !== canonicalJson(result.editedValue)) {
|
|
162
|
+
throw new Error(`Review result ${result.reviewItemName} contradicts its canonical ReviewDecision.`);
|
|
163
|
+
}
|
|
164
|
+
if ((result.decision === "could-not-confirm") !== (decision.resolution === "could_not_confirm")) {
|
|
165
|
+
throw new Error(`Review result ${result.reviewItemName} contradicts its canonical review resolution.`);
|
|
166
|
+
}
|
|
167
|
+
const expectedEffective = result.editedValue !== undefined && result.decision === "accept-proposed"
|
|
168
|
+
? result.editedValue
|
|
169
|
+
: selected.value;
|
|
170
|
+
if (canonicalJson(expectedEffective) !== canonicalJson(result.effectiveValue)) {
|
|
171
|
+
throw new Error(`Review result ${result.reviewItemName} effective value is not canonical.`);
|
|
172
|
+
}
|
|
173
|
+
for (const candidate of item.spec.candidates) {
|
|
174
|
+
if (canonicalJson(claimTargetIdentity(candidate.claimTarget)) !== canonicalJson(claimTargetIdentity(selected.claimTarget))) {
|
|
175
|
+
throw new Error(`ReviewItem ${item.metadata.name} candidates carry conflicting claim targets.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function claimTargetIdentity(target) {
|
|
180
|
+
const { claimId: _claimId, ...identity } = target;
|
|
181
|
+
return identity;
|
|
182
|
+
}
|
|
183
|
+
function projectCandidate(item, input) {
|
|
184
|
+
const rawSourceId = input.projection?.rawSourceId ?? input.source.sourceId ?? `${item.metadata.name}.${input.id}.source`;
|
|
185
|
+
const extractionId = input.projection?.extractionId ?? input.extraction.extractionId ?? `${item.metadata.name}.${input.id}.extraction`;
|
|
186
|
+
const candidateSetId = input.projection?.candidateSetId ?? item.spec.projection?.candidateSetId ?? `${item.metadata.name}.candidates`;
|
|
187
|
+
const sourceKind = input.source.kind;
|
|
188
|
+
const observedAt = input.source.observedAt;
|
|
189
|
+
const locatorScheme = input.source.locatorScheme ?? input.locator?.scheme;
|
|
190
|
+
const extractor = input.extraction.extractor;
|
|
191
|
+
const extractedAt = input.extraction.extractedAt;
|
|
192
|
+
if (!sourceKind || !observedAt || !locatorScheme || !extractor || !extractedAt) {
|
|
193
|
+
throw new Error(`ReviewCandidate ${input.id} lacks source or extraction provenance required for TrustInput projection.`);
|
|
194
|
+
}
|
|
195
|
+
const rawSource = {
|
|
196
|
+
id: rawSourceId,
|
|
197
|
+
kind: sourceKind,
|
|
198
|
+
sourceRef: input.source.sourceRef,
|
|
199
|
+
observedAt,
|
|
200
|
+
...(input.source.fetchedAt ? { fetchedAt: input.source.fetchedAt } : {}),
|
|
201
|
+
...(input.source.checksum ? { checksum: input.source.checksum } : {}),
|
|
202
|
+
locatorScheme,
|
|
203
|
+
};
|
|
204
|
+
const extraction = {
|
|
205
|
+
id: extractionId,
|
|
206
|
+
sourceId: rawSourceId,
|
|
207
|
+
target: input.extraction.target,
|
|
208
|
+
value: input.value,
|
|
209
|
+
...(input.extraction.confidence ?? input.confidence) !== undefined
|
|
210
|
+
? { confidence: input.extraction.confidence ?? input.confidence }
|
|
211
|
+
: {},
|
|
212
|
+
...(input.locator?.locator ? { locator: input.locator.locator } : {}),
|
|
213
|
+
...(input.locator?.excerpt ? { excerpt: input.locator.excerpt } : {}),
|
|
214
|
+
extractor,
|
|
215
|
+
extractedAt,
|
|
216
|
+
...(input.extraction.model ? { metadata: { model: input.extraction.model } } : {}),
|
|
217
|
+
};
|
|
218
|
+
const candidate = {
|
|
219
|
+
id: input.projection?.candidateId ?? input.id,
|
|
220
|
+
extractionId,
|
|
221
|
+
value: input.value,
|
|
222
|
+
...(input.confidence !== undefined ? { confidence: input.confidence } : {}),
|
|
223
|
+
...(input.sourceRank !== undefined ? { sourceRank: input.sourceRank } : {}),
|
|
224
|
+
...(input.rejectionReason ? { rejectionReason: input.rejectionReason } : {}),
|
|
225
|
+
metadata: {
|
|
226
|
+
candidateSetId,
|
|
227
|
+
...(input.role ? { role: input.role } : {}),
|
|
228
|
+
...(input.producer ? { producer: input.producer } : {}),
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
return { rawSource, extraction, candidate };
|
|
232
|
+
}
|
|
233
|
+
function uniqueBy(values, id, label) {
|
|
234
|
+
const output = new Map();
|
|
235
|
+
for (const value of values) {
|
|
236
|
+
const key = requireNonEmpty(id(value), `${label} identity`);
|
|
237
|
+
if (output.has(key)) {
|
|
238
|
+
throw new Error(`Canonical review projection received duplicate ${label} ${key}.`);
|
|
239
|
+
}
|
|
240
|
+
output.set(key, value);
|
|
241
|
+
}
|
|
242
|
+
return output;
|
|
243
|
+
}
|
|
244
|
+
function addConsistent(records, value, label) {
|
|
245
|
+
const existing = records.get(value.id);
|
|
246
|
+
if (existing && canonicalJson(existing) !== canonicalJson(value)) {
|
|
247
|
+
throw new Error(`Canonical review projection found conflicting ${label} ${value.id}.`);
|
|
248
|
+
}
|
|
249
|
+
records.set(value.id, value);
|
|
250
|
+
}
|
|
251
|
+
function requireNonEmpty(value, label) {
|
|
252
|
+
if (!value.trim())
|
|
253
|
+
throw new Error(`${label} must be non-empty.`);
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
function requireTimestamp(value, label) {
|
|
257
|
+
if (!value.trim() || Number.isNaN(Date.parse(value)))
|
|
258
|
+
throw new Error(`${label} must be an ISO timestamp.`);
|
|
259
|
+
}
|
|
260
|
+
function assertSingleProjectionId(label, itemName, values) {
|
|
261
|
+
const ids = new Set(values.filter((value) => value !== undefined));
|
|
262
|
+
if (ids.size > 1) {
|
|
263
|
+
throw new Error(`ReviewItem ${itemName} carries conflicting ${label} projection ids.`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -123,6 +123,23 @@ export interface RejectExtractionImprovementProposalInput {
|
|
|
123
123
|
}
|
|
124
124
|
/** One producer/store disposition is allowed for each shared dispositionKey. */
|
|
125
125
|
export type ExtractionImprovementDisposition = ExtractionImprovementActivationRequest | ExtractionImprovementRejection;
|
|
126
|
+
export interface ExtractionImprovementDispositionConflict {
|
|
127
|
+
kind: "survey.extraction-improvement-disposition-conflict";
|
|
128
|
+
dispositionKey: string;
|
|
129
|
+
/** Canonically ordered distinct records. The fold never selects a winner. */
|
|
130
|
+
dispositions: readonly ExtractionImprovementDisposition[];
|
|
131
|
+
}
|
|
132
|
+
export interface FoldExtractionImprovementDispositionsResult {
|
|
133
|
+
/** One canonically ordered record for each uncontested disposition key. */
|
|
134
|
+
dispositions: readonly ExtractionImprovementDisposition[];
|
|
135
|
+
/** Canonically ordered conflicts for keys with more than one distinct record. */
|
|
136
|
+
conflicts: readonly ExtractionImprovementDispositionConflict[];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Folds producer-owned disposition records without performing I/O or selecting
|
|
140
|
+
* between conflicting decisions. Byte-equivalent replay is idempotent.
|
|
141
|
+
*/
|
|
142
|
+
export declare function foldExtractionImprovementDispositions(input: readonly ExtractionImprovementDisposition[]): FoldExtractionImprovementDispositionsResult;
|
|
126
143
|
/** Builds a frozen draft from validated canonical records. Performs no I/O. */
|
|
127
144
|
export declare function buildExtractionImprovementProposal(input: BuildExtractionImprovementProposalInput): ExtractionImprovementDraft;
|
|
128
145
|
/** Emits a reversible activation request, never an activation. */
|
|
@@ -2,6 +2,39 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { buildReviewItemsFromExtractionEnvelopeImport } from "./extraction-envelope.js";
|
|
3
3
|
import { reviewResourceApiVersion } from "./review-resource.js";
|
|
4
4
|
import { canonicalJson } from "./review-workbench/canonical.js";
|
|
5
|
+
/**
|
|
6
|
+
* Folds producer-owned disposition records without performing I/O or selecting
|
|
7
|
+
* between conflicting decisions. Byte-equivalent replay is idempotent.
|
|
8
|
+
*/
|
|
9
|
+
export function foldExtractionImprovementDispositions(input) {
|
|
10
|
+
const byKey = new Map();
|
|
11
|
+
for (const disposition of input) {
|
|
12
|
+
const records = byKey.get(disposition.dispositionKey) ?? new Map();
|
|
13
|
+
records.set(canonicalJson(disposition), disposition);
|
|
14
|
+
byKey.set(disposition.dispositionKey, records);
|
|
15
|
+
}
|
|
16
|
+
const dispositions = [];
|
|
17
|
+
const conflicts = [];
|
|
18
|
+
for (const dispositionKey of [...byKey.keys()].sort()) {
|
|
19
|
+
const records = [...byKey.get(dispositionKey).entries()]
|
|
20
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
21
|
+
.map(([, disposition]) => disposition);
|
|
22
|
+
if (records.length === 1) {
|
|
23
|
+
dispositions.push(records[0]);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
conflicts.push(Object.freeze({
|
|
27
|
+
kind: "survey.extraction-improvement-disposition-conflict",
|
|
28
|
+
dispositionKey,
|
|
29
|
+
dispositions: Object.freeze(records),
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
dispositions: Object.freeze(dispositions),
|
|
35
|
+
conflicts: Object.freeze(conflicts),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
5
38
|
/** Builds a frozen draft from validated canonical records. Performs no I/O. */
|
|
6
39
|
export function buildExtractionImprovementProposal(input) {
|
|
7
40
|
const diagnosis = normalizeDiagnosis(input.diagnosis);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -14,12 +14,14 @@ export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-r
|
|
|
14
14
|
export type { CurrentProposedCandidateRole, ReviewedCurrentProposedResolutionInput, } from "./reviewed-current-proposed-resolution.js";
|
|
15
15
|
export { buildSurveyTrustBundle } from "./to-surface.js";
|
|
16
16
|
export type { BuildSurveyTrustBundleOptions } from "./to-surface.js";
|
|
17
|
+
export { buildCanonicalReviewedTrustInput } from "./canonical-reviewed-trust-input.js";
|
|
18
|
+
export type { BuildCanonicalReviewedTrustInputOptions, CanonicalReviewedTrustInput, } from "./canonical-reviewed-trust-input.js";
|
|
17
19
|
export { buildSurveyLearningProjections } from "./learning-projections.js";
|
|
18
20
|
export type { LearningProjection, LearningProjectionKind, LearningProjectionSeverity, LearningProjectionSignal, } from "./learning-projections.js";
|
|
19
21
|
export { buildReviewedLearningUpdateProposal } from "./learning-update-proposal.js";
|
|
20
22
|
export type { LearningUpdateEvidenceReference, LearningUpdateProposal, OpaqueEvidenceReference, ProvenanceReference, ReviewedLearningUpdateProposalInput, ReviewProofReference, } from "./learning-update-proposal.js";
|
|
21
|
-
export { approveExtractionImprovementProposal, buildExtractionImprovementProposal, rejectExtractionImprovementProposal, } from "./extraction-improvement-proposal.js";
|
|
22
|
-
export type { ApproveExtractionImprovementProposalInput, AcceptedExtractionDiagnosis, BadExtractionDiagnosis, BuildExtractionImprovementProposalInput, ExtractionImprovementActivationRequest, ExtractionImprovementDiagnosis, ExtractionImprovementDisposition, ExtractionImprovementDraft, ExtractionImprovementLineage, ExtractionImprovementRejection, ExtractionImprovementRecordDigests, ExtractionImprovementReview, ExtractionTaskSpecReference, InsufficientSourceEvidenceDiagnosis, ProducerApproval, RejectExtractionImprovementProposalInput, } from "./extraction-improvement-proposal.js";
|
|
23
|
+
export { approveExtractionImprovementProposal, buildExtractionImprovementProposal, foldExtractionImprovementDispositions, rejectExtractionImprovementProposal, } from "./extraction-improvement-proposal.js";
|
|
24
|
+
export type { ApproveExtractionImprovementProposalInput, AcceptedExtractionDiagnosis, BadExtractionDiagnosis, BuildExtractionImprovementProposalInput, ExtractionImprovementActivationRequest, ExtractionImprovementDiagnosis, ExtractionImprovementDisposition, ExtractionImprovementDispositionConflict, ExtractionImprovementDraft, ExtractionImprovementLineage, ExtractionImprovementRejection, ExtractionImprovementRecordDigests, ExtractionImprovementReview, ExtractionTaskSpecReference, FoldExtractionImprovementDispositionsResult, InsufficientSourceEvidenceDiagnosis, ProducerApproval, RejectExtractionImprovementProposalInput, } from "./extraction-improvement-proposal.js";
|
|
23
25
|
export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalReviewProofJson, hashCanonicalReviewProofPayload, verifyCanonicalReviewProofPayload, REVIEW_PROOF_CONTRACT_VERSION, REVIEW_PROOF_PACKAGE_NAME, REVIEW_PROOF_SCHEMA, REVIEW_PROOF_SCHEMA_VERSION, } from "./review-proof.js";
|
|
24
26
|
export type { CanonicalReviewProofPayload, CanonicalReviewProofPayloadV1, CanonicalReviewProofPayloadV2, ReviewProofInput, } from "./review-proof.js";
|
|
25
27
|
export { fieldObservation } from "./field-observation.js";
|
package/dist/src/index.js
CHANGED
|
@@ -6,9 +6,10 @@ export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from
|
|
|
6
6
|
export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
|
|
7
7
|
export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
|
|
8
8
|
export { buildSurveyTrustBundle } from "./to-surface.js";
|
|
9
|
+
export { buildCanonicalReviewedTrustInput } from "./canonical-reviewed-trust-input.js";
|
|
9
10
|
export { buildSurveyLearningProjections } from "./learning-projections.js";
|
|
10
11
|
export { buildReviewedLearningUpdateProposal } from "./learning-update-proposal.js";
|
|
11
|
-
export { approveExtractionImprovementProposal, buildExtractionImprovementProposal, rejectExtractionImprovementProposal, } from "./extraction-improvement-proposal.js";
|
|
12
|
+
export { approveExtractionImprovementProposal, buildExtractionImprovementProposal, foldExtractionImprovementDispositions, rejectExtractionImprovementProposal, } from "./extraction-improvement-proposal.js";
|
|
12
13
|
export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalReviewProofJson, hashCanonicalReviewProofPayload, verifyCanonicalReviewProofPayload, REVIEW_PROOF_CONTRACT_VERSION, REVIEW_PROOF_PACKAGE_NAME, REVIEW_PROOF_SCHEMA, REVIEW_PROOF_SCHEMA_VERSION, } from "./review-proof.js";
|
|
13
14
|
export { fieldObservation } from "./field-observation.js";
|
|
14
15
|
export { repeatedObservation } from "./repeated-observation.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/survey",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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",
|
|
@@ -18,10 +18,6 @@
|
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
20
20
|
".": "./dist/src/index.js",
|
|
21
|
-
"./anthropic": {
|
|
22
|
-
"types": "./dist/src/anthropic.d.ts",
|
|
23
|
-
"default": "./dist/src/anthropic.js"
|
|
24
|
-
},
|
|
25
21
|
"./review-workbench": {
|
|
26
22
|
"types": "./dist/src/review-workbench/review-workbench.d.ts",
|
|
27
23
|
"default": "./dist/src/review-workbench/review-workbench.js"
|
|
@@ -55,7 +51,8 @@
|
|
|
55
51
|
"typecheck": "tsc --noEmit",
|
|
56
52
|
"test": "npm run build && node --test dist/tests/*.test.js",
|
|
57
53
|
"test:browser": "playwright test",
|
|
58
|
-
"
|
|
54
|
+
"test:browser:concurrent": "npm run build && node scripts/test-playwright-concurrency.mjs",
|
|
55
|
+
"verify": "node scripts/check-content-boundary.cjs && npm run check:decisions && npm run typecheck && npm test && npm run check:review-workbench-assets && npm run check:review-workbench && npm run test:browser && npm run test:browser:concurrent",
|
|
59
56
|
"check:content-boundary": "node scripts/check-content-boundary.cjs",
|
|
60
57
|
"check:decisions": "node scripts/check-decisions.cjs check",
|
|
61
58
|
"gen:decisions-index": "node scripts/check-decisions.cjs gen-index",
|
|
@@ -74,19 +71,14 @@
|
|
|
74
71
|
"@kontourai/surface": "^2.9.0"
|
|
75
72
|
},
|
|
76
73
|
"peerDependencies": {
|
|
77
|
-
"@anthropic-ai/sdk": ">=0.20.0",
|
|
78
74
|
"typescript": ">=5.0.0"
|
|
79
75
|
},
|
|
80
76
|
"peerDependenciesMeta": {
|
|
81
|
-
"@anthropic-ai/sdk": {
|
|
82
|
-
"optional": true
|
|
83
|
-
},
|
|
84
77
|
"typescript": {
|
|
85
78
|
"optional": true
|
|
86
79
|
}
|
|
87
80
|
},
|
|
88
81
|
"devDependencies": {
|
|
89
|
-
"@anthropic-ai/sdk": "^0.54.0",
|
|
90
82
|
"@kontourai/traverse": "^0.19.0",
|
|
91
83
|
"@kontourai/ui": "^1.1.0",
|
|
92
84
|
"@playwright/test": "^1.60.0",
|
package/dist/src/anthropic.d.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anthropic production adapters for Survey's pluggable interfaces.
|
|
3
|
-
*
|
|
4
|
-
* ADR 0003 §4 compliance: these implementations are PROPOSERS only. Every
|
|
5
|
-
* output is a proposal (MappingProposal / ExtractedStatement) that goes
|
|
6
|
-
* through the existing review/auto-accept machinery before counting.
|
|
7
|
-
* Nothing here bypasses review.
|
|
8
|
-
*
|
|
9
|
-
* Subpath export: import from "@kontourai/survey/anthropic" — this module is
|
|
10
|
-
* NOT re-exported from the main index.ts so consumers without @anthropic-ai/sdk
|
|
11
|
-
* pay nothing.
|
|
12
|
-
*
|
|
13
|
-
* Injected client: both factories accept an optional pre-built client so tests
|
|
14
|
-
* can inject a fake without hitting the network. If no client is provided, one
|
|
15
|
-
* is constructed from opts.apiKey (falling back to process.env.ANTHROPIC_API_KEY).
|
|
16
|
-
*/
|
|
17
|
-
import type { MappingProposer } from "./inquiry-mapping.js";
|
|
18
|
-
import type { UtteranceClaimExtractor } from "./agent-utterance.js";
|
|
19
|
-
export interface AnthropicToolResultBlock {
|
|
20
|
-
type: "tool_result";
|
|
21
|
-
tool_use_id: string;
|
|
22
|
-
content: string;
|
|
23
|
-
}
|
|
24
|
-
export interface AnthropicToolUseBlock {
|
|
25
|
-
type: "tool_use";
|
|
26
|
-
id: string;
|
|
27
|
-
name: string;
|
|
28
|
-
input: unknown;
|
|
29
|
-
}
|
|
30
|
-
export type AnthropicContentBlock = {
|
|
31
|
-
type: "text";
|
|
32
|
-
text: string;
|
|
33
|
-
} | AnthropicToolUseBlock;
|
|
34
|
-
export interface AnthropicMessage {
|
|
35
|
-
id: string;
|
|
36
|
-
type: "message";
|
|
37
|
-
role: "assistant";
|
|
38
|
-
content: AnthropicContentBlock[];
|
|
39
|
-
model: string;
|
|
40
|
-
stop_reason: string | null;
|
|
41
|
-
usage: {
|
|
42
|
-
input_tokens: number;
|
|
43
|
-
output_tokens: number;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
export interface AnthropicTool {
|
|
47
|
-
name: string;
|
|
48
|
-
description: string;
|
|
49
|
-
input_schema: {
|
|
50
|
-
type: "object";
|
|
51
|
-
properties: Record<string, unknown>;
|
|
52
|
-
required?: string[];
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
export interface AnthropicMessageCreateParams {
|
|
56
|
-
model: string;
|
|
57
|
-
max_tokens: number;
|
|
58
|
-
messages: Array<{
|
|
59
|
-
role: "user" | "assistant";
|
|
60
|
-
content: string;
|
|
61
|
-
}>;
|
|
62
|
-
tools: AnthropicTool[];
|
|
63
|
-
tool_choice: {
|
|
64
|
-
type: "tool";
|
|
65
|
-
name: string;
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Minimal interface matching @anthropic-ai/sdk Anthropic.messages.create.
|
|
70
|
-
* Accept the real SDK client or a test double.
|
|
71
|
-
*/
|
|
72
|
-
export interface AnthropicMessagesClient {
|
|
73
|
-
create(params: AnthropicMessageCreateParams): Promise<AnthropicMessage>;
|
|
74
|
-
}
|
|
75
|
-
export interface AnthropicAdapterOptions {
|
|
76
|
-
/** Injected client (real or mock). If absent, one is built from apiKey. */
|
|
77
|
-
client?: AnthropicMessagesClient;
|
|
78
|
-
/** API key. Falls back to ANTHROPIC_API_KEY env var. */
|
|
79
|
-
apiKey?: string;
|
|
80
|
-
/** Model to use. Defaults to "claude-sonnet-4-6". */
|
|
81
|
-
model?: string;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Create a MappingProposer backed by Anthropic's API using forced tool-use.
|
|
85
|
-
*
|
|
86
|
-
* ADR 0003 §4: returns PROPOSALS only — they flow through the existing
|
|
87
|
-
* review/auto-accept machinery before counting as mappings.
|
|
88
|
-
*
|
|
89
|
-
* Tool output is validated strictly: malformed items (missing required fields,
|
|
90
|
-
* out-of-range confidence, no target and no rule) are filtered out rather than
|
|
91
|
-
* silently accepted.
|
|
92
|
-
*/
|
|
93
|
-
export declare function createAnthropicMappingProposer(opts?: AnthropicAdapterOptions): MappingProposer;
|
|
94
|
-
/**
|
|
95
|
-
* Create a UtteranceClaimExtractor backed by Anthropic's API using forced tool-use.
|
|
96
|
-
*
|
|
97
|
-
* ADR 0003 §4: returns EXTRACTED STATEMENTS only — they carry full provenance
|
|
98
|
-
* (excerpt, span, extractor name, confidence) and flow through the Inquiry
|
|
99
|
-
* pipeline. They are never treated as authoritative.
|
|
100
|
-
*
|
|
101
|
-
* Malformed tool output is rejected/filtered — items missing required fields
|
|
102
|
-
* (subjectId, fieldOrBehavior, excerpt, confidence) are dropped.
|
|
103
|
-
*/
|
|
104
|
-
export declare function createAnthropicUtteranceExtractor(opts?: AnthropicAdapterOptions): UtteranceClaimExtractor;
|
package/dist/src/anthropic.js
DELETED
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anthropic production adapters for Survey's pluggable interfaces.
|
|
3
|
-
*
|
|
4
|
-
* ADR 0003 §4 compliance: these implementations are PROPOSERS only. Every
|
|
5
|
-
* output is a proposal (MappingProposal / ExtractedStatement) that goes
|
|
6
|
-
* through the existing review/auto-accept machinery before counting.
|
|
7
|
-
* Nothing here bypasses review.
|
|
8
|
-
*
|
|
9
|
-
* Subpath export: import from "@kontourai/survey/anthropic" — this module is
|
|
10
|
-
* NOT re-exported from the main index.ts so consumers without @anthropic-ai/sdk
|
|
11
|
-
* pay nothing.
|
|
12
|
-
*
|
|
13
|
-
* Injected client: both factories accept an optional pre-built client so tests
|
|
14
|
-
* can inject a fake without hitting the network. If no client is provided, one
|
|
15
|
-
* is constructed from opts.apiKey (falling back to process.env.ANTHROPIC_API_KEY).
|
|
16
|
-
*/
|
|
17
|
-
const DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
18
|
-
/**
|
|
19
|
-
* Build or return a messages client from options.
|
|
20
|
-
* Dynamic-imports @anthropic-ai/sdk only when no client is injected,
|
|
21
|
-
* keeping the optional peer dep out of the eager module graph.
|
|
22
|
-
*/
|
|
23
|
-
async function resolveClient(opts) {
|
|
24
|
-
if (opts.client)
|
|
25
|
-
return opts.client;
|
|
26
|
-
// Dynamically load the SDK — only reachable when no client is injected.
|
|
27
|
-
// Uses a variable module specifier so TypeScript does not try to resolve
|
|
28
|
-
// the optional peer dep at compile time. At runtime the SDK must be installed.
|
|
29
|
-
const sdkModule = "@anthropic-ai/sdk";
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
31
|
-
const sdkImport = await Function("m", "return import(m)")(sdkModule);
|
|
32
|
-
const { default: Anthropic } = sdkImport;
|
|
33
|
-
const apiKey = opts.apiKey ?? process.env["ANTHROPIC_API_KEY"];
|
|
34
|
-
if (!apiKey) {
|
|
35
|
-
throw new Error("AnthropicAdapter: no API key. Provide opts.apiKey, set ANTHROPIC_API_KEY, or inject opts.client.");
|
|
36
|
-
}
|
|
37
|
-
const sdk = new Anthropic({ apiKey });
|
|
38
|
-
return sdk.messages;
|
|
39
|
-
}
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// JSON tool schemas
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
const MAPPING_PROPOSAL_TOOL = {
|
|
44
|
-
name: "submit_mapping_proposals",
|
|
45
|
-
description: "Submit an array of candidate mappings from the natural-language question to registered canonical claim targets or derivation rules. " +
|
|
46
|
-
"You are PROPOSING for human review — every proposal must carry a rationale and confidence score. " +
|
|
47
|
-
"Per ADR 0003 §4, proposals are reviewable records; they do not resolve questions by themselves.",
|
|
48
|
-
input_schema: {
|
|
49
|
-
type: "object",
|
|
50
|
-
properties: {
|
|
51
|
-
proposals: {
|
|
52
|
-
type: "array",
|
|
53
|
-
items: {
|
|
54
|
-
type: "object",
|
|
55
|
-
properties: {
|
|
56
|
-
proposedTargetSubjectType: {
|
|
57
|
-
type: "string",
|
|
58
|
-
description: "subjectType of the canonical claim target (omit if proposing a rule)",
|
|
59
|
-
},
|
|
60
|
-
proposedTargetSubjectId: {
|
|
61
|
-
type: "string",
|
|
62
|
-
description: "subjectId of the canonical claim target (omit if proposing a rule)",
|
|
63
|
-
},
|
|
64
|
-
proposedTargetFieldOrBehavior: {
|
|
65
|
-
type: "string",
|
|
66
|
-
description: "fieldOrBehavior of the canonical claim target (omit if proposing a rule)",
|
|
67
|
-
},
|
|
68
|
-
proposedRuleId: {
|
|
69
|
-
type: "string",
|
|
70
|
-
description: "Id of the derivation rule this question maps to (omit if proposing a target)",
|
|
71
|
-
},
|
|
72
|
-
confidence: {
|
|
73
|
-
type: "number",
|
|
74
|
-
description: "Confidence in this mapping (0.0–1.0)",
|
|
75
|
-
},
|
|
76
|
-
rationale: {
|
|
77
|
-
type: "string",
|
|
78
|
-
description: "Human-readable explanation of why this mapping is proposed",
|
|
79
|
-
},
|
|
80
|
-
excerpt: {
|
|
81
|
-
type: "string",
|
|
82
|
-
description: "Verbatim excerpt from the question that drove the suggestion",
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
required: ["confidence", "rationale"],
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
required: ["proposals"],
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
const UTTERANCE_EXTRACTION_TOOL = {
|
|
93
|
-
name: "submit_extracted_statements",
|
|
94
|
-
description: "Submit an array of factual statements extracted from the agent utterance. " +
|
|
95
|
-
"Each statement maps to a canonical claim target with full provenance (excerpt, span, confidence). " +
|
|
96
|
-
"You are EXTRACTING FOR REVIEW — output is a proposal queue, not authoritative truth. " +
|
|
97
|
-
"Per ADR 0003 §4, every extracted statement requires a rationale and confidence score.",
|
|
98
|
-
input_schema: {
|
|
99
|
-
type: "object",
|
|
100
|
-
properties: {
|
|
101
|
-
statements: {
|
|
102
|
-
type: "array",
|
|
103
|
-
items: {
|
|
104
|
-
type: "object",
|
|
105
|
-
properties: {
|
|
106
|
-
subjectType: {
|
|
107
|
-
type: "string",
|
|
108
|
-
description: "The canonical subjectType (use 'unknown' if uncertain)",
|
|
109
|
-
},
|
|
110
|
-
subjectId: {
|
|
111
|
-
type: "string",
|
|
112
|
-
description: "The entity or resource the statement is about",
|
|
113
|
-
},
|
|
114
|
-
fieldOrBehavior: {
|
|
115
|
-
type: "string",
|
|
116
|
-
description: "The property or behavior being claimed",
|
|
117
|
-
},
|
|
118
|
-
value: {
|
|
119
|
-
description: "The claimed value (string, number, boolean, or null)",
|
|
120
|
-
},
|
|
121
|
-
excerpt: {
|
|
122
|
-
type: "string",
|
|
123
|
-
description: "Verbatim text from the utterance that contains this claim",
|
|
124
|
-
},
|
|
125
|
-
spanStart: {
|
|
126
|
-
type: "number",
|
|
127
|
-
description: "0-indexed character offset where the excerpt starts in the utterance",
|
|
128
|
-
},
|
|
129
|
-
spanEnd: {
|
|
130
|
-
type: "number",
|
|
131
|
-
description: "0-indexed character offset where the excerpt ends in the utterance",
|
|
132
|
-
},
|
|
133
|
-
confidence: {
|
|
134
|
-
type: "number",
|
|
135
|
-
description: "Extraction confidence (0.0–1.0)",
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
required: ["subjectId", "fieldOrBehavior", "excerpt", "confidence"],
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
required: ["statements"],
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
// Tool output parsing helpers
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
/**
|
|
149
|
-
* Extract the first tool_use block with the given name from a message.
|
|
150
|
-
* Returns undefined if not found (malformed output is rejected, never silently accepted).
|
|
151
|
-
*/
|
|
152
|
-
function extractToolUseInput(message, toolName) {
|
|
153
|
-
for (const block of message.content) {
|
|
154
|
-
if (block.type === "tool_use" && block.name === toolName) {
|
|
155
|
-
return block.input;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return undefined;
|
|
159
|
-
}
|
|
160
|
-
function isRecord(value) {
|
|
161
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
162
|
-
}
|
|
163
|
-
function isArray(value) {
|
|
164
|
-
return Array.isArray(value);
|
|
165
|
-
}
|
|
166
|
-
function stringOrUndefined(value) {
|
|
167
|
-
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
168
|
-
}
|
|
169
|
-
function numberInRange(value, min, max) {
|
|
170
|
-
if (typeof value !== "number" || !isFinite(value))
|
|
171
|
-
return undefined;
|
|
172
|
-
if (value < min || value > max)
|
|
173
|
-
return undefined;
|
|
174
|
-
return value;
|
|
175
|
-
}
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
// createAnthropicMappingProposer
|
|
178
|
-
// ---------------------------------------------------------------------------
|
|
179
|
-
/**
|
|
180
|
-
* Create a MappingProposer backed by Anthropic's API using forced tool-use.
|
|
181
|
-
*
|
|
182
|
-
* ADR 0003 §4: returns PROPOSALS only — they flow through the existing
|
|
183
|
-
* review/auto-accept machinery before counting as mappings.
|
|
184
|
-
*
|
|
185
|
-
* Tool output is validated strictly: malformed items (missing required fields,
|
|
186
|
-
* out-of-range confidence, no target and no rule) are filtered out rather than
|
|
187
|
-
* silently accepted.
|
|
188
|
-
*/
|
|
189
|
-
export function createAnthropicMappingProposer(opts = {}) {
|
|
190
|
-
const model = opts.model ?? DEFAULT_MODEL;
|
|
191
|
-
return {
|
|
192
|
-
name: `anthropic-mapping-proposer:${model}`,
|
|
193
|
-
async propose(question, context) {
|
|
194
|
-
const client = await resolveClient(opts);
|
|
195
|
-
// Build context summary for the prompt
|
|
196
|
-
const claimsContext = buildClaimsContext(context.bundle);
|
|
197
|
-
const rulesContext = buildRulesContext(context.rules);
|
|
198
|
-
const systemPrompt = [
|
|
199
|
-
"You are a mapping proposer for the Kontour trust ledger.",
|
|
200
|
-
"Your role is to PROPOSE (not decide) how a natural-language question maps to a registered canonical claim or derivation rule.",
|
|
201
|
-
"Every proposal you return will be reviewed by a human or auto-accept policy before it counts.",
|
|
202
|
-
"Do not make up claim targets that are not in the registered list below.",
|
|
203
|
-
"Return only proposals you genuinely believe are plausible mappings — with honest confidence scores.",
|
|
204
|
-
"",
|
|
205
|
-
claimsContext,
|
|
206
|
-
rulesContext,
|
|
207
|
-
]
|
|
208
|
-
.filter(Boolean)
|
|
209
|
-
.join("\n");
|
|
210
|
-
const userMessage = `Question to map: "${question}"`;
|
|
211
|
-
const message = await client.create({
|
|
212
|
-
model,
|
|
213
|
-
max_tokens: 1024,
|
|
214
|
-
messages: [{ role: "user", content: `${systemPrompt}\n\n${userMessage}` }],
|
|
215
|
-
tools: [MAPPING_PROPOSAL_TOOL],
|
|
216
|
-
tool_choice: { type: "tool", name: "submit_mapping_proposals" },
|
|
217
|
-
});
|
|
218
|
-
const input = extractToolUseInput(message, "submit_mapping_proposals");
|
|
219
|
-
if (!isRecord(input))
|
|
220
|
-
return [];
|
|
221
|
-
const rawProposals = input["proposals"];
|
|
222
|
-
if (!isArray(rawProposals))
|
|
223
|
-
return [];
|
|
224
|
-
const proposedAt = new Date().toISOString();
|
|
225
|
-
const results = [];
|
|
226
|
-
for (const item of rawProposals) {
|
|
227
|
-
const proposal = parseMappingProposalItem(item, question, model, proposedAt);
|
|
228
|
-
if (proposal)
|
|
229
|
-
results.push(proposal);
|
|
230
|
-
}
|
|
231
|
-
return results;
|
|
232
|
-
},
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
function parseMappingProposalItem(item, question, proposedBy, proposedAt) {
|
|
236
|
-
if (!isRecord(item))
|
|
237
|
-
return undefined;
|
|
238
|
-
const raw = item;
|
|
239
|
-
const confidence = numberInRange(raw.confidence, 0, 1);
|
|
240
|
-
const rationale = stringOrUndefined(raw.rationale);
|
|
241
|
-
// Both required fields must be present
|
|
242
|
-
if (confidence === undefined || rationale === undefined)
|
|
243
|
-
return undefined;
|
|
244
|
-
const subjectType = stringOrUndefined(raw.proposedTargetSubjectType);
|
|
245
|
-
const subjectId = stringOrUndefined(raw.proposedTargetSubjectId);
|
|
246
|
-
const fieldOrBehavior = stringOrUndefined(raw.proposedTargetFieldOrBehavior);
|
|
247
|
-
const ruleId = stringOrUndefined(raw.proposedRuleId);
|
|
248
|
-
const excerpt = stringOrUndefined(raw.excerpt);
|
|
249
|
-
// Exactly one of (target triple) or ruleId must be present
|
|
250
|
-
const hasTarget = subjectType !== undefined && subjectId !== undefined && fieldOrBehavior !== undefined;
|
|
251
|
-
const hasRule = ruleId !== undefined;
|
|
252
|
-
if (!hasTarget && !hasRule)
|
|
253
|
-
return undefined;
|
|
254
|
-
const proposedTarget = hasTarget
|
|
255
|
-
? { subjectType: subjectType, subjectId: subjectId, fieldOrBehavior: fieldOrBehavior }
|
|
256
|
-
: undefined;
|
|
257
|
-
const id = `proposal.anthropic.${encodeId(question)}.${Date.now()}`;
|
|
258
|
-
return {
|
|
259
|
-
id,
|
|
260
|
-
question,
|
|
261
|
-
proposedTarget,
|
|
262
|
-
proposedRuleId: hasRule ? ruleId : undefined,
|
|
263
|
-
confidence,
|
|
264
|
-
rationale,
|
|
265
|
-
excerpt,
|
|
266
|
-
proposedBy,
|
|
267
|
-
proposedAt,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
// createAnthropicUtteranceExtractor
|
|
272
|
-
// ---------------------------------------------------------------------------
|
|
273
|
-
/**
|
|
274
|
-
* Create a UtteranceClaimExtractor backed by Anthropic's API using forced tool-use.
|
|
275
|
-
*
|
|
276
|
-
* ADR 0003 §4: returns EXTRACTED STATEMENTS only — they carry full provenance
|
|
277
|
-
* (excerpt, span, extractor name, confidence) and flow through the Inquiry
|
|
278
|
-
* pipeline. They are never treated as authoritative.
|
|
279
|
-
*
|
|
280
|
-
* Malformed tool output is rejected/filtered — items missing required fields
|
|
281
|
-
* (subjectId, fieldOrBehavior, excerpt, confidence) are dropped.
|
|
282
|
-
*/
|
|
283
|
-
export function createAnthropicUtteranceExtractor(opts = {}) {
|
|
284
|
-
const model = opts.model ?? DEFAULT_MODEL;
|
|
285
|
-
return {
|
|
286
|
-
name: `anthropic-utterance-extractor:${model}`,
|
|
287
|
-
async extract(utterance) {
|
|
288
|
-
const client = await resolveClient(opts);
|
|
289
|
-
const systemPrompt = [
|
|
290
|
-
"You are a factual statement extractor for the Kontour trust ledger.",
|
|
291
|
-
"Your role is to identify every factual claim in the agent utterance and extract it with full provenance.",
|
|
292
|
-
"Each extracted statement will be reviewed for trust coverage — you are NOT deciding truth, only extracting for review.",
|
|
293
|
-
"Extract only statements that assert factual properties of named entities.",
|
|
294
|
-
"Skip opinions, predictions, and procedural descriptions.",
|
|
295
|
-
"Provide honest confidence scores — low confidence for ambiguous phrasing.",
|
|
296
|
-
"Include the exact verbatim excerpt and 0-indexed character span offsets.",
|
|
297
|
-
].join("\n");
|
|
298
|
-
const userMessage = `Extract factual statements from this agent utterance:\n\n"${utterance}"`;
|
|
299
|
-
const message = await client.create({
|
|
300
|
-
model,
|
|
301
|
-
max_tokens: 2048,
|
|
302
|
-
messages: [{ role: "user", content: `${systemPrompt}\n\n${userMessage}` }],
|
|
303
|
-
tools: [UTTERANCE_EXTRACTION_TOOL],
|
|
304
|
-
tool_choice: { type: "tool", name: "submit_extracted_statements" },
|
|
305
|
-
});
|
|
306
|
-
const input = extractToolUseInput(message, "submit_extracted_statements");
|
|
307
|
-
if (!isRecord(input))
|
|
308
|
-
return [];
|
|
309
|
-
const rawStatements = input["statements"];
|
|
310
|
-
if (!isArray(rawStatements))
|
|
311
|
-
return [];
|
|
312
|
-
const results = [];
|
|
313
|
-
for (const item of rawStatements) {
|
|
314
|
-
const statement = parseExtractedStatementItem(item, utterance);
|
|
315
|
-
if (statement)
|
|
316
|
-
results.push(statement);
|
|
317
|
-
}
|
|
318
|
-
return results;
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
function parseExtractedStatementItem(item, utterance) {
|
|
323
|
-
if (!isRecord(item))
|
|
324
|
-
return undefined;
|
|
325
|
-
const raw = item;
|
|
326
|
-
const subjectId = stringOrUndefined(raw.subjectId);
|
|
327
|
-
const fieldOrBehavior = stringOrUndefined(raw.fieldOrBehavior);
|
|
328
|
-
const excerpt = stringOrUndefined(raw.excerpt);
|
|
329
|
-
const confidence = numberInRange(raw.confidence, 0, 1);
|
|
330
|
-
// All required fields must be present
|
|
331
|
-
if (!subjectId || !fieldOrBehavior || !excerpt || confidence === undefined)
|
|
332
|
-
return undefined;
|
|
333
|
-
const subjectType = stringOrUndefined(raw.subjectType) ?? "unknown";
|
|
334
|
-
// Validate span if provided — both start and end must be valid integers
|
|
335
|
-
// within the utterance length
|
|
336
|
-
let span;
|
|
337
|
-
if (typeof raw.spanStart === "number" && typeof raw.spanEnd === "number") {
|
|
338
|
-
const start = Math.trunc(raw.spanStart);
|
|
339
|
-
const end = Math.trunc(raw.spanEnd);
|
|
340
|
-
if (Number.isFinite(start) &&
|
|
341
|
-
Number.isFinite(end) &&
|
|
342
|
-
start >= 0 &&
|
|
343
|
-
end > start &&
|
|
344
|
-
end <= utterance.length) {
|
|
345
|
-
span = { start, end };
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return {
|
|
349
|
-
target: { subjectType, subjectId, fieldOrBehavior },
|
|
350
|
-
value: raw.value ?? undefined,
|
|
351
|
-
excerpt,
|
|
352
|
-
span,
|
|
353
|
-
confidence,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
// ---------------------------------------------------------------------------
|
|
357
|
-
// Prompt context builders
|
|
358
|
-
// ---------------------------------------------------------------------------
|
|
359
|
-
function buildClaimsContext(bundle) {
|
|
360
|
-
if (!bundle || bundle.claims.length === 0)
|
|
361
|
-
return "";
|
|
362
|
-
const lines = [
|
|
363
|
-
"Registered canonical claim targets (use ONLY these as proposedTarget):",
|
|
364
|
-
...bundle.claims.map((c) => ` - subjectType="${c.subjectType}" subjectId="${c.subjectId}" fieldOrBehavior="${c.fieldOrBehavior}"`),
|
|
365
|
-
];
|
|
366
|
-
return lines.join("\n");
|
|
367
|
-
}
|
|
368
|
-
function buildRulesContext(rules) {
|
|
369
|
-
if (!rules || rules.length === 0)
|
|
370
|
-
return "";
|
|
371
|
-
const lines = [
|
|
372
|
-
"Registered derivation rules (use rule id as proposedRuleId):",
|
|
373
|
-
...rules.map((r) => ` - id="${r.id}" name="${r.name}"`),
|
|
374
|
-
];
|
|
375
|
-
return lines.join("\n");
|
|
376
|
-
}
|
|
377
|
-
function encodeId(value) {
|
|
378
|
-
return value
|
|
379
|
-
.toLowerCase()
|
|
380
|
-
.replace(/\s+/g, "-")
|
|
381
|
-
.replace(/[^a-z0-9\-]/g, "")
|
|
382
|
-
.slice(0, 40);
|
|
383
|
-
}
|