rulebookguard 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { M as FormatError, N as formatIssue, a as CliError, c as openCatalog, d as scanTestTags, f as findRulebookRoot, g as expandGlobs, l as writeJson, o as findRule, p as loadCatalog, r as lintCommand, s as gitFallbackWarning, u as writeLines, v as toRepoPath } from "./lint-C1yUeLOq.mjs";
3
- import { t as rulesForPath } from "./anchors-BFJCm1NU.mjs";
4
- import { readdir, writeFile } from "node:fs/promises";
2
+ import { F as messages, I as FormatError, L as formatIssue, O as CONTEXT_FILE_NAME, _ as expandGlobs, a as CliError, c as openCatalog, f as scanTestTags, l as writeJson, m as loadCatalog, o as findRule, p as findRulebookRoot, r as lintCommand, s as gitFallbackWarning, u as writeLines, y as toRepoPath } from "./lint-DfibQDW3.mjs";
3
+ import { t as rulesForPath } from "./anchors-C2j9c5MI.mjs";
4
+ import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
5
5
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { parseArgs } from "node:util";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
@@ -80,7 +80,7 @@ function render(path, rules, text) {
80
80
  return parts.join("\n\n");
81
81
  }
82
82
  // package.json
83
- var version = "0.5.1";
83
+ var version = "0.7.0";
84
84
  // src/mcp/server.ts
