From c896feb70916600db5f68b1d98c190e833ba6c11 Mon Sep 17 00:00:00 2001 From: arzafran Date: Tue, 2 Jun 2026 10:52:56 -0300 Subject: [PATCH] feat: deslop advisory probe + PR-by-default workflow Adds a deslop advisory probe to the proof-of-work gate (the framework-agnostic sibling to react-doctor) and makes opening a PR the default workflow. - proof-of-work: detectDeslop / runDeslop / pure sumDeslopFindings; bun run proof appends a deslop pass when deslop-cli is a dependency. Advisory, opt-in, pinned. - permissions: narrow Bash(npx deslop:*) allow (+ regenerated docs). - rules/typescript.md + proof-of-work skill: document the probe. - rules/git.md: 'Open a PR by default' note. - .github/PULL_REQUEST_TEMPLATE.md: dogfoods the plain-English standard. typecheck + 449 tests + lint + lint:skills clean. --- .github/PULL_REQUEST_TEMPLATE.md | 23 +++++++++++++++++ CHANGELOG.md | 16 ++++++++++++ config/30-permissions.json | 1 + docs/settings-reference.md | 1 + rules/git.md | 6 +++++ rules/typescript.md | 1 + skills/proof-of-work/SKILL.md | 2 +- src/lib/proof-of-work.ts | 43 +++++++++++++++++++++++++++++++- src/scripts/proof.ts | 27 ++++++++++++++------ src/setup.ts | 2 +- tests/proof-of-work.test.ts | 31 +++++++++++++++++++++++ 11 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e73994a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ +## What this does + + + +## Summary + + + +- + +## Test Plan + +- [ ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b60644..ee51b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ All notable changes to cc-settings are documented here. ## [Unreleased] +## [11.16.0] — 2026-06-02 + +Two additions: a **deslop advisory probe** in the proof-of-work gate (the framework-agnostic sibling to react-doctor), and a shift to **PR-by-default** as the standard workflow (with a repo PR template that dogfoods the plain-English standard). + +### Added + +- **deslop advisory probe** — `src/lib/proof-of-work.ts` gains `detectDeslop()`, `runDeslop()`, and the pure, unit-tested `sumDeslopFindings()`; `bun run proof` appends a deslop pass when the project depends on `deslop-cli`. Same shape as the react-doctor probe: **advisory** (never flips the verdict or exit code), and **opt-in by dependency** so local `npx` resolves the pinned binary with no network fetch. deslop (millionco, MIT) is a framework-agnostic cross-file dead-code / unused-export / circular-import scanner — the deterministic floor under the `deslopper` agent, catching what Biome's per-file linting can't. Reports total findings across categories (`X findings`). Silent for projects without it. +- **`config/30-permissions.json`** — narrow `Bash(npx deslop:*)` allow rule (not a blanket `npx`). `docs/settings-reference.md` permissions block regenerated. +- **`rules/typescript.md`** — Tools bullet documenting the pinned deslop invocation as an advisory pre-filter. +- **`.github/PULL_REQUEST_TEMPLATE.md`** — a repo PR template embodying the v11.15.0 plain-English standard ("What this does" → Summary → Test Plan). + +### Changed + +- **PR-by-default workflow** — `rules/git.md` adds an "Open a PR by default" note: feature-branch + PR is the norm (most Darkroom client projects protect `main`), and direct `git push origin main` is the reserved exception for repos that explicitly allow it. Corrects the prior assumption that direct-to-main was a standing default. +- **`skills/proof-of-work/SKILL.md`** — the advisory-probe paragraph now covers both react-doctor and deslop (was react-doctor only). + ## [11.15.1] — 2026-06-02 Close two doc/wiring drifts left by this session's feature releases — surfaced by an audit of "what does each change touch vs what should it touch". diff --git a/config/30-permissions.json b/config/30-permissions.json index 1a4acb7..a62de59 100644 --- a/config/30-permissions.json +++ b/config/30-permissions.json @@ -21,6 +21,7 @@ "Bash(biome:*)", "Bash(lighthouse:*)", "Bash(npx react-doctor:*)", + "Bash(npx deslop:*)", "Bash(open -a:*)", "Bash(ls:*)", "Bash(pwd:*)", diff --git a/docs/settings-reference.md b/docs/settings-reference.md index ec30d23..1aa1d05 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -658,6 +658,7 @@ Bash(gh:*) Bash(biome:*) Bash(lighthouse:*) Bash(npx react-doctor:*) +Bash(npx deslop:*) Bash(open -a:*) Bash(ls:*) Bash(pwd:*) diff --git a/rules/git.md b/rules/git.md index 81ef070..539105a 100644 --- a/rules/git.md +++ b/rules/git.md @@ -114,6 +114,12 @@ Technical bullets — the *how*, for reviewers reading the code. - No jargon dump, no restating the diff, no filler ("This PR introduces a comprehensive…"). - If you can't say what it does in plain English, that's a signal the change is unclear — not a reason to reach for bigger words. +### Open a PR by default +Default to a feature branch + PR, even for small changes — most Darkroom client +projects protect `main` and review through PRs. Direct `git push origin main` is +the exception, reserved for repos that explicitly allow it and changes that are +trivial and self-evident. When unsure, open a PR. + ### Before Merging - CI passes, code reviewed, no conflicts, branch up to date diff --git a/rules/typescript.md b/rules/typescript.md index 857771e..14433f6 100644 --- a/rules/typescript.md +++ b/rules/typescript.md @@ -89,3 +89,4 @@ function get(obj: T, key: keyof T) { return obj[key] } ## Tools - **Biome** - Linting and formatting - **tsc --noEmit** - Type checking +- **deslop** *(optional — when `deslop-cli` is in the project's deps)* — framework-agnostic cross-file dead-code / unused-export / circular-import scanner that catches what Biome's per-file linting can't. Run pinned: `npx deslop` (`--json` for a machine-readable report, `--fail-on-issues` to gate CI). `bun run proof` auto-includes it as an advisory signal when present. diff --git a/skills/proof-of-work/SKILL.md b/skills/proof-of-work/SKILL.md index 885606d..871c996 100644 --- a/skills/proof-of-work/SKILL.md +++ b/skills/proof-of-work/SKILL.md @@ -20,7 +20,7 @@ It detects `typecheck` / `test` / `lint` from `package.json`, runs them cheapest - exit 0 → `review-ready ✓` - exit 1 → `NOT review-ready ✗` — fix the failing gate before a human looks -For React projects that depend on `react-doctor`, the gate also runs it as an **advisory** probe (the project's pinned binary, telemetry off): the score is reported but never flips the verdict — a deterministic signal alongside the hard gates, not a blocker. It stays silent for projects that don't depend on it. +Projects can opt into **advisory** probes by depending on the tool — the gate then runs the project's pinned binary: **react-doctor** (React render/quality score, telemetry off) and/or **deslop** (framework-agnostic cross-file dead-code count). Advisory results are reported but never flip the verdict — deterministic signals alongside the hard gates, not blockers. Silent for projects that don't depend on them. For UI changes, attach a screenshot (`/qa` or the chrome-devtools MCP) as the visual half of the proof — tests can't prove "looks right". diff --git a/src/lib/proof-of-work.ts b/src/lib/proof-of-work.ts index a671b97..ce0d4b7 100644 --- a/src/lib/proof-of-work.ts +++ b/src/lib/proof-of-work.ts @@ -12,7 +12,7 @@ export type GateName = "typecheck" | "test" | "lint"; // Advisory probes run alongside the hard gates but never flip the verdict. -export type ProbeName = GateName | "react-doctor"; +export type ProbeName = GateName | "react-doctor" | "deslop"; export interface GateResult { gate: ProbeName; @@ -46,6 +46,15 @@ export function detectReactDoctor(deps: Record): boolean { return "react-doctor" in deps; } +/** deslop is an OPTIONAL advisory probe — a framework-agnostic cross-file + * dead-code / unused-export scanner (millionco `deslop-cli`) that catches what + * Biome's per-file linting can't. Same opt-in rule as react-doctor: runs only + * when the project depends on `deslop-cli`, so local `npx` resolves the pinned + * binary with no network fetch. Pass the merged deps + devDeps map. */ +export function detectDeslop(deps: Record): boolean { + return "deslop-cli" in deps; +} + /** Overall verdict: green iff no NON-ADVISORY gate FAILED. Skips are allowed (a * project without a lint script isn't "not review-ready"), and advisory probes * never block regardless of their status. */ @@ -103,3 +112,35 @@ export async function runReactDoctor(cwd: string): Promise { } return { gate: "react-doctor", status: "pass", detail: `score ${score}/100`, advisory: true }; } + +/** Run the deslop advisory probe. Local `npx` resolves the project's pinned + * binary; `--json` returns a flat object of finding-category arrays plus scalar + * metadata (`totalFiles`, `analysisTimeMs`, …). Total findings = the sum of the + * array-valued fields, so the scalars are naturally excluded. Always advisory: + * any non-zero exit or unparseable output is a SKIP, never a fail. */ +export async function runDeslop(cwd: string): Promise { + const proc = Bun.spawn(["npx", "deslop", "--json"], { + cwd, + stdout: "pipe", + stderr: "ignore", + }); + const [out, exit] = await Promise.all([new Response(proc.stdout).text(), proc.exited]); + const total = sumDeslopFindings(out); + if (exit !== 0 || total === null) { + return { gate: "deslop", status: "skip", detail: "unavailable", advisory: true }; + } + return { gate: "deslop", status: "pass", detail: `${total} findings`, advisory: true }; +} + +/** Sum the finding-category arrays in a deslop `--json` report. Returns null if + * the text isn't a parseable JSON object (pure, so it's unit-testable without a + * subprocess). */ +export function sumDeslopFindings(jsonText: string): number | null { + try { + const report = JSON.parse(jsonText) as Record; + if (typeof report !== "object" || report === null) return null; + return Object.values(report).reduce((n, v) => n + (Array.isArray(v) ? v.length : 0), 0); + } catch { + return null; + } +} diff --git a/src/scripts/proof.ts b/src/scripts/proof.ts index 04514fa..a6b4a8d 100644 --- a/src/scripts/proof.ts +++ b/src/scripts/proof.ts @@ -8,14 +8,17 @@ // scarce serial resource (human attention) on judgment, not on confirming what // a machine could. // -// For React projects that depend on react-doctor, an extra ADVISORY probe runs -// its deterministic scan. It reports a score but never flips the verdict. +// Projects can opt into extra ADVISORY probes by depending on the tool: +// react-doctor (React render/quality) and deslop (framework-agnostic cross-file +// dead code). They report a signal but never flip the verdict. import { allGreen, + detectDeslop, detectGates, detectReactDoctor, formatReport, + runDeslop, runGates, runReactDoctor, } from "../lib/proof-of-work.ts"; @@ -28,16 +31,24 @@ const pkg = (await Bun.file("package.json") devDependencies?: Record; }; const gates = detectGates(pkg.scripts ?? {}); -const hasReactDoctor = detectReactDoctor({ ...pkg.dependencies, ...pkg.devDependencies }); +const deps = { ...pkg.dependencies, ...pkg.devDependencies }; +const hasReactDoctor = detectReactDoctor(deps); +const hasDeslop = detectDeslop(deps); -if (gates.length === 0 && !hasReactDoctor) { +const advisoryLabels = [ + ...(hasReactDoctor ? ["react-doctor (advisory)"] : []), + ...(hasDeslop ? ["deslop (advisory)"] : []), +]; + +if (gates.length === 0 && advisoryLabels.length === 0) { console.log("Proof of work: no verify scripts (typecheck/test/lint) in package.json — skipping."); process.exit(0); } -const labels = [...gates, ...(hasReactDoctor ? ["react-doctor (advisory)"] : [])]; -console.log(`Running proof-of-work gates: ${labels.join(", ")} …\n`); -const results = await runGates(gates, process.cwd()); -if (hasReactDoctor) results.push(await runReactDoctor(process.cwd())); +console.log(`Running proof-of-work gates: ${[...gates, ...advisoryLabels].join(", ")} …\n`); +const cwd = process.cwd(); +const results = await runGates(gates, cwd); +if (hasReactDoctor) results.push(await runReactDoctor(cwd)); +if (hasDeslop) results.push(await runDeslop(cwd)); console.log(formatReport(results)); process.exit(allGreen(results) ? 0 : 1); diff --git a/src/setup.ts b/src/setup.ts index fd310f4..e10d899 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -50,7 +50,7 @@ import type { StatusData } from "./lib/status-types.ts"; import { buildVersionDelta, readInstalledVersion } from "./lib/version-delta.ts"; import { Settings } from "./schemas/settings.ts"; -const VERSION = "11.15.1"; // close session drift: proof-of-work skill documents react-doctor probe; reviewer agent carries plain-English standard +const VERSION = "11.16.0"; // deslop advisory probe in proof-of-work; PR-by-default workflow + PULL_REQUEST_TEMPLATE const CLAUDE_DIR = join(homedir(), ".claude"); // --- Arg parsing --------------------------------------------------------- diff --git a/tests/proof-of-work.test.ts b/tests/proof-of-work.test.ts index f719076..0f879ba 100644 --- a/tests/proof-of-work.test.ts +++ b/tests/proof-of-work.test.ts @@ -6,9 +6,11 @@ import { describe, expect, test } from "bun:test"; import { allGreen, + detectDeslop, detectGates, detectReactDoctor, formatReport, + sumDeslopFindings, } from "../src/lib/proof-of-work.ts"; describe("proof-of-work lib", () => { @@ -63,4 +65,33 @@ describe("proof-of-work lib", () => { expect(report).toContain("ℹ react-doctor — score 87/100 (advisory)"); expect(report).toContain("review-ready ✓"); }); + + test("detectDeslop: true only when deslop-cli is a dependency", () => { + expect(detectDeslop({ "deslop-cli": "0.0.14", typescript: "6" })).toBe(true); + // deslop-js (the lib) is not the bin package — only deslop-cli counts + expect(detectDeslop({ "deslop-js": "0.0.14" })).toBe(false); + expect(detectDeslop({})).toBe(false); + }); + + test("sumDeslopFindings: sums finding arrays, ignores scalar metadata", () => { + const report = JSON.stringify({ + unusedFiles: [{ path: "a" }, { path: "b" }], + unusedExports: [{ name: "x" }], + circularDependencies: [], + totalFiles: 120, + analysisTimeMs: 42, + }); + expect(sumDeslopFindings(report)).toBe(3); + expect(sumDeslopFindings("{}")).toBe(0); + expect(sumDeslopFindings("not json")).toBeNull(); + }); + + test("deslop advisory: rendered with (advisory), never blocks the verdict", () => { + const results = [ + { gate: "typecheck" as const, status: "pass" as const }, + { gate: "deslop" as const, status: "pass" as const, detail: "12 findings", advisory: true }, + ]; + expect(allGreen(results)).toBe(true); + expect(formatReport(results)).toContain("ℹ deslop — 12 findings (advisory)"); + }); });