@kontourai/survey 1.1.1 → 1.2.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/README.md CHANGED
@@ -96,6 +96,8 @@ One observation, one chain: the page it came from, what the extractor read, who
96
96
 
97
97
  Keep producer operational state outside Survey. Queue status, reviewer form state, retries, source caches, and product policy decisions belong in the producer's own data model. Survey carries only the portable evidence chain records needed by Surface.
98
98
 
99
+ When you build an `authorized-action` authorizing block outside the workbench, pair `buildAuthorizedActionAuthorizing` with `buildPromptRef({ module, component, version?, scheme? })` — `buildPromptRef` formats a well-formed, versioned `promptRef` (bare `"review-workbench/decision-card@v1"` or scheme-prefixed `"survey://<module>/<component>@v1"`) that `buildAuthorizedActionAuthorizing` accepts directly, instead of hand-formatting the string.
100
+
99
101
  ## Review Workbench embed
100
102
 
101
103
  **Web component** (shadow DOM, no framework required):
@@ -1,7 +1,8 @@
1
- import type { CandidateSet, ClaimTarget, EscalationRecord, Extraction, Interpretation, RawSource, ReviewOutcome, SurveyInput } from "./types.js";
1
+ import type { CandidateSet, ClaimTarget, EscalationRecord, Extraction, Interpretation, ReviewStatus, RawSource, ReviewOutcome, SurveyInput } from "./types.js";
2
2
  export interface SurveyInputBuilderArgs {
3
3
  source: string;
4
4
  generatedAt?: string;
5
+ contractVersion?: string;
5
6
  }
