Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/state-space.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# State-space safety

CodeDecay evaluates **bounded cache/feature-flag state matrices** from fixture
experiments. It does not flush production caches or contact remote flag
providers without explicit configuration.

## What it can establish

- State dimensions: flags, config, cache state/version, tenant, cohort, revision
- Bounded pairwise or explicit combinations with coverage accounting
- Cold/warm/stale cache comparisons and flag-interaction oracles
- Distinction between confirmed regression, passed oracle, provider-blocked,
bounds-blocked, and untested/pruned combinations
- Repair tasks that attach a durable regression test id after a confirmed defect

## What it cannot establish

- Exhaustive coverage of the full state space
- Production cache/flag behavior
- A `fullyVerified: true` result (always false in this slice)

## CLI / MCP

```bash
codedecay state-space --experiment experiment.json --surface src/cache/profile.ts
codedecay state-space --experiment experiment.json --target-kind fixture-local --format json
```

MCP tool: `state_space_safety`.
5 changes: 5 additions & 0 deletions packages/cli/src/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { runMcpCommand as runMcpCommandWithDependencies } from "./mcp";
import { runMigrationCommand as runMigrationCommandWithDependencies } from "./migration";
import { runConcurrencyCommand as runConcurrencyCommandWithDependencies } from "./concurrency";
import { runStateSpaceCommand as runStateSpaceCommandWithDependencies } from "./state-space";
import { runProductCommand as runProductCommandWithDependencies } from "./product";
import { runRedteamCommand as runRedteamCommandWithDependencies } from "./redteam";
import { runRevalidateCommand as runRevalidateCommandWithDependencies } from "./revalidate";
Expand Down Expand Up @@ -104,6 +105,10 @@ export function createCommandHandlers(options: CommandRegistryOptions): Record<s
resolveRepoRoot: getRepoRootForCli,
writeOutput: writeCliOutput
}),
"state-space": (context) => runStateSpaceCommandWithDependencies(context, {
resolveRepoRoot: getRepoRootForCli,
writeOutput: writeCliOutput
}),
memory: (context) => runMemoryCommandWithDependencies(context, { resolveRepoRoot: getRepoRootForCli }),
"memory-import": (context) => runMemoryImportCommandWithDependencies(context, { resolveRepoRoot: getRepoRootForCli }),
"memory-learn": (context) => runMemoryLearnCommandWithDependencies(context, { resolveRepoRoot: getRepoRootForCli }),
Expand Down
24 changes: 24 additions & 0 deletions packages/cli/src/commands/state-space.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { resolve } from "node:path";
import { analyzeStateSpaceSafety, renderStateSpaceSafetyMarkdown } from "@submuxhq/codedecay-knowledge";
import { parseStateSpaceArgs } from "../parsers/args";
import type { CliCommandContext, CliRuntime, StateSpaceOptions } from "../types";

export interface RunStateSpaceCommandDependencies {
resolveRepoRoot(cwd: string, options: StateSpaceOptions): string;
writeOutput(input: { cwd: string; output?: string | undefined; rendered: string; runtime: CliRuntime }): void;
}

