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>
- 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.
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
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.