@descent-vtt/spec-graph 0.9.10 → 0.11.0
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/CHANGELOG.md +86 -2
- package/README.md +468 -313
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +112 -67
- package/dist/cli.js.map +1 -1
- package/dist/extract.js +66 -6
- package/dist/extract.js.map +1 -1
- package/dist/glob.d.ts +11 -0
- package/dist/glob.js +17 -1
- package/dist/glob.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.js +5 -2
- package/dist/lifecycle.js.map +1 -1
- package/dist/report.d.ts +22 -0
- package/dist/report.js +50 -3
- package/dist/report.js.map +1 -1
- package/dist/sections.d.ts +7 -1
- package/dist/sections.js +9 -1
- package/dist/sections.js.map +1 -1
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +11 -1
- package/dist/vendor/spec-core/markdown/frontmatter.js +20 -3
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
- package/dist/vendor/spec-core/pattern/glob.d.ts +85 -0
- package/dist/vendor/spec-core/pattern/glob.js +207 -17
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
- package/dist/vendor/spec-core/pattern/index.d.ts +2 -2
- package/dist/vendor/spec-core/pattern/index.js +1 -1
- package/dist/vendor/spec-core/pattern/index.js.map +1 -1
- package/dist/vendor/spec-core/text/index.d.ts +1 -0
- package/dist/vendor/spec-core/text/index.js +1 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -1
- package/dist/vendor/spec-core/text/width.d.ts +30 -0
- package/dist/vendor/spec-core/text/width.js +113 -0
- package/dist/vendor/spec-core/text/width.js.map +1 -0
- package/dist/yaml.js +6 -2
- package/dist/yaml.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,12 @@ checked.
|
|
|
17
17
|
|
|
18
18
|
Exit code 1. The obligation was about to disappear.
|
|
19
19
|
|
|
20
|
+
The terms the spec-* tools share are defined in the family
|
|
21
|
+
[glossary](https://github.com/DescentVTT/spec-core/blob/main/docs/concepts.md),
|
|
22
|
+
and the [tutorial](https://github.com/DescentVTT/spec-core/blob/main/docs/tutorial.md)
|
|
23
|
+
takes one small repository through a round of work, from the brief to the
|
|
24
|
+
archive, in ten steps.
|
|
25
|
+
|
|
20
26
|
---
|
|
21
27
|
|
|
22
28
|
## Why this exists
|
|
@@ -104,11 +110,13 @@ spec-graph --strict
|
|
|
104
110
|
```
|
|
105
111
|
|
|
106
112
|
Every escalated finding says so, so you can always tell what the build would do
|
|
107
|
-
without the flag. Two deliberate limits:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
without the flag. Two deliberate limits:
|
|
114
|
+
|
|
115
|
+
- Strict leaves `info` rules alone. Those are advisory by design, and promoting
|
|
116
|
+
them would reintroduce exactly the noise the defaults avoid.
|
|
117
|
+
- An explicit `--rule` always wins, so `--strict --rule state-conflict=warn`
|
|
118
|
+
turns strict on and exempts one rule rather than making you choose between
|
|
119
|
+
all of it and none.
|
|
112
120
|
|
|
113
121
|
## How it reads your documents
|
|
114
122
|
|
|
@@ -125,11 +133,19 @@ onto four phases, chosen so one question can be answered from the phase alone:
|
|
|
125
133
|
| `frozen` | `final`, `ratified`, `locked`, `published`, … | **no** |
|
|
126
134
|
| `retired` | `superseded`, `deprecated`, `rejected`, `withdrawn`, … | **no** |
|
|
127
135
|
|
|
128
|
-
The status is found wherever your team writes it
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
The status is found wherever your team writes it, and however it is dressed
|
|
137
|
+
(`**Accepted** (2026-03-01) ✅`). A document may be written in any language; its
|
|
138
|
+
status is read in English (`status: superseded`):
|
|
139
|
+
|
|
140
|
+
- front matter: `status:`, `state:`, `stage:`
|
|
141
|
+
- a `## Status` section
|
|
142
|
+
- a `| Status | Accepted |` table at the top
|
|
143
|
+
- a `docs/adr/archive/` directory
|
|
144
|
+
|
|
145
|
+
`Accepted, later superseded by ADR-0009` is retired: retirement is terminal, so
|
|
146
|
+
a retirement word anywhere wins.
|
|
147
|
+
|
|
148
|
+
Front matter is read as YAML reads it, and a value YAML would not read -
|
|
133
149
|
`status: Superseded by ADR-9: see notes`, where a plain value cannot hold `: ` -
|
|
134
150
|
is left out rather than guessed at. Every run says how many statuses, ids and
|
|
135
151
|
relations it left out that way, and `--verbose` says where and why: quote the
|
|
@@ -155,18 +171,33 @@ that `provisionally accepted` is a draft.
|
|
|
155
171
|
| Where | Read from |
|
|
156
172
|
| --- | --- |
|
|
157
173
|
| Front matter, the first key present | `status`, `state`, `stage`, `lifecycle`, `phase`, `adr-status` |
|
|
158
|
-
| A section, under the heading | `Status`, `State`, `Stage`, `Lifecycle`, `Current status` |
|
|
174
|
+
| A section, under the heading, with or without a colon | `Status`, `State`, `Stage`, `Lifecycle`, `Current status` |
|
|
175
|
+
| A two-column table above the first `##`, in the cell right of the first key in the left column, the header row included | `Status`, `State` |
|
|
159
176
|
| A directory, for a document that declares no status | `archive`, `archived`, `attic`, `deprecated`, `graveyard`, `historical`, `obsolete`, `rejected`, `retired`, `superseded`, `superceded`, `withdrawn` |
|
|
160
177
|
|
|
178
|
+
The table is where many teams put a document's fields, `| Status | Accepted |`:
|
|
179
|
+
|
|
180
|
+
- It is read where a `## Status` section is, after one: a document with both is
|
|
181
|
+
read from the section, and front matter still wins over either.
|
|
182
|
+
- A key is compared whole, in any case, emphasis stripped.
|
|
183
|
+
- A table with more columns, one under a heading below the title, and one
|
|
184
|
+
spec-graph reads as a register are never read for the document's status: a
|
|
185
|
+
legend or a register describes other things.
|
|
186
|
+
|
|
161
187
|
</details>
|
|
162
188
|
|
|
163
189
|
`archived` is none of the four. It is what spec-brief writes on a round of work
|
|
164
|
-
it has closed, and every spec-* tool reads it as a **record**:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
it has closed, and every spec-* tool reads it as a **record**:
|
|
191
|
+
|
|
192
|
+
- depending on it is normal;
|
|
193
|
+
- its unticked boxes are not anyone's work;
|
|
194
|
+
- its links are still checked;
|
|
195
|
+
- new work handed to it is still a ghost handover.
|
|
196
|
+
|
|
197
|
+
See [Journals, changelogs and minutes](#journals-changelogs-and-minutes). Unlike
|
|
198
|
+
a journal, it still answers for what it claims: a supersession it declares, a
|
|
169
199
|
cycle it closes and a link to itself are reported as they would be anywhere.
|
|
200
|
+
|
|
170
201
|
To retire a decision, say `superseded` or `deprecated` -
|
|
171
202
|
`archived, superseded by ADR-0009` is retired - and a document under an
|
|
172
203
|
`archive/` directory that says nothing about itself is still retired.
|
|
@@ -222,26 +253,31 @@ code. *"We resolved to keep the queue"* does not close anything.
|
|
|
222
253
|
| `rejected` | `[-]` | `rejected`, `declined`, `dropped`, `not doing`, `will not do`, `abandoned`, `cancelled`, `canceled`, `withdrawn` | `rejected`, `declined` |
|
|
223
254
|
| `obviated` | | `no longer applicable`, `no longer relevant`, `no longer needed`, `overtaken by events`, `premise invalid`, `obviated`, `obsolete`, `not applicable`, `moot`, `void`, `n/a`, `obe` | `obviated`, `moot` |
|
|
224
255
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
256
|
+
Where a marker counts, and how it may be written:
|
|
257
|
+
|
|
258
|
+
- A marker counts where a statement starts: at the start of the item or of a
|
|
259
|
+
line, after the end of a sentence, or after `(` or `[`.
|
|
260
|
+
- Punctuated means followed by `:`, `:`, `-`, `–`, `—` or `.`, so `Done.`
|
|
261
|
+
closes an item; emphasised means the emphasis closes on the marker, so
|
|
262
|
+
`**Closed beta**` does not.
|
|
263
|
+
- A marker of more than one word may be spaced, hyphenated or joined by an
|
|
264
|
+
underscore, whichever the table has: `**Tech-debt:**` and `TECH_DEBT:` are
|
|
265
|
+
`tech debt`. An apostrophe may be straight or curly.
|
|
232
266
|
|
|
233
267
|
</details>
|
|
234
268
|
|
|
235
269
|
Without a box, a bullet is an obligation only at the top level of a section
|
|
236
270
|
whose heading names open work, however the heading is cased, spaced, emphasised
|
|
237
271
|
or punctuated - `## ❓ **Open questions:**` is one. The section ends at the next
|
|
238
|
-
heading of its level.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`
|
|
272
|
+
heading of its level. Every other bullet is the document's own prose.
|
|
273
|
+
|
|
274
|
+
**Headings that hold obligations:** `Open Questions`, `Open Question`,
|
|
275
|
+
`Unresolved Questions`, `Unanswered Questions`, `Questions`, `Open Issues`,
|
|
276
|
+
`Action Items`, `Actions`, `TODO`, `To Do`, `To-Do`, `TODOs`, `Next Steps`,
|
|
277
|
+
`Follow Ups`, `Follow-Ups`, `Followups`, `Follow Up`, `Follow-Up`,
|
|
242
278
|
`Unresolved`, `Outstanding`, `Outstanding Questions`, `Tasks`, `Task List`,
|
|
243
279
|
`Work Items`, `Remaining Work`, `Decisions Needed`, `Blockers`, `Parking Lot`,
|
|
244
|
-
`Future Work`, `Deferred`.
|
|
280
|
+
`Future Work`, `Deferred`.
|
|
245
281
|
|
|
246
282
|
### References, resolved forgivingly and validated strictly
|
|
247
283
|
|
|
@@ -260,15 +296,16 @@ trusts:
|
|
|
260
296
|
- **Bare identifiers in prose are opportunistic.** They are reported only when
|
|
261
297
|
their family already exists in the corpus. `ADR-0099` in a repository of ADRs
|
|
262
298
|
is a real dangling citation; `SHA-256` in the same repository is a sentence.
|
|
263
|
-
A one-letter prefix is never a family - `Q3`, `x86`, `p99` - and nor are
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
`
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
299
|
+
- A one-letter prefix is never a family - `Q3`, `x86`, `p99` - and nor are
|
|
300
|
+
the prefixes of versions, pages, protocols and platforms, even in a
|
|
301
|
+
repository that has a family of that name.
|
|
302
|
+
- **Never a citation:** `ver 3`, `version 4`, `pp.12`, `Fig.3`, `Figure 4`,
|
|
303
|
+
`Table 2`, `tbl 1`, `section 4`, `sect 2`, `sec 5`, `step 2`, `item 3`,
|
|
304
|
+
`No.5`, `num 7`, `line 42`, `ln 12`, `col 3`, `port 8080`, `PR 45`,
|
|
305
|
+
`issue 42`, `GH-123`, `UTF-8`, `ASCII 7`, `SHA-256`, `MD5`, `HTTP 2`,
|
|
306
|
+
`HTTPS 2`, `IPv6`, `IPv4 32`, `IPv6 128`, `TLS 1.3`, `SSL 3`, `ES2015`,
|
|
307
|
+
`ECMA-262`, `p99 250`, `base64`, `SQL-92`, `ARM64`, `x86-64`, `Win32`,
|
|
308
|
+
`Node 22`, `Python 3`, `Java 21`, `Go 1.22`, `cpp20`.
|
|
272
309
|
- **Bare numbers are family-scoped.** `0007` inside an RFC means `RFC-0007`,
|
|
273
310
|
never `ADR-0007`. A repository with both is ordinary, and guessing would be
|
|
274
311
|
worse than silence. A document's family is the prefix of its identifier, or
|
|
@@ -283,66 +320,20 @@ trusts:
|
|
|
283
320
|
so `[ADR-7](https://example.com/adr-7)` yields one reference rather than three.
|
|
284
321
|
- **Source links are not spec links.** `[rules.ts](../../src/rules.ts)` is not a
|
|
285
322
|
broken reference. That is [spec-guard](#relationship-to-spec-guard)'s job.
|
|
286
|
-
Nor is a link to
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
## ADR-0007: Shard the write path
|
|
301
|
-
|
|
302
|
-
**Status:** Superseded by ADR-0012
|
|
303
|
-
|
|
304
|
-
Shard by tenant id.
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Each section with an identifier *and* a status becomes a specification in its own
|
|
308
|
-
right, with its own lifecycle, its own obligations and its own relations —
|
|
309
|
-
resolvable from anywhere in the corpus as `ADR-0007`, whether it lives in a
|
|
310
|
-
register or in its own file. Moving it out later breaks no citation.
|
|
311
|
-
|
|
312
|
-
A register kept as a table works the same way, with relations typed by the column
|
|
313
|
-
header you wrote:
|
|
314
|
-
|
|
315
|
-
```md
|
|
316
|
-
| ID | Title | Status | Depends on | Superseded by |
|
|
317
|
-
| :------- | :------------- | :--------- | :--------- | :------------ |
|
|
318
|
-
| ADR-0001 | Use one writer | Superseded | - | ADR-0003 |
|
|
319
|
-
| ADR-0002 | Cache eviction | Accepted | ADR-0001 | - |
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
Findings point at the declaring cell, not at the file. A cell that is empty or
|
|
323
|
-
holds only a placeholder — `-`, `—`, `n/a`, `none`, `nil`, `TBD` — says nothing.
|
|
324
|
-
A status cell is read as a status line under a heading is, so
|
|
325
|
-
`Superseded by ADR-0003` there names the row's successor.
|
|
326
|
-
|
|
327
|
-
A row is named by its ID column, and a title is only a title. A row whose ID cell
|
|
328
|
-
names nothing — empty, a placeholder, or a `?` nobody has numbered yet — is not a
|
|
329
|
-
specification. An identifier a title opens with belongs to the document the row
|
|
330
|
-
is *about* — an issue reading
|
|
331
|
-
`| OI-V-05 | ADR-040's enforcement point has no browser test |` is filed under
|
|
332
|
-
`OI-V-05`, and citations of `ADR-040` still reach `ADR-040`. The same holds for
|
|
333
|
-
a whole file: an explicit `id:` (or `adr:`, `adr-id:`, `rfc:`, `rfc-id:`,
|
|
334
|
-
`kep-number:`, `number:` or `slug:`), then the file name, then the H1, and the
|
|
335
|
-
H1 is read as a name only when neither of the others gave one.
|
|
336
|
-
|
|
337
|
-
The register itself stays in the graph as the thing that holds them, so
|
|
338
|
-
`spec-graph query 'document[id=register] -contains-> document'` lists what is
|
|
339
|
-
inside it.
|
|
340
|
-
|
|
341
|
-
Both forms need **two** signals, and that is deliberate: `## Q3 2026 Roadmap`
|
|
342
|
-
parses as family `Q`, number 3, and `## v1.2.0` in a changelog parses as family
|
|
343
|
-
`v`, number 1. Neither declares a status, so neither is a specification. A table
|
|
344
|
-
of identifiers and prose is a citation list, not a register. See
|
|
345
|
-
[ADR-0009](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0009-a-specification-is-a-region.md).
|
|
323
|
+
Nor is a link to:
|
|
324
|
+
- any other file that is not a document: an image, a dotfile such as
|
|
325
|
+
`../.nvmrc`, or a drawing such as `arch.excalidraw`, however long its
|
|
326
|
+
extension. A dot followed by a digit, as in `ADR-0007.1`, is part of an
|
|
327
|
+
identifier and is still resolved.
|
|
328
|
+
- a directory, `../guides/` or `../guides` alike - without the slash the disk
|
|
329
|
+
says which it is, and a name with no slash at all, `guides`, is an
|
|
330
|
+
identifier.
|
|
331
|
+
|
|
332
|
+
A document is named by an explicit `id:` (or `adr:`, `adr-id:`, `rfc:`,
|
|
333
|
+
`rfc-id:`, `kep-number:`, `number:` or `slug:`), then by its file name, then by
|
|
334
|
+
its H1, and the H1 is read as a name only when neither of the others gave one.
|
|
335
|
+
A row of a register is named by its ID column instead: see
|
|
336
|
+
[Registers](#registers-many-specifications-in-one-file).
|
|
346
337
|
|
|
347
338
|
### When `[[...]]` tags a concept
|
|
348
339
|
|
|
@@ -374,7 +365,7 @@ still a relation in the graph no matter what you exclude — not even
|
|
|
374
365
|
`--ignore-ref "*"` can delete one. See
|
|
375
366
|
[ADR-0008](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0008-wiki-links-carry-no-path.md).
|
|
376
367
|
|
|
377
|
-
|
|
368
|
+
### A gap that is deliberate
|
|
378
369
|
|
|
379
370
|
Real filing histories have holes: an ADR number reserved and then withdrawn, a
|
|
380
371
|
document that was folded into another before it was ever committed. Something
|
|
@@ -447,13 +438,19 @@ Blocked by [ADR-0009](0009-compliance.md). → blocked-by
|
|
|
447
438
|
|
|
448
439
|
A phrase governs a reference when it stands no more than 40 characters before
|
|
449
440
|
it, in the same statement: a sentence, a paragraph, a list item or a table cell.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
441
|
+
|
|
442
|
+
- A negation between the two - `owned by nobody` - cancels it.
|
|
443
|
+
- A sentence written in Chinese ends at `。`, `?`, `!` or `;`, with a space
|
|
444
|
+
after it or none, so `Deferred to later。另見 ADR-0002` hands nothing to
|
|
445
|
+
ADR-0002. A stop only ends a statement sooner: it reads less, never more.
|
|
446
|
+
- Prose reads both directions too: `supersedes` and `superseded by`, `amends`
|
|
447
|
+
and `amended by`, `blocked by` and `blocks` or `blocking`, `depends on` and
|
|
448
|
+
`depended on by` - the second of each pair points back at the document that
|
|
449
|
+
wrote it.
|
|
450
|
+
- A phrase that follows the reference makes the reference its subject -
|
|
451
|
+
`[ADR-0009](0009.md) supersedes this` - except a `depends-on` or `assumes`
|
|
452
|
+
phrase: `[ADR-0002](0002.md) requires a migration` says nothing about this
|
|
453
|
+
document.
|
|
457
454
|
|
|
458
455
|
A link under a heading that files it as bookkeeping is `relates-to` unless a
|
|
459
456
|
phrase beside it says more. **Bookkeeping headings:** `See also`, `References`,
|
|
@@ -462,12 +459,142 @@ phrase beside it says more. **Bookkeeping headings:** `See also`, `References`,
|
|
|
462
459
|
`Changelog`, `Change log`, `Revision history`, `Bibliography`, `Sources`,
|
|
463
460
|
`Appendix`, `More information`, `Resources`, `Index`.
|
|
464
461
|
|
|
465
|
-
Two properties do the work
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
462
|
+
Two properties do the work:
|
|
463
|
+
|
|
464
|
+
- `loadBearing` relations (`assumes`, `depends-on`, `blocked-by`, `amends`) make
|
|
465
|
+
the source's validity depend on the target — those are what turn stale.
|
|
466
|
+
- `transfersObligation` relations (`delegates-to`, `blocked-by`) move work —
|
|
467
|
+
those are what become ghosts.
|
|
468
|
+
|
|
469
|
+
A "See also" link is neither, and is never reported, which is what keeps the
|
|
470
|
+
signal-to-noise ratio high enough to leave the tool switched on.
|
|
471
|
+
|
|
472
|
+
### Registers: many specifications in one file
|
|
473
|
+
|
|
474
|
+
One file per decision is the MADR, KEP and RFC layout, and it is what most
|
|
475
|
+
repositories do. Plenty keep a register instead — one document holding dozens of
|
|
476
|
+
decisions. **A specification is a region of a file, not a file**, so both work:
|
|
477
|
+
|
|
478
|
+
```md
|
|
479
|
+
## ADR-0007: Shard the write path
|
|
480
|
+
|
|
481
|
+
**Status:** Superseded by ADR-0012
|
|
482
|
+
|
|
483
|
+
Shard by tenant id.
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Each section with an identifier *and* a status - a `**Status:**` line or a
|
|
487
|
+
`### Status` section under its heading - becomes a
|
|
488
|
+
specification in its own right, with its own lifecycle, its own obligations and
|
|
489
|
+
its own relations — resolvable from anywhere in the corpus as `ADR-0007`,
|
|
490
|
+
whether it lives in a register or in its own file. Moving it out later breaks
|
|
491
|
+
no citation.
|
|
492
|
+
|
|
493
|
+
A register kept as a table works the same way, with relations typed by the column
|
|
494
|
+
header you wrote:
|
|
495
|
+
|
|
496
|
+
```md
|
|
497
|
+
| ID | Title | Status | Depends on | Superseded by |
|
|
498
|
+
| :------- | :------------- | :--------- | :--------- | :------------ |
|
|
499
|
+
| ADR-0001 | Use one writer | Superseded | - | ADR-0003 |
|
|
500
|
+
| ADR-0002 | Cache eviction | Accepted | ADR-0001 | - |
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Findings point at the declaring cell, not at the file. A cell that is empty or
|
|
504
|
+
holds only a placeholder — `-`, `—`, `n/a`, `none`, `nil`, `TBD` — says nothing.
|
|
505
|
+
A status cell is read as a status line under a heading is, so
|
|
506
|
+
`Superseded by ADR-0003` there names the row's successor.
|
|
507
|
+
|
|
508
|
+
A row is named by its ID column, and a title is only a title:
|
|
509
|
+
|
|
510
|
+
- A row whose ID cell names nothing — empty, a placeholder, or a `?` nobody has
|
|
511
|
+
numbered yet — is not a specification.
|
|
512
|
+
- An identifier a title opens with belongs to the document the row is *about* —
|
|
513
|
+
an issue reading `| OI-V-05 | ADR-040's enforcement point has no browser test |`
|
|
514
|
+
is filed under `OI-V-05`, and citations of `ADR-040` still reach `ADR-040`.
|
|
515
|
+
|
|
516
|
+
The same holds for a whole file, whose H1 is a name only when nothing else gave
|
|
517
|
+
one: see [References](#references-resolved-forgivingly-and-validated-strictly).
|
|
518
|
+
|
|
519
|
+
The register itself stays in the graph as the thing that holds them, so
|
|
520
|
+
`spec-graph query 'document[id=register] -contains-> document'` lists what is
|
|
521
|
+
inside it.
|
|
522
|
+
|
|
523
|
+
Both forms need **two** signals, and that is deliberate: `## Q3 2026 Roadmap`
|
|
524
|
+
parses as family `Q`, number 3, and `## v1.2.0` in a changelog parses as family
|
|
525
|
+
`v`, number 1. Neither declares a status, so neither is a specification. A table
|
|
526
|
+
of identifiers and prose is a citation list, not a register. See
|
|
527
|
+
[ADR-0009](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0009-a-specification-is-a-region.md).
|
|
528
|
+
|
|
529
|
+
## Directives
|
|
530
|
+
|
|
531
|
+
Inference covers almost everything, but inference you cannot override is a trap.
|
|
532
|
+
Directives are ordinary HTML comments — invisible in every Markdown renderer:
|
|
533
|
+
|
|
534
|
+
```md
|
|
535
|
+
<!-- @spec-node id="ADR-0007" status="accepted" aliases="sharding, adr-7" -->
|
|
536
|
+
<!-- @spec-item id="shard-key" state="narrowed" -->
|
|
537
|
+
<!-- @spec-edge kind="delegates-to" to="ADR-0011#scope" -->
|
|
538
|
+
<!-- @spec-ignore -->
|
|
539
|
+
<!-- @spec-history -->
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Each takes these attributes. Any other is a parse problem, listed by
|
|
543
|
+
`--verbose`, and the directive still applies:
|
|
544
|
+
|
|
545
|
+
| directive | attributes |
|
|
546
|
+
| --- | --- |
|
|
547
|
+
| `@spec-node` | `id`, `status`, `title`, `aliases`, `kind` |
|
|
548
|
+
| `@spec-item` | `id`, `state`, `title`, `owner`, `note` |
|
|
549
|
+
| `@spec-edge` | `kind`, `to`, `from`, `reason` |
|
|
550
|
+
| `@spec-ignore` | `reason` |
|
|
551
|
+
| `@spec-history` | none |
|
|
552
|
+
|
|
553
|
+
- `reason`, `note`, `owner` and a node's `kind` are for whoever reads the
|
|
554
|
+
source; spec-graph reads nothing from them.
|
|
555
|
+
- `aliases` splits at commas, semicolons and spaces.
|
|
556
|
+
- A value is quoted with either quote mark, or bare when it holds no space or
|
|
557
|
+
quote, and an attribute with no value is a flag.
|
|
558
|
+
- A directive is the first thing in its comment, so a comment that only
|
|
559
|
+
mentions one is a comment.
|
|
560
|
+
|
|
561
|
+
A directive always wins, and the report says the state came from a directive, so
|
|
562
|
+
an override is visible rather than mysterious.
|
|
563
|
+
|
|
564
|
+
A `@spec-item` annotates one item: the one directly below it, with nothing but
|
|
565
|
+
blank lines or other comments between, or else the one it is written on or
|
|
566
|
+
indented under. An id that two items end up with is a parse problem, listed by
|
|
567
|
+
`--verbose`, and the first item keeps it.
|
|
568
|
+
|
|
569
|
+
## Journals, changelogs and minutes
|
|
570
|
+
|
|
571
|
+
A 2024 journal noting *"decision deferred to ADR-002"* is not delegating
|
|
572
|
+
anything. It is reporting that somebody once did. When ADR-002 retires in 2026,
|
|
573
|
+
the note does not become a defect — there is nothing in it for anyone to fix.
|
|
574
|
+
|
|
575
|
+
Declare those files and spec-graph stops holding them to a lifecycle they never
|
|
576
|
+
had:
|
|
577
|
+
|
|
578
|
+
```json
|
|
579
|
+
{ "historyPatterns": ["**/JOURNAL_*.md", "archive/**"] }
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
or, for one file, `<!-- @spec-history -->` at the top of it. A brief whose
|
|
583
|
+
status is `archived` is a record already, with nothing declared, though not a
|
|
584
|
+
log: the supersessions, cycles and self-references it declares are still
|
|
585
|
+
checked.
|
|
586
|
+
|
|
587
|
+
| | |
|
|
588
|
+
| --- | --- |
|
|
589
|
+
| its links resolve | **still checked** — a broken link is broken whoever wrote it |
|
|
590
|
+
| its checkboxes | not obligations, and not in the headline count |
|
|
591
|
+
| its delegations | reports of what was said |
|
|
592
|
+
| work handed **into** it | **still checked** — a log will never act on it |
|
|
593
|
+
|
|
594
|
+
That last row is the point. Excluding the file with `--ignore` would have
|
|
595
|
+
silenced the whole lot, including the links, and a journal full of 404s is
|
|
596
|
+
exactly what this tool is for. See
|
|
597
|
+
[ADR-0011](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0011-a-record-is-not-a-specification.md).
|
|
471
598
|
|
|
472
599
|
## The query language
|
|
473
600
|
|
|
@@ -535,12 +662,10 @@ predicate is linear in the subject whatever the pattern — `^([A-Za-z0-9_]+[ ]?
|
|
|
535
662
|
against a fifty-four-character title takes `RegExp` 103 seconds and this 13
|
|
536
663
|
microseconds. It reads the usual syntax minus backreferences and lookaround,
|
|
537
664
|
which are not regular; both are refused when the selector is read, with the
|
|
538
|
-
character pointed at.
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
are spec-core's, the library the spec-* tools share, copied into this package
|
|
543
|
-
rather than installed.
|
|
665
|
+
character pointed at. The automaton is spec-core's, the library the spec-* tools
|
|
666
|
+
share, copied into this package rather than installed, and so is the one
|
|
667
|
+
[globs](#patterns) are matched by. See
|
|
668
|
+
[ADR-0017](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0017-a-predicate-must-finish.md).
|
|
544
669
|
|
|
545
670
|
On a register, a region answers its file's front matter for every descriptive
|
|
546
671
|
key — `fm.owner` on a decision inside a register is the register's owner — while
|
|
@@ -612,74 +737,6 @@ spec-graph: .spec-graph.json: rules.owner: "{1.phse}" asks for an attribute
|
|
|
612
737
|
|
|
613
738
|
See [ADR-0016](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0016-a-query-needs-a-sentence.md).
|
|
614
739
|
|
|
615
|
-
## Directives
|
|
616
|
-
|
|
617
|
-
Inference covers almost everything, but inference you cannot override is a trap.
|
|
618
|
-
Directives are ordinary HTML comments — invisible in every Markdown renderer:
|
|
619
|
-
|
|
620
|
-
```md
|
|
621
|
-
<!-- @spec-node id="ADR-0007" status="accepted" aliases="sharding, adr-7" -->
|
|
622
|
-
<!-- @spec-item id="shard-key" state="narrowed" -->
|
|
623
|
-
<!-- @spec-edge kind="delegates-to" to="ADR-0011#scope" -->
|
|
624
|
-
<!-- @spec-ignore -->
|
|
625
|
-
<!-- @spec-history -->
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
Each takes these attributes. Any other is a parse problem, listed by
|
|
629
|
-
`--verbose`, and the directive still applies:
|
|
630
|
-
|
|
631
|
-
| directive | attributes |
|
|
632
|
-
| --- | --- |
|
|
633
|
-
| `@spec-node` | `id`, `status`, `title`, `aliases`, `kind` |
|
|
634
|
-
| `@spec-item` | `id`, `state`, `title`, `owner`, `note` |
|
|
635
|
-
| `@spec-edge` | `kind`, `to`, `from`, `reason` |
|
|
636
|
-
| `@spec-ignore` | `reason` |
|
|
637
|
-
| `@spec-history` | none |
|
|
638
|
-
|
|
639
|
-
`reason`, `note`, `owner` and a node's `kind` are for whoever reads the source;
|
|
640
|
-
spec-graph reads nothing from them. `aliases` splits at commas, semicolons and
|
|
641
|
-
spaces. A value is quoted with either quote mark, or bare when it holds no space
|
|
642
|
-
or quote, and an attribute with no value is a flag. A directive is the first
|
|
643
|
-
thing in its comment, so a comment that only mentions one is a comment.
|
|
644
|
-
|
|
645
|
-
A directive always wins, and the report says the state came from a directive, so
|
|
646
|
-
an override is visible rather than mysterious.
|
|
647
|
-
|
|
648
|
-
A `@spec-item` annotates one item: the one directly below it, with nothing but
|
|
649
|
-
blank lines or other comments between, or else the one it is written on or
|
|
650
|
-
indented under. An id that two items end up with is a parse problem, listed by
|
|
651
|
-
`--verbose`, and the first item keeps it.
|
|
652
|
-
|
|
653
|
-
## Journals, changelogs and minutes
|
|
654
|
-
|
|
655
|
-
A 2024 journal noting *"decision deferred to ADR-002"* is not delegating
|
|
656
|
-
anything. It is reporting that somebody once did. When ADR-002 retires in 2026,
|
|
657
|
-
the note does not become a defect — there is nothing in it for anyone to fix.
|
|
658
|
-
|
|
659
|
-
Declare those files and spec-graph stops holding them to a lifecycle they never
|
|
660
|
-
had:
|
|
661
|
-
|
|
662
|
-
```json
|
|
663
|
-
{ "historyPatterns": ["**/JOURNAL_*.md", "archive/**"] }
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
or, for one file, `<!-- @spec-history -->` at the top of it. A brief whose
|
|
667
|
-
status is `archived` is a record already, with nothing declared, though not a
|
|
668
|
-
log: the supersessions, cycles and self-references it declares are still
|
|
669
|
-
checked.
|
|
670
|
-
|
|
671
|
-
| | |
|
|
672
|
-
| --- | --- |
|
|
673
|
-
| its links resolve | **still checked** — a broken link is broken whoever wrote it |
|
|
674
|
-
| its checkboxes | not obligations, and not in the headline count |
|
|
675
|
-
| its delegations | reports of what was said |
|
|
676
|
-
| work handed **into** it | **still checked** — a log will never act on it |
|
|
677
|
-
|
|
678
|
-
That last row is the point. Excluding the file with `--ignore` would have
|
|
679
|
-
silenced the whole lot, including the links, and a journal full of 404s is
|
|
680
|
-
exactly what this tool is for. See
|
|
681
|
-
[ADR-0011](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0011-a-record-is-not-a-specification.md).
|
|
682
|
-
|
|
683
740
|
## Adopting this on a repository that predates it
|
|
684
741
|
|
|
685
742
|
Fifty findings on day one is not a report, it is a decision to be ignored. So
|
|
@@ -736,64 +793,6 @@ the other case and stays optional, because a repository declares the path before
|
|
|
736
793
|
the first run records the file — `--verbose` says when it read nothing. Either
|
|
737
794
|
may be absolute. See [ADR-0012](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0012-a-baseline-is-a-ratchet.md).
|
|
738
795
|
|
|
739
|
-
## In CI
|
|
740
|
-
|
|
741
|
-
The exit code is the contract: `0` clean, `1` findings, `2` the tool could not
|
|
742
|
-
run. That is all most pipelines need.
|
|
743
|
-
|
|
744
|
-
For annotations on the diff rather than a line in a log, emit SARIF and hand it
|
|
745
|
-
to the uploader that already exists:
|
|
746
|
-
|
|
747
|
-
```yaml
|
|
748
|
-
- run: npx spec-graph --format sarif > spec-graph.sarif
|
|
749
|
-
continue-on-error: true
|
|
750
|
-
- uses: github/codeql-action/upload-sarif@v3
|
|
751
|
-
with:
|
|
752
|
-
sarif_file: spec-graph.sarif
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
Findings land as comments on the changed lines, and they persist across commits
|
|
756
|
-
without drifting, because the `partialFingerprints` spec-graph writes are the
|
|
757
|
-
same identity a baseline is keyed on — the rule, the specification, and the
|
|
758
|
-
citation, with no line number in it (ADR-0012).
|
|
759
|
-
|
|
760
|
-
On GitLab, `--format gitlab` writes the Code Quality report a merge request
|
|
761
|
-
reads, and the merge request shows which findings it introduced and which it
|
|
762
|
-
resolved:
|
|
763
|
-
|
|
764
|
-
```yaml
|
|
765
|
-
spec-graph:
|
|
766
|
-
script:
|
|
767
|
-
- npx spec-graph --format gitlab > gl-code-quality-report.json
|
|
768
|
-
artifacts:
|
|
769
|
-
when: always
|
|
770
|
-
reports:
|
|
771
|
-
codequality: gl-code-quality-report.json
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
Each finding is an issue with its rule as `check_name`, its message and hint as
|
|
775
|
-
`description`, and its file and line as `location`. Severities map as `error`
|
|
776
|
-
to `critical` - or `major` when only `--strict` made it an error - `warn` to
|
|
777
|
-
`minor` and `info` to `info`. The `fingerprint` is a SHA-256 of the rule, the
|
|
778
|
-
specification and what within it - a citation's target, or the document at the
|
|
779
|
-
other end - which is what a baseline counts by. There is no line in it, so a
|
|
780
|
-
finding a paragraph moved is not reported as new, and no message, so ticking
|
|
781
|
-
one of three open boxes does not make an `orphaned-obligation` new either; the
|
|
782
|
-
second of two findings identical in all three is told apart by its order. `when: always` keeps the report when the job fails, which
|
|
783
|
-
it does, with exit `1`, whenever there is an error to report.
|
|
784
|
-
|
|
785
|
-
`--format json` is the machine-readable output to parse if you are building
|
|
786
|
-
something of your own: it carries the baseline note, the summary and the
|
|
787
|
-
per-file detail that SARIF and Code Quality have nowhere to put.
|
|
788
|
-
|
|
789
|
-
**There is no `--watch`.** A full run on this repository takes 60 ms, so the
|
|
790
|
-
loop that would justify a resident process is one line of shell, and it belongs
|
|
791
|
-
to whatever the developer already uses rather than to this tool:
|
|
792
|
-
|
|
793
|
-
```bash
|
|
794
|
-
npx spec-graph --format json | jq -r '.summary'
|
|
795
|
-
```
|
|
796
|
-
|
|
797
796
|
## Configuration
|
|
798
797
|
|
|
799
798
|
Anything you repeat on every run belongs to the repository rather than to the
|
|
@@ -823,10 +822,15 @@ command. spec-graph reads the first of `.spec-graph.json`,
|
|
|
823
822
|
The file is looked for in the working directory and then upward, as far as the
|
|
824
823
|
repository — a directory holding `.git` — and **the directory holding it becomes
|
|
825
824
|
the root**. So a run from `packages/auth` reports what a run from the top
|
|
826
|
-
reports, byte for byte, because every path here is relative to the root.
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
825
|
+
reports, byte for byte, because every path here is relative to the root.
|
|
826
|
+
|
|
827
|
+
- The nearest file wins.
|
|
828
|
+
- `--root` names the root yourself and turns discovery off.
|
|
829
|
+
- A path typed on the command line stays relative to where you typed it, brace
|
|
830
|
+
alternative by brace alternative: `{/docs,x}` typed in `sub` is `/docs` or
|
|
831
|
+
`sub/x`.
|
|
832
|
+
|
|
833
|
+
See [ADR-0018](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0018-the-configuration-file-is-the-root.md).
|
|
830
834
|
|
|
831
835
|
A flag always wins over the file, and list flags **add** to it rather than
|
|
832
836
|
replacing it — a `--ignore-ref` on the command line is one more exclusion, not a
|
|
@@ -839,15 +843,19 @@ it can be combined with any `--format` and leave the document on stdout intact.
|
|
|
839
843
|
— the lines under it in the terminal, `related` in JSON, `relatedLocations` in
|
|
840
844
|
SARIF — and is 8 unless set; `0` lists none.
|
|
841
845
|
|
|
842
|
-
A broken config stops the run with exit `2`, each problem named on stderr
|
|
846
|
+
A broken config stops the run with exit `2`, each problem named on stderr:
|
|
843
847
|
invalid JSON, a value of the wrong type, an unknown key, a rule that does not
|
|
844
|
-
compile.
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
848
|
+
compile. Why a mistake stops the run:
|
|
849
|
+
|
|
850
|
+
- A configuration that did not load checks a different repository than the one
|
|
851
|
+
configured, and would report *that* one as consistent: a `{1.phse}` for
|
|
852
|
+
`{1.phase}` in one rule's message is a green build over the files the
|
|
853
|
+
correctly spelled rule fails.
|
|
854
|
+
- An unknown key counts, for the same reason it is reported at all — a silently
|
|
855
|
+
ignored `ignoreReference` is a configuration that looks applied and is not.
|
|
856
|
+
|
|
857
|
+
`--no-config` checks on defaults instead, and the exit code then says which run
|
|
858
|
+
it was.
|
|
851
859
|
|
|
852
860
|
### Patterns
|
|
853
861
|
|
|
@@ -870,52 +878,76 @@ recorded on a Windows checkout holds in Linux CI:
|
|
|
870
878
|
| `!docs/drafts/**` | takes back what an earlier pattern matched; the last to match wins |
|
|
871
879
|
| `docs\adr\*.md` | the same as `docs/adr/*.md`: a `\` is a separator |
|
|
872
880
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
in
|
|
881
|
+
Three kinds of pattern stop the run with exit `2`, and the run names the
|
|
882
|
+
pattern, wherever it was written:
|
|
883
|
+
|
|
884
|
+
- a pattern that cannot mean a path under the root — an unclosed `[` or `{`,
|
|
885
|
+
`..`, a lone `.` or `./`, an empty one, or braces that give one of them,
|
|
886
|
+
`{./,docs}`;
|
|
887
|
+
- an extended glob, `+(a|b)`: write `{a,b}`;
|
|
888
|
+
- one too large to compile: braces that expand to more than 256 patterns, or a
|
|
889
|
+
pattern past 65,536 automaton states.
|
|
890
|
+
|
|
891
|
+
Globs - the patterns to check, `--ignore`, `--ignore-ref`, `--history` - are
|
|
892
|
+
matched by an automaton rather than by `RegExp`, because a glob with three stars
|
|
893
|
+
in it is enough to keep `RegExp` busy for two minutes over a long reference
|
|
894
|
+
target. See
|
|
895
|
+
[ADR-0017](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0017-a-predicate-must-finish.md).
|
|
896
|
+
|
|
897
|
+
A class never matches a `/`. A parenthesis with no `|` in its group is itself,
|
|
898
|
+
so `C++(notes).md` names that file. A `..` typed below the root,
|
|
899
|
+
`spec-graph "../*.md"` from `docs/deep`, is resolved against where you typed it;
|
|
900
|
+
one that climbs past the root is refused, and so is a pattern typed in a
|
|
901
|
+
directory whose name a pattern would read as syntax, such as `notes[1]`.
|
|
902
|
+
|
|
903
|
+
A bare name and a path read differently as an `--ignore`:
|
|
904
|
+
|
|
905
|
+
- A bare `--ignore` name, `--ignore drafts`, still prunes that directory at any
|
|
906
|
+
depth, even where a pattern starts inside it or names it.
|
|
907
|
+
- A `!` before one gives the name back, as a `.gitignore` line does:
|
|
908
|
+
`--ignore drafts --ignore '!drafts'` reads every `drafts`, and the last bare
|
|
909
|
+
name to name a directory decides.
|
|
910
|
+
- A path, `--ignore "docs/*"`, takes out what it matches wherever a pattern
|
|
911
|
+
starts: `docs/drafts/**/*.md` finds nothing under it, as `docs/**/*.md` finds
|
|
912
|
+
nothing in `docs/drafts`.
|
|
890
913
|
|
|
891
914
|
The walk never enters `.git`, `.hg`, `.svn`, `.cache`, `.next`, `.nuxt`,
|
|
892
915
|
`.turbo`, `.venv`, `node_modules`, `bower_components`, `vendor`, `dist`,
|
|
893
916
|
`build`, `out`, `target`, `coverage` or `__pycache__` on its way to what the
|
|
894
|
-
patterns name. Unlike an `--ignore`, these yield
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
917
|
+
patterns name. Unlike an `--ignore`, these yield:
|
|
918
|
+
|
|
919
|
+
- to a pattern that starts inside one: `vendor/specs/*.md` is read from there,
|
|
920
|
+
whatever the other patterns reach;
|
|
921
|
+
- to a plain name for one: `vendor` and `docs/vendor` read those directories
|
|
922
|
+
as `vendor/` and `docs/vendor/` do, or the file, where one of them is a file;
|
|
923
|
+
- to a `!` before their name: `--ignore '!vendor'` reads every `vendor`, the
|
|
924
|
+
rest of the list still skipped inside it. A `!` before a path takes back only
|
|
925
|
+
what a path `--ignore` took out, so `!docs/vendor` gives back nothing the walk
|
|
926
|
+
skips: name it among the patterns.
|
|
927
|
+
|
|
928
|
+
Nor does the walk follow a link, a pattern that starts beyond one included:
|
|
929
|
+
`docs/linked/*.md` reads nothing through the link `docs/linked`, as
|
|
930
|
+
`docs/**/*.md` reads nothing there. `followSymlinks` in the
|
|
931
|
+
[API](#programmatic-api) follows links, and:
|
|
932
|
+
|
|
933
|
+
- prunes a link to a directory as it prunes a directory of that name and path:
|
|
934
|
+
a linked `node_modules` is skipped, and an `--ignore` takes a link out;
|
|
935
|
+
- never follows a link back into a directory the walk is inside, so a cycle is
|
|
936
|
+
read once, and says nothing of it, as it says nothing of a link it does not
|
|
937
|
+
follow.
|
|
938
|
+
|
|
939
|
+
`--ignore-ref` is matched against reference targets rather than paths:
|
|
940
|
+
|
|
941
|
+
- a bare pattern names one target exactly;
|
|
942
|
+
- case is ignored on every host;
|
|
943
|
+
- a `\` escapes the next character;
|
|
944
|
+
- `.` and `..` are the text of the link, in a brace alternative as anywhere
|
|
945
|
+
else: `docs/{.,x}` names `docs/.` and `docs/x`, and `{./,docs}` the targets
|
|
946
|
+
under `./`, and `docs`;
|
|
947
|
+
- a leading `/` is the text of the link too: `{/docs/a.md,x}` names the target
|
|
948
|
+
`/docs/a.md`, as `/docs/a.md` does, and not `docs/a.md`.
|
|
949
|
+
|
|
950
|
+
See [ADR-0022](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0022-globs-are-the-family-path-dialect.md).
|
|
919
951
|
|
|
920
952
|
### Family rules
|
|
921
953
|
|
|
@@ -976,8 +1008,8 @@ spec-graph diff <before> <after> Compare two JSON graph exports.
|
|
|
976
1008
|
--record-baseline <f> Write today's findings as accepted debt, exit 0
|
|
977
1009
|
--ratchet Also fail when a baseline entry no longer occurs
|
|
978
1010
|
--no-config Ignore .spec-graph.json and the package.json key
|
|
979
|
-
--format <fmt> human | json | sarif (check) |
|
|
980
|
-
| markdown (check, diff)
|
|
1011
|
+
--format <fmt> human | json | sarif (check) | github (check)
|
|
1012
|
+
| gitlab (check) | markdown (check, diff)
|
|
981
1013
|
--graph-format <fmt> dot | mermaid | json
|
|
982
1014
|
--documents-only Hide items; their relations lift onto their documents
|
|
983
1015
|
--rule <id>=<severity> error | warn | info | off (repeatable)
|
|
@@ -994,13 +1026,110 @@ never masquerades as a passing build, and neither does a pattern that matches
|
|
|
994
1026
|
nothing, a configuration file that did not load, or a `--baseline` that is not
|
|
995
1027
|
there.
|
|
996
1028
|
|
|
997
|
-
|
|
1029
|
+
**There is no `--watch`.** A full run on this repository takes 60 ms, so the
|
|
1030
|
+
loop that would justify a resident process is one line of shell, and it belongs
|
|
1031
|
+
to whatever the developer already uses rather than to this tool:
|
|
1032
|
+
|
|
1033
|
+
```bash
|
|
1034
|
+
npx spec-graph --format json | jq -r '.summary'
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
## In CI
|
|
1038
|
+
|
|
1039
|
+
The exit code is the contract: `0` clean, `1` findings, `2` the tool could not
|
|
1040
|
+
run. That is all most pipelines need.
|
|
1041
|
+
|
|
1042
|
+
Beyond that, a format puts the findings where somebody will look:
|
|
1043
|
+
|
|
1044
|
+
| Format | Where the findings land |
|
|
1045
|
+
| --- | --- |
|
|
1046
|
+
| `--format sarif` | GitHub code scanning: comments on the changed lines |
|
|
1047
|
+
| `--format github` | annotations on the diff, from the job's log, with no upload step |
|
|
1048
|
+
| `--format gitlab` | the Code Quality report of a GitLab merge request |
|
|
1049
|
+
| `--format markdown` | a table for the job summary, where reviewers look |
|
|
1050
|
+
| `--format json` | a script or a bot of your own |
|
|
1051
|
+
|
|
1052
|
+
### Code scanning, with SARIF
|
|
1053
|
+
|
|
1054
|
+
For annotations on the diff rather than a line in a log, emit SARIF and hand it
|
|
1055
|
+
to the uploader that already exists. The upload needs `security-events: write`,
|
|
1056
|
+
and a job that declares its permissions has only those, so it names
|
|
1057
|
+
`contents: read` for its checkout as well:
|
|
998
1058
|
|
|
999
1059
|
```yaml
|
|
1000
|
-
|
|
1001
|
-
|
|
1060
|
+
permissions:
|
|
1061
|
+
contents: read
|
|
1062
|
+
security-events: write
|
|
1063
|
+
steps:
|
|
1064
|
+
# check out the repository and set up Node, then:
|
|
1065
|
+
- run: npx spec-graph --format sarif > spec-graph.sarif
|
|
1066
|
+
continue-on-error: true
|
|
1067
|
+
- uses: github/codeql-action/upload-sarif@v4
|
|
1068
|
+
with:
|
|
1069
|
+
sarif_file: spec-graph.sarif
|
|
1002
1070
|
```
|
|
1003
1071
|
|
|
1072
|
+
Findings land as comments on the changed lines, and they persist across commits
|
|
1073
|
+
without drifting, because the `partialFingerprints` spec-graph writes are the
|
|
1074
|
+
same identity a baseline is keyed on — the rule, the specification, and the
|
|
1075
|
+
citation, with no line number in it (ADR-0012).
|
|
1076
|
+
|
|
1077
|
+
### Annotations, with `--format github`
|
|
1078
|
+
|
|
1079
|
+
Without code scanning, `--format github` writes one GitHub Actions workflow
|
|
1080
|
+
command per finding, and the job's log turns each into an annotation on the
|
|
1081
|
+
diff, with no upload step:
|
|
1082
|
+
|
|
1083
|
+
```yaml
|
|
1084
|
+
- run: npx spec-graph --format github
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
```text
|
|
1088
|
+
::error file=docs/adr/0003-event-log.md,line=13,title=ghost-handover::open obligation delegates to ADR-0002, which is retired. nothing will be read from ADR-0002 again - re-home this in a live document, or close it here
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
- An `error` finding is an `error`, a `warn` a `warning` and an `info` a
|
|
1092
|
+
`notice`, as spec-guard writes them, and one only `--strict` made an error is
|
|
1093
|
+
titled `ghost-handover (strict)`.
|
|
1094
|
+
- The message carries the hint.
|
|
1095
|
+
- `%` and line breaks are escaped everywhere, and `:` and `,` in the file and
|
|
1096
|
+
the title, as GitHub reads them.
|
|
1097
|
+
- A clean run writes nothing.
|
|
1098
|
+
|
|
1099
|
+
### Code Quality, with `--format gitlab`
|
|
1100
|
+
|
|
1101
|
+
On GitLab, `--format gitlab` writes the Code Quality report a merge request
|
|
1102
|
+
reads, and the merge request shows which findings it introduced and which it
|
|
1103
|
+
resolved:
|
|
1104
|
+
|
|
1105
|
+
```yaml
|
|
1106
|
+
spec-graph:
|
|
1107
|
+
script:
|
|
1108
|
+
- npx spec-graph --format gitlab > gl-code-quality-report.json
|
|
1109
|
+
artifacts:
|
|
1110
|
+
when: always
|
|
1111
|
+
reports:
|
|
1112
|
+
codequality: gl-code-quality-report.json
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
Each finding is an issue:
|
|
1116
|
+
|
|
1117
|
+
- `check_name` is its rule, `description` its message and hint, and `location`
|
|
1118
|
+
its file and line.
|
|
1119
|
+
- Severities map as `error` to `critical` - or `major` when only `--strict` made
|
|
1120
|
+
it an error - `warn` to `minor` and `info` to `info`.
|
|
1121
|
+
- The `fingerprint` is a SHA-256 of the rule, the specification and what within
|
|
1122
|
+
it - a citation's target, or the document at the other end - which is what a
|
|
1123
|
+
baseline counts by. There is no line in it, so a finding a paragraph moved is
|
|
1124
|
+
not reported as new, and no message, so ticking one of three open boxes does
|
|
1125
|
+
not make an `orphaned-obligation` new either; the second of two findings
|
|
1126
|
+
identical in all three is told apart by its order.
|
|
1127
|
+
|
|
1128
|
+
`when: always` keeps the report when the job fails, which it does, with exit
|
|
1129
|
+
`1`, whenever there is an error to report.
|
|
1130
|
+
|
|
1131
|
+
### A summary for reviewers, with `--format markdown`
|
|
1132
|
+
|
|
1004
1133
|
`--format markdown` writes the same facts as a table, for the place reviewers
|
|
1005
1134
|
actually look:
|
|
1006
1135
|
|
|
@@ -1014,6 +1143,17 @@ It leads with the verdict, counts the corpus, gives every finding its hint, and
|
|
|
1014
1143
|
*names* the baseline entries that no longer occur rather than counting them —
|
|
1015
1144
|
on a pull request a count is the one thing a reader cannot act on.
|
|
1016
1145
|
|
|
1146
|
+
### JSON, for anything of your own
|
|
1147
|
+
|
|
1148
|
+
`--format json` is the machine-readable output to parse if you are building
|
|
1149
|
+
something of your own: it carries the baseline note, the summary and the
|
|
1150
|
+
per-file detail that SARIF and Code Quality have nowhere to put.
|
|
1151
|
+
|
|
1152
|
+
```yaml
|
|
1153
|
+
- name: Check the specification graph
|
|
1154
|
+
run: npx spec-graph "docs/**/*.md" --format json > spec-graph.json
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1017
1157
|
The JSON report is versioned, flat, and carries start and end positions for
|
|
1018
1158
|
every finding, so a bot can annotate a diff:
|
|
1019
1159
|
|
|
@@ -1038,6 +1178,8 @@ every finding, so a bot can annotate a diff:
|
|
|
1038
1178
|
}
|
|
1039
1179
|
```
|
|
1040
1180
|
|
|
1181
|
+
### What a pull request changed
|
|
1182
|
+
|
|
1041
1183
|
A check says what is wrong. `spec-graph diff` says what a pull request changed:
|
|
1042
1184
|
documents added, removed, moved or accepted, relations added or removed, and
|
|
1043
1185
|
obligations resolved or reopened. It compares two exports made by the same
|
|
@@ -1050,15 +1192,20 @@ binary, so CI makes one from the base branch and one from the pull request:
|
|
|
1050
1192
|
- run: npx spec-graph diff base.json head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
1051
1193
|
```
|
|
1052
1194
|
|
|
1053
|
-
It names only what it can tell apart
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1195
|
+
It names only what it can tell apart:
|
|
1196
|
+
|
|
1197
|
+
- A document keeps its id through a rename, so a moved document reads as moved.
|
|
1198
|
+
- An obligation's id is its position in its section, so inserting a question
|
|
1199
|
+
renumbers every question below it. An obligation is therefore paired only by
|
|
1200
|
+
an id its author declared, or by its document, section and title when nothing
|
|
1201
|
+
else shares them.
|
|
1202
|
+
- Anything unpaired is reported as having appeared or disappeared, never as
|
|
1203
|
+
resolved or reopened.
|
|
1204
|
+
- Findings are left to `--baseline`, which already reports what is new.
|
|
1205
|
+
|
|
1206
|
+
The exit code is `0` whether anything changed or not: a diff describes, and
|
|
1207
|
+
`check` gates. See
|
|
1208
|
+
[ADR-0020](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0020-a-diff-names-what-it-can-tell-apart.md).
|
|
1062
1209
|
|
|
1063
1210
|
## Visualising
|
|
1064
1211
|
|
|
@@ -1130,9 +1277,9 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
|
|
|
1130
1277
|
than installed. So the whole package is auditable in an afternoon — and nothing
|
|
1131
1278
|
in the pipeline can take longer than its input.
|
|
1132
1279
|
|
|
1133
|
-
**Verified, not just covered.** 1,
|
|
1280
|
+
**Verified, not just covered.** 1,880 tests; 99.4% statement and 99.8% line
|
|
1134
1281
|
coverage. Coverage says a line ran, so the suite is also held to a mutation
|
|
1135
|
-
score - 96.
|
|
1282
|
+
score - 96.32% over 8,393 mutants, measured by CI's full sweep, and a build
|
|
1136
1283
|
fails under 93 - because a vocabulary entry or a boundary
|
|
1137
1284
|
condition can be weakened by an ordinary-looking refactor without a single test
|
|
1138
1285
|
going red. [ADR-0007](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0007-mutation-testing.md) is straight about what
|
|
@@ -1157,6 +1304,14 @@ about it.
|
|
|
1157
1304
|
consistent with *each other*. They share conventions and a philosophy, compose
|
|
1158
1305
|
cleanly, and neither requires the other.
|
|
1159
1306
|
|
|
1307
|
+
## Contributing
|
|
1308
|
+
|
|
1309
|
+
How to build, test and release spec-graph, and where a change usually goes, is
|
|
1310
|
+
in [CONTRIBUTING.md](https://github.com/DescentVTT/spec-graph/blob/main/CONTRIBUTING.md).
|
|
1311
|
+
Report a vulnerability privately, as
|
|
1312
|
+
[SECURITY.md](https://github.com/DescentVTT/spec-graph/blob/main/SECURITY.md)
|
|
1313
|
+
describes, and not in a public issue.
|
|
1314
|
+
|
|
1160
1315
|
## Licence
|
|
1161
1316
|
|
|
1162
1317
|
MIT
|