@rse/specbook 0.9.5 → 0.9.7

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.
Files changed (61) hide show
  1. package/README.md +108 -328
  2. package/dst/specbook-api.d.ts +13 -20
  3. package/dst/specbook-api.js +43 -39
  4. package/dst/specbook-cli.js +42 -55
  5. package/dst/specbook-cmd-describe.d.ts +5 -2
  6. package/dst/specbook-cmd-describe.js +30 -91
  7. package/dst/specbook-cmd-export.d.ts +3 -3
  8. package/dst/specbook-cmd-export.js +23 -19
  9. package/dst/specbook-cmd-init.d.ts +3 -3
  10. package/dst/specbook-cmd-init.js +38 -15
  11. package/dst/specbook-cmd-lint.d.ts +5 -6
  12. package/dst/specbook-cmd-lint.js +61 -24
  13. package/dst/specbook-config.d.ts +2 -2
  14. package/dst/specbook-config.js +82 -73
  15. package/dst/specbook-diagnostic.js +4 -1
  16. package/dst/specbook-diagram.d.ts +6 -5
  17. package/dst/specbook-diagram.js +94 -104
  18. package/dst/specbook-export-ast.d.ts +3 -3
  19. package/dst/specbook-export-ast.js +1 -1
  20. package/dst/specbook-export-common.d.ts +11 -9
  21. package/dst/specbook-export-common.js +34 -22
  22. package/dst/specbook-export-html-search.js +10 -14
  23. package/dst/specbook-export-html.css +37 -32
  24. package/dst/specbook-export-html.d.ts +4 -4
  25. package/dst/specbook-export-html.js +153 -107
  26. package/dst/specbook-export-logo-dark.svg +35 -0
  27. package/dst/specbook-export-logo-light.svg +35 -0
  28. package/dst/specbook-export-md.d.ts +3 -3
  29. package/dst/specbook-export-md.js +50 -24
  30. package/dst/specbook-export-pdf.d.ts +4 -2
  31. package/dst/specbook-export-pdf.js +48 -38
  32. package/dst/{specbook-struct-schema.d.ts → specbook-format-schema.d.ts} +3 -2
  33. package/dst/{specbook-struct-schema.js → specbook-format-schema.js} +8 -4
  34. package/dst/specbook-format-spec.d.ts +31 -0
  35. package/dst/{specbook-struct-spec.js → specbook-format-spec.js} +10 -10
  36. package/dst/specbook-format.md +750 -0
  37. package/dst/specbook-link.d.ts +3 -2
  38. package/dst/specbook-link.js +7 -4
  39. package/dst/specbook-mcp.d.ts +2 -1
  40. package/dst/specbook-mcp.js +30 -62
  41. package/dst/specbook-parse-common.d.ts +5 -3
  42. package/dst/specbook-parse-common.js +8 -0
  43. package/dst/specbook-parse-semantic.d.ts +6 -4
  44. package/dst/specbook-parse-semantic.js +98 -106
  45. package/dst/specbook-parse-syntax.d.ts +2 -2
  46. package/dst/specbook-parse-syntax.js +175 -144
  47. package/dst/specbook-parse-value.d.ts +1 -1
  48. package/dst/specbook-parse-value.js +6 -3
  49. package/dst/specbook-parse.d.ts +4 -7
  50. package/dst/specbook-parse.js +28 -35
  51. package/dst/specbook-verbose.d.ts +2 -0
  52. package/dst/specbook-verbose.js +17 -0
  53. package/package.json +3 -7
  54. package/dst/specbook-cmd-edit.d.ts +0 -9
  55. package/dst/specbook-cmd-edit.js +0 -74
  56. package/dst/specbook-cmd-import.d.ts +0 -9
  57. package/dst/specbook-cmd-import.js +0 -78
  58. package/dst/specbook-export-logo.svg +0 -35
  59. package/dst/specbook-llm.d.ts +0 -13
  60. package/dst/specbook-llm.js +0 -51
  61. package/dst/specbook-struct-spec.d.ts +0 -31
@@ -4,38 +4,42 @@
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";
12
10
  import { lint } from "./specbook-cmd-lint.js";
13
11
  import { exportSpecification, parseOutputSpec, formats } from "./specbook-cmd-export.js";
