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
38 changes: 38 additions & 0 deletions docs/concurrency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Concurrency safety

CodeDecay evaluates **deterministic fixture oracles** for retries, duplicate
delivery, lost updates, and idempotency. It does not spawn a distributed
scheduler or contact production queues.

## What it can establish

- Experiment inputs: actors, operations, seeded schedules, retry/duplicate
policy, fault points, state oracle, bounds, cleanup
- Candidate surfaces from routes/jobs/locks/idempotency/retry mentions
(keyword = candidate, not proof)
- Falsifiable invariants: exactly-once, at-least-once-safe, no-lost-update,
monotonic state, bounded retries, compensating action
- Deterministic barrier schedules with reproducible seeds
- Bound gates for parallelism, repetitions, timeout, and network targets
- Distinction between confirmed race, passed oracle, inconclusive stress,
bounds-blocked, and needs-human
- Repair tasks that attach a durable regression test id after a confirmed defect

## What it cannot establish

- Absence of races in production
- Safety from low-repetition probabilistic stress alone
- A `fullyVerified: true` result (always false in this slice)
- Queue/webhook/cron/lock/outbox execution (extension boundaries only)

## CLI / MCP

```bash
codedecay concurrency --experiment experiment.json --surface src/jobs/payout.ts
codedecay concurrency --experiment experiment.json --target-kind fixture-local --format json
```

MCP tool: `concurrency_safety`.

Approved bounded execution through the execution package remains a future
adapter; this slice stays local, deterministic, and command-free.
24 changes: 24 additions & 0 deletions packages/cli/src/commands/concurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { resolve } from "node:path";
import { analyzeConcurrencySafety, renderConcurrencySafetyMarkdown } from "@submuxhq/codedecay-knowledge";
import { parseConcurrencyArgs } from "../parsers/args";
import type { CliCommandContext, CliRuntime, ConcurrencyOptions } from "../types";

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

export function runConcurrencyCommand(context: CliCommandContext, dependencies: RunConcurrencyCommandDependencies): void {
const options = parseConcurrencyArgs(context.args);
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
const rootDir = dependencies.resolveRepoRoot(cwd, options);
const report = analyzeConcurrencySafety({
rootDir,
experimentFile: options.experimentFile,
surfaceFiles: options.surfaceFiles,
targetKind: options.targetKind,
cleanupPlan: options.cleanupPlan
});
const rendered = options.format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderConcurrencySafetyMarkdown(report);
dependencies.writeOutput({ cwd: rootDir, output: options.output, rendered, runtime: context.runtime });
}
5 changes: 5 additions & 0 deletions packages/cli/src/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "./memory";
import { runMcpCommand as runMcpCommandWithDependencies } from "./mcp";
import { runMigrationCommand as runMigrationCommandWithDependencies } from "./migration";
import { runConcurrencyCommand as runConcurrencyCommandWithDependencies } from "./concurrency";
import { runProductCommand as runProductCommandWithDependencies } from "./product";
import { runRedteamCommand as runRedteamCommandWithDependencies } from "./redteam";
import { runRevalidateCommand as runRevalidateCommandWithDependencies } from "./revalidate";
Expand Down Expand Up @@ -99,6 +100,10 @@ export function createCommandHandlers(options: CommandRegistryOptions): Record<s
resolveRepoRoot: getRepoRootForCli,
writeOutput: writeCliOutput
}),
concurrency: (context) => runConcurrencyCommandWithDependencies(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
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 @@ -28,6 +28,31 @@ export const ANALYSIS_COMMAND_DOCS: Record<string, CommandDoc> = {
"See docs/migration.md for what plan-ready vs fully-verified means."
]
},
concurrency: {
name: "concurrency",
summary: "Plan and evaluate deterministic concurrency/idempotency oracles.",
usage: ["codedecay concurrency [options]"],
description: [
"Load a seeded concurrency experiment fixture, detect candidate surfaces, enforce disposable bounds, and evaluate duplicate-delivery / lost-update oracles without spawning a scheduler or contacting production queues."
],
options: [
{ flag: "--experiment <path>", description: "Repo-local concurrency experiment JSON fixture" },
{ flag: "--surface <path>", description: "Source file to scan for concurrency candidates; 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 concurrency --experiment .codedecay/concurrency/duplicate.json --surface src/jobs/payout.ts",
"codedecay concurrency --experiment experiment.json --target-kind fixture-local --format json"
],
notes: [
"This command is oracle/plan-only: it does not run parallel load generators or touch production queues.",
"Stress-only results stay inconclusive. See docs/concurrency.md."
]
},
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", "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", "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 @@ -13,6 +13,7 @@ export { parseLoopArgs } from "./loop";
export { parseMcpArgs } from "./mcp";
export { parseMemoryArgs, parseMemoryImportArgs, parseMemoryLearnArgs, parseMemoryLearningArgs, parseMemorySetupArgs } from "./memory";
export { parseMigrationArgs } from "./migration";
export { parseConcurrencyArgs } from "./concurrency";
export { parseRevalidateArgs } from "./revalidate";
export { parseRuntimeArgs } from "./runtime";
export { parseProductArgs } from "./product";
Expand Down
50 changes: 50 additions & 0 deletions packages/cli/src/parsers/concurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ConcurrencyOptions } from "../types";
import { requireValue } from "./primitives";
import { HelpRequested, throwUnknownOption } from "./shared";