export function runStateSpaceCommand(context: CliCommandContext, dependencies: RunStateSpaceCommandDependencies): void {
const options = parseStateSpaceArgs(context.args);
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
const rootDir = dependencies.resolveRepoRoot(cwd, options);
const report = analyzeStateSpaceSafety({
rootDir,
experimentFile: options.experimentFile,
surfaceFiles: options.surfaceFiles,
targetKind: options.targetKind,
cleanupPlan: options.cleanupPlan
});
const rendered = options.format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderStateSpaceSafetyMarkdown(report);
dependencies.writeOutput({ cwd: rootDir, output: options.output, rendered, runtime: context.runtime });
}
25 changes: 25 additions & 0 deletions packages/cli/src/docs/command-docs/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ export const ANALYSIS_COMMAND_DOCS: Record<string, CommandDoc> = {
"Stress-only results stay inconclusive. See docs/concurrency.md."
]
},
"state-space": {
name: "state-space",
summary: "Plan and evaluate bounded cache/feature-flag state matrices.",
usage: ["codedecay state-space [options]"],
description: [
"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."
],
options: [
{ flag: "--experiment <path>", description: "Repo-local state-space experiment JSON fixture" },
{ flag: "--surface <path>", description: "Source file to scan for state dimensions; repeatable" },
{ flag: "--target-kind <kind>", description: "fixture-local | disposable-local | remote-unapproved | production-like | unspecified" },
{ flag: "--cleanup-plan <text>", description: "Disposable target cleanup plan" },
{ flag: "--cwd <path>", description: "Working directory" },
{ flag: "--format <json|markdown>", description: "Output format" },
{ flag: "--output <path>", description: "Write report to a file" }
],
examples: [
"codedecay state-space --experiment .codedecay/state-space/stale-cache.json --surface src/cache/profile.ts",
"codedecay state-space --experiment experiment.json --target-kind fixture-local --format json"
],
notes: [
"Coverage is bounded and never implies exhaustive proof. See docs/state-space.md.",
"Remote flag providers stay blocked unless explicitly configured."
]
},
runtime: {
name: "runtime",
summary: "Ingest local runtime exports as redacted engineering evidence.",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/docs/command-docs/order.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
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;
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;
export const UTILITY_COMMAND_ORDER = ["help", "man", "update", "uninstall", "version"] as const;
export const ROOT_FLAG_ALIASES = ["--help", "-h", "--version", "-V"] as const;
1 change: 1 addition & 0 deletions packages/cli/src/parsers/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { parseMcpArgs } from "./mcp";
export { parseMemoryArgs, parseMemoryImportArgs, parseMemoryLearnArgs, parseMemoryLearningArgs, parseMemorySetupArgs } from "./memory";
export { parseMigrationArgs } from "./migration";
export { parseConcurrencyArgs } from "./concurrency";
export { parseStateSpaceArgs } from "./state-space";
export { parseRevalidateArgs } from "./revalidate";
export { parseRuntimeArgs } from "./runtime";
export { parseProductArgs } from "./product";
Expand Down
50 changes: 50 additions & 0 deletions packages/cli/src/parsers/state-space.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { StateSpaceOptions } from "../types";
import { requireValue } from "./primitives";
import { HelpRequested, throwUnknownOption } from "./shared";

export function parseStateSpaceArgs(args: string[]): StateSpaceOptions {
const options: StateSpaceOptions = { surfaceFiles: [], format: "markdown" };
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (!arg) continue;
if (arg === "--help" || arg === "-h") throw new HelpRequested();
const [flag, inline] = splitArg(arg);
const value = () => inline ?? requireValue(args, index, flag);
if (flag === "--experiment") options.experimentFile = value();
else if (flag === "--surface") options.surfaceFiles.push(value());
else if (flag === "--cwd") options.cwd = value();
else if (flag === "--output") options.output = value();
else if (flag === "--format") options.format = parseFormat(value());
else if (flag === "--target-kind") options.targetKind = parseTarget(value());
else if (flag === "--cleanup-plan") options.cleanupPlan = value();
else {
throwUnknownOption(arg, "state-space");
continue;
}
if (inline === undefined) index += 1;
}
return options;
}

function splitArg(arg: string): [string, string | undefined] {
const index = arg.indexOf("=");
return index < 0 ? [arg, undefined] : [arg.slice(0, index), arg.slice(index + 1)];
}

function parseFormat(value: string): StateSpaceOptions["format"] {
if (value === "json" || value === "markdown") return value;
throw new Error(`Invalid state-space format "${value}". Expected json or markdown.`);
}

