scaffold decisions/ with backlog from planning sessions

Adopt settled-reach's Q&D record convention. Confirmed decisions
live under decisions/<domain>.md as D-NNN; open questions under
questions-<domain>.md as Q-NNN; rejected alternatives in rejected.md
as R-NNN. Markdown is source of truth; .pql/pql.db (added later) is a
query index.

Backlog captured from the Tier-0 Flutter planning sessions: bare
WidgetsApp, theme pipeline, kernel admission rule, feature-first
layout, a11y + i18n as Tier-0 contracts, test pyramid, kanban over
Scrum, pql-owns-planning, Python stopgap sunset clause.

ADR migration (D-001, D-003-D-006 confirmed, R-002 rejected) is
staged for the next commit so the diff stays readable.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-04-21 17:10:42 +02:00
co-authored by Claude
parent 77e395b3f2
commit 4d515ce51e
16 changed files with 653 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
# Decisions
Confirmed decisions, open questions, and rejected alternatives for clide.
Decisions are split by domain. When unsure where a record belongs: if
it constrains **how we build**, it's architecture. If it defines **what
ships to users**, it's extensions / accessibility. If it defines **how
we verify**, it's testing. If it defines **what the toolchain looks
like**, it's tooling. If it defines **how the team works**, it's
process.
Cross-domain records live in one file with `[D-NNN]`-shaped cross-
references in related files. Split threshold: when any file exceeds
~350 lines, review whether it should split (see settled-reach's
`questions-*.md` split pattern for precedent).
## Domain files
| File | Domain |
|------|--------|
| [architecture.md](architecture.md) | Core, rendering, IPC, kernel, panel manager |
| [extensions.md](extensions.md) | Extension contract, Lua runtime, grain, contribution points |
| [accessibility.md](accessibility.md) | A11y + i18n policy, WCAG gates |
| [testing.md](testing.md) | Test pyramid, drivers, client-side constraint |
| [tooling.md](tooling.md) | Toolchain, supply chain, CI, ignore strategy |
| [process.md](process.md) | Q&D system, kanban, commit conventions, changelog |
| [rejected.md](rejected.md) | Rejected alternatives across all domains |
| [questions.md](questions.md) | Master index of open questions |
| [questions-architecture.md](questions-architecture.md) | Architecture Qs |
| [questions-extensions.md](questions-extensions.md) | Extension Qs |
| [questions-accessibility.md](questions-accessibility.md) | A11y / i18n Qs |
| [questions-testing.md](questions-testing.md) | Testing Qs |
| [questions-process.md](questions-process.md) | Process + tooling Qs |
## Record shape
Confirmed decisions (`D-NNN`):
```markdown
### D-NNN: Short title
- **Date:** YYYY-MM-DD
- **Decision:** one-sentence summary, then details.
- **Rationale:** why this over alternatives.
- **Cost:** known downsides / what we're accepting.
- **Raised by:** who proposed / endorsed.
```
Domain-specific fields (`Kill switch:`, `Evaluation reports:`,
`Amendment:`, `Cross-reference:`) are additive. Amendments are inline
and dated: `**Amendment (YYYY-MM-DD):** …`. Cross-references use
markdown anchor links with the full slug:
`[D-005](architecture.md#d-005-dart-core-ptyc-peer)`.
Open questions (`Q-NNN`):
```markdown
### Q-NNN: Short question-form title
- **Status:** Open | Partially resolved → [D-NNN] | Resolved → [D-NNN]
- **Question:** ...
- **Context:** ...
- **Assigned to:** (optional)
- **Source:** (optional)
```
Rejected alternatives (`R-NNN`):
```markdown
### R-NNN: Short rejected-option title
- **Rejected:** YYYY-MM-DD
- **Reason:** ...
- **Cross-reference:** [D-NNN] (what was picked instead)
```
## Claiming an ID
Until the pql planning subcommands land ([`Q-021`](questions-process.md)),
claim IDs by inspecting the highest existing `D-NNN` / `Q-NNN` /
`R-NNN` in the target file and incrementing.
Once `pql decisions claim D <domain> "title"` exists, use that —
same semantics, no race on concurrent sessions.
## Querying
Stopgap today: `tools/scripts/plan decisions …` reads and writes
`.pql/pql.db` (gitignored; markdown is source of truth). Replaced
by `pql decisions …` when pql ships parity — see
[D-040](process.md#d-040-python-stopgap-under-toolsscriptsplan) and
[R-011](rejected.md#r-011-permanent-stopgap).
Common queries:
```bash
tools/scripts/plan decisions list --type confirmed --domain architecture
tools/scripts/plan decisions show D-005 --with-refs
tools/scripts/plan decisions coverage # D-records without tickets
tools/scripts/plan decisions validate # pre-push parser gate
```
## Adding a decision
1. Edit the appropriate domain file.
2. Follow the record shape above.
3. Run `tools/scripts/plan decisions validate` (also runs in
`make push-check`).
4. Commit. The SQLite index rebuilds from markdown on any
`tools/scripts/plan decisions sync`.
+28
View File
@@ -0,0 +1,28 @@
# Accessibility + i18n Decisions
A11y + i18n are Tier-0 contracts, not Tier-6 polish.
---
### D-020: A11y is a Tier-0 contract
- **Date:** 2026-04-21
- **Decision:** Every widget primitive wraps its interaction surface in a `Semantics` node at the point of creation. A11y coverage is a test-time gate (`ci/test_a11y.sh`), not a post-hoc polish pass. `ensureSemantics()` fires at app boot; Flutter's semantics tree is always populated.
- **Rationale:** Retrofitting a11y onto a grown UI is what every project that skips this promises to do later and then doesn't. Making it a Tier-0 contract costs one `Semantics` line per primitive and a semantic-coverage test; postponing costs a rewrite.
- **Cost:** Widget authors maintain correct labels; tests reject new primitives without semantics. Enforced by `app/test/a11y/` coverage tests.
- **Raised by:** 2026-04-21 planning.
### D-021: i18n is a Tier-0 contract (fframe pattern + locale-fallback chain)
- **Date:** 2026-04-21
- **Decision:** All user-facing strings resolve through a namespaced i18n catalogue loader ported from fframe's text-driven pattern, extended with a locale-fallback chain fframe lacks. JSON per locale; `I18n.of(context).t('namespace.key', {vars})`. Missing keys resolve down the chain (e.g. `en_GB``en` → default), never fail silently; missing at the base locale logs a dev-mode error.
- **Rationale:** Flutter's `intl` + ARB codegen is inflexible for plugin-contributed catalogs (see [R-004](rejected.md#r-004-flutter-intl-and-arb-codegen)) — we need per-extension catalogs that merge without a codegen step. fframe's shape fits; its silent-fallback behaviour does not, so we add the chain.
- **Cost:** JSON has no comments and no trailing commas; translation tooling has to accept that. Separate `i18n` facade on every feature.
- **Raised by:** 2026-04-21 planning.
### D-022: WCAG-AA contrast gate on bundled themes
- **Date:** 2026-04-21
- **Decision:** Every bundled theme must pass a WCAG-AA contrast check on its canonical token pairs (text/background, link/background, focus-ring/background) at test time. `ci/test_a11y.sh` runs the gate; CI fails on regressions.
- **Rationale:** Themes drift under "looks nicer" tweaks; contrast regressions land silently. Running the gate on every PR is the cheapest insurance. Ran the gate on initial themes — caught one summer-night muted token at 2.81:1 (below AA), fixed before landing.
- **Cost:** Third-party themes (Tier 6) won't be gated until an extension-time test hook lands. Bundled themes are gated today.
- **Raised by:** 2026-04-21 planning.
---
+67
View File
@@ -0,0 +1,67 @@
# Architecture Decisions
Core, rendering, IPC, kernel, panel manager.
---
### D-007: App root is bare `WidgetsApp`
- **Date:** 2026-04-21
- **Decision:** The Flutter app root is `WidgetsApp`, not `MaterialApp` or `CupertinoApp`. Clide's look is fully custom; the Material/Cupertino shells would drag in opinionated theming, default icons, and platform chrome we'd then have to fight.
- **Rationale:** Clide is a Linux-primary desktop IDE with a custom theme pipeline and custom primitives (panels, tabs, panes, canvas). Material's implicit theming collides with [D-009](#d-009-three-tier-theme-pipeline); Cupertino is iOS-flavoured. `WidgetsApp` gives us routing, locale, focus traversal, semantics, and Directionality without aesthetic baggage.
- **Cost:** We build and own every primitive; no `ElevatedButton` fallback. See [R-003](rejected.md#r-003-materialapp-root) and [R-007](rejected.md#r-007-cupertinoapp-root).
- **Raised by:** 2026-04-21 planning.
### D-008: Feature-first folder layout
- **Date:** 2026-04-21
- **Decision:** Under `app/lib/`, organise by feature (`kernel/`, `extension/`, `widgets/`, `builtin/<name>/`) rather than by layer (`models/`, `views/`, `controllers/`). Private implementation lives under each feature's `src/`; the feature's public surface is a barrel file at the feature root (e.g. `app/lib/kernel/kernel.dart`).
- **Rationale:** Features grow and get deleted as units; layer-first layouts fragment a feature across three directories and make deletions risky. Matches extensions-as-features (every extension already has its own folder).
- **Cost:** Imports cross features only via the barrel — enforce by review, no automated check yet.
- **Raised by:** 2026-04-21 planning.
### D-009: Three-tier theme pipeline
- **Date:** 2026-04-21
- **Decision:** Themes resolve through three layers: (1) palette — raw named colours per theme YAML; (2) semantic — roles like `surface.background`, `text.primary`, `accent.focus`; (3) surface — component-scoped tokens derived from semantic roles (button bg/fg/border hover/pressed/disabled states).
- **Rationale:** Direct palette-to-component binding collapses under multi-theme work; VS Code's 600-token surface map is the proof. The semantic layer is where a11y contrast gates apply; the surface layer is where components bind.
- **Cost:** Three layers to keep coherent per theme. Contrast gate ([D-022](accessibility.md#d-022-wcag-aa-contrast-gate-on-bundled-themes)) enforces the semantic layer on every bundled theme.
- **Raised by:** 2026-04-21 planning.
### D-010: State management — `ChangeNotifier` + `ListenableBuilder`
- **Date:** 2026-04-21
- **Decision:** Per-feature state uses `ChangeNotifier` exposed through a feature facade (singleton-per-kernel); widgets subscribe via `ListenableBuilder`. No Riverpod, Provider, BLoC, or Redux.
- **Rationale:** SDK-shipped, zero deps, trivial to fake in tests (hand-rolled fakes in [D-025](testing.md#d-025-mocks-mocktail-at-io-plus-hand-rolled-fakes)). Violates [D-031 prefer-zero-deps](tooling.md#d-031-prefer-zero-deps-exact-pin) otherwise. See [R-008](rejected.md#r-008-riverpod-provider-bloc-for-state).
- **Cost:** No codegen ergonomics; manual `notifyListeners()` discipline. The `ListenableBuilder.listenable` contract rejects rebuilds outside the subscribed notifier — intentional.
- **Raised by:** 2026-04-21 planning.
### D-011: Panel manager is kernel; layout is data; three-column is a preset
- **Date:** 2026-04-21
- **Decision:** The kernel owns a panel manager that treats layout as declarative data (tree of splits + leaves). The default "three-column IDE" (sidebar / editor / assistant) is one preset; alternative presets (writer-focus single-column, debugger four-pane) ship as data, not code forks.
- **Rationale:** Hard-coded three-column layouts paint us into corners when future tiers add canvas, graph, terminal-grid. Data-driven layout also lets extensions contribute presets without patching the panel manager.
- **Cost:** More kernel surface up-front; pays back at Tier 5 (canvas) and Tier 6 (extension-contributed layouts).
- **Raised by:** 2026-04-21 planning.
### D-012: Kernel admission rule — mandatory shared singletons only
- **Date:** 2026-04-21
- **Decision:** A service joins the kernel only if it is (a) mandatory for app boot and (b) a shared singleton across features. Everything else is an extension or a feature-local service.
- **Rationale:** Keeps the kernel auditable. Previous drafts piled "useful globals" into the kernel; result was a 40-service god-object. The admission rule forced 18 services out of 31 candidates.
- **Cost:** Some legitimate cross-cutting concerns (telemetry, crash reporter when they land) must pass the test; we expect a few more admissions as Tiers 3-6 land.
- **Raised by:** 2026-04-21 planning.
### D-013: Git hardcoded in kernel project-loader
- **Date:** 2026-04-21
- **Decision:** The kernel's project loader treats "repo root" as a `git` concept — runs `git rev-parse --show-toplevel` to find workspace root, subscribes to filesystem events, and shells out to `git` for status/diff/stage. No VCS abstraction layer.
- **Rationale:** Option B (VCS abstraction) is premature generalisation — we have one VCS today, Mercurial/Fossil/Sapling users are a rounding error on the Linux desktop IDE market, and the abstraction adds a seam that has to be tested against nothing. When a second VCS shows up we refactor.
- **Cost:** Adding Mercurial support later costs a real refactor, not just a plugin. Acceptable.
- **Raised by:** 2026-04-21 planning.
### D-014: Two-tier disable — kernel locked, everything else extension-shaped
- **Date:** 2026-04-21
- **Decision:** Kernel services cannot be disabled at runtime. Extensions (including every bundled built-in) can be toggled via the extension manager. This creates exactly two disable tiers: kernel (always on) and extension (toggleable).
- **Rationale:** A three-tier system (kernel / bundled-cannot-disable / user-can-disable) is dishonest — if a "bundled built-in" can't be disabled, it's kernel and belongs in kernel admission review. Forcing every bundled feature to pass the extension contract is also the best test we have that the contract is actually usable.
- **Cost:** Disabling `builtin.default_layout` by mistake produces an empty window. Mitigated by the kernel's first-boot defaults and a "reset extensions" action.
- **Raised by:** 2026-04-21 planning.
---
*Architectural backlog from the claudian lineage lives below; ADR
migrations (D-001, D-003, D-004, D-005, D-006) follow when commit #2
runs.*
+46
View File
@@ -0,0 +1,46 @@
# Extension Decisions
Extension contract, Lua runtime, grain, contribution points.
---
### D-015: Extension grain — container-level, multi-contribution
- **Date:** 2026-04-21
- **Decision:** An extension is a shipping unit that contributes one or more named contributions (panel, command, theme, keybinding, language, layout, provider, view). Grain is container-sized — a single `builtin.git` extension contributes panel + commands + keybindings + status-bar items; we do not ship one extension per contribution.
- **Rationale:** Finer grain (one extension per contribution) multiplies manifest files with no win and fragments ownership. Coarser grain (one mega-extension per domain) hides which parts a user might reasonably disable.
- **Cost:** Extension authors make a taste call about grouping; disagreements go to review.
- **Raised by:** 2026-04-21 planning.
### D-016: Built-ins in Dart, third-party in sandboxed Lua
- **Date:** 2026-04-21
- **Decision:** Bundled extensions (every `app/lib/builtin/<name>`) are Dart — they link into the app binary. Third-party extensions (Tier 6) run in sandboxed Lua via the `ptyc`-peer Lua runtime (see [D-019](#d-019-lua-runtime-as-ptyc-peer-supporter-tool)). The contribution contract is language-agnostic — same contribution shapes, same manifest schema.
- **Rationale:** Dart built-ins get full SDK power (custom painters, isolates, FFI); third-party Lua gets a narrow capability API, no arbitrary syscalls, no deps on pub.dev. VS Code's Node-runs-with-full-power model is a supply-chain nightmare we're explicitly rejecting.
- **Cost:** Two implementation paths for the same contract; we pay in API design to keep them equivalent at the seams.
- **Raised by:** 2026-04-21 planning.
### D-017: Panels are extension-shaped from day one
- **Date:** 2026-04-21
- **Decision:** Every bundled panel (file tree, git, problems, pql query, terminal, etc.) is a contribution on the extension contract, not a hardcoded widget tree in the panel manager. First party contributes via Dart; third party contributes via Lua; same contract.
- **Rationale:** Forces the contract to be real on day one. The alternative ("extensions can contribute panels *later*") always becomes "the contract doesn't quite fit our bundled panels, so built-ins get a shortcut" — and the shortcut becomes permanent.
- **Cost:** Every panel goes through the manifest/registration path even when it's trivial. Price paid once.
- **Raised by:** 2026-04-21 planning.
### D-018: YAML for themes + manifests; JSON for i18n catalogs
- **Date:** 2026-04-21
- **Decision:** Themes and extension manifests are YAML; i18n catalogues are JSON (fframe parity — see [D-021](accessibility.md#d-021-i18n-is-a-tier-0-contract)).
- **Rationale:** YAML for human-edited config files (themes, manifests) — comments, multi-line strings, less noise. JSON for machine-written / machine-read files (i18n catalogs get generated by translation tooling eventually). Mixing is fine; each format is where it's best.
- **Cost:** Two parsers in the tree. `yaml: 3.1.3` is exact-pinned.
- **Raised by:** 2026-04-21 planning.
### D-019: Lua runtime as `ptyc`-peer supporter tool
- **Date:** 2026-04-21
- **Decision:** Third-party extensions run Lua inside a sandboxed runtime (working name TBD; same peer-status as `ptyc` and `pql`). The runtime vendors liblua, links from Dart via `dart:ffi`, exposes a narrow capability API, and takes contributions as a declarative render-intent DSL (widget shapes, not arbitrary widget code).
- **Rationale:** Lua is small, embeddable, battle-tested (Neovim, World of Warcraft, Redis). Native sandboxing at the VM level is cheap. A declarative render-intent DSL keeps "run arbitrary Flutter widgets from Lua" off the table — the runtime interprets the DSL into Dart widgets, which keeps Tier 6 supply-chain risk bounded.
- **Cost:** Runtime is a separate supporter tool to build; ffi is tricky. Deferred to Tier 6; only the slot is reserved now.
- **Raised by:** 2026-04-21 planning.
---
*See also the existing `builtin.grammars_core` stub for tree-sitter
questions ([Q-015](questions-process.md#q-015-editor-tab-full-lsp-vs-tree-sitter-only),
[Q-016](questions-process.md#q-016-tree-sitter-dart-grammar-maintenance)).*
+57
View File
@@ -0,0 +1,57 @@
# Process Decisions
Q&D record system itself, kanban, commit conventions, changelog.
---
### D-034: Q&D record system
- **Date:** 2026-04-21
- **Decision:** Adopt settled-reach's Q&D record convention. Confirmed decisions are `D-NNN` under `decisions/<domain>.md`; open questions are `Q-NNN` under `decisions/questions-<domain>.md`; rejected alternatives are `R-NNN` under `decisions/rejected.md`. Markdown is the source of truth; `.pql/pql.db` is a query index built from markdown. Record shape and claiming rules live in [`decisions/README.md`](README.md).
- **Rationale:** Two places currently hold clide's architectural knowledge — ADRs and scattered plan files — and neither lets an agent or reviewer locate "the unresolved thing in this subsystem." Q&D fixes that: one index, one shape, one claim rule. Proven in daily use in settled-reach.
- **Cost:** One more directory to maintain. A learning curve for contributors (tiny: read `decisions/README.md`).
- **Raised by:** 2026-04-21 planning.
### D-035: Kanban / waterfall, not Scrum
- **Date:** 2026-04-21
- **Decision:** Ticketing is kanban + waterfall. Tickets flow backlog → ready → in_progress → review → done → cancelled. No sprints, no velocity, no story points. Settled-reach's Scrum layer (sprints, sprint reviews, sprint close as a sync event) is stripped.
- **Rationale:** Clide has a solo-or-small-team cadence. Sprint ceremonies add overhead without adding signal at this scale. Kanban matches how the work actually happens.
- **Cost:** No natural "sprint close" event to sync shared state. See [Q-022](questions-process.md#q-022-ticket-persistence-strategy).
- **Raised by:** 2026-04-21 planning.
### D-036: `.claude/` is committed project surface, managed through the IDE
- **Date:** 2026-04-21
- **Decision:** `.claude/` (hooks, skills, agents, MCP settings) is committed alongside code. Only `.claude/settings.local.json` is gitignored. The reserved `builtin.claude-control` extension surfaces `.claude/` as a first-class sidebar tab (sub-tabs: Settings / Skills / Agents / Hooks / MCP) in a future tier.
- **Rationale:** `.claude/` is project governance — same status as `CLAUDE.md`, `decisions/`, `Makefile`. Treating it as dotfile-cruft loses project-wide conventions (skills, hooks) that should travel with the repo.
- **Cost:** Contributors commit Claude Code config alongside code changes. Discipline required; minor.
- **Raised by:** 2026-04-21 planning. Distinct from the existing `builtin.claude` stub reserved for Tier 1's "run Claude Code in a PTY pane."
### D-037: Commit conventions per git-commit skill
- **Date:** 2026-04-21
- **Decision:** Commits follow `.claude/skills/git-commit/SKILL.md`: imperative subject ≤ 70 chars, no `feat:`/`fix:` type prefixes, no emojis, optional body wrapped at ~72 chars, multi-line messages via HEREDOC, attribution trailer `Co-Authored-By: Claude <noreply@anthropic.com>` (the model-identifier variant the harness produces is also accepted).
- **Rationale:** Python-era clide under `legacy/` used Conventional Commits; the Flutter rebuild does not. Imperative mood reads better for a project-governance log; types are noise when every commit is scoped to a subsystem already.
- **Cost:** Contributors with Conventional Commits muscle memory adjust.
- **Raised by:** 2026-04-21 planning.
### D-038: Changelog discipline — Keep a Changelog 1.1.0
- **Date:** 2026-04-21
- **Decision:** `CHANGELOG.md` follows Keep a Changelog 1.1.0. Every user-visible commit adds an entry under `## [Unreleased]` in the appropriate subsection (Added / Changed / Deprecated / Removed / Fixed / Security). Cutting a release moves entries under a dated heading and bumps `project.yaml` `version:` in the same commit. Pure bookkeeping commits (comment-only, .gitignore tweak, lint config) skip the changelog.
- **Rationale:** Release notes that have to be written after the fact aren't written. Writing them per commit keeps the log honest.
- **Cost:** One extra edit per user-visible commit; zero if the change is invisible.
- **Raised by:** 2026-04-21 planning.
### D-039: Planning tooling lives in pql, not clide
- **Date:** 2026-04-21
- **Decision:** Planning subcommands (`decisions`, `ticket`, `plan`) land in pql's repo long-term. Clide consumes them via shell-out, matching [D-003](architecture.md)'s wrap-don't-duplicate rule for pql. Clide does not grow Dart subcommands for planning.
- **Rationale:** A terminal user or a user in VS Code / JetBrains still needs Q&D access. Binding planning tooling to clide-the-Flutter-app would cut them off from their own work — see [R-009](rejected.md#r-009-port-planning-tooling-into-clide). pql is already the CLI, already universal, already wrapped by clide.
- **Cost:** Planning features don't ship until pql catches up. Mitigated by [D-040](#d-040-python-stopgap-under-toolsscriptsplan). Gated by [Q-021](questions-process.md#q-021-pql-absorbs-planning-vs-keeps-separate).
- **Raised by:** 2026-04-21 planning.
### D-040: Python stopgap under `tools/scripts/plan`
- **Date:** 2026-04-21
- **Decision:** A time-limited Python port of settled-reach's `decisions_sync.py` + `ticket` + `decision` scripts lives at `tools/scripts/plan` with support modules under `tools/scripts/planning/`. Writes to `.pql/pql.db` (gitignored). Ticket IDs are `T-NNN` (TEXT PK, reshape from settled-reach's integers). Same schema, same markdown, same verb shape as the eventual `pql` subcommands.
- **Sunset:** Delete the stopgap when pql ships `pql decisions sync | validate | list | show | claim | coverage` + `pql ticket new | list | show | status | assign | block | board` with feature parity, and reads the same `.pql/pql.db` file the stopgap wrote. Removal commit shape: [R-011](rejected.md#r-011-permanent-stopgap).
- **Rationale:** Planning tooling must work day one. Pql's Go implementation won't land for at least a cycle or two. Without a stopgap, the convention lives on paper; with one, tickets + decisions are queryable from today. Same schema means migration is call-site find-replace (`tools/scripts/plan ``pql `), no data migration.
- **Cost:** Python dep on contributors' machines (already present on most Linux dists). One time-limited tool to maintain. See [R-010](rejected.md#r-010-python-script-stopgap-at-toolingdb) for why `tools/scripts/plan` and not `tooling/db/`.
- **Raised by:** 2026-04-21 planning.
---
+17
View File
@@ -0,0 +1,17 @@
# Open Questions — Accessibility + i18n
---
### Q-013: Web production-mode a11y
- **Status:** Open
- **Question:** In a Flutter web release build, is the semantics tree always on (what we need for Playwright and for end-user screen readers) or gated behind an accessibility toggle (Flutter's default)?
- **Context:** Today the driver clicks `flt-semantics-placeholder` to activate. For user-facing builds we need semantics-always-on.
- **Source:** 2026-04-21 planning.
### Q-014: i18n plurals / gender / date-format tooling
- **Status:** Open
- **Question:** fframe's pattern covers straight key→string lookup with variable interpolation. Plurals, gendered forms, and ICU-style date formatting aren't in scope there. Do we add them to the i18n facade, defer to a runtime library (violates [D-031](tooling.md#d-031-prefer-zero-deps-exact-pin)), or require catalogues to provide pre-formatted strings per count/gender?
- **Context:** Probably becomes painful at Tier 3 (git panel, problem counts) and Tier 4 (pql results).
- **Source:** 2026-04-21 planning.
---
+62
View File
@@ -0,0 +1,62 @@
# Open Questions — Architecture
IPC, events, canvas, window chrome, macOS signing, pql absorption,
ticket persistence.
---
### Q-001: Authorisation granularity on the IPC socket
- **Status:** Open
- **Question:** The daemon's token auth is coarse (allow all / deny all). Do we need per-subsystem grants later (e.g. restrict `git push`), and if so, what's the model — capability tokens? An explicit grant table per client? Time-limited grants?
- **Context:** Surfaced in the old ADR 0006 open-questions footer; deferred until Tier 1 is in real use.
- **Source:** ADR 0006 (migrated to [D-006](architecture.md)).
### Q-002: Back-pressure on event streams
- **Status:** Open
- **Question:** A subscriber that falls behind on `pane.output` (a firehose) needs a policy: drop oldest, block producer, coalesce, or kill subscriber. Which?
- **Context:** The event bus is in-memory; back-pressure policy is undefined. Defer until Tier 1 is in real use and we have a real firehose to measure against.
- **Source:** ADR 0006 (migrated to [D-006](architecture.md)).
### Q-003: Event persistence + audit/undo
- **Status:** Open
- **Question:** Events are in-memory only in v1. If a future need (audit log, undo history) wants persistence, is it a property of the bus or a subsystem that subscribes and writes?
- **Context:** ADR 0006 leaned "subsystem that subscribes and writes" but didn't commit.
- **Source:** ADR 0006 (migrated to [D-006](architecture.md)).
### Q-004: `.canvas` schema compatibility with Obsidian
- **Status:** Open
- **Question:** Clide's canvas (Tier 5) should read/write something — either Obsidian's `.canvas` JSON schema verbatim, a compatible-ish superset, or our own format. Each has trade-offs.
- **Context:** Obsidian's canvas users might want their canvases portable; conversely, bending to Obsidian's schema constrains our canvas features.
- **Source:** CLAUDE.md "Open questions" footer.
### Q-005: IPC wire-format stability + `schema_version:`
- **Status:** Open
- **Question:** When do we freeze the IPC envelope / schema and introduce `schema_version:` in `project.yaml`? What's the bump policy for breaking changes?
- **Context:** Covered partially by [D-006](architecture.md)'s `v: 1` starting point; CLAUDE.md flags this as "decide when the first real subcommand lands."
- **Source:** CLAUDE.md "Open questions" footer.
### Q-006: Window chrome — native frame vs frameless custom
- **Status:** Open
- **Question:** Does clide ship with the OS-native window frame (title bar, min/max/close from the WM) or a frameless custom chrome that gives us pixel control at the cost of reimplementing window controls per-platform?
- **Context:** Surfaced during Tier-0 plumbing discussion; decision deferred.
- **Source:** 2026-04-21 planning.
### Q-007: macOS app bundle signing / notarisation
- **Status:** Open
- **Question:** Distributing a signed macOS `.app` requires a Developer ID and a notarisation pipeline. Do we gate macOS builds on this (Tier 6), or ship unsigned with a known "right-click, open" user workflow for early testers?
- **Context:** Linux is primary; macOS is a stretch target. Notarisation is a separate cost from the Flutter build.
- **Source:** 2026-04-21 planning.
### Q-021: Pql absorbs planning vs keeps separate
- **Status:** Open
- **Question:** Three shapes for planning tooling's long-term home: (A) Pql absorbs planning — `pql decisions …` + `pql ticket …` subcommands; clide shells out. (B) Clide absorbs pql — reverse [D-003](architecture.md), one big Dart tool. (C) Separate new binary just for planning.
- **Context:** User is leaning (A). This plan assumes (A) without committing. If (A) doesn't land, [D-040](process.md#d-040-python-stopgap-under-toolsscriptsplan)'s sunset condition changes. Gates all tooling work. Integration constraints that shape this question are captured in [D-039](process.md#d-039-planning-tooling-lives-in-pql) / [R-009](rejected.md#r-009-port-planning-tooling-into-clide).
- **Source:** 2026-04-21 planning.
### Q-022: Ticket persistence strategy
- **Status:** Open
- **Question:** Once [Q-021](#q-021-pql-absorbs-planning-vs-keeps-separate) resolves in favour of (A), how do tickets handle shared team state? (1) Never commit (per-dev, ephemeral — works for solo). (2) Commit on milestone (settled-reach's sprint-close pattern — kanban has no natural equivalent, `release` or `tier-cut` is the closest). (3) Markdown mirror — every mutation writes `tickets/T-NNN.md` alongside SQLite; git-legible authoritative record; DB is rebuildable. (3) is probably the eventual answer.
- **Context:** Kanban's lack of a sync event breaks settled-reach's SQLite-authoritative approach the moment two devs collaborate.
- **Source:** 2026-04-21 planning.
---
+25
View File
@@ -0,0 +1,25 @@
# Open Questions — Extensions
Extension API shape, Lua runtime vendoring, manifest schema version.
---
### Q-008: Extension API shape — widgets, subcommands, both?
- **Status:** Open
- **Question:** Should extensions contribute widgets (panels, tabs, status-bar items), subcommands (CLI verbs), or both? Both is the obvious answer but has a cost in API surface that must be designed carefully to satisfy user/Claude parity ([D-006](architecture.md)).
- **Context:** CLAUDE.md flags this as "decide during Tier 6." `builtin.*` stubs today contribute widgets + commands + keybindings; the third-party Lua contract has to match.
- **Source:** CLAUDE.md "Open questions" footer.
### Q-009: Lua runtime vendoring
- **Status:** Open
- **Question:** Does the Lua supporter tool ([D-019](extensions.md#d-019-lua-runtime-as-ptyc-peer-supporter-tool)) bundle liblua source (build with the binary) or link system liblua (smaller binary, fragile ABI)?
- **Context:** `ptyc` has no deps; Lua is different — it's a whole VM. Bundling is the straightforward choice but locks a Lua version per clide release.
- **Source:** 2026-04-21 planning.
### Q-010: Extension manifest `schema_version:`
- **Status:** Open
- **Question:** What's the manifest schema-version scheme and bump policy? Coupled with [Q-005](questions-architecture.md#q-005-ipc-wire-format-stability) (IPC wire format) — both want a versioning story.
- **Context:** Today's manifests have no `schema_version:`. Adding one is cheap; the hard part is deciding when we bump.
- **Source:** 2026-04-21 planning.
---
+44
View File
@@ -0,0 +1,44 @@
# Open Questions — Process + Tooling
Editor tab, tree-sitter, icon set, theme hot-reload, kernel DB access.
Tooling-domain questions currently live here too. Split into
`questions-tooling.md` if this file outgrows ~350 lines.
---
### Q-015: Editor tab — full LSP vs tree-sitter-only highlight
- **Status:** Open
- **Question:** Tier 2's editor tab: do we integrate a full LSP story (analyzer server + hovers + completions + diagnostics) or ship tree-sitter-only syntax highlighting and defer LSP to Tier 6?
- **Context:** Full LSP is a large subsystem; tree-sitter is a weekend. User is a heavy LSP user in other IDEs — missing it hurts. CLAUDE.md flags this as "decide during Tier 2."
- **Source:** CLAUDE.md "Open questions" footer.
### Q-016: `tree-sitter-dart` grammar maintenance
- **Status:** Open
- **Question:** `UserNobody14/tree-sitter-dart` is archived. `nielsenko/tree-sitter-dart` is the maintained fork. Do we pin `nielsenko/`, mirror it in-repo, or lean on the Dart analyzer's own semantic output and skip tree-sitter for Dart?
- **Context:** If tree-sitter is the Tier-2 answer ([Q-015](#q-015-editor-tab-full-lsp-vs-tree-sitter-only)), grammar sourcing matters.
- **Source:** 2026-04-21 planning.
### Q-017: Icon set growth
- **Status:** Open
- **Question:** Hand-drawn `CustomPainter` catalogue (total control, pixel-perfect on every theme, slow to grow) vs SVG + parser (faster to grow, one more dep, theming is harder)?
- **Context:** We rejected Nerd-font glyphs ([R-006](rejected.md#r-006-nerd-font-glyph-icons)); something has to fill the gap.
- **Source:** 2026-04-21 planning.
### Q-018: Theme hot-reload in release builds
- **Status:** Open
- **Question:** The theme picker supports live-reloading a YAML during development. Does the same path stay open in release builds (user tweaks `~/.config/clide/themes/foo.yaml` and the app re-reads on focus) or is release-build theming restricted to built-in + settings-UI-installed themes?
- **Context:** Hot-reload is powerful for theme authoring but opens a file-watch + re-parse path in release code.
- **Source:** 2026-04-21 planning.
### Q-019: (withdrawn)
- **Status:** Resolved → n/a
- **Note:** Earlier floated as "ticket markdown mirror vs SQLite" — no longer a split question. Markdown mirror is tracked in [Q-022](questions-architecture.md#q-022-ticket-persistence-strategy); SQLite is the current stopgap per [D-040](process.md#d-040-python-stopgap-under-toolsscriptsplan).
### Q-020: Kernel DB service — namespaced SQL access?
- **Status:** Open
- **Question:** Do extensions get namespaced SQL access to `.clide/clide.db` (tables prefixed `ext_<id>_…`) or stay on the `kernel.settings` key/value facade? Admission-level question ([D-012](architecture.md#d-012-kernel-admission-rule)).
- **Context:** Some extensions (tickets, canvas, graph) naturally want relational storage. K/V gets awkward fast.
- **Source:** 2026-04-21 planning.
---
+17
View File
@@ -0,0 +1,17 @@
# Open Questions — Testing
---
### Q-011: Coverage gates — hard thresholds vs soft reporting
- **Status:** Open
- **Question:** `ci/test_coverage.sh` emits lcov + a summary. Do we gate merges on a hard threshold (fail < 80%), report softly, or tier per directory (kernel > 90%, built-ins > 70%, widgets covered by goldens exempt)?
- **Context:** Hard thresholds force tests-for-coverage-sake; soft reporting gets ignored.
- **Source:** 2026-04-21 planning.
### Q-012: Screen-reader automation (axe-core via Playwright)
- **Status:** Open
- **Question:** [D-022](accessibility.md#d-022-wcag-aa-contrast-gate-on-bundled-themes) gates contrast at build time. Do we also run axe-core against the WASM build in Playwright for runtime a11y issues (missing labels, invalid roles, orphan focusables)?
- **Context:** axe-core is JS; runs in the browser against the rendered tree. Extra CI time; extra signal.
- **Source:** 2026-04-21 planning.
---
+24
View File
@@ -0,0 +1,24 @@
# Open Questions — Master Index
Open questions live in per-domain `questions-<domain>.md` files.
This index is a pointer and a place to record the most-load-bearing
open questions with one-line summaries.
## By domain
| File | Topics |
|------|--------|
| [questions-architecture.md](questions-architecture.md) | IPC, events, canvas, window chrome, macOS signing, pql absorption, ticket persistence |
| [questions-extensions.md](questions-extensions.md) | Extension API shape, Lua runtime vendoring, manifest schema version |
| [questions-accessibility.md](questions-accessibility.md) | Web-mode a11y, i18n plurals/gender/dates |
| [questions-testing.md](questions-testing.md) | Coverage gates, screen-reader automation |
| [questions-process.md](questions-process.md) | Editor tab (LSP vs tree-sitter), icon set, theme hot-reload, kernel DB access, planning-tool location |
## Load-bearing questions (gate other work)
- **[Q-021](questions-process.md#q-021-pql-absorbs-planning-vs-keeps-separate)** — Pql absorbs planning features vs clide absorbs pql vs separate CLI. Blocks the stopgap sunset and shapes the pql-side planning session.
- **[Q-022](questions-process.md#q-022-ticket-persistence-strategy)** — Ticket persistence: per-dev only / milestone-committed / markdown-mirrored. Shapes multi-contributor story.
- **[Q-005](questions-architecture.md#q-005-ipc-wire-format-stability)** — IPC wire-format stability and `schema_version:` in `project.yaml`. Decide when the first real subcommand lands.
- **[Q-015](questions-process.md#q-015-editor-tab-full-lsp-vs-tree-sitter-only)** — Editor tab: full LSP integration vs tree-sitter-only highlight. Decide during Tier 2.
---
+53
View File
@@ -0,0 +1,53 @@
# Rejected Alternatives
Alternatives considered and rejected, with rationale preserved for
future reference.
---
### R-003: `MaterialApp` root
- **Rejected:** 2026-04-21
- **Reason:** Dragged in Material theming, default icons, and platform chrome that fought the custom three-tier theme pipeline ([D-009](architecture.md#d-009-three-tier-theme-pipeline)). Every bundled theme had to override Material defaults to look like clide; the overrides were visible in widget tests as "why is this `ElevatedButton` colored this way."
- **Cross-reference:** [D-007](architecture.md#d-007-app-root-is-bare-widgetsapp)
### R-004: Flutter `intl` + ARB codegen for i18n
- **Rejected:** 2026-04-21
- **Reason:** ARB codegen is inflexible for plugin-contributed catalogs — every catalogue needs a codegen pass, every extension ships with pre-generated Dart, and runtime merging is fighting the tool. The fframe text-driven pattern reads JSON at runtime with no codegen, which fits extension-shipped catalogs cleanly.
- **Cross-reference:** [D-021](accessibility.md#d-021-i18n-is-a-tier-0-contract)
### R-005: Patrol test runner
- **Rejected:** 2026-04-21
- **Reason:** Adds a dependency (violates [D-031](tooling.md#d-031-prefer-zero-deps-exact-pin)) for a capability we get from Playwright + Flutter's own semantics tree. Patrol's value proposition (native-gesture emulation) is less relevant on Linux desktop than on mobile.
- **Cross-reference:** [D-026](testing.md#d-026-web-driver-raw-playwright-plus-flutter-semantics)
### R-006: Nerd-font glyph icons
- **Rejected:** 2026-04-21
- **Reason:** TUI hangover from the Python-era clide under `legacy/`. Not desktop-native; forces a font dependency; doesn't theme consistently. Clide uses custom icon primitives (Tier 6 revisits with proper icon-set design).
- **Cross-reference:** [Q-017](questions-process.md#q-017-icon-set-growth)
### R-007: `CupertinoApp` root
- **Rejected:** 2026-04-21
- **Reason:** iOS-opinionated; wrong shell for a Linux-primary desktop IDE. Same theming-collision problem as [R-003](#r-003-materialapp-root).
- **Cross-reference:** [D-007](architecture.md#d-007-app-root-is-bare-widgetsapp)
### R-008: Riverpod / Provider / BLoC for state
- **Rejected:** 2026-04-21
- **Reason:** Violates [D-031](tooling.md#d-031-prefer-zero-deps-exact-pin). `ChangeNotifier` + `ListenableBuilder` ship in the SDK, fake trivially, and cover the state model we need. The ergonomic wins of Riverpod / Provider don't clear the "new dependency" bar at clide's scale.
- **Cross-reference:** [D-010](architecture.md#d-010-state-management-changenotifier)
### R-009: Port planning tooling into clide
- **Rejected:** 2026-04-21
- **Reason:** Earlier in the planning session the assumption was "clide owns Dart subcommands for decisions + tickets." That breaks the day a contributor works in a terminal or in VS Code / JetBrains — they have no `clide` binary to run. Reversing: pql owns planning long-term (see [D-039](process.md#d-039-planning-tooling-lives-in-pql)); clide consumes via shell-out.
- **Cross-reference:** [D-039](process.md#d-039-planning-tooling-lives-in-pql)
### R-010: Python-script stopgap under `tooling/db/`
- **Rejected:** 2026-04-21
- **Reason:** Location, not language. Settled-reach puts scripts at `tooling/db/` — copying that path here creates a script-pollution problem: every project using the pattern commits its own copy. The accepted Python port ([D-040](process.md#d-040-python-stopgap-under-toolsscriptsplan)) lives at `tools/scripts/plan`, clearly signalled as dev-tooling and time-limited.
- **Cross-reference:** [D-040](process.md#d-040-python-stopgap-under-toolsscriptsplan)
### R-011: Permanent stopgap
- **Rejected:** 2026-04-21
- **Reason:** If the Python port under `tools/scripts/plan` outlasts pql's feature parity, delete it. The deletion commit should be one changeset: remove `tools/scripts/plan`, remove its Makefile target (`decisions-validate` rewires to `pql decisions validate`), add a `CHANGELOG.md` entry under Removed, and verify `.pql/pql.db` still opens under the new `pql` binary.
- **Cross-reference:** [D-040](process.md#d-040-python-stopgap-under-toolsscriptsplan)
---
+63
View File
@@ -0,0 +1,63 @@
# Testing Decisions
Test pyramid, drivers, client-side constraint.
---
### D-023: Test pyramid — seven layers
- **Date:** 2026-04-21
- **Decision:** The pyramid has seven layers: unit (pure Dart) → widget (pumped + find) → golden (visual primitives) → a11y (semantics coverage + keyboard + contrast + i18n) → integration (`flutter test integration_test/`) → E2E (Playwright driving the WASM build + `clide --daemon` subprocess) → startup-smoke (`ci/smoke_bundle.sh`: build Linux release, run under xvfb for 5 s).
- **Rationale:** Each layer catches a distinct regression class. Skipping any layer means that class ships unprotected. Pushed back when earlier rounds proposed "just widget + E2E"; widget can't catch paint regressions (that's golden), E2E can't catch a11y tree drift (that's semantics).
- **Cost:** Seven CI jobs; total wall time budgeted at < 15 min. Pre-push runs layers 1-4 (< 90 s — see [D-029](#d-029-pre-push-gate-fast-layer-only)).
- **Raised by:** 2026-04-21 planning.
### D-024: Golden tests — primitives only, Alchemist + Ahem
- **Date:** 2026-04-21
- **Decision:** Goldens cover primitive widgets only (button, tab, panel header, token-bound surfaces). Composed layouts are tested via widget-find assertions, not pixel goldens. Golden rendering uses Alchemist with the Ahem font to get deterministic text metrics across platforms.
- **Rationale:** Pixel goldens of composed layouts churn constantly (one tweak → fifty golden diffs) without catching more than primitive goldens would. Alchemist + Ahem sidesteps the "font rendering differs between Linux CI and macOS dev" trap.
- **Cost:** Goldens have zero real text; layouts rely on widget tests. Acceptable.
- **Raised by:** 2026-04-21 planning.
### D-025: Mocks — mocktail at IO, hand-rolled fakes for ChangeNotifiers
- **Date:** 2026-04-21
- **Decision:** `mocktail 1.0.4` mocks IO boundaries (sockets, processes, `dart:io` File/Directory). `ChangeNotifier` facades get hand-rolled fakes — tiny classes that extend `ChangeNotifier` with test-controlled setters. No `mocktail` for notifiers.
- **Rationale:** Mocking a `ChangeNotifier` with a generated mock hides subscription bugs — `notifyListeners` becomes a mock call instead of actually firing. Hand-rolled fakes exercise the real subscription machinery.
- **Cost:** Roughly 20 lines per fake. Rounds out to less code than configuring a mocktail whenCall chain.
- **Raised by:** 2026-04-21 planning.
### D-026: Web driver — raw Playwright + Flutter semantics
- **Date:** 2026-04-21
- **Decision:** The browser-side E2E driver uses Playwright directly against Flutter's semantics tree (`flt-semantics[aria-label]`). No Patrol, no flutter_driver for web. The driver (`tools/ui/driver.ts`) clicks `flt-semantics-placeholder` on load to activate semantics, then queries by substring aria-label (Flutter merges sibling labels).
- **Rationale:** Patrol adds a dependency for a capability we get from semantics + Playwright directly. Labels are the a11y tree we already contract to maintain ([D-020](accessibility.md#d-020-a11y-is-a-tier-0-contract)); reusing them for E2E is a win.
- **Cost:** Driver has to know Flutter's sibling-merging behaviour — documented in `docs/testing/claude-ui-workflow.md`.
- **Raised by:** 2026-04-21 planning.
### D-027: Startup regression gate
- **Date:** 2026-04-21
- **Decision:** Two gates guard boot regressions: `integration_test/app_starts_test.dart` (fast — boots the app under `flutter test`) and `ci/smoke_bundle.sh` (slow — `flutter build linux`, run the bundle under `xvfb` for 5 s, assert no exit code). Both run in CI as `startup-bundle` job.
- **Rationale:** The fast integration test catches "boot hangs in Dart land"; the bundle smoke catches "boot breaks under release compile + production xvfb" — different regression classes.
- **Cost:** One extra CI job + `xvfb` on the runner. Five seconds of boot is enough; we've already caught one regression at this gate.
- **Raised by:** 2026-04-21 planning.
### D-028: Test organisation — mirror `lib/` in `test/`
- **Date:** 2026-04-21
- **Decision:** Every test file lives at the same relative path as its subject. `app/lib/kernel/src/i18n/catalog_loader.dart` pairs with `app/test/kernel/i18n/catalog_loader_test.dart`. No separate `unit/` vs `widget/` directories; test type is detected by what the test imports.
- **Rationale:** Matching paths makes "jump to test" predictable in any editor. Type-by-imports matches how `flutter test` already works.
- **Cost:** Large feature folders mirror into large test folders. Acceptable.
- **Raised by:** 2026-04-21 planning.
### D-029: Pre-push gate — fast layer only
- **Date:** 2026-04-21
- **Decision:** `make push-check` runs analyze + format + unit + widget + golden + a11y, target < 90 s. Integration, E2E, and startup-bundle run in CI but not on pre-push.
- **Rationale:** Pre-push gates that exceed ~90 s get disabled by muscle memory ("just push, it'll catch in CI"). Keeping the gate fast keeps it respected. Integration + E2E + bundle still gate merge via CI.
- **Cost:** Some regressions land on `main` that CI catches. Rollback or hotfix — acceptable for a solo-or-small-team cadence.
- **Raised by:** 2026-04-21 planning.
### D-030: Tests are client-side only
- **Date:** 2026-04-21
- **Decision:** No test hits the network. No test depends on remote fixtures, shared DBs, or state outside the test process. Fakes and fixtures live in-tree.
- **Rationale:** Network-dependent tests flake; flaky tests get quarantined; quarantined tests get deleted. Client-side-only makes CI deterministic offline.
- **Cost:** pql / daemon / extension tests stand up real subprocesses and real sockets locally — no mocked network convenience.
- **Raised by:** 2026-04-21 planning.
---
+28
View File
@@ -0,0 +1,28 @@
# Tooling Decisions
Toolchain, supply chain, CI, ignore strategy.
---
### D-031: Prefer-zero-deps, exact-pin
- **Date:** 2026-04-21
- **Decision:** Default to writing code ourselves. Every third-party Dart dependency needs a paragraph of justification in the PR that adds it. What stays is exact-pinned in `pubspec.yaml` (no caret ranges), `pubspec.lock` is committed, and advisories are reviewed before every bump.
- **Rationale:** Supply-chain gate. Flutter SDK + Dart SDK give us most of what we need; the dependencies we keep are the ones we can't reasonably write (yaml parser, mocktail, alchemist). Exact-pin because caret ranges mean "the CVE bumps itself in silently."
- **Cost:** Longer PR descriptions for deps; occasional reinvention of a convenience. Accepted.
- **Raised by:** 2026-04-21 planning; reinforced by user feedback memory.
### D-032: CI — Gitea primary, Linux-only runners, not yet activated
- **Date:** 2026-04-21
- **Decision:** CI config lives at `.gitea/workflows/test.yml` (Gitea Actions consumes GitHub-Actions syntax). Runners are Linux only; macOS is tested locally. The workflow is ready but Gitea Actions is not yet activated on the instance — the file is a staged pipeline for review. If the repo moves to GitHub, the file copies to `.github/workflows/test.yml` verbatim.
- **Rationale:** We want the CI story defined before we turn CI on — lower blast radius on early red builds. GitHub portability is free because the syntax is shared.
- **Cost:** PRs don't run CI yet; `make push-check` is the gate until activation.
- **Raised by:** 2026-04-21 planning.
### D-033: Golden-output ignore pattern — `coverage.*` excludes output, not scripts
- **Date:** 2026-04-21
- **Decision:** `.gitignore` excludes `coverage.*` (the lcov output files from `flutter test --coverage`). Coverage-related scripts are named `ci/test_coverage.sh` (not `ci/coverage.sh`) to stay outside the pattern.
- **Rationale:** An earlier draft named the script `ci/coverage.sh` and it was silently git-ignored. Renaming the script is cheaper than narrowing the gitignore pattern (which risks re-introducing output churn).
- **Cost:** Script names have a convention to follow.
- **Raised by:** 2026-04-21 planning (caught during commit rehearsal).
---