@kontourai/survey 1.4.0 → 1.5.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/dist/src/agent-utterance.d.ts +25 -0
- package/dist/src/agent-utterance.js +206 -94
- package/dist/src/console/review-console-server.js +3 -7
- package/dist/src/field-observation.d.ts +2 -16
- package/dist/src/field-observation.js +2 -14
- package/dist/src/inquiry-mapping.d.ts +2 -46
- package/dist/src/inquiry-mapping.js +36 -38
- package/dist/src/mcp/review-mcp.js +12 -12
- package/dist/src/observation-helper.d.ts +47 -1
- package/dist/src/observation-helper.js +41 -2
- package/dist/src/producer-discipline.d.ts +40 -0
- package/dist/src/producer-discipline.js +13 -0
- package/dist/src/producer-profile.d.ts +134 -0
- package/dist/src/producer-profile.js +124 -0
- package/dist/src/raw-source.d.ts +18 -0
- package/dist/src/raw-source.js +28 -13
- package/dist/src/repeated-observation.d.ts +2 -16
- package/dist/src/repeated-observation.js +2 -14
- package/dist/src/review-workbench/server-review-session.d.ts +14 -1
- package/dist/src/review-workbench/server-review-session.js +16 -0
- package/dist/src/schema-mapping.js +30 -36
- package/dist/src/source-of-authority-observation.js +6 -9
- package/dist/src/to-surface.js +6 -9
- package/package.json +5 -2
|
@@ -96,6 +96,30 @@ export interface UtteranceStatementRecords {
|
|
|
96
96
|
candidate: Candidate;
|
|
97
97
|
candidateSet: CandidateSet;
|
|
98
98
|
}
|
|
99
|
+
interface BuildUtteranceRecordsParams {
|
|
100
|
+
sourceId: string;
|
|
101
|
+
utterance: string;
|
|
102
|
+
extracted: ExtractedStatement[];
|
|
103
|
+
extractorName: string;
|
|
104
|
+
observedAt: string;
|
|
105
|
+
}
|
|
106
|
+
interface UtteranceRecordsResult {
|
|
107
|
+
records: UtteranceStatementRecords[];
|
|
108
|
+
extractions: Extraction[];
|
|
109
|
+
candidateSets: CandidateSet[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build the full set of Survey records for every extracted statement in one
|
|
113
|
+
* utterance: per-statement Extractions/Candidates plus per-target grouped
|
|
114
|
+
* Candidate Sets. This is the shared orchestrator both `utteranceToSurveyInput`
|
|
115
|
+
* and `surveyAgentUtterance` call, so both callers derive these records
|
|
116
|
+
* identically — Slice 1's "single derivation path" invariant, preserved.
|
|
117
|
+
*
|
|
118
|
+
* `records[idx]` corresponds to `extracted[idx]` for every idx — `items` and
|
|
119
|
+
* `records` are both built via `.map` over the same `extracted[]` array in
|
|
120
|
+
* the same order; only `candidateSets` is deduped/grouped by target.
|
|
121
|
+
*/
|
|
122
|
+
export declare function buildUtteranceRecords(params: BuildUtteranceRecordsParams): UtteranceRecordsResult;
|
|
99
123
|
/**
|
|
100
124
|
* Project an agent utterance and its extracted statements into the standard
|
|
101
125
|
* SurveyInput shape so they can flow into buildSurveyTrustBundle.
|
|
@@ -164,3 +188,4 @@ export declare function surveyAgentUtterance(utterance: string, extractor: Utter
|
|
|
164
188
|
* alone in this reference implementation.
|
|
165
189
|
*/
|
|
166
190
|
export declare const referenceUtteranceExtractor: UtteranceClaimExtractor;
|
|
191
|
+
export {};
|
|
@@ -18,6 +18,177 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { resolveInquiry } from "@kontourai/surface";
|
|
20
20
|
import { lookupMapping, resolveQuestion } from "./inquiry-mapping.js";
|
|
21
|
+
import { projectProposalsToCandidateSet } from "./producer-profile.js";
|
|
22
|
+
/**
|
|
23
|
+
* The Candidate Conflict comparison key for an utterance-proposed value.
|
|
24
|
+
*
|
|
25
|
+
* Neither extractor (the reference extractor below, nor the Anthropic-backed
|
|
26
|
+
* one in `./anthropic.js`) normalizes `value` before it reaches
|
|
27
|
+
* `ExtractedStatement` — case and internal formatting are preserved
|
|
28
|
+
* verbatim. String values are the only case where "representation noise"
|
|
29
|
+
* (leading/trailing whitespace from excerpt boundaries, incidental case
|
|
30
|
+
* differences like "Healthy" vs "healthy") is plausible given the two
|
|
31
|
+
* extractors' actual output, so this key trims + lowercases STRING values
|
|
32
|
+
* in the COMPARISON KEY ONLY — the stored `Candidate.value`/`Extraction.value`
|
|
33
|
+
* stay byte-for-byte verbatim; this function only feeds `equivalenceKey`,
|
|
34
|
+
* never `value`. Non-string values (number, boolean, null — the other types
|
|
35
|
+
* the Anthropic tool schema permits) compare via exact canonical
|
|
36
|
+
* `JSON.stringify`, so there is no cross-type coercion that could silently
|
|
37
|
+
* equate e.g. "5" and 5, or lose a genuine numeric disagreement (5 vs 6 is
|
|
38
|
+
* never noise). This mirrors the Producer Profile core's established
|
|
39
|
+
* pattern: each profile decides its own narrow equivalence definition
|
|
40
|
+
* (see `./producer-profile.js`); the core itself does not own this decision.
|
|
41
|
+
*/
|
|
42
|
+
function utteranceEquivalenceKey(value) {
|
|
43
|
+
const normalized = value ?? null;
|
|
44
|
+
if (typeof normalized === "string") {
|
|
45
|
+
return `str:${normalized.trim().toLowerCase()}`;
|
|
46
|
+
}
|
|
47
|
+
return `json:${JSON.stringify(normalized)}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the Extraction and CandidateSetProposal for a single extracted
|
|
51
|
+
* statement. This centralizes the Source Locator rule (span-first,
|
|
52
|
+
* excerpt-fallback — the single locator rule this module guarantees) and
|
|
53
|
+
* hands the resulting proposal off to `groupUtteranceExtractionsByTarget`
|
|
54
|
+
* for per-target projection through the Producer Profile core.
|
|
55
|
+
*/
|
|
56
|
+
function buildUtteranceExtraction(params) {
|
|
57
|
+
const { sourceId, idx, statement, utterance, extractorName, observedAt } = params;
|
|
58
|
+
const statementId = `${sourceId}.statement.${idx}`;
|
|
59
|
+
const extractionId = `${statementId}.extraction`;
|
|
60
|
+
const candidateId = `${statementId}.candidate`;
|
|
61
|
+
// Compute locator — required for non-manual-entry sources
|
|
62
|
+
// (assertProducerDiscipline throws without it). Source Locator rule:
|
|
63
|
+
// span-first, excerpt-fallback — UNCHANGED from Slice 1.
|
|
64
|
+
const locator = spanToLocator(statement.span) ?? excerptLocator(utterance, statement.excerpt);
|
|
65
|
+
const extraction = {
|
|
66
|
+
id: extractionId,
|
|
67
|
+
sourceId,
|
|
68
|
+
target: canonicalTargetKey(statement.target),
|
|
69
|
+
value: statement.value ?? null,
|
|
70
|
+
confidence: statement.confidence,
|
|
71
|
+
locator,
|
|
72
|
+
excerpt: statement.excerpt,
|
|
73
|
+
extractor: extractorName,
|
|
74
|
+
extractedAt: observedAt,
|
|
75
|
+
metadata: {
|
|
76
|
+
agentUtterance: {
|
|
77
|
+
span: statement.span,
|
|
78
|
+
excerpt: statement.excerpt,
|
|
79
|
+
extractorName,
|
|
80
|
+
confidence: statement.confidence,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const proposal = {
|
|
85
|
+
candidateId,
|
|
86
|
+
extractionId,
|
|
87
|
+
value: statement.value ?? null,
|
|
88
|
+
confidence: statement.confidence,
|
|
89
|
+
equivalenceKey: utteranceEquivalenceKey(statement.value),
|
|
90
|
+
metadata: {
|
|
91
|
+
span: statement.span,
|
|
92
|
+
excerpt: statement.excerpt,
|
|
93
|
+
extractorName,
|
|
94
|
+
confidence: statement.confidence,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
return { extraction, proposal };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Group extraction/proposal pairs by canonical target and project each
|
|
101
|
+
* group through the Producer Profile core's `projectProposalsToCandidateSet`
|
|
102
|
+
* — one Candidate Set per target, carrying every statement's Candidate for
|
|
103
|
+
* that target. Status is `"conflict"` when the group's statements disagree
|
|
104
|
+
* under `utteranceEquivalenceKey`, `"needs-review"` otherwise (including the
|
|
105
|
+
* common single-statement case, which reproduces Slice 1's exact prior
|
|
106
|
+
* per-statement behavior).
|
|
107
|
+
*
|
|
108
|
+
* `Map` preserves insertion order, so the returned groups (and therefore the
|
|
109
|
+
* `candidateSets` array `buildUtteranceRecords` derives from them) are in
|
|
110
|
+
* deterministic first-occurrence-of-target order across the utterance's
|
|
111
|
+
* statements.
|
|
112
|
+
*/
|
|
113
|
+
function groupUtteranceExtractionsByTarget(sourceId, items) {
|
|
114
|
+
const order = [];
|
|
115
|
+
const byTarget = new Map();
|
|
116
|
+
for (const item of items) {
|
|
117
|
+
const key = canonicalTargetKey(item.statement.target);
|
|
118
|
+
if (!byTarget.has(key)) {
|
|
119
|
+
byTarget.set(key, []);
|
|
120
|
+
order.push(key);
|
|
121
|
+
}
|
|
122
|
+
byTarget.get(key).push(item);
|
|
123
|
+
}
|
|
124
|
+
const groups = new Map();
|
|
125
|
+
for (const targetKey of order) {
|
|
126
|
+
const groupItems = byTarget.get(targetKey);
|
|
127
|
+
const first = groupItems[0].statement.target;
|
|
128
|
+
const proposals = groupItems.map((i) => i.proposal);
|
|
129
|
+
const { candidateSet, candidates } = projectProposalsToCandidateSet(targetKey, proposals, {
|
|
130
|
+
candidateSetId: `${sourceId}.target.${targetKey}.candidate-set`,
|
|
131
|
+
candidateSetMetadata: {
|
|
132
|
+
agentUtterance: {
|
|
133
|
+
target: {
|
|
134
|
+
subjectType: first.subjectType,
|
|
135
|
+
subjectId: first.subjectId,
|
|
136
|
+
fieldOrBehavior: first.fieldOrBehavior,
|
|
137
|
+
},
|
|
138
|
+
statementCount: proposals.length,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
candidateSetRationale: (status, groupProposals) => status === "conflict"
|
|
142
|
+
? `${groupProposals.length} statement(s) disagree for ${targetKey}: ${[...new Set(groupProposals.map((p) => p.equivalenceKey))].join(", ")}`
|
|
143
|
+
: `${groupProposals.length} statement(s) agree for ${targetKey}.`,
|
|
144
|
+
});
|
|
145
|
+
// Mirrors schema-mapping's post-core convention: a winner only exists
|
|
146
|
+
// when the group agrees; a conflicting group has no selected candidate
|
|
147
|
+
// yet (nothing to select — that's the point of a Candidate Conflict,
|
|
148
|
+
// CONTEXT.md's "Candidate Conflict" entry). For a single-statement group
|
|
149
|
+
// this reproduces Slice 1's exact old behavior (selectedCandidateId ===
|
|
150
|
+
// the one candidate's id).
|
|
151
|
+
candidateSet.selectedCandidateId = candidateSet.status !== "conflict" ? candidates[0]?.id : undefined;
|
|
152
|
+
groups.set(targetKey, { targetKey, candidateSet, candidates });
|
|
153
|
+
}
|
|
154
|
+
return groups;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Build the full set of Survey records for every extracted statement in one
|
|
158
|
+
* utterance: per-statement Extractions/Candidates plus per-target grouped
|
|
159
|
+
* Candidate Sets. This is the shared orchestrator both `utteranceToSurveyInput`
|
|
160
|
+
* and `surveyAgentUtterance` call, so both callers derive these records
|
|
161
|
+
* identically — Slice 1's "single derivation path" invariant, preserved.
|
|
162
|
+
*
|
|
163
|
+
* `records[idx]` corresponds to `extracted[idx]` for every idx — `items` and
|
|
164
|
+
* `records` are both built via `.map` over the same `extracted[]` array in
|
|
165
|
+
* the same order; only `candidateSets` is deduped/grouped by target.
|
|
166
|
+
*/
|
|
167
|
+
export function buildUtteranceRecords(params) {
|
|
168
|
+
const { sourceId, utterance, extracted, extractorName, observedAt } = params;
|
|
169
|
+
const items = extracted.map((statement, idx) => {
|
|
170
|
+
const { extraction, proposal } = buildUtteranceExtraction({
|
|
171
|
+
sourceId,
|
|
172
|
+
idx,
|
|
173
|
+
statement,
|
|
174
|
+
utterance,
|
|
175
|
+
extractorName,
|
|
176
|
+
observedAt,
|
|
177
|
+
});
|
|
178
|
+
return { statement, extraction, proposal };
|
|
179
|
+
});
|
|
180
|
+
const groups = groupUtteranceExtractionsByTarget(sourceId, items);
|
|
181
|
+
const records = items.map((item) => {
|
|
182
|
+
const group = groups.get(canonicalTargetKey(item.statement.target));
|
|
183
|
+
const candidate = group.candidates.find((c) => c.id === item.proposal.candidateId);
|
|
184
|
+
return { extraction: item.extraction, candidate, candidateSet: group.candidateSet };
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
records,
|
|
188
|
+
extractions: items.map((i) => i.extraction),
|
|
189
|
+
candidateSets: [...groups.values()].map((g) => g.candidateSet),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
21
192
|
// ---------------------------------------------------------------------------
|
|
22
193
|
// SurveyInput projection
|
|
23
194
|
// ---------------------------------------------------------------------------
|
|
@@ -60,80 +231,35 @@ export function utteranceToSurveyInput(utterance, extracted, context) {
|
|
|
60
231
|
inlineText: utterance,
|
|
61
232
|
metadata: { agentId },
|
|
62
233
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
234
|
+
// Batched, per-target-grouped provenance construction (Producer Profile
|
|
235
|
+
// core) — replaces the old per-statement builder call. Claims below stay
|
|
236
|
+
// one-per-statement; `record.candidateSet.id`/`record.candidate.id` may be
|
|
237
|
+
// shared across several claims when statements share a target (legal).
|
|
238
|
+
const { records, extractions, candidateSets } = buildUtteranceRecords({
|
|
239
|
+
sourceId,
|
|
240
|
+
utterance,
|
|
241
|
+
extracted,
|
|
242
|
+
extractorName,
|
|
243
|
+
observedAt,
|
|
244
|
+
});
|
|
245
|
+
const claims = extracted.map((statement, idx) => {
|
|
68
246
|
const statementId = `${sourceId}.statement.${idx}`;
|
|
69
|
-
const extractionId = `${statementId}.extraction`;
|
|
70
|
-
const candidateId = `${statementId}.candidate`;
|
|
71
|
-
const candidateSetId = `${statementId}.candidate-set`;
|
|
72
247
|
const claimId = `${statementId}.claim`;
|
|
73
|
-
|
|
74
|
-
// (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
sourceId,
|
|
79
|
-
target: canonicalTargetKey(statement.target),
|
|
80
|
-
value: statement.value ?? null,
|
|
81
|
-
confidence: statement.confidence,
|
|
82
|
-
locator,
|
|
83
|
-
excerpt: statement.excerpt,
|
|
84
|
-
extractor: extractorName,
|
|
85
|
-
extractedAt: observedAt,
|
|
86
|
-
metadata: {
|
|
87
|
-
agentUtterance: {
|
|
88
|
-
span: statement.span,
|
|
89
|
-
excerpt: statement.excerpt,
|
|
90
|
-
extractorName,
|
|
91
|
-
confidence: statement.confidence,
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
const candidate = {
|
|
96
|
-
id: candidateId,
|
|
97
|
-
extractionId,
|
|
98
|
-
value: statement.value ?? null,
|
|
99
|
-
confidence: statement.confidence,
|
|
100
|
-
metadata: {
|
|
101
|
-
agentUtterance: {
|
|
102
|
-
span: statement.span,
|
|
103
|
-
excerpt: statement.excerpt,
|
|
104
|
-
extractorName,
|
|
105
|
-
confidence: statement.confidence,
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
// needs-review status → statusFor returns "proposed" (no review outcome)
|
|
110
|
-
const candidateSet = {
|
|
111
|
-
id: candidateSetId,
|
|
112
|
-
target: canonicalTargetKey(statement.target),
|
|
113
|
-
candidates: [candidate],
|
|
114
|
-
selectedCandidateId: candidateId,
|
|
115
|
-
status: "needs-review",
|
|
116
|
-
metadata: {
|
|
117
|
-
agentUtterance: {
|
|
118
|
-
subjectType: statement.target.subjectType,
|
|
119
|
-
subjectId: statement.target.subjectId,
|
|
120
|
-
fieldOrBehavior: statement.target.fieldOrBehavior,
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
// Unreviewed: status is omitted so statusFor() computes "proposed"
|
|
125
|
-
// assertProducerDiscipline: no verified/assumed without review → compliant
|
|
126
|
-
const claimTarget = {
|
|
248
|
+
const record = records[idx];
|
|
249
|
+
// Unreviewed: status is omitted so statusFor() computes "proposed" (or
|
|
250
|
+
// "disputed" for a claim whose shared candidateSet.status is "conflict").
|
|
251
|
+
// assertProducerDiscipline: no verified/assumed without review → compliant.
|
|
252
|
+
return {
|
|
127
253
|
id: claimId,
|
|
128
|
-
candidateSetId,
|
|
129
|
-
candidateId,
|
|
254
|
+
candidateSetId: record.candidateSet.id,
|
|
255
|
+
candidateId: record.candidate.id,
|
|
130
256
|
subjectType: statement.target.subjectType,
|
|
131
257
|
subjectId: statement.target.subjectId,
|
|
132
258
|
facet: "agent-utterance.profile",
|
|
133
259
|
claimType: "agent-extraction",
|
|
134
260
|
fieldOrBehavior: statement.target.fieldOrBehavior,
|
|
135
261
|
value: statement.value,
|
|
136
|
-
// status intentionally omitted → computed
|
|
262
|
+
// status intentionally omitted → computed by statusFor
|
|
137
263
|
impactLevel: "low",
|
|
138
264
|
collectedBy: extractorName,
|
|
139
265
|
metadata: {
|
|
@@ -144,15 +270,12 @@ export function utteranceToSurveyInput(utterance, extracted, context) {
|
|
|
144
270
|
excerpt: statement.excerpt,
|
|
145
271
|
span: statement.span,
|
|
146
272
|
confidence: statement.confidence,
|
|
147
|
-
locator,
|
|
273
|
+
locator: record.extraction.locator,
|
|
148
274
|
},
|
|
149
275
|
},
|
|
150
276
|
},
|
|
151
277
|
};
|
|
152
|
-
|
|
153
|
-
candidateSets.push(candidateSet);
|
|
154
|
-
claims.push(claimTarget);
|
|
155
|
-
}
|
|
278
|
+
});
|
|
156
279
|
return {
|
|
157
280
|
source,
|
|
158
281
|
generatedAt: observedAt,
|
|
@@ -196,31 +319,22 @@ export async function surveyAgentUtterance(utterance, extractor, context) {
|
|
|
196
319
|
};
|
|
197
320
|
// Step 2: Extract statements
|
|
198
321
|
const extracted = await Promise.resolve(extractor.extract(utterance));
|
|
322
|
+
// Batched, grouped provenance construction — kept for provenance/doc-comment
|
|
323
|
+
// fidelity across the whole utterance (grouping needs every statement of a
|
|
324
|
+
// target's group present at once; a per-statement call cannot compute it).
|
|
325
|
+
// Still not surfaced on UtteranceStatement/UtteranceTrustReport this slice
|
|
326
|
+
// (unchanged from Slice 1's own scoping note) — wiring is left for a future
|
|
327
|
+
// slice, same as before.
|
|
328
|
+
buildUtteranceRecords({
|
|
329
|
+
sourceId,
|
|
330
|
+
utterance,
|
|
331
|
+
extracted,
|
|
332
|
+
extractorName: extractor.name,
|
|
333
|
+
observedAt,
|
|
334
|
+
});
|
|
199
335
|
// Step 3 & 4: Resolve each statement and build the report
|
|
200
336
|
const statements = [];
|
|
201
337
|
for (const statement of extracted) {
|
|
202
|
-
const statementId = `${sourceId}.statement.${statements.length}`;
|
|
203
|
-
// Build Survey records for provenance
|
|
204
|
-
const extractionId = `${statementId}.extraction`;
|
|
205
|
-
const extraction = {
|
|
206
|
-
id: extractionId,
|
|
207
|
-
sourceId,
|
|
208
|
-
target: canonicalTargetKey(statement.target),
|
|
209
|
-
value: statement.value ?? null,
|
|
210
|
-
confidence: statement.confidence,
|
|
211
|
-
locator: statement.span ? `text-span:${statement.span.start}-${statement.span.end}` : undefined,
|
|
212
|
-
excerpt: statement.excerpt,
|
|
213
|
-
extractor: extractor.name,
|
|
214
|
-
extractedAt: observedAt,
|
|
215
|
-
metadata: {
|
|
216
|
-
agentUtterance: {
|
|
217
|
-
span: statement.span,
|
|
218
|
-
excerpt: statement.excerpt,
|
|
219
|
-
extractorName: extractor.name,
|
|
220
|
-
confidence: statement.confidence,
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
338
|
// Resolve the claim
|
|
225
339
|
let inquiryRecord;
|
|
226
340
|
if (mappings && mappings.length > 0) {
|
|
@@ -253,8 +367,6 @@ export async function surveyAgentUtterance(utterance, extractor, context) {
|
|
|
253
367
|
inquiryRecord,
|
|
254
368
|
badge,
|
|
255
369
|
});
|
|
256
|
-
// Suppress unused-variable warning for extraction
|
|
257
|
-
void extraction;
|
|
258
370
|
}
|
|
259
371
|
return { source, statements };
|
|
260
372
|
}
|
|
@@ -15,8 +15,8 @@ import { watch } from "node:fs";
|
|
|
15
15
|
import { readFile as readFileAsync, writeFile as writeFileAsync, rename as renameAsync } from "node:fs/promises";
|
|
16
16
|
import { resolve, join, dirname, extname, normalize, sep } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
import {
|
|
19
|
-
import { createServerReviewSessionRecord, deriveServerReviewSessionApplyResult, } from "../review-workbench/server-review-session.js";
|
|
18
|
+
import { defaultReviewSessionName, } from "../review-workbench/review-workbench.js";
|
|
19
|
+
import { createServerReviewSessionRecord, currentSessionState, deriveServerReviewSessionApplyResult, } from "../review-workbench/server-review-session.js";
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// Constants
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
@@ -63,10 +63,6 @@ async function writeSessionAtomic(path, content) {
|
|
|
63
63
|
await writeFileAsync(tmp, JSON.stringify(content, null, 2), "utf8");
|
|
64
64
|
await renameAsync(tmp, path);
|
|
65
65
|
}
|
|
66
|
-
function currentState(content) {
|
|
67
|
-
const { snapshot, events } = content;
|
|
68
|
-
return events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot;
|
|
69
|
-
}
|
|
70
66
|
// ---------------------------------------------------------------------------
|
|
71
67
|
// SSE broadcaster
|
|
72
68
|
// ---------------------------------------------------------------------------
|
|
@@ -496,7 +492,7 @@ export async function startReviewConsoleServer(options) {
|
|
|
496
492
|
session: content.session,
|
|
497
493
|
snapshot: content.snapshot,
|
|
498
494
|
events: content.events,
|
|
499
|
-
state:
|
|
495
|
+
state: currentSessionState(content.snapshot, content.events),
|
|
500
496
|
});
|
|
501
497
|
return;
|
|
502
498
|
}
|
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
import type { SurveyObservationInput } from "./builder.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
field: string;
|
|
5
|
-
value: TValue;
|
|
6
|
-
rawSource: SurveyObservationInput["rawSource"];
|
|
7
|
-
extraction: Omit<SurveyObservationInput["extraction"], "target" | "value" | "excerpt"> & {
|
|
8
|
-
target?: string;
|
|
9
|
-
excerpt?: string | null;
|
|
10
|
-
};
|
|
11
|
-
reviewOutcome?: SurveyObservationInput["reviewOutcome"];
|
|
12
|
-
claim: Omit<SurveyObservationInput["claim"], "fieldOrBehavior" | "value"> & {
|
|
13
|
-
fieldOrBehavior?: string;
|
|
14
|
-
};
|
|
15
|
-
candidate?: SurveyObservationInput["candidate"];
|
|
16
|
-
candidateSet?: SurveyObservationInput["candidateSet"];
|
|
2
|
+
import { type ObservationAuthoringInput } from "./observation-helper.js";
|
|
3
|
+
export interface FieldObservationInput<TValue> extends ObservationAuthoringInput<TValue> {
|
|
17
4
|
representation?: "scalar";
|
|
18
|
-
metadata?: Record<string, unknown>;
|
|
19
5
|
}
|
|
20
6
|
export declare function fieldObservation<TValue>(input: FieldObservationInput<TValue>): SurveyObservationInput;
|
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildFieldObservation } from "./observation-helper.js";
|
|
2
2
|
export function fieldObservation(input) {
|
|
3
|
-
|
|
4
|
-
return buildObservation({
|
|
5
|
-
...input,
|
|
6
|
-
surveyMetadata: {
|
|
7
|
-
field: { representation },
|
|
8
|
-
},
|
|
9
|
-
defaultExcerpt: `${input.field}: ${valueSummary(input.value)}`,
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
function valueSummary(value) {
|
|
13
|
-
if (value === null || value === undefined)
|
|
14
|
-
return "<empty>";
|
|
15
|
-
return String(value);
|
|
3
|
+
return buildFieldObservation(input);
|
|
16
4
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import type { DerivationRule, InquiryRecord, TrustBundle } from "@kontourai/surface";
|
|
21
21
|
import type { CanonicalClaimTarget } from "@kontourai/surface";
|
|
22
22
|
import type { Candidate, CandidateSet, ReviewOutcome } from "./types.js";
|
|
23
|
+
import type { ReviewItem } from "./review-resource.js";
|
|
23
24
|
/**
|
|
24
25
|
* A single machine- or human-generated suggestion that a natural-language
|
|
25
26
|
* question maps to a canonical claim target or a named derivation rule.
|
|
@@ -197,52 +198,7 @@ export declare function resolveQuestion(bundle: TrustBundle, question: string, o
|
|
|
197
198
|
export declare function buildMappingReviewItems(candidateSets: Array<{
|
|
198
199
|
candidateSet: CandidateSet;
|
|
199
200
|
candidates: Candidate[];
|
|
200
|
-
}>):
|
|
201
|
-
apiVersion: "survey.kontourai.io/v1alpha1";
|
|
202
|
-
kind: "ReviewItem";
|
|
203
|
-
metadata: {
|
|
204
|
-
name: string;
|
|
205
|
-
labels?: Record<string, string>;
|
|
206
|
-
};
|
|
207
|
-
spec: {
|
|
208
|
-
target: string;
|
|
209
|
-
candidates: Array<{
|
|
210
|
-
id: string;
|
|
211
|
-
role: "proposed";
|
|
212
|
-
value: unknown;
|
|
213
|
-
confidence?: number;
|
|
214
|
-
source: {
|
|
215
|
-
sourceRef: string;
|
|
216
|
-
kind: "inquiry-question";
|
|
217
|
-
observedAt: string;
|
|
218
|
-
locatorScheme: "text";
|
|
219
|
-
};
|
|
220
|
-
extraction: {
|
|
221
|
-
target: string;
|
|
222
|
-
confidence?: number;
|
|
223
|
-
extractor: string;
|
|
224
|
-
extractedAt: string;
|
|
225
|
-
};
|
|
226
|
-
claimTarget: {
|
|
227
|
-
subjectType: string;
|
|
228
|
-
subjectId: string;
|
|
229
|
-
facet: string;
|
|
230
|
-
claimType: string;
|
|
231
|
-
fieldOrBehavior: string;
|
|
232
|
-
impactLevel: "low";
|
|
233
|
-
};
|
|
234
|
-
projection?: {
|
|
235
|
-
candidateSetId: string;
|
|
236
|
-
candidateId: string;
|
|
237
|
-
};
|
|
238
|
-
}>;
|
|
239
|
-
candidateSetStatus: "needs-review" | "conflict";
|
|
240
|
-
rationale?: string;
|
|
241
|
-
};
|
|
242
|
-
status: {
|
|
243
|
-
observedCandidateCount: number;
|
|
244
|
-
};
|
|
245
|
-
}>;
|
|
201
|
+
}>): ReviewItem[];
|
|
246
202
|
/**
|
|
247
203
|
* Reference MappingProposer for tests.
|
|
248
204
|
*
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* and lives in the flow-agents repo.
|
|
19
19
|
*/
|
|
20
20
|
import { resolveInquiry } from "@kontourai/surface";
|
|
21
|
+
import { AUTO_ACCEPT_ACTOR, AUTO_ACCEPT_WITHIN_COMFORT_ZONE, getProducerProposal, hasCandidateConflict, meetsAutoAcceptThreshold, projectProposalsToCandidateSet, } from "./producer-profile.js";
|
|
22
|
+
import { reviewResourceApiVersion } from "./review-resource.js";
|
|
21
23
|
// ---------------------------------------------------------------------------
|
|
22
24
|
// Question normalization
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
@@ -42,9 +44,18 @@ export function normalizeQuestion(question) {
|
|
|
42
44
|
.trim()
|
|
43
45
|
.replace(/[.?!,;]+$/u, "");
|
|
44
46
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The Candidate Conflict comparison key for a single mapping proposal: keys
|
|
49
|
+
* by canonical claim target (subjectType/subjectId/fieldOrBehavior) or by
|
|
50
|
+
* derivation rule id. Two proposals with the same key "agree"; more than one
|
|
51
|
+
* distinct key across a group of proposals is a conflict (see
|
|
52
|
+
* hasCandidateConflict).
|
|
53
|
+
*/
|
|
54
|
+
function mappingEquivalenceKey(proposal) {
|
|
55
|
+
return proposal.proposedTarget
|
|
56
|
+
? `target:${proposal.proposedTarget.subjectType}/${proposal.proposedTarget.subjectId}/${proposal.proposedTarget.fieldOrBehavior}`
|
|
57
|
+
: `rule:${proposal.proposedRuleId}`;
|
|
58
|
+
}
|
|
48
59
|
/**
|
|
49
60
|
* Project an array of proposals for a single question into Survey's existing
|
|
50
61
|
* Candidate / CandidateSet shapes so they flow through the existing review
|
|
@@ -60,46 +71,33 @@ export function normalizeQuestion(question) {
|
|
|
60
71
|
*/
|
|
61
72
|
export function proposalsToCandidateSet(question, proposals) {
|
|
62
73
|
const normalized = normalizeQuestion(question);
|
|
63
|
-
const
|
|
64
|
-
|
|
74
|
+
const candidateSetProposals = proposals.map((proposal) => ({
|
|
75
|
+
candidateId: `mapping-candidate.${proposal.id}`,
|
|
65
76
|
extractionId: proposal.id,
|
|
66
77
|
value: proposal.proposedTarget ?? proposal.proposedRuleId ?? null,
|
|
67
78
|
confidence: proposal.confidence,
|
|
79
|
+
equivalenceKey: mappingEquivalenceKey(proposal),
|
|
68
80
|
metadata: {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
proposedAt: proposal.proposedAt,
|
|
78
|
-
},
|
|
81
|
+
proposalId: proposal.id,
|
|
82
|
+
proposedTarget: proposal.proposedTarget,
|
|
83
|
+
proposedRuleId: proposal.proposedRuleId,
|
|
84
|
+
confidence: proposal.confidence,
|
|
85
|
+
rationale: proposal.rationale,
|
|
86
|
+
excerpt: proposal.excerpt,
|
|
87
|
+
proposedBy: proposal.proposedBy,
|
|
88
|
+
proposedAt: proposal.proposedAt,
|
|
79
89
|
},
|
|
80
90
|
}));
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
id: `mapping-candidate-set.${normalized}`,
|
|
85
|
-
target: normalized,
|
|
86
|
-
candidates,
|
|
87
|
-
status,
|
|
88
|
-
metadata: {
|
|
91
|
+
return projectProposalsToCandidateSet(normalized, candidateSetProposals, {
|
|
92
|
+
candidateSetId: `mapping-candidate-set.${normalized}`,
|
|
93
|
+
candidateSetMetadata: {
|
|
89
94
|
inquiryMapping: {
|
|
90
95
|
question,
|
|
91
96
|
normalizedQuestion: normalized,
|
|
92
97
|
kind: "inquiry-question",
|
|
93
98
|
},
|
|
94
99
|
},
|
|
95
|
-
};
|
|
96
|
-
return { candidateSet, candidates };
|
|
97
|
-
}
|
|
98
|
-
function proposalsDisagree(proposals) {
|
|
99
|
-
const keys = new Set(proposals.map((p) => p.proposedTarget
|
|
100
|
-
? `target:${p.proposedTarget.subjectType}/${p.proposedTarget.subjectId}/${p.proposedTarget.fieldOrBehavior}`
|
|
101
|
-
: `rule:${p.proposedRuleId}`));
|
|
102
|
-
return keys.size > 1;
|
|
100
|
+
});
|
|
103
101
|
}
|
|
104
102
|
// ---------------------------------------------------------------------------
|
|
105
103
|
// Review outcome → InquiryMapping
|
|
@@ -119,7 +117,7 @@ export function applyMappingReview(candidateSet, reviewOutcome) {
|
|
|
119
117
|
if (!candidate) {
|
|
120
118
|
throw new Error(`applyMappingReview: no candidate found for id ${candidateId ?? "<none>"}`);
|
|
121
119
|
}
|
|
122
|
-
const meta = candidate
|
|
120
|
+
const meta = getProducerProposal(candidate);
|
|
123
121
|
const proposalId = meta?.proposalId ?? candidate.extractionId;
|
|
124
122
|
const status = reviewOutcome.status === "verified" || reviewOutcome.status === "assumed" || reviewOutcome.status === "rejected"
|
|
125
123
|
? reviewOutcome.status
|
|
@@ -152,20 +150,20 @@ export function applyAutoAcceptPolicy(proposals, policy) {
|
|
|
152
150
|
if (proposals.length === 0)
|
|
153
151
|
return [];
|
|
154
152
|
// If proposals disagree, none can be auto-accepted
|
|
155
|
-
if (proposals.
|
|
153
|
+
if (hasCandidateConflict(proposals.map((p) => ({ equivalenceKey: mappingEquivalenceKey(p) }))))
|
|
156
154
|
return [];
|
|
157
155
|
return proposals
|
|
158
|
-
.filter((p) => p.confidence
|
|
156
|
+
.filter((p) => meetsAutoAcceptThreshold(p.confidence, policy.minConfidence))
|
|
159
157
|
.map((proposal) => ({
|
|
160
158
|
id: `inquiry-mapping.auto.${normalizeQuestion(proposal.question)}`,
|
|
161
159
|
normalizedQuestion: normalizeQuestion(proposal.question),
|
|
162
160
|
target: proposal.proposedTarget,
|
|
163
161
|
ruleId: proposal.proposedRuleId,
|
|
164
162
|
status: "assumed",
|
|
165
|
-
reviewedBy:
|
|
163
|
+
reviewedBy: AUTO_ACCEPT_ACTOR,
|
|
166
164
|
reviewedAt: proposal.proposedAt,
|
|
167
165
|
rationale: `Auto-accepted: confidence ${proposal.confidence} >= threshold ${policy.minConfidence}. ${proposal.rationale}`,
|
|
168
|
-
withinComfortZone:
|
|
166
|
+
withinComfortZone: AUTO_ACCEPT_WITHIN_COMFORT_ZONE,
|
|
169
167
|
proposalId: proposal.id,
|
|
170
168
|
}));
|
|
171
169
|
}
|
|
@@ -266,7 +264,7 @@ export function resolveQuestion(bundle, question, options) {
|
|
|
266
264
|
*/
|
|
267
265
|
export function buildMappingReviewItems(candidateSets) {
|
|
268
266
|
return candidateSets.map(({ candidateSet, candidates }) => ({
|
|
269
|
-
apiVersion:
|
|
267
|
+
apiVersion: reviewResourceApiVersion,
|
|
270
268
|
kind: "ReviewItem",
|
|
271
269
|
metadata: {
|
|
272
270
|
name: candidateSet.id,
|
|
@@ -275,7 +273,7 @@ export function buildMappingReviewItems(candidateSets) {
|
|
|
275
273
|
spec: {
|
|
276
274
|
target: candidateSet.target,
|
|
277
275
|
candidates: candidates.map((candidate) => {
|
|
278
|
-
const meta = candidate
|
|
276
|
+
const meta = getProducerProposal(candidate);
|
|
279
277
|
const targetOrRule = meta?.proposedTarget
|
|
280
278
|
? `${meta.proposedTarget.subjectType}/${meta.proposedTarget.subjectId}/${meta.proposedTarget.fieldOrBehavior}`
|
|
281
279
|
: `rule:${meta?.proposedRuleId ?? "unknown"}`;
|