@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 +18 -12
- package/dst/specbook-api.d.ts +3 -2
- package/dst/specbook-api.js +5 -5
- package/dst/specbook-cli.js +21 -13
- package/dst/specbook-cmd-export.js +46 -5
- package/dst/specbook-cmd-lint.js +12 -7
- package/dst/specbook-cmd-preview.js +5 -5
- package/dst/specbook-export-pdf.js +2 -2
- package/dst/specbook-format.md +2 -1
- package/dst/specbook-verbose.d.ts +5 -1
- package/dst/specbook-verbose.js +22 -0
- package/package.json +2 -2
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
|
[](https://github.com/rse)
|
|
10
12
|
[](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
|
|
189
|
-
coverage ratios: the ones the `referenced` object
|
|
190
|
-
the ones the `coverage` object kinds report, 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
|
package/dst/specbook-api.d.ts
CHANGED
|
@@ -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,
|
|
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 };
|
package/dst/specbook-api.js
CHANGED
|
@@ -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 = "
|
|
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), "
|
|
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)), "
|
|
152
|
+
(err instanceof Error ? err.message : String(err)), "none");
|
|
153
153
|
}
|
|
154
154
|
return files;
|
|
155
155
|
}, verbose);
|
package/dst/specbook-cli.js
CHANGED
|
@@ -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
|
-
|
|
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) =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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)`, "
|
|
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
|
|
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", "
|
|
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", "
|
|
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
|
|
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)), "
|
|
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
|
-
|
|
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)), "
|
|
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 */
|
package/dst/specbook-cmd-lint.js
CHANGED
|
@@ -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
|
|
79
|
-
unreferenced objects of the latter by
|
|
80
|
-
former are already
|
|
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)
|
|
97
|
-
|
|
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`, "
|
|
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`, "
|
|
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}`, "
|
|
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)}`, "
|
|
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)}`, "
|
|
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})`, "
|
|
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", "
|
|
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,
|
package/dst/specbook-format.md
CHANGED
|
@@ -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`
|
|
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 = "
|
|
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;
|
package/dst/specbook-verbose.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|