@kontourai/survey 1.5.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.
@@ -18,7 +18,7 @@
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";
21
+ import { evaluateAutoAccept, getProducerProposal, hasCandidateConflict, projectProposalsToCandidateSet, } from "./producer-profile.js";
22
22
  import { reviewResourceApiVersion } from "./review-resource.js";
23
23
  // ---------------------------------------------------------------------------
24
24
  // Question normalization
@@ -152,20 +152,25 @@ export function applyAutoAcceptPolicy(proposals, policy) {
152
152
  // If proposals disagree, none can be auto-accepted
153
153
  if (hasCandidateConflict(proposals.map((p) => ({ equivalenceKey: mappingEquivalenceKey(p) }))))
154
154
  return [];
155
- return proposals
156
- .filter((p) => meetsAutoAcceptThreshold(p.confidence, policy.minConfidence))
157
- .map((proposal) => ({
158
- id: `inquiry-mapping.auto.${normalizeQuestion(proposal.question)}`,
159
- normalizedQuestion: normalizeQuestion(proposal.question),
160
- target: proposal.proposedTarget,
161
- ruleId: proposal.proposedRuleId,
162
- status: "assumed",
163
- reviewedBy: AUTO_ACCEPT_ACTOR,
164
- reviewedAt: proposal.proposedAt,
165
- rationale: `Auto-accepted: confidence ${proposal.confidence} >= threshold ${policy.minConfidence}. ${proposal.rationale}`,
166
- withinComfortZone: AUTO_ACCEPT_WITHIN_COMFORT_ZONE,
167
- proposalId: proposal.id,
168
- }));
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
+ });
169
174
  }
170
175
  // ---------------------------------------------------------------------------
171
176
  // Mapping lookup
@@ -117,18 +117,88 @@ export declare const AUTO_ACCEPT_WITHIN_COMFORT_ZONE: true;
117
117
  /**
118
118
  * The one auto-accept threshold rule every Producer Profile applies: a
119
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.
120
+ * (inclusive) the policy's minimum confidence.
125
121
  *
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.
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.
133
130
  */
134
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;
@@ -105,20 +105,54 @@ export const AUTO_ACCEPT_WITHIN_COMFORT_ZONE = true;
105
105
  /**
106
106
  * The one auto-accept threshold rule every Producer Profile applies: a
107
107
  * confidence value clears an auto-accept policy iff it is at or above
108
- * (inclusive) the policy's minimum confidence. This is the only piece of
109
- * auto-accept *mechanics* that is identical across profiles today — each
110
- * profile decides its own iteration granularity (per-proposal filter vs.
111
- * per-group max-confidence gate) and output record shape around this call;
112
- * the core does not decide that.
108
+ * (inclusive) the policy's minimum confidence.
113
109
  *
114
- * These three exports are the ONLY auto-accept semantics the two profiles
115
- * genuinely share today (see the Slice 3 plan's Part (a) field-by-field
116
- * diff table). Everything else about auto-accept — iteration granularity
117
- * (per-proposal vs. per-group), output record type and cardinality, id
118
- * templates, timestamp source, and rationale string format — diverges
119
- * between profiles and stays entirely per-profile; this module does not
120
- * decide any of it.
110
+ * This is a low-level primitive used by {@link evaluateAutoAccept} below,
111
+ * which is now the single place that decides the gate/rationale/`reviewedAt`
112
+ * auto-accept policy for both profiles (see
113
+ * `docs/decisions/producer-profile.md`, "Auto-accept policy unification").
114
+ * What still stays entirely per-profile: output record shapes (e.g.
115
+ * `InquiryMapping` vs. schema-mapping's inline `ReviewOutcome`), id
116
+ * templates, and each profile's own selection/iteration algorithm for which
117
+ * candidate's evidence gets passed into that decision.
121
118
  */
122
119
  export function meetsAutoAcceptThreshold(confidence, minConfidence) {
123
120
  return confidence >= minConfidence;
124
121
  }
122
+ /**
123
+ * The one core auto-accept policy decision every Producer Profile delegates
124
+ * to, per the owner-accepted semantics recorded in
125
+ * `docs/decisions/producer-profile.md` ("Auto-accept policy unification"):
126
+ *
127
+ * 1. Gate on the accepted evidence's OWN confidence (not a group's), via
128
+ * {@link meetsAutoAcceptThreshold} — and never accept when `hasConflict`.
129
+ * 2. Compose a rationale citing that same gate-clearing confidence, and
130
+ * append `evidence.rationale` when present (`!== undefined`).
131
+ * 3. Stamp `reviewedAt` from `evidence.proposedAt` when present, falling
132
+ * back to `fallbackTimestamp` (and reporting which source was used via
133
+ * `reviewedAtSource`) when a profile's evidence carries no timestamp of
134
+ * its own.
135
+ * 4. Always report `actor: AUTO_ACCEPT_ACTOR` and
136
+ * `withinComfortZone: AUTO_ACCEPT_WITHIN_COMFORT_ZONE` (ADR 0003 §4:
137
+ * auto-accept only ever yields "assumed" with the comfort-zone posture).
138
+ *
139
+ * This function decides the policy only — it never renders a review outcome
140
+ * or claim-status record itself (ADR 0003 §4). Each profile still renders
141
+ * its own distinct record shape (`InquiryMapping` vs. schema-mapping's
142
+ * inline `ReviewOutcome`) from this decision's fields.
143
+ */
144
+ export function evaluateAutoAccept(evidence, hasConflict, policy, fallbackTimestamp) {
145
+ const accepted = !hasConflict && meetsAutoAcceptThreshold(evidence.confidence, policy.minConfidence);
146
+ const rationale = `Auto-accepted: confidence ${evidence.confidence} >= threshold ${policy.minConfidence}.` +
147
+ (evidence.rationale !== undefined ? ` ${evidence.rationale}` : "");
148
+ const reviewedAt = evidence.proposedAt ?? fallbackTimestamp;
149
+ return {
150
+ accepted,
151
+ confidence: evidence.confidence,
152
+ rationale,
153
+ reviewedAt,
154
+ reviewedAtSource: evidence.proposedAt !== undefined ? "proposedAt" : "fallback",
155
+ actor: AUTO_ACCEPT_ACTOR,
156
+ withinComfortZone: AUTO_ACCEPT_WITHIN_COMFORT_ZONE,
157
+ };
158
+ }
@@ -20,7 +20,7 @@
20
20
  * so that resolveInquiry can resolve across systems with weakest-link
21
21
  * capping.
22
22
  */
23
- import { AUTO_ACCEPT_ACTOR, AUTO_ACCEPT_WITHIN_COMFORT_ZONE, getProducerProposal, meetsAutoAcceptThreshold, projectProposalsToCandidateSet, } from "./producer-profile.js";
23
+ import { evaluateAutoAccept, getProducerProposal, projectProposalsToCandidateSet, } from "./producer-profile.js";
24
24
  import { buildSurveyTrustBundle } from "./to-surface.js";
25
25
  // ---------------------------------------------------------------------------
26
26
  // Canonical pair key
@@ -156,29 +156,42 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
156
156
  });
157
157
  candidateSet.selectedCandidateId = candidateSet.status !== "conflict" ? candidates[0]?.id : undefined;
158
158
  candidateSets.push(candidateSet);
159
- // Auto-accept policy: non-conflicting proposals above threshold → assumed
160
- let autoReviewStatus;
161
- if (candidateSet.status !== "conflict" && options.autoAcceptMinConfidence !== undefined) {
162
- const topConfidence = Math.max(...pairProposals.map((p) => p.confidence));
163
- if (meetsAutoAcceptThreshold(topConfidence, options.autoAcceptMinConfidence)) {
164
- autoReviewStatus = "assumed";
165
- }
166
- }
159
+ // Auto-accept policy: delegate the gate/rationale/reviewedAt decision to
160
+ // the core (docs/decisions/producer-profile.md — gates on the SELECTED
161
+ // candidate's own confidence, not the group's; keeps the existing
162
+ // candidates[0]-based selection algorithm unchanged, out of scope here).
167
163
  const selectedCandidate = candidateSet.selectedCandidateId
168
164
  ? candidates.find((c) => c.id === candidateSet.selectedCandidateId)
169
165
  : candidates[0];
170
- if (autoReviewStatus && selectedCandidate) {
171
- const reviewId = `schema-mapping.review.${pairKey}`;
172
- reviewOutcomes.push({
173
- id: reviewId,
174
- candidateSetId,
175
- candidateId: selectedCandidate.id,
176
- status: autoReviewStatus,
177
- actor: AUTO_ACCEPT_ACTOR,
178
- reviewedAt: generatedAt,
179
- rationale: `Auto-accepted: confidence ${selectedCandidate.confidence} >= threshold ${options.autoAcceptMinConfidence}`,
180
- withinComfortZone: AUTO_ACCEPT_WITHIN_COMFORT_ZONE,
181
- });
166
+ const hasConflict = candidateSet.status === "conflict";
167
+ if (!hasConflict && options.autoAcceptMinConfidence !== undefined && selectedCandidate) {
168
+ const selectedProposal = getProducerProposal(selectedCandidate);
169
+ const decision = evaluateAutoAccept({
170
+ // Fail-closed fallback: Candidate.confidence is `number | undefined`
171
+ // (src/types.ts), but no conforming SchemaMappingExtractor can
172
+ // produce an undefined confidence today (MappingProposalRecord.confidence
173
+ // is required and is copied straight through to Candidate.confidence
174
+ // at construction), so this is unreachable through the typed contract.
175
+ // NEGATIVE_INFINITY (not 0) guarantees rejection regardless of
176
+ // autoAcceptMinConfidence's sign, matching old code's NaN-poisoning
177
+ // behavior of never auto-accepting on a missing confidence.
178
+ confidence: selectedCandidate.confidence ?? Number.NEGATIVE_INFINITY,
179
+ rationale: selectedProposal?.rationale,
180
+ proposedAt: selectedProposal?.proposedAt,
181
+ }, hasConflict, { minConfidence: options.autoAcceptMinConfidence }, generatedAt);
182
+ if (decision.accepted) {
183
+ const reviewId = `schema-mapping.review.${pairKey}`;
184
+ reviewOutcomes.push({
185
+ id: reviewId,
186
+ candidateSetId,
187
+ candidateId: selectedCandidate.id,
188
+ status: "assumed",
189
+ actor: decision.actor,
190
+ reviewedAt: decision.reviewedAt,
191
+ rationale: decision.rationale,
192
+ withinComfortZone: decision.withinComfortZone,
193
+ });
194
+ }
182
195
  }
183
196
  // Project to ClaimTarget (subjectType "system-field", fieldOrBehavior "maps-to")
184
197
  if (selectedCandidate) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Producer-side source, extraction, candidate, and review contracts for projecting verified claims into Surface.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",