rulebookguard 0.7.0 → 0.8.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 (3) hide show
  1. package/README.md +37 -18
  2. package/dist/cli.mjs +142 -59
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -52,8 +52,8 @@ npm install --save-dev rulebookguard
52
52
 
53
53
  A first rule in four steps:
54
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`.
55
+ 1. Set up the rulebook: `npx rulebookguard init clients --ci` writes `rulebookguard.config.yaml`, the `clients` context and a CI workflow (see [Configuration](#configuration) to adjust `testGlobs`).
56
+ 2. Scaffold a rule: `npx rulebookguard new clients` creates `docs/rules/clients/R-CLI-001.md` as `proposed`.
57
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
58
  4. Run `npx rulebookguard lint`, and add it to CI with the [CI recipe](#ci-recipe).
59
59
 
@@ -213,22 +213,38 @@ SELECT count(*) FROM invoices WHERE number IS NULL;
213
213
 
214
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).
215
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 |
216
+ | Command | What it does |
217
+ | ----------------------------------------------------------------- | ------------------------------------------------------------ |
218
+ | [`init <context>`](#init-context---prefix-p---language-enes---ci) | sets up the config, a first context and optionally CI |
219
+ | [`contexts`](#contexts-l0) | the contexts map (L0) |
220
+ | [`list`](#list---context-c---status-s-l1) | rule ids and summaries (L1) |
221
+ | [`show <id>`](#show-id-l2) | the full rule (L2) |
222
+ | [`for <path>`](#for-path) | the active rules anchored to a file |
223
+ | [`impact <id>`](#impact-id) | the files and tests a rule touches |
224
+ | [`new <context>`](#new-context---prefix-p) | scaffolds the next rule of a context |
225
+ | [`lint`](#lint---base-ref---json) | checks the whole catalog, and optionally changes since a ref |
226
+ | [`hook`](#hook) | Claude Code hook that injects the rules of a file |
227
+ | [`mcp`](#mcp---project-dir) | MCP server for AI agents |
228
+ | [`site:generate`](#sitegenerate---out-dir---base-path) | builds a static site of the rulebook |
229
+ | [`site:run`](#siterun---out-dir---base-path---port-n) | builds the site and serves it locally |
229
230
 
230
231
  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`.
231
232
 
