b3030d75a1
9-task TDD plan implementing docs/superpowers/specs/2026-05-16-regression-skill-design.md: run.mjs split into exported pure functions (resolveBinary, parsers, computeVerdict, formatters, CHECKS registry) + main orchestrator; co-located run.test.mjs (node:test — 36 unit tests + unknown-arg subprocess test, ruflo-queen-hook.test.mjs pattern); SKILL.md; functional verification per spec §10. Next: subagent-driven-development or executing-plans. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
975 lines
41 KiB
Markdown
975 lines
41 KiB
Markdown
# /regression Skill Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Build the `/regression` Claude Code skill — one invocation that runs the project regression sweep at two tiers and reports a canonical status line plus a machine GREEN/RED verdict.
|
||
|
||
**Architecture:** A skill directory `.claude/skills/regression/` with three files: `SKILL.md` (the skill doc), `run.mjs` (a Node ESM orchestrator split into exported pure functions + a `main` orchestrator), and `run.test.mjs` (co-located `node:test` tests). The pure layer (parsers, verdict, formatting, platform fork) is unit-tested TDD-style; the I/O layer (`runCheck`, `main`) is subprocess-tested for the argument-error path and covered by functional verification for real tool runs.
|
||
|
||
**Tech Stack:** Node ≥20 ESM (`node:child_process`, `node:fs`, `node:path`, `node:process`, `node:url`), built-in `node:test` + `node:assert/strict`. Zero npm dependencies. Wraps existing npm/composer scripts and the `bin/gitleaks` + `bin/lychee` binaries.
|
||
|
||
**Source spec:** `docs/superpowers/specs/2026-05-16-regression-skill-design.md` (commits `5e6b1b6`, `e822925`).
|
||
|
||
**Conventions for the executing engineer:**
|
||
|
||
- The skill `.mjs` files are plain Node ESM, like `tools/*.mjs`. They are NOT covered by the project's ESLint config (which globs `app/resources/js/**`), so no ESLint gate applies to them.
|
||
- `lefthook` pre-commit on a `.mjs`-only commit runs **only gitleaks** (markdownlint/cspell glob `*.md`; pint/larastan/eslint/squawk/stylelint do not match). The `SKILL.md` commit additionally triggers markdownlint + cspell.
|
||
- Test runner: `node --test .claude/skills/regression/run.test.mjs` (Node ≥20, no deps). Run it from the repo root.
|
||
- The reference pattern for a tested `.mjs` is `tools/ruflo-queen-hook.test.mjs` (exported pure functions unit-tested + `main` subprocess-tested via `execFileSync`).
|
||
- Commit messages end with the trailer `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>`.
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
| File | Responsibility |
|
||
|---|---|
|
||
| `.claude/skills/regression/run.mjs` | Orchestrator. Exports pure functions: `resolveBinary`, `buildHeader`, `parseExit`, `parsePest`, `parseVitest`, `parseViteBuild`, `parseLarastan`, `parseGitleaks`, `parseLychee`, `computeVerdict`, `buildCanonicalLine`, `formatRow`, `verdictLine`, the `CHECKS` registry, and `main`. Internal `runCheck` does the `spawnSync` I/O. Runs `main` only when executed directly (guarded by `import.meta.url`). |
|
||
| `.claude/skills/regression/run.test.mjs` | Co-located `node:test` suite. Unit-tests every pure function on fixture strings; subprocess-tests `main` for the unknown-argument path. |
|
||
| `.claude/skills/regression/SKILL.md` | The skill document — frontmatter (`name`, `description`), "Когда использовать", "Workflow", "Уровни", "Правила инвокации (self-restraint)", "Caveats", "Не использовать когда". |
|
||
|
||
Tasks 1–7 build `run.mjs` + `run.test.mjs` incrementally (one pure-function group per task, TDD). Task 8 writes `SKILL.md`. Task 9 is functional verification.
|
||
|
||
---
|
||
|
||
### Task 1: Scaffold + `resolveBinary` + `buildHeader` + `parseExit`
|
||
|
||
Bootstrap the skill directory and the three smallest pure helpers: platform binary resolution, the output header line, and the exit-code token.
|
||
|
||
**Files:**
|
||
|
||
- Create: `.claude/skills/regression/run.mjs`
|
||
- Create: `.claude/skills/regression/run.test.mjs`
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
Create `.claude/skills/regression/run.test.mjs`:
|
||
|
||
```javascript
|
||
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
resolveBinary, buildHeader, parseExit,
|
||
} from './run.mjs';
|
||
|
||
test('resolveBinary: win32 → bin\\<name>.exe', () => {
|
||
assert.equal(resolveBinary('gitleaks', 'win32'), 'bin\\gitleaks.exe');
|
||
});
|
||
test('resolveBinary: non-win32 → bare name on PATH', () => {
|
||
assert.equal(resolveBinary('lychee', 'linux'), 'lychee');
|
||
assert.equal(resolveBinary('lychee', 'darwin'), 'lychee');
|
||
});
|
||
|
||
test('buildHeader: starts with the tier banner', () => {
|
||
assert.ok(buildHeader('quick').startsWith('─ /regression quick '));
|
||
assert.ok(buildHeader('full').startsWith('─ /regression full '));
|
||
});
|
||
test('buildHeader: is padded with dashes', () => {
|
||
assert.ok(buildHeader('full').length >= 30);
|
||
});
|
||
|
||
test('parseExit: builds "<label> <code>" token', () => {
|
||
assert.equal(parseExit('Pint', 0), 'Pint 0');
|
||
assert.equal(parseExit('ESLint', 1), 'ESLint 1');
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: FAIL — `Cannot find module ... run.mjs` (the module does not exist yet).
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
Create `.claude/skills/regression/run.mjs`:
|
||
|
||
```javascript
|
||
#!/usr/bin/env node
|
||
// .claude/skills/regression/run.mjs
|
||
// Regression sweep orchestrator for the /regression skill.
|
||
// Design: docs/superpowers/specs/2026-05-16-regression-skill-design.md
|
||
import process from 'node:process';
|
||
|
||
// ── pure: platform binary resolution ───────────────────────────────
|
||
export function resolveBinary(name, platform = process.platform) {
|
||
return platform === 'win32' ? `bin\\${name}.exe` : name;
|
||
}
|
||
|
||
// ── pure: output header line ───────────────────────────────────────
|
||
export function buildHeader(tier) {
|
||
const head = `─ /regression ${tier} `;
|
||
return head + '─'.repeat(Math.max(3, 48 - head.length));
|
||
}
|
||
|
||
// ── pure: exit-code token ──────────────────────────────────────────
|
||
export function parseExit(label, code) {
|
||
return `${label} ${code}`;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: PASS — 5 tests pass, 0 fail.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add .claude/skills/regression/run.mjs .claude/skills/regression/run.test.mjs
|
||
git commit -m "feat(regression): skill scaffold + resolveBinary/buildHeader/parseExit
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
(`lefthook` runs only gitleaks on this `.mjs`-only commit — expect "no leaks found".)
|
||
|
||
---
|
||
|
||
### Task 2: Test-count parsers — `parsePest` + `parseVitest`
|
||
|
||
Two parsers that turn raw Pest/Vitest summary output into canonical count tokens.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `.claude/skills/regression/run.mjs`
|
||
- Modify: `.claude/skills/regression/run.test.mjs`
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
Append to `.claude/skills/regression/run.test.mjs` (and add `parsePest, parseVitest` to the import from `./run.mjs`):
|
||
|
||
```javascript
|
||
test('parsePest: passed + skipped, no failures → total derived', () => {
|
||
const out = ' Tests: 3 skipped, 739 passed (2104 assertions)\n Duration: 71.23s';
|
||
assert.equal(parsePest(out), 'Pest 742/739/3sk/0');
|
||
});
|
||
test('parsePest: with failures', () => {
|
||
const out = ' Tests: 2 failed, 1 skipped, 736 passed (2090 assertions)';
|
||
assert.equal(parsePest(out), 'Pest 739/736/1sk/2');
|
||
});
|
||
test('parsePest: passed only → zeros for skipped/failed', () => {
|
||
assert.equal(parsePest(' Tests: 19 passed (44 assertions)'), 'Pest 19/19/0sk/0');
|
||
});
|
||
|
||
test('parseVitest: files + passed + skipped', () => {
|
||
const out = ' Test Files 92 passed (92)\n Tests 774 passed | 3 skipped (777)\n Duration 12.6s';
|
||
assert.equal(parseVitest(out), 'Vitest 92f/774/3sk/0');
|
||
});
|
||
test('parseVitest: with failures, does not confuse "Test Files" with "Tests"', () => {
|
||
const out = ' Test Files 2 failed | 90 passed (92)\n Tests 5 failed | 769 passed (774)';
|
||
assert.equal(parseVitest(out), 'Vitest 90f/769/0sk/5');
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: FAIL — `parsePest is not a function` / import error for the new names.
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
Add to `.claude/skills/regression/run.mjs` after `parseExit`:
|
||
|
||
```javascript
|
||
// ── pure: test-count parsers ───────────────────────────────────────
|
||
export function parsePest(stdout) {
|
||
const passed = Number(stdout.match(/(\d+)\s+passed/)?.[1] ?? 0);
|
||
const skipped = Number(stdout.match(/(\d+)\s+skipped/)?.[1] ?? 0);
|
||
const failed = Number(stdout.match(/(\d+)\s+failed/)?.[1] ?? 0);
|
||
return `Pest ${passed + skipped + failed}/${passed}/${skipped}sk/${failed}`;
|
||
}
|
||
|
||
export function parseVitest(stdout) {
|
||
const files = Number(stdout.match(/Test Files\s+(\d+)\s+passed/)?.[1] ?? 0);
|
||
const line = stdout.match(/^\s*Tests\s+.+$/m)?.[0] ?? '';
|
||
const passed = Number(line.match(/(\d+)\s+passed/)?.[1] ?? 0);
|
||
const skipped = Number(line.match(/(\d+)\s+skipped/)?.[1] ?? 0);
|
||
const failed = Number(line.match(/(\d+)\s+failed/)?.[1] ?? 0);
|
||
return `Vitest ${files}f/${passed}/${skipped}sk/${failed}`;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: PASS — 10 tests pass total (5 from Task 1 + 5 new).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add .claude/skills/regression/run.mjs .claude/skills/regression/run.test.mjs
|
||
git commit -m "feat(regression): Pest + Vitest count parsers
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Content parsers — `parseViteBuild` + `parseLarastan` + `parseGitleaks` + `parseLychee`
|
||
|
||
The remaining four parsers: Vite build time, Larastan error count, gitleaks leaks/commits, lychee OK/errors.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `.claude/skills/regression/run.mjs`
|
||
- Modify: `.claude/skills/regression/run.test.mjs`
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
Append to `.claude/skills/regression/run.test.mjs` (add `parseViteBuild, parseLarastan, parseGitleaks, parseLychee` to the import):
|
||
|
||
```javascript
|
||
test('parseViteBuild: extracts build time', () => {
|
||
assert.equal(parseViteBuild('✓ 312 modules transformed.\n✓ built in 2.03s'), 'Vite build 2.03s');
|
||
});
|
||
test('parseViteBuild: no match → "?"', () => {
|
||
assert.equal(parseViteBuild('build crashed'), 'Vite build ?s');
|
||
});
|
||
|
||
test('parseLarastan: clean → 0', () => {
|
||
assert.equal(parseLarastan(' [OK] No errors'), 'Larastan 0');
|
||
});
|
||
test('parseLarastan: counts errors', () => {
|
||
assert.equal(parseLarastan(' [ERROR] Found 2 errors'), 'Larastan 2');
|
||
});
|
||
|
||
test('parseGitleaks: clean → 0 leaks', () => {
|
||
const out = 'INF 442 commits scanned.\nINF no leaks found';
|
||
assert.equal(parseGitleaks(out, 0), 'gitleaks 0/442');
|
||
});
|
||
test('parseGitleaks: leaks found (non-zero exit)', () => {
|
||
const out = 'INF 442 commits scanned.\nWRN 3 leaks found';
|
||
assert.equal(parseGitleaks(out, 1), 'gitleaks 3/442');
|
||
});
|
||
|
||
test('parseLychee: OK + errors', () => {
|
||
const out = '🔍 325 Total (in 9s)\n✅ 325 OK\n🚫 0 Errors';
|
||
assert.equal(parseLychee(out), 'lychee 325/0');
|
||
});
|
||
test('parseLychee: with broken links', () => {
|
||
const out = '🔍 327 Total\n✅ 325 OK\n🚫 2 Errors';
|
||
assert.equal(parseLychee(out), 'lychee 325/2');
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: FAIL — import error / `parseViteBuild is not a function`.
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
Add to `.claude/skills/regression/run.mjs` after `parseVitest`:
|
||
|
||
```javascript
|
||
// ── pure: content parsers ──────────────────────────────────────────
|
||
export function parseViteBuild(stdout) {
|
||
const m = stdout.match(/built in ([\d.]+)\s*s/i);
|
||
return `Vite build ${m ? m[1] : '?'}s`;
|
||
}
|
||
|
||
export function parseLarastan(stdout) {
|
||
const m = stdout.match(/Found (\d+) error/i);
|
||
return `Larastan ${m ? m[1] : 0}`;
|
||
}
|
||
|
||
export function parseGitleaks(stdout, code) {
|
||
const commits = stdout.match(/(\d+)\s+commits?\s+scanned/i)?.[1] ?? '?';
|
||
const leaks = code === 0
|
||
? '0'
|
||
: (stdout.match(/(\d+)\s+leaks?\s+found/i)?.[1]
|
||
?? stdout.match(/leaks?\s+found:?\s*(\d+)/i)?.[1]
|
||
?? '≥1');
|
||
return `gitleaks ${leaks}/${commits}`;
|
||
}
|
||
|
||
export function parseLychee(stdout) {
|
||
const ok = stdout.match(/(\d+)\s+OK/)?.[1] ?? '?';
|
||
const errors = stdout.match(/(\d+)\s+Errors?/i)?.[1] ?? '0';
|
||
return `lychee ${ok}/${errors}`;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: PASS — 18 tests pass total.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add .claude/skills/regression/run.mjs .claude/skills/regression/run.test.mjs
|
||
git commit -m "feat(regression): Vite build / Larastan / gitleaks / lychee parsers
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Verdict logic — `computeVerdict`
|
||
|
||
Turn an array of check results into a GREEN / RED / RED-INCOMPLETE verdict and an exit code.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `.claude/skills/regression/run.mjs`
|
||
- Modify: `.claude/skills/regression/run.test.mjs`
|
||
|
||
A result object has the shape `{ id, label, code, skipped, ms, token }`. `computeVerdict` reads only `label`, `code`, `skipped`.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
Append to `.claude/skills/regression/run.test.mjs` (add `computeVerdict` to the import):
|
||
|
||
```javascript
|
||
test('computeVerdict: all exit 0 → GREEN, exit code 0', () => {
|
||
const v = computeVerdict([
|
||
{ label: 'Pint', code: 0, skipped: false },
|
||
{ label: 'ESLint', code: 0, skipped: false },
|
||
]);
|
||
assert.equal(v.verdict, 'GREEN');
|
||
assert.equal(v.exitCode, 0);
|
||
assert.deepEqual(v.failed, []);
|
||
});
|
||
test('computeVerdict: one non-zero exit → RED, exit code 1', () => {
|
||
const v = computeVerdict([
|
||
{ label: 'Pint', code: 0, skipped: false },
|
||
{ label: 'Larastan', code: 1, skipped: false },
|
||
]);
|
||
assert.equal(v.verdict, 'RED');
|
||
assert.equal(v.exitCode, 1);
|
||
assert.deepEqual(v.failed, ['Larastan']);
|
||
});
|
||
test('computeVerdict: a skipped check → RED-INCOMPLETE', () => {
|
||
const v = computeVerdict([
|
||
{ label: 'Pint', code: 0, skipped: false },
|
||
{ label: 'gitleaks', code: null, skipped: true },
|
||
]);
|
||
assert.equal(v.verdict, 'RED-INCOMPLETE');
|
||
assert.equal(v.exitCode, 1);
|
||
assert.deepEqual(v.skipped, ['gitleaks']);
|
||
});
|
||
test('computeVerdict: skipped takes precedence over a failure', () => {
|
||
const v = computeVerdict([
|
||
{ label: 'Larastan', code: 1, skipped: false },
|
||
{ label: 'lychee', code: null, skipped: true },
|
||
]);
|
||
assert.equal(v.verdict, 'RED-INCOMPLETE');
|
||
assert.deepEqual(v.failed, ['Larastan']);
|
||
assert.deepEqual(v.skipped, ['lychee']);
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: FAIL — `computeVerdict is not a function`.
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
Add to `.claude/skills/regression/run.mjs` after `parseLychee`:
|
||
|
||
```javascript
|
||
// ── pure: verdict ──────────────────────────────────────────────────
|
||
export function computeVerdict(results) {
|
||
const skipped = results.filter((r) => r.skipped).map((r) => r.label);
|
||
const failed = results
|
||
.filter((r) => !r.skipped && r.code !== 0)
|
||
.map((r) => r.label);
|
||
if (skipped.length) return { verdict: 'RED-INCOMPLETE', exitCode: 1, failed, skipped };
|
||
if (failed.length) return { verdict: 'RED', exitCode: 1, failed, skipped };
|
||
return { verdict: 'GREEN', exitCode: 0, failed, skipped };
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: PASS — 22 tests pass total.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add .claude/skills/regression/run.mjs .claude/skills/regression/run.test.mjs
|
||
git commit -m "feat(regression): GREEN/RED/RED-INCOMPLETE verdict logic
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Output formatting — `buildCanonicalLine` + `formatRow` + `verdictLine`
|
||
|
||
Three pure formatters: the canonical status line, a per-check table row, and the verdict line.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `.claude/skills/regression/run.mjs`
|
||
- Modify: `.claude/skills/regression/run.test.mjs`
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
Append to `.claude/skills/regression/run.test.mjs` (add `buildCanonicalLine, formatRow, verdictLine` to the import):
|
||
|
||
```javascript
|
||
test('buildCanonicalLine: joins tokens in result order with " / "', () => {
|
||
const results = [
|
||
{ token: 'Pint 0' }, { token: 'ESLint 0' }, { token: 'Pest 742/739/3sk/0' },
|
||
];
|
||
assert.equal(buildCanonicalLine(results), 'Pint 0 / ESLint 0 / Pest 742/739/3sk/0');
|
||
});
|
||
|
||
test('formatRow: passed check → ✅ mark, label, code, time', () => {
|
||
const row = formatRow({ label: 'Pint', code: 0, ms: 1800, skipped: false });
|
||
assert.ok(row.startsWith('[✅] Pint'));
|
||
assert.ok(row.includes('1.8s'));
|
||
});
|
||
test('formatRow: failed check → ❌ mark', () => {
|
||
assert.ok(formatRow({ label: 'Larastan', code: 1, ms: 8400, skipped: false }).startsWith('[❌] Larastan'));
|
||
});
|
||
test('formatRow: skipped check → ⚠ mark + SKIPPED', () => {
|
||
const row = formatRow({ label: 'gitleaks', code: null, ms: 0, skipped: true });
|
||
assert.ok(row.startsWith('[⚠] gitleaks'));
|
||
assert.ok(row.includes('SKIPPED'));
|
||
});
|
||
|
||
test('verdictLine: GREEN', () => {
|
||
const line = verdictLine({ verdict: 'GREEN', failed: [], skipped: [] }, 12);
|
||
assert.ok(line.includes('🟢 GREEN'));
|
||
assert.ok(line.includes('12'));
|
||
});
|
||
test('verdictLine: RED lists failed checks', () => {
|
||
const line = verdictLine({ verdict: 'RED', failed: ['Larastan'], skipped: [] }, 12);
|
||
assert.ok(line.includes('🔴 RED'));
|
||
assert.ok(line.includes('Larastan'));
|
||
});
|
||
test('verdictLine: RED-INCOMPLETE lists skipped checks', () => {
|
||
const line = verdictLine({ verdict: 'RED-INCOMPLETE', failed: [], skipped: ['gitleaks'] }, 12);
|
||
assert.ok(line.includes('🟠 RED-INCOMPLETE'));
|
||
assert.ok(line.includes('gitleaks'));
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: FAIL — `buildCanonicalLine is not a function`.
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
Add to `.claude/skills/regression/run.mjs` after `computeVerdict`:
|
||
|
||
```javascript
|
||
// ── pure: output formatting ────────────────────────────────────────
|
||
export function buildCanonicalLine(results) {
|
||
return results.map((r) => r.token).join(' / ');
|
||
}
|
||
|
||
export function formatRow(r) {
|
||
const mark = r.skipped ? '⚠' : r.code === 0 ? '✅' : '❌';
|
||
const label = r.label.padEnd(14);
|
||
const status = r.skipped
|
||
? 'SKIPPED — binary not found'
|
||
: `${r.code} ${(r.ms / 1000).toFixed(1)}s`;
|
||
return `[${mark}] ${label}${status}`;
|
||
}
|
||
|
||
export function verdictLine(v, total) {
|
||
if (v.verdict === 'GREEN') {
|
||
return `🟢 GREEN — все ${total} проверок passed`;
|
||
}
|
||
if (v.verdict === 'RED-INCOMPLETE') {
|
||
const tail = v.failed.length ? `; провал: ${v.failed.join(', ')}` : '';
|
||
return `🟠 RED-INCOMPLETE — не прогналось: ${v.skipped.join(', ')}${tail}`;
|
||
}
|
||
return `🔴 RED — ${v.failed.length}/${total} failed: ${v.failed.join(', ')}`;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: PASS — 29 tests pass total.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add .claude/skills/regression/run.mjs .claude/skills/regression/run.test.mjs
|
||
git commit -m "feat(regression): canonical line / row / verdict formatters
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: `CHECKS` registry
|
||
|
||
The data table of all 12 checks: id, label, tiers, working directory, command, parser.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `.claude/skills/regression/run.mjs`
|
||
- Modify: `.claude/skills/regression/run.test.mjs`
|
||
|
||
Each check is either **script-based** (has `cmd` — an npm/composer script line) or **binary-based** (has `bin` + `argv` — a `bin/` executable). `cwd` is `'.'` for repo-root checks or `'app'` for app-level checks. `parse` receives `(combinedOutput, exitCode)`.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
Append to `.claude/skills/regression/run.test.mjs` (add `CHECKS` to the import):
|
||
|
||
```javascript
|
||
test('CHECKS: quick tier has exactly 6 checks', () => {
|
||
assert.equal(CHECKS.filter((c) => c.tiers.includes('quick')).length, 6);
|
||
});
|
||
test('CHECKS: full tier has exactly 12 checks', () => {
|
||
assert.equal(CHECKS.filter((c) => c.tiers.includes('full')).length, 12);
|
||
});
|
||
test('CHECKS: quick is a strict subset of full', () => {
|
||
const full = new Set(CHECKS.filter((c) => c.tiers.includes('full')).map((c) => c.id));
|
||
for (const c of CHECKS.filter((c) => c.tiers.includes('quick'))) {
|
||
assert.ok(full.has(c.id), `${c.id} in quick must also be in full`);
|
||
}
|
||
});
|
||
test('CHECKS: every check has id, label, cwd, parse, and a command source', () => {
|
||
for (const c of CHECKS) {
|
||
assert.ok(c.id && c.label && c.cwd, `${c.id}: id/label/cwd`);
|
||
assert.equal(typeof c.parse, 'function', `${c.id}: parse is a function`);
|
||
assert.ok(c.cmd || (c.bin && Array.isArray(c.argv)), `${c.id}: has cmd or bin+argv`);
|
||
}
|
||
});
|
||
test('CHECKS: ids are unique', () => {
|
||
assert.equal(new Set(CHECKS.map((c) => c.id)).size, CHECKS.length);
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: FAIL — `CHECKS` is undefined / not exported.
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
Add to `.claude/skills/regression/run.mjs` after `verdictLine`:
|
||
|
||
```javascript
|
||
// ── data: checks registry ──────────────────────────────────────────
|
||
// Script-based checks carry `cmd`; binary-based checks carry `bin` + `argv`.
|
||
// `parse(combinedOutput, exitCode)` → canonical token. `cwd`: '.' = repo root,
|
||
// 'app' = the Laravel app. Execution order: quick checks first, then heavy.
|
||
export const CHECKS = [
|
||
{
|
||
id: 'pint', label: 'Pint', tiers: ['quick', 'full'], cwd: 'app',
|
||
cmd: 'composer pint:test', parse: (_o, c) => parseExit('Pint', c),
|
||
},
|
||
{
|
||
id: 'eslint', label: 'ESLint', tiers: ['quick', 'full'], cwd: 'app',
|
||
cmd: 'npm run lint:vue', parse: (_o, c) => parseExit('ESLint', c),
|
||
},
|
||
{
|
||
id: 'prettier', label: 'Prettier', tiers: ['quick', 'full'], cwd: 'app',
|
||
cmd: 'npm run format:check', parse: (_o, c) => parseExit('Prettier', c),
|
||
},
|
||
{
|
||
id: 'vue-tsc', label: 'vue-tsc', tiers: ['quick', 'full'], cwd: 'app',
|
||
cmd: 'npm run type-check', parse: (_o, c) => parseExit('vue-tsc', c),
|
||
},
|
||
{
|
||
id: 'markdownlint', label: 'markdownlint', tiers: ['quick', 'full'], cwd: '.',
|
||
cmd: 'npm run lint:md', parse: (_o, c) => parseExit('markdownlint', c),
|
||
},
|
||
{
|
||
id: 'cspell', label: 'cspell', tiers: ['quick', 'full'], cwd: '.',
|
||
cmd: 'npm run spell', parse: (_o, c) => parseExit('cspell', c),
|
||
},
|
||
{
|
||
id: 'larastan', label: 'Larastan', tiers: ['full'], cwd: 'app',
|
||
cmd: 'composer stan', parse: (o) => parseLarastan(o),
|
||
},
|
||
{
|
||
id: 'pest', label: 'Pest', tiers: ['full'], cwd: 'app',
|
||
cmd: 'composer test:parallel', parse: (o) => parsePest(o),
|
||
},
|
||
{
|
||
id: 'vitest', label: 'Vitest', tiers: ['full'], cwd: 'app',
|
||
cmd: 'npm run test:vue', parse: (o) => parseVitest(o),
|
||
},
|
||
{
|
||
id: 'vite-build', label: 'Vite build', tiers: ['full'], cwd: 'app',
|
||
cmd: 'npm run build', parse: (o) => parseViteBuild(o),
|
||
},
|
||
{
|
||
id: 'lychee', label: 'lychee', tiers: ['full'], cwd: '.',
|
||
bin: 'lychee',
|
||
argv: ['--config', '.lychee.toml', 'docs/**/*.md', 'db/**/*.md', '*.md'],
|
||
parse: (o) => parseLychee(o),
|
||
},
|
||
{
|
||
id: 'gitleaks', label: 'gitleaks', tiers: ['full'], cwd: '.',
|
||
bin: 'gitleaks',
|
||
argv: ['detect', '--source', '.', '--no-banner', '--config', '.gitleaks.toml', '--redact'],
|
||
parse: (o, c) => parseGitleaks(o, c),
|
||
},
|
||
];
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: PASS — 34 tests pass total.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add .claude/skills/regression/run.mjs .claude/skills/regression/run.test.mjs
|
||
git commit -m "feat(regression): 12-check registry (quick=6, full=12)
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: I/O layer — `runCheck` + `main` + subprocess test
|
||
|
||
The orchestrator: `runCheck` spawns one check; `main` parses the tier argument, runs the tier's checks, prints the table / canonical line / verdict, prints failed-check output, and sets the process exit code.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `.claude/skills/regression/run.mjs`
|
||
- Modify: `.claude/skills/regression/run.test.mjs`
|
||
|
||
`main` is subprocess-tested only for the **unknown-argument** path (deterministic, fast). Real `quick`/`full` runs are covered by Task 9 functional verification — running them inside `run.test.mjs` would execute the whole sweep on every `node --test`.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
Append to `.claude/skills/regression/run.test.mjs`. Add these imports at the top of the file (alongside the existing `node:test` import):
|
||
|
||
```javascript
|
||
import { execFileSync } from 'node:child_process';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const RUN = fileURLToPath(new URL('./run.mjs', import.meta.url));
|
||
```
|
||
|
||
Then append the tests:
|
||
|
||
```javascript
|
||
test('main: unknown argument → exit code 2 + error on stderr', () => {
|
||
try {
|
||
execFileSync(process.execPath, [RUN, 'bogus'], { encoding: 'utf8', stdio: 'pipe' });
|
||
assert.fail('expected non-zero exit');
|
||
} catch (err) {
|
||
assert.equal(err.status, 2);
|
||
assert.match(String(err.stderr), /unknown argument/i);
|
||
}
|
||
});
|
||
test('main: importing run.mjs does not auto-run the sweep', () => {
|
||
// If the import.meta guard were broken, importing run.mjs at the top of this
|
||
// file would have spawned a full sweep. Reaching this assertion proves it did not.
|
||
assert.ok(true);
|
||
});
|
||
```
|
||
|
||
Note: `process` is already available; add `import process from 'node:process';` at the top of the test file if not present.
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: FAIL — `main` is undefined, so `run.mjs bogus` does nothing meaningful / exits 0, and `err.status` is not 2.
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
Add the I/O imports to the top of `.claude/skills/regression/run.mjs` (alongside `import process`):
|
||
|
||
```javascript
|
||
import { spawnSync } from 'node:child_process';
|
||
import { existsSync } from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||
```
|
||
|
||
Add to the end of `.claude/skills/regression/run.mjs`, after the `CHECKS` registry:
|
||
|
||
```javascript
|
||
// ── I/O: run one check ─────────────────────────────────────────────
|
||
function runCheck(check, repoRoot) {
|
||
const cwd = check.cwd === '.' ? repoRoot : path.join(repoRoot, check.cwd);
|
||
const start = Date.now();
|
||
const skippedResult = (reason) => ({
|
||
id: check.id, label: check.label, skipped: true, code: null,
|
||
ms: Date.now() - start, token: `${check.label} SKIPPED`, stdout: '', stderr: reason,
|
||
});
|
||
|
||
let command;
|
||
if (check.bin) {
|
||
const bin = resolveBinary(check.bin);
|
||
// bin/ executables: existsSync pre-check on Windows (the project ships
|
||
// bin\gitleaks.exe / bin\lychee.exe; on POSIX they come from PATH).
|
||
if (process.platform === 'win32' && !existsSync(path.join(repoRoot, bin))) {
|
||
return skippedResult(`${bin} not found`);
|
||
}
|
||
command = [bin, ...check.argv].join(' ');
|
||
} else {
|
||
command = check.cmd;
|
||
}
|
||
|
||
const res = spawnSync(command, {
|
||
cwd, shell: true, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
|
||
});
|
||
const ms = Date.now() - start;
|
||
// ENOENT (POSIX missing binary) or shell exit 127 (command not found) → SKIPPED.
|
||
if ((res.error && res.error.code === 'ENOENT') || res.status === 127) {
|
||
return skippedResult(`command not found: ${command}`);
|
||
}
|
||
const stdout = res.stdout ?? '';
|
||
const stderr = res.stderr ?? '';
|
||
const code = res.status ?? 1;
|
||
const token = check.parse(`${stdout}\n${stderr}`, code);
|
||
return { id: check.id, label: check.label, skipped: false, code, ms, token, stdout, stderr };
|
||
}
|
||
|
||
// ── orchestrator ───────────────────────────────────────────────────
|
||
export function main(argv) {
|
||
const tier = argv[0] ?? 'full';
|
||
if (tier !== 'quick' && tier !== 'full') {
|
||
process.stderr.write(
|
||
`regression: unknown argument "${tier}". Usage: run.mjs [quick|full]\n`,
|
||
);
|
||
process.exitCode = 2;
|
||
return;
|
||
}
|
||
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url));
|
||
const checks = CHECKS.filter((c) => c.tiers.includes(tier));
|
||
|
||
process.stdout.write(`${buildHeader(tier)}\n`);
|
||
const results = [];
|
||
for (const check of checks) {
|
||
const r = runCheck(check, repoRoot);
|
||
results.push(r);
|
||
process.stdout.write(`${formatRow(r)}\n`);
|
||
}
|
||
process.stdout.write(`${'─'.repeat(48)}\n`);
|
||
process.stdout.write(`Canonical: ${buildCanonicalLine(results)}\n`);
|
||
|
||
const v = computeVerdict(results);
|
||
process.stdout.write(`VERDICT: ${verdictLine(v, results.length)}\n`);
|
||
|
||
// Full output of failed checks, so failures are visible with file:line.
|
||
// (Refines spec §7 "выше таблицы" → printed after the verdict, to keep the
|
||
// live per-check table readable during a multi-minute run.)
|
||
for (const r of results) {
|
||
if (!r.skipped && r.code !== 0) {
|
||
process.stdout.write(`\n── ${r.label} output ──\n${r.stdout}\n${r.stderr}\n`);
|
||
}
|
||
}
|
||
process.exitCode = v.exitCode;
|
||
}
|
||
|
||
// Run main only when executed directly (not when imported by run.test.mjs).
|
||
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
||
main(process.argv.slice(2));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: PASS — 36 tests pass total. The `bogus`-argument subprocess exits 2 with "unknown argument" on stderr.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add .claude/skills/regression/run.mjs .claude/skills/regression/run.test.mjs
|
||
git commit -m "feat(regression): runCheck I/O layer + main orchestrator
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: `SKILL.md`
|
||
|
||
The skill document. No TDD — it is documentation. Structure mirrors `.claude/skills/q-item-add/SKILL.md` and `.claude/skills/rls-check/SKILL.md`.
|
||
|
||
**Files:**
|
||
|
||
- Create: `.claude/skills/regression/SKILL.md`
|
||
|
||
- [ ] **Step 1: Write `SKILL.md`**
|
||
|
||
Create `.claude/skills/regression/SKILL.md` with exactly this content:
|
||
|
||
````markdown
|
||
---
|
||
name: regression
|
||
description: |
|
||
Run the project regression sweep and report a canonical status line + GREEN/RED verdict.
|
||
Two tiers: `quick` (lint/format/type-check — seconds) and `full` (everything incl.
|
||
Pest --parallel, Larastan, Vitest, Vite build, lychee, gitleaks — minutes).
|
||
Claude auto-runs only `quick` (e.g. during verification-before-completion);
|
||
`full` runs only on explicit `/regression full` or with user confirmation.
|
||
---
|
||
|
||
# Regression — канонический регрессионный свод
|
||
|
||
## Когда использовать
|
||
|
||
Перед закрытием задачи/спринта (`full`) или для быстрого фидбэка по ходу работы
|
||
(`quick`). Скилл инкапсулирует ~12 команд свода, разбросанных по `package.json`,
|
||
`app/package.json`, `app/composer.json` и `lefthook.yml`, в один вызов с
|
||
детерминированной канонической строкой и машинным вердиктом.
|
||
|
||
Invoke via `/regression [quick|full]` (без аргумента → `full`).
|
||
|
||
## Workflow
|
||
|
||
1. Определить уровень из аргумента: `quick`, `full`, либо `full` по умолчанию.
|
||
2. Запустить через Bash из корня репозитория:
|
||
|
||
```bash
|
||
node .claude/skills/regression/run.mjs <tier>
|
||
```
|
||
|
||
3. Показать пользователю полный вывод скрипта (таблица + каноническая строка +
|
||
вердикт + вывод упавших проверок).
|
||
4. Интерпретировать вердикт:
|
||
- `GREEN` — свод чист, exit-код 0.
|
||
- `RED` — перечислены упавшие проверки; полный вывод каждой — после вердикта.
|
||
- `RED-INCOMPLETE` — проверка не прогналась (нет бинаря); свод неполон,
|
||
зелёным признать нельзя.
|
||
|
||
## Уровни
|
||
|
||
- **`quick`** (6 проверок, секунды): Pint, ESLint, Prettier, vue-tsc,
|
||
markdownlint, cspell.
|
||
- **`full`** (12 проверок, минуты): всё из `quick` + Larastan, Pest `--parallel`,
|
||
Vitest, Vite build, lychee, gitleaks.
|
||
|
||
## Правила инвокации (self-restraint)
|
||
|
||
- Claude **авто-запускает только `quick`** — в том числе в рамках
|
||
`superpowers:verification-before-completion` перед claim «готово» / «passed» /
|
||
«closed».
|
||
- `full` Claude **сам не запускает** — только по явному `/regression full` от
|
||
пользователя ИЛИ запросив подтверждение («запускаю полный свод, ~5–10 мин — ок?»).
|
||
- Скилл **не правит `CLAUDE.md`** — он только печатает каноническую строку в
|
||
stdout; вставка строки в `CLAUDE.md` — отдельно, через канал
|
||
`claude-md-management` (`CLAUDE.md` §5 п.10).
|
||
|
||
## Caveats
|
||
|
||
- **Pest `--parallel` flake (квирки 72/73/77).** Если Pest показал 1–3 ошибки,
|
||
похожие на Redis-race / cumulative-state / unique-key-collision, — перепрогнать
|
||
`full` один раз ИЛИ свериться с агентом `pest-parallel-debugger` до объявления
|
||
реального RED.
|
||
- **ruflo daemon (квирк 93).** Перед baseline-критичным `full` рассмотреть
|
||
`pm2 stop ruflo-daemon` — worker-jitter усиливает Pest-flake.
|
||
- gitleaks и lychee: на Windows берутся из `bin\*.exe`, на Linux/Mac CI — из
|
||
`PATH`. Отсутствие бинаря → `[⚠] SKIPPED` + вердикт `RED-INCOMPLETE`.
|
||
|
||
## Не использовать когда
|
||
|
||
- Нужна одна конкретная проверка — запусти её npm/composer-скрипт напрямую
|
||
(быстрее, чем весь свод).
|
||
- Pa11y и Semgrep SAST — это CI-tier, в свод намеренно не входят (см. дизайн-спек
|
||
`docs/superpowers/specs/2026-05-16-regression-skill-design.md` §5).
|
||
````
|
||
|
||
- [ ] **Step 2: Verify the skill document**
|
||
|
||
Run: `npx markdownlint-cli2 ".claude/skills/regression/SKILL.md"`
|
||
Expected: `0 error(s)`.
|
||
|
||
Run: `npx cspell --no-progress --no-summary ".claude/skills/regression/SKILL.md"`
|
||
Expected: exit 0, no output. If cspell reports an unknown word that is a valid project transliteration, add it (lowercase) to `cspell-words.txt` under a dated comment header and re-run.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add .claude/skills/regression/SKILL.md
|
||
# include cspell-words.txt in this commit only if Step 2 required adding words
|
||
git commit -m "feat(regression): SKILL.md — skill doc + invocation rules
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
(This commit touches a `.md` file, so `lefthook` runs gitleaks + markdownlint + cspell. All must pass.)
|
||
|
||
---
|
||
|
||
### Task 9: Functional verification + final report
|
||
|
||
Verify the whole skill end-to-end against the spec §10 functional checklist. No new code — this task runs the skill and confirms behaviour.
|
||
|
||
**Files:**
|
||
|
||
- No file changes (verification only). If a defect is found, fix it in `run.mjs`, re-run `node --test`, and commit the fix as `fix(regression): ...`.
|
||
|
||
- [ ] **Step 1: Full unit suite green**
|
||
|
||
Run: `node --test .claude/skills/regression/run.test.mjs`
|
||
Expected: 36 tests pass, 0 fail.
|
||
|
||
- [ ] **Step 2: Happy path — `quick`**
|
||
|
||
Run: `node .claude/skills/regression/run.mjs quick`
|
||
Expected: header `─ /regression quick ─…`, 6 rows, a `Canonical:` line with 6 tokens, `VERDICT:` line. On a clean tree → `🟢 GREEN`, exit code 0. Verify the exit code: `echo $?` (bash) / `$LASTEXITCODE` (PowerShell) → `0`.
|
||
|
||
- [ ] **Step 3: Happy path — `full`**
|
||
|
||
Run: `node .claude/skills/regression/run.mjs full`
|
||
Expected: 12 rows, `Canonical:` line with 12 tokens, verdict. This run takes minutes (Pest `--parallel` + Vitest + Vite build). Cross-check the count tokens against the tools' own summaries — e.g. the `Pest …` token must match Pest's `Tests:` line. If Pest shows 1–3 intermittent errors matching quirk 72/73/77, re-run once before treating RED as real (see `SKILL.md` Caveats).
|
||
|
||
- [ ] **Step 4: RED path**
|
||
|
||
Introduce a deliberate lint error in a TypeScript file, e.g. append `const x:number='oops'` to a scratch line of an existing `app/resources/js/**/*.ts` file. Run: `node .claude/skills/regression/run.mjs quick`
|
||
Expected: `🔴 RED`, exit code 1, the verdict lists `vue-tsc` and/or `ESLint`, and the failed check's full output is printed after the verdict. Then revert the deliberate error and confirm `quick` returns to `🟢 GREEN`.
|
||
|
||
- [ ] **Step 5: RED-INCOMPLETE path**
|
||
|
||
Temporarily rename `bin/gitleaks.exe` → `bin/gitleaks.exe.bak`. Run: `node .claude/skills/regression/run.mjs full`
|
||
Expected: the gitleaks row shows `[⚠] gitleaks … SKIPPED`, the verdict is `🟠 RED-INCOMPLETE`, exit code 1. Then rename the binary back and confirm `full` no longer reports the skip.
|
||
|
||
- [ ] **Step 6: Final commit (only if Steps 1–5 required a fix)**
|
||
|
||
If any step revealed a defect that you fixed in `run.mjs`/`run.test.mjs`, commit it:
|
||
|
||
```bash
|
||
git add .claude/skills/regression/run.mjs .claude/skills/regression/run.test.mjs
|
||
git commit -m "fix(regression): <what the functional verification caught>
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
If Steps 1–5 all passed with no fix needed, there is nothing to commit — the skill is complete.
|
||
|
||
- [ ] **Step 7: Report**
|
||
|
||
Report to the user: the skill is verified per spec §10 (unit suite green + functional checklist passed), with the actual `node --test` output and the `quick`/`full` verdict lines quoted. Note that `superpowers:writing-skills` discipline is satisfied (skill verified before being declared done).
|
||
|
||
---
|
||
|
||
## Self-Review
|
||
|
||
**1. Spec coverage** — every spec section maps to a task:
|
||
|
||
- §2 scope (two tiers, orchestrator, canonical line, invocation rules, caveats, `run.test.mjs`) → Tasks 1–8.
|
||
- §3 decisions Q1–Q4 + U1–U3 + P1 → encoded in `CHECKS` tiers (Task 6), `.mjs` orchestrator (Tasks 1–7), `main` default `full` (Task 7), `run.test.mjs` (Tasks 1–7).
|
||
- §4 components (`SKILL.md`, `run.mjs`, `run.test.mjs`) → Tasks 8, 1–7, 1–7.
|
||
- §5 check matrix (12 checks, exclusions) → `CHECKS` registry (Task 6).
|
||
- §6 execution model (run-all, sequential, quick-first, platform fork) → `main` + `runCheck` (Task 7), `resolveBinary` (Task 1).
|
||
- §7 output (table, canonical line, verdict, exit codes) → Tasks 5, 7.
|
||
- §8 error handling (missing binary → SKIPPED/RED-INCOMPLETE, Pest flake caveat, ruflo caveat) → `runCheck` (Task 7), `SKILL.md` Caveats (Task 8).
|
||
- §9 invocation/self-restraint → `SKILL.md` (Task 8); frontmatter has no `disable-model-invocation` (Task 8).
|
||
- §10 testing (`node:test` unit + subprocess + functional 1–3) → Tasks 1–7 unit, Task 7 subprocess, Task 9 functional.
|
||
- §12 normative compliance (no `CLAUDE.md` edit, no schema/Q-item) → enforced by design; `SKILL.md` states the no-`CLAUDE.md`-edit rule.
|
||
|
||
No gaps.
|
||
|
||
**2. Placeholder scan** — every code step contains complete, runnable code; every test step contains real assertions on concrete fixtures; every run step has an exact command and expected output. No "TBD" / "implement later" / "add error handling".
|
||
|
||
**3. Type consistency** — the result object shape `{ id, label, code, skipped, ms, token, stdout, stderr }` is produced by `runCheck` (Task 7) and consumed by `computeVerdict` (reads `label`/`code`/`skipped` — Task 4), `formatRow` (reads `label`/`code`/`ms`/`skipped` — Task 5), `buildCanonicalLine` (reads `token` — Task 5). `parse(combinedOutput, exitCode)` signature: defined in `CHECKS` (Task 6), called by `runCheck` (Task 7), matches each parser's signature (Tasks 2–3 — content parsers ignore the 2nd arg except `parseGitleaks`). `computeVerdict` returns `{ verdict, exitCode, failed, skipped }`, consumed by `verdictLine` and `main` (Tasks 5, 7) — consistent. Function names are identical across all tasks.
|
||
|
||
---
|
||
|
||
**Execution note:** `run.mjs`/`run.test.mjs` commits are gitleaks-only under `lefthook`; the `SKILL.md` commit additionally runs markdownlint + cspell. Plan and spec are consistent; the one deliberate refinement (failed-check output printed after the verdict rather than literally "above the table") is annotated inline in Task 7.
|