Resolves#46. Reported by @mrsuit92.
Python projects where each top-level directory is a runnable application
root (a common service-style monorepo layout) had `import config` from
`service-a/main.py` produce 0 dependency edges, even when
`service-a/config.py` sits next to the importer. At runtime Python
resolves this correctly because the importer's directory is sys.path[0]
when the file is run as `python main.py` from inside its own directory.
The static resolver did not check that path.
The Python case in graph-resolution.ts only tried:
<projectPath>/<module>.py
<projectPath>/src/<module>.py
<projectPath>/lib/<module>.py
It did not try `<sourceDir>/<module>.py`, so non-relative sibling
imports never resolved. Relative imports (`from .config import ...`)
already used sourceDir and worked.
Fix: add `<sourceDir>/<module>.py` as the LAST fallback, after the
existing project-root and src/lib checks. Tried last to preserve
project-root precedence, so any layout that resolved before this PR
continues to resolve to the same file. resolveRelativePath also handles
the `<sourceDir>/<module>/__init__.py` package case via its built-in
Python init fallback, so package-style sibling imports work too.
Tests: 5 new cases in tests/unit/graph-resolution.test.ts covering
sibling-flat resolution, dotted module paths, package via __init__.py,
project-root precedence preservation, and the negative case (no match
anywhere). Existing 730 tests continue to pass; total now 735.
typecheck, biome, and CodeRabbit local review all clean.
Co-authored-by: mrsuit92 <mrsuit92@users.noreply.github.com>
Resolves#43.
On Linux/Node combinations where one ast-grep grammar package's prebuilt
parser binary is missing for the host architecture, the v1.8.3 loader
silently failed to register every dynamic grammar in the batch, not just
the broken one. registerDynamicLanguage iterates and accesses each
module's lazy libraryPath getter; one throwing getter aborts the call
atomically and zero grammars end up registered.
Fix: pre-validate each grammar's libraryPath getter inside the per-
grammar try/catch so a missing prebuild is contained to that grammar.
Build the batch object with only the survivors and make ONE atomic
registerDynamicLanguage call. Standard environments are unaffected
because all grammars pass pre-validation. Affected environments lose
only the unloadable grammar, the rest register cleanly.
Also captures the actual error reason (the previous empty `catch {}`
discarded it), bumps symbol- and import-extraction failure logs from
debug to warn with one-shot dedupe per language, exposes loaded/failed
grammars via a new getDynamicLanguageStatus() API, and renders an "AST
grammars" block in codebase_graph_status output so users see loader
state without enabling debug logging.
Empirically verified against @ast-grep/napi@0.40.5 in a clean Node
environment. Two probes confirmed the napi semantics: sequential
register({A}); register({B}) calls are REPLACING (so per-language
registration is broken), and batch register with one bad getter is
ATOMIC (so pre-validation before the batch call is the only correct
pattern). All 721 existing unit tests continue to pass unchanged. Adds
9 new tests for the loader status API; total 730 pass. typecheck and
biome clean. CodeRabbit returned no findings on this diff.
Co-authored-by: X-Adam <X-Adam@users.noreply.github.com>
LM Studio's Local Server speaks the OpenAI-compatible /v1/embeddings
protocol, so users running it as their model host (chat plus embedding in
one desktop app, GGUF model management) had no clean integration path.
Changes:
- src/services/provider-lmstudio.ts: new LMStudioEmbeddingProvider wrapping
the OpenAI SDK with a custom baseURL (default http://localhost:1234/v1).
Sends a placeholder API key to satisfy the OpenAI SDK while LM Studio's
Local Server runs without auth by default. Skips the dimensions parameter
because LM Studio models have no Matryoshka projection. Forces
encoding_format=float to defeat the OpenAI SDK 6.x base64 default, which
would otherwise mangle LM Studio's plain-array responses into 1024 zeros.
- src/services/embedding-config.ts: extends the EmbeddingProvider union,
reads LMSTUDIO_URL and LMSTUDIO_API_KEY, fail-fast validation when
EMBEDDING_PROVIDER=lmstudio without EMBEDDING_MODEL or EMBEDDING_DIMENSIONS.
- src/services/embedding-provider.ts: factory case for lmstudio with a
dynamic import to avoid loading the OpenAI SDK at startup for ollama users.
- ensureReady distinguishes "LM Studio unreachable" from "reachable but
embedding model not loaded" so the operator knows whether to start the
Local Server or load the configured model.
- src/services/qdrant.ts: minor refactor to extract the hybrid-search query
payload to a local const for readability.
- README.md: dedicated LM Studio section, MCP host config example, env-var
table entries.
- tests/unit/embedding-config.test.ts: 8 new cases (required-env validation,
URL default and override, optional API key, context-length override).
- tests/unit/embedding-provider.test.ts: 3 new cases (factory wiring,
ensureReady error format against a closed port, healthCheck unreachable
output).
Backward compatible. The lmstudio provider is opt-in via
EMBEDDING_PROVIDER=lmstudio. Existing ollama, openai, and google paths are
untouched.
Address CodeRabbit findings on PR #36:
1. The previous patch attached `QDRANT_API_KEY` as an `api-key` header
regardless of URL scheme, which would leak the secret on the wire if
a user configured an authenticated Qdrant over plain HTTP. Add a
guard that rejects the combination and throws a specific error,
placed before the readiness probe so its message is not masked by
the generic "Cannot reach external Qdrant" handler. Loopback URLs
(`localhost`, `127.0.0.1`, `[::1]`) are accepted on `http://` so
local-dev workflows where users run authenticated Qdrant on plain
HTTP keep working. The URL is parsed (rather than checked with
startsWith) so hostnames like `http://localhost.evil.com` are not
mistaken for loopback.
2. The "omits api-key header when QDRANT_API_KEY is not set" test
relied on the spread of the real `constants.js` module, which means
it would flake to a header-attached state on any developer machine
with `QDRANT_API_KEY` exported in the shell. Pass
`QDRANT_API_KEY: undefined` explicitly so the override always wins.
3. Add two tests covering the new guard: one asserting the rejection
on plain HTTP for non-loopback hosts, and one asserting that the
localhost exception still attaches the api-key header.
4. Document the HTTPS requirement (and localhost exception) in the
`QDRANT_API_KEY` row of the README configuration table.
External Qdrant deployments that require authentication (notably Qdrant Cloud,
which returns 403 on every endpoint including /healthz without an api-key
header) cannot pass the existing readiness check, so codebase_index fails with
"Cannot reach external Qdrant" even though codebase_health and codebase_search
still succeed -- their qdrant-js client paths attach the key automatically.
Forward QDRANT_API_KEY as an `api-key` request header from
ensureExternalQdrantReady() by extending waitForService() with an optional
RequestInit. Docker-managed and unauthenticated external Qdrant deployments
remain unaffected (no init forwarded -> no headers).
Covered by two new unit tests in docker.test.ts.
The previous regex `^namespace\s+([\w.]+)` was anchored at column 0, so
nested namespace declarations (the `namespace Inner` block inside
`namespace Outer { ... }`) were silently missed. The capture group also
accepted invalid identifiers like `1Foo` and matched on stray
occurrences of the word `namespace` not followed by `;` or `{`.
The new regex `^\s*namespace\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*(?=[;{])`:
- `^\s*` allows leading whitespace, so indented (nested) declarations
are captured.
- The dotted-identifier capture requires each segment to start with a
letter or underscore, matching C# identifier rules.
- `(?=[;{])` requires a real terminator, filtering out spurious matches.
Adds three regression tests: nested-namespace capture, rejection of
digit-leading identifiers, and the terminator-lookahead behaviour.
Found by CodeRabbit on PR #34 (nitpick).
`buildCsNamespaceMap` previously iterated `fileSet` in fs.readdir()
traversal order, which POSIX does not guarantee. Combined with the
`candidates[0]` selection in `resolveImport`, this meant a `using`
directive could resolve to different files on different machines or
runs.
Sort the `.cs` paths lexically before scanning so candidate lists are
stable. Adds a regression test that builds the map twice from sets
populated in different orders and asserts both produce the same
candidate sequence and the same first candidate.
Found by CodeRabbit on PR #34.
C# imports previously always returned null because there is no direct
filename mapping for `using X.Y.Z;`. As a result `codebase_graph_query`
reported zero edges for C# projects, and the symbol-level tools
(impact, flow, symbol) silently lost cross-file resolution because they
walk the file-import graph for Tier 2/3 candidates.
Build a namespace-to-files map once per graph build by regex-scanning
every `.cs` file for `^namespace X.Y.Z` (covers both block-scoped and
file-scoped C# 10+ syntax) and consult it from the csharp branch of
resolveImport. When a namespace spans multiple files the first
candidate is returned; multi-file fan-out is tracked as a follow-up.
External namespaces (System.*, Microsoft.*) are still filtered upstream
by isExternalModule, so the map is only consulted for project-internal
names.
Cost is O(n) reads at graph-build time, gated by a `hasCs` precheck so
non-C# projects pay nothing.
Adds 12 unit tests covering block-scoped, file-scoped, multi-file,
multi-namespace-per-file, commented-out lines, non-.cs files, empty
projects, unknown namespaces, and external (System.*) filtering.
Symbol view (full rebuild)
──────────────────────────
Replaces the unusable "show all symbols at once" mode with a focus
graph (SourceTrail / IntelliJ pattern):
- Landing state shows the alphabetical list of all symbols in the
sidebar; canvas shows a "pick a symbol" overlay constrained to the
canvas area (right: 340px) so the list stays visible.
- Three entry paths to seed: list click, search bar, or symbol click
from a file's sidebar in Files view.
- Once seeded, canvas shows the symbol + its 2-hop callers/callees
neighbourhood (auto-falls back to depth 1 if > 60 nodes).
- Clicking any neighbour re-centres on it. Seed has a distinctive
orange ring + always-visible label so the anchor is obvious.
- "← Back to symbol list" link in the sidebar restores the empty
state — explicit way out instead of relying on accidental
empty-canvas clicks.
- Light file-grouping: each symbol's border colour is a stable hash
of its file path; symbols from the same file share a colour.
Layout dropdown clean-up
────────────────────────
Removed Force-directed (cose). Force-directed is the wrong algorithm
for code dependency graphs — hub clusters collapse, orphans fly off,
labels overlap, no tuning fixes the underlying shape mismatch. Tried
fcose as a substitute; same fundamental problem on dense code graphs.
Default is now Dagre TB; remaining options are Concentric, Breadth-
first, Grid, Circle — all deterministic.
UX polish
─────────
- autoungrabify: true — disables single-click drag, fixes "node jumps
on click" (trackpad clicks always have some motion which Cytoscape's
default interprets as a node grab).
- Tap-to-highlight neighbourhood — clicking any node highlights its
direct neighbours and fades the rest. Less aggressive than the
transitive blast-radius / call-flow buttons in the sidebar.
- Zoom-bound label visibility — labels hidden below zoom 0.55 so 100+
node graphs aren't a soup of overlapping text. Selected / highlighted
nodes always show their label.
- Layout-position persistence — switching Files ↔ Symbols and back
preserves Files-view positions instead of re-running the layout.
TDZ regression test
───────────────────
New tests/unit/viewer-app.test.ts runs the bundled viewer-app.js in a
sandboxed node:vm context with mocked DOM + Cytoscape, triggering
every function-typed style closure on a fake element. Catches TDZ
("Cannot access X before initialization") and other reference errors
at unit-test time. Would have caught both the prior `cy` and
`LABEL_ZOOM_THRESHOLD` ordering bugs before they reached the browser.
Stats-line consistency
──────────────────────
graph-visualize-html.ts — top bar now displays embedded counts
(sym.symbols.length / sym.symbolEdges.length) instead of meta counts.
Previously the top bar said "630 symbols" while the sidebar's "All
symbols" said "722" because meta excludes synthetic <module>
placeholders. Capped mode keeps meta counts with a "(capped)"
qualifier since the embedded set is empty there.
Quality gates
─────────────
- Biome lint: clean
- TypeScript (tsc): clean
- Unit tests: 687/687 (incl. the new viewer-app.js evaluation test)
String.prototype.replace with a string replacement argument interprets
`$&`, `$'`, `` $` ``, and `$$` as special tokens (match, post-match,
pre-match, literal-$). The vendored bundles contain the classic
regex-escape idiom `"\\$&"` (`cytoscape.min.js` and `dagre.min.js` each
have one), which the asset-injection pipeline was corrupting into e.g.
`"\\{{CYTOSCAPE}}"` in the generated HTML. The visible symptom was a
broken Cytoscape search-box escape path whenever a user typed a regex
metacharacter into the live-search input.
Fix: convert the six asset/data replacements in buildInteractiveGraphHtml
to function-replacer form (`() => assets.cytoscape`), which uses the
returned string literally with no `$`-token interpretation. Same class
of bug would have affected embedded JSON had a file path or symbol name
contained `$&` — now neutralised for the whole chain.
Regression test added: asserts the `\\$&` idiom survives verbatim in the
rendered HTML and that neither `\\{{CYTOSCAPE}}` nor `\\{{DAGRE}}`
appears. A single `expect(html).toContain('"\\\\$&"')` catches the bug.
Quality gates (all green):
- Biome lint: clean
- TypeScript (tsc --noEmit): clean
- Unit tests: 686/686 (was 685; +1 regression test)
- CodeRabbit: No findings
- Snyk: 0 issues
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Interactive Viewer (primary)
────────────────────────────
codebase_graph_visualize now accepts mode="mermaid" (default, existing
behaviour — text Mermaid diagram) or mode="interactive". Interactive
mode generates a self-contained HTML page and opens it in the user's
default browser via the `open` npm package (cross-platform: macOS,
Linux, Windows). Cytoscape.js 3.30.2 + Dagre 0.8.5 + cytoscape-dagre
2.5.0 are vendored under src/assets/ — no CDN, works offline.
Features:
- File view — every source file as a node, imports as edges, language
colour-coded, circular deps highlighted in red.
- Symbol view toggle — functions/classes/methods as nodes with call
edges (confidence-styled). Embedded when the symbol graph fits under
20k symbols / 60k call edges; above that threshold the file view
remains usable and a banner directs users to codebase_impact /
codebase_symbols for symbol-level queries.
- Sidebar on node click — imports, dependents, per-file symbol list
(first 30 + link to codebase_symbols), action buttons for blast
radius and call flow.
- Right-click any node → blast radius overlay (reverse-transitive
closure). Call-flow button on the sidebar for forward traversal.
- Live search across files and symbols, six Cytoscape layouts
(Dagre / force / concentric / breadth-first / grid / circle),
PNG export (filename sanitised for cross-platform safety).
- `open: false` parameter skips auto-launch and just returns the file
path — useful in headless environments.
Viewer is XSS-safe by construction: all DOM built with createElement
+ textContent (no innerHTML anywhere); embedded JSON escapes every
"<" as \u003c so a stray </script> in a file path or symbol name
cannot break out of the script-type="application/json" container.
New files:
- src/assets/{cytoscape.min.js,dagre.min.js,cytoscape-dagre.js,
viewer-template.html,viewer-styles.css,viewer-app.js}
- scripts/copy-assets.mjs — postbuild copier (tsc does not handle
non-TS files); wired into npm run build and prepublishOnly
- src/services/graph-visualize-html.ts — HTML builder with scale-cap
logic (MAX_SYMBOLS / MAX_EDGES / MAX_SYMS_PER_FILE) and parallel
per-file Qdrant payload loading
- src/services/graph-visualize-browser.ts — temp-file write +
cross-platform open wrapper
- tests/unit/graph-visualize-html.test.ts — 5 tests (self-contained,
escape-safety, symbolMode omitted/capped, cycle marking)
- tests/unit/graph-visualize-browser.test.ts — 4 tests (deterministic
path, overwrite, success + failure paths)
New runtime dependency: open@^10.2.0 (Sindre Sorhus, zero transitive
deps, cross-platform).
British-English doc sweep (secondary)
─────────────────────────────────────
Switched all project docs to British English spelling:
behavior → behaviour organized → organised
color-coded → colour-coded initialization → initialisation
visualization → visualisation customization → customisation
recognized → recognised optimized → optimised
acknowledgment → acknowledgement finalize → finalise
analyzing → analysing apologizing → apologising
sexualized → sexualised
Affected files: README, DEVELOPER, AGENTS, CLAUDE, GEMINI, SECURITY,
CONTRIBUTING, CODE_OF_CONDUCT, agents/codebase-explorer.md,
skills/codebase-exploration/{SKILL.md,references/tool-reference.md},
skills/codebase-management/references/tool-reference.md.
Also surfaced Impact Analysis in the top-level README paragraph.
Docs
────
- README: "Interactive graph explorer" subsection under Impact Analysis,
tool-table row updated.
- DEVELOPER.md: architecture section under codebase_graph_visualize
covering asset layout, data flow, cap logic, XSS-safety invariants.
- AGENTS.md / CLAUDE.md / GEMINI.md: new "User asks for a visual /
interactive / shareable graph" row in the tool-routing table.
- skills/codebase-exploration/: SKILL.md bullet + tool-reference.md
full mode description.
- CHANGELOG.md: "Interactive Graph Explorer" section under Unreleased.
Quality gates (all green)
─────────────────────────
- Biome lint: clean
- TypeScript (tsc): clean
- Unit tests: 685/685
- Integration tests: 154/154 (real Qdrant + Ollama)
- CodeRabbit: No findings (1 fix applied — filename sanitisation)
- Snyk code test: 0 issues
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the four reviewer-flagged gaps from the previous round:
1. **Phase F wired into the watcher / `codebase_update`.**
`rebuildGraph(path, { skipSymbolGraph: true })` now exposes a
file-import-only build mode. `services/indexer.ts` calls it +
`updateChangedFilesSymbolGraph(...)` when meta exists AND ≤ 50 files
changed (`INCREMENTAL_SYMBOL_THRESHOLD`); falls back to full rebuild
above that. Measured speedup on a 1000-file synthetic repo: full
rebuild 6.55 s → Phase F single-file update 197 ms (~33×).
2. **Real end-to-end scale test.**
New `tests/integration/symbol-graph-scale.test.ts` generates 1000
synthetic Python files × 20 symbols/file (20k symbols) against a real
Qdrant, asserts (a) full rebuild within budget, (b) cold listSymbols /
getImpactRadius queries within budget, (c) Phase F update ≥ 4× faster
than full rebuild. `SCALE_LARGE=1` pushes to 10k files / 200k symbols.
3. **Smoke benchmark numbers captured.**
New `scripts/benchmark-graph.ts` runs `rebuildGraph` against any
target dir and emits JSON + a Markdown row. Numbers for SocratiCode
itself (82 files / 571 symbols / 9914 call edges / 0.90 s / 167 MB
RSS) and the synthetic 1000-file repo are now in DEVELOPER.md
§ "Real-world benchmark numbers".
4. **Logger test flake fixed.**
`services/logger.ts` exposes `setLogLevel` / `getLogLevel`;
`tests/unit/logger.test.ts` pins the level in beforeEach and restores
in afterEach. Verified deterministic with `SOCRATICODE_LOG_LEVEL=debug`
set in the shell environment.
### Bug discovered + fixed by the new benchmark
Running `scripts/benchmark-graph.ts` against SocratiCode itself crashed
the symbol graph build with `TypeError: existing.push is not a function`.
Root cause: shard maps used `shard[name]` bracket access on a plain
`{}`, which returned `Object.prototype.constructor` (a function) for
common method names like `constructor`, `toString`, `hasOwnProperty`.
Fixed by guarding all reads with `Object.hasOwn` in
`services/code-graph.ts` and `services/symbol-graph-incremental.ts`.
Added a regression test in
`tests/integration/symbol-graph-incremental.test.ts`.
### QA
- Biome lint: clean (auto-fixed 1 file).
- VS Code Problems panel: clean.
- Unit tests: 676/676 pass (29 files); reproducible.
- Integration tests touched: 45/45 pass (incremental, scale,
indexer, code-graph).
- CodeRabbit review: no findings.
- Snyk Code: 0 issues.
### Doc updates
- DEVELOPER.md: removed "watcher still triggers full rebuild" wording,
added "Real-world benchmark numbers" subsection with measured table.
- CHANGELOG.md: removed "Known Limitations" block; added new
Bug Fixes entries (prototype keys, logger flake) and a Performance
entry for the wired Phase F path with measured numbers.
Addresses six gaps in the prior Impact Analysis work:
1. **Scale benchmarks** (was missing): tests/unit/symbol-graph-scale.test.ts
exercises sharding/hashing at 10k–100k symbol volumes with loose
regression thresholds (>10× slowdown to fail).
2. **Per-language symbol-extraction tests** (was ~15% of plan):
tests/unit/graph-symbols.test.ts now covers Rust, Java/Kotlin/Scala (JVM),
C#, C/C++, Ruby, PHP, Swift, Bash, and the regex fallback path.
3. **Phase F (per-file incremental updates)** — implemented as
src/services/symbol-graph-incremental.ts with
updateChangedFilesSymbolGraph(). Re-extracts changed files, diffs against
persisted payloads via contentHash, patches only affected name (≤27) and
reverse-call (≤256) shards, and updates meta counts incrementally.
Integration tests in tests/integration/symbol-graph-incremental.test.ts.
Watcher wiring is documented as a follow-up in CHANGELOG (still does
full rebuild on save).
4. **Integration tests for the four new MCP tools**: codebase_impact,
codebase_flow, codebase_symbol, codebase_symbols added to
tests/integration/tools.test.ts.
5. **Symbol-graph store unit tests**: tests/unit/symbol-graph-store.test.ts
covers nameShardKey, allNameShardKeys, reverseShardKey, reverseShardHex,
contentHashOf — 14 tests.
6. **Bug fix**: Java/Kotlin/Swift/Scala silently failed because ast-grep
throws 'Invalid Kind' when a queried node-kind doesn't exist for that
grammar (e.g. object_declaration is Kotlin-only). The outer try/catch in
extractSymbolsAndCalls swallowed the error and returned only <module>.
Fixed via safeFindAll wrapper applied to all 36 call sites in
src/services/graph-symbols.ts.
Also fixes a CodeRabbit-flagged comment/code mismatch in
src/services/graph-entrypoints.ts (now actually checks for it/describe
test names as the comment claimed).
Tests: 676 unit pass. Lint clean. CodeRabbit clean. Snyk clean.
- 4 new unit test files (22 tests) for graph-symbols, graph-symbol-resolution,
graph-entrypoints, symbol-graph-cache. All 799 tests pass.
- README/AGENTS/CLAUDE/GEMINI: document codebase_impact, codebase_flow,
codebase_symbol, codebase_symbols and updated workflow guidance.
- DEVELOPER.md: new architectural section for symbol-level call graph,
including sharded storage layout and resolution confidence levels.
- CHANGELOG: Unreleased section for the impact-analysis feature.
- Apply CodeRabbit fixes: dedupe in-flight cache loads, set ImpactResult.truncated
when frontier extends beyond depth limit, drop redundant String() around Lang enum.
- Switch reverse-shard hash from SHA1 to SHA256 (Snyk hardening; non-cryptographic use).
1. Global config fallback: relative artifact paths are now resolved against
the global config directory (not the project root) when loading from fallback.
Absolute paths are left unchanged. Project-level config behavior is unaffected.
2. EMBEDDING_BATCH_SIZE: replaced Number.parseInt with Number() + Number.isInteger()
so that inputs like "64abc", "1.5", and "" are correctly rejected instead of
silently accepted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use Object.hasOwn() instead of `in` for log level validation
- Normalize relativePath in shouldIgnore() for Windows compatibility
- Reject zero/negative/decimal values in embedding config (Number() instead of parseInt())
- Update aggregate test count to 765
- Harden code-graph test assertion with explicit toBeDefined()
- Scope dedupe key to label::relativePath in mergeMultiCollectionResults
so same file in different projects is not collapsed
- Compute embedding once in searchMultipleCollections via internal
searchChunksWithVector helper (avoids N redundant API calls)
- Compare base project hashes (not branch-suffixed names) when skipping
duplicate linked projects in resolveLinkedCollections
- Use path.resolve() for platform-neutral assertion in query-tools tests
- Update multi-collection-search tests for new cross-project dedup semantics
- Refactor git temp repos into initTempRepo() helper using git config
instead of env vars (resolves Critical finding)
- Restore SOCRATICODE_LINKED_PROJECTS and SOCRATICODE_PROJECT_ID to
original values in resolveLinkedCollections afterEach
- Fix TypeScript 'never' type errors in query-tools mocks by adding
explicit SearchResult[] return types
CI runners have no git user.name/email configured, causing
'git commit --allow-empty' to fail with 'Author identity unknown'.
Pass GIT_AUTHOR_NAME/EMAIL and GIT_COMMITTER_NAME/EMAIL env vars.
CI checks out in detached HEAD state, causing detectGitBranch(process.cwd())
to return null. Replace process.cwd() with temporary git repos that have a
known branch and initial commit, making tests deterministic everywhere.
- query-tools.test.ts: 6 new tests for includeLinked parameter:
- omitted/false → calls searchChunks (not searchMultipleCollections)
- true → calls searchMultipleCollections via resolveLinkedCollections
- passes collections and args correctly
- project label appears in output
- no project tag when field absent
- multi-collection-search.test.ts: 1 new test for searchMultipleCollections
empty-input short-circuit (internal searchChunks calls cannot be mocked
from the same module — integration covered by query-tools tests)
resolveLinkedCollections() was calling projectIdFromPath() for linked
projects, which with SOCRATICODE_BRANCH_AWARE=true appended the linked
project's git branch to its ID. Linked projects indexed without
branch-aware mode don't have __branch collections, so lookups failed.
Extract coreProjectId() helper (hash-only, no branch suffix) and use
it for linked projects. Also fix dedup to compare collection names
instead of raw IDs to handle the branch suffix mismatch.
Adds unit test verifying linked collection names are branch-agnostic.
- detectGitBranch() detects current git branch via git rev-parse
- sanitizeBranchName() converts branch names to Qdrant-safe suffixes
- When SOCRATICODE_BRANCH_AWARE=true, projectIdFromPath appends __branch
to create separate indexes per branch
- Explicit SOCRATICODE_PROJECT_ID takes precedence (no branch suffix)
- 14 unit tests for sanitization, detection, and integration
Relates to #19
- loadLinkedProjects() reads .socraticode.json config and env var
- resolveLinkedCollections() maps linked projects to collection names
- codebase_search gains includeLinked parameter for cross-project queries
- Results tagged with [project-label] when searching across projects
- 10 unit tests for config loading and collection resolution
Relates to #20
Add searchMultipleCollections() and mergeMultiCollectionResults() to qdrant.ts.
Queries multiple Qdrant collections in parallel, merges results using Reciprocal
Rank Fusion (k=60), and deduplicates by relativePath (first collection wins).
Add optional 'project' field to SearchResult type for source attribution.
This is the shared foundation for linked projects (#20) and branch-aware
indexing (#19).
Refs: #19, #20
Single-module resolution (src/main/java/…) already worked.
Multi-module layouts like:
module-a/sub/src/main/java/com/example/Foo.java
were silently unresolved because the three fixed src-dir prefixes
never matched. The dependency graph therefore always produced 0
edges for any Java/Kotlin/Scala project that follows the standard
Maven/Gradle multi-module layout.
Fix: introduce `buildJvmSuffixMap()` which scans `fileSet` once
(O(n)) and registers every JVM source file by its class-path key
(i.e. everything after src/main/<lang>/). `resolveImport` accepts
the map as an optional last argument and falls back to it when the
existing prefix-based approach yields no result.
- `buildJvmSuffixMap` exported for reuse / testing.
- Map is built once per `buildCodeGraph` call, only when the
project contains at least one JVM file — zero cost for other
language projects.
- Lookup is O(1) per import, replacing a worst-case O(n) linear
scan on every miss.
- Works on both Windows (backslash) and POSIX (forward-slash)
because the map key is built with `path.sep`.
- 7 new unit tests covering: map construction, test-source
exclusion, multi-module Java resolution, Kotlin resolution,
unresolvable class, stdlib guard.
Fixes: enterprise Java/Spring Boot codebases (30+ Maven modules)
reporting 0 edges in codebase_graph_stats.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add CSS @import extraction from Svelte/Vue <style> blocks and standalone
CSS/SCSS/SASS/LESS files. Add path alias resolution from tsconfig.json /
jsconfig.json compilerOptions.paths with extends chain support.
ensureOllamaReady() was called unconditionally on every search and context
operation, causing failures for OpenAI and Google embedding users. Now only
called when embeddingProvider is ollama; otherwise the configured provider
is initialized via getEmbeddingProvider().
Fixes#7
Co-authored-by: pineapplestrikesback <pineapplestrikesback@users.noreply.github.com>
Svelte and Vue files were included in the graph as leaf nodes (targets
of import edges) but their own imports were never extracted because
no ast-grep grammar exists for these languages.
This adds support by parsing .svelte/.vue files as HTML (built-in
grammar), extracting <script> block content, and re-parsing it as
TypeScript to extract imports using the existing JS/TS logic.
Changes:
- code-graph.ts: register .svelte and .vue in getAstGrepLang()
- graph-imports.ts: add Svelte/Vue script extraction handler; refactor
JS/TS import extraction into shared extractJsTsImportsFromNode()
helper to avoid duplication
- graph-resolution.ts: add "svelte" and "vue" cases to resolveImport(),
with .svelte/.vue as first-priority extension resolution
- graph-imports.test.ts: add tests for Svelte (static, dynamic, module,
no-script, JS-only) and Vue import extraction
Zero new dependencies — uses the built-in Lang.Html and Lang.TypeScript
grammars from @ast-grep/napi.
Known limitation: path aliases ($lib/, @/) are not resolved and will be
treated as external packages (same as current JS/TS behavior).
When SOCRATICODE_PROJECT_ID is set to share a Qdrant collection across
git worktrees, the indexer still used absolute paths as keys in the file
hash map, chunk IDs, and for deleting file chunks. This caused every
worktree to see all files as "new" and trigger a full re-index, defeating
the purpose of the shared project ID feature.
Switch all internal keying from absolute paths to relative paths:
- chunkId() now hashes on relativePath for stable IDs across worktrees
- File hash map (change detection) keyed by relativePath
- deleteFileChunks() filters on relativePath Qdrant field
- Deleted file detection uses relative path sets
Absolute paths are now only used for actual file I/O (stat, readFile).
When working with git worktrees (or any setup where the same codebase lives
in multiple directories), each path currently gets its own Qdrant collection.
This means the same codebase is indexed multiple times.
This change adds a SOCRATICODE_PROJECT_ID environment variable that, when set,
overrides the path-based project ID generation. All directories sharing the
same SOCRATICODE_PROJECT_ID will use the same Qdrant collections (codebase,
codegraph, context), eliminating redundant indexing.
The value must match [a-zA-Z0-9_-]+ to remain Qdrant-friendly. An error is
thrown at startup if the value contains invalid characters.