legacymaxxing 0.1.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/dist/errors.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Typed error: a stable string `code` (asserted in tests) plus a numeric
3
+ * `exitCode` (what the OS sees). Conversion to a process exit happens ONLY at the
4
+ * top-level catch in cli.ts. Reserved exit codes:
5
+ * 1 runtime · 2 invalid usage/preconditions · 3 dirty worktree ·
6
+ * 4 provider auth/config · 5 provider quota · 6 validation/gate failed ·
7
+ * 7 lock conflict / external-CLI failure · 8 malformed model output.
8
+ */
9
+ export class ToolError extends Error {
10
+ exitCode;
11
+ code;
12
+ constructor(message, exitCode = 1, code = "runtime") {
13
+ super(message);
14
+ this.name = "ToolError";
15
+ this.exitCode = exitCode;
16
+ this.code = code;
17
+ }
18
+ }
19
+ export function errMessage(error) {
20
+ if (error instanceof Error)
21
+ return error.message;
22
+ return String(error);
23
+ }
package/dist/fs.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { ZodType } from "zod";
2
+ export declare function ensureDir(dir: string): Promise<void>;
3
+ /** Atomic write: tmp file + rename, so a crash never leaves a half-written record. */
4
+ export declare function writeJson(path: string, value: unknown): Promise<void>;
5
+ /** Read + schema-validate. Corrupt JSON and schema mismatch BOTH surface as a typed error. */
6
+ export declare function readJson<T>(path: string, schema: ZodType<T>): Promise<T>;
7
+ export declare function pathExists(path: string): Promise<boolean>;
8
+ export declare function isDirectory(path: string): Promise<boolean>;
package/dist/fs.js ADDED
@@ -0,0 +1,47 @@
1
+ import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { ToolError } from "./errors.js";
5
+ export async function ensureDir(dir) {
6
+ await mkdir(dir, { recursive: true });
7
+ }
8
+ /** Atomic write: tmp file + rename, so a crash never leaves a half-written record. */
9
+ export async function writeJson(path, value) {
10
+ await ensureDir(dirname(path));
11
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`;
12
+ await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
13
+ await rename(tmp, path);
14
+ }
15
+ /** Read + schema-validate. Corrupt JSON and schema mismatch BOTH surface as a typed error. */
16
+ export async function readJson(path, schema) {
17
+ const raw = await readFile(path, "utf8");
18
+ let parsed;
19
+ try {
20
+ parsed = JSON.parse(raw);
21
+ }
22
+ catch {
23
+ throw new ToolError(`corrupt state: ${path}`, 1, "malformed-state");
24
+ }
25
+ const result = schema.safeParse(parsed);
26
+ if (!result.success) {
27
+ throw new ToolError(`invalid state: ${path}`, 1, "malformed-state");
28
+ }
29
+ return result.data;
30
+ }
31
+ export async function pathExists(path) {
32
+ try {
33
+ await stat(path);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ export async function isDirectory(path) {
41
+ try {
42
+ return (await stat(path)).isDirectory();
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export declare function isGitRepo(root: string): Promise<boolean>;
2
+ export declare function headSha(root: string): Promise<string | null>;
3
+ /**
4
+ * Paths with uncommitted changes, excluding any under `excludePrefixes` (our own
5
+ * state dir and the analysis dir, which the agent legitimately writes).
6
+ */
7
+ export declare function worktreeDirtyPaths(root: string, excludePrefixes: string[]): Promise<string[]>;
8
+ /** Mutating phases require a clean git worktree so a bad rewrite is recoverable. */
9
+ export declare function assertCleanWorktree(root: string, excludePrefixes: string[]): Promise<void>;
package/dist/git.js ADDED
@@ -0,0 +1,60 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { ToolError } from "./errors.js";
4
+ const exec = promisify(execFile);
5
+ export async function isGitRepo(root) {
6
+ try {
7
+ await exec("git", ["-C", root, "rev-parse", "--is-inside-work-tree"]);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export async function headSha(root) {
15
+ try {
16
+ const { stdout } = await exec("git", ["-C", root, "rev-parse", "HEAD"]);
17
+ return stdout.trim();
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /**
24
+ * Paths with uncommitted changes, excluding any under `excludePrefixes` (our own
25
+ * state dir and the analysis dir, which the agent legitimately writes).
26
+ */
27
+ export async function worktreeDirtyPaths(root, excludePrefixes) {
28
+ let stdout = "";
29
+ try {
30
+ ({ stdout } = await exec("git", ["-C", root, "status", "--porcelain"]));
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ const dirty = [];
36
+ for (const line of stdout.split("\n")) {
37
+ if (line.length < 4)
38
+ continue;
39
+ let path = line.slice(3).trim();
40
+ const arrow = path.indexOf(" -> ");
41
+ if (arrow !== -1)
42
+ path = path.slice(arrow + 4);
43
+ if (path.length === 0)
44
+ continue;
45
+ if (excludePrefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))
46
+ continue;
47
+ dirty.push(path);
48
+ }
49
+ return dirty;
50
+ }
51
+ /** Mutating phases require a clean git worktree so a bad rewrite is recoverable. */
52
+ export async function assertCleanWorktree(root, excludePrefixes) {
53
+ if (!(await isGitRepo(root))) {
54
+ throw new ToolError("mutating phase requires a git repository", 3, "dirty-worktree");
55
+ }
56
+ const dirty = await worktreeDirtyPaths(root, excludePrefixes);
57
+ if (dirty.length > 0) {
58
+ throw new ToolError(`dirty worktree: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", ..." : ""}`, 3, "dirty-worktree");
59
+ }
60
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The modernization phase state-machine. PHASE_NAMES is a topological order:
3
+ * `next`/`loop` walk it left to right. Each phase declares its dependencies, its
4
+ * gate, whether it mutates, and the artifacts the agent is expected to write.
5
+ *
6
+ * Adding breadth (a new phase) is a single entry here plus a prompt brief; the
7
+ * command core never changes.
8
+ */
9
+ export declare const PHASE_NAMES: readonly ["assess", "map", "extract", "brief", "transform", "reimagine", "harden"];
10
+ export type PhaseName = (typeof PHASE_NAMES)[number];
11
+ export type ArtifactKind = "md" | "mermaid" | "json" | "html" | "dir" | "patch";
12
+ export type ArtifactSpec = {
13
+ /** Path relative to --root. `module` is only consulted by module-scoped phases. */
14
+ path: (system: string, module: string | null) => string;
15
+ kind: ArtifactKind;
16
+ required: boolean;
17
+ };
18
+ export type PhaseSpec = {
19
+ name: PhaseName;
20
+ /** Writes code/files outside analysis state → guarded behind --apply + clean worktree. */
21
+ mutating: boolean;
22
+ /** Produces, then waits for a human `gate` decision before it counts as done. */
23
+ gated: boolean;
24
+ /** Phases that must be done (validated or gate-approved) before this can run. */
25
+ dependsOn: PhaseName[];
26
+ /** Gated phases whose decision must be `approve` before this can run. */
27
+ requiresApprovedGate: PhaseName[];
28
+ needsModule: boolean;
29
+ needsVision: boolean;
30
+ artifacts: ArtifactSpec[];
31
+ };
32
+ export declare const PHASES: Record<PhaseName, PhaseSpec>;
33
+ export declare function isPhaseName(value: string): value is PhaseName;
package/dist/phases.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The modernization phase state-machine. PHASE_NAMES is a topological order:
3
+ * `next`/`loop` walk it left to right. Each phase declares its dependencies, its
4
+ * gate, whether it mutates, and the artifacts the agent is expected to write.
5
+ *
6
+ * Adding breadth (a new phase) is a single entry here plus a prompt brief; the
7
+ * command core never changes.
8
+ */
9
+ export const PHASE_NAMES = [
10
+ "assess",
11
+ "map",
12
+ "extract",
13
+ "brief",
14
+ "transform",
15
+ "reimagine",
16
+ "harden",
17
+ ];
18
+ const analysis = (file) => (system) => `analysis/${system}/${file}`;
19
+ export const PHASES = {
20
+ assess: {
21
+ name: "assess",
22
+ mutating: false,
23
+ gated: false,
24
+ dependsOn: [],
25
+ requiresApprovedGate: [],
26
+ needsModule: false,
27
+ needsVision: false,
28
+ artifacts: [
29
+ { path: analysis("ASSESSMENT.md"), kind: "md", required: true },
30
+ { path: analysis("ARCHITECTURE.mmd"), kind: "mermaid", required: true },
31
+ ],
32
+ },
33
+ map: {
34
+ name: "map",
35
+ mutating: false,
36
+ gated: false,
37
+ dependsOn: ["assess"],
38
+ requiresApprovedGate: [],
39
+ needsModule: false,
40
+ needsVision: false,
41
+ artifacts: [
42
+ { path: analysis("topology.json"), kind: "json", required: true },
43
+ { path: analysis("TOPOLOGY.html"), kind: "html", required: true },
44
+ { path: analysis("call-graph.mmd"), kind: "mermaid", required: false },
45
+ ],
46
+ },
47
+ extract: {
48
+ name: "extract",
49
+ mutating: false,
50
+ gated: false,
51
+ dependsOn: ["assess"],
52
+ requiresApprovedGate: [],
53
+ needsModule: false,
54
+ needsVision: false,
55
+ artifacts: [
56
+ { path: analysis("BUSINESS_RULES.md"), kind: "md", required: true },
57
+ { path: analysis("DATA_OBJECTS.md"), kind: "md", required: false },
58
+ ],
59
+ },
60
+ brief: {
61
+ name: "brief",
62
+ mutating: false,
63
+ gated: true,
64
+ dependsOn: ["assess", "map", "extract"],
65
+ requiresApprovedGate: [],
66
+ needsModule: false,
67
+ needsVision: false,
68
+ artifacts: [{ path: analysis("MODERNIZATION_BRIEF.md"), kind: "md", required: true }],
69
+ },
70
+ transform: {
71
+ name: "transform",
72
+ mutating: true,
73
+ gated: false,
74
+ dependsOn: ["brief"],
75
+ requiresApprovedGate: ["brief"],
76
+ needsModule: true,
77
+ needsVision: false,
78
+ artifacts: [
79
+ { path: (s, m) => `modernized/${s}/${m ?? "module"}`, kind: "dir", required: true },
80
+ { path: analysis("TRANSFORMATION_NOTES.md"), kind: "md", required: true },
81
+ ],
82
+ },
83
+ reimagine: {
84
+ name: "reimagine",
85
+ mutating: true,
86
+ gated: false,
87
+ dependsOn: ["brief"],
88
+ requiresApprovedGate: ["brief"],
89
+ needsModule: false,
90
+ needsVision: true,
91
+ artifacts: [
92
+ { path: (s) => `modernized/${s}-reimagined`, kind: "dir", required: true },
93
+ { path: analysis("AI_NATIVE_SPEC.md"), kind: "md", required: true },
94
+ ],
95
+ },
96
+ harden: {
97
+ name: "harden",
98
+ mutating: false,
99
+ gated: false,
100
+ dependsOn: ["assess"],
101
+ requiresApprovedGate: [],
102
+ needsModule: false,
103
+ needsVision: false,
104
+ artifacts: [
105
+ { path: analysis("SECURITY_FINDINGS.md"), kind: "md", required: true },
106
+ { path: analysis("security_remediation.patch"), kind: "patch", required: false },
107
+ ],
108
+ },
109
+ };
110
+ export function isPhaseName(value) {
111
+ return PHASE_NAMES.includes(value);
112
+ }
@@ -0,0 +1,11 @@
1
+ import { type PhaseName } from "./phases.js";
2
+ import type { StackSeed } from "./detectors/types.js";
3
+ export type PromptContext = {
4
+ system: string;
5
+ legacyPath: string;
6
+ seeds: StackSeed[];
7
+ module: string | null;
8
+ vision: string | null;
9
+ };
10
+ /** Deterministic prompt assembly. No model call happens here. */
11
+ export declare function buildPrompt(ctx: PromptContext, phase: PhaseName): string;
@@ -0,0 +1,38 @@
1
+ import { PHASES } from "./phases.js";
2
+ const PHASE_BRIEF = {
3
+ assess: "Inventory the legacy system: languages, size, build system, integrations, technical debt, security posture, and documentation gaps.",
4
+ map: "Build dependency and topology maps: call graph, data lineage, entry points, dead-end candidates, and the critical-path business flow.",
5
+ extract: "Mine embedded business rules into Given/When/Then rule cards, each with file:line citations and a confidence rating.",
6
+ brief: "Synthesize the discovery artifacts into an approval-gated modernization brief: target architecture, phased plan with entry/exit criteria, behavior contract, validation strategy, and open questions.",
7
+ transform: "Surgically rewrite ONE module (strangler-fig) with characterization tests proving behavior equivalence. Touch only the target module.",
8
+ reimagine: "Greenfield rebuild from the extracted intent (NOT a structural port), with executable acceptance tests and a knowledge handoff.",
9
+ harden: "Security-harden the LEGACY system in place-of-record only: OWASP/CWE scan, CVEs, secrets, injection. Produce a reviewable patch — DO NOT apply it.",
10
+ };
11
+ /** Deterministic prompt assembly. No model call happens here. */
12
+ export function buildPrompt(ctx, phase) {
13
+ const spec = PHASES[phase];
14
+ const artifactLines = spec.artifacts.map((artifact) => `- ${artifact.path(ctx.system, ctx.module)} (${artifact.kind}${artifact.required ? ", required" : ", optional"})`);
15
+ const seedLines = ctx.seeds.length > 0
16
+ ? ctx.seeds.map((seed) => `- ${seed.language}: ${seed.files} files`).join("\n")
17
+ : "- (no known languages detected by the deterministic scan)";
18
+ const sections = [
19
+ `# legacymaxxing phase: ${phase}`,
20
+ "",
21
+ PHASE_BRIEF[phase],
22
+ "",
23
+ "## Legacy system",
24
+ `Name: ${ctx.system}`,
25
+ `Read-only source root: ${ctx.legacyPath}`,
26
+ "Do NOT modify any file under the legacy source root.",
27
+ "",
28
+ "## Detected stacks (deterministic pre-scan)",
29
+ seedLines,
30
+ "",
31
+ ];
32
+ if (ctx.module)
33
+ sections.push("## Target module", ctx.module, "");
34
+ if (ctx.vision)
35
+ sections.push("## Target vision", ctx.vision, "");
36
+ sections.push("## Write exactly these artifacts", ...artifactLines, "", "## Final output", "After writing the artifacts, emit as the FINAL line a single JSON object (and nothing after it):", '{"summary": string, "artifacts": [{"path": string, "kind": "md|mermaid|json|html|dir|patch"}], "notes": string | null}');
37
+ return sections.join("\n");
38
+ }
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import type { PhaseName } from "./phases.js";
3
+ import { type PhaseOutput } from "./types.js";
4
+ export type ProviderOptions = {
5
+ model: string | null;
6
+ noInput: boolean;
7
+ };
8
+ export type RunPhaseArgs = {
9
+ root: string;
10
+ system: string;
11
+ phase: PhaseName;
12
+ prompt: string;
13
+ mutating: boolean;
14
+ module: string | null;
15
+ vision: string | null;
16
+ options: ProviderOptions;
17
+ };
18
+ export type Provider = {
19
+ name: string;
20
+ /** Verify the provider CLI is present + authed. Returns a human-readable detail. */
21
+ check(root: string): Promise<string>;
22
+ /** Run one phase. Returns raw `unknown` — parsed only at the boundary below. */
23
+ runPhase(args: RunPhaseArgs): Promise<unknown>;
24
+ };
25
+ export declare function formatZodError(error: z.ZodError): string;
26
+ /** THE single boundary. All model output is `unknown` until it passes here. */
27
+ export declare function parseOrThrow<T>(schema: z.ZodType<T>, input: unknown, label: string): T;
28
+ /**
29
+ * Partition a phase output: a fundamentally wrong container throws
30
+ * malformed-output (8); individual bad artifact items are dropped, not fatal.
31
+ */
32
+ export declare function partitionArtifacts(raw: unknown, label: string): {
33
+ output: PhaseOutput;
34
+ dropped: number;
35
+ };
36
+ export declare function providerByName(name: string): Provider;
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import { ToolError } from "./errors.js";
3
+ import { phaseOutputArtifactSchema } from "./types.js";
4
+ import { codexProvider } from "./providers/codex.js";
5
+ import { mockProvider } from "./providers/mock.js";
6
+ export function formatZodError(error) {
7
+ return error.issues
8
+ .map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
9
+ .join("; ");
10
+ }
11
+ /** THE single boundary. All model output is `unknown` until it passes here. */
12
+ export function parseOrThrow(schema, input, label) {
13
+ const result = schema.safeParse(input);
14
+ if (result.success)
15
+ return result.data;
16
+ throw new ToolError(`${label}: ${formatZodError(result.error)}`, 8, "malformed-output");
17
+ }
18
+ const containerSchema = z.object({
19
+ summary: z.string(),
20
+ artifacts: z.array(z.unknown()),
21
+ notes: z.string().nullable(),
22
+ });
23
+ /**
24
+ * Partition a phase output: a fundamentally wrong container throws
25
+ * malformed-output (8); individual bad artifact items are dropped, not fatal.
26
+ */
27
+ export function partitionArtifacts(raw, label) {
28
+ const container = parseOrThrow(containerSchema, raw, label);
29
+ const kept = [];
30
+ let dropped = 0;
31
+ for (const item of container.artifacts) {
32
+ const result = phaseOutputArtifactSchema.safeParse(item);
33
+ if (result.success)
34
+ kept.push(result.data);
35
+ else
36
+ dropped += 1;
37
+ }
38
+ return {
39
+ output: { summary: container.summary, artifacts: kept, notes: container.notes },
40
+ dropped,
41
+ };
42
+ }
43
+ export function providerByName(name) {
44
+ switch (name) {
45
+ case "codex":
46
+ return codexProvider;
47
+ case "mock":
48
+ return mockProvider;
49
+ default:
50
+ throw new ToolError(`unsupported provider: ${name}`, 2, "unsupported-provider");
51
+ }
52
+ }
@@ -0,0 +1,11 @@
1
+ import type { Provider } from "../provider.js";
2
+ /**
3
+ * The first real provider: shells out to the `codex` CLI. Read-only sandbox by
4
+ * default; only mutating phases get `workspace-write`. Auth/quota are detected
5
+ * from output and mapped to exit codes 4/5.
6
+ *
7
+ * NOTE: this path is not exercised in CI (the `mock` provider covers the
8
+ * pipeline). The exact `codex exec` flags may need tuning against your installed
9
+ * codex version — see README.
10
+ */
11
+ export declare const codexProvider: Provider;
@@ -0,0 +1,73 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { ToolError, errMessage } from "../errors.js";
4
+ const exec = promisify(execFile);
5
+ /**
6
+ * The first real provider: shells out to the `codex` CLI. Read-only sandbox by
7
+ * default; only mutating phases get `workspace-write`. Auth/quota are detected
8
+ * from output and mapped to exit codes 4/5.
9
+ *
10
+ * NOTE: this path is not exercised in CI (the `mock` provider covers the
11
+ * pipeline). The exact `codex exec` flags may need tuning against your installed
12
+ * codex version — see README.
13
+ */
14
+ export const codexProvider = {
15
+ name: "codex",
16
+ check: async (root) => {
17
+ try {
18
+ const { stdout } = await exec("codex", ["--version"], { cwd: root });
19
+ return stdout.trim() || "codex available";
20
+ }
21
+ catch (error) {
22
+ throw new ToolError(`codex CLI not found or not runnable: ${errMessage(error)}`, 4, "provider-auth");
23
+ }
24
+ },
25
+ runPhase: async (args) => {
26
+ const sandbox = args.mutating ? "workspace-write" : "read-only";
27
+ const cmdArgs = ["exec", "--json", "--sandbox", sandbox];
28
+ if (args.options.model)
29
+ cmdArgs.push("--model", args.options.model);
30
+ cmdArgs.push(args.prompt);
31
+ let stdout = "";
32
+ try {
33
+ ({ stdout } = await exec("codex", cmdArgs, {
34
+ cwd: args.root,
35
+ maxBuffer: 64 * 1024 * 1024,
36
+ }));
37
+ }
38
+ catch (error) {
39
+ const text = errMessage(error);
40
+ if (/auth|login|unauthor/iu.test(text)) {
41
+ throw new ToolError(`codex auth failure: ${text}`, 4, "provider-auth");
42
+ }
43
+ if (/quota|rate.?limit|\b429\b/iu.test(text)) {
44
+ throw new ToolError(`codex quota/rate-limit: ${text}`, 5, "provider-quota");
45
+ }
46
+ throw new ToolError(`codex invocation failed: ${text}`, 7, "external-cli");
47
+ }
48
+ return extractManifest(stdout);
49
+ },
50
+ };
51
+ /** Find the last line that parses as a JSON object — the phase output manifest. */
52
+ function extractManifest(stdout) {
53
+ const trimmed = stdout.trim();
54
+ try {
55
+ return JSON.parse(trimmed);
56
+ }
57
+ catch {
58
+ // fall through to line scan
59
+ }
60
+ const lines = trimmed.split("\n");
61
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
62
+ const line = lines[index]?.trim();
63
+ if (!line || !line.startsWith("{"))
64
+ continue;
65
+ try {
66
+ return JSON.parse(line);
67
+ }
68
+ catch {
69
+ // keep scanning earlier lines
70
+ }
71
+ }
72
+ throw new ToolError("codex produced no parseable JSON manifest", 8, "malformed-output");
73
+ }
@@ -0,0 +1,8 @@
1
+ import type { Provider } from "../provider.js";
2
+ /**
3
+ * Deterministic, network-free provider. It "does the agent's work" by writing a
4
+ * minimal-but-valid artifact for every expected output of the phase, then
5
+ * returning a well-formed manifest — so the whole pipeline (assemble → run →
6
+ * boundary parse → validate → record) is testable end to end.
7
+ */
8
+ export declare const mockProvider: Provider;
@@ -0,0 +1,52 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { PHASES } from "../phases.js";
4
+ /**
5
+ * Deterministic, network-free provider. It "does the agent's work" by writing a
6
+ * minimal-but-valid artifact for every expected output of the phase, then
7
+ * returning a well-formed manifest — so the whole pipeline (assemble → run →
8
+ * boundary parse → validate → record) is testable end to end.
9
+ */
10
+ export const mockProvider = {
11
+ name: "mock",
12
+ check: async () => "mock provider ready",
13
+ runPhase: async (args) => {
14
+ const spec = PHASES[args.phase];
15
+ const artifacts = [];
16
+ for (const artifact of spec.artifacts) {
17
+ const rel = artifact.path(args.system, args.module);
18
+ await writeArtifact(args.root, rel, artifact.kind, args);
19
+ artifacts.push({ path: rel, kind: artifact.kind });
20
+ }
21
+ return {
22
+ summary: `mock ${args.phase} for ${args.system}`,
23
+ artifacts,
24
+ notes: null,
25
+ };
26
+ },
27
+ };
28
+ async function writeArtifact(root, rel, kind, args) {
29
+ const abs = join(root, rel);
30
+ if (kind === "dir") {
31
+ await mkdir(abs, { recursive: true });
32
+ await writeFile(join(abs, "README.md"), `# ${args.system} (mock ${args.phase})\n`, "utf8");
33
+ await writeFile(join(abs, "acceptance.test.txt"), "mock characterization test\n", "utf8");
34
+ return;
35
+ }
36
+ await mkdir(dirname(abs), { recursive: true });
37
+ await writeFile(abs, bodyFor(kind, args), "utf8");
38
+ }
39
+ function bodyFor(kind, args) {
40
+ switch (kind) {
41
+ case "json":
42
+ return `${JSON.stringify({ system: args.system, phase: args.phase, mock: true }, null, 2)}\n`;
43
+ case "mermaid":
44
+ return "graph TD;\n Legacy-->Modern;\n";
45
+ case "html":
46
+ return `<!doctype html><title>${args.system} ${args.phase}</title><body>mock</body>\n`;
47
+ case "patch":
48
+ return "--- a/legacy\n+++ b/legacy\n@@ -0,0 +1 @@\n+mock remediation (review before applying)\n";
49
+ default:
50
+ return `# ${args.system} — ${args.phase}\n\nGenerated by the mock provider.\n`;
51
+ }
52
+ }
@@ -0,0 +1,16 @@
1
+ export type StatePaths = {
2
+ stateDir: string;
3
+ config: string;
4
+ project: string;
5
+ phases: string;
6
+ runs: string;
7
+ locks: string;
8
+ };
9
+ export declare function statePaths(stateDir: string): StatePaths;
10
+ export declare function phaseRecordPath(stateDir: string, system: string, phase: string): string;
11
+ export declare function runRecordPath(stateDir: string, runId: string): string;
12
+ export declare function lockPath(stateDir: string, system: string, phase: string): string;
13
+ /** Exclusive-create lock; EEXIST → lock-conflict (7). Enables future --jobs N. */
14
+ export declare function claim(path: string, runId: string): Promise<void>;
15
+ export declare function release(path: string): Promise<void>;
16
+ export declare function listLocks(stateDir: string): Promise<string[]>;
package/dist/state.js ADDED
@@ -0,0 +1,54 @@
1
+ import { open, readdir, rm } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { ToolError } from "./errors.js";
4
+ import { ensureDir } from "./fs.js";
5
+ export function statePaths(stateDir) {
6
+ return {
7
+ stateDir,
8
+ config: join(stateDir, "config.json"),
9
+ project: join(stateDir, "project.json"),
10
+ phases: join(stateDir, "phases"),
11
+ runs: join(stateDir, "runs"),
12
+ locks: join(stateDir, "locks"),
13
+ };
14
+ }
15
+ export function phaseRecordPath(stateDir, system, phase) {
16
+ return join(stateDir, "phases", system, `${phase}.json`);
17
+ }
18
+ export function runRecordPath(stateDir, runId) {
19
+ return join(stateDir, "runs", `${runId}.json`);
20
+ }
21
+ export function lockPath(stateDir, system, phase) {
22
+ return join(stateDir, "locks", `${system}-${phase}.json`);
23
+ }
24
+ /** Exclusive-create lock; EEXIST → lock-conflict (7). Enables future --jobs N. */
25
+ export async function claim(path, runId) {
26
+ await ensureDir(dirname(path));
27
+ try {
28
+ const fd = await open(path, "wx");
29
+ await fd.writeFile(JSON.stringify({
30
+ lockedByRunId: runId,
31
+ lockedAt: new Date().toISOString(),
32
+ pid: process.pid,
33
+ }));
34
+ await fd.close();
35
+ }
36
+ catch (error) {
37
+ if (error.code === "EEXIST") {
38
+ throw new ToolError(`phase locked: ${path}`, 7, "lock-conflict");
39
+ }
40
+ throw error;
41
+ }
42
+ }
43
+ export async function release(path) {
44
+ await rm(path, { force: true });
45
+ }
46
+ export async function listLocks(stateDir) {
47
+ try {
48
+ const entries = await readdir(join(stateDir, "locks"));
49
+ return entries.filter((name) => name.endsWith(".json"));
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ }