flagrm 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +70 -31
  3. package/dist/adapters/angular/component-files.js +41 -0
  4. package/dist/adapters/angular/flow.js +121 -0
  5. package/dist/adapters/angular/holders.js +194 -0
  6. package/dist/adapters/angular/index.js +20 -1
  7. package/dist/adapters/angular/inputs.js +181 -0
  8. package/dist/adapters/dotnet/index.js +11 -1
  9. package/dist/cli.js +62 -21
  10. package/dist/core/baseline.js +98 -45
  11. package/dist/core/change-filter.js +68 -0
  12. package/dist/core/config-snapshot.js +36 -6
  13. package/dist/core/config.js +23 -7
  14. package/dist/core/detect.js +2 -2
  15. package/dist/core/doctor.js +51 -7
  16. package/dist/core/environment.js +25 -3
  17. package/dist/core/git.js +80 -16
  18. package/dist/core/hook.js +9 -6
  19. package/dist/core/install.js +26 -11
  20. package/dist/core/inventory.js +3 -1
  21. package/dist/core/markdown.js +4 -0
  22. package/dist/core/name-owner.js +179 -0
  23. package/dist/core/report.js +13 -0
  24. package/dist/core/run-cache.js +137 -0
  25. package/dist/core/scope-files.js +14 -0
  26. package/dist/core/scope.js +11 -2
  27. package/dist/core/test-runner.js +136 -0
  28. package/dist/core/types.js +9 -4
  29. package/dist/core/verify/commands.js +170 -29
  30. package/dist/core/verify/coverage.js +92 -0
  31. package/dist/core/verify/dead-code.js +9 -2
  32. package/dist/core/verify/index.js +42 -8
  33. package/dist/core/verify/log-results.js +127 -0
  34. package/dist/core/verify/orphans.js +175 -0
  35. package/dist/core/verify/removed-tests.js +90 -7
  36. package/dist/core/verify/test-results.js +56 -7
  37. package/package.json +3 -1
  38. package/skills/flagrm-remove/SKILL.md +31 -11
  39. package/skills/flagrm-remove/batch.md +38 -0
  40. package/skills/flagrm-verify/SKILL.md +12 -6
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Read test counts and names from the result files a test command writes:
3
- * TRX (`dotnet test --logger trx`) and JUnit XML (Karma, Jest, Vitest, NUnit
4
- * and most CI reporters). Both are parsed with regexes over tags and
5
- * attributes, which is enough for these machine-written formats.
3
+ * TRX (`dotnet test --logger trx`), JUnit XML (Karma, Jest, Vitest, NUnit
4
+ * and most CI reporters) and Jest JSON (`jest --json`, Vitest's `json`
5
+ * reporter). They are parsed with regexes over tags and attributes, or
6
+ * JSON.parse, which is enough for these machine-written formats.
6
7
  */
7
8
  import fs from "node:fs";
8
9
  import path from "node:path";
@@ -142,13 +143,59 @@ export function parseJUnit(xml) {
142
143
  }
143
144
  return results;
144
145
  }
