engineering-behavior-observatory 0.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.
Files changed (178) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +151 -0
  3. package/contracts/codex-app-server-0.150.1/manifest.json +25 -0
  4. package/contracts/codex-app-server-0.150.1/schema/InitializeParams.json +84 -0
  5. package/contracts/codex-app-server-0.150.1/schema/JSONRPCMessage.json +137 -0
  6. package/contracts/codex-app-server-0.150.1/schema/v2/ThreadReadParams.json +17 -0
  7. package/contracts/codex-app-server-0.150.1/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
  8. package/contracts/codex-app-server-0.150.1/schema/v2/TurnInterruptParams.json +17 -0
  9. package/contracts/codex-app-server-0.150.1/types/AskForApproval.ts +5 -0
  10. package/contracts/codex-app-server-0.150.1/types/ClientNotification.ts +5 -0
  11. package/contracts/codex-app-server-0.150.1/types/SandboxMode.ts +5 -0
  12. package/contracts/codex-app-server-0.150.1/types/ThreadReadParams.ts +9 -0
  13. package/contracts/codex-app-server-0.150.1/types/TokenUsageBreakdown.ts +5 -0
  14. package/contracts/codex-app-server-0.150.1/types/TurnInterruptParams.ts +5 -0
  15. package/contracts/codex-app-server-0.153.4/manifest.json +27 -0
  16. package/contracts/codex-app-server-0.153.4/schema/JSONRPCMessage.json +137 -0
  17. package/contracts/codex-app-server-0.153.4/schema/v1/InitializeParams.json +84 -0
  18. package/contracts/codex-app-server-0.153.4/schema/v2/ThreadReadParams.json +17 -0
  19. package/contracts/codex-app-server-0.153.4/schema/v2/ThreadStartParams.json +515 -0
  20. package/contracts/codex-app-server-0.153.4/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
  21. package/contracts/codex-app-server-0.153.4/schema/v2/TurnInterruptParams.json +17 -0
  22. package/contracts/codex-app-server-0.153.4/schema/v2/TurnStartParams.json +921 -0
  23. package/contracts/codex-app-server-0.153.4/types/AskForApproval.ts +5 -0
  24. package/contracts/codex-app-server-0.153.4/types/ClientNotification.ts +5 -0
  25. package/contracts/codex-app-server-0.153.4/types/SandboxMode.ts +5 -0
  26. package/contracts/codex-app-server-0.153.4/types/ThreadReadParams.ts +12 -0
  27. package/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.ts +5 -0
  28. package/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.ts +5 -0
  29. package/contracts/openhands-agent-server-v1.44.1.json +65 -0
  30. package/contracts/openhands-agent-server-v1.46.0.json +46 -0
  31. package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.d.ts +9 -0
  32. package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.js +2 -0
  33. package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.d.ts +3 -0
  34. package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.js +2 -0
  35. package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.d.ts +1 -0
  36. package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.js +2 -0
  37. package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.d.ts +10 -0
  38. package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.js +2 -0
  39. package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.d.ts +8 -0
  40. package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.js +2 -0
  41. package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.d.ts +4 -0
  42. package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.js +2 -0
  43. package/dist/src/agent-sdk-normalizer.d.ts +22 -0
  44. package/dist/src/agent-sdk-normalizer.js +995 -0
  45. package/dist/src/agent-sdk-run.d.ts +31 -0
  46. package/dist/src/agent-sdk-run.js +242 -0
  47. package/dist/src/agent-sdk-runner.d.ts +96 -0
  48. package/dist/src/agent-sdk-runner.js +407 -0
  49. package/dist/src/agent-sdk.d.ts +294 -0
  50. package/dist/src/agent-sdk.js +644 -0
  51. package/dist/src/aggregation.d.ts +183 -0
  52. package/dist/src/aggregation.js +513 -0
  53. package/dist/src/artifacts.d.ts +36 -0
  54. package/dist/src/artifacts.js +1372 -0
  55. package/dist/src/atlas-grafana.d.ts +377 -0
  56. package/dist/src/atlas-grafana.js +47 -0
  57. package/dist/src/atlas-html.d.ts +2 -0
  58. package/dist/src/atlas-html.js +34 -0
  59. package/dist/src/atlas.d.ts +130 -0
  60. package/dist/src/atlas.js +235 -0
  61. package/dist/src/behavior-assertions.d.ts +95 -0
  62. package/dist/src/behavior-assertions.js +154 -0
  63. package/dist/src/cli.d.ts +5 -0
  64. package/dist/src/cli.js +957 -0
  65. package/dist/src/codex-judge.d.ts +44 -0
  66. package/dist/src/codex-judge.js +214 -0
  67. package/dist/src/codex-run.d.ts +114 -0
  68. package/dist/src/codex-run.js +519 -0
  69. package/dist/src/codex.d.ts +187 -0
  70. package/dist/src/codex.js +1028 -0
  71. package/dist/src/contracts.d.ts +155 -0
  72. package/dist/src/contracts.js +866 -0
  73. package/dist/src/corpus.d.ts +67 -0
  74. package/dist/src/corpus.js +540 -0
  75. package/dist/src/cursor-sdk-runner.d.ts +81 -0
  76. package/dist/src/cursor-sdk-runner.js +362 -0
  77. package/dist/src/cursor-sdk.d.ts +81 -0
  78. package/dist/src/cursor-sdk.js +1078 -0
  79. package/dist/src/deepseek-adapter.d.ts +152 -0
  80. package/dist/src/deepseek-adapter.js +777 -0
  81. package/dist/src/exports.d.ts +69 -0
  82. package/dist/src/exports.js +966 -0
  83. package/dist/src/human-calibration.d.ts +167 -0
  84. package/dist/src/human-calibration.js +618 -0
  85. package/dist/src/index.d.ts +71 -0
  86. package/dist/src/index.js +38 -0
  87. package/dist/src/lifecycle.d.ts +196 -0
  88. package/dist/src/lifecycle.js +1889 -0
  89. package/dist/src/normalization-integrity.d.ts +128 -0
  90. package/dist/src/normalization-integrity.js +429 -0
  91. package/dist/src/openhands-run.d.ts +36 -0
  92. package/dist/src/openhands-run.js +415 -0
  93. package/dist/src/openhands.d.ts +130 -0
  94. package/dist/src/openhands.js +859 -0
  95. package/dist/src/pi.d.ts +203 -0
  96. package/dist/src/pi.js +1345 -0
  97. package/dist/src/process-protocol.d.ts +245 -0
  98. package/dist/src/process-protocol.js +1261 -0
  99. package/dist/src/retained-evidence.d.ts +12 -0
  100. package/dist/src/retained-evidence.js +201 -0
  101. package/dist/src/run-bundles.d.ts +193 -0
  102. package/dist/src/run-bundles.js +993 -0
  103. package/dist/src/scheduler.d.ts +109 -0
  104. package/dist/src/scheduler.js +1080 -0
  105. package/dist/src/semantic-judge.d.ts +194 -0
  106. package/dist/src/semantic-judge.js +875 -0
  107. package/dist/src/structural-observations.d.ts +124 -0
  108. package/dist/src/structural-observations.js +671 -0
  109. package/dist/src/task-packets.d.ts +115 -0
  110. package/dist/src/task-packets.js +683 -0
  111. package/dist/src/uniform-events.d.ts +120 -0
  112. package/dist/src/uniform-events.js +158 -0
  113. package/dist/src/verifiers.d.ts +116 -0
  114. package/dist/src/verifiers.js +819 -0
  115. package/dist/src/workspaces.d.ts +56 -0
  116. package/dist/src/workspaces.js +1314 -0
  117. package/docs/README.md +60 -0
  118. package/docs/development/README.md +71 -0
  119. package/docs/development/documentation-sync.md +58 -0
  120. package/docs/development/extension-contracts.md +307 -0
  121. package/docs/evaluation/README.md +48 -0
  122. package/docs/evaluation/aggregation.md +112 -0
  123. package/docs/evaluation/behavior-assertions.md +45 -0
  124. package/docs/evaluation/human-calibration.md +170 -0
  125. package/docs/evaluation/normalization-integrity.md +80 -0
  126. package/docs/evaluation/semantic-judge.md +188 -0
  127. package/docs/evaluation/structural-observations.md +84 -0
  128. package/docs/evaluation/uniform-events.md +131 -0
  129. package/docs/guides/atlas.md +144 -0
  130. package/docs/guides/evidence-and-sharing.md +100 -0
  131. package/docs/guides/operator-guide.md +424 -0
  132. package/docs/guides/quickstart.md +152 -0
  133. package/docs/guides/telemetry.md +66 -0
  134. package/docs/harnesses/README.md +31 -0
  135. package/docs/harnesses/claude-agent-sdk.md +61 -0
  136. package/docs/harnesses/codex-harness.md +174 -0
  137. package/docs/harnesses/cursor-sdk.md +149 -0
  138. package/docs/harnesses/deepseek-harness.md +134 -0
  139. package/docs/harnesses/openhands-agent-server.md +153 -0
  140. package/docs/harnesses/pi-sdk.md +66 -0
  141. package/docs/reference/README.md +19 -0
  142. package/docs/reference/agent-sdk-operational-runner.md +118 -0
  143. package/docs/reference/cli.md +114 -0
  144. package/docs/reference/contracts.md +222 -0
  145. package/docs/reference/run-bundle-contract.md +354 -0
  146. package/docs/reference/run-lifecycle.md +54 -0
  147. package/examples/cursor-sdk/README.md +11 -0
  148. package/examples/cursor-sdk/capture-profile.json +1 -0
  149. package/examples/cursor-sdk/harness.json +1 -0
  150. package/examples/cursor-sdk/model.json +1 -0
  151. package/examples/cursor-sdk/native-limits.json +1 -0
  152. package/examples/cursor-sdk/native-tool-policy.json +1 -0
  153. package/ontology/behavior-categories.v1.json +46 -0
  154. package/package.json +66 -0
  155. package/release/0.1.0/KNOWN_LIMITATIONS.md +35 -0
  156. package/release/0.1.0/README.md +46 -0
  157. package/release/0.1.0/reproducibility.json +61 -0
  158. package/release/0.2.0/KNOWN_LIMITATIONS.md +40 -0
  159. package/release/0.2.0/README.md +42 -0
  160. package/release/0.2.0/reproducibility.json +73 -0
  161. package/release/0.2.1/KNOWN_LIMITATIONS.md +18 -0
  162. package/release/0.2.1/README.md +63 -0
  163. package/release/0.2.1/reproducibility.json +73 -0
  164. package/release/README.md +13 -0
  165. package/schemas/aggregation.v1.json +293 -0
  166. package/schemas/behavior-assertions.v1.json +220 -0
  167. package/schemas/deepseek-runtime-composition.v1.schema.json +122 -0
  168. package/schemas/experiment.v1.schema.json +188 -0
  169. package/schemas/human-calibration.v1.json +336 -0
  170. package/schemas/normalization-integrity.v1.json +302 -0
  171. package/schemas/run-bundles/v1.json +1040 -0
  172. package/schemas/run-queue.v1.schema.json +206 -0
  173. package/schemas/semantic-judge.v1.json +380 -0
  174. package/schemas/structural-observations.v1.json +131 -0
  175. package/schemas/task-packet-freeze.v1.schema.json +106 -0
  176. package/schemas/task-packet.v1.schema.json +234 -0
  177. package/schemas/uniform-events/v1.json +289 -0
  178. package/scripts/atlas-grafana.sh +18 -0
@@ -0,0 +1,618 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
+ import lockfile from "proper-lockfile";
5
+ import { validateBehaviorAssertion, validateBehaviorReview, } from "./behavior-assertions.js";
6
+ import { createRetainedBehaviorEvidence } from "./retained-evidence.js";
7
+ import { assertNoDuplicateJsonKeys, canonicalizeMetadata, digestMetadata, validateArtifact, validateRunManifestEvidence, writeMetadataAtomically, } from "./artifacts.js";
8
+ import { readBoundedFile } from "./scheduler.js";
9
+ export async function selectReviewSample(sourceSet, criteria, now = () => new Date().toISOString()) {
10
+ assertValid("review source set", sourceSet);
11
+ assertValid("review sample criteria", criteria);
12
+ if (new Set(criteria.strata.map(({ id }) => id)).size !== criteria.strata.length) {
13
+ throw new Error("Review sample stratum IDs must be unique.");
14
+ }
15
+ for (const { id, filters } of criteria.strata) {
16
+ if (filters.confidence?.min !== undefined && filters.confidence.max !== undefined
17
+ && filters.confidence.min > filters.confidence.max) {
18
+ throw new Error(`Review sample stratum "${id}" has an inverted confidence range.`);
19
+ }
20
+ }
21
+ const canonicalSources = {
22
+ ...structuredClone(sourceSet),
23
+ sources: sourceSet.sources.map((source) => ({
24
+ ...structuredClone(source),
25
+ bundleRoot: resolve(source.bundleRoot),
26
+ assertionPath: resolve(source.assertionPath),
27
+ })),
28
+ };
29
+ const loaded = await loadCandidates(canonicalSources.sources);
30
+ const identities = loaded.map(({ assertion }) => assertionKey(assertion));
31
+ if (new Set(identities).size !== identities.length)
32
+ throw new Error("Review source assertion identities must be unique.");
33
+ const assignments = new Map();
34
+ for (const candidate of loaded) {
35
+ const matches = criteria.strata.filter(({ filters }) => matchesFilters(candidate, filters));
36
+ if (matches.length > 1)
37
+ throw new Error(`Review assertion "${candidate.assertion.id}" matches more than one stratum.`);
38
+ if (matches[0] !== undefined)
39
+ assignments.set(assertionKey(candidate.assertion), matches[0].id);
40
+ }
41
+ const candidates = [];
42
+ const strata = criteria.strata.map((stratum) => {
43
+ const eligible = loaded.filter(({ assertion }) => assignments.get(assertionKey(assertion)) === stratum.id)
44
+ .sort((left, right) => seededKey(criteria.seed, stratum.id, left.assertion).localeCompare(seededKey(criteria.seed, stratum.id, right.assertion)));
45
+ candidates.push(...eligible.slice(0, stratum.sampleSize).map(publicCandidate));
46
+ return { id: stratum.id, eligible: eligible.length, selected: Math.min(eligible.length, stratum.sampleSize), requested: stratum.sampleSize };
47
+ });
48
+ const sample = {
49
+ schemaVersion: "ebo.review-sample/v1",
50
+ createdAt: now(),
51
+ sources: canonicalSources,
52
+ criteria: structuredClone(criteria),
53
+ population: {
54
+ sourceCount: loaded.length,
55
+ eligibleAssertionIds: loaded.filter(({ assertion }) => assignments.has(assertionKey(assertion))).map(({ assertion }) => assertion.id).sort(),
56
+ selectedAssertionIds: candidates.map(({ assertion }) => assertion.id),
57
+ unavailableStrata: strata.filter(({ eligible }) => eligible === 0).map(({ id }) => id),
58
+ strata,
59
+ },
60
+ candidates,
61
+ };
62
+ validateReviewSample(sample);
63
+ return sample;
64
+ }
65
+ export async function writeReviewPacket(selection, outputRoot, now = () => new Date().toISOString()) {
66
+ await revalidateReviewSample(selection);
67
+ const root = resolve(outputRoot);
68
+ assertCalibrationDestination(selection.sources.sources.map(({ bundleRoot }) => bundleRoot), root);
69
+ if (existsSync(root))
70
+ throw new Error("Review packet destination already exists.");
71
+ const parent = dirname(root);
72
+ mkdirSync(parent, { recursive: true, mode: 0o700 });
73
+ const stagingRoot = mkdtempSync(join(parent, ".ebo-review-packet-"));
74
+ let published = false;
75
+ try {
76
+ mkdirSync(join(stagingRoot, "evidence"), { mode: 0o700 });
77
+ const items = [];
78
+ for (const candidate of selection.candidates) {
79
+ const loaded = await reloadCandidate(candidate);
80
+ if (loaded.assertion.digest !== digest(loaded.assertionDocument)) {
81
+ throw new Error(`Review assertion "${loaded.assertion.id}" changed after sample selection.`);
82
+ }
83
+ const item = packetItem(root, loaded);
84
+ for (const citation of item.citations) {
85
+ writeFileSync(join(stagingRoot, citation.href), renderEvidenceHtml(citation), { encoding: "utf8", mode: 0o600, flag: "wx" });
86
+ delete citation.content;
87
+ }
88
+ items.push(item);
89
+ }
90
+ const packet = {
91
+ schemaVersion: "ebo.review-packet/v1",
92
+ createdAt: now(),
93
+ selection: { schemaVersion: selection.schemaVersion, digest: digest(selection) },
94
+ evidenceBoundary: {
95
+ classification: "restricted-local-only",
96
+ copiedNativeEvidence: true,
97
+ note: "Only cited native records are copied into this restricted local packet; links retain the source bundle path. This is not a partner or public export.",
98
+ },
99
+ items,
100
+ };
101
+ assertValid("review packet", packet);
102
+ writeFileSync(join(stagingRoot, "index.html"), renderPacketHtml(packet), { encoding: "utf8", mode: 0o600, flag: "wx" });
103
+ writeFileSync(join(stagingRoot, "packet.json"), `${canonicalizeMetadata(packet)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
104
+ renameSync(stagingRoot, root);
105
+ published = true;
106
+ }
107
+ finally {
108
+ if (!published)
109
+ rmSync(stagingRoot, { recursive: true, force: true });
110
+ }
111
+ }
112
+ export async function importReviewDecision(selection, historyPath, decision) {
113
+ await revalidateReviewSample(selection);
114
+ assertValid("human review decision", decision);
115
+ assertCalibrationDestination(selection.sources.sources.map(({ bundleRoot }) => bundleRoot), historyPath);
116
+ let release;
117
+ try {
118
+ release = await lockfile.lock(resolve(historyPath), { realpath: false, retries: 0, stale: 30_000, update: 10_000 });
119
+ }
120
+ catch (error) {
121
+ if (error.code === "ELOCKED")
122
+ throw new Error("Another review-history import is already in progress.");
123
+ throw error;
124
+ }
125
+ // ponytail: one local lock serializes imports; use transactional storage only if a hosted multi-writer workflow is introduced.
126
+ try {
127
+ const selectionBinding = { schemaVersion: selection.schemaVersion, digest: digest(selection) };
128
+ const existing = existsSync(historyPath) ? readJson(historyPath) : {
129
+ schemaVersion: "ebo.review-history/v1",
130
+ selection: selectionBinding,
131
+ decisions: [],
132
+ };
133
+ validateReviewHistory(selection, existing);
134
+ const duplicate = existing.decisions.find(({ id }) => id === decision.id);
135
+ if (duplicate !== undefined) {
136
+ if (canonicalizeMetadata(duplicate) !== canonicalizeMetadata(decision)) {
137
+ throw new Error(`Review decision "${decision.id}" conflicts with an existing decision.`);
138
+ }
139
+ return { appended: false, history: existing };
140
+ }
141
+ const matchingId = selection.candidates.filter(({ assertion }) => assertion.id === decision.assertion.id);
142
+ if (matchingId.length === 0)
143
+ throw new Error(`Review decision targets unknown assertion "${decision.assertion.id}".`);
144
+ const candidate = matchingId.find(({ assertion }) => assertionKey(assertion) === assertionKey(decision.assertion));
145
+ if (candidate === undefined)
146
+ throw new Error("Review decision assertion binding is stale.");
147
+ const loaded = await reloadCandidate(candidate);
148
+ if (canonicalizeMetadata(decision.assertion) !== canonicalizeMetadata(loaded.assertion)) {
149
+ throw new Error("Review decision assertion binding is stale.");
150
+ }
151
+ const expectedPrevious = existing.decisions.length === 0 ? null : {
152
+ schemaVersion: existing.schemaVersion,
153
+ digest: digest(existing),
154
+ };
155
+ if (canonicalizeMetadata(decision.previousHistory) !== canonicalizeMetadata(expectedPrevious)) {
156
+ throw new Error("Review decision previous-history binding is stale.");
157
+ }
158
+ validateDecisionSemantics(decision, indexDecisions(existing.decisions));
159
+ validateBehaviorReview(loaded.assertionDocument, asBehaviorReview(decision));
160
+ const history = { ...existing, decisions: [...existing.decisions, structuredClone(decision)] };
161
+ validateReviewHistory(selection, history);
162
+ await writeMetadataAtomically(dirname(resolve(historyPath)), resolve(historyPath).split(sep).at(-1), history, undefined, { overwrite: true });
163
+ return { appended: true, history };
164
+ }
165
+ finally {
166
+ await release();
167
+ }
168
+ }
169
+ export async function summarizeCalibration(selection, history) {
170
+ await revalidateReviewSample(selection);
171
+ validateReviewHistory(selection, history);
172
+ const byCategory = new Map();
173
+ for (const candidate of selection.candidates) {
174
+ const group = byCategory.get(candidate.context.categoryId) ?? [];
175
+ group.push(candidate);
176
+ byCategory.set(candidate.context.categoryId, group);
177
+ }
178
+ const summary = {
179
+ schemaVersion: "ebo.calibration-summary/v1",
180
+ selection: { schemaVersion: selection.schemaVersion, digest: digest(selection) },
181
+ history: { schemaVersion: history.schemaVersion, digest: digest(history) },
182
+ totals: counts(selection.candidates, history.decisions),
183
+ categories: [...byCategory].sort(([left], [right]) => left.localeCompare(right))
184
+ .map(([categoryId, candidates]) => ({ categoryId, counts: counts(candidates, history.decisions) })),
185
+ };
186
+ assertValid("calibration summary", summary);
187
+ return summary;
188
+ }
189
+ export function validateReviewHistory(selection, history) {
190
+ validateReviewSample(selection);
191
+ assertValid("review history", history);
192
+ if (history.selection.digest !== digest(selection))
193
+ throw new Error("Review history selection binding is stale.");
194
+ const decisionIndex = indexDecisions();
195
+ const candidatesById = new Map();
196
+ for (const candidate of selection.candidates) {
197
+ const matches = candidatesById.get(candidate.assertion.id) ?? [];
198
+ matches.push(candidate);
199
+ candidatesById.set(candidate.assertion.id, matches);
200
+ }
201
+ const prefix = createHash("sha256").update('{"decisions":[');
202
+ const suffix = `],"schemaVersion":${canonicalizeMetadata(history.schemaVersion)},"selection":${canonicalizeMetadata(history.selection)}}`;
203
+ for (const [index, decision] of history.decisions.entries()) {
204
+ if (decisionIndex.byId.has(decision.id))
205
+ throw new Error(`Review history repeats decision "${decision.id}".`);
206
+ const matchingId = candidatesById.get(decision.assertion.id) ?? [];
207
+ if (matchingId.length === 0)
208
+ throw new Error(`Review history targets unknown assertion "${decision.assertion.id}".`);
209
+ const candidate = matchingId.find(({ assertion }) => assertionKey(assertion) === assertionKey(decision.assertion));
210
+ if (candidate === undefined)
211
+ throw new Error(`Review history decision "${decision.id}" has a stale assertion binding.`);
212
+ if (canonicalizeMetadata(candidate.assertion) !== canonicalizeMetadata(decision.assertion)) {
213
+ throw new Error(`Review history decision "${decision.id}" has a stale assertion binding.`);
214
+ }
215
+ const expected = index === 0 ? null : {
216
+ schemaVersion: history.schemaVersion,
217
+ digest: `sha256:${prefix.copy().update(suffix).digest("hex")}`,
218
+ };
219
+ if (canonicalizeMetadata(decision.previousHistory) !== canonicalizeMetadata(expected)) {
220
+ throw new Error(`Review history decision "${decision.id}" has a stale previous-history binding.`);
221
+ }
222
+ validateDecisionSemantics(decision, decisionIndex);
223
+ prefix.update(`${index === 0 ? "" : ","}${canonicalizeMetadata(decision)}`);
224
+ indexDecision(decisionIndex, decision);
225
+ }
226
+ }
227
+ export function validateReviewSample(selection) {
228
+ assertValid("review sample", selection);
229
+ const candidateBindings = selection.candidates.map(({ assertion }) => assertionKey(assertion));
230
+ if (new Set(candidateBindings).size !== candidateBindings.length) {
231
+ throw new Error("Review sample candidate bindings must be unique.");
232
+ }
233
+ if (selection.population.sourceCount !== selection.sources.sources.length) {
234
+ throw new Error("Review sample source count does not match its retained source set.");
235
+ }
236
+ if (canonicalizeMetadata(selection.population.selectedAssertionIds)
237
+ !== canonicalizeMetadata(selection.candidates.map(({ assertion }) => assertion.id))) {
238
+ throw new Error("Review sample selected assertion IDs do not match its candidates.");
239
+ }
240
+ if (selection.population.strata.length !== selection.criteria.strata.length) {
241
+ throw new Error("Review sample strata do not match its retained criteria.");
242
+ }
243
+ const matched = new Map();
244
+ for (const candidate of selection.candidates) {
245
+ const strata = selection.criteria.strata.filter(({ filters }) => matchesFilters(candidate, filters));
246
+ if (strata.length !== 1)
247
+ throw new Error(`Review candidate "${candidate.assertion.id}" must match exactly one retained stratum.`);
248
+ matched.set(strata[0].id, (matched.get(strata[0].id) ?? 0) + 1);
249
+ }
250
+ for (const [index, criterion] of selection.criteria.strata.entries()) {
251
+ const stratum = selection.population.strata[index];
252
+ if (stratum.id !== criterion.id || stratum.requested !== criterion.sampleSize
253
+ || stratum.selected !== (matched.get(criterion.id) ?? 0)
254
+ || stratum.selected > stratum.eligible) {
255
+ throw new Error(`Review sample stratum "${criterion.id}" is inconsistent with its criteria or selected candidates.`);
256
+ }
257
+ }
258
+ const unavailable = selection.population.strata.filter(({ eligible }) => eligible === 0).map(({ id }) => id);
259
+ if (canonicalizeMetadata(selection.population.unavailableStrata) !== canonicalizeMetadata(unavailable)
260
+ || selection.population.eligibleAssertionIds.length !== selection.population.strata.reduce((total, { eligible }) => total + eligible, 0)) {
261
+ throw new Error("Review sample eligible or unavailable population is inconsistent with its strata.");
262
+ }
263
+ const sources = new Set(selection.sources.sources.map((source) => sourceKey(resolve(source.bundleRoot), resolve(source.assertionPath), source.taskContext)));
264
+ for (const candidate of selection.candidates) {
265
+ if (!sources.has(sourceKey(candidate.source.bundleRoot, candidate.source.assertionPath, candidate.context.taskContext))) {
266
+ throw new Error(`Review candidate "${candidate.assertion.id}" is absent from the retained source set.`);
267
+ }
268
+ }
269
+ }
270
+ export async function revalidateReviewSample(selection) {
271
+ validateReviewSample(selection);
272
+ const expected = await selectReviewSample(selection.sources, selection.criteria, () => selection.createdAt);
273
+ if (canonicalizeMetadata(expected) !== canonicalizeMetadata(selection)) {
274
+ throw new Error("Review sample does not match the reproducible selection from its retained sources and criteria.");
275
+ }
276
+ }
277
+ async function loadCandidates(sources) {
278
+ const grouped = new Map();
279
+ for (const source of sources) {
280
+ const root = resolve(source.bundleRoot);
281
+ const group = grouped.get(root) ?? [];
282
+ group.push(source);
283
+ grouped.set(root, group);
284
+ }
285
+ const loaded = [];
286
+ for (const [bundleRoot, group] of grouped) {
287
+ const evidence = await createRetainedBehaviorEvidence(bundleRoot);
288
+ const manifest = readManifest(bundleRoot);
289
+ for (const source of group)
290
+ loaded.push(await loadCandidate(source, evidence, manifest));
291
+ }
292
+ return loaded;
293
+ }
294
+ async function loadCandidate(source, evidence, manifest, retainCapture = false) {
295
+ const bundleRoot = resolve(source.bundleRoot);
296
+ const assertionPath = resolve(source.assertionPath);
297
+ const assertionDocument = readJson(assertionPath);
298
+ const resolvedEvidence = evidence ?? await createRetainedBehaviorEvidence(bundleRoot);
299
+ const resolvedManifest = manifest ?? readManifest(bundleRoot);
300
+ await validateBehaviorAssertion(assertionDocument, resolvedEvidence.dataset, resolvedEvidence.resolver);
301
+ if (resolvedManifest.run.id !== assertionDocument.runId || resolvedManifest.attempt.id !== assertionDocument.attemptId) {
302
+ throw new Error(`Review assertion "${assertionDocument.id}" belongs to another run bundle.`);
303
+ }
304
+ const outcome = terminalVerifierOutcome(resolvedEvidence.outcomeCapture, resolvedManifest);
305
+ return {
306
+ assertion: { id: assertionDocument.id, schemaVersion: assertionDocument.schemaVersion, digest: digest(assertionDocument) },
307
+ source: { bundleRoot, assertionPath },
308
+ context: {
309
+ runId: resolvedManifest.run.id,
310
+ attemptId: resolvedManifest.attempt.id,
311
+ taskId: resolvedManifest.run.task.id,
312
+ taskContext: source.taskContext,
313
+ modelId: resolvedManifest.run.model.id,
314
+ harnessId: resolvedManifest.run.harness.id,
315
+ terminalState: resolvedManifest.terminal.state,
316
+ outcome,
317
+ categoryId: assertionDocument.behavior.categoryId,
318
+ abstained: assertionDocument.judgment.disposition === "abstained",
319
+ confidence: assertionDocument.judgment.disposition === "assessed" ? assertionDocument.judgment.confidence.value : null,
320
+ },
321
+ assertionDocument,
322
+ manifest: resolvedManifest,
323
+ ...(retainCapture ? { capture: resolvedEvidence.capture } : {}),
324
+ };
325
+ }
326
+ async function reloadCandidate(candidate) {
327
+ const loaded = await loadCandidate({ ...candidate.source, taskContext: candidate.context.taskContext }, undefined, undefined, true);
328
+ if (canonicalizeMetadata(publicCandidate(loaded)) !== canonicalizeMetadata(candidate)) {
329
+ throw new Error(`Review candidate "${candidate.assertion.id}" source metadata changed after selection.`);
330
+ }
331
+ return loaded;
332
+ }
333
+ function publicCandidate(candidate) {
334
+ const { assertionDocument: _assertionDocument, manifest: _manifest, capture: _capture, ...result } = candidate;
335
+ return structuredClone(result);
336
+ }
337
+ function matchesFilters(candidate, filters) {
338
+ const values = [
339
+ [filters.taskIds, candidate.context.taskId],
340
+ [filters.modelIds, candidate.context.modelId],
341
+ [filters.harnessIds, candidate.context.harnessId],
342
+ [filters.outcomes, candidate.context.outcome],
343
+ [filters.terminalStates, candidate.context.terminalState],
344
+ [filters.categoryIds, candidate.context.categoryId],
345
+ [filters.abstentions, candidate.context.abstained],
346
+ ];
347
+ if (values.some(([allowed, value]) => allowed !== undefined && !allowed.includes(value)))
348
+ return false;
349
+ const confidence = candidate.context.confidence;
350
+ if (filters.confidence === undefined)
351
+ return true;
352
+ if (confidence === null)
353
+ return false;
354
+ return (filters.confidence.min === undefined || confidence >= filters.confidence.min)
355
+ && (filters.confidence.max === undefined || confidence <= filters.confidence.max);
356
+ }
357
+ function packetItem(root, candidate) {
358
+ const descriptors = new Map(candidate.manifest.evidence.map((descriptor) => [descriptor.id, descriptor]));
359
+ return {
360
+ assertion: structuredClone(candidate.assertionDocument),
361
+ assertionBinding: structuredClone(candidate.assertion),
362
+ context: structuredClone(candidate.context),
363
+ source: { evidenceRoot: toPosix(relative(root, candidate.source.bundleRoot)) || "." },
364
+ citations: candidate.assertionDocument.judgment.citations.map((citation) => {
365
+ const descriptor = descriptors.get(citation.nativeReference.artifactId);
366
+ const relativePath = citation.nativeReference.artifactId === "manifest" ? "manifest.json" : descriptor?.relativePath;
367
+ if (relativePath === undefined)
368
+ throw new Error(`Assertion "${candidate.assertion.id}" cites an unknown native artifact.`);
369
+ const target = relative(root, join(candidate.source.bundleRoot, relativePath));
370
+ const content = resolveCitationContent(candidate, citation.nativeReference);
371
+ return {
372
+ ...structuredClone(citation),
373
+ sharingClass: descriptor?.sharingClass ?? "restricted",
374
+ href: `evidence/${createHash("sha256").update(canonicalizeMetadata([candidate.assertion, citation])).digest("hex")}.html`,
375
+ nativeHref: encodeRelativePath(target),
376
+ content,
377
+ };
378
+ }),
379
+ };
380
+ }
381
+ function renderPacketHtml(packet) {
382
+ const items = packet.items.map((item) => {
383
+ const assertion = item.assertion;
384
+ const citations = item.citations
385
+ .map((citation) => `<li><a href="${escapeHtml(citation.href)}">${escapeHtml(citation.eventId)}</a> — ${escapeHtml(citation.nativeReference.recordLocator)} (${escapeHtml(citation.sharingClass)})</li>`).join("");
386
+ return `<article id="${anchor(assertionKey(item.assertionBinding))}"><h2>${escapeHtml(assertion.id)}</h2><dl><dt>Run / attempt</dt><dd>${escapeHtml(item.context.runId)} / ${escapeHtml(item.context.attemptId)}</dd><dt>Task</dt><dd>${escapeHtml(item.context.taskId)}</dd><dt>Task context</dt><dd>${escapeHtml(item.context.taskContext)}</dd><dt>Model / harness</dt><dd>${escapeHtml(item.context.modelId)} / ${escapeHtml(item.context.harnessId)}</dd><dt>Outcome</dt><dd>${escapeHtml(item.context.outcome)}</dd><dt>Category</dt><dd>${escapeHtml(item.context.categoryId)}</dd></dl><h3>Judgment</h3><p>${escapeHtml(assertion.judgment.disposition === "assessed" ? assertion.judgment.assessment : `abstained: ${assertion.judgment.reason}`)}</p><h3>Rationale</h3><p>${escapeHtml(assertion.judgment.rationale)}</p><h3>Alternative explanation</h3><p>${escapeHtml(assertion.judgment.alternativeExplanation)}</p><h3>Native evidence</h3><ul>${citations || "<li>No citations (judge abstention).</li>"}</ul><p><a href="#top">Back to index</a></p></article>`;
387
+ }).join("\n");
388
+ const index = packet.items.map((item) => `<li><a href="#${anchor(assertionKey(item.assertionBinding))}">${escapeHtml(item.assertion.id)} — ${escapeHtml(item.context.runId)} / ${escapeHtml(item.context.attemptId)}</a></li>`).join("");
389
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>EBO human review packet</title><style>body{font:16px/1.5 system-ui;max-width:72rem;margin:auto;padding:2rem}article{border-top:1px solid #bbb;margin-top:2rem}dt{font-weight:700}dd{margin-bottom:.5rem}code{overflow-wrap:anywhere}</style></head><body id="top"><h1>EBO human review packet</h1><p>Restricted local evidence. Exact cited records are copied into this packet; links return to the native source artifacts.</p><p>Created ${escapeHtml(packet.createdAt)}</p><nav aria-label="Assertions"><ol>${index}</ol></nav>${items}</body></html>\n`;
390
+ }
391
+ function renderEvidenceHtml(citation) {
392
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(citation.eventId)}</title><style>body{font:16px/1.5 system-ui;max-width:72rem;margin:auto;padding:2rem}pre{white-space:pre-wrap;overflow-wrap:anywhere;border:1px solid #bbb;padding:1rem}</style></head><body><h1>${escapeHtml(citation.eventId)}</h1><p>Restricted local evidence extracted from <a href="../${escapeHtml(citation.nativeHref)}">the native artifact</a> at <code>${escapeHtml(citation.nativeReference.recordLocator)}</code>.</p><pre>${escapeHtml(canonicalizeMetadata(citation.content))}</pre><p><a href="../index.html">Back to packet</a></p></body></html>\n`;
393
+ }
394
+ function resolveCitationContent(candidate, reference) {
395
+ const captured = candidate.capture?.records.find(({ reference: current }) => current.artifactId === reference.artifactId
396
+ && current.recordLocator === reference.recordLocator);
397
+ if (captured === undefined)
398
+ throw new Error(`Assertion "${candidate.assertion.id}" cites a native record that cannot be rendered.`);
399
+ const record = captured.record;
400
+ const nativeSession = ["codex-app-server", "openhands-agent-server", "deepseek-harness", "cursor-sdk", "pi-sdk"].includes(candidate.manifest.run.harness.id);
401
+ return structuredClone(nativeSession ? record : record.document);
402
+ }
403
+ export function terminalVerifierOutcome(capture, manifest) {
404
+ if (manifest.run.assessmentMode === "observational")
405
+ return "unavailable";
406
+ const workspaceId = manifest.terminal.workspaceArtifactId;
407
+ if (workspaceId === undefined)
408
+ return "unavailable";
409
+ const outcomes = capture.records.flatMap(({ record }) => {
410
+ if (record.kind !== "verifier" || typeof record.document !== "object" || record.document === null)
411
+ return [];
412
+ const verifier = record.document;
413
+ return verifier.workspace?.artifactId === workspaceId && ["passed", "failed", "not-run", "error"].includes(String(verifier.status))
414
+ ? [verifier.status] : [];
415
+ });
416
+ const distinct = [...new Set(outcomes)];
417
+ if (distinct.length > 1)
418
+ throw new Error("Run bundle has conflicting verifier outcomes for the terminal workspace.");
419
+ return distinct[0] ?? "unavailable";
420
+ }
421
+ function validateDecisionSemantics(decision, index) {
422
+ if (decision.kind === "review" && decision.adjudicates !== undefined)
423
+ throw new Error("A review decision cannot adjudicate prior decisions.");
424
+ if (decision.kind !== "adjudication")
425
+ return;
426
+ if (decision.adjudicates === undefined || decision.adjudicates.length < 2)
427
+ throw new Error("An adjudication must bind at least two prior review decisions.");
428
+ if (new Set(decision.adjudicates).size !== decision.adjudicates.length)
429
+ throw new Error("Adjudication decision IDs must be unique.");
430
+ const key = assertionKey(decision.assertion);
431
+ for (const id of decision.adjudicates) {
432
+ const target = index.byId.get(id);
433
+ if (target === undefined || target.kind !== "review" || assertionKey(target.assertion) !== key) {
434
+ throw new Error(`Adjudication targets unknown or unrelated review decision "${id}".`);
435
+ }
436
+ }
437
+ const current = [...(index.latestReviews.get(key)?.values() ?? [])].map(({ id }) => id).sort();
438
+ if (canonicalizeMetadata([...decision.adjudicates].sort()) !== canonicalizeMetadata(current)) {
439
+ throw new Error("Adjudication must bind the current latest decision from every reviewer.");
440
+ }
441
+ }
442
+ function indexDecisions(decisions = []) {
443
+ const index = { byId: new Map(), latestReviews: new Map() };
444
+ for (const decision of decisions)
445
+ indexDecision(index, decision);
446
+ return index;
447
+ }
448
+ function indexDecision(index, decision) {
449
+ index.byId.set(decision.id, decision);
450
+ if (decision.kind !== "review")
451
+ return;
452
+ const key = assertionKey(decision.assertion);
453
+ const reviews = index.latestReviews.get(key) ?? new Map();
454
+ reviews.set(decision.reviewer.id, decision);
455
+ index.latestReviews.set(key, reviews);
456
+ }
457
+ function asBehaviorReview(decision) {
458
+ return {
459
+ schemaVersion: "ebo.behavior-review/v1",
460
+ id: decision.id,
461
+ assertion: structuredClone(decision.assertion),
462
+ state: decision.state,
463
+ reviewer: structuredClone(decision.reviewer),
464
+ rationale: decision.rationale,
465
+ };
466
+ }
467
+ function counts(candidates, decisions) {
468
+ const ids = new Set(candidates.map(({ assertion }) => assertionKey(assertion)));
469
+ const relevant = decisions.filter(({ assertion }) => ids.has(assertionKey(assertion)));
470
+ const latestReviews = latestHumanReviews(relevant);
471
+ const judgeHuman = latestReviews.filter(({ state, assertion }) => state !== "insufficient-evidence"
472
+ && candidates.find((candidate) => assertionKey(candidate.assertion) === assertionKey(assertion))?.context.abstained === false);
473
+ const pairResults = [];
474
+ for (const candidate of candidates) {
475
+ const reviews = latestReviews.filter(({ assertion, state }) => assertionKey(assertion) === assertionKey(candidate.assertion)
476
+ && state !== "insufficient-evidence");
477
+ for (let left = 0; left < reviews.length; left += 1) {
478
+ for (let right = left + 1; right < reviews.length; right += 1)
479
+ pairResults.push(reviews[left].state === reviews[right].state);
480
+ }
481
+ }
482
+ const outcomes = candidates.map((candidate) => effectiveReviewOutcome(candidate, relevant));
483
+ const disputed = candidates.filter((candidate, index) => {
484
+ if (outcomes[index] !== "unresolved")
485
+ return false;
486
+ const own = relevant.filter(({ assertion }) => assertionKey(assertion) === assertionKey(candidate.assertion));
487
+ if (currentAdjudication(own)?.state === "disputed")
488
+ return true;
489
+ const reviews = latestReviews.filter(({ assertion }) => assertionKey(assertion) === assertionKey(candidate.assertion));
490
+ return reviews.some(({ state }) => state === "disputed") || new Set(reviews.filter(({ state }) => state !== "insufficient-evidence").map(({ state }) => state)).size > 1;
491
+ }).length;
492
+ const adjudications = relevant.filter(({ kind }) => kind === "adjudication");
493
+ return {
494
+ selectedAssertions: candidates.length,
495
+ judgeAbstentions: candidates.filter(({ context }) => context.abstained).length,
496
+ humanDecisionAbstentions: relevant.filter(({ state }) => state === "insufficient-evidence").length,
497
+ disputedAssertions: disputed,
498
+ unresolvedAssertions: outcomes.filter((outcome) => outcome === "unresolved" || outcome === "unreviewed" || outcome === "judge-abstained").length,
499
+ confirmedEligibleAssertions: outcomes.filter((outcome) => outcome === "confirmed").length,
500
+ rejectedAssertions: outcomes.filter((outcome) => outcome === "rejected").length,
501
+ judgeHumanAgreement: agreement(judgeHuman.map(({ state }) => state === "confirmed"), "judge-human decision pairs on non-abstaining assertions"),
502
+ humanHumanAgreement: agreement(pairResults, "distinct-human reviewer pairs on the same assertion with comparable decisions"),
503
+ adjudication: {
504
+ denominator: adjudications.length,
505
+ population: "human adjudication decisions",
506
+ confirmed: adjudications.filter(({ state }) => state === "confirmed").length,
507
+ rejected: adjudications.filter(({ state }) => state === "rejected").length,
508
+ unresolved: adjudications.filter(({ state }) => state === "disputed" || state === "insufficient-evidence").length,
509
+ },
510
+ };
511
+ }
512
+ export function effectiveReviewOutcome(candidate, decisions) {
513
+ if (candidate.context.abstained)
514
+ return "judge-abstained";
515
+ const own = decisions.filter(({ assertion }) => assertionKey(assertion) === assertionKey(candidate.assertion));
516
+ const adjudication = currentAdjudication(own);
517
+ if (adjudication !== undefined)
518
+ return adjudication.state === "confirmed" ? "confirmed" : adjudication.state === "rejected" ? "rejected" : "unresolved";
519
+ const reviews = latestHumanReviews(own);
520
+ if (reviews.length === 0)
521
+ return "unreviewed";
522
+ if (reviews.some(({ state }) => state === "disputed" || state === "insufficient-evidence"))
523
+ return "unresolved";
524
+ const states = new Set(reviews.map(({ state }) => state));
525
+ if (states.size !== 1)
526
+ return "unresolved";
527
+ return states.has("confirmed") ? "confirmed" : "rejected";
528
+ }
529
+ export function isDisputedReviewOutcome(candidate, decisions) {
530
+ if (effectiveReviewOutcome(candidate, decisions) !== "unresolved")
531
+ return false;
532
+ const own = decisions.filter(({ assertion }) => assertionKey(assertion) === assertionKey(candidate.assertion));
533
+ const adjudication = currentAdjudication(own);
534
+ if (adjudication !== undefined)
535
+ return adjudication.state === "disputed";
536
+ const reviews = latestHumanReviews(own);
537
+ return reviews.some(({ state }) => state === "disputed")
538
+ || new Set(reviews.filter(({ state }) => state !== "insufficient-evidence").map(({ state }) => state)).size > 1;
539
+ }
540
+ function currentAdjudication(decisions) {
541
+ const lastReviewIndex = decisions.findLastIndex(({ kind }) => kind === "review");
542
+ const adjudicationIndex = decisions.findLastIndex(({ kind }) => kind === "adjudication");
543
+ return adjudicationIndex > lastReviewIndex ? decisions[adjudicationIndex] : undefined;
544
+ }
545
+ function latestHumanReviews(decisions) {
546
+ const latest = new Map();
547
+ for (const decision of decisions) {
548
+ if (decision.kind === "review")
549
+ latest.set(`${assertionKey(decision.assertion)}\u0000${decision.reviewer.id}`, decision);
550
+ }
551
+ return [...latest.values()];
552
+ }
553
+ function agreement(results, population) {
554
+ if (results.length === 0)
555
+ return { status: "unavailable", denominator: 0, population, reason: "No comparable decisions are available." };
556
+ const agreements = results.filter(Boolean).length;
557
+ return { status: "available", denominator: results.length, population, agreements, disagreements: results.length - agreements, rate: agreements / results.length };
558
+ }
559
+ function readManifest(bundleRoot) {
560
+ const path = join(bundleRoot, "manifest.json");
561
+ const manifest = readJson(path);
562
+ const errors = [...validateArtifact(path, manifest), ...validateRunManifestEvidence(path, manifest, bundleRoot)];
563
+ if (errors.length > 0)
564
+ throw new Error(errors.map(({ field, message }) => `${field}: ${message}`).join("\n"));
565
+ return manifest;
566
+ }
567
+ function readJson(path) {
568
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(readBoundedFile(path));
569
+ assertNoDuplicateJsonKeys(text);
570
+ return JSON.parse(text);
571
+ }
572
+ function assertValid(label, value) {
573
+ const errors = validateArtifact(label, value);
574
+ if (errors.length > 0)
575
+ throw new Error(errors.map(({ field, message }) => `${label} ${field}: ${message}`).join("\n"));
576
+ }
577
+ function digest(value) {
578
+ return `sha256:${digestMetadata(value).value}`;
579
+ }
580
+ function seededKey(seed, stratum, assertion) {
581
+ return createHash("sha256").update(canonicalizeMetadata([seed, stratum, assertion.id, assertion.digest])).digest("hex");
582
+ }
583
+ export function assertCalibrationDestination(sourceRoots, destination) {
584
+ const requested = resolve(destination);
585
+ let ancestor = dirname(requested);
586
+ while (!existsSync(ancestor)) {
587
+ const parent = dirname(ancestor);
588
+ if (parent === ancestor)
589
+ break;
590
+ ancestor = parent;
591
+ }
592
+ const output = resolve(realpathSync(ancestor), relative(ancestor, requested));
593
+ for (const root of sourceRoots.map((sourceRoot) => realpathSync(sourceRoot))) {
594
+ const locator = relative(root, output);
595
+ if (locator === "" || locator !== ".." && !locator.startsWith(`..${sep}`)) {
596
+ throw new Error("Calibration outputs must be written outside immutable source evidence.");
597
+ }
598
+ }
599
+ }
600
+ function encodeRelativePath(path) {
601
+ return toPosix(path).split("/").map((segment) => encodeURIComponent(segment)).join("/");
602
+ }
603
+ function assertionKey(assertion) {
604
+ return canonicalizeMetadata([assertion.id, assertion.schemaVersion, assertion.digest]);
605
+ }
606
+ function sourceKey(bundleRoot, assertionPath, taskContext) {
607
+ return canonicalizeMetadata([bundleRoot, assertionPath, taskContext]);
608
+ }
609
+ function anchor(value) {
610
+ return `assertion-${createHash("sha256").update(value).digest("hex").slice(0, 16)}`;
611
+ }
612
+ function toPosix(path) {
613
+ return sep === "/" ? path : path.split(sep).join("/");
614
+ }
615
+ export function escapeHtml(value) {
616
+ const entities = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
617
+ return String(value).replace(/[&<>"']/gu, (character) => entities[character]);
618
+ }