# Session handoff — 2026-06-22 (79)

A long session continuing the open-issue sweep (handoff-77/78). Cleared the
**help** issue (#36) and the **schema-cache** bug (#39) earlier; this note
mainly records the big piece: **dropping the `blob` column type** (ADR-0005
Amendment 2), which grew out of a one-line copy-rule fix.

## §1. State

**Branch `main`.** Commits this session (`1a2002d` ADR-0005 Am2 + `6b4c4dc`
blob removal, plus the earlier `07575da` #39 · `e88fa79` handoff-77 · `3ad4aff`
#36 · `64818c0` handoff-78) — **the user pushed these.** A follow-up
**`style:` rustfmt commit + this CLAUDE.md/handoff doc commit** land on top
(see §5) and are the only unpushed work at handoff time.

**Suite green: 2521 passed / 0 failed / 1 ignored** (1810 lib + 8 e2e_pty + 503
it + 200 typing), `fmt --check` + `clippy -D warnings` clean — **verified via
`nix develop -c`** (the CI-exact way; see §5's lesson).

**Closed this session:** #36, #39. **Open:** #37 (clause hints), #38
(diagnostic route — needs a do/defer/close call), #40 (winget release notes).

## §2. What shipped — drop `blob` (ADR-0005 Amendment 2)

**Why** (recorded in the ADR): `blob` was a dead-end type — declarable but
never fillable (no literal in either mode, `seed`-unsupported), so a blob
column could only ever hold NULL. It rode along as the engine's fourth
storage class and never got the pedagogical scrutiny UUIDs (excluded) and
compound PKs (defended) did. The vocabulary is now **nine types**.

**Discovered via:** the `value.rs:106` binder message *"…not supported in DSL
yet"* — a user-facing copy-rule violation (it `passthrough`es to the user via
`DbError::InvalidValue`). Rather than reword, we removed the type; the message
is deleted outright.

**Implementation (test-first throughout):**
- **Removal** (compiler-guided): `Type::Blob` + its `keyword`/`sqlite`/`all()`/
  `from_sql_name` alias arms; `Value::Blob`; `CellValue::Blob(Vec<u8>)` + the
  base64 CSV encode/decode; the `BLOB_SLOT` grammar slot; completion's `blob`
  candidate; `output_render`'s `BLOB`; the type-change `↔ blob` static refusal;
  the seed blob arms; the binder refusal. User-facing strings: dropped `blob`
  from `help.types_reference` and removed the `hint.value_slot_blob` slot hint.
  `base64` **stays** (clipboard OSC-52).
- **Backward-compat migration** (the ADR-0015 framework's **first real use**):
  a **v1→v2 format bump**. `parse_schema` hard-pinned the version to 1, so this
  rippled into a `CURRENT_SCHEMA_VERSION = 2` constant used by the serializer +
  parser + skeleton, the first registered migrator (`migrate_v1_to_v2`:
  rewrites `type: blob`→`type: text` + bumps the version), and ~30 `version: 1`
  test fixtures. The runtime (already wired to `ensure_project_yaml_migrated`)
  **forces a `.db` rebuild** from the migrated text **only when a blob column
  was actually converted** (`body_declares_blob_column` pre-read at both open
  sites) — the stale `.db` keeps a `STRICT … BLOB` engine column + `"blob"`
  metadata it would otherwise use as-is. Conversion target **text** (not drop)
  was the user's call (non-destructive, CSV-free, PK/FK-safe).
- **Tests:** migrator unit tests (incl. a column *named* `blob` + a `blob`
  substring in a CHECK left untouched); a **full-stack** integration test
  `tests/it/blob_removal_migration.rs` (v1 blob project → migrate → `text`
  column + row data preserved + `.bak`); a **Tier-4 PTY** end-to-end test
  `e2e_pty::opens_a_legacy_v1_blob_project_by_migrating_to_text` (the real
  binary opens a seeded legacy v1-blob project via `--resume` → migrate +
  rebuild + `text` column) — added in the implementation `/runda` pass to
  cover the runtime's migrate-on-open glue; updated all-types tests to nine;
  regenerated 5 typing-surface snapshots (reviewed — only the `blob`/`data`
  candidate vanished).
- **Docs:** ADR-0005 Amendment 2 (committed `1a2002d`) + README index; swept
  `ten-type`→`nine-type` / removed `blob` from CLAUDE.md, `requirements.md`,
  the website (`reference/types.md`, the seed doc's stale `not null blob`
  bullet, the `rdbms.mjs` highlight grammar), and the cross-reference counts in
  ADR-0030/0033/0035. CHANGELOG `[Unreleased] → Removed`.

## §3. Decisions & notes for next time

- **convert-to-text, not drop-the-column** (user, 2026-06-22): drop would force
  rewriting every `data/*.csv` past the rebuild's strict header check — not
  worth it for a few-days-old, few-users tool.
- **Full v2 framework migration, not parse-time leniency** (user): the proper
  recorded migration with `.bak`, exercising the dormant framework.
- **`BlobLit` grammar terminal deliberately left** — a pre-existing,
  `#[allow(dead_code)]` speculative token never used by any grammar; orthogonal
  to the type vocabulary. Its "blob literal" string is unreachable. Removing it
  is a separate optional cleanup (touches the grammar core).
- **Known assumption:** the migrator + `body_declares_blob_column` match the
  serializer's *inline* column format (`- { name: …, type: … }`). A
  hand-written *block-style* `project.yaml` with `type: blob` wouldn't convert
  (and would then fail to parse). `project.yaml` is always machine-serialized
  inline, so this is fine in practice — noted in case a future format change
  moves away from inline columns.
- **Other ADRs (0030/0035) keep their historical `blob` content** (type-change
  matrix, `binary`/`varbinary` aliases) per the project's supersede-don't-rewrite
  ADR philosophy; ADR-0005 Amendment 2 is the authoritative record.

## §4. Process pins / next

- Commits user-confirmed, no AI attribution, append-only, on `main`; **push is
  the user's step.**
- **`/runda` was run on both the ADR (design) and the implementation.** The
  implementation pass found one real gap — the runtime's migrate-on-open glue
  was untested — closed with the new Tier-4 PTY test. It also verified the
  import path migrates (extract→migrate→rebuild) and the non-migration
  `parse_schema` callers fail-safe.
- Consider a `cargo sweep` at this milestone.
- **Next open issues:** #37 (clause hints, on-mission), #38 (escalate
  do/defer/close first), #40 (winget release notes).

## §5. CI/fmt lesson (cost a red CI run — don't repeat)

The blob commit `6b4c4dc` **failed the CI `fmt` gate** (run 92, `ci.yaml`).
Two compounding causes, both fixed here:

1. **`fmt` IS gated now** (`cargo fmt --check`, ADR-ci-002 Amendment 1 / issue
   #35) — but `CLAUDE.md` still said "fmt is intentionally not gated yet." That
   stale note bred complacency. **Corrected `CLAUDE.md`** (the gate is fmt +
   clippy + test, run via `nix develop -c`).
2. **A broken local check masked the failure:** `cargo fmt --check 2>&1 | tail
   -1 && echo "fmt clean"` tests `tail`'s exit code (always 0), **not** fmt's —
   so "fmt clean" printed over real diffs. **Always read the exit code**, never
   a piped `… | tail && echo clean`.

**Going forward, verify the CI way:** `nix develop -c cargo fmt --check` /
`nix develop -c cargo clippy --all-targets -- -D warnings` / `nix develop -c
cargo test` — matching the pinned 1.95.0 toolchain *and* the devShell env, and
checking exit codes. (A bare host `cargo` happened to be 1.95.0 this time, so
the divergence was the masking pipe, not the toolchain — but `nix develop -c`
guards both.) The `style:` rustfmt fix + this doc update land on top of
`6b4c4dc`; CI re-runs green on the next push.