6
7
  export interface SurveyClaimRecord {
7
8
  rawSource: RawSource;
@@ -54,6 +55,7 @@ export interface CandidateReviewRecordInput {
54
55
  export declare class SurveyInputBuilder {
55
56
  private readonly source;
56
57
  private readonly generatedAt;
58
+ private readonly contractVersion;
57
59
  private readonly rawSources;
58
60
  private readonly extractions;
59
61
  private readonly candidateSets;
@@ -78,3 +80,4 @@ export declare class SurveyInputBuilder {
78
80
  private addRecordRawSource;
79
81
  }
80
82
  export declare function candidateReviewRecord(input: CandidateReviewRecordInput): SurveyClaimRecord[];
83
+ export declare function candidateSetStatusFor(reviewStatus?: ReviewStatus): CandidateSet["status"];
@@ -1,6 +1,8 @@
1
+ import { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
1
2
  export class SurveyInputBuilder {
2
3
  source;
3
4
  generatedAt;
5
+ contractVersion;
4
6
  rawSources = new Map();
5
7
  extractions = new Map();
6
8
  candidateSets = new Map();
@@ -11,6 +13,7 @@ export class SurveyInputBuilder {
11
13
  constructor(args) {
12
14
  this.source = args.source;
13
15
  this.generatedAt = args.generatedAt ?? new Date().toISOString();
16
+ this.contractVersion = args.contractVersion ?? SURVEY_INPUT_CONTRACT_VERSION;
14
17
  }
15
18
  addRawSource(rawSource) {
16
19
  addUnique(this.rawSources, rawSource, "raw source");
@@ -64,6 +67,7 @@ export class SurveyInputBuilder {
64
67
  }
65
68
  build() {
66
69
  return {
70
+ contractVersion: this.contractVersion,
67
71
  source: this.source,
68
72
  generatedAt: this.generatedAt,
69
73
  rawSources: [...this.rawSources.values()],
@@ -206,7 +210,7 @@ function observationIds(rootId, observation) {
206
210
  reviewOutcomeId: observation.reviewOutcome?.id ?? `${rootId}.review`,
207
211
  };
208
212
  }
209
- function candidateSetStatusFor(reviewStatus) {
213
+ export function candidateSetStatusFor(reviewStatus) {
210
214
  if (!reviewStatus || reviewStatus === "proposed")
211
215
  return "needs-review";
212
216
  return "resolved";
@@ -0,0 +1,55 @@
1
+ import type { CandidateSetStatus } from "./types.js";
2
+ import { type ClaimTargetHint, type ExtractionReference, type ProducerPolicy, type ReviewItem, type ReviewLocator, type SourceReference, type SurveyRecordProjectionHint } from "./review-resource.js";
3
+ /**
4
+ * A single candidate value for a current/proposed {@link ReviewItem}. Carries
5
+ * the candidate value plus its source, extraction, and claim-target evidence.
6
+ * The builder owns id, role, and candidate-set wiring; the caller owns the
7
+ * value and its domain vocabulary.
8
+ */
9
+ export interface CurrentProposedCandidateInput {
10
+ readonly value: unknown;
11
+ readonly confidence?: number;
12
+ readonly sourceRank?: number;
13
+ readonly rejectionReason?: string;
14
+ readonly source: SourceReference;
15
+ readonly locator?: ReviewLocator;
16
+ readonly extraction: ExtractionReference;
17
+ readonly claimTarget: ClaimTargetHint;
18
+ readonly projection?: SurveyRecordProjectionHint;
19
+ readonly producer?: Record<string, unknown>;
20
+ }
21
+ /**
22
+ * Input for {@link currentProposedReviewItem} — the generic envelope, id, and
23
+ * role wiring for a two-candidate current/proposed ReviewItem.
24
+ */
25
+ export interface CurrentProposedReviewItemInput {
26
+ readonly name: string;
27
+ readonly target: string;
28
+ readonly current: CurrentProposedCandidateInput;
29
+ readonly proposed: CurrentProposedCandidateInput;
30
+ readonly candidateSetStatus?: CandidateSetStatus;
31
+ readonly selectedCandidateRole?: "current" | "proposed";
32
+ readonly rationale?: string;
33
+ readonly labels?: Record<string, string>;
34
+ readonly producerMetadata?: Record<string, unknown>;
35
+ readonly producerPolicy?: ProducerPolicy;
36
+ readonly projection?: SurveyRecordProjectionHint;
37
+ readonly candidateIdSuffix?: {
38
+ readonly current?: string;
39
+ readonly proposed?: string;
40
+ };
41
+ }
42
+ /**
43
+ * Builds the two-candidate current/proposed {@link ReviewItem} envelope that
44
+ * producers otherwise hand-assemble: candidate ids, roles, candidate-set id,
45
+ * observed-candidate count, and selected-candidate mirroring. The caller still
46
+ * owns each candidate's value, source, extraction, and claim-target vocabulary.
47
+ *
48
+ * Candidate ids default to `<name>.current` / `<name>.proposed` (the trailing
49
+ * segment is overridable via `candidateIdSuffix`); the candidate-set id defaults
50
+ * to `<name>.candidates` (overridable via `projection.candidateSetId`);
51
+ * `candidateSetStatus` defaults to `"needs-review"`. `status.observedCandidateCount`
52
+ * is always 2, and `status.selectedCandidateId` mirrors `spec.selectedCandidateId`
53
+ * when `selectedCandidateRole` is set.
54
+ */
55
+ export declare function currentProposedReviewItem(input: CurrentProposedReviewItemInput): ReviewItem;
@@ -0,0 +1,64 @@
1
+ import { reviewResourceApiVersion, } from "./review-resource.js";
2
+ /**
3
+ * Builds the two-candidate current/proposed {@link ReviewItem} envelope that
4
+ * producers otherwise hand-assemble: candidate ids, roles, candidate-set id,
5
+ * observed-candidate count, and selected-candidate mirroring. The caller still
6
+ * owns each candidate's value, source, extraction, and claim-target vocabulary.
7
+ *
8
+ * Candidate ids default to `<name>.current` / `<name>.proposed` (the trailing
9
+ * segment is overridable via `candidateIdSuffix`); the candidate-set id defaults
10
+ * to `<name>.candidates` (overridable via `projection.candidateSetId`);
11
+ * `candidateSetStatus` defaults to `"needs-review"`. `status.observedCandidateCount`
12
+ * is always 2, and `status.selectedCandidateId` mirrors `spec.selectedCandidateId`
13
+ * when `selectedCandidateRole` is set.
14
+ */
15
+ export function currentProposedReviewItem(input) {
16
+ const candidateSetId = input.projection?.candidateSetId ?? `${input.name}.candidates`;
17
+ const currentId = `${input.name}.${input.candidateIdSuffix?.current ?? "current"}`;
18
+ const proposedId = `${input.name}.${input.candidateIdSuffix?.proposed ?? "proposed"}`;
19
+ const selectedCandidateId = input.selectedCandidateRole === "current"
20
+ ? currentId
21
+ : input.selectedCandidateRole === "proposed"
22
+ ? proposedId
23
+ : undefined;
24
+ const currentCandidate = buildCandidate("current", currentId, input.current, candidateSetId);
25
+ const proposedCandidate = buildCandidate("proposed", proposedId, input.proposed, candidateSetId);
26
+ return {
27
+ apiVersion: reviewResourceApiVersion,
28
+ kind: "ReviewItem",
29
+ metadata: {
30
+ name: input.name,
31
+ ...(input.labels ? { labels: input.labels } : {}),
32
+ ...(input.producerMetadata ? { producer: input.producerMetadata } : {}),
33
+ },
34
+ spec: {
35
+ target: input.target,
36
+ candidates: [currentCandidate, proposedCandidate],
37
+ candidateSetStatus: input.candidateSetStatus ?? "needs-review",
38
+ ...(selectedCandidateId ? { selectedCandidateId } : {}),
39
+ ...(input.rationale ? { rationale: input.rationale } : {}),
40
+ ...(input.producerPolicy ? { producerPolicy: input.producerPolicy } : {}),
41
+ projection: { ...input.projection, candidateSetId },
42
+ },
43
+ status: {
44
+ observedCandidateCount: 2,
45
+ ...(selectedCandidateId ? { selectedCandidateId } : {}),
46
+ },
47
+ };
48
+ }
49
+ function buildCandidate(role, id, input, candidateSetId) {
50
+ return {
51
+ id,
52
+ role,
53
+ value: input.value,
54
+ ...(input.confidence !== undefined ? { confidence: input.confidence } : {}),
55
+ ...(input.sourceRank !== undefined ? { sourceRank: input.sourceRank } : {}),
56
+ ...(input.rejectionReason ? { rejectionReason: input.rejectionReason } : {}),
57
+ source: input.source,
58
+ ...(input.locator ? { locator: input.locator } : {}),
59
+ extraction: input.extraction,
60
+ claimTarget: input.claimTarget,
61
+ projection: { candidateSetId, candidateId: id, ...input.projection },
62
+ ...(input.producer ? { producer: input.producer } : {}),
63
+ };
64
+ }
@@ -1,7 +1,8 @@
1
1
  export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, EscalationDimension, EscalationRecord, Extraction, Interpretation, LocatorScheme, RawSource, RawSourceKind, ReviewAuthorizing, ReviewAuthorizingAuthorizedAction, ReviewAuthorizingExchange, ReviewAuthorizingExplicitStatement, ReviewAuthorizingKind, ReviewOutcome, ReviewStatus, SurveyInput, } from "./types.js";
2
+ export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
2
3
  export { reviewResourceApiVersion } from "./review-resource.js";
3
- export type { CandidateRole, ClaimTargetHint, ExtractionReference, ResourceEnvelope, ResourceMetadata, ReviewActor, ReviewCandidate, ReviewDecision, ReviewDecisionSpec, ReviewDecisionStatus, ReviewItem, ReviewItemSpec, ReviewItemStatus, ReviewLocator, ReviewResource, ReviewResourceApiVersion, ReviewResourceKind, ReviewSession, ReviewSessionEvent, ReviewSessionEventSpec, ReviewSessionEventStatus, ReviewSessionEventType, ReviewSessionSpec, ReviewSessionStatus, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
4
- export { candidateReviewRecord, SurveyInputBuilder } from "./builder.js";
4
+ export type { CandidateRole, ClaimTargetHint, ProducerPolicy, ExtractionReference, ResourceEnvelope, ResourceMetadata, ReviewActor, ReviewCandidate, ReviewDecision, ReviewDecisionMode, ReviewDecisionSpec, ReviewDecisionStatus, ReviewItem, ReviewItemSpec, ReviewItemStatus, ReviewLocator, ReviewResource, ReviewResourceApiVersion, ReviewResourceKind, ReviewSession, ReviewSessionEvent, ReviewSessionEventSpec, ReviewSessionEventStatus, ReviewSessionEventType, ReviewSessionSpec, ReviewSessionStatus, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
5
+ export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
5
6
  export type { CandidateReviewRecordInput, SurveyClaimRecord, SurveyInputBuilderArgs, SurveyObservationInput, } from "./builder.js";
6
7
  export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
7
8
  export type { ReviewedCandidateResolutionInput } from "./reviewed-candidate-resolution.js";
@@ -29,7 +30,11 @@ export { referenceUtteranceExtractor, surveyAgentUtterance, utteranceToSurveyInp
29
30
  export type { ExtractedStatement, StatementBadge, UtteranceClaimExtractor, UtteranceStatement, UtteranceStatementRecords, UtteranceTrustReport, } from "./agent-utterance.js";
30
31
  export { mappingReviewToSurface, referenceSchemaExtractor, surveySchemaMapping, } from "./schema-mapping.js";
31
32
  export type { MappingProposalRecord, ReviewedMapping, SchemaMappingExtractor, SchemaMappingOptions, SystemFieldRef, } from "./schema-mapping.js";
32
- export { buildAuthorizedActionAuthorizing, isValidAuthorizing, validateAuthorizing } from "./review-authorizing.js";
33
- export type { BuildAuthorizedActionAuthorizingInput, ReviewAuthorizingIssue, ReviewAuthorizingIssueCode, } from "./review-authorizing.js";
33
+ export { buildAuthorizedActionAuthorizing, buildPromptRef, isValidAuthorizing, validateAuthorizing } from "./review-authorizing.js";
34
+ export type { BuildAuthorizedActionAuthorizingInput, BuildPromptRefInput, ReviewAuthorizingIssue, ReviewAuthorizingIssueCode, } from "./review-authorizing.js";
35
+ export { confidenceBasisForReview, defineProductVocabulary, stableId } from "./vocabulary.js";
36
+ export type { ConfidenceBasisForReviewInput, ProductVocabularyDefinition, } from "./vocabulary.js";
37
+ export { currentProposedReviewItem } from "./current-proposed-review-item.js";
38
+ export type { CurrentProposedCandidateInput, CurrentProposedReviewItemInput, } from "./current-proposed-review-item.js";
34
39
  export { deriveOversightMetrics, mergeTrustBundleWithOversightMetrics, oversightMetricsToClaims, } from "./oversight-metrics.js";
35
40
  export type { AggregateOversightMetrics, DeriveOversightMetricsOptions, OversightMetrics, OversightMetricsClaimsSubject, OversightQualityClaim, ReviewerOversightMetrics, } from "./oversight-metrics.js";
package/dist/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
+ export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
1
2
  export { reviewResourceApiVersion } from "./review-resource.js";
2
- export { candidateReviewRecord, SurveyInputBuilder } from "./builder.js";
3
+ export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
3
4
  export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
4
5
  export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
5
6
  export { buildSurveyTrustBundle } from "./to-surface.js";
@@ -13,5 +14,7 @@ export { apiRecordSource, manualEntrySource, policyStandardSource, uploadedDocum
13
14
  export { applyAutoAcceptPolicy, applyMappingReview, buildMappingReviewItems, lookupMapping, lookupRejectedMapping, normalizeQuestion, proposalsToCandidateSet, referenceMappingProposer, resolveQuestion, } from "./inquiry-mapping.js";
14
15
  export { referenceUtteranceExtractor, surveyAgentUtterance, utteranceToSurveyInput, } from "./agent-utterance.js";
15
16
  export { mappingReviewToSurface, referenceSchemaExtractor, surveySchemaMapping, } from "./schema-mapping.js";
16
- export { buildAuthorizedActionAuthorizing, isValidAuthorizing, validateAuthorizing } from "./review-authorizing.js";
17
+ export { buildAuthorizedActionAuthorizing, buildPromptRef, isValidAuthorizing, validateAuthorizing } from "./review-authorizing.js";
18
+ export { confidenceBasisForReview, defineProductVocabulary, stableId } from "./vocabulary.js";
19
+ export { currentProposedReviewItem } from "./current-proposed-review-item.js";
17
20
  export { deriveOversightMetrics, mergeTrustBundleWithOversightMetrics, oversightMetricsToClaims, } from "./oversight-metrics.js";
@@ -37,3 +37,23 @@ export interface BuildAuthorizedActionAuthorizingInput {
37
37
  * runs validateAuthorizing and degrades gracefully instead of throwing.
38
38
  */
39
39
  export declare function buildAuthorizedActionAuthorizing(input: BuildAuthorizedActionAuthorizingInput): ReviewAuthorizingAuthorizedAction;
40
+ export interface BuildPromptRefInput {
41
+ readonly module: string;
42
+ readonly component: string;
43
+ readonly version?: string;
44
+ readonly scheme?: string;
45
+ }
46
+ /**
47
+ * Builds a well-formed `promptRef` for an `authorized-action` authorizing block,
48
+ * generalizing the workbench's `<module>/<component>@<version>` convention.
49
+ *
50
+ * Without a `scheme` the result is bare (`"review-workbench/decision-card@v1"`),
51
+ * matching the workbench's own internal prompt refs. With a `scheme` the result
52
+ * is prefixed (`"survey://rules-admin/keep-current@v1"`) for producers that
53
+ * namespace their prompt refs. `version` defaults to `"v1"`.
54
+ *
55
+ * The returned string is a valid `promptRef` input to
56
+ * {@link buildAuthorizedActionAuthorizing}. Throws on an empty `module` or
57
+ * `component`, mirroring that helper's throw-on-invalid-input style.
58
+ */
59
+ export declare function buildPromptRef(input: BuildPromptRefInput): string;
@@ -123,3 +123,30 @@ export function buildAuthorizedActionAuthorizing(input) {
123
123
  }
124
124
  return block;
125
125
  }
126
+ /**
127
+ * Builds a well-formed `promptRef` for an `authorized-action` authorizing block,
128
+ * generalizing the workbench's `<module>/<component>@<version>` convention.
129
+ *
130
+ * Without a `scheme` the result is bare (`"review-workbench/decision-card@v1"`),
131
+ * matching the workbench's own internal prompt refs. With a `scheme` the result
132
+ * is prefixed (`"survey://rules-admin/keep-current@v1"`) for producers that
133
+ * namespace their prompt refs. `version` defaults to `"v1"`.
134
+ *
135
+ * The returned string is a valid `promptRef` input to
136
+ * {@link buildAuthorizedActionAuthorizing}. Throws on an empty `module` or
137
+ * `component`, mirroring that helper's throw-on-invalid-input style.
138
+ */
139
+ export function buildPromptRef(input) {
140
+ const module = input.module?.trim();
141
+ const component = input.component?.trim();
142
+ if (!module) {
143
+ throw new Error("buildPromptRef: 'module' must be a non-empty string.");
144
+ }
145
+ if (!component) {
146
+ throw new Error("buildPromptRef: 'component' must be a non-empty string.");
147
+ }
148
+ const version = input.version?.trim() || "v1";
149
+ const scheme = input.scheme?.trim();
150
+ const ref = `${module}/${component}@${version}`;
151
+ return scheme ? `${scheme}://${ref}` : ref;
152
+ }
@@ -59,6 +59,25 @@ export interface SurveyRecordProjectionHint {
59
59
  reviewOutcomeId?: ReviewOutcome["id"];
60
60
  claimId?: ClaimTarget["id"];
61
61
  }
62
+ /**
63
+ * Well-known decision-mode vocabulary for {@link ProducerPolicy.decisionMode}.
64
+ * A producer declares how a ReviewItem is allowed to be resolved:
65
+ * `keep-current` — only a keep-current decision is admissible.
66
+ * `current-proposed` — only the current or proposed candidate may be selected.
67
+ * `free-select` — any declared candidate may be selected.
68
+ * Enforcement is opt-in; see `assertReviewDecisionModeAllows` and the
69
+ * `enforceProducerPolicy` option on `applyReviewSession`.
70
+ */
71
+ export type ReviewDecisionMode = "keep-current" | "current-proposed" | "free-select";
72
+ /**
73
+ * Producer-declared policy carried on a ReviewItem. The well-known
74
+ * `decisionMode` sub-key is typed and optionally enforceable; all other keys
75
+ * remain opaque to Survey (tolerated via the index signature, never inspected).
76
+ */
77
+ export interface ProducerPolicy {
78
+ decisionMode?: ReviewDecisionMode;
79
+ [key: string]: unknown;
80
+ }
62
81
  export interface ReviewCandidate {
63
82
  id: string;
64
83
  role?: CandidateRole;
@@ -79,7 +98,7 @@ export interface ReviewItemSpec {
79
98
  candidateSetStatus?: CandidateSetStatus;
80
99
  selectedCandidateId?: string;
81
100
  rationale?: string;
82
- producerPolicy?: Record<string, unknown>;
101
+ producerPolicy?: ProducerPolicy;
83
102
  projection?: SurveyRecordProjectionHint;
84
103
  }
85
104
  export interface ReviewItemStatus {
@@ -0,0 +1,34 @@
1
+ import type { ReviewItem } from "../review-resource.js";
2
+ import type { ReviewWorkbenchResult } from "./review-workbench.js";
3
+ export type ReviewDecisionModeIssueCode = "unknown-decision-mode" | "decision-not-allowed" | "candidate-not-in-item";
4
+ export interface ReviewDecisionModeIssue {
5
+ readonly code: ReviewDecisionModeIssueCode;
6
+ readonly message: string;
7
+ }
8
+ /**
9
+ * The subset of a {@link ReviewWorkbenchResult} a decision-mode check inspects.
10
+ */
11
+ export type ReviewDecisionModeResult = Pick<ReviewWorkbenchResult, "decision" | "selectedCandidateId" | "selectedCandidateRole">;
12
+ /**
13
+ * Validates a review result against the item's declared `producerPolicy.decisionMode`.
14
+ *
15
+ * Returns an empty array when the item declares no `producerPolicy` or no
16
+ * `decisionMode` (the default, un-enforced posture). For a known mode it checks:
17
+ * `keep-current` — only a `keep-current` decision is admissible.
18
+ * `current-proposed` — only the current or proposed candidate role may be selected.
19
+ * `free-select` — the selected candidate must be one declared on the item.
20
+ * An unrecognized `decisionMode` string fails closed with a single
21
+ * `unknown-decision-mode` issue.
22
+ */
23
+ export declare function validateReviewDecisionMode(item: ReviewItem, result: ReviewDecisionModeResult): ReviewDecisionModeIssue[];
24
+ export declare class DecisionModeViolationError extends Error {
25
+ readonly name = "DecisionModeViolationError";
26
+ readonly issues: readonly ReviewDecisionModeIssue[];
27
+ constructor(reviewItemName: string, issues: readonly ReviewDecisionModeIssue[]);
28
+ }
29
+ /**
30
+ * Asserts a review result satisfies the item's declared decision mode, throwing
31
+ * a {@link DecisionModeViolationError} otherwise. Mirrors the
32
+ * validate-then-assert idiom in `review-authorizing.ts`.
33
+ */
34
+ export declare function assertReviewDecisionModeAllows(item: ReviewItem, result: ReviewDecisionModeResult): void;
@@ -0,0 +1,73 @@
1
+ const KNOWN_DECISION_MODES = new Set([
2
+ "keep-current",
3
+ "current-proposed",
4
+ "free-select",
5
+ ]);
6
+ /**
7
+ * Validates a review result against the item's declared `producerPolicy.decisionMode`.
8
+ *
9
+ * Returns an empty array when the item declares no `producerPolicy` or no
10
+ * `decisionMode` (the default, un-enforced posture). For a known mode it checks:
11
+ * `keep-current` — only a `keep-current` decision is admissible.
12
+ * `current-proposed` — only the current or proposed candidate role may be selected.
13
+ * `free-select` — the selected candidate must be one declared on the item.
14
+ * An unrecognized `decisionMode` string fails closed with a single
15
+ * `unknown-decision-mode` issue.
16
+ */
17
+ export function validateReviewDecisionMode(item, result) {
18
+ const decisionMode = item.spec.producerPolicy?.decisionMode;
19
+ if (decisionMode === undefined) {
20
+ return [];
21
+ }
22
+ if (!KNOWN_DECISION_MODES.has(decisionMode)) {
23
+ return [{
24
+ code: "unknown-decision-mode",
25
+ message: `ReviewItem ${item.metadata.name} declares an unrecognized producerPolicy.decisionMode '${String(decisionMode)}'.`,
26
+ }];
27
+ }
28
+ if (decisionMode === "keep-current") {
29
+ if (result.decision !== "keep-current") {
30
+ return [{
31
+ code: "decision-not-allowed",
32
+ message: `ReviewItem ${item.metadata.name} declares decisionMode 'keep-current'; decision '${result.decision}' is not allowed.`,
33
+ }];
34
+ }
35
+ return [];
36
+ }
37
+ if (decisionMode === "current-proposed") {
38
+ if (result.selectedCandidateRole !== "current" && result.selectedCandidateRole !== "proposed") {
39
+ return [{
40
+ code: "decision-not-allowed",
41
+ message: `ReviewItem ${item.metadata.name} declares decisionMode 'current-proposed'; selected candidate role '${result.selectedCandidateRole ?? "unknown"}' is not current or proposed.`,
42
+ }];
43
+ }
44
+ return [];
45
+ }
46
+ // free-select: the selected candidate must be one declared on the item.
47
+ if (!item.spec.candidates.some((candidate) => candidate.id === result.selectedCandidateId)) {
48
+ return [{
49
+ code: "candidate-not-in-item",
50
+ message: `ReviewItem ${item.metadata.name} declares decisionMode 'free-select'; selected candidate '${result.selectedCandidateId}' is not declared on the item.`,
51
+ }];
52
+ }
53
+ return [];
54
+ }
55
+ export class DecisionModeViolationError extends Error {
56
+ name = "DecisionModeViolationError";
57
+ issues;
58
+ constructor(reviewItemName, issues) {
59
+ super(`ReviewItem ${reviewItemName} violates its declared producerPolicy.decisionMode: ${issues.map((issue) => issue.message).join(" ")}`);
60
+ this.issues = issues;
61
+ }
62
+ }
63
+ /**
64
+ * Asserts a review result satisfies the item's declared decision mode, throwing
65
+ * a {@link DecisionModeViolationError} otherwise. Mirrors the
66
+ * validate-then-assert idiom in `review-authorizing.ts`.
67
+ */
68
+ export function assertReviewDecisionModeAllows(item, result) {
69
+ const issues = validateReviewDecisionMode(item, result);
70
+ if (issues.length > 0) {
71
+ throw new DecisionModeViolationError(item.metadata.name, issues);
72
+ }
73
+ }
@@ -152,7 +152,7 @@ export declare const regulatedRuleConflictReviewItemExample: {
152
152
  candidateSetId: string;
153
153
  };
154
154
  producerPolicy: {
155
- decisionMode: string;
155
+ decisionMode: "keep-current";
156
156
  sourceAuthorityProjection: string;
157
157
  feedbackTags: string[];
158
158
  };
@@ -290,7 +290,7 @@ export declare const facilityCredentialReviewItemExample: {
290
290
  candidateSetId: string;
291
291
  };
292
292
  producerPolicy: {
293
- decisionMode: string;
293
+ decisionMode: "current-proposed";
294
294
  sourceAuthorityProjection: string;
295
295
  feedbackTags: string[];
296
296
  };
@@ -577,7 +577,7 @@ export declare const reviewWorkbenchQueueExamples: (ReviewItem | {
577
577
  candidateSetId: string;
578
578
  };
579
579
  producerPolicy: {
580
- decisionMode: string;
580
+ decisionMode: "keep-current";
581
581
  sourceAuthorityProjection: string;
582
582
  feedbackTags: string[];
583
583
  };
@@ -6,6 +6,7 @@ export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionRe
6
6
  export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, type ReviewCandidatePresentation, type ReviewCandidatePresentationContext, type ReviewItemPresentation, type ReviewItemPresentationContext, type ReviewPresentationAdapter, type ReviewPresentationLink, type ReviewResultPresentation, type ReviewTracePresentationContext, type ReviewTraceRef, type ReviewValuePresentationContext, } from "./review-presentation.js";
7
7
  export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
8
8
  export { validateReviewSessionEventsForSnapshot, type ReviewSessionReplayIssue, type ReviewSessionReplayIssueCode, } from "./review-session-replay.js";
9
+ export { assertReviewDecisionModeAllows, DecisionModeViolationError, validateReviewDecisionMode, type ReviewDecisionModeIssue, type ReviewDecisionModeIssueCode, type ReviewDecisionModeResult, } from "./producer-decision-mode.js";
9
10
  export declare function buildReviewDecision(state: ReviewWorkbenchState): ReviewDecision | undefined;
10
11
  export interface ReviewWorkbenchSessionExport {
11
12
  readonly session: ReviewSession;
@@ -10,6 +10,7 @@ export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionRe
10
10
  export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
11
11
  export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
12
12
  export { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
13
+ export { assertReviewDecisionModeAllows, DecisionModeViolationError, validateReviewDecisionMode, } from "./producer-decision-mode.js";
13
14
  export function buildReviewDecision(state) {
14
15
  if (!state.decision) {
15
16
  return undefined;
@@ -1,6 +1,7 @@
1
- import { type DeriveReviewSessionApplyResultForSnapshotResult, type ReviewSessionApplyResolutionRequirement } from "./review-workbench.js";
1
+ import { type DeriveReviewSessionApplyResultForSnapshotResult, type MapReviewWorkbenchResultsToApplyActionsOptions, type ReviewApplyActionIssue, type ReviewApplyActionMapping, type ReviewSessionApplyIssue, type ReviewSessionApplyResolutionRequirement, type ReviewWorkbenchResult } from "./review-workbench.js";
2
+ import { type ReviewDecisionModeIssue } from "./producer-decision-mode.js";
2
3
  import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
3
- import type { ReviewSessionEvent } from "../review-resource.js";
4
+ import type { ReviewDecision, ReviewSessionEvent } from "../review-resource.js";
4
5
  import type { ReviewQueueSessionState } from "./review-queue-session.js";
5
6
  export interface ServerReviewSessionRecord {
6
7
  readonly sessionName: string;
@@ -63,3 +64,68 @@ export declare function assertServerReviewSessionFreshness(record: ServerReviewS
63
64
  export declare function validateServerReviewSessionEvents(record: ServerReviewSessionRecord, events: readonly ReviewSessionEvent[]): ServerReviewSessionEventValidationIssue[];
64
65
  export declare function assertServerReviewSessionEvents(record: ServerReviewSessionRecord, events: readonly ReviewSessionEvent[]): void;
65
66
  export declare function deriveServerReviewSessionApplyResult(options: DeriveServerReviewSessionApplyResultOptions): DeriveReviewSessionApplyResultForSnapshotResult;
67
+ export interface ApplyReviewSessionOptions<TAction = never> {
68
+ /** Pre-built server record. When omitted, one is built from `snapshot`. */
69
+ readonly record?: ServerReviewSessionRecord;
70
+ /** Reviewed snapshot to build a server record from when `record` is omitted. */
71
+ readonly snapshot?: ReviewQueueSessionState;
72
+ /** Session name for the built record; falls back to the first event's session name. */
73
+ readonly sessionName?: string;
74
+ /** `updatedAt` stamp for the built record. */
75
+ readonly recordUpdatedAt?: string | Date;
76
+ readonly events: readonly ReviewSessionEvent[];
77
+ readonly currentSnapshot?: ReviewQueueSessionState;
78
+ readonly currentEventCount?: number;
79
+ readonly requiredResolvedItems?: ReviewSessionApplyResolutionRequirement;
80
+ /** When true, each result is checked against its item's producerPolicy.decisionMode. */
81
+ readonly enforceProducerPolicy?: boolean;
82
+ /** When provided, results are mapped to product apply actions in the same call. */
83
+ readonly mapActions?: Omit<MapReviewWorkbenchResultsToApplyActionsOptions<TAction>, "results" | "items">;
84
+ }
85
+ export type ApplyReviewSessionIssue = ReviewSessionApplyIssue | {
86
+ readonly code: "stale-session";
87
+ readonly message: string;
88
+ } | {
89
+ readonly code: "invalid-events";
90
+ readonly message: string;
91
+ } | {
92
+ readonly code: "decision-mode-violation";
93
+ readonly reviewItemName: string;
94
+ readonly message: string;
95
+ readonly issues: readonly ReviewDecisionModeIssue[];
96
+ } | {
97
+ readonly code: "action-mapping-failed";
98
+ readonly message: string;
99
+ readonly issues: readonly ReviewApplyActionIssue[];
100
+ };
101
+ export type ApplyReviewSessionResult<TAction = never> = {
102
+ readonly ok: true;
103
+ readonly issues: readonly [];
104
+ readonly decisions: readonly ReviewDecision[];
105
+ readonly results: readonly ReviewWorkbenchResult[];
106
+ readonly actions: readonly ReviewApplyActionMapping<TAction>[];
107
+ readonly replayedSession: ReviewQueueSessionState;
108
+ } | {
109
+ readonly ok: false;
110
+ readonly issues: readonly ApplyReviewSessionIssue[];
111
+ readonly decisions: readonly ReviewDecision[];
112
+ readonly results: readonly ReviewWorkbenchResult[];
113
+ readonly actions: readonly ReviewApplyActionMapping<TAction>[];
114
+ };
115
+ /**
116
+ * One-call server apply: collapses the "resolve server record → derive apply
117
+ * result → normalize freshness/event errors → (optionally) enforce
118
+ * producerPolicy.decisionMode → (optionally) map results to product actions"
119
+ * choreography into a single call.
120
+ *
121
+ * Returns a discriminated `{ ok }` result rather than throwing for expected
122
+ * failure modes (stale session, invalid events, unresolved items, decision-mode
123
+ * violations, action-mapping failures) — matching the
124
+ * `deriveReviewSessionApplyResultForSnapshot` idiom. Unexpected errors still
125
+ * propagate.
126
+ *
127
+ * Supply either a pre-built `record` or a `snapshot` (a record is then built via
128
+ * `createServerReviewSessionRecord`). `enforceProducerPolicy` is off by default;
129
+ * unset `producerPolicy`/`decisionMode` never changes behavior.
130
+ */
131
+ export declare function applyReviewSession<TAction = never>(options: ApplyReviewSessionOptions<TAction>): ApplyReviewSessionResult<TAction>;
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { deriveReviewSessionApplyResultForSnapshot, } from "./review-workbench.js";
2
+ import { deriveReviewSessionApplyResultForSnapshot, mapReviewWorkbenchResultsToApplyActions, ReviewApplyActionMappingError, } from "./review-workbench.js";
3
+ import { validateReviewDecisionMode, } from "./producer-decision-mode.js";
3
4
  import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
4
5
  import { canonicalJson } from "./canonical.js";
5
6
  export class StaleServerReviewSessionError extends Error {
@@ -114,3 +115,113 @@ function sha256(value) {
114
115
  function isoTimestamp(value) {
115
116
  return value instanceof Date ? value.toISOString() : value;
116
117
  }
118
+ /**
119
+ * One-call server apply: collapses the "resolve server record → derive apply
120
+ * result → normalize freshness/event errors → (optionally) enforce
121
+ * producerPolicy.decisionMode → (optionally) map results to product actions"
122
+ * choreography into a single call.
123
+ *
124
+ * Returns a discriminated `{ ok }` result rather than throwing for expected
125
+ * failure modes (stale session, invalid events, unresolved items, decision-mode
126
+ * violations, action-mapping failures) — matching the
127
+ * `deriveReviewSessionApplyResultForSnapshot` idiom. Unexpected errors still
128
+ * propagate.
129
+ *
130
+ * Supply either a pre-built `record` or a `snapshot` (a record is then built via
131
+ * `createServerReviewSessionRecord`). `enforceProducerPolicy` is off by default;
132
+ * unset `producerPolicy`/`decisionMode` never changes behavior.
133
+ */
134
+ export function applyReviewSession(options) {
135
+ const record = resolveApplyReviewSessionRecord(options);
136
+ let derived;
137
+ try {
138
+ derived = deriveServerReviewSessionApplyResult({
139
+ record,
140
+ events: options.events,
141
+ currentSnapshot: options.currentSnapshot,
142
+ currentEventCount: options.currentEventCount,
143
+ requiredResolvedItems: options.requiredResolvedItems,
144
+ });
145
+ }
146
+ catch (error) {
147
+ if (error instanceof StaleServerReviewSessionError) {
148
+ return { ok: false, issues: [{ code: "stale-session", message: error.message }], decisions: [], results: [], actions: [] };
149
+ }
150
+ if (error instanceof ServerReviewSessionEventValidationError) {
151
+ return { ok: false, issues: [{ code: "invalid-events", message: error.message }], decisions: [], results: [], actions: [] };
152
+ }
153
+ throw error;
154
+ }
155
+ if (!derived.ok) {
156
+ return { ok: false, issues: derived.issues, decisions: derived.decisions, results: derived.results, actions: [] };
157
+ }
158
+ if (options.enforceProducerPolicy) {
159
+ const itemsByName = new Map(record.snapshot.items.map((item) => [item.metadata.name, item]));
160
+ const violations = [];
161
+ for (const result of derived.results) {
162
+ const item = itemsByName.get(result.reviewItemName);
163
+ if (!item) {
164
+ continue;
165
+ }
166
+ const issues = validateReviewDecisionMode(item, result);
167
+ if (issues.length > 0) {
168
+ violations.push({
169
+ code: "decision-mode-violation",
170
+ reviewItemName: result.reviewItemName,
171
+ message: issues.map((issue) => issue.message).join(" "),
172
+ issues,
173
+ });
174
+ }
175
+ }
176
+ if (violations.length > 0) {
177
+ return { ok: false, issues: violations, decisions: derived.decisions, results: derived.results, actions: [] };
178
+ }
179
+ }
180
+ let actions = [];
181
+ if (options.mapActions) {
182
+ try {
183
+ actions = mapReviewWorkbenchResultsToApplyActions({
184
+ ...options.mapActions,
185
+ results: derived.results,
186
+ items: record.snapshot.items,
187
+ });
188
+ }
189
+ catch (error) {
190
+ if (error instanceof ReviewApplyActionMappingError) {
191
+ return {
192
+ ok: false,
193
+ issues: [{ code: "action-mapping-failed", message: error.message, issues: error.issues }],
194
+ decisions: derived.decisions,
195
+ results: derived.results,
196
+ actions: [],
197
+ };
198
+ }
199
+ throw error;
200
+ }
201
+ }
202
+ return {
203
+ ok: true,
204
+ issues: [],
205
+ decisions: derived.decisions,
206
+ results: derived.results,
207
+ actions,
208
+ replayedSession: derived.replayedSession,
209
+ };
210
+ }
211
+ function resolveApplyReviewSessionRecord(options) {
212
+ if (options.record) {
213
+ return options.record;
214
+ }
215
+ if (!options.snapshot) {
216
+ throw new Error("applyReviewSession requires either a server `record` or a `snapshot` to build one.");
217
+ }
218
+ const sessionName = options.sessionName ?? options.events[0]?.spec.sessionName;
219
+ if (!sessionName) {
220
+ throw new Error("applyReviewSession requires a `sessionName` (or a `record`, or events that name their session) to build a server record.");
221
+ }
222
+ return createServerReviewSessionRecord({
223
+ sessionName,
224
+ snapshot: options.snapshot,
225
+ updatedAt: options.recordUpdatedAt,
226
+ });
227
+ }
@@ -129,7 +129,23 @@ export interface ClaimTarget {
129
129
  eventMethod?: string;
130
130
  metadata?: Record<string, unknown>;
131
131
  }
132
+ /**
133
+ * Shape-contract version for the flat record arrays in a {@link SurveyInput}
134
+ * batch (`rawSources`, `extractions`, `candidateSets`, `reviewOutcomes`,
135
+ * `claims`). Stamped by {@link SurveyInputBuilder.build} when a batch does not
136
+ * set one of its own. See `docs/record-contracts.md` for the compatibility
137
+ * policy — a future breaking change to any of those record shapes must bump
138
+ * this constant and document the migration before release.
139
+ */
140
+ export declare const SURVEY_INPUT_CONTRACT_VERSION = "1";
132
141
  export interface SurveyInput {
142
+ /**
143
+ * Identifies the shape contract for the record arrays in this batch. Optional
144
+ * and defaults to {@link SURVEY_INPUT_CONTRACT_VERSION} when omitted, so
145
+ * batches built before this field existed remain valid. Compatibility marker
146
+ * only — Survey does not gate records on it. See `docs/record-contracts.md`.
147
+ */
148
+ contractVersion?: string;
133
149
  source: string;
134
150
  generatedAt: string;
135
151
  rawSources: RawSource[];
package/dist/src/types.js CHANGED
@@ -1 +1,9 @@
1
- export {};
1
+ /**
2
+ * Shape-contract version for the flat record arrays in a {@link SurveyInput}
3
+ * batch (`rawSources`, `extractions`, `candidateSets`, `reviewOutcomes`,
4
+ * `claims`). Stamped by {@link SurveyInputBuilder.build} when a batch does not
5
+ * set one of its own. See `docs/record-contracts.md` for the compatibility
6
+ * policy — a future breaking change to any of those record shapes must bump
7
+ * this constant and document the migration before release.
8
+ */
9
+ export const SURVEY_INPUT_CONTRACT_VERSION = "1";
@@ -0,0 +1,78 @@
1
+ import type { ConfidenceBasis, ImpactLevel, TrustStatus } from "@kontourai/surface";
2
+ /**
3
+ * Builds a stable, url-safe identifier from ordered parts. Each part is
4
+ * lowercased, non-alphanumeric runs collapse to a single hyphen, leading and
5
+ * trailing hyphens are trimmed, and parts join with a dot.
6
+ *
7
+ * `stableId(["Public Record", "entity-123", "current"])` → `"public-record.entity-123.current"`.
8
+ *
9
+ * Producers use this to derive deterministic candidate, candidate-set, and claim
10
+ * identifiers from domain values without inventing their own slug helper.
11
+ */
12
+ export declare function stableId(parts: ReadonlyArray<string | number>): string;
13
+ /**
14
+ * A product's Survey/Surface vocabulary: the subject type and surface it
15
+ * projects onto, its claim-type names, and its decision-effect names. Generic
16
+ * over the caller's claim-type and decision-effect key maps so the concrete
17
+ * string literals stay visible to callers.
18
+ */
19
+ export interface ProductVocabularyDefinition<TClaimTypes extends Readonly<Record<string, string>>, TDecisionEffects extends Readonly<Record<string, string>>> {
20
+ readonly subjectType: string;
21
+ readonly surface: string;
22
+ readonly claimTypes: TClaimTypes;
23
+ readonly decisionEffects: TDecisionEffects;
24
+ }
25
+ /**
26
+ * Defines a product vocabulary as a deep-frozen, discoverable value that a
27
+ * `currentProposedReviewItem` caller can pass instead of loose top-level
28
+ * constants. Returns the same shape it received, frozen so callers cannot
29
+ * mutate a shared vocabulary at runtime.
30
+ */
31
+ export declare function defineProductVocabulary<TClaimTypes extends Readonly<Record<string, string>>, TDecisionEffects extends Readonly<Record<string, string>>>(definition: ProductVocabularyDefinition<TClaimTypes, TDecisionEffects>): ProductVocabularyDefinition<TClaimTypes, TDecisionEffects>;
32
+ export interface ConfidenceBasisForReviewInput {
33
+ readonly status: TrustStatus;
34
+ readonly impactLevel: ImpactLevel;
35
+ readonly extractionConfidence?: number;
36
+ readonly sourceQuality?: ConfidenceBasis["sourceQuality"];
37
+ readonly reviewerAuthority?: ConfidenceBasis["reviewerAuthority"];
38
+ readonly evidenceStrength?: ConfidenceBasis["evidenceStrength"];
39
+ }
40
+ /**
41
+ * Builds a {@link ConfidenceBasis} from a reviewed status and impact level.
42
+ *
43
+ * The two known real-world consumers hand-roll *different* five-field
44
+ * mappings, not one shared algorithm: one derives `sourceQuality` and
45
+ * `evidenceStrength` from whether any extraction/review support exists at
46
+ * all, and never returns `"strong"` for either field even when `status` is
47
+ * `"verified"` with no supporting evidence; the other derives `sourceQuality`
48
+ * from the extracted document's source type (independent of `status`) and
49
+ * `evidenceStrength` from `status` alone, never returning `"weak"` or
50
+ * `"none"`. There is no single formula that reproduces both, so this helper
51
+ * does not attempt to guess one.
52
+ *
53
+ * Defaults are therefore conservative-by-default: `sourceQuality` defaults to
54
+ * `"unknown"` and `evidenceStrength` defaults to `"none"` — the weakest
55
+ * possible values — unless the caller supplies an explicit value. The only
56
+ * field this helper derives from `status` without an explicit override is
57
+ * `reviewerAuthority` (`"operator"` when `status === "verified"`, otherwise
58
+ * `"none"`), because that is the one mapping both known real algorithms
59
+ * agree on for every non-verified status, and it is exactly what the more
60
+ * conservative of the two returns for the verified case too. `impactLevel`
61
+ * is always the caller-supplied value (never defaulted). `extractionConfidence`
62
+ * is copied through and included on the result only when provided; its mere
63
+ * presence no longer implies a stronger `sourceQuality`/`evidenceStrength` by
64
+ * default — callers who know that an extraction or review should count as
65
+ * support must say so explicitly.
66
+ *
67
+ * Producers with domain knowledge about their own source quality or evidence
68
+ * strength should pass `sourceQuality`/`evidenceStrength` explicitly instead
69
+ * of relying on the bare defaults — for example, a source-type-driven
70
+ * mapping (strong for corrected/high-confidence documents, moderate for
71
+ * medium-confidence documents, weak otherwise) the way one known consumer
72
+ * derives `sourceQuality` from its extracted record's source type. This
73
+ * function is a conservative, independently-designed baseline, not a
74
+ * behavioral match for either known consumer's algorithm; verify against
75
+ * your own data before treating it as a drop-in replacement for hand-rolled
76
+ * mapping logic.
77
+ */
78
+ export declare function confidenceBasisForReview(input: ConfidenceBasisForReviewInput): ConfidenceBasis;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Builds a stable, url-safe identifier from ordered parts. Each part is
3
+ * lowercased, non-alphanumeric runs collapse to a single hyphen, leading and
4
+ * trailing hyphens are trimmed, and parts join with a dot.
5
+ *
6
+ * `stableId(["Public Record", "entity-123", "current"])` → `"public-record.entity-123.current"`.
7
+ *
8
+ * Producers use this to derive deterministic candidate, candidate-set, and claim
9
+ * identifiers from domain values without inventing their own slug helper.
10
+ */
11
+ export function stableId(parts) {
12
+ return parts
13
+ .map((part) => String(part)
14
+ .replace(/[^a-zA-Z0-9]+/g, "-")
15
+ .replace(/^-|-$/g, "")
16
+ .toLowerCase())
17
+ .join(".");
18
+ }
19
+ /**
20
+ * Defines a product vocabulary as a deep-frozen, discoverable value that a
21
+ * `currentProposedReviewItem` caller can pass instead of loose top-level
22
+ * constants. Returns the same shape it received, frozen so callers cannot
23
+ * mutate a shared vocabulary at runtime.
24
+ */
25
+ export function defineProductVocabulary(definition) {
26
+ return deepFreeze({
27
+ subjectType: definition.subjectType,
28
+ surface: definition.surface,
29
+ claimTypes: { ...definition.claimTypes },
30
+ decisionEffects: { ...definition.decisionEffects },
31
+ });
32
+ }
33
+ /**
34
+ * Builds a {@link ConfidenceBasis} from a reviewed status and impact level.
35
+ *
36
+ * The two known real-world consumers hand-roll *different* five-field
37
+ * mappings, not one shared algorithm: one derives `sourceQuality` and
38
+ * `evidenceStrength` from whether any extraction/review support exists at
39
+ * all, and never returns `"strong"` for either field even when `status` is
40
+ * `"verified"` with no supporting evidence; the other derives `sourceQuality`
41
+ * from the extracted document's source type (independent of `status`) and
42
+ * `evidenceStrength` from `status` alone, never returning `"weak"` or
43
+ * `"none"`. There is no single formula that reproduces both, so this helper
44
+ * does not attempt to guess one.
45
+ *
46
+ * Defaults are therefore conservative-by-default: `sourceQuality` defaults to
47
+ * `"unknown"` and `evidenceStrength` defaults to `"none"` — the weakest
48
+ * possible values — unless the caller supplies an explicit value. The only
49
+ * field this helper derives from `status` without an explicit override is
50
+ * `reviewerAuthority` (`"operator"` when `status === "verified"`, otherwise
51
+ * `"none"`), because that is the one mapping both known real algorithms
52
+ * agree on for every non-verified status, and it is exactly what the more
53
+ * conservative of the two returns for the verified case too. `impactLevel`
54
+ * is always the caller-supplied value (never defaulted). `extractionConfidence`
55
+ * is copied through and included on the result only when provided; its mere
56
+ * presence no longer implies a stronger `sourceQuality`/`evidenceStrength` by
57
+ * default — callers who know that an extraction or review should count as
58
+ * support must say so explicitly.
59
+ *
60
+ * Producers with domain knowledge about their own source quality or evidence
61
+ * strength should pass `sourceQuality`/`evidenceStrength` explicitly instead
62
+ * of relying on the bare defaults — for example, a source-type-driven
63
+ * mapping (strong for corrected/high-confidence documents, moderate for
64
+ * medium-confidence documents, weak otherwise) the way one known consumer
65
+ * derives `sourceQuality` from its extracted record's source type. This
66
+ * function is a conservative, independently-designed baseline, not a
67
+ * behavioral match for either known consumer's algorithm; verify against
68
+ * your own data before treating it as a drop-in replacement for hand-rolled
69
+ * mapping logic.
70
+ */
71
+ export function confidenceBasisForReview(input) {
72
+ const verified = input.status === "verified";
73
+ const basis = {
74
+ sourceQuality: input.sourceQuality ?? "unknown",
75
+ reviewerAuthority: input.reviewerAuthority ?? (verified ? "operator" : "none"),
76
+ evidenceStrength: input.evidenceStrength ?? "none",
77
+ impactLevel: input.impactLevel,
78
+ };
79
+ if (input.extractionConfidence !== undefined) {
80
+ basis.extractionConfidence = input.extractionConfidence;
81
+ }
82
+ return basis;
83
+ }
84
+ function deepFreeze(value) {
85
+ if (value && typeof value === "object") {
86
+ for (const nested of Object.values(value)) {
87
+ deepFreeze(nested);
88
+ }
89
+ Object.freeze(value);
90
+ }
91
+ return value;
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
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",
@@ -68,7 +68,7 @@
68
68
  "check:generated-css": "node scripts/copy-review-workbench-package-assets.cjs --check"
69
69
  },
70
70
  "dependencies": {
71
- "@kontourai/surface": "^0.9.0"
71
+ "@kontourai/surface": "^1.3.0"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "@anthropic-ai/sdk": ">=0.20.0"