Skip to content

Implement cross repo references#2

Merged
hlubek merged 14 commits into
mainfrom
worktree-cross-repo-refs
Jul 8, 2026
Merged

Implement cross repo references#2
hlubek merged 14 commits into
mainfrom
worktree-cross-repo-refs

Conversation

@hlubek

@hlubek hlubek commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

No description provided.

hlubek and others added 9 commits July 7, 2026 21:27
…cceptance

<repo-id>:<entry-id> refs now store verbatim and validate syntactically at
every parse/resolve site: excluded from local reverse indexes and dangling
checks, pass-through in short-ID resolution, accepted by --refs and the
engine typed store, with lifecycle edges (closes/supersedes) rejected at
the boundary. Dangling detection honors the forward-class exemption
(surfaces, required-by) per d-cpt-uh0. Config gains the committed repo_id
identity field.

Part of d-tac-i6f (cross-repo references, foundation layer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A cross-repo ref renders as an upstream leaf carrying the verbatim
<repo-id>:<entry-id> plus [unresolved: repo X] in the qualifier slot —
in sdd show trees (plain and styled), view expand(refs) sub-lines, and
the show envelope (verbatim refs). ShowTreeItem gains CrossRepoID and
NodeID() so a later resolved remote node reuses the same shape; the
RefExpansion row carries UnresolvedRepo the same way. MCP show/view
inherit through the shared finders and presenters.

Part of d-tac-i6f (cross-repo references, foundation layer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A backward-class cross-repo ref now produces a high-severity
cross-repo-ref-unresolved finding unless it resolves in the target
repo's cached graph; forward-class kinds (surfaces, required-by) are
exempt per d-cpt-uh0. The check takes a resolver seam — nil (no
connected-repos machinery) reports every repo unavailable; the
discovery layer wires the cache-backed, fetch-on-miss implementation.
Local ref resolution stays where it was: dangling local refs hard-block
at write-time validation before pre-flight runs.

Part of d-tac-i6f (cross-repo references, foundation layer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user-global config at $XDG_CONFIG_HOME/sdd/config.yaml (default
~/.config/sdd/config.yaml) holds connected repos {repo_id, clone_url}
plus the single global embedding config defining the shared vector
space. Caches are managed read-only clones at $XDG_CACHE_HOME/sdd/
<repo-id>/ with lazy clone, cooldown-gated pull (marker in .git/,
attempts not successes), forced pull, and declared-repo-id
verification for repo add. model.DeriveRepoID normalizes ssh/https
remote forms to the canonical host/path identity. Plain functions
throughout — handlers orchestrate and invalidate the GraphSource.

Part of d-tac-i6f (cross-repo references, discovery layer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…graphs

model.MultiGraph is the cross-graph read model: local graph plus member
graphs keyed by repo-id, loaded lazily through a loader closure wired
only inside the finder-owned GraphSource — CLI commands, MCP reads,
engine serves, and capture-time resolution all inherit the one seam.
Member graphs come from connected-repos caches as pure reads; a
not-connected repo resolves to the unresolved state while a corrupt
cache propagates its error.

Show trees now hop graphs at cross-repo edges: the walk continues in
the owning member graph with every node repo-qualified, dedup runs on
the (repo-id, entry-id) colon key, embedded entries dedup by bare ID so
exactly one copy surfaces, and supersede head-walks stay within the
owning graph with trails qualified for display. sdd show accepts a
<repo-id>:<entry-id> primary through the same path. Ref expansion, the
ref-kind applicability check, and the LLM ref-meta context all resolve
remote targets through the assembly.

Part of d-tac-i6f (cross-repo references, discovery layer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h-on-miss

sdd repo add clones the target's cache, verifies the repo declares the
identity its URL derives, and registers the connection; list/remove
manage entries and sync force-pulls (all or named). sdd init derives
repo_id from the origin remote — recorded on fresh init, upserted into
configs that predate the field, never overwriting a recorded value and
never user-choosable.

Capture now reads the live caches: referenced repos are refreshed
(lazy clone + cooldown pull) before the graph loads, and a
backward-class cross-repo target still missing from a connected cache
triggers one forced fetch and reload before pre-flight judges — so a
just-pushed remote entry doesn't false-block, while a genuinely absent
target still blocks. sdd show freshens the caches of any repos named
by colon-form IDs before reading.

Part of d-tac-i6f (cross-repo references, discovery layer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…x exclusion

sdd search gains --repo (repeatable, additive to local) and --all-repos;
finders.MultiSearch resolves member sources from the query graph's
cross-graph assembly and merges every hit into one list by comparable
score, remote hits repo-prefixed with status derived in the owning
graph. The side-effect half lives in Handler.PrepareCrossRepoSearch —
cache freshen plus per-repo index lazy-fill under the shared embedder
(user-global config wins for cross-repo queries), mirroring how local
lazy-fill precedes the search finder. Member indexes build at
<cache>/.index excluding embedded entries; member hits on embedded
entries are dropped so exactly one copy surfaces. A drifted member
index re-embeds on the next cross-graph search rather than being
excluded, and sdd lint reports the pending drift per repo.

sdd view gains the same flags, rendering the layout over each selected
repo's graph under a repo heading; the MCP search and view tools accept
equivalent repos/all_repos parameters, and MCP show freshens the caches
of colon-ID arguments — all served by the same finders as the CLI.

Part of d-tac-i6f (cross-repo references, discovery layer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mary

Two fixes from live end-to-end verification. RepoAdd now clones into a
staging dir first and establishes the identity from the repo_id the
target declares — canonical by design — moving the clone into its cache
location afterwards; a clone URL that derives an identity (ssh/https)
still must match the declared value, while local paths and offline
mirrors work without the cross-check. And sdd new gains --summary: a
caller-supplied summary stores verbatim and skips LLM generation
entirely, for callers that already hold a faithful summary or run
without a reachable LLM.

Part of d-tac-i6f (cross-repo references).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A caller-supplied Summary stores verbatim in the written entry's
frontmatter and the LLM runner is provably never consulted — the stub
runner fails the test on any call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread cmd/sdd/repo.go Outdated
Comment thread cmd/sdd/search.go Outdated
Comment thread internal/finders/graphsource_multigraph_test.go Outdated
Comment thread internal/finders/search_multi_test.go Outdated
Comment thread internal/mcpserver/tools.go Outdated
Comment thread internal/repos/cache.go Outdated
Comment thread internal/repos/cache.go Outdated
hlubek and others added 4 commits July 8, 2026 09:26
The character-identical collectors crossRepoIDsIn (CLI) and crossRepoIDsOf
(MCP server) collapse onto one pure function beside SplitCrossRepoID —
push-down and single-path per the PR #2 review (d-tac-fyd, finding 1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every git subprocess sdd runs now lives in internal/git behind the
consumer-defined interfaces (handlers.Committer/Brancher/Mover/Puller,
finders.GitSyncer): git.CLI implements them all, with the staged-deletion
commit variant on git.RemovalCommitter since it shares the Commit method
name. The loose helpers (repo root, user.name, remote get-url) move as
package functions, and the pathspec-scoping regression tests move with the
code. cmd/sdd keeps wiring only — a future (possibly partial) go-git swap
touches nothing but this package (d-tac-fyd, finding 3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
internal/repos stops reading XDG env vars as hidden globals: Locations
carries the config path and cache root, resolved once by
repos.DefaultLocations() at the composition roots (CLI helpers, sdd serve).
On top sit two abstractions following the read/write seam — a pure Registry
(connections, cache layout; injected into finders.Options and the search
finder) and a side-effectful Manager (clone, pull, config writes; wraps the
Registry plus a narrow Git interface implemented by internal/git; injected
into handlers.Options). Finder purity is now type-enforced: the read side's
dependency has no clone or pull. Config reads stay lazy per call so a
long-lived MCP server sees new connections without restart. Tests build
Locations over temp dirs — the XDG t.Setenv workarounds are gone except in
the test of DefaultLocations itself (d-tac-fyd, finding 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
populateIndexLint/populateRepoIndexLint were finder-shaped code in the CLI
layer: config → manifest → fingerprint drift counts. They now live beside
Finder.Lint as Finder.IndexLint, taking an IndexLintQuery (resolved
embedding config + index dir — flag merging stays the shell's only job);
the cross-repo section reads through the injected Registry with no CLI-side
wiring. The deliberate degrade-silently posture (lint never blocks on
absent index machinery) moves with the code (d-tac-fyd, finding 4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hlubek

hlubek commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Review round 1 addressed in four commits (recorded in the graph as directive d-tac-fyd, surfaced by evaluation s-tac-vyq):

  • 6458e34crossRepoIDsIn/crossRepoIDsOf unified as model.CrossRepoIDs beside SplitCrossRepoID (comments on cmd/sdd/repo.go:112, internal/mcpserver/tools.go:934).
  • 090b010 — new internal/git adapter package: all git execution behind the consumer-defined interfaces (handlers.Committer/Brancher/Mover/Puller, finders.GitSyncer), including the legacy exec code from cmd/sdd/main.go and sync.go; a future go-git swap is confined to one package (comment on internal/repos/cache.go:179).
  • b3e09bbinternal/repos reworked onto explicit Locations (XDG resolution only in repos.DefaultLocations(), called at composition roots), split into a pure Registry injected into finders and a side-effectful Manager (clone/pull/config writes, holds the git dep) injected into handlers — finder purity is now type-enforced. All XDG t.Setenv in the flagged tests replaced by temp-dir Locations (comments on cache.go:33 and both test files).
  • 4281cbapopulateIndexLint/populateRepoIndexLint moved into finders as Finder.IndexLint; the CLI keeps only flag→config resolution (comment on cmd/sdd/search.go:131).

Verified: full test suite, go vet, gofmt, golangci-lint clean; live two-repo fixture re-run against the rebuilt binary (repo add/list/sync/remove, capture with resolvable cross-repo ref, resolve-or-block on an absent target, cross-repo show, merged search --repo, per-repo view --all-repos, lint).

🤖 Generated with Claude Code

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hlubek hlubek merged commit dbd8013 into main Jul 8, 2026
3 checks passed
hlubek added a commit that referenced this pull request Jul 8, 2026
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