Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .claude/skills/refactoring-entry/LESSONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Lessons

Append-only log, newest at the bottom. Each entry: what happened, what it cost, what to do
differently. A line marked **rule** is binding on the next run until it is folded into SKILL.md,
brief.md or workflow.js and deleted here. Keep the calibration table current.

## Calibration

| knob | value | why |
|---|---|---|
| hedgehog (Scala) | `qa.hedgehog::hedgehog-core:0.14.0`, `hedgehog-runner:0.14.0` | 0.13.0 is refused by scala-cli's outdated-dep check |
| Scala | 3.3.4 via scala-cli 1.12.x | matches the site's other Scala |
| Haskell | GHC 9.8.4 + hedgehog, docker image `cp-hedgehog` | no host ghc; nix store is read-only |
| `cp-hedgehog` build | ~2.5 min (cabal update + install --lib hedgehog) | one-off per machine |
| test count | 100 default; **500** when a property has an equality boundary | measured: 100 tests missed `<` vs `<=` mutants 60–90% of the time |
| generator ranges | narrow (−30..30, limits 0..20) for boundary-heavy code; full `Int` only in adversarial probes | narrow ranges hit `next == -limit` often; wide ones almost never |
| pane width | source lines ≤ 72 chars | 77-char comment lines overflowed a 608px pane at 0.78rem mono |
| workflow | 22 agents, ≈ 65 min wall-clock over two runs (12 min to the first crash + 53 min resumed); the reviewer's mutation + probe round is the long pole (≈ 15–20 min) | extract-method, 2026-09-03 |

## 2026-09-03 — 01 Extract method (first entry; the skill was extracted from this run)

- **Toolchain.** No ghc on the host; `nix shell`/`nix-shell` fail (registry needs network config; store is
read-only). `docker run haskell:9.8-slim` + `cabal install --lib hedgehog` works; baked into `cp-hedgehog`
and into run.sh's fallback. scala-hedgehog 0.14.0: `Test.renderReport(..., ansiCodesSupported = false)`;
`object Props extends Properties` brings its own main, so run with `--main-class spec`.
- **Reviewer, round 1 (blocking).** Example 02's generators were too weak: mutants `tx < 0 → tx <= 0` and
`next < -limit → next <= -limit` survived most runs in both languages. Cause: wide ranges (−500..500)
rarely hit the equality boundary and `fee = 0` hides the fee branch. Fix: ranges −30..30 / 0..20 / fee
1..20 and `withTests 500`; both mutants then caught 10/10. → became the generator rules in brief.md.
- **Reviewer, round 1 (optional, applied).** Haskell Before had the step as a `where` binding while Scala
had an inline lambda; the two Befores were not parallel. Rewritten as an inline lambda. → rule in brief.
- **Reviewer, round 2 (optional, applied).** Comment "never throws" overclaimed: GHC `div` overflows at
`minBound / -1`. Reworded, and the page says the partiality is preserved, not fixed. `foldl` → `foldl'`.
- **Citation skeptic.** One of 15 references was refuted on its *claim*, not its existence: "spends most of
its effort on this analysis" about Stocker's Scala refactoring thesis was not supported; softened to
"must perform exactly this analysis". → skeptic prompt now names overstatement as a failure.
- **Harness.** A session limit killed the reviewer mid-run; `Workflow({scriptPath, resumeFromRunId})`
replayed the 18 finished agents from cache and re-ran only the reviewer. Cached agents may return empty
results; read `journal.jsonl` first.
- **Jekyll.** `exclude:` the sources dir in `_config.yml`; `include_relative` still reads excluded files.
`{% highlight scala %}{% include_relative … %}{% endhighlight %}` works inside raw HTML `<div>`s, so the
side-by-side panes need no `markdown="1"`. Dot-dirs (`.scala-build`, `.bsp`) are ignored by Jekyll
automatically. Two 80-column panes do not fit the 68ch prose measure; `.rf-pair` bleeds to
`min(100vw - 3rem, 1360px)` on ≥1000px viewports.
- **Time.** Research + 15 citation checks ran in parallel with the examples build (both ≈ 10 min); the
reviewer's two rounds plus the fix took most of the 53-minute resumed run; diagrams with browser
render-checks ≈ 15 min. Toolchain probing before the workflow cost ≈ 10 min the first time. The main
agent used the wait to write the page and check the layout in a scratch build — do the same.
- **Browser contention.** The diagram agent and the main agent share one Chrome; resize/reload calls on
the main agent's tab hang for minutes while the other is screenshotting. Do layout screenshots before
the Diagrams phase starts, or after it ends.

