Skip to content

feat: cross-namespace refs — materialized ccg:// links, federated reads, cross-repo analysis#31

Merged
tae2089 merged 10 commits into
mainfrom
feat/cross-namespace-refs
Jul 19, 2026
Merged

feat: cross-namespace refs — materialized ccg:// links, federated reads, cross-repo analysis#31
tae2089 merged 10 commits into
mainfrom
feat/cross-namespace-refs

Conversation

@tae2089

@tae2089 tae2089 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

What

Turns annotation-declared ccg:// references into a first-class cross-repository (cross-namespace) system, in three layers:

  1. Materialization — new cross_refs table (migration 000015, SQLite + PostgreSQL). Every @see ccg://{namespace}/{path}#{symbol} tag becomes a row with a symbolic target plus derived state (resolved_node_id, status: resolved|dead, source). A sync hook after every build/update rebuilds outbound rows and re-resolves inbound rows, so links survive replace-style rebuilds (webhook Replace: true regenerates node ids), go dead when the target symbol disappears, and recover when it returns. Resolution shares one matcher with ccg lint, so dead-ref findings and cross_refs status can never disagree.
  2. Federated readssearch, query_graph, list_graph_stats, search_docs accept namespaces: [] and fan out per namespace at the handler layer. Results are labeled per namespace; single-namespace responses are byte-identical to before. Store query paths keep their single-namespace invariant.
  3. Cross-repo analysisget_impact_radius / trace_flow accept cross_namespace: true: a namespace-agnostic reader merges resolved refs into traversal as synthetic cross_ref edges (never persisted), so the existing BFS/trace algorithms run unchanged across repo boundaries. New list_cross_refs tool exposes the repository-level dependency map (tool count 17 → 18).

