@diffci.com/diffci 0.1.10 → 0.1.12

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.
Files changed (73) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +301 -226
  3. package/action.yml +154 -154
  4. package/dist-client/src/client/cli.js +153 -21
  5. package/dist-client/src/client/economics.js +31 -0
  6. package/dist-client/src/client/mcp.js +196 -0
  7. package/dist-client/src/client/observe.js +40 -9
  8. package/docs/agent-adoption-kit.md +95 -0
  9. package/docs/agent-adoption-targets.md +52 -0
  10. package/docs/ai-agents.md +87 -0
  11. package/docs/claude-code.md +17 -0
  12. package/docs/codex.md +17 -0
  13. package/docs/copilot.md +18 -0
  14. package/docs/cursor.md +18 -0
  15. package/docs/grok.md +17 -0
  16. package/docs/language-support.md +118 -70
  17. package/docs/mcp.md +21 -0
  18. package/llms.txt +37 -0
  19. package/node_modules/@babel/parser/bin/babel-parser.js +0 -0
  20. package/node_modules/@diffci.com/core/README.md +9 -1
  21. package/node_modules/@diffci.com/core/dist/cache/economics-context.d.ts +2 -0
  22. package/node_modules/@diffci.com/core/dist/cache/economics-context.js +21 -0
  23. package/node_modules/@diffci.com/core/dist/cache/economics-context.js.map +1 -0
  24. package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.d.ts +5 -0
  25. package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.js +100 -0
  26. package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.js.map +1 -0
  27. package/node_modules/@diffci.com/core/dist/planner/test-command.js +32 -0
  28. package/node_modules/@diffci.com/core/dist/planner/test-command.js.map +1 -1
  29. package/node_modules/@diffci.com/core/dist/repo/adapters/go.d.ts +3 -0
  30. package/node_modules/@diffci.com/core/dist/repo/adapters/go.js +24 -6
  31. package/node_modules/@diffci.com/core/dist/repo/adapters/go.js.map +1 -1
  32. package/node_modules/@diffci.com/core/dist/repo/adapters/index.js +2 -1
  33. package/node_modules/@diffci.com/core/dist/repo/adapters/index.js.map +1 -1
  34. package/node_modules/@diffci.com/core/dist/repo/adapters/maven.d.ts +3 -0
  35. package/node_modules/@diffci.com/core/dist/repo/adapters/maven.js +65 -0
  36. package/node_modules/@diffci.com/core/dist/repo/adapters/maven.js.map +1 -0
  37. package/node_modules/@diffci.com/core/dist/repo/adapters/types.d.ts +11 -0
  38. package/node_modules/@diffci.com/core/dist/repo/adapters/types.js.map +1 -1
  39. package/node_modules/@diffci.com/core/dist/repo/adapters/vue.js +142 -14
  40. package/node_modules/@diffci.com/core/dist/repo/adapters/vue.js.map +1 -1
  41. package/node_modules/@diffci.com/core/dist/repo/graph.d.ts +4 -0
  42. package/node_modules/@diffci.com/core/dist/repo/graph.js +169 -19
  43. package/node_modules/@diffci.com/core/dist/repo/graph.js.map +1 -1
  44. package/node_modules/@diffci.com/core/dist/repo/impact.js +22 -5
  45. package/node_modules/@diffci.com/core/dist/repo/impact.js.map +1 -1
  46. package/node_modules/@diffci.com/core/dist/repo/repo-config.d.ts +15 -0
  47. package/node_modules/@diffci.com/core/dist/repo/repo-config.js +13 -2
  48. package/node_modules/@diffci.com/core/dist/repo/repo-config.js.map +1 -1
  49. package/node_modules/@diffci.com/core/dist/repo/test-discovery.js +2 -0
  50. package/node_modules/@diffci.com/core/dist/repo/test-discovery.js.map +1 -1
  51. package/node_modules/@diffci.com/core/dist/repo/types.d.ts +17 -0
  52. package/node_modules/@diffci.com/core/dist/repo/vue-scope.d.ts +4 -0
  53. package/node_modules/@diffci.com/core/dist/repo/vue-scope.js +157 -0
  54. package/node_modules/@diffci.com/core/dist/repo/vue-scope.js.map +1 -0
  55. package/node_modules/@diffci.com/core/src/cache/economics-context.ts +19 -0
  56. package/node_modules/@diffci.com/core/src/cache/vue-analysis-cache.ts +80 -0
  57. package/node_modules/@diffci.com/core/src/planner/test-command.ts +28 -0
  58. package/node_modules/@diffci.com/core/src/repo/adapters/go.ts +24 -5
  59. package/node_modules/@diffci.com/core/src/repo/adapters/index.ts +2 -1
  60. package/node_modules/@diffci.com/core/src/repo/adapters/maven.ts +38 -0
  61. package/node_modules/@diffci.com/core/src/repo/adapters/types.ts +5 -0
  62. package/node_modules/@diffci.com/core/src/repo/adapters/vue.ts +120 -15
  63. package/node_modules/@diffci.com/core/src/repo/graph.ts +146 -20
  64. package/node_modules/@diffci.com/core/src/repo/impact.ts +20 -4
  65. package/node_modules/@diffci.com/core/src/repo/repo-config.ts +20 -2
  66. package/node_modules/@diffci.com/core/src/repo/test-discovery.ts +2 -0
  67. package/node_modules/@diffci.com/core/src/repo/types.ts +11 -0
  68. package/node_modules/@diffci.com/core/src/repo/vue-scope.ts +122 -0
  69. package/node_modules/nanoid/bin/nanoid.cjs +0 -0
  70. package/node_modules/typescript/bin/tsc +0 -0
  71. package/node_modules/typescript/bin/tsserver +0 -0
  72. package/node_modules/yaml/bin.mjs +0 -0
  73. package/package.json +155 -131
