diff --git a/docs/design/dfd-store-clusters.md b/docs/design/dfd-store-clusters.md index 476c304..e343c02 100644 --- a/docs/design/dfd-store-clusters.md +++ b/docs/design/dfd-store-clusters.md @@ -185,7 +185,7 @@ Two views produce different stack sets from the same expanded graph. Both are co ``` per-process view (default): for each process P, for each direction: - members = db stores P touches in that direction + members = stores P touches in that direction if |members| ≥ 2 → one stack node, id stack:-- else → the plain store node, as today two processes with the identical member set produce the same id and share the stack @@ -219,6 +219,14 @@ collapse level (stores | clusters | groups), applied to the rows of every stack: The two-member threshold governs implicit grouping only; a `cluster:` entry gets its cluster row even for one member, showing the member count in parentheses, because the author asked for it. A plain `db:` entry that is the only member of a cluster or group present gets a plain row, because it asked for that. +Store identity is row-local, not stack-local. Each concrete store row keeps its store kind, +uses that kind's theme color, and carries a kind-specific numbered cap (`D#` database, +`C#` cache, `Q#` queue, `F#` file, `Do#` document, `M#` manual, `O#` other). Numbering +is independent within each kind. A mixed stack therefore remains one routing/grouping +unit without allowing its first non-database member to recolor every sibling. Collapsed +cluster, subtype, and entity-group rows remain database-colored and use their unnumbered +`C`/`G` aggregate caps. + A stack edge aggregates its members' parsed edges. Its chip shows the members' authored labels one per line, then one column-preview line covering every unlabelled member, so a labelled cluster riding with two unlabelled tables reads `Tag junctions` over `tag_id, memory_id`; with no labelled member the chip is that single preview line. The hover tooltip lists one line per member with its columns, and the contract dialog lists every member's columns. Stack ids are member sets plus direction, so saved drag positions survive reloads and two processes with the same set converge on one node. Positions are stored per view, keyed by the diagram's layout fingerprint plus the view name, so toggling views never applies one view's drag to the other. Fingerprints are computed on the expanded parsed edges, so a diagram authored with `cluster:` tokens and the same diagram authored with `db:` entries share a fingerprint. diff --git a/docs/guides/flows.md b/docs/guides/flows.md index 933ab57..76a8f09 100644 --- a/docs/guides/flows.md +++ b/docs/guides/flows.md @@ -1,7 +1,7 @@ # Process flows -A model can carry data flow diagrams (DFDs) alongside its entities. A DFD shows what the business *does*: numbered processes transform data, external entities send it in and receive it out, and data stores hold what persists between steps. ignatius renders them in the SSADM style with Gane-Sarson notation — open-ended `D#` store boxes, numbered process hubs, green external boxes — in the **Flows** view of the app. +A model can carry data flow diagrams (DFDs) alongside its entities. A DFD shows what the business *does*: numbered processes transform data, external entities send it in and receive it out, and data stores hold what persists between steps. ignatius renders them in the SSADM style with Gane-Sarson notation — open-ended store boxes with kind-specific numbered caps, numbered process hubs, green external boxes — in the **Flows** view of the app. The same markdown-first rule applies: flows live as `.md` files with YAML frontmatter, the diagram is generated, and a `db:` store in a flow is the *same entity* you modeled in the ERD — clicking it opens the full entity dialog with columns, relationships, and examples. @@ -156,7 +156,7 @@ Append-only log of raw gateway responses. Used for reconciliation and dispute resolution; never read back during normal processing. Retained for 7 years. ``` -Store kinds also drive each node's color in the diagram — theme-aware and overridable under `theme.flowKinds` in `ignatius.yml` (see [Themes and branding](themes-and-branding.md)). The full kind vocabulary is in the [glossary](../glossary.md). +Store kinds drive both the numbered cap and the color of each concrete store row: `D#` database, `C#` cache, `Q#` queue, `F#` file, `Do#` document, `M#` manual, and `O#` other. Each kind has its own number sequence. In a mixed stack, every row keeps its own kind color rather than tinting the whole stack; collapsed entity cluster/group rows remain database-colored with their unnumbered `C`/`G` caps. Colors are theme-aware and overridable under `theme.flowKinds` in `ignatius.yml` (see [Themes and branding](themes-and-branding.md)). The full kind vocabulary is in the [glossary](../glossary.md). ## Sub-DFDs @@ -251,7 +251,7 @@ A lone table is a table row at every level. ### Stacks and the stack dialog -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 ` subtypes` when it is not. An adjacency stack is labelled `N stores`, never a member's name. Every stack carries one ⓘ badge. +A stack draws visible rows: concrete stores carry their kind-specific numbered cap and color; cluster and subtype rows cap with unnumbered `C`, group rows with unnumbered `G`, 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 ` 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. 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/.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. diff --git a/docs/spec/dfd-store-clusters.md b/docs/spec/dfd-store-clusters.md index 2b63f97..eb2d1fb 100644 --- a/docs/spec/dfd-store-clusters.md +++ b/docs/spec/dfd-store-clusters.md @@ -189,14 +189,17 @@ src/flow-view/elk-flow-layout.ts threads view/collapseLevel/clusters/subtypeClusters/groups opts into buildFlowData src/flow-view/FlowDiagramSvg.tsx - StoreNode - stacked variant — visible rows; a cluster or subtype row caps `C`, a group row caps `G`, store - rows cap `D#`; a C or G row draws the stacked-paper marker (two filled sheets behind it offset - 3px and 6px down-right showing bottom edges, left stair segments, and the top-right mark, no - cap divider on the sheets, the stack's left border and cap divider drawn per row, a reserve - of offset plus stroke plus two pixels after the row, none after the last row, whose box ends - on the back sheet with no closing line); the duplicate marker is drawn in the connected view - only + StoreNode / stacked variant + every concrete store row retains its endpoint kind and resolves its own theme color; mixed + stacks never inherit one member's color across every row; kind-local numbered caps are + `D#` db, `C#` cache, `Q#` queue, `F#` file, `Do#` document, `M#` manual, `O#` other + (each kind has an independent sequence); a cluster or subtype row caps unnumbered `C`, a + group row caps unnumbered `G`, and those aggregate entity rows use db colors; a C or G row + draws the stacked-paper marker (two filled sheets behind it offset 3px and 6px down-right + showing bottom edges, left stair segments, and the top-right mark, no cap divider on the + sheets, the stack's left border and cap divider drawn per row, a reserve of offset plus + stroke plus two pixels after the row, none after the last row, whose box ends on the back + sheet with no closing line); the duplicate marker is drawn in the connected view only StackDialog trigger — click on a stack node opens the row-list dialog EdgeContractDialog trigger — click on any data-carrying edge chip opens the contract dialog chip rendering — label only; no member or column count suffix @@ -206,8 +209,9 @@ src/app/components/flow-node/StackDialog.tsx StackDialog — titled by source (cluster label, basetype, group label, "N stores", or "Read stack" / "Write stack"), with the feeding processes as dotted-number links in the header; rows at the current collapse level; opening a cluster/subtype/group row shows - its members with D#s; a member opens its entity dialog; an author-cluster row also renders - the file's markdown body, a subtype row links the basetype (or reads " subtypes" + its members with their kind-specific numbered caps; a member opens its entity or store + dialog; an author-cluster row also renders the file's markdown body, a subtype row links + the basetype (or reads " subtypes" when the basetype itself isn't a member), a group row shows the group's description, and an adjacency row lists the shared reader and writer processes instead of any body diff --git a/docs/wiki/feature-map.md b/docs/wiki/feature-map.md index f932964..68f4f21 100644 --- a/docs/wiki/feature-map.md +++ b/docs/wiki/feature-map.md @@ -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 <basetype>"/"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___`) 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 + 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/.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) | +| 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; mixed stacks keep per-row kind colors and kind-local numbered caps `D#`/`C#`/`Q#`/`F#`/`Do#`/`M#`/`O#`; working unnumbered C/G row disclosures; `clusters/.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 (Stores; Labels, stacks, clusters, and groups), folder-format (`clusters/`), validation (cluster rules) | SKILL core rule (labels and clusters always), dfd-authoring (store-kind visual identity, 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; `` and `` managed regions beside hand-authored ``; `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). diff --git a/docs/wiki/flow-view.md b/docs/wiki/flow-view.md index f317fa0..b26edbc 100644 --- a/docs/wiki/flow-view.md +++ b/docs/wiki/flow-view.md @@ -84,7 +84,7 @@ An author cluster's 2-per-process qualification (`qualifyPerProcess`, `STACK_THR | `clusters` | One row per explicit `cluster:`-tagged group and per subtype family with 2+ members in this stack, then one row per remaining loose store. | | `groups` | One row per entity group with 2+ members in this stack (nesting its cluster/subtype and table rows as `children`), then a cluster/subtype spanning 2+ groups as its own row, then loose tables. | -A `store` row caps with its own `D#`; a `cluster`/`subtype` row caps `C`; a `group` row caps `G` — never a number, since only a store row is one. `stackRowBodyText(row)` returns the text a row renders (a store row's `displayName`, a grouped row's `