feat(laymos): within-layer module groups + module-tile highlight fix - #20
feat(laymos): within-layer module groups + module-tile highlight fix#20kishorenuma wants to merge 11 commits into
Conversation
…light
Introduce Module Groups, a within-layer disclosure tier that clusters
sibling modules the way a Layer Graph clusters layers. A group is a
named, described set of modules that all live in the same layer, so the
module view reads as a handful of labeled clusters (Layer > Group >
Module) instead of a flat list.
Core (laymos):
- `group(name, modules, { description })` builder and `ModuleGroup` type
- optional `moduleGroups` config field; members reuse values already
declared in `config.modules`, mirroring module rules
- validation: same-layer members, at-most-one-group membership, unique
names, non-empty description, non-empty membership, reused values
- report gains an always-present `moduleGroups` registry
- CONTEXT glossary entry and README usage
Frontend:
- model exposes `groups` and `groupByModule`; layers carry `groupNames`
- packed layout renders each group as a labeled band within its layer
container, with an ungrouped tail; configs without groups are unchanged
- fix: module tiles now read selection state (selected/related/dimmed)
from node data instead of React context, so clicking a module repaints
the node highlight immediately instead of only after a pan/drag
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9039c6b82e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| violationCount: memberPaths.reduce( | ||
| (total, path) => total + (modules.get(path)?.violationCount ?? 0), | ||
| 0, |
There was a problem hiding this comment.
Count each intra-group violation once
When a violating file import connects two modules in the same group, this sum counts the violation once for each endpoint because incidentViolations increments both edge.from and edge.to. The group header therefore displays 2 violations for a single invalid import; derive the group total directly from the violating file edges touching its members, deduplicating edges whose endpoints are both inside the group.
Useful? React with 👍 / 👎.
Add `groupedModuleArchitectureReport` — a dense report whose crowded `application` and `domain` layers are chunked with Module Groups, including an ungrouped tail in `application` and untouched `entry` / `platform` layers so grouped bands, the bare tail, and mixed layers all render in one view. Wire it into the cosmos fixture as "grouped architecture" and lock its shape with a model test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
Replace the always-expanded labeled bands with real progressive disclosure. A group now collapses to a single node by default, so a crowded layer reads as a handful of group nodes (plus any ungrouped tiles) instead of every module at once. Clicking a group expands it into a labeled band of its member tiles; clicking the header collapses it again. - layout: `layerGroupLayout` packs collapsed group nodes and ungrouped tiles into the layer grid, and stacks expanded groups as bands below; height reflects `expandedGroups`, width is unchanged so lanes stay stable across expand/collapse - edges: a selected module's edges route into a collapsed group node and fan out to members once expanded (`endpointId` resolves layer > group > module) - one `module-group` node renders both states (collapsed card with handles + count, expanded header with collapse affordance); toggled via `onToggleGroup` on the interaction context - groups collapsed by default; state resets on report change - layout tests lock collapse, expand, and edge-into-group routing Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
Selection highlights (and the edges that appear on selection) lagged until an unrelated pan or drag. Root cause: the graph passed derived `nodes`/`edges` as read-only props, so xyflow synced them through a post-paint effect and left the canvas a frame behind each state change until a viewport event flushed the store. Make it a properly controlled flow. `useFlowElements` owns xyflow's node/edge state via `useNodesState`/`useEdgesState`, pushes each new computed layout in a `useLayoutEffect` (pre-paint), and wires `onNodesChange`/`onEdgesChange`. The layout stays the single source of truth; xyflow now repaints selection state on the same commit as the click that caused it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
Expanding a group no longer relocates its members to a labeled band at the bottom of the layer. The group now opens where it sits: collapsed groups and ungrouped modules flow as tile rows, and an expanded group becomes a full-width nested container in that same flow, holding its member tiles behind its own header. The header carries the group name, its counts, and a minimise button (top-right) that collapses it again. - layout: `layerGroupLayout` walks layer items in order and splits them into tile-row blocks and in-place container blocks; container members pack inside the box, width stays stable, height reflects what's open - new `module-group-container` background node (header + minimise); `module-group` is now collapsed-only - member tiles render above the container (layer-parented, higher z-index); edges still resolve layer > collapsed-group > module - layout tests updated to assert the container/collapsed split Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
A control-panel switch (shown only when the report declares groups) expands every group at once. Turning it on force-expands all groups; turning it off hands control back to manual so the reader can collapse each one individually. Clicking any single group drops out of "expand all" and back into manual state. State resets with the report. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
… bands Inside an expanded group container, members now sort by their group-internal edges: seeds (nothing else in the group imports them) on top, the connective core in the middle, leaves (they import nothing else in the group) at the bottom, separated by labeled hairlines. The in-between modules read apart from the group's entry and exit points. Single-band groups render unlabeled, exactly as before. The grouped fixture gains a few intra-group imports so the bands are exercised. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
…buttons Swap the "Expand all groups" switch for two explicit actions — "Expand all" and "Collapse all". Expand all is disabled once every group is open; collapse all is disabled once every group is closed, so the control always reflects what's actionable. Individual group toggles are unchanged. Removes the auto-expand mode in favour of directly driving the expanded-groups set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
…ting Module (and collapsed-group) tiles no longer ellipsize their labels — the whole point of a node is to say what the module is. The layout now computes one uniform tile size per report that fits the longest label: labels are monospace, so width follows deterministically from character count (no canvas measuring, works in tests). A label too long for the max width wraps onto extra lines and every tile grows to match, keeping the pack uniform. Tile width/height thread through the packing helpers via a cached ModuleMetrics; the label CSS switches from truncate to break-words. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
Remove the "seeds"/"core"/"leaves" text from the expanded group container. The members stay sorted into those bands and a thin hairline still delineates them, just without the labels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
Hovering a collapsed group node now previews its connectivity, the way selecting a module does: the group's boundary edges (those crossing into or out of the group) light up routed to the group node, the group and the modules it connects to highlight, and everything else dims. Internal edges stay hidden since a collapsed group draws them inside itself. The preview is transient — it overrides the current selection only while the pointer is on the group — via a new getGroupHoverSelection and hovered group state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma9T44AwqyiidS5XrwcvVL
Why
The module view removes file-level noise, but a layer with twenty modules is still twenty boxes — the flat list overwhelms. Layers don't have this problem because a layer never floats alone: it lives inside a Layer Graph, the handful-sized bundle a reader actually scans. Modules had no equivalent tier. This PR adds one, and fixes a graph-repaint bug found along the way.
Module Groups — a within-layer disclosure tier
A Module Group is a named, described cluster of sibling modules that all live in the same layer. It is to modules what a Layer Graph is to layers, scoped to a single layer so it nests instead of forming a second hierarchy:
Design write-up:
devtools/laymos/docs/adr/0020-module-groups.md.Config API
Mirrors the
moduleRulesconvention — members reuse values already declared inmodules, so a module is never declared twice:Invariants (both are config errors when broken)
Plus: unique group names, non-empty description, non-empty membership, and members must reuse declared
modulesvalues. Groups are purely organizational — they impose no import rule;canImport/canImportedBystay module-level.Rendering — progressive disclosure (collapse to a node)
This is the point of the tier, so the UI treats it as one:
endpointIdresolves layer → group → module). Selecting a module dims unrelated groups and highlights related ones.Try it in cosmos →
laymos-modules→ grouped architecture (a dense report whoseapplicationanddomainlayers are grouped, with an ungrouped tail inapplicationand untouchedentry/platform).Where it flows
laymos—group()builder +ModuleGrouptype + validation.moduleGroupsregistry (name → { description, modules }), empty when unused. The group's layer is inferred by consumers, the way a module's layer already is.groups/groupByModuleand layers carrygroupNames; the packed layout collapses/expands groups per the above.Bug fix: graph didn't repaint until a drag
Clicking a module updated the selection and recomputed the layout, but the canvas — the appearing edges and the node highlighting — didn't repaint until an unrelated pan or drag "revealed" the already-computed state.
Root cause: the graph passed derived
nodes/edgesto<ReactFlow>as read-only props, so xyflow synced them into its store through a post-paint effect and could leave the canvas a commit behind each state change until a viewport event flushed it.Fix: make it a properly controlled flow. A small
useFlowElementshook owns xyflow's node/edge state (useNodesState/useEdgesState), pushes each new computed layout in auseLayoutEffect(pre-paint), and wiresonNodesChange/onEdgesChange. The computed layout stays the single source of truth; xyflow now repaints selection state on the same commit as the click. (The module tiles also readselected/related/dimmedfrom nodedata— xyflow's reactive channel — rather than from React context.)Considered & rejected
Testing
laymos:pnpm build && pnpm lint(format + eslint + tsc + CLI self-lint) andpnpm test— 83 passed, incl. 6 group-validation cases and a report-emission case.frontend:vitestonlaymos-modules— 29 passed, incl. model group tests and layout tests that lock collapse, expand, and edge-into-group routing;laymos-layers21 passed;vp checkclean on touched files.Note: the grouped geometry and the repaint timing are verified by construction + the layout tests rather than visually (no UI in the build env) — the cosmos "grouped architecture" fixture makes it a quick check.
🤖 Generated with Claude Code