Skip to content

Commit fd04863

Browse files
developzoneioclaude
andcommitted
SW-31: ship threshold-calibration machinery, verdict insufficient data
Gate Complexity trips were never recorded as metrics and this repo's real corpus is n=1 closed spec, not the "n=1 risk" the ticket assumed - so this lands the measurement machinery (a new gate:"complexity"/split event inferred from index.md, /sd:status --calibration, judgement-call caveats on the other five thresholds) and an ADR recording the honest verdict at this corpus size, rather than manufacturing a calibrated number from one spec. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent af12c9f commit fd04863

10 files changed

Lines changed: 493 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
## [Unreleased]
1111

1212
### Added
13+
- **Threshold calibration machinery** (SW-31) - `spec-gate` (`hooks/bash/spec-gate.sh`,
14+
`hooks/powershell/spec-gate.ps1`) now infers a completed Gate Complexity split from `index.md`'s
15+
own state (a `FEAT-X` row archived alongside a registered `FEAT-X-<slug>` child) and records it as
16+
a new `gate:"complexity"`/`decision:"split"` metrics event - this repo's first metric for a gate
17+
that is otherwise decided as model-executed prose. Recorded only when the `index.md` edit is
18+
actually allowed through, never on a `block` exit, so the count under-reports on any project that
19+
leaves `index.md` protected (the default). `/sd:status --calibration` (new optional flag,
20+
default invocation's read contract unchanged) reports task/layer/file distributions from spec
21+
artifacts alongside the new split count, framed as `insufficient data (n=<n>)` below the
22+
CONTRIBUTING re-calibration trigger. `docs/adr/0004-threshold-calibration.md` records this run's
23+
verdict (insufficient data on every threshold at the current n=1 corpus) and the deliberately
24+
declined scope (full trip-rate instrumentation); `templates/project-config.template.json` now
25+
marks `retroStaleMinutes`, `debounceMinutes`, and `maxLessons` as unmeasured judgement calls,
26+
matching the existing `maxSizeKb` caveat. CONTRIBUTING names the re-calibration ritual (every 20
27+
closed specs, or each minor release).
28+
1329
- **Install-time version stamp** (SW-29) - `install/install.ps1` / `install/install.sh` now write
1430
`specwright-version.txt` into every installed `<area>/sd/` root, parsed at install time from the
1531
newest dated `## [x.y.z] - <date>` heading in `CHANGELOG.md` - the same source `versionClaims`

CONTRIBUTING.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ per-file-type guidelines, and how to test changes locally.
1010
- [Project goals and non-goals](#project-goals-and-non-goals)
1111
- [Repo layout](#repo-layout)
1212
- [The manifest](#the-manifest)
13+
- [Threshold re-calibration](#threshold-re-calibration)
1314
- [PR process](#pr-process)
1415
- [Per-file-type guidelines](#per-file-type-guidelines)
1516
- [Commands (`commands/*.md`)](#commands-commandsmd)
@@ -155,6 +156,24 @@ notices.
155156

156157
---
157158

159+
## Threshold re-calibration
160+
161+
Every hardcoded threshold in this repo (Gate Complexity's tasks/layers/files limits,
162+
`retroStaleMinutes`, `debounceMinutes`, `maxLessons`, `metrics.maxSizeKb`, the perf gate's noise
163+
floor) started as an estimate, not a measurement - see `docs/adr/0004-threshold-calibration.md`.
164+
Re-run the calibration pass **every 20 closed specs, or at each minor release, whichever comes
165+
first**:
166+
167+
1. Run `/sd:status --calibration` against the accumulated `.specs/index.md` and
168+
`.specs/_metrics/events.jsonl`.
169+
2. For each threshold, record the verdict - keep, change, or insufficient data - in a new ADR under
170+
`docs/adr/`. "Insufficient data" is a legitimate, expected outcome at a thin corpus size; do not
171+
change a threshold without a stated measurement behind it.
172+
3. Where a threshold's rationale in `templates/project-config.template.json` is still a judgement
173+
call (no measured basis), leave its `_..._use` caveat in place rather than removing it.
174+
175+
---
176+
158177
## PR process
159178

160179
1. **Open an issue first** for anything larger than a typo or a small docs fix. State:

commands/status.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Read-only summary of the metrics log and spec registry - what is in progress, where gates fire, where friction concentrates
3-
argument-hint: (none)
3+
argument-hint: (none) or --calibration
44
---
55

66
# /sd:status - metrics and registry summary
@@ -15,7 +15,8 @@ The event schema is documented in `docs/architecture.md` - see "Event log". This
1515
of any read contract: a generation may be discarded on the next roll, so counting it would report a
1616
window that cannot be reproduced.
1717

18-
Takes no argument.
18+
Takes no argument, or the single optional flag `--calibration` (SW-31). Plain `/sd:status` never
19+
reads anything beyond the two files above - `--calibration` is strictly additive: see Phase 3b.
1920

2021
## State machine
2122

@@ -113,6 +114,36 @@ present only lines that have data behind them - omit an empty friction section r
113114
log. Either the work is not happening or metrics started after the spec did; say which is not
114115
determinable from the log.
115116

117+
## Phase 3b - Calibration (`--calibration` only)
118+
119+
Skipped entirely for a plain `/sd:status` invocation - the default read contract (`events.jsonl` +
120+
`index.md`) is unchanged, and Phases 1-3 above already ran unmodified. When `--calibration` is
121+
passed, additionally glob `<spec.dir>/*/02-tasks.md` and `<spec.dir>/*/03-decisions.md` for every
122+
spec directory present, at any lifecycle status - a thin corpus needs every data point it has, not
123+
only `done` ones.
124+
125+
For each spec directory found, from `02-tasks.md`:
126+
- **Tasks**: count `### T<NN>` headings, tolerant of the ``-prefixed variant (`### ✅ T01`) per
127+
`docs/adr/0002-complexity-triage-decomposition.md`'s own warning that a naive `^### T<NN>`
128+
counter silently reads a checked-off task as zero.
129+
- **Layers**: count distinct `Layer:` field values across the spec's task blocks, **excluding
130+
`Tests`/`Config`** per ADR 0002's own exclusion rule, so this count stays comparable to the
131+
threshold it is calibrating. Field values are read with the tolerant grammar in
132+
`skills/sd-atomic-task-format/SKILL.md` (bullet `-`/`*`, `**` optional, colon inside or outside
133+
the emphasis) - do not write a narrower matcher here.
134+
- **Files**: count distinct paths across the spec's `Files:` fields, deduplicated within the spec.
135+
136+
From the already-loaded `events.jsonl` (fixed key order, same exact-substring counting as Phase 1):
137+
- **Complexity-gate splits**: `grep -c '"gate":"complexity","decision":"split"'`. This is an
138+
inferred signal, not a direct observation - see `docs/architecture.md` "Event log" for what it
139+
can and cannot detect (it never sees a bare trip, only a completed split).
140+
141+
**Sample-size framing.** Let *n* be the number of spec directories found. When *n* is below the
142+
CONTRIBUTING re-calibration trigger (20 closed specs), render every computed number - never hide a
143+
count that was actually produced - but prefix the section with `insufficient data (n=<n>)` rather
144+
than presenting the distribution as a basis for changing any threshold. This mirrors the ST002-ST004
145+
degrade convention above: a thin corpus is a labelled state, not a silently-confident report.
146+
116147
## Phase 4 - Render
117148

118149
```
@@ -150,6 +181,19 @@ Extensions seen on code-edit gates: .cs (12), .ts (4)
150181
- <spec>: N code-edit warns ignored
151182
- <spec>: retro stale count reached N
152183
- <spec>: in progress but absent from the log
184+
185+
<only when --calibration was passed:>
186+
187+
## Calibration (n=<n> specs<, insufficient data when n is below the CONTRIBUTING trigger>)
188+
189+
| Metric | Distribution |
190+
|---|---|
191+
| Tasks per spec | ... |
192+
| Layers touched (excl. Tests/Config) | ... |
193+
| Files touched | ... |
194+
195+
Complexity-gate splits observed: <count> (inferred from index.md, not a direct trip observation -
196+
see docs/architecture.md "Event log")
153197
```
154198

155199
Degrade states render the same skeleton with the metrics sections replaced by exactly one labelled
@@ -175,3 +219,6 @@ Each names the reason and the path, so "quiet" is never confused with "clean".
175219
command, or anything from `commands.*` in project-config.
176220
- Do not guess at numbers. Every figure in the output comes from a counting command that was
177221
actually run; if a count could not be produced, say so in place of the number.
222+
- `--calibration` may additionally read `02-tasks.md` / `03-decisions.md` under every spec
223+
directory (Phase 3b). It still never writes anything, still never reads `events.jsonl.1`, and a
224+
plain `/sd:status` invocation's read set is unaffected by the flag's existence.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# ADR 0004: threshold calibration returns "insufficient data" - ships measurement, not new numbers
2+
3+
- Status: proposed
4+
- Date: 2026-08-09
5+
- Source spec: Jira SW-31 (`FEAT-threshold-recalibration`)
6+
- Relates to: SW-13 (ADR 0002, Gate Complexity thresholds); SW-10 / SW-16 (metrics log and
7+
`/sd:status`); SW-30 (second example corpus)
8+
- Supersedes: none
9+
10+
## Context
11+
12+
SW-31 asks for a repeatable calibration pass: turn accumulated `.specs/_metrics/events.jsonl` and
13+
spec-artifact data into evidence for or against each threshold the engine enforces - Gate
14+
Complexity (tasks > 8, layers > 2, files > 8, from ADR 0002), `retroStaleMinutes` (30),
15+
`debounceMinutes` (10), `maxLessons` (3), `metrics.maxSizeKb` (1024), and the perf gate's 5% noise
16+
floor - plus a standing ritual so calibration happens again.
17+
18+
Running that pass against this repo's actual state surfaced two things the ticket's own text
19+
didn't anticipate:
20+
21+
1. **The corpus is thinner than "n=1 risk" implies.** specwright does not dogfood itself - there is
22+
no `.specs/` directory in this repo at all. `examples/fixture-project`, the second corpus SW-31
23+
was waiting on (SW-30), has exactly **one** closed spec (`FEAT-todo-priority`) and no
24+
`events.jsonl` history. Real accumulated data is n=1, not a "canyon" or a "cluster" - there is
25+
nothing to fit a distribution to yet.
26+
2. **Gate Complexity trips were never recorded.** The event schema
27+
(`docs/architecture.md`, "Event log") only ever emitted `gate: verify|protected|code-edit`.
28+
Gate Complexity (ADR 0002) is decided as model-executed prose inside `/sd:feature` Phase 3 Gate
29+
2 - ADR 0002 itself names this an accepted, unresolved limitation ("no script... exercises the
30+
threshold arithmetic... automatically"). Before this ADR, there was no path to ever answering
31+
"what is the trip rate" from measured data, regardless of corpus size.
32+
33+
Per ADR 0002's own "Scope declined" precedent, this ADR ships the machinery to close gap (2)
34+
**partially** - split detection only, not full trip-rate - and accepts gap (1) as the honest
35+
current state rather than manufacturing evidence from a single spec ("Re-fitting to the same five
36+
specs would launder a guess as data" - SW-31's own note, which applies at n=1 even more directly
37+
than the n=5 case it was written about).
38+
39+
## Decision
40+
41+
1. **New inferred metric: `gate:"complexity"` / `decision:"split"`.** `spec-gate`
42+
(`hooks/bash/spec-gate.sh`, `hooks/powershell/spec-gate.ps1`) now watches every `index.md` edit
43+
for the structural trace a completed Gate Complexity split leaves behind: a `FEAT-X` row newly
44+
transitioning to `archived` alongside any `FEAT-X-<slug>` row already registered (on disk or in
45+
the same pending edit), per `commands/feature.md`'s Face B "approve split" steps. This is
46+
observational only, computed from data the workflow already writes - no change to
47+
`commands/feature.md`'s Gate 2 (a HARD gate) or `agents/spec-architect.md` was made or is
48+
needed. It is emitted **only when the edit is actually allowed through**, never on a `block`
49+
exit - a denied edit never reaches disk, so a detected pattern inside it did not really happen.
50+
Under the default config, `.specs/index.md` is itself listed in `paths.protected`
51+
(`templates/project-config.template.json`), so most direct `index.md` edits are already blocked
52+
before this metric ever gets a chance to fire (`docs/architecture.md`'s existing note on the
53+
`decision` field: "Most direct index edits are blocked by `paths.protected`, so `block` is the
54+
common case"). That is a pre-existing, documented property of `spec-gate`, not something this
55+
ADR introduces or changes - but it does mean the split count will under-count real splits on
56+
any project that leaves `index.md` protected, which is the default. Fixing that tension (how
57+
`/sd:feature`'s own Gate 2 writes are meant to reach a protected `index.md` at all) is out of
58+
scope here; `commands/spec.md` already carries the same open tension for the `done` transition
59+
via its `verifyGate` carve-out.
60+
2. **`/sd:status --calibration`.** A new optional view (`commands/status.md`, Phase 3b) reports
61+
task/layer/file distributions read from spec artifacts, plus the `gate:"complexity"`/`split`
62+
count from `events.jsonl`. The default `/sd:status` invocation's read contract is unchanged.
63+
3. **The five non-Gate-Complexity thresholds are marked as judgement calls**, not measured values,
64+
directly in `templates/project-config.template.json` (`_retroStaleMinutes_use`,
65+
`_debounceMinutes_use`, `_maxLessons_use`, matching the existing `_maxSizeKb_use` caveat). Gate
66+
Complexity is left as-is; ADR 0002 already recorded its measured basis (the SW-13 corpus trace),
67+
which nothing in this ADR revisits or invalidates.
68+
4. **Calibration verdict, this run: insufficient data, for every threshold.** n=1 closed spec
69+
across both corpora, zero real `gate:"complexity"` events (the metric only exists as of this
70+
ADR), zero real `events.jsonl` history outside test fixtures. No threshold changes size on this
71+
run - per SW-31's own acceptance criterion, this is the honest and expected outcome at this
72+
corpus size, not a failure of the calibration pass.
73+
5. **CONTRIBUTING names the re-calibration trigger**: every 20 closed specs, or each minor release,
74+
whichever comes first (SW-31's own proposed cadence). The next run of `/sd:status --calibration`
75+
is the mechanism that answers whether that bar has been met.
76+
77+
## Consequences
78+
79+
**Positive.** Real calibration becomes possible going forward without re-opening this ticket - the
80+
next `/sd:status --calibration` run after real specs accumulate reads live data instead of nothing.
81+
The judgement-call caveats make future readers of `project-config.template.json` unable to mistake
82+
an untuned default for a measured one.
83+
84+
**Negative.** `gate:"complexity"`/`split` cannot detect a bare trip (Face A vs. Face B "no-split"
85+
look identical in `index.md`) - see "Scope declined" below. The split-detection heuristic itself is
86+
best-effort: two unrelated specs that happen to share an id prefix (`FEAT-auth` / `FEAT-auth-v2`)
87+
would misread as parent/child. Acceptable for an observational, non-gate-affecting metric, same
88+
class of limitation the file already accepts elsewhere (Rule 0's bundled-edit limitation).
89+
Recording only on an allowed edit (see Decision 1) means the metric under-counts on any project
90+
that leaves `index.md` protected by default - it will report fewer splits than actually occurred,
91+
never more; a project relying on this signal needs to make the archive-plus-child edit reachable.
92+
93+
**Scope declined.** Full trip-rate instrumentation (recording every Gate 2 resolution, not only
94+
completed splits) is **not** built here. The only way to observe it directly is a marker the
95+
architect or the `/sd:feature` command writes at Gate 2 resolution time - and making a HARD gate's
96+
prose responsible for reliably feeding a metrics pipeline would break the invariant
97+
`docs/architecture.md` states plainly: "`spec-gate` and `subagent-retro` are the hooks that
98+
record." A future spec that wants full trip-rate should design the instrumentation around a
99+
deterministic, hook-observable signal (or accept a model-authored marker as an explicit, separate
100+
trade-off) rather than retrofitting it into this "cheap ticket."
101+
102+
## Follow-up
103+
104+
Re-run this calibration once `.specs/index.md` (or `examples/fixture-project/.specs/index.md`)
105+
records at least 20 closed specs, or at the next minor release - whichever comes first, per
106+
CONTRIBUTING. A future ADR should supersede this one with the first real verdict.

0 commit comments

Comments
 (0)