@diffci.com/diffci 0.1.5 → 0.1.7

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
@@ -6,7 +6,8 @@
6
6
 
7
7
  **Find test-selection opportunities in your CI before changing what it runs.** DiffCI analyzes a
8
8
  commit's changes and dependency graph, then reports which test files it would select, why it falls
9
- back to a full run, and whether it can propose a test command. The CLI and Action are observation-only.
9
+ back to a full run, and whether it can propose a test command. The `observe` command and Action are observation-only;
10
+ the opt-in `pilot` and `verify-savings` commands execute tests.
10
11
 
11
12
  From an existing repository checkout, with Node.js 22.5+ and Git installed:
12
13
 
@@ -14,6 +15,23 @@ From an existing repository checkout, with Node.js 22.5+ and Git installed:
14
15
  npx @diffci.com/diffci@latest observe --no-send
15
16
  ```
16
17
 
18
+ For the fastest self-serve runtime pilot, run one paired check from the repository root:
19
+
20
+ ```bash
21
+ npx @diffci.com/diffci@latest pilot --full "npm test"
22
+ ```
23
+
24
+ On Windows PowerShell, quote the package name:
25
+
26
+ ```powershell
27
+ npx '@diffci.com/diffci@latest' pilot --full "npm test"
28
+ ```
29
+
30
+ This executes the full and selected commands sequentially, and writes `diffci-observe.json`,
31
+ `diffci-savings.json`, and `diffci-savings.md` to a sibling `diffci-output` folder outside the checkout.
32
+ The commands you supply may create files or otherwise change the checkout. One paired run is preliminary
33
+ timing evidence; repeat comparisons and account for cache effects before claiming savings.
34
+
17
35
  **Upgrade from 0.1.3:** tests excluded by a source-only `tsconfig.json` could be discovered without
18
36
  their dependency edges, producing an incomplete selection. This is fixed in **0.1.4**. Revalidate
19
37
  affected observations before using them as opportunity evidence; see the
@@ -34,8 +52,8 @@ not Cal.com's production savings or a prediction for your repository.
34
52
  Selection counts alone do not establish runtime savings. Observation mode measures neither the
35
53
  selected test execution nor realized savings.
36
54
 
37
- For a self-serve paired runtime check, run `observe` first and then run `verify-savings` against the
38
- observation report. It compares your normal full command with
55
+ For an advanced paired runtime check, you can still run `observe` first and then run `verify-savings`
56
+ against the observation report. It compares your normal full command with
39
57
  DiffCI's proposed selected command and writes JSON plus Markdown evidence; see
40
58
  [`docs/npm-adoption.md`](docs/npm-adoption.md#self-serve-runtime-pilot).
41
59
 
@@ -28,7 +28,7 @@
28
28
  import { execFileSync } from "node:child_process";
29
29
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
30
30
  import { tmpdir } from "node:os";
31
- import { dirname, join, resolve } from "node:path";
31
+ import { basename, dirname, join, resolve } from "node:path";
32
32
  import { observe, isInsideRepository } from "./observe.js";
33
33
  import { submitObservation } from "./submit.js";
34
34
  import { formatVerifySavingsSummary, runVerifySavings, writeVerifySavingsReport } from "./verify-savings.js";
@@ -286,9 +286,65 @@ function runVerifySavingsCommand(flags, env) {
286
286
  console.log(` markdown: ${options.markdown}`);
287
287
  return report.comparison.fullCommandSucceeded && report.comparison.selectedCommandSucceeded ? 0 : 1;
288
288
  }
289
+ function defaultPilotOutputDir(repoPath) {
290
+ return join(dirname(repoPath), "diffci-output");
291
+ }
292
+ async function runPilot(flags, env) {
293
+ const full = stringFlag(flags, "full");
294
+ if (!full) {
295
+ console.error('--full <command> is required, for example: diffci pilot --full "npm test"');
296
+ return 1;
297
+ }
298
+ const repoPath = resolve(stringFlag(flags, "repo") ?? env.GITHUB_WORKSPACE ?? process.cwd());
299
+ const outDir = resolve(stringFlag(flags, "out-dir") ?? defaultPilotOutputDir(repoPath));
300
+ if (isInsideRepository(repoPath, outDir) || outDir === repoPath) {
301
+ console.error(`Refusing to write pilot reports inside the repository: ${outDir}. Pass --out-dir with a path outside the checkout.`);
302
+ return 2;
303
+ }
304
+ const label = stringFlag(flags, "label") ?? basename(repoPath);
305
+ const observationPath = join(outDir, "diffci-observe.json");
306
+ const savingsPath = join(outDir, "diffci-savings.json");
307
+ const markdownPath = join(outDir, "diffci-savings.md");
308
+ const identity = observerIdentity();
309
+ const observation = await observe({
310
+ repoPath,
311
+ env: env,
312
+ version: identity.version,
313
+ engineSha: identity.sha,
314
+ baseOverride: stringFlag(flags, "base"),
315
+ headOverride: stringFlag(flags, "head"),
316
+ redactPaths: flags["redact-paths"] === true,
317
+ reportPath: observationPath,
318
+ });
319
+ mkdirSync(outDir, { recursive: true });
320
+ writeFileSync(observationPath, `${JSON.stringify(observation, null, 2)}\n`, "utf8");
321
+ console.log(summarise(observation));
322
+ console.log(` observation report: ${observationPath}`);
323
+ if (observation.status !== "OBSERVED") {
324
+ console.log("DiffCI pilot stopped before timing because observation did not produce a selectable report.");
325
+ return 1;
326
+ }
327
+ const savings = runVerifySavings({
328
+ full,
329
+ selectedFromReport: observationPath,
330
+ out: savingsPath,
331
+ markdown: markdownPath,
332
+ label,
333
+ cwd: repoPath,
334
+ timeoutMs: numberFlag(flags, "timeout-ms") ?? 30 * 60 * 1000,
335
+ analysisOverheadMs: numberFlag(flags, "analysis-overhead-ms"),
336
+ tailBytes: numberFlag(flags, "tail-bytes") ?? 12_000,
337
+ });
338
+ writeVerifySavingsReport(savings, { out: savingsPath, markdown: markdownPath });
339
+ console.log(formatVerifySavingsSummary(savings));
340
+ console.log(` savings report: ${savingsPath}`);
341
+ console.log(` markdown: ${markdownPath}`);
342
+ return savings.comparison.fullCommandSucceeded && savings.comparison.selectedCommandSucceeded ? 0 : 1;
343
+ }
289
344
  const USAGE = `diffci - observation-only change-aware CI analysis
