@rse/specbook 1.2.6 → 1.2.7

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
@@ -131,17 +131,17 @@ Commands:
131
131
 
132
132
  ```bash
133
133
  $ specbook init \
134
- [-v|--verbose] \
134
+ [-v|--verbose [<level>]] \
135
135
  [-c|--config <schema-yaml-file>] \
136
136
  [-b|--basedir <spec-md-file-basedir>]
137
137
 
138
138
  $ specbook lint \
139
- [-v|--verbose] \
139
+ [-v|--verbose [<level>]] \
140
140
  [-c|--config <schema-yaml-file>] \
141
141
  [-b|--basedir <spec-md-file-basedir>]
142
142
 
143
143
  $ specbook export \
144
- [-v|--verbose] \
144
+ [-v|--verbose [<level>]] \
145
145
  [-c|--config <schema-yaml-file>] \
146
146
  [-b|--basedir <spec-md-file-basedir>] \
147
147
  [-o|--output [<format>:]<output-file>] \
@@ -149,14 +149,14 @@ $ specbook export \
149
149
  [...]
150
150
 
151
151
  $ specbook preview \
152
- [-v|--verbose] \
152
+ [-v|--verbose [<level>]] \
153
153
  [-c|--config <schema-yaml-file>] \
154
154
  [-b|--basedir <spec-md-file-basedir>] \
155
155
  [-a|--addr <ip-addr>] \
156
156
  [-p|--port <tcp-port>]
157
157
 
158
158
  $ specbook describe \
159
- [-v|--verbose] \
159
+ [-v|--verbose [<level>]] \
160
160
  [-c|--config <schema-yaml-file>] \
161
161
  [-b|--basedir <spec-md-file-basedir>] \
162
162
  [-e|--embed] \
@@ -166,13 +166,17 @@ $ specbook describe \
166
166
  [-o|--output <output-file>]
167
167
 
168
168
  $ specbook mcp \
169
- [-v|--verbose]
169
+ [-v|--verbose [<level>]]
170
170
  ```
171
171
 
172
172
  Options:
173
173
 
174
- - `-v|--verbose`:
175
- Enable verbose logging of processing information to `stderr`.
174
+ - `-v|--verbose [<level>]`:
175
+ Enable verbose logging of processing information to `stderr` at the
176
+ given verbosity level: `0` (the default) logs the notices only, `1`
177
+ (the bare flag) also the regular processing information, `2` also
178
+ the reference coverage ratios, and `3` also the unreferenced objects
179
+ of a reference coverage.
176
180
 
177
181
  - `-c|--config <schema-yaml-file>`:
178
182
  The YAML schema configuration (default: the bundled standard schema
@@ -185,10 +189,10 @@ Options:
185
189
  all diagnostics and fail on any error among them (a warning, like a
186
190
  lapse of the reference coverage a `referenced` object kind demands,
187
191
  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.
192
+ exported. The verbose output additionally states at verbosity level
193
+ `2` the reference coverage ratios: the ones the `referenced` object
194
+ kinds receive and the ones the `coverage` object kinds report, the
195
+ latter naming the unreferenced objects at level `3`.
192
196
  The option accepts glob patterns and can be given multiple times: the
193
197
  matching files (in the order of the patterns and alphabetically within
194
198
  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"))
@@ -72,7 +72,7 @@ export const watchSpecification = async (run, verbose) => {
72
72
  "error" event would otherwise terminate the process */
73
73
  watcher.on("error", (err) => {
74
74
  verbose("observing failed: " +
75
- (err instanceof Error ? err.message : String(err)), "notice");
75
+ (err instanceof Error ? err.message : String(err)), "none");
76
76
  });
77
77
  /* perform a re-export and re-synchronize the observed files */
78
78
  const cycle = async () => {
@@ -95,7 +95,7 @@ export const watchSpecification = async (run, verbose) => {
95
95
  timer = undefined;
96
96
  chain = chain.then(cycle).catch((err) => {
97
97
  verbose("re-export failed: " +
98
- (err instanceof Error ? err.message : String(err)), "notice");
98
+ (err instanceof Error ? err.message : String(err)), "none");
99
99
  });
100
100
  }, watchDelay);
101
101
  });
@@ -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.7",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "Markdown-based Specification Format (API, CLI and MCP service)",
6
6
  "license": "Apache-2.0",