diff --git a/Dockerfile b/Dockerfile index 86bcd33..af2d510 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ # open http://localhost:4747 # # Then point traces at it from the host: -# trace dynamic --node 9229 --bp app.js:42 --curl '…' --emit http://localhost:4747 +# trace run --node 9229 --bp app.js:42 --curl '…' --emit http://localhost:4747 # # The image is the full CLI (ENTRYPOINT `trace`); CMD defaults to `serve`, which launches the standalone # dashboard. Override to run other subcommands, e.g. docker run trace-cli doctor. diff --git a/README.md b/README.md index 85e7ea7..a8aa7f1 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,9 @@ trace-cli run --chrome --url http://localhost:5173/route --breakpoint src/pages/ Library (TypeScript) ```ts -import { DynamicCommand, Trace } from "trace-cli"; // DynamicCommand powers `trace run` +import { RunCommand, Trace } from "trace-cli"; // RunCommand powers `trace run` -const { trace } = await new DynamicCommand().run({ +const { trace } = await new RunCommand().run({ target: "node", port: 9229, curl: 'curl -s http://127.0.0.1:3100/price?qty=3', breakpoints: ["test/servers/node-api/server.js:42"], diff --git a/docker-compose.yml b/docker-compose.yml index 9847596..fde4f4e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ # Emit traces from the host (the CLI runs where your debug target is reachable). Chrome traces record by # default and upload the video to mock-aws, attaching a link to the trace: # export S3_ENDPOINT=http://localhost:19000 -# TRACE_COLLECTOR_URL=http://localhost:14747 trace dynamic --chrome 9222 --url http://localhost:3000 --bp src/App.tsx:9 +# TRACE_COLLECTOR_URL=http://localhost:14747 trace run --chrome 9222 --url http://localhost:3000 --bp src/App.tsx:9 # # Bringing the stack up runs the whole end-to-end demo too (Node + React + Chrome targets, all in # one demo container), no host setup — `docker compose up --build` starts everything; `docker compose down` stops it all: diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 9fcdf2e..9d40480 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -84,7 +84,7 @@ trace schema # print the JSON Schema (the contract) src/ cli.js # commander root; registers subcommands commands/ - dynamic.js # wraps engine/trace.js → envelope (today's behavior, incl. auto-recorded Chrome replay) + run.js # wraps engine/trace.js → envelope (today's behavior, incl. auto-recorded Chrome replay) static.js # deps | complexity | symbols | search dispatch exec.js # otel-cli exec spans.js # otel store query diff --git a/src/cli/Cli.ts b/src/cli/Cli.ts index 4f533a6..9af1932 100644 --- a/src/cli/Cli.ts +++ b/src/cli/Cli.ts @@ -9,7 +9,10 @@ import { InputManager } from "../io/InputManager.js"; import { InputError } from "../io/InputError.js"; import { ProcessingManager, EngineAbortError } from "../io/ProcessingManager.js"; import { OutputManager } from "../io/OutputManager.js"; -import type { ProcessingResult, OutputResult } from "../io/descriptors.js"; +import type { + ProcessingResult, OutputResult, OutputOptions, + RawRunInput, RawGraphInput, RawDepsInput, RawComplexityInput, RawSymbolsInput, +} from "../io/descriptors.js"; import { VERSION } from "../shared/version.js"; import { DEFAULT_NODE_PORT, DEFAULT_COLLECTOR_PORT } from "../shared/defaults.js"; import { logger } from "../shared/logger.js"; @@ -43,20 +46,20 @@ export class Cli { } /** Shared tail for the static analyses: render the envelope, forward it to a collector, exit on its error state. */ - async #finishStatic(result: ProcessingResult, options: any): Promise { + async #finishStatic(result: ProcessingResult, options: OutputOptions): Promise { const out = this.#output.emit(result, options); this.#writeTrace(out); await this.#processing.forwardStatic(result.trace); // post-gate, explicit-only (TRACE_COLLECTOR_URL) process.exit(out.exitCode); } - async #runDynamic(options: any): Promise { + async #runTrace(options: RawRunInput): Promise { let normalized; try { normalized = this.#input.acceptRun(options); } catch (error) { if (error instanceof InputError) usage(error.message); throw error; } let result: ProcessingResult; - try { result = await this.#processing.runDynamic(normalized); } + try { result = await this.#processing.runTrace(normalized); } catch (error) { // The run threw; ProcessingManager already flushed the terminal envelope to the collector and logged the // ENGINE_FATAL cause. Surface the failure as a non-zero exit, matching the old inline behavior. @@ -68,26 +71,26 @@ export class Cli { process.exit(out.exitCode); } - async #runGraph(options: any): Promise { + async #runGraph(options: RawGraphInput): Promise { let request; try { request = this.#input.acceptGraph(options); } catch (error) { if (error instanceof InputError) usage(error.message); throw error; } await this.#finishStatic(await this.#processing.runGraph(request), options); } - async #runDeps(options: any): Promise { + async #runDeps(options: RawDepsInput): Promise { let request; try { request = this.#input.acceptDeps(options); } catch (error) { if (error instanceof InputError) usage(error.message); throw error; } await this.#finishStatic(await this.#processing.runDeps(request), options); } - async #runComplexity(path: string, options: any): Promise { + async #runComplexity(path: string, options: Omit): Promise { const request = this.#input.acceptComplexity({ ...options, path }); await this.#finishStatic(await this.#processing.runComplexity(request), options); } - async #runSymbols(file: string, options: any): Promise { + async #runSymbols(file: string, options: Omit): Promise { let request; try { request = this.#input.acceptSymbols({ ...options, file }); } catch (error) { if (error instanceof InputError) usage(error.message); throw error; } @@ -119,7 +122,7 @@ export class Cli { .option("--json [path]", "envelope as JSON: to a file if a path is given, else to stdout") .option("--concise", "trim the PRINTED --json envelope (stdout/file) for token-tight agent reads: per hit, locals collapse to key names and the call stack keeps its top 2 frames (watched --expression values, location & timing kept). Does NOT affect --emit — the collector always receives the full envelope. Re-run --detailed for everything.") .option("--detailed", "full --json envelope: every local's value and the complete call stack at each hit (the default)") - .action((options) => this.#runDynamic(options)); + .action((options) => this.#runTrace(options)); // static analysis — code structure without running the app. Each command shells out to one analyzer and // emits the same Trace envelope as the runtime `run` command (call graph · deps · complexity · symbols). diff --git a/src/cli/CommandInputs.ts b/src/cli/CommandInputs.ts index b0a46f1..d690ec5 100644 --- a/src/cli/CommandInputs.ts +++ b/src/cli/CommandInputs.ts @@ -43,7 +43,7 @@ export function validateSteps(rawSteps: string[]): string[] { } /** Input contract for `trace-cli run`. */ -export class DynamicInput { +export class RunInput { @IsIn(Object.values(TargetKind)) target: TargetKind; // In Chrome launch mode the port isn't known until the browser is spawned, so only range-check a real port. @ValidateIf((input) => !input.launch) @IsInt() @Min(1) @Max(MAX_PORT) port: number; @@ -55,7 +55,7 @@ export class DynamicInput { @IsOptional() @IsArray() @IsString({ each: true }) steps?: string[]; // chrome: the ordered UI journey @IsOptional() @IsString() curl?: string; - constructor(init: Partial = {}) { + constructor(init: Partial = {}) { this.target = init.target ?? TargetKind.Node; this.port = init.port ?? 0; this.breakpoints = init.breakpoints ?? []; diff --git a/src/cli/commands/DoctorCommand.ts b/src/cli/commands/DoctorCommand.ts index f88667d..856ff89 100644 --- a/src/cli/commands/DoctorCommand.ts +++ b/src/cli/commands/DoctorCommand.ts @@ -15,7 +15,7 @@ export interface ToolStatus { name: string; pillar: string; purpose: string; pre const TOOLS: ToolDef[] = [ { name: "node", pillar: "engine", purpose: "Node --inspect (CDP) target", cmd: "node", args: ["--version"] }, { name: "chrome", pillar: "frontend", purpose: "Chrome target / recording frames", chrome: true }, - { name: "ffmpeg", pillar: "frontend", purpose: "dynamic --record video", cmd: "ffmpeg", args: ["-version"] }, + { name: "ffmpeg", pillar: "frontend", purpose: "run --record video", cmd: "ffmpeg", args: ["-version"] }, { name: "rg", pillar: "static", purpose: "static search (ripgrep)", cmd: "rg", args: ["--version"] }, { name: "lizard", pillar: "static", purpose: "static complexity", cmd: "lizard", args: ["--version"] }, { name: "tree-sitter", pillar: "static", purpose: "static symbols (AST)", cmd: "tree-sitter", args: ["--version"] }, diff --git a/src/cli/commands/GraphCommand.ts b/src/cli/commands/GraphCommand.ts index 85287b9..fb1bd8c 100644 --- a/src/cli/commands/GraphCommand.ts +++ b/src/cli/commands/GraphCommand.ts @@ -29,7 +29,7 @@ export interface GraphRequest { * graph rooted at the entry, and normalize it into one Trace envelope (`data.graph`). The provider is the * injected collaborator (Dependency Inversion); this class owns the use-case and the envelope, not the analysis. * A resolution/analysis failure becomes an error diagnostic on a still-well-formed envelope, matching how the - * dynamic command surfaces engine failures — an agent always gets back a Trace. + * run command surfaces engine failures — an agent always gets back a Trace. */ export class GraphCommand extends TraceCommand { async run(request: GraphRequest): Promise { diff --git a/src/cli/commands/DynamicCommand.ts b/src/cli/commands/RunCommand.ts similarity index 95% rename from src/cli/commands/DynamicCommand.ts rename to src/cli/commands/RunCommand.ts index 90318f2..0c851c5 100644 --- a/src/cli/commands/DynamicCommand.ts +++ b/src/cli/commands/RunCommand.ts @@ -17,12 +17,12 @@ import { logger } from "../../shared/logger.js"; import { Code } from "../../shared/codes.js"; import { TraceCommand } from "./TraceCommand.js"; -const log = logger.child({ component: "dynamic" }); +const log = logger.child({ component: "run" }); -export type DynamicTargetKind = TargetKind; +export type RunTargetKind = TargetKind; -export interface DynamicRequest extends TraceOptions { - target: DynamicTargetKind; +export interface RunRequest extends TraceOptions { + target: RunTargetKind; launch?: boolean; // chrome: spawn a throwaway headless Chrome instead of attaching to `port` profileDir?: string; // chrome: launch on a persistent --user-data-dir (a real, logged-in profile) headed?: boolean; // chrome: launch the browser visibly instead of headless @@ -35,17 +35,17 @@ export interface DynamicRequest extends TraceOptions { onProgress?: (trace: Trace) => void; } -export interface DynamicResult { trace: Trace; capture: CaptureResult; } +export interface RunResult { trace: Trace; capture: CaptureResult; } /** Context shared by the running (partial) and final envelopes of one trace run. */ interface RunCtx { sessionId: string; target: TargetKind; trigger: string | null; args: Record; startedAtMs: number; } /** - * DynamicCommand — orchestrates a breakpoint trace: pick the tracer by target, run it, normalize the + * RunCommand — orchestrates a breakpoint trace: pick the tracer by target, run it, normalize the * capture into a Trace (lineage + diagnostics), and (for Chrome) record + upload the replay. Collaborators * are injected (Tracer, ArtifactStore) — Dependency Inversion; this class owns the use-case, not the IO. */ -export class DynamicCommand extends TraceCommand { +export class RunCommand extends TraceCommand { constructor( private readonly tracer: Tracer = new Tracer(), private readonly artifacts?: ArtifactStore, @@ -53,7 +53,7 @@ export class DynamicCommand extends TraceCommand super(); } - async run(request: DynamicRequest): Promise { + async run(request: RunRequest): Promise { const startedAtMs = this.started(); const sessionId = request.sessionId ?? randomUUID(); const isChrome = request.target === TargetKind.Chrome; @@ -172,7 +172,7 @@ export class DynamicCommand extends TraceCommand }); } - /** Human view of a dynamic trace: the breakpoint/timeline render plus the lineage panel. */ + /** Human view of a breakpoint trace: the breakpoint/timeline render plus the lineage panel. */ render(trace: Trace): string { return Renderer.render(trace) + Renderer.renderLineage(trace.data.lineage); } diff --git a/src/cli/commands/ServeCommand.ts b/src/cli/commands/ServeCommand.ts index d31a1b7..ee2363e 100644 --- a/src/cli/commands/ServeCommand.ts +++ b/src/cli/commands/ServeCommand.ts @@ -39,7 +39,7 @@ export class ServeCommand extends CliCommand { // default); on any other --port a run must point at this URL explicitly, so don't make the user guess it. const collectorUrl = `http://localhost:${port}`; log.info("starting dashboard", { url: collectorUrl, host }); - log.info("stream traces here", { hint: `trace dynamic … --emit ${collectorUrl} (or: export TRACE_COLLECTOR_URL=${collectorUrl})` }); + log.info("stream traces here", { hint: `trace run … --emit ${collectorUrl} (or: export TRACE_COLLECTOR_URL=${collectorUrl})` }); const child = spawn(process.execPath, [SERVER_ENTRY], { stdio: "inherit", env: { ...process.env, PORT: String(port), HOSTNAME: host, DATABASE_URL: databaseUrl }, diff --git a/src/cli/commands/ShellAnalysisCommand.ts b/src/cli/commands/ShellAnalysisCommand.ts index e2493ef..b248f1d 100644 --- a/src/cli/commands/ShellAnalysisCommand.ts +++ b/src/cli/commands/ShellAnalysisCommand.ts @@ -25,7 +25,7 @@ export interface AnalysisOutcome { * declares its identity (tool/command/errorCode/component) and supplies only the two parts that differ: * the tool call ({@link invocation}) and how to read its output ({@link interpret}). A tool that is missing, * times out, or emits unparseable output becomes a `` error on a still-well-formed Trace, honouring - * the same "an agent always gets a Trace" contract the dynamic/graph commands keep. + * the same "an agent always gets a Trace" contract the run/graph commands keep. */ export abstract class ShellAnalysisCommand }> extends TraceCommand { /** Binary to spawn, e.g. `"madge"`. */ protected abstract readonly tool: string; diff --git a/src/cli/commands/TraceCommand.ts b/src/cli/commands/TraceCommand.ts index 6e13f5c..8b6fbe2 100644 --- a/src/cli/commands/TraceCommand.ts +++ b/src/cli/commands/TraceCommand.ts @@ -22,7 +22,7 @@ export interface Envelope { /** * TraceCommand — the {@link CliCommand} specialization for every command that produces a Trace envelope - * (dynamic, graph, doctor). Each subclass assembles only its own `data` payload + diagnostics; the base owns + * (run, graph, doctor). Each subclass assembles only its own `data` payload + diagnostics; the base owns * the part they all repeated verbatim — stamping version, timestamp and duration, and deriving `ok` from the * diagnostics — so the envelope shape stays identical across commands and lives in exactly one place. * `render` is the human view each trace-producing command must supply (what prints when `--json` is off). diff --git a/src/engine/Tracer.ts b/src/engine/Tracer.ts index c3eeb63..dca43ad 100644 --- a/src/engine/Tracer.ts +++ b/src/engine/Tracer.ts @@ -160,7 +160,7 @@ export class Tracer { const parsedSteps = steps.map((step) => JourneyRunner.parseStep(step)); const screencaster = new Screencaster(CAPTURE_VIEWPORT); // portrait-ish: fills the replay's left pane, no letterbox const config: TraceConfig = { bps: BreakpointResolver.resolveAll(breakpoints, root), root, exprs, frames, maxHits, onEvent: options.onEvent }; - // Wrap the running browser (DynamicCommand already launched/attached it) as a session so target discovery + // Wrap the running browser (RunCommand already launched/attached it) as a session so target discovery // goes through the bridge, not raw CdpDriver calls. const runner = new JourneyRunner(ChromeLauncher.attach(port), screencaster, config); let stepResults: StepResult[] = []; diff --git a/src/index.ts b/src/index.ts index c9a918a..dae9955 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,8 @@ export { PostgresSessionStore } from "./collector/PostgresSessionStore.js"; export { createSessionStore } from "./collector/createSessionStore.js"; export type { StoreOptions } from "./collector/createSessionStore.js"; export type { SessionStore, SessionSummary } from "./collector/SessionStore.js"; -export { DynamicCommand } from "./cli/commands/DynamicCommand.js"; +export { RunCommand } from "./cli/commands/RunCommand.js"; +export type { RunRequest, RunResult, RunTargetKind } from "./cli/commands/RunCommand.js"; export { DoctorCommand } from "./cli/commands/DoctorCommand.js"; export { ExportSkillCommand } from "./cli/commands/ExportSkillCommand.js"; export { Cli } from "./cli/Cli.js"; diff --git a/src/io/InputError.ts b/src/io/InputError.ts index 64fa130..fba2de5 100644 --- a/src/io/InputError.ts +++ b/src/io/InputError.ts @@ -9,7 +9,7 @@ import { Code } from "../shared/codes.js"; * • HTTP → `400 { error, code, problems }`. * * `message` is the single human line (the exact string the CLI prints). `problems` carries the individual - * validation lines when the error aggregates several (a failed `DynamicInput.validate()` / `validateSteps()`); + * validation lines when the error aggregates several (a failed `RunInput.validate()` / `validateSteps()`); * these are already redaction-safe (step problems are labelled by index + action, never the raw typed value). */ export class InputError extends Error { diff --git a/src/io/InputManager.ts b/src/io/InputManager.ts index 6f207ef..9434929 100644 --- a/src/io/InputManager.ts +++ b/src/io/InputManager.ts @@ -36,7 +36,7 @@ const redactStep = (step: string) => step.startsWith("type:") ? step.replace(/=. export class InputManager { #validator = new InputValidator(); - /** Validate + normalize a `run` invocation into a dynamic request + the collector (`--emit`) policy. */ + /** Validate + normalize a `run` invocation into a run request + the collector (`--emit`) policy. */ acceptRun(raw: RawRunInput): NormalizedRun { this.#validator.guardRunFlags(raw); const { target, port, launch, profileDir, headed } = pickTarget(raw); @@ -44,7 +44,7 @@ export class InputManager { // Chrome trigger = an ordered UI journey; --url is shorthand for a leading `goto:`. Node trigger = a curl. const steps: string[] = isChrome ? [...(raw.url ? [`goto:${raw.url}`] : []), ...raw.step] : []; this.#validator.guardRunTrigger(raw, { target, isChrome, steps }); - this.#validator.validateDynamic({ target, port, launch, profileDir, headed, breakpoints: raw.breakpoint, exprs: raw.expression, steps, curl: raw.curl }); + this.#validator.validateRun({ target, port, launch, profileDir, headed, breakpoints: raw.breakpoint, exprs: raw.expression, steps, curl: raw.curl }); this.#validator.validateSteps(steps); const request: NormalizedRun["request"] = { diff --git a/src/io/InputValidator.ts b/src/io/InputValidator.ts index 6bb00f8..416d808 100644 --- a/src/io/InputValidator.ts +++ b/src/io/InputValidator.ts @@ -1,10 +1,10 @@ import { TargetKind } from "../domain/Target.js"; -import { DynamicInput, GraphInput, validateSteps } from "../cli/CommandInputs.js"; +import { RunInput, GraphInput, validateSteps } from "../cli/CommandInputs.js"; import { InputError } from "./InputError.js"; import type { RawRunInput } from "./descriptors.js"; -/** The normalized dynamic fields the strict DTO validation needs (target + trigger, post-`pickTarget`). */ -export interface DynamicFields { +/** The normalized run fields the strict DTO validation needs (target + trigger, post-`pickTarget`). */ +export interface RunFields { target: TargetKind; port: number; launch?: boolean; profileDir?: string; headed?: boolean; breakpoints: string[]; exprs: string[]; steps: string[]; curl?: string; } @@ -39,9 +39,9 @@ export class InputValidator { if (!isChrome && !raw.curl) throw new InputError(`${target} target needs --curl`); } - /** Strict DTO validation of the normalized dynamic input (the class-validator regime). */ - validateDynamic(fields: DynamicFields): void { - const problems = new DynamicInput(fields).validate(); + /** Strict DTO validation of the normalized run input (the class-validator regime). */ + validateRun(fields: RunFields): void { + const problems = new RunInput(fields).validate(); if (problems.length) throw new InputError(`invalid input — ${problems.join("; ")}`, problems); } diff --git a/src/io/ProcessingManager.ts b/src/io/ProcessingManager.ts index a510560..5264247 100644 --- a/src/io/ProcessingManager.ts +++ b/src/io/ProcessingManager.ts @@ -1,7 +1,7 @@ import { Tracer } from "../engine/Tracer.js"; import { S3ArtifactStore } from "../storage/S3ArtifactStore.js"; import { Collector, type EmitResult } from "../collector/Collector.js"; -import { DynamicCommand } from "../cli/commands/DynamicCommand.js"; +import { RunCommand } from "../cli/commands/RunCommand.js"; import { GraphCommand } from "../cli/commands/GraphCommand.js"; import { DepsCommand } from "../cli/commands/DepsCommand.js"; import { ComplexityCommand } from "../cli/commands/ComplexityCommand.js"; @@ -18,7 +18,7 @@ import type { const log = logger.child({ component: "processing" }); /** - * EngineAbortError — thrown by ProcessingManager.runDynamic when the dynamic run threw (attach failed, + * EngineAbortError — thrown by ProcessingManager.runTrace when the run threw (attach failed, * engine crashed, recording threw). By this point the terminal envelope has already been streamed to the * collector and the emit chain flushed, so each frontend just decides the response: the CLI exits 1, HTTP * answers 500, MCP returns an error tool result. Carries the original cause for logging. @@ -47,21 +47,21 @@ export function emitFailureMessage(collector: string, count: number, last: EmitR * ProcessingManager — the orchestration tier. Owns everything that turns a validated request into a finished * {@link Trace}: resolving the collector, serializing the streaming emit chain, wiring `onProgress`, the abort * flush, and folding a collector failure into a diagnostic — then running the command. It performs no stdout / - * `process.exit`; it returns a {@link ProcessingResult} the OutputManager + adapter consume. The dynamic command + * `process.exit`; it returns a {@link ProcessingResult} the OutputManager + adapter consume. The run command * is injected (so tests can drive it with a fake tracer); the static commands are cheap to construct per call. */ export class ProcessingManager { constructor( - private readonly dynamic: DynamicCommand = new DynamicCommand(new Tracer(), new S3ArtifactStore()), + private readonly runCommand: RunCommand = new RunCommand(new Tracer(), new S3ArtifactStore()), ) {} /** - * Run a dynamic breakpoint trace. Streams to the collector (an explicit --emit / TRACE_COLLECTOR_URL wins, + * Run a breakpoint trace. Streams to the collector (an explicit --emit / TRACE_COLLECTOR_URL wins, * else a locally-running dashboard is auto-discovered). Emits are serialized through one promise chain so a * slow POST can't land a stale envelope after a newer one. On a run that throws, the terminal envelope was * already emitted via onProgress — flush the chain so that POST lands, then throw {@link EngineAbortError}. */ - async runDynamic(normalized: NormalizedRun): Promise { + async runTrace(normalized: NormalizedRun): Promise { const collector = await Collector.resolve(normalized.emit); let emitChain: Promise = Promise.resolve(); // Only the count and the most recent failure are surfaced, so keep just those — not every failed result. @@ -74,7 +74,7 @@ export class ProcessingManager { let trace: Trace; try { - ({ trace } = await this.dynamic.run({ + ({ trace } = await this.runCommand.run({ ...normalized.request, ...(emitToCollector ? { onProgress: (intermediateTrace: Trace) => emitToCollector(intermediateTrace.toJSON()) } : {}), })); @@ -82,7 +82,7 @@ export class ProcessingManager { // The run threw. It already emitted a TERMINAL envelope via onProgress that clears the dashboard's // "running" session — flush the chain so that POST actually lands, then surface the failure to the caller. if (emitToCollector) await emitChain; - log.error("dynamic trace aborted before completion", { code: Code.ENGINE_FATAL, err: error }); + log.error("run aborted before completion", { code: Code.ENGINE_FATAL, err: error }); throw new EngineAbortError(error); } @@ -97,7 +97,7 @@ export class ProcessingManager { trace.diagnostics.push(Diagnostic.warn(Code.EMIT, emitFailureMessage(collector, emitFailureCount, lastEmitFailure))); } } - return { trace, render: () => this.dynamic.render(trace) }; + return { trace, render: () => this.runCommand.render(trace) }; } async runGraph(request: GraphRequest): Promise { @@ -132,7 +132,7 @@ export class ProcessingManager { /** * Forward a finished STATIC envelope to a collector — explicit-only (`TRACE_COLLECTOR_URL`), never the - * dynamic auto-discovery: a static analysis carries no sessionId, so the session dashboard can't ingest it. + * run command's auto-discovery: a static analysis carries no sessionId, so the session dashboard can't ingest it. * Called by the adapter AFTER the output gate so the collector sees the same gated envelope the CLI printed. */ async forwardStatic(trace: Trace): Promise { diff --git a/src/io/descriptors.ts b/src/io/descriptors.ts index 845653c..0241f3c 100644 --- a/src/io/descriptors.ts +++ b/src/io/descriptors.ts @@ -1,5 +1,5 @@ import type { Trace } from "../domain/Trace.js"; -import type { DynamicRequest } from "../cli/commands/DynamicCommand.js"; +import type { RunRequest } from "../cli/commands/RunCommand.js"; import type { GraphRequest } from "../cli/commands/GraphCommand.js"; import type { DepsRequest } from "../cli/commands/DepsCommand.js"; import type { ComplexityRequest } from "../cli/commands/ComplexityCommand.js"; @@ -71,13 +71,13 @@ export interface RawSymbolsInput { // ── normalized input (InputManager → ProcessingManager) ──────────────────────────────────────────────────── -/** The dynamic-run request minus the live streaming sink — ProcessingManager owns and injects `onProgress`. */ -export type RunRequest = Omit; +/** The run request minus the live streaming sink — ProcessingManager owns and injects `onProgress`. */ +export type NormalizedRunRequest = Omit; -/** The dynamic command's normalized form: the validated request plus the resolved collector policy (the raw +/** The run command's normalized form: the validated request plus the resolved collector policy (the raw * `--emit` value; `Collector.resolve` turns it into an explicit target or auto-discovers a local dashboard). */ export interface NormalizedRun { - request: RunRequest; + request: NormalizedRunRequest; emit?: string | null; } diff --git a/src/io/index.ts b/src/io/index.ts index da5c97a..92145f8 100644 --- a/src/io/index.ts +++ b/src/io/index.ts @@ -12,5 +12,5 @@ export { OutputManager, condense } from "./OutputManager.js"; export { OutputValidator } from "./OutputValidator.js"; export type { RawRunInput, RawGraphInput, RawDepsInput, RawComplexityInput, RawSymbolsInput, - NormalizedRun, RunRequest, ProcessingResult, OutputOptions, OutputResult, FileOutput, OutputLog, + NormalizedRun, NormalizedRunRequest, ProcessingResult, OutputOptions, OutputResult, FileOutput, OutputLog, } from "./descriptors.js"; diff --git a/src/shared/codes.ts b/src/shared/codes.ts index e1af794..77863a7 100644 --- a/src/shared/codes.ts +++ b/src/shared/codes.ts @@ -20,7 +20,7 @@ export const Code = { /** the outgoing envelope failed its own JSON-Schema validation (a structural bug — should never ship) */ SCHEMA: "E_SCHEMA", - // ── dynamic trace (`run`) ──────────────────────────────────────────────── + // ── run (breakpoint trace) ─────────────────────────────────────────────── /** the tracer engine threw — no usable trace was produced */ ENGINE_FATAL: "ENGINE_FATAL", /** a Chrome journey step (goto/click/type/…) failed */ diff --git a/src/shared/runTool.ts b/src/shared/runTool.ts index ab44ecb..d3abe25 100644 --- a/src/shared/runTool.ts +++ b/src/shared/runTool.ts @@ -15,7 +15,7 @@ export interface ToolRun { * runTool — spawn a backing CLI tool, capture its output, and NEVER throw. A missing binary (ENOENT), a * timeout, or a non-zero exit all resolve to `{ ok: false, error }` so the calling command can turn the * failure into an error diagnostic on a still-well-formed Trace — the same "an agent always gets a Trace" - * contract the dynamic/graph commands honour. The shared seam for the static analyses (madge/lizard/tree-sitter). + * contract the run/graph commands honour. The shared seam for the static analyses (madge/lizard/tree-sitter). * * Output is captured to temp FILES, not pipes. A child that prints a large payload and then calls * `process.exit()` (madge does this) truncates piped stdout to the OS pipe buffer (~64KB on macOS) — the diff --git a/test/io-input.test.js b/test/io-input.test.js index 1290192..53c5dfd 100644 --- a/test/io-input.test.js +++ b/test/io-input.test.js @@ -1,7 +1,7 @@ // InputManager tests — the input tier. It accepts a transport-neutral parsed object (NOT argv), throws a // structured InputError (never process.exit) on bad input, and normalizes the rest into a typed request. // Pure + synchronous: no engine, no network. Covers the guards, the SECURITY-critical step redaction, and the -// target/args normalization that used to be inlined in Cli.#runDynamic. +// target/args normalization that used to be inlined in Cli.#runTrace. import "reflect-metadata"; import { test } from "node:test"; import assert from "node:assert/strict"; @@ -58,7 +58,7 @@ test("acceptRun rejects an unknown step verb as an invalid-step InputError carry assert.ok(error.problems.length >= 1); }); -test("acceptRun surfaces a DynamicInput violation (out-of-range port) as an invalid-input InputError", () => { +test("acceptRun surfaces a RunInput violation (out-of-range port) as an invalid-input InputError", () => { const error = thrown(() => im.acceptRun(runRaw({ node: "70000", curl: "c" }))); assert.ok(error instanceof InputError); assert.match(error.message, /^invalid input —/); diff --git a/test/io-processing.test.js b/test/io-processing.test.js index 9b211f5..32d572f 100644 --- a/test/io-processing.test.js +++ b/test/io-processing.test.js @@ -1,6 +1,6 @@ // ProcessingManager tests — the orchestration tier. Owns the collector wiring (resolve + serialized emit chain // + onProgress + the failure→diagnostic fold), the abort path (EngineAbortError), the static collector forward, -// and the command render thunks. The DynamicCommand is injected (a fake, like dynamic-diagnostics.test.js) and +// and the command render thunks. The RunCommand is injected (a fake, like run-diagnostics.test.js) and // Collector's static helpers are stubbed so no real network is touched. `node --test` isolates each file in its // own process, so these stubs never leak into other suites. import "reflect-metadata"; @@ -17,36 +17,36 @@ const mkTrace = () => new Trace({ meta: new TraceMeta({ at: "2026-01-01T00:00:00.000Z" }), data: new TraceData({ events: [] }), ok: true, }); -// A duck-typed DynamicCommand: `run` is the injected behavior, `render` proves the thunk is bound to it. -const fakeDynamic = (run) => ({ run, render: (trace) => `rendered:${trace.command}` }); +// A duck-typed RunCommand: `run` is the injected behavior, `render` proves the thunk is bound to it. +const fakeRun = (run) => ({ run, render: (trace) => `rendered:${trace.command}` }); -test("runDynamic: with no collector → returns the trace and a render thunk bound to the command", async () => { +test("runTrace: with no collector → returns the trace and a render thunk bound to the command", async () => { Collector.resolve = async () => null; // nothing configured, nothing discovered const trace = mkTrace(); - const pm = new ProcessingManager(fakeDynamic(async () => ({ trace }))); - const result = await pm.runDynamic({ request: { target: "node" }, emit: null }); + const pm = new ProcessingManager(fakeRun(async () => ({ trace }))); + const result = await pm.runTrace({ request: { target: "node" }, emit: null }); assert.equal(result.trace, trace); assert.equal(result.render(), "rendered:run.node"); }); -test("runDynamic: a throwing run rejects with EngineAbortError carrying the cause", async () => { +test("runTrace: a throwing run rejects with EngineAbortError carrying the cause", async () => { Collector.resolve = async () => null; - const pm = new ProcessingManager(fakeDynamic(async () => { throw new Error("attach failed"); })); + const pm = new ProcessingManager(fakeRun(async () => { throw new Error("attach failed"); })); await assert.rejects( - () => pm.runDynamic({ request: { target: "node" }, emit: null }), + () => pm.runTrace({ request: { target: "node" }, emit: null }), (error) => error instanceof EngineAbortError && error.code === "ENGINE_FATAL" && /attach failed/.test(error.message), ); }); -test("runDynamic: a failing collector emit folds into an EMIT warn diagnostic on the returned trace", async () => { +test("runTrace: a failing collector emit folds into an EMIT warn diagnostic on the returned trace", async () => { const emitted = []; Collector.resolve = async () => "http://collector.test"; Collector.emit = async (_url, envelope) => { emitted.push(envelope); return { ok: false, status: 400, body: "bad envelope" }; }; const trace = mkTrace(); // The fake streams one running envelope (onProgress), then returns the final one — both POSTs fail. - const pm = new ProcessingManager(fakeDynamic(async (opts) => { opts.onProgress?.(trace); return { trace }; })); + const pm = new ProcessingManager(fakeRun(async (opts) => { opts.onProgress?.(trace); return { trace }; })); - const result = await pm.runDynamic({ request: { target: "node" }, emit: null }); + const result = await pm.runTrace({ request: { target: "node" }, emit: null }); assert.ok(emitted.length >= 1, "the collector should have received at least one envelope"); const emitDiag = result.trace.diagnostics.find((d) => d.code === "EMIT_FAILED"); assert.ok(emitDiag && emitDiag.level === "warn", "a failed emit must surface as an EMIT warn diagnostic"); diff --git a/test/io-validators.test.js b/test/io-validators.test.js index a89397f..f163bf8 100644 --- a/test/io-validators.test.js +++ b/test/io-validators.test.js @@ -29,8 +29,8 @@ test("InputValidator.guardRunTrigger: requires a breakpoint first, then the targ assert.equal(iv.guardRunTrigger(runRaw({ curl: "c" }), { target: TargetKind.Node, isChrome: false, steps: [] }), undefined); }); -test("InputValidator.validateDynamic: an out-of-range port throws InputError carrying the problems", () => { - const error = thrown(() => iv.validateDynamic({ target: TargetKind.Node, port: 70000, breakpoints: ["a:1"], exprs: [], steps: [] })); +test("InputValidator.validateRun: an out-of-range port throws InputError carrying the problems", () => { + const error = thrown(() => iv.validateRun({ target: TargetKind.Node, port: 70000, breakpoints: ["a:1"], exprs: [], steps: [] })); assert.ok(error instanceof InputError); assert.match(error.message, /^invalid input —/); assert.ok(error.problems.length >= 1); diff --git a/test/journey.test.js b/test/journey.test.js index 1274f39..1b9997e 100644 --- a/test/journey.test.js +++ b/test/journey.test.js @@ -1,4 +1,4 @@ -// JourneyRunner.parseStep — the pure `--step` parser DynamicCommand feeds the Chrome journey. The runner's +// JourneyRunner.parseStep — the pure `--step` parser RunCommand feeds the Chrome journey. The runner's // CDP driving needs a live Chrome (not exercised here); this pins the parsing contract. Run via `npm test`. import "reflect-metadata"; // StepResult uses class-validator decorators (the domain loads this via Trace.ts) import { test } from "node:test"; diff --git a/test/dynamic-diagnostics.test.js b/test/run-diagnostics.test.js similarity index 82% rename from test/dynamic-diagnostics.test.js rename to test/run-diagnostics.test.js index dca0dc7..a03bd17 100644 --- a/test/dynamic-diagnostics.test.js +++ b/test/run-diagnostics.test.js @@ -1,11 +1,11 @@ -// DynamicCommand diagnostics: a trace run must make its failures legible in the envelope (not just stderr), +// RunCommand diagnostics: a trace run must make its failures legible in the envelope (not just stderr), // and a thrown run must emit a TERMINAL envelope so the dashboard's "running" session resolves instead of // hanging forever. Injects a fake tracer so we exercise the envelope/diagnostic logic without a real CDP target. import "reflect-metadata"; import { test } from "node:test"; import assert from "node:assert/strict"; -import { DynamicCommand } from "../dist/cli/commands/DynamicCommand.js"; +import { RunCommand } from "../dist/cli/commands/RunCommand.js"; import { TargetKind } from "../dist/domain/Target.js"; const fakeTracer = (behavior) => ({ @@ -17,7 +17,7 @@ const nodeCapture = (over = {}) => ({ target: TargetKind.Node, trigger: "curl lo test("a thrown run emits a TERMINAL envelope (running cleared, ENGINE_FATAL) so the dashboard resolves", async () => { const seen = []; - const cmd = new DynamicCommand(fakeTracer(() => { throw new Error("attach failed: ECONNREFUSED"); })); + const cmd = new RunCommand(fakeTracer(() => { throw new Error("attach failed: ECONNREFUSED"); })); await assert.rejects( cmd.run({ target: TargetKind.Node, port: 9229, onProgress: (t) => seen.push(t) }), @@ -37,14 +37,14 @@ test("a thrown run emits a TERMINAL envelope (running cleared, ENGINE_FATAL) so }); test("a captured fatal (no throw) yields ok:false + an ENGINE_FATAL diagnostic in the envelope", async () => { - const cmd = new DynamicCommand(fakeTracer(() => nodeCapture({ fatal: "debugger disconnected" }))); + const cmd = new RunCommand(fakeTracer(() => nodeCapture({ fatal: "debugger disconnected" }))); const { trace } = await cmd.run({ target: TargetKind.Node, port: 9229 }); assert.equal(trace.ok, false); assert.ok(trace.diagnostics.some((d) => d.code === "ENGINE_FATAL")); }); test("a clean empty node trace stays ok:true and not running (no false alarms)", async () => { - const cmd = new DynamicCommand(fakeTracer(() => nodeCapture())); + const cmd = new RunCommand(fakeTracer(() => nodeCapture())); const { trace } = await cmd.run({ target: TargetKind.Node, port: 9229 }); assert.equal(trace.ok, true); assert.equal(trace.meta.running, undefined, "the final envelope is not flagged running"); diff --git a/ui/app/page.tsx b/ui/app/page.tsx index d71756f..d7bd62e 100644 --- a/ui/app/page.tsx +++ b/ui/app/page.tsx @@ -381,7 +381,7 @@ export default function Home() {
Select a session, or run
- trace dynamic … --emit http://localhost:4000 + trace run … --emit http://localhost:4000
) : (