290
345
 
291
346
  Usage:
347
+ diffci pilot --full <command> [--repo <path>] [--out-dir <dir>] [--label <name>]
292
348
  diffci observe [--repo <path>] [--out <file>] [--base <sha> --head <sha>]
293
349
  [--redact-paths] [--json] [--quiet] [--fail-on-error]
294
350
  [--api-url <url> --api-token <token>] [--no-send]
@@ -298,6 +354,7 @@ Usage:
298
354
  diffci verify-workflow [--repo <path>]
299
355
  diffci version
300
356
 
357
+ pilot runs observe and verify-savings together, writing reports to ../diffci-output by default.
301
358
  observe analyses the checkout and writes one JSON report. It runs nothing and changes nothing.
302
359
  verify-savings runs both commands and reports measured paired runtime; it is an opt-in pilot command.
303
360
  verify-workflow checks that the job running DiffCI cannot affect any other job, and exits 1 if it can.
@@ -315,6 +372,9 @@ async function main() {
315
372
  return;
316
373
  }
317
374
  switch (command) {
375
+ case "pilot":
376
+ process.exitCode = await runPilot(flags, env);
377
+ return;
318
378
  case "observe":
319
379
  process.exitCode = await runObserve(flags, env);
320
380
  return;
@@ -333,8 +393,15 @@ async function main() {
333
393
  }
334
394
  }
335
395
  main().catch((error) => {
336
- // Reaching here means a defect outside observe()'s own guard. It still must not take a build down:
337
- // the failure is printed, and the exit code stays 0 unless the caller asked otherwise.
338
- console.error(`DiffCI observer failed: ${error instanceof Error ? error.message : String(error)}`);
339
- process.exitCode = process.argv.includes("--fail-on-error") ? 1 : 0;
396
+ const command = process.argv[2];
397
+ const message = error instanceof Error ? error.message : String(error);
398
+ if (command === "observe") {
399
+ // Reaching here means a defect outside observe()'s own guard. It still must not take a build down:
400
+ // the failure is printed, and the exit code stays 0 unless the caller asked otherwise.
401
+ console.error(`DiffCI observer failed: ${message}`);
402
+ process.exitCode = process.argv.includes("--fail-on-error") ? 1 : 0;
403
+ return;
404
+ }
405
+ console.error(`DiffCI ${command ?? "command"} failed: ${message}`);
406
+ process.exitCode = 1;
340
407
  });
