diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 3a36c85b..b0ee7711 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-05-06T20:47:49Z", + "exported_at": "2026-05-06T20:55:15Z", "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:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-3", @@ -19,7 +19,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-4", @@ -29,7 +29,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-5", @@ -39,7 +39,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-6", @@ -49,7 +49,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-7", @@ -59,7 +59,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-8", @@ -69,7 +69,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-9", @@ -79,7 +79,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-10", @@ -89,7 +89,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-11", @@ -99,7 +99,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-12", @@ -109,7 +109,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-13", @@ -119,7 +119,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-14", @@ -129,7 +129,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-15", @@ -139,7 +139,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-16", @@ -149,7 +149,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-17", @@ -159,7 +159,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-18", @@ -169,7 +169,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-19", @@ -179,7 +179,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-20", @@ -189,7 +189,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-21", @@ -199,7 +199,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-22", @@ -209,7 +209,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-23", @@ -219,7 +219,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-24", @@ -229,7 +229,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-25", @@ -239,7 +239,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-26", @@ -249,7 +249,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-27", @@ -259,7 +259,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-28", @@ -269,7 +269,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-29", @@ -279,7 +279,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-30", @@ -289,7 +289,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-31", @@ -299,7 +299,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-32", @@ -309,7 +309,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-33", @@ -319,7 +319,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-34", @@ -329,7 +329,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-35", @@ -339,7 +339,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-36", @@ -349,7 +349,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-37", @@ -359,7 +359,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-38", @@ -369,7 +369,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-39", @@ -379,7 +379,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-40", @@ -389,7 +389,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-41", @@ -399,7 +399,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-42", @@ -409,7 +409,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-43", @@ -419,7 +419,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-44", @@ -429,7 +429,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-45", @@ -439,7 +439,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-46", @@ -449,7 +449,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-47", @@ -459,7 +459,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-48", @@ -469,7 +469,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-49", @@ -479,7 +479,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-50", @@ -489,7 +489,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-51", @@ -499,7 +499,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-52", @@ -509,7 +509,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-53", @@ -519,7 +519,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-54", @@ -529,7 +529,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-55", @@ -539,7 +539,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-56", @@ -549,7 +549,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-57", @@ -559,7 +559,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-58", @@ -569,7 +569,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-59", @@ -579,7 +579,7 @@ "status": "active", "date": "2026-04-25", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-60", @@ -589,7 +589,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-61", @@ -599,7 +599,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-62", @@ -609,7 +609,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-63", @@ -619,7 +619,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-64", @@ -629,7 +629,7 @@ "status": "active", "date": "2026-05-03", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-65", @@ -639,7 +639,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "D-66", @@ -649,7 +649,7 @@ "status": "active", "date": "2026-05-06", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-1", @@ -658,7 +658,7 @@ "title": "Authorisation granularity on the IPC socket", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-2", @@ -667,7 +667,7 @@ "title": "Back-pressure on event streams", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-3", @@ -676,7 +676,7 @@ "title": "Event persistence + audit/undo", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-4", @@ -685,7 +685,7 @@ "title": "`.canvas` schema compatibility with Obsidian", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-5", @@ -694,7 +694,7 @@ "title": "IPC wire-format stability + `schema_version:`", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-6", @@ -703,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:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-7", @@ -712,7 +712,7 @@ "title": "macOS app bundle signing / notarisation", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-8", @@ -721,7 +721,7 @@ "title": "Extension API shape — widgets, subcommands, both?", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-9", @@ -730,7 +730,7 @@ "title": "Lua runtime vendoring", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-10", @@ -739,7 +739,7 @@ "title": "Extension manifest `schema_version:`", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-11", @@ -748,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:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-12", @@ -757,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:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-13", @@ -766,7 +766,7 @@ "title": "Web production-mode a11y", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-14", @@ -775,7 +775,7 @@ "title": "i18n plurals / gender / date-format tooling", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-15", @@ -784,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:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-16", @@ -793,7 +793,7 @@ "title": "`tree-sitter-dart` grammar maintenance", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-17", @@ -802,7 +802,7 @@ "title": "Icon set growth", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-18", @@ -811,7 +811,7 @@ "title": "Theme hot-reload in release builds", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-19", @@ -820,7 +820,7 @@ "title": "(withdrawn)", "status": "resolved", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-20", @@ -829,7 +829,7 @@ "title": "Kernel DB service — namespaced SQL access?", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-21", @@ -838,7 +838,7 @@ "title": "Pql absorbs planning vs keeps separate", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-22", @@ -847,7 +847,7 @@ "title": "Ticket persistence strategy", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-23", @@ -856,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:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-25", @@ -865,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:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-26", @@ -874,7 +874,7 @@ "title": "Small screen layout (\u003c 1000px)", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-27", @@ -883,7 +883,7 @@ "title": "Two-editor split", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-28", @@ -892,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:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-29", @@ -901,7 +901,7 @@ "title": "Branch picker location", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-30", @@ -910,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:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "Q-31", @@ -919,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:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-2", @@ -929,7 +929,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-3", @@ -939,7 +939,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-4", @@ -949,7 +949,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-5", @@ -959,7 +959,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-6", @@ -969,7 +969,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-7", @@ -979,7 +979,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-8", @@ -989,7 +989,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-9", @@ -999,7 +999,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-10", @@ -1009,7 +1009,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-11", @@ -1019,7 +1019,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" }, { "id": "R-12", @@ -1029,7 +1029,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 20:36:42" + "synced_at": "2026-05-06 20:54:33" } ], "decision_refs": [ @@ -2779,6 +2779,18 @@ "decision_ref": "D-66", "created_at": "2026-05-06 20:38:14", "updated_at": "2026-05-06 20:44:47" + }, + { + "id": "T-91", + "type": "task", + "parent_id": "T-89", + "title": "test sweep: drive lib/src/terminal/ to 95%", + "description": "Second child of T-89. The dominant coverage hole — `lib/src/terminal/` is 12 / 3167 lines (0.4%), about 56% of the global gap.\n\n**Today's distribution within `lib/src/terminal/src/`:**\n\n| sub-area | hit / total | % | shape |\n|---|---|---|---|\n| `core/buffer/` | 0 / 581 | 0.0% | pure data structures |\n| `core/escape/` | 1 / 473 | 0.2% | pure parser logic |\n| `core/input/` | 4 / 292 | 1.4% | key + keytab tables |\n| `core/(root)` | 0 / 188 | 0.0% | callback wiring |\n| `core/mouse/` | 3 / 51 | 5.9% | encoder logic |\n| `ui/` | 3 / 823 | 0.4% | painters + render objects |\n| `(root)` | 0 / 471 | 0.0% | `Terminal` + `TerminalView` |\n| `utils/` | 1 / 251 | 0.4% | circular buffer etc. |\n\n**Strategy — pure logic first, then UI:**\n\n1. **`core/buffer/`** (581 lines, biggest single chunk, all pure Dart) — line/segment/cell models, scrollback math, range queries. Unit tests exercise the data structure directly.\n2. **`core/escape/`** (473 lines, pure state machine) — feed ANSI/VT byte streams in, assert the resulting buffer/cursor state. Reusable corpus from xterm.js / xterm.dart upstream is fair game.\n3. **`core/input/`** (292 lines) — key + keytab tables; assert mapping from logical key to escape bytes per terminal mode.\n4. **`core/mouse/`** (51 lines) — encoder for SGR-1006 / X11 mouse protocols. Small, fast.\n5. **`utils/`** (251 lines) — circular buffer + helpers; trivial unit tests.\n6. **`(root)`** (`terminal.dart`, `terminal_view.dart`, 471 lines) — orchestration; some unit-testable, some needs widget tests.\n7. **`ui/`** (823 lines) — painters and render objects; widget + golden tests. Hardest, most pixel-sensitive.\n\n**Acceptance:**\n- `lib/src/terminal/` line coverage ≥ 95%.\n- All tests fast (\u003c5 s for the area's test files combined; no real PTY shell-out).\n- Helper fixtures live in `test/terminal/_helpers/` if they get reused across files.\n- Per D-66, no `// ignore:` directives or analysis_options excludes added to dodge coverage.\n- `coverage_floor:` in `pubspec.yaml` is bumped in lockstep — every commit that adds covering tests bumps the floor by the measured delta (rounded down to the integer below the new percentage).\n\n**Strategy on commit cadence:**\nLand tests in batches per sub-area (one commit per `core/buffer/`, `core/escape/`, etc.). Each batch:\n- Adds tests under `test/terminal/\u003csub-area\u003e/`.\n- Re-measures coverage, bumps `coverage_floor:` to the new integer (or as high as the gate currently allows).\n- Documents in CHANGELOG under \"Changed\" (test-only changes can also fold into one CHANGELOG entry per area).\n\n**Out of scope:**\n- Refactoring `lib/src/terminal/` for testability — if a piece is genuinely untestable, surface it (don't silence it). Code under `lib/` is owned regardless of upstream attribution; refactor to make it testable rather than carving it out.\n- Branch coverage (D-66 explicitly excludes).\n- Mouse / keyboard *protocol* round-trips against real TUIs (that's T-74 and integration work).\n\n**Cross-references:** D-66 (coverage gate), T-89 (epic), T-90 (gate plumbing).\n", + "status": "in_progress", + "priority": "high", + "decision_ref": "D-66", + "created_at": "2026-05-06 20:49:47", + "updated_at": "2026-05-06 20:50:32" } ], "ticket_deps": null, @@ -4398,6 +4410,25 @@ "old_value": "in_progress", "new_value": "done", "changed_at": "2026-05-06 20:44:47" + }, + { + "ticket_id": "T-91", + "field": "parent_id", + "new_value": "T-89", + "changed_at": "2026-05-06 20:49:47" + }, + { + "ticket_id": "T-91", + "field": "description", + "new_value": "Second child of T-89. The dominant coverage hole — `lib/src/terminal/` is 12 / 3167 lines (0.4%), about 56% of the global gap.\n\n**Today's distribution within `lib/src/terminal/src/`:**\n\n| sub-area | hit / total | % | shape |\n|---|---|---|---|\n| `core/buffer/` | 0 / 581 | 0.0% | pure data structures |\n| `core/escape/` | 1 / 473 | 0.2% | pure parser logic |\n| `core/input/` | 4 / 292 | 1.4% | key + keytab tables |\n| `core/(root)` | 0 / 188 | 0.0% | callback wiring |\n| `core/mouse/` | 3 / 51 | 5.9% | encoder logic |\n| `ui/` | 3 / 823 | 0.4% | painters + render objects |\n| `(root)` | 0 / 471 | 0.0% | `Terminal` + `TerminalView` |\n| `utils/` | 1 / 251 | 0.4% | circular buffer etc. |\n\n**Strategy — pure logic first, then UI:**\n\n1. **`core/buffer/`** (581 lines, biggest single chunk, all pure Dart) — line/segment/cell models, scrollback math, range queries. Unit tests exercise the data structure directly.\n2. **`core/escape/`** (473 lines, pure state machine) — feed ANSI/VT byte streams in, assert the resulting buffer/cursor state. Reusable corpus from xterm.js / xterm.dart upstream is fair game.\n3. **`core/input/`** (292 lines) — key + keytab tables; assert mapping from logical key to escape bytes per terminal mode.\n4. **`core/mouse/`** (51 lines) — encoder for SGR-1006 / X11 mouse protocols. Small, fast.\n5. **`utils/`** (251 lines) — circular buffer + helpers; trivial unit tests.\n6. **`(root)`** (`terminal.dart`, `terminal_view.dart`, 471 lines) — orchestration; some unit-testable, some needs widget tests.\n7. **`ui/`** (823 lines) — painters and render objects; widget + golden tests. Hardest, most pixel-sensitive.\n\n**Acceptance:**\n- `lib/src/terminal/` line coverage ≥ 95%.\n- All tests fast (\u003c5 s for the area's test files combined; no real PTY shell-out).\n- Helper fixtures live in `test/terminal/_helpers/` if they get reused across files.\n- Per D-66, no `// ignore:` directives or analysis_options excludes added to dodge coverage.\n- `coverage_floor:` in `pubspec.yaml` is bumped in lockstep — every commit that adds covering tests bumps the floor by the measured delta (rounded down to the integer below the new percentage).\n\n**Strategy on commit cadence:**\nLand tests in batches per sub-area (one commit per `core/buffer/`, `core/escape/`, etc.). Each batch:\n- Adds tests under `test/terminal/\u003csub-area\u003e/`.\n- Re-measures coverage, bumps `coverage_floor:` to the new integer (or as high as the gate currently allows).\n- Documents in CHANGELOG under \"Changed\" (test-only changes can also fold into one CHANGELOG entry per area).\n\n**Out of scope:**\n- Refactoring `lib/src/terminal/` for testability — if a piece is genuinely untestable, surface it (don't silence it). Code under `lib/` is owned regardless of upstream attribution; refactor to make it testable rather than carving it out.\n- Branch coverage (D-66 explicitly excludes).\n- Mouse / keyboard *protocol* round-trips against real TUIs (that's T-74 and integration work).\n\n**Cross-references:** D-66 (coverage gate), T-89 (epic), T-90 (gate plumbing).\n", + "changed_at": "2026-05-06 20:50:29" + }, + { + "ticket_id": "T-91", + "field": "status", + "old_value": "backlog", + "new_value": "in_progress", + "changed_at": "2026-05-06 20:50:32" } ] } diff --git a/pubspec.yaml b/pubspec.yaml index 9e367fc9..e4c0d018 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ 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 +coverage_floor: 36 # Project metadata (was project.yaml, folded in per D-056). # version: above is the single source of truth. The Makefile reads diff --git a/test/terminal/buffer/range_test.dart b/test/terminal/buffer/range_test.dart new file mode 100644 index 00000000..4098438e --- /dev/null +++ b/test/terminal/buffer/range_test.dart @@ -0,0 +1,354 @@ +/// Pure-Dart tests for the BufferRange family (CellOffset, BufferSegment, +/// BufferRangeLine, BufferRangeBlock). No Flutter dependency. +library; + +import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart'; +import 'package:clide/src/terminal/src/core/buffer/range.dart'; +import 'package:clide/src/terminal/src/core/buffer/range_block.dart'; +import 'package:clide/src/terminal/src/core/buffer/range_line.dart'; +import 'package:clide/src/terminal/src/core/buffer/segment.dart'; +import 'package:test/test.dart'; + +void main() { + group('CellOffset', () { + test('equality + hashCode for same coordinates', () { + const a = CellOffset(3, 5); + const b = CellOffset(3, 5); + expect(a, b); + expect(a.hashCode, b.hashCode); + expect(a.isEqual(b), isTrue); + expect(identical(a, a), isTrue); // hits the identity branch in == + expect(a == 'CellOffset(3, 5)', isFalse); // hits the type-mismatch branch + }); + + test('inequality for different coordinates', () { + expect(const CellOffset(3, 5), isNot(const CellOffset(4, 5))); + expect(const CellOffset(3, 5).isEqual(const CellOffset(3, 6)), isFalse); + }); + + test('isBefore / isAfter — different rows', () { + const top = CellOffset(9, 1); + const bottom = CellOffset(0, 2); + expect(top.isBefore(bottom), isTrue); + expect(bottom.isAfter(top), isTrue); + expect(top.isAfter(bottom), isFalse); + expect(bottom.isBefore(top), isFalse); + }); + + test('isBefore / isAfter — same row, different columns', () { + const left = CellOffset(2, 4); + const right = CellOffset(7, 4); + expect(left.isBefore(right), isTrue); + expect(right.isAfter(left), isTrue); + expect(left.isAfter(right), isFalse); + expect(right.isBefore(left), isFalse); + }); + + test('isBefore / isAfter — equal positions are neither', () { + const a = CellOffset(2, 2); + const b = CellOffset(2, 2); + expect(a.isBefore(b), isFalse); + expect(a.isAfter(b), isFalse); + }); + + test('isBeforeOrSame / isAfterOrSame include equal positions', () { + const a = CellOffset(3, 3); + const b = CellOffset(3, 3); + const earlier = CellOffset(2, 3); + const later = CellOffset(4, 3); + expect(a.isBeforeOrSame(b), isTrue); + expect(a.isAfterOrSame(b), isTrue); + expect(earlier.isBeforeOrSame(a), isTrue); + expect(later.isAfterOrSame(a), isTrue); + expect(later.isBeforeOrSame(a), isFalse); + expect(earlier.isAfterOrSame(a), isFalse); + }); + + test('isAtSameRow / isAtSameColumn', () { + expect(const CellOffset(2, 5).isAtSameRow(const CellOffset(9, 5)), isTrue); + expect(const CellOffset(2, 5).isAtSameRow(const CellOffset(2, 6)), isFalse); + expect(const CellOffset(4, 1).isAtSameColumn(const CellOffset(4, 9)), isTrue); + expect(const CellOffset(4, 1).isAtSameColumn(const CellOffset(5, 9)), isFalse); + }); + + test('isWithin delegates to BufferRange.contains', () { + final range = BufferRangeLine(const CellOffset(0, 0), const CellOffset(5, 0)); + expect(const CellOffset(3, 0).isWithin(range), isTrue); + expect(const CellOffset(3, 1).isWithin(range), isFalse); + }); + + test('toString shape', () { + expect(const CellOffset(2, 3).toString(), 'CellOffset(2, 3)'); + }); + }); + + group('BufferSegment', () { + test('isWithin — wrong line is never within', () { + const seg = BufferSegment(_dummyRange, 4, 2, 8); + expect(seg.isWithin(const CellOffset(5, 3)), isFalse); + expect(seg.isWithin(const CellOffset(5, 5)), isFalse); + }); + + test('isWithin — bounded segment respects start and end', () { + const seg = BufferSegment(_dummyRange, 4, 2, 8); + expect(seg.isWithin(const CellOffset(2, 4)), isTrue); // at start + expect(seg.isWithin(const CellOffset(5, 4)), isTrue); // middle + expect(seg.isWithin(const CellOffset(8, 4)), isTrue); // at end + expect(seg.isWithin(const CellOffset(1, 4)), isFalse); // before start + expect(seg.isWithin(const CellOffset(9, 4)), isFalse); // after end + }); + + test('isWithin — null start means line beginning', () { + const seg = BufferSegment(_dummyRange, 0, null, 5); + expect(seg.isWithin(const CellOffset(0, 0)), isTrue); + expect(seg.isWithin(const CellOffset(5, 0)), isTrue); + expect(seg.isWithin(const CellOffset(6, 0)), isFalse); + }); + + test('isWithin — null end means line end', () { + const seg = BufferSegment(_dummyRange, 0, 3, null); + expect(seg.isWithin(const CellOffset(3, 0)), isTrue); + expect(seg.isWithin(const CellOffset(999, 0)), isTrue); + expect(seg.isWithin(const CellOffset(2, 0)), isFalse); + }); + + test('equality + hashCode', () { + const a = BufferSegment(_dummyRange, 1, 2, 3); + const b = BufferSegment(_dummyRange, 1, 2, 3); + const c = BufferSegment(_dummyRange, 1, 2, 4); + expect(a, b); + expect(a.hashCode, b.hashCode); + expect(a, isNot(c)); + expect(a == 'segment', isFalse); + expect(identical(a, a), isTrue); + }); + + test('toString shape — bounded and unbounded', () { + expect( + const BufferSegment(_dummyRange, 4, 2, 8).toString(), + 'Segment(4, 2 -> 8)', + ); + expect( + const BufferSegment(_dummyRange, 0, null, null).toString(), + 'Segment(0, start -> end)', + ); + }); + }); + + group('BufferRangeLine', () { + test('collapsed range has equal begin and end', () { + final r = BufferRangeLine.collapsed(const CellOffset(2, 3)); + expect(r.isCollapsed, isTrue); + expect(r.begin, const CellOffset(2, 3)); + expect(r.end, const CellOffset(2, 3)); + }); + + test('isNormalized true when begin <= end', () { + final r = BufferRangeLine(const CellOffset(0, 0), const CellOffset(3, 1)); + expect(r.isNormalized, isTrue); + }); + + test('normalized swaps begin/end when reversed', () { + final r = BufferRangeLine(const CellOffset(3, 1), const CellOffset(0, 0)); + expect(r.isNormalized, isFalse); + final n = r.normalized; + expect(n.begin, const CellOffset(0, 0)); + expect(n.end, const CellOffset(3, 1)); + expect(n.normalized, n); // already normalized → identity + }); + + test('toSegments — single line returns one bounded segment', () { + final r = BufferRangeLine(const CellOffset(2, 0), const CellOffset(5, 0)); + final segs = r.toSegments().toList(); + expect(segs, hasLength(1)); + expect(segs[0].line, 0); + expect(segs[0].start, 2); + expect(segs[0].end, 5); + }); + + test('toSegments — multiple lines: first bounded-left, last bounded-right, middle unbounded', () { + final r = BufferRangeLine(const CellOffset(2, 0), const CellOffset(5, 2)); + final segs = r.toSegments().toList(); + expect(segs.map((s) => s.line), [0, 1, 2]); + expect(segs[0].start, 2); + expect(segs[0].end, isNull); + expect(segs[1].start, isNull); + expect(segs[1].end, isNull); + expect(segs[2].start, isNull); + expect(segs[2].end, 5); + }); + + test('contains positions inside the range, including endpoints', () { + final r = BufferRangeLine(const CellOffset(2, 0), const CellOffset(5, 2)); + expect(r.contains(const CellOffset(2, 0)), isTrue); + expect(r.contains(const CellOffset(0, 1)), isTrue); // mid line + expect(r.contains(const CellOffset(5, 2)), isTrue); + expect(r.contains(const CellOffset(1, 0)), isFalse); + expect(r.contains(const CellOffset(6, 2)), isFalse); + }); + + test('contains works on a denormalized range too (normalizes internally)', () { + final r = BufferRangeLine(const CellOffset(5, 2), const CellOffset(2, 0)); + expect(r.contains(const CellOffset(0, 1)), isTrue); + }); + + test('merge picks earliest begin and latest end', () { + final a = BufferRangeLine(const CellOffset(2, 0), const CellOffset(5, 0)); + final b = BufferRangeLine(const CellOffset(0, 0), const CellOffset(9, 0)); + final m = a.merge(b); + expect(m.begin, const CellOffset(0, 0)); + expect(m.end, const CellOffset(9, 0)); + }); + + test('extend grows the range to cover an outside position', () { + final r = BufferRangeLine(const CellOffset(2, 1), const CellOffset(5, 1)); + final earlier = r.extend(const CellOffset(0, 0)); + expect(earlier.begin, const CellOffset(0, 0)); + expect(earlier.end, const CellOffset(5, 1)); + final later = r.extend(const CellOffset(9, 3)); + expect(later.begin, const CellOffset(2, 1)); + expect(later.end, const CellOffset(9, 3)); + }); + + test('equality + hashCode + toString', () { + final a = BufferRangeLine(const CellOffset(0, 0), const CellOffset(1, 1)); + final b = BufferRangeLine(const CellOffset(0, 0), const CellOffset(1, 1)); + final c = BufferRangeLine(const CellOffset(0, 0), const CellOffset(1, 2)); + expect(a, b); + expect(a.hashCode, b.hashCode); + expect(a, isNot(c)); + expect(a == 'line range', isFalse); + expect(identical(a, a), isTrue); + expect(a.toString(), startsWith('Line Range(')); + }); + }); + + group('BufferRangeBlock', () { + test('isNormalized requires top-left + bottom-right corners', () { + final tlBr = BufferRangeBlock(const CellOffset(2, 1), const CellOffset(5, 4)); + expect(tlBr.isNormalized, isTrue); + + final trBl = BufferRangeBlock(const CellOffset(5, 1), const CellOffset(2, 4)); + expect(trBl.isNormalized, isFalse); + }); + + test('normalized produces top-left / bottom-right corners', () { + final r = BufferRangeBlock(const CellOffset(5, 1), const CellOffset(2, 4)); + final n = r.normalized; + expect(n.begin, const CellOffset(2, 1)); + expect(n.end, const CellOffset(5, 4)); + expect(n.normalized, n); // already normalized branch + }); + + test('toSegments — bounded on every line', () { + final r = BufferRangeBlock(const CellOffset(2, 0), const CellOffset(5, 2)); + final segs = r.toSegments().toList(); + expect(segs.map((s) => s.line), [0, 1, 2]); + for (final s in segs) { + expect(s.start, 2); + expect(s.end, 5); + } + }); + + test('toSegments — denormalized input still yields normalized output', () { + final r = BufferRangeBlock(const CellOffset(5, 2), const CellOffset(2, 0)); + final segs = r.toSegments().toList(); + // After internal normalization the segment range is x:[2..5] over y:[0..2]. + expect(segs.map((s) => s.line), [0, 1, 2]); + for (final s in segs) { + expect(s.start, 2); + expect(s.end, 5); + } + }); + + test('contains — inside, on edge, outside', () { + final r = BufferRangeBlock(const CellOffset(2, 0), const CellOffset(5, 2)); + expect(r.contains(const CellOffset(2, 0)), isTrue); // tl + expect(r.contains(const CellOffset(5, 2)), isTrue); // br + expect(r.contains(const CellOffset(3, 1)), isTrue); // inside + expect(r.contains(const CellOffset(1, 1)), isFalse); // left of block + expect(r.contains(const CellOffset(6, 1)), isFalse); // right of block + expect(r.contains(const CellOffset(3, 3)), isFalse); // below block + }); + + test('contains works on denormalized blocks', () { + final r = BufferRangeBlock(const CellOffset(5, 2), const CellOffset(2, 0)); + expect(r.contains(const CellOffset(3, 1)), isTrue); + }); + + test('extend — position inside is identity', () { + final r = BufferRangeBlock(const CellOffset(2, 0), const CellOffset(5, 2)); + final same = r.extend(const CellOffset(3, 1)); + expect(same, r); + }); + + test('extend — position outside grows the block', () { + final r = BufferRangeBlock(const CellOffset(2, 1), const CellOffset(5, 3)); + final out = r.extend(const CellOffset(0, 0)); + expect(out.begin, const CellOffset(0, 0)); + expect(out.end, const CellOffset(5, 3)); + }); + + test('merge — combines two blocks into the smallest enclosing block', () { + final a = BufferRangeBlock(const CellOffset(0, 0), const CellOffset(2, 2)); + final b = BufferRangeBlock(const CellOffset(5, 5), const CellOffset(7, 7)); + final m = a.merge(b); + expect(m.begin, const CellOffset(0, 0)); + expect(m.end, const CellOffset(7, 7)); + }); + + test('equality + hashCode + toString', () { + final a = BufferRangeBlock(const CellOffset(0, 0), const CellOffset(1, 1)); + final b = BufferRangeBlock(const CellOffset(0, 0), const CellOffset(1, 1)); + final c = BufferRangeBlock(const CellOffset(0, 0), const CellOffset(1, 2)); + expect(a, b); + expect(a.hashCode, b.hashCode); + expect(a, isNot(c)); + expect(a == 'block range', isFalse); + expect(identical(a, a), isTrue); + expect(a.toString(), startsWith('Block Range(')); + }); + }); + + group('BufferRange (abstract base) — collapsed + isCollapsed + isNormalized', () { + test('collapsed range is normalized and collapsed', () { + final r = BufferRangeLine.collapsed(const CellOffset(3, 3)); + expect(r.isCollapsed, isTrue); + expect(r.isNormalized, isTrue); + }); + + test('block collapsed constructor', () { + final r = BufferRangeBlock.collapsed(const CellOffset(2, 2)); + expect(r.isCollapsed, isTrue); + expect(r.begin, r.end); + }); + + test('base ==/hashCode/toString via direct subclass that does not override them', () { + const a = _StubRange(CellOffset(0, 0), CellOffset(1, 1)); + const b = _StubRange(CellOffset(0, 0), CellOffset(1, 1)); + const c = _StubRange(CellOffset(0, 0), CellOffset(2, 2)); + expect(a, b); // same begin + end via base == + expect(a.hashCode, b.hashCode); + expect(a, isNot(c)); + expect(a == 'not a range', isFalse); // type-mismatch branch + expect(identical(a, a), isTrue); + expect(a.toString(), 'Range(CellOffset(0, 0), CellOffset(1, 1))'); + }); + }); +} + +const _dummyRange = _StubRange(CellOffset(0, 0), CellOffset(0, 0)); + +class _StubRange extends BufferRange { + const _StubRange(super.begin, super.end); + @override + BufferRange get normalized => this; + @override + Iterable toSegments() => const []; + @override + bool contains(CellOffset position) => false; + @override + BufferRange merge(BufferRange range) => this; + @override + BufferRange extend(CellOffset position) => this; +}