-
Notifications
You must be signed in to change notification settings - Fork 0
Comparing changes
Open a pull request
base repository: aiatelie/ai-atelie
base: main
head repository: aiatelie/ai-atelie
compare: feat/storage-driver-and-project-list
- 13 commits
- 28 files changed
- 2 contributors
Commits on May 6, 2026
-
feat(api): add storage driver interface with fs + memory implementations
Introduce a swappable storage layer behind a single StorageDriver interface. Three primitives: - JsonKv: versioned JSON blobs with ETag + If-Match + subscribe (backs .meta/<key>.json per project and SHARED_ROOT/<key>.json) - BlobStore: bytes with subscribe, .meta/* hidden by design (backs project source files, manifest, uploads) - AppendLog: sketched only — both drivers throw on calls. Lands in a follow-up that moves comment-undo snapshots from in-memory Map to disk-backed log. Two implementations ship together so the seam is provable: - fs-driver mirrors the current byte layout exactly (same atomic tmp+rename, same W/"<base36-mtime>" ETags, same fs.watch debounce for reload events). fs.watch is the canonical source of BlobStore events so out-of-band agent CLI writes still drive the iframe reload SSE. - memory-driver stores everything in Map<>; used by tests to verify routes work against any conforming driver. No behavior change yet. Routes still call node:fs/promises directly. The repository layer and the route refactor land in the next two commits. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for 036de6b - Browse repository at this point
Copy the full SHA 036de6bView commit details -
feat(api): add storage repositories layer
Four repos sit between routes and the StorageDriver — routes call typed domain methods, never the driver primitives: - ProjectRepo: list, exists, getManifest, updateManifest, create, delete + the shared file-path validator (refuses manifest.json overwrites, dot-prefix segments, traversal). - ProjectMetaRepo: thin pass-through over the per-project JsonKv. - ProjectFilesRepo: BlobStore + page/component/asset/config classification the file-tree response uses for icons. - SharedRepo: workspace-wide JsonKv. ProjectManifest moves from routes/projects.ts into storage/repos/types.ts since it's a domain shape, not an HTTP shape. getRepos() returns a singleton bound to the boot-time driver; rebindRepos(driver) is the test helper. Still no behavior change. Routes refactor to use these repos in the next commit. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for e40f675 - Browse repository at this point
Copy the full SHA e40f675View commit details -
refactor(api): shared route uses SharedRepo
routes/shared.ts no longer imports node:fs/promises. The PATCH path goes through SharedRepo.put which returns a typed result with the ETag baked in; conflicts are surfaced as a discriminated union, so the 412 path is a single if instead of a stat+compare dance. The SSE channel now multiplexes two sources: driver-level JsonKv events (auto-fired on every PATCH) and the workspace event-bus (`sharedEvents`, kept for non-storage signals like "projects index changed" emitted from project lifecycle routes). Backward-compatible on the wire — both still send `data: <key>`. Verified via Hono fetch smoke: GET 404 / PATCH 200+etag / GET 200 / GET If-None-Match 304 / PATCH bad If-Match 412 with current_etag / PATCH good If-Match 200 / bad key 400 — all match the previous implementation byte-for-byte. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for 37efdca - Browse repository at this point
Copy the full SHA 37efdcaView commit details -
refactor(api): projects route uses storage repos; sseChannels shrinks
routes/projects.ts no longer imports node:fs/promises. The 720-LOC file becomes ~530 LOC of HTTP shape: parsing, validation, MIME lookup, reload-script injection, EDITMODE regex, starter HTML/CSS templates. Everything storage-shaped routes through getRepos(): /api/projects → projects.list() /api/projects/create → projects.create({id,name,starters}) /api/projects/:id/manifest → projects.getManifest / updateManifest /api/projects/:id/__meta-events → projectMeta.subscribe (driver-internal) /api/projects/:id/meta/:key → projectMeta.get / put (JsonKv) /api/projects/:id/files → projectFiles.list /api/projects/:id/file/upload → projects.validateFilePath + projectFiles.write /api/projects/:id/tweak → projectFiles.readText / write (regex stays in route) /api/projects/:id/inspector-css → projectFiles.readText / write /api/projects/:id/file/delete → projects.validateFilePath + projectFiles.delete DELETE /api/projects/:id → projects.delete (driver tears down channels) /p/:id/__reload → projectFiles.subscribe (BlobStore via fs.watch) /p/:id/_preview/* → projectFiles.exists + synthesized preview HTML /p/:id/* → projectFiles.read + reload-script + MIME services/sseChannels.ts shrinks to just the workspace event-bus (sharedEvents + broadcastShared). The per-project meta and reload channels became driver-internal; nothing outside the driver creates EventEmitters or calls fs.watch any more. End-to-end Hono fetch smoke covers 26 cases — list/create/manifest/files/ meta GET+PATCH+ETag+If-None-Match+If-Match/static serve with reload script injection/.meta refused/traversal refused/upload/delete/tweak EDITMODE/ tweak missing-block/inspector-css/project delete — all match the previous implementation. API boots cleanly with /api/health responding 200. Co-Authored-By: Claude Opus 4.7 <[email protected]>Configuration menu - View commit details
-
Copy full SHA for 856d923 - Browse repository at this point
Copy the full SHA 856d923View commit details -
test(api): driver-level + route-level swap tests prove the seam
Two test files under api/src/storage/, both run via 'bun test': - driver-swap.test.ts uses describe.each over [fs, memory] to run the same 13 scenarios against both drivers — project lifecycle, JsonKv round-trips with ETag and If-Match conflict/success, JsonKv subscribe, BlobStore write/read/list, BlobStore traversal + dot-prefix refusal, BlobStore subscribe under fs.watch debounce, shared kv subscribe, deleteProject teardown. - route-swap.test.ts swaps the memory driver in via rebindRepos() and hits the actual Hono routes from routes/projects.ts: create/list/ manifest/delete, meta GET 404 + PATCH + 304 + If-Match conflict + If-Match success, static serve injecting the reload script, .meta/* refused via /p/:id/*, tweak rewriting an EDITMODE block. The routes cannot tell which driver is underneath. Adds a 'test' script to api/package.json mirroring the web workspace's 'bun test src' convention so the tests are discoverable. Total: 30 pass / 0 fail across both files. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for 26547aa - Browse repository at this point
Copy the full SHA 26547aaView commit details -
fix(api): comment-undo snapshots persist across restart
closes #11 Replaces the in-memory Map<turnId, Snapshot> in services/snapshots.ts with a SharedRepo-backed JsonKv store (web/.data/snapshot-<turnId>.json). Comment-undo now survives daemon restart, bun --watch reloads, and SIGTERM/SIGINT — the original footgun the issue called out. API shape: recordSnapshot(turnId, projectId | null) → captures the right scope and persists in one call getSnapshot(turnId) → reads from disk applySnapshot(snap) → routes through ProjectFilesRepo for project scope, fs.writeFile for legacy LEGACY_EDITOR_ROOT/src diffSnapshot(snap) → no longer needs rootDir argument (snap carries its own scope) deleteSnapshot(turnId) → drops the entry Workspace-scope (not project-scope) means /api/comment-undo can look up by turnId without knowing the projectId. Trade-off: deleting a project leaves its snapshots dangling until the LRU evicts them — acceptable because LRU caps total at 64 entries. Five new tests in services/snapshots.test.ts cover the persistence contract: - record + retrieve - restart simulation: rebuild driver against the same SHARED_ROOT, read snapshot back - applySnapshot reverts modified files - deleteSnapshot removes the entry - LRU sort-by-createdAt ordering Bumps the BlobStore subscribe test wait window from 100ms → 350ms total (50ms attach + 300ms after writes) — fs.watch starts asynchronously on macOS and was occasionally missing the first write under test load. Implementation chose JsonKv over the AppendLog primitive because snapshots are keyed (by turnId) not sequential. AppendLog stays a stub in both drivers until a real append-log consumer (run-event ring buffer #12) is built. 35 / 35 tests pass. API boots cleanly. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for f28f8ef - Browse repository at this point
Copy the full SHA f28f8efView commit details -
fix(web): home page renders skeleton instead of \"No projects yet\" o…
…n a fresh browser closes #55 The home page used to flash an "empty" state on first paint of any browser without a localStorage cache — the cache hydrated synchronously to [], rendered EmptyState, then the async /api/projects fetch populated it. Same problem for fresh Playwright contexts (the CUJ flake) and any cleared-cache user. The fix is small because lib/projects.ts already does SWR-style caching; it just didn't tell callers about the in-flight first fetch. - `firstFetchPending` flag flips true at boot when localStorage is empty (or has zero projects), false when /api/projects resolves (success OR failure — an offline first paint falls through to the empty state instead of spinning forever). - `useProjects()` exposes it as `loading`. - A localStorage cache with N>0 projects renders immediately and doesn't show loading; the server fetch refines it in the background. - Projects.tsx renders a 3-card skeleton when loading + empty, the real EmptyState only when not-loading + empty. - Footer pill: "Local-only · stored in your browser" → "Local-first · stored on disk". The old text was misleading once the API became the source of truth (which it has been for a while). Tests: - 3 new bun-test cases over the loading flag with a DOM shim: · fresh browser (empty localStorage) → loading flips true then false · warm cache (1 project) → loading false from the start · network failure → loading flips off so EmptyState can render - All 42 existing web tests still pass. - Browser-checked: vite booted in the worktree on :15173, headless Chromium loaded /, confirmed new pill text rendered, projects loaded, zero page errors. Doesn't touch any other localStorage callers — comments and threads are already on disk via .meta/, editor-overrides.v1 and drawings.v1 are still per-device by design and have their own follow-up issues. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for d37805a - Browse repository at this point
Copy the full SHA d37805aView commit details -
test(web): PR evidence spec — pill text + create-project + meta ETag …
…flow A small Playwright spec under web/tests/e2e/ targeted at this PR's specific surface (not the CUJ — the CUJ stays the load-bearing end-to-end test). Two scenarios: - home page: GET /api/health → 200, GET /api/projects → array, rendered footer pill matches "Local-first · stored on disk" (proof the #55 copy change shipped), section label shows project count. - editor: create new project via the modal, wait for /editor?p=p_* URL, confirm iframe is mounted with a src. Then exercise the meta endpoint round-trip end-to-end through the new repo layer: PATCH /meta/threads → 200 with ETag, GET with If-None-Match → 304, PATCH with wrong If-Match → 412, PATCH with correct If-Match → 200. Cleanup deletes the test project. Useful as evidence for this PR and as a fast smoke for the same surface in future PRs (runs in the default `bun run test:e2e` group; takes ~3 seconds). Co-Authored-By: Claude Opus 4.7 <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for 4d063f7 - Browse repository at this point
Copy the full SHA 4d063f7View commit details -
feat(api): real AppendLog implementation in fs + memory drivers
The third driver primitive moves from stub to real impl. Per-project monotonic seq log; FS driver writes to web/projects/<id>/.meta/history.jsonl, memory driver holds an array. Both implementations honor: - append(entries) returns the new lastSeq; seqs are monotonic from 1 even across daemon restarts (FS reloads from disk on first call). - read({ sinceSeq, limit, reverse }) for resumption, replay, paginated reads. - subscribe(undefined, fn) for live-only event flow; subscribe({sinceSeq}, fn) replays past entries then attaches for live, with a buffer that drains on the same fn call so consumers see no gaps and no dupes — what an SSE handler with Last-Event-ID needs verbatim. - truncateBefore(seq) compaction, atomic on FS via tmp + rename. FS impl serializes appends via a per-channel promise chain so concurrent callers can't corrupt the seq counter or interleave lines. Filesystem events for history.jsonl don't fire BlobStore events (the blob watcher filters .meta/) and don't appear in JsonKv.list (which only lists *.json). 5 new test scenarios in driver-swap.test.ts run via describe.each against both drivers (10 new test instances): seq monotonicity, read filters, live subscribe, replay-then-live with sinceSeq, truncateBefore. Total api suite: 35 → 45 tests, all green. No new consumer wired in this commit — interface ready for the Last-Event-ID-resumable run-events SSE (issue #12) when that feature lands. Snapshots stay on JsonKv (keyed by turnId, not sequential, so not an AppendLog workload). Co-Authored-By: Claude Opus 4.7 <[email protected]>Configuration menu - View commit details
-
Copy full SHA for c0e4d39 - Browse repository at this point
Copy the full SHA c0e4d39View commit details -
fix(web): close project-list race that drops just-created projects
Surfaced by the CUJ on a fresh browser. The sequence: T0: page mounts → bootCache fires GET /api/projects (response stale, returns [demo] because the user hasn't created anything yet) T2: user creates "CUJ Hello World" → POST commits, cache locally seeded with [demo, p_new], active = p_new T3: T0's GET response finally arrives → mergeServerWithLocal sees server=[demo] only, drops the local-only p_new, falls back active to demo T4: user lands in /editor — but active is now `demo`, not `p_new` The fix: AbortController on fetchFromServer. Mutations (createProject, deleteProject) call invalidateInflightFetch() before they touch cache, which aborts any in-flight stale GET so its response is silently dropped. The SSE-triggered fetchFromServer that fires next will see the post-mutation server state. Adds one new bun test (4 → 4+1) that proves the contract: while a slow GET is in-flight, createProject seeds cache; releasing the slow GET later does not undo the seed; the in-flight fetch's signal is observed as aborted. This was the load-bearing bug behind the CUJ failures we'd been seeing on both `main` and the storage-driver branch — same screenshot, editor landing on the demo project. CUJ now passes in 1.4 minutes (the assertion-loosening commit follows). Co-Authored-By: Claude Opus 4.7 <[email protected]>Configuration menu - View commit details
-
Copy full SHA for 69cb5ab - Browse repository at this point
Copy the full SHA 69cb5abView commit details -
test(web): CUJ asserts iframe truth, not jsx-file-structure coincidence
The previous step 8 polled web/projects/<id>/*.jsx for a regex match on light + dark + "hello world". With the project-list race fixed (preceding commit), the agent now reliably runs in the new project — and legitimately solves the prompt by editing index.html + style.css in place rather than creating a separate `*.jsx` file. The visible canvas is identical either way; the file-structure assertion was over-fitted to one particular shape of agent output. Step 8 now polls the iframe DOM directly for two "Hello World" instances + light/dark indicators (the actual user-observable success state). Step 9 becomes a sanity check that the agent wrote *something* to disk beyond the unchanged starter — failing loudly if a turn produces only chat output. CUJ_JOURNAL.md updated with the `evolved` entry per the journal contract. Wall time: 1.4 minutes on the happy path, down from 7-minute timeouts on the previous runs against the unfixed race. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for 30e30f1 - Browse repository at this point
Copy the full SHA 30e30f1View commit details -
Configuration menu - View commit details
-
Copy full SHA for 9f02ae9 - Browse repository at this point
Copy the full SHA 9f02ae9View commit details -
fix(web): editor-overrides + drawings move from localStorage to .meta/
Both modules used to write to a single workspace-wide localStorage key (`editor-overrides.v1` and `drawings.v1`). Two bugs followed: - Cross-project leak: same route name in two projects (e.g. both have `index.html`) shared the same data slot. - No cross-browser visibility: open the editor on one machine, the overrides/drawings are invisible on another. Migrated to a three-layer pattern that mirrors what threads.ts and comments.ts already do: • memory cache (per-project Map) for the sync render hot path (applyOverrides, useStrokes, the dirty-tabs hook etc.) • localStorage `<key>:<projectId>` for tab-reload survival • /api/projects/:id/meta/{inspector-overrides,drawings} as the cross-browser source of truth, pushed with a 250ms debounce via the new projectMetaSync helper projectMetaSync (new ~125-line module) wraps the meta endpoint with: • debounced PATCH (collapses N rapid writes into one network round-trip) • If-Match conflict handling — re-fetch + retry once on 412 • silent failure on offline; in-memory state stays the truth until the next successful push reconciles Legacy migration: on first read of either module, if the workspace-wide legacy key still exists, attribute its contents to the active project and drop the legacy key. Cross-project route-name collisions get absorbed into whichever project happens to be active at that moment — known one-time loss; the legacy shape was already buggy. API surface stays unchanged — setOverride(route,…), addStroke(route,…), applyOverrides(doc,route), readRoute(route), useStrokes(route), useOverrideCount(route) all still exist with the same signatures. Internally they read getActiveProject() to scope the data per project. Net: 43/43 web tests still pass. Co-Authored-By: Claude Opus 4.7 <[email protected]>Configuration menu - View commit details
-
Copy full SHA for b030e7e - Browse repository at this point
Copy the full SHA b030e7eView commit details
This comparison is taking too long to generate.
Unfortunately it looks like we can’t render this comparison for you right now. It might be too big, or there might be something weird with your repository.
You can try running this command locally to see the comparison on your machine:
git diff main...feat/storage-driver-and-project-list