## 2026-09-03 — 01 Extract method renamed to "Extract / Inline method" (post-PR-12 review round 3)

- **Reviewer (kryptt).** Rename the entry to "Extract / Inline Method" and add a first **Motivation** section
stating why you would go in either direction: duplication and long methods drive Extract; speculative
generality and coupling drive Inline. Also (skill-wide): Motivation is always the first section of an entry,
and every entry shows up with its dual. → folded into SKILL.md §3 (page section order + the always-first
Motivation section with the two-direction motivators), brief.md, and the workflow.js research prompt
(Motivation heading, first, with the same motivator guidance). Reference page updated: title, subtitle, intro,
new Motivation section.

- **Rule.** Prompt-forced page structure in workflow.js: the research prompt now enumerates the exact H2
headings in order with Motivation first — check that list whenever a section is added or renamed, or the
assemble-the-page step and the research step will disagree on shape.

## 2026-09-03 — Reference citations are links (post-PR-12 review round 6)

- **Reviewer (kryptt).** In-text `[n]` references must be links that navigate to the reference list.
Also update the skill with this info. Implemented:
- In-text citations → `[[n](#ref-n)]`, rendering as [<a href="#ref-n">n</a>].
- Reference list items → raw HTML `<li id="ref-N">…</li>` inside a plain `<ol>`.
- **Kramdown traps (rule).** Kramdown 1.x will not process markdown inside raw block HTML (`<li>`, even
with `markdown="1"` — it escapes the inner tags); IALs on numbered list items (`{: #ref-N}` on a line
after the item) attach to the *enclosing* `<ol>` at a break point and split the list. The working
pattern is: a single `<ol>` with raw `<li id="ref-N">` items, and hand-render inline markdown to
`<em>`/`<a>` yourself. Folded into SKILL.md §3, workflow.js research prompt, and brief.md.
114 changes: 114 additions & 0 deletions .claude/skills/refactoring-entry/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
name: refactoring-entry
description: Add one entry to the Constructive Programming refactoring catalogue at /refactorings/ — research with verified citations, 3 Before/After examples in Scala 3 and Haskell, hedgehog property-based proof of equivalence, diagrams, page, PR. Use when asked to "do the next refactoring", "add <name> to the catalogue", or "write the <name> entry".
---

# Refactoring catalogue entry

One entry = one refactoring from `_data/refactorings.yml`, read as an equation between programs
and *checked* with property-based tests. The reference implementation is `/refactorings/extract-method/`
(page `pages/refactorings/extract-method.md`, sources `pages/refactorings/extract-method/`).
Copy its shape; do not redesign it.

Each entry is written **with its dual**: the refactoring and its inverse are one equation, and the
page presents both directions ("Extract / Inline", "Replace X with Y / Replace Y with X"). The list name
in `_data/refactorings.yml` carries both sides, e.g. `Extract / Inline method`.

## 0. Read the lessons first

Read `LESSONS.md` in this directory, top to bottom. It is the memory of every previous entry: what
the reviewer caught, what broke, what the calibration table says. Anything marked **rule** there
overrides the defaults below. If a lesson has become permanent, it has already been folded into
the steps below and the `workflow.js` prompts; if you find one that has not, fold it in now.

