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 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.12.0 lint
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.12.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.12.0 new clients` creates `docs/rules/clients/R-CLI-001.md` as `proposed`.
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.12.0 lint`, and add it to CI with the [CI recipe](#ci-recipe).
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 their line ranges; `--json` rows carry them as `regions: [{ line, endLine }]` (or `[]`). Exits 1 when the path is outside the rulebook root.
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.12.0 -p @rulebookguard/site@0.12.0 rulebookguard site:run`.
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.12.0 lint --base origin/${{ github.base_ref }}
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, marked with `@implements`) only when the user asks for the behavior change.
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`, and marks the code that enforces each rule with `@implements` … `@endimplements`. 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 adds only marker comments to code: a person reviews the diff and activates what holds. Ask for it by name, for example "use rule-miner on the clients context".
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.12.0 mcp` from the directory Kiro starts it in, and needs Node.js and network access the first time.
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
 
@@ -1,4 +1,4 @@
1
- import { S as toRepoPath, y as createGlobMatcher } from "./lint-HEXtuztv.mjs";
1
+ import { S as createGlobMatcher, T as toRepoPath } from "./lint-BdC2U8K3.mjs";
2
2
  // src/catalog/anchors.ts
3
3
  function rulesForPath(rules, path) {
4
4
  const target = toRepoPath(path);
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { B as FormatError, F as splitFrontmatter, I as CONFIG_FILE_NAME, M as CONTEXT_FILE_NAME, S as toRepoPath, V as formatIssue, _ as loadCatalog, b as expandGlobs, c as gitFallbackWarning, d as writeJson, f as writeLines, g as findRulebookRoot, h as scanTestTags, i as lintCommand, k as isRuleId, l as openCatalog, o as CliError, p as scanImplementations, s as findRule, t as changedFiles, u as shownPath, z as messages } from "./lint-HEXtuztv.mjs";
3
- import { t as rulesForPath } from "./anchors-JxhjEzRN.mjs";
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 none = messages(catalog.config.language).none;
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
- ...rule.evidence?.length ? ["evidence:", ...indent(rule.evidence, none)] : [],
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.length === 0 ? "" : ` (${text.lines(formatSpans(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
- return render(repoPath, rules, await regionsIn(root, repoPath).catch(() => () => []), TEXT[catalog.config.language ?? "en"]);
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-COuF4ib4.mjs");
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 loadRule, B as FormatError, D as findRuleRegions, E as RULE_TAG_PATTERN, F as splitFrontmatter, I as CONFIG_FILE_NAME, L as loadConfig, M as CONTEXT_FILE_NAME, N as loadContext, O as findRuleTags, P as parseContext, R as parseConfig, S as toRepoPath, T as RULE_MARKER_PATTERN, V as formatIssue, _ as loadCatalog, a as pendingImplementation, b as expandGlobs, g as findRulebookRoot, h as scanTestTags, j as parseRule, k as isRuleId, m as scanTestRegions, n as formatFinding, p as scanImplementations, r as lintCatalog, v as relativeToRoot, w as IMPLEMENTS_MARKER_PATTERN, x as listRepoFiles, y as createGlobMatcher } from "./lint-HEXtuztv.mjs";
2
- import { t as rulesForPath } from "./anchors-JxhjEzRN.mjs";
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 { loadRule as A, FormatError as B, runGit as C, findRuleRegions as D, RULE_TAG_PATTERN as E, splitFrontmatter as F, CONFIG_FILE_NAME as I, loadConfig as L, CONTEXT_FILE_NAME as M, loadContext as N, findRuleTags as O, parseContext as P, parseConfig as R, toRepoPath as S, RULE_MARKER_PATTERN as T, formatIssue as V, loadCatalog as _, pendingImplementation as a, expandGlobs as b, gitFallbackWarning as c, writeJson as d, writeLines as f, findRulebookRoot as g, scanTestTags as h, lintCommand as i, parseRule as j, isRuleId as k, openCatalog as l, scanTestRegions as m, formatFinding as n, CliError as o, scanImplementations as p, lintCatalog as r, findRule as s, changedFiles as t, shownPath as u, relativeToRoot as v, IMPLEMENTS_MARKER_PATTERN as w, listRepoFiles as x, createGlobMatcher as y, messages as z };
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, S as toRepoPath, b as expandGlobs, c as gitFallbackWarning, f as writeLines, l as openCatalog, m as scanTestRegions, o as CliError, r as lintCatalog } from "./lint-HEXtuztv.mjs";
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((item) => `\`${item}\``), text.none),
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
- for (const directory of from) {
445
- let entry;
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.12.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": "0.12.0",
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.12.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, the test tags and the `@implements` markers. Never delete files, commit or push.
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: skip it and suggest that context. Run the `lint` tool once now and keep its findings as the baseline.
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. **Mine.** Read the code 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.
24
- 4. **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.
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
- 5. **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.
29
- 6. **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.
30
- 7. **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.
31
- 8. **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.
32
- 9. **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.
33
- 10. **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.
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. **Rules proposed:** one line per rule: id, summary, evidence, `@implements` regions as `path:line-endLine`, tagged tests (or "none"). Then the suggested tags you did not apply, each with its test `path:line` and the reason.
40
- 2. **Skipped:** each candidate you did not write and why (technical detail, covered by R-…, no evidence, over budget).
41
- 3. **Differences with existing rules:** rule id, what it says, what the code does, with `path:line`.
42
- 4. **Questions for the business:** grouped by rule id.
43
- 5. **Lint:** result of the last run.
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.
@@ -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.12.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. The exact lines live in `@implements` regions. No evidence, no extracted rule.
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. Wrap the code that enforces the rule between an `@implements <ID>` comment line and an `@endimplements <ID>` comment line, in a file the rule anchors, so the rule points at those exact lines.
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`.
@@ -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
- }