@rse/specbook 1.2.6 → 1.2.8

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
@@ -6,6 +6,8 @@ SpecBook
6
6
 
7
7
  **Markdown-based Specification Format**
8
8
 
9
+ [specbook.tools](https://specbook.tools)
10
+
9
11
  [![github (author stars)](https://img.shields.io/github/stars/rse?logo=github&label=author%20stars&color=%233377aa)](https://github.com/rse)
10
12
  [![github (author followers)](https://img.shields.io/github/followers/rse?label=author%20followers&logo=github&color=%234477aa)](https://github.com/rse)
11
13
 
@@ -131,17 +133,17 @@ Commands:
131
133
 
132
134
  ```bash
133
135
  $ specbook init \
134
- [-v|--verbose] \
136
+ [-v|--verbose [<level>]] \
135
137
  [-c|--config <schema-yaml-file>] \
136
138
  [-b|--basedir <spec-md-file-basedir>]
137
139
 
138
140
  $ specbook lint \
139
- [-v|--verbose] \
141
+ [-v|--verbose [<level>]] \
140
142
  [-c|--config <schema-yaml-file>] \
141
143
  [-b|--basedir <spec-md-file-basedir>]
142
144
 
143
145
  $ specbook export \
144
- [-v|--verbose] \
146
+ [-v|--verbose [<level>]] \
145
147
  [-c|--config <schema-yaml-file>] \
146
148
  [-b|--basedir <spec-md-file-basedir>] \
147
149
  [-o|--output [<format>:]<output-file>] \
@@ -149,14 +151,14 @@ $ specbook export \
149
151
  [...]
150
152
 
151
153
  $ specbook preview \
152
- [-v|--verbose] \
154
+ [-v|--verbose [<level>]] \
153
155
  [-c|--config <schema-yaml-file>] \
154
156
  [-b|--basedir <spec-md-file-basedir>] \
155
157
  [-a|--addr <ip-addr>] \
156
158
  [-p|--port <tcp-port>]
157
159
 
158
160
  $ specbook describe \
159
- [-v|--verbose] \
161
+ [-v|--verbose [<level>]] \
160
162
  [-c|--config <schema-yaml-file>] \
161
163
  [-b|--basedir <spec-md-file-basedir>] \
162
164
  [-e|--embed] \
@@ -166,13 +168,17 @@ $ specbook describe \
166
168
  [-o|--output <output-file>]
167
169
 
168
170
  $ specbook mcp \
169
- [-v|--verbose]
171
+ [-v|--verbose [<level>]]
170
172
  ```
171
173
 
172
174
  Options:
173
175
 
174
- - `-v|--verbose`:
175
- Enable verbose logging of processing information to `stderr`.
176
+ - `-v|--verbose [<level>]`:
177
+ Enable verbose logging of processing information to `stderr` at the
178
+ given verbosity level: `0` (the default) logs the notices only, `1`
179
+ (the bare flag) also the regular processing information, `2` also
180
+ the reference coverage ratios, and `3` also the unreferenced objects
181
+ of a reference coverage.
176
182
 
177
183
  - `-c|--config <schema-yaml-file>`:
178
184
  The YAML schema configuration (default: the bundled standard schema
@@ -185,10 +191,10 @@ Options:
185
191
  all diagnostics and fail on any error among them (a warning, like a
186
192
  lapse of the reference coverage a `referenced` object kind demands,
187
193
  is reported only), so a partial or invalid specification is never
188
- exported. The verbose output additionally states the reference
189
- coverage ratios: the ones the `referenced` object kinds receive and
190
- the ones the `coverage` object kinds report, the latter naming the
191
- unreferenced objects.
194
+ exported. The verbose output additionally states at verbosity level
195
+ `2` the reference coverage ratios: the ones the `referenced` object
196
+ kinds receive and the ones the `coverage` object kinds report, the
197
+ latter naming the unreferenced objects at level `3`.
192
198
  The option accepts glob patterns and can be given multiple times: the
193
199
  matching files (in the order of the patterns and alphabetically within
194
200
  a pattern, where the literal `std` names the bundled standard schema
@@ -3,8 +3,9 @@ import { type LintResult } from "./specbook-cmd-lint.js";
3
3
  import { parseOutputSpec, formats, type ExportFormat } from "./specbook-cmd-export.js";
4
4
  import { previewAddr, previewPort } from "./specbook-cmd-preview.js";
5
5
  import { describeFormats, describeParts, parseDescribeFormat, parseDescribePart, compressLevels, parseCompressLevel, type DescribeFormat, type DescribePart, type CompressLevel } from "./specbook-cmd-describe.js";
6
- import { literal, renderVerbose, type Verbose, type VerboseLevel } from "./specbook-verbose.js";
7
- export { literal, renderVerbose, type Verbose, type VerboseLevel };
6
+ import { literal, renderVerbose, verbosities, verbosityOf, parseVerbosity, type Verbose, type VerboseLevel, type Verbosity } from "./specbook-verbose.js";
7
+ export { literal, renderVerbose, verbosities, verbosityOf, parseVerbosity };
8
+ export { type Verbose, type VerboseLevel, type Verbosity };
8
9
  export { formats, parseOutputSpec, type ExportFormat };
9
10
  export { previewAddr, previewPort };
10
11
  export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
@@ -14,9 +14,9 @@ import { lint } from "./specbook-cmd-lint.js";
14
14
  import { exportSpecification, watchSpecification, parseOutputSpec, formats, requireBrowser } from "./specbook-cmd-export.js";
15
15
  import { servePreview, previewAddr, previewPort } from "./specbook-cmd-preview.js";
16
16
  import { describeFormat, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, compressLevels, parseCompressLevel } from "./specbook-cmd-describe.js";
17
- import { literal, renderVerbose } from "./specbook-verbose.js";
17
+ import { literal, renderVerbose, verbosities, verbosityOf, parseVerbosity } from "./specbook-verbose.js";
18
18
  /* re-export the central types for API consumers */
19
- export { literal, renderVerbose };
19
+ export { literal, renderVerbose, verbosities, verbosityOf, parseVerbosity };
20
20
  export { formats, parseOutputSpec };
21
21
  export { previewAddr, previewPort };
22
22
  export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
@@ -37,7 +37,7 @@ export class SpecBook {
37
37
  }
38
38
  /* bind the verbose sink to a particular command */
39
39
  verboseOf(cmd) {
40
- return (msg, level = "debug") => this.verbose(cmd, msg, level);
40
+ return (msg, level = "notice") => this.verbose(cmd, msg, level);
41
41
  }
42
42
  /* determine the files of the YAML schema configuration out of the
43
43
  glob patterns, in the order of the patterns and alphabetically
@@ -96,7 +96,7 @@ export class SpecBook {
96
96
  throw new Error("invalid specification:\n" +
97
97
  result.diagnostics.map(renderDiagnostic).join("\n"));
98
98
  for (const diagnostic of result.diagnostics)
99
- verbose(renderDiagnostic(diagnostic), "notice");
99
+ verbose(renderDiagnostic(diagnostic), "none");
100
100
  if (result.specification.artifacts.length === 0)
101
101
  throw new Error("unexportable specification: no artifacts found");
102
102
  const buffers = new Array();
@@ -149,7 +149,7 @@ export class SpecBook {
149
149
  }
150
150
  catch (err) {
151
151
  verbose("export failed: " +
152
- (err instanceof Error ? err.message : String(err)), "notice");
152
+ (err instanceof Error ? err.message : String(err)), "none");
153
153
  }
154
154
  return files;
155
155
  }, verbose);
@@ -8,19 +8,23 @@ import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { Command, CommanderError } from "commander";
10
10
  import chalk from "chalk";
11
- import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, renderVerbose, literal, parseOutputSpec, previewAddr, previewPort, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, parseCompressLevel, version } from "./specbook-api.js";
11
+ import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, renderVerbose, literal, parseOutputSpec, previewAddr, previewPort, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, parseCompressLevel, parseVerbosity, verbosityOf, version } from "./specbook-api.js";
12
12
  import { serveMcp } from "./specbook-mcp.js";
13
13
  /* route verbose messages to stderr, keeping stdout reserved for the
14
14
  command outputs and the MCP protocol, and qualify every message with
15
15
  the tool name plus the scope path of the emitting command, where
16
16
  Chalk styles the output only if the terminal supports colors; the
17
- "notice" messages pass regardless of the verbose option, as they
17
+ verbosity of the verbose option gates the messages by their level,
18
+ so the "none" messages pass regardless of the option, as they
18
19
  report environment problems the user has to see */
19
- const verboseOf = (opts, ...scope) => (cmd, msg, level) => {
20
- if (opts.verbose || level === "notice") {
21
- const scopePath = [...scope, cmd].map((segment) => chalk.bold(segment)).join(": ");
22
- process.stderr.write(`specbook: ${scopePath}: ${renderVerbose(msg, chalk.blue)}\n`);
23
- }
20
+ const verboseOf = (opts, ...scope) => {
21
+ const verbosity = parseVerbosity(opts.verbose);
22
+ return (cmd, msg, level) => {
23
+ if (verbosityOf[level] <= verbosity) {
24
+ const scopePath = [...scope, cmd].map((segment) => chalk.bold(segment)).join(": ");
25
+ process.stderr.write(`specbook: ${scopePath}: ${renderVerbose(msg, chalk.blue)}\n`);
26
+ }
27
+ };
24
28
  };
25
29
  /* write a buffer to a standard stream, awaiting the write callback
26
30
  which only fires once the data has been flushed to the underlying
@@ -41,7 +45,7 @@ const writeOutput = async (output, data, cmd, verbose) => {
41
45
  await writeStdout(data);
42
46
  else {
43
47
  await fs.promises.writeFile(output, data);
44
- verbose(cmd, `wrote "${literal(output)}" (${literal(Buffer.byteLength(data))} bytes)`, "debug");
48
+ verbose(cmd, `wrote "${literal(output)}" (${literal(Buffer.byteLength(data))} bytes)`, "notice");
45
49
  }
46
50
  };
47
51
  /* determine an option default value, overridable via a
@@ -56,9 +60,13 @@ const envDefaultFlag = (name, fallback) => {
56
60
  const value = process.env[`SPECBOOK_${name.toUpperCase().replace(/-/g, "_")}`];
57
61
  return value !== undefined ? !(/^(?:|0|false|no|off)$/i).test(value) : fallback;
58
62
  };
59
- /* provide the verbose option of all sub-commands */
63
+ /* provide the verbose option of all sub-commands, whose optional level
64
+ selects the verbosity (where the environment default carries a level
65
+ or a boolean word) */
60
66
  const withVerboseOption = (command) => command
61
- .option("-v, --verbose", "print verbose processing information to stderr", envDefaultFlag("verbose", false));
67
+ .option("-v, --verbose [level]", "print verbose processing information to stderr " +
68
+ "(0: none, 1: notices, 2: also the details like the coverage ratios, " +
69
+ "3: also the traces like the unreferenced objects; default: 0, bare flag: 1)", envDefault("verbose", "0"));
62
70
  /* provide the repeatable schema configuration option, whose files or
63
71
  glob patterns are merged in order (with "std" naming the bundled
64
72
  standard one), and determine its value, where the environment default
@@ -96,7 +104,7 @@ program.name("specbook")
96
104
  withVerboseOption(program.command("mcp"))
97
105
  .description("run as MCP stdio server")
98
106
  .action(async (opts) => {
99
- verboseOf(opts)("mcp", "starting MCP server on stdio", "debug");
107
+ verboseOf(opts)("mcp", "starting MCP server on stdio", "notice");
100
108
  await serveMcp(verboseOf(opts, "mcp"));
101
109
  });
102
110
  /* the init command creates the configured artifact files */
@@ -116,13 +124,13 @@ withCommonOptions(program.command("lint"))
116
124
  const specbook = new SpecBook({ verbose: verboseOf(opts) });
117
125
  const result = await specbook.lint({ config: configOf(opts), basedir: opts.basedir });
118
126
  for (const diagnostic of result.diagnostics)
119
- await writeStdout(opts.verbose ?
127
+ await writeStdout(parseVerbosity(opts.verbose) > 0 ?
120
128
  renderDiagnosticVerbose(diagnostic, process.stdout.isTTY === true) :
121
129
  `${renderDiagnostic(diagnostic)}\n`);
122
130
  if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error"))
123
131
  process.exitCode = 1;
124
132
  else
125
- verboseOf(opts)("lint", "specification valid", "debug");
133
+ verboseOf(opts)("lint", "specification valid", "notice");
126
134
  });
127
135
  /* the export command parses the input once and writes every output */
128
136
  withCommonOptions(program.command("export"))
@@ -3,6 +3,7 @@
3
3
  ** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
+ import * as fs from "node:fs";
6
7
  /* Chokidar is aliased in "package.json", as Nunjucks declares the
7
8
  (unused) Chokidar 3 as an optional peer dependency and the current
8
9
  Chokidar under its plain name would raise a peer warning on install */
@@ -56,23 +57,45 @@ export const parseOutputSpec = (spec) => {
56
57
  /* the quiet period the observed files have to stay silent for before a
57
58
  burst of changes is coalesced into a single re-export */
58
59
  const watchDelay = 1000;
60
+ /* the interval of the safety-net scan, which catches the changes the
61
+ file-system watcher missed: a watcher can die unnoticed -- macOS
62
+ tears down its FSEvents streams across a sleep/wake cycle -- and
63
+ would then leave the export stale for the rest of the process life */
64
+ const scanDelay = 30 * 1000;
65
+ /* take the modification time snapshot of the observed files, where an
66
+ absent file (a still missing artifact) is remembered as absent, too,
67
+ so its creation and removal are noticed like a modification */
68
+ const snapshot = (files) => {
69
+ const times = new Map();
70
+ for (const file of files) {
71
+ try {
72
+ times.set(file, fs.statSync(file).mtimeMs);
73
+ }
74
+ catch {
75
+ times.set(file, 0);
76
+ }
77
+ }
78
+ return times;
79
+ };
59
80
  /* keep a specification export in sync with its sources, where "run"
60
81
  performs one export and returns the files to observe: the initial
61
82
  export is performed up-front and every observed change triggers a
62
83
  re-export once the sources fell silent again. The observed set is
63
84
  re-synchronized after every run, as an edit can add or drop an
64
85
  embedded asset. The returned promise settles once the initial export
65
- is done, while the active watcher keeps the process alive afterwards */
86
+ is done, while the active watcher and the safety-net scan keep the
87
+ process alive afterwards */
66
88
  export const watchSpecification = async (run, verbose) => {
67
89
  /* perform the regular export before entering the observe loop */
68
90
  let observed = await run();
91
+ let times = snapshot(observed);
69
92
  const watcher = watch(observed, { ignoreInitial: true });
70
93
  verbose(`observing ${literal(observed.length)} specification file(s) for changes`);
71
94
  /* report the failures of the observation itself, as an unhandled
72
95
  "error" event would otherwise terminate the process */
73
96
  watcher.on("error", (err) => {
74
97
  verbose("observing failed: " +
75
- (err instanceof Error ? err.message : String(err)), "notice");
98
+ (err instanceof Error ? err.message : String(err)), "none");
76
99
  });
77
100
  /* perform a re-export and re-synchronize the observed files */
78
101
  const cycle = async () => {
@@ -80,6 +103,7 @@ export const watchSpecification = async (run, verbose) => {
80
103
  watcher.unwatch(observed.filter((file) => !files.includes(file)));
81
104
  watcher.add(files.filter((file) => !observed.includes(file)));
82
105
  observed = files;
106
+ times = snapshot(files);
83
107
  };
84
108
  /* restart the quiet period on every change, so a burst collapses
85
109
  into a single re-export, and chain the runs onto each other, so a
@@ -88,17 +112,34 @@ export const watchSpecification = async (run, verbose) => {
88
112
  and hence silently ending the observe loop) */
89
113
  let chain = Promise.resolve();
90
114
  let timer;
91
- watcher.on("all", () => {
115
+ const schedule = () => {
92
116
  if (timer !== undefined)
93
117
  clearTimeout(timer);
94
118
  timer = setTimeout(() => {
95
119
  timer = undefined;
96
120
  chain = chain.then(cycle).catch((err) => {
97
121
  verbose("re-export failed: " +
98
- (err instanceof Error ? err.message : String(err)), "notice");
122
+ (err instanceof Error ? err.message : String(err)), "none");
99
123
  });
100
124
  }, watchDelay);
101
- });
125
+ };
126
+ watcher.on("all", schedule);
127
+ /* compare the modification times regularly, so a change the
128
+ watcher never reported still reaches the export, and re-establish
129
+ the watchers on such a miss, as a watcher which lost a file never
130
+ regains it on its own and the following changes would be caught
131
+ by the scan only */
132
+ setInterval(() => {
133
+ for (const [file, time] of snapshot(observed))
134
+ if (times.get(file) !== time) {
135
+ verbose(`observing missed the change of "${literal(file)}" ` +
136
+ "-- re-establishing the file observation", "none");
137
+ watcher.unwatch(observed);
138
+ watcher.add(observed);
139
+ schedule();
140
+ break;
141
+ }
142
+ }, scanDelay);
102
143
  };
103
144
  /* render a specification into the requested format, where "realtime"
104
145
  injects the client-side script of the live preview into the HTML */
@@ -75,9 +75,10 @@ export const lint = (options) => {
75
75
  options.verbose(`parsed specification defining ${literal(stats.objects)} object(s) ` +
76
76
  `and ${literal(stats.links)} link relationship(s)`);
77
77
  /* report the reference coverage the "referenced"-flagged object
78
- kinds receive and the "coverage"-configured objects report, the
79
- unreferenced objects of the latter by name (the ones of the
80
- former are already reported as warnings) */
78
+ kinds receive and the "coverage"-configured objects report as
79
+ additional figures, the unreferenced objects of the latter by
80
+ name as a lengthy detail (the ones of the former are already
81
+ reported as warnings) */
81
82
  if (config !== undefined && result.specification.artifacts.length > 0) {
82
83
  const index = buildLinkIndex(result.specification);
83
84
  const schemas = collectSchemas(result.specification, config);
@@ -88,13 +89,17 @@ export const lint = (options) => {
88
89
  const chain = chainOf(index, covered[0] ?? uncovered[0]);
89
90
  options.verbose((schema.referenced ?? []).map((entry) => `"${literal(entry)}"`).join(" or ") +
90
91
  ` references ${ratio(covered, uncovered)} ${schema.kind} object(s)` +
91
- (chain.length > 1 ? ` below ${label(chain[0])}` : ""));
92
+ (chain.length > 1 ? ` below ${label(chain[0])}` : ""), "detail");
92
93
  }
93
94
  for (const [object, entries] of specCoverage(index, schemas))
94
- for (const { pattern, covered, uncovered } of entries)
95
+ for (const { pattern, covered, uncovered } of entries) {
95
96
  options.verbose(`${label(object)} references ${ratio(covered, uncovered)} ` +
96
- `"${literal(pattern)}" object(s)` + (uncovered.length > 0 ?
97
- `, unreferenced: ${uncovered.map(label).join(", ")}` : ""));
97
+ `"${literal(pattern)}" object(s)`, "detail");
98
+ if (uncovered.length > 0)
99
+ options.verbose(`${label(object)} leaves ${literal(uncovered.length)} ` +
100
+ `"${literal(pattern)}" object(s) unreferenced: ` +
101
+ uncovered.map(label).join(", "), "trace");
102
+ }
98
103
  }
99
104
  /* report the non-optional artifacts absent from the specification,
100
105
  against their loaded artifact file (an absent or unreadable file
@@ -36,16 +36,16 @@ export const servePreview = async (options) => {
36
36
  /* identify the client by its remote address and port */
37
37
  const addr = request.socket.remoteAddress ?? "unknown";
38
38
  const client = `${addr.includes(":") ? `[${addr}]` : addr}:${request.socket.remotePort ?? 0}`;
39
- options.verbose(`client ${literal(client)} connected`, "notice");
39
+ options.verbose(`client ${literal(client)} connected`, "none");
40
40
  clients.add(socket);
41
41
  socket.on("close", () => {
42
42
  clients.delete(socket);
43
- options.verbose(`client ${literal(client)} disconnected`, "notice");
43
+ options.verbose(`client ${literal(client)} disconnected`, "none");
44
44
  });
45
45
  /* report a failing client, as an unhandled "error" event
46
46
  would otherwise terminate the process */
47
47
  socket.on("error", (err) => {
48
- options.verbose(`client ${literal(client)} failed: ${err.message}`, "notice");
48
+ options.verbose(`client ${literal(client)} failed: ${err.message}`, "none");
49
49
  });
50
50
  }
51
51
  });
@@ -53,12 +53,12 @@ export const servePreview = async (options) => {
53
53
  await fastify.listen({ host: options.addr, port: options.port });
54
54
  const host = options.addr.includes(":") ? `[${options.addr}]` : options.addr;
55
55
  const url = `http://${host}:${options.port}/`;
56
- options.verbose(`listening on ${literal(url)}`, "notice");
56
+ options.verbose(`listening on ${literal(url)}`, "none");
57
57
  return {
58
58
  update: (buffer) => {
59
59
  /* the first export turns the 503 placeholder into the document */
60
60
  if (html === undefined)
61
- options.verbose(`serving ${literal(url)}`, "notice");
61
+ options.verbose(`serving ${literal(url)}`, "none");
62
62
  html = buffer;
63
63
  options.verbose(`notifying ${literal(clients.size)} preview client(s)`);
64
64
  for (const client of clients)
@@ -176,7 +176,7 @@ const resolveBrowser = async (verbose) => {
176
176
  const chrome = await chromium.launch({ channel: "chrome" });
177
177
  await chrome.close();
178
178
  verbose("Playwright Chromium unavailable -- falling back to the " +
179
- `system-installed Google Chrome (${chromiumRemedy})`, "notice");
179
+ `system-installed Google Chrome (${chromiumRemedy})`, "none");
180
180
  return { channel: "chrome" };
181
181
  }
182
182
  catch {
@@ -248,7 +248,7 @@ export const htmlToPdf = async (renderHtmlPass, heading, outline, titlePage, ver
248
248
  would otherwise ship silently wrong page numbers */
249
249
  if (!stable)
250
250
  verbose(`the ToC page numbers did not stabilize within ${literal(tocPasses)} ` +
251
- "passes -- they can be off by a page", "notice");
251
+ "passes -- they can be off by a page", "none");
252
252
  /* render the final document, decorated with header/footer */
253
253
  const decorated = await renderPdf(html, {
254
254
  displayHeaderFooter: true,
@@ -622,7 +622,8 @@ the description of the reporting object (one row per pattern, labeled
622
622
  by the kinds of the matching objects, with the covered and total counts
623
623
  and the ratio as a bar), the AST exports attach the counts as the
624
624
  `coverage` field of the object, and the verbose output of `lint` and
625
- `export` names the unreferenced objects.
625
+ `export` states the ratio at its higher levels and names the
626
+ unreferenced objects at its highest one.
626
627
 
627
628
  ### Format
628
629
 
@@ -1,4 +1,8 @@
1
- export type VerboseLevel = "debug" | "notice";
1
+ export type VerboseLevel = "none" | "notice" | "detail" | "trace";
2
2
  export type Verbose = (msg: string, level?: VerboseLevel) => void;
3
+ export declare const verbosities: readonly [0, 1, 2, 3];
4
+ export type Verbosity = typeof verbosities[number];
5
+ export declare const verbosityOf: Record<VerboseLevel, Verbosity>;
6
+ export declare const parseVerbosity: (value: string | number | boolean) => Verbosity;
3
7
  export declare const literal: (value: string | number) => string;
4
8
  export declare const renderVerbose: (msg: string, style?: (value: string) => string) => string;
@@ -3,6 +3,28 @@
3
3
  ** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
+ /* the verbosity a consumer runs at, as the least one surfacing the
7
+ messages of each level: a message passes a consumer whose verbosity
8
+ is at least the one of its level */
9
+ export const verbosities = [0, 1, 2, 3];
10
+ export const verbosityOf = { none: 0, notice: 1, detail: 2, trace: 3 };
11
+ /* parse and validate a verbosity specification, where a bare flag (or
12
+ a boolean word) selects the regular processing information */
13
+ export const parseVerbosity = (value) => {
14
+ let level;
15
+ if (typeof value === "boolean")
16
+ level = value ? 1 : 0;
17
+ else if (typeof value === "string" && (/^(?:true|yes|on)$/i).test(value))
18
+ level = 1;
19
+ else if (typeof value === "string" && (/^(?:false|no|off)$/i).test(value))
20
+ level = 0;
21
+ else
22
+ level = typeof value === "string" && !(/^\d+$/).test(value) ? NaN : Number(value);
23
+ if (!verbosities.includes(level))
24
+ throw new Error(`unknown verbosity "${value}" ` +
25
+ `(supported: ${verbosities.join(", ")})`);
26
+ return level;
27
+ };
6
28
  /* the sentinel characters delimiting a marked literal value, taken
7
29
  from the Unicode private use area, so they never clash with the
8
30
  regular content of a verbose message */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rse/specbook",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "Markdown-based Specification Format (API, CLI and MCP service)",
6
6
  "license": "Apache-2.0",
@@ -54,7 +54,7 @@
54
54
  "globals": "17.12.0",
55
55
  "markdownlint": "0.41.1",
56
56
  "markdownlint-cli2": "0.23.2",
57
- "@types/node": "26.4.1",
57
+ "@types/node": "26.5.0",
58
58
  "@types/subset-font": "2.3.0",
59
59
  "@types/nunjucks": "3.2.6",
60
60
  "@types/ws": "8.18.1",