## 1. Set up

1. Branch from `master`: `refactoring/<slug>`.
2. Pick the entry: the first item in `_data/refactorings.yml` without a `slug`, unless the user
names one. Add `slug: <slug>` to it (that is what links it on the index page).
3. Toolchain check (the workflow agents assume these work):
- `scala-cli --version` (hedgehog `qa.hedgehog::hedgehog-core:0.14.0` + `hedgehog-runner:0.14.0`).
- `docker image inspect cp-hedgehog` — if missing, `sh pages/refactorings/extract-method/run.sh`
builds it (about 2.5 minutes). GHC is not installed on the host; nix is read-only.
4. Write the brief to the scratchpad: copy `brief.md` from this directory, fill the `<...>` fields.
The workflow agents read the brief, not this file.

## 2. Run the workflow (research ∥ examples → verify loop → diagrams)

```
Workflow({
scriptPath: ".claude/skills/refactoring-entry/workflow.js",
args: {
slug: "<slug>", name: "<Name>", number: <n>, total: 35,
brief: "<abs path to brief.md>", scratch: "<abs scratchpad dir>",
repo: "<abs repo root>",
seeds: ["<paper or tool that anchors the OO side>", "<the FP-side paper>", ...],
examples: ["<example 1 idea>", "<example 2 idea>", "<example 3 idea>"]
}
})
```

- The script runs two tracks in parallel: **research → per-citation skeptics → fix-up**, and
**examples → reviewer (mutation + adversarial probes) → fix, up to 3 rounds → diagrams**.
- If an agent dies (session limit, API error), relaunch with `resumeFromRunId`; finished agents replay
from cache. Read `journal.jsonl` before assuming a result is empty.
- While it runs: write `pages/refactorings/<slug>.md` from the extract-method page (next step), and
build the site in a scratch copy with stub SVGs to catch Liquid errors early.

## 3. Assemble the page

Start from `pages/refactorings/extract-method.md`. Keep: front matter shape (`layout: page`,
`subtitle` carries the koan and names both directions, `permalink: /refactorings/<slug>/`, `tags`,
`hide: true` so entry pages stay out of the top-right nav), the crumb line, the
section order (Motivation · The move · To and from + koan figure ·
Three examples · Pitfalls + a collapsible "The functional reading" footnote ·
Verification · References) — **Motivation is always
the first section, before The move** — the `rf-pair` /
`rf-figure` / `rf-spec` markup, and the `run.sh` instructions. Replace the prose with
`<scratch>/research.md` (after its citation fix-up) and write one paragraph per example from
the examples agent's `what_changes` notes.

Motivation states why you would move in either direction: what smell drives the move, and
what smell drives its inverse. Extract-style motivators include duplication and methods that
have grown too long; inline-style motivators include speculative generality and coupling
through a seam's implementation rather than its contract. Two short paragraphs, one per
direction, before any mechanics — the reader should know when to reach for the move before
how to perform it.

Rules that came from review, keep them:
- In-text citations are links: write `[[n](#ref-n)]` (render → [<a href="#ref-n">n</a>]);
each reference list item is raw HTML `<li id="ref-N">…</li>` inside a plain `<ol>`, with
markdown inline formatting hand-rendered to `<em>` and `<a href>` (Kramdown will not process
markdown inside raw block HTML, and IALs on numbered items break the list).
- Source lines ≤ 72 characters or the pane scrolls horizontally on a 1280px screen.
- No `{{` `}}` `{%` `%}` in any included source (Liquid runs before highlighting).
- Copy `run.sh` from extract-method unchanged into the new sources directory.
- Add the new sources directory to `exclude:` in `_config.yml` (sources are included via
`include_relative`, not published as static files).
- Do not claim totality or purity the code does not have (e.g. `Int` `div` overflow).

## 4. Verify before the PR

