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
25 changes: 16 additions & 9 deletions .claude/skills/wave-run/task_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
task_prompt.py w1 U1 U2 U3 one prompt per task, separated by "=====" lines
task_prompt.py w4 S1

The task's table row (with the table header) or its "### <task>" section is pasted in verbatim, so the
subagent never opens the wave file. The first line of each prompt, starting with "#", is for the coordinator:
The task's "### <task>" section and its table row (with the table header) are pasted in verbatim, so the
subagent never opens the wave file. The section holds the spec; the row adds the tier and ownership. The first line of each prompt, starting with "#", is for the coordinator:
the Agent tool `model` to use. Anything the script can't infer is left as <FILL: ...>; fill it in before
dispatching.
"""
Expand All @@ -28,6 +28,7 @@
Do not change production code. Do not run git. Do not edit go.mod.
Navigate with the LSP tool (load it with ToolSearch "select:LSP"); get model shapes with the model-shape
skill (`make model T=<Model>`); never read pkg/model/core. On a failing test, follow the test-failure skill.
Never cat a whole file: grep -n or LSP documentSymbol first, then Read only the lines you need.
Create fixtures with the test factories. If a helper is missing, stop and report exactly what you need rather
than writing ORM calls in your test.
If you find a bug: write the test for correct behavior, add t.Skip("known bug: <describe>"), and put a
Expand Down Expand Up @@ -60,6 +61,7 @@
report it under needs:. Do not run git. Do not edit go.mod.
Navigate with the LSP tool (load it with ToolSearch "select:LSP"); get model shapes with the model-shape
skill (`make model T=<Model>`); never read pkg/model/core. On a failing test, follow the test-failure skill.
Never cat a whole file: grep -n or LSP documentSymbol first, then Read only the lines you need.
Behavior must not change: the E2E tests (e2e/), the browser tests (e2e/browser) and the seed crawl are not
edited. A test you move may change its call site but not its assertions. If an assertion has to change,
stop and report it.
Expand Down Expand Up @@ -119,9 +121,9 @@ def build(wave, task):
path = ROOT / "docs" / "plan" / f"{wave.lower()}.md"
text = path.read_text()
lines = text.splitlines()
excerpt, fields = from_table(lines, task)
if excerpt is None:
excerpt = from_section(lines, task) or from_paragraph(text, task)
row, fields = from_table(lines, task)
section = from_section(lines, task)
excerpt = "\n\n".join(x for x in (section, row) if x) or from_paragraph(text, task)
if excerpt is None:
sys.exit(f"task {task} not found in {path.relative_to(ROOT)}")

Expand All @@ -131,7 +133,9 @@ def build(wave, task):
if not tier:
m = re.search(r"\b(cheap|mid|strong)\b", excerpt)
tier = m.group(1) if m else ""
where = next((v for k, v in fields.items() if k.startswith("package")), excerpt)
m = re.search(r"\bOwns (.+?)\.(?=\s|\||$)", excerpt, re.S)
owned = m.group(1).strip() if m else None
where = owned or next((v for k, v in fields.items() if k.startswith("package")), excerpt)
pkgs = re.findall(r"`((?:pkg|cmd|e2e)/[\w/.-]+)`", where)
dirs = [p.rsplit("/", 1)[0] if p.endswith(".go") else p.rstrip("/") for p in pkgs]
pkg = " ".join(f"./{d}/..." for d in dict.fromkeys(dirs)) or "<FILL: ./pkg/x/...>"
Expand All @@ -149,11 +153,14 @@ def build(wave, task):
browser = wave.lower() == "w6"
if browser:
pkg = "./e2e/browser/... TAGS=browser"
body = TEMPLATE.format(task=f"{wave.upper()}.{task}", excerpt=excerpt, pkg=pkg, done=done, report=REPORT,
template = TEMPLATE
if wave.lower() == "w0":
template = template.replace("You are adding tests to pcom.", "You are building pcom's test infrastructure.")
body = template.format(task=f"{wave.upper()}.{task}", excerpt=excerpt, pkg=pkg, done=done, report=REPORT,
testcmd=(BROWSER_TESTCMD if browser else TESTCMD).format(pkg=pkg),
owns="<FILL: new _test.go files and testdata/ in "
owns=owned or ("<FILL: new _test.go files and testdata/ in "
+ ("e2e/browser/<area>_test.go" if browser else pkg if pkgs else "the task's packages")
+ ">")
+ ">"))
return "\n".join([header, body])


Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y libvips-dev

- name: Run tests
run: make test
run: make cover

- name: Upload coverage
uses: codecov/codecov-action@v7
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.cover/
coverage.out
16 changes: 14 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: shell tunnel lint test build check fix check-q test-q vet-q cover-q model
.PHONY: shell tunnel lint test test-short cover build check fix check-q test-q vet-q cover-q model

PKG ?= ./...

Expand All @@ -18,7 +18,19 @@ lint:
golangci-lint run ./... --timeout=5m

test:
go test ./...
go test -coverprofile=coverage.out ./...

test-short:
go test -short ./...

COVDIR := $(CURDIR)/.cover

cover:
@rm -rf $(COVDIR)
@mkdir -p $(COVDIR)
@GOCOVERDIR=$(COVDIR) go test -cover ./... -args -test.gocoverdir=$(COVDIR)
@go tool covdata percent -i=$(COVDIR) | perl -pe 's/\t\t\t/\n/g' | grep "coverage:" | grep -v github.com/can3p/pcom/pkg/model/core
@go tool covdata textfmt -i=$(COVDIR) -o coverage.out

build:
go build -v ./...
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ make lint # golangci-lint
```

Docker must be running: tests that touch the database start a Postgres
container via `testcontainers/postgres`.
container via `pkg/testutil/testdb`.

### psql access

Expand Down
2 changes: 2 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ignore:
- "pkg/model/core/**"
51 changes: 51 additions & 0 deletions docs/archive/history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Modernization history

Finished waves, newest last. Each entry records what was built, what turned
out wrong, what was left out on purpose, and what the wave cost in tokens.

## W0 — Test foundation (2026-09-26, branch `test/w0-foundation`)

**Built.**

- `pkg/testutil/testdb.New(t)` over gogo `v0.0.2`'s `testcontainers/postgres`
(a template database, one copy per test). pcom's dockertest-based copy is deleted, and the
existing DB tests use the new helper.
- `pkg/testutil/factory`: a builder per table, the graph relationships, and
readers in `read.go`, with a self-test that builds every entity. It doesn't import
`testing`, so the seed command can use it.
- `pkg/testutil`: `Must`, `fakesender`, `fakestorage`, `ginctx`, `golden`.
- `e2e/`: builds `./cmd/web` once with `-cover`, runs it per test against
its own database with a stub asset manifest, and drives it with a
cookie-jar client that sends the scraped CSRF token and exposes htmx
headers. Two smoke tests (anonymous home, factory user logs in to `/feed`).
- `make test` writes `coverage.out`, `make test-short` skips E2E, `make cover`
merges unit and E2E binary coverage into one per-package table;
`codecov.yml`; `docs/testing.md` rewritten with worked examples.
- `task_prompt.py` now pastes a task's `###` section together with its table
row, and takes ownership from "Owns …".

**Turned out wrong.**

- The plan assumed the E2E binary could simply be killed. It has no signal
handling, and a `-cover` binary writes coverage only on a normal exit. A build
overlay adds a SIGTERM handler. It can't go into `cmd/web`, because the cover tool ignores
overlays on the packages it instruments, and it can't go into a dependency, because the module cache
can't be overlaid. So it goes into `pkg/types`, a package without statements, which is left out of
`-coverpkg`.
- `go test -cover` gives the test process its own `GOCOVERDIR`; the harness
passes the binary the `-test.gocoverdir` value instead.
- Small shape changes against the plan: `Client.PostJSON` takes a path, not
an action name; `ginctx.New` takes options (`WithUser`, `WithCSPNonces`)
after the four planned arguments.

**Left out.** `pkg/feedops/testutil` stays, for W2.D8. (CI switched from
`make test` to `make cover` before merge, so Codecov sees E2E coverage: Q14.)

**Cost.** 5 sessions (coordinator plus 4 subagents: 2 sonnet builders, 1 haiku, 1 sonnet docs),
280 turns in total; the coordinator peaked at 148k context with 88k of tool
results, and the subagents at 43k–167k (the factories were the most expensive:
74 turns, 122k of results). 33 wasteful calls across all agents, nearly all
`cat` of whole files, plus 2 raw builds. Skills: wave-run and wave-close once
each. model-shape was used through `make model` (5 calls). LSP was loaded once
in total and test-failure never, because nothing failed. This is the first
recorded wave, so there is no earlier cost line to compare with.
6 changes: 3 additions & 3 deletions docs/implementation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Forward-looking only. This file is the **index**: status, ground rules and how
waves are run. Each wave's tasks live in their own file, `docs/plan/<wave>.md`
(`w0.md`, `w1.md`, … `wb.md`, `r1.md`, …). **Read this file and the one wave file
(`w1.md`, … `wb.md`, `r1.md`, …). **Read this file and the one wave file
you are working on; never the other wave files.**

Running a wave (prompts, dispatch, verification) is the `wave-run` skill;
Expand Down Expand Up @@ -44,7 +44,7 @@ Related documents:

| Wave | Name | Depends on | State | Branch |
|---|---|---|---|---|
| W0 | Test foundation | — (gogo `v0.0.2` released) | not started | `test/w0-foundation` |
| W0 | Test foundation | — (gogo `v0.0.2` released) | done | `test/w0-foundation` |
| W1 | Unit tests, no database | W0 | not started | `test/w1-unit` |
| W2 | Package tests against Postgres | W0 | not started | `test/w2-db` |
| W3 | End-to-end HTTP tests | W0 | not started | `test/w3-e2e` |
Expand All @@ -69,7 +69,7 @@ W0 ─────────┼── W3 (6 tasks) ──┼── W5 ──
R3: after W5 R6: any time
```

Each wave's tasks are in `docs/plan/<id>.md` (lowercase: `w0.md`, `wb.md`, `r1.md`).
Each wave's tasks are in `docs/plan/<id>.md` (lowercase: `w1.md`, `wb.md`, `r1.md`); a finished wave's file is deleted and its record moves to `docs/archive/history.md`.

**After W0 lands, W1, W2, W3, W4 and W6 are independent of each other** and
can run at the same time: about 40 tasks in total, each owning disjoint files.
Expand Down
17 changes: 17 additions & 0 deletions docs/lessons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Lessons

Generalizable lessons from running the waves. Wave-specific notes go in
`docs/archive/history.md`.

- **Coverage from a subprocess.** A binary built with `go build -cover` writes
its data only when it exits normally (main returns, or `os.Exit`). Stop it
with a signal it handles, not a kill. Under `go test -cover`, pass the child the
test's `-test.gocoverdir` value: the test process's own `GOCOVERDIR` is
not the directory the caller asked for.
- **Build overlays and coverage.** `go build -overlay` files are ignored by
the cover tool for instrumented packages, and files under the module cache
can't be overlaid at all. To inject test-only code, overlay a package that is
excluded from `-coverpkg`.
- **Subagents cat files.** Even with AGENTS.md's reading rules, W0's
subagents printed whole files 33 times and loaded LSP once. The task
preamble now says it explicitly.
3 changes: 2 additions & 1 deletion docs/open-questions.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ Raised by the 2026-09-21 modernization survey.
instead? That decouples templates and services from the ORM, at the cost
of mapping code. Deciding before R5 matters, because bob changes the
generated types anyway (see `docs/plan/r5.md`).

## Decided

- **2026-09-21. Q2, the API deletes any post:** fixed right away in PR #118,
Expand Down Expand Up @@ -76,3 +75,5 @@ Raised by the 2026-09-21 modernization survey.
- **2026-09-24. Browser tests:** playwright-go in `e2e/browser`, behind a
build tag, reusing the E2E harness and the factories (W6). No pixel
snapshots until browsers run in the tools container.
- **2026-09-26. Q14, E2E coverage in CI:** CI runs `make cover`, so Codecov
gets the merged unit and E2E coverage, `cmd/web` included.
2 changes: 0 additions & 2 deletions docs/plan/r6.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
- Drop `alecthomas/assert` for testify.
- Drop both `pkg/errors` and `friendsofgo/errors` for stdlib `errors` and
`fmt.Errorf("%w")`.
- `ory/dockertest` goes away with W0.T0.1, once pcom uses gogo's
testcontainers.
- Check the unmaintained `antonlindstrom/pgstore` and `volatiletech/null`
(the latter goes with bob).
- Remove `plan.txt` (its content can go to GitHub issues or `docs/`) and
Expand Down
Loading
Loading