@diffci.com/diffci 0.1.12 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,10 +5,10 @@
5
5
  [![npm provenance](https://img.shields.io/badge/npm-provenance-blue)](https://docs.npmjs.com/generating-provenance-statements)
6
6
  [![GitHub Action](https://img.shields.io/badge/action-DiffCI%2FDiffCI.com%40v0.1.4-blue)](https://github.com/DiffCI/DiffCI.com)
7
7
 
8
- **Find test-selection opportunities in your CI before changing what it runs.** DiffCI analyzes a
9
- commit's changes and dependency graph, then reports which test files it would select, why it falls
10
- back to a full run, and whether it can propose a test command. The `observe` command and Action are observation-only;
11
- the opt-in `pilot` and `verify-savings` commands execute tests.
8
+ **Find test-selection opportunities in your CI before changing what it runs.** DiffCI analyzes a
9
+ commit's changes and dependency graph, then reports which test files it would select, why it falls
10
+ back to a full run, and whether it can propose a test command. `check` also runs paired full and
11
+ selected commands when it can infer them. The `observe` command and Action remain observation-only.
12
12
 
13
13
  **For AI coding agents:** DiffCI gives Codex, Claude Code, Cursor, GitHub Copilot, Grok, and similar
14
14
  tools a safe default CI/CD validation command.
@@ -25,22 +25,23 @@ For AI coding agents, seed the repository with default instructions:
25
25
  npx @diffci.com/diffci@latest init
26
26
  ```
27
27
 
28
- For the fastest self-serve runtime pilot, run one paired check from the repository root:
29
-
30
- ```bash
31
- npx @diffci.com/diffci@latest pilot --full "npm test"
32
- ```
28
+ For a self-serve runtime comparison, run one command from the repository root:
29
+
30
+ ```bash
31
+ npx @diffci.com/diffci@latest check
32
+ ```
33
33
 
34
34
  On Windows PowerShell, quote the package name:
35
35
 
36
36
  ```powershell
37
- npx '@diffci.com/diffci@latest' pilot --full "npm test"
38
- ```
39
-
40
- This executes the full and selected commands sequentially, and writes `diffci-observe.json`,
41
- `diffci-savings.json`, and `diffci-savings.md` to a sibling `diffci-output` folder outside the checkout.
42
- The commands you supply may create files or otherwise change the checkout. One paired run is preliminary
43
- timing evidence; repeat comparisons and account for cache effects before claiming savings.
37
+ npx '@diffci.com/diffci@latest' check
38
+ ```
39
+
40
+ `check` infers a full test command, runs it and DiffCI's selected command, and prints gross test-time
41
+ and net runtime changes when both pass. It writes observation and savings reports outside the checkout.
42
+ Test commands may create files or change the checkout. One paired run is preliminary evidence; repeat
43
+ comparisons and account for cache effects before claiming CI savings. On a full-validation fallback,
44
+ `check` runs the full command once and reports 0% reduction.
44
45
 
45
46
  **Upgrade from 0.1.3:** tests excluded by a source-only `tsconfig.json` could be discovered without
46
47
  their dependency edges, producing an incomplete selection. This is fixed in **0.1.4**. Revalidate
@@ -53,8 +54,9 @@ comparison, add `--base <base-sha> --head <head-sha>`. DiffCI prints the selecti
53
54
  and the path to a JSON report outside your checkout. `REFUSED` or `ERROR` is not a successful analysis;
54
55
  check the reported status even when the command exits successfully. See the
55
56
  [support matrix](docs/language-support.md) for setup requirements and supported workloads.
56
- `check` is an agent-friendly alias for `observe --no-send`: it runs no tests, changes no CI behavior,
57
- and sends nothing by default. See [`docs/ai-agents.md`](docs/ai-agents.md) for Claude Code, Codex,
57
+ `check` runs inferred full and selected commands in the checkout and sends nothing by default.
58
+ The commands may write generated files. Use `observe --no-send` for analysis without execution.
59
+ See [`docs/ai-agents.md`](docs/ai-agents.md) for Claude Code, Codex,
58
60
  Cursor, GitHub Copilot, and similar tools.
59
61
 
60
62
  **Measured example:** a controlled Cal.com replay showed **44.2% net reduction in a job-equivalent
@@ -62,8 +64,8 @@ install + pretest + test workload**, including analysis overhead. This is one sa
62
64
  not Cal.com's production savings or a prediction for your repository.
63
65
  [Read the timings and method](docs/research/2026-08-24-calcom-execution-observability/11-frozen-identity-and-complete-job-savings.md).
64
66
 
65
- Selection counts alone do not establish runtime savings. Observation mode measures neither the
66
- selected test execution nor realized savings.
67
+ Selection counts alone do not establish runtime savings. `check` reports a measured percentage only
68
+ when both commands pass; `observe` does not execute tests.
67
69
 
68
70
  For an advanced paired runtime check, you can still run `observe` first and then run `verify-savings`
69
71
  against the observation report. It compares your normal full command with
@@ -106,7 +108,7 @@ DiffCI's hosted service requires an explicitly configured endpoint and token.
106
108
  | [`@diffci.com/diffci`](https://www.npmjs.com/package/@diffci.com/diffci) | Try `observe` locally, run an opt-in runtime pilot, or install the GitHub Action from this repository | Published npm CLI and Action |
107
109
 
108
110
  The CLI bundles a pinned revision of the [Core engine](https://github.com/DiffCI/core) from GitHub. Users install only
109
- `@diffci.com/diffci`; the `pilot` command above stays the same. Core performs Git analysis,
111
+ `@diffci.com/diffci`; the `check` command above uses it directly. Core performs Git analysis,
110
112
  dependency graphs, impact, path baseline, and selected-command planning. The report format and
111
113
  non-interfering GitHub Action remain in this repository. See
112
114
  [`docs/package-relationship.md`](docs/package-relationship.md) for the source relationship.
@@ -1,20 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `diffci` - the client-side command (Phase 02, 2026-08-26).
3
+ * `diffci` - the client-side command.
4
4
  *
5
- * This is the binary a third-party repository runs in its own CI. Everything it does is observation:
6
- * it reads a checkout, writes one JSON report to a path outside that checkout, prints a summary,
7
- * optionally sends that report to DiffCI, and exits 0. There is no mode in this file that runs, skips,
8
- * cancels or re-orders anything, and the absence is deliberate - the seven-day Phase 02 criterion is
9
- * "CI byte-identical", and a flag that could change what CI runs is a flag that will eventually be set
10
- * by accident.
5
+ * `observe` and the GitHub Action remain observation-only. `check` measures a full command and a
6
+ * selected command when both can be inferred; it never changes what required CI executes.
11
7
  *
12
8
  * Sending is opt-in and off unless both an API URL and a token are supplied (Phase 03). Without them
13
9
  * the observer is exactly what Phase 02 shipped: a local analysis whose output never leaves the runner.
14
10
  *
15
11
  * Commands:
16
12
  * init seed a repository with AI-agent instructions for using DiffCI
17
- * check agent-friendly alias for observe --no-send
13
+ * check analysis plus automatic paired runtime measurement
18
14
  * observe analyse the checkout and write an observation report
19
15
  * verify-savings run a paired full-versus-selected timing check
20
16
  * verify-workflow check that a DiffCI job in this repository's workflows cannot affect other jobs
@@ -32,8 +28,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
32
28
  import { tmpdir } from "node:os";
33
29
  import { basename, dirname, join, resolve } from "node:path";
34
30
  import { observe, isInsideRepository } from "./observe.js";
31
+ import { inferFullCommand } from "./full-command.js";
35
32
  import { submitObservation } from "./submit.js";
36
- import { formatVerifySavingsSummary, runVerifySavings, writeVerifySavingsReport } from "./verify-savings.js";
33
+ import { formatVerifySavingsSummary, measureCommand, runVerifySavings, writeVerifySavingsReport } from "./verify-savings.js";
37
34
  import { auditWorkflows, isNonInterfering } from "./workflow-guard.js";
38
35
  function parseArgs(argv) {
39
36
  const args = argv.slice(2);
@@ -111,13 +108,16 @@ Default command:
111
108
  npx @diffci.com/diffci@latest check
112
109
  \`\`\`
113
110
 
114
- What this does:
115
-
116
- - analyzes the current checkout and commit range;
117
- - writes a JSON report outside the repository by default;
118
- - prints selected tests, fallback reasons, and proposed commands;
119
- - sends nothing unless an endpoint and token are explicitly configured;
120
- - runs, skips, cancels, or reorders no tests.
111
+ What this does:
112
+
113
+ - analyzes the current checkout and commit range;
114
+ - runs the repository's inferred full test command and DiffCI's selected command when safe to compare;
115
+ - prints measured paired runtime when both commands pass;
116
+ - writes reports outside the repository and sends nothing;
117
+ - does not skip, cancel, or reorder required CI tests.
118
+
119
+ Test commands may write generated files to the checkout. Use \`observe --no-send\` when analysis
120
+ without test execution is needed.
121
121
 
122
122
  If DiffCI reports \`REFUSED\` or \`ERROR\`, treat that as an installation or analysis issue and continue with the repository's normal test commands. If DiffCI reports a selected command, prefer using it as evidence for what changed, not as permission to skip required CI.
123
123
  `;
@@ -132,7 +132,7 @@ Before proposing, committing, or opening pull requests, run:
132
132
  npx @diffci.com/diffci@latest check
133
133
  \`\`\`
134
134
 
135
- DiffCI is observation-only by default. It analyzes the change, writes a report outside the checkout, and sends nothing without explicit configuration. Do not use a DiffCI selection as permission to skip required project CI.
135
+ DiffCI check analyzes the change and runs full and selected test commands when it can infer them. It sends nothing. Test commands may write generated files. Use observe --no-send for analysis without execution. Required project CI remains authoritative.
136
136
  `;
137
137
  const GITHUB_COPILOT_INSTRUCTIONS = `# Repository CI/CD Validation
138
138
 
@@ -142,12 +142,12 @@ Use DiffCI as the default change-aware CI/CD validation lens:
142
142
  npx @diffci.com/diffci@latest check
143
143
  \`\`\`
144
144
 
145
- DiffCI's default agent command is observation-only. It should inform which tests and CI paths are relevant, while the repository's required checks remain authoritative.
145
+ DiffCI check analyzes the change and runs inferred full and selected test commands to measure a paired runtime. It sends nothing; required repository checks remain authoritative.
146
146
  `;
147
147
  const DIFFCI_CONFIG = `{
148
148
  "$schema": "https://diffci.com/schemas/diffci.config.schema.json",
149
149
  "agentDefaultCommand": "npx @diffci.com/diffci@latest check",
150
- "mode": "observe",
150
+ "mode": "check",
151
151
  "sendReports": false
152
152
  }
153
153
  `;
@@ -167,7 +167,7 @@ jobs:
167
167
  - uses: actions/setup-node@v4
168
168
  with:
169
169
  node-version: 22
170
- - run: npx @diffci.com/diffci@${version} check
170
+ - run: npx @diffci.com/diffci@${version} observe --no-send
171
171
  `;
172
172
  }
173
173
  function writeInitFile(repoPath, relativePath, content, force) {
@@ -205,7 +205,7 @@ function formatFinding(finding) {
205
205
  const where = finding.job ? `${finding.workflow}#${finding.job}` : finding.workflow;
206
206
  return ` [${finding.severity}] ${finding.code} (${where})\n ${finding.message}`;
207
207
  }
208
- function summarise(report) {
208
+ function summarise(report, executionFollows = false) {
209
209
  const lines = [];
210
210
  lines.push(`DiffCI observation: ${report.status} (${report.stage})`);
211
211
  if (report.reason)
@@ -222,6 +222,20 @@ function summarise(report) {
222
222
  lines.push(` Vue suite: ${result.vueScope.packageRoot} (${result.vueScope.testConfig})`);
223
223
  lines.push(` selection: ${result.selectedTests.length}/${result.totalTestCount} test files, from ${result.changedFileCount} changed file(s)`);
224
224
  lines.push(` comparator: a simple path-rule CI would have run ${result.pathBaseline.mode === "FULL" ? "everything" : `${result.pathBaseline.selectedTestCount} test file(s)`}`);
225
+ if (result.mode === "FULL") {
226
+ lines.push(" planned reduction: 0% test files (full validation required); runtime savings unmeasured");
227
+ }
228
+ else if (result.commandRefusalReason || result.proposedCommands.length === 0) {
229
+ lines.push(" planned reduction: unavailable (no runnable selected command); runtime savings unmeasured");
230
+ }
231
+ else if (result.totalTestCount > 0) {
232
+ const avoided = Math.max(0, result.totalTestCount - result.selectedTests.length);
233
+ const percent = (avoided / result.totalTestCount) * 100;
234
+ lines.push(` planned reduction: ${avoided}/${result.totalTestCount} test files (${percent.toFixed(1)}%) vs full; runtime savings unmeasured`);
235
+ }
236
+ else {
237
+ lines.push(" planned reduction: unavailable (no discovered tests); runtime savings unmeasured");
238
+ }
225
239
  lines.push(` graph: ${result.graph.nodes} nodes, confidence ${result.graph.effectiveConfidence ?? result.graph.confidence}`);
226
240
  if (result.fallbackReasons.length > 0) {
227
241
  lines.push(` fallback: ${result.fallbackReasons.join("; ")}`);
@@ -234,16 +248,93 @@ function summarise(report) {
234
248
  lines.push(" blind spot: this repository declares a test framework and DiffCI discovered none of its tests");
235
249
  }
236
250
  }
237
- lines.push(` non-interference: worktree ${report.nonInterference.worktreeUnchanged ? "unchanged" : "CHANGED - report this"}, report written ${report.nonInterference.reportWrittenOutsideRepository ? "outside" : "INSIDE"} the checkout`);
251
+ lines.push(` non-interference${executionFollows ? " (analysis phase)" : ""}: worktree ${report.nonInterference.worktreeUnchanged ? "unchanged" : "CHANGED - report this"}, report written ${report.nonInterference.reportWrittenOutsideRepository ? "outside" : "INSIDE"} the checkout`);
238
252
  const blocking = report.nonInterference.workflowFindings.filter((f) => f.severity === "BLOCKING");
239
253
  if (blocking.length > 0) {
240
254
  lines.push(` workflow: ${blocking.length} blocking finding(s) - this installation CAN affect other jobs:`);
241
255
  for (const finding of blocking)
242
256
  lines.push(formatFinding(finding));
243
257
  }
244
- lines.push(" DiffCI changed nothing: no test was run, skipped, cancelled or re-ordered by this step.");
258
+ lines.push(executionFollows
259
+ ? " Analysis phase complete; check runs test commands only when a valid comparison is available."
260
+ : " DiffCI changed nothing: no test was run, skipped, cancelled or re-ordered by this step.");
245
261
  return lines.join("\n");
246
262
  }
263
+ async function runCheck(flags, env) {
264
+ const repoPath = resolve(stringFlag(flags, "repo") ?? env.GITHUB_WORKSPACE ?? process.cwd());
265
+ const reportPath = resolve(stringFlag(flags, "out") ?? defaultReportPath(env));
266
+ const observationCode = await runObserve({ ...flags, out: reportPath, quiet: true, "no-send": true }, env);
267
+ if (observationCode !== 0)
268
+ return observationCode;
269
+ const observation = JSON.parse(readFileSync(reportPath, "utf8"));
270
+ const print = (message) => { if (flags.quiet !== true && flags.json !== true)
271
+ console.log(message); };
272
+ print(summarise(observation, true));
273
+ print(` observation report: ${reportPath}`);
274
+ if (observation.status !== "OBSERVED" || !observation.result) {
275
+ print(" timing: unavailable because analysis did not complete");
276
+ if (flags.json === true)
277
+ console.log(JSON.stringify({ observation, timing: null }, null, 2));
278
+ return flags["fail-on-error"] === true ? 1 : 0;
279
+ }
280
+ const checkedOutHead = execFileSync("git", ["rev-parse", "HEAD"], { cwd: repoPath, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
281
+ if (observation.commitRange?.headSha !== checkedOutHead) {
282
+ const reason = "the analyzed head is not checked out; timing would execute a different revision";
283
+ print(` timing: unavailable (${reason})`);
284
+ if (flags.json === true)
285
+ console.log(JSON.stringify({ observation, timing: null, reason }, null, 2));
286
+ return 1;
287
+ }
288
+ const inferred = inferFullCommand(repoPath);
289
+ if (!inferred.command) {
290
+ print(` timing: unavailable (${inferred.reason})`);
291
+ if (flags.json === true)
292
+ console.log(JSON.stringify({ observation, timing: null, reason: inferred.reason }, null, 2));
293
+ return 0;
294
+ }
295
+ print(` full command: ${inferred.command} (${inferred.reason})`);
296
+ const timeoutMs = numberFlag(flags, "timeout-ms") ?? 30 * 60 * 1000;
297
+ const tailBytes = numberFlag(flags, "tail-bytes") ?? 12_000;
298
+ const savingsPath = reportPath.endsWith(".json") ? reportPath.slice(0, -5) + "-savings.json" : reportPath + ".savings.json";
299
+ const markdownPath = savingsPath.replace(/\.json$/, ".md");
300
+ if (observation.result.mode === "FULL") {
301
+ print(" running full validation...");
302
+ const full = measureCommand(inferred.command, { cwd: repoPath, timeoutMs, tailBytes });
303
+ writeFileSync(savingsPath, `${JSON.stringify({ schema: "diffci.fullValidation.v1", observationReportPath: reportPath, full }, null, 2)}\n`, "utf8");
304
+ print(full.exitCode === 0 && !full.timedOut
305
+ ? `DiffCI check: full validation passed in ${(full.wallMs / 1000).toFixed(2)}s; 0% measured reduction for this commit`
306
+ : `DiffCI check: full validation failed (exit ${String(full.exitCode)}); savings unavailable`);
307
+ print(` execution report: ${savingsPath}`);
308
+ if (flags.json === true)
309
+ console.log(JSON.stringify({ observation, full }, null, 2));
310
+ return full.exitCode === 0 && !full.timedOut ? 0 : 1;
311
+ }
312
+ if (observation.result.commandRefusalReason || observation.result.proposedCommands.length !== 1) {
313
+ const reason = observation.result.commandRefusalReason ?? "the selection has no single runnable command";
314
+ print(` timing: unavailable (${reason})`);
315
+ if (flags.json === true)
316
+ console.log(JSON.stringify({ observation, timing: null, reason }, null, 2));
317
+ return 0;
318
+ }
319
+ print(" running full and selected validation...");
320
+ const savings = runVerifySavings({
321
+ full: inferred.command,
322
+ selectedFromReport: reportPath,
323
+ out: savingsPath,
324
+ markdown: markdownPath,
325
+ label: stringFlag(flags, "label") ?? basename(repoPath),
326
+ cwd: repoPath,
327
+ timeoutMs,
328
+ tailBytes,
329
+ });
330
+ writeVerifySavingsReport(savings, { out: savingsPath, markdown: markdownPath });
331
+ print(formatVerifySavingsSummary(savings));
332
+ print(` savings report: ${savingsPath}`);
333
+ print(` markdown: ${markdownPath}`);
334
+ if (flags.json === true)
335
+ console.log(JSON.stringify({ observation, savings }, null, 2));
336
+ return savings.comparison.fullCommandSucceeded && savings.comparison.selectedCommandSucceeded ? 0 : 1;
337
+ }
247
338
  async function runObserve(flags, env) {
248
339
  const repoPath = resolve(typeof flags.repo === "string" ? flags.repo : env.GITHUB_WORKSPACE ?? process.cwd());
249
340
  const reportPath = resolve(typeof flags.out === "string" ? flags.out : defaultReportPath(env));
@@ -445,6 +536,14 @@ async function runPilot(flags, env) {
445
536
  console.log("DiffCI pilot stopped before timing because observation did not produce a selectable report.");
446
537
  return 1;
447
538
  }
539
+ if (observation.result?.mode === "FULL") {
540
+ console.log("DiffCI pilot: 0% planned test-file reduction on this commit (full validation required). No paired timing was run.");
541
+ return 0;
542
+ }
543
+ if (!observation.result?.proposedCommands.length || observation.result.commandRefusalReason) {
544
+ console.log("DiffCI pilot: no runnable selected command. No paired timing was run.");
545
+ return 1;
546
+ }
448
547
  const savings = runVerifySavings({
449
548
  full,
450
549
  selectedFromReport: observationPath,
@@ -462,12 +561,12 @@ async function runPilot(flags, env) {
462
561
  console.log(` markdown: ${markdownPath}`);
463
562
  return savings.comparison.fullCommandSucceeded && savings.comparison.selectedCommandSucceeded ? 0 : 1;
464
563
  }
465
- const USAGE = `diffci - observation-only change-aware CI analysis
564
+ const USAGE = `diffci - change-aware CI analysis and paired timing
466
565
 
467
566
  Usage:
468
567
  diffci init [--repo <path>] [--workflow] [--force]
469
- diffci check [--repo <path>] [--out <file>] [--base <sha> --head <sha>]
470
- [--redact-paths] [--json] [--quiet] [--fail-on-error]
568
+ diffci check [--repo <path>] [--out <file>] [--base <sha> --head <sha>]
569
+ [--redact-paths] [--json] [--quiet] [--fail-on-error] [--timeout-ms <ms>]
471
570
  diffci pilot --full <command> [--repo <path>] [--out-dir <dir>] [--label <name>]
472
571
  diffci observe [--repo <path>] [--out <file>] [--base <sha> --head <sha>]
473
572
  [--redact-paths] [--json] [--quiet] [--fail-on-error]
@@ -479,9 +578,9 @@ Usage:
479
578
  diffci version
480
579
 
481
580
  init writes AGENTS.md, CLAUDE.md, Cursor rules, Copilot instructions, and diffci.config.json.
482
- check is the default AI-agent command: it is observe with sending disabled.
581
+ check analyzes the change, runs inferred full and selected commands, and shows measured savings.
483
582
  pilot runs observe and verify-savings together, writing reports to ../diffci-output by default.
484
- observe analyses the checkout and writes one JSON report. It runs nothing and changes nothing.
583
+ observe analyses the checkout and writes one JSON report. It runs nothing and changes nothing.
485
584
  verify-savings runs both commands and reports measured paired runtime; it is an opt-in pilot command.
486
585
  verify-workflow checks that the job running DiffCI cannot affect any other job, and exits 1 if it can.
487
586
 
@@ -502,7 +601,7 @@ async function main() {
502
601
  process.exitCode = runInit(flags, env);
503
602
  return;
504
603
  case "check":
505
- process.exitCode = await runObserve({ ...flags, "no-send": true }, env);
604
+ process.exitCode = await runCheck(flags, env);
506
605
  return;
507
606
  case "pilot":
508
607
  process.exitCode = await runPilot(flags, env);
@@ -0,0 +1,46 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { readRepositoryConfig } from "@diffci.com/core/repo/repo-config";
4
+ /** Choose the repository's conventional full test command without executing anything. */
5
+ export function inferFullCommand(repoPath) {
6
+ if (existsSync(join(repoPath, "pom.xml"))) {
7
+ const config = readRepositoryConfig(repoPath);
8
+ if (config.configurationError)
9
+ return { reason: `DiffCI configuration error: ${config.configurationError}` };
10
+ const goal = config.maven?.goal ?? "test";
11
+ const profiles = config.maven?.profiles?.length ? ` -P ${config.maven.profiles.join(",")}` : "";
12
+ return {
13
+ command: `mvn${profiles} ${goal}`,
14
+ reason: config.maven ? "Maven goal and profiles from DiffCI configuration" : "Maven default goal: test; confirm this matches CI",
15
+ };
16
+ }
17
+ const packagePath = join(repoPath, "package.json");
18
+ if (existsSync(packagePath)) {
19
+ let pkg;
20
+ try {
21
+ pkg = JSON.parse(readFileSync(packagePath, "utf8"));
22
+ }
23
+ catch {
24
+ return { reason: "package.json could not be read" };
25
+ }
26
+ if (typeof pkg.scripts?.test !== "string" || !pkg.scripts.test.trim()) {
27
+ return { reason: "package.json has no test script" };
28
+ }
29
+ const manager = typeof pkg.packageManager === "string" ? pkg.packageManager.split("@")[0] : undefined;
30
+ if (manager === "pnpm" || manager === "yarn" || manager === "bun") {
31
+ return { command: `${manager} test`, reason: `package.json test script via ${manager}` };
32
+ }
33
+ if (manager && manager !== "npm")
34
+ return { reason: `unsupported package manager: ${manager}` };
35
+ if (existsSync(join(repoPath, "pnpm-lock.yaml")))
36
+ return { command: "pnpm test", reason: "package.json test script with pnpm lockfile" };
37
+ if (existsSync(join(repoPath, "yarn.lock")))
38
+ return { command: "yarn test", reason: "package.json test script with Yarn lockfile" };
39
+ if (existsSync(join(repoPath, "bun.lock")) || existsSync(join(repoPath, "bun.lockb")))
40
+ return { command: "bun test", reason: "package.json test script with Bun lockfile" };
41
+ return { command: "npm test", reason: "package.json test script" };
42
+ }
43
+ if (existsSync(join(repoPath, "go.mod")))
44
+ return { command: "go test ./...", reason: "root Go module" };
45
+ return { reason: "no supported full test command could be inferred" };
46
+ }
@@ -6,7 +6,7 @@ const SERVER_INFO = { name: "diffci-mcp", version: "0.1.0" };
6
6
  const tools = [
7
7
  {
8
8
  name: "diffci_check",
9
- description: "Run DiffCI's observation-only AI-agent validation command. It sends nothing by default and does not run, skip, cancel, or reorder tests.",
9
+ description: "Analyze the repository, run inferred full and selected test commands when available, and report paired runtime. Sends nothing to DiffCI Cloud.",
10
10
  inputSchema: {
11
11
  type: "object",
12
12
  properties: {
@@ -33,7 +33,7 @@ function resolveSelection(options) {
33
33
  throw new Error("--selected <command> or --selected-from-report <path> is required");
34
34
  return { command: options.selected, source: "manual" };
35
35
  }
36
- function measureCommand(command, options) {
36
+ export function measureCommand(command, options) {
37
37
  const startedAt = new Date().toISOString();
38
38
  const started = Date.now();
39
39
  const shellCommand = process.platform === "win32" ? "powershell.exe" : "sh";
@@ -69,6 +69,7 @@ export function buildVerifySavingsReport(input) {
69
69
  const overhead = input.analysisOverheadMs ?? 0;
70
70
  const netSelectedMs = input.selected.wallMs + overhead;
71
71
  const deltaMs = input.full.wallMs - netSelectedMs;
72
+ const grossPercentChange = input.full.wallMs > 0 ? ((input.full.wallMs - input.selected.wallMs) / input.full.wallMs) * 100 : 0;
72
73
  const percentChange = input.full.wallMs > 0 ? (deltaMs / input.full.wallMs) * 100 : 0;
73
74
  const fullCommandSucceeded = input.full.exitCode === 0 && !input.full.timedOut;
74
75
  const selectedCommandSucceeded = input.selected.exitCode === 0 && !input.selected.timedOut;
@@ -100,6 +101,7 @@ export function buildVerifySavingsReport(input) {
100
101
  selectedWallMs: input.selected.wallMs,
101
102
  netSelectedMs,
102
103
  deltaMs,
104
+ grossPercentChange,
103
105
  percentChange,
104
106
  selectedCommandSucceeded,
105
107
  fullCommandSucceeded,
@@ -144,6 +146,7 @@ ${!report.comparison.fullCommandSucceeded || !report.comparison.selectedCommandS
144
146
  | --- | ---: |
145
147
  | Full runtime | ${formatMs(report.comparison.fullWallMs)} |
146
148
  | Selected runtime | ${formatMs(report.comparison.selectedWallMs)} |
149
+ | Test execution change vs full | ${formatPercent(report.comparison.grossPercentChange)} |
147
150
  | DiffCI analysis overhead | ${overhead} |
148
151
  | Net selected runtime | ${formatMs(report.comparison.netSelectedMs)} |
149
152
  | Delta vs full | ${formatMs(report.comparison.deltaMs)} ${deltaLabel} |
@@ -191,7 +194,10 @@ export function formatVerifySavingsSummary(report) {
191
194
  (report.comparison.missedFailureSignal ? "\n warning: full failed while selected passed; inspect outputs before claiming safety" : "");
192
195
  }
193
196
  const lines = [
194
- `DiffCI verify-savings: ${report.comparison.deltaMs >= 0 ? "faster" : "slower"} by ${formatMs(Math.abs(report.comparison.deltaMs))}`,
197
+ `DiffCI verify-savings: test execution ${Math.abs(report.comparison.grossPercentChange).toFixed(1)}% ${report.comparison.grossPercentChange >= 0 ? "faster" : "slower"} in this paired run`,
198
+ ` full: ${formatMs(report.comparison.fullWallMs)}`,
199
+ ` selected: ${formatMs(report.comparison.selectedWallMs)} + analysis ${formatMs(report.analysisOverheadMs ?? 0)} = ${formatMs(report.comparison.netSelectedMs)}`,
200
+ ` net including analysis: ${Math.abs(report.comparison.percentChange).toFixed(1)}% ${report.comparison.deltaMs >= 0 ? "faster" : "slower"} (${formatMs(Math.abs(report.comparison.deltaMs))} ${report.comparison.deltaMs >= 0 ? "saved" : "added"})`,
195
201
  ];
196
202
  if (report.comparison.missedFailureSignal) {
197
203
  lines.push(" warning: full failed while selected passed; inspect outputs before claiming safety");
@@ -23,8 +23,9 @@ Then ask your coding agent to run:
23
23
  npx @diffci.com/diffci@latest check
24
24
  ```
25
25
 
26
- DiffCI is observation-only by default: it analyzes the change, writes a report outside the checkout,
27
- sends nothing unless explicitly configured, and does not run, skip, cancel, or reorder tests.
26
+ DiffCI check analyzes the change and runs inferred full and selected test commands to measure time.
27
+ It writes reports outside the checkout and sends nothing. Test commands may write generated files.
28
+ Use `observe --no-send` for analysis only.
28
29
  ```
29
30
 
30
31
  ## Agent Instruction Snippet
@@ -71,11 +72,11 @@ Title: Add optional DiffCI instructions for AI coding agents
71
72
 
72
73
  This adds an optional instruction for coding agents to run DiffCI before marking changes PR-ready.
73
74
 
74
- DiffCI is observation-only by default:
75
+ DiffCI check runs a paired test comparison when commands can be inferred:
75
76
 
76
77
  - it analyzes the change and writes a local report;
77
78
  - it sends nothing without explicit configuration;
78
- - it does not run, skip, cancel, or reorder tests;
79
+ - it runs full and selected test commands but does not skip required CI;
79
80
  - the repository's existing required CI remains authoritative.
80
81
 
81
82
  Default command:
@@ -46,7 +46,7 @@ Before marking changes PR-ready, run:
46
46
  npx @diffci.com/diffci@latest check
47
47
  ```
48
48
 
49
- DiffCI is observation-only by default: it analyzes the change, writes a report outside the checkout,
50
- sends nothing unless explicitly configured, and does not run, skip, cancel, or reorder tests. Existing
51
- required CI remains authoritative.
49
+ DiffCI check analyzes the change and runs inferred full and selected test commands to measure time.
50
+ It writes reports outside the checkout and sends nothing. Test commands may write generated files.
51
+ Existing required CI remains authoritative; use `observe --no-send` for analysis only.
52
52
  ```
package/docs/ai-agents.md CHANGED
@@ -3,14 +3,15 @@
3
3
  DiffCI is the default CI/CD validation lens agents can run before proposing, committing, or opening a
4
4
  pull request.
5
5
 
6
- The safe default command is:
6
+ The default command is:
7
7
 
8
8
  ```bash
9
9
  npx @diffci.com/diffci@latest check
10
10
  ```
11
11
 
12
- `check` is an alias for `observe --no-send`. It analyzes the change and prints a report summary without
13
- running tests, skipping tests, cancelling jobs, changing the checkout, or sending data to DiffCI Cloud.
12
+ `check` analyzes the change and runs inferred full and selected test commands when possible. It
13
+ prints paired runtime when both pass and sends nothing to DiffCI Cloud. Test commands may change
14
+ the checkout. Use `observe --no-send` when you need analysis without running tests.
14
15
 
15
16
  ## Install Agent Instructions
16
17
 
@@ -12,6 +12,7 @@ Default validation command:
12
12
  npx @diffci.com/diffci@latest check
13
13
  ```
14
14
 
15
- Use DiffCI before PR-ready answers. `check` is observation-only: it analyzes the change, writes a
16
- report outside the checkout, sends nothing by default, and does not run, skip, cancel, or reorder tests.
15
+ Use DiffCI before PR-ready answers. `check` analyzes the change and runs inferred full and selected
16
+ test commands to measure time. It writes reports outside the checkout and sends nothing by default.
17
+ Test commands may write generated files. Use `observe --no-send` for analysis only.
17
18
  Keep the repository's required checks authoritative.
package/docs/codex.md CHANGED
@@ -12,6 +12,7 @@ Default validation command:
12
12
  npx @diffci.com/diffci@latest check
13
13
  ```
14
14
 
15
- Use DiffCI before PR-ready answers. `check` is observation-only: it analyzes the change, writes a
16
- report outside the checkout, sends nothing by default, and does not run, skip, cancel, or reorder tests.
15
+ Use DiffCI before PR-ready answers. `check` analyzes the change and runs inferred full and selected
16
+ test commands to measure time. It writes reports outside the checkout and sends nothing by default.
17
+ Test commands may write generated files. Use `observe --no-send` for analysis only.
17
18
  Keep the repository's required checks authoritative.
package/docs/copilot.md CHANGED
@@ -12,7 +12,7 @@ Default validation command:
12
12
  npx @diffci.com/diffci@latest check
13
13
  ```
14
14
 
15
- Use DiffCI before PR-ready answers. `init` writes `.github/copilot-instructions.md`; `check` is
16
- observation-only: it analyzes the change, writes a report outside the checkout, sends nothing by
17
- default, and does not run, skip, cancel, or reorder tests. Keep the repository's required checks
18
- authoritative.
15
+ Use DiffCI before PR-ready answers. `init` writes `.github/copilot-instructions.md`; `check`
16
+ analyzes the change and runs inferred full and selected test commands to measure time. It sends
17
+ nothing by default. Test commands may write generated files; use `observe --no-send` for analysis
18
+ only. Keep the repository's required checks authoritative.
package/docs/cursor.md CHANGED
@@ -12,7 +12,7 @@ Default validation command:
12
12
  npx @diffci.com/diffci@latest check
13
13
  ```
14
14
 
15
- Use DiffCI before PR-ready changes. `init` writes `.cursor/rules/diffci.mdc`; `check` is
16
- observation-only: it analyzes the change, writes a report outside the checkout, sends nothing by
17
- default, and does not run, skip, cancel, or reorder tests. Keep the repository's required checks
18
- authoritative.
15
+ Use DiffCI before PR-ready changes. `init` writes `.cursor/rules/diffci.mdc`; `check` analyzes the
16
+ change and runs inferred full and selected test commands to measure time. It sends nothing by
17
+ default. Test commands may write generated files; use `observe --no-send` for analysis only. Keep
18
+ the repository's required checks authoritative.
package/docs/grok.md CHANGED
@@ -12,6 +12,7 @@ Default validation command:
12
12
  npx @diffci.com/diffci@latest check
13
13
  ```
14
14
 
15
- Use DiffCI before PR-ready answers. `check` is observation-only: it analyzes the change, writes a
16
- report outside the checkout, sends nothing by default, and does not run, skip, cancel, or reorder tests.
15
+ Use DiffCI before PR-ready answers. `check` analyzes the change and runs inferred full and selected
16
+ test commands to measure time. It writes reports outside the checkout and sends nothing by default.
17
+ Test commands may write generated files. Use `observe --no-send` for analysis only.
17
18
  Keep the repository's required checks authoritative.
package/docs/mcp.md CHANGED
@@ -10,12 +10,12 @@ npx -p @diffci.com/diffci@latest diffci-mcp
10
10
 
11
11
  Available tools:
12
12
 
13
- - `diffci_check` - runs `diffci check`, the observation-only AI-agent validation command.
13
+ - `diffci_check` - runs `diffci check`, including inferred full and selected test commands.
14
14
  - `diffci_init` - runs `diffci init` to seed agent instruction files.
15
15
  - `diffci_verify_workflow` - runs `diffci verify-workflow` to check non-interference.
16
16
 
17
- The MCP server delegates to the same open-source CLI. `diffci_check` sends nothing by default and does
18
- not run, skip, cancel, or reorder tests.
17
+ The MCP server delegates to the same open-source CLI. `diffci_check` sends nothing to DiffCI Cloud,
18
+ but it runs test commands when it can infer them. Use the CLI's `observe --no-send` for analysis only.
19
19
 
20
20
  Keep required project CI authoritative. DiffCI output is a validation lens, not permission to skip
21
21
  required checks.
@@ -59,6 +59,12 @@ Ask maintainers for a small, reversible experiment:
59
59
  Use this when a maintainer asks whether selecting fewer tests would actually make their CI faster.
60
60
  This is a paired local measurement, not a production-savings claim.
61
61
 
62
+ For one command that infers the full test command and prints the measured percentage when both runs
63
+ pass, use `npx @diffci.com/diffci@latest check` from the repository root. For Maven, `check` reads
64
+ the goal and profiles from `diffci.json` when present; otherwise it uses `mvn test` and says that
65
+ the default may differ from CI. Run it on the intended CI runner: timings from another machine are
66
+ not CI savings. A full-validation fallback runs the full command once and reports 0% reduction.
67
+
62
68
  Step 1: create an observation report without sending it anywhere.
63
69
 
64
70
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diffci.com/diffci",
3
- "version": "0.1.12",
3
+ "version": "0.2.0",
4
4
  "description": "DiffCI - deterministic change-aware CI planning",
5
5
  "keywords": [
6
6
  "ci",