@kontourai/survey 1.3.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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/example-data/corrected-document-candidates.js +2 -2
  3. package/dist/example-data/public-directory-review-resource.d.ts +2 -2
  4. package/dist/example-data/public-directory-review-resource.js +2 -2
  5. package/dist/example-data/public-field-review.js +2 -2
  6. package/dist/example-data/regulated-document-review-resource.d.ts +2 -2
  7. package/dist/example-data/regulated-document-review-resource.js +2 -2
  8. package/dist/examples/public-field-observation.js +1 -1
  9. package/dist/examples/review-workbench/downstream-public-directory-adapter.js +1 -1
  10. package/dist/src/agent-utterance.d.ts +25 -0
  11. package/dist/src/agent-utterance.js +207 -95
  12. package/dist/src/console/review-console-server.js +3 -7
  13. package/dist/src/field-observation.d.ts +2 -16
  14. package/dist/src/field-observation.js +2 -14
  15. package/dist/src/index.d.ts +1 -1
  16. package/dist/src/inquiry-mapping.d.ts +2 -46
  17. package/dist/src/inquiry-mapping.js +37 -39
  18. package/dist/src/mcp/review-mcp.js +12 -12
  19. package/dist/src/observation-helper.d.ts +47 -1
  20. package/dist/src/observation-helper.js +41 -2
  21. package/dist/src/oversight-metrics.d.ts +2 -2
  22. package/dist/src/oversight-metrics.js +1 -1
  23. package/dist/src/producer-discipline.d.ts +40 -0
  24. package/dist/src/producer-discipline.js +13 -0
  25. package/dist/src/producer-profile.d.ts +134 -0
  26. package/dist/src/producer-profile.js +124 -0
  27. package/dist/src/raw-source.d.ts +18 -0
  28. package/dist/src/raw-source.js +28 -13
  29. package/dist/src/repeated-observation.d.ts +2 -16
  30. package/dist/src/repeated-observation.js +2 -14
  31. package/dist/src/review-proof.d.ts +2 -2
  32. package/dist/src/review-proof.js +2 -2
  33. package/dist/src/review-resource.d.ts +5 -1
  34. package/dist/src/review-workbench/review-workbench-data.d.ts +10 -10
  35. package/dist/src/review-workbench/review-workbench-data.js +6 -6
  36. package/dist/src/review-workbench/review-workbench.js +1 -1
  37. package/dist/src/review-workbench/server-review-session.d.ts +14 -1
  38. package/dist/src/review-workbench/server-review-session.js +16 -0
  39. package/dist/src/schema-mapping.js +32 -38
  40. package/dist/src/source-of-authority-observation.js +6 -9
  41. package/dist/src/to-surface.js +8 -11
  42. package/dist/src/types.d.ts +6 -1
  43. package/dist/src/vocabulary.d.ts +46 -4
  44. package/dist/src/vocabulary.js +29 -3
  45. package/package.json +6 -3
@@ -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
- }>): Array<{
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
- surface: 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
- // Proposal → CandidateSet projection
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 candidates = proposals.map((proposal) => ({
64
- id: `mapping-candidate.${proposal.id}`,
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
- mappingProposal: {
70
- proposalId: proposal.id,
71
- proposedTarget: proposal.proposedTarget,
72
- proposedRuleId: proposal.proposedRuleId,
73
- confidence: proposal.confidence,
74
- rationale: proposal.rationale,
75
- excerpt: proposal.excerpt,
76
- proposedBy: proposal.proposedBy,
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
- // Determine status: conflict if proposals disagree on target/rule
82
- const status = proposals.length > 1 && proposalsDisagree(proposals) ? "conflict" : "needs-review";
83
- const candidateSet = {
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.metadata?.mappingProposal;
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.length > 1 && proposalsDisagree(proposals))
153
+ if (hasCandidateConflict(proposals.map((p) => ({ equivalenceKey: mappingEquivalenceKey(p) }))))
156
154
  return [];
157
155
  return proposals
158
- .filter((p) => p.confidence >= policy.minConfidence)
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: "auto-accept-policy",
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: true,
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: "survey.kontourai.io/v1alpha1",
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.metadata?.mappingProposal;
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"}`;
@@ -299,7 +297,7 @@ export function buildMappingReviewItems(candidateSets) {
299
297
  claimTarget: {
300
298
  subjectType: meta?.proposedTarget?.subjectType ?? "inquiry",
301
299
  subjectId: meta?.proposedTarget?.subjectId ?? candidateSet.target,
302
- surface: "inquiry.mapping",
300
+ facet: "inquiry.mapping",
303
301
  claimType: "inquiry-mapping",
304
302
  fieldOrBehavior: meta?.proposedTarget?.fieldOrBehavior ?? meta?.proposedRuleId ?? "unknown",
305
303
  impactLevel: "low",
@@ -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, replayReviewSessionEvents, reviewSessionSummary, workbenchDecisionDefinitions, } from "../review-workbench/review-workbench.js";
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 = events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot;
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 = events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot;
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, "&quot;");
113
113
  }
114
114
  function buildReviewCardHtml(item, snapshot, events) {
115
- const current = events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot;
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 = events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot;
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(events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot),
306
- activeItemName: (events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot).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(events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot);
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 = events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot;
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 = events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot;
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 = events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot;
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
- export interface BuildObservationInput<TValue> {
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
- function mergeObservationMetadata(claimMetadata, metadata, surveyMetadata) {
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),
@@ -113,8 +113,8 @@ export interface OversightMetricsClaimsSubject {
113
113
  readonly subjectType: string;
114
114
  /** Surface subject id (e.g. a session name or actor id). */
115
115
  readonly subjectId: string;
116
- /** Surface name (e.g. "review.oversight"). */
117
- readonly surface: string;
116
+ /** Surface facet (e.g. "review.oversight"). */
117
+ readonly facet: string;
118
118
  /** Actor id to record on events. */
119
119
  readonly actor: string;
120
120
  /** ISO 8601 timestamp for claim created/updated times. */
@@ -217,7 +217,7 @@ export function oversightMetricsToClaims(metrics, subject) {
217
217
  id: claimId,
218
218
  subjectType: subject.subjectType,
219
219
  subjectId: subject.subjectId,
220
- surface: subject.surface,
220
+ facet: subject.facet,
221
221
  claimType: "oversight-quality",
222
222
  fieldOrBehavior,
223
223
  value,
@@ -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,134 @@
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. This is the only piece of
121
+ * auto-accept *mechanics* that is identical across profiles today — each
122
+ * profile decides its own iteration granularity (per-proposal filter vs.
123
+ * per-group max-confidence gate) and output record shape around this call;
124
+ * the core does not decide that.
125
+ *
126
+ * These three exports are the ONLY auto-accept semantics the two profiles
127
+ * genuinely share today (see the Slice 3 plan's Part (a) field-by-field
128
+ * diff table). Everything else about auto-accept — iteration granularity
129
+ * (per-proposal vs. per-group), output record type and cardinality, id
130
+ * templates, timestamp source, and rationale string format — diverges
131
+ * between profiles and stays entirely per-profile; this module does not
132
+ * decide any of it.
133
+ */
134
+ export declare function meetsAutoAcceptThreshold(confidence: number, minConfidence: number): boolean;