GitNexus/GUARDRAILS.md
Gergő Magyar 30676685e0
fix(rename): apply the exact reviewed edit plan (#3617)
* fix(rename): bind apply to reviewed plans (U7)

Retain exact semantic edits behind single-use preview tokens and reject
source, target, inventory and graph drift before application. Preserve
explicit partial-write accounting and keep large unedited sources covered
with hashes instead of retained text.

Refs #2973

* fix(rename): cover unindexed sources (U7)

Address PR #3617 review by binding every supported source to streamed byte
hashes and canonical targets, including tracked and untracked files absent
from the graph. Keep the exact reviewed edits and bound evidence memory.

Use conventional even-sample medians and extend the existing CI benchmark
to capture and validate unindexed sources without relaxing timing gates.

Note: pre-existing packed Tree-sitter GLIBCXX_3.4.31 host failure in
parse-cache-identity-package.test.ts is not addressed by this PR.

* fix(rename): preserve lazy startup (U7)

Keep language providers off the MCP startup path; load their existing
classification only when rename inventory is requested. The existing
12 startup closure checks now pass.

Address PR #3617 review by including token validation in reviewed apply
timing and growing indexed evidence before preview, preserving the
20MiB controls under a 256MiB native pool. Migrate dispatch and retention
tests to the preview token contract, keeping write-failure oracles.

Note: pre-existing auto-sync /tmp fixture security and installed ARM64
grammar-header failures remain outside this PR; the packed Tree-sitter
GLIBCXX_3.4.31 host failure also reproduces on unchanged main.

* test(rename): always remove apply fixture (U7)

Own the temporary directory from creation in an outer finally so setup
and preview assertions cannot bypass cleanup. Keep spy restoration in
its inner finally and preserve all write-failure assertions.

Address PR #3617 cleanup feedback.

* fix(rename): retain targets and config evidence

* test(rename): verify approved physical output

* test(rename): cover native filesystem matrix

* test(rename): align preview contract fixtures

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-17 19:38:01 +01:00

12 KiB

Guardrails — GitNexus

Rules for human contributors and AI agents. Complements AGENTS.md (workflows) and CONTRIBUTING.md (PR process).

Scope (least privilege)

  • Read: Source, tests, docs, public config as needed.
  • Write: Only files required for the fix or feature; no unrelated formatting or refactors.
  • Execute: Tests, typecheck, documented CLI commands. No destructive commands on user data without approval.
  • Off-limits: Other people's machines, production deployments you don't own, credentials you lack permission to use.

Maintainer may widen scope per task.


Non-negotiables

  1. Never commit secrets — API keys, tokens, real .env values, private URLs, session cookies. Use .env.example with placeholders.
  2. Never rename with find-and-replace in GitNexus-indexed projects — use rename with dry_run: true first (omitting dry_run also previews), review exact graph-backed semantic spans and coverage, then apply with rename, dry_run: false, and that preview's plan_token. Unsupported targets fail closed; inspect and refactor them manually with language-aware tools. No separate gitnexus rename CLI exists.
  3. Run impact analysis before editing shared symbols — impact (upstream) for functions/classes/methods others call. Do not ignore HIGH/CRITICAL without maintainer sign-off.
  4. Run detect_changes before commit — confirm diffs map to expected symbols/processes when the graph is available.
  5. Preserve embeddings — plain npx gitnexus analyze now preserves any embeddings recorded in the index metadata (.gitnexus/gitnexus.json, mirrored to the legacy meta.json) — the previous behavior wiped them. Use --embeddings to also generate vectors for new/changed nodes; use --drop-embeddings only when an explicit wipe is intended (e.g., model swap).
  6. Never terminate() a worker that may be inside a native call — killing a worker thread mid-N-API aborts the entire process (Napi::Error → std::terminate → SIGABRT, #2432), so a timeout meant to trigger a graceful fallback takes the whole run down instead. Any worker running native code (tree-sitter grammars, LadybugDB, Icebug) must either reach a JS-visible safe point first — the parse pool's shutdownDrainMs handshake in src/core/ingestion/workers/worker-pool.ts — or be abandoned with unref() and left to exit on its own. A one-shot worker that ends after a single postMessage needs no terminate() at all: it exits by itself. This bites hardest on the path you cannot test locally, because the abort only reproduces once the native module actually loads.

Signs (recurring failure patterns)

Format: Trigger → Instruction → Reason. Append new Signs when the same mistake repeats.

Stale graph after edits

  • Trigger: MCP warns index is behind HEAD, or search doesn't match latest commit.
  • Do: npx gitnexus analyze (plus --embeddings if used). Runs incrementally by default — the pipeline parses every file every run (cross-file resolution requires it), but tree-sitter dispatch is skipped for unchanged file chunks via the content-addressed cache, and only changed-file rows (plus their importers, transitively) are rewritten in LadybugDB. When the effective write set exceeds ~50% of the repo's files (minimum 50 files), the run transparently switches to the full wipe + bulk-COPY write plan and logs "switching to a full DB write" — expected behavior, not a bug, and file-level bookkeeping stays incremental. That same line also appears — regardless of write-set size, even for a one-file change — when a LadybugDB extension the existing index depends on cannot load on this machine (VECTOR, #2623; FTS, #2841), because a DB carrying those indexes refuses all row-level DML until the extension is loaded; run gitnexus doctor for live extension status and re-run with GITNEXUS_LBUG_EXTENSION_INSTALL=auto (with network access) to allow one bounded install attempt. The rebuild is one-shot: it clears the indexes, so the next run goes back to the incremental plan.
  • Why: Tools query LadybugDB from last analyze; git changes are invisible until re-indexed.

Index seems corrupt or "incremental" is misbehaving

  • Trigger: analyze produces unexpected results, or incrementalInProgress is set in the index metadata (.gitnexus/gitnexus.json / legacy meta.json), or the index is in a half-state after a crash.
  • Do: npx gitnexus analyze --force to rebuild the graph and FTS indexes. This may reuse unchanged parser output; when debugging parser/capture changes, use npx gitnexus analyze --no-parse-cache to rebuild that output too. The dirty-flag check forces the graph rebuild automatically when a previous incremental run didn't complete cleanly. A dirty-flag recovery rebuild parks the interrupted run's sidecars beside the DB as lbug.wal.dirty-recovery / lbug.shadow.dirty-recovery for post-mortem debugging — harmless, and removable with npx gitnexus clean --lbug-sidecars. Safe to delete the .gitnexus/parse-cache/ directory (and any legacy .gitnexus/parse-cache.json) at any time — content-addressed, will be regenerated.
  • Why: Incremental writeback is selective DB row replacement; if the on-disk state is inconsistent for any reason, a full rebuild is the cheapest path back to a known-good index.

Embeddings vanished after analyze

  • Trigger: Semantic search quality drops; stats.embeddings in the index metadata (gitnexus.json / legacy meta.json) is 0 after refresh.
  • Do: Re-run npx gitnexus analyze --embeddings to regenerate. Check the analyze log for a Warning: could not load cached embeddings line — if present, the cache restore failed (corrupt DB / schema mismatch) and the rebuild had nothing to preserve. If you intentionally passed --drop-embeddings, this is expected.
  • Why: Plain analyze preserves prior vectors by re-inserting them after the rebuild; ways to end up at zero include an explicit --drop-embeddings, a cache-load failure (now logged), or a model/dimension change that invalidates the cache — but zero is no longer the only embedding-loss signature to watch for; see the Sign below for the non-zero, partial-failure case. A dirty-recovery run that cannot move the crashed WAL aside now either discards it (logged: forensics lost, embeddings still preserved) or fails fast with a lock error naming the holder — it never silently zeroes embeddings.

Analyze finishes but embeddings are incomplete (partial embedding index)

  • Trigger: npx gitnexus status reports incompleteReasons: ["embedding-checkpoint-pending"] (or the human-readable "Index incomplete reasons" line); stats.embeddings is honest and non-zero, and the preceding analyze log showed a Warning: N node(s) lost their embeddings to embedding-endpoint failures line (#2790).
  • 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.

Scope extraction is incomplete

  • Trigger: npx gitnexus status reports incompleteReasons: ["scope-extraction-failed"] when files were omitted, or incompleteReasons: ["scope-extraction-unverified"] when the index predates the completeness receipt or its metadata is unreadable. impact/context reports the same uncertainty as epistemic: "lower-bound"; confirmed omissions set causes.scopeExtractionFiles > 0.
  • Do: Re-run npx gitnexus analyze (--force for a full graph rebuild). If the reason persists, inspect the scope-extraction warnings and treat impact counts as floors until the affected source is supported or corrected.
  • Why: Parsing continued, but scope captures for the reported file count could not be produced even after the main-thread fallback. Calls, inheritance, imports, or accesses originating there may therefore be absent from the graph.

Analyze reports INCOMPLETE with a collapsed graph write

  • Trigger: Analysis rejects a staged graph write with expected and persisted relationship counts, or an in-place run prints Repository indexed INCOMPLETELY, exits nonzero, and records incompleteReasons: ["graph-write-collapsed"].
  • Do: Inspect COPY errors, available memory, and free disk space on the volume holding .gitnexus/; confirm no second analyze is running against the same repo, then run npx gitnexus doctor. For native buffer-pool exhaustion, see RUNBOOK.md for GITNEXUS_LBUG_BUFFER_POOL_SIZE and memory-budget guidance. Re-run npx gitnexus analyze --force after addressing the cause.
  • Why: Far fewer relationships are readable back than the pipeline produced. A staged build throws before publication and preserves the previous index, if any. In-place writes cannot roll back, so the incomplete stamp and nonzero exit prevent a partial graph from being presented as successful. Recognized buffer exhaustion also throws immediately during COPY or fallback inserts. The collapse check compares in-memory totals (including streamed rows) against the post-write count, stays unmeasurable 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.
  • Do: npx gitnexus analyze in the target repo; verify npx gitnexus list shows it.
  • Why: MCP discovers repos via ~/.gitnexus/registry.json, populated by analyze.

Wrong repo in multi-repo setups

  • Trigger: Query/impact results belong to another project.
  • Do: Confirm an MCP default is configured or the GitNexus process was launched inside the intended registered path without crossing into an unindexed nested Git checkout. Otherwise call list_repos, then pass repo on subsequent tools; pass it for mutating tools when multiple repos are registered and no MCP default exists.
  • Why: Read-only tools derive their default from MCP configuration or a process cwd that stays within one registered Git boundary. Outside those paths the target remains ambiguous, and mutating tools stay explicit unless configuration supplies the target.

LadybugDB lock / "database busy"

  • Trigger: Errors opening .gitnexus/lbug while MCP and analyze both run.
  • Do: Stop overlapping processes (one writer at a time). Retry analyze or restart MCP.
  • Why: Embedded DB expects single-process ownership. @ladybugdb/core 0.18.0 also reports this contention as "Only one write transaction at a time is allowed in the system." — our busy/lock retry matcher (isDbBusyError in src/core/lbug/lbug-config.ts) recognizes this exact string too, so it's auto-retried the same as any other lock error. If you see that exact message, it's the same "one writer at a time" issue above, not a new failure mode.

Publishing & supply chain

  • npm: Do not publish from unreviewed automation. Bump version intentionally; tag releases to match package.json.
  • Dependencies: Minimal, auditable package.json changes; run tests and CI after lockfile updates.
  • License: PolyForm Noncommercial 1.0.0 — do not relicense without maintainer approval.

Escalation

Stop and ask a human maintainer when:

  • Impact analysis shows HIGH/CRITICAL risk and the task still requires the change.
  • You need to alter CI, release, or security-sensitive config.
  • Requirements conflict (e.g. "speed up analyze" vs "must keep all embeddings on huge repo").
  • You are unsure whether data loss is acceptable (clean, forced migrations, schema changes).