@vela-science/canopus 0.7.0 → 0.8.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +67 -145
  3. package/capsules/erdos1056-k15/bin/linux-arm64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
  4. package/capsules/erdos1056-k15/bin/linux-x86_64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
  5. package/compatibility.json +25 -0
  6. package/dist/src/{receipt/map.d.ts → candidate/finalize.d.ts} +2 -9
  7. package/dist/src/candidate/finalize.js +71 -0
  8. package/dist/src/cli.js +89 -195
  9. package/dist/src/contracts/mission.d.ts +1 -13
  10. package/dist/src/contracts/mission.js +4 -42
  11. package/dist/src/contracts/validation.d.ts +1 -25
  12. package/dist/src/contracts/validation.js +1 -95
  13. package/dist/src/engines/codex-tools-native.js +2 -2
  14. package/dist/src/index.d.ts +5 -34
  15. package/dist/src/index.js +4 -34
  16. package/dist/src/mission/prepare.d.ts +10 -1
  17. package/dist/src/mission/prepare.js +29 -30
  18. package/dist/src/product/coverage.d.ts +17 -0
  19. package/dist/src/product/coverage.js +137 -0
  20. package/dist/src/product/doctor.d.ts +1 -2
  21. package/dist/src/product/doctor.js +29 -8
  22. package/dist/src/product/profile.js +8 -2
  23. package/dist/src/product/replay.js +11 -12
  24. package/dist/src/product/run.d.ts +5 -9
  25. package/dist/src/product/run.js +17 -81
  26. package/dist/src/product/submission.d.ts +40 -0
  27. package/dist/src/product/submission.js +217 -0
  28. package/dist/src/product/submit.d.ts +22 -0
  29. package/dist/src/product/submit.js +264 -0
  30. package/dist/src/product/version.d.ts +2 -2
  31. package/dist/src/product/version.js +27 -4
  32. package/dist/src/projection/current-run.d.ts +12 -0
  33. package/dist/src/projection/current-run.js +94 -0
  34. package/dist/src/projection/failure.d.ts +17 -7
  35. package/dist/src/projection/failure.js +43 -12
  36. package/dist/src/projection/retained-mission.d.ts +6 -0
  37. package/dist/src/projection/retained-mission.js +41 -0
  38. package/dist/src/projection/retained-run.d.ts +7 -0
  39. package/dist/src/projection/retained-run.js +50 -0
  40. package/dist/src/run.d.ts +43 -34
  41. package/dist/src/run.js +34 -397
  42. package/dist/src/util/canonical.d.ts +1 -5
  43. package/dist/src/util/canonical.js +1 -53
  44. package/dist/src/vela/cli.d.ts +7 -42
  45. package/dist/src/vela/cli.js +102 -475
  46. package/dist/src/vela/types.d.ts +3 -19
  47. package/dist/src/verifier/run.js +1 -1
  48. package/dist/src/workspace/prepare.d.ts +1 -1
  49. package/dist/src/workspace/prepare.js +15 -15
  50. package/docs/EVALUATION.md +142 -0
  51. package/docs/MISSIONS.md +42 -80
  52. package/docs/RUN_RECORD.md +77 -99
  53. package/docs/adr/0010-nonmutating-runs-and-explicit-submission.md +191 -0
  54. package/missions/erdos1056-k15-next/mission.draft.json +10 -8
  55. package/package.json +18 -15
  56. package/profiles/{erdos1056-k15-10428801-10429000.json → erdos1056-k15-10429201-10429400.json} +10 -10
  57. package/schemas/failure.v1.json +17 -0
  58. package/schemas/mission.v0.json +3 -4
  59. package/schemas/mission.v1.json +7 -28
  60. package/schemas/{run.v1.json → run.v2.json} +58 -116
  61. package/schemas/submission-bundle.v1.json +63 -0
  62. package/toolchain.lock.json +31 -0
  63. package/capsules/formal-erdos-505-test-dim-one/verifier +0 -105
  64. package/capsules/quantum-10-1-4/verifier.py +0 -152
  65. package/capsules/sidon-a24/bin/linux-arm64/verifier +0 -0
  66. package/capsules/sidon-a24/bin/linux-x86_64/verifier +0 -0
  67. package/dist/src/engines/codex-exec.d.ts +0 -17
  68. package/dist/src/engines/codex-exec.js +0 -136
  69. package/dist/src/engines/codex-tool-free.d.ts +0 -26
  70. package/dist/src/engines/codex-tool-free.js +0 -213
  71. package/dist/src/projection/diagnostic.d.ts +0 -12
  72. package/dist/src/projection/diagnostic.js +0 -61
  73. package/dist/src/projection/public-run.d.ts +0 -59
  74. package/dist/src/projection/public-run.js +0 -106
  75. package/dist/src/projection/publication.d.ts +0 -36
  76. package/dist/src/projection/publication.js +0 -47
  77. package/dist/src/projection/run.d.ts +0 -95
  78. package/dist/src/projection/run.js +0 -173
  79. package/dist/src/receipt/map.js +0 -168
  80. package/dist/src/repair/plan.d.ts +0 -2
  81. package/dist/src/repair/plan.js +0 -22
  82. package/dist/src/roles.d.ts +0 -2
  83. package/dist/src/roles.js +0 -9
  84. package/missions/formal-erdos-505-test-dim-one-gpt56/mission.draft.json +0 -40
  85. package/missions/quantum-10-1-4-retry/mission.draft.json +0 -40
  86. package/missions/sidon-a24-at-least-7194-gpt56/mission.draft.json +0 -42
  87. package/missions/sidon-a24-at-least-7194-gpt56-v2/mission.draft.json +0 -42
  88. package/missions/sidon-a24-at-least-7194-gpt56-v3/mission.draft.json +0 -42
  89. package/profiles/formal-erdos-505-test-dim-one-gpt56.json +0 -35
  90. package/profiles/quantum-10-1-4-stabilizer-retry.json +0 -35
  91. package/profiles/sidon-a24-at-least-7194-gpt56-v2.json +0 -35
  92. package/profiles/sidon-a24-at-least-7194-gpt56-v3.json +0 -35
  93. package/profiles/sidon-a24-at-least-7194-gpt56.json +0 -35
  94. package/schemas/canopus.public-run.v1.json +0 -116
