@rse/specbook 0.9.9 → 1.0.1

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
@@ -31,7 +31,7 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
31
31
  - **Versatile Object to Markdown Mapping**:
32
32
  Objects are authored as plain Markdown headings, property lists,
33
33
  and description prose. The objects can be mapped to nested sections
34
- ("complex" format) or a compact bullet point lists ("concise" and
34
+ ("complex" format) or compact bullet point lists ("concise" and
35
35
  "group" format).
36
36
 
37
37
  - **Wiki-Style Object Linking**:
@@ -52,7 +52,7 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
52
52
  schema, whose nodes and edges are automatically derived from the
53
53
  object model and its references. The diagrams are rendered with the
54
54
  sibling project [Gradia](https://github.com/rse/gradia), which is
55
- specialized on rendering object models.
55
+ specialized in rendering object models.
56
56
 
57
57
  - **CLI, MCP, and API Interface**:
58
58
  All commands are implemented once in the API class `SpecBook`, on
@@ -80,45 +80,37 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
80
80
  It is intended as a polished, paginated offline document for handing
81
81
  over to customers.
82
82
 
83
- Overview Poster
84
- ---------------
83
+ Overview
84
+ --------
85
85
 
86
86
  [![poster-1](etc/poster-1.png)](etc/poster-1.pdf)
87
87
 
88
88
  [![poster-2](etc/poster-2.png)](etc/poster-2.pdf)
89
89
 
90
- Rendering Example
91
- -----------------
90
+ Installation
91
+ ------------
92
92
 
93
- ### HTML Rendering (Light Theme)
94
-
95
- ![screenshot-light](etc/screenshot-light.png)
96
-
97
- ### HTML Rendering (Dark Theme)
98
-
99
- ![screenshot-dark](etc/screenshot-dark.png)
100
-
101
- ### PDF Rendering
102
-
103
- ![screenshot-print](etc/screenshot-print.png)
93
+ ```bash
94
+ $ npm install -g @rse/specbook
95
+ ```
104
96
 
105
97
  Usage
106
98
  -----
107
99
 
108
100
  ### CLI
109
101
 
110
- ```
102
+ ```bash
111
103
  $ specbook init [-v] [-c <yaml-file>] [-b <basedir>]
112
104
  $ specbook lint [-v] [-c <yaml-file>] [-b <basedir>]
113
105
  $ specbook export [-v] [-c <yaml-file>] [-b <basedir>] [-o [<format>:]<output-file>] [...]
114
- $ specbook describe [-v] [-c <yaml-file>] [-b <basedir>] [-e] [-o <markdown-file>]
106
+ $ specbook describe [-v] [-c <yaml-file>] [-b <basedir>] [-e] [-f <format>] [-p <part>] [-o <markdown-file>]
115
107
  $ specbook mcp [-v]
116
108
  ```
117
109
 
118
110
  The YAML schema configuration `-c`/`--config` (default: the bundled
119
111
  standard schema configuration `specbook-format.yaml`) determines the
120
112
  specification: exactly the artifact files its `file` fields reference
121
- are loaded and parsed, all other Markdown files below the base
113
+ are loaded and parsed; all other Markdown files below the base
122
114
  directory are ignored. A referenced
123
115
  file which is absent is reported, unless all of its artifacts are
124
116
  `optional`, and so is an artifact absent from its present file, unless
@@ -142,13 +134,24 @@ and the latest `Modified:` timestamp of all artifacts, so the
142
134
  per-artifact timestamps do not survive this round-trip.
143
135
 
144
136
  The `describe` command outputs the description of the SpecBook models
145
- and formats. If the configuration file `-c`/`--config` or the base
146
- directory `-b`/`--basedir` is given, it additionally appends a *SpecBook
147
- Project Instantiation* section which points to these artifacts of the
148
- particular project. With `-e`/`--embed` the YAML schema configuration is
149
- embedded verbatim instead of just being referenced, so the resulting
150
- document describes the specification format of the project entirely on
151
- its own.
137
+ and formats, extended by a *SpecBook Project Instantiation* section which
138
+ points to the YAML schema configuration `-c`/`--config` and the base
139
+ directory `-b`/`--basedir`. The schema configuration falls back onto the
140
+ bundled standard one, embedded verbatim, as long as no particular
141
+ configuration file `-c`/`--config` is given. With `-e`/`--embed` the
142
+ given YAML schema configuration is embedded verbatim instead of just
143
+ being referenced, so the resulting document describes the specification
144
+ format entirely on its own.
145
+
146
+ The `describe` document part `-p`/`--part` (default: `all`) reduces the
147
+ output to a single part: `meta` for the description of the generic
148
+ SpecBook models and formats, `schema` for the YAML schema configuration
149
+ (the given one, referenced or embedded with `-e`/`--embed`, else the
150
+ bundled standard one, embedded), or `spec` for the reference to
151
+ the base directory. The output format `-f`/`--format` (default: `md`)
152
+ switches from this rendered Markdown onto the raw original file content
153
+ with `raw`, which is available for the file-backed parts `meta` and
154
+ `schema` only.
152
155
 
153
156
  The default value of every CLI option `--xxx` can be overridden by a
154
157
  corresponding `SPECBOOK_XXX` environment variable (e.g.
@@ -173,6 +176,21 @@ const result = await specbook.lint({
173
176
  Run `specbook mcp` to serve the tools `specbook_init`, `specbook_lint`,
174
177
  `specbook_export`, and `specbook_describe` over stdio.
175
178
 
179
+ Rendering Examples
180
+ ------------------
181
+
182
+ ### HTML Rendering (Light Theme)
183
+
184
+ ![screenshot-light](etc/screenshot-light.png)
185
+
186
+ ### HTML Rendering (Dark Theme)
187
+
188
+ ![screenshot-dark](etc/screenshot-dark.png)
189
+
190
+ ### PDF Rendering
191
+
192
+ ![screenshot-print](etc/screenshot-print.png)
193
+
176
194
  License
177
195
  -------
178
196
 
@@ -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": "0.9.9",
3
+ "version": "1.0.1",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "Markdown-based Specification Format (API, CLI and MCP service)",
6
6
  "license": "Apache-2.0",