@kontourai/survey 1.18.1 → 2.0.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/README.md +1 -1
- package/dist/src/agent-utterance.js +4 -5
- package/dist/src/canonical-reviewed-trust-input.d.ts +31 -0
- package/dist/src/canonical-reviewed-trust-input.js +265 -0
- package/dist/src/extraction-improvement-proposal.d.ts +148 -0
- package/dist/src/extraction-improvement-proposal.js +396 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +2 -0
- package/dist/src/to-surface.d.ts +11 -0
- package/dist/src/to-surface.js +27 -6
- package/package.json +3 -11
- package/dist/src/anthropic.d.ts +0 -104
- package/dist/src/anthropic.js +0 -383
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { buildReviewItemsFromExtractionEnvelopeImport } from "./extraction-envelope.js";
|
|
3
|
+
import { reviewResourceApiVersion } from "./review-resource.js";
|
|
4
|
+
import { canonicalJson } from "./review-workbench/canonical.js";
|
|
5
|
+
/**
|
|
6
|
+
* Folds producer-owned disposition records without performing I/O or selecting
|
|
7
|
+
* between conflicting decisions. Byte-equivalent replay is idempotent.
|
|
8
|
+
*/
|
|
9
|
+
export function foldExtractionImprovementDispositions(input) {
|
|
10
|
+
const byKey = new Map();
|
|
11
|
+
for (const disposition of input) {
|
|
12
|
+
const records = byKey.get(disposition.dispositionKey) ?? new Map();
|
|
13
|
+
records.set(canonicalJson(disposition), disposition);
|
|
14
|
+
byKey.set(disposition.dispositionKey, records);
|
|
15
|
+
}
|
|
16
|
+
const dispositions = [];
|
|
17
|
+
const conflicts = [];
|
|
18
|
+
for (const dispositionKey of [...byKey.keys()].sort()) {
|
|
19
|
+
const records = [...byKey.get(dispositionKey).entries()]
|
|
20
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
21
|
+
.map(([, disposition]) => disposition);
|
|
22
|
+
if (records.length === 1) {
|
|
23
|
+
dispositions.push(records[0]);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
conflicts.push(Object.freeze({
|
|
27
|
+
kind: "survey.extraction-improvement-disposition-conflict",
|
|
28
|
+
dispositionKey,
|
|
29
|
+
dispositions: Object.freeze(records),
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
dispositions: Object.freeze(dispositions),
|
|
35
|
+
conflicts: Object.freeze(conflicts),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Builds a frozen draft from validated canonical records. Performs no I/O. */
|
|
39
|
+
export function buildExtractionImprovementProposal(input) {
|
|
40
|
+
const diagnosis = normalizeDiagnosis(input.diagnosis);
|
|
41
|
+
const lineage = projectValidatedLineage(input);
|
|
42
|
+
const review = projectValidatedReview(input, diagnosis, lineage.proposalId);
|
|
43
|
+
const outcome = exactlyOne(input.survey.reviewOutcomes, ({ id }) => id === input.reviewOutcomeId, "review outcome");
|
|
44
|
+
lineage.recordDigests.reviewOutcome = recordDigest("ReviewOutcome", outcome);
|
|
45
|
+
const payload = {
|
|
46
|
+
kind: "survey.extraction-improvement-proposal",
|
|
47
|
+
schemaVersion: 1,
|
|
48
|
+
state: "draft",
|
|
49
|
+
createdAt: canonicalTimestamp(input.createdAt, "createdAt"),
|
|
50
|
+
lineage,
|
|
51
|
+
diagnosis,
|
|
52
|
+
review,
|
|
53
|
+
};
|
|
54
|
+
return freeze({ id: digest("survey.extraction-improvement-proposal/v1", payload), ...payload });
|
|
55
|
+
}
|
|
56
|
+
/** Emits a reversible activation request, never an activation. */
|
|
57
|
+
export function approveExtractionImprovementProposal(input) {
|
|
58
|
+
assertDraftIntegrity(input.draft);
|
|
59
|
+
if (input.draft.diagnosis.kind === "insufficient-source-evidence") {
|
|
60
|
+
throw new Error("insufficient-source-evidence proposals cannot request a task-spec change");
|
|
61
|
+
}
|
|
62
|
+
const approval = normalizeApproval(input.approval);
|
|
63
|
+
const nextTaskSpec = normalizeTaskSpecReference(input.nextTaskSpec, "nextTaskSpec");
|
|
64
|
+
const rollbackTaskSpec = normalizeTaskSpecReference(input.rollbackTaskSpec, "rollbackTaskSpec");
|
|
65
|
+
if (nextTaskSpec.version === input.draft.lineage.taskSpec.version || nextTaskSpec.digest === input.draft.lineage.taskSpec.digest) {
|
|
66
|
+
throw new Error("nextTaskSpec must be a new version with a new digest");
|
|
67
|
+
}
|
|
68
|
+
if (canonicalJson(rollbackTaskSpec) !== canonicalJson(input.draft.lineage.taskSpec)) {
|
|
69
|
+
throw new Error("rollbackTaskSpec must exactly identify the draft's prior task spec");
|
|
70
|
+
}
|
|
71
|
+
const changes = input.draft.diagnosis.requestedTaskChanges;
|
|
72
|
+
const addsExample = changes.includes("example-addition") || changes.includes("grounded-positive-example");
|
|
73
|
+
const changesGuidance = changes.includes("guidance-update") || changes.includes("guidance-affirmation");
|
|
74
|
+
assertExampleDigestChange(input.draft.lineage.taskSpec.exampleDigests, nextTaskSpec.exampleDigests, addsExample);
|
|
75
|
+
const guidanceChangeProofDigest = input.guidanceChangeProofDigest === undefined
|
|
76
|
+
? undefined
|
|
77
|
+
: sha256Digest(input.guidanceChangeProofDigest, "guidanceChangeProofDigest");
|
|
78
|
+
if (changesGuidance && guidanceChangeProofDigest === undefined) {
|
|
79
|
+
throw new Error("guidance changes require guidanceChangeProofDigest");
|
|
80
|
+
}
|
|
81
|
+
if (!changesGuidance && guidanceChangeProofDigest !== undefined) {
|
|
82
|
+
throw new Error("guidanceChangeProofDigest is not allowed without a guidance remedy");
|
|
83
|
+
}
|
|
84
|
+
const dispositionKey = dispositionKeyFor(input.draft.id);
|
|
85
|
+
const payload = {
|
|
86
|
+
dispositionKey,
|
|
87
|
+
kind: "survey.extraction-improvement-activation-request",
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
state: "approved",
|
|
90
|
+
draftId: input.draft.id,
|
|
91
|
+
approval,
|
|
92
|
+
nextTaskSpec,
|
|
93
|
+
rollbackTaskSpec,
|
|
94
|
+
...(guidanceChangeProofDigest ? { guidanceChangeProofDigest } : {}),
|
|
95
|
+
};
|
|
96
|
+
return freeze({ id: digest("survey.extraction-improvement-activation-request/v1", payload), ...payload });
|
|
97
|
+
}
|
|
98
|
+
/** Emits an idempotent terminal rejection with the same disposition key as approval. */
|
|
99
|
+
export function rejectExtractionImprovementProposal(input) {
|
|
100
|
+
assertDraftIntegrity(input.draft);
|
|
101
|
+
const rejection = normalizeRejection(input.rejection);
|
|
102
|
+
const payload = {
|
|
103
|
+
dispositionKey: dispositionKeyFor(input.draft.id),
|
|
104
|
+
kind: "survey.extraction-improvement-rejection",
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
state: "rejected",
|
|
107
|
+
draftId: input.draft.id,
|
|
108
|
+
rejection,
|
|
109
|
+
};
|
|
110
|
+
return freeze({ id: digest("survey.extraction-improvement-rejection/v1", payload), ...payload });
|
|
111
|
+
}
|
|
112
|
+
function projectValidatedLineage(input) {
|
|
113
|
+
const canonicalItems = buildReviewItemsFromExtractionEnvelopeImport(input.extractionImport);
|
|
114
|
+
if (!Number.isSafeInteger(input.proposalIndex) || input.proposalIndex < 0 || input.proposalIndex >= canonicalItems.length) {
|
|
115
|
+
throw new Error("proposalIndex does not identify a grounded extraction proposal");
|
|
116
|
+
}
|
|
117
|
+
const canonicalItem = canonicalItems[input.proposalIndex];
|
|
118
|
+
if (canonicalJson(canonicalItem) !== canonicalJson(input.reviewItem)) {
|
|
119
|
+
throw new Error("reviewItem is not the canonical projection of extractionImport and proposalIndex");
|
|
120
|
+
}
|
|
121
|
+
const envelope = input.extractionImport.spec.envelope;
|
|
122
|
+
const proposal = envelope.result.proposals[input.proposalIndex];
|
|
123
|
+
const artifact = envelope.result.preparedArtifact;
|
|
124
|
+
if (!envelope.result.taskDigest || !artifact || !envelope.source.snapshotRef) {
|
|
125
|
+
throw new Error("extraction improvement requires task, prepared-artifact, and source-snapshot grounding");
|
|
126
|
+
}
|
|
127
|
+
const candidate = exactlyOne(input.reviewItem.spec.candidates, () => true, "review candidate");
|
|
128
|
+
if (!candidate.extraction.extractionId)
|
|
129
|
+
throw new Error("canonical review candidate requires an extraction id");
|
|
130
|
+
return {
|
|
131
|
+
taskSpec: normalizeTaskSpecReference({
|
|
132
|
+
version: nonEmpty(input.priorTaskSpecVersion, "priorTaskSpecVersion"),
|
|
133
|
+
digest: envelope.result.taskDigest,
|
|
134
|
+
exampleDigests: envelope.result.exampleDigests ?? [],
|
|
135
|
+
}, "lineage.taskSpec"),
|
|
136
|
+
extractionImportName: input.extractionImport.metadata.name,
|
|
137
|
+
extractionId: candidate.extraction.extractionId,
|
|
138
|
+
proposalId: candidate.id,
|
|
139
|
+
reviewItemName: input.reviewItem.metadata.name,
|
|
140
|
+
reviewDecisionName: validateReviewDecision(input.reviewDecision, input.reviewItem),
|
|
141
|
+
reviewOutcomeId: nonEmpty(input.reviewOutcomeId, "reviewOutcomeId"),
|
|
142
|
+
sourceSnapshotRef: envelope.source.snapshotRef,
|
|
143
|
+
preparedArtifact: { ref: artifact.ref, digest: rawSha256Digest(artifact.digest, "preparedArtifact.digest") },
|
|
144
|
+
excerptLocator: nonEmpty(proposal.provenance.locator, "proposal.provenance.locator"),
|
|
145
|
+
recordDigests: {
|
|
146
|
+
extractionImport: recordDigest("ExtractionEnvelopeImport", input.extractionImport),
|
|
147
|
+
reviewItem: recordDigest("ReviewItem", input.reviewItem),
|
|
148
|
+
reviewDecision: recordDigest("ReviewDecision", input.reviewDecision),
|
|
149
|
+
reviewOutcome: "",
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function projectValidatedReview(input, diagnosis, proposalId) {
|
|
154
|
+
const outcome = exactlyOne(input.survey.reviewOutcomes, ({ id }) => id === input.reviewOutcomeId, "review outcome");
|
|
155
|
+
const decision = input.reviewDecision;
|
|
156
|
+
const projection = decision.spec.projection;
|
|
157
|
+
if (!projection || projection.reviewOutcomeId !== outcome.id || projection.candidateSetId !== outcome.candidateSetId) {
|
|
158
|
+
throw new Error("reviewDecision projection must identify the concrete Survey review outcome and candidate set");
|
|
159
|
+
}
|
|
160
|
+
if (decision.spec.candidateId !== outcome.candidateId || projection.candidateId !== outcome.candidateId) {
|
|
161
|
+
throw new Error("reviewDecision and reviewOutcome candidate lineage do not match");
|
|
162
|
+
}
|
|
163
|
+
if (outcome.candidateId !== undefined && outcome.candidateId !== proposalId) {
|
|
164
|
+
throw new Error("any identified review candidate must be the canonical imported proposal");
|
|
165
|
+
}
|
|
166
|
+
assertDecisionOutcomeField(decision.spec.status, outcome.status, "status");
|
|
167
|
+
assertDecisionOutcomeField(decision.spec.resolution, outcome.resolution, "resolution");
|
|
168
|
+
assertDecisionOutcomeField(decision.spec.resolutionReason, outcome.resolutionReason, "resolutionReason");
|
|
169
|
+
assertDecisionOutcomeField(decision.spec.actor?.id, outcome.actor, "actor");
|
|
170
|
+
assertDecisionOutcomeField(decision.spec.reviewedAt, outcome.reviewedAt, "reviewedAt");
|
|
171
|
+
assertDecisionOutcomeField(decision.spec.rationale, outcome.rationale, "rationale");
|
|
172
|
+
assertSetEqual(decision.spec.evidenceIds ?? [], outcome.evidenceIds ?? [], "evidenceIds");
|
|
173
|
+
assertSetEqual(decision.spec.attemptEvidenceIds ?? [], outcome.attemptEvidenceIds ?? [], "attemptEvidenceIds");
|
|
174
|
+
assertDiagnosisReview(diagnosis, outcome);
|
|
175
|
+
const review = {
|
|
176
|
+
resolution: outcome.resolution,
|
|
177
|
+
status: outcome.status,
|
|
178
|
+
reviewedAt: canonicalTimestamp(outcome.reviewedAt, "reviewOutcome.reviewedAt"),
|
|
179
|
+
rationale: nonEmpty(outcome.rationale ?? outcome.resolutionReason, "review rationale"),
|
|
180
|
+
evidenceIds: sortedUnique(outcome.evidenceIds ?? [], "review.evidenceIds", nonEmpty),
|
|
181
|
+
attemptEvidenceIds: sortedUnique(outcome.attemptEvidenceIds ?? [], "review.attemptEvidenceIds", nonEmpty),
|
|
182
|
+
};
|
|
183
|
+
return review;
|
|
184
|
+
}
|
|
185
|
+
function validateReviewDecision(decision, item) {
|
|
186
|
+
if (!decision || decision.apiVersion !== reviewResourceApiVersion || decision.kind !== "ReviewDecision") {
|
|
187
|
+
throw new Error("reviewDecision must be a canonical ReviewDecision resource");
|
|
188
|
+
}
|
|
189
|
+
if (decision.spec.reviewItemName !== item.metadata.name)
|
|
190
|
+
throw new Error("reviewDecision does not join to reviewItem");
|
|
191
|
+
return nonEmpty(decision.metadata.name, "reviewDecision.metadata.name");
|
|
192
|
+
}
|
|
193
|
+
function assertDiagnosisReview(diagnosis, outcome) {
|
|
194
|
+
if (diagnosis.kind === "accepted-extraction") {
|
|
195
|
+
if (outcome.resolution !== "accepted" || (outcome.status !== "verified" && outcome.status !== "assumed")) {
|
|
196
|
+
throw new Error("accepted-extraction requires an accepted verified or assumed review outcome");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (diagnosis.kind === "bad-extraction") {
|
|
200
|
+
if (outcome.resolution !== "rejected" || outcome.status !== "rejected") {
|
|
201
|
+
throw new Error("bad-extraction requires an explicit rejected review outcome");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (outcome.resolution !== "could_not_confirm") {
|
|
205
|
+
throw new Error("insufficient-source-evidence requires an explicit could_not_confirm review outcome");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function normalizeDiagnosis(value) {
|
|
209
|
+
const diagnosis = requiredObject(value, "diagnosis");
|
|
210
|
+
if (diagnosis.kind === "bad-extraction") {
|
|
211
|
+
const requestedTaskChanges = normalizeChanges(diagnosis.requestedTaskChanges, "diagnosis.requestedTaskChanges", ["example-addition", "guidance-update"]);
|
|
212
|
+
return { kind: "bad-extraction", requestedTaskChanges };
|
|
213
|
+
}
|
|
214
|
+
if (diagnosis.kind === "accepted-extraction") {
|
|
215
|
+
const requestedTaskChanges = normalizeChanges(diagnosis.requestedTaskChanges, "diagnosis.requestedTaskChanges", ["grounded-positive-example", "guidance-affirmation"]);
|
|
216
|
+
return { kind: "accepted-extraction", requestedTaskChanges };
|
|
217
|
+
}
|
|
218
|
+
if (diagnosis.kind === "insufficient-source-evidence") {
|
|
219
|
+
const sourceRemediation = diagnosis.sourceRemediation;
|
|
220
|
+
if (sourceRemediation !== "obtain-authoritative-source" && sourceRemediation !== "refresh-source-snapshot" && sourceRemediation !== "expand-source-scope") {
|
|
221
|
+
throw new Error("insufficient-source-evidence requires an explicit source remediation");
|
|
222
|
+
}
|
|
223
|
+
return { kind: "insufficient-source-evidence", sourceRemediation };
|
|
224
|
+
}
|
|
225
|
+
throw new Error("diagnosis.kind must be explicit");
|
|
226
|
+
}
|
|
227
|
+
function normalizeChanges(value, label, allowed) {
|
|
228
|
+
const changes = sortedUnique(value, label, (change, field) => {
|
|
229
|
+
if (typeof change !== "string" || !allowed.includes(change))
|
|
230
|
+
throw new Error(`${field} contains an unsupported task change`);
|
|
231
|
+
return change;
|
|
232
|
+
});
|
|
233
|
+
if (changes.length === 0)
|
|
234
|
+
throw new Error(`${label} requires at least one explicit task change`);
|
|
235
|
+
return changes;
|
|
236
|
+
}
|
|
237
|
+
function assertExampleDigestChange(prior, next, addsExample) {
|
|
238
|
+
const priorSet = new Set(prior);
|
|
239
|
+
const nextSet = new Set(next);
|
|
240
|
+
const strictSuperset = prior.every((value) => nextSet.has(value)) && next.length > prior.length;
|
|
241
|
+
if (addsExample && !strictSuperset)
|
|
242
|
+
throw new Error("example remedy requires next example digests to be a strict superset");
|
|
243
|
+
if (!addsExample && canonicalJson(prior) !== canonicalJson(next))
|
|
244
|
+
throw new Error("next example digests changed without an example remedy");
|
|
245
|
+
}
|
|
246
|
+
function assertDraftIntegrity(draft) {
|
|
247
|
+
if (!draft || draft.kind !== "survey.extraction-improvement-proposal" || draft.schemaVersion !== 1 || draft.state !== "draft") {
|
|
248
|
+
throw new Error("disposition requires a draft extraction improvement proposal");
|
|
249
|
+
}
|
|
250
|
+
const normalizedDiagnosis = normalizeDiagnosis(draft.diagnosis);
|
|
251
|
+
if (canonicalJson(normalizedDiagnosis) !== canonicalJson(draft.diagnosis))
|
|
252
|
+
throw new Error("draft diagnosis is not canonical");
|
|
253
|
+
const normalizedTask = normalizeTaskSpecReference(draft.lineage.taskSpec, "draft.lineage.taskSpec");
|
|
254
|
+
if (canonicalJson(normalizedTask) !== canonicalJson(draft.lineage.taskSpec))
|
|
255
|
+
throw new Error("draft task reference is not canonical");
|
|
256
|
+
for (const [field, value] of Object.entries(draft.lineage.recordDigests))
|
|
257
|
+
sha256Digest(value, `draft.lineage.recordDigests.${field}`);
|
|
258
|
+
rawSha256Digest(draft.lineage.preparedArtifact.digest, "draft.lineage.preparedArtifact.digest");
|
|
259
|
+
nonEmpty(draft.lineage.preparedArtifact.ref, "draft.lineage.preparedArtifact.ref");
|
|
260
|
+
for (const [field, value] of Object.entries({
|
|
261
|
+
extractionImportName: draft.lineage.extractionImportName,
|
|
262
|
+
extractionId: draft.lineage.extractionId,
|
|
263
|
+
proposalId: draft.lineage.proposalId,
|
|
264
|
+
reviewItemName: draft.lineage.reviewItemName,
|
|
265
|
+
reviewDecisionName: draft.lineage.reviewDecisionName,
|
|
266
|
+
reviewOutcomeId: draft.lineage.reviewOutcomeId,
|
|
267
|
+
sourceSnapshotRef: draft.lineage.sourceSnapshotRef,
|
|
268
|
+
excerptLocator: draft.lineage.excerptLocator,
|
|
269
|
+
}))
|
|
270
|
+
nonEmpty(value, `draft.lineage.${field}`);
|
|
271
|
+
canonicalTimestamp(draft.createdAt, "draft.createdAt");
|
|
272
|
+
canonicalTimestamp(draft.review.reviewedAt, "draft.review.reviewedAt");
|
|
273
|
+
nonEmpty(draft.review.rationale, "draft.review.rationale");
|
|
274
|
+
const evidenceIds = sortedUnique(draft.review.evidenceIds, "draft.review.evidenceIds", nonEmpty);
|
|
275
|
+
const attemptEvidenceIds = sortedUnique(draft.review.attemptEvidenceIds, "draft.review.attemptEvidenceIds", nonEmpty);
|
|
276
|
+
if (canonicalJson(evidenceIds) !== canonicalJson(draft.review.evidenceIds)
|
|
277
|
+
|| canonicalJson(attemptEvidenceIds) !== canonicalJson(draft.review.attemptEvidenceIds)) {
|
|
278
|
+
throw new Error("draft review evidence is not canonical");
|
|
279
|
+
}
|
|
280
|
+
assertDraftDiagnosisReview(draft.diagnosis, draft.review);
|
|
281
|
+
const { id, ...payload } = draft;
|
|
282
|
+
if (id !== digest("survey.extraction-improvement-proposal/v1", payload)) {
|
|
283
|
+
throw new Error("extraction improvement draft does not match its immutable identity");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function assertDraftDiagnosisReview(diagnosis, review) {
|
|
287
|
+
if (diagnosis.kind === "accepted-extraction") {
|
|
288
|
+
if (review.resolution !== "accepted" || (review.status !== "verified" && review.status !== "assumed")) {
|
|
289
|
+
throw new Error("draft accepted-extraction review is inconsistent");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (diagnosis.kind === "bad-extraction") {
|
|
293
|
+
if (review.resolution !== "rejected" || review.status !== "rejected")
|
|
294
|
+
throw new Error("draft bad-extraction review is inconsistent");
|
|
295
|
+
}
|
|
296
|
+
else if (review.resolution !== "could_not_confirm") {
|
|
297
|
+
throw new Error("draft insufficient-source-evidence review is inconsistent");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function normalizeTaskSpecReference(value, label) {
|
|
301
|
+
const task = requiredObject(value, label);
|
|
302
|
+
return {
|
|
303
|
+
version: nonEmpty(task.version, `${label}.version`),
|
|
304
|
+
digest: sha256Digest(task.digest, `${label}.digest`),
|
|
305
|
+
exampleDigests: sortedUnique(task.exampleDigests, `${label}.exampleDigests`, sha256Digest),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function normalizeApproval(value) {
|
|
309
|
+
const approval = requiredObject(value, "approval");
|
|
310
|
+
return {
|
|
311
|
+
id: nonEmpty(approval.id, "approval.id"), actor: nonEmpty(approval.actor, "approval.actor"),
|
|
312
|
+
approvedAt: canonicalTimestamp(approval.approvedAt, "approval.approvedAt"),
|
|
313
|
+
rationale: nonEmpty(approval.rationale, "approval.rationale"),
|
|
314
|
+
evidenceIds: sortedUnique(approval.evidenceIds, "approval.evidenceIds", nonEmpty),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function normalizeRejection(value) {
|
|
318
|
+
const rejection = requiredObject(value, "rejection");
|
|
319
|
+
return {
|
|
320
|
+
id: nonEmpty(rejection.id, "rejection.id"), actor: nonEmpty(rejection.actor, "rejection.actor"),
|
|
321
|
+
rejectedAt: canonicalTimestamp(rejection.rejectedAt, "rejection.rejectedAt"),
|
|
322
|
+
rationale: nonEmpty(rejection.rationale, "rejection.rationale"),
|
|
323
|
+
evidenceIds: sortedUnique(rejection.evidenceIds, "rejection.evidenceIds", nonEmpty),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function dispositionKeyFor(draftId) {
|
|
327
|
+
return digest("survey.extraction-improvement-disposition/v1", { draftId });
|
|
328
|
+
}
|
|
329
|
+
function recordDigest(kind, value) {
|
|
330
|
+
return digest("survey.extraction-improvement-record/v1", { kind, value });
|
|
331
|
+
}
|
|
332
|
+
function assertDecisionOutcomeField(left, right, label) {
|
|
333
|
+
if (canonicalJson(left ?? null) !== canonicalJson(right ?? null))
|
|
334
|
+
throw new Error(`reviewDecision and reviewOutcome ${label} do not match`);
|
|
335
|
+
}
|
|
336
|
+
function assertSetEqual(left, right, label) {
|
|
337
|
+
const a = sortedUnique(left, `reviewDecision.${label}`, nonEmpty);
|
|
338
|
+
const b = sortedUnique(right, `reviewOutcome.${label}`, nonEmpty);
|
|
339
|
+
if (canonicalJson(a) !== canonicalJson(b))
|
|
340
|
+
throw new Error(`reviewDecision and reviewOutcome ${label} do not match`);
|
|
341
|
+
}
|
|
342
|
+
function exactlyOne(records, predicate, label) {
|
|
343
|
+
const matches = records.filter(predicate);
|
|
344
|
+
if (matches.length !== 1)
|
|
345
|
+
throw new Error(`extraction improvement requires exactly one ${label}; found ${matches.length}`);
|
|
346
|
+
return matches[0];
|
|
347
|
+
}
|
|
348
|
+
function requiredObject(value, label) {
|
|
349
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
350
|
+
throw new Error(`${label} must be an object`);
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
function nonEmpty(value, label) {
|
|
354
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
355
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
function sha256Digest(value, label) {
|
|
359
|
+
const digestValue = nonEmpty(value, label);
|
|
360
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(digestValue))
|
|
361
|
+
throw new Error(`${label} must be a sha256 digest`);
|
|
362
|
+
return digestValue;
|
|
363
|
+
}
|
|
364
|
+
function rawSha256Digest(value, label) {
|
|
365
|
+
const digestValue = nonEmpty(value, label);
|
|
366
|
+
if (!/^[a-f0-9]{64}$/.test(digestValue))
|
|
367
|
+
throw new Error(`${label} must be an unprefixed sha256 digest`);
|
|
368
|
+
return digestValue;
|
|
369
|
+
}
|
|
370
|
+
function canonicalTimestamp(value, label) {
|
|
371
|
+
const timestamp = nonEmpty(value, label);
|
|
372
|
+
const parsed = new Date(timestamp);
|
|
373
|
+
if (Number.isNaN(parsed.valueOf()) || parsed.toISOString() !== timestamp)
|
|
374
|
+
throw new Error(`${label} must be a canonical ISO timestamp`);
|
|
375
|
+
return timestamp;
|
|
376
|
+
}
|
|
377
|
+
function sortedUnique(values, label, normalize) {
|
|
378
|
+
if (!Array.isArray(values))
|
|
379
|
+
throw new Error(`${label} must be an array`);
|
|
380
|
+
const keyed = values.map((value, index) => normalize(value, `${label}[${index}]`))
|
|
381
|
+
.map((value) => [canonicalJson(value), value]).sort(([left], [right]) => left.localeCompare(right));
|
|
382
|
+
if (keyed.some(([value], index) => index > 0 && keyed[index - 1][0] === value))
|
|
383
|
+
throw new Error(`${label} must not contain duplicate values`);
|
|
384
|
+
return keyed.map(([, value]) => value);
|
|
385
|
+
}
|
|
386
|
+
function digest(domain, payload) {
|
|
387
|
+
return `sha256:${createHash("sha256").update(canonicalJson({ domain, payload })).digest("hex")}`;
|
|
388
|
+
}
|
|
389
|
+
function freeze(value) {
|
|
390
|
+
if (value && typeof value === "object") {
|
|
391
|
+
for (const nested of Object.values(value))
|
|
392
|
+
freeze(nested);
|
|
393
|
+
Object.freeze(value);
|
|
394
|
+
}
|
|
395
|
+
return value;
|
|
396
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -14,10 +14,14 @@ export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-r
|
|
|
14
14
|
export type { CurrentProposedCandidateRole, ReviewedCurrentProposedResolutionInput, } from "./reviewed-current-proposed-resolution.js";
|
|
15
15
|
export { buildSurveyTrustBundle } from "./to-surface.js";
|
|
16
16
|
export type { BuildSurveyTrustBundleOptions } from "./to-surface.js";
|
|
17
|
+
export { buildCanonicalReviewedTrustInput } from "./canonical-reviewed-trust-input.js";
|
|
18
|
+
export type { BuildCanonicalReviewedTrustInputOptions, CanonicalReviewedTrustInput, } from "./canonical-reviewed-trust-input.js";
|
|
17
19
|
export { buildSurveyLearningProjections } from "./learning-projections.js";
|
|
18
20
|
export type { LearningProjection, LearningProjectionKind, LearningProjectionSeverity, LearningProjectionSignal, } from "./learning-projections.js";
|
|
19
21
|
export { buildReviewedLearningUpdateProposal } from "./learning-update-proposal.js";
|
|
20
22
|
export type { LearningUpdateEvidenceReference, LearningUpdateProposal, OpaqueEvidenceReference, ProvenanceReference, ReviewedLearningUpdateProposalInput, ReviewProofReference, } from "./learning-update-proposal.js";
|
|
23
|
+
export { approveExtractionImprovementProposal, buildExtractionImprovementProposal, foldExtractionImprovementDispositions, rejectExtractionImprovementProposal, } from "./extraction-improvement-proposal.js";
|
|
24
|
+
export type { ApproveExtractionImprovementProposalInput, AcceptedExtractionDiagnosis, BadExtractionDiagnosis, BuildExtractionImprovementProposalInput, ExtractionImprovementActivationRequest, ExtractionImprovementDiagnosis, ExtractionImprovementDisposition, ExtractionImprovementDispositionConflict, ExtractionImprovementDraft, ExtractionImprovementLineage, ExtractionImprovementRejection, ExtractionImprovementRecordDigests, ExtractionImprovementReview, ExtractionTaskSpecReference, FoldExtractionImprovementDispositionsResult, InsufficientSourceEvidenceDiagnosis, ProducerApproval, RejectExtractionImprovementProposalInput, } from "./extraction-improvement-proposal.js";
|
|
21
25
|
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";
|
|
22
26
|
export type { CanonicalReviewProofPayload, CanonicalReviewProofPayloadV1, CanonicalReviewProofPayloadV2, ReviewProofInput, } from "./review-proof.js";
|
|
23
27
|
export { fieldObservation } from "./field-observation.js";
|
package/dist/src/index.js
CHANGED
|
@@ -6,8 +6,10 @@ export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from
|
|
|
6
6
|
export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
|
|
7
7
|
export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
|
|
8
8
|
export { buildSurveyTrustBundle } from "./to-surface.js";
|
|
9
|
+
export { buildCanonicalReviewedTrustInput } from "./canonical-reviewed-trust-input.js";
|
|
9
10
|
export { buildSurveyLearningProjections } from "./learning-projections.js";
|
|
10
11
|
export { buildReviewedLearningUpdateProposal } from "./learning-update-proposal.js";
|
|
12
|
+
export { approveExtractionImprovementProposal, buildExtractionImprovementProposal, foldExtractionImprovementDispositions, rejectExtractionImprovementProposal, } from "./extraction-improvement-proposal.js";
|
|
11
13
|
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";
|
|
12
14
|
export { fieldObservation } from "./field-observation.js";
|
|
13
15
|
export { repeatedObservation } from "./repeated-observation.js";
|
package/dist/src/to-surface.d.ts
CHANGED
|
@@ -18,6 +18,17 @@ export interface SurveyCalibrationOptions {
|
|
|
18
18
|
}
|
|
19
19
|
export interface BuildSurveyTrustBundleOptions {
|
|
20
20
|
reviewProofs?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Caller-owned identity for one projection or resolution context, such as a
|
|
23
|
+
* review-session, proposal, or append-only persistence record id. When set,
|
|
24
|
+
* Survey folds it into generated Evidence and VerificationEvent ids so
|
|
25
|
+
* repeated projections of the same Claim cannot collide. Claim ids and all
|
|
26
|
+
* legacy output remain unchanged when omitted.
|
|
27
|
+
*
|
|
28
|
+
* Use a portable resource-name fragment: ASCII letters, digits, `.`, `_`,
|
|
29
|
+
* `:`, or `-`, beginning with a letter or digit.
|
|
30
|
+
*/
|
|
31
|
+
projectionContextId?: string;
|
|
21
32
|
/**
|
|
22
33
|
* Populate `conclusionConfidence.value` from empirical review calibration —
|
|
23
34
|
* "how often this extractor's proposals at this confidence were affirmed by a
|
package/dist/src/to-surface.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { buildReviewProofAnchor } from "./review-proof.js";
|
|
2
3
|
import { assertReviewOutcomeDiscipline } from "./producer-discipline.js";
|
|
3
4
|
import { deriveCalibration } from "./calibration.js";
|
|
@@ -5,6 +6,7 @@ import { deriveCalibration } from "./calibration.js";
|
|
|
5
6
|
* accuracy is emitted as a `conclusionConfidence.value`. */
|
|
6
7
|
const DEFAULT_CALIBRATION_MIN_SAMPLES = 20;
|
|
7
8
|
export function buildSurveyTrustBundle(input, options = {}) {
|
|
9
|
+
const projectionContextId = validateProjectionContextId(options.projectionContextId);
|
|
8
10
|
const rawSources = indexById(input.rawSources, "raw source");
|
|
9
11
|
const extractions = indexById(input.extractions, "extraction");
|
|
10
12
|
const candidateSets = indexById(input.candidateSets, "candidate set");
|
|
@@ -37,7 +39,7 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
37
39
|
const claimValue = projection.value ?? candidate.value;
|
|
38
40
|
const createdAt = projection.createdAt ?? extraction.extractedAt;
|
|
39
41
|
const updatedAt = projection.updatedAt ?? projectionReview?.reviewedAt ?? input.generatedAt;
|
|
40
|
-
const evidenceId =
|
|
42
|
+
const evidenceId = projectionRecordId(projection.id, projectionContextId, "claim-evidence", "evidence.source");
|
|
41
43
|
const claim = {
|
|
42
44
|
id: projection.id,
|
|
43
45
|
subjectType: projection.subjectType,
|
|
@@ -147,7 +149,7 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
147
149
|
});
|
|
148
150
|
const rationale = projectionReview?.rationale ?? candidateSet.rationale;
|
|
149
151
|
events.push({
|
|
150
|
-
id:
|
|
152
|
+
id: projectionRecordId(projection.id, projectionContextId, "claim-event", `event.${status}`),
|
|
151
153
|
claimId: projection.id,
|
|
152
154
|
status,
|
|
153
155
|
actor: projection.actor ?? projectionReview?.actor ?? projection.collectedBy,
|
|
@@ -158,7 +160,7 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
158
160
|
notes: rationale,
|
|
159
161
|
});
|
|
160
162
|
}
|
|
161
|
-
projectInterpretations({ input, rawSources, claims, evidence, events });
|
|
163
|
+
projectInterpretations({ input, rawSources, claims, evidence, events, projectionContextId });
|
|
162
164
|
if (input.escalations) {
|
|
163
165
|
const claimIds = new Set(claims.map((c) => c.id));
|
|
164
166
|
for (const escalation of input.escalations) {
|
|
@@ -170,7 +172,7 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
170
172
|
throw new Error(`Escalation ${escalation.id} references unknown claim ${escalation.attachToClaimId}`);
|
|
171
173
|
}
|
|
172
174
|
events.push({
|
|
173
|
-
id:
|
|
175
|
+
id: projectionRecordId(escalation.id, projectionContextId, "escalation-event", "event"),
|
|
174
176
|
claimId: escalation.attachToClaimId,
|
|
175
177
|
status: "disputed",
|
|
176
178
|
actor: escalation.raisedBy,
|
|
@@ -190,6 +192,22 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
190
192
|
events,
|
|
191
193
|
};
|
|
192
194
|
}
|
|
195
|
+
function validateProjectionContextId(contextId) {
|
|
196
|
+
if (contextId === undefined)
|
|
197
|
+
return undefined;
|
|
198
|
+
if (typeof contextId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(contextId)) {
|
|
199
|
+
throw new Error("projectionContextId must be a non-empty portable resource-name fragment");
|
|
200
|
+
}
|
|
201
|
+
return contextId;
|
|
202
|
+
}
|
|
203
|
+
function projectionRecordId(ownerId, contextId, recordKind, legacySuffix) {
|
|
204
|
+
if (!contextId)
|
|
205
|
+
return `${ownerId}.${legacySuffix}`;
|
|
206
|
+
const digest = createHash("sha256")
|
|
207
|
+
.update(JSON.stringify(["survey-projection-context-v1", recordKind, ownerId, contextId]))
|
|
208
|
+
.digest("hex");
|
|
209
|
+
return `survey.projection.v1.${digest}.${legacySuffix}`;
|
|
210
|
+
}
|
|
193
211
|
function projectInterpretations(input) {
|
|
194
212
|
const interpretations = input.input.interpretations
|
|
195
213
|
? [...indexById(input.input.interpretations, "interpretation").values()]
|
|
@@ -204,6 +222,7 @@ function projectInterpretations(input) {
|
|
|
204
222
|
claims: input.claims,
|
|
205
223
|
claimsById,
|
|
206
224
|
input: input.input,
|
|
225
|
+
projectionContextId: input.projectionContextId,
|
|
207
226
|
});
|
|
208
227
|
input.evidence.push(projection.evidence);
|
|
209
228
|
input.events.push(projection.event);
|
|
@@ -227,6 +246,7 @@ function buildInterpretationProjection(input) {
|
|
|
227
246
|
claim,
|
|
228
247
|
anchorSource,
|
|
229
248
|
policyStandard,
|
|
249
|
+
projectionContextId: input.projectionContextId,
|
|
230
250
|
});
|
|
231
251
|
return {
|
|
232
252
|
claim,
|
|
@@ -236,6 +256,7 @@ function buildInterpretationProjection(input) {
|
|
|
236
256
|
interpretation: input.interpretation,
|
|
237
257
|
claim,
|
|
238
258
|
evidenceId: evidence.id,
|
|
259
|
+
projectionContextId: input.projectionContextId,
|
|
239
260
|
}),
|
|
240
261
|
};
|
|
241
262
|
}
|
|
@@ -248,7 +269,7 @@ function requirePolicyStandardAnchor(interpretation, rawSources) {
|
|
|
248
269
|
}
|
|
249
270
|
function createInterpretationAnchorEvidence(input) {
|
|
250
271
|
return {
|
|
251
|
-
id:
|
|
272
|
+
id: projectionRecordId(input.interpretation.id, input.projectionContextId, "interpretation-evidence", "evidence.anchor"),
|
|
252
273
|
claimId: input.claim.id,
|
|
253
274
|
evidenceType: "policy_rule",
|
|
254
275
|
method: "anchoring",
|
|
@@ -271,7 +292,7 @@ function createInterpretationAnchorEvidence(input) {
|
|
|
271
292
|
}
|
|
272
293
|
function createInterpretationEvent(input) {
|
|
273
294
|
return {
|
|
274
|
-
id:
|
|
295
|
+
id: projectionRecordId(input.interpretation.id, input.projectionContextId, "interpretation-event", "event"),
|
|
275
296
|
claimId: input.claim.id,
|
|
276
297
|
status: input.claim.status ?? "proposed",
|
|
277
298
|
actor: input.interpretation.actor,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/survey",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|
|
@@ -18,10 +18,6 @@
|
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
20
20
|
".": "./dist/src/index.js",
|
|
21
|
-
"./anthropic": {
|
|
22
|
-
"types": "./dist/src/anthropic.d.ts",
|
|
23
|
-
"default": "./dist/src/anthropic.js"
|
|
24
|
-
},
|
|
25
21
|
"./review-workbench": {
|
|
26
22
|
"types": "./dist/src/review-workbench/review-workbench.d.ts",
|
|
27
23
|
"default": "./dist/src/review-workbench/review-workbench.js"
|
|
@@ -55,7 +51,8 @@
|
|
|
55
51
|
"typecheck": "tsc --noEmit",
|
|
56
52
|
"test": "npm run build && node --test dist/tests/*.test.js",
|
|
57
53
|
"test:browser": "playwright test",
|
|
58
|
-
"
|
|
54
|
+
"test:browser:concurrent": "npm run build && node scripts/test-playwright-concurrency.mjs",
|
|
55
|
+
"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 && npm run test:browser:concurrent",
|
|
59
56
|
"check:content-boundary": "node scripts/check-content-boundary.cjs",
|
|
60
57
|
"check:decisions": "node scripts/check-decisions.cjs check",
|
|
61
58
|
"gen:decisions-index": "node scripts/check-decisions.cjs gen-index",
|
|
@@ -74,19 +71,14 @@
|
|
|
74
71
|
"@kontourai/surface": "^2.9.0"
|
|
75
72
|
},
|
|
76
73
|
"peerDependencies": {
|
|
77
|
-
"@anthropic-ai/sdk": ">=0.20.0",
|
|
78
74
|
"typescript": ">=5.0.0"
|
|
79
75
|
},
|
|
80
76
|
"peerDependenciesMeta": {
|
|
81
|
-
"@anthropic-ai/sdk": {
|
|
82
|
-
"optional": true
|
|
83
|
-
},
|
|
84
77
|
"typescript": {
|
|
85
78
|
"optional": true
|
|
86
79
|
}
|
|
87
80
|
},
|
|
88
81
|
"devDependencies": {
|
|
89
|
-
"@anthropic-ai/sdk": "^0.54.0",
|
|
90
82
|
"@kontourai/traverse": "^0.19.0",
|
|
91
83
|
"@kontourai/ui": "^1.1.0",
|
|
92
84
|
"@playwright/test": "^1.60.0",
|