@@ -11,11 +11,11 @@ function readSelectionFromObservation(path) {
11
11
  const parsed = JSON.parse(readFileSync(absolutePath, "utf8"));
12
12
  if (parsed.status !== "OBSERVED")
13
13
  throw new Error(`--selected-from-report requires an OBSERVED report; got ${String(parsed.status)}`);
14
- const command = Array.isArray(parsed.result?.proposedCommands) && typeof parsed.result.proposedCommands[0] === "string"
15
- ? parsed.result.proposedCommands[0]
16
- : undefined;
17
- if (!command)
18
- throw new Error("--selected-from-report did not contain result.proposedCommands[0]");
14
+ const commands = parsed.result?.proposedCommands;
15
+ if (!Array.isArray(commands) || commands.length !== 1 || typeof commands[0] !== "string" || !commands[0].trim()) {
16
+ throw new Error("--selected-from-report requires exactly one non-empty proposed command; use --selected with an explicit command covering the complete selection for multi-command plans");
17
+ }
18
+ const command = commands[0];
19
19
  const selectedTests = Array.isArray(parsed.result?.selectedTests) ? parsed.result.selectedTests : undefined;
20
20
  return {
21
21
  command,
@@ -122,7 +122,9 @@ export function renderVerifySavingsMarkdown(report) {
122
122
  const title = report.label ? `# DiffCI Verify Savings: ${report.label}` : "# DiffCI Verify Savings";
123
123
  const warning = report.comparison.missedFailureSignal
124
124
  ? "\n> WARNING: Full failed while selected passed. Do not treat this selected command as safe until the full-run failure is understood.\n"
125
- : "";
125
+ : !report.comparison.fullCommandSucceeded || !report.comparison.selectedCommandSucceeded
126
+ ? "\n> WARNING: One or both commands failed. This comparison is invalid as savings evidence; timings below are diagnostic only.\n"
127
+ : "";
126
128
  const selectionCounts = report.selectedTestCount !== undefined && report.totalTestCount !== undefined
127
129
  ? `\nSelected tests: ${report.selectedTestCount} of ${report.totalTestCount}\n`
128
130
  : "";
@@ -137,6 +139,7 @@ This report compares a full command with a selected command on the same checkout
137
139
 
138
140
  ## Result
139
141
 
142
+ ${!report.comparison.fullCommandSucceeded || !report.comparison.selectedCommandSucceeded ? "Comparison invalid: command failure. Do not interpret the timing difference as savings.\n" : ""}
140
143
  | Measure | Value |
141
144
  | --- | ---: |
142
145
  | Full runtime | ${formatMs(report.comparison.fullWallMs)} |
@@ -183,6 +186,10 @@ export function writeVerifySavingsReport(report, paths) {
183
186
  writeText(paths.markdown, renderVerifySavingsMarkdown(report));
184
187
  }
185
188
  export function formatVerifySavingsSummary(report) {
189
+ if (!report.comparison.fullCommandSucceeded || !report.comparison.selectedCommandSucceeded) {
190
+ return "DiffCI verify-savings: comparison invalid because one or both commands failed" +
191
+ (report.comparison.missedFailureSignal ? "\n warning: full failed while selected passed; inspect outputs before claiming safety" : "");
192
+ }
186
193
  const lines = [
187
194
  `DiffCI verify-savings: ${report.comparison.deltaMs >= 0 ? "faster" : "slower"} by ${formatMs(Math.abs(report.comparison.deltaMs))}`,
188
195
  ];
@@ -62,7 +62,7 @@ This is a paired local measurement, not a production-savings claim.
62
62
  Step 1: create an observation report without sending it anywhere.
63
63
 
64
64
  ```bash
65
- npx @diffci.com/diffci@latest observe --no-send --out ./diffci-observation.json
65
+ npx @diffci.com/diffci@latest observe --no-send --out ../diffci-output/diffci-observation.json
66
66
  ```
67
67
 
68
68
  Step 2: run the paired pilot.
@@ -72,13 +72,17 @@ npx @diffci.com/diffci@latest verify-savings \
72
72
  --label owner/repo \
73
73
  --repo /path/to/their/repo \
74
74
  --full "npm test" \
75
- --selected-from-report /path/to/their/repo/diffci-observation.json \
76
- --out ./diffci-verify-savings.json \
77
- --markdown ./diffci-verify-savings.md
75
+ --selected-from-report ../diffci-output/diffci-observation.json \
76
+ --out ../diffci-output/diffci-verify-savings.json \
77
+ --markdown ../diffci-output/diffci-verify-savings.md
78
78
  ```
79
79
 
80
80
  What the report means:
81
81
 
82
+ Run both steps from the same repository root with the same checked-out revision. These commands
83
+ execute repository code. The full run can warm caches for the selected run, so repeat comparisons
84
+ with controlled cache state before drawing conclusions. A passing pair does not establish selection safety.
85
+
82
86
  - Full runtime is measured from `--full`.
83
87
  - Selected runtime is measured from DiffCI's proposed command in the observation report.
84
88
  - DiffCI analysis overhead is imported from `timings.totalMs` in the observation report unless
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diffci.com/diffci",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "DiffCI - deterministic change-aware CI planning",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {