Skip to content

feat(learner): collapse cross-OS duplicate projects in the registry#22

Open
Sergio-LPA wants to merge 1 commit into
Luispitik:mainfrom
Sergio-LPA:feat/crossos-registry
Open

feat(learner): collapse cross-OS duplicate projects in the registry#22
Sergio-LPA wants to merge 1 commit into
Luispitik:mainfrom
Sergio-LPA:feat/crossos-registry

Conversation

@Sergio-LPA

Copy link
Copy Markdown

Problem

The per-project id is sha256(remote || root). For a project with no git remote, the id is derived from the root — which differs per OS:

  • macOS/Linux: /Users/me/Proj
  • Windows: C:/Users/Me/Proj

So the same project gets a different id on each machine. When the registry (_sinapsis-projects.json) is shared across machines — e.g. a Nextcloud/Dropbox/iCloud-synced ~/.claude — that one project shows up as two entries, duplicating it in /projects, /eod, and cross-project instinct search.

Projects with a remote are unaffected (the remote is identical across machines, so the id already matches).

Fix

Match on a cross-OS-stable key instead of the raw id: the git remote when present, otherwise the project name. The two sightings then collapse into a single entry:

  • each per-OS id is preserved in aliases[] (so future sessions resolve without a re-scan)
  • each observed root is stored under roots.{posix,windows}
  • the remote key takes precedence over the name, so two genuinely different projects that happen to share a folder name never merge

The change is purely additivealiases, roots and crossKey are new optional fields; existing readers that don't use them are unaffected. Behaviour for remote-backed projects is unchanged.

Tests

New tests/test-crossos-registry.sh — 5 hermetic tests driving the real hook via a sandbox HOME:

  1. two-OS, no-remote sighting collapses into one entry
  2. both roots.posix and roots.windows recorded
  3. the second per-OS id registered as an alias
  4. remote key beats name (same-named distinct projects stay separate)
  5. idempotent on re-run

Existing suites re-run clean:

  • test-install-upgrade 21/21
  • test-registry-isolation 4/4
  • test-eod-gather 8/8

Notes

  • The name-based fallback could, in theory, merge two different projects that share a folder name across machines and have no remote on either. The remote key takes precedence to minimise this; documented in the code comment.
  • Context: surfaced on a real Mac + Windows setup syncing ~/.claude via Nextcloud, where non-git folders (the same logical project) appeared twice in /eod. Follow-on to the v4.5.1 cross-OS /eod work.

The per-project id is sha256(remote || root). A project with no git remote
therefore gets a DIFFERENT id on each OS (its root differs: /Users/me/Proj
vs C:/Users/Me/Proj), so with a shared registry (e.g. a synced folder) the
same project appears twice — duplicated in /projects, /eod and cross-project
instinct search.

Match on a cross-OS-stable key instead: the git remote when present, else
the project name. The two sightings collapse into one entry — each per-OS id
kept in aliases[], each observed root under roots.{posix,windows}. Projects
with a remote already shared one id and are unaffected; the remote key beats
the name so two distinct same-named projects never merge.

Purely additive: aliases, roots and crossKey are optional fields ignored by
readers that don't use them.

New tests/test-crossos-registry.sh (5 hermetic tests). Existing suites pass:
test-install-upgrade 21/21, test-registry-isolation 4/4, test-eod-gather 8/8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@Luispitik Luispitik left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diagnosis is right and in scope: project ids are sha256(remote || root), so a no-remote project gets a different id per OS and duplicates in synced registries. The diff applies clean on v4.6.1 and the new code is safe (no eval, execFileSync with args). But I reproduced three functional problems by running the patched hook, so this needs another iteration:

  1. Same-machine false merge (regression, data loss). Two different no-remote projects that share a folder name on the same machine (think clientA/scripts and clientB/scripts) collapse silently into one entry, and one root disappears from the registry (roots.windows overwritten — same os-family). Reproduced with the real hook. /eod resolves roots by name, so it would attribute git commits to the wrong project. Fix: only collapse by name when the os-family actually differs, e.g.
    registry.projects.find(p => p && p.crossKey === crossKey && p.root && projectRoot
      && osFamily(p.root) !== osFamily(projectRoot))
  2. Pre-existing duplicates never merge. The lookup matches by id first, so a registry that already contains both entries (the synced-via-Nextcloud scenario that motivates this PR) keeps both forever — each machine just updates its own entry. Reproduced. Add a post-upsert migration pass that merges entries with identical crossKey (union of aliases/roots, oldest created, newest last_seen) — or hook it into _dream.sh hygiene. As written, the PR only prevents future duplicates from a clean registry; it doesn't cure the reported symptom.
  3. The new test fails on Windows Git Bash (suite exit 1; 1 of the 5 cases): MSYS converts the posix fixture path /Users/tester/CrossProj passed as argv into C:/Program Files/Git/Users/..., so roots.posix never registers. Export MSYS_NO_PATHCONV=1 / MSYS2_ARG_CONV_EXCL='*' at the top of the test, or pass fixture paths via env vars instead of argv. Also drop the || r.projects[0].id==='bbbbbbbbbbbb' clause in test 3 — as written it passes even when the posix run was never processed, which is exactly what happens on Git Bash today.

Please also:

  • Add an anti-regression test: two same-name no-remote projects with different roots in the same os-family must stay 2 entries.
  • Teach the consumers about aliases: commands/projects.md (step 3) should also count observations from homunculus/projects/{alias}/; _eod-gather.sh should register aliases in its registry[] map (for (const a of p.aliases||[]) registry[a] = {name:p.name, root:p.root}).
  • Document the known asymmetry in a code comment: if remote detection fails on one machine (git not on PATH for execFileSync), that sighting falls back to a name: crossKey and won't match the remote: entry — duplicate persists.

Good direction — with the cross-OS restriction and the migration pass this becomes a clean merge. Windows Git Bash compatibility is non-negotiable in this repo (it's where most users hit bugs).

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.

2 participants