diff --git a/docs/catalog.md b/docs/catalog.md index 7245634f6b..61d6c353b7 100644 --- a/docs/catalog.md +++ b/docs/catalog.md @@ -71,7 +71,7 @@ plugin manifests and kept in sync by CI. Never hand-edit it; the category vocabu - [`bugs`](../plugins/bugs): Produces from an informal defect description a structured five-field bug report: title, steps to reproduce, expected vs actual, severity with justification, and suggested fix location. Read-only by default: it emits the report and never edits code, opens a PR, or files an issue on its own. - [`debugging`](../plugins/debugging): Debug observed failures via a disciplined six-phase loop: build a fast deterministic reproduction signal, reproduce, rank falsifiable hypotheses, instrument, fix with a regression test, then clean up and post-mortem. - [`docs-hygiene`](../plugins/docs-hygiene): Documentation-hygiene toolkit: compress (flavor-trim markdown with a semantic-diff safety net), audit-noise (classify markdown noise), extract-ssot (deduplicate repeated content into a single source of truth), audit-encapsulation (detect citations into skill-private surfaces), rename-references (sweep stale references after renames), audit-derivability (classify whether a whole document earns its existence: could a fresh agent re-derive it from the code?), audit-progressive-disclosure (grade instruction files against a load-tier model for split opportunities and hub/spoke disclosure defects), write-for-agents (authoring-time doctrine that fires while agent-consumed markdown is being written), write-for-humans (the same moment for the other reader, covering end-user READMEs, RFCs, release notes and guides, and resolving the consuming project's own style guide first), and a file-name set that plans, applies, and enforces a casing rule across a doc tree: setup (the one configuration surface), audit-file-names (read-only inventory plus the reference sweep), realign-file-names (the executor, one human acceptance per file), and generate-file-name-gate (emits the standalone check that keeps the tree from drifting back). -- [`code-tidying`](../plugins/code-tidying): Code tidying and comment hygiene: /code-tidying:tidy proactively hunts a rotated, glob-scoped lane for Beck-style tidyings under a research-backed scope budget and ships one tight PR; /code-tidying:batch-simplify sweeps a time window, a branch, or an entire repository through grouped, dependency-ordered simplification waves with a fix-first deferral contract that resolves deferrals in the same run instead of filing issues; /code-tidying:dissolve-comments enforces self-describing expressive code over a diff or target, widening to the branch diff and then the whole repository when the tree is clean: it deletes zero-information comments, dissolves code-expressible ones into names and structure behind a tests gate (safe mode restricts applied edits to removals), and keeps only terse load-bearing comments code cannot express; /code-tidying:audit-comment-residue is a read-only classifier that flags history, plan, conversational, and ticket/PR residue in code comments for author-applied deletion; /code-tidying:audit-dead-code is a read-only whole-repo dead-code hunter running four labelled lanes of unequal confidence (knip for TS/JS, vulture for Python, gopls for Go, and a portable grep lane for shell and other symbol languages), adjudicating every candidate against dynamic-usage evidence into a dead, uncertain, or alive verdict. Project-specific tidy lanes are scaffolded into a tracked .claude/tidy-lanes/ config folder by a re-runnable setup skill. +- [`code-tidying`](../plugins/code-tidying): Code tidying and comment hygiene: /code-tidying:tidy proactively hunts a rotated, glob-scoped lane for Beck-style tidyings under a research-backed scope budget and ships one tight PR; /code-tidying:batch-simplify sweeps a time window, a branch, or an entire repository through grouped, dependency-ordered simplification waves with a fix-first deferral contract that resolves deferrals in the same run instead of filing issues; /code-tidying:dissolve-comments enforces self-describing expressive code over a diff or target, widening to the branch diff and then the whole repository when the tree is clean: it deletes zero-information comments, dissolves code-expressible ones into names and structure behind a tests gate (safe mode restricts applied edits to removals, the aggressive dial keeps only exempt surfaces and terse warnings, and strip deletes every comment but the exempt surfaces without rewriting code), and keeps only terse load-bearing comments code cannot express; /code-tidying:audit-comment-residue is a read-only classifier that flags history, plan, conversational, and ticket/PR residue in code comments for author-applied deletion; /code-tidying:audit-dead-code is a read-only whole-repo dead-code hunter running four labelled lanes of unequal confidence (knip for TS/JS, vulture for Python, gopls for Go, and a portable grep lane for shell and other symbol languages), adjudicating every candidate against dynamic-usage evidence into a dead, uncertain, or alive verdict. Project-specific tidy lanes are scaffolded into a tracked .claude/tidy-lanes/ config folder by a re-runnable setup skill. - [`coupling`](../plugins/coupling): Iteratively reduces coupling in any repository at any altitude, whether documents, code modules, applications, or repositories: scans for change-transmitting dependencies typed against a coupling model (strength ladder, connascence, volatility weighting), verifies every finding, applies a budgeted batch of safe behavior-preserving reductions, and keeps a durable ledger so structural candidates route to design lanes and repeated runs continue where the last stopped. - [`repo-hygiene`](../plugins/repo-hygiene): Repo hygiene action-router: /repo-hygiene:clean sweeps reclaimable caches, build artifacts, and stale git metadata, and can realign the working tree to a fresh-pull state, dry-run-first, with destructive tiers gated behind explicit confirmation and a session-scoped destructive-command guard. Ecosystem targets are detected at runtime; secrets, runtime dependencies, and skill data are preserved by default. - [`repo-fleet-hygiene`](../plugins/repo-fleet-hygiene): Cross-repository Git/GitHub fleet discovery, evidence rollup, and a gated apply verb that executes a prior fleet action plan behind one confirmation. Audit stays read-only and confidence-tiered; apply mutates only with --apply plus interactive confirmation or --yes. diff --git a/plugins/code-tidying/.claude-plugin/plugin.json b/plugins/code-tidying/.claude-plugin/plugin.json index b68bc470f6..7bc83b0f38 100644 --- a/plugins/code-tidying/.claude-plugin/plugin.json +++ b/plugins/code-tidying/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "code-tidying", - "version": "0.19.8", - "description": "Code tidying and comment hygiene: /code-tidying:tidy proactively hunts a rotated, glob-scoped lane for Beck-style tidyings under a research-backed scope budget and ships one tight PR; /code-tidying:batch-simplify sweeps a time window, a branch, or an entire repository through grouped, dependency-ordered simplification waves with a fix-first deferral contract that resolves deferrals in the same run instead of filing issues; /code-tidying:dissolve-comments enforces self-describing expressive code over a diff or target, widening to the branch diff and then the whole repository when the tree is clean: it deletes zero-information comments, dissolves code-expressible ones into names and structure behind a tests gate (safe mode restricts applied edits to removals), and keeps only terse load-bearing comments code cannot express; /code-tidying:audit-comment-residue is a read-only classifier that flags history, plan, conversational, and ticket/PR residue in code comments for author-applied deletion; /code-tidying:audit-dead-code is a read-only whole-repo dead-code hunter running four labelled lanes of unequal confidence (knip for TS/JS, vulture for Python, gopls for Go, and a portable grep lane for shell and other symbol languages), adjudicating every candidate against dynamic-usage evidence into a dead, uncertain, or alive verdict. Project-specific tidy lanes are scaffolded into a tracked .claude/tidy-lanes/ config folder by a re-runnable setup skill.", + "version": "0.20.0", + "description": "Code tidying and comment hygiene: /code-tidying:tidy proactively hunts a rotated, glob-scoped lane for Beck-style tidyings under a research-backed scope budget and ships one tight PR; /code-tidying:batch-simplify sweeps a time window, a branch, or an entire repository through grouped, dependency-ordered simplification waves with a fix-first deferral contract that resolves deferrals in the same run instead of filing issues; /code-tidying:dissolve-comments enforces self-describing expressive code over a diff or target, widening to the branch diff and then the whole repository when the tree is clean: it deletes zero-information comments, dissolves code-expressible ones into names and structure behind a tests gate (safe mode restricts applied edits to removals, the aggressive dial keeps only exempt surfaces and terse warnings, and strip deletes every comment but the exempt surfaces without rewriting code), and keeps only terse load-bearing comments code cannot express; /code-tidying:audit-comment-residue is a read-only classifier that flags history, plan, conversational, and ticket/PR residue in code comments for author-applied deletion; /code-tidying:audit-dead-code is a read-only whole-repo dead-code hunter running four labelled lanes of unequal confidence (knip for TS/JS, vulture for Python, gopls for Go, and a portable grep lane for shell and other symbol languages), adjudicating every candidate against dynamic-usage evidence into a dead, uncertain, or alive verdict. Project-specific tidy lanes are scaffolded into a tracked .claude/tidy-lanes/ config folder by a re-runnable setup skill.", "author": { "name": "Melodic Software", "email": "info@melodicsoftware.com" @@ -30,7 +30,7 @@ "comment_posture": { "type": "string", "title": "dissolve-comments posture", - "description": "How dissolve-comments treats a kept comment. strict (default): every kept comment is held to class_c_max_lines and rewritten terser when over it, with the removed narrative staged for the commit message; balanced: the same triage, but an over-budget comment is reported instead of rewritten; conservative: class-A deletions only, every class-B item and class-C rewrite is proposed. Doubt keeps the comment in every posture. Any other value is read as strict.", + "description": "How dissolve-comments treats a kept comment. strict (default): every kept comment is held to class_c_max_lines and rewritten terser when over it, with the removed narrative staged for the commit message; balanced: the same triage, but an over-budget comment is reported instead of rewritten; conservative: class-A deletions only, every class-B item and class-C rewrite is proposed; aggressive: only exempt surfaces, paired records, and terse warnings of consequence survive, and every other comment is staged and deleted. The per-run tokens safe, strip, and aggressive beat this value, safe first. No posture loosens a gate: every applied deletion still carries the token proof and every tier-2 or tier-3 move still needs a test net. Any other value is read as strict.", "default": "strict" }, "class_c_max_lines": { diff --git a/plugins/code-tidying/CHANGELOG.md b/plugins/code-tidying/CHANGELOG.md index 0a5fc78893..e1a600552c 100644 --- a/plugins/code-tidying/CHANGELOG.md +++ b/plugins/code-tidying/CHANGELOG.md @@ -3,6 +3,37 @@ All notable changes to the `code-tidying` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.20.0] + +### Added + +- **`dissolve-comments` gains an aggressive dial.** `aggressive` (a per-run token and a + `comment_posture` value) keeps only the exempt surfaces, paired comment-plus-test records, and + terse warnings of consequence; every other comment is staged and deleted, rationale included. + `strip` (a per-run token) deletes every comment but the exempt surfaces and paired records and + rewrites no code. Precedence is `safe`, then `strip`, then `aggressive`, and a token beats the + standing posture. +- **`--notes `** appends the staged commit-message block to an untracked or out-of-repo file. + A tracked path is refused and the run continues with the report as the only vehicle. +- **A calibration eval suite** under `plugins/code-tidying/evals/`, run with `claude plugin eval`: + three frozen real sections, invented fixtures per triage class, exempt surfaces, marker rows, + Python docstrings, paired records, and the dial interactions. + +### Changed + +- **"The posture ladder only descends" is replaced by "no knob loosens a gate."** The dials widen + what a run removes; they change no proof. Deletions still carry COMMENT-ONLY, function-local + renames RENAME-ONLY, tier-2 and tier-3 moves a discovered test net, and an UNPROVABLE file still + yields proposals only. +- **Two rules now hold in every mode:** a comment paired with a regression test is never deleted + alone, and an identifier a repo-local marker row pins is never renamed. +- **The tier tables in `safety.md` and `dissolving-moves.md` agree.** The merged set is 16 moves: + tier 2 gains Replace Nested Conditional with Guard Clauses and Introduce Special Case, tier 3 + gains Inline Function in `safety.md` and Extract Class in `dissolving-moves.md`. The apply-capacity + counts read 2 of 16 and 0 of 16. +- **The `Intentional-removal:` trailer is conditional.** The staged block carries it only where the + target repository's own gates read that trailer. + ## [0.19.8] ### Changed diff --git a/plugins/code-tidying/README.md b/plugins/code-tidying/README.md index 5f5c8243a2..aa504749bf 100644 --- a/plugins/code-tidying/README.md +++ b/plugins/code-tidying/README.md @@ -17,7 +17,11 @@ Six skills, one capability: behind a token-level proof (`change-shape.py`, so they act on a repository with no test suite); additive refactors need a discovered test net; interface-creating ones are proposal-first. `safe` mode restricts applied - edits to removals. Ships a comment census with a token estimate and a + edits to removals; `aggressive` keeps only exempt surfaces, paired records, + and terse warnings of consequence, staging every other comment's narrative + before deleting it; `strip` deletes every comment but the exempt surfaces and + rewrites no code. `--notes ` writes the staged block to an untracked + file as well. No dial loosens a gate. Ships a comment census with a token estimate and a cross-language commented-out-code detector, and probes its reading layers (`scc`, `pygments`, `tree-sitter`, `ruff`, `ast-grep`) at run time, naming what each absent one costs. @@ -126,14 +130,15 @@ personal variation is limited to lane names the team does not track: an uncommit ## Configuration -Four `userConfig` options. Three tune `dissolve-comments` and loosen nothing; -the fourth is the personal-posture channel of the exclusion override above, and -loosening is its whole job: +Four `userConfig` options. Three tune `dissolve-comments`, and none of them +loosens a gate: they set what a run removes, never what it may apply without a +proof. The fourth is the personal-posture channel of the exclusion override +above, and loosening is its whole job: | Option | Default | Effect | |---|---|---| | `hard_exclusions` | `enforce` | `enforce` keeps every GLOBAL HARD **path** entry blocking; `advisory` reports each match and blocks nothing, so runs may reach lint config, agent config, CI workflows, and hook chains. Path entries only: the behavioral guards, the work-tracking entries, and the self-update protections hold at either value. | -| `comment_posture` | `strict` | `strict` rewrites an over-budget kept comment terser and stages the removed narrative; `balanced` reports it instead; `conservative` applies class-A deletions only and proposes everything else. Doubt keeps the comment in every posture. | +| `comment_posture` | `strict` | `strict` rewrites an over-budget kept comment terser and stages the removed narrative; `balanced` reports it instead; `conservative` applies class-A deletions only and proposes everything else; `aggressive` keeps only exempt surfaces, paired records, and terse warnings. The per-run tokens `safe`, `strip`, and `aggressive` beat this value, `safe` first. | | `class_c_max_lines` | `2` | Line budget for a kept (class-C) comment before it is rewritten. | | `apply_local_renames` | `true` | Apply a function-local rename that `change-shape.py` certifies as RENAME-ONLY even with no test net; `false` proposes it. | @@ -157,7 +162,7 @@ reads it from. | Option | Type | Default | Environment variable | Description | | --- | --- | --- | --- | --- | | `hard_exclusions` | string | `"enforce"` | `CLAUDE_PLUGIN_OPTION_HARD_EXCLUSIONS` | How tidy, dissolve-comments, and batch-simplify treat the GLOBAL HARD path list in skills/tidy/reference/exclusions.md. enforce (default): a path on that list is dropped before triage; advisory: the list is reported per path and never blocks, so a run may edit lint config, agent config, CI workflows, and hook chains. advisory is the standing form of the per-run override argument and is lifted for path entries only: the behavioral guards, the work-tracking entries, and the SELF-UPDATE EXTRA HARD list hold under every value. Any other value is read as enforce. | -| `comment_posture` | string | `"strict"` | `CLAUDE_PLUGIN_OPTION_COMMENT_POSTURE` | How dissolve-comments treats a kept comment. strict (default): every kept comment is held to class_c_max_lines and rewritten terser when over it, with the removed narrative staged for the commit message; balanced: the same triage, but an over-budget comment is reported instead of rewritten; conservative: class-A deletions only, every class-B item and class-C rewrite is proposed. Doubt keeps the comment in every posture. Any other value is read as strict. | +| `comment_posture` | string | `"strict"` | `CLAUDE_PLUGIN_OPTION_COMMENT_POSTURE` | How dissolve-comments treats a kept comment. strict (default): every kept comment is held to class_c_max_lines and rewritten terser when over it, with the removed narrative staged for the commit message; balanced: the same triage, but an over-budget comment is reported instead of rewritten; conservative: class-A deletions only, every class-B item and class-C rewrite is proposed; aggressive: only exempt surfaces, paired records, and terse warnings of consequence survive, and every other comment is staged and deleted. The per-run tokens safe, strip, and aggressive beat this value, safe first. No posture loosens a gate: every applied deletion still carries the token proof and every tier-2 or tier-3 move still needs a test net. Any other value is read as strict. | | `class_c_max_lines` | number
*min 1, max 40* | `2` | `CLAUDE_PLUGIN_OPTION_CLASS_C_MAX_LINES` | Lines a kept (class-C) comment may run before dissolve-comments rewrites it terser, staging any removed narrative for the commit message. A genuinely load-bearing multi-line contract may exceed it when the report says why. | | `apply_local_renames` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_APPLY_LOCAL_RENAMES` | When true (default), a function-local Rename Variable whose edit change-shape.py certifies as RENAME-ONLY is applied and reported with its identifier mapping even when no test net is discovered. When false, such renames are proposed. | diff --git a/plugins/code-tidying/evals/.editorconfig b/plugins/code-tidying/evals/.editorconfig new file mode 100644 index 0000000000..c0e6b9916e --- /dev/null +++ b/plugins/code-tidying/evals/.editorconfig @@ -0,0 +1,9 @@ +# Eval fixtures are frozen corpus, not source: real sections copied byte for byte +# at a pinned commit, plus invented samples whose whitespace is part of what the +# graders measure. A Makefile recipe needs its tab, and normalizing any of it +# would silently change what the suite tests. + +[*] +indent_style = unset +indent_size = unset +trim_trailing_whitespace = false diff --git a/plugins/code-tidying/evals/fixtures/Makefile.txt b/plugins/code-tidying/evals/fixtures/Makefile.txt new file mode 100644 index 0000000000..9908587ee7 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/Makefile.txt @@ -0,0 +1,3 @@ +.PHONY: test +test: + bash class-b.test.sh diff --git a/plugins/code-tidying/evals/fixtures/check-markers.sh.txt b/plugins/code-tidying/evals/fixtures/check-markers.sh.txt new file mode 100755 index 0000000000..4a83ac66eb --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/check-markers.sh.txt @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Fails when a marker listed in scripts/restoration-markers.txt is missing from src/. +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +status=0 +while IFS= read -r marker; do + [[ -z "$marker" ]] && continue + if ! grep -rqF -- "$marker" "$root/src"; then + echo "missing restoration marker: $marker" >&2 + status=1 + fi +done <"$root/scripts/restoration-markers.txt" +exit "$status" diff --git a/plugins/code-tidying/evals/fixtures/class-a.sh.txt b/plugins/code-tidying/evals/fixtures/class-a.sh.txt new file mode 100755 index 0000000000..b98a46de4c --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/class-a.sh.txt @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +counter=0 +total=0 + +for value in "$@"; do + total=$((total + value)) + # increment counter + counter=$((counter + 1)) +done + +# old_total=$((total * 2)) +# echo "$old_total" + +echo "count=$counter total=$total" diff --git a/plugins/code-tidying/evals/fixtures/class-b.sh.txt b/plugins/code-tidying/evals/fixtures/class-b.sh.txt new file mode 100755 index 0000000000..ec4aadceb0 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/class-b.sh.txt @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +cache_ttl_seconds() { + local days="$1" + # seconds per day + local ttl=$((days * 86400)) + echo "$ttl" +} + +greeting() { + # user's display name + local n="$1" + echo "Hello, $n" +} diff --git a/plugins/code-tidying/evals/fixtures/class-b.test.sh.txt b/plugins/code-tidying/evals/fixtures/class-b.test.sh.txt new file mode 100755 index 0000000000..9826988b82 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/class-b.test.sh.txt @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/class-b.sh" + +fail=0 +[[ "$(cache_ttl_seconds 2)" == "172800" ]] || { + echo "cache_ttl_seconds 2: expected 172800" >&2 + fail=1 +} +[[ "$(greeting Ada)" == "Hello, Ada" ]] || { + echo "greeting Ada: expected Hello, Ada" >&2 + fail=1 +} +exit "$fail" diff --git a/plugins/code-tidying/evals/fixtures/class-c.sh.txt b/plugins/code-tidying/evals/fixtures/class-c.sh.txt new file mode 100755 index 0000000000..5e5575eeca --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/class-c.sh.txt @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +lock_file="${LOCK_FILE:-worker.lock}" +api_url="${API_URL:-https://api.example.com}" + +prepare_lock() { + : >"$lock_file" +} + +start_worker() { + echo "worker started with lock $lock_file" +} + +fetch_status() { + local attempt + # Retries three times because the upstream API drops the first connection after idle. + local retries=3 + for ((attempt = 1; attempt <= retries; attempt++)); do + if curl --silent --max-time "$timeout" "$api_url/status"; then + return 0 + fi + done + return 1 +} + +# set the counter to zero +counter=0 + +# Uses a 5 second timeout to stay under the load balancer's 6 second idle cutoff. +timeout=5 + +# Must run before start_worker, or the lock file deadlocks the worker. +prepare_lock +start_worker + +fetch_status || counter=$((counter + 1)) +echo "failures: $counter" diff --git a/plugins/code-tidying/evals/fixtures/dc-notes.md.txt b/plugins/code-tidying/evals/fixtures/dc-notes.md.txt new file mode 100644 index 0000000000..395028ae4a --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/dc-notes.md.txt @@ -0,0 +1 @@ +seed notes diff --git a/plugins/code-tidying/evals/fixtures/edge-marker.sh.txt b/plugins/code-tidying/evals/fixtures/edge-marker.sh.txt new file mode 100755 index 0000000000..b5cfc8df36 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/edge-marker.sh.txt @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +next_page_window() { + local offset="$1" page_size="$2" + # end offset of the current page + gql_page_end=$((offset + page_size)) + echo "$offset $gql_page_end" +} diff --git a/plugins/code-tidying/evals/fixtures/edge-paired.sh.txt b/plugins/code-tidying/evals/fixtures/edge-paired.sh.txt new file mode 100755 index 0000000000..7ba9124d11 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/edge-paired.sh.txt @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +has_input() { + # Empty input must return 1, not 0; tests/edge-paired.test.sh pins this. + [[ -n "${1:-}" ]] || return 1 + return 0 +} diff --git a/plugins/code-tidying/evals/fixtures/edge-paired.test.sh.txt b/plugins/code-tidying/evals/fixtures/edge-paired.test.sh.txt new file mode 100755 index 0000000000..6bd42749a3 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/edge-paired.test.sh.txt @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/../src/edge-paired.sh" + +if has_input ""; then + echo "has_input returned 0 for empty input; expected 1" >&2 + exit 1 +fi +has_input "x" +echo "ok" diff --git a/plugins/code-tidying/evals/fixtures/edge_docstring.py.txt b/plugins/code-tidying/evals/fixtures/edge_docstring.py.txt new file mode 100644 index 0000000000..3dc73c4ba3 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/edge_docstring.py.txt @@ -0,0 +1,8 @@ +def _parse(line): + """Split a header line into key and value.""" + key, _, value = line.partition(":") + return key.strip(), value.strip() + + +def parse_headers(lines): + return dict(_parse(line) for line in lines if ":" in line) diff --git a/plugins/code-tidying/evals/fixtures/exempt.py.txt b/plugins/code-tidying/evals/fixtures/exempt.py.txt new file mode 100755 index 0000000000..b01cdb2f64 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/exempt.py.txt @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Example Corp. +# Licensed under the MIT License. See LICENSE in the project root. + +URL = "https://example.com/api/v1/pages?include=title,author,updated_at&sort=updated_at&order=descending&limit=100" # noqa: E501 long canonical URL + +# TODO(#12): replace polling with a webhook +POLL_SECONDS = 30 + + +def fetch_pages(client): + pages = client.get(URL) + # Sorted here because the API returns pages in arbitrary order. + pages.sort(key=lambda page: page["updated_at"]) + return pages + + +def retry_delay(attempt): + # dissolve-comments-ignore + return min(2**attempt, POLL_SECONDS) # capped so a retry never outlasts one poll interval diff --git a/plugins/code-tidying/evals/fixtures/hook-utils-header.sh.txt b/plugins/code-tidying/evals/fixtures/hook-utils-header.sh.txt new file mode 100644 index 0000000000..c8fc6af87b --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/hook-utils-header.sh.txt @@ -0,0 +1,59 @@ +# shellcheck shell=bash +# Shared hook utility library for this marketplace's hook plugins. Sourced +# (not executed): kill switch, file_path parsing + path normalization, +# repo-root resolution, additionalContext accumulator, telemetry envelope. +# +# SINGLE SOURCE OF TRUTH: lib/hook-utils.sh at the marketplace repo root. The +# copies at plugins/*/hooks/hook-utils.sh exist because installed plugins are +# cache-isolated and must be self-contained — never edit a copy. Edit the +# source and run scripts/sync-hook-utils.sh; CI rejects drifted copies. +# +# CALLING CONVENTION: a helper that produces a value is spelled +# `hook::_to [args…]` and writes into the caller's variable. That +# is the one convention; call it directly rather than wrapping it in `$( )`. +# GNU Bash forks a subshell for every command substitution even when the body +# is only builtins (Command Substitution, Bash Reference Manual; +# https://mywiki.wooledge.org/CommandSubstitution), and on Windows Git Bash a +# fork is a non-copy-on-write Win32 CreateProcess costing milliseconds, so a +# capture around a `_to` helper is pure loss on hooks that run per edit. +# Five helpers still print instead — hook::json_escape, hook::physical_path, +# hook::repo_root, hook::buffer_stdin and hook::read_file_path (which reads +# fd0 and has no `_to` twin). Each carries, at its definition, the one-line +# reason its capture is still paid for. + +# Guard against double-sourcing. +[[ -n "${_HOOK_UTILS_LOADED:-}" ]] && return 0 +readonly _HOOK_UTILS_LOADED=1 + +# Per-hook kill switch via the plugin's _enabled userConfig boolean, +# read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. +# Exits 0 (allow) if disabled. Place after source, before stdin parsing. +# hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { + local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 +} diff --git a/plugins/code-tidying/evals/fixtures/hook-utils-unprovable.sh.txt b/plugins/code-tidying/evals/fixtures/hook-utils-unprovable.sh.txt new file mode 100644 index 0000000000..828f487835 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/hook-utils-unprovable.sh.txt @@ -0,0 +1,24 @@ +# hook::resolve_read_timeout_to +# Write the resolved timeout into in THIS shell. GNU Bash runs command +# substitution in a subshell even when the body is only builtins (Command +# Execution Environment), so a capture here is a startup fork on every hook; +# this is the only spelling, and the suite pins that buffer_stdin uses it. +hook::resolve_read_timeout_to() { + local __hu_dest="$1" + local __hu_t="${CLAUDE_PLUGIN_OPTION_STDIN_READ_TIMEOUT:-2}" + if [[ "$__hu_t" != "2" ]]; then + if ! [[ "$__hu_t" =~ ^[0-9]+(\.[0-9]+)?$ ]] || [[ "$__hu_t" =~ ^0+(\.0+)?$ ]] || + { [[ "$__hu_t" == *.* ]] && ! hook::read_supports_fractional_timeout; }; then + __hu_t=2 + elif [[ "$__hu_t" =~ ^([0-9]+)(\.([0-9]+))?$ ]]; then + local whole="${BASH_REMATCH[1]}" frac="${BASH_REMATCH[3]:-}" + frac="${frac}000000" + frac="${frac:0:6}" + local micros=$((10#$whole * 1000000 + 10#$frac)) + if ((micros < HOOK_STDIN_READ_TIMEOUT_MIN_MICROS)); then + __hu_t=2 + fi + fi + fi + printf -v "$__hu_dest" '%s' "$__hu_t" +} diff --git a/plugins/code-tidying/evals/fixtures/restoration-markers.txt.txt b/plugins/code-tidying/evals/fixtures/restoration-markers.txt.txt new file mode 100644 index 0000000000..0e47ab3a49 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/restoration-markers.txt.txt @@ -0,0 +1 @@ +gql_page_end= diff --git a/plugins/code-tidying/evals/fixtures/seed.sh b/plugins/code-tidying/evals/fixtures/seed.sh new file mode 100755 index 0000000000..0375c60180 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/seed.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Seeds the eval workspace with fixtures/.txt copied to , committed once. +# Usage: seed.sh ... +set -euo pipefail + +fixtures="$(dirname "${BASH_SOURCE[0]}")" +git init -q +for name in "$@"; do + mkdir -p "$(dirname "$name")" + cp "$fixtures/$(basename "$name").txt" "$name" + git add "$name" +done +git -c user.name=eval -c user.email=eval@example.invalid commit -q -m "add fixtures" diff --git a/plugins/code-tidying/evals/fixtures/silent-revert-design.sh.txt b/plugins/code-tidying/evals/fixtures/silent-revert-design.sh.txt new file mode 100644 index 0000000000..f980df2f5e --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/silent-revert-design.sh.txt @@ -0,0 +1,39 @@ +# WHY BLAME-OF-DELETED-LINES, AND NOT THE ALTERNATIVES +# --------------------------------------------------- +# Three designs were measured against the real history before this one was +# chosen. +# +# Curated marker strings (#2691's own suggestion 3). Catches only what +# somebody pre-registered. Nobody had registered #2644, #2642, #2635 or +# #2639 -- registration happens after you already know a fix matters, which +# is exactly the knowledge the incident destroys. It also decays: the list is +# only as fresh as the last person who remembered to append to it. +# +# Merge-base staleness (the PR's branch point vs. what landed since). +# Tested and REJECTED on evidence: it exonerates all three real incidents, +# because all three branches were up to date in history. It is a +# false-negative machine for the exact class it is meant to catch. +# +# PR-creation-time overlap (culprit landed after the PR was opened). +# Tested and rejected as non-discriminating: at the 17-concurrent-PR rate the +# ADR records for this repo, nearly every PR has siblings landing while it is +# open, so it fires on almost everything. +# +# What is left is content: blame the lines a merge deleted and see who had just +# added them. Measured over the 500 first-parent commits of main ending at +# 738791c45, the three known incidents score 853 / 451 / 346 lines against a +# single recent commit -- plus a fourth attribution of 298 lines on the SAME +# #2633 squash, whose deletions trace to two different culprits and are +# reported separately. Unlike the two designs rejected above, this one fires on +# all three rather than exonerating them. +# +# What it does NOT buy is a clean split between incidents and ordinary work. +# Measured below, the two populations OVERLAP on volume -- the smallest true +# finding scores under both verified-legitimate fires. So what makes the +# canary livable is the disposition path and the non-blocking posture, not a +# number that separates the shapes. No number does. + +die() { + echo "check-silent-revert: $*" >&2 + exit 2 +} diff --git a/plugins/code-tidying/evals/fixtures/statusline-stamp.sh.txt b/plugins/code-tidying/evals/fixtures/statusline-stamp.sh.txt new file mode 100644 index 0000000000..581039dfc4 --- /dev/null +++ b/plugins/code-tidying/evals/fixtures/statusline-stamp.sh.txt @@ -0,0 +1,17 @@ +# Read one of this writer's epoch-second stamp files into the NAMED variable, or +# 0 when the file is absent, unreadable, empty, or holds anything that is not a +# plain integer. Five call sites read a stamp exactly this way, and the +# validation is the load-bearing half: bash evaluates the TEXT of an arithmetic +# operand, so a stamp shaped like `a[$(cmd)]` would run cmd on every render. +# Naming it once keeps all five spelled identically, the same reason +# _rlg_bash_at_least exists. Builtins throughout, so no call site pays a +# process — every one of them is on the render path. +_rlg_read_stamp() { + local _var="$1" _val=0 + if [[ -f "$2" ]]; then + IFS= read -r _val <"$2" || _val=0 + [[ "$_val" =~ ^[0-9]+$ ]] || _val=0 + fi + printf -v "$_var" '%s' "$_val" + return 0 +} diff --git a/plugins/code-tidying/evals/interaction-dot-aggressive-path/case.yaml b/plugins/code-tidying/evals/interaction-dot-aggressive-path/case.yaml new file mode 100644 index 0000000000..1aefac752c --- /dev/null +++ b/plugins/code-tidying/evals/interaction-dot-aggressive-path/case.yaml @@ -0,0 +1,43 @@ +schema_version: "1.1" +name: interaction-dot-aggressive-path +description: ./aggressive is read as a directory target under the default posture, not as the aggressive mode +tags: [calibration, interaction, invented] +expected_outcome: aggressive/class-c.sh is triaged under default strict; the set-the-counter comment is deleted; the retry rationale, absent from history, is kept; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments ./aggressive + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: class-a-deleted + type: regex + pattern: '#[^\n]*set the counter to zero' + match: not_contains + target: { source: file, path: aggressive/class-c.sh } + - name: retry-rationale-kept + type: regex + pattern: '#[^\n]*upstream API' + target: { source: file, path: aggressive/class-c.sh } + - name: kept-comment-succinct + type: regex + pattern: '(^[ \t]*#(?!!| shellcheck )[^\n]*\n){3}' + flags: m + match: not_contains + target: { source: file, path: aggressive/class-c.sh } + - name: code-intact + type: regex + pattern: '^prepare_lock\(\) \{\n : >"\$lock_file"\n\}[\s\S]*^ local retries=3\n for \(\(attempt = 1; attempt <= retries; attempt\+\+\)\); do\n if curl --silent --max-time "\$timeout" "\$api_url/status"; then\n return 0\n fi\n done\n return 1\n\}[\s\S]*^counter=0$[\s\S]*^timeout=5$[\s\S]*^prepare_lock\nstart_worker\n\nfetch_status \|\| counter=\$\(\(counter \+ 1\)\)\necho "failures: \$counter"$' + flags: m + target: { source: file, path: aggressive/class-c.sh } + - name: path-triaged + type: tool_used + tool: Bash + input_match: aggressive/class-c + arm: both + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/interaction-dot-aggressive-path/scaffold.sh b/plugins/code-tidying/evals/interaction-dot-aggressive-path/scaffold.sh new file mode 100755 index 0000000000..d899fda131 --- /dev/null +++ b/plugins/code-tidying/evals/interaction-dot-aggressive-path/scaffold.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" aggressive/class-c.sh +git -c user.name=eval -c user.email=eval@example.invalid commit --amend -q -m "add fixtures" -m "Uses a 5 second timeout to stay under the load balancer's 6 second idle cutoff." diff --git a/plugins/code-tidying/evals/interaction-non-interactive-no-target/case.yaml b/plugins/code-tidying/evals/interaction-non-interactive-no-target/case.yaml new file mode 100644 index 0000000000..ecc11500ed --- /dev/null +++ b/plugins/code-tidying/evals/interaction-non-interactive-no-target/case.yaml @@ -0,0 +1,54 @@ +schema_version: "1.1" +name: interaction-non-interactive-no-target +description: aggressive with no target in a non-interactive session states its posture, applies no class-C deletion, and does not end on a question +tags: [calibration, interaction, aggressive, invented] +expected_outcome: The run names the rung the ladder resolved and states that it is non-interactive (safe mode, and the explicit-target re-run that would apply the full contract). No class-C comment is deleted, the warning and the retry rationale stay, code is unchanged, and the final message is a report rather than a question to the user. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: warning-kept + type: regex + pattern: '#[^\n]*deadlock' + flags: i + target: { source: file, path: class-c.sh } + - name: retry-rationale-kept + type: regex + pattern: '#[^\n]*upstream API' + target: { source: file, path: class-c.sh } + - name: non-interactive-posture-named + type: regex + pattern: 'non-interactive' + flags: i + target: last_message + - name: rung-reported + type: regex + pattern: 'rung|uncommitted|files=0|branch diff' + flags: i + target: last_message + - name: does-not-end-on-a-question + type: regex + pattern: '(shall I|should I|would you like|do you want|may I) [^\n]*\?' + flags: i + match: not_contains + target: last_message + - name: kept-comment-succinct + type: regex + pattern: '(^[ \t]*#(?!!| shellcheck )[^\n]*\n){3}' + flags: m + match: not_contains + target: { source: file, path: class-c.sh } + - name: code-intact + type: regex + pattern: '^prepare_lock\(\) \{\n : >"\$lock_file"\n\}[\s\S]*^ local retries=3\n for \(\(attempt = 1; attempt <= retries; attempt\+\+\)\); do\n if curl --silent --max-time "\$timeout" "\$api_url/status"; then\n return 0\n fi\n done\n return 1\n\}[\s\S]*^counter=0$[\s\S]*^timeout=5$[\s\S]*^prepare_lock\nstart_worker\n\nfetch_status \|\| counter=\$\(\(counter \+ 1\)\)\necho "failures: \$counter"$' + flags: m + target: { source: file, path: class-c.sh } + - name: scope-resolved + type: tool_used + tool: Bash + input_match: scope-code-files + arm: both diff --git a/plugins/code-tidying/evals/interaction-non-interactive-no-target/scaffold.sh b/plugins/code-tidying/evals/interaction-non-interactive-no-target/scaffold.sh new file mode 100755 index 0000000000..483561a85d --- /dev/null +++ b/plugins/code-tidying/evals/interaction-non-interactive-no-target/scaffold.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-c.sh +git -c user.name=eval -c user.email=eval@example.invalid commit --amend -q -m "add fixtures" -m "Uses a 5 second timeout to stay under the load balancer's 6 second idle cutoff." diff --git a/plugins/code-tidying/evals/interaction-notes-tracked-refused/case.yaml b/plugins/code-tidying/evals/interaction-notes-tracked-refused/case.yaml new file mode 100644 index 0000000000..9b32c476bc --- /dev/null +++ b/plugins/code-tidying/evals/interaction-notes-tracked-refused/case.yaml @@ -0,0 +1,32 @@ +schema_version: "1.1" +name: interaction-notes-tracked-refused +description: --notes pointing at a tracked file is refused and the file is left unchanged +tags: [calibration, interaction, aggressive, invented] +expected_outcome: The run refuses to write notes into the tracked notes/dc-notes.md, says so in the final message, and the file still reads seed notes; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive --notes notes/dc-notes.md class-c.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: notes-not-written + type: regex + pattern: 'upstream API' + match: not_contains + target: { source: file, path: notes/dc-notes.md } + - name: notes-seed-intact + type: regex + pattern: '^seed notes\n$' + target: { source: file, path: notes/dc-notes.md } + - name: refusal-reported + type: regex + pattern: 'refus|tracked' + flags: i + target: last_message + - name: code-intact + type: regex + pattern: '^prepare_lock\(\) \{\n : >"\$lock_file"\n\}[\s\S]*^ local retries=3\n for \(\(attempt = 1; attempt <= retries; attempt\+\+\)\); do\n if curl --silent --max-time "\$timeout" "\$api_url/status"; then\n return 0\n fi\n done\n return 1\n\}[\s\S]*^counter=0$[\s\S]*^timeout=5$[\s\S]*^prepare_lock\nstart_worker\n\nfetch_status \|\| counter=\$\(\(counter \+ 1\)\)\necho "failures: \$counter"$' + flags: m + target: { source: file, path: class-c.sh } diff --git a/plugins/code-tidying/evals/interaction-notes-tracked-refused/scaffold.sh b/plugins/code-tidying/evals/interaction-notes-tracked-refused/scaffold.sh new file mode 100755 index 0000000000..5d51a6dd78 --- /dev/null +++ b/plugins/code-tidying/evals/interaction-notes-tracked-refused/scaffold.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-c.sh notes/dc-notes.md +git -c user.name=eval -c user.email=eval@example.invalid commit --amend -q -m "add fixtures" -m "Uses a 5 second timeout to stay under the load balancer's 6 second idle cutoff." diff --git a/plugins/code-tidying/evals/interaction-notes-untracked/case.yaml b/plugins/code-tidying/evals/interaction-notes-untracked/case.yaml new file mode 100644 index 0000000000..a197dce361 --- /dev/null +++ b/plugins/code-tidying/evals/interaction-notes-untracked/case.yaml @@ -0,0 +1,37 @@ +schema_version: "1.1" +name: interaction-notes-untracked +description: --notes pointing into an untracked directory writes the staged narrative block to that file +tags: [calibration, interaction, aggressive, invented] +expected_outcome: notes/dc-notes.md is created and carries the staged rationale for the removed retry and timeout comments; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive --notes notes/dc-notes.md class-c.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: notes-file-created + type: file_exists + path: notes/dc-notes.md + - name: notes-carry-staged-block + type: regex + pattern: 'upstream API|load balancer' + flags: i + target: { source: file, path: notes/dc-notes.md } + - name: kept-comment-succinct + type: regex + pattern: '(^[ \t]*#(?!!| shellcheck )[^\n]*\n){3}' + flags: m + match: not_contains + target: { source: file, path: class-c.sh } + - name: code-intact + type: regex + pattern: '^prepare_lock\(\) \{\n : >"\$lock_file"\n\}[\s\S]*^ local retries=3\n for \(\(attempt = 1; attempt <= retries; attempt\+\+\)\); do\n if curl --silent --max-time "\$timeout" "\$api_url/status"; then\n return 0\n fi\n done\n return 1\n\}[\s\S]*^counter=0$[\s\S]*^timeout=5$[\s\S]*^prepare_lock\nstart_worker\n\nfetch_status \|\| counter=\$\(\(counter \+ 1\)\)\necho "failures: \$counter"$' + flags: m + target: { source: file, path: class-c.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/interaction-notes-untracked/scaffold.sh b/plugins/code-tidying/evals/interaction-notes-untracked/scaffold.sh new file mode 100755 index 0000000000..58a99ab077 --- /dev/null +++ b/plugins/code-tidying/evals/interaction-notes-untracked/scaffold.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-c.sh +git -c user.name=eval -c user.email=eval@example.invalid commit --amend -q -m "add fixtures" -m "Uses a 5 second timeout to stay under the load balancer's 6 second idle cutoff." +mkdir -p notes diff --git a/plugins/code-tidying/evals/interaction-safe-aggressive/case.yaml b/plugins/code-tidying/evals/interaction-safe-aggressive/case.yaml new file mode 100644 index 0000000000..f98e8f1157 --- /dev/null +++ b/plugins/code-tidying/evals/interaction-safe-aggressive/case.yaml @@ -0,0 +1,47 @@ +schema_version: "1.1" +name: interaction-safe-aggressive +description: safe combined with aggressive on the class-C script applies only the class-A deletion +tags: [calibration, interaction, aggressive, invented] +expected_outcome: safe wins over aggressive, so only the set-the-counter comment is deleted; the warning and both rationale comments stay; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments safe aggressive class-c.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: class-a-deleted + type: regex + pattern: '#[^\n]*set the counter to zero' + match: not_contains + target: { source: file, path: class-c.sh } + - name: warning-kept + type: regex + pattern: '#[^\n]*deadlock' + flags: i + target: { source: file, path: class-c.sh } + - name: retry-rationale-kept + type: regex + pattern: '#[^\n]*upstream API' + target: { source: file, path: class-c.sh } + - name: timeout-rationale-kept + type: regex + pattern: '#[^\n]*load balancer' + target: { source: file, path: class-c.sh } + - name: kept-comment-succinct + type: regex + pattern: '(^[ \t]*#(?!!| shellcheck )[^\n]*\n){3}' + flags: m + match: not_contains + target: { source: file, path: class-c.sh } + - name: code-intact + type: regex + pattern: '^prepare_lock\(\) \{\n : >"\$lock_file"\n\}[\s\S]*^ local retries=3\n for \(\(attempt = 1; attempt <= retries; attempt\+\+\)\); do\n if curl --silent --max-time "\$timeout" "\$api_url/status"; then\n return 0\n fi\n done\n return 1\n\}[\s\S]*^counter=0$[\s\S]*^timeout=5$[\s\S]*^prepare_lock\nstart_worker\n\nfetch_status \|\| counter=\$\(\(counter \+ 1\)\)\necho "failures: \$counter"$' + flags: m + target: { source: file, path: class-c.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/interaction-safe-aggressive/scaffold.sh b/plugins/code-tidying/evals/interaction-safe-aggressive/scaffold.sh new file mode 100755 index 0000000000..483561a85d --- /dev/null +++ b/plugins/code-tidying/evals/interaction-safe-aggressive/scaffold.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-c.sh +git -c user.name=eval -c user.email=eval@example.invalid commit --amend -q -m "add fixtures" -m "Uses a 5 second timeout to stay under the load balancer's 6 second idle cutoff." diff --git a/plugins/code-tidying/evals/interaction-safe-strip/case.yaml b/plugins/code-tidying/evals/interaction-safe-strip/case.yaml new file mode 100644 index 0000000000..809c21bad7 --- /dev/null +++ b/plugins/code-tidying/evals/interaction-safe-strip/case.yaml @@ -0,0 +1,47 @@ +schema_version: "1.1" +name: interaction-safe-strip +description: safe combined with strip on the class-C script applies only the class-A deletion +tags: [calibration, interaction, strip, invented] +expected_outcome: safe wins over strip, so only the set-the-counter comment is deleted; the warning and both rationale comments stay; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments safe strip class-c.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: class-a-deleted + type: regex + pattern: '#[^\n]*set the counter to zero' + match: not_contains + target: { source: file, path: class-c.sh } + - name: warning-kept + type: regex + pattern: '#[^\n]*deadlock' + flags: i + target: { source: file, path: class-c.sh } + - name: retry-rationale-kept + type: regex + pattern: '#[^\n]*upstream API' + target: { source: file, path: class-c.sh } + - name: timeout-rationale-kept + type: regex + pattern: '#[^\n]*load balancer' + target: { source: file, path: class-c.sh } + - name: kept-comment-succinct + type: regex + pattern: '(^[ \t]*#(?!!| shellcheck )[^\n]*\n){3}' + flags: m + match: not_contains + target: { source: file, path: class-c.sh } + - name: code-intact + type: regex + pattern: '^prepare_lock\(\) \{\n : >"\$lock_file"\n\}[\s\S]*^ local retries=3\n for \(\(attempt = 1; attempt <= retries; attempt\+\+\)\); do\n if curl --silent --max-time "\$timeout" "\$api_url/status"; then\n return 0\n fi\n done\n return 1\n\}[\s\S]*^counter=0$[\s\S]*^timeout=5$[\s\S]*^prepare_lock\nstart_worker\n\nfetch_status \|\| counter=\$\(\(counter \+ 1\)\)\necho "failures: \$counter"$' + flags: m + target: { source: file, path: class-c.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/interaction-safe-strip/scaffold.sh b/plugins/code-tidying/evals/interaction-safe-strip/scaffold.sh new file mode 100755 index 0000000000..483561a85d --- /dev/null +++ b/plugins/code-tidying/evals/interaction-safe-strip/scaffold.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-c.sh +git -c user.name=eval -c user.email=eval@example.invalid commit --amend -q -m "add fixtures" -m "Uses a 5 second timeout to stay under the load balancer's 6 second idle cutoff." diff --git a/plugins/code-tidying/evals/interaction-unprovable-excerpt/case.yaml b/plugins/code-tidying/evals/interaction-unprovable-excerpt/case.yaml new file mode 100644 index 0000000000..c7f77a79ef --- /dev/null +++ b/plugins/code-tidying/evals/interaction-unprovable-excerpt/case.yaml @@ -0,0 +1,42 @@ +schema_version: "1.1" +name: interaction-unprovable-excerpt +description: aggressive on a real lib/hook-utils.sh excerpt that change-shape cannot parse names the file UNPROVABLE up front and applies no deletion +tags: [calibration, interaction, aggressive, real] +expected_outcome: The self-parse check runs before the census and reports the file as UNPROVABLE first; every comment is left in place and any removal is only proposed; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive hook-utils-unprovable.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: unprovable-named + type: regex + pattern: 'UNPROVABLE' + target: last_message + - name: signature-comment-kept + type: regex + pattern: '#[^\n]*hook::resolve_read_timeout_to ' + target: { source: file, path: hook-utils-unprovable.sh } + - name: fork-rationale-kept + type: regex + pattern: '#[^\n]*startup fork on every hook' + target: { source: file, path: hook-utils-unprovable.sh } + - name: spelling-pin-kept + type: regex + pattern: '#[^\n]*buffer_stdin uses it' + target: { source: file, path: hook-utils-unprovable.sh } + - name: code-intact + type: regex + pattern: 'hook::resolve_read_timeout_to\(\) \{\n local __hu_dest="\$1"[\s\S]* local micros=\$\(\(10#\$whole \* 1000000 \+ 10#\$frac\)\)[\s\S]* printf -v "\$__hu_dest" ''%s'' "\$__hu_t"\n\}' + target: { source: file, path: hook-utils-unprovable.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both + - name: self-parse-before-census + type: tool_order + before: { tool: Bash, input_match: change-shape } + after: { tool: Bash, input_match: comment-census } diff --git a/plugins/code-tidying/evals/interaction-unprovable-excerpt/scaffold.sh b/plugins/code-tidying/evals/interaction-unprovable-excerpt/scaffold.sh new file mode 100755 index 0000000000..9d6e2e6c4b --- /dev/null +++ b/plugins/code-tidying/evals/interaction-unprovable-excerpt/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" hook-utils-unprovable.sh diff --git a/plugins/code-tidying/evals/invented-class-a-aggressive/case.yaml b/plugins/code-tidying/evals/invented-class-a-aggressive/case.yaml new file mode 100644 index 0000000000..e377c38bbe --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-a-aggressive/case.yaml @@ -0,0 +1,37 @@ +schema_version: "1.1" +name: invented-class-a-aggressive +description: aggressive on an invented script deletes a restating comment and a commented-out code block and keeps the shebang +tags: [calibration, aggressive, invented] +expected_outcome: The increment-counter comment and both commented-out lines are deleted; the shebang stays; every code line is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive class-a.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: restating-deleted + type: regex + pattern: '#[^\n]*increment counter' + match: not_contains + target: { source: file, path: class-a.sh } + - name: dead-code-deleted + type: regex + pattern: '#[^\n]*old_total' + match: not_contains + target: { source: file, path: class-a.sh } + - name: shebang-kept + type: regex + pattern: '^#!/usr/bin/env bash\n' + target: { source: file, path: class-a.sh } + - name: code-intact + type: regex + pattern: '^counter=0\ntotal=0\n\nfor value in "\$@"; do\n total=\$\(\(total \+ value\)\)$[\s\S]*^ counter=\$\(\(counter \+ 1\)\)\ndone$[\s\S]*^echo "count=\$counter total=\$total"$' + flags: m + target: { source: file, path: class-a.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/invented-class-a-aggressive/scaffold.sh b/plugins/code-tidying/evals/invented-class-a-aggressive/scaffold.sh new file mode 100755 index 0000000000..af69f30f0b --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-a-aggressive/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-a.sh diff --git a/plugins/code-tidying/evals/invented-class-a-strip/case.yaml b/plugins/code-tidying/evals/invented-class-a-strip/case.yaml new file mode 100644 index 0000000000..27e2b558aa --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-a-strip/case.yaml @@ -0,0 +1,37 @@ +schema_version: "1.1" +name: invented-class-a-strip +description: strip on an invented script deletes a restating comment and a commented-out code block and keeps the shebang +tags: [calibration, strip, invented] +expected_outcome: The increment-counter comment and both commented-out lines are deleted; the shebang stays; every code line is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments strip class-a.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: restating-deleted + type: regex + pattern: '#[^\n]*increment counter' + match: not_contains + target: { source: file, path: class-a.sh } + - name: dead-code-deleted + type: regex + pattern: '#[^\n]*old_total' + match: not_contains + target: { source: file, path: class-a.sh } + - name: shebang-kept + type: regex + pattern: '^#!/usr/bin/env bash\n' + target: { source: file, path: class-a.sh } + - name: code-intact + type: regex + pattern: '^counter=0\ntotal=0\n\nfor value in "\$@"; do\n total=\$\(\(total \+ value\)\)$[\s\S]*^ counter=\$\(\(counter \+ 1\)\)\ndone$[\s\S]*^echo "count=\$counter total=\$total"$' + flags: m + target: { source: file, path: class-a.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/invented-class-a-strip/scaffold.sh b/plugins/code-tidying/evals/invented-class-a-strip/scaffold.sh new file mode 100755 index 0000000000..af69f30f0b --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-a-strip/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-a.sh diff --git a/plugins/code-tidying/evals/invented-class-b-aggressive/case.yaml b/plugins/code-tidying/evals/invented-class-b-aggressive/case.yaml new file mode 100644 index 0000000000..5c2cb493e0 --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-b-aggressive/case.yaml @@ -0,0 +1,46 @@ +schema_version: "1.1" +name: invented-class-b-aggressive +description: aggressive on an invented script with a Makefile test net dissolves a magic-literal comment and a vague-local comment into names +tags: [calibration, aggressive, invented] +expected_outcome: 86400 becomes a SECONDS_PER_DAY name and n becomes display_name behind the discovered make test net, then both comments are deleted; the untouched lines are unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive class-b.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: magic-literal-named + type: regex + pattern: 'seconds_per_day' + flags: i + target: { source: file, path: class-b.sh } + - name: vague-local-renamed + type: regex + pattern: 'display_name' + target: { source: file, path: class-b.sh } + - name: seconds-comment-deleted + type: regex + pattern: '#[^\n]*seconds per day' + match: not_contains + target: { source: file, path: class-b.sh } + - name: display-name-comment-deleted + type: regex + pattern: '#[^\n]*display name' + match: not_contains + target: { source: file, path: class-b.sh } + - name: code-intact + type: regex + pattern: 'cache_ttl_seconds\(\) \{\n local days="\$1"\n' + target: { source: file, path: class-b.sh } + - name: test-net-ran + type: tool_used + tool: Bash + input_match: make test|class-b\.test + arm: both + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/invented-class-b-aggressive/scaffold.sh b/plugins/code-tidying/evals/invented-class-b-aggressive/scaffold.sh new file mode 100755 index 0000000000..91a2a96e79 --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-b-aggressive/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-b.sh class-b.test.sh Makefile diff --git a/plugins/code-tidying/evals/invented-class-b-strip/case.yaml b/plugins/code-tidying/evals/invented-class-b-strip/case.yaml new file mode 100644 index 0000000000..5aff2b4bee --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-b-strip/case.yaml @@ -0,0 +1,38 @@ +schema_version: "1.1" +name: invented-class-b-strip +description: strip on an invented script with a Makefile test net deletes the magic-literal and vague-local comments without renaming anything +tags: [calibration, strip, invented] +expected_outcome: Both comments are deleted; the 86400 and n= lines are byte-identical; no SECONDS_PER_DAY name is introduced. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments strip class-b.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: seconds-comment-deleted + type: regex + pattern: '#[^\n]*seconds per day' + match: not_contains + target: { source: file, path: class-b.sh } + - name: display-name-comment-deleted + type: regex + pattern: '#[^\n]*display name' + match: not_contains + target: { source: file, path: class-b.sh } + - name: code-intact + type: regex + pattern: '^ local ttl=\$\(\(days \* 86400\)\)$[\s\S]*^ local n="\$1"\n echo "Hello, \$n"$' + flags: m + target: { source: file, path: class-b.sh } + - name: no-constant-introduced + type: regex + pattern: 'SECONDS_PER_DAY' + match: not_contains + target: { source: file, path: class-b.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/invented-class-b-strip/scaffold.sh b/plugins/code-tidying/evals/invented-class-b-strip/scaffold.sh new file mode 100755 index 0000000000..91a2a96e79 --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-b-strip/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-b.sh class-b.test.sh Makefile diff --git a/plugins/code-tidying/evals/invented-class-c-aggressive/case.yaml b/plugins/code-tidying/evals/invented-class-c-aggressive/case.yaml new file mode 100644 index 0000000000..3f39dea3ce --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-c-aggressive/case.yaml @@ -0,0 +1,63 @@ +schema_version: "1.1" +name: invented-class-c-aggressive +description: aggressive on an invented script keeps a load-bearing warning and deletes two rationale comments and a restating comment with the narrative staged for the next commit +tags: [calibration, aggressive, invented] +expected_outcome: The lock-deadlock warning survives in at most two lines; the retry rationale (absent from history), the timeout rationale (in the commit message), and the set-the-counter comment are deleted; both rationales are staged and the report names the next commit as their landing place; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive class-c.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: warning-kept + type: regex + pattern: '#[^\n]*deadlock' + flags: i + target: { source: file, path: class-c.sh } + - name: retry-rationale-deleted + type: regex + pattern: '#[^\n]*upstream API' + match: not_contains + target: { source: file, path: class-c.sh } + - name: timeout-rationale-deleted + type: regex + pattern: '#[^\n]*load balancer' + match: not_contains + target: { source: file, path: class-c.sh } + - name: class-a-deleted + type: regex + pattern: '#[^\n]*set the counter to zero' + match: not_contains + target: { source: file, path: class-c.sh } + - name: kept-comment-succinct + type: regex + pattern: '(^[ \t]*#(?!!| shellcheck )[^\n]*\n){3}' + flags: m + match: not_contains + target: { source: file, path: class-c.sh } + - name: code-intact + type: regex + pattern: '^prepare_lock\(\) \{\n : >"\$lock_file"\n\}[\s\S]*^ local retries=3\n for \(\(attempt = 1; attempt <= retries; attempt\+\+\)\); do\n if curl --silent --max-time "\$timeout" "\$api_url/status"; then\n return 0\n fi\n done\n return 1\n\}[\s\S]*^counter=0$[\s\S]*^timeout=5$[\s\S]*^prepare_lock\nstart_worker\n\nfetch_status \|\| counter=\$\(\(counter \+ 1\)\)\necho "failures: \$counter"$' + flags: m + target: { source: file, path: class-c.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both + - name: landing-place-named + type: regex + pattern: 'next commit' + flags: i + target: last_message + - name: no-intentional-removal-trailer + type: regex + pattern: '^[ \t]*Intentional-removal:' + flags: m + match: not_contains + target: last_message + - name: narrative-staged + type: llm + criteria: PASS when the final report contains a proposed commit-message block that preserves both removed rationales, namely that fetch_status retries three times because the upstream API drops the first connection after idle, and that the 5 second timeout stays under the load balancer's 6 second idle cutoff. FAIL when either rationale appears nowhere in the report. diff --git a/plugins/code-tidying/evals/invented-class-c-aggressive/scaffold.sh b/plugins/code-tidying/evals/invented-class-c-aggressive/scaffold.sh new file mode 100755 index 0000000000..483561a85d --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-c-aggressive/scaffold.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-c.sh +git -c user.name=eval -c user.email=eval@example.invalid commit --amend -q -m "add fixtures" -m "Uses a 5 second timeout to stay under the load balancer's 6 second idle cutoff." diff --git a/plugins/code-tidying/evals/invented-class-c-strip/case.yaml b/plugins/code-tidying/evals/invented-class-c-strip/case.yaml new file mode 100644 index 0000000000..64ffb901c9 --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-c-strip/case.yaml @@ -0,0 +1,36 @@ +schema_version: "1.1" +name: invented-class-c-strip +description: strip on an invented script deletes the warning, both rationale comments, and the restating comment, and stages the narrative +tags: [calibration, strip, invented] +expected_outcome: Every comment but the shebang is deleted; code is unchanged; the report stages the lock-deadlock warning and both rationales. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments strip class-c.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: no-comments-left + type: regex + pattern: '^[ \t]*#(?!!)' + flags: m + match: not_contains + target: { source: file, path: class-c.sh } + - name: shebang-kept + type: regex + pattern: '^#!/usr/bin/env bash\n' + target: { source: file, path: class-c.sh } + - name: code-intact + type: regex + pattern: '^prepare_lock\(\) \{\n : >"\$lock_file"\n\}[\s\S]*^ local retries=3\n for \(\(attempt = 1; attempt <= retries; attempt\+\+\)\); do\n if curl --silent --max-time "\$timeout" "\$api_url/status"; then\n return 0\n fi\n done\n return 1\n\}[\s\S]*^counter=0$[\s\S]*^timeout=5$[\s\S]*^prepare_lock\nstart_worker\n\nfetch_status \|\| counter=\$\(\(counter \+ 1\)\)\necho "failures: \$counter"$' + flags: m + target: { source: file, path: class-c.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both + - name: narrative-staged + type: llm + criteria: PASS when the final report contains a proposed commit-message block that preserves the removed information, including the warning that prepare_lock must run before start_worker or the lock file deadlocks the worker. FAIL when that warning appears nowhere in the report. diff --git a/plugins/code-tidying/evals/invented-class-c-strip/scaffold.sh b/plugins/code-tidying/evals/invented-class-c-strip/scaffold.sh new file mode 100755 index 0000000000..483561a85d --- /dev/null +++ b/plugins/code-tidying/evals/invented-class-c-strip/scaffold.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" class-c.sh +git -c user.name=eval -c user.email=eval@example.invalid commit --amend -q -m "add fixtures" -m "Uses a 5 second timeout to stay under the load balancer's 6 second idle cutoff." diff --git a/plugins/code-tidying/evals/invented-edge-aggressive/case.yaml b/plugins/code-tidying/evals/invented-edge-aggressive/case.yaml new file mode 100644 index 0000000000..32f394a0ad --- /dev/null +++ b/plugins/code-tidying/evals/invented-edge-aggressive/case.yaml @@ -0,0 +1,51 @@ +schema_version: "1.1" +name: invented-edge-aggressive +description: aggressive on an invented src tree deletes a comment above a marker-listed identifier without renaming it, proposes a private docstring, and keeps a comment paired with a regression test +tags: [calibration, aggressive, invented] +expected_outcome: The end-offset comment is deleted and gql_page_end is never renamed because scripts/restoration-markers.txt lists it; the _parse docstring is unchanged and reported as a proposal; the empty-input comment paired with tests/edge-paired.test.sh stays; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive src + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: marker-comment-deleted + type: regex + pattern: '#[^\n]*end offset of the current page' + match: not_contains + target: { source: file, path: src/edge-marker.sh } + - name: marker-identifier-unchanged + type: regex + pattern: '^ gql_page_end=\$\(\(offset \+ page_size\)\)$' + flags: m + target: { source: file, path: src/edge-marker.sh } + - name: docstring-unchanged + type: regex + pattern: 'def _parse\(line\):\n """Split a header line into key and value\."""\n key, _, value = line\.partition\(":"\)' + target: { source: file, path: src/edge_docstring.py } + - name: docstring-reported + type: regex + pattern: 'docstring' + flags: i + target: last_message + - name: paired-comment-kept + type: regex + pattern: '#[^\n]*Empty input' + target: { source: file, path: src/edge-paired.sh } + - name: kept-comment-succinct + type: regex + pattern: '(^[ \t]*#(?!!| shellcheck )[^\n]*\n){3}' + flags: m + match: not_contains + target: { source: file, path: src/edge-paired.sh } + - name: code-intact + type: regex + pattern: '\[\[ -n "\$\{1:-\}" \]\] \|\| return 1\n return 0\n\}' + target: { source: file, path: src/edge-paired.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/invented-edge-aggressive/scaffold.sh b/plugins/code-tidying/evals/invented-edge-aggressive/scaffold.sh new file mode 100755 index 0000000000..71610d9c2f --- /dev/null +++ b/plugins/code-tidying/evals/invented-edge-aggressive/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" scripts/restoration-markers.txt scripts/check-markers.sh src/edge-marker.sh src/edge_docstring.py src/edge-paired.sh tests/edge-paired.test.sh diff --git a/plugins/code-tidying/evals/invented-edge-strip/case.yaml b/plugins/code-tidying/evals/invented-edge-strip/case.yaml new file mode 100644 index 0000000000..e85bd43ba8 --- /dev/null +++ b/plugins/code-tidying/evals/invented-edge-strip/case.yaml @@ -0,0 +1,51 @@ +schema_version: "1.1" +name: invented-edge-strip +description: strip on an invented src tree deletes a comment above a marker-listed identifier without renaming it, proposes a private docstring, and keeps a comment paired with a regression test +tags: [calibration, strip, invented] +expected_outcome: The end-offset comment is deleted and gql_page_end is never renamed because scripts/restoration-markers.txt lists it; the _parse docstring is unchanged and reported as a proposal; the empty-input comment paired with tests/edge-paired.test.sh stays; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments strip src + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: marker-comment-deleted + type: regex + pattern: '#[^\n]*end offset of the current page' + match: not_contains + target: { source: file, path: src/edge-marker.sh } + - name: marker-identifier-unchanged + type: regex + pattern: '^ gql_page_end=\$\(\(offset \+ page_size\)\)$' + flags: m + target: { source: file, path: src/edge-marker.sh } + - name: docstring-unchanged + type: regex + pattern: 'def _parse\(line\):\n """Split a header line into key and value\."""\n key, _, value = line\.partition\(":"\)' + target: { source: file, path: src/edge_docstring.py } + - name: docstring-reported + type: regex + pattern: 'docstring' + flags: i + target: last_message + - name: paired-comment-kept + type: regex + pattern: '#[^\n]*Empty input' + target: { source: file, path: src/edge-paired.sh } + - name: kept-comment-succinct + type: regex + pattern: '(^[ \t]*#(?!!| shellcheck )[^\n]*\n){3}' + flags: m + match: not_contains + target: { source: file, path: src/edge-paired.sh } + - name: code-intact + type: regex + pattern: '\[\[ -n "\$\{1:-\}" \]\] \|\| return 1\n return 0\n\}' + target: { source: file, path: src/edge-paired.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/invented-edge-strip/scaffold.sh b/plugins/code-tidying/evals/invented-edge-strip/scaffold.sh new file mode 100755 index 0000000000..71610d9c2f --- /dev/null +++ b/plugins/code-tidying/evals/invented-edge-strip/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" scripts/restoration-markers.txt scripts/check-markers.sh src/edge-marker.sh src/edge_docstring.py src/edge-paired.sh tests/edge-paired.test.sh diff --git a/plugins/code-tidying/evals/invented-exempt-aggressive/case.yaml b/plugins/code-tidying/evals/invented-exempt-aggressive/case.yaml new file mode 100644 index 0000000000..670ed3ad2c --- /dev/null +++ b/plugins/code-tidying/evals/invented-exempt-aggressive/case.yaml @@ -0,0 +1,49 @@ +schema_version: "1.1" +name: invented-exempt-aggressive +description: aggressive on an invented Python module keeps every exempt surface and deletes the one ordinary rationale comment +tags: [calibration, aggressive, invented] +expected_outcome: The shebang, the two-line license header, the noqa justification, the TODO(#12) marker, and the dissolve-comments-ignore line with the comment it covers all stay; the arbitrary-order rationale is deleted; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive exempt.py + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: shebang-kept + type: regex + pattern: '^#!/usr/bin/env python3\n' + target: { source: file, path: exempt.py } + - name: license-kept + type: regex + pattern: '^# Copyright \(c\) 2026 Example Corp\.\n# Licensed under the MIT License\. See LICENSE in the project root\.$' + flags: m + target: { source: file, path: exempt.py } + - name: noqa-kept + type: regex + pattern: '# noqa: E501 long canonical URL' + target: { source: file, path: exempt.py } + - name: todo-kept + type: regex + pattern: '# TODO\(#12\): replace polling with a webhook' + target: { source: file, path: exempt.py } + - name: ignore-marker-kept + type: regex + pattern: '# dissolve-comments-ignore\n return min\(2\*\*attempt, POLL_SECONDS\) # capped so a retry never outlasts one poll interval' + target: { source: file, path: exempt.py } + - name: rationale-deleted + type: regex + pattern: '#[^\n]*arbitrary order' + match: not_contains + target: { source: file, path: exempt.py } + - name: code-intact + type: regex + pattern: '^ pages = client\.get\(URL\)$[\s\S]*^ pages\.sort\(key=lambda page: page\["updated_at"\]\)\n return pages$' + flags: m + target: { source: file, path: exempt.py } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/invented-exempt-aggressive/scaffold.sh b/plugins/code-tidying/evals/invented-exempt-aggressive/scaffold.sh new file mode 100755 index 0000000000..af2235976d --- /dev/null +++ b/plugins/code-tidying/evals/invented-exempt-aggressive/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" exempt.py diff --git a/plugins/code-tidying/evals/invented-exempt-strip/case.yaml b/plugins/code-tidying/evals/invented-exempt-strip/case.yaml new file mode 100644 index 0000000000..e9bd050ce9 --- /dev/null +++ b/plugins/code-tidying/evals/invented-exempt-strip/case.yaml @@ -0,0 +1,49 @@ +schema_version: "1.1" +name: invented-exempt-strip +description: strip on an invented Python module keeps every exempt surface and deletes the one ordinary rationale comment +tags: [calibration, strip, invented] +expected_outcome: The shebang, the two-line license header, the noqa justification, the TODO(#12) marker, and the dissolve-comments-ignore line with the comment it covers all stay; the arbitrary-order rationale is deleted; code is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments strip exempt.py + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: shebang-kept + type: regex + pattern: '^#!/usr/bin/env python3\n' + target: { source: file, path: exempt.py } + - name: license-kept + type: regex + pattern: '^# Copyright \(c\) 2026 Example Corp\.\n# Licensed under the MIT License\. See LICENSE in the project root\.$' + flags: m + target: { source: file, path: exempt.py } + - name: noqa-kept + type: regex + pattern: '# noqa: E501 long canonical URL' + target: { source: file, path: exempt.py } + - name: todo-kept + type: regex + pattern: '# TODO\(#12\): replace polling with a webhook' + target: { source: file, path: exempt.py } + - name: ignore-marker-kept + type: regex + pattern: '# dissolve-comments-ignore\n return min\(2\*\*attempt, POLL_SECONDS\) # capped so a retry never outlasts one poll interval' + target: { source: file, path: exempt.py } + - name: rationale-deleted + type: regex + pattern: '#[^\n]*arbitrary order' + match: not_contains + target: { source: file, path: exempt.py } + - name: code-intact + type: regex + pattern: '^ pages = client\.get\(URL\)$[\s\S]*^ pages\.sort\(key=lambda page: page\["updated_at"\]\)\n return pages$' + flags: m + target: { source: file, path: exempt.py } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/invented-exempt-strip/scaffold.sh b/plugins/code-tidying/evals/invented-exempt-strip/scaffold.sh new file mode 100755 index 0000000000..af2235976d --- /dev/null +++ b/plugins/code-tidying/evals/invented-exempt-strip/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" exempt.py diff --git a/plugins/code-tidying/evals/probe-environment/case.yaml b/plugins/code-tidying/evals/probe-environment/case.yaml new file mode 100644 index 0000000000..a01316d6bd --- /dev/null +++ b/plugins/code-tidying/evals/probe-environment/case.yaml @@ -0,0 +1,15 @@ +schema_version: "1.1" +name: probe-environment +description: Confirms the eval run resolves a python3 with the comment-tooling wheels and a make binary +tags: [probe] +runs: 1 +execution: + prompt: "Run this exact command with the Bash tool and report its full output verbatim: command -v python3; python3 -c 'import pygments, tree_sitter, tree_sitter_bash' && echo imports-$((40+2)); command -v make; echo PATH=$PATH" + allowed_tools: [Bash] + max_turns: 5 + timeout_seconds: 180 +graders: + - name: wheels-import + type: regex + pattern: imports-42 + target: trace diff --git a/plugins/code-tidying/evals/probe-explicit-target/case.yaml b/plugins/code-tidying/evals/probe-explicit-target/case.yaml new file mode 100644 index 0000000000..ad4ea1ea13 --- /dev/null +++ b/plugins/code-tidying/evals/probe-explicit-target/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: probe-explicit-target +description: Confirms the slash invocation fires dissolve-comments, its proof script runs, and file graders read the edited file +tags: [probe] +runs: 1 +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments app.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: class-a-deleted + type: regex + pattern: "#[^\\n]*increment counter" + match: not_contains + target: { source: file, path: app.sh } + - name: directive-kept + type: regex + pattern: "# shellcheck disable=SC2034" + target: { source: file, path: app.sh } + - name: code-kept + type: regex + pattern: "counter=\\$\\(\\(counter \\+ 1\\)\\)" + target: { source: file, path: app.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both diff --git a/plugins/code-tidying/evals/probe-explicit-target/fixture/app.sh.txt b/plugins/code-tidying/evals/probe-explicit-target/fixture/app.sh.txt new file mode 100755 index 0000000000..da0c9e46a8 --- /dev/null +++ b/plugins/code-tidying/evals/probe-explicit-target/fixture/app.sh.txt @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2034 +unused_flag=1 +counter=0 +# increment counter +counter=$((counter + 1)) +echo "$counter" diff --git a/plugins/code-tidying/evals/probe-explicit-target/scaffold.sh b/plugins/code-tidying/evals/probe-explicit-target/scaffold.sh new file mode 100755 index 0000000000..1314a4bd81 --- /dev/null +++ b/plugins/code-tidying/evals/probe-explicit-target/scaffold.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Seeds the eval workspace with app.sh and one commit. +set -euo pipefail + +cp "$(dirname "${BASH_SOURCE[0]}")/fixture/app.sh.txt" app.sh +git init -q +git add app.sh +git -c user.name=eval -c user.email=eval@example.invalid commit -q -m "add app" diff --git a/plugins/code-tidying/evals/real-hook-utils-header-aggressive/case.yaml b/plugins/code-tidying/evals/real-hook-utils-header-aggressive/case.yaml new file mode 100644 index 0000000000..a3e81cc960 --- /dev/null +++ b/plugins/code-tidying/evals/real-hook-utils-header-aggressive/case.yaml @@ -0,0 +1,86 @@ +schema_version: "1.1" +name: real-hook-utils-header-aggressive +description: aggressive on the lib/hook-utils.sh header keeps the directive, a one-line SSOT warning, and a one-line _to calling convention, and deletes the rest +tags: [calibration, aggressive, real] +expected_outcome: The shellcheck directive stays on line 1; the SSOT warning, the _to calling convention, and the kill switch's required placement each survive within the line budget; the library description, fork-cost reasoning, five-printer list, double-sourcing comment, no-fleet-switch rationale, and the hook::is_enabled design rationale are deleted with their narrative staged; the guard and both function bodies are unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive hook-utils-header.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: directive-kept + type: regex + pattern: '^# shellcheck shell=bash\n' + target: { source: file, path: hook-utils-header.sh } + - name: ssot-warning-kept + type: regex + pattern: '#[^\n]*(SSOT|single source)' + flags: i + target: { source: file, path: hook-utils-header.sh } + - name: ssot-sync-kept + type: regex + pattern: '#[^\n]*sync-hook-utils' + target: { source: file, path: hook-utils-header.sh } + - name: calling-convention-kept + type: regex + pattern: '#[^\n]*_to' + target: { source: file, path: hook-utils-header.sh } + - name: description-deleted + type: regex + pattern: '#[^\n]*(hook utility library|additionalContext accumulator|telemetry envelope|kill switch, file_path parsing)' + flags: i + match: not_contains + target: { source: file, path: hook-utils-header.sh } + - name: fork-cost-deleted + type: regex + pattern: '#[^\n]*(CreateProcess|copy-on-write|forks a subshell)' + match: not_contains + target: { source: file, path: hook-utils-header.sh } + - name: print-helpers-list-deleted + type: regex + pattern: '#[^\n]*Five helpers still print' + match: not_contains + target: { source: file, path: hook-utils-header.sh } + - name: double-sourcing-deleted + type: regex + pattern: '#[^\n]*double-sourcing' + flags: i + match: not_contains + target: { source: file, path: hook-utils-header.sh } + - name: kill-switch-placement-kept + type: regex + pattern: '#[^\n]*(place after source|before stdin parsing)' + flags: i + target: { source: file, path: hook-utils-header.sh } + - name: fleet-switch-rationale-deleted + type: regex + pattern: '#[^\n]*(safe-mode|disableAllHooks|fleet)' + match: not_contains + target: { source: file, path: hook-utils-header.sh } + - name: is-enabled-comment-deleted + type: regex + pattern: '#[^\n]*(transparent wrapper|statusline|predicate)' + flags: i + match: not_contains + target: { source: file, path: hook-utils-header.sh } + - name: kept-comment-succinct + type: regex + pattern: '(^[ \t]*#(?!!| shellcheck )[^\n]{2,}\n){3}' + flags: m + match: not_contains + target: { source: file, path: hook-utils-header.sh } + - name: code-intact + type: regex + pattern: '\[\[ -n "\$\{_HOOK_UTILS_LOADED:-\}" \]\] && return 0\nreadonly _HOOK_UTILS_LOADED=1[\s\S]*hook::is_enabled\(\) \{\n local var_name="CLAUDE_PLUGIN_OPTION_\$\{1\}_ENABLED"\n \[\[ "\$\{!var_name:-true\}" == "true" \]\]\n\}\n\nhook::check_enabled\(\) \{\n hook::is_enabled "\$1" \|\| exit 0\n\}' + target: { source: file, path: hook-utils-header.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both + - name: narrative-staged + type: llm + criteria: PASS when the final report contains a proposed commit-message block that preserves the removed rationale, including that a command substitution forks a subshell (a Win32 CreateProcess on Git Bash) so a capture around a _to helper is pure loss, and that the statusline tee needs hook::is_enabled because it is a transparent wrapper that must still pass output through when disabled. FAIL when either reason appears nowhere in the report. diff --git a/plugins/code-tidying/evals/real-hook-utils-header-aggressive/scaffold.sh b/plugins/code-tidying/evals/real-hook-utils-header-aggressive/scaffold.sh new file mode 100755 index 0000000000..8528871210 --- /dev/null +++ b/plugins/code-tidying/evals/real-hook-utils-header-aggressive/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" hook-utils-header.sh diff --git a/plugins/code-tidying/evals/real-hook-utils-header-strip/case.yaml b/plugins/code-tidying/evals/real-hook-utils-header-strip/case.yaml new file mode 100644 index 0000000000..4ce02372a7 --- /dev/null +++ b/plugins/code-tidying/evals/real-hook-utils-header-strip/case.yaml @@ -0,0 +1,35 @@ +schema_version: "1.1" +name: real-hook-utils-header-strip +description: strip on the lib/hook-utils.sh header deletes every comment but the shellcheck directive, rewrites no code, and stages the narrative +tags: [calibration, strip, real] +expected_outcome: Only the shellcheck shell=bash directive remains as a comment; the guard and both function bodies are byte-identical; the removed narrative, including the SSOT warning, is staged in the report. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments strip hook-utils-header.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: directive-kept + type: regex + pattern: '^# shellcheck shell=bash\n' + target: { source: file, path: hook-utils-header.sh } + - name: only-directive-left + type: regex + pattern: '^\s*#(?! shellcheck shell=bash)' + flags: m + match: not_contains + target: { source: file, path: hook-utils-header.sh } + - name: code-intact + type: regex + pattern: '\[\[ -n "\$\{_HOOK_UTILS_LOADED:-\}" \]\] && return 0\nreadonly _HOOK_UTILS_LOADED=1[\s\S]*hook::is_enabled\(\) \{\n local var_name="CLAUDE_PLUGIN_OPTION_\$\{1\}_ENABLED"\n \[\[ "\$\{!var_name:-true\}" == "true" \]\]\n\}\n\nhook::check_enabled\(\) \{\n hook::is_enabled "\$1" \|\| exit 0\n\}' + target: { source: file, path: hook-utils-header.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both + - name: narrative-staged + type: llm + criteria: PASS when the final report contains a proposed commit-message block that preserves the removed information, including the SSOT warning (lib/hook-utils.sh is the source, the plugin copies are never edited, scripts/sync-hook-utils.sh propagates it, and CI rejects drifted copies). FAIL when that warning appears nowhere in the report. diff --git a/plugins/code-tidying/evals/real-hook-utils-header-strip/scaffold.sh b/plugins/code-tidying/evals/real-hook-utils-header-strip/scaffold.sh new file mode 100755 index 0000000000..8528871210 --- /dev/null +++ b/plugins/code-tidying/evals/real-hook-utils-header-strip/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" hook-utils-header.sh diff --git a/plugins/code-tidying/evals/real-silent-revert-design-aggressive/case.yaml b/plugins/code-tidying/evals/real-silent-revert-design-aggressive/case.yaml new file mode 100644 index 0000000000..82d0882579 --- /dev/null +++ b/plugins/code-tidying/evals/real-silent-revert-design-aggressive/case.yaml @@ -0,0 +1,42 @@ +schema_version: "1.1" +name: real-silent-revert-design-aggressive +description: aggressive on the check-silent-revert.sh design block deletes the rejected-alternatives history and keeps only a terse threshold warning +tags: [calibration, aggressive, real] +expected_outcome: The heading, the three rejected designs, and the measured result are deleted with their narrative staged; the warning that no volume threshold separates incidents from routine work survives in at most two comment lines; die() is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive silent-revert-design.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: design-history-deleted + type: regex + pattern: '#[^\n]*(Curated marker|Merge-base staleness|PR-creation-time|Three designs|blame the lines|NOT THE ALTERNATIVES)' + flags: i + match: not_contains + target: { source: file, path: silent-revert-design.sh } + - name: threshold-warning-kept + type: regex + pattern: '#[^\n]*(threshold|number)' + flags: i + target: { source: file, path: silent-revert-design.sh } + - name: kept-comment-succinct + type: regex + pattern: '(^#[^\n]*\n){3}' + flags: m + match: not_contains + target: { source: file, path: silent-revert-design.sh } + - name: code-intact + type: regex + pattern: 'die\(\) \{\n echo "check-silent-revert: \$\*" >&2\n exit 2\n\}' + target: { source: file, path: silent-revert-design.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both + - name: narrative-staged + type: llm + criteria: PASS when the final report contains a proposed commit-message block that preserves the removed design history, naming the rejected alternatives (curated marker strings, merge-base staleness, PR-creation-time overlap) and that blame of deleted lines was chosen. FAIL when that history appears nowhere in the report. diff --git a/plugins/code-tidying/evals/real-silent-revert-design-aggressive/scaffold.sh b/plugins/code-tidying/evals/real-silent-revert-design-aggressive/scaffold.sh new file mode 100755 index 0000000000..49b16c05ee --- /dev/null +++ b/plugins/code-tidying/evals/real-silent-revert-design-aggressive/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" silent-revert-design.sh diff --git a/plugins/code-tidying/evals/real-silent-revert-design-strip/case.yaml b/plugins/code-tidying/evals/real-silent-revert-design-strip/case.yaml new file mode 100644 index 0000000000..9977c0c210 --- /dev/null +++ b/plugins/code-tidying/evals/real-silent-revert-design-strip/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: real-silent-revert-design-strip +description: strip on the check-silent-revert.sh design block deletes every comment, rewrites no code, and stages the narrative +tags: [calibration, strip, real] +expected_outcome: All 34 comment lines are deleted; die() is unchanged; the removed design history and threshold warning are staged in the report. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments strip silent-revert-design.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: no-comments-left + type: regex + pattern: '^\s*#' + flags: m + match: not_contains + target: { source: file, path: silent-revert-design.sh } + - name: code-intact + type: regex + pattern: 'die\(\) \{\n echo "check-silent-revert: \$\*" >&2\n exit 2\n\}' + target: { source: file, path: silent-revert-design.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both + - name: narrative-staged + type: llm + criteria: PASS when the final report contains a proposed commit-message block that preserves the removed design history (the rejected alternatives and the chosen blame-of-deleted-lines design) and the warning that no volume threshold separates incidents from routine deletions. FAIL when either appears nowhere in the report. diff --git a/plugins/code-tidying/evals/real-silent-revert-design-strip/scaffold.sh b/plugins/code-tidying/evals/real-silent-revert-design-strip/scaffold.sh new file mode 100755 index 0000000000..49b16c05ee --- /dev/null +++ b/plugins/code-tidying/evals/real-silent-revert-design-strip/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" silent-revert-design.sh diff --git a/plugins/code-tidying/evals/real-statusline-stamp-aggressive/case.yaml b/plugins/code-tidying/evals/real-statusline-stamp-aggressive/case.yaml new file mode 100644 index 0000000000..5c5d2e49cd --- /dev/null +++ b/plugins/code-tidying/evals/real-statusline-stamp-aggressive/case.yaml @@ -0,0 +1,51 @@ +schema_version: "1.1" +name: real-statusline-stamp-aggressive +description: aggressive on the statusline-tee.sh stamp reader keeps only a terse security warning and leaves the code intact +tags: [calibration, aggressive, real] +expected_outcome: The contract, single-reader rationale, and render-path comments are deleted with their narrative staged; the arithmetic-injection warning survives in at most two comment lines; the function body is unchanged. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments aggressive statusline-stamp.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: contract-deleted + type: regex + pattern: '#[^\n]*epoch-second stamp' + match: not_contains + target: { source: file, path: statusline-stamp.sh } + - name: injection-warning-kept + type: regex + pattern: '#[^\n]*arithmetic' + flags: i + target: { source: file, path: statusline-stamp.sh } + - name: single-reader-rationale-deleted + type: regex + pattern: '#[^\n]*(spelled identically|_rlg_bash_at_least)' + match: not_contains + target: { source: file, path: statusline-stamp.sh } + - name: render-path-deleted + type: regex + pattern: '#[^\n]*render path' + match: not_contains + target: { source: file, path: statusline-stamp.sh } + - name: kept-comment-succinct + type: regex + pattern: '(^#[^\n]*\n){3}' + flags: m + match: not_contains + target: { source: file, path: statusline-stamp.sh } + - name: code-intact + type: regex + pattern: '_rlg_read_stamp\(\) \{\n local _var="\$1" _val=0\n if \[\[ -f "\$2" \]\]; then\n IFS= read -r _val <"\$2" \|\| _val=0\n \[\[ "\$_val" =~ \^\[0-9\]\+\$ \]\] \|\| _val=0\n fi\n printf -v "\$_var" ''%s'' "\$_val"\n return 0\n\}' + target: { source: file, path: statusline-stamp.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both + - name: narrative-staged + type: llm + criteria: PASS when the final report contains a proposed commit-message block that preserves the removed rationale, namely that one reader serves five call sites and that the reader uses builtins only because it runs on every status-line render. FAIL when the removed rationale appears nowhere in the report. diff --git a/plugins/code-tidying/evals/real-statusline-stamp-aggressive/scaffold.sh b/plugins/code-tidying/evals/real-statusline-stamp-aggressive/scaffold.sh new file mode 100755 index 0000000000..46e3ddee84 --- /dev/null +++ b/plugins/code-tidying/evals/real-statusline-stamp-aggressive/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" statusline-stamp.sh diff --git a/plugins/code-tidying/evals/real-statusline-stamp-strip/case.yaml b/plugins/code-tidying/evals/real-statusline-stamp-strip/case.yaml new file mode 100644 index 0000000000..ceb45c514b --- /dev/null +++ b/plugins/code-tidying/evals/real-statusline-stamp-strip/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: real-statusline-stamp-strip +description: strip on the statusline-tee.sh stamp reader deletes every comment, rewrites no code, and stages the narrative +tags: [calibration, strip, real] +expected_outcome: All eight comment lines are deleted, including the arithmetic-injection warning; the function body is byte-identical; the removed narrative is staged in the report. +context: + scaffold_script: scaffold.sh +execution: + prompt: /code-tidying:dissolve-comments strip statusline-stamp.sh + allowed_tools: [Read, Glob, Grep, Skill, Bash, Edit, Write] + max_turns: 80 + timeout_seconds: 1200 +graders: + - name: no-comments-left + type: regex + pattern: '^\s*#' + flags: m + match: not_contains + target: { source: file, path: statusline-stamp.sh } + - name: code-intact + type: regex + pattern: '_rlg_read_stamp\(\) \{\n local _var="\$1" _val=0\n if \[\[ -f "\$2" \]\]; then\n IFS= read -r _val <"\$2" \|\| _val=0\n \[\[ "\$_val" =~ \^\[0-9\]\+\$ \]\] \|\| _val=0\n fi\n printf -v "\$_var" ''%s'' "\$_val"\n return 0\n\}' + target: { source: file, path: statusline-stamp.sh } + - name: proof-ran + type: tool_used + tool: Bash + input_match: change-shape + arm: both + - name: narrative-staged + type: llm + criteria: PASS when the final report contains a proposed commit-message block that preserves the removed information, including the arithmetic-injection warning (bash evaluates arithmetic operand text, so a stamp like a[$(cmd)] would run cmd). FAIL when that warning appears nowhere in the report. diff --git a/plugins/code-tidying/evals/real-statusline-stamp-strip/scaffold.sh b/plugins/code-tidying/evals/real-statusline-stamp-strip/scaffold.sh new file mode 100755 index 0000000000..46e3ddee84 --- /dev/null +++ b/plugins/code-tidying/evals/real-statusline-stamp-strip/scaffold.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec bash "$(dirname "${BASH_SOURCE[0]}")/../fixtures/seed.sh" statusline-stamp.sh diff --git a/plugins/code-tidying/scripts/evals-fixtures.test.sh b/plugins/code-tidying/scripts/evals-fixtures.test.sh new file mode 100755 index 0000000000..b8a15b5875 --- /dev/null +++ b/plugins/code-tidying/scripts/evals-fixtures.test.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Contract: every fixture the dissolve-comments eval suite seeds still parses in +# its own language and still self-certifies through change-shape.py, so a case +# that scores 0 means the skill regressed rather than the corpus rotting. The +# UNPROVABLE excerpt is named here because its exit 21 is the point of the case +# it feeds, not a defect. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +plugin="$(cd "$here/.." && pwd)" +fixtures="$plugin/evals/fixtures" +change_shape="$plugin/scripts/change-shape.py" +rc=0 + +ok() { printf 'ok: %s\n' "$1"; } +fail() { + printf 'FAIL: %s\n' "$1" >&2 + rc=1 +} + +shell_fixtures=( + check-markers.sh.txt + class-a.sh.txt + class-b.sh.txt + class-b.test.sh.txt + class-c.sh.txt + edge-marker.sh.txt + edge-paired.sh.txt + edge-paired.test.sh.txt + hook-utils-header.sh.txt + silent-revert-design.sh.txt + statusline-stamp.sh.txt +) +python_fixtures=(edge_docstring.py.txt exempt.py.txt) +other_fixtures=(Makefile.txt dc-notes.md.txt restoration-markers.txt.txt) +unprovable_fixture=hook-utils-unprovable.sh.txt + +scratch="$(mktemp -d)" +trap 'rm -rf "$scratch"' EXIT + +check_self_certifies() { + local src="$1" name="$2" lang="$3" want="$4" got=0 + cp "$src" "$scratch/$name" + python3 "$change_shape" --lang "$lang" "$scratch/$name" "$scratch/$name" >/dev/null 2>&1 || got=$? + [[ "$got" == "$want" ]] || fail "$name: change-shape exit $got, expected $want" +} + +for file in "${shell_fixtures[@]}"; do + src="$fixtures/$file" + [[ -f "$src" ]] || { + fail "$file is missing" + continue + } + bash -n "$src" || fail "$file does not parse as bash" + check_self_certifies "$src" "${file%.txt}" bash 0 + ok "$file parses and self-certifies COMMENT-ONLY" +done + +for file in "${python_fixtures[@]}"; do + src="$fixtures/$file" + [[ -f "$src" ]] || { + fail "$file is missing" + continue + } + cp "$src" "$scratch/${file%.txt}" + python3 -m py_compile "$scratch/${file%.txt}" || fail "$file does not compile" + check_self_certifies "$src" "${file%.txt}" python 0 + ok "$file compiles and self-certifies COMMENT-ONLY" +done + +check_self_certifies "$fixtures/$unprovable_fixture" "${unprovable_fixture%.txt}" bash 21 +ok "$unprovable_fixture still reads UNPROVABLE (exit 21)" + +for file in "${other_fixtures[@]}"; do + [[ -f "$fixtures/$file" ]] || fail "$file is missing" +done +ok "the non-source fixtures are present" + +# Every fixture ships to feed a case: a scaffold must seed it by name. +for src in "$fixtures"/*.txt; do + base="$(basename "$src" .txt)" + grep -rqF -- "$base" "$plugin"/evals/*/scaffold.sh "$fixtures/seed.sh" || + fail "$base is seeded by no scaffold" +done +ok "every fixture is seeded by a scaffold" + +exit "$rc" diff --git a/plugins/code-tidying/skills/dissolve-comments/SKILL.md b/plugins/code-tidying/skills/dissolve-comments/SKILL.md index 4c54139626..fca40b5df0 100644 --- a/plugins/code-tidying/skills/dissolve-comments/SKILL.md +++ b/plugins/code-tidying/skills/dissolve-comments/SKILL.md @@ -1,7 +1,7 @@ --- -description: "Enforce self-describing code over a diff, branch, or ranked repository: a three-way comment triage that deletes zero-information comments, dissolves code-expressible ones into names and structure by behavior-preserving refactoring, and keeps only terse, load-bearing comments code cannot express. Deletions and local renames apply behind a token-level proof, other refactors behind a test net, else proposed; 'safe' mode restricts applied edits to removals. Use when: 'dissolve comments', 'remove comments', 'strip agent comments', 'too many comments', 'make it self-documenting', 'make the code expressive', 'comments must earn their keep', after an agent wrote over-commented code. Skip when: read-only residue classification (audit-comment-residue), structural tidyings (tidy), simplification waves (batch-simplify), markdown noise (docs-hygiene audit-noise), adding why-comments (tidy #14). Never touches public-API doc comments, license headers, or machine-read directives." -argument-hint: "[safe] [override] [target]" -allowed-tools: ["Bash(${CLAUDE_SKILL_DIR}/scripts/scope-code-files.sh:*)", "Bash(${CLAUDE_SKILL_DIR}/scripts/comment-tooling-probe.sh:*)", "Bash(${CLAUDE_SKILL_DIR}/scripts/change-shape.sh:*)", "Bash(${CLAUDE_SKILL_DIR}/scripts/comment-census.sh:*)", "Bash(${CLAUDE_SKILL_DIR}/scripts/commented-out-code.sh:*)", "Bash(${CLAUDE_SKILL_DIR}/scripts/rank-comment-targets.sh:*)", "Bash(git branch:*)", "Bash(git log:*)", "Bash(grep:*)", "Bash(echo:*)"] +description: "Enforce self-describing code over a diff, branch, or ranked repo: a three-way triage: delete zero-information comments, dissolve code-expressible ones into names and structure by behavior-preserving refactors, keep only terse load-bearing comments code cannot express. Deletions and local renames need a token proof, other refactors a test net, else proposed; 'safe' removals only, 'aggressive' deletes non-exempt comments with narrative staged, 'strip' deletes all but exempt surfaces, no rewrite. Use when: 'dissolve comments', 'remove comments', 'strip all comments', 'strip agent comments', 'too many comments', 'aggressive comment removal', 'make it self-documenting', 'make the code expressive', 'comments must earn their keep', after an agent wrote over-commented code. Skip when: residue classification (audit-comment-residue), tidyings (tidy), simplification (batch-simplify), markdown noise (audit-noise), why-comments (tidy #14). Never touches public-API docs, license headers, or machine-read directives." +argument-hint: "[safe] [aggressive|strip] [override] [--notes ] [target]" +allowed-tools: ["Bash(${CLAUDE_SKILL_DIR}/scripts/scope-code-files.sh:*)", "Bash(${CLAUDE_SKILL_DIR}/scripts/comment-tooling-probe.sh:*)", "Bash(${CLAUDE_SKILL_DIR}/scripts/change-shape.sh:*)", "Bash(${CLAUDE_SKILL_DIR}/scripts/comment-census.sh:*)", "Bash(${CLAUDE_SKILL_DIR}/scripts/commented-out-code.sh:*)", "Bash(${CLAUDE_SKILL_DIR}/scripts/rank-comment-targets.sh:*)", "Bash(git branch:*)", "Bash(git log:*)", "Bash(git ls-files:*)", "Bash(grep:*)", "Bash(echo:*)"] disable-model-invocation: false user-invocable: true shell: bash @@ -37,7 +37,7 @@ argument names an explicit target**, and the tooling line is re-derived in step Arguments: `$ARGUMENTS` Posture: `${user_config.comment_posture}` (unexpanded or empty means `strict`; any value outside -`strict`, `balanced`, `conservative` is read as `strict`). +`strict`, `balanced`, `conservative`, `aggressive` is read as `strict`). Kept-comment line budget: `${user_config.class_c_max_lines}` (unexpanded or empty means `2`). Apply proven local renames without a test net: `${user_config.apply_local_renames}` (unexpanded or empty means `true`). @@ -64,8 +64,8 @@ line budget, and worked examples: [reference/triage.md](reference/triage.md). |---|---|---| | **A, zero/negative information** | Restates adjacent code, obsolete, commented-out code | Delete outright, certified by the token proof | | **B, information code could carry** | The comment compensates for a naming/structure deficiency. Empty by construction on a data or config file (TOML, YAML, JSON), which has no naming channel, so the pass there degrades to A plus C | Refactor until the comment is superfluous, then delete, never delete first | -| **C, information code cannot carry** | Why/rationale, constraint, warning, contract, negative or operational information | **Kept** when load-bearing at the point of reading and not recoverable where a reader would look; held to the line budget once the exempt-surface check has cleared it, rewritten terser when over it, narrative staged | -| **C, same test failed** | Inexpressible, but the earn-its-keep test's criterion 2 fails: recoverable from version control, an ADR, or an external source | **Deleted** under `strict`, certified by the same token proof class A uses, narrative staged before the deletion is final; **proposed** under `safe` and `conservative`, which apply class-A deletions only. The negative branch of the class-C test, not a fourth class | +| **C, information code cannot carry** | Why/rationale, constraint, warning, contract, negative or operational information | **Kept** when load-bearing at the point of reading and not recoverable where a reader would look; held to the line budget once the exempt-surface check has cleared it, rewritten terser when over it, narrative staged. Under `aggressive` only a warning of consequence is kept, and under `strip` nothing in this class is | +| **C, same test failed** | Inexpressible, but the earn-its-keep test's criterion 2 fails: recoverable from version control, an ADR, or an external source | **Deleted** under `strict`, certified by the same token proof class A uses, narrative staged before the deletion is final; **proposed** under `safe` and `conservative`, which apply class-A deletions only. Under `aggressive` and `strip` criterion 2 is not run at all on a non-survivor: the comment is staged and deleted whether or not the reasoning is recoverable. The negative branch of the class-C test, not a fourth class | The two class-C rows are one class and one test, whose three criteria must **all** hold, named on each side, so a comment that fails it has somewhere to go. A criterion-1 failure is not this branch: @@ -80,22 +80,61 @@ Class-B moves and their tiers: [reference/dissolving-moves.md](reference/dissolv | *(empty)* | Triage the code files of the narrowest scope that resolves: uncommitted diff → branch diff → whole repository, resolved by `scope-code-files.sh` ([reference/scope.md](reference/scope.md)). On the repository rung, order the files with `rank-comment-targets.py` first. Pass `--allow-path ` for each path the `override` argument or the repository overrides file lifted, so the administrative gate does not re-drop those files and does not ungate every other administrative path. Pass `--override-exclusions` only when `hard_exclusions` is `advisory`, which lifts the whole HARD path list. | | `` | Triage a single file or directory (already-committed code is fine here). The pre-computed scope line above is **void** under an explicit target: that line runs the diff ladder unconditionally, so it names files this run is not triaging. Ignore it and do not run `scope-code-files.sh`. | | `safe [target]` | **Safe mode**: only class-A deletions are applied; every class-B treatment and class-C rewrite is emitted as a proposal. For codebases whose guardrails you do not know. | +| `aggressive [target]` | **Aggressive dial**: the survivor list below is the whole of what stays. Every other comment goes, rationale included, with its narrative staged; a class-B comment is dissolved when its move's gate passes and otherwise kept with a proposal. Gates are unchanged. Combines with `override` and `--notes`; `safe` beats it. | +| `strip [target]` | **Strip**: delete every comment except the survivor list, rewrite no code, certify each deletion COMMENT-ONLY, and stage the narrative. A class-B comment is deleted rather than dissolved, so its information reaches the staged block instead of the code. `safe` beats it, and `strip` beats `aggressive`. | +| `--notes ` | Append the staged block to `` as well as reporting it. Refuse a symlink outright, then check the path with `git ls-files --error-unmatch `, which reads the index entry and never the destination a link points at. The path must be untracked or outside the repository; a tracked or symlinked path is refused, the run continues, and the block is reported only. | | `override [target]` | **Lift the GLOBAL HARD path list** for this run's target, so `/code-tidying:dissolve-comments override ruff.toml` triages a file the list would otherwise drop. Combines with `safe`. Strip the token before reading the target; match it whole, and treat `./override` as a path. Path entries only, and every lifted path is named in the step 7 report with the channel that lifted it. | Posture `conservative` is safe mode as a standing default; `balanced` keeps the full contract but -reports an over-budget class-C comment instead of rewriting it. - -**The posture ladder only descends.** `strict` is both the default and the ceiling; `balanced`, -`conservative` and `safe` each narrow what gets applied, `class_c_max_lines` bottoms out at 1, and -nothing removes more than `strict` does. That is deliberate: no knob loosens a gate -([reference/safety.md](reference/safety.md)). It is stated here because a user wanting a more -aggressive pass would otherwise hunt for a setting that does not exist. - -In every posture and mode, doubt keeps the comment: "when uncertain, keep or propose" is doctrine, -not timidity. Doubt means an unresolved *classification*, not a resolved one whose verdict is -delete. A criterion-2 failure established by the step-5 evidence check is not doubt, and the tie- -break does not reinstate it; that rule is what stops the earn-its-keep test from collapsing into -"keep everything". +reports an over-budget class-C comment instead of rewriting it; posture `aggressive` is the +`aggressive` row above as a standing default. A per-run token beats the standing posture, and +precedence among tokens is `safe`, then `strip`, then `aggressive`. `./safe`, `./aggressive`, +`./strip` and `./override` are paths, not tokens. + +**No knob loosens a gate.** `aggressive` and `strip` widen *what is triaged away*; they change no +gate and no proof. Every applied deletion still carries the COMMENT-ONLY verdict, every applied +function-local rename still carries RENAME-ONLY, tier-2 and tier-3 moves still need a discovered +test net, and an UNPROVABLE file still yields proposals only. A `strict` run and an `aggressive` run +differ in what they decide to remove, never in what they are allowed to prove. + +**What survives `aggressive` and `strip`** (the whole list; `reference/safety.md` carries the +detail): + +- the exempt surfaces: public-API doc comments **in the language's structured doc-comment form** + (a docstring, an XML doc, JSDoc or TSDoc, a GoDoc sentence) attached to a public declaration; a + free-form comment block in a language with no doc-comment form, shell and make among them, is not + a doc comment and takes the ordinary triage. Also legal headers, machine-read directives (universal + and repo-local), units, sentinels, ownership, thread-safety and ordering **annotations on the + adjacent declaration** (`# seconds`, `# -1 means unset`; a sentence explaining a choice is not an + annotation), suppression justifications paired with their waiver, `TODO(#issue)` markers, and + lines carrying `dissolve-comments-ignore`; +- a comment that is one half of a comment-plus-regression-test pair, because deleting half of a + paired record is a correctness bug; +- under `aggressive` only, a **warning of consequence**: a comment naming a runtime failure a caller + hits by using the code as written, such as a required call order, a precondition, or a required + call form a caller would otherwise get wrong. The *reason a + value was chosen*, another system's limit, an upstream's behavior, a past incident, is rationale + rather than a warning: it is staged and deleted. A warning earns its keep only when the failure it + names is **not visible in the adjacent code**: if the body a reader is already looking at shows + the behavior (an `exit` in the function, a guard, a return), the comment restates code and goes. + A warning belongs to the declaration it sits on and addresses that declaration's caller; a comment + explaining why a *sibling* exists, or why the interface is shaped as it is, is design rationale + and goes; Held to `class_c_max_lines` and rewritten terser + when over it. Every survivor must be succinct, clear, and justified in the report: name the + consequence, not the history. + +In `strict`, `balanced`, `conservative` and `safe`, doubt keeps the comment: "when uncertain, keep +or propose" is doctrine, not timidity. Doubt means an unresolved *classification*, not a resolved +one whose verdict is delete. A criterion-2 failure established by the step-5 evidence check is not +doubt, and the tie-break does not reinstate it; that rule is what stops the earn-its-keep test from +collapsing into "keep everything". + +Under `aggressive` and `strip` the tie-break is narrower, because everything not on the survivor +list is leaving anyway: doubt whether a comment is an exempt surface, a paired record, or a +load-bearing warning **keeps it**. Doubt between classes does not keep it, and resolves to the +treatment that preserves the information: A-versus-B doubt resolves to B (dissolve when the gate +passes, else keep with a proposal under `aggressive`, delete with the narrative staged under +`strip`), and B-versus-C doubt resolves to B. Default mode applies the full contract: class A applies, each deletion certified by a token-level proof that no code changed; class B applies **per its tier**: a function-local rename behind the @@ -104,7 +143,7 @@ move behind the net and proposal-first. Whatever a tier's gate does not pass is the proof tool, the test-discovery procedure, and the mode ladder: [reference/safety.md](reference/safety.md). **Class B applies less than it looks like it does**, and a run planned around it should know that -first: 2 of 15 moves need no test net, 0 of 15 apply with tree-sitter absent, and no move dissolves +first: 2 of 16 moves need no test net, 0 of 16 apply with tree-sitter absent, and no move dissolves a *why*. Both limits are deliberate. See "Apply capacity" in [reference/dissolving-moves.md](reference/dissolving-moves.md) for the numbers and what follows from them. @@ -112,22 +151,36 @@ from them. ## Hard rules - **Never delete information without a landing place.** A class-B comment's information moves into - code *before* the comment goes. Removed narrative (rationale, justification) is staged in the - output as a proposed commit-message block for `/source-control:commit`. Text is never silently - destroyed. + code *before* the comment goes, except under `strip`, which rewrites nothing and sends that + information to the staged block instead. Removed narrative (rationale, justification) is staged in + the output as a proposed commit-message block for `/source-control:commit`, and `--notes ` + appends it to an untracked or out-of-repo file as well. The block carries an + `Intentional-removal:` line only when the target repository's own scripts or CI read that trailer + (`grep -rl 'Intentional-removal:'` over its gate scripts and workflows); elsewhere the line is + noise. On an explicit target over already-committed code the landing place is the next commit + touching that code, named in the report. Text is never silently destroyed. - **Every applied edit passes the gate its tier names; lint never opens one.** Deletions and function-local renames are certified by `${CLAUDE_SKILL_DIR}/scripts/change-shape.sh` (COMMENT-ONLY, RENAME-ONLY); additive and interface-creating moves need a discovered test net. - Any other verdict reverts the edit and demotes it to a proposal. + Any other verdict reverts the edit and demotes it to a proposal. A Python docstring is a string + token, not a comment, so removing one reads CODE-CHANGED and is always a proposal, private + docstrings included. - **RENAME-ONLY is a shape claim, not a safety claim.** It rejects a rename that misses a reference or lands on a name the file already uses, but cannot see other files, reflection, or string-keyed access. A rename applied on its strength is reported with its mapping, never silently. - **Exempt surfaces are invisible to this skill** ([reference/safety.md](reference/safety.md)): public-API doc comments; legal headers; machine-read directives, universal and repo-local; - units, sentinels and suppression justifications; `TODO(#issue)` markers; lines carrying - `dissolve-comments-ignore`. **Negative and operational information are not on that list.** They + unit, sentinel, ownership, thread-safety and ordering annotations on the adjacent declaration, + never a sentence explaining a choice; suppression justifications; `TODO(#issue)` markers; lines + carrying `dissolve-comments-ignore`. **Negative and operational information are not on that list.** They are class C with a raised evidence bar, held to the same test and budget as any class-C comment. Exempting the category outright would contradict this skill's own eval 13. +- **A paired record is never half-deleted.** A comment asserting something about code that is not + present, paired with a regression test that pins it, is one artifact in two places; deleting the + comment alone is a correctness bug. Kept in every mode, `strip` included. +- **An identifier named in a repo-local marker row is never renamed.** A gate that pins + `=` as an exactly-once marker (step 2 discovers these) turns that spelling into compiler + input. Dissolve the comment if it earns dissolving, but leave the identifier alone. - **Path exclusions are the plugin's standard tier**, tidy's [exclusions reference](${CLAUDE_PLUGIN_ROOT}/skills/tidy/reference/exclusions.md) GLOBAL HARD list. Agent/enforcement config, CI workflows, hook chains, lint config are not edited unless a @@ -150,7 +203,14 @@ from them. the pre-computed scope line is void, `scope-code-files.sh` is not run, and no file outside the target is triaged or reported. Empty argument: run `scope-code-files.sh` (never the truncated preview), confirm a widening to the repository rung interactively, and take any widened rung in - safe mode when non-interactive. On the repository rung, run `${CLAUDE_SKILL_DIR}/scripts/rank-comment-targets.sh` and triage + safe mode when non-interactive, **whatever the posture or dial token**: `aggressive` and `strip` + reach a widened rung only through an interactive confirmation, and an explicit target is the + other way to mean it. **You are non-interactive whenever the `AskUserQuestion` tool is + unavailable.** There, take the widened rung in safe mode, say "safe mode, non-interactive + widening" in the report, and name the explicit-target re-run that would apply the full contract; + do not end the turn on a question nobody can answer. The same holds when the ladder stops on a + rung with no code files: report the rung and its count, say the session is non-interactive, name + the explicit-target re-run, and stop there rather than asking. On the repository rung, run `${CLAUDE_SKILL_DIR}/scripts/rank-comment-targets.sh` and triage in its order. When an override channel is active, hand its resolved reach to the ranker so the administrative gate does not re-drop a lifted path: `--allow-path ` per path the `override` argument or the repository overrides file lifted, and `--override-exclusions` only for @@ -210,8 +270,12 @@ from them. content is rationale, run `git log -L ,:` over its own lines and check the repo's ADR or decision-log directory where one is declared; recoverable there **fails** the criterion, absent from both **passes**, unreadable history is recorded as unavailable and keeps - the comment. Full procedure: [reference/triage.md](reference/triage.md). Done when every comment - carries one class and every class-C candidate a criterion-2 verdict with its evidence. + the comment. Under `aggressive` and `strip` this evidence check is skipped for every comment + outside the survivor list: the verdict is the same either way, and the `git log -L` per comment + is the expensive half of a run. Full procedure: [reference/triage.md](reference/triage.md). Done when every comment + carries one class, and every class-C candidate the run still tests carries a criterion-2 verdict + with its evidence: under `aggressive` and `strip` that is the survivors only, since the rest are + leaving whatever the evidence says. 6. **Apply**, one item at a time, each behind its tier's gate. Class A: delete, run `change-shape.py` on before and after; anything but COMMENT-ONLY (exit 0) restores the comment. Class B: apply the named move, run the tier's gate, then delete the comment. Class C: check the @@ -219,8 +283,16 @@ from them. at any length. A non-exempt comment that **failed** criterion 2 is, under `strict`, staged then deleted behind the same COMMENT-ONLY proof class A uses; under `safe` or `conservative` it is proposed instead, since those modes apply class-A deletions only, and a rationale comment is not class - A however its test resolved. A non-exempt comment over budget is rewritten to the budget under - `strict` with the narrative staged, reported instead under `balanced`; its carve-out reason names + A however its test resolved. Under `aggressive` and `strip` every non-survivor is staged and then + deleted behind that same COMMENT-ONLY proof, class C included, and under `strip` a class-B + comment takes that path rather than its move; under `aggressive` a class-B move still applies + only when its tier's gate passes, and the comment stays with a proposal when it does not. A non-exempt comment over budget is rewritten to the budget under + `strict` and under `aggressive`, each rewrite carrying the narrative to the staged block and the + COMMENT-ONLY proof like any other edit; it is reported instead under `balanced`. An over-budget + survivor left as it stands is a failed run, not a conservative one. **A kept comment stays + where it is and keeps its own words**: rewriting shortens the comment that is there, and never + relocates it, merges two comments, or writes a new one. A comment whose referent is gone is + deleted, not re-authored. its carve-out reason names every kept comment by file and line, written once for a group that enumerates its members. Where most of a file's class-C comments carry contract, negative, or operational information, say so once as a whole-file verdict with its count and suspend the budget for that file. Criterion 2 @@ -231,7 +303,11 @@ from them. versus proposed with each applied item's verdict (and the mapping for every RENAME-ONLY), the staged commit-message block, the class-C keeps and rewrites with one-line reasons (grouped where several share one, every member still named) and **each keep naming its criterion-2 evidence** - from step 5, plus any whole-file budget suspension. Under a whole-file verdict, report that + from step 5, plus any whole-file budget suspension. Under `aggressive` and `strip` a keep names + which survivor rule earned it (exempt surface, paired record, warning of consequence) in place of + criterion-2 evidence, and the report names the block's landing place: the pending commit, the + next commit touching already-committed code, or the `--notes` path, with a refused tracked path + said plainly. Under a whole-file verdict, report that file's class-C keeps as a count per reason group rather than a line each; the per-keep evidence line is owed only for keeps the run actually searched. Then the census delta, `comment-census.py --baseline` pointed at the exact `baseline.json` step 4 wrote, in lines, bytes and estimated @@ -242,20 +318,29 @@ from them. ## What this skill is NOT -- **Not "delete all comments."** Class C survives on the earn-its-keep test; exempt surfaces are - never touched. +- **Not "delete all comments."** `strip` comes closest and still keeps the survivor list: exempt + surfaces and paired records are never touched, in any mode. Without a dial token, class C survives + on the earn-its-keep test. - **Not `/code-tidying:audit-comment-residue`**, the read-only residue classifier. Run that for findings without changes. - **Not `/code-tidying:tidy` or `/code-tidying:batch-simplify`.** No lane rotation, no scope budget, no wave machinery: one pass over one resolved scope. - **Not a bug-hunter or general simplifier.** `/code-review` and `/simplify` own those. +## Next + +`/source-control:commit`, which takes the staged commit-message block this run printed and lands the +removed narrative with the diff that removed it. + ## Gotchas - A comment that *looks* like restatement can disambiguate genuinely ambiguous code. Misclassifying - B as A is the information-destroying failure; when uncertain, keep or propose. + B as A is the information-destroying failure; when uncertain, keep or propose. Under `aggressive` + and `strip` the same doubt resolves to B, which is why those modes stage before they delete. - Rationale for a *rejected* approach has no referent in the adjacent code, the same surface as a - stale comment. It is class C by default ([reference/safety.md](reference/safety.md)). + stale comment. It is class C by default ([reference/safety.md](reference/safety.md)), and under + `aggressive` and `strip` it is staged and deleted: rejected alternatives belong in the commit or + PR that removed them. - Extraction has a cost curve: a name that must grow megasyllabic to stay honest signals the information did not fit the name channel. Short name plus terse comment, or Inline Function, beats a dishonest long name ([reference/dissolving-moves.md](reference/dissolving-moves.md)). diff --git a/plugins/code-tidying/skills/dissolve-comments/evals/evals.json b/plugins/code-tidying/skills/dissolve-comments/evals/evals.json index 739a4e491b..64bf5dea11 100644 --- a/plugins/code-tidying/skills/dissolve-comments/evals/evals.json +++ b/plugins/code-tidying/skills/dissolve-comments/evals/evals.json @@ -180,6 +180,42 @@ "Deletes the two-line block with a COMMENT-ONLY verdict and keeps the prose comment for ordinary triage", "Notes ruff as absent with the capability lost, rather than failing" ] + }, + { + "id": 16, + "name": "aggressive-deletes-non-exempt-rationale-with-staging", + "prompt": "/code-tidying:dissolve-comments aggressive lib/worker.sh\n\n(lib/worker.sh is committed. It carries a rationale comment explaining why a retry count is three, a warning that one function must run before another or the lock file deadlocks, a license header, and a '# shellcheck disable=SC2034' directive with its justification. The repository has no test suite and tree-sitter is present for bash.)", + "narration": true, + "expectations": [ + "Deletes the retry rationale with its narrative staged in the proposed commit-message block, without running a git log -L recoverability check on it", + "Keeps the deadlock warning, rewritten to at most class_c_max_lines, and names it as a warning of consequence rather than citing criterion-2 evidence", + "Keeps the license header and the shellcheck directive with its justification untouched", + "Certifies each deletion COMMENT-ONLY with change-shape.py and reports the next commit touching the file as the staged block's landing place" + ] + }, + { + "id": 17, + "name": "strip-keeps-exempt-surfaces-and-rewrites-nothing", + "prompt": "/code-tidying:dissolve-comments strip lib/worker.sh\n\n(Same file as the aggressive case: a rationale comment, a deadlock warning, a license header, a shellcheck directive with its justification, and a block comment narrating what a six-line section does. A test suite exists and is discoverable.)", + "narration": true, + "expectations": [ + "Deletes the rationale, the warning, and the narrating block comment, each certified COMMENT-ONLY", + "Does NOT extract a function for the narrating comment even though a test net exists, because strip rewrites no code, and sends that comment's information to the staged block instead", + "Keeps the license header and the shellcheck directive with its justification", + "Leaves every non-comment line byte-identical" + ] + }, + { + "id": 18, + "name": "standing-aggressive-posture-yields-to-safe", + "prompt": "/code-tidying:dissolve-comments safe lib/worker.sh\n\n(comment_posture is aggressive. The file carries one restating comment above the line it restates and one rationale comment whose reasoning is absent from git history.)", + "narration": true, + "expectations": [ + "Applies the class-A deletion of the restating comment", + "Does NOT delete the rationale comment: safe wins over the standing aggressive posture, and only class-A deletions are applied", + "Emits the rationale removal as a proposal with its narrative staged", + "Names safe mode as the effective mode in the report rather than the standing posture" + ] } ] } diff --git a/plugins/code-tidying/skills/dissolve-comments/reference/dissolving-moves.md b/plugins/code-tidying/skills/dissolve-comments/reference/dissolving-moves.md index 141d5a08ba..17dfe07013 100644 --- a/plugins/code-tidying/skills/dissolve-comments/reference/dissolving-moves.md +++ b/plugins/code-tidying/skills/dissolve-comments/reference/dissolving-moves.md @@ -12,7 +12,7 @@ net; interface-creating moves need a test net and stay proposals in non-interact |---|---| | 1, token-proven | Rename Variable, Rename Field (function-local identifiers only) | | 2, test-gated | Extract Variable, Replace Magic Literal, Introduce Assertion, Slide Statements, Decompose Conditional, Replace Nested Conditional with Guard Clauses, Introduce Special Case | -| 3, test-gated and proposal-first | Extract Function, Change Function Declaration, Move Statements into Function, Replace Inline Code with Function Call, Introduce Parameter Object, Inline Function | +| 3, test-gated and proposal-first | Extract Function, Change Function Declaration, Extract Class, Move Statements into Function, Replace Inline Code with Function Call, Introduce Parameter Object, Inline Function | | Comment shape being dissolved | Named refactoring | |---|---| @@ -54,11 +54,11 @@ net; interface-creating moves need a test net and stay proposals in non-interact Class B reads like the skill's main engine. On many repositories it turns over nothing, and a run planned around it should know the three limits up front. All three are deliberate. -- **2 of the 15 moves need no test net.** Only Rename Variable and Rename Field are tier 1, and +- **2 of the 16 moves need no test net.** Only Rename Variable and Rename Field are tier 1, and only on a *function-local* identifier. Every other move adds tokens, so the token proof reports CODE-CHANGED by construction ([safety.md](safety.md)) and a discovered test net is required; without one they are proposed, never applied. -- **0 of 15 apply with tree-sitter absent.** The proof is unavailable, so "tier 1 without its proof +- **0 of 16 apply with tree-sitter absent.** The proof is unavailable, so "tier 1 without its proof is tier 2" ([safety.md](safety.md)) demotes the two renames into the test-net tier with everything else. On a repository with neither a runnable test net nor tree-sitter, a class-B pass produces a proposal list and no edits. diff --git a/plugins/code-tidying/skills/dissolve-comments/reference/safety.md b/plugins/code-tidying/skills/dissolve-comments/reference/safety.md index b17001c191..aff1c90f94 100644 --- a/plugins/code-tidying/skills/dissolve-comments/reference/safety.md +++ b/plugins/code-tidying/skills/dissolve-comments/reference/safety.md @@ -13,8 +13,12 @@ behavior, while a token comparison is exhaustive over the file. |---|---|---|---| | **Default** | Applied, each deletion certified by the tier-0 proof | Applied per the tier table below; otherwise proposed | Earn-its-keep triage; a criterion-2 failure is deleted behind the tier-0 proof, an over-budget comment rewritten; narrative staged before either | | **`safe`** | Applied, same certification | Always proposed: no code-structure change is applied | Same triage, but **nothing class-C is applied**: a criterion-2 deletion and an over-budget rewrite are both proposed, with the narrative staged. Only class A deletes here | +| **`aggressive`** | Applied, same certification | Dissolved when the tier's gate passes; otherwise the comment stays with a proposal | Earn-its-keep is replaced by the survivor list: exempt surfaces, paired records, and warnings of consequence within `class_c_max_lines` stay, and every other class-C comment is staged and deleted behind the tier-0 proof | +| **`strip`** | Applied, same certification | Deleted as a comment, no move attempted, narrative staged | Same survivor list minus the warnings: only exempt surfaces and paired records stay | -`conservative` is `safe` as a standing default, so it reads the `safe` row. The class-C column is +`conservative` is `safe` as a standing default, so it reads the `safe` row, and posture +`aggressive` reads the `aggressive` row. A per-run token beats the standing posture; precedence is +`safe`, then `strip`, then `aggressive`. The class-C column is the one to get right: the triage still runs in every mode and still returns a verdict, but a verdict is not an application. `safe` narrowing class C to proposals is what makes "only class-A deletions are applied" in the action router true rather than approximately true. @@ -28,8 +32,8 @@ or excluded path, or delete text without a landing place (staging rule below). |---|---|---|---| | **0** | Class-A deletion | `change-shape.py` verdict **COMMENT-ONLY** | The comment-stripped token sequence is identical, so no code token moved. Exhaustive over the file; needs no tests, no build, no config | | **1** | Rename Variable / Rename Field on a function-local identifier | verdict **RENAME-ONLY** under one consistent mapping, and the identifier is neither exported nor public | A shape claim: every differing token is an identifier under one injective old→new mapping, no old name survives at an unchanged position, and no new name was already in use in the file. It cannot see other files, reflection, or string-keyed access, so it earns application plus a flagged review line in the report, never silence | -| **2** | Additive local move: Extract Variable, Replace Magic Literal, Introduce Assertion, Slide Statements, Decompose Conditional | discovered test net, run before and after | These add tokens, so the token proof reports CODE-CHANGED by construction and cannot certify them. Only tests attest behavior preservation here | -| **3** | Interface-creating move: Extract Function, Change Function Declaration, Extract Class, Introduce Parameter Object, Move Statements into Function, Replace Inline Code with Function Call | discovered test net, and **always a proposal in a non-interactive run** | Creates or renames an interface other code depends on. Ousterhout (APOSD §9.8) and Anthropic's own overeagerness guidance both warn against automating exactly this; the test net is necessary, not sufficient | +| **2** | Additive local move: Extract Variable, Replace Magic Literal, Introduce Assertion, Slide Statements, Decompose Conditional, Replace Nested Conditional with Guard Clauses, Introduce Special Case | discovered test net, run before and after | These add tokens, so the token proof reports CODE-CHANGED by construction and cannot certify them. Only tests attest behavior preservation here | +| **3** | Interface-creating move: Extract Function, Change Function Declaration, Extract Class, Introduce Parameter Object, Move Statements into Function, Replace Inline Code with Function Call, Inline Function | discovered test net, and **always a proposal in a non-interactive run** | Creates or renames an interface other code depends on. Ousterhout (APOSD §9.8) and Anthropic's own overeagerness guidance both warn against automating exactly this; the test net is necessary, not sufficient | `change-shape.py` is at `../../../scripts/change-shape.py` (relative to this file; the `${CLAUDE_PLUGIN_ROOT}` token is substituted in `SKILL.md` but **not** in a reference file, which @@ -55,10 +59,12 @@ probe reported: a pygments-level read may still apply deletions; a grep-level re in a language with heredocs or block comments, because it cannot tell a comment from string data. Tier 1 without its proof is tier 2. -Two user-config knobs move tiers without changing any gate: `apply_local_renames=false` makes -tier 1 a proposal even when RENAME-ONLY holds, and posture `conservative` makes every tier above 0 -a proposal (safe mode as a standing default). Nothing loosens a gate: no knob applies an edit its -tier's proof did not pass. +Knobs move tiers without changing any gate: `apply_local_renames=false` makes tier 1 a proposal even +when RENAME-ONLY holds, and posture `conservative` makes every tier above 0 a proposal (safe mode as +a standing default). `aggressive` and `strip` move in the other direction and still change no gate: +they widen which comments are triaged away, while every applied deletion carries COMMENT-ONLY, every +applied rename carries RENAME-ONLY, tiers 2 and 3 keep their test net, and an UNPROVABLE file yields +proposals only. No knob applies an edit its tier's proof did not pass. ## The test net (tiers 2 and 3) @@ -80,7 +86,10 @@ open the apply path, because they cannot attest behavior preservation. ## Exempt surfaces (never touched, any mode) -- Public-API doc comments: docstrings, C# XML docs, JSDoc/TSDoc on exported/public surfaces. Python +- Public-API doc comments, in the language's structured doc-comment form: docstrings, C# XML docs, + JSDoc/TSDoc, GoDoc sentences, on exported/public surfaces. A language with no doc-comment form, + shell and make among them, has no exempt surface here: a header block there is an ordinary comment + and takes the ordinary triage. Python has no export keyword, so the rule there is the leading underscore: a module docstring, and the docstring of any module, class, function, method, or attribute whose name does not start with an underscore, is public and exempt. A leading underscore marks it private, and a private docstring @@ -90,8 +99,12 @@ open the apply path, because they cannot attest behavior preservation. `#pragma warning`), region markers, editor folds, encoding cookies - **Repo-local machine-read markers**, discovered per run. See the section below. The universal pragmas above are the floor, not the list -- Units, ranges, boundary semantics, sentinel values, ownership and lifetime, thread-safety, and - ordering guarantees. A comment naming what `-1` or `nullptr` means is a contract, not narration +- Units, ranges, boundary semantics, sentinel values, ownership and lifetime, thread-safety and + ordering guarantees, **as an annotation on the adjacent declaration**: `# seconds`, `# -1 means + unset`, `# caller owns the handle`. A comment naming what `-1` or `nullptr` means is a contract, + not narration. A *sentence* about why a value was chosen or when a function must be called is not + an annotation: it is class C, held to the earn-its-keep test, the budget, and the dials like any + other comment - Suppression justifications: the reason attached to a lint waiver, a cast-safety claim, or a narrowing assertion (`@SuppressWarnings("unchecked") // safe because …`). The waiver is a directive and the reason is what makes it reviewable. Removing either breaks the pair @@ -208,6 +221,15 @@ code, note in the report that the narrative belongs with the *next* commit touch or keep the comment if no vehicle exists (staging with no landing place is not a deletion licence). +`--notes ` gives the block a second home: the run appends it to that file as well as +reporting it. Refuse a symlink first: `git ls-files --error-unmatch ` reads the index entry +for the path it is given, so an untracked link pointing at a tracked file passes that check while +the append lands on the tracked target. Then the path must be untracked or outside the repository; +a tracked or symlinked path is refused and the run continues with the report as the only vehicle. The block carries an `Intentional-removal:` line only where the target +repository's own gate scripts or CI read that trailer, since elsewhere it is a line no tool will +ever match. Under `strip` the block carries more than usual: a class-B comment's information lands +there rather than in a rewrite, so a thin staged block under `strip` is a defect, not a clean run. + ## Gotcha: rejected-alternative rationale reads exactly like residue The highest-cost misclassification this skill can make is deleting a comment that records why an diff --git a/plugins/code-tidying/skills/dissolve-comments/reference/scope.md b/plugins/code-tidying/skills/dissolve-comments/reference/scope.md index a83b762bc6..0b5312e2f0 100644 --- a/plugins/code-tidying/skills/dissolve-comments/reference/scope.md +++ b/plugins/code-tidying/skills/dissolve-comments/reference/scope.md @@ -27,7 +27,8 @@ The ladder advances on **absence** of a rung, never on emptiness: a rung that ex code files is reported with `files=0`, so a docs-only branch reports its files as out of scope instead of silently escalating to the whole repository. Widening to the `repository` rung is confirmed with the user in an interactive session; a non-interactive run proceeds in **safe** mode -on any widened rung. +on any widened rung, whatever the posture or dial token. `aggressive` and `strip` reach a widened +rung only through that interactive confirmation; an explicit target is the other way to mean it. Granularity is per file: every comment in a listed file is triaged, not only the lines the diff added. That is deliberate. A pull request that touches a file is the moment its existing comments diff --git a/plugins/code-tidying/skills/dissolve-comments/reference/triage.md b/plugins/code-tidying/skills/dissolve-comments/reference/triage.md index c124abdf5f..099f936f4b 100644 --- a/plugins/code-tidying/skills/dissolve-comments/reference/triage.md +++ b/plugins/code-tidying/skills/dissolve-comments/reference/triage.md @@ -82,7 +82,8 @@ A comment survives only if **all three** hold: reason once, provided the group **enumerates every member by file and line**; the sentence is what may be written once, never the naming. What never survives is length spent on justification narrative. Posture `balanced` reports an over-budget comment instead of rewriting - it; `conservative` proposes the rewrite. + it; `conservative` proposes the rewrite; `aggressive` keeps only a warning of consequence and + rewrites it to the budget, and `strip` keeps nothing here. **When the test fails.** A comment that passes criterion 1 and fails criterion 2 is **deleted** under `strict`, behind the same COMMENT-ONLY token proof class A uses, with its narrative staged @@ -92,8 +93,12 @@ Criterion 3 has its own treatment, the rewrite above; only criterion 2 sends a c Under `safe` mode and posture `conservative` this deletion is **proposed, never applied**. Those modes apply class-A deletions only, and a comment that reached this branch is class C whatever its -test returned. The mode ladder narrows what is applied, and it does not get to be widened by a -verdict reached inside it. +test returned. What the run may apply is set by the mode, never by a verdict reached inside it. + +Under `aggressive` and `strip` the criteria are not what decides. The survivor list in `SKILL.md` +does: an exempt surface, a paired record, and (under `aggressive`) a warning of consequence stay, +and every other comment here is staged and deleted behind the same token proof, recoverable +rationale or not. Criterion 2 is not evaluated for them, since its answer changes nothing. **Whole-file verdict.** Where the majority of a file's class-C comments carry contract, negative, or operational information, the file is contract-heavy rather than over-narrated. The report states