export function parseConcurrencyArgs(args: string[]): ConcurrencyOptions {
const options: ConcurrencyOptions = { 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, "concurrency");
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): ConcurrencyOptions["format"] {
if (value === "json" || value === "markdown") return value;
throw new Error(`Invalid concurrency format "${value}". Expected json or markdown.`);
}

function parseTarget(value: string): NonNullable<ConcurrencyOptions["targetKind"]> {
if (
value === "unspecified" ||
value === "fixture-local" ||
value === "disposable-local" ||
value === "remote-unapproved" ||
value === "production-like"
) {
return value;
}
throw new Error(`Invalid concurrency target kind "${value}".`);
}
12 changes: 12 additions & 0 deletions packages/cli/src/types/concurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ConfigFormat } from "./common";
import type { ConcurrencyTargetKind } from "@submuxhq/codedecay-knowledge";

export interface ConcurrencyOptions {
cwd?: string | undefined;
experimentFile?: string | undefined;
surfaceFiles: string[];
targetKind?: ConcurrencyTargetKind | undefined;
cleanupPlan?: string | undefined;
format: ConfigFormat;
output?: string | undefined;
}
1 change: 1 addition & 0 deletions packages/cli/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from "./execution";
export * from "./llm-review";
export * from "./loop";
export * from "./maintenance";
export * from "./concurrency";
export * from "./migration";
export * from "./mcp";
export * from "./memory";
Expand Down
48 changes: 48 additions & 0 deletions packages/cli/test/built-cli-concurrency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { copyFileSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { beforeAll, describe, expect, it } from "vitest";
import { createRepo, ensureBuiltCli, runBuilt } from "./helpers/built-cli";

const fixtures = join(dirname(fileURLToPath(import.meta.url)), "../../knowledge/test/fixtures/concurrency");

beforeAll(ensureBuiltCli, 120_000);

describe("built codedecay concurrency workflow", () => {
it("evaluates a fixture oracle from the bundled CLI without spawning a scheduler", () => {
const root = createRepo({ "README.md": "fixture\n" });
mkdirSync(join(root, "experiments"), { recursive: true });
copyFileSync(join(fixtures, "idempotent.json"), join(root, "experiments", "idempotent.json"));
const result = runBuilt([
"concurrency",
"--cwd",
root,
"--experiment",
"experiments/idempotent.json",
"--format",
"json"
]);
expect(result.status).toBe(0);
expect(result.stderr).toBe("");
const report = JSON.parse(result.stdout) as {
verdict: string;
fullyVerified: boolean;
safety: Record<string, boolean>;
};
expect(report.verdict).toBe("passed-oracle");
expect(report.fullyVerified).toBe(false);
expect(report.safety).toMatchObject({
commandsExecuted: false,
networkCalled: false,
schedulerSpawned: false
});
});

it("exposes concurrency help from the bundled command registry", () => {
createRepo({ "README.md": "fixture\n" });
const result = runBuilt(["concurrency", "--help"]);
expect(result.status).toBe(0);
expect(result.stdout).toContain("CodeDecay concurrency");
expect(result.stdout).toContain("--experiment <path>");
});
});
73 changes: 73 additions & 0 deletions packages/cli/test/concurrency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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/concurrency");
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});

describe("codedecay concurrency CLI", () => {
it("evaluates a duplicate-delivery fixture in a child repository", async () => {
const root = createRepo();
mkdirSync(join(root, "experiments"), { recursive: true });
copyFileSync(join(fixtures, "duplicate-delivery.json"), join(root, "experiments", "duplicate.json"));
const result = await run([
"concurrency",
"--cwd",
root,
"--experiment",
"experiments/duplicate.json",
"--format",
"json",
"--output",
"reports/concurrency.json"
]);
const report = JSON.parse(readFileSync(join(root, "reports", "concurrency.json"), "utf8")) as {
verdict: string;
fullyVerified: boolean;
safety: Record<string, boolean>;
};
expect(result).toEqual({ exitCode: 0, stdout: "", stderr: "" });
expect(report.verdict).toBe("confirmed-race");
expect(report.fullyVerified).toBe(false);
expect(report.safety).toMatchObject({
commandsExecuted: false,
networkCalled: false,
schedulerSpawned: false
});
});

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

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-concurrency-cli-${Date.now()}-${Math.random().toString(16).slice(2)}`);
mkdirSync(root, { recursive: true });
execFileSync("git", ["init", "-q"], { cwd: root });
roots.push(root);
return root;
}
Loading
Loading