diff --git a/MEMORY.md b/MEMORY.md index a64d3a7..b77cfd8 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -8,10 +8,19 @@ ## Now / next (the resume point) -- **Nothing is in flight.** `main` is the complete state; no open PRs; the only - branch is `main`. v1.4.1 (docs-only, migration-readiness) is committed on - `main` — if no `v1.4.1` tag exists yet, tagging it is pending the owner's - confirmation (a tag push triggers the release; see CLAUDE.md → Releasing). +- **In flight: the v2 design doc** + (`docs/superpowers/specs/2026-08-04-v2-github-backend-design.md`, branch + `docs/v2-design`). v2 makes every item a GitHub issue (collision-free ids **by + construction** — retiring the whole #12/#13/#14 triad), a 4-level + release→milestone→feature→task tree, and a **pluggable `Backend` interface** + (GitHub is one impl). Settled via a ten-fork brainstorm + an adversarial + (Fable) review; open items are flagged in the doc. No v2 *code* yet. v1.4.1 is + committed and tagged on `main`. +- **Strategic direction:** finish the 1.x queue (below) → **freeze 1.x** → build + v2 against that doc. The freeze has a HARD prerequisite (see the #14 bullet): a + `selfupdate` cross-major guard must ship in the last 1.x release *before* v2 + lands on `main`, or vendored 1.x copies (`taxjot`/`byteorb`/`stead`) + self-update across the major boundary into a different file format. - **Next up, if told "continue where we left off": issue #14 — `validate --refs`**, a reference-integrity check. Agreed shape (triage comment on the issue): v1 detects *dangling* refs only (`@deps=`/`@rel=`/`## ` notes header pointing at an ID @@ -20,7 +29,11 @@ in `tasks.py` is already half the engine; short-form IDs (`T-12` ≡ `T-0012`) must be handled (see the v1.3.0 LEARNINGS entry). Fold #25 (validate silently ignores unknown args) into the same round — that bug becomes dangerous the moment - `validate` grows its first flag. + `validate` grows its first flag. **Also bundle into this same last-1.x release a + `selfupdate` cross-major guard** (refuse cross-major updates, or repoint + canonical to a frozen `v1` branch) — HARD prereq before v2 merges to `main` + (v2 doc → Sequencing). v2's GitHub-issue ids make the collision triad moot, so + #14 is explicitly a *1.x-only* deliverable. - Queue after #14 (recommended order, from the issue-triage comments): #16 (CLI verb for the `# Milestones` registry), #11's remaining half (`note ID` verb — retitle shipped in v1.2.0), #6 (refuse mutating commands on main), #5 (docs: CLI-only vs @@ -30,7 +43,15 @@ `@pr`/`@branch` freshness). #13's `reserve N` stays deferred unless a real gap is observed (recorded on the issue). -## What just happened (2026-06-21 → 2026-07-15) +## What just happened (2026-06-21 → 2026-08-04) + +**2026-08-04 — v2 planning opened.** A ten-fork brainstorm plus an adversarial +(Fable) review produced the committed v2 design doc: GitHub issues as a +pluggable synced backend, identity ⟂ organization, allocate-on-`new` / +batch-on-`sync`, prose repo-primary (offline-first preserved). No v2 code yet; +the 1.x queue ships first, then a 1.x freeze. See the doc's Sequencing. + +**2026-06-21 → 2026-07-16 — the six-release 1.x run.** Six releases, each driven by real usage in consumer repos. The *why* behind the decisions is in `LEARNINGS.md`; the renumber and id-allocation rounds also have full diff --git a/docs/superpowers/specs/2026-08-04-v2-github-backend-design.md b/docs/superpowers/specs/2026-08-04-v2-github-backend-design.md new file mode 100644 index 0000000..f3d3045 --- /dev/null +++ b/docs/superpowers/specs/2026-08-04-v2-github-backend-design.md @@ -0,0 +1,394 @@ +# Design: v2 — GitHub Issues as the synced backend + +**Status:** DRAFT. Load-bearing decisions settled in a brainstorm with the +maintainer on 2026-08-04 (10 forks + the backend abstraction, below), then +revised the same day after an adversarial review: the offline-first fork +resolved to "prose is repo-primary" (decision 7c), and three foundational fixes +folded in — file-authoritative in v2.0, a reconciliation nonce (decision 11), +and a `selfupdate` cross-major guard (Sequencing). Remaining items (exact file +layout, id display, config/repo-binding, migration mechanics, pull semantics, +GitHub operational ceilings) are written here as *recommendations to red-pen*, +not settled. + +**Scope:** this is the design for the **next major version (2.0)**. It +deliberately renegotiates two of v1's founding constraints — see "Founding +tension". It is *not* a 1.x change. The current 1.x queue (finish `#14` +`validate --refs` + `#25`, then freeze the 1.x line) ships first; see +"Sequencing". + +--- + +## Motivation + +Every hard problem v1 fought was a symptom of **locally-allocated IDs**. Two +branches each run `new`, each reads the same high-water mark from their local +`TASKS.md`, and the merge produces a duplicate (`T-2006`, byteorb, 2026-07-02). +The whole triad exists to cope with this: `#12` renumber (fix), `#13` +allocation (prevent), `#14` validate --refs (detect). + +v2 removes the *cause*: **an item's identity is a GitHub issue number**, which +GitHub allocates atomically and globally. On the online path collisions are +impossible by construction; the offline path has a narrow residual window that +decision 11 (reconciliation nonce + random provisional handles) closes. The +triad is *mostly* retired: `renumber` and `--base` go away, but a +`validate --refs`-style **dangling-reference check becomes more valuable** in v2 +(references to issues later closed, transferred, or stripped of their `type:*` +label), so it survives as a v2 feature rather than dying with v1. + +Two more asks the maintainer raised, both delivered by the same model: + +- A real hierarchy: **release → milestone → feature → task** (v1 has only + feature → task, with milestones as a flat tag registry). +- Cheap **rehome / reorganize / rename** of any level, without breaking + references. + +## Founding tension (why this is 2.0, not 1.5) + +| v1 constraint | v2 status | +|---|---| +| Single file (`tasks.py`) | **Kept.** All engine stays in one file. | +| Zero third-party deps (stdlib only) | **Kept.** GitHub over stdlib `urllib` (as `selfupdate` already does) + a token. No `gh` dependency, no PyPI packages. | +| Python 3.8+ | **Kept.** | +| No network / runs anywhere | **Renegotiated, but the core promise is kept.** Structure *and* authored prose (descriptions, acceptance criteria) are repo-primary (decision 7c), so a clone is self-contained: all reads, edits, and views work fully offline. Only **allocation (`new`) and `sync`** need network. The concession: threaded *discussion* (issue comments) lives on GitHub, not in the clone. | + +## The spine: separate *identity* from *organization* + +Everything below falls out of one idea. + +- **Identity layer (the backend).** Every release, milestone, feature, and task + **is a backend item** whose id the backend allocates atomically and globally — + collision-free by construction. In the GitHub implementation an item is a + GitHub issue (distinguished by a `type:*` label) and the id is the issue + number. +- **Organization layer (local overlay).** The `release → milestone → feature → + task` tree is a structure that *references* backend ids. It is **mirrored to** + the backend's parent/child graph (GitHub **sub-issues**). In v2.0 the **file + is authoritative** for structure and the backend graph is its durable mirror + (authority may invert or go bidirectional in v2.1). Re-home = move a reference + / re-parent. Rename = edit a title. Neither touches identity, so neither + breaks references. + +Because identity ⟂ organization, the maintainer's headline ask (move a feature +to another milestone, a milestone to another release, rename any of them) is +always a cheap structural edit — never a renumber. + +### The backend is an interface; GitHub is one implementation + +The engine talks to a small, provider-agnostic **`Backend` interface**; GitHub +is the first (and only shipping) implementation. Nothing in the core model — +the 4-level tree, type-tagged items, hybrid status, the overlay, the verbs — is +GitHub-specific. GitHub-specific choices (issues + `type:*` labels, sub-issues +for the tree, labels for state/priority/effort, native Milestones, Release/tag +on ship) are **facts of the GitHub impl**, isolated behind the interface. This +keeps the single-file, zero-dep constraint (interface + impls all live in +`tasks.py`; the GitHub impl uses stdlib `urllib`), future-proofs against other +providers (GitLab, a self-hosted tracker, …), and makes the test suite clean: +an **in-memory fake backend** is just another impl, so e2e runs stay offline and +deterministic without mocking network calls. + +Minimum interface surface (provider maps each to its own primitives): + +- `allocate(item) -> id` — atomically mint an id (GitHub: create an issue). +- `update(id, fields)` — push structured fields (GitHub: title/labels/state). +- `link(child_id, parent_id)` / `unlink` — the tree (GitHub: sub-issues). +- `fetch(ids…)` / `list()` — pull remote structure + state for reconcile. +- `body_read/write(id, prose)` — the issue body / prose surface. + +The core operates only in terms of these; the collision-free guarantee is a +property the *impl* must provide (GitHub does, via issue numbers). A degenerate +local-only backend that allocates ids itself would forfeit that guarantee — so +"local-only" is explicitly **not** a supported production backend, only the +shape the in-memory test impl borrows. + +--- + +## Settled decisions (10 brainstorm forks + the backend abstraction) + +0. **The backend is an interface; GitHub is one impl** (cross-cutting; see the + spine). Everything numbered below describes the *core model* or the *GitHub + implementation* of it — the two are kept separate on purpose. + + +1. **Strict, total containment tree.** One release holds many milestones, one + milestone many features, one feature many tasks. Every item has **exactly + one** parent one level up. No item spans multiple parents. No orphan state. + +2. **Defaults at the container levels only.** A release default (e.g. `future`) + and a milestone default (e.g. `backlog`/`m1`) exist as real, named, + renamable containers that new items fall into by convention — so the tree is + always total without an "unassigned" special case. **Features and tasks are + always explicitly named** by a human; there is no default feature or default + task. Consequence: the smallest thing you create standalone is a *feature* + (auto-homing into `future → backlog`); a *task* always needs its named + feature (`--under`), as in v1. + +3. **Everything is a type-labelled issue.** `type:release`, `type:milestone`, + `type:feature`, `type:task`. Uniform identity at every level, collision-free + everywhere, nested via GitHub sub-issues. + +4. **Container identity comes from GitHub too.** Releases and milestones are + issues (not locally-allocated ids) precisely so two branches creating a + container cannot collide — local allocation would reintroduce the very + problem v2 kills. Name ≠ identity, so rename is free. + - **Release:** an issue. **No native GitHub Release object** as the planning + container — native Releases are tag-bound shipped-artifact objects, can't + nest milestones, and a `future` bucket has no tag. *Optional later:* link a + release-issue to a real GitHub Release/tag when it actually ships. + - **Milestone:** an issue (source of truth). *Optional projection (flag / + v2.1):* mirror each milestone-issue to a **native GitHub Milestone** and + stamp it onto child issues, purely so GitHub's UI lights up (progress bars, + `milestone:` filter). The issue graph stays authoritative; the native + Milestone is a downstream reflection. + +5. **Hybrid status at container levels.** Each container carries a small + *intentional* state (`open → active/current → closed`) **plus** a *computed* + progress rollup (`5/8 done`). The state is chosen (lets `next`/"current" + point at the active milestone/release the way v1's `Now` did); the progress + is derived, so it never lies. Tasks keep v1's lifecycle + (`todo/doing/done/skipped/deferred`). + +6. **Offline `new` → provisional id, reconciled on sync.** When allocation can't + reach GitHub, create with a clearly-marked, **randomly-suffixed** temporary + handle (e.g. `T-local-a3f2`, *not* sequential — so two offline branches can + never mint the same handle, and a merge juxtaposes rather than conflates + them; sequential handles would re-import the exact byteorb collision). Fully + usable locally; the next `sync` mints the real issue **parents-first** and + swaps in the real number, repointing local references. Idempotency is *not* + free from GitHub — see decision 11 for the mechanism. The commit-refusal guard + stays deferred (non-colliding handles make it unnecessary until proven + otherwise). + +7. **Content split (option c): structure + authored prose are repo-primary; + discussion is GitHub-only.** The **file owns structure** (title, state, + parent, deps, effort, priority, milestone) — small, orderable, conflict-clean. + The other v1 fields become **issue labels/attributes**. **Authored prose** + (description, acceptance criteria, v1's `# Notes`) *also* lives in the repo — + greppable and editable **offline** — and is **pushed to the issue body as a + mirror** on sync (push-dominant, last-writer-is-local; no free-text merge). + Only **threaded discussion (issue comments)** is GitHub-only (click-through): + it's the genuinely collaborative surface, and you never need last week's + comments to implement a task — you need the spec, and the spec is local. + - *Why not prose-on-GitHub-only* (an earlier draft): it over-rotated — the + merge hazard argues only against *bidirectional* prose sync, not against + prose *living* in the repo. Push-dominant dissolves it, and keeping prose + local preserves the value prop v1 is built on (a self-contained clone). + - *Edge — body edited in the GitHub UI* would be clobbered by push. Handle it + the repo's way: on pull, **detect divergence and report, don't merge** (the + "detect, don't rewrite" template), plus a "managed by tasks — edit in repo" + marker in the body. + - *Where prose lives* (open item): leaning **per-item files** (`tasks/.md`) + so `TASKS.md` stays a lean index and a body edit doesn't churn the tree. + +8. **Uniform reorg surface.** Two verbs cover all four levels, since everything + is an issue in a strict tree: + - `mv --under ` — rehome; the **whole subtree travels**, and + because a task's effective milestone/release is *derived from position*, + rehoming a feature auto-re-milestones its tasks. Strict-tree **adjacency is + enforced** — you may only move something under a parent exactly one level + up; a malformed move is rejected, not silently flattened. + - `set --title "..."` — rename, identical at every level. + - `mv --to-default` — kick a **feature** back to `backlog` or a + **milestone** back to `future`. **Rejected for a task** — a task has no + default (no default feature exists); move it under a named feature instead. + - Subtree moves are **allowed but warn** when they carry a `doing` task + (harden to a block later only if needed). + +9. **`Now`/`Backlog`/`Skipped` sections and feature *shadows* dissolve into + views.** The tree says *where* a thing belongs; hybrid state says *what's + active*. So there are no physical `Now`/`Backlog`/`Skipped` regions and **no + shadows** (a v1 workaround for the lack of an explicit tree — and the source + of the whole `#17` shadow-sync bug class). Instead: + - `tasks now` = query: open work in the `current` milestone. + - `tasks backlog` = query: everything in `future` / not in the active + milestone. + - `skipped` = a task-state filter. + The muscle memory survives as commands even though the storage does not. + Requirement from the maintainer: this must be handled **cleanly** (migration + + view-queries tidy, not hacky). + +10. **Sync cadence: allocate-on-`new`, batch-on-`sync`.** Cadence tracks what + each operation *risks*: + - **`new` risks collision** (it allocates) → it **syncs**: mints the issue + immediately when online, so the online path **never produces a provisional + id**. Offline degrades to decision 6. + - **Updates risk only staleness, never collision** (`set`/`mv`/`done`/`link` + mutate an item that already has identity) → **local, instant, batched to + an explicit `sync`**. Offline-identical to online. + - Principle: **sync at the point of allocation, batch at the point of + mutation.** This is strictly better than a fully-explicit cadence *for the + collision goal*, because it makes provisional ids the rare offline + exception instead of the common case. + +11. **Reconciliation is nonce-keyed (the idempotency mechanism).** GitHub gives + no idempotency for free: issue-create has no idempotency key, and search is + eventually consistent (minutes), so "search before mint" has a real window. + Mechanism: stamp a **client-generated nonce** (UUID) into every draft at + creation *and* into the minted issue (hidden body marker or a dedicated + label). `sync` adopts strictly **by nonce** — never by title (titles are + editable and non-unique). Recovery after a create that times out uses the + **list** API (read-your-writes consistent), *not* search, filtered by nonce, + to decide "already minted?" before retrying. This — with the random handles + of decision 6 — is what actually makes the offline path collision-free; it is + settled design, not an open item. + +--- + +## Command surface (sketch) + +``` +# create (allocation → syncs when online, provisional when offline) +tasks new feature "Auth revamp" # → future → backlog +tasks new feature "Auth revamp" --milestone M#42 +tasks new task "Hash tokens" --under F#101 +tasks new milestone "v2.0" --release R#7 +tasks new release "Q4" + +# reorg (local; pushed on next sync) +tasks mv F#101 --under M#88 # rehome a feature (+ its tasks) +tasks mv M#88 --under R#9 # rehome a milestone (+ subtree) +tasks mv F#101 --to-default # feature → backlog (task: rejected) +tasks set M#88 --title "Payments v2" # rename any level + +# state (local; pushed on next sync) +tasks start T#210 | done T#210 | skip | defer +tasks activate M#88 # set the 'current' milestone + +# views (queries over the tree; no storage) +tasks now | backlog | tree | next + +# the network seam +tasks sync # mint drafts, push edits, pull +``` + +## Mapping to GitHub primitives (the GitHub impl) + +This table is the **GitHub implementation** of the `Backend` interface — one +provider's answer. Another impl would map the same core concepts to its own +primitives. + +| v2 concept | GitHub | Notes | +|---|---|---| +| release / milestone / feature / task | **Issue** + `type:*` label | one shared number space; type from label | +| parent/child (the tree) | **Sub-issues** | durable mirror; the file is authoritative in v2.0 | +| task state | issue open/closed + `status:*` label | `doing`/`skipped`/`deferred` as labels | +| priority / effort | labels (`priority:P1`, `effort:4h`) | patch only managed namespaces on push | +| container intentional state | `state:active` / `closed` | rollup progress is computed, not stored | +| authored prose | repo (per-item file) → **issue body** | repo-primary; pushed as a mirror on sync (7c) | +| discussion | **issue comments** | GitHub-only; click-through, not in the clone | +| milestone (optional projection) | **native GitHub Milestone** | flag / v2.1, mirror only | +| release (optional, on ship) | native GitHub Release/tag | link, later | + +--- + +## Open items — recommendations to red-pen + +These are *not* settled; drafted so the doc is complete. (Three earlier open +items were promoted to settled decisions in the 2026-08-04 revision — structure +authority, idempotency mechanism, and prose location.) + +- **Local file layout.** Recommendation: `TASKS.md` is a **lean index** — a tree + grouped release → milestone → feature, each line carrying its `#` id, + type, state, and key labels — with **authored prose in per-item files** + (`tasks/.md`), pushed to issue bodies per decision 7c. A `# Meta` header + binds the file to its repo. Needs a concrete mockup + `schema.md` rewrite. +- **Id display.** With one shared number space, type comes from the label, not a + prefix. Recommendation: display type-hinted (`F#101`, `T#210`, `M#88`, `R#7`) + for scanning; canonical id is the bare issue number. Confirm. +- **Config / repo-binding + auth.** Recommendation: a `# Meta` line + (`repo: owner/name`) binds the file to its repo; token from + `GH_TOKEN`/`GITHUB_TOKEN` env, optionally borrowing `gh auth token` if present + (convenience, **not** a hard `gh` dependency). Still to decide: required token + scope (fine-grained *Issues: RW* vs classic `repo`), private-repo behavior, + and the **tokenless contract** — `validate` and all views/reads MUST work with + no token (the CI story depends on it); only `new`/`sync` may require one. +- **Default-container uniqueness.** `future`/`backlog` are singletons, but GitHub + enforces *id* uniqueness, not *name* — two machines could each mint their own + `future`, forking the tree. Resolution: mint the defaults **exactly once** in + `init`/`migrate-v2` and record their issue numbers in `# Meta` (never lazily + mint); `activate` is push-time "set `state:active` here, clear it elsewhere"; + `validate` flags duplicate defaults / multiple actives. +- **Migration (v1 → v2).** A one-time `tasks migrate-v2`: mint an issue per + `F-`/`T-` item, map the `# Milestones` registry to `type:milestone` issues, + create a `future` release, rewrite `TASKS.md`. **Forward-recoverable, not + reversible** — git reverts the file, but minted issues can't be bulk-deleted + and every mint notifies watchers. So: **nonce-mark** every migrated issue + (decision 11) for adopt-not-remint re-runs; make it **resumable/checkpointed** + (secondary create limits, below); dry-run first; and decide the + done/skipped-item rule (mint-then-close = issue noise vs don't-mint = broken + references — leaning mint-then-close). +- **Pull / conflict semantics (v2.0).** File-authoritative, push-dominant. Pull + is **refuse-on-divergence + report, never merge** (the "detect, don't rewrite" + template); full bidirectional merge → v2.1. Define what counts as divergence + per field (structure, labels, body). +- **Label-set merge.** `update(id, fields)` must **patch only the managed + namespaces** (`type:`/`status:`/`priority:`/`effort:`/`state:`) and never + clobber human-added UI labels — push-dominant makes wholesale replacement a + data-loss bug. +- **Pull-side sanitization.** Sync writes remote titles/labels into the file; an + issue titled `Fix login @deps=T-9 …` is the v1.1.0 "prose parsed as data" + class, now editable by anyone who can retitle an issue. Titles MUST be escaped + on pull. +- **GitHub operational ceilings — verify current numbers (past the Jan-2026 + cutoff).** (a) **Sub-issue fan-out** (~100 children/parent): a long-lived + `backlog` or a big feature will exceed it — decide the policy (shard by + generation? force splits? refuse?) before promising a "strict, total" tree at + scale. (b) **Secondary create limits** (~20/min): `migrate-v2` and bulk `sync` + must throttle + resume. (c) REST sub-issue endpoints key on the issue + **database id**, not its number (extra fetch per link); unauthenticated reads + are 60/hr, so a token is effectively mandatory for anything networked. +- **Foreign / mutated / deleted issues.** Define sync behavior for: an issue with + no `type:*` label (filed in the UI), a stripped label, an issue **transferred** + to another repo (its number changes — identity breaks), an issue deleted in the + UI, and closed-vs-deleted semantics. Relatedly there is **no `rm`/archive verb** + yet — decide whether v2 needs one. +- **Native GitHub issue-types — deliberately NOT used.** GitHub's first-class + issue types are **org-only** (unavailable on a user-owned repo like this one), + which is the concrete reason for `type:*` labels. Recorded so the impl's first + reviewer doesn't "helpfully" switch to native types. +- **Partial-sync failure.** Parents-first minting that dies mid-batch leaves a + half-real tree — decide journal-and-resume vs best-effort-and-report. +- **Consumer story.** v1 is vendored into `taxjot`/`byteorb`/`stead`; v2 is a + breaking, opt-in change. Freeze the 1.x line at its last release (still + `selfupdate`-able *within* 1.x once the cross-major guard lands — see + Sequencing); consumers migrate to 2.x deliberately. + +## Non-goals (v2.0) + +- Bidirectional conflict-merge (→ v2.1). +- Native GitHub Milestone projection (flag / v2.1). +- Native GitHub Release linking on ship (later). +- Blocking (vs warning) subtree moves that carry `doing` work (later if needed). +- Refusing to commit provisional ids (belt-and-suspenders; add only if it bites). + +## Testing posture + +Same discipline as v1: behavior-first e2e scenarios, portable shell, `make +check` green before any commit. New surfaces needing coverage: `new` +online-mint vs offline-provisional; `sync` idempotency (adopt-not-double-mint); +`mv` subtree travel + adjacency enforcement + auto-re-milestone; the +`now`/`backlog` view-queries; `migrate-v2` dry-run + resumability. The suite runs +against a **file-backed fake backend** (an impl of the `Backend` interface, +selected by a test-only `TASKS_BACKEND=fake` env var and persisted to JSON so +state survives the per-assertion CLI invocations the bash harness spawns), so it +stays offline and deterministic with no network mocking. Note the irony and gate +it: this fake is exactly the "local-only backend" the spine calls unsupported for +production — the env gate keeps it test-only. + +## Sequencing + +1. **1.x line first:** finish `#14` (`validate --refs`) + fold in `#25` + (unknown-arg guard), per the existing plan. Ship as the last 1.x feature — + useful to consumers still on v1 during the v2 build. Note in its PR that the + collision triad it belongs to is retired at the v2 line. +2. **`selfupdate` cross-major guard — HARD PREREQUISITE, ships in that same last + 1.x release.** Verified against the code: `DEFAULT_CANONICAL_SOURCE` + (`tasks.py:210`) points at `main`'s raw `tasks.py`, and `selfupdate` + overwrites whenever the fetched `_version_tuple` is greater (`:3167`) — no + major-boundary check, and the payload validator a v2 file still passes. So the + moment v2.0 lands on `main`, every vendored 1.x copy that runs `selfupdate` + (against the *trusted default*, no flag) silently crosses into a different + file format + a network dependency. The last 1.x release **must** either + refuse cross-major updates in `selfupdate` or repoint canonical to a frozen + `v1` branch — **before** v2 touches `main`. +3. **Freeze 1.x**, then build v2 against this doc, test-first, decisions above + pinned. Red-pen the open items before implementation of each touches them.