@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 +23 -21
- package/dist-client/src/client/cli.js +128 -29
- package/dist-client/src/client/full-command.js +46 -0
- package/dist-client/src/client/mcp.js +1 -1
- package/dist-client/src/client/verify-savings.js +8 -2
- package/docs/agent-adoption-kit.md +5 -4
- package/docs/agent-adoption-targets.md +3 -3
- package/docs/ai-agents.md +4 -3
- package/docs/claude-code.md +3 -2
- package/docs/codex.md +3 -2
- package/docs/copilot.md +4 -4
- package/docs/cursor.md +4 -4
- package/docs/grok.md +3 -2
- package/docs/mcp.md +3 -3
- package/docs/npm-adoption.md +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
[](https://docs.npmjs.com/generating-provenance-statements)
|
|
6
6
|
[](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.
|
|
11
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npx @diffci.com/diffci@latest
|
|
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'
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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`
|
|
57
|
-
|
|
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.
|
|
66
|
-
|
|
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 `
|
|
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
|
|
3
|
+
* `diffci` - the client-side command.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
-
-
|
|
118
|
-
- prints
|
|
119
|
-
-
|
|
120
|
-
-
|
|
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
|
|
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
|
|
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": "
|
|
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}
|
|
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(
|
|
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 -
|
|
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
|
|
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
|
|
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: "
|
|
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.
|
|
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
|
|
27
|
-
|
|
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
|
|
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
|
|
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
|
|
50
|
-
|
|
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
|
|
6
|
+
The default command is:
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
9
|
npx @diffci.com/diffci@latest check
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`check`
|
|
13
|
-
|
|
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
|
|
package/docs/claude-code.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`
|
|
16
|
-
|
|
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`
|
|
16
|
-
|
|
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`
|
|
16
|
-
|
|
17
|
-
|
|
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`
|
|
16
|
-
|
|
17
|
-
default
|
|
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`
|
|
16
|
-
|
|
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`,
|
|
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
|
|
18
|
-
|
|
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.
|
package/docs/npm-adoption.md
CHANGED
|
@@ -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
|