146
+ /**
147
+ * Jest's `--json` output (Vitest's `json` reporter writes the same shape): a
148
+ * test is named by its `describe` titles and its own, joined like JUnit names;
149
+ * a test outside any `describe` by its file name. Undefined when the text isn't that.
150
+ */
151
+ export function parseJestJson(text) {
152
+ let json;
153
+ try {
154
+ json = JSON.parse(text);
155
+ }
156
+ catch {
157
+ return undefined;
158
+ }
159
+ if (!Array.isArray(json?.testResults))
160
+ return undefined;
161
+ const results = empty();
162
+ for (const file of json.testResults) {
163
+ if (!file || typeof file !== "object" || !Array.isArray(file.assertionResults ?? []))
164
+ return undefined;
165
+ for (const test of file.assertionResults ?? []) {
166
+ if (!test || typeof test !== "object")
167
+ return undefined;
168
+ const owners = test.ancestorTitles?.length ? test.ancestorTitles : file.name ? [path.basename(file.name)] : [];
169
+ const status = test.status ?? "";
170
+ record(results, [...owners, test.title ?? ""].join(JUNIT_SEPARATOR), status === "passed" ? "passed" : status === "failed" ? "failed" : "skipped");
171
+ }
172
+ }
173
+ return results;
174
+ }
175
+ /**
176
+ * One result file by its content: TRX, Jest JSON or JUnit XML. Undefined when
177
+ * it can't be read as any, or the XML document isn't complete (still being written).
178
+ */
179
+ function parseResultFile(text) {
180
+ if (/^\s*\{/.test(text))
181
+ return parseJestJson(text);
182
+ if (/<TestRun\b/.test(text))
183
+ return /<\/TestRun>\s*$/.test(text) ? parseTrx(text) : undefined;
184
+ return /<testsuites?\b/.test(text) && /(?:<\/testsuites?>|<testsuites?\b[^>]*\/>)\s*$/.test(text)
185
+ ? parseJUnit(text)
186
+ : undefined;
187
+ }
188
+ /** Whether results name every test that ran (read from result files), not only the failing ones a log names. */
189
+ export function hasTestNames(results) {
190
+ return results !== undefined && results.source !== "log";
191
+ }
145
192
  /**
146
193
  * Merge every result file matching `pattern` (an absolute path or glob)
147
194
  * modified at or after `since` — test runners keep older result files around.
148
195
  * Files in `claimed` that haven't changed since are skipped: another
149
196
  * project's run just wrote them, and an overlapping pattern
150
197
  * (`./**\/TestResults/*.trx`) would count them twice. The files read are
151
- * added to `claimed`. Undefined when no such file exists.
198
+ * added to `claimed`. Undefined when no such file exists or none can be read.
152
199
  */
153
200
  export function collectTestResults(pattern, since, claimed = new Map()) {
154
201
  // Filesystem timestamps can be coarser than Date.now().
@@ -164,8 +211,10 @@ export function collectTestResults(pattern, since, claimed = new Map()) {
164
211
  return undefined;
165
212
  const merged = empty();
166
213
  for (const { file } of files) {
167
- const xml = fs.readFileSync(file, "utf8");
168
- const parsed = /<TestRun\b/.test(xml) ? parseTrx(xml) : parseJUnit(xml);
214
+ // A file that is still being written, or isn't a result file, is skipped rather than failing the run.
215
+ const parsed = parseResultFile(fs.readFileSync(file, "utf8"));
216
+ if (!parsed)
217
+ continue;
169
218
  merged.total += parsed.total;
170
219
  merged.passed += parsed.passed;
171
220
  merged.failed += parsed.failed;
@@ -174,5 +223,5 @@ export function collectTestResults(pattern, since, claimed = new Map()) {
174
223
  merged.failedNames.push(...parsed.failedNames);
175
224
  merged.files.push(file);
176
225
  }
177
- return merged;
226
+ return merged.files.length ? merged : undefined;
178
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flagrm",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Guardrails for removing feature flags with AI coding agents (Claude Code, Copilot, Codex): skills, a baseline, a verify gate and a Stop hook.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -55,12 +55,14 @@
55
55
  "commander": "^13.1.0",
56
56
  "fast-glob": "^3.3.3",
57
57
  "picocolors": "^1.1.1",
58
+ "picomatch": "^4.0.7",
58
59
  "typescript": "^5.7.0",
59
60
  "yaml": "^2.7.0"
60
61
  },
61
62
  "devDependencies": {
62
63
  "@biomejs/biome": "2.5.10",
63
64
  "@types/node": "^22.13.0",
65
+ "@types/picomatch": "^4.0.3",
64
66
  "tsx": "^4.19.0",
65
67
  "vitest": "^3.0.0"
66
68
  }
@@ -16,9 +16,11 @@ You find and edit the code. `flagrm` records the state before you start and
16
16
  decides when you are done: **done means `flagrm verify <flag> --json` exited 0.**
17
17
  Run it as `npx --no-install flagrm`, one command per call, no `||` (plain `npx flagrm` is another package).
18
18
 
19
+ Several flags (`/flagrm-remove A B C`): read [batch.md](batch.md) first; it runs this workflow once per flag.
20
+
19
21
  ## 1. Preconditions
20
22
 
21
- - `git status --porcelain` must print nothing. Otherwise ask the user to commit or stash.
23
+ - `flagrm doctor` must not warn about uncommitted files (flagrm's own setup files and `exclude`d paths don't count). Otherwise ask the user to commit or stash.
22
24
  - No `flagrm.config.yaml`: run `flagrm init` and fill in project paths and build/test commands with the user.
23
25
  Its doctor reports a missing SDK, `cargo` or Docker: show it. For `cargo`/Docker, offer `flagrm scope`; `--write` only on a yes.
24
26
  - `flagrm list --json`: find the flag in `flags[]`. Not there: show the closest names and ask.
@@ -30,12 +32,12 @@ Run it as `npx --no-install flagrm`, one command per call, no `||` (plain `npx f
30
32
 
31
33
  `flagrm baseline <flag> --json` records the git commit, build and test results and the flag's `names[]`.
32
34
  A `checks[]` entry with a non-zero `exitCode` and a `failure`: a setup problem. Show the user `failure.message` (and `flagrm scope`); once fixed, rerun with `--force`.
33
- Without `failure`: a build, stop. Tests: stop if the failures (`results.failedTests`) could cover the flag's code; else ask the user to accept them as known.
35
+ Without `failure`: a build, stop. Tests: stop if the failures (`results.failedTests`) could cover the flag's code; else ask the user to accept them as known, and on a yes run `flagrm baseline <flag> --accept failures`.
34
36
 
35
37
  ## 3. Find every usage
36
38
 
37
39
  - Grep for each name in `names[]`: code, templates, config and tests. `parameterizedTests[]` are test calls passing ON/OFF.
38
- - `suggestedNames[]`: values of the flag with generic names. Record each that carries only this flag, a local, parameter or field with `--file <its file>`.
40
+ - `suggestedNames[]`: values of the flag with generic names. Record each that carries only this flag, a local, parameter or field with `--file <its file>` (repeat `--file` for each file; a component's template is added for you).
39
41
  - Follow the value further: a method or property returning it, a field or local holding it, a parameter
40
42
  it's passed as, a DI-registered bool. Record each so `verify` checks it, then grep for it:
41
43
  `flagrm baseline <flag> --name IsNewCheckoutEnabledAsync --kind wrapper` (`--kind alias` for a constant like `Flags.NewCheckout`).
@@ -67,22 +69,40 @@ Read `patterns.md` in this skill's directory when a case is unclear.
67
69
  | Check | Fix |
68
70
  |---|---|
69
71
  | `leftovers` | Remove each reference listed. A comment is a warning: reword or delete it. A test checking text you deleted tests the OFF path: delete it. ON text no test checks: say so in your report |
70
- | `dead-code` | Delete what the removal left unused |
72
+ | `dead-code` | Delete what the removal left unused. A declaration "referenced only where it is declared" (warning): delete it if only the OFF path used it; else say why it stays |
71
73
  | `build` | Fix the error your edit caused; don't touch unrelated code. `tsc` skips templates: grep `.html` for each member you removed or renamed |
72
74
  | `tests` | A new failure means ON behavior changed: fix the edit, not the test. Known failures (failed at the baseline) only warn. A test listed as no longer running (`unexplained`) must be one you removed on purpose: say why. A changed test file whose tests didn't run: run them yourself and report it |
75
+ | `lint` | Fix what your edit caused. An already-failing lint fails only on new error lines: fix those; other new lines it lists only warn |
76
+ | `coverage` | Tell the user which changed files no command checks; don't add or change commands yourself |
77
+
78
+ Exit 2 is a usage or config error: read the message. `--skip build,tests,lint,coverage` is fine for a quick check in between; only a full run counts, and the Stop hook won't let you finish on one with `--skip`. After three failed attempts on one check, show the user the findings. Don't run code generators or formatters after the passing verify; if you must, verify again.
79
+
80
+ ## 6. Hand back
81
+
82
+ 1. `flagrm verify <flag> --md` on the final tree (it reuses the passing build, test and lint runs; edit nothing after it): show its output exactly as printed; don't reword it or change its numbers. It must pass: the Stop hook checks it.
83
+ 2. **Notes**, 3–5 lines: what you kept on purpose, each remaining warning with its reason, follow-ups in the flag service. If `state` wasn't `on` and nobody confirmed ON, start with "Assumed ON everywhere".
84
+ 3. The commit message:
85
+
86
+ <!-- commit-message:start -->
87
+ **Proposed commit message**, in a code block:
88
+
89
+ ```
90
+ chore: remove <flag> feature flag
73
91
 
74
- Exit 2 is a usage or config error: read the message. `--skip build,tests` is fine for a quick check
75
- in between; only a full run counts, and the Stop hook won't let you finish on one with `--skip`. After three failed attempts on one check, show the user the findings.
92
+ <one or two sentences: which path stays and what was deleted>
93
+ ```
76
94
 
77
- ## 6. Report
95
+ - Follow the repository's convention instead when `git log --oneline -10` shows a different one.
96
+ - A ticket in the branch name (`feature/feat-24586`): add it the way the log does (`#24586`, `FEAT-24586`), else `(#24586)` after the subject.
97
+ - Wrap the body at 72 columns.
98
+ <!-- commit-message:end -->
78
99
 
79
- In 3–5 lines: what was removed, what you kept on purpose, and each remaining warning with its
80
- reason. If `state` wasn't `on` and nobody confirmed ON, start with "Assumed ON everywhere".
81
- Suggest `/flagrm-verify <flag>` for the overview and commit message. Don't commit.
100
+ Commit only when the user asked (in a batch: their answer at the start).
82
101
 
83
102
  ## Rules
84
103
 
85
104
  - Never lift code out of the OFF path into live code.
86
105
  - Match the file's formatting; don't refactor unrelated code.
87
106
  - Never edit `.flagrm/`. Rerun `flagrm baseline <flag>` only with `--name`, or with `--force` before your first edit (it refuses after).
88
- - Never `--skip` a failing check, and never change a test's expected value or the build/test commands to pass.
107
+ - Never `--skip` a failing check, and never change a test's expected value or the build, test or lint commands or `covers` to pass. If the user changes the config mid-removal (say, to leave out tests that need services), run `flagrm baseline <flag> --accept config` once they confirm.
108
+ - `baseline` and `verify` reuse a passing verify's build, test and lint results on the same tree and commands. If the user changed the machine (SDK, Docker, services) since, add `--no-reuse`.
@@ -0,0 +1,38 @@
1
+ # Remove several flags
2
+
3
+ Run SKILL.md's workflow once per flag, in the order given, one commit per flag.
4
+ Each baseline reuses the previous flag's passing verify (seconds) only if the commit leaves the tree as verified.
5
+
6
+ ## Once, before the first flag
7
+
8
+ 1. SKILL.md step 1 preconditions. Find every flag in `flagrm list --json`; for one that's missing, show the closest names and ask.
9
+ 2. One table: flag, `state`, and for any not `on` the environments in `config[]` that change.
10
+ Ask once: "Remove all of these as ON everywhere?" On a no, drop the flags the user names.
11
+ 3. Ask once: "Commit each flag once its verify passes?"
12
+
13
+ ## Per flag
14
+
15
+ Copy this checklist for each flag and tick it as you go:
16
+
17
+ ```
18
+ <flag>:
19
+ - [ ] baseline (step 2)
20
+ - [ ] every usage found (step 3)
21
+ - [ ] removed (step 4)
22
+ - [ ] verify passes (step 5)
23
+ - [ ] handed back (step 6)
24
+ - [ ] committed
25
+ ```
26
+
27
+ - The ON answer above is step 1's state answer: don't ask again.
28
+ - Commit answer yes: commit with the proposed message, then start the next flag. No: show the message,
29
+ stop, and continue once the user says it's committed. The next baseline needs the previous removal committed.
30
+ - A flag that needs the user (three failed attempts on one check, an unclear case) pauses the batch.
31
+ Never start a flag while the previous one is uncommitted.
32
+ - Optional: if you can start subagents, run each flag in its own, one at a time. A subagent sees no
33
+ conversation history: give it the flag, that it is confirmed ON everywhere, the commit answer, and
34
+ "follow flagrm-remove's SKILL.md steps 2–6".
35
+
36
+ ## At the end
37
+
38
+ One table: flag, commit, remaining warnings, kept on purpose.
@@ -35,14 +35,20 @@ Output, in this order:
35
35
  - follow-ups outside the repo: archive the flag in the flag service
36
36
  (LaunchDarkly, Azure App Configuration, ...), remove keys from deployment
37
37
  settings and secret stores.
38
- 3. **Proposed commit message**, in a code block:
38
+ 3. The commit message:
39
39
 
40
- ```
41
- chore: remove <flag> feature flag
40
+ <!-- commit-message:start -->
41
+ **Proposed commit message**, in a code block:
42
42
 
43
- <one or two sentences: which path stays and what was deleted>
44
- ```
43
+ ```
44
+ chore: remove <flag> feature flag
45
45
 
46
- Follow the repository's convention instead when `git log --oneline -10` shows a different one.
46
+ <one or two sentences: which path stays and what was deleted>
47
+ ```
48
+
49
+ - Follow the repository's convention instead when `git log --oneline -10` shows a different one.
50
+ - A ticket in the branch name (`feature/feat-24586`): add it the way the log does (`#24586`, `FEAT-24586`), else `(#24586)` after the subject.
51
+ - Wrap the body at 72 columns.
52
+ <!-- commit-message:end -->
47
53
 
48
54
  Never commit, push or open a PR unless asked. Write a PR description only on request, from the same output.