14
- import { describeConfiguration } from "./specbook-cmd-describe.js";
15
- import { importSpecification } from "./specbook-cmd-import.js";
16
- import { editSpecification } from "./specbook-cmd-edit.js";
12
+ import { describeFormat } from "./specbook-cmd-describe.js";
13
+ import { literal, renderVerbose } from "./specbook-verbose.js";
17
14
  /* re-export the central types for API consumers */
15
+ export { literal, renderVerbose };
18
16
  export { formats, parseOutputSpec };
19
17
  export { renderDiagnostic, renderDiagnosticVerbose };
20
- /* the own version, taken from the package manifest, which resides one
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;
29
24
  constructor(options = {}) {
30
25
  this.verbose = options.verbose ?? (() => { });
31
26
  }
32
- /* load a mandatory YAML schema configuration, failing on any problem */
33
- requireConfig(file) {
27
+ /* bind the verbose sink to a particular command */
28
+ verboseOf(cmd) {
29
+ return (msg) => this.verbose(cmd, msg);
30
+ }
31
+ /* require the file of the mandatory YAML schema configuration */
32
+ requireConfigFile(file) {
34
33
  if (file === undefined)
35
34
  throw new Error("YAML schema configuration required");
36
- this.verbose(`loading configuration "${file}"`);
35
+ return file;
36
+ }
37
+ /* load a mandatory YAML schema configuration, failing on any problem */
38
+ requireConfig(file, verbose) {
39
+ file = this.requireConfigFile(file);
40
+ verbose(`loading configuration "${literal(file)}"`);
37
41
  const { config, diagnostics } = loadConfig(file);
38
- if (config === null)
42
+ if (config === undefined)
39
43
  throw new Error("invalid configuration:\n" +
40
44
  diagnostics.map(renderDiagnostic).join("\n"));
41
45
  return config;
@@ -43,44 +47,44 @@ export class SpecBook {
43
47
  /* initialize the configured specification artifact files
44
48
  below the base directory */
45
49
  async init(options) {
46
- return initSpecification({ ...options,
47
- config: this.requireConfig(options.config), verbose: this.verbose });
50
+ const verbose = this.verboseOf("init");
51
+ return initSpecification({ config: this.requireConfig(options.config, verbose),
52
+ basedir: options.basedir ?? ".", verbose });
48
53
  }
49
54
  /* lint the specification Markdown files below the base directory */
50
55
  async lint(options) {
51
- return lint({ config: options.config, basedir: options.basedir ?? ".", verbose: this.verbose });
56
+ return lint({ config: this.requireConfigFile(options.config),
57
+ basedir: options.basedir ?? ".", verbose: this.verboseOf("lint") });
52
58
  }
53
59
  /* export the specification Markdown files below the base directory
54
60
  as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown,
55
61
  parsing the input just once and returning one buffer per
56
- requested format (best-effort: diagnostics do not prevent the
57
- export, as validation is the concern of lint) */
62
+ requested format (strict: any diagnostic prevents the export,
63
+ as a partial or invalid specification must never be emitted) */
58
64
  async export(options) {
59
- const result = lint({ config: options.config, basedir: options.basedir ?? ".", verbose: this.verbose });
60
- for (const diagnostic of result.diagnostics)
61
- this.verbose(`diagnostic: ${renderDiagnostic(diagnostic)}`);
62
- if (result.specification.artifacts.length === 0)
63
- throw new Error("unexportable specification:\n" +
65
+ const verbose = this.verboseOf("export");
66
+ const result = lint({ config: this.requireConfigFile(options.config),
67
+ basedir: options.basedir ?? ".", verbose });
68
+ if (result.diagnostics.length > 0)
69
+ throw new Error("invalid specification:\n" +
64
70
  result.diagnostics.map(renderDiagnostic).join("\n"));
71
+ if (result.specification.artifacts.length === 0)
72
+ throw new Error("unexportable specification: no artifacts found");
65
73
  const buffers = new Array();
66
74
  for (const format of options.formats ?? ["json"])
67
- buffers.push(await exportSpecification(result.specification, format, this.verbose, result.config));
75
+ buffers.push(await exportSpecification(result.specification, format, verbose, result.config));
68
76
  return buffers;
69
77
  }
70
- /* describe the configured specification format as Markdown */
78
+ /* describe the generic SpecBook models and formats as Markdown,
79
+ optionally pointing to the artifacts of the particular project,
80
+ whose YAML schema configuration is validated if given */
71
81
  async describe(options) {
72
- return describeConfiguration(this.requireConfig(options.config));
73
- }
74
- /* import foreign sources into the specification artifact files
75
- below the base directory */
76
- async import(options) {
77
- return importSpecification({ ...options,
78
- config: this.requireConfig(options.config), verbose: this.verbose });
79
- }
80
- /* apply an edit request to the specification artifact files
81
- below the base directory */
82
- async edit(options) {
83
- return editSpecification({ ...options,
84
- config: this.requireConfig(options.config), verbose: this.verbose });
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");
88
+ return describeFormat(options);
85
89
  }
86
90
  }
@@ -6,13 +6,18 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import { Command } from "commander";
9
- import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, parseOutputSpec, version } from "./specbook-api.js";
9
+ import chalk from "chalk";
10
+ import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, renderVerbose, literal, parseOutputSpec, version } from "./specbook-api.js";
10
11
  import { serveMcp } from "./specbook-mcp.js";
11
- /* route verbose messages to stderr, keeping stdout reserved
12
- for the command outputs and the MCP protocol */
13
- const verboseOf = (opts) => (msg) => {
14
- if (opts.verbose)
15
- process.stderr.write(`specbook: ${msg}\n`);
12
+ /* route verbose messages to stderr, keeping stdout reserved for the
13
+ command outputs and the MCP protocol, and qualify every message with
14
+ the tool name plus the scope path of the emitting command, where
15
+ Chalk styles the output only if the terminal supports colors */
16
+ const verboseOf = (opts, ...scope) => (cmd, msg) => {
17
+ if (opts.verbose) {
18
+ const scopePath = [...scope, cmd].map((segment) => chalk.bold(segment)).join(": ");
19
+ process.stderr.write(`specbook: ${scopePath}: ${renderVerbose(msg, chalk.blue)}\n`);
20
+ }
16
21
  };
17
22
  /* write a buffer to stdout, awaiting the write callback which only
18
23
  fires once the data has been flushed to the underlying pipe, so a
@@ -28,12 +33,12 @@ const writeStdout = (data) => {
28
33
  });
29
34
  };
30
35
  /* write a command result to the output file or stdout */
31
- const writeOutput = async (output, data, verbose) => {
36
+ const writeOutput = async (output, data, cmd, verbose) => {
32
37
  if (output === "-")
33
38
  await writeStdout(data);
34
39
  else {
35
40
  await fs.promises.writeFile(output, data);
36
- verbose(`wrote "${output}" (${Buffer.byteLength(data)} bytes)`);
41
+ verbose(cmd, `wrote "${literal(output)}" (${literal(Buffer.byteLength(data))} bytes)`);
37
42
  }
38
43
  };
39
44
  /* determine an option default value, overridable via a
@@ -45,10 +50,14 @@ const envDefaultFlag = (name, fallback) => {
45
50
  const value = process.env[`SPECBOOK_${name.toUpperCase().replace(/-/g, "_")}`];
46
51
  return value !== undefined ? !(/^(?:|0|false|no|off)$/i).test(value) : fallback;
47
52
  };
48
- /* provide the common options of all sub-commands */
49
- const withCommonOptions = (command) => command
50
- .option("-v, --verbose", "print verbose processing information to stderr", envDefaultFlag("verbose", false))
51
- .option("-c, --config <yaml-file>", "YAML schema configuration file", envDefault("config"));
53
+ /* provide the verbose option of all sub-commands */
54
+ const withVerboseOption = (command) => command
55
+ .option("-v, --verbose", "print verbose processing information to stderr", envDefaultFlag("verbose", false));
56
+ /* provide the common options of the specification processing sub-commands,
57
+ for which the YAML schema configuration is mandatory */
58
+ const withCommonOptions = (command) => withVerboseOption(command)
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", "."));
52
61
  /* parse the command line */
53
62
  const program = new Command();
54
63
  program.name("specbook")
@@ -57,16 +66,16 @@ program.name("specbook")
57
66
  .action(() => {
58
67
  program.help();
59
68
  });
60
- program.command("mcp")
69
+ /* the mcp command runs all other commands as MCP tools over stdio */
70
+ withVerboseOption(program.command("mcp"))
61
71
  .description("run as MCP stdio server")
62
- .option("-v, --verbose", "print verbose processing information to stderr", envDefaultFlag("verbose", false))
63
72
  .action(async (opts) => {
64
- verboseOf(opts)("starting MCP server on stdio");
65
- await serveMcp(verboseOf(opts));
73
+ verboseOf(opts)("mcp", "starting MCP server on stdio");
74
+ await serveMcp(verboseOf(opts, "mcp"));
66
75
  });
76
+ /* the init command creates the configured artifact files */
67
77
  withCommonOptions(program.command("init"))
68
78
  .description("initialize the configured specification artifact files below the base directory")
69
- .option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
70
79
  .action(async (opts) => {
71
80
  const specbook = new SpecBook({ verbose: verboseOf(opts) });
72
81
  const created = await specbook.init({ config: opts.config, basedir: opts.basedir });
@@ -74,9 +83,9 @@ withCommonOptions(program.command("init"))
74
83
  `initialized artifact file(s): ${created.join(", ")}\n` :
75
84
  "no artifact files were created\n");
76
85
  });
86
+ /* the lint command reports all diagnostics and fails on any of them */
77
87
  withCommonOptions(program.command("lint"))
78
88
  .description("lint the specification Markdown files below the base directory")
79
- .option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
80
89
  .action(async (opts) => {
81
90
  const specbook = new SpecBook({ verbose: verboseOf(opts) });
82
91
  const result = await specbook.lint({ config: opts.config, basedir: opts.basedir });
@@ -87,61 +96,39 @@ withCommonOptions(program.command("lint"))
87
96
  if (result.diagnostics.length > 0)
88
97
  process.exitCode = 1;
89
98
  else
90
- verboseOf(opts)("specification valid");
99
+ verboseOf(opts)("lint", "specification valid");
91
100
  });
101
+ /* the export command parses the input once and writes every output */
92
102
  withCommonOptions(program.command("export"))
93
103
  .description("export the specification Markdown files below the base directory " +
94
104
  "as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown")
95
- .option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
96
105
  .option("-o, --output [<format>:]<output-file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
97
- "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())
98
107
  .action(async (opts) => {
99
108
  const specbook = new SpecBook({ verbose: verboseOf(opts) });
100
109
  const outputs = (opts.output.length > 0 ? opts.output : [envDefault("output") ?? "-"])
101
- .map((spec) => parseOutputSpec(spec));
110
+ .map(parseOutputSpec);
102
111
  /* parse the input once and export each distinct format once */
103
112
  const distinct = Array.from(new Set(outputs.map(({ format }) => format)));
104
113
  const buffers = await specbook.export({ config: opts.config, basedir: opts.basedir,
105
114
  formats: distinct });
106
115
  for (const { format, output } of outputs)
107
- await writeOutput(output, buffers[distinct.indexOf(format)], verboseOf(opts));
116
+ await writeOutput(output, buffers[distinct.indexOf(format)], "export", verboseOf(opts));
108
117
  });
109
- withCommonOptions(program.command("describe"))
110
- .description("describe the configured specification format as Markdown")
118
+ /* the describe command also describes the generic SpecBook models and
119
+ formats alone, so its YAML schema configuration stays optional */
120
+ withVerboseOption(program.command("describe"))
121
+ .description("describe the SpecBook models and formats as Markdown")
122
+ .option("-c, --config <yaml-file>", "YAML schema configuration file", envDefault("config"))
123
+ .option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir"))
124
+ .option("-e, --embed", "embed the YAML schema configuration instead of just referencing it", envDefaultFlag("embed", false))
111
125
  .option("-o, --output <markdown-file>", "output file (\"-\" for stdout)", envDefault("output", "-"))
112
126
  .action(async (opts) => {
113
127
  const specbook = new SpecBook({ verbose: verboseOf(opts) });
114
- const text = await specbook.describe({ config: opts.config });
115
- await writeOutput(opts.output, text, verboseOf(opts));
116
- });
117
- withCommonOptions(program.command("import"))
118
- .description("import foreign sources into the specification artifact files below the base directory")
119
- .option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
120
- .option("--provider <provider>", "AI provider (\"anthropic\", \"openai\", \"openrouter\" or \"ollama\")", envDefault("provider"))
121
- .option("--model <model>", "AI model", envDefault("model"))
122
- .argument("<input-files...>", "foreign source files to import")
123
- .action(async (inputs, opts) => {
124
- const specbook = new SpecBook({ verbose: verboseOf(opts) });
125
- const written = await specbook.import({ config: opts.config, basedir: opts.basedir,
126
- inputs, provider: opts.provider, model: opts.model });
127
- await writeStdout(written.length > 0 ?
128
- `imported into artifact file(s): ${written.join(", ")}\n` :
129
- "no artifact files were changed\n");
130
- });
131
- withCommonOptions(program.command("edit"))
132
- .description("apply a free-text edit request to the specification artifact files below the base directory")
133
- .option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
134
- .option("--provider <provider>", "AI provider (\"anthropic\", \"openai\", \"openrouter\" or \"ollama\")", envDefault("provider"))
135
- .option("--model <model>", "AI model", envDefault("model"))
136
- .argument("<query>", "free-text edit request")
137
- .action(async (query, opts) => {
138
- const specbook = new SpecBook({ verbose: verboseOf(opts) });
139
- const written = await specbook.edit({ config: opts.config, basedir: opts.basedir,
140
- query, provider: opts.provider, model: opts.model });
141
- await writeStdout(written.length > 0 ?
142
- `edited artifact file(s): ${written.join(", ")}\n` :
143
- "no artifact files were changed\n");
128
+ const text = await specbook.describe({ config: opts.config, basedir: opts.basedir, embed: opts.embed });
129
+ await writeOutput(opts.output, text, "describe", verboseOf(opts));
144
130
  });
131
+ /* run the command line program */
145
132
  try {
146
133
  await program.parseAsync();
147
134
  }
@@ -1,2 +1,5 @@
1
- import { type SchemaSpecification } from "./specbook-struct-schema.js";
2
- export declare const describeConfiguration: (config: SchemaSpecification) => string;
1
+ export declare const describeFormat: (options: {
2
+ config?: string;
3
+ basedir?: string;
4
+ embed?: boolean;
5
+ }) => string;
@@ -3,97 +3,36 @@
3
3
  ** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
+ import * as fs from "node:fs";
6
7
  import textframe from "textframe";
7
- /* collapse folded YAML prose into a single line */
8
- const collapse = (text) => (text ?? "").replace(/\s+/g, " ").trim();
9
- /* the static description of the generic Markdown syntax */
10
- const syntax = textframe `
11
- # SpecBook Specification Format
12
-
13
- A specification consists of Markdown *artifact* files, each carrying a
14
- frontmatter with \`Created:\` and \`Modified:\` timestamps (format
15
- \`yyyy-LL-dd HH:mm\`) and a tree of *objects*. Every object has a *kind*,
16
- a *name*, a unique anchor *id*, optional *properties* (key/value pairs),
17
- an optional *description* statement with an optional rationale, and
18
- optional *child* objects. Two concrete syntaxes exist:
19
-
20
- ## Complex Format (used on levels 1-3)
21
-
22
- \`\`\`
23
- # <kind>: <name> (<id>)
24
-
25
- - <key>: <value>
26
- - [...]
27
-
28
- <statement>, BECAUSE <rationale>.
29
- \`\`\`
30
-
31
- Level 1 (\`#\`) carries the artifact id in parentheses. Levels 2 (\`##\`)
32
- and 3 (\`###\`) instead carry an HTML anchor \`<a id="<id>"></a>\` after
33
- the name, where the id uses the artifact id as its prefix.
34
-
35
- ## Concise Format (used on level 4 and deeper)
36
-
37
- \`\`\`
38
- - <kind>: <name>; <key>: <value>; [...]; <statement>, BECAUSE <rationale>.
39
- \`\`\`
40
-
41
- Child objects nest as indented list items below their parent item.
42
-
43
- ## Primary Marker
44
-
45
- In both formats a literal \`(*)\` directly after the \`<name>\` marks the
46
- object as *primary* (e.g. the primary attribute of an entity). It is
47
- distinct from an id given as \`(<id>)\` and both may be combined.
48
-
49
- ## Configured Artifacts
50
-
51
- The following artifacts, object kinds, and properties are configured.
52
- Object kinds and property names are *case-sensitive* and must be
53
- written exactly as configured. Properties marked \`required\` must be
54
- present, and property values must match their given constraint (if
55
- any, else any value is allowed): \`/xxx/\` requires a match of the
56
- regular expression, \`[[xxx]]\` requires exactly one Wiki-style link
57
- reference resolving into the (usually wildcard) reference pattern,
58
- \`enum(xxx,yyy)\` requires one of the listed members, \`tags(xxx,yyy)\`
59
- requires a comma-separated set of the listed members (each at most
60
- once), and \`list(xxx[, ...])\` requires a comma-separated list of
61
- items, each matching one of the alternative constraints.
62
- `;
63
- /* describe a single configured property */
64
- const describeProperty = (prop, indent) => {
65
- let line = `${indent}- property \`${prop.name}\` (${prop.optional === true ? "optional" : "required"}`;
66
- if (prop.value !== undefined)
67
- line += `, constraint \`${prop.value}\``;
68
- line += `): ${collapse(prop.desc)}`;
69
- return line;
70
- };
71
- /* describe a single configured object kind (and recursively its childs) */
72
- const describeObject = (object, level, indent) => {
73
- const lines = new Array();
74
- lines.push(`${indent}- object kind \`${object.kind}\` on level ${level}` +
75
- `${object.optional === true ? " (optional)" : ""}: ${collapse(object.desc)}`);
76
- for (const prop of object.props ?? [])
77
- lines.push(describeProperty(prop, `${indent} `));
78
- for (const child of object.childs ?? [])
79
- lines.push(describeObject(child, level + 1, `${indent} `));
80
- return lines.join("\n");
81
- };
82
- /* describe the configured specification format as Markdown */
83
- export const describeConfiguration = (config) => {
84
- const sections = new Array();
85
- for (const artifact of config) {
86
- const lines = new Array();
87
- lines.push(`### ${artifact.kind}: ${artifact.name ?? ""} (${artifact.kind}-${artifact.id ?? ""})`);
88
- lines.push("");
89
- if (artifact.file !== undefined)
90
- lines.push(`- file: \`${artifact.file}\``);
91
- lines.push(`- description: ${collapse(artifact.desc)}`);
92
- for (const prop of artifact.props ?? [])
93
- lines.push(describeProperty(prop, ""));
94
- for (const child of artifact.childs ?? [])
95
- lines.push(describeObject(child, 2, ""));
96
- sections.push(lines.join("\n"));
8
+ /* provide the build-time bundled description of the generic
9
+ SpecBook models and formats */
10
+ const description = () => fs.readFileSync(new URL("specbook-format.md", import.meta.url), "utf8");
11
+ /* describe the generic SpecBook models and formats as Markdown,
12
+ optionally extended by the references to (or the embeddings of) the
13
+ artifacts of the particular project instantiation */
14
+ export const describeFormat = (options) => {
15
+ const sections = [description()];
16
+ if (options.config !== undefined || options.basedir !== undefined) {
17
+ sections.push("SpecBook Project Instantiation\n" +
18
+ "==============================\n");
19
+ if (options.config !== undefined && options.embed === true)
20
+ sections.push(textframe(`
21
+ The **SpecBook SCHEMA Model** of this particular project is the following
22
+ YAML schema configuration of the file \`${options.config}\`:
23
+ `) + "\n```yaml\n" +
24
+ fs.readFileSync(options.config, "utf8").replace(/\n*$/, "\n") +
25
+ "```\n");
26
+ else if (options.config !== undefined)
27
+ sections.push(textframe(`
28
+ The **SpecBook SCHEMA Model** of this particular project is the YAML
29
+ schema configuration in the file \`${options.config}\`.
30
+ `));
31
+ if (options.basedir !== undefined)
32
+ sections.push(textframe(`
33
+ The **SpecBook SPEC Model** of this particular project is the set of
34
+ specification Markdown files in the directory \`${options.basedir}\`.
35
+ `));
97
36
  }
98
- return `${syntax}\n${sections.join("\n\n")}\n`;
37
+ return sections.join("\n");
99
38
  };
@@ -1,9 +1,9 @@
1
- import type { Specification } from "./specbook-struct-spec.js";
2
- import type { SchemaSpecification } from "./specbook-struct-schema.js";
1
+ import type { Spec } from "./specbook-format-spec.js";
2
+ import type { Schema } from "./specbook-format-schema.js";
3
3
  export declare const formats: readonly ["json", "json5", "yaml", "toon", "html", "pdf", "md"];
4
4
  export type ExportFormat = typeof formats[number];
5
5
  export declare const parseOutputSpec: (spec: string) => {
6
6
  format: ExportFormat;
7
7
  output: string;
8
8
  };
9
- export declare const exportSpecification: (specification: Specification, format: ExportFormat, verbose: (msg: string) => void, config?: SchemaSpecification) => Promise<Buffer>;
9
+ export declare const exportSpecification: (specification: Spec, format: ExportFormat, verbose: (msg: string) => void, config?: Schema) => Promise<Buffer>;
@@ -4,35 +4,39 @@
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";
11
11
  import { renderHtml, htmlOutline } from "./specbook-export-html.js";
12
12
  import { htmlToPdf } from "./specbook-export-pdf.js";
13
+ import { literal } from "./specbook-verbose.js";
13
14
  /* the supported export formats */
14
15
  export const formats = ["json", "json5", "yaml", "toon", "html", "pdf", "md"];
15
- /* the filename extensions mapping onto the export formats */
16
- const extensions = {
17
- json: "json",
18
- json5: "json5",
19
- yaml: "yaml", yml: "yaml",
20
- toon: "toon",
21
- html: "html", htm: "html",
22
- pdf: "pdf",
23
- md: "md", markdown: "md"
24
- };
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
+ ]);
25
26
  /* parse an output specification "[<format>:]<filename>", inferring the
26
27
  format from the filename extension when not explicitly given
27
28
  (plain "-" for stdout defaults to JSON) */
28
29
  export const parseOutputSpec = (spec) => {
29
30
  const prefixed = spec.match(/^([a-zA-Z0-9]+):(.+)$/);
30
- if (prefixed !== null && formats.includes(prefixed[1]))
31
- return { format: prefixed[1], output: prefixed[2] };
31
+ if (prefixed !== null) {
32
+ const explicit = extensions.get(prefixed[1].toLowerCase());
33
+ if (explicit !== undefined)
34
+ return { format: explicit, output: prefixed[2] };
35
+ }
32
36
  if (spec === "-")
33
37
  return { format: "json", output: spec };
34
38
  const ext = spec.match(/\.([a-zA-Z0-9]+)$/);
35
- const format = ext !== null ? extensions[ext[1].toLowerCase()] : undefined;
39
+ const format = ext !== null ? extensions.get(ext[1].toLowerCase()) : undefined;
36
40
  if (format !== undefined)
37
41
  return { format, output: spec };
38
42
  throw new Error(`unable to infer export format from output "${spec}" ` +
@@ -42,20 +46,20 @@ export const parseOutputSpec = (spec) => {
42
46
  export const exportSpecification = async (specification, format, verbose, config) => {
43
47
  if (!formats.includes(format))
44
48
  throw new Error(`unknown export format "${format}"`);
45
- verbose(`exporting specification as "${format}"`);
49
+ verbose(`exporting specification as "${literal(format)}"`);
46
50
  if (format === "json" || format === "json5" || format === "yaml" || format === "toon")
47
51
  return renderAst(specification, format, config);
48
52
  else if (format === "md")
49
- return Buffer.from(await renderMarkdown(specification, config), "utf8");
53
+ return Buffer.from(renderMarkdown(specification, config), "utf8");
50
54
  /* the HTML-based formats share the stylesheet, with the embedded
51
55
  fonts subsetted to the CHARSET of the specification (if any) */
52
56
  const charset = documentCharset(specification);
53
- if (charset !== undefined)
54
- verbose(`subsetting embedded fonts to charset "${charset}"`);
57
+ if (charset !== undefined && charsetCodepoints(charset) !== undefined)
58
+ verbose(`subsetting embedded fonts to charset "${literal(charset)}"`);
55
59
  /* the theme tone (THEME-TONE) drives the layer-1 color spread
56
60
  variables and the PDF decoration colors */
57
61
  const tone = documentThemeTone(specification) ?? "#336699";
58
- verbose(`generating theme color spreads (tone "${tone}")`);
62
+ verbose(`generating theme color spreads (tone "${literal(tone)}")`);
59
63
  const colors = themeColors(tone);
60
64
  /* the paper size (PAPER-SIZE) drives the PDF page setup and the
61
65
  print-time height constraint of the diagrams */
@@ -1,7 +1,7 @@
1
- import { type SchemaSpecification } from "./specbook-struct-schema.js";
1
+ import { type Schema } from "./specbook-format-schema.js";
2
2
  export interface InitOptions {
3
- config: SchemaSpecification;
4
- basedir?: string;
3
+ config: Schema;
4
+ basedir: string;
5
5
  verbose: (msg: string) => void;
6
6
  }
7
7
  export declare const initSpecification: (options: InitOptions) => string[];
@@ -5,35 +5,58 @@
5
5
  */
6
6
  import * as fs from "node:fs";
7
7
  import * as path from "node:path";
8
- import { timestamp } from "./specbook-llm.js";
8
+ import { literal } from "./specbook-verbose.js";
9
+ /* the current time in the frontmatter timestamp format */
10
+ const timestamp = () => {
11
+ const d = new Date();
12
+ const pad = (n) => String(n).padStart(2, "0");
13
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
14
+ `${pad(d.getHours())}:${pad(d.getMinutes())}`;
15
+ };
9
16
  /* initialize the configured specification artifact files below the
10
- base directory with their frontmatter and artifact heading */
17
+ base directory with their frontmatter and artifact headings, where
18
+ all artifacts configured onto the same file reside in it side by
19
+ side, following each other on level 1 */
11
20
  export const initSpecification = (options) => {
12
- const basedir = options.basedir ?? ".";
13
- fs.mkdirSync(basedir, { recursive: true });
21
+ fs.mkdirSync(options.basedir, { recursive: true });
14
22
  const now = timestamp();
15
23
  const created = new Array();
24
+ /* group the configured artifacts by their file, preserving the
25
+ schema order both of the files and of the artifacts within */
26
+ const groups = new Map();
16
27
  for (const artifact of options.config) {
17
28
  if (artifact.file === undefined)
18
29
  continue;
19
- const file = path.join(basedir, artifact.file);
20
- if (fs.existsSync(file)) {
21
- options.verbose(`skipping existing artifact file "${artifact.file}"`);
30
+ const group = groups.get(artifact.file);
31
+ if (group === undefined)
32
+ groups.set(artifact.file, [artifact]);
33
+ else
34
+ group.push(artifact);
35
+ }
36
+ /* create each absent artifact file with its frontmatter and
37
+ the level 1 headings of its artifacts */
38
+ for (const [file, artifacts] of groups) {
39
+ const target = path.join(options.basedir, file);
40
+ if (fs.existsSync(target)) {
41
+ options.verbose(`skipping existing artifact file "${literal(file)}"`);
22
42
  continue;
23
43
  }
24
- const name = artifact.name ?? "";
25
- const paren = artifact.id !== undefined ? ` (${artifact.id})` : "";
44
+ const headings = artifacts.map((artifact) => {
45
+ const name = artifact.name ?? "";
46
+ const paren = artifact.id !== undefined ? ` (${artifact.id})` : "";
47
+ return `# ${artifact.kind}: ${name}${paren}\n\n`;
48
+ }).join("");
26
49
  const text = "---\n" +
27
50
  `Created: ${now}\n` +
28
51
  `Modified: ${now}\n` +
29
52
  "---\n" +
30
53
  "\n" +
31
- `# ${artifact.kind}: ${name}${paren}\n` +
32
- "\n";
33
- fs.mkdirSync(path.dirname(file), { recursive: true });
34
- fs.writeFileSync(file, text, "utf8");
35
- options.verbose(`created artifact file "${artifact.file}"`);
36
- created.push(artifact.file);
54
+ headings;
55
+ fs.mkdirSync(path.dirname(target), { recursive: true });
56
+ fs.writeFileSync(target, text, "utf8");
57
+ options.verbose(`created artifact file "${literal(file)}" ` +
58
+ `with ${literal(artifacts.length)} artifact(s)`);
59
+ created.push(file);
37
60
  }
38
61
  return created;
39
62
  };