Slice 8: v1 ships — integration coverage, runbook, spec corrections
- Five new integration test files raise the suite from 75 to 96 green: test_hygiene_vertical (7), test_branch_path_routing (4), test_metadata_pr_merge (3), test_cache_bootstrap (4), test_e2e_smoke (3). The smoke test walks propose → super-draft → edit branch → body-edit PR → graduate → active-RFC PR → merge → notification → hygiene-sweep deletion end-to-end. - deploy/RUNBOOK.md replaces the prior DEPLOY.md stub as a real runbook: prerequisites, first-time bring-up, day-2 ops (logs, DB backup, secret rotation, the §12 hygiene cadence), rollback shape, troubleshooting table. - backend/.env.example grows the SMTP block, HYGIENE_TICK_SECONDS, and WEBHOOK_EMAIL_BOUNCE_SECRET with inline commentary. - README points to RUNBOOK.md; the "what the build lets you do" section adds Slices 7 and 8. - docs/DEV.md gets a Slice 8 — shipped section; the "Next slice" footer becomes the v1-complete epitaph. - SPEC corrections per the §19.3 working agreement: §10.7 names the shared §12 sweep; §12 names the bot as actuator and the per-user branch_chat_seen preservation contract; §19.1 marks v1 complete and records Slice 8; the five §19.2 candidates Slice 8 folded in are marked settled with pointers at the resolution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+124
-60
@@ -288,6 +288,120 @@ adds the `email_opt_out_all` column to `users` for the bounce
|
||||
webhook. Topic 13 settled the rest of the §5 surface before the
|
||||
build started, so no further migrations were needed.
|
||||
|
||||
### Slice 8 — shipped
|
||||
|
||||
The hardening pass — the last slice of the v1 build. §12 + §10.7
|
||||
branch hygiene, the §19.2 candidates that cluster with the hygiene
|
||||
work, the dev/prod deployment shape, and the end-to-end smoke pass.
|
||||
|
||||
The §12 30/90 timers live in
|
||||
[`backend/app/hygiene.py`](../backend/app/hygiene.py) as a
|
||||
`HygieneScheduler` that mirrors `DigestScheduler`'s shape — same
|
||||
`start` / `stop` / `_loop` contract, same `HYGIENE_TICK_SECONDS`
|
||||
env override (default 3600), same `run_tick(now=...)` test seam so
|
||||
the integration tests compress the 30/90-day windows without
|
||||
monkey-patching the clock. Each tick runs four queries in the order
|
||||
"delete first, close second" — the 90-day boundary takes priority
|
||||
when a single sweep crosses both, which is the cache-bootstrap and
|
||||
clock-jump case the brief calls out. Order:
|
||||
|
||||
1. §10.7 90-day post-merge delete (against `state IN ('open', 'closed')`
|
||||
joined to a merged PR past the cutoff)
|
||||
2. §12 90-day stale-closed delete (closed branches past `closed_at +
|
||||
60d` since the prior 30d close)
|
||||
3. §11.5 30-day idle close (open branches with no PR past the
|
||||
cutoff)
|
||||
4. §10.7 30-day post-merge close (open branches with a merged PR
|
||||
past the cutoff)
|
||||
|
||||
The bot gains a `delete_branch` method that accepts `actor: Actor |
|
||||
None`; the timer paths pass `None`, the audit row lands with
|
||||
`actor_user_id=NULL` and `on_behalf_of=<bot login>` per §15.9's
|
||||
"system-generated events" rule — "the app" in the noun slot. The
|
||||
three action kinds (`close_idle_branch`, `delete_stale_branch`,
|
||||
`delete_post_merge_branch`) are intentionally absent from
|
||||
`notify._AUTO_WATCH_ACTIONS` and `notify._ROUTING`, so no
|
||||
notifications fire. The branches being touched are stale by
|
||||
definition; the affected population would be churn-grade noise per
|
||||
§15.4. Pinned branches skip both passes. Per-user `branch_chat_seen`
|
||||
cursors survive branch deletion — chat history is app-canonical, not
|
||||
cached.
|
||||
|
||||
The §19.2 candidates the hardening pass folded in:
|
||||
|
||||
- **Branch-name path routing.** Every `branches/<branch>` route in
|
||||
[`api_branches.py`](../backend/app/api_branches.py) and
|
||||
[`api_prs.py`](../backend/app/api_prs.py) is now `{branch:path}`.
|
||||
The bare `GET /api/rfcs/{slug}/branches/{branch:path}` is declared
|
||||
*last* among the branch-scoped GETs, so the deeper `threads` and
|
||||
`threads/{thread_id}/messages` GETs match before the greedy path
|
||||
matcher swallows their sub-paths. The literal-prefix POST
|
||||
`branches/main/promote-to-branch` doesn't collide with any other
|
||||
POST suffix; ordering there is incidental.
|
||||
- **Cache bootstrap from a pre-existing meta repo.** Exercised
|
||||
directly by `test_cache_bootstrap.py`: an audit-log-empty FakeGitea
|
||||
with PRs whose `gitea_opener` is the bot, the trailer parsed from
|
||||
the body, the raw login as last resort. The `_resolve_actor`
|
||||
fallback chain Slice 1 introduced now has an explicit test surface
|
||||
against history the bot did not author.
|
||||
- **In-app merge for metadata PRs.** [`api_prs._require_pr`](../backend/app/api_prs.py)
|
||||
extends to `pr_kind='meta_metadata'`. The diff-rendered review
|
||||
surface degrades gracefully (a metadata PR has no body diff worth
|
||||
reviewing); the merge gesture lands in-app rather than forcing the
|
||||
Gitea round-trip.
|
||||
- **Graduation rollback's branch cleanup.** [`api_graduation._undo_open_pr`](../backend/app/api_graduation.py)
|
||||
now deletes the `graduate-<slug>-<6hex>` branch after closing the
|
||||
PR, so failed-graduation branches don't accumulate on the meta
|
||||
repo across retries.
|
||||
- **Email bounce webhook authentication seam.** [`api_notifications.email_bounce`](../backend/app/api_notifications.py)
|
||||
checks `WEBHOOK_EMAIL_BOUNCE_SECRET` when set, refusing unsigned
|
||||
POSTs with 401. Unset preserves the v1 unauthenticated behavior
|
||||
for dev.
|
||||
|
||||
The deployment shape: [`deploy/RUNBOOK.md`](../deploy/RUNBOOK.md) is
|
||||
rewritten from the prior `DEPLOY.md` stub into a real runbook —
|
||||
prerequisites, first-time bring-up, day-2 operations (logs, database
|
||||
backup, secret rotation, the §12 hygiene cadence), rollback shape,
|
||||
and a troubleshooting table. The README's `.env` table grows the
|
||||
SMTP block, `HYGIENE_TICK_SECONDS`, and `WEBHOOK_EMAIL_BOUNCE_SECRET`.
|
||||
[`backend/.env.example`](../backend/.env.example) carries the same
|
||||
fields with inline commentary.
|
||||
|
||||
Slice 8 ships covered by:
|
||||
|
||||
- [`test_hygiene_vertical.py`](../backend/tests/test_hygiene_vertical.py)
|
||||
— seven tests covering the 30d close, 90d delete, 90d post-merge
|
||||
delete, pinned-branch exemption, per-user-cursor preservation, the
|
||||
no-notification decision, and the graduation-rollback branch
|
||||
cleanup.
|
||||
- [`test_branch_path_routing.py`](../backend/tests/test_branch_path_routing.py)
|
||||
— four tests covering the slashed-branch GET, the deeper threads
|
||||
GET still routing for both slashed and unslashed branches, and a
|
||||
POST against a slashed branch.
|
||||
- [`test_metadata_pr_merge.py`](../backend/tests/test_metadata_pr_merge.py)
|
||||
— three tests covering the in-app merge of a `meta_metadata` PR,
|
||||
the contributor refusal, and the withdraw path.
|
||||
- [`test_cache_bootstrap.py`](../backend/tests/test_cache_bootstrap.py)
|
||||
— four tests covering the audit-log / trailer / raw-login fallback
|
||||
chain in `_resolve_actor`.
|
||||
- [`test_e2e_smoke.py`](../backend/tests/test_e2e_smoke.py) — three
|
||||
tests: the full lifecycle walk (propose → super-draft → edit
|
||||
branch → body-edit PR → graduate → active-RFC PR → merge →
|
||||
notification → hygiene-sweep deletion), the bounce-webhook signing
|
||||
seam refusing unsigned POSTs when the secret is set, and the
|
||||
unauthenticated open path when the secret is unset.
|
||||
|
||||
The full Slices 1–8 test suite is 96/96 green. The FakeGitea grew a
|
||||
`DELETE /repos/{owner}/{repo}/branches/{branch:path}` handler and a
|
||||
slashed-branch `GET /branches/{branch:path}` for these tests.
|
||||
|
||||
No schema migrations. Two minor spec corrections — §12 grew an
|
||||
explicit note that the per-user `branch_chat_seen` cursor survives
|
||||
branch deletion (the §11.5 contract made implicit; running code
|
||||
asked for the load-bearing line to live in §12 too), and §10.7
|
||||
grew a one-line pointer that the timer rides on §12's sweep rather
|
||||
than its own schedule.
|
||||
|
||||
### Slice 7 — shipped
|
||||
|
||||
The §14 chrome, the §15 settings neighborhood, and the §6/§17 admin
|
||||
@@ -590,65 +704,15 @@ spec:
|
||||
renames — these are not shipped in any slice. They earn their own
|
||||
topic sessions when use surfaces evidence they matter.
|
||||
|
||||
## Next slice
|
||||
## After v1
|
||||
|
||||
**Slice 8: hardening — the last slice of the v1 build.**
|
||||
v1 ships. Slice 8 was the last slice of the build. Subsequent sessions
|
||||
pick from §19.2 by user choice per §19.3's working agreement — drive a
|
||||
topic to decision, fold it in, update §19.2, hand off. They need not
|
||||
be sequential; the user picks the next topic based on what evidence
|
||||
the running app surfaces.
|
||||
|
||||
With Slice 7 shipped, every structural beat the spec commits to is
|
||||
live and every surface the framework exposes has chrome around it.
|
||||
What remains is the hardening pass that lets a single-operator
|
||||
deployment actually run end-to-end without hand-holding. Three
|
||||
pieces hang together:
|
||||
|
||||
- **The §12 30/90 branch-hygiene timers.** §11.5 names the branch
|
||||
lifecycle (open → merged → 30d read-only → 90d deleted-by-bot,
|
||||
with the per-user message-cursor preservation contract); §12
|
||||
formalizes the policy. The wiring is a scheduled task next to
|
||||
the existing `DigestScheduler` — same `run_tick` test-seam shape.
|
||||
The §10.7 90-day deletion timer Slice 3 left explicitly deferred
|
||||
lives here too. Touches `cache.Reconciler` (the natural place to
|
||||
fire the hygiene sweep), `bot.delete_branch` (the §12 actuator,
|
||||
not yet exercised), and the §19.2 cache-bootstrap topic if the
|
||||
hygiene sweep also rebuilds branch state from Gitea after a
|
||||
cache wipe.
|
||||
- **An end-to-end smoke pass** over the working surfaces. Propose
|
||||
→ super-draft → branch → PR → merge → graduate → active-RFC PR
|
||||
→ notification → inbox → email — one or two `test_e2e_smoke.py`
|
||||
cases that exercise the seams a per-slice test wouldn't. Plus
|
||||
the §19.2 follow-ons the hardening pass is the natural place to
|
||||
fold in: branch-name path routing (`{branch:path}` everywhere
|
||||
with route-ordering discipline), cache bootstrap from a
|
||||
pre-existing meta repo (the audit-log-first attribution shape
|
||||
exercised against history the bot did not author), in-app merge
|
||||
for metadata PRs, the graduation rollback's branch cleanup, and
|
||||
the small Slice-2-onward follow-ons that are deferred until the
|
||||
hardening pass demands them.
|
||||
- **The dev/prod deployment shape.** `deploy/` already carries an
|
||||
nginx vhost, a systemd unit, and a runbook stub. Slice 8 proves
|
||||
the bring-up against a fresh host, settles the secret-material
|
||||
handling (the existing `.env.example` plus the §15.4 SMTP
|
||||
wiring), wires the §6 / §15.4 SMTP credentials, and lands the
|
||||
README updates that take a new operator from `git clone` to a
|
||||
signed-in browser.
|
||||
|
||||
What Slice 8 does NOT own:
|
||||
|
||||
- New surfaces. The v1 surface is complete; the hardening pass is
|
||||
about making what's there resilient, observable, and operable.
|
||||
- The §16 deferred items.
|
||||
- The §19.2 candidate set as a whole — the hardening pass folds in
|
||||
the candidates that naturally cluster with hygiene timers, cache
|
||||
rebuild, and deployment; the rest stay queued for post-v1
|
||||
sessions.
|
||||
|
||||
The carryovers Slice 8 inherits — the full §11.5 / §12 spec text,
|
||||
the existing `cache.Reconciler` and `DigestScheduler` shape, the
|
||||
deploy/ infrastructure, and the 75/75 green test suite.
|
||||
|
||||
The next build session should read `SPEC.md`, `README.md`,
|
||||
`docs/DEV.md`, and `SPEC.md`'s §19.1 and pick up Slice 8 cleanly
|
||||
without re-briefing. The working agreement in §19.3 continues to
|
||||
apply: implement the slice, correct the spec only where running
|
||||
code reveals it was wrong at a structural level, accumulate new
|
||||
candidate topics in §19.2, do not extend the spec beyond what the
|
||||
slice requires.
|
||||
There is no "next slice" footer here because there isn't a next
|
||||
slice. The work mode has shifted: the build is the source-of-truth
|
||||
artifact, and §19.2 is the queue of decisions to settle when their
|
||||
turn comes.
|
||||
|
||||
Reference in New Issue
Block a user