@rse/specbook 1.5.2 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +201 -0
- package/README.md +288 -43
- package/dst/specbook-api.d.ts +23 -3
- package/dst/specbook-api.js +133 -59
- package/dst/specbook-cli.js +93 -26
- package/dst/specbook-cmd-describe.js +4 -4
- package/dst/specbook-cmd-export-schema.d.ts +8 -0
- package/dst/specbook-cmd-export-schema.js +441 -0
- package/dst/specbook-cmd-export.js +23 -6
- package/dst/specbook-cmd-init-sample.d.ts +4 -0
- package/dst/specbook-cmd-init-sample.js +322 -0
- package/dst/specbook-cmd-init.d.ts +1 -0
- package/dst/specbook-cmd-init.js +42 -10
- package/dst/specbook-cmd-lint.js +46 -48
- package/dst/specbook-cmd-query.d.ts +30 -0
- package/dst/specbook-cmd-query.js +165 -0
- package/dst/specbook-config.d.ts +9 -1
- package/dst/specbook-config.js +135 -10
- package/dst/specbook-coverage.js +2 -2
- package/dst/specbook-diagram.d.ts +7 -5
- package/dst/specbook-diagram.js +546 -143
- package/dst/specbook-export-ast.d.ts +1 -1
- package/dst/specbook-export-ast.js +11 -8
- package/dst/specbook-export-code.js +3 -3
- package/dst/specbook-export-common.d.ts +1 -1
- package/dst/specbook-export-common.js +3 -2
- package/dst/specbook-export-diagram.d.ts +1 -1
- package/dst/specbook-export-diagram.js +25 -7
- package/dst/specbook-export-html-fold.d.ts +16 -6
- package/dst/specbook-export-html-fold.js +65 -44
- package/dst/specbook-export-html-render.js +107 -26
- package/dst/specbook-export-html-search.js +18 -20
- package/dst/specbook-export-html-template.js +38 -9
- package/dst/specbook-export-html-widget.d.ts +2 -0
- package/dst/specbook-export-html-widget.js +219 -19
- package/dst/specbook-export-html.css +208 -38
- package/dst/specbook-export-html.js +96 -53
- package/dst/specbook-export-md.d.ts +2 -1
- package/dst/specbook-export-md.js +7 -8
- package/dst/specbook-export-pdf.js +1 -1
- package/dst/specbook-files.d.ts +2 -0
- package/dst/specbook-files.js +41 -0
- package/dst/specbook-format-meta.yaml +401 -0
- package/dst/specbook-format-pm.yaml +1240 -0
- package/dst/specbook-format-schema.d.ts +33 -3
- package/dst/specbook-format-schema.js +49 -3
- package/dst/specbook-format.md +427 -47
- package/dst/specbook-format.yaml +524 -228
- package/dst/specbook-ignore.js +2 -2
- package/dst/specbook-link.d.ts +4 -0
- package/dst/specbook-link.js +61 -6
- package/dst/specbook-mcp.js +103 -19
- package/dst/specbook-parse-semantic.d.ts +1 -0
- package/dst/specbook-parse-semantic.js +11 -4
- package/dst/specbook-parse-syntax.d.ts +1 -0
- package/dst/specbook-parse-syntax.js +1 -1
- package/dst/specbook-parse.d.ts +1 -1
- package/dst/specbook-parse.js +3 -3
- package/dst/specbook-project.d.ts +2 -0
- package/dst/specbook-project.js +15 -5
- package/dst/specbook-remote.d.ts +28 -0
- package/dst/specbook-remote.js +429 -0
- package/package.json +32 -20
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Specification Book (SpecBook)
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import ASTQ from "astq";
|
|
7
|
+
import { Marked } from "marked";
|
|
8
|
+
import JSON5 from "json5";
|
|
9
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
10
|
+
import { encode as encodeToon } from "@toon-format/toon";
|
|
11
|
+
import { buildLinkIndex, referenceTargets, resolveUnique, expandReferences } from "./specbook-link.js";
|
|
12
|
+
import { parseOutputSpec } from "./specbook-cmd-export.js";
|
|
13
|
+
/* the fixed attributes of an object node, which win over
|
|
14
|
+
the same-named property keys */
|
|
15
|
+
const objectFields = ["kind", "id", "anchor", "paren", "name", "primary",
|
|
16
|
+
"description", "rationale", "elaboration"];
|
|
17
|
+
/* the private Markdown renderer of the text helpers, as the
|
|
18
|
+
"marked" singleton is shared with the application */
|
|
19
|
+
const marked = new Marked();
|
|
20
|
+
/* render Markdown tokens into plain text: the blocks joined by the
|
|
21
|
+
separator, the inline markup dropped, and the code kept verbatim */
|
|
22
|
+
const plainOf = (tokens, separator) => tokens.map((generic) => {
|
|
23
|
+
const token = generic;
|
|
24
|
+
if (token.type === "space" || token.type === "hr" || token.type === "html" || token.type === "def")
|
|
25
|
+
return "";
|
|
26
|
+
else if (token.type === "code" || token.type === "codespan")
|
|
27
|
+
return token.text;
|
|
28
|
+
else if (token.type === "br")
|
|
29
|
+
return "\n";
|
|
30
|
+
else if (token.type === "blockquote")
|
|
31
|
+
return plainOf(token.tokens, "\n\n");
|
|
32
|
+
else if (token.type === "list")
|
|
33
|
+
return token.items.map((item) => plainOf(item.tokens, "\n")).join("\n");
|
|
34
|
+
else if (token.type === "table")
|
|
35
|
+
return [token.header, ...token.rows].map((row) => row.map((cell) => plainOf(cell.tokens, "")).join(" | ")).join("\n");
|
|
36
|
+
else if ("tokens" in token && token.tokens !== undefined)
|
|
37
|
+
return plainOf(token.tokens, "");
|
|
38
|
+
else
|
|
39
|
+
return "text" in token ? token.text : "";
|
|
40
|
+
}).filter((part) => part !== "").join(separator);
|
|
41
|
+
/* the serialization formats of the query results (the AST export ones) */
|
|
42
|
+
export const queryFormats = ["json", "json5", "yaml", "toon"];
|
|
43
|
+
/* determine the format and file of a query output: an explicit format
|
|
44
|
+
wins, else it is inferred exactly like the one of an export output
|
|
45
|
+
(a "<format>:" prefix, else the filename extension, "-" being JSON),
|
|
46
|
+
where only the serialization formats apply */
|
|
47
|
+
export const parseQueryOutput = (output, format) => {
|
|
48
|
+
const spec = format !== undefined ? { format, output } : parseOutputSpec(output);
|
|
49
|
+
if (!queryFormats.includes(spec.format))
|
|
50
|
+
throw new Error(`unsupported query format "${spec.format}" (supported: ${queryFormats.join(", ")})`);
|
|
51
|
+
return { format: spec.format, output: spec.output };
|
|
52
|
+
};
|
|
53
|
+
/* render query result nodes in a serialization format: their subtrees
|
|
54
|
+
included, the embedded file contents dropped (the bulk no consumer
|
|
55
|
+
can use), and the dates as ISO strings */
|
|
56
|
+
export const renderNodes = (nodes, format = "json") => {
|
|
57
|
+
const plain = JSON.parse(JSON.stringify(nodes, (key, value) => key === "embedding" ? undefined : value));
|
|
58
|
+
if (format === "json")
|
|
59
|
+
return JSON.stringify(plain, null, 4) + "\n";
|
|
60
|
+
else if (format === "json5")
|
|
61
|
+
return JSON5.stringify(plain, null, 4) + "\n";
|
|
62
|
+
else if (format === "yaml")
|
|
63
|
+
return stringifyYaml(plain);
|
|
64
|
+
else
|
|
65
|
+
return encodeToon(plain) + "\n";
|
|
66
|
+
};
|
|
67
|
+
/* a loaded specification, queryable through the XPath-like ASTq
|
|
68
|
+
selectors: the root node (type "specification") holds the artifacts
|
|
69
|
+
(type "artifact"), which hold the object trees (typed by their kind,
|
|
70
|
+
whitespace dashed), and the pre-registered function "refs()" yields
|
|
71
|
+
the objects the references of the current object resolve to */
|
|
72
|
+
export class SpecBookDocument {
|
|
73
|
+
specification;
|
|
74
|
+
config;
|
|
75
|
+
diagnostics;
|
|
76
|
+
astq = new ASTQ();
|
|
77
|
+
parents = new WeakMap();
|
|
78
|
+
attrs = new WeakMap();
|
|
79
|
+
index;
|
|
80
|
+
constructor(specification, config, diagnostics, origins) {
|
|
81
|
+
this.specification = specification;
|
|
82
|
+
this.config = config;
|
|
83
|
+
this.diagnostics = diagnostics;
|
|
84
|
+
this.index = buildLinkIndex(specification);
|
|
85
|
+
/* index the parents once, as the objects carry no parent reference */
|
|
86
|
+
const walk = (object, parent) => {
|
|
87
|
+
this.parents.set(object, parent);
|
|
88
|
+
for (const child of object.children)
|
|
89
|
+
walk(child, object);
|
|
90
|
+
};
|
|
91
|
+
for (const artifact of specification.artifacts) {
|
|
92
|
+
this.parents.set(artifact, specification);
|
|
93
|
+
for (const object of artifact.objects)
|
|
94
|
+
walk(object, artifact);
|
|
95
|
+
}
|
|
96
|
+
/* determine the attributes of a node once: the timestamps and
|
|
97
|
+
source file of an artifact, and the fixed fields of an object
|
|
98
|
+
followed by its properties (the first of duplicate keys winning) */
|
|
99
|
+
const attributesOf = (node) => {
|
|
100
|
+
let attrs = this.attrs.get(node);
|
|
101
|
+
if (attrs === undefined) {
|
|
102
|
+
attrs = new Map();
|
|
103
|
+
if ("objects" in node) {
|
|
104
|
+
attrs.set("created", node.created.toISOString());
|
|
105
|
+
attrs.set("modified", node.modified.toISOString());
|
|
106
|
+
attrs.set("file", origins.get(node));
|
|
107
|
+
}
|
|
108
|
+
else if ("kind" in node) {
|
|
109
|
+
const fields = [node.kind, node.id, node.anchor, node.paren, node.name, node.primary,
|
|
110
|
+
node.description?.description, node.description?.rationale, node.description?.elaboration];
|
|
111
|
+
for (const [i, value] of fields.entries())
|
|
112
|
+
if (value !== undefined)
|
|
113
|
+
attrs.set(objectFields[i], value);
|
|
114
|
+
for (const property of node.properties)
|
|
115
|
+
if (!objectFields.includes(property.key) && !attrs.has(property.key))
|
|
116
|
+
attrs.set(property.key, property.value);
|
|
117
|
+
}
|
|
118
|
+
this.attrs.set(node, attrs);
|
|
119
|
+
}
|
|
120
|
+
return attrs;
|
|
121
|
+
};
|
|
122
|
+
/* access the specification tree exclusively, as the plain AST
|
|
123
|
+
objects would otherwise be tasted by the built-in JSON adapter */
|
|
124
|
+
const adapter = {
|
|
125
|
+
taste: (node) => node === specification || this.parents.has(node),
|
|
126
|
+
getParentNode: (node) => this.parents.get(node) ?? null,
|
|
127
|
+
getChildNodes: (node) => "artifacts" in node ? node.artifacts :
|
|
128
|
+
"objects" in node ? node.objects : node.children,
|
|
129
|
+
getNodeType: (node) => "artifacts" in node ? "specification" :
|
|
130
|
+
"objects" in node ? "artifact" : node.kind.replace(/\s+/g, "-"),
|
|
131
|
+
getNodeAttrNames: (node) => Array.from(attributesOf(node).keys()),
|
|
132
|
+
getNodeAttrValue: (node, attr) => attributesOf(node).get(attr)
|
|
133
|
+
};
|
|
134
|
+
this.astq.adapter(adapter, true);
|
|
135
|
+
this.astq.func("refs", (_adapter, node) => "kind" in node ? referenceTargets(this.index, node) : []);
|
|
136
|
+
}
|
|
137
|
+
/* query the nodes an ASTq selector matches, starting at the root,
|
|
138
|
+
with the values of the "{name}" parameters of the selector */
|
|
139
|
+
query(selector, params) {
|
|
140
|
+
return this.astq.query(this.specification, selector, params);
|
|
141
|
+
}
|
|
142
|
+
/* the parent of a node (undefined for the root) */
|
|
143
|
+
parent(node) {
|
|
144
|
+
return this.parents.get(node);
|
|
145
|
+
}
|
|
146
|
+
/* register a custom query function (or replace a registered one) */
|
|
147
|
+
func(name, fn) {
|
|
148
|
+
this.astq.func(name, fn);
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
/* replace the references of a Markdown text (outside its code) by
|
|
152
|
+
the names of their targets, resolved nearest to the referencing
|
|
153
|
+
object (if given), an unresolvable one keeping its reference text */
|
|
154
|
+
expand(text, from) {
|
|
155
|
+
return expandReferences(text, (reference) => resolveUnique(this.index, reference, from).target?.name ?? reference);
|
|
156
|
+
}
|
|
157
|
+
/* render a Markdown text (a description part or property value) into plain text */
|
|
158
|
+
plain(text, from) {
|
|
159
|
+
return plainOf(marked.lexer(this.expand(text, from)), "\n\n");
|
|
160
|
+
}
|
|
161
|
+
/* render a Markdown text (a description part or property value) into an HTML fragment */
|
|
162
|
+
html(text, from) {
|
|
163
|
+
return marked.parse(this.expand(text, from), { async: false });
|
|
164
|
+
}
|
|
165
|
+
}
|
package/dst/specbook-config.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { LineCounter, type Document } from "yaml";
|
|
2
|
-
import { Schema } from "./specbook-format-schema.js";
|
|
2
|
+
import { Schema, type SchemaObject, type SchemaProperty } from "./specbook-format-schema.js";
|
|
3
3
|
import { type Diagnostic } from "./specbook-diagnostic.js";
|
|
4
4
|
type YamlPath = (string | number)[];
|
|
5
5
|
export declare const lineColOfPath: (doc: Document, lines: LineCounter, path: YamlPath) => {
|
|
6
6
|
line: number;
|
|
7
7
|
column: number;
|
|
8
8
|
};
|
|
9
|
+
export interface ConfigOverlap {
|
|
10
|
+
artifact: string;
|
|
11
|
+
files: string[];
|
|
12
|
+
}
|
|
13
|
+
export type ConfigOrigins = Map<string, string[]>;
|
|
14
|
+
export declare const originKey: (chain: SchemaObject[], prop?: SchemaProperty) => string;
|
|
9
15
|
export declare const configStatistics: (config: Schema) => {
|
|
10
16
|
objects: number;
|
|
11
17
|
links: number;
|
|
@@ -13,5 +19,7 @@ export declare const configStatistics: (config: Schema) => {
|
|
|
13
19
|
export declare const loadConfig: (files: string[]) => {
|
|
14
20
|
config?: Schema;
|
|
15
21
|
diagnostics: Diagnostic[];
|
|
22
|
+
overlaps: ConfigOverlap[];
|
|
23
|
+
origins: ConfigOrigins;
|
|
16
24
|
};
|
|
17
25
|
export {};
|
package/dst/specbook-config.js
CHANGED
|
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import { parseDocument, isNode, LineCounter } from "yaml";
|
|
8
8
|
import * as v from "valibot";
|
|
9
9
|
import { mergeWith, isPlainObject } from "es-toolkit";
|
|
10
|
-
import { Schema } from "./specbook-format-schema.js";
|
|
10
|
+
import { Schema, diagramsOf, reservedAttributes } from "./specbook-format-schema.js";
|
|
11
11
|
import { compileValueExpr, anchored, admitsReferences } from "./specbook-parse-value.js";
|
|
12
12
|
/* determine line/column of a YAML document path via the node ranges
|
|
13
13
|
of the parsed document, falling back to the closest ancestor node */
|
|
@@ -25,14 +25,16 @@ export const lineColOfPath = (doc, lines, path) => {
|
|
|
25
25
|
/* the identity by which the elements of the lists of the configuration
|
|
26
26
|
are matched on merging: the artifacts by their kind plus id or name
|
|
27
27
|
and the nested objects by their kind alone (exactly like the sibling
|
|
28
|
-
resolution), the properties by their name,
|
|
29
|
-
by their value */
|
|
28
|
+
resolution), the properties by their name, the diagrams by their
|
|
29
|
+
type, and the scalar entries by their value */
|
|
30
30
|
const identityOf = (item, list) => {
|
|
31
31
|
if (!isPlainObject(item))
|
|
32
32
|
return String(item);
|
|
33
33
|
const object = item;
|
|
34
34
|
if (list === "props")
|
|
35
35
|
return String(object.name ?? "");
|
|
36
|
+
else if (list === "diagram")
|
|
37
|
+
return String(object.type ?? "graph");
|
|
36
38
|
else if (list === "children")
|
|
37
39
|
return String(object.kind ?? "");
|
|
38
40
|
else
|
|
@@ -60,7 +62,15 @@ const mergeConfig = (target, source, list) => {
|
|
|
60
62
|
return items;
|
|
61
63
|
}
|
|
62
64
|
else if (isPlainObject(target) && isPlainObject(source))
|
|
63
|
-
return mergeWith(target, source, (t, s, key) =>
|
|
65
|
+
return mergeWith(target, source, (t, s, key) => {
|
|
66
|
+
if (Array.isArray(t) && Array.isArray(s))
|
|
67
|
+
return mergeConfig(t, s, key);
|
|
68
|
+
/* a single diagram and a list of diagrams merge as lists */
|
|
69
|
+
if (key === "diagram" && (Array.isArray(t) || Array.isArray(s))
|
|
70
|
+
&& t !== undefined && t !== null && s !== undefined && s !== null)
|
|
71
|
+
return mergeConfig(Array.isArray(t) ? t : [t], Array.isArray(s) ? s : [s], key);
|
|
72
|
+
return undefined;
|
|
73
|
+
});
|
|
64
74
|
else
|
|
65
75
|
return source;
|
|
66
76
|
};
|
|
@@ -75,6 +85,59 @@ const unshared = (value) => {
|
|
|
75
85
|
else
|
|
76
86
|
return value;
|
|
77
87
|
};
|
|
88
|
+
/* the key of a node of the merged configuration in its origins: the
|
|
89
|
+
identities of the object kinds along its path (the artifact first),
|
|
90
|
+
plus the identity of a property of the last one */
|
|
91
|
+
export const originKey = (chain, prop) => chain.map((object, i) => identityOf(object, i > 0 ? "children" : undefined)).join("/") +
|
|
92
|
+
(prop !== undefined ? `#${identityOf(prop, "props")}` : "");
|
|
93
|
+
/* determine the configuration files contributing every object kind
|
|
94
|
+
and property, by walking the raw documents along the very identities
|
|
95
|
+
the merge matches the list elements with */
|
|
96
|
+
const originsOf = (docs) => {
|
|
97
|
+
const origins = new Map();
|
|
98
|
+
const add = (key, file) => {
|
|
99
|
+
const files = origins.get(key) ?? [];
|
|
100
|
+
if (!files.includes(file))
|
|
101
|
+
files.push(file);
|
|
102
|
+
origins.set(key, files);
|
|
103
|
+
};
|
|
104
|
+
const walk = (items, prefix, depth, file) => {
|
|
105
|
+
if (!Array.isArray(items))
|
|
106
|
+
return;
|
|
107
|
+
for (const item of items) {
|
|
108
|
+
if (!isPlainObject(item))
|
|
109
|
+
continue;
|
|
110
|
+
const object = item;
|
|
111
|
+
const key = (prefix !== "" ? `${prefix}/` : "") + identityOf(item, depth > 1 ? "children" : undefined);
|
|
112
|
+
add(key, file);
|
|
113
|
+
if (Array.isArray(object.props))
|
|
114
|
+
for (const prop of object.props)
|
|
115
|
+
if (isPlainObject(prop))
|
|
116
|
+
add(`${key}#${identityOf(prop, "props")}`, file);
|
|
117
|
+
walk(object.children, key, depth + 1, file);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
for (const { file, raw } of docs)
|
|
121
|
+
walk(raw, "", 1, file);
|
|
122
|
+
return origins;
|
|
123
|
+
};
|
|
124
|
+
/* determine the artifacts several configuration files define */
|
|
125
|
+
const overlapsOf = (docs) => {
|
|
126
|
+
const defined = new Map();
|
|
127
|
+
for (const { file, raw } of docs)
|
|
128
|
+
if (Array.isArray(raw))
|
|
129
|
+
for (const item of raw) {
|
|
130
|
+
if (!isPlainObject(item))
|
|
131
|
+
continue;
|
|
132
|
+
const object = item;
|
|
133
|
+
const kind = String(object.kind ?? "");
|
|
134
|
+
const label = object.id !== undefined ? `${kind} (${String(object.id)})` : `${kind}: ${String(object.name ?? "")}`;
|
|
135
|
+
const overlap = defined.get(identityOf(item)) ?? { artifact: label, files: [] };
|
|
136
|
+
overlap.files.push(file);
|
|
137
|
+
defined.set(identityOf(item), overlap);
|
|
138
|
+
}
|
|
139
|
+
return Array.from(defined.values()).filter((overlap) => overlap.files.length > 1);
|
|
140
|
+
};
|
|
78
141
|
/* determine file and line/column of a path into the merged
|
|
79
142
|
configuration by translating the path into every document (from the
|
|
80
143
|
last to the first one, as a later document overrides an earlier one),
|
|
@@ -190,6 +253,58 @@ const checkAutomaton = (object, at, diagnose) => {
|
|
|
190
253
|
if (nodes !== undefined && propOf(nodes, automaton[field]) === undefined)
|
|
191
254
|
diagnose([...at, "automaton", field], `"automaton" ${field} property "${automaton[field]}" is not a property of "${automaton.nodes}"`);
|
|
192
255
|
};
|
|
256
|
+
/* check the diagrams of an object (if any): a displayed property must
|
|
257
|
+
not be named like a node attribute Gradia consumes instead of
|
|
258
|
+
displaying it (e.g. "mark" or "layer"), as it would silently alter
|
|
259
|
+
the node instead of annotating it */
|
|
260
|
+
const checkDiagrams = (object, at, diagnose) => {
|
|
261
|
+
for (const [i, diagram] of diagramsOf(object).entries()) {
|
|
262
|
+
const base = Array.isArray(object.diagram) ? [...at, "diagram", i] : [...at, "diagram"];
|
|
263
|
+
for (const [j, key] of (diagram.properties ?? []).entries())
|
|
264
|
+
if (reservedAttributes.includes(key))
|
|
265
|
+
diagnose([...base, "properties", j], `diagram property "${key}" collides with the reserved Gradia node attribute "${key}"`);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
/* check the multiplicity of an object (if any): it names a property
|
|
269
|
+
of the object kind and a valid regular expression */
|
|
270
|
+
const checkMultiple = (object, at, diagnose) => {
|
|
271
|
+
const multiple = object.multiple;
|
|
272
|
+
if (multiple === undefined)
|
|
273
|
+
return;
|
|
274
|
+
if (!(object.props ?? []).some((p) => p.name === multiple.property))
|
|
275
|
+
diagnose([...at, "multiple", "property"], `"multiple" property "${multiple.property}" is not a property of "${object.kind}"`);
|
|
276
|
+
try {
|
|
277
|
+
RegExp(multiple.pattern);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
diagnose([...at, "multiple", "pattern"], `invalid "multiple" pattern "${multiple.pattern}": ` +
|
|
281
|
+
(err instanceof Error ? err.message : String(err)));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
/* check the property-driven badge of an object (if any): it names a
|
|
285
|
+
property of the object kind, and for an "enum" valued one each mapped
|
|
286
|
+
value is a member of it (else the mapping would never apply) */
|
|
287
|
+
const checkBadge = (object, at, diagnose) => {
|
|
288
|
+
const badge = object.badge;
|
|
289
|
+
if (badge === undefined || typeof badge === "string")
|
|
290
|
+
return;
|
|
291
|
+
const prop = (object.props ?? []).find((p) => p.name === badge.property);
|
|
292
|
+
if (prop === undefined) {
|
|
293
|
+
diagnose([...at, "badge", "property"], `"badge" property "${badge.property}" is not a property of "${object.kind}"`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
let expr;
|
|
297
|
+
try {
|
|
298
|
+
expr = prop.value !== undefined ? compileValueExpr(prop.value) : undefined;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
/* an invalid value expression is reported on its own */
|
|
302
|
+
}
|
|
303
|
+
if (expr?.kind === "enum")
|
|
304
|
+
for (const value of Object.keys(badge.map))
|
|
305
|
+
if (!expr.members.includes(value))
|
|
306
|
+
diagnose([...at, "badge", "map", value], `"badge" map value "${value}" is not a member of "${prop.value}"`);
|
|
307
|
+
};
|
|
193
308
|
/* check the constraints of a structurally valid configuration which
|
|
194
309
|
are beyond its schema: sibling objects have to stay distinctly
|
|
195
310
|
resolvable, "file" fields are allowed on the first (artifact) level
|
|
@@ -197,8 +312,12 @@ const checkAutomaton = (object, at, diagnose) => {
|
|
|
197
312
|
property value and marker expressions have to be syntactically
|
|
198
313
|
valid, "referenced" and "coverage" entries have to be reference
|
|
199
314
|
expressions, an "automaton" has to name existing child kinds and
|
|
200
|
-
properties,
|
|
201
|
-
|
|
315
|
+
properties, a "badge" has to name an existing property and its
|
|
316
|
+
members, a "multiple" an existing property and a valid regular
|
|
317
|
+
expression, a diagram must not display a property named like a
|
|
318
|
+
reserved Gradia node attribute, and the names of non-artifact
|
|
319
|
+
objects have to be valid patterns (as they are compiled into
|
|
320
|
+
regular expressions) */
|
|
202
321
|
const checkConstraints = (config, posOfPath) => {
|
|
203
322
|
const diagnostics = new Array();
|
|
204
323
|
const diagnose = (path, message) => {
|
|
@@ -226,6 +345,9 @@ const checkConstraints = (config, posOfPath) => {
|
|
|
226
345
|
diagnose([...at, "file"], `"file" field "${object.file}" is not a relative path below the base directory`);
|
|
227
346
|
checkProperties(object.props ?? [], at, diagnose);
|
|
228
347
|
checkAutomaton(object, at, diagnose);
|
|
348
|
+
checkBadge(object, at, diagnose);
|
|
349
|
+
checkMultiple(object, at, diagnose);
|
|
350
|
+
checkDiagrams(object, at, diagnose);
|
|
229
351
|
for (const field of ["referenced", "coverage"])
|
|
230
352
|
for (const [j, entry] of (object[field] ?? []).entries()) {
|
|
231
353
|
/* a reference coverage entry has to be a single
|
|
@@ -279,7 +401,8 @@ export const configStatistics = (config) => {
|
|
|
279
401
|
/* load and validate the YAML schema configuration, merged in order out
|
|
280
402
|
of its files (the later ones into the earlier ones), where every file
|
|
281
403
|
has to be readable and syntactically valid on its own, while the
|
|
282
|
-
merged result alone is validated
|
|
404
|
+
merged result alone is validated, and report the artifacts several
|
|
405
|
+
files define plus the files contributing every node */
|
|
283
406
|
export const loadConfig = (files) => {
|
|
284
407
|
if (files.length === 0)
|
|
285
408
|
throw new Error("no configuration files given");
|
|
@@ -314,8 +437,10 @@ export const loadConfig = (files) => {
|
|
|
314
437
|
if (doc.errors.length === 0)
|
|
315
438
|
docs.push({ file, doc, lines, raw: doc.toJS() });
|
|
316
439
|
}
|
|
440
|
+
const overlaps = overlapsOf(docs);
|
|
441
|
+
const origins = originsOf(docs);
|
|
317
442
|
if (diagnostics.length > 0)
|
|
318
|
-
return { diagnostics };
|
|
443
|
+
return { diagnostics, overlaps, origins };
|
|
319
444
|
/* merge the documents in order, out of unshared deep copies of
|
|
320
445
|
the conversions, as the merge mutates its target while the raw
|
|
321
446
|
ones have to stay intact for the path translation */
|
|
@@ -332,9 +457,9 @@ export const loadConfig = (files) => {
|
|
|
332
457
|
diagnostics.push({ file, line, column, severity: "error",
|
|
333
458
|
message: `invalid configuration: ${where}${issue.message}` });
|
|
334
459
|
}
|
|
335
|
-
return { diagnostics };
|
|
460
|
+
return { diagnostics, overlaps, origins };
|
|
336
461
|
}
|
|
337
462
|
/* check the constraints beyond the structural schema */
|
|
338
463
|
diagnostics.push(...checkConstraints(result.output, posOfPath));
|
|
339
|
-
return { config: diagnostics.length === 0 ? result.output : undefined, diagnostics };
|
|
464
|
+
return { config: diagnostics.length === 0 ? result.output : undefined, diagnostics, overlaps, origins };
|
|
340
465
|
};
|
package/dst/specbook-coverage.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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 { referenceRegex, resolveUnique, resolveSet, chainOf,
|
|
6
|
+
import { referenceRegex, resolveUnique, resolveSet, chainOf, referenceText } from "./specbook-link.js";
|
|
7
7
|
import { compileValueExpr } from "./specbook-parse-value.js";
|
|
8
8
|
/* the referencing objects of every referenced object, resolved from
|
|
9
9
|
the names, property values, descriptions, and rationales of all
|
|
@@ -19,7 +19,7 @@ const referrerIndex = (index) => {
|
|
|
19
19
|
if (object.description !== undefined)
|
|
20
20
|
texts.push(object.description.description, object.description.rationale ?? "", object.description.elaboration ?? "");
|
|
21
21
|
for (const text of texts)
|
|
22
|
-
for (const m of
|
|
22
|
+
for (const m of referenceText(text).matchAll(referenceRegex)) {
|
|
23
23
|
const target = resolveUnique(index, m[1].trim(), object).target;
|
|
24
24
|
if (target !== undefined && !chain.includes(target))
|
|
25
25
|
referrers.set(target, (referrers.get(target) ?? new Set()).add(object));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Spec, SpecObject } from "./specbook-format-spec.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type Schema, type SchemaDiagram } from "./specbook-format-schema.js";
|
|
3
3
|
import type { ParseContext } from "./specbook-parse-common.js";
|
|
4
4
|
import type { OmitAspect } from "./specbook-export-common.js";
|
|
5
5
|
import { type Verbose } from "./specbook-verbose.js";
|
|
@@ -8,14 +8,16 @@ export interface DiagramError {
|
|
|
8
8
|
object?: SpecObject;
|
|
9
9
|
}
|
|
10
10
|
export interface DiagramResult {
|
|
11
|
+
type: DiagramType;
|
|
11
12
|
spec?: string;
|
|
12
13
|
config?: SchemaDiagram["config"];
|
|
13
14
|
columns?: number;
|
|
14
15
|
errors: DiagramError[];
|
|
15
16
|
}
|
|
16
|
-
export
|
|
17
|
-
export declare const
|
|
18
|
-
export declare const
|
|
17
|
+
export type DiagramType = NonNullable<SchemaDiagram["type"]>;
|
|
18
|
+
export declare const specDiagrams: (specification: Spec, config: Schema) => Map<SpecObject, DiagramResult[]>;
|
|
19
|
+
export declare const omittedDiagrams: (specification: Spec, config: Schema, omit: Set<OmitAspect>) => Set<DiagramResult>;
|
|
20
|
+
export declare const renderDiagrams: (specification: Spec, config: Schema, verbose?: Verbose, omit?: Set<"diagram:graph" | "diagram:hub" | "diagram:grid" | "diagram:state" | "diagram:layer" | "diagram:slice" | "diagram:tree" | "diagram:matrix" | "diagram:sequence" | "diagram:petri" | "diagram:gantt" | "diagram:code" | "diagram:image" | "diagram:listing" | "diagram:1" | "diagram:2" | "diagram:3" | "text:long">) => Promise<Map<SpecObject, (DiagramResult & {
|
|
19
21
|
svg: string;
|
|
20
|
-
}>>;
|
|
22
|
+
})[]>>;
|
|
21
23
|
export declare const validateDiagrams: (ctx: ParseContext, specification: Spec, config: Schema) => void;
|