Skip to content

Commit b127169

Browse files
committed
feat(state-space): close bounded cache/flag matrix oracles for #688
Add deterministic state-space fixtures for stale-cache and flag-interaction UATs, with coverage accounting, provider gates, CLI/MCP, and docs.
1 parent b6422fd commit b127169

31 files changed

Lines changed: 1558 additions & 4 deletions

docs/state-space.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# State-space safety
2+
3+
CodeDecay evaluates **bounded cache/feature-flag state matrices** from fixture
4+
experiments. It does not flush production caches or contact remote flag
5+
providers without explicit configuration.
6+
7+
## What it can establish
8+
9+
- State dimensions: flags, config, cache state/version, tenant, cohort, revision
10+
- Bounded pairwise or explicit combinations with coverage accounting
11+
- Cold/warm/stale cache comparisons and flag-interaction oracles
12+
- Distinction between confirmed regression, passed oracle, provider-blocked,
13+
bounds-blocked, and untested/pruned combinations
14+
- Repair tasks that attach a durable regression test id after a confirmed defect
15+
16+
## What it cannot establish
17+
18+
- Exhaustive coverage of the full state space
19+
- Production cache/flag behavior
20+
- A `fullyVerified: true` result (always false in this slice)
21+
22+
## CLI / MCP
23+
24+
```bash
25+
codedecay state-space --experiment experiment.json --surface src/cache/profile.ts
26+
codedecay state-space --experiment experiment.json --target-kind fixture-local --format json
27+
```
28+
29+
MCP tool: `state_space_safety`.

packages/cli/src/commands/registry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { runMcpCommand as runMcpCommandWithDependencies } from "./mcp";
1919
import { runMigrationCommand as runMigrationCommandWithDependencies } from "./migration";
2020
import { runConcurrencyCommand as runConcurrencyCommandWithDependencies } from "./concurrency";
21+
import { runStateSpaceCommand as runStateSpaceCommandWithDependencies } from "./state-space";
2122
import { runProductCommand as runProductCommandWithDependencies } from "./product";
2223
import { runRedteamCommand as runRedteamCommandWithDependencies } from "./redteam";
2324
import { runRevalidateCommand as runRevalidateCommandWithDependencies } from "./revalidate";
@@ -104,6 +105,10 @@ export function createCommandHandlers(options: CommandRegistryOptions): Record<s
104105
resolveRepoRoot: getRepoRootForCli,
105106
writeOutput: writeCliOutput
106107
}),
108+
"state-space": (context) => runStateSpaceCommandWithDependencies(context, {
109+
resolveRepoRoot: getRepoRootForCli,
110+
writeOutput: writeCliOutput
111+
}),
107112
memory: (context) => runMemoryCommandWithDependencies(context, { resolveRepoRoot: getRepoRootForCli }),
108113
"memory-import": (context) => runMemoryImportCommandWithDependencies(context, { resolveRepoRoot: getRepoRootForCli }),
109114
"memory-learn": (context) => runMemoryLearnCommandWithDependencies(context, { resolveRepoRoot: getRepoRootForCli }),
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { resolve } from "node:path";
2+
import { analyzeStateSpaceSafety, renderStateSpaceSafetyMarkdown } from "@submuxhq/codedecay-knowledge";
3+
import { parseStateSpaceArgs } from "../parsers/args";
4+
import type { CliCommandContext, CliRuntime, StateSpaceOptions } from "../types";
5+
6+
export interface RunStateSpaceCommandDependencies {
7+
resolveRepoRoot(cwd: string, options: StateSpaceOptions): string;
8+
writeOutput(input: { cwd: string; output?: string | undefined; rendered: string; runtime: CliRuntime }): void;
9+
}
10+
11+
export function runStateSpaceCommand(context: CliCommandContext, dependencies: RunStateSpaceCommandDependencies): void {
12+
const options = parseStateSpaceArgs(context.args);
13+
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
14+
const rootDir = dependencies.resolveRepoRoot(cwd, options);
15+
const report = analyzeStateSpaceSafety({
16+
rootDir,
17+
experimentFile: options.experimentFile,
18+
surfaceFiles: options.surfaceFiles,
19+
targetKind: options.targetKind,
20+
cleanupPlan: options.cleanupPlan
21+
});
22+
const rendered = options.format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderStateSpaceSafetyMarkdown(report);
23+
dependencies.writeOutput({ cwd: rootDir, output: options.output, rendered, runtime: context.runtime });
24+
}

