Address CodeRabbit review:
- getLanguageFromExtension/getAstGrepLang lowercase the extension before
consulting the override map, so an uppercase ext still matches the
lowercased keys (production callers already lowercase; .R collapses to
.r with the same value, so no other behavior changes).
- README: clarify EXTRA_EXTENSIONS indexes as plaintext / leaf nodes,
versus EXTENSION_LANGUAGE_MAP's full language treatment.
- Make the isIndexableFile override test hermetic (save/restore the prior
map entry, assert against the captured baseline).
EXTRA_EXTENSIONS indexes unknown extensions as plaintext, losing AST
chunking, symbols, and the call graph. EXTENSION_LANGUAGE_MAP lets you
map an extension to a real language (e.g. .inc:php) so it gets full
language treatment end to end.
Format `.inc:php,.module:php`. Each entry resolves to the target
language's canonical extension, so both the language label
(getLanguageFromExtension) and the AST grammar (getAstGrepLang) come
from the existing maps and stay consistent, without re-encoding their
different vocabularies (shell vs bash, Lang enums for JS/TS/HTML/CSS).
Mapped extensions are auto-discovered (indexer + watcher) without also
needing EXTRA_EXTENSIONS, and can override built-ins (e.g. .h:cpp).
Targets with no AST grammar (the reporter's .foo:bar) and malformed
entries are rejected and warned about at startup.
Additive and backward compatible: with the var unset, behavior is
unchanged; no new dependency; no data migration.
The provider is an OpenAI-compatible client for the LiteLLM proxy server,
not the LiteLLM library, and it forwards EMBEDDING_MODEL verbatim without
routing provider/model strings. Document that, and how to reach a backend
like OpenRouter by registering it in the proxy's config.yaml model_list.
extractFromDart skipped any class member parsing as
`declaration > <signature>` with no function body, so abstract bodyless
getters, setters, and methods were dropped. Operators were missed
entirely (with or without a body): operator_signature was unhandled and
operators are not named by an identifier.
- declaration branch now also handles function/getter/setter/operator
signatures (abstract members); fields are still skipped.
- method_signature branch now handles operator_signature (operators with
a body), named operator<token> (operator+, operator==, operator[]).
- Surface unparseable files: a one-time warn (per-file debug) when a Dart
file has ERROR nodes, which for the bundled grammar means Dart 3 class
modifiers (sealed/base/interface/final/mixin class) or extension types
it cannot parse; those declarations are skipped, siblings still extract.
Additive and backward compatible: no existing symbol changes id or kind,
no new dependency, no data migration; changed files are re-extracted and
replaced per-file on the next index.
The vendored @ast-grep/lang-dart 0.0.7 grammar predates Dart 3.3
extension types and parses them to ERROR nodes; the kinds a newer
tree-sitter-dart exposes (extension_type_declaration) do not exist in
this version, so matching on them would be unreachable code. Document
the limitation at the walk site and lock in the contract: no throw, no
bogus symbols from the ERROR region, and the rest of the file still
extracts normally.
Dart previously fell through to the regex symbol fallback, which cannot
match type-first signatures (void foo(), Future<int> baz() async), so
classes, methods, and calls were invisible to codebase_symbol,
codebase_flow, and codebase_impact, and files were chunked by line
count instead of declaration boundaries.
- Register @ast-grep/lang-dart as a dynamic grammar (per-grammar
failure isolation keeps missing prebuilds on the regex fallback).
- Add extractFromDart: classes, mixins (trait), enums, extensions,
typedefs, type-first top-level functions, getters/setters, and
constructors including named and factory forms. Scope ranges are
stitched from Dart's sibling function_signature/function_body pairs.
Calls are recovered from argument_part nodes: method calls, bare
calls, constructor invocations, prefixed calls, and cascades.
- Add dart to TOP_LEVEL_KINDS so chunking follows declaration
boundaries; signature and body kinds are both listed so the
overlap-merge fuses each pair into one region.
- Add dart main() to ENTRY_POINT_NAMES for entry-point detection.
- Move Dart to Full Support in the README language matrix.
startWatching can throw outside its internal guard (lock-file writes,
gitignore reads). Wrap it so a watcher failure degrades to a warning
instead of skipping the project's catch-up update, which would leave
the index stale.
SOCRATICODE_AUTO_RESUME=all resumes the file watcher and incremental
catch-up for every indexed project with a stored path on server
startup. SOCRATICODE_AUTO_RESUME_PROJECTS accepts an explicit
comma-separated list and takes precedence. Default (both unset) keeps
the original cwd-only behavior.
Multi-project resume is strictly sequential so N projects never run
concurrent embedding catch-ups, and every skipped project (missing
directory, no stored path, not indexed) is logged at warn level.
Address maintainer review on PR #67.
- Add a dedicated `describe("Lua")` block locking in qualified
`Table.method` / `T:m()` names, `local function`, both
`T.f = function() … end` and `local f = function() … end` assignment
forms, call attribution to the enclosing function, top-level calls
falling back to `<module>`, and dotted-callee resolution.
- Move the former "handles Lua via regex fallback" test out of the
regex-fallback block (Lua now routes to extractFromLua) and give it
real assertions; drop "Lua" from that block's title.
- Add a whitelist `.gitignore` regression test (`/*` then `!/src/`)
asserting files under the re-included dir are discovered; export
getGraphableFiles so the test can exercise discovery directly.
- Match the dynamic-grammar style for the Lua parse call
(`parse("lua" as unknown as Lang, source)`).
Two related graph-engine fixes that together make the code graph work on Lua
projects (and unblock any repo using a whitelist-style .gitignore):
- getGraphableFiles skipped every directory whose bare name matched .gitignore
but was re-included only in trailing-slash form (e.g. `/*` then `!/src/`).
The walk never descended, producing an empty graph for ALL languages on such
repos. Check the directory form (trailing slash), per gitignore dir semantics.
- Lua had no dedicated ast-grep extractor and fell through to the regex
fallback, which records `Mod` for `function Mod.parse()`. Add extractFromLua
(function_declaration dotted/method/local names, `T.f = function()` assigns,
and call sites) and register the @ast-grep/lang-lua grammar, so namespace-table
style resolves to precise qualified symbols.
The previous invite (5DrMXfNG) expired. Replace all 6 occurrences
across README.md, extension/README.md, and SUPPORT.md with the new
permanent invite (dHNMKVY2J2).
Address CodeRabbit review: also normalize stored graph node keys when
comparing, not just the query input. Handles pre-fix Windows caches
where node keys still contain backslashes until the graph is rebuilt.
On Windows, path.relative() and path.join() return backslash separators.
Graph node keys were stored with native separators, but query inputs use
forward slashes, causing silent lookup failures on Windows.
Add toForwardSlash() utility and apply it at build time (file walker,
resolution functions) and query time (getFileDependencies,
getSymbolContext, listSymbols) for defense-in-depth.
No-op on macOS/Linux where path.relative() already returns forward
slashes. Existing Windows symbol graph caches require one rebuild.
Fixes#60
Third-pass review caught that the callback shape from 5cd9db0
(while correctly fixing the stderr-truncation concern CodeRabbit
raised) introduced an async-exit window. With process.exit(1)
inside the write callback, on Node 26+ the rest of the file's
top-level code runs before termination: imports' top-level
evaluation, the McpServer/tool registrations, and the start of
main()'s connect — the MCP host can briefly see a handshake
begin before the process dies.
fs.writeSync(2, msg) is the canonical Node pattern for "print
fatal error then die" — blocking (no truncation when stderr is
piped) AND synchronous (so process.exit(1) runs before any
further top-level code). Strictly better than the callback shape
on both axes.
Also soften comment phrasing to reduce rot risk:
- "Candidate fixes already in flight" -> "Upstream PRs under discussion"
- "Once one lands" -> "If either lands -- or any other fix supersedes them"
Verified: full 4-line stderr message survives piping to a file
on Node 26.0.0; exit code 1 preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per CodeRabbit review on #59: process.stderr.write() is async when
stderr is piped (every MCP host captures stderr to surface server
logs), so a bare `process.exit(1)` immediately after the write
terminates synchronously without draining I/O — risking truncation
of the compatibility warning that this guard exists to surface.
Move the exit into the write callback so the message is guaranteed
to flush before the process terminates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Under Node 26+, the very first qdrant request crashes with
`UND_ERR_INVALID_ARG: invalid onError method`. Root cause is a version
mismatch: @qdrant/js-client-rest constructs an undici.Agent from its
pinned undici ^6 and passes it as the dispatcher to Node's built-in
fetch(), which under Node 26 uses a newer undici with stricter
dispatcher-hook validation.
The bug surfaces on the first real codebase_search / codebase_index
call — the MCP handshake succeeds, then everything fails. The error
message gives no hint about Node version, so users on Node 26+ lose
significant time debugging.
This change:
- Adds a runtime pre-flight check at index.ts entry that prints a
clear actionable error and exits 1. Per ESM the imports below
evaluate first, but qdrant-js's module init is side-effect-light,
so exiting at the first top-level statement is enough.
- Tightens engines.node to `>=18.0.0 <26.0.0` so npm/npx warns at
install time.
Both can be reverted once one of qdrant/qdrant-js#123 (undici major
upgrade) or qdrant/qdrant-js#128 (inject fetch) lands.
Refs: qdrant/qdrant-js#134
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The OpenAI SDK's `client.models.list()` returns a `PagePromise` that
implements `AsyncIterable<Model>` and auto-paginates on demand. The
previous implementation read `modelList.data` directly, which only
contains the first page. Today's LiteLLM proxy returns the entire
`model_list` from `config.yaml` in a single response so the bug is
latent, but a future LiteLLM build (or an upstream proxy in front of
it) that paginates `/v1/models` would cause `ensureReady` and
`healthCheck` to throw a spurious "alias not registered" error for
any alias landing on a non-first page.
Switch both checks to `for await (const m of client.models.list())`
and accumulate ids into a single array. Equivalent to the SDK's
documented async-iteration pattern; `PagePromise` is itself the
iterable, so no extra `await` is needed before the loop. Inline
comment explains why the iteration matters even though today's
LiteLLM doesn't paginate, so the pattern survives future drive-by
"simplifications".
Surfaced by CodeRabbit on PR review of 1708510.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LiteLLM Proxy Server (https://docs.litellm.ai/docs/simple_proxy) exposes an
OpenAI-compatible /v1/embeddings endpoint and fans out to 100+ underlying
providers (OpenAI, Anthropic, Cohere, Voyage, HuggingFace, Bedrock, Vertex AI,
Ollama, ...). Mirroring the lmstudio strategy (PR #42 + commit bb141a0) but
with three meaningful differences that justify a dedicated provider rather
than a flag on provider-openai:
- Authentication is mandatory. LiteLLM gates /v1/models with the master key
or a virtual key, unlike LM Studio (no auth by default) and OpenAI (cloud
key). LITELLM_API_KEY is checked at config-load time; the provider also
duck-types 401/403 in ensureReady/healthCheck via err.status to surface a
distinct "auth rejected" message vs. "proxy unreachable".
- Model aliases come from the proxy's config.yaml, so EMBEDDING_MODEL and
EMBEDDING_DIMENSIONS have no sensible defaults. Fail-fast in
loadEmbeddingConfig with provider-specific error messages pointing at
litellm_params.model in the proxy config and at the underlying alias's
output dim.
- Whether dimensions can be forwarded depends on the underlying provider:
Matryoshka-aware models (text-embedding-3-*, voyage-3) accept it,
non-Matryoshka backends (BGE, nomic, Cohere v3) reject. Made opt-in via
LITELLM_SEND_DIMENSIONS=true rather than hardcoded like provider-openai
does for text-embedding-3-*, since LiteLLM aliases are user-defined.
Encoding-format=float fix from bb141a0 ports verbatim — the OpenAI SDK 6.x
base64-decode path corrupts any backend that returns plain JSON float arrays
(many LiteLLM aliases do, including Ollama-routed and tei-wrapped ones).
Files:
- src/services/provider-litellm.ts: new LiteLLMEmbeddingProvider with the
same OpenAI-SDK + custom baseURL pattern. Default baseURL
http://localhost:4000/v1 (LiteLLM's default port, /v1 prefix required).
Batch size 256 — between OpenAI's 512 and LM Studio's 64, since the
practical ceiling depends on whichever provider the alias resolves to.
ensureReady distinguishes proxy-unreachable / auth-rejected /
alias-not-registered. Lists up to 10 currently-registered models in the
alias-missing error so the operator can sanity-check their config.yaml
without leaving the log.
- src/services/embedding-config.ts: extends EmbeddingProvider union with
"litellm", adds litellmUrl to EmbeddingConfig, fail-fast validation for
LITELLM_API_KEY + EMBEDDING_MODEL + EMBEDDING_DIMENSIONS (key first so a
virtual-key user fixes the easy problem before touching the proxy
config), updates Invalid EMBEDDING_PROVIDER message and hasApiKey log
expression.
- src/services/embedding-provider.ts: factory case for litellm with dynamic
import to avoid loading the OpenAI SDK at startup for non-litellm users.
- README.md: dedicated LiteLLM section, MCP host config example, env-var
table entries for EMBEDDING_PROVIDER / EMBEDDING_MODEL /
EMBEDDING_DIMENSIONS / EMBEDDING_CONTEXT_LENGTH (clarifying which require
manual values for litellm), new LiteLLM Configuration table.
- tests/unit/embedding-config.test.ts: 9 new cases (model + dim + key
required, error-ordering, URL default + override, dimensions parsing,
EMBEDDING_CONTEXT_LENGTH override for unknown aliases, auto-detection
when alias matches a known model name) plus updated "full external
config" expected object and updated invalid-provider error message.
- tests/unit/embedding-provider.test.ts: factory test for litellm, plus 4
cases against a deliberately-closed port (config rejects construction
without API_KEY, ensureReady unreachable error format, healthCheck
short-circuits on missing key without a network call, healthCheck
reaches "Not reachable" path without throwing).
Backward compatible. The litellm provider is opt-in via
EMBEDDING_PROVIDER=litellm. Existing ollama, openai, google, and lmstudio
paths are untouched.
Verified: 64/64 unit tests pass on the touched suites; biome lint clean;
tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The deliberate catch-and-rethrow sites in `loadProjectHashes` and
`getCollectionInfo` were re-throwing the raw Qdrant client error. With
no wrapping, the original message ("Internal Server Error") landed
verbatim in the MCP response and `codebase_status` `lastCompleted.error`
field, leaving consumers no way to distinguish which operation failed,
which collection it targeted, or whether the underlying error carried
an HTTP status code.
Add a `wrapQdrantError(operation, context, err)` helper and apply it at
both rethrow sites. The new error message looks like:
loadProjectHashes(collName=codebase_xxx) failed [status 500]: Internal Server Error
The original error is preserved via `cause`, so any consumer that walks
the cause chain still has full access. Also pick up `statusCode` as a
fallback for clients that use that field name.
Behaviour preserved:
- 404 / not-found still returns null from `getCollectionInfo` (no change).
- Re-throw is still the path for transient/unknown errors (no behaviour
change for the deliberate hardening that protects against destructive
clean-start cascades).
Adds `tests/unit/qdrant-error-wrapping.test.ts` covering the 404 pass-
through, wrapped-message format, status-code inclusion, missing-status
case, and `cause` preservation.
Closes#55.
Adds an optional `projectId` field to `.socraticode.json` so teams can
commit a stable project identifier to the repo. Without this field the
project ID is derived from the SHA-256 of the absolute checkout path,
which means the same project resolves to a different Qdrant collection
on every machine, OS user, filesystem layout, or worktree. With it,
every checkout addresses the same `codebase_*`, `codegraph_*`, and
`context_*` collections regardless of where the working tree lives on
disk.
This is the path-independent, multi-project complement to the existing
`SOCRATICODE_PROJECT_ID` env var. The env var is process-scoped and
global to all projects in a host, so it does not scale to a developer
who works on several projects on one laptop. The file is per-project
and shared across teammates via git.
Resolution precedence (highest first):
1. `SOCRATICODE_PROJECT_ID` env var (per-machine override)
2. `projectId` in .socraticode.json (committed, team-wide)
3. SHA-256 prefix of the absolute path (existing default)
Both override paths trim whitespace, validate against `[a-zA-Z0-9_-]+`,
and throw on invalid characters so a misconfigured value cannot
silently route a project to the wrong (or empty) collection. Malformed
JSON, missing fields, wrong types, and empty/whitespace-only values
fall through to the next precedence level so the MCP server stays
resilient against hand-edited config files. Branch-aware mode is
suppressed for either explicit override since explicit identifiers
are stable by intent.
Also fixes a pre-existing bug in `resolveLinkedCollections`: linked
projects were resolved via `coreProjectId(linkedPath)` (path hash
only), so a linked project that pinned its own `projectId` in
`.socraticode.json` would silently miss its actual data during
cross-project search. Linked-project resolution now goes through a
new `effectiveBaseProjectId` helper that honors the committed value,
preserving symmetry: a project addresses the same Qdrant collection
whether it is the current root or a linked dependency. Dedup is
tightened to use the same effective base ID, so two paths pinning the
same shared identifier collapse to a single result.
The env var deliberately does not leak into linked-project collection
names. It is process-scoped and applying it as a single value to every
linked path would collapse them onto the env-var collection, silently
losing per-project isolation.
Tests: 16 new cases in tests/unit/config.test.ts, written TDD-style
(RED to GREEN). Coverage:
- `projectIdFromPath` (13): file resolution, ignores path
differences when file projectId is set, whitespace trimming,
throws on invalid characters, falls back to hash on
empty/whitespace/wrong-type/null/missing-file/malformed-JSON,
env-var precedence over file, branch-suffix suppression, and
coexistence with `linkedProjects` in the same file.
- `resolveLinkedCollections` (3): linked project's committed
projectId honored, dedup on shared committed projectId, env var
does not leak into linked-project collection names.
The new branch-aware-suppression test explicitly disables git
`commit.gpgsign` and `tag.gpgsign` in its throwaway-repo fixture so
the test is robust against the developer's global git config.
Backwards compatible: zero behaviour change for users who do not adopt
the new field. The `SocratiCodeConfig` interface gains an optional
field; existing `linkedProjects` parsing is functionally identical
(routed through the new shared `loadSocratiCodeConfig` helper).
Composes cleanly with the recently-added `QDRANT_COLLECTION_PREFIX`:
prefix + projectId combine into `<prefix>codebase_<projectId>` as
expected.
README and DEVELOPER documentation updated: new "Team-Shared Index
(committed `projectId`)" section in README between Git Worktrees and
Cross-Project Search, and the env-var table notes the new precedence.
DEVELOPER's "Project ID & Collection Naming" section now documents
the three-level precedence and explains why both override paths
suppress the branch-aware suffix.
Co-authored-by: airmonitor <tomasz.szuster@gmail.com>
indexAllArtifacts and ensureArtifactsIndexed previously called saveContextMetadata only once, after the entire indexing pass completed. When the underlying loop took longer than the MCP client's tool-call timeout, completed artifacts appeared unindexed because their state was never persisted, and partial progress was lost.
This patch saves the metadata snapshot after every successfully indexed artifact, so each artifact's success is durable as soon as the indexing for it returns. It also seeds the in-flight stateMap from the previously-loaded existingStates so that interrupted runs can preserve completed work for artifacts already finished, and uses that same original snapshot to identify orphan artifacts that need cleanup when the config has changed.
Backwards compatible: a successful full run produces exactly the same final on-disk state as before. The only behavioural difference is in the interrupted-mid-run case, where the new code retains more state instead of losing everything since the last full pass.
Tests: 3 new cases in tests/unit/context-artifacts-checkpoint.test.ts covering the checkpointing path during full indexing, preservation of earlier successes when a later artifact fails, and preservation of up-to-date states while re-indexing stale ones. Existing unit tests continue to pass unchanged.
Co-authored-by: jackblackjack chugarev@gmail.com
Resolves#49. Reported by @awbait.
When sharing a single Qdrant server across multiple applications
(SocratiCode + Open-WebUI + custom RAG, etc.) or across multiple
SocratiCode instances (per-project, per-environment, per-user), the
fixed `codebase_<id>` / `codegraph_<id>` / `context_<id>` /
`<id>_symgraph_*` / `socraticode_metadata` collection names risk
colliding with other apps and prevent isolation between SocratiCode
instances.
This patch adds an optional QDRANT_COLLECTION_PREFIX env var that, when
set, is prepended verbatim to every Qdrant collection name SocratiCode
creates, queries, lists, or deletes. Default empty string preserves the
existing collection names exactly: fully backwards compatible.
Touchpoints (mechanical, no logic changes):
- src/constants.ts: new QDRANT_COLLECTION_PREFIX export with eager
validation. Qdrant accepts only [a-zA-Z0-9_-] in collection names; an
invalid prefix throws at module load with a message naming the
offending value, before any Qdrant call is attempted.
- src/config.ts: all six collection-name generators
(collectionName, graphCollectionName, contextCollectionName,
symgraphMetaCollectionName, symgraphFileCollectionName,
symgraphIndexCollectionName) prepend the prefix. Generator semantics
are otherwise unchanged.
- src/services/qdrant.ts: METADATA_COLLECTION (the global
socraticode_metadata collection used for cross-project state) also
honours the prefix, so two SocratiCode instances on one Qdrant keep
their metadata isolated as well as their per-project collections.
The two startsWith() filters in listCodebaseCollections — used by
codebase_list_projects to discover this instance's collections —
build the match prefix from QDRANT_COLLECTION_PREFIX so a prefixed
instance only sees its own collections, not those of co-tenants.
- src/tools/manage-tools.ts: codebase_list_projects similarly uses the
prefix in its filters. The projectId extraction (formerly
c.replace("codebase_", "")) now slices the full
${prefix}codebase_ token so the recovered id is correct under any
prefix; the codegraph cross-reference uses the same prefixed name.
Tests: 20 new test cases in tests/unit/qdrant-collection-prefix.test.ts
covering:
- Default empty prefix preserves the legacy collection-name forms for
all six generators (regression guard against backward-compat break).
- Empty-string env var is treated identically to unset.
- Non-empty prefix prepends correctly to all six generators, including
the suffix-style symgraph names.
- Two different prefixes produce disjoint collection-name sets for the
same projectId (the multi-instance isolation property).
- Validation rejects whitespace, slash, colon, and unicode characters.
- The error message includes the offending value for discoverability.
- Validation accepts the full set of legal characters.
Existing 752 unit tests continue to pass unchanged. Total: 772.
typecheck, biome, and CodeRabbit local review all clean. README
updated to document the new env var alongside the other QDRANT_*
settings, including the user-side responsibility to remove old
collections when changing prefix between runs.
Co-authored-by: awbait <awbait@users.noreply.github.com>
Address CodeRabbit review on PR #48. The early `isExternalModule` check
in resolveImport was filtering out any import starting with `golang.org/`
before the Go case had a chance to match it against the local module
path. This blocked legitimate local imports for any project whose own
module path starts with `golang.org/` (the Go team's own packages like
golang.org/x/sync, golang.org/x/net, etc., where each one's go.mod
declares `module golang.org/x/<name>`).
Skip the early external check for Go specifically. The Go case in
resolveImport already does its own module-path-aware classification
and returns null for everything outside the local module, including
stdlib and third-party deps. No regression in those cases.
New regression test asserts that
`module golang.org/x/custom` + `import "golang.org/x/custom/internal"`
resolves to the local internal/ package. Confirmed the test fails
without the fix and passes with it. Total: 752 unit tests pass.
Co-authored-by: mrsuit92 <mrsuit92@users.noreply.github.com>
Resolves#45. Reported by @mrsuit92.
Go projects produced 0 dependency edges in codebase_graph_query and
codebase_graph_stats even though import extraction worked correctly.
The Go case in resolveImport returned null unconditionally, with a
comment that resolution required go.mod analysis. This patch adds
that analysis and wires it into the resolver, mirroring the existing
buildJvmSuffixMap and buildCsNamespaceMap patterns.
Mechanism:
- buildGoModuleInfo reads <projectPath>/go.mod once at graph-build
time, parses the `module <path>` directive, and walks the file set
to build a directory-to-representative-file map for every Go
package. _test.go files are excluded from representative selection
because Go does not allow them to be imported from non-test code in
other packages. Files are sorted lexicographically for
deterministic representative selection across machines and runs.
Returns null when go.mod is missing or has no parseable module
directive; the resolver treats null as "no Go resolution available"
and behaves exactly as before this patch in those cases.
- The Go case in resolveImport now strips the module path prefix
from the import (handling the bare-module-path root case as well
as subpackage paths) and looks up the resulting directory in the
package map. Imports outside the module path return null and are
treated as external dependencies (or stdlib already filtered
upstream by isExternalModule).
- Map keys are forward-slash paths, not OS-native, so resolution
works on Windows: Go imports are always forward-slash regardless
of host OS, but path.dirname produces backslashes on Windows for
nested directories. Normalising the key to forward slashes at
build time keeps the lookup correct across platforms.
Limitations (deferred to follow-up issues if any user reports them):
- The parenthesised module ( path ) form in go.mod is not parsed.
Not used by any mainstream Go project (verified against cobra,
gin-gonic/gin, uber-go/zap real-world go.mod files).
- vendor/ directory shadowing of external imports is not honoured.
- replace directives in go.mod are not honoured.
- go.work multi-module workspaces are not handled (each workspace
module would need its own go.mod read and prefix matching).
These are real Go features but each one widens the patch and
narrowly affects specific user populations. They can be added as
separate small PRs if a real user hits them.
Tests: 16 new cases in tests/unit/graph-resolution.test.ts covering
the new buildGoModuleInfo function (parses simple go.mod, handles
leading whitespace and trailing content, returns null on missing or
malformed go.mod, excludes _test.go from representative selection,
omits test-only directories, uses forward-slash keys for nested
packages) and the Go resolveImport case (back-compat null without
goModuleInfo, subpackage import resolves to lex-smallest non-test
.go file, root-package import resolves to a project-root .go file,
external imports return null, missing or malformed go.mod returns
null, _test.go excluded from representative selection, lexically
smallest file picked deterministically, similar-prefix imports do
not falsely resolve, nested-package imports work cross-platform).
Existing 735 unit tests continue to pass unchanged. Total: 751.
typecheck, biome, and CodeRabbit local review all clean. CodeRabbit
caught a real Windows path-separator bug in the first iteration; the
fix and a regression test for it are included.
Co-authored-by: mrsuit92 <mrsuit92@users.noreply.github.com>
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>