package/action.yml CHANGED
@@ -1,155 +1,155 @@
1
- # DiffCI observer - the GitHub Action a third-party repository installs (Phase 02, 2026-08-26).
2
- #
3
- # WHAT THIS DOES: analyses one commit range against the checkout the job already has, writes a JSON
4
- # report to the runner's temp directory, prints a summary to the job log and the job summary, optionally
5
- # uploads the report as an artifact, and - only when `api-url` and `api-token` are both set - sends that
6
- # same report to DiffCI. With no token configured nothing leaves the runner at all.
7
- #
8
- # WHAT THIS DOES NOT DO, and cannot: run tests, skip tests, cancel a job, re-order steps, write to the
9
- # repository, comment on a pull request, or set a check status. There is no input below that turns any
10
- # of that on, because the claim being tested for seven days is that installing DiffCI leaves CI
11
- # byte-identical, and a flag that could change what CI runs would eventually be set by accident.
12
- #
13
- # INSTALL IT AS ITS OWN JOB. A job of its own is what makes the claim structural rather than careful:
14
- #
15
- # jobs:
16
- # diffci:
17
- # runs-on: ubuntu-latest
18
- # continue-on-error: true # a DiffCI failure must not become the workflow's conclusion
19
- # permissions:
20
- # contents: read
21
- # steps:
22
- # - uses: actions/checkout@v4
23
- # with:
24
- # fetch-depth: 0 # the base commit must exist locally, or DiffCI refuses
1
+ # DiffCI observer - the GitHub Action a third-party repository installs (Phase 02, 2026-08-26).
2
+ #
3
+ # WHAT THIS DOES: analyses one commit range against the checkout the job already has, writes a JSON
4
+ # report to the runner's temp directory, prints a summary to the job log and the job summary, optionally
5
+ # uploads the report as an artifact, and - only when `api-url` and `api-token` are both set - sends that
6
+ # same report to DiffCI. With no token configured nothing leaves the runner at all.
7
+ #
8
+ # WHAT THIS DOES NOT DO, and cannot: run tests, skip tests, cancel a job, re-order steps, write to the
9
+ # repository, comment on a pull request, or set a check status. There is no input below that turns any
10
+ # of that on, because the claim being tested for seven days is that installing DiffCI leaves CI
11
+ # byte-identical, and a flag that could change what CI runs would eventually be set by accident.
12
+ #
13
+ # INSTALL IT AS ITS OWN JOB. A job of its own is what makes the claim structural rather than careful:
14
+ #
15
+ # jobs:
16
+ # diffci:
17
+ # runs-on: ubuntu-latest
18
+ # continue-on-error: true # a DiffCI failure must not become the workflow's conclusion
19
+ # permissions:
20
+ # contents: read
21
+ # steps:
22
+ # - uses: actions/checkout@v4
23
+ # with:
24
+ # fetch-depth: 0 # the base commit must exist locally, or DiffCI refuses
25
25
  # - uses: DiffCI/DiffCI.com@<40-character commit sha>
