@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.
- package/README.md +131 -5
- package/dist/astGrep.d.ts +3 -0
- package/dist/astGrep.js +15 -2
- package/dist/astGrep.js.map +1 -1
- package/dist/doctor.js +38 -2
- package/dist/doctor.js.map +1 -1
- package/dist/hookPostedit.js +18 -1
- package/dist/hookPostedit.js.map +1 -1
- package/dist/presetDrift.d.ts +31 -0
- package/dist/presetDrift.js +226 -0
- package/dist/presetDrift.js.map +1 -0
- package/dist/presetDriftTest.d.ts +1 -0
- package/dist/presetDriftTest.js +136 -0
- package/dist/presetDriftTest.js.map +1 -0
- package/dist/repoRules.d.ts +27 -0
- package/dist/repoRules.js +86 -0
- package/dist/repoRules.js.map +1 -0
- package/dist/repoRulesTest.d.ts +1 -0
- package/dist/repoRulesTest.js +122 -0
- package/dist/repoRulesTest.js.map +1 -0
- package/dist/screenScope.d.ts +16 -5
- package/dist/screenScope.js +66 -21
- package/dist/screenScope.js.map +1 -1
- package/dist/screenScopeTest.js +67 -7
- package/dist/screenScopeTest.js.map +1 -1
- package/dist/siblingCloneTest.d.ts +1 -0
- package/dist/siblingCloneTest.js +201 -0
- package/dist/siblingCloneTest.js.map +1 -0
- package/dist/testRules.js +77 -0
- package/dist/testRules.js.map +1 -1
- package/dist/twinCoverageTest.d.ts +1 -0
- package/dist/twinCoverageTest.js +262 -0
- package/dist/twinCoverageTest.js.map +1 -0
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +33 -8
- package/dist/verify.js.map +1 -1
- package/dist/verifyDiff.js +6 -5
- package/dist/verifyDiff.js.map +1 -1
- package/package.json +7 -3
- package/rules/__fixtures__/demo-mode-by-default/bad.ts +22 -0
- package/rules/__fixtures__/demo-mode-by-default/bad.tsx +22 -0
- package/rules/__fixtures__/demo-mode-by-default/good.ts +21 -0
- package/rules/__fixtures__/demo-mode-by-default/good.tsx +21 -0
- package/rules/__fixtures__/json-roundtrip/bad.ts +6 -0
- package/rules/__fixtures__/json-roundtrip/bad.tsx +8 -0
- package/rules/__fixtures__/json-roundtrip/good.ts +5 -0
- package/rules/__fixtures__/json-roundtrip/good.tsx +7 -0
- package/rules/__fixtures__/kind-if-without-match/bad.ts +17 -0
- package/rules/__fixtures__/kind-if-without-match/bad.tsx +18 -0
- package/rules/__fixtures__/kind-if-without-match/good.ts +14 -0
- package/rules/__fixtures__/kind-if-without-match/good.tsx +13 -0
- package/rules/__fixtures__/literal-union-in-component/components/bad.tsx +16 -0
- package/rules/__fixtures__/literal-union-in-component/components/good.tsx +25 -0
- package/rules/__fixtures__/non-exhaustive-match/bad.tsx +12 -0
- package/rules/__fixtures__/non-exhaustive-match/good.tsx +14 -0
- package/rules/__fixtures__/silent-default-return/bad.ts +9 -0
- package/rules/__fixtures__/silent-default-return/bad.tsx +10 -0
- package/rules/__fixtures__/silent-default-return/good.ts +17 -0
- package/rules/__fixtures__/silent-default-return/good.tsx +11 -0
- package/rules/demo-mode-by-default-ts.yml +17 -1
- package/rules/demo-mode-by-default.yml +17 -1
- package/rules/json-roundtrip-tsx.yml +26 -0
- package/rules/kind-if-without-match-tsx.yml +31 -0
- package/rules/literal-union-in-component-tsx.yml +81 -0
- package/rules/no-local-kit-clone-tsx.yml +16 -0
- package/rules/no-local-kit-clone.yml +16 -0
- package/rules/non-exhaustive-match-tsx.yml +48 -0
- 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.
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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 +
|
|
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
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
|
-
|
|
17
|
-
|
|
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);
|
package/dist/astGrep.js.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/doctor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAUjE,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrC,IAAI,EAAE,eAAe,IAAI,IAAI,GAAG,IAAI,QAAQ,EAAE;SAC/C,CAAC;IACJ,CAAC;IACD,OAAO;QACL,KAAK,EAAE,IAAI;QACX,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,GAAG,EAAE,QAAQ,CAAC,GAAG;QACjB,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,KAAK,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,iBAAiB,CAAC,EAAE,CAAC;QAChD,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC;aAC3B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;aACjC,IAAI,EAAE,CAAC;QACV,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAClB,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACxC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,eAAe,CAAC,GAAW;IAClC,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IACrC,KAAK,CAAC,cAAc,CAAC,GAAG,
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
|
package/dist/hookPostedit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/hookPostedit.js.map
CHANGED
|
@@ -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
|