diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index b5b106bb..f8e16e9d 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -8827,3 +8827,28 @@ RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG rendere PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on. EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.', NULL, '2026-06-28 20:25:50', '2026-06-28 20:25:50.683', '2026-06-28 20:25:50.683', NULL, 'e8481d8539f90b8d5fd2dddb1932e2a0', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'description', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError. + +SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set. + +MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context. + +RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md. + +PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on. + +EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError. + +SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set. + +MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context. + +RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md. + +PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on. + +EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope. + +COMPLETE (2026-06-28): per-object overlay done — captions (data-label/data-description) + lightbox (data-lightbox tap-to-zoom), positioned via the shared svgViewportFit so they track the painted SVG. Full T-318 scope built + tested: document envelope (draw_doc), template dispatch (draw_dispatch), DrawingCard widget, the draw CLI command (draw_commands), the extension wire (DrawingMessage + subscriber + renderer + boot registration), and the per-object overlay. ~40 tests across dart + flutter test. All acceptance met (raw SVG draws; a template dispatches; objects render captions; unknown template/primitive → userError). Templates d2/icon/compare/image are separate children under T-317; T-494 (d2) is next, gated on T-495.', NULL, '2026-06-28 20:40:54', '2026-06-28 20:40:54.751', '2026-06-28 20:40:54.751', NULL, '3c1694be81a3d05e22c8c5a2221e99fd', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'status', 'in_progress', 'done', NULL, '2026-06-28 20:40:54', '2026-06-28 20:40:54.785', '2026-06-28 20:40:54.785', NULL, '389acc78ce89d7225e268e20f3822fee', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYPJ6FTEPP4JK3D7D01ZSMM', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 21:00:18', '2026-06-28 21:00:18.364', '2026-06-28 21:00:18.364', NULL, '00fa58dcedc18f6ce827b5a0ef6ad928', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 579dabba..1bb1abbd 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -10614,3 +10614,36 @@ RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG rendere PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on. EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:40', '2026-06-28 20:25:50.683', NULL, 'a5a8e71c0e31375e52004b37b181b484', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'story', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card core: canvas engine + JSON schema + template dispatch', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError. + +SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set. + +MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context. + +RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md. + +PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on. + +EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope. + +COMPLETE (2026-06-28): per-object overlay done — captions (data-label/data-description) + lightbox (data-lightbox tap-to-zoom), positioned via the shared svgViewportFit so they track the painted SVG. Full T-318 scope built + tested: document envelope (draw_doc), template dispatch (draw_dispatch), DrawingCard widget, the draw CLI command (draw_commands), the extension wire (DrawingMessage + subscriber + renderer + boot registration), and the per-object overlay. ~40 tests across dart + flutter test. All acceptance met (raw SVG draws; a template dispatches; objects render captions; unknown template/primitive → userError). Templates d2/icon/compare/image are separate children under T-317; T-494 (d2) is next, gated on T-495.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:40', '2026-06-28 20:40:54.751', NULL, 'aad56f10912aec6041ae4e4a749196cb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'story', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card core: canvas engine + JSON schema + template dispatch', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError. + +SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set. + +MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context. + +RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md. + +PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on. + +EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope. + +COMPLETE (2026-06-28): per-object overlay done — captions (data-label/data-description) + lightbox (data-lightbox tap-to-zoom), positioned via the shared svgViewportFit so they track the painted SVG. Full T-318 scope built + tested: document envelope (draw_doc), template dispatch (draw_dispatch), DrawingCard widget, the draw CLI command (draw_commands), the extension wire (DrawingMessage + subscriber + renderer + boot registration), and the per-object overlay. ~40 tests across dart + flutter test. All acceptance met (raw SVG draws; a template dispatches; objects render captions; unknown template/primitive → userError). Templates d2/icon/compare/image are separate children under T-317; T-494 (d2) is next, gated on T-495.', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:40', '2026-06-28 20:40:54.785', NULL, '61c3c704be2b8282530d1459ec1655f6', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYPJ6FTEPP4JK3D7D01ZSMM', 'task', NULL, 'Supporter-binary path resolution: explicit user-scope overrides + first-run auto-detect', 'Implement D-104. clide resolves external supporter binaries (claude, d2, future supporter tools) via an explicit user-scope ''tools:'' map (name → absolute path), consulted FIRST. + +Resolution order: (1) explicit settings path → use it; honest error + fallback-with-warning if missing/non-exec. (2) bundled/pinned (pql D-58, git D-59). (3) login-shell PATH (T-439). (4) bare PATH. + +First run: auto-detect each tool ONCE — probe the login-shell PATH + well-known dirs (~/.local/bin, /usr/local/bin, /opt/homebrew, /home/linuxbrew/.linuxbrew/bin) — and WRITE the discovered absolute paths into the map (pinned, not re-probed each launch). Provide a re-detect action. + +Storage: user-scope SettingsStore keyed by machine (D-93) — never committed. Surface in the Config tab (T-414) + flag unresolved tools in the Problems panel (echoes D-58''s soft floor). Generalizes D-58''s CLIDE_PQL_BIN override to all supporter tools. First consumer: the d2 template (T-494).', 'in_progress', 'medium', NULL, NULL, 'D-104', '2026-06-28 17:50:02.366', '2026-06-28 21:00:18.364', NULL, 'de1818996b3226c6df2516d1aff9cfad', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/lib/src/env/supporter_binaries.dart b/lib/src/env/supporter_binaries.dart new file mode 100644 index 00000000..7909ca70 --- /dev/null +++ b/lib/src/env/supporter_binaries.dart @@ -0,0 +1,93 @@ +/// Resolves external supporter binaries (claude, d2, …) to absolute paths +/// (T-495, D-104). +/// +/// Order: an explicit user-scope **override** map is consulted FIRST; then the +/// login-shell / process search PATH ([resolvedToolPath]); then the well-known +/// user/local bin dirs — including **Homebrew-on-Linux** (`/home/linuxbrew/...`), +/// which the standard PATH expansion omits and a login-shell probe misses when +/// `brew shellenv` lives only in `~/.bashrc`. That omission is the gap D-104 +/// fixes. +/// +/// [detect] probes those locations to seed the override map on first run (or a +/// re-detect), so resolution is **pinned** thereafter — deterministic, not a +/// per-launch heuristic. A pin that no longer points at a file is reported by +/// [isStalePin] so the caller can warn + re-detect rather than silently fail. +/// +/// Pure Dart (injected `exists` / `searchPath`), Flutter-free; runs under +/// `dart test`. +library; + +import 'dart:io'; + +import 'shell_env.dart' show resolvedToolPath; + +/// Whether [absolutePath] points at an existing file (symlinks followed). +typedef PathExists = bool Function(String absolutePath); + +class SupporterBinaries { + SupporterBinaries({Map overrides = const {}, PathExists? exists, String Function()? searchPath, String? home, bool isWindows = false}) + : _overrides = overrides, + _exists = exists ?? _fileExists, + _searchPath = searchPath ?? resolvedToolPath, + _home = home ?? Platform.environment['HOME'], + _sep = isWindows ? ';' : ':'; + + final Map _overrides; + final PathExists _exists; + final String Function() _searchPath; + final String? _home; + final String _sep; + + /// Resolve [name] to an absolute path, or `null`. Order: explicit override (if + /// it still exists) → search PATH → well-known dirs. + String? resolve(String name) { + final pinned = _overrides[name]; + if (pinned != null && pinned.isNotEmpty && _exists(pinned)) return pinned; + for (final dir in _searchDirs()) { + final p = '$dir/$name'; + if (_exists(p)) return p; + } + return null; + } + + /// True when [name]'s override is set but no longer points at a file — a stale + /// pin (the tool moved on an upgrade). The caller warns and can re-detect. + bool isStalePin(String name) { + final p = _overrides[name]; + return p != null && p.isNotEmpty && !_exists(p); + } + + /// Probe for [names], returning name→absolute-path for those found — the seed + /// for the override map on first run / re-detect. + Map detect(Iterable names) { + final dirs = _searchDirs().toList(); + final out = {}; + for (final name in names) { + for (final dir in dirs) { + final p = '$dir/$name'; + if (_exists(p)) { + out[name] = p; + break; + } + } + } + return out; + } + + Iterable _searchDirs() sync* { + final seen = {}; + for (final dir in _searchPath().split(_sep)) { + if (dir.isNotEmpty && seen.add(dir)) yield dir; + } + for (final dir in _wellKnownDirs()) { + if (seen.add(dir)) yield dir; + } + } + + List _wellKnownDirs() { + final h = _home ?? ''; + return [if (h.isNotEmpty) '$h/.local/bin', '/usr/local/bin', '/opt/homebrew/bin', '/opt/homebrew/sbin', '/home/linuxbrew/.linuxbrew/bin']; + } +} + +bool _fileExists(String p) => File(p).existsSync(); diff --git a/test/src/env/supporter_binaries_test.dart b/test/src/env/supporter_binaries_test.dart new file mode 100644 index 00000000..9faece8d --- /dev/null +++ b/test/src/env/supporter_binaries_test.dart @@ -0,0 +1,62 @@ +import 'package:clide/src/env/supporter_binaries.dart'; +import 'package:test/test.dart'; + +void main() { + SupporterBinaries make({Map overrides = const {}, Set files = const {}, String path = '/usr/bin:/bin', String home = '/home/u'}) => + SupporterBinaries(overrides: overrides, exists: files.contains, searchPath: () => path, home: home); + + group('resolve', () { + test('an explicit override wins', () { + final r = make(overrides: {'d2': '/opt/d2'}, files: {'/opt/d2', '/usr/bin/d2'}); + expect(r.resolve('d2'), '/opt/d2'); + }); + + test('a stale override falls through to the PATH', () { + final r = make(overrides: {'d2': '/gone/d2'}, files: {'/usr/bin/d2'}); + expect(r.resolve('d2'), '/usr/bin/d2'); + }); + + test('searches the PATH when there is no override', () { + final r = make(files: {'/bin/d2'}); + expect(r.resolve('d2'), '/bin/d2'); + }); + + test('finds a tool in linuxbrew via the well-known dirs (the D-104 gap)', () { + final r = make(files: {'/home/linuxbrew/.linuxbrew/bin/d2'}); + expect(r.resolve('d2'), '/home/linuxbrew/.linuxbrew/bin/d2'); + }); + + test('finds a tool in ~/.local/bin', () { + final r = make(files: {'/home/u/.local/bin/pql'}, path: '/usr/bin'); + expect(r.resolve('pql'), '/home/u/.local/bin/pql'); + }); + + test('returns null when unresolved', () { + expect(make().resolve('nope'), isNull); + }); + }); + + group('isStalePin', () { + test('true when the override points at a missing file', () { + expect(make(overrides: {'d2': '/gone'}).isStalePin('d2'), isTrue); + }); + test('false when the override exists', () { + expect(make(overrides: {'d2': '/ok'}, files: {'/ok'}).isStalePin('d2'), isFalse); + }); + test('false when there is no override', () { + expect(make().isStalePin('d2'), isFalse); + }); + }); + + group('detect', () { + test('finds tools across the PATH and well-known dirs', () { + final r = make(files: {'/usr/bin/claude', '/home/linuxbrew/.linuxbrew/bin/d2'}, path: '/usr/bin'); + expect(r.detect(['claude', 'd2', 'missing']), {'claude': '/usr/bin/claude', 'd2': '/home/linuxbrew/.linuxbrew/bin/d2'}); + }); + + test('prefers the earliest matching directory', () { + final r = make(files: {'/usr/bin/d2', '/home/linuxbrew/.linuxbrew/bin/d2'}, path: '/usr/bin'); + expect(r.detect(['d2'])['d2'], '/usr/bin/d2'); + }); + }); +}