@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
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Canonical proposal-metadata key
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* The one canonical `Candidate.metadata` key every Producer Profile uses to
|
|
26
|
+
* carry its profile-specific proposal payload. Replaces the per-profile keys
|
|
27
|
+
* (`mappingProposal`, `schemaMappingProposal`) each profile used before
|
|
28
|
+
* adopting this core module.
|
|
29
|
+
*/
|
|
30
|
+
export const PRODUCER_PROPOSAL_METADATA_KEY = "producerProposal";
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Candidate Conflict rule
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* The shared Candidate Conflict rule: a group of proposals conflicts iff it
|
|
36
|
+
* carries more than one distinct `equivalenceKey`. A group of 0 or 1
|
|
37
|
+
* proposals can never conflict.
|
|
38
|
+
*/
|
|
39
|
+
export function hasCandidateConflict(proposals) {
|
|
40
|
+
return new Set(proposals.map((p) => p.equivalenceKey)).size > 1;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build one Candidate Set (and its Candidates) from one target's proposal
|
|
44
|
+
* group. Grouping proposals by target itself stays a caller concern — this
|
|
45
|
+
* function projects exactly one group per call; it never reaches across
|
|
46
|
+
* multiple targets on its own. `status` is `"conflict"` when
|
|
47
|
+
* {@link hasCandidateConflict} is true for `proposals`, otherwise
|
|
48
|
+
* `"needs-review"` (an empty `proposals` array yields `"needs-review"` with
|
|
49
|
+
* an empty `candidates` array). `selectedCandidateId` is left unset — both
|
|
50
|
+
* profiles compute it themselves, or not at all, per their own review flow.
|
|
51
|
+
*/
|
|
52
|
+
export function projectProposalsToCandidateSet(target, proposals, options) {
|
|
53
|
+
const candidates = proposals.map((proposal) => ({
|
|
54
|
+
id: proposal.candidateId,
|
|
55
|
+
extractionId: proposal.extractionId,
|
|
56
|
+
value: proposal.value,
|
|
57
|
+
confidence: proposal.confidence,
|
|
58
|
+
metadata: {
|
|
59
|
+
[PRODUCER_PROPOSAL_METADATA_KEY]: proposal.metadata,
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
const status = hasCandidateConflict(proposals) ? "conflict" : "needs-review";
|
|
63
|
+
const candidateSet = {
|
|
64
|
+
id: options.candidateSetId,
|
|
65
|
+
target,
|
|
66
|
+
candidates,
|
|
67
|
+
status,
|
|
68
|
+
rationale: options.candidateSetRationale?.(status, proposals),
|
|
69
|
+
metadata: options.candidateSetMetadata,
|
|
70
|
+
};
|
|
71
|
+
return { candidateSet, candidates };
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Typed proposal-metadata accessor
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* Typed read-back of the proposal payload a Candidate carries under
|
|
78
|
+
* {@link PRODUCER_PROPOSAL_METADATA_KEY}. Returns `undefined` if the
|
|
79
|
+
* Candidate, its `metadata`, or the key itself is absent — never throws.
|
|
80
|
+
*
|
|
81
|
+
* No fallback reads of any legacy per-profile metadata key are performed
|
|
82
|
+
* (Owner decision: no legacy support).
|
|
83
|
+
*/
|
|
84
|
+
export function getProducerProposal(candidate) {
|
|
85
|
+
return candidate?.metadata?.[PRODUCER_PROPOSAL_METADATA_KEY];
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Shared auto-accept primitives
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
/**
|
|
91
|
+
* Actor identity every Producer Profile's auto-accept policy uses when it
|
|
92
|
+
* accepts a proposal without human review. Shared literal — see ADR 0003
|
|
93
|
+
* §4 (the core never decides "verified"; auto-accept only ever produces
|
|
94
|
+
* "assumed" + comfort-zone true).
|
|
95
|
+
*/
|
|
96
|
+
export const AUTO_ACCEPT_ACTOR = "auto-accept-policy";
|
|
97
|
+
/**
|
|
98
|
+
* The comfort-zone posture every Producer Profile's auto-accept policy
|
|
99
|
+
* sets when it accepts a proposal: `withinComfortZone: true` always — an
|
|
100
|
+
* auto-accepted proposal is, by definition, one the policy's declared
|
|
101
|
+
* threshold covers, so there is nothing "outside comfort zone" about an
|
|
102
|
+
* auto-accept decision (ADR 0003 §4).
|
|
103
|
+
*/
|
|
104
|
+
export const AUTO_ACCEPT_WITHIN_COMFORT_ZONE = true;
|
|
105
|
+
/**
|
|
106
|
+
* The one auto-accept threshold rule every Producer Profile applies: a
|
|
107
|
+
* confidence value clears an auto-accept policy iff it is at or above
|
|
108
|
+
* (inclusive) the policy's minimum confidence.
|
|
109
|
+
*
|
|
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.
|
|
118
|
+
*/
|
|
119
|
+
export function meetsAutoAcceptThreshold(confidence, minConfidence) {
|
|
120
|
+
return confidence >= minConfidence;
|
|
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
|
+
}
|
package/dist/src/raw-source.d.ts
CHANGED
|
@@ -39,6 +39,24 @@ export interface PolicyStandardSourceInput extends Omit<RawSourceInput, "locator
|
|
|
39
39
|
locatorScheme?: LocatorScheme;
|
|
40
40
|
metadata?: Record<string, unknown>;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Raw Source kinds whose factories accept a caller-supplied `locatorScheme`
|
|
44
|
+
* override and otherwise fall back to a per-kind default. `uploaded-document`
|
|
45
|
+
* has no default (`undefined`) because `UploadedDocumentSourceInput.locatorScheme`
|
|
46
|
+
* is required, not optional — modeled here as a real, explicit table entry
|
|
47
|
+
* rather than an omission, so a caller-supplied value is the only source of
|
|
48
|
+
* truth for that kind.
|
|
49
|
+
*
|
|
50
|
+
* Module-internal seam: consumed by relative import from
|
|
51
|
+
* `rawSourceWithDefaultLocatorScheme` below and by
|
|
52
|
+
* `tests/raw-source-defaults.test.ts`, NOT re-exported from `src/index.ts`.
|
|
53
|
+
*/
|
|
54
|
+
export declare const DEFAULT_LOCATOR_SCHEME: {
|
|
55
|
+
"uploaded-document": undefined;
|
|
56
|
+
"api-record": LocatorScheme;
|
|
57
|
+
"web-page": LocatorScheme;
|
|
58
|
+
"manual-entry": LocatorScheme;
|
|
59
|
+
};
|
|
42
60
|
export declare function uploadedDocumentSource(input: UploadedDocumentSourceInput): RawSource;
|
|
43
61
|
export declare function apiRecordSource(input: ApiRecordSourceInput): RawSource;
|
|
44
62
|
export declare function webPageSource(input: WebPageSourceInput): RawSource;
|
package/dist/src/raw-source.js
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw Source kinds whose factories accept a caller-supplied `locatorScheme`
|
|
3
|
+
* override and otherwise fall back to a per-kind default. `uploaded-document`
|
|
4
|
+
* has no default (`undefined`) because `UploadedDocumentSourceInput.locatorScheme`
|
|
5
|
+
* is required, not optional — modeled here as a real, explicit table entry
|
|
6
|
+
* rather than an omission, so a caller-supplied value is the only source of
|
|
7
|
+
* truth for that kind.
|
|
8
|
+
*
|
|
9
|
+
* Module-internal seam: consumed by relative import from
|
|
10
|
+
* `rawSourceWithDefaultLocatorScheme` below and by
|
|
11
|
+
* `tests/raw-source-defaults.test.ts`, NOT re-exported from `src/index.ts`.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_LOCATOR_SCHEME = {
|
|
14
|
+
"uploaded-document": undefined,
|
|
15
|
+
"api-record": "structured-field",
|
|
16
|
+
"web-page": "html",
|
|
17
|
+
"manual-entry": "structured-field",
|
|
18
|
+
};
|
|
19
|
+
function rawSourceWithDefaultLocatorScheme(kind, input) {
|
|
20
|
+
return rawSource(kind, {
|
|
21
|
+
locatorScheme: DEFAULT_LOCATOR_SCHEME[kind],
|
|
22
|
+
...input,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
1
25
|
export function uploadedDocumentSource(input) {
|
|
2
|
-
return
|
|
26
|
+
return rawSourceWithDefaultLocatorScheme("uploaded-document", input);
|
|
3
27
|
}
|
|
4
28
|
export function apiRecordSource(input) {
|
|
5
|
-
return
|
|
6
|
-
locatorScheme: "structured-field",
|
|
7
|
-
...input,
|
|
8
|
-
});
|
|
29
|
+
return rawSourceWithDefaultLocatorScheme("api-record", input);
|
|
9
30
|
}
|
|
10
31
|
export function webPageSource(input) {
|
|
11
|
-
return
|
|
12
|
-
locatorScheme: "html",
|
|
13
|
-
...input,
|
|
14
|
-
});
|
|
32
|
+
return rawSourceWithDefaultLocatorScheme("web-page", input);
|
|
15
33
|
}
|
|
16
34
|
export function manualEntrySource(input) {
|
|
17
|
-
return
|
|
18
|
-
locatorScheme: "structured-field",
|
|
19
|
-
...input,
|
|
20
|
-
});
|
|
35
|
+
return rawSourceWithDefaultLocatorScheme("manual-entry", input);
|
|
21
36
|
}
|
|
22
37
|
export function policyStandardSource(input) {
|
|
23
38
|
const policyStandard = {
|
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
import type { SurveyObservationInput } from "./builder.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
field: string;
|
|
5
|
-
value: readonly TItem[];
|
|
6
|
-
rawSource: SurveyObservationInput["rawSource"];
|
|
7
|
-
extraction: Omit<SurveyObservationInput["extraction"], "target" | "value" | "excerpt"> & {
|
|
8
|
-
target?: string;
|
|
9
|
-
excerpt?: string | null;
|
|
10
|
-
};
|
|
11
|
-
reviewOutcome?: SurveyObservationInput["reviewOutcome"];
|
|
12
|
-
claim: Omit<SurveyObservationInput["claim"], "fieldOrBehavior" | "value"> & {
|
|
13
|
-
fieldOrBehavior?: string;
|
|
14
|
-
};
|
|
15
|
-
candidate?: SurveyObservationInput["candidate"];
|
|
16
|
-
candidateSet?: SurveyObservationInput["candidateSet"];
|
|
2
|
+
import { type ObservationAuthoringInput } from "./observation-helper.js";
|
|
3
|
+
export interface RepeatedObservationInput<TItem> extends ObservationAuthoringInput<readonly TItem[]> {
|
|
17
4
|
representation?: "aggregate-array";
|
|
18
|
-
metadata?: Record<string, unknown>;
|
|
19
5
|
}
|
|
20
6
|
export declare function repeatedObservation<TItem>(input: RepeatedObservationInput<TItem>): SurveyObservationInput;
|
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildRepeatedObservation } from "./observation-helper.js";
|
|
2
2
|
export function repeatedObservation(input) {
|
|
3
|
-
|
|
4
|
-
const value = [...input.value];
|
|
5
|
-
return buildObservation({
|
|
6
|
-
...input,
|
|
7
|
-
value,
|
|
8
|
-
surveyMetadata: {
|
|
9
|
-
repeated: {
|
|
10
|
-
representation,
|
|
11
|
-
itemCount: value.length,
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
defaultExcerpt: `${input.field}: ${value.length} item(s)`,
|
|
15
|
-
});
|
|
3
|
+
return buildRepeatedObservation(input);
|
|
16
4
|
}
|
|
@@ -2,7 +2,7 @@ import { type DeriveReviewSessionApplyResultForSnapshotResult, type MapReviewWor
|
|
|
2
2
|
import { type ReviewDecisionModeIssue } from "./producer-decision-mode.js";
|
|
3
3
|
import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
|
|
4
4
|
import type { ReviewDecision, ReviewSessionEvent } from "../review-resource.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type ReviewQueueSessionState } from "./review-queue-session.js";
|
|
6
6
|
export interface ServerReviewSessionRecord {
|
|
7
7
|
readonly sessionName: string;
|
|
8
8
|
readonly snapshot: ReviewQueueSessionState;
|
|
@@ -59,6 +59,19 @@ export interface DeriveServerReviewSessionApplyResultOptions {
|
|
|
59
59
|
}
|
|
60
60
|
export declare function createServerReviewSessionRecord(options: CreateServerReviewSessionRecordOptions): ServerReviewSessionRecord;
|
|
61
61
|
export declare function hashReviewSessionSnapshot(snapshot: ReviewQueueSessionState): string;
|
|
62
|
+
/**
|
|
63
|
+
* Project the current review session state from a snapshot plus its
|
|
64
|
+
* append-only event log: replay events over the snapshot when any exist,
|
|
65
|
+
* otherwise return the snapshot itself unchanged (same object reference —
|
|
66
|
+
* no eager replay, no copy). Collapses the
|
|
67
|
+
* `events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot`
|
|
68
|
+
* conditional that was duplicated across the MCP and console review
|
|
69
|
+
* adapters (src/mcp/review-mcp.ts, src/console/review-console-server.ts) —
|
|
70
|
+
* same shape of fix as canonicalJson in ./canonical.ts, which was
|
|
71
|
+
* extracted after two near-identical copies of a derivation desynced
|
|
72
|
+
* (audit 2026-06-28, ops#24).
|
|
73
|
+
*/
|
|
74
|
+
export declare function currentSessionState(snapshot: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): ReviewQueueSessionState;
|
|
62
75
|
export declare function compareServerReviewSessionFreshness(record: ServerReviewSessionRecord, snapshot: ReviewQueueSessionState, eventCount?: number): ServerReviewSessionFreshnessComparison;
|
|
63
76
|
export declare function assertServerReviewSessionFreshness(record: ServerReviewSessionRecord, snapshot: ReviewQueueSessionState, eventCount?: number): void;
|
|
64
77
|
export declare function validateServerReviewSessionEvents(record: ServerReviewSessionRecord, events: readonly ReviewSessionEvent[]): ServerReviewSessionEventValidationIssue[];
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { deriveReviewSessionApplyResultForSnapshot, mapReviewWorkbenchResultsToApplyActions, ReviewApplyActionMappingError, } from "./review-workbench.js";
|
|
3
3
|
import { validateReviewDecisionMode, } from "./producer-decision-mode.js";
|
|
4
4
|
import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
5
|
+
import { replayReviewSessionEvents } from "./review-queue-session.js";
|
|
5
6
|
import { canonicalJson } from "./canonical.js";
|
|
6
7
|
export class StaleServerReviewSessionError extends Error {
|
|
7
8
|
name = "StaleServerReviewSessionError";
|
|
@@ -34,6 +35,21 @@ export function createServerReviewSessionRecord(options) {
|
|
|
34
35
|
export function hashReviewSessionSnapshot(snapshot) {
|
|
35
36
|
return sha256(canonicalJson(snapshot));
|
|
36
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Project the current review session state from a snapshot plus its
|
|
40
|
+
* append-only event log: replay events over the snapshot when any exist,
|
|
41
|
+
* otherwise return the snapshot itself unchanged (same object reference —
|
|
42
|
+
* no eager replay, no copy). Collapses the
|
|
43
|
+
* `events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot`
|
|
44
|
+
* conditional that was duplicated across the MCP and console review
|
|
45
|
+
* adapters (src/mcp/review-mcp.ts, src/console/review-console-server.ts) —
|
|
46
|
+
* same shape of fix as canonicalJson in ./canonical.ts, which was
|
|
47
|
+
* extracted after two near-identical copies of a derivation desynced
|
|
48
|
+
* (audit 2026-06-28, ops#24).
|
|
49
|
+
*/
|
|
50
|
+
export function currentSessionState(snapshot, events) {
|
|
51
|
+
return events.length > 0 ? replayReviewSessionEvents(snapshot, events) : snapshot;
|
|
52
|
+
}
|
|
37
53
|
export function compareServerReviewSessionFreshness(record, snapshot, eventCount) {
|
|
38
54
|
const actualSnapshotHash = hashReviewSessionSnapshot(snapshot);
|
|
39
55
|
const eventCountMatches = record.eventCount === undefined || eventCount === undefined || record.eventCount === eventCount;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* so that resolveInquiry can resolve across systems with weakest-link
|
|
21
21
|
* capping.
|
|
22
22
|
*/
|
|
23
|
+
import { evaluateAutoAccept, getProducerProposal, projectProposalsToCandidateSet, } from "./producer-profile.js";
|
|
23
24
|
import { buildSurveyTrustBundle } from "./to-surface.js";
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// Canonical pair key
|
|
@@ -84,10 +85,7 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
|
|
|
84
85
|
const subjectId = mappingSubjectId(first.sourceField, first.targetField);
|
|
85
86
|
const candidateSetId = `schema-mapping.candidate-set.${pairKey}`;
|
|
86
87
|
const claimId = `schema-mapping.claim.${pairKey}`;
|
|
87
|
-
|
|
88
|
-
const relations = new Set(pairProposals.map((p) => p.relation));
|
|
89
|
-
const status = relations.size > 1 ? "conflict" : "needs-review";
|
|
90
|
-
const candidates = pairProposals.map((proposal) => {
|
|
88
|
+
const candidateSetProposals = pairProposals.map((proposal) => {
|
|
91
89
|
// Use the source system's RawSource for this extraction
|
|
92
90
|
const rawSource = sourceById.get(proposal.sourceField.system) ?? rawSources[0];
|
|
93
91
|
const extractionId = `schema-mapping.extraction.${proposal.id}`;
|
|
@@ -120,7 +118,7 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
|
|
|
120
118
|
};
|
|
121
119
|
extractions.push(extraction);
|
|
122
120
|
return {
|
|
123
|
-
|
|
121
|
+
candidateId: `schema-mapping.candidate.${proposal.id}`,
|
|
124
122
|
extractionId,
|
|
125
123
|
value: {
|
|
126
124
|
relation: proposal.relation,
|
|
@@ -128,70 +126,79 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
|
|
|
128
126
|
conversion: proposal.conversion,
|
|
129
127
|
},
|
|
130
128
|
confidence: proposal.confidence,
|
|
129
|
+
equivalenceKey: proposal.relation,
|
|
131
130
|
metadata: {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
proposedAt: proposal.proposedAt,
|
|
143
|
-
},
|
|
131
|
+
proposalId: proposal.id,
|
|
132
|
+
sourceField: proposal.sourceField,
|
|
133
|
+
targetField: proposal.targetField,
|
|
134
|
+
relation: proposal.relation,
|
|
135
|
+
conversion: proposal.conversion,
|
|
136
|
+
evidence: proposal.evidence,
|
|
137
|
+
confidence: proposal.confidence,
|
|
138
|
+
rationale: proposal.rationale,
|
|
139
|
+
proposedBy: proposal.proposedBy,
|
|
140
|
+
proposedAt: proposal.proposedAt,
|
|
144
141
|
},
|
|
145
142
|
};
|
|
146
143
|
});
|
|
147
|
-
const candidateSet = {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
candidates,
|
|
151
|
-
selectedCandidateId: status !== "conflict" ? candidates[0]?.id : undefined,
|
|
152
|
-
status,
|
|
153
|
-
rationale: status === "conflict"
|
|
154
|
-
? `Proposals disagree on relation for pair ${pairKey}: ${[...relations].join(", ")}`
|
|
155
|
-
: `${candidates.length} proposal(s) agree on relation "${first.relation}" for pair ${pairKey}.`,
|
|
156
|
-
metadata: {
|
|
144
|
+
const { candidateSet, candidates } = projectProposalsToCandidateSet(`schema-mapping:${pairKey}`, candidateSetProposals, {
|
|
145
|
+
candidateSetId,
|
|
146
|
+
candidateSetMetadata: {
|
|
157
147
|
schemaMapping: {
|
|
158
148
|
pairKey,
|
|
159
149
|
sourceField: first.sourceField,
|
|
160
150
|
targetField: first.targetField,
|
|
161
151
|
},
|
|
162
152
|
},
|
|
163
|
-
|
|
153
|
+
candidateSetRationale: (status, proposals) => status === "conflict"
|
|
154
|
+
? `Proposals disagree on relation for pair ${pairKey}: ${[...new Set(proposals.map((p) => p.equivalenceKey))].join(", ")}`
|
|
155
|
+
: `${proposals.length} proposal(s) agree on relation "${first.relation}" for pair ${pairKey}.`,
|
|
156
|
+
});
|
|
157
|
+
candidateSet.selectedCandidateId = candidateSet.status !== "conflict" ? candidates[0]?.id : undefined;
|
|
164
158
|
candidateSets.push(candidateSet);
|
|
165
|
-
// Auto-accept policy:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (topConfidence >= options.autoAcceptMinConfidence) {
|
|
170
|
-
autoReviewStatus = "assumed";
|
|
171
|
-
}
|
|
172
|
-
}
|
|
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).
|
|
173
163
|
const selectedCandidate = candidateSet.selectedCandidateId
|
|
174
164
|
? candidates.find((c) => c.id === candidateSet.selectedCandidateId)
|
|
175
165
|
: candidates[0];
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
}
|
|
188
195
|
}
|
|
189
196
|
// Project to ClaimTarget (subjectType "system-field", fieldOrBehavior "maps-to")
|
|
190
197
|
if (selectedCandidate) {
|
|
191
198
|
const review = reviewOutcomes.find((r) => r.candidateSetId === candidateSetId);
|
|
192
199
|
const claimStatus = review
|
|
193
200
|
? review.status
|
|
194
|
-
: (status === "conflict" ? "disputed" : undefined);
|
|
201
|
+
: (candidateSet.status === "conflict" ? "disputed" : undefined);
|
|
195
202
|
const claimTarget = {
|
|
196
203
|
id: claimId,
|
|
197
204
|
candidateSetId,
|
|
@@ -266,7 +273,7 @@ export function mappingReviewToSurface(reviewedMappings, options = {}) {
|
|
|
266
273
|
const rawSources = [];
|
|
267
274
|
const seenSources = new Set();
|
|
268
275
|
for (const rm of accepted) {
|
|
269
|
-
const meta = rm.selectedCandidate
|
|
276
|
+
const meta = getProducerProposal(rm.selectedCandidate);
|
|
270
277
|
const sourceField = meta?.sourceField ?? rm.proposal.sourceField;
|
|
271
278
|
const sourceId = `schema-mapping.source.${sourceField.system}`;
|
|
272
279
|
if (!seenSources.has(sourceId)) {
|
|
@@ -290,7 +297,7 @@ export function mappingReviewToSurface(reviewedMappings, options = {}) {
|
|
|
290
297
|
const reviewOutcomes = [];
|
|
291
298
|
const claims = [];
|
|
292
299
|
for (const rm of accepted) {
|
|
293
|
-
const meta = rm.selectedCandidate
|
|
300
|
+
const meta = getProducerProposal(rm.selectedCandidate);
|
|
294
301
|
const sourceField = meta?.sourceField ?? rm.proposal.sourceField;
|
|
295
302
|
const targetField = meta?.targetField ?? rm.proposal.targetField;
|
|
296
303
|
const relation = (meta?.relation ?? rm.proposal.relation);
|
|
@@ -379,7 +386,7 @@ export function mappingReviewToSurface(reviewedMappings, options = {}) {
|
|
|
379
386
|
// subject and back-references the mapping claim via mappingClaimId.
|
|
380
387
|
const identityLinks = [];
|
|
381
388
|
for (const rm of accepted) {
|
|
382
|
-
const meta = rm.selectedCandidate
|
|
389
|
+
const meta = getProducerProposal(rm.selectedCandidate);
|
|
383
390
|
const sourceField = meta?.sourceField ?? rm.proposal.sourceField;
|
|
384
391
|
const targetField = meta?.targetField ?? rm.proposal.targetField;
|
|
385
392
|
const relation = (meta?.relation ?? rm.proposal.relation);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildObservation } from "./observation-helper.js";
|
|
2
|
+
import { assertReviewOutcomeDiscipline } from "./producer-discipline.js";
|
|
2
3
|
export class SourceOfAuthorityObservationBuilder {
|
|
3
4
|
state;
|
|
4
5
|
constructor(args) {
|
|
@@ -115,15 +116,11 @@ function assertVerifiedPosture(input) {
|
|
|
115
116
|
if (!input.extraction.locator) {
|
|
116
117
|
throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without a source locator`);
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
if (!input.reviewOutcome.reviewedAt) {
|
|
125
|
-
throw new Error(`Source-of-authority observation ${input.id} cannot be ${status} without reviewedAt`);
|
|
126
|
-
}
|
|
119
|
+
assertReviewOutcomeDiscipline({
|
|
120
|
+
subject: `Source-of-authority observation ${input.id}`,
|
|
121
|
+
status,
|
|
122
|
+
review: input.reviewOutcome,
|
|
123
|
+
});
|
|
127
124
|
}
|
|
128
125
|
function claimStatus(claimStatusValue, reviewStatus) {
|
|
129
126
|
return claimStatusValue ?? reviewStatus;
|
package/dist/src/to-surface.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildReviewProofAnchor } from "./review-proof.js";
|
|
2
|
+
import { assertReviewOutcomeDiscipline } from "./producer-discipline.js";
|
|
2
3
|
export function buildSurveyTrustBundle(input, options = {}) {
|
|
3
4
|
const rawSources = indexById(input.rawSources, "raw source");
|
|
4
5
|
const extractions = indexById(input.extractions, "extraction");
|
|
@@ -334,15 +335,11 @@ function statusFor(input) {
|
|
|
334
335
|
return "proposed";
|
|
335
336
|
}
|
|
336
337
|
function assertProducerDiscipline(input) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
if ((input.status === "verified" || input.status === "assumed") && !input.review?.reviewedAt) {
|
|
344
|
-
throw new Error(`Claim ${input.projection.id} cannot be ${input.status} without reviewedAt`);
|
|
345
|
-
}
|
|
338
|
+
assertReviewOutcomeDiscipline({
|
|
339
|
+
subject: `Claim ${input.projection.id}`,
|
|
340
|
+
status: input.status,
|
|
341
|
+
review: input.review,
|
|
342
|
+
});
|
|
346
343
|
if (input.rawSource.kind !== "manual-entry" && !input.extraction.locator) {
|
|
347
344
|
throw new Error(`Claim ${input.projection.id} needs a source locator for ${input.rawSource.kind}`);
|
|
348
345
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/survey",
|
|
3
|
-
"version": "1.
|
|
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",
|
|
@@ -55,8 +55,11 @@
|
|
|
55
55
|
"typecheck": "tsc --noEmit",
|
|
56
56
|
"test": "npm run build && node --test dist/tests/*.test.js",
|
|
57
57
|
"test:browser": "playwright test",
|
|
58
|
-
"verify": "node scripts/check-content-boundary.cjs && npm run typecheck && npm test && npm run check:review-workbench-assets && npm run check:review-workbench && npm run test:browser",
|
|
58
|
+
"verify": "node scripts/check-content-boundary.cjs && npm run check:decisions && npm run typecheck && npm test && npm run check:review-workbench-assets && npm run check:review-workbench && npm run test:browser",
|
|
59
59
|
"check:content-boundary": "node scripts/check-content-boundary.cjs",
|
|
60
|
+
"check:decisions": "node scripts/check-decisions.cjs check",
|
|
61
|
+
"gen:decisions-index": "node scripts/check-decisions.cjs gen-index",
|
|
62
|
+
"freeze:adrs": "node scripts/freeze-adrs.mjs",
|
|
60
63
|
"sync:review-workbench-assets": "node scripts/sync-review-workbench-assets.cjs",
|
|
61
64
|
"check:review-workbench-assets": "node scripts/sync-review-workbench-assets.cjs --check",
|
|
62
65
|
"check:review-workbench": "npm run check:review-workbench:static && npm run check:generated-css",
|