# Invariants, Failure Modes & Safe-Change Rules

> A synthesis of every load-bearing constraint in the system. Hard invariants: (1) use web-tree-sitter (WASM) only — native bindings break on darwin/arm64 + Node 24; (2) dashboard imports only browser-safe core subpath exports; (3) graphs inside git worktrees are redirected to the main repo root; (4) all five version fields must be bumped in sync when releasing. Key failure modes: stale graph after code changes (fix: run /understand or enable autoUpdate), broken incremental update when lastCommitHash is missing from config.json (fix: --full rebuild), dashboard blank on schema mismatch (fix: check WarningBanner, validate graph JSON against schema.ts). Safe-change rules: adding a new language extractor only requires a new file under extractors/ plus registry entry; adding a new edge type requires updating schema.ts alias maps and the EDGE_CATEGORY_MAP in store.ts; dashboard layout changes are isolated to components/ and never touch core.

- Repository: Lum1104/Understand-Anything
- GitHub: https://github.com/Lum1104/Understand-Anything
- Human wiki: https://grok-wiki.com/public/wiki/lum1104-understand-anything-3b923df96896
- Complete Markdown: https://grok-wiki.com/public/wiki/lum1104-understand-anything-3b923df96896/llms-full.txt

## Source Files

- `understand-anything-plugin/packages/core/src/schema.ts`
- `understand-anything-plugin/packages/core/src/staleness.ts`
- `understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts`
- `understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx`
- `understand-anything-plugin/CLAUDE.md`
- `understand-anything-plugin/skills/understand/SKILL.md`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [understand-anything-plugin/packages/core/src/schema.ts](understand-anything-plugin/packages/core/src/schema.ts)
- [understand-anything-plugin/packages/core/src/staleness.ts](understand-anything-plugin/packages/core/src/staleness.ts)
- [understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts](understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts)
- [understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx](understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx)
- [understand-anything-plugin/packages/core/src/plugins/extractors/index.ts](understand-anything-plugin/packages/core/src/plugins/extractors/index.ts)
- [understand-anything-plugin/packages/core/src/persistence/index.ts](understand-anything-plugin/packages/core/src/persistence/index.ts)
- [understand-anything-plugin/packages/core/src/types.ts](understand-anything-plugin/packages/core/src/types.ts)
- [understand-anything-plugin/packages/dashboard/src/store.ts](understand-anything-plugin/packages/dashboard/src/store.ts)
- [understand-anything-plugin/skills/understand/SKILL.md](understand-anything-plugin/skills/understand/SKILL.md)
- [understand-anything-plugin/src/__tests__/worktree-redirect.test.mjs](understand-anything-plugin/src/__tests__/worktree-redirect.test.mjs)
</details>

# Invariants, Failure Modes & Safe-Change Rules

This page collects every load-bearing constraint in the Understand-Anything system: hard invariants that must never be violated, failure modes a developer will encounter when those invariants slip, and a set of safe-change rules that describe the minimal, bounded edits needed to extend the system. Read this before touching tree-sitter configuration, graph schema, dashboard imports, or release scripts.

Understand-Anything is a multi-layer system: a CLI skill invokes LLM agents to produce a `knowledge-graph.json`, which a React dashboard consumes. Many of the invariants live at the seams between those layers — the graph schema, the persistence layer, and the boundary between browser-safe and Node.js-only code. Violating any one of them silently corrupts a downstream consumer; the failure often appears far from the cause.

---

## Hard Invariants

### 1. Use `web-tree-sitter` (WASM) — Never Native Bindings

`TreeSitterPlugin` imports `web-tree-sitter` exclusively and loads language grammars as `.wasm` files resolved via `require.resolve`. Native `tree-sitter` Node.js bindings are **not used** anywhere in the project.

```typescript
// understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts:1
import { createRequire } from "node:module";
// ...
const mod = await import("web-tree-sitter");
const ParserCls = mod.Parser;
```

The CLAUDE.md project file states the reason explicitly: native bindings fail on darwin/arm64 + Node 24. The `packages/core/package.json` records the dependency as `"web-tree-sitter": "^0.26.6"` — there is no `tree-sitter` (native) entry.

**What breaks if violated:** On Apple Silicon with Node 24, `import('tree-sitter')` throws a native-binding error at startup, preventing any structural analysis from running. Because `TreeSitterPlugin.init()` is guarded by an `_initialized` flag, a partially initialized plugin will return empty `StructuralAnalysis` objects (`{ functions: [], classes: [], imports: [], exports: [] }`) for every file rather than crashing loudly.

Sources: [understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts:125-197](), [understand-anything-plugin/packages/core/package.json]()

---

### 2. Dashboard Imports Only Browser-Safe Core Subpath Exports

