mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 08:11:17 -10:00
fix(cli): Restore-Packet M2a R2 — schema packaging fix; validator self-contained after raw tsc emit
Guard BLOCKED M2a at de2dbd4 with a build/packaging gap: schema-validator
read `restore-summary.schema.json` from disk via import.meta.url, but tsc
does not copy non-TS files into dist. Compiled validator hit ENOENT at
runtime in the guard's reproducer (raw `npx tsc --outDir <cache> --rootDir
src` + `node -e "import(...).then(m => m.validateRestoreSummary({}))"`).
R2 fix (combined approach 2 + approach 1 from guard verdict):
(1) src/restore-packet/schema-validator.ts: schema is now embedded as a
typed TS const RESTORE_SUMMARY_SCHEMA (verbatim from contract § 2.1 and
the canonical .json file). Validator uses the const directly; no
import.meta.url file read at runtime. tsc emits the const into compiled
JS, so `tsc` alone (no copy step) leaves a working validator.
(2) src/schemas/restore-summary.schema.json: kept as the canonical source
for downstream IDE / tooling consumption. Unchanged content.
(3) test/restore-packet.test.ts: added a drift-catcher test that loads
the JSON file and asserts `RESTORE_SUMMARY_SCHEMA` (the TS const) equals
it byte-for-byte. Prevents the two forms from diverging silently. Test
count 17 → 18.
(4) packages/cli/package.json: build script changed from `tsc` to
`tsc && mkdir -p dist/schemas && cp src/schemas/*.json dist/schemas/`.
This is approach 1 from the guard verdict — keeps the JSON Schema
reachable in `dist/schemas/` for downstream tooling AFTER `npm run build`.
Note: the guard's reproducer uses raw `tsc` (not `npm run build`), so the
copy step is for distribution only; the runtime correctness comes from
approach 2 (the embedded TS const).
Re-verification (per guard's required-fix list):
- `npx vitest run test/restore-packet.test.ts` → 18/18 pass (M2a focused
suite + drift-catcher; M2a's prior 17 still green plus the new test).
- `npx tsc --noEmit` → exit 0; no type errors.
- Guard's reproducer:
rm -rf node_modules/.cache/openrig-cli-m2a-r2-build-check
npx tsc --outDir node_modules/.cache/openrig-cli-m2a-r2-build-check --rootDir src
node -e "import('node_modules/.cache/openrig-cli-m2a-r2-build-check/restore-packet/schema-validator.js').then((m) => { const r = m.validateRestoreSummary({}); console.log('valid:', r.valid, 'errors:', r.errors.length); }).catch(...)"
→ output: `valid: false errors: 21` (21 required-field violations for
empty `{}` summary, exactly as expected from the schema's 21-required-
field set). NO ENOENT.
Full CLI suite re-run (per Quality Lesson v8 — never silently downgrade):
- npx vitest run (full suite) → 658/662 pass.
- M2a baseline was 657/661; R2 adds the drift-catcher test, so totals
shift by +1/+1. Failure pattern UNCHANGED:
- test/chatroom.test.ts > Chatroom CLI > chatroom wait detects a newly
arriving post-start message (10s timing flake)
- test/config.test.ts > DaemonClient config integration > ConfigStore
resolves non-default host/port from config file (env pollution from
prior test runs)
- test/preflight.test.ts > Preflight CLI > rig preflight --json prints
structured result (host port 7433 in use; real daemon running)
- test/preflight.test.ts > Preflight CLI > rig preflight prints check
marks and exits 0 on pass (same host port 7433 issue)
All four are pre-existing environmental flakes (host has a real daemon
on port 7433; chatroom timing flake; env pollution between test runs).
None of the four failures touch any file in this R2 commit. No
regression introduced.
Scope (R2 stays narrow per dispatch):
- `restore-packet write` still a M2a shell that enforces mutual-exclusion
+ required-source behavior + returns the M2a deferred-implementation
error. UNCHANGED.
- `restore-packet read` + `restore-packet validate` still return M3-pending
errors. UNCHANGED.
- M2b parsers + redaction + omitted-records + packet-writer + daemon
route extension + daemon route tests NOT touched.
- No openrig source changes outside the three R2 files.
Posture: full QA/testing window (per dispatch's required-posture line).
Source qitem: qitem-20260502005113-24bf9de8 (M2a R2 dispatch).
Guard verdict: rigs/openrig-velocity/state/orch/restore-packet-M2a-guard-checkpoint-block-2026-05-02.md.
This commit is contained in:
1 parent
de2dbd4f3a
commit
40d07241ab
3 files changed
+201
-13
No files matched your search
@@ -35,7 +35,7 @@
|
||||
"scripts"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "tsc && mkdir -p dist/schemas && cp src/schemas/*.json dist/schemas/",
|
||||
"postinstall": "node scripts/check-abi.mjs",
|
||||
"prepublishOnly": "bash ../../scripts/build-package.sh",
|
||||
"test": "vitest run",
|
||||
|
||||
@@ -11,12 +11,18 @@
|
||||
// list. Each error names: field path, value (truncated/escaped), rule, and
|
||||
// severity. Required-field violations are severity `error`; optional-field
|
||||
// malformations are severity `warning` per § 8.
|
||||
//
|
||||
// M2a R2 packaging fix: the schema is embedded directly in this module as
|
||||
// a typed TS const (RESTORE_SUMMARY_SCHEMA) rather than read from the
|
||||
// sibling .json file at runtime. tsc emits the const into the compiled
|
||||
// validator JS, so the validator works after `tsc` emit without any extra
|
||||
// build-script copy step. The canonical JSON Schema file at
|
||||
// `packages/cli/src/schemas/restore-summary.schema.json` remains the
|
||||
// source of truth for downstream IDE / tooling consumption; a drift-catcher
|
||||
// test in `test/restore-packet.test.ts` asserts the two stay byte-equivalent.
|
||||
|
||||
import Ajv from "ajv";
|
||||
import addFormats from "ajv-formats";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
export interface ValidationError {
|
||||
field: string;
|
||||
@@ -30,22 +36,178 @@ export interface ValidationResult {
|
||||
errors: ValidationError[];
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
// RESTORE_SUMMARY_SCHEMA — verbatim mirror of
|
||||
// `packages/cli/src/schemas/restore-summary.schema.json`. Keep these in
|
||||
// lockstep; the drift-catcher test enforces equivalence.
|
||||
export const RESTORE_SUMMARY_SCHEMA: Record<string, unknown> = {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
$id: "https://openrig.dev/schemas/restore-summary.schema.json",
|
||||
title: "Restore Packet Summary",
|
||||
description:
|
||||
"Machine-readable metadata for a cross-runtime restore packet per the v0 paper standard.",
|
||||
type: "object",
|
||||
required: [
|
||||
"source_session_id",
|
||||
"source_rig",
|
||||
"source_runtime",
|
||||
"source_cwd",
|
||||
"target_rig",
|
||||
"target_runtime",
|
||||
"target_workspace_root",
|
||||
"default_target_repo",
|
||||
"role_pointer",
|
||||
"bounded_latest_transcript",
|
||||
"touched_files",
|
||||
"durable_pointers",
|
||||
"current_work_summary",
|
||||
"next_owner",
|
||||
"caveats",
|
||||
"authority_boundaries",
|
||||
"omitted_classes",
|
||||
"redaction_policy_id",
|
||||
"source_trust_ranking",
|
||||
"generator_version",
|
||||
"generated_at",
|
||||
],
|
||||
properties: {
|
||||
source_session_id: { type: "string", minLength: 1 },
|
||||
source_rig: { type: "string", minLength: 1 },
|
||||
source_runtime: {
|
||||
type: "string",
|
||||
enum: ["claude-code", "codex", "terminal", "external"],
|
||||
},
|
||||
source_cwd: { type: "string", pattern: "^/" },
|
||||
target_rig: { type: "string", minLength: 1 },
|
||||
target_runtime: {
|
||||
type: "string",
|
||||
enum: ["claude-code", "codex", "terminal", "external"],
|
||||
},
|
||||
target_workspace_root: { type: "string", pattern: "^/" },
|
||||
default_target_repo: { type: ["string", "null"] },
|
||||
role_pointer: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
description:
|
||||
"Path or URI pointing to the role guidance file the restored seat should consume. REQUIRED per v0 standard field #9; resolves at restore time.",
|
||||
},
|
||||
bounded_latest_transcript: {
|
||||
type: "object",
|
||||
required: ["path", "message_count", "bound"],
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
message_count: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
description: "Actual count of extracted messages in transcript-latest.md.",
|
||||
},
|
||||
bound: {
|
||||
type: "integer",
|
||||
const: 120,
|
||||
description:
|
||||
"v0 bound is fixed at 120 messages to match Velocity prior art (per contract § 7). v1+ may make configurable; v0 schema requires const 120 so packets with the wrong bound cannot pass.",
|
||||
},
|
||||
},
|
||||
},
|
||||
full_transcript: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
line_count: { type: "integer", minimum: 0 },
|
||||
},
|
||||
description:
|
||||
"Optional per v0 standard field #11. Generator emits when full transcript material is available (e.g., from --source-jsonl or full-read --source-session); omitted otherwise.",
|
||||
},
|
||||
touched_files: {
|
||||
type: "object",
|
||||
required: ["path", "top_paths"],
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
top_paths: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
required: ["path", "count"],
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
count: { type: "integer", minimum: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
durable_pointers: {
|
||||
type: "object",
|
||||
required: [
|
||||
"queue_pointers",
|
||||
"progress_pointers",
|
||||
"field_note_pointers",
|
||||
"artifact_pointers",
|
||||
],
|
||||
properties: {
|
||||
queue_pointers: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "References to durable queue items (qitem ids or queue-file paths).",
|
||||
},
|
||||
progress_pointers: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "References to PROGRESS.md cursors the source seat was operating against.",
|
||||
},
|
||||
field_note_pointers: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "References to field-notes folders or files.",
|
||||
},
|
||||
artifact_pointers: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Other durable artifacts (proof packets, slice packets, candidate dossiers).",
|
||||
},
|
||||
},
|
||||
description:
|
||||
"REQUIRED per v0 standard field #13. Pointers to durable work the source seat was operating against; restored seat reads to recover work-context.",
|
||||
},
|
||||
current_work_summary: { type: "string", minLength: 1 },
|
||||
next_owner: { type: "string", minLength: 1 },
|
||||
caveats: { type: "array", items: { type: "string" } },
|
||||
authority_boundaries: { type: "string", minLength: 1 },
|
||||
omitted_classes: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
enum: ["reasoning_records", "raw_tool_outputs", "function_call_output", "redacted_secrets"],
|
||||
},
|
||||
},
|
||||
redaction_policy_id: { type: "string", enum: ["velocity-v1", "openrig-v0"] },
|
||||
source_trust_ranking: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"rig_whoami",
|
||||
"target_rigspec",
|
||||
"bounded_latest_transcript",
|
||||
"full_transcript",
|
||||
"touched_files",
|
||||
"restore_summary",
|
||||
],
|
||||
},
|
||||
minItems: 1,
|
||||
},
|
||||
generator_version: { type: "string", minLength: 1 },
|
||||
generated_at: { type: "string", format: "date-time" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
let cachedValidator: ReturnType<Ajv["compile"]> | null = null;
|
||||
|
||||
function loadSchema(): object {
|
||||
// src/restore-packet/schema-validator.ts → src/schemas/restore-summary.schema.json
|
||||
const schemaPath = resolve(__dirname, "..", "schemas", "restore-summary.schema.json");
|
||||
return JSON.parse(readFileSync(schemaPath, "utf-8")) as object;
|
||||
}
|
||||
|
||||
function getValidator(): ReturnType<Ajv["compile"]> {
|
||||
if (cachedValidator) return cachedValidator;
|
||||
const ajv = new Ajv({ allErrors: true, strict: false });
|
||||
addFormats(ajv);
|
||||
cachedValidator = ajv.compile(loadSchema());
|
||||
cachedValidator = ajv.compile(RESTORE_SUMMARY_SCHEMA);
|
||||
return cachedValidator;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,12 @@
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { Command } from "commander";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import {
|
||||
validateRestoreSummary,
|
||||
RESTORE_SUMMARY_SCHEMA,
|
||||
type ValidationResult,
|
||||
} from "../src/restore-packet/schema-validator.js";
|
||||
import { restorePacketCommand } from "../src/commands/restore-packet.js";
|
||||
@@ -144,6 +148,28 @@ describe("M2 restore-packet schema-validator", () => {
|
||||
const result = validateRestoreSummary(summary);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
// M2a R2 drift-catcher: the canonical JSON Schema file at
|
||||
// src/schemas/restore-summary.schema.json and the embedded TS const
|
||||
// RESTORE_SUMMARY_SCHEMA must stay byte-equivalent. Embedding the schema
|
||||
// in the validator is the M2a R2 packaging fix (tsc emits the const into
|
||||
// dist/restore-packet/schema-validator.js, so the validator works after
|
||||
// raw `tsc` emit without any extra build-script copy step). The JSON file
|
||||
// remains the canonical source for downstream tooling that reads JSON
|
||||
// Schema directly. This test fails loudly if either form drifts.
|
||||
it("M2a R2 drift-catcher: TS const RESTORE_SUMMARY_SCHEMA equals the canonical JSON schema file", () => {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const schemaJsonPath = resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"schemas",
|
||||
"restore-summary.schema.json",
|
||||
);
|
||||
const jsonForm: unknown = JSON.parse(readFileSync(schemaJsonPath, "utf-8"));
|
||||
expect(RESTORE_SUMMARY_SCHEMA).toEqual(jsonForm);
|
||||
});
|
||||
});
|
||||
|
||||
describe("M2 restore-packet CLI command shell + mutual-exclusion", () => {
|
||||
|
||||
Reference in new issue
Block a user