rulebookguard 0.5.1 → 0.7.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 CHANGED
@@ -1,17 +1,42 @@
1
+ <!-- prettier-ignore -->
2
+ <div align="center">
3
+
1
4
  # rulebookguard
2
5
 
6
+ [![npm version](https://img.shields.io/npm/v/rulebookguard?style=flat-square)](https://www.npmjs.com/package/rulebookguard)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/JuanCJR/rulebookguard/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/JuanCJR/rulebookguard/actions/workflows/ci.yml)
8
+ ![Node version](https://img.shields.io/node/v/rulebookguard?style=flat-square)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
10
+ [![License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](LICENSE)
11
+
3
12
  Keep business rules as the single source of truth, readable by humans and AI, and bound to the code and tests that implement them.
4
13
 
5
- Business rules usually end up scattered across code, tickets and people's memory. Neither a developer's memory nor an AI model's reading of the code is deterministic. `rulebookguard` keeps each rule in its own small Markdown file, links it to the code it governs (anchors) and to the tests that prove it (`@rule` tags), and fails CI when they drift apart.
14
+ [Overview](#overview) • [Get started](#getting-started) • [Writing rules](#writing-rules) • [Commands](#commands) • [CI recipe](#ci-recipe) • [Claude Code plugin](#claude-code-plugin)
15
+
16
+ </div>
17
+
18
+ ## Overview
19
+
20
+ Business rules usually end up scattered across code, tickets and people's memory. Neither a developer's memory nor an AI model's reading of the code is deterministic.
21
+
22
+ `rulebookguard` keeps each rule in its own small Markdown file, links it to the code it governs (**anchors**) and to the tests that prove it (**`@rule` … `@endrule` regions**), and fails CI when they drift apart. It is language-agnostic: rules are Markdown, and tests in any language link to them through a comment.
23
+
24
+ ```text
25
+ docs/rules/clients/R-CLI-001.md ── anchors ──▶ src/clients/**/*.ts
26
+ ▲
27
+ └── @rule R-CLI-001 … @endrule R-CLI-001 in src/clients/create.test.ts
28
+ ```
29
+
30
+ ## Features
6
31
 
7
32
  - **Atomic rules.** One file per rule, with YAML frontmatter validated against a versioned JSON Schema.
8
33
  - **Progressive disclosure.** Contexts map (L0), `id + summary` per context (L1), the full rule (L2), so neither people nor AI models have to read the whole catalog.
9
34
  - **Inverse index.** Given a file path, list the active rules anchored to it. Deterministic glob matching, no embeddings.
10
- - **Linter.** Untested rules, tags pointing at missing or deprecated rules, dead anchors, invalid files, and rules changed without a test change.
35
+ - **Linter.** Untested rules, tags pointing at missing or deprecated rules, dead anchors, invalid files, and rules changed without a change in their tests.
36
+ - **AI-ready.** A Claude Code hook, an MCP server and a skill hand the rules of a file to an agent before it edits it.
37
+ - **Static site.** A read-only, searchable view of the rulebook for the whole team.
11
38
 
12
- It is language-agnostic: rules are Markdown, and tests in any language link to them through a comment.
13
-
14
- ## Install
39
+ ## Getting started
15
40
 
16
41
  Requires Node.js 22 or later.
17
42
 
@@ -25,6 +50,13 @@ Or add it to a project:
25
50
  npm install --save-dev rulebookguard
26
51
  ```
27
52
 
53
+ A first rule in four steps:
54
+
55
+ 1. Create `rulebookguard.config.yaml` at the repository root (see [Configuration](#configuration)).
56
+ 2. Scaffold a rule: `npx rulebookguard new clients` creates the context and `docs/rules/clients/R-CLI-001.md` as `proposed`.
57
+ 3. Write the rule, add its `anchors`, set `status: active`, and wrap the test that proves it with `@rule R-CLI-001` and `@endrule R-CLI-001`.
58
+ 4. Run `npx rulebookguard lint`, and add it to CI with the [CI recipe](#ci-recipe).
59
+
28
60
  ## Configuration
29
61
 
30
62
  Put `rulebookguard.config.yaml` at the repository root. Commands find it by walking up from the current directory, and every path in the catalog is relative to the directory that holds it.
@@ -39,13 +71,23 @@ contextBudget: 30 # default: 30; lint warns when a context holds more rules
39
71
  language: es # default: en; language of the text that rulebookguard generates
40
72
  ```
41
73
 
42
- `schema` and `testGlobs` are required (`testGlobs: []` is valid but finds no tags). Unknown fields are rejected.
74
+ `schema` and `testGlobs` are required (`testGlobs: []` is valid but finds no tags). Unknown fields are rejected. The JSON Schemas ship with the package under `rulebookguard/schemas/v1/`.
75
+
76
+ `language` (`en` or `es`) sets the language of the text rulebookguard writes for you: the template of `new`, the hook context, the site, and every message printed once the config is loaded (lint findings, errors, warnings and headers, also the `message` of `--json` output). Lint codes, JSON keys and values such as `active` stay in English, and so do `--help`, argument errors and the findings of an invalid config file, which come before the config is read. Rule content is free text and is never checked for language, so rules can be written in any language whatever this setting says.
77
+
78
+ > [!NOTE]
79
+ > `language` needs rulebookguard 0.1.1 or later; older versions reject it as an unknown field.
43
80
 
44
- `language` (`en` or `es`) sets the language of the text rulebookguard writes for you, such as the template of `new`. Rule content is free text and is never checked for language, so rules can be written in any language whatever this setting says. `language` needs rulebookguard 0.1.1 or later; older versions reject it as an unknown field. The JSON Schemas ship with the package under `rulebookguard/schemas/v1/`.
81
+ <details>
82
+ <summary>How files are listed</summary>
45
83
 
46
84
  Inside a git work tree, files are listed with `git ls-files --cached --others --exclude-standard`, so anything in `.gitignore` (`dist/`, `coverage/`, caches) is never matched by anchors or test globs; files inside submodules are listed too. A submodule path that is a symlink, or that resolves outside the rulebook root, is skipped. A test file that is a symlink is scanned for `@rule` tags only when its target lies inside the rulebook root. If one submodule cannot be listed, commands print `warning: could not list files of submodule "<path>" (<reason>); skipping it.` and keep the rest of the git listing. Outside git, or when the rulebook root itself is git-ignored, the whole tree is walked, skipping only `.git` and `node_modules`. If git fails (for example, git is missing from `PATH` or the repository has dubious ownership), commands print one warning on stderr and walk the tree instead.
47
85
 
48
- ## Catalog layout
86
+ </details>
87
+
88
+ ## Writing rules
89
+
90
+ ### Catalog layout
49
91
 
50
92
  Rules are grouped by context, one directory per context:
51
93
 
@@ -74,7 +116,7 @@ The clients context owns customer identity and contact data.
74
116
 
75
117
  The `context` field must match the directory name, and every rule id in the directory must start with `R-<prefix>-`.
76
118
 
77
- ## Rule file format
119
+ ### Rule file format
78
120
 
79
121
  ````markdown
80
122
  ---
@@ -114,15 +156,14 @@ Scenario: Duplicate document in the same country
114
156
 
115
157
  The Markdown body holds the full rule and its examples (Gherkin scenarios or tables). Rules do not list their tests: the link lives only in the tests, through `@rule` tags.
116
158
 
117
- ## The `@rule` tag
159
+ ### Linking tests with `@rule` … `@endrule`
118
160
 
119
- A test links to a rule with `@rule <ID>` anywhere in a file matched by `testGlobs`. One line can carry several tags. A comment is the usual place, but the tag is matched as text, so it also counts inside a test name or a string:
161
+ A test links to a rule with a region: `@rule <ID>` opens it and `@endrule <ID>` closes it, in a file matched by `testGlobs`. Both lines belong to the region. Regions may nest (a suite tagged with one rule holding tests tagged with others) as long as they close in order. One line can carry several markers.
120
162
 
121
- ```ts
122
- it('rejects a duplicate document @rule R-CLI-001', () => {});
123
- ```
163
+ > [!IMPORTANT]
164
+ > Every `@rule` needs its `@endrule`, otherwise `lint` reports `unclosed-rule-tag`; an `@endrule` with no open `@rule` of the same id is reported as `unmatched-endrule-tag`. The region tells `lint --base` which lines belong to the rule: a changed rule needs a change inside one of its regions, not anywhere in the test file.
124
165
 
125
- To mention a rule in a test file without linking to it, write the id without the `@rule` prefix.
166
+ A comment is the usual place, but the markers are matched as text, so they also count inside a test name or a string. To mention a rule in a test file without linking to it, write the id without the `@rule` prefix.
126
167
 
127
168
  TypeScript or JavaScript:
128
169
 
@@ -131,6 +172,7 @@ TypeScript or JavaScript:
131
172
  it('rejects a duplicate document in the same country', () => {
132
173
  /* ... */
133
174
  });
175
+ // @endrule R-CLI-001
134
176
  ```
135
177
 
136
178
  Go:
@@ -138,6 +180,7 @@ Go:
138
180
  ```go
139
181
  // @rule R-CLI-001 @rule R-BIL-001
140
182
  func TestCreateClient(t *testing.T) {}
183
+ // @endrule R-BIL-001 @endrule R-CLI-001
141
184
  ```
142
185
 
143
186
  Python:
@@ -146,6 +189,7 @@ Python:
146
189
  # @rule R-CLI-001
147
190
  def test_duplicate_document():
148
191
  ...
192
+ # @endrule R-CLI-001
149
193
  ```
150
194
 
151
195
  Java or Kotlin:
@@ -154,6 +198,7 @@ Java or Kotlin:
154
198
  /** @rule R-CLI-001 */
155
199
  @Test
156
200
  void rejectsDuplicateDocument() {}
201
+ /** @endrule R-CLI-001 */
157
202
  ```
158
203
 
159
204
  SQL:
@@ -161,12 +206,27 @@ SQL:
161
206
  ```sql
162
207
  -- @rule R-BIL-002
163
208
  SELECT count(*) FROM invoices WHERE number IS NULL;
209
+ -- @endrule R-BIL-002
164
210
  ```
165
211
 
166
212
  ## Commands
167
213
 
168
214
  `rulebookguard --help` prints the command list. Exit codes: `0` success, `1` failure (unknown id or context, missing config, lint errors, git errors), `2` usage error (unknown command or option, missing or extra argument).
169
215
 
216
+ | Command | What it does |
217
+ | ------------------------------------------------------ | ------------------------------------------------------------ |
218
+ | [`contexts`](#contexts-l0) | the contexts map (L0) |
219
+ | [`list`](#list---context-c---status-s-l1) | rule ids and summaries (L1) |
220
+ | [`show <id>`](#show-id-l2) | the full rule (L2) |
221
+ | [`for <path>`](#for-path) | the active rules anchored to a file |
222
+ | [`impact <id>`](#impact-id) | the files and tests a rule touches |
223
+ | [`new <context>`](#new-context---prefix-p) | scaffolds the next rule of a context |
224
+ | [`lint`](#lint---base-ref---json) | checks the whole catalog, and optionally changes since a ref |
225
+ | [`hook`](#hook) | Claude Code hook that injects the rules of a file |
226
+ | [`mcp`](#mcp---project-dir) | MCP server for AI agents |
227
+ | [`site:generate`](#sitegenerate---out-dir---base-path) | builds a static site of the rulebook |
228
+ | [`site:run`](#siterun---out-dir---base-path---port-n) | builds the site and serves it locally |
229
+
170
230
  The read commands (`contexts`, `list`, `show`, `for`, `impact`) accept `--json` for machine consumers. When the catalog has problems they still run, and print one warning on stderr pointing at `lint`.
171
231
 
172
232
  ### `contexts` (L0)
@@ -184,7 +244,7 @@ $ rulebookguard list --context clients --status active
184
244
  R-CLI-001 [active] A client document number is unique per country.
185
245
  ```
186
246
 
187
- Exits 1 for an unknown context and 2 for an invalid status.
247
+ Prints `No rules found.` when no rule matches, and `contexts` says so when the catalog has no context yet. Exits 1 for an unknown context and 2 for an invalid status.
188
248
 
189
249
  ### `show <id>` (L2)
190
250
 
@@ -194,7 +254,9 @@ R-CLI-001: A client document number is unique per country.
194
254
  context: clients
195
255
  status: active
196
256
  entities: Client
197
- anchors: src/clients/**/*.ts, src/api/clients.controller.ts
257
+ anchors:
258
+ src/clients/**/*.ts
259
+ src/api/clients.controller.ts
198
260
  supersedes: R-CLI-000
199
261
  file: docs/rules/clients/R-CLI-001.md
200
262
 
@@ -231,7 +293,7 @@ Tests tagging R-CLI-001 (1):
231
293
 
232
294
  Exits 1 for an unknown id.
233
295
 
234
- ### `new <context>`
296
+ ### `new <context> [--prefix <P>]`
235
297
 
236
298
  Scaffolds the next free id of a context as a `proposed` rule and prints its path. Ids are never reused, and an existing file is never overwritten. The placeholder text of the new rule follows `language` in the config.
237
299
 
@@ -240,11 +302,63 @@ $ rulebookguard new clients
240
302
  docs/rules/clients/R-CLI-004.md
241
303
  ```
242
304
 
243
- Exits 1 for an unknown context or when the context has no free id left.
305
+ When the context does not exist yet, `new` creates its `_context.md` first and prints both paths. The prefix defaults to the first three letters and digits of the name, upper-cased; `--prefix` sets another one.
306
+
307
+ ```sh
308
+ $ rulebookguard new inventory
309
+ docs/rules/inventory/_context.md
310
+ docs/rules/inventory/R-INV-001.md
311
+ ```
312
+
313
+ Exits 1, writing nothing, when the context has no free id left, when the name is not lowercase letters, digits, `-` or `_`, when the prefix is invalid or already used by another context, or when `--prefix` differs from the prefix of an existing context.
314
+
315
+ ### `lint [--base <ref>] [--json]`
316
+
317
+ Checks the whole catalog. Each finding is printed as `<severity> <code> <file>[:line] <message>`, with a `hint:` line under it that says how to fix it, followed by a count.
318
+
319
+ | Severity | Code | Meaning |
320
+ | -------- | ------------------------ | ------------------------------------------------------------------ |
321
+ | error | `untested-rule` | an active rule has no test tagging it |
322
+ | error | `unknown-rule-tag` | a test tags a rule id that does not exist |
323
+ | error | `deprecated-rule-tag` | a test tags a deprecated rule |
324
+ | error | `unclosed-rule-tag` | a `@rule` has no matching `@endrule` |
325
+ | error | `unmatched-endrule-tag` | an `@endrule` has no open `@rule` of the same id |
326
+ | error | `unmatched-anchor` | an anchor glob matches no file |
327
+ | error | `schema` | a config, context or rule file breaks the JSON Schema |
328
+ | error | `duplicate-id` | two rule files share an id |
329
+ | error | `file-name-mismatch` | a rule file is not named `<id>.md` |
330
+ | error | `id-prefix-mismatch` | a rule id does not use its context prefix |
331
+ | error | `context-mismatch` | a `context` field does not match its directory |
332
+ | error | `missing-context-file` | a context directory has no `_context.md` |
333
+ | error | `missing-rules-dir` | `rulesDir` does not exist |
334
+ | error | `pending-implementation` | with `--base`: an active rule changed, but none of its regions did |
335
+ | warning | `context-over-budget` | a context holds more rules than `contextBudget` |
336
+
337
+ ```sh
338
+ $ rulebookguard lint
339
+ error untested-rule docs/rules/billing/R-BIL-002.md active rule R-BIL-002 has no test tagging it
340
+ hint: wrap a test in a file matched by testGlobs with "@rule R-BIL-002" and "@endrule R-BIL-002", or set the rule's status to proposed
341
+ warning context-over-budget docs/rules/clients/_context.md context "clients" has 31 rules, more than the contextBudget of 30
342
+ hint: split the context into smaller ones, or raise contextBudget in rulebookguard.config.yaml
343
+ 1 error, 1 warning
344
+ ```
345
+
346
+ Exits 1 when there is any error, and 0 when there are only warnings or nothing at all (`No issues found.`). An invalid `rulebookguard.config.yaml` is reported as `schema` findings too; tags of a rule file that fails the schema are not reported again as `unknown-rule-tag`.
347
+
348
+ With `--base <ref>`, lint also compares `<ref>` with the working tree (`git diff --name-only <ref>` plus untracked files, so uncommitted work counts). A rule whose file changed must come with a change inside at least one of its `@rule` … `@endrule` regions (`git diff -U0 <ref>`; a test file that is still untracked counts whole, and a deletion counts when it falls strictly inside the region), otherwise it is reported as `pending-implementation`. A change elsewhere in the same test file, such as another test or a blank line between regions, does not count; an active rule that no test tags at all is reported only as `untested-rule`. Only rules whose status is `active` after the change are checked: new active rules are checked, `proposed` and `deprecated` rules are not. Outside a git work tree, with a ref git cannot resolve, or when git fails while listing the changes, lint fails with exit code 1.
349
+
350
+ ```sh
351
+ $ rulebookguard lint --base origin/main
352
+ error pending-implementation docs/rules/clients/R-CLI-001.md rule R-CLI-001 changed since origin/main but none of the tests tagging it changed
353
+ hint: update or add a test inside an "@rule R-CLI-001" … "@endrule R-CLI-001" region that covers the changed rule
354
+ 1 error, 0 warnings
355
+ ```
356
+
357
+ `--json` prints `{ "findings": [{ "severity", "code", "file", "line"?, "message", "hint" }], "errors", "warnings" }`.
244
358
 
245
359
  ### `hook`
246
360
 
247
- A Claude Code hook for `PreToolUse` and `PostToolUse` events of tools that take a `file_path` (the Claude Code plugin below wires it up). It reads the hook event as JSON on stdin, takes `tool_input.file_path` (relative paths resolve against the event's `cwd`), finds `rulebookguard.config.yaml` by walking up from that file, and prints the active rules anchored to it as additional context for Claude, under the same `hookEventName`:
361
+ A Claude Code hook for `PreToolUse` and `PostToolUse` events of tools that take a `file_path` (the [Claude Code plugin](#claude-code-plugin) wires it up). It reads the hook event as JSON on stdin, takes `tool_input.file_path` (relative paths resolve against the event's `cwd`), finds `rulebookguard.config.yaml` by walking up from that file, and prints the active rules anchored to it as additional context for Claude, under the same `hookEventName`:
248
362
 
249
363
  ```sh
250
364
  $ echo '{"hook_event_name":"PostToolUse","tool_name":"Read","tool_input":{"file_path":"/repo/src/billing/invoice.go"}}' | rulebookguard hook
@@ -253,7 +367,10 @@ $ echo '{"hook_event_name":"PostToolUse","tool_name":"Read","tool_input":{"file_
253
367
 
254
368
  Claude Code adds the context next to the tool result, so a `PostToolUse` hook on `Read` delivers the rules together with the file contents, before Claude writes a change (Claude Code requires reading a file before editing it).
255
369
 
256
- Each rule comes with its id, context, summary, rule file and full text; when the texts add up to more than 10,000 characters only the summaries are listed, with a pointer to `rulebookguard show <id>`. The header follows `language` in the config. The hook never blocks a tool and never answers the permission prompt: with no config, a path outside the rulebook root, no active rule, another event, unreadable input or any error, it prints nothing and exits 0.
370
+ Each rule comes with its id, context, summary, rule file and full text; when the texts add up to more than 10,000 characters only the summaries are listed, with a pointer to `rulebookguard show <id>`. The header follows `language` in the config.
371
+
372
+ > [!NOTE]
373
+ > The hook never blocks a tool and never answers the permission prompt: with no config, a path outside the rulebook root, no active rule, another event, unreadable input or any error, it prints nothing and exits 0.
257
374
 
258
375
  ### `mcp [--project <dir>]`
259
376
 
@@ -268,11 +385,12 @@ The rulebook is found from `--project` (default: the current directory). A comma
268
385
 
269
386
  ### `site:generate [--out <dir>] [--base <path>]`
270
387
 
271
- The site commands build with Astro, which lives in a separate package so that installing the linter does not download it. Install it next to `rulebookguard` (it requires Node.js 22.23.4 or later):
272
-
273
- ```sh
274
- npm install --save-dev rulebookguard @rulebookguard/site
275
- ```
388
+ > [!IMPORTANT]
389
+ > The site commands build with Astro, which lives in a separate package so that installing the linter does not download it. Install it next to `rulebookguard` (it requires Node.js 22.23.4 or later):
390
+ >
391
+ > ```sh
392
+ > npm install --save-dev rulebookguard @rulebookguard/site
393
+ > ```
276
394
 
277
395
  Without it, the site commands fail, exit 1, with `The site commands need the @rulebookguard/site package. Install it next to rulebookguard: npm install --save-dev @rulebookguard/site`. One-off, without installing: `npx -p rulebookguard -p @rulebookguard/site rulebookguard site:run`.
278
396
 
@@ -304,46 +422,10 @@ The site has:
304
422
  - **Health:** the `lint` findings when the site was built.
305
423
  - Full-text search (Pagefind).
306
424
 
307
- Generated text and the interface follow `language` in the config. The site is a view: rules are still edited in their files, through pull requests. `site` replaces `--out` only when it is empty or was written by an earlier `site`; any other non-empty directory makes it fail, exit 1, without touching it. Add the output directory to `.gitignore` unless you mean to commit it.
308
-
309
- ### `lint [--base <ref>] [--json]`
310
-
311
- Checks the whole catalog. Each finding is printed as `<severity> <code> <file>[:line] <message>`, followed by a count.
312
-
313
- | Severity | Code | Meaning |
314
- | -------- | ------------------------ | ---------------------------------------------------------------- |
315
- | error | `untested-rule` | an active rule has no test tagging it |
316
- | error | `unknown-rule-tag` | a test tags a rule id that does not exist |
317
- | error | `deprecated-rule-tag` | a test tags a deprecated rule |
318
- | error | `unmatched-anchor` | an anchor glob matches no file |
319
- | error | `schema` | a config, context or rule file breaks the JSON Schema |
320
- | error | `duplicate-id` | two rule files share an id |
321
- | error | `file-name-mismatch` | a rule file is not named `<id>.md` |
322
- | error | `id-prefix-mismatch` | a rule id does not use its context prefix |
323
- | error | `context-mismatch` | a `context` field does not match its directory |
324
- | error | `missing-context-file` | a context directory has no `_context.md` |
325
- | error | `missing-rules-dir` | `rulesDir` does not exist |
326
- | error | `pending-implementation` | with `--base`: an active rule changed, but none of its tests did |
327
- | warning | `context-over-budget` | a context holds more rules than `contextBudget` |
328
-
329
- ```sh
330
- $ rulebookguard lint
331
- error untested-rule docs/rules/billing/R-BIL-002.md active rule R-BIL-002 has no test tagging it
332
- warning context-over-budget docs/rules/clients/_context.md context "clients" has 31 rules, more than the contextBudget of 30
333
- 1 error, 1 warning
334
- ```
335
-
336
- Exits 1 when there is any error, and 0 when there are only warnings or nothing at all (`No issues found.`). An invalid `rulebookguard.config.yaml` is reported as `schema` findings too; tags of a rule file that fails the schema are not reported again as `unknown-rule-tag`.
337
-
338
- With `--base <ref>`, lint also compares `<ref>` with the working tree (`git diff --name-only <ref>` plus untracked files, so uncommitted work counts). A rule whose file changed must come with a change in at least one test that tags it, otherwise it is reported as `pending-implementation`; an active rule that no test tags at all is reported only as `untested-rule`. Only rules whose status is `active` after the change are checked: new active rules are checked, `proposed` and `deprecated` rules are not. Outside a git work tree, with a ref git cannot resolve, or when git fails while listing the changes, lint fails with exit code 1.
425
+ Generated text and the interface follow `language` in the config. The site is a view: rules are still edited in their files, through pull requests.
339
426
 
340
- ```sh
341
- $ rulebookguard lint --base origin/main
342
- error pending-implementation docs/rules/clients/R-CLI-001.md rule R-CLI-001 changed since origin/main but none of the tests tagging it changed
343
- 1 error, 0 warnings
344
- ```
345
-
346
- `--json` prints `{ "findings": [{ "severity", "code", "file", "line"?, "message" }], "errors", "warnings" }`.
427
+ > [!WARNING]
428
+ > `site` replaces `--out` only when it is empty or was written by an earlier `site`; any other non-empty directory makes it fail, exit 1, without touching it. Add the output directory to `.gitignore` unless you mean to commit it.
347
429
 
348
430
  ## CI recipe
349
431
 
@@ -363,7 +445,8 @@ jobs:
363
445
  - run: npx --yes rulebookguard@0.1.0 lint --base origin/main
364
446
  ```
365
447
 
366
- Pin the version, or add rulebookguard as a dev dependency and run it from the lockfile, so a new release never changes CI results by itself.
448
+ > [!TIP]
449
+ > Pin the version, or add rulebookguard as a dev dependency and run it from the lockfile, so a new release never changes CI results by itself.
367
450
 
368
451
  ## Claude Code plugin
369
452
 
@@ -383,18 +466,3 @@ claude plugin install rulebookguard@rulebookguard
383
466
  Or, inside Claude Code 2.1.275 or later: `/plugin install rulebookguard --marketplace JuanCJR/rulebookguard`.
384
467
 
385
468
  The plugin runs the project's own rulebookguard (`node_modules/rulebookguard`, in the project directory or a parent) when its version is at least the plugin's; otherwise it runs `npx rulebookguard@<plugin version>`, which needs Node.js and network access the first time. The hooks never block a tool: any failure, including a timeout, lets it go ahead without rules.
386
-
387
- ## Releasing
388
-
389
- Releases are published to npm from GitHub Actions with [trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm token is stored and every version carries a provenance attestation. To release, bump `version` in `package.json` and in `plugin/.claude-plugin/plugin.json` (a test keeps them equal), merge it to `main`, then push a matching tag:
390
-
391
- ```sh
392
- git tag v0.1.0
393
- git push origin v0.1.0
394
- ```
395
-
396
- The `Release` workflow fails without publishing when the tag does not match the package version, or when `vp check`, `vp test` or `vp pack` fails.
397
-
398
- ## License
399
-
400
- MIT
@@ -1,4 +1,4 @@
1
- import { h as createGlobMatcher, v as toRepoPath } from "./lint-C1yUeLOq.mjs";
1
+ import { g as createGlobMatcher, y as toRepoPath } from "./lint-DfibQDW3.mjs";
2
2
  // src/catalog/anchors.ts
3
3
  function rulesForPath(rules, path) {
4
4
  const target = toRepoPath(path);