@@ -0,0 +1,264 @@
1
+ import { constants } from "node:fs";
2
+ import { access, mkdtemp, realpath, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { protocolDigest, sha256Bytes } from "../util/canonical.js";
6
+ import { runCommand } from "../util/command.js";
7
+ import { readBoundedRegularFile } from "../util/files.js";
8
+ import { SUPPORTED_VELA_VERSION } from "./version.js";
9
+ import { findExecutable } from "./runtime.js";
10
+ import { verifySubmission, } from "./submission.js";
11
+ async function command(runner, argv, cwd, maxOutputBytes = 8 * 1024 * 1024) {
12
+ const result = await runner({
13
+ argv,
14
+ cwd,
15
+ env: process.env,
16
+ timeoutMs: 120_000,
17
+ maxOutputBytes,
18
+ });
19
+ if (result.exitCode !== 0) {
20
+ let diagnostic = "no structured error";
21
+ try {
22
+ const parsed = JSON.parse(result.stdout.toString("utf8"));
23
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
24
+ const error = parsed.error;
25
+ if (typeof error === "object" && error !== null && !Array.isArray(error)) {
26
+ const message = error.message;
27
+ if (typeof message === "string" && message.length > 0) {
28
+ diagnostic = [...message]
29
+ .slice(0, 512)
30
+ .join("")
31
+ .replace(/\b(?:sk|sess|key)-[A-Za-z0-9_-]{8,}\b/gu, "[secret-redacted]");
32
+ }
33
+ }
34
+ }
35
+ }
36
+ catch {
37
+ // Raw subprocess output is represented only by its digest below.
38
+ }
39
+ throw new Error(`${argv[0]} ${argv[1] ?? ""} exited ${result.exitCode}: ${diagnostic}; ` +
40
+ `stdout_sha256=${sha256Bytes(result.stdout)}; stderr_sha256=${sha256Bytes(result.stderr)}`);
41
+ }
42
+ return result.stdout.toString("utf8").trim();
43
+ }
44
+ function parseManifest(value) {
45
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
46
+ throw new Error("Submission bundle manifest is not an object");
47
+ }
48
+ const object = value;
49
+ const exactKeys = (record, expected, label) => {
50
+ const observed = Object.keys(record).sort();
51
+ const wanted = [...expected].sort();
52
+ if (observed.length !== wanted.length || observed.some((key, index) => key !== wanted[index])) {
53
+ throw new Error(`${label} has unsupported or missing fields`);
54
+ }
55
+ };
56
+ const requiredString = (record, key, label) => {
57
+ const field = record[key];
58
+ if (typeof field !== "string" || field.length === 0 || field !== field.trim()) {
59
+ throw new Error(`${label}.${key} must be non-empty trimmed text`);
60
+ }
61
+ return field;
62
+ };
63
+ const requiredObject = (record, key, label) => {
64
+ const field = record[key];
65
+ if (typeof field !== "object" || field === null || Array.isArray(field)) {
66
+ throw new Error(`${label}.${key} must be an object`);
67
+ }
68
+ return field;
69
+ };
70
+ const safePath = (candidate, label) => {
71
+ if (path.isAbsolute(candidate) ||
72
+ candidate.includes("\\") ||
73
+ candidate.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
74
+ throw new Error(`${label} must be a safe relative POSIX path`);
75
+ }
76
+ return candidate;
77
+ };
78
+ exactKeys(object, [
79
+ "schema",
80
+ "run_id",
81
+ "run_root",
82
+ "source",
83
+ "producer",
84
+ "submission_id",
85
+ "submission_root",
86
+ "identity_binding_id",
87
+ "artifacts",
88
+ ], "Submission bundle manifest");
89
+ const source = requiredObject(object, "source", "manifest");
90
+ exactKeys(source, ["git_commit", "git_tree", "vela_version", "vela_sha256"], "manifest.source");
91
+ const rawArtifacts = object.artifacts;
92
+ if (!Array.isArray(rawArtifacts) || rawArtifacts.length === 0) {
93
+ throw new Error("Submission bundle contains no Artifacts");
94
+ }
95
+ const artifacts = rawArtifacts.map((entry, index) => {
96
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
97
+ throw new Error(`manifest.artifacts[${index}] must be an object`);
98
+ }
99
+ const artifact = entry;
100
+ exactKeys(artifact, ["source", "frontier_path", "digest", "bytes"], `manifest.artifacts[${index}]`);
101
+ const bytes = artifact.bytes;
102
+ if (!Number.isSafeInteger(bytes) || Number(bytes) <= 0) {
103
+ throw new Error(`manifest.artifacts[${index}].bytes must be a positive safe integer`);
104
+ }
105
+ return {
106
+ source: safePath(requiredString(artifact, "source", `manifest.artifacts[${index}]`), `manifest.artifacts[${index}].source`),
107
+ frontier_path: safePath(requiredString(artifact, "frontier_path", `manifest.artifacts[${index}]`), `manifest.artifacts[${index}].frontier_path`),
108
+ digest: requiredString(artifact, "digest", `manifest.artifacts[${index}]`),
109
+ bytes: Number(bytes),
110
+ };
111
+ });
112
+ const manifest = {
113
+ schema: requiredString(object, "schema", "manifest"),
114
+ run_id: requiredString(object, "run_id", "manifest"),
115
+ run_root: requiredString(object, "run_root", "manifest"),
116
+ source: {
117
+ git_commit: requiredString(source, "git_commit", "manifest.source"),
118
+ git_tree: requiredString(source, "git_tree", "manifest.source"),
119
+ vela_version: requiredString(source, "vela_version", "manifest.source"),
120
+ vela_sha256: requiredString(source, "vela_sha256", "manifest.source"),
121
+ },
122
+ producer: requiredString(object, "producer", "manifest"),
123
+ submission_id: requiredString(object, "submission_id", "manifest"),
124
+ submission_root: requiredString(object, "submission_root", "manifest"),
125
+ identity_binding_id: requiredString(object, "identity_binding_id", "manifest"),
126
+ artifacts,
127
+ };
128
+ if (manifest.schema !== "canopus.submission-bundle.v1") {
129
+ throw new Error("Submission bundle schema must be canopus.submission-bundle.v1");
130
+ }
131
+ if (new Set(manifest.artifacts.map((artifact) => artifact.source)).size !== manifest.artifacts.length ||
132
+ new Set(manifest.artifacts.map((artifact) => artifact.frontier_path)).size !== manifest.artifacts.length) {
133
+ throw new Error("Submission bundle contains duplicate Artifact paths");
134
+ }
135
+ return manifest;
136
+ }
137
+ export async function submitBundle(options) {
138
+ const runner = options.runner ?? runCommand;
139
+ const bundle = await realpath(options.bundle);
140
+ const frontier = await realpath(options.frontier);
141
+ const manifest = parseManifest(JSON.parse((await readBoundedRegularFile(path.join(bundle, "manifest.json"), 8 * 1024 * 1024)).toString("utf8")));
142
+ const submission = JSON.parse((await readBoundedRegularFile(path.join(bundle, "submission.json"), 8 * 1024 * 1024)).toString("utf8"));
143
+ verifySubmission(submission);
144
+ if (submission.submission_id !== manifest.submission_id) {
145
+ throw new Error("Submission bundle identity mismatch");
146
+ }
147
+ if (submission.provenance.producer !== manifest.producer ||
148
+ submission.authentication.identity_binding.binding_id !== manifest.identity_binding_id) {
149
+ throw new Error("Submission bundle producer binding mismatch");
150
+ }
151
+ if (protocolDigest(submission) !== manifest.submission_root) {
152
+ throw new Error("Submission bundle root mismatch");
153
+ }
154
+ const declaredArtifacts = submission.artifacts
155
+ .map((artifact) => `${artifact.path}\0${artifact.digest}`)
156
+ .sort();
157
+ const bundledArtifacts = manifest.artifacts
158
+ .map((artifact) => `${artifact.frontier_path}\0${artifact.digest}`)
159
+ .sort();
160
+ if (declaredArtifacts.length !== bundledArtifacts.length ||
161
+ declaredArtifacts.some((entry, index) => entry !== bundledArtifacts[index])) {
162
+ throw new Error("Submission and bundle Artifact bindings disagree");
163
+ }
164
+ const status = await command(runner, ["git", "status", "--porcelain=v1", "--untracked-files=all"], frontier);
165
+ if (status !== "")
166
+ throw new Error("source frontier must be clean before submit");
167
+ const before = await command(runner, ["git", "rev-parse", "--verify", "HEAD^{commit}"], frontier);
168
+ const sourceTree = await command(runner, ["git", "rev-parse", "--verify", `${manifest.source.git_commit}^{tree}`], frontier);
169
+ const sourceOrigin = await command(runner, ["git", "remote", "get-url", "origin"], frontier);
170
+ if (sourceTree !== manifest.source.git_tree) {
171
+ throw new Error("retained Run source commit no longer matches its exact Git tree");
172
+ }
173
+ if (before !== manifest.source.git_commit) {
174
+ const ancestry = await runner({
175
+ argv: ["git", "merge-base", "--is-ancestor", manifest.source.git_commit, before],
176
+ cwd: frontier,
177
+ env: process.env,
178
+ timeoutMs: 120_000,
179
+ maxOutputBytes: 64 * 1024,
180
+ });
181
+ if (ancestry.exitCode !== 0) {
182
+ throw new Error("current frontier is not a descendant of the retained Run source commit");
183
+ }
184
+ }
185
+ const vela = await findExecutable(options.velaBinary ?? process.env.VELA_BIN ?? "vela");
186
+ const observedVela = sha256Bytes(await readBoundedRegularFile(vela, 512 * 1024 * 1024));
187
+ const version = await command(runner, [vela, "--version"], frontier, 64 * 1024);
188
+ if (version !== `vela ${SUPPORTED_VELA_VERSION}`) {
189
+ throw new Error(`Canopus requires registration through vela ${SUPPORTED_VELA_VERSION}, observed ${version}; ` +
190
+ `the retained Run source binary remains recorded separately as ${manifest.source.vela_version} ` +
191
+ `(${manifest.source.vela_sha256})`);
192
+ }
193
+ const temporary = await mkdtemp(path.join(os.tmpdir(), "canopus-submit-"));
194
+ const clone = path.join(temporary, "frontier");
195
+ try {
196
+ await command(runner, ["git", "clone", "--no-hardlinks", "--", frontier, clone], temporary);
197
+ const cloneHead = await command(runner, ["git", "rev-parse", "--verify", "HEAD^{commit}"], clone);
198
+ if (cloneHead !== before)
199
+ throw new Error("disposable submit clone does not match source HEAD");
200
+ await command(runner, ["git", "remote", "set-url", "origin", sourceOrigin], clone);
201
+ for (const artifact of manifest.artifacts) {
202
+ const source = path.join(bundle, artifact.source);
203
+ await access(source, constants.R_OK);
204
+ const bytes = await readBoundedRegularFile(source, artifact.bytes + 1);
205
+ if (bytes.length !== artifact.bytes || sha256Bytes(bytes) !== artifact.digest) {
206
+ throw new Error(`bundle Artifact ${artifact.digest} is missing or corrupt`);
207
+ }
208
+ }
209
+ const resultText = await command(runner, [
210
+ vela,
211
+ "submit",
212
+ path.join(bundle, "submission.json"),
213
+ "--frontier",
214
+ clone,
215
+ "--as",
216
+ manifest.producer,
217
+ "--json",
218
+ ], clone);
219
+ const result = JSON.parse(resultText);
220
+ if (result.ok !== true ||
221
+ result.schema !== "vela.submit-result.v1" ||
222
+ result.submission_id !== manifest.submission_id ||
223
+ result.submission_root !== manifest.submission_root ||
224
+ result.route !== "pending_review" ||
225
+ result.accepted_event_delta !== 0) {
226
+ throw new Error("Vela returned an invalid or authority-changing submit result");
227
+ }
228
+ const after = await command(runner, ["git", "rev-parse", "--verify", "HEAD^{commit}"], clone);
229
+ if (after === before)
230
+ throw new Error("Vela submit produced no registration commit");
231
+ const sourceStill = await command(runner, ["git", "rev-parse", "--verify", "HEAD^{commit}"], frontier);
232
+ const sourceStatus = await command(runner, ["git", "status", "--porcelain=v1", "--untracked-files=all"], frontier);
233
+ if (sourceStill !== before || sourceStatus !== "") {
234
+ throw new Error("source frontier changed before the verified registration was ready");
235
+ }
236
+ await command(runner, ["git", "fetch", "--no-tags", "--", clone, after], frontier);
237
+ await command(runner, ["git", "merge", "--ff-only", "FETCH_HEAD"], frontier);
238
+ const installed = await command(runner, ["git", "rev-parse", "--verify", "HEAD^{commit}"], frontier);
239
+ if (installed !== after)
240
+ throw new Error("source frontier did not fast-forward to the registration");
241
+ const finalStatus = await command(runner, ["git", "status", "--porcelain=v1", "--untracked-files=all"], frontier);
242
+ if (finalStatus !== "")
243
+ throw new Error("source frontier is dirty after submit");
244
+ return {
245
+ schema: "canopus.submit-result.v1",
246
+ ok: true,
247
+ submission_id: manifest.submission_id,
248
+ submission_root: manifest.submission_root,
249
+ registration_record_id: String(result.registration_record_id),
250
+ registration_record_root: String(result.registration_record_root),
251
+ proposal_id: String(result.proposal_id),
252
+ claim_id: String(result.claim_id),
253
+ route: "pending_review",
254
+ accepted_event_delta: 0,
255
+ source_commit_before: before,
256
+ source_commit_after: after,
257
+ registration_binary_version: version,
258
+ registration_binary_sha256: observedVela,
259
+ };
260
+ }
261
+ finally {
262
+ await rm(temporary, { recursive: true, force: true });
263
+ }
264
+ }
@@ -1,3 +1,3 @@
1
1
  export declare const CANOPUS_VERSION: string;
