@rse/specbook 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -328
- package/dst/specbook-api.d.ts +13 -20
- package/dst/specbook-api.js +43 -39
- package/dst/specbook-cli.js +42 -55
- package/dst/specbook-cmd-describe.d.ts +5 -2
- package/dst/specbook-cmd-describe.js +30 -91
- package/dst/specbook-cmd-export.d.ts +3 -3
- package/dst/specbook-cmd-export.js +23 -19
- package/dst/specbook-cmd-init.d.ts +3 -3
- package/dst/specbook-cmd-init.js +38 -15
- package/dst/specbook-cmd-lint.d.ts +5 -6
- package/dst/specbook-cmd-lint.js +61 -24
- package/dst/specbook-config.d.ts +2 -2
- package/dst/specbook-config.js +82 -73
- package/dst/specbook-diagnostic.js +4 -1
- package/dst/specbook-diagram.d.ts +6 -5
- package/dst/specbook-diagram.js +94 -104
- package/dst/specbook-export-ast.d.ts +3 -3
- package/dst/specbook-export-ast.js +1 -1
- package/dst/specbook-export-common.d.ts +11 -9
- package/dst/specbook-export-common.js +34 -22
- package/dst/specbook-export-html-search.js +10 -14
- package/dst/specbook-export-html.css +37 -32
- package/dst/specbook-export-html.d.ts +4 -4
- package/dst/specbook-export-html.js +153 -107
- package/dst/specbook-export-logo-dark.svg +35 -0
- package/dst/specbook-export-logo-light.svg +35 -0
- package/dst/specbook-export-md.d.ts +3 -3
- package/dst/specbook-export-md.js +50 -24
- package/dst/specbook-export-pdf.d.ts +4 -2
- package/dst/specbook-export-pdf.js +48 -38
- package/dst/{specbook-struct-schema.d.ts → specbook-format-schema.d.ts} +3 -2
- package/dst/{specbook-struct-schema.js → specbook-format-schema.js} +8 -4
- package/dst/specbook-format-spec.d.ts +31 -0
- package/dst/{specbook-struct-spec.js → specbook-format-spec.js} +10 -10
- package/dst/specbook-format.md +750 -0
- package/dst/specbook-link.d.ts +3 -2
- package/dst/specbook-link.js +7 -4
- package/dst/specbook-mcp.d.ts +2 -1
- package/dst/specbook-mcp.js +30 -62
- package/dst/specbook-parse-common.d.ts +5 -3
- package/dst/specbook-parse-common.js +8 -0
- package/dst/specbook-parse-semantic.d.ts +6 -4
- package/dst/specbook-parse-semantic.js +98 -106
- package/dst/specbook-parse-syntax.d.ts +2 -2
- package/dst/specbook-parse-syntax.js +175 -144
- package/dst/specbook-parse-value.d.ts +1 -1
- package/dst/specbook-parse-value.js +6 -3
- package/dst/specbook-parse.d.ts +4 -7
- package/dst/specbook-parse.js +28 -35
- package/dst/specbook-verbose.d.ts +2 -0
- package/dst/specbook-verbose.js +17 -0
- package/package.json +3 -7
- package/dst/specbook-cmd-edit.d.ts +0 -9
- package/dst/specbook-cmd-edit.js +0 -74
- package/dst/specbook-cmd-import.d.ts +0 -9
- package/dst/specbook-cmd-import.js +0 -78
- package/dst/specbook-export-logo.svg +0 -35
- package/dst/specbook-llm.d.ts +0 -13
- package/dst/specbook-llm.js +0 -51
- package/dst/specbook-struct-spec.d.ts +0 -31
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
|
|
2
|
+
SpecBook Models and Formats
|
|
3
|
+
===========================
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
╭───────────────────╮ ╭───────────────────╮
|
|
7
|
+
│ SpecBook │ │ SpecBook │
|
|
8
|
+
│ SCHEMA │ │ SPEC │
|
|
9
|
+
│ META Model │ │ META Model │
|
|
10
|
+
│ (internal) │ │ (internal) │
|
|
11
|
+
╰─────────┬─────────╯ ╰─────────┬─────────╯
|
|
12
|
+
│ │
|
|
13
|
+
│ │
|
|
14
|
+
╭─────────▼─────────╮ ┏━━━━━━━━━▼━━━━━━━━━┓
|
|
15
|
+
│ SpecBook │ ┃ SpecBook ┃
|
|
16
|
+
│ SCHEMA ├──────▶ SPEC ┃
|
|
17
|
+
│ Model │ ┃ Model ┃
|
|
18
|
+
╰───────────────────╯ ┗━━━━━━━━━━━━━━━━━━━┛
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**SpecBook** deals with four distinct object models and their particular
|
|
22
|
+
format, each represented in a formal language and serving a particular
|
|
23
|
+
task:
|
|
24
|
+
|
|
25
|
+
- **SpecBook SCHEMA Meta Model**:
|
|
26
|
+
This is an object model, described in *TypeScript*, internal
|
|
27
|
+
to **SpecBook**, which defines the format of all **SpecBook**
|
|
28
|
+
*Schemas*. It is generic and applied onto the **SpecBook SCHEMA
|
|
29
|
+
Model** to validate that it is technically well-structured.
|
|
30
|
+
|
|
31
|
+
- **SpecBook SPEC Meta Model**:
|
|
32
|
+
This is an object model, described in *TypeScript*, internal
|
|
33
|
+
to **SpecBook**, which defines the format of all **SpecBook**
|
|
34
|
+
*Specifications*. It is generic and applied onto the **SpecBook SPEC
|
|
35
|
+
Model** to validate that it is technically well-structured.
|
|
36
|
+
|
|
37
|
+
- **SpecBook SCHEMA Model**:
|
|
38
|
+
This is an object model, described in *YAML*, outside of
|
|
39
|
+
**SpecBook**, which defines a particular **SpecBook** *Schema*.
|
|
40
|
+
It is user-supplied and non-generic but reusable and applied onto
|
|
41
|
+
the **SpecBook SPEC Model** to validate that it is domain-wise
|
|
42
|
+
well-structured.
|
|
43
|
+
|
|
44
|
+
- **SpecBook SPEC Model**:
|
|
45
|
+
This is an object model, described in *Markdown*, outside
|
|
46
|
+
of **SpecBook**, which defines a particular **SpecBook**
|
|
47
|
+
*Specification*. It is user-supplied and non-generic and
|
|
48
|
+
non-reusable and is the standalone specification of a particular
|
|
49
|
+
project.
|
|
50
|
+
|
|
51
|
+
SpecBook SCHEMA Meta Model
|
|
52
|
+
--------------------------
|
|
53
|
+
|
|
54
|
+
### Structure
|
|
55
|
+
|
|
56
|
+
This meta model defines which domain-specific objects are allowed in
|
|
57
|
+
a *Schema*: the object kinds with their `file` mapping on the first
|
|
58
|
+
level, the object kinds of the deeper levels, and their properties
|
|
59
|
+
with optional value constraints: regular expressions `/xxx/` (implicitly
|
|
60
|
+
anchored, so they have to match the value as a whole),
|
|
61
|
+
object references `[[xxx]]`, enumerations `enum(xxx,yyy)`, tag sets
|
|
62
|
+
`tags(xxx,yyy)`, and lists `list(xxx[, ...])` of such alternatives.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
type Schema = SchemaObject[]
|
|
66
|
+
type SchemaObject = {
|
|
67
|
+
kind: string
|
|
68
|
+
name?: string
|
|
69
|
+
id?: string
|
|
70
|
+
file?: string
|
|
71
|
+
desc?: string
|
|
72
|
+
optional?: boolean
|
|
73
|
+
diagram?: SchemaDiagram
|
|
74
|
+
format?: SchemaFormat
|
|
75
|
+
props?: SchemaProperty[]
|
|
76
|
+
childs?: SchemaObject[]
|
|
77
|
+
}
|
|
78
|
+
type SchemaDiagram = {
|
|
79
|
+
type?: "graph" | "hub" | "grid"
|
|
80
|
+
nodes?: string
|
|
81
|
+
edges?: string
|
|
82
|
+
center?: string
|
|
83
|
+
links?: "props" | "all"
|
|
84
|
+
edgeSource?: string
|
|
85
|
+
edgeTarget?: string
|
|
86
|
+
edgeArity?: string
|
|
87
|
+
hierarchy?: boolean
|
|
88
|
+
onlyConnected?: boolean
|
|
89
|
+
collapse?: boolean
|
|
90
|
+
qualified?: boolean
|
|
91
|
+
properties?: string[]
|
|
92
|
+
config?: Partial<GradiaConfig>
|
|
93
|
+
}
|
|
94
|
+
type SchemaFormat = {
|
|
95
|
+
type?: "auto" | "complex" | "concise"
|
|
96
|
+
maxTableColumns?: number
|
|
97
|
+
withUnusedProps?: boolean
|
|
98
|
+
}
|
|
99
|
+
type SchemaProperty = {
|
|
100
|
+
name: string
|
|
101
|
+
desc?: string
|
|
102
|
+
value?: string
|
|
103
|
+
optional?: boolean
|
|
104
|
+
}
|
|
105
|
+
type SchemaGradiaConfig = Partial<{
|
|
106
|
+
"font-family": string
|
|
107
|
+
"font-embed": boolean
|
|
108
|
+
"color-node-regular-name": string
|
|
109
|
+
"color-node-regular-box": string
|
|
110
|
+
"color-node-regular-border": string
|
|
111
|
+
"color-node-primary-name": string
|
|
112
|
+
"color-node-primary-box": string
|
|
113
|
+
"color-node-primary-border": string
|
|
114
|
+
"color-node-ghost-name": string
|
|
115
|
+
"color-node-ghost-box": string
|
|
116
|
+
"color-node-ghost-border": string
|
|
117
|
+
"color-group-name": string
|
|
118
|
+
"color-group-box": string
|
|
119
|
+
"color-group-border": string
|
|
120
|
+
"color-edge-line": string
|
|
121
|
+
"color-edge-name": string
|
|
122
|
+
"color-edge-arity": string
|
|
123
|
+
"color-edge-halo": string
|
|
124
|
+
"size-canvas-margin": number
|
|
125
|
+
"size-node-width-min": number
|
|
126
|
+
"size-node-width-max": number
|
|
127
|
+
"size-node-height-scale": number
|
|
128
|
+
"size-edge-corner-radius": number
|
|
129
|
+
"size-edge-hop-radius": number
|
|
130
|
+
"size-edge-track-gap": number
|
|
131
|
+
"size-edge-port-gap": number
|
|
132
|
+
"group-box-padding": number
|
|
133
|
+
"group-box-gap": number
|
|
134
|
+
"graph-columns-max": number
|
|
135
|
+
"graph-channel-width-max": number
|
|
136
|
+
"graph-channel-width-min": number
|
|
137
|
+
"graph-gutter-height-max": number
|
|
138
|
+
"graph-gutter-height-min": number
|
|
139
|
+
"graph-node-separation": number
|
|
140
|
+
"graph-rank-separation": number
|
|
141
|
+
"graph-node-degree-max": number
|
|
142
|
+
"hub-channel-width-max": number
|
|
143
|
+
"hub-channel-width-min": number
|
|
144
|
+
"hub-node-gap": number
|
|
145
|
+
"hub-node-degree-max": number
|
|
146
|
+
"grid-columns-max": number
|
|
147
|
+
"grid-columns-min": number
|
|
148
|
+
"grid-gap-horizontal": number
|
|
149
|
+
"grid-gap-vertical": number
|
|
150
|
+
"grid-node-width-equal": boolean
|
|
151
|
+
}>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- `Schema`:
|
|
155
|
+
whole schema: the object kinds the corpus consists of,
|
|
156
|
+
BECAUSE this configures SpecBook for a particular context
|
|
157
|
+
|
|
158
|
+
- `SchemaObject`:
|
|
159
|
+
object kind: an artifact (level 1) or an object nested below it,
|
|
160
|
+
BECAUSE artifacts and their content follow one recursive structure
|
|
161
|
+
|
|
162
|
+
- `SchemaObject.kind: string`:
|
|
163
|
+
kind name of the object (e.g. `SPEC`, `REQUIREMENT`),
|
|
164
|
+
BECAUSE it discriminates objects and anchors `[[xxx]]` references
|
|
165
|
+
|
|
166
|
+
- `SchemaObject.name?: string`:
|
|
167
|
+
exact artifact name (level 1) or name regex (deeper levels),
|
|
168
|
+
BECAUSE artifacts are fixed documents, deeper names need a rule only
|
|
169
|
+
|
|
170
|
+
- `SchemaObject.id?: string`:
|
|
171
|
+
short locally-unique identifier the object explicitly carries in its heading,
|
|
172
|
+
BECAUSE stable ids keep `[[xxx]]` references free of prose names
|
|
173
|
+
|
|
174
|
+
- `SchemaObject.file?: string`:
|
|
175
|
+
Markdown file the level 1 artifact is generated into and resides in,
|
|
176
|
+
optionally shared with the other artifacts carrying the same file,
|
|
177
|
+
BECAUSE artifacts must map deterministically onto files
|
|
178
|
+
|
|
179
|
+
- `SchemaObject.desc?: string`:
|
|
180
|
+
prose description of the object kind,
|
|
181
|
+
BECAUSE it guides the authors and instructs the AI
|
|
182
|
+
|
|
183
|
+
- `SchemaObject.optional?: boolean`:
|
|
184
|
+
whether the kind may be absent below its parent or, for a level 1
|
|
185
|
+
artifact, from its file (default: `false`),
|
|
186
|
+
BECAUSE not every kind is mandatory below every parent object
|
|
187
|
+
|
|
188
|
+
- `SchemaObject.diagram?: SchemaDiagram`:
|
|
189
|
+
diagram generated for every object of this kind,
|
|
190
|
+
BECAUSE relations buried in references are graspable only when drawn
|
|
191
|
+
|
|
192
|
+
- `SchemaObject.format?: SchemaFormat`:
|
|
193
|
+
HTML/PDF rendering format of the child objects of this kind,
|
|
194
|
+
BECAUSE rich objects need sections, uniform ones read as tables
|
|
195
|
+
|
|
196
|
+
- `SchemaObject.props?: SchemaProperty[]`:
|
|
197
|
+
properties allowed on objects of this kind,
|
|
198
|
+
BECAUSE they carry the structured, machine-checkable content
|
|
199
|
+
|
|
200
|
+
- `SchemaObject.childs?: SchemaObject[]`:
|
|
201
|
+
object kinds allowed one level below this object (RECURSION),
|
|
202
|
+
BECAUSE the nesting defines and bounds the document structure
|
|
203
|
+
|
|
204
|
+
- `SchemaDiagram`:
|
|
205
|
+
diagram derived for every object of an object kind,
|
|
206
|
+
BECAUSE the relations of a specification are graspable only when drawn
|
|
207
|
+
|
|
208
|
+
- `SchemaDiagram.type?: "graph" | "hub" | "grid"`:
|
|
209
|
+
diagram shape: `graph` (free), `hub` (centered), `grid` (edge-less),
|
|
210
|
+
BECAUSE one object set tells different stories as different shapes
|
|
211
|
+
|
|
212
|
+
- `SchemaDiagram.nodes?: string`:
|
|
213
|
+
comma-separated `[[xxx]]` patterns of nodes (default: self and descendants),
|
|
214
|
+
BECAUSE a diagram is readable only when reduced to relevant objects
|
|
215
|
+
|
|
216
|
+
- `SchemaDiagram.edges?: string`:
|
|
217
|
+
comma-separated `[[xxx]]` patterns of the objects acting as edges, not nodes,
|
|
218
|
+
BECAUSE some relations are objects, yet still connect two nodes
|
|
219
|
+
|
|
220
|
+
- `SchemaDiagram.center?: string`:
|
|
221
|
+
object a `hub` diagram is projected onto (default: `self`),
|
|
222
|
+
BECAUSE a hub needs one focus, not always the owning object
|
|
223
|
+
|
|
224
|
+
- `SchemaDiagram.links?: "props" | "all"`:
|
|
225
|
+
edge source: property values (`props`) or also texts (`all`),
|
|
226
|
+
BECAUSE prose references are incidental and need an opt-in
|
|
227
|
+
|
|
228
|
+
- `SchemaDiagram.edgeSource?: string`:
|
|
229
|
+
edge property naming the source node (default: the parent object),
|
|
230
|
+
BECAUSE an edge object is not always nested below its own source
|
|
231
|
+
|
|
232
|
+
- `SchemaDiagram.edgeTarget?: string`:
|
|
233
|
+
edge property naming the target node (default: the first non-source
|
|
234
|
+
property whose value is a single reference),
|
|
235
|
+
BECAUSE the convention breaks on multiple references per object
|
|
236
|
+
|
|
237
|
+
- `SchemaDiagram.edgeArity?: string`:
|
|
238
|
+
edge property carrying the edge arity (default: `ARITY`),
|
|
239
|
+
BECAUSE cardinality labels are named domain-specifically
|
|
240
|
+
|
|
241
|
+
- `SchemaDiagram.hierarchy?: boolean`:
|
|
242
|
+
whether containment edges derive from the nesting (default: `false`),
|
|
243
|
+
BECAUSE the nesting carries no reference and would stay invisible
|
|
244
|
+
|
|
245
|
+
- `SchemaDiagram.onlyConnected?: boolean`:
|
|
246
|
+
whether a `graph` drops its edge-less nodes (default: `false`),
|
|
247
|
+
BECAUSE isolated nodes dilute a diagram about relationships
|
|
248
|
+
|
|
249
|
+
- `SchemaDiagram.collapse?: boolean`:
|
|
250
|
+
whether a single-node diagram is omitted (default: `true`),
|
|
251
|
+
BECAUSE it carries no information but still consumes space
|
|
252
|
+
|
|
253
|
+
- `SchemaDiagram.qualified?: boolean`:
|
|
254
|
+
whether every node is labeled with its kind (default: `false`),
|
|
255
|
+
BECAUSE names alone are ambiguous across multiple object kinds
|
|
256
|
+
|
|
257
|
+
- `SchemaDiagram.properties?: string[]`:
|
|
258
|
+
property names attached to the nodes as key/value annotations,
|
|
259
|
+
BECAUSE the node name alone rarely makes the nodes comparable
|
|
260
|
+
|
|
261
|
+
- `SchemaDiagram.config?: SchemaGradiaConfig`:
|
|
262
|
+
arbitrary Gradia rendering options (e.g. `grid-columns-max: 5`),
|
|
263
|
+
BECAUSE layout tuning is cosmetics, not a schema concern
|
|
264
|
+
|
|
265
|
+
- `SchemaFormat`:
|
|
266
|
+
HTML/PDF rendering of the child objects of an object kind,
|
|
267
|
+
BECAUSE one uniform rendering fits neither rich nor numerous objects
|
|
268
|
+
|
|
269
|
+
- `SchemaFormat.type?: "auto" | "complex" | "concise"`:
|
|
270
|
+
rendering: `complex` (sections), `concise` (tables), or `auto`,
|
|
271
|
+
BECAUSE sections bury small objects, tables truncate rich ones
|
|
272
|
+
|
|
273
|
+
- `SchemaFormat.maxTableColumns?: number`:
|
|
274
|
+
maximum column count of the tables below (default: `4`),
|
|
275
|
+
BECAUSE the fixed print/PDF page width bounds the table width
|
|
276
|
+
|
|
277
|
+
- `SchemaFormat.withUnusedProps?: boolean`:
|
|
278
|
+
whether unused properties still render (default: `false`),
|
|
279
|
+
BECAUSE empty entries are noise, unless the gap is the message
|
|
280
|
+
|
|
281
|
+
- `SchemaProperty`:
|
|
282
|
+
property allowed on the objects of an object kind,
|
|
283
|
+
BECAUSE properties carry the structured content beside the prose
|
|
284
|
+
|
|
285
|
+
- `SchemaProperty.name: string`:
|
|
286
|
+
property key as it occurs in the Markdown content,
|
|
287
|
+
BECAUSE the key binds the authored content to its constraint
|
|
288
|
+
|
|
289
|
+
- `SchemaProperty.desc?: string`:
|
|
290
|
+
prose description of the property,
|
|
291
|
+
BECAUSE it guides the authors and instructs the AI
|
|
292
|
+
|
|
293
|
+
- `SchemaProperty.value?: string`:
|
|
294
|
+
constraint: regexp `/xxx/`, link `[[xxx]]`, `enum(...)`, `tags(...)`, `list(...)`,
|
|
295
|
+
BECAUSE constrained values stay checkable and resolvable
|
|
296
|
+
|
|
297
|
+
- `SchemaProperty.optional?: boolean`:
|
|
298
|
+
whether the property may be missing (default: `false`),
|
|
299
|
+
BECAUSE not every property applies to every object
|
|
300
|
+
|
|
301
|
+
- `SchemaGradiaConfig`:
|
|
302
|
+
the Gradia rendering options, mirrored from `@rse/gradia`,
|
|
303
|
+
BECAUSE the diagram cosmetics remain Gradia's concern, not SpecBook's
|
|
304
|
+
|
|
305
|
+
- `SchemaGradiaConfig."font-*": string | boolean`:
|
|
306
|
+
font of all diagram texts and whether it is embedded into the SVG,
|
|
307
|
+
BECAUSE an embedded font renders alike where it is not installed
|
|
308
|
+
|
|
309
|
+
- `SchemaGradiaConfig."color-node-*": string`:
|
|
310
|
+
colors of the node boxes in their regular, primary, and ghost role,
|
|
311
|
+
BECAUSE the role of a node has to be readable at a glance
|
|
312
|
+
|
|
313
|
+
- `SchemaGradiaConfig."color-group-*", "color-edge-*": string`:
|
|
314
|
+
colors of the group boxes and of the edges and their labels,
|
|
315
|
+
BECAUSE both have to stay visible yet recede behind the nodes
|
|
316
|
+
|
|
317
|
+
- `SchemaGradiaConfig."size-*", "group-box-*": number`:
|
|
318
|
+
geometry of the canvas, the node boxes, and the edge routing,
|
|
319
|
+
BECAUSE the geometry has to fit both the page and the text lengths
|
|
320
|
+
|
|
321
|
+
- `SchemaGradiaConfig."graph-*": number`:
|
|
322
|
+
layout of a `graph`: columns, channel and gutter bounds, and separations,
|
|
323
|
+
BECAUSE a free network needs room for its edges, not just its nodes
|
|
324
|
+
|
|
325
|
+
- `SchemaGradiaConfig."hub-*": number`:
|
|
326
|
+
layout of a `hub`: channel widths, node gap, and node degree,
|
|
327
|
+
BECAUSE a hub grows sideways and has to be bounded per side
|
|
328
|
+
|
|
329
|
+
- `SchemaGradiaConfig."grid-*": number | boolean`:
|
|
330
|
+
layout of a `grid`: column bounds, tile gaps, and tile width,
|
|
331
|
+
BECAUSE a catalog reads best as evenly sized tiles in stable columns
|
|
332
|
+
|
|
333
|
+
### Links
|
|
334
|
+
|
|
335
|
+
The edges are derived from the `[[xxx]]` references of the node objects
|
|
336
|
+
-- from their property values only, or, with `links: all`, from their
|
|
337
|
+
descriptions as well.
|
|
338
|
+
|
|
339
|
+
An object acting as an edge (`edges`) connects its parent object to the
|
|
340
|
+
object its target property references. Where an edge object is a sibling
|
|
341
|
+
of its own source instead of a child of it, `edgeSource` names the
|
|
342
|
+
property carrying that source.
|
|
343
|
+
|
|
344
|
+
As the *nesting* of the objects carries no such reference, `hierarchy:
|
|
345
|
+
true` additionally derives a containment edge from every node object to
|
|
346
|
+
each of its child objects which is part of the node set.
|
|
347
|
+
|
|
348
|
+
This is what turns a parent object into the hub of its own children. A
|
|
349
|
+
`grid` is edge-less by definition and hence accepts neither `edges` nor
|
|
350
|
+
`hierarchy`.
|
|
351
|
+
|
|
352
|
+
### Diagrams
|
|
353
|
+
|
|
354
|
+
An object kind can additionally carry a `diagram` field, which derives
|
|
355
|
+
a [Gradia](https://github.com/rse/gradia) diagram for every object of
|
|
356
|
+
that kind. Its `type` selects the diagram shape (`graph`, `hub`, or
|
|
357
|
+
`grid`), `nodes` and `edges` select the participating objects through
|
|
358
|
+
comma-separated `[[xxx]]` reference patterns (`nodes` defaults to the
|
|
359
|
+
object itself plus all objects below it), and `center` names the object
|
|
360
|
+
a `hub` is projected onto (default: `self`).
|
|
361
|
+
|
|
362
|
+
`onlyConnected` drops the edge-less nodes of a `graph`, `collapse`
|
|
363
|
+
(default: `true`) omits a degenerated diagram consisting of a single
|
|
364
|
+
node only, `qualified` labels every node with its object kind,
|
|
365
|
+
`properties` lists the property names whose values are attached to the
|
|
366
|
+
nodes as key/value annotations (with every `[[xxx]]` reference stripped
|
|
367
|
+
to its target object name), and `config` passes arbitrary Gradia
|
|
368
|
+
rendering options (e.g. `grid-columns-max: 5`) through to the diagram.
|
|
369
|
+
|
|
370
|
+
### Format
|
|
371
|
+
|
|
372
|
+
An object kind can furthermore carry a `format` field, which controls
|
|
373
|
+
how the HTML/PDF export renders the child objects of every object of
|
|
374
|
+
that kind. Its `type` selects the rendering: `complex` (nested
|
|
375
|
+
sections), `concise` (compact per-kind tables), or the default `auto`,
|
|
376
|
+
which collapses only the deepest level into tables.
|
|
377
|
+
|
|
378
|
+
Below a `concise` object, unconfigured children implicitly stay
|
|
379
|
+
`concise` and render as sub-tables inside the description cells, while
|
|
380
|
+
an explicitly configured `type` is always honored, even a `complex`
|
|
381
|
+
rendering pressed into a cell. `maxTableColumns` (default: `4`) bounds
|
|
382
|
+
the columns of the compact tables below the object (wider groups chunk
|
|
383
|
+
their properties into embedded per-object tables), and `withUnusedProps`
|
|
384
|
+
(default: `false`) unconditionally renders the defined but unused
|
|
385
|
+
properties of that kind, as empty property table lines or table columns.
|
|
386
|
+
Objects collapsed into tables carry no headings and hence leave the
|
|
387
|
+
table of contents and the PDF outline.
|
|
388
|
+
|
|
389
|
+
SpecBook SPEC Meta Model
|
|
390
|
+
------------------------
|
|
391
|
+
|
|
392
|
+
### Structure
|
|
393
|
+
|
|
394
|
+
This meta model defines which domain-specific objects are allowed in a
|
|
395
|
+
*Specification*.
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
type Spec = {
|
|
399
|
+
artifacts: SpecArtifact[]
|
|
400
|
+
}
|
|
401
|
+
type SpecArtifact = {
|
|
402
|
+
created: Date
|
|
403
|
+
modified: Date
|
|
404
|
+
objects: SpecObject[]
|
|
405
|
+
}
|
|
406
|
+
type SpecObject = {
|
|
407
|
+
kind: string
|
|
408
|
+
id: string
|
|
409
|
+
anchor?: string
|
|
410
|
+
paren?: string
|
|
411
|
+
name: string
|
|
412
|
+
primary?: boolean
|
|
413
|
+
description?: SpecDescription
|
|
414
|
+
properties: SpecProperty[]
|
|
415
|
+
childs: SpecObject[]
|
|
416
|
+
}
|
|
417
|
+
type SpecDescription = {
|
|
418
|
+
description: string
|
|
419
|
+
rationale?: string
|
|
420
|
+
embedding?: string[]
|
|
421
|
+
}
|
|
422
|
+
type SpecProperty = {
|
|
423
|
+
key: string
|
|
424
|
+
value: string
|
|
425
|
+
embedding?: string[]
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
- `Spec`:
|
|
430
|
+
whole specification: the artifacts the corpus consists of,
|
|
431
|
+
BECAUSE a specification is one corpus of separately authored files
|
|
432
|
+
|
|
433
|
+
- `Spec.artifacts: SpecArtifact[]`:
|
|
434
|
+
all artifacts parsed from the Markdown files the SCHEMA references,
|
|
435
|
+
BECAUSE the corpus is parsed as a whole to resolve references
|
|
436
|
+
|
|
437
|
+
- `SpecArtifact`:
|
|
438
|
+
single artifact: one level 1 heading plus its file timestamps,
|
|
439
|
+
BECAUSE artifacts are the unit of authoring, review, and generation
|
|
440
|
+
|
|
441
|
+
- `SpecArtifact.created: Date`:
|
|
442
|
+
creation timestamp from the `Created:` "frontmatter" field,
|
|
443
|
+
BECAUSE an origin has to survive Git checkouts and file copies
|
|
444
|
+
|
|
445
|
+
- `SpecArtifact.modified: Date`:
|
|
446
|
+
last modification timestamp from the `Modified:` "frontmatter" field,
|
|
447
|
+
BECAUSE freshness steers review and reconciliation of an artifact
|
|
448
|
+
|
|
449
|
+
- `SpecArtifact.objects: SpecObject[]`:
|
|
450
|
+
root objects of the artifact, opened by its level 1 heading,
|
|
451
|
+
BECAUSE an artifact is the tree below exactly one root object
|
|
452
|
+
|
|
453
|
+
- `SpecObject`:
|
|
454
|
+
object of the artifact tree: the atom of a specification,
|
|
455
|
+
BECAUSE every statement needs an addressable, kind-typed carrier
|
|
456
|
+
|
|
457
|
+
- `SpecObject.kind: string`:
|
|
458
|
+
kind name of the object (e.g. `ENTITY`), case-sensitive,
|
|
459
|
+
BECAUSE the kind decides which schema rules apply to the object
|
|
460
|
+
|
|
461
|
+
- `SpecObject.id: string`:
|
|
462
|
+
locally-unique anchor id, explicitly given or "slugified" from the name,
|
|
463
|
+
BECAUSE every object has to be linkable by a stable handle
|
|
464
|
+
|
|
465
|
+
- `SpecObject.anchor?: string`:
|
|
466
|
+
explicit `{{xxx}}` anchor given in the heading, if any,
|
|
467
|
+
BECAUSE an explicit anchor keeps ids stable when names change
|
|
468
|
+
|
|
469
|
+
- `SpecObject.paren?: string`:
|
|
470
|
+
parenthesized `(xxx)` token trailing the name, if any,
|
|
471
|
+
BECAUSE one token serves as artifact id, property value, or anchor
|
|
472
|
+
|
|
473
|
+
- `SpecObject.name: string`:
|
|
474
|
+
name of the object, as written in its heading or list item,
|
|
475
|
+
BECAUSE the name is the human handle and a reference target
|
|
476
|
+
|
|
477
|
+
- `SpecObject.primary?: boolean`:
|
|
478
|
+
whether the `(*)` marker flags the object as primary,
|
|
479
|
+
BECAUSE some kinds have a distinguished member among their peers
|
|
480
|
+
|
|
481
|
+
- `SpecObject.description?: SpecDescription`:
|
|
482
|
+
description statement of the object, with its rationale,
|
|
483
|
+
BECAUSE the WHAT alone leaves the WHY of an object unrecorded
|
|
484
|
+
|
|
485
|
+
- `SpecObject.properties: SpecProperty[]`:
|
|
486
|
+
key/value properties of the object, in canonical order,
|
|
487
|
+
BECAUSE properties carry the structured, machine-checkable content
|
|
488
|
+
|
|
489
|
+
- `SpecObject.childs: SpecObject[]`:
|
|
490
|
+
child objects nested one level below this object (RECURSION),
|
|
491
|
+
BECAUSE a specification is a tree, not a flat list of statements
|
|
492
|
+
|
|
493
|
+
- `SpecDescription`:
|
|
494
|
+
description of an object: statement, rationale, and images,
|
|
495
|
+
BECAUSE the WHAT and the WHY are stored apart to be rendered apart
|
|
496
|
+
|
|
497
|
+
- `SpecDescription.description: string`:
|
|
498
|
+
statement text of the description (the WHAT),
|
|
499
|
+
BECAUSE the WHAT is the substance every object has to carry
|
|
500
|
+
|
|
501
|
+
- `SpecDescription.rationale?: string`:
|
|
502
|
+
rationale text behind the `, BECAUSE ` split (the WHY),
|
|
503
|
+
BECAUSE a statement without its WHY cannot be judged or revised
|
|
504
|
+
|
|
505
|
+
- `SpecDescription.embedding?: string[]`:
|
|
506
|
+
image files embedded via ``, inlined at parse time,
|
|
507
|
+
one entry per file in markup order (empty for an unreadable file),
|
|
508
|
+
BECAUSE the exports have to stand alone, without the image files
|
|
509
|
+
|
|
510
|
+
- `SpecProperty`:
|
|
511
|
+
key/value property attached to an object,
|
|
512
|
+
BECAUSE the structured facts of an object belong beside its prose
|
|
513
|
+
|
|
514
|
+
- `SpecProperty.key: string`:
|
|
515
|
+
property key, as written in the Markdown content,
|
|
516
|
+
BECAUSE the key selects the schema rule the value is checked by
|
|
517
|
+
|
|
518
|
+
- `SpecProperty.value: string`:
|
|
519
|
+
property value, joined from its (possibly wrapped) lines,
|
|
520
|
+
BECAUSE constraints and references are checked against this text
|
|
521
|
+
|
|
522
|
+
- `SpecProperty.embedding?: string[]`:
|
|
523
|
+
image files embedded in the value, inlined at parse time,
|
|
524
|
+
one entry per file in markup order (empty for an unreadable file),
|
|
525
|
+
BECAUSE the exports have to stand alone, without the image files
|
|
526
|
+
|
|
527
|
+
SpecBook SCHEMA Model
|
|
528
|
+
---------------------
|
|
529
|
+
|
|
530
|
+
### Artifacts
|
|
531
|
+
|
|
532
|
+
A schema consists of a YAML file, which is exactly a serialization of
|
|
533
|
+
the object model defined by the **SpecBook SCHEMA Meta Model**.
|
|
534
|
+
|
|
535
|
+
SpecBook SPEC Model
|
|
536
|
+
-------------------
|
|
537
|
+
|
|
538
|
+
### Artifacts
|
|
539
|
+
|
|
540
|
+
A specification consists of Markdown *artifact* files, which the SCHEMA
|
|
541
|
+
Model references by their `file` fields and which are resolved against
|
|
542
|
+
a base directory. Every file has to start with
|
|
543
|
+
a "frontmatter" block carrying the `Created:` and `Modified:` timestamps
|
|
544
|
+
(format `yyyy-LL-dd HH:mm`):
|
|
545
|
+
|
|
546
|
+
```
|
|
547
|
+
---
|
|
548
|
+
Created: 2026-06-18 10:18
|
|
549
|
+
Modified: 2026-06-18 10:18
|
|
550
|
+
---
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Every level 1 heading opens a new *artifact*, so usually each file
|
|
554
|
+
carries exactly one. Several artifacts nevertheless reside in one file
|
|
555
|
+
whenever the schema configures the same `file` for them: they then
|
|
556
|
+
follow each other on level 1, below the single frontmatter block of that
|
|
557
|
+
file. Both ATX headings (`# <text/>`, `## <text/>`, ...) and Setext
|
|
558
|
+
headings (`<text/>` underlined with `===` or `---`) are supported.
|
|
559
|
+
|
|
560
|
+
### Object Model
|
|
561
|
+
|
|
562
|
+
An artifact carries a tree of *objects*. Every object has a mandatory
|
|
563
|
+
*kind* (e.g. `ENTITY`), a mandatory *name*, an anchor *id* (explicitly
|
|
564
|
+
given or derived from the name, and unique among the sibling objects of
|
|
565
|
+
its kind), an optional *primary* marker, optional *properties* (key/value
|
|
566
|
+
pairs), an optional *description* statement with an optional rationale,
|
|
567
|
+
and optional *child* objects. Which kinds, properties, and nestings are
|
|
568
|
+
allowed is defined by the schema.
|
|
569
|
+
|
|
570
|
+
### Format Variant: Complex Format
|
|
571
|
+
|
|
572
|
+
This Markdown format to represent objects is usually used on object
|
|
573
|
+
hierarchy levels 1-3 (`#`, `##`, `###`). A heading opens the object,
|
|
574
|
+
an optional unordered list carries its properties, and the remaining
|
|
575
|
+
content up to the next heading is its description:
|
|
576
|
+
|
|
577
|
+
```
|
|
578
|
+
# <kind/>: <name/>
|
|
579
|
+
|
|
580
|
+
- <key/>: <value/>
|
|
581
|
+
- [...]
|
|
582
|
+
|
|
583
|
+
<statement/>, BECAUSE <rationale/>.
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
The heading level directly reflects the nesting level: a `##` object
|
|
587
|
+
becomes a child of the preceding `#` object, a `###` object a child of
|
|
588
|
+
the preceding `##` object, etc. Skipping a level (a heading without a
|
|
589
|
+
parent object on the level above) is an error.
|
|
590
|
+
|
|
591
|
+
### Format Variant: Concise Format
|
|
592
|
+
|
|
593
|
+
This Markdown format to represent objects is usually used on level 4
|
|
594
|
+
and deeper. A single unordered list item carries the entire object as
|
|
595
|
+
`;`-separated segments -- the `<kind/>: <name/>` head, the `<key/>:
|
|
596
|
+
<value/>` properties, and the trailing description -- and may wrap over
|
|
597
|
+
multiple (indented) lines:
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
- <kind/>: <name/>; <key/>: <value/>; [...];
|
|
601
|
+
<statement/>, BECAUSE <rationale/>.
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
A list item is recognized as a Concise Format object (instead of a
|
|
605
|
+
property of the enclosing object) by carrying at least one `;` on its
|
|
606
|
+
first line. Every segment of the form `<key/>: <value/>` (with a
|
|
607
|
+
possibly empty value) becomes a property, unless it contains a
|
|
608
|
+
` BECAUSE ` or is the last segment of a `.`-terminated item (as a
|
|
609
|
+
description like `Note: xxx` has to survive the round-trip through
|
|
610
|
+
exported Markdown); all remaining segments are joined into the
|
|
611
|
+
description. Child objects nest as indented list items below their
|
|
612
|
+
parent item, where key/value items become properties and concise items
|
|
613
|
+
become child objects of that parent:
|
|
614
|
+
|
|
615
|
+
```
|
|
616
|
+
- <kind/>: <name/>; <key/>: <value/>;
|
|
617
|
+
<statement/>, BECAUSE <rationale/>.
|
|
618
|
+
|
|
619
|
+
- <kind/>: <name/>; <key/>: <value/>;
|
|
620
|
+
<statement/>, BECAUSE <rationale/>.
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Format Variant: Grouped Format
|
|
624
|
+
|
|
625
|
+
This Markdown format to represent objects is a special variant of the
|
|
626
|
+
Concise Format. Here, a heading carrying just a *kind* and no `<kind/>:
|
|
627
|
+
<name/>` pair opens a *grouping container* instead of an object. The
|
|
628
|
+
list items below it are Concise Format items whose kind comes from that
|
|
629
|
+
heading, so they start with the (optionally backquoted) name directly:
|
|
630
|
+
|
|
631
|
+
```
|
|
632
|
+
### <kind/>
|
|
633
|
+
|
|
634
|
+
- `<name/>`; <key/>: <value/>; [...];
|
|
635
|
+
<statement/>, BECAUSE <rationale/>.
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
The heading kind is taken literally, so `### STATE` groups objects of
|
|
639
|
+
kind `STATE`. The grouped objects become children of the object the
|
|
640
|
+
heading is nested under. An unquoted name must not contain a `:` (else
|
|
641
|
+
the item would look like a key/value pair) -- backquote it in this case.
|
|
642
|
+
|
|
643
|
+
### Format Usage
|
|
644
|
+
|
|
645
|
+
When **SpecBook** itself generates specification Markdown, it emits
|
|
646
|
+
the Complex Format on levels 1-3 and the Concise Format from level 4
|
|
647
|
+
upwards -- unless a descendant of an object carries a multi-line
|
|
648
|
+
description (a fenced code block, an ordered list, a blockquote, or
|
|
649
|
+
multiple paragraphs), which no single list item can carry: all child
|
|
650
|
+
objects of such an object then stay in the Complex Format (as sibling
|
|
651
|
+
objects have to share the format), with headings reaching down to level
|
|
652
|
+
6. When **SpecBook** edits existing files, it mirrors the format each
|
|
653
|
+
existing object already uses.
|
|
654
|
+
|
|
655
|
+
### Names, Anchors, and Ids
|
|
656
|
+
|
|
657
|
+
In all formats, the `<name/>` may carry trailing decorations, in any
|
|
658
|
+
order:
|
|
659
|
+
|
|
660
|
+
- `{{<id/>}}`: the explicit Wiki-style anchor, setting the object id
|
|
661
|
+
(e.g. `## ENTITY: Attendee Browser {{attendee}}`).
|
|
662
|
+
|
|
663
|
+
- `(<token/>)`: a parenthesized token with three possible roles: on
|
|
664
|
+
level 1 it becomes the artifact id (e.g. `# SPEC: Data Model (DM)`);
|
|
665
|
+
on deeper levels it acts as the implicit anchor id.
|
|
666
|
+
|
|
667
|
+
- `(*)`: the *primary* marker, flagging the object as primary (e.g.
|
|
668
|
+
the primary attributes of an entity).
|
|
669
|
+
|
|
670
|
+
When no explicit id is given, the id is derived by "slugifying" the name
|
|
671
|
+
(lowercased, with non-alphanumeric character runs dashed). An explicit
|
|
672
|
+
`{{<id/>}}` anchor always takes precedence over a `(<token/>)` id. When
|
|
673
|
+
the schema configures a fixed `id` for an object, it has to be
|
|
674
|
+
explicitly written in the input, via either `{{<id/>}}` or `(<id/>)`.
|
|
675
|
+
|
|
676
|
+
### Properties
|
|
677
|
+
|
|
678
|
+
In the Complex Format, each property is an unordered list item of the
|
|
679
|
+
form `<key/>: <value/>`, whose value may continue on the following
|
|
680
|
+
indented lines of the item (joined with spaces). In the Concise Format,
|
|
681
|
+
each property is a `<key/>: <value/>` segment. A value carrying a `;`
|
|
682
|
+
cannot be a segment and has to start on a continuation line of a
|
|
683
|
+
key/value item, as a `;` on the first line of a list item marks a
|
|
684
|
+
Concise Format object. The key is matched
|
|
685
|
+
against the schema exactly as written and may occur only once per
|
|
686
|
+
object, and the value has to satisfy the configured constraint as a
|
|
687
|
+
whole.
|
|
688
|
+
|
|
689
|
+
### Descriptions and Rationales
|
|
690
|
+
|
|
691
|
+
The description of a Complex Format object is all block content below
|
|
692
|
+
its heading (up to the next heading) except the property list:
|
|
693
|
+
paragraphs, blockquotes, ordered lists (e.g. scenario steps), and
|
|
694
|
+
fenced code blocks. A fenced code block
|
|
695
|
+
of language `gradia` is skipped, as it is the derived diagram which
|
|
696
|
+
**SpecBook** itself emits into exported Markdown and which must not
|
|
697
|
+
become authored content on a re-parse. In the Concise Format, the
|
|
698
|
+
description is formed by the non-property segments of the item.
|
|
699
|
+
|
|
700
|
+
A description is split into its *statement* and *rationale* at the first
|
|
701
|
+
`, BECAUSE ` (or `, **BECAUSE** `) occurrence:
|
|
702
|
+
|
|
703
|
+
```
|
|
704
|
+
The event is created and configured but not visible to attendees,
|
|
705
|
+
BECAUSE an event needs a private setup phase.
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Wiki-Style References
|
|
709
|
+
|
|
710
|
+
A `[[<reference/>]]` anywhere in a name, property value, description, or
|
|
711
|
+
rationale is a Wiki-style reference to another object, which is checked
|
|
712
|
+
for unique resolvability and rendered as a hyperlink in the HTML/PDF
|
|
713
|
+
exports. A reference is a `.`-separated path of segments, where each
|
|
714
|
+
segment matches a single object as:
|
|
715
|
+
|
|
716
|
+
- `<id/>`: the object id or explicit anchor (e.g. `[[attendee]]`)
|
|
717
|
+
- `<name/>`: the object name (e.g. `[[Attendee Browser]]`)
|
|
718
|
+
- `<kind/>:<name-or-id/>`: kind-qualified (e.g. `[[ENTITY:Event]]`)
|
|
719
|
+
- `<kind/>:*` or `*`: wildcards (for match sets, see below)
|
|
720
|
+
|
|
721
|
+
Each part can be double-quoted to allow spaces, dots, and colons inside
|
|
722
|
+
it (e.g. `[["Vue.js"]]`, whose dot would otherwise be taken as a path
|
|
723
|
+
separator). A multi-segment path matches object chains connected by
|
|
724
|
+
direct parent-to-child steps, where the leading segments up to the root
|
|
725
|
+
may be omitted (e.g. `[[FV.relay]]` matches the object `relay` directly
|
|
726
|
+
below the artifact with id `FV`).
|
|
727
|
+
|
|
728
|
+
A reference has to resolve to exactly one object: a single-segment
|
|
729
|
+
reference tries the variants (1) id/anchor, (2) name, and (3)
|
|
730
|
+
`<kind/>:<name-or-id/>` in order, where the first variant yielding any
|
|
731
|
+
matches decides; several matches are an ambiguity and zero matches an
|
|
732
|
+
unresolvable reference, both reported by the linter. Wildcard references
|
|
733
|
+
resolve into match *sets* and are used in the schema configuration to
|
|
734
|
+
constrain reference-valued properties (e.g. `[[PERSONA:*]]` or
|
|
735
|
+
`[[DM.ENTITY:*]]`).
|
|
736
|
+
|
|
737
|
+
### Image Embeddings
|
|
738
|
+
|
|
739
|
+
A Markdown image `` inside a description or property
|
|
740
|
+
value embeds a local image file, resolved relative to the artifact file:
|
|
741
|
+
SVG files are inlined as-is; PNG/JPEG files are embedded as base64
|
|
742
|
+
`data:` URLs. URLs and other file types are left untouched.
|
|
743
|
+
|
|
744
|
+
A reference carrying the `{theme}` placeholder -- e.g.
|
|
745
|
+
`` -- is a *theme-aware* embedding: it expands
|
|
746
|
+
into the two variants `light` and `dark`, both of which have to exist and
|
|
747
|
+
both of which are embedded, as two consecutive entries in this order. The
|
|
748
|
+
HTML export shows just the variant matching the color theme currently
|
|
749
|
+
active in the document, while the PDF export (like print in general)
|
|
750
|
+
always uses the `light` variant.
|