Skip to content

Build foundational reusable package @cluesmith/codev-artifact-canvas for cross-surface markdown artifact review #945

Description

@amrmelsayed

Problem

Codev's natural-language artifacts — specs, plans, reviews — need an interactive rendering and review surface. Today the only place architects can add review comments to these files is the VSCode source editor (line gutter +, shipped in #857). Two structural gaps follow:

  1. vscode: add review comments from the markdown preview pane (hover-+ per block, no editor mode-switch) #859 discovered that VSCode's built-in markdown preview can't host the required interactions. The platform's previewScripts / markdownItPlugins / previewStyles contribution points are render-only with no back-channel messaging to the extension host. Comment-from-preview can't be added by extending the built-in preview; it requires owning the preview surface (CustomTextEditorProvider + own webview).

  2. The dashboard has no spec/plan/review surface at all. Today's dashboard shows builders, PRs, and backlog items but offers no reading or review-comment affordance for the underlying artifacts. Architects working away from VSCode (meetings, different machines, eventually mobile) currently have no review path.

Both gaps share a root cause: there is no reusable layer for rendering Codev artifacts + adding interactive overlays. Building it once per surface (VSCode webview, dashboard route, future mobile wrapper) would mean three implementations to maintain and three places where the UX could diverge. Building it once as a shared package and adapting per surface is the only path that scales as the family of review-surface features (#858#863) grows.

Proposal

A new package @cluesmith/codev-artifact-canvas at packages/artifact-canvas/ providing:

  • A markdown renderer with source-position metadata (markdown-it + data-line source mapping rule)
  • Interactive overlays for comment authoring (hover-+), navigation (TOC, marker minimap), and state surfaces (reading progress, AC progress, frontmatter badges)
  • Adapter interfaces for all I/O (file read/write, marker mutation, theme tokens) — implementations live in the host (VSCode extension, dashboard React route, future mobile)
  • React-based components for parity with the existing dashboard codebase; embeddable in VSCode webviews and future Capacitor/Tauri wrappers without re-architecture

Why "canvas"

The package is an interactive surface for acting on artifacts (annotate, navigate, mark progress) — not a passive viewer/reader. "Canvas" connotes the bidirectional read+act use case that the package exists to enable. Later phases use an actual HTML <canvas> element for the marker minimap (perf — same primitive VSCode's own editor minimap uses) and potentially for region-selection lassos that produce text-addressable line-range anchors. The name is accurate at v1 (metaphor) and becomes literal at v2 (#863's minimap).

Architectural shape

packages/artifact-canvas/                    NEW
├── src/
│   ├── renderer/                            markdown-it + data-line source mapping
│   ├── overlays/                            hover-`+`, floating TOC, marker minimap
│   ├── widgets/                             reading-progress, AC-progress, frontmatter badges
│   ├── panels/                              review-summary list component (consumed as panel by hosts)
│   ├── adapters/                            INTERFACES ONLY — no implementations
│   │   ├── FileAdapter.ts                   read document content, watch changes
│   │   ├── MarkerAdapter.ts                 read markers, write new markers, attribution
│   │   └── ThemeAdapter.ts                  resolve theme tokens
│   ├── components/                          React components composing the above
│   └── index.ts                             public API surface
├── package.json
└── README.md

packages/vscode/                             EXISTS — host
├── src/
│   └── artifact-canvas-host.ts              NEW (in pir-859 re-plan after this lands):
│                                             CustomTextEditorProvider wrapping the package
│                                             - FileAdapter against vscode.workspace.fs
│                                             - MarkerAdapter against existing plan-review.ts logic
│                                             - ThemeAdapter against `--vscode-*` CSS variables
└── ...

packages/dashboard/                          EXISTS — host (separate future issue)
├── src/
│   └── routes/artifact-canvas/              NEW (separate follow-up):
│                                             React route embedding the package
│                                             - FileAdapter against Tower REST endpoints
│                                             - MarkerAdapter against Tower mutation endpoint
│                                             - ThemeAdapter against dashboard design tokens
└── ...

Text-as-source-of-truth invariant (architectural guardrail)

Every interactive affordance the package surfaces must serialize its output to structured text in the source markdown (or clearly-delimited adjacent text files). This invariant exists because every annotation has two audiences who need to act on it precisely:

  1. Team members reading the same file later (during review, after a context switch)
  2. Claude as builder when spawned to address the feedback (annotations are typically requests for source changes)

Both audiences need text they can read deterministically. Freehand drawings, voice notes, image overlays — any output that requires interpretation rather than precise read — are out of scope for the package's annotation surfaces.

Concrete consequences:

  • Comment overlays serialize to text markers in the source markdown. The package is serialization-agnostic — hosts own the on-disk format via the MarkerAdapter. The existing vscode host convention (vscode: spec/plan review comments — polish pass (placeholders, reviews/ coverage, author identity, panel discoverability) #857) writes <!-- REVIEW(@author): text --> immediately after the targeted block, with the anchor line implicit from the marker's source position; this stays untouched. Future hosts (dashboard, mobile) may choose a different on-disk shape as long as the MarkerAdapter.list/add contract holds.
  • Region-anchored comments (if added later) carry {lineStart, lineEnd} in the in-memory ReviewMarker shape; on-disk serialization is again the host's call (could be a new comment form like <!-- REVIEW(@author, lines=N-M): text -->, could be a side-file entry — package doesn't dictate).
  • AC progress checkboxes mutate - [ ]- [x] in source
  • Canvas rendering choices (e.g. minimap, future lasso) are rendering and input primitives — the underlying data they read from and write to remains text

This filter applies to every dependent issue. The acceptance criteria for the package include a test asserting that no affordance produces output that isn't either (a) source-markdown text mutation, or (b) a clearly-delimited text artifact alongside the source.

Adapter contracts (skeleton — subject to plan-approval refinement)

interface FileAdapter {
  read(uri: string): Promise<string>;
  watch(uri: string, onChange: (content: string) => void): Disposable;
}

interface MarkerAdapter {
  list(uri: string): Promise<ReviewMarker[]>;
  add(uri: string, line: number, text: string, author: string): Promise<void>;
  // Future: addRegion(uri, lineStart, lineEnd, text, author)
  // Future: setCheckbox(uri, line, checked) for AC progress
}

interface ThemeAdapter {
  resolve(token: string): string;        // e.g. resolve("background") → host-specific CSS variable or color
  onChange(handler: () => void): Disposable;
}

interface ReviewMarker {
  author: string;
  line: number;
  // Future: lineRange?: { start: number; end: number };
  text: string;
  raw: string;                            // host-provided opaque round-trip data (e.g. the original `<!-- REVIEW(...) -->` text for the vscode host; could be any other shape for other hosts) — the package treats this as a black box and hands it back unchanged on update/delete
}

Phase decomposition (proposed for SPIR plan)

Phase Scope
specify Lock package boundary; lock adapter interface contracts; lock theming-via-CSS-variables strategy; lock text-as-source-of-truth invariant
plan Phase decomposition: (1) package skeleton + markdown-it renderer + data-line source-mapping, (2) hover-+ overlay + InputBox-equivalent (host-driven via adapter), (3) MarkerAdapter wire-up + REVIEW marker writing
implement Build the package per the phase plan; no host integration yet (pir-859 re-plan does the VSCode adapter; separate future issue does the dashboard adapter)
review Unit tests for renderer (data-line attribution), overlay (hover state, click → adapter.add), adapter interface compliance
verify Confirm via a smoke-test host (could be a minimal VSCode webview or a Vite dev server route) that the package renders + accepts comments end-to-end

Why React

The dashboard is already React. VSCode webviews handle React fine (the dashboard ships as a React app served by Tower; the same bundle approach works inside a webview). Future mobile via Capacitor/Tauri uses React-compatible techniques. Framework-agnostic Web Components were considered but rejected — the dashboard React investment makes React-native components dramatically cheaper to embed in the dashboard route.

What this unlocks

Issue Status What it gets after this lands
#859 (currently on HOLD at plan-approval) Re-plans against this package — vscode adapter implementation becomes thin (CustomTextEditor wraps the package, implements FileAdapter/MarkerAdapter/ThemeAdapter, ~200 LOC)
#860 (review summary panel) Ships as a panel component within the package; hosts mount it as a webview panel (VSCode) or sidebar route (dashboard)
#861 (TOC + per-heading mini-toolbar) Overlay component within the package; same in both surfaces
#862 (reading progress + AC progress + frontmatter badges) Widget components within the package; same in both surfaces
#863 (inline marker rendering + minimap) Rendering layer additions within the package; this is where the actual <canvas> first appears (marker minimap for perf)
Dashboard artifact-canvas route (NEW — separate future issue) Becomes possible — same package, dashboard-side adapter
Future mobile artifact review Becomes possible — same package, mobile-side adapter

Out of scope

  • VSCode host integration — handled by pir-859's re-plan after this lands. This issue ships the package; pir-859 ships the comment-from-preview feature consuming the package.
  • Dashboard host integration — separate future issue. Designed for from day 1 (adapter contracts make it cheap) but not implemented here.
  • Mobile host integration — designed for, not implemented.
  • Freehand sketch annotation — explicitly rejected by the text-as-source-of-truth invariant. Sketches can't be read precisely by team members OR by Claude-as-builder, so they fail the package's annotation contract.
  • Region-lasso anchoring (v3 candidate) — viable in principle (lasso produces text line ranges); not in scope for the package's v1.
  • Diff rendering (vscode: viewSpecDiff / viewPlanDiff — side-by-side diff of current vs prior revision for spec/plan files #858) — uses VSCode's built-in vscode.diff command in vscode; can use a separate library in dashboard. Stays out of the package; reimplementing diff inside the package adds maintenance without benefit.
  • Custom heading numbering, math rendering (KaTeX), Mermaid diagrams, code syntax highlighting — markdown-it core only for v1. Each is a follow-up if/when needed.

Why SPIR (not PIR, not AIR)

  • Package boundary is one-shot — get the adapter interfaces wrong and 6+ dependent issues need rework. The spec phase exists exactly to lock structural decisions before any code commits to them.
  • Cross-package blast radius — affects the codev monorepo's package layout, the dashboard's eventual consumption story, pir-859's re-plan dependency. PIR's plan-approval gate doesn't surface enough; SPIR's spec-approval makes the package boundary a deliberate contract.
  • Real spec decisions beyond what fits in a plan: package vs sub-packages within a folder; adapter sync vs async semantics; whether ThemeAdapter is push-based or pull-based; serialization format for region-anchored markers (forward compat); whether to ship a CSS-modules-based theme system or a pure CSS-variable approach.

PIR or AIR would force these decisions into plan-approval bundled with implementation tradeoffs, which is exactly the situation SPIR is designed to avoid.

Acceptance criteria

  • packages/artifact-canvas/ exists with package.json declaring @cluesmith/codev-artifact-canvas, peer dep on react/react-dom, dep on markdown-it.
  • Renderer produces HTML with data-line attributes on block tokens (paragraphs, headings, list items, code blocks); unit test covers attribution.
  • Comment overlay component renders a hover-+ affordance on rendered blocks; clicking it invokes a callback receiving {line: number}; the callback's text-input flow + write-back lives in the host adapter (not the package).
  • Three adapter interfaces (FileAdapter, MarkerAdapter, ThemeAdapter) are exported; the package has zero direct file-system, fetch, or VSCode API imports.
  • Theming via CSS custom properties; package supplies a default-theme stylesheet that maps to --codev-canvas-* variables; hosts override by providing host-specific values (e.g. --codev-canvas-foreground: var(--vscode-foreground) in VSCode).
  • A smoke-test host (Vite dev server or minimal VSCode webview, implementer's choice) demonstrates end-to-end: load a sample markdown, render, hover, click +, adapter receives the call, marker round-trips.
  • Build produces a CJS+ESM bundle suitable for both VSCode webview consumption (CJS or ESM) and dashboard Vite consumption (ESM).
  • No affordance in the package produces output that isn't either source-markdown text mutation OR a clearly-delimited text artifact alongside the source (text-as-source-of-truth invariant test).
  • README documents the adapter contracts + a host-implementation example.
  • No regression to existing VSCode editor-side review flow (vscode: spec/plan review comments — polish pass (placeholders, reviews/ coverage, author identity, panel discoverability) #857 stays untouched).

Dependencies

  • Blocks: pir-859 (currently on HOLD); will be released to re-plan against this once the package ships.
  • Blocked by: nothing.
  • Coordinates with: existing packages/codev-types, packages/codev-core conventions for monorepo package shape.

Estimated scope

  • Package skeleton + markdown-it + data-line + hover-+ + adapter contracts + smoke-test host: ~600-1000 LOC of new package code, ~100-200 LOC of smoke-test scaffolding, plus tests.
  • Larger than the typical PIR but appropriate for SPIR.

Metadata

Metadata

Assignees

Labels

area/cross-cuttingTouches multiple areas — needs coordinated handling

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions