@descent-vtt/spec-graph 0.9.10 → 0.10.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 +68 -2
- package/README.md +522 -319
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +112 -67
- package/dist/cli.js.map +1 -1
- package/dist/extract.d.ts +14 -1
- package/dist/extract.js +114 -10
- 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.d.ts +11 -1
- package/dist/lifecycle.js +187 -28
- 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 +10 -2
- package/dist/sections.js.map +1 -1
- package/dist/state.js +19 -3
- package/dist/state.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,18 @@ 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, in English or in Chinese
|
|
137
|
+
(`## 狀態` / `已接受`), and however it is dressed (`**Accepted** (2026-03-01) ✅`):
|
|
138
|
+
|
|
139
|
+
- front matter: `status:`, `state:`, `stage:`
|
|
140
|
+
- a `## Status` section
|
|
141
|
+
- a `| Status | Accepted |` table at the top
|
|
142
|
+
- a `docs/adr/archive/` directory
|
|
143
|
+
|
|
144
|
+
`Accepted, later superseded by ADR-0009` is retired: retirement is terminal, so
|
|
145
|
+
a retirement word anywhere wins.
|
|
146
|
+
|
|
147
|
+
Front matter is read as YAML reads it, and a value YAML would not read -
|
|
133
148
|
`status: Superseded by ADR-9: see notes`, where a plain value cannot hold `: ` -
|
|
134
149
|
is left out rather than guessed at. Every run says how many statuses, ids and
|
|
135
150
|
relations it left out that way, and `--verbose` says where and why: quote the
|
|
@@ -152,21 +167,66 @@ that `provisionally accepted` is a draft.
|
|
|
152
167
|
| `active` | `accepted`, `active`, `approved`, `adopted`, `agreed`, `implementable`, `implemented`, `implementing`, `current`, `effective`, `in-effect`, `enforced`, `stable`, `merged`, `released`, `shipped`, `live`, `done`, `complete`, `completed`, `signed-off`, `committed` |
|
|
153
168
|
| `draft` | `draft`, `drafting`, `proposed`, `proposal`, `provisional`, `provisionally-accepted`, `prospective`, `wip`, `work-in-progress`, `in-progress`, `in_progress`, `review`, `in-review`, `under-review`, `reviewing`, `discussion`, `discussing`, `pending`, `idea`, `exploratory`, `candidate`, `open`, `new`, `unreviewed`, `rfc`, `experimental`, `alpha`, `beta`, `incubating` |
|
|
154
169
|
|
|
170
|
+
Chinese is read in Traditional and Simplified characters, each word as the
|
|
171
|
+
English word it translates, so it lands in that word's row: `延後` is
|
|
172
|
+
`deferred` and retired, `封存` is `archived` and a record, and `暫定` is
|
|
173
|
+
`provisionally accepted`, so `暫定接受` is a draft.
|
|
174
|
+
|
|
175
|
+
- **A Chinese word counts wherever it stands in the status**, because Chinese
|
|
176
|
+
puts no space between words: `已接受,後被ADR-0003取代` holds `已接受` and a
|
|
177
|
+
supersession.
|
|
178
|
+
- **Except directly after a negation:** `不`, `未`, `非`, `沒`, `没`, `無`,
|
|
179
|
+
`无`, `勿`, `尚未` or `不再`. `未接受`, `尚未核准` and `不再生效` say nothing;
|
|
180
|
+
a word that starts with one, `不採納`, is itself.
|
|
181
|
+
- **Superseded is also `被` followed by `取代`, `替代` or `取而代之`**, within 30
|
|
182
|
+
characters and in the same sentence: `已被 ADR-0003 取代` is retired, and
|
|
183
|
+
names ADR-0003 as what replaced it.
|
|
184
|
+
- **`已取代` alone is retired.** Followed by a name, after spaces or a colon -
|
|
185
|
+
`已取代 ADR-0002` - it says what this document replaced, and is not read.
|
|
186
|
+
- **`取代` with no `被`**, as in `已接受(取代 ADR-0002)`, retires nothing.
|
|
187
|
+
|
|
188
|
+
| Phase | Chinese status words |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `retired` | `已被取代`, `被取代`, `已取代`, `已棄用`, `棄用`, `已弃用`, `弃用`, `已廢棄`, `廢棄`, `已废弃`, `废弃`, `已停用`, `已過時`, `已过时`, `已否決`, `否決`, `已否决`, `否决`, `已拒絕`, `已拒绝`, `不採納`, `不采纳`, `已撤回`, `撤回`, `已作廢`, `作廢`, `已作废`, `作废`, `延後`, `延后`, `暫緩`, `暂缓`, `擱置`, `搁置` |
|
|
191
|
+
| `record` | `封存`, `已封存`, `歸檔`, `已歸檔`, `归档`, `已归档` |
|
|
192
|
+
| `frozen` | `已定案`, `定案`, `已凍結`, `已冻结` |
|
|
193
|
+
| `active` | `已接受`, `接受`, `已採納`, `採納`, `已采纳`, `采纳`, `已核准`, `核准`, `已批准`, `批准`, `已生效`, `生效`, `已實施`, `已实施`, `已完成` |
|
|
194
|
+
| `draft` | `暫定`, `暂定`, `草稿`, `草案`, `提議`, `提议`, `提案`, `審查中`, `审查中`, `審核中`, `审核中`, `討論中`, `讨论中`, `待審`, `待审`, `待審核`, `待审核` |
|
|
195
|
+
|
|
155
196
|
| Where | Read from |
|
|
156
197
|
| --- | --- |
|
|
157
|
-
| 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
|
|
198
|
+
| Front matter, the first key present | `status`, `狀態`, `状态`, `state`, `stage`, `lifecycle`, `phase`, `adr-status` |
|
|
199
|
+
| A section, under the heading, with or without a colon, `:` or `:` | `Status`, `State`, `Stage`, `Lifecycle`, `Current status`, `狀態`, `状态` |
|
|
200
|
+
| 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
201
|
| A directory, for a document that declares no status | `archive`, `archived`, `attic`, `deprecated`, `graveyard`, `historical`, `obsolete`, `rejected`, `retired`, `superseded`, `superceded`, `withdrawn` |
|
|
160
202
|
|
|
203
|
+
In front matter only an ASCII colon ends a key, as YAML has it: `狀態:已接受`
|
|
204
|
+
there is a line left unread, where a heading or a label takes either colon.
|
|
205
|
+
|
|
206
|
+
The table is where many teams put a document's fields, `| 狀態 | 已接受 |` or
|
|
207
|
+
`| Status | Accepted |`:
|
|
208
|
+
|
|
209
|
+
- It is read where a `## Status` section is, after one: a document with both is
|
|
210
|
+
read from the section, and front matter still wins over either.
|
|
211
|
+
- A key is compared whole, in any case, emphasis stripped.
|
|
212
|
+
- A table with more columns, one under a heading below the title, and one
|
|
213
|
+
spec-graph reads as a register are never read for the document's status: a
|
|
214
|
+
legend or a register describes other things.
|
|
215
|
+
|
|
161
216
|
</details>
|
|
162
217
|
|
|
163
218
|
`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
|
-
|
|
219
|
+
it has closed, and every spec-* tool reads it as a **record**:
|
|
220
|
+
|
|
221
|
+
- depending on it is normal;
|
|
222
|
+
- its unticked boxes are not anyone's work;
|
|
223
|
+
- its links are still checked;
|
|
224
|
+
- new work handed to it is still a ghost handover.
|
|
225
|
+
|
|
226
|
+
See [Journals, changelogs and minutes](#journals-changelogs-and-minutes). Unlike
|
|
227
|
+
a journal, it still answers for what it claims: a supersession it declares, a
|
|
169
228
|
cycle it closes and a link to itself are reported as they would be anywhere.
|
|
229
|
+
|
|
170
230
|
To retire a decision, say `superseded` or `deprecated` -
|
|
171
231
|
`archived, superseded by ADR-0009` is retired - and a document under an
|
|
172
232
|
`archive/` directory that says nothing about itself is still retired.
|
|
@@ -215,33 +275,42 @@ code. *"We resolved to keep the queue"* does not close anything.
|
|
|
215
275
|
| Disposition | Checkboxes | Markers | `state=` |
|
|
216
276
|
| --- | --- | --- | --- |
|
|
217
277
|
| `unresolved` | `[ ]` `[?]` `[!]` `[*]` | `unresolved`, `still open`, `open question`, `tbd`, `undecided`, `reopened` | `open`, `unresolved`, `todo` |
|
|
218
|
-
| `narrowed` | `[~]` `[/]` | `partially resolved`, `partially answered`, `partially done`, `partly resolved`, `narrowed`, `narrowed to`, `scoped down`, `reduced scope`, `reduced to`, `partial`, `in progress`, `in_progress`, `remaining`, `split
|
|
219
|
-
| `delegated` | | `delegated`, `delegated to`, `moved to`, `tracked in`, `tracked by`, `handed off`, `handed off to`, `handed to`, `deferred to`, `follow up in`, `follow-up in`, `followup in`, `continued in`, `owned by
|
|
220
|
-
| `satisfied` | `[x]` `[X]` `[+]` | `resolved`, `answered`, `decided`, `settled`, `done`, `complete`, `completed`, `closed`, `fixed`, `shipped`, `addressed`, `implemented`, `solved`, `confirmed
|
|
221
|
-
| `accepted-debt` | | `accepted debt`, `accepted as debt`, `accepted risk`, `known limitation`, `known issue`, `known gap`, `technical debt`, `tech debt`, `wontfix`, `won't fix`, `won’t fix`, `will not fix`, `by design`, `tolerated`, `living with it
|
|
278
|
+
| `narrowed` | `[~]` `[/]` | `partially resolved`, `partially answered`, `partially done`, `partly resolved`, `narrowed`, `narrowed to`, `scoped down`, `reduced scope`, `reduced to`, `partial`, `in progress`, `in_progress`, `remaining`, `split`, `部分解決`, `部分解决` | `narrowed`, `partial` |
|
|
279
|
+
| `delegated` | | `delegated`, `delegated to`, `moved to`, `tracked in`, `tracked by`, `handed off`, `handed off to`, `handed to`, `deferred to`, `follow up in`, `follow-up in`, `followup in`, `continued in`, `owned by`, `已移交` | `delegated` |
|
|
280
|
+
| `satisfied` | `[x]` `[X]` `[+]` | `resolved`, `answered`, `decided`, `settled`, `done`, `complete`, `completed`, `closed`, `fixed`, `shipped`, `addressed`, `implemented`, `solved`, `confirmed`, `已解決`, `已解决`, `已決定`, `已决定`, `已定案` | `satisfied`, `resolved`, `done`, `closed` |
|
|
281
|
+
| `accepted-debt` | | `accepted debt`, `accepted as debt`, `accepted risk`, `known limitation`, `known issue`, `known gap`, `technical debt`, `tech debt`, `wontfix`, `won't fix`, `won’t fix`, `will not fix`, `by design`, `tolerated`, `living with it`, `技術債`, `技术债`, `已知限制` | `accepted-debt`, `debt`, `wontfix` |
|
|
222
282
|
| `rejected` | `[-]` | `rejected`, `declined`, `dropped`, `not doing`, `will not do`, `abandoned`, `cancelled`, `canceled`, `withdrawn` | `rejected`, `declined` |
|
|
223
283
|
| `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
284
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
285
|
+
Where a marker counts, and how it may be written:
|
|
286
|
+
|
|
287
|
+
- A marker counts where a statement starts: at the start of the item or of a
|
|
288
|
+
line, after the end of a sentence - a Chinese one, `。`, `?`, `!` or `;`,
|
|
289
|
+
with a space after it or none - or after `(` or `[`.
|
|
290
|
+
- Punctuated means followed by `:`, `:`, `-`, `–`, `—` or `.`, so `Done.`
|
|
291
|
+
closes an item; emphasised means the emphasis closes on the marker, so
|
|
292
|
+
`**Closed beta**` does not.
|
|
293
|
+
- A marker of more than one word may be spaced, hyphenated or joined by an
|
|
294
|
+
underscore, whichever the table has: `**Tech-debt:**` and `TECH_DEBT:` are
|
|
295
|
+
`tech debt`. An apostrophe may be straight or curly.
|
|
296
|
+
- A Chinese marker is the English one it translates, and has no case to shout
|
|
297
|
+
in: `已決定:` and `**已決定**` close an item, and `我們已決定採用` does not.
|
|
232
298
|
|
|
233
299
|
</details>
|
|
234
300
|
|
|
235
301
|
Without a box, a bullet is an obligation only at the top level of a section
|
|
236
302
|
whose heading names open work, however the heading is cased, spaced, emphasised
|
|
237
303
|
or punctuated - `## ❓ **Open questions:**` is one. The section ends at the next
|
|
238
|
-
heading of its level.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`
|
|
304
|
+
heading of its level. Every other bullet is the document's own prose.
|
|
305
|
+
|
|
306
|
+
**Headings that hold obligations:** `Open Questions`, `Open Question`,
|
|
307
|
+
`Unresolved Questions`, `Unanswered Questions`, `Questions`, `Open Issues`,
|
|
308
|
+
`Action Items`, `Actions`, `TODO`, `To Do`, `To-Do`, `TODOs`, `Next Steps`,
|
|
309
|
+
`Follow Ups`, `Follow-Ups`, `Followups`, `Follow Up`, `Follow-Up`,
|
|
242
310
|
`Unresolved`, `Outstanding`, `Outstanding Questions`, `Tasks`, `Task List`,
|
|
243
311
|
`Work Items`, `Remaining Work`, `Decisions Needed`, `Blockers`, `Parking Lot`,
|
|
244
|
-
`Future Work`, `Deferred
|
|
312
|
+
`Future Work`, `Deferred`, `待決事項`, `待决事项`, `未決問題`, `未决问题`,
|
|
313
|
+
`待辦事項`, `待办事项`, `後續工作`, `后续工作`.
|
|
245
314
|
|
|
246
315
|
### References, resolved forgivingly and validated strictly
|
|
247
316
|
|
|
@@ -260,15 +329,16 @@ trusts:
|
|
|
260
329
|
- **Bare identifiers in prose are opportunistic.** They are reported only when
|
|
261
330
|
their family already exists in the corpus. `ADR-0099` in a repository of ADRs
|
|
262
331
|
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
|
-
|
|
332
|
+
- A one-letter prefix is never a family - `Q3`, `x86`, `p99` - and nor are
|
|
333
|
+
the prefixes of versions, pages, protocols and platforms, even in a
|
|
334
|
+
repository that has a family of that name.
|
|
335
|
+
- **Never a citation:** `ver 3`, `version 4`, `pp.12`, `Fig.3`, `Figure 4`,
|
|
336
|
+
`Table 2`, `tbl 1`, `section 4`, `sect 2`, `sec 5`, `step 2`, `item 3`,
|
|
337
|
+
`No.5`, `num 7`, `line 42`, `ln 12`, `col 3`, `port 8080`, `PR 45`,
|
|
338
|
+
`issue 42`, `GH-123`, `UTF-8`, `ASCII 7`, `SHA-256`, `MD5`, `HTTP 2`,
|
|
339
|
+
`HTTPS 2`, `IPv6`, `IPv4 32`, `IPv6 128`, `TLS 1.3`, `SSL 3`, `ES2015`,
|
|
340
|
+
`ECMA-262`, `p99 250`, `base64`, `SQL-92`, `ARM64`, `x86-64`, `Win32`,
|
|
341
|
+
`Node 22`, `Python 3`, `Java 21`, `Go 1.22`, `cpp20`.
|
|
272
342
|
- **Bare numbers are family-scoped.** `0007` inside an RFC means `RFC-0007`,
|
|
273
343
|
never `ADR-0007`. A repository with both is ordinary, and guessing would be
|
|
274
344
|
worse than silence. A document's family is the prefix of its identifier, or
|
|
@@ -283,66 +353,20 @@ trusts:
|
|
|
283
353
|
so `[ADR-7](https://example.com/adr-7)` yields one reference rather than three.
|
|
284
354
|
- **Source links are not spec links.** `[rules.ts](../../src/rules.ts)` is not a
|
|
285
355
|
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).
|
|
356
|
+
Nor is a link to:
|
|
357
|
+
- any other file that is not a document: an image, a dotfile such as
|
|
358
|
+
`../.nvmrc`, or a drawing such as `arch.excalidraw`, however long its
|
|
359
|
+
extension. A dot followed by a digit, as in `ADR-0007.1`, is part of an
|
|
360
|
+
identifier and is still resolved.
|
|
361
|
+
- a directory, `../guides/` or `../guides` alike - without the slash the disk
|
|
362
|
+
says which it is, and a name with no slash at all, `guides`, is an
|
|
363
|
+
identifier.
|
|
364
|
+
|
|
365
|
+
A document is named by an explicit `id:` (or `adr:`, `adr-id:`, `rfc:`,
|
|
366
|
+
`rfc-id:`, `kep-number:`, `number:` or `slug:`), then by its file name, then by
|
|
367
|
+
its H1, and the H1 is read as a name only when neither of the others gave one.
|
|
368
|
+
A row of a register is named by its ID column instead: see
|
|
369
|
+
[Registers](#registers-many-specifications-in-one-file).
|
|
346
370
|
|
|
347
371
|
### When `[[...]]` tags a concept
|
|
348
372
|
|
|
@@ -374,7 +398,7 @@ still a relation in the graph no matter what you exclude — not even
|
|
|
374
398
|
`--ignore-ref "*"` can delete one. See
|
|
375
399
|
[ADR-0008](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0008-wiki-links-carry-no-path.md).
|
|
376
400
|
|
|
377
|
-
|
|
401
|
+
### A gap that is deliberate
|
|
378
402
|
|
|
379
403
|
Real filing histories have holes: an ADR number reserved and then withdrawn, a
|
|
380
404
|
document that was folded into another before it was ever committed. Something
|
|
@@ -447,27 +471,178 @@ Blocked by [ADR-0009](0009-compliance.md). → blocked-by
|
|
|
447
471
|
|
|
448
472
|
A phrase governs a reference when it stands no more than 40 characters before
|
|
449
473
|
it, in the same statement: a sentence, a paragraph, a list item or a table cell.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
474
|
+
|
|
475
|
+
- A negation between the two - `owned by nobody` - cancels it.
|
|
476
|
+
- Prose reads both directions too: `supersedes` and `superseded by`, `amends`
|
|
477
|
+
and `amended by`, `blocked by` and `blocks` or `blocking`, `depends on` and
|
|
478
|
+
`depended on by` - the second of each pair points back at the document that
|
|
479
|
+
wrote it.
|
|
480
|
+
- A phrase that follows the reference makes the reference its subject -
|
|
481
|
+
`[ADR-0009](0009.md) supersedes this` - except a `depends-on` or `assumes`
|
|
482
|
+
phrase: `[ADR-0002](0002.md) requires a migration` says nothing about this
|
|
483
|
+
document.
|
|
484
|
+
|
|
485
|
+
In Chinese a phrase before the reference hands the work on, as `deferred to`
|
|
486
|
+
does. **Chinese hand-off phrases:** `延後至`, `延后至`, `延至`, `移交至`,
|
|
487
|
+
`移交給`, `移交给`, `留待`, `交由`, `併入`, `并入`.
|
|
488
|
+
|
|
489
|
+
- Chinese puts no space between words, so one counts wherever it stands,
|
|
490
|
+
`此問題延後至ADR-0002`, except directly after a negation - `不`, `未`, `非`,
|
|
491
|
+
`沒`, `没`, `無`, `无`, `勿`, `尚未` or `不再` - so `未移交給 ADR-0002` hands
|
|
492
|
+
nothing on.
|
|
493
|
+
- An English phrase counts beside Chinese as well, `此問題deferred to ADR-0002`:
|
|
494
|
+
a Han character is not part of an English word.
|
|
495
|
+
- A statement also ends at `。`, `?`, `!` and `;`, with a space after it or
|
|
496
|
+
none, so `上次延後至別處。另見 ADR-0002` hands nothing to ADR-0002.
|
|
497
|
+
- A phrase written around the reference, `由 ADR-0002 決定` or
|
|
498
|
+
`在 ADR-0002 中追蹤`, is not read, and neither are `根據`, `依據` and `基於`:
|
|
499
|
+
Chinese uses them for any citation, and an `assumes` read from one would make
|
|
500
|
+
a stale premise of it.
|
|
457
501
|
|
|
458
502
|
A link under a heading that files it as bookkeeping is `relates-to` unless a
|
|
459
503
|
phrase beside it says more. **Bookkeeping headings:** `See also`, `References`,
|
|
460
504
|
`Reference`, `Related`, `Related work`, `Related decisions`,
|
|
461
505
|
`Related documents`, `Links`, `Further reading`, `Prior art`, `History`,
|
|
462
506
|
`Changelog`, `Change log`, `Revision history`, `Bibliography`, `Sources`,
|
|
463
|
-
`Appendix`, `More information`, `Resources`, `Index
|
|
507
|
+
`Appendix`, `More information`, `Resources`, `Index`, `參考資料`, `参考资料`,
|
|
508
|
+
`相關文件`, `相关文件`, `延伸閱讀`, `延伸阅读`, `附錄`, `附录`, `修訂紀錄`, `修订记录`.
|
|
509
|
+
|
|
510
|
+
Two properties do the work:
|
|
464
511
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
512
|
+
- `loadBearing` relations (`assumes`, `depends-on`, `blocked-by`, `amends`) make
|
|
513
|
+
the source's validity depend on the target — those are what turn stale.
|
|
514
|
+
- `transfersObligation` relations (`delegates-to`, `blocked-by`) move work —
|
|
515
|
+
those are what become ghosts.
|
|
516
|
+
|
|
517
|
+
A "See also" link is neither, and is never reported, which is what keeps the
|
|
518
|
+
signal-to-noise ratio high enough to leave the tool switched on.
|
|
519
|
+
|
|
520
|
+
### Registers: many specifications in one file
|
|
521
|
+
|
|
522
|
+
One file per decision is the MADR, KEP and RFC layout, and it is what most
|
|
523
|
+
repositories do. Plenty keep a register instead — one document holding dozens of
|
|
524
|
+
decisions. **A specification is a region of a file, not a file**, so both work:
|
|
525
|
+
|
|
526
|
+
```md
|
|
527
|
+
## ADR-0007: Shard the write path
|
|
528
|
+
|
|
529
|
+
**Status:** Superseded by ADR-0012
|
|
530
|
+
|
|
531
|
+
Shard by tenant id.
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Each section with an identifier *and* a status - a `**Status:**` line,
|
|
535
|
+
`**狀態:**` in Chinese, or a `### Status` section under its heading - becomes a
|
|
536
|
+
specification in its own right, with its own lifecycle, its own obligations and
|
|
537
|
+
its own relations — resolvable from anywhere in the corpus as `ADR-0007`,
|
|
538
|
+
whether it lives in a register or in its own file. Moving it out later breaks
|
|
539
|
+
no citation.
|
|
540
|
+
|
|
541
|
+
A register kept as a table works the same way, with relations typed by the column
|
|
542
|
+
header you wrote:
|
|
543
|
+
|
|
544
|
+
```md
|
|
545
|
+
| ID | Title | Status | Depends on | Superseded by |
|
|
546
|
+
| :------- | :------------- | :--------- | :--------- | :------------ |
|
|
547
|
+
| ADR-0001 | Use one writer | Superseded | - | ADR-0003 |
|
|
548
|
+
| ADR-0002 | Cache eviction | Accepted | ADR-0001 | - |
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Findings point at the declaring cell, not at the file. A cell that is empty or
|
|
552
|
+
holds only a placeholder — `-`, `—`, `n/a`, `none`, `nil`, `TBD` — says nothing.
|
|
553
|
+
A status cell is read as a status line under a heading is, so
|
|
554
|
+
`Superseded by ADR-0003` there names the row's successor.
|
|
555
|
+
|
|
556
|
+
A row is named by its ID column, and a title is only a title:
|
|
557
|
+
|
|
558
|
+
- A row whose ID cell names nothing — empty, a placeholder, or a `?` nobody has
|
|
559
|
+
numbered yet — is not a specification.
|
|
560
|
+
- An identifier a title opens with belongs to the document the row is *about* —
|
|
561
|
+
an issue reading `| OI-V-05 | ADR-040's enforcement point has no browser test |`
|
|
562
|
+
is filed under `OI-V-05`, and citations of `ADR-040` still reach `ADR-040`.
|
|
563
|
+
|
|
564
|
+
The same holds for a whole file, whose H1 is a name only when nothing else gave
|
|
565
|
+
one: see [References](#references-resolved-forgivingly-and-validated-strictly).
|
|
566
|
+
|
|
567
|
+
The register itself stays in the graph as the thing that holds them, so
|
|
568
|
+
`spec-graph query 'document[id=register] -contains-> document'` lists what is
|
|
569
|
+
inside it.
|
|
570
|
+
|
|
571
|
+
Both forms need **two** signals, and that is deliberate: `## Q3 2026 Roadmap`
|
|
572
|
+
parses as family `Q`, number 3, and `## v1.2.0` in a changelog parses as family
|
|
573
|
+
`v`, number 1. Neither declares a status, so neither is a specification. A table
|
|
574
|
+
of identifiers and prose is a citation list, not a register. See
|
|
575
|
+
[ADR-0009](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0009-a-specification-is-a-region.md).
|
|
576
|
+
|
|
577
|
+
## Directives
|
|
578
|
+
|
|
579
|
+
Inference covers almost everything, but inference you cannot override is a trap.
|
|
580
|
+
Directives are ordinary HTML comments — invisible in every Markdown renderer:
|
|
581
|
+
|
|
582
|
+
```md
|
|
583
|
+
<!-- @spec-node id="ADR-0007" status="accepted" aliases="sharding, adr-7" -->
|
|
584
|
+
<!-- @spec-item id="shard-key" state="narrowed" -->
|
|
585
|
+
<!-- @spec-edge kind="delegates-to" to="ADR-0011#scope" -->
|
|
586
|
+
<!-- @spec-ignore -->
|
|
587
|
+
<!-- @spec-history -->
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Each takes these attributes. Any other is a parse problem, listed by
|
|
591
|
+
`--verbose`, and the directive still applies:
|
|
592
|
+
|
|
593
|
+
| directive | attributes |
|
|
594
|
+
| --- | --- |
|
|
595
|
+
| `@spec-node` | `id`, `status`, `title`, `aliases`, `kind` |
|
|
596
|
+
| `@spec-item` | `id`, `state`, `title`, `owner`, `note` |
|
|
597
|
+
| `@spec-edge` | `kind`, `to`, `from`, `reason` |
|
|
598
|
+
| `@spec-ignore` | `reason` |
|
|
599
|
+
| `@spec-history` | none |
|
|
600
|
+
|
|
601
|
+
- `reason`, `note`, `owner` and a node's `kind` are for whoever reads the
|
|
602
|
+
source; spec-graph reads nothing from them.
|
|
603
|
+
- `aliases` splits at commas, semicolons and spaces.
|
|
604
|
+
- A value is quoted with either quote mark, or bare when it holds no space or
|
|
605
|
+
quote, and an attribute with no value is a flag.
|
|
606
|
+
- A directive is the first thing in its comment, so a comment that only
|
|
607
|
+
mentions one is a comment.
|
|
608
|
+
|
|
609
|
+
A directive always wins, and the report says the state came from a directive, so
|
|
610
|
+
an override is visible rather than mysterious.
|
|
611
|
+
|
|
612
|
+
A `@spec-item` annotates one item: the one directly below it, with nothing but
|
|
613
|
+
blank lines or other comments between, or else the one it is written on or
|
|
614
|
+
indented under. An id that two items end up with is a parse problem, listed by
|
|
615
|
+
`--verbose`, and the first item keeps it.
|
|
616
|
+
|
|
617
|
+
## Journals, changelogs and minutes
|
|
618
|
+
|
|
619
|
+
A 2024 journal noting *"decision deferred to ADR-002"* is not delegating
|
|
620
|
+
anything. It is reporting that somebody once did. When ADR-002 retires in 2026,
|
|
621
|
+
the note does not become a defect — there is nothing in it for anyone to fix.
|
|
622
|
+
|
|
623
|
+
Declare those files and spec-graph stops holding them to a lifecycle they never
|
|
624
|
+
had:
|
|
625
|
+
|
|
626
|
+
```json
|
|
627
|
+
{ "historyPatterns": ["**/JOURNAL_*.md", "archive/**"] }
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
or, for one file, `<!-- @spec-history -->` at the top of it. A brief whose
|
|
631
|
+
status is `archived` is a record already, with nothing declared, though not a
|
|
632
|
+
log: the supersessions, cycles and self-references it declares are still
|
|
633
|
+
checked.
|
|
634
|
+
|
|
635
|
+
| | |
|
|
636
|
+
| --- | --- |
|
|
637
|
+
| its links resolve | **still checked** — a broken link is broken whoever wrote it |
|
|
638
|
+
| its checkboxes | not obligations, and not in the headline count |
|
|
639
|
+
| its delegations | reports of what was said |
|
|
640
|
+
| work handed **into** it | **still checked** — a log will never act on it |
|
|
641
|
+
|
|
642
|
+
That last row is the point. Excluding the file with `--ignore` would have
|
|
643
|
+
silenced the whole lot, including the links, and a journal full of 404s is
|
|
644
|
+
exactly what this tool is for. See
|
|
645
|
+
[ADR-0011](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0011-a-record-is-not-a-specification.md).
|
|
471
646
|
|
|
472
647
|
## The query language
|
|
473
648
|
|
|
@@ -535,12 +710,10 @@ predicate is linear in the subject whatever the pattern — `^([A-Za-z0-9_]+[ ]?
|
|
|
535
710
|
against a fifty-four-character title takes `RegExp` 103 seconds and this 13
|
|
536
711
|
microseconds. It reads the usual syntax minus backreferences and lookaround,
|
|
537
712
|
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.
|
|
713
|
+
character pointed at. The automaton is spec-core's, the library the spec-* tools
|
|
714
|
+
share, copied into this package rather than installed, and so is the one
|
|
715
|
+
[globs](#patterns) are matched by. See
|
|
716
|
+
[ADR-0017](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0017-a-predicate-must-finish.md).
|
|
544
717
|
|
|
545
718
|
On a register, a region answers its file's front matter for every descriptive
|
|
546
719
|
key — `fm.owner` on a decision inside a register is the register's owner — while
|
|
@@ -612,74 +785,6 @@ spec-graph: .spec-graph.json: rules.owner: "{1.phse}" asks for an attribute
|
|
|
612
785
|
|
|
613
786
|
See [ADR-0016](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0016-a-query-needs-a-sentence.md).
|
|
614
787
|
|
|
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
788
|
## Adopting this on a repository that predates it
|
|
684
789
|
|
|
685
790
|
Fifty findings on day one is not a report, it is a decision to be ignored. So
|
|
@@ -736,64 +841,6 @@ the other case and stays optional, because a repository declares the path before
|
|
|
736
841
|
the first run records the file — `--verbose` says when it read nothing. Either
|
|
737
842
|
may be absolute. See [ADR-0012](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0012-a-baseline-is-a-ratchet.md).
|
|
738
843
|
|
|
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
844
|
## Configuration
|
|
798
845
|
|
|
799
846
|
Anything you repeat on every run belongs to the repository rather than to the
|
|
@@ -823,10 +870,15 @@ command. spec-graph reads the first of `.spec-graph.json`,
|
|
|
823
870
|
The file is looked for in the working directory and then upward, as far as the
|
|
824
871
|
repository — a directory holding `.git` — and **the directory holding it becomes
|
|
825
872
|
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
|
-
|
|
873
|
+
reports, byte for byte, because every path here is relative to the root.
|
|
874
|
+
|
|
875
|
+
- The nearest file wins.
|
|
876
|
+
- `--root` names the root yourself and turns discovery off.
|
|
877
|
+
- A path typed on the command line stays relative to where you typed it, brace
|
|
878
|
+
alternative by brace alternative: `{/docs,x}` typed in `sub` is `/docs` or
|
|
879
|
+
`sub/x`.
|
|
880
|
+
|
|
881
|
+
See [ADR-0018](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0018-the-configuration-file-is-the-root.md).
|
|
830
882
|
|
|
831
883
|
A flag always wins over the file, and list flags **add** to it rather than
|
|
832
884
|
replacing it — a `--ignore-ref` on the command line is one more exclusion, not a
|
|
@@ -839,15 +891,19 @@ it can be combined with any `--format` and leave the document on stdout intact.
|
|
|
839
891
|
— the lines under it in the terminal, `related` in JSON, `relatedLocations` in
|
|
840
892
|
SARIF — and is 8 unless set; `0` lists none.
|
|
841
893
|
|
|
842
|
-
A broken config stops the run with exit `2`, each problem named on stderr
|
|
894
|
+
A broken config stops the run with exit `2`, each problem named on stderr:
|
|
843
895
|
invalid JSON, a value of the wrong type, an unknown key, a rule that does not
|
|
844
|
-
compile.
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
896
|
+
compile. Why a mistake stops the run:
|
|
897
|
+
|
|
898
|
+
- A configuration that did not load checks a different repository than the one
|
|
899
|
+
configured, and would report *that* one as consistent: a `{1.phse}` for
|
|
900
|
+
`{1.phase}` in one rule's message is a green build over the files the
|
|
901
|
+
correctly spelled rule fails.
|
|
902
|
+
- An unknown key counts, for the same reason it is reported at all — a silently
|
|
903
|
+
ignored `ignoreReference` is a configuration that looks applied and is not.
|
|
904
|
+
|
|
905
|
+
`--no-config` checks on defaults instead, and the exit code then says which run
|
|
906
|
+
it was.
|
|
851
907
|
|
|
852
908
|
### Patterns
|
|
853
909
|
|
|
@@ -870,52 +926,76 @@ recorded on a Windows checkout holds in Linux CI:
|
|
|
870
926
|
| `!docs/drafts/**` | takes back what an earlier pattern matched; the last to match wins |
|
|
871
927
|
| `docs\adr\*.md` | the same as `docs/adr/*.md`: a `\` is a separator |
|
|
872
928
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
in
|
|
929
|
+
Three kinds of pattern stop the run with exit `2`, and the run names the
|
|
930
|
+
pattern, wherever it was written:
|
|
931
|
+
|
|
932
|
+
- a pattern that cannot mean a path under the root — an unclosed `[` or `{`,
|
|
933
|
+
`..`, a lone `.` or `./`, an empty one, or braces that give one of them,
|
|
934
|
+
`{./,docs}`;
|
|
935
|
+
- an extended glob, `+(a|b)`: write `{a,b}`;
|
|
936
|
+
- one too large to compile: braces that expand to more than 256 patterns, or a
|
|
937
|
+
pattern past 65,536 automaton states.
|
|
938
|
+
|
|
939
|
+
Globs - the patterns to check, `--ignore`, `--ignore-ref`, `--history` - are
|
|
940
|
+
matched by an automaton rather than by `RegExp`, because a glob with three stars
|
|
941
|
+
in it is enough to keep `RegExp` busy for two minutes over a long reference
|
|
942
|
+
target. See
|
|
943
|
+
[ADR-0017](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0017-a-predicate-must-finish.md).
|
|
944
|
+
|
|
945
|
+
A class never matches a `/`. A parenthesis with no `|` in its group is itself,
|
|
946
|
+
so `C++(notes).md` names that file. A `..` typed below the root,
|
|
947
|
+
`spec-graph "../*.md"` from `docs/deep`, is resolved against where you typed it;
|
|
948
|
+
one that climbs past the root is refused, and so is a pattern typed in a
|
|
949
|
+
directory whose name a pattern would read as syntax, such as `notes[1]`.
|
|
950
|
+
|
|
951
|
+
A bare name and a path read differently as an `--ignore`:
|
|
952
|
+
|
|
953
|
+
- A bare `--ignore` name, `--ignore drafts`, still prunes that directory at any
|
|
954
|
+
depth, even where a pattern starts inside it or names it.
|
|
955
|
+
- A `!` before one gives the name back, as a `.gitignore` line does:
|
|
956
|
+
`--ignore drafts --ignore '!drafts'` reads every `drafts`, and the last bare
|
|
957
|
+
name to name a directory decides.
|
|
958
|
+
- A path, `--ignore "docs/*"`, takes out what it matches wherever a pattern
|
|
959
|
+
starts: `docs/drafts/**/*.md` finds nothing under it, as `docs/**/*.md` finds
|
|
960
|
+
nothing in `docs/drafts`.
|
|
890
961
|
|
|
891
962
|
The walk never enters `.git`, `.hg`, `.svn`, `.cache`, `.next`, `.nuxt`,
|
|
892
963
|
`.turbo`, `.venv`, `node_modules`, `bower_components`, `vendor`, `dist`,
|
|
893
964
|
`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
|
-
|
|
965
|
+
patterns name. Unlike an `--ignore`, these yield:
|
|
966
|
+
|
|
967
|
+
- to a pattern that starts inside one: `vendor/specs/*.md` is read from there,
|
|
968
|
+
whatever the other patterns reach;
|
|
969
|
+
- to a plain name for one: `vendor` and `docs/vendor` read those directories
|
|
970
|
+
as `vendor/` and `docs/vendor/` do, or the file, where one of them is a file;
|
|
971
|
+
- to a `!` before their name: `--ignore '!vendor'` reads every `vendor`, the
|
|
972
|
+
rest of the list still skipped inside it. A `!` before a path takes back only
|
|
973
|
+
what a path `--ignore` took out, so `!docs/vendor` gives back nothing the walk
|
|
974
|
+
skips: name it among the patterns.
|
|
975
|
+
|
|
976
|
+
Nor does the walk follow a link, a pattern that starts beyond one included:
|
|
977
|
+
`docs/linked/*.md` reads nothing through the link `docs/linked`, as
|
|
978
|
+
`docs/**/*.md` reads nothing there. `followSymlinks` in the
|
|
979
|
+
[API](#programmatic-api) follows links, and:
|
|
980
|
+
|
|
981
|
+
- prunes a link to a directory as it prunes a directory of that name and path:
|
|
982
|
+
a linked `node_modules` is skipped, and an `--ignore` takes a link out;
|
|
983
|
+
- never follows a link back into a directory the walk is inside, so a cycle is
|
|
984
|
+
read once, and says nothing of it, as it says nothing of a link it does not
|
|
985
|
+
follow.
|
|
986
|
+
|
|
987
|
+
`--ignore-ref` is matched against reference targets rather than paths:
|
|
988
|
+
|
|
989
|
+
- a bare pattern names one target exactly;
|
|
990
|
+
- case is ignored on every host;
|
|
991
|
+
- a `\` escapes the next character;
|
|
992
|
+
- `.` and `..` are the text of the link, in a brace alternative as anywhere
|
|
993
|
+
else: `docs/{.,x}` names `docs/.` and `docs/x`, and `{./,docs}` the targets
|
|
994
|
+
under `./`, and `docs`;
|
|
995
|
+
- a leading `/` is the text of the link too: `{/docs/a.md,x}` names the target
|
|
996
|
+
`/docs/a.md`, as `/docs/a.md` does, and not `docs/a.md`.
|
|
997
|
+
|
|
998
|
+
See [ADR-0022](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0022-globs-are-the-family-path-dialect.md).
|
|
919
999
|
|
|
920
1000
|
### Family rules
|
|
921
1001
|
|
|
@@ -976,8 +1056,8 @@ spec-graph diff <before> <after> Compare two JSON graph exports.
|
|
|
976
1056
|
--record-baseline <f> Write today's findings as accepted debt, exit 0
|
|
977
1057
|
--ratchet Also fail when a baseline entry no longer occurs
|
|
978
1058
|
--no-config Ignore .spec-graph.json and the package.json key
|
|
979
|
-
--format <fmt> human | json | sarif (check) |
|
|
980
|
-
| markdown (check, diff)
|
|
1059
|
+
--format <fmt> human | json | sarif (check) | github (check)
|
|
1060
|
+
| gitlab (check) | markdown (check, diff)
|
|
981
1061
|
--graph-format <fmt> dot | mermaid | json
|
|
982
1062
|
--documents-only Hide items; their relations lift onto their documents
|
|
983
1063
|
--rule <id>=<severity> error | warn | info | off (repeatable)
|
|
@@ -994,13 +1074,110 @@ never masquerades as a passing build, and neither does a pattern that matches
|
|
|
994
1074
|
nothing, a configuration file that did not load, or a `--baseline` that is not
|
|
995
1075
|
there.
|
|
996
1076
|
|
|
997
|
-
|
|
1077
|
+
**There is no `--watch`.** A full run on this repository takes 60 ms, so the
|
|
1078
|
+
loop that would justify a resident process is one line of shell, and it belongs
|
|
1079
|
+
to whatever the developer already uses rather than to this tool:
|
|
1080
|
+
|
|
1081
|
+
```bash
|
|
1082
|
+
npx spec-graph --format json | jq -r '.summary'
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
## In CI
|
|
1086
|
+
|
|
1087
|
+
The exit code is the contract: `0` clean, `1` findings, `2` the tool could not
|
|
1088
|
+
run. That is all most pipelines need.
|
|
1089
|
+
|
|
1090
|
+
Beyond that, a format puts the findings where somebody will look:
|
|
1091
|
+
|
|
1092
|
+
| Format | Where the findings land |
|
|
1093
|
+
| --- | --- |
|
|
1094
|
+
| `--format sarif` | GitHub code scanning: comments on the changed lines |
|
|
1095
|
+
| `--format github` | annotations on the diff, from the job's log, with no upload step |
|
|
1096
|
+
| `--format gitlab` | the Code Quality report of a GitLab merge request |
|
|
1097
|
+
| `--format markdown` | a table for the job summary, where reviewers look |
|
|
1098
|
+
| `--format json` | a script or a bot of your own |
|
|
1099
|
+
|
|
1100
|
+
### Code scanning, with SARIF
|
|
1101
|
+
|
|
1102
|
+
For annotations on the diff rather than a line in a log, emit SARIF and hand it
|
|
1103
|
+
to the uploader that already exists. The upload needs `security-events: write`,
|
|
1104
|
+
and a job that declares its permissions has only those, so it names
|
|
1105
|
+
`contents: read` for its checkout as well:
|
|
998
1106
|
|
|
999
1107
|
```yaml
|
|
1000
|
-
|
|
1001
|
-
|
|
1108
|
+
permissions:
|
|
1109
|
+
contents: read
|
|
1110
|
+
security-events: write
|
|
1111
|
+
steps:
|
|
1112
|
+
# check out the repository and set up Node, then:
|
|
1113
|
+
- run: npx spec-graph --format sarif > spec-graph.sarif
|
|
1114
|
+
continue-on-error: true
|
|
1115
|
+
- uses: github/codeql-action/upload-sarif@v4
|
|
1116
|
+
with:
|
|
1117
|
+
sarif_file: spec-graph.sarif
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
Findings land as comments on the changed lines, and they persist across commits
|
|
1121
|
+
without drifting, because the `partialFingerprints` spec-graph writes are the
|
|
1122
|
+
same identity a baseline is keyed on — the rule, the specification, and the
|
|
1123
|
+
citation, with no line number in it (ADR-0012).
|
|
1124
|
+
|
|
1125
|
+
### Annotations, with `--format github`
|
|
1126
|
+
|
|
1127
|
+
Without code scanning, `--format github` writes one GitHub Actions workflow
|
|
1128
|
+
command per finding, and the job's log turns each into an annotation on the
|
|
1129
|
+
diff, with no upload step:
|
|
1130
|
+
|
|
1131
|
+
```yaml
|
|
1132
|
+
- run: npx spec-graph --format github
|
|
1002
1133
|
```
|
|
1003
1134
|
|
|
1135
|
+
```text
|
|
1136
|
+
::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
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
- An `error` finding is an `error`, a `warn` a `warning` and an `info` a
|
|
1140
|
+
`notice`, as spec-guard writes them, and one only `--strict` made an error is
|
|
1141
|
+
titled `ghost-handover (strict)`.
|
|
1142
|
+
- The message carries the hint.
|
|
1143
|
+
- `%` and line breaks are escaped everywhere, and `:` and `,` in the file and
|
|
1144
|
+
the title, as GitHub reads them.
|
|
1145
|
+
- A clean run writes nothing.
|
|
1146
|
+
|
|
1147
|
+
### Code Quality, with `--format gitlab`
|
|
1148
|
+
|
|
1149
|
+
On GitLab, `--format gitlab` writes the Code Quality report a merge request
|
|
1150
|
+
reads, and the merge request shows which findings it introduced and which it
|
|
1151
|
+
resolved:
|
|
1152
|
+
|
|
1153
|
+
```yaml
|
|
1154
|
+
spec-graph:
|
|
1155
|
+
script:
|
|
1156
|
+
- npx spec-graph --format gitlab > gl-code-quality-report.json
|
|
1157
|
+
artifacts:
|
|
1158
|
+
when: always
|
|
1159
|
+
reports:
|
|
1160
|
+
codequality: gl-code-quality-report.json
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
Each finding is an issue:
|
|
1164
|
+
|
|
1165
|
+
- `check_name` is its rule, `description` its message and hint, and `location`
|
|
1166
|
+
its file and line.
|
|
1167
|
+
- Severities map as `error` to `critical` - or `major` when only `--strict` made
|
|
1168
|
+
it an error - `warn` to `minor` and `info` to `info`.
|
|
1169
|
+
- The `fingerprint` is a SHA-256 of the rule, the specification and what within
|
|
1170
|
+
it - a citation's target, or the document at the other end - which is what a
|
|
1171
|
+
baseline counts by. There is no line in it, so a finding a paragraph moved is
|
|
1172
|
+
not reported as new, and no message, so ticking one of three open boxes does
|
|
1173
|
+
not make an `orphaned-obligation` new either; the second of two findings
|
|
1174
|
+
identical in all three is told apart by its order.
|
|
1175
|
+
|
|
1176
|
+
`when: always` keeps the report when the job fails, which it does, with exit
|
|
1177
|
+
`1`, whenever there is an error to report.
|
|
1178
|
+
|
|
1179
|
+
### A summary for reviewers, with `--format markdown`
|
|
1180
|
+
|
|
1004
1181
|
`--format markdown` writes the same facts as a table, for the place reviewers
|
|
1005
1182
|
actually look:
|
|
1006
1183
|
|
|
@@ -1014,6 +1191,17 @@ It leads with the verdict, counts the corpus, gives every finding its hint, and
|
|
|
1014
1191
|
*names* the baseline entries that no longer occur rather than counting them —
|
|
1015
1192
|
on a pull request a count is the one thing a reader cannot act on.
|
|
1016
1193
|
|
|
1194
|
+
### JSON, for anything of your own
|
|
1195
|
+
|
|
1196
|
+
`--format json` is the machine-readable output to parse if you are building
|
|
1197
|
+
something of your own: it carries the baseline note, the summary and the
|
|
1198
|
+
per-file detail that SARIF and Code Quality have nowhere to put.
|
|
1199
|
+
|
|
1200
|
+
```yaml
|
|
1201
|
+
- name: Check the specification graph
|
|
1202
|
+
run: npx spec-graph "docs/**/*.md" --format json > spec-graph.json
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1017
1205
|
The JSON report is versioned, flat, and carries start and end positions for
|
|
1018
1206
|
every finding, so a bot can annotate a diff:
|
|
1019
1207
|
|
|
@@ -1038,6 +1226,8 @@ every finding, so a bot can annotate a diff:
|
|
|
1038
1226
|
}
|
|
1039
1227
|
```
|
|
1040
1228
|
|
|
1229
|
+
### What a pull request changed
|
|
1230
|
+
|
|
1041
1231
|
A check says what is wrong. `spec-graph diff` says what a pull request changed:
|
|
1042
1232
|
documents added, removed, moved or accepted, relations added or removed, and
|
|
1043
1233
|
obligations resolved or reopened. It compares two exports made by the same
|
|
@@ -1050,15 +1240,20 @@ binary, so CI makes one from the base branch and one from the pull request:
|
|
|
1050
1240
|
- run: npx spec-graph diff base.json head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
1051
1241
|
```
|
|
1052
1242
|
|
|
1053
|
-
It names only what it can tell apart
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1243
|
+
It names only what it can tell apart:
|
|
1244
|
+
|
|
1245
|
+
- A document keeps its id through a rename, so a moved document reads as moved.
|
|
1246
|
+
- An obligation's id is its position in its section, so inserting a question
|
|
1247
|
+
renumbers every question below it. An obligation is therefore paired only by
|
|
1248
|
+
an id its author declared, or by its document, section and title when nothing
|
|
1249
|
+
else shares them.
|
|
1250
|
+
- Anything unpaired is reported as having appeared or disappeared, never as
|
|
1251
|
+
resolved or reopened.
|
|
1252
|
+
- Findings are left to `--baseline`, which already reports what is new.
|
|
1253
|
+
|
|
1254
|
+
The exit code is `0` whether anything changed or not: a diff describes, and
|
|
1255
|
+
`check` gates. See
|
|
1256
|
+
[ADR-0020](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0020-a-diff-names-what-it-can-tell-apart.md).
|
|
1062
1257
|
|
|
1063
1258
|
## Visualising
|
|
1064
1259
|
|
|
@@ -1130,9 +1325,9 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
|
|
|
1130
1325
|
than installed. So the whole package is auditable in an afternoon — and nothing
|
|
1131
1326
|
in the pipeline can take longer than its input.
|
|
1132
1327
|
|
|
1133
|
-
**Verified, not just covered.** 1,
|
|
1328
|
+
**Verified, not just covered.** 1,996 tests; 99.4% statement and 99.8% line
|
|
1134
1329
|
coverage. Coverage says a line ran, so the suite is also held to a mutation
|
|
1135
|
-
score - 96.
|
|
1330
|
+
score - 96.34% over 8,575 mutants, measured by CI's full sweep, and a build
|
|
1136
1331
|
fails under 93 - because a vocabulary entry or a boundary
|
|
1137
1332
|
condition can be weakened by an ordinary-looking refactor without a single test
|
|
1138
1333
|
going red. [ADR-0007](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0007-mutation-testing.md) is straight about what
|
|
@@ -1157,6 +1352,14 @@ about it.
|
|
|
1157
1352
|
consistent with *each other*. They share conventions and a philosophy, compose
|
|
1158
1353
|
cleanly, and neither requires the other.
|
|
1159
1354
|
|
|
1355
|
+
## Contributing
|
|
1356
|
+
|
|
1357
|
+
How to build, test and release spec-graph, and where a change usually goes, is
|
|
1358
|
+
in [CONTRIBUTING.md](https://github.com/DescentVTT/spec-graph/blob/main/CONTRIBUTING.md).
|
|
1359
|
+
Report a vulnerability privately, as
|
|
1360
|
+
[SECURITY.md](https://github.com/DescentVTT/spec-graph/blob/main/SECURITY.md)
|
|
1361
|
+
describes, and not in a public issue.
|
|
1362
|
+
|
|
1160
1363
|
## Licence
|
|
1161
1364
|
|
|
1162
1365
|
MIT
|