@kontourai/survey 1.4.0 → 1.6.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 +52 -49
- 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 +204 -0
- package/dist/src/producer-profile.js +158 -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 +59 -52
- package/dist/src/source-of-authority-observation.js +6 -9
- package/dist/src/to-surface.js +6 -9
- package/package.json +5 -2
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* and lives in the flow-agents repo.
|
|
19
19
|
*/
|
|
20
20
|
import { resolveInquiry } from "@kontourai/surface";
|
|
21
|
+
import { evaluateAutoAccept, getProducerProposal, hasCandidateConflict, 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,22 +150,27 @@ 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
|
-
return proposals
|
|
158
|
-
|
|
159
|
-
.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
155
|
+
return proposals.flatMap((proposal) => {
|
|
156
|
+
const decision = evaluateAutoAccept({ confidence: proposal.confidence, rationale: proposal.rationale, proposedAt: proposal.proposedAt }, false, policy, proposal.proposedAt);
|
|
157
|
+
if (!decision.accepted)
|
|
158
|
+
return [];
|
|
159
|
+
return [
|
|
160
|
+
{
|
|
161
|
+
id: `inquiry-mapping.auto.${normalizeQuestion(proposal.question)}`,
|
|
162
|
+
normalizedQuestion: normalizeQuestion(proposal.question),
|
|
163
|
+
target: proposal.proposedTarget,
|
|
164
|
+
ruleId: proposal.proposedRuleId,
|
|
165
|
+
status: "assumed",
|
|
166
|
+
reviewedBy: decision.actor,
|
|
167
|
+
reviewedAt: decision.reviewedAt,
|
|
168
|
+
rationale: decision.rationale,
|
|
169
|
+
withinComfortZone: decision.withinComfortZone,
|
|
170
|
+
proposalId: proposal.id,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
});
|
|
171
174
|
}
|
|
172
175
|
// ---------------------------------------------------------------------------
|
|
173
176
|
// Mapping lookup
|
|
@@ -266,7 +269,7 @@ export function resolveQuestion(bundle, question, options) {
|
|
|
266
269
|
*/
|
|
267
270
|
export function buildMappingReviewItems(candidateSets) {
|
|
268
271
|
return candidateSets.map(({ candidateSet, candidates }) => ({
|
|
269
|
-
apiVersion:
|
|
272
|
+
apiVersion: reviewResourceApiVersion,
|
|
270
273
|
kind: "ReviewItem",
|
|
271
274
|
metadata: {
|
|
272
275
|
name: candidateSet.id,
|
|
@@ -275,7 +278,7 @@ export function buildMappingReviewItems(candidateSets) {
|
|
|
275
278
|
spec: {
|
|
276
279
|
target: candidateSet.target,
|
|
277
280
|
candidates: candidates.map((candidate) => {
|
|
278
|
-
const meta = candidate
|
|
281
|
+
const meta = getProducerProposal(candidate);
|
|
279
282
|
const targetOrRule = meta?.proposedTarget
|
|
280
283
|
? `${meta.proposedTarget.subjectType}/${meta.proposedTarget.subjectId}/${meta.proposedTarget.fieldOrBehavior}`
|
|
281
284
|
: `rule:${meta?.proposedRuleId ?? "unknown"}`;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { readFile, writeFile, rename } from "node:fs/promises";
|
|
3
3
|
import { resolve, dirname } from "node:path";
|
|
4
|
-
import { buildReviewSessionEvents, currentReviewItem, deriveQueueRowStatus, nextUnresolvedItemName,
|
|
5
|
-
import { createServerReviewSessionRecord, deriveServerReviewSessionApplyResult, } from "../review-workbench/server-review-session.js";
|
|
4
|
+
import { buildReviewSessionEvents, currentReviewItem, deriveQueueRowStatus, nextUnresolvedItemName, reviewSessionSummary, workbenchDecisionDefinitions, } from "../review-workbench/review-workbench.js";
|
|
5
|
+
import { createServerReviewSessionRecord, currentSessionState, deriveServerReviewSessionApplyResult, } from "../review-workbench/server-review-session.js";
|
|
6
6
|
/**
|
|
7
7
|
* Minimal Model Context Protocol server over stdio for review-queue inspection
|
|
8
8
|
* and decision-making against a session JSON file.
|
|
@@ -42,7 +42,7 @@ async function writeSessionFileAtomic(path, content) {
|
|
|
42
42
|
}
|
|
43
43
|
// ---- Queue helpers -------------------------------------------------------
|
|
44
44
|
function queueSummaryText(snapshot, events) {
|
|
45
|
-
const current =
|
|
45
|
+
const current = currentSessionState(snapshot, events);
|
|
46
46
|
const summary = reviewSessionSummary(current);
|
|
47
47
|
const total = current.items.length;
|
|
48
48
|
const resolved = total - summary.unresolved;
|
|
@@ -66,7 +66,7 @@ function queueSummaryText(snapshot, events) {
|
|
|
66
66
|
].join("\n");
|
|
67
67
|
}
|
|
68
68
|
function itemDetailText(item, snapshot, events) {
|
|
69
|
-
const current =
|
|
69
|
+
const current = currentSessionState(snapshot, events);
|
|
70
70
|
const status = deriveQueueRowStatus(item, current);
|
|
71
71
|
const decision = current.decisionsByItemName[item.metadata.name];
|
|
72
72
|
const note = current.notesByItemName[item.metadata.name];
|
|
@@ -112,7 +112,7 @@ function escapeHtml(text) {
|
|
|
112
112
|
.replace(/"/g, """);
|
|
113
113
|
}
|
|
114
114
|
function buildReviewCardHtml(item, snapshot, events) {
|
|
115
|
-
const current =
|
|
115
|
+
const current = currentSessionState(snapshot, events);
|
|
116
116
|
const summary = reviewSessionSummary(current);
|
|
117
117
|
const total = current.items.length;
|
|
118
118
|
const resolved = total - summary.unresolved;
|
|
@@ -293,7 +293,7 @@ async function toolQueue(options) {
|
|
|
293
293
|
const text = queueSummaryText(snapshot, events);
|
|
294
294
|
const queueData = {
|
|
295
295
|
items: snapshot.items.map((item) => {
|
|
296
|
-
const current =
|
|
296
|
+
const current = currentSessionState(snapshot, events);
|
|
297
297
|
return {
|
|
298
298
|
name: item.metadata.name,
|
|
299
299
|
target: item.spec.target,
|
|
@@ -302,14 +302,14 @@ async function toolQueue(options) {
|
|
|
302
302
|
candidateSetStatus: item.spec.candidateSetStatus,
|
|
303
303
|
};
|
|
304
304
|
}),
|
|
305
|
-
summary: reviewSessionSummary(
|
|
306
|
-
activeItemName: (
|
|
305
|
+
summary: reviewSessionSummary(currentSessionState(snapshot, events)),
|
|
306
|
+
activeItemName: currentSessionState(snapshot, events).activeItemName,
|
|
307
307
|
};
|
|
308
308
|
const content = [
|
|
309
309
|
{ type: "text", text: `${text}\n\n${JSON.stringify(queueData, null, 2)}` },
|
|
310
310
|
];
|
|
311
311
|
if (!options.noUi) {
|
|
312
|
-
const activeItem = currentReviewItem(
|
|
312
|
+
const activeItem = currentReviewItem(currentSessionState(snapshot, events));
|
|
313
313
|
content.push(buildUiResource(activeItem, snapshot, events, "queue"));
|
|
314
314
|
}
|
|
315
315
|
return content;
|
|
@@ -317,7 +317,7 @@ async function toolQueue(options) {
|
|
|
317
317
|
async function toolItem(itemName, options) {
|
|
318
318
|
const file = await readSessionFile(options.sessionPath);
|
|
319
319
|
const { snapshot, events } = file;
|
|
320
|
-
const current =
|
|
320
|
+
const current = currentSessionState(snapshot, events);
|
|
321
321
|
const item = current.items.find((i) => i.metadata.name === itemName);
|
|
322
322
|
if (!item) {
|
|
323
323
|
throw new DomainError(`Unknown review item: ${itemName}`);
|
|
@@ -354,7 +354,7 @@ async function toolDecide(itemName, mcpDecision, note, options) {
|
|
|
354
354
|
}
|
|
355
355
|
const file = await readSessionFile(options.sessionPath);
|
|
356
356
|
const { snapshot, events } = file;
|
|
357
|
-
const current =
|
|
357
|
+
const current = currentSessionState(snapshot, events);
|
|
358
358
|
const item = current.items.find((i) => i.metadata.name === itemName);
|
|
359
359
|
if (!item) {
|
|
360
360
|
throw new DomainError(`Unknown review item: ${itemName}`);
|
|
@@ -436,7 +436,7 @@ function buildUiResource(item, snapshot, events, instance) {
|
|
|
436
436
|
// embedded `queue` resource carries — here served via resources/read).
|
|
437
437
|
async function readQueuePanelHtml(options) {
|
|
438
438
|
const { snapshot, events } = await readSessionFile(options.sessionPath);
|
|
439
|
-
const current =
|
|
439
|
+
const current = currentSessionState(snapshot, events);
|
|
440
440
|
const activeItem = currentReviewItem(current);
|
|
441
441
|
return buildReviewCardHtml(activeItem, snapshot, events);
|
|
442
442
|
}
|
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
import type { SurveyObservationInput } from "./builder.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Observation authoring core — CONTEXT.md "Observation" / "Field Observation
|
|
4
|
+
* and Repeated Observation" ("helper shapes for authoring Observations, not
|
|
5
|
+
* separate domain concepts").
|
|
6
|
+
*
|
|
7
|
+
* `buildObservation`/`BuildObservationInput` are the shared authoring
|
|
8
|
+
* primitive: they own the `extraction.target`/`value`/`excerpt` and
|
|
9
|
+
* `claim.fieldOrBehavior`/`value`/`metadata` assembly (including the
|
|
10
|
+
* three-way `claim.metadata` / caller `metadata` / representation-supplied
|
|
11
|
+
* `surveyMetadata` merge below). Consumed by relative import from
|
|
12
|
+
* `field-observation.ts`, `repeated-observation.ts` (via the
|
|
13
|
+
* representation-keyed `buildFieldObservation`/`buildRepeatedObservation`
|
|
14
|
+
* wrappers below), and `source-of-authority-observation.ts`, which calls
|
|
15
|
+
* `buildObservation` directly with its own `surveyMetadata`/`defaultExcerpt`.
|
|
16
|
+
* None of this is re-exported from `src/index.ts`.
|
|
17
|
+
*
|
|
18
|
+
* `ObservationAuthoringInput` is the shared base shape (id/field/value/
|
|
19
|
+
* rawSource/extraction/reviewOutcome/claim/candidate/candidateSet/metadata)
|
|
20
|
+
* common to `BuildObservationInput` and the two public skins' input types
|
|
21
|
+
* (`FieldObservationInput`, `RepeatedObservationInput`); the skins extend it
|
|
22
|
+
* with only their own `representation` literal.
|
|
23
|
+
*
|
|
24
|
+
* `buildFieldObservation`/`buildRepeatedObservation` are the representation-
|
|
25
|
+
* keyed layer above `buildObservation`: each owns the default-excerpt
|
|
26
|
+
* formula and the `surveyMetadata` sub-key (`field` vs `repeated`) for its
|
|
27
|
+
* representation. They do not change `buildObservation`'s own signature or
|
|
28
|
+
* body — `source-of-authority-observation.ts` depends on that staying
|
|
29
|
+
* exactly as-is.
|
|
30
|
+
*
|
|
31
|
+
* `mergeObservationMetadata`/`mergeNestedRecords` are exported as a
|
|
32
|
+
* module-internal seam (like `src/producer-discipline.ts`) purely for direct
|
|
33
|
+
* test import (`tests/observation-helper.test.ts`) — consumed by relative
|
|
34
|
+
* import only, NOT re-exported from `src/index.ts`. Their bodies, including
|
|
35
|
+
* the `mergeNestedRecords` nested-record-vs-scalar asymmetry and the `??`
|
|
36
|
+
* null/undefined coalescing, are unchanged characterization, not a defect.
|
|
37
|
+
*/
|
|
38
|
+
export interface ObservationAuthoringInput<TValue> {
|
|
3
39
|
id: string;
|
|
4
40
|
field: string;
|
|
5
41
|
value: TValue;
|
|
@@ -15,7 +51,17 @@ export interface BuildObservationInput<TValue> {
|
|
|
15
51
|
candidate?: SurveyObservationInput["candidate"];
|
|
16
52
|
candidateSet?: SurveyObservationInput["candidateSet"];
|
|
17
53
|
metadata?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
export interface BuildObservationInput<TValue> extends ObservationAuthoringInput<TValue> {
|
|
18
56
|
surveyMetadata: Record<string, unknown>;
|
|
19
57
|
defaultExcerpt: string;
|
|
20
58
|
}
|
|
21
59
|
export declare function buildObservation<TValue>(input: BuildObservationInput<TValue>): SurveyObservationInput;
|
|
60
|
+
export declare function buildFieldObservation<TValue>(input: ObservationAuthoringInput<TValue> & {
|
|
61
|
+
representation?: "scalar";
|
|
62
|
+
}): SurveyObservationInput;
|
|
63
|
+
export declare function buildRepeatedObservation<TItem>(input: ObservationAuthoringInput<readonly TItem[]> & {
|
|
64
|
+
representation?: "aggregate-array";
|
|
65
|
+
}): SurveyObservationInput;
|
|
66
|
+
export declare function mergeObservationMetadata(claimMetadata: Record<string, unknown> | undefined, metadata: Record<string, unknown> | undefined, surveyMetadata: Record<string, unknown>): Record<string, unknown>;
|
|
67
|
+
export declare function mergeNestedRecords(claimSurvey: Record<string, unknown>, survey: Record<string, unknown>, surveyMetadata: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -19,7 +19,46 @@ export function buildObservation(input) {
|
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Representation-keyed layer above `buildObservation`. Owns the
|
|
24
|
+
* `surveyMetadata` sub-key name, the default-`representation` literal, and
|
|
25
|
+
* the default-excerpt formula for the "field" (scalar) and "repeated"
|
|
26
|
+
* (aggregate-array) representations — the knowledge previously duplicated
|
|
27
|
+
* inline in `field-observation.ts`/`repeated-observation.ts`. Called only by
|
|
28
|
+
* the two thin public skins; `buildObservation` itself stays representation-
|
|
29
|
+
* agnostic.
|
|
30
|
+
*/
|
|
31
|
+
function valueSummary(value) {
|
|
32
|
+
if (value === null || value === undefined)
|
|
33
|
+
return "<empty>";
|
|
34
|
+
return String(value);
|
|
35
|
+
}
|
|
36
|
+
export function buildFieldObservation(input) {
|
|
37
|
+
const representation = input.representation ?? "scalar";
|
|
38
|
+
return buildObservation({
|
|
39
|
+
...input,
|
|
40
|
+
surveyMetadata: {
|
|
41
|
+
field: { representation },
|
|
42
|
+
},
|
|
43
|
+
defaultExcerpt: `${input.field}: ${valueSummary(input.value)}`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function buildRepeatedObservation(input) {
|
|
47
|
+
const representation = input.representation ?? "aggregate-array";
|
|
48
|
+
const value = [...input.value];
|
|
49
|
+
return buildObservation({
|
|
50
|
+
...input,
|
|
51
|
+
value,
|
|
52
|
+
surveyMetadata: {
|
|
53
|
+
repeated: {
|
|
54
|
+
representation,
|
|
55
|
+
itemCount: value.length,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
defaultExcerpt: `${input.field}: ${value.length} item(s)`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function mergeObservationMetadata(claimMetadata, metadata, surveyMetadata) {
|
|
23
62
|
const claimSurvey = claimMetadata?.survey && isRecord(claimMetadata.survey) ? claimMetadata.survey : {};
|
|
24
63
|
const survey = metadata?.survey && isRecord(metadata.survey) ? metadata.survey : {};
|
|
25
64
|
return {
|
|
@@ -32,7 +71,7 @@ function mergeObservationMetadata(claimMetadata, metadata, surveyMetadata) {
|
|
|
32
71
|
},
|
|
33
72
|
};
|
|
34
73
|
}
|
|
35
|
-
function mergeNestedRecords(claimSurvey, survey, surveyMetadata) {
|
|
74
|
+
export function mergeNestedRecords(claimSurvey, survey, surveyMetadata) {
|
|
36
75
|
const merged = {};
|
|
37
76
|
const keys = new Set([
|
|
38
77
|
...Object.keys(claimSurvey),
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TrustStatus } from "@kontourai/surface";
|
|
2
|
+
/**
|
|
3
|
+
* Producer Discipline core — CONTEXT.md "Producer Discipline" /
|
|
4
|
+
* "Source-of-Authority Observation".
|
|
5
|
+
*
|
|
6
|
+
* The one piece of the review-discipline rule proven identical, field by
|
|
7
|
+
* field, across both existing enforcement points (2026-07 exploration):
|
|
8
|
+
* - src/to-surface.ts's assertProducerDiscipline (verified/assumed claims)
|
|
9
|
+
* - src/source-of-authority-observation.ts's assertVerifiedPosture
|
|
10
|
+
* (verified/assumed source-of-authority observations)
|
|
11
|
+
* Both require, in the same relative order, when status is "verified" or
|
|
12
|
+
* "assumed": (1) a review outcome exists, (2) it has a reviewer (actor),
|
|
13
|
+
* (3) it has a reviewedAt time — with identical error text modulo the
|
|
14
|
+
* subject noun ("Claim X" vs. "Source-of-authority observation X"), which
|
|
15
|
+
* each call site supplies.
|
|
16
|
+
*
|
|
17
|
+
* The two sites' SOURCE LOCATOR requirements are NOT identical (to-surface
|
|
18
|
+
* gates on rawSource.kind !== "manual-entry" regardless of status;
|
|
19
|
+
* source-of-authority-observation gates on status verified/assumed
|
|
20
|
+
* regardless of rawSource.kind, with no manual-entry exemption), and
|
|
21
|
+
* source-of-authority-observation has an additional sourceRef check
|
|
22
|
+
* to-surface does not have. Both stay call-site-local — this module does
|
|
23
|
+
* not decide them.
|
|
24
|
+
*
|
|
25
|
+
* Module-internal seam (like src/producer-profile.ts): consumed by
|
|
26
|
+
* relative import from src/to-surface.ts and
|
|
27
|
+
* src/source-of-authority-observation.ts, NOT re-exported from
|
|
28
|
+
* src/index.ts.
|
|
29
|
+
*/
|
|
30
|
+
export interface ReviewOutcomePosture {
|
|
31
|
+
actor?: string;
|
|
32
|
+
reviewedAt?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function assertReviewOutcomeDiscipline(input: {
|
|
35
|
+
/** Message subject, e.g. `Claim ${id}` or `Source-of-authority observation ${id}` —
|
|
36
|
+
* each call site supplies its own noun so error text is unchanged. */
|
|
37
|
+
subject: string;
|
|
38
|
+
status: TrustStatus | undefined;
|
|
39
|
+
review?: ReviewOutcomePosture;
|
|
40
|
+
}): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function assertReviewOutcomeDiscipline(input) {
|
|
2
|
+
if (input.status !== "verified" && input.status !== "assumed")
|
|
3
|
+
return;
|
|
4
|
+
if (!input.review) {
|
|
5
|
+
throw new Error(`${input.subject} cannot be ${input.status} without a review outcome`);
|
|
6
|
+
}
|
|
7
|
+
if (!input.review.actor) {
|
|
8
|
+
throw new Error(`${input.subject} cannot be ${input.status} without review actor authority`);
|
|
9
|
+
}
|
|
10
|
+
if (!input.review.reviewedAt) {
|
|
11
|
+
throw new Error(`${input.subject} cannot be ${input.status} without reviewedAt`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Producer Profile core — ADR 0003 §4, CONTEXT.md "Producer Profile".
|
|
3
|
+
*
|
|
4
|
+
* This module carries the shared scaffolding every Producer Profile
|
|
5
|
+
* (inquiry-mapping, schema-mapping, and — from Slice 4 — agent-utterance)
|
|
6
|
+
* needs to turn its own proposals into Survey's existing Candidate/Candidate
|
|
7
|
+
* Set records: a generic proposal -> Candidate Set projection grouped by
|
|
8
|
+
* target, the Candidate Conflict rule, and one canonical `Candidate.metadata`
|
|
9
|
+
* key with a typed accessor, replacing each profile's hand-rolled projection,
|
|
10
|
+
* conflict check, and `as`-cast metadata round-trip.
|
|
11
|
+
*
|
|
12
|
+
* This is a module-internal seam: its exports are consumed directly by
|
|
13
|
+
* profile modules via relative import and are NOT re-exported from
|
|
14
|
+
* `src/index.ts`.
|
|
15
|
+
*
|
|
16
|
+
* Hard constraint (ADR 0003 §4): this module never decides a review outcome
|
|
17
|
+
* or a claim status. It only shapes proposal-backed Candidate/Candidate Set
|
|
18
|
+
* records — every profile still routes its output through Survey's existing
|
|
19
|
+
* review -> claim machinery unchanged.
|
|
20
|
+
*/
|
|
21
|
+
import type { Candidate, CandidateSet, CandidateSetStatus } from "./types.js";
|
|
22
|
+
/**
|
|
23
|
+
* The one canonical `Candidate.metadata` key every Producer Profile uses to
|
|
24
|
+
* carry its profile-specific proposal payload. Replaces the per-profile keys
|
|
25
|
+
* (`mappingProposal`, `schemaMappingProposal`) each profile used before
|
|
26
|
+
* adopting this core module.
|
|
27
|
+
*/
|
|
28
|
+
export declare const PRODUCER_PROPOSAL_METADATA_KEY: "producerProposal";
|
|
29
|
+
/**
|
|
30
|
+
* One profile-adapted proposal, ready to be projected into a Candidate inside
|
|
31
|
+
* a shared Candidate Set.
|
|
32
|
+
*/
|
|
33
|
+
export interface CandidateSetProposal<TValue = unknown, TMetadata = unknown> {
|
|
34
|
+
/**
|
|
35
|
+
* Caller-supplied, fully-formed Candidate id. Not templated by the core so
|
|
36
|
+
* each profile keeps its own distinct id scheme byte-for-byte.
|
|
37
|
+
*/
|
|
38
|
+
candidateId: string;
|
|
39
|
+
/**
|
|
40
|
+
* Caller-supplied, fully-formed Extraction id this proposal traces back to.
|
|
41
|
+
* Not templated by the core for the same reason as `candidateId`.
|
|
42
|
+
*/
|
|
43
|
+
extractionId: string;
|
|
44
|
+
/** The proposed value, copied through to the projected Candidate verbatim. */
|
|
45
|
+
value: TValue;
|
|
46
|
+
/** Optional proposer confidence, copied through to the projected Candidate. */
|
|
47
|
+
confidence?: number;
|
|
48
|
+
/**
|
|
49
|
+
* The Candidate Conflict comparison key. Required, and deliberately not
|
|
50
|
+
* derived from `value` by the core, so each profile controls exactly what
|
|
51
|
+
* "agrees" means for its own domain (e.g. a compound value may still be
|
|
52
|
+
* considered equivalent under a narrower key than a full deep-compare).
|
|
53
|
+
*/
|
|
54
|
+
equivalenceKey: string;
|
|
55
|
+
/**
|
|
56
|
+
* The profile's own payload, stored verbatim under
|
|
57
|
+
* {@link PRODUCER_PROPOSAL_METADATA_KEY} on the projected Candidate's
|
|
58
|
+
* `metadata`.
|
|
59
|
+
*/
|
|
60
|
+
metadata: TMetadata;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The shared Candidate Conflict rule: a group of proposals conflicts iff it
|
|
64
|
+
* carries more than one distinct `equivalenceKey`. A group of 0 or 1
|
|
65
|
+
* proposals can never conflict.
|
|
66
|
+
*/
|
|
67
|
+
export declare function hasCandidateConflict(proposals: Array<Pick<CandidateSetProposal, "equivalenceKey">>): boolean;
|
|
68
|
+
export interface ProjectProposalsToCandidateSetOptions {
|
|
69
|
+
/** The id for the projected Candidate Set. */
|
|
70
|
+
candidateSetId: string;
|
|
71
|
+
/** Optional metadata to attach to the projected Candidate Set. */
|
|
72
|
+
candidateSetMetadata?: Record<string, unknown>;
|
|
73
|
+
/**
|
|
74
|
+
* Optional rationale-builder for the projected Candidate Set, given the
|
|
75
|
+
* computed status and the input proposals.
|
|
76
|
+
*/
|
|
77
|
+
candidateSetRationale?: (status: CandidateSetStatus, proposals: CandidateSetProposal[]) => string | undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build one Candidate Set (and its Candidates) from one target's proposal
|
|
81
|
+
* group. Grouping proposals by target itself stays a caller concern — this
|
|
82
|
+
* function projects exactly one group per call; it never reaches across
|
|
83
|
+
* multiple targets on its own. `status` is `"conflict"` when
|
|
84
|
+
* {@link hasCandidateConflict} is true for `proposals`, otherwise
|
|
85
|
+
* `"needs-review"` (an empty `proposals` array yields `"needs-review"` with
|
|
86
|
+
* an empty `candidates` array). `selectedCandidateId` is left unset — both
|
|
87
|
+
* profiles compute it themselves, or not at all, per their own review flow.
|
|
88
|
+
*/
|
|
89
|
+
export declare function projectProposalsToCandidateSet<TValue = unknown, TMetadata = unknown>(target: string, proposals: Array<CandidateSetProposal<TValue, TMetadata>>, options: ProjectProposalsToCandidateSetOptions): {
|
|
90
|
+
candidateSet: CandidateSet;
|
|
91
|
+
candidates: Candidate[];
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Typed read-back of the proposal payload a Candidate carries under
|
|
95
|
+
* {@link PRODUCER_PROPOSAL_METADATA_KEY}. Returns `undefined` if the
|
|
96
|
+
* Candidate, its `metadata`, or the key itself is absent — never throws.
|
|
97
|
+
*
|
|
98
|
+
* No fallback reads of any legacy per-profile metadata key are performed
|
|
99
|
+
* (Owner decision: no legacy support).
|
|
100
|
+
*/
|
|
101
|
+
export declare function getProducerProposal<TMetadata>(candidate: Candidate | undefined): TMetadata | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Actor identity every Producer Profile's auto-accept policy uses when it
|
|
104
|
+
* accepts a proposal without human review. Shared literal — see ADR 0003
|
|
105
|
+
* §4 (the core never decides "verified"; auto-accept only ever produces
|
|
106
|
+
* "assumed" + comfort-zone true).
|
|
107
|
+
*/
|
|
108
|
+
export declare const AUTO_ACCEPT_ACTOR: "auto-accept-policy";
|
|
109
|
+
/**
|
|
110
|
+
* The comfort-zone posture every Producer Profile's auto-accept policy
|
|
111
|
+
* sets when it accepts a proposal: `withinComfortZone: true` always — an
|
|
112
|
+
* auto-accepted proposal is, by definition, one the policy's declared
|
|
113
|
+
* threshold covers, so there is nothing "outside comfort zone" about an
|
|
114
|
+
* auto-accept decision (ADR 0003 §4).
|
|
115
|
+
*/
|
|
116
|
+
export declare const AUTO_ACCEPT_WITHIN_COMFORT_ZONE: true;
|
|
117
|
+
/**
|
|
118
|
+
* The one auto-accept threshold rule every Producer Profile applies: a
|
|
119
|
+
* confidence value clears an auto-accept policy iff it is at or above
|
|
120
|
+
* (inclusive) the policy's minimum confidence.
|
|
121
|
+
*
|
|
122
|
+
* This is a low-level primitive used by {@link evaluateAutoAccept} below,
|
|
123
|
+
* which is now the single place that decides the gate/rationale/`reviewedAt`
|
|
124
|
+
* auto-accept policy for both profiles (see
|
|
125
|
+
* `docs/decisions/producer-profile.md`, "Auto-accept policy unification").
|
|
126
|
+
* What still stays entirely per-profile: output record shapes (e.g.
|
|
127
|
+
* `InquiryMapping` vs. schema-mapping's inline `ReviewOutcome`), id
|
|
128
|
+
* templates, and each profile's own selection/iteration algorithm for which
|
|
129
|
+
* candidate's evidence gets passed into that decision.
|
|
130
|
+
*/
|
|
131
|
+
export declare function meetsAutoAcceptThreshold(confidence: number, minConfidence: number): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* The accepted-candidate-shaped evidence `evaluateAutoAccept` decides over.
|
|
134
|
+
* Deliberately narrow: only the fields the auto-accept policy itself reads,
|
|
135
|
+
* not a whole proposal/candidate shape, so any profile can adapt its own
|
|
136
|
+
* proposal type into this without a dependency the other direction.
|
|
137
|
+
*/
|
|
138
|
+
export interface AutoAcceptEvidence {
|
|
139
|
+
/**
|
|
140
|
+
* The accepted evidence's OWN confidence — this is what gates AND what the
|
|
141
|
+
* composed rationale cites (owner-accepted decisions 1 and 2 in
|
|
142
|
+
* `docs/decisions/producer-profile.md`; fixes schema-mapping's pre-Slice-3
|
|
143
|
+
* group-max-gate / selected-candidate-confidence-rationale mismatch).
|
|
144
|
+
*/
|
|
145
|
+
confidence: number;
|
|
146
|
+
/**
|
|
147
|
+
* The evidence's own rationale, appended to the composed rationale when
|
|
148
|
+
* present (decision 2; mirrors inquiry-mapping's pre-existing behavior).
|
|
149
|
+
* Presence is decided with `!== undefined`, not truthiness, so an
|
|
150
|
+
* empty-string rationale is still appended.
|
|
151
|
+
*/
|
|
152
|
+
rationale?: string;
|
|
153
|
+
/**
|
|
154
|
+
* ISO 8601 timestamp of when this specific evidence was proposed (decision
|
|
155
|
+
* 3). When absent, `evaluateAutoAccept` falls back to `fallbackTimestamp`
|
|
156
|
+
* and reports that in `reviewedAtSource`.
|
|
157
|
+
*/
|
|
158
|
+
proposedAt?: string;
|
|
159
|
+
}
|
|
160
|
+
/** The auto-accept policy `evaluateAutoAccept` gates against. */
|
|
161
|
+
export interface AutoAcceptPolicy {
|
|
162
|
+
/** Minimum confidence (inclusive) a proposal must clear to auto-accept. */
|
|
163
|
+
minConfidence: number;
|
|
164
|
+
}
|
|
165
|
+
/** The unified auto-accept decision `evaluateAutoAccept` returns. */
|
|
166
|
+
export interface AutoAcceptDecision {
|
|
167
|
+
/** `true` iff there is no conflict and `evidence.confidence` clears `policy.minConfidence`. */
|
|
168
|
+
accepted: boolean;
|
|
169
|
+
/** The confidence value that was gated on (== `evidence.confidence`). */
|
|
170
|
+
confidence: number;
|
|
171
|
+
/** Composed rationale — always computed; callers only use it when `accepted`. */
|
|
172
|
+
rationale: string;
|
|
173
|
+
/** The resolved review timestamp — `evidence.proposedAt` when present, `fallbackTimestamp` otherwise. */
|
|
174
|
+
reviewedAt: string;
|
|
175
|
+
/** Which source `reviewedAt` came from. */
|
|
176
|
+
reviewedAtSource: "proposedAt" | "fallback";
|
|
177
|
+
/** Always `AUTO_ACCEPT_ACTOR`. */
|
|
178
|
+
actor: typeof AUTO_ACCEPT_ACTOR;
|
|
179
|
+
/** Always `AUTO_ACCEPT_WITHIN_COMFORT_ZONE`. */
|
|
180
|
+
withinComfortZone: typeof AUTO_ACCEPT_WITHIN_COMFORT_ZONE;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The one core auto-accept policy decision every Producer Profile delegates
|
|
184
|
+
* to, per the owner-accepted semantics recorded in
|
|
185
|
+
* `docs/decisions/producer-profile.md` ("Auto-accept policy unification"):
|
|
186
|
+
*
|
|
187
|
+
* 1. Gate on the accepted evidence's OWN confidence (not a group's), via
|
|
188
|
+
* {@link meetsAutoAcceptThreshold} — and never accept when `hasConflict`.
|
|
189
|
+
* 2. Compose a rationale citing that same gate-clearing confidence, and
|
|
190
|
+
* append `evidence.rationale` when present (`!== undefined`).
|
|
191
|
+
* 3. Stamp `reviewedAt` from `evidence.proposedAt` when present, falling
|
|
192
|
+
* back to `fallbackTimestamp` (and reporting which source was used via
|
|
193
|
+
* `reviewedAtSource`) when a profile's evidence carries no timestamp of
|
|
194
|
+
* its own.
|
|
195
|
+
* 4. Always report `actor: AUTO_ACCEPT_ACTOR` and
|
|
196
|
+
* `withinComfortZone: AUTO_ACCEPT_WITHIN_COMFORT_ZONE` (ADR 0003 §4:
|
|
197
|
+
* auto-accept only ever yields "assumed" with the comfort-zone posture).
|
|
198
|
+
*
|
|
199
|
+
* This function decides the policy only — it never renders a review outcome
|
|
200
|
+
* or claim-status record itself (ADR 0003 §4). Each profile still renders
|
|
201
|
+
* its own distinct record shape (`InquiryMapping` vs. schema-mapping's
|
|
202
|
+
* inline `ReviewOutcome`) from this decision's fields.
|
|
203
|
+
*/
|
|
204
|
+
export declare function evaluateAutoAccept(evidence: AutoAcceptEvidence, hasConflict: boolean, policy: AutoAcceptPolicy, fallbackTimestamp: string): AutoAcceptDecision;
|