Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/wiki/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Ignatius — Bun/TypeScript markdown-driven ERD modeler with a unif
---

<wiki-type>repo</wiki-type>
<scan-sha>08b8eeaf632f7cc3e9d655f1b1b1b0369d94bb8c</scan-sha>
<scan-sha>f4d2d14b4db849f2c97d39176536d5f5c80d100d</scan-sha>
<wiki-schema>1</wiki-schema>

# Project signals
Expand Down Expand Up @@ -48,8 +48,8 @@ No linter or formatter configured in package.json.

| Language | LOC | Files | % |
|----------|-----|-------|---|
| TypeScript | 59159 | 265 | 70% |
| Markdown | 20461 | 298 | 24% |
| TypeScript | 59201 | 265 | 70% |
| Markdown | 20483 | 298 | 24% |
| CSS | 3004 | 2 | 3% |
| YAML | 1340 | 14 | 1% |
| Shell | 116 | 1 | 0% |
Expand All @@ -63,7 +63,7 @@ No linter or formatter configured in package.json.
- CI pipeline: install deps → cache Playwright → build bundle + stable-names → compile binary → run all `test/checks/*.ts` → typecheck (`continue-on-error: true`).
- Release pipeline: [`.github/workflows/release-please.yml`](../../.github/workflows/release-please.yml) (release-please driven; a `build` job gated on `release_created` compiles the 5 platform binaries + checksums and attaches them to the release in the same push-to-main run). [`install.sh`](../../install.sh) (repo root) is the curl-able CLI installer that pulls those binaries from `releases/latest/download`.
- Binary is built locally or in CI via `bun run build:cli`; produces `dist/ignatius`.
- package.json `name` is `ignatius`, version is `0.12.0`. The repo *directory* is still named `derek-db-generator/` — the one remaining derek reference, a known leftover.
- package.json `name` is `ignatius`, version is `0.14.0`. The repo *directory* is still named `derek-db-generator/` — the one remaining derek reference, a known leftover.

---

