@descent-vtt/spec-harness 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +86 -4
- package/README.md +54 -13
- package/dist/audit.d.ts +9 -2
- package/dist/audit.js +31 -1
- package/dist/audit.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +28 -18
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +1 -0
- package/dist/commands.js.map +1 -1
- package/dist/configure.d.ts +25 -2
- package/dist/configure.js +44 -4
- package/dist/configure.js.map +1 -1
- package/dist/formats.js +3 -1
- package/dist/formats.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/junit.js +13 -2
- package/dist/junit.js.map +1 -1
- package/dist/manifests.d.ts +31 -4
- package/dist/manifests.js +71 -24
- package/dist/manifests.js.map +1 -1
- package/dist/round.d.ts +3 -1
- package/dist/round.js +15 -6
- package/dist/round.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +3 -1
- package/dist/setup.js +13 -14
- package/dist/setup.js.map +1 -1
- package/package.json +3 -3
- package/skills/close-round/SKILL.md +28 -10
- package/skills/draft-brief/SKILL.md +24 -6
- package/skills/run-round/SKILL.md +22 -4
- package/skills/split-goal/SKILL.md +19 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Rounds of agent work under a brief: context, a guard on every write, escalations a person signs, an audit at the end. Needs Claude Code 2.1.139 or later, and @descent-vtt/spec-harness and @descent-vtt/spec-brief 0.2.0 or later installed in the project.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DescentVTT",
|
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,88 @@ only fixes, reports less or documents, so `^0.7.0` takes only releases that
|
|
|
9
9
|
cannot turn a run red. The family's policy is
|
|
10
10
|
[spec-core's ADR-0009](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0009-versions-before-1-0.md).
|
|
11
11
|
|
|
12
|
+
## 0.10.0
|
|
13
|
+
|
|
14
|
+
The skills and the README gave `npx` the tools' bare command names, which on
|
|
15
|
+
npm are not this project's packages; the audit sees a dependency a round
|
|
16
|
+
allowed to run install scripts; and git's hook runs `node`, not `npx`.
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
|
|
20
|
+
- Every command in the four skills, in the MCP prompts that serve the same
|
|
21
|
+
text, and in the README gives `npx` the package's full name behind
|
|
22
|
+
`--no-install`, as in
|
|
23
|
+
`npx --no-install @descent-vtt/spec-harness audit --format json`.
|
|
24
|
+
`spec-harness` without the scope is another publisher's package on npm,
|
|
25
|
+
and `spec-brief`, `spec-guard` and `spec-graph` without it belonged to
|
|
26
|
+
nobody on 2026-10-07. `npx` given a name the project has not installed
|
|
27
|
+
fetches the package of that name and runs it, unasked when no terminal is
|
|
28
|
+
attached, as under an agent: the skills told an agent to run fifteen such
|
|
29
|
+
commands in whatever tree it stood in. Each skill now opens by saying what
|
|
30
|
+
to do when a command stops for a missing package - install the project's
|
|
31
|
+
dependencies, as with `npm ci` - and never to drop the flag or the scope
|
|
32
|
+
([Names](README.md#names), ADR-0009, and the family's
|
|
33
|
+
[adopting guide](https://github.com/DescentVTT/spec-core/blob/main/docs/adopting.md#names)).
|
|
34
|
+
The hooks, the MCP server and git's hook run `node` with the installed
|
|
35
|
+
file's path and were not exposed.
|
|
36
|
+
Upgrading: update the Claude Code plugin, or the package, to get the
|
|
37
|
+
skills as they are now. Where a CI job, a script or a prompt of your own
|
|
38
|
+
runs `npx <tool>`, write `npx --no-install @descent-vtt/<tool>`;
|
|
39
|
+
`--no-install` before the bare name is not enough, since npm still runs a
|
|
40
|
+
copy an earlier fetch left in its cache. If a bare name was ever run
|
|
41
|
+
through `npx` where the harness was not installed, empty that cache: the
|
|
42
|
+
`_npx` directory below the path `npm config get cache` prints.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- `doctor` and `init` note a pre-commit hook that still runs spec-harness
|
|
47
|
+
through `npx`, with the line that runs it with `node`;
|
|
48
|
+
`doctor --format json` gains `gitHook.note`. A note, never a failure.
|
|
49
|
+
- `readManifest` and `diffManifest` in the programmatic API give a
|
|
50
|
+
`package.json`'s install-script entries and what changed in them, as
|
|
51
|
+
`installScripts`.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- `audit` reports a package a round allowed to run install scripts - an
|
|
56
|
+
entry added to `allowScripts` in a `package.json`, where npm 12 keeps its
|
|
57
|
+
approvals, or turned from `false` to `true` - as `install-script-allowed`,
|
|
58
|
+
a warning as a new dependency is; a denial and a removed entry are notes.
|
|
59
|
+
`--format json` gains `installScripts`
|
|
60
|
+
([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
61
|
+
Upgrading: a round that approves a dependency's install script now fails
|
|
62
|
+
`audit --strict`; say in the brief why it must run and audit without
|
|
63
|
+
`--strict`, or remove the entry.
|
|
64
|
+
- `init --git-hook` writes a pre-commit hook that runs `node` and the script
|
|
65
|
+
in the project's install, as the Claude Code hooks are run, where it ran
|
|
66
|
+
<!-- bare-name: the hook as it was -->
|
|
67
|
+
`npx --no-install spec-harness hook git`: a commit starts no npm. Where
|
|
68
|
+
the harness is not installed in the work tree, the hook says so and stops
|
|
69
|
+
the commit
|
|
70
|
+
([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
|
|
71
|
+
Upgrading: a hook written before keeps working; to move it, use the line
|
|
72
|
+
`doctor` names, or delete the hook and run `init --git-hook --write`.
|
|
73
|
+
|
|
74
|
+
## 0.9.1
|
|
75
|
+
|
|
76
|
+
A command or a JUnit entity named `constructor`, `toString` or `__proto__`
|
|
77
|
+
is the unknown name it is, and the suite now runs against the siblings' newest
|
|
78
|
+
releases and, in CI, their minimums.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- A command named as something every JavaScript object answers to -
|
|
83
|
+
`spec-harness constructor`, `toString`, `__proto__` - is the unknown
|
|
84
|
+
command it is: `unknown command "constructor"; see spec-harness --help`
|
|
85
|
+
with exit 2, where it ended on a stack trace with exit 1.
|
|
86
|
+
- `probe` leaves an entity of such a name in a JUnit report as written, as
|
|
87
|
+
it leaves any entity XML does not name: `&constructor;` or `&toString;` in
|
|
88
|
+
a test's name, its class or a failure's message was replaced with the
|
|
89
|
+
source of a function, so a probe's `test:` could not name that test.
|
|
90
|
+
- `formatSarif`, called with a finding whose rule is so named, describes the
|
|
91
|
+
rule by its name, as it does any rule the audit does not have; the
|
|
92
|
+
description had no text. No command makes such a finding.
|
|
93
|
+
|
|
12
94
|
## 0.9.0
|
|
13
95
|
|
|
14
96
|
spec-harness refuses sibling JSON of a shape it does not read, with exit 2,
|
|
@@ -498,9 +580,9 @@ A signed ruling now works from `init` onwards. An end-to-end check of 0.1.1
|
|
|
498
580
|
from npm, in a repository made with `git init`, could not finish the round
|
|
499
581
|
the README describes: `init` named no base and did not load the spec-brief
|
|
500
582
|
plugin, so no ruling verified, and spec-brief's archive refused the file a
|
|
501
|
-
person had allowed. After upgrading, run
|
|
502
|
-
|
|
503
|
-
wrote.
|
|
583
|
+
person had allowed. After upgrading, run
|
|
584
|
+
`npx --no-install @descent-vtt/spec-harness init --write` again: it adds what
|
|
585
|
+
is missing and replaces the `npx` hooks and server 0.1 wrote.
|
|
504
586
|
|
|
505
587
|
### Fixed
|
|
506
588
|
|
|
@@ -525,7 +607,7 @@ wrote.
|
|
|
525
607
|
- The plugin's MCP server and hooks, and those `init` writes, run `node`
|
|
526
608
|
with `${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin/spec-harness.js`.
|
|
527
609
|
Claude Code starts a plugin's server in the plugin's directory, where
|
|
528
|
-
`npx
|
|
610
|
+
the `npx` entry 0.1 wrote found no spec-harness, and on Windows
|
|
529
611
|
starts a server without a shell, where `npx` cannot start at all. The
|
|
530
612
|
server is told the project with `--root`, and reads `CLAUDE_PROJECT_DIR`
|
|
531
613
|
when no root is named. Use the plugin or `init`'s hooks and server, not
|
package/README.md
CHANGED
|
@@ -8,10 +8,24 @@ The family's words - brief, round, wave, ruling, premise, the two kinds of plugi
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install --save-dev @descent-vtt/spec-harness @descent-vtt/spec-brief
|
|
11
|
-
npx spec-harness init # the plan: what it would configure, and why
|
|
12
|
-
npx spec-harness init --write # apply it
|
|
11
|
+
npx --no-install @descent-vtt/spec-harness init # the plan: what it would configure, and why
|
|
12
|
+
npx --no-install @descent-vtt/spec-harness init --write # apply it
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## Names
|
|
16
|
+
|
|
17
|
+
The package is `@descent-vtt/spec-harness`, and the command it installs is `spec-harness`. The name without the scope is not this project: on npm, `spec-harness` is another publisher's package, and it installs a command of the same name.
|
|
18
|
+
|
|
19
|
+
`npx` fetches and runs the package of whatever name it is given when the project has none installed - a fresh clone, a worktree before `npm ci`, a CI job without the install step - and without a terminal, as under an agent, it does not ask first. So give `npx` the full name:
|
|
20
|
+
|
|
21
|
+
- `npx --no-install @descent-vtt/spec-harness` in a project that installed it: it runs that install, the version the lockfile pins, and where there is none it stops with an error that names this package. Every command in this README and in the skills is written so.
|
|
22
|
+
- `npx @descent-vtt/spec-harness`, without `--no-install`, fetches this package where nothing is installed. The harness works from a project's install, its own and spec-brief's, so that is for a look at `--help` and little else.
|
|
23
|
+
|
|
24
|
+
<!-- bare-name: the two forms in the next sentence are shown as what not to write -->
|
|
25
|
+
Never `npx spec-harness`, and not `npx --no-install spec-harness` either: `--no-install` stops a download, and npm still runs a copy of the other package that an earlier fetch left in its cache, and where nothing is installed it reports that package, by its own version, as the one to install. If the bare name was ever run through `npx` on a machine, in a tree without the install, empty npm's cache of fetched commands there: the `_npx` directory below the path `npm config get cache` prints.
|
|
26
|
+
|
|
27
|
+
The same holds for the siblings: `@descent-vtt/spec-brief`, `@descent-vtt/spec-guard` and `@descent-vtt/spec-graph`, whose names without the scope belonged to nobody on npm on 2026-10-07. The hooks, the MCP server and git's hook go through no `npx` at all: they run `node` with the path of the installed file ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)). The family's [adopting guide](https://github.com/DescentVTT/spec-core/blob/main/docs/adopting.md#names) has what was measured, under npm 10, 11 and 12.
|
|
28
|
+
|
|
15
29
|
## Requirements
|
|
16
30
|
|
|
17
31
|
- **spec-brief 0.2.0 or later**, required: the brief is the contract, spec-brief is its reader, and 0.2.0 is the first whose archive asks this package's spec-brief plugin about signed rulings.
|
|
@@ -109,7 +123,7 @@ May the round write this?
|
|
|
109
123
|
Paths are compared as the filesystem spells them, links resolved and case corrected, so `SRC/db/schema.ts` does not walk past a protection on `src/db/schema.ts` on Windows.
|
|
110
124
|
|
|
111
125
|
- `spec-harness hook claude` answers Claude Code's PreToolUse and PostToolUse hooks: a refusal before the write, with the reason and the next step; a warning after a write outside the scope. It never answers `allow`, which would skip the person's own permission prompt.
|
|
112
|
-
- `spec-harness hook git` is a pre-commit hook for any agent or none.
|
|
126
|
+
- `spec-harness hook git` is a pre-commit hook for any agent or none. `init --git-hook` writes it; in a hook of your own it is the line `node node_modules/@descent-vtt/spec-harness/bin/spec-harness.js hook git`.
|
|
113
127
|
|
|
114
128
|
A guard is a guardrail - an agent that writes through a shell passes it - so the audit and spec-brief's archive are the gates ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
115
129
|
|
|
@@ -117,14 +131,14 @@ A guard is a guardrail - an agent that writes through a shell passes it - so the
|
|
|
117
131
|
|
|
118
132
|
```bash
|
|
119
133
|
# the agent, when the round cannot be done without a protected file
|
|
120
|
-
npx spec-harness escalate --path src/db/schema.ts \
|
|
134
|
+
npx --no-install @descent-vtt/spec-harness escalate --path src/db/schema.ts \
|
|
121
135
|
--reason "Rotation needs a rotated_at column." \
|
|
122
136
|
--option "Allow: one additive column" --option "Refuse: rotation keeps no timestamp" \
|
|
123
137
|
--recommend "Allow; the column is additive."
|
|
124
138
|
|
|
125
139
|
# the person
|
|
126
|
-
npx spec-harness escalate --show E-012-1
|
|
127
|
-
npx spec-harness rule E-012-1 --allow --note "Add rotated_at only."
|
|
140
|
+
npx --no-install @descent-vtt/spec-harness escalate --show E-012-1
|
|
141
|
+
npx --no-install @descent-vtt/spec-harness rule E-012-1 --allow --note "Add rotated_at only."
|
|
128
142
|
git commit -S -m "ruling R-012-1: allow" -- briefs/012_rotate-tokens.md
|
|
129
143
|
```
|
|
130
144
|
|
|
@@ -155,7 +169,28 @@ One report on a round, in four parts:
|
|
|
155
169
|
| The archive | What spec-brief's archive would refuse or warn about, run with `--dry-run`. |
|
|
156
170
|
| Assertions | The brief's own assertions through spec-guard: a goal that fails, or a premise (under a heading such as *The Defect, Measured*) that still holds after the round meant to change it. |
|
|
157
171
|
| Rulings | Rulings whose signatures do not verify. |
|
|
158
|
-
| Dependencies | Every dependency the round added to `package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, `requirements*.txt`, NuGet project files or a `Gemfile`. |
|
|
172
|
+
| Dependencies | Every dependency the round added to `package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, `requirements*.txt`, NuGet project files or a `Gemfile`, and every package it [allowed to run install scripts](#install-scripts) in a `package.json`. |
|
|
173
|
+
|
|
174
|
+
#### Install scripts
|
|
175
|
+
|
|
176
|
+
A dependency's install script is code no reviewer read, run on every contributor's machine and in CI when the dependency is installed. npm 12 runs one only where `allowScripts` in the project's `package.json` allows the package, and `npm install-scripts approve` writes the approval there. So the audit reads that field in each `package.json` the round changed, as npm reads it: an entry whose value is `true` is an approval, one whose value is `false` a denial, and any other value neither.
|
|
177
|
+
|
|
178
|
+
| The round | The audit reports |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| added an entry that is `true`, or turned one from `false` to `true` | `install-script-allowed`, a warning, as a new dependency is: it fails the audit under `--strict`, and no ruling covers it. |
|
|
181
|
+
| added an entry that is `false`, or turned one from `true` to `false` | `install-script-denied`, a note. |
|
|
182
|
+
| removed an entry | `install-script-entry-removed`, a note, which says whether the entry allowed or denied. |
|
|
183
|
+
|
|
184
|
+
- A denial and a removed entry are no grant, so neither is a warning.
|
|
185
|
+
- An entry is named as the manifest names it. npm pins an approval to the version a person reviewed, `canvas@3.1.0`, so one moved to `canvas@3.2.0` is a new approval, and the old entry a removed one.
|
|
186
|
+
- `--format json` lists them as `installScripts`, beside `dependencies`: each with its `file`, its `section` (`allowScripts`), its `name`, and `before` and `after`, each `"allowed"`, `"denied"` or `null`.
|
|
187
|
+
|
|
188
|
+
It reads no other grant ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md) has the reasons):
|
|
189
|
+
|
|
190
|
+
- `.npmrc`: npm reads `allow-scripts` there when `package.json` holds no entry, and `dangerously-allow-all-scripts` whenever it is set. `.npmrc` is not a manifest the audit reads.
|
|
191
|
+
- pnpm: pnpm 11 and 12 keep their approvals as `allowBuilds` in `pnpm-workspace.yaml`, YAML the audit has no reader for, and read nothing from the `pnpm` field of `package.json`. Bun's `trustedDependencies` and Yarn's `dependenciesMeta` are not read either.
|
|
192
|
+
- The package's own `preinstall`, `install`, `postinstall` and `prepare` scripts. They are the repository's own commands, on a line of the round's diff, where an approval turns on code that is in no diff; and `prepare` is where a package builds, so a round changes it for ordinary reasons. To have a round stop before it changes them, protect `package.json` in the brief.
|
|
193
|
+
- Whether npm honours a key. npm passes over a semver range, `canvas@^3`, and a dist-tag, `canvas@latest`, and reads the field in the project's root alone, warning about one in a workspace. The audit reports the entry as written, in whichever `package.json` holds it.
|
|
159
194
|
|
|
160
195
|
#### What it could not measure
|
|
161
196
|
|
|
@@ -175,7 +210,7 @@ measured: goals: 2 held, 0 failed · premises: 1 retired, 0 holding · archive:
|
|
|
175
210
|
0 error(s), 0 warning(s), 1 note(s)
|
|
176
211
|
```
|
|
177
212
|
|
|
178
|
-
A brief that declares no assertion says `goals: none declared`, and draws no warning: a brief without assertions is a brief, and the archive still measures its round.
|
|
213
|
+
A brief that declares no assertion says `goals: none declared`, and draws no warning: a brief without assertions is a brief, and the archive still measures its round. A round that changed an install-script policy has its entries counted at the end of the line, `· install scripts: 2 changed`; a round that changed none says nothing of them there.
|
|
179
214
|
|
|
180
215
|
`--format json` has the same as `measured`, beside `counts`, and `audit_round` says it too:
|
|
181
216
|
|
|
@@ -189,6 +224,7 @@ A brief that declares no assertion says `goals: none declared`, and draws no war
|
|
|
189
224
|
| `unreadableAssertions` | a count |
|
|
190
225
|
| `rulings` | counts: `verified`, `unverified` |
|
|
191
226
|
| `dependencies` | counts: `changed`, `unread` |
|
|
227
|
+
| `installScripts` | a count: `changed` |
|
|
192
228
|
|
|
193
229
|
#### For a forge
|
|
194
230
|
|
|
@@ -212,7 +248,7 @@ spec-harness:
|
|
|
212
248
|
image: node:22
|
|
213
249
|
script:
|
|
214
250
|
- npm ci
|
|
215
|
-
- npx spec-harness audit --format gitlab > gl-spec-harness.json
|
|
251
|
+
- npx --no-install @descent-vtt/spec-harness audit --format gitlab > gl-spec-harness.json
|
|
216
252
|
artifacts:
|
|
217
253
|
when: always
|
|
218
254
|
reports:
|
|
@@ -276,7 +312,9 @@ Configures the family to agree. It prints the plan, merges into files that exist
|
|
|
276
312
|
- **git's pre-commit hook**, where git runs it from: `core.hooksPath`, relative, absolute or under `~`, and the shared hooks of a linked worktree.
|
|
277
313
|
- It refuses a commit that changes what the active brief protects, for any agent or none, a shell's writes included.
|
|
278
314
|
- With `--git-hook` it is written; without, the plan advises it (`advise`).
|
|
279
|
-
-
|
|
315
|
+
- It runs `node` and the script in the project's `node_modules`, as the Claude Code hooks do, and no `npx`. git starts a pre-commit hook at the top of the work tree it commits in, so the script is read from there: a linked worktree is guarded by its own install, and so is each repository that shares a hooks directory. Where the install is not there, the hook says so and the commit waits.
|
|
316
|
+
- It is a POSIX `sh` script on every platform: Git for Windows runs a hook with the `sh` it ships.
|
|
317
|
+
- A hook that exists is never replaced: `init` says to add the line to it. One that runs spec-harness through `npx`, as `init` wrote it through 0.9.1, keeps working, and `init` names the line that runs it with `node`.
|
|
280
318
|
- **The allowed-signers file** `rulings.allowedSigners` names, `.github/allowed_signers` by default: one line per person, and a FIDO2 key, `ssh-keygen -t ed25519-sk`, for anyone whose agent runs as them (`advise`).
|
|
281
319
|
- With it, how the forge keeps it, the ADRs, the tool configurations and the CI configuration from changing unread, as [spec-core's ADR-0005](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0005-the-family-contract.md) asks; [spec-core's adopting guide](https://github.com/DescentVTT/spec-core/blob/main/docs/adopting.md) has the settings:
|
|
282
320
|
- GitHub: CODEOWNERS and a protected branch;
|
|
@@ -306,6 +344,7 @@ The first thing to run when a hook refuses something unexpectedly. It reports:
|
|
|
306
344
|
- A `claude` that is not found, a Windows script that runs no `@anthropic-ai/claude-code` package whose package.json gives a version, and a version that cannot be read are *cannot tell*, never fine; `--strict` fails them.
|
|
307
345
|
- The Claude Code in an editor or the desktop app may be another release than the one on `PATH`: check it there with `claude --version` or `/status`.
|
|
308
346
|
- **git's pre-commit hook**: whether it runs spec-harness, where git runs it from: installed, missing, a hook of the repository's own without the line, or, outside Windows, one git skips because it is not executable.
|
|
347
|
+
- A hook that runs it through `npx` is one that runs it. A note names the line that runs it with `node`: `npx` starts npm to start node on every commit, and under npm 12 prints two `npm notice run` lines each time. A note, never a failure; `--format json` has it as `gitHook.note`.
|
|
309
348
|
|
|
310
349
|
## As a Claude Code plugin
|
|
311
350
|
|
|
@@ -337,7 +376,7 @@ The Claude Code plugin's hooks and server are the ones `init` writes into `.clau
|
|
|
337
376
|
|
|
338
377
|
So after installing the plugin where `init` has run:
|
|
339
378
|
|
|
340
|
-
1. Run `npx spec-harness doctor`: a double install exits 1.
|
|
379
|
+
1. Run `npx --no-install @descent-vtt/spec-harness doctor`: a double install exits 1.
|
|
341
380
|
2. Take the `spec-harness` entries out of those two files, or turn the plugin off for the project.
|
|
342
381
|
|
|
343
382
|
## In a repository that is not Node
|
|
@@ -355,7 +394,9 @@ The hooks name `${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin
|
|
|
355
394
|
- On Windows `npx` is a shim that cannot start without a shell.
|
|
356
395
|
- The copy in the project is the version the project pinned, the one `doctor` checks the siblings against.
|
|
357
396
|
|
|
358
|
-
|
|
397
|
+
git's hook names the same script, `node_modules/@descent-vtt/spec-harness/bin/spec-harness.js`, from the top of the work tree, where git starts it. `npx` would start npm to start node on every commit.
|
|
398
|
+
|
|
399
|
+
A person who runs the tools by hand runs `npx --no-install @descent-vtt/spec-harness`, from the root, giving `npx` the package's [full name](#names).
|
|
359
400
|
|
|
360
401
|
## Configuration
|
|
361
402
|
|
|
@@ -368,7 +409,7 @@ A person who runs the tools by hand runs `npx spec-harness`, from the root.
|
|
|
368
409
|
| `base` | the remote's default branch | What rounds are measured from, and where allowed signers are read. `init` names it, since a repository git did not clone may have no default branch recorded. |
|
|
369
410
|
| `rulings.section` | `"Rulings"` | The brief section holding the rulings table. |
|
|
370
411
|
| `rulings.allowedSigners` | `".github/allowed_signers"` | The allowed-signers file, read from the base branch. |
|
|
371
|
-
| `dependencies.manifests` | the list under [`audit`](#audit-brief) | Manifest names whose added dependencies the audit reports. A name that cannot be read is a warning in the audit. |
|
|
412
|
+
| `dependencies.manifests` | the list under [`audit`](#audit-brief) | Manifest names whose added dependencies the audit reports, and, in a `package.json`, the install scripts it allowed. A name that cannot be read is a warning in the audit. |
|
|
372
413
|
| `context.budget` | `60000` | Characters a context packet may hold; cited documents fill what the rest leaves, and those that do not fit are named by path. |
|
|
373
414
|
| `assertions.premises` | `The Defect, Measured`, `Premises`, `Preconditions` | Sections whose assertions state what was true before the round. |
|
|
374
415
|
| `probes.runs`, `probes.timeout` | `2`, `600` | Runs per probe, and seconds per run. |
|
package/dist/audit.d.ts
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* - The rulings: each one whose signature does not verify is reported, since
|
|
16
16
|
* an unverified ruling allows nothing.
|
|
17
17
|
* - And the one question nobody else asks: which dependencies the round
|
|
18
|
-
* added
|
|
18
|
+
* added, and which packages it allowed to run install scripts. Either is
|
|
19
|
+
* code no reviewer read.
|
|
19
20
|
*
|
|
20
21
|
* A part that could not be measured is a finding, never a silence, and what
|
|
21
22
|
* was measured is reported beside what was found: an audit that finds nothing
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
* nothing (spec-core ADR-0005).
|
|
24
25
|
*/
|
|
25
26
|
import { type VerifiedRuling } from './guard.js';
|
|
26
|
-
import type { DependencyChange } from './manifests.js';
|
|
27
|
+
import type { DependencyChange, InstallScriptChange } from './manifests.js';
|
|
27
28
|
import type { BriefRow, Finding, Severity } from './types.js';
|
|
28
29
|
export interface AssertionOutcome {
|
|
29
30
|
readonly ok: boolean;
|
|
@@ -52,6 +53,8 @@ export interface AuditInput {
|
|
|
52
53
|
readonly unmeasured: string | null;
|
|
53
54
|
readonly dependencies: {
|
|
54
55
|
readonly changes: readonly DependencyChange[];
|
|
56
|
+
/** The entries of an install-script policy the round added, removed or turned; none when not given. */
|
|
57
|
+
readonly installScripts?: readonly InstallScriptChange[] | undefined;
|
|
55
58
|
readonly unread: readonly string[];
|
|
56
59
|
/** Names in `dependencies.manifests` that could not be read, with spec-core's reason; none when not given. */
|
|
57
60
|
readonly unreadNames?: readonly {
|
|
@@ -118,6 +121,10 @@ export interface Measured {
|
|
|
118
121
|
readonly changed: number;
|
|
119
122
|
readonly unread: number;
|
|
120
123
|
};
|
|
124
|
+
/** Entries of an install-script policy the round added, removed or turned, in the manifests read for dependencies. */
|
|
125
|
+
readonly installScripts: {
|
|
126
|
+
readonly changed: number;
|
|
127
|
+
};
|
|
121
128
|
}
|
|
122
129
|
export interface AuditReport {
|
|
123
130
|
readonly findings: readonly Finding[];
|
package/dist/audit.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* - The rulings: each one whose signature does not verify is reported, since
|
|
16
16
|
* an unverified ruling allows nothing.
|
|
17
17
|
* - And the one question nobody else asks: which dependencies the round
|
|
18
|
-
* added
|
|
18
|
+
* added, and which packages it allowed to run install scripts. Either is
|
|
19
|
+
* code no reviewer read.
|
|
19
20
|
*
|
|
20
21
|
* A part that could not be measured is a finding, never a silence, and what
|
|
21
22
|
* was measured is reported beside what was found: an audit that finds nothing
|
|
@@ -49,6 +50,9 @@ export const RULES = {
|
|
|
49
50
|
'new-dependency': 'A dependency the round added: code no reviewer read.',
|
|
50
51
|
'dependency-removed': 'A dependency the round removed.',
|
|
51
52
|
'dependency-changed': 'A dependency the round moved to another version.',
|
|
53
|
+
'install-script-allowed': 'A package the round allowed to run install scripts: code no reviewer read, run on every install.',
|
|
54
|
+
'install-script-denied': 'A package the round denied its install scripts.',
|
|
55
|
+
'install-script-entry-removed': "An entry the round removed from a manifest's install-script policy.",
|
|
52
56
|
};
|
|
53
57
|
function finding(rule, severity, message, hint, file, line, subject) {
|
|
54
58
|
return {
|
|
@@ -205,6 +209,27 @@ export function audit(input) {
|
|
|
205
209
|
out.push(finding('dependency-changed', 'note', `the round moved "${change.name}" from ${change.before} to ${change.after} in ${where}`, 'nothing to do, if the brief meant it', change.file, undefined, subject));
|
|
206
210
|
}
|
|
207
211
|
}
|
|
212
|
+
// An entry is named as written, a version with it: npm pins an approval to
|
|
213
|
+
// the version a person reviewed, so another version is another approval.
|
|
214
|
+
// Only an approval is a grant. A denial and a removed approval stop a
|
|
215
|
+
// script, and a removed denial approves nothing by itself: notes, as a
|
|
216
|
+
// removed dependency is.
|
|
217
|
+
const installScripts = input.dependencies.installScripts ?? [];
|
|
218
|
+
for (const change of installScripts) {
|
|
219
|
+
const where = `${change.file} (${change.section})`;
|
|
220
|
+
const subject = `${change.name} (${change.section})`;
|
|
221
|
+
if (change.after === 'allowed') {
|
|
222
|
+
out.push(finding('install-script-allowed', 'warning', `the round allowed "${change.name}" to run install scripts in ${where}${change.before === 'denied' ? ', which denied it before' : ''}`, 'say in the brief why its install scripts must run, or remove the entry; an install script is code no reviewer read, run on every install', change.file, undefined, subject));
|
|
223
|
+
}
|
|
224
|
+
else if (change.after === 'denied') {
|
|
225
|
+
const was = change.before === 'allowed' ? ', which allowed them before' : '';
|
|
226
|
+
out.push(finding('install-script-denied', 'note', `the round denied "${change.name}" its install scripts in ${where}${was}`, 'nothing to do, if the brief meant it', change.file, undefined, subject));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const was = change.before === 'allowed' ? 'allowed' : 'denied';
|
|
230
|
+
out.push(finding('install-script-entry-removed', 'note', `the round removed "${change.name}" from ${where}, which ${was} its install scripts`, 'nothing to do, if the brief meant it', change.file, undefined, subject));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
208
233
|
const measured = {
|
|
209
234
|
changes: input.unmeasured === null ? 'measured' : 'unmeasured',
|
|
210
235
|
archive: 'unavailable' in input.archive ? 'unavailable' : 'asked',
|
|
@@ -214,6 +239,7 @@ export function audit(input) {
|
|
|
214
239
|
unreadableAssertions: unreadable.length,
|
|
215
240
|
rulings: { verified: input.verifiedRulings.length, unverified: input.unverifiedRulings.length },
|
|
216
241
|
dependencies: { changed: input.dependencies.changes.length, unread: input.dependencies.unread.length },
|
|
242
|
+
installScripts: { changed: installScripts.length },
|
|
217
243
|
};
|
|
218
244
|
return { findings: out, counts: tally(out), measured };
|
|
219
245
|
}
|
|
@@ -244,6 +270,10 @@ export function describeMeasured(measured) {
|
|
|
244
270
|
parts.push(measured.archive === 'asked' ? 'archive: asked' : 'archive: not asked');
|
|
245
271
|
parts.push(rulings.verified + rulings.unverified === 0 ? 'rulings: none' : `rulings: ${rulings.verified} verified, ${rulings.unverified} unverified`);
|
|
246
272
|
parts.push(measured.changes === 'unmeasured' ? 'dependencies: not measured' : `dependencies: ${dependencies.changed} changed, ${dependencies.unread} unread`);
|
|
273
|
+
// Said only when a policy changed, as an assertion that cannot be read is:
|
|
274
|
+
// most repositories have no such policy, and the line is a person's.
|
|
275
|
+
if (measured.installScripts.changed > 0)
|
|
276
|
+
parts.push(`install scripts: ${measured.installScripts.changed} changed`);
|
|
247
277
|
return `measured: ${parts.join(' · ')}`;
|
|
248
278
|
}
|
|
249
279
|
//# sourceMappingURL=audit.js.map
|
package/dist/audit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAuB,MAAM,YAAY,CAAC;AAEpE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAgF1C;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAqC;IACrD,UAAU,EAAE,wDAAwD;IACpE,mBAAmB,EAAE,+DAA+D;IACpF,sBAAsB,EAAE,0CAA0C;IAClE,sBAAsB,EAAE,iFAAiF;IACzG,aAAa,EAAE,yCAAyC;IACxD,eAAe,EAAE,2DAA2D;IAC5E,iBAAiB,EAAE,+DAA+D;IAClF,eAAe,EAAE,kFAAkF;IACnG,mBAAmB,EAAE,iEAAiE;IACtF,mBAAmB,EAAE,iDAAiD;IACtE,mBAAmB,EAAE,yFAAyF;IAC9G,oBAAoB,EAAE,+EAA+E;IACrG,sBAAsB,EAAE,gFAAgF;IACxG,sBAAsB,EAAE,uGAAuG;IAC/H,iBAAiB,EAAE,oEAAoE;IACvF,gBAAgB,EAAE,sDAAsD;IACxE,oBAAoB,EAAE,iCAAiC;IACvD,oBAAoB,EAAE,kDAAkD;CACzE,CAAC;AAEF,SAAS,OAAO,CAAC,IAAY,EAAE,QAAkB,EAAE,OAAe,EAAE,IAAY,EAAE,IAAa,EAAE,IAAa,EAAE,OAAgB;IAC9H,OAAO;QACL,IAAI;QACJ,QAAQ;QACR,OAAO;QACP,IAAI;QACJ,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QACvC,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QACvC,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,QAA2C,EAAE,QAA2B;IAChG,MAAM,IAAI,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC3F,yEAAyE;IACzE,mEAAmE;IACnE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;AAC3F,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAoC,EACpC,OAA0F,EAC1F,MAAe;IAEf,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,OAAO,CACZ,iBAAiB,EACjB,MAAM,EACN,SAAS,KAAK,CAAC,EAAE,2DAA2D,OAAO,CAAC,WAAW,EAAE,EACjG,yEAAyE,EACzE,KAAK,CAAC,IAAI,EACV,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,WAAW,CACpB,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CACZ,eAAe,EACf,OAAO,EACP,SAAS,KAAK,CAAC,EAAE,+BAA+B,OAAO,CAAC,WAAW,KAAK,OAAO,CAAC,OAAO,EAAE,EACzF,2IAA2I,EAC3I,KAAK,CAAC,IAAI,EACV,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,WAAW,CACpB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAoC,EAAE,SAA8B;IAC3G,OAAO,OAAO,CACZ,sBAAsB,EACtB,SAAS,EACT,6CAA6C,KAAK,CAAC,EAAE,+CAA+C,SAAS,CAAC,OAAO,EAAE,EACvH,wBAAwB,KAAK,CAAC,IAAI,+DAA+D,EACjG,KAAK,CAAC,IAAI,EACV,SAAS,CAAC,IAAI,EACd,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,CACrB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,WAAW,CAAC,MAAqB,EAAE,KAAiB;IAC3D,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,KAAK,gBAAgB,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,eAAe,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACzI,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IAC7C,MAAM,OAAO,GAAG,UAAU,MAAM,CAAC,EAAE,eAAe,MAAM,CAAC,MAAM,aAAa,CAAC;IAC7E,OAAO,KAAK,CAAC,YAAY;QACvB,CAAC,CAAC,GAAG,OAAO,uJAAuJ;QACnK,CAAC,CAAC,GAAG,OAAO,iHAAiH,MAAM,uEAAuE,CAAC;AAC/M,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,KAAiB;IACrC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IACxB,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAE5C,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAC9B,GAAG,CAAC,IAAI,CACN,OAAO,CACL,YAAY,EACZ,SAAS,EACT,0CAA0C,KAAK,CAAC,UAAU,EAAE,EAC5D,uGAAuG,EACvG,KAAK,CAAC,IAAI,CACX,CACF,CAAC;IACJ,CAAC;IAED,IAAI,aAAa,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QACnC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,SAAS,EAAE,0CAA0C,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,4CAA4C,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACrL,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5E,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,MAAM,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAClJ,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QACrC,MAAM,WAAW,GAAI,KAAK,CAAC,UAAsC,CAAC,WAAW,CAAC;QAC9E,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,SAAS,EAAE,wCAAwC,WAAW,EAAE,EAAE,4CAA4C,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACxK,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;YACvC,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;YACvF,IAAI,OAAO,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC;gBAC1B,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;gBACtB,GAAG,CAAC,IAAI,CACN,OAAO,CACL,eAAe,EACf,SAAS,EACT,0CAA0C,OAAO,CAAC,WAAW,EAAE,EAC/D,oHAAoH,EACpH,KAAK,CAAC,IAAI,EACV,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,WAAW,CACpB,CACF,CAAC;YACJ,CAAC;iBAAM,IAAI,OAAO,EAAE,CAAC;gBACnB,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;gBACtB,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,MAAM,EAAE,qDAAqD,OAAO,CAAC,WAAW,EAAE,EAAE,eAAe,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;YAC3L,CAAC;iBAAM,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;gBACvB,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;gBAClB,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,WAAW,KAAK,OAAO,CAAC,OAAO,EAAE,EAAE,wCAAwC,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;YACnL,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IACD,2EAA2E;IAC3E,0EAA0E;IAC1E,MAAM,UAAU,GAAG,KAAK,CAAC,oBAAoB,IAAI,EAAE,CAAC;IACpD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,GAAG,CAAC,IAAI,CACN,OAAO,CACL,sBAAsB,EACtB,SAAS,EACT,mFAAmF,SAAS,CAAC,OAAO,EAAE,EACtG,wBAAwB,KAAK,CAAC,IAAI,sEAAsE,EACxG,KAAK,CAAC,IAAI,EACV,SAAS,CAAC,IAAI,EACd,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,CACrB,CACF,CAAC;IACJ,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC7C,GAAG,CAAC,IAAI,CACN,OAAO,CACL,mBAAmB,EACnB,SAAS,EACT,UAAU,MAAM,CAAC,EAAE,oBAAoB,MAAM,CAAC,MAAM,EAAE,EACtD,oHAAoH,EACpH,KAAK,CAAC,IAAI,EACV,SAAS,EACT,MAAM,CAAC,EAAE,CACV,CACF,CAAC;IACJ,CAAC;IAED,uEAAuE;IACvE,2EAA2E;IAC3E,0EAA0E;IAC1E,gDAAgD;IAChD,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,IAAI,KAAK,IAAI;YAAE,SAAS;QAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,6EAA6E,CAAC,CAAC,CAAC,6GAA6G,CAAC;QAC9N,GAAG,CAAC,IAAI,CACN,OAAO,CACL,mBAAmB,EACnB,SAAS,EACT,SAAS,KAAK,CAAC,EAAE,cAAc,OAAO,MAAM,IAAI,EAAE,EAClD,uCAAuC,KAAK,CAAC,IAAI,iGAAiG,EAClJ,KAAK,CAAC,IAAI,EACV,SAAS,EACT,OAAO,CACR,CACF,CAAC;IACJ,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,eAAe,EAAE,CAAC;QAC3C,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;YAC7B,IAAI,IAAI,KAAK,IAAI;gBAAE,SAAS;YAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,2EAA2E,CAAC,CAAC,CAAC,2GAA2G,CAAC;YAC1N,GAAG,CAAC,IAAI,CACN,OAAO,CACL,oBAAoB,EACpB,SAAS,EACT,UAAU,MAAM,CAAC,EAAE,YAAY,OAAO,MAAM,IAAI,EAAE,EAClD,oIAAoI,EACpI,KAAK,CAAC,IAAI,EACV,SAAS,EACT,GAAG,MAAM,CAAC,EAAE,IAAI,OAAO,EAAE,CAC1B,CACF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,KAAK,CAAC,YAAY,CAAC,WAAW,IAAI,EAAE,EAAE,CAAC;QACpE,GAAG,CAAC,IAAI,CACN,OAAO,CACL,sBAAsB,EACtB,SAAS,EACT,mCAAmC,IAAI,4BAA4B,MAAM,iCAAiC,EAC1G,6BAA6B,WAAW,6BAA6B,EACrE,WAAW,EACX,SAAS,EACT,IAAI,CACL,CACF,CAAC;IACJ,CAAC;IACD,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,YAAY,CAAC,WAAW,IAAI,EAAE,EAAE,CAAC;QACnE,0EAA0E;QAC1E,wEAAwE;QACxE,uBAAuB;QACvB,MAAM,IAAI,GAAG,KAAK;YAChB,CAAC,CAAC,wHAAwH;YAC1H,CAAC,CAAC,+IAA+I,CAAC;QACpJ,GAAG,CAAC,IAAI,CACN,OAAO,CACL,sBAAsB,EACtB,SAAS,EACT,mCAAmC,IAAI,MAAM,IAAI,EAAE,EACnD,uCAAuC,WAAW,mEAAmE,EACrH,WAAW,EACX,SAAS,EACT,IAAI,CACL,CACF,CAAC;IACJ,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;QAC7C,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,SAAS,EAAE,GAAG,IAAI,iDAAiD,EAAE,kDAAkD,EAAE,IAAI,CAAC,CAAC,CAAC;IACtK,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;QAChD,MAAM,KAAK,GAAG,GAAG,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,OAAO,GAAG,CAAC;QACnD,MAAM,OAAO,GAAG,GAAG,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,OAAO,GAAG,CAAC;QACrD,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YAC3B,GAAG,CAAC,IAAI,CACN,OAAO,CACL,gBAAgB,EAChB,SAAS,EACT,mBAAmB,MAAM,CAAC,SAAS,gBAAgB,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,EAAE,OAAO,KAAK,EAAE,EAC7H,4FAA4F,EAC5F,MAAM,CAAC,IAAI,EACX,SAAS,EACT,OAAO,CACR,CACF,CAAC;QACJ,CAAC;aAAM,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YACjC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,MAAM,EAAE,sBAAsB,MAAM,CAAC,IAAI,UAAU,KAAK,EAAE,EAAE,eAAe,EAAE,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;QACxJ,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,MAAM,EAAE,oBAAoB,MAAM,CAAC,IAAI,UAAU,MAAM,CAAC,MAAM,OAAO,MAAM,CAAC,KAAK,OAAO,KAAK,EAAE,EAAE,sCAAsC,EAAE,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;QACpN,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAa;QACzB,OAAO,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY;QAC9D,OAAO,EAAE,aAAa,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO;QACjE,UAAU,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa;QACnE,KAAK;QACL,QAAQ;QACR,oBAAoB,EAAE,UAAU,CAAC,MAAM;QACvC,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,eAAe,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,iBAAiB,CAAC,MAAM,EAAE;QAC/F,YAAY,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE;KACvG,CAAC;IACF,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC;AACzD,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,KAAK,CAAC,QAA4B;IAChD,MAAM,MAAM,GAA6B,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC3E,KAAK,MAAM,IAAI,IAAI,QAAQ;QAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAkB;IACjD,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,QAAQ,CAAC;IAC5D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,QAAQ,CAAC,UAAU,KAAK,aAAa,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IACpC,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,IAAI,UAAU,KAAK,CAAC,MAAM,SAAS,CAAC,CAAC;QAC3H,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,GAAG,QAAQ,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,aAAa,QAAQ,CAAC,OAAO,aAAa,QAAQ,CAAC,OAAO,UAAU,CAAC,CAAC;IAC3J,CAAC;IACD,IAAI,QAAQ,CAAC,oBAAoB,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,0BAA0B,QAAQ,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAC7G,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC;IACnF,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,YAAY,OAAO,CAAC,QAAQ,cAAc,OAAO,CAAC,UAAU,aAAa,CAAC,CAAC;IACtJ,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,KAAK,YAAY,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC,iBAAiB,YAAY,CAAC,OAAO,aAAa,YAAY,CAAC,MAAM,SAAS,CAAC,CAAC;IAC9J,OAAO,aAAa,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;AAC1C,CAAC","sourcesContent":["/**\n * Did the round stay inside the lines?\n *\n * One report, from three sources and one question of its own:\n *\n * - spec-brief's archive, asked with `--dry-run`: open boxes, protected files\n * changed, files outside the scope, a tree that was not committed. The\n * archive is spec-brief's decision, so its reasons are carried as they are\n * - one defect, one finding - and the harness does not measure the scope a\n * second time.\n * - spec-guard, over the brief's own assertions: the goals must hold now. A\n * premise - an assertion in a section that states what was true before the\n * round, such as \"The Defect, Measured\" - is expected to stop holding, and\n * one that still holds says the defect may still be there.\n * - The rulings: each one whose signature does not verify is reported, since\n * an unverified ruling allows nothing.\n * - And the one question nobody else asks: which dependencies the round\n * added. A dependency is code no reviewer read.\n *\n * A part that could not be measured is a finding, never a silence, and what\n * was measured is reported beside what was found: an audit that finds nothing\n * because it checked nothing must not read as one that checked and found\n * nothing (spec-core ADR-0005).\n */\n\nimport { CONFIG_FILE } from './config.js';\nimport { PLUGIN } from './configure.js';\nimport { rooted, rulingFor, type VerifiedRuling } from './guard.js';\nimport type { DependencyChange } from './manifests.js';\nimport { sameSection } from './reader.js';\nimport type { BriefRow, Finding, Severity } from './types.js';\n\nexport interface AssertionOutcome {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly line: number;\n /** The brief section the directive sits in, or `null`. */\n readonly section: string | null;\n /** Every section it sits in, outermost first; `[section]` when not given. */\n readonly enclosing?: readonly string[] | undefined;\n}\n\n/** An assertion spec-guard could not read, as it reports it: nothing it states was run. */\nexport interface UnreadableAssertion {\n readonly message: string;\n readonly line: number;\n /** The directive as written. */\n readonly raw: string;\n}\n\n/** A reason spec-brief's archive gives; a `protected-file` refusal names its `path`. */\nexport interface ArchiveReason extends Finding {\n readonly path?: string | undefined;\n}\n\nexport interface AuditInput {\n readonly brief: BriefRow;\n /** Why the round's changes could not be measured, when they could not. */\n readonly unmeasured: string | null;\n readonly dependencies: {\n readonly changes: readonly DependencyChange[];\n readonly unread: readonly string[];\n /** Names in `dependencies.manifests` that could not be read, with spec-core's reason; none when not given. */\n readonly unreadNames?: readonly { readonly name: string; readonly reason: string }[] | undefined;\n /** Names in `dependencies.manifests` a leading `/` roots, and whether every alternative of each is rooted; none when not given. */\n readonly rootedNames?: readonly { readonly name: string; readonly whole: boolean }[] | undefined;\n };\n readonly archive: { readonly blocking: readonly ArchiveReason[]; readonly warnings: readonly ArchiveReason[] } | { readonly unavailable: string };\n readonly assertions: readonly AssertionOutcome[] | { readonly unavailable: string };\n /** The assertions in the brief spec-guard could not read; none when not given. */\n readonly unreadableAssertions?: readonly UnreadableAssertion[] | undefined;\n /** Section names whose assertions are premises. Compared without case, emphasis, a leading number or a trailing colon. */\n readonly premiseSections: readonly string[];\n readonly unverifiedRulings: readonly { readonly id: string; readonly reason: string }[];\n /** The rulings whose signatures verify, which a protected file the archive refuses may be covered by. */\n readonly verifiedRulings: readonly VerifiedRuling[];\n /** Whether spec-brief's configuration loads this package's plugin, through which its archive learns of them. */\n readonly pluginLoaded: boolean;\n}\n\n/**\n * What the audit measured, beside what it found. A script tells \"nothing\n * found\" from \"nothing checked\" here: `assertions: \"unavailable\"` with no\n * goal failed is an audit that ran no goal.\n */\nexport interface Measured {\n /** Whether the round's changes were measured from a base; the dependencies are read from them. */\n readonly changes: 'measured' | 'unmeasured';\n /** Whether spec-brief's archive answered. */\n readonly archive: 'asked' | 'unavailable';\n /** Whether spec-guard ran the brief's assertions. */\n readonly assertions: 'run' | 'unavailable';\n readonly goals: { readonly held: number; readonly failed: number };\n readonly premises: { readonly retired: number; readonly holding: number };\n /** Assertions spec-guard could not read, so ran none of. */\n readonly unreadableAssertions: number;\n /** The rulings that allow something, by whether their signatures verify. */\n readonly rulings: { readonly verified: number; readonly unverified: number };\n /** Dependencies the round added, removed or moved, and manifests it changed that could not be read. */\n readonly dependencies: { readonly changed: number; readonly unread: number };\n}\n\nexport interface AuditReport {\n readonly findings: readonly Finding[];\n readonly counts: Readonly<Record<Severity, number>>;\n readonly measured: Measured;\n}\n\n/**\n * Each rule the audit and `premises` report, as a reader of SARIF sees it\n * described. A reason spec-brief's archive gives is `archive/<its rule>`.\n */\nexport const RULES: Readonly<Record<string, string>> = {\n unmeasured: \"The round's changes could not be measured from a base.\",\n 'archive-unchecked': \"spec-brief's archive could not be asked what it would refuse.\",\n 'assertions-unchecked': \"The brief's assertions could not be run.\",\n 'assertion-unreadable': 'An assertion in the brief spec-guard cannot read, so nothing it states was run.',\n 'goal-failed': 'A goal the brief asserts does not hold.',\n 'premise-holds': 'A premise still holds after the round meant to change it.',\n 'premise-retired': 'A premise no longer holds, as the round on the brief intends.',\n 'stale-premise': \"A live brief's premise no longer holds: what it was written against has changed.\",\n 'ruling-unverified': 'A ruling whose signature does not verify, which allows nothing.',\n 'ruling-unreadable': 'A row of the rulings table that cannot be read.',\n 'protection-rooted': \"A protection rooted at the filesystem's root, which protects no path of the repository.\",\n 'ruling-path-rooted': \"A signed ruling's path rooted at the filesystem's root, which allows nothing.\",\n 'manifest-name-unread': 'A name in dependencies.manifests that cannot be read, which names no manifest.',\n 'manifest-name-rooted': \"A name in dependencies.manifests rooted at the filesystem's root, where no file of the repository is.\",\n 'manifest-unread': 'A manifest the round changed that cannot be read for dependencies.',\n 'new-dependency': 'A dependency the round added: code no reviewer read.',\n 'dependency-removed': 'A dependency the round removed.',\n 'dependency-changed': 'A dependency the round moved to another version.',\n};\n\nfunction finding(rule: string, severity: Severity, message: string, hint: string, file?: string, line?: number, subject?: string): Finding {\n return {\n rule,\n severity,\n message,\n hint,\n ...(file === undefined ? {} : { file }),\n ...(line === undefined ? {} : { line }),\n ...(subject === undefined ? {} : { subject }),\n };\n}\n\n/**\n * Whether an assertion's section is a premise section. Compared as the\n * rulings section is, and as spec-brief compares section names: a heading\n * written `## 3. **The Defect, Measured:**` is still the premise section, and\n * an assertion under it read as a goal would fail the round for retiring it.\n */\nexport function isPremise(sections: string | null | readonly string[], premises: readonly string[]): boolean {\n const list = sections === null ? [] : typeof sections === 'string' ? [sections] : sections;\n // A premise section's subsections are still premises: `### Before` under\n // `## The Defect, Measured` states what was true before the round.\n return list.some((section) => premises.some((premise) => sameSection(section, premise)));\n}\n\n/**\n * A live brief's premise that no longer holds, as `premises` reports it. On\n * the brief a round is working on - the one the flag, SPEC_BRIEF or the\n * branch names - that is what the round set out to do, and it is reported as\n * the audit reports it; on any other, what the brief was written against has\n * changed under it.\n */\nexport function premiseFinding(\n brief: Pick<BriefRow, 'id' | 'file'>,\n outcome: { readonly description: string; readonly message: string; readonly line: number },\n active: boolean,\n): Finding {\n if (active) {\n return finding(\n 'premise-retired',\n 'note',\n `brief ${brief.id}'s premise no longer holds, as the round on it intends: ${outcome.description}`,\n 'nothing to do: this is the round that changes it, and audit measures it',\n brief.file,\n outcome.line,\n outcome.description,\n );\n }\n return finding(\n 'stale-premise',\n 'error',\n `brief ${brief.id}'s premise no longer holds: ${outcome.description}: ${outcome.message}`,\n 'what the brief was written against has changed; archive the brief if its work is done, or rewrite its premise before a round is run on it',\n brief.file,\n outcome.line,\n outcome.description,\n );\n}\n\n/**\n * A premise spec-guard cannot read, as `premises` reports it: nothing checks\n * whether it still holds, so a stale brief would pass for a live one. A\n * warning, as the audit's is, which fails the run under `--strict`.\n */\nexport function unreadablePremiseFinding(brief: Pick<BriefRow, 'id' | 'file'>, assertion: UnreadableAssertion): Finding {\n return finding(\n 'assertion-unreadable',\n 'warning',\n `spec-guard cannot read a premise of brief ${brief.id}, so whether it still holds is not checked: ${assertion.message}`,\n `fix the directive in ${brief.file}; until spec-guard can read it, nothing checks what it states`,\n brief.file,\n assertion.line,\n assertion.raw.trim(),\n );\n}\n\n/**\n * The next step for a reason the archive gives. A protected file a verified\n * ruling covers is refused only because the archive did not learn of the\n * ruling, and recording a departure in the brief would not fix that.\n */\nfunction archiveHint(reason: ArchiveReason, input: AuditInput): string {\n const ruling = reason.rule === 'protected-file' && reason.path !== undefined ? rulingFor(input.verifiedRulings, reason.path) : undefined;\n if (ruling === undefined) return reason.hint;\n const allowed = `ruling ${ruling.id}, signed by ${ruling.signer}, allows it`;\n return input.pluginLoaded\n ? `${allowed}, and the archive still refused it: check that spec-brief loads the spec-harness installed here and measures from the same base (spec-harness doctor)`\n : `${allowed}, but spec-brief does not load spec-harness's plugin, which is how its archive learns of signed rulings: add \"${PLUGIN}\" to \"plugins\" in its configuration, or run spec-harness init --write`;\n}\n\nexport function audit(input: AuditInput): AuditReport {\n const { brief } = input;\n const out: Finding[] = [];\n const goals = { held: 0, failed: 0 };\n const premises = { retired: 0, holding: 0 };\n\n if (input.unmeasured !== null) {\n out.push(\n finding(\n 'unmeasured',\n 'warning',\n `the round's changes were not measured: ${input.unmeasured}`,\n 'pass --base <ref>, set \"base\" in .spec-harness.json, or audit on the round\\'s branch before it merges',\n brief.file,\n ),\n );\n }\n\n if ('unavailable' in input.archive) {\n out.push(finding('archive-unchecked', 'warning', `what the archive would say is unknown: ${input.archive.unavailable}`, 'install spec-brief, or fix what stopped it', brief.file));\n } else {\n for (const reason of [...input.archive.blocking, ...input.archive.warnings]) {\n out.push(finding(`archive/${reason.rule}`, reason.severity, reason.message, archiveHint(reason, input), reason.file, reason.line, reason.path));\n }\n }\n\n if (!Array.isArray(input.assertions)) {\n const unavailable = (input.assertions as { unavailable: string }).unavailable;\n out.push(finding('assertions-unchecked', 'warning', `the brief's assertions were not run: ${unavailable}`, 'install spec-guard, or fix what stopped it', brief.file));\n } else {\n for (const outcome of input.assertions) {\n const premise = isPremise(outcome.enclosing ?? outcome.section, input.premiseSections);\n if (premise && outcome.ok) {\n premises.holding += 1;\n out.push(\n finding(\n 'premise-holds',\n 'warning',\n `a premise still holds after the round: ${outcome.description}`,\n 'the round set out to change what this premise states; check that it did, or move the assertion out of the premises',\n brief.file,\n outcome.line,\n outcome.description,\n ),\n );\n } else if (premise) {\n premises.retired += 1;\n out.push(finding('premise-retired', 'note', `a premise no longer holds, as the round intended: ${outcome.description}`, 'nothing to do', brief.file, outcome.line, outcome.description));\n } else if (!outcome.ok) {\n goals.failed += 1;\n out.push(finding('goal-failed', 'error', `${outcome.description}: ${outcome.message}`, 'the round is not done until this holds', brief.file, outcome.line, outcome.description));\n } else {\n goals.held += 1;\n }\n }\n }\n // Whether a goal or a premise, an assertion spec-guard cannot read was not\n // run, and dropping it would let the audit pass on what it never checked.\n const unreadable = input.unreadableAssertions ?? [];\n for (const assertion of unreadable) {\n out.push(\n finding(\n 'assertion-unreadable',\n 'warning',\n `spec-guard cannot read an assertion in the brief, so nothing it states was run: ${assertion.message}`,\n `fix the directive in ${brief.file}; until spec-guard can read it, the audit measures nothing it states`,\n brief.file,\n assertion.line,\n assertion.raw.trim(),\n ),\n );\n }\n\n for (const ruling of input.unverifiedRulings) {\n out.push(\n finding(\n 'ruling-unverified',\n 'warning',\n `ruling ${ruling.id} allows nothing: ${ruling.reason}`,\n 'a ruling counts when the commit that last changed its row is signed by a key in the base branch\\'s allowed signers',\n brief.file,\n undefined,\n ruling.id,\n ),\n );\n }\n\n // A protection or a ruling's path a leading `/` roots can be read, and\n // names no path the guard or the archive decides, all repository-relative:\n // the file it was meant to protect was the round's to write, and the file\n // a ruling was meant to allow is still refused.\n for (const pattern of brief.protectedFiles) {\n const root = rooted(pattern);\n if (root === null) continue;\n const what = root === 'whole' ? 'which a leading \"/\" roots at the filesystem\\'s root, so it protects no path' : 'an alternative of which a leading \"/\" roots at the filesystem\\'s root, so that alternative protects no path';\n out.push(\n finding(\n 'protection-rooted',\n 'warning',\n `brief ${brief.id} protects \"${pattern}\", ${what}`,\n `write it without the leading \"/\" in ${brief.file}, since a protection is read from the repository's root, and check what the round changed there`,\n brief.file,\n undefined,\n pattern,\n ),\n );\n }\n for (const ruling of input.verifiedRulings) {\n for (const pattern of ruling.paths) {\n const root = rooted(pattern);\n if (root === null) continue;\n const what = root === 'whole' ? 'which a leading \"/\" roots at the filesystem\\'s root, so it allows nothing' : 'an alternative of which a leading \"/\" roots at the filesystem\\'s root, so that alternative allows nothing';\n out.push(\n finding(\n 'ruling-path-rooted',\n 'warning',\n `ruling ${ruling.id} allows \"${pattern}\", ${what}`,\n 'a ruling\\'s path is read from the repository\\'s root: escalate again, and have the person rule on the path without the leading \"/\"',\n brief.file,\n undefined,\n `${ruling.id} ${pattern}`,\n ),\n );\n }\n }\n\n for (const { name, reason } of input.dependencies.unreadNames ?? []) {\n out.push(\n finding(\n 'manifest-name-unread',\n 'warning',\n `\"dependencies.manifests\" names \"${name}\", which cannot be read: ${reason}; no manifest it names was read`,\n `fix or remove the name in ${CONFIG_FILE}; the other names were read`,\n CONFIG_FILE,\n undefined,\n name,\n ),\n );\n }\n for (const { name, whole } of input.dependencies.rootedNames ?? []) {\n // Every path the audit reads is repository-relative, so a rooted name, or\n // a rooted alternative of one, names no manifest, as silently as a name\n // that cannot be read.\n const what = whole\n ? 'a leading \"/\" roots it at the filesystem\\'s root, where no file of the repository is, so no manifest it names was read'\n : 'a leading \"/\" roots an alternative of it at the filesystem\\'s root, where no file of the repository is, so that alternative names no manifest';\n out.push(\n finding(\n 'manifest-name-rooted',\n 'warning',\n `\"dependencies.manifests\" names \"${name}\": ${what}`,\n `write it without the leading \"/\" in ${CONFIG_FILE}, since a name is matched at any depth; the other names were read`,\n CONFIG_FILE,\n undefined,\n name,\n ),\n );\n }\n for (const file of input.dependencies.unread) {\n out.push(finding('manifest-unread', 'warning', `${file} changed and could not be read for dependencies`, 'check its dependencies by hand; the audit cannot', file));\n }\n for (const change of input.dependencies.changes) {\n const where = `${change.file} (${change.section})`;\n const subject = `${change.name} (${change.section})`;\n if (change.before === null) {\n out.push(\n finding(\n 'new-dependency',\n 'warning',\n `the round added ${change.ecosystem} dependency \"${change.name}\"${change.after === '' ? '' : ` ${change.after}`} in ${where}`,\n 'say in the brief why it is needed, or remove it; a new dependency is code no reviewer read',\n change.file,\n undefined,\n subject,\n ),\n );\n } else if (change.after === null) {\n out.push(finding('dependency-removed', 'note', `the round removed \"${change.name}\" from ${where}`, 'nothing to do', change.file, undefined, subject));\n } else {\n out.push(finding('dependency-changed', 'note', `the round moved \"${change.name}\" from ${change.before} to ${change.after} in ${where}`, 'nothing to do, if the brief meant it', change.file, undefined, subject));\n }\n }\n\n const measured: Measured = {\n changes: input.unmeasured === null ? 'measured' : 'unmeasured',\n archive: 'unavailable' in input.archive ? 'unavailable' : 'asked',\n assertions: Array.isArray(input.assertions) ? 'run' : 'unavailable',\n goals,\n premises,\n unreadableAssertions: unreadable.length,\n rulings: { verified: input.verifiedRulings.length, unverified: input.unverifiedRulings.length },\n dependencies: { changed: input.dependencies.changes.length, unread: input.dependencies.unread.length },\n };\n return { findings: out, counts: tally(out), measured };\n}\n\n/** How many findings of each severity. */\nexport function tally(findings: readonly Finding[]): Record<Severity, number> {\n const counts: Record<Severity, number> = { error: 0, warning: 0, note: 0 };\n for (const item of findings) counts[item.severity] += 1;\n return counts;\n}\n\n/**\n * What the audit measured, in one line a person reads above the counts. A\n * brief that declares no assertion says so, and draws no warning: a brief\n * without assertions is a brief, and the archive still measured its round.\n */\nexport function describeMeasured(measured: Measured): string {\n const { goals, premises, rulings, dependencies } = measured;\n const parts: string[] = [];\n if (measured.assertions === 'unavailable') {\n parts.push('assertions: not run');\n } else {\n parts.push(goals.held + goals.failed === 0 ? 'goals: none declared' : `goals: ${goals.held} held, ${goals.failed} failed`);\n parts.push(premises.retired + premises.holding === 0 ? 'premises: none declared' : `premises: ${premises.retired} retired, ${premises.holding} holding`);\n }\n if (measured.unreadableAssertions > 0) parts.push(`unreadable assertions: ${measured.unreadableAssertions}`);\n parts.push(measured.archive === 'asked' ? 'archive: asked' : 'archive: not asked');\n parts.push(rulings.verified + rulings.unverified === 0 ? 'rulings: none' : `rulings: ${rulings.verified} verified, ${rulings.unverified} unverified`);\n parts.push(measured.changes === 'unmeasured' ? 'dependencies: not measured' : `dependencies: ${dependencies.changed} changed, ${dependencies.unread} unread`);\n return `measured: ${parts.join(' · ')}`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Did the round stay inside the lines?\n *\n * One report, from three sources and one question of its own:\n *\n * - spec-brief's archive, asked with `--dry-run`: open boxes, protected files\n * changed, files outside the scope, a tree that was not committed. The\n * archive is spec-brief's decision, so its reasons are carried as they are\n * - one defect, one finding - and the harness does not measure the scope a\n * second time.\n * - spec-guard, over the brief's own assertions: the goals must hold now. A\n * premise - an assertion in a section that states what was true before the\n * round, such as \"The Defect, Measured\" - is expected to stop holding, and\n * one that still holds says the defect may still be there.\n * - The rulings: each one whose signature does not verify is reported, since\n * an unverified ruling allows nothing.\n * - And the one question nobody else asks: which dependencies the round\n * added, and which packages it allowed to run install scripts. Either is\n * code no reviewer read.\n *\n * A part that could not be measured is a finding, never a silence, and what\n * was measured is reported beside what was found: an audit that finds nothing\n * because it checked nothing must not read as one that checked and found\n * nothing (spec-core ADR-0005).\n */\n\nimport { CONFIG_FILE } from './config.js';\nimport { PLUGIN } from './configure.js';\nimport { rooted, rulingFor, type VerifiedRuling } from './guard.js';\nimport type { DependencyChange, InstallScriptChange } from './manifests.js';\nimport { sameSection } from './reader.js';\nimport type { BriefRow, Finding, Severity } from './types.js';\n\nexport interface AssertionOutcome {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly line: number;\n /** The brief section the directive sits in, or `null`. */\n readonly section: string | null;\n /** Every section it sits in, outermost first; `[section]` when not given. */\n readonly enclosing?: readonly string[] | undefined;\n}\n\n/** An assertion spec-guard could not read, as it reports it: nothing it states was run. */\nexport interface UnreadableAssertion {\n readonly message: string;\n readonly line: number;\n /** The directive as written. */\n readonly raw: string;\n}\n\n/** A reason spec-brief's archive gives; a `protected-file` refusal names its `path`. */\nexport interface ArchiveReason extends Finding {\n readonly path?: string | undefined;\n}\n\nexport interface AuditInput {\n readonly brief: BriefRow;\n /** Why the round's changes could not be measured, when they could not. */\n readonly unmeasured: string | null;\n readonly dependencies: {\n readonly changes: readonly DependencyChange[];\n /** The entries of an install-script policy the round added, removed or turned; none when not given. */\n readonly installScripts?: readonly InstallScriptChange[] | undefined;\n readonly unread: readonly string[];\n /** Names in `dependencies.manifests` that could not be read, with spec-core's reason; none when not given. */\n readonly unreadNames?: readonly { readonly name: string; readonly reason: string }[] | undefined;\n /** Names in `dependencies.manifests` a leading `/` roots, and whether every alternative of each is rooted; none when not given. */\n readonly rootedNames?: readonly { readonly name: string; readonly whole: boolean }[] | undefined;\n };\n readonly archive: { readonly blocking: readonly ArchiveReason[]; readonly warnings: readonly ArchiveReason[] } | { readonly unavailable: string };\n readonly assertions: readonly AssertionOutcome[] | { readonly unavailable: string };\n /** The assertions in the brief spec-guard could not read; none when not given. */\n readonly unreadableAssertions?: readonly UnreadableAssertion[] | undefined;\n /** Section names whose assertions are premises. Compared without case, emphasis, a leading number or a trailing colon. */\n readonly premiseSections: readonly string[];\n readonly unverifiedRulings: readonly { readonly id: string; readonly reason: string }[];\n /** The rulings whose signatures verify, which a protected file the archive refuses may be covered by. */\n readonly verifiedRulings: readonly VerifiedRuling[];\n /** Whether spec-brief's configuration loads this package's plugin, through which its archive learns of them. */\n readonly pluginLoaded: boolean;\n}\n\n/**\n * What the audit measured, beside what it found. A script tells \"nothing\n * found\" from \"nothing checked\" here: `assertions: \"unavailable\"` with no\n * goal failed is an audit that ran no goal.\n */\nexport interface Measured {\n /** Whether the round's changes were measured from a base; the dependencies are read from them. */\n readonly changes: 'measured' | 'unmeasured';\n /** Whether spec-brief's archive answered. */\n readonly archive: 'asked' | 'unavailable';\n /** Whether spec-guard ran the brief's assertions. */\n readonly assertions: 'run' | 'unavailable';\n readonly goals: { readonly held: number; readonly failed: number };\n readonly premises: { readonly retired: number; readonly holding: number };\n /** Assertions spec-guard could not read, so ran none of. */\n readonly unreadableAssertions: number;\n /** The rulings that allow something, by whether their signatures verify. */\n readonly rulings: { readonly verified: number; readonly unverified: number };\n /** Dependencies the round added, removed or moved, and manifests it changed that could not be read. */\n readonly dependencies: { readonly changed: number; readonly unread: number };\n /** Entries of an install-script policy the round added, removed or turned, in the manifests read for dependencies. */\n readonly installScripts: { readonly changed: number };\n}\n\nexport interface AuditReport {\n readonly findings: readonly Finding[];\n readonly counts: Readonly<Record<Severity, number>>;\n readonly measured: Measured;\n}\n\n/**\n * Each rule the audit and `premises` report, as a reader of SARIF sees it\n * described. A reason spec-brief's archive gives is `archive/<its rule>`.\n */\nexport const RULES: Readonly<Record<string, string>> = {\n unmeasured: \"The round's changes could not be measured from a base.\",\n 'archive-unchecked': \"spec-brief's archive could not be asked what it would refuse.\",\n 'assertions-unchecked': \"The brief's assertions could not be run.\",\n 'assertion-unreadable': 'An assertion in the brief spec-guard cannot read, so nothing it states was run.',\n 'goal-failed': 'A goal the brief asserts does not hold.',\n 'premise-holds': 'A premise still holds after the round meant to change it.',\n 'premise-retired': 'A premise no longer holds, as the round on the brief intends.',\n 'stale-premise': \"A live brief's premise no longer holds: what it was written against has changed.\",\n 'ruling-unverified': 'A ruling whose signature does not verify, which allows nothing.',\n 'ruling-unreadable': 'A row of the rulings table that cannot be read.',\n 'protection-rooted': \"A protection rooted at the filesystem's root, which protects no path of the repository.\",\n 'ruling-path-rooted': \"A signed ruling's path rooted at the filesystem's root, which allows nothing.\",\n 'manifest-name-unread': 'A name in dependencies.manifests that cannot be read, which names no manifest.',\n 'manifest-name-rooted': \"A name in dependencies.manifests rooted at the filesystem's root, where no file of the repository is.\",\n 'manifest-unread': 'A manifest the round changed that cannot be read for dependencies.',\n 'new-dependency': 'A dependency the round added: code no reviewer read.',\n 'dependency-removed': 'A dependency the round removed.',\n 'dependency-changed': 'A dependency the round moved to another version.',\n 'install-script-allowed': 'A package the round allowed to run install scripts: code no reviewer read, run on every install.',\n 'install-script-denied': 'A package the round denied its install scripts.',\n 'install-script-entry-removed': \"An entry the round removed from a manifest's install-script policy.\",\n};\n\nfunction finding(rule: string, severity: Severity, message: string, hint: string, file?: string, line?: number, subject?: string): Finding {\n return {\n rule,\n severity,\n message,\n hint,\n ...(file === undefined ? {} : { file }),\n ...(line === undefined ? {} : { line }),\n ...(subject === undefined ? {} : { subject }),\n };\n}\n\n/**\n * Whether an assertion's section is a premise section. Compared as the\n * rulings section is, and as spec-brief compares section names: a heading\n * written `## 3. **The Defect, Measured:**` is still the premise section, and\n * an assertion under it read as a goal would fail the round for retiring it.\n */\nexport function isPremise(sections: string | null | readonly string[], premises: readonly string[]): boolean {\n const list = sections === null ? [] : typeof sections === 'string' ? [sections] : sections;\n // A premise section's subsections are still premises: `### Before` under\n // `## The Defect, Measured` states what was true before the round.\n return list.some((section) => premises.some((premise) => sameSection(section, premise)));\n}\n\n/**\n * A live brief's premise that no longer holds, as `premises` reports it. On\n * the brief a round is working on - the one the flag, SPEC_BRIEF or the\n * branch names - that is what the round set out to do, and it is reported as\n * the audit reports it; on any other, what the brief was written against has\n * changed under it.\n */\nexport function premiseFinding(\n brief: Pick<BriefRow, 'id' | 'file'>,\n outcome: { readonly description: string; readonly message: string; readonly line: number },\n active: boolean,\n): Finding {\n if (active) {\n return finding(\n 'premise-retired',\n 'note',\n `brief ${brief.id}'s premise no longer holds, as the round on it intends: ${outcome.description}`,\n 'nothing to do: this is the round that changes it, and audit measures it',\n brief.file,\n outcome.line,\n outcome.description,\n );\n }\n return finding(\n 'stale-premise',\n 'error',\n `brief ${brief.id}'s premise no longer holds: ${outcome.description}: ${outcome.message}`,\n 'what the brief was written against has changed; archive the brief if its work is done, or rewrite its premise before a round is run on it',\n brief.file,\n outcome.line,\n outcome.description,\n );\n}\n\n/**\n * A premise spec-guard cannot read, as `premises` reports it: nothing checks\n * whether it still holds, so a stale brief would pass for a live one. A\n * warning, as the audit's is, which fails the run under `--strict`.\n */\nexport function unreadablePremiseFinding(brief: Pick<BriefRow, 'id' | 'file'>, assertion: UnreadableAssertion): Finding {\n return finding(\n 'assertion-unreadable',\n 'warning',\n `spec-guard cannot read a premise of brief ${brief.id}, so whether it still holds is not checked: ${assertion.message}`,\n `fix the directive in ${brief.file}; until spec-guard can read it, nothing checks what it states`,\n brief.file,\n assertion.line,\n assertion.raw.trim(),\n );\n}\n\n/**\n * The next step for a reason the archive gives. A protected file a verified\n * ruling covers is refused only because the archive did not learn of the\n * ruling, and recording a departure in the brief would not fix that.\n */\nfunction archiveHint(reason: ArchiveReason, input: AuditInput): string {\n const ruling = reason.rule === 'protected-file' && reason.path !== undefined ? rulingFor(input.verifiedRulings, reason.path) : undefined;\n if (ruling === undefined) return reason.hint;\n const allowed = `ruling ${ruling.id}, signed by ${ruling.signer}, allows it`;\n return input.pluginLoaded\n ? `${allowed}, and the archive still refused it: check that spec-brief loads the spec-harness installed here and measures from the same base (spec-harness doctor)`\n : `${allowed}, but spec-brief does not load spec-harness's plugin, which is how its archive learns of signed rulings: add \"${PLUGIN}\" to \"plugins\" in its configuration, or run spec-harness init --write`;\n}\n\nexport function audit(input: AuditInput): AuditReport {\n const { brief } = input;\n const out: Finding[] = [];\n const goals = { held: 0, failed: 0 };\n const premises = { retired: 0, holding: 0 };\n\n if (input.unmeasured !== null) {\n out.push(\n finding(\n 'unmeasured',\n 'warning',\n `the round's changes were not measured: ${input.unmeasured}`,\n 'pass --base <ref>, set \"base\" in .spec-harness.json, or audit on the round\\'s branch before it merges',\n brief.file,\n ),\n );\n }\n\n if ('unavailable' in input.archive) {\n out.push(finding('archive-unchecked', 'warning', `what the archive would say is unknown: ${input.archive.unavailable}`, 'install spec-brief, or fix what stopped it', brief.file));\n } else {\n for (const reason of [...input.archive.blocking, ...input.archive.warnings]) {\n out.push(finding(`archive/${reason.rule}`, reason.severity, reason.message, archiveHint(reason, input), reason.file, reason.line, reason.path));\n }\n }\n\n if (!Array.isArray(input.assertions)) {\n const unavailable = (input.assertions as { unavailable: string }).unavailable;\n out.push(finding('assertions-unchecked', 'warning', `the brief's assertions were not run: ${unavailable}`, 'install spec-guard, or fix what stopped it', brief.file));\n } else {\n for (const outcome of input.assertions) {\n const premise = isPremise(outcome.enclosing ?? outcome.section, input.premiseSections);\n if (premise && outcome.ok) {\n premises.holding += 1;\n out.push(\n finding(\n 'premise-holds',\n 'warning',\n `a premise still holds after the round: ${outcome.description}`,\n 'the round set out to change what this premise states; check that it did, or move the assertion out of the premises',\n brief.file,\n outcome.line,\n outcome.description,\n ),\n );\n } else if (premise) {\n premises.retired += 1;\n out.push(finding('premise-retired', 'note', `a premise no longer holds, as the round intended: ${outcome.description}`, 'nothing to do', brief.file, outcome.line, outcome.description));\n } else if (!outcome.ok) {\n goals.failed += 1;\n out.push(finding('goal-failed', 'error', `${outcome.description}: ${outcome.message}`, 'the round is not done until this holds', brief.file, outcome.line, outcome.description));\n } else {\n goals.held += 1;\n }\n }\n }\n // Whether a goal or a premise, an assertion spec-guard cannot read was not\n // run, and dropping it would let the audit pass on what it never checked.\n const unreadable = input.unreadableAssertions ?? [];\n for (const assertion of unreadable) {\n out.push(\n finding(\n 'assertion-unreadable',\n 'warning',\n `spec-guard cannot read an assertion in the brief, so nothing it states was run: ${assertion.message}`,\n `fix the directive in ${brief.file}; until spec-guard can read it, the audit measures nothing it states`,\n brief.file,\n assertion.line,\n assertion.raw.trim(),\n ),\n );\n }\n\n for (const ruling of input.unverifiedRulings) {\n out.push(\n finding(\n 'ruling-unverified',\n 'warning',\n `ruling ${ruling.id} allows nothing: ${ruling.reason}`,\n 'a ruling counts when the commit that last changed its row is signed by a key in the base branch\\'s allowed signers',\n brief.file,\n undefined,\n ruling.id,\n ),\n );\n }\n\n // A protection or a ruling's path a leading `/` roots can be read, and\n // names no path the guard or the archive decides, all repository-relative:\n // the file it was meant to protect was the round's to write, and the file\n // a ruling was meant to allow is still refused.\n for (const pattern of brief.protectedFiles) {\n const root = rooted(pattern);\n if (root === null) continue;\n const what = root === 'whole' ? 'which a leading \"/\" roots at the filesystem\\'s root, so it protects no path' : 'an alternative of which a leading \"/\" roots at the filesystem\\'s root, so that alternative protects no path';\n out.push(\n finding(\n 'protection-rooted',\n 'warning',\n `brief ${brief.id} protects \"${pattern}\", ${what}`,\n `write it without the leading \"/\" in ${brief.file}, since a protection is read from the repository's root, and check what the round changed there`,\n brief.file,\n undefined,\n pattern,\n ),\n );\n }\n for (const ruling of input.verifiedRulings) {\n for (const pattern of ruling.paths) {\n const root = rooted(pattern);\n if (root === null) continue;\n const what = root === 'whole' ? 'which a leading \"/\" roots at the filesystem\\'s root, so it allows nothing' : 'an alternative of which a leading \"/\" roots at the filesystem\\'s root, so that alternative allows nothing';\n out.push(\n finding(\n 'ruling-path-rooted',\n 'warning',\n `ruling ${ruling.id} allows \"${pattern}\", ${what}`,\n 'a ruling\\'s path is read from the repository\\'s root: escalate again, and have the person rule on the path without the leading \"/\"',\n brief.file,\n undefined,\n `${ruling.id} ${pattern}`,\n ),\n );\n }\n }\n\n for (const { name, reason } of input.dependencies.unreadNames ?? []) {\n out.push(\n finding(\n 'manifest-name-unread',\n 'warning',\n `\"dependencies.manifests\" names \"${name}\", which cannot be read: ${reason}; no manifest it names was read`,\n `fix or remove the name in ${CONFIG_FILE}; the other names were read`,\n CONFIG_FILE,\n undefined,\n name,\n ),\n );\n }\n for (const { name, whole } of input.dependencies.rootedNames ?? []) {\n // Every path the audit reads is repository-relative, so a rooted name, or\n // a rooted alternative of one, names no manifest, as silently as a name\n // that cannot be read.\n const what = whole\n ? 'a leading \"/\" roots it at the filesystem\\'s root, where no file of the repository is, so no manifest it names was read'\n : 'a leading \"/\" roots an alternative of it at the filesystem\\'s root, where no file of the repository is, so that alternative names no manifest';\n out.push(\n finding(\n 'manifest-name-rooted',\n 'warning',\n `\"dependencies.manifests\" names \"${name}\": ${what}`,\n `write it without the leading \"/\" in ${CONFIG_FILE}, since a name is matched at any depth; the other names were read`,\n CONFIG_FILE,\n undefined,\n name,\n ),\n );\n }\n for (const file of input.dependencies.unread) {\n out.push(finding('manifest-unread', 'warning', `${file} changed and could not be read for dependencies`, 'check its dependencies by hand; the audit cannot', file));\n }\n for (const change of input.dependencies.changes) {\n const where = `${change.file} (${change.section})`;\n const subject = `${change.name} (${change.section})`;\n if (change.before === null) {\n out.push(\n finding(\n 'new-dependency',\n 'warning',\n `the round added ${change.ecosystem} dependency \"${change.name}\"${change.after === '' ? '' : ` ${change.after}`} in ${where}`,\n 'say in the brief why it is needed, or remove it; a new dependency is code no reviewer read',\n change.file,\n undefined,\n subject,\n ),\n );\n } else if (change.after === null) {\n out.push(finding('dependency-removed', 'note', `the round removed \"${change.name}\" from ${where}`, 'nothing to do', change.file, undefined, subject));\n } else {\n out.push(finding('dependency-changed', 'note', `the round moved \"${change.name}\" from ${change.before} to ${change.after} in ${where}`, 'nothing to do, if the brief meant it', change.file, undefined, subject));\n }\n }\n // An entry is named as written, a version with it: npm pins an approval to\n // the version a person reviewed, so another version is another approval.\n // Only an approval is a grant. A denial and a removed approval stop a\n // script, and a removed denial approves nothing by itself: notes, as a\n // removed dependency is.\n const installScripts = input.dependencies.installScripts ?? [];\n for (const change of installScripts) {\n const where = `${change.file} (${change.section})`;\n const subject = `${change.name} (${change.section})`;\n if (change.after === 'allowed') {\n out.push(\n finding(\n 'install-script-allowed',\n 'warning',\n `the round allowed \"${change.name}\" to run install scripts in ${where}${change.before === 'denied' ? ', which denied it before' : ''}`,\n 'say in the brief why its install scripts must run, or remove the entry; an install script is code no reviewer read, run on every install',\n change.file,\n undefined,\n subject,\n ),\n );\n } else if (change.after === 'denied') {\n const was = change.before === 'allowed' ? ', which allowed them before' : '';\n out.push(finding('install-script-denied', 'note', `the round denied \"${change.name}\" its install scripts in ${where}${was}`, 'nothing to do, if the brief meant it', change.file, undefined, subject));\n } else {\n const was = change.before === 'allowed' ? 'allowed' : 'denied';\n out.push(\n finding('install-script-entry-removed', 'note', `the round removed \"${change.name}\" from ${where}, which ${was} its install scripts`, 'nothing to do, if the brief meant it', change.file, undefined, subject),\n );\n }\n }\n\n const measured: Measured = {\n changes: input.unmeasured === null ? 'measured' : 'unmeasured',\n archive: 'unavailable' in input.archive ? 'unavailable' : 'asked',\n assertions: Array.isArray(input.assertions) ? 'run' : 'unavailable',\n goals,\n premises,\n unreadableAssertions: unreadable.length,\n rulings: { verified: input.verifiedRulings.length, unverified: input.unverifiedRulings.length },\n dependencies: { changed: input.dependencies.changes.length, unread: input.dependencies.unread.length },\n installScripts: { changed: installScripts.length },\n };\n return { findings: out, counts: tally(out), measured };\n}\n\n/** How many findings of each severity. */\nexport function tally(findings: readonly Finding[]): Record<Severity, number> {\n const counts: Record<Severity, number> = { error: 0, warning: 0, note: 0 };\n for (const item of findings) counts[item.severity] += 1;\n return counts;\n}\n\n/**\n * What the audit measured, in one line a person reads above the counts. A\n * brief that declares no assertion says so, and draws no warning: a brief\n * without assertions is a brief, and the archive still measured its round.\n */\nexport function describeMeasured(measured: Measured): string {\n const { goals, premises, rulings, dependencies } = measured;\n const parts: string[] = [];\n if (measured.assertions === 'unavailable') {\n parts.push('assertions: not run');\n } else {\n parts.push(goals.held + goals.failed === 0 ? 'goals: none declared' : `goals: ${goals.held} held, ${goals.failed} failed`);\n parts.push(premises.retired + premises.holding === 0 ? 'premises: none declared' : `premises: ${premises.retired} retired, ${premises.holding} holding`);\n }\n if (measured.unreadableAssertions > 0) parts.push(`unreadable assertions: ${measured.unreadableAssertions}`);\n parts.push(measured.archive === 'asked' ? 'archive: asked' : 'archive: not asked');\n parts.push(rulings.verified + rulings.unverified === 0 ? 'rulings: none' : `rulings: ${rulings.verified} verified, ${rulings.unverified} unverified`);\n parts.push(measured.changes === 'unmeasured' ? 'dependencies: not measured' : `dependencies: ${dependencies.changed} changed, ${dependencies.unread} unread`);\n // Said only when a policy changed, as an assertion that cannot be read is:\n // most repositories have no such policy, and the line is a person's.\n if (measured.installScripts.changed > 0) parts.push(`install scripts: ${measured.installScripts.changed} changed`);\n return `measured: ${parts.join(' · ')}`;\n}\n"]}
|
package/dist/cli.d.ts
CHANGED
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type CliIO } from './workspace.js';
|
|
11
11
|
export { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version, type CliIO } from './workspace.js';
|
|
12
|
-
export declare const HELP = "spec-harness - context, guards, audits and rulings for rounds of agent work\n\nUsage:\n spec-harness <command> [options]\n\nCommands:\n context [brief] What an agent needs to start a round: the brief, its scope,\n rulings, dependencies, the rules in force, the cited documents.\n guard <path...> May the round write these paths? Exit 1 if one is refused.\n hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).\n hook git Answer git's pre-commit hook for the staged files.\n escalate Ask a person to rule: --path <file> (repeatable) --reason <why>\n [--option \"<label>: <cost>\"]... [--recommend <text>].\n --list shows what waits; --show <id> prints one memo.\n rule <id> Record a person's ruling in the brief: --allow or --deny, with\n --note. It counts once committed signed.\n rulings [brief] The brief's rulings, and whether each one's signature verifies.\n audit [brief] Did the round stay inside the lines: what the archive would say,\n the brief's assertions, unverified rulings, new dependencies.\n probe [brief] Run the brief's probes: red at the base, green at the head.\n --at base|head|both, --id <probe>.\n premises Do the live briefs' premises still hold? For CI: exit 1 when one\n is stale, or, with --strict, when spec-guard cannot read one.\n init Configure the spec-* tools to agree. Prints the plan; --write\n applies it; --git-hook adds a pre-commit hook.\n mcp Serve start_round, check_path, request_escalation, audit_round,\n list_rounds and the workflow prompts over MCP on stdio.\n doctor Which siblings are installed, at which versions, which brief is\n named, how Claude Code runs the guard, and git's hook. Exit 1\n when a sibling is older than this release needs, the guard is\n installed twice, or Claude Code is too old to run its hooks.\n\nOptions:\n --brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.\n --base <ref> What the round is measured from. Otherwise \"base\" in\n .spec-harness.json, then the remote's default branch.\n --root <dir> Run from another directory.\n --format <fmt> pretty or json; audit and premises also gitlab, sarif or github.\n --strict Warnings fail the run.\n --help, --version\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n";
|
|
12
|
+
export declare const HELP = "spec-harness - context, guards, audits and rulings for rounds of agent work\n\nUsage:\n spec-harness <command> [options]\n\nCommands:\n context [brief] What an agent needs to start a round: the brief, its scope,\n rulings, dependencies, the rules in force, the cited documents.\n guard <path...> May the round write these paths? Exit 1 if one is refused.\n hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).\n hook git Answer git's pre-commit hook for the staged files.\n escalate Ask a person to rule: --path <file> (repeatable) --reason <why>\n [--option \"<label>: <cost>\"]... [--recommend <text>].\n --list shows what waits; --show <id> prints one memo.\n rule <id> Record a person's ruling in the brief: --allow or --deny, with\n --note. It counts once committed signed.\n rulings [brief] The brief's rulings, and whether each one's signature verifies.\n audit [brief] Did the round stay inside the lines: what the archive would say,\n the brief's assertions, unverified rulings, new dependencies\n and the install scripts it allowed.\n probe [brief] Run the brief's probes: red at the base, green at the head.\n --at base|head|both, --id <probe>.\n premises Do the live briefs' premises still hold? For CI: exit 1 when one\n is stale, or, with --strict, when spec-guard cannot read one.\n init Configure the spec-* tools to agree. Prints the plan; --write\n applies it; --git-hook adds a pre-commit hook.\n mcp Serve start_round, check_path, request_escalation, audit_round,\n list_rounds and the workflow prompts over MCP on stdio.\n doctor Which siblings are installed, at which versions, which brief is\n named, how Claude Code runs the guard, and git's hook. Exit 1\n when a sibling is older than this release needs, the guard is\n installed twice, or Claude Code is too old to run its hooks.\n\nOptions:\n --brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.\n --base <ref> What the round is measured from. Otherwise \"base\" in\n .spec-harness.json, then the remote's default branch.\n --root <dir> Run from another directory.\n --format <fmt> pretty or json; audit and premises also gitlab, sarif or github.\n --strict Warnings fail the run.\n --help, --version\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n";
|
|
13
13
|
export declare function run(argv: readonly string[], io: CliIO): Promise<number>;
|
|
14
14
|
export declare function main(argv?: readonly string[]): Promise<number>;
|