1. `sh pages/refactorings/<slug>/run.sh` ends with `all properties passed` — run it yourself, do
not trust the agent's report alone.
2. `bundle exec jekyll build --destination <scratch>/site` exits 0; open the page over
`python3 -m http.server` and screenshot at 1280 and 390 wide; no horizontal page scroll,
diagrams legible in light and dark.
3. Re-read the reviewer's last report; every **blocking** item is fixed, every optional item is
either applied or deliberately declined in the PR description.

## 5. Ship

Commit sources + page + data + config on the branch; push (SSH remote works; if `gh auth status`
fails ask the user to run `! gh auth login -h github.com -p ssh -w`); open the PR with the
reviewer's mutation table and probe summary in the body. The PR preview URL is
`https://www.constructive.dev/pr-preview/pr-<N>/refactorings/<slug>/`.

## 6. Improve the skill (mandatory, last)

Append a dated entry to `LESSONS.md` with: reviewer findings (blocking and optional), toolchain
failures and their fixes, wall-clock and agent count from the workflow result, anything the
prose reviewers (citation skeptics) refuted, and any generator/test-count calibration that
changed. Then act on it: if a lesson is a rule, edit the rule into this file or into the prompts
in `workflow.js` so the next run does not have to rediscover it; if a lesson retires an older one,
delete the older one. Commit the skill change in the same PR.
88 changes: 88 additions & 0 deletions .claude/skills/refactoring-entry/brief.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Brief: "<Name>" — entry <n> of the Constructive Programming refactoring catalogue

## Context
- Repo: <repo root> (Jekyll site for www.constructive.dev), branch `refactoring/<slug>`.
- Section: /refactorings/ (index at pages/refactorings.md, list in _data/refactorings.yml). This entry lives at
/refactorings/<slug>/ (page pages/refactorings/<slug>.md, sources pages/refactorings/<slug>/).
- Reference entry, copy its shape exactly: pages/refactorings/extract-method.md and pages/refactorings/extract-method/.
- Methodology framing (pages/about.md): Constructive Programming = restrictions that make code easier to reason about:
referential transparency, totality, termination; Curry–Howard–Lambek. Refactorings are read as *equivalences between
programs* that you can state and CHECK — hence property-based tests proving before == after.
- Audience: senior engineers and CTOs who know OO refactoring (Fowler) and want the typed-FP reading of it.
- Every entry is presented "to & from": the move AND its inverse (<Name> <-> <inverse name>). The catalogue name
carries both sides ("Extract / Inline method"), so does the page title and subtitle; the two directions are
one equation.

