diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 8b1f8394..ae81e1af 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-05-06T21:51:46Z", + "exported_at": "2026-05-06T22:03:19Z", "decisions": [ { "id": "D-1", @@ -9,7 +9,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-3", @@ -19,7 +19,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-4", @@ -29,7 +29,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-5", @@ -39,7 +39,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-6", @@ -49,7 +49,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-7", @@ -59,7 +59,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-8", @@ -69,7 +69,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-9", @@ -79,7 +79,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-10", @@ -89,7 +89,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-11", @@ -99,7 +99,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-12", @@ -109,7 +109,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-13", @@ -119,7 +119,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-14", @@ -129,7 +129,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-15", @@ -139,7 +139,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-16", @@ -149,7 +149,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-17", @@ -159,7 +159,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-18", @@ -169,7 +169,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-19", @@ -179,7 +179,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-20", @@ -189,7 +189,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-21", @@ -199,7 +199,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-22", @@ -209,7 +209,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-23", @@ -219,7 +219,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-24", @@ -229,7 +229,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-25", @@ -239,7 +239,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-26", @@ -249,7 +249,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-27", @@ -259,7 +259,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-28", @@ -269,7 +269,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-29", @@ -279,7 +279,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-30", @@ -289,7 +289,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-31", @@ -299,7 +299,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-32", @@ -309,7 +309,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-33", @@ -319,7 +319,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-34", @@ -329,7 +329,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-35", @@ -339,7 +339,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-36", @@ -349,7 +349,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-37", @@ -359,7 +359,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-38", @@ -369,7 +369,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-39", @@ -379,7 +379,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-40", @@ -389,7 +389,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-41", @@ -399,7 +399,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-42", @@ -409,7 +409,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-43", @@ -419,7 +419,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-44", @@ -429,7 +429,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-45", @@ -439,7 +439,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-46", @@ -449,7 +449,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-47", @@ -459,7 +459,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-48", @@ -469,7 +469,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-49", @@ -479,7 +479,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-50", @@ -489,7 +489,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-51", @@ -499,7 +499,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-52", @@ -509,7 +509,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-53", @@ -519,7 +519,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-54", @@ -529,7 +529,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-55", @@ -539,7 +539,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-56", @@ -549,7 +549,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-57", @@ -559,7 +559,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-58", @@ -569,7 +569,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-59", @@ -579,7 +579,7 @@ "status": "active", "date": "2026-04-25", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-60", @@ -589,7 +589,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-61", @@ -599,7 +599,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-62", @@ -609,7 +609,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-63", @@ -619,7 +619,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-64", @@ -629,7 +629,7 @@ "status": "active", "date": "2026-05-03", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-65", @@ -639,7 +639,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-66", @@ -649,7 +649,7 @@ "status": "active", "date": "2026-05-06", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "Q-9", @@ -730,7 +730,7 @@ "title": "Lua runtime vendoring", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "Q-17", @@ -802,7 +802,7 @@ "title": "Icon set growth", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "Q-19", @@ -820,7 +820,7 @@ "title": "(withdrawn)", "status": "resolved", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "Q-22", @@ -847,7 +847,7 @@ "title": "Ticket persistence strategy", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "Q-27", @@ -883,7 +883,7 @@ "title": "Two-editor split", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "Q-29", @@ -901,7 +901,7 @@ "title": "Branch picker location", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-2", @@ -929,7 +929,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-3", @@ -939,7 +939,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-4", @@ -949,7 +949,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-5", @@ -959,7 +959,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-6", @@ -969,7 +969,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-7", @@ -979,7 +979,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-8", @@ -989,7 +989,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-9", @@ -999,7 +999,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-10", @@ -1009,7 +1009,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-11", @@ -1019,7 +1019,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-12", @@ -1029,7 +1029,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" } ], "decision_refs": [ diff --git a/lib/src/terminal/src/core/cursor.dart b/lib/src/terminal/src/core/cursor.dart index e06c980b..741f3d67 100644 --- a/lib/src/terminal/src/core/cursor.dart +++ b/lib/src/terminal/src/core/cursor.dart @@ -81,7 +81,7 @@ class CursorStyle { bool get isFaint => (attrs & CellAttr.faint) != 0; - bool get isItalis => (attrs & CellAttr.italic) != 0; + bool get isItalic => (attrs & CellAttr.italic) != 0; bool get isUnderline => (attrs & CellAttr.underline) != 0; diff --git a/lib/src/terminal/src/core/reflow.dart b/lib/src/terminal/src/core/reflow.dart index 05451458..4812e8bb 100644 --- a/lib/src/terminal/src/core/reflow.dart +++ b/lib/src/terminal/src/core/reflow.dart @@ -16,8 +16,6 @@ class _LineBuilder { int get length => _length; - bool get isEmpty => _length == 0; - bool get isNotEmpty => _length != 0; /// Adds a range of cells from [src] to the builder. Anchors within the range diff --git a/pubspec.yaml b/pubspec.yaml index 2bd5f6da..1750723c 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: 52 +coverage_floor: 54 # 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/core/core_test.dart b/test/terminal/core/core_test.dart new file mode 100644 index 00000000..f67c9b92 --- /dev/null +++ b/test/terminal/core/core_test.dart @@ -0,0 +1,408 @@ +/// Pure-Dart tests for the small `lib/src/terminal/src/core/*.dart` +/// files — cell, cursor, charset, tabs, reflow. +library; + +import 'package:clide/src/terminal/src/core/buffer/line.dart'; +import 'package:clide/src/terminal/src/core/cell.dart'; +import 'package:clide/src/terminal/src/core/charset.dart'; +import 'package:clide/src/terminal/src/core/cursor.dart'; +import 'package:clide/src/terminal/src/core/reflow.dart'; +import 'package:clide/src/terminal/src/core/tabs.dart'; +import 'package:clide/src/terminal/src/utils/circular_buffer.dart'; +import 'package:test/test.dart'; + +void main() { + group('CellData', () { + test('explicit constructor stores all four channels', () { + final c = CellData(foreground: 1, background: 2, flags: 4, content: 8); + expect(c.foreground, 1); + expect(c.background, 2); + expect(c.flags, 4); + expect(c.content, 8); + }); + + test('CellData.empty factory zeroes everything', () { + final c = CellData.empty(); + expect(c.foreground, 0); + expect(c.background, 0); + expect(c.flags, 0); + expect(c.content, 0); + }); + + test('getHash combines all four channels (different inputs → different hash)', () { + final a = CellData(foreground: 1, background: 2, flags: 3, content: 4); + final b = CellData(foreground: 1, background: 2, flags: 3, content: 4); + expect(a.getHash(), b.getHash()); + final c = CellData(foreground: 1, background: 2, flags: 3, content: 5); + expect(a.getHash(), isNot(c.getHash())); + }); + + test('toString shape is the documented debug format', () { + final c = CellData(foreground: 1, background: 2, flags: 4, content: 8); + expect( + c.toString(), + 'CellData{foreground: 1, background: 2, flags: 4, content: 8}', + ); + }); + }); + + group('CursorStyle', () { + test('default constructor zeroes all three fields', () { + final c = CursorStyle(); + expect(c.foreground, 0); + expect(c.background, 0); + expect(c.attrs, 0); + }); + + test('every set / unset attr toggles the right CellAttr bit', () { + final c = CursorStyle(); + // Pair each setter with its corresponding getter and unsetter. + final cases = <(void Function(), void Function(), bool Function())>[ + (c.setBold, c.unsetBold, () => c.isBold), + (c.setFaint, c.unsetFaint, () => c.isFaint), + (c.setItalic, c.unsetItalic, () => c.isItalic), + (c.setUnderline, c.unsetUnderline, () => c.isUnderline), + (c.setBlink, c.unsetBlink, () => c.isBlink), + (c.setInverse, c.unsetInverse, () => c.isInverse), + (c.setInvisible, c.unsetInvisible, () => c.isInvisible), + ]; + for (final (set, unset, getter) in cases) { + c.attrs = 0; + expect(getter(), isFalse); + set(); + expect(getter(), isTrue); + unset(); + expect(getter(), isFalse); + } + // Strikethrough has no getter — exercise its set / unset directly. + c.attrs = 0; + c.setStrikethrough(); + expect(c.attrs & CellAttr.strikethrough, isNot(0)); + c.unsetStrikethrough(); + expect(c.attrs & CellAttr.strikethrough, 0); + }); + + test('foreground 16 / 256 / RGB encode the right CellColor type bits', () { + final c = CursorStyle(); + c.setForegroundColor16(3); + expect(c.foreground, 3 | CellColor.named); + c.setForegroundColor256(200); + expect(c.foreground, 200 | CellColor.palette); + c.setForegroundColorRgb(0x10, 0x20, 0x30); + expect(c.foreground, 0x10 << 16 | 0x20 << 8 | 0x30 | CellColor.rgb); + c.resetForegroundColor(); + expect(c.foreground, 0); + }); + + test('background 16 / 256 / RGB encode the right CellColor type bits', () { + final c = CursorStyle(); + c.setBackgroundColor16(5); + expect(c.background, 5 | CellColor.named); + c.setBackgroundColor256(123); + expect(c.background, 123 | CellColor.palette); + c.setBackgroundColorRgb(1, 2, 3); + expect(c.background, 1 << 16 | 2 << 8 | 3 | CellColor.rgb); + c.resetBackgroundColor(); + expect(c.background, 0); + }); + + test('reset wipes all three fields', () { + final c = CursorStyle() + ..setBold() + ..setForegroundColor16(2) + ..setBackgroundColor256(99); + c.reset(); + expect(c.foreground, 0); + expect(c.background, 0); + expect(c.attrs, 0); + }); + + test('CursorStyle.empty exposes a default-zero singleton', () { + // Don't mutate this — it's a shared singleton. + expect(CursorStyle.empty.foreground, 0); + expect(CursorStyle.empty.background, 0); + expect(CursorStyle.empty.attrs, 0); + }); + }); + + group('CursorPosition', () { + test('exposes mutable x / y fields', () { + final p = CursorPosition(3, 5); + expect(p.x, 3); + expect(p.y, 5); + p.x = 7; + p.y = 9; + expect(p.x, 7); + expect(p.y, 9); + }); + }); + + group('Charset', () { + test('asciiTranslator is identity', () { + expect(asciiTranslator(0x41), 0x41); + expect(asciiTranslator(0x7e), 0x7e); + }); + + test('decSpecGraphicsTranslator maps the documented codepoints', () { + // 0x6a → BOX DRAWINGS LIGHT UP AND LEFT (0x2518) + expect(decSpecGraphicsTranslator(0x6a), 0x2518); + // 0x71 → BOX DRAWINGS LIGHT HORIZONTAL (0x2500) + expect(decSpecGraphicsTranslator(0x71), 0x2500); + }); + + test('decSpecGraphicsTranslator passes unmapped low-codepoints through', () { + expect(decSpecGraphicsTranslator(0x41), 0x41); // 'A' is unmapped + }); + + test('decSpecGraphicsTranslator passes high codepoints through', () { + // The spec only maps 0x5f..0x7e; anything ≥ 127 is identity. + expect(decSpecGraphicsTranslator(0x100), 0x100); + expect(decSpecGraphicsTranslator(0x4E2D), 0x4E2D); + }); + + test('translate defaults to ascii when no charset is designated', () { + final c = Charset(); + expect(c.translate(0x6a), 0x6a); + }); + + test('designate + use switch the active translator', () { + final c = Charset(); + // Designate the DEC special-graphics charset at slot 0… + c.designate(0, '0'.codeUnitAt(0)); + // …and select slot 0 as the active charset. + c.use(0); + expect(c.translate(0x6a), 0x2518); // now maps via DEC graphics + // Switch back to a slot with no designated charset → ascii fallback. + c.use(1); + expect(c.translate(0x6a), 0x6a); + }); + + test('designate ignores unknown charset names without changing state', () { + final c = Charset(); + c.designate(0, 0xFFFF); // not in the _charsets table + c.use(0); + expect(c.translate(0x6a), 0x6a); // still ascii + }); + + test('save / restore round-trips both the map and the active index', () { + final c = Charset(); + c.designate(0, '0'.codeUnitAt(0)); + c.use(0); + c.save(); + // Mutate after save… + c.designate(0, 'B'.codeUnitAt(0)); // ascii in slot 0 — translate is identity + c.use(1); + expect(c.translate(0x6a), 0x6a); + // …restore should reinstate the saved DEC-graphics binding. + c.restore(); + expect(c.translate(0x6a), 0x2518); + }); + }); + + group('TabStops', () { + test('default state has tab stops every 8 columns', () { + final t = TabStops(); + for (var i = 0; i < 100; i++) { + expect(t.isSetAt(i), i % 8 == 0, reason: 'col $i'); + } + }); + + test('find — returns first set stop in [start, end)', () { + final t = TabStops(); + // Default: 0, 8, 16, 24, ... + expect(t.find(1, 20), 8); + expect(t.find(8, 20), 8); + expect(t.find(9, 16), isNull); // no stops in [9, 16) + expect(t.find(0, 1), 0); + }); + + test('find returns null when start >= end', () { + final t = TabStops(); + expect(t.find(10, 10), isNull); + expect(t.find(20, 5), isNull); + }); + + test('find clamps end to the underlying array length', () { + final t = TabStops(); + // 99999 > _kMaxColumns; should still return a valid stop without + // walking out of bounds. + expect(t.find(0, 99999), 0); + }); + + test('setAt + clearAt toggle individual columns', () { + final t = TabStops(); + t.clearAt(8); + expect(t.isSetAt(8), isFalse); + t.setAt(3); + expect(t.isSetAt(3), isTrue); + t.clearAt(3); + expect(t.isSetAt(3), isFalse); + }); + + test('clearAll wipes every stop without restoring the default grid', () { + final t = TabStops(); + t.clearAll(); + for (var i = 0; i < 100; i++) { + expect(t.isSetAt(i), isFalse, reason: 'col $i'); + } + }); + + test('reset clears then re-installs the default 8-column grid', () { + final t = TabStops(); + t.setAt(3); + t.clearAt(8); + t.reset(); + // 3 should no longer be set; 8 should be set again. + expect(t.isSetAt(3), isFalse); + expect(t.isSetAt(8), isTrue); + }); + }); + + group('reflow', () { + BufferLine line(int width, [String? text]) { + final l = BufferLine(width); + if (text != null) { + for (var i = 0; i < text.length; i++) { + l.setCodePoint(i, text.codeUnitAt(i)); + } + } + return l; + } + + IndexAwareCircularBuffer ring(List ls) { + final r = IndexAwareCircularBuffer(ls.length + 4); + for (final l in ls) { + r.push(l); + } + return r; + } + + test('empty input produces empty output', () { + final out = reflow(ring([]), 80, 80); + expect(out, isEmpty); + }); + + test('single empty line is reused as-is', () { + final l = line(8); + final out = reflow(ring([l]), 8, 8); + expect(out.length, 1); + expect(identical(out.first, l), isTrue); + }); + + test('grow width keeps lines and resizes them to the new width', () { + final out = reflow(ring([line(4, 'abcd')]), 4, 8); + expect(out, hasLength(1)); + expect(out.first.length, 8); + expect(out.first.getCodePoint(0), 'a'.codeUnitAt(0)); + expect(out.first.getCodePoint(3), 'd'.codeUnitAt(0)); + }); + + test('shrink width splits a single full line into wrapped pieces', () { + // 8 chars on a width-8 line, reflowed to width 4 → two width-4 lines. + final out = reflow(ring([line(8, 'abcdefgh')]), 8, 4); + expect(out, hasLength(2)); + expect(out[0].getCodePoint(0), 'a'.codeUnitAt(0)); + expect(out[0].isWrapped, isFalse); // first line of a logical run + expect(out[1].getCodePoint(0), 'e'.codeUnitAt(0)); + expect(out[1].isWrapped, isTrue); + }); + + test('continues a wrapped run across input lines and re-emits wrapped output', () { + // Two width-4 lines forming a single logical line "abcdefgh", reflowed + // back to width 8 → a single line of 8 chars. + final a = line(4, 'abcd'); + final b = line(4, 'efgh')..isWrapped = true; + final out = reflow(ring([a, b]), 4, 8); + expect(out, hasLength(1)); + expect(out.first.getCodePoint(0), 'a'.codeUnitAt(0)); + expect(out.first.getCodePoint(7), 'h'.codeUnitAt(0)); + }); + + test('shrink across a wide char does not split it across two output lines', () { + // Layout on a width-6 input: 'ab中cd' — the wide char straddles + // columns 2..3. Reflow to width 4 → first line is 'ab' + space (the + // wide char wouldn't fit), second line carries the wide char + 'cd'. + final l = BufferLine(6); + l.setCodePoint(0, 'a'.codeUnitAt(0)); + l.setCodePoint(1, 'b'.codeUnitAt(0)); + l.setCodePoint(2, '中'.runes.first); // width 2 → occupies 2..3 + l.setCodePoint(4, 'c'.codeUnitAt(0)); + l.setCodePoint(5, 'd'.codeUnitAt(0)); + + final out = reflow(ring([l]), 6, 4); + expect(out.length, greaterThanOrEqualTo(2)); + // First output line should not have started its last cell on the wide + // char's first half — i.e. cell index 3 should be empty (width-clamp + // branch fired during the initial fill). + expect(out[0].getCodePoint(3), 0); + }); + + test('shrink with a wide char at the new boundary takes the from=newWidth-1 path', () { + // The branch at reflow.dart:88 only fires when the cell at the new + // boundary (newWidth-1) is itself the first half of a wide char. + final l = BufferLine(6); + l.setCodePoint(0, 'a'.codeUnitAt(0)); + l.setCodePoint(1, 'b'.codeUnitAt(0)); + l.setCodePoint(2, 'c'.codeUnitAt(0)); + l.setCodePoint(3, '中'.runes.first); // wide → straddles 3..4 + l.setCodePoint(5, 'd'.codeUnitAt(0)); + + // newWidth=4 → boundary cell is 3, which is the wide char's first half. + final out = reflow(ring([l]), 6, 4); + expect(out.length, greaterThanOrEqualTo(2)); + // The wide char should not survive as a half-cell on the first line. + expect(out[0].getCodePoint(3), 0); + }); + + test('anchors on the source line tail (past trimmedLength) get reparented', () { + // The post-loop branch in `_addPart` reparents anchors whose x sits + // beyond the trimmed-content range — the empty tail of an over-wide + // source line. The anchor moves off the source onto whichever + // builder line was active at the time, regardless of whether that + // builder line ends up emitted — so the only contract we can rely + // on is that the anchor no longer points to the source. + final src = line(12, 'abcdefgh'); // width 12, content 8 + final tail = src.createAnchor(10); // past trimmedLength (8) + reflow(ring([src]), 12, 4); + expect(tail.line, isNot(equals(src))); + }); + + test('inner wide-char clamp during _addPart leaves the wide cell to the next iteration', () { + // The clamp at reflow.dart:124 fires when the iteration is about to + // fill the builder AND the last cell would be the first half of a + // wide char — _addPart pushes that copy to the next iteration. + final src = BufferLine(9); + for (var i = 0; i < 7; i++) { + src.setCodePoint(i, 'a'.codeUnitAt(0) + i); + } + // Set wide at index 7 (straddles 7..8). + src.setCodePoint(7, '中'.runes.first); + final out = reflow(ring([src]), 9, 4); + expect(out.length, greaterThanOrEqualTo(2)); + }); + + test('anchors on the source line get reparented to the reflowed output', () { + // CellAnchor.attached delegates to its owning BufferLine.attached + // (which itself reflects whether the line sits in an + // IndexAwareCircularBuffer). Reflow returns a plain List, so the + // output lines aren't "attached" — but anchors should still point + // to one of the lines in the result. + final src = line(8, 'abcdefgh'); + final anchorAtStart = src.createAnchor(0); + final anchorMid = src.createAnchor(5); + final out = reflow(ring([src]), 8, 4); + expect(out, hasLength(2)); + + // anchorAtStart was at x=0 on the original line; reflow's first + // step is `_lines.add(line)`, so the original line is the first + // output. The anchor should still point to that same instance. + expect(out.contains(anchorAtStart.line), isTrue); + + // anchorMid was at x=5; the [4..8) tail is split out via _addPart + // and lands on the second output line, with the anchor reparented + // there. + expect(out.contains(anchorMid.line), isTrue); + expect(anchorMid.line, isNot(equals(anchorAtStart.line))); + }); + }); +}