@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 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
- - **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.
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
- ### CLI
100
+ ### API
101
101
 
102
- ```bash
103
- $ specbook init [-v] [-c <yaml-file>] [-b <basedir>]
104
- $ specbook lint [-v] [-c <yaml-file>] [-b <basedir>]
105
- $ specbook export [-v] [-c <yaml-file>] [-b <basedir>] [-o [<format>:]<output-file>] [...]
106
- $ specbook describe [-v] [-c <yaml-file>] [-b <basedir>] [-e] [-o <markdown-file>]
107
- $ specbook mcp [-v]
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
- The YAML schema configuration `-c`/`--config` (default: the bundled
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, renderVerbose } from "@rse/specbook"
225
+ import { SpecBook } from "@rse/specbook"
154
226
 
155
227
  const specbook = new SpecBook({
156
- verbose: (cmd, msg) => console.error(`specbook: ${cmd}: ${renderVerbose(msg)}`)
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
- ### MCP
235
+ ### CLI
236
+
237
+ Commands:
164
238
 
165
- Run `specbook mcp` to serve the tools `specbook_init`, `specbook_lint`,
166
- `specbook_export`, and `specbook_describe` over stdio.
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
  ------------------
@@ -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
  }
@@ -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
- optionally pointing to the artifacts of the particular project,
84
- whose YAML schema configuration is validated if given */
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
- if (options.config !== undefined)
88
- this.requireConfig(options.config, verbose);
89
- else if (options.embed === true)
90
- throw new Error("YAML schema configuration required for embedding");
91
- verbose("describing the SpecBook models and formats");
92
- return describeFormat(options);
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
  }
@@ -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", envDefault("config"))
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, embed: opts.embed });
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
- if (options.config !== undefined || options.basedir !== undefined) {
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 (options.config !== undefined && options.embed === true)
20
- sections.push(textframe(`
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(textframe(`
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 else than the single paragraph a description usually is */
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 --, and the optionally derived Gradia diagram spec embedded
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))}` +
@@ -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
- format, each represented in a formal language and serving a particular
22
+ formats, each represented in a formal language and serving a particular
23
23
  task:
24
24
 
25
25
  - **SpecBook SCHEMA Meta Model**:
@@ -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. If the YAML " +
109
- "schema configuration or the base directory is given, the description additionally points " +
110
- "to the artifacts of that particular project.",
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 of " +
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rse/specbook",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "Markdown-based Specification Format (API, CLI and MCP service)",
6
6
  "license": "Apache-2.0",