Skip to content

Hand the projects store its clock, ids and disk (#115) - #127

Merged
shovon merged 1 commit into
mainfrom
worktree-issue-115
Oct 28, 2026
Merged

shovon merged 1 commit into
mainfrom
worktree-issue-115

Conversation

@shovon

@shovon shovon commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Closes #115.

What changed

The store (app/src/main/features/projects/projects-store.ts) now takes { disk, now, newId, pathKey } beside the database:

  • disk is a Disk it declares itself, with only the three calls it makes: canonicalize, isDirectory and probe.
  • now dates an add and an open, and newId names a new project.
  • pathKey is the rule that makes two paths one directory. It comes from a pure pathKeyOn(platform), which replaces the old toPathKey that read process.platform.

The store no longer imports node:fs or node:crypto, and never calls new Date() itself.

The real disk (app/src/main/features/projects/disk/disk.ts, new) holds realpath, stat, and the probe with its 2-second timeout, all moved out of the store unchanged. It's the one place under projects/ that touches the filesystem.

Which project is open (open-project.ts): Registry loses checkAvailability. createOpenProject(registry, probe, onMoved) takes a Probe of a path, and requireProject probes the open project's own folder.

index.ts makes one createDisk() and one now, and hands them to the store, to createOpenProject (as disk.probe) and to the pages session. It also passes randomUUID and pathKeyOn(process.platform).

app/CLAUDE.md names the new signatures. It also adds the rule that a module declares only the part of a dependency it calls: the store's Disk and open-project's Probe are both met by the one disk.

For the reviewer

  • One behaviour change: before, requireProject checked availability by id, through the store. If the project had been removed between currentProject() and that check, the project was no longer found and the write was refused. Now it probes the project's path directly, so a write typed just before its project was removed lands in that project's folder. That's the same "lands where it was typed" rule requireProject already keeps when the open project changes. Nothing else about what is accepted or refused changes.
  • The projects:availability channel still goes through store.checkAvailability(ids), which now probes through the disk it was handed.
  • path.basename stays in the store. It's string handling, not I/O.

Tests

  • disk.test.ts (3 tests, new): canonicalizing through a symlink and .., a folder vs. a file vs. a missing path, and what the probe says for each.
  • projects-store.test.ts (4 existing tests kept on the real disk, 6 new on a disk that holds only what a test says):
    • exact ids and dates from the clock and ids the store was handed
    • pathKeyOn for darwin, win32 and linux, and a second spelling being "already tracked" on the first two and "added" on Linux, all on one machine
    • not-a-directory and unreadable
    • a share that doesn't answer reported as unknown with no 2-second wait, and an untracked id left out
    • relocating keeps the id and refuses a directory that is already tracked
  • open-project.test.ts: the stub registry now carries its own probe, which createOpenProject takes as a Registry all the same. One new test checks that requireProject probes the open project's folder and nothing else, and probes nothing when no project is open.
  • End to end, in a scratch instance (its own --user-data-dir and scratch folders), through window.pbnotes:
    • adding a project gave a real UUID and the current time
    • the same folder spelt in other case was already tracked (macOS)
    • a file and a missing path failed with the right reasons
    • opening set lastOpenedAt, availability said available, and show and a write went through requireProject
  • npm test (178 tests across all workspaces), npm run typecheck and npm run lint pass.

🤖 Generated with Claude Code

createProjectsStore read the clock and randomness itself (new Date() in
addProject and touchProject, randomUUID() for a new id), read
process.platform to fold case, and checked the filesystem directly. That
was against the rule that I/O is made at the composition root, and it
meant the tests could not pin an id or a date, or test the case rule for
any platform but the one they ran on.

The store now takes { disk, now, newId, pathKey } beside the database. It
declares the disk it needs as Disk: canonicalize, isDirectory and probe.
Case folding is a pure pathKeyOn(platform). The real disk is disk/disk.ts,
the one place under projects/ that touches the filesystem, made once in
index.ts beside one shared clock.

open-project.ts no longer asks the store for availability by id. It
declares a Probe of a path, and index.ts hands it disk.probe. A write
typed just before its project was removed now lands in that project's
folder instead of being refused, the same "lands where it was typed" rule
requireProject already keeps for a change of project.

New tests pin the id and the dates, check the case rule for macOS, Windows
and Linux on one machine, report an unreachable share as unknown without a
two-second wait, cover not-a-directory, unreadable and relocation on a disk
that holds only what a test says, check that requireProject probes the open
project's own folder, and check the real disk through a symlink and ..

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shovon
shovon merged commit d629d2a into main Oct 28, 2026
1 check passed
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.

Hand the projects store its clock, ids and filesystem checks

1 participant