The dashboard (`packages/dashboard`) must import from `@understand-anything/core/search`, `@understand-anything/core/types`, and `@understand-anything/core/schema` — never from the bare `@understand-anything/core` entry point.

```typescript
// understand-anything-plugin/packages/dashboard/src/store.ts:2-8
import { SearchEngine } from "@understand-anything/core/search";
import type { SearchResult } from "@understand-anything/core/search";
import type { GraphIssue } from "@understand-anything/core/schema";
import type { GraphNode, KnowledgeGraph, TourStep } from "@understand-anything/core/types";
```

The main entry point pulls in Node.js modules (`node:fs`, `node:path`, `child_process`, etc.) that Vite's browser bundler cannot resolve. The subpath exports expose only modules free of Node.js globals.

**What breaks if violated:** Vite will fail to bundle with errors like `"fs" is not defined` or `Cannot resolve "node:child_process"`. The dashboard will not build; or if built with a loose resolver, the runtime will throw on the first import.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:1-10]()

---

### 3. Graphs Inside Git Worktrees Are Redirected to the Main Repo Root

When `/understand` runs inside a git worktree (not the main checkout), `PROJECT_ROOT` is automatically redirected to the main repo root before writing any output. The detection compares `git rev-parse --git-dir` to `git rev-parse --git-common-dir`; they differ in a worktree.

```bash
# From understand-anything-plugin/skills/understand/SKILL.md Phase 0 step 1
COMMON_DIR=$(git -C "$PROJECT_ROOT" rev-parse --git-common-dir 2>/dev/null)
GIT_DIR=$(git -C "$PROJECT_ROOT" rev-parse --git-dir 2>/dev/null)
if [ -n "$COMMON_DIR" ] && [ -n "$GIT_DIR" ]; then
  COMMON_ABS=$(cd "$PROJECT_ROOT" && cd "$COMMON_DIR" 2>/dev/null && pwd -P)
  GIT_ABS=$(cd "$PROJECT_ROOT" && cd "$GIT_DIR" 2>/dev/null && pwd -P)
  if [ -n "$COMMON_ABS" ] && [ "$COMMON_ABS" != "$GIT_ABS" ]; then
    MAIN_ROOT=$(dirname "$COMMON_ABS")
    if [ -d "$MAIN_ROOT" ] && [ "${UNDERSTAND_NO_WORKTREE_REDIRECT:-0}" != "1" ]; then
      PROJECT_ROOT="$MAIN_ROOT"
    fi
  fi
fi
```

This behavior is tested in `src/__tests__/worktree-redirect.test.mjs`, which creates a real git worktree and verifies both redirect and opt-out behavior. The escape hatch `UNDERSTAND_NO_WORKTREE_REDIRECT=1` is available for intentional per-worktree graphs.

**What breaks if violated:** Claude Code worktrees are ephemeral; `.understand-anything/` written there is destroyed when the session ends, silently discarding the knowledge graph. The graph appears to be generated successfully but is then gone.

Sources: [understand-anything-plugin/skills/understand/SKILL.md:33-53](), [understand-anything-plugin/src/__tests__/worktree-redirect.test.mjs:65-89]()

---

### 4. All Five Version Fields Must Be Bumped in Sync on Release

The project carries five separate version fields that must stay identical. The CLAUDE.md lists them:

| File | Field |
|---|---|
| `understand-anything-plugin/package.json` | `"version"` |
| `understand-anything-plugin/.claude-plugin/plugin.json` | `"version"` |
| `.claude-plugin/plugin.json` | `"version"` |
| `.cursor-plugin/plugin.json` | `"version"` |
| `.copilot-plugin/plugin.json` | `"version"` |

Note: `.claude-plugin/marketplace.json` intentionally does **not** carry a version field — adding one causes marketplace schema validation failures.

**What breaks if violated:** Claude Code's plugin cache key is the version string from the marketplace entry. A version mismatch between the cache directory and the installed plugin causes the plugin to serve stale code until the user manually uninstalls and reinstalls. On Cursor or Copilot, plugins with mismatched versions may fail to activate.

Sources: [understand-anything-plugin/CLAUDE.md]() (Testing Local Plugin Changes and Versioning sections)

---

## Key Failure Modes

### Stale Graph After Code Changes

**Symptom:** The dashboard shows architecture that no longer matches the code — functions, classes, or modules that were renamed or deleted still appear as nodes.

**Root cause:** The graph is a snapshot keyed to a git commit hash (`project.gitCommitHash` in `knowledge-graph.json`). The staleness check in `staleness.ts` runs `git diff <lastCommitHash>..HEAD --name-only` to detect changed files. If `/understand` is never re-run, the graph never updates.

