@kontourai/survey 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/survey-review-console.mjs +0 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/inquiry-mapping.js +20 -15
- package/dist/src/producer-profile.d.ts +82 -12
- package/dist/src/producer-profile.js +46 -12
- package/dist/src/review-authorizing.d.ts +1 -1
- package/dist/src/review-authorizing.js +10 -0
- package/dist/src/review-proof.d.ts +40 -7
- package/dist/src/review-proof.js +95 -2
- package/dist/src/schema-mapping.js +34 -21
- package/package.json +1 -1
|
File without changes
|
package/dist/src/index.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ export { buildSurveyTrustBundle } from "./to-surface.js";
|
|
|
12
12
|
export type { BuildSurveyTrustBundleOptions } from "./to-surface.js";
|
|
13
13
|
export { buildSurveyLearningProjections } from "./learning-projections.js";
|
|
14
14
|
export type { LearningProjection, LearningProjectionKind, LearningProjectionSeverity, LearningProjectionSignal, } from "./learning-projections.js";
|
|
15
|
-
export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalReviewProofJson, hashCanonicalReviewProofPayload, REVIEW_PROOF_CONTRACT_VERSION, REVIEW_PROOF_PACKAGE_NAME, REVIEW_PROOF_SCHEMA, REVIEW_PROOF_SCHEMA_VERSION, } from "./review-proof.js";
|
|
16
|
-
export type { CanonicalReviewProofPayload, ReviewProofInput, } from "./review-proof.js";
|
|
15
|
+
export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalReviewProofJson, hashCanonicalReviewProofPayload, verifyCanonicalReviewProofPayload, REVIEW_PROOF_CONTRACT_VERSION, REVIEW_PROOF_PACKAGE_NAME, REVIEW_PROOF_SCHEMA, REVIEW_PROOF_SCHEMA_VERSION, } from "./review-proof.js";
|
|
16
|
+
export type { CanonicalReviewProofPayload, CanonicalReviewProofPayloadV1, CanonicalReviewProofPayloadV2, ReviewProofInput, } from "./review-proof.js";
|
|
17
17
|
export { fieldObservation } from "./field-observation.js";
|
|
18
18
|
export type { FieldObservationInput } from "./field-observation.js";
|
|
19
19
|
export { repeatedObservation } from "./repeated-observation.js";
|
package/dist/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js"
|
|
|
5
5
|
export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
|
|
6
6
|
export { buildSurveyTrustBundle } from "./to-surface.js";
|
|
7
7
|
export { buildSurveyLearningProjections } from "./learning-projections.js";
|
|
8
|
-
export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalReviewProofJson, hashCanonicalReviewProofPayload, REVIEW_PROOF_CONTRACT_VERSION, REVIEW_PROOF_PACKAGE_NAME, REVIEW_PROOF_SCHEMA, REVIEW_PROOF_SCHEMA_VERSION, } from "./review-proof.js";
|
|
8
|
+
export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalReviewProofJson, hashCanonicalReviewProofPayload, verifyCanonicalReviewProofPayload, REVIEW_PROOF_CONTRACT_VERSION, REVIEW_PROOF_PACKAGE_NAME, REVIEW_PROOF_SCHEMA, REVIEW_PROOF_SCHEMA_VERSION, } from "./review-proof.js";
|
|
9
9
|
export { fieldObservation } from "./field-observation.js";
|
|
10
10
|
export { repeatedObservation } from "./repeated-observation.js";
|
|
11
11
|
export { sourceOfAuthorityObservation, sourceOfAuthorityObservationBuilder, SourceOfAuthorityObservationBuilder, } from "./source-of-authority-observation.js";
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* and lives in the flow-agents repo.
|
|
19
19
|
*/
|
|
20
20
|
import { resolveInquiry } from "@kontourai/surface";
|
|
21
|
-
import {
|
|
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
|
-
|
|
157
|
-
.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
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
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReviewAuthorizing, ReviewAuthorizingAuthorizedAction } from "./types.js";
|
|
2
|
-
export type ReviewAuthorizingIssueCode = "not-an-object" | "missing-kind" | "unknown-kind" | "missing-statement" | "missing-prompt" | "missing-response" | "missing-prompt-ref" | "missing-rendered-prompt" | "missing-action" | "invalid-action" | "missing-authority-ref";
|
|
2
|
+
export type ReviewAuthorizingIssueCode = "not-an-object" | "missing-kind" | "unknown-kind" | "missing-statement" | "invalid-source" | "missing-prompt" | "missing-response" | "missing-prompt-ref" | "missing-rendered-prompt" | "missing-action" | "invalid-action" | "missing-authority-ref";
|
|
3
3
|
export interface ReviewAuthorizingIssue {
|
|
4
4
|
readonly code: ReviewAuthorizingIssueCode;
|
|
5
5
|
readonly message: string;
|
|
@@ -43,6 +43,7 @@ function validateExplicitStatement(block) {
|
|
|
43
43
|
message: "explicit-statement authorizing block requires a non-empty 'statement' string.",
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
+
validateOptionalSource(block.source, issues);
|
|
46
47
|
return issues;
|
|
47
48
|
}
|
|
48
49
|
function validateExchange(block) {
|
|
@@ -59,8 +60,17 @@ function validateExchange(block) {
|
|
|
59
60
|
message: "exchange authorizing block requires a non-empty 'response' string (both halves required for self-contained testimony).",
|
|
60
61
|
});
|
|
61
62
|
}
|
|
63
|
+
validateOptionalSource(block.source, issues);
|
|
62
64
|
return issues;
|
|
63
65
|
}
|
|
66
|
+
function validateOptionalSource(source, issues) {
|
|
67
|
+
if (source !== undefined && (typeof source !== "string" || source.trim() === "")) {
|
|
68
|
+
issues.push({
|
|
69
|
+
code: "invalid-source",
|
|
70
|
+
message: "authorizing 'source', when present, must be a non-empty string.",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
64
74
|
function validateAuthorizedAction(block) {
|
|
65
75
|
const issues = [];
|
|
66
76
|
if (!block.promptRef || typeof block.promptRef !== "string" || block.promptRef.trim() === "") {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { IntegrityAnchor } from "@kontourai/surface";
|
|
2
|
-
import type { Candidate, CandidateSet, ClaimTarget, Extraction, RawSource, ReviewOutcome } from "./types.js";
|
|
2
|
+
import type { Candidate, CandidateSet, ClaimTarget, Extraction, RawSource, ReviewAuthorizing, ReviewOutcome } from "./types.js";
|
|
3
3
|
export declare const REVIEW_PROOF_SCHEMA = "survey.review-proof";
|
|
4
|
-
export declare const REVIEW_PROOF_SCHEMA_VERSION =
|
|
4
|
+
export declare const REVIEW_PROOF_SCHEMA_VERSION = 2;
|
|
5
5
|
export declare const REVIEW_PROOF_PACKAGE_NAME = "@kontourai/survey";
|
|
6
|
-
export declare const REVIEW_PROOF_CONTRACT_VERSION = "
|
|
6
|
+
export declare const REVIEW_PROOF_CONTRACT_VERSION = "2";
|
|
7
|
+
declare const LEGACY_REVIEW_PROOF_SCHEMA_VERSION = 1;
|
|
8
|
+
declare const LEGACY_REVIEW_PROOF_CONTRACT_VERSION = "1";
|
|
7
9
|
export interface ReviewProofInput {
|
|
8
10
|
rawSource: RawSource;
|
|
9
11
|
extraction: Extraction;
|
|
@@ -15,13 +17,13 @@ export interface ReviewProofInput {
|
|
|
15
17
|
observedAt?: string;
|
|
16
18
|
metadata?: Record<string, unknown>;
|
|
17
19
|
}
|
|
18
|
-
export interface
|
|
20
|
+
export interface CanonicalReviewProofPayloadV1 {
|
|
19
21
|
schemaVersion: 1;
|
|
20
22
|
proof: {
|
|
21
23
|
schema: typeof REVIEW_PROOF_SCHEMA;
|
|
22
|
-
schemaVersion: typeof
|
|
24
|
+
schemaVersion: typeof LEGACY_REVIEW_PROOF_SCHEMA_VERSION;
|
|
23
25
|
packageName: typeof REVIEW_PROOF_PACKAGE_NAME;
|
|
24
|
-
packageVersion: typeof
|
|
26
|
+
packageVersion: typeof LEGACY_REVIEW_PROOF_CONTRACT_VERSION;
|
|
25
27
|
issuer: string;
|
|
26
28
|
producer: string;
|
|
27
29
|
issuedAt: string;
|
|
@@ -110,7 +112,38 @@ export interface CanonicalReviewProofPayload {
|
|
|
110
112
|
eventMethod?: string;
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
|
-
|
|
115
|
+
type CanonicalReviewAuthorizing = (Extract<ReviewAuthorizing, {
|
|
116
|
+
kind: "explicit-statement";
|
|
117
|
+
}> & {
|
|
118
|
+
authorityRef?: never;
|
|
119
|
+
}) | (Extract<ReviewAuthorizing, {
|
|
120
|
+
kind: "exchange";
|
|
121
|
+
}> & {
|
|
122
|
+
authorityRef?: never;
|
|
123
|
+
}) | (Extract<ReviewAuthorizing, {
|
|
124
|
+
kind: "authorized-action";
|
|
125
|
+
}> & {
|
|
126
|
+
source?: never;
|
|
127
|
+
});
|
|
128
|
+
export type CanonicalReviewProofPayloadV2 = Omit<CanonicalReviewProofPayloadV1, "schemaVersion" | "proof" | "reviewOutcome"> & {
|
|
129
|
+
schemaVersion: 2;
|
|
130
|
+
proof: Omit<CanonicalReviewProofPayloadV1["proof"], "schemaVersion" | "packageVersion"> & {
|
|
131
|
+
schemaVersion: typeof REVIEW_PROOF_SCHEMA_VERSION;
|
|
132
|
+
packageVersion: typeof REVIEW_PROOF_CONTRACT_VERSION;
|
|
133
|
+
};
|
|
134
|
+
reviewOutcome?: NonNullable<CanonicalReviewProofPayloadV1["reviewOutcome"]> & {
|
|
135
|
+
authorizing?: CanonicalReviewAuthorizing;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
export type CanonicalReviewProofPayload = CanonicalReviewProofPayloadV1 | CanonicalReviewProofPayloadV2;
|
|
139
|
+
export declare function buildCanonicalReviewProofPayload(input: ReviewProofInput): CanonicalReviewProofPayloadV2;
|
|
114
140
|
export declare function canonicalReviewProofJson(payload: CanonicalReviewProofPayload): string;
|
|
115
141
|
export declare function hashCanonicalReviewProofPayload(payload: CanonicalReviewProofPayload): string;
|
|
142
|
+
/**
|
|
143
|
+
* Verifies the integrity hash of a persisted v1 or v2 canonical review proof.
|
|
144
|
+
* Envelope compatibility and v2 authorizing admissibility are checked before
|
|
145
|
+
* the expected hash is compared.
|
|
146
|
+
*/
|
|
147
|
+
export declare function verifyCanonicalReviewProofPayload(payload: unknown, expectedHash: string): boolean;
|
|
116
148
|
export declare function buildReviewProofAnchor(input: ReviewProofInput): IntegrityAnchor;
|
|
149
|
+
export {};
|
package/dist/src/review-proof.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { validateAuthorizing } from "./review-authorizing.js";
|
|
2
3
|
export const REVIEW_PROOF_SCHEMA = "survey.review-proof";
|
|
3
|
-
export const REVIEW_PROOF_SCHEMA_VERSION =
|
|
4
|
+
export const REVIEW_PROOF_SCHEMA_VERSION = 2;
|
|
4
5
|
export const REVIEW_PROOF_PACKAGE_NAME = "@kontourai/survey";
|
|
5
6
|
// Version of the review proof contract emitted by this helper. This is intentionally
|
|
6
7
|
// independent from the npm package release version because it participates in hashes.
|
|
7
|
-
export const REVIEW_PROOF_CONTRACT_VERSION = "
|
|
8
|
+
export const REVIEW_PROOF_CONTRACT_VERSION = "2";
|
|
9
|
+
const LEGACY_REVIEW_PROOF_SCHEMA_VERSION = 1;
|
|
10
|
+
const LEGACY_REVIEW_PROOF_CONTRACT_VERSION = "1";
|
|
8
11
|
export function buildCanonicalReviewProofPayload(input) {
|
|
9
12
|
assertCandidateConsistency(input);
|
|
10
13
|
return {
|
|
@@ -78,6 +81,7 @@ export function buildCanonicalReviewProofPayload(input) {
|
|
|
78
81
|
reviewedAt: input.reviewOutcome.reviewedAt,
|
|
79
82
|
rationale: input.reviewOutcome.rationale,
|
|
80
83
|
evidenceIds: input.reviewOutcome.evidenceIds ? [...input.reviewOutcome.evidenceIds].sort() : undefined,
|
|
84
|
+
authorizing: copyCanonicalAuthorizing(input.reviewOutcome.authorizing),
|
|
81
85
|
}
|
|
82
86
|
: undefined,
|
|
83
87
|
claim: {
|
|
@@ -111,6 +115,45 @@ export function buildCanonicalReviewProofPayload(input) {
|
|
|
111
115
|
},
|
|
112
116
|
};
|
|
113
117
|
}
|
|
118
|
+
function copyCanonicalAuthorizing(authorizing) {
|
|
119
|
+
if (authorizing === undefined)
|
|
120
|
+
return undefined;
|
|
121
|
+
const kind = authorizing.kind;
|
|
122
|
+
let snapshot;
|
|
123
|
+
switch (kind) {
|
|
124
|
+
case "explicit-statement":
|
|
125
|
+
snapshot = {
|
|
126
|
+
kind,
|
|
127
|
+
statement: authorizing.statement,
|
|
128
|
+
source: authorizing.source,
|
|
129
|
+
};
|
|
130
|
+
break;
|
|
131
|
+
case "exchange":
|
|
132
|
+
snapshot = {
|
|
133
|
+
kind,
|
|
134
|
+
prompt: authorizing.prompt,
|
|
135
|
+
response: authorizing.response,
|
|
136
|
+
source: authorizing.source,
|
|
137
|
+
};
|
|
138
|
+
break;
|
|
139
|
+
case "authorized-action":
|
|
140
|
+
snapshot = {
|
|
141
|
+
kind,
|
|
142
|
+
promptRef: authorizing.promptRef,
|
|
143
|
+
renderedPrompt: authorizing.renderedPrompt,
|
|
144
|
+
action: authorizing.action,
|
|
145
|
+
authorityRef: authorizing.authorityRef,
|
|
146
|
+
};
|
|
147
|
+
break;
|
|
148
|
+
default:
|
|
149
|
+
snapshot = { kind };
|
|
150
|
+
}
|
|
151
|
+
const issues = validateAuthorizing(snapshot);
|
|
152
|
+
if (issues.length > 0) {
|
|
153
|
+
throw new Error(`Canonical review proof has invalid authorizing: ${issues.map((issue) => issue.message).join(" ")}`);
|
|
154
|
+
}
|
|
155
|
+
return snapshot;
|
|
156
|
+
}
|
|
114
157
|
function assertCandidateConsistency(input) {
|
|
115
158
|
if (input.extraction.sourceId !== input.rawSource.id) {
|
|
116
159
|
throw new Error(`Review proof extraction sourceId "${input.extraction.sourceId}" does not match raw source id "${input.rawSource.id}".`);
|
|
@@ -143,6 +186,56 @@ export function canonicalReviewProofJson(payload) {
|
|
|
143
186
|
export function hashCanonicalReviewProofPayload(payload) {
|
|
144
187
|
return createHash("sha256").update(canonicalReviewProofJson(payload)).digest("hex");
|
|
145
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Verifies the integrity hash of a persisted v1 or v2 canonical review proof.
|
|
191
|
+
* Envelope compatibility and v2 authorizing admissibility are checked before
|
|
192
|
+
* the expected hash is compared.
|
|
193
|
+
*/
|
|
194
|
+
export function verifyCanonicalReviewProofPayload(payload, expectedHash) {
|
|
195
|
+
try {
|
|
196
|
+
const snapshot = canonicalize(payload);
|
|
197
|
+
const canonicalJson = JSON.stringify(snapshot);
|
|
198
|
+
if (typeof canonicalJson !== "string" || !isRecord(snapshot) || !isRecord(snapshot.proof))
|
|
199
|
+
return false;
|
|
200
|
+
if (snapshot.proof.schema !== REVIEW_PROOF_SCHEMA)
|
|
201
|
+
return false;
|
|
202
|
+
if (snapshot.proof.packageName !== REVIEW_PROOF_PACKAGE_NAME)
|
|
203
|
+
return false;
|
|
204
|
+
const schemaVersion = snapshot.schemaVersion;
|
|
205
|
+
const proofSchemaVersion = snapshot.proof.schemaVersion;
|
|
206
|
+
const packageVersion = snapshot.proof.packageVersion;
|
|
207
|
+
const reviewOutcome = snapshot.reviewOutcome;
|
|
208
|
+
if (schemaVersion === LEGACY_REVIEW_PROOF_SCHEMA_VERSION) {
|
|
209
|
+
if (proofSchemaVersion !== LEGACY_REVIEW_PROOF_SCHEMA_VERSION
|
|
210
|
+
|| packageVersion !== LEGACY_REVIEW_PROOF_CONTRACT_VERSION) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if (isRecord(reviewOutcome) && Object.prototype.hasOwnProperty.call(reviewOutcome, "authorizing")) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (schemaVersion === REVIEW_PROOF_SCHEMA_VERSION) {
|
|
218
|
+
if (proofSchemaVersion !== REVIEW_PROOF_SCHEMA_VERSION || packageVersion !== REVIEW_PROOF_CONTRACT_VERSION) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
if (reviewOutcome !== undefined && !isRecord(reviewOutcome))
|
|
222
|
+
return false;
|
|
223
|
+
if (isRecord(reviewOutcome)
|
|
224
|
+
&& Object.prototype.hasOwnProperty.call(reviewOutcome, "authorizing")
|
|
225
|
+
&& reviewOutcome.authorizing !== undefined
|
|
226
|
+
&& validateAuthorizing(reviewOutcome.authorizing).length > 0) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return createHash("sha256").update(canonicalJson).digest("hex") === expectedHash;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
146
239
|
export function buildReviewProofAnchor(input) {
|
|
147
240
|
const payload = buildCanonicalReviewProofPayload(input);
|
|
148
241
|
const hash = hashCanonicalReviewProofPayload(payload);
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* so that resolveInquiry can resolve across systems with weakest-link
|
|
21
21
|
* capping.
|
|
22
22
|
*/
|
|
23
|
-
import {
|
|
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:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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