@kontourai/survey 0.4.24 → 0.5.1

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.
Files changed (37) hide show
  1. package/README.md +10 -10
  2. package/dist/example-data/corrected-document-candidates.d.ts +2 -0
  3. package/dist/{fixtures → example-data}/corrected-document-candidates.js +3 -3
  4. package/dist/{fixtures → example-data}/downstream-public-directory-proposal.d.ts +1 -1
  5. package/dist/{fixtures → example-data}/downstream-public-directory-proposal.js +1 -1
  6. package/dist/{fixtures → example-data}/public-directory-review-resource.d.ts +2 -2
  7. package/dist/{fixtures → example-data}/public-directory-review-resource.js +2 -2
  8. package/dist/example-data/public-field-review.d.ts +2 -0
  9. package/dist/{fixtures → example-data}/public-field-review.js +2 -2
  10. package/dist/{fixtures → example-data}/regulated-document-review-resource.d.ts +1 -1
  11. package/dist/{fixtures → example-data}/regulated-document-review-resource.js +3 -3
  12. package/dist/examples/public-field-observation.js +4 -4
  13. package/dist/examples/review-workbench/downstream-public-directory-adapter.d.ts +1 -1
  14. package/dist/examples/review-workbench/facility-credential-consumer.d.ts +2 -2
  15. package/dist/examples/review-workbench/facility-credential-consumer.js +8 -8
  16. package/dist/examples/review-workbench/server-apply-consumer.d.ts +1 -1
  17. package/dist/examples/review-workbench/server-apply-consumer.js +3 -3
  18. package/dist/src/agent-utterance.d.ts +166 -0
  19. package/dist/src/agent-utterance.js +373 -0
  20. package/dist/src/anthropic.d.ts +104 -0
  21. package/dist/src/anthropic.js +383 -0
  22. package/dist/src/index.d.ts +8 -2
  23. package/dist/src/index.js +4 -1
  24. package/dist/src/inquiry-mapping.d.ts +256 -0
  25. package/dist/src/inquiry-mapping.js +385 -0
  26. package/dist/src/review-workbench/review-queue-session.js +3 -3
  27. package/dist/src/review-workbench/review-surface-preview.js +1 -1
  28. package/dist/src/review-workbench/review-workbench-data.d.ts +4 -4
  29. package/dist/src/review-workbench/review-workbench-data.js +14 -14
  30. package/dist/src/schema-mapping.d.ts +196 -0
  31. package/dist/src/schema-mapping.js +486 -0
  32. package/dist/src/to-surface.d.ts +3 -3
  33. package/dist/src/to-surface.js +1 -1
  34. package/dist/src/types.d.ts +2 -2
  35. package/package.json +20 -7
  36. package/dist/fixtures/corrected-document-candidates.d.ts +0 -2
  37. package/dist/fixtures/public-field-review.d.ts +0 -2
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Agent-utterance producer profile — ADR 0003 step 6.
3
+ *
4
+ * This module implements Survey as a producer pointed at agent utterances
5
+ * instead of web sources. Each factual statement in agent prose is extracted
6
+ * as a candidate claim and run through the Inquiry pipeline.
7
+ *
8
+ * Integration point: surveyAgentUtterance is the clean entry point for
9
+ * consumers wanting to "spell-check" an agent's output for evidence. Flow-agent
10
+ * hook wiring (connecting this function to a live agent's output pipeline) is
11
+ * out of scope for this module and lives in the flow-agents repo.
12
+ *
13
+ * Hard constraint (ADR 0003 §4): nothing here silently decides. The
14
+ * UtteranceClaimExtractor is a pluggable interface; implementations may be
15
+ * deterministic or model-backed, but they are always extractors — their output
16
+ * has full provenance (excerpt, span, extractor name, confidence) and is
17
+ * run through the Inquiry pipeline rather than treated as authoritative.
18
+ */
19
+ import { resolveInquiry } from "@kontourai/surface";
20
+ import { lookupMapping, resolveQuestion } from "./inquiry-mapping.js";
21
+ // ---------------------------------------------------------------------------
22
+ // SurveyInput projection
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Project an agent utterance and its extracted statements into the standard
26
+ * SurveyInput shape so they can flow into buildSurveyTrustBundle.
27
+ *
28
+ * Each extracted statement lands as:
29
+ * RawSource (agent-utterance) → Extraction (with text-span locator) →
30
+ * Candidate → CandidateSet (needs-review, no review outcome) → ClaimTarget
31
+ *
32
+ * Status discipline (ADR 0003 §4, to-surface.ts producer rules):
33
+ * - All claims project as "proposed" — unreviewed extractions are proposals,
34
+ * never authoritative. assertProducerDiscipline forbids verified/assumed
35
+ * without a review outcome.
36
+ * - agent-utterance is not a manual-entry source, so extraction.locator is
37
+ * required. Span-located statements use text-span:start-end; span-less
38
+ * statements use text-span derived from the excerpt offset in the utterance
39
+ * (best-effort, 0-based).
40
+ *
41
+ * The returned SurveyInput can be passed directly to buildSurveyTrustBundle
42
+ * to produce a TrustBundle with full provenance in the Trust Bundle metadata.
43
+ *
44
+ * @param utterance - The raw agent utterance text.
45
+ * @param extracted - ExtractedStatements produced by a UtteranceClaimExtractor.
46
+ * @param context - agentId, extractor name, optional now timestamp.
47
+ */
48
+ export function utteranceToSurveyInput(utterance, extracted, context) {
49
+ const { agentId, extractorName, now } = context;
50
+ const observedAt = (now ?? new Date()).toISOString();
51
+ const source = context.source ?? `agent-utterance:${agentId}`;
52
+ // One shared RawSource for the entire utterance
53
+ const sourceId = `agent-utterance:${agentId}:${observedAt}`;
54
+ const rawSource = {
55
+ id: sourceId,
56
+ kind: "agent-utterance",
57
+ sourceRef: `agent-utterance://${agentId}/${observedAt}`,
58
+ observedAt,
59
+ locatorScheme: "text-span",
60
+ inlineText: utterance,
61
+ metadata: { agentId },
62
+ };
63
+ const extractions = [];
64
+ const candidateSets = [];
65
+ const claims = [];
66
+ for (let idx = 0; idx < extracted.length; idx++) {
67
+ const statement = extracted[idx];
68
+ const statementId = `${sourceId}.statement.${idx}`;
69
+ const extractionId = `${statementId}.extraction`;
70
+ const candidateId = `${statementId}.candidate`;
71
+ const candidateSetId = `${statementId}.candidate-set`;
72
+ const claimId = `${statementId}.claim`;
73
+ // Compute locator — required for non-manual-entry sources
74
+ // (assertProducerDiscipline throws without it)
75
+ const locator = spanToLocator(statement.span) ?? excerptLocator(utterance, statement.excerpt);
76
+ const extraction = {
77
+ id: extractionId,
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 = {
127
+ id: claimId,
128
+ candidateSetId,
129
+ candidateId,
130
+ subjectType: statement.target.subjectType,
131
+ subjectId: statement.target.subjectId,
132
+ surface: "agent-utterance.profile",
133
+ claimType: "agent-extraction",
134
+ fieldOrBehavior: statement.target.fieldOrBehavior,
135
+ value: statement.value,
136
+ // status intentionally omitted → computed as "proposed" by statusFor
137
+ impactLevel: "low",
138
+ collectedBy: extractorName,
139
+ metadata: {
140
+ survey: {
141
+ agentUtterance: {
142
+ agentId,
143
+ extractorName,
144
+ excerpt: statement.excerpt,
145
+ span: statement.span,
146
+ confidence: statement.confidence,
147
+ locator,
148
+ },
149
+ },
150
+ },
151
+ };
152
+ extractions.push(extraction);
153
+ candidateSets.push(candidateSet);
154
+ claims.push(claimTarget);
155
+ }
156
+ return {
157
+ source,
158
+ generatedAt: observedAt,
159
+ rawSources: [rawSource],
160
+ extractions,
161
+ candidateSets,
162
+ reviewOutcomes: [],
163
+ claims,
164
+ };
165
+ }
166
+ // ---------------------------------------------------------------------------
167
+ // Main entry point
168
+ // ---------------------------------------------------------------------------
169
+ /**
170
+ * Survey an agent utterance, returning a trust report for each extracted claim.
171
+ *
172
+ * Steps:
173
+ * 1. Build a RawSource for the utterance (kind: "agent-utterance").
174
+ * 2. Run the extractor → project each statement into Survey records with full
175
+ * provenance (excerpt, span locator, extractor name, confidence).
176
+ * 3. Resolve each extracted claim against the bundle via resolveInquiry or
177
+ * resolveQuestion (if mappings are provided).
178
+ * 4. Return an UtteranceTrustReport with per-statement badges.
179
+ *
180
+ * This function is the integration point for consumers. Flow-agent hook wiring
181
+ * lives in the flow-agents repo.
182
+ */
183
+ export async function surveyAgentUtterance(utterance, extractor, context) {
184
+ const { bundle, mappings, rules, now, agentId } = context;
185
+ const observedAt = (now ?? new Date()).toISOString();
186
+ // Step 1: Build a RawSource for this utterance
187
+ const sourceId = `agent-utterance:${agentId}:${observedAt}`;
188
+ const source = {
189
+ id: sourceId,
190
+ kind: "agent-utterance",
191
+ sourceRef: `agent-utterance://${agentId}/${observedAt}`,
192
+ observedAt,
193
+ locatorScheme: "text-span",
194
+ inlineText: utterance,
195
+ metadata: { agentId },
196
+ };
197
+ // Step 2: Extract statements
198
+ const extracted = await Promise.resolve(extractor.extract(utterance));
199
+ // Step 3 & 4: Resolve each statement and build the report
200
+ const statements = [];
201
+ 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
+ // Resolve the claim
225
+ let inquiryRecord;
226
+ if (mappings && mappings.length > 0) {
227
+ // If we have question-level mappings, check them first by building
228
+ // a question from the target
229
+ const syntheticQuestion = targetToQuestion(statement.target);
230
+ const mapping = lookupMapping(mappings, syntheticQuestion);
231
+ if (mapping) {
232
+ inquiryRecord = resolveQuestion(bundle, syntheticQuestion, {
233
+ mappings,
234
+ rules,
235
+ now,
236
+ askedBy: agentId,
237
+ });
238
+ }
239
+ else {
240
+ // No mapping: resolve directly by canonical target
241
+ inquiryRecord = resolveByTarget(bundle, statement.target, agentId, observedAt, rules, now);
242
+ }
243
+ }
244
+ else {
245
+ // Resolve directly by canonical target
246
+ inquiryRecord = resolveByTarget(bundle, statement.target, agentId, observedAt, rules, now);
247
+ }
248
+ const badge = badgeFromRecord(inquiryRecord);
249
+ statements.push({
250
+ excerpt: statement.excerpt,
251
+ span: statement.span,
252
+ target: statement.target,
253
+ inquiryRecord,
254
+ badge,
255
+ });
256
+ // Suppress unused-variable warning for extraction
257
+ void extraction;
258
+ }
259
+ return { source, statements };
260
+ }
261
+ // ---------------------------------------------------------------------------
262
+ // Internal helpers
263
+ // ---------------------------------------------------------------------------
264
+ function resolveByTarget(bundle, target, askedBy, askedAt, rules, now) {
265
+ const id = `inquiry.direct.${canonicalTargetKey(target)}.${askedAt}`;
266
+ const inquiry = {
267
+ id,
268
+ question: targetToQuestion(target),
269
+ target,
270
+ askedBy,
271
+ askedAt,
272
+ };
273
+ return resolveInquiry(bundle, inquiry, { now, rules });
274
+ }
275
+ function canonicalTargetKey(target) {
276
+ return `${target.subjectType}/${target.subjectId}/${target.fieldOrBehavior}`;
277
+ }
278
+ function targetToQuestion(target) {
279
+ return `${target.subjectId} ${target.fieldOrBehavior}`;
280
+ }
281
+ function badgeFromRecord(record) {
282
+ if (record.outcome === "unsupported")
283
+ return "unsupported";
284
+ const status = record.answer?.status;
285
+ if (!status)
286
+ return "unsupported";
287
+ if (status === "verified")
288
+ return "verified";
289
+ if (status === "assumed")
290
+ return "assumed";
291
+ if (status === "stale")
292
+ return "stale";
293
+ if (status === "disputed")
294
+ return "disputed";
295
+ if (status === "rejected" || status === "superseded")
296
+ return "rejected";
297
+ return "unsupported";
298
+ }
299
+ /**
300
+ * Convert a text-span to a locator string.
301
+ */
302
+ function spanToLocator(span) {
303
+ if (!span)
304
+ return undefined;
305
+ return `text-span:${span.start}-${span.end}`;
306
+ }
307
+ /**
308
+ * Best-effort locator from excerpt text — find the first occurrence of the
309
+ * excerpt in the utterance and use that as a text-span locator.
310
+ * Falls back to text-span:0-0 if the excerpt is not found.
311
+ */
312
+ function excerptLocator(utterance, excerpt) {
313
+ const idx = utterance.indexOf(excerpt);
314
+ if (idx >= 0) {
315
+ return `text-span:${idx}-${idx + excerpt.length}`;
316
+ }
317
+ // Fallback: anchor to start (preserves discipline contract; locator is
318
+ // best-effort for span-less extractors)
319
+ return `text-span:0-${excerpt.length}`;
320
+ }
321
+ // ---------------------------------------------------------------------------
322
+ // Reference extractor (deterministic, for tests — not for production use)
323
+ // ---------------------------------------------------------------------------
324
+ /**
325
+ * Reference UtteranceClaimExtractor for tests.
326
+ *
327
+ * REFERENCE IMPLEMENTATION ONLY — not suitable for production extraction.
328
+ *
329
+ * Parsing strategy: looks for statements matching the pattern:
330
+ * "<subjectId> <fieldOrBehavior> is <value>"
331
+ * or
332
+ * "<subjectId> <fieldOrBehavior>: <value>"
333
+ *
334
+ * where subjectId and fieldOrBehavior are single words. This intentionally
335
+ * simple and transparent pattern lets tests be deterministic.
336
+ *
337
+ * The subjectType is always "unknown" since it cannot be inferred from text
338
+ * alone in this reference implementation.
339
+ */
340
+ export const referenceUtteranceExtractor = {
341
+ name: "reference-utterance-extractor",
342
+ extract(utterance) {
343
+ const results = [];
344
+ // Pattern: "<word> <word> is <value>" or "<word> <word>: <value>"
345
+ // Value is a single non-whitespace token; trailing punctuation is stripped.
346
+ const isPattern = /\b(\S+)\s+(\S+)\s+is\s+(\S+)/giu;
347
+ const colonPattern = /\b(\S+)\s+(\S+):\s*(\S+)/giu;
348
+ for (const pattern of [isPattern, colonPattern]) {
349
+ let match;
350
+ while ((match = pattern.exec(utterance)) !== null) {
351
+ const [full, subjectId, fieldOrBehavior, rawValue] = match;
352
+ if (!subjectId || !fieldOrBehavior || rawValue === undefined)
353
+ continue;
354
+ const start = match.index;
355
+ const end = start + full.length;
356
+ // Strip trailing punctuation from the captured value
357
+ const value = rawValue.replace(/[.!?,;]+$/u, "");
358
+ results.push({
359
+ target: {
360
+ subjectType: "unknown",
361
+ subjectId: subjectId.toLowerCase(),
362
+ fieldOrBehavior: fieldOrBehavior.toLowerCase(),
363
+ },
364
+ value,
365
+ excerpt: full.replace(/[.!?,;]+$/u, ""),
366
+ span: { start, end },
367
+ confidence: 0.6,
368
+ });
369
+ }
370
+ }
371
+ return results;
372
+ },
373
+ };
@@ -0,0 +1,104 @@
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;