2
- export declare const SUPPORTED_VELA_VERSION = "0.915.1";
3
- export declare const SUPPORTED_CODEX_VERSION = "0.145.0";
2
+ export declare const SUPPORTED_VELA_VERSION: string;
3
+ export declare const SUPPORTED_CODEX_VERSION: string;
@@ -1,8 +1,31 @@
1
- import { readFileSync } from "node:fs";
2
- const packageJson = JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8"));
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ const packageUrl = [
4
+ new URL("../../package.json", import.meta.url),
5
+ new URL("../../../package.json", import.meta.url),
6
+ ].find((candidate) => existsSync(fileURLToPath(candidate)));
7
+ if (packageUrl === undefined)
8
+ throw new Error("Canopus package.json is missing");
9
+ const packageJson = JSON.parse(readFileSync(packageUrl, "utf8"));
10
+ const toolchainUrl = [
11
+ new URL("../../toolchain.lock.json", import.meta.url),
12
+ new URL("../../../toolchain.lock.json", import.meta.url),
13
+ ].find((candidate) => existsSync(fileURLToPath(candidate)));
14
+ if (toolchainUrl === undefined)
15
+ throw new Error("Canopus toolchain.lock.json is missing");
16
+ const toolchain = JSON.parse(readFileSync(toolchainUrl, "utf8"));
3
17
  if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
