From 411cac9b90a3562a59334f5275ff48f21437ce86 Mon Sep 17 00:00:00 2001 From: ReidenXerx Date: Thu, 6 Aug 2026 21:58:43 +0300 Subject: [PATCH] docs: document the new incomplete reason, the UNKNOWN verdict and the id churn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found the code changes landed without the guidance around them, and an agent following this repo's own rules would have been told the wrong thing. `graph-write-collapsed` joined `INDEX_INCOMPLETE_REASONS` with no Sign block and no recovery section, while the precedent it cites (`embedding-checkpoint-pending`) has both — so `gitnexus status` would surface a new string naming silent wrong answers with nothing explaining trigger or remedy. Added to RUNBOOK and GUARDRAILS, including why this reason alone also fails the exit code. `AGENTS.md` said MUST warn on HIGH or CRITICAL and never mentioned UNKNOWN, and the shipped impact skill's risk table had no UNKNOWN row and still implied few-callers ⇒ LOW. An agent obeying those rules literally sees `risk: UNKNOWN` and proceeds, which negates the change the verdict exists to make. Both copies of both skills updated. `MIGRATION.md` now records that process ids do not survive this release — positional ids plus depth-first tracing, source-order siblings and round-robin selection mean `proc_7_handle` is a different flow afterwards. Bounded honestly: nothing in-repo joins on a raw process id, so it is index churn, not a broken consumer. `ARCHITECTURE.md`'s scope-resolution stage list gains the two new stages. The guide skill's node list gains `Property` and `TypeAlias` — the two node types this work most prominently creates. Also, on the pair-CSV preflight review asked to confirm: the hard abort IS deliberate, because a fallback recovering zero rows is the confident-empty failure this work targets. But the transient the message itself names — a second concurrent analyze sharing `.gitnexus/csv` — is a race, so the check now re-looks three times over ~150ms before declaring the file gone. Long enough to ride out a rename, far too short to mask a file that is genuinely missing. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/gitnexus-guide/SKILL.md | 2 +- .../skills/gitnexus-impact-analysis/SKILL.md | 8 ++++ AGENTS.md | 3 +- ARCHITECTURE.md | 3 ++ GUARDRAILS.md | 6 +++ MIGRATION.md | 13 ++++++ RUNBOOK.md | 10 +++++ .../skills/gitnexus-guide/SKILL.md | 2 +- .../skills/gitnexus-impact-analysis/SKILL.md | 8 ++++ .../passes/imported-value-refs.ts | 3 +- .../passes/unique-name-properties.ts | 3 +- .../scope-resolution/pipeline/run.ts | 14 +++--- gitnexus/src/core/lbug/lbug-adapter.ts | 44 ++++++++++++++----- gitnexus/src/storage/repo-manager.ts | 18 ++++---- .../impact-zero-caller-risk.test.ts | 14 +++++- .../resolvers/typescript-alias-fields.test.ts | 20 ++++----- 16 files changed, 125 insertions(+), 46 deletions(-) diff --git a/.claude/skills/gitnexus-guide/SKILL.md b/.claude/skills/gitnexus-guide/SKILL.md index e52560422..36bc4fd40 100644 --- a/.claude/skills/gitnexus-guide/SKILL.md +++ b/.claude/skills/gitnexus-guide/SKILL.md @@ -139,7 +139,7 @@ Lightweight reads (~100-500 tokens) for navigation: ## Graph Schema -**Nodes:** File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process, Route, Tool, plus language-specific types (Struct, Enum, Trait, Impl, Namespace, Module, …) and BasicBlock (`--pdg` indexes only). The full node list lives in `gitnexus://repo/{name}/schema`. +**Nodes:** File, Folder, Function, Class, Interface, Method, Property, TypeAlias, CodeElement, Community, Process, Route, Tool, plus language-specific types (Struct, Enum, Trait, Impl, Namespace, Module, …) and BasicBlock (`--pdg` indexes only). The full node list lives in `gitnexus://repo/{name}/schema`. **Edges (via CodeRelation.type):** CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, CONTAINS, MEMBER_OF, HAS_METHOD, HAS_PROPERTY, ACCESSES, METHOD_OVERRIDES, METHOD_IMPLEMENTS, STEP_IN_PROCESS, HANDLES_ROUTE, FETCHES, HANDLES_TOOL, ENTRY_POINT_OF, WRAPS, QUERIES, INJECTS, plus `--pdg`-only types (CFG, REACHING_DEF, TAINTED, SANITIZES, TAINT_PATH, CDG — zero rows on a default index). Read `gitnexus://repo/{name}/schema` before writing Cypher — it is the authoritative schema for the indexed repo. diff --git a/.claude/skills/gitnexus-impact-analysis/SKILL.md b/.claude/skills/gitnexus-impact-analysis/SKILL.md index 0b81795de..2e34f86f6 100644 --- a/.claude/skills/gitnexus-impact-analysis/SKILL.md +++ b/.claude/skills/gitnexus-impact-analysis/SKILL.md @@ -53,6 +53,14 @@ description: "Use when the user wants to know what will break if they change som | 5-15 symbols, 2-5 processes | MEDIUM | | >15 symbols or many processes | HIGH | | Critical path (auth, payments) | CRITICAL | +| **Zero callers found** | **UNKNOWN** | + +`UNKNOWN` is not a low rung on this scale — it means the walk could not answer. +An empty caller set is equally consistent with "genuinely unused" and "the +callers are not resolvable by the index" (plain-object property access, dynamic +dispatch, cross-language calls), so few-callers ⇒ LOW does **not** apply. The +result carries a `riskNote` saying so. Confirm with a text search before +treating the symbol as safe to change or delete. ## Tools diff --git a/AGENTS.md b/AGENTS.md index 032a3a6b6..f4fcef0af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,6 +120,7 @@ This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 rela - **MUST run impact analysis before editing.** Use `impact({target: "symbolName", direction: "upstream"})` (MCP) or `node .gitnexus/run.cjs impact "symbolName" --direction upstream --repo .` (CLI fallback); report callers, processes, and risk. Never substitute grep for graph analysis. For unified PDG impact, add `mode: "pdg"` with optional `line: ` — it returns statement-level `affectedStatements` over CDG + REACHING_DEF and inter-procedural symbols in `interproceduralByDepth`/`byDepth`; no-layer/degraded PDG results are UNKNOWN-risk notes (`--pdg` layer). CLI equivalent: `node .gitnexus/run.cjs impact "symbolName" --direction upstream --mode pdg --line --repo .`. - **MUST analyze graph changes before committing.** Use `detect_changes({scope: "all"})` (MCP) or `node .gitnexus/run.cjs detect-changes --scope all --repo .` (CLI fallback). For regression review: `detect_changes({scope: "compare", base_ref: "main"})` or `node .gitnexus/run.cjs detect-changes --scope compare --base-ref "main" --repo .`. - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- **MUST treat `risk: UNKNOWN` as unresolved, not as low.** An empty caller set is not evidence the symbol is unused — it can also mean the callers are not resolvable by the index (plain-object property access, dynamic dispatch, cross-language calls). `impact` pairs `UNKNOWN` with a `riskNote` saying so. Confirm with a text search before treating the symbol as safe to change or delete; do not proceed on the strength of a zero. - When exploring unfamiliar code, use `query({search_query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. - When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`. - For security review, `explain({target: "fileOrSymbol"})` lists taint findings (source→sink flows; needs `analyze --pdg`). @@ -128,7 +129,7 @@ This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 rela ## Never Do - NEVER edit a function, class, or method before MCP/CLI impact analysis. -- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis, and never read `UNKNOWN` as an all-clear — it means the walk could not answer, which is the one verdict that requires confirming by other means. - NEVER rename symbols with find-and-replace — use `rename` which understands the call graph. - NEVER commit before MCP/CLI graph change analysis. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 42160627e..0590855ac 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -214,6 +214,9 @@ Language-agnostic scope-resolution resolver. This is the resolution path for eve │ emitReferencesViaLookup ── uses handledSites + deferred-site skip set │ emitPropertyDispatchCalls ── registration USES + conservative CALLS │ emitCallableValueFlow ── assigned/passed callable invocation CALLS + │ emitImportedValueReferences ── cross-file value reads via finalized imports + │ emitUniqueNamePropertyAccesses ── LAST-RESORT property reads by name, + │ narrowed same-file → direct-import, refusing to choose otherwise │ emitImportEdges ▼ KnowledgeGraph (IMPORTS / CALLS / ACCESSES / INHERITS / USES) diff --git a/GUARDRAILS.md b/GUARDRAILS.md index 3fe36a875..ba8c03761 100644 --- a/GUARDRAILS.md +++ b/GUARDRAILS.md @@ -52,6 +52,12 @@ Format: **Trigger → Instruction → Reason**. Append new Signs when the same m - **Do:** Re-run plain `npx gitnexus analyze` — no `--embeddings` flag needed. A retained `embeddingCheckpoint` in the index metadata forces embedding generation for exactly the pending nodes regardless of flags, and clears once they succeed. `--drop-embeddings` abandons the pending nodes instead of retrying them; `--force` also discards the checkpoint (with a warning) and rebuilds without resuming it. - **Why:** A long analyze run against a flaky HTTP embedding endpoint tolerates bounded sub-batch failures instead of aborting the whole run: it deletes the affected nodes' embedding rows (so they hold zero rows, never a partial set) and records those nodes as pending in `embeddingCheckpoint`. `stats.embeddings` stays an honest, non-zero count of everything that did succeed, so this state never trips the "Embeddings vanished" Sign above — `embedding-checkpoint-pending` is the only reliable signal. +### Analyze reports INCOMPLETE with a collapsed graph write + +- **Trigger:** `npx gitnexus status` reports `incompleteReasons: ["graph-write-collapsed"]`; the analyze summary printed `Repository indexed INCOMPLETELY` naming an expected and a persisted relationship count, and the CLI exited non-zero. +- **Do:** Re-run `npx gitnexus analyze --force`. If it recurs, check free disk space on the volume holding `.gitnexus/`, confirm no second `analyze` is running against the same repo (both stage through `.gitnexus/csv`), then run `npx gitnexus doctor`. +- **Why:** The run finished and wrote metadata, but far fewer relationships are readable back than the pipeline produced. Nothing throws: the DB holds rows and the metadata is valid, so every query answers with missing edges rather than an error — a confident empty answer, which is worse than a failure because it looks like a result. Unlike `incremental-in-progress` and `embedding-checkpoint-pending`, which describe a run that did what it said and left work for next time, this one means most of your edges are gone, so it is the one incomplete reason that also fails the exit code. The check compares in-memory totals (including rows streamed out of the heap) against the post-write count, refuses to answer when the count cannot be read, and is skipped on incremental runs where whole-scope counts are not comparable. + ### MCP lists no repos - **Trigger:** MCP stderr says no indexed repos. diff --git a/MIGRATION.md b/MIGRATION.md index 9c6c1de2d..f63d18c87 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -106,6 +106,19 @@ Running `npx gitnexus analyze` writes both `gitnexus.json` and `meta.json` with identical content. A pre-existing repo that only has `meta.json` gets `gitnexus.json` bootstrapped from it on the first run. +### Process ids are not stable across this release + +`Process` ids are positional (`proc__`), and this release changes +both which execution flows are detected and the order they are selected in: +tracing is depth-first, sibling branches follow source order, and selection +round-robins across terminals so one flow cannot take every slot. A given +`proc_7_handle` before the upgrade is not the same flow afterwards. + +Nothing in GitNexus persists or joins on a raw process id across a re-index — +the MCP resource keys by label — so this is one-time index churn rather than a +broken consumer. If you have external tooling that stored a process id, re- +resolve it by label after the next analyze. + ### What about rollback? Downgrading to an older GitNexus version is safe: `meta.json` is always diff --git a/RUNBOOK.md b/RUNBOOK.md index e34d20c2b..0f5c8b7bb 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -66,6 +66,16 @@ npx gitnexus analyze No `--embeddings` flag needed — a retained checkpoint forces embedding generation for the pending nodes regardless of flags, and clears once they succeed. `--drop-embeddings` abandons the pending nodes instead of retrying them; `--force` also discards the checkpoint (with a warning) and rebuilds without resuming it. +**Collapsed graph write (analyze exits NON-ZERO and says INCOMPLETE):** A run can finish writing metadata while only a fraction of the relationships it produced are readable back from the index — edges collapsing to a small share of what was built, or a `CodeRelation` table that never materialized (which reads as a persisted count of zero). Because the metadata IS written and the DB does hold rows, nothing looks broken: queries answer with missing edges rather than an error, which is a confident empty answer rather than a failure. `npx gitnexus status` reports `incompleteReasons: ["graph-write-collapsed"]`, the analyze summary prints `Repository indexed INCOMPLETELY` with the expected and persisted counts, and the CLI exits non-zero so automation is not told an unusable index is fine. + +Recovery is a full rebuild: + +```bash +npx gitnexus analyze --force +``` + +If it recurs, the cause is almost always environmental rather than a code defect: check free disk space on the volume holding `.gitnexus/`, make sure no second `analyze` is running against the same repo (both use `.gitnexus/csv` for staging), then run `npx gitnexus doctor`. The check compares in-memory relationship totals (including streamed rows) against what the DB hands back, and is deliberately skipped on incremental runs, where the two counts are not comparable. + **Large repos:** Analyze may skip or limit embedding work when node counts are very high; watch CLI output. --- diff --git a/gitnexus-claude-plugin/skills/gitnexus-guide/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-guide/SKILL.md index e52560422..36bc4fd40 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-guide/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-guide/SKILL.md @@ -139,7 +139,7 @@ Lightweight reads (~100-500 tokens) for navigation: ## Graph Schema -**Nodes:** File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process, Route, Tool, plus language-specific types (Struct, Enum, Trait, Impl, Namespace, Module, …) and BasicBlock (`--pdg` indexes only). The full node list lives in `gitnexus://repo/{name}/schema`. +**Nodes:** File, Folder, Function, Class, Interface, Method, Property, TypeAlias, CodeElement, Community, Process, Route, Tool, plus language-specific types (Struct, Enum, Trait, Impl, Namespace, Module, …) and BasicBlock (`--pdg` indexes only). The full node list lives in `gitnexus://repo/{name}/schema`. **Edges (via CodeRelation.type):** CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, CONTAINS, MEMBER_OF, HAS_METHOD, HAS_PROPERTY, ACCESSES, METHOD_OVERRIDES, METHOD_IMPLEMENTS, STEP_IN_PROCESS, HANDLES_ROUTE, FETCHES, HANDLES_TOOL, ENTRY_POINT_OF, WRAPS, QUERIES, INJECTS, plus `--pdg`-only types (CFG, REACHING_DEF, TAINTED, SANITIZES, TAINT_PATH, CDG — zero rows on a default index). Read `gitnexus://repo/{name}/schema` before writing Cypher — it is the authoritative schema for the indexed repo. diff --git a/gitnexus-claude-plugin/skills/gitnexus-impact-analysis/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-impact-analysis/SKILL.md index 0b81795de..2e34f86f6 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-impact-analysis/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-impact-analysis/SKILL.md @@ -53,6 +53,14 @@ description: "Use when the user wants to know what will break if they change som | 5-15 symbols, 2-5 processes | MEDIUM | | >15 symbols or many processes | HIGH | | Critical path (auth, payments) | CRITICAL | +| **Zero callers found** | **UNKNOWN** | + +`UNKNOWN` is not a low rung on this scale — it means the walk could not answer. +An empty caller set is equally consistent with "genuinely unused" and "the +callers are not resolvable by the index" (plain-object property access, dynamic +dispatch, cross-language calls), so few-callers ⇒ LOW does **not** apply. The +result carries a `riskNote` saying so. Confirm with a text search before +treating the symbol as safe to change or delete. ## Tools diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/imported-value-refs.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/imported-value-refs.ts index a95ca87c1..6e47b8b88 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/imported-value-refs.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/imported-value-refs.ts @@ -28,6 +28,7 @@ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexe import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js'; import { tryEmitEdge } from '../graph-bridge/edges.js'; import { findValueBindingInScope } from '../scope/walkers.js'; +import { callableFlowSiteKey } from './callable-value-flow.js'; /** * Confidence for a reference resolved through a finalized import binding. @@ -58,7 +59,7 @@ export function emitImportedValueReferences( // A member read (`obj.field`) is the receiver-bound passes' business; // this pass exists for the BARE identifier an import binds. if (site.explicitReceiver !== undefined) continue; - const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`; + const siteKey = callableFlowSiteKey(parsed.filePath, site.atRange); if (skipSites.has(siteKey)) continue; const def = findValueBindingInScope(site.inScope, site.name, indexes); diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts index 604383b27..d175dbd4c 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts @@ -68,6 +68,7 @@ import type { KnowledgeGraph } from '../../../graph/types.js'; import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js'; import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js'; import { resolveCallerGraphId } from '../graph-bridge/ids.js'; +import { callableFlowSiteKey } from './callable-value-flow.js'; /** * Confidence for a workspace-unique name match. Deliberately the global tier's @@ -277,7 +278,7 @@ export function emitUniqueNamePropertyAccesses( // is no object whose member this could be, and matching one by name // would link a local variable to an unrelated object's key. if (site.explicitReceiver === undefined) continue; - const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`; + const siteKey = callableFlowSiteKey(parsed.filePath, site.atRange); if (skipSites.has(siteKey)) continue; const candidates = byName.get(site.name); diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts index 6ff612b8d..43d26a955 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts @@ -163,9 +163,7 @@ function preEmitInheritanceEdges( if (site.kind !== 'inherits') continue; const scope = scopes.scopeTree.getScope(site.inScope); const siteKey = - scope?.filePath !== undefined - ? `${scope.filePath}:${site.atRange.startLine}:${site.atRange.startCol}` - : undefined; + scope?.filePath !== undefined ? callableFlowSiteKey(scope.filePath, site.atRange) : undefined; if (siteKey !== undefined) { // Intentionally suppress every `inherits` site from the generic // reference bridge, even when this pre-pass can't emit an EXTENDS @@ -441,12 +439,12 @@ interface RunScopeResolutionStats { * #2437 false-safe gap for exactly those keys (names are in the warn log). */ readonly propertyDispatchSkippedKeys: number; - /** - * ACCESSES edges recovered by workspace-unique property name (A1/A5) — the - * last-resort pass for receivers no precise pass could type. - */ /** Cross-file value references resolved through finalized import bindings. */ readonly importedValueRefEdges: number; + /** + * ACCESSES edges recovered by property NAME (A1/A5) — the last-resort pass + * for receivers no precise pass could type. + */ readonly uniqueNamePropertyEdges: number; /** * Read/write sites left unresolved because two or more `Property` defs share @@ -1001,7 +999,7 @@ export function runScopeResolution( const fromFilePath = indexes.scopeTree.getScope(fromScope)?.filePath; if (fromFilePath === undefined) continue; for (const ref of refs) { - uniqueNameSkipSites.add(`${fromFilePath}:${ref.atRange.startLine}:${ref.atRange.startCol}`); + uniqueNameSkipSites.add(callableFlowSiteKey(fromFilePath, ref.atRange)); } } // Gated on the language's own field-name-fallback policy. A statically-typed diff --git a/gitnexus/src/core/lbug/lbug-adapter.ts b/gitnexus/src/core/lbug/lbug-adapter.ts index 21745f095..d00206446 100644 --- a/gitnexus/src/core/lbug/lbug-adapter.ts +++ b/gitnexus/src/core/lbug/lbug-adapter.ts @@ -978,14 +978,6 @@ const copyCsvWithRetry = async ( } }; -/** - * Bulk-COPY every node CSV sequentially on the single writable connection - * (LadybugDB allows one write txn at a time). Extracted from loadGraphToLbug so - * it can run either at the node-phase boundary — overlapping the relationship - * emit pass (#2203) — or after emit in the serial escape-hatch path. Each COPY - * keeps the IGNORE_ERRORS=true retry; a hard failure throws (no node rows ⇒ the - * relationship COPY would dangle on missing endpoints). - */ /** * A staging CSV named in the COPY manifest is gone by the time COPY runs. * @@ -1005,6 +997,38 @@ export const missingStagingCsvError = (table: string, csvPath: string, rows: num `\`gitnexus analyze --force\`.`, ); +/** + * Bulk-COPY every node CSV sequentially on the single writable connection + * (LadybugDB allows one write txn at a time). Extracted from loadGraphToLbug so + * it can run either at the node-phase boundary — overlapping the relationship + * emit pass (#2203) — or after emit in the serial escape-hatch path. Each COPY + * keeps the IGNORE_ERRORS=true retry; a hard failure throws (no node rows ⇒ the + * relationship COPY would dangle on missing endpoints). + */ +/** + * Re-check a staging CSV a few times before declaring it gone. + * + * Review asked whether turning a silent degrade into a hard abort was + * deliberate. It is — a fallback that recovers zero rows is exactly the + * confident-empty failure this work is about, so failing loud is right. But the + * transient the error message itself names, a second concurrent `analyze` + * sharing `.gitnexus/csv`, is a RACE, and aborting a multi-minute rebuild on + * one stat() is a harsh answer to a file that may reappear microseconds later. + * + * Bounded and short: three extra looks over ~150ms total. Long enough to ride + * out a rename or a slow network filesystem, far too short to mask a file that + * is genuinely gone. + */ +const stagingCsvExists = async (csvPath: string): Promise => { + const RETRY_DELAYS_MS = [25, 50, 75]; + if (existsSync(csvPath)) return true; + for (const delay of RETRY_DELAYS_MS) { + await new Promise((resolve) => setTimeout(resolve, delay)); + if (existsSync(csvPath)) return true; + } + return false; +}; + const copyNodeCSVs = async ( targetConn: lbug.Connection, nodeFileEntries: [NodeTableName, { csvPath: string; rows: number }][], @@ -1016,7 +1040,7 @@ const copyNodeCSVs = async ( stepsDone++; log(`Loading nodes ${stepsDone}/${totalSteps}: ${table} (${rows.toLocaleString()} rows)`); - if (!existsSync(csvPath)) throw missingStagingCsvError(table, csvPath, rows); + if (!(await stagingCsvExists(csvPath))) throw missingStagingCsvError(table, csvPath, rows); const copyQuery = getCopyQuery(table, normalizeCopyPath(csvPath)); await copyCsvWithRetry(targetConn, copyQuery, (retryErr) => { @@ -1238,7 +1262,7 @@ export const loadGraphToLbug = async ( // Same guarantee as the node COPY: a pair file only reaches this loop // with rows on it, so an absent file means it vanished mid-run. This is // the `rel_Folder_File.csv` ENOENT the field reports end on. - if (!existsSync(pairCsvPath)) { + if (!(await stagingCsvExists(pairCsvPath))) { throw missingStagingCsvError(`${fromLabel} -> ${toLabel}`, pairCsvPath, rows); } const normalizedPath = normalizeCopyPath(pairCsvPath); diff --git a/gitnexus/src/storage/repo-manager.ts b/gitnexus/src/storage/repo-manager.ts index 9da04bde5..f03181f08 100644 --- a/gitnexus/src/storage/repo-manager.ts +++ b/gitnexus/src/storage/repo-manager.ts @@ -303,15 +303,6 @@ export interface RepoMeta { * Map keys are repo-relative paths. */ fileHashes?: Record; - /** - * Crash-recovery dirty flag — a generic marker written to the metadata - * file (gitnexus.json + its meta.json mirror) BEFORE any destructive DB - * mutation by BOTH writeback branches (incremental since its introduction; - * full rebuilds over an existing meta since #2099 F1); cleared on success - * by overwriting the metadata file. If a run crashes between, the next - * run sees the flag and forces a full rebuild — the cheapest path back - * to a known-good index. - */ /** * Set when a run finished but the persisted edge count came back far short * of what the pipeline produced — the B2 "refresh reports SUCCESS while the @@ -331,6 +322,15 @@ export interface RepoMeta { /** Relationships readable from the DB after the write. */ persisted: number; }; + /** + * Crash-recovery dirty flag — a generic marker written to the metadata + * file (gitnexus.json + its meta.json mirror) BEFORE any destructive DB + * mutation by BOTH writeback branches (incremental since its introduction; + * full rebuilds over an existing meta since #2099 F1); cleared on success + * by overwriting the metadata file. If a run crashes between, the next + * run sees the flag and forces a full rebuild — the cheapest path back + * to a known-good index. + */ incrementalInProgress?: { /** When the run started (epoch ms). */ startedAt: number; diff --git a/gitnexus/test/integration/impact-zero-caller-risk.test.ts b/gitnexus/test/integration/impact-zero-caller-risk.test.ts index 684432646..305fb51ba 100644 --- a/gitnexus/test/integration/impact-zero-caller-risk.test.ts +++ b/gitnexus/test/integration/impact-zero-caller-risk.test.ts @@ -16,7 +16,7 @@ import { it, expect, beforeAll, vi } from 'vitest'; import { LocalBackend } from '../../src/mcp/local/local-backend.js'; import { listRegisteredRepos } from '../../src/storage/repo-manager.js'; -import { withTestLbugDB } from '../helpers/test-indexed-db.js'; +import { withTestLbugDB, type IndexedDBHandle } from '../helpers/test-indexed-db.js'; vi.mock('../../src/storage/repo-manager.js', () => ({ listRegisteredRepos: vi.fn().mockResolvedValue([]), @@ -38,12 +38,22 @@ const SEED = [ `CREATE (t2:Function {id: 'Function:src/b.ts:orphanTwin', name: 'orphanTwin', filePath: 'src/b.ts', startLine: 1, endLine: 3, isExported: true, content: '', description: ''})`, ]; +type BackendHandle = IndexedDBHandle & { _backend?: LocalBackend }; + withTestLbugDB( 'impact-zero-caller-risk', (handle) => { let backend: LocalBackend; beforeAll(() => { - backend = (handle as any)._backend; + // Typed and null-checked, matching `caller-identity-regression.test.ts` + // in this directory. An `as any` read here turns "the harness never + // attached the backend" into an undefined-property crash several lines + // later instead of a message naming the cause. + const ext = handle as BackendHandle; + if (!ext._backend) { + throw new Error('LocalBackend not initialized — afterSetup did not attach _backend'); + } + backend = ext._backend; }); it('reports UNKNOWN, not LOW, when an upstream walk resolves no callers', async () => { diff --git a/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts b/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts index 74e839917..80e68e4bf 100644 --- a/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts +++ b/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts @@ -59,20 +59,16 @@ describe('TypeScript type-alias and interface members (A4)', () => { expect(nodesOfLabel('Property')).toContain('ifaceSlots'); }); - // The EDGES are not landed yet — nodes and declarations are. + // Member edges land through the PRECISE path only. The shape is a class-like + // scope (`interface_declaration` and `type_alias_declaration value: + // (object_type)` both emit `@scope.class`) and `property_signature` emits + // `@declaration.property`, so a typed receiver resolves to the shape's scope + // and finds the member there. // - // Established: the shape is a class-like scope already (`interface_declaration` - // and `type_alias_declaration value:(object_type)` both emit `@scope.class`), - // and `property_signature` now emits `@declaration.property` alongside the - // pre-existing `method_signature` -> `@declaration.method`. So the receiver - // has a scope and the scope has members, yet no ACCESSES forms — the missing - // link is owner/type-binding, i.e. the member def carrying an `ownerId` that - // the typed receiver resolves to via `findOwnedMember`. - // - // There is deliberately NO name-based safety net here: TypeScript sets + // There is deliberately NO name-based safety net: TypeScript sets // `fieldFallbackOnMethodLookup: false` (scope-resolver.ts) because name - // matching over-connects in a typed language, and the unique-name pass - // honors that opt-out. The precise path is the only route for TS, by design. + // matching over-connects in a typed language, and the unique-name pass honors + // that opt-out. The precise path is the only route for TS, by design. it('links an interface field to its consumer', () => { expect(readersOf('ifaceSlots')).toContain('renderIface'); });