397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
187 lines
6.3 KiB
JavaScript
187 lines
6.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// tools/test-rollback.mjs — Rollback planner + executor for the LLM-first router overhaul.
|
|
//
|
|
// Plan: docs/superpowers/plans/2026-05-25-llm-first-router-overhaul.md Task 1.
|
|
// Spec: docs/superpowers/specs/2026-05-24-llm-first-router-overhaul-design.md §13 (rollback).
|
|
//
|
|
// Two responsibilities:
|
|
// 1. planRollback() — pure, returns a description of what rollback does (testable)
|
|
// 2. dryRun() / execRollback() — CLI entry points
|
|
//
|
|
// Safety:
|
|
// - execFileSync (no shell, no command injection)
|
|
// - Entry-point guard uses resolve() (Windows + Cyrillic paths safe, per quirk #103)
|
|
// - episodes-*.jsonl and observer/notes/* are PRESERVED, never reverted (G5/G6)
|
|
// - Parser stays forward-compatible to schema v4 after rollback (G5, Task 15)
|
|
|
|
import {
|
|
existsSync,
|
|
copyFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
mkdirSync,
|
|
statSync,
|
|
} from 'node:fs';
|
|
import { join, resolve } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ARCHIVE = 'docs/archive/llm-bootstrap-2026-05';
|
|
|
|
/**
|
|
* Pure description of the rollback plan.
|
|
* Used by tools/test-rollback.test.mjs and as the source of truth for the CLI.
|
|
*/
|
|
export function planRollback() {
|
|
return {
|
|
gitTag: 'brain-pre-llm-bootstrap',
|
|
gitStrategy: 'git checkout brain-pre-llm-bootstrap -- <tracked paths>',
|
|
userLevelRestores: [
|
|
{
|
|
from: `${ARCHIVE}/settings-snapshot/user-settings.json.pre-overhaul`,
|
|
to: '~/.claude/settings.json',
|
|
},
|
|
{ from: `${ARCHIVE}/user-hooks/*`, to: '~/.claude/hooks/' },
|
|
],
|
|
flagStrategy: 'restore-snapshot-delete-new',
|
|
preserve: [
|
|
'docs/observer/episodes-*.jsonl',
|
|
'docs/observer/notes/*',
|
|
],
|
|
parserNote:
|
|
'после отката parser остаётся forward-compatible к v4 эпизодам (read-only graceful skip) — Task 15 (G5)',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Dry-run: verify rollback artefacts exist and surface missing ones.
|
|
* Returns true if rollback is ready, false otherwise.
|
|
*/
|
|
export function dryRun() {
|
|
const plan = planRollback();
|
|
let ok = true;
|
|
|
|
const baseSnap = `${ARCHIVE}/settings-snapshot/user-settings.json.pre-overhaul`;
|
|
if (!existsSync(baseSnap)) {
|
|
console.error('MISSING snapshot:', baseSnap);
|
|
ok = false;
|
|
}
|
|
const projSnap = `${ARCHIVE}/settings-snapshot/project-settings.json.pre-overhaul`;
|
|
if (!existsSync(projSnap)) {
|
|
console.error('MISSING snapshot:', projSnap);
|
|
ok = false;
|
|
}
|
|
const hooksDir = `${ARCHIVE}/user-hooks`;
|
|
if (!existsSync(hooksDir) || readdirSync(hooksDir).length === 0) {
|
|
console.error('MISSING or empty hooks snapshot:', hooksDir);
|
|
ok = false;
|
|
}
|
|
const nodesSnap = `${ARCHIVE}/nodes-yaml-archive/nodes.yaml.pre-overhaul`;
|
|
if (!existsSync(nodesSnap)) {
|
|
console.error('MISSING snapshot:', nodesSnap);
|
|
ok = false;
|
|
}
|
|
|
|
try {
|
|
execFileSync('git', ['rev-parse', plan.gitTag], { stdio: 'pipe' });
|
|
} catch {
|
|
console.error('MISSING git tag:', plan.gitTag);
|
|
ok = false;
|
|
}
|
|
|
|
console.log(ok ? '[dry-run] OK — rollback ready' : '[dry-run] FAIL — see above');
|
|
return ok;
|
|
}
|
|
|
|
/**
|
|
* Execute rollback of user-level state + runtime flags.
|
|
* Git-tracked rollback is left to the operator (separate manual step in ROLLBACK.md)
|
|
* to keep destructive `git checkout` explicit.
|
|
*/
|
|
export function execRollback() {
|
|
const home = homedir();
|
|
|
|
// 1. user settings.json
|
|
const usFrom = `${ARCHIVE}/settings-snapshot/user-settings.json.pre-overhaul`;
|
|
if (existsSync(usFrom)) {
|
|
copyFileSync(usFrom, join(home, '.claude', 'settings.json'));
|
|
console.log('[execute] restored ~/.claude/settings.json');
|
|
} else {
|
|
console.error('[execute] SKIP user settings — snapshot missing');
|
|
}
|
|
|
|
// 2. user hooks (full directory restore — wipe new hooks, restore snapshot)
|
|
const hooksSrc = `${ARCHIVE}/user-hooks`;
|
|
const hooksDst = join(home, '.claude', 'hooks');
|
|
if (existsSync(hooksSrc)) {
|
|
if (!existsSync(hooksDst)) mkdirSync(hooksDst, { recursive: true });
|
|
// wipe current
|
|
for (const f of readdirSync(hooksDst)) {
|
|
const fp = join(hooksDst, f);
|
|
if (statSync(fp).isFile()) rmSync(fp);
|
|
}
|
|
// restore snapshot
|
|
let count = 0;
|
|
for (const f of readdirSync(hooksSrc)) {
|
|
const sp = join(hooksSrc, f);
|
|
if (statSync(sp).isFile()) {
|
|
copyFileSync(sp, join(hooksDst, f));
|
|
count++;
|
|
}
|
|
}
|
|
console.log(`[execute] restored ~/.claude/hooks/ (${count} files)`);
|
|
} else {
|
|
console.error('[execute] SKIP user hooks — snapshot missing');
|
|
}
|
|
|
|
// 3. runtime flags: delete *-mode.json files not present in snapshot, restore snapshot files
|
|
const runtimeDir = join(home, '.claude', 'runtime');
|
|
const snapDir = `${ARCHIVE}/runtime-flags-snapshot`;
|
|
if (existsSync(runtimeDir)) {
|
|
const snapFlags = existsSync(snapDir) ? readdirSync(snapDir) : [];
|
|
let deleted = 0;
|
|
for (const f of readdirSync(runtimeDir).filter((x) => x.endsWith('-mode.json'))) {
|
|
if (!snapFlags.includes(f)) {
|
|
rmSync(join(runtimeDir, f));
|
|
deleted++;
|
|
}
|
|
}
|
|
let restored = 0;
|
|
for (const f of snapFlags) {
|
|
copyFileSync(join(snapDir, f), join(runtimeDir, f));
|
|
restored++;
|
|
}
|
|
console.log(
|
|
`[execute] runtime flags: deleted ${deleted} new, restored ${restored} from snapshot`,
|
|
);
|
|
} else {
|
|
console.error('[execute] SKIP runtime flags — ~/.claude/runtime/ missing');
|
|
}
|
|
|
|
console.log(
|
|
'[execute] user-level + flags restored. ' +
|
|
'Now run: git checkout brain-pre-llm-bootstrap -- . && npm install',
|
|
);
|
|
}
|
|
|
|
// Entry-point guard — Cyrillic-safe (quirk #103: import.meta.url === argv[1] fails on RU paths).
|
|
const argv1 = process.argv[1] ? resolve(process.argv[1]) : '';
|
|
const here = fileURLToPath(import.meta.url);
|
|
const isMain = argv1 && argv1 === here;
|
|
|
|
if (isMain) {
|
|
const mode = process.argv[2];
|
|
if (mode === '--dry-run') {
|
|
process.exit(dryRun() ? 0 : 1);
|
|
} else if (mode === '--execute') {
|
|
execRollback();
|
|
} else {
|
|
console.log('usage: node tools/test-rollback.mjs --dry-run | --execute');
|
|
console.log('');
|
|
console.log(' --dry-run verify rollback artefacts are in place; exit 0 if ready');
|
|
console.log(' --execute restore user-level state + runtime flags from snapshot');
|
|
console.log(' (run "git checkout brain-pre-llm-bootstrap -- ." separately)');
|
|
}
|
|
}
|