rulebookguard 0.4.0 → 0.5.1
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 +16 -2
- package/dist/cli.mjs +3 -2
- package/dist/{site-BgH5724T.mjs → site-BiiOPVEe.mjs} +26 -71
- package/package.json +11 -5
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ It is language-agnostic: rules are Markdown, and tests in any language link to t
|
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
-
Requires Node.js 22
|
|
16
|
+
Requires Node.js 22 or later.
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
npx rulebookguard lint
|
|
@@ -116,7 +116,13 @@ The Markdown body holds the full rule and its examples (Gherkin scenarios or tab
|
|
|
116
116
|
|
|
117
117
|
## The `@rule` tag
|
|
118
118
|
|
|
119
|
-
A test links to a rule with `@rule <ID>`
|
|
119
|
+
A test links to a rule with `@rule <ID>` anywhere in a file matched by `testGlobs`. One line can carry several tags. A comment is the usual place, but the tag is matched as text, so it also counts inside a test name or a string:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
it('rejects a duplicate document @rule R-CLI-001', () => {});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
To mention a rule in a test file without linking to it, write the id without the `@rule` prefix.
|
|
120
126
|
|
|
121
127
|
TypeScript or JavaScript:
|
|
122
128
|
|
|
@@ -262,6 +268,14 @@ The rulebook is found from `--project` (default: the current directory). A comma
|
|
|
262
268
|
|
|
263
269
|
### `site:generate [--out <dir>] [--base <path>]`
|
|
264
270
|
|
|
271
|
+
The site commands build with Astro, which lives in a separate package so that installing the linter does not download it. Install it next to `rulebookguard` (it requires Node.js 22.23.4 or later):
|
|
272
|
+
|
|
273
|
+
```sh
|
|
274
|
+
npm install --save-dev rulebookguard @rulebookguard/site
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
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 -p @rulebookguard/site rulebookguard site:run`.
|
|
278
|
+
|
|
265
279
|
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`).
|
|
266
280
|
|
|
267
281
|
```sh
|
package/dist/cli.mjs
CHANGED
|
@@ -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.
|
|
83
|
+
var version = "0.5.1";
|
|
84
84
|
// src/mcp/server.ts
|
|
85
85
|
function createRulesServer(projectDir) {
|
|
86
86
|
const server = new McpServer({
|
|
@@ -334,6 +334,7 @@ Commands:
|
|
|
334
334
|
site:run [--out <dir>] [--base <path>] [--port <n>]
|
|
335
335
|
build the site, then serve it on 127.0.0.1 until Ctrl+C;
|
|
336
336
|
--port: port to serve on (default: 4321)
|
|
337
|
+
site commands need @rulebookguard/site installed
|
|
337
338
|
|
|
338
339
|
Exit codes: 0 success, 1 failure (lint: any error), 2 usage error.
|
|
339
340
|
`;
|
|
@@ -440,7 +441,7 @@ async function dispatch(argv, io) {
|
|
|
440
441
|
return 0;
|
|
441
442
|
}
|
|
442
443
|
if (name === "site" || name === "site:generate" || name === "site:run") {
|
|
443
|
-
const { siteCommand } = await import("./site-
|
|
444
|
+
const { siteCommand } = await import("./site-BiiOPVEe.mjs");
|
|
444
445
|
await siteCommand(io, {
|
|
445
446
|
out: values.out,
|
|
446
447
|
base: values.base,
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { a as CliError, c as openCatalog, d as scanTestTags, g as expandGlobs, n as lintCatalog, s as gitFallbackWarning, u as writeLines, y as runGit } from "./lint-C1yUeLOq.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
5
|
import { createServer } from "node:http";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
7
|
// src/site/data.ts
|
|
11
8
|
const FIELD = "";
|
|
12
9
|
async function siteData(catalog, onGitFallback) {
|
|
@@ -396,7 +393,7 @@ function demote(markdown) {
|
|
|
396
393
|
}).join("\n");
|
|
397
394
|
}
|
|
398
395
|
// src/commands/site.ts
|
|
399
|
-
const
|
|
396
|
+
const SITE_PACKAGE = "@rulebookguard/site";
|
|
400
397
|
/** Marks an output directory as written by `site`, so a rebuild may replace it. */
|
|
401
398
|
const MARKER = ".rulebookguard-site";
|
|
402
399
|
/**
|
|
@@ -408,11 +405,12 @@ async function siteCommand(io, options, until = untilInterrupted()) {
|
|
|
408
405
|
const fromCwd = relative(io.cwd, out);
|
|
409
406
|
const shown = fromCwd === "" ? "." : fromCwd.startsWith("..") || isAbsolute(fromCwd) ? out : fromCwd;
|
|
410
407
|
await assertReplaceable(out, shown);
|
|
408
|
+
const site = await importSitePackage([catalog.root, dirname(fileURLToPath(import.meta.url))]);
|
|
411
409
|
const language = catalog.config.language ?? "en";
|
|
412
410
|
const base = normalizeBase(options.base ?? "/");
|
|
413
411
|
const server = options.serve ? await listen(out, base, options.port ?? 4321) : void 0;
|
|
414
412
|
try {
|
|
415
|
-
await buildSite(catalog, out, language, base, io);
|
|
413
|
+
await buildSite(site, catalog, out, language, base, io);
|
|
416
414
|
} catch (error) {
|
|
417
415
|
server?.close();
|
|
418
416
|
throw error;
|
|
@@ -427,7 +425,21 @@ async function siteCommand(io, options, until = untilInterrupted()) {
|
|
|
427
425
|
server.closeAllConnections();
|
|
428
426
|
});
|
|
429
427
|
}
|
|
430
|
-
|
|
428
|
+
/** Imports the site builder from the first of `from` that resolves it. */
|
|
429
|
+
async function importSitePackage(from) {
|
|
430
|
+
for (const directory of from) {
|
|
431
|
+
let entry;
|
|
432
|
+
try {
|
|
433
|
+
entry = createRequire(join(directory, "package.json")).resolve(SITE_PACKAGE);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (error.code === "MODULE_NOT_FOUND") continue;
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
return await import(pathToFileURL(entry).href);
|
|
439
|
+
}
|
|
440
|
+
throw new CliError(`The site commands need the ${SITE_PACKAGE} package. Install it next to rulebookguard: npm install --save-dev ${SITE_PACKAGE}`);
|
|
441
|
+
}
|
|
442
|
+
async function buildSite(site, catalog, out, language, base, io) {
|
|
431
443
|
const files = sitePages(await siteData(catalog, gitFallbackWarning(io)), {
|
|
432
444
|
language,
|
|
433
445
|
base
|
|
@@ -448,25 +460,12 @@ async function buildSite(catalog, out, language, base, io) {
|
|
|
448
460
|
}))
|
|
449
461
|
];
|
|
450
462
|
files.set("astro.config.mjs", astroConfig(text.title, language, base, sidebar));
|
|
451
|
-
const work = join(siteWorkRoot(), randomUUID());
|
|
452
463
|
try {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
await buildWithAstro(work);
|
|
458
|
-
await rm(out, {
|
|
459
|
-
recursive: true,
|
|
460
|
-
force: true
|
|
461
|
-
});
|
|
462
|
-
await cp(join(work, "dist"), out, { recursive: true });
|
|
463
|
-
await writeFile(join(out, MARKER), "");
|
|
464
|
-
} finally {
|
|
465
|
-
await rm(work, {
|
|
466
|
-
recursive: true,
|
|
467
|
-
force: true
|
|
468
|
-
});
|
|
464
|
+
await site.buildSite(files, out);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
throw error instanceof site.SiteBuildError ? new CliError(error.message) : error;
|
|
469
467
|
}
|
|
468
|
+
await writeFile(join(out, MARKER), "");
|
|
470
469
|
}
|
|
471
470
|
function untilInterrupted() {
|
|
472
471
|
return new Promise((stop) => {
|
|
@@ -541,25 +540,6 @@ async function sendNotFound(out, response) {
|
|
|
541
540
|
const page = await readFile(join(out, "404.html")).catch(() => "Not found");
|
|
542
541
|
response.writeHead(404, { "content-type": CONTENT_TYPES[".html"] }).end(page);
|
|
543
542
|
}
|
|
544
|
-
/**
|
|
545
|
-
* Starlight injects its routes as package specifiers that Astro resolves from the project root, so the generated
|
|
546
|
-
* project must live inside the node_modules directory that holds rulebookguard's own dependencies.
|
|
547
|
-
*/
|
|
548
|
-
function siteWorkRoot() {
|
|
549
|
-
let directory = dirname(fileURLToPath(import.meta.url));
|
|
550
|
-
while (!isOwnPackageRoot(directory)) {
|
|
551
|
-
const parent = dirname(directory);
|
|
552
|
-
if (parent === directory) throw new Error("rulebookguard package root not found");
|
|
553
|
-
directory = parent;
|
|
554
|
-
}
|
|
555
|
-
const parent = dirname(directory);
|
|
556
|
-
const modules = basename(parent) === "node_modules" ? parent : join(directory, "node_modules");
|
|
557
|
-
return join(modules, ".rulebookguard-site");
|
|
558
|
-
}
|
|
559
|
-
function isOwnPackageRoot(directory) {
|
|
560
|
-
const file = join(directory, "package.json");
|
|
561
|
-
return existsSync(file) && JSON.parse(readFileSync(file, "utf8")).name === "rulebookguard";
|
|
562
|
-
}
|
|
563
543
|
async function assertReplaceable(out, shown) {
|
|
564
544
|
let entries;
|
|
565
545
|
try {
|
|
@@ -570,31 +550,6 @@ async function assertReplaceable(out, shown) {
|
|
|
570
550
|
}
|
|
571
551
|
if (entries.length > 0 && !entries.includes(MARKER)) throw new CliError(`${shown} is not empty and was not written by "rulebookguard site"; choose another --out or empty it first.`);
|
|
572
552
|
}
|
|
573
|
-
async function buildWithAstro(work) {
|
|
574
|
-
const astro = join(dirname(siteWorkRoot()), "astro");
|
|
575
|
-
const { bin } = JSON.parse(readFileSync(join(astro, "package.json"), "utf8"));
|
|
576
|
-
try {
|
|
577
|
-
await execFileAsync(process.execPath, [
|
|
578
|
-
join(astro, bin.astro),
|
|
579
|
-
"build",
|
|
580
|
-
"--silent"
|
|
581
|
-
], {
|
|
582
|
-
cwd: work,
|
|
583
|
-
env: {
|
|
584
|
-
...process.env,
|
|
585
|
-
ASTRO_TELEMETRY_DISABLED: "1"
|
|
586
|
-
},
|
|
587
|
-
maxBuffer: 67108864
|
|
588
|
-
});
|
|
589
|
-
} catch (error) {
|
|
590
|
-
const { stderr, stdout } = error;
|
|
591
|
-
throw new CliError(`The site build failed:\n${astroFailureDetail(`${stderr ?? ""}${stdout ?? ""}`)}`);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
/** The first lines of Astro's error output, without its stack trace. */
|
|
595
|
-
function astroFailureDetail(output) {
|
|
596
|
-
return output.split("\n").filter((line) => !/^\s*at /.test(line) && !/^\s*Stack trace:/.test(line)).filter((line) => line.trim() !== "").slice(0, 12).join("\n");
|
|
597
|
-
}
|
|
598
553
|
function astroConfig(title, language, base, sidebar) {
|
|
599
554
|
return `import starlight from '@astrojs/starlight';
|
|
600
555
|
import { defineConfig } from 'astro/config';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rulebookguard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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": {
|
|
@@ -28,23 +28,29 @@
|
|
|
28
28
|
"./package.json": "./package.json"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@astrojs/markdown-remark": "^7.3.2",
|
|
32
|
-
"@astrojs/starlight": "^0.42.6",
|
|
33
31
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
34
32
|
"ajv": "^8.20.0",
|
|
35
|
-
"astro": "^7.3.8",
|
|
36
33
|
"picomatch": "^4.0.7",
|
|
37
34
|
"yaml": "^2.9.1",
|
|
38
35
|
"zod": "^4.6.7"
|
|
39
36
|
},
|
|
40
37
|
"devDependencies": {
|
|
41
38
|
"@modelcontextprotocol/client": "2.3.1",
|
|
39
|
+
"@rulebookguard/site": "0.5.1",
|
|
42
40
|
"@types/node": "^20.19.43",
|
|
43
41
|
"@types/picomatch": "^4.0.3",
|
|
44
42
|
"typescript": "^7.0.2",
|
|
45
43
|
"vite-plus": "^1.4.0"
|
|
46
44
|
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@rulebookguard/site": "^0.5.1"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"@rulebookguard/site": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
47
53
|
"engines": {
|
|
48
|
-
"node": ">=22
|
|
54
|
+
"node": ">=22"
|
|
49
55
|
}
|
|
50
56
|
}
|