@agentvibes/guardrails 0.1.0 → 0.2.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 (68) hide show
  1. package/README.md +131 -5
  2. package/dist/astGrep.d.ts +3 -0
  3. package/dist/astGrep.js +15 -2
  4. package/dist/astGrep.js.map +1 -1
  5. package/dist/doctor.js +38 -2
  6. package/dist/doctor.js.map +1 -1
  7. package/dist/hookPostedit.js +18 -1
  8. package/dist/hookPostedit.js.map +1 -1
  9. package/dist/presetDrift.d.ts +31 -0
  10. package/dist/presetDrift.js +226 -0
  11. package/dist/presetDrift.js.map +1 -0
  12. package/dist/presetDriftTest.d.ts +1 -0
  13. package/dist/presetDriftTest.js +136 -0
  14. package/dist/presetDriftTest.js.map +1 -0
  15. package/dist/repoRules.d.ts +27 -0
  16. package/dist/repoRules.js +86 -0
  17. package/dist/repoRules.js.map +1 -0
  18. package/dist/repoRulesTest.d.ts +1 -0
  19. package/dist/repoRulesTest.js +122 -0
  20. package/dist/repoRulesTest.js.map +1 -0
  21. package/dist/screenScope.d.ts +16 -5
  22. package/dist/screenScope.js +66 -21
  23. package/dist/screenScope.js.map +1 -1
  24. package/dist/screenScopeTest.js +67 -7
  25. package/dist/screenScopeTest.js.map +1 -1
  26. package/dist/siblingCloneTest.d.ts +1 -0
  27. package/dist/siblingCloneTest.js +201 -0
  28. package/dist/siblingCloneTest.js.map +1 -0
  29. package/dist/testRules.js +77 -0
  30. package/dist/testRules.js.map +1 -1
  31. package/dist/twinCoverageTest.d.ts +1 -0
  32. package/dist/twinCoverageTest.js +262 -0
  33. package/dist/twinCoverageTest.js.map +1 -0
  34. package/dist/verify.d.ts +1 -1
  35. package/dist/verify.js +33 -8
  36. package/dist/verify.js.map +1 -1
  37. package/dist/verifyDiff.js +6 -5
  38. package/dist/verifyDiff.js.map +1 -1
  39. package/package.json +7 -3
  40. package/rules/__fixtures__/demo-mode-by-default/bad.ts +22 -0
  41. package/rules/__fixtures__/demo-mode-by-default/bad.tsx +22 -0
  42. package/rules/__fixtures__/demo-mode-by-default/good.ts +21 -0
  43. package/rules/__fixtures__/demo-mode-by-default/good.tsx +21 -0
  44. package/rules/__fixtures__/json-roundtrip/bad.ts +6 -0
  45. package/rules/__fixtures__/json-roundtrip/bad.tsx +8 -0
  46. package/rules/__fixtures__/json-roundtrip/good.ts +5 -0
  47. package/rules/__fixtures__/json-roundtrip/good.tsx +7 -0
  48. package/rules/__fixtures__/kind-if-without-match/bad.ts +17 -0
  49. package/rules/__fixtures__/kind-if-without-match/bad.tsx +18 -0
  50. package/rules/__fixtures__/kind-if-without-match/good.ts +14 -0
  51. package/rules/__fixtures__/kind-if-without-match/good.tsx +13 -0
  52. package/rules/__fixtures__/literal-union-in-component/components/bad.tsx +16 -0
  53. package/rules/__fixtures__/literal-union-in-component/components/good.tsx +25 -0
  54. package/rules/__fixtures__/non-exhaustive-match/bad.tsx +12 -0
  55. package/rules/__fixtures__/non-exhaustive-match/good.tsx +14 -0
  56. package/rules/__fixtures__/silent-default-return/bad.ts +9 -0
  57. package/rules/__fixtures__/silent-default-return/bad.tsx +10 -0
  58. package/rules/__fixtures__/silent-default-return/good.ts +17 -0
  59. package/rules/__fixtures__/silent-default-return/good.tsx +11 -0
  60. package/rules/demo-mode-by-default-ts.yml +17 -1
  61. package/rules/demo-mode-by-default.yml +17 -1
  62. package/rules/json-roundtrip-tsx.yml +26 -0
  63. package/rules/kind-if-without-match-tsx.yml +31 -0
  64. package/rules/literal-union-in-component-tsx.yml +81 -0
  65. package/rules/no-local-kit-clone-tsx.yml +16 -0
  66. package/rules/no-local-kit-clone.yml +16 -0
  67. package/rules/non-exhaustive-match-tsx.yml +48 -0
  68. package/rules/silent-default-return-tsx.yml +33 -0
