@rse/specbook 1.2.5 → 1.2.6
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 +9 -3
- package/dst/specbook-cmd-lint.js +25 -1
- package/dst/specbook-config.js +18 -17
- package/dst/specbook-coverage.d.ts +17 -0
- package/dst/specbook-coverage.js +106 -0
- package/dst/specbook-diagram.js +12 -2
- package/dst/specbook-export-ast.js +11 -1
- package/dst/specbook-export-html.css +65 -7
- package/dst/specbook-export-html.js +46 -3
- package/dst/specbook-format-schema.d.ts +2 -0
- package/dst/specbook-format-schema.js +3 -1
- package/dst/specbook-format.md +34 -5
- package/dst/specbook-format.yaml +100 -16
- package/dst/specbook-parse-semantic.js +6 -42
- package/dst/specbook-parse.d.ts +1 -1
- package/dst/specbook-parse.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,14 +72,17 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
|
|
|
72
72
|
- **AST Exports for AI/LLMs**:
|
|
73
73
|
The parsed specification Abstract Syntax Tree (AST) can be exported in
|
|
74
74
|
JSON, JSON5, YAML, or TOON format for machine consumption, with the
|
|
75
|
-
derived diagram of an object attached to it as a textual Gradia spec
|
|
75
|
+
derived diagram of an object attached to it as a textual Gradia spec
|
|
76
|
+
and the reference coverage it reports attached as its counts.
|
|
76
77
|
Together with the `describe` command, which explains the models and
|
|
77
78
|
formats, this enables LLMs to both read and write specifications.
|
|
78
79
|
|
|
79
80
|
- **HTML Export for Developers**:
|
|
80
81
|
The HTML export is a self-contained single document with a title page,
|
|
81
82
|
a table-of-contents (also as a slide-in side panel for quick navigation),
|
|
82
|
-
a diagram-of-contents, a fuzzy full-text search, embedded images,
|
|
83
|
+
a diagram-of-contents, a fuzzy full-text search, embedded images,
|
|
84
|
+
reference coverage tables (e.g. the share of the use cases, scenarios,
|
|
85
|
+
requirements, and rules the test cases verify), a
|
|
83
86
|
scroll progress meter (which also scrolls back to the top on click),
|
|
84
87
|
description popups (which show the schema and corpus description of
|
|
85
88
|
an object kind, property, or object instance on hover), and a
|
|
@@ -182,7 +185,10 @@ Options:
|
|
|
182
185
|
all diagnostics and fail on any error among them (a warning, like a
|
|
183
186
|
lapse of the reference coverage a `referenced` object kind demands,
|
|
184
187
|
is reported only), so a partial or invalid specification is never
|
|
185
|
-
exported.
|
|
188
|
+
exported. The verbose output additionally states the reference
|
|
189
|
+
coverage ratios: the ones the `referenced` object kinds receive and
|
|
190
|
+
the ones the `coverage` object kinds report, the latter naming the
|
|
191
|
+
unreferenced objects.
|
|
186
192
|
The option accepts glob patterns and can be given multiple times: the
|
|
187
193
|
matching files (in the order of the patterns and alphabetically within
|
|
188
194
|
a pattern, where the literal `std` names the bundled standard schema
|
package/dst/specbook-cmd-lint.js
CHANGED
|
@@ -7,7 +7,9 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { loadConfig, configStatistics } from "./specbook-config.js";
|
|
9
9
|
import { literal } from "./specbook-verbose.js";
|
|
10
|
-
import { parseSpecification, specStatistics, resolveArtifact } from "./specbook-parse.js";
|
|
10
|
+
import { parseSpecification, specStatistics, resolveArtifact, collectSchemas } from "./specbook-parse.js";
|
|
11
|
+
import { buildLinkIndex, chainOf, plainText } from "./specbook-link.js";
|
|
12
|
+
import { referencedCoverage, specCoverage, coverageRatio } from "./specbook-coverage.js";
|
|
11
13
|
/* collect the distinct artifact files the schema configuration
|
|
12
14
|
references, in their declaration order, each mapped onto whether
|
|
13
15
|
all of its artifacts are optional and hence the file may be absent */
|
|
@@ -72,6 +74,28 @@ export const lint = (options) => {
|
|
|
72
74
|
const stats = specStatistics(result.specification);
|
|
73
75
|
options.verbose(`parsed specification defining ${literal(stats.objects)} object(s) ` +
|
|
74
76
|
`and ${literal(stats.links)} link relationship(s)`);
|
|
77
|
+
/* report the reference coverage the "referenced"-flagged object
|
|
78
|
+
kinds receive and the "coverage"-configured objects report, the
|
|
79
|
+
unreferenced objects of the latter by name (the ones of the
|
|
80
|
+
former are already reported as warnings) */
|
|
81
|
+
if (config !== undefined && result.specification.artifacts.length > 0) {
|
|
82
|
+
const index = buildLinkIndex(result.specification);
|
|
83
|
+
const schemas = collectSchemas(result.specification, config);
|
|
84
|
+
const label = (object) => `${object.kind} "${literal(plainText(object.name))}"`;
|
|
85
|
+
const ratio = (covered, uncovered) => `${literal(covered.length)} of ${literal(covered.length + uncovered.length)} ` +
|
|
86
|
+
`(${literal(coverageRatio(covered.length, covered.length + uncovered.length))}%)`;
|
|
87
|
+
for (const { schema, covered, uncovered } of referencedCoverage(index, schemas)) {
|
|
88
|
+
const chain = chainOf(index, covered[0] ?? uncovered[0]);
|
|
89
|
+
options.verbose((schema.referenced ?? []).map((entry) => `"${literal(entry)}"`).join(" or ") +
|
|
90
|
+
` references ${ratio(covered, uncovered)} ${schema.kind} object(s)` +
|
|
91
|
+
(chain.length > 1 ? ` below ${label(chain[0])}` : ""));
|
|
92
|
+
}
|
|
93
|
+
for (const [object, entries] of specCoverage(index, schemas))
|
|
94
|
+
for (const { pattern, covered, uncovered } of entries)
|
|
95
|
+
options.verbose(`${label(object)} references ${ratio(covered, uncovered)} ` +
|
|
96
|
+
`"${literal(pattern)}" object(s)` + (uncovered.length > 0 ?
|
|
97
|
+
`, unreferenced: ${uncovered.map(label).join(", ")}` : ""));
|
|
98
|
+
}
|
|
75
99
|
/* report the non-optional artifacts absent from the specification,
|
|
76
100
|
against their loaded artifact file (an absent or unreadable file
|
|
77
101
|
is already reported above) or else against the configuration */
|
package/dst/specbook-config.js
CHANGED
|
@@ -190,10 +190,10 @@ const checkAutomaton = (object, at, diagnose) => {
|
|
|
190
190
|
are beyond its schema: sibling objects have to stay distinctly
|
|
191
191
|
resolvable, "file" fields are allowed on the first (artifact) level
|
|
192
192
|
only, property value and marker expressions have to be syntactically
|
|
193
|
-
valid, "referenced" entries have to be reference
|
|
194
|
-
"automaton" has to name existing child kinds and
|
|
195
|
-
names of non-artifact objects have to be valid
|
|
196
|
-
compiled into regular expressions) */
|
|
193
|
+
valid, "referenced" and "coverage" entries have to be reference
|
|
194
|
+
expressions, an "automaton" has to name existing child kinds and
|
|
195
|
+
properties, and the names of non-artifact objects have to be valid
|
|
196
|
+
patterns (as they are compiled into regular expressions) */
|
|
197
197
|
const checkConstraints = (config, posOfPath) => {
|
|
198
198
|
const diagnostics = new Array();
|
|
199
199
|
const diagnose = (path, message) => {
|
|
@@ -216,20 +216,21 @@ const checkConstraints = (config, posOfPath) => {
|
|
|
216
216
|
diagnose([...at, "file"], `"file" field is only allowed on the first (artifact) level (found on level ${depth})`);
|
|
217
217
|
checkProperties(object.props ?? [], at, diagnose);
|
|
218
218
|
checkAutomaton(object, at, diagnose);
|
|
219
|
-
for (const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
219
|
+
for (const field of ["referenced", "coverage"])
|
|
220
|
+
for (const [j, entry] of (object[field] ?? []).entries()) {
|
|
221
|
+
/* a reference coverage entry has to be a single
|
|
222
|
+
(usually wildcard) reference expression */
|
|
223
|
+
let expr;
|
|
224
|
+
try {
|
|
225
|
+
expr = compileValueExpr(entry);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
diagnose([...at, field, j], `invalid "${field}" entry "${entry}": ` +
|
|
229
|
+
(err instanceof Error ? err.message : String(err)));
|
|
230
|
+
}
|
|
231
|
+
if (expr !== undefined && expr.kind !== "reference")
|
|
232
|
+
diagnose([...at, field, j], `"${field}" entry "${entry}" is not a single "[[xxx]]" reference`);
|
|
225
233
|
}
|
|
226
|
-
catch (err) {
|
|
227
|
-
diagnose([...at, "referenced", j], `invalid "referenced" entry "${entry}": ` +
|
|
228
|
-
(err instanceof Error ? err.message : String(err)));
|
|
229
|
-
}
|
|
230
|
-
if (expr !== undefined && expr.kind !== "reference")
|
|
231
|
-
diagnose([...at, "referenced", j], `"referenced" entry "${entry}" is not a single "[[xxx]]" reference`);
|
|
232
|
-
}
|
|
233
234
|
if (depth > 1 && object.name !== undefined) {
|
|
234
235
|
try {
|
|
235
236
|
anchored(object.name);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SpecObject } from "./specbook-format-spec.js";
|
|
2
|
+
import { type SchemaObject } from "./specbook-format-schema.js";
|
|
3
|
+
import { type LinkIndex } from "./specbook-link.js";
|
|
4
|
+
export interface ReferencedCoverage {
|
|
5
|
+
schema: SchemaObject;
|
|
6
|
+
covered: SpecObject[];
|
|
7
|
+
uncovered: SpecObject[];
|
|
8
|
+
}
|
|
9
|
+
export declare const referencedCoverage: (index: LinkIndex, schemas: Map<SpecObject, SchemaObject>) => ReferencedCoverage[];
|
|
10
|
+
export interface Coverage {
|
|
11
|
+
pattern: string;
|
|
12
|
+
kinds: string[];
|
|
13
|
+
covered: SpecObject[];
|
|
14
|
+
uncovered: SpecObject[];
|
|
15
|
+
}
|
|
16
|
+
export declare const specCoverage: (index: LinkIndex, schemas: Map<SpecObject, SchemaObject>) => Map<SpecObject, Coverage[]>;
|
|
17
|
+
export declare const coverageRatio: (covered: number, total: number) => number;
|
|
@@ -0,0 +1,106 @@
|
|
|
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 { referenceRegex, resolveUnique, resolveSet, chainOf, plainText } from "./specbook-link.js";
|
|
7
|
+
import { compileValueExpr } from "./specbook-parse-value.js";
|
|
8
|
+
/* the referencing objects of every referenced object, resolved from
|
|
9
|
+
the names, property values, descriptions, and rationales of all
|
|
10
|
+
objects, where the references from within the subtree of the
|
|
11
|
+
referenced object itself do not count (an object cannot cover
|
|
12
|
+
itself), leniently skipping the unresolvable references, as those
|
|
13
|
+
are already reported by the reference pass */
|
|
14
|
+
const referrerIndex = (index) => {
|
|
15
|
+
const referrers = new Map();
|
|
16
|
+
for (const { object } of index) {
|
|
17
|
+
const chain = chainOf(index, object);
|
|
18
|
+
const texts = [object.name, ...object.properties.map((p) => p.value)];
|
|
19
|
+
if (object.description !== undefined)
|
|
20
|
+
texts.push(object.description.description, object.description.rationale ?? "");
|
|
21
|
+
for (const text of texts)
|
|
22
|
+
for (const m of plainText(text).matchAll(referenceRegex)) {
|
|
23
|
+
const target = resolveUnique(index, m[1].trim(), object).target;
|
|
24
|
+
if (target !== undefined && !chain.includes(target))
|
|
25
|
+
referrers.set(target, (referrers.get(target) ?? new Set()).add(object));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return referrers;
|
|
29
|
+
};
|
|
30
|
+
/* whether an object is covered by a set of source objects: it is
|
|
31
|
+
referenced from an object lying below one of the sources (itself
|
|
32
|
+
or through an ancestor) or, when lifting, so is one of its
|
|
33
|
+
descendants */
|
|
34
|
+
const isCovered = (index, referrers, sources, object, lifting) => {
|
|
35
|
+
for (const referrer of referrers.get(object) ?? [])
|
|
36
|
+
if (chainOf(index, referrer).some((o) => sources.has(o)))
|
|
37
|
+
return true;
|
|
38
|
+
return lifting && object.children.some((child) => isCovered(index, referrers, sources, child, true));
|
|
39
|
+
};
|
|
40
|
+
/* the objects matching the reference patterns of a schema field */
|
|
41
|
+
const matchingObjects = (index, patterns) => {
|
|
42
|
+
const objects = new Array();
|
|
43
|
+
for (const pattern of patterns) {
|
|
44
|
+
const expr = compileValueExpr(pattern);
|
|
45
|
+
if (expr.kind === "reference")
|
|
46
|
+
for (const object of resolveSet(index, expr.pattern))
|
|
47
|
+
if (!objects.includes(object))
|
|
48
|
+
objects.push(object);
|
|
49
|
+
}
|
|
50
|
+
return objects;
|
|
51
|
+
};
|
|
52
|
+
/* determine the coverage the objects of every "referenced"-flagged
|
|
53
|
+
kind receive, one entry per flagged schema node (in the order of
|
|
54
|
+
the first objects of the nodes), as the semantic validation
|
|
55
|
+
reports every uncovered object and the verbose output the ratio */
|
|
56
|
+
export const referencedCoverage = (index, schemas) => {
|
|
57
|
+
const referrers = referrerIndex(index);
|
|
58
|
+
const results = new Map();
|
|
59
|
+
for (const [object, schema] of schemas) {
|
|
60
|
+
if (schema.referenced === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
let result = results.get(schema);
|
|
63
|
+
if (result === undefined) {
|
|
64
|
+
result = { schema, sources: new Set(matchingObjects(index, schema.referenced)),
|
|
65
|
+
covered: [], uncovered: [] };
|
|
66
|
+
results.set(schema, result);
|
|
67
|
+
}
|
|
68
|
+
const bucket = isCovered(index, referrers, result.sources, object, false) ?
|
|
69
|
+
result.covered : result.uncovered;
|
|
70
|
+
bucket.push(object);
|
|
71
|
+
}
|
|
72
|
+
return Array.from(results.values()).map(({ schema, covered, uncovered }) => ({ schema, covered, uncovered }));
|
|
73
|
+
};
|
|
74
|
+
/* determine the coverage the objects of every "coverage"-configured
|
|
75
|
+
kind report, one entry per configured pattern: the objects matching
|
|
76
|
+
the pattern (those below the reporting object itself excluded) count
|
|
77
|
+
as covered once referenced -- themselves or through one of their
|
|
78
|
+
descendants, so a use case is covered through its scenarios -- from
|
|
79
|
+
the reporting object or one of its descendants */
|
|
80
|
+
export const specCoverage = (index, schemas) => {
|
|
81
|
+
const referrers = referrerIndex(index);
|
|
82
|
+
const results = new Map();
|
|
83
|
+
for (const [object, schema] of schemas) {
|
|
84
|
+
if (schema.coverage === undefined)
|
|
85
|
+
continue;
|
|
86
|
+
const sources = new Set([object]);
|
|
87
|
+
const entries = new Array();
|
|
88
|
+
for (const pattern of schema.coverage) {
|
|
89
|
+
const targets = matchingObjects(index, [pattern])
|
|
90
|
+
.filter((target) => !chainOf(index, target).includes(object));
|
|
91
|
+
const entry = { pattern, kinds: [], covered: [], uncovered: [] };
|
|
92
|
+
for (const target of targets) {
|
|
93
|
+
if (!entry.kinds.includes(target.kind))
|
|
94
|
+
entry.kinds.push(target.kind);
|
|
95
|
+
const bucket = isCovered(index, referrers, sources, target, true) ?
|
|
96
|
+
entry.covered : entry.uncovered;
|
|
97
|
+
bucket.push(target);
|
|
98
|
+
}
|
|
99
|
+
entries.push(entry);
|
|
100
|
+
}
|
|
101
|
+
results.set(object, entries);
|
|
102
|
+
}
|
|
103
|
+
return results;
|
|
104
|
+
};
|
|
105
|
+
/* the percentage of a coverage, rounded to the integer */
|
|
106
|
+
export const coverageRatio = (covered, total) => total > 0 ? Math.round(covered * 100 / total) : 0;
|
package/dst/specbook-diagram.js
CHANGED
|
@@ -340,7 +340,9 @@ const deriveCenter = (object, diagram, nodes, nodeSet, index, anchors, parenProp
|
|
|
340
340
|
direction property of the node objects: the "inbound" value
|
|
341
341
|
maps onto a node-to-center edge, the "outbound" value onto a
|
|
342
342
|
center-to-node edge, and the "both" value onto both (which
|
|
343
|
-
Gradia renders as an input node plus an output "ghost" node)
|
|
343
|
+
Gradia renders as an input node plus an output "ghost" node),
|
|
344
|
+
named by the "labeled" property and/or by the mediating object
|
|
345
|
+
the "via" property references (as "via <name>") */
|
|
344
346
|
const deriveCenterEdges = (cfg, nodes, center, index, parenProps, errors) => {
|
|
345
347
|
const edges = new Array();
|
|
346
348
|
for (const node of nodes) {
|
|
@@ -356,9 +358,17 @@ const deriveCenterEdges = (cfg, nodes, center, index, parenProps, errors) => {
|
|
|
356
358
|
`carries the unmapped diagram "centerEdges" value "${value}"` });
|
|
357
359
|
continue;
|
|
358
360
|
}
|
|
361
|
+
const expand = (text) => plainText(expandReferences(text, (reference) => resolveUnique(index, reference, node).target?.name ?? reference));
|
|
359
362
|
const label = cfg.labeled !== undefined ?
|
|
360
363
|
propValue(parenProps, node, cfg.labeled) : undefined;
|
|
361
|
-
const
|
|
364
|
+
const via = cfg.via !== undefined ?
|
|
365
|
+
propValue(parenProps, node, cfg.via) : undefined;
|
|
366
|
+
const names = new Array();
|
|
367
|
+
if (label !== undefined)
|
|
368
|
+
names.push(expand(label));
|
|
369
|
+
if (via !== undefined)
|
|
370
|
+
names.push(`via ${expand(via)}`);
|
|
371
|
+
const name = names.length > 0 ? names.join(" ") : undefined;
|
|
362
372
|
if (inbound || both)
|
|
363
373
|
edges.push({ source: node, target: center, name });
|
|
364
374
|
if (outbound || both)
|
|
@@ -7,6 +7,9 @@ import JSON5 from "json5";
|
|
|
7
7
|
import { stringify as stringifyYaml } from "yaml";
|
|
8
8
|
import { encode as encodeToon } from "@toon-format/toon";
|
|
9
9
|
import { specDiagrams } from "./specbook-diagram.js";
|
|
10
|
+
import { specCoverage } from "./specbook-coverage.js";
|
|
11
|
+
import { buildLinkIndex } from "./specbook-link.js";
|
|
12
|
+
import { collectSchemas } from "./specbook-parse-semantic.js";
|
|
10
13
|
import { isTitleObject } from "./specbook-export-common.js";
|
|
11
14
|
/* render the specification AST into a serialization format */
|
|
12
15
|
export const renderAst = (specification, format, config) => {
|
|
@@ -16,13 +19,20 @@ export const renderAst = (specification, format, config) => {
|
|
|
16
19
|
"diagram" fields onto the corresponding plain object nodes
|
|
17
20
|
(an invalid diagram situation omits the diagram, as it is
|
|
18
21
|
already reported as a lint diagnostic, and the diagram of
|
|
19
|
-
the title object is reserved for the HTML/PDF export)
|
|
22
|
+
the title object is reserved for the HTML/PDF export), and
|
|
23
|
+
the reference coverage of the coverage-configured objects as
|
|
24
|
+
"coverage" fields (the counts only, as the export is intended
|
|
25
|
+
for machine consumption) */
|
|
20
26
|
if (config !== undefined) {
|
|
21
27
|
const diagrams = specDiagrams(specification, config);
|
|
28
|
+
const coverages = specCoverage(buildLinkIndex(specification), collectSchemas(specification, config));
|
|
22
29
|
const walk = (object, node) => {
|
|
23
30
|
const result = diagrams.get(object);
|
|
24
31
|
if (result?.spec !== undefined && !isTitleObject(object))
|
|
25
32
|
node.diagram = result.spec;
|
|
33
|
+
const coverage = coverages.get(object);
|
|
34
|
+
if (coverage !== undefined)
|
|
35
|
+
node.coverage = coverage.map(({ pattern, covered, uncovered }) => ({ pattern, covered: covered.length, total: covered.length + uncovered.length }));
|
|
26
36
|
object.children.forEach((child, i) => walk(child, node.children[i]));
|
|
27
37
|
};
|
|
28
38
|
specification.artifacts.forEach((artifact, i) => artifact.objects.forEach((object, j) => walk(object, plain.artifacts[i].objects[j])));
|
|
@@ -486,7 +486,7 @@ div.fold-switch div.fold-controls svg {
|
|
|
486
486
|
/* the folding of the diagrams and the over-long table cells
|
|
487
487
|
(screen-only): a diagram sits in a fold container carrying a chevron
|
|
488
488
|
mark in the muted symbol color of the anchors at its top left corner
|
|
489
|
-
-- in the page margin beside the content, and in
|
|
489
|
+
-- in the page margin beside the content, and in a flex row left of
|
|
490
490
|
the diagram inside a table cell, where no margin is left -- while an
|
|
491
491
|
over-long cell carries its own chevron in a rounded grey box behind
|
|
492
492
|
the last still shown word; a folded diagram leaves the icon of its
|
|
@@ -543,19 +543,25 @@ span.fold-label svg {
|
|
|
543
543
|
height: 1.1rem;
|
|
544
544
|
}
|
|
545
545
|
@media screen {
|
|
546
|
+
td div.fold {
|
|
547
|
+
display: flex;
|
|
548
|
+
align-items: flex-start;
|
|
549
|
+
}
|
|
546
550
|
td div.fold > span.fold-mark {
|
|
547
551
|
position: static;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
552
|
+
flex: 0 0 auto;
|
|
553
|
+
margin: 0.3rem 0.3rem 0 0;
|
|
554
|
+
}
|
|
555
|
+
td div.fold > div.diagram {
|
|
556
|
+
flex: 1 1 auto;
|
|
557
|
+
min-width: 0;
|
|
551
558
|
}
|
|
552
559
|
body:not(.searching) div.fold.folded > span.fold-label {
|
|
553
560
|
display: block;
|
|
554
561
|
}
|
|
555
562
|
body:not(.searching) td div.fold.folded > span.fold-label {
|
|
556
|
-
display:
|
|
557
|
-
|
|
558
|
-
margin-bottom: 0;
|
|
563
|
+
display: block;
|
|
564
|
+
margin: 0.15rem 0 0 0;
|
|
559
565
|
}
|
|
560
566
|
body:not(.searching) div.fold.folded > div.diagram,
|
|
561
567
|
body:not(.searching) td.folded .fold-rest {
|
|
@@ -1103,6 +1109,58 @@ table.objects td section {
|
|
|
1103
1109
|
table.objects td section h6 {
|
|
1104
1110
|
margin: 0.5rem 0 0.25rem 0;
|
|
1105
1111
|
}
|
|
1112
|
+
/* the reference coverage tables of the coverage-configured objects:
|
|
1113
|
+
the headers like the "Properties" and "Diagram" headers of the
|
|
1114
|
+
compact tables (as they name neither object kinds nor properties),
|
|
1115
|
+
the counts in narrow columns, the ratio as a bar in the accent
|
|
1116
|
+
color over the remaining width, followed by its percentage */
|
|
1117
|
+
table.coverage {
|
|
1118
|
+
border-collapse: collapse;
|
|
1119
|
+
margin: 1rem 0;
|
|
1120
|
+
width: 100%;
|
|
1121
|
+
}
|
|
1122
|
+
table.coverage th,
|
|
1123
|
+
table.coverage td {
|
|
1124
|
+
border: 0;
|
|
1125
|
+
border-left: 1px solid var(--theme-color-specbook-border);
|
|
1126
|
+
padding: 0.3rem 0.6rem;
|
|
1127
|
+
text-align: left;
|
|
1128
|
+
vertical-align: top;
|
|
1129
|
+
white-space: nowrap;
|
|
1130
|
+
}
|
|
1131
|
+
table.coverage th:first-child,
|
|
1132
|
+
table.coverage td:first-child {
|
|
1133
|
+
border-left: 0;
|
|
1134
|
+
}
|
|
1135
|
+
table.coverage td:first-child {
|
|
1136
|
+
font-weight: 600;
|
|
1137
|
+
}
|
|
1138
|
+
table.coverage th {
|
|
1139
|
+
background-color: var(--theme-color-specbook-tint);
|
|
1140
|
+
font-weight: normal;
|
|
1141
|
+
color: var(--theme-color-specbook-accent);
|
|
1142
|
+
}
|
|
1143
|
+
table.coverage tbody tr.even td {
|
|
1144
|
+
background-color: var(--theme-color-specbook-stripe);
|
|
1145
|
+
}
|
|
1146
|
+
table.coverage th.ratio,
|
|
1147
|
+
table.coverage td.ratio {
|
|
1148
|
+
width: 100%;
|
|
1149
|
+
}
|
|
1150
|
+
table.coverage span.bar {
|
|
1151
|
+
display: inline-block;
|
|
1152
|
+
vertical-align: middle;
|
|
1153
|
+
width: 12rem;
|
|
1154
|
+
height: 0.6rem;
|
|
1155
|
+
border-radius: 0.3rem;
|
|
1156
|
+
background-color: var(--theme-color-specbook-tint-deep);
|
|
1157
|
+
overflow: hidden;
|
|
1158
|
+
}
|
|
1159
|
+
table.coverage span.bar > span {
|
|
1160
|
+
display: block;
|
|
1161
|
+
height: 100%;
|
|
1162
|
+
background-color: var(--theme-color-specbook-accent);
|
|
1163
|
+
}
|
|
1106
1164
|
/* the object descriptions and their embedded images */
|
|
1107
1165
|
p.description,
|
|
1108
1166
|
div.description {
|
|
@@ -15,6 +15,7 @@ import { embeddingRegex, embeddingMimeType, embeddingThemes, embeddingVariants }
|
|
|
15
15
|
import { escapeHtml, stylesheet, searchScript, fallbackLogo, isTitleObject, titleObject, documentTitle, documentLang, documentThemeStyle } from "./specbook-export-common.js";
|
|
16
16
|
import { collectSchemas } from "./specbook-parse-semantic.js";
|
|
17
17
|
import { renderDiagrams } from "./specbook-diagram.js";
|
|
18
|
+
import { specCoverage, coverageRatio } from "./specbook-coverage.js";
|
|
18
19
|
/* ==== Templates ==== */
|
|
19
20
|
/* the built-in Nunjucks templates for the HTML export */
|
|
20
21
|
const templates = {
|
|
@@ -154,9 +155,23 @@ const templates = {
|
|
|
154
155
|
{{ Object.diagram }}
|
|
155
156
|
{{ Object.properties }}
|
|
156
157
|
{{ Object.description }}
|
|
158
|
+
{{ Object.coverage }}
|
|
157
159
|
{{ Object.children }}
|
|
158
160
|
</section>
|
|
159
161
|
`,
|
|
162
|
+
/* <Coverage/> */
|
|
163
|
+
"Coverage": textframe `
|
|
164
|
+
<table class="coverage">
|
|
165
|
+
<thead>
|
|
166
|
+
<tr><th>Coverage</th><th>Covered</th><th>Total</th><th class="ratio">Ratio</th></tr>
|
|
167
|
+
</thead>
|
|
168
|
+
<tbody>
|
|
169
|
+
{% for entry in Coverage %}
|
|
170
|
+
<tr{% if entry.even %} class="even"{% endif %}><td>{{ entry.label }}</td><td>{{ entry.covered }}</td><td>{{ entry.total }}</td><td class="ratio"><span class="bar"><span style="width: {{ entry.ratio }}%"></span></span> {{ entry.ratio }}%</td></tr>
|
|
171
|
+
{% endfor %}
|
|
172
|
+
</tbody>
|
|
173
|
+
</table>
|
|
174
|
+
`,
|
|
160
175
|
/* <Properties/> */
|
|
161
176
|
"Properties": textframe `
|
|
162
177
|
<table class="props"{% if Fold %} data-fold-height="{{ Fold }}"{% endif %}>
|
|
@@ -979,14 +994,15 @@ const render = (name, context) => {
|
|
|
979
994
|
};
|
|
980
995
|
/* the active per-document reference expander, fully-qualified
|
|
981
996
|
anchor paths, member-carrying property value constraints, object
|
|
982
|
-
schema nodes, pre-rendered diagram SVGs,
|
|
983
|
-
of the schema nodes, and object parents of
|
|
984
|
-
title paths (all set during HTML rendering) */
|
|
997
|
+
schema nodes, pre-rendered diagram SVGs, reference coverages,
|
|
998
|
+
description popup keys of the schema nodes, and object parents of
|
|
999
|
+
the description popup title paths (all set during HTML rendering) */
|
|
985
1000
|
let linker = null;
|
|
986
1001
|
let anchors = null;
|
|
987
1002
|
let members = null;
|
|
988
1003
|
let schemas = null;
|
|
989
1004
|
let diagrams = null;
|
|
1005
|
+
let coverages = null;
|
|
990
1006
|
let infoKeys = null;
|
|
991
1007
|
let infoParents = null;
|
|
992
1008
|
/* the object whose texts are currently rendered, scoping the
|
|
@@ -1292,6 +1308,30 @@ const diagramOf = (object) => {
|
|
|
1292
1308
|
const diagram = diagrams?.get(object);
|
|
1293
1309
|
return diagram !== undefined ? safe(`<div class="diagram">${diagram}</div>`) : "";
|
|
1294
1310
|
};
|
|
1311
|
+
/* the reference coverage of an object as a table: one row per
|
|
1312
|
+
configured pattern, labeled by the kinds of the matching objects,
|
|
1313
|
+
marked as object kinds and carrying the description popup of the
|
|
1314
|
+
first matching object of each kind (by the bare pattern, if none
|
|
1315
|
+
matches), with the counts and the ratio as a bar (empty for an
|
|
1316
|
+
object without a configured coverage) */
|
|
1317
|
+
const coverageOf = (object) => {
|
|
1318
|
+
const coverage = coverages?.get(object);
|
|
1319
|
+
if (coverage === undefined)
|
|
1320
|
+
return "";
|
|
1321
|
+
const kindLabel = (entry, kind) => {
|
|
1322
|
+
const first = [...entry.covered, ...entry.uncovered].find((target) => target.kind === kind);
|
|
1323
|
+
return `<span class="object-kind"${first !== undefined ? infoAttr(first) : ""}>${escapeHtml(kind)}</span>`;
|
|
1324
|
+
};
|
|
1325
|
+
return safe(render("Coverage", { Coverage: coverage.map((entry, i) => ({
|
|
1326
|
+
label: entry.kinds.length > 0 ?
|
|
1327
|
+
safe(entry.kinds.map((kind) => kindLabel(entry, kind)).join(", ")) :
|
|
1328
|
+
entry.pattern.replace(/^\[\[(.*)\]\]$/s, "$1"),
|
|
1329
|
+
covered: entry.covered.length,
|
|
1330
|
+
total: entry.covered.length + entry.uncovered.length,
|
|
1331
|
+
ratio: coverageRatio(entry.covered.length, entry.covered.length + entry.uncovered.length),
|
|
1332
|
+
even: i % 2 === 1
|
|
1333
|
+
})) }));
|
|
1334
|
+
};
|
|
1295
1335
|
/* render a single-kind group of children into one compact table:
|
|
1296
1336
|
the name first, then the property columns, then the description;
|
|
1297
1337
|
a group wider than maxColumns, or carrying diagrams, instead chunks
|
|
@@ -1386,6 +1426,7 @@ const renderObject = (object, level, concise) => {
|
|
|
1386
1426
|
Fold: formatOf(object)?.maxCellHeight })) : "",
|
|
1387
1427
|
description: object.description !== undefined ?
|
|
1388
1428
|
safe(renderDescription(object.description)) : "",
|
|
1429
|
+
coverage: coverageOf(object),
|
|
1389
1430
|
children: safe(renderChildren(object, level + 1, concise))
|
|
1390
1431
|
} }));
|
|
1391
1432
|
};
|
|
@@ -1572,6 +1613,7 @@ export const renderHtml = async (specification, config, tocPages, css, realtime
|
|
|
1572
1613
|
members = config !== undefined ? collectMembers(config, new Map()) : null;
|
|
1573
1614
|
schemas = config !== undefined ? collectSchemas(specification, config) : null;
|
|
1574
1615
|
diagrams = rendered;
|
|
1616
|
+
coverages = schemas !== null ? specCoverage(index, schemas) : null;
|
|
1575
1617
|
/* collect the schema descriptions for the description popups,
|
|
1576
1618
|
plus the object parents composing their title paths */
|
|
1577
1619
|
let info = null;
|
|
@@ -1638,6 +1680,7 @@ export const renderHtml = async (specification, config, tocPages, css, realtime
|
|
|
1638
1680
|
members = null;
|
|
1639
1681
|
schemas = null;
|
|
1640
1682
|
diagrams = null;
|
|
1683
|
+
coverages = null;
|
|
1641
1684
|
infoKeys = null;
|
|
1642
1685
|
infoParents = null;
|
|
1643
1686
|
}
|
|
@@ -10,6 +10,7 @@ export type SchemaObject = {
|
|
|
10
10
|
refs?: string;
|
|
11
11
|
optional?: boolean;
|
|
12
12
|
referenced?: string[];
|
|
13
|
+
coverage?: string[];
|
|
13
14
|
automaton?: SchemaAutomaton;
|
|
14
15
|
diagram?: SchemaDiagram;
|
|
15
16
|
format?: SchemaFormat;
|
|
@@ -36,6 +37,7 @@ export type SchemaDiagramCenterEdges = {
|
|
|
36
37
|
outbound?: string;
|
|
37
38
|
both?: string;
|
|
38
39
|
labeled?: string;
|
|
40
|
+
via?: string;
|
|
39
41
|
};
|
|
40
42
|
export type SchemaDiagramNest = {
|
|
41
43
|
properties?: string[];
|
|
@@ -48,7 +48,8 @@ const SchemaDiagramCenterEdges = v.strictObject({
|
|
|
48
48
|
inbound: v.optional(v.string()),
|
|
49
49
|
outbound: v.optional(v.string()),
|
|
50
50
|
both: v.optional(v.string()),
|
|
51
|
-
labeled: v.optional(v.string())
|
|
51
|
+
labeled: v.optional(v.string()),
|
|
52
|
+
via: v.optional(v.string())
|
|
52
53
|
});
|
|
53
54
|
const SchemaDiagramNest = v.strictObject({
|
|
54
55
|
properties: v.optional(v.array(v.string())),
|
|
@@ -93,6 +94,7 @@ const SchemaObject = v.strictObject({
|
|
|
93
94
|
refs: v.optional(v.string()),
|
|
94
95
|
optional: v.optional(v.boolean()),
|
|
95
96
|
referenced: v.optional(v.pipe(v.array(v.string()), v.minLength(1))),
|
|
97
|
+
coverage: v.optional(v.pipe(v.array(v.string()), v.minLength(1))),
|
|
96
98
|
automaton: v.optional(SchemaAutomaton),
|
|
97
99
|
diagram: v.optional(SchemaDiagram),
|
|
98
100
|
format: v.optional(SchemaFormat),
|
package/dst/specbook-format.md
CHANGED
|
@@ -74,6 +74,7 @@ type SchemaObject = {
|
|
|
74
74
|
refs?: string
|
|
75
75
|
optional?: boolean
|
|
76
76
|
referenced?: string[]
|
|
77
|
+
coverage?: string[]
|
|
77
78
|
automaton?: SchemaAutomaton
|
|
78
79
|
diagram?: SchemaDiagram
|
|
79
80
|
format?: SchemaFormat
|
|
@@ -100,6 +101,7 @@ type SchemaDiagramCenterEdges = {
|
|
|
100
101
|
outbound?: string
|
|
101
102
|
both?: string
|
|
102
103
|
labeled?: string
|
|
104
|
+
via?: string
|
|
103
105
|
}
|
|
104
106
|
type SchemaDiagram = {
|
|
105
107
|
type?: "graph" | "hub" | "grid"
|
|
@@ -235,6 +237,13 @@ type SchemaGradiaConfig = Partial<{
|
|
|
235
237
|
at least once, where a lapse is reported as a warning only,
|
|
236
238
|
BECAUSE a term nobody uses or a requirement no use case exercises is dead weight
|
|
237
239
|
|
|
240
|
+
- `SchemaObject.coverage?: string[]`:
|
|
241
|
+
wildcard references (e.g. `[[SCENARIO:*]]` for the use case scenarios)
|
|
242
|
+
matching the objects whose reference coverage every object of this kind
|
|
243
|
+
reports: the share of them referenced -- themselves or through their
|
|
244
|
+
descendants -- from the object or its descendants (see below),
|
|
245
|
+
BECAUSE a test artifact has to state how much of the specification it verifies
|
|
246
|
+
|
|
238
247
|
- `SchemaObject.automaton?: SchemaAutomaton`:
|
|
239
248
|
finite state machine the child objects of every object of this kind form,
|
|
240
249
|
BECAUSE a lifecycle is only trustworthy when its dead-ends and
|
|
@@ -319,9 +328,11 @@ type SchemaGradiaConfig = Partial<{
|
|
|
319
328
|
objects whose `inbound` value maps onto a node-to-center edge, whose
|
|
320
329
|
`outbound` value onto a center-to-node edge, and whose `both` value
|
|
321
330
|
onto both edges (the second placement rendered as a "ghost" node),
|
|
322
|
-
each optionally named by the value of the `labeled` property
|
|
323
|
-
|
|
324
|
-
|
|
331
|
+
each optionally named by the value of the `labeled` property and/or
|
|
332
|
+
by the mediating object referenced by the `via` property (as `via
|
|
333
|
+
<name>`), BECAUSE the nodes carry no `[[xxx]]` reference to a
|
|
334
|
+
synthetic center, so their relation to it lives in a direction
|
|
335
|
+
property instead
|
|
325
336
|
|
|
326
337
|
- `SchemaDiagram.links?: "props" | "all"`:
|
|
327
338
|
edge source: property values (`props`) or also texts (`all`),
|
|
@@ -595,6 +606,24 @@ afterwards merge into one with their arities summed, so the edges
|
|
|
595
606
|
inside a container stay node-to-node while the bundles of references
|
|
596
607
|
into a container become single edges onto it.
|
|
597
608
|
|
|
609
|
+
### Coverage
|
|
610
|
+
|
|
611
|
+
An object kind can carry a `coverage` field, a list of `[[xxx]]`
|
|
612
|
+
reference patterns, which makes every object of that kind report how
|
|
613
|
+
much of the objects matching each pattern it references: the *coverage*
|
|
614
|
+
is the share of the matching objects (those below the reporting object
|
|
615
|
+
itself excluded) which are referenced from the reporting object or one
|
|
616
|
+
of its descendants, where a matching object also counts as referenced
|
|
617
|
+
through one of its descendants, so a use case is covered as soon as one
|
|
618
|
+
of its scenarios is. While the `referenced` field demands the coverage
|
|
619
|
+
of an object kind and warns about every lapse, the `coverage` field
|
|
620
|
+
merely measures it: the HTML/PDF export renders a coverage table below
|
|
621
|
+
the description of the reporting object (one row per pattern, labeled
|
|
622
|
+
by the kinds of the matching objects, with the covered and total counts
|
|
623
|
+
and the ratio as a bar), the AST exports attach the counts as the
|
|
624
|
+
`coverage` field of the object, and the verbose output of `lint` and
|
|
625
|
+
`export` names the unreferenced objects.
|
|
626
|
+
|
|
598
627
|
### Format
|
|
599
628
|
|
|
600
629
|
An object kind can furthermore carry a `format` field, which controls
|
|
@@ -908,7 +937,7 @@ In all formats, the `<name/>` may carry trailing decorations, in any
|
|
|
908
937
|
order:
|
|
909
938
|
|
|
910
939
|
- `{{<id/>}}`: the explicit Wiki-style anchor, setting the object id
|
|
911
|
-
(e.g. `## ENTITY: Attendee
|
|
940
|
+
(e.g. `## ENTITY: Attendee {{attendee}}`).
|
|
912
941
|
|
|
913
942
|
- `(<token/>)`: a parenthesized token with three possible roles: on
|
|
914
943
|
level 1 it becomes the artifact id (e.g. `# DATA: Data Model (DM)`);
|
|
@@ -967,7 +996,7 @@ exports. A reference is a `.`-separated path of segments, where each
|
|
|
967
996
|
segment matches a single object as:
|
|
968
997
|
|
|
969
998
|
- `<id/>`: the object id or explicit anchor (e.g. `[[attendee]]`)
|
|
970
|
-
- `<name/>`: the object name (e.g. `[[Attendee
|
|
999
|
+
- `<name/>`: the object name (e.g. `[[Attendee]]`)
|
|
971
1000
|
- `<kind/>:<name-or-id/>`: kind-qualified (e.g. `[[ENTITY:Event]]`)
|
|
972
1001
|
- `<kind/>:*` or `*`: wildcards (for match sets, see below)
|
|
973
1002
|
|
package/dst/specbook-format.yaml
CHANGED
|
@@ -711,6 +711,28 @@
|
|
|
711
711
|
several requirements otherwise) which is verifiable against its `METRIC`, written
|
|
712
712
|
with exactly one keyword `MUST`, `SHOULD`, or `COULD` matching its `PRIORITY`, plus
|
|
713
713
|
a 1-sentence rationale ("why").
|
|
714
|
+
|
|
715
|
+
The requirement is answered by the architecture, never the other way round: a
|
|
716
|
+
Quality Perspectives tactic addresses it through its mechanism, a Design Decisions
|
|
717
|
+
decision is driven by it as a force, and an Operations View concern fulfills it
|
|
718
|
+
through its procedure, so every requirement is referenced from at least one of
|
|
719
|
+
them (a `WONT` requirement, which the architecture rightly ignores, is the tolerated
|
|
720
|
+
exception the lint warning cannot tell apart) and its hub diagram shows what
|
|
721
|
+
answers it.
|
|
722
|
+
referenced:
|
|
723
|
+
- "[[TACTIC:*]]"
|
|
724
|
+
- "[[DECISION:*]]"
|
|
725
|
+
- "[[CONCERN:*]]"
|
|
726
|
+
diagram:
|
|
727
|
+
type: hub
|
|
728
|
+
center: self
|
|
729
|
+
nodes: "[[NR.REQUIREMENT:*]], [[FR.REQUIREMENT:*]], [[PREMISE:*]], [[RULE:*]],
|
|
730
|
+
[[TACTIC:*]], [[DECISION:*]], [[CONCERN:*]], [[TEST-CASE:*]]"
|
|
731
|
+
labeled: true
|
|
732
|
+
qualified: true
|
|
733
|
+
config:
|
|
734
|
+
size-node-width-max: 440
|
|
735
|
+
size-node-height-scale: 1.75
|
|
714
736
|
props:
|
|
715
737
|
- name: PRIORITY
|
|
716
738
|
desc: >
|
|
@@ -1077,7 +1099,8 @@
|
|
|
1077
1099
|
type: hub
|
|
1078
1100
|
center: self
|
|
1079
1101
|
nodes: "[[USE-CASE:*]], [[ROLE:*]], [[PERSONA:*]], [[STEP:*]], [[ACTIVITY:*]],
|
|
1080
|
-
[[FR.REQUIREMENT:*]], [[RULE:*]], [[TRANSITION:*]], [[PERMISSION:*]]
|
|
1102
|
+
[[FR.REQUIREMENT:*]], [[RULE:*]], [[TRANSITION:*]], [[PERMISSION:*]],
|
|
1103
|
+
[[PRINCIPLE:*]], [[CV.ENTITY:*]]"
|
|
1081
1104
|
labeled: true
|
|
1082
1105
|
qualified: true
|
|
1083
1106
|
config:
|
|
@@ -2133,7 +2156,7 @@
|
|
|
2133
2156
|
diagram:
|
|
2134
2157
|
type: hub
|
|
2135
2158
|
center: self
|
|
2136
|
-
nodes: "[[PRINCIPLE:*]], [[PERSONA:*]], [[STEP:*]], [[PATTERN:*]]"
|
|
2159
|
+
nodes: "[[PRINCIPLE:*]], [[PERSONA:*]], [[STEP:*]], [[USE-CASE:*]], [[PATTERN:*]]"
|
|
2137
2160
|
labeled: true
|
|
2138
2161
|
qualified: true
|
|
2139
2162
|
config:
|
|
@@ -2151,16 +2174,32 @@
|
|
|
2151
2174
|
desc: >
|
|
2152
2175
|
Comma-separated references to the User Personas objects whose experience
|
|
2153
2176
|
the principle governs, omitted for a principle governing the experience of
|
|
2154
|
-
all users alike
|
|
2177
|
+
all users alike; this scopes the principle only, its origin is the
|
|
2178
|
+
`MOTIVATED-BY` and its realization the `USE-CASES`.
|
|
2155
2179
|
value: list([[PERSONA:*]])
|
|
2156
2180
|
optional: true
|
|
2157
2181
|
- name: MOTIVATED-BY
|
|
2158
2182
|
desc: >
|
|
2159
2183
|
Comma-separated references to the User Journey step objects whose
|
|
2160
2184
|
`PAIN-POINT` or `OPPORTUNITY` the principle answers, so that a principle is
|
|
2161
|
-
grounded in the observed experience rather than in taste alone
|
|
2185
|
+
grounded in the observed experience rather than in taste alone; this is the
|
|
2186
|
+
origin of the principle (why it exists), while the `USE-CASES` are its
|
|
2187
|
+
realization (where it can be checked).
|
|
2162
2188
|
value: list([[STEP:*]])
|
|
2163
2189
|
optional: true
|
|
2190
|
+
- name: USE-CASES
|
|
2191
|
+
desc: >
|
|
2192
|
+
Comma-separated references to the USE-CASES objects whose scenario steps
|
|
2193
|
+
exhibit the behavior the principle prescribes (e.g. a `Moderation-Gated
|
|
2194
|
+
Visibility` surfaces in the step where the submitted message is acknowledged
|
|
2195
|
+
but not yet public), or, where not every scenario of a use case does so, to
|
|
2196
|
+
the very `SCENARIO` objects doing so, tracing the principle into the
|
|
2197
|
+
step-by-step flows against which its realization is checked, while the
|
|
2198
|
+
concrete user interface realizing it stays with the Dialog Patterns and the
|
|
2199
|
+
Dialog Storyboard; omitted for a principle governing no use case flow in
|
|
2200
|
+
particular.
|
|
2201
|
+
value: list([[USE-CASE:*]], [[SCENARIO:*]])
|
|
2202
|
+
optional: true
|
|
2164
2203
|
- name: TRADE-OFF
|
|
2165
2204
|
desc: >
|
|
2166
2205
|
The competing concern the principle deliberately subordinates, stated so
|
|
@@ -2595,6 +2634,7 @@
|
|
|
2595
2634
|
inbound: Inbound
|
|
2596
2635
|
outbound: Outbound
|
|
2597
2636
|
both: Bidirectional
|
|
2637
|
+
via: VIA
|
|
2598
2638
|
qualified: true
|
|
2599
2639
|
properties:
|
|
2600
2640
|
- KIND
|
|
@@ -2633,8 +2673,20 @@
|
|
|
2633
2673
|
sides initiate), independent of the direction the exchanged information
|
|
2634
2674
|
flows in, which the `DATA` property describes (e.g. a datastore the
|
|
2635
2675
|
solution reads from is `Outbound`, as the solution initiates, while the
|
|
2636
|
-
data flows in).
|
|
2676
|
+
data flows in). For a mediated entity (see `VIA`), where the mediator
|
|
2677
|
+
initiates every interaction, the direction of the data flow instead.
|
|
2637
2678
|
value: enum(Inbound,Outbound,Bidirectional)
|
|
2679
|
+
- name: VIA
|
|
2680
|
+
desc: >
|
|
2681
|
+
Reference to the entity mediating the exchange with this one, when the
|
|
2682
|
+
solution and the entity have no direct technical interaction at all,
|
|
2683
|
+
but a person or system in between carries the data (e.g. a manager
|
|
2684
|
+
uploading the file exported by an external system), so the context
|
|
2685
|
+
diagram marks the edge as `via` that entity instead of suggesting an
|
|
2686
|
+
online integration; omitted for a directly interacting entity.
|
|
2687
|
+
value: "[[CV.ENTITY:*]]"
|
|
2688
|
+
optional: true
|
|
2689
|
+
acyclic: true
|
|
2638
2690
|
- name: TERMS
|
|
2639
2691
|
desc: >
|
|
2640
2692
|
Comma-separated references to the GLOSSARY terms the entity appears as in
|
|
@@ -2661,6 +2713,15 @@
|
|
|
2661
2713
|
re-stating them; omitted when no modeled role stands behind the entity.
|
|
2662
2714
|
value: list([[ROLE:*]])
|
|
2663
2715
|
optional: true
|
|
2716
|
+
- name: USE-CASES
|
|
2717
|
+
desc: >
|
|
2718
|
+
Comma-separated references to the USE-CASES objects whose interaction
|
|
2719
|
+
crosses the boundary at this entity (the use cases a `Person` entity
|
|
2720
|
+
performs, or the ones exchanging data with a `System` or `Service`
|
|
2721
|
+
entity), so the use case responsible for an entity is found without a
|
|
2722
|
+
text search; omitted when no modeled use case touches the entity.
|
|
2723
|
+
value: list([[USE-CASE:*]])
|
|
2724
|
+
optional: true
|
|
2664
2725
|
- name: PREMISES
|
|
2665
2726
|
desc: >
|
|
2666
2727
|
Comma-separated references to the SOLUTION-PREMISES objects (usually of
|
|
@@ -2714,7 +2775,8 @@
|
|
|
2714
2775
|
diagram:
|
|
2715
2776
|
type: hub
|
|
2716
2777
|
center: self
|
|
2717
|
-
nodes: "[[FV.COMPONENT:*]], [[FR.REQUIREMENT:*]], [[INTERFACE:*]], [[DM.ENTITY:*]]
|
|
2778
|
+
nodes: "[[FV.COMPONENT:*]], [[FR.REQUIREMENT:*]], [[INTERFACE:*]], [[DM.ENTITY:*]],
|
|
2779
|
+
[[TACTIC:*]], [[DECISION:*]]"
|
|
2718
2780
|
labeled: true
|
|
2719
2781
|
qualified: true
|
|
2720
2782
|
properties:
|
|
@@ -2853,7 +2915,8 @@
|
|
|
2853
2915
|
diagram:
|
|
2854
2916
|
type: hub
|
|
2855
2917
|
center: self
|
|
2856
|
-
nodes: "[[TS.TIER:*.COMPONENT:*]], [[FV.COMPONENT:*]], [[
|
|
2918
|
+
nodes: "[[TS.TIER:*.COMPONENT:*]], [[FV.COMPONENT:*]], [[TACTIC:*]],
|
|
2919
|
+
[[DECISION:*]]"
|
|
2857
2920
|
labeled: true
|
|
2858
2921
|
qualified: true
|
|
2859
2922
|
properties:
|
|
@@ -3176,7 +3239,22 @@
|
|
|
3176
3239
|
states the product choice), does not restate the runtime structure (a Functionality
|
|
3177
3240
|
View `Module` or `Subsystem` groups the logic), does not cover production
|
|
3178
3241
|
procedures (an Operations View concern does), and the trade-off that produced a
|
|
3179
|
-
rule is a Design Decisions decision referencing the aspect through `AFFECTS
|
|
3242
|
+
rule is a Design Decisions decision referencing the aspect through `AFFECTS`,
|
|
3243
|
+
while the quality the rule protects (usually a `Maintainability` requirement) is
|
|
3244
|
+
addressed by a Quality Perspectives tactic referencing the aspect through
|
|
3245
|
+
`AFFECTS`, too, so the hub diagram of an aspect shows the decision and the tactic
|
|
3246
|
+
behind it.
|
|
3247
|
+
diagram:
|
|
3248
|
+
type: hub
|
|
3249
|
+
center: self
|
|
3250
|
+
nodes: "[[ASPECT:*]], [[TACTIC:*]], [[DECISION:*]]"
|
|
3251
|
+
labeled: true
|
|
3252
|
+
qualified: true
|
|
3253
|
+
properties:
|
|
3254
|
+
- CATEGORY
|
|
3255
|
+
config:
|
|
3256
|
+
size-node-width-max: 440
|
|
3257
|
+
size-node-height-scale: 1.75
|
|
3180
3258
|
props:
|
|
3181
3259
|
- name: CATEGORY
|
|
3182
3260
|
desc: >
|
|
@@ -3229,7 +3307,7 @@
|
|
|
3229
3307
|
diagram:
|
|
3230
3308
|
type: hub
|
|
3231
3309
|
center: self
|
|
3232
|
-
nodes: "[[UNIT:*]], [[FV.COMPONENT:*]], [[NODE:*]]"
|
|
3310
|
+
nodes: "[[UNIT:*]], [[FV.COMPONENT:*]], [[NODE:*]], [[TACTIC:*]], [[DECISION:*]]"
|
|
3233
3311
|
labeled: true
|
|
3234
3312
|
qualified: true
|
|
3235
3313
|
properties:
|
|
@@ -3330,7 +3408,7 @@
|
|
|
3330
3408
|
diagram:
|
|
3331
3409
|
type: hub
|
|
3332
3410
|
center: self
|
|
3333
|
-
nodes: "[[DV.TIER:*]], [[NODE:*]]"
|
|
3411
|
+
nodes: "[[DV.TIER:*]], [[NODE:*]], [[TACTIC:*]], [[DECISION:*]]"
|
|
3334
3412
|
labeled: true
|
|
3335
3413
|
qualified: true
|
|
3336
3414
|
properties:
|
|
@@ -3380,7 +3458,7 @@
|
|
|
3380
3458
|
diagram:
|
|
3381
3459
|
type: hub
|
|
3382
3460
|
center: self
|
|
3383
|
-
nodes: "[[AREA:*]], [[NODE:*]]"
|
|
3461
|
+
nodes: "[[AREA:*]], [[NODE:*]], [[TACTIC:*]], [[DECISION:*]]"
|
|
3384
3462
|
labeled: true
|
|
3385
3463
|
qualified: true
|
|
3386
3464
|
properties:
|
|
@@ -3407,7 +3485,7 @@
|
|
|
3407
3485
|
type: hub
|
|
3408
3486
|
center: self
|
|
3409
3487
|
nodes: "[[DV.TIER:*]], [[AREA:*]], [[NODE:*]], [[NETWORK:*]], [[FV.COMPONENT:*]],
|
|
3410
|
-
[[UNIT:*]]"
|
|
3488
|
+
[[UNIT:*]], [[TACTIC:*]], [[DECISION:*]]"
|
|
3411
3489
|
labeled: true
|
|
3412
3490
|
qualified: true
|
|
3413
3491
|
properties:
|
|
@@ -3504,7 +3582,7 @@
|
|
|
3504
3582
|
diagram:
|
|
3505
3583
|
type: hub
|
|
3506
3584
|
center: self
|
|
3507
|
-
nodes: "[[NETWORK:*]], [[NODE:*]]"
|
|
3585
|
+
nodes: "[[NETWORK:*]], [[NODE:*]], [[TACTIC:*]], [[DECISION:*]]"
|
|
3508
3586
|
labeled: true
|
|
3509
3587
|
qualified: true
|
|
3510
3588
|
properties:
|
|
@@ -3886,10 +3964,11 @@
|
|
|
3886
3964
|
Comma-separated references to the NON-FUNCTIONAL-REQUIREMENTS or
|
|
3887
3965
|
SOLUTION-PREMISES objects forcing the decision (e.g. the compliance
|
|
3888
3966
|
obligation, the budget constraint, the dependency taken as given), anchoring
|
|
3889
|
-
the forces `WHEN` describes in prose
|
|
3890
|
-
|
|
3967
|
+
the forces `WHEN` describes in prose: as a decision is the response to
|
|
3968
|
+
forces, at least one is always named, usually a Non-Functional Requirement
|
|
3969
|
+
and only exceptionally a premise alone, and a decision finding no
|
|
3970
|
+
requirement to name reveals a missing one rather than a needless reference.
|
|
3891
3971
|
value: list([[NR.*]], [[PREMISE:*]])
|
|
3892
|
-
optional: true
|
|
3893
3972
|
- name: AFFECTS
|
|
3894
3973
|
desc: >
|
|
3895
3974
|
Comma-separated references to the objects of the DATA, APIS, UXUI, or ARCH
|
|
@@ -4014,6 +4093,11 @@
|
|
|
4014
4093
|
or fail, and many test cases (e.g. of Non-Functional Requirements or of cross-session rules)
|
|
4015
4094
|
have no use case counterpart at all.
|
|
4016
4095
|
optional: true
|
|
4096
|
+
coverage:
|
|
4097
|
+
- "[[USE-CASE:*]]"
|
|
4098
|
+
- "[[SCENARIO:*]]"
|
|
4099
|
+
- "[[REQUIREMENT:*]]"
|
|
4100
|
+
- "[[RULE:*]]"
|
|
4017
4101
|
refs: >
|
|
4018
4102
|
- ISO/IEC/IEEE 29119-3:2021: Software and systems engineering -- Software testing -- Part 3: Test documentation (2021),
|
|
4019
4103
|
https://www.iso.org/standard/79429.html
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { referenceRegex, resolveUnique, resolveSet, chainOf, plainText, assignId } from "./specbook-link.js";
|
|
7
7
|
import { compileValueExpr, splitItems, anchored } from "./specbook-parse-value.js";
|
|
8
|
+
import { referencedCoverage } from "./specbook-coverage.js";
|
|
8
9
|
/* the syntax of a value consisting of exactly one Wiki-style reference */
|
|
9
10
|
const singleReferenceRegex = /^\[\[([^[\]]+)\]\]$/;
|
|
10
11
|
/* match a full property value directly against a value expression
|
|
@@ -431,50 +432,13 @@ const checkRelations = (ctx, schemas) => {
|
|
|
431
432
|
of the flag's reference patterns, itself or through an ancestor, while
|
|
432
433
|
the references from within the subtree of the object itself do not
|
|
433
434
|
count (a lapse is a warning only, as the specification stays valid) */
|
|
434
|
-
const checkReferenced = (ctx,
|
|
435
|
-
|
|
436
|
-
(
|
|
437
|
-
const referrers = new Map();
|
|
438
|
-
const walk = (object) => {
|
|
439
|
-
const chain = chainOf(ctx.linkIndex, object);
|
|
440
|
-
const texts = [object.name, ...object.properties.map((p) => p.value)];
|
|
441
|
-
if (object.description !== undefined)
|
|
442
|
-
texts.push(object.description.description, object.description.rationale ?? "");
|
|
443
|
-
for (const text of texts)
|
|
444
|
-
for (const m of plainText(text).matchAll(referenceRegex)) {
|
|
445
|
-
const target = resolveUnique(ctx.linkIndex, m[1].trim(), object).target;
|
|
446
|
-
if (target !== undefined && !chain.includes(target))
|
|
447
|
-
referrers.set(target, (referrers.get(target) ?? new Set()).add(object));
|
|
448
|
-
}
|
|
449
|
-
object.children.forEach(walk);
|
|
450
|
-
};
|
|
451
|
-
for (const artifact of specification.artifacts)
|
|
452
|
-
artifact.objects.forEach(walk);
|
|
453
|
-
/* resolve the reference patterns of every flagged kind into the
|
|
454
|
-
admissible referencing objects (once per shared schema node) */
|
|
455
|
-
const sources = new Map();
|
|
456
|
-
for (const [object, schema] of schemas) {
|
|
457
|
-
if (schema.referenced === undefined)
|
|
458
|
-
continue;
|
|
459
|
-
let admissible = sources.get(schema);
|
|
460
|
-
if (admissible === undefined) {
|
|
461
|
-
admissible = new Set();
|
|
462
|
-
for (const entry of schema.referenced) {
|
|
463
|
-
const expr = compileValueExpr(entry);
|
|
464
|
-
if (expr.kind === "reference")
|
|
465
|
-
for (const source of resolveSet(ctx.linkIndex, expr.pattern))
|
|
466
|
-
admissible.add(source);
|
|
467
|
-
}
|
|
468
|
-
sources.set(schema, admissible);
|
|
469
|
-
}
|
|
470
|
-
const covered = Array.from(referrers.get(object) ?? [])
|
|
471
|
-
.some((referrer) => chainOf(ctx.linkIndex, referrer).some((o) => admissible.has(o)));
|
|
472
|
-
if (!covered) {
|
|
435
|
+
const checkReferenced = (ctx, schemas) => {
|
|
436
|
+
for (const { schema, uncovered } of referencedCoverage(ctx.linkIndex, schemas))
|
|
437
|
+
for (const object of uncovered) {
|
|
473
438
|
const meta = ctx.metaOf(object);
|
|
474
439
|
ctx.diagnose(meta.file, meta.line, `${object.kind} "${object.name}" is not referenced from any object matching ` +
|
|
475
|
-
schema.referenced.map((entry) => `"${entry}"`).join(" or "), "warning");
|
|
440
|
+
(schema.referenced ?? []).map((entry) => `"${entry}"`).join(" or "), "warning");
|
|
476
441
|
}
|
|
477
|
-
}
|
|
478
442
|
};
|
|
479
443
|
/* validate the parsed specification against the configuration */
|
|
480
444
|
export const validate = (ctx, specification, config) => {
|
|
@@ -527,7 +491,7 @@ export const validate = (ctx, specification, config) => {
|
|
|
527
491
|
been validated on its own */
|
|
528
492
|
checkRelations(ctx, schemas);
|
|
529
493
|
checkAutomata(ctx, schemas);
|
|
530
|
-
checkReferenced(ctx,
|
|
494
|
+
checkReferenced(ctx, schemas);
|
|
531
495
|
/* order the artifacts exactly along the schema definition */
|
|
532
496
|
specification.artifacts.sort((a, b) => (position.get(a) ?? config.length) - (position.get(b) ?? config.length));
|
|
533
497
|
};
|
package/dst/specbook-parse.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Spec } from "./specbook-format-spec.js";
|
|
|
2
2
|
import { type Schema } from "./specbook-format-schema.js";
|
|
3
3
|
import { type SourceFile, type ParseResult } from "./specbook-parse-common.js";
|
|
4
4
|
export { type SourceFile, type ParseResult } from "./specbook-parse-common.js";
|
|
5
|
-
export { resolveArtifact } from "./specbook-parse-semantic.js";
|
|
5
|
+
export { resolveArtifact, collectSchemas } from "./specbook-parse-semantic.js";
|
|
6
6
|
export declare const parseSpecification: (sources: SourceFile[], config?: Schema) => ParseResult;
|
|
7
7
|
export declare const specStatistics: (specification: Spec) => {
|
|
8
8
|
objects: number;
|
package/dst/specbook-parse.js
CHANGED
|
@@ -10,7 +10,7 @@ import { ParseContext } from "./specbook-parse-common.js";
|
|
|
10
10
|
import { parseFile } from "./specbook-parse-syntax.js";
|
|
11
11
|
import { validate, validateReferences } from "./specbook-parse-semantic.js";
|
|
12
12
|
import { validateDiagrams } from "./specbook-diagram.js";
|
|
13
|
-
export { resolveArtifact } from "./specbook-parse-semantic.js";
|
|
13
|
+
export { resolveArtifact, collectSchemas } from "./specbook-parse-semantic.js";
|
|
14
14
|
/* parse a set of specification Markdown files into the specification
|
|
15
15
|
AST and optionally validate the result against a configuration */
|
|
16
16
|
export const parseSpecification = (sources, config) => {
|