```typescript
// understand-anything-plugin/packages/core/src/staleness.ts:34-43
export function isStale(projectDir: string, lastCommitHash: string): StalenessResult {
  const changedFiles = getChangedFiles(projectDir, lastCommitHash);
  return {
    stale: changedFiles.length > 0,
    changedFiles,
  };
}
```

**Fix options:**
- Run `/understand` manually after committing changes.
- Run `/understand --auto-update` once to enable the commit hook (`autoUpdate: true` in `.understand-anything/config.json`). From that point, the graph updates automatically on each commit.

Sources: [understand-anything-plugin/packages/core/src/staleness.ts:13-43](), [understand-anything-plugin/packages/core/src/types.ts:116-120]()

---

### Broken Incremental Update: `lastCommitHash` Missing

**Symptom:** The incremental update path (`git diff <lastCommitHash>..HEAD`) fails or exits immediately with "Graph is up to date" even though files changed.

**Root cause:** Incremental updates read `gitCommitHash` from `.understand-anything/meta.json`. If `meta.json` is absent or corrupt, or if the graph was written without a meta file, the Phase 0 decision logic in the SKILL has no hash to diff against. The skill falls through to "No existing graph or meta → full analysis" — but a corrupt `meta.json` (present but unparseable) may silently skip the full rebuild.

```markdown
# understand-anything-plugin/skills/understand/SKILL.md Phase 0, step 7
| Condition | Action |
|---|---|
| No existing graph or meta | Full analysis (all phases) |
| Existing graph + changed files | Incremental update (re-analyze changed files only) |
```

**Fix:** Run `/understand --full` to force a complete rebuild. This always produces a fresh `meta.json` with the current `gitCommitHash`.

Sources: [understand-anything-plugin/skills/understand/SKILL.md:143-159](), [understand-anything-plugin/packages/core/src/persistence/index.ts:107-116]()

---

### Dashboard Blank or Broken: Schema Mismatch

**Symptom:** The dashboard shows a red banner ("Dashboard hit N fatal errors") or renders a blank graph view. The `WarningBanner` component is the visible signal.

**Root cause:** `loadGraph()` in `persistence/index.ts` calls `validateGraph()` on every load. `validateGraph` runs a four-tier pipeline:

```
Tier 1: sanitizeGraph  → null-to-empty-array coercions, lowercase enums
Tier 2: autoFixGraph   → missing fields defaulted, alias mapping
Tier 3: Drop           → invalid individual nodes or edges are silently removed
Tier 4: Fatal          → no valid nodes, missing project metadata, malformed collections
```

The `WarningBanner` component distinguishes fatal from non-fatal issues by color and copy text:

```typescript
// understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx:9-16
const hasFatal = issues.some((i) => i.level === "fatal");
const lines = hasFatal
  ? [
      "Some of these issues look like dashboard rendering bugs.",
      "Please file an issue at github.com/Lum1104/Understand-Anything/issues...",
    ]
  : [
      "These are LLM generation errors — not a system bug.",
      "You can ask your agent to fix these specific issues in knowledge-graph.json",
    ];
```

Fatal issues (red banner) indicate a bug in the dashboard or ELK layout. Non-fatal issues with dropped nodes (amber banner) indicate the LLM generated schema-invalid data.

**Fix:**
1. Open the dashboard and expand the `WarningBanner`.
2. Use the "Copy Issues" button to get the structured issue list.
3. For fatal issues: file a GitHub bug report with the copied text.
4. For dropped/auto-corrected issues: paste the issue list to your agent and ask it to repair `knowledge-graph.json` directly.
5. If the graph is too corrupt: re-run `/understand --full` to regenerate from scratch.

Sources: [understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx:8-43](), [understand-anything-plugin/packages/core/src/schema.ts:499-663]()

---

### Graph Validation Pipeline (Tier Summary)

```text
Input JSON
    │
    ▼ Tier 1: sanitizeGraph
    │  • null → [] for tour, layers
    │  • null → undefined for optional node fields
    │  • lowercase type, complexity, direction strings
    │
    ▼ Tier 2: autoFixGraph + normalizeGraph (alias maps)
    │  • Missing type    → "file"      (node)  / "depends_on" (edge)
    │  • Missing direction → "forward"
    │  • Missing weight  → 0.5; weight clamped [0,1]
    │  • "fn" → "function", "extends" → "inherits", etc.
    │
    ▼ Tier 3: Per-item validation (Zod parse)
    │  • Invalid nodes DROPPED (level: "dropped")
    │  • Edges with dangling source/target DROPPED
    │  • Layer/tour nodeIds filtered to surviving node IDs
    │
    ▼ Tier 4: Fatal checks
       • Not an object → fatal
       • Collections not arrays → fatal
       • Missing project metadata → fatal
       • No valid nodes remaining → fatal
```