function parseTarget(value: string): NonNullable<StateSpaceOptions["targetKind"]> {
if (
value === "unspecified" ||
value === "fixture-local" ||
value === "disposable-local" ||
value === "remote-unapproved" ||
value === "production-like"
) {
return value;
}
throw new Error(`Invalid state-space target kind "${value}".`);
}
1 change: 1 addition & 0 deletions packages/cli/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from "./llm-review";
export * from "./loop";
export * from "./maintenance";
export * from "./concurrency";
export * from "./state-space";
export * from "./migration";
export * from "./mcp";
export * from "./memory";
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/types/state-space.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ConfigFormat } from "./common";
import type { StateSpaceTargetKind } from "@submuxhq/codedecay-knowledge";

export interface StateSpaceOptions {
cwd?: string | undefined;
experimentFile?: string | undefined;
surfaceFiles: string[];
targetKind?: StateSpaceTargetKind | undefined;
cleanupPlan?: string | undefined;
format: ConfigFormat;
output?: string | undefined;
}
69 changes: 69 additions & 0 deletions packages/cli/test/state-space.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { execFileSync } from "node:child_process";
import { copyFileSync, mkdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { runCli } from "../src/index";

const fixtures = join(dirname(fileURLToPath(import.meta.url)), "../../knowledge/test/fixtures/state-space");
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});

describe("codedecay state-space CLI", () => {
it("evaluates a stale-cache fixture in a child repository", async () => {
const root = createRepo();
mkdirSync(join(root, "experiments"), { recursive: true });
copyFileSync(join(fixtures, "stale-cache.json"), join(root, "experiments", "stale-cache.json"));
const result = await run([
"state-space",
"--cwd",
root,
"--experiment",
"experiments/stale-cache.json",
"--format",
"json",
"--output",
"reports/state-space.json"
]);
const report = JSON.parse(readFileSync(join(root, "reports", "state-space.json"), "utf8")) as {
verdict: string;
fullyVerified: boolean;
coverage: { exhaustive: boolean };
};
expect(result).toEqual({ exitCode: 0, stdout: "", stderr: "" });
expect(report.verdict).toBe("confirmed-regression");
expect(report.fullyVerified).toBe(false);
expect(report.coverage.exhaustive).toBe(false);
});

it("exposes state-space help", async () => {
const result = await run(["state-space", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("CodeDecay state-space");
});
});

async function run(args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
let stdout = "";
let stderr = "";
const exitCode = await runCli(args, {
stdout: (text) => {
stdout += text;
},
stderr: (text) => {
stderr += text;
}
});
return { exitCode, stdout, stderr };
}

function createRepo(): string {
const root = join(tmpdir(), `codedecay-state-space-cli-${Date.now()}-${Math.random().toString(16).slice(2)}`);
mkdirSync(root, { recursive: true });
execFileSync("git", ["init", "-q"], { cwd: root });
roots.push(root);
return root;
}
22 changes: 22 additions & 0 deletions packages/knowledge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ export type {
ConcurrencyTargetKind,
ConcurrencyVerdict
} from "./concurrency/types";
export { analyzeStateSpaceSafety } from "./state-space/analyze";
export type { AnalyzeStateSpaceSafetyOptions } from "./state-space/analyze";
export { gateStateSpaceBounds } from "./state-space/bounds";
export { detectStateSpaceCandidates } from "./state-space/detect";
export { evaluateStateSpaceOracle, generateStateSpaceCombinations } from "./state-space/oracles";
export { renderStateSpaceSafetyMarkdown } from "./state-space/render";
export {
STATE_SPACE_DEFAULT_BOUNDS,
STATE_SPACE_EVIDENCE_SCHEMA_VERSION
} from "./state-space/types";
export type {
StateSpaceBounds,
StateSpaceCandidate,
StateSpaceCombination,
StateSpaceCoverageReport,
StateSpaceDimension,
StateSpaceExperimentInput,
StateSpaceExperimentKind,
StateSpaceSafetyReport,
StateSpaceTargetKind,
StateSpaceVerdict
} from "./state-space/types";
export type { IngestRuntimeEvidenceOptions } from "./runtime/ingest";
export { renderRuntimeEvidenceMarkdown } from "./runtime/render";
export { RUNTIME_EVIDENCE_SCHEMA_VERSION } from "./runtime/types";
Expand Down
Loading
Loading