specrails 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/openspec-apply-change/SKILL.md +156 -0
- package/.claude/skills/openspec-archive-change/SKILL.md +114 -0
- package/.claude/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.claude/skills/openspec-continue-change/SKILL.md +118 -0
- package/.claude/skills/openspec-explore/SKILL.md +290 -0
- package/.claude/skills/openspec-ff-change/SKILL.md +101 -0
- package/.claude/skills/openspec-new-change/SKILL.md +74 -0
- package/.claude/skills/openspec-onboard/SKILL.md +529 -0
- package/.claude/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.claude/skills/openspec-verify-change/SKILL.md +168 -0
- package/README.md +226 -0
- package/VERSION +1 -0
- package/bin/specrails.js +41 -0
- package/commands/setup.md +851 -0
- package/install.sh +488 -0
- package/package.json +34 -0
- package/prompts/analyze-codebase.md +87 -0
- package/prompts/generate-personas.md +61 -0
- package/prompts/infer-conventions.md +72 -0
- package/templates/agents/sr-architect.md +194 -0
- package/templates/agents/sr-backend-developer.md +54 -0
- package/templates/agents/sr-backend-reviewer.md +139 -0
- package/templates/agents/sr-developer.md +146 -0
- package/templates/agents/sr-doc-sync.md +167 -0
- package/templates/agents/sr-frontend-developer.md +48 -0
- package/templates/agents/sr-frontend-reviewer.md +132 -0
- package/templates/agents/sr-product-analyst.md +36 -0
- package/templates/agents/sr-product-manager.md +148 -0
- package/templates/agents/sr-reviewer.md +265 -0
- package/templates/agents/sr-security-reviewer.md +178 -0
- package/templates/agents/sr-test-writer.md +163 -0
- package/templates/claude-md/root.md +50 -0
- package/templates/commands/sr/batch-implement.md +282 -0
- package/templates/commands/sr/compat-check.md +271 -0
- package/templates/commands/sr/health-check.md +396 -0
- package/templates/commands/sr/implement.md +972 -0
- package/templates/commands/sr/product-backlog.md +195 -0
- package/templates/commands/sr/refactor-recommender.md +169 -0
- package/templates/commands/sr/update-product-driven-backlog.md +272 -0
- package/templates/commands/sr/why.md +96 -0
- package/templates/personas/persona.md +43 -0
- package/templates/personas/the-maintainer.md +78 -0
- package/templates/rules/layer.md +8 -0
- package/templates/security/security-exemptions.yaml +20 -0
- package/templates/settings/confidence-config.json +17 -0
- package/templates/settings/settings.json +15 -0
- package/templates/web-manager/README.md +107 -0
- package/templates/web-manager/client/index.html +12 -0
- package/templates/web-manager/client/package-lock.json +1727 -0
- package/templates/web-manager/client/package.json +20 -0
- package/templates/web-manager/client/src/App.tsx +83 -0
- package/templates/web-manager/client/src/components/AgentActivity.tsx +19 -0
- package/templates/web-manager/client/src/components/CommandInput.tsx +81 -0
- package/templates/web-manager/client/src/components/LogStream.tsx +57 -0
- package/templates/web-manager/client/src/components/PipelineSidebar.tsx +65 -0
- package/templates/web-manager/client/src/components/SearchBox.tsx +34 -0
- package/templates/web-manager/client/src/hooks/usePipeline.ts +62 -0
- package/templates/web-manager/client/src/hooks/useWebSocket.ts +59 -0
- package/templates/web-manager/client/src/main.tsx +9 -0
- package/templates/web-manager/client/tsconfig.json +21 -0
- package/templates/web-manager/client/tsconfig.node.json +11 -0
- package/templates/web-manager/client/vite.config.ts +13 -0
- package/templates/web-manager/package-lock.json +3327 -0
- package/templates/web-manager/package.json +30 -0
- package/templates/web-manager/server/hooks.test.ts +196 -0
- package/templates/web-manager/server/hooks.ts +71 -0
- package/templates/web-manager/server/index.test.ts +186 -0
- package/templates/web-manager/server/index.ts +99 -0
- package/templates/web-manager/server/spawner.test.ts +319 -0
- package/templates/web-manager/server/spawner.ts +89 -0
- package/templates/web-manager/server/types.ts +46 -0
- package/templates/web-manager/tsconfig.json +14 -0
- package/templates/web-manager/vitest.config.ts +8 -0
- package/update.sh +877 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "Health Check Dashboard"
|
|
3
|
+
description: "Run a comprehensive codebase health check — tests, linting, coverage, complexity, and dependency audit. Compare with previous runs to detect regressions."
|
|
4
|
+
category: Workflow
|
|
5
|
+
tags: [workflow, health, quality, dashboard]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Run a full health check for **{{PROJECT_NAME}}**: detect available tools, execute each quality check, compare results against the previous run, detect regressions, compute a health grade, and store a snapshot for future comparison.
|
|
9
|
+
|
|
10
|
+
**Input:** $ARGUMENTS — optional flags:
|
|
11
|
+
- `--since <date>` — use the report from this date (ISO format: YYYY-MM-DD) as the comparison baseline instead of the most recent
|
|
12
|
+
- `--only <checks>` — comma-separated subset to run. Valid values: `tests`, `coverage`, `lint`, `complexity`, `deps`, `perf`
|
|
13
|
+
- `--save` — always save the snapshot even when `--only` is used (default: skip save for partial runs)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Phase 0: Argument Parsing
|
|
18
|
+
|
|
19
|
+
Parse `$ARGUMENTS` to set runtime variables.
|
|
20
|
+
|
|
21
|
+
**Variables to set:**
|
|
22
|
+
|
|
23
|
+
- `COMPARE_DATE` — string (ISO date) or empty string. Default: `""` (use most recent report).
|
|
24
|
+
- `CHECKS_FILTER` — array of check names or the string `"all"`. Default: `"all"`.
|
|
25
|
+
- `SAVE_SNAPSHOT` — boolean. Default: `true` when `CHECKS_FILTER="all"`, `false` for partial runs unless `--save` is present.
|
|
26
|
+
|
|
27
|
+
**Parsing rules:**
|
|
28
|
+
|
|
29
|
+
1. Scan `$ARGUMENTS` for `--since <date>`. If found, set `COMPARE_DATE=<date>`. Strip from arguments.
|
|
30
|
+
2. Scan for `--only <checks>`. If found:
|
|
31
|
+
- Split `<checks>` on commas to produce an array.
|
|
32
|
+
- Validate each entry against the allowed set: `tests`, `coverage`, `lint`, `complexity`, `deps`, `perf`.
|
|
33
|
+
- If any unknown value is found: print `Error: unknown check "<value>". Valid checks: tests, coverage, lint, complexity, deps, perf` and stop.
|
|
34
|
+
- Set `CHECKS_FILTER=<validated-array>`.
|
|
35
|
+
- Set `SAVE_SNAPSHOT=false` (partial run — snapshot may be incomplete).
|
|
36
|
+
3. Scan for `--save`. If found, set `SAVE_SNAPSHOT=true` regardless of `CHECKS_FILTER`.
|
|
37
|
+
|
|
38
|
+
**Print active configuration:**
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Running checks: <all | comma-separated list> | Comparing to: <COMPARE_DATE or "latest">
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Phase 1: Toolchain Detection
|
|
47
|
+
|
|
48
|
+
Detect available tools for each check category. Run all detections simultaneously (in parallel). For each category, try tools in the order listed — use the first one found.
|
|
49
|
+
|
|
50
|
+
If `CHECKS_FILTER` is not `"all"`, skip detection for categories not in the filter.
|
|
51
|
+
|
|
52
|
+
For each category, set two variables:
|
|
53
|
+
- `TOOL_<CHECK>` — the tool name or command string (e.g., `"jest"`, `"eslint"`)
|
|
54
|
+
- `TOOL_<CHECK>_AVAILABLE` — boolean (`true` / `false`)
|
|
55
|
+
|
|
56
|
+
**Detection sequences:**
|
|
57
|
+
|
|
58
|
+
- **tests:** Try in order: `jest`, `vitest`, `mocha`, `pytest`, `go test`, `cargo test`, `rspec`, `dotnet test`. If none found, check whether `{{CI_COMMANDS}}` provides a test command — use it as fallback and set `TOOL_TESTS="ci-commands"`.
|
|
59
|
+
- **coverage:** Try in order: `nyc`, `c8`, `pytest-cov`, `coverage` (Python), `go test -cover`, `cargo tarpaulin`, `lcov`.
|
|
60
|
+
- **lint:** Try in order: `eslint`, `pylint`, `flake8`, `ruff`, `golangci-lint`, `rubocop`, `cargo clippy`.
|
|
61
|
+
- **complexity:** Try in order: `lizard`, `radon`, `gocyclo`, `plato`. If none found, set `TOOL_COMPLEXITY_AVAILABLE=false` — complexity will be estimated from linter output if lint ran.
|
|
62
|
+
- **deps:** Try in order: `npm audit`, `pip-audit`, `govulncheck`, `cargo audit`, `bundle audit`.
|
|
63
|
+
- **perf:** Look for a performance entry point at these paths in order:
|
|
64
|
+
1. `scripts/perf.sh`
|
|
65
|
+
2. `scripts/benchmark.sh`
|
|
66
|
+
3. A `"perf"` or `"benchmark"` script key in `package.json`
|
|
67
|
+
4. A `perf` or `benchmark` target in `Makefile`
|
|
68
|
+
|
|
69
|
+
If found, set `TOOL_PERF=<path-or-command>` and `TOOL_PERF_AVAILABLE=true`. Otherwise set `TOOL_PERF_AVAILABLE=false`.
|
|
70
|
+
|
|
71
|
+
**Detection summary table** (print after all probes complete):
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
| Category | Available | Tool |
|
|
75
|
+
|------------|-----------|-------------------|
|
|
76
|
+
| tests | yes/no | <tool or N/A> |
|
|
77
|
+
| coverage | yes/no | <tool or N/A> |
|
|
78
|
+
| lint | yes/no | <tool or N/A> |
|
|
79
|
+
| complexity | yes/no | <tool or N/A> |
|
|
80
|
+
| deps | yes/no | <tool or N/A> |
|
|
81
|
+
| perf | yes/no | <tool or N/A> |
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Phase 2: Load Previous Report
|
|
87
|
+
|
|
88
|
+
Read `.claude/health-history/` to find the comparison baseline.
|
|
89
|
+
|
|
90
|
+
**Variables to set:**
|
|
91
|
+
- `PREV_REPORT_PATH` — absolute file path or `null`
|
|
92
|
+
- `IS_FIRST_RUN` — boolean
|
|
93
|
+
- `PREV_REPORT` — parsed JSON object or `null`
|
|
94
|
+
|
|
95
|
+
**Logic:**
|
|
96
|
+
|
|
97
|
+
1. Check whether `.claude/health-history/` exists and contains `.json` files.
|
|
98
|
+
- If directory is absent or empty: set `IS_FIRST_RUN=true`, `PREV_REPORT_PATH=null`, `PREV_REPORT=null`. Print: `First run — no previous report found. Regression comparison is not available.` Proceed.
|
|
99
|
+
|
|
100
|
+
2. If reports exist and `COMPARE_DATE` is empty: select the most recently modified `.json` file.
|
|
101
|
+
|
|
102
|
+
3. If `COMPARE_DATE` is set: find the report whose filename date component is closest to `COMPARE_DATE` (without exceeding it). If no report matches within 7 days, print: `Warning: no report found near <COMPARE_DATE>. Falling back to most recent.` Then use the most recent.
|
|
103
|
+
|
|
104
|
+
4. Set `IS_FIRST_RUN=false`, `PREV_REPORT_PATH=<path>`, load file content into `PREV_REPORT`.
|
|
105
|
+
|
|
106
|
+
5. Print one line:
|
|
107
|
+
|
|
108
|
+
- First run: `Baseline: first run (no comparison)`
|
|
109
|
+
- Report found: `Comparing to: <YYYY-MM-DD> (<short-sha from filename>)`
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Phase 3: Run Checks
|
|
114
|
+
|
|
115
|
+
Run checks **sequentially** in this order: `tests`, `coverage`, `lint`, `complexity`, `deps`, `perf`. Sequential execution avoids resource contention that would skew timing and coverage metrics.
|
|
116
|
+
|
|
117
|
+
For each check, follow this pattern:
|
|
118
|
+
|
|
119
|
+
**Skip condition:** If `TOOL_<CHECK>_AVAILABLE=false` OR check is excluded by `CHECKS_FILTER`:
|
|
120
|
+
- Set `RESULT_<CHECK> = { status: "skipped", tool: null, metrics: null }`
|
|
121
|
+
- Print `<check>: SKIPPED`
|
|
122
|
+
- Continue to next check.
|
|
123
|
+
|
|
124
|
+
**Run:** Execute the tool with the command shown below. If the tool exits non-zero: set `status: "fail"`, capture the error message, record whatever partial metrics are available, and continue — do NOT abort the command.
|
|
125
|
+
|
|
126
|
+
**Store:** Set `RESULT_<CHECK>` to a structured object with `status`, `tool`, and `metrics` fields.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### Check: tests
|
|
131
|
+
|
|
132
|
+
Run command (first tool that matches):
|
|
133
|
+
|
|
134
|
+
- `jest`: `jest --json 2>/dev/null` — parse JSON stdout for `numPassedTests`, `numFailedTests`, `numPendingTests`, `testResults[].duration`
|
|
135
|
+
- `vitest`: `vitest run --reporter=json 2>/dev/null` — parse JSON for equivalent fields
|
|
136
|
+
- `mocha`: `mocha --reporter json 2>/dev/null` — parse `stats` object
|
|
137
|
+
- `pytest`: `pytest --tb=no -q 2>&1` — extract pass/fail/skip counts from summary line
|
|
138
|
+
- `go test`: `go test ./... -v 2>&1` — count `--- PASS`, `--- FAIL`, `--- SKIP` lines
|
|
139
|
+
- `cargo test`: `cargo test 2>&1` — parse `test result:` summary line
|
|
140
|
+
- `rspec`: `rspec --format json 2>/dev/null` — parse JSON
|
|
141
|
+
- `dotnet test`: `dotnet test --logger "console;verbosity=normal" 2>&1` — extract summary
|
|
142
|
+
- `ci-commands` fallback: run `{{CI_COMMANDS}}` and extract pass/fail counts from output using best-effort parsing
|
|
143
|
+
|
|
144
|
+
**Metrics to extract:** `tests_total`, `tests_passed`, `tests_failed`, `tests_skipped`, `pass_rate` (0.0–100.0), `duration_seconds`.
|
|
145
|
+
|
|
146
|
+
Set `RESULT_TESTS`.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### Check: coverage
|
|
151
|
+
|
|
152
|
+
Run command:
|
|
153
|
+
|
|
154
|
+
- `nyc` / `c8`: `nyc report --reporter=text-summary 2>/dev/null` or `c8 report --reporter=text-summary 2>/dev/null` — parse "Statements" or "Lines" coverage percentage
|
|
155
|
+
- `pytest-cov`: re-run as `pytest --cov --cov-report=term-missing -q 2>&1` or read `.coverage` via `coverage report 2>/dev/null`
|
|
156
|
+
- `coverage` (Python): `coverage report 2>/dev/null` — extract `TOTAL` line percentage
|
|
157
|
+
- `go test -cover`: `go test -cover ./... 2>&1` — extract `coverage: N.N%` from each package, compute mean
|
|
158
|
+
- `cargo tarpaulin`: `cargo tarpaulin --out Stdout 2>/dev/null` — extract coverage percentage
|
|
159
|
+
- `lcov`: `lcov --summary coverage.info 2>/dev/null` — extract lines-found/lines-hit
|
|
160
|
+
|
|
161
|
+
**Metrics to extract:** `coverage_pct` (float), `coverage_type` (`"line"` / `"branch"` / `"statement"`).
|
|
162
|
+
|
|
163
|
+
Set `RESULT_COVERAGE`.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### Check: lint
|
|
168
|
+
|
|
169
|
+
Run command:
|
|
170
|
+
|
|
171
|
+
- `eslint`: `eslint . --format json 2>/dev/null` — count `severity: 2` (errors) and `severity: 1` (warnings) across all results; count files analyzed
|
|
172
|
+
- `pylint`: `pylint --output-format json <src-dir-or-.> 2>/dev/null` — count messages by type (`error`, `warning`)
|
|
173
|
+
- `flake8`: `flake8 --format default . 2>&1` — count lines with `E` prefix (errors) vs `W` prefix (warnings)
|
|
174
|
+
- `ruff`: `ruff check --output-format json . 2>/dev/null` — parse JSON array, count by severity
|
|
175
|
+
- `golangci-lint`: `golangci-lint run --out-format json 2>/dev/null` — count issues by severity
|
|
176
|
+
- `rubocop`: `rubocop --format json 2>/dev/null` — parse offenses by severity
|
|
177
|
+
- `cargo clippy`: `cargo clippy --message-format json 2>/dev/null` — count `"level":"error"` and `"level":"warning"` messages
|
|
178
|
+
|
|
179
|
+
**Metrics to extract:** `lint_errors`, `lint_warnings`, `lint_files_checked`. Compute `lint_score = max(0, 100 - lint_errors * 5 - lint_warnings * 1)`.
|
|
180
|
+
|
|
181
|
+
Set `RESULT_LINT`.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### Check: complexity
|
|
186
|
+
|
|
187
|
+
If `TOOL_COMPLEXITY_AVAILABLE=false`:
|
|
188
|
+
- If `RESULT_LINT` is available (lint ran and has output): set `complexity_source: "estimated"` — use Claude's reasoning to estimate complexity signals from lint output (e.g., complexity-related lint rules fired). Set numeric metrics to `null`.
|
|
189
|
+
- Otherwise: set `complexity_source: "unavailable"`, all metrics `null`, `status: "skipped"`.
|
|
190
|
+
|
|
191
|
+
Run command (if tool available):
|
|
192
|
+
|
|
193
|
+
- `lizard`: `lizard . --csv 2>/dev/null` — parse CSV, compute average CCN, max CCN, count functions with CCN > 10
|
|
194
|
+
- `radon`: `radon cc . -a -j 2>/dev/null` — parse JSON, use `average_complexity` field; count items with complexity > 10
|
|
195
|
+
- `gocyclo`: `gocyclo -over 10 . 2>/dev/null` and `gocyclo -avg . 2>/dev/null` — extract average and functions exceeding threshold
|
|
196
|
+
- `plato`: `plato -r -d /tmp/plato-report . 2>/dev/null && cat /tmp/plato-report/report.json` — extract `summary.average.maintainability`
|
|
197
|
+
|
|
198
|
+
**Metrics to extract:** `avg_cyclomatic_complexity` (float), `max_cyclomatic_complexity` (int), `high_complexity_functions` (int, count with CCN > 10), `complexity_source` (`"measured"` / `"estimated"` / `"unavailable"`).
|
|
199
|
+
|
|
200
|
+
Set `RESULT_COMPLEXITY`. Status is `"measured"` when a tool ran, `"estimated"` when inferred, `"skipped"` when neither is possible.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### Check: deps
|
|
205
|
+
|
|
206
|
+
Run command:
|
|
207
|
+
|
|
208
|
+
- `npm audit`: `npm audit --json 2>/dev/null` — parse `vulnerabilities` object; count by `severity` field
|
|
209
|
+
- `pip-audit`: `pip-audit --format json 2>/dev/null` — parse JSON array; count by `fix_versions` presence and severity
|
|
210
|
+
- `govulncheck`: `govulncheck ./... 2>&1` — extract vulnerability counts from summary; if no JSON flag, use Claude's reasoning on text output
|
|
211
|
+
- `cargo audit`: `cargo audit --json 2>/dev/null` — parse `vulnerabilities.list`, count by `advisory.severity`
|
|
212
|
+
- `bundle audit`: `bundle audit check 2>&1` — parse output for `Insecure Source` and `Unpatched versions`; classify by severity from advisory text
|
|
213
|
+
|
|
214
|
+
**Metrics to extract:** `vuln_critical`, `vuln_high`, `vuln_moderate`, `vuln_low`, `vuln_total`.
|
|
215
|
+
|
|
216
|
+
Set `RESULT_DEPS`.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### Check: perf
|
|
221
|
+
|
|
222
|
+
If `TOOL_PERF_AVAILABLE=false`: set `RESULT_PERF = { status: "skipped", tool: null, metrics: null }`, print `perf: SKIPPED`, continue.
|
|
223
|
+
|
|
224
|
+
Run the detected entry point. After it completes, attempt to parse its stdout for these standard keys:
|
|
225
|
+
- `p50`, `p50_ms`, `median_ms` → `perf_p50_ms`
|
|
226
|
+
- `p95`, `p95_ms` → `perf_p95_ms`
|
|
227
|
+
- `p99`, `p99_ms` → `perf_p99_ms`
|
|
228
|
+
- Any remaining numeric key-value pairs → `perf_custom`
|
|
229
|
+
|
|
230
|
+
If the script output does not contain recognizable keys, set all latency fields to `null` and store the raw output in `perf_custom.raw`.
|
|
231
|
+
|
|
232
|
+
**Metrics to extract:** `perf_p50_ms`, `perf_p95_ms`, `perf_p99_ms`, `perf_custom`.
|
|
233
|
+
|
|
234
|
+
Set `RESULT_PERF`.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
**Phase 3 summary** (print after all checks):
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
tests: <PASS|FAIL|SKIPPED> (<tool>)
|
|
242
|
+
coverage: <PASS|FAIL|SKIPPED> (<tool>)
|
|
243
|
+
lint: <PASS|FAIL|SKIPPED> (<tool>)
|
|
244
|
+
complexity: <MEASURED|ESTIMATED|SKIPPED> (<tool>)
|
|
245
|
+
deps: <PASS|FAIL|SKIPPED> (<tool>)
|
|
246
|
+
perf: <PASS|FAIL|SKIPPED> (<tool>)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Phase 4: Build Health Report
|
|
252
|
+
|
|
253
|
+
Using all `RESULT_<CHECK>` values and `PREV_REPORT` (if `IS_FIRST_RUN=false`), compute the final health report object.
|
|
254
|
+
|
|
255
|
+
### Step 1: Compute per-metric deltas
|
|
256
|
+
|
|
257
|
+
For each numeric metric, compute `delta = current_value - prev_value`. If `IS_FIRST_RUN=true`, set all deltas to `"N/A (first run)"`.
|
|
258
|
+
|
|
259
|
+
Delta notation convention:
|
|
260
|
+
- For metrics where higher is better (pass_rate, coverage_pct, lint_score): positive delta = improvement, negative delta = regression.
|
|
261
|
+
- For metrics where lower is better (lint_errors, vuln_*, high_complexity_functions): positive delta = regression, negative delta = improvement.
|
|
262
|
+
|
|
263
|
+
### Step 2: Detect regressions
|
|
264
|
+
|
|
265
|
+
A regression is triggered when any of the following thresholds is crossed vs. the previous report:
|
|
266
|
+
|
|
267
|
+
| Check | Threshold |
|
|
268
|
+
|-------|-----------|
|
|
269
|
+
| tests | `pass_rate` drops by more than 1% |
|
|
270
|
+
| coverage | `coverage_pct` drops by more than 2 percentage points |
|
|
271
|
+
| lint | `lint_errors` increases vs. previous |
|
|
272
|
+
| lint | `lint_score` drops by more than 5 points |
|
|
273
|
+
| complexity | `high_complexity_functions` increases vs. previous |
|
|
274
|
+
| deps | `vuln_critical` increases vs. previous |
|
|
275
|
+
| deps | `vuln_high` increases vs. previous |
|
|
276
|
+
| perf | `perf_p50_ms` increases by more than 10% vs. previous |
|
|
277
|
+
|
|
278
|
+
If `IS_FIRST_RUN=true`: set `REGRESSIONS=[]` (no regression detection possible on first run).
|
|
279
|
+
|
|
280
|
+
Build `REGRESSIONS` as a list of objects: `{ check, metric, previous, current, delta }`.
|
|
281
|
+
|
|
282
|
+
### Step 3: Assign health grade
|
|
283
|
+
|
|
284
|
+
Evaluate criteria in order from F to A; assign the first grade whose criteria are met:
|
|
285
|
+
|
|
286
|
+
| Grade | Criteria |
|
|
287
|
+
|-------|----------|
|
|
288
|
+
| F | Test suite fails to run (RESULT_TESTS.status = "fail") OR pass_rate < 50% |
|
|
289
|
+
| D | Multiple regressions detected (len(REGRESSIONS) >= 2) OR pass_rate < 80% OR vuln_critical > 2 |
|
|
290
|
+
| C | One regression detected OR pass_rate 80–89% OR vuln_critical > 0 |
|
|
291
|
+
| B | No critical regressions. Any one of: pass_rate 90–94%, OR coverage_pct 70–79%, OR lint_errors 1–5, OR vuln_high <= 2 |
|
|
292
|
+
| A | No regressions. pass_rate >= 95%. coverage_pct >= 80% (if measured). lint_errors == 0 (if measured). vuln_critical == 0 AND vuln_high == 0 (if measured). |
|
|
293
|
+
|
|
294
|
+
When `IS_FIRST_RUN=true`, regressions cannot be detected — base the grade on absolute metric thresholds only (no regression criteria apply).
|
|
295
|
+
|
|
296
|
+
When a check is SKIPPED, omit its metric from grade criteria (do not penalize for unavailable tools).
|
|
297
|
+
|
|
298
|
+
### Step 4: Assemble HEALTH_REPORT
|
|
299
|
+
|
|
300
|
+
Build `HEALTH_REPORT` as a structured object matching the JSON storage schema exactly:
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
HEALTH_REPORT = {
|
|
304
|
+
schema_version: "1",
|
|
305
|
+
project: "{{PROJECT_NAME}}",
|
|
306
|
+
timestamp: <ISO 8601 current datetime>,
|
|
307
|
+
git_sha: <full SHA from `git rev-parse HEAD` or "unknown">,
|
|
308
|
+
git_short_sha: <7-char SHA from `git rev-parse --short HEAD` or "unknown">,
|
|
309
|
+
git_branch: <branch from `git rev-parse --abbrev-ref HEAD` or "unknown">,
|
|
310
|
+
checks: {
|
|
311
|
+
tests: { status, tool, metrics: { tests_total, tests_passed, tests_failed, tests_skipped, pass_rate, duration_seconds } },
|
|
312
|
+
coverage: { status, tool, metrics: { coverage_pct, coverage_type } },
|
|
313
|
+
lint: { status, tool, metrics: { lint_errors, lint_warnings, lint_score, lint_files_checked } },
|
|
314
|
+
complexity: { status, tool, metrics: { avg_cyclomatic_complexity, max_cyclomatic_complexity, high_complexity_functions, complexity_source } },
|
|
315
|
+
deps: { status, tool, metrics: { vuln_critical, vuln_high, vuln_moderate, vuln_low, vuln_total } },
|
|
316
|
+
perf: { status, tool, metrics: { perf_p50_ms, perf_p95_ms, perf_p99_ms, perf_custom } }
|
|
317
|
+
},
|
|
318
|
+
grade: <"A"|"B"|"C"|"D"|"F">,
|
|
319
|
+
regressions: <REGRESSIONS array>,
|
|
320
|
+
comparison_report: <PREV_REPORT_PATH basename or null>
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Phase 5: Display Report and Store Snapshot
|
|
327
|
+
|
|
328
|
+
### Action 1: Display
|
|
329
|
+
|
|
330
|
+
Render the health report to the terminal using Markdown formatting:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
## Codebase Health Report — {{PROJECT_NAME}}
|
|
334
|
+
Date: <ISO date> | Commit: <git_short_sha> | Compared to: <previous report date or "first run">
|
|
335
|
+
|
|
336
|
+
Overall Grade: <A/B/C/D/F> (<one-line summary>)
|
|
337
|
+
|
|
338
|
+
### Test Suite [<PASS/FAIL/SKIPPED>]
|
|
339
|
+
Tests: N passed, N failed, N skipped (N total)
|
|
340
|
+
Pass rate: N% <delta: (+N%) or (-N%) or N/A (first run)>
|
|
341
|
+
Duration: Xs
|
|
342
|
+
|
|
343
|
+
### Code Coverage [<PASS/FAIL/SKIPPED/ESTIMATED>]
|
|
344
|
+
Coverage: N% <delta vs previous>
|
|
345
|
+
Type: line/branch/statement
|
|
346
|
+
|
|
347
|
+
### Linting [<PASS/FAIL/SKIPPED>]
|
|
348
|
+
Score: N/100 <delta vs previous>
|
|
349
|
+
Errors: N Warnings: N
|
|
350
|
+
|
|
351
|
+
### Complexity [<MEASURED/ESTIMATED/SKIPPED>]
|
|
352
|
+
Avg CCN: N Max CCN: N
|
|
353
|
+
High-complexity functions: N (>10 CCN) <delta vs previous>
|
|
354
|
+
|
|
355
|
+
### Dependencies [<PASS/FAIL/SKIPPED>]
|
|
356
|
+
Vulnerabilities: N critical, N high, N moderate, N low
|
|
357
|
+
|
|
358
|
+
### Performance [<PASS/FAIL/SKIPPED>]
|
|
359
|
+
p50: Nms p95: Nms p99: Nms <delta vs previous>
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
Regressions detected: N
|
|
363
|
+
<if N > 0, list each:>
|
|
364
|
+
- <check>: <metric> changed from X to Y (<delta>)
|
|
365
|
+
<if N == 0:>
|
|
366
|
+
No regressions detected.
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
For delta display: wrap positive deltas on error/failure metrics in `(+N)` to indicate regression; wrap negative deltas on pass-rate/coverage in `(-N%)` styled as improvement. For terminal rendering, use plain notation — the sign alone conveys direction.
|
|
370
|
+
|
|
371
|
+
### Action 2: Store snapshot
|
|
372
|
+
|
|
373
|
+
Only store if `SAVE_SNAPSHOT=true`.
|
|
374
|
+
|
|
375
|
+
1. Determine filename: `<YYYY-MM-DD>-<git_short_sha>.json` where the date is today's ISO date. If git is unavailable, use `<YYYY-MM-DD>-unknown.json`.
|
|
376
|
+
2. Create `.claude/health-history/` if it does not exist (idempotent — no error if already present).
|
|
377
|
+
3. Write `HEALTH_REPORT` serialized as JSON to `.claude/health-history/<filename>`.
|
|
378
|
+
4. Print: `Stored: .claude/health-history/<filename>`
|
|
379
|
+
|
|
380
|
+
### Housekeeping notice
|
|
381
|
+
|
|
382
|
+
After writing (or after checking the directory if `SAVE_SNAPSHOT=false`), count `.json` files in `.claude/health-history/`. If count > 30, print:
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
Note: .claude/health-history/ has N reports. Consider pruning old ones with:
|
|
386
|
+
ls -t .claude/health-history/ | tail -n +31 | xargs -I{} rm .claude/health-history/{}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### .gitignore suggestion
|
|
390
|
+
|
|
391
|
+
Check whether `.claude/health-history` appears in `.gitignore` (if `.gitignore` exists). If it does not appear, print:
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
Tip: health history reports are local artifacts. Add to .gitignore:
|
|
395
|
+
echo '.claude/health-history/' >> .gitignore
|
|
396
|
+
```
|