Commit Graph

205 Commits

  • Merge pull request #204 from Lum1104/feat/semantic-batching-and-output-chunking
    fix(#159): semantic batching + bundled importMap + Phase 1 speedup
  • fix(dashboard): add explicit @source for Tailwind v4 detection
    Tailwind v4's default source detection walks the nearest .git and
    collects tracked files via git ls-files. When the dashboard sources
    sit inside a gitignored subtree of an ancestor repo (e.g. the default
    marketplace install path ~/.claude/plugins/cache/, which is ignored by
    ~/.claude/.gitignore), detection returns 0 files and the Oxide engine
    skips all utility generation — the dashboard renders unstyled.
    
    Adding explicit @source directives is the supported Tailwind v4 escape
    hatch and is a no-op for installs where automatic detection works.
    
    Verified: built CSS bundle jumps from ~9 KB to ~55 KB and utility
    classes (.flex, .grid, .absolute, .w-full, .h-full) are present.
    
    Fixes #179
  • chore(dependencies): update astro and vite versions in package.json and pnpm-lock.yaml
    Bumps astro to version 6.3.7 and vite to version 6.4.2 across relevant package files to ensure compatibility and access to the latest features.
  • chore(lint): switch to recommended baseline, fix errors, wire into CI
    - typescript-eslint preset: strict -> recommended for a usable first-pass
      baseline (per PR discussion); ratchet up in a follow-up.
    - Drop the projectService/parserOptions block. Neither `recommended` nor
      `strict` is type-aware, so it was unused; removing it also avoids the
      pnpm-workspace tsconfig-resolution failure mode flagged in review.
    - Add Node + browser globals via the `globals` package so .mjs scripts and
      the dashboard stop hitting `no-undef`.
    - Expand ignores: built bundles (**/public/**), Astro generated (.astro/),
      and .private/ (eval scratch). Cuts 2400+ errors in vendored output.
    - Allow `_`-prefixed unused vars/args/caught errors; skip irregular
      whitespace inside comments (json-parser intentionally embeds ZWSP-escaped
      block-comment examples in JSDoc).
    - Fix the residual 13 genuine errors: drop dead imports/vars, replace
      two `as any[]` in schema.ts with `Array<Record<string, unknown>>`,
      drop unused destructure in change-classifier, drop unused catch binding
      in extract-structure.mjs.
    - Add EOF newline to eslint.config.mjs.
    - Refresh pnpm-lock.yaml.
    - Add `pnpm lint` step to .github/workflows/ci.yml so the tooling
      actually enforces something.
    
    pnpm lint now exits 0 locally; 33+13 test files / 1445 tests still pass.
  • refactor(onboarding): theme tokens, lifted state, a11y
    - Replace hardcoded hex with var(--color-*) and CJK font stacks with
      var(--font-sans) / var(--font-heading) so the overlay tracks the theme
      picker and uses the project's typography (DM Serif Display).
    - Lift dismiss/visibility state to Dashboard (shouldShowOnboarding +
      showOnboarding useState + dismissOnboarding callback). Gate the
      Suspense mount with a boolean so the lazy chunk is only fetched on
      first visit, matching the PathFinderModal / KeyboardShortcutsHelp
      mount pattern.
    - Add capture-phase Escape handler (stopPropagation prevents the global
      shortcut chain from also firing) and role="dialog" / aria-modal /
      aria-labelledby on the card for screen readers.
  • fix(onboarding): include class/function in node-type description
    Schema (packages/core/src/schema.ts) defines node types: file, function,
    class, module, concept, config, document, ... The welcome step body only
    listed "file, concept, entity, claim", missing the most common code-side
    types. Updated all 6 locales (en / zh / zh-TW / ja / ko / ru) to mention
    file / class / function from code plus concept / entity / claim from the
    knowledge wiki.
  • fix(onboarding): wire to i18n + add missing ua-fade-in keyframes
    - Replace hardcoded Chinese strings with t.onboarding.* via useI18n
    - Add `onboarding` namespace to en / zh / zh-TW / ja / ko / ru locales
    - Inject @keyframes ua-fade-in (was referenced in inline style but
      never defined, so the overlay popped in instead of fading)
  • Merge pull request #142 from zhushen12580/feature/language-parameter
    feat: Add --language parameter for localized content generation
  • fix: Address code review feedback for PR #142
    - Remove invalid allowBuilds from pnpm-workspace.yaml (use onlyBuiltDependencies in root package.json)
    - Use data-testid for search input selector (fixes / keyboard shortcut for non-English locales)
  • feat(dashboard): allow ACCESS_TOKEN override via env var
    Honor UNDERSTAND_ACCESS_TOKEN if set, falling back to the random 16-byte
    hex token. Lets the dev token survive across server restarts so shared
    dashboard URLs don't rot, and makes the auth path easier to script in
    tests.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix: Wrap MobileLayout with I18nProvider; use outputLanguage key in config
    P1: MobileLayout was missing I18nProvider wrapper, causing useI18n
        to throw error on mobile devices. Now both desktop and mobile
        layouts are wrapped with I18nProvider.
    
    P2: SKILL.md used 'language' key but Dashboard reads 'outputLanguage'.
        Fixed config.json key name to match ProjectConfig type definition.
    
    All tests passed:
    - Core: 670 tests
    - Dashboard: 42 tests
  • feat(dashboard): Add i18n support for localized UI text
    - Add outputLanguage field to ProjectConfig type
    - Create /config.json endpoint in vite.config.ts
    - Build locale files for 5 languages (en, zh, zh-TW, ja, ko)
    - Add I18nProvider context and useI18n hook
    - Update 5 components (ProjectOverview, NodeInfo, FileExplorer, FilterPanel, PersonaSelector)
    - Dashboard reads language from config.json and displays localized UI
    
    All tests passed:
    - Core: 670 tests
    - Dashboard: 42 tests
  • fix(dashboard): reset fn toggle on view switch; hide detail toolbar in domain view
    - Issue 1: setDetailLevel now resets showFunctionsInClassView so the fn
      toggle doesn't resurrect when re-entering class view after a file-view
      round-trip.
    - Issue 2: detail-level toolbar (Files/+Classes/fn) now gated on
      viewMode !== "domain" so it doesn't render in domain view where it has
      no effect.
  • feat(dashboard): first-visit onboarding overlay
    Add a 5-step modal that walks new users through the dashboard's core operations
    on first visit. Auto-hides via localStorage after dismiss; can be force-shown
    with `?onboard=force` for screenshots and demos.
    
    ## What it teaches
    
    1. What the graph represents (entities/relations from code or wiki)
    2. Three view buttons (Overview / Learn / Deep Dive) — each answers a different
       question
    3. Search + node click — find by name, click for details panel
    4. Layer switch + Project Tour — drill into a category, or follow a guided
       walkthrough
    5. Hidden features (Filter / Export / Path / Theme) and Shift+? for keyboard
       shortcuts
    
    ## Design
    
    - Inline styles, no extra CSS file — easier to land in the existing structure
    - Lazy-loaded via Suspense like the other modals (KeyboardShortcutsHelp,
      PathFinderModal) so it ships in a separate chunk
    - Architectural-minimalism dark palette consistent with the existing dashboard:
      off-black surface, warm accent (#c8a882), Noto Serif SC headings, generous
      whitespace
    - localStorage key `ua-onboarding-dismissed-v1` — versioned so future content
      changes can re-trigger
    - Accessible: keyboard-navigable buttons, click-outside to close (without
      remembering dismiss), explicit "不再显示" / "Skip" affordance
    
    ## Tested
    
    - Windows 11 + Chrome via Playwright: 5 steps render, progress bar tracks,
      prev/next/dismiss/finish all work, localStorage persists dismiss across
      reloads, `?onboard=force` re-shows for testing
    - No new dependencies (uses React 19 hooks already present)
    - No changes to data flow, store, or other components — strictly additive
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • Merge pull request #122 from Lum1104/feat/issue-113-tested-by-coverage
    feat: deterministic tested_by edges + dashboard badge (#113)
  • feat(dashboard): add file/class dual-view toggle to reduce graph clutter
    Add detailLevel state ("file" | "class") to separate architecture-level
    file dependencies from code-structure class views. File view shows only
    file nodes and file→file edges (imports/depends_on), eliminating ~85%
    of nodes/edges that previously caused severe zoom/pan lag. Class view
    adds class nodes via contains edges with an optional function toggle.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • feat: customizable heading font via theme settings (#121)
    Add a `headingFont` option to ThemeConfig that lets users switch
    heading typography between Serif (default), Sans, and Mono via the
    existing Theme Picker UI.
    
    - New `--font-heading` CSS custom property (defaults to `--font-serif`)
    - Theme engine applies the selected font on config change
    - ThemePicker gets a "Heading Font" toggle section
    - All 15 component files updated from `font-serif` to `font-heading`
    - Selection persists in localStorage alongside other theme settings
    - Backwards-compatible: existing configs without `headingFont` default to serif
    
    Closes #120
  • fix(pipeline): close 12 sources of silent data loss in graph extraction
    A deep audit of the project-scanner → file-analyzer → merge pipeline
    turned up a wide range of silent data-loss bugs. Each one alone is
    small; together they were producing graphs with very few import edges,
    missing sub-file nodes for non-code formats, and inconsistent metrics.
    
    Root-cause fixes (high impact):
    
    - project-scanner.md: extend import-pattern table to resolve absolute
      imports for Python (`from a.b.c import x`), TS/JS (tsconfig.json
      paths/baseUrl aliases), Java/Kotlin (`com.foo.Bar` ↔ file paths),
      Ruby (`require 'foo/bar'` load-path), PHP (composer PSR-4 namespaces),
      and C/C++ (`#include` headers). Was relative-only, which produced
      empty importMap entries for the majority of real projects.
    - project-scanner.md: add `.ps1`, `.bat`, `.cmd`, `.jsonc` to language
      table; require non-null `language` field with an explicit fallback.
    - file-analyzer.md: document `sections`, `definitions`, `services`,
      `endpoints`, `steps`, `resources` in the extraction-output schema and
      spell out the sub-file node-creation rules per category. Was missing,
      so per-table / endpoint / resource nodes were never created from
      SQL / OpenAPI / Terraform / K8s / Dockerfile parser output.
    - file-analyzer.md: add explicit source-reading fallback rules for
      PowerShell, Batch, Bash, Swift, Kotlin (no tree-sitter coverage).
    - yaml-parser: declare `kubernetes`, `docker-compose`, `github-actions`,
      `openapi` languages so files the language-registry tags with those
      ids actually get section extraction. Recognize quoted top-level keys
      (e.g. `"on":` in GitHub Actions). Emit one section per entry for
      array-root YAML documents.
    - json-parser: declare `json-schema`, `openapi`; add `stripJsoncSyntax`
      helper that removes line / block comments and trailing commas before
      parse so `.jsonc` files (wrangler, tsconfig with comments) parse cleanly.
    - shell-parser: declare `jenkinsfile`. Tighten function-detection regex
      to require a reachable `{` brace so `name() echo hi` and patterns
      appearing inside heredocs are no longer false-positives.
    - markdown-parser: track fenced-code-block state and skip headings
      inside ``` / ~~~ blocks (`# install` shell comments were being
      emitted as level-1 sections).
    - merge-batch-graphs.py: add `article`, `entity`, `topic`, `claim`,
      `source` to VALID_NODE_PREFIXES and TYPE_TO_PREFIX so knowledge-base
      node types stop being flagged unknown / coerced to `file:`. Add
      `direction` to the edge dedup key so `forward` and `bidirectional`
      variants of the same (src, tgt, type) don't overwrite each other.
      Use a placeholder in bare-id fallback when `filePath` is missing on
      function/class nodes so unrelated `parse()` functions don't merge.
    - typescript-extractor: actually compute `isDefault` for default
      exports (was always emitted as `false` from buildResult).
    - extract-structure.mjs: match `wc -l` semantics for `totalLines` so
      the scanner's `sizeLines` and the extractor's `totalLines` agree on
      POSIX text files. Filter the parser-imports fallback to relative-only
      so `importCount` semantics stay *internal-import* whether the scanner
      resolved them or not. Drop unused `isCode` local.
    
    Tests: +19 cases covering JSONC parsing, markdown fenced-code skip,
    YAML quoted-keys / array-root, shell function false-positives,
    extract-structure import fallback semantics + totalLines off-by-one.
    764 passing (was 745).
    
    Bumps version to 2.6.2 across the five tracked manifests.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • Merge pull request #117 from DenisBalan/patch-1
    Fix dashboard URL format in vite.config.ts
  • feat(dashboard): tested badge on node cards (#113)
    Render a small green dot next to the complexity badge whenever a node's
    tags contain "tested" — surfacing the deterministic linker's signal so
    users can see at a glance which files have paired tests.
    
    Plumb node.tags through both CustomNodeData construction sites in
    GraphView.tsx; KnowledgeGraphView.tsx already passes tags.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • feat: customizable heading font via theme settings
    Add a `headingFont` option to ThemeConfig that lets users switch
    heading typography between Serif (default), Sans, and Mono via the
    existing Theme Picker UI.
    
    - New `--font-heading` CSS custom property (defaults to `--font-serif`)
    - Theme engine applies the selected font on config change
    - ThemePicker gets a "Heading Font" toggle section
    - All 15 component files updated from `font-serif` to `font-heading`
    - Selection persists in localStorage alongside other theme settings
    - Backwards-compatible: existing configs without `headingFont` default to serif
    
    Closes #120
  • feat(dashboard): add mobile layout and responsive fixes
    Bumps to 2.6.0.
    
    - MobileLayout activates via useIsMobile at <768px with bottom-tab
      navigation (Graph/Info/Files); panes stay mounted (visibility
      toggle) to preserve ReactFlow dimensions and FileExplorer state.
    - MobileDrawer holds persona, view mode, diff, node-type filters,
      layers, and tool buttons (Filter/Export/Path/Theme/Help).
    - Selecting a node auto-pivots to Info; CodeViewer is always
      fullscreen on mobile; SearchBar collapses to a 🔍 toggle.
    - Homepage Hero/Footer/Install responsive: drop nowrap on title and
      tagline, stack title spans for editorial wrap, full-width CTAs at
      <480px, narrow-width spacing refinements.
    - Desktop dashboard: sidebar telescopes 260/300/360px, header gaps
      tighten, Path button label collapses to icon at narrow widths.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix(dashboard): suppress TourFitView overlay flicker after fallback
    Follow-up on the Codex P2: now that `useNodes` is in the effect deps,
    every node update during a step that already timed out re-enters the
    poll, sets `tourFitPending=true`, runs RAF for 4s, hits the silent
    fallback path, and clears the flag. Visually the "Locating tour
    highlight…" overlay would flash on every reflow even though the user
    has already given up waiting. Skip the pending flag once
    `fallbackKeyRef` matches the current step — the retry still runs
    silently so a late Stage 2 can still upgrade to the proper fit.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix(dashboard): TourFitView timeout no longer freezes refit
    Codex review on PR #114: when the RAF poll window expires before
    highlighted nodes have been measured, the timeout fallback was setting
    `fittedKeyRef.current = targetKey`, marking the step as fitted even
    though the proper highlight fit never ran. If Stage 2 layout landed
    after the 4s cap, the effect early-returned on the next nodes update
    because the target key already matched, so the camera stayed pinned to
    the fallback layer fit instead of zooming onto the actual highlights.
    
    Fix:
    
      - Subscribe to React Flow's user-node array via `useNodes()` so the
        effect re-fires when Stage 2 finally produces the highlighted ids
        after the per-step RAF poll has already given up.
      - On timeout, pan into the layer for usability but do NOT set
        `fittedKeyRef`. The next nodes update gets another shot at the
        highlight fit, and on success `fittedKeyRef` records the proper fit.
      - Use a separate `fallbackKeyRef` to ensure the fallback `fitView`
        fires at most once per step — without this, every subsequent nodes
        update during the unready window would trigger a viewport jump.
      - Reset both refs when `tourHighlightedNodeIds` clears (stop tour).
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix(dashboard): clear pendingFocusContainer on layer/state resets
    Codex review on PR #114 flagged that `layerResetIfChanged` cleared
    `containerLayoutCache` and `expandedContainers` but left
    `pendingFocusContainer` intact. Because container ids collide across
    layers (the very reason the cache reset exists), a manual expand in
    layer A that hadn't yet hit its 1.2s clear timer could leak its id
    into layer B's namespace and recenter the viewport on an unrelated
    container right after navigation.
    
    The same hazard applies to every other reset path that drops the
    container caches. Add `pendingFocusContainer: null` to all of them:
    
      - layerResetIfChanged (tour cross-layer reset, the originally flagged
        site)
      - drillIntoLayer
      - navigateToOverview
      - setFocusNode
      - setPersona
      - setGraph
      - toggleNodeTypeFilter
      - clearContainerLayouts
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix(dashboard): tour navigation glitches across layers
    Four related issues that surfaced while walking the Learn-mode tour
    through a multi-layer project (microservices-demo):
    
    1. Tour auto-expand never released. The tour effect that expands
       highlighted nodes' containers had no corresponding collapse when the
       step changed, so containers accumulated open as the user advanced.
       Track the set of containers we expanded and release any not needed
       by the current step; user-toggled containers are never tracked here,
       so they're never auto-collapsed.
    
    2. Manual container toggle yanked off-screen. Stage 2 reflow shifted
       the just-clicked container away from the cursor. `toggleContainer`
       now records `pendingFocusContainer` on expand; GraphView locks the
       viewport onto that container's centre with the current zoom so it
       appears to expand in place.
    
    3. Tour fitView fired before highlighted children existed. A single
       RAF after `tourHighlightedNodeIds` change wasn't enough — child
       nodes only appear once Stage 2 layout writes
       `containerLayoutCache`, and React Flow only knows their absolute
       position after a measure pass. `useNodes()` doesn't fire on
       measure completion, so we poll `getInternalNode().measured` each
       frame (up to ~4s) and call `fitView({ nodes })` once every
       highlight is measured, with `maxZoom: 1.2 / minZoom: 0.4`. While
       waiting, a new `tourFitPending` flag drives a "Locating tour
       highlight…" overlay so the user knows the layout is still settling.
    
    4. Cross-layer tour transitions reused stale Stage 2 cache. Container
       ids derive from per-layer state (folder names in folder strategy,
       `container:cluster-N` in community strategy) and collide across
       layers — API Contracts and Load Testing both produce
       `container:cluster-0`. `setTourStep` / `nextTourStep` /
       `prevTourStep` / `startTour` didn't reset the container caches the
       way `drillIntoLayer` does, so when tour crossed a layer the new
       layer's expanded containers hit the previous layer's cache, Stage 2
       skipped its rerun, and children never showed. Extracted
       `layerResetIfChanged` and applied it in all four tour actions.
    
    Verified end-to-end against microservices-demo via headless Chrome:
    all 15 tour steps now expand the right container(s), zoom onto the
    referenced files, and collapse the previous step's auto-expansions.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix(dashboard): preserve any-layer-wins membership for filterNodes
    Reviewer @Lum1104 (PR #112) caught a silent semantic regression: the new
    `filterNodes` reads layer membership through `nodeIdToLayerId.get(node.id)`,
    which is first-wins. The pre-#112 path was any-layer-wins —
    `layers.some(layer => filters.layerIds.has(layer.id) && layer.nodeIds.includes(node.id))`.
    For a node X listed in both L1 and L2 with only L2 selected, the old code
    kept X; the new code dropped it. The schema permits multi-layer membership,
    so this was a behavior change, not a bug fix.
    
    Fix: keep two distinct indexes in the store. Both are rebuilt once on
    `setGraph`, so the O(1)-per-node performance win from #112 is preserved.
    
      - `nodeIdToLayerId: Map<string, string>` — first-matching-layer wins.
        Drives navigation (drillIntoLayer / tour step → layer / sidebar
        history) where one canonical layer is the right answer. Unchanged.
    
      - `nodeIdToLayerIds: Map<string, Set<string>>` — every layer the node
        belongs to. Drives `filterNodes` membership checks. Restores
        any-layer-wins exactly.
    
    `filterNodes` now iterates the (small) layer-id set per node looking for
    intersection with `filters.layerIds`. ExportMenu reads
    `nodeIdToLayerIds` from the store.
    
    Verified locally:
    
      - Added `filters.test.ts` regression: node in (L1, L2) with only L2
        selected must pass. Failed against the first-wins implementation;
        passes now.
      - `pnpm --filter @understand-anything/dashboard test` — 42 / 42 pass
        (was 41; +1 multi-layer regression test; perf-guard at 100 layers ×
        100 nodes still <50 ms).
      - `pnpm --filter @understand-anything/dashboard exec tsc --noEmit` — clean.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix(dashboard): O(N+K) per-layer aggregations, kill quadratic Array.includes (#102)
    Three hot paths in the dashboard ran `layer.nodeIds.includes(node.id)`,
    which is O(K) per check. Combined with their enclosing loops they
    collectively spent quadratic time per render of the overview / per
    filter recompute / per node-selection event. On the 4.8 MB knowledge
    graph reported in #102, the overview render alone took ~470 ms of
    synchronous main-thread work before ELK / React Flow ran — long
    enough for the page to register as unresponsive.
    
    Fix: precompute two indexes once when a graph is loaded.
    
      - `nodesById: Map<string, GraphNode>`
      - `nodeIdToLayerId: Map<string, string>`  (first layer wins, matching
        prior `findNodeLayer` semantics)
    
    Both live in `useDashboardStore` and are rebuilt by `setGraph`. The
    three call sites:
    
    1. `useOverviewGraph` (GraphView.tsx) — per-layer complexity aggregation
       moved into a new `computeLayerStats(layer, nodesById)` helper that
       walks `layer.nodeIds` instead of filtering all `graph.nodes`. Search
       match counts now read straight from `nodeIdToLayerId` instead of
       rebuilding a layer index on every searchResults change.
    
    2. `filterNodes` (utils/filters.ts) — takes `nodeIdToLayerId` instead of
       `Layer[]`; the layer-membership check is one Map.get() per node.
       Updated `ExportMenu.tsx` caller to pass the store-level index.
    
    3. `findNodeLayer` (store.ts) — replaced with `nodeIdToLayerId.get()` at
       the four call sites. `navigateTourToLayer` helper updated to take the
       index rather than the whole graph.
    
    Behavior is preserved exactly:
    
      - "First layer wins" semantics for nodes that appear in multiple
        layers (#102 schema doesn't forbid this).
      - 30 % aggregate-complexity threshold pinned by tests.
      - Layer filter that excludes layer-less orphans, but ungated when
        no layers are selected.
    
    Verified locally:
    
      Bench (`scripts/benchmark-aggregations.mjs`, node 22):
        100 layers × 200 nodes (#102 shape):  475 ms → 2 ms  (232× faster)
        50  layers × 200 nodes:               116 ms → 0.6 ms (190× faster)
        30  layers × 100 nodes:                12 ms → 0.2 ms (63×  faster)
    
      Tests: `pnpm --filter @understand-anything/dashboard test`
        24 → 41 pass (+17 new tests across `layerStats.test.ts` and
        `filters.test.ts`, including a #102 perf-regression guard at
        100 layers × 100 nodes < 50 ms).
      `pnpm --filter @understand-anything/core test` — 654 / 654 pass.
      `pnpm --filter @understand-anything/dashboard exec tsc -b` — clean.
      `pnpm --filter @understand-anything/dashboard build` — clean.
    
    Pre-existing on master and not from this branch: `pnpm lint` errors
    out with "eslint: command not found" — `eslint` isn't installed by any
    package and the root `lint` script is bare `eslint .`. Out of scope here.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix(dashboard): address Codex review
    P1: Anchor edges at the collapsed-side container atom when only one
    side of an aggregated edge is expanded. The previous pass always
    emitted file→file endpoints, but a collapsed container's children
    aren't rendered as nodes — React Flow silently dropped those edges.
    Now the collapsed endpoint stays as the container id, and the
    expanded endpoint becomes the real file id; multiple files
    collapsing onto the same (container → file) pair are deduped.
    
    P2: Pending-fit ref + post-render trigger replaces the fixed 50ms
    timer for layer-transition fitView. ELK takes ~125ms+ on medium
    layers, so the timer was firing before positions arrived and the
    viewport stayed on the old layer. Now we mark a pending fit on
    navigation change and execute it once `nodes` populate, then
    requestAnimationFrame so React Flow has placed the nodes.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix(dashboard): render intra-container edges when container is expanded
    Stage 1 stashes intra-folder file→file edges on `topo.intraContainer`
    but `expandedEdges` only walked `topo.edges` (aggregated cross-
    container only). When a user expanded a container the children
    appeared but their internal wiring was invisible, even though the
    NodeInfo sidebar listed the connections.
    
    Now after the inflated cross-container pass, also push every
    intra-container edge whose owning container is expanded.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix(dashboard): clear container cache on focus, deprecate applyDagreLayout
    setFocusNode was missed in the C1 cache-reset pass. Focus mode narrows
    filteredGraphNodes to focus + 1-hop neighbors, so a container that
    survives still has a subset of its children — the cache must drop
    or it'll keep returning positions for filtered-out ids.
    
    Mark applyDagreLayout @deprecated. The structural views all use ELK
    now; the helper is retained for one release as a quick fallback path
    and removed in the next.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • fix(dashboard): address final review C1 + C2
    C1 (cache leak across topology changes):
    Container ids derive from folder names (`container:auth`, etc.) and
    collide across layers, personas, and node-type filters. Without a
    reset, navigating from Layer A to Layer B left Stage 2's cache and
    size memory keyed by Layer A children, so Stage 2 short-circuited
    and Layer B's children silently disappeared.
    
    Reset containerLayoutCache, containerSizeMemory, and
    expandedContainers in:
    - drillIntoLayer (most visible — layer drill)
    - navigateToOverview (drilling out)
    - toggleNodeTypeFilter (filter change shifts container.nodeIds)
    - setPersona (persona filters node types)
    
    setGraph already resets all three on full graph reload — unchanged.
    
    C2 (bundle regression):
    Add elkjs and graphology rules to vite manualChunks. The main index
    chunk drops from 525KB gzipped back to 62KB, with ELK split into a
    parallel-loadable 439KB chunk. Restores baseline first-paint cost.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • chore(dashboard): address Task 14 + 15 review concerns
    Task 14 stale TODO:
    - Delete the TODO(Task 14) comment that this very task already
      resolved (selection→container neighbor highlighting now flows
      through isFocusedViaChild).
    
    Task 15 visible container size:
    - mergeElkPositions now propagates width/height from the ELK output
      back onto the React Flow node, not just position. After a
      stage1Tick-driven re-layout, the container atom resizes to match
      the actual Stage 2 footprint instead of staying clipped at the
      pre-expansion estimate. ELK echoes back the same width/height we
      pass in for non-container nodes, so this is a no-op for them.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • chore(dashboard): address Task 12 + 13 review concerns
    Task 13 onMove auto-expand:
    - Skip when event === null so React Flow's programmatic moves
      (fitView at layer entry, zoomTo from search) don't cascade-expand
      every container the moment the layer paints.
    - Track previous zoom; only fire when zoom actually increased. Pans
      and zoom-outs are now no-ops, so a user who manually collapses a
      container while zoomed in can keep it that way.
    
    Task 12 inflated edge ids:
    - Append index suffix so parallel edges with identical
      (source, target, type) — which the schema doesn't disallow — don't
      collide in React Flow's edge map.
    
    Other Task 12 concerns (per-container Stage 2 cancellation, O(K·E)
    edge bucket pre-pass, extent:"parent" clipping pre-Task-15) are
    known trade-offs documented in the plan; deferring to follow-ups.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • chore(dashboard): address Task 11 review minors
    - Stable handleContainerToggle via useCallback so ContainerNode's
      memo() actually short-circuits. Reading toggleContainer through
      getState() keeps Stage 1 from accidentally subscribing to
      expandedContainers.
    - Cap pre-expansion container size at 800x600 so layers with many
      files don't render as huge empty boxes during Stage 1. Stage 2
      measurement and Task 15's reflow handle the real size.
    - Update stale "dagre relayout" comment now that ELK is the layout
      engine; flag the Task 14 follow-up for selection→container
      neighbor highlighting via inline TODO.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • chore(dashboard): address Task 9 + 10 review concerns
    Cross-cutting fixes for the dagre→ELK migration pattern:
    
    - nodesToElkInput accepts an optional layoutOptionsOverride so views
      can override defaults (e.g. direction) without forking the helper.
    - DomainGraphView restores its original LR layout by passing
      { "elk.direction": "RIGHT" } — the previous commit silently shifted
      it to TB because the helper hardcoded DOWN.
    - Both overview and DomainGraphView now .catch the ELK promise so
      strict-mode (DEV) failures don't surface as unhandled rejections.
    - Both also console.warn returned issues until Task 16 wires the
      WarningBanner funnel; auto-corrected/dropped issues no longer
      silently disappear in production.
    - Minor: DRY the duplicate `as unknown as Node[]` cast in GraphView.
    
    Ranksep label-aware spacing in DomainGraphView is intentionally
    deferred — see Task 14 polish or follow-up.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • chore(dashboard): address Task 6 + 8 review minors
    ContainerNode (Task 8 quality review):
    - Add accessibility: role="button", tabIndex, aria-expanded,
      aria-label with item count + state, focus-ring on focus, Enter/Space
      keyboard activation. Containers are the primary lazy-load
      interaction so keyboard support shouldn't be a regression.
    - Use var(--color-diff-changed) and the font-serif Tailwind class
      instead of literals so theme switching still works.
    - Replace the 🔍 emoji with a typographic "N hits" pill matching the
      badge pattern used by LayerClusterNode.
    - Add a displayName so React DevTools shows "ContainerNode" rather
      than "Memo(Anonymous)".
    
    elk-layout.test.ts (Task 6 quality review):
    - Drop the (c as { x?: number }).x casts now that ElkChild declares
      x?/y? directly. Tests now exercise the typed contract.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>