Skip to content

chore: library migration + lint/format + CI playwright bootstrap#29

Merged
fluid-chey merged 22 commits into
mainfrom
chore/library-migration-and-tooling
Apr 22, 2026
Merged

chore: library migration + lint/format + CI playwright bootstrap#29
fluid-chey merged 22 commits into
mainfrom
chore/library-migration-and-tooling

Conversation

@fluid-chey
Copy link
Copy Markdown
Collaborator

Summary

  • Library migration: picocolors, yargs, zod, parse5, AbortController, @microsoft/fetch-event-source, @tanstack/react-query, @radix-ui/react-dialog
  • Shared test infra: Anthropic SDK mock, SSE fixtures, HTML fixtures, CLI harness
  • CI: matrix + build job + Playwright webServer bootstrap (no more "assume vite is running")
  • Tooling: ESLint + Prettier configured, entire tree auto-formatted, lint errors fixed
  • Docs: UX improvements plan

Test plan

  • npm run typecheck (canvas + mcp) clean
  • Full Playwright e2e — 98/98 passing (43.7s)
  • CI green on this PR
  • Spot-check modal (radix dialog) and asset list (react-query) in dev

🤖 Generated with Claude Code

fluid-chey and others added 22 commits April 21, 2026 16:13
Finalize deletions of old 4-stage pipeline artifacts and remove orphaned
tooling, tests, screenshots, and components that no longer have callers
after the creative-agent rewrite.

- Pipeline tooling: eval-harness, visual-eval, compare-runs,
  report-summary, reseed-patterns, overnight-review, test-prompts,
  pipeline-tools, simulate-pipeline
- sim-executor subagent (referenced deleted pipeline-tools.cjs)
- Dead skills: fluid-one-pager, fluid-social, fluid-theme-section,
  overnight-run, simulate-pipeline, fluid-campaign
- Deleted components: SidebarNotes, Timeline, TimelineNode, VersionGrid
  (+ corresponding tests and skill-paths test)
- Removed MCP tools: iterate, iterate-request, read-annotations,
  read-history, read-statuses
- Old e2e specs (10 files) and their 58 tracked screenshots
- Stale canvas/dist output, canvas/test-results, canvas/.claude (dup),
  install.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rename

Rewrite project docs to match the post-refactor state: single creative
agent (no staged pipeline), DB-backed brand data, Fluid Creative OS →
Fluid DesignOS. Update archetype docs so selection is described through
agent tools (list_archetypes, read_archetype) rather than the removed
api-pipeline.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed brand data

- .gitignore: add canvas/dist/, output/, dated overnight-review snapshots
- settings.json: drop Read perms for removed brand/ and patterns/ dirs,
  add Read perm for AGENTS.md
- sync.sh: distribute the current skill set (drop fluid-social,
  fluid-one-pager, fluid-theme-section; add fluid-design-os-feedback,
  feedback-ingest); preflight-check .claude/skills/ instead of brand/
- brand-intelligence: rewrite to point at HTTP API + SQLite tables
  (canvas/fluid.db) instead of the retired MCP brand tools

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove code paths for the retired MCP tools (read_annotations,
read_statuses, read_history, iterate_request) — only push_asset remains.
Drop corresponding legacy types (Lineage, SessionSummary,
GenerateRequestBody, VariationStatus alias, CampaignChannelSlots, etc.)
now that the single-agent flow no longer needs them. Rename the
StatusBadge status type to VersionStatus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eight-task plan covering navigation bug fixes, Creations tab filters,
creation preview scaling, asset preview rendering, transparency grid,
and font previews.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…es, CLI harness, CI

Phase 0 of the library-migration plan. Adds the test scaffolding that every
later migration will lean on — none of the runtime code is touched yet.

- canvas/src/__tests__/helpers/anthropic-mock.ts: Scripted MockAnthropic
  matching the messages.create surface used in agent.ts (non-streaming,
  optional AbortSignal), with queued text/tool_use responses, scripted
  errors (status-bearing for retry tests), and cancellation-aware delays.
- canvas/src/__tests__/helpers/sse-fixtures.ts: makeSSEResponse / makeSSEStream
  build Response objects from SSE event arrays; supports chunk splitting,
  keep-alives, errorAtEnd for connection-drop simulations, and an explicitly
  malformed variant. 6 sanity tests; 7 on the mock.
- tools/__fixtures__/html/: nine fixtures covering clean social/website
  cases plus edge cases regex validators mishandle (alt with >, hex in meta,
  target-comment inside attribute, self-closing img, multi-style-block + pre,
  SVG CDATA, url() in attrs and style blocks).