233
+ ### `init <context> [--prefix <P>] [--language <en|es>] [--ci]`
234
+
235
+ Sets up a rulebook in the current directory and prints the paths it wrote: `rulebookguard.config.yaml`, then the context, the same one `new` would create (`--prefix` works the same way).
236
+
237
+ ```sh
238
+ $ rulebookguard init clients --ci
239
+ rulebookguard.config.yaml
240
+ docs/rules/clients/_context.md
241
+ .github/workflows/rulebookguard.yml
242
+ ```
243
+
244
+ The config uses `rulesDir: docs/rules` and these `testGlobs`, which you can narrow afterwards: `**/*.test.*`, `**/*.spec.*`, `**/test_*.py` and `**/*_test.go`. `--language` writes `language` into the config, so the context template and later messages follow it. `--ci` also writes the GitHub Actions workflow of the [CI recipe](#ci-recipe), pinned to the running version.
245
+
246
+ Exits 1, writing nothing, when `rulebookguard.config.yaml` (or, with `--ci`, the workflow) already exists in the current directory, or when the context name or prefix is invalid. Exits 2 for a `--language` other than `en` or `es`.
247
+
232
248
  ### `contexts` (L0)
233
249
 
234
250
  ```sh
@@ -429,7 +445,7 @@ Generated text and the interface follow `language` in the config. The site is a
429
445
 
430
446
  ## CI recipe
431
447
 
432
- Run the linter on every pull request, comparing against the main branch. The full history is needed so that `origin/main` exists.
448
+ Run the linter on every pull request, comparing against the branch it targets. The full history is needed so that the base branch exists. `rulebookguard init --ci` writes this file as `.github/workflows/rulebookguard.yml`.
433
449
 
434
450
  ```yaml
435
451
  name: rules
@@ -442,7 +458,10 @@ jobs:
442
458
  - uses: actions/checkout@v7.0.1
443
459
  with:
444
460
  fetch-depth: 0
445
- - run: npx --yes rulebookguard@0.1.0 lint --base origin/main
461
+ - uses: actions/setup-node@v6
462
+ with:
463
+ node-version: 22
464
+ - run: npx --yes rulebookguard@0.8.0 lint --base origin/${{ github.base_ref }}
446
465
  ```
447
466
 
448
467
  > [!TIP]
@@ -465,4 +484,4 @@ claude plugin install rulebookguard@rulebookguard
465
484
 
466
485
  Or, inside Claude Code 2.1.275 or later: `/plugin install rulebookguard --marketplace JuanCJR/rulebookguard`.
467
486
 
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.
487
+ 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. In a checkout of rulebookguard itself it runs the checkout's own built CLI (`dist/cli.mjs`, from `vp pack`), whatever its version. The hooks never block a tool: any failure, including a timeout, lets it go ahead without rules.
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { F as messages, I as FormatError, L as formatIssue, O as CONTEXT_FILE_NAME, _ as expandGlobs, a as CliError, c as openCatalog, f as scanTestTags, l as writeJson, m as loadCatalog, o as findRule, p as findRulebookRoot, r as lintCommand, s as gitFallbackWarning, u as writeLines, y as toRepoPath } from "./lint-DfibQDW3.mjs";
2
+ import { F as messages, I as FormatError, L as formatIssue, M as CONFIG_FILE_NAME, O as CONTEXT_FILE_NAME, _ as expandGlobs, a as CliError, c as openCatalog, f as scanTestTags, l as writeJson, m as loadCatalog, o as findRule, p as findRulebookRoot, r as lintCommand, s as gitFallbackWarning, u as writeLines, y as toRepoPath } from "./lint-DfibQDW3.mjs";
3
3
  import { t as rulesForPath } from "./anchors-C2j9c5MI.mjs";
4
- import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
4
+ import { access, mkdir, readdir, rm, writeFile } from "node:fs/promises";
5
5
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { parseArgs } from "node:util";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
@@ -80,60 +80,7 @@ function render(path, rules, text) {
80
80
  return parts.join("\n\n");
81
81
  }
82
82
  // package.json
83
- var version = "0.7.0";
84
- // src/mcp/server.ts
85
- function createRulesServer(projectDir) {
86
- const server = new McpServer({
87
- name: "rulebookguard",
88
- version
89
- });
90
- server.registerTool("rules_for", {
91
- description: "Active business rules anchored to a file (id, context, summary, rule file). Call it before changing a file; the rules are the source of truth for its behavior.",
92
- inputSchema: z.object({ path: z.string().describe("File path, absolute or relative to the project directory") }),
93
- annotations: { readOnlyHint: true }
94
- }, ({ path }) => cli(projectDir, [
95
- "for",
96
- path,
97
- "--json"
98
- ]));
99
- server.registerTool("rules_show", {
100
- description: "Full text of one business rule: summary, anchors, entities, supersedes and the rule body with its examples.",
101
- inputSchema: z.object({ id: z.string().describe("Rule id, for example R-CLI-001") }),
102
- annotations: { readOnlyHint: true }
103
- }, ({ id }) => cli(projectDir, [
104
- "show",
105
- id,
106
- "--json"
107
- ]));
108
- return server;
109
- }
110
- async function cli(cwd, args) {
111
- let stdout = "";
112
- let stderr = "";
113
- if (await runCli(args, {
114
- cwd,
115
- stdout: (text) => {
116
- stdout += text;
117
- },
118
- stderr: (text) => {
119
- stderr += text;
120
- }
121
- }) === 0) return { content: [{
122
- type: "text",
123
- text: stdout
124
- }] };
125
- return {
126
- content: [{
127
- type: "text",
128
- text: stderr.trim()
129
- }],
130
- isError: true
131
- };
132
- }
133
- // src/commands/mcp.ts
134
- async function mcpCommand(io, project) {
135
- await createRulesServer(resolve(io.cwd, project ?? ".")).connect(new StdioServerTransport());
136
- }
83
+ var version = "0.8.0";
137
84
  // src/commands/new.ts
138
85
  const MAX_NUMBER = 999;
139
86
  const CONTEXT_NAME = /^[a-z0-9][a-z0-9_-]*$/;
@@ -163,7 +110,7 @@ async function createContext(catalog, name, option, text) {
163
110
  const directory = `${toRepoPath(catalog.config.rulesDir).replace(/\/+$/, "")}/${name}`;
164
111
  const file = `${directory}/${CONTEXT_FILE_NAME}`;
165
112
  await mkdir(join(catalog.root, directory), { recursive: true });
166
- await writeNew(catalog, file, contextTemplate(name, prefix, text), text);
113
+ await writeNew$1(catalog, file, contextTemplate(name, prefix, text), text);
167
114
  return {
168
115
  file,
169
116
  context: name,
@@ -181,10 +128,10 @@ async function writeNextRule(catalog, context, text) {
181
128
  if (last >= MAX_NUMBER) throw new CliError(text.noFreeId(context.context, `R-${context.prefix}-${MAX_NUMBER}`));
182
129
  const id = `R-${context.prefix}-${String(last + 1).padStart(3, "0")}`;
183
130
  const file = `${directory}/${id}.md`;
184
- await writeNew(catalog, file, ruleTemplate(id, context.context, catalog.config.language ?? "en"), text);
131
+ await writeNew$1(catalog, file, ruleTemplate(id, context.context, catalog.config.language ?? "en"), text);
185
132
  return file;
186
133
  }
187
- async function writeNew(catalog, file, content, text) {
134
+ async function writeNew$1(catalog, file, content, text) {
188
135
  try {
189
136
  await writeFile(join(catalog.root, file), content, { flag: "wx" });
190
137
  } catch (error) {
@@ -243,6 +190,116 @@ ${text.scenario}
243
190
  \`\`\`
244
191
  `;
245
192
  }
