install ratcheting line-coverage gate at 34% floor (T-90)

First child of T-89. Codifies "don't make coverage worse" as a
durable pre-push contract before any test-writing children land.

- pubspec.yaml: new `coverage_floor: 34` key. Single source of
  truth for the floor; ratchets up only.
- ci/coverage_gate.sh: parses coverage/lcov.info (LH/LF), reads
  the floor from pubspec.yaml, exits non-zero if integer-truncated
  measured % drops below it. Self-contained awk parser — no `lcov`
  CLI dependency.
- ci/test.sh: flutter test now runs with --coverage, so the gate
  reads fresh data without an extra test invocation. Wall time
  delta is small and stays inside the < 90 s pre-push budget
  (D-29).
- Makefile: new `coverage-gate` target wires the script in;
  `push-check` adds it as a dependency. The .githooks/pre-push
  hook (already wired) picks this up automatically.
- .gitignore: ignore /coverage/ wholesale; the floor lives in
  pubspec.yaml, nothing under coverage/ is committed.

Decision recorded as D-66 (decisions/testing.md). End target is
95%; reaching it is tracked as the rest of T-89's children.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-06 22:44:43 +02:00
co-authored by Claude
parent a67a768592
commit 1151a0ce80
8 changed files with 298 additions and 113 deletions
+5
View File
@@ -36,6 +36,11 @@ tools/ui/.serve.pid
# -- dart doc output (generated by `dart doc`, uploaded as CI artefact) -
/doc/
# -- coverage output (regenerated by every `flutter test --coverage`).
# Floor lives in pubspec.yaml `coverage_floor:`; nothing under
# coverage/ is committed.
/coverage/
# -- Test, coverage, profile output ------------------------------------
*.test
*.out
+216 -110
View File
@@ -1,5 +1,5 @@
{
"exported_at": "2026-05-06T20:29:23Z",
"exported_at": "2026-05-06T20:44:43Z",
"decisions": [
{
"id": "D-1",
@@ -9,7 +9,7 @@
"status": "active",
"date": "2026-04-20",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-3",
@@ -19,7 +19,7 @@
"status": "active",
"date": "2026-04-20",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-4",
@@ -29,7 +29,7 @@
"status": "active",
"date": "2026-04-20",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-5",
@@ -39,7 +39,7 @@
"status": "active",
"date": "2026-04-20",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-6",
@@ -49,7 +49,7 @@
"status": "active",
"date": "2026-04-20",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-7",
@@ -59,7 +59,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-8",
@@ -69,7 +69,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-9",
@@ -79,7 +79,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-10",
@@ -89,7 +89,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-11",
@@ -99,7 +99,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-12",
@@ -109,7 +109,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-13",
@@ -119,7 +119,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-14",
@@ -129,7 +129,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-15",
@@ -139,7 +139,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/extensions.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-16",
@@ -149,7 +149,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/extensions.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-17",
@@ -159,7 +159,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/extensions.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-18",
@@ -169,7 +169,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/extensions.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-19",
@@ -179,7 +179,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/extensions.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-20",
@@ -189,7 +189,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/accessibility.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-21",
@@ -199,7 +199,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/accessibility.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-22",
@@ -209,7 +209,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/accessibility.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-23",
@@ -219,7 +219,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/testing.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-24",
@@ -229,7 +229,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/testing.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-25",
@@ -239,7 +239,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/testing.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-26",
@@ -249,7 +249,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/testing.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-27",
@@ -259,7 +259,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/testing.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-28",
@@ -269,7 +269,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/testing.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-29",
@@ -279,7 +279,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/testing.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-30",
@@ -289,7 +289,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/testing.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-31",
@@ -299,7 +299,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-32",
@@ -309,7 +309,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-33",
@@ -319,7 +319,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-34",
@@ -329,7 +329,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-35",
@@ -339,7 +339,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-36",
@@ -349,7 +349,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-37",
@@ -359,7 +359,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-38",
@@ -369,7 +369,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-39",
@@ -379,7 +379,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-40",
@@ -389,7 +389,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-41",
@@ -399,7 +399,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-42",
@@ -409,7 +409,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-43",
@@ -419,7 +419,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-44",
@@ -429,7 +429,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-45",
@@ -439,7 +439,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-46",
@@ -449,7 +449,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/extensions.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-47",
@@ -459,7 +459,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-48",
@@ -469,7 +469,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-49",
@@ -479,7 +479,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-50",
@@ -489,7 +489,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-51",
@@ -499,7 +499,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-52",
@@ -509,7 +509,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-53",
@@ -519,7 +519,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-54",
@@ -529,7 +529,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-55",
@@ -539,7 +539,7 @@
"status": "active",
"date": "2026-04-23",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-56",
@@ -549,7 +549,7 @@
"status": "active",
"date": "2026-04-23",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-57",
@@ -559,7 +559,7 @@
"status": "active",
"date": "2026-04-23",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-58",
@@ -569,7 +569,7 @@
"status": "active",
"date": "2026-04-23",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-59",
@@ -579,7 +579,7 @@
"status": "active",
"date": "2026-04-25",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-60",
@@ -589,7 +589,7 @@
"status": "active",
"date": "2026-04-26",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-61",
@@ -599,7 +599,7 @@
"status": "active",
"date": "2026-04-26",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-62",
@@ -609,7 +609,7 @@
"status": "active",
"date": "2026-04-26",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-63",
@@ -619,7 +619,7 @@
"status": "active",
"date": "2026-04-26",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-64",
@@ -629,7 +629,7 @@
"status": "active",
"date": "2026-05-03",
"file_path": "decisions/architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-65",
@@ -639,7 +639,17 @@
"status": "active",
"date": "2026-04-26",
"file_path": "decisions/tooling.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "D-66",
"type": "confirmed",
"domain": "testing",
"title": "Line coverage gate at 95%, ratcheted from current",
"status": "active",
"date": "2026-05-06",
"file_path": "decisions/testing.md",
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-1",
@@ -648,7 +658,7 @@
"title": "Authorisation granularity on the IPC socket",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-2",
@@ -657,7 +667,7 @@
"title": "Back-pressure on event streams",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-3",
@@ -666,7 +676,7 @@
"title": "Event persistence + audit/undo",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-4",
@@ -675,7 +685,7 @@
"title": "`.canvas` schema compatibility with Obsidian",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-5",
@@ -684,7 +694,7 @@
"title": "IPC wire-format stability + `schema_version:`",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-6",
@@ -693,7 +703,7 @@
"title": "Window chrome — native frame vs frameless custom",
"status": "resolved",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-7",
@@ -702,7 +712,7 @@
"title": "macOS app bundle signing / notarisation",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-8",
@@ -711,7 +721,7 @@
"title": "Extension API shape — widgets, subcommands, both?",
"status": "open",
"file_path": "decisions/questions-extensions.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-9",
@@ -720,7 +730,7 @@
"title": "Lua runtime vendoring",
"status": "open",
"file_path": "decisions/questions-extensions.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-10",
@@ -729,7 +739,7 @@
"title": "Extension manifest `schema_version:`",
"status": "open",
"file_path": "decisions/questions-extensions.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-11",
@@ -738,7 +748,7 @@
"title": "Coverage gates — hard thresholds vs soft reporting",
"status": "open",
"file_path": "decisions/questions-testing.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-12",
@@ -747,7 +757,7 @@
"title": "Screen-reader automation (axe-core via Playwright)",
"status": "open",
"file_path": "decisions/questions-testing.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-13",
@@ -756,7 +766,7 @@
"title": "Web production-mode a11y",
"status": "open",
"file_path": "decisions/questions-accessibility.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-14",
@@ -765,7 +775,7 @@
"title": "i18n plurals / gender / date-format tooling",
"status": "open",
"file_path": "decisions/questions-accessibility.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-15",
@@ -774,7 +784,7 @@
"title": "Editor tab — full LSP vs tree-sitter-only highlight",
"status": "open",
"file_path": "decisions/questions-process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-16",
@@ -783,7 +793,7 @@
"title": "`tree-sitter-dart` grammar maintenance",
"status": "open",
"file_path": "decisions/questions-process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-17",
@@ -792,7 +802,7 @@
"title": "Icon set growth",
"status": "open",
"file_path": "decisions/questions-process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-18",
@@ -801,7 +811,7 @@
"title": "Theme hot-reload in release builds",
"status": "open",
"file_path": "decisions/questions-process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-19",
@@ -810,7 +820,7 @@
"title": "(withdrawn)",
"status": "resolved",
"file_path": "decisions/questions-process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-20",
@@ -819,7 +829,7 @@
"title": "Kernel DB service — namespaced SQL access?",
"status": "open",
"file_path": "decisions/questions-process.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-21",
@@ -828,7 +838,7 @@
"title": "Pql absorbs planning vs keeps separate",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-22",
@@ -837,7 +847,7 @@
"title": "Ticket persistence strategy",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-23",
@@ -846,7 +856,7 @@
"title": "SSH-remote development — run clide against a remote workspace",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-25",
@@ -855,7 +865,7 @@
"title": "Body text face — mono everywhere vs Josefin Sans UI + mono code",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-26",
@@ -864,7 +874,7 @@
"title": "Small screen layout (\u003c 1000px)",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-27",
@@ -873,7 +883,7 @@
"title": "Two-editor split",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-28",
@@ -882,7 +892,7 @@
"title": "Terminal strip scope — shell only or logs/errors/tests",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-29",
@@ -891,7 +901,7 @@
"title": "Branch picker location",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-30",
@@ -900,7 +910,7 @@
"title": "Focus behavior when editor is dirty and viewer is peeked",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "Q-31",
@@ -909,7 +919,7 @@
"title": "XWayland fallback for frameless — proper Wayland protocol needed",
"status": "open",
"file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-2",
@@ -919,7 +929,7 @@
"status": "active",
"date": "2026-04-20",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-3",
@@ -929,7 +939,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-4",
@@ -939,7 +949,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-5",
@@ -949,7 +959,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-6",
@@ -959,7 +969,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-7",
@@ -969,7 +979,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-8",
@@ -979,7 +989,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-9",
@@ -989,7 +999,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-10",
@@ -999,7 +1009,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-11",
@@ -1009,7 +1019,7 @@
"status": "active",
"date": "2026-04-21",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
},
{
"id": "R-12",
@@ -1019,7 +1029,7 @@
"status": "active",
"date": "2026-04-22",
"file_path": "decisions/rejected.md",
"synced_at": "2026-05-06 20:27:32"
"synced_at": "2026-05-06 20:36:42"
}
],
"decision_refs": [
@@ -1545,6 +1555,12 @@
"ref_type": "references",
"note": "**Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml), [D-61](#d-61-dependency-vetting-checklist), `POLICY.md`."
},
{
"source_id": "D-66",
"target_id": "D-29",
"ref_type": "references",
"note": "**Cost:** Pre-push wall time grows by `flutter test --coverage` (currently ≈11 s on this tree). Acceptable within D-29's \u003c 90 s budget; reassess if it slips. Floor bumps require an explicit edit ..."
},
{
"source_id": "D-7",
"target_id": "D-9",
@@ -2593,20 +2609,20 @@
"type": "task",
"title": "ship matching bold weight to restore semantic bold rendering",
"description": "Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.\n\n**Acceptance:**\n- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).\n- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.\n- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).\n\n**Options to evaluate:**\n- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)\n- Switch to a variable JetBrainsMono and request a specific weight axis position.\n- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).\n\n---\nRefinement (2026-05-06):\n\nInvestigation showed the perceived over-bolding in the Claude pane was synthetic-bold smearing (Flutter overpaints when no Bold.ttf is registered), not aggressive-but-legitimate bold. Visual A/B confirms a real bold face renders crisp emphasis without smearing. No per-pane renderer config needed.\n\n**Concrete fix shape:**\n1. `pubspec.yaml` (family JetBrainsMono): register `JetBrainsMono-Bold.ttf` with `weight: 700` and `JetBrainsMono-BoldItalic.ttf` with `weight: 700, style: italic`. Files are already on disk under `assets/fonts/jetbrains_mono/`.\n2. `assets/licenses.yaml` JetBrainsMono entry: update `weights_bundled` to `[Regular, Italic, Bold, BoldItalic]` per D-42 (must match what is actually wired into the font family).\n3. `lib/src/terminal/src/ui/painter.dart:198`: revert the `bold: false` override and delete the now-stale comment block at lines 193-195.\n4. Test: render a mixed-weight buffer and assert `paragraph.maxIntrinsicWidth` matches the all-regular measurement (cell drift = 0). If parity fails, the fallback options stay open (variable JetBrainsMono / Berkeley Mono / IBM Plex Mono).\n\nWorkaround comment (`Skip bold rendering — Flutter`s synthetic bold...`) goes away in step 3.",
"status": "in_progress",
"status": "done",
"priority": "medium",
"created_at": "2026-05-05 12:53:21",
"updated_at": "2026-05-06 16:13:39"
"updated_at": "2026-05-06 20:29:28"
},
{
"id": "T-74",
"type": "task",
"title": "forward real mouse events to TUI apps (not just PgUp/PgDown)",
"description": "Mouse wheel scroll is currently translated to PgUp/PgDown key input in `lib/src/terminal/src/terminal_view.dart` as a pragmatic workaround — TUIs that capture mouse events without binding the wheel get a no-op otherwise. The downside: any TUI that does its own wheel handling (selection, click-to-position) loses the actual mouse coordinates because all we send is keyboard.\n\n**Acceptance:**\n- The terminal forwards `PointerScrollEvent` and click/drag/release as proper xterm mouse-protocol escape sequences (CSI `M` / SGR variant per the report mode the inner program declared via `?1000h` / `?1006h`).\n- Tested with `vim` (mouse mode `a`), `htop`, `less` — all should respond to mouse selection / click correctly.\n- Falls back to PgUp/PgDown only when the inner program has not declared a mouse mode.\n- The `mouseInput` API on `Terminal` is wired through `Listener.onPointerSignal` instead of being bypassed.\n\n**Why this isn`t the default already:** the bypass was the cheapest fix to get scroll working at all when the multitab/Scrollable ate scroll events. With those fixes landed, the right-shaped mouse forwarding is the proper next step.",
"status": "in_progress",
"status": "ready",
"priority": "medium",
"created_at": "2026-05-05 12:53:22",
"updated_at": "2026-05-06 16:13:39"
"updated_at": "2026-05-06 20:40:13"
},
{
"id": "T-75",
@@ -2740,6 +2756,29 @@
"decision_ref": "D-59",
"created_at": "2026-05-06 14:34:35",
"updated_at": "2026-05-06 15:10:35"
},
{
"id": "T-89",
"type": "epic",
"title": "drive line coverage from 35% to 95%",
"description": "Today: total line coverage **34.9%** (2984/8549 lines). Per area:\n\n| area | hit / total | % |\n|---|---|---|\n| `lib/src/terminal/` | 12 / 3167 | **0.4%** |\n| `lib/widgets/` | 537 / 1184 | 45.4% |\n| `lib/src/pql/` | 45 / 94 | 47.9% |\n| `lib/builtin/` | 269 / 550 | 48.9% |\n| `lib/kernel/` | 889 / 1691 | 52.6% |\n| `lib/src/daemon/` | 320 / 584 | 54.8% |\n| `lib/src/pty/` | 130 / 216 | 60.2% |\n| `lib/src/git/` | 356 / 518 | 68.7% |\n| `lib/src/ipc/` | 110 / 156 | 70.5% |\n| `lib/src/files/` | 98 / 133 | 73.7% |\n| `lib/extension/` | 41 / 54 | 75.9% |\n| `lib/src/editor/` | 107 / 129 | 82.9% |\n| `lib/src/panes/` | 70 / 73 | 95.9% |\n\nThe dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md (\"Own the rendering stack\") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.\n\n**End state:**\n- Total line coverage ≥ 95% (no per-area carve-outs).\n- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.\n- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.\n\n**Acceptance criteria for closing this epic:**\n1. `coverage/floor.txt` reads `95` (or higher).\n2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.\n3. The pre-push gate computes coverage and rejects pushes below the floor.\n4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.\n\n**Strategy / child shape:**\nThe gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.\n\n1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.\n2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area's structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).\n3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.\n4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.\n\n**Out of scope:**\n- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).\n- Integration / E2E coverage (only widget + unit feed lcov).\n- `lib/test_app.dart` exclusion: verify in the gate child whether Dart's coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.\n\n**Cross-references:** D-66 (this epic's decision record), D-29 (pre-push gate scope), \"Own the rendering stack\" guardrail (CLAUDE.md).\n",
"status": "in_progress",
"priority": "high",
"decision_ref": "D-66",
"created_at": "2026-05-06 20:36:42",
"updated_at": "2026-05-06 20:40:16"
},
{
"id": "T-90",
"type": "task",
"parent_id": "T-89",
"title": "install ratcheting coverage gate at current floor",
"description": "First child of T-89. Locks in the no-regression contract before any test-writing children land — so subsequent floor bumps are explicit and reviewable.\n\n**What lands:**\n\n1. **`coverage/floor.txt`** — committed file containing today's actual line-coverage number (currently `34` after rounding down from 34.9% to give a small margin against measurement jitter; the gate child re-measures on first install and writes the exact number it observes minus 1 if needed).\n2. **`ci/coverage_gate.sh`** — runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info` (sum of LH / sum of LF * 100), compares to `coverage/floor.txt`, exits non-zero if below, prints the actual percentage either way.\n3. **`Makefile` `coverage-gate` target** — wraps `ci/coverage_gate.sh` for local use.\n4. **`make push-check`** — adds `coverage-gate` to its dependency list. Pre-push hook (already wired via `.githooks/pre-push`) picks this up automatically.\n5. **`coverage/README.md`** — short note explaining the floor file, how to bump it (rule: bump only in the same commit that adds covering tests, with the bump reflecting the measured improvement), and the link to D-66.\n\n**Acceptance:**\n- `make coverage-gate` passes on a clean checkout (floor matches reality).\n- `make push-check` includes the gate.\n- A test deletion would fail `make coverage-gate` (verify by experiment, not just by reading the script — invariant has to actually hold).\n- Pre-push hook runs the gate (verify by attempting a deliberately-failing push and seeing it blocked).\n- `coverage/lcov.info` is gitignored (it's regenerated on every run; only `floor.txt` is committed).\n\n**Out of scope:**\n- Bumping the floor toward 95% — that's the test-writing children's job.\n- Branch coverage — D-66 explicitly excludes.\n- Per-area floors — D-66 chose a single global floor for simplicity.\n\n**Notes:**\n- `lcov` CLI is not installed on this dev machine; parser must be self-contained (awk or python). Don't introduce `lcov` as a dependency of the gate.\n- `coverage/lcov.info` is the standard Dart output path of `flutter test --coverage`. Don't relocate.\n- `flutter test --coverage` runs all tests including `forkpty`-tagged ones, which fail under the flutter test runner (see comment in `test/pty/session_test.dart`); `--exclude-tags forkpty` matches `ci/test.sh`. Forkpty-tagged tests don't currently feed lcov because they run via `dart test`; check whether their coverage can be merged later (separate ticket if not).\n",
"status": "in_progress",
"priority": "high",
"decision_ref": "D-66",
"created_at": "2026-05-06 20:38:14",
"updated_at": "2026-05-06 20:40:16"
}
],
"ticket_deps": null,
@@ -4285,6 +4324,73 @@
"old_value": "backlog",
"new_value": "in_progress",
"changed_at": "2026-05-06 16:13:39"
},
{
"ticket_id": "T-73",
"field": "status",
"old_value": "in_progress",
"new_value": "done",
"changed_at": "2026-05-06 20:29:28"
},
{
"ticket_id": "T-89",
"field": "description",
"new_value": "Today: total line coverage **34.9%** (2984/8549 lines). Per area:\n\n| area | hit / total | % |\n|---|---|---|\n| `lib/src/terminal/` | 12 / 3167 | **0.4%** |\n| `lib/widgets/` | 537 / 1184 | 45.4% |\n| `lib/src/pql/` | 45 / 94 | 47.9% |\n| `lib/builtin/` | 269 / 550 | 48.9% |\n| `lib/kernel/` | 889 / 1691 | 52.6% |\n| `lib/src/daemon/` | 320 / 584 | 54.8% |\n| `lib/src/pty/` | 130 / 216 | 60.2% |\n| `lib/src/git/` | 356 / 518 | 68.7% |\n| `lib/src/ipc/` | 110 / 156 | 70.5% |\n| `lib/src/files/` | 98 / 133 | 73.7% |\n| `lib/extension/` | 41 / 54 | 75.9% |\n| `lib/src/editor/` | 107 / 129 | 82.9% |\n| `lib/src/panes/` | 70 / 73 | 95.9% |\n\nThe dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md (\"Own the rendering stack\") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.\n\n**End state:**\n- Total line coverage ≥ 95% (no per-area carve-outs).\n- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.\n- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.\n\n**Acceptance criteria for closing this epic:**\n1. `coverage/floor.txt` reads `95` (or higher).\n2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.\n3. The pre-push gate computes coverage and rejects pushes below the floor.\n4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.\n\n**Strategy / child shape:**\nThe gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.\n\n1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.\n2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area's structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).\n3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.\n4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.\n\n**Out of scope:**\n- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).\n- Integration / E2E coverage (only widget + unit feed lcov).\n- `lib/test_app.dart` exclusion: verify in the gate child whether Dart's coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.\n\n**Cross-references:** D-66 (this epic's decision record), D-29 (pre-push gate scope), \"Own the rendering stack\" guardrail (CLAUDE.md).\n",
"changed_at": "2026-05-06 20:37:40"
},
{
"ticket_id": "T-90",
"field": "parent_id",
"new_value": "T-89",
"changed_at": "2026-05-06 20:38:24"
},
{
"ticket_id": "T-90",
"field": "description",
"new_value": "First child of T-89. Locks in the no-regression contract before any test-writing children land — so subsequent floor bumps are explicit and reviewable.\n\n**What lands:**\n\n1. **`coverage/floor.txt`** — committed file containing today's actual line-coverage number (currently `34` after rounding down from 34.9% to give a small margin against measurement jitter; the gate child re-measures on first install and writes the exact number it observes minus 1 if needed).\n2. **`ci/coverage_gate.sh`** — runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info` (sum of LH / sum of LF * 100), compares to `coverage/floor.txt`, exits non-zero if below, prints the actual percentage either way.\n3. **`Makefile` `coverage-gate` target** — wraps `ci/coverage_gate.sh` for local use.\n4. **`make push-check`** — adds `coverage-gate` to its dependency list. Pre-push hook (already wired via `.githooks/pre-push`) picks this up automatically.\n5. **`coverage/README.md`** — short note explaining the floor file, how to bump it (rule: bump only in the same commit that adds covering tests, with the bump reflecting the measured improvement), and the link to D-66.\n\n**Acceptance:**\n- `make coverage-gate` passes on a clean checkout (floor matches reality).\n- `make push-check` includes the gate.\n- A test deletion would fail `make coverage-gate` (verify by experiment, not just by reading the script — invariant has to actually hold).\n- Pre-push hook runs the gate (verify by attempting a deliberately-failing push and seeing it blocked).\n- `coverage/lcov.info` is gitignored (it's regenerated on every run; only `floor.txt` is committed).\n\n**Out of scope:**\n- Bumping the floor toward 95% — that's the test-writing children's job.\n- Branch coverage — D-66 explicitly excludes.\n- Per-area floors — D-66 chose a single global floor for simplicity.\n\n**Notes:**\n- `lcov` CLI is not installed on this dev machine; parser must be self-contained (awk or python). Don't introduce `lcov` as a dependency of the gate.\n- `coverage/lcov.info` is the standard Dart output path of `flutter test --coverage`. Don't relocate.\n- `flutter test --coverage` runs all tests including `forkpty`-tagged ones, which fail under the flutter test runner (see comment in `test/pty/session_test.dart`); `--exclude-tags forkpty` matches `ci/test.sh`. Forkpty-tagged tests don't currently feed lcov because they run via `dart test`; check whether their coverage can be merged later (separate ticket if not).\n",
"changed_at": "2026-05-06 20:39:05"
},
{
"ticket_id": "T-74",
"field": "status",
"old_value": "in_progress",
"new_value": "ready",
"changed_at": "2026-05-06 20:39:45"
},
{
"ticket_id": "T-89",
"field": "status",
"old_value": "backlog",
"new_value": "in_progress",
"changed_at": "2026-05-06 20:39:45"
},
{
"ticket_id": "T-90",
"field": "status",
"old_value": "backlog",
"new_value": "in_progress",
"changed_at": "2026-05-06 20:39:45"
},
{
"ticket_id": "T-74",
"field": "status",
"old_value": "ready",
"new_value": "ready",
"changed_at": "2026-05-06 20:40:13"
},
{
"ticket_id": "T-89",
"field": "status",
"old_value": "in_progress",
"new_value": "in_progress",
"changed_at": "2026-05-06 20:40:16"
},
{
"ticket_id": "T-90",
"field": "status",
"old_value": "in_progress",
"new_value": "in_progress",
"changed_at": "2026-05-06 20:40:16"
}
]
}
+8
View File
@@ -18,6 +18,14 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Added
- Pre-push coverage gate — `make push-check` (and the
`.githooks/pre-push` hook that calls it) now runs
`ci/coverage_gate.sh`, which fails if total line coverage drops
below the `coverage_floor:` value in `pubspec.yaml`. The floor
starts at 34% (today's measured floor) and only ratchets up; the
end target is 95% (D-66). `ci/test.sh` now writes
`coverage/lcov.info` as a side effect of the unit/widget/golden
run so the gate adds no extra test invocation.
- Staged `dart doc` CI job — generates and uploads an HTML API
reference for the public `lib/` surface. The step wraps
`dart doc --validate-links` and grep-fails the build on any warning,
+5 -1
View File
@@ -106,6 +106,10 @@ test-all: test-core test test-a11y test-integration test-e2e ## Everything, sequ
coverage: ## flutter test --coverage + lcov summary.
ci/test_coverage.sh
.PHONY: coverage-gate
coverage-gate: ## Coverage gate — fails if total line % < coverage/floor.txt (D-66). Assumes `make test` ran first.
ci/coverage_gate.sh
.PHONY: smoke-bundle
smoke-bundle: ## Build Linux release bundle and run it under xvfb for 5s.
ci/smoke_bundle.sh
@@ -276,7 +280,7 @@ decisions-validate: ## Parser dry-run over decisions/*.md.
pql decisions validate
.PHONY: push-check
push-check: decisions-validate test-core test test-a11y ## Pre-push gate.
push-check: decisions-validate test-core test test-a11y coverage-gate ## Pre-push gate.
.PHONY: hooks
hooks: ## Install the repo's git hooks.
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Coverage gate — fails if total line coverage drops below the
# `coverage_floor:` value in pubspec.yaml. Driven by D-66.
#
# Reads coverage/lcov.info (generated by `flutter test --coverage`,
# which `ci/test.sh` runs as part of the fast suite). Parses the
# total LF/LH counts and compares the integer percentage against
# the floor. The floor only ratchets up — bumping it requires an
# explicit edit to pubspec.yaml committed alongside the test
# additions that earned the bump.
#
# Self-contained parser (awk) — does not depend on `lcov` being
# installed on the dev machine.
set -euo pipefail
cd "$(dirname "$0")/.."
LCOV=coverage/lcov.info
if [[ ! -f "$LCOV" ]]; then
echo "==> coverage gate: $LCOV missing — run \`make test\` first (it writes lcov)" >&2
exit 2
fi
floor=$(awk -F: '/^coverage_floor:/ {gsub(/ /,"",$2); print $2; exit}' pubspec.yaml)
if [[ -z "$floor" ]]; then
echo "==> coverage gate: pubspec.yaml is missing coverage_floor: — see D-66" >&2
exit 2
fi
read measured measured_int < <(
awk -F: '
/^LF:/ { lf += $2 }
/^LH:/ { lh += $2 }
END {
pct = (lh / lf) * 100
printf "%.2f %d\n", pct, int(pct)
}
' "$LCOV"
)
if (( measured_int < floor )); then
echo "==> coverage gate FAIL: ${measured}% < floor ${floor}%"
echo " Add tests, or — if the drop is intentional — explain in the commit and lower the floor explicitly."
exit 1
fi
if (( measured_int > floor )); then
echo "==> coverage gate OK: ${measured}% (floor ${floor}%) — ${measured_int}% available; consider bumping coverage/floor.txt to ${measured_int}"
else
echo "==> coverage gate OK: ${measured}% (floor ${floor}%)"
fi
+2 -2
View File
@@ -14,5 +14,5 @@ dart format --set-exit-if-changed .
echo "==> dart test (forkpty — incompatible with flutter test runner)"
dart test --tags forkpty test/pty/session_test.dart
echo "==> flutter test (unit + widget + golden)"
flutter test --exclude-tags forkpty
echo "==> flutter test --coverage (unit + widget + golden)"
flutter test --coverage --exclude-tags forkpty
+8
View File
@@ -60,4 +60,12 @@ Test pyramid, drivers, client-side constraint.
- **Cost:** pql / daemon / extension tests stand up real subprocesses and real sockets locally — no mocked network convenience.
- **Raised by:** 2026-04-21 planning.
### D-66: Line coverage gate at 95%, ratcheted from current
- **Date:** 2026-05-06
- **Decision:** The pre-push gate runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info`, and hard-fails if total line coverage drops below a committed floor at `coverage/floor.txt`. The floor starts at the actual current coverage (≈35%, dragged down by `lib/src/terminal/`'s 0.4%) and only ever ratchets up. The end target is 95%; getting there is tracked as a campaign of deliberate floor bumps under one epic ticket. **No carve-outs** — code under `lib/` is owned regardless of file-header attribution, including the terminal emulator port. Branch coverage is not gated (Dart's lcov output models it weakly). Lint suppressions to dodge the gate are never acceptable.
- **Rationale:** A flat 95% threshold today blocks every push; an informational coverage report rots into noise. The committed-floor ratchet makes "don't make it worse" the durable rule and turns the journey to 95% into explicit, reviewed bumps rather than a single overnight cliff. Excluding `forkpty`-tagged tests matches `ci/test.sh` (forkpty + flutter test runner are incompatible — see `test/pty/session_test.dart`).
- **Cost:** Pre-push wall time grows by `flutter test --coverage` (currently ≈11 s on this tree). Acceptable within D-29's < 90 s budget; reassess if it slips. Floor bumps require an explicit edit to `coverage/floor.txt` in the same commit that adds tests — so contributors can't silently raise it.
- **Cross-reference:** [D-29](#d-29-pre-push-gate-fast-layer-only).
- **Raised by:** 2026-05-06 — coverage triage during T-73 follow-up.
---
+4
View File
@@ -16,6 +16,10 @@ publish_to: none
version: 2.0.0
repository: https://github.com/postmeridiem/clide
# Pre-push line-coverage floor. Ratchets up only — see D-66.
# Reading: `awk -F: '/^coverage_floor:/ {gsub(/ /,"",$2); print $2}' pubspec.yaml`.
coverage_floor: 34
# Project metadata (was project.yaml, folded in per D-056).
# version: above is the single source of truth. The Makefile reads
# it for build stamping. Cutting a release bumps version + changelog