26
- #
27
- # `diffci verify-workflow` checks those properties against your own workflow files and exits non-zero
28
- # if any of them does not hold. Run it before the observation window starts.
29
- name: DiffCI observer
30
- description: Observation-only change-aware CI analysis. Runs nothing, changes nothing, skips nothing.
31
- branding:
32
- icon: eye
33
- color: gray-dark
34
-
35
- inputs:
36
- repository-path:
37
- description: The checkout to observe. Defaults to the workspace.
38
- required: false
39
- default: ${{ github.workspace }}
40
- base-sha:
41
- description: Base commit. Leave empty to derive it from the event (pull request base, push before).
42
- required: false
43
- default: ""
44
- head-sha:
45
- description: Head commit. Must be given together with base-sha.
46
- required: false
47
- default: ""
48
- node-version:
49
- description: Node version used to run the observer. Only affects this job.
50
- required: false
51
- default: "22"
52
- report-path:
53
- description: Where to write the report. Must be outside the checkout. Defaults to RUNNER_TEMP.
54
- required: false
55
- default: ""
56
- redact-paths:
57
- description: Replace every file path in the report with a stable 12-character digest.
58
- required: false
59
- default: "false"
60
- upload-artifact:
61
- description: Upload the report as a workflow artifact.
62
- required: false
63
- default: "true"
64
- artifact-name:
65
- description: Name of the uploaded artifact.
66
- required: false
67
- default: diffci-observation
68
- artifact-retention-days:
69
- description: Retention for the uploaded artifact.
70
- required: false
71
- default: "7"
72
- api-url:
73
- description: >-
74
- DiffCI ingest endpoint. Leave empty and nothing is sent anywhere - the report stays on the runner
75
- as an artifact. Must be https.
76
- required: false
77
- default: ""
78
- api-token:
79
- description: >-
80
- Ingest token for THIS repository, from a repository secret. Pass it as the workflow expression
81
- for secrets.DIFFCI_TOKEN, never as a literal. Nothing is sent without it. (This text deliberately
82
- does not spell out the expression: GitHub evaluates expressions inside action metadata, and
83
- "secrets" is not a context an action file may use - a literal expression here made the whole
84
- action fail to load on every run until 2026-09-06.)
85
- required: false
86
- default: ""
87
- fail-on-error:
88
- description: >-
89
- Fail this step when the observation refuses or errors. Default false, so a DiffCI problem never
90
- shows up as a red step in your CI. Turn it on only while debugging an installation.
91
- required: false
92
- default: "false"
93
-
94
- outputs:
95
- report-path:
96
- description: Absolute path of the written report.
97
- value: ${{ steps.observe.outputs.report-path }}
98
- status:
99
- description: OBSERVED, REFUSED, or ERROR.
100
- value: ${{ steps.observe.outputs.status }}
101
-
102
- runs:
103
- using: composite
104
- steps:
105
- - name: Set up Node for the observer
106
- uses: actions/setup-node@v4
107
- with:
108
- node-version: ${{ inputs.node-version }}
109
-
110
- # Installed into the action's own directory, never into the observed repository: nothing here
111
- # touches the workspace, its node_modules, or its lockfile. --omit=dev keeps this to TypeScript and
112
- # a YAML parser; --ignore-scripts means no dependency of DiffCI's runs code on your runner.
113
- - name: Install observer dependencies
114
- shell: bash
115
- working-directory: ${{ github.action_path }}
116
- run: npm ci --omit=dev --no-audit --fund=false --ignore-scripts
117
-
118
- - name: Build observer
119
- shell: bash
120
- working-directory: ${{ github.action_path }}
121
- run: node_modules/.bin/tsc -p tsconfig.client.json
122
-
123
- - name: Observe
124
- id: observe
125
- shell: bash
126
- env:
127
- DIFFCI_REPO: ${{ inputs.repository-path }}
128
- DIFFCI_BASE: ${{ inputs.base-sha }}
129
- DIFFCI_HEAD: ${{ inputs.head-sha }}
130
- DIFFCI_OUT: ${{ inputs.report-path }}
131
- DIFFCI_REDACT: ${{ inputs.redact-paths }}
132
- DIFFCI_FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
133
- # Passed through the environment, never on the command line: an argv is visible to every other
134
- # process on the runner and shows up in traces. The observer reads these directly and never
135
- # prints them.
136
- DIFFCI_API_URL: ${{ inputs.api-url }}
137
- DIFFCI_TOKEN: ${{ inputs.api-token }}
138
- run: |
139
- set -euo pipefail
140
- args=(observe --repo "$DIFFCI_REPO")
141
- if [ -n "$DIFFCI_BASE" ]; then args+=(--base "$DIFFCI_BASE"); fi
142
- if [ -n "$DIFFCI_HEAD" ]; then args+=(--head "$DIFFCI_HEAD"); fi
143
- if [ -n "$DIFFCI_OUT" ]; then args+=(--out "$DIFFCI_OUT"); fi
144
- if [ "$DIFFCI_REDACT" = "true" ]; then args+=(--redact-paths); fi
145
- if [ "$DIFFCI_FAIL_ON_ERROR" = "true" ]; then args+=(--fail-on-error); fi
146
- node "$GITHUB_ACTION_PATH/dist-client/src/client/cli.js" "${args[@]}"
147
-
148
- - name: Upload observation report
149
- if: ${{ inputs.upload-artifact == 'true' && steps.observe.outputs.report-path != '' }}
150
- uses: actions/upload-artifact@v4
151
- with:
152
- name: ${{ inputs.artifact-name }}
153
- path: ${{ steps.observe.outputs.report-path }}
154
- retention-days: ${{ inputs.artifact-retention-days }}
155
- if-no-files-found: warn
26
+ #
27
+ # `diffci verify-workflow` checks those properties against your own workflow files and exits non-zero
28
+ # if any of them does not hold. Run it before the observation window starts.
29
+ name: DiffCI observer
30
+ description: Observation-only change-aware CI analysis. Runs nothing, changes nothing, skips nothing.
31
+ branding:
32
+ icon: eye
33
+ color: gray-dark
34
+
35
+ inputs:
36
+ repository-path:
37
+ description: The checkout to observe. Defaults to the workspace.
38
+ required: false
39
+ default: ${{ github.workspace }}
40
+ base-sha:
41
+ description: Base commit. Leave empty to derive it from the event (pull request base, push before).
42
+ required: false
43
+ default: ""
44
+ head-sha:
45
+ description: Head commit. Must be given together with base-sha.
46
+ required: false
47
+ default: ""
48
+ node-version:
49
+ description: Node version used to run the observer. Only affects this job.
50
+ required: false
51
+ default: "22"
52
+ report-path:
53
+ description: Where to write the report. Must be outside the checkout. Defaults to RUNNER_TEMP.
54
+ required: false
55
+ default: ""
56
+ redact-paths:
57
+ description: Replace every file path in the report with a stable 12-character digest.
58
+ required: false
59
+ default: "false"
60
+ upload-artifact:
61
+ description: Upload the report as a workflow artifact.
62
+ required: false
63
+ default: "true"
64
+ artifact-name:
65
+ description: Name of the uploaded artifact.
66
+ required: false
67
+ default: diffci-observation
68
+ artifact-retention-days:
69
+ description: Retention for the uploaded artifact.
70
+ required: false
71
+ default: "7"
72
+ api-url:
73
+ description: >-
74
+ DiffCI ingest endpoint. Leave empty and nothing is sent anywhere - the report stays on the runner
75
+ as an artifact. Must be https.
76
+ required: false
77
+ default: ""
78
+ api-token:
79
+ description: >-
80
+ Ingest token for THIS repository, from a repository secret. Pass it as the workflow expression
81
+ for secrets.DIFFCI_TOKEN, never as a literal. Nothing is sent without it. (This text deliberately
82
+ does not spell out the expression: GitHub evaluates expressions inside action metadata, and
83
+ "secrets" is not a context an action file may use - a literal expression here made the whole
84
+ action fail to load on every run until 2026-09-06.)
85
+ required: false
86
+ default: ""
87
+ fail-on-error:
88
+ description: >-
89
+ Fail this step when the observation refuses or errors. Default false, so a DiffCI problem never
90
+ shows up as a red step in your CI. Turn it on only while debugging an installation.
91
+ required: false
92
+ default: "false"
93
+
94
+ outputs:
95
+ report-path:
96
+ description: Absolute path of the written report.
97
+ value: ${{ steps.observe.outputs.report-path }}
98
+ status:
99
+ description: OBSERVED, REFUSED, or ERROR.
100
+ value: ${{ steps.observe.outputs.status }}
101
+
102
+ runs:
103
+ using: composite
104
+ steps:
105
+ - name: Set up Node for the observer
106
+ uses: actions/setup-node@v4
107
+ with:
108
+ node-version: ${{ inputs.node-version }}
109
+
110
+ # Installed into the action's own directory, never into the observed repository: nothing here
111
+ # touches the workspace, its node_modules, or its lockfile. --omit=dev keeps this to TypeScript and
112
+ # a YAML parser; --ignore-scripts means no dependency of DiffCI's runs code on your runner.
113
+ - name: Install observer dependencies
114
+ shell: bash
115
+ working-directory: ${{ github.action_path }}
116
+ run: npm ci --omit=dev --no-audit --fund=false --ignore-scripts
117
+
118
+ - name: Build observer
119
+ shell: bash
120
+ working-directory: ${{ github.action_path }}
121
+ run: node_modules/.bin/tsc -p tsconfig.client.json
122
+
123
+ - name: Observe
124
+ id: observe
125
+ shell: bash
126
+ env:
127
+ DIFFCI_REPO: ${{ inputs.repository-path }}
128
+ DIFFCI_BASE: ${{ inputs.base-sha }}
129
+ DIFFCI_HEAD: ${{ inputs.head-sha }}
130
+ DIFFCI_OUT: ${{ inputs.report-path }}
131
+ DIFFCI_REDACT: ${{ inputs.redact-paths }}
132
+ DIFFCI_FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
133
+ # Passed through the environment, never on the command line: an argv is visible to every other
134
+ # process on the runner and shows up in traces. The observer reads these directly and never
135
+ # prints them.
136
+ DIFFCI_API_URL: ${{ inputs.api-url }}
137
+ DIFFCI_TOKEN: ${{ inputs.api-token }}
138
+ run: |
139
+ set -euo pipefail
140
+ args=(observe --repo "$DIFFCI_REPO")
141
+ if [ -n "$DIFFCI_BASE" ]; then args+=(--base "$DIFFCI_BASE"); fi
142
+ if [ -n "$DIFFCI_HEAD" ]; then args+=(--head "$DIFFCI_HEAD"); fi
143
+ if [ -n "$DIFFCI_OUT" ]; then args+=(--out "$DIFFCI_OUT"); fi
144
+ if [ "$DIFFCI_REDACT" = "true" ]; then args+=(--redact-paths); fi
145
+ if [ "$DIFFCI_FAIL_ON_ERROR" = "true" ]; then args+=(--fail-on-error); fi
146
+ node "$GITHUB_ACTION_PATH/dist-client/src/client/cli.js" "${args[@]}"
147
+
148
+ - name: Upload observation report
149
+ if: ${{ inputs.upload-artifact == 'true' && steps.observe.outputs.report-path != '' }}
150
+ uses: actions/upload-artifact@v4
151
+ with:
152
+ name: ${{ inputs.artifact-name }}
153
+ path: ${{ steps.observe.outputs.report-path }}
154
+ retention-days: ${{ inputs.artifact-retention-days }}
155
+ if-no-files-found: warn
@@ -13,6 +13,8 @@
13
13
  * the observer is exactly what Phase 02 shipped: a local analysis whose output never leaves the runner.
