@rse/specbook 1.0.0 → 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 +19 -8
- 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
|
@@ -103,7 +103,7 @@ Usage
|
|
|
103
103
|
$ specbook init [-v] [-c <yaml-file>] [-b <basedir>]
|
|
104
104
|
$ specbook lint [-v] [-c <yaml-file>] [-b <basedir>]
|
|
105
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>]
|
|
106
|
+
$ specbook describe [-v] [-c <yaml-file>] [-b <basedir>] [-e] [-f <format>] [-p <part>] [-o <markdown-file>]
|
|
107
107
|
$ specbook mcp [-v]
|
|
108
108
|
```
|
|
109
109
|
|
|
@@ -134,13 +134,24 @@ and the latest `Modified:` timestamp of all artifacts, so the
|
|
|
134
134
|
per-artifact timestamps do not survive this round-trip.
|
|
135
135
|
|
|
136
136
|
The `describe` command outputs the description of the SpecBook models
|
|
137
|
-
and formats
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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.
|
|
144
155
|
|
|
145
156
|
The default value of every CLI option `--xxx` can be overridden by a
|
|
146
157
|
corresponding `SPECBOOK_XXX` environment variable (e.g.
|
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) => {
|