Also fixes the Postgres integration suite, broken on main since 5404e24: the compose file's --allow-repo=*/* is rejected by that commit's multi-owner collision guard, so ccg-server refused to start. Pinned to testadmin/* (the suite's Gitea admin).

Why

Namespaces fully isolate per-repository graphs; the only cross-namespace primitive was list_namespaces. Cross-repo dependencies declared via @see ccg:// were plain tag text — not queryable, not traversable, no reverse lookup. Annotation refs were chosen over import-based linking because they are language-agnostic (polyglot service boundaries have no imports) and reuse the existing parser/lint/wiki infrastructure. The source column leaves room for future import-derived signals without schema rework.

Reviewer notes

  • Commits are split Tidy First: one structural refactor (shared ref matcher extraction), then three behavioral commits (materialization → federated reads → analysis), docs, and the integration fix.

  • cross_refs is derived state: fully rebuildable from annotations, deleted/re-created per source namespace inside one transaction. No raw SQL outside migration files; both backends get the same 000015.

  • The synthetic cross_ref edge kind never reaches the edges table; the flow tracer accepts it as a continuation only because regular stores never emit it.

  • query_graph federation isolates per-namespace node not found as a per-namespace error entry instead of failing the whole call; file_summary still returns a zero summary for missing files (pre-existing behavior).

  • The integration suite gained Phase 13: a real webhook push of a ccg:// annotation, then list_cross_refs (outbound/inbound), cross_namespace: true impact with namespace labels, default-scoped impact, and federated search — all verified against PostgreSQL. Banner now reports 16/18.

  • Known tradeoff (reviewed, deliberately kept): cross-ref sync runs post-commit and fails hard. A sync failure after a committed build makes build_or_update_graph report total failure (skipping FTS/flow postprocess and cache flush) and skips webhook cache invalidation. Accepted because the failure is rare, cross_refs is self-healing derived state (webhook retry / cache TTL / next build), and softening it would change the Build/Update result contract late in the PR. Revisit if sync failures are observed in practice.

Verification

  • CGO_ENABLED=1 go test -tags "fts5" ./... -count=1 — all 43 packages green. Includes an end-to-end test: Go source comment @see ccg://… → tree-sitter build → resolved cross_refs row → target rebuild → id remap / dead / revive.
  • ./scripts/integration-test.sh (Gitea + PostgreSQL + ccg Docker) — PASSED after the allow-repo fix: webhook push → clone → build → Postgres store pipeline, migrations incl. 000015, MCP tool phases.

🤖 Generated with Claude Code

tae2089 and others added 10 commits July 19, 2026 10:54
Pull the node-matching query into ccgRefNodeQuery so lint validation and
the upcoming cross-ref materialization judge ccg:// targets identically.
No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a cross_refs table (migration 000015, SQLite and PostgreSQL) that
stores every @see ccg://{namespace}/{path}#{symbol} annotation tag as a
queryable row with a symbolic target plus derived resolution state
(resolved_node_id, status resolved|dead, source annotation).

A new crossref.Service syncs the table after every ingest commit:
outbound rows are rebuilt from the namespace's current doc tags and
inbound rows are re-resolved because replace-style builds regenerate
node ids. The sync hook is optional on workflow.Service and wired in
CLI build/update, the MCP build tool, and the webhook server runtime.

Resolution reuses the matcher shared with lint (ccgRefNodeExists) so
dead-ref findings and cross_refs status never disagree. Namespace-scope
refs resolve without a node target; path-scope refs prefer the file
node; malformed refs are skipped (lint owns reporting).

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

Add an optional 'namespaces' array parameter to the four read tools.
Federation fans out per namespace at the handler level so every store
query path keeps its single-namespace invariant:

- search merges per-namespace candidate pools, reranks once, and labels
  each item with its namespace (single-namespace responses are
  byte-identical to before).
- query_graph runs the extracted per-namespace body for each namespace
  and groups outcomes; a missing target in one namespace becomes a
  per-namespace error instead of failing the call.
- list_graph_stats and search_docs return per-namespace groups without
  merging counts across unrelated graphs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a namespace-agnostic CrossNamespaceReader that reads nodes and
edges by globally-unique id and merges resolved cross_refs rows into
traversal as synthetic cross_ref edges. Existing BFS and flow-trace
algorithms run unchanged on top of it.

- get_impact_radius and trace_flow accept cross_namespace: true and
  traverse resolved ccg:// refs in both directions; impact results
  carry a namespace label in cross mode only.
- New list_cross_refs tool exposes the repository-level dependency map
  (direction outbound/inbound/both, optional status filter). MCP tool
  count goes from 17 to 18.
- The synthetic cross_ref edge kind is never persisted; the flow tracer
  accepts it as a continuation because regular stores never emit it.

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

Update English and Korean guides (mcp-tools, annotations, architecture,
counts in README/development/runtime-layout), CLAUDE.md, and the
ccg-analyze / ccg-namespace skills for the new cross-namespace surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove leftover // test comment after the import block in
internal/app/analyze/ports.go that failed gofmt's blank-line
separation. Comment-only change; gofmt now passes and
internal/app/analyze tests are green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The multi-owner collision guard added in 5404e24 rejects wildcard
owners in --allow-repo, so the integration compose file's '*/*' rule
made ccg-server refuse to start and the suite fail before any test
phase. Pin the rule to the suite's Gitea admin account (testadmin/*)
and ignore the suite's default artifacts/ output directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Phase 13: push a ccg:// annotation from sample-go targeting
sample-calc through the real Gitea webhook pipeline, then assert
list_cross_refs (outbound resolved + inbound), cross-namespace impact
radius with namespace labels, default namespace-scoped impact, and
federated search labeling — all against PostgreSQL.

The phase polls list_cross_refs directly because wait_for_webhook_sync
returns as soon as a namespace has any nodes, which is immediate on a
re-push. Banner updated to 16/18 verified tools.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The caller's limit was applied to the globally reranked merged pool, so
one namespace with high-scoring hits could push every other namespace
out of the response entirely. Rank globally without truncation, then
select with a per-namespace quota (limit/namespaces, minimum one) and
fill the remaining slots in global rank order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flow memberships were stamped with the start namespace, so members
reached through a cross_ref edge carried an unusable label and clients
could not resolve foreign node ids. The tracer now batch-loads member
nodes when the reader supports it and stamps each membership with the
node's stored namespace; the MCP response exposes it only in
cross-namespace mode, keeping single-namespace payloads unchanged. The
persisted-flow rebuild path is unaffected because FlowRebuildStore has
no batch node read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tae2089
tae2089 merged commit b6dba0f into main Jul 19, 2026
1 check passed
@tae2089
tae2089 deleted the feat/cross-namespace-refs branch July 19, 2026 02:44
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