## This refactoring
Every entry carries the structure: Motivation first, then the move, the
functional reading (a collapsible footnote inside Pitfalls), to & from,
examples, pitfalls, verification, references. Fill the fields below; omit one
when it does not apply.
- OO reading: <one paragraph: what the OO/imperative world calls it, where it is catalogued — ONLY when
one exists; many moves (higher-order, higher-kinded, dependent-typed) have no OO counterpart, and for
those the OO reading is dropped entirely>
- FP reading: <one paragraph: what it is in a referentially transparent language; which papers/transformations>
- Dual: <the reverse move — ALWAYS present. Every refactoring is presented as a pair: extract / inline
method, introduce / eliminate generics, ... The catalogue name, page title and subtitle carry both
sides; the two directions are one equation>
- Motivation (page section 1, before any mechanics): <why go in each direction — the smell that drives the move,
the smell that drives its inverse (e.g. duplication / long methods vs speculative generality / coupling)>
- Known anchors to verify and cite (find more; verify all): <list>
- References render as raw HTML: each item is `<li id="ref-N">…</li>` inside a plain `<ol>` (Kramdown
won't run markdown inside raw block HTML, so inline `*em*` → `<em>` and `<url>` → `<a href>` by hand).
In-text citations are the links `[[n](#ref-n)]`.
- Example ideas (adjust if you find better, keep the progression simple → free variables/effects → recursion/laziness):
1. <...>
2. <...>
3. <...>

## Code layout (examples agent writes these; reviewer runs them)
pages/refactorings/<slug>/
run.sh # copied unchanged from extract-method; runs every NN-*/ dir in both languages
NN-<ex>/Before.scala # object Before { ... } the pre-refactoring program
NN-<ex>/After.scala # object After { ... } the refactored version (same public entry point signature)
NN-<ex>/Spec.scala # hedgehog property: forAll generated inputs, Before.f(x) ==== After.f(x). `@main def spec()`.
NN-<ex>/Before.hs # module Before where ...
NN-<ex>/After.hs # module After where ...
NN-<ex>/Spec.hs # hedgehog property, main :: IO (); exit non-zero on failure
- The web page `include_relative`s these files verbatim into highlighted panes, so:
* every file self-contained, readable, SHORT (Before/After 8–25 lines each); lines ≤ 72 characters;
* NO `{{`, `}}`, `{%`, `%}` sequences anywhere (Liquid would eat them);
* one-line comment at the top of Before/After saying what the program does; no essay comments;
* the Scala and Haskell Befores must be parallel (same shape: if Scala has an inline lambda, so does Haskell).
- Scala: Scala 3.3.x, directives at the top of Spec.scala only:
//> using scala 3.3.4
//> using dep qa.hedgehog::hedgehog-core:0.14.0
//> using dep qa.hedgehog::hedgehog-runner:0.14.0
Run: `scala-cli run pages/refactorings/<slug>/NN-ex --main-class spec` (Properties has its own main; name ours `spec`)
Known-good hedgehog 0.14.0 runner (renderReport's 4th param is `ansiCodesSupported`):
import hedgehog.*, hedgehog.core.*, hedgehog.runner.*
object Props extends Properties:
def tests: List[Test] = List(property("name", prop))
def prop: Property = for x <- Gen.int(Range.linear(-100, 100)).forAll yield Before.f(x) ==== After.f(x)
@main def spec(): Unit =
val results = Props.tests.map { t =>
val r = Property.check(t.withConfig(PropertyConfig.default), t.result, Seed.fromTime())
println(Test.renderReport("Props", t, r, ansiCodesSupported = false)); r.status }
if !results.forall(_ == Status.ok) then sys.exit(1)
`.withTests(500)` on a property raises its count.
- Haskell: GHC 9.8.4 + hedgehog. No ghc on this machine; docker image `cp-hedgehog` (run.sh builds it if missing):
docker run --rm -v "$PWD/pages/refactorings/<slug>:/w" -w /w cp-hedgehog runghc -iNN-ex NN-ex/Spec.hs
(from the repo root). Spec.hs: `main = do ok <- checkParallel (Group "Props" [...]); unless ok exitFailure`.
`withTests 500 $ property $ do ...` raises the count. Prefer `foldl'` over `foldl` for strict accumulators.
- Run everything at once: `sh pages/refactorings/<slug>/run.sh` (from anywhere).

## Generators (from LESSONS.md — the reviewer will mutation-test them)
- Design generators around the program's boundaries (equality tests, zero, empty, sign changes): small ranges
(e.g. -30..30) hit boundaries far more often than wide ones; exclude values that make a branch invisible
(e.g. fee = 0). Use 500 tests when a boundary matters.
- A second property per example is expected when natural (the extracted piece on its own; a law it obeys).
- In Haskell probe laziness/strictness with `undefined` where the refactoring could change what is forced;
in Scala reason about evaluation count (def vs val, by-name) and say so in a comment when it matters.

## Style
- Site prose: crisp, editorial, no hype; sentences, not bullets, in body text (the caveats section may use a list).
- Claims about tools and papers must be modest and literally supported by the cited source.
- Code: idiomatic, set in a very common domain subject matter, and just enough code to get the point
across — no accidental complexity, no clever tricks; the refactoring must be the only difference
between Before and After.
Loading
Loading