- tools/__tests__/helpers/run-cli.cjs: spawnSync wrapper returning
  stdout/stderr/status for CLI characterization tests; 4 sanity tests.
- tools/package.json: new — test script runs node:test on *.test.cjs files.
- .github/workflows/test.yml: unit / tools / e2e jobs on push + PR
  (e2e currently non-blocking; Phase 3 adds playwright webServer bootstrap
  and the coverage / Node matrix).

Baseline: canvas 334 passed / 7 pre-existing jsdom fetch failures, tools 4/4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces inline ANSI escape tables in brand-compliance.cjs,
dimension-check.cjs, and schema-validation.cjs with picocolors. Library
honors FORCE_COLOR and NO_COLOR natively, which the prior code did not.
Adds tools/__tests__/color-output.test.cjs covering both forced-on and
forced-off paths against real fixture files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces hand-rolled process.argv.indexOf('--flag') patterns across five
CLIs (brand-compliance, dimension-check, scaffold, feedback-ingest,
db-import) with a yargs chain per tool. Every tool now gets --help for
free plus .strict() rejection of unknown flags. Adds
tools/__tests__/cli-args.test.cjs to pin --help/unknown-flag/positional
behavior across the five.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces hand-rolled typeof/Array.isArray shape checks in tools/schema-validation.cjs,
tools/validate-archetypes.cjs, and canvas/src/lib/slot-schema.ts with zod schemas.

- tools/schemas/gold-standard.cjs: LiquidSchemaSchema validates the shape of
  a parsed {% schema %} block; Gold Standard count requirements remain
  hand-written semantic logic (renamed GOLD_STANDARD_SCHEMA → GOLD_STANDARD_REQUIREMENTS).
- tools/schemas/archetype.cjs: ArchetypeSchema with discriminated-union
  FieldSchema; zod issues map back to existing error codes
  (MISSING_WIDTH / UNKNOWN_FIELD_TYPE / HAS_TEMPLATE_ID / etc.) so operator
  muscle memory is preserved.
- canvas/src/lib/slot-schema.ts: zod schemas alongside type aliases — every
  type is now z.infer<typeof XZ>. Existing helper functions unchanged.
- isUsableStoredSlotSchema uses safeParse + fields.length > 0.
- validate-archetypes.cjs: FLUID_ARCHETYPES_DIR env override added so
  characterization tests can point at a throwaway fixture dir.

New test files:
- tools/__tests__/schema-validation.test.cjs (6 tests)
- tools/__tests__/validate-archetypes.test.cjs (7 tests)
- canvas/src/__tests__/template-configs.test.ts (23 tests)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces line-based regex scanning in brand-compliance.cjs and
dimension-check.cjs with a parse5 AST walk for HTML-structure-dependent
checks. Fixes three classes of regex bugs the characterization suite now
pins:

1. hex-in-non-style scanning: #RRGGBB inside <meta theme-color>, <pre>,
   or data attributes is no longer reported as a CSS color violation.
2. <img> with alt containing '>': decorative-img detection now reads
   attrs from the AST, not from a truncated regex match.
3. dimension comment inside an attribute: extractDimensions scans
   comment nodes only, not arbitrary text. Tightened to anchored regex
   so fixture-description comments (multi-line, containing the pattern
   as quoted text) are not treated as directives.

Out of scope: canvas/src/server/watcher.ts. Its asset-path rewriting is
a broad string substitute (4 call sites) that correctly handles comments,
data attrs, and pre blocks — a parse5 rewrite that only walked
src/href/url() tokens would regress those cases. Watcher stays regex.

Non-structural checks (CSS rule matching in checkMultipleAccentColors /
checkBodyCopyColor / checkHeadlineLetterSpacing / etc.) continue to
operate on raw text — parse5 doesn't help there; csstree is the right
tool for CSS-rule shape work and that's out of scope for this migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the per-session { cancelled: boolean } handle with a native
AbortController. The signal is forwarded to anthropic.messages.create
and to render_preview, so cancel lands mid-SDK-call rather than waiting
for the SDK to return.

- activeSessions: Map<chatId, Set<AbortController>>
- cancelChat(chatId): aborts every controller in the set
- createMessageWithRetry is now exported; accepts signal; its backoff sleep
  aborts on signal rather than polling every 100ms
- executeTool takes signal; guards on signal.aborted before dispatching
- renderPreviewTool / renderPreview thread signal through; each await in
  the Playwright sequence re-checks signal.aborted so cancel mid-render
  drops through to the finally (page.close)
- sleepOrAbort helper replaces the polling-backoff loop

Adds canvas/src/__tests__/agent-cancel.test.ts:
- createMessageWithRetry happy path, 429 retry, non-retriable 4xx,
  already-aborted signal, mid-call abort, abort during backoff sleep