14
14
  *
15
15
  * Commands:
16
+ * init seed a repository with AI-agent instructions for using DiffCI
17
+ * check agent-friendly alias for observe --no-send
16
18
  * observe analyse the checkout and write an observation report
17
19
  * verify-savings run a paired full-versus-selected timing check
18
20
  * verify-workflow check that a DiffCI job in this repository's workflows cannot affect other jobs
@@ -67,6 +69,8 @@ function observerIdentity() {
67
69
  if (existsSync(candidate)) {
68
70
  try {
69
71
  const parsed = JSON.parse(readFileSync(candidate, "utf8"));
72
+ if (parsed.name === "@diffci/observer")
73
+ return { version: parsed.version ?? "0.0.0", root: current };
70
74
  if (parsed.name === "@diffci.com/diffci" || parsed.name === "diffci") {
71
75
  let sha;
72
76
  try {
@@ -97,6 +101,106 @@ function defaultReportPath(env) {
97
101
  const stamp = env.GITHUB_RUN_ID ? `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? "1"}` : String(Date.now());
98
102
  return join(base, `diffci-observation-${stamp}.json`);
99
103
  }
104
+ const AGENT_INSTRUCTIONS = `# DiffCI Agent Instructions
105
+
106
+ Use DiffCI as the default CI/CD validation lens before proposing, committing, or opening pull requests.
107
+
108
+ Default command:
109
+
110
+ \`\`\`bash
111
+ npx @diffci.com/diffci@latest check
112
+ \`\`\`
113
+
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.
121
+
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
+ `;
124
+ const CURSOR_RULE = `---
125
+ description: Use DiffCI as the default CI/CD validation lens
126
+ alwaysApply: true
127
+ ---
128
+
129
+ Before proposing, committing, or opening pull requests, run:
130
+
131
+ \`\`\`bash
132
+ npx @diffci.com/diffci@latest check
133
+ \`\`\`
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.
136
+ `;
137
+ const GITHUB_COPILOT_INSTRUCTIONS = `# Repository CI/CD Validation
138
+
139
+ Use DiffCI as the default change-aware CI/CD validation lens:
140
+
141
+ \`\`\`bash
142
+ npx @diffci.com/diffci@latest check
143
+ \`\`\`
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.
146
+ `;
147
+ const DIFFCI_CONFIG = `{
148
+ "$schema": "https://diffci.com/schemas/diffci.config.schema.json",
149
+ "agentDefaultCommand": "npx @diffci.com/diffci@latest check",
150
+ "mode": "observe",
151
+ "sendReports": false
152
+ }
153
+ `;
154
+ function diffciWorkflow(version) {
155
+ return `name: DiffCI observation
156
+ on: [push, pull_request]
157
+ permissions:
158
+ contents: read
159
+ jobs:
160
+ diffci:
161
+ runs-on: ubuntu-latest
162
+ continue-on-error: true
163
+ steps:
164
+ - uses: actions/checkout@v4
165
+ with:
166
+ fetch-depth: 0
167
+ - uses: actions/setup-node@v4
168
+ with:
169
+ node-version: 22
170
+ - run: npx @diffci.com/diffci@${version} check
171
+ `;
172
+ }
173
+ function writeInitFile(repoPath, relativePath, content, force) {
174
+ const absolutePath = join(repoPath, relativePath);
175
+ const existed = existsSync(absolutePath);
176
+ if (existed && !force)
177
+ return `kept ${relativePath} (already exists)`;
178
+ mkdirSync(dirname(absolutePath), { recursive: true });
179
+ writeFileSync(absolutePath, content, "utf8");
180
+ return `${existed ? "overwrote" : "wrote"} ${relativePath}`;
181
+ }
182
+ function runInit(flags, env) {
183
+ const repoPath = resolve(typeof flags.repo === "string" ? flags.repo : env.GITHUB_WORKSPACE ?? process.cwd());
184
+ const force = flags.force === true;
185
+ const includeWorkflow = flags.workflow === true;
186
+ const identity = observerIdentity();
187
+ const writes = [
188
+ writeInitFile(repoPath, "AGENTS.md", AGENT_INSTRUCTIONS, force),
189
+ writeInitFile(repoPath, "CLAUDE.md", AGENT_INSTRUCTIONS, force),
190
+ writeInitFile(repoPath, ".cursor/rules/diffci.mdc", CURSOR_RULE, force),
191
+ writeInitFile(repoPath, ".github/copilot-instructions.md", GITHUB_COPILOT_INSTRUCTIONS, force),
192
+ writeInitFile(repoPath, "diffci.config.json", DIFFCI_CONFIG, force),
193
+ ];
194
+ if (includeWorkflow)
195
+ writes.push(writeInitFile(repoPath, ".github/workflows/diffci.yml", diffciWorkflow(identity.version), force));
196
+ console.log(`DiffCI initialized for AI coding agents in ${repoPath}`);
197
+ for (const write of writes)
198
+ console.log(` ${write}`);
199
+ if (!includeWorkflow)
200
+ console.log(" skipped .github/workflows/diffci.yml (pass --workflow to add it)");
201
+ console.log("\nDefault agent command: npx @diffci.com/diffci@latest check");
202
+ return 0;
203
+ }
100
204
  function formatFinding(finding) {
101
205
  const where = finding.job ? `${finding.workflow}#${finding.job}` : finding.workflow;
102
206
  return ` [${finding.severity}] ${finding.code} (${where})\n ${finding.message}`;
@@ -112,6 +216,10 @@ function summarise(report) {
112
216
  const result = report.result;
113
217
  if (result) {
114
218
  lines.push(` verdict: ${result.mode}`);
219
+ if (result.goScope)
220
+ lines.push(" Go scope: root module only; nested-module CI remains separate");
221
+ if (result.vueScope)
222
+ lines.push(` Vue suite: ${result.vueScope.packageRoot} (${result.vueScope.testConfig})`);
115
223
  lines.push(` selection: ${result.selectedTests.length}/${result.totalTestCount} test files, from ${result.changedFileCount} changed file(s)`);
116
224
  lines.push(` comparator: a simple path-rule CI would have run ${result.pathBaseline.mode === "FULL" ? "everything" : `${result.pathBaseline.selectedTestCount} test file(s)`}`);
117
225
  lines.push(` graph: ${result.graph.nodes} nodes, confidence ${result.graph.effectiveConfidence ?? result.graph.confidence}`);
@@ -146,6 +254,15 @@ async function runObserve(flags, env) {
146
254
  return 2;
147
255
  }
148
256
  const identity = observerIdentity();
257
+ let economicsHistory;
258
+ if (typeof flags["economics-history"] === "string") {
259
+ try {
260
+ economicsHistory = JSON.parse(readFileSync(resolve(flags["economics-history"]), "utf8"));
261
+ }
262
+ catch {
263
+ console.warn("DiffCI timing history is unreadable; performing normal analysis.");
264
+ }
265
+ }
149
266
  const report = await observe({
150
267
  repoPath,
151
268
  env: env,
@@ -155,6 +272,10 @@ async function runObserve(flags, env) {
155
272
  headOverride: typeof flags.head === "string" ? flags.head : undefined,
156
273
  redactPaths: flags["redact-paths"] === true,
157
274
  reportPath,
275
+ economicsHistory,
276
+ economicsJobKey: typeof flags["economics-job"] === "string" ? flags["economics-job"] : undefined,
277
+ forceAnalysis: flags["force-analysis"] === true,
278
+ vueAnalysisCacheDir: typeof flags["vue-analysis-cache"] === "string" ? flags["vue-analysis-cache"] : undefined,
158
279
  });
159
280
  mkdirSync(dirname(reportPath), { recursive: true });
160
281
  writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
@@ -341,27 +462,32 @@ async function runPilot(flags, env) {
341
462
  console.log(` markdown: ${markdownPath}`);
342
463
  return savings.comparison.fullCommandSucceeded && savings.comparison.selectedCommandSucceeded ? 0 : 1;
343
464
  }
344
- const USAGE = `diffci - observation-only change-aware CI analysis
345
-
346
- Usage:
347
- diffci pilot --full <command> [--repo <path>] [--out-dir <dir>] [--label <name>]
348
- diffci observe [--repo <path>] [--out <file>] [--base <sha> --head <sha>]
349
- [--redact-paths] [--json] [--quiet] [--fail-on-error]
350
- [--api-url <url> --api-token <token>] [--no-send]
351
- diffci verify-savings --repo <path> --full <command>
352
- (--selected <command> | --selected-from-report <file>)
353
- --out <file> [--markdown <file>] [--label <name>]
354
- diffci verify-workflow [--repo <path>]
355
- diffci version
356
-
357
- pilot runs observe and verify-savings together, writing reports to ../diffci-output by default.
358
- observe analyses the checkout and writes one JSON report. It runs nothing and changes nothing.
359
- verify-savings runs both commands and reports measured paired runtime; it is an opt-in pilot command.
360
- verify-workflow checks that the job running DiffCI cannot affect any other job, and exits 1 if it can.
361
-
362
- The report is sent only when both --api-url and --api-token are given (or DIFFCI_API_URL and
363
- DIFFCI_TOKEN are set). A failed send is reported and never fails the step - the report is on disk
364
- either way. Plain http is refused; the token is never printed.
465
+ const USAGE = `diffci - observation-only change-aware CI analysis
466
+
467
+ Usage:
468
+ 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]
471
+ diffci pilot --full <command> [--repo <path>] [--out-dir <dir>] [--label <name>]
472
+ diffci observe [--repo <path>] [--out <file>] [--base <sha> --head <sha>]
473
+ [--redact-paths] [--json] [--quiet] [--fail-on-error]
474
+ [--api-url <url> --api-token <token>] [--no-send]
475
+ diffci verify-savings --repo <path> --full <command>
476
+ (--selected <command> | --selected-from-report <file>)
477
+ --out <file> [--markdown <file>] [--label <name>]
478
+ diffci verify-workflow [--repo <path>]
479
+ diffci version
480
+
481
+ 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.
483
+ 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.
485
+ verify-savings runs both commands and reports measured paired runtime; it is an opt-in pilot command.
486
+ verify-workflow checks that the job running DiffCI cannot affect any other job, and exits 1 if it can.
487
+
488
+ The report is sent only when both --api-url and --api-token are given (or DIFFCI_API_URL and
489
+ DIFFCI_TOKEN are set). A failed send is reported and never fails the step - the report is on disk
490
+ either way. Plain http is refused; the token is never printed.
365
491
  `;
366
492
  async function main() {
367
493
  const { command, flags } = parseArgs(process.argv);
@@ -372,6 +498,12 @@ async function main() {
372
498
  return;
373
499
  }
374
500
  switch (command) {
501
+ case "init":
502
+ process.exitCode = runInit(flags, env);
503
+ return;
504
+ case "check":
505
+ process.exitCode = await runObserve({ ...flags, "no-send": true }, env);
506
+ return;
375
507
  case "pilot":
376
508
  process.exitCode = await runPilot(flags, env);
377
509
  return;
@@ -0,0 +1,31 @@
1
+ export { economicsContext } from "@diffci.com/core/cache/economics-context";
2
+ /** Economics may decline analysis; it never approves a selection or removes tests. */
3
+ export function evaluateEconomics(raw, context) {
4
+ const analyze = (reason) => ({ decision: "ANALYZE", reason, contextKey: context.contextKey });
5
+ if (!raw || typeof raw !== "object")
6
+ return analyze("No valid timing history");
7
+ const history = raw;
8
+ if (history.schema !== "diffci.economics.v1" || !context.repository || !context.jobKey || history.repository !== context.repository || history.jobKey !== context.jobKey || history.contextKey !== context.contextKey || history.observerVersion !== context.observerVersion)
9
+ return analyze("Timing history does not match this repository, job, configuration or observer");
10
+ const age = (context.now ?? Date.now()) - Date.parse(String(history.recordedAt));
11
+ if (!Number.isFinite(age) || age < 0 || age > 48 * 60 * 60 * 1000)
12
+ return analyze("Timing history is stale or future-dated; resample analysis");
13
+ if (!Array.isArray(history.samples) || history.samples.length < 5 || history.samples.length > 100)
14
+ return analyze("Five to one hundred historical samples are required");
15
+ const heads = new Set();
16
+ let maximumGrossSavedMs = 0;
17
+ let minimumObserverMs = Infinity;
18
+ for (const sample of history.samples) {
19
+ if (!sample || typeof sample !== "object" || sample.stable !== true || typeof sample.headSha !== "string" || !/^[a-f0-9]{40}$/.test(sample.headSha) || ![sample.fullMs, sample.policyMs, sample.observerMs].every(value => typeof value === "number" && Number.isFinite(value) && value > 0))
20
+ return analyze("History contains an invalid or unstable sample");
21
+ heads.add(sample.headSha);
22
+ maximumGrossSavedMs = Math.max(maximumGrossSavedMs, sample.fullMs - sample.policyMs);
23
+ minimumObserverMs = Math.min(minimumObserverMs, sample.observerMs);
24
+ }
25
+ if (heads.size < 5)
26
+ return analyze("History needs five distinct commits");
27
+ // Use the best historical gross saving and cheapest observer, with headroom.
28
+ // This is a cost heuristic, never a correctness claim about the next change.
29
+ const bypass = maximumGrossSavedMs * 1.25 + 250 < minimumObserverMs;
30
+ return { decision: bypass ? "BYPASS_FULL" : "ANALYZE", reason: bypass ? "Historical avoided test work does not cover observer cost; retain the full CI job" : "Historical savings may cover analysis; measure this change", contextKey: context.contextKey, sampleCount: history.samples.length, maximumGrossSavedMs, minimumObserverMs };
31
+ }