Sources: [understand-anything-plugin/packages/core/src/schema.ts:499-663]()

---

## Safe-Change Rules

### Adding a New Language Extractor

**Scope:** `packages/core/src/plugins/extractors/` only. No changes needed to tree-sitter-plugin, schema, or dashboard.

**Steps:**
1. Create `<language>-extractor.ts` implementing the `LanguageExtractor` interface from `extractors/types.ts`.
2. Export the class from `extractors/index.ts` and add an instance to `builtinExtractors`:

```typescript
// understand-anything-plugin/packages/core/src/plugins/extractors/index.ts:24-34
export const builtinExtractors: LanguageExtractor[] = [
  new TypeScriptExtractor(),
  new PythonExtractor(),
  // ... add your new extractor here
];
```

3. Add a WASM grammar package for the language to the language config (in `packages/core/src/languages/configs/`). `TreeSitterPlugin` will pick it up automatically via the `builtinExtractors` registry.

**What you do NOT need to touch:** `tree-sitter-plugin.ts` (it registers extractors from `builtinExtractors` by default), `schema.ts`, `store.ts`, or any dashboard file.

Sources: [understand-anything-plugin/packages/core/src/plugins/extractors/index.ts:24-34](), [understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts:88-98]()

---

### Adding a New Edge Type

Adding an edge type touches two files: the schema (source of truth for LLM and validation) and the dashboard store (source of truth for UI filtering). Both must be updated together.

**Step 1 — `packages/core/src/schema.ts`:** Add the new edge type string to `EdgeTypeSchema`:

```typescript
// understand-anything-plugin/packages/core/src/schema.ts:4-14
export const EdgeTypeSchema = z.enum([
  "imports", "exports", "contains", ...
  // add "your_new_edge" here
]);
```

Optionally add LLM alias entries to `EDGE_TYPE_ALIASES` if agents are likely to produce variant spellings.

**Step 2 — `packages/core/src/types.ts`:** Mirror the new type in the `EdgeType` union.

**Step 3 — `packages/dashboard/src/store.ts`:** Add the new edge type to the appropriate category in `EDGE_CATEGORY_MAP`:

```typescript
// understand-anything-plugin/packages/dashboard/src/store.ts:31-40
export const EDGE_CATEGORY_MAP: Record<EdgeCategory, string[]> = {
  structural: ["imports", "exports", "contains", "inherits", "implements"],
  behavioral: ["calls", "subscribes", "publishes", "middleware"],
  // add "your_new_edge" to the correct category, or add a new EdgeCategory key
};
```

If the new edge belongs to a new category, `EdgeCategory`, `ALL_EDGE_CATEGORIES`, and `FilterState` must also be updated in `store.ts`.

**What you do NOT need to touch:** Dashboard layout components, `WarningBanner`, any skill or agent prompt (schema change is picked up automatically at validation time).

Sources: [understand-anything-plugin/packages/core/src/schema.ts:4-14, 78-125](), [understand-anything-plugin/packages/dashboard/src/store.ts:31-40]()

---

### Dashboard Layout Changes Are Isolated to `components/`

The dashboard renders a graph-first layout (75% graph + 360px sidebar). Layout changes — rearranging panels, resizing areas, swapping sidebar tabs — are contained within `packages/dashboard/src/components/`. They do not touch:

- `packages/core/` — any change there modifies schema, staleness, or persistence behavior for all consumers.
- `packages/dashboard/src/store.ts` — the Zustand store owns graph state and filter state; layout components read from it via selectors but do not own it.
- Skills or agents — they write `knowledge-graph.json`; they have no knowledge of dashboard layout.

The safe boundary for layout work: read state via store selectors, render it in components. State shape changes (new filter dimensions, new sidebar tabs that require new state fields) require a coordinated `store.ts` edit, but those are data-model changes, not layout changes.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:100-120]()

---

## Summary

The four non-negotiable invariants — WASM-only tree-sitter, browser-safe dashboard imports, worktree-to-main-root redirect, and five-file version sync — each protect a different layer boundary. Violations are often silent: the wrong grammar loader returns empty analysis, an unsafe import makes a build fail, a worktree graph evaporates, a version mismatch serves cached code. The three common failure modes (stale graph, missing `lastCommitHash`, schema mismatch) each have a clear fix path anchored to `--full` rebuild or the `WarningBanner` copy-to-agent workflow. Safe changes stay within their layer: new extractors live only in `extractors/`, new edge types update both `schema.ts` and `store.ts`'s `EDGE_CATEGORY_MAP` in lockstep, and dashboard layout work never leaves `components/`.

Sources: [understand-anything-plugin/packages/core/src/schema.ts:499-510]()