- cancelChat + activeSessions registration (via __registerSessionForTests /
  __getActiveSessionCount / __clearActiveSessionsForTests hooks)

Also adds tools/node_modules/ to .gitignore (missed when tools/package.json
landed in Phase 0; harmless so far since it was never staged).

p-retry wrapping is not included in this commit — the hand-rolled retry
loop with the new sleepOrAbort is short enough that replacing it adds
dependency weight without simplifying the logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the hand-rolled reader.read() / TextDecoder / split('\n')
loop in sendMessage with fetchEventSource. The library absorbs framing
robustness (mid-chunk splits, CRLF variants, keep-alive comments) and
its onerror/onclose hooks close a pre-existing UX bug: if the server
killed the connection mid-stream, the UI stayed in `isStreaming: true`
forever. Now onerror emits a Connection error notice and onclose
explicitly finalizes streaming.

- Unified AbortController cancellation: signal on fetchEventSource is
  the same controller stored in the chat state; cancelGeneration still
  calls abort().
- openWhenHidden: true — streaming must continue while the tab is hidden.
- onerror throws to disable the library's built-in reconnect/retry.

Adds canvas/src/__tests__/chat-store-sse.test.ts (node env) covering
text-delta accumulation, tool_start/tool_result, creation_ready hook,
malformed JSON tolerance, connection-dropped recovery, and mid-chunk
splits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the hand-rolled module-level cache (cachedAssets / inflight /
listener Set) in useAssets with a useQuery hook. The public API of the
hook is unchanged — consumers keep destructuring { assets, loading,
error, invalidate } — but dedup, caching, and error handling are now
library-managed.

- App.tsx wraps the tree in QueryClientProvider; the client is a module
  singleton in src/lib/query-client.ts with staleTime: Infinity,
  refetchOnWindowFocus: false, retry: false (matching prior behavior).
- invalidate() is queryClient.invalidateQueries({ queryKey: ['assets'] }).

Adds canvas/src/__tests__/useAssets.test.tsx covering dedup across
consumers, invalidate-triggered refetch, unmount/remount cache hit, and
error-path surfacing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates AppShell's CreateNewChoiceModal and DAMPicker's three
fixed-overlay states (no-token, load-error, loading) to Radix Dialog.
Visual styles are ported verbatim — Radix is headless — so there is no
appearance change. What changes is accessibility: focus trap,
Escape-closes, focus restoration on unmount, and aria-labelledby /
aria-describedby wiring come from the library.

- Dialog.Title provided via a VisuallyHidden wrapper for modals without
  a visible title.
- Backdrop click still closes (onOpenChange fires with false; matches
  prior behavior).
- The imperative DamPicker SDK modal (opened by picker.open()) is
  unchanged — it's outside React's component tree and has its own
  lifecycle.
- CreateNewChoiceModal caller updated to pass open prop so Radix
  controls visibility directly (conditional wrapper kept for tab guard).

Adds canvas/src/__tests__/modal-a11y.test.tsx covering focus trap,
Escape close, backdrop click, and inner-click-doesn't-close.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promotes the CI skeleton from Phase 0 into something that actually
gates PRs:

- unit job now runs on Node 20 AND Node 22 (fail-fast: false) so we
  catch a version-specific API drift before someone merges it.
- build job (new): cd canvas && npm run build. Catches TypeScript
  regressions the unit tests can miss (type-only errors don't fail
  vitest since vitest strips types).
- e2e job stops pretending Vite is already running. playwright.config.ts
  gains a webServer block that boots `npm run dev` and waits for :5174.
  PLAYWRIGHT_SKIP_WEBSERVER=1 escape hatch restores the old behavior
  for local debugging.
- Playwright browser cache keyed on canvas/package-lock.json so
  subsequent CI runs don't re-download Chromium.

Coverage gating (source plan mentioned vitest --coverage with per-file
thresholds) is deferred — the migrated files are well-covered by the
characterization suites this branch added, and a threshold file adds
mechanical overhead for little additional signal right now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a scalable lint/format baseline so new code stays consistent:
- eslint 9 flat config with typescript-eslint, react, react-hooks, react-refresh
- prettier config (100-col, single quotes, trailing commas)
- eslint-config-prettier to avoid style conflicts
- new scripts: lint, lint:fix, format, format:check, typecheck
- prettier-formatted the entire src/mcp/scripts tree

Also clears 30 real lint errors surfaced by the new config:
- fix no-constant-binary-expression bug in rowToIteration (Number(x) ?? 0 → || 0)
- document 21 intentionally-empty catches (idempotent migrations, best-effort cleanup)
- @ts-ignore@ts-expect-error, prefer-const, unused-var, useless-escape cleanups

