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
5 changes: 5 additions & 0 deletions packages/cli/src/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { runMcpCommand as runMcpCommandWithDependencies } from "./mcp";
import { runProductCommand as runProductCommandWithDependencies } from "./product";
import { runRedteamCommand as runRedteamCommandWithDependencies } from "./redteam";
import { runRevalidateCommand as runRevalidateCommandWithDependencies } from "./revalidate";
import { runRuntimeCommand as runRuntimeCommandWithDependencies } from "./runtime";
import { runSessionCommand as runSessionCommandWithDependencies } from "./session";
import { runSnapshotCommand as runSnapshotCommandWithDependencies } from "./snapshot";
import { createProductTargetReport as createProductTargetReportWithRuntime } from "../product/runtime";
Expand Down Expand Up @@ -118,6 +119,10 @@ export function createCommandHandlers(options: CommandRegistryOptions): Record<s
resolveRepoRoot: getRepoRootForCli,
writeOutput: writeCliOutput
}),
runtime: (context) => runRuntimeCommandWithDependencies(context, {
resolveRepoRoot: getRepoRootForCli,
writeOutput: writeCliOutput
}),
session: (context) => runSessionCommandWithDependencies(context, {
createAnalysisContext: createAnalysisContextForCli,
resolveRepoRoot: getRepoRootForCli,
Expand Down
34 changes: 34 additions & 0 deletions packages/cli/src/commands/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { resolve } from "node:path";
import {
ingestRuntimeEvidence,
loadServiceTopologyManifest,
renderRuntimeEvidenceMarkdown
} from "@submuxhq/codedecay-knowledge";
import { parseRuntimeArgs } from "../parsers/args";
import type { CliCommandContext, CliRuntime, RuntimeOptions } from "../types";

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

export function runRuntimeCommand(context: CliCommandContext, dependencies: RunRuntimeCommandDependencies): void {
const options = parseRuntimeArgs(context.args);
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
const rootDir = dependencies.resolveRepoRoot(cwd, options);
const topology = options.topology
? loadServiceTopologyManifest({ rootDir, path: options.topology })
: undefined;
const report = ingestRuntimeEvidence({
rootDir,
otlpPath: options.telemetry,
errorsPath: options.errors,
topology,
headRevision: options.headRevision,
environment: options.environment
});
const rendered = options.format === "json"
? `${JSON.stringify(report, null, 2)}\n`
: renderRuntimeEvidenceMarkdown(report);
dependencies.writeOutput({ cwd: rootDir, output: options.output, rendered, runtime: context.runtime });
}
21 changes: 21 additions & 0 deletions packages/cli/src/docs/command-docs/analysis.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import type { CommandDoc } from "../../renderers/discovery";

export const ANALYSIS_COMMAND_DOCS: Record<string, CommandDoc> = {
runtime: {
name: "runtime",
summary: "Ingest local runtime exports as redacted engineering evidence.",
usage: ["codedecay runtime [options]"],
description: ["Read local OTLP JSON traces and structured error exports, correlate them with an optional service topology, and emit revision-aware investigation evidence."],
options: [
{ flag: "--telemetry <path>", description: "Repo-local OTLP JSON trace export" },
{ flag: "--errors <path>", description: "Repo-local structured error export" },
{ flag: "--topology <path>", description: "Optional repo-local service topology manifest" },
{ flag: "--head-revision <revision>", description: "Current source revision used to classify evidence trust" },
{ flag: "--environment <name>", description: "Environment label when an export omits one" },
{ flag: "--cwd <path>", description: "Repository working directory (default: current directory)" },
{ flag: "--format <format>", description: "json or markdown (default: markdown)" },
{ flag: "--output <path>", description: "Write evidence report to a file instead of stdout" }
],
examples: [
"codedecay runtime --telemetry .codedecay/runtime/traces.json --head-revision $(git rev-parse HEAD)",
"codedecay runtime --errors .codedecay/runtime/errors.json --format json"
],
notes: ["Inputs must resolve inside the repository. The command performs no network calls or project command execution."]
},
analyze: {
name: "analyze",
summary: "Deterministic PR risk, impact, and decay report.",
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", "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", "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, parseMemorySetupArgs } from "./memory";
export { parseRevalidateArgs } from "./revalidate";
export { parseRuntimeArgs } from "./runtime";
export { parseProductArgs } from "./product";
export { parseRedteamArgs } from "./redteam";
export { parseSessionArgs } from "./session";
Expand Down
46 changes: 46 additions & 0 deletions packages/cli/src/parsers/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { RuntimeOptions } from "../types";
import { requireValue } from "./primitives";
import { HelpRequested, throwUnknownOption } from "./shared";

export function parseRuntimeArgs(args: string[]): RuntimeOptions {
const options: RuntimeOptions = { format: "markdown" };
const valueOptions = new Map<string, keyof RuntimeOptions>([
["cwd", "cwd"],
["telemetry", "telemetry"],
["errors", "errors"],
["topology", "topology"],
["head-revision", "headRevision"],
["environment", "environment"],
["output", "output"]
]);

for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (!arg) continue;
if (arg === "--help" || arg === "-h") throw new HelpRequested();
if (arg.startsWith("--format=")) {
options.format = parseFormat(arg.slice("--format=".length));
continue;
}
if (arg === "--format") {
options.format = parseFormat(requireValue(args, index, arg));
index += 1;
continue;
}
const match = /^--([^=]+)(?:=(.*))?$/.exec(arg);
const key = match?.[1] ? valueOptions.get(match[1]) : undefined;
if (key) {
const value = match?.[2] ?? requireValue(args, index, arg);
options[key] = value as never;
if (match?.[2] === undefined) index += 1;
continue;
}
throwUnknownOption(arg, "runtime");
}
return options;
}

function parseFormat(value: string): RuntimeOptions["format"] {
if (value === "json" || value === "markdown") return value;
throw new Error(`Invalid runtime format "${value}". Expected json or markdown.`);
}
1 change: 1 addition & 0 deletions packages/cli/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export * from "./memory";
export * from "./product";
export * from "./redteam";
export * from "./revalidate";
export * from "./runtime";
export * from "./session";
export * from "./snapshot";
12 changes: 12 additions & 0 deletions packages/cli/src/types/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ConfigFormat } from "./common";

export interface RuntimeOptions {
cwd?: string | undefined;
telemetry?: string | undefined;
errors?: string | undefined;
topology?: string | undefined;
headRevision?: string | undefined;
environment?: string | undefined;
format: ConfigFormat;
output?: string | undefined;
}
66 changes: 66 additions & 0 deletions packages/cli/test/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { runCli } from "../src/index";

const roots: string[] = [];

afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});

