@descent-vtt/spec-brief 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/README.md +92 -22
  3. package/dist/archive.d.ts +52 -2
  4. package/dist/archive.js +205 -37
  5. package/dist/archive.js.map +1 -1
  6. package/dist/brief.d.ts +2 -0
  7. package/dist/brief.js +5 -1
  8. package/dist/brief.js.map +1 -1
  9. package/dist/cli.d.ts +1 -1
  10. package/dist/cli.js +48 -14
  11. package/dist/cli.js.map +1 -1
  12. package/dist/collisions.d.ts +43 -12
  13. package/dist/collisions.js +90 -58
  14. package/dist/collisions.js.map +1 -1
  15. package/dist/config.d.ts +18 -0
  16. package/dist/config.js +66 -11
  17. package/dist/config.js.map +1 -1
  18. package/dist/corpus.d.ts +21 -1
  19. package/dist/corpus.js +51 -2
  20. package/dist/corpus.js.map +1 -1
  21. package/dist/engine.d.ts +33 -3
  22. package/dist/engine.js +112 -9
  23. package/dist/engine.js.map +1 -1
  24. package/dist/frontmatter.d.ts +18 -61
  25. package/dist/frontmatter.js +31 -300
  26. package/dist/frontmatter.js.map +1 -1
  27. package/dist/glob.d.ts +77 -54
  28. package/dist/glob.js +104 -433
  29. package/dist/glob.js.map +1 -1
  30. package/dist/index.d.ts +10 -8
  31. package/dist/index.js +8 -6
  32. package/dist/index.js.map +1 -1
  33. package/dist/links.d.ts +1 -1
  34. package/dist/links.js +3 -4
  35. package/dist/links.js.map +1 -1
  36. package/dist/lint.d.ts +33 -2
  37. package/dist/lint.js +9 -2
  38. package/dist/lint.js.map +1 -1
  39. package/dist/markdown.d.ts +27 -23
  40. package/dist/markdown.js +66 -235
  41. package/dist/markdown.js.map +1 -1
  42. package/dist/plugins.d.ts +14 -0
  43. package/dist/plugins.js +142 -6
  44. package/dist/plugins.js.map +1 -1
  45. package/dist/report.d.ts +40 -3
  46. package/dist/report.js +227 -11
  47. package/dist/report.js.map +1 -1
  48. package/dist/rules.d.ts +22 -4
  49. package/dist/rules.js +175 -45
  50. package/dist/rules.js.map +1 -1
  51. package/dist/scaffold.d.ts +1 -1
  52. package/dist/scaffold.js +10 -9
  53. package/dist/scaffold.js.map +1 -1
  54. package/dist/schedule.d.ts +133 -0
  55. package/dist/schedule.js +265 -0
  56. package/dist/schedule.js.map +1 -0
  57. package/dist/scope.d.ts +52 -0
  58. package/dist/scope.js +56 -0
  59. package/dist/scope.js.map +1 -0
  60. package/dist/text.d.ts +13 -0
  61. package/dist/text.js +23 -0
  62. package/dist/text.js.map +1 -1
  63. package/dist/trigger.d.ts +17 -0
  64. package/dist/trigger.js +49 -0
  65. package/dist/trigger.js.map +1 -0
  66. package/dist/types.d.ts +14 -5
  67. package/dist/types.js.map +1 -1
  68. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
  69. package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
  70. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
  71. package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
  72. package/dist/vendor/spec-core/markdown/headings.js +199 -0
  73. package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
  74. package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
  75. package/dist/vendor/spec-core/markdown/index.js +4 -0
  76. package/dist/vendor/spec-core/markdown/index.js.map +1 -0
  77. package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
  78. package/dist/vendor/spec-core/markdown/layout.js +28 -0
  79. package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
  80. package/dist/vendor/spec-core/markdown/links.d.ts +25 -0
  81. package/dist/vendor/spec-core/markdown/links.js +394 -0
  82. package/dist/vendor/spec-core/markdown/links.js.map +1 -0
  83. package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
  84. package/dist/vendor/spec-core/markdown/lists.js +127 -0
  85. package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
  86. package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
  87. package/dist/vendor/spec-core/markdown/scan.js +463 -0
  88. package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
  89. package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
  90. package/dist/vendor/spec-core/markdown/syntax.js +181 -0
  91. package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
  92. package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
  93. package/dist/vendor/spec-core/markdown/tables.js +86 -0
  94. package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
  95. package/dist/vendor/spec-core/markdown/types.d.ts +290 -0
  96. package/dist/vendor/spec-core/markdown/types.js +9 -0
  97. package/dist/vendor/spec-core/markdown/types.js.map +1 -0
  98. package/dist/vendor/spec-core/path/index.d.ts +1 -0
  99. package/dist/vendor/spec-core/path/index.js +2 -0
  100. package/dist/vendor/spec-core/path/index.js.map +1 -0
  101. package/dist/vendor/spec-core/path/posix.d.ts +83 -0
  102. package/dist/vendor/spec-core/path/posix.js +150 -0
  103. package/dist/vendor/spec-core/path/posix.js.map +1 -0
  104. package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
  105. package/dist/vendor/spec-core/pattern/automaton.js +340 -0
  106. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
  107. package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
  108. package/dist/vendor/spec-core/pattern/charset.js +116 -0
  109. package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
  110. package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
  111. package/dist/vendor/spec-core/pattern/glob.js +481 -0
  112. package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
  113. package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
  114. package/dist/vendor/spec-core/pattern/index.js +4 -0
  115. package/dist/vendor/spec-core/pattern/index.js.map +1 -0
  116. package/dist/vendor/spec-core/pattern/regex.d.ts +67 -0
  117. package/dist/vendor/spec-core/pattern/regex.js +813 -0
  118. package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
  119. package/dist/vendor/spec-core/text/index.d.ts +2 -0
  120. package/dist/vendor/spec-core/text/index.js +2 -0
  121. package/dist/vendor/spec-core/text/index.js.map +1 -0
  122. package/dist/vendor/spec-core/text/lines.d.ts +72 -0
  123. package/dist/vendor/spec-core/text/lines.js +156 -0
  124. package/dist/vendor/spec-core/text/lines.js.map +1 -0
  125. package/package.json +4 -3
  126. package/schema.json +17 -0
  127. package/src/vendor/spec-core/LICENSE +21 -0
  128. package/src/vendor/spec-core/README.md +8 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,307 @@
2
2
 