193
+ // src/commands/init.ts
194
+ const WORKFLOW_FILE = ".github/workflows/rulebookguard.yml";
195
+ const TEST_GLOBS = [
196
+ "**/*.test.*",
197
+ "**/*.spec.*",
198
+ "**/test_*.py",
199
+ "**/*_test.go"
200
+ ];
201
+ async function initCommand(io, name, options) {
202
+ const root = io.cwd;
203
+ const text = messages(options.language);
204
+ const files = options.ci ? [CONFIG_FILE_NAME, WORKFLOW_FILE] : [CONFIG_FILE_NAME];
205
+ for (const file of files) if (await exists(join(root, file))) throw new CliError(text.alreadyExists(file));
206
+ const written = [];
207
+ try {
208
+ await writeNew(root, CONFIG_FILE_NAME, configTemplate(options.language), written);
209
+ const context = await createContext(await loadCatalog(root), name, options.prefix, text);
210
+ written.push(context.file);
211
+ if (options.ci) await writeNew(root, WORKFLOW_FILE, WORKFLOW, written);
212
+ } catch (error) {
213
+ await Promise.all(written.map((file) => rm(join(root, file), { force: true })));
214
+ if (error.code === "EEXIST") throw new CliError(text.alreadyExists(written.at(-1) ?? "rulebookguard.config.yaml"));
215
+ throw error;
216
+ }
217
+ writeLines(io, written);
218
+ }
219
+ async function writeNew(root, file, content, written) {
220
+ await mkdir(dirname(join(root, file)), { recursive: true });
221
+ await writeFile(join(root, file), content, { flag: "wx" });
222
+ written.push(file);
223
+ }
224
+ async function exists(path) {
225
+ try {
226
+ await access(path);
227
+ return true;
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+ function configTemplate(language) {
233
+ return `schema: 1\nrulesDir: docs/rules\ntestGlobs:\n${TEST_GLOBS.map((glob) => ` - '${glob}'\n`).join("")}${language === void 0 ? "" : `language: ${language}\n`}`;
234
+ }
235
+ const WORKFLOW = `name: rules
236
+ on: pull_request
237
+
238
+ jobs:
239
+ rulebookguard:
240
+ runs-on: ubuntu-latest
241
+ steps:
242
+ - uses: actions/checkout@v7.0.1
243
+ with:
244
+ fetch-depth: 0
245
+ - uses: actions/setup-node@v6
246
+ with:
247
+ node-version: 22
248
+ - run: npx --yes rulebookguard@${version} lint --base origin/\${{ github.base_ref }}
249
+ `;
250
+ // src/mcp/server.ts
251
+ function createRulesServer(projectDir) {
252
+ const server = new McpServer({
253
+ name: "rulebookguard",
254
+ version
255
+ });
256
+ server.registerTool("rules_for", {
257
+ description: "Active business rules anchored to a file (id, context, summary, rule file). Call it before changing a file; the rules are the source of truth for its behavior.",
258
+ inputSchema: z.object({ path: z.string().describe("File path, absolute or relative to the project directory") }),
259
+ annotations: { readOnlyHint: true }
260
+ }, ({ path }) => cli(projectDir, [
261
+ "for",
262
+ path,
263
+ "--json"
264
+ ]));
265
+ server.registerTool("rules_show", {
266
+ description: "Full text of one business rule: summary, anchors, entities, supersedes and the rule body with its examples.",
267
+ inputSchema: z.object({ id: z.string().describe("Rule id, for example R-CLI-001") }),
268
+ annotations: { readOnlyHint: true }
269
+ }, ({ id }) => cli(projectDir, [
270
+ "show",
271
+ id,
272
+ "--json"
273
+ ]));
274
+ return server;
275
+ }
276
+ async function cli(cwd, args) {
277
+ let stdout = "";
278
+ let stderr = "";
279
+ if (await runCli(args, {
280
+ cwd,
281
+ stdout: (text) => {
282
+ stdout += text;
283
+ },
284
+ stderr: (text) => {
285
+ stderr += text;
286
+ }
287
+ }) === 0) return { content: [{
288
+ type: "text",
289
+ text: stdout
290
+ }] };
291
+ return {
292
+ content: [{
293
+ type: "text",
294
+ text: stderr.trim()
295
+ }],
296
+ isError: true
297
+ };
298
+ }
299
+ // src/commands/mcp.ts
300
+ async function mcpCommand(io, project) {
301
+ await createRulesServer(resolve(io.cwd, project ?? ".")).connect(new StdioServerTransport());
302
+ }
246
303
  // src/commands/read.ts
247
304
  const RULE_STATUSES = [
248
305
  "proposed",
@@ -377,6 +434,11 @@ function compare(a, b) {
377
434
  const USAGE = `Usage: rulebookguard <command> [options]
378
435
 
379
436
  Commands:
437
+ init <context> [--prefix <P>] [--language <en|es>] [--ci]
438
+ create rulebookguard.config.yaml and a first context in
439
+ the current directory; --language: language of generated
440
+ text (default: en), --ci: also a GitHub Actions workflow
441
+ that runs lint --base on pull requests
380
442
  contexts [--json] L0: list the contexts
381
443
  list [--context <c>] [--status <s>] [--json] L1: id and summary of each rule
382
444
  show <id> [--json] L2: the full rule
@@ -407,6 +469,15 @@ const SITE_OPTIONS = {
407
469
  base: { type: "string" }
408
470
  };
409
471
  const COMMANDS = {
472
+ init: {
473
+ usage: "rulebookguard init <context> [--prefix <P>] [--language <en|es>] [--ci]",
474
+ argument: "context",
475
+ options: {
476
+ prefix: { type: "string" },
477
+ language: { type: "string" },
478
+ ci: { type: "boolean" }
479
+ }
480
+ },
410
481
  contexts: {
411
482
  usage: "rulebookguard contexts [--json]",
412
483
  options: JSON_OPTION
@@ -513,6 +584,14 @@ async function dispatch(argv, io) {
513
584
  });
514
585
  return 0;
515
586
  }
587
+ if (name === "init") {
588
+ await initCommand(io, argument, {
589
+ prefix: values.prefix,
590
+ language: initLanguage(values.language, spec.usage),
591
+ ci: values.ci === true
592
+ });
593
+ return 0;
594
+ }
516
595
  if (name === "lint") return lintCommand(io, {
517
596
  base: values.base,
518
597
  json
@@ -542,6 +621,10 @@ async function dispatch(argv, io) {
542
621
  }
543
622
  return 0;
544
623
  }
624
+ function initLanguage(value, usage) {
625
+ if (value === void 0 || value === "en" || value === "es") return value;
626
+ throw new CliError(`--language must be en or es.\nUsage: ${usage}`, 2);
627
+ }
545
628
  function sitePort(value, usage) {
546
629
  if (value === void 0) return;
547
630
  const port = Number(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulebookguard",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Keep business rules as the single source of truth, bound to code and tests.",
5
5
  "homepage": "https://github.com/JuanCJR/rulebookguard#readme",
6
6
  "bugs": {
@@ -36,14 +36,14 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@modelcontextprotocol/client": "2.3.1",
39
- "@rulebookguard/site": "0.7.0",
39
+ "@rulebookguard/site": "0.8.0",
40
40
  "@types/node": "^20.19.43",
41
41
  "@types/picomatch": "^4.0.3",
42
42
  "typescript": "^7.0.2",
43
43
  "vite-plus": "^1.4.0"
44
44
  },
45
45
  "peerDependencies": {
46
- "@rulebookguard/site": "^0.7.0"
46
+ "@rulebookguard/site": "^0.8.0"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "@rulebookguard/site": {