@gotgenes/pi-autoformat 0.1.0 → 4.0.3

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 (75) hide show
  1. package/.github/workflows/ci.yml +1 -3
  2. package/.github/workflows/release-please.yml +29 -0
  3. package/.markdownlint-cli2.yaml +14 -2
  4. package/.pi/extensions/pi-autoformat/config.json +3 -6
  5. package/.pi/prompts/README.md +59 -0
  6. package/.pi/prompts/plan-issue.md +64 -0
  7. package/.pi/prompts/retro.md +144 -0
  8. package/.pi/prompts/ship-issue.md +77 -0
  9. package/.pi/prompts/tdd-plan.md +67 -0
  10. package/.pi/skills/pi-extension-lifecycle/SKILL.md +256 -0
  11. package/.release-please-manifest.json +1 -1
  12. package/AGENTS.md +39 -0
  13. package/CHANGELOG.md +365 -0
  14. package/README.md +42 -109
  15. package/biome.json +1 -1
  16. package/docs/assets/logo.png +0 -0
  17. package/docs/assets/logo.svg +533 -0
  18. package/docs/configuration.md +358 -38
  19. package/docs/plans/0001-initial-implementation-plan.md +17 -9
  20. package/docs/plans/0002-richer-tui-formatter-summaries.md +220 -0
  21. package/docs/plans/0003-additional-pi-mutation-tools.md +273 -0
  22. package/docs/plans/0004-shell-driven-mutation-coverage.md +296 -0
  23. package/docs/plans/0010-acceptance-test-coverage.md +240 -0
  24. package/docs/plans/0012-remove-unused-formatter-extensions-field.md +152 -0
  25. package/docs/plans/0013-fallback-chain-step-type.md +280 -0
  26. package/docs/plans/0014-batch-by-default-formatter-dispatch.md +195 -0
  27. package/docs/plans/0015-builtin-treefmt-and-treefmt-nix-support.md +290 -0
  28. package/docs/plans/0016-detailed-formatter-output-on-failure.md +245 -0
  29. package/docs/plans/0022-pi-coding-agent-types.md +201 -0
  30. package/docs/plans/0027-format-before-agent-exit-follow-up-turn.md +355 -0
  31. package/docs/plans/0031-turn-end-flush-with-change-detection.md +365 -0
  32. package/docs/retro/0002-richer-tui-formatter-summaries.md +47 -0
  33. package/docs/retro/0013-fallback-chain-step-type.md +67 -0
  34. package/docs/retro/0015-builtin-treefmt-and-treefmt-nix-support.md +56 -0
  35. package/docs/retro/0016-detailed-formatter-output-on-failure.md +60 -0
  36. package/docs/retro/0022-pi-coding-agent-types.md +62 -0
  37. package/docs/testing.md +95 -0
  38. package/package.json +30 -11
  39. package/prek.toml +2 -2
  40. package/schemas/pi-autoformat.schema.json +145 -21
  41. package/src/builtin-formatters.ts +205 -0
  42. package/src/command-probe.ts +66 -0
  43. package/src/config-loader.ts +829 -90
  44. package/src/custom-mutation-tools.ts +125 -0
  45. package/src/extension.ts +469 -82
  46. package/src/format-scope.ts +118 -0
  47. package/src/formatter-config.ts +73 -36
  48. package/src/formatter-executor.ts +230 -34
  49. package/src/formatter-output-report.ts +149 -0
  50. package/src/formatter-registry.ts +139 -30
  51. package/src/index.ts +26 -5
  52. package/src/prompt-autoformatter.ts +148 -23
  53. package/src/shell-mutation-detector.ts +572 -0
  54. package/src/touched-files-queue.ts +72 -11
  55. package/test/acceptance-event-bus.test.ts +138 -0
  56. package/test/acceptance.test.ts +69 -0
  57. package/test/builtin-formatters.test.ts +382 -0
  58. package/test/command-probe.test.ts +79 -0
  59. package/test/config-loader.test.ts +640 -21
  60. package/test/custom-mutation-tools.test.ts +190 -0
  61. package/test/extension.test.ts +1535 -158
  62. package/test/fallback-acceptance.test.ts +98 -0
  63. package/test/fixtures/event-bus-emitter.ts +26 -0
  64. package/test/fixtures/formatter-recorder.mjs +25 -0
  65. package/test/format-scope.test.ts +139 -0
  66. package/test/formatter-config.test.ts +56 -5
  67. package/test/formatter-executor.test.ts +555 -35
  68. package/test/formatter-output-report.test.ts +178 -0
  69. package/test/formatter-registry.test.ts +330 -37
  70. package/test/helpers/rpc.ts +146 -0
  71. package/test/prompt-autoformatter.test.ts +315 -22
  72. package/test/schema.test.ts +149 -0
  73. package/test/shell-mutation-detector.test.ts +221 -0
  74. package/test/touched-files-queue.test.ts +40 -1
  75. package/test/types/theme-stub.test-d.ts +42 -0