4
18
  throw new Error("Canopus package version is missing");
5
19
  }
6
20
  export const CANOPUS_VERSION = packageJson.version;
7
- export const SUPPORTED_VELA_VERSION = "0.915.1";
8
- export const SUPPORTED_CODEX_VERSION = "0.145.0";
21
+ if (toolchain.schema !== "canopus.toolchain-lock.v1") {
22
+ throw new Error("Canopus toolchain lock schema is unsupported");
23
+ }
24
+ if (typeof toolchain.vela?.version !== "string" || toolchain.vela.version.length === 0) {
25
+ throw new Error("Canopus Vela toolchain version is missing");
26
+ }
27
+ if (typeof toolchain.codex?.version !== "string" || toolchain.codex.version.length === 0) {
28
+ throw new Error("Canopus Codex toolchain version is missing");
29
+ }
30
+ export const SUPPORTED_VELA_VERSION = toolchain.vela.version;
31
+ export const SUPPORTED_CODEX_VERSION = toolchain.codex.version;
@@ -0,0 +1,12 @@
1
+ import type { CurrentRunRecord } from "../run.js";
2
+ export declare function parseCurrentRunRecord(value: unknown): CurrentRunRecord;
3
+ export declare function projectCurrentRun(record: CurrentRunRecord): {
4
+ schema: "canopus.run-projection.v2";
5
+ authority: "read_only_projection";
6
+ run_id: string;
7
+ target: string;
8
+ candidate_digest: string;
9
+ verifier_status: "passed" | "failed" | "error";
10
+ submitted: false;
11
+ clean_clone_reproduced: true;
12
+ };
@@ -0,0 +1,94 @@
1
+ import { arrayAt, enumAt, exactKeys, gitObjectAt, integerAt, objectAt, relativePathAt, sha256At, stringAt, } from "../contracts/validation.js";
2
+ function literal(value, expected, at) {
3
+ if (value !== expected)
4
+ throw new Error(`${at} must be ${String(expected)}`);
5
+ return expected;
6
+ }
7
+ function rootsAt(value, at) {
8
+ const roots = objectAt(value, at);
9
+ exactKeys(roots, ["git_commit", "git_tree", "vela_repository"], [], at);
10
+ return {
11
+ git_commit: gitObjectAt(roots.git_commit, `${at}.git_commit`),
12
+ git_tree: gitObjectAt(roots.git_tree, `${at}.git_tree`),
13
+ vela_repository: sha256At(roots.vela_repository, `${at}.vela_repository`),
14
+ };
15
+ }
16
+ export function parseCurrentRunRecord(value) {
17
+ const record = objectAt(value, "run");
18
+ exactKeys(record, [
19
+ "schema", "run_id", "status", "effect", "authority", "external_gate_credit",
20
+ "mission", "candidate", "verifier", "submission", "reproduction", "budget",
21
+ ], [], "run");
22
+ const mission = objectAt(record.mission, "run.mission");
23
+ exactKeys(mission, ["id", "target", "digest", "starting_roots"], [], "run.mission");
24
+ const candidate = objectAt(record.candidate, "run.candidate");
25
+ exactKeys(candidate, ["digest", "status", "claim", "artifacts", "caveats"], [], "run.candidate");
26
+ const verifier = objectAt(record.verifier, "run.verifier");
27
+ exactKeys(verifier, ["status", "sandbox", "record"], [], "run.verifier");
28
+ const verifierRecord = objectAt(verifier.record, "run.verifier.record");
29
+ exactKeys(verifierRecord, ["argv", "executable_digest", "exit_code", "stdout_digest", "stderr_digest", "duration_ms"], [], "run.verifier.record");
30
+ const reproduction = objectAt(record.reproduction, "run.reproduction");
31
+ exactKeys(reproduction, ["matched", "roots", "verifier_status", "stdout_digest", "stderr_digest"], [], "run.reproduction");
32
+ const budget = objectAt(record.budget, "run.budget");
33
+ exactKeys(budget, [
34
+ "research_elapsed_ms", "research_processes", "research_output_bytes",
35
+ "prompt_bytes", "artifact_bytes", "attempts", "observed_tokens",
36
+ ], [], "run.budget");
37
+ rootsAt(mission.starting_roots, "run.mission.starting_roots");
38
+ rootsAt(reproduction.roots, "run.reproduction.roots");
39
+ literal(record.schema, "canopus.run.v2", "run.schema");
40
+ literal(record.status, "completed", "run.status");
41
+ literal(record.effect, "none", "run.effect");
42
+ literal(record.authority, "non_authoritative", "run.authority");
43
+ literal(record.external_gate_credit, false, "run.external_gate_credit");
44
+ literal(record.submission, null, "run.submission");
45
+ stringAt(record.run_id, "run.run_id", { min: 5, max: 128 });
46
+ stringAt(mission.id, "run.mission.id", { min: 1, max: 134 });
47
+ stringAt(mission.target, "run.mission.target", { min: 1, max: 256 });
48
+ sha256At(mission.digest, "run.mission.digest");
49
+ sha256At(candidate.digest, "run.candidate.digest");
50
+ enumAt(candidate.status, "run.candidate.status", ["success", "null", "failed"]);
51
+ stringAt(candidate.claim, "run.candidate.claim", { min: 1, max: 8192 });
52
+ arrayAt(candidate.artifacts, "run.candidate.artifacts", { min: 1, max: 10 }, (item, at) => {
53
+ const artifact = objectAt(item, at);
54
+ exactKeys(artifact, ["path", "kind", "digest", "bytes"], [], at);
55
+ relativePathAt(artifact.path, `${at}.path`);
56
+ stringAt(artifact.kind, `${at}.kind`, { min: 1, max: 128 });
57
+ sha256At(artifact.digest, `${at}.digest`);
58
+ integerAt(artifact.bytes, `${at}.bytes`, 0, 1_073_741_824);
59
+ return true;
60
+ });
61
+ arrayAt(candidate.caveats, "run.candidate.caveats", { min: 1, max: 10 }, (item, at) => stringAt(item, at, { min: 1, max: 4096 }));
62
+ literal(verifier.status, "passed", "run.verifier.status");
63
+ enumAt(verifier.sandbox, "run.verifier.sandbox", ["macos_sandbox", "container_network_denied"]);
64
+ arrayAt(verifierRecord.argv, "run.verifier.record.argv", { min: 1, max: 64 }, (item, at) => stringAt(item, at, { max: 4096 }));
65
+ sha256At(verifierRecord.executable_digest, "run.verifier.record.executable_digest");
66
+ integerAt(verifierRecord.exit_code, "run.verifier.record.exit_code", -1, 255);
67
+ sha256At(verifierRecord.stdout_digest, "run.verifier.record.stdout_digest");
68
+ sha256At(verifierRecord.stderr_digest, "run.verifier.record.stderr_digest");
69
+ integerAt(verifierRecord.duration_ms, "run.verifier.record.duration_ms", 0, 3_600_000);
70
+ literal(reproduction.matched, true, "run.reproduction.matched");
71
+ enumAt(reproduction.verifier_status, "run.reproduction.verifier_status", ["passed", "failed", "error"]);
72
+ sha256At(reproduction.stdout_digest, "run.reproduction.stdout_digest");
73
+ sha256At(reproduction.stderr_digest, "run.reproduction.stderr_digest");
74
+ integerAt(budget.research_elapsed_ms, "run.budget.research_elapsed_ms", 0, 3_600_000);
75
+ integerAt(budget.research_processes, "run.budget.research_processes", 0, 64);
76
+ integerAt(budget.research_output_bytes, "run.budget.research_output_bytes", 0, 67_108_864);
77
+ integerAt(budget.prompt_bytes, "run.budget.prompt_bytes", 0, 8_388_608);
78
+ integerAt(budget.artifact_bytes, "run.budget.artifact_bytes", 0, 1_073_741_824);
79
+ integerAt(budget.attempts, "run.budget.attempts", 0, 8);
80
+ integerAt(budget.observed_tokens, "run.budget.observed_tokens", 0, 1_000_000);
81
+ return value;
82
+ }
83
+ export function projectCurrentRun(record) {
84
+ return {
85
+ schema: "canopus.run-projection.v2",
86
+ authority: "read_only_projection",
87
+ run_id: record.run_id,
88
+ target: record.mission.target,
89
+ candidate_digest: record.candidate.digest,
90
+ verifier_status: record.verifier.status,
91
+ submitted: false,
92
+ clean_clone_reproduced: true,
93
+ };
94
+ }
@@ -1,10 +1,20 @@
1
- export declare const FAILURE_RECORD_SCHEMA: "canopus.failure.v0";
2
- export declare const FAILURE_PROJECTION_SCHEMA: "canopus.failure-projection.v0";
1
+ export declare const FAILURE_RECORD_SCHEMA: "canopus.failure.v1";
2
+ export declare const HISTORICAL_FAILURE_RECORD_SCHEMA: "canopus.failure.v0";
3
+ export declare const FAILURE_PROJECTION_SCHEMA: "canopus.failure-projection.v1";
3
4
  export interface FailureRecord {
4
5
  schema: typeof FAILURE_RECORD_SCHEMA;
5
6
  run_id: string;
6
7
  error: string;
7
8
  phase: string;
9
+ effect: "none";
10
+ activity_tip: string;
11
+ authority: "non_authoritative";
12
+ }
13
+ export interface HistoricalFailureRecord {
14
+ schema: typeof HISTORICAL_FAILURE_RECORD_SCHEMA;
15
+ run_id: string;
16
+ error: string;
17
+ phase: string;
8
18
  landing_observed: boolean;
9
19
  landing_recovery: string | null;
10
20
  activity_tip: string;
@@ -17,10 +27,10 @@ export interface FailureProjection {
17
27
  status: "failed";
18
28
  phase: string;
19
29
  error: string;
20
- landing_status: "not_attempted" | "observed_requires_recovery";
21
- landing_recovery: string | null;
30
+ effect: "none" | "historical_landing_observed";
31
+ recovery: string | null;
22
32
  activity_tip: string;
23
- deletion_test: "Vela state does not depend on this projection; inspect retained recovery evidence for any observed landing effect.";
33
+ deletion_test: "Vela state does not depend on this projection; show retained recovery evidence for any historical landing effect.";
24
34
  }
25
- export declare function parseFailureRecord(value: unknown): FailureRecord;
26
- export declare function projectFailure(record: FailureRecord): FailureProjection;
35
+ export declare function parseFailureRecord(value: unknown): FailureRecord | HistoricalFailureRecord;
36
+ export declare function projectFailure(record: FailureRecord | HistoricalFailureRecord): FailureProjection;
@@ -1,6 +1,7 @@
1
1
  import { exactKeys, objectAt, relativePathAt, sha256At, stringAt, } from "../contracts/validation.js";
2
- export const FAILURE_RECORD_SCHEMA = "canopus.failure.v0";
3
- export const FAILURE_PROJECTION_SCHEMA = "canopus.failure-projection.v0";
2
+ export const FAILURE_RECORD_SCHEMA = "canopus.failure.v1";
3
+ export const HISTORICAL_FAILURE_RECORD_SCHEMA = "canopus.failure.v0";
4
+ export const FAILURE_PROJECTION_SCHEMA = "canopus.failure-projection.v1";
4
5
  function literal(value, expected, at) {
5
6
  if (value !== expected)
6
7
  throw new Error(`${at} must be ${String(expected)}`);
@@ -13,13 +14,44 @@ function booleanAt(value, at) {
13
14
  }
14
15
  export function parseFailureRecord(value) {
15
16
  const record = objectAt(value, "failure");
17
+ if (record.schema === HISTORICAL_FAILURE_RECORD_SCHEMA) {
18
+ exactKeys(record, [
19
+ "schema",
20
+ "run_id",
21
+ "error",
22
+ "phase",
23
+ "landing_observed",
24
+ "landing_recovery",
25
+ "activity_tip",
26
+ "authority",
27
+ ], [], "failure");
28
+ return {
29
+ schema: HISTORICAL_FAILURE_RECORD_SCHEMA,
30
+ run_id: stringAt(record.run_id, "failure.run_id", {
31
+ min: 5,
32
+ max: 128,
33
+ pattern: /^run_[A-Za-z0-9._-]+$/u,
34
+ }),
35
+ error: stringAt(record.error, "failure.error", { min: 1, max: 16_384 }),
36
+ phase: stringAt(record.phase, "failure.phase", {
37
+ min: 1,
38
+ max: 128,
39
+ pattern: /^[a-z][a-z0-9_]*$/u,
40
+ }),
41
+ landing_observed: booleanAt(record.landing_observed, "failure.landing_observed"),
42
+ landing_recovery: record.landing_recovery === null
43
+ ? null
44
+ : relativePathAt(record.landing_recovery, "failure.landing_recovery"),
45
+ activity_tip: sha256At(record.activity_tip, "failure.activity_tip"),
46
+ authority: literal(record.authority, "non_authoritative", "failure.authority"),
47
+ };
48
+ }
16
49
  exactKeys(record, [
17
50
  "schema",
18
51
  "run_id",
19
52
  "error",
20
53
  "phase",
21
- "landing_observed",
22
- "landing_recovery",
54
+ "effect",
23
55
  "activity_tip",
24
56
  "authority",
25
57
  ], [], "failure");
@@ -36,18 +68,17 @@ export function parseFailureRecord(value) {
36
68
  max: 128,
37
69
  pattern: /^[a-z][a-z0-9_]*$/u,
38
70
  }),
39
- landing_observed: booleanAt(record.landing_observed, "failure.landing_observed"),
40
- landing_recovery: record.landing_recovery === null
41
- ? null
42
- : relativePathAt(record.landing_recovery, "failure.landing_recovery"),
71
+ effect: literal(record.effect, "none", "failure.effect"),
43
72
  activity_tip: sha256At(record.activity_tip, "failure.activity_tip"),
44
73
  authority: literal(record.authority, "non_authoritative", "failure.authority"),
45
74
  };
46
75
  }
