@descent-vtt/spec-brief 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +264 -0
- package/README.md +90 -20
- package/dist/archive.d.ts +52 -2
- package/dist/archive.js +205 -37
- package/dist/archive.js.map +1 -1
- package/dist/brief.d.ts +2 -0
- package/dist/brief.js +5 -1
- package/dist/brief.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +48 -14
- package/dist/cli.js.map +1 -1
- package/dist/collisions.d.ts +43 -12
- package/dist/collisions.js +90 -58
- package/dist/collisions.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +58 -10
- package/dist/config.js.map +1 -1
- package/dist/corpus.d.ts +21 -1
- package/dist/corpus.js +51 -2
- package/dist/corpus.js.map +1 -1
- package/dist/engine.d.ts +33 -3
- package/dist/engine.js +112 -9
- package/dist/engine.js.map +1 -1
- package/dist/frontmatter.d.ts +18 -61
- package/dist/frontmatter.js +31 -300
- package/dist/frontmatter.js.map +1 -1
- package/dist/glob.d.ts +77 -54
- package/dist/glob.js +104 -433
- package/dist/glob.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/links.d.ts +1 -1
- package/dist/links.js +3 -4
- package/dist/links.js.map +1 -1
- package/dist/lint.d.ts +33 -2
- package/dist/lint.js +9 -2
- package/dist/lint.js.map +1 -1
- package/dist/markdown.d.ts +26 -23
- package/dist/markdown.js +60 -237
- package/dist/markdown.js.map +1 -1
- package/dist/plugins.d.ts +14 -0
- package/dist/plugins.js +142 -6
- package/dist/plugins.js.map +1 -1
- package/dist/report.d.ts +40 -3
- package/dist/report.js +227 -11
- package/dist/report.js.map +1 -1
- package/dist/rules.d.ts +22 -4
- package/dist/rules.js +175 -45
- package/dist/rules.js.map +1 -1
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.js +10 -9
- package/dist/scaffold.js.map +1 -1
- package/dist/schedule.d.ts +133 -0
- package/dist/schedule.js +265 -0
- package/dist/schedule.js.map +1 -0
- package/dist/scope.d.ts +52 -0
- package/dist/scope.js +56 -0
- package/dist/scope.js.map +1 -0
- package/dist/text.d.ts +13 -0
- package/dist/text.js +23 -0
- package/dist/text.js.map +1 -1
- package/dist/trigger.d.ts +17 -0
- package/dist/trigger.js +49 -0
- package/dist/trigger.js.map +1 -0
- package/dist/types.d.ts +14 -5
- package/dist/types.js.map +1 -1
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
- package/dist/vendor/spec-core/markdown/headings.js +199 -0
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
- package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
- package/dist/vendor/spec-core/markdown/index.js +4 -0
- package/dist/vendor/spec-core/markdown/index.js.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
- package/dist/vendor/spec-core/markdown/layout.js +28 -0
- package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
- package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/links.js +364 -0
- package/dist/vendor/spec-core/markdown/links.js.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/lists.js +127 -0
- package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
- package/dist/vendor/spec-core/markdown/scan.js +439 -0
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
- package/dist/vendor/spec-core/markdown/syntax.js +181 -0
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
- package/dist/vendor/spec-core/markdown/tables.js +86 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
- package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
- package/dist/vendor/spec-core/markdown/types.js +9 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -0
- package/dist/vendor/spec-core/path/index.d.ts +1 -0
- package/dist/vendor/spec-core/path/index.js +2 -0
- package/dist/vendor/spec-core/path/index.js.map +1 -0
- package/dist/vendor/spec-core/path/posix.d.ts +83 -0
- package/dist/vendor/spec-core/path/posix.js +150 -0
- package/dist/vendor/spec-core/path/posix.js.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
- package/dist/vendor/spec-core/pattern/automaton.js +340 -0
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
- package/dist/vendor/spec-core/pattern/charset.js +116 -0
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
- package/dist/vendor/spec-core/pattern/glob.js +481 -0
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
- package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
- package/dist/vendor/spec-core/pattern/index.js +4 -0
- package/dist/vendor/spec-core/pattern/index.js.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts +67 -0
- package/dist/vendor/spec-core/pattern/regex.js +813 -0
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
- package/dist/vendor/spec-core/text/index.d.ts +2 -0
- package/dist/vendor/spec-core/text/index.js +2 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -0
- package/dist/vendor/spec-core/text/lines.d.ts +72 -0
- package/dist/vendor/spec-core/text/lines.js +156 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -0
- package/package.json +3 -2
- package/schema.json +17 -0
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,270 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes, newest first. Versions follow [semver](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 0.2.0
|
|
6
|
+
|
|
7
|
+
Scopes are read by spec-core's glob automaton, so a collision is proved with a
|
|
8
|
+
file both briefs would write and protected paths are subtracted from a scope
|
|
9
|
+
exactly. `spec-brief schedule` places briefs in waves from their dependencies
|
|
10
|
+
and provable collisions. Deferred work waits on an observable trigger. A plugin
|
|
11
|
+
may waive a `protected-file` or `out-of-scope` refusal, which is how
|
|
12
|
+
spec-harness applies a signed ruling. Briefs are read with spec-core's
|
|
13
|
+
CommonMark scanner, and the review of 0.1.0 is fixed throughout.
|
|
14
|
+
|
|
15
|
+
### Upgrading from 0.1.0
|
|
16
|
+
|
|
17
|
+
- **A configuration that already uses the word "deferred" no longer loads.**
|
|
18
|
+
Deferred work is a new status, and its word is `"deferred"` unless the
|
|
19
|
+
configuration says otherwise, so `"status": { "draft": "deferred" }` now
|
|
20
|
+
names two statuses with one word and stops the run with exit 2: "the
|
|
21
|
+
status words must differ: "deferred" is the word for draft and deferred".
|
|
22
|
+
Set `"status": { "deferred": null }` where the repository defers no work,
|
|
23
|
+
or give the new status a word of its own, `"deferred": "parked"`.
|
|
24
|
+
- **JSON documents are `schemaVersion: 2`**, every command's: the version
|
|
25
|
+
is one number for all of them, and `matrix`'s document changed meaning. A
|
|
26
|
+
collision is a pair of briefs and carries `overlaps`, a list of
|
|
27
|
+
`{ patterns, witness }`, in place of `patterns` and `witness`, and a
|
|
28
|
+
witness is always a file, never a directory; a `sharedDirectories` entry
|
|
29
|
+
carries `directories` in place of `directory`; and deferred briefs, and
|
|
30
|
+
the briefs waiting on them, left `waves[].briefs` for `deferred` and
|
|
31
|
+
`waiting`. `list`, `lint` and `archive` changed only by adding: `trigger`
|
|
32
|
+
and `sections`, a `"deferred"` status, and `path` and `paths` on a
|
|
33
|
+
finding. A reader of those documents reads version 2 as it read 1.
|
|
34
|
+
- **`**` inside a name is an error**, where it was read as `*`: a scope
|
|
35
|
+
written `docs/**.md` is a `glob` error in `lint`, and a `files` or
|
|
36
|
+
`exclude` pattern written so stops the run with exit 2. Parentheses are
|
|
37
|
+
literal unless a group holds a `|`. Both are under "Globs" below.
|
|
38
|
+
- **A note that continues its box's paragraph no longer closes the box**:
|
|
39
|
+
`- [ ] Add a cache` over ` **Rejected** by 012` refuses an archival as
|
|
40
|
+
`open-task` until the note is a bullet or has a blank line above it. See
|
|
41
|
+
the entry on open boxes below.
|
|
42
|
+
|
|
43
|
+
### Breaking (library API)
|
|
44
|
+
|
|
45
|
+
- `Config['status']['deferred']` is required, `null` where a repository
|
|
46
|
+
has no such word. A configuration from `resolveConfig` or
|
|
47
|
+
`DEFAULT_CONFIG` has it; one built by hand must set it.
|
|
48
|
+
- `Status` gains `'deferred'`, and `Format` and `FORMATS` gain
|
|
49
|
+
`'gitlab'`: an exhaustive `switch` over either needs the new case.
|
|
50
|
+
- `CollisionReport` gains the required `deferred` and `waiting`, and
|
|
51
|
+
`WaveMatrix` the required `undecided`; a report built by hand carries
|
|
52
|
+
them. A wave's `briefs` no longer holds a deferred brief or one waiting on
|
|
53
|
+
deferred work.
|
|
54
|
+
- `Collision` carries `overlaps` in place of `patterns` and `witness`, and
|
|
55
|
+
`SharedDirectory` carries `directories` in place of `directory`, as the
|
|
56
|
+
JSON does.
|
|
57
|
+
- `parseGlob`'s `isFile` option is replaced by `literal` (`'file'`,
|
|
58
|
+
`'directory'`, `'either'` or a function, such as `readingIn(files)`), a
|
|
59
|
+
`Glob` carries `compiled` and `literals` in place of `alternatives`, and
|
|
60
|
+
`intersectGlobs` throws where the search is undecided.
|
|
61
|
+
|
|
62
|
+
### Changes
|
|
63
|
+
|
|
64
|
+
- Markdown is read by spec-core's scanner and front matter by its reader,
|
|
65
|
+
copied into `src/vendor/spec-core/` beside the glob engine and verified by
|
|
66
|
+
hash (ADR-0001, amended). What a brief's structure is stays as it was: a
|
|
67
|
+
section, and the title, is an ATX heading, `## Name`, outside a block
|
|
68
|
+
quote, so prose over a `---` line is still prose and a rule, not a section,
|
|
69
|
+
and a heading quoted from another document is not one of the brief's; a task
|
|
70
|
+
is a list item outside a block quote with `[ ]`, `[x]` or `[X]`. What counts
|
|
71
|
+
as code, a comment or a link is now what CommonMark says it is:
|
|
72
|
+
- **Indented code and raw-text HTML are code.** Four spaces at the top
|
|
73
|
+
level after a blank line, and a `<pre>`, `<script>`, `<style>` or
|
|
74
|
+
`<textarea>` block, hold no heading, task or link: a box in an indented
|
|
75
|
+
example no longer refuses an archival, and a fence indented four spaces
|
|
76
|
+
there no longer hides the rest of the brief. Inside a list item, code
|
|
77
|
+
starts four columns past the item's text: a box written that deep after a
|
|
78
|
+
blank line is code, and a fence that deep opens nothing.
|
|
79
|
+
- **A `<!--` in the middle of a line with no `-->` after it is text.** It
|
|
80
|
+
hid everything after it, so every later section was missing. One that
|
|
81
|
+
opens a line still runs to the end.
|
|
82
|
+
- **A heading's text leaves out a comment on its line**:
|
|
83
|
+
`## Intent <!-- required -->` fills `Intent`, where it was reported
|
|
84
|
+
missing.
|
|
85
|
+
- **A code span may close on a later line of its paragraph**, and a link
|
|
86
|
+
inside one is not rewritten.
|
|
87
|
+
- **A thematic break ends a task**, so a note after `***` no longer closes
|
|
88
|
+
the box above it; and **a tab indents to the next multiple of four
|
|
89
|
+
columns**, so a tab-indented note under a box indented by two spaces is
|
|
90
|
+
part of the item.
|
|
91
|
+
- **A link is one CommonMark reads.** `a](b.md)` and `[a](b.md c)` are text
|
|
92
|
+
and are no longer rewritten on archival; a definition in a block quote
|
|
93
|
+
is, and so is a destination after link text that runs over lines.
|
|
94
|
+
- **TOML front matter** between `+++` lines is recognised. `front-matter`
|
|
95
|
+
reports it, where it was read as body text and only the missing status
|
|
96
|
+
was reported.
|
|
97
|
+
- **An inline list on the line under its key** - `affectedFiles:` and then
|
|
98
|
+
` [src/**]` - is reported as "an inline list starts on the line after
|
|
99
|
+
its key; write it after the colon", where the advice was to quote it,
|
|
100
|
+
which would have made the list a string.
|
|
101
|
+
- Nothing is written into front matter that cannot hold it: TOML, or a block
|
|
102
|
+
never closed. `schedule --write` and `unarchive` refuse such a brief as
|
|
103
|
+
`front-matter`, and so does `archive` where lint's own `front-matter` error
|
|
104
|
+
does not refuse it already. `unarchive` stopped with an unexpected error on
|
|
105
|
+
a block never closed, as `archive` did with an empty banner template, and
|
|
106
|
+
`archive` otherwise prepended a second block above the banner.
|
|
107
|
+
- Reopening a brief that had no front matter, in a repository with no status
|
|
108
|
+
field, gives it back with none. Archival wrote a block to hold the
|
|
109
|
+
integrity hash, and `unarchive` took the hash out and left `---` over
|
|
110
|
+
`---` behind; a block the reopening empties now goes with it.
|
|
111
|
+
- A carriage return that ends no line, as a `\r\r\n` ending leaves one, is
|
|
112
|
+
read as a space; line numbers are unchanged. A front-matter line ending in
|
|
113
|
+
one now reads, where it was "not a key: value line".
|
|
114
|
+
- Library API: `Brief.frontMatter` is spec-core's `FrontMatter`. It gains
|
|
115
|
+
`kind` (`'yaml'` or `'toml'`), and each entry gains `parent`, `keyStart`,
|
|
116
|
+
`valueStart` and `valueEnd`, offsets into the brief's `lines` joined by
|
|
117
|
+
`\n`. `Brief.scan` gains `links`, the destinations archival rewrites.
|
|
118
|
+
- Globs are spec-core's `path` dialect, matched and intersected by its
|
|
119
|
+
automaton, which is copied into `src/vendor/spec-core/` and verified by
|
|
120
|
+
hash. The syntax is unchanged but for two refusals; what it means changes
|
|
121
|
+
in four places.
|
|
122
|
+
- **`**` inside a name is an error**: `docs/**.md`, `**.ts`, `a**b`. It
|
|
123
|
+
was read as `*`, one level, and dropped every nested file from a scope
|
|
124
|
+
that tools elsewhere read as recursive. The message says to write
|
|
125
|
+
`docs/**/*.md` for any depth, or `*.md` for one level. A scope pattern
|
|
126
|
+
written so is a `glob` error in `lint`, and a `files` or `exclude`
|
|
127
|
+
pattern stops the run with exit 2.
|
|
128
|
+
- **Parentheses are literal unless a group holds a `|`**:
|
|
129
|
+
`C++(notes).md`, `*(2017).md` and `@(a)` are names, where every
|
|
130
|
+
`?(`, `*(`, `+(`, `@(` or `!(` was refused as an extended glob.
|
|
131
|
+
`+(a|b)` is still refused, and the message now says to write `{a,b}`,
|
|
132
|
+
and a literal parenthesis as `[(]`.
|
|
133
|
+
- **A literal path is read from the tree.** A file the tree holds is that
|
|
134
|
+
file, a directory it holds is everything beneath it, and a path it does
|
|
135
|
+
not hold is a file unless written with a trailing `/`. The extension
|
|
136
|
+
guess is gone: it read `Dockerfile` as a directory, so `Dockerfile` and
|
|
137
|
+
`**/x.ts` collided through `Dockerfile/x.ts`, and read `docs/v1.2` as a
|
|
138
|
+
file, missing every collision inside it. Without a tree - the library
|
|
139
|
+
called with no files - every literal is a file.
|
|
140
|
+
- **A trailing `/**` or `/` is a directory's contents**, at least one name
|
|
141
|
+
below it: `src/**` no longer matches `src`, and a collision witness is
|
|
142
|
+
always a file, `docs/adr/x` where it was `docs/adr`.
|
|
143
|
+
- **`./` alone, and a `..` segment, are refused in the core's words**
|
|
144
|
+
("the pattern names the root itself", "a pattern cannot climb out of its
|
|
145
|
+
root"), and a lone `}` is a literal rather than an error. A leading `/`
|
|
146
|
+
and a leading `!` are refused as before.
|
|
147
|
+
- **Protection wins.** What a brief may write is `affectedFiles` less
|
|
148
|
+
`protectedFiles`, so `affectedFiles: [src/**]` with
|
|
149
|
+
`protectedFiles: [src/db/schema.ts]` is valid. `scope-contradiction` now
|
|
150
|
+
fires only for affected patterns the protections cover entirely, as one
|
|
151
|
+
finding per brief naming each; it used to fire for any file in both.
|
|
152
|
+
`matrix` leaves out a file either brief of a pair protects.
|
|
153
|
+
- `literal-read-as-file`, a note: a path with no glob syntax that the tree
|
|
154
|
+
does not hold and whose name has no extension, such as `src/newmod`, is
|
|
155
|
+
read as a file, and the hint says to write `src/newmod/` for a directory.
|
|
156
|
+
`glob-matches-nothing` leaves such a pattern to it.
|
|
157
|
+
- `collision-undecided`, a warning: a pair of briefs whose collision the
|
|
158
|
+
witness search could not decide within its budget. It is never reported
|
|
159
|
+
as a collision and never as clean. `matrix` marks the pair `?`, and its
|
|
160
|
+
JSON gains an `undecided` list per wave (`{ a, b, patterns }`).
|
|
161
|
+
- Deferred work has a status: `status.deferred` in configuration, `"deferred"`
|
|
162
|
+
by default and `null` for a repository without one. A deferred brief lives
|
|
163
|
+
in the briefs directory, is never ready, is listed and left out by
|
|
164
|
+
`matrix`, and is refused by `archive` as `archive-deferred`. It names the
|
|
165
|
+
event that brings it back in `trigger`, a new built-in field that `list`
|
|
166
|
+
reports. A brief that depends on deferred work, directly or through another
|
|
167
|
+
brief, runs in no wave either: `matrix` lists it under `waits`, and in JSON
|
|
168
|
+
under `waiting` with the brief it waits on, and compares it with nothing,
|
|
169
|
+
as `schedule` places it nowhere.
|
|
170
|
+
- `deferral-trigger`, an error: a deferred brief with no `trigger`, or one
|
|
171
|
+
that is only a date or a time - `2026-10`, `Q3`, `next month`, `October`,
|
|
172
|
+
`in two weeks` - or a placeholder. "when the second tenant signs" and
|
|
173
|
+
"when p95 > 200 ms" pass.
|
|
174
|
+
- `spec-brief schedule` computes the waves the live briefs can run in
|
|
175
|
+
(ADR-0011): dependency order, ties by id, each brief in the lowest wave
|
|
176
|
+
after its live dependencies that holds nothing it collides with, a brief
|
|
177
|
+
with no scope in a wave of its own. Each brief's proposed wave is shown
|
|
178
|
+
beside its declared one with the reason for a move - the dependency, or the
|
|
179
|
+
brief and the file that kept it out of a wave - and deferred briefs, the
|
|
180
|
+
briefs waiting on them, and dependency cycles are listed. `--write` sets
|
|
181
|
+
`wave` in the front matter of each brief that moves, every other line
|
|
182
|
+
untouched and a comment after the old wave kept, as one transaction, and
|
|
183
|
+
writes nothing over a cycle. A declared
|
|
184
|
+
wave that differs from the computed one is judged as `lint` and `matrix`
|
|
185
|
+
judge it: no collision in it, dependencies before it, dependents after it.
|
|
186
|
+
One that holds is a person's choice to keep, and its move is a note.
|
|
187
|
+
Exit 0 when every declared wave holds, 1 when a brief declares no wave or
|
|
188
|
+
one that does not hold, or on a cycle. Pretty, JSON, SARIF, GitHub and
|
|
189
|
+
GitLab output; `wave-schedule`, an error by default, is the finding per
|
|
190
|
+
move, and a note for a move whose declared wave holds.
|
|
191
|
+
- A plugin may export a `waive` hook beside its rules (ADR-0007, amended).
|
|
192
|
+
After an archival is planned, and only when it has a refusal a plugin may
|
|
193
|
+
lift, each hook is given `{ root, brief: { id, file, text }, findings,
|
|
194
|
+
base, commit }`, a frozen copy, and answers `[{ rule, path, reason }]`. A
|
|
195
|
+
`protected-file` or `out-of-scope` refusal of the plan's own that it names
|
|
196
|
+
by rule and path becomes a `waived` note; a waiver for any other rule is
|
|
197
|
+
ignored with a `waiver-ignored` warning, and one that matches no refusal
|
|
198
|
+
changes nothing. A hook that returns nothing waives nothing; one that
|
|
199
|
+
throws - a write to the copy included - or answers in another shape stops
|
|
200
|
+
the run with exit 2. This is how spec-harness's plugin lets a signed ruling
|
|
201
|
+
allow a protected file, without spec-brief reading signatures.
|
|
202
|
+
- A `protected-file` refusal names its file in a new `path` field, one
|
|
203
|
+
finding per file as before, and an `out-of-scope` finding lists its files
|
|
204
|
+
in `paths`. Both appear in the JSON of `archive`; a plugin reads them.
|
|
205
|
+
- `--format gitlab` wherever `sarif` is offered - `lint`, `matrix`,
|
|
206
|
+
`schedule` - writes a GitLab Code Quality report: an array of
|
|
207
|
+
`{ description, check_name, fingerprint, severity, location }`, errors as
|
|
208
|
+
`major`, warnings as `minor`, notes as `info`, and a fingerprint that
|
|
209
|
+
hashes the rule, the file and the message but not the line.
|
|
210
|
+
- 24 lint rules and 5 collision rules.
|
|
211
|
+
- The library's `parseGlob`, `matchGlob`, `intersectGlobs` and `globBase`
|
|
212
|
+
keep their signatures over the new engine, with the options and shapes
|
|
213
|
+
under "Breaking (library API)" above. `globWitness`, `globCovers`,
|
|
214
|
+
`globBases`, `readingIn`, `meet`, `scopeOf`, `contradictions` and
|
|
215
|
+
`waitingOnDeferred` are new. `intersectTokens` is gone.
|
|
216
|
+
- A `shared-directory` finding has a hint, and a `glob` finding says what a
|
|
217
|
+
scope pattern looks like.
|
|
218
|
+
- Versions are staged by CI through npm's trusted publishing, with
|
|
219
|
+
provenance that names the commit and the run, and a maintainer releases
|
|
220
|
+
each with a second factor (ADR-0010).
|
|
221
|
+
- `archive` no longer passes a scope it never measured. With no `--commit`,
|
|
222
|
+
`--base` or `archiving.base`, without git, or with a commit already in the
|
|
223
|
+
base branch - where the diff from the merge base is empty - a brief that
|
|
224
|
+
declares `affectedFiles` or `protectedFiles` gets `scope-unmeasured`, a
|
|
225
|
+
warning that names the next step, and a refusal under `--strict`. It is the
|
|
226
|
+
first archive rule, and configuration sets its severity like any other.
|
|
227
|
+
- An open box is closed only by a note under it that starts with a
|
|
228
|
+
disposition marker, and a note starts a bullet, a paragraph after a blank
|
|
229
|
+
line, or a line after one that ends a sentence (`.`, `?` or `!`, then
|
|
230
|
+
only closing marks). A marker anywhere in the item used to count, the
|
|
231
|
+
box's own text included, so `- [ ] Explain why the **Rejected** designs
|
|
232
|
+
failed` archived as dispositioned, and so did the same sentence wrapped
|
|
233
|
+
after "the", indented or not. A line that continues a sentence no longer
|
|
234
|
+
closes the box, whatever it starts with; `- [ ] Add a cache.` over
|
|
235
|
+
` **Rejected** by 012` still does. A note under a box whose text ends
|
|
236
|
+
without a stop, `- [ ] Add a cache` over ` **Rejected** by 012`, now
|
|
237
|
+
refuses the archival as `open-task`, and the hint says to make it a bullet
|
|
238
|
+
or put a blank line above it.
|
|
239
|
+
- `archiving.rewriteLinks: false` now holds for the links other briefs hold
|
|
240
|
+
to a moving brief, which were rewritten regardless. Those left pointing at
|
|
241
|
+
the old path are reported as `stale-link`, a warning naming each file and
|
|
242
|
+
line, on `archive` and `unarchive` alike; `unarchive --strict` refuses on
|
|
243
|
+
it, as `archive --strict` does.
|
|
244
|
+
- `matrix` reports two briefs whose scopes meet as one collision, however
|
|
245
|
+
many pairs of their patterns meet, and the finding lists every pair with a
|
|
246
|
+
path both cover. A pair writing into several shared directories is likewise
|
|
247
|
+
one `shared-directory` finding.
|
|
248
|
+
- A plugin package whose `exports` offer only the `import` condition loads.
|
|
249
|
+
Packages were resolved with `require.resolve`, which reads `exports` under
|
|
250
|
+
the require conditions, so an ESM-only plugin could not be found.
|
|
251
|
+
A subpath is held to `exports` as Node holds it: a target, or what a
|
|
252
|
+
`*` matched, with a `.`, `..` or `node_modules` segment is refused,
|
|
253
|
+
split on `/` and on a backslash alike, so `pkg/..\..\outside` cannot
|
|
254
|
+
climb out of `node_modules` on Windows.
|
|
255
|
+
- A brace group is split on its own commas only: `{[,]x,y}` has the two
|
|
256
|
+
alternatives `[,]x` and `y`, where a comma or brace inside a class used to
|
|
257
|
+
split the group or unbalance it.
|
|
258
|
+
- A section rule takes a `hint`: what the section must answer. A new brief
|
|
259
|
+
carries it as a comment under the heading, `missing-section`,
|
|
260
|
+
`empty-section` and `placeholder` findings give it as their hint, and the
|
|
261
|
+
JSON of `list` and `lint` gains `sections`, every section the configuration
|
|
262
|
+
asks for with its type, switches and hint. The default sections carry one,
|
|
263
|
+
and `init` writes them out. `empty-section` and `placeholder` findings have
|
|
264
|
+
a hint even without one.
|
|
265
|
+
- A `--base` or `archiving.base` that names no commit stops `archive` with a
|
|
266
|
+
message that says so, where git's failure to diff from it surfaced as an
|
|
267
|
+
unexpected error with a stack trace.
|
|
268
|
+
|
|
5
269
|
## 0.1.0
|
|
6
270
|
|
|
7
271
|
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
|
-
|
|
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
|
|
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
|
|
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 `
|
|
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` |
|
|
167
|
+
| `archiving.dispositions` | `**Delegated`, `**Accepted debt`, `**Rejected` | What a note under an open box starts with to close it. |
|
|
140
168
|
| `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. |
|
|
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)`, `` - 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 |
|
|
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
|
-
| `
|
|
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` -
|
|
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. `
|
|
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`, `
|
|
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.
|
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
|
|
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;
|