3
3
  Notable changes, newest first. Versions follow [semver](https://semver.org).
4
4
 
5
+ ## 0.2.1
6
+
7
+ `spec-brief init` points `$schema` at the schema of the installed version, and
8
+ the README's links to files the package does not ship resolve on npmjs.com and
9
+ in `node_modules`. spec-core's scanner now reads an image inside a link's text
10
+ itself; archival rewrites and restores such a badge as 0.2.0 did, each
11
+ destination once. Nothing needs to change in a repository that upgrades.
12
+
13
+ ### Changed
14
+
15
+ - `npm publish` in a checkout refuses to run outside GitHub Actions, so a
16
+ version cannot reach npm from a workstation by mistake, without provenance;
17
+ spec-harness 0.1.0 did. The release never runs it: it stages a tarball it
18
+ packed.
19
+ - spec-core at 8840d36. Its Markdown scanner reads an image inside a link's
20
+ text, as CommonMark renders it, and lists it after the link it lies in:
21
+ `[![build](badge.svg)](actions)` gives both. spec-brief read the link's
22
+ text itself, and still does for what the scanner leaves there - a link in
23
+ a link's text, which CommonMark takes as the link - so a destination found
24
+ both ways is kept once, and archival rewrites the badge inside a link once,
25
+ as before, and restores it on reopening.
26
+ - The scanner makes a document's links, list items and directives mask the
27
+ first time they are read, and keeps them. The answers are the same.
28
+
29
+ ### Fixed
30
+
31
+ - `spec-brief init` points `$schema` at
32
+ `./node_modules/@descent-vtt/spec-brief/schema.json`, the schema of the
33
+ version installed, as the README recommends. It wrote a URL on GitHub that
34
+ follows main, so an editor judged the configuration by a schema the
35
+ installed version might not share. A configuration `init` already wrote
36
+ keeps its URL until it is edited; either loads.
37
+ - The README's links to `docs/adr/` and `src/config.ts` are GitHub URLs. The
38
+ package ships neither, so the relative links were dead on npmjs.com and in
39
+ `node_modules`. A test holds every link in a document the package ships to
40
+ a file it ships, or to one the repository holds by URL.
41
+
42
+ ## 0.2.0
43
+
44
+ Scopes are read by spec-core's glob automaton, so a collision is proved with a
45
+ file both briefs would write and protected paths are subtracted from a scope
46
+ exactly. `spec-brief schedule` places briefs in waves from their dependencies
47
+ and provable collisions. Deferred work waits on an observable trigger. A plugin
48
+ may waive a `protected-file` or `out-of-scope` refusal, which is how
49
+ spec-harness applies a signed ruling. Briefs are read with spec-core's
50
+ CommonMark scanner, and the review of 0.1.0 is fixed throughout.
51
+
52
+ ### Upgrading from 0.1.0
53
+
54
+ - **A configuration that already uses the word "deferred" no longer loads.**
55
+ Deferred work is a new status, and its word is `"deferred"` unless the
56
+ configuration says otherwise, so `"status": { "draft": "deferred" }` now
57
+ names two statuses with one word and stops the run with exit 2: "the
58
+ status words must differ: "deferred" is the word for draft and deferred".
59
+ Set `"status": { "deferred": null }` where the repository defers no work,
60
+ or give the new status a word of its own, `"deferred": "parked"`.
61
+ - **JSON documents are `schemaVersion: 2`**, every command's: the version
62
+ is one number for all of them, and `matrix`'s document changed meaning. A
63
+ collision is a pair of briefs and carries `overlaps`, a list of
64
+ `{ patterns, witness }`, in place of `patterns` and `witness`, and a
65
+ witness is always a file, never a directory; a `sharedDirectories` entry
66
+ carries `directories` in place of `directory`; and deferred briefs, and
67
+ the briefs waiting on them, left `waves[].briefs` for `deferred` and
68
+ `waiting`. `list`, `lint` and `archive` changed only by adding: `trigger`
69
+ and `sections`, a `"deferred"` status, and `path` and `paths` on a
70
+ finding. A reader of those documents reads version 2 as it read 1.
71
+ - **`**` inside a name is an error**, where it was read as `*`: a scope
72
+ written `docs/**.md` is a `glob` error in `lint`, and a `files` or
73
+ `exclude` pattern written so stops the run with exit 2. Parentheses are
74
+ literal unless a group holds a `|`. Both are under "Globs" below.
75
+ - **A note that continues its box's paragraph no longer closes the box**:
76
+ `- [ ] Add a cache` over ` **Rejected** by 012` refuses an archival as
77
+ `open-task` until the note is a bullet or has a blank line above it. See
78
+ the entry on open boxes below.
79
+
80
+ ### Breaking (library API)
81
+
82
+ - `Config['status']['deferred']` is required, `null` where a repository
83
+ has no such word. A configuration from `resolveConfig` or
84
+ `DEFAULT_CONFIG` has it; one built by hand must set it.
85
+ - `Status` gains `'deferred'`, and `Format` and `FORMATS` gain
86
+ `'gitlab'`: an exhaustive `switch` over either needs the new case.
87
+ - `CollisionReport` gains the required `deferred` and `waiting`, and
88
+ `WaveMatrix` the required `undecided`; a report built by hand carries
89
+ them. A wave's `briefs` no longer holds a deferred brief or one waiting on
90
+ deferred work.
91
+ - `Collision` carries `overlaps` in place of `patterns` and `witness`, and
92
+ `SharedDirectory` carries `directories` in place of `directory`, as the
93
+ JSON does.
94
+ - `parseGlob`'s `isFile` option is replaced by `literal` (`'file'`,
95
+ `'directory'`, `'either'` or a function, such as `readingIn(files)`), a
96
+ `Glob` carries `compiled` and `literals` in place of `alternatives`, and
97
+ `intersectGlobs` throws where the search is undecided.
98
+
99
+ ### Changes
100
+
101
+ - Markdown is read by spec-core's scanner and front matter by its reader,
102
+ copied into `src/vendor/spec-core/` beside the glob engine and verified by
103
+ hash (ADR-0001, amended). What a brief's structure is stays as it was: a
104
+ section, and the title, is an ATX heading, `## Name`, outside a block
105
+ quote, so prose over a `---` line is still prose and a rule, not a section,
106
+ and a heading quoted from another document is not one of the brief's; a task
107
+ is a list item outside a block quote with `[ ]`, `[x]` or `[X]`. What counts
108
+ as code, a comment or a link is now what CommonMark says it is:
109
+ - **Indented code and raw-text HTML are code.** Four spaces at the top
110
+ level after a blank line, and a `<pre>`, `<script>`, `<style>` or
111
+ `<textarea>` block, hold no heading, task or link: a box in an indented
112
+ example no longer refuses an archival, and a fence indented four spaces
113
+ there no longer hides the rest of the brief. Inside a list item, code
114
+ starts four columns past the item's text: a box written that deep after a
115
+ blank line is code, and a fence that deep opens nothing.
116
+ - **A `<!--` in the middle of a line with no `-->` after it is text.** It
117
+ hid everything after it, so every later section was missing. One that
118
+ opens a line still runs to the end.
119
+ - **A heading's text leaves out a comment on its line**:
120
+ `## Intent <!-- required -->` fills `Intent`, where it was reported
121
+ missing.
122
+ - **A code span may close on a later line of its paragraph**, and a link
123
+ inside one is not rewritten.
124
+ - **A thematic break ends a task**, so a note after `***` no longer closes
125
+ the box above it; and **a tab indents to the next multiple of four
126
+ columns**, so a tab-indented note under a box indented by two spaces is
127
+ part of the item.
128
+ - **A link is one CommonMark reads.** `a](b.md)` and `[a](b.md c)` are text
129
+ and are no longer rewritten on archival; a definition in a block quote
130
+ is, and so is a destination after link text that runs over lines.
131
+ - **TOML front matter** between `+++` lines is recognised. `front-matter`
132
+ reports it, where it was read as body text and only the missing status
133
+ was reported.
134
+ - **An inline list on the line under its key** - `affectedFiles:` and then
135
+ ` [src/**]` - is reported as "an inline list starts on the line after
136
+ its key; write it after the colon", where the advice was to quote it,
137
+ which would have made the list a string.
138
+ - Nothing is written into front matter that cannot hold it: TOML, or a block
139
+ never closed. `schedule --write` and `unarchive` refuse such a brief as
140
+ `front-matter`, and so does `archive` where lint's own `front-matter` error
141
+ does not refuse it already. `unarchive` stopped with an unexpected error on
142
+ a block never closed, as `archive` did with an empty banner template, and
143
+ `archive` otherwise prepended a second block above the banner.
144
+ - Reopening a brief that had no front matter, in a repository with no status
145
+ field, gives it back with none. Archival wrote a block to hold the
146
+ integrity hash, and `unarchive` took the hash out and left `---` over
147
+ `---` behind; a block the reopening empties now goes with it.
148
+ - A carriage return that ends no line, as a `\r\r\n` ending leaves one, is
149
+ read as a space; line numbers are unchanged. A front-matter line ending in
150
+ one now reads, where it was "not a key: value line".
151
+ - Library API: `Brief.frontMatter` is spec-core's `FrontMatter`. It gains
152
+ `kind` (`'yaml'` or `'toml'`), and each entry gains `parent`, `keyStart`,
153
+ `valueStart` and `valueEnd`, offsets into the brief's `lines` joined by
154
+ `\n`. `Brief.scan` gains `links`, the destinations archival rewrites.
155
+ - Globs are spec-core's `path` dialect, matched and intersected by its
156
+ automaton, which is copied into `src/vendor/spec-core/` and verified by
157
+ hash. The syntax is unchanged but for two refusals; what it means changes
158
+ in four places.
159
+ - **`**` inside a name is an error**: `docs/**.md`, `**.ts`, `a**b`. It
160
+ was read as `*`, one level, and dropped every nested file from a scope
161
+ that tools elsewhere read as recursive. The message says to write
162
+ `docs/**/*.md` for any depth, or `*.md` for one level. A scope pattern
163
+ written so is a `glob` error in `lint`, and a `files` or `exclude`
164
+ pattern stops the run with exit 2.
165
+ - **Parentheses are literal unless a group holds a `|`**:
166
+ `C++(notes).md`, `*(2017).md` and `@(a)` are names, where every
167
+ `?(`, `*(`, `+(`, `@(` or `!(` was refused as an extended glob.
168
+ `+(a|b)` is still refused, and the message now says to write `{a,b}`,
169
+ and a literal parenthesis as `[(]`.
170
+ - **A literal path is read from the tree.** A file the tree holds is that
171
+ file, a directory it holds is everything beneath it, and a path it does
172
+ not hold is a file unless written with a trailing `/`. The extension
173
+ guess is gone: it read `Dockerfile` as a directory, so `Dockerfile` and
174
+ `**/x.ts` collided through `Dockerfile/x.ts`, and read `docs/v1.2` as a
175
+ file, missing every collision inside it. Without a tree - the library
176
+ called with no files - every literal is a file.
177
+ - **A trailing `/**` or `/` is a directory's contents**, at least one name
178
+ below it: `src/**` no longer matches `src`, and a collision witness is
179
+ always a file, `docs/adr/x` where it was `docs/adr`.
180
+ - **`./` alone, and a `..` segment, are refused in the core's words**
181
+ ("the pattern names the root itself", "a pattern cannot climb out of its
182
+ root"), and a lone `}` is a literal rather than an error. A leading `/`
183
+ and a leading `!` are refused as before.
184
+ - **Protection wins.** What a brief may write is `affectedFiles` less
185
+ `protectedFiles`, so `affectedFiles: [src/**]` with
186
+ `protectedFiles: [src/db/schema.ts]` is valid. `scope-contradiction` now
187
+ fires only for affected patterns the protections cover entirely, as one
188
+ finding per brief naming each; it used to fire for any file in both.
189
+ `matrix` leaves out a file either brief of a pair protects.
190
+ - `literal-read-as-file`, a note: a path with no glob syntax that the tree
191
+ does not hold and whose name has no extension, such as `src/newmod`, is
192
+ read as a file, and the hint says to write `src/newmod/` for a directory.
193
+ `glob-matches-nothing` leaves such a pattern to it.
194
+ - `collision-undecided`, a warning: a pair of briefs whose collision the
195
+ witness search could not decide within its budget. It is never reported
196
+ as a collision and never as clean. `matrix` marks the pair `?`, and its
197
+ JSON gains an `undecided` list per wave (`{ a, b, patterns }`).
198
+ - Deferred work has a status: `status.deferred` in configuration, `"deferred"`
199
+ by default and `null` for a repository without one. A deferred brief lives
200
+ in the briefs directory, is never ready, is listed and left out by
201
+ `matrix`, and is refused by `archive` as `archive-deferred`. It names the
202
+ event that brings it back in `trigger`, a new built-in field that `list`
203
+ reports. A brief that depends on deferred work, directly or through another
204
+ brief, runs in no wave either: `matrix` lists it under `waits`, and in JSON
205
+ under `waiting` with the brief it waits on, and compares it with nothing,
206
+ as `schedule` places it nowhere.
207
+ - `deferral-trigger`, an error: a deferred brief with no `trigger`, or one
208
+ that is only a date or a time - `2026-10`, `Q3`, `next month`, `October`,
209
+ `in two weeks` - or a placeholder. "when the second tenant signs" and
210
+ "when p95 > 200 ms" pass.
211
+ - `spec-brief schedule` computes the waves the live briefs can run in
212
+ (ADR-0011): dependency order, ties by id, each brief in the lowest wave
213
+ after its live dependencies that holds nothing it collides with, a brief
214
+ with no scope in a wave of its own. Each brief's proposed wave is shown
215
+ beside its declared one with the reason for a move - the dependency, or the
216
+ brief and the file that kept it out of a wave - and deferred briefs, the
217
+ briefs waiting on them, and dependency cycles are listed. `--write` sets
218
+ `wave` in the front matter of each brief that moves, every other line
219
+ untouched and a comment after the old wave kept, as one transaction, and
220
+ writes nothing over a cycle. A declared
221
+ wave that differs from the computed one is judged as `lint` and `matrix`
222
+ judge it: no collision in it, dependencies before it, dependents after it.
223
+ One that holds is a person's choice to keep, and its move is a note.
224
+ Exit 0 when every declared wave holds, 1 when a brief declares no wave or
225
+ one that does not hold, or on a cycle. Pretty, JSON, SARIF, GitHub and
226
+ GitLab output; `wave-schedule`, an error by default, is the finding per
227
+ move, and a note for a move whose declared wave holds.
228
+ - A plugin may export a `waive` hook beside its rules (ADR-0007, amended).
229
+ After an archival is planned, and only when it has a refusal a plugin may
230
+ lift, each hook is given `{ root, brief: { id, file, text }, findings,
231
+ base, commit }`, a frozen copy, and answers `[{ rule, path, reason }]`. A
232
+ `protected-file` or `out-of-scope` refusal of the plan's own that it names
233
+ by rule and path becomes a `waived` note; a waiver for any other rule is
234
+ ignored with a `waiver-ignored` warning, and one that matches no refusal
235
+ changes nothing. A hook that returns nothing waives nothing; one that
236
+ throws - a write to the copy included - or answers in another shape stops
237
+ the run with exit 2. This is how spec-harness's plugin lets a signed ruling
238
+ allow a protected file, without spec-brief reading signatures.
239
+ - A `protected-file` refusal names its file in a new `path` field, one
240
+ finding per file as before, and an `out-of-scope` finding lists its files
241
+ in `paths`. Both appear in the JSON of `archive`; a plugin reads them.
242
+ - `--format gitlab` wherever `sarif` is offered - `lint`, `matrix`,
243
+ `schedule` - writes a GitLab Code Quality report: an array of
244
+ `{ description, check_name, fingerprint, severity, location }`, errors as
245
+ `major`, warnings as `minor`, notes as `info`, and a fingerprint that
246
+ hashes the rule, the file and the message but not the line.
247
+ - 24 lint rules and 5 collision rules.
248
+ - The library's `parseGlob`, `matchGlob`, `intersectGlobs` and `globBase`
249
+ keep their signatures over the new engine, with the options and shapes
250
+ under "Breaking (library API)" above. `globWitness`, `globCovers`,
251
+ `globBases`, `readingIn`, `meet`, `scopeOf`, `contradictions` and
252
+ `waitingOnDeferred` are new. `intersectTokens` is gone.
253
+ - A `shared-directory` finding has a hint, and a `glob` finding says what a
254
+ scope pattern looks like.
255
+ - Versions are staged by CI through npm's trusted publishing, with
256
+ provenance that names the commit and the run, and a maintainer releases
257
+ each with a second factor (ADR-0010).
258
+ - `archive` no longer passes a scope it never measured. With no `--commit`,
259
+ `--base` or `archiving.base`, without git, or with a commit already in the
260
+ base branch - where the diff from the merge base is empty - a brief that
261
+ declares `affectedFiles` or `protectedFiles` gets `scope-unmeasured`, a
262
+ warning that names the next step, and a refusal under `--strict`. It is the
263
+ first archive rule, and configuration sets its severity like any other.
264
+ - An open box is closed only by a note under it that starts with a
265
+ disposition marker, and a note starts a bullet, a paragraph after a blank
266
+ line, or a line after one that ends a sentence (`.`, `?` or `!`, then
267
+ only closing marks). A marker anywhere in the item used to count, the
268
+ box's own text included, so `- [ ] Explain why the **Rejected** designs
269
+ failed` archived as dispositioned, and so did the same sentence wrapped
270
+ after "the", indented or not. A line that continues a sentence no longer
271
+ closes the box, whatever it starts with; `- [ ] Add a cache.` over
272
+ ` **Rejected** by 012` still does. A note under a box whose text ends
273
+ without a stop, `- [ ] Add a cache` over ` **Rejected** by 012`, now
274
+ refuses the archival as `open-task`, and the hint says to make it a bullet
275
+ or put a blank line above it.
276
+ - `archiving.rewriteLinks: false` now holds for the links other briefs hold
277
+ to a moving brief, which were rewritten regardless. Those left pointing at
278
+ the old path are reported as `stale-link`, a warning naming each file and
279
+ line, on `archive` and `unarchive` alike; `unarchive --strict` refuses on
280
+ it, as `archive --strict` does.
281
+ - `matrix` reports two briefs whose scopes meet as one collision, however
282
+ many pairs of their patterns meet, and the finding lists every pair with a
283
+ path both cover. A pair writing into several shared directories is likewise
284
+ one `shared-directory` finding.
285
+ - A plugin package whose `exports` offer only the `import` condition loads.
286
+ Packages were resolved with `require.resolve`, which reads `exports` under
287
+ the require conditions, so an ESM-only plugin could not be found.
288
+ A subpath is held to `exports` as Node holds it: a target, or what a
289
+ `*` matched, with a `.`, `..` or `node_modules` segment is refused,
290
+ split on `/` and on a backslash alike, so `pkg/..\..\outside` cannot
291
+ climb out of `node_modules` on Windows.
292
+ - A brace group is split on its own commas only: `{[,]x,y}` has the two
293
+ alternatives `[,]x` and `y`, where a comma or brace inside a class used to
294
+ split the group or unbalance it.
295
+ - A section rule takes a `hint`: what the section must answer. A new brief
296
+ carries it as a comment under the heading, `missing-section`,
297
+ `empty-section` and `placeholder` findings give it as their hint, and the
298
+ JSON of `list` and `lint` gains `sections`, every section the configuration
299
+ asks for with its type, switches and hint. The default sections carry one,
300
+ and `init` writes them out. `empty-section` and `placeholder` findings have
301
+ a hint even without one.
302
+ - A `--base` or `archiving.base` that names no commit stops `archive` with a
303
+ message that says so, where git's failure to diff from it surfaced as an
304
+ unexpected error with a stack trace.
305
+
5
306
  ## 0.1.0
6
307
 
7
308
  The first release.
package/README.md CHANGED
@@ -48,7 +48,11 @@ Every privilege change issues a new session token and invalidates the old one.
48
48
  - [ ] a rotated token is rejected by every endpoint
49
49
  ```
50
50
 
51
- The id comes from the file name (`012_rotate-session-tokens.md`) or from an `id` field. `affectedFiles` is the scope the round may write; `protectedFiles` is what it is not empowered to change, and archival refuses a round that changed it.
51
+ Work put off for later is a brief too, with `status: deferred` and a `trigger` naming the event that brings it back - "when the second tenant signs", "when p95 exceeds 200 ms", never a date. A deferred brief stays in the briefs directory, is never ready, and runs in no wave until someone sets it `active`.
52
+
53
+ The id comes from the file name (`012_rotate-session-tokens.md`) or from an `id` field. `affectedFiles` is the scope the round may write; `protectedFiles` is what it is not empowered to change, and archival refuses a round that changed it. Protection wins: what a round may write is `affectedFiles` less `protectedFiles`, so "all of `src/` but the schema" is `affectedFiles: [src/**]` with `protectedFiles: [src/db/schema.ts]`.
54
+
55
+ Scopes are globs in spec-core's `path` dialect, the one every spec-\* tool reads: `*`, `?`, `[a-z]` and `{a,b}` within a name, `**` for any number of directories, compared case-sensitively on every host. `**` is a whole segment: `docs/**.md` is refused, since tools read it three ways - write `docs/**/*.md` for any depth, or `docs/*.md` for one level. Parentheses are literal, `C++(notes).md`, unless a group holds a `|`: an extended glob such as `+(a|b)` is refused, and written `{a,b}`. A trailing `/**` or `/` is a directory's contents - at least one name below it, never the directory itself. A path with no glob syntax is read from the tree: a file the tree holds is that file, a directory it holds is everything beneath it, and a path it does not hold yet is a file unless it ends in `/`. Write `src/newmod/` for a directory the round creates; `lint` notes a bare `src/newmod` it cannot place.
52
56
 
53
57
  ## Commands
54
58
 
@@ -58,7 +62,7 @@ Writes `.spec-brief.json` with every default spelled out, and creates the brief
58
62
 
59
63
  ### `spec-brief new <title>`
60
64
 
61
- Scaffolds a brief with the next free number - one more than the highest id, live or archived, since ids are never reused - and every required section, each holding a hint in a comment. A comment is not content, so a fresh brief reads as unwritten until someone writes it. `--id`, `--type`, `--wave`, `--depends-on 7,8` and `--date` fill the front matter.
65
+ Scaffolds a brief with the next free number - one more than the highest id, live or archived, since ids are never reused - and every required section, each holding its `hint` in a comment. A comment is not content, so a fresh brief reads as unwritten until someone writes it. `--id`, `--type`, `--wave`, `--depends-on 7,8` and `--date` fill the front matter.
62
66
 
63
67
  ### `spec-brief lint [brief...]`
64
68
 
@@ -76,11 +80,11 @@ briefs/035_the-background-side.md
76
80
 
77
81
  ### `spec-brief list`
78
82
 
79
- Live briefs with their status, wave, task count and readiness. A brief is *ready* when every brief it depends on is archived. `--ready` shows only those, which is the question an orchestrator asks; `--archived` includes the archive; `--format json` gives an orchestrator the whole table.
83
+ Live briefs with their status, wave, task count and readiness. A brief is *ready* when every brief it depends on is archived, and it is neither a draft nor deferred. `--ready` shows only those, which is the question an orchestrator asks; `--archived` includes the archive; `--format json` gives an orchestrator the whole table, and the sections the configuration asks for, each with its `hint`.
80
84
 
81
85
  ### `spec-brief matrix`
82
86
 
83
- Compares the `affectedFiles` of every pair of live briefs in the same wave, `--all-waves` for every pair.
87
+ Compares the `affectedFiles` of every pair of live briefs in the same wave, `--all-waves` for every pair. Deferred briefs, and briefs that wait on one directly or through another, are listed and left out: they run in no wave until the deferred work comes back, as `schedule` places them nowhere, so the two agree on which briefs run side by side.
84
88
 
85
89
  ```text
86
90
  wave 1 · 3 briefs
@@ -92,28 +96,52 @@ wave 1 · 3 briefs
92
96
  ? 003 declares no affectedFiles and cannot be checked
93
97
  ```
94
98
 
95
- Scopes are intersected as globs, not compared as strings: the two above share no prefix and still meet, and the file named is one both patterns match. A brief with no scope is reported as unscoped rather than counted as safe. Exit 1 on a collision.
99
+ Scopes are intersected as globs, not compared as strings: the two above share no prefix and still meet, and the file named is one both patterns match and neither brief protects - always a file, never a directory. Two briefs whose scopes meet in several places are one collision, listing every pair of patterns that meets. A brief with no scope is reported as unscoped rather than counted as safe. The search for a shared file has a budget; a pair it cannot decide within it is marked `?` and reported as `collision-undecided`, a warning, never as a collision and never as clean. Exit 1 on a collision.
100
+
101
+ ### `spec-brief schedule`
102
+
103
+ Computes the waves the live briefs can run in, drafts included, and sets each beside the wave it declares. A brief runs after every dependency that is still live - an archived one is done - and shares a wave with no brief whose writable scope meets its own. Briefs are taken in dependency order, ties broken by id, and each goes into the lowest wave that is after its dependencies and holds nothing it collides with. A brief with no `affectedFiles` cannot be proved apart from anything, so it runs in a wave of its own and is reported as `unscoped`. The first wave is the lowest any brief declares, or 1.
104
+
105
+ ```text
106
+ wave 1 · 2 briefs
107
+ 001 Rotate session tokens
108
+ 003 Audit log moves from wave 2
109
+ wave 2 also holds
110
+ nothing holds it later
111
+ wave 2 · 1 brief
112
+ 002 Store sessions moves from wave 1
113
+ wave 1 does not hold: 001 there also writes src/auth/session.ts
114
+ not wave 1, where 001 also writes src/auth/session.ts ("src/**/session.ts" and "src/auth/**")
115
+
116
+ waits: 007 on 006, which is deferred
117
+ deferred: 006
118
+ 2 briefs would move; "spec-brief schedule --write" writes the waves
119
+ ```
120
+
121
+ Each move says why: the dependency that sets the earliest wave, and for every wave passed over, the brief there and the file both would write. It says first whether the declared wave holds - whether `lint` and `matrix` would pass it: no brief there that it collides with, its dependencies in earlier waves, and the briefs that depend on it in later ones. A deferred brief, and one that waits on it, is placed nowhere; a dependency cycle is an error, and nothing in or after it is placed. `--write` sets `wave` in the front matter of each brief whose wave changes - every other line as it was, in one transaction - and writes nothing over a cycle. A comment after the old wave, `wave: 2 # after the review`, stays after the new one. A brief whose lines end in both LF and CRLF is written with the ending of its first line throughout, as `archive` writes one. `--format json` gives an orchestrator the waves, each brief's declared and proposed wave with the reasons and whether the declared one holds, and what waits on what; `sarif`, `github` and `gitlab` carry a `wave-schedule` finding per move.
122
+
123
+ The result is the same every time for the same briefs. It is a valid schedule, not always the shortest: no fast method promises the fewest waves, and a person can always move a brief later by hand, which `lint` and `matrix` then check. So a move is an error only when the declared wave does not hold, or there is none: a declared wave that holds is the person's to keep, and its move is a note. Exit 0 when every declared wave holds, 1 when a brief declares no wave or one that does not hold, or the dependencies form a cycle, 2 when the run cannot be trusted.
96
124
 
97
125
  ### `spec-brief archive <brief>`
98
126
 
99
127
  Closes a round. Refused, with every reason, when:
100
128
 
101
129
  - the brief has lint errors, is a draft, or depends on a brief that is still live;
102
- - a task item is neither ticked nor dispositioned - an open box counts as closed when a note under it starts with one of the `dispositions` (`**Delegated`, `**Accepted debt`, `**Rejected` by default);
130
+ - a task item is neither ticked nor dispositioned - an open box counts as closed when a note under it starts with one of the `dispositions` (`**Delegated`, `**Accepted debt`, `**Rejected` by default), a note being a line under the box, above any box nested in it, that starts a bullet, a paragraph after a blank line, or a line after one that ends a sentence. A line that continues a sentence is the box's text wrapping, not a note, so `- [ ] Explain why the` over ` **Rejected** designs failed` is open, and `- [ ] Add a cache.` over ` **Rejected** by 012` is closed;
103
131
  - the working tree holds uncommitted work outside the brief directories (`--allow-dirty` to proceed);
104
- - the round's commit changed a file in `protectedFiles`.
132
+ - the round's commit changed a file in `protectedFiles` - one refusal per file, naming it, unless a plugin that verifies rulings [waives it](#waiving-a-refusal).
105
133
 
106
- Changes outside `affectedFiles` are a warning, and an error under `--strict`. Otherwise it writes the archived brief with its status set, a frozen banner under the front matter, every relative link rewritten for the archive directory, and an `integrity` hash; rewrites the links other live briefs hold to it; and removes the original. `--commit <rev>` records the commit and the files it changed; `--base <rev>` measures from the merge base instead, for a branch of several commits; `--pr <n>` links the pull request; `--summary <text>` is what the round did, in the banner. `--dry-run` prints the plan and the banner and writes nothing. Archiving an archived brief does nothing and exits 0.
134
+ Changes outside `affectedFiles` are a warning, and an error under `--strict`. A scope is never passed by checks that measured nothing: when the files the round changed are unknown - no `--commit`, `--base` or `archiving.base`, `--no-git`, or a commit already in the base branch, as on `main` after the merge - a brief that declares one gets `scope-unmeasured`, a warning, and a refusal under `--strict`. Otherwise it writes the archived brief with its status set, a frozen banner under the front matter, every relative link rewritten for the archive directory, and an `integrity` hash; rewrites the links other live briefs hold to it; and removes the original. With `archiving.rewriteLinks` off, no link is rewritten, and the links other live briefs are left holding are reported as `stale-link`. `--commit <rev>` records the commit and the files it changed; `--base <rev>` measures from the merge base instead, for a branch of several commits; `--pr <n>` links the pull request; `--summary <text>` is what the round did, in the banner. `--dry-run` prints the plan and the banner and writes nothing. Archiving an archived brief does nothing and exits 0.
107
135
 
108
136
  spec-brief reads git and never writes it. Review the change and commit it with the round.
109
137
 
110
138
  ### `spec-brief unarchive <brief>`
111
139
 
112
- Reopens an archived brief: the banner and the hash come off, the status goes back to the live word, and the links are rewritten again. A brief archived and reopened is the brief it was, blank lines and final newline included, with two exceptions: the status line is written in its plain spelling, and a relative link that had to be rewritten comes back in its shortest form (`./b.md` returns as `b.md`).
140
+ Reopens an archived brief: the banner and the hash come off, the status goes back to the live word, and the links are rewritten again. A brief archived and reopened is the brief it was, blank lines and final newline included, with four exceptions: the status line is written in its plain spelling; a relative link that had to be rewritten comes back in its shortest form (`./b.md` returns as `b.md`); an empty front matter block, `---` over `---`, comes back as none, since archival gives a brief with no front matter a block for its hash and reopening takes away a block it leaves empty; and a brief whose lines end in both LF and CRLF comes back with the ending of its first line throughout.
113
141
 
114
142
  ### Every command
115
143
 
116
- `--root <dir>` runs from another directory. `--config <file>` names the configuration; `--no-config` uses the defaults. `--format` is `pretty`, `json`, `sarif` or `github` where the command reports findings. `--strict` makes warnings fail the run. `--no-git` leaves git out even inside a repository: the tree is read from disk, and `archive` records no commit. `--color` and `--no-color` override `NO_COLOR`, `FORCE_COLOR` and the terminal check. `--help` and `--version` do what they say.
144
+ `--root <dir>` runs from another directory. `--config <file>` names the configuration; `--no-config` uses the defaults. `--format` is `pretty`, `json`, `sarif`, `github` or `gitlab` where the command reports findings. `--strict` makes warnings fail the run. `--no-git` leaves git out even inside a repository: the tree is read from disk, and `archive` records no commit. `--color` and `--no-color` override `NO_COLOR`, `FORCE_COLOR` and the terminal check. `--help` and `--version` do what they say.
117
145
 
118
146
  **Exit codes:** `0` clean, `1` findings, a collision, or a refused action, `2` the run could not be trusted - a bad flag, a configuration that does not load, a brief that does not exist. A run over a briefs directory that does not exist exits 2, because a check over nothing looks exactly like a clean one.
119
147
 
@@ -129,16 +157,16 @@ Reopens an archived brief: the banner and the hash come off, the status goes bac
129
157
  | `exclude` | `[]` | File-name globs that are never briefs, such as an index. |
130
158
  | `template` | `null` | A template file for `new`, with `{id}`, `{title}`, `{date}`, `{type}`, `{wave}`, `{status}`. |
131
159
  | `id` | `{ "source": "filename", "separator": "_", "digits": 3 }` | Where an id comes from, and how a new one is written. |
132
- | `status` | `{ "field": "status", "draft": "draft", "active": "active", "archived": "archived" }` | The words a repository uses. `field: null` reads status from location alone. |
133
- | `sections` | Intent, Negative Scope, Not Empowered (optional), Invariants (checklist) | Sections every live brief carries: a name, or `{ name, aliases, mustContain, checklist, optional }`. |
160
+ | `status` | `{ "field": "status", "draft": "draft", "active": "active", "deferred": "deferred", "archived": "archived" }` | The words a repository uses. `field: null` reads status from location alone; `draft` and `deferred` may be `null` where a repository has no such word. |
161
+ | `sections` | Intent, Negative Scope, Not Empowered (optional), Invariants (checklist) | Sections every live brief carries: a name, or `{ name, aliases, mustContain, checklist, optional, hint }`. `hint` says what the section must answer: a new brief carries it as a comment under the heading, a finding that the section is missing or unwritten gives it as the next step, and the JSON of `list` and `lint` reports it, so a tool helping to write a brief can ask for each section by what it is for. |
134
162
  | `sectionOrder` | `false` | Sections must appear in the listed order. |
135
163
  | `types` | `feature`, `defect`, `refactor`, `chore` | Brief types and the sections each adds. |
136
164
  | `placeholders` | `TBD`, `TODO`, `FIXME`, ... | Words that mark a section as unwritten. |
137
165
  | `fields` | `[]` | Front-matter keys the repository uses beyond the built-in ones. |
138
166
  | `archiving.tasks` | `"all"` | `"all"`, or the sections whose boxes must be closed. |
139
- | `archiving.dispositions` | `**Delegated`, `**Accepted debt`, `**Rejected` | Text under an open box that closes it. |
140
- | `archiving.banner` | see [`src/config.ts`](src/config.ts) | Banner lines, with `{date}`, `{summary}`, `{pr}`, `{commit}`, `{diffstat}`, `{links}`, `{id}`, `{title}`, `{author}`. A line with an empty placeholder is left out. |
141
- | `archiving.rewriteLinks` | `true` | Rewrite relative links when a brief moves. |
167
+ | `archiving.dispositions` | `**Delegated`, `**Accepted debt`, `**Rejected` | What a note under an open box starts with to close it. |
168
+ | `archiving.banner` | see [`src/config.ts`](https://github.com/DescentVTT/spec-brief/blob/main/src/config.ts) | Banner lines, with `{date}`, `{summary}`, `{pr}`, `{commit}`, `{diffstat}`, `{links}`, `{id}`, `{title}`, `{author}`. A line with an empty placeholder is left out. |
169
+ | `archiving.rewriteLinks` | `true` | Rewrite relative links when a brief moves: its own, and the ones other live briefs hold to it. Off, neither is edited, and the links other briefs are left holding are reported as `stale-link`. |
142
170
  | `archiving.freeze` | `true` | Write an integrity hash, so a later edit is caught. |
143
171
  | `archiving.base` | `null` | Branch the diff is measured from, such as `"main"`. |
144
172
  | `rules` | `{}` | Severity per rule: `off`, `note`, `warning` or `error`. |
@@ -172,12 +200,14 @@ A repository whose briefs have eight ordered sections, `proposed` and `archived`
172
200
 
173
201
  Section names compare without case, typographic quotes, emphasis, a leading number or a trailing colon, and a heading may add a qualifier after a separator: `## 2. Commander’s Intent:` fills `Commander's Intent`, and `## Invariants (must hold)` fills `Invariants`.
174
202
 
203
+ A brief is read as CommonMark reads it wherever that decides what is code or a comment - fenced and indented code, `<pre>`, `<script>`, `<style>` and `<textarea>` blocks, code spans, HTML comments - and nothing inside those is structure. A section, and the title, is an ATX heading, `## Name`, outside a block quote: prose over a `---` line is prose and a rule, and a heading quoted from another document is not one of the brief's sections. A task is a list item outside a block quote with `[ ]`, `[x]` or `[X]`. The links archival rewrites are the ones that write a destination: `[text](dest)`, `![alt](dest)` - one inside a link's text too - and a definition, `[label]: dest`, through which a reference is rewritten once.
204
+
175
205
  ## Rules
176
206
 
177
207
  <!-- rules:start -->
178
208
  | Rule | Default | Reports |
179
209
  | --- | --- | --- |
180
- | `front-matter` | error | Front matter that does not parse, a duplicate key, YAML beyond the flat subset. |
210
+ | `front-matter` | error | Front matter that does not parse, a duplicate key, YAML beyond the flat subset, TOML. |
181
211
  | `field` | error | A field spec-brief reads with a value of the wrong shape: a wave that is not a whole number, a list where one value belongs. |
182
212
  | `unknown-field` | warning | A front-matter key nobody declared, with the one probably meant. |
183
213
  | `status` | error | No status, a word the configuration does not use, or a status that disagrees with the directory. |
@@ -195,25 +225,42 @@ Section names compare without case, typographic quotes, emphasis, a leading numb
195
225
  | `dependency` | error | A dependency on itself, or on a brief that does not exist. |
196
226
  | `dependency-cycle` | error | Live briefs that depend on each other in a cycle, reported once, as a path. |
197
227
  | `wave-order` | error | A live dependency that does not run in an earlier wave. |
228
+ | `deferral-trigger` | error | A deferred brief with no `trigger`, or one that is only a date or a time - `2026-10`, `Q3`, `next month`, `October` - or a placeholder, rather than an event such as "when the second tenant signs". |
198
229
  | `glob` | error | A scope pattern spec-brief cannot read. |
199
- | `scope-contradiction` | error | A file that is both in `affectedFiles` and in `protectedFiles`. |
230
+ | `scope-contradiction` | error | Patterns in `affectedFiles` that `protectedFiles` cover entirely, so nothing of them is writable - one finding per brief, naming each. A pattern the search cannot decide is a warning. |
200
231
  | `glob-matches-nothing` | note | A scope pattern that matches no file git sees, tracked or untracked - expected when the round creates it. |
232
+ | `literal-read-as-file` | note | A path with no glob syntax that the tree does not hold and whose name has no extension, such as `src/newmod`: read as a file, and `src/newmod/` if a directory was meant. |
201
233
  | `archive-freeze` | error | An archived brief that changed after it was archived. |
202
234
  | `collision` | error | Two briefs in one wave whose scopes can name the same file (`matrix`). |
203
- | `unscoped` | note | A brief sharing a wave that declares no scope (`matrix`). |
235
+ | `collision-undecided` | warning | Two briefs in one wave whose collision the search could not decide within its budget (`matrix`), or a wave `schedule` passed over for it. |
236
+ | `unscoped` | note | A brief sharing a wave that declares no scope (`matrix`), or one `schedule` runs alone. |
204
237
  | `shared-directory` | off | Two briefs in one wave writing into the same directory (`matrix`). |
238
+ | `wave-schedule` | error | A brief with no wave, or a declared wave that does not hold - a collision in it, a dependency out of order - with the wave `schedule` computes and why (`schedule`). A declared wave that holds and is not the computed one is a note. |
239
+ | `scope-unmeasured` | warning | A brief with a scope archived without the files its round changed, so nothing checked the scope (`archive`). |
240
+ | `stale-link` | warning | Links in live briefs left pointing where a brief used to be, when `archiving.rewriteLinks` is off (`archive`, `unarchive`). |
205
241
  <!-- rules:end -->
206
242
 
207
- `archive` refuses with its own reasons - `open-task`, `archive-draft`, `dependency-open`, `dirty-tree`, `protected-file`, `out-of-scope`, `archive-exists` - which are not lint rules: they are about whether this round is done, not whether the brief is well written.
243
+ `archive` refuses with its own reasons - `open-task`, `archive-draft`, `archive-deferred`, `dependency-open`, `dirty-tree`, `protected-file`, `out-of-scope`, `archive-exists` - and says `waived` and `waiver-ignored` of what [plugins](#waiving-a-refusal) lift; none of these are lint rules: they are about whether this round is done, not whether the brief is well written. The rules marked `archive` above are raised by archival too, and configuration sets their severity like any other.
208
244
 
209
245
  ## In CI
210
246
 
211
247
  ```yaml
212
248
  - run: npx spec-brief lint --format github
213
249
  - run: npx spec-brief matrix --format github
250
+ - run: npx spec-brief schedule --format github
214
251
  ```
215
252
 
216
- `github` writes workflow commands, which annotate the pull request with no upload and no permission. `sarif` writes SARIF 2.1.0 for code-scanning upload. `json` is a versioned document (`schemaVersion`) for anything else.
253
+ `github` writes workflow commands, which annotate the pull request with no upload and no permission. `sarif` writes SARIF 2.1.0 for code-scanning upload. `gitlab` writes a GitLab Code Quality report - an array of `{ description, check_name, fingerprint, severity, location: { path, lines: { begin } } }` - for a merge request to show:
254
+
255
+ ```yaml
256
+ spec-brief:
257
+ script: npx spec-brief lint --format gitlab > gl-code-quality.json
258
+ artifacts:
259
+ reports:
260
+ codequality: gl-code-quality.json
261
+ ```
262
+
263
+ An error is `major`, a warning `minor` and a note `info`; GitLab's `critical` and `blocker` are for security holes and crashes, which no finding here is. The fingerprint is a SHA-256 of the rule, the file and the message, and not the line, so a finding that moves is the same issue. `json` is a versioned document for anything else: `schemaVersion` changes when a field changes meaning, and is 2 since a collision became a pair of briefs rather than a pair of patterns.
217
264
 
218
265
  ## As a library
219
266
 
@@ -224,11 +271,14 @@ const engine = await BriefEngine.open({ cwd: process.cwd() });
224
271
  const findings = await engine.lint();
225
272
  const ready = engine.ready(); // what can run now
226
273
  const { report } = await engine.collisions(); // who collides with whom
274
+ const { schedule } = await engine.schedule(); // the waves, computed
227
275
  const plan = await engine.planArchive('012', { commit: 'HEAD', pr: 41 });
228
276
  if (plan.blocking.length === 0) await engine.apply(plan);
229
277
  ```
230
278
 
231
- Everything below the engine is a pure function of text: `parseBrief`, `lint`, `collisions`, `intersectGlobs`, `planArchive`. `MemoryFileSystem` runs an engine over files that were never written, which is how a harness can ask what archiving a brief would do.
279
+ Everything below the engine is a pure function of text: `parseBrief`, `lint`, `collisions`, `meet`, `globWitness`, `planArchive`. `MemoryFileSystem` runs an engine over files that were never written, which is how a harness can ask what archiving a brief would do.
280
+
281
+ `parseGlob`, `matchGlob`, `intersectGlobs` and `globBase` keep their 0.1 signatures as a compatibility layer over the spec-core engine. They follow its dialect: a literal is read as a file unless `parseGlob` is told otherwise (`{ literal: 'directory' | 'either' | (path) => ... }`, where `readingIn(files)` reads it from a tree), and `intersectGlobs` throws where `globWitness` would answer `undecided`.
232
282
 
233
283
  ## Plugins
234
284
 
@@ -253,6 +303,26 @@ export default (options) => ({
253
303
 
254
304
  This is where integrations belong. A tool that defines a format - signed departures, recorded reproducers, a code graph's blast radius - is the one that can check it, and spec-brief carries no copy of formats it does not own. Loading a plugin runs its code, exactly as loading a linter configuration does.
255
305
 
306
+ A path is relative to the root. A package is found from the root as an `import` there would find it, its `exports` read under the import conditions, so a plugin published as ESM only loads, and so does a subpath such as `@descent-vtt/spec-harness/spec-brief-plugin`.
307
+
308
+ ### Waiving a refusal
309
+
310
+ A plugin may also export `waive`, which lets the archive accept a change its own check allows - spec-harness's plugin verifies a signed ruling for a protected path, which spec-brief does not know how to read.
311
+
312
+ ```js
313
+ export default () => ({
314
+ name: 'rulings',
315
+ rules: [],
316
+ // context: { root, brief: { id, file, text }, findings, base, commit }
317
+ waive: async ({ findings }) =>
318
+ findings
319
+ .filter((f) => f.rule === 'protected-file' && verifiedRulingCovers(f.path))
320
+ .map((f) => ({ rule: f.rule, path: f.path, reason: 'ruling R3, signed by alice, allows it' })),
321
+ });
322
+ ```
323
+
324
+ `findings` are the archive's refusals, as a frozen copy: a hook reads them and cannot change them, and a write to them throws. A `protected-file` refusal is one per file and names its `path`; an `out-of-scope` refusal, which refuses only under `--strict`, lists its `paths`. Those two are the only ones a plugin can waive: a waiver matches one of the plan's own refusals by rule and path and turns it into a `waived` note naming the plugin, the rule, the path and the reason; a waiver for any other rule is ignored with a `waiver-ignored` warning. The hook is asked only when the plan has a refusal it could lift. A hook that returns nothing waives nothing. One that throws - a write to `findings` included - or answers in another shape stops the run with exit 2, as a plugin that fails to load does.
325
+
256
326
  ## What it does not do
257
327
 
258
328
  - **No status file.** There is no `manifest.json` to keep in step. A brief and the directory it sits in are the record; `list --format json` is the index, computed when asked. A committed index is also the one file every round merging in parallel edits.
@@ -262,7 +332,7 @@ This is where integrations belong. A tool that defines a format - signed departu
262
332
 
263
333
  ## Design
264
334
 
265
- The decisions and what they cost are in [`docs/adr/`](docs/adr/README.md).
335
+ The decisions and what they cost are in [`docs/adr/`](https://github.com/DescentVTT/spec-brief/blob/main/docs/adr/README.md).
266
336
 
267
337
  ## License
268
338
 
package/dist/archive.d.ts CHANGED
@@ -15,14 +15,36 @@ export interface PullRequest {
15
15
  readonly number: number;
16
16
  readonly url: string | null;
17
17
  }
18
+ /**
19
+ * Why the files a round changed are unknown. `git`: there was no git to ask.
20
+ * `revision`: no commit was named and no base configured, so no diff was
21
+ * read. `merged`: the commit is already in the base branch, so the diff from
22
+ * their merge base is empty whatever the round changed - archiving on the base
23
+ * branch after the merge.
24
+ */
25
+ export type Unmeasured = {
26
+ readonly reason: 'git';
27
+ } | {
28
+ readonly reason: 'revision';
29
+ } | {
30
+ readonly reason: 'merged';
31
+ readonly base: string;
32
+ readonly commit: string;
33
+ };
18
34
  export interface ArchiveRequest {
19
35
  /** `YYYY-MM-DD`. */
20
36
  readonly date: string;
21
37
  readonly summary?: string | undefined;
22
38
  readonly pr?: PullRequest | undefined;
23
39
  readonly commit?: CommitInfo | undefined;
24
- /** The files the round changed, when a commit is known. */
40
+ /** The files the round changed, when they were read. */
25
41
  readonly changes?: readonly FileChange[] | undefined;
42
+ /**
43
+ * Why `changes` is missing, when the caller knows. Without changes the
44
+ * scope checks have nothing to check, and a plan that said nothing would
45
+ * read as a round that stayed in scope.
46
+ */
47
+ readonly unmeasured?: Unmeasured | undefined;
26
48
  /** Uncommitted paths, when the working tree was read. */
27
49
  readonly dirty?: readonly string[] | undefined;
28
50
  readonly allowDirty?: boolean | undefined;
@@ -73,9 +95,37 @@ export declare function openTasks(brief: Brief, corpus: Corpus): Brief['tasks'];
73
95
  */
74
96
  export declare function renderBanner(template: readonly string[], values: Readonly<Record<string, string>>): string[];
75
97
  export declare function planArchive(corpus: Corpus, brief: Brief, request: ArchiveRequest): Plan;
98
+ /** A plugin's answer to a refusal it may lift: which rule, for which path, and why. */
99
+ export interface Waiver {
100
+ readonly rule: string;
101
+ readonly path: string;
102
+ readonly reason: string;
103
+ }
104
+ /**
105
+ * The refusals a plugin may lift. A tool that verifies a ruling can say a
106
+ * protected file was allowed to change, or that a file outside the scope was;
107
+ * whether a round is done is spec-brief's to say.
108
+ */
109
+ export declare const WAIVABLE: readonly string[];
110
+ /**
111
+ * The plan with the refusals plugins waived turned into notes. A waiver
112
+ * matches a blocking finding by rule and path: a protected-file finding is
113
+ * replaced by the note, and an out-of-scope finding loses the path and goes
114
+ * when none is left. A waiver that matches nothing changes nothing, and one
115
+ * for any other rule is ignored with a warning, which refuses under --strict
116
+ * as any warning does.
117
+ */
118
+ export declare function applyWaivers(plan: Plan, waivers: readonly {
119
+ readonly plugin: string;
120
+ readonly waiver: Waiver;
121
+ }[], strict?: boolean): Plan;
122
+ export interface UnarchiveRequest {
123
+ /** Warnings refuse the reopening, as they fail lint. */
124
+ readonly strict?: boolean | undefined;
125
+ }
76
126
  /**
77
127
  * Reopens an archived brief: the banner and the freeze come off, the status
78
128
  * goes back to the live word, and the links are rewritten for the directory
79
129
  * it returns to. What the round wrote stays in history, where it belongs.
80
130
  */
81
- export declare function planUnarchive(corpus: Corpus, brief: Brief): Plan;
131
+ export declare function planUnarchive(corpus: Corpus, brief: Brief, request?: UnarchiveRequest): Plan;