Lint result: 0 errors, 205 warnings (incremental quality signals for future work).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Executes the full library-migration plan. Ten commits land on the branch
plus this merge commit:

  1. chore(test): shared infra — Anthropic mock, SSE fixtures, HTML fixtures, CLI harness, CI
  2. refactor(tools): picocolors for terminal output
  3. refactor(tools): yargs for CLI arg parsing
  4. refactor: zod for schema validation
  5. refactor(tools): parse5 for HTML validators
  6. refactor(server): AbortController for agent cancellation
  7. refactor(client): @microsoft/fetch-event-source for SSE
  8. refactor(client): @tanstack/react-query for useAssets
  9. refactor(client): @radix-ui/react-dialog for modals
 10. ci: matrix, build job, playwright webServer bootstrap

Conflict resolution: branch was cut from 460df2c; main moved forward
with 2848c0f (eslint + prettier auto-format across 135 files). Migration
code is preferred for every functional file, then re-formatted with
prettier so it matches the new style baseline. canvas/package-lock.json
was regenerated via npm install against the merged canvas/package.json
(both sides added dependencies).

Post-merge:
  - canvas: 384 pass / 7 pre-existing fail / 1 skipped / 1 todo, tsc clean
  - tools:  57 pass / 0 fail
  - bundle: 804 kB / 226 kB gzip

Pre-existing failures (AppShell.test.tsx × 1, brand-seeder.test.ts × 5,
template-gallery.test.tsx × 1) are the same 7 that were failing before
this work began — they are jsdom/fetch issues unrelated to the migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Canvas unit tests:
- Update brand-seeder test to match current pattern-seeds on disk (14 files,
  categories: colors/typography/archetypes) — prior expectations (12,
  design-tokens, layout-archetype) were from an earlier seeder design.
- Update template-gallery test: TemplateCustomizer.onCreated signature is
  now (campaignId, creationId, iterationId), not (campaignId).
- Update AppShell "templates" test: TemplatesScreen fetches templates from
  the DB (jsdom has no server), so assert the screen mounts with its header
  rather than a specific template iframe.
- setup.ts: give jsdom a real origin so relative fetch('/api/...') parses,
  and add a benign default fetch stub so components with boot-time fetches
  (ChatSidebar.loadChats etc.) don't crash tests that don't mock fetch.

CI:
- Canvas unit job now installs tools/ deps (canvas tests shell out to
  tools/*.cjs which require picocolors etc.) and Playwright chromium
  (needed by tests/render-engine.test.ts).
- tools/ test script: switch glob from single-quoted literal (bash doesn't
  expand, Node 20's --test doesn't glob) to an explicit globSync spawn so
  it works on both Node 20 and 22.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
globSync was only added to node:fs in Node 22 — the CI CLI tools job uses
Node 20 and was failing fast with TypeError. Swap to readdirSync with
{recursive: true} (supported since Node 20.1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove 3 unused @deprecated re-exports in preview-utils.ts
- Extract slugify() to lib/slugify.ts; delete copies in agent-tools, db-api
- Add stripHtmlExt() helper, replace 4 hand-rolled regex sites
- Replace isNamedFontPreset 9-branch chain (3x in editor.ts) with helper
- Drop dead selectedTemplateId state in both BuildHero files
- Replace ad-hoc Date.now()+Math.random() ids with nanoid()
- readArchetype / listArchetypes: swap existsSync+readFileSync TOCTOU
  pairs for try/catch reads (halves syscalls)
- scheduleUndoSnapshot takes a thunk so debounce-burst calls skip the
  per-keystroke structuredClone; factor the 4x duplicated commit
  callback into a shared commitUndoSnapshot closure (-55 lines in
  store/editor.ts, eliminates wasted clones on text edits)

Net: 14 files, +80 / -174. Zero regressions — typecheck, 57/57 tools,
canvas unit suite baseline preserved, Playwright 98/98 green.
- db-import.cjs: lazy-load better-sqlite3 (it lives in canvas/node_modules)
  so --help exits 0 in the CLI-tools CI job where canvas deps aren't
  installed.
- color-output baseline tests: clear CI env var in the 'no TTY baseline'
  cases. picocolors auto-enables color when CI is set, even without a TTY,
  which broke the baseline 'no ANSI' assertions on GitHub Actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fluid-chey fluid-chey merged commit 14303ea into main Apr 22, 2026
5 checks passed
@fluid-chey fluid-chey deleted the chore/library-migration-and-tooling branch April 22, 2026 20:04
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.

1 participant