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
3 changes: 2 additions & 1 deletion docs/design/dfd-store-clusters.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ Two earlier attempts inform this one. The ELK spike in `docs/spec/dfd-overhaul.m


- Goals:
- A prose `label:` on any input or output entry, shown on the edge chip in place of the column list. Clicking any chip that carries data opens a contract dialog listing which columns come from which store.
- A prose `label:` on any input or output entry, shown on the edge chip in place of the column list. Clicking any chip that carries data opens a contract dialog listing which columns come from which store; dragging it slides it along its routed edge without losing the pointer's grab point.
- A per-process view, the default, where each process's read set and write set render as one stack each, two processes with the identical set share a stack, and a store in two stacks is flagged as duplicated in the data without drawing the marker, since repetition is the rule in that view. The connected view, today's rendering with grouping applied, stays as the toggle and keeps drawing the duplicate marker, where a repeat is the exception.
- A collapse level, stores, clusters, or groups, that decides what one row of a stack stands for. A cluster is a subtype family from the entity model or an author-declared set in a `clusters/` file. A group is the entity's existing `group:`.
- A process file can reference a cluster directly with a `cluster:` entry, name the flow, and give each member's columns in one entry.
- Clicking a stack opens a dialog listing its rows; a row opens to its members; a member opens its entity dialog.
- Edge-label chips determine the space between the process band and adjacent store bands: the rendered chip height plus 20px clearance on each side, so multi-line labels never cover a process or store stack.
- In the connected view, adjacency stacking: stores wired to exactly the same processes in the same directions collapse into one honestly labelled stack. Switchable off in `ignatius.yml`.
- Validation, balancing, leveling, usage indexes, fingerprints, search, static export, and the modeling skill all understand labels, clusters, and stacks.
- Non-goals:
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/flows.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,9 @@ A lone table is a table row at every level.

A stack draws visible rows: store rows carry their own D# cap; cluster and subtype rows cap with `C`, group rows with `G` (never a D#, since only a store row is one) alongside the label, a `(N)` count, and a peek marker meaning more sit inside. A subtype row reads the basetype name when the basetype is among the touched stores, or `<Basetype> subtypes` when it is not. An adjacency stack is labelled `N stores`, never a member's name. Every stack carries one ⓘ badge.

Clicking a stack opens the stack dialog. Its title depends on the stack's source (a cluster's `label:`, the basetype name, the group's `label:`, `N stores` for adjacency, or `Read stack` / `Write stack` for a plain per-process stack), followed by the feeding processes and the rows at the current collapse level. Opening a row expands to its members; opening a member opens its entity or doc dialog. An author cluster row also shows the `clusters/<slug>.md` file's body; a subtype row links the basetype; a group row shows the group's description; an adjacency row lists its reader and writer processes.
Clicking a stack opens the stack dialog. Its title depends on the stack's source (a cluster's `label:`, the basetype name, the group's `label:`, `N stores` for adjacency, or `Read stack` / `Write stack` for a plain per-process stack), followed by the feeding processes and the rows at the current collapse level. A row's chevron expands and collapses its member rows; opening a member opens its entity or doc dialog. An author cluster row also shows the `clusters/<slug>.md` file's body; a subtype row links the basetype; a group row shows its description; an adjacency stack lists its shared readers and writers.

Stack edge chips show the members' authored labels one per line, then one column-preview line covering every unlabelled member; if nothing is labelled, one preview line for the whole stack. Drag positions are saved per view, so a drag in the per-process view never applies to the connected view.
Stack edge chips show the members' authored labels one per line, then one column-preview line covering every unlabelled member; if nothing is labelled, one preview line for the whole stack. The viewer reserves each chip's rendered height plus 20px above and below between the process and store bands, so multi-line labels do not cover either box. Drag a chip to slide it along its routed edge; the point you grab stays under the pointer instead of snapping the chip's centre to it. Drag positions are saved per view, so a drag in the per-process view never applies to the connected view.


### Controls
Expand Down
14 changes: 14 additions & 0 deletions docs/spec/dfd-store-clusters.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Every input/output entry may carry a prose `label:`, shown on the edge chip in p
- [ ] Dialogs follow the existing flow-surface open rule: opening `StackDialog` or the contract dialog, then clicking through to an entity, closes the currently-open dialog before the entity dialog opens (the same rule `FlowSurface`'s `open` handler already applies to the doc dialog in `src/app/views/flow/FlowsView.tsx`).
- [ ] The dictionary's process IO table (`IoTable.tsx`) shows an entry's `label:` in place of its column list whenever one is present, matching the canvas chip.
- [ ] A stack edge whose members mix labelled and unlabelled entries shows the authored labels one per line followed by exactly one column-preview line covering every unlabelled member (Merge Tag's per-process write chip on `tag-administration` reads `Tag junctions` / `tag_id, memory_id`); a stack edge with no labelled member shows one preview line; the hover tooltip on any stack edge lists one `Store: col, col` line per member.
- [ ] Label clearance: ELK derives inter-band spacing from the tallest rendered edge chip, reserving the chip's full height plus 20px above and below (with the historical 60px minimum), so a multi-line stack label does not overlap its process or store grouping.
- [ ] Label dragging: once pointer movement crosses the click threshold, the chip slides along its routed edge while preserving the initial pointer-to-chip-centre offset; the first drag frame never snaps the chip centre to the pointer.
- [ ] `bun run test` passes (all `test/checks/*.ts`, exit 0), including new checks for labels, cluster parsing/expansion/validation, per-process and connected-view grouping, and the collapse level. `bun run build:cli` succeeds. Touched files introduce zero new `tsc --noEmit` errors vs. `bun run typecheck` baseline.
- [ ] `docs/guides/flows.md`, `docs/guides/folder-format.md`, `docs/guides/validation.md`, `docs/glossary.md`, `skills/ignatius-modeling/references/{dfd-authoring,flow-templates,discover-flow,verification}.md`, and `docs/wiki/feature-map.md` all describe labels, the two views, the collapse level, clusters, groups, and adjacency — including the one-sentence exception the `cluster:` prefix carves out of the otherwise-closed endpoint-prefix set.

Expand Down Expand Up @@ -353,6 +355,18 @@ src/generators/app.ts

**Superseded:** outline-only sheets; the closing line and trailing clearance under a stack that ends in a grouped row.

### 2026-09-09 — label-aware band spacing and working stack disclosures

**What changed:** ELK's inter-band spacing now grows from the tallest rendered edge chip: chip height plus 20px clearance on each side, with the former 60px spacing retained as the minimum. Stack-dialog C/G rows now mount their member rows only while the disclosure is open, so the chevron controls visible content instead of acting decoratively.

**Why:** Multi-line labels could cover the process box or adjacent store grouping, and the stack dialog rendered member rows outside its `<details>` element regardless of whether the disclosure was open.

### 2026-09-09 — stable label dragging

**What changed:** Edge-chip dragging now applies the pointer's world-space delta to the chip's starting centre before projecting that candidate point onto the routed edge.

**Why:** Projecting the pointer itself discarded the offset between the pointer and chip centre, so the label jumped as soon as a drag began unless it happened to be grabbed exactly at its centre.


## Implementation log

Expand Down
2 changes: 1 addition & 1 deletion docs/wiki/feature-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Paths are relative to [`docs/design/`](../design), [`docs/spec/`](../spec), [`do
| DD spotlight inherited 1:1 key-inheritance connections (#9, CP7; SUPERSEDED by `key-inheritance-lineage` CP-A — kept for history; bounded to subtype clusters, single-level; pure [`src/app/logic/spotlight-inherited.ts`](../../src/app/logic/spotlight-inherited.ts) `buildInheritedConnections(index, entityId)` → `InheritedConnection[]` (`{otherId, direction, via}`, `INHERITED_IDENTITY='identity'`); member → basetype + sibling identity links + basetype's direct rels (via=basetype); basetype → members + each member's direct rels (via=member); transitive rels de-dup against the active's own direct edges, identity links exempt; general identifying-1:1 dependent tables were a noted non-goal here — now generalized below; `SpotlightOverlay` draws a THIRD line category DOTTED in `--spotlight-line-inherited` green via the CP6 `separateSpotlightLines` path with "via &lt;basetype&gt;"/"shared key" pills + off-screen `spotlight-chip--inherited` chips; `DictionaryView` folds inherited ids into `spotlitIds`+`focusSet`; `buildSpotlightConnections` unchanged) | viewer-ux-polish | viewer-ux-polish | — | — |
| Key-inheritance lineage (GENERALIZES #9/CP7 above; corrected to the key-edge connected-component model — [`src/app/logic/spotlight-inherited.ts`](../../src/app/logic/spotlight-inherited.ts) `buildInheritedConnections` keeps its export name + `InheritedConnection {otherId,direction,via}` shape + `INHERITED_IDENTITY='identity'` so `DictionaryView`/`SpotlightOverlay`/`GraphView` are unchanged; **lineage follows ONLY key edges** — an edge whose child-side FK cols (`Object.keys(edge.on)`) are ALL ⊆ the child PK (`pkByNode.get(edge.source)`), a SUBSET test (FK ⊆ PK, non-empty), NEVER a secondary/non-key FK; this one predicate catches identifying-1:many (FK a PROPER subset of the PK, e.g. `SalesInvoice→Party` on `party_no`) AND subtype member→basetype (FK==full PK) — empirically `edge.identifying`==FK⊆PK on `key-inherited`; **lineage** = transitive connected component over key edges in BOTH directions (undirected), cycle-safe visited map; inherited = lineage − self − direct real-edge neighbours (those render solid); `direction='out'` (DD draws ONE source-out line — single arrowhead at the far/member end, pointing FROM the active card OUT to the member; was `'both'`), `via`=nearest key-edge predecessor on the path (or `INHERITED_IDENTITY`); bundle one per otherId, sort by otherId, singleton lineage → []; **DD inherited lines are SHIFT-GATED (mirrors DG):** in the browse lens the dotted inherited lines appear ONLY while Shift is held over an active (hover/pin) card — `DictionaryView` carries a `shiftHeld` state driven by a document `keydown`/`keyup` pair on `Shift` + a `window` blur reset; the `inheritedConnections` useMemo returns `[]` unless `shiftHeld && activeId`, and the inherited-id foldings into `spotlitIds`/`focusSet` are gated on `shiftHeld` too (no lit/extra-focused inherited cards or off-screen inherited chips without Shift); FK (solid) + flow (dashed) lines are UNCHANGED (plain hover/pin); `SpotlightOverlay` unchanged (renders `inheritedConnections=[]` as zero lines/chips); no longer walks subtype-cluster maps (member→basetype IS a key edge) and no longer calls `buildSpotlightConnections` for de-dup; SUPERSEDES the old subtype-cluster + dependent-identifying-1:1 (FK==full PK + 1:1) + per-member secondary-FK expansion, which over-connected via secondary FKs (`SI_Line→Product`/`→LineItemType`) and missed identifying-1:many lineage; proven on [`models/key-inherited`](../../models/key-inherited): `SSN` reaches the party-keyed family (`SalesInvoice`/`SI_Line`/`SalesOrder`/`SO_Line`/`PaymentAllocation`…) and EXCLUDES `Product`/`Subscription`/`LineItemType`/`PartyType`, `SI_Line` no longer over-connects, ORM surrogate-PK models have zero lineage. **CP-B (DG dotted lines), SHIFT+HOVER trigger:** lineage is revealed by SHIFT+HOVER, NOT click/select. While Shift is held and the pointer is over a node, `GraphView` `enterLineageHover` calls `drawInheritedEdges` to add EPHEMERAL dotted cytoscape edges (class `inherited`, id `_inherited_<sel>__<other>`) from the hovered node to each inherited `otherId` present in cy, then `applyFocusTiers`; styled in `styles.ts` (`edge.inherited`: dotted, arrowless, thin) using the new `SPOTLIGHT_LINE_INHERITED[mode]` constant exported from `theme-css-vars.ts` (single source of truth shared with the DD CSS var so DG==DD); endpoints folded into the focus-fade `keep` set so they stay lit. Trigger wiring: `mouseover` branches on `evt.originalEvent?.shiftKey` (shift → lineage, no-shift → plain direct-neighbour fade); `mouseout` exits (`exitLineageHover`); a document-level `Shift` keydown/keyup pair toggles lineage on the live hovered node (`hoveredNodeIdRef`) so holding/releasing Shift while already hovering works; all state in refs (stale-closure-safe), listeners removed in cy-init cleanup. A plain click now SELECTS + opens the modal only (no lineage); select/navigate/panel/hash-restore paths no longer draw lineage. `clearInheritedEdges` (`cy.remove('edge.inherited')`) on mouseout/shift-release/deselect/reselect/reset/applyLayoutMode-before-ELK/no-entity-restore/teardown; never enters model/`layoutFingerprint`/`layout-store`/static-export/ELK — added after layout, removed before re-layout; no-leak unit check (`test-inherited-edges-no-leak.ts`, trigger-agnostic) + Playwright DG check (`test-graph-inherited-edges.ts`: plain click → 0 inherited; shift+hover Identity → dotted rays; mouseout → 0; shift+hover ITIN strictly larger transitive set; plain hover → 0; deselect → 0) + visual screenshot ([`test/visual/test-graph-inherited-lines.ts`](../../test/visual/test-graph-inherited-lines.ts): SSN + SI_Line owner cases via shift+hover). **3-tier focus opacity:** `GraphView` `applyFocusTiers(focusNode)` (run on shift+hover lineage + plain hover; cleared on mouseout/shift-release/deselect/reselect/relayout/teardown) splits the focused state into DIRECT (focused node + its REAL graph neighbors via `connectedEdges().not('.inherited')` + identifying lineage/descendants + joiners → opacity **1.0**), INHERITED/ancestral (the `edge.inherited` rays + their target nodes minus direct, `inherited-dim` → **0.5**), UNRELATED (everything else, `faded` → **0.2**); `styles.ts` `.faded` 0.3→0.2, new `.inherited-dim` 0.5, `edge.inherited` opacity 0.85→0.5; direct wins de-dup (`.difference(direct)` + `buildInheritedConnections` already de-dups); visual harness reads per-tier opacity off the live cy and asserts `direct>inherited>unrelated`) | key-inheritance-lineage | key-inheritance-lineage | — | — |
| Glossary of app terms (DG/DD/DFD/DE/DS/EE; DS⊃DE) | — | — | `../glossary.md` | — |
| Store clusters (flow labels on every entry + contract dialog; per-process view default with connected view as toggle; collapse level stores/clusters/groups; `clusters/<slug>.md` author clusters + `cluster:` token, the one exception to the closed endpoint-prefix set; five `flow.cluster_*` validation rules; adjacency stacking, `flow_view: { adjacency_stacks: false }`) | dfd-store-clusters | dfd-store-clusters | flows (Labels, stacks, clusters, and groups), folder-format (`clusters/`), validation (cluster rules) | SKILL core rule (labels and clusters always), dfd-authoring (How the diagram reads, F4a cluster step + stack row count, F5 label rules), flow-templates (`cluster:` entry in the process template, labelled worked example + `clusters/settlement.md`, cluster template), templates (`flow_view:` in `ignatius.yml`), discover-flow (Gate 4 names the data and the set), reverse-engineering (R2 labels + transaction-written clusters), verification (cluster rule table + flow self-check 5-7: labels, cluster hygiene, legibility) |
| Store clusters (flow labels on every entry + label-height-aware inter-band clearance + stable grab-offset-preserving chip drag; contract dialog; per-process view default with connected view as toggle; collapse level stores/clusters/groups; working C/G row disclosures; `clusters/<slug>.md` author clusters + `cluster:` token, the one exception to the closed endpoint-prefix set; five `flow.cluster_*` validation rules; adjacency stacking, `flow_view: { adjacency_stacks: false }`) | dfd-store-clusters | dfd-store-clusters | flows (Labels, stacks, clusters, and groups), folder-format (`clusters/`), validation (cluster rules) | SKILL core rule (labels and clusters always), dfd-authoring (How the diagram reads, F4a cluster step + stack row count, F5 label rules), flow-templates (`cluster:` entry in the process template, labelled worked example + `clusters/settlement.md`, cluster checklist), discover-flow (Gate 5), verification (cluster rule table + stack legibility check) |
| Model index routing (generated per-folder `index.md` routers with name/kind/description/link tables; rolled-up SHA digests; `<ignatius-index>` and `<ignatius-breadcrumb>` managed regions beside hand-authored `<ignatius-rules>`; `index_file:` and `harness:` keys in `ignatius.yml`; `ignatius index [--agents]` writes routers and in-folder `AGENTS.md` / [`CLAUDE.md`](../../CLAUDE.md) shim / `SKILL.md` guidance; `ignatius validate --index` reports drift as `index.stale`; `description:` on all five file kinds is the router payload; [`models/llm-memory-db-mssql`](../../models/llm-memory-db-mssql) is the fully realized exemplar) | model-index-routing | model-index-routing | folder-format | SKILL core rule (description-always, reserved `index_file`), entity-flow E1b, dfd-authoring F6a, conventions (reserved filename), verification (`validate --index` in the loop + `config.index_file_*` / `index.*` rule rows) |

◆ **Process flows — implemented and first-class, now an in-app view.** The `ignatius flow` CLI subcommand has been removed; flows are the **Flows** view inside the unified SPA (`serve`) and are included in the single `export -o model.html` file. The process dictionary is fused into the unified **Dictionary** view (no separate `/flow-dict` route). The **flow viewer is a custom SVG renderer** ([`src/flow-view/`](../../src/flow-view), separate from the ERD's Cytoscape): Gane-Sarson notation (open-ended `D#` stores, numbered process hubs, green externals), banded DFD layout (hub-and-spoke to stores/externals, no process-to-process, shared store bridging). Every node carries a ⓘ badge — a **`db:` store** opens the rich `SelectedEntityModal` (attributes, relationships, examples) shared with graph nodes; a process / external / non-`db` store opens the plain markdown doc dialog. Flow bodies parse `[[wiki-links]]` that route in-dialog across both flow nodes and ERD entities. [`models/key-inherited`](../../models/key-inherited) carries demo DFDs (`order-to-cash` with a sub-DFD + `refund`). Skill coverage: the `ignatius-modeling` skill's `flow` mode authors flow markdown (see the skill-modes row above). Guide coverage: [`docs/guides/flows.md`](../guides/flows.md) (folder format, process frontmatter, endpoints, sub-DFDs, viewing) plus the flow rule catalog in [`docs/guides/validation.md`](../guides/validation.md). See [`docs/spec/process-flows.md`](../spec/process-flows.md) Non-goals + the design's Open questions for the deferred set (queue-payload validation, usage index).
Expand Down
Loading
Loading