describe("codedecay runtime CLI", () => {
it("writes revision-aware, topology-correlated local evidence", async () => {
const root = createRepo();
mkdirSync(join(root, ".codedecay", "runtime"), { recursive: true });
writeFileSync(join(root, ".codedecay", "runtime", "traces.json"), JSON.stringify({
resourceSpans: [{
resource: { attributes: [attr("service.name", "api"), attr("service.version", "head")] },
scopeSpans: [{ spans: [{ name: "GET /health", spanId: "abc", flags: 1, startTimeUnixNano: "0", endTimeUnixNano: "2000000", attributes: [attr("http.route", "/health")] }] }]
}]
}), "utf8");
writeFileSync(join(root, "topology.json"), JSON.stringify({
schemaVersion: 1,
nodes: [{ id: "service:api", kind: "service", label: "api", confidence: "declared", freshness: "unknown", trustClass: "declared-context", sources: [{ kind: "manifest", source: "fixture", repositoryId: "repo", revision: "head" }], limitations: [] }],
edges: [],
limitations: []
}), "utf8");

const result = await run(["runtime", "--cwd", root, "--telemetry", ".codedecay/runtime/traces.json", "--topology", "topology.json", "--head-revision", "head", "--format", "json", "--output", "reports/runtime.json"]);
const report = JSON.parse(readFileSync(join(root, "reports", "runtime.json"), "utf8")) as { operations: Array<{ trust: string; topologyNodeIds: string[] }>; safety: { networkCalled: boolean } };

expect(result).toEqual({ exitCode: 0, stdout: "", stderr: "" });
expect(report.operations[0]).toMatchObject({ trust: "current-revision", topologyNodeIds: ["service:api"] });
expect(report.safety.networkCalled).toBe(false);
});

it("renders useful limitations with no configured exports", async () => {
const result = await run(["runtime", "--cwd", createRepo()]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("No runtime operations were ingested.");
expect(result.stdout).toContain("No local OpenTelemetry export was configured");
expect(result.stdout).toContain("No structured error export was configured");
expect(result.stdout).toContain("no network or command execution");
});
});

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

function attr(key: string, value: string): unknown {
return { key, value: { stringValue: value } };
}
11 changes: 11 additions & 0 deletions packages/knowledge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ export {
SERVICE_TOPOLOGY_NODE_KINDS,
SERVICE_TOPOLOGY_SCHEMA_VERSION
} from "./topology/types";
export { ingestRuntimeEvidence } from "./runtime/ingest";
export type { IngestRuntimeEvidenceOptions } from "./runtime/ingest";
export { renderRuntimeEvidenceMarkdown } from "./runtime/render";
export { RUNTIME_EVIDENCE_SCHEMA_VERSION } from "./runtime/types";
export type {
RuntimeErrorEvidence,
RuntimeEvidenceReport,
RuntimeEvidenceSource,
RuntimeEvidenceTrust,
RuntimeOperationEvidence
} from "./runtime/types";
export type {
ServiceTopologyConfidence,
ServiceTopologyEdge,
Expand Down
Loading
Loading