Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: aiatelie/ai-atelie
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: aiatelie/ai-atelie
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: feat/storage-driver-and-project-list
Choose a head ref
Checking mergeability… Don’t worry, you can still create the pull request.
  • 13 commits
  • 28 files changed
  • 2 contributors

Commits on May 6, 2026

  1. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    036de6b View commit details
    Browse the repository at this point in the history
  2. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    e40f675 View commit details
    Browse the repository at this point in the history
  3. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    37efdca View commit details
    Browse the repository at this point in the history
  4. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    856d923 View commit details
    Browse the repository at this point in the history
  5. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    26547aa View commit details
    Browse the repository at this point in the history
  6. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    f28f8ef View commit details
    Browse the repository at this point in the history
  7. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    d37805a View commit details
    Browse the repository at this point in the history
  8. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    4d063f7 View commit details
    Browse the repository at this point in the history
  9. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    c0e4d39 View commit details
    Browse the repository at this point in the history
  10. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    69cb5ab View commit details
    Browse the repository at this point in the history
  11. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    30e30f1 View commit details
    Browse the repository at this point in the history
  12. Configuration menu
    Copy the full SHA
    9f02ae9 View commit details
    Browse the repository at this point in the history
  13. 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]>
    whatiskadudoing and claude committed May 6, 2026
    Configuration menu
    Copy the full SHA
    b030e7e View commit details
    Browse the repository at this point in the history
Loading