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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,133 @@ All notable changes to this project are documented here. The format follows
|
|
|
5
5
|
[Semantic Versioning](https://semver.org/). Before 1.0, a minor version may
|
|
6
6
|
contain breaking changes.
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-10-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `testResults` reads Jest JSON (`jest --json`, Vitest's `json` reporter); a
|
|
15
|
+
result file that doesn't parse, or an XML file that isn't complete, is skipped.
|
|
16
|
+
- Without `testResults`, `verify` reads the changed test files to list
|
|
17
|
+
deleted and renamed tests, and warns when the log's test count dropped by
|
|
18
|
+
more than the diff deletes.
|
|
19
|
+
- Without `testResults`, `verify` reads failing test names and counts from
|
|
20
|
+
the test command's log (Karma, Jest, Vitest, `dotnet test`; Go and pytest
|
|
21
|
+
names), so a test that already failed at the baseline is a known failure
|
|
22
|
+
there too.
|
|
23
|
+
- `doctor`'s warning about a test command without `testResults` says how
|
|
24
|
+
to get results for the detected runner (Vitest, Jest, Karma, `dotnet test`).
|
|
25
|
+
- An optional per-project `lint` command (lint or type-check), run by
|
|
26
|
+
`baseline`, `verify` and `doctor --run`. `verify`'s new `lint` check fails
|
|
27
|
+
on lint errors the removal caused; a lint that already failed at the
|
|
28
|
+
baseline only warns, unless its log has error lines the baseline's didn't
|
|
29
|
+
(line and column numbers ignored).
|
|
30
|
+
- `verify`'s new `coverage` check warns about changed files that no build,
|
|
31
|
+
test or lint command checks; a project's optional `covers` globs say which
|
|
32
|
+
files its commands check (default: everything under its path). Files
|
|
33
|
+
already dirty at the baseline and unchanged since don't count, nor do
|
|
34
|
+
documentation and deleted files outside every project. With
|
|
35
|
+
`--strict`, such a file now fails verify.
|
|
36
|
+
- `/flagrm-remove` takes several flags (`/flagrm-remove A B C`): it asks
|
|
37
|
+
once whether they are all ON and whether to commit each one, then removes
|
|
38
|
+
them one after another, one commit per flag.
|
|
39
|
+
- `/flagrm-remove` ends with the verify overview and a proposed commit
|
|
40
|
+
message, so `/flagrm-verify` is only needed to check a removal later. The
|
|
41
|
+
commit message takes the ticket from the branch name and wraps its body.
|
|
42
|
+
- A passing full verify saves its build and test results; `baseline` and
|
|
43
|
+
`verify` on the same tree with the same commands reuse them instead of
|
|
44
|
+
running them again, and say so. Removing flags one after another, each
|
|
45
|
+
next baseline takes seconds, and so does `/flagrm-verify` after a
|
|
46
|
+
removal. `--no-reuse` runs them anyway.
|
|
47
|
+
- Angular: a renamed import of a flag registry
|
|
48
|
+
(`import { FeatureFlags as AppFeatureFlags }`) is followed, so
|
|
49
|
+
`AppFeatureFlags.X` is recorded and checked like `FeatureFlags.X`.
|
|
50
|
+
- Angular: a name checked only in a component's `.ts` file is checked in
|
|
51
|
+
its `templateUrl` template too, whether discovery or `--file` scoped it.
|
|
52
|
+
- Angular: `list` and `baseline` follow a flag's value through the fields,
|
|
53
|
+
getters, parameterless methods and locals holding an evaluation of it
|
|
54
|
+
(`isPaginationEnabled = this.ff.isFeatureFlagEnabled(…)`), names derived
|
|
55
|
+
from those, and one hop into the child component input a template binds
|
|
56
|
+
one of them to. Names that mention the flag are recorded; generic ones are
|
|
57
|
+
listed as `suggestedNames`.
|
|
58
|
+
- `baseline --file` repeats: `--name x --file a.ts --file b.ts` records the
|
|
59
|
+
name for every file in one call.
|
|
60
|
+
- `baseline --name` refuses a name whose code reads only other flags (a
|
|
61
|
+
helper named like this flag that evaluates a different one), since
|
|
62
|
+
`verify` would then ask to delete the other flag's code.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- **Breaking:** JSON schema version 8: `--json`, `baseline.json` and `verify.json` gain
|
|
67
|
+
the `lint` and `coverage` checks, `lint` runs and `git.dirtyHashes`.
|
|
68
|
+
Version 6 and 7 baselines are still read.
|
|
69
|
+
- The Angular adapter's default test command now writes test results when
|
|
70
|
+
the workspace's runner can without setup: JUnit for Angular's Vitest
|
|
71
|
+
builder (`ng test --reporters=junit --output-file`) and plain Vitest, Jest
|
|
72
|
+
JSON for Jest, and JUnit for Karma once karma.conf configures
|
|
73
|
+
`karma-junit-reporter`. Results go to `.flagrm/test-results/<project>/`.
|
|
74
|
+
A baseline taken before upgrading reports the test command as changed
|
|
75
|
+
once; `flagrm baseline <flag> --accept config` or a new baseline clears it.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- A failed test run without test results showed only "exited with 1":
|
|
80
|
+
`verify` now lists the log's error lines, as for a failed build.
|
|
81
|
+
- A lint, build or test command that fails because its package script
|
|
82
|
+
(`npm run lint` without a `lint` script) or ESLint config is missing is a
|
|
83
|
+
setup problem: `verify` and `doctor --run` fail on it instead of treating
|
|
84
|
+
it as lint debt.
|
|
85
|
+
- Deleting a committed file that matches `exclude` (say, a generator removing
|
|
86
|
+
`graphify-out/graph.html`) no longer invalidates a passing verify: the
|
|
87
|
+
Stop hook's tree fingerprint now leaves out deletions under `exclude`, as
|
|
88
|
+
it already did for edits and new files.
|
|
89
|
+
- `/flagrm-remove` tells the agent not to run code generators or formatters
|
|
90
|
+
after the passing verify, or to verify again if it must.
|
|
91
|
+
|
|
92
|
+
## [0.3.0] - 2026-10-07
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
|
|
96
|
+
- **Breaking:** `build` and `test` each fall back to the adapter's default on
|
|
97
|
+
their own. Setting only `test` no longer drops the default build, so the
|
|
98
|
+
dead-code check has a build log to read. `build: false` / `test: false`
|
|
99
|
+
turn one off. The default .NET test command writes TRX to
|
|
100
|
+
`.flagrm/test-results/<project>/` and sets `testResults`, so tests are
|
|
101
|
+
compared by name and tests that already failed before the removal only
|
|
102
|
+
warn. `flagrm scope --write` writes that `testResults` out along with the
|
|
103
|
+
test command, and no longer folds long commands over two lines.
|
|
104
|
+
- flagrm's own setup files (`flagrm.config.yaml`, `.gitignore`,
|
|
105
|
+
`.claude/settings.json`, the installed `flagrm-*` skills and prompt files,
|
|
106
|
+
`AGENTS.md`, `flagrm.slnf`) and paths matching `exclude` no longer count as
|
|
107
|
+
part of a removal: verify's changed files, `baseline --force` and the Stop
|
|
108
|
+
hook ignore them, so generated output such as `graphify-out/` no longer
|
|
109
|
+
invalidates a passing verify. An `exclude` entry that names a directory
|
|
110
|
+
covers everything under it, as it already did for scanning.
|
|
111
|
+
- `flagrm doctor` and the `/flagrm-remove` precondition accept a tree whose
|
|
112
|
+
only uncommitted files are flagrm's setup files or paths matching `exclude`,
|
|
113
|
+
so a freshly run `flagrm init` no longer blocks the first removal.
|
|
114
|
+
|
|
115
|
+
### Added
|
|
116
|
+
|
|
117
|
+
- `flagrm doctor` marks the build and test commands that are adapter
|
|
118
|
+
defaults, and warns about a test command without `testResults`. With
|
|
119
|
+
`--run` it fails when the test command writes no results matching
|
|
120
|
+
`testResults`, so a broken setup shows up before a removal instead of
|
|
121
|
+
halfway through, and only warns when tests already fail: the baseline
|
|
122
|
+
records those as known failures.
|
|
123
|
+
- `flagrm baseline <flag> --accept config,failures` acknowledges a change to
|
|
124
|
+
the flagrm config or the baseline's failing tests, also mid-removal, when
|
|
125
|
+
`--force` is no longer allowed. verify stops warning about the accepted
|
|
126
|
+
config (tests it leaves out still count as excluded by config) and lists
|
|
127
|
+
accepted known failures as info, so `--strict` can pass. A changed `tools:`
|
|
128
|
+
no longer warns at all.
|
|
129
|
+
- The `dead-code` check warns about a method, property, field or type the
|
|
130
|
+
removal left referenced only where it is declared. Compilers don't report
|
|
131
|
+
that for public or internal members. The check is text-based: strings,
|
|
132
|
+
templates, Razor views and config files count as references, comments
|
|
133
|
+
don't, and language keywords and the flag's recorded names are left out.
|
|
134
|
+
|
|
8
135
|
## [0.2.1] - 2026-10-04
|
|
9
136
|
|
|
10
137
|
### Fixed
|
|
@@ -144,6 +271,8 @@ removes the flag through the `flagrm-remove` skill, and flagrm checks the result
|
|
|
144
271
|
`doctor` warns about an incremental `dotnet build`, which hides existing
|
|
145
272
|
warnings from the dead-code comparison.
|
|
146
273
|
|
|
274
|
+
[0.4.0]: https://github.com/dino-cosic/flagrm/releases/tag/v0.4.0
|
|
275
|
+
[0.3.0]: https://github.com/dino-cosic/flagrm/releases/tag/v0.3.0
|
|
147
276
|
[0.2.1]: https://github.com/dino-cosic/flagrm/releases/tag/v0.2.1
|
|
148
277
|
[0.2.0]: https://github.com/dino-cosic/flagrm/releases/tag/v0.2.0
|
|
149
278
|
[0.1.1]: https://github.com/dino-cosic/flagrm/releases/tag/v0.1.1
|
package/README.md
CHANGED
|
@@ -8,8 +8,8 @@ Remove feature flags with your AI coding agent, safely.
|
|
|
8
8
|
flagrm gives Claude Code, GitHub Copilot and Codex a flag-removal skill and the
|
|
9
9
|
guardrails to check the result. It records a baseline before the edit, then
|
|
10
10
|
gates the removal with `flagrm verify`, which fails while the flag is still
|
|
11
|
-
referenced, the build breaks, a test regresses, or the removal leaves
|
|
12
|
-
code behind.
|
|
11
|
+
referenced, the build breaks, a test or lint regresses, or the removal leaves
|
|
12
|
+
unused code behind.
|
|
13
13
|
|
|
14
14
|
The agent does the reasoning and the edits: finding the flag, its aliases and
|
|
15
15
|
wrappers, removing the OFF path, and refactoring methods the flag was passed
|
|
@@ -18,7 +18,7 @@ into. flagrm decides when the work is done.
|
|
|
18
18
|
Angular and .NET projects work out of the box. Any other stack works through the
|
|
19
19
|
generic adapter.
|
|
20
20
|
|
|
21
|
-
> **Status:** flagrm is at version 0.
|
|
21
|
+
> **Status:** flagrm is at version 0.4. Until 1.0, a minor release may contain
|
|
22
22
|
> breaking changes.
|
|
23
23
|
|
|
24
24
|
## Requirements
|
|
@@ -48,14 +48,11 @@ agent:
|
|
|
48
48
|
/flagrm-remove NewCheckout
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
You get a short overview (checks, changed files, tests deleted or renamed) and
|
|
58
|
-
a proposed commit message. The agent never commits for you. The overview looks
|
|
51
|
+
The agent ends with a short overview (checks, changed files, tests deleted or
|
|
52
|
+
renamed) and a proposed commit message; it commits only when you ask. Several
|
|
53
|
+
flags at once: `/flagrm-remove NewCheckout DarkMode` asks the ON question and
|
|
54
|
+
whether to commit each flag once, then removes them one after another. To
|
|
55
|
+
check a removal later, run `/flagrm-verify NewCheckout`. The overview looks
|
|
59
56
|
like this (abridged):
|
|
60
57
|
|
|
61
58
|
```md
|
|
@@ -67,6 +64,8 @@ like this (abridged):
|
|
|
67
64
|
| dead-code | ✅ pass | no new unused-code diagnostics |
|
|
68
65
|
| build | ✅ pass | web, api built successfully |
|
|
69
66
|
| tests | ✅ pass | tests passed (web 24 → 22: 2 deleted, 1 renamed; api 18 → 16: 2 deleted) |
|
|
67
|
+
| lint | ✅ pass | web linted cleanly |
|
|
68
|
+
| coverage | ✅ pass | every changed file is built, tested or linted |
|
|
70
69
|
|
|
71
70
|
**Files changed since baseline (26):** `backend/src/Shop.Api/Services/CheckoutService.cs`, ...
|
|
72
71
|
|
|
@@ -97,26 +96,29 @@ of these reasons is a warning, for the agent to explain in its notes.
|
|
|
97
96
|
`flagrm baseline <flag>`, which records the git commit, the build and test
|
|
98
97
|
results, and the flag's names. It finds every usage with its own search,
|
|
99
98
|
records wrappers and aliases with `flagrm baseline <flag> --name`, removes
|
|
100
|
-
the flag, and repeats `flagrm verify <flag>` until it passes.
|
|
99
|
+
the flag, and repeats `flagrm verify <flag>` until it passes. It ends with
|
|
100
|
+
the overview and a commit message. With several flags it asks once whether
|
|
101
|
+
they are all ON and whether to commit each, then removes them in order,
|
|
102
|
+
one commit each.
|
|
101
103
|
3. **The Stop hook** (Claude Code) keeps the agent from finishing while a
|
|
102
104
|
removal is in progress and not verified. In progress means a file git
|
|
103
|
-
tracks changed on top of the baseline commit (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
4. **`/flagrm-verify <flag>`**
|
|
107
|
-
and a commit message.
|
|
105
|
+
tracks changed on top of the baseline commit (untracked files and
|
|
106
|
+
flagrm's own setup files don't count, so a removal paused on a question to
|
|
107
|
+
the user doesn't block), or the last verify used `--skip`.
|
|
108
|
+
4. **`/flagrm-verify <flag>`** checks a removal later: runs
|
|
109
|
+
`flagrm verify <flag> --md` and adds notes and a commit message.
|
|
108
110
|
|
|
109
111
|
## Commands
|
|
110
112
|
|
|
111
113
|
| Command | What it does | Exit codes |
|
|
112
114
|
|---|---|---|
|
|
113
115
|
| `init` | Set up the config and `.gitignore` entry, and each chosen AI tool's files (asks, or `--tool claude-code,copilot,codex`; saved as `tools:`). Never overwrites or deletes | 0, 2 usage error |
|
|
114
|
-
| `doctor` | Check the config, build and
|
|
116
|
+
| `doctor` | Check the config, the build, test and lint commands (marking adapter defaults; `--run` runs them and fails when tests write no readable `testResults`), the .NET SDK `global.json` asks for, `cargo` and Docker where projects need them, git, `.gitignore`, installed skills and hook, and abandoned baselines | 0 ok, 1 problems |
|
|
115
117
|
| `scope` | For each .NET project, show what the machine lacks and which projects a solution filter would leave out; `--write` creates `flagrm.slnf`, adds it to `.git/info/exclude` and points the project's build and test at it | 0 nothing missing, 1 something missing |
|
|
116
118
|
| `update` | Refresh the installed skills, prompt files, `AGENTS.md` block and hook from this flagrm version, for the config's `tools:` | 0 |
|
|
117
|
-
| `list` | List every feature flag in the project: definitions, configured state per environment, reference counts | 0 |
|
|
118
|
-
| `baseline <flag>` | Before any edit, record the git commit, build and
|
|
119
|
-
| `verify <flag>` | Gate the removal with `leftovers`, `dead-code`, `build` and `
|
|
119
|
+
| `list` | List every feature flag in the project: definitions, configured state per environment, reference counts; flow names include renamed imports of a flag registry (`import { FeatureFlags as AppFeatureFlags }`) and, in Angular, the fields, getters, methods and locals holding a flag and a child component's input bound to one | 0 |
|
|
120
|
+
| `baseline <flag>` | Before any edit, record the git commit, build, test and lint results and the flag's names in `.flagrm/<flag>/baseline.json`. In .NET and Angular code the names include the locals, fields, getters, parameters and methods the flag's value travels through when they name the flag (a local, parameter or field is checked only in its own file, and an Angular component's template); generic or ambiguous ones are listed as `suggestedNames`, with `parameterizedTests` that pass the value as a literal. `--file` (repeatable) limits `--name` entries to those files (and records them as wrappers). `--name X --kind alias\|wrapper` adds names to an existing baseline (a name whose declaration reads only other flags is refused); `--json` prints a summary (test counts, at most 20 failed test names; the file keeps everything); `--force` starts over, but only while the code is unedited since the baseline commit; `--accept config,failures` acknowledges, also mid-removal, a config change or the baseline's failing tests so verify stops warning about them; `--no-checks` skips build, tests and lint; build, test and lint results of a passing verify on the same tree and commands are reused (`--no-reuse` runs them) | 0, 2 usage error |
|
|
121
|
+
| `verify <flag>` | Gate the removal with `leftovers`, `dead-code`, `build`, `tests`, `lint` and `coverage`. Writes `.flagrm/<flag>/verify.json`. `--md` prints the overview, `--json` the full result, `--skip` skips checks, `--strict` fails on warnings; reuses its own or another flag's passing runs on the same tree and commands (`--no-reuse` runs them) | 0 pass, 1 fail, 2 usage error |
|
|
120
122
|
| `hook stop` | The Claude Code Stop hook installed by `init` | always 0 |
|
|
121
123
|
|
|
122
124
|
`list`, `baseline` and `verify` accept `--json`, `--config <path>`, and
|
|
@@ -127,12 +129,14 @@ of these reasons is a warning, for the agent to explain in its notes.
|
|
|
127
129
|
| Check | Fails when |
|
|
128
130
|
|---|---|
|
|
129
131
|
| `leftovers` | A recorded name (the flag literal, its constants, wrappers the agent recorded) is still in code or config. A mention in a comment is a warning, and so is a test that checks text the removal deleted from the code (a test of the OFF path that never names the flag), or deleted text whose replacement on the same line no test checks (lost ON coverage) |
|
|
130
|
-
| `dead-code` | The compiler reports new unused code (locals, imports, private members) in changed files |
|
|
132
|
+
| `dead-code` | The compiler reports new unused code (locals, imports, private members) in changed files. A method, property, field or type the removal left named only where it is declared is a warning (compilers don't report public members) |
|
|
131
133
|
| `build` | A project's build command fails |
|
|
132
|
-
| `tests` | A test that passed at the baseline fails.
|
|
134
|
+
| `tests` | A test that passed at the baseline fails. Tests that already failed at the baseline are known failures and only warn; without `testResults` they are read from the test log (Karma, Jest, Vitest, `dotnet test`), and count as known only when the log names every failing test. A test that no longer runs though the diff neither deletes nor renames it and the test config is unchanged, and a changed test file none of whose tests ran, are warnings for the agent to account for. Without `testResults`, deleted and renamed tests come from the changed test files, and a test count that dropped by more than the diff deletes is a warning (info when the changed test files use data rows such as `[InlineData]`, `TestCase`, `DataRow` or `.each`, whose cases count separately, or when the test config changed since the baseline) |
|
|
135
|
+
| `lint` | The project's `lint` command fails where it passed at the baseline. A lint that already failed at the baseline only warns, unless its log has error lines the baseline's didn't (line and column numbers ignored); a missing package script or ESLint config is a setup problem and fails |
|
|
136
|
+
| `coverage` | A changed file that no project's build, test or lint command checks (outside every project with a command, or outside its `covers` globs) is a warning, so it fails only with `--strict`. Files already dirty at the baseline and unchanged since don't count, nor do projects whose commands `--skip` left out |
|
|
133
137
|
|
|
134
138
|
The baseline also records the configuration it was taken with: each project's
|
|
135
|
-
resolved build and
|
|
139
|
+
resolved build, test and lint commands, the files they name (a `.slnf`, a
|
|
136
140
|
`.runsettings`, a filter list) and the text of `flagrm.config.yaml`. When any
|
|
137
141
|
of it changed since, the check it affects warns and shows the difference: a
|
|
138
142
|
narrower test command makes "no new failures" mean less.
|
|
@@ -152,6 +156,8 @@ projects:
|
|
|
152
156
|
build: npm run build
|
|
153
157
|
test: npx ng test --watch=false
|
|
154
158
|
testResults: ./frontend/test-results/junit.xml
|
|
159
|
+
lint: npx eslint e2e && npx tsc --noEmit -p e2e # optional lint / type-check
|
|
160
|
+
covers: ['src/**', 'e2e/**'] # what the commands check (default: everything under path)
|
|
155
161
|
methods: [isEnabled, watch] # flag evaluation methods
|
|
156
162
|
- name: api
|
|
157
163
|
adapter: dotnet
|
|
@@ -168,13 +174,46 @@ exclude: ['**/Migrations/**']
|
|
|
168
174
|
```
|
|
169
175
|
|
|
170
176
|
Paths are relative to the config file, and commands run inside each project's
|
|
171
|
-
path.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
path. `build` and `test` are resolved one at a time:
|
|
178
|
+
|
|
179
|
+
| In the config | Command that runs |
|
|
180
|
+
|---|---|
|
|
181
|
+
| absent | the adapter's default (`dotnet build --no-incremental`, `npx ng build`, and so on) |
|
|
182
|
+
| a command | that command |
|
|
183
|
+
| `false` | none, not even the default |
|
|
184
|
+
|
|
185
|
+
`testResults` (JUnit XML, TRX or Jest JSON; a path or a glob) lets `verify`
|
|
186
|
+
compare individual tests against the baseline. Only files written during
|
|
187
|
+
the run are read, and every matching file is merged, so a sharded run can
|
|
188
|
+
use `./test-results/*.xml`. The default .NET test command writes TRX to
|
|
189
|
+
`.flagrm/test-results/<project>/`. So does the default Angular test command,
|
|
190
|
+
when the workspace's runner can write results without setup: JUnit for
|
|
191
|
+
Angular's Vitest builder and Vitest, Jest JSON for Jest, and JUnit for Karma
|
|
192
|
+
once karma.conf configures `karma-junit-reporter`. A `test` command of your
|
|
193
|
+
own needs its own `testResults`, and `flagrm doctor` says what to add.
|
|
194
|
+
Without result files, `verify` reads failing test names and counts from the
|
|
195
|
+
test log and deleted tests from the diff, which is coarser: passing tests
|
|
196
|
+
aren't named. `timeout` (seconds) fails a project's build or test command
|
|
197
|
+
that runs longer, for example a test runner left in watch mode. `tools` lists
|
|
198
|
+
the AI coding tools whose files `init` and `update` manage (`claude-code`,
|
|
199
|
+
`copilot`, `codex`); dropping one leaves its files in place, and `init` names
|
|
200
|
+
them so you can delete them.
|
|
201
|
+
|
|
202
|
+
`lint` is an optional lint or type-check command (no adapter default) that
|
|
203
|
+
`baseline` and `verify` run like `build` and `test`; use it for code the build
|
|
204
|
+
and tests don't reach, such as end-to-end specs. A lint that already fails at
|
|
205
|
+
the baseline only warns, unless the removal adds error lines. flagrm doesn't
|
|
206
|
+
check formatting: put a formatter's check (`npx prettier --check .`) in
|
|
207
|
+
`lint` if you want it. `covers` lists the files (globs relative to `path`) the
|
|
208
|
+
project's commands check; the `coverage` check warns about a changed file no
|
|
209
|
+
project with a command covers. Without `covers`, a project covers everything
|
|
210
|
+
under its path.
|
|
211
|
+
|
|
212
|
+
Paths matching `exclude` (relative to the config file or a project's path; a
|
|
213
|
+
directory excludes everything under it) and flagrm's own setup files don't
|
|
214
|
+
count as part of a removal, so generated output such as `graphify-out/` that
|
|
215
|
+
changes during a session doesn't invalidate a passing verify or block the Stop
|
|
216
|
+
hook. `--exclude` on the command line doesn't reach the Stop hook.
|
|
178
217
|
|
|
179
218
|
### When the machine can't build everything
|
|
180
219
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** The files that share a component's scope: its `templateUrl` template. Parsed, never type-checked. */
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
export function componentScopeFiles(file) {
|
|
6
|
+
if (!file.endsWith(".ts"))
|
|
7
|
+
return [];
|
|
8
|
+
let text;
|
|
9
|
+
try {
|
|
10
|
+
text = fs.readFileSync(file, "utf8");
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
if (!text.includes("templateUrl"))
|
|
16
|
+
return [];
|
|
17
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const statement of sf.statements) {
|
|
20
|
+
if (!ts.isClassDeclaration(statement))
|
|
21
|
+
continue;
|
|
22
|
+
for (const decorator of ts.getDecorators(statement) ?? []) {
|
|
23
|
+
const call = decorator.expression;
|
|
24
|
+
if (!ts.isCallExpression(call) || call.expression.getText(sf) !== "Component")
|
|
25
|
+
continue;
|
|
26
|
+
const options = call.arguments[0];
|
|
27
|
+
if (!options || !ts.isObjectLiteralExpression(options))
|
|
28
|
+
continue;
|
|
29
|
+
for (const prop of options.properties) {
|
|
30
|
+
if (!ts.isPropertyAssignment(prop) || prop.name.getText(sf) !== "templateUrl")
|
|
31
|
+
continue;
|
|
32
|
+
if (!ts.isStringLiteralLike(prop.initializer))
|
|
33
|
+
continue;
|
|
34
|
+
const template = path.resolve(path.dirname(file), prop.initializer.text);
|
|
35
|
+
if (fs.existsSync(template))
|
|
36
|
+
out.push(template);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a flag's value goes in TypeScript code: renamed imports of a flag
|
|
3
|
+
* registry (`import { FeatureFlags as AppFeatureFlags }`), so `AppFeatureFlags.X`
|
|
4
|
+
* is checked like `FeatureFlags.X`, and the fields, getters, methods and
|
|
5
|
+
* locals holding an evaluation of the flag (`holders.ts`), and a child
|
|
6
|
+
* component's input a template binds one of them to (`inputs.ts`). Each file
|
|
7
|
+
* is parsed at most once, never type-checked.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import ts from "typescript";
|
|
12
|
+
import { isTestPath } from "../../core/scan.js";
|
|
13
|
+
import { collectSites, deriveHolders, evaluationMatcher, skipParens } from "./holders.js";
|
|
14
|
+
import { childInputs } from "./inputs.js";
|
|
15
|
+
/** A renamed named import: `import { A as B }`, `import type { A as B }`, `import X, { A as B }`. */
|
|
16
|
+
const RENAMED_IMPORT = /\bimport\s*(?:type\s+)?(?:[\w$]+\s*,\s*)?\{[^}]*\bas\s/;
|
|
17
|
+
export function angularFlow(ctx, flags, evalMethods) {
|
|
18
|
+
const files = parseFiles(ctx, evalMethods);
|
|
19
|
+
const names = files.flatMap((f) => importAliases(ctx, f, flags));
|
|
20
|
+
if (evalMethods.length === 0)
|
|
21
|
+
return { names, tests: [] };
|
|
22
|
+
const code = files.filter((f) => !f.isTest);
|
|
23
|
+
const evaluates = evaluationMatcher(evalMethods, flagOfArgument(flags, names));
|
|
24
|
+
const sites = collectSites(code);
|
|
25
|
+
let holders = deriveHolders(sites, evaluates);
|
|
26
|
+
// One hop: a child's inputs bound to a holder, then what the child derives from them.
|
|
27
|
+
const inputs = childInputs(code, holders);
|
|
28
|
+
if (inputs.length > 0)
|
|
29
|
+
holders = deriveHolders(sites, evaluates, inputs);
|
|
30
|
+
for (const h of holders) {
|
|
31
|
+
if (evalMethods.includes(h.name))
|
|
32
|
+
continue;
|
|
33
|
+
const same = (n) => n.flag === h.flag && n.name === h.name && n.kind === h.kind && n.file === h.file;
|
|
34
|
+
if (names.some(same))
|
|
35
|
+
continue;
|
|
36
|
+
names.push({
|
|
37
|
+
flag: h.flag,
|
|
38
|
+
name: h.name,
|
|
39
|
+
kind: h.kind,
|
|
40
|
+
...(h.ambiguous ? { ambiguous: true } : {}),
|
|
41
|
+
project: ctx.name,
|
|
42
|
+
file: h.file,
|
|
43
|
+
line: h.line,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return { names, tests: [] };
|
|
47
|
+
}
|
|
48
|
+
/** The `.ts` files any pass needs, each read and parsed once. */
|
|
49
|
+
function parseFiles(ctx, evalMethods) {
|
|
50
|
+
const files = [];
|
|
51
|
+
for (const file of ctx.files) {
|
|
52
|
+
if (!file.endsWith(".ts"))
|
|
53
|
+
continue;
|
|
54
|
+
let text;
|
|
55
|
+
try {
|
|
56
|
+
text = fs.readFileSync(file, "utf8");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const wanted = RENAMED_IMPORT.test(text) ||
|
|
62
|
+
(evalMethods.length > 0 && (text.includes("@Component") || evalMethods.some((m) => text.includes(m))));
|
|
63
|
+
if (!wanted)
|
|
64
|
+
continue;
|
|
65
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
66
|
+
files.push({ file, text, sf, isTest: isTestPath(path.relative(ctx.root, file)) });
|
|
67
|
+
}
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
/** The flag names a renamed import in `f` gives: `AppFeatureFlags.X` for `FeatureFlags.X`. */
|
|
71
|
+
function importAliases(ctx, f, flags) {
|
|
72
|
+
const names = [];
|
|
73
|
+
for (const statement of f.sf.statements) {
|
|
74
|
+
const bindings = ts.isImportDeclaration(statement) ? statement.importClause?.namedBindings : undefined;
|
|
75
|
+
if (!bindings || !ts.isNamedImports(bindings))
|
|
76
|
+
continue;
|
|
77
|
+
for (const element of bindings.elements) {
|
|
78
|
+
if (!element.propertyName)
|
|
79
|
+
continue;
|
|
80
|
+
const original = element.propertyName.text;
|
|
81
|
+
const renamed = element.name.text;
|
|
82
|
+
const line = f.sf.getLineAndCharacterOfPosition(element.getStart(f.sf)).line + 1;
|
|
83
|
+
for (const { flag, aliases } of flags) {
|
|
84
|
+
for (const alias of aliases) {
|
|
85
|
+
if (!alias.startsWith(`${original}.`))
|
|
86
|
+
continue;
|
|
87
|
+
const name = `${renamed}${alias.slice(original.length)}`;
|
|
88
|
+
names.push({ flag, name, kind: "alias", project: ctx.name, file: f.file, line });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return names;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The flag an eval call's first argument names: its string literal, one of
|
|
97
|
+
* its qualified names (`FeatureFlags.X`), or an import alias of one declared
|
|
98
|
+
* in the same file (`AppFeatureFlags.X`).
|
|
99
|
+
*/
|
|
100
|
+
function flagOfArgument(flags, aliases) {
|
|
101
|
+
const literals = new Set(flags.map((f) => f.flag));
|
|
102
|
+
const qualified = new Map();
|
|
103
|
+
for (const { flag, aliases: names } of flags) {
|
|
104
|
+
for (const name of names)
|
|
105
|
+
if (!qualified.has(name))
|
|
106
|
+
qualified.set(name, flag);
|
|
107
|
+
}
|
|
108
|
+
const imported = new Map();
|
|
109
|
+
for (const a of aliases) {
|
|
110
|
+
const inFile = imported.get(a.file) ?? new Map();
|
|
111
|
+
inFile.set(a.name, a.flag);
|
|
112
|
+
imported.set(a.file, inFile);
|
|
113
|
+
}
|
|
114
|
+
return (file, arg) => {
|
|
115
|
+
const value = skipParens(arg);
|
|
116
|
+
if (ts.isStringLiteralLike(value))
|
|
117
|
+
return literals.has(value.text) ? value.text : undefined;
|
|
118
|
+
const text = value.getText(file.sf).replace(/\s+/g, "");
|
|
119
|
+
return qualified.get(text) ?? imported.get(file.file)?.get(text);
|
|
120
|
+
};
|
|
121
|
+
}
|