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.
- package/README.md +37 -18
- package/dist/cli.mjs +142 -59
- 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.
|
|
56
|
-
2. Scaffold a rule: `npx rulebookguard new clients` creates
|
|
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
|
|
217
|
-
|
|
|
218
|
-
| [`
|
|
219
|
-
| [`
|
|
220
|
-
| [`
|
|
221
|
-
| [`
|
|
222
|
-
| [`
|
|
223
|
-
| [`
|
|
224
|
-
| [`
|
|
225
|
-
| [`
|
|
226
|
-
| [`
|
|
227
|
-
| [`
|
|
228
|
-
| [`site:
|
|
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
|
|
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
|
-
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
46
|
+
"@rulebookguard/site": "^0.8.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
49
49
|
"@rulebookguard/site": {
|