@rse/specbook 1.4.0 → 1.4.2
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 +4 -0
- package/dst/specbook-api.d.ts +5 -1
- package/dst/specbook-api.js +16 -4
- package/dst/specbook-cli.js +8 -5
- package/dst/specbook-cmd-init.js +16 -2
- package/dst/specbook-diagram.js +3 -7
- package/dst/specbook-export-common.d.ts +4 -0
- package/dst/specbook-export-common.js +16 -0
- package/dst/specbook-export-diagram.js +6 -4
- package/dst/specbook-export-html-render.js +2 -3
- package/dst/specbook-format.md +5 -3
- package/dst/specbook-format.yaml +41 -1
- package/dst/specbook-mcp.js +9 -5
- package/dst/specbook-project.d.ts +1 -0
- package/dst/specbook-project.js +10 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -324,6 +324,10 @@ validated (an unknown entry or a wrongly typed value fails the command),
|
|
|
324
324
|
and both an explicitly supplied option and its environment variable
|
|
325
325
|
win over its entry.
|
|
326
326
|
|
|
327
|
+
`specbook init` creates such a file in the current working directory
|
|
328
|
+
(unless one already applies), so the options of the initialization
|
|
329
|
+
stick for all subsequent commands.
|
|
330
|
+
|
|
327
331
|
The default value of every CLI option `--xxx` can be overridden
|
|
328
332
|
by a corresponding `SPECBOOK_XXX` environment variable (e.g.
|
|
329
333
|
`SPECBOOK_BASEDIR`, `SPECBOOK_CONFIG`, `SPECBOOK_OUTPUT`,
|
package/dst/specbook-api.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface ProjectOptions {
|
|
|
28
28
|
basedir?: string;
|
|
29
29
|
cwd?: string;
|
|
30
30
|
}
|
|
31
|
+
export interface InitResult {
|
|
32
|
+
files: string[];
|
|
33
|
+
project?: string;
|
|
34
|
+
}
|
|
31
35
|
export interface ExportRequest extends ProjectOptions {
|
|
32
36
|
formats?: ExportFormat[];
|
|
33
37
|
realtime?: boolean;
|
|
@@ -46,7 +50,7 @@ export declare class SpecBook {
|
|
|
46
50
|
private project;
|
|
47
51
|
private configFiles;
|
|
48
52
|
private requireConfig;
|
|
49
|
-
init(options: ProjectOptions): Promise<
|
|
53
|
+
init(options: ProjectOptions): Promise<InitResult>;
|
|
50
54
|
lint(options: ProjectOptions & {
|
|
51
55
|
gitignore?: boolean;
|
|
52
56
|
}): Promise<LintResult>;
|
package/dst/specbook-api.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as path from "node:path";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { glob } from "glob";
|
|
10
10
|
import { loadConfig, configStatistics } from "./specbook-config.js";
|
|
11
|
-
import { loadProject, projectFile } from "./specbook-project.js";
|
|
11
|
+
import { loadProject, writeProject, projectFile } from "./specbook-project.js";
|
|
12
12
|
import { renderDiagnostic, renderDiagnosticVerbose } from "./specbook-diagnostic.js";
|
|
13
13
|
import { initSpecification } from "./specbook-cmd-init.js";
|
|
14
14
|
import { lint } from "./specbook-cmd-lint.js";
|
|
@@ -62,6 +62,7 @@ export class SpecBook {
|
|
|
62
62
|
const given = (value) => options.cwd !== undefined ? path.resolve(cwd, value) : value;
|
|
63
63
|
const derived = (value) => options.cwd !== undefined ? value : path.relative(cwd, value) || ".";
|
|
64
64
|
return {
|
|
65
|
+
file: project?.file,
|
|
65
66
|
config: config !== undefined && config.length > 0 ?
|
|
66
67
|
config.map((pattern) => pattern === "std" ? pattern : given(pattern)) :
|
|
67
68
|
project?.config?.map((pattern) => pattern === "std" ? pattern : derived(pattern)),
|
|
@@ -105,13 +106,24 @@ export class SpecBook {
|
|
|
105
106
|
`and ${literal(stats.links)} link relationship(s)`);
|
|
106
107
|
return config;
|
|
107
108
|
}
|
|
108
|
-
/* initialize the configured specification artifact files
|
|
109
|
-
|
|
109
|
+
/* initialize the configured specification artifact files below the
|
|
110
|
+
base directory and persist the effective project options into a
|
|
111
|
+
new project configuration file in the working directory, unless
|
|
112
|
+
one already applies (as a new one would shadow it) */
|
|
110
113
|
async init(options) {
|
|
111
114
|
const verbose = this.verboseOf("init");
|
|
112
115
|
const project = this.project(options, verbose);
|
|
113
|
-
|
|
116
|
+
const files = initSpecification({ config: this.requireConfig(await this.configFiles(project.config), verbose),
|
|
114
117
|
basedir: project.basedir ?? ".", verbose });
|
|
118
|
+
if (project.file !== undefined) {
|
|
119
|
+
verbose(`skipping existing project configuration "${literal(project.file)}"`);
|
|
120
|
+
return { files };
|
|
121
|
+
}
|
|
122
|
+
const cwd = path.resolve(options.cwd ?? ".");
|
|
123
|
+
const relative = (value) => path.relative(cwd, path.resolve(cwd, value)) || ".";
|
|
124
|
+
const file = writeProject(cwd, project.config?.map((pattern) => pattern === "std" ? pattern : relative(pattern)) ?? ["std"], relative(project.basedir ?? "."));
|
|
125
|
+
verbose(`created project configuration "${literal(file)}"`);
|
|
126
|
+
return { files, project: options.cwd !== undefined ? file : path.relative(process.cwd(), file) };
|
|
115
127
|
}
|
|
116
128
|
/* lint the specification Markdown files below the base directory,
|
|
117
129
|
where "gitignore" treats an artifact file Git excludes from its
|
package/dst/specbook-cli.js
CHANGED
|
@@ -130,15 +130,18 @@ withVerboseOption(program.command("mcp"))
|
|
|
130
130
|
verboseOf(opts)("mcp", "starting MCP server on stdio", "notice");
|
|
131
131
|
await serveMcp(verboseOf(opts, "mcp"));
|
|
132
132
|
});
|
|
133
|
-
/* the init command creates the configured artifact files
|
|
133
|
+
/* the init command creates the configured artifact files and,
|
|
134
|
+
unless one already applies, the project configuration file */
|
|
134
135
|
withCommonOptions(program.command("init"))
|
|
135
136
|
.description("initialize the configured specification artifact files below the base directory")
|
|
136
137
|
.action(async (opts) => {
|
|
137
138
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
138
|
-
const
|
|
139
|
-
await writeStdout(
|
|
140
|
-
`initialized artifact file(s): ${
|
|
141
|
-
"no artifact files were created\n")
|
|
139
|
+
const result = await specbook.init({ config: configOf(opts), basedir: opts.basedir });
|
|
140
|
+
await writeStdout((result.files.length > 0 ?
|
|
141
|
+
`initialized artifact file(s): ${result.files.join(", ")}\n` :
|
|
142
|
+
"no artifact files were created\n") +
|
|
143
|
+
(result.project !== undefined ?
|
|
144
|
+
`created project configuration file: ${result.project}\n` : ""));
|
|
142
145
|
});
|
|
143
146
|
/* the lint command reports all diagnostics and fails on any error */
|
|
144
147
|
withGitignoreOption(withCommonOptions(program.command("lint")))
|
package/dst/specbook-cmd-init.js
CHANGED
|
@@ -7,8 +7,22 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { literal } from "./specbook-verbose.js";
|
|
9
9
|
import { formatTimestamp } from "./specbook-export-md.js";
|
|
10
|
+
import { titleProperties } from "./specbook-export-common.js";
|
|
11
|
+
/* render the property list of the title artifact, seeding the properties
|
|
12
|
+
the exports interpret themselves with their rendering defaults (as far
|
|
13
|
+
as the schema configures them, as any other is reported as unknown) */
|
|
14
|
+
const titleList = (artifact) => {
|
|
15
|
+
if (artifact.kind !== "META" || artifact.name?.toUpperCase() !== "TITLE")
|
|
16
|
+
return "";
|
|
17
|
+
const props = titleProperties.filter((prop) => artifact.props?.some((p) => p.name === prop.name) === true);
|
|
18
|
+
if (props.length === 0)
|
|
19
|
+
return "";
|
|
20
|
+
const width = Math.max(...props.map((prop) => prop.name.length)) + 2;
|
|
21
|
+
return props.map((prop) => `- ${`${prop.name}:`.padEnd(width)}${prop.init}`.trimEnd() + "\n").join("") + "\n";
|
|
22
|
+
};
|
|
10
23
|
/* initialize the configured specification artifact files below the
|
|
11
|
-
base directory with their frontmatter and artifact headings
|
|
24
|
+
base directory with their frontmatter and artifact headings (plus
|
|
25
|
+
the title properties), where
|
|
12
26
|
all artifacts configured onto the same file reside in it side by
|
|
13
27
|
side, following each other on level 1 */
|
|
14
28
|
export const initSpecification = (options) => {
|
|
@@ -38,7 +52,7 @@ export const initSpecification = (options) => {
|
|
|
38
52
|
const headings = artifacts.map((artifact) => {
|
|
39
53
|
const name = artifact.name ?? "";
|
|
40
54
|
const paren = artifact.id !== undefined ? ` (${artifact.id})` : "";
|
|
41
|
-
return `# ${artifact.kind}: ${name}${paren}\n\n
|
|
55
|
+
return `# ${artifact.kind}: ${name}${paren}\n\n` + titleList(artifact);
|
|
42
56
|
}).join("");
|
|
43
57
|
const text = "---\n" +
|
|
44
58
|
`Created: ${now}\n` +
|
package/dst/specbook-diagram.js
CHANGED
|
@@ -344,14 +344,10 @@ const deriveCenter = (ctx, object, diagram, nodes, nodeSet, errors) => {
|
|
|
344
344
|
return undefined;
|
|
345
345
|
}
|
|
346
346
|
centerUrl = `#${anchors.get(source) ?? source.id}`;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
errors.push({ reason: `diagram "center" source lacks the property "${centerCfg.property}"` });
|
|
351
|
-
return undefined;
|
|
352
|
-
}
|
|
347
|
+
const value = centerCfg.property !== undefined ?
|
|
348
|
+
propValue(parenProps, source, centerCfg.property) : undefined;
|
|
349
|
+
if (value !== undefined)
|
|
353
350
|
label = plainText(expandReferences(value, (ref) => resolveUnique(index, ref, source).target?.name ?? ref));
|
|
354
|
-
}
|
|
355
351
|
else if (label === undefined)
|
|
356
352
|
label = plainText(source.name);
|
|
357
353
|
}
|
|
@@ -31,6 +31,10 @@ export declare const paperSizeDefault = "A4";
|
|
|
31
31
|
export declare const paperSetup: (paper: string) => PaperSetup;
|
|
32
32
|
export declare const paperLength: (setup: PaperSetup, value: number) => string;
|
|
33
33
|
export declare const documentPaperSize: (specification: Spec) => string;
|
|
34
|
+
export declare const titleProperties: {
|
|
35
|
+
name: string;
|
|
36
|
+
init: string;
|
|
37
|
+
}[];
|
|
34
38
|
export declare const paperStylesheet: (paper: string) => string;
|
|
35
39
|
export declare const charsetCodepoints: (charset: string) => number[] | undefined;
|
|
36
40
|
export declare const subsetStylesheet: (charset?: string) => Promise<string>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import { embeddingThemes, darkMark } from "./specbook-parse-common.js";
|
|
8
8
|
import { plainText } from "./specbook-link.js";
|
|
9
|
+
import { themeToneDefault } from "./specbook-theme.js";
|
|
9
10
|
/* escape a text for embedding into template HTML (text and attributes) */
|
|
10
11
|
export const escapeHtml = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
11
12
|
.replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -111,6 +112,21 @@ export const documentPaperSize = (specification) => {
|
|
|
111
112
|
`(expected ${paperSizes.join(", ")})`);
|
|
112
113
|
return paper;
|
|
113
114
|
};
|
|
115
|
+
/* the properties of the title object the exports interpret themselves,
|
|
116
|
+
each with the value "init" seeds it with (its rendering default, an
|
|
117
|
+
empty value counting as an absent property) */
|
|
118
|
+
export const titleProperties = [
|
|
119
|
+
{ name: "LOGO", init: "" },
|
|
120
|
+
{ name: "TITLE", init: "Specification" },
|
|
121
|
+
{ name: "SUBTITLE", init: "" },
|
|
122
|
+
{ name: "AUTHOR", init: "" },
|
|
123
|
+
{ name: "VERSION", init: "" },
|
|
124
|
+
{ name: "LANG", init: "en" },
|
|
125
|
+
{ name: "CHARSET", init: "UTF-8" },
|
|
126
|
+
{ name: "THEME-STYLE", init: "Light" },
|
|
127
|
+
{ name: "THEME-TONE", init: themeToneDefault },
|
|
128
|
+
{ name: "PAPER-SIZE", init: paperSizeDefault }
|
|
129
|
+
];
|
|
114
130
|
/* the vertical room (in rem) the introducing heading of a diagram
|
|
115
131
|
claims above it on the same page: without this reserve a maximally
|
|
116
132
|
sized diagram could not share its page with the heading, which
|
|
@@ -81,16 +81,18 @@ const mermaidColors = (colors, style) => {
|
|
|
81
81
|
theme color spreads after the shape of the D2 themes "Neutral
|
|
82
82
|
Default" (light) and "Dark Mauve" (dark): the neutrals "N1" (text)
|
|
83
83
|
to "N7" (background), the base colors "B1" (strokes) to "B6"
|
|
84
|
-
(fills), and the alternative accents "AA*"/"AB*"
|
|
84
|
+
(fills), and the alternative accents "AA*"/"AB*", where the fills
|
|
85
|
+
"B4" to "B6" of the nesting levels 1 to 3 are spread far apart and,
|
|
86
|
+
unlike in D2, recede towards the outer levels (the containers) */
|
|
85
87
|
const d2Colors = (colors, style) => {
|
|
86
88
|
const [b, a, s] = [colors.base, colors.accent, colors.signal];
|
|
87
89
|
return style === "dark" ? {
|
|
88
90
|
N1: b[28], N2: b[25], N3: b[22], N4: b[13], N5: b[10], N6: b[8], N7: b[6],
|
|
89
|
-
B1: a[21], B2: a[21], B3: a[13], B4:
|
|
91
|
+
B1: a[21], B2: a[21], B3: a[13], B4: b[9], B5: a[0], B6: a[5],
|
|
90
92
|
AA2: s[19], AA4: s[10], AA5: s[8], AB4: s[10], AB5: s[8]
|
|
91
93
|
} : {
|
|
92
94
|
N1: b[3], N2: b[13], N3: b[19], N4: b[25], N5: b[27], N6: b[29], N7: b[31],
|
|
93
|
-
B1: a[7], B2: a[11], B3: a[27], B4: a[
|
|
95
|
+
B1: a[7], B2: a[11], B3: a[27], B4: a[31], B5: a[28], B6: a[24],
|
|
94
96
|
AA2: s[13], AA4: s[28], AA5: s[30], AB4: s[28], AB5: s[30]
|
|
95
97
|
};
|
|
96
98
|
};
|
|
@@ -128,7 +130,7 @@ const d2Error = (err) => {
|
|
|
128
130
|
const renderD2 = (source, colors, style) => {
|
|
129
131
|
const overrides = Object.entries(colors).map(([key, color]) => `${key}: "${color}"`).join("; ");
|
|
130
132
|
const request = d2Chain.then(async () => {
|
|
131
|
-
d2 ??= import("@
|
|
133
|
+
d2 ??= import("@d2lang/d2").then((module) => new module.D2())
|
|
132
134
|
.catch((err) => {
|
|
133
135
|
d2 = null;
|
|
134
136
|
throw err;
|
|
@@ -8,7 +8,7 @@ import { markedSmartypants } from "marked-smartypants";
|
|
|
8
8
|
import { resolveUnique, expandReferences } from "./specbook-link.js";
|
|
9
9
|
import { compileValueExpr, splitItems } from "./specbook-parse-value.js";
|
|
10
10
|
import { embeddingRegex, embeddingCount, embeddingThemes, darkMark } from "./specbook-parse-common.js";
|
|
11
|
-
import { escapeHtml, fallbackLogo, imageDark, isTitleObject } from "./specbook-export-common.js";
|
|
11
|
+
import { escapeHtml, fallbackLogo, imageDark, isTitleObject, titleProperties } from "./specbook-export-common.js";
|
|
12
12
|
import { isDocument } from "./specbook-export-image.js";
|
|
13
13
|
import { embeddedSource, fenceLanguage, diagramKey, omittedDiagram } from "./specbook-export-diagram.js";
|
|
14
14
|
import { embeddedListing, fencedListing } from "./specbook-export-code.js";
|
|
@@ -680,8 +680,7 @@ export const renderTitlePage = (object, created, modified) => {
|
|
|
680
680
|
const value = prop(name);
|
|
681
681
|
return value !== undefined && value.trim() !== "" ? inline(value) : "";
|
|
682
682
|
};
|
|
683
|
-
const rest = object.properties.filter((property) => !
|
|
684
|
-
"LANG", "CHARSET", "PAPER-SIZE", "THEME-STYLE", "THEME-TONE"].includes(property.key));
|
|
683
|
+
const rest = object.properties.filter((property) => !titleProperties.some((prop) => prop.name === property.key));
|
|
685
684
|
/* the logo is rendered above the title, from the embedded image content
|
|
686
685
|
of the LOGO property or, for a non-embeddable reference, as its inline
|
|
687
686
|
Markdown; without a (non-empty) LOGO property the built-in SpecBook
|
package/dst/specbook-format.md
CHANGED
|
@@ -323,7 +323,8 @@ type SchemaGradiaConfig = Partial<{
|
|
|
323
323
|
- `SchemaDiagram.center?: string | SchemaDiagramCenter`:
|
|
324
324
|
object a `hub` diagram is projected onto (default: `self`), or a
|
|
325
325
|
synthetic center node, labeled from a referenced `source` object (or
|
|
326
|
-
one of its properties, via `property
|
|
326
|
+
one of its properties, via `property`, falling back onto the `label`
|
|
327
|
+
or the object name while absent) or a literal `label`, typed
|
|
327
328
|
(under `qualified`) by `kind`, and linked to the `source` object,
|
|
328
329
|
BECAUSE the focus of a hub is not always a specification object --
|
|
329
330
|
the solution itself, e.g., sits in the middle of a context diagram
|
|
@@ -572,7 +573,8 @@ Instead of naming an existing object, `center` can also declare a
|
|
|
572
573
|
*synthetic* center node, which represents something no specification
|
|
573
574
|
object models -- like the solution itself in the middle of a context
|
|
574
575
|
diagram. Such a center is labeled from a referenced `source` object (or
|
|
575
|
-
one of its properties, via `property
|
|
576
|
+
one of its properties, via `property`, falling back onto the `label` or
|
|
577
|
+
the object name while the property is absent) or a literal `label`, and links
|
|
576
578
|
to the `source` object. As the nodes carry no `[[xxx]]` reference to a
|
|
577
579
|
synthetic center, `centerEdges` synthesizes the edges from a direction
|
|
578
580
|
`property` of the node objects: its `inbound` value maps onto a
|
|
@@ -1146,7 +1148,7 @@ its own and embedded like an image -- ``,
|
|
|
1146
1148
|
``, or `` -- inside a
|
|
1147
1149
|
description or property value, where its source is inlined at parse time.
|
|
1148
1150
|
The HTML/PDF export renders every such diagram (Mermaid through
|
|
1149
|
-
`beautiful-mermaid`, D2 through `@
|
|
1151
|
+
`beautiful-mermaid`, D2 through `@d2lang/d2`) in the theme colors of
|
|
1150
1152
|
the document, once per color theme, like a theme-aware image. It folds,
|
|
1151
1153
|
maximizes, and is omitted (`-O diagram:code`, for diagrams as code, or
|
|
1152
1154
|
by nesting level) like a derived Gradia diagram. A diagram failing to render
|
package/dst/specbook-format.yaml
CHANGED
|
@@ -93,7 +93,9 @@
|
|
|
93
93
|
#
|
|
94
94
|
# - All artifacts are optional, so that the schema can back not only
|
|
95
95
|
# a full specification but also the individual document types of
|
|
96
|
-
# the modeling coverage matrix above on their own.
|
|
96
|
+
# the modeling coverage matrix above on their own. Likewise, the
|
|
97
|
+
# properties and child objects of all artifacts are optional, so
|
|
98
|
+
# that a freshly initialized specification already lints cleanly.
|
|
97
99
|
#
|
|
98
100
|
# - The traditional hard Software Requirements Specification (SRS)
|
|
99
101
|
# vs. Architecture Description (AD) document split is
|
|
@@ -128,6 +130,7 @@
|
|
|
128
130
|
optional: true
|
|
129
131
|
- name: TITLE
|
|
130
132
|
desc: The title of the specification.
|
|
133
|
+
optional: true
|
|
131
134
|
- name: SUBTITLE
|
|
132
135
|
desc: The subtitle of the specification.
|
|
133
136
|
optional: true
|
|
@@ -182,17 +185,21 @@
|
|
|
182
185
|
- name: PURPOSE
|
|
183
186
|
desc: >
|
|
184
187
|
Why the solution exists.
|
|
188
|
+
optional: true
|
|
185
189
|
- name: TARGET-AUDIENCE
|
|
186
190
|
desc: >
|
|
187
191
|
Whom the solution serves.
|
|
192
|
+
optional: true
|
|
188
193
|
- name: SOLUTION
|
|
189
194
|
desc: >
|
|
190
195
|
What the solution is, in a single sentence naming its kind or
|
|
191
196
|
category (e.g. a self-hosted, browser-based streaming platform).
|
|
197
|
+
optional: true
|
|
192
198
|
- name: VALUE-PROPOSITION
|
|
193
199
|
desc: >
|
|
194
200
|
The benefit the solution offers its target audience, stated in
|
|
195
201
|
absolute terms without reference to alternatives.
|
|
202
|
+
optional: true
|
|
196
203
|
- name: DIFFERENTIATORS
|
|
197
204
|
desc: >
|
|
198
205
|
How the solution stands apart from the alternatives, naming the
|
|
@@ -266,6 +273,7 @@
|
|
|
266
273
|
enforces itself is a Domain Rule or a Non-Functional Requirement instead) and
|
|
267
274
|
states a fact or event, not a decision (a deliberate exclusion is the `SCOPE` of
|
|
268
275
|
the Solution Vision or a `WONT` requirement instead).
|
|
276
|
+
optional: true
|
|
269
277
|
format:
|
|
270
278
|
type: compact
|
|
271
279
|
maxTableColumns: 5
|
|
@@ -341,6 +349,7 @@
|
|
|
341
349
|
(the `BROADER` term where one exists) and adds the characteristics delimiting the
|
|
342
350
|
term from its siblings (e.g. `A Message submitted by an Attendee as ...`), so that
|
|
343
351
|
the hierarchy and the definitions cross-check each other.
|
|
352
|
+
optional: true
|
|
344
353
|
format:
|
|
345
354
|
type: compact
|
|
346
355
|
maxTableColumns: 5
|
|
@@ -438,6 +447,7 @@
|
|
|
438
447
|
exactly this part and hold exactly the rights the Authorization Model grants to it
|
|
439
448
|
(plus those of the roles it `INHERITS`), while a persona holding several roles
|
|
440
449
|
holds the union of them.
|
|
450
|
+
optional: true
|
|
441
451
|
format:
|
|
442
452
|
maxTableColumns: 5
|
|
443
453
|
referenced:
|
|
@@ -482,6 +492,7 @@
|
|
|
482
492
|
persona's own voice, like a "tagline", "mantra", or "defining statement" -- that
|
|
483
493
|
captures their core attitude, motivation, frustration, or need in a single memorable
|
|
484
494
|
line. Non-user stakeholders are modeled as personas of type `Stakeholder`.
|
|
495
|
+
optional: true
|
|
485
496
|
format:
|
|
486
497
|
maxTableColumns: 5
|
|
487
498
|
props:
|
|
@@ -573,6 +584,7 @@
|
|
|
573
584
|
desc: >
|
|
574
585
|
The end-to-end journey of a single persona (or of multiple personas sharing the same
|
|
575
586
|
experience as role variants), named with a 2-5 word summary of the pursued scenario.
|
|
587
|
+
optional: true
|
|
576
588
|
props:
|
|
577
589
|
- name: ACTORS
|
|
578
590
|
desc: >
|
|
@@ -660,6 +672,7 @@
|
|
|
660
672
|
event-driven (`When <trigger>, the system MUST ...`), state-driven (`While <state>,
|
|
661
673
|
the system MUST ...`), optional-feature (`Where <feature>, the system MUST ...`), or
|
|
662
674
|
unwanted-behavior (`If <condition>, then the system MUST ...`).
|
|
675
|
+
optional: true
|
|
663
676
|
format:
|
|
664
677
|
maxTableColumns: 6
|
|
665
678
|
referenced:
|
|
@@ -734,6 +747,7 @@
|
|
|
734
747
|
them (a `WONT` requirement, which the architecture rightly ignores, is the tolerated
|
|
735
748
|
exception the lint warning cannot tell apart) and its hub diagram shows what
|
|
736
749
|
answers it.
|
|
750
|
+
optional: true
|
|
737
751
|
referenced:
|
|
738
752
|
- "[[TACTIC:*]]"
|
|
739
753
|
- "[[DECISION:*]]"
|
|
@@ -838,6 +852,7 @@
|
|
|
838
852
|
<outcome>` item per case, applied in order with the first matching case winning,
|
|
839
853
|
and a closing sentence stating the outcome of the remaining cases ("otherwise")
|
|
840
854
|
and carrying the rationale.
|
|
855
|
+
optional: true
|
|
841
856
|
format:
|
|
842
857
|
maxTableColumns: 5
|
|
843
858
|
referenced:
|
|
@@ -950,6 +965,7 @@
|
|
|
950
965
|
cases, a use case spans several activities, and an activity performed outside the
|
|
951
966
|
solution is covered by none at all -- so the workflow is no mere sequence of use
|
|
952
967
|
cases and cannot be assembled from them.
|
|
968
|
+
optional: true
|
|
953
969
|
automaton:
|
|
954
970
|
nodes: ACTIVITY
|
|
955
971
|
edges: HANDOFF
|
|
@@ -1123,6 +1139,7 @@
|
|
|
1123
1139
|
relate it n:m to the Domain Workflow activities -- a use case spans several
|
|
1124
1140
|
activities and an activity is covered by several use cases -- so the two decompose
|
|
1125
1141
|
the same domain independently instead of nesting into each other.
|
|
1142
|
+
optional: true
|
|
1126
1143
|
diagram:
|
|
1127
1144
|
type: hub
|
|
1128
1145
|
center: self
|
|
@@ -1295,6 +1312,7 @@
|
|
|
1295
1312
|
A named entity of the data model, carrying a Pascal-cased, 1-3 word long,
|
|
1296
1313
|
per-artifact unique name, and described by a 1-sentence description ("what") plus a
|
|
1297
1314
|
1-sentence rationale ("why").
|
|
1315
|
+
optional: true
|
|
1298
1316
|
diagram:
|
|
1299
1317
|
type: hub
|
|
1300
1318
|
center: self
|
|
@@ -1462,6 +1480,7 @@
|
|
|
1462
1480
|
state machine: exactly one initial state, at least one final state, every state
|
|
1463
1481
|
reachable from the initial state, and some final state reachable from every state
|
|
1464
1482
|
(a state from which no final state can be reached anymore is a livelock).
|
|
1483
|
+
optional: true
|
|
1465
1484
|
automaton:
|
|
1466
1485
|
nodes: STATE
|
|
1467
1486
|
edges: TRANSITION
|
|
@@ -1625,6 +1644,7 @@
|
|
|
1625
1644
|
they are granted, so that the permissions of all roles form the access matrix of
|
|
1626
1645
|
roles against entities. Any operation, state, or condition not covered by a
|
|
1627
1646
|
permission of the role (or of a role it `INHERITS`) is forbidden.
|
|
1647
|
+
optional: true
|
|
1628
1648
|
format:
|
|
1629
1649
|
type: compact
|
|
1630
1650
|
maxTableColumns: 8
|
|
@@ -1738,6 +1758,7 @@
|
|
|
1738
1758
|
belongs to, where the kind-specific parts (HTTP method, GraphQL operation, topic
|
|
1739
1759
|
direction, command) are carried by the `OPERATION` and `LOCATOR` properties of the
|
|
1740
1760
|
endpoints only.
|
|
1761
|
+
optional: true
|
|
1741
1762
|
diagram:
|
|
1742
1763
|
type: hub
|
|
1743
1764
|
center: self
|
|
@@ -1993,6 +2014,7 @@
|
|
|
1993
2014
|
Endpoints form a flat list: a hierarchy of sub-commands or sub-resources
|
|
1994
2015
|
is expressed by a path-style `LOCATOR` (e.g. `remote add`,
|
|
1995
2016
|
`/events/{eventId}/chat`) only.
|
|
2017
|
+
optional: true
|
|
1996
2018
|
diagram:
|
|
1997
2019
|
type: hub
|
|
1998
2020
|
center: self
|
|
@@ -2226,6 +2248,7 @@
|
|
|
2226
2248
|
is a Dialog Pattern instead), decides between design alternatives (the alternative
|
|
2227
2249
|
it rejects is its `TRADE-OFF`), and is not verifiable by a metric (a measurable
|
|
2228
2250
|
quality bound is a Non-Functional Requirement instead).
|
|
2251
|
+
optional: true
|
|
2229
2252
|
referenced:
|
|
2230
2253
|
- "[[DIALOG-PATTERN:*]]"
|
|
2231
2254
|
format:
|
|
@@ -2325,6 +2348,7 @@
|
|
|
2325
2348
|
solution phrases what it says, not what it does (a behavior is an Interaction
|
|
2326
2349
|
Concept principle instead) and not what a term means (a definition is a Domain
|
|
2327
2350
|
Glossary term instead).
|
|
2351
|
+
optional: true
|
|
2328
2352
|
format:
|
|
2329
2353
|
type: compact
|
|
2330
2354
|
diagram:
|
|
@@ -2410,6 +2434,7 @@
|
|
|
2410
2434
|
occurring in a single flow is a Dialog Storyboard frame instead) and is bound to a
|
|
2411
2435
|
particular kind of screen or flow (a solution-wide rule independent of any screen
|
|
2412
2436
|
is an Interaction Concept principle instead).
|
|
2437
|
+
optional: true
|
|
2413
2438
|
diagram:
|
|
2414
2439
|
type: hub
|
|
2415
2440
|
center: self
|
|
@@ -2500,6 +2525,7 @@
|
|
|
2500
2525
|
one flow, as seen by its actors (the intent and the information exchanged are a Use
|
|
2501
2526
|
Cases scenario instead, and a structure recurring across flows is a Dialog Pattern
|
|
2502
2527
|
instead).
|
|
2528
|
+
optional: true
|
|
2503
2529
|
diagram:
|
|
2504
2530
|
type: hub
|
|
2505
2531
|
center: self
|
|
@@ -2611,6 +2637,7 @@
|
|
|
2611
2637
|
`MOCKUP` or a Dialog Storyboard frame instead), and is a decision rather than a
|
|
2612
2638
|
list of token values (the values are in the design system) or a measurable bound
|
|
2613
2639
|
(a bound is a Non-Functional Requirement instead).
|
|
2640
|
+
optional: true
|
|
2614
2641
|
format:
|
|
2615
2642
|
type: compact
|
|
2616
2643
|
maxTableColumns: 5
|
|
@@ -2670,6 +2697,7 @@
|
|
|
2670
2697
|
is a reference rendering of the final look of one screen (a low-fidelity sketch of
|
|
2671
2698
|
a screen within its flow is a Dialog Storyboard frame instead) and shows rules
|
|
2672
2699
|
applied rather than stating them (a rule is an `ELEMENT` instead).
|
|
2700
|
+
optional: true
|
|
2673
2701
|
format:
|
|
2674
2702
|
type: complex
|
|
2675
2703
|
diagram:
|
|
@@ -2754,6 +2782,7 @@
|
|
|
2754
2782
|
solution deploys and operates itself (e.g. a managed database of its own) is a
|
|
2755
2783
|
Deployment View node instead -- only a party or system the solution merely consumes
|
|
2756
2784
|
or serves belongs to the system context.
|
|
2785
|
+
optional: true
|
|
2757
2786
|
format:
|
|
2758
2787
|
type: compact
|
|
2759
2788
|
maxTableColumns: 5
|
|
@@ -2872,6 +2901,7 @@
|
|
|
2872
2901
|
otherwise) and which elaborates how the component fulfills it and collaborates
|
|
2873
2902
|
with the components it depends on, plus a 1-sentence rationale ("why") for the
|
|
2874
2903
|
component's existence and boundary.
|
|
2904
|
+
optional: true
|
|
2875
2905
|
format:
|
|
2876
2906
|
type: compact
|
|
2877
2907
|
maxTableColumns: 5
|
|
@@ -2993,6 +3023,7 @@
|
|
|
2993
3023
|
or a shared module used by client and server), so a product
|
|
2994
3024
|
used on several tiers is a single component here instead of
|
|
2995
3025
|
one per tier; referenced by no Deployment View tier.
|
|
3026
|
+
optional: true
|
|
2996
3027
|
diagram:
|
|
2997
3028
|
type: hub
|
|
2998
3029
|
center: self
|
|
@@ -3355,6 +3386,7 @@
|
|
|
3355
3386
|
addressed by a Quality Perspectives tactic referencing the aspect through
|
|
3356
3387
|
`AFFECTS`, too, so the hub diagram of an aspect shows the decision and the tactic
|
|
3357
3388
|
behind it.
|
|
3389
|
+
optional: true
|
|
3358
3390
|
diagram:
|
|
3359
3391
|
type: hub
|
|
3360
3392
|
center: self
|
|
@@ -3415,6 +3447,7 @@
|
|
|
3415
3447
|
Model interface (e.g. of kind `MQTT` or `WebSocket`), and the platform element
|
|
3416
3448
|
hosting a managed broker a Deployment View `Managed` node -- only the executing
|
|
3417
3449
|
structure itself belongs to the Concurrency View.
|
|
3450
|
+
optional: true
|
|
3418
3451
|
diagram:
|
|
3419
3452
|
type: hub
|
|
3420
3453
|
center: self
|
|
@@ -3515,6 +3548,7 @@
|
|
|
3515
3548
|
`TIER`, a location-level tier (e.g. a data center) groups the tiers referencing it
|
|
3516
3549
|
via `PART-OF`, and a single deployment unit within a tier is a `NODE` -- only the
|
|
3517
3550
|
partition itself belongs here.
|
|
3551
|
+
optional: true
|
|
3518
3552
|
format:
|
|
3519
3553
|
type: concise
|
|
3520
3554
|
diagram:
|
|
@@ -3594,6 +3628,7 @@
|
|
|
3594
3628
|
itself belongs to the Deployment View, with the tier and area the node belongs to
|
|
3595
3629
|
and the network segments it is attached to as its `TIER`, `AREA`, and `NETWORK`
|
|
3596
3630
|
siblings.
|
|
3631
|
+
optional: true
|
|
3597
3632
|
diagram:
|
|
3598
3633
|
type: hub
|
|
3599
3634
|
center: self
|
|
@@ -3764,6 +3799,7 @@
|
|
|
3764
3799
|
deployable artifact is a `Build` aspect of the Development Aspects, and the products
|
|
3765
3800
|
used are Technology Stack `Operate-Time` components referenced through `TOOLS` --
|
|
3766
3801
|
only the procedure itself, its trigger, and its responsible operator belong here.
|
|
3802
|
+
optional: true
|
|
3767
3803
|
diagram:
|
|
3768
3804
|
type: hub
|
|
3769
3805
|
center: self
|
|
@@ -3885,6 +3921,7 @@
|
|
|
3885
3921
|
(e.g. a hosting or budget mandate) protects no quality and is a Design Decisions
|
|
3886
3922
|
decision alone -- only the mechanism, the quality it achieves, and the elements it
|
|
3887
3923
|
shapes belong here.
|
|
3924
|
+
optional: true
|
|
3888
3925
|
format:
|
|
3889
3926
|
type: compact
|
|
3890
3927
|
diagram:
|
|
@@ -4046,6 +4083,7 @@
|
|
|
4046
4083
|
decision is under-justified. The rationale should make the decision feel earned,
|
|
4047
4084
|
not asserted - if you delete the rationale and the decision suddenly looks
|
|
4048
4085
|
arbitrary, the rationale was doing its job.
|
|
4086
|
+
optional: true
|
|
4049
4087
|
format:
|
|
4050
4088
|
type: complex
|
|
4051
4089
|
diagram:
|
|
@@ -4253,6 +4291,7 @@
|
|
|
4253
4291
|
`TOOLS`, and the rule about the source layout, build, or process it lives in is a
|
|
4254
4292
|
Development Aspects aspect -- only the recurring code shape, the elements written
|
|
4255
4293
|
that way, and the deviation ruled out belong here.
|
|
4294
|
+
optional: true
|
|
4256
4295
|
format:
|
|
4257
4296
|
type: complex
|
|
4258
4297
|
diagram:
|
|
@@ -4380,6 +4419,7 @@
|
|
|
4380
4419
|
concurrency unit when its collaboration or contention is exercised, an aspect of
|
|
4381
4420
|
the Development Aspects or a Design Decisions decision when its rule is checked by
|
|
4382
4421
|
an architectural fitness function (e.g. no dependency edge against the layering).
|
|
4422
|
+
optional: true
|
|
4383
4423
|
format:
|
|
4384
4424
|
type: compact
|
|
4385
4425
|
diagram:
|
package/dst/specbook-mcp.js
CHANGED
|
@@ -46,14 +46,18 @@ export const serveMcp = async (verbose) => {
|
|
|
46
46
|
server.registerTool("specbook_init", {
|
|
47
47
|
title: "Initialize Specification",
|
|
48
48
|
description: "Initialize the configured specification artifact files below the base directory " +
|
|
49
|
-
"with their frontmatter and artifact heading, skipping already existing files
|
|
49
|
+
"with their frontmatter and artifact heading, skipping already existing files, and " +
|
|
50
|
+
`create the project configuration file "${projectFile}" in the working directory, ` +
|
|
51
|
+
"persisting the effective schema configuration and base directory, unless one already applies.",
|
|
50
52
|
inputSchema: { config, basedir, cwd }
|
|
51
53
|
}, async (args) => {
|
|
52
54
|
try {
|
|
53
|
-
const
|
|
54
|
-
return { content: [{ type: "text", text:
|
|
55
|
-
`initialized artifact file(s): ${
|
|
56
|
-
"no artifact files were created"
|
|
55
|
+
const result = await specbook.init(args);
|
|
56
|
+
return { content: [{ type: "text", text: (result.files.length > 0 ?
|
|
57
|
+
`initialized artifact file(s): ${result.files.join(", ")}` :
|
|
58
|
+
"no artifact files were created") +
|
|
59
|
+
(result.project !== undefined ?
|
|
60
|
+
`\ncreated project configuration file: ${result.project}` : "") }] };
|
|
57
61
|
}
|
|
58
62
|
catch (err) {
|
|
59
63
|
return errorResult(err);
|
package/dst/specbook-project.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
-
import { parseDocument, LineCounter } from "yaml";
|
|
8
|
+
import { parseDocument, stringify, LineCounter } from "yaml";
|
|
9
9
|
import * as v from "valibot";
|
|
10
10
|
import { lineColOfPath } from "./specbook-config.js";
|
|
11
11
|
import { renderDiagnostic } from "./specbook-diagnostic.js";
|
|
@@ -104,3 +104,12 @@ export const loadProject = (dir) => {
|
|
|
104
104
|
basedir: basedir !== undefined ? path.resolve(base, basedir) : undefined
|
|
105
105
|
};
|
|
106
106
|
};
|
|
107
|
+
/* write a new project configuration file into the directory, with the
|
|
108
|
+
schema configuration patterns and the base directory given relative
|
|
109
|
+
to it, never overwriting an existing file */
|
|
110
|
+
export const writeProject = (dir, config, basedir) => {
|
|
111
|
+
const file = path.join(dir, projectFile);
|
|
112
|
+
const text = stringify({ config: config.length === 1 ? config[0] : config, basedir });
|
|
113
|
+
fs.writeFileSync(file, text, { encoding: "utf8", flag: "wx" });
|
|
114
|
+
return file;
|
|
115
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rse/specbook",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"description": "Markdown-based Specification Format (API, CLI and MCP service)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [ "dst" ],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"marked": "18.0
|
|
20
|
+
"marked": "18.1.0",
|
|
21
21
|
"marked-smartypants": "1.1.12",
|
|
22
22
|
"yaml": "2.9.1",
|
|
23
23
|
"json5": "2.2.3",
|
|
@@ -28,18 +28,18 @@
|
|
|
28
28
|
"@rse/mrcs": "0.9.2",
|
|
29
29
|
"@rse/gradia": "1.4.2",
|
|
30
30
|
"beautiful-mermaid": "1.1.3",
|
|
31
|
-
"@
|
|
31
|
+
"@d2lang/d2": "0.1.34",
|
|
32
32
|
"shiki": "4.5.0",
|
|
33
33
|
"pdfjs-dist": "6.4.299",
|
|
34
34
|
"@napi-rs/canvas": "1.0.10",
|
|
35
|
-
"@toon-format/toon": "4.
|
|
35
|
+
"@toon-format/toon": "4.4.0",
|
|
36
36
|
"valibot": "1.5.0",
|
|
37
37
|
"nunjucks": "3.2.4",
|
|
38
38
|
"@rse/nunjucks-addons": "2.2.0",
|
|
39
39
|
"textframe": "1.2.1",
|
|
40
40
|
"@swc/html": "1.16.13",
|
|
41
41
|
"tokenizr": "2.2.1",
|
|
42
|
-
"playwright": "1.
|
|
42
|
+
"playwright": "1.64.0",
|
|
43
43
|
"source-code-error": "1.2.0",
|
|
44
44
|
"commander": "15.0.0",
|
|
45
45
|
"chalk": "6.0.1",
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
"es-toolkit": "1.52.0",
|
|
50
50
|
"fastify": "5.12.5",
|
|
51
51
|
"@fastify/websocket": "11.3.3",
|
|
52
|
-
"@modelcontextprotocol/sdk": "1.32.
|
|
52
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
53
53
|
"zod": "4.6.5"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"chokidar-cli": "3.0.0",
|
|
57
57
|
"eslint": "10.9.0",
|
|
58
58
|
"@eslint/js": "10.0.1",
|
|
59
|
-
"typescript-eslint": "8.71.
|
|
59
|
+
"typescript-eslint": "8.71.1",
|
|
60
60
|
"eslint-plugin-promise": "7.3.0",
|
|
61
61
|
"neostandard": "0.14.0-next.1",
|
|
62
62
|
"globals": "17.13.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"source-sans": "3.52.0",
|
|
74
74
|
"@fontsource/dejavu-mono": "5.3.0",
|
|
75
75
|
"@fontsource/noto-sans-symbols": "5.3.0",
|
|
76
|
-
"postcss": "8.5.
|
|
76
|
+
"postcss": "8.5.29",
|
|
77
77
|
"postcss-cli": "12.0.0",
|
|
78
78
|
"postcss-url": "10.1.4",
|
|
79
79
|
"stylus": "0.64.0",
|