Skip to content

[Spec 945] Foundational reusable package @cluesmith/codev-artifact-canvas#1027

Open
amrmelsayed wants to merge 66 commits into
mainfrom
builder/spir-945
Open

[Spec 945] Foundational reusable package @cluesmith/codev-artifact-canvas#1027
amrmelsayed wants to merge 66 commits into
mainfrom
builder/spir-945

Conversation

@amrmelsayed

@amrmelsayed amrmelsayed commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Builds @cluesmith/codev-artifact-canvas (packages/artifact-canvas/) — a host-agnostic React library for rendering and reviewing Codev markdown artifacts (specs/plans/reviews) across surfaces (VSCode, dashboard, future mobile). Delivered over 4 SPIR implement phases.

This package ships standalone — no host integration (that is #859 and follow-ups). Hosts wire it up by implementing three small adapter interfaces.

Closes #945.

What's included

  • Dual-format build (CJS + ESM + .d.ts) via tsup; React externalized as a peer (^18 || ^19).
  • Locked adapter interfaces (interfaces only; implementations live in hosts): FileAdapter (read + watch), MarkerAdapter (list/add), ThemeAdapter (JS-side, off the v1 render path).
  • Markdown renderer: markdown-it with html:false + DOMPurify sanitization, 0-based data-line attribution on block tokens.
  • ArtifactCanvas component: intent-only comment overlay (emits onAddComment(line), never writes markers itself), minimal v1 marker rendering, adapter-driven data flow with request-versioning, warn-once out-of-range handling, a no-watcher refreshKey refresh path, and overlay-anchor reconciliation against the reloaded DOM. Keyboard-accessible (focusable blocks, Enter/Space, ARIA).
  • CSS-variable theming ("Model A"): 8 --codev-canvas-* tokens + a ./default-theme.css export.
  • Smoke-test host: an automated end-to-end round-trip test (proves the review loop goes through text, mouse + keyboard), a examples/ Vite dev page, and a comprehensive README.md.
  • Repo wiring: root build graph, CI step (.github/workflows/test.yml), lockstep bump (scripts/bump-all.sh), and release-protocol enumeration. Not independently npm-published in v1 — consumed via workspace:* and bundled by hosts.

Tests

34 passing in the package (5 files: data-line, sanitization, artifact-canvas, import-boundary, end-to-end). Build, type-check, and a headless examples/ bundle all verified. All 6 CI jobs green.

Notes for the reviewer

  • Deviations from plan and the full consultation history are documented in the review.
  • Flaky tests: 4 pre-existing @cluesmith/codev tests (unrelated to this package) are .skip-quarantined on this branch per the builder protocol's flaky-test rule (architect-authorized). See the review's Flaky Tests section — un-skip tracked as a follow-up.
  • Branch was rebased onto current main (then at vscode: Forward file/symbol/hunk/selection references into the builder PTY (#789) #1023). A CI-only e2e race (an unconditional activeLine reset that could clobber a hover when the initial async load's effect landed late under CI's slower scheduling) was fixed by validating the overlay anchor against the reloaded DOM instead; covered by a deterministic regression test.

Artifacts

  • Spec: codev/specs/945-build-foundational-reusable-pa.md
  • Plan: codev/plans/945-build-foundational-reusable-pa.md
  • Review: codev/reviews/945-build-foundational-reusable-pa.md

…AC contradiction

- Add D7 + Security Considerations: markdown-it html:false + DOMPurify sanitize
  before render; dompurify dep; new AC + Test Scenario 8; cites #48 precedent.
- Resolve D6-vs-AC/Scenario-3 contradiction: package is intent-only (emits
  onAddComment(line)); host owns input + calls MarkerAdapter.add. Fixed D3, AC
  item, Test Scenario 3, and interface annotation to match.
- Fold cheap Claude items: 0-based ReviewMarker.line; blockquotes/tables in AC;
  Disposable-teardown Test Scenario 9.
- Correct Consultation Log + thread to reflect actual verdicts (Gemini+Codex
  REQUEST_CHANGES, Claude APPROVE) instead of the earlier inaccurate summary.
…esolve ThemeAdapter/CSS

- D2: separate async (read + MarkerAdapter) from sync (watch/onChange return
  Disposable immediately) — fixes the iter-2 Codex async/sync contradiction.
- Add ArtifactCanvasProps to the locked interface block: canonical
  onAddComment(line: number) intent seam (+ adapters, uri, onError). D6/AC/Test
  reference it. Exported from public API.
- D4 Model A: CSS variables are the sole v1 theming mechanism + enumerated token
  vocabulary; ThemeAdapter.resolve()/onChange are JS-side (#863 canvas), off the
  v1 render path. Test 4 reframed.
- D6: component auto-re-calls MarkerAdapter.list on watch (host doesn't retrigger).
- New AC: hover-+ is keyboard-accessible (Enter/Space + SR label).
- Consultation Log: honest iter-2 verdicts + iter-3 changes.
…d e2e test

- Phase 1: add repo build/test/release wiring deliverables (root package.json
  build+test, scripts/bump-all.sh) + acceptance criterion — fixes Codex #1
  (package would otherwise be an orphan, not in the build/version flow).
- Phase 4: lock an automated end-to-end round-trip test at
  src/__tests__/end-to-end.test.tsx (Vitest+TL+stub fixtures) as the primary
  contract proof; examples/ Vite page is now a dev aid — fixes Codex #2.
- Fold Claude notes: jsdom test env, tsup-rationale README note, optional
  React-18 smoke, P3-density escape hatch. Add plan Consultation Log.
…nder path + e2e round-trips through text

- P3: themeAdapter accepted as a prop but NOT subscribed/used for v1 render
  (CSS-var theming only, D4 Model A); only FileAdapter.watch subscribed;
  resolve/onChange exercised solely by the scenario-4 contract test. Fixes
  Codex iter-2 #1.
- P4: e2e test now requires stub MarkerAdapter.add to serialize a positional
  REVIEW marker into the markdown string; read/watch/list derive from that
  text (not in-memory). Fixes Codex iter-2 #2 (text round-trip).
- Fold Claude nits: accurate root build-script form + insertion point; root
  test convention (per-package + CI); @types/markdown-it/@types/dompurify +
  vite devDeps; tsconfig base; scenario-6 invariant echo.
… contradiction + name CI/release wiring

- Resolve P1 test-wiring self-contradiction: root build includes the package;
  root test is NOT extended; package's own test runs in CI. AC reworded to match.
- Name .github/workflows/test.yml explicitly (dedicated artifact-canvas step) —
  the repo's actual CI mechanism, not the root test script.
- Add release-protocol update deliverable + explicit publish decision (not
  independently npm-published in v1; bundled by hosts via workspace:*).
- Fold Claude tsconfig note (override module/moduleResolution to ESNext/bundler).
…tics + out-of-range-marker policy

- P3: add explicit adapter error-semantics deliverable + AC + tests (spec D2
  locked: guarded read/watch/list/resolve/onChange → onError + console; failed
  list() preserves prior markers). Fixes Codex iter-4 #1.
- P3: resolve the spec-deferred out-of-range-marker policy — ignore + warn once
  (over clamp/hard-error) + test. Fixes Codex iter-4 #2.
- P4: name examples/ entrypoint (index.html + main.tsx + vite.config.ts,
  dev:example script). Fold Claude notes: CI step placement, publish-analogy
  precision, vite devDep timing.
…release git-add blocks

- P3: the v1 component guards only the calls it makes (read/watch/list);
  ThemeAdapter.resolve/onChange are not called by it (D4 Model A) so they're out
  of its error scope — error handling belongs to the scenario-4 contract test /
  #863 consumer. Removes the iter-5 self-contradiction. AC updated.
- P1 release deliverable: also update the hardcoded stable/RC git-add command
  blocks to stage packages/artifact-canvas/package.json (not just the enumeration).

Per human decision after iter-5: fix these two and take the plan to the
plan-approval gate (no 6th consult).
…canvas package

Phase 1 of the artifact-canvas plan: package skeleton, dual-format build, locked
adapter interfaces, theme tokens, and repo wiring.

Package (packages/artifact-canvas/):
- package.json (@cluesmith/codev-artifact-canvas 3.1.7; peer react/react-dom ^18||^19;
  deps markdown-it + dompurify; dual CJS+ESM exports + ./default-theme.css)
- tsup dual-format build (CJS+ESM+dts, react external, copies default-theme.css)
- tsconfig (extends config base; ESNext/bundler/react-jsx; jsdom vitest)
- adapters/{FileAdapter,MarkerAdapter,ThemeAdapter}.ts — interfaces only
- types.ts: ReviewMarker, Disposable, ArtifactCanvasProps (onAddComment intent seam)
- default-theme.css (8 --codev-canvas-* tokens), ArtifactCanvas placeholder, index.ts
- tests: import-boundary (no vscode/node/fs/fetch in shipped src); build-smoke script

Repo wiring:
- root build script (artifact-canvas after core, before codev)
- scripts/bump-all.sh (version-aligned bump loop)
- .github/workflows/test.yml (build+smoke and unit-test steps)
- release protocol (enumeration + stable/RC git-add blocks; publish untouched — not v1-published)

Verified: package build+tests+smoke green; full root build green; codev suite green.
…p injectable-logger; resolve() full property name)

Implement consult iter-1 (Codex) caught that the code was correct but the spec prose
was stale on two deferred-item decisions the plan mandated syncing:
- D2: drop the 'injectable logger' claim → 'logged to the console' + onError? (deferred #1).
  Also dropped ThemeAdapter.resolve/.onChange from D2's guarded-calls list for consistency
  with D4 Model A (v1 component doesn't call them).
- ThemeAdapter.resolve interface comment: full property name example
  resolve('--codev-canvas-foreground') instead of ('foreground') (deferred #2).
…e protocol

iter-2 Codex (COMMENT): the RC-publishing comment still said only codev/core/types
are bumped; artifact-canvas is now in the RC bump + git-add. Sync the comment.
…urce-mapping + DOMPurify sanitization

Phase 2: the markdown rendering layer.
- src/renderer/renderer.ts: markdown-it (html:false, linkify) + a codev_data_line core
  rule stamping 0-based token.map[0] onto block tokens (headings, paragraphs, lists,
  list items, blockquotes, fenced/indented code, tables); output sanitized via DOMPurify
  before return. data-* attrs (incl. data-line) preserved.
- src/renderer/MarkdownView.tsx: React component rendering the sanitized HTML.
- Export renderMarkdown + MarkdownView from index.ts.
- Tests (13/13): data-line attribution across block types; sanitization.

Plan deviation (documented): deferred-#3's 'javascript: link proves DOMPurify' premise is
wrong (markdown-it validateLink neutralizes javascript: before DOMPurify), so the sanitize
step is proven via a DOMPurify.sanitize spy instead; all 3 defense layers kept.
porch's tests check runs the whole codev suite; it failed on pre-existing flaky tests
(non-deterministic: 7 failures full-suite → 1 on isolated re-run; 0 in Phase 1), all in
codev files unrelated to the artifact-canvas work. Quarantined per the builder protocol's
Handling Flaky Tests rule (architect-authorized) with describe.skip + // FLAKY annotations:
- tunnel-integration.test.ts (file-watcher timing)
- default-branch.test.ts (git-fixture isolation)
- non-main-default-branch.test.ts (git-fixture isolation; all 3 describes)
- team-cli.test.ts (afx team deprecation only; spy ordering)

Documented in codev/reviews/945-*.md (Flaky Tests). Architect filing a tracker for the
underlying fix so the skips are not permanent.
…for out-of-range markers

Codex iter-3 (REQUEST_CHANGES, MEDIUM): out-of-range markers were warned on every list()
reload, but the deferred-#4 AC says 'dropped and warned once'. Added a warnedRef Set to
dedup the console.warn per unique stale marker; tightened the test to assert warn fires
exactly once across a watch reload. 28/28 green.
…r no-watcher refresh (D6)

Codex iter-4 (HIGH): spec D6 promised a no-watcher host could force refresh by re-rendering,
but the effect's stable deps mean a same-props re-render does not re-fetch (correct React).
Architect-authorized additive fix:
- ArtifactCanvasProps gains an OPTIONAL refreshKey?: number | string (no-op default; hosts
  with a watcher omit it → behavior unchanged). Added to the effect deps so bumping it forces
  a fresh read + marker-list.
- Corrected spec D6 wording to name refreshKey as the contract (host bumps it on data change;
  plain re-render does not re-fetch).
- Test: refreshKey 1->2 triggers a second read + new content (no watcher involved). 29/29.
…eload; pin out-of-range marker channel to console.warn

iter-5 Codex (REQUEST_CHANGES):
- Stale activeLine: activeLine persisted across watch/refreshKey reloads without
  validation, so the overlay could render + on — and emit onAddComment for — a line
  the reloaded document no longer contained. Fixed at root: reset activeLine on every
  content change. Regression test: after a reload that removes the hovered block, no +
  shows and onAddComment never fires for the removed line.
- Out-of-range marker channel: plan wording was ambiguous (onError?/console.warn).
  Tightened to console.warn-only; onError? is reserved for genuine adapter failures
  (an out-of-range marker is data-hygiene, not a failure). Architect-authorized
  plan-wording tightening (same basis as the iter-4 D6 spec tightening).
…tiveLine fix; out-of-range channel tightening)
…ples dev page, README

- src/__tests__/fixtures/: stub adapters (createStubHost + named stub*Adapter factories over a
  shared text store; text-as-source-of-truth, #857) + a realistic sample artifact with a seeded
  REVIEW marker.
- src/__tests__/end-to-end.test.tsx: the primary contract proof — render → existing marker →
  hover/click + AND focus/Enter → onAddComment (0-based) → host add serializes INTO text → watch
  replays → list re-derives → new marker renders. Asserts the marker lands in the text store
  (round-trip THROUGH text, not a UI-only refresh).
- examples/: Vite dev page reusing the same stubs (pnpm dev:example); excluded from the package.
- README.md: 3 adapter contracts, ArtifactCanvasProps, --codev-canvas-* tokens + override, host
  walkthrough, why-tsup rationale, scope.

build 0, 33/33 tests, check-types 0, examples bundles headlessly, dist clean (no test/example leak).
…e:* (not npm-published in v1)

iter-1 Codex (HIGH): README told consumers to 'pnpm add @cluesmith/codev-artifact-canvas', but
v1 is not independently npm-published (per spec-945 / release/protocol.md) — it's consumed via
workspace:* and bundled by hosts. Rewrote the Install section to add it as a host workspace
dependency; removed all npm-install guidance. Docs-only.
…stead of blind reset (fixes CI-only e2e race)

The iter-5 stale-activeLine reset (useEffect(() => setActiveLine(null), [content])) was
unconditional. The initial async load sets content outside the test's act(), so that effect
could fire AFTER a hover set activeLine, clobbering it — the + overlay never mounted and the
Phase 4 MOUSE e2e test's findByRole timed out. Reproduced only under CI's slower scheduling
(local passes 10x full-suite).

Fix: validate the overlay anchor against the reloaded DOM (clear activeLine only when its
[data-line] element is genuinely absent), folded into the decoration effect keyed on [html].
A valid hovered line survives (no clobber); a removed/shortened block still clears (preserves
the iter-5 Codex intent). Added a deterministic regression test proven to fail against the old
blind-reset and pass against the validation fix. 34/34 green.
…plan approved (PR consult items)

PR consult (re-run on final code) Codex REQUEST_CHANGES, both fixed:
- Version lockstep: artifact-canvas was 3.1.7 while the workspace is 3.1.9 (rebase side effect);
  bumped to 3.1.9 to restore the invariant the release/bump wiring relies on. Lockfile unchanged.
- Approved plan artifact still said 'Status: draft' with no approval frontmatter though
  status.yaml shows plan-approval approved; added frontmatter + flipped Status to approved.
No code-behavior change; build + 34 tests green.
…I-only keyboard test race)

3rd CI-only failure (Phase 3 keyboard test, 'expected -1 to be +0'). Evidence shows the failing
assertion is expect(p.tabIndex).toBe(0), NOT onAddComment: markdown-it deterministically yields
data-line='0' (proven; data-line.test passes in the same CI run), so onAddComment(-1) is
impossible; -1 is a <p>'s default tabIndex. The decoration effect set tabIndex=0 AFTER render and
the test read it synchronously when the element appeared, racing the effect under CI's slower
scheduling (same race family as the e2e activeLine fix).

Fix: stamp tabindex='0' at render time in the renderer core rule (alongside data-line), so
focusability is in the HTML the instant a block mounts. Removed the redundant el.tabIndex=0 from
the decoration effect. Added a deterministic renderer test for render-time tabindex. Preempts the
same latent race in the e2e keyboard test. types 0, build 0, 35/35 ×12 runs green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant