spec-ctx 0.4.0 → 0.5.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/README.md CHANGED
@@ -25,19 +25,84 @@ Three things go wrong.
25
25
  | Each session starts cold. | `status`, `task next`, and the generated handoff say where the work stands. |
26
26
  | The agent ticks `[x]` by hand. | `[x]` appears only after the Verify command of the task exits 0. The run leaves an evidence file. |
27
27
 
28
- A later edit can break a task that passed. `check --strict` names each task whose files changed after its Verify passed.
28
+ A later edit can break a task that passed. `check --strict` names each task whose files changed after its Verify passed. While a change has an open task or no task, it prints a `deferred:` line for the delta items it did not test yet.
29
29
 
30
30
  No LLM call. No daemon. The same files give the same output.
31
31
 
32
+ ## Try it in two minutes
33
+
34
+ Paste this block in a shell. It builds a toy project, then runs the loop on one task. It needs Node 22, `git`, and `npx`.
35
+
36
+ <!-- try:begin -->
37
+ ```bash
38
+ set -e
39
+ mkdir -p spec-ctx-try && cd spec-ctx-try && git init -q
40
+ mkdir -p openspec/specs/user-auth openspec/changes/add-mfa/specs/user-auth src tests
41
+ cat > openspec/specs/user-auth/spec.md <<'MD'
42
+ # user-auth
43
+
44
+ ## Requirements
45
+
46
+ ### Requirement: Login with a password
47
+ The system SHALL let a user log in with a username and a password.
48
+
49
+ #### Scenario: Correct password
50
+ - **WHEN** a user enters a correct password
51
+ - **THEN** the user is logged in
52
+ MD
53
+ cat > openspec/changes/add-mfa/proposal.md <<'MD'
54
+ ## Why
55
+ Passwords alone are weak.
56
+
57
+ ## What Changes
58
+ - Ask for a TOTP code after the password.
59
+ MD
60
+ cat > openspec/changes/add-mfa/specs/user-auth/spec.md <<'MD'
61
+ ## ADDED Requirements
62
+
63
+ ### Requirement: TOTP second factor
64
+ The system SHALL ask for a TOTP code after a correct password.
65
+
66
+ #### Scenario: Correct code
67
+ - **WHEN** a user enters a correct TOTP code
68
+ - **THEN** the user is logged in
69
+ MD
70
+ cat > openspec/changes/add-mfa/tasks.md <<'MD'
71
+ ## 1. Login
72
+
73
+ - [ ] 1.1 Add the TOTP prompt to the login form
74
+ MD
75
+ printf '## Context\nThe login form has one step.\n' > openspec/changes/add-mfa/design.md
76
+ printf 'export const prompt = "password";\n' > src/login.ts
77
+ npx -y spec-ctx init --all
78
+ npx -y spec-ctx adopt
79
+ sed -i.bak 's/^- \[ \] 1.1 Add the TOTP prompt to the login form$/- [ ] 1.1 Add the TOTP prompt to the login form [REQ-UA-002] Verify: `grep -q totp src\/login.ts`/' openspec/changes/add-mfa/tasks.md && rm openspec/changes/add-mfa/tasks.md.bak
80
+ printf '// Covers: REQ-UA-002\n' > tests/login.test.ts
81
+ npx -y spec-ctx status
82
+ npx -y spec-ctx task next
83
+ npx -y spec-ctx task start add-mfa/1.1
84
+ npx -y spec-ctx task done add-mfa/1.1 || echo "refused, as expected"
85
+ printf '// REQ-UA-002\nexport const prompt = "password, then totp";\n' > src/login.ts
86
+ npx -y spec-ctx task done add-mfa/1.1
87
+ npx -y spec-ctx context add-mfa/1.1
88
+ npx -y spec-ctx context src/login.ts
89
+ npx -y spec-ctx handoff write
90
+ ```
91
+ <!-- try:end -->
92
+
93
+ On this toy project the bundle footer shows more than 100 percent of the spec text, because the tree is tiny. On a real repo the share is small. Step 4 of the next section shows 3.6 percent.
94
+
95
+ `status` prints a `next:` line that asks for plan approval with `handoff ask --plan`. The `specctx-run` skill handles that step. This demo skips it.
96
+
32
97
  ## See it work
33
98
 
34
99
  Six commands on a small project. Each clip is one command.
35
100
 
36
101
  ### 1. Give every requirement an ID
37
102
 
38
- OpenSpec names a requirement by its header text. A reword breaks every link to it. `adopt` gives each requirement and scenario an ID that stays. Run it once on a project that has specs. A new project gets IDs from `init --all`.
103
+ OpenSpec names a requirement by its header text. A reword breaks every link to it. `adopt` gives each requirement and scenario an ID that stays.
39
104
 
40
- ![The adopt dry run lists each header and its new ID](docs/demo/clips/1-adopt.svg)
105
+ ![The adopt dry run lists each header and its new ID](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/1-adopt.svg)
41
106
 
42
107
  You see: the dry run prints each header and the ID it gets. Then `adopt` writes them.
43
108
 
@@ -45,54 +110,60 @@ You see: the dry run prints each header and the ID it gets. Then `adopt` writes
45
110
 
46
111
  `status` shows the open change, the task counts, and the next command to run.
47
112
 
48
- ![One change with two tasks done and one open](docs/demo/clips/2-status.svg)
113
+ ![One change with two tasks done and one open](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/2-status.svg)
49
114
 
50
115
  You see: one change, two tasks done, one open, and the hint.
51
116
 
52
117
  ### 3. What is next
53
118
 
54
- `task next` picks the first open task. It prints the requirement IDs the task cites and the Verify command, the shell command that proves the task is done.
119
+ `task next` picks the first open task. It prints the requirements the task cites and its Verify command, the shell command that proves the task is done.
55
120
 
56
- ![Task add-mfa/1.3 with its requirement and Verify command](docs/demo/clips/3-next.svg)
121
+ ![Task add-mfa/1.3 with its requirement and Verify command](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/3-next.svg)
57
122
 
58
123
  You see: task `add-mfa/1.3`, its requirement, and `grep -q totp src/login.ts`.
59
124
 
60
125
  ### 4. Only the spec this task needs
61
126
 
62
- `task start` prints the requirement, its scenarios, the task line, and the test that covers it. Nothing else. The last line is the size of that bundle next to the size of all spec text.
127
+ `task start` prints the context of the task. It holds the task line, the requirements it cites with their scenarios, the code and test lines that tag them, and the decisions. The blocks fit a character budget. The last line compares that size with all spec text.
63
128
 
64
- ![The bundle of one task and its size footer](docs/demo/clips/4-start.svg)
129
+ ![The bundle of one task and its size footer](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/4-start.svg)
65
130
 
66
131
  You see: 711 characters, 3.6 percent of the tree. The agent reads this instead of the tree.
67
132
 
68
133
  ### 5. Done means Verify passed
69
134
 
70
- `task done` runs the Verify command. On exit 1 the box stays open. On exit 0 the box flips to `[x]` and an evidence file records the files that were checked. The agent cannot tick the box by hand.
135
+ `task done` runs the Verify command. On exit 1 the box stays open. On exit 0 the box flips to `[x]` and an evidence file records the files that were checked.
71
136
 
72
- ![A refused done, the edit, and a passing done](docs/demo/clips/5-done.svg)
137
+ ![A refused done, the edit, and a passing done](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/5-done.svg)
73
138
 
74
139
  You see: a red refusal, the edit, then a green done line.
75
140
 
76
141
  ### 6. Hand off to the next session
77
142
 
78
- `handoff write` puts the state in one file: the change, the done tasks, the current task, and the open questions. The next session starts from it. The questions live in `.ctx/questions.json`. `handoff ask` adds one and `handoff resolve <n>` answers it.
143
+ `handoff write` puts the state in one file: the change, the done tasks, the current task, and the open questions. The next session starts from it.
79
144
 
80
- ![The head of the handoff file](docs/demo/clips/6-handoff.svg)
145
+ ![The head of the handoff file](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/6-handoff.svg)
81
146
 
82
147
  You see: the head of `.ctx/handoff.md`.
83
148
 
84
- The transcript of all six clips is in `docs/demo/loop.txt`. The clips run on a copy of `tests/fixtures/no-id-repo` plus the six capabilities in `docs/demo/specs/`. `bash docs/demo/record.sh` records them again.
149
+ The transcript of all six clips is in `docs/demo/loop.txt`. `bash docs/demo/record.sh` records them again.
85
150
 
86
151
  ## Quickstart
87
152
 
153
+ spec-ctx needs Node 22. Run it in a project after `openspec init`.
154
+
88
155
  ```bash
89
156
  npx -y spec-ctx init --all # config, ID templates, rules, and the agent skill
157
+ npx -y spec-ctx adopt # only when your specs have no IDs yet
90
158
  npx -y spec-ctx task next # the next task and the command that proves it
91
159
  npx -y spec-ctx task start <task> # only the spec that this task needs
92
160
  npx -y spec-ctx task done <task> # runs that command. A pass ticks the box.
93
161
  ```
94
162
 
95
- A project with specs but no IDs needs `specctx adopt` first. See [Two paths](#two-paths).
163
+ - `adopt --dry-run` shows the ID that each header gets and writes nothing.
164
+ - `adopt --dry-run` exits 1 when headers need IDs. A script with `set -e` must allow that code.
165
+ - [`docs/getting-started.md`](https://github.com/sonqh/spec-ctx/blob/main/docs/getting-started.md) walks the fresh path with real output.
166
+ - [`docs/reference.md`](https://github.com/sonqh/spec-ctx/blob/main/docs/reference.md) holds the install options, the path for an existing project, every command, and the config file.
96
167
 
97
168
  ## How it finds the context
98
169
 
@@ -105,9 +176,9 @@ graph LR
105
176
  R -->|"Covers:"| X["tests/login.test.ts"]
106
177
  ```
107
178
 
108
- The ID is the stable key. A reword of a header does not change it. The index is the graph of IDs: tasks, requirements, scenarios, decisions, code tags, and test tags. `bundle` walks the edges to the configured depth inside a character budget. A decision record joins the graph through its `Refs:` line. The demo fixture has none, so the graph above shows no decision.
179
+ The ID is the stable key. A reword of a header does not change it. The index is the graph of IDs: tasks, requirements, scenarios, decisions, code tags, and test tags. A decision record joins the graph through its `Refs:` line.
109
180
 
110
- `specctx related` prints each neighbor of an ID and the reason for the link. This is the output for the demo project after its last step:
181
+ `specctx context <task|ID|path|change>` is the one command to run before you edit. It walks the edges inside a character budget and prints one provenance line for each block. When a block does not fit, a `truncated:` line names it. `related` prints each neighbor and the reason for the link:
111
182
 
112
183
  ```text
113
184
  $ specctx related REQ-UA-004
@@ -118,160 +189,17 @@ code src/login.ts:1 active src/login.ts:1 implements REQ-UA-004
118
189
  test tests/login.test.ts:1 active tests/login.test.ts:1 covers REQ-UA-004
119
190
  ```
120
191
 
192
+ The reference has the budget rules and the lookups by file, by change, and by task.
193
+
121
194
  ## The life cycle for agents
122
195
 
123
196
  `init --all` installs one skill, `specctx-run`. The user types one prompt, such as "build this" or "continue". The skill loops until the change reaches the archive or a human must decide.
124
197
 
125
- - It runs `specctx next --json`. The engine reads the files on disk and prints one action: `plan`, `cite`, `start`, `work`, `fix`, `repair`, `gate`, `archive`, `ask`, or `stop`.
126
- - It acts on the action with the exact commands, runs `specctx handoff write`, and repeats.
127
- - It stops twice for a human by default: at the plan, and before archive. A Verify command that fails three times writes a question and stops too.
128
- - `specctx-bootstrap`, `specctx-next`, and `specctx-done` stay for one release as aliases. Each one says: run `specctx-run`.
129
-
130
- The commands the skill runs for one task:
131
-
132
- ```bash
133
- specctx init --all # config, ID templates, rules, and the skill
134
- specctx next --json # the next action, its task, and its command
135
- specctx task start <task> # the bundle of that task and its size
136
- specctx task done <task> # run Verify, write evidence, set the checkbox
137
- specctx gate --strict # check, baseline, linters, openspec validate, length
138
- specctx handoff write # .ctx/handoff.md for the next session
139
- ```
140
-
141
- The rule: the agent never opens `openspec/specs/` by hand. It uses `task start` or `bundle`. It never ticks a checkbox and never runs `/opsx:apply`. The skill is in [`skills/`](skills/). Install it alone with `npx skills add sonqh/spec-ctx` or `specctx init --skills`.
142
-
143
- ## Install
144
-
145
- spec-ctx needs Node 22. Run it in a project after `openspec init`.
146
-
147
- ```bash
148
- npx -y spec-ctx init --all # from npm
149
- npm install -g spec-ctx # puts specctx on your path
150
- npx -y github:sonqh/spec-ctx#<sha> init --all # from git, for an unreleased commit
151
- ```
152
-
153
- The git form builds the package on install with Node only. Pin a commit SHA. A branch name such as `main` also works, but it moves.
154
-
155
- The command writes `spec-ctx.toml`, copies the templates with ID placeholders to `openspec/templates/spec-driven-ids/`, adds the spec-ctx `rules` and `operations` to `openspec/config.yaml`, and installs the `specctx-run` skill and its three aliases into `.claude/skills/` and `.agents/skills/`. The project keeps the stock `schema: spec-driven`. Upstream OpenSpec injects the rules into every artifact instruction. It pins the OpenSpec version it finds in `[gate] openspec`. It adds four lines to `.gitignore`: `.ctx/index.json`, `adopt-report.md`, `.ctx/current.json`, and `.ctx/gate-log.jsonl`. The rest of `.ctx/` is meant for git, `questions.json` included.
156
-
157
- The examples below use `specctx`. With `npx`, write `npx -y spec-ctx` in its place. Each GitHub release also carries a single binary for Linux and macOS.
158
-
159
- ## Two paths
160
-
161
- [`docs/getting-started.md`](docs/getting-started.md) walks the fresh path on upstream OpenSpec with real output, for a developer who knows OpenSpec.
162
-
163
- ### Fresh project
164
-
165
- ```bash
166
- npx -y @fission-ai/openspec@latest init --tools <tool>
167
- npx spec-ctx init --all
168
- specctx gate --strict
169
- ```
170
-
171
- `--tools <tool>` names your agent, such as `claude`, and skips the interactive picker. `--no-animation` keeps logs clean. `init --all` merges the ID rules into an existing `rules` key of `openspec/config.yaml` item by item and prints `merged openspec/config.yaml rules (<n> added)`.
172
-
173
- ### Existing OpenSpec project
174
-
175
- ```bash
176
- npx spec-ctx init --all
177
- specctx adopt --dry-run # the ID that each header gets. Writes nothing.
178
- specctx adopt # writes the IDs into main specs and active deltas
179
- specctx baseline write
180
- specctx gate --strict
181
- ```
182
-
183
- - `adopt` gives an ID to every requirement and scenario header without one, in `openspec/specs/` and in the deltas of active changes. A delta header gets the main ID by its text or through its RENAMED line. A delta header that matches neither keeps its text and goes into `adopt-report.md`.
184
- - `adopt` derives a capability code from each folder name, for example `UA` for `user-auth`. It writes the codes into `spec-ctx.toml`. Rename a code there before the IDs spread.
185
- - `adopt` runs `check` before and after the write. A finding that the write added undoes every file. A second run prints `nothing to adopt`.
186
- - `baseline write` records the debt of the day in `.ctx/baseline.json`. The debt has five kinds. Ticked tasks without evidence. Legacy tasks without a citation or a Verify code span. Delta headers that match no main requirement. Hard lint findings per file and rule on the gate globs. `check` hides that debt and prints `baselined: <n>`. `gate` fails the lint step only when a count rises. New debt still shows.
187
- - `gate` compares `.ctx/baseline.json` with the copy at the merge base of `HEAD` and `[gate] base`, default `origin/main`. A new entry fails the `baseline` step. Commit a grown baseline with a subject that starts with `baseline:` to pass once. In CI, check out with `fetch-depth: 0`, so the merge base exists. Without git or a merge base the step prints `skipped`.
188
- - `[gate] markdown` in `spec-ctx.toml` names the markdown globs that `gate` lints. `init` writes `[]`, so the gate lints nothing until you opt in. The comment in the file names the globs this repository uses.
189
- - Legacy tasks stay in the baseline until an agent adds `[REQ-...]` citations and a `Verify:` code span. `specctx baseline prune` then drops them. No command adds a single entry.
190
-
191
- Both paths have an end to end test on upstream OpenSpec: `tests/paths/fresh.sh` and `tests/paths/existing.sh`. `tests/paths/rules-only.sh` shows that the merged `rules` reach the stock `spec-driven` schema.
192
-
193
- ## Commands
194
-
195
- | Command | Use |
196
- | --- | --- |
197
- | `build` | Write `.ctx/index.json`. Every other command rebuilds a stale index first. |
198
- | `status` | Active changes with task counts, the current task, and open questions. |
199
- | `next` | The next action of the loop from the files on disk: `plan`, `cite`, `start`, `work`, `fix`, `repair`, `gate`, `archive`, `ask`, or `stop`. `--json` prints one object. Flag: `--change`. |
200
- | `query <term...>` | Entries that hold every term. Flags: `--kind`, `--change`, `--limit`, `--all`. |
201
- | `related <ID>` | Linked entries with the reason for each link. Flags: `--depth`, `--all`. |
202
- | `bundle <ID...>` | Spec text of the IDs, their scenarios, decisions, and neighbors. Flags: `--budget`, `--depth`, `--all`. |
203
- | `check` | ID and link integrity. `--strict` adds test coverage and evidence. `retitle-needs-renamed` names a retitled delta header that needs a RENAMED pair before `openspec archive`. |
204
- | `id new`, `id retire` | Allocate and retire IDs. |
205
- | `task next`, `start`, `verify`, `done`, `reopen` | The task state machine. |
206
- | `handoff write`, `ask`, `resolve` | The session handoff and its open questions in `.ctx/questions.json`. |
207
- | `init` | Config file, ID templates and rules, and skills. Flags: `--schema`, `--skills`, `--all`, `--force`. |
208
- | `adopt` | IDs for a project without them. `--dry-run` writes nothing. |
209
- | `baseline write`, `prune` | The debt ratchet in `.ctx/baseline.json`. |
210
- | `gate` | `check`, the baseline against the merge base, both markdown linters, `openspec validate`, and the 500 character length check on active deltas. |
211
- | `lint tells`, `lint ste` | The two markdown linters. Flags: `--baseline`, `--disable`, `--json`, `--selftest`. |
212
- | `help <command>` | The page of one command. `specctx <command> --help` prints the same page. |
213
-
214
- Every command takes `--root <path>` and `--json`. A task ID takes the change key, such as `008/1.1` or `add-mfa/1.1`, or the full folder name. Outside an OpenSpec project every command but `init` exits 2. Exit codes: 0 for success, 1 for findings or a failed Verify command, 2 for a usage error, 3 for an internal error.
215
-
216
- ## Config reference
217
-
218
- `spec-ctx.toml` sits at the project root. Every key is optional.
219
-
220
- ```toml
221
- [scan]
222
- specs = ["openspec/specs/**/*.md"] # main specs
223
- changes = ["openspec/changes/**/*.md"] # active and archived changes
224
- decisions = ["decisions/*.md"] # decision records
225
- code = ["src/**"] # files with code tags
226
- tests = ["tests/**"] # files with Covers tags and scenario IDs
227
- ignore = [] # globs that the scan skips
228
-
229
- [ids]
230
- caps = [] # capability codes. Empty means any code is valid.
231
- task_pattern = '\d+\.\d+' # the task number in a tasks.md checkbox
232
- brackets = false # true: adopt, id new, and init --schema write [REQ-UA-001] in headers. The scan reads both forms.
233
-
234
- [ids.paths]
235
- UA = "user-auth" # capability path of each code. adopt writes the derived ones.
236
-
237
- [bundle]
238
- budget = 8000 # characters
239
- depth = 1
240
-
241
- [verify]
242
- timeout_s = 600
243
-
244
- [gate]
245
- markdown = [] # globs that gate lints. Empty means no lint. This repo opts in.
246
- openspec = "npx -y @fission-ai/openspec@1.15.0" # the command that runs OpenSpec. init pins the version it found.
247
- base = "origin/main" # the ref whose merge base holds the baseline that gate compares with
248
-
249
- [loop]
250
- max_attempts = 3 # Verify failures on one task before task done writes a question
251
- archive = "ask" # "ask" stops before archive for a human. "auto" lets the loop archive.
252
- ```
253
-
254
- With `brackets = true`, `init --schema` installs templates with `[REQ-<CAP>-NNN]` and `[SCN-<CAP>-NNN-N]` in the headers. The merged rules show the same form. Task citations, `Refs:` lines, and tags stay bare. After you change the value, run `specctx init --schema` again. It switches each template and rule that you did not edit. It keeps the ones you edited, unless you add `--force`.
255
-
256
- Two environment variables change the engine:
257
-
258
- | Variable | Effect |
259
- | --- | --- |
260
- | `SPECCTX_OPENSPEC` | The command that `gate` runs for `openspec validate` and `next` runs for `openspec status`. It overrides `[gate] openspec` for one run. At `init` time it is written into the file as it is, and no version detection runs. Tests and offline machines set it. |
261
- | `SPECCTX_BASELINE_SUBJECT` | The commit subject that the `baseline` step of `gate` tests for `baseline:`. It replaces the subject of `HEAD`. Tests set it. |
262
-
263
- ## Markdown linters
264
-
265
- `specctx lint tells` and `specctx lint ste` replace `scripts/tells-lint.py` and `scripts/ste-lint.py` from the ste-humanize skill. They take the same flags and give the same output and exit codes. They need Node only.
198
+ 1. It runs `specctx next --json`. The engine reads the files on disk and prints one action, such as `start`, `fix`, or `archive`.
199
+ 2. It runs the exact commands of that action.
200
+ 3. It runs `specctx handoff write` and repeats.
266
201
 
267
- ```bash
268
- # before
269
- python3 scripts/tells-lint.py --baseline 0 README.md
270
- python3 scripts/ste-lint.py --baseline 0 --disable synonym-rotation README.md
271
- # after
272
- npx spec-ctx lint tells --baseline 0 README.md
273
- npx spec-ctx lint ste --baseline 0 --disable synonym-rotation README.md
274
- ```
202
+ It stops for a human at the plan and before archive. A Verify command that fails three times writes a question and stops too. The agent never opens `openspec/specs/` by hand, never ticks a checkbox, and never runs `/opsx:apply`. `specctx-bootstrap`, `specctx-next`, and `specctx-done` stay for one release as aliases of `specctx-run`. The skill is in [`skills/`](https://github.com/sonqh/spec-ctx/blob/main/skills/).
275
203
 
276
204
  ## GitHub Action
277
205
 
@@ -282,29 +210,7 @@ npx spec-ctx lint ste --baseline 0 --disable synonym-rotation README.md
282
210
  strict: true
283
211
  ```
284
212
 
285
- The action runs `specctx gate`: `check --strict`, both markdown linters, `openspec validate --all --strict`, and the length check.
286
-
287
- ### Rerun
288
-
289
- ```yaml
290
- on: pull_request
291
- permissions:
292
- contents: read
293
- jobs:
294
- rerun:
295
- runs-on: ubuntu-latest
296
- steps:
297
- - uses: actions/checkout@v4
298
- with: { fetch-depth: 0 }
299
- - uses: sonqh/spec-ctx@v0
300
- with:
301
- rerun: true
302
- base: origin/${{ github.base_ref }}
303
- ```
304
-
305
- With `rerun: true` the action runs `specctx gate --rerun` and no other step. It runs the Verify command of each task that the pull request closed and compares the exit code with the evidence. `base` is the ref for the merge base. Without `rerun`, the action ignores `base`.
306
-
307
- On `push`, the default base gives a merge base equal to HEAD, so the rerun compares nothing. It prints one line and passes. Pass `base: ${{ github.event.before }}` on push. A push that creates a branch gives forty zeros. The rerun prints `rerun: base is the zero commit (a new branch), nothing to compare` and passes. After a force push the old commit may be missing from the clone. The rerun then prints `rerun: base <ref> is not in this clone. Fetch it or use fetch-depth: 0` and exits 1.
213
+ The action runs `specctx gate`: `check --strict`, both markdown linters, `openspec validate --all --strict`, and the length check. With `rerun: true` it runs the Verify command of each task that the pull request closed. The reference has the full workflow.
308
214
 
309
215
  A Verify command is code from the pull request. The boundary:
310
216
 
@@ -315,28 +221,44 @@ A Verify command is code from the pull request. The boundary:
315
221
  - For fork pull requests, turn on the repository setting that asks for approval before a workflow runs.
316
222
  - Check out with `fetch-depth: 0`. The merge base needs full history.
317
223
 
224
+ Tests and offline machines set `SPECCTX_OPENSPEC` to the command that runs OpenSpec.
225
+
318
226
  ## Status
319
227
 
320
- - Published: `spec-ctx@0.4.0` on npm, with tags `v0.4.0` and `v0` and binaries on the GitHub release.
321
- - Archived: 001 to 025, 027 to 034, and 036 to 042. No change is open. See `CHANGELOG.md` for each release.
322
- - A version bump on `main` publishes the next release. See `docs/CONTRIBUTING.md`.
323
- - Upstream: the ID scheme is proposed as a comment on [OpenSpec#1967](https://github.com/Fission-AI/OpenSpec/issues/1967#issuecomment-6414314730). The schema bundle is submitted to the community table in [OpenSpec#2157](https://github.com/Fission-AI/OpenSpec/issues/2157). No pull request until a maintainer asks.
228
+ - Published: `spec-ctx@0.5.0` on npm, with binaries on the GitHub release. See `CHANGELOG.md` for each release.
229
+ - Archived: 001 to 025, 027 to 034, 036 to 053, and 055 to 057. No change is open.
230
+ - Upstream: the ID scheme is proposed in [OpenSpec#1967](https://github.com/Fission-AI/OpenSpec/issues/1967#issuecomment-6414314730). The schema bundle is in the community table of [OpenSpec#2157](https://github.com/Fission-AI/OpenSpec/issues/2157).
324
231
 
325
232
  ## Measured
326
233
 
327
- `specctx stats --write` measures this repository after each change. The newest file is `docs/metrics/2026-10-30.md`. Its total row:
234
+ `specctx stats --write` measures this repository after each change. The newest file is `docs/metrics/2026-11-05.md`. Its total row:
328
235
 
329
236
  | change | context_ratio | evidence_ratio | stale_catches | cycle_time_median_s | debt |
330
237
  | --- | --- | --- | --- | --- | --- |
331
- | total | 0.0228 (n=116) | 0.93 (204/220) | 56 (of 106 reopens) | 61 (n=116) | 0 (0 baselined + 0 new) |
238
+ | total | 0.0164 (n=154) | 0.94 (257/273) | 65 (of 122 reopens) | 61 (n=154) | 0 (0 baselined + 0 new) |
332
239
 
333
- - `context_ratio`: the size of one task bundle divided by the size of all spec text. Lower is better.
334
- - `evidence_ratio`: done tasks with a passed Verify command, divided by all done tasks. The rest have a manual note. Higher is better.
335
- - `stale_catches`: reopened tasks that `check --strict` flagged as stale, out of all reopened tasks. A higher share means the check found the drift before a person did.
336
- - `cycle_time_median_s`: the median number of seconds from `task start` to `task done`. Lower is faster.
240
+ - `context_ratio`: the size of one task bundle divided by the size of all spec text. Lower is better. Change 047 on 2026-10-30 made the bundle hold more, so older values do not compare.
241
+ - `evidence_ratio`: done tasks with a passed Verify command, divided by all done tasks. Higher is better.
242
+ - `stale_catches`: reopened tasks that `check --strict` flagged as stale, out of all reopened tasks.
243
+ - `cycle_time_median_s`: the median seconds from `task start` to `task done`.
337
244
  - `debt`: findings that the baseline hides plus new findings. Zero is the goal.
338
245
 
339
- The value in brackets is the count of inputs. A value without inputs reads n/a.
246
+ The value in brackets is the count of inputs.
247
+
248
+ ## Report a bug
249
+
250
+ spec-ctx keeps a small run journal on your machine. It helps the team see where first runs fail. Nothing leaves your machine unless you file a report.
251
+
252
+ - What it holds: one JSON line per command in `.ctx/local/runs.jsonl`. Each line has the time, the version, the command name, the exit code, the run time, and signals such as `init-ok` or `empty-context`.
253
+ - What it never holds: flag values, paths, IDs, change names, spec text, or error messages.
254
+ - Where it is: `.ctx/local/` in the project root. A `.gitignore` of `*` in that folder keeps it out of git. The file rotates at 256 KB.
255
+ - How to turn it off: set `SPECCTX_JOURNAL=0`.
256
+ - How to file a bug:
257
+ 1. Run `specctx report > report.md`.
258
+ 2. Read `report.md`. Remove what you do not want to share.
259
+ 3. Run `gh issue create -R sonqh/spec-ctx --body-file report.md`.
260
+
261
+ `specctx report` only prints. It never runs `gh` and never uses the network.
340
262
 
341
263
  ## Contribute
342
264
 
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { UsageExit } from "../cli/errors.js";
5
5
  import { CONFIG_FILE } from "../config.js";
6
6
  import { loadConfigOrExit } from "../cli/config.js";
7
+ import { runCommand } from "../runcmd.js";
7
8
  import { dumps } from "../util/json.js";
8
9
  import { configText, withCaps } from "./config-write.js";
9
10
  export const REPORT_FILE = "adopt-report.md";
@@ -57,7 +58,7 @@ export function reportText(plan) {
57
58
  }
58
59
  return out.join("\n") + "\n";
59
60
  }
60
- export async function applyAdopt(root, _cfg, plan, io, json) {
61
+ export async function applyAdopt(root, cfg, plan, io, json) {
61
62
  const cfgPath = join(root, CONFIG_FILE);
62
63
  const cfgBefore = existsSync(cfgPath) ? readFileSync(cfgPath, "utf8") : null;
63
64
  const cfgAfter = withCaps(configText(root), plan.caps);
@@ -120,8 +121,11 @@ export async function applyAdopt(root, _cfg, plan, io, json) {
120
121
  // REQ-CLI-007 only the codes in spec-ctx.toml changed: no header, no report
121
122
  if (!report)
122
123
  io.out(`wrote the codes to ${CONFIG_FILE}\nnothing to adopt\n`);
123
- else
124
+ else {
124
125
  io.out(`adopted ${plan.edits.length} header(s) in ${files} file(s). Report: ${REPORT_FILE}\n`);
126
+ // REQ-ADOPT-002 adopt writes no baseline. The next step is a separate commit (044/D4, ADR-006 item 6).
127
+ io.out(`Commit the baseline with a subject that starts with baseline:.\nnext: ${runCommand(cfg.cli.run, "baseline write")}\n`);
128
+ }
125
129
  }
126
130
  return 0;
127
131
  }
@@ -10,5 +10,5 @@ export const ASSETS = {
10
10
  "skills/specctx-bootstrap/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtYm9vdHN0cmFwCmRlc2NyaXB0aW9uOiBTdGFydCBhIHNlc3Npb24gaW4gYW4gT3BlblNwZWMgcHJvamVjdCB0aGF0IHVzZXMgc3BlYy1jdHguIFVzZSBhdCB0aGUgc3RhcnQgb2YgZXZlcnkgc2Vzc2lvbiwgYW5kIG9uICJzdGFydCIsICJib290c3RyYXAiLCBvciAid2hhdCBzaG91bGQgSSB3b3JrIG9uIi4gTG9hZHMgb25seSB0aGUgc3BlYyB0ZXh0IHRoYXQgdGhlIG5leHQgdGFzayBuZWVkcy4KLS0tCgpUaGlzIHNraWxsIGlzIG5vdyBgc3BlY2N0eC1ydW5gLiBSdW4gaXQuCg==",
11
11
  "skills/specctx-done/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtZG9uZQpkZXNjcmlwdGlvbjogQ2xvc2UgYSB0YXNrIGluIGFuIE9wZW5TcGVjIHByb2plY3QgdGhhdCB1c2VzIHNwZWMtY3R4LiBSdW5zIHRoZSBWZXJpZnkgY29tbWFuZCwgd3JpdGVzIGV2aWRlbmNlLCBzZXRzIHRoZSBjaGVja2JveCwgd3JpdGVzIHRoZSBoYW5kb2ZmLCBhbmQgc2hvd3MgdGhlIG1ldHJpY3MuIFVzZSB3aGVuIHRoZSB3b3JrIG9mIGEgdGFzayBpcyBjb21wbGV0ZSwgb3Igb24gImRvbmUiLCAiY2xvc2UgdGhlIHRhc2siLCBhbmQgImVuZCB0aGUgc2Vzc2lvbiIuCi0tLQoKVGhpcyBza2lsbCBpcyBub3cgYHNwZWNjdHgtcnVuYC4gUnVuIGl0Lgo=",
12
12
  "skills/specctx-next/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtbmV4dApkZXNjcmlwdGlvbjogUGljayBhbmQgc3RhcnQgdGhlIG5leHQgdGFzayBpbiB0aGUgbWlkZGxlIG9mIGEgc2Vzc2lvbiBpbiBhbiBPcGVuU3BlYyBwcm9qZWN0IHRoYXQgdXNlcyBzcGVjLWN0eC4gVXNlIGFmdGVyIHlvdSBjbG9zZSBhIHRhc2ssIG9yIG9uICJuZXh0IHRhc2siIGFuZCAid2hhdCBpcyBuZXh0Ii4KLS0tCgpUaGlzIHNraWxsIGlzIG5vdyBgc3BlY2N0eC1ydW5gLiBSdW4gaXQuCg==",
13
- "skills/specctx-run/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtcnVuCmRlc2NyaXB0aW9uOiBSdW4gdGhlIHNwZWMtY3R4IGxvb3AgaW4gYW4gT3BlblNwZWMgcHJvamVjdC4gVXNlIG9uICJydW4iLCAiY29udGludWUiLCAibmV4dCB0YXNrIiwgImJ1aWxkIHRoaXMiLCAiY2Fycnkgb24iLCBhbmQgb24gYSBmaXJzdCBtZXNzYWdlIHRoYXQgZGVzY3JpYmVzIGEgY2hhbmdlIHRvIGJ1aWxkLiBUaGUgZW5naW5lIHBpY2tzIHRoZSBuZXh0IGFjdGlvbi4gVGhlIHNraWxsIGFjdHMgb24gaXQgdW50aWwgdGhlIGNoYW5nZSByZWFjaGVzIHRoZSBhcmNoaXZlIG9yIGEgaHVtYW4gbXVzdCBkZWNpZGUuCi0tLQoKIyBzcGVjY3R4LXJ1bgoKUnVuIGBzcGVjY3R4YCBhcyBgbnB4IHNwZWMtY3R4YCB3aGVuIGl0IGlzIG5vdCBpbnN0YWxsZWQuIGBvcGVuc3BlY2AgaXMgdGhlIGNvbW1hbmQgaW4gYFtnYXRlXSBvcGVuc3BlY2Agb2YgYHNwZWMtY3R4LnRvbWxgLiBXaGVuIHRoYXQga2V5IGlzIGFic2VudCwgYG9wZW5zcGVjYCBpcyBgbnB4IC15IEBmaXNzaW9uLWFpL29wZW5zcGVjQGxhdGVzdGAuCgojIyBUaGUgbG9vcAoKMS4gUnVuIGBzcGVjY3R4IG5leHQgLS1qc29uYC4gUmVhZCBgYWN0aW9uYCwgYGNoYW5nZWAgKGA8Yz5gKSwgYHRhc2tgIChgPHQ+YCksIGBjb21tYW5kYCwgYHJlYXNvbmAsIGFuZCBgaHVtYW5gLgoyLiBBY3Qgb24gYGFjdGlvbmAgYXMgdGhlIHRhYmxlIGJlbG93IHNheXMuCjMuIFJ1biBgc3BlY2N0eCBoYW5kb2ZmIHdyaXRlYC4KNC4gUmVwZWF0IGZyb20gc3RlcCAxIHVudGlsIGBhY3Rpb25gIGlzIGBzdG9wYCBvciBgaHVtYW5gIGlzIHRydWUuCgojIyBBY3Rpb25zCgotIGBwbGFuYDogdGFrZSB0aGUgY2hhbmdlIG5hbWUgZnJvbSB0aGUgcmVxdWVzdCBvZiB0aGUgdXNlciBpbiBrZWJhYiBjYXNlLiBUaGF0IG5hbWUgaXMgYDxjPmAuIEZvbGxvdyB0aGUgdXBzdHJlYW0gYG9wZW5zcGVjLXByb3Bvc2VgIHNraWxsIGZvciBgPGM+YC4gUmVhZCBleGlzdGluZyByZXF1aXJlbWVudHMgd2l0aCBgc3BlY2N0eCBidW5kbGUgPGlkPmAgb3IgYHNwZWNjdHggcXVlcnlgLCBuZXZlciBgb3BlbnNwZWMgc2hvd2AuIFRha2UgZXZlcnkgSUQgZnJvbSBgc3BlY2N0eCBpZCBuZXcgUkVRIDxDQVA+YCBhbmQgYHNwZWNjdHggaWQgbmV3IFNDTiA8Q0FQPiAtLWZvciA8UkVRPmAuIFdoZW4gdGhlIGNoYW5nZSBuZWVkcyBzZXZlcmFsIElEcyBvZiBvbmUga2luZCwgcnVuIGBzcGVjY3R4IGlkIG5ldyAtLWNvdW50IDxuPmAgb25jZS4gR2l2ZSBldmVyeSB0YXNrIGEgYFtSRVEtLi4uXWAgY2l0YXRpb24gYW5kIGEgYFZlcmlmeTpgIGNvZGUgc3Bhbi4gVGhlbiBydW4gYHNwZWNjdHggY2hlY2sgLS1zdHJpY3RgIGFuZCBgc3BlY2N0eCBnYXRlIC0tc3RyaWN0YC4gVGhlIGdhdGUgcnVucyB0aGUgT3BlblNwZWMgdmFsaWRhdGUgYW5kIHRoZSBzcGVjLWxlbmd0aCBjaGVjay4gRml4IGV2ZXJ5IGZpbmRpbmcuIFRoZW4gcnVuIGBzcGVjY3R4IG5leHQgLS1qc29uYCBhbmQgcnVuIHRoZSBjb21tYW5kIGl0IHByaW50cy4gVGhpcyBpcyB0aGUgcGxhbiBzdG9wLiBgbmV4dGAgcmV0dXJucyBgYXNrYCB3aXRoIGBodW1hbjogdHJ1ZWAgdW50aWwgdGhlIHVzZXIgcmVzb2x2ZXMgdGhlIHF1ZXN0aW9uLgotIGBjaXRlYDogcnVuIGBzcGVjY3R4IHF1ZXJ5IC0tY2hhbmdlIDxjPiAtLWtpbmQgcmVxYC4gQWRkIHRoZSBgW1JFUS0uLi5dYCBjaXRhdGlvbiBhbmQgYSBgVmVyaWZ5OmAgY29kZSBzcGFuIHRvIHRoZSB0YXNrIGxpbmUgbmFtZWQgaW4gYHRhc2tgLiBSdW4gYHNwZWNjdHggY2hlY2tgLiBDb250aW51ZS4KLSBgc3RhcnRgOiBydW4gYHNwZWNjdHggdGFzayBzdGFydCA8dD5gLiBSZWFkIHRoZSBidW5kbGUgaXQgcHJpbnRzLiBDb250aW51ZS4KLSBgd29ya2A6IGltcGxlbWVudCB0aGUgdGFzay4gVGFnIGNvZGUgd2l0aCBgLy8gUkVRLS4uLmAuIFRhZyB0ZXN0cyB3aXRoIGAvLyBDb3ZlcnM6IFJFUS0uLi5gLiBUaGVuIHJ1biBgc3BlY2N0eCB0YXNrIGRvbmUgPHQ+YC4KLSBgZml4YDogcmVhZCBgcmVhc29uYC4gSXQgaG9sZHMgdGhlIHRhaWwgb2YgdGhlIGZhaWxlZCBWZXJpZnkgb3V0cHV0LiBGaXggdGhlIGNhdXNlLiBSdW4gYHNwZWNjdHggdGFzayBkb25lIDx0PmAgYWdhaW4uCi0gYHJlcGFpcmA6IHJ1biB0aGUgY29tbWFuZCBpbiB0aGUgYGNvbW1hbmRgIGZpZWxkLiBJdCBydW5zIFZlcmlmeSBhZ2FpbiBmb3IgYSB0aWNrZWQgdGFzayB3aG9zZSBldmlkZW5jZSBpcyBhYnNlbnQgb3Igc3RhbGUuCi0gYGdhdGVgOiBydW4gYHNwZWNjdHggZ2F0ZSAtLXN0cmljdGAuIEZpeCBldmVyeSBmaW5kaW5nLiBGb3IgYSBgc3RhbGUtZXZpZGVuY2VgIGZpbmRpbmcsIGdvIGJhY2sgdG8gc3RlcCAxLiBgbmV4dGAgcmV0dXJucyBgcmVwYWlyYCBhbmQgaXRzIGNvbW1hbmQgcmVwYWlycyBldmVyeSBzdGFsZSB0YXNrLiBSdW4gdGhlIGdhdGUgYWdhaW4gdW50aWwgaXQgZXhpdHMgMC4KLSBgYXJjaGl2ZWA6IHdoZW4gYGh1bWFuYCBpcyB0cnVlLCBydW4gYHNwZWNjdHggaGFuZG9mZiBhc2sgLS1jaGFuZ2UgPGM+ICJhcmNoaXZlIDxjPj8iYCBhbmQgc3RvcC4gVGhpcyBpcyB0aGUgYXJjaGl2ZSBzdG9wLiBgbmV4dGAgcmV0dXJucyBgYXNrYCB1bnRpbCB0aGUgdXNlciByZXNvbHZlcyB0aGUgcXVlc3Rpb24uIFdoZW4gYGh1bWFuYCBpcyBmYWxzZSwgdGhlIHVzZXIgYXBwcm92ZWQgb3IgYFtsb29wXSBhcmNoaXZlYCBpcyBgYXV0b2AuIFRoZW4gcnVuIGVhY2ggc3RlcCB1bmRlciBgb3BlcmF0aW9ucy5hcmNoaXZlLmd1aWRhbmNlYCBpbiBgb3BlbnNwZWMvY29uZmlnLnlhbWxgLiBUaGVuIHJ1biB0aGUgY29tbWFuZCBpbiB0aGUgYGNvbW1hbmRgIGZpZWxkLiBUaGVuIHJ1biBgb3BlbnNwZWMgdmFsaWRhdGUgLS1hbGwgLS1zdHJpY3RgLCBgc3BlY2N0eCBjaGVjayAtLXN0cmljdGAsIGFuZCBgc3BlY2N0eCBoYW5kb2ZmIHdyaXRlYC4KLSBgYXNrYDogcHJpbnQgYHJlYXNvbmAuIEl0IGlzIHRoZSBvcGVuIHF1ZXN0aW9uLiBTdG9wLiBUaGUgdXNlciBhbnN3ZXJzIHdpdGggYHNwZWNjdHggaGFuZG9mZiByZXNvbHZlIDxuPiAiPGFuc3dlcj4iYC4KLSBgc3RvcGA6IHByaW50IGByZWFzb25gLiBTdG9wLgoKIyMgUnVsZXMKCi0gVGhpcyBza2lsbCBvdmVycmlkZXMgdHdvIHN0ZXBzIG9mIHRoZSB1cHN0cmVhbSBgb3BlbnNwZWMtcHJvcG9zZWAgc2tpbGwuIEl0IHJlYWRzIHNwZWNzIHdpdGggYHNwZWNjdHggYnVuZGxlYCBvciBgc3BlY2N0eCBxdWVyeWAsIG5vdCBgb3BlbnNwZWMgc2hvd2AuIEl0IG5ldmVyIHJ1bnMgdGhlIGNsb3NpbmcgYC9vcHN4OmFwcGx5YC4KLSBOZXZlciB0aWNrIGEgY2hlY2tib3ggYnkgaGFuZC4gYHNwZWNjdHggdGFzayBkb25lYCBpcyB0aGUgb25seSB3YXkgYSB0YXNrIGNsb3Nlcy4KLSBOZXZlciB1c2UgYC0tbWFudWFsYCBvbiBhIHRhc2sgd2l0aCBhIFZlcmlmeSBjb21tYW5kLgotIE5ldmVyIGNhbGwgYC9vcHN4OmFwcGx5YC4gVGhpcyBza2lsbCByZXBsYWNlcyBpdC4KLSBUbyB3cml0ZSBhIE1PRElGSUVEIGJsb2NrLCBydW4gYHNwZWNjdHggYnVuZGxlIDxSRVE+IC0tYmxvY2tgLiBJdCBwcmludHMgdGhlIGJsb2NrIGFzIGl0IHNpdHMgaW4gdGhlIHNwZWMgZmlsZS4gQSBNT0RJRklFRCBibG9jayBrZWVwcyBldmVyeSBzY2VuYXJpbyB0aXRsZSB1bmNoYW5nZWQuCi0gYGdhdGVgIGNvdW50cyB0aGUgUmVmcyBsaW5lIGluIHRoZSA1MDAgY2hhcmFjdGVycyBvZiBhIHJlcXVpcmVtZW50LgotIFRoZSBDSEFOR0VMT0cgZW50cnkgZ29lcyB1bmRlciBgIyMgVW5yZWxlYXNlZGAsIG5ldmVyIHVuZGVyIGEgcmVsZWFzZWQgdmVyc2lvbi4KLSBBZnRlciBhbiBlZGl0IHVuZGVyIGBza2lsbHMvYCwgcnVuIGBidW4gcnVuIGJ1aWxkICYmIG5vZGUgZGlzdC9jbGkuanMgaW5pdCAtLXNraWxscyAtLWZvcmNlYCB0byByZWZyZXNoIHRoZSBjb3BpZXMuCi0gTmV2ZXIgcmVhZCBgb3BlbnNwZWMvc3BlY3MvYCBvciBgb3BlbnNwZWMvY2hhbmdlcy8qL3NwZWNzL2AgZGlyZWN0bHkuIFVzZSB0aGUgYnVuZGxlIGZyb20gYHRhc2sgc3RhcnRgIG9yIGBzcGVjY3R4IGJ1bmRsZSA8aWQ+YC4KLSBTdG9wIG9uIGFueSBgYWN0aW9uYCBub3QgaW4gdGhlIGxpc3QgYWJvdmUuIFByaW50IHRoZSBvYmplY3QgYW5kIGFzayB0aGUgdXNlci4KLSBLZWVwIG5vIHN0YXRlIGluIHlvdXIgY29udGV4dC4gQSBuZXcgc2Vzc2lvbiBzdGFydHMgdGhlIHNhbWUgbG9vcCBhZ2FpbiB3aXRoIHN0ZXAgMS4K",
13
+ "skills/specctx-run/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtcnVuCmRlc2NyaXB0aW9uOiBSdW4gdGhlIHNwZWMtY3R4IGxvb3AgaW4gYW4gT3BlblNwZWMgcHJvamVjdC4gVXNlIG9uICJydW4iLCAiY29udGludWUiLCAibmV4dCB0YXNrIiwgImJ1aWxkIHRoaXMiLCAiY2Fycnkgb24iLCBhbmQgb24gYSBmaXJzdCBtZXNzYWdlIHRoYXQgZGVzY3JpYmVzIGEgY2hhbmdlIHRvIGJ1aWxkLiBUaGUgZW5naW5lIHBpY2tzIHRoZSBuZXh0IGFjdGlvbi4gVGhlIHNraWxsIGFjdHMgb24gaXQgdW50aWwgdGhlIGNoYW5nZSByZWFjaGVzIHRoZSBhcmNoaXZlIG9yIGEgaHVtYW4gbXVzdCBkZWNpZGUuCi0tLQoKIyBzcGVjY3R4LXJ1bgoKUnVuIGBzcGVjY3R4YCBhcyBgbnB4IHNwZWMtY3R4YCB3aGVuIGl0IGlzIG5vdCBpbnN0YWxsZWQuIGBvcGVuc3BlY2AgaXMgdGhlIGNvbW1hbmQgaW4gYFtnYXRlXSBvcGVuc3BlY2Agb2YgYHNwZWMtY3R4LnRvbWxgLiBXaGVuIHRoYXQga2V5IGlzIGFic2VudCwgYG9wZW5zcGVjYCBpcyBgbnB4IC15IEBmaXNzaW9uLWFpL29wZW5zcGVjQGxhdGVzdGAuCgojIyBUaGUgbG9vcAoKMS4gUnVuIGBzcGVjY3R4IG5leHQgLS1qc29uYC4gUmVhZCBgYWN0aW9uYCwgYGNoYW5nZWAgKGA8Yz5gKSwgYHRhc2tgIChgPHQ+YCksIGBjb21tYW5kYCwgYHJlYXNvbmAsIGFuZCBgaHVtYW5gLgoyLiBBY3Qgb24gYGFjdGlvbmAgYXMgdGhlIHRhYmxlIGJlbG93IHNheXMuCjMuIFJ1biBgc3BlY2N0eCBoYW5kb2ZmIHdyaXRlYC4KNC4gUmVwZWF0IGZyb20gc3RlcCAxIHVudGlsIGBhY3Rpb25gIGlzIGBzdG9wYCBvciBgaHVtYW5gIGlzIHRydWUuCgojIyBBY3Rpb25zCgotIGBwbGFuYDogdGFrZSB0aGUgY2hhbmdlIG5hbWUgZnJvbSB0aGUgcmVxdWVzdCBvZiB0aGUgdXNlciBpbiBrZWJhYiBjYXNlLiBUaGF0IG5hbWUgaXMgYDxjPmAuIEZvbGxvdyB0aGUgdXBzdHJlYW0gYG9wZW5zcGVjLXByb3Bvc2VgIHNraWxsIGZvciBgPGM+YC4gUmVhZCBleGlzdGluZyByZXF1aXJlbWVudHMgd2l0aCBgc3BlY2N0eCBidW5kbGUgPGlkPmAgb3IgYHNwZWNjdHggcXVlcnlgLCBuZXZlciBgb3BlbnNwZWMgc2hvd2AuIFRha2UgZXZlcnkgSUQgZnJvbSBgc3BlY2N0eCBpZCBuZXcgUkVRIDxDQVA+YCBhbmQgYHNwZWNjdHggaWQgbmV3IFNDTiA8Q0FQPiAtLWZvciA8UkVRPmAuIFdoZW4gdGhlIGNoYW5nZSBuZWVkcyBzZXZlcmFsIElEcyBvZiBvbmUga2luZCwgcnVuIGBzcGVjY3R4IGlkIG5ldyAtLWNvdW50IDxuPmAgb25jZS4gR2l2ZSBldmVyeSB0YXNrIGEgYFtSRVEtLi4uXWAgY2l0YXRpb24gYW5kIGEgYFZlcmlmeTpgIGNvZGUgc3Bhbi4gVGhlbiBydW4gYHNwZWNjdHggY2hlY2sgLS1zdHJpY3RgIGFuZCBgc3BlY2N0eCBnYXRlIC0tc3RyaWN0YC4gVGhlIGdhdGUgcnVucyB0aGUgT3BlblNwZWMgdmFsaWRhdGUgYW5kIHRoZSBzcGVjLWxlbmd0aCBjaGVjay4gRml4IGV2ZXJ5IGZpbmRpbmcuIFRoZW4gcnVuIGBzcGVjY3R4IG5leHQgLS1qc29uYCBhbmQgcnVuIHRoZSBjb21tYW5kIGl0IHByaW50cy4gVGhpcyBpcyB0aGUgcGxhbiBzdG9wLiBgbmV4dGAgcmV0dXJucyBgYXNrYCB3aXRoIGBodW1hbjogdHJ1ZWAgdW50aWwgdGhlIGh1bWFuIGFwcHJvdmVzIHRoZSBxdWVzdGlvbi4gWW91IG5ldmVyIHJlc29sdmUgYSBwbGFuIHF1ZXN0aW9uIHlvdXJzZWxmLgotIGBjaXRlYDogcnVuIGBzcGVjY3R4IHF1ZXJ5IC0tY2hhbmdlIDxjPiAtLWtpbmQgcmVxYC4gQWRkIHRoZSBgW1JFUS0uLi5dYCBjaXRhdGlvbiBhbmQgYSBgVmVyaWZ5OmAgY29kZSBzcGFuIHRvIHRoZSB0YXNrIGxpbmUgbmFtZWQgaW4gYHRhc2tgLiBSdW4gYHNwZWNjdHggY2hlY2tgLiBDb250aW51ZS4KLSBgc3RhcnRgOiBydW4gYHNwZWNjdHggdGFzayBzdGFydCA8dD5gLiBSZWFkIHRoZSBidW5kbGUgaXQgcHJpbnRzLiBDb250aW51ZS4KLSBgd29ya2A6IGltcGxlbWVudCB0aGUgdGFzay4gVGFnIGNvZGUgd2l0aCBgLy8gUkVRLS4uLmAuIFRhZyB0ZXN0cyB3aXRoIGAvLyBDb3ZlcnM6IFJFUS0uLi5gLiBUaGVuIHJ1biBgc3BlY2N0eCB0YXNrIGRvbmUgPHQ+YC4KLSBgZml4YDogcmVhZCBgcmVhc29uYC4gSXQgaG9sZHMgdGhlIHRhaWwgb2YgdGhlIGZhaWxlZCBWZXJpZnkgb3V0cHV0LiBGaXggdGhlIGNhdXNlLiBSdW4gYHNwZWNjdHggdGFzayBkb25lIDx0PmAgYWdhaW4uCi0gYHJlcGFpcmA6IHJ1biB0aGUgY29tbWFuZCBpbiB0aGUgYGNvbW1hbmRgIGZpZWxkLiBJdCBydW5zIFZlcmlmeSBhZ2FpbiBmb3IgYSB0aWNrZWQgdGFzayB3aG9zZSBldmlkZW5jZSBpcyBhYnNlbnQgb3Igc3RhbGUuCi0gYGdhdGVgOiBydW4gYHNwZWNjdHggZ2F0ZSAtLXN0cmljdGAuIEZpeCBldmVyeSBmaW5kaW5nLiBGb3IgYSBgc3RhbGUtZXZpZGVuY2VgIGZpbmRpbmcsIGdvIGJhY2sgdG8gc3RlcCAxLiBgbmV4dGAgcmV0dXJucyBgcmVwYWlyYCBhbmQgaXRzIGNvbW1hbmQgcmVwYWlycyBldmVyeSBzdGFsZSB0YXNrLiBSdW4gdGhlIGdhdGUgYWdhaW4gdW50aWwgaXQgZXhpdHMgMC4KLSBgYXJjaGl2ZWA6IHdoZW4gYGh1bWFuYCBpcyB0cnVlLCBydW4gYHNwZWNjdHggaGFuZG9mZiBhc2sgLS1jaGFuZ2UgPGM+ICJhcmNoaXZlIDxjPj8iYCBhbmQgc3RvcC4gVGhpcyBpcyB0aGUgYXJjaGl2ZSBzdG9wLiBgbmV4dGAgcmV0dXJucyBgYXNrYCB1bnRpbCB0aGUgaHVtYW4gYXBwcm92ZXMgdGhlIHF1ZXN0aW9uLiBZb3UgbmV2ZXIgcmVzb2x2ZSBhbiBhcmNoaXZlIHF1ZXN0aW9uIHlvdXJzZWxmLiBBZnRlciBhIHJlamVjdGlvbiwgYG5leHRgIHJldHVybnMgYGFyY2hpdmVgIHdpdGggYGh1bWFuOiB0cnVlYCBhZ2Fpbi4gV2hlbiBgaHVtYW5gIGlzIGZhbHNlLCB0aGUgdXNlciBhcHByb3ZlZCBvciBgW2xvb3BdIGFyY2hpdmVgIGlzIGBhdXRvYC4gVGhlbiBydW4gZWFjaCBzdGVwIHVuZGVyIGBvcGVyYXRpb25zLmFyY2hpdmUuZ3VpZGFuY2VgIGluIGBvcGVuc3BlYy9jb25maWcueWFtbGAuIFRoZW4gcnVuIHRoZSBjb21tYW5kIGluIHRoZSBgY29tbWFuZGAgZmllbGQuIFRoZW4gcnVuIGBvcGVuc3BlYyB2YWxpZGF0ZSAtLWFsbCAtLXN0cmljdGAsIGBzcGVjY3R4IGNoZWNrIC0tc3RyaWN0YCwgYW5kIGBzcGVjY3R4IGhhbmRvZmYgd3JpdGVgLgotIGBhc2tgOiBwcmludCBgcmVhc29uYC4gSXQgaXMgdGhlIG9wZW4gcXVlc3Rpb24uIFN0b3AuIFlvdSBuZXZlciByZXNvbHZlIGEgcGxhbiBvciBhcmNoaXZlIHF1ZXN0aW9uIHlvdXJzZWxmLiBUaGUgaHVtYW4gZG9lcywgd2l0aCBgc3BlY2N0eCBoYW5kb2ZmIHJlc29sdmUgPG4+IC0tYXBwcm92ZSAiPGFuc3dlcj4iYCBvciBgLS1yZWplY3QgIjxhbnN3ZXI+ImAuIEEgcmVqZWN0aW9uIGtlZXBzIHRoZSBzdG9wLiBBbnkgb3RoZXIgcXVlc3Rpb24gdGFrZXMgYHNwZWNjdHggaGFuZG9mZiByZXNvbHZlIDxuPiAiPGFuc3dlcj4iYC4KLSBgc3RvcGA6IHByaW50IGByZWFzb25gLiBTdG9wLgoKIyMgUnVsZXMKCi0gVGhpcyBza2lsbCBvdmVycmlkZXMgdHdvIHN0ZXBzIG9mIHRoZSB1cHN0cmVhbSBgb3BlbnNwZWMtcHJvcG9zZWAgc2tpbGwuIEl0IHJlYWRzIHNwZWNzIHdpdGggYHNwZWNjdHggYnVuZGxlYCBvciBgc3BlY2N0eCBxdWVyeWAsIG5vdCBgb3BlbnNwZWMgc2hvd2AuIEl0IG5ldmVyIHJ1bnMgdGhlIGNsb3NpbmcgYC9vcHN4OmFwcGx5YC4KLSBOZXZlciB0aWNrIGEgY2hlY2tib3ggYnkgaGFuZC4gYHNwZWNjdHggdGFzayBkb25lYCBpcyB0aGUgb25seSB3YXkgYSB0YXNrIGNsb3Nlcy4KLSBOZXZlciB1c2UgYC0tbWFudWFsYCBvbiBhIHRhc2sgd2l0aCBhIFZlcmlmeSBjb21tYW5kLgotIE5ldmVyIGNhbGwgYC9vcHN4OmFwcGx5YC4gVGhpcyBza2lsbCByZXBsYWNlcyBpdC4KLSBUbyB3cml0ZSBhIE1PRElGSUVEIGJsb2NrLCBydW4gYHNwZWNjdHggYnVuZGxlIDxSRVE+IC0tYmxvY2tgLiBJdCBwcmludHMgdGhlIGJsb2NrIGFzIGl0IHNpdHMgaW4gdGhlIHNwZWMgZmlsZS4gQSBNT0RJRklFRCBibG9jayBrZWVwcyBldmVyeSBzY2VuYXJpbyB0aXRsZSB1bmNoYW5nZWQuCi0gYGdhdGVgIGNvdW50cyB0aGUgUmVmcyBsaW5lIGluIHRoZSA1MDAgY2hhcmFjdGVycyBvZiBhIHJlcXVpcmVtZW50LgotIFRoZSBDSEFOR0VMT0cgZW50cnkgZ29lcyB1bmRlciBgIyMgVW5yZWxlYXNlZGAsIG5ldmVyIHVuZGVyIGEgcmVsZWFzZWQgdmVyc2lvbi4KLSBBZnRlciBhbiBlZGl0IHVuZGVyIGBza2lsbHMvYCwgcnVuIGBidW4gcnVuIGJ1aWxkICYmIG5vZGUgZGlzdC9jbGkuanMgaW5pdCAtLXNraWxscyAtLWZvcmNlYCB0byByZWZyZXNoIHRoZSBjb3BpZXMuCi0gTmV2ZXIgcmVhZCBgb3BlbnNwZWMvc3BlY3MvYCBvciBgb3BlbnNwZWMvY2hhbmdlcy8qL3NwZWNzL2AgZGlyZWN0bHkuIFVzZSB0aGUgYnVuZGxlIGZyb20gYHRhc2sgc3RhcnRgIG9yIGBzcGVjY3R4IGJ1bmRsZSA8aWQ+YC4KLSBTdG9wIG9uIGFueSBgYWN0aW9uYCBub3QgaW4gdGhlIGxpc3QgYWJvdmUuIFByaW50IHRoZSBvYmplY3QgYW5kIGFzayB0aGUgdXNlci4KLSBLZWVwIG5vIHN0YXRlIGluIHlvdXIgY29udGV4dC4gQSBuZXcgc2Vzc2lvbiBzdGFydHMgdGhlIHNhbWUgbG9vcCBhZ2FpbiB3aXRoIHN0ZXAgMS4K",
14
14
  };
@@ -122,6 +122,65 @@ export function weakVerifyFindings(entries) {
122
122
  export function weakVerifyWarnings(entries) {
123
123
  return weakVerifyFindings(entries).map((f) => `weak-verify ${f.id} ${f.file}:${f.line}`);
124
124
  }
125
+ /**
126
+ * REQ-CHK-010 REQ-CHK-008 REQ-CHK-009 one predicate for the delta test rule and the deferred notice (052/D1).
127
+ * A gap is a delta requirement or scenario of an active change that no test names. The rule reports the gaps of
128
+ * a change that has a task and no open task. The notice lists the gaps that an open task or a missing task hides.
129
+ */
130
+ export function deltaGaps(entries, data) {
131
+ const covered = new Set(entries.filter((e) => e.kind === "test").flatMap((e) => e.refs));
132
+ const named = new Set(data.scn_named ?? []);
133
+ // an ID that a REMOVED block of an active change names waits for archive, so it needs no test
134
+ const pending = new Set();
135
+ for (const e of entries) {
136
+ if (e.status !== "removed")
137
+ continue;
138
+ pending.add(e.id);
139
+ for (const sc of e.scenarios ?? [])
140
+ pending.add(sc);
141
+ }
142
+ const out = new Map();
143
+ const slot = (change) => {
144
+ let g = out.get(change);
145
+ if (!g)
146
+ out.set(change, (g = { open: 0, all: 0, gaps: [] }));
147
+ return g;
148
+ };
149
+ for (const e of entries) {
150
+ if (e.kind !== "task" || e.change === "main" || e.file.includes("/archive/"))
151
+ continue;
152
+ const g = slot(e.change);
153
+ g.all++;
154
+ if (e.status !== "done")
155
+ g.open++;
156
+ }
157
+ const seen = new Set();
158
+ for (const e of entries) {
159
+ if ((e.kind !== "scn" && e.kind !== "req") || e.change === "main" || (e.status !== "active" && e.status !== "modified"))
160
+ continue;
161
+ if (pending.has(e.id))
162
+ continue;
163
+ const rule = e.kind === "req" ? (covered.has(e.id) ? null : "uncovered") : named.has(e.id) ? null : "scenario-untested";
164
+ if (!rule)
165
+ continue;
166
+ const gap = [e.file, e.line, rule, e.id];
167
+ const key = e.change + JSON.stringify(gap);
168
+ if (seen.has(key))
169
+ continue;
170
+ seen.add(key);
171
+ slot(e.change).gaps.push(gap);
172
+ }
173
+ return out;
174
+ }
175
+ export function deferredOf(entries, data) {
176
+ const out = [];
177
+ for (const [change, g] of deltaGaps(entries, data)) {
178
+ // 052/D1 to include a change that has no task, the condition is `g.all === 0 || g.open > 0`
179
+ if ((g.all === 0 || g.open > 0) && g.gaps.length)
180
+ out.push({ change, untested: g.gaps.length, open: g.open });
181
+ }
182
+ return out.sort((x, y) => cmpTuple([x.change], [y.change]));
183
+ }
125
184
  /** Sorted, unique findings. The scan never reads .ctx/, so evidence adds no index entry. */
126
185
  export function check(root, cfg, data, strict) {
127
186
  const entries = data.entries;
@@ -190,29 +249,10 @@ export function check(root, cfg, data, strict) {
190
249
  if (e.kind === "scn" && !named.has(e.id))
191
250
  findings.push([e.file, e.line, "scenario-untested", e.id]);
192
251
  }
193
- // REQ-CHK-008 a delta scenario needs a test once its change has no open task (038/D1, D2)
194
- const tasksOf = new Map();
195
- for (const e of entries) {
196
- if (e.kind !== "task" || e.change === "main" || e.file.includes("/archive/"))
197
- continue;
198
- const t = tasksOf.get(e.change) ?? { open: 0, all: 0 };
199
- t.all++;
200
- if (e.status !== "done")
201
- t.open++;
202
- tasksOf.set(e.change, t);
203
- }
204
- for (const e of entries) {
205
- if ((e.kind !== "scn" && e.kind !== "req") || e.change === "main" || (e.status !== "active" && e.status !== "modified"))
206
- continue;
207
- const t = tasksOf.get(e.change);
208
- if (!t || t.open > 0 || pending.has(e.id))
209
- continue;
210
- // REQ-CHK-009 a delta requirement needs a Covers tag on the same terms (039/D2)
211
- if (e.kind === "req" && !covered.has(e.id))
212
- findings.push([e.file, e.line, "uncovered", e.id]);
213
- if (e.kind === "scn" && !named.has(e.id))
214
- findings.push([e.file, e.line, "scenario-untested", e.id]);
215
- }
252
+ // REQ-CHK-008 REQ-CHK-009 a delta item needs a test once its change has a task and no open task (038/D1, 038/D2, 039/D2)
253
+ for (const g of deltaGaps(entries, data).values())
254
+ if (g.open === 0 && g.all > 0)
255
+ findings.push(...g.gaps);
216
256
  findings.push(...evidenceFindings(root, entries, listEvidence(root)));
217
257
  }
218
258
  const uniq = new Map();