@descent-vtt/spec-graph 0.9.2 → 0.9.4
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 +154 -0
- package/README.md +165 -16
- package/dist/baseline.js +6 -0
- package/dist/baseline.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +47 -14
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -3
- package/dist/config.js +16 -8
- package/dist/config.js.map +1 -1
- package/dist/diff.js +14 -0
- package/dist/diff.js.map +1 -1
- package/dist/directives.js +9 -6
- package/dist/directives.js.map +1 -1
- package/dist/extract.js +116 -56
- package/dist/extract.js.map +1 -1
- package/dist/glob.js +28 -16
- package/dist/glob.js.map +1 -1
- package/dist/graph.d.ts +2 -1
- package/dist/graph.js +10 -1
- package/dist/graph.js.map +1 -1
- package/dist/identity.d.ts +6 -0
- package/dist/identity.js +68 -68
- package/dist/identity.js.map +1 -1
- package/dist/lifecycle.js +17 -11
- package/dist/lifecycle.js.map +1 -1
- package/dist/report.js +34 -5
- package/dist/report.js.map +1 -1
- package/dist/resolve.d.ts +8 -0
- package/dist/resolve.js +49 -16
- package/dist/resolve.js.map +1 -1
- package/dist/rules.js +101 -24
- package/dist/rules.js.map +1 -1
- package/dist/runner.d.ts +1 -0
- package/dist/runner.js +25 -5
- package/dist/runner.js.map +1 -1
- package/dist/sections.d.ts +9 -0
- package/dist/sections.js +60 -9
- package/dist/sections.js.map +1 -1
- package/dist/select.d.ts +18 -2
- package/dist/select.js +46 -16
- package/dist/select.js.map +1 -1
- package/dist/source.js +2 -0
- package/dist/source.js.map +1 -1
- package/dist/state.js +46 -12
- package/dist/state.js.map +1 -1
- package/dist/yaml.js +11 -3
- package/dist/yaml.js.map +1 -1
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,160 @@
|
|
|
3
3
|
Notable changes, newest first. Versions follow [semver](https://semver.org):
|
|
4
4
|
a patch fixes behaviour without asking anything of a repository that upgrades.
|
|
5
5
|
|
|
6
|
+
## 0.9.4
|
|
7
|
+
|
|
8
|
+
Three false alarms, each raised in review of 0.9.3's changes: a loop that
|
|
9
|
+
takes a supersession to close read as a delegation cycle, a question handed
|
|
10
|
+
on to a live document read as an orphaned obligation, and a link to a
|
|
11
|
+
directory without its trailing slash read as a reference outside the corpus.
|
|
12
|
+
On upgrading, expect those findings to go, and a finding for each kind
|
|
13
|
+
where one cycle held a loop of obligations and a loop of supersessions
|
|
14
|
+
together; under `--ratchet`, record the baseline once.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A loop that takes a supersession to close is not a delegation cycle.** A
|
|
19
|
+
document superseded twice over handing its question to the latest successor,
|
|
20
|
+
or a superseded document handing its question to a third that hands it on to
|
|
21
|
+
the successor, read as `delegation cycle ... nothing in it can ever land`.
|
|
22
|
+
The question lands on the successor. Obligations and supersessions are now
|
|
23
|
+
searched for cycles apart, and each finding names only its own relations.
|
|
24
|
+
- **A question a retired or frozen document delegated to a live document is
|
|
25
|
+
not an orphaned obligation.** It was counted as open work the document still
|
|
26
|
+
held, though the finding's hint - move it to a live document - had been
|
|
27
|
+
followed. Delegated into another sealed document, it is still counted.
|
|
28
|
+
- **A link to a directory without its trailing slash is not a reference.**
|
|
29
|
+
`[the guides](../guides)` warned that it "resolves to a file that is not a
|
|
30
|
+
specification" and suggested include patterns that could not help, while
|
|
31
|
+
`../guides/` was quiet. `analyseSources` takes an `isDirectory` option for
|
|
32
|
+
this; without it a directory link is a broken reference as before.
|
|
33
|
+
|
|
34
|
+
## 0.9.3
|
|
35
|
+
|
|
36
|
+
Thirteen defects, found by reading the mutation survivors of nineteen
|
|
37
|
+
modules rather than the score, and four approved changes that make the graph
|
|
38
|
+
report less that is not wrong. Among the defects: `Provisionally accepted` read as
|
|
39
|
+
active, `Superceded by` named no successor, `ES2015` and `ARM64` were
|
|
40
|
+
read as citations, `spec-graph query` printed transitive paths wrong, a
|
|
41
|
+
register could be retired by a decision inside it, and adding a pattern
|
|
42
|
+
could make a walk find fewer files. The changes: a link to a dotfile or a
|
|
43
|
+
file with a long extension is checked as a file, a superseded document
|
|
44
|
+
handing its question to its successor is not a delegation cycle, a table
|
|
45
|
+
register's status cell names its successor, and underscore, curly and dashed
|
|
46
|
+
spellings read as their plain forms. The README now lists every vocabulary
|
|
47
|
+
the graph reads, and a test holds each list to the code. On upgrading, expect
|
|
48
|
+
findings that were false to go and a few to arrive where a successor or a
|
|
49
|
+
status is now read; under `--ratchet`, record the baseline once.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **`Provisionally accepted` reads as a draft.** The vocabulary listed it
|
|
54
|
+
among the draft words, but `active` is read first and it contains
|
|
55
|
+
`accepted`, so it read as active and the entry never matched anything.
|
|
56
|
+
- **`Superceded by ADR-0009` names ADR-0009**, as `Superseded by ADR-0009`
|
|
57
|
+
does. The misspelling already retired the document; the pattern that reads
|
|
58
|
+
the successor out of a status spelled it `superseced`.
|
|
59
|
+
- **A status that wraps onto a second line still names its successor.** A
|
|
60
|
+
`@spec-node status="Superseded by ADR-0009, ..."` whose value ran onto
|
|
61
|
+
another line retired the document and recorded no supersession.
|
|
62
|
+
- **The `--ignore-ref` a concept tag's hint suggests silences it.** `[[trap-55]]`
|
|
63
|
+
suggested `--ignore-ref "trap *"`, which matches `trap 55` and not
|
|
64
|
+
`trap-55`; the glob keeps the separator now, `trap-*`, and `[[trap55]]`
|
|
65
|
+
suggests `trap*`.
|
|
66
|
+
- **A list item that opens a fence of five tildes is not struck through.**
|
|
67
|
+
`~~~~~` begins and ends with `~~`, and read as a strikethrough of `~`, so
|
|
68
|
+
the item read as done.
|
|
69
|
+
- **`spec-graph query` prints a path the way the selector reads.** A
|
|
70
|
+
transitive step printed the end of its path in the middle of it:
|
|
71
|
+
`document[id=ADR-0004] =supersedes=> document`, the README's own kind of
|
|
72
|
+
query, printed `ADR-0004 -supersedes-> ADR-0002 -supersedes-> ADR-0002` for
|
|
73
|
+
a chain through ADR-0003. A backward step printed as a relation pointing the
|
|
74
|
+
other way. Every edge now names the node it reaches, as `-kind->` or
|
|
75
|
+
`<-kind-` in the direction it was walked. `--format json` was always right.
|
|
76
|
+
- **A register row whose ID cell names nothing is not a specification.** A
|
|
77
|
+
row with only `?` in its ID cell, or a section whose `@spec-node` id is a
|
|
78
|
+
lone backslash, fell back to the file's own name: the file contained itself
|
|
79
|
+
and the row or section vanished. Such a row now stays part of the file, and
|
|
80
|
+
such a section keeps its heading's id.
|
|
81
|
+
- **A declared id keeps its zero padding when its family has a digit in it.**
|
|
82
|
+
`id: S3-0012` was reported as `S3-12`, because the padding was read from the
|
|
83
|
+
first run of digits, which is the family's. An id is now written with the
|
|
84
|
+
number as it was declared. Citations resolved either way.
|
|
85
|
+
- **A register is not retired by a decision inside it.** A register whose
|
|
86
|
+
sections kept their status under a `### Status` heading read the first
|
|
87
|
+
one as the file's own: a first decision `Superseded by ADR-0009` made the
|
|
88
|
+
register retired and superseded by ADR-0009, so open work in its
|
|
89
|
+
introduction was an `orphaned-obligation`. A status section inside a
|
|
90
|
+
section is that section's; the file's is one written above them all.
|
|
91
|
+
- **`ES2015`, `ARM64`, `Win32`, `base64` and `cpp20` are prose, not
|
|
92
|
+
citations**, and nor are `x86` and `p99`. Each was read with every digit
|
|
93
|
+
but the last in its prefix - `ES201` and 5 - and so walked past the list of
|
|
94
|
+
prefixes that are never a family and the rule that one letter is none; a
|
|
95
|
+
repository with an `ES` family was told `ES2015` did not resolve. The
|
|
96
|
+
prefix is now read as resolution reads it, `ES` and 2015. The README lists
|
|
97
|
+
what is never a citation.
|
|
98
|
+
- **What a walk skips no longer depends on the other patterns beside it.**
|
|
99
|
+
`docs/vendor/specs/*.md` found its files alone and none of them beside
|
|
100
|
+
`docs/**/*.md`: the broader walk never enters `vendor`, and the narrower
|
|
101
|
+
pattern's own starting point was dropped as already covered by it. And
|
|
102
|
+
`--ignore drafts` took out `docs/drafts/` for `docs/**/*.md` but not for
|
|
103
|
+
`docs/drafts/*.md`, whose walk started inside it. A pattern that starts
|
|
104
|
+
inside a directory the walk skips by default is read from there now,
|
|
105
|
+
whatever the other patterns reach, and a bare `--ignore` name prunes at any
|
|
106
|
+
depth, a pattern's starting point included.
|
|
107
|
+
- **`not_planned` and `in_progress` read as `not planned` and `in progress`.**
|
|
108
|
+
The underscore was stripped as emphasis and ran the two words into one, so
|
|
109
|
+
a status spelled that way was `unknown` and a marker spelled that way was
|
|
110
|
+
prose. Every status word and marker of more than one word may be joined by
|
|
111
|
+
an underscore now; an underscore around a word is still emphasis.
|
|
112
|
+
- **`Won’t fix:` with a curly apostrophe is accepted debt**, as `Won't fix:`
|
|
113
|
+
is, and **a marker followed by an en or em dash is punctuated**, as one
|
|
114
|
+
followed by a hyphen is: `Resolved – one node` closes the item. The pattern
|
|
115
|
+
already found both dashes and the check after it asked for a hyphen. A
|
|
116
|
+
status reading `Superseded – ADR-0009` names its successor.
|
|
117
|
+
- **A link to a dotfile or to a file with a long extension is not a broken
|
|
118
|
+
reference.** `../.nvmrc` counted as having no extension and
|
|
119
|
+
`arch.excalidraw` as an identifier with a dot in it, so both were read as
|
|
120
|
+
documents and reported when no document answered. Both are links to files
|
|
121
|
+
now, like a link to source code or an image. A link to a missing `.md` is
|
|
122
|
+
still reported, and `ADR-0007.1` is still an identifier.
|
|
123
|
+
- **A superseded document handing its question to its successor is not a
|
|
124
|
+
`circular-delegation`.** A document superseded by another that deferred its
|
|
125
|
+
open question to that other was reported as a delegation cycle across the
|
|
126
|
+
two, though nothing in it is passed back. Two documents delegating to each
|
|
127
|
+
other are still a cycle, as is a question handed anywhere but to the
|
|
128
|
+
document that superseded the one handing it.
|
|
129
|
+
- **A register row whose status cell says `Superseded by ADR-0002` is
|
|
130
|
+
superseded by ADR-0002**, as a section of a register kept under headings
|
|
131
|
+
already was. The status cell was skipped by prose scanning like a relation
|
|
132
|
+
column, though it types no relation, so the row was retired with no
|
|
133
|
+
successor.
|
|
134
|
+
- **A status section followed at once by a setext heading says nothing.** Its
|
|
135
|
+
text line starts with no `#`, so it read as the status: a register with
|
|
136
|
+
`### Status` above a decision underlined as a heading took that decision's
|
|
137
|
+
title, `ADR-0002: B`, for its own status. A line that opens a heading ends
|
|
138
|
+
the search now, whichever kind of heading it is; one that starts with `#`
|
|
139
|
+
and is not a heading is read.
|
|
140
|
+
- **A duplicate document id draws no containment between unrelated files.** A
|
|
141
|
+
register whose own id another file already had was drawn as that other
|
|
142
|
+
file containing the register's rows, and a row whose id another file had
|
|
143
|
+
was drawn inside the register as that other file. Containment is drawn only
|
|
144
|
+
between a file and a region of it the graph kept; the duplicate is still
|
|
145
|
+
reported as a parse problem.
|
|
146
|
+
|
|
147
|
+
### Documentation
|
|
148
|
+
|
|
149
|
+
- The README lists every status word, front-matter key, heading and directory
|
|
150
|
+
the lifecycle is read from, and every checkbox, marker and `state=` word an
|
|
151
|
+
item's state is, where it gave a few of each and an ellipsis.
|
|
152
|
+
- The README lists the directories a walk never enters, which it did not
|
|
153
|
+
name.
|
|
154
|
+
- The README lists the attributes each directive takes, where it showed a few
|
|
155
|
+
of them by example.
|
|
156
|
+
- `maxRelated`, the configuration key that caps how many related locations one
|
|
157
|
+
finding lists, is in the README and `--help`. It was read, and never
|
|
158
|
+
mentioned.
|
|
159
|
+
|
|
6
160
|
## 0.9.2
|
|
7
161
|
|
|
8
162
|
Three small fixes found when 0.9.1 was read as a user reads it: `rules
|
package/README.md
CHANGED
|
@@ -71,8 +71,8 @@ npx @descent-vtt/spec-graph "docs/**/*.md"
|
|
|
71
71
|
| `ghost-handover` | error | An open obligation handed to a document that can no longer absorb it |
|
|
72
72
|
| `stale-premise` | error | A live document resting on a decision that was retired or obviated |
|
|
73
73
|
| `broken-reference` | error | A citation naming a document or anchor that does not exist |
|
|
74
|
-
| `circular-delegation` | error | Obligations
|
|
75
|
-
| `orphaned-obligation` | error | A retired or frozen document still holding open work |
|
|
74
|
+
| `circular-delegation` | error | Obligations in a cycle, so none can ever land, or supersessions in a cycle - not a loop that takes one of each to close, such as a superseded document handing its question to what replaced it |
|
|
75
|
+
| `orphaned-obligation` | error | A retired or frozen document still holding open work - not a question it delegated to a document still taking work |
|
|
76
76
|
| `live-supersession` | error | A superseded document still presenting itself as current |
|
|
77
77
|
| `reference-outside-corpus` | warn | A citation naming a real document the include patterns did not reach |
|
|
78
78
|
| `ambiguous-reference` | warn | A citation matching more than one document |
|
|
@@ -135,6 +135,31 @@ is left out rather than guessed at. Every run says how many statuses, ids and
|
|
|
135
135
|
relations it left out that way, and `--verbose` says where and why: quote the
|
|
136
136
|
value and it is read.
|
|
137
137
|
|
|
138
|
+
<details>
|
|
139
|
+
<summary>Every status word, and where a status is looked for</summary>
|
|
140
|
+
|
|
141
|
+
A word counts whole and in any case, once emphasis, links, HTML, emoji and dates
|
|
142
|
+
are stripped, so `Unapproved` is not `approved`. A hyphenated word matches
|
|
143
|
+
spaced, or joined by an underscore, as well: `signed-off` reads `Signed off` and
|
|
144
|
+
`signed_off`. A status holding words from two rows takes the higher row, except
|
|
145
|
+
that `provisionally accepted` is a draft.
|
|
146
|
+
|
|
147
|
+
| Phase | Status words |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `retired` | `superseded`, `superceded`, `supersedes-by`, `replaced`, `replaced-by`, `deprecated`, `obsolete`, `obsoleted`, `retired`, `rejected`, `declined`, `withdrawn`, `abandoned`, `cancelled`, `canceled`, `dropped`, `revoked`, `reverted`, `moved`, `historical`, `defunct`, `inactive`, `dead`, `void`, `closed`, `postponed`, `deferred`, `not-planned`, `not_planned`, `wontfix` |
|
|
150
|
+
| `record` | `archived`, `archive` |
|
|
151
|
+
| `frozen` | `final`, `finalised`, `finalized`, `frozen`, `locked`, `ratified`, `immutable`, `sealed`, `standard`, `published` |
|
|
152
|
+
| `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
|
+
| `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
|
+
|
|
155
|
+
| Where | Read from |
|
|
156
|
+
| --- | --- |
|
|
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` |
|
|
159
|
+
| A directory, for a document that declares no status | `archive`, `archived`, `attic`, `deprecated`, `graveyard`, `historical`, `obsolete`, `rejected`, `retired`, `superseded`, `superceded`, `withdrawn` |
|
|
160
|
+
|
|
161
|
+
</details>
|
|
162
|
+
|
|
138
163
|
`archived` is none of the four. It is what spec-brief writes on a round of work
|
|
139
164
|
it has closed, and every spec-* tool reads it as a **record**: depending on it
|
|
140
165
|
is normal, its unticked boxes are not anyone's work, its links are still
|
|
@@ -184,6 +209,40 @@ Markers only count in the forms documents actually use — punctuated
|
|
|
184
209
|
(`Resolved:`), emphasised (`**Moot**`) or shouted (`RESOLVED`) — and never inside
|
|
185
210
|
code. *"We resolved to keep the queue"* does not close anything.
|
|
186
211
|
|
|
212
|
+
<details>
|
|
213
|
+
<summary>Every checkbox, marker and <code>state=</code> word</summary>
|
|
214
|
+
|
|
215
|
+
| Disposition | Checkboxes | Markers | `state=` |
|
|
216
|
+
| --- | --- | --- | --- |
|
|
217
|
+
| `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` | `narrowed`, `partial` |
|
|
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` | `delegated` |
|
|
220
|
+
| `satisfied` | `[x]` `[X]` `[+]` | `resolved`, `answered`, `decided`, `settled`, `done`, `complete`, `completed`, `closed`, `fixed`, `shipped`, `addressed`, `implemented`, `solved`, `confirmed` | `satisfied`, `resolved`, `done`, `closed` |
|
|
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` | `accepted-debt`, `debt`, `wontfix` |
|
|
222
|
+
| `rejected` | `[-]` | `rejected`, `declined`, `dropped`, `not doing`, `will not do`, `abandoned`, `cancelled`, `canceled`, `withdrawn` | `rejected`, `declined` |
|
|
223
|
+
| `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
|
+
|
|
225
|
+
A marker counts where a statement starts: at the start of the item or of a line,
|
|
226
|
+
after the end of a sentence, or after `(` or `[`. Punctuated means followed by
|
|
227
|
+
`:`, `:`, `-`, `–`, `—` or `.`, so `Done.` closes an item; emphasised means the
|
|
228
|
+
emphasis closes on the marker, so `**Closed beta**` does not. A marker of more
|
|
229
|
+
than one word may be spaced, hyphenated or joined by an underscore, whichever
|
|
230
|
+
the table has: `**Tech-debt:**` and `TECH_DEBT:` are `tech debt`. An apostrophe
|
|
231
|
+
may be straight or curly.
|
|
232
|
+
|
|
233
|
+
</details>
|
|
234
|
+
|
|
235
|
+
Without a box, a bullet is an obligation only at the top level of a section
|
|
236
|
+
whose heading names open work, however the heading is cased, spaced, emphasised
|
|
237
|
+
or punctuated - `## ❓ **Open questions:**` is one. The section ends at the next
|
|
238
|
+
heading of its level. **Headings that hold obligations:** `Open Questions`,
|
|
239
|
+
`Open Question`, `Unresolved Questions`, `Unanswered Questions`, `Questions`,
|
|
240
|
+
`Open Issues`, `Action Items`, `Actions`, `TODO`, `To Do`, `To-Do`, `TODOs`,
|
|
241
|
+
`Next Steps`, `Follow Ups`, `Follow-Ups`, `Followups`, `Follow Up`, `Follow-Up`,
|
|
242
|
+
`Unresolved`, `Outstanding`, `Outstanding Questions`, `Tasks`, `Task List`,
|
|
243
|
+
`Work Items`, `Remaining Work`, `Decisions Needed`, `Blockers`, `Parking Lot`,
|
|
244
|
+
`Future Work`, `Deferred`. Every other bullet is the document's own prose.
|
|
245
|
+
|
|
187
246
|
### References, resolved forgivingly and validated strictly
|
|
188
247
|
|
|
189
248
|
All of these reach the same document:
|
|
@@ -201,9 +260,22 @@ trusts:
|
|
|
201
260
|
- **Bare identifiers in prose are opportunistic.** They are reported only when
|
|
202
261
|
their family already exists in the corpus. `ADR-0099` in a repository of ADRs
|
|
203
262
|
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 the
|
|
264
|
+
prefixes of versions, pages, protocols and platforms, even in a repository
|
|
265
|
+
that has a family of that name. **Never a citation:** `ver 3`, `version 4`,
|
|
266
|
+
`pp.12`, `Fig.3`, `Figure 4`, `Table 2`, `tbl 1`, `section 4`, `sect 2`,
|
|
267
|
+
`sec 5`, `step 2`, `item 3`, `No.5`, `num 7`, `line 42`, `ln 12`, `col 3`,
|
|
268
|
+
`port 8080`, `PR 45`, `issue 42`, `GH-123`, `UTF-8`, `ASCII 7`, `SHA-256`,
|
|
269
|
+
`MD5`, `HTTP 2`, `HTTPS 2`, `IPv6`, `IPv4 32`, `IPv6 128`, `TLS 1.3`,
|
|
270
|
+
`SSL 3`, `ES2015`, `ECMA-262`, `p99 250`, `base64`, `SQL-92`, `ARM64`,
|
|
271
|
+
`x86-64`, `Win32`, `Node 22`, `Python 3`, `Java 21`, `Go 1.22`, `cpp20`.
|
|
204
272
|
- **Bare numbers are family-scoped.** `0007` inside an RFC means `RFC-0007`,
|
|
205
273
|
never `ADR-0007`. A repository with both is ordinary, and guessing would be
|
|
206
|
-
worse than silence.
|
|
274
|
+
worse than silence. A document's family is the prefix of its identifier, or
|
|
275
|
+
else the nearest directory that names one: `adr/`, `adrs/`, `decisions/`,
|
|
276
|
+
`decision-records/` and `architecture-decisions/` hold ADRs; `rfc/` and
|
|
277
|
+
`rfcs/` RFCs; `kep/`, `keps/` and `enhancements/` KEPs; and `proposal(s)/`,
|
|
278
|
+
`design(s)/`, `prd(s)/` and `spec(s)/` a family each.
|
|
207
279
|
- **Two candidates is worse than none.** An ambiguous citation is reported, not
|
|
208
280
|
bound to whichever document was indexed first.
|
|
209
281
|
- **Code never counts.** Links inside fences and inline spans are masked before
|
|
@@ -211,6 +283,12 @@ trusts:
|
|
|
211
283
|
so `[ADR-7](https://example.com/adr-7)` yields one reference rather than three.
|
|
212
284
|
- **Source links are not spec links.** `[rules.ts](../../src/rules.ts)` is not a
|
|
213
285
|
broken reference. That is [spec-guard](#relationship-to-spec-guard)'s job.
|
|
286
|
+
Nor is a link to any other file that is not a document: an image, a dotfile
|
|
287
|
+
such as `../.nvmrc`, or a drawing such as `arch.excalidraw`, however long its
|
|
288
|
+
extension. A dot followed by a digit, as in `ADR-0007.1`, is part of an
|
|
289
|
+
identifier and is still resolved. A link to a directory is not a reference
|
|
290
|
+
either, `../guides/` or `../guides` alike - without the slash the disk says
|
|
291
|
+
which it is, and a name with no slash at all, `guides`, is an identifier.
|
|
214
292
|
|
|
215
293
|
### Registers: many specifications in one file
|
|
216
294
|
|
|
@@ -241,14 +319,20 @@ header you wrote:
|
|
|
241
319
|
| ADR-0002 | Cache eviction | Accepted | ADR-0001 | - |
|
|
242
320
|
```
|
|
243
321
|
|
|
244
|
-
Findings point at the declaring cell, not at the file.
|
|
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.
|
|
245
326
|
|
|
246
|
-
A row is named by its ID column, and a title is only a title.
|
|
247
|
-
|
|
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
|
|
248
331
|
`| OI-V-05 | ADR-040's enforcement point has no browser test |` is filed under
|
|
249
332
|
`OI-V-05`, and citations of `ADR-040` still reach `ADR-040`. The same holds for
|
|
250
|
-
a whole file: an explicit `id
|
|
251
|
-
|
|
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.
|
|
252
336
|
|
|
253
337
|
The register itself stays in the graph as the thing that holds them, so
|
|
254
338
|
`spec-graph query 'document[id=register] -contains-> document'` lists what is
|
|
@@ -335,6 +419,23 @@ depends-on: [ADR-0004, RFC-0011]
|
|
|
335
419
|
---
|
|
336
420
|
```
|
|
337
421
|
|
|
422
|
+
A front-matter key is read however it is punctuated or cased - `depends-on`,
|
|
423
|
+
`depends_on` and `dependsOn` are one key - and every relation is spelled in
|
|
424
|
+
both directions, so a repository writes whichever half its filing convention
|
|
425
|
+
keeps ([ADR-0014](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0014-a-relation-is-spelled-both-ways.md)).
|
|
426
|
+
Written in ADR-0001, `key: ADR-0002` means:
|
|
427
|
+
|
|
428
|
+
| Relation | ADR-0001 → ADR-0002 | ADR-0002 → ADR-0001 |
|
|
429
|
+
| --- | --- | --- |
|
|
430
|
+
| `supersedes` | `supersedes`, `supercedes`, `replaces`, `obsoletes`, `deprecates` | `superseded-by`, `superceded-by`, `replaced-by`, `obsoleted-by`, `deprecated-by`, `rolled-into` |
|
|
431
|
+
| `amends` | `amends`, `extends`, `refines`, `clarifies`, `revises` | `amended-by`, `extended-by`, `refined-by`, `clarified-by`, `revised-by` |
|
|
432
|
+
| `depends-on` | `depends-on`, `dependent-on`, `dependencies`, `requires`, `builds-on`, `relies-on` | `depended-on-by`, `required-by`, `dependents` |
|
|
433
|
+
| `assumes` | `assumes` | `assumed-by` |
|
|
434
|
+
| `blocked-by` | `blocked-by`, `blocked-on`, `waiting-on`, `waiting-for`, `gated-on`, `gated-by` | `blocks` |
|
|
435
|
+
| `delegates-to` | `delegates-to`, `delegated-to`, `deferred-to`, `tracked-in`, `tracked-by`, `continued-in` | `delegated-from` |
|
|
436
|
+
| `relates-to` | `related`, `relates-to`, `related-to`, `see-also` | |
|
|
437
|
+
| `references` | `references`, `reference`, `refs` | `referenced-by` |
|
|
438
|
+
|
|
338
439
|
```md
|
|
339
440
|
The chunking scheme is constrained by [ADR-0002](0002-rows.md). → assumes
|
|
340
441
|
Which policy? Deferred to [ADR-0011](0011-policy.md). → delegates-to
|
|
@@ -344,6 +445,23 @@ Blocked by [ADR-0009](0009-compliance.md). → blocked-by
|
|
|
344
445
|
- [ADR-0001](0001-intro.md) → relates-to
|
|
345
446
|
```
|
|
346
447
|
|
|
448
|
+
A phrase governs a reference when it stands no more than 40 characters before
|
|
449
|
+
it, in the same statement: a sentence, a paragraph, a list item or a table cell.
|
|
450
|
+
A negation between the two - `owned by nobody` - cancels it. Prose reads both
|
|
451
|
+
directions too: `supersedes` and `superseded by`, `amends` and `amended by`,
|
|
452
|
+
`blocked by` and `blocks` or `blocking`, `depends on` and `depended on by` - the
|
|
453
|
+
second of each pair points back at the document that wrote it. A phrase that follows the reference
|
|
454
|
+
makes the reference its subject - `[ADR-0009](0009.md) supersedes this` - except
|
|
455
|
+
a `depends-on` or `assumes` phrase: `[ADR-0002](0002.md) requires a migration`
|
|
456
|
+
says nothing about this document.
|
|
457
|
+
|
|
458
|
+
A link under a heading that files it as bookkeeping is `relates-to` unless a
|
|
459
|
+
phrase beside it says more. **Bookkeeping headings:** `See also`, `References`,
|
|
460
|
+
`Reference`, `Related`, `Related work`, `Related decisions`,
|
|
461
|
+
`Related documents`, `Links`, `Further reading`, `Prior art`, `History`,
|
|
462
|
+
`Changelog`, `Change log`, `Revision history`, `Bibliography`, `Sources`,
|
|
463
|
+
`Appendix`, `More information`, `Resources`, `Index`.
|
|
464
|
+
|
|
347
465
|
Two properties do the work. `loadBearing` relations (`assumes`, `depends-on`,
|
|
348
466
|
`blocked-by`, `amends`) make the source's validity depend on the target — those
|
|
349
467
|
are what turn stale. `transfersObligation` relations (`delegates-to`,
|
|
@@ -396,7 +514,7 @@ document[phase=active] -delegates-to-> item[openness=open]
|
|
|
396
514
|
└──────┘└────────────┘ └─────────────┘ └─────────────────┘
|
|
397
515
|
node predicate relation node
|
|
398
516
|
|
|
399
|
-
Node types document · item · *
|
|
517
|
+
Node types document · item · * (also documents docs doc items nodes node any)
|
|
400
518
|
Attributes id kind title path file line phase status receptivity alias
|
|
401
519
|
document state (or disposition) openness section text body
|
|
402
520
|
evidence conflicted fm.<front-matter-key>
|
|
@@ -405,9 +523,12 @@ Relations -kind-> <-kind- =kind=> <=kind= (= forms are transitive)
|
|
|
405
523
|
comma-separate kinds: -assumes,depends-on->
|
|
406
524
|
```
|
|
407
525
|
|
|
408
|
-
Items inherit their document's lifecycle
|
|
409
|
-
|
|
410
|
-
|
|
526
|
+
Items inherit their document's lifecycle - `phase`, `receptivity`, `status` - and
|
|
527
|
+
answer to its `path` and its `alias` spellings, so `item[phase=retired]` means
|
|
528
|
+
what you expect. A query evaluates to **paths**, not endpoints, which is why a
|
|
529
|
+
finding can name both ends and the line that connects them, and `query` prints
|
|
530
|
+
each one the way the selector reads, every document a transitive step passed
|
|
531
|
+
through included.
|
|
411
532
|
|
|
412
533
|
`~=` is a regular expression matched by an automaton that cannot backtrack, so a
|
|
413
534
|
predicate is linear in the subject whatever the pattern — `^([A-Za-z0-9_]+[ ]?)+$`
|
|
@@ -504,6 +625,23 @@ Directives are ordinary HTML comments — invisible in every Markdown renderer:
|
|
|
504
625
|
<!-- @spec-history -->
|
|
505
626
|
```
|
|
506
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
|
+
|
|
507
645
|
A directive always wins, and the report says the state came from a directive, so
|
|
508
646
|
an override is visible rather than mysterious.
|
|
509
647
|
|
|
@@ -672,6 +810,7 @@ command. spec-graph reads the first of `.spec-graph.json`,
|
|
|
672
810
|
"ratchet": true,
|
|
673
811
|
"severities": { "self-reference": "off" },
|
|
674
812
|
"strict": true,
|
|
813
|
+
"maxRelated": 3,
|
|
675
814
|
"rules": {
|
|
676
815
|
"no-draft-dependency": {
|
|
677
816
|
"query": "document[phase=active] -depends-on-> document[phase=draft]",
|
|
@@ -696,6 +835,10 @@ which file was read, on stderr with everything else a run says about itself, so
|
|
|
696
835
|
it can be combined with any `--format` and leave the document on stdout intact.
|
|
697
836
|
`--no-config` skips the mechanism entirely.
|
|
698
837
|
|
|
838
|
+
`maxRelated` has no flag. It caps how many related locations one finding lists
|
|
839
|
+
— the lines under it in the terminal, `related` in JSON, `relatedLocations` in
|
|
840
|
+
SARIF — and is 8 unless set; `0` lists none.
|
|
841
|
+
|
|
699
842
|
A broken config stops the run with exit `2`, each problem named on stderr —
|
|
700
843
|
invalid JSON, a value of the wrong type, an unknown key, a rule that does not
|
|
701
844
|
compile. A configuration that did not load checks a different repository than
|
|
@@ -732,7 +875,13 @@ extended glob, `+(a|b)`: write `{a,b}`. A parenthesis with no `|` in its group
|
|
|
732
875
|
is itself, so `C++(notes).md` names that file. A `..` typed below the root,
|
|
733
876
|
`spec-graph "../*.md"` from `docs/deep`, is resolved against where you typed
|
|
734
877
|
it. A bare `--ignore` name, `--ignore drafts`, still prunes that directory at
|
|
735
|
-
any depth.
|
|
878
|
+
any depth, even where a pattern starts inside it.
|
|
879
|
+
|
|
880
|
+
The walk never enters `.git`, `.hg`, `.svn`, `.cache`, `.next`, `.nuxt`,
|
|
881
|
+
`.turbo`, `.venv`, `node_modules`, `bower_components`, `vendor`, `dist`,
|
|
882
|
+
`build`, `out`, `target`, `coverage` or `__pycache__` on its way to what the
|
|
883
|
+
patterns name. Unlike an `--ignore`, these yield to a pattern that starts inside
|
|
884
|
+
one: `vendor/specs/*.md` is read from there, whatever the other patterns reach.
|
|
736
885
|
|
|
737
886
|
`--ignore-ref` is matched against reference targets rather than paths: a bare
|
|
738
887
|
pattern names one target exactly, case is ignored on every host, a `\` escapes
|
|
@@ -952,10 +1101,10 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
|
|
|
952
1101
|
than installed. So the whole package is auditable in an afternoon — and nothing
|
|
953
1102
|
in the pipeline can take longer than its input.
|
|
954
1103
|
|
|
955
|
-
**Verified, not just covered.** 1,
|
|
1104
|
+
**Verified, not just covered.** 1,776 tests; 99.3% statement and 99.7% line
|
|
956
1105
|
coverage. Coverage says a line ran, so the suite is also held to a mutation
|
|
957
|
-
score -
|
|
958
|
-
because a vocabulary entry or a boundary
|
|
1106
|
+
score - 96.34% over 8,242 mutants, measured by CI's full sweep, and a build
|
|
1107
|
+
fails under 93 - because a vocabulary entry or a boundary
|
|
959
1108
|
condition can be weakened by an ordinary-looking refactor without a single test
|
|
960
1109
|
going red. [ADR-0007](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0007-mutation-testing.md) is straight about what
|
|
961
1110
|
that number is and is not.
|
package/dist/baseline.js
CHANGED
|
@@ -81,6 +81,10 @@ export function formatBaseline(graph, diagnostics) {
|
|
|
81
81
|
const findings = [...tally(graph, diagnostics).values()].sort(compareEntries);
|
|
82
82
|
return `${JSON.stringify({ version: BASELINE_VERSION, findings }, null, 2)}\n`;
|
|
83
83
|
}
|
|
84
|
+
// A string comparison is reached only when the two differ, where `<` and `<=`
|
|
85
|
+
// agree. Nothing sorted here has two entries alike in all three fields - a
|
|
86
|
+
// tally is keyed on them, and a stale entry is taken once per key - so the
|
|
87
|
+
// last guard never sees two equal subjects. Those mutants are equivalent.
|
|
84
88
|
function compareEntries(a, b) {
|
|
85
89
|
if (a.rule !== b.rule)
|
|
86
90
|
return a.rule < b.rule ? -1 : 1;
|
|
@@ -145,6 +149,8 @@ function readEntry(row, where, problems) {
|
|
|
145
149
|
problems.push(`${where}: "document" and "subject" must be strings`);
|
|
146
150
|
return null;
|
|
147
151
|
}
|
|
152
|
+
// `Number.isInteger` is false for anything but a number, so the `typeof`
|
|
153
|
+
// only narrows the type for `count < 1`, and its mutant is equivalent.
|
|
148
154
|
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {
|
|
149
155
|
problems.push(`${where}: "count" must be a whole number of at least 1`);
|
|
150
156
|
return null;
|
package/dist/baseline.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Accepted debt, and the ratchet that keeps it from growing.\n *\n * A linter introduced to a repository that predates it reports everything at\n * once. Fifty findings on day one is not a report, it is a decision to be\n * ignored: the team cannot stop to fix them, so they turn the tool off, or they\n * turn the rules off and the next specification rots unwatched.\n *\n * A baseline is the third answer. It records what was already wrong on the day\n * the tool arrived, and reports only what happened since. Debt can be paid down\n * whenever there is room, and in the meantime nothing new gets in.\n *\n * The whole module is pure. Reading and writing the file is the CLI's job, for\n * the same reason extraction never touches a disk.\n */\n\nimport type { SpecGraph } from './graph.js';\nimport { RULE_IDS } from './rules.js';\nimport { isProjectRule, type AnyRuleId, type Diagnostic, type RuleId } from './types.js';\n\n/** The current file format. Bumped only for a change old files cannot survive. */\nexport const BASELINE_VERSION = 1;\n\n/** One class of accepted finding, and how many of it there were. */\nexport interface BaselineEntry {\n readonly rule: AnyRuleId;\n /** The specification the finding is about, not the file it was found in. */\n readonly document: string;\n /** What within it: a citation target, or the document at the other end. */\n readonly subject: string;\n readonly count: number;\n}\n\nexport interface Baseline {\n readonly version: number;\n readonly findings: readonly BaselineEntry[];\n}\n\nexport const EMPTY_BASELINE: Baseline = Object.freeze({ version: BASELINE_VERSION, findings: [] });\n\n/**\n * What makes two findings the same finding.\n *\n * Not the line. A baseline keyed on line numbers is invalidated by any edit\n * above it, which turns every unrelated pull request into a wall of findings\n * nobody introduced. Not the message either: rewording a diagnostic would\n * silently expire every baseline in the world.\n *\n * What survives is the pair the finding is actually about - which specification,\n * and what within it. Both are ids that outlive a file being renamed, a section\n * being moved into its own file, or an obligation being reordered, because that\n * is exactly what identifiers were made stable for (ADR-0009).\n *\n * The cost is deliberate coarseness: two broken links from one document to the\n * same target are one entry with a count of two. Paying that buys a baseline\n * that does not lie the first time somebody reformats a paragraph.\n */\nexport function fingerprintOf(graph: SpecGraph, diagnostic: Diagnostic): { document: string; subject: string } {\n const first = diagnostic.nodes[0] ?? '';\n const document = graph.owningDocument(first)?.id ?? first;\n if (diagnostic.target !== null) return { document, subject: diagnostic.target };\n\n const second = diagnostic.nodes[1];\n const subject = second === undefined ? '' : (graph.owningDocument(second)?.id ?? second);\n // A document-level rule lists its own items as the rest of `nodes`, so the\n // other end resolves back to where it started. Saying so twice in the file\n // would only invite a reader to look for a distinction that is not there.\n return { document, subject: subject === document ? '' : subject };\n}\n\nfunction keyOf(rule: AnyRuleId, document: string, subject: string): string {\n // Tab-joined: the three parts are ids and targets, and a tab is the one\n // character none of them can contain.\n return `${rule}\\t${document}\\t${subject}`;\n}\n\n/** Counts findings by fingerprint. */\nfunction tally(graph: SpecGraph, diagnostics: readonly Diagnostic[]): Map<string, BaselineEntry> {\n const out = new Map<string, BaselineEntry>();\n for (const diagnostic of diagnostics) {\n const { document, subject } = fingerprintOf(graph, diagnostic);\n const key = keyOf(diagnostic.rule, document, subject);\n const seen = out.get(key);\n out.set(key, {\n rule: diagnostic.rule,\n document,\n subject,\n count: (seen?.count ?? 0) + 1,\n });\n }\n return out;\n}\n\n/**\n * Renders a baseline for the findings given.\n *\n * Sorted by rule, then document, then subject, and written with two-space\n * indentation: the file lands in a repository and is read in diffs, so its byte\n * order has to be a function of its content and nothing else. No timestamp, for\n * the same reason - a generated date makes every re-record a change even when\n * nothing changed.\n */\nexport function formatBaseline(graph: SpecGraph, diagnostics: readonly Diagnostic[]): string {\n const findings = [...tally(graph, diagnostics).values()].sort(compareEntries);\n return `${JSON.stringify({ version: BASELINE_VERSION, findings }, null, 2)}\\n`;\n}\n\nfunction compareEntries(a: BaselineEntry, b: BaselineEntry): number {\n if (a.rule !== b.rule) return a.rule < b.rule ? -1 : 1;\n if (a.document !== b.document) return a.document < b.document ? -1 : 1;\n if (a.subject !== b.subject) return a.subject < b.subject ? -1 : 1;\n return 0;\n}\n\nexport interface ParsedBaseline {\n readonly baseline: Baseline;\n /** Reported, never silently dropped. A baseline nobody can read is worse\n * than none: it suppresses findings the reader cannot account for. */\n readonly problems: readonly string[];\n}\n\n/** Parses a baseline file. Never throws; an unusable file yields an empty one. */\nexport function parseBaseline(raw: string, source: string): ParsedBaseline {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);\n } catch (error) {\n return { baseline: EMPTY_BASELINE, problems: [`${source} is not valid JSON: ${(error as Error).message}`] };\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { baseline: EMPTY_BASELINE, problems: [`${source} must contain a JSON object`] };\n }\n\n const record = parsed as Record<string, unknown>;\n const problems: string[] = [];\n if (record['version'] !== BASELINE_VERSION) {\n return {\n baseline: EMPTY_BASELINE,\n problems: [`${source} is version ${String(record['version'])}, and this build reads version ${BASELINE_VERSION}`],\n };\n }\n\n const rows = record['findings'];\n if (!Array.isArray(rows)) {\n return { baseline: EMPTY_BASELINE, problems: [`${source}: \"findings\" must be an array`] };\n }\n\n const findings: BaselineEntry[] = [];\n for (const [index, row] of rows.entries()) {\n const entry = readEntry(row, `${source}: findings[${index}]`, problems);\n if (entry !== null) findings.push(entry);\n }\n return { baseline: { version: BASELINE_VERSION, findings }, problems };\n}\n\nfunction readEntry(row: unknown, where: string, problems: string[]): BaselineEntry | null {\n if (typeof row !== 'object' || row === null || Array.isArray(row)) {\n problems.push(`${where} must be an object`);\n return null;\n }\n const fields = row as Record<string, unknown>;\n const rule = fields['rule'];\n // A project rule is accepted on its namespace alone, because the baseline is\n // read before the configuration that defines it has any say - and an entry\n // rejected here would silently un-accept debt a team had already signed off.\n // A name nothing defines suppresses nothing and is reported as stale, which\n // is the same answer by a route that keeps the file readable.\n if (typeof rule !== 'string' || !(RULE_IDS.includes(rule as RuleId) || isProjectRule(rule as AnyRuleId))) {\n problems.push(`${where}: unknown rule ${JSON.stringify(rule)}`);\n return null;\n }\n const document = fields['document'];\n const subject = fields['subject'] ?? '';\n const count = fields['count'] ?? 1;\n if (typeof document !== 'string' || typeof subject !== 'string') {\n problems.push(`${where}: \"document\" and \"subject\" must be strings`);\n return null;\n }\n if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {\n problems.push(`${where}: \"count\" must be a whole number of at least 1`);\n return null;\n }\n return { rule: rule as AnyRuleId, document, subject, count };\n}\n\n/**\n * An entry the run did not spend, and why.\n *\n * The distinction is the whole value of the label. `paid` is the ratchet\n * working: the defect was fixed and the exemption can be struck. `gone` is not\n * an achievement at all - the document the entry is about was not in this\n * corpus, so nothing whatever is known about the defect. Both make the file\n * stale and both need it re-recorded, but only one of them is good news, and a\n * report that says \"paid\" over a narrowed `patterns` glob is congratulating a\n * team for losing sight of the problem.\n */\nexport interface StaleEntry extends BaselineEntry {\n readonly reason: 'paid' | 'gone';\n}\n\nexport interface BaselineOutcome {\n /** Findings the baseline does not account for. These are the report. */\n readonly kept: readonly Diagnostic[];\n /** How many were accounted for. Reported as a number, not as a list. */\n readonly suppressed: number;\n /**\n * Entries whose debt was not spent, wholly or partly.\n *\n * The ratchet: once a repository is clean of something, saying so in the file\n * stops it coming back unnoticed.\n */\n readonly stale: readonly StaleEntry[];\n}\n\n/**\n * Removes the findings a baseline accounts for.\n *\n * Where a baseline allows two of something and three exist, one is reported.\n * Which one is not a meaningful question - they share a fingerprint - so the\n * findings are taken in report order and the surplus is what is left.\n */\nexport function applyBaseline(\n graph: SpecGraph,\n diagnostics: readonly Diagnostic[],\n baseline: Baseline,\n): BaselineOutcome {\n const budget = new Map<string, number>();\n for (const entry of baseline.findings) {\n const key = keyOf(entry.rule, entry.document, entry.subject);\n budget.set(key, (budget.get(key) ?? 0) + entry.count);\n }\n\n const kept: Diagnostic[] = [];\n let suppressed = 0;\n for (const diagnostic of diagnostics) {\n const { document, subject } = fingerprintOf(graph, diagnostic);\n const key = keyOf(diagnostic.rule, document, subject);\n const left = budget.get(key) ?? 0;\n if (left <= 0) {\n kept.push(diagnostic);\n continue;\n }\n budget.set(key, left - 1);\n suppressed += 1;\n }\n\n const stale: StaleEntry[] = [];\n for (const entry of baseline.findings) {\n const key = keyOf(entry.rule, entry.document, entry.subject);\n const unspent = budget.get(key) ?? 0;\n if (unspent <= 0) continue;\n // Asked of the graph rather than of the filesystem, because the graph is\n // what this run looked at. A document that still exists on disk and fell\n // outside the include patterns is gone for the purposes of the answer, and\n // that is the case worth telling apart - it is the one a team reaches by\n // editing a glob rather than by fixing anything.\n const reason = graph.document(entry.document) === undefined ? 'gone' : 'paid';\n stale.push({ ...entry, count: Math.min(unspent, entry.count), reason });\n // An entry counted once; a duplicate key in a hand-edited file must not\n // claim the same surplus twice.\n budget.set(key, 0);\n }\n\n return { kept, suppressed, stale: stale.sort(compareEntries) };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAgD,MAAM,YAAY,CAAC;AAEzF,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAiBlC,MAAM,CAAC,MAAM,cAAc,GAAa,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;AAEnG;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,UAAsB;IACpE,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,KAAK,CAAC;IAC1D,IAAI,UAAU,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC;IAEhF,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,MAAM,CAAC,CAAC;IACzF,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;AACpE,CAAC;AAED,SAAS,KAAK,CAAC,IAAe,EAAE,QAAgB,EAAE,OAAe;IAC/D,wEAAwE;IACxE,sCAAsC;IACtC,OAAO,GAAG,IAAI,KAAK,QAAQ,KAAK,OAAO,EAAE,CAAC;AAC5C,CAAC;AAED,sCAAsC;AACtC,SAAS,KAAK,CAAC,KAAgB,EAAE,WAAkC;IACjE,MAAM,GAAG,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC7C,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QAC/D,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE;YACX,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,QAAQ;YACR,OAAO;YACP,KAAK,EAAE,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC;SAC9B,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,KAAgB,EAAE,WAAkC;IACjF,MAAM,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC9E,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AACjF,CAAC;AAED,8EAA8E;AAC9E,2EAA2E;AAC3E,2EAA2E;AAC3E,0EAA0E;AAC1E,SAAS,cAAc,CAAC,CAAgB,EAAE,CAAgB;IACxD,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvD,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ;QAAE,OAAO,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvE,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO;QAAE,OAAO,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,OAAO,CAAC,CAAC;AACX,CAAC;AASD,kFAAkF;AAClF,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,MAAc;IACvD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACzE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,uBAAwB,KAAe,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;IAC9G,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,6BAA6B,CAAC,EAAE,CAAC;IAC1F,CAAC;IAED,MAAM,MAAM,GAAG,MAAiC,CAAC;IACjD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,gBAAgB,EAAE,CAAC;QAC3C,OAAO;YACL,QAAQ,EAAE,cAAc;YACxB,QAAQ,EAAE,CAAC,GAAG,MAAM,eAAe,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,kCAAkC,gBAAgB,EAAE,CAAC;SAClH,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,+BAA+B,CAAC,EAAE,CAAC;IAC5F,CAAC;IAED,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,KAAK,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE,GAAG,MAAM,cAAc,KAAK,GAAG,EAAE,QAAQ,CAAC,CAAC;QACxE,IAAI,KAAK,KAAK,IAAI;YAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC;AACzE,CAAC;AAED,SAAS,SAAS,CAAC,GAAY,EAAE,KAAa,EAAE,QAAkB;IAChE,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,oBAAoB,CAAC,CAAC;QAC5C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,GAA8B,CAAC;IAC9C,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5B,6EAA6E;IAC7E,2EAA2E;IAC3E,6EAA6E;IAC7E,4EAA4E;IAC5E,8DAA8D;IAC9D,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAc,CAAC,IAAI,aAAa,CAAC,IAAiB,CAAC,CAAC,EAAE,CAAC;QACzG,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,kBAAkB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChE,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,4CAA4C,CAAC,CAAC;QACpE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,yEAAyE;IACzE,uEAAuE;IACvE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,gDAAgD,CAAC,CAAC;QACxE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,IAAiB,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC/D,CAAC;AA+BD;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAgB,EAChB,WAAkC,EAClC,QAAkB;IAElB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7D,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,IAAI,GAAiB,EAAE,CAAC;IAC9B,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QAC/D,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;YACd,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACtB,SAAS;QACX,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC;QAC1B,UAAU,IAAI,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,OAAO,IAAI,CAAC;YAAE,SAAS;QAC3B,yEAAyE;QACzE,yEAAyE;QACzE,2EAA2E;QAC3E,yEAAyE;QACzE,iDAAiD;QACjD,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QAC9E,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;QACxE,wEAAwE;QACxE,gCAAgC;QAChC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;AACjE,CAAC","sourcesContent":["/**\n * Accepted debt, and the ratchet that keeps it from growing.\n *\n * A linter introduced to a repository that predates it reports everything at\n * once. Fifty findings on day one is not a report, it is a decision to be\n * ignored: the team cannot stop to fix them, so they turn the tool off, or they\n * turn the rules off and the next specification rots unwatched.\n *\n * A baseline is the third answer. It records what was already wrong on the day\n * the tool arrived, and reports only what happened since. Debt can be paid down\n * whenever there is room, and in the meantime nothing new gets in.\n *\n * The whole module is pure. Reading and writing the file is the CLI's job, for\n * the same reason extraction never touches a disk.\n */\n\nimport type { SpecGraph } from './graph.js';\nimport { RULE_IDS } from './rules.js';\nimport { isProjectRule, type AnyRuleId, type Diagnostic, type RuleId } from './types.js';\n\n/** The current file format. Bumped only for a change old files cannot survive. */\nexport const BASELINE_VERSION = 1;\n\n/** One class of accepted finding, and how many of it there were. */\nexport interface BaselineEntry {\n readonly rule: AnyRuleId;\n /** The specification the finding is about, not the file it was found in. */\n readonly document: string;\n /** What within it: a citation target, or the document at the other end. */\n readonly subject: string;\n readonly count: number;\n}\n\nexport interface Baseline {\n readonly version: number;\n readonly findings: readonly BaselineEntry[];\n}\n\nexport const EMPTY_BASELINE: Baseline = Object.freeze({ version: BASELINE_VERSION, findings: [] });\n\n/**\n * What makes two findings the same finding.\n *\n * Not the line. A baseline keyed on line numbers is invalidated by any edit\n * above it, which turns every unrelated pull request into a wall of findings\n * nobody introduced. Not the message either: rewording a diagnostic would\n * silently expire every baseline in the world.\n *\n * What survives is the pair the finding is actually about - which specification,\n * and what within it. Both are ids that outlive a file being renamed, a section\n * being moved into its own file, or an obligation being reordered, because that\n * is exactly what identifiers were made stable for (ADR-0009).\n *\n * The cost is deliberate coarseness: two broken links from one document to the\n * same target are one entry with a count of two. Paying that buys a baseline\n * that does not lie the first time somebody reformats a paragraph.\n */\nexport function fingerprintOf(graph: SpecGraph, diagnostic: Diagnostic): { document: string; subject: string } {\n const first = diagnostic.nodes[0] ?? '';\n const document = graph.owningDocument(first)?.id ?? first;\n if (diagnostic.target !== null) return { document, subject: diagnostic.target };\n\n const second = diagnostic.nodes[1];\n const subject = second === undefined ? '' : (graph.owningDocument(second)?.id ?? second);\n // A document-level rule lists its own items as the rest of `nodes`, so the\n // other end resolves back to where it started. Saying so twice in the file\n // would only invite a reader to look for a distinction that is not there.\n return { document, subject: subject === document ? '' : subject };\n}\n\nfunction keyOf(rule: AnyRuleId, document: string, subject: string): string {\n // Tab-joined: the three parts are ids and targets, and a tab is the one\n // character none of them can contain.\n return `${rule}\\t${document}\\t${subject}`;\n}\n\n/** Counts findings by fingerprint. */\nfunction tally(graph: SpecGraph, diagnostics: readonly Diagnostic[]): Map<string, BaselineEntry> {\n const out = new Map<string, BaselineEntry>();\n for (const diagnostic of diagnostics) {\n const { document, subject } = fingerprintOf(graph, diagnostic);\n const key = keyOf(diagnostic.rule, document, subject);\n const seen = out.get(key);\n out.set(key, {\n rule: diagnostic.rule,\n document,\n subject,\n count: (seen?.count ?? 0) + 1,\n });\n }\n return out;\n}\n\n/**\n * Renders a baseline for the findings given.\n *\n * Sorted by rule, then document, then subject, and written with two-space\n * indentation: the file lands in a repository and is read in diffs, so its byte\n * order has to be a function of its content and nothing else. No timestamp, for\n * the same reason - a generated date makes every re-record a change even when\n * nothing changed.\n */\nexport function formatBaseline(graph: SpecGraph, diagnostics: readonly Diagnostic[]): string {\n const findings = [...tally(graph, diagnostics).values()].sort(compareEntries);\n return `${JSON.stringify({ version: BASELINE_VERSION, findings }, null, 2)}\\n`;\n}\n\n// A string comparison is reached only when the two differ, where `<` and `<=`\n// agree. Nothing sorted here has two entries alike in all three fields - a\n// tally is keyed on them, and a stale entry is taken once per key - so the\n// last guard never sees two equal subjects. Those mutants are equivalent.\nfunction compareEntries(a: BaselineEntry, b: BaselineEntry): number {\n if (a.rule !== b.rule) return a.rule < b.rule ? -1 : 1;\n if (a.document !== b.document) return a.document < b.document ? -1 : 1;\n if (a.subject !== b.subject) return a.subject < b.subject ? -1 : 1;\n return 0;\n}\n\nexport interface ParsedBaseline {\n readonly baseline: Baseline;\n /** Reported, never silently dropped. A baseline nobody can read is worse\n * than none: it suppresses findings the reader cannot account for. */\n readonly problems: readonly string[];\n}\n\n/** Parses a baseline file. Never throws; an unusable file yields an empty one. */\nexport function parseBaseline(raw: string, source: string): ParsedBaseline {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);\n } catch (error) {\n return { baseline: EMPTY_BASELINE, problems: [`${source} is not valid JSON: ${(error as Error).message}`] };\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { baseline: EMPTY_BASELINE, problems: [`${source} must contain a JSON object`] };\n }\n\n const record = parsed as Record<string, unknown>;\n const problems: string[] = [];\n if (record['version'] !== BASELINE_VERSION) {\n return {\n baseline: EMPTY_BASELINE,\n problems: [`${source} is version ${String(record['version'])}, and this build reads version ${BASELINE_VERSION}`],\n };\n }\n\n const rows = record['findings'];\n if (!Array.isArray(rows)) {\n return { baseline: EMPTY_BASELINE, problems: [`${source}: \"findings\" must be an array`] };\n }\n\n const findings: BaselineEntry[] = [];\n for (const [index, row] of rows.entries()) {\n const entry = readEntry(row, `${source}: findings[${index}]`, problems);\n if (entry !== null) findings.push(entry);\n }\n return { baseline: { version: BASELINE_VERSION, findings }, problems };\n}\n\nfunction readEntry(row: unknown, where: string, problems: string[]): BaselineEntry | null {\n if (typeof row !== 'object' || row === null || Array.isArray(row)) {\n problems.push(`${where} must be an object`);\n return null;\n }\n const fields = row as Record<string, unknown>;\n const rule = fields['rule'];\n // A project rule is accepted on its namespace alone, because the baseline is\n // read before the configuration that defines it has any say - and an entry\n // rejected here would silently un-accept debt a team had already signed off.\n // A name nothing defines suppresses nothing and is reported as stale, which\n // is the same answer by a route that keeps the file readable.\n if (typeof rule !== 'string' || !(RULE_IDS.includes(rule as RuleId) || isProjectRule(rule as AnyRuleId))) {\n problems.push(`${where}: unknown rule ${JSON.stringify(rule)}`);\n return null;\n }\n const document = fields['document'];\n const subject = fields['subject'] ?? '';\n const count = fields['count'] ?? 1;\n if (typeof document !== 'string' || typeof subject !== 'string') {\n problems.push(`${where}: \"document\" and \"subject\" must be strings`);\n return null;\n }\n // `Number.isInteger` is false for anything but a number, so the `typeof`\n // only narrows the type for `count < 1`, and its mutant is equivalent.\n if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {\n problems.push(`${where}: \"count\" must be a whole number of at least 1`);\n return null;\n }\n return { rule: rule as AnyRuleId, document, subject, count };\n}\n\n/**\n * An entry the run did not spend, and why.\n *\n * The distinction is the whole value of the label. `paid` is the ratchet\n * working: the defect was fixed and the exemption can be struck. `gone` is not\n * an achievement at all - the document the entry is about was not in this\n * corpus, so nothing whatever is known about the defect. Both make the file\n * stale and both need it re-recorded, but only one of them is good news, and a\n * report that says \"paid\" over a narrowed `patterns` glob is congratulating a\n * team for losing sight of the problem.\n */\nexport interface StaleEntry extends BaselineEntry {\n readonly reason: 'paid' | 'gone';\n}\n\nexport interface BaselineOutcome {\n /** Findings the baseline does not account for. These are the report. */\n readonly kept: readonly Diagnostic[];\n /** How many were accounted for. Reported as a number, not as a list. */\n readonly suppressed: number;\n /**\n * Entries whose debt was not spent, wholly or partly.\n *\n * The ratchet: once a repository is clean of something, saying so in the file\n * stops it coming back unnoticed.\n */\n readonly stale: readonly StaleEntry[];\n}\n\n/**\n * Removes the findings a baseline accounts for.\n *\n * Where a baseline allows two of something and three exist, one is reported.\n * Which one is not a meaningful question - they share a fingerprint - so the\n * findings are taken in report order and the surplus is what is left.\n */\nexport function applyBaseline(\n graph: SpecGraph,\n diagnostics: readonly Diagnostic[],\n baseline: Baseline,\n): BaselineOutcome {\n const budget = new Map<string, number>();\n for (const entry of baseline.findings) {\n const key = keyOf(entry.rule, entry.document, entry.subject);\n budget.set(key, (budget.get(key) ?? 0) + entry.count);\n }\n\n const kept: Diagnostic[] = [];\n let suppressed = 0;\n for (const diagnostic of diagnostics) {\n const { document, subject } = fingerprintOf(graph, diagnostic);\n const key = keyOf(diagnostic.rule, document, subject);\n const left = budget.get(key) ?? 0;\n if (left <= 0) {\n kept.push(diagnostic);\n continue;\n }\n budget.set(key, left - 1);\n suppressed += 1;\n }\n\n const stale: StaleEntry[] = [];\n for (const entry of baseline.findings) {\n const key = keyOf(entry.rule, entry.document, entry.subject);\n const unspent = budget.get(key) ?? 0;\n if (unspent <= 0) continue;\n // Asked of the graph rather than of the filesystem, because the graph is\n // what this run looked at. A document that still exists on disk and fell\n // outside the include patterns is gone for the purposes of the answer, and\n // that is the case worth telling apart - it is the one a team reaches by\n // editing a glob rather than by fixing anything.\n const reason = graph.document(entry.document) === undefined ? 'gone' : 'paid';\n stale.push({ ...entry, count: Math.min(unspent, entry.count), reason });\n // An entry counted once; a duplicate key in a hand-edited file must not\n // claim the same surplus twice.\n budget.set(key, 0);\n }\n\n return { kept, suppressed, stale: stale.sort(compareEntries) };\n}\n"]}
|
package/dist/cli.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface CliIO {
|
|
|
77
77
|
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
78
78
|
readonly isTTY?: boolean | undefined;
|
|
79
79
|
}
|
|
80
|
-
export declare const HELP = "spec-graph - turn Markdown specifications into a verifiable graph\n\nUSAGE\n spec-graph [check] [patterns...] [options]\n spec-graph query <selector|project:rule> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [rule-id] [--explain]\n spec-graph diff <before.json> <after.json> [--format human|json|markdown]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector - or a registered project rule, by its id - and\n print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the diagnostics that will run, built in and project. Name one\n to see only that one; --explain adds its selector and the ADR that\n decided it.\n diff Compare two graph exports: documents added, removed, moved or\n accepted; relations added or removed; and obligations resolved or\n reopened where they can be told apart. Make both exports with the\n same spec-graph. Exits 0 whether anything changed or not.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against. Without it,\n .spec-graph.json is looked for in the working\n directory and then upward as far as the repository,\n and the directory holding it becomes the root - so a\n run from a subdirectory reports what a run from the\n top reports. Paths typed on the command line stay\n relative to where they were typed.\n --ignore <glob> Skip paths. Repeatable.\n --ignore-ref <glob> Do not report these reference targets when they fail\n to resolve, for repositories where [[...]] tags a\n concept rather than naming a file. Repeatable.\n Suppresses findings only, never edges.\n --family <name> Families a bare identifier in prose may name. When\n given, everything else stays prose. Repeatable.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable.\n --history <glob> Files that log what was decided rather than deciding\n it - journals, changelogs, minutes. Their links are\n still checked; their obligations are not. Repeatable.\n --baseline <file> Accept the findings recorded in this file and report\n only what is new since. The file has to be there: a\n path that reads nothing accepts nothing and says so\n nowhere. A \"baseline\" in the configuration may name a\n file not recorded yet; a path typed here may not.\n --record-baseline <f> Write today's findings to this file as accepted debt,\n and exit 0 without judging them.\n --ratchet Also fail when a baseline entry no longer occurs, so\n a paid-off exemption cannot outlive the defect.\n --no-config Ignore .spec-graph.json and the package.json key.\n --format <fmt> human, json, sarif, gitlab or markdown. sarif is the\n interchange format GitHub code scanning and editors\n already read, and gitlab the Code Quality report a\n GitLab merge request reads, both for check; markdown\n is a table for a pull-request comment or\n $GITHUB_STEP_SUMMARY, for check or diff\n (default: human)\n --graph-format <fmt> dot, mermaid or json (default: dot)\n --documents-only Leave items out of the exported graph\n --rule <id>=<severity> Override one rule: error, warn, info or off. A project\n rule is named in full: project:no-draft-dependency.\n Repeatable.\n --max <n> Show at most n findings (0 = no limit)\n --max-warnings <n> Fail when warnings exceed n (default: no limit)\n --strict Raise every warning to an error. An explicit --rule\n still wins, so --strict --rule x=warn exempts x.\n --color / --no-color Force colour on or off\n --ascii Use ASCII glyphs only\n --verbose Include parse problems, per-file detail, and every\n reference --ignore-ref or --ignore-family silenced\n -h, --help Show this help\n -v, --version Show the version\n\nSELECTORS\n A selector is a path through the graph.\n\n item[openness=open] -delegates-to-> document[phase=retired]\n document[phase=active] -assumes-> document[phase=retired]\n document =supersedes=> document\n *[path^=docs/adr] -contains-> item[state=accepted-debt]\n\n Node types: document, item, * (any)\n Attributes: id, kind, title, path, file, line, phase, status, receptivity,\n alias, document, state (or disposition), openness, section,\n text, body, evidence, conflicted, fm.<front-matter-key>\n Operators: = != ^= $= *= ~= and [attr] for \"is present\"\n ~= is a regular expression, matched by an automaton that cannot\n backtrack: linear in the subject, whatever the pattern. It reads\n the usual syntax minus backreferences and lookaround, which are\n not regular - both are refused when the selector is read, with\n the character pointed at\n Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)\n\nEXIT CODES\n 0 clean\n 1 findings at error severity (or over --max-warnings)\n 2 the tool could not run\n\nCONFIGURATION\n Anything repeated on every run belongs in the repository rather than in the\n command. spec-graph reads the first of these that exists:\n\n .spec-graph.json, spec-graph.config.json, or a \"spec-graph\" key in\n package.json\n\n { \"patterns\": [\"docs/**/*.md\"],\n \"ignoreReferences\": [\"trap *\"],\n \"ignoreFamilies\": [\"RFC\"],\n \"historyPatterns\": [\"**/JOURNAL_*.md\", \"archive/**\"],\n \"baseline\": \".spec-graph-baseline.json\",\n \"severities\": { \"self-reference\": \"off\" },\n \"strict\": true }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it.\n\n A problem in that file stops the run with exit 2, named on stderr: invalid\n JSON, an unknown key, a rule that does not compile. A configuration that did\n not load checks a different repository than the one configured, and would\n report that one as consistent. --no-config runs on defaults instead.\n\nPROJECT RULES\n A convention spec-graph never anticipated is a selector plus a sentence, and\n belongs in the same file. Every rule declared here runs beside the built-ins\n and is reported, baselined, escalated by --strict and silenced by --rule in\n exactly the same way.\n\n { \"rules\": {\n \"no-draft-dependency\": {\n \"query\": \"document[phase=active] -depends-on-> document[phase=draft]\",\n \"message\": \"{0} depends on {1}, which is still a draft\",\n \"hint\": \"wait for {1} to be accepted, or drop it from {0.path}\",\n \"severity\": \"error\" } } }\n\n query One selector, or a list of them read as a union.\n message The headline. {0} is the first node on the path, {1} the next;\n {1.phase} and {0.fm.owner} read any selector attribute.\n hint The next action. Optional, and templated the same way.\n severity error, warn, info or off. Defaults to warn - a rule a team has\n just written has not yet earned the right to fail their build.\n\n The id is the name with project: in front, which is why it can never collide\n with a built-in. A selector that does not parse, or a {2} the query can never\n reach, is reported when the file is read rather than found missing later.\n\n To see what one matches without copying its selector out of the file:\n\n spec-graph query project:no-draft-dependency --verbose\n\nADOPTING THIS ON AN OLD REPOSITORY\n Record what is already wrong, then report only what happens next:\n\n spec-graph check --record-baseline .spec-graph-baseline.json\n git add .spec-graph-baseline.json\n\n The file is keyed by specification and citation, not by line number, so it\n survives edits, moves and renames. Findings that stop occurring are reported\n so the file can be tightened; nothing new gets in.\n\nEXAMPLES\n spec-graph \"docs/**/*.md\"\n spec-graph check --rule self-reference=off --format json\n spec-graph query 'item[openness=open] -delegates-to-> document[phase=retired]'\n spec-graph check --ignore-ref \"trap *\" # [[trap 55]] tags a concept, not a file\n spec-graph graph --documents-only --graph-format mermaid > graph.mmd\n spec-graph check --history \"**/JOURNAL_*.md\" # a log is not a specification\n spec-graph check --baseline .spec-graph-baseline.json\n spec-graph rules --explain # including this repository's own\n spec-graph rules ghost-handover --explain # and why it exists\n spec-graph check --rule project:no-draft-dependency=off\n spec-graph query project:no-draft-dependency # what does that rule match?\n spec-graph check --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n spec-graph check --format gitlab > gl-code-quality-report.json\n spec-graph diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
|
|
80
|
+
export declare const HELP = "spec-graph - turn Markdown specifications into a verifiable graph\n\nUSAGE\n spec-graph [check] [patterns...] [options]\n spec-graph query <selector|project:rule> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [rule-id] [--explain]\n spec-graph diff <before.json> <after.json> [--format human|json|markdown]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector - or a registered project rule, by its id - and\n print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the diagnostics that will run, built in and project. Name one\n to see only that one; --explain adds its selector and the ADR that\n decided it.\n diff Compare two graph exports: documents added, removed, moved or\n accepted; relations added or removed; and obligations resolved or\n reopened where they can be told apart. Make both exports with the\n same spec-graph. Exits 0 whether anything changed or not.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against. Without it,\n .spec-graph.json is looked for in the working\n directory and then upward as far as the repository,\n and the directory holding it becomes the root - so a\n run from a subdirectory reports what a run from the\n top reports. Paths typed on the command line stay\n relative to where they were typed.\n --ignore <glob> Skip paths. Repeatable.\n --ignore-ref <glob> Do not report these reference targets when they fail\n to resolve, for repositories where [[...]] tags a\n concept rather than naming a file. Repeatable.\n Suppresses findings only, never edges.\n --family <name> Families a bare identifier in prose may name. When\n given, everything else stays prose. Repeatable.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable.\n --history <glob> Files that log what was decided rather than deciding\n it - journals, changelogs, minutes. Their links are\n still checked; their obligations are not. Repeatable.\n --baseline <file> Accept the findings recorded in this file and report\n only what is new since. The file has to be there: a\n path that reads nothing accepts nothing and says so\n nowhere. A \"baseline\" in the configuration may name a\n file not recorded yet; a path typed here may not.\n --record-baseline <f> Write today's findings to this file as accepted debt,\n and exit 0 without judging them.\n --ratchet Also fail when a baseline entry no longer occurs, so\n a paid-off exemption cannot outlive the defect.\n --no-config Ignore .spec-graph.json and the package.json key.\n --format <fmt> human, json, sarif, gitlab or markdown. sarif is the\n interchange format GitHub code scanning and editors\n already read, and gitlab the Code Quality report a\n GitLab merge request reads, both for check; markdown\n is a table for a pull-request comment or\n $GITHUB_STEP_SUMMARY, for check or diff\n (default: human)\n --graph-format <fmt> dot, mermaid or json (default: dot)\n --documents-only Leave items out of the exported graph\n --rule <id>=<severity> Override one rule: error, warn, info or off. A project\n rule is named in full: project:no-draft-dependency.\n Repeatable.\n --max <n> Show at most n findings (0 = no limit)\n --max-warnings <n> Fail when warnings exceed n (default: no limit)\n --strict Raise every warning to an error. An explicit --rule\n still wins, so --strict --rule x=warn exempts x.\n --color / --no-color Force colour on or off\n --ascii Use ASCII glyphs only\n --verbose Include parse problems, per-file detail, and every\n reference --ignore-ref or --ignore-family silenced\n -h, --help Show this help\n -v, --version Show the version\n\nSELECTORS\n A selector is a path through the graph.\n\n item[openness=open] -delegates-to-> document[phase=retired]\n document[phase=active] -assumes-> document[phase=retired]\n document =supersedes=> document\n *[path^=docs/adr] -contains-> item[state=accepted-debt]\n\n Node types: document, item, * (any)\n Attributes: id, kind, title, path, file, line, phase, status, receptivity,\n alias, document, state (or disposition), openness, section,\n text, body, evidence, conflicted, fm.<front-matter-key>\n Operators: = != ^= $= *= ~= and [attr] for \"is present\"\n ~= is a regular expression, matched by an automaton that cannot\n backtrack: linear in the subject, whatever the pattern. It reads\n the usual syntax minus backreferences and lookaround, which are\n not regular - both are refused when the selector is read, with\n the character pointed at\n Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)\n\nEXIT CODES\n 0 clean\n 1 findings at error severity (or over --max-warnings)\n 2 the tool could not run\n\nCONFIGURATION\n Anything repeated on every run belongs in the repository rather than in the\n command. spec-graph reads the first of these that exists:\n\n .spec-graph.json, spec-graph.config.json, or a \"spec-graph\" key in\n package.json\n\n { \"patterns\": [\"docs/**/*.md\"],\n \"ignoreReferences\": [\"trap *\"],\n \"ignoreFamilies\": [\"RFC\"],\n \"historyPatterns\": [\"**/JOURNAL_*.md\", \"archive/**\"],\n \"baseline\": \".spec-graph-baseline.json\",\n \"severities\": { \"self-reference\": \"off\" },\n \"strict\": true,\n \"maxRelated\": 3 }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it. maxRelated has no flag: it caps how many related locations one\n finding lists, 8 unless set, and 0 lists none.\n\n A problem in that file stops the run with exit 2, named on stderr: invalid\n JSON, an unknown key, a rule that does not compile. A configuration that did\n not load checks a different repository than the one configured, and would\n report that one as consistent. --no-config runs on defaults instead.\n\nPROJECT RULES\n A convention spec-graph never anticipated is a selector plus a sentence, and\n belongs in the same file. Every rule declared here runs beside the built-ins\n and is reported, baselined, escalated by --strict and silenced by --rule in\n exactly the same way.\n\n { \"rules\": {\n \"no-draft-dependency\": {\n \"query\": \"document[phase=active] -depends-on-> document[phase=draft]\",\n \"message\": \"{0} depends on {1}, which is still a draft\",\n \"hint\": \"wait for {1} to be accepted, or drop it from {0.path}\",\n \"severity\": \"error\" } } }\n\n query One selector, or a list of them read as a union.\n message The headline. {0} is the first node on the path, {1} the next;\n {1.phase} and {0.fm.owner} read any selector attribute.\n hint The next action. Optional, and templated the same way.\n severity error, warn, info or off. Defaults to warn - a rule a team has\n just written has not yet earned the right to fail their build.\n\n The id is the name with project: in front, which is why it can never collide\n with a built-in. A selector that does not parse, or a {2} the query can never\n reach, is reported when the file is read rather than found missing later.\n\n To see what one matches without copying its selector out of the file:\n\n spec-graph query project:no-draft-dependency --verbose\n\nADOPTING THIS ON AN OLD REPOSITORY\n Record what is already wrong, then report only what happens next:\n\n spec-graph check --record-baseline .spec-graph-baseline.json\n git add .spec-graph-baseline.json\n\n The file is keyed by specification and citation, not by line number, so it\n survives edits, moves and renames. Findings that stop occurring are reported\n so the file can be tightened; nothing new gets in.\n\nEXAMPLES\n spec-graph \"docs/**/*.md\"\n spec-graph check --rule self-reference=off --format json\n spec-graph query 'item[openness=open] -delegates-to-> document[phase=retired]'\n spec-graph check --ignore-ref \"trap *\" # [[trap 55]] tags a concept, not a file\n spec-graph graph --documents-only --graph-format mermaid > graph.mmd\n spec-graph check --history \"**/JOURNAL_*.md\" # a log is not a specification\n spec-graph check --baseline .spec-graph-baseline.json\n spec-graph rules --explain # including this repository's own\n spec-graph rules ghost-handover --explain # and why it exists\n spec-graph check --rule project:no-draft-dependency=off\n spec-graph query project:no-draft-dependency # what does that rule match?\n spec-graph check --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n spec-graph check --format gitlab > gl-code-quality-report.json\n spec-graph diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
|
|
81
81
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
82
82
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
83
83
|
/** Runs the CLI and returns the process exit code. Never throws. */
|