@rse/specbook 1.2.3 → 1.2.4

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,9 @@ SpecBook
6
6
 
7
7
  **Markdown-based Specification Format**
8
8
 
9
+ [![github (author stars)](https://img.shields.io/github/stars/rse?logo=github&label=author%20stars&color=%233377aa)](https://github.com/rse)
10
+ [![github (author followers)](https://img.shields.io/github/followers/rse?label=author%20followers&logo=github&color=%234477aa)](https://github.com/rse)
11
+
9
12
  About
10
13
  -----
11
14
 
@@ -43,10 +46,15 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
43
46
  - **Strict Validation with Custom Schema**:
44
47
  The YAML schema configuration strictly defines the allowed object
45
48
  kinds, hierarchies, and properties, whose values are constrained by an
46
- expression language (regex, enum, tags, list, and reference). Violations
47
- are reported as file- and line-precise diagnostics, while the reference
48
- coverage an object kind demands is reported as a warning only. A bundled
49
- standard schema configuration applies if no particular one is given.
49
+ expression language (regex, enum, tags, list, and reference). Beyond the
50
+ plain values, it constrains the uniqueness and presence of a property
51
+ among the sibling objects and the shape of a reference-valued property
52
+ (local, symmetric, and/or acyclic), and it can declare the child objects
53
+ of an object kind a finite state machine, whose reachability, dead-ends,
54
+ and livelocks are then checked. Violations are reported as file- and
55
+ line-precise diagnostics, while the reference coverage an object kind
56
+ demands is reported as a warning only. A bundled standard schema
57
+ configuration applies if no particular one is given.
50
58
 
51
59
  - **Object Model Diagram Visualisation**:
52
60
  Object kinds can declare "graph", "hub", or "grid" diagrams in the
@@ -63,26 +71,39 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
63
71
 
64
72
  - **AST Exports for AI/LLMs**:
65
73
  The parsed specification Abstract Syntax Tree (AST) can be exported in
66
- JSON, JSON5, YAML, or TOON format for machine consumption. Together
67
- with the `describe` command, which explains the models and formats,
68
- this enables LLMs to both read and write specifications.
74
+ JSON, JSON5, YAML, or TOON format for machine consumption, with the
75
+ derived diagram of an object attached to it as a textual Gradia spec.
76
+ Together with the `describe` command, which explains the models and
77
+ formats, this enables LLMs to both read and write specifications.
69
78
 
70
79
  - **HTML Export for Developers**:
71
- The HTML export is a self-contained single document with a
72
- table-of-contents (also as a slide-in side panel for quick navigation),
73
- full-text search, embedded images, a scroll progress meter (which
74
- also scrolls back to the top on click), and a light/dark
75
- theme toggle. It is intended for the day-to-day online reading during
76
- development. The HTML export can map all object kinds to nested
77
- sections or compact tables, or an automatic mixture which collapses
78
- only the deepest level into tables. The HTML export even can be
79
- previewed in the browser with live updates.
80
+ The HTML export is a self-contained single document with a title page,
81
+ a table-of-contents (also as a slide-in side panel for quick navigation),
82
+ a diagram-of-contents, a fuzzy full-text search, embedded images, a
83
+ scroll progress meter (which also scrolls back to the top on click),
84
+ description popups (which show the schema and corpus description of
85
+ an object kind, property, or object instance on hover), and a
86
+ light/dark theme toggle. It is intended for the day-to-day online
87
+ reading during development. The HTML export can map all object kinds
88
+ to nested sections or compact tables, or an automatic mixture which
89
+ collapses only the deepest level into tables. The HTML export even can
90
+ be previewed in the browser with live updates.
80
91
 
81
92
  - **PDF Export for Customers**:
82
93
  The PDF export prints the HTML rendering via Chromium and post-processes
83
- it with page numbers, headers/footers, and a hierarchical PDF outline.
84
- It is intended as a polished, paginated offline document for handing
85
- over to customers.
94
+ it with page numbers, headers/footers, a brand bar, and a hierarchical
95
+ PDF outline. The paper size (A4, Letter, or Legal) drives the pagination
96
+ and scales the diagrams down to fit onto a single page. It is intended
97
+ as a polished, paginated offline document for handing over to customers.
98
+
99
+ - **Document Theming and Typography**:
100
+ The title object of the specification drives the entire document:
101
+ its title, subtitle, author, version, and logo fill the title page
102
+ (with an optional light/dark variant of the logo), its language
103
+ selects the smart typography quote style, its color tone seeds the
104
+ theme color spreads of both the light and the dark theme, and its
105
+ character set subsets the embedded fonts down to the actually needed
106
+ glyphs.
86
107
 
87
108
  Overview
88
109
  --------
@@ -101,170 +122,6 @@ $ npm install -g @rse/specbook
101
122
  Usage
102
123
  -----
103
124
 
104
- ### API
105
-
106
- ```ts
107
- /* the SpecBook API, providing all commands the CLI and MCP service expose */
108
- export class SpecBook {
109
- constructor (options?: SpecBookOptions)
110
-
111
- /* initialize the configured specification artifact files below the
112
- base directory and return the list of the generated files */
113
- init (options: {
114
- config?: string[], /* YAML schema configuration files or glob patterns, merged
115
- in order (default: bundled standard one, named "std") */
116
- basedir?: string /* base directory (default: ".") */
117
- }): Promise<string[]>
118
-
119
- /* parse and validate the specification Markdown files below the base directory */
120
- lint (options: {
121
- config?: string[],
122
- basedir?: string
123
- }): Promise<LintResult>
124
-
125
- /* export the specification, parsing the input just once and
126
- returning one buffer per requested format (strict: any error
127
- diagnostic rejects the export with an "Error", while the warnings
128
- are surfaced as "notice" verbose messages) */
129
- export (options: {
130
- config?: string[],
131
- basedir?: string,
132
- formats?: ExportFormat[], /* requested output formats (default: [ "json" ]) */
133
- realtime?: boolean /* inject the live preview script into the HTML */
134
- }): Promise<Buffer[]>
135
-
136
- /* export the specification like "export" and then keep it in sync by
137
- re-exporting it on every change of a specification file (a failed
138
- re-export is reported and leaves the observe loop intact) */
139
- watch (options: {
140
- config?: string[],
141
- basedir?: string,
142
- formats?: ExportFormat[],
143
- realtime?: boolean,
144
- outputs?: string[], /* paths "onExport" writes, refused if observed */
145
- onExport: (buffers: Buffer[]) => void | Promise<void>
146
- }): Promise<void>
147
-
148
- /* serve the HTML export as a live preview on "http://<addr>:<port>/",
149
- kept in sync like "watch" and updated in the browser on every change */
150
- preview (options: {
151
- config?: string[],
152
- basedir?: string,
153
- addr?: string, /* listening IP address (default: "127.0.0.1") */
154
- port?: number /* listening TCP port (default: 12345) */
155
- }): Promise<void>
156
-
157
- /* describe the SpecBook models and formats, optionally pointing to
158
- the artifacts of the particular project */
159
- describe (options: {
160
- config?: string[],
161
- basedir?: string,
162
- embed?: boolean, /* embed the schema configuration instead of referencing it */
163
- compress?: CompressLevel, /* compression level of the schema configuration (default: 1) */
164
- format?: DescribeFormat, /* output format (default: "md") */
165
- part?: DescribePart /* document part (default: "all") */
166
- }): Promise<string>
167
- }
168
-
169
- /* the constructor options and the sink of the verbose messages,
170
- receiving the emitting command and the message */
171
- export interface SpecBookOptions {
172
- verbose?: VerboseSink
173
- }
174
- export type VerboseSink = (cmd: string, msg: string) => void
175
-
176
- /* the result of the "lint" command and its file- and line-precise diagnostics */
177
- export interface LintResult {
178
- specification: Spec
179
- diagnostics: Diagnostic[]
180
- config?: Schema
181
- files: string[] /* the artifact files plus their embedded assets */
182
- }
183
- export interface Diagnostic {
184
- file: string
185
- line: number
186
- column: number
187
- severity: "error" | "warning"
188
- message: string
189
- }
190
-
191
- /* the supported export formats and describe formats/parts/compression levels */
192
- export const formats: readonly [ "json", "json5", "yaml", "toon", "html", "pdf", "md" ]
193
- export const describeFormats: readonly [ "md", "raw" ]
194
- export const describeParts: readonly [ "all", "meta", "schema", "spec" ]
195
- export const compressLevels: readonly [ 0, 1, 2, 3 ]
196
- export type ExportFormat = typeof formats[number]
197
- export type DescribeFormat = typeof describeFormats[number]
198
- export type DescribePart = typeof describeParts[number]
199
- export type CompressLevel = typeof compressLevels[number]
200
-
201
- /* the Abstract Syntax Tree (AST) of a parsed specification */
202
- export type Spec = {
203
- artifacts: SpecArtifact[]
204
- }
205
- export type SpecArtifact = {
206
- created: Date
207
- modified: Date
208
- objects: SpecObject[]
209
- }
210
- export type SpecObject = {
211
- kind: string
212
- id: string
213
- anchor?: string
214
- paren?: string
215
- name: string
216
- primary?: boolean
217
- description?: SpecDescription
218
- properties: SpecProperty[]
219
- childs: SpecObject[]
220
- }
221
- export type SpecDescription = {
222
- description: string
223
- rationale?: string
224
- embedding?: string[]
225
- }
226
- export type SpecProperty = {
227
- key: string
228
- value: string
229
- embedding?: string[]
230
- }
231
-
232
- /* the YAML schema configuration, where the nested "diagram" and
233
- "format" structures are detailed in the "describe" output */
234
- export type Schema = SchemaObject[]
235
- export type SchemaObject = {
236
- kind: string
237
- name?: string
238
- id?: string
239
- file?: string
240
- desc?: string
241
- optional?: boolean
242
- diagram?: SchemaDiagram
243
- format?: SchemaFormat
244
- props?: SchemaProperty[]
245
- childs?: SchemaObject[]
246
- }
247
- export type SchemaProperty = {
248
- name: string
249
- desc?: string
250
- value?: string
251
- optional?: boolean
252
- }
253
- ```
254
-
255
- Example:
256
-
257
- ```ts
258
- import { SpecBook } from "@rse/specbook"
259
-
260
- const specbook = new SpecBook({
261
- verbose: (cmd, msg) => console.error(`specbook: ${cmd}: ${msg}`)
262
- })
263
- const result = await specbook.lint({
264
- basedir: "smp/broadcast"
265
- })
266
- ```
267
-
268
125
  ### CLI
269
126
 
270
127
  Commands:
@@ -354,13 +211,14 @@ Options:
354
211
 
355
212
  - `-w|--watch` (`export` only):
356
213
  Keep the outputs in sync with their sources: after the regular export,
357
- the referenced artifact files and all assets they embed are observed,
358
- and every change re-exports the specification once the sources stayed
359
- silent for one second. A failed re-export is reported and leaves the
360
- observe loop intact, so a transiently invalid specification does not
361
- end the watch. The outputs have to be regular files, as `-` (stdout)
362
- cannot receive a repeated export, and none of them may be an observed
363
- source file itself, as its own write would re-trigger the observation
214
+ the YAML schema configuration files, the referenced artifact files, and
215
+ all assets they embed are observed, and every change re-exports the
216
+ specification once the sources stayed silent for one second. A failed
217
+ re-export is reported and leaves the observe loop intact, so a
218
+ transiently invalid specification (or configuration) does not end the
219
+ watch. The outputs have to be regular files, as `-` (stdout) cannot
220
+ receive a repeated export, and none of them may be an observed source
221
+ file itself, as its own write would re-trigger the observation
364
222
  endlessly.
365
223
 
366
224
  - `-a|--addr <ip-addr>`, `-p|--port <tcp-port>` (`preview` only):
@@ -369,11 +227,13 @@ Options:
369
227
  `http://<ip-addr>:<tcp-port>/`, kept in sync with its sources exactly
370
228
  like `export --watch`, and updated in the browser after every change
371
229
  through a WebSocket connection the served page keeps open, where the
372
- document is replaced in place, so the scroll position survives (a
373
- failed re-export keeps the previous HTML in place). A status icon
374
- left of the theme switcher shows the connection state: grey while
375
- connected, in the search highlight color while disconnected, and
376
- blinking for 2s after every update.
230
+ document is replaced in place, so the scroll position and the theme
231
+ choice survive (a failed re-export keeps the previous HTML in place,
232
+ and a request before the first successful export is answered with a
233
+ placeholder page which replaces itself with the document once that
234
+ export arrives). A status tab at the bottom of the brand bar shows
235
+ the connection state: its plug icon carries the search highlight
236
+ color while disconnected and blinks for 2s after every update.
377
237
 
378
238
  - `-o|--output <output-file>` (`describe` only):
379
239
  The output file (default: `-` for stdout) receives the described
@@ -413,26 +273,74 @@ explicitly supplied option always wins. As `-c|--config` is repeatable,
413
273
  `SPECBOOK_CONFIG` carries a list of patterns separated by the path
414
274
  delimiter of the platform (`:` on Unix, `;` on Windows).
415
275
 
416
- Example:
276
+ Example: Simple
277
+ ---------------
278
+
279
+ Check out the *simple* specification of simple
280
+ example data model:
281
+
282
+ ### Sources
283
+
284
+ - [Specification](smp/sample/sample.md)
285
+ - [Schema](smp/sample/sample.yaml)
286
+
287
+ ### Generation
417
288
 
418
289
  ```bash
419
- $ specbook lint -v -b smp/broadcast
290
+ $ specbook lint -v \
291
+ -b smp/sample \
292
+ -o smp/sample/sample.html \
293
+ -o smp/sample/sample.pdf
420
294
  ```
421
295
 
422
- Rendering Examples
423
- ------------------
296
+ ### HTML Rendering
297
+
298
+ ![screenshot-sample-light](etc/screenshot-sample-light.png)
299
+ ![screenshot-sample-dark](etc/screenshot-sample-dark.png)
424
300
 
425
- ### HTML Rendering (Light Theme)
301
+ Example: Complex
302
+ ----------------
426
303
 
427
- ![screenshot-light](etc/screenshot-light.png)
304
+ Check out the *complex* specification of the Broadcast
305
+ application, based on **SpecBook**'s built-in "standard" schema:
428
306
 
429
- ### HTML Rendering (Dark Theme)
307
+ ### Sources
430
308
 
431
- ![screenshot-dark](etc/screenshot-dark.png)
309
+ - [Specification](smp/broadcast/)
310
+ - [Schema](src/specbook-format.d/)
311
+
312
+ ### Generation
313
+
314
+ ```bash
315
+ $ specbook lint -v \
316
+ -b smp/broadcast \
317
+ -o smp/broadcast/broadcast.html \
318
+ -o smp/broadcast/broadcast.pdf
319
+ ```
320
+
321
+ ### HTML Rendering
322
+
323
+ ![screenshot-broadcast-light](etc/screenshot-broadcast-light.png)
324
+ ![screenshot-broadcast-dark](etc/screenshot-broadcast-dark.png)
432
325
 
433
326
  ### PDF Rendering
434
327
 
435
- ![screenshot-print](etc/screenshot-print.png)
328
+ ![screenshot-broadcast-print](etc/screenshot-broadcast-print.png)
329
+
330
+ See Also
331
+ --------
332
+
333
+ - [Gradia](https://github.com/rse/gradia)
334
+ - [ASE](https://ase.tools)
335
+
336
+ Support
337
+ -------
338
+
339
+ **SpecBook** is developed in the experience context of industrial Software
340
+ Engineering at the [*msg group*](https://www.msg.group) and in the
341
+ educational context of the *Software Engineering Academy (SEA)*. **SpecBook**
342
+ development is supported by *msg Research* and *Software Engineering
343
+ Academy (SEA)*.
436
344
 
437
345
  License
438
346
  -------
@@ -3,7 +3,10 @@
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 { watch } from "chokidar";
6
+ /* Chokidar is aliased in "package.json", as Nunjucks declares the
7
+ (unused) Chokidar 3 as an optional peer dependency and the current
8
+ Chokidar under its plain name would raise a peer warning on install */
9
+ import { watch } from "chokidar5";
7
10
  import { minify } from "@swc/html";
8
11
  import { documentTitle, documentLogo, documentCharset, documentThemeTone, subsetStylesheet, documentPaperSize, paperStylesheet, charsetCodepoints } from "./specbook-export-common.js";
9
12
  import { themeColors, themeStylesheet, themeMapping } from "./specbook-theme.js";
@@ -33,7 +33,7 @@ const identityOf = (item, list) => {
33
33
  const object = item;
34
34
  if (list === "props")
35
35
  return String(object.name ?? "");
36
- else if (list === "childs")
36
+ else if (list === "children")
37
37
  return String(object.kind ?? "");
38
38
  else
39
39
  return `${String(object.kind ?? "")}:${String(object.id ?? object.name ?? "")}`;
@@ -158,7 +158,7 @@ const checkAutomaton = (object, at, diagnose) => {
158
158
  if (object.automaton === undefined)
159
159
  return;
160
160
  const automaton = object.automaton;
161
- const childOf = (kind) => (object.childs ?? []).find((c) => c.kind === kind);
161
+ const childOf = (kind) => (object.children ?? []).find((c) => c.kind === kind);
162
162
  const propOf = (child, name) => (child?.props ?? []).find((p) => p.name === name);
163
163
  /* an invalid value expression is reported on its own (see
164
164
  "checkProperties"), so it must not cascade into a shape diagnostic */
@@ -207,7 +207,7 @@ const checkConstraints = (config, posOfPath) => {
207
207
  const seen = new Set();
208
208
  for (const [i, object] of objects.entries()) {
209
209
  const at = [...path, i];
210
- const ident = identityOf(object, depth > 1 ? "childs" : undefined);
210
+ const ident = identityOf(object, depth > 1 ? "children" : undefined);
211
211
  if (seen.has(ident))
212
212
  diagnose([...at, "kind"], `object "${ident}" collides with a preceding sibling object (the later one is unreachable)`);
213
213
  else
@@ -239,8 +239,8 @@ const checkConstraints = (config, posOfPath) => {
239
239
  (err instanceof Error ? err.message : String(err)));
240
240
  }
241
241
  }
242
- if (object.childs !== undefined)
243
- check(object.childs, [...at, "childs"], depth + 1);
242
+ if (object.children !== undefined)
243
+ check(object.children, [...at, "children"], depth + 1);
244
244
  }
245
245
  };
246
246
  check(config, [], 1);
@@ -258,7 +258,7 @@ export const configStatistics = (config) => {
258
258
  for (const prop of object.props ?? [])
259
259
  if (prop.value !== undefined && admitsReferences(compileValueExpr(prop.value)))
260
260
  links++;
261
- for (const child of object.childs ?? [])
261
+ for (const child of object.children ?? [])
262
262
  walk(child);
263
263
  };
264
264
  for (const object of config)
@@ -109,7 +109,7 @@ const deriveReferenceEdges = (diagram, nodes, nodeSet, index, anchors, parents,
109
109
  const walk = (object) => {
110
110
  objects.push(object);
111
111
  if (diagram.deep === true)
112
- for (const child of object.childs)
112
+ for (const child of object.children)
113
113
  if (!nodeSet.has(child))
114
114
  walk(child);
115
115
  };
@@ -310,7 +310,7 @@ const deriveCenter = (object, diagram, nodes, nodeSet, index, anchors, parenProp
310
310
  /* inject the synthetic center node into the node set, with a
311
311
  collision-free id ("@" never occurs in an anchor path) */
312
312
  const center = { kind: centerCfg.kind ?? "",
313
- id: `${anchors.get(object) ?? object.id}-@center`, name: label, properties: [], childs: [] };
313
+ id: `${anchors.get(object) ?? object.id}-@center`, name: label, properties: [], children: [] };
314
314
  nodes.unshift(center);
315
315
  nodeSet.add(center);
316
316
  return { center, centerUrl };
@@ -363,7 +363,7 @@ const deriveDiagram = (object, diagram, index, anchors, parents, parenProps) =>
363
363
  nodes = new Array();
364
364
  const walk = (o) => {
365
365
  nodes.push(o);
366
- o.childs.forEach(walk);
366
+ o.children.forEach(walk);
367
367
  };
368
368
  walk(object);
369
369
  }
@@ -23,7 +23,7 @@ export const renderAst = (specification, format, config) => {
23
23
  const result = diagrams.get(object);
24
24
  if (result?.spec !== undefined && !isTitleObject(object))
25
25
  node.diagram = result.spec;
26
- object.childs.forEach((child, i) => walk(child, node.childs[i]));
26
+ object.children.forEach((child, i) => walk(child, node.children[i]));
27
27
  };
28
28
  specification.artifacts.forEach((artifact, i) => artifact.objects.forEach((object, j) => walk(object, plain.artifacts[i].objects[j])));
29
29
  }
@@ -26,7 +26,7 @@ export const titleObject = (specification) => {
26
26
  for (const object of objects) {
27
27
  if (isTitleObject(object))
28
28
  return object;
29
- const found = search(object.childs);
29
+ const found = search(object.children);
30
30
  if (found !== undefined)
31
31
  return found;
32
32
  }
@@ -4,7 +4,7 @@ import type { Verbose } from "./specbook-verbose.js";
4
4
  export type OutlineEntry = {
5
5
  title: string;
6
6
  anchor: string;
7
- childs: OutlineEntry[];
7
+ children: OutlineEntry[];
8
8
  };
9
9
  export declare const htmlOutline: (specification: Spec, config?: Schema) => OutlineEntry[];
10
10
  export declare const titlePageObject: (specification: Spec) => SpecObject | undefined;