package/README.md CHANGED
@@ -84,7 +84,14 @@ Workspaces that reach outside their repo (`link:../<repo>/...` overrides,
84
84
  `sibling_repos: "owner/repo owner2/repo2"`: the gate then checks the main
85
85
  repo out at `repos/<org>/<repo>` depth and shallow-clones each sibling at its
86
86
  own `repos/<owner>/<name>`, reproducing a `gits/<org>/<repo>` disk layout so
87
- the relative escapes resolve. Private siblings additionally need
87
+ the relative escapes resolve. **Each sibling path is removed and re-cloned on
88
+ every run.** A self-hosted runner keeps its `_work` directory between jobs and
89
+ `actions/checkout` cleans only its own path, so the leftovers used to make the
90
+ step exit 128 on every run after the first — before install, so the ratchet
91
+ showed as `skipped` rather than as not-run (is-8774fe0e). Re-cloning also keeps
92
+ the sibling current: gating against last week's sibling is a wrong answer, not
93
+ a cheap one. Listing the repo under test in `sibling_repos` is refused — that
94
+ path holds the run's own checkout. Private siblings additionally need
88
95
  `secrets: { sibling_token: <PAT with read on them> }` — the default job token
89
96
  cannot reach other repos. `@v0` is a
90
97
  moving tag that follows validated releases, actions-style; pin `@<commit-sha>`
@@ -187,6 +194,40 @@ that this package contains none.
187
194
  { "extends": "@agentvibes/guardrails/tsconfig" }
188
195
  ```
189
196
 
197
+ ### One biome config per repo, and it is the preset
198
+
199
+ The biome preset is a preset, not a starting point. `guardrails verify` and
200
+ `guardrails doctor` check the repo for drift and name the offending keys in one
201
+ line. Three things count as drift:
202
+
203
+ | | what | why |
204
+ |---|---|---|
205
+ | a | `biome.json` does not list `@agentvibes/guardrails/biome` in `extends` | it is not using the preset at all |
206
+ | b | it extends the preset and then carries its own `formatter`, `javascript.formatter`, `json.formatter`, `linter` or `assist` | those are the preset's decisions, restated locally so they can drift |
207
+ | c | a second `biome.json` / `biome.jsonc` exists in the tree (outside `node_modules`, `dist`, `.claude` and the other skipped dirs) | two configs means two answers |
208
+
209
+ Two things stay legal, because they scope rather than restyle: `files` (a repo
210
+ decides which of ITS paths are linted) and an `overrides` entry that only turns
211
+ `suspicious.noConsole` off for some paths (a CLI has to print).
212
+
213
+ **Enforcement is opt-in per repo, for now.** Add
214
+
215
+ ```toml
216
+ # .agentvibes/project.toml
217
+ [biome]
218
+ preset = "enforced"
219
+ ```
220
+
221
+ and `verify` exits 2 on drift, `doctor` exits 1. Without it both still SAY the
222
+ drift — `verify` prints it as a note, `doctor` as a `biome preset` line — and
223
+ neither changes its exit code. The reason is sequencing, not softness: epic
224
+ is-a70a5963 is migrating 37 configs, and 5 of the 7 repos running this gate do
225
+ not conform yet. Each migration adds the line as its last step; when the last
226
+ one lands, the default flips to enforced.
227
+
228
+ The reusable gate workflow runs `verify-diff`, not `verify`, so a repo that opts
229
+ in should add a `guardrails verify` (or `doctor`) step to its own CI.
230
+
190
231
  ## Rules and fixtures
191
232
 
192
233
  - `rules/` — the canon. Stable ids, each message is a mini-manual (why + fix).
@@ -202,6 +243,42 @@ that this package contains none.
202
243
  `rules/`; the eight that remain are repo-local by decision, with the reason
203
244
  for each recorded in `candidates/README.md`. Still loaded nowhere.
204
245
 
246
+ ### Scopes — the repo states a fact, the canon says which rules stand down
247
+
248
+ Some rules cannot be scoped by a glob inside the rule file, because the layout
249
+ they need to know about is the repo's, not the canon's. Those facts go in
250
+ `.agentvibes/project.toml` under `[verify]`:
251
+
252
+ ```toml
253
+ [verify]
254
+ screens = "^src/screens/" # these paths ARE screens
255
+ kit_source = "^src/" # this tree IS @agentvibes/kit
256
+ ```
257
+
258
+ Each key states a fact about the repo — not a suppression. Which rules stop
259
+ applying is the canon's business, declared in the rule itself:
260
+
261
+ ```yaml
262
+ metadata:
263
+ appliesTo: not-screens # direct-store-import
264
+ appliesTo: not-kit-source # no-local-kit-clone, + its tsx twin
265
+ ```
266
+
267
+ `screens` exists because a screen owns its page store and must import the class
268
+ `direct-store-import` bans in a component; `kit_source` because
269
+ `no-local-kit-clone` is error-tier and fires on the six canonical declarations
270
+ inside `@agentvibes/kit` itself — the definitions it tells everyone else to
271
+ import.
272
+
273
+ Absent key = absent behaviour: a repo that says nothing is scanned exactly as it
274
+ was. A pattern that is not a valid regular expression exits 2 with the reason
275
+ rather than being dropped — a scope the gate cannot honour must fail loudly. The
276
+ scope registry is closed (`SCOPES` in `src/screenScope.ts`); a free-form
277
+ per-rule path map in user config would be the growing allowlist this refuses.
278
+ `pnpm test:screens` asserts both directions of each scope AND that every
279
+ registered scope has at least one rule carrying its marker — a scope nothing
280
+ declares filters nothing while looking like it works.
281
+
205
282
  Per-repo severity RAISE: a repo that held a rule stricter than the canon keeps
206
283
  its gate via `[severity]` in `.agentvibes/project.toml`
207
284
  (`zod-optional-nullable = "error"`) — applied through ast-grep's native
@@ -211,6 +288,37 @@ are refused (exit 2); weakening has its own sanctioned homes (warn-tier biome
211
288
  deviation, `[verify] exclude` for vendored trees, justified per-line
212
289
  `ast-grep-ignore`).
213
290
 
291
+ ## Rule twins — one rule, two ids
292
+
293
+ ast-grep's `typescript` and `tsx` languages are **disjoint, not nested**: a rule
294
+ declared `language: typescript` reads `.ts` and never `.tsx`, and vice versa.
295
+ There is no way to say "both" — `language: [typescript, tsx]` fails to parse,
296
+ `languageGlobs` reassigns `.ts` to the tsx language and blinds every remaining
297
+ `typescript` rule (measured corpus-wide: 5 rules dark, 6,094 findings lost
298
+ against 334 gained), and two files sharing an id are refused outright.
299
+
300
+ So a rule that must run on both is **two files with two ids**: `catch-empty.yml`
301
+ and `catch-empty-tsx.yml`. The trailing `-tsx` is what you suppress with in a
302
+ `.tsx` file, and the finding prints the id, so copying from the output is always
303
+ right. A handful of pairs are spelled the other way round — the tsx arm came
304
+ first and the `.ts` half is the `-ts` suffix (`demo-mode-by-default` +
305
+ `demo-mode-by-default-ts`). Both spellings are one family.
306
+
307
+ The two arms are one rule wearing two ids, so their `severity`, `files`,
308
+ `ignores`, `utils` and `rule` blocks must stay byte-identical; only `id`,
309
+ `language` and the message differ (the twin names its own suppression id).
310
+
311
+ **This is enforced, not documented-and-hoped:** `pnpm test:twins`
312
+ (`src/twinCoverageTest.ts`, part of `pnpm check`) fails when a rule family has
313
+ only one arm and no entry in its `EXCEPTIONS` table, and when two arms that do
314
+ exist have drifted apart. Each exception says *why* — either the missing arm is
315
+ impossible (the rule matches JSX nodes, or its `files:` globs name only one
316
+ extension) or it is a real gap, and then the entry carries the measurement and
317
+ the issue that owns it. The failure this prevents is specific and has happened:
318
+ a rule pointed at a language it cannot match reports zero, which is exactly what
319
+ a correct rule with nothing to find reports. `test:rules` catches that for every
320
+ rule it asserts; `test:twins` catches the arm that was never written.
321
+
214
322
  ## Repo-local extra rules
215
323
 
216
324
  A rule that encodes ONE repo's convention does not belong in the canon — but it
@@ -224,9 +332,26 @@ ruleDirs:
224
332
  - .ast-grep/rules # repo-local extras
225
333
  ```
226
334
 
227
- Both directories load together and findings print their own rule id, so a local
228
- rule suppresses under its own name. Verified: a canon rule and a repo-local rule
229
- firing side by side in one `ast-grep scan -c sgconfig.yml` run.
335
+ `guardrails verify` and `verify-diff` scan with **both** configs the bundled
336
+ canon and, when the working directory has one, the repo's own `sgconfig.yml`
337
+ and merge the findings, deduped by rule + file + line (the repo config almost
338
+ always lists the canon `ruleDir` too, so every canon finding otherwise arrives
339
+ twice). Two directions matter and both are pinned by `pnpm test:repo-rules`:
340
+
341
+ - Repo-local rules run under `verify`, gate on error tier, and print their own
342
+ rule id, so a local rule suppresses under its own name. In 0.1.0 they did
343
+ not run at all — `verify` read only the package's config, and the intranet and
344
+ DataSpool each added a bare `ast-grep scan -c sgconfig.yml` as a second gate
345
+ stage to work around it.
346
+ - A repo whose `sgconfig.yml` forgets the canon `ruleDir` does **not** lose the
347
+ canon. The canon is always scanned; the repo's config is additive.
348
+
349
+ A repo `sgconfig.yml` that ast-grep cannot load (unreadable `ruleDir`, unparsable
350
+ YAML) exits 2 with ast-grep's own reason. It never reads as "no findings" — a
351
+ rule set that failed to load reports exactly what a rule set with nothing to find
352
+ reports, and that silence is the failure this package exists to remove.
353
+ `guardrails doctor` lists the `ruleDirs` the repo config declares, so a repo that
354
+ believes it has local rules can see whether the config actually names them.
230
355
 
231
356
  Keep local ids distinct from canon ids — ast-grep refuses to load two rules with
232
357
  the same id, and a repo-local override of a canon rule is a silent fork rather
@@ -235,5 +360,6 @@ with fixtures instead of copying it into a second repo.
235
360
 
236
361
  ## Development
237
362
 
238
- `pnpm check` = build (tsc) + biome + fixture harness + gate-can-go-red test.
363
+ `pnpm check` = build (tsc) + biome + fixture harness + twin coverage +
364
+ gate-can-go-red test + metrics/init/severity/screens/hooks/iterate/leaks.
239
365
  CI runs exactly that, blocking.
package/dist/astGrep.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { Finding, Severity } from "./findings.js";
2
+ /** ast-grep could not load a config, so nothing was scanned with it. */
3
+ export declare class AstGrepConfigError extends Error {
4
+ }
2
5
  export interface AstGrepRow {
3
6
  ruleId: string;
4
7
  file: string;
package/dist/astGrep.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { resolveTool } from "./toolResolve.js";
3
+ /** ast-grep could not load a config, so nothing was scanned with it. */
4
+ export class AstGrepConfigError extends Error {
5
+ }
3
6
  /** Run `ast-grep scan -c <config> --json` and return structured matches. */
4
7
  export function scan(configPath, targets, extraArgs = []) {
5
8
  const tool = resolveTool("ast-grep");
@@ -13,8 +16,18 @@ export function scan(configPath, targets, extraArgs = []) {
13
16
  if (res.error)
14
17
  throw res.error;
15
18
  const out = res.stdout.trim();
16
- if (out === "")
17
- return [];
19
+ // Exit 0 = clean scan, 1 = the scan ran and found error-tier matches (stdout
20
+ // carries them either way). Everything else means ast-grep never scanned:
21
+ // 6 is an unreadable ruleDir, 8 an unparsable config. Returning [] there
22
+ // would report "no findings" for a rule set that never loaded — the exact
23
+ // silence this package exists to remove. Measured against ast-grep 0.45.2.
24
+ const status = res.status ?? -1;
25
+ if (out === "") {
26
+ if (status === 0)
27
+ return [];
28
+ throw new AstGrepConfigError(`ast-grep exited ${status} and scanned nothing with config ${configPath}. ` +
29
+ `Its output:\n${res.stderr.trim().slice(0, 2000)}`);
30
+ }
18
31
  let parsed;
19
32
  try {
20
33
  parsed = JSON.parse(out);
@@ -1 +1 @@
1
- {"version":3,"file":"astGrep.js","sourceRoot":"","sources":["../src/astGrep.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAuB/C,4EAA4E;AAC5E,MAAM,UAAU,IAAI,CAClB,UAAkB,EAClB,OAAiB,EACjB,YAAsB,EAAE;IAExB,MAAM,IAAI,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACrC,MAAM,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;IACnC,IAAI,GAAG,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAC9D,MAAM,GAAG,GAAG,SAAS,CACnB,GAAG,EACH,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,SAAS,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,EACzE;QACE,QAAQ,EAAE,MAAM;QAChB,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;KAC7B,CACF,CAAC;IACF,IAAI,GAAG,CAAC,KAAK;QAAE,MAAM,GAAG,CAAC,KAAK,CAAC;IAC/B,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAC1B,IAAI,MAAsB,CAAC;IAC3B,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAmB,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,2CAA2C,GAAG,CAAC,MAAM,eAAe,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,CAChG,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACxB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC;QACjC,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC;QACpD,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,QAAQ,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI;KAClE,CAAC,CAAC,CAAC;AACN,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,YAAY,CAC1B,UAAkB,EAClB,OAAiB,EACjB,YAAsB,EAAE;IAExB,OAAO,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,SAAS,CAAC;SACxC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC;SACjE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACX,IAAI,EAAE,CAAC,CAAC,MAAM;QACd,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,IAAI,EAAE,CAAC,CAAC,SAAS;QACjB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,MAAM,EAAE,UAAmB;KAC5B,CAAC,CAAC,CAAC;AACR,CAAC"}
1
+ {"version":3,"file":"astGrep.js","sourceRoot":"","sources":["../src/astGrep.ts"],"names":[],"mappings":"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"}
package/dist/doctor.js CHANGED
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { packageRoot, rulesDir, structureRulesDir } from "./packagePaths.js";
5
+ import { driftMessage, presetDrift, presetEnforced } from "./presetDrift.js";
6
+ import { declaredRuleDirs } from "./repoRules.js";
5
7
  import { pinnedVersion, tryResolveTool } from "./toolResolve.js";
6
8
  function reportTool(name) {
7
9
  const pin = pinnedVersion(name);
@@ -42,7 +44,18 @@ export function rulesetSha() {
42
44
  function discoverConfigs(cwd) {
43
45
  const found = {};
44
46
  const sg = join(cwd, "sgconfig.yml");
45
- found["sgconfig.yml"] = existsSync(sg) ? "present" : "absent";
47
+ if (existsSync(sg)) {
48
+ // Name the ruleDirs, not just the file. `verify` scans with this config IN
49
+ // ADDITION to the canon (is-f58cd6b4), so what it lists is what the repo
50
+ // adds on top — and a repo that thinks it has local rules can see here
51
+ // whether the config actually declares the directory holding them.
52
+ const dirs = declaredRuleDirs(sg);
53
+ found["sgconfig.yml"] =
54
+ dirs.length > 0 ? `present, ruleDirs: ${dirs.join(", ")}` : "present, no ruleDirs declared";
55
+ }
56
+ else {
57
+ found["sgconfig.yml"] = "absent";
58
+ }
46
59
  const biomePath = join(cwd, "biome.json");
47
60
  if (existsSync(biomePath)) {
48
61
  const extendsPreset = readFileSync(biomePath, "utf8").includes("@agentvibes/guardrails/biome");
@@ -76,6 +89,15 @@ export function runDoctor(json) {
76
89
  },
77
90
  rulesetSha: rulesetSha(),
78
91
  configDiscovery: discoverConfigs(process.cwd()),
92
+ // Preset drift (is-9c7a78d7). `doctor` REPORTS it wherever it exists —
93
+ // seeing it is the point of this command — and exits non-zero only where
94
+ // the repo has opted in with `[biome] preset = "enforced"`, the same switch
95
+ // `verify` reads. Reporting everywhere and gating on the flag is what lets
96
+ // the epic migrate 37 configs without reddening 5 gates on day one.
97
+ presetDrift: {
98
+ enforced: presetEnforced(process.cwd()),
99
+ findings: presetDrift(process.cwd()),
100
+ },
79
101
  };
80
102
  if (json) {
81
103
  console.log(JSON.stringify(result, null, 2));
@@ -95,8 +117,22 @@ export function runDoctor(json) {
95
117
  for (const [k, v] of Object.entries(result.configDiscovery)) {
96
118
  console.log(` ${k.padEnd(28)} ${v}`);
97
119
  }
120
+ if (result.presetDrift.findings.length === 0) {
121
+ console.log("biome preset no drift");
122
+ }
123
+ else {
124
+ console.log(`biome preset ${result.presetDrift.enforced ? "DRIFT (enforced here)" : "drift (not enforced here yet)"}`);
125
+ for (const f of result.presetDrift.findings)
126
+ console.log(` ${f.detail}`);
127
+ if (!result.presetDrift.enforced) {
128
+ console.log(' Add [biome] preset = "enforced" to .agentvibes/project.toml once this repo is migrated (epic is-a70a5963).');
129
+ }
130
+ }
98
131
  }
99
132
  const allFound = result.tools["ast-grep"].found && result.tools.biome.found;
100
- return allFound ? 0 : 1;
133
+ const drifted = result.presetDrift.enforced && result.presetDrift.findings.length > 0;
134
+ if (drifted && !json)
135
+ console.error(`guardrails doctor: ${driftMessage(result.presetDrift.findings)}`);
136
+ return allFound && !drifted ? 0 : 1;
101
137
  }
102
138
  //# sourceMappingURL=doctor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, relative } from "node:path";
4
+ import { AstGrepConfigError } from "./astGrep.js";
4
5
  import { formatFinding } from "./findings.js";
5
6
  import { addedLines, gitRoot, resolveMergeBase } from "./gitDiff.js";
6
7
  import { severityRaiseArgs } from "./severity.js";
@@ -81,7 +82,23 @@ export function runHookPostedit() {
81
82
  catch {
82
83
  severityArgs = [];
83
84
  }
84
- let findings = collectVerifyFindings([file], severityArgs);
85
+ // The repo root, not the hook's cwd, is what decides which `sgconfig.yml`
86
+ // adds repo-local rules — an agent editing a monorepo package is rarely
87
+ // sitting at the root the config belongs to.
88
+ let findings;
89
+ try {
90
+ findings = collectVerifyFindings([file], severityArgs, repoRoot ?? dirname(file));
91
+ }
92
+ catch (err) {
93
+ if (err instanceof AstGrepConfigError) {
94
+ // Same self-defect rule as the [severity] table above: a repo config
95
+ // ast-grep cannot load must not fail the edit. It is reported once here
96
+ // and fails loudly in verify/verify-diff, which is where it gates.
97
+ console.error(`guardrails hook-postedit: ${err.message}`);
98
+ return 0;
99
+ }
100
+ throw err;
101
+ }
85
102
  const lines = changedLines(tool, file);
86
103
  if (lines !== undefined) {
87
104
  // An EMPTY set is a real answer (a revert git already collapsed) — there
@@ -1 +1 @@
1
- {"version":3,"file":"hookPostedit.js","sourceRoot":"","sources":["../src/hookPostedit.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hookPostedit.js","sourceRoot":"","sources":["../src/hookPostedit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /** The package specifier a consuming repo must extend. */
2
+ export declare const PRESET_SPECIFIER = "@agentvibes/guardrails/biome";
3
+ export interface DriftFinding {
4
+ /** What is wrong, keyed for the one-line message. */
5
+ key: string;
6
+ detail: string;
7
+ }
8
+ /** Every biome config in the tree, repo-root-relative, excluding SKIP_DIRS. */
9
+ export declare function findBiomeConfigs(root: string): string[];
10
+ /**
11
+ * Everything wrong with `root`'s biome configuration; [] when it conforms.
12
+ * `root` is the repo being checked, not this package.
13
+ */
14
+ export declare function presetDrift(root: string): DriftFinding[];
15
+ /**
16
+ * Whether drift GATES in this repo, from `[biome] preset = "enforced"` in
17
+ * `.agentvibes/project.toml`.
18
+ *
19
+ * Opt-in per repo, then flipped (owner, 2026-09-06). Failing every repo the day
20
+ * this shipped would have turned 5 of the 7 repos that run this gate red —
21
+ * SiteCraftMonorepo, faceless-photo-lib, agent-session-observatory, tg-gallery,
22
+ * merkle-substrate — before the migration issue for each was worked, blocking
23
+ * unrelated work in all five. Each child of epic is-a70a5963 adds the line as
24
+ * its last step; when the last child lands, this default flips to enforced.
25
+ *
26
+ * Unenforced is NOT silent: `verify` prints the drift as a note and `doctor`
27
+ * always reports it. What the flag decides is the exit code, not the message.
28
+ */
29
+ export declare function presetEnforced(cwd: string): boolean;
30
+ /** The one-line message the gate prints; "" when there is no drift. */
31
+ export declare function driftMessage(findings: DriftFinding[]): string;
@@ -0,0 +1,226 @@
1
+ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { packageRoot } from "./packagePaths.js";
4
+ import { readTomlTable } from "./tomlTable.js";
5
+ // Preset-drift guard (is-9c7a78d7). The migration epic is-a70a5963 collapses 37
6
+ // hand-rolled biome configs into one preset; nothing stopped a repo from
7
+ // re-growing its own the week after. Owner's rule, 2026-09-03: «we should only
8
+ // use agentvibes … no need to have many biome configs». So the tool is the
9
+ // enforcement, not the convention.
10
+ //
11
+ // Three ways a repo drifts:
12
+ // a. its `biome.json` does not extend `@agentvibes/guardrails/biome`,
13
+ // b. it extends the preset and then restates the preset's own decisions —
14
+ // a top-level `formatter`, `javascript.formatter`, `json.formatter`,
15
+ // `linter` or `assist` key,
16
+ // c. a second `biome.json` / `biome.jsonc` sits somewhere in the tree.
17
+ //
18
+ // What stays legal, because it is scoping rather than style: `files` (a repo
19
+ // decides which of ITS paths are linted) and an `overrides` entry that only
20
+ // turns `noConsole` off for some paths (a CLI has to print).
21
+ //
22
+ // A repo with no biome config at all is not drifting and reports nothing —
23
+ // there is no config to have re-grown. `guardrails doctor` still shows it as
24
+ // absent, and `guardrails init` writes one.
25
+ /** The package specifier a consuming repo must extend. */
26
+ export const PRESET_SPECIFIER = "@agentvibes/guardrails/biome";
27
+ /** Top-level keys the preset owns; a repo restating them has forked it. */
28
+ const PRESET_OWNED = ["formatter", "linter", "assist"];
29
+ /** Nested `<section>.formatter` keys the preset owns. */
30
+ const PRESET_OWNED_NESTED = ["javascript", "json"];
31
+ /** Keys an override may carry besides its rule content — path scoping only. */
32
+ const OVERRIDE_SCOPING = new Set(["includes", "files", "ignore"]);
33
+ const SKIP_DIRS = new Set([
34
+ "node_modules",
35
+ "dist",
36
+ "build",
37
+ ".next",
38
+ ".git",
39
+ "coverage",
40
+ ".turbo",
41
+ ".claude",
42
+ ]);
43
+ /** Every biome config in the tree, repo-root-relative, excluding SKIP_DIRS. */
44
+ export function findBiomeConfigs(root) {
45
+ const out = [];
46
+ const visit = (dir) => {
47
+ let entries;
48
+ try {
49
+ entries = readdirSync(dir);
50
+ }
51
+ catch {
52
+ return;
53
+ }
54
+ for (const entry of entries) {
55
+ if (SKIP_DIRS.has(entry))
56
+ continue;
57
+ const path = join(dir, entry);
58
+ let st;
59
+ try {
60
+ st = lstatSync(path);
61
+ }
62
+ catch {
63
+ continue;
64
+ }
65
+ if (st.isDirectory()) {
66
+ visit(path);
67
+ continue;
68
+ }
69
+ if (st.isFile() && (entry === "biome.json" || entry === "biome.jsonc")) {
70
+ out.push(relative(root, path));
71
+ }
72
+ }
73
+ };
74
+ visit(root);
75
+ return out.sort();
76
+ }
77
+ function readConfig(path) {
78
+ try {
79
+ // biome.jsonc allows comments; strip line comments before parsing rather
80
+ // than adding a JSONC dependency for a check that only reads a few keys.
81
+ const raw = readFileSync(path, "utf8").replace(/^\s*\/\/.*$/gm, "");
82
+ const parsed = JSON.parse(raw);
83
+ return typeof parsed === "object" && parsed !== null ? parsed : undefined;
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ function extendsList(config) {
90
+ const e = config.extends;
91
+ if (typeof e === "string")
92
+ return [e];
93
+ if (Array.isArray(e))
94
+ return e.filter((x) => typeof x === "string");
95
+ return [];
96
+ }
97
+ /**
98
+ * True when this repo IS the package that publishes the preset. Its root config
99
+ * extends `./configs/biome.json` by path (the specifier would resolve through
100
+ * node_modules to itself), and `configs/biome.json` is the preset SOURCE, not a
101
+ * competing config — so both are exempt from (a) and (c).
102
+ */
103
+ function isPresetSource(root) {
104
+ return resolve(root) === resolve(packageRoot);
105
+ }
106
+ /** Overrides that do more than turn `noConsole` off for some paths. */
107
+ function offendingOverrides(config) {
108
+ const overrides = config.overrides;
109
+ // ast-grep-ignore: silent-default-return -- "no overrides key" and "overrides is not a list" both mean the config has no override to object to; [] is the real answer, not a swallowed failure
110
+ if (!Array.isArray(overrides))
111
+ return [];
112
+ const bad = [];
113
+ overrides.forEach((entry, i) => {
114
+ if (typeof entry !== "object" || entry === null)
115
+ return;
116
+ const o = entry;
117
+ for (const key of Object.keys(o)) {
118
+ if (OVERRIDE_SCOPING.has(key))
119
+ continue;
120
+ if (key !== "linter") {
121
+ bad.push(`overrides[${i}].${key}`);
122
+ continue;
123
+ }
124
+ // A linter override is allowed if the ONLY rule it touches is noConsole.
125
+ const rules = o.linter?.rules;
126
+ const groups = typeof rules === "object" && rules !== null ? rules : {};
127
+ const groupNames = Object.keys(groups);
128
+ const onlySuspicious = groupNames.length === 1 && groupNames[0] === "suspicious";
129
+ const suspicious = groups.suspicious;
130
+ const ruleNames = typeof suspicious === "object" && suspicious !== null ? Object.keys(suspicious) : [];
131
+ const onlyNoConsole = ruleNames.length === 1 && ruleNames[0] === "noConsole";
132
+ if (!onlySuspicious || !onlyNoConsole) {
133
+ bad.push(`overrides[${i}].linter.rules (only suspicious.noConsole may be overridden)`);
134
+ }
135
+ }
136
+ });
137
+ return bad;
138
+ }
139
+ /**
140
+ * Everything wrong with `root`'s biome configuration; [] when it conforms.
141
+ * `root` is the repo being checked, not this package.
142
+ */
143
+ export function presetDrift(root) {
144
+ const configs = findBiomeConfigs(root);
145
+ if (configs.length === 0)
146
+ return [];
147
+ const findings = [];
148
+ const source = isPresetSource(root);
149
+ // (c) One config, at the root. The preset source itself carries a second one
150
+ // — `configs/biome.json` IS the preset — and that is not drift.
151
+ const extras = configs.filter((c) => c !== "biome.json" && !(source && c === join("configs", "biome.json")));
152
+ if (extras.length > 0) {
153
+ findings.push({
154
+ key: "extra-config",
155
+ detail: `${extras.length} biome config(s) besides the root one: ${extras.join(", ")}`,
156
+ });
157
+ }
158
+ const rootPath = join(root, "biome.json");
159
+ if (!existsSync(rootPath)) {
160
+ findings.push({
161
+ key: "no-root-config",
162
+ detail: `biome config(s) present (${configs.join(", ")}) but no root biome.json extending ${PRESET_SPECIFIER}`,
163
+ });
164
+ return findings;
165
+ }
166
+ const config = readConfig(rootPath);
167
+ if (config === undefined) {
168
+ findings.push({ key: "unreadable", detail: "biome.json is not readable JSON" });
169
+ return findings;
170
+ }
171
+ // (a) Extends the preset. The preset source extends its own file by path.
172
+ const ext = extendsList(config);
173
+ const conforms = source
174
+ ? ext.some((e) => e.endsWith("configs/biome.json"))
175
+ : ext.includes(PRESET_SPECIFIER);
176
+ if (!conforms) {
177
+ findings.push({
178
+ key: "extends",
179
+ detail: ext.length === 0
180
+ ? `biome.json has no "extends" — it must extend ${PRESET_SPECIFIER}`
181
+ : `biome.json extends ${ext.join(", ")} — it must extend ${PRESET_SPECIFIER}`,
182
+ });
183
+ }
184
+ // (b) Does not restate what the preset decides.
185
+ const owned = PRESET_OWNED.filter((k) => config[k] !== undefined);
186
+ const nested = PRESET_OWNED_NESTED.filter((k) => config[k]?.formatter !== undefined).map((k) => `${k}.formatter`);
187
+ const restated = [...owned, ...nested];
188
+ if (restated.length > 0) {
189
+ findings.push({
190
+ key: "own-keys",
191
+ detail: `biome.json carries its own ${restated.join(", ")} — the preset owns these`,
192
+ });
193
+ }
194
+ const badOverrides = offendingOverrides(config);
195
+ if (badOverrides.length > 0) {
196
+ findings.push({
197
+ key: "overrides",
198
+ detail: `${badOverrides.join(", ")} — an override may only scope paths and turn suspicious.noConsole off`,
199
+ });
200
+ }
201
+ return findings;
202
+ }
203
+ /**
204
+ * Whether drift GATES in this repo, from `[biome] preset = "enforced"` in
205
+ * `.agentvibes/project.toml`.
206
+ *
207
+ * Opt-in per repo, then flipped (owner, 2026-09-06). Failing every repo the day
208
+ * this shipped would have turned 5 of the 7 repos that run this gate red —
209
+ * SiteCraftMonorepo, faceless-photo-lib, agent-session-observatory, tg-gallery,
210
+ * merkle-substrate — before the migration issue for each was worked, blocking
211
+ * unrelated work in all five. Each child of epic is-a70a5963 adds the line as
212
+ * its last step; when the last child lands, this default flips to enforced.
213
+ *
214
+ * Unenforced is NOT silent: `verify` prints the drift as a note and `doctor`
215
+ * always reports it. What the flag decides is the exit code, not the message.
216
+ */
217
+ export function presetEnforced(cwd) {
218
+ return readTomlTable(join(cwd, ".agentvibes", "project.toml"), "biome").preset === "enforced";
219
+ }
220
+ /** The one-line message the gate prints; "" when there is no drift. */
221
+ export function driftMessage(findings) {
222
+ if (findings.length === 0)
223
+ return "";
224
+ return `biome preset drift: ${findings.map((f) => f.detail).join("; ")}. One preset everywhere — extend ${PRESET_SPECIFIER} and delete the local copy (epic is-a70a5963).`;
225
+ }
226
+ //# sourceMappingURL=presetDrift.js.map