85
85
  function createRulesServer(projectDir) {
86
86
  const server = new McpServer({
@@ -136,24 +136,71 @@ async function mcpCommand(io, project) {
136
136
  }
137
137
  // src/commands/new.ts
138
138
  const MAX_NUMBER = 999;
139
- async function newCommand(catalog, io, name) {
139
+ const CONTEXT_NAME = /^[a-z0-9][a-z0-9_-]*$/;
140
+ const PREFIX = /^[A-Z][A-Z0-9]*$/;
141
+ async function newCommand(catalog, io, name, prefix) {
142
+ const text = messages(catalog.config.language);
140
143
  const context = catalog.contexts.find((item) => item.context === name);
141
- if (context === void 0) throw new CliError(`Unknown context "${name}". Run "rulebookguard contexts" to list them.`);
144
+ if (context !== void 0) {
145
+ if (prefix !== void 0 && prefix !== context.prefix) throw new CliError(text.prefixMismatch(name, context.prefix, prefix));
146
+ writeLines(io, [await writeNextRule(catalog, context, text)]);
147
+ return;
148
+ }
149
+ const created = await createContext(catalog, name, prefix, text);
150
+ try {
151
+ writeLines(io, [created.file, await writeNextRule(catalog, created, text)]);
152
+ } catch (error) {
153
+ await rm(join(catalog.root, created.file), { force: true });
154
+ throw error;
155
+ }
156
+ }
157
+ async function createContext(catalog, name, option, text) {
158
+ if (!CONTEXT_NAME.test(name)) throw new CliError(text.invalidContextName(name));
159
+ const prefix = option ?? name.replace(/[^a-z0-9]/g, "").slice(0, 3).toUpperCase();
160
+ if (!PREFIX.test(prefix)) throw new CliError(text.invalidPrefix(prefix));
161
+ const owner = catalog.contexts.find((item) => item.prefix === prefix);
162
+ if (owner !== void 0) throw new CliError(text.prefixTaken(prefix, owner.context));
163
+ const directory = `${toRepoPath(catalog.config.rulesDir).replace(/\/+$/, "")}/${name}`;
164
+ const file = `${directory}/${CONTEXT_FILE_NAME}`;
165
+ await mkdir(join(catalog.root, directory), { recursive: true });
166
+ await writeNew(catalog, file, contextTemplate(name, prefix, text), text);
167
+ return {
168
+ file,
169
+ context: name,
170
+ prefix,
171
+ summary: "",
172
+ body: ""
173
+ };
174
+ }
175
+ async function writeNextRule(catalog, context, text) {
142
176
  const directory = context.file.slice(0, context.file.lastIndexOf("/"));
143
177
  const idPattern = new RegExp(`^R-${context.prefix}-([0-9]{3})$`);
144
178
  const fileNames = (await readdir(join(catalog.root, directory))).filter((file) => file.endsWith(".md")).map((file) => file.slice(0, -3));
145
179
  const taken = [...catalog.rules.map((rule) => rule.id), ...fileNames].map((id) => idPattern.exec(id)?.[1]).filter((digits) => digits !== void 0).map(Number);
146
180
  const last = Math.max(0, ...taken);
147
- if (last >= MAX_NUMBER) throw new CliError(`No free rule id left in context "${name}" (R-${context.prefix}-${MAX_NUMBER} is taken).`);
181
+ if (last >= MAX_NUMBER) throw new CliError(text.noFreeId(context.context, `R-${context.prefix}-${MAX_NUMBER}`));
148
182
  const id = `R-${context.prefix}-${String(last + 1).padStart(3, "0")}`;
149
183
  const file = `${directory}/${id}.md`;
184
+ await writeNew(catalog, file, ruleTemplate(id, context.context, catalog.config.language ?? "en"), text);
185
+ return file;
186
+ }
187
+ async function writeNew(catalog, file, content, text) {
150
188
  try {
151
- await writeFile(join(catalog.root, file), template(id, context.context, catalog.config.language ?? "en"), { flag: "wx" });
189
+ await writeFile(join(catalog.root, file), content, { flag: "wx" });
152
190
  } catch (error) {
153
- if (error.code === "EEXIST") throw new CliError(`${file} already exists; nothing was written.`);
191
+ if (error.code === "EEXIST") throw new CliError(text.alreadyExists(file));
154
192
  throw error;
155
193
  }
156
- writeLines(io, [file]);
194
+ }
195
+ function contextTemplate(name, prefix, text) {
196
+ return `---
197
+ context: ${name}
198
+ prefix: ${prefix}
199
+ summary: '${text.contextTemplateSummary}'
200
+ ---
201
+
202
+ ${text.contextTemplateBody}
203
+ `;
157
204
  }
158
205
  const TEMPLATE_TEXT = {
159
206
  en: {
@@ -176,7 +223,7 @@ Escenario: TODO
176
223
  Entonces TODO`
177
224
  }
178
225
  };
179
- function template(id, context, language) {
226
+ function ruleTemplate(id, context, language) {
180
227
  const text = TEMPLATE_TEXT[language];
181
228
  return `---
182
229
  id: ${id}
@@ -214,17 +261,26 @@ function contextsCommand(catalog, io, json) {
214
261
  writeJson(io, { contexts });
215
262
  return;
216
263
  }
217
- writeLines(io, contexts.map((item) => `${item.context} (${item.prefix}, ${item.rules} ${item.rules === 1 ? "rule" : "rules"}): ${item.summary}`));
264
+ if (contexts.length === 0) {
265
+ writeLines(io, [messages(catalog.config.language).noContexts]);
266
+ return;
267
+ }
268
+ writeLines(io, contexts.map((item) => `${item.context} (${item.prefix}, ${messages(catalog.config.language).ruleCount(item.rules)}): ${item.summary}`));
218
269
  }
219
270
  function listCommand(catalog, io, options) {
220
271
  const { context, status } = options;
221
- if (status !== void 0 && !RULE_STATUSES.includes(status)) throw new CliError(`Invalid status "${status}": expected one of ${RULE_STATUSES.join(", ")}.`, 2);
222
- if (context !== void 0 && !catalog.contexts.some((item) => item.context === context)) throw new CliError(`Unknown context "${context}". Run "rulebookguard contexts" to list them.`);
272
+ const text = messages(catalog.config.language);
273
+ if (status !== void 0 && !RULE_STATUSES.includes(status)) throw new CliError(text.invalidStatus(status, RULE_STATUSES.join(", ")), 2);
274
+ if (context !== void 0 && !catalog.contexts.some((item) => item.context === context)) throw new CliError(text.unknownContext(context));
223
275
  const rules = sortById(catalog.rules).filter((rule) => context === void 0 || rule.context === context).filter((rule) => status === void 0 || rule.status === status).map(toSummary);
224
276
  if (options.json) {
225
277
  writeJson(io, { rules });
226
278
  return;
227
279
  }
280
+ if (rules.length === 0) {
281
+ writeLines(io, [text.noRules]);
282
+ return;
283
+ }
228
284
  writeLines(io, rules.map((rule) => `${rule.id} [${rule.status}] ${rule.summary}`));
229
285
  }
230
286
  function showCommand(catalog, io, id, json) {
@@ -240,19 +296,22 @@ function showCommand(catalog, io, id, json) {
240
296
  } });
241
297
  return;
242
298
  }
299
+ const none = messages(catalog.config.language).none;
243
300
  const header = [
244
301
  `${rule.id}: ${rule.summary}`,
245
302
  `context: ${rule.context}`,
246
303
  `status: ${rule.status}`,
247
- `entities: ${rule.entities.join(", ")}`,
248
- `anchors: ${rule.anchors.join(", ")}`,
304
+ `entities: ${rule.entities.length === 0 ? none : rule.entities.join(", ")}`,
305
+ "anchors:",
306
+ ...indent(rule.anchors, none),
249
307
  ...rule.supersedes === void 0 ? [] : [`supersedes: ${rule.supersedes}`],
250
308
  `file: ${rule.file}`
251
309
  ];
252
310
  io.stdout(`${header.join("\n")}\n\n${rule.body}`);
253
311
  }
254
312
  function forCommand(catalog, io, input, json) {
255
- const path = toRootPath(catalog.root, resolve(io.cwd, input), input);
313
+ const text = messages(catalog.config.language);
314
+ const path = toRootPath(catalog, resolve(io.cwd, input), input);
256
315
  const rules = rulesForPath(catalog.rules, path).filter((rule) => rule.status === "active").map((rule) => ({
257
316
  ...toSummary(rule),
258
317
  file: rule.file
@@ -265,14 +324,15 @@ function forCommand(catalog, io, input, json) {
265
324
  return;
266
325
  }
267
326
  if (rules.length === 0) {
268
- writeLines(io, [`No active rules anchored to ${path}.`]);
327
+ writeLines(io, [text.noActiveRulesFor(path)]);
269
328
  return;
270
329
  }
271
330
  writeLines(io, rules.map((rule) => `${rule.id} (${rule.context}): ${rule.summary}`));
272
331
  }
273
332
  async function impactCommand(catalog, io, id, json) {
274
333
  const rule = findRule(catalog, id);
275
- const warn = gitFallbackWarning(io);
334
+ const text = messages(catalog.config.language);
335
+ const warn = gitFallbackWarning(io, catalog.config.language);
276
336
  const files = await expandGlobs(catalog.root, rule.anchors, warn);
277
337
  const tests = (await scanTestTags(catalog.root, catalog.config.testGlobs, warn)).get(id) ?? [];
278
338
  if (json) {
@@ -285,15 +345,15 @@ async function impactCommand(catalog, io, id, json) {
285
345
  }
286
346
  writeLines(io, [
287
347
  `${rule.id}: ${rule.summary}`,
288
- `Files matched by anchors (${files.length}):`,
289
- ...indent(files),
290
- `Tests tagging ${rule.id} (${tests.length}):`,
291
- ...indent(tests.map((test) => `${test.file}:${test.line}`))
348
+ text.filesMatched(files.length),
349
+ ...indent(files, text.none),
350
+ text.testsTagging(rule.id, tests.length),
351
+ ...indent(tests.map((test) => `${test.file}:${test.line}`), text.none)
292
352
  ]);
293
353
  }
294
- function toRootPath(root, absolute, input) {
295
- const path = toRepoPath(relative(root, absolute));
296
- if (path === "" || path === ".." || path.startsWith("../") || isAbsolute(path)) throw new CliError(`Path "${input}" is outside the rulebook root ${root}.`);
354
+ function toRootPath(catalog, absolute, input) {
355
+ const path = toRepoPath(relative(catalog.root, absolute));
356
+ if (path === "" || path === ".." || path.startsWith("../") || isAbsolute(path)) throw new CliError(messages(catalog.config.language).outsideRoot(input, catalog.root));
297
357
  return path;
298
358
  }
299
359
  function toSummary(rule) {
@@ -304,8 +364,8 @@ function toSummary(rule) {
304
364
  summary: rule.summary
305
365
  };
306
366
  }
307
- function indent(lines) {
308
- return lines.length === 0 ? [" (none)"] : lines.map((line) => ` ${line}`);
367
+ function indent(lines, none) {
368
+ return lines.length === 0 ? [` ${none}`] : lines.map((line) => ` ${line}`);
309
369
  }
310
370
  function sortById(rules) {
311
371
  return [...rules].sort((a, b) => compare(a.id, b.id));
@@ -322,11 +382,14 @@ Commands:
322
382
  show <id> [--json] L2: the full rule
323
383
  for <path> [--json] active rules anchored to a path
324
384
  impact <id> [--json] files matched by a rule's anchors and tests tagging it
325
- new <context> scaffold the next free rule id as a proposed rule
385
+ new <context> [--prefix <P>] scaffold the next free rule id as a proposed rule;
386
+ creates the context first when it does not exist,
387
+ --prefix: its id prefix (default: first 3 letters)
326
388
  lint [--base <ref>] [--json] check the catalog, anchors and test tags;
327
389
  --base: rules changed since <ref> need a changed test
328
- hook Claude Code PreToolUse hook: reads the event on stdin
329
- and prints the rules anchored to the edited file
390
+ hook Claude Code hook for PreToolUse and PostToolUse: reads
391
+ the event on stdin and prints the rules anchored to
392
+ the file of the tool (Read, Edit, Write...)
330
393
  mcp [--project <dir>] MCP server over stdio with rules_for and rules_show;
331
394
  --project: directory to find the rulebook from (default: cwd)
332
395
  site:generate [--out <dir>] [--base <path>] build the read-only static site of the rulebook (alias: site);
@@ -372,9 +435,9 @@ const COMMANDS = {
372
435
  options: JSON_OPTION
373
436
  },
374
437
  new: {
375
- usage: "rulebookguard new <context>",
438
+ usage: "rulebookguard new <context> [--prefix <P>]",
376
439
  argument: "context",
377
- options: {}
440
+ options: { prefix: { type: "string" } }
378
441
  },
379
442
  hook: {
380
443
  usage: "rulebookguard hook",
@@ -416,7 +479,7 @@ async function runCli(argv, io) {
416
479
  return error.exitCode;
417
480
  }
418
481
  if (error instanceof FormatError) {
419
- io.stderr(`${error.issues.map(formatIssue).join("\n")}\n`);
482
+ io.stderr(`${error.issues.map((issue) => formatIssue(issue)).join("\n")}\n`);
420
483
  return 1;
421
484
  }
422
485
  throw error;
@@ -441,7 +504,7 @@ async function dispatch(argv, io) {
441
504
  return 0;
442
505
  }
443
506
  if (name === "site" || name === "site:generate" || name === "site:run") {
444
- const { siteCommand } = await import("./site-BiiOPVEe.mjs");
507
+ const { siteCommand } = await import("./site-DmY5YVte.mjs");
445
508
  await siteCommand(io, {
446
509
  out: values.out,
447
510
  base: values.base,
@@ -475,7 +538,7 @@ async function dispatch(argv, io) {
475
538
  case "impact":
476
539
  await impactCommand(catalog, io, argument, json);
477
540
  break;
478
- case "new": await newCommand(catalog, io, argument);
541
+ case "new": await newCommand(catalog, io, argument, values.prefix);
479
542
  }
480
543
  return 0;
481
544
  }
package/dist/index.d.mts CHANGED
@@ -1,5 +1,9 @@
1
1
  type RuleStatus = "proposed" | "active" | "deprecated";
2
2
  type RuleLanguage = "en" | "es";
3
+ interface ParseOptions {
4
+ /** Language of the issue messages; `en` when unset. */
5
+ language?: RuleLanguage | undefined;
6
+ }
3
7
  interface RulebookConfig {
4
8
  schema: 1;
5
9
  rulesDir: string;
@@ -34,8 +38,8 @@ export declare const CONFIG_FILE_NAME = "rulebookguard.config.yaml";
34
38
  export declare function parseConfig(source: string, file: string): RulebookConfig;
35
39
  export declare function loadConfig(path: string): Promise<RulebookConfig>;
36
40
  export declare const CONTEXT_FILE_NAME = "_context.md";
37
- export declare function parseContext(source: string, file: string): ContextDoc;
38
- export declare function loadContext(path: string): Promise<ContextDoc>;
41
+ export declare function parseContext(source: string, file: string, { language }?: ParseOptions): ContextDoc;
42
+ export declare function loadContext(path: string, options?: ParseOptions): Promise<ContextDoc>;
39
43
  interface FormatIssue {
40
44
  file: string;
41
45
  field?: string;
@@ -50,16 +54,29 @@ interface Frontmatter {
50
54
  data: unknown;
51
55
  body: string;
52
56
  }
53
- export declare function splitFrontmatter(source: string, file: string): Frontmatter;
57
+ export declare function splitFrontmatter(source: string, file: string, { language }?: ParseOptions): Frontmatter;
54
58
  export declare function isRuleId(value: string): boolean;
55
- export declare function parseRule(source: string, file: string): RuleDoc;
56
- export declare function loadRule(path: string): Promise<RuleDoc>;
59
+ export declare function parseRule(source: string, file: string, { language }?: ParseOptions): RuleDoc;
60
+ export declare function loadRule(path: string, options?: ParseOptions): Promise<RuleDoc>;
57
61
  interface RuleTag {
58
62
  id: string;
59
63
  line: number;
60
64
  }
65
+ interface RuleRegion extends RuleTag {
66
+ endLine: number;
67
+ }
68
+ type RuleTagProblemCode = "unclosed-rule-tag" | "unmatched-endrule-tag";
69
+ interface RuleTagProblem extends RuleTag {
70
+ code: RuleTagProblemCode;
71
+ }
72
+ interface RuleRegions {
73
+ regions: RuleRegion[];
74
+ problems: RuleTagProblem[];
75
+ }
61
76
  export declare const RULE_TAG_PATTERN: RegExp;
77
+ export declare const RULE_MARKER_PATTERN: RegExp;
62
78
  export declare function findRuleTags(source: string): RuleTag[];
79
+ export declare function findRuleRegions(source: string): RuleRegions;
63
80
  export declare function rulesForPath(rules: RuleDoc[], path: string): RuleDoc[];
64
81
  export declare function toRepoPath(path: string): string;
65
82
  export declare function createGlobMatcher(globs: string[]): (path: string) => boolean;
@@ -89,8 +106,22 @@ interface TagLocation {
89
106
  file: string;
90
107
  line: number;
91
108
  }
109
+ interface TagRegion extends TagLocation {
110
+ endLine: number;
111
+ }
112
+ interface TagProblem extends TagLocation {
113
+ code: RuleTagProblemCode;
114
+ id: string;
115
+ }
92
116
  type TagIndex = Map<string, TagLocation[]>;
117
+ type RegionIndex = Map<string, TagRegion[]>;
118
+ interface TagScan {
119
+ tags: TagIndex;
120
+ regions: RegionIndex;
121
+ problems: TagProblem[];
122
+ }
93
123
  export declare function scanTestTags(root: string, testGlobs: string[], onGitFallback?: GitFallbackListener): Promise<TagIndex>;
124
+ export declare function scanTestRegions(root: string, testGlobs: string[], onGitFallback?: GitFallbackListener): Promise<TagScan>;
94
125
  interface ContextsJson {
95
126
  contexts: {
96
127
  context: string;
@@ -129,6 +160,8 @@ interface ImpactJson {
129
160
  files: string[];
130
161
  tests: TagLocation[];
131
162
  }
163
+ /** Changed spans in the working tree; a pure deletion sits between two lines, as `n + 0.5`. */
164
+ type LineSpan = [start: number, end: number];
132
165
  type LintSeverity = "error" | "warning";
133
166
  interface LintFinding {
134
167
  severity: LintSeverity;
@@ -136,6 +169,8 @@ interface LintFinding {
136
169
  file: string;
137
170
  line?: number;
138
171
  message: string;
172
+ /** One line on how to fix the finding. */
173
+ hint: string;
139
174
  }
140
175
  interface LintJson {
141
176
  findings: LintFinding[];
@@ -146,7 +181,7 @@ interface LintOptions {
146
181
  base?: string | undefined;
147
182
  json: boolean;
148
183
  }
149
- export declare function lintCatalog(catalog: Catalog, tags?: TagIndex, onGitFallback?: GitFallbackListener): Promise<LintFinding[]>;
150
- export declare function pendingImplementation(catalog: Catalog, tags: TagIndex, base: string, changed: string[]): LintFinding[];
184
+ export declare function lintCatalog(catalog: Catalog, scan?: TagScan, onGitFallback?: GitFallbackListener): Promise<LintFinding[]>;
185
+ export declare function pendingImplementation(catalog: Catalog, scan: TagScan, base: string, changed: string[], lines: Map<string, LineSpan[]>): LintFinding[];
151
186
  export declare function formatFinding(finding: LintFinding): string;
152
- export type { Catalog, CatalogIssue, CatalogIssueCode, ContextDoc, ContextFrontmatter, ContextsJson, ForJson, FormatIssue, Frontmatter, GitFallbackListener, ImpactJson, LintFinding, LintJson, LintOptions, LintSeverity, ListJson, RuleDoc, RuleFrontmatter, RuleLanguage, RuleStatus, RuleSummaryJson, RuleTag, RulebookConfig, ShowJson, TagIndex, TagLocation };
187
+ export type { Catalog, CatalogIssue, CatalogIssueCode, ContextDoc, ContextFrontmatter, ContextsJson, ForJson, FormatIssue, Frontmatter, GitFallbackListener, ImpactJson, LintFinding, LintJson, LintOptions, LintSeverity, ListJson, ParseOptions, RegionIndex, RuleDoc, RuleFrontmatter, RuleLanguage, RuleRegion, RuleRegions, RuleStatus, RuleSummaryJson, RuleTag, RuleTagProblem, RuleTagProblemCode, RulebookConfig, ShowJson, TagIndex, TagLocation, TagProblem, TagRegion, TagScan };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { A as loadConfig, C as loadRule, D as parseContext, E as loadContext, M as FormatError, N as formatIssue, O as splitFrontmatter, S as isRuleId, T as CONTEXT_FILE_NAME, _ as listRepoFiles, b as RULE_TAG_PATTERN, d as scanTestTags, f as findRulebookRoot, g as expandGlobs, h as createGlobMatcher, i as pendingImplementation, j as parseConfig, k as CONFIG_FILE_NAME, m as relativeToRoot, n as lintCatalog, p as loadCatalog, t as formatFinding, v as toRepoPath, w as parseRule, x as findRuleTags } from "./lint-C1yUeLOq.mjs";
2
- import { t as rulesForPath } from "./anchors-BFJCm1NU.mjs";
3
- export { CONFIG_FILE_NAME, CONTEXT_FILE_NAME, FormatError, RULE_TAG_PATTERN, createGlobMatcher, expandGlobs, findRuleTags, findRulebookRoot, formatFinding, formatIssue, isRuleId, lintCatalog, listRepoFiles, loadCatalog, loadConfig, loadContext, loadRule, parseConfig, parseContext, parseRule, pendingImplementation, relativeToRoot, rulesForPath, scanTestTags, splitFrontmatter, toRepoPath };
1
+ import { A as parseContext, C as findRuleRegions, D as parseRule, E as loadRule, I as FormatError, L as formatIssue, M as CONFIG_FILE_NAME, N as loadConfig, O as CONTEXT_FILE_NAME, P as parseConfig, S as RULE_TAG_PATTERN, T as isRuleId, _ as expandGlobs, d as scanTestRegions, f as scanTestTags, g as createGlobMatcher, h as relativeToRoot, i as pendingImplementation, j as splitFrontmatter, k as loadContext, m as loadCatalog, n as lintCatalog, p as findRulebookRoot, t as formatFinding, v as listRepoFiles, w as findRuleTags, x as RULE_MARKER_PATTERN, y as toRepoPath } from "./lint-DfibQDW3.mjs";
2
+ import { t as rulesForPath } from "./anchors-C2j9c5MI.mjs";
3
+ export { CONFIG_FILE_NAME, CONTEXT_FILE_NAME, FormatError, 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, scanTestRegions, scanTestTags, splitFrontmatter, toRepoPath };