@@ -0,0 +1,152 @@
1
+ ---
2
+ issue: 12
3
+ issue_title: "Remove unused `extensions` field from formatter definitions"
4
+ ---
5
+
6
+ # Plan: Remove Unused `extensions` Field From Formatter Definitions (Issue #12)
7
+
8
+ ## Problem Statement
9
+
10
+ `FormatterDefinition.extensions` is required by the type, populated in built-in defaults, validated by the config loader, declared in the JSON schema, and shown in docs — but no code in the dispatch path reads it.
11
+ Dispatch is purely chain-driven (`config.chains?.[extension]`).
12
+ The field is dead weight and a maintenance trap: users updating `chains` can leave per-formatter `extensions` metadata stale with no feedback.
13
+ Removing it sharpens the conceptual model: `formatters` answers **how** to invoke a tool, `chains` answers **when** and **in what order**.
14
+
15
+ This is a **breaking change** to the public config and TypeScript types.
16
+
17
+ ## Goals
18
+
19
+ - Drop `extensions` from `FormatterDefinition` (TypeScript type, schema, defaults, docs, README).
20
+ - Have the config loader silently drop a stray `extensions` key on a user-provided formatter, with one non-fatal config-issue notice per occurrence so stale metadata is visible without breaking existing configs.
21
+ - Keep dispatch behavior unchanged — `chains` already drives everything.
22
+ - Keep schema, loader, defaults, and docs aligned (per AGENTS.md).
23
+
24
+ This is a **breaking change** for users who write code against the exported `FormatterDefinition` type.
25
+ Runtime config files that still carry `extensions` continue to load.
26
+
27
+ ## Non-Goals
28
+
29
+ - Adding per-formatter `when` predicates (referenced in the issue as future work — leave attachment point clean, do not implement).
30
+ - Adding chain-level `fallback` steps (#13).
31
+ - Auto-rewriting users' on-disk config files.
32
+ - Changing dispatch, batching, or chain resolution.
33
+
34
+ ## Background
35
+
36
+ Relevant modules:
37
+
38
+ - `src/formatter-registry.ts` — declares `FormatterDefinition` (with required `extensions`); `groupFilesByChain` and `resolveChain` never read `extensions`.
39
+ - `src/formatter-config.ts` — built-in defaults set `extensions` on `prettier` and `markdownlint-cli2`; the same extensions are also enumerated in `chains`, so `chains` is the single source of truth at runtime.
40
+ - `src/config-loader.ts` — `validateFormatterDefinition` requires both `command` and `extensions`, calls `validateExtensionArray`, and returns the validated `extensions` on the resolved definition.
41
+ - `schemas/pi-autoformat.schema.json` — `formatterDefinition.extensions` is declared (not in `required`, but documented).
42
+ - `docs/configuration.md` and `README.md` — example configs and the formatter-definition reference both list `extensions`.
43
+ - Tests across `test/config-loader.test.ts`, `test/formatter-registry.test.ts`, `test/formatter-config.test.ts` construct definitions with `extensions`.
44
+
45
+ ## Design Overview
46
+
47
+ ### Type shape
48
+
49
+ ```typescript
50
+ // before
51
+ export type FormatterDefinition = {
52
+ command: string[];
53
+ extensions: string[];
54
+ environment?: Record<string, string>;
55
+ disabled?: boolean;
56
+ };
57
+
58
+ // after
59
+ export type FormatterDefinition = {
60
+ command: string[];
61
+ environment?: Record<string, string>;
62
+ disabled?: boolean;
63
+ };
64
+ ```
65
+
66
+ ### Config-loader migration
67
+
68
+ `validateFormatterDefinition` currently treats `extensions` as a required, validated property and rejects unknown keys.
69
+ After the change:
70
+
71
+ - `extensions` is **not** in `FormatterDefinition` and is **not** required.
72
+ - If a user config still includes `extensions` on a formatter, the loader emits a single non-fatal config issue (`formatters.<name>.extensions` → "Deprecated. Remove this field; dispatch is driven by `chains`. The value is ignored.") and discards the value.
73
+ This is consistent with the existing config-issue plumbing — surfacing the trap without breaking startup.
74
+ - Any other unknown formatter key continues to raise the existing "Unknown formatter property." issue.
75
+
76
+ ### Schema
77
+
78
+ - Remove the `extensions` property from `$defs.formatterDefinition.properties`.
79
+ - Schema stays `additionalProperties: false`.
80
+ Editor validators will flag stale `extensions` keys as unknown — that is the desired UX for editor users; the runtime loader still tolerates them with a deprecation notice for already-deployed configs.
81
+
82
+ ### Defaults
83
+
84
+ `DEFAULT_FORMATTER_CONFIG.formatters.prettier` and `markdownlint-cli2` lose their `extensions` arrays.
85
+ The `chains` map is unchanged and remains the single source of truth.
86
+
87
+ ### Docs
88
+
89
+ - `docs/configuration.md` and `README.md`: drop `extensions` from JSON examples and from the `FormatterDefinition` reference.
90
+ - Add a short note in `docs/configuration.md` explaining that legacy `extensions` keys are accepted but ignored, and recommending removal.
91
+
92
+ ### Edge cases
93
+
94
+ - A formatter that currently has only `command` + `extensions` and no chain entry: previously already a no-op at dispatch (chain-driven); behavior unchanged.
95
+ - An empty user-supplied `extensions: []`: previously failed validation (`minItems: 1`).
96
+ After: still surfaces the deprecation notice and is dropped — does not fail.
97
+
98
+ ## Module-Level Changes
99
+
100
+ - `src/formatter-registry.ts`
101
+ - Remove `extensions: string[]` from `FormatterDefinition`.
102
+ - `src/formatter-config.ts`
103
+ - Remove `extensions` arrays from the two built-in formatter defaults.
104
+ - `src/config-loader.ts`
105
+ - Remove `validateExtensionArray` (no other callers) **only if unused** after the change; otherwise leave in place.
106
+ - In `validateFormatterDefinition`:
107
+ - Drop `extensions` from the required-fields gate.
108
+ - Replace the `key === "extensions"` branch with a single deprecation notice and skip storing the value.
109
+ - Stop returning `extensions` on the resolved definition.
110
+ - `schemas/pi-autoformat.schema.json`
111
+ - Remove the `extensions` property from `$defs.formatterDefinition.properties`.
112
+ - `docs/configuration.md`, `README.md`
113
+ - Strip `extensions` from examples and field reference; add deprecation note.
114
+ - `test/config-loader.test.ts`, `test/formatter-registry.test.ts`, `test/formatter-config.test.ts`, and any other test that constructs a `FormatterDefinition`
115
+ - Drop `extensions` from constructed definitions.
116
+ - Add new coverage (see TDD Order).
117
+
118
+ ## TDD Order
119
+
120
+ 1. **red** — Add a `config-loader.test.ts` case: a user formatter with `extensions: [".ts"]` loads successfully, the resolved definition has no `extensions` key, and a single config issue is recorded for `formatters.<name>.extensions` describing the deprecation.
121
+ commit: `test: cover deprecated extensions field on formatter`
122
+ 2. **green** — Update `validateFormatterDefinition` to drop `extensions` from the required gate, emit the deprecation notice, and stop populating the field.
123
+ Remove `validateExtensionArray` if it has no remaining callers.
124
+ commit: `feat!: drop extensions field from formatter definitions`
125
+ 3. **red→green** — Update `formatter-registry.ts` type, `formatter-config.ts` defaults, and any existing tests that constructed `extensions`.
126
+ Tests should still pass (or be updated to no longer reference `extensions`).
127
+ Add a `formatter-config.test.ts` assertion that `DEFAULT_FORMATTER_CONFIG.formatters.prettier` has no `extensions` key.
128
+ commit: `feat!: remove extensions from FormatterDefinition type and defaults`
129
+ 4. **red→green** — Update `schemas/pi-autoformat.schema.json` to remove the `extensions` property and add a schema-shape test (or extend an existing one) that verifies the schema no longer declares it.
130
+ commit: `feat!: drop extensions from pi-autoformat JSON schema`
131
+ 5. **docs** — Update `docs/configuration.md` and `README.md`: strip `extensions` from examples and reference; add a deprecation note pointing users at `chains`.
132
+ commit: `docs: remove formatter extensions field and note deprecation`
133
+
134
+ Each cycle leaves the repo in a green state.
135
+
136
+ ## Risks and Mitigations
137
+
138
+ - **Risk:** Existing user configs on disk still contain `extensions`.
139
+ **Mitigation:** Loader accepts and ignores them with a single deprecation notice per formatter; no startup failure.
140
+ - **Risk:** Editor validation flags `extensions` as an unknown property under `additionalProperties: false`.
141
+ **Mitigation:** This is the intended signal — the runtime tolerates it. Document the removal in `docs/configuration.md`.
142
+ - **Risk:** Downstream code imports `FormatterDefinition` and constructs values with `extensions`.
143
+ **Mitigation:** Marked as breaking (`feat!:`). Release-please will bump major; CHANGELOG will call out the removal.
144
+ - **Risk:** Tests across multiple files reference `extensions`.
145
+ **Mitigation:** TDD cycle 3 sweeps all tests in one commit; CI catches stragglers.
146
+
147
+ ## Open Questions
148
+
149
+ - Should the deprecation notice be a one-time aggregate ("config still uses `extensions` on N formatters") instead of one-per-formatter?
150
+ Defer until we see real configs in the wild; per-formatter is more actionable.
151
+ - Should we provide a one-shot codemod (`pi-autoformat migrate`) to strip `extensions` from on-disk configs?
152
+ Defer; the deprecation notice is sufficient for now.
@@ -0,0 +1,280 @@
1
+ ---
2
+ issue: 13
3
+ issue_title: "Add `fallback` step type to formatter chains"
4
+ ---
5
+
6
+ # Plan: Add `fallback` Step Type to Formatter Chains (Issue #13)
7
+
8
+ ## Problem Statement
9
+
10
+ Today, `chains` entries are flat lists of formatter names — every named formatter runs in order and is expected to apply.
11
+ That works when each step does a distinct job (Prettier formats, then markdownlint-cli2 lints), but it does not handle the common case where a project standardizes on **one of several alternatives** (e.g. Biome *or* Prettier).
12
+ Users either list both (the second undoes the first or fails noisily) or pick one globally (wrong half the time).
13
+ A `fallback` step type lets a single global default chain adapt to whichever tool a given repo actually uses, with no per-project boilerplate.
14
+
15
+ ## Goals
16
+
17
+ - Extend chain step shape: each step is either a string (single formatter, current behavior) or `{ "fallback": [name, ...] }`.
18
+ - Implement `PATH`-only fallthrough semantics: skip when the command is not on `PATH`; stop on first formatter that runs (success **or** non-zero exit); group is a no-op if no formatter is on `PATH`.
19
+ - Cache `PATH` probes per flush so a chain step does not re-probe the same command repeatedly.
20
+ - Surface which fallback alternative actually ran when it was not the first listed (e.g. `prettier (fallback after biome unavailable)`).
21
+ - Update schema, config loader, docs, and README in the same change (per AGENTS.md).
22
+ - Stay **fully backward compatible** with existing string-only chains.
23
+
24
+ This is a **non-breaking** change.
25
+ Existing configs and types continue to work unchanged.
26
+
27
+ ## Non-Goals
28
+
29
+ - A per-formatter `when: { configExists: [...] }` predicate — explicitly deferred per the issue.
30
+ - Auto-detecting which formatter a repo "really" uses based on its config files.
31
+ - Parallel probing of fallback alternatives — sequential is fine; the list is short and the probe is cached.
32
+ - Cross-flush caching of `PATH` probes (a flush is short-lived; per-flush is enough).
33
+ - Reworking `groupFilesByChain`'s grouping key beyond what the new shape requires.
34
+
35
+ ## Background
36
+
37
+ Relevant modules:
38
+
39
+ - `src/formatter-registry.ts` — declares `FormatterConfig` with `chains?: Record<string, string[]>` and exposes:
40
+ - `groupFilesByChain(files, config)` — keys files by chain identity (joined with `\u0000`).
41
+ - `resolveChain(chainNames, config)` — turns formatter names into `ResolvedFormatter[]`, dropping disabled / missing entries.
42
+ - `src/formatter-executor.ts` — `executeChainGroup` runs a resolved chain once per group, appending the group's files as trailing args. Returns one `BatchRun` per chain step.
43
+ - `src/prompt-autoformatter.ts` — orchestrates flush: queue → `groupFilesByChain` → `resolveChain` → `executeChainGroup`.
44
+ - `src/config-loader.ts` — `validateChains` uses `validateStringArray`, so today every step must be a string.
45
+ - `src/formatter-config.ts` — built-in defaults populate `chains` with string arrays only.
46
+ - `schemas/pi-autoformat.schema.json` — `chains` items are `{ "type": "string" }`.
47
+ - `src/extension.ts` — `summarizeFailures` reads `run.formatterName` / `run.files` from `BatchRun`s.
48
+
49
+ The architecture is already chain-oriented and batch-based; the work is to extend the chain *step* type and have the executor decide, per fallback group, which single formatter actually runs.
50
+
51
+ ## Design Overview
52
+
53
+ ### Data shapes
54
+
55
+ A chain becomes a list of *steps*.
56
+ A step is either a formatter name (string) or a fallback group.
57
+
58
+ ```typescript
59
+ export type FallbackChainStep = {
60
+ fallback: string[];
61
+ };
62
+
63
+ export type ChainStep = string | FallbackChainStep;
64
+
65
+ export type FormatterConfig = {
66
+ formatters: Record<string, FormatterDefinition>;
67
+ chains?: Record<string, ChainStep[]>;
68
+ };
69
+ ```
70
+
71
+ Internally, after validation, we normalize every step to a discriminated form so downstream code does not branch on `typeof`:
72
+
73
+ ```typescript
74
+ type NormalizedChainStep =
75
+ | { kind: "single"; formatter: string }
76
+ | { kind: "fallback"; formatters: string[] };
77
+ ```
78
+
79
+ The string form is sugar for `{ kind: "single", formatter: name }`.
80
+ A `{ fallback: [a] }` group with one entry is *not* rewritten into a single step — it stays a fallback group of one so reporting (and `PATH` probing) is consistent.
81
+
82
+ ### Grouping key
83
+
84
+ `groupFilesByChain` currently keys on the chain-name list joined by `\u0000`.
85
+ The key must remain stable and JSON-comparable for normalized steps.
86
+ Use a canonical encoding:
87
+
88
+ - single step → `"S:<name>"`
89
+ - fallback group → `"F:<a>|<b>|<c>"`
90
+
91
+ Then join steps with `\u0000`.
92
+ This keeps grouping behavior identical for string-only chains (different prefix is fine — the encoding is internal) and gives fallback groups a unique, comparable identity.
93
+
94
+ ### Resolution and execution
95
+
96
+ `resolveChain(steps, config)` returns a list of *resolved steps*, where each step carries either one resolved formatter or an ordered list of resolved-formatter alternatives:
97
+
98
+ ```typescript
99
+ type ResolvedSingleStep = {
100
+ kind: "single";
101
+ formatter: ResolvedFormatter;
102
+ };
103
+
104
+ type ResolvedFallbackStep = {
105
+ kind: "fallback";
106
+ alternatives: ResolvedFormatter[]; // disabled/unknown formatters dropped
107
+ };
108
+
109
+ type ResolvedChainStep = ResolvedSingleStep | ResolvedFallbackStep;
110
+ ```
111
+
112
+ A fallback step whose alternatives all reduce to disabled / unknown is dropped (same posture as a single step that names a missing formatter).
113
+
114
+ `executeChainGroup` then, for each resolved step:
115
+
116
+ - **single**: behaves exactly as today.
117
+ - **fallback**: probes each alternative's `command[0]` against `PATH`, **in order**, using a cached probe.
118
+ The first alternative that is on `PATH` runs.
119
+ If it exits 0 → success, stop.
120
+ If it exits non-zero → failure, stop, report (do **not** mask by trying the next alternative).
121
+ If none of the alternatives are on `PATH` → no `BatchRun` is emitted (group is a no-op as specified).
122
+
123
+ ### `PATH` probing
124
+
125
+ Implemented as a small helper that:
126
+
127
+ - accepts an absolute path verbatim (returns true if the file exists and is executable).
128
+ - otherwise walks `process.env.PATH`, checking each segment for an executable matching the command name (Windows handling is out of scope; this extension already assumes POSIX-style invocation).
129
+ - caches results in a `Map<string, boolean>` injected per flush so a chain with the same fallback group across many file groups probes once.
130
+
131
+ The probe is dependency-injectable so tests can stub it without touching the filesystem.
132
+
133
+ ### Reporting
134
+
135
+ `BatchRun` gains an optional `fallbackContext` field used purely for human-readable reporting:
136
+
137
+ ```typescript
138
+ export type BatchRun = {
139
+ formatterName: string;
140
+ command: string[];
141
+ files: string[];
142
+ success: boolean;
143
+ exitCode: number;
144
+ stdout?: string;
145
+ stderr?: string;
146
+ fallbackContext?: {
147
+ skipped: string[]; // formatter names skipped because not on PATH
148
+ };
149
+ };
150
+ ```
151
+
152
+ `extension.ts` `summarizeFailures` and the success summary path render this as `"<name> (fallback after <skipped...> unavailable)"` when `skipped` is non-empty.
153
+ When the fallback group's first alternative wins, `fallbackContext` is omitted (quiet, same as a single-formatter step).
154
+
155
+ A fallback group that ends in "all alternatives missing" emits **no** `BatchRun` — there is nothing to report at the chain-step level — but logs a single config-issue-style notice through the existing reporter so the user is not silently formatted-nothing.
156
+ This is delivered via the standard reporter, not a new channel.
157
+
158
+ ### Config validation
159
+
160
+ `validateChains` becomes:
161
+
162
+ - accept either a string or a `{ fallback: string[] }` object per array entry.
163
+ - reject any other shape with a clear path-aware message.
164
+ - require `fallback` to be a non-empty array of non-empty strings.
165
+ - reject unknown sibling keys on a fallback object (`additionalProperties: false`).
166
+ - emit a non-fatal config-issue (and skip the offending step) when a fallback alternative names a formatter that is not present in `config.formatters` and is not a known built-in default.
167
+ This catches typos cheaply.
168
+ - the same name-existence check is applied to single string steps for consistency (it is currently silently dropped at resolve time).
169
+
170
+ ### Edge cases
171
+
172
+ - Empty `fallback: []` → validation error.
173
+ - Fallback containing one entry → legal, treated as a fallback-of-one (still PATH-probed; a missing tool quietly no-ops instead of trying to run and failing).
174
+ - A fallback alternative is `disabled: true` → treat as if absent (skip without probing).
175
+ - A fallback step where every alternative is disabled or unknown → resolved-step is dropped; behaves like a no-op step.
176
+ - Mixing fallback and single steps in one chain (e.g. `[{fallback: [...]}, "markdownlint-cli2"]`) → fully supported; second step always runs.
177
+ - Same fallback group declared on multiple extensions → grouping key is identical, so files share the group and the `PATH` probe runs once.
178
+
179
+ ## Module-Level Changes
180
+
181
+ - `src/formatter-registry.ts`
182
+ - Export `ChainStep`, `FallbackChainStep`, `NormalizedChainStep`, `ResolvedChainStep`, `ResolvedSingleStep`, `ResolvedFallbackStep`.
183
+ - Update `FormatterConfig.chains` to `Record<string, ChainStep[]>`.
184
+ - Add a `normalizeChainStep` helper used by both grouping and resolution.
185
+ - Update `groupFilesByChain` to use the new canonical encoding for the grouping key.
186
+ - Update `resolveChain` to return `ResolvedChainStep[]` and to drop fallback steps with no usable alternatives.
187
+ - `src/formatter-executor.ts`
188
+ - Add `CommandProbe` type (`(command: string) => boolean | Promise<boolean>`) and a default `PATH`-walking implementation.
189
+ - Add a per-flush cache wrapper.
190
+ - Update `ChainGroupInput.chain` to `ResolvedChainStep[]`.
191
+ - Update `executeChainGroup` to dispatch by step kind, applying fallback semantics for fallback steps.
192
+ - Add `fallbackContext` to `BatchRun` (optional).
193
+ - `src/prompt-autoformatter.ts`
194
+ - Construct one shared probe cache per `flushPrompt` call and pass it into `executeChainGroup`.
195
+ - No external API change.
196
+ - `src/config-loader.ts`
197
+ - Replace the current `validateStringArray`-based chain validator with a per-step validator that accepts string or `{ fallback: string[] }`.
198
+ - Add formatter-name existence checks (non-fatal, single config-issue per offense).
199
+ - `src/formatter-config.ts`
200
+ - Defaults stay string-only (no behavior change).
201
+ - `schemas/pi-autoformat.schema.json`
202
+ - `chains.additionalProperties.items` becomes a `oneOf` of `string` and `{ fallback: string[] }`.
203
+ - Document fallback semantics inline.
204
+ - `src/extension.ts`
205
+ - Render `fallbackContext.skipped` in success and failure summaries.
206
+ - Surface "all fallback alternatives missing" as a one-line notice.
207
+ - `README.md`
208
+ - Add the **Choosing a chain strategy** section with the project-vs-global recommendation.
209
+ - Add the **Fallback caveat** block next to the new feature description.
210
+ - Update the chains example to show a fallback group.
211
+ - `docs/configuration.md`
212
+ - Extend the `chains` section with the new step shape, semantics table, and fallback caveat.
213
+
214
+ ## TDD Order
215
+
216
+ 1. **Schema test for chain step shape.**
217
+ Extend `test/schema.test.ts` with valid string-and-fallback fixtures and invalid `{}` / `{ fallback: [] }` / extra-key fixtures.
218
+ Update `schemas/pi-autoformat.schema.json` to make them pass.
219
+ Commit: `test: cover fallback chain step shape in schema`, then `feat: allow fallback chain steps in schema`.
220
+ 2. **Config-loader: accept fallback steps.**
221
+ In `test/config-loader.test.ts`, cover string steps still loading, fallback objects loading, malformed fallback rejected, unknown sibling keys rejected, empty fallback rejected.
222
+ Implement in `src/config-loader.ts`.
223
+ Commit: `test: cover fallback step validation`, then `feat: validate fallback chain steps in config loader`.
224
+ 3. **Config-loader: formatter-name existence check.**
225
+ Tests for fallback referencing an unknown formatter (non-fatal config-issue, step dropped) and same for a single string step.
226
+ Implement.
227
+ Commit: `test: warn on chain steps referencing unknown formatters`, then `feat: surface unknown formatter names in chains as config issues`.
228
+ 4. **Registry: chain step types and grouping key.**
229
+ In `test/formatter-registry.test.ts`, cover normalized step shapes, grouping key stability for string-only chains, distinct grouping for differing fallback orderings, identical grouping for identical fallback orderings.
230
+ Implement type changes plus key encoding.
231
+ Commit: `test: extend chain grouping for fallback steps`, then `feat: support fallback steps in chain grouping`.
232
+ 5. **Registry: resolve chain returns resolved steps.**
233
+ Tests for resolving a single-only chain, a fallback with mixed disabled/unknown alternatives, dropping a fallback whose alternatives all reduce away.
234
+ Implement.
235
+ Commit: `test: resolve fallback steps to alternatives`, then `feat: resolve fallback chain steps`.
236
+ 6. **Executor: PATH probing helper.**
237
+ New tests for a probe helper covering absolute-path executables, `PATH`-walked executables, missing commands, and cache reuse.
238
+ Implement and inject.
239
+ Commit: `test: cover PATH-probe helper`, then `feat: add PATH probe with per-flush cache`.
240
+ 7. **Executor: fallback dispatch semantics.**
241
+ Tests for: first alternative present (runs, no `fallbackContext`), first missing / second present (runs, `fallbackContext.skipped` populated), first present and exits non-zero (failure surfaced, second NOT tried), all missing (no `BatchRun` emitted), single steps unchanged.
242
+ Implement in `executeChainGroup`.
243
+ Commit: `test: cover fallback dispatch semantics`, then `feat: dispatch fallback chain steps`.
244
+ 8. **Prompt autoformatter: shared probe cache.**
245
+ Test that two file groups whose chains share a fallback group probe each command at most once per flush.
246
+ Wire the cache through `flushPrompt`.
247
+ Commit: `test: share PATH probe cache across chain groups in a flush`, then `feat: share PATH probe cache across flush`.
248
+ 9. **Reporting: surface fallback context.**
249
+ Tests in `test/extension.test.ts` (or wherever `summarizeFailures` is exercised) covering success render with `fallbackContext`, failure render with `fallbackContext`, and the "all fallback alternatives missing" notice.
250
+ Implement in `src/extension.ts`.
251
+ Commit: `test: render fallback context in summaries`, then `feat: surface fallback context in flush reporting`.
252
+ 10. **Acceptance + smoke.**
253
+ End-to-end test in `test/acceptance.test.ts`: a `.ts` chain `[{ fallback: ["biome", "prettier"] }]` with biome absent and prettier present produces a single prettier batch run with `fallbackContext.skipped = ["biome"]`.
254
+ Commit: `test: end-to-end fallback chain run`.
255
+ 11. **Docs.**
256
+ Update `README.md` (Choosing-a-chain-strategy section + Fallback caveat + example) and `docs/configuration.md` (chain shape + semantics table).
257
+ Commit: `docs: document fallback chain steps and project-config recommendation`.
258
+
259
+ ## Risks and Mitigations
260
+
261
+ - **Risk: silent fallback hides missing project config.**
262
+ A globally installed Biome will format with built-in defaults in a Prettier repo.
263
+ *Mitigation:* required README **Fallback caveat** block plus the **Choosing a chain strategy** recommendation; the docs explicitly point users at project-level chains. No runtime mechanism added (per AGENTS.md: "Mechanism is forever; docs are reversible.").
264
+ - **Risk: PATH probing is platform-fragile.**
265
+ *Mitigation:* dependency-inject the probe so tests stub it; default implementation walks `process.env.PATH` and `fs.access(X_OK)`-style checks. Match the existing extension's POSIX assumption.
266
+ - **Risk: probe overhead on every flush.**
267
+ *Mitigation:* per-flush cache; only probe a command once even across many fallback groups.
268
+ - **Risk: grouping-key collision between old encoding and new.**
269
+ *Mitigation:* the encoding is internal — keys are computed fresh from the in-memory config every flush, never persisted. No migration needed.
270
+ - **Risk: users typo a formatter name in a fallback group.**
271
+ *Mitigation:* config loader emits a non-fatal config-issue per occurrence and drops the offending entry.
272
+ - **Risk: scope creep into a `when` predicate.**
273
+ *Mitigation:* explicitly out of scope per the issue and re-asserted in Non-Goals.
274
+
275
+ ## Open Questions
276
+
277
+ - Should the "all fallback alternatives missing" notice be one-shot per flush, or one per group?
278
+ Tentatively: one per group, but quiet — defer until we see real noise.
279
+ - Should the success summary always mention the fallback alternative that ran, or only when it was not the first listed?
280
+ Tentatively: only when it was not the first, matching the issue's reporting guidance.
@@ -0,0 +1,195 @@
1
+ ---
2
+ issue: 14
3
+ issue_title: "Batch-by-default formatter dispatch"
4
+ ---
5
+
6
+ # Plan: Batch-by-Default Formatter Dispatch (Issue #14)
7
+
8
+ ## Problem Statement
9
+
10
+ The current executor invokes each formatter once per touched file, substituting `$FILE` per call.
11
+ This pays N startup costs for N touched files, produces N noisy notifications, and is incompatible with batch-only tools like `treefmt`.
12
+ Effectively every common formatter (prettier, biome, ruff, black, gofmt, rustfmt, shfmt, dprint, markdownlint-cli2, etc.) accepts multiple paths in a single invocation, so per-file dispatch is a self-imposed limitation.
13
+
14
+ ## Goals
15
+
16
+ - Run each formatter step **once per chain group**, with all touched files in that group appended as trailing arguments.
17
+ - Adopt a single "command + args, then file paths appended" convention that mirrors `pi-formatter`'s `appendFile` shape.
18
+ - Make `tool`-mode formatting degenerate cleanly to a single-path batch.
19
+ - Surface batch failures clearly: exit code + which files were in the batch.
20
+
21
+ This is a **breaking change**.
22
+ We are not maintaining `$FILE` substitution backcompat — see Compatibility below.
23
+
24
+ ## Non-Goals
25
+
26
+ - Implementing the `fallback` step type (#13).
27
+ This plan only ensures the new batch executor is shaped so that #13 can plug in at the group level later.
28
+ - Built-in `treefmt` support (separate issue).
29
+ This plan unblocks it but does not add it.
30
+ - Parallel chain-group execution.
31
+ Groups still flush sequentially.
32
+ - Reworking the touched-files queue, mutation detectors, or scope resolution.
33
+ - Per-file outcome parsing from formatter stdout/stderr.
34
+ The result shape leaves room for it; v1 reports per-batch only.
35
+
36
+ ## Background
37
+
38
+ Relevant existing pieces:
39
+
40
+ - `src/formatter-registry.ts` — resolves a chain per file, substitutes `$FILE` in each formatter command.
41
+ - `src/formatter-executor.ts` — runs a chain for a single file, command-by-command.
42
+ - `src/prompt-autoformatter.ts` — loops over touched files, resolving and executing a chain per file.
43
+ - `src/extension.ts` — `defaultReportFlushResult` summarizes per-file results from `PromptAutoformatterResult`.
44
+
45
+ The architecture is already chain-oriented; the work is to lift the unit of execution from "one file" to "one group of files that share a chain."
46
+
47
+ ## Design Overview
48
+
49
+ ### Dispatch model
50
+
51
+ 1. After flushing the touched-files queue, group files by their chain identity (same ordered list of formatter names → same group).
52
+ Files with no chain are dropped as today.
53
+ 2. For each group, resolve the chain once (formatter name → command + env), then run each chain step once with the group's file paths appended as trailing arguments.
54
+ 3. Chain steps run sequentially within a group.
55
+ Groups themselves are processed sequentially (no concurrency change).
56
+ 4. A failing step does not abort later steps in the group — same policy as today's per-file chain.
57
+
58
+ **Grouping key**: the chain's formatter-name list joined with a NUL separator (e.g. `"prettier\0markdownlint-cli2"`).
59
+ Stable, comparable, independent of file paths.
60
+
61
+ **Separation of concerns**: grouping is about files ("which files share a chain?"); resolution is about formatters ("what command does this chain expand to?").
62
+ The two operations are split into distinct functions so neither has to know about the other's inputs.
63
+
64
+ ### Formatter command convention
65
+
66
+ A formatter's `command` is **configured args only**.
67
+ The executor appends the batch's file paths verbatim:
68
+
69
+ ```json
70
+ "formatters": {
71
+ "prettier": { "command": ["prettier", "--write"] },
72
+ "markdownlint-cli2": { "command": ["markdownlint-cli2", "--fix"] }
73
+ }
74
+ ```
75
+
76
+ No substitution.
77
+ No special tokens.
78
+ The schema rejects `$FILE` as invalid.
79
+
80
+ ### Compatibility
81
+
82
+ This is a breaking change.
83
+ Configs containing `$FILE` are rejected at config-load time with a clear validation issue:
84
+
85
+ > Formatter `prettier`: `$FILE` is no longer supported.
86
+ > Remove it — file paths are appended automatically.
87
+ > See `docs/configuration.md`.
88
+
89
+ The formatter is treated as misconfigured (skipped) until the user updates the config.
90
+ We do not silently strip the token, because silently changing the resolved command would mask real misconfiguration (e.g. `--stdin-filepath $FILE -` becoming `--stdin-filepath -`).
91
+
92
+ Validation lives in the config loader alongside existing schema checks and surfaces through the established `reportConfigIssues` path.
93
+
94
+ ### Failure handling
95
+
96
+ - Each chain-step invocation produces one `BatchRun` with: command, exit code, stdout, stderr, and the file paths it ran against.
97
+ - v1 does not parse formatter output for per-file outcomes.
98
+ On non-zero exit, the entire batch is reported as failed and stderr is surfaced once.
99
+ This avoids per-formatter parsers and keeps the contract narrow.
100
+ - Per-file output parsing is a follow-up; the result shape leaves room for it.
101
+
102
+ ### Result shape
103
+
104
+ `PromptAutoformatterResult` becomes batch-first:
105
+
106
+ ```ts
107
+ type BatchRun = {
108
+ formatterName: string;
109
+ command: string[]; // resolved command including appended files
110
+ files: string[]; // files in this batch
111
+ success: boolean;
112
+ exitCode: number;
113
+ stdout?: string;
114
+ stderr?: string;
115
+ };
116
+
117
+ type ChainGroupResult = {
118
+ chain: string[]; // formatter names, in order
119
+ files: string[]; // files in this group
120
+ runs: BatchRun[]; // one per chain step
121
+ };
122
+
123
+ type PromptAutoformatterResult = {
124
+ groups: ChainGroupResult[];
125
+ };
126
+ ```
127
+
128
+ The extension's reporter summarizes:
129
+
130
+ - success: `Autoformatted N files across M batches` (file list when short).
131
+ - failure: per-batch lines naming the formatter, exit code, and the files it touched.
132
+
133
+ ### Interaction with `tool` mode
134
+
135
+ In `tool` mode the queue is flushed after every successful mutation, so a group is naturally size 1.
136
+ The new executor still runs once, appending the single path — behavior is unchanged from the user's perspective.
137
+
138
+ ## Module-Level Changes
139
+
140
+ - `src/formatter-registry.ts`
141
+ - Remove `$FILE` substitution entirely.
142
+ - Add `groupFilesByChain(files, config)` → `Array<{ chain: string[]; files: string[] }>`.
143
+ Pure grouping; no formatter resolution.
144
+ - Add `resolveChain(chainNames, config)` → `ResolvedFormatter[]`.
145
+ Pure resolution; no file-path involvement.
146
+ Skips disabled or missing formatters as today.
147
+ - The legacy per-file `resolveFormatterChainForFile` is removed.
148
+ - `src/formatter-executor.ts`
149
+ - Replace `executeFormatterChain` with `executeChainGroup({ chain, files }, runner, options)` that runs each step once with appended file paths and returns `{ runs: BatchRun[] }`.
150
+ - `src/prompt-autoformatter.ts`
151
+ - Drive the new grouping + group-execution path.
152
+ Return the new `groups`-based result shape.
153
+ - `src/extension.ts`
154
+ - Update `defaultReportFlushResult` to read the new shape.
155
+ - `src/config-loader.ts`
156
+ - Reject `$FILE` in any formatter command with a config-issue.
157
+ - `src/formatter-config.ts`
158
+ - Drop `$FILE` from default formatter commands.
159
+ - `schemas/pi-autoformat.schema.json`
160
+ - Update the `command` description to document "args + appended file paths."
161
+ - Add a pattern or `not` constraint that rejects `$FILE` in `command` items.
162
+ - `docs/configuration.md`, `README.md`, `CHANGELOG.md`
163
+ - Document the new convention, the breaking change, and the per-batch reporting behavior.
164
+
165
+ ## TDD Order
166
+
167
+ Each step is a small red→green→commit cycle.
168
+
169
+ 1. **Registry: chain resolution.** Tests for `resolveChain(names, config)` covering ordered resolution, disabled-formatter skip, missing-formatter skip, environment passthrough.
170
+ 2. **Registry: file grouping.** Tests for `groupFilesByChain(files, config)` covering: mixed extensions → distinct groups; same extension → one group; different extensions sharing a chain → one group; files with no chain dropped; deterministic group and file ordering.
171
+ 3. **Executor: batch dispatch.** Tests for `executeChainGroup` covering single-file batch, multi-file batch, multi-step chain (each step runs once with all files appended), step failure does not abort later steps, environment overrides propagate.
172
+ 4. **Executor: command shape.** Tests asserting configured args come first and file paths are appended verbatim.
173
+ 5. **Config loader: `$FILE` rejection.** Test that a formatter command containing `$FILE` produces a validation issue and the formatter is excluded from the active config.
174
+ 6. **PromptAutoformatter: end-to-end.** Tests for the new `groups`-based result shape: mixed-extension touched set produces one group per chain; tool-mode size-1 batch still works; empty touched set yields no groups; existing chain steps run once each.
175
+ 7. **Extension: reporting.** Tests for the updated `defaultReportFlushResult` covering success summary across multiple groups and per-batch failure lines.
176
+ 8. **Defaults + schema alignment.** Update built-in formatter defaults and schema; acceptance/smoke tests assert defaults contain no `$FILE` and the schema rejects it.
177
+ 9. **Docs + changelog.** Update `docs/configuration.md`, `README.md`, and `CHANGELOG.md`.
178
+
179
+ Commit at each step using Conventional Commits, e.g. `test: cover chain grouping`, `feat: group touched files by chain`, `feat!: batch-dispatch chain steps`, `feat!: drop $FILE substitution`, `docs: document batch dispatch convention`.
180
+
181
+ ## Risks and Mitigations
182
+
183
+ - **Risk:** A user upgrades and finds their `$FILE`-using config rejected. **Mitigation:** Validation message names the formatter, says exactly what to do, and points at `docs/configuration.md`.
184
+ CHANGELOG entry flags the breaking change.
185
+ Major version bump.
186
+ - **Risk:** A formatter that genuinely needs one-file-per-invocation (e.g. a tool requiring `--stdin-filepath`) is now broken. **Mitigation:** Not present in any formatter listed in #14.
187
+ If a real case appears, add a `batch: false` per-formatter opt-out in a follow-up.
188
+ - **Risk:** A formatter aborts on the first file with an error, silently skipping later files in the batch. **Mitigation:** Documented as a known limitation; per-file output parsing is a follow-up.
189
+ Exit code is still surfaced and stderr is preserved.
190
+
191
+ ## Open Questions
192
+
193
+ - Per-formatter `batch: false` escape hatch — defer until a concrete case appears.
194
+ - Whether to fail-loud (skip the formatter) or fail-fatal (refuse to load config) on `$FILE`.
195
+ Plan goes with fail-loud-skip; revisit if it turns out to be confusing in practice.