@rse/specbook 1.0.0 → 1.0.2
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 +272 -110
- package/dst/specbook-api.d.ts +5 -0
- package/dst/specbook-api.js +22 -10
- package/dst/specbook-cli.js +8 -4
- package/dst/specbook-cmd-describe.d.ts +9 -0
- package/dst/specbook-cmd-describe.js +78 -15
- package/dst/specbook-export-html.js +1 -1
- package/dst/specbook-export-md.js +1 -1
- package/dst/specbook-format.md +1 -1
- package/dst/specbook-mcp.js +16 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,63 +22,63 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
|
|
|
22
22
|
|
|
23
23
|
**SpecBook** provides the following distinct features:
|
|
24
24
|
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
25
|
+
- **Hierarchical Object Model**:
|
|
26
|
+
A specification is a tree of typed objects, each with a kind and
|
|
27
|
+
name, an optional id, optional properties, optional description, and
|
|
28
|
+
optional child objects. The allowed object hierarchy is defined per
|
|
29
|
+
context by the YAML schema configuration.
|
|
30
|
+
|
|
31
|
+
- **Versatile Object to Markdown Mapping**:
|
|
32
|
+
Objects are authored as plain Markdown headings, property lists,
|
|
33
|
+
and description prose. The objects can be mapped to nested sections
|
|
34
|
+
("complex" format) or compact bullet point lists ("concise" and
|
|
35
|
+
"group" format).
|
|
36
|
+
|
|
37
|
+
- **Wiki-Style Object Linking**:
|
|
38
|
+
Objects can hierarchically reference each other through Wiki-style
|
|
39
|
+
`[[xxx]]` references, which are resolved against the locally-unique
|
|
40
|
+
ids and names of all objects. In the HTML and PDF exports they become
|
|
41
|
+
navigable links onto precise anchors.
|
|
42
|
+
|
|
43
|
+
- **Strict Validation with Custom Schema**:
|
|
44
|
+
The YAML schema configuration strictly defines the allowed object
|
|
45
|
+
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. A bundled standard
|
|
48
|
+
schema configuration applies if no particular one is given.
|
|
49
|
+
|
|
50
|
+
- **Object Model Diagram Visualisation**:
|
|
51
|
+
Object kinds can declare "graph", "hub", or "grid" diagrams in the
|
|
52
|
+
schema, whose nodes and edges are automatically derived from the
|
|
53
|
+
object model and its references. The diagrams are rendered with the
|
|
54
|
+
sibling project [Gradia](https://github.com/rse/gradia), which is
|
|
55
|
+
specialized in rendering object models.
|
|
56
|
+
|
|
57
|
+
- **CLI, MCP, and API Interface**:
|
|
58
|
+
All commands are implemented once in the API class `SpecBook`, on
|
|
59
|
+
which both the CLI and the MCP service are just thin wrappers.
|
|
60
|
+
This way, humans, scripts, and AI agents use exactly the same
|
|
61
|
+
functionality.
|
|
62
|
+
|
|
63
|
+
- **AST Exports for AI/LLMs**:
|
|
64
|
+
The parsed specification Abstract Syntax Tree (AST) can be exported in
|
|
65
|
+
JSON, JSON5, YAML, or TOON format for machine consumption. Together
|
|
66
|
+
with the `describe` command, which explains the models and formats,
|
|
67
|
+
this enables LLMs to both read and write specifications.
|
|
68
|
+
|
|
69
|
+
- **HTML Export for Developers**:
|
|
70
|
+
The HTML export is a self-contained single document with a
|
|
71
|
+
table-of-contents, full-text search, embedded images, and a light/dark
|
|
72
|
+
theme toggle. It is intended for the day-to-day online reading during
|
|
73
|
+
development. The HTML export can map all object kinds to nested
|
|
74
|
+
sections or compact tables, or an automatic mixture which collapses
|
|
75
|
+
only the deepest level into tables.
|
|
76
|
+
|
|
77
|
+
- **PDF Export for Customers**:
|
|
78
|
+
The PDF export prints the HTML rendering via Chromium and post-processes
|
|
79
|
+
it with page numbers, headers/footers, and a hierarchical PDF outline.
|
|
80
|
+
It is intended as a polished, paginated offline document for handing
|
|
81
|
+
over to customers.
|
|
82
82
|
|
|
83
83
|
Overview
|
|
84
84
|
--------
|
|
@@ -97,73 +97,235 @@ $ npm install -g @rse/specbook
|
|
|
97
97
|
Usage
|
|
98
98
|
-----
|
|
99
99
|
|
|
100
|
-
###
|
|
100
|
+
### API
|
|
101
101
|
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
```ts
|
|
103
|
+
/* the SpecBook API, providing all commands the CLI and MCP service expose */
|
|
104
|
+
export class SpecBook {
|
|
105
|
+
constructor (options?: SpecBookOptions)
|
|
106
|
+
|
|
107
|
+
/* initialize the configured specification artifact files below the
|
|
108
|
+
base directory and return the list of the generated files */
|
|
109
|
+
init (options: {
|
|
110
|
+
config?: string, /* YAML schema configuration (default: bundled standard one) */
|
|
111
|
+
basedir?: string /* base directory (default: ".") */
|
|
112
|
+
}): Promise<string[]>
|
|
113
|
+
|
|
114
|
+
/* parse and validate the specification Markdown files below the base directory */
|
|
115
|
+
lint (options: {
|
|
116
|
+
config?: string,
|
|
117
|
+
basedir?: string
|
|
118
|
+
}): Promise<LintResult>
|
|
119
|
+
|
|
120
|
+
/* export the specification, parsing the input just once and
|
|
121
|
+
returning one buffer per requested format (strict: any diagnostic
|
|
122
|
+
rejects the export with an "Error") */
|
|
123
|
+
export (options: {
|
|
124
|
+
config?: string,
|
|
125
|
+
basedir?: string,
|
|
126
|
+
formats?: ExportFormat[] /* requested output formats (default: [ "json" ]) */
|
|
127
|
+
}): Promise<Buffer[]>
|
|
128
|
+
|
|
129
|
+
/* describe the SpecBook models and formats, optionally pointing to
|
|
130
|
+
the artifacts of the particular project */
|
|
131
|
+
describe (options: {
|
|
132
|
+
config?: string,
|
|
133
|
+
basedir?: string,
|
|
134
|
+
embed?: boolean, /* embed the schema configuration instead of referencing it */
|
|
135
|
+
format?: DescribeFormat, /* output format (default: "md") */
|
|
136
|
+
part?: DescribePart /* document part (default: "all") */
|
|
137
|
+
}): Promise<string>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* the constructor options and the sink of the verbose messages,
|
|
141
|
+
receiving the emitting command and the message */
|
|
142
|
+
export interface SpecBookOptions {
|
|
143
|
+
verbose?: VerboseSink
|
|
144
|
+
}
|
|
145
|
+
export type VerboseSink = (cmd: string, msg: string) => void
|
|
146
|
+
|
|
147
|
+
/* the result of the "lint" command and its file- and line-precise diagnostics */
|
|
148
|
+
export interface LintResult {
|
|
149
|
+
specification: Spec
|
|
150
|
+
diagnostics: Diagnostic[]
|
|
151
|
+
config?: Schema
|
|
152
|
+
}
|
|
153
|
+
export interface Diagnostic {
|
|
154
|
+
file: string
|
|
155
|
+
line: number
|
|
156
|
+
column: number
|
|
157
|
+
message: string
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* the supported export formats and describe formats/parts */
|
|
161
|
+
export const formats: readonly [ "json", "json5", "yaml", "toon", "html", "pdf", "md" ]
|
|
162
|
+
export const describeFormats: readonly [ "md", "raw" ]
|
|
163
|
+
export const describeParts: readonly [ "all", "meta", "schema", "spec" ]
|
|
164
|
+
export type ExportFormat = typeof formats[number]
|
|
165
|
+
export type DescribeFormat = typeof describeFormats[number]
|
|
166
|
+
export type DescribePart = typeof describeParts[number]
|
|
167
|
+
|
|
168
|
+
/* the Abstract Syntax Tree (AST) of a parsed specification */
|
|
169
|
+
export type Spec = {
|
|
170
|
+
artifacts: SpecArtifact[]
|
|
171
|
+
}
|
|
172
|
+
export type SpecArtifact = {
|
|
173
|
+
created: Date
|
|
174
|
+
modified: Date
|
|
175
|
+
objects: SpecObject[]
|
|
176
|
+
}
|
|
177
|
+
export type SpecObject = {
|
|
178
|
+
kind: string
|
|
179
|
+
id: string
|
|
180
|
+
anchor?: string
|
|
181
|
+
paren?: string
|
|
182
|
+
name: string
|
|
183
|
+
primary?: boolean
|
|
184
|
+
description?: SpecDescription
|
|
185
|
+
properties: SpecProperty[]
|
|
186
|
+
childs: SpecObject[]
|
|
187
|
+
}
|
|
188
|
+
export type SpecDescription = {
|
|
189
|
+
description: string
|
|
190
|
+
rationale?: string
|
|
191
|
+
embedding?: string[]
|
|
192
|
+
}
|
|
193
|
+
export type SpecProperty = {
|
|
194
|
+
key: string
|
|
195
|
+
value: string
|
|
196
|
+
embedding?: string[]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* the YAML schema configuration, where the nested "diagram" and
|
|
200
|
+
"format" structures are detailed in the "describe" output */
|
|
201
|
+
export type Schema = SchemaObject[]
|
|
202
|
+
export type SchemaObject = {
|
|
203
|
+
kind: string
|
|
204
|
+
name?: string
|
|
205
|
+
id?: string
|
|
206
|
+
file?: string
|
|
207
|
+
desc?: string
|
|
208
|
+
optional?: boolean
|
|
209
|
+
diagram?: SchemaDiagram
|
|
210
|
+
format?: SchemaFormat
|
|
211
|
+
props?: SchemaProperty[]
|
|
212
|
+
childs?: SchemaObject[]
|
|
213
|
+
}
|
|
214
|
+
export type SchemaProperty = {
|
|
215
|
+
name: string
|
|
216
|
+
desc?: string
|
|
217
|
+
value?: string
|
|
218
|
+
optional?: boolean
|
|
219
|
+
}
|
|
108
220
|
```
|
|
109
221
|
|
|
110
|
-
|
|
111
|
-
standard schema configuration `specbook-format.yaml`) determines the
|
|
112
|
-
specification: exactly the artifact files its `file` fields reference
|
|
113
|
-
are loaded and parsed; all other Markdown files below the base
|
|
114
|
-
directory are ignored. A referenced
|
|
115
|
-
file which is absent is reported, unless all of its artifacts are
|
|
116
|
-
`optional`, and so is an artifact absent from its present file, unless
|
|
117
|
-
it is `optional` itself. Both `lint` and `export` report all
|
|
118
|
-
diagnostics and fail on any of them, so a partial or invalid
|
|
119
|
-
specification is never exported.
|
|
120
|
-
|
|
121
|
-
The base directory `-b`/`--basedir` (default: `.`) is the directory the
|
|
122
|
-
referenced artifact files are resolved against, and generated
|
|
123
|
-
specification Markdown files are placed inside it, too.
|
|
124
|
-
|
|
125
|
-
The export output option `-o`/`--output` (default: `-` for stdout) can
|
|
126
|
-
occur multiple times. The format (`json`, `json5`, `yaml`, `toon`,
|
|
127
|
-
`html`, `pdf`, or `md`) is inferred from the filename extension, unless
|
|
128
|
-
it is explicitly given as a `<format>:` prefix, and plain `-` (stdout)
|
|
129
|
-
defaults to JSON.
|
|
130
|
-
|
|
131
|
-
The `md` format normalizes the entire corpus into a *single* Markdown
|
|
132
|
-
document, whose single frontmatter block carries the earliest `Created:`
|
|
133
|
-
and the latest `Modified:` timestamp of all artifacts, so the
|
|
134
|
-
per-artifact timestamps do not survive this round-trip.
|
|
135
|
-
|
|
136
|
-
The `describe` command outputs the description of the SpecBook models
|
|
137
|
-
and formats. If the configuration file `-c`/`--config` or the base
|
|
138
|
-
directory `-b`/`--basedir` is given, it additionally appends a *SpecBook
|
|
139
|
-
Project Instantiation* section which points to these artifacts of the
|
|
140
|
-
particular project. With `-e`/`--embed` the YAML schema configuration is
|
|
141
|
-
embedded verbatim instead of just being referenced, so the resulting
|
|
142
|
-
document describes the specification format of the project entirely on
|
|
143
|
-
its own.
|
|
144
|
-
|
|
145
|
-
The default value of every CLI option `--xxx` can be overridden by a
|
|
146
|
-
corresponding `SPECBOOK_XXX` environment variable (e.g.
|
|
147
|
-
`SPECBOOK_BASEDIR`, `SPECBOOK_CONFIG`, `SPECBOOK_OUTPUT`,
|
|
148
|
-
`SPECBOOK_VERBOSE`), while an explicitly supplied option always wins.
|
|
149
|
-
|
|
150
|
-
### API
|
|
222
|
+
Example:
|
|
151
223
|
|
|
152
224
|
```ts
|
|
153
|
-
import { SpecBook
|
|
225
|
+
import { SpecBook } from "@rse/specbook"
|
|
154
226
|
|
|
155
227
|
const specbook = new SpecBook({
|
|
156
|
-
verbose: (cmd, msg) => console.error(`specbook: ${cmd}: ${
|
|
228
|
+
verbose: (cmd, msg) => console.error(`specbook: ${cmd}: ${msg}`)
|
|
157
229
|
})
|
|
158
230
|
const result = await specbook.lint({
|
|
159
231
|
basedir: "smp/broadcast"
|
|
160
232
|
})
|
|
161
233
|
```
|
|
162
234
|
|
|
163
|
-
###
|
|
235
|
+
### CLI
|
|
236
|
+
|
|
237
|
+
Commands:
|
|
164
238
|
|
|
165
|
-
|
|
166
|
-
|
|
239
|
+
```bash
|
|
240
|
+
$ specbook init \
|
|
241
|
+
[-v|--verbose] \
|
|
242
|
+
[-c|--config <schema-yaml-file>] \
|
|
243
|
+
[-b|--basedir <spec-md-file-basedir>]
|
|
244
|
+
|
|
245
|
+
$ specbook lint \
|
|
246
|
+
[-v|--verbose] \
|
|
247
|
+
[-c|--config <schema-yaml-file>] \
|
|
248
|
+
[-b|--basedir <spec-md-file-basedir>]
|
|
249
|
+
|
|
250
|
+
$ specbook export \
|
|
251
|
+
[-v|--verbose] \
|
|
252
|
+
[-c|--config <schema-yaml-file>] \
|
|
253
|
+
[-b|--basedir <spec-md-file-basedir>] \
|
|
254
|
+
[-o|--output [<format>:]<output-file>] [...]
|
|
255
|
+
|
|
256
|
+
$ specbook describe \
|
|
257
|
+
[-v|--verbose] \
|
|
258
|
+
[-c|--config <schema-yaml-file>] \
|
|
259
|
+
[-b|--basedir <spec-md-file-basedir>] \
|
|
260
|
+
[-e|--embed] \
|
|
261
|
+
[-f|--format <format>] \
|
|
262
|
+
[-p|--part <part>] \
|
|
263
|
+
[-o|--output <output-file>]
|
|
264
|
+
|
|
265
|
+
$ specbook mcp \
|
|
266
|
+
[-v|--verbose]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Options:
|
|
270
|
+
|
|
271
|
+
- `-v|--verbose`:
|
|
272
|
+
Enable verbose logging of processing information to `stderr`.
|
|
273
|
+
|
|
274
|
+
- `-c|--config <schema-yaml-file>`:
|
|
275
|
+
The YAML schema configuration (default: the bundled standard schema
|
|
276
|
+
configuration `specbook-format.yaml`) determines the specification:
|
|
277
|
+
exactly the artifact files its `file` fields reference are loaded and
|
|
278
|
+
parsed; all other Markdown files below the base directory are ignored.
|
|
279
|
+
A referenced file which is absent is reported, unless all of its
|
|
280
|
+
artifacts are `optional`, and so is an artifact absent from its present
|
|
281
|
+
file, unless it is `optional` itself. Both `lint` and `export` report
|
|
282
|
+
all diagnostics and fail on any of them, so a partial or invalid
|
|
283
|
+
specification is never exported.
|
|
284
|
+
|
|
285
|
+
- `-b|--basedir <spec-md-file-basedir>`:
|
|
286
|
+
The base directory (default: `.`) is the directory the referenced
|
|
287
|
+
artifact files are resolved against, and generated specification
|
|
288
|
+
Markdown files are placed inside it, too.
|
|
289
|
+
|
|
290
|
+
- `-o|--output [<format>:]<output-file>` (`export` only):
|
|
291
|
+
The output file (default: `-` for stdout) can be given multiple times.
|
|
292
|
+
The format (`json`, `json5`, `yaml`, `toon`, `html`, `pdf`, or `md`)
|
|
293
|
+
is inferred from the filename extension, unless it is explicitly given
|
|
294
|
+
as a `<format>:` prefix, and plain `-` (stdout) defaults to JSON.
|
|
295
|
+
The `md` export format normalizes the entire corpus into a *single*
|
|
296
|
+
Markdown document.
|
|
297
|
+
|
|
298
|
+
- `-o|--output <output-file>` (`describe` only):
|
|
299
|
+
The output file (default: `-` for stdout) receives the described
|
|
300
|
+
Markdown document.
|
|
301
|
+
|
|
302
|
+
- `-e|--embed` (`describe` only):
|
|
303
|
+
Embed the given YAML schema configuration verbatim instead of just
|
|
304
|
+
referencing it, so the resulting document describes the specification
|
|
305
|
+
format entirely on its own.
|
|
306
|
+
|
|
307
|
+
- `-f|--format <format>` (`describe` only):
|
|
308
|
+
The output format (default: `md`) switches from the rendered Markdown
|
|
309
|
+
onto the raw original file content with `raw`, which is available for
|
|
310
|
+
the file-backed parts `meta` and `schema` only.
|
|
311
|
+
|
|
312
|
+
- `-p|--part <part>` (`describe` only):
|
|
313
|
+
The document part (default: `all`) reduces the output to a single part:
|
|
314
|
+
`meta` for the description of the generic SpecBook models and formats,
|
|
315
|
+
`schema` for the YAML schema configuration (the given one, referenced
|
|
316
|
+
or embedded with `-e|--embed`, else the bundled standard one,
|
|
317
|
+
embedded), or `spec` for the reference to the base directory.
|
|
318
|
+
|
|
319
|
+
The default value of every CLI option `--xxx` can be overridden
|
|
320
|
+
by a corresponding `SPECBOOK_XXX` environment variable (e.g.
|
|
321
|
+
`SPECBOOK_BASEDIR`, `SPECBOOK_CONFIG`, `SPECBOOK_OUTPUT`,
|
|
322
|
+
`SPECBOOK_VERBOSE`), while an explicitly supplied option always wins.
|
|
323
|
+
|
|
324
|
+
Example:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
$ specbook lint -v -b smp/broadcast
|
|
328
|
+
```
|
|
167
329
|
|
|
168
330
|
Rendering Examples
|
|
169
331
|
------------------
|
package/dst/specbook-api.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic } from "./specbook-diagnostic.js";
|
|
2
2
|
import { type LintResult } from "./specbook-cmd-lint.js";
|
|
3
3
|
import { parseOutputSpec, formats, type ExportFormat } from "./specbook-cmd-export.js";
|
|
4
|
+
import { describeFormats, describeParts, parseDescribeFormat, parseDescribePart, type DescribeFormat, type DescribePart } from "./specbook-cmd-describe.js";
|
|
4
5
|
import { literal, renderVerbose } from "./specbook-verbose.js";
|
|
5
6
|
export { literal, renderVerbose };
|
|
6
7
|
export { formats, parseOutputSpec, type ExportFormat };
|
|
8
|
+
export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
|
|
9
|
+
export { type DescribeFormat, type DescribePart };
|
|
7
10
|
export { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic };
|
|
8
11
|
export { type LintResult };
|
|
9
12
|
export type { Spec, SpecArtifact, SpecObject, SpecDescription, SpecProperty } from "./specbook-format-spec.js";
|
|
@@ -37,5 +40,7 @@ export declare class SpecBook {
|
|
|
37
40
|
config?: string;
|
|
38
41
|
basedir?: string;
|
|
39
42
|
embed?: boolean;
|
|
43
|
+
format?: DescribeFormat;
|
|
44
|
+
part?: DescribePart;
|
|
40
45
|
}): Promise<string>;
|
|
41
46
|
}
|
package/dst/specbook-api.js
CHANGED
|
@@ -10,11 +10,12 @@ import { renderDiagnostic, renderDiagnosticVerbose } from "./specbook-diagnostic
|
|
|
10
10
|
import { initSpecification } from "./specbook-cmd-init.js";
|
|
11
11
|
import { lint } from "./specbook-cmd-lint.js";
|
|
12
12
|
import { exportSpecification, parseOutputSpec, formats } from "./specbook-cmd-export.js";
|
|
13
|
-
import { describeFormat } from "./specbook-cmd-describe.js";
|
|
13
|
+
import { describeFormat, describeFormats, describeParts, parseDescribeFormat, parseDescribePart } from "./specbook-cmd-describe.js";
|
|
14
14
|
import { literal, renderVerbose } from "./specbook-verbose.js";
|
|
15
15
|
/* re-export the central types for API consumers */
|
|
16
16
|
export { literal, renderVerbose };
|
|
17
17
|
export { formats, parseOutputSpec };
|
|
18
|
+
export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
|
|
18
19
|
export { renderDiagnostic, renderDiagnosticVerbose };
|
|
19
20
|
/* our own version, taken from the package manifest, which resides one
|
|
20
21
|
level above both the source and the compiled module directory */
|
|
@@ -79,16 +80,27 @@ export class SpecBook {
|
|
|
79
80
|
buffers.push(await exportSpecification(result.specification, format, verbose, result.config));
|
|
80
81
|
return buffers;
|
|
81
82
|
}
|
|
82
|
-
/* describe the generic SpecBook models and formats as Markdown
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
/* describe the generic SpecBook models and formats as Markdown (or
|
|
84
|
+
as the raw original file content), either entirely or reduced to
|
|
85
|
+
a single part, optionally pointing to the artifacts of the
|
|
86
|
+
particular project, whose YAML schema configuration is validated
|
|
87
|
+
before use */
|
|
85
88
|
async describe(options) {
|
|
86
89
|
const verbose = this.verboseOf("describe");
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const format = options.format ?? "md";
|
|
91
|
+
const part = options.part ?? "all";
|
|
92
|
+
/* the schema-bearing parts require a schema configuration, so
|
|
93
|
+
they fall back onto the bundled standard one, which is always
|
|
94
|
+
embedded, as its bundled file is no meaningful reference */
|
|
95
|
+
const standard = options.config === undefined
|
|
96
|
+
&& (part === "all" || part === "schema" || options.embed === true);
|
|
97
|
+
const config = options.config ?? (standard ? standardConfig : undefined);
|
|
98
|
+
const embed = options.embed === true || standard;
|
|
99
|
+
const basedir = options.basedir ?? (part === "spec" ? "." : undefined);
|
|
100
|
+
if (config !== undefined)
|
|
101
|
+
this.requireConfig(config, verbose);
|
|
102
|
+
verbose(`describing the "${literal(part)}" part of the SpecBook models and formats ` +
|
|
103
|
+
`in the "${literal(format)}" format`);
|
|
104
|
+
return describeFormat({ ...options, config, basedir, embed, standard, format, part });
|
|
93
105
|
}
|
|
94
106
|
}
|
package/dst/specbook-cli.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import chalk from "chalk";
|
|
10
|
-
import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, renderVerbose, literal, parseOutputSpec, version } from "./specbook-api.js";
|
|
10
|
+
import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, renderVerbose, literal, parseOutputSpec, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, version } from "./specbook-api.js";
|
|
11
11
|
import { serveMcp } from "./specbook-mcp.js";
|
|
12
12
|
/* route verbose messages to stderr, keeping stdout reserved for the
|
|
13
13
|
command outputs and the MCP protocol, and qualify every message with
|
|
@@ -120,13 +120,17 @@ withCommonOptions(program.command("export"))
|
|
|
120
120
|
formats alone, so its YAML schema configuration stays optional */
|
|
121
121
|
withVerboseOption(program.command("describe"))
|
|
122
122
|
.description("describe the SpecBook models and formats as Markdown")
|
|
123
|
-
.option("-c, --config <yaml-file>", "YAML schema configuration file"
|
|
123
|
+
.option("-c, --config <yaml-file>", "YAML schema configuration file " +
|
|
124
|
+
"(default: the bundled standard schema configuration, embedded verbatim)", envDefault("config"))
|
|
124
125
|
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir"))
|
|
125
|
-
.option("-e, --embed", "embed the YAML schema configuration instead of just referencing it", envDefaultFlag("embed", false))
|
|
126
|
+
.option("-e, --embed", "embed the given YAML schema configuration instead of just referencing it", envDefaultFlag("embed", false))
|
|
127
|
+
.option("-f, --format <format>", `output format (${describeFormats.join(", ")})`, envDefault("format", "md"))
|
|
128
|
+
.option("-p, --part <part>", `document part (${describeParts.join(", ")})`, envDefault("part", "all"))
|
|
126
129
|
.option("-o, --output <markdown-file>", "output file (\"-\" for stdout)", envDefault("output", "-"))
|
|
127
130
|
.action(async (opts) => {
|
|
128
131
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
129
|
-
const text = await specbook.describe({ config: opts.config, basedir: opts.basedir,
|
|
132
|
+
const text = await specbook.describe({ config: opts.config, basedir: opts.basedir,
|
|
133
|
+
embed: opts.embed, format: parseDescribeFormat(opts.format), part: parseDescribePart(opts.part) });
|
|
130
134
|
await writeOutput(opts.output, text, "describe", verboseOf(opts));
|
|
131
135
|
});
|
|
132
136
|
/* run the command line program */
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
export declare const describeFormats: readonly ["md", "raw"];
|
|
2
|
+
export declare const describeParts: readonly ["all", "meta", "schema", "spec"];
|
|
3
|
+
export type DescribeFormat = typeof describeFormats[number];
|
|
4
|
+
export type DescribePart = typeof describeParts[number];
|
|
5
|
+
export declare const parseDescribeFormat: (value: string) => DescribeFormat;
|
|
6
|
+
export declare const parseDescribePart: (value: string) => DescribePart;
|
|
1
7
|
export declare const describeFormat: (options: {
|
|
2
8
|
config?: string;
|
|
3
9
|
basedir?: string;
|
|
4
10
|
embed?: boolean;
|
|
11
|
+
standard?: boolean;
|
|
12
|
+
format?: DescribeFormat;
|
|
13
|
+
part?: DescribePart;
|
|
5
14
|
}) => string;
|
|
@@ -5,31 +5,94 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import textframe from "textframe";
|
|
8
|
+
/* the supported description output formats and document parts */
|
|
9
|
+
export const describeFormats = ["md", "raw"];
|
|
10
|
+
export const describeParts = ["all", "meta", "schema", "spec"];
|
|
11
|
+
/* parse and validate an output format or document part specification */
|
|
12
|
+
const parseChoice = (choices, kind, value) => {
|
|
13
|
+
if (!choices.includes(value))
|
|
14
|
+
throw new Error(`unknown describe ${kind} "${value}" ` +
|
|
15
|
+
`(supported: ${choices.join(", ")})`);
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
18
|
+
export const parseDescribeFormat = (value) => parseChoice(describeFormats, "format", value);
|
|
19
|
+
export const parseDescribePart = (value) => parseChoice(describeParts, "part", value);
|
|
8
20
|
/* provide the build-time bundled description of the generic
|
|
9
21
|
SpecBook models and formats */
|
|
10
22
|
const description = () => fs.readFileSync(new URL("specbook-format.md", import.meta.url), "utf8");
|
|
23
|
+
/* render the reference to (or the embedding of) the YAML schema configuration,
|
|
24
|
+
where the bundled standard one is embedded without its leading comment block */
|
|
25
|
+
const schemaSection = (config, embed, standard) => {
|
|
26
|
+
if (!embed)
|
|
27
|
+
return textframe(`
|
|
28
|
+
The **SpecBook SCHEMA Model** is the YAML schema configuration in file:
|
|
29
|
+
@${config}
|
|
30
|
+
`);
|
|
31
|
+
let yaml = fs.readFileSync(config, "utf8");
|
|
32
|
+
if (standard)
|
|
33
|
+
yaml = yaml.replace(/^(?:[ \t]*##.*\n)+\s*/, "");
|
|
34
|
+
return textframe(`
|
|
35
|
+
The **SpecBook SCHEMA Model** is the following YAML schema configuration:
|
|
36
|
+
`) + "\n```yaml\n" + yaml.replace(/\n*$/, "\n") + "```\n\n";
|
|
37
|
+
};
|
|
38
|
+
/* render the reference to the specification Markdown files */
|
|
39
|
+
const specSection = (basedir) => textframe(`
|
|
40
|
+
The **SpecBook SPEC Model** is the set of Markdown specification files in directory:
|
|
41
|
+
@${basedir}
|
|
42
|
+
`) + "\n";
|
|
11
43
|
/* describe the generic SpecBook models and formats as Markdown,
|
|
12
44
|
optionally extended by the references to (or the embeddings of) the
|
|
13
|
-
artifacts of the particular project instantiation
|
|
45
|
+
artifacts of the particular project instantiation, and optionally
|
|
46
|
+
reduced to a single part or to the raw original file content, where
|
|
47
|
+
"standard" marks the configuration as the bundled standard one,
|
|
48
|
+
which still belongs to the generic description */
|
|
14
49
|
export const describeFormat = (options) => {
|
|
50
|
+
const format = options.format ?? "md";
|
|
51
|
+
const part = options.part ?? "all";
|
|
52
|
+
if (!describeFormats.includes(format))
|
|
53
|
+
throw new Error(`unknown describe format "${format}"`);
|
|
54
|
+
if (!describeParts.includes(part))
|
|
55
|
+
throw new Error(`unknown describe part "${part}"`);
|
|
56
|
+
/* the raw format emits the original file content of a single part,
|
|
57
|
+
so it is available for the file-backed parts only */
|
|
58
|
+
if (format === "raw") {
|
|
59
|
+
if (part === "all" || part === "spec")
|
|
60
|
+
throw new Error(`format "raw" is not available for part "${part}", ` +
|
|
61
|
+
"as this part is not backed by a single original file");
|
|
62
|
+
else if (part === "meta")
|
|
63
|
+
return description();
|
|
64
|
+
else if (options.config === undefined)
|
|
65
|
+
throw new Error("format \"raw\" for part \"schema\" requires a YAML schema configuration");
|
|
66
|
+
else
|
|
67
|
+
return fs.readFileSync(options.config, "utf8");
|
|
68
|
+
}
|
|
69
|
+
/* the md format renders a single part standalone */
|
|
70
|
+
if (part === "meta")
|
|
71
|
+
return description();
|
|
72
|
+
else if (part === "schema") {
|
|
73
|
+
if (options.config === undefined)
|
|
74
|
+
throw new Error("part \"schema\" requires a YAML schema configuration");
|
|
75
|
+
return schemaSection(options.config, options.embed === true, options.standard === true);
|
|
76
|
+
}
|
|
77
|
+
else if (part === "spec") {
|
|
78
|
+
if (options.basedir === undefined)
|
|
79
|
+
throw new Error("part \"spec\" requires a base directory");
|
|
80
|
+
return specSection(options.basedir);
|
|
81
|
+
}
|
|
82
|
+
/* the md format renders all parts as a single coherent document,
|
|
83
|
+
where the schema configuration and the base directory are the
|
|
84
|
+
artifacts of the project instantiation and hence are always
|
|
85
|
+
placed under their own section */
|
|
15
86
|
const sections = [description()];
|
|
16
|
-
|
|
87
|
+
const schema = options.config !== undefined ?
|
|
88
|
+
schemaSection(options.config, options.embed === true, options.standard === true) : undefined;
|
|
89
|
+
if (schema !== undefined || options.basedir !== undefined) {
|
|
17
90
|
sections.push("SpecBook Project Instantiation\n" +
|
|
18
91
|
"==============================\n");
|
|
19
|
-
if (
|
|
20
|
-
sections.push(
|
|
21
|
-
The **SpecBook SCHEMA Model** is the following YAML schema configuration:
|
|
22
|
-
`) + "\n```yaml\n" +
|
|
23
|
-
fs.readFileSync(options.config, "utf8").replace(/\n*$/, "\n") +
|
|
24
|
-
"```\n");
|
|
25
|
-
else if (options.config !== undefined)
|
|
26
|
-
sections.push(textframe(`
|
|
27
|
-
The **SpecBook SCHEMA Model** is the YAML schema configuration in file \`${options.config}\`.
|
|
28
|
-
`));
|
|
92
|
+
if (schema !== undefined)
|
|
93
|
+
sections.push(schema);
|
|
29
94
|
if (options.basedir !== undefined)
|
|
30
|
-
sections.push(
|
|
31
|
-
The **SpecBook SPEC Model** is the set of Markdown specification files in directory \`${options.basedir}\`.
|
|
32
|
-
`));
|
|
95
|
+
sections.push(specSection(options.basedir));
|
|
33
96
|
}
|
|
34
97
|
return sections.join("\n");
|
|
35
98
|
};
|
|
@@ -249,7 +249,7 @@ const inline = (text) => safe(marked.parseInline(linker !== null ? linker(text)
|
|
|
249
249
|
constructs (lists, quotes, code blocks, tables) intact */
|
|
250
250
|
const block = (text) => safe(marked.parse(linker !== null ? linker(text) : text, { async: false }));
|
|
251
251
|
/* check whether a text carries any block-level Markdown, i.e. is
|
|
252
|
-
anything
|
|
252
|
+
anything other than the single paragraph a description usually is */
|
|
253
253
|
const isBlock = (text) => {
|
|
254
254
|
const tokens = marked.lexer(text).filter((token) => token.type !== "space");
|
|
255
255
|
return tokens.length > 0 && !(tokens.length === 1 && tokens[0].type === "paragraph");
|
|
@@ -66,7 +66,7 @@ const renderConciseMd = (object, depth = 0) => {
|
|
|
66
66
|
level 4 upwards in the Concise Format -- unless a descendant carries
|
|
67
67
|
a description no concise item can carry, so all childs (as siblings
|
|
68
68
|
have to share the format) stay in the Complex Format down to heading
|
|
69
|
-
level 6
|
|
69
|
+
level 6 -- and the optionally derived Gradia diagram spec embedded
|
|
70
70
|
as a "gradia" fenced code block below the heading */
|
|
71
71
|
const renderObjectMd = (object, level, diagrams) => {
|
|
72
72
|
const heading = `${"#".repeat(level)}${" ".repeat(Math.max(1, 4 - level))}` +
|
package/dst/specbook-format.md
CHANGED
|
@@ -19,7 +19,7 @@ SpecBook Models and Formats
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
**SpecBook** deals with four distinct object models and their particular
|
|
22
|
-
|
|
22
|
+
formats, each represented in a formal language and serving a particular
|
|
23
23
|
task:
|
|
24
24
|
|
|
25
25
|
- **SpecBook SCHEMA Meta Model**:
|
package/dst/specbook-mcp.js
CHANGED
|
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import { SpecBook, renderDiagnostic, formats, parseOutputSpec, version } from "./specbook-api.js";
|
|
10
|
+
import { SpecBook, renderDiagnostic, formats, parseOutputSpec, describeFormats, describeParts, version } from "./specbook-api.js";
|
|
11
11
|
/* render an error with its cause chain into a tool error result */
|
|
12
12
|
const errorResult = (err) => {
|
|
13
13
|
let msg = err instanceof Error ? err.message : String(err);
|
|
@@ -105,14 +105,23 @@ export const serveMcp = async (verbose) => {
|
|
|
105
105
|
});
|
|
106
106
|
server.registerTool("specbook_describe", {
|
|
107
107
|
title: "Describe Specification Format",
|
|
108
|
-
description: "Describe the SpecBook models and formats as a Markdown document
|
|
109
|
-
"schema configuration
|
|
110
|
-
"
|
|
108
|
+
description: "Describe the SpecBook models and formats as a Markdown document, which embeds the " +
|
|
109
|
+
"bundled standard YAML schema configuration as long as no particular one is given. If the " +
|
|
110
|
+
"YAML schema configuration or the base directory is given, the description additionally " +
|
|
111
|
+
"points to the artifacts of that particular project. The description can be reduced to a " +
|
|
112
|
+
"single part and emitted as the raw original file content instead of Markdown.",
|
|
111
113
|
inputSchema: {
|
|
112
|
-
config: z.string().optional().describe("YAML schema configuration file"
|
|
114
|
+
config: z.string().optional().describe("YAML schema configuration file " +
|
|
115
|
+
"(default: the bundled standard schema configuration, embedded verbatim)"),
|
|
113
116
|
basedir: z.string().optional().describe("base directory of the specification Markdown files"),
|
|
114
|
-
embed: z.boolean().optional().describe("embed the YAML schema configuration instead
|
|
115
|
-
"just referencing it (default: false)"),
|
|
117
|
+
embed: z.boolean().optional().describe("embed the given YAML schema configuration instead " +
|
|
118
|
+
"of just referencing it (default: false; the bundled standard one is always embedded)"),
|
|
119
|
+
format: z.enum(describeFormats).optional().describe("output format: \"md\" for Markdown or " +
|
|
120
|
+
"\"raw\" for the raw original file content of the part (default: \"md\")"),
|
|
121
|
+
part: z.enum(describeParts).optional().describe("document part: \"all\" for the entire " +
|
|
122
|
+
"description, \"meta\" for the generic models and formats, \"schema\" for the YAML schema " +
|
|
123
|
+
"configuration, or \"spec\" for the specification files (default: \"all\"; \"raw\" is " +
|
|
124
|
+
"available for \"meta\" and \"schema\" only)"),
|
|
116
125
|
output: z.string().optional().describe("output file path (\"-\" or omitted returns the description directly)")
|
|
117
126
|
}
|
|
118
127
|
}, async (args) => {
|