rulebookguard 0.12.0 → 0.13.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 -13
- package/dist/{anchors-JxhjEzRN.mjs → anchors-BGiDD0zY.mjs} +1 -1
- package/dist/cli.mjs +70 -10
- package/dist/index.d.mts +12 -0
- package/dist/index.mjs +2 -2
- package/dist/{lint-HEXtuztv.mjs → lint-BdC2U8K3.mjs} +239 -5
- package/dist/{site-COuF4ib4.mjs → site-hSQxz6_e.mjs} +13 -17
- package/package.json +8 -3
- package/plugin/agents/rule-miner.md +17 -15
- package/plugin/plugin.json +1 -1
- package/plugin/skills/rule-writing/SKILL.md +1 -1
- package/plugin/skills/rules/SKILL.md +1 -1
- package/npm-shrinkwrap.json +0 -148
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Requires Node.js 22 or later.
|
|
|
43
43
|
rulebookguard does not need to be a dependency of your project. Nothing in your code imports it, so you can run it with `npx`, pinning the version so that every machine and CI run the same one:
|
|
44
44
|
|
|
45
45
|
```sh
|
|
46
|
-
npx rulebookguard@0.
|
|
46
|
+
npx rulebookguard@0.13.0 lint
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Each release locks its own dependencies for npm and `npx`, so a pinned version keeps running the same code. Other package managers, such as `pnpm dlx`, ignore that lock and resolve the dependency ranges. Your `package.json` and lockfile stay untouched.
|
|
@@ -56,10 +56,10 @@ npm install --save-dev rulebookguard
|
|
|
56
56
|
|
|
57
57
|
A first rule in four steps:
|
|
58
58
|
|
|
59
|
-
1. Set up the rulebook: `npx rulebookguard@0.
|
|
60
|
-
2. Scaffold a rule: `npx rulebookguard@0.
|
|
59
|
+
1. Set up the rulebook: `npx rulebookguard@0.13.0 init clients --ci` writes `rulebookguard.config.yaml`, the `clients` context and a CI workflow (see [Configuration](#configuration) to adjust `testGlobs`).
|
|
60
|
+
2. Scaffold a rule: `npx rulebookguard@0.13.0 new clients` creates `docs/rules/clients/R-CLI-001.md` as `proposed`.
|
|
61
61
|
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`.
|
|
62
|
-
4. Run `npx rulebookguard@0.
|
|
62
|
+
4. Run `npx rulebookguard@0.13.0 lint`, and add it to CI with the [CI recipe](#ci-recipe).
|
|
63
63
|
|
|
64
64
|
## Configuration
|
|
65
65
|
|
|
@@ -164,6 +164,29 @@ Scenario: Duplicate document in the same country
|
|
|
164
164
|
|
|
165
165
|
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.
|
|
166
166
|
|
|
167
|
+
### Evidence
|
|
168
|
+
|
|
169
|
+
`evidence` links a rule to the code and tests where it shows, without touching them. Each entry is a path, optionally followed by what to look for in that file:
|
|
170
|
+
|
|
171
|
+
| entry | resolves to |
|
|
172
|
+
| ------------------------------------------------------------ | ------------------------------------------------- |
|
|
173
|
+
| `src/clients/create.ts validateDocument` | the definition of `validateDocument` |
|
|
174
|
+
| `src/clients/service.ts Clients.create` | `create` inside `Clients` (`::` and `#` work too) |
|
|
175
|
+
| `test/clients/create.test.ts "rejects a duplicate document"` | the test with that name |
|
|
176
|
+
| `src/clients/create.ts` | the file |
|
|
177
|
+
|
|
178
|
+
Lines are found when the rule is read, so the link follows the code as it moves. Built in, a heuristic finds the line where a symbol is defined (function, method, class, constant, assignment or key, in most languages) or the line holding a quoted string. With the optional [`@rulebookguard/symbols`](#exact-ranges-with-rulebookguardsymbols) package installed, files in the languages it knows resolve to the full range of the definition or of the test. `lint` warns with `dead-evidence` when the file is gone or the symbol is not found, so a rename shows up instead of leaving a stale link. Deprecated rules are not checked. An older `path:line` or `path:line-endLine` entry still works, and only its file is checked.
|
|
179
|
+
|
|
180
|
+
#### Exact ranges with `@rulebookguard/symbols`
|
|
181
|
+
|
|
182
|
+
The heuristic gives one line. For the full range of a function, class, method or test, install the optional package, which reads the code with [tree-sitter](https://tree-sitter.github.io/):
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
npm install --save-dev rulebookguard @rulebookguard/symbols
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
It knows JavaScript, TypeScript (and TSX), Python, Go, Java, C# and Rust; files in other languages keep using the heuristic, and so does any file it fails to read. It is a separate package because its grammars add about 11 MB; they ship inside it, so installing it builds nothing. Without it, nothing fails: lines come from the heuristic.
|
|
189
|
+
|
|
167
190
|
### Linking tests with `@rule` … `@endrule`
|
|
168
191
|
|
|
169
192
|
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.
|
|
@@ -308,7 +331,7 @@ file: docs/rules/clients/R-CLI-001.md
|
|
|
308
331
|
...
|
|
309
332
|
```
|
|
310
333
|
|
|
311
|
-
`supersedes`, `dependsOn`, `conflictsWith`, `origin` and `evidence` lines appear only when the rule sets them; `--json` always has `supersedes` and `origin` (or `null`), and `dependsOn`, `conflictsWith` and `evidence` (or `[]`). An `implementations:` section lists the [`@implements` regions](#marking-the-code-that-enforces-a-rule-with-implements--endimplements) of the rule as `file:line-endLine`, read from the files its anchors match; `--json` has them as `implementations: [{ file, line, endLine }]` (or `[]`). Exits 1 for an unknown id.
|
|
334
|
+
`supersedes`, `dependsOn`, `conflictsWith`, `origin` and `evidence` lines appear only when the rule sets them; `--json` always has `supersedes` and `origin` (or `null`), and `dependsOn`, `conflictsWith` and `evidence` (or `[]`). Each [evidence](#evidence) entry with a symbol is followed by the lines it resolves to, as `(lines a-b)`; `--json` keeps `evidence` as written and adds `evidenceLinks: [{ entry, file, line, endLine }]`, with `null` lines for an entry that does not resolve (or `[]`). An `implementations:` section lists the [`@implements` regions](#marking-the-code-that-enforces-a-rule-with-implements--endimplements) of the rule as `file:line-endLine`, read from the files its anchors match; `--json` has them as `implementations: [{ file, line, endLine }]` (or `[]`). Exits 1 for an unknown id.
|
|
312
335
|
|
|
313
336
|
### `search <text> [--context <c>] [--json]`
|
|
314
337
|
|
|
@@ -330,7 +353,7 @@ $ rulebookguard for src/clients/create.ts
|
|
|
330
353
|
R-CLI-001 (clients): A client document number is unique per country. (lines 12-30)
|
|
331
354
|
```
|
|
332
355
|
|
|
333
|
-
When the file holds `@implements` regions of a rule, its line ends with
|
|
356
|
+
When the file holds `@implements` regions of a rule, or lines its [evidence](#evidence) resolves to, its line ends with those line ranges; `--json` rows carry them apart, as `regions: [{ line, endLine }]` and `evidence: [{ entry, line, endLine }]` (each `[]` when there are none). Exits 1 when the path is outside the rulebook root.
|
|
334
357
|
|
|
335
358
|
### `for --changed <ref>`
|
|
336
359
|
|
|
@@ -413,6 +436,7 @@ Checks the whole catalog. Each finding is printed as `<severity> <code> <file>[:
|
|
|
413
436
|
| error | `dependency-cycle` | `dependsOn` links form a cycle (reported once, at its lowest id) |
|
|
414
437
|
| error | `active-rule-conflict` | two active rules where one lists the other in `conflictsWith` |
|
|
415
438
|
| warning | `context-over-budget` | a context holds more rules than `contextBudget` |
|
|
439
|
+
| warning | `dead-evidence` | an `evidence` entry names a file or a symbol that no longer exists |
|
|
416
440
|
|
|
417
441
|
```sh
|
|
418
442
|
$ rulebookguard lint
|
|
@@ -504,7 +528,7 @@ $ echo '{"hook_event_name":"PostToolUse","tool_name":"Read","tool_input":{"file_
|
|
|
504
528
|
|
|
505
529
|
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).
|
|
506
530
|
|
|
507
|
-
Each rule comes with its id, context, summary, rule file, the lines of its `@implements` regions in that file (when there are any) 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.
|
|
531
|
+
Each rule comes with its id, context, summary, rule file, the lines of its `@implements` regions and of its evidence in that file (when there are any) 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.
|
|
508
532
|
|
|
509
533
|
> [!NOTE]
|
|
510
534
|
> 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.
|
|
@@ -533,7 +557,7 @@ The rulebook is found from `--project` (default: the current directory). A comma
|
|
|
533
557
|
> npm install --save-dev rulebookguard @rulebookguard/site
|
|
534
558
|
> ```
|
|
535
559
|
|
|
536
|
-
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@0.
|
|
560
|
+
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@0.13.0 -p @rulebookguard/site@0.13.0 rulebookguard site:run`.
|
|
537
561
|
|
|
538
562
|
Builds a read-only static site of the rulebook with [Astro Starlight](https://starlight.astro.build) into `--out` (default `site`, relative to the current directory) and prints that path. Open it from disk or publish it as is, for example on GitHub Pages; pass `--base` when it is served from a sub-path (`--base /rules`).
|
|
539
563
|
|
|
@@ -559,7 +583,7 @@ The site has:
|
|
|
559
583
|
- **Home:** the contexts map, with how many rules of each status every context holds.
|
|
560
584
|
- **A page per context** with its description and rules.
|
|
561
585
|
- **All rules:** one table filterable by text, context and status.
|
|
562
|
-
- **A page per rule:** summary, status, entities, rule file, the supersedes chain both ways, the origin (an `extracted` rule still `proposed` is flagged as not validated by the business yet) and evidence, the rule text, its anchors and the files they match, the tests tagging it, and the git history of the rule file.
|
|
586
|
+
- **A page per rule:** summary, status, entities, rule file, the supersedes chain both ways, the origin (an `extracted` rule still `proposed` is flagged as not validated by the business yet) and evidence, each entry with the `file:line-endLine` it resolves to, the rule text, its anchors and the files they match, the tests tagging it, and the git history of the rule file.
|
|
563
587
|
- **Health:** the `lint` findings when the site was built.
|
|
564
588
|
- Full-text search (Pagefind).
|
|
565
589
|
|
|
@@ -586,7 +610,7 @@ jobs:
|
|
|
586
610
|
- uses: actions/setup-node@v6
|
|
587
611
|
with:
|
|
588
612
|
node-version: 22
|
|
589
|
-
- run: npx --yes rulebookguard@0.
|
|
613
|
+
- run: npx --yes rulebookguard@0.13.0 lint --base origin/${{ github.base_ref }}
|
|
590
614
|
```
|
|
591
615
|
|
|
592
616
|
> [!TIP]
|
|
@@ -598,9 +622,9 @@ The plugin in [`plugin/`](plugin) connects the rulebook to Claude Code:
|
|
|
598
622
|
|
|
599
623
|
- **Hooks:** after every `Read` and before every `Write`, it runs `rulebookguard hook`, so Claude gets the active rules anchored to a file together with its contents, before it writes a change. A `Write` of a new file, which needs no `Read`, gets them right after the file is created.
|
|
600
624
|
- **MCP server:** the tools of `rulebookguard mcp` (`rules_for`, `rules_show`, `contexts`, `rules_list`, `rules_search`, `lint`), for rules of files Claude has not opened yet and for working on the rulebook itself.
|
|
601
|
-
- **Skill** `rulebookguard:rules`: the protocol, that is, follow the rules, and change a rule (then its tests, then the code,
|
|
625
|
+
- **Skill** `rulebookguard:rules`: the protocol, that is, follow the rules, and change a rule (then its tests, then the code, inside its `@implements` regions when it has any; new regions only on request) only when the user asks for the behavior change.
|
|
602
626
|
- **Skill** `rulebookguard:rule-writing`: the shared writing guide, so a rule reads the same whoever writes it: one verifiable behavior, a one-line summary in business language, Gherkin examples, and `origin` and `evidence` for rules lifted from code.
|
|
603
|
-
- **Agent** `rulebookguard:rule-miner`: lifts the business rules existing code already enforces, one context at a time. It writes them as `proposed` rules with `origin: extracted`, evidence as `path symbol` and questions for the business, through `rulebookguard new
|
|
627
|
+
- **Agent** `rulebookguard:rule-miner`: lifts the business rules existing code already enforces, one context at a time. It writes them as `proposed` rules with `origin: extracted`, evidence as `path symbol` and questions for the business, through `rulebookguard new`. It traces every flow of the context from its entry points down to persistence before writing, and its report starts with the flows it traced. It does not edit production code: it suggests the lines that enforce each rule, and adds `@implements` … `@endimplements` markers only when asked. It checks existing rules first to avoid duplicates, stays within `contextBudget`, wraps tests that already verify a rule in `@rule` … `@endrule`, and finishes only when `lint` has no errors of its own. It never activates a rule or edits existing rules, and touches code only to add marker comments when asked: a person reviews the diff and activates what holds. Ask for it by name, for example "use rule-miner on the clients context".
|
|
604
628
|
- **Agent** `rulebookguard:rule-author`: designs the rules a feature needs before its code. By default it writes nothing: it reads the rulebook by levels and returns a rule change plan with new rules and their Gherkin, changed rules as diffs, deprecations, conflicts with active rules, test skeletons and open questions, then stops for a person to approve. Called again with the approved plan, it writes the new rules as `proposed` through `rulebookguard new`, applies the approved diffs and finishes when `lint` has no errors of its own. Tests and code come after, as the `rules` skill says. Ask for it by name, for example "use rule-author for this feature: …".
|
|
605
629
|
|
|
606
630
|
Install it from this repository's marketplace:
|
|
@@ -624,7 +648,7 @@ Kiro gets the same rules through three pieces:
|
|
|
624
648
|
| Power in [`plugin/`](plugin) | the `rules` and `rule-writing` skills and the MCP server | import the `plugin` folder |
|
|
625
649
|
| [Custom agents](#export-kiro-agents---out-dir) | `rule-author` and `rule-miner` | `rulebookguard export kiro-agents` |
|
|
626
650
|
|
|
627
|
-
To install the power, open the Powers panel in Kiro, choose **Add Custom Power**, then **Import power from a folder** with a clone of this repository's `plugin` folder. Its MCP server runs `npx --yes rulebookguard@0.
|
|
651
|
+
To install the power, open the Powers panel in Kiro, choose **Add Custom Power**, then **Import power from a folder** with a clone of this repository's `plugin` folder. Its MCP server runs `npx --yes rulebookguard@0.13.0 mcp` from the directory Kiro starts it in, and needs Node.js and network access the first time.
|
|
628
652
|
|
|
629
653
|
Keep `rulebookguard.config.yaml` at the root of the Kiro workspace: Kiro reads `.kiro/` from there, and the steering patterns are relative to the rulebook root. There is no hook for Kiro, because steering files already deliver the rules of a matching file.
|
|
630
654
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { t as rulesForPath } from "./anchors-
|
|
2
|
+
import { C as expandGlobs, F as CONTEXT_FILE_NAME, H as messages, M as isRuleId, R as splitFrontmatter, T as toRepoPath, U as FormatError, W as formatIssue, b as loadCatalog, c as gitFallbackWarning, d as writeJson, f as writeLines, g as scanImplementations, h as parseEvidence, i as lintCommand, l as openCatalog, o as CliError, p as createEvidenceResolver, s as findRule, t as changedFiles, u as shownPath, v as scanTestTags, y as findRulebookRoot, z as CONFIG_FILE_NAME } from "./lint-BdC2U8K3.mjs";
|
|
3
|
+
import { t as rulesForPath } from "./anchors-BGiDD0zY.mjs";
|
|
4
4
|
import { access, mkdir, readFile, 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";
|
|
@@ -358,6 +358,7 @@ async function showCommand(catalog, io, id, json) {
|
|
|
358
358
|
const rule = findRule(catalog, id);
|
|
359
359
|
const files = await expandGlobs(catalog.root, rule.anchors, gitFallbackWarning(io, catalog.config.language));
|
|
360
360
|
const implementations = (await scanImplementations(catalog.root, files)).regions.get(rule.id) ?? [];
|
|
361
|
+
const links = await resolveEvidence(catalog, rule.evidence ?? []);
|
|
361
362
|
if (json) {
|
|
362
363
|
writeJson(io, { rule: {
|
|
363
364
|
...toSummary(rule),
|
|
@@ -368,13 +369,20 @@ async function showCommand(catalog, io, id, json) {
|
|
|
368
369
|
conflictsWith: rule.conflictsWith ?? [],
|
|
369
370
|
origin: rule.origin ?? null,
|
|
370
371
|
evidence: rule.evidence ?? [],
|
|
372
|
+
evidenceLinks: links.map(({ entry, file, line, endLine }) => ({
|
|
373
|
+
entry,
|
|
374
|
+
file,
|
|
375
|
+
line,
|
|
376
|
+
endLine
|
|
377
|
+
})),
|
|
371
378
|
implementations,
|
|
372
379
|
file: rule.file,
|
|
373
380
|
body: rule.body
|
|
374
381
|
} });
|
|
375
382
|
return;
|
|
376
383
|
}
|
|
377
|
-
const
|
|
384
|
+
const text = messages(catalog.config.language);
|
|
385
|
+
const none = text.none;
|
|
378
386
|
const header = [
|
|
379
387
|
`${rule.id}: ${rule.summary}`,
|
|
380
388
|
`context: ${rule.context}`,
|
|
@@ -386,7 +394,7 @@ async function showCommand(catalog, io, id, json) {
|
|
|
386
394
|
...rule.dependsOn?.length ? [`dependsOn: ${rule.dependsOn.join(", ")}`] : [],
|
|
387
395
|
...rule.conflictsWith?.length ? [`conflictsWith: ${rule.conflictsWith.join(", ")}`] : [],
|
|
388
396
|
...rule.origin === void 0 ? [] : [`origin: ${rule.origin}`],
|
|
389
|
-
...
|
|
397
|
+
...links.length ? ["evidence:", ...indent(links.map((link) => withLines(link, text.lines)), none)] : [],
|
|
390
398
|
...implementations.length ? ["implementations:", ...indent(implementations.map((region) => `${region.file}:${region.line}-${region.endLine}`), none)] : [],
|
|
391
399
|
`file: ${rule.file}`
|
|
392
400
|
];
|
|
@@ -396,10 +404,12 @@ async function forCommand(catalog, io, input, json) {
|
|
|
396
404
|
const text = messages(catalog.config.language);
|
|
397
405
|
const path = toRootPath(catalog, resolve(io.cwd, input), input);
|
|
398
406
|
const regions = await regionsIn(catalog.root, path);
|
|
407
|
+
const evidence = await evidenceIn(catalog, path);
|
|
399
408
|
const rules = rulesForPath(catalog.rules, path).filter((rule) => rule.status === "active").map((rule) => ({
|
|
400
409
|
...toSummary(rule),
|
|
401
410
|
file: rule.file,
|
|
402
|
-
regions: regions(rule.id)
|
|
411
|
+
regions: regions(rule.id),
|
|
412
|
+
evidence: evidence(rule.id)
|
|
403
413
|
}));
|
|
404
414
|
if (json) {
|
|
405
415
|
writeJson(io, {
|
|
@@ -412,7 +422,7 @@ async function forCommand(catalog, io, input, json) {
|
|
|
412
422
|
writeLines(io, [text.noActiveRulesFor(path)]);
|
|
413
423
|
return;
|
|
414
424
|
}
|
|
415
|
-
writeLines(io, rules.map((rule) => `${rule.id} (${rule.context}): ${rule.summary}${rule.regions.
|
|
425
|
+
writeLines(io, rules.map((rule) => `${rule.id} (${rule.context}): ${rule.summary}${linesNote(mergeSpans(rule.regions, rule.evidence), text.lines)}`));
|
|
416
426
|
}
|
|
417
427
|
async function forChangedCommand(catalog, io, base, json) {
|
|
418
428
|
const text = messages(catalog.config.language);
|
|
@@ -448,6 +458,54 @@ async function regionsIn(root, path) {
|
|
|
448
458
|
endLine
|
|
449
459
|
}));
|
|
450
460
|
}
|
|
461
|
+
async function resolveEvidence(catalog, entries) {
|
|
462
|
+
if (entries.length === 0) return [];
|
|
463
|
+
const resolveEntry = await createEvidenceResolver(catalog.root);
|
|
464
|
+
return Promise.all(entries.map(resolveEntry));
|
|
465
|
+
}
|
|
466
|
+
/** The resolved evidence lines each rule has in `path`, by rule id. */
|
|
467
|
+
async function evidenceIn(catalog, path) {
|
|
468
|
+
const found = /* @__PURE__ */ new Map();
|
|
469
|
+
const inPath = catalog.rules.map((rule) => ({
|
|
470
|
+
id: rule.id,
|
|
471
|
+
entries: (rule.evidence ?? []).filter((entry) => parseEvidence(entry).file === path)
|
|
472
|
+
})).filter(({ entries }) => entries.length > 0);
|
|
473
|
+
if (inPath.length === 0) return () => [];
|
|
474
|
+
const resolveEntry = await createEvidenceResolver(catalog.root);
|
|
475
|
+
for (const { id, entries } of inPath) {
|
|
476
|
+
const spans = [];
|
|
477
|
+
for (const { entry, line, endLine } of await Promise.all(entries.map(resolveEntry))) if (line !== null && endLine !== null) spans.push({
|
|
478
|
+
entry,
|
|
479
|
+
line,
|
|
480
|
+
endLine
|
|
481
|
+
});
|
|
482
|
+
if (spans.length > 0) found.set(id, spans);
|
|
483
|
+
}
|
|
484
|
+
return (id) => found.get(id) ?? [];
|
|
485
|
+
}
|
|
486
|
+
/** Region and evidence lines together, in order and without repeats. */
|
|
487
|
+
function mergeSpans(...groups) {
|
|
488
|
+
const seen = /* @__PURE__ */ new Set();
|
|
489
|
+
return groups.flat().map(({ line, endLine }) => ({
|
|
490
|
+
line,
|
|
491
|
+
endLine
|
|
492
|
+
})).sort((a, b) => a.line - b.line || a.endLine - b.endLine).filter(({ line, endLine }) => {
|
|
493
|
+
const key = `${line}-${endLine}`;
|
|
494
|
+
if (seen.has(key)) return false;
|
|
495
|
+
seen.add(key);
|
|
496
|
+
return true;
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
function linesNote(spans, lines) {
|
|
500
|
+
return spans.length === 0 ? "" : ` (${lines(formatSpans(spans))})`;
|
|
501
|
+
}
|
|
502
|
+
function withLines(link, lines) {
|
|
503
|
+
if (link.line === null || link.endLine === null || parseEvidence(link.entry).lines !== void 0) return link.entry;
|
|
504
|
+
return `${link.entry}${linesNote([{
|
|
505
|
+
line: link.line,
|
|
506
|
+
endLine: link.endLine
|
|
507
|
+
}], lines)}`;
|
|
508
|
+
}
|
|
451
509
|
function formatSpans(spans) {
|
|
452
510
|
return spans.map(({ line, endLine }) => `${line}-${endLine}`).join(", ");
|
|
453
511
|
}
|
|
@@ -532,7 +590,9 @@ async function hookContext(event, cwd) {
|
|
|
532
590
|
const repoPath = path.split(sep).join("/");
|
|
533
591
|
const rules = rulesForPath(catalog.rules, repoPath).filter((rule) => rule.status === "active");
|
|
534
592
|
if (rules.length === 0) return;
|
|
535
|
-
|
|
593
|
+
const regions = await regionsIn(root, repoPath).catch(() => () => []);
|
|
594
|
+
const evidence = await evidenceIn(catalog, repoPath).catch(() => () => []);
|
|
595
|
+
return render(repoPath, rules, (id) => mergeSpans(regions(id), evidence(id)), TEXT[catalog.config.language ?? "en"]);
|
|
536
596
|
}
|
|
537
597
|
function parseEvent(input) {
|
|
538
598
|
let event;
|
|
@@ -749,7 +809,7 @@ function createRulesServer(projectDir) {
|
|
|
749
809
|
version
|
|
750
810
|
});
|
|
751
811
|
server.registerTool("rules_for", {
|
|
752
|
-
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.",
|
|
812
|
+
description: "Active business rules anchored to a file (id, context, summary, rule file, and the lines of the file each rule names through @implements regions or evidence). Call it before changing a file; the rules are the source of truth for its behavior.",
|
|
753
813
|
inputSchema: z.object({ path: z.string().describe("File path, absolute or relative to the project directory") }),
|
|
754
814
|
annotations: { readOnlyHint: true }
|
|
755
815
|
}, ({ path }) => cli(projectDir, [
|
|
@@ -758,7 +818,7 @@ function createRulesServer(projectDir) {
|
|
|
758
818
|
"--json"
|
|
759
819
|
]));
|
|
760
820
|
server.registerTool("rules_show", {
|
|
761
|
-
description: "Full text of one business rule: summary, anchors, entities, supersedes and the rule body with its examples.",
|
|
821
|
+
description: "Full text of one business rule: summary, anchors, entities, supersedes, evidence with the lines each entry resolves to, and the rule body with its examples.",
|
|
762
822
|
inputSchema: z.object({ id: z.string().describe("Rule id, for example R-CLI-001") }),
|
|
763
823
|
annotations: { readOnlyHint: true }
|
|
764
824
|
}, ({ id }) => cli(projectDir, [
|
|
@@ -1017,7 +1077,7 @@ async function dispatch(argv, io) {
|
|
|
1017
1077
|
return 0;
|
|
1018
1078
|
}
|
|
1019
1079
|
if (name === "site" || name === "site:generate" || name === "site:run") {
|
|
1020
|
-
const { siteCommand } = await import("./site-
|
|
1080
|
+
const { siteCommand } = await import("./site-hSQxz6_e.mjs");
|
|
1021
1081
|
await siteCommand(io, {
|
|
1022
1082
|
out: values.out,
|
|
1023
1083
|
base: values.base,
|
package/dist/index.d.mts
CHANGED
|
@@ -167,6 +167,8 @@ interface ShowJson {
|
|
|
167
167
|
conflictsWith: string[];
|
|
168
168
|
origin: RuleOrigin | null;
|
|
169
169
|
evidence: string[];
|
|
170
|
+
/** Each evidence entry with the lines it resolves to, or null lines when it does not. */
|
|
171
|
+
evidenceLinks: EvidenceLinkJson[];
|
|
170
172
|
implementations: TagRegion[];
|
|
171
173
|
file: string;
|
|
172
174
|
body: string;
|
|
@@ -177,8 +179,18 @@ interface ForJson {
|
|
|
177
179
|
rules: (RuleSummaryJson & {
|
|
178
180
|
file: string;
|
|
179
181
|
regions: LineSpanJson[];
|
|
182
|
+
evidence: EvidenceSpanJson[];
|
|
180
183
|
})[];
|
|
181
184
|
}
|
|
185
|
+
interface EvidenceLinkJson {
|
|
186
|
+
entry: string;
|
|
187
|
+
file: string;
|
|
188
|
+
line: number | null;
|
|
189
|
+
endLine: number | null;
|
|
190
|
+
}
|
|
191
|
+
interface EvidenceSpanJson extends LineSpanJson {
|
|
192
|
+
entry: string;
|
|
193
|
+
}
|
|
182
194
|
interface ForChangedJson {
|
|
183
195
|
base: string;
|
|
184
196
|
files: string[];
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { t as rulesForPath } from "./anchors-
|
|
1
|
+
import { A as findRuleRegions, B as loadConfig, C as expandGlobs, D as IMPLEMENTS_MARKER_PATTERN, F as CONTEXT_FILE_NAME, I as loadContext, L as parseContext, M as isRuleId, N as loadRule, O as RULE_MARKER_PATTERN, P as parseRule, R as splitFrontmatter, S as createGlobMatcher, T as toRepoPath, U as FormatError, V as parseConfig, W as formatIssue, _ as scanTestRegions, a as pendingImplementation, b as loadCatalog, g as scanImplementations, j as findRuleTags, k as RULE_TAG_PATTERN, n as formatFinding, r as lintCatalog, v as scanTestTags, w as listRepoFiles, x as relativeToRoot, y as findRulebookRoot, z as CONFIG_FILE_NAME } from "./lint-BdC2U8K3.mjs";
|
|
2
|
+
import { t as rulesForPath } from "./anchors-BGiDD0zY.mjs";
|
|
3
3
|
export { CONFIG_FILE_NAME, CONTEXT_FILE_NAME, FormatError, IMPLEMENTS_MARKER_PATTERN, RULE_MARKER_PATTERN, RULE_TAG_PATTERN, createGlobMatcher, expandGlobs, findRuleRegions, findRuleTags, findRulebookRoot, formatFinding, formatIssue, isRuleId, lintCatalog, listRepoFiles, loadCatalog, loadConfig, loadContext, loadRule, parseConfig, parseContext, parseRule, pendingImplementation, relativeToRoot, rulesForPath, scanImplementations, scanTestRegions, scanTestTags, splitFrontmatter, toRepoPath };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { access, lstat, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
2
3
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
3
4
|
import { YAMLParseError, parse } from "yaml";
|
|
@@ -5,6 +6,7 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
5
6
|
import picomatch from "picomatch";
|
|
6
7
|
import { execFile } from "node:child_process";
|
|
7
8
|
import { promisify } from "node:util";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
10
|
// src/format/errors.ts
|
|
9
11
|
function formatIssue(issue) {
|
|
10
12
|
return issue.field === void 0 ? `${issue.file}: ${issue.message}` : `${issue.file}: field "${issue.field}" ${issue.message}`;
|
|
@@ -18,7 +20,7 @@ var FormatError = class extends Error {
|
|
|
18
20
|
}
|
|
19
21
|
};
|
|
20
22
|
// src/format/read.ts
|
|
21
|
-
async function readSource(path) {
|
|
23
|
+
async function readSource$1(path) {
|
|
22
24
|
try {
|
|
23
25
|
return await readFile(path, "utf8");
|
|
24
26
|
} catch (error) {
|
|
@@ -212,6 +214,8 @@ const MESSAGES = {
|
|
|
212
214
|
unmatchedEndimplementsTag: (id) => `@endimplements ${id} has no open @implements ${id} before it`,
|
|
213
215
|
implementsOutsideAnchors: (id) => `marks code as implementing ${id}, but no anchor of ${id} matches this file`,
|
|
214
216
|
contextOverBudget: (context, count, budget) => `context "${context}" has ${count} rules, more than the contextBudget of ${budget}`,
|
|
217
|
+
deadEvidenceFile: (id, entry, file) => `evidence "${entry}" of ${id}: ${file} does not exist`,
|
|
218
|
+
deadEvidenceSymbol: (id, entry, symbol, file) => `evidence "${entry}" of ${id}: "${symbol}" is not found in ${file}`,
|
|
215
219
|
pendingImplementation: (id, base) => `rule ${id} changed since ${base} but none of the tests tagging it changed`,
|
|
216
220
|
unknownRuleReference: (field, id) => `${field} names ${id}, which does not exist`,
|
|
217
221
|
selfRuleReference: (field) => `${field} names the rule itself`,
|
|
@@ -259,6 +263,7 @@ const MESSAGES = {
|
|
|
259
263
|
unmatchedEndimplementsTagHint: (id) => `add "@implements ${id}" before the code that enforces the rule, or remove the stray @endimplements`,
|
|
260
264
|
implementsOutsideAnchorsHint: (id) => `add an anchor to ${id} that matches this file, or move the region to a file it anchors`,
|
|
261
265
|
contextOverBudgetHint: "split the context into smaller ones, or raise contextBudget in rulebookguard.config.yaml",
|
|
266
|
+
deadEvidenceHint: "point the entry at the file and symbol that show the rule now (\"path symbol\"), or remove it",
|
|
262
267
|
pendingImplementationHint: (id) => `update or add a test inside an "@rule ${id}" … "@endrule ${id}" region that covers the changed rule`,
|
|
263
268
|
lintSummary: (errors, warnings) => `${errors} ${errors === 1 ? "error" : "errors"}, ${warnings} ${warnings === 1 ? "warning" : "warnings"}`,
|
|
264
269
|
needsWorkTree: (command, root) => `${command} needs a git work tree, but ${root} is not inside one.`,
|
|
@@ -321,6 +326,8 @@ const MESSAGES = {
|
|
|
321
326
|
unmatchedEndimplementsTag: (id) => `@endimplements ${id} no tiene un @implements ${id} abierto antes`,
|
|
322
327
|
implementsOutsideAnchors: (id) => `marca código como implementación de ${id}, pero ningún anchor de ${id} coincide con este archivo`,
|
|
323
328
|
contextOverBudget: (context, count, budget) => `el contexto "${context}" tiene ${count} reglas, más que el contextBudget de ${budget}`,
|
|
329
|
+
deadEvidenceFile: (id, entry, file) => `la evidencia "${entry}" de ${id}: ${file} no existe`,
|
|
330
|
+
deadEvidenceSymbol: (id, entry, symbol, file) => `la evidencia "${entry}" de ${id}: "${symbol}" no aparece en ${file}`,
|
|
324
331
|
pendingImplementation: (id, base) => `la regla ${id} cambió desde ${base} pero no cambió ninguno de los tests que la etiquetan`,
|
|
325
332
|
unknownRuleReference: (field, id) => `${field} nombra a ${id}, que no existe`,
|
|
326
333
|
selfRuleReference: (field) => `${field} nombra a la propia regla`,
|
|
@@ -368,6 +375,7 @@ const MESSAGES = {
|
|
|
368
375
|
unmatchedEndimplementsTagHint: (id) => `agrega "@implements ${id}" antes del código que cumple la regla, o quita el @endimplements que sobra`,
|
|
369
376
|
implementsOutsideAnchorsHint: (id) => `agrega a ${id} un anchor que coincida con este archivo, o mueve la región a un archivo que ancle`,
|
|
370
377
|
contextOverBudgetHint: "divide el contexto en otros más chicos o sube contextBudget en rulebookguard.config.yaml",
|
|
378
|
+
deadEvidenceHint: "apunta la entrada al archivo y al símbolo que muestran la regla hoy (\"ruta símbolo\"), o quítala",
|
|
371
379
|
pendingImplementationHint: (id) => `actualiza o agrega un test dentro de una región "@rule ${id}" … "@endrule ${id}" que cubra el cambio de la regla`,
|
|
372
380
|
lintSummary: (errors, warnings) => `${errors} ${errors === 1 ? "error" : "errores"}, ${warnings} ${warnings === 1 ? "aviso" : "avisos"}`,
|
|
373
381
|
needsWorkTree: (command, root) => `${command} necesita un work tree de git, pero ${root} no está en uno.`,
|
|
@@ -459,7 +467,7 @@ function parseConfig(source, file) {
|
|
|
459
467
|
return validate(validators.config, parseYaml(source, file), file);
|
|
460
468
|
}
|
|
461
469
|
async function loadConfig(path) {
|
|
462
|
-
return parseConfig(await readSource(path), path);
|
|
470
|
+
return parseConfig(await readSource$1(path), path);
|
|
463
471
|
}
|
|
464
472
|
// src/format/frontmatter.ts
|
|
465
473
|
const OPENING = /^---\r?\n/;
|
|
@@ -495,7 +503,7 @@ function parseContext(source, file, { language } = {}) {
|
|
|
495
503
|
};
|
|
496
504
|
}
|
|
497
505
|
async function loadContext(path, options) {
|
|
498
|
-
return parseContext(await readSource(path), path, options);
|
|
506
|
+
return parseContext(await readSource$1(path), path, options);
|
|
499
507
|
}
|
|
500
508
|
// src/format/rule.ts
|
|
501
509
|
const RULE_ID = /^R-[A-Z][A-Z0-9]*-[0-9]{3}$/;
|
|
@@ -511,7 +519,7 @@ function parseRule(source, file, { language } = {}) {
|
|
|
511
519
|
};
|
|
512
520
|
}
|
|
513
521
|
async function loadRule(path, options) {
|
|
514
|
-
return parseRule(await readSource(path), path, options);
|
|
522
|
+
return parseRule(await readSource$1(path), path, options);
|
|
515
523
|
}
|
|
516
524
|
// src/format/tag.ts
|
|
517
525
|
const RULE_TAG_PATTERN = /(?<![\w@.-])@rule\s+(R-[A-Z][A-Z0-9]*-[0-9]{3})(?![\w-])/g;
|
|
@@ -974,6 +982,213 @@ function push(index, id, value) {
|
|
|
974
982
|
function sortById(index) {
|
|
975
983
|
return new Map([...index].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
976
984
|
}
|
|
985
|
+
// src/format/evidence.ts
|
|
986
|
+
const LINE_SUFFIX = /^(.*):([0-9]+)(?:-([0-9]+))?$/;
|
|
987
|
+
const QUOTED = /^(["'`])(.+)\1$/;
|
|
988
|
+
function parseEvidence(entry) {
|
|
989
|
+
const trimmed = entry.trim();
|
|
990
|
+
const space = trimmed.search(/\s/);
|
|
991
|
+
const path = repoPath(space === -1 ? trimmed : trimmed.slice(0, space));
|
|
992
|
+
const rest = space === -1 ? "" : trimmed.slice(space).trim();
|
|
993
|
+
const lines = LINE_SUFFIX.exec(path);
|
|
994
|
+
if (lines !== null && rest === "") {
|
|
995
|
+
const line = Number(lines[2]);
|
|
996
|
+
return {
|
|
997
|
+
entry,
|
|
998
|
+
file: lines[1],
|
|
999
|
+
lines: {
|
|
1000
|
+
line,
|
|
1001
|
+
endLine: lines[3] === void 0 ? line : Number(lines[3])
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (rest === "") return {
|
|
1006
|
+
entry,
|
|
1007
|
+
file: path
|
|
1008
|
+
};
|
|
1009
|
+
const quoted = QUOTED.exec(rest);
|
|
1010
|
+
return {
|
|
1011
|
+
entry,
|
|
1012
|
+
file: path,
|
|
1013
|
+
symbol: quoted === null ? {
|
|
1014
|
+
kind: "name",
|
|
1015
|
+
parts: rest.split(/\.|::|#/).filter((part) => part !== "")
|
|
1016
|
+
} : {
|
|
1017
|
+
kind: "text",
|
|
1018
|
+
text: quoted[2]
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function repoPath(path) {
|
|
1023
|
+
return path.replaceAll("\\", "/").replace(/^(?:\.\/)+/, "");
|
|
1024
|
+
}
|
|
1025
|
+
const MODIFIERS = "(?:(?:public|private|protected|internal|static|async|override|abstract|virtual|sealed|final|export|default|get|set|readonly|unsafe|extern|synchronized|pub(?:\\([^)]*\\))?)\\s+)*";
|
|
1026
|
+
function escape(text) {
|
|
1027
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1028
|
+
}
|
|
1029
|
+
function definitionPatterns(name) {
|
|
1030
|
+
const n = escape(name);
|
|
1031
|
+
const end = "(?![\\w$])";
|
|
1032
|
+
return [
|
|
1033
|
+
new RegExp(`(?:^|[^\\w$.])(?:function\\*?|def|fn|func|class|interface|type|enum|struct|trait|impl|module|namespace|record|object|const|let|var|val|sub|macro|alias)\\s+${n}${end}`),
|
|
1034
|
+
new RegExp(`\\bfunc\\s*\\([^)]*\\)\\s*${n}${end}`),
|
|
1035
|
+
new RegExp(`^\\s*${MODIFIERS}${n}\\s*(?::[^=]*)?=(?![=>])`),
|
|
1036
|
+
new RegExp(`["']${n}["']\\s*:`),
|
|
1037
|
+
new RegExp(`^\\s*${n}\\s*:`),
|
|
1038
|
+
new RegExp(`^\\s*${MODIFIERS}(?:[\\w$<>\\[\\],.?*&]+\\s+)?${n}\\s*(?:<[^>]*>)?\\s*\\([^;]*$`)
|
|
1039
|
+
];
|
|
1040
|
+
}
|
|
1041
|
+
/** The 1-based line defining `name`, searching after line `after`; comment lines never count. */
|
|
1042
|
+
function findDefinitionLine(lines, name, after = 0) {
|
|
1043
|
+
for (const pattern of definitionPatterns(name)) for (let index = after; index < lines.length; index += 1) {
|
|
1044
|
+
const text = lines[index];
|
|
1045
|
+
if (!COMMENT_LINE.test(text) && pattern.test(text)) return index + 1;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
/** The 1-based line holding `text` as a whole quoted string, such as a test name. */
|
|
1049
|
+
function findTextLine(lines, text) {
|
|
1050
|
+
const pattern = new RegExp(`(["'\`])${escape(text)}\\1`);
|
|
1051
|
+
const index = lines.findIndex((line) => pattern.test(line));
|
|
1052
|
+
return index === -1 ? void 0 : index + 1;
|
|
1053
|
+
}
|
|
1054
|
+
// src/optional-package.ts
|
|
1055
|
+
/** Imports `name` from the first of `from` that resolves it, or returns undefined when none does. */
|
|
1056
|
+
async function importOptionalPackage(name, from) {
|
|
1057
|
+
for (const directory of from) {
|
|
1058
|
+
let entry;
|
|
1059
|
+
try {
|
|
1060
|
+
entry = createRequire(join(directory, "package.json")).resolve(name);
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
if (error.code === "MODULE_NOT_FOUND") continue;
|
|
1063
|
+
throw error;
|
|
1064
|
+
}
|
|
1065
|
+
return await import(pathToFileURL(entry).href);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
// src/catalog/evidence.ts
|
|
1069
|
+
const SYMBOLS_PACKAGE = "@rulebookguard/symbols";
|
|
1070
|
+
/**
|
|
1071
|
+
* Resolves evidence entries to the lines they name: exact ranges through `@rulebookguard/symbols`
|
|
1072
|
+
* when it is installed and knows the language, otherwise the definition line a regex finds.
|
|
1073
|
+
*/
|
|
1074
|
+
async function createEvidenceResolver(root, loadSymbols = () => importOptionalPackage(SYMBOLS_PACKAGE, [root, dirname(fileURLToPath(import.meta.url))])) {
|
|
1075
|
+
const realRoot = await realpath(root);
|
|
1076
|
+
const symbols = await loadSymbols().catch(() => void 0);
|
|
1077
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
1078
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1079
|
+
const kindOf = (file) => {
|
|
1080
|
+
let kind = kinds.get(file);
|
|
1081
|
+
if (kind === void 0) {
|
|
1082
|
+
kind = pathKind(join(root, file), realRoot);
|
|
1083
|
+
kinds.set(file, kind);
|
|
1084
|
+
}
|
|
1085
|
+
return kind;
|
|
1086
|
+
};
|
|
1087
|
+
const sourceOf = (file) => {
|
|
1088
|
+
let source = sources.get(file);
|
|
1089
|
+
if (source === void 0) {
|
|
1090
|
+
source = readSource(file, join(root, file), symbols);
|
|
1091
|
+
sources.set(file, source);
|
|
1092
|
+
}
|
|
1093
|
+
return source;
|
|
1094
|
+
};
|
|
1095
|
+
return async (entry) => {
|
|
1096
|
+
const parsed = parseEvidence(entry);
|
|
1097
|
+
const symbol = symbolText(parsed);
|
|
1098
|
+
const link = {
|
|
1099
|
+
entry,
|
|
1100
|
+
file: parsed.file,
|
|
1101
|
+
...symbol === void 0 ? {} : { symbol },
|
|
1102
|
+
line: null,
|
|
1103
|
+
endLine: null
|
|
1104
|
+
};
|
|
1105
|
+
const kind = await kindOf(parsed.file);
|
|
1106
|
+
if (kind === "missing" || kind === "directory" && parsed.symbol !== void 0) return {
|
|
1107
|
+
...link,
|
|
1108
|
+
problem: "missing-file"
|
|
1109
|
+
};
|
|
1110
|
+
if (parsed.lines !== void 0) return {
|
|
1111
|
+
...link,
|
|
1112
|
+
...parsed.lines
|
|
1113
|
+
};
|
|
1114
|
+
if (parsed.symbol === void 0) return link;
|
|
1115
|
+
const source = await sourceOf(parsed.file);
|
|
1116
|
+
const span = parsed.symbol.kind === "name" ? resolveName(parsed.symbol.parts, source) : resolveText(parsed.symbol.text, source);
|
|
1117
|
+
return span === void 0 ? {
|
|
1118
|
+
...link,
|
|
1119
|
+
problem: "missing-symbol"
|
|
1120
|
+
} : {
|
|
1121
|
+
...link,
|
|
1122
|
+
...span
|
|
1123
|
+
};
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function symbolText(parsed) {
|
|
1127
|
+
if (parsed.symbol === void 0) return;
|
|
1128
|
+
if (parsed.symbol.kind === "text") return parsed.symbol.text;
|
|
1129
|
+
const trimmed = parsed.entry.trim();
|
|
1130
|
+
return trimmed.slice(trimmed.search(/\s/)).trim();
|
|
1131
|
+
}
|
|
1132
|
+
async function pathKind(path, realRoot) {
|
|
1133
|
+
try {
|
|
1134
|
+
if (!await resolvesInside(path, realRoot)) return "missing";
|
|
1135
|
+
const info = await stat(path);
|
|
1136
|
+
return info.isFile() ? "file" : info.isDirectory() ? "directory" : "missing";
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
if (error.code === "ENOTDIR") return "missing";
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
async function readSource(file, path, symbols) {
|
|
1143
|
+
const text = await readFile(path, "utf8");
|
|
1144
|
+
let read;
|
|
1145
|
+
try {
|
|
1146
|
+
read = await symbols?.readSymbols(file, text);
|
|
1147
|
+
} catch {
|
|
1148
|
+
read = void 0;
|
|
1149
|
+
}
|
|
1150
|
+
return {
|
|
1151
|
+
lines: text.split(/\r?\n/),
|
|
1152
|
+
symbols: read
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function resolveName(parts, source) {
|
|
1156
|
+
if (parts.length === 0) return;
|
|
1157
|
+
return (source.symbols === void 0 ? void 0 : inDefinitions(parts, source.symbols.definitions)) ?? byRegex(parts, source.lines);
|
|
1158
|
+
}
|
|
1159
|
+
function inDefinitions(parts, definitions) {
|
|
1160
|
+
let scope;
|
|
1161
|
+
for (const part of parts) {
|
|
1162
|
+
const outer = scope;
|
|
1163
|
+
scope = definitions.find((definition) => definition.name === part && (outer === void 0 || definition !== outer && definition.line >= outer.line && definition.endLine <= outer.endLine));
|
|
1164
|
+
if (scope === void 0) return;
|
|
1165
|
+
}
|
|
1166
|
+
return scope === void 0 ? void 0 : {
|
|
1167
|
+
line: scope.line,
|
|
1168
|
+
endLine: scope.endLine
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function byRegex(parts, lines) {
|
|
1172
|
+
let line = 0;
|
|
1173
|
+
for (const part of parts) {
|
|
1174
|
+
const found = findDefinitionLine(lines, part, line);
|
|
1175
|
+
if (found === void 0) return;
|
|
1176
|
+
line = found;
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
line,
|
|
1180
|
+
endLine: line
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
function resolveText(text, source) {
|
|
1184
|
+
const call = source.symbols?.calls(text)[0];
|
|
1185
|
+
if (call !== void 0) return call;
|
|
1186
|
+
const line = findTextLine(source.lines, text);
|
|
1187
|
+
return line === void 0 ? void 0 : {
|
|
1188
|
+
line,
|
|
1189
|
+
endLine: line
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
977
1192
|
var CliError = class extends Error {
|
|
978
1193
|
exitCode;
|
|
979
1194
|
constructor(message, exitCode = 1) {
|
|
@@ -1109,6 +1324,25 @@ async function lintCatalog(catalog, scan, onGitFallback) {
|
|
|
1109
1324
|
hint: text.contextOverBudgetHint
|
|
1110
1325
|
});
|
|
1111
1326
|
}
|
|
1327
|
+
findings.push(...await deadEvidence(catalog, text));
|
|
1328
|
+
return findings;
|
|
1329
|
+
}
|
|
1330
|
+
async function deadEvidence(catalog, text) {
|
|
1331
|
+
const rules = catalog.rules.filter((rule) => rule.status !== "deprecated" && (rule.evidence?.length ?? 0) > 0);
|
|
1332
|
+
if (rules.length === 0) return [];
|
|
1333
|
+
const resolve = await createEvidenceResolver(catalog.root);
|
|
1334
|
+
const findings = [];
|
|
1335
|
+
for (const rule of rules) for (const entry of rule.evidence ?? []) {
|
|
1336
|
+
const link = await resolve(entry);
|
|
1337
|
+
if (link.problem === void 0) continue;
|
|
1338
|
+
findings.push({
|
|
1339
|
+
severity: "warning",
|
|
1340
|
+
code: "dead-evidence",
|
|
1341
|
+
file: rule.file,
|
|
1342
|
+
message: link.problem === "missing-file" ? text.deadEvidenceFile(rule.id, entry, link.file) : text.deadEvidenceSymbol(rule.id, entry, link.symbol ?? "", link.file),
|
|
1343
|
+
hint: text.deadEvidenceHint
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1112
1346
|
return findings;
|
|
1113
1347
|
}
|
|
1114
1348
|
async function implementations(catalog, files, invalid, successors, text) {
|
|
@@ -1234,4 +1468,4 @@ function sortFindings(findings) {
|
|
|
1234
1468
|
function compare(a, b) {
|
|
1235
1469
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
1236
1470
|
}
|
|
1237
|
-
export {
|
|
1471
|
+
export { findRuleRegions as A, loadConfig as B, expandGlobs as C, IMPLEMENTS_MARKER_PATTERN as D, runGit as E, CONTEXT_FILE_NAME as F, messages as H, loadContext as I, parseContext as L, isRuleId as M, loadRule as N, RULE_MARKER_PATTERN as O, parseRule as P, splitFrontmatter as R, createGlobMatcher as S, toRepoPath as T, FormatError as U, parseConfig as V, formatIssue as W, scanTestRegions as _, pendingImplementation as a, loadCatalog as b, gitFallbackWarning as c, writeJson as d, writeLines as f, scanImplementations as g, parseEvidence as h, lintCommand as i, findRuleTags as j, RULE_TAG_PATTERN as k, openCatalog as l, importOptionalPackage as m, formatFinding as n, CliError as o, createEvidenceResolver as p, lintCatalog as r, findRule as s, changedFiles as t, shownPath as u, scanTestTags as v, listRepoFiles as w, relativeToRoot as x, findRulebookRoot as y, CONFIG_FILE_NAME as z };
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { C as runGit,
|
|
2
|
-
import { createRequire } from "node:module";
|
|
1
|
+
import { C as expandGlobs, E as runGit, T as toRepoPath, _ as scanTestRegions, c as gitFallbackWarning, f as writeLines, h as parseEvidence, l as openCatalog, m as importOptionalPackage, o as CliError, p as createEvidenceResolver, r as lintCatalog } from "./lint-BdC2U8K3.mjs";
|
|
3
2
|
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
4
3
|
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
6
|
// src/site/data.ts
|
|
8
7
|
const FIELD = "";
|
|
9
8
|
async function siteData(catalog, onGitFallback) {
|
|
10
9
|
const scan = await scanTestRegions(catalog.root, catalog.config.testGlobs, onGitFallback);
|
|
11
10
|
const byId = new Map(catalog.rules.map((rule) => [rule.id, rule]));
|
|
12
11
|
const rules = [...catalog.rules].sort((a, b) => compare(a.id, b.id));
|
|
12
|
+
const resolveEvidence = rules.some((rule) => (rule.evidence?.length ?? 0) > 0) ? await createEvidenceResolver(catalog.root) : void 0;
|
|
13
13
|
return {
|
|
14
14
|
contexts: [...catalog.contexts].sort((a, b) => compare(a.context, b.context)),
|
|
15
15
|
rules: await Promise.all(rules.map(async (rule) => ({
|
|
@@ -18,7 +18,8 @@ async function siteData(catalog, onGitFallback) {
|
|
|
18
18
|
tests: scan.tags.get(rule.id) ?? [],
|
|
19
19
|
history: await history(catalog.root, rule.file),
|
|
20
20
|
supersedes: chain(rule, byId),
|
|
21
|
-
supersededBy: rules.find((other) => other.supersedes === rule.id)?.id
|
|
21
|
+
supersededBy: rules.find((other) => other.supersedes === rule.id)?.id,
|
|
22
|
+
evidence: resolveEvidence === void 0 ? [] : await Promise.all((rule.evidence ?? []).map(resolveEvidence))
|
|
22
23
|
}))),
|
|
23
24
|
findings: await lintCatalog(catalog, scan, onGitFallback)
|
|
24
25
|
};
|
|
@@ -228,7 +229,7 @@ export const collections = { docs: defineCollection({ loader: docsLoader(), sche
|
|
|
228
229
|
]));
|
|
229
230
|
return files;
|
|
230
231
|
}
|
|
231
|
-
function rulePage({ rule, files, tests, history, supersedes, supersededBy }, text, base, ruleLink) {
|
|
232
|
+
function rulePage({ rule, files, tests, history, supersedes, supersededBy, evidence }, text, base, ruleLink) {
|
|
232
233
|
const rows = [
|
|
233
234
|
[text.status, rule.status],
|
|
234
235
|
[text.context, `[${rule.context}](${base}${rule.context}/)`],
|
|
@@ -238,7 +239,6 @@ function rulePage({ rule, files, tests, history, supersedes, supersededBy }, tex
|
|
|
238
239
|
if (supersedes.length > 0) rows.push([text.supersedes, supersedes.map(ruleLink).join(" → ")]);
|
|
239
240
|
if (supersededBy !== void 0) rows.push([text.supersededBy, ruleLink(supersededBy)]);
|
|
240
241
|
if (rule.origin !== void 0) rows.push([text.origin, rule.origin === "extracted" && rule.status === "proposed" ? `${rule.origin}, ${text.notValidated}` : rule.origin]);
|
|
241
|
-
const evidence = rule.evidence ?? [];
|
|
242
242
|
return page(rule.id, rule.summary, [
|
|
243
243
|
`**${cell(rule.summary)}**`,
|
|
244
244
|
"",
|
|
@@ -253,7 +253,7 @@ function rulePage({ rule, files, tests, history, supersedes, supersededBy }, tex
|
|
|
253
253
|
...evidence.length === 0 ? [] : [
|
|
254
254
|
`## ${text.evidence}`,
|
|
255
255
|
"",
|
|
256
|
-
...list(evidence.map(
|
|
256
|
+
...list(evidence.map(evidenceItem), text.none),
|
|
257
257
|
""
|
|
258
258
|
],
|
|
259
259
|
`## ${text.anchors}`,
|
|
@@ -381,6 +381,10 @@ function page(title, description, lines) {
|
|
|
381
381
|
""
|
|
382
382
|
].join("\n");
|
|
383
383
|
}
|
|
384
|
+
function evidenceItem(link) {
|
|
385
|
+
const item = `\`${link.entry}\``;
|
|
386
|
+
return link.line === null || link.endLine === null || parseEvidence(link.entry).lines !== void 0 ? item : `${item} → \`${link.file}:${link.line}-${link.endLine}\``;
|
|
387
|
+
}
|
|
384
388
|
function list(items, empty) {
|
|
385
389
|
return items.length === 0 ? [empty] : items.map((item) => `- ${item}`);
|
|
386
390
|
}
|
|
@@ -441,16 +445,8 @@ async function siteCommand(io, options, until = untilInterrupted()) {
|
|
|
441
445
|
}
|
|
442
446
|
/** Imports the site builder from the first of `from` that resolves it. */
|
|
443
447
|
async function importSitePackage(from) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
try {
|
|
447
|
-
entry = createRequire(join(directory, "package.json")).resolve(SITE_PACKAGE);
|
|
448
|
-
} catch (error) {
|
|
449
|
-
if (error.code === "MODULE_NOT_FOUND") continue;
|
|
450
|
-
throw error;
|
|
451
|
-
}
|
|
452
|
-
return await import(pathToFileURL(entry).href);
|
|
453
|
-
}
|
|
448
|
+
const site = await importOptionalPackage(SITE_PACKAGE, from);
|
|
449
|
+
if (site !== void 0) return site;
|
|
454
450
|
throw new CliError(`The site commands need the ${SITE_PACKAGE} package. Install it next to rulebookguard: npm install --save-dev ${SITE_PACKAGE}`);
|
|
455
451
|
}
|
|
456
452
|
async function buildSite(site, catalog, out, language, base, io) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rulebookguard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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": {
|
|
@@ -40,18 +40,23 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@modelcontextprotocol/client": "2.3.1",
|
|
43
|
-
"@rulebookguard/site": "
|
|
43
|
+
"@rulebookguard/site": "workspace:*",
|
|
44
|
+
"@rulebookguard/symbols": "workspace:*",
|
|
44
45
|
"@types/node": "^20.19.43",
|
|
45
46
|
"@types/picomatch": "^4.0.3",
|
|
46
47
|
"typescript": "^7.0.2",
|
|
47
48
|
"vite-plus": "^1.4.0"
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
50
|
-
"@rulebookguard/site": "^0.
|
|
51
|
+
"@rulebookguard/site": "^0.13.0",
|
|
52
|
+
"@rulebookguard/symbols": "^0.13.0"
|
|
51
53
|
},
|
|
52
54
|
"peerDependenciesMeta": {
|
|
53
55
|
"@rulebookguard/site": {
|
|
54
56
|
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@rulebookguard/symbols": {
|
|
59
|
+
"optional": true
|
|
55
60
|
}
|
|
56
61
|
},
|
|
57
62
|
"engines": {
|
|
@@ -14,30 +14,32 @@ You lift the business rules that existing code already enforces into the ruleboo
|
|
|
14
14
|
- Every rule you write has `status: proposed` and `origin: extracted`. Never set `active`.
|
|
15
15
|
- Create rule files only with `rulebookguard.mjs new <context>` (on PATH while the plugin is enabled; if the command is not found, `npx rulebookguard new <context>`). Never write a rule file by hand or pick an id yourself.
|
|
16
16
|
- No evidence, no rule: each rule names at least one place in the code that enforces it.
|
|
17
|
-
- Never edit an existing rule, production code, test logic, the config, or files outside the rulebook
|
|
17
|
+
- Never edit an existing rule, production code, test logic, the config, or files outside the rulebook and the test tags. Add `@implements` markers only when the request asks for them (step 10). Never delete files, commit or push.
|
|
18
18
|
|
|
19
19
|
## Steps
|
|
20
20
|
|
|
21
|
-
1. **Scope.** Take the context from the request. Read `rulebookguard.config.yaml` (`rulesDir`, `testGlobs`, and `contextBudget`, 30 when unset). Find the code area: the one the user names, or the anchors of the context's existing rules. If neither tells you, ask. A check the area only passes through from another module belongs to the context that owns that module:
|
|
21
|
+
1. **Scope.** Take the context from the request. Read `rulebookguard.config.yaml` (`rulesDir`, `testGlobs`, and `contextBudget`, 30 when unset). Find the code area: the one the user names, or the anchors of the context's existing rules. If neither tells you, ask. A check the area only passes through from another module belongs to the context that owns that module: do not write a rule for it here, and suggest that context. Run the `lint` tool once now and keep its findings as the baseline.
|
|
22
22
|
2. **Read the rulebook by levels.** `contexts`, then `rules_list` for the context, then `rules_show` for the rules close to the area. Never read the whole catalog.
|
|
23
|
-
3. **
|
|
24
|
-
4. **
|
|
23
|
+
3. **Trace every flow to the bottom.** List the entry points of the area (handlers, routes, commands, jobs, event consumers, public use cases). From each one, follow every call the business outcome depends on, through services, helpers, validators, guards, mappers and repositories, down to persistence and external calls: database constraints and defaults in schemas or migrations, enums and state machines, shared constants, configuration values the code reads. Do not stop at the first function that looks like the answer: a rule is often completed by a check two or three calls deeper. Stop only where the code leaves the context or is pure technical plumbing. A check owned by another module counts toward that module's context: read it to understand the flow, but do not write it here (step 1). Keep the list of entry points and how deep each one went for the report.
|
|
24
|
+
4. **Mine.** Read the code you traced and its tests. Look for decisions the business would recognize: validations, limits, states and transitions, calculations, permissions, deadlines. Use `git log` on the files for intent (commit messages, ticket keys); treat it as a hint, not proof.
|
|
25
|
+
5. **Sort each candidate** into one of the kinds below. One rule per outcome the business would tell apart: two failures with different consequences are two rules; two causes of the same outcome are one.
|
|
25
26
|
- a business rule, which you write;
|
|
26
27
|
- a technical detail (retries, caching, formats, framework behavior), which you skip;
|
|
27
28
|
- a doubt (it may be a bug or a technical value), which you write with the doubt as a question.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
6. **Deduplicate.** For each candidate run `rules_search` once per key word (it needs every word to match, so one word at a time finds more), and compare with the context's `rules_list`. If a rule already states it, do not write it; report "covered by R-…". If a rule states something different from what the code does, do not touch it; report the difference.
|
|
30
|
+
7. **Stay within budget.** Write at most `contextBudget` minus the rules the context already has. Report the rest as pending, with a suggested split of the context.
|
|
31
|
+
8. **Write.** Run `new` for each rule and fill the file it prints: a `summary` in business language, `entities`, narrow `anchors`, the rule text, at least one Gherkin scenario taken from the code or its tests, and a "Questions for the business" list. The template has no `origin` or `evidence`: add `origin: extracted` and an `evidence` list to the frontmatter, after `summary`, with every place in code and tests as `path symbol` (for example `src/clients/create.ts validateDocument`), never with line numbers. Ask only where a value or branch might be technical or a bug rather than intended, for example "Is the limit of 3 attempts a business rule or a technical value?"; at most three questions per rule, and none when the code and its tests leave no doubt. If `new` created the context, fill its `_context.md` summary and body too.
|
|
32
|
+
9. **Tag existing tests.** When a test in a file matched by `testGlobs` already verifies a rule, add a `@rule <ID>` comment on its own line above the test and an `@endrule <ID>` comment on its own line after it, in that file's comment syntax (a marker after code on the same line does not count). Change nothing else in the test. If you are not sure a test covers the rule, or the test file is not matched by `testGlobs`, list it as a suggestion instead; never edit `testGlobs` or the config.
|
|
33
|
+
10. **Suggest regions, mark only on request.** The `evidence` entries already link each rule to its code: `rulebookguard show` resolves them to lines. Do not edit production code by default; for each rule, find the lines that enforce it and list them in the report as suggested regions. Only when the request asks you to mark the code, wrap the code that enforces each rule you wrote between an `@implements <ID>` comment on its own line and an `@endimplements <ID>` comment on its own line, in that file's comment syntax and in a file the rule anchors (widen the rule's `anchors` if needed). Add only those two lines: never change the code itself.
|
|
34
|
+
11. **Lint.** Run the `lint` tool. Fix every error that is not in the baseline and run it again. You are not done while one remains. Report the baseline errors, without fixing them.
|
|
34
35
|
|
|
35
36
|
## Report
|
|
36
37
|
|
|
37
38
|
End with this report and nothing after it:
|
|
38
39
|
|
|
39
|
-
1. **
|
|
40
|
-
2. **
|
|
41
|
-
3. **
|
|
42
|
-
4. **
|
|
43
|
-
5. **
|
|
40
|
+
1. **Flows traced:** one line per entry point: `path symbol`, the deepest place reached (`path symbol`), and where and why the trace stopped (left the context, technical plumbing). Then the entry points you did not trace and why.
|
|
41
|
+
2. **Rules proposed:** one line per rule: id, summary, evidence, regions as `path:line-endLine` (suggested, or "marked" when you added the `@implements` markers), tagged tests (or "none"). Then the suggested tags you did not apply, each with its test `path:line` and the reason.
|
|
42
|
+
3. **Skipped:** each candidate you did not write and why (technical detail, covered by R-…, no evidence, over budget).
|
|
43
|
+
4. **Differences with existing rules:** rule id, what it says, what the code does, with `path:line`.
|
|
44
|
+
5. **Questions for the business:** grouped by rule id.
|
|
45
|
+
6. **Lint:** result of the last run.
|
package/plugin/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "rulebookguard",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"description": "Business rules as the source of truth: follow, look up and write the rules anchored to your code.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Juan Carlos Jimenez"
|
|
@@ -22,7 +22,7 @@ Follow these rules for every rule file, whether a person or an agent writes it,
|
|
|
22
22
|
- `supersedes`: the id of the rule this one replaces; set the old one to `deprecated`.
|
|
23
23
|
- `dependsOn`: rules that must hold for this one to make sense. `conflictsWith`: rules that cannot be active together with this one.
|
|
24
24
|
- `origin`: `extracted` when the rule was lifted from existing code, `authored` when written from a feature or a decision.
|
|
25
|
-
- `evidence`: for extracted rules, every place in code or tests where the behavior shows, as `path symbol` (the function, method or constant), never `path:line`: line numbers go stale when the code moves.
|
|
25
|
+
- `evidence`: for extracted rules, every place in code or tests where the behavior shows, as `path symbol` (the function, method or constant), never `path:line`: line numbers go stale when the code moves. Write a test name as a quoted string (`test/clients/create.test.ts "rejects a duplicate document"`) and a method as `Class.method`. `rulebookguard show` resolves each entry to the lines it names, and `lint` warns with `dead-evidence` when the file or the symbol is gone. Optional `@implements` regions mark exact lines when a symbol is too coarse. No evidence, no extracted rule.
|
|
26
26
|
|
|
27
27
|
## The body
|
|
28
28
|
|
|
@@ -20,7 +20,7 @@ Only when the user asks for the behavior change:
|
|
|
20
20
|
|
|
21
21
|
1. Edit the rule file first: its summary, body and examples. A new rule starts with `rulebookguard new <context>` as `proposed`; it becomes `active` only when the user confirms it.
|
|
22
22
|
2. Update or add the tests that verify it. A test links to a rule by sitting between a `@rule <ID>` comment right above it and an `@endrule <ID>` comment right after it; the link lives only in the test.
|
|
23
|
-
3. Change the code until those tests pass.
|
|
23
|
+
3. Change the code until those tests pass. If the rule already has `@implements <ID>` … `@endimplements <ID>` regions, keep the enforcing code inside them. Add a new region only when the user asks for one, in a file the rule anchors: regions are optional.
|
|
24
24
|
4. Run `rulebookguard lint` or the `lint` tool, and `rulebookguard lint --base <base branch>` before opening a pull request. A changed rule whose tagging tests did not change fails as `pending-implementation`.
|
|
25
25
|
|
|
26
26
|
Retire a rule by setting `status: deprecated` and removing its `@rule`/`@endrule` tags and its `@implements`/`@endimplements` regions; a replacement rule names it in `supersedes`.
|
package/npm-shrinkwrap.json
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "rulebookguard",
|
|
3
|
-
"version": "0.12.0",
|
|
4
|
-
"lockfileVersion": 3,
|
|
5
|
-
"requires": true,
|
|
6
|
-
"packages": {
|
|
7
|
-
"": {
|
|
8
|
-
"name": "rulebookguard",
|
|
9
|
-
"version": "0.12.0",
|
|
10
|
-
"license": "MIT",
|
|
11
|
-
"dependencies": {
|
|
12
|
-
"@modelcontextprotocol/server": "2.3.1",
|
|
13
|
-
"ajv": "8.20.0",
|
|
14
|
-
"picomatch": "4.0.7",
|
|
15
|
-
"yaml": "2.9.1",
|
|
16
|
-
"zod": "4.6.7"
|
|
17
|
-
},
|
|
18
|
-
"bin": {
|
|
19
|
-
"rulebookguard": "dist/cli.mjs"
|
|
20
|
-
},
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": ">=22"
|
|
23
|
-
},
|
|
24
|
-
"peerDependencies": {
|
|
25
|
-
"@rulebookguard/site": "^0.12.0"
|
|
26
|
-
},
|
|
27
|
-
"peerDependenciesMeta": {
|
|
28
|
-
"@rulebookguard/site": {
|
|
29
|
-
"optional": true
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"node_modules/@modelcontextprotocol/core": {
|
|
34
|
-
"version": "2.3.1",
|
|
35
|
-
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.3.1.tgz",
|
|
36
|
-
"integrity": "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g==",
|
|
37
|
-
"license": "Apache-2.0",
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"zod": "^4.2.0"
|
|
40
|
-
},
|
|
41
|
-
"engines": {
|
|
42
|
-
"node": ">=20"
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
"node_modules/@modelcontextprotocol/server": {
|
|
46
|
-
"version": "2.3.1",
|
|
47
|
-
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.1.tgz",
|
|
48
|
-
"integrity": "sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw==",
|
|
49
|
-
"license": "Apache-2.0",
|
|
50
|
-
"dependencies": {
|
|
51
|
-
"@modelcontextprotocol/core": "2.3.1",
|
|
52
|
-
"zod": "^4.2.0"
|
|
53
|
-
},
|
|
54
|
-
"engines": {
|
|
55
|
-
"node": ">=20"
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
"node_modules/ajv": {
|
|
59
|
-
"version": "8.20.0",
|
|
60
|
-
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
|
61
|
-
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
|
62
|
-
"license": "MIT",
|
|
63
|
-
"dependencies": {
|
|
64
|
-
"fast-deep-equal": "^3.1.3",
|
|
65
|
-
"fast-uri": "^3.0.1",
|
|
66
|
-
"json-schema-traverse": "^1.0.0",
|
|
67
|
-
"require-from-string": "^2.0.2"
|
|
68
|
-
},
|
|
69
|
-
"funding": {
|
|
70
|
-
"type": "github",
|
|
71
|
-
"url": "https://github.com/sponsors/epoberezkin"
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
"node_modules/fast-deep-equal": {
|
|
75
|
-
"version": "3.1.3",
|
|
76
|
-
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
77
|
-
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
|
78
|
-
"license": "MIT"
|
|
79
|
-
},
|
|
80
|
-
"node_modules/fast-uri": {
|
|
81
|
-
"version": "3.1.9",
|
|
82
|
-
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
|
|
83
|
-
"integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
|
|
84
|
-
"funding": [
|
|
85
|
-
{
|
|
86
|
-
"type": "github",
|
|
87
|
-
"url": "https://github.com/sponsors/fastify"
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
"type": "opencollective",
|
|
91
|
-
"url": "https://opencollective.com/fastify"
|
|
92
|
-
}
|
|
93
|
-
],
|
|
94
|
-
"license": "BSD-3-Clause"
|
|
95
|
-
},
|
|
96
|
-
"node_modules/json-schema-traverse": {
|
|
97
|
-
"version": "1.0.0",
|
|
98
|
-
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
|
99
|
-
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
|
100
|
-
"license": "MIT"
|
|
101
|
-
},
|
|
102
|
-
"node_modules/picomatch": {
|
|
103
|
-
"version": "4.0.7",
|
|
104
|
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
|
105
|
-
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
|
106
|
-
"license": "MIT",
|
|
107
|
-
"engines": {
|
|
108
|
-
"node": ">=12"
|
|
109
|
-
},
|
|
110
|
-
"funding": {
|
|
111
|
-
"url": "https://github.com/sponsors/jonschlinkert"
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
"node_modules/require-from-string": {
|
|
115
|
-
"version": "2.0.2",
|
|
116
|
-
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
|
117
|
-
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
|
118
|
-
"license": "MIT",
|
|
119
|
-
"engines": {
|
|
120
|
-
"node": ">=0.10.0"
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
"node_modules/yaml": {
|
|
124
|
-
"version": "2.9.1",
|
|
125
|
-
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
|
|
126
|
-
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
|
|
127
|
-
"license": "ISC",
|
|
128
|
-
"bin": {
|
|
129
|
-
"yaml": "bin.mjs"
|
|
130
|
-
},
|
|
131
|
-
"engines": {
|
|
132
|
-
"node": ">= 14.6"
|
|
133
|
-
},
|
|
134
|
-
"funding": {
|
|
135
|
-
"url": "https://github.com/sponsors/eemeli"
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
"node_modules/zod": {
|
|
139
|
-
"version": "4.6.7",
|
|
140
|
-
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
|
|
141
|
-
"integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
|
|
142
|
-
"license": "MIT",
|
|
143
|
-
"funding": {
|
|
144
|
-
"url": "https://github.com/sponsors/colinhacks"
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|