packages/cli/src/docs/command-docs/analysis.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,31 @@ export const ANALYSIS_COMMAND_DOCS: Record<string, CommandDoc> = {
5353
"Stress-only results stay inconclusive. See docs/concurrency.md."
5454
]
5555
},
56+
"state-space": {
57+
name: "state-space",
58+
summary: "Plan and evaluate bounded cache/feature-flag state matrices.",
59+
usage: ["codedecay state-space [options]"],
60+
description: [
61+
"Load a seeded state-space experiment fixture, detect cache/flag candidates, generate bounded pairwise or explicit combinations, and evaluate stale-cache / flag-interaction oracles without contacting remote providers by default."
62+
],
63+
options: [
64+
{ flag: "--experiment <path>", description: "Repo-local state-space experiment JSON fixture" },
65+
{ flag: "--surface <path>", description: "Source file to scan for state dimensions; repeatable" },
66+
{ flag: "--target-kind <kind>", description: "fixture-local | disposable-local | remote-unapproved | production-like | unspecified" },
67+
{ flag: "--cleanup-plan <text>", description: "Disposable target cleanup plan" },
68+
{ flag: "--cwd <path>", description: "Working directory" },
69+
{ flag: "--format <json|markdown>", description: "Output format" },
70+
{ flag: "--output <path>", description: "Write report to a file" }
71+
],
72+
examples: [
73+
"codedecay state-space --experiment .codedecay/state-space/stale-cache.json --surface src/cache/profile.ts",
74+
"codedecay state-space --experiment experiment.json --target-kind fixture-local --format json"
75+
],
76+
notes: [
77+
"Coverage is bounded and never implies exhaustive proof. See docs/state-space.md.",
78+
"Remote flag providers stay blocked unless explicitly configured."
79+
]
80+
},
5681
runtime: {
5782
name: "runtime",
5883
summary: "Ingest local runtime exports as redacted engineering evidence.",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export const COMMAND_ORDER = ["ai", "session", "context", "analyze", "runtime", "migration", "concurrency", "topology", "benchmark", "snapshot", "redteam", "revalidate", "llm-review", "agent", "loop", "doctor", "config", "memory", "memory-import", "memory-learn", "execute", "differential", "product", "dashboard", "mcp"] as const;
1+
export const COMMAND_ORDER = ["ai", "session", "context", "analyze", "runtime", "migration", "concurrency", "state-space", "topology", "benchmark", "snapshot", "redteam", "revalidate", "llm-review", "agent", "loop", "doctor", "config", "memory", "memory-import", "memory-learn", "execute", "differential", "product", "dashboard", "mcp"] as const;
22
export const UTILITY_COMMAND_ORDER = ["help", "man", "update", "uninstall", "version"] as const;
33
export const ROOT_FLAG_ALIASES = ["--help", "-h", "--version", "-V"] as const;

packages/cli/src/parsers/args.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { parseMcpArgs } from "./mcp";
1414
export { parseMemoryArgs, parseMemoryImportArgs, parseMemoryLearnArgs, parseMemoryLearningArgs, parseMemorySetupArgs } from "./memory";
1515
export { parseMigrationArgs } from "./migration";
1616
export { parseConcurrencyArgs } from "./concurrency";
17+
export { parseStateSpaceArgs } from "./state-space";
1718
export { parseRevalidateArgs } from "./revalidate";
1819
export { parseRuntimeArgs } from "./runtime";
1920
export { parseProductArgs } from "./product";
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { StateSpaceOptions } from "../types";
2+
import { requireValue } from "./primitives";
3+
import { HelpRequested, throwUnknownOption } from "./shared";
4+
5+
export function parseStateSpaceArgs(args: string[]): StateSpaceOptions {
6+
const options: StateSpaceOptions = { surfaceFiles: [], format: "markdown" };
7+
for (let index = 0; index < args.length; index += 1) {
8+
const arg = args[index];
9+
if (!arg) continue;
10+
if (arg === "--help" || arg === "-h") throw new HelpRequested();
11+
const [flag, inline] = splitArg(arg);
12+
const value = () => inline ?? requireValue(args, index, flag);
13+
if (flag === "--experiment") options.experimentFile = value();
14+
else if (flag === "--surface") options.surfaceFiles.push(value());
15+
else if (flag === "--cwd") options.cwd = value();
16+
else if (flag === "--output") options.output = value();
17+
else if (flag === "--format") options.format = parseFormat(value());
18+
else if (flag === "--target-kind") options.targetKind = parseTarget(value());
19+
else if (flag === "--cleanup-plan") options.cleanupPlan = value();
20+
else {
21+
throwUnknownOption(arg, "state-space");
22+
continue;
23+
}
24+
if (inline === undefined) index += 1;
25+
}
26+
return options;
27+
}
28+
29+
function splitArg(arg: string): [string, string | undefined] {
30+
const index = arg.indexOf("=");
31+
return index < 0 ? [arg, undefined] : [arg.slice(0, index), arg.slice(index + 1)];
32+
}
33+
34+
function parseFormat(value: string): StateSpaceOptions["format"] {
35+
if (value === "json" || value === "markdown") return value;
36+
throw new Error(`Invalid state-space format "${value}". Expected json or markdown.`);
37+
}
38+
39+
function parseTarget(value: string): NonNullable<StateSpaceOptions["targetKind"]> {
40+
if (
41+
value === "unspecified" ||
42+
value === "fixture-local" ||
43+
value === "disposable-local" ||
44+
value === "remote-unapproved" ||
45+
value === "production-like"
46+
) {
47+
return value;
48+
}
49+
throw new Error(`Invalid state-space target kind "${value}".`);
50+
}

packages/cli/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export * from "./llm-review";
1313
export * from "./loop";
1414
export * from "./maintenance";
1515
export * from "./concurrency";
16+
export * from "./state-space";
1617
export * from "./migration";
1718
export * from "./mcp";
1819
export * from "./memory";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { ConfigFormat } from "./common";
2+
import type { StateSpaceTargetKind } from "@submuxhq/codedecay-knowledge";
3+
4+
export interface StateSpaceOptions {
5+
cwd?: string | undefined;
6+
experimentFile?: string | undefined;
7+
surfaceFiles: string[];
8+
targetKind?: StateSpaceTargetKind | undefined;
9+
cleanupPlan?: string | undefined;
10+
format: ConfigFormat;
11+
output?: string | undefined;
12+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { execFileSync } from "node:child_process";
2+
import { copyFileSync, mkdirSync, readFileSync, rmSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { dirname, join } from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
import { afterEach, describe, expect, it } from "vitest";
7+
import { runCli } from "../src/index";
8+
9+
const fixtures = join(dirname(fileURLToPath(import.meta.url)), "../../knowledge/test/fixtures/state-space");
10+
const roots: string[] = [];
11+
afterEach(() => {
12+
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
13+
});
14+
15+
describe("codedecay state-space CLI", () => {
16+
it("evaluates a stale-cache fixture in a child repository", async () => {
17+
const root = createRepo();
18+
mkdirSync(join(root, "experiments"), { recursive: true });
19+
copyFileSync(join(fixtures, "stale-cache.json"), join(root, "experiments", "stale-cache.json"));
20+
const result = await run([
21+
"state-space",
22+
"--cwd",
23+
root,
24+
"--experiment",
25+
"experiments/stale-cache.json",
26+
"--format",
27+
"json",
28+
"--output",
29+
"reports/state-space.json"
30+
]);
31+
const report = JSON.parse(readFileSync(join(root, "reports", "state-space.json"), "utf8")) as {
32+
verdict: string;
33+
fullyVerified: boolean;
34+
coverage: { exhaustive: boolean };
35+
};
36+
expect(result).toEqual({ exitCode: 0, stdout: "", stderr: "" });
37+
expect(report.verdict).toBe("confirmed-regression");
38+
expect(report.fullyVerified).toBe(false);
39+
expect(report.coverage.exhaustive).toBe(false);
40+
});
41+
42+
it("exposes state-space help", async () => {
43+
const result = await run(["state-space", "--help"]);
44+
expect(result.exitCode).toBe(0);
45+
expect(result.stdout).toContain("CodeDecay state-space");
46+
});
47+
});
48+
49+
async function run(args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
50+
let stdout = "";
51+
let stderr = "";
52+
const exitCode = await runCli(args, {
53+
stdout: (text) => {
54+
stdout += text;
55+
},
56+
stderr: (text) => {
57+
stderr += text;
58+
}
59+
});
60+
return { exitCode, stdout, stderr };
61+
}
62+
63+
function createRepo(): string {
64+
const root = join(tmpdir(), `codedecay-state-space-cli-${Date.now()}-${Math.random().toString(16).slice(2)}`);
65+
mkdirSync(root, { recursive: true });
66+
execFileSync("git", ["init", "-q"], { cwd: root });
67+
roots.push(root);
68+
return root;
69+
}

0 commit comments

Comments
 (0)