@rse/specbook 0.9.6 → 0.9.8
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 +65 -10
- package/dst/specbook-api.js +19 -18
- package/dst/specbook-cli.js +9 -6
- package/dst/specbook-cmd-describe.js +1 -3
- package/dst/specbook-cmd-export.js +24 -17
- package/dst/specbook-cmd-init.d.ts +1 -1
- package/dst/specbook-cmd-init.js +5 -4
- package/dst/specbook-cmd-lint.d.ts +0 -1
- package/dst/specbook-cmd-lint.js +29 -5
- package/dst/specbook-config.d.ts +1 -1
- package/dst/specbook-config.js +90 -71
- package/dst/specbook-diagnostic.js +4 -1
- package/dst/specbook-diagram.d.ts +8 -3
- package/dst/specbook-diagram.js +101 -105
- package/dst/specbook-export-ast.d.ts +1 -1
- package/dst/specbook-export-ast.js +1 -1
- package/dst/specbook-export-common.d.ts +2 -1
- package/dst/specbook-export-common.js +12 -14
- package/dst/specbook-export-html-search.js +10 -14
- package/dst/specbook-export-html.css +24 -29
- package/dst/specbook-export-html.js +101 -83
- package/dst/specbook-export-md.d.ts +1 -1
- package/dst/specbook-export-md.js +56 -23
- package/dst/specbook-export-pdf.d.ts +4 -2
- package/dst/specbook-export-pdf.js +48 -38
- package/dst/specbook-format-schema.js +6 -3
- package/dst/specbook-format.md +47 -20
- package/dst/specbook-link.d.ts +1 -0
- package/dst/specbook-link.js +7 -4
- package/dst/specbook-mcp.js +17 -10
- package/dst/specbook-parse-common.d.ts +2 -1
- package/dst/specbook-parse-common.js +2 -0
- package/dst/specbook-parse-semantic.d.ts +4 -2
- package/dst/specbook-parse-semantic.js +114 -43
- package/dst/specbook-parse-syntax.js +179 -137
- package/dst/specbook-parse-value.d.ts +1 -1
- package/dst/specbook-parse-value.js +9 -5
- package/dst/specbook-parse.d.ts +2 -5
- package/dst/specbook-parse.js +27 -34
- package/dst/specbook-verbose.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,15 +22,62 @@ 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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
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 a 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.
|
|
48
|
+
|
|
49
|
+
- **Object Model Diagram Visualisation**:
|
|
50
|
+
Object kinds can declare "graph", "hub", or "grid" diagrams in the
|
|
51
|
+
schema, whose nodes and edges are automatically derived from the
|
|
52
|
+
object model and its references. The diagrams are rendered with the
|
|
53
|
+
sibling project [Gradia](https://github.com/rse/gradia), which is
|
|
54
|
+
specialized on rendering object models.
|
|
55
|
+
|
|
56
|
+
- **CLI, MCP, and API Interface**:
|
|
57
|
+
All commands are implemented once in the API class `SpecBook`, on
|
|
58
|
+
which both the CLI and the MCP service are just thin wrappers.
|
|
59
|
+
This way, humans, scripts, and AI agents use exactly the same
|
|
60
|
+
functionality.
|
|
61
|
+
|
|
62
|
+
- **AST Exports for AI/LLMs**:
|
|
63
|
+
The parsed specification Abstract Syntax Tree (AST) can be exported in
|
|
64
|
+
JSON, JSON5, YAML, or TOON format for machine consumption. Together
|
|
65
|
+
with the `describe` command, which explains the models and formats,
|
|
66
|
+
this enables LLMs to both read and write specifications.
|
|
67
|
+
|
|
68
|
+
- **HTML Export for Developers**:
|
|
69
|
+
The HTML export is a self-contained single document with a
|
|
70
|
+
table-of-contents, full-text search, embedded images, and a light/dark
|
|
71
|
+
theme toggle. It is intended for the day-to-day online reading during
|
|
72
|
+
development. The HTML export can map all object kinds to nested
|
|
73
|
+
sections or compact tables, or an automatic mixture which collapses
|
|
74
|
+
only the deepest level into tables.
|
|
75
|
+
|
|
76
|
+
- **PDF Export for Customers**:
|
|
77
|
+
The PDF export prints the HTML rendering via Chromium and post-processes
|
|
78
|
+
it with page numbers, headers/footers, and a hierarchical PDF outline.
|
|
79
|
+
It is intended as a polished, paginated offline document for handing
|
|
80
|
+
over to customers.
|
|
34
81
|
|
|
35
82
|
Overview Poster
|
|
36
83
|
---------------
|
|
@@ -70,7 +117,10 @@ The YAML schema configuration `-c`/`--config` is mandatory for `init`,
|
|
|
70
117
|
artifact files its `file` fields reference are loaded and parsed, all
|
|
71
118
|
other Markdown files below the base directory are ignored. A referenced
|
|
72
119
|
file which is absent is reported, unless all of its artifacts are
|
|
73
|
-
`optional
|
|
120
|
+
`optional`, and so is an artifact absent from its present file, unless
|
|
121
|
+
it is `optional` itself. Both `lint` and `export` report all
|
|
122
|
+
diagnostics and fail on any of them, so a partial or invalid
|
|
123
|
+
specification is never exported.
|
|
74
124
|
|
|
75
125
|
The base directory `-b`/`--basedir` (default: `.`) is the directory the
|
|
76
126
|
referenced artifact files are resolved against, and generated
|
|
@@ -82,6 +132,11 @@ occur multiple times. The format (`json`, `json5`, `yaml`, `toon`,
|
|
|
82
132
|
it is explicitly given as a `<format>:` prefix, and plain `-` (stdout)
|
|
83
133
|
defaults to JSON.
|
|
84
134
|
|
|
135
|
+
The `md` format normalizes the entire corpus into a *single* Markdown
|
|
136
|
+
document, whose single frontmatter block carries the earliest `Created:`
|
|
137
|
+
and the latest `Modified:` timestamp of all artifacts, so the
|
|
138
|
+
per-artifact timestamps do not survive this round-trip.
|
|
139
|
+
|
|
85
140
|
The `describe` command outputs the description of the SpecBook models
|
|
86
141
|
and formats. If the configuration file `-c`/`--config` or the base
|
|
87
142
|
directory `-b`/`--basedir` is given, it additionally appends a *SpecBook
|
package/dst/specbook-api.js
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
|
-
import * as path from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
7
|
import { loadConfig } from "./specbook-config.js";
|
|
10
8
|
import { renderDiagnostic, renderDiagnosticVerbose } from "./specbook-diagnostic.js";
|
|
11
9
|
import { initSpecification } from "./specbook-cmd-init.js";
|
|
@@ -17,12 +15,9 @@ import { literal, renderVerbose } from "./specbook-verbose.js";
|
|
|
17
15
|
export { literal, renderVerbose };
|
|
18
16
|
export { formats, parseOutputSpec };
|
|
19
17
|
export { renderDiagnostic, renderDiagnosticVerbose };
|
|
20
|
-
/*
|
|
18
|
+
/* our own version, taken from the package manifest, which resides one
|
|
21
19
|
level above both the source and the compiled module directory */
|
|
22
|
-
export const version = (()
|
|
23
|
-
const manifest = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
24
|
-
return JSON.parse(fs.readFileSync(manifest, "utf8")).version;
|
|
25
|
-
})();
|
|
20
|
+
export const version = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
26
21
|
/* the SpecBook API */
|
|
27
22
|
export class SpecBook {
|
|
28
23
|
verbose;
|
|
@@ -44,7 +39,7 @@ export class SpecBook {
|
|
|
44
39
|
file = this.requireConfigFile(file);
|
|
45
40
|
verbose(`loading configuration "${literal(file)}"`);
|
|
46
41
|
const { config, diagnostics } = loadConfig(file);
|
|
47
|
-
if (config ===
|
|
42
|
+
if (config === undefined)
|
|
48
43
|
throw new Error("invalid configuration:\n" +
|
|
49
44
|
diagnostics.map(renderDiagnostic).join("\n"));
|
|
50
45
|
return config;
|
|
@@ -53,8 +48,8 @@ export class SpecBook {
|
|
|
53
48
|
below the base directory */
|
|
54
49
|
async init(options) {
|
|
55
50
|
const verbose = this.verboseOf("init");
|
|
56
|
-
return initSpecification({
|
|
57
|
-
|
|
51
|
+
return initSpecification({ config: this.requireConfig(options.config, verbose),
|
|
52
|
+
basedir: options.basedir ?? ".", verbose });
|
|
58
53
|
}
|
|
59
54
|
/* lint the specification Markdown files below the base directory */
|
|
60
55
|
async lint(options) {
|
|
@@ -64,26 +59,32 @@ export class SpecBook {
|
|
|
64
59
|
/* export the specification Markdown files below the base directory
|
|
65
60
|
as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown,
|
|
66
61
|
parsing the input just once and returning one buffer per
|
|
67
|
-
requested format (
|
|
68
|
-
|
|
62
|
+
requested format (strict: any diagnostic prevents the export,
|
|
63
|
+
as a partial or invalid specification must never be emitted) */
|
|
69
64
|
async export(options) {
|
|
70
65
|
const verbose = this.verboseOf("export");
|
|
71
66
|
const result = lint({ config: this.requireConfigFile(options.config),
|
|
72
67
|
basedir: options.basedir ?? ".", verbose });
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (result.specification.artifacts.length === 0)
|
|
76
|
-
throw new Error("unexportable specification:\n" +
|
|
68
|
+
if (result.diagnostics.length > 0)
|
|
69
|
+
throw new Error("invalid specification:\n" +
|
|
77
70
|
result.diagnostics.map(renderDiagnostic).join("\n"));
|
|
71
|
+
if (result.specification.artifacts.length === 0)
|
|
72
|
+
throw new Error("unexportable specification: no artifacts found");
|
|
78
73
|
const buffers = new Array();
|
|
79
74
|
for (const format of options.formats ?? ["json"])
|
|
80
75
|
buffers.push(await exportSpecification(result.specification, format, verbose, result.config));
|
|
81
76
|
return buffers;
|
|
82
77
|
}
|
|
83
78
|
/* describe the generic SpecBook models and formats as Markdown,
|
|
84
|
-
optionally pointing to the artifacts of the particular project
|
|
79
|
+
optionally pointing to the artifacts of the particular project,
|
|
80
|
+
whose YAML schema configuration is validated if given */
|
|
85
81
|
async describe(options) {
|
|
86
|
-
this.verboseOf("describe")
|
|
82
|
+
const verbose = this.verboseOf("describe");
|
|
83
|
+
if (options.config !== undefined)
|
|
84
|
+
this.requireConfig(options.config, verbose);
|
|
85
|
+
else if (options.embed === true)
|
|
86
|
+
throw new Error("YAML schema configuration required for embedding");
|
|
87
|
+
verbose("describing the SpecBook models and formats");
|
|
87
88
|
return describeFormat(options);
|
|
88
89
|
}
|
|
89
90
|
}
|
package/dst/specbook-cli.js
CHANGED
|
@@ -56,7 +56,8 @@ const withVerboseOption = (command) => command
|
|
|
56
56
|
/* provide the common options of the specification processing sub-commands,
|
|
57
57
|
for which the YAML schema configuration is mandatory */
|
|
58
58
|
const withCommonOptions = (command) => withVerboseOption(command)
|
|
59
|
-
.requiredOption("-c, --config <yaml-file>", "YAML schema configuration file", envDefault("config"))
|
|
59
|
+
.requiredOption("-c, --config <yaml-file>", "YAML schema configuration file", envDefault("config"))
|
|
60
|
+
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."));
|
|
60
61
|
/* parse the command line */
|
|
61
62
|
const program = new Command();
|
|
62
63
|
program.name("specbook")
|
|
@@ -65,15 +66,16 @@ program.name("specbook")
|
|
|
65
66
|
.action(() => {
|
|
66
67
|
program.help();
|
|
67
68
|
});
|
|
69
|
+
/* the mcp command runs all other commands as MCP tools over stdio */
|
|
68
70
|
withVerboseOption(program.command("mcp"))
|
|
69
71
|
.description("run as MCP stdio server")
|
|
70
72
|
.action(async (opts) => {
|
|
71
73
|
verboseOf(opts)("mcp", "starting MCP server on stdio");
|
|
72
74
|
await serveMcp(verboseOf(opts, "mcp"));
|
|
73
75
|
});
|
|
76
|
+
/* the init command creates the configured artifact files */
|
|
74
77
|
withCommonOptions(program.command("init"))
|
|
75
78
|
.description("initialize the configured specification artifact files below the base directory")
|
|
76
|
-
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
|
|
77
79
|
.action(async (opts) => {
|
|
78
80
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
79
81
|
const created = await specbook.init({ config: opts.config, basedir: opts.basedir });
|
|
@@ -81,9 +83,9 @@ withCommonOptions(program.command("init"))
|
|
|
81
83
|
`initialized artifact file(s): ${created.join(", ")}\n` :
|
|
82
84
|
"no artifact files were created\n");
|
|
83
85
|
});
|
|
86
|
+
/* the lint command reports all diagnostics and fails on any of them */
|
|
84
87
|
withCommonOptions(program.command("lint"))
|
|
85
88
|
.description("lint the specification Markdown files below the base directory")
|
|
86
|
-
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
|
|
87
89
|
.action(async (opts) => {
|
|
88
90
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
89
91
|
const result = await specbook.lint({ config: opts.config, basedir: opts.basedir });
|
|
@@ -96,16 +98,16 @@ withCommonOptions(program.command("lint"))
|
|
|
96
98
|
else
|
|
97
99
|
verboseOf(opts)("lint", "specification valid");
|
|
98
100
|
});
|
|
101
|
+
/* the export command parses the input once and writes every output */
|
|
99
102
|
withCommonOptions(program.command("export"))
|
|
100
103
|
.description("export the specification Markdown files below the base directory " +
|
|
101
104
|
"as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown")
|
|
102
|
-
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
|
|
103
105
|
.option("-o, --output [<format>:]<output-file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
|
|
104
|
-
"from the filename extension unless explicitly prefixed", (value, previous) => previous.concat(value),
|
|
106
|
+
"from the filename extension unless explicitly prefixed", (value, previous) => previous.concat(value), new Array())
|
|
105
107
|
.action(async (opts) => {
|
|
106
108
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
107
109
|
const outputs = (opts.output.length > 0 ? opts.output : [envDefault("output") ?? "-"])
|
|
108
|
-
.map(
|
|
110
|
+
.map(parseOutputSpec);
|
|
109
111
|
/* parse the input once and export each distinct format once */
|
|
110
112
|
const distinct = Array.from(new Set(outputs.map(({ format }) => format)));
|
|
111
113
|
const buffers = await specbook.export({ config: opts.config, basedir: opts.basedir,
|
|
@@ -126,6 +128,7 @@ withVerboseOption(program.command("describe"))
|
|
|
126
128
|
const text = await specbook.describe({ config: opts.config, basedir: opts.basedir, embed: opts.embed });
|
|
127
129
|
await writeOutput(opts.output, text, "describe", verboseOf(opts));
|
|
128
130
|
});
|
|
131
|
+
/* run the command line program */
|
|
129
132
|
try {
|
|
130
133
|
await program.parseAsync();
|
|
131
134
|
}
|
|
@@ -4,12 +4,10 @@
|
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
|
-
import * as path from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
7
|
import textframe from "textframe";
|
|
10
8
|
/* provide the build-time bundled description of the generic
|
|
11
9
|
SpecBook models and formats */
|
|
12
|
-
const description = () => fs.readFileSync(
|
|
10
|
+
const description = () => fs.readFileSync(new URL("specbook-format.md", import.meta.url), "utf8");
|
|
13
11
|
/* describe the generic SpecBook models and formats as Markdown,
|
|
14
12
|
optionally extended by the references to (or the embeddings of) the
|
|
15
13
|
artifacts of the particular project instantiation */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
6
|
import { minify } from "@swc/html";
|
|
7
|
-
import { documentTitle, documentLogo, documentCharset, documentThemeTone, subsetStylesheet, documentPaperSize, paperStylesheet } from "./specbook-export-common.js";
|
|
7
|
+
import { documentTitle, documentLogo, documentCharset, documentThemeTone, subsetStylesheet, documentPaperSize, paperStylesheet, charsetCodepoints } from "./specbook-export-common.js";
|
|
8
8
|
import { themeColors, themeStylesheet, themeMapping } from "./specbook-theme.js";
|
|
9
9
|
import { renderAst } from "./specbook-export-ast.js";
|
|
10
10
|
import { renderMarkdown } from "./specbook-export-md.js";
|
|
@@ -13,27 +13,34 @@ import { htmlToPdf } from "./specbook-export-pdf.js";
|
|
|
13
13
|
import { literal } from "./specbook-verbose.js";
|
|
14
14
|
/* the supported export formats */
|
|
15
15
|
export const formats = ["json", "json5", "yaml", "toon", "html", "pdf", "md"];
|
|
16
|
-
/* the filename extensions mapping onto the export formats */
|
|
17
|
-
const extensions =
|
|
18
|
-
json
|
|
19
|
-
json5
|
|
20
|
-
yaml
|
|
21
|
-
toon
|
|
22
|
-
html
|
|
23
|
-
pdf
|
|
24
|
-
md
|
|
25
|
-
|
|
16
|
+
/* the format names and filename extensions mapping onto the export formats */
|
|
17
|
+
const extensions = new Map([
|
|
18
|
+
["json", "json"],
|
|
19
|
+
["json5", "json5"],
|
|
20
|
+
["yaml", "yaml"], ["yml", "yaml"],
|
|
21
|
+
["toon", "toon"],
|
|
22
|
+
["html", "html"], ["htm", "html"],
|
|
23
|
+
["pdf", "pdf"],
|
|
24
|
+
["md", "md"], ["markdown", "md"]
|
|
25
|
+
]);
|
|
26
26
|
/* parse an output specification "[<format>:]<filename>", inferring the
|
|
27
27
|
format from the filename extension when not explicitly given
|
|
28
28
|
(plain "-" for stdout defaults to JSON) */
|
|
29
29
|
export const parseOutputSpec = (spec) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
/* a single-character prefix is never a format name, so a Windows
|
|
31
|
+
drive letter still falls through to the extension inference */
|
|
32
|
+
const prefixed = spec.match(/^([a-zA-Z0-9]{2,}):(.+)$/);
|
|
33
|
+
if (prefixed !== null) {
|
|
34
|
+
const explicit = extensions.get(prefixed[1].toLowerCase());
|
|
35
|
+
if (explicit === undefined)
|
|
36
|
+
throw new Error(`unknown export format "${prefixed[1]}" in output "${spec}" ` +
|
|
37
|
+
`(supported: ${Array.from(extensions.keys()).join(", ")})`);
|
|
38
|
+
return { format: explicit, output: prefixed[2] };
|
|
39
|
+
}
|
|
33
40
|
if (spec === "-")
|
|
34
41
|
return { format: "json", output: spec };
|
|
35
42
|
const ext = spec.match(/\.([a-zA-Z0-9]+)$/);
|
|
36
|
-
const format = ext !== null ? extensions
|
|
43
|
+
const format = ext !== null ? extensions.get(ext[1].toLowerCase()) : undefined;
|
|
37
44
|
if (format !== undefined)
|
|
38
45
|
return { format, output: spec };
|
|
39
46
|
throw new Error(`unable to infer export format from output "${spec}" ` +
|
|
@@ -47,11 +54,11 @@ export const exportSpecification = async (specification, format, verbose, config
|
|
|
47
54
|
if (format === "json" || format === "json5" || format === "yaml" || format === "toon")
|
|
48
55
|
return renderAst(specification, format, config);
|
|
49
56
|
else if (format === "md")
|
|
50
|
-
return Buffer.from(
|
|
57
|
+
return Buffer.from(renderMarkdown(specification, config), "utf8");
|
|
51
58
|
/* the HTML-based formats share the stylesheet, with the embedded
|
|
52
59
|
fonts subsetted to the CHARSET of the specification (if any) */
|
|
53
60
|
const charset = documentCharset(specification);
|
|
54
|
-
if (charset !== undefined)
|
|
61
|
+
if (charset !== undefined && charsetCodepoints(charset) !== undefined)
|
|
55
62
|
verbose(`subsetting embedded fonts to charset "${literal(charset)}"`);
|
|
56
63
|
/* the theme tone (THEME-TONE) drives the layer-1 color spread
|
|
57
64
|
variables and the PDF decoration colors */
|
package/dst/specbook-cmd-init.js
CHANGED
|
@@ -18,8 +18,7 @@ const timestamp = () => {
|
|
|
18
18
|
all artifacts configured onto the same file reside in it side by
|
|
19
19
|
side, following each other on level 1 */
|
|
20
20
|
export const initSpecification = (options) => {
|
|
21
|
-
|
|
22
|
-
fs.mkdirSync(basedir, { recursive: true });
|
|
21
|
+
fs.mkdirSync(options.basedir, { recursive: true });
|
|
23
22
|
const now = timestamp();
|
|
24
23
|
const created = new Array();
|
|
25
24
|
/* group the configured artifacts by their file, preserving the
|
|
@@ -34,8 +33,10 @@ export const initSpecification = (options) => {
|
|
|
34
33
|
else
|
|
35
34
|
group.push(artifact);
|
|
36
35
|
}
|
|
36
|
+
/* create each absent artifact file with its frontmatter and
|
|
37
|
+
the level 1 headings of its artifacts */
|
|
37
38
|
for (const [file, artifacts] of groups) {
|
|
38
|
-
const target = path.join(basedir, file);
|
|
39
|
+
const target = path.join(options.basedir, file);
|
|
39
40
|
if (fs.existsSync(target)) {
|
|
40
41
|
options.verbose(`skipping existing artifact file "${literal(file)}"`);
|
|
41
42
|
continue;
|
|
@@ -43,7 +44,7 @@ export const initSpecification = (options) => {
|
|
|
43
44
|
const headings = artifacts.map((artifact) => {
|
|
44
45
|
const name = artifact.name ?? "";
|
|
45
46
|
const paren = artifact.id !== undefined ? ` (${artifact.id})` : "";
|
|
46
|
-
return `# ${artifact.kind}: ${name}${paren}\n
|
|
47
|
+
return `# ${artifact.kind}: ${name}${paren}\n\n`;
|
|
47
48
|
}).join("");
|
|
48
49
|
const text = "---\n" +
|
|
49
50
|
`Created: ${now}\n` +
|
package/dst/specbook-cmd-lint.js
CHANGED
|
@@ -7,11 +7,11 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { loadConfig } from "./specbook-config.js";
|
|
9
9
|
import { literal } from "./specbook-verbose.js";
|
|
10
|
-
import { parseSpecification } from "./specbook-parse.js";
|
|
10
|
+
import { parseSpecification, resolveArtifact } from "./specbook-parse.js";
|
|
11
11
|
/* collect the distinct artifact files the schema configuration
|
|
12
12
|
references, in their declaration order, each mapped onto whether
|
|
13
13
|
all of its artifacts are optional and hence the file may be absent */
|
|
14
|
-
|
|
14
|
+
const schemaFiles = (config) => {
|
|
15
15
|
const files = new Map();
|
|
16
16
|
for (const artifact of config) {
|
|
17
17
|
if (artifact.file === undefined)
|
|
@@ -28,17 +28,16 @@ export const lint = (options) => {
|
|
|
28
28
|
options.verbose(`loading configuration "${literal(options.config)}"`);
|
|
29
29
|
const loaded = loadConfig(options.config);
|
|
30
30
|
diagnostics.push(...loaded.diagnostics);
|
|
31
|
-
const config = loaded.config
|
|
31
|
+
const config = loaded.config;
|
|
32
32
|
/* read the artifact files the configuration references, resolved
|
|
33
33
|
against the base directory and tolerating an absent file if all
|
|
34
34
|
its artifacts are optional */
|
|
35
35
|
const files = config !== undefined ? schemaFiles(config) : new Map();
|
|
36
|
-
options.verbose(`parsing ${literal(files.size)} specification file(s) ` +
|
|
37
|
-
`below "${literal(options.basedir)}"`);
|
|
38
36
|
if (config !== undefined && files.size === 0)
|
|
39
37
|
diagnostics.push({ file: options.config, line: 1, column: 1,
|
|
40
38
|
message: "no artifact files configured" });
|
|
41
39
|
const sources = new Array();
|
|
40
|
+
const present = new Set();
|
|
42
41
|
for (const [name, optional] of files) {
|
|
43
42
|
const file = path.join(options.basedir, name);
|
|
44
43
|
if (!fs.existsSync(file)) {
|
|
@@ -49,13 +48,38 @@ export const lint = (options) => {
|
|
|
49
48
|
}
|
|
50
49
|
try {
|
|
51
50
|
sources.push({ file, text: fs.readFileSync(file, "utf8") });
|
|
51
|
+
present.add(name);
|
|
52
52
|
}
|
|
53
53
|
catch (err) {
|
|
54
54
|
diagnostics.push({ file, line: 1, column: 1,
|
|
55
55
|
message: `unreadable file: ${err instanceof Error ? err.message : String(err)}` });
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/* parse and validate the artifact files against the configuration */
|
|
59
|
+
options.verbose(`parsing ${literal(sources.length)} specification file(s) ` +
|
|
60
|
+
`below "${literal(options.basedir)}"`);
|
|
58
61
|
const result = parseSpecification(sources, config);
|
|
59
62
|
diagnostics.push(...result.diagnostics);
|
|
63
|
+
/* report the non-optional artifacts absent from the specification,
|
|
64
|
+
against their loaded artifact file (an absent or unreadable file
|
|
65
|
+
is already reported above) or else against the configuration */
|
|
66
|
+
if (config !== undefined) {
|
|
67
|
+
const found = new Set();
|
|
68
|
+
for (const artifact of result.specification.artifacts)
|
|
69
|
+
for (const object of artifact.objects) {
|
|
70
|
+
const schema = resolveArtifact(config, object);
|
|
71
|
+
if (schema !== undefined)
|
|
72
|
+
found.add(schema);
|
|
73
|
+
}
|
|
74
|
+
for (const schema of config) {
|
|
75
|
+
if (schema.optional === true || found.has(schema)
|
|
76
|
+
|| (schema.file !== undefined && !present.has(schema.file)))
|
|
77
|
+
continue;
|
|
78
|
+
const file = schema.file !== undefined ? path.join(options.basedir, schema.file) : options.config;
|
|
79
|
+
const paren = schema.id !== undefined ? ` (${schema.id})` : "";
|
|
80
|
+
diagnostics.push({ file, line: 1, column: 1,
|
|
81
|
+
message: `missing artifact "${schema.kind}: ${schema.name ?? ""}${paren}"` });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
60
84
|
return { specification: result.specification, diagnostics, config };
|
|
61
85
|
};
|
package/dst/specbook-config.d.ts
CHANGED