diff --git a/.github/scripts/update-vendored-grammars.mjs b/.github/scripts/update-vendored-grammars.mjs index 769310bd7..0554d3253 100644 --- a/.github/scripts/update-vendored-grammars.mjs +++ b/.github/scripts/update-vendored-grammars.mjs @@ -21,7 +21,7 @@ * node update-vendored-grammars.mjs # detect only → JSON report on stdout * node update-vendored-grammars.mjs --apply X # re-vendor grammar X in place * - * tree-sitter-c is MONITORED but report-only (`hold`): it is ABI-pinned at 0.21.4 + * tree-sitter-c and tree-sitter-objc are MONITORED but report-only (`hold`): c is ABI-pinned at 0.21.4 * (#1242/#858) and must not auto-bump without a tree-sitter runtime upgrade, so an * available c update is detected + reported but never auto-applied — even if it is * ABI-13/14. A maintainer re-vendors it deliberately. diff --git a/.github/vendored-grammars.json b/.github/vendored-grammars.json index 945164348..228f93447 100644 --- a/.github/vendored-grammars.json +++ b/.github/vendored-grammars.json @@ -6,6 +6,11 @@ "upstream": { "npm": "tree-sitter-c" }, "hold": "ABI-pinned at 0.21.4 (#1242/#858) — needs a tree-sitter runtime upgrade before bumping" }, + "objc": { + "name": "tree-sitter-objc", + "upstream": { "npm": "tree-sitter-objc" }, + "hold": "Pinned at 3.0.2 for the Objective-C provider MVP; carries darwin/linux arm64+x64 prebuilds compatible with the current tree-sitter runtime (linux-arm64 built from vendored source because the upstream npm artifact is mislabeled)" + }, "swift": { "name": "tree-sitter-swift", "upstream": { "npm": "tree-sitter-swift" } diff --git a/.github/workflows/build-tree-sitter-prebuilds.yml b/.github/workflows/build-tree-sitter-prebuilds.yml index 2411748c5..5f04b70af 100644 --- a/.github/workflows/build-tree-sitter-prebuilds.yml +++ b/.github/workflows/build-tree-sitter-prebuilds.yml @@ -7,7 +7,7 @@ name: Build tree-sitter prebuilds # # Grammars covered here (the at-risk set — everything else already ships 6 # upstream prebuilds AND stays dependency-review-tracked, so it is left alone). -# All five are vendored under gitnexus/vendor/; `kind` (below) only picks where +# All six are vendored under gitnexus/vendor/; `kind` (below) only picks where # the build job fetches the C source to compile: # - tree-sitter-c (vendored prebuild-only; built from the published npm # package — closes upstream's 4/6 ARM gap #2116 for a @@ -17,6 +17,8 @@ name: Build tree-sitter prebuilds # - tree-sitter-kotlin (vendored source; built from gitnexus/vendor/ — pinned to # an unreleased main commit for `fun interface` support # (#169) that no npm release carries yet) +# - tree-sitter-objc (vendored source; built from gitnexus/vendor/ — pinned +# for the Objective-C provider MVP) # - tree-sitter-swift (vendored source; built from gitnexus/vendor/ — its # prebuilds were originally upstream-shipped, now # GitNexus-cross-built like the rest for uniformity) @@ -25,7 +27,7 @@ name: Build tree-sitter prebuilds # all 6 targets ({linux,darwin,win32}-{x64,arm64}). tree-sitter grammars are # N-API, so one ABI-stable .node per platform-arch works across all Node majors. # -# COST DISCIPLINE — this is a HEAVY native matrix (up to 3 grammars x 6 runners, +# COST DISCIPLINE — this is a HEAVY native matrix (up to 6 grammars x 6 runners, # incl. macOS + arm64). It is DELIBERATELY NOT wired into normal PR/push CI. It # runs only: # 1. on manual dispatch (workflow_dispatch); or @@ -55,7 +57,7 @@ on: workflow_dispatch: inputs: grammars: - description: 'Comma-separated grammar shortnames to build (c,dart,proto,kotlin,swift), or "all".' + description: 'Comma-separated grammar shortnames to build (c,dart,proto,kotlin,objc,swift), or "all".' required: false type: string default: 'all' @@ -86,7 +88,7 @@ on: - '!gitnexus/vendor/tree-sitter-*/prebuilds/**' # Self-test: re-run the guard if a future grammar pin is reintroduced in # the main package.json (optionalDependencies fallback). No-op otherwise — - # all five grammars are now fully vendored (kotlin included). + # all six grammars are now fully vendored (kotlin and objc included). - 'gitnexus/package.json' # Self-test: re-run the guard (normally a no-op) when the recipe changes. - '.github/workflows/build-tree-sitter-prebuilds.yml' @@ -153,6 +155,9 @@ jobs: // unreleased main commit for `fun interface` support (#169) that no // npm release carries yet — so it must build from the vendored source. kotlin: { name: 'tree-sitter-kotlin', kind: 'vendored' }, + // Objective-C is vendored WITH its source and its native bindings + // must be recut together with the pinned grammar snapshot. + objc: { name: 'tree-sitter-objc', kind: 'vendored' }, // swift is vendored WITH its source (parser.c/scanner.c/binding.gyp), // so it builds from gitnexus/vendor/ like dart/proto. Its prebuilds // were originally upstream-shipped; rebuilding them here unifies it. diff --git a/.github/workflows/tree-sitter-upgrade-readiness.yml b/.github/workflows/tree-sitter-upgrade-readiness.yml index 13b79e093..88ee0bd42 100644 --- a/.github/workflows/tree-sitter-upgrade-readiness.yml +++ b/.github/workflows/tree-sitter-upgrade-readiness.yml @@ -4,7 +4,7 @@ name: Tree-sitter Upgrade Readiness # 1. Peer-dep compatibility — can each NPM-installed grammar install cleanly # with tree-sitter@0.25.0 without --legacy-peer-deps? # 2. Vendored grammars — each grammar in .github/vendored-grammars.json -# (c/swift/kotlin/dart/proto) is classified by its vendored ABI, read +# (c/swift/kotlin/dart/proto/objc) is classified by its vendored ABI, read # straight from gitnexus/vendor//src/parser.c (NOT node_modules, # which is never populated for vendored grammars — that mismatch is why # the report used to render bare "?" placeholders, #858). diff --git a/.gitignore b/.gitignore index e16544f71..2adcef3b3 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,8 @@ eval/.hypothesis/ # Local docs — planning output (gitnexus-plan / gitnexus-work) stays local, not tracked docs/* +!docs/fork/ +!docs/fork/** gitnexus/test/fixtures/mini-repo/*.md gitnexus/test/fixtures/mini-repo/.claude diff --git a/README.md b/README.md index e26373aff..d49e32b4a 100644 --- a/README.md +++ b/README.md @@ -514,6 +514,8 @@ Most `analyze` knobs are also CLI flags (`--workers`, `--worker-timeout`, `--max | `GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS` | `30000` | Worker idle timeout in milliseconds before retry/fallback. Equivalent to `--worker-timeout ` × 1000. | Slow-parsing files (large minified JS, deeply-nested TS types) that legitimately need more than 30s. | | `GITNEXUS_WORKER_READY_TIMEOUT_MS` | `5000` | Startup budget in milliseconds for a parse worker to load its grammar bindings and report `{type:'ready'}`. Slots that miss it are treated as startup crashes. | Slow or heavily loaded hosts where a full pool cold-starting concurrently needs more than 5s, and analyze aborts with "did not report ready within 5000ms". | | `GITNEXUS_FTS_STEMMER` | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` for matching repository comments. Re-run `gitnexus analyze --repair-fts` after changing it. | Keyword search quality is poor for non-English comments or identifiers under English stemming. | +| `GITNEXUS_STORAGE_PATH` | `/.gitnexus/` | Absolute directory for one repository index, including graph data, metadata, caches, locks, and branch indexes. The resolved path is registered so `status`, MCP, and `serve` can reopen the index later. | An external runner manages short-lived checkouts or wants indexes outside the source tree. | +| `GITNEXUS_CONTENT_RETENTION` | `full` | Source-text retention profile: `full` keeps file and symbol text, `symbol` keeps symbol snippets without full file content, and `none` keeps the structural graph without source body text. | You need to reduce persisted source text while preserving graph structure. | | `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold in bytes. Equivalent to `--wal-checkpoint-threshold `. `-1` keeps LadybugDB's stock threshold (~16 MiB). Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. | You need a larger or smaller WAL auto-checkpoint threshold for your analyze workload. | | `GITNEXUS_LBUG_BUFFER_POOL_SIZE` | min(2 GiB, 80% RAM) | LadybugDB buffer-pool ceiling in bytes for every GitNexus database (analyze, MCP server, serve, group bridges). `0` restores LadybugDB's native unbounded default of 80% of system RAM; invalid values warn and fall back to the default (#2557). During `analyze` the pool is right-sized to the graph, scaled on non-4 KiB-page hosts by the page-size granule ratio up to min(2 GiB × pageSize/4 KiB, 80% RAM) (#2631); this env var overrides all of that as an absolute value. | A long-lived `gitnexus mcp` or a big incremental `analyze` uses too much memory, or a huge repo's working set genuinely needs a pool larger than 2 GiB. | | `GITNEXUS_LBUG_MAX_DB_SIZE` | `17179869184` (16 GiB) | Maximum size in bytes of a single LadybugDB database file — an mmap/disk-address-space ceiling, not a memory limit (it does not constrain the buffer pool). Invalid values silently fall back to the default. | Indexing a genuinely huge monorepo whose on-disk graph index approaches 16 GiB. | @@ -579,6 +581,7 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas | Swift | — | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | C | — | — | ✓ | — | ✓ | ✓ | — | ✓ | ✓ | | C++ | — | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | +| Objective-C | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | | Dart | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | **Imports** — cross-file import resolution · **Named Bindings** — `import { X as Y }` / re-export tracking · **Exports** — public/exported symbol detection · **Heritage** — class inheritance, interfaces, mixins · **Type Annotations** — explicit type extraction for receiver resolution · **Constructor Inference** — infer receiver type from constructor calls (`self`/`this` resolution included for all languages) · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics @@ -589,7 +592,7 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas GitNexus uses a **global registry** so one MCP server can serve multiple indexed repos. No per-project MCP config needed — set it up once and it works everywhere. -Each `gitnexus analyze` stores the index in `.gitnexus/` inside the repo (portable, gitignored) and registers a pointer in `~/.gitnexus/registry.json`. When an AI agent starts, the MCP server reads the registry and can serve any indexed repo. LadybugDB connections are opened lazily on first query and evicted after 5 minutes of inactivity (max 5 concurrent). If only one repo is indexed, the `repo` parameter is optional on all tools — agents don't need to change anything. +Each `gitnexus analyze` stores the index in `.gitnexus/` inside the repo by default (portable, gitignored) and registers a pointer in `~/.gitnexus/registry.json`; `GITNEXUS_STORAGE_PATH` can redirect one index to an absolute external directory. When an AI agent starts, the MCP server reads the registry and can serve any indexed repo. LadybugDB connections are opened lazily on first query and evicted after 5 minutes of inactivity (max 5 concurrent). If only one repo is indexed, the `repo` parameter is optional on all tools — agents don't need to change anything.
Architecture diagram @@ -1007,7 +1010,7 @@ Built by the community — not officially maintained, but worth checking out. ## Security & Privacy -- **CLI**: everything runs locally on your machine. No network calls. Index stored in `.gitnexus/` (gitignored). Global registry at `~/.gitnexus/` stores only paths and metadata. +- **CLI**: everything runs locally on your machine. No network calls. Index stored in `.gitnexus/` by default (gitignored), or in `GITNEXUS_STORAGE_PATH` when configured. Global registry at `~/.gitnexus/` stores only paths and metadata. - **Web**: everything runs in your browser. No code uploaded to any server. API keys stored in localStorage only. - Open source — audit the code yourself. diff --git a/gitnexus-shared/src/graph/types.ts b/gitnexus-shared/src/graph/types.ts index d9d916e0d..86955e68c 100644 --- a/gitnexus-shared/src/graph/types.ts +++ b/gitnexus-shared/src/graph/types.ts @@ -14,6 +14,8 @@ export type NodeLabel = | 'Folder' | 'File' | 'Class' + | 'Protocol' + | 'Category' | 'Function' | 'Method' | 'Variable' diff --git a/gitnexus-shared/src/language-detection.ts b/gitnexus-shared/src/language-detection.ts index 6d031d761..5098e575a 100644 --- a/gitnexus-shared/src/language-detection.ts +++ b/gitnexus-shared/src/language-detection.ts @@ -32,6 +32,7 @@ const EXTENSION_MAP: Record = { [SupportedLanguages.Python]: ['.py'], [SupportedLanguages.Java]: ['.java'], [SupportedLanguages.C]: ['.c'], + [SupportedLanguages.ObjectiveC]: ['.m', '.mm'], [SupportedLanguages.CPlusPlus]: [ '.cpp', '.cc', @@ -110,6 +111,7 @@ const SYNTAX_MAP: Record = { [SupportedLanguages.Python]: 'python', [SupportedLanguages.Java]: 'java', [SupportedLanguages.C]: 'c', + [SupportedLanguages.ObjectiveC]: 'objectivec', [SupportedLanguages.CPlusPlus]: 'cpp', [SupportedLanguages.CSharp]: 'csharp', [SupportedLanguages.Go]: 'go', diff --git a/gitnexus-shared/src/languages.ts b/gitnexus-shared/src/languages.ts index 29edb8d1e..cdbdd71bc 100644 --- a/gitnexus-shared/src/languages.ts +++ b/gitnexus-shared/src/languages.ts @@ -11,6 +11,7 @@ export enum SupportedLanguages { Java = 'java', C = 'c', CPlusPlus = 'cpp', + ObjectiveC = 'objective-c', CSharp = 'csharp', Go = 'go', Ruby = 'ruby', diff --git a/gitnexus-shared/src/lbug/schema-constants.ts b/gitnexus-shared/src/lbug/schema-constants.ts index 350aa273d..1fda55cd8 100644 --- a/gitnexus-shared/src/lbug/schema-constants.ts +++ b/gitnexus-shared/src/lbug/schema-constants.ts @@ -13,6 +13,8 @@ export const NODE_TABLES = [ 'Folder', 'Function', 'Class', + 'Protocol', + 'Category', 'Interface', 'Method', 'CodeElement', diff --git a/gitnexus-shared/src/scope-resolution/language-classification.ts b/gitnexus-shared/src/scope-resolution/language-classification.ts index 10c556cda..90d462922 100644 --- a/gitnexus-shared/src/scope-resolution/language-classification.ts +++ b/gitnexus-shared/src/scope-resolution/language-classification.ts @@ -9,7 +9,8 @@ * Initial classification (locked in Ring 1 #910): * - production: javascript, typescript, python, java, c, cpp, csharp, go, * ruby, rust, php, kotlin, swift, dart - * - experimental: vue (embedded-language / SFC complexity), + * - experimental: objective-c (fork provider MVP), + * vue (embedded-language / SFC complexity), * cobol (regex-provider path) * - quarantined: (none) */ @@ -31,6 +32,7 @@ export const LanguageClassifications: Readonly = new Set = new Set([ 'Class', + 'Protocol', + 'Category', 'Interface', 'Enum', 'Struct', diff --git a/gitnexus-web/src/components/CodeReferencesPanel.tsx b/gitnexus-web/src/components/CodeReferencesPanel.tsx index 5818614f9..d16048fc7 100644 --- a/gitnexus-web/src/components/CodeReferencesPanel.tsx +++ b/gitnexus-web/src/components/CodeReferencesPanel.tsx @@ -16,7 +16,7 @@ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { useAppState } from '../hooks/useAppState'; import { type GraphNode, getSyntaxLanguageFromFilename } from 'gitnexus-shared'; import { NODE_COLORS } from '../lib/constants'; -import { readFile, type ReadFileResult } from '../services/backend-client'; +import { BackendError, readFile, type ReadFileResult } from '../services/backend-client'; import { useTranslation } from 'react-i18next'; const getSyntaxLanguage = (filePath: string | undefined): string => { @@ -205,6 +205,7 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = const CONTEXT_LINES = 50; // lines of context above and below the symbol const [fileResult, setFileResult] = useState(null); + const [sourceUnavailable, setSourceUnavailable] = useState(false); const [isLoadingFile, setIsLoadingFile] = useState(false); const selectedViewerRef = useRef(null); @@ -214,12 +215,14 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = useEffect(() => { if (!selectedFilePath) { setFileResult(null); + setSourceUnavailable(false); return; } let cancelled = false; setIsLoadingFile(true); setFileResult(null); + setSourceUnavailable(false); // Determine read range: full file for File nodes, buffered for symbols const startLine = selectedNode?.properties?.startLine as number | undefined; @@ -242,9 +245,12 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = setIsLoadingFile(false); } }) - .catch(() => { + .catch((error) => { if (!cancelled) { setFileResult(null); + setSourceUnavailable( + error instanceof BackendError && error.code === 'source_unavailable', + ); setIsLoadingFile(false); } }); @@ -384,7 +390,7 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = -
+
{isLoadingFile ? (
@@ -426,7 +432,9 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = ) : (
- {selectedIsFile ? ( + {sourceUnavailable ? ( + <>{t('graph:codePanel.sourceUnavailable')} + ) : selectedIsFile ? ( <>{t('graph:codePanel.codeNotAvailable', { path: selectedFilePath })} ) : ( <>{t('graph:codePanel.selectFile')} @@ -457,7 +465,7 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = {t('graph:codePanel.references', { count: aiReferences.length })}
-
+
{refsWithSnippets.map( ({ ref, content, start, highlightStart, highlightEnd, totalLines }) => { const nodeColor = ref.label diff --git a/gitnexus-web/src/components/FileTreePanel.tsx b/gitnexus-web/src/components/FileTreePanel.tsx index a3c7487a9..6d2a559aa 100644 --- a/gitnexus-web/src/components/FileTreePanel.tsx +++ b/gitnexus-web/src/components/FileTreePanel.tsx @@ -389,7 +389,7 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => {
{/* File tree */} -
+
{fileTree.length === 0 ? (
{t('graph:fileTree.noFilesLoaded')} @@ -413,7 +413,7 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => { )} {activeTab === 'filters' && ( -
+

{t('graph:fileTree.nodeTypes')} diff --git a/gitnexus-web/src/components/Header.tsx b/gitnexus-web/src/components/Header.tsx index 8e8d41d08..a1b777422 100644 --- a/gitnexus-web/src/components/Header.tsx +++ b/gitnexus-web/src/components/Header.tsx @@ -247,7 +247,7 @@ export const Header = ({ />

-
+
{filteredRepos.length === 0 ? (
{t('header:noRepositoriesFound', { query: repoSearchQuery })} diff --git a/gitnexus-web/src/components/ProcessesPanel.tsx b/gitnexus-web/src/components/ProcessesPanel.tsx index add4c255b..e89074feb 100644 --- a/gitnexus-web/src/components/ProcessesPanel.tsx +++ b/gitnexus-web/src/components/ProcessesPanel.tsx @@ -364,7 +364,7 @@ export const ProcessesPanel = () => {
{/* Process list */} -
+
{/* View All Processes Card */}
{showResults && queryResult.rows.length > 0 && ( -
+
diff --git a/gitnexus-web/src/components/RightPanel.tsx b/gitnexus-web/src/components/RightPanel.tsx index 7038895d8..9b1fa5ab3 100644 --- a/gitnexus-web/src/components/RightPanel.tsx +++ b/gitnexus-web/src/components/RightPanel.tsx @@ -301,7 +301,7 @@ export const RightPanel = () => { )} {/* Messages */} -
+
{chatMessages.length === 0 ? (
@@ -426,7 +426,7 @@ export const RightPanel = () => { onKeyDown={handleKeyDown} placeholder={t('chat:input.placeholder')} rows={1} - className="scrollbar-thin min-h-[36px] flex-1 resize-none border-none bg-transparent text-sm text-text-primary outline-none placeholder:text-text-muted" + className="min-h-[36px] flex-1 resize-none scrollbar-thin border-none bg-transparent text-sm text-text-primary outline-none placeholder:text-text-muted" style={{ height: '36px', overflowY: 'hidden' }} />