@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.
Files changed (61) hide show
  1. package/README.md +108 -328
  2. package/dst/specbook-api.d.ts +13 -20
  3. package/dst/specbook-api.js +43 -39
  4. package/dst/specbook-cli.js +42 -55
  5. package/dst/specbook-cmd-describe.d.ts +5 -2
  6. package/dst/specbook-cmd-describe.js +30 -91
  7. package/dst/specbook-cmd-export.d.ts +3 -3
  8. package/dst/specbook-cmd-export.js +23 -19
  9. package/dst/specbook-cmd-init.d.ts +3 -3
  10. package/dst/specbook-cmd-init.js +38 -15
  11. package/dst/specbook-cmd-lint.d.ts +5 -6
  12. package/dst/specbook-cmd-lint.js +61 -24
  13. package/dst/specbook-config.d.ts +2 -2
  14. package/dst/specbook-config.js +82 -73
  15. package/dst/specbook-diagnostic.js +4 -1
  16. package/dst/specbook-diagram.d.ts +6 -5
  17. package/dst/specbook-diagram.js +94 -104
  18. package/dst/specbook-export-ast.d.ts +3 -3
  19. package/dst/specbook-export-ast.js +1 -1
  20. package/dst/specbook-export-common.d.ts +11 -9
  21. package/dst/specbook-export-common.js +34 -22
  22. package/dst/specbook-export-html-search.js +10 -14
  23. package/dst/specbook-export-html.css +37 -32
  24. package/dst/specbook-export-html.d.ts +4 -4
  25. package/dst/specbook-export-html.js +153 -107
  26. package/dst/specbook-export-logo-dark.svg +35 -0
  27. package/dst/specbook-export-logo-light.svg +35 -0
  28. package/dst/specbook-export-md.d.ts +3 -3
  29. package/dst/specbook-export-md.js +50 -24
  30. package/dst/specbook-export-pdf.d.ts +4 -2
  31. package/dst/specbook-export-pdf.js +48 -38
  32. package/dst/{specbook-struct-schema.d.ts → specbook-format-schema.d.ts} +3 -2
  33. package/dst/{specbook-struct-schema.js → specbook-format-schema.js} +8 -4
  34. package/dst/specbook-format-spec.d.ts +31 -0
  35. package/dst/{specbook-struct-spec.js → specbook-format-spec.js} +10 -10
  36. package/dst/specbook-format.md +750 -0
  37. package/dst/specbook-link.d.ts +3 -2
  38. package/dst/specbook-link.js +7 -4
  39. package/dst/specbook-mcp.d.ts +2 -1
  40. package/dst/specbook-mcp.js +30 -62
  41. package/dst/specbook-parse-common.d.ts +5 -3
  42. package/dst/specbook-parse-common.js +8 -0
  43. package/dst/specbook-parse-semantic.d.ts +6 -4
  44. package/dst/specbook-parse-semantic.js +98 -106
  45. package/dst/specbook-parse-syntax.d.ts +2 -2
  46. package/dst/specbook-parse-syntax.js +175 -144
  47. package/dst/specbook-parse-value.d.ts +1 -1
  48. package/dst/specbook-parse-value.js +6 -3
  49. package/dst/specbook-parse.d.ts +4 -7
  50. package/dst/specbook-parse.js +28 -35
  51. package/dst/specbook-verbose.d.ts +2 -0
  52. package/dst/specbook-verbose.js +17 -0
  53. package/package.json +3 -7
  54. package/dst/specbook-cmd-edit.d.ts +0 -9
  55. package/dst/specbook-cmd-edit.js +0 -74
  56. package/dst/specbook-cmd-import.d.ts +0 -9
  57. package/dst/specbook-cmd-import.js +0 -78
  58. package/dst/specbook-export-logo.svg +0 -35
  59. package/dst/specbook-llm.d.ts +0 -13
  60. package/dst/specbook-llm.js +0 -51
  61. 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 `![xxx](yyy)`, 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 `![<alt/>](<file/>)` 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
+ `![Logo](logo-{theme}.svg)` -- 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.