47
76
  export function projectFailure(record) {
48
- if (record.landing_observed !== (record.landing_recovery !== null)) {
77
+ if (record.schema === HISTORICAL_FAILURE_RECORD_SCHEMA &&
78
+ record.landing_observed !== (record.landing_recovery !== null)) {
49
79
  throw new Error("failure landing observation and recovery evidence disagree");
50
80
  }
81
+ const historicalObserved = record.schema === HISTORICAL_FAILURE_RECORD_SCHEMA && record.landing_observed;
51
82
  return {
52
83
  schema: FAILURE_PROJECTION_SCHEMA,
53
84
  authority: "read_only_projection",
@@ -55,9 +86,9 @@ export function projectFailure(record) {
55
86
  status: "failed",
56
87
  phase: record.phase,
57
88
  error: record.error,
58
- landing_status: record.landing_observed ? "observed_requires_recovery" : "not_attempted",
59
- landing_recovery: record.landing_recovery,
89
+ effect: historicalObserved ? "historical_landing_observed" : "none",
90
+ recovery: record.schema === HISTORICAL_FAILURE_RECORD_SCHEMA ? record.landing_recovery : null,
60
91
  activity_tip: record.activity_tip,
61
- deletion_test: "Vela state does not depend on this projection; inspect retained recovery evidence for any observed landing effect.",
92
+ deletion_test: "Vela state does not depend on this projection; show retained recovery evidence for any historical landing effect.",
62
93
  };
63
94
  }
@@ -0,0 +1,6 @@
1
+ import { type MissionV1 } from "../contracts/mission.js";
2
+ export declare function parseRetainedMission(value: unknown): {
3
+ mission: MissionV1;
4
+ exactRoot: string;
5
+ exactRoots: string;
6
+ };
@@ -0,0 +1,41 @@
1
+ import { parseMission } from "../contracts/mission.js";
2
+ import { arrayAt, enumAt, exactKeys, integerAt, objectAt, sha256At, stringAt, } from "../contracts/validation.js";
3
+ import { canonicalJcs, canonicalJson, sha256Bytes } from "../util/canonical.js";
4
+ import { normalizeRetainedRunRoots } from "./retained-run.js";
5
+ export function parseRetainedMission(value) {
6
+ const raw = objectAt(value, "retained Mission");
7
+ const roots = objectAt(raw.roots, "retained Mission.roots");
8
+ if (raw.strict_baseline !== undefined) {
9
+ const baseline = objectAt(raw.strict_baseline, "retained Mission.strict_baseline");
10
+ exactKeys(baseline, ["status", "blocker_count", "blockers_root", "rule_counts"], [], "retained Mission.strict_baseline");
11
+ enumAt(baseline.status, "retained Mission.strict_baseline.status", ["pass", "fail"]);
12
+ integerAt(baseline.blocker_count, "retained Mission.strict_baseline.blocker_count", 0, 1_000_000);
13
+ sha256At(baseline.blockers_root, "retained Mission.strict_baseline.blockers_root");
14
+ arrayAt(baseline.rule_counts, "retained Mission.strict_baseline.rule_counts", { min: 0, max: 256 }, (item, at) => {
15
+ const count = objectAt(item, at);
16
+ exactKeys(count, ["count", "rule"], [], at);
17
+ integerAt(count.count, `${at}.count`, 0, 1_000_000);
18
+ stringAt(count.rule, `${at}.rule`, {
19
+ min: 1,
20
+ max: 128,
21
+ pattern: /^[a-z][a-z0-9_]*$/u,
22
+ });
23
+ return true;
24
+ });
25
+ }
26
+ const parseable = { ...raw };
27
+ delete parseable.strict_baseline;
28
+ const normalized = {
29
+ ...parseable,
30
+ roots: normalizeRetainedRunRoots(roots, "retained Mission.roots"),
31
+ };
32
+ const mission = parseMission(normalized);
33
+ if (mission.schema !== "canopus.mission.v1") {
34
+ throw new Error("current reader requires canopus.mission.v1");
35
+ }
36
+ return {
37
+ mission,
38
+ exactRoot: sha256Bytes(canonicalJson(raw)),
39
+ exactRoots: canonicalJcs(roots),
40
+ };
41
+ }
@@ -0,0 +1,7 @@
1
+ import { parseCurrentRunRecord } from "./current-run.js";
2
+ export declare function normalizeRetainedRunRoots(value: unknown, at: string): Record<string, string>;
3
+ export declare function parseRetainedRunRecord(value: unknown): {
4
+ record: ReturnType<typeof parseCurrentRunRecord>;
5
+ exactRoot: string;
6
+ exactStartingRoots: string;
7
+ };