@diffci.com/diffci 0.1.12 → 0.2.1
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 +138 -29
- package/dist-client/src/client/full-command.js +71 -0
- package/dist-client/src/client/mcp.js +1 -1
- package/dist-client/src/client/verify-savings.js +12 -4
- 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, inferSelectedCommand } 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,103 @@ 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
|
+
const selected = inferSelectedCommand(repoPath, observation.result.proposedCommands[0], observation.result.selectedTests);
|
|
320
|
+
if (!selected.command) {
|
|
321
|
+
print(` timing: unavailable (${selected.reason})`);
|
|
322
|
+
if (flags.json === true)
|
|
323
|
+
console.log(JSON.stringify({ observation, timing: null, reason: selected.reason }, null, 2));
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
if (selected.command !== observation.result.proposedCommands[0])
|
|
327
|
+
print(` selected command: ${selected.command} (${selected.reason})`);
|
|
328
|
+
print(" running full and selected validation...");
|
|
329
|
+
const savings = runVerifySavings({
|
|
330
|
+
full: inferred.command,
|
|
331
|
+
selectedFromReport: reportPath,
|
|
332
|
+
selectedCommandOverride: selected.command,
|
|
333
|
+
out: savingsPath,
|
|
334
|
+
markdown: markdownPath,
|
|
335
|
+
label: stringFlag(flags, "label") ?? basename(repoPath),
|
|
336
|
+
cwd: repoPath,
|
|
337
|
+
timeoutMs,
|
|
338
|
+
tailBytes,
|
|
339
|
+
});
|
|
340
|
+
writeVerifySavingsReport(savings, { out: savingsPath, markdown: markdownPath });
|
|
341
|
+
print(formatVerifySavingsSummary(savings));
|
|
342
|
+
print(` savings report: ${savingsPath}`);
|
|
343
|
+
print(` markdown: ${markdownPath}`);
|
|
344
|
+
if (flags.json === true)
|
|
345
|
+
console.log(JSON.stringify({ observation, savings }, null, 2));
|
|
346
|
+
return savings.comparison.fullCommandSucceeded && savings.comparison.selectedCommandSucceeded ? 0 : 1;
|
|
347
|
+
}
|
|
247
348
|
async function runObserve(flags, env) {
|
|
248
349
|
const repoPath = resolve(typeof flags.repo === "string" ? flags.repo : env.GITHUB_WORKSPACE ?? process.cwd());
|
|
249
350
|
const reportPath = resolve(typeof flags.out === "string" ? flags.out : defaultReportPath(env));
|
|
@@ -445,6 +546,14 @@ async function runPilot(flags, env) {
|
|
|
445
546
|
console.log("DiffCI pilot stopped before timing because observation did not produce a selectable report.");
|
|
446
547
|
return 1;
|
|
447
548
|
}
|
|
549
|
+
if (observation.result?.mode === "FULL") {
|
|
550
|
+
console.log("DiffCI pilot: 0% planned test-file reduction on this commit (full validation required). No paired timing was run.");
|
|
551
|
+
return 0;
|
|
552
|
+
}
|
|
553
|
+
if (!observation.result?.proposedCommands.length || observation.result.commandRefusalReason) {
|
|
554
|
+
console.log("DiffCI pilot: no runnable selected command. No paired timing was run.");
|
|
555
|
+
return 1;
|
|
556
|
+
}
|
|
448
557
|
const savings = runVerifySavings({
|
|
449
558
|
full,
|
|
450
559
|
selectedFromReport: observationPath,
|
|
@@ -462,12 +571,12 @@ async function runPilot(flags, env) {
|
|
|
462
571
|
console.log(` markdown: ${markdownPath}`);
|
|
463
572
|
return savings.comparison.fullCommandSucceeded && savings.comparison.selectedCommandSucceeded ? 0 : 1;
|
|
464
573
|
}
|
|
465
|
-
const USAGE = `diffci -
|
|
574
|
+
const USAGE = `diffci - change-aware CI analysis and paired timing
|
|
466
575
|
|
|
467
576
|
Usage:
|
|
468
577
|
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]
|
|
578
|
+
diffci check [--repo <path>] [--out <file>] [--base <sha> --head <sha>]
|
|
579
|
+
[--redact-paths] [--json] [--quiet] [--fail-on-error] [--timeout-ms <ms>]
|
|
471
580
|
diffci pilot --full <command> [--repo <path>] [--out-dir <dir>] [--label <name>]
|
|
472
581
|
diffci observe [--repo <path>] [--out <file>] [--base <sha> --head <sha>]
|
|
473
582
|
[--redact-paths] [--json] [--quiet] [--fail-on-error]
|
|
@@ -479,9 +588,9 @@ Usage:
|
|
|
479
588
|
diffci version
|
|
480
589
|
|
|
481
590
|
init writes AGENTS.md, CLAUDE.md, Cursor rules, Copilot instructions, and diffci.config.json.
|
|
482
|
-
check
|
|
591
|
+
check analyzes the change, runs inferred full and selected commands, and shows measured savings.
|
|
483
592
|
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.
|
|
593
|
+
observe analyses the checkout and writes one JSON report. It runs nothing and changes nothing.
|
|
485
594
|
verify-savings runs both commands and reports measured paired runtime; it is an opt-in pilot command.
|
|
486
595
|
verify-workflow checks that the job running DiffCI cannot affect any other job, and exits 1 if it can.
|
|
487
596
|
|
|
@@ -502,7 +611,7 @@ async function main() {
|
|
|
502
611
|
process.exitCode = runInit(flags, env);
|
|
503
612
|
return;
|
|
504
613
|
case "check":
|
|
505
|
-
process.exitCode = await
|
|
614
|
+
process.exitCode = await runCheck(flags, env);
|
|
506
615
|
return;
|
|
507
616
|
case "pilot":
|
|
508
617
|
process.exitCode = await runPilot(flags, env);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readRepositoryConfig } from "@diffci.com/core/repo/repo-config";
|
|
4
|
+
/** Preserve the repository's TypeScript test loader for selected node:test files. */
|
|
5
|
+
export function inferSelectedCommand(repoPath, proposed, selectedTests) {
|
|
6
|
+
if (!/^node --test(?:\s|$)/.test(proposed) || !selectedTests.some(path => /\.(?:ts|tsx|mts|cts)$/.test(path))) {
|
|
7
|
+
return { command: proposed, reason: "DiffCI proposed command" };
|
|
8
|
+
}
|
|
9
|
+
const packagePath = join(repoPath, "package.json");
|
|
10
|
+
if (!existsSync(packagePath))
|
|
11
|
+
return { reason: "TypeScript tests require a declared test loader" };
|
|
12
|
+
let script;
|
|
13
|
+
try {
|
|
14
|
+
script = JSON.parse(readFileSync(packagePath, "utf8")).scripts?.test;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { reason: "package.json could not be read" };
|
|
18
|
+
}
|
|
19
|
+
if (typeof script !== "string")
|
|
20
|
+
return { reason: "TypeScript tests require a declared test script" };
|
|
21
|
+
const runners = [...script.matchAll(/\btsx(?:\s+--conditions\s+[\w,-]+)?\s+--test\b/g)];
|
|
22
|
+
if (runners.length !== 1)
|
|
23
|
+
return { reason: "Cannot identify one TypeScript node:test runner in the test script" };
|
|
24
|
+
return {
|
|
25
|
+
command: proposed.replace(/^node --test/, `npx --no-install ${runners[0][0]}`),
|
|
26
|
+
reason: "TypeScript node:test runner from package.json test script",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Choose the repository's conventional full test command without executing anything. */
|
|
30
|
+
export function inferFullCommand(repoPath) {
|
|
31
|
+
if (existsSync(join(repoPath, "pom.xml"))) {
|
|
32
|
+
const config = readRepositoryConfig(repoPath);
|
|
33
|
+
if (config.configurationError)
|
|
34
|
+
return { reason: `DiffCI configuration error: ${config.configurationError}` };
|
|
35
|
+
const goal = config.maven?.goal ?? "test";
|
|
36
|
+
const profiles = config.maven?.profiles?.length ? ` -P ${config.maven.profiles.join(",")}` : "";
|
|
37
|
+
return {
|
|
38
|
+
command: `mvn${profiles} ${goal}`,
|
|
39
|
+
reason: config.maven ? "Maven goal and profiles from DiffCI configuration" : "Maven default goal: test; confirm this matches CI",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const packagePath = join(repoPath, "package.json");
|
|
43
|
+
if (existsSync(packagePath)) {
|
|
44
|
+
let pkg;
|
|
45
|
+
try {
|
|
46
|
+
pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { reason: "package.json could not be read" };
|
|
50
|
+
}
|
|
51
|
+
if (typeof pkg.scripts?.test !== "string" || !pkg.scripts.test.trim()) {
|
|
52
|
+
return { reason: "package.json has no test script" };
|
|
53
|
+
}
|
|
54
|
+
const manager = typeof pkg.packageManager === "string" ? pkg.packageManager.split("@")[0] : undefined;
|
|
55
|
+
if (manager === "pnpm" || manager === "yarn" || manager === "bun") {
|
|
56
|
+
return { command: `${manager} test`, reason: `package.json test script via ${manager}` };
|
|
57
|
+
}
|
|
58
|
+
if (manager && manager !== "npm")
|
|
59
|
+
return { reason: `unsupported package manager: ${manager}` };
|
|
60
|
+
if (existsSync(join(repoPath, "pnpm-lock.yaml")))
|
|
61
|
+
return { command: "pnpm test", reason: "package.json test script with pnpm lockfile" };
|
|
62
|
+
if (existsSync(join(repoPath, "yarn.lock")))
|
|
63
|
+
return { command: "yarn test", reason: "package.json test script with Yarn lockfile" };
|
|
64
|
+
if (existsSync(join(repoPath, "bun.lock")) || existsSync(join(repoPath, "bun.lockb")))
|
|
65
|
+
return { command: "bun test", reason: "package.json test script with Bun lockfile" };
|
|
66
|
+
return { command: "npm test", reason: "package.json test script" };
|
|
67
|
+
}
|
|
68
|
+
if (existsSync(join(repoPath, "go.mod")))
|
|
69
|
+
return { command: "go test ./...", reason: "root Go module" };
|
|
70
|
+
return { reason: "no supported full test command could be inferred" };
|
|
71
|
+
}
|
|
@@ -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: {
|
|
@@ -27,13 +27,15 @@ function readSelectionFromObservation(path) {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
function resolveSelection(options) {
|
|
30
|
-
if (options.selectedFromReport)
|
|
31
|
-
|
|
30
|
+
if (options.selectedFromReport) {
|
|
31
|
+
const selection = readSelectionFromObservation(options.selectedFromReport);
|
|
32
|
+
return options.selectedCommandOverride ? { ...selection, command: options.selectedCommandOverride } : selection;
|
|
33
|
+
}
|
|
32
34
|
if (!options.selected)
|
|
33
35
|
throw new Error("--selected <command> or --selected-from-report <path> is required");
|
|
34
36
|
return { command: options.selected, source: "manual" };
|
|
35
37
|
}
|
|
36
|
-
function measureCommand(command, options) {
|
|
38
|
+
export function measureCommand(command, options) {
|
|
37
39
|
const startedAt = new Date().toISOString();
|
|
38
40
|
const started = Date.now();
|
|
39
41
|
const shellCommand = process.platform === "win32" ? "powershell.exe" : "sh";
|
|
@@ -69,6 +71,7 @@ export function buildVerifySavingsReport(input) {
|
|
|
69
71
|
const overhead = input.analysisOverheadMs ?? 0;
|
|
70
72
|
const netSelectedMs = input.selected.wallMs + overhead;
|
|
71
73
|
const deltaMs = input.full.wallMs - netSelectedMs;
|
|
74
|
+
const grossPercentChange = input.full.wallMs > 0 ? ((input.full.wallMs - input.selected.wallMs) / input.full.wallMs) * 100 : 0;
|
|
72
75
|
const percentChange = input.full.wallMs > 0 ? (deltaMs / input.full.wallMs) * 100 : 0;
|
|
73
76
|
const fullCommandSucceeded = input.full.exitCode === 0 && !input.full.timedOut;
|
|
74
77
|
const selectedCommandSucceeded = input.selected.exitCode === 0 && !input.selected.timedOut;
|
|
@@ -100,6 +103,7 @@ export function buildVerifySavingsReport(input) {
|
|
|
100
103
|
selectedWallMs: input.selected.wallMs,
|
|
101
104
|
netSelectedMs,
|
|
102
105
|
deltaMs,
|
|
106
|
+
grossPercentChange,
|
|
103
107
|
percentChange,
|
|
104
108
|
selectedCommandSucceeded,
|
|
105
109
|
fullCommandSucceeded,
|
|
@@ -144,6 +148,7 @@ ${!report.comparison.fullCommandSucceeded || !report.comparison.selectedCommandS
|
|
|
144
148
|
| --- | ---: |
|
|
145
149
|
| Full runtime | ${formatMs(report.comparison.fullWallMs)} |
|
|
146
150
|
| Selected runtime | ${formatMs(report.comparison.selectedWallMs)} |
|
|
151
|
+
| Test execution change vs full | ${formatPercent(report.comparison.grossPercentChange)} |
|
|
147
152
|
| DiffCI analysis overhead | ${overhead} |
|
|
148
153
|
| Net selected runtime | ${formatMs(report.comparison.netSelectedMs)} |
|
|
149
154
|
| Delta vs full | ${formatMs(report.comparison.deltaMs)} ${deltaLabel} |
|
|
@@ -191,7 +196,10 @@ export function formatVerifySavingsSummary(report) {
|
|
|
191
196
|
(report.comparison.missedFailureSignal ? "\n warning: full failed while selected passed; inspect outputs before claiming safety" : "");
|
|
192
197
|
}
|
|
193
198
|
const lines = [
|
|
194
|
-
`DiffCI verify-savings: ${report.comparison.
|
|
199
|
+
`DiffCI verify-savings: test execution ${Math.abs(report.comparison.grossPercentChange).toFixed(1)}% ${report.comparison.grossPercentChange >= 0 ? "faster" : "slower"} in this paired run`,
|
|
200
|
+
` full: ${formatMs(report.comparison.fullWallMs)}`,
|
|
201
|
+
` selected: ${formatMs(report.comparison.selectedWallMs)} + analysis ${formatMs(report.analysisOverheadMs ?? 0)} = ${formatMs(report.comparison.netSelectedMs)}`,
|
|
202
|
+
` 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
203
|
];
|
|
196
204
|
if (report.comparison.missedFailureSignal) {
|
|
197
205
|
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
|