@rse/specbook 0.9.7 → 0.9.9
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 -13
- package/dst/specbook-api.d.ts +5 -4
- package/dst/specbook-api.js +14 -10
- package/dst/specbook-cli.js +3 -2
- package/dst/specbook-cmd-describe.js +3 -6
- package/dst/specbook-cmd-export.js +7 -3
- package/dst/specbook-cmd-lint.js +1 -1
- package/dst/specbook-config.js +14 -4
- package/dst/specbook-diagram.d.ts +5 -1
- package/dst/specbook-diagram.js +31 -19
- package/dst/specbook-export-html.js +2 -1
- package/dst/specbook-export-md.js +21 -14
- package/dst/specbook-format.md +28 -15
- package/dst/specbook-format.yaml +1484 -0
- package/dst/specbook-mcp.js +6 -3
- package/dst/specbook-parse-common.d.ts +1 -0
- package/dst/specbook-parse-common.js +2 -0
- package/dst/specbook-parse-semantic.js +22 -4
- package/dst/specbook-parse-syntax.js +22 -7
- package/dst/specbook-parse-value.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,8 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
|
|
|
44
44
|
The YAML schema configuration strictly defines the allowed object
|
|
45
45
|
kinds, hierarchies, and properties, whose values are constrained by an
|
|
46
46
|
expression language (regex, enum, tags, list, and reference). Violations
|
|
47
|
-
are reported as file- and line-precise diagnostics.
|
|
47
|
+
are reported as file- and line-precise diagnostics. A bundled standard
|
|
48
|
+
schema configuration applies if no particular one is given.
|
|
48
49
|
|
|
49
50
|
- **Object Model Diagram Visualisation**:
|
|
50
51
|
Object kinds can declare "graph", "hub", or "grid" diagrams in the
|
|
@@ -82,7 +83,9 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
|
|
|
82
83
|
Overview Poster
|
|
83
84
|
---------------
|
|
84
85
|
|
|
85
|
-
[](etc/poster.pdf)
|
|
86
|
+
[](etc/poster-1.pdf)
|
|
87
|
+
|
|
88
|
+
[](etc/poster-2.pdf)
|
|
86
89
|
|
|
87
90
|
Rendering Example
|
|
88
91
|
-----------------
|
|
@@ -105,17 +108,18 @@ Usage
|
|
|
105
108
|
### CLI
|
|
106
109
|
|
|
107
110
|
```
|
|
108
|
-
$ specbook init [-v] -c <yaml-file> [-b <basedir>]
|
|
109
|
-
$ specbook lint [-v] -c <yaml-file> [-b <basedir>]
|
|
110
|
-
$ specbook export [-v] -c <yaml-file> [-b <basedir>] [-o [<format>:]<output-file>] [...]
|
|
111
|
+
$ specbook init [-v] [-c <yaml-file>] [-b <basedir>]
|
|
112
|
+
$ specbook lint [-v] [-c <yaml-file>] [-b <basedir>]
|
|
113
|
+
$ specbook export [-v] [-c <yaml-file>] [-b <basedir>] [-o [<format>:]<output-file>] [...]
|
|
111
114
|
$ specbook describe [-v] [-c <yaml-file>] [-b <basedir>] [-e] [-o <markdown-file>]
|
|
112
115
|
$ specbook mcp [-v]
|
|
113
116
|
```
|
|
114
117
|
|
|
115
|
-
The YAML schema configuration `-c`/`--config`
|
|
116
|
-
|
|
117
|
-
artifact files its `file` fields reference
|
|
118
|
-
other Markdown files below the base
|
|
118
|
+
The YAML schema configuration `-c`/`--config` (default: the bundled
|
|
119
|
+
standard schema configuration `specbook-format.yaml`) determines the
|
|
120
|
+
specification: exactly the artifact files its `file` fields reference
|
|
121
|
+
are loaded and parsed, all other Markdown files below the base
|
|
122
|
+
directory are ignored. A referenced
|
|
119
123
|
file which is absent is reported, unless all of its artifacts are
|
|
120
124
|
`optional`, and so is an artifact absent from its present file, unless
|
|
121
125
|
it is `optional` itself. Both `lint` and `export` report all
|
|
@@ -132,6 +136,11 @@ occur multiple times. The format (`json`, `json5`, `yaml`, `toon`,
|
|
|
132
136
|
it is explicitly given as a `<format>:` prefix, and plain `-` (stdout)
|
|
133
137
|
defaults to JSON.
|
|
134
138
|
|
|
139
|
+
The `md` format normalizes the entire corpus into a *single* Markdown
|
|
140
|
+
document, whose single frontmatter block carries the earliest `Created:`
|
|
141
|
+
and the latest `Modified:` timestamp of all artifacts, so the
|
|
142
|
+
per-artifact timestamps do not survive this round-trip.
|
|
143
|
+
|
|
135
144
|
The `describe` command outputs the description of the SpecBook models
|
|
136
145
|
and formats. If the configuration file `-c`/`--config` or the base
|
|
137
146
|
directory `-b`/`--basedir` is given, it additionally appends a *SpecBook
|
|
@@ -144,9 +153,7 @@ its own.
|
|
|
144
153
|
The default value of every CLI option `--xxx` can be overridden by a
|
|
145
154
|
corresponding `SPECBOOK_XXX` environment variable (e.g.
|
|
146
155
|
`SPECBOOK_BASEDIR`, `SPECBOOK_CONFIG`, `SPECBOOK_OUTPUT`,
|
|
147
|
-
`SPECBOOK_VERBOSE`), while an explicitly supplied option always wins.
|
|
148
|
-
`SPECBOOK_CONFIG` value also satisfies the otherwise mandatory
|
|
149
|
-
`-c`/`--config` option.
|
|
156
|
+
`SPECBOOK_VERBOSE`), while an explicitly supplied option always wins.
|
|
150
157
|
|
|
151
158
|
### API
|
|
152
159
|
|
|
@@ -157,7 +164,6 @@ const specbook = new SpecBook({
|
|
|
157
164
|
verbose: (cmd, msg) => console.error(`specbook: ${cmd}: ${renderVerbose(msg)}`)
|
|
158
165
|
})
|
|
159
166
|
const result = await specbook.lint({
|
|
160
|
-
config: "smp/ase.specbook.yaml",
|
|
161
167
|
basedir: "smp/broadcast"
|
|
162
168
|
})
|
|
163
169
|
```
|
package/dst/specbook-api.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { type LintResult };
|
|
|
9
9
|
export type { Spec, SpecArtifact, SpecObject, SpecDescription, SpecProperty } from "./specbook-format-spec.js";
|
|
10
10
|
export type { Schema, SchemaObject, SchemaProperty } from "./specbook-format-schema.js";
|
|
11
11
|
export declare const version: string;
|
|
12
|
+
export declare const standardConfig: string;
|
|
12
13
|
export type VerboseSink = (cmd: string, msg: string) => void;
|
|
13
14
|
export interface SpecBookOptions {
|
|
14
15
|
verbose?: VerboseSink;
|
|
@@ -17,18 +18,18 @@ export declare class SpecBook {
|
|
|
17
18
|
private verbose;
|
|
18
19
|
constructor(options?: SpecBookOptions);
|
|
19
20
|
private verboseOf;
|
|
20
|
-
private
|
|
21
|
+
private configFile;
|
|
21
22
|
private requireConfig;
|
|
22
23
|
init(options: {
|
|
23
|
-
config
|
|
24
|
+
config?: string;
|
|
24
25
|
basedir?: string;
|
|
25
26
|
}): Promise<string[]>;
|
|
26
27
|
lint(options: {
|
|
27
|
-
config
|
|
28
|
+
config?: string;
|
|
28
29
|
basedir?: string;
|
|
29
30
|
}): Promise<LintResult>;
|
|
30
31
|
export(options: {
|
|
31
|
-
config
|
|
32
|
+
config?: string;
|
|
32
33
|
basedir?: string;
|
|
33
34
|
formats?: ExportFormat[];
|
|
34
35
|
}): Promise<Buffer[]>;
|
package/dst/specbook-api.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
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 { fileURLToPath } from "node:url";
|
|
7
8
|
import { loadConfig } from "./specbook-config.js";
|
|
8
9
|
import { renderDiagnostic, renderDiagnosticVerbose } from "./specbook-diagnostic.js";
|
|
9
10
|
import { initSpecification } from "./specbook-cmd-init.js";
|
|
@@ -18,6 +19,10 @@ export { renderDiagnostic, renderDiagnosticVerbose };
|
|
|
18
19
|
/* our own version, taken from the package manifest, which resides one
|
|
19
20
|
level above both the source and the compiled module directory */
|
|
20
21
|
export const version = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
22
|
+
/* the bundled standard YAML schema configuration, which resides
|
|
23
|
+
alongside the compiled module and is used whenever no particular
|
|
24
|
+
schema configuration is given */
|
|
25
|
+
export const standardConfig = fileURLToPath(new URL("specbook-format.yaml", import.meta.url));
|
|
21
26
|
/* the SpecBook API */
|
|
22
27
|
export class SpecBook {
|
|
23
28
|
verbose;
|
|
@@ -28,16 +33,15 @@ export class SpecBook {
|
|
|
28
33
|
verboseOf(cmd) {
|
|
29
34
|
return (msg) => this.verbose(cmd, msg);
|
|
30
35
|
}
|
|
31
|
-
/*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return file;
|
|
36
|
+
/* determine the file of the YAML schema configuration, falling
|
|
37
|
+
back onto the bundled standard schema configuration */
|
|
38
|
+
configFile(file) {
|
|
39
|
+
return file ?? standardConfig;
|
|
36
40
|
}
|
|
37
|
-
/* load a
|
|
41
|
+
/* load a YAML schema configuration, failing on any problem */
|
|
38
42
|
requireConfig(file, verbose) {
|
|
39
|
-
file = this.
|
|
40
|
-
verbose(`loading configuration "${literal(file)}"`);
|
|
43
|
+
file = this.configFile(file);
|
|
44
|
+
verbose(`loading schema configuration "${literal(file)}"`);
|
|
41
45
|
const { config, diagnostics } = loadConfig(file);
|
|
42
46
|
if (config === undefined)
|
|
43
47
|
throw new Error("invalid configuration:\n" +
|
|
@@ -53,7 +57,7 @@ export class SpecBook {
|
|
|
53
57
|
}
|
|
54
58
|
/* lint the specification Markdown files below the base directory */
|
|
55
59
|
async lint(options) {
|
|
56
|
-
return lint({ config: this.
|
|
60
|
+
return lint({ config: this.configFile(options.config),
|
|
57
61
|
basedir: options.basedir ?? ".", verbose: this.verboseOf("lint") });
|
|
58
62
|
}
|
|
59
63
|
/* export the specification Markdown files below the base directory
|
|
@@ -63,7 +67,7 @@ export class SpecBook {
|
|
|
63
67
|
as a partial or invalid specification must never be emitted) */
|
|
64
68
|
async export(options) {
|
|
65
69
|
const verbose = this.verboseOf("export");
|
|
66
|
-
const result = lint({ config: this.
|
|
70
|
+
const result = lint({ config: this.configFile(options.config),
|
|
67
71
|
basedir: options.basedir ?? ".", verbose });
|
|
68
72
|
if (result.diagnostics.length > 0)
|
|
69
73
|
throw new Error("invalid specification:\n" +
|
package/dst/specbook-cli.js
CHANGED
|
@@ -54,9 +54,10 @@ const envDefaultFlag = (name, fallback) => {
|
|
|
54
54
|
const withVerboseOption = (command) => command
|
|
55
55
|
.option("-v, --verbose", "print verbose processing information to stderr", envDefaultFlag("verbose", false));
|
|
56
56
|
/* provide the common options of the specification processing sub-commands,
|
|
57
|
-
for which the YAML schema configuration
|
|
57
|
+
for which the YAML schema configuration falls back onto the standard one */
|
|
58
58
|
const withCommonOptions = (command) => withVerboseOption(command)
|
|
59
|
-
.
|
|
59
|
+
.option("-c, --config <yaml-file>", "YAML schema configuration file " +
|
|
60
|
+
"(default: the bundled standard schema configuration)", envDefault("config"))
|
|
60
61
|
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."));
|
|
61
62
|
/* parse the command line */
|
|
62
63
|
const program = new Command();
|
|
@@ -18,20 +18,17 @@ export const describeFormat = (options) => {
|
|
|
18
18
|
"==============================\n");
|
|
19
19
|
if (options.config !== undefined && options.embed === true)
|
|
20
20
|
sections.push(textframe(`
|
|
21
|
-
The **SpecBook SCHEMA Model**
|
|
22
|
-
YAML schema configuration of the file \`${options.config}\`:
|
|
21
|
+
The **SpecBook SCHEMA Model** is the following YAML schema configuration:
|
|
23
22
|
`) + "\n```yaml\n" +
|
|
24
23
|
fs.readFileSync(options.config, "utf8").replace(/\n*$/, "\n") +
|
|
25
24
|
"```\n");
|
|
26
25
|
else if (options.config !== undefined)
|
|
27
26
|
sections.push(textframe(`
|
|
28
|
-
The **SpecBook SCHEMA Model**
|
|
29
|
-
schema configuration in the file \`${options.config}\`.
|
|
27
|
+
The **SpecBook SCHEMA Model** is the YAML schema configuration in file \`${options.config}\`.
|
|
30
28
|
`));
|
|
31
29
|
if (options.basedir !== undefined)
|
|
32
30
|
sections.push(textframe(`
|
|
33
|
-
The **SpecBook SPEC Model** of
|
|
34
|
-
specification Markdown files in the directory \`${options.basedir}\`.
|
|
31
|
+
The **SpecBook SPEC Model** is the set of Markdown specification files in directory \`${options.basedir}\`.
|
|
35
32
|
`));
|
|
36
33
|
}
|
|
37
34
|
return sections.join("\n");
|
|
@@ -27,11 +27,15 @@ const extensions = new Map([
|
|
|
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
|
-
|
|
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,}):(.+)$/);
|
|
31
33
|
if (prefixed !== null) {
|
|
32
34
|
const explicit = extensions.get(prefixed[1].toLowerCase());
|
|
33
|
-
if (explicit
|
|
34
|
-
|
|
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] };
|
|
35
39
|
}
|
|
36
40
|
if (spec === "-")
|
|
37
41
|
return { format: "json", output: spec };
|
package/dst/specbook-cmd-lint.js
CHANGED
|
@@ -25,7 +25,7 @@ const schemaFiles = (config) => {
|
|
|
25
25
|
export const lint = (options) => {
|
|
26
26
|
const diagnostics = new Array();
|
|
27
27
|
/* load the mandatory YAML schema configuration */
|
|
28
|
-
options.verbose(`loading configuration "${literal(options.config)}"`);
|
|
28
|
+
options.verbose(`loading schema configuration "${literal(options.config)}"`);
|
|
29
29
|
const loaded = loadConfig(options.config);
|
|
30
30
|
diagnostics.push(...loaded.diagnostics);
|
|
31
31
|
const config = loaded.config;
|
package/dst/specbook-config.js
CHANGED
|
@@ -22,10 +22,11 @@ const lineColOfPath = (doc, lines, path) => {
|
|
|
22
22
|
return { line: 1, column: 1 };
|
|
23
23
|
};
|
|
24
24
|
/* check the constraints of a structurally valid configuration which
|
|
25
|
-
are beyond its schema:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
be valid patterns (as they
|
|
25
|
+
are beyond its schema: sibling objects have to stay distinctly
|
|
26
|
+
resolvable, "file" fields are allowed on the first (artifact) level
|
|
27
|
+
only, property value expressions have to be syntactically valid, and
|
|
28
|
+
the names of non-artifact objects have to be valid patterns (as they
|
|
29
|
+
are compiled into regular expressions) */
|
|
29
30
|
const checkConstraints = (config, file, posOfPath) => {
|
|
30
31
|
const diagnostics = new Array();
|
|
31
32
|
const diagnose = (path, message) => {
|
|
@@ -33,8 +34,17 @@ const checkConstraints = (config, file, posOfPath) => {
|
|
|
33
34
|
diagnostics.push({ file, line: pos.line, column: pos.column, message });
|
|
34
35
|
};
|
|
35
36
|
const check = (objects, path, depth) => {
|
|
37
|
+
/* the sibling objects are resolved by kind and id on the first
|
|
38
|
+
(artifact) level and by kind alone below it, so a sibling
|
|
39
|
+
colliding on this key stays unreachable dead configuration */
|
|
40
|
+
const seen = new Set();
|
|
36
41
|
for (const [i, object] of objects.entries()) {
|
|
37
42
|
const at = [...path, i];
|
|
43
|
+
const ident = depth > 1 ? object.kind : `${object.kind}:${object.id ?? object.name ?? ""}`;
|
|
44
|
+
if (seen.has(ident))
|
|
45
|
+
diagnose([...at, "kind"], `object "${ident}" collides with a preceding sibling object (the later one is unreachable)`);
|
|
46
|
+
else
|
|
47
|
+
seen.add(ident);
|
|
38
48
|
if (depth > 1 && object.file !== undefined)
|
|
39
49
|
diagnose([...at, "file"], `"file" field is only allowed on the first (artifact) level (found on level ${depth})`);
|
|
40
50
|
for (const [j, prop] of (object.props ?? []).entries()) {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { Spec, SpecObject } from "./specbook-format-spec.js";
|
|
2
2
|
import type { Schema, SchemaDiagram } from "./specbook-format-schema.js";
|
|
3
3
|
import type { ParseContext } from "./specbook-parse-common.js";
|
|
4
|
+
export interface DiagramError {
|
|
5
|
+
reason: string;
|
|
6
|
+
object?: SpecObject;
|
|
7
|
+
}
|
|
4
8
|
export interface DiagramResult {
|
|
5
9
|
spec?: string;
|
|
6
10
|
config?: SchemaDiagram["config"];
|
|
7
|
-
errors:
|
|
11
|
+
errors: DiagramError[];
|
|
8
12
|
}
|
|
9
13
|
export declare const specDiagrams: (specification: Spec, config: Schema) => Map<SpecObject, DiagramResult>;
|
|
10
14
|
export declare const validateDiagrams: (ctx: ParseContext, specification: Spec, config: Schema) => void;
|
package/dst/specbook-diagram.js
CHANGED
|
@@ -110,13 +110,13 @@ const deriveEdges = (diagram, type, nodes, nodeSet, edgeObjects, index, anchors,
|
|
|
110
110
|
edgeNode(edgeObject, diagram.edgeSource) : parents.get(edgeObject);
|
|
111
111
|
const target = edgeNode(edgeObject, diagram.edgeTarget);
|
|
112
112
|
if (source === undefined) {
|
|
113
|
-
errors.push(diagram.edgeSource !== undefined ?
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
errors.push({ object: edgeObject, reason: diagram.edgeSource !== undefined ?
|
|
114
|
+
"carries no resolvable source reference" :
|
|
115
|
+
"has no parent object as source" });
|
|
116
116
|
continue;
|
|
117
117
|
}
|
|
118
118
|
if (target === undefined) {
|
|
119
|
-
errors.push(
|
|
119
|
+
errors.push({ object: edgeObject, reason: "carries no resolvable target reference" });
|
|
120
120
|
continue;
|
|
121
121
|
}
|
|
122
122
|
if (!nodeSet.has(source) || !nodeSet.has(target))
|
|
@@ -136,11 +136,11 @@ const deriveEdges = (diagram, type, nodes, nodeSet, edgeObjects, index, anchors,
|
|
|
136
136
|
}
|
|
137
137
|
else {
|
|
138
138
|
if (diagram.edges !== undefined)
|
|
139
|
-
errors.push("\"grid\" diagram cannot carry an \"edges\" configuration");
|
|
139
|
+
errors.push({ reason: "\"grid\" diagram cannot carry an \"edges\" configuration" });
|
|
140
140
|
if (diagram.hierarchy === true)
|
|
141
|
-
errors.push("\"grid\" diagram cannot carry a \"hierarchy\" configuration");
|
|
141
|
+
errors.push({ reason: "\"grid\" diagram cannot carry a \"hierarchy\" configuration" });
|
|
142
142
|
if (diagram.onlyConnected === true)
|
|
143
|
-
errors.push("\"grid\" diagram cannot carry an \"onlyConnected\" configuration");
|
|
143
|
+
errors.push({ reason: "\"grid\" diagram cannot carry an \"onlyConnected\" configuration" });
|
|
144
144
|
}
|
|
145
145
|
/* deduplicate the edges (the same reference can occur in
|
|
146
146
|
multiple texts of the same node object) */
|
|
@@ -167,7 +167,7 @@ const resolvePatterns = (index, errors, value, field) => {
|
|
|
167
167
|
const pattern = m[1].trim();
|
|
168
168
|
const set = resolveSet(index, pattern);
|
|
169
169
|
if (set.length === 0)
|
|
170
|
-
errors.push(`unresolvable diagram "${field}" pattern "[[${pattern}]]"`);
|
|
170
|
+
errors.push({ reason: `unresolvable diagram "${field}" pattern "[[${pattern}]]"` });
|
|
171
171
|
for (const match of set) {
|
|
172
172
|
if (!seen.has(match)) {
|
|
173
173
|
seen.add(match);
|
|
@@ -176,7 +176,7 @@ const resolvePatterns = (index, errors, value, field) => {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
if (!found)
|
|
179
|
-
errors.push(`diagram "${field}" carries no "[[...]]" reference pattern`);
|
|
179
|
+
errors.push({ reason: `diagram "${field}" carries no "[[...]]" reference pattern` });
|
|
180
180
|
return matches;
|
|
181
181
|
};
|
|
182
182
|
/* derive the Gradia spec of a single object from its "diagram:"
|
|
@@ -210,13 +210,13 @@ const deriveDiagram = (object, diagram, index, anchors, parents) => {
|
|
|
210
210
|
resolved object of an explicit "[[...]]" reference otherwise */
|
|
211
211
|
let center = object;
|
|
212
212
|
if (type !== "hub" && diagram.center !== undefined)
|
|
213
|
-
errors.push(`"${type}" diagram cannot carry a "center" configuration`);
|
|
213
|
+
errors.push({ reason: `"${type}" diagram cannot carry a "center" configuration` });
|
|
214
214
|
if (type === "hub" && diagram.center !== undefined && diagram.center !== "self") {
|
|
215
215
|
const reference = diagram.center.match(referenceOnce)?.[1].trim();
|
|
216
216
|
const resolved = reference !== undefined ? resolveUnique(index, reference) : undefined;
|
|
217
217
|
if (resolved?.target === undefined) {
|
|
218
|
-
errors.push((resolved?.ambiguous === true ? "ambiguous" : "unresolvable") +
|
|
219
|
-
|
|
218
|
+
errors.push({ reason: (resolved?.ambiguous === true ? "ambiguous" : "unresolvable") +
|
|
219
|
+
` diagram "center" reference "${diagram.center}"` });
|
|
220
220
|
return { errors };
|
|
221
221
|
}
|
|
222
222
|
center = resolved.target;
|
|
@@ -227,7 +227,7 @@ const deriveDiagram = (object, diagram, index, anchors, parents) => {
|
|
|
227
227
|
Gradia requires exactly this constrained topology */
|
|
228
228
|
if (type === "hub") {
|
|
229
229
|
if (!nodeSet.has(center))
|
|
230
|
-
errors.push(`"hub" diagram center "${center.name}" is not part of the node set`);
|
|
230
|
+
errors.push({ reason: `"hub" diagram center "${center.name}" is not part of the node set` });
|
|
231
231
|
else {
|
|
232
232
|
edges = edges.filter((edge) => (edge.source === center || edge.target === center)
|
|
233
233
|
&& edge.source !== edge.target);
|
|
@@ -250,7 +250,7 @@ const deriveDiagram = (object, diagram, index, anchors, parents) => {
|
|
|
250
250
|
nodes = nodes.filter((node) => connected.has(node));
|
|
251
251
|
}
|
|
252
252
|
if (nodes.length === 0)
|
|
253
|
-
errors.push("diagram yields no nodes");
|
|
253
|
+
errors.push({ reason: "diagram yields no nodes" });
|
|
254
254
|
if (errors.length > 0)
|
|
255
255
|
return { errors };
|
|
256
256
|
/* the "collapse" handling (enabled by default) silently omits a
|
|
@@ -277,9 +277,21 @@ export const specDiagrams = (specification, config) => {
|
|
|
277
277
|
/* validate all configured diagrams of a specification, reporting
|
|
278
278
|
every invalid diagram situation as a file/line-precise diagnostic */
|
|
279
279
|
export const validateDiagrams = (ctx, specification, config) => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
for (const error of result.errors)
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
const seen = new Set();
|
|
281
|
+
for (const [object, result] of specDiagrams(specification, config))
|
|
282
|
+
for (const error of result.errors) {
|
|
283
|
+
/* attribute an edge object situation to the offending edge
|
|
284
|
+
object itself and report it just once, as a wildcard
|
|
285
|
+
"edges" pattern lets every diagram walk the very same
|
|
286
|
+
edge objects again */
|
|
287
|
+
const meta = ctx.objectMeta.get(error.object ?? object) ?? { file: "", line: 1 };
|
|
288
|
+
const message = error.object !== undefined ?
|
|
289
|
+
`invalid diagram edge on ${error.object.kind} "${error.object.name}": ${error.reason}` :
|
|
290
|
+
`invalid diagram on ${object.kind} "${object.name}": ${error.reason}`;
|
|
291
|
+
const key = `${meta.file}${meta.line}${message}`;
|
|
292
|
+
if (seen.has(key))
|
|
293
|
+
continue;
|
|
294
|
+
seen.add(key);
|
|
295
|
+
ctx.diagnose(meta.file, meta.line, message);
|
|
296
|
+
}
|
|
285
297
|
};
|
|
@@ -342,7 +342,8 @@ const inlineValue = (kind, { key, value, embedding }) => {
|
|
|
342
342
|
.map((content) => `<div class="embedding">${content}</div>`).join("");
|
|
343
343
|
if (member === undefined || text === "")
|
|
344
344
|
return safe(`${inline(text)}${embeddings}`);
|
|
345
|
-
const items = member === "tags" ?
|
|
345
|
+
const items = member === "tags" ?
|
|
346
|
+
splitItems(text).filter((item) => item !== "") : [text];
|
|
346
347
|
return safe(items.map((item) => `<span class="value-member">${inline(item)}</span>`).join(" ") + embeddings);
|
|
347
348
|
};
|
|
348
349
|
/* expand the inline Markdown of the property values */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
6
|
import { specDiagrams } from "./specbook-diagram.js";
|
|
7
|
+
import { becauseRegex } from "./specbook-parse-common.js";
|
|
7
8
|
/* format a timestamp in the frontmatter format */
|
|
8
9
|
const formatTimestamp = (date) => {
|
|
9
10
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -19,6 +20,10 @@ const renderKeyValuesMd = (properties, indent = "") => {
|
|
|
19
20
|
`${indent}- ${property.key}:\n${indent} ${property.value}` :
|
|
20
21
|
`${indent}- ${`${property.key}:`.padEnd(width)} ${property.value}`.trimEnd()).join("\n");
|
|
21
22
|
};
|
|
23
|
+
/* whether a property can be a Concise Format segment: a value carrying
|
|
24
|
+
a ";" would split the segment, and a value carrying the rationale
|
|
25
|
+
marker would not be taken for a property at all on re-parsing */
|
|
26
|
+
const segmentableMd = (property) => !property.value.includes(";") && !becauseRegex.test(property.value);
|
|
22
27
|
/* render a description statement with its optional rationale */
|
|
23
28
|
const renderDescriptionMd = (description) => description.description + (description.rationale !== undefined ?
|
|
24
29
|
`, BECAUSE ${description.rationale}` : "");
|
|
@@ -27,20 +32,22 @@ const renderDescriptionMd = (description) => description.description + (descript
|
|
|
27
32
|
const nameSuffixMd = (object) => (object.primary === true ? " (*)" : "") +
|
|
28
33
|
(object.paren !== undefined ? ` (${object.paren})` : "") +
|
|
29
34
|
(object.anchor !== undefined ? ` {{${object.anchor}}}` : "");
|
|
30
|
-
/* whether any descendant of an object carries a
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
/* whether any descendant of an object carries a description which no
|
|
36
|
+
Concise Format item can carry -- a multi-line one (a fenced code
|
|
37
|
+
block, an ordered list, a blockquote, or multiple paragraphs) or one
|
|
38
|
+
carrying a ";", as only a ";"-free description stays a single segment
|
|
39
|
+
on re-parsing instead of splitting off a spurious property */
|
|
40
|
+
const complexBelowMd = (object) => object.childs.some((child) => (child.description !== undefined
|
|
41
|
+
&& /[\n;]/.test(renderDescriptionMd(child.description))) || complexBelowMd(child));
|
|
35
42
|
/* render an object in the Concise Format as a list item of the
|
|
36
43
|
given nesting depth, with its childs as nested list items */
|
|
37
44
|
const renderConciseMd = (object, depth = 0) => {
|
|
38
45
|
const indent = " ".repeat(depth * 4);
|
|
39
46
|
const segments = [`${object.kind}: ${object.name}${nameSuffixMd(object)}`];
|
|
40
|
-
/* a
|
|
41
|
-
|
|
42
|
-
const wrapped = object.properties.filter((property) => property
|
|
43
|
-
const inline = object.properties.filter((property) =>
|
|
47
|
+
/* a property which cannot be a segment has to become a nested
|
|
48
|
+
key/value item below the item instead */
|
|
49
|
+
const wrapped = object.properties.filter((property) => !segmentableMd(property));
|
|
50
|
+
const inline = object.properties.filter((property) => segmentableMd(property));
|
|
44
51
|
segments.push(...inline.map((property) => `${property.key}: ${property.value}`));
|
|
45
52
|
if (object.description !== undefined)
|
|
46
53
|
segments.push(renderDescriptionMd(object.description));
|
|
@@ -57,10 +64,10 @@ const renderConciseMd = (object, depth = 0) => {
|
|
|
57
64
|
};
|
|
58
65
|
/* render an object in the Complex Format (levels 1-3), with childs from
|
|
59
66
|
level 4 upwards in the Concise Format -- unless a descendant carries
|
|
60
|
-
a
|
|
61
|
-
the format) stay in the Complex Format down to heading
|
|
62
|
-
and the optionally derived Gradia diagram spec embedded
|
|
63
|
-
"gradia" fenced code block below the heading */
|
|
67
|
+
a description no concise item can carry, so all childs (as siblings
|
|
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
|
|
70
|
+
as a "gradia" fenced code block below the heading */
|
|
64
71
|
const renderObjectMd = (object, level, diagrams) => {
|
|
65
72
|
const heading = `${"#".repeat(level)}${" ".repeat(Math.max(1, 4 - level))}` +
|
|
66
73
|
`${object.kind}: ${object.name}${nameSuffixMd(object)}`;
|
|
@@ -74,7 +81,7 @@ const renderObjectMd = (object, level, diagrams) => {
|
|
|
74
81
|
parts.push(renderDescriptionMd(object.description));
|
|
75
82
|
/* the childs share one format, as a concise item following
|
|
76
83
|
a heading would attach to the heading's object instead */
|
|
77
|
-
if (level >= 3 && !
|
|
84
|
+
if (level >= 3 && !complexBelowMd(object))
|
|
78
85
|
parts.push(object.childs.map((child) => renderConciseMd(child)).join("\n"));
|
|
79
86
|
else
|
|
80
87
|
parts.push(...object.childs.map((child) => renderObjectMd(child, level + 1, diagrams)));
|
package/dst/specbook-format.md
CHANGED
|
@@ -37,9 +37,9 @@ task:
|
|
|
37
37
|
- **SpecBook SCHEMA Model**:
|
|
38
38
|
This is an object model, described in *YAML*, outside of
|
|
39
39
|
**SpecBook**, which defines a particular **SpecBook** *Schema*.
|
|
40
|
-
It is user-supplied
|
|
41
|
-
the **SpecBook SPEC Model** to
|
|
42
|
-
well-structured.
|
|
40
|
+
It is user-supplied (or the bundled standard one) and non-generic
|
|
41
|
+
but reusable and applied onto the **SpecBook SPEC Model** to
|
|
42
|
+
validate that it is domain-wise well-structured.
|
|
43
43
|
|
|
44
44
|
- **SpecBook SPEC Model**:
|
|
45
45
|
This is an object model, described in *Markdown*, outside
|
|
@@ -160,7 +160,8 @@ type SchemaGradiaConfig = Partial<{
|
|
|
160
160
|
BECAUSE artifacts and their content follow one recursive structure
|
|
161
161
|
|
|
162
162
|
- `SchemaObject.kind: string`:
|
|
163
|
-
kind name of the object (e.g. `SPEC`, `REQUIREMENT`),
|
|
163
|
+
kind name of the object (e.g. `SPEC`, `REQUIREMENT`), unique among
|
|
164
|
+
its siblings, on level 1 together with its `id`,
|
|
164
165
|
BECAUSE it discriminates objects and anchors `[[xxx]]` references
|
|
165
166
|
|
|
166
167
|
- `SchemaObject.name?: string`:
|
|
@@ -530,7 +531,9 @@ SpecBook SCHEMA Model
|
|
|
530
531
|
### Artifacts
|
|
531
532
|
|
|
532
533
|
A schema consists of a YAML file, which is exactly a serialization of
|
|
533
|
-
the object model defined by the **SpecBook SCHEMA Meta Model**.
|
|
534
|
+
the object model defined by the **SpecBook SCHEMA Meta Model**. If no
|
|
535
|
+
particular schema is given, the standard schema configuration bundled
|
|
536
|
+
with **SpecBook** applies.
|
|
534
537
|
|
|
535
538
|
SpecBook SPEC Model
|
|
536
539
|
-------------------
|
|
@@ -557,6 +560,12 @@ follow each other on level 1, below the single frontmatter block of that
|
|
|
557
560
|
file. Both ATX headings (`# <text/>`, `## <text/>`, ...) and Setext
|
|
558
561
|
headings (`<text/>` underlined with `===` or `---`) are supported.
|
|
559
562
|
|
|
563
|
+
As the timestamps are a property of the *file* and not of the individual
|
|
564
|
+
artifact, the normalized Markdown export -- which merges the entire
|
|
565
|
+
corpus into a single document -- carries the earliest `Created:` and the
|
|
566
|
+
latest `Modified:` timestamp of all artifacts in its single frontmatter
|
|
567
|
+
block, so the per-artifact timestamps do not survive this round-trip.
|
|
568
|
+
|
|
560
569
|
### Object Model
|
|
561
570
|
|
|
562
571
|
An artifact carries a tree of *objects*. Every object has a mandatory
|
|
@@ -644,12 +653,13 @@ the item would look like a key/value pair) -- backquote it in this case.
|
|
|
644
653
|
|
|
645
654
|
When **SpecBook** itself generates specification Markdown, it emits
|
|
646
655
|
the Complex Format on levels 1-3 and the Concise Format from level 4
|
|
647
|
-
upwards -- unless a descendant of an object carries a
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
656
|
+
upwards -- unless a descendant of an object carries a description which
|
|
657
|
+
no single list item can carry: a multi-line one (a fenced code block, an
|
|
658
|
+
ordered list, a blockquote, or multiple paragraphs) or one carrying a
|
|
659
|
+
`;` (which a re-parse would split into a spurious property plus a
|
|
660
|
+
truncated description). All child objects of such an object then stay in
|
|
661
|
+
the Complex Format (as sibling objects have to share the format), with
|
|
662
|
+
headings reaching down to level 6. When **SpecBook** edits existing files, it mirrors the format each
|
|
653
663
|
existing object already uses.
|
|
654
664
|
|
|
655
665
|
### Names, Anchors, and Ids
|
|
@@ -679,9 +689,10 @@ In the Complex Format, each property is an unordered list item of the
|
|
|
679
689
|
form `<key/>: <value/>`, whose value may continue on the following
|
|
680
690
|
indented lines of the item (joined with spaces). In the Concise Format,
|
|
681
691
|
each property is a `<key/>: <value/>` segment. A value carrying a `;`
|
|
682
|
-
cannot be a segment and has to
|
|
683
|
-
|
|
684
|
-
|
|
692
|
+
or a ` BECAUSE ` cannot be a segment and has to become a key/value item
|
|
693
|
+
nested below the item instead -- a `;` value additionally starting on a
|
|
694
|
+
continuation line of that item, as a `;` on the first line of a list
|
|
695
|
+
item marks a Concise Format object. The key is matched
|
|
685
696
|
against the schema exactly as written and may occur only once per
|
|
686
697
|
object, and the value has to satisfy the configured constraint as a
|
|
687
698
|
whole.
|
|
@@ -695,7 +706,9 @@ fenced code blocks. A fenced code block
|
|
|
695
706
|
of language `gradia` is skipped, as it is the derived diagram which
|
|
696
707
|
**SpecBook** itself emits into exported Markdown and which must not
|
|
697
708
|
become authored content on a re-parse. In the Concise Format, the
|
|
698
|
-
description is formed by the non-property segments of the item
|
|
709
|
+
description is formed by the non-property segments of the item, so a
|
|
710
|
+
description carrying a `;` cannot be a segment either and forces its
|
|
711
|
+
object into the Complex Format.
|
|
699
712
|
|
|
700
713
|
A description is split into its *statement* and *rationale* at the first
|
|
701
714
|
`, BECAUSE ` (or `, **BECAUSE** `) occurrence:
|