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.
- package/CHANGELOG.md +129 -0
- package/README.md +70 -31
- package/dist/adapters/angular/component-files.js +41 -0
- package/dist/adapters/angular/flow.js +121 -0
- package/dist/adapters/angular/holders.js +194 -0
- package/dist/adapters/angular/index.js +20 -1
- package/dist/adapters/angular/inputs.js +181 -0
- package/dist/adapters/dotnet/index.js +11 -1
- package/dist/cli.js +62 -21
- package/dist/core/baseline.js +98 -45
- package/dist/core/change-filter.js +68 -0
- package/dist/core/config-snapshot.js +36 -6
- package/dist/core/config.js +23 -7
- package/dist/core/detect.js +2 -2
- package/dist/core/doctor.js +51 -7
- package/dist/core/environment.js +25 -3
- package/dist/core/git.js +80 -16
- package/dist/core/hook.js +9 -6
- package/dist/core/install.js +26 -11
- package/dist/core/inventory.js +3 -1
- package/dist/core/markdown.js +4 -0
- package/dist/core/name-owner.js +179 -0
- package/dist/core/report.js +13 -0
- package/dist/core/run-cache.js +137 -0
- package/dist/core/scope-files.js +14 -0
- package/dist/core/scope.js +11 -2
- package/dist/core/test-runner.js +136 -0
- package/dist/core/types.js +9 -4
- package/dist/core/verify/commands.js +170 -29
- package/dist/core/verify/coverage.js +92 -0
- package/dist/core/verify/dead-code.js +9 -2
- package/dist/core/verify/index.js +42 -8
- package/dist/core/verify/log-results.js +127 -0
- package/dist/core/verify/orphans.js +175 -0
- package/dist/core/verify/removed-tests.js +90 -7
- package/dist/core/verify/test-results.js +56 -7
- package/package.json +3 -1
- package/skills/flagrm-remove/SKILL.md +31 -11
- package/skills/flagrm-remove/batch.md +38 -0
- 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`)
|
|
4
|
-
* and most CI reporters)
|
|
5
|
-
*
|
|
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
|
-
|
|
168
|
-
const parsed =
|
|
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.
|
|
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
|
-
- `
|
|
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
|
-
|
|
75
|
-
|
|
92
|
+
<one or two sentences: which path stays and what was deleted>
|
|
93
|
+
```
|
|
76
94
|
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
38
|
+
3. The commit message:
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
<!-- commit-message:start -->
|
|
41
|
+
**Proposed commit message**, in a code block:
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
```
|
|
44
|
+
chore: remove <flag> feature flag
|
|
45
45
|
|
|
46
|
-
|
|
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.
|