@cairnkeep/cli 2.3.1 → 2.4.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/CHANGELOG.md +69 -0
- package/README.md +221 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +1 -1
- package/docs/design/meta-agent.md +439 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/operating.md +531 -4
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +8 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +4 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +48 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +602 -3
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +251 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +37 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { CAPABILITY_IDS, capabilityIdSchema } from "./capability-schema.js";
|
|
5
|
+
export const EVAL_SCHEMA_VERSION = 1;
|
|
6
|
+
export const EVAL_ADAPTER_RESULT_STATUSES = ["completed", "adapter_error"];
|
|
7
|
+
export const EVAL_OBSERVATION_TERMINAL_STATES = [
|
|
8
|
+
"completed",
|
|
9
|
+
"verifier_failed",
|
|
10
|
+
"timeout",
|
|
11
|
+
"cancelled",
|
|
12
|
+
"adapter_error",
|
|
13
|
+
"invalid_result",
|
|
14
|
+
];
|
|
15
|
+
export const EVAL_PASS_STATES = ["passed", "failed", "unknown"];
|
|
16
|
+
export const EVAL_EXPERIMENT_KINDS = ["two_pass", "ablation"];
|
|
17
|
+
const MAX_TEXT_BYTES = 1024 * 1024;
|
|
18
|
+
const MAX_COMMAND_ARGS = 256;
|
|
19
|
+
const MAX_TASKS = 10_000;
|
|
20
|
+
const MAX_REFERENCES = 10_000;
|
|
21
|
+
const MAX_SNAPSHOT_FILES = 10_000;
|
|
22
|
+
const identifierSchema = z.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
23
|
+
const referenceSchema = z.string().min(1).max(1024)
|
|
24
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/)
|
|
25
|
+
.refine((value) => !value.includes("//") && !value.split("/").includes(".."), "Reference is not canonical.");
|
|
26
|
+
const valueFreeCodeSchema = z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._-]*$/);
|
|
27
|
+
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
28
|
+
const commitSchema = z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/);
|
|
29
|
+
const boundedTextSchema = z.string().max(MAX_TEXT_BYTES);
|
|
30
|
+
const nonnegativeIntegerSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
31
|
+
const positiveIntegerSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
32
|
+
export const evalRelativePathSchema = z.string().min(1).max(1024).superRefine((value, context) => {
|
|
33
|
+
if (isAbsolute(value) || value.startsWith("/") || value.endsWith("/") || value.includes("\\") || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
34
|
+
context.addIssue({ code: "custom", message: "Evaluation paths must be relative canonical paths." });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const segments = value.split("/");
|
|
38
|
+
if (segments.some((segment) => segment === "" || segment === "..")) {
|
|
39
|
+
context.addIssue({ code: "custom", message: "Evaluation path contains an invalid segment." });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
export const evalCommandSchema = z.strictObject({
|
|
43
|
+
program: z.string().min(1).max(4096).refine((value) => !/[\u0000\r\n]/.test(value)),
|
|
44
|
+
args: z.array(z.string().max(16_384).refine((value) => !value.includes("\u0000"))).max(MAX_COMMAND_ARGS),
|
|
45
|
+
});
|
|
46
|
+
export const evalLimitsSchema = z.strictObject({
|
|
47
|
+
elapsed_ms: z.number().int().positive().max(86_400_000),
|
|
48
|
+
stdout_bytes: z.number().int().positive().max(16 * 1024 * 1024),
|
|
49
|
+
});
|
|
50
|
+
const gitSourceSchema = z.strictObject({
|
|
51
|
+
kind: z.literal("git"),
|
|
52
|
+
repository: evalRelativePathSchema,
|
|
53
|
+
revision: commitSchema,
|
|
54
|
+
});
|
|
55
|
+
const bundledFileSchema = z.strictObject({
|
|
56
|
+
path: evalRelativePathSchema,
|
|
57
|
+
content: z.string().max(256 * 1024),
|
|
58
|
+
});
|
|
59
|
+
const bundledFakeSourceSchema = z.strictObject({
|
|
60
|
+
kind: z.literal("bundled_fake"),
|
|
61
|
+
identifier: z.literal("cairn-offline-fake-v1"),
|
|
62
|
+
files: z.array(bundledFileSchema).min(1).max(1024).superRefine((files, context) => {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
for (const [index, file] of files.entries()) {
|
|
65
|
+
if (seen.has(file.path)) {
|
|
66
|
+
context.addIssue({ code: "custom", path: [index, "path"], message: "Bundled file paths must be unique." });
|
|
67
|
+
}
|
|
68
|
+
seen.add(file.path);
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
export const evalTaskSourceSchema = z.discriminatedUnion("kind", [gitSourceSchema, bundledFakeSourceSchema]);
|
|
73
|
+
export const evalTaskSchema = z.strictObject({
|
|
74
|
+
id: identifierSchema,
|
|
75
|
+
input: boundedTextSchema,
|
|
76
|
+
workspace: z.strictObject({ path: evalRelativePathSchema }),
|
|
77
|
+
prepare: evalCommandSchema,
|
|
78
|
+
verify: evalCommandSchema,
|
|
79
|
+
limits: evalLimitsSchema,
|
|
80
|
+
});
|
|
81
|
+
export const evalTaskSetSchema = z.strictObject({
|
|
82
|
+
schema_version: z.literal(EVAL_SCHEMA_VERSION),
|
|
83
|
+
id: identifierSchema,
|
|
84
|
+
source: evalTaskSourceSchema,
|
|
85
|
+
tasks: z.array(evalTaskSchema).min(1).max(MAX_TASKS).superRefine((tasks, context) => {
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
for (const [index, task] of tasks.entries()) {
|
|
88
|
+
if (seen.has(task.id)) {
|
|
89
|
+
context.addIssue({ code: "custom", path: [index, "id"], message: "Evaluation task IDs must be unique." });
|
|
90
|
+
}
|
|
91
|
+
seen.add(task.id);
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
export const evalAdapterConfigSchema = z.strictObject({
|
|
96
|
+
schema_version: z.literal(EVAL_SCHEMA_VERSION),
|
|
97
|
+
id: identifierSchema,
|
|
98
|
+
command: evalCommandSchema,
|
|
99
|
+
turn_semantics: z.strictObject({
|
|
100
|
+
id: identifierSchema,
|
|
101
|
+
description: z.string().min(1).max(1024),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
const evalArmSchema = z.union([z.literal("baseline"), z.literal("treatment")]);
|
|
105
|
+
const evalPassSchema = z.union([z.literal("run1"), z.literal("run2")]);
|
|
106
|
+
export const evalAdapterRequestSchema = z.strictObject({
|
|
107
|
+
schema_version: z.literal(EVAL_SCHEMA_VERSION),
|
|
108
|
+
experiment_id: identifierSchema,
|
|
109
|
+
task_id: identifierSchema,
|
|
110
|
+
arm: evalArmSchema,
|
|
111
|
+
repetition: nonnegativeIntegerSchema,
|
|
112
|
+
pass: evalPassSchema,
|
|
113
|
+
workspace_path: evalRelativePathSchema,
|
|
114
|
+
notes_path: evalRelativePathSchema.nullable(),
|
|
115
|
+
input: boundedTextSchema,
|
|
116
|
+
limits: evalLimitsSchema,
|
|
117
|
+
seed: z.string().min(1).max(256),
|
|
118
|
+
expected_capability_digest: digestSchema,
|
|
119
|
+
output_path: evalRelativePathSchema,
|
|
120
|
+
});
|
|
121
|
+
const usageSchema = z.strictObject({
|
|
122
|
+
input_tokens: nonnegativeIntegerSchema.optional(),
|
|
123
|
+
output_tokens: nonnegativeIntegerSchema.optional(),
|
|
124
|
+
reasoning_tokens: nonnegativeIntegerSchema.optional(),
|
|
125
|
+
cache_read_tokens: nonnegativeIntegerSchema.optional(),
|
|
126
|
+
cache_write_tokens: nonnegativeIntegerSchema.optional(),
|
|
127
|
+
total_tokens: nonnegativeIntegerSchema.optional(),
|
|
128
|
+
});
|
|
129
|
+
const componentIdentitySchema = z.strictObject({
|
|
130
|
+
id: identifierSchema,
|
|
131
|
+
version: z.string().min(1).max(128).optional(),
|
|
132
|
+
});
|
|
133
|
+
export const evalAdapterResultSchema = z.strictObject({
|
|
134
|
+
schema_version: z.literal(EVAL_SCHEMA_VERSION),
|
|
135
|
+
status: z.enum(EVAL_ADAPTER_RESULT_STATUSES),
|
|
136
|
+
error_code: valueFreeCodeSchema.optional(),
|
|
137
|
+
turns: z.strictObject({ value: nonnegativeIntegerSchema, semantics: identifierSchema }).optional(),
|
|
138
|
+
usage: usageSchema.optional(),
|
|
139
|
+
cost: z.strictObject({
|
|
140
|
+
amount: z.number().finite().nonnegative(),
|
|
141
|
+
currency: z.string().regex(/^[A-Z]{3}$/),
|
|
142
|
+
}).optional(),
|
|
143
|
+
harness: componentIdentitySchema.optional(),
|
|
144
|
+
adapter: componentIdentitySchema.optional(),
|
|
145
|
+
model: z.strictObject({
|
|
146
|
+
id: identifierSchema,
|
|
147
|
+
config_id: identifierSchema.optional(),
|
|
148
|
+
}).optional(),
|
|
149
|
+
observed_capability_digest: digestSchema.optional(),
|
|
150
|
+
trajectory_ref: referenceSchema.optional(),
|
|
151
|
+
artifact_refs: z.array(referenceSchema).max(MAX_REFERENCES).optional(),
|
|
152
|
+
});
|
|
153
|
+
const capabilityStateSchema = z.strictObject({ id: capabilityIdSchema, enabled: z.boolean() });
|
|
154
|
+
const capabilityStateListSchema = z.array(capabilityStateSchema).length(CAPABILITY_IDS.length).superRefine((rows, context) => {
|
|
155
|
+
const expected = [...CAPABILITY_IDS];
|
|
156
|
+
if (rows.some((row, index) => row.id !== expected[index])) {
|
|
157
|
+
context.addIssue({ code: "custom", message: "Capability state rows must use canonical capability order." });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const processObservationSchema = z.strictObject({
|
|
161
|
+
exit_code: z.number().int().nullable(),
|
|
162
|
+
signal: z.enum(["SIGTERM", "SIGKILL", "SIGINT", "SIGHUP"]).nullable(),
|
|
163
|
+
error_code: valueFreeCodeSchema.nullable(),
|
|
164
|
+
cleanup: z.enum(["pending", "closed", "terminated", "killed", "failed"]),
|
|
165
|
+
});
|
|
166
|
+
const verifierObservationSchema = z.strictObject({
|
|
167
|
+
state: z.enum(["pending", "not_run", "completed", "error"]),
|
|
168
|
+
reason: valueFreeCodeSchema.nullable(),
|
|
169
|
+
});
|
|
170
|
+
const noteSnapshotEntrySchema = z.strictObject({
|
|
171
|
+
path: evalRelativePathSchema,
|
|
172
|
+
digest: digestSchema,
|
|
173
|
+
bytes: nonnegativeIntegerSchema,
|
|
174
|
+
});
|
|
175
|
+
const noteObservationSchema = z.strictObject({
|
|
176
|
+
distiller_id: identifierSchema.nullable(),
|
|
177
|
+
distiller_config_digest: digestSchema.nullable(),
|
|
178
|
+
trajectory_ref: referenceSchema.nullable(),
|
|
179
|
+
distillation_outcome: z.enum(["not_applicable", "success", "no_notes", "failed", "skipped"]),
|
|
180
|
+
eligibility_reason: valueFreeCodeSchema,
|
|
181
|
+
note_snapshot_digest: digestSchema.nullable(),
|
|
182
|
+
note_snapshot_manifest: z.array(noteSnapshotEntrySchema).max(MAX_SNAPSHOT_FILES),
|
|
183
|
+
notes_exposed: z.boolean(),
|
|
184
|
+
});
|
|
185
|
+
export const evalObservationSchema = z.strictObject({
|
|
186
|
+
schema_version: z.literal(EVAL_SCHEMA_VERSION),
|
|
187
|
+
observation_id: identifierSchema,
|
|
188
|
+
task_id: identifierSchema,
|
|
189
|
+
schedule_index: nonnegativeIntegerSchema,
|
|
190
|
+
arm: evalArmSchema,
|
|
191
|
+
disabled_capability: capabilityIdSchema.nullable(),
|
|
192
|
+
arm_order: nonnegativeIntegerSchema,
|
|
193
|
+
repetition: nonnegativeIntegerSchema,
|
|
194
|
+
pass: evalPassSchema,
|
|
195
|
+
seed: z.string().min(1).max(256),
|
|
196
|
+
state: z.enum(["pending", "terminal"]),
|
|
197
|
+
terminal_state: z.enum(EVAL_OBSERVATION_TERMINAL_STATES).nullable(),
|
|
198
|
+
process: processObservationSchema,
|
|
199
|
+
verifier: verifierObservationSchema,
|
|
200
|
+
pass_state: z.enum(EVAL_PASS_STATES),
|
|
201
|
+
expected_capabilities: capabilityStateListSchema,
|
|
202
|
+
observed_capabilities: capabilityStateListSchema,
|
|
203
|
+
expected_capability_digest: digestSchema,
|
|
204
|
+
observed_capability_digest: digestSchema.nullable(),
|
|
205
|
+
capability_status: z.enum(["pending", "valid", "mismatch", "unavailable"]),
|
|
206
|
+
capability_digest_match: z.boolean().nullable(),
|
|
207
|
+
four_cell_id: identifierSchema,
|
|
208
|
+
notes: noteObservationSchema,
|
|
209
|
+
result: evalAdapterResultSchema.nullable(),
|
|
210
|
+
missing_reasons: z.array(valueFreeCodeSchema).max(256),
|
|
211
|
+
});
|
|
212
|
+
const nullableMetricSchema = z.number().finite().nullable();
|
|
213
|
+
const populationSchema = z.strictObject({
|
|
214
|
+
full: nonnegativeIntegerSchema,
|
|
215
|
+
executed: nonnegativeIntegerSchema,
|
|
216
|
+
eligible: nonnegativeIntegerSchema,
|
|
217
|
+
paired: nonnegativeIntegerSchema,
|
|
218
|
+
note_eligible: nonnegativeIntegerSchema,
|
|
219
|
+
});
|
|
220
|
+
const missingnessSummarySchema = z.strictObject({
|
|
221
|
+
count: nonnegativeIntegerSchema,
|
|
222
|
+
reasons: z.array(valueFreeCodeSchema).max(256),
|
|
223
|
+
});
|
|
224
|
+
const conditionPopulationSchema = z.strictObject({
|
|
225
|
+
full: nonnegativeIntegerSchema,
|
|
226
|
+
executed: nonnegativeIntegerSchema,
|
|
227
|
+
eligible: nonnegativeIntegerSchema,
|
|
228
|
+
});
|
|
229
|
+
function conditionLevelSchema(arm, pass) {
|
|
230
|
+
return z.strictObject({
|
|
231
|
+
arm: z.literal(arm),
|
|
232
|
+
pass: z.literal(pass),
|
|
233
|
+
value: nullableMetricSchema,
|
|
234
|
+
valid_task_ids: z.array(identifierSchema).max(MAX_TASKS),
|
|
235
|
+
valid_task_count: nonnegativeIntegerSchema,
|
|
236
|
+
population: conditionPopulationSchema,
|
|
237
|
+
missing: missingnessSummarySchema,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const aggregateCommonShape = {
|
|
241
|
+
metric_id: identifierSchema,
|
|
242
|
+
direction: identifierSchema,
|
|
243
|
+
semantics: identifierSchema.nullable(),
|
|
244
|
+
currency: z.string().regex(/^[A-Z]{3}$/).nullable(),
|
|
245
|
+
estimate: nullableMetricSchema,
|
|
246
|
+
within_arm: z.strictObject({ baseline: nullableMetricSchema, treatment: nullableMetricSchema }),
|
|
247
|
+
endpoint_delta: nullableMetricSchema,
|
|
248
|
+
difference_in_differences: nullableMetricSchema,
|
|
249
|
+
uncertainty: z.strictObject({
|
|
250
|
+
algorithm: z.literal("paired-bootstrap"),
|
|
251
|
+
version: z.literal(1),
|
|
252
|
+
seed: z.string().min(1).max(256),
|
|
253
|
+
stream: z.string().min(1).max(512),
|
|
254
|
+
iterations: positiveIntegerSchema.max(1_000_000),
|
|
255
|
+
confidence: z.number().gt(0).lt(1),
|
|
256
|
+
quantile: z.literal("percentile-nearest-rank-v1"),
|
|
257
|
+
interval: z.strictObject({ low: z.number().finite(), high: z.number().finite() }).nullable(),
|
|
258
|
+
null_reason: valueFreeCodeSchema.nullable(),
|
|
259
|
+
}),
|
|
260
|
+
warnings: z.array(valueFreeCodeSchema).max(256),
|
|
261
|
+
valid_task_ids: z.array(identifierSchema).max(MAX_TASKS),
|
|
262
|
+
valid_pair_count: nonnegativeIntegerSchema,
|
|
263
|
+
population: populationSchema,
|
|
264
|
+
missing: missingnessSummarySchema,
|
|
265
|
+
};
|
|
266
|
+
const baselineRun1LevelSchema = conditionLevelSchema("baseline", "run1");
|
|
267
|
+
const baselineRun2LevelSchema = conditionLevelSchema("baseline", "run2");
|
|
268
|
+
const treatmentRun1LevelSchema = conditionLevelSchema("treatment", "run1");
|
|
269
|
+
const treatmentRun2LevelSchema = conditionLevelSchema("treatment", "run2");
|
|
270
|
+
export const evalAggregateSchema = z.discriminatedUnion("comparison_id", [
|
|
271
|
+
z.strictObject({
|
|
272
|
+
comparison_id: z.literal("memory-baseline"),
|
|
273
|
+
...aggregateCommonShape,
|
|
274
|
+
condition_levels: z.tuple([baselineRun1LevelSchema, baselineRun2LevelSchema]),
|
|
275
|
+
}),
|
|
276
|
+
z.strictObject({
|
|
277
|
+
comparison_id: z.literal("memory-treatment"),
|
|
278
|
+
...aggregateCommonShape,
|
|
279
|
+
condition_levels: z.tuple([treatmentRun1LevelSchema, treatmentRun2LevelSchema]),
|
|
280
|
+
}),
|
|
281
|
+
z.strictObject({
|
|
282
|
+
comparison_id: z.literal("endpoint-treatment-minus-baseline"),
|
|
283
|
+
...aggregateCommonShape,
|
|
284
|
+
condition_levels: z.tuple([baselineRun2LevelSchema, treatmentRun2LevelSchema]),
|
|
285
|
+
}),
|
|
286
|
+
z.strictObject({
|
|
287
|
+
comparison_id: z.literal("difference-in-differences"),
|
|
288
|
+
...aggregateCommonShape,
|
|
289
|
+
condition_levels: z.tuple([
|
|
290
|
+
baselineRun1LevelSchema,
|
|
291
|
+
baselineRun2LevelSchema,
|
|
292
|
+
treatmentRun1LevelSchema,
|
|
293
|
+
treatmentRun2LevelSchema,
|
|
294
|
+
]),
|
|
295
|
+
}),
|
|
296
|
+
]);
|
|
297
|
+
const scheduleRowSchema = z.strictObject({
|
|
298
|
+
observation_id: identifierSchema,
|
|
299
|
+
task_id: identifierSchema,
|
|
300
|
+
arm: evalArmSchema,
|
|
301
|
+
repetition: nonnegativeIntegerSchema,
|
|
302
|
+
pass: evalPassSchema,
|
|
303
|
+
seed: z.string().min(1).max(256),
|
|
304
|
+
});
|
|
305
|
+
const evidenceProvenanceSchema = z.strictObject({
|
|
306
|
+
schema_version: z.literal(EVAL_SCHEMA_VERSION),
|
|
307
|
+
evidence_scope: z.enum(["offline-framework", "live-evaluation"]),
|
|
308
|
+
source_commit: commitSchema,
|
|
309
|
+
package_version: z.string().min(1).max(128),
|
|
310
|
+
runtime_id: identifierSchema,
|
|
311
|
+
task_set_digest: digestSchema,
|
|
312
|
+
report_digest: digestSchema,
|
|
313
|
+
schema_digests: z.array(digestSchema).length(2),
|
|
314
|
+
note_snapshot_digests: z.array(digestSchema).max(MAX_TASKS),
|
|
315
|
+
missingness_digest: digestSchema,
|
|
316
|
+
claim_anchors: z.array(referenceSchema).max(256),
|
|
317
|
+
});
|
|
318
|
+
export const evalReportSchema = z.strictObject({
|
|
319
|
+
schema_version: z.literal(EVAL_SCHEMA_VERSION),
|
|
320
|
+
experiment_id: identifierSchema,
|
|
321
|
+
status: z.enum(["partial", "final"]),
|
|
322
|
+
experiment_kind: z.enum(EVAL_EXPERIMENT_KINDS),
|
|
323
|
+
task_set_digest: digestSchema,
|
|
324
|
+
adapter_config_digest: digestSchema,
|
|
325
|
+
source_revision: commitSchema,
|
|
326
|
+
schedule_digest: digestSchema,
|
|
327
|
+
created_at: z.iso.datetime(),
|
|
328
|
+
updated_at: z.iso.datetime(),
|
|
329
|
+
runtime: z.strictObject({
|
|
330
|
+
platform: identifierSchema,
|
|
331
|
+
arch: identifierSchema,
|
|
332
|
+
node: z.string().min(1).max(128),
|
|
333
|
+
cairnkeep: z.string().min(1).max(128),
|
|
334
|
+
}),
|
|
335
|
+
schedule: z.array(scheduleRowSchema).max(MAX_TASKS * 100),
|
|
336
|
+
observations: z.array(evalObservationSchema).max(MAX_TASKS * 100),
|
|
337
|
+
aggregates: z.array(evalAggregateSchema).max(10_000),
|
|
338
|
+
missingness: z.strictObject({ digest: digestSchema, count: nonnegativeIntegerSchema, reasons: z.array(valueFreeCodeSchema).max(256) }),
|
|
339
|
+
warnings: z.array(valueFreeCodeSchema).max(256),
|
|
340
|
+
evidence: evidenceProvenanceSchema,
|
|
341
|
+
});
|
|
342
|
+
function sortCanonical(value) {
|
|
343
|
+
if (Array.isArray(value))
|
|
344
|
+
return value.map(sortCanonical);
|
|
345
|
+
if (value && typeof value === "object") {
|
|
346
|
+
return Object.fromEntries(Object.entries(value)
|
|
347
|
+
.sort(([left], [right]) => left.localeCompare(right, "en"))
|
|
348
|
+
.map(([key, child]) => [key, sortCanonical(child)]));
|
|
349
|
+
}
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
export function canonicalJson(value) {
|
|
353
|
+
return JSON.stringify(sortCanonical(value));
|
|
354
|
+
}
|
|
355
|
+
export function canonicalBytes(value) {
|
|
356
|
+
return Buffer.from(canonicalJson(value), "utf8");
|
|
357
|
+
}
|
|
358
|
+
export function sha256Hex(value) {
|
|
359
|
+
return createHash("sha256").update(value).digest("hex");
|
|
360
|
+
}
|
|
361
|
+
export function canonicalDigest(value) {
|
|
362
|
+
return sha256Hex(canonicalBytes(value));
|
|
363
|
+
}
|
|
364
|
+
export const evalReportRuntimeContract = z.toJSONSchema(evalReportSchema, {
|
|
365
|
+
target: "draft-2020-12",
|
|
366
|
+
io: "input",
|
|
367
|
+
});
|
|
368
|
+
export const evalReportRuntimeContractDigest = canonicalDigest(evalReportRuntimeContract);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const DEFAULT_ITERATIONS = 10_000;
|
|
3
|
+
const DEFAULT_CONFIDENCE = 0.95;
|
|
4
|
+
function resolvedOptions(options) {
|
|
5
|
+
const iterations = options.iterations ?? DEFAULT_ITERATIONS;
|
|
6
|
+
const confidence = options.confidence ?? DEFAULT_CONFIDENCE;
|
|
7
|
+
if (!Number.isSafeInteger(iterations) || iterations < 1 || iterations > 1_000_000) {
|
|
8
|
+
throw new Error("Bootstrap iterations must be an integer between 1 and 1000000.");
|
|
9
|
+
}
|
|
10
|
+
if (!Number.isFinite(confidence) || confidence <= 0 || confidence >= 1) {
|
|
11
|
+
throw new Error("Bootstrap confidence must be between zero and one.");
|
|
12
|
+
}
|
|
13
|
+
if (!options.seed || !options.comparison_id || !options.metric_id) {
|
|
14
|
+
throw new Error("Bootstrap seed, comparison_id, and metric_id are required.");
|
|
15
|
+
}
|
|
16
|
+
return { ...options, iterations, confidence };
|
|
17
|
+
}
|
|
18
|
+
export function createEvalPrng(options) {
|
|
19
|
+
const resolved = resolvedOptions(options);
|
|
20
|
+
let counter = 0;
|
|
21
|
+
let bytes = Buffer.alloc(0);
|
|
22
|
+
let offset = 0;
|
|
23
|
+
return {
|
|
24
|
+
index(population) {
|
|
25
|
+
if (!Number.isSafeInteger(population) || population < 1) {
|
|
26
|
+
throw new Error("PRNG population must be a positive integer.");
|
|
27
|
+
}
|
|
28
|
+
if (offset + 4 > bytes.length) {
|
|
29
|
+
bytes = createHash("sha256")
|
|
30
|
+
.update(`${resolved.seed}\0${resolved.comparison_id}\0${resolved.metric_id}\0${counter}`)
|
|
31
|
+
.digest();
|
|
32
|
+
counter += 1;
|
|
33
|
+
offset = 0;
|
|
34
|
+
}
|
|
35
|
+
const value = bytes.readUInt32BE(offset);
|
|
36
|
+
offset += 4;
|
|
37
|
+
return value % population;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function finite(value) {
|
|
42
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
43
|
+
}
|
|
44
|
+
export function summarizeTaskRepetitions(rows) {
|
|
45
|
+
const grouped = new Map();
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
const taskRows = grouped.get(row.task_id) ?? [];
|
|
48
|
+
taskRows.push(row);
|
|
49
|
+
grouped.set(row.task_id, taskRows);
|
|
50
|
+
}
|
|
51
|
+
const summaries = [];
|
|
52
|
+
for (const [taskId, taskRows] of grouped) {
|
|
53
|
+
const paired = taskRows.filter((row) => finite(row.reference) && finite(row.comparison));
|
|
54
|
+
if (paired.length === 0)
|
|
55
|
+
continue;
|
|
56
|
+
const reference = paired.reduce((sum, row) => sum + row.reference, 0) / paired.length;
|
|
57
|
+
const comparison = paired.reduce((sum, row) => sum + row.comparison, 0) / paired.length;
|
|
58
|
+
summaries.push({
|
|
59
|
+
task_id: taskId,
|
|
60
|
+
reference,
|
|
61
|
+
comparison,
|
|
62
|
+
difference: comparison - reference,
|
|
63
|
+
repetitions: paired.length,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return summaries;
|
|
67
|
+
}
|
|
68
|
+
export function bootstrapPairedInterval(differences, options) {
|
|
69
|
+
const resolved = resolvedOptions(options);
|
|
70
|
+
const stream = `${resolved.seed}\0${resolved.comparison_id}\0${resolved.metric_id}`;
|
|
71
|
+
const warnings = differences.length < 20 ? ["small_sample"] : [];
|
|
72
|
+
const base = {
|
|
73
|
+
algorithm: "paired-bootstrap",
|
|
74
|
+
version: 1,
|
|
75
|
+
seed: resolved.seed,
|
|
76
|
+
stream,
|
|
77
|
+
iterations: resolved.iterations,
|
|
78
|
+
confidence: resolved.confidence,
|
|
79
|
+
quantile: "percentile-nearest-rank-v1",
|
|
80
|
+
warnings,
|
|
81
|
+
};
|
|
82
|
+
if (differences.length < 2) {
|
|
83
|
+
return {
|
|
84
|
+
...base,
|
|
85
|
+
interval: null,
|
|
86
|
+
null_reason: "fewer_than_two_pairs",
|
|
87
|
+
wording: "inconclusive",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (differences.some((value) => !Number.isFinite(value))) {
|
|
91
|
+
throw new Error("Bootstrap differences must all be finite.");
|
|
92
|
+
}
|
|
93
|
+
const prng = createEvalPrng(resolved);
|
|
94
|
+
const means = new Array(resolved.iterations);
|
|
95
|
+
for (let iteration = 0; iteration < resolved.iterations; iteration += 1) {
|
|
96
|
+
let sum = 0;
|
|
97
|
+
for (let draw = 0; draw < differences.length; draw += 1) {
|
|
98
|
+
sum += differences[prng.index(differences.length)];
|
|
99
|
+
}
|
|
100
|
+
means[iteration] = sum / differences.length;
|
|
101
|
+
}
|
|
102
|
+
means.sort((left, right) => left - right);
|
|
103
|
+
const tail = (1 - resolved.confidence) / 2;
|
|
104
|
+
const interval = {
|
|
105
|
+
low: means[Math.floor((means.length - 1) * tail)],
|
|
106
|
+
high: means[Math.ceil((means.length - 1) * (1 - tail))],
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
...base,
|
|
110
|
+
interval,
|
|
111
|
+
null_reason: null,
|
|
112
|
+
wording: warnings.length > 0 || (interval.low <= 0 && interval.high >= 0)
|
|
113
|
+
? "inconclusive"
|
|
114
|
+
: "estimate",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function missingSummary(rows, validTaskIds) {
|
|
118
|
+
const byTask = new Map();
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
if (!validTaskIds.has(row.task_id) && !byTask.has(row.task_id)) {
|
|
121
|
+
byTask.set(row.task_id, row.missing_reason ?? "paired_measurement_missing");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const counts = new Map();
|
|
125
|
+
for (const reason of byTask.values())
|
|
126
|
+
counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
127
|
+
return {
|
|
128
|
+
count: byTask.size,
|
|
129
|
+
reasons: [...counts].map(([reason, count]) => ({ reason, count })),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export function computePairedEstimate(rows, options) {
|
|
133
|
+
const summaries = summarizeTaskRepetitions(rows);
|
|
134
|
+
const validTaskIds = summaries.map((row) => row.task_id);
|
|
135
|
+
const differences = summaries.map((row) => row.difference);
|
|
136
|
+
const interval = bootstrapPairedInterval(differences, options);
|
|
137
|
+
return {
|
|
138
|
+
...interval,
|
|
139
|
+
estimate: differences.length === 0
|
|
140
|
+
? null
|
|
141
|
+
: differences.reduce((sum, value) => sum + value, 0) / differences.length,
|
|
142
|
+
valid_task_ids: validTaskIds,
|
|
143
|
+
valid_pair_count: validTaskIds.length,
|
|
144
|
+
missing: missingSummary(rows, new Set(validTaskIds)),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function computeDifferenceInDifferences(rows, options) {
|
|
148
|
+
const pairedRows = rows.map((row) => {
|
|
149
|
+
const valid = finite(row.baseline_run1)
|
|
150
|
+
&& finite(row.baseline_run2)
|
|
151
|
+
&& finite(row.treatment_run1)
|
|
152
|
+
&& finite(row.treatment_run2);
|
|
153
|
+
return {
|
|
154
|
+
task_id: row.task_id,
|
|
155
|
+
reference: valid ? row.baseline_run2 - row.baseline_run1 : null,
|
|
156
|
+
comparison: valid ? row.treatment_run2 - row.treatment_run1 : null,
|
|
157
|
+
missing_reason: row.missing_reason,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
return computePairedEstimate(pairedRows, options);
|
|
161
|
+
}
|