Expand Down Expand Up @@ -248,7 +248,7 @@ Exports: `validateModel(model: Model): ValidationResult`, `formatFindingsForStde
**Cross-view search (graph-flow-search):** owns per-surface search state that survives view switches because GraphView and FlowsView stay mounted-but-inactive and DictionaryView is keep-mounted via CSS. Graph search: `graphSearchTerm`/`graphSearchIncludeBody` (`useState`) plus `graphSearchCursorRef` (Enter-to-cycle cursor, reset to `-1` whenever the term or body toggle changes). `graphSearchActive = graphSearchTerm.trim() !== ''`; `graphSearchMatchIds` (`useMemo`) runs `entityMatches` from [`src/app/logic/search.ts`](../../src/app/logic/search.ts) over `model.nodes`, sorted ascending by id; `graphSearchMatches` is `null` when inactive or a `Set` (possibly empty — an active search with zero matches still dims everything) when active, matching the contract `GraphView`'s `searchMatches` prop expects. `handleGraphSearchEnter` advances `graphSearchCursorRef` modulo the match count, wrapping, and calls `graphViewRef.current?.navigateToEntity(nextId)`. Flow search: `flowSearchTerm`/`flowSearchIncludeBody`; `flowSearchActive`; `flowSearchResults` (`useMemo`) calls `searchFlowDiagrams` from the same module; `flowSearchTokens` is the `Set` of each result's `token` (or `null`) threaded into `FlowsView`'s `searchTokens` prop. `handleFlowSearchSelect(diagramId)` and `handleFlowSearchEnter()` (opens the first result) both route through `flowsViewRef.current?.selectDiagramById` — no new navigation machinery. Neither graph nor flow search state ever touches the model, layout fingerprint, layout-store, or URL hash. `graphSearchBarRef`/`flowSearchBarRef` (`SearchBarHandle`) let `handleKeyboardSearchFocus` (wired to `useKeyboardShortcuts`'s `onSearch`) focus the active view's `SearchBar` input; the Dictionary path instead calls `dictViewRef.current?.focusSearch()` since its search input lives inside `DictionaryView`, not a `SearchBar`. The Graph `SearchBar` gets a live `matchCount`/`totalCount`; the Flows `SearchBar` always passes `matchCount={null}` / `totalCount={0}` (no "n of N" readout — the results dropdown is the readout). `bannerRef` measures the global-error banner's real rendered height via `ResizeObserver` (in a `useLayoutEffect`) and writes it into the `--search-bar-top` CSS custom property so `SearchBar` (`.viewer-search-bar`) sits below the banner instead of being occluded by it; the property is removed when the banner isn't visible, letting `styles.css`'s 12px fallback apply.

- **Hooks:**
- [`src/app/hooks/useModelData.ts`](../../src/app/hooks/useModelData.ts) (158L) — exports `useModelData(opts?)`. Unified SSE subscription + model/flow fetch + findings state. Static mode: reads `window.__MODEL__` / `__FLOW_MODEL__` once on mount. Live mode: boots with parallel `/api/model` + `/api/flow`, then re-fetches on every `model-changed` SSE event. Returns `{ model, findings: ModelFindings, flowDiagrams, flowFindings: FlowFindings, layoutKeyRef, bannerDismissed, setBannerDismissed }`. Exports `ModelFindings` and `FlowFindings` types.
- [`src/app/hooks/useModelData.ts`](../../src/app/hooks/useModelData.ts) (173L) — exports `useModelData(opts?)`. Unified SSE subscription + model/flow fetch + findings state. Static mode: reads `window.__MODEL__` / `__FLOW_MODEL__` once on mount. Live mode: boots with parallel `/api/model` + `/api/flow`, then re-fetches on every `model-changed` SSE event. Returns `{ model, findings: ModelFindings, flowDiagrams, flowFindings: FlowFindings, layoutKeyRef, bannerDismissed, setBannerDismissed }`. Exports `ModelFindings` and `FlowFindings` types. **StrictMode double-fetch guard:** the boot `useEffect` declares a local `let ignore = false`, set to `true` in the returned cleanup; every `setState` call inside the boot `Promise.all(...).then(...)` and the SSE `model-changed` handler's `Promise.all(...).then(...)` (and both `.catch()` handlers) is gated on `if (ignore) return` / `if (!ignore)`. Guards against React StrictMode's dev-mode mount→cleanup→mount double-invoke: without the flag, the first (stale) invocation's in-flight fetch still resolves after its own effect cleanup ran and calls `setModel`/`setFlowDiagrams` with a fresh reference, retriggering every effect keyed on `model`/`flowDiagrams` (including the flow renderer's mount effect in `FlowsView.tsx`) while the second (real) mount is still settling.
- [`src/app/hooks/useHashRoute.ts`](../../src/app/hooks/useHashRoute.ts) — exports `useHashRoute(opts?)`. Owns hash read/write and `popstate` back/forward restoration. Seeds from `location.hash` on mount. Accepts an `onRestoreDfd` callback for DFD deep-link restore. **Entity modal history (#6/#8):** `entity=` in the hash is the single source of truth for the modal stack. Opening an entity calls `openEntity(id)` (→ `history.pushState`; deduped when the hash already carries this same entity — an FK hop back to the same entity or a re-open does not stack a duplicate history entry); closing calls `closeEntity()` (→ `history.replaceState`). `popstate` invokes the `onEntityChange(id | null)` option to reconcile the shell — the shell opens/closes the modal in response without pushing another history entry. GraphView no longer writes `entity=`. Returns `{ view, setView, openEntity, closeEntity }`.
- [`src/app/hooks/useThemeMode.ts`](../../src/app/hooks/useThemeMode.ts) (36L) — exports `useThemeMode(themeConfig?, model?)`. Seeds from `window.__THEME_MODE__` or localStorage. Calls `applyThemeCssVars` on change (also re-fires on `model` identity change, so an SSE-delivered model with the same `themeConfig` identity still reapplies CSS vars). Returns `{ themeMode, toggleTheme }`.
- [`src/app/hooks/useKeyboardShortcuts.ts`](../../src/app/hooks/useKeyboardShortcuts.ts) (93L) — registers exactly ONE global `keydown` listener for the unified SPA keyboard shortcuts (g/d/f/l/b/?/[`/`](../..)/Cmd-Ctrl-k). Stale-closure hazard avoided via `configRef` (latest `view`/callbacks stored in a ref, updated each render; listener reads from ref). Editable guard: returns true when focus is in `INPUT`/`TEXTAREA`/`SELECT`/`contenteditable` or inside `.modal`. `KeyboardShortcutsConfig` interface carries: `view`, `onView`, `onToggleLayout`, `onToggleLens`, `onZoomIn`, `onZoomOut`, `onZoomReset`, `onHelp`, `onSearch` (**graph-flow-search**: fires on [`/`](../..) or Cmd/Ctrl+`k` — focuses the active view's search input; both resolve to the same `{type:'search'}` action in `resolveShortcut`, so the hook itself is agnostic to which key triggered it). Dispatches through `resolveShortcut` from [`src/app/logic/shortcuts.ts`](../../src/app/logic/shortcuts.ts). Imported by [`src/app/App.tsx`](../../src/app/App.tsx).
Expand Down Expand Up @@ -314,7 +314,7 @@ Exports: `validateModel(model: Model): ValidationResult`, `formatFindingsForStde
- [`src/app/views/graph/navigator.ts`](../../src/app/views/graph/navigator.ts) — `mountNavigator` / `teardownNavigator` / `NavigatorInstance` — cytoscape-navigator lifecycle helpers. `teardownNavigator` calls `nav._removeCyListeners?.()` BEFORE `nav.destroy()`: `destroy()` removes overlay/window listeners but leaks the cy `'resize'` + onRender listeners, which fire `Navigator.resize→boundingBox` on a destroyed core (`isHeadless` of null); removing them while cy is alive fixes that crash. `test-navigator-teardown.ts` pins this call-order contract.
- [`src/app/views/graph/styles.ts`](../../src/app/views/graph/styles.ts) (243L) — `buildStyles(groups, theme, mode)` → cytoscape stylesheet array. **3-tier focus opacity:** `.faded` selector sets `opacity: 0.2` (unrelated tier, changed from 0.3); `.inherited-dim` selector sets `opacity: 0.5` (inherited/ancestral tier); `edge.inherited` opacity changed from 0.85 to 0.5 so dotted rays match their `.inherited-dim` target nodes. Includes `edge.inherited` selector: dotted line, `SPOTLIGHT_LINE_INHERITED[themeMode]` color from [`src/app/dom/theme-css-vars.ts`](../../src/app/dom/theme-css-vars.ts), thinner than direct edges. Length-graded de-emphasis (data set by `organic-layout.ts`'s `gradeEdgeSpans` after layout/drag): `edge[span = "mid"]` → `opacity: 0.5`; `edge[span = "far"]` → `opacity: 0.22, width: 1, z-index: -1` — placed after the identifying/dashed rules so the span grading wins the cascade; long-haul cross-domain wires de-emphasize at overview but stay hoverable/selectable and regain full presence as you zoom in or follow them. **Graph search (graph-flow-search CP2):** `SEARCH_MATCH_BORDER` (`Record<ThemeMode, string>` — dark `#fde047` / light `#ca8a04`), a distinct gold/yellow chosen so it never reads as the amber `spotlight-line-out` lineage color or a group's border color, mirroring the Dictionary view's `--dd-search-highlight` yellow so "search" reads as one visual language across surfaces. `.search-dim` sets `opacity: 0.2`; `node.search-match` sets `border-width: 3` / `border-color: SEARCH_MATCH_BORDER[mode]`. Both rules are pushed LAST in the stylesheet array so they win the cascade over the span-graded edge opacity and the per-group border-color rules.
- [`src/app/views/dict/DictionaryView.tsx`](../../src/app/views/dict/DictionaryView.tsx) (1452L) — `DictionaryView` is a `forwardRef` exposing `DictionaryViewHandle { toggleLens(): void; focusSearch(): void }` (`toggleLens` wraps the existing `switchLens`; `focusSearch` — added for **graph-flow-search** — focuses `searchInputRef`, the dict search `<input>`, in response to the shell's [`/`](../..) shortcut). Keep-mounted via CSS `display:none`. Imports `SYNTHETIC_DIAGRAM_IDS` from [`src/flows/flow-derive-levels.ts`](../../src/flows/flow-derive-levels.ts) to exclude synthesized context/L1 diagrams from the DD sidebar. Owns DD CSS Custom Highlight search (CP9), `beforeprint`/`afterprint` (CP10), DD sidebar process nesting with `compareDottedProcesses` (CP24). Its own search (`nodeMatchesSearch`/`processMatchesSearch`/`externalMatchesSearch`/`storeMatchesSearch` from [`src/app/logic/search.ts`](../../src/app/logic/search.ts), always matching columns + body) is unchanged by graph-flow-search — the new title-first, body-opt-in matchers (`entityMatches`, `flowProcessMatches`, etc.) are exclusive to the Graph and Flows `SearchBar` surfaces. `kbd-hint` B badge present on the lens toggle control. **Browse lens (dd-spotlight-grid):** lens toggle (`'read' | 'browse'`) persisted to `localStorage`. Browse lens renders entity groups + Processes / External Entities / Data Stores sections. Spotlight state: `hoverId`, `pinnedId`, `labelHoverCardId`, `focusId` (focus mode). FK connections from `buildSpotlightConnections`; flow connections from `buildFlowSpotlightConnections`. `SpotlightOverlay` mounted when `activeId !== null`. Fixed frosted search bar (`.dict-search-bar`); search and focus are mutually exclusive. **DD inherited lines are SHIFT-GATED:** a `shiftHeld` boolean state is driven by a document-level `keydown`/`keyup` pair on `Shift` + a `window` `blur` reset (registered in a dependency-free `useEffect`); the `inheritedConnections` useMemo returns `[]` unless `shiftHeld && activeId` (deps include `shiftHeld`), and the inherited-id foldings into `spotlitIds`/`focusSet` are wrapped with `if (shiftHeld)` — so without Shift no inherited card is lit, extra-focused, or surfaced as an off-screen chip. FK (solid) and flow (dashed) spotlight lines are unchanged (show on plain hover/pin). `SpotlightOverlay` is unchanged.
- [`src/app/views/flow/FlowsView.tsx`](../../src/app/views/flow/FlowsView.tsx) (864L) — exports `FlowsView` (forwardRef) and `FlowsViewHandle` type. Owns `FlowChrome` chrome + imperative SVG renderer lifecycle. Calls `buildFlowDocResolver` + `buildAllFlowNodeIds` + `buildFlowNodeUsageIndex`. **dfd-overhaul wiring:** `renderDiagram` calls `computeElkLayout(diagram, { workerFactory })` and passes `elkPositions` + `elkEdgeRoutes` into `FlowDiagramSvg`; `terminateQuietly` is called after ELK resolves; banded `computeFlowLayout` used only on ELK failure. `FlowsViewHandle` exposes: `selectDiagramById`, `resetLayout`, `zoomIn`, `zoomOut`, `setPercent`, `resetZoom`, `openFlowToken`. DFD URL deep-link: `selectDiagramById` uses `findDiagramPath` to build the full breadcrumb stack (survives page refresh). SVG wheel delta tamed to `0.95`/`1.05` per tick (CP23). **Cross-view search (graph-flow-search CP3):** accepts a `searchTokens: ReadonlySet<string> | null` prop (base tokens — `proc:`/`ext:`/`<kind>:<name>`/entity `db:` — that keep full opacity; `null` = no active search). Passed as `initialSearchTokens` into `initFlowGraphCore`, which seeds `currentSearchTokens` in its closure and registers a live-update hook via `chromeCallbacks.onRegisterSearchTokens` — the same pattern as the existing `onRegisterRetheme` hook: calling the registered function re-renders `FlowSurface`/`FlowDiagramSvg` in place (`root.render(...)`) with the new token set, no renderer teardown, so the drill-down stack and breadcrumb position survive every keystroke. `FlowsView` stores the registered function in `flowSearchTokensRef`; a dedicated `useEffect` keyed on `[searchTokens, isActive]` calls it whenever `searchTokens` changes while the flow view is active, mirroring the theme-mode `retheme` effect. `FlowDiagramSvg` ([`src/flow-view/FlowDiagramSvg.tsx`](../../src/flow-view/FlowDiagramSvg.tsx)) consumes `searchTokens` in its `nodeOpacity`/`edgeOpacity` helpers: a node dims to `DIM_OPACITY = 0.3` unless `searchTokens.has(baseToken(id))`; an edge dims unless either endpoint's base token matches. `baseToken(id)` strips the render-time `--src`/`--snk`/`--read`/`--write` role-split suffix before comparing, so a role-split node still matches on its underlying token.
- [`src/app/views/flow/FlowsView.tsx`](../../src/app/views/flow/FlowsView.tsx) (891L) — exports `FlowsView` (forwardRef) and `FlowsViewHandle` type. Owns `FlowChrome` chrome + imperative SVG renderer lifecycle. Calls `buildFlowDocResolver` + `buildAllFlowNodeIds` + `buildFlowNodeUsageIndex`. **dfd-overhaul wiring:** `renderDiagram` calls `computeElkLayout(diagram, { workerFactory })` and passes `elkPositions` + `elkEdgeRoutes` into `FlowDiagramSvg`; `terminateQuietly` is called after ELK resolves; banded `computeFlowLayout` used only on ELK failure. `FlowsViewHandle` exposes: `selectDiagramById`, `resetLayout`, `zoomIn`, `zoomOut`, `setPercent`, `resetZoom`, `openFlowToken`. DFD URL deep-link: `selectDiagramById` uses `findDiagramPath` to build the full breadcrumb stack (survives page refresh). SVG wheel delta tamed to `0.95`/`1.05` per tick (CP23). **Cross-view search (graph-flow-search CP3):** accepts a `searchTokens: ReadonlySet<string> | null` prop (base tokens — `proc:`/`ext:`/`<kind>:<name>`/entity `db:` — that keep full opacity; `null` = no active search). Passed as `initialSearchTokens` into `initFlowGraphCore`, which seeds `currentSearchTokens` in its closure and registers a live-update hook via `chromeCallbacks.onRegisterSearchTokens` — the same pattern as the existing `onRegisterRetheme` hook: calling the registered function re-renders `FlowSurface`/`FlowDiagramSvg` in place (`root.render(...)`) with the new token set, no renderer teardown, so the drill-down stack and breadcrumb position survive every keystroke. `FlowsView` stores the registered function in `flowSearchTokensRef`; a dedicated `useEffect` keyed on `[searchTokens, isActive]` calls it whenever `searchTokens` changes while the flow view is active, mirroring the theme-mode `retheme` effect. `FlowDiagramSvg` ([`src/flow-view/FlowDiagramSvg.tsx`](../../src/flow-view/FlowDiagramSvg.tsx)) consumes `searchTokens` in its `nodeOpacity`/`edgeOpacity` helpers: a node dims to `DIM_OPACITY = 0.3` unless `searchTokens.has(baseToken(id))`; an edge dims unless either endpoint's base token matches. `baseToken(id)` strips the render-time `--src`/`--snk`/`--read`/`--write` role-split suffix before comparing, so a role-split node still matches on its underlying token. **`disposed` teardown guard (race fix):** `initFlowGraphCore` declares `let disposed = false` alongside its existing `renderGen` counter. The async `renderDiagram(diagram)` checks `if (disposed) return` on entry and `if (renderGen !== myGen || disposed) return` after every `await computeElkLayout(diagram)` resolution (both the success path and the `catch` block). The teardown function `initFlowGraphCore` returns sets `disposed = true` as its first statement, before unregistering chrome callbacks or unmounting `svgRoot`. Guards against an orphaned `renderDiagram` continuation resuming after its owning `initFlowGraphCore` instance was torn down while still awaiting ELK (React StrictMode's dev-mode mount→cleanup→mount, or navigating away and back before ELK finishes) — `svgRoot` was still `null` at teardown time so there was nothing to unmount, and without this flag the orphaned call would resume, find the shared `container` DOM node still connected (`App.tsx` keeps every view's container mounted across view switches), and inject a second live React root into it, racing the real instance for the same container.
- [`src/app/views/flow/LegendModal.tsx`](../../src/app/views/flow/LegendModal.tsx) (204L) — `LegendModal` component.

- **Globals:** [`src/app/globals.d.ts`](../../src/app/globals.d.ts) (46L) — `window.__MODEL__`, `window.__THEME_MODE__`, `window.__IGNATIUS_MODE__` ('live'|'static'), `window.__LAYOUT_KEY__`, `window.__FLOW_MODEL__`, `window.__FLOW_LAYOUT_KEYS__`, `window.__IGNATIUS_CY__`, `window.__IGNATIUS_CY_GEN__`, `window.__IGNATIUS_FLOW_READY__`, `window.__IGNATIUS_FLOW_GEN__`, `window.__IGNATIUS_ACTIVE_FLOW_DFD__`, `window.__IGNATIUS_PERF__`.
Expand Down
Loading
Loading