flagrm 0.3.0 → 0.4.1
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 +100 -0
- package/README.md +56 -29
- 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/cli.js +41 -21
- package/dist/core/baseline.js +60 -31
- package/dist/core/change-filter.js +3 -2
- package/dist/core/config-snapshot.js +26 -3
- package/dist/core/config.js +5 -1
- package/dist/core/detect.js +4 -4
- package/dist/core/doctor.js +26 -6
- package/dist/core/environment.js +25 -3
- package/dist/core/git.js +50 -3
- package/dist/core/hook.js +13 -2
- 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 +7 -0
- package/dist/core/run-cache.js +147 -0
- package/dist/core/scan.js +30 -3
- package/dist/core/scope-files.js +14 -0
- package/dist/core/scope.js +3 -3
- package/dist/core/test-runner.js +136 -0
- package/dist/core/types.js +9 -4
- package/dist/core/verify/commands.js +160 -22
- package/dist/core/verify/coverage.js +92 -0
- package/dist/core/verify/index.js +32 -5
- package/dist/core/verify/log-results.js +127 -0
- package/dist/core/verify/removed-tests.js +90 -7
- package/dist/core/verify/test-results.js +58 -10
- package/package.json +4 -4
- package/skills/flagrm-remove/SKILL.md +28 -8
- package/skills/flagrm-remove/batch.md +38 -0
- package/skills/flagrm-verify/SKILL.md +12 -6
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
* Why each baseline test no longer runs. Deleting the OFF path's tests and
|
|
3
3
|
* dropping the flag from test names is part of a removal, and a narrower test
|
|
4
4
|
* command leaves tests out on purpose; only the rest needs explaining.
|
|
5
|
+
* Without result files, the changed test files' declarations stand in for the
|
|
6
|
+
* names (`declaredTestDiff`).
|
|
5
7
|
*/
|
|
6
8
|
import fs from "node:fs";
|
|
7
9
|
import path from "node:path";
|
|
8
10
|
import { flagWords, words } from "../flag-words.js";
|
|
9
11
|
import { fileAt } from "../git.js";
|
|
10
|
-
import { escapeRegExp, isTestPath } from "../scan.js";
|
|
12
|
+
import { computeCodeMask, escapeRegExp, isTestPath } from "../scan.js";
|
|
11
13
|
import { JUNIT_SEPARATOR, testKey } from "./test-results.js";
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Sort the tests that no longer run (`removed`) using the new ones (`added`)
|
|
16
|
+
* and the diff. `files`: the changed test files when the caller already read them.
|
|
17
|
+
*/
|
|
18
|
+
export function classifyRemovedTests(removed, added, afterNames, ctx, files = changedTestFiles(ctx)) {
|
|
14
19
|
const out = { deleted: [], renamed: [], excludedByConfig: [], unexplained: [] };
|
|
15
20
|
const renamedTo = matchRenames(removed, added, flagWords(ctx.names));
|
|
16
21
|
const stillRuns = new Set(afterNames.map(testKey));
|
|
17
|
-
const files = changedTestFiles(ctx);
|
|
18
22
|
for (const name of removed) {
|
|
19
23
|
const to = renamedTo.get(testKey(name));
|
|
20
24
|
if (to)
|
|
@@ -83,7 +87,7 @@ function matchRenames(removed, added, flag) {
|
|
|
83
87
|
}
|
|
84
88
|
return out;
|
|
85
89
|
}
|
|
86
|
-
function changedTestFiles(ctx) {
|
|
90
|
+
function changedTestFiles(ctx, includeNew = false) {
|
|
87
91
|
if (!ctx.sha)
|
|
88
92
|
return [];
|
|
89
93
|
const out = [];
|
|
@@ -91,7 +95,7 @@ function changedTestFiles(ctx) {
|
|
|
91
95
|
if (!isTestPath(rel))
|
|
92
96
|
continue;
|
|
93
97
|
const before = fileAt(ctx.root, ctx.sha, rel);
|
|
94
|
-
if (before === undefined)
|
|
98
|
+
if (before === undefined && !includeNew)
|
|
95
99
|
continue;
|
|
96
100
|
let now;
|
|
97
101
|
try {
|
|
@@ -100,7 +104,7 @@ function changedTestFiles(ctx) {
|
|
|
100
104
|
catch {
|
|
101
105
|
now = undefined;
|
|
102
106
|
}
|
|
103
|
-
out.push({ before, now });
|
|
107
|
+
out.push({ rel, before: before ?? "", now });
|
|
104
108
|
}
|
|
105
109
|
return out;
|
|
106
110
|
}
|
|
@@ -123,6 +127,10 @@ function isDeleted(name, keyStillRuns, files) {
|
|
|
123
127
|
}
|
|
124
128
|
/** xUnit (`InlineData`, `[Bit]AutoData(...)` with values), NUnit and MSTest attributes that each add a data row. */
|
|
125
129
|
const DATA_ROW = /\b(?:\w*InlineData|\w*AutoData|TestCase|DataRow)\s*\(/g;
|
|
130
|
+
/** Whether a test file has data-row attributes or `.each` cases. */
|
|
131
|
+
function hasDataRows(text) {
|
|
132
|
+
return new RegExp(DATA_ROW.source).test(text) || /\.each\s*[(`]/.test(text);
|
|
133
|
+
}
|
|
126
134
|
/**
|
|
127
135
|
* How many data rows `own` has in `text`: the row attributes between the end
|
|
128
136
|
* of the previous member and its declaration. For JUnit names (`it.each`
|
|
@@ -139,3 +147,78 @@ function dataRows(text, own, junit) {
|
|
|
139
147
|
}
|
|
140
148
|
return rows;
|
|
141
149
|
}
|
|
150
|
+
// --- declarations ----------------------------------------------------------------
|
|
151
|
+
const JS_TEST = /(?<![\w$.])(describe|fdescribe|xdescribe|context|suite|it|fit|xit|test)(?:\.(?:only|skip|todo))?(?:\.each\s*(?:\([\s\S]*?\)|`[\s\S]*?`))?\s*\(\s*(['"`])((?:\\.|(?!\2)[^\\])*)\2/g;
|
|
152
|
+
/** `it`/`test` titles prefixed with the titles of the `describe` blocks around them (by brace depth over code). */
|
|
153
|
+
function jsTests(text) {
|
|
154
|
+
const code = computeCodeMask(text);
|
|
155
|
+
const out = [];
|
|
156
|
+
const open = [];
|
|
157
|
+
let depth = 0;
|
|
158
|
+
let pos = 0;
|
|
159
|
+
for (const m of text.matchAll(JS_TEST)) {
|
|
160
|
+
if (!code[m.index])
|
|
161
|
+
continue;
|
|
162
|
+
for (; pos < m.index; pos++)
|
|
163
|
+
if (code[pos])
|
|
164
|
+
depth += text[pos] === "{" ? 1 : text[pos] === "}" ? -1 : 0;
|
|
165
|
+
while (open.length && open[open.length - 1].depth >= depth)
|
|
166
|
+
open.pop();
|
|
167
|
+
if (/describe|context|suite/.test(m[1]))
|
|
168
|
+
open.push({ title: m[3], depth });
|
|
169
|
+
else
|
|
170
|
+
out.push([...open.map((d) => d.title), m[3]].join(JUNIT_SEPARATOR));
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
const CS_TEST = /\[(?:Fact|Theory|Test|TestCase|TestMethod)\b[^\]]*\][\s\S]*?\b(?:void|Task|ValueTask)(?:<[^>]*>)?\s+(\w+)\s*\(/g;
|
|
175
|
+
/** Test methods as TRX names them without data-row arguments: `Namespace.Class.Method`. */
|
|
176
|
+
function csTests(text) {
|
|
177
|
+
const ns = /^\s*namespace\s+([\w.]+)/m.exec(text)?.[1];
|
|
178
|
+
return [...text.matchAll(CS_TEST)].map((m) => {
|
|
179
|
+
const cls = [...text.slice(0, m.index).matchAll(/\bclass\s+(\w+)/g)].pop()?.[1];
|
|
180
|
+
return [ns, cls, m[1]].filter(Boolean).join(".");
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/** The tests a test file declares, named as result files name them where the syntax allows. */
|
|
184
|
+
export function declaredTests(text, file) {
|
|
185
|
+
if (/\.[cm]?[jt]sx?$/.test(file))
|
|
186
|
+
return jsTests(text);
|
|
187
|
+
if (file.endsWith(".cs"))
|
|
188
|
+
return csTests(text);
|
|
189
|
+
if (file.endsWith(".go"))
|
|
190
|
+
return [...text.matchAll(/\bfunc (Test\w+)\s*\(/g)].map((m) => m[1]);
|
|
191
|
+
if (file.endsWith(".py"))
|
|
192
|
+
return [...text.matchAll(/^\s*def (test_\w+)\s*\(/gm)].map((m) => m[1]);
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
/** `from` without the names in `other`, counting duplicates. */
|
|
196
|
+
function minus(from, other) {
|
|
197
|
+
const left = new Map();
|
|
198
|
+
for (const name of other)
|
|
199
|
+
left.set(name, (left.get(name) ?? 0) + 1);
|
|
200
|
+
return from.filter((name) => {
|
|
201
|
+
const n = left.get(name) ?? 0;
|
|
202
|
+
left.set(name, n - 1);
|
|
203
|
+
return n <= 0;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* What the changed test files declared at the baseline and don't now, the
|
|
208
|
+
* new declarations, and all declared now: the stand-in for result files'
|
|
209
|
+
* names when there are none.
|
|
210
|
+
*/
|
|
211
|
+
export function declaredTestDiff(ctx) {
|
|
212
|
+
const before = [];
|
|
213
|
+
const now = [];
|
|
214
|
+
let rows = false;
|
|
215
|
+
// A file new since the baseline declared nothing then, so a moved spec's tests cancel out.
|
|
216
|
+
const files = changedTestFiles(ctx, true);
|
|
217
|
+
for (const file of files) {
|
|
218
|
+
rows ||= hasDataRows(file.before) || (file.now !== undefined && hasDataRows(file.now));
|
|
219
|
+
before.push(...declaredTests(file.before, file.rel));
|
|
220
|
+
if (file.now !== undefined)
|
|
221
|
+
now.push(...declaredTests(file.now, file.rel));
|
|
222
|
+
}
|
|
223
|
+
return { removed: minus(before, now), added: minus(now, before), now, rows, files };
|
|
224
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
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";
|
|
9
|
-
import
|
|
10
|
+
import { globAbsolute } from "../scan.js";
|
|
10
11
|
function attributes(tag) {
|
|
11
12
|
const out = {};
|
|
12
13
|
for (const m of tag.matchAll(/([\w:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g))
|
|
@@ -142,19 +143,64 @@ 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().
|
|
155
202
|
const cutoff = since - 2000;
|
|
156
|
-
const files =
|
|
157
|
-
.sync(path.sep === "\\" ? pattern.replace(/\\/g, "/") : pattern, { absolute: true, dot: true })
|
|
203
|
+
const files = globAbsolute(pattern)
|
|
158
204
|
.map((file) => ({ file, mtime: fs.statSync(file).mtimeMs }))
|
|
159
205
|
.filter(({ file, mtime }) => mtime >= cutoff && claimed.get(file) !== mtime)
|
|
160
206
|
.sort((a, b) => a.file.localeCompare(b.file));
|
|
@@ -164,8 +210,10 @@ export function collectTestResults(pattern, since, claimed = new Map()) {
|
|
|
164
210
|
return undefined;
|
|
165
211
|
const merged = empty();
|
|
166
212
|
for (const { file } of files) {
|
|
167
|
-
|
|
168
|
-
const parsed =
|
|
213
|
+
// A file that is still being written, or isn't a result file, is skipped rather than failing the run.
|
|
214
|
+
const parsed = parseResultFile(fs.readFileSync(file, "utf8"));
|
|
215
|
+
if (!parsed)
|
|
216
|
+
continue;
|
|
169
217
|
merged.total += parsed.total;
|
|
170
218
|
merged.passed += parsed.passed;
|
|
171
219
|
merged.failed += parsed.failed;
|
|
@@ -174,5 +222,5 @@ export function collectTestResults(pattern, since, claimed = new Map()) {
|
|
|
174
222
|
merged.failedNames.push(...parsed.failedNames);
|
|
175
223
|
merged.files.push(file);
|
|
176
224
|
}
|
|
177
|
-
return merged;
|
|
225
|
+
return merged.files.length ? merged : undefined;
|
|
178
226
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagrm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -53,17 +53,17 @@
|
|
|
53
53
|
],
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"commander": "^13.1.0",
|
|
56
|
-
"fast-glob": "^3.3.3",
|
|
57
56
|
"picocolors": "^1.1.1",
|
|
58
57
|
"picomatch": "^4.0.7",
|
|
58
|
+
"tinyglobby": "^0.2.17",
|
|
59
59
|
"typescript": "^5.7.0",
|
|
60
60
|
"yaml": "^2.7.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@biomejs/biome": "2.5.
|
|
63
|
+
"@biomejs/biome": "2.5.15",
|
|
64
64
|
"@types/node": "^22.13.0",
|
|
65
65
|
"@types/picomatch": "^4.0.3",
|
|
66
66
|
"tsx": "^4.19.0",
|
|
67
|
-
"vitest": "^
|
|
67
|
+
"vitest": "^4.1.11"
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -16,6 +16,8 @@ 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.
|
|
@@ -35,7 +37,7 @@ Without `failure`: a build, stop. Tests: stop if the failures (`results.failedTe
|
|
|
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`).
|
|
@@ -70,19 +72,37 @@ Read `patterns.md` in this skill's directory when a case is unclear.
|
|
|
70
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.
|