Goal: make every multi-file feature folder under packages/client/src/ declare an explicit public surface (a barrel index.ts) and let dependency-cruiser enforce that outside callers reach only through it. Internal modules stop being callable by accident; the folder becomes a real unit of refactoring.
This is the boundary discussion that came up during #922 review. The conclusion: the right tool for kolu is dependency-cruiser (already being introduced on the depcruise-setup branch — must land first), not Biome (which only restricts which modules are importable globally, not who imports them).
Depends on
Candidate folders
Folders inside packages/client/src/ that have ≥5 files, a discrete concern, and a narrow public surface today (the actual external imports — most files are internal-by-convention):
| Folder |
Files |
Likely public surface |
canvas/ |
~23 |
TerminalCanvas, the public hooks (useCanvasArrange, useCanvasFocus), the dock model |
terminal/ |
~14 |
TerminalContent, AgentIndicator, ChecksIndicator, SubPanelTabBar, SearchBar |
right-panel/ |
~14 |
RightPanel, RightPanelLayout, openInCodeTab, useRightPanel |
comments/ |
12 |
CommentComposer, CommentsTray, CommentTextSurface, CommentIframeSurface, useCommentScrollRequest (5 names) |
recorder/ |
6 |
RecordButton, RecordPopover |
input/ |
6 |
useShortcuts, actions, platform helpers |
settings/ |
5 |
SettingsPopover, tips, useColorScheme, useTips |
ui/ is intentionally a broad component library and stays out. rpc/ and debug/ are too small to be worth a barrel.
Per-folder mechanical recipe
-
Create <folder>/index.ts re-exporting only the names imported from outside the folder today. grep -rn "from \"\\.\\./<folder>/" packages/client/src/ | grep -v "^packages/client/src/<folder>/" enumerates the de-facto surface.
-
Rewrite the external import sites to from "../<folder>" (no deep paths).
-
Add a rule to .dependency-cruiser.mjs (one rule covers all folders via a from/to pattern with back-substitution — see existing integrations-no-siblings for the pattern):
{
name: "client-feature-folder-encapsulation",
severity: "warn", // promote to "error" once findings clear
comment:
"Outside a client feature folder, imports must go through the " +
"folder's barrel — no deep imports into individual files.",
from: {
path: "^packages/client/src/(canvas|terminal|right-panel|comments|recorder|input|settings)/.+",
// exempt files inside the same folder
},
to: {
path: "^packages/client/src/(canvas|terminal|right-panel|comments|recorder|input|settings)/[^/]+",
pathNot: "^packages/client/src/[^/]+/index\\.ts$",
},
}
The from/to capture-group trick from integrations-no-siblings lets us write one rule rather than seven. The exact regex needs care — the rule must fire only on cross-folder edges, not folder-internal ones.
-
Ratchet: start at warn, fix the violations in batches, flip to error once each folder is clean.
Out of scope
packages/client/src/*.ts(x) top-level files — they're already the de-facto barrel. App.tsx pulls them directly; that's fine.
- Promoting any folder to its own workspace package. Boundary enforcement gets us 90% of the benefit at 5% of the cost. Promote only if a folder actually wants to be reused outside the client app.
packages/server/, packages/integrations/* — the layer rules already in .dependency-cruiser.mjs (anyagent-leaf, integrations-no-siblings, client-no-integration-runtime) cover the cross-package axis. This issue is specifically about feature folders inside a single package.
Notes from #922
The comments/ folder is the worked example. Its actual public surface (from the PR-review investigation):
CommentComposer, CommentsTray, CommentTextSurface, CommentIframeSurface, useCommentScrollRequest
Everything else (useComments, useTextSelection, highlightOverlay, formatMarkdown, SelectionPill, Comment/PersistedShape types, useComposer) is consumed only inside the folder. A barrel of 5 lines would lock that down — but only the dependency-cruiser rule turns it from convention into a CI gate.
Why dependency-cruiser, not Biome
biome lint's noRestrictedImports only restricts which modules are importable globally; it has no equivalent of ESLint's no-restricted-paths (the importer-location axis), so it can't express "anything outside comments/ may not deep-import into it." dependency-cruiser does this natively via from/to path patterns, and gets us the architecture graph (just depcruise-graph) as a side effect.
Goal: make every multi-file feature folder under
packages/client/src/declare an explicit public surface (a barrelindex.ts) and letdependency-cruiserenforce that outside callers reach only through it. Internal modules stop being callable by accident; the folder becomes a real unit of refactoring.This is the boundary discussion that came up during #922 review. The conclusion: the right tool for kolu is
dependency-cruiser(already being introduced on thedepcruise-setupbranch — must land first), not Biome (which only restricts which modules are importable globally, not who imports them).Depends on
depcruise-setupbranch merged (PR TBD). This issue's enforcement step plugs a new rule into.dependency-cruiser.mjs.Candidate folders
Folders inside
packages/client/src/that have ≥5 files, a discrete concern, and a narrow public surface today (the actual external imports — most files are internal-by-convention):canvas/TerminalCanvas, the public hooks (useCanvasArrange,useCanvasFocus), the dock modelterminal/TerminalContent,AgentIndicator,ChecksIndicator,SubPanelTabBar,SearchBarright-panel/RightPanel,RightPanelLayout,openInCodeTab,useRightPanelcomments/CommentComposer,CommentsTray,CommentTextSurface,CommentIframeSurface,useCommentScrollRequest(5 names)recorder/RecordButton,RecordPopoverinput/useShortcuts,actions, platform helperssettings/SettingsPopover,tips,useColorScheme,useTipsui/is intentionally a broad component library and stays out.rpc/anddebug/are too small to be worth a barrel.Per-folder mechanical recipe
Create
<folder>/index.tsre-exporting only the names imported from outside the folder today.grep -rn "from \"\\.\\./<folder>/" packages/client/src/ | grep -v "^packages/client/src/<folder>/"enumerates the de-facto surface.Rewrite the external import sites to
from "../<folder>"(no deep paths).Add a rule to
.dependency-cruiser.mjs(one rule covers all folders via afrom/topattern with back-substitution — see existingintegrations-no-siblingsfor the pattern):The
from/tocapture-group trick fromintegrations-no-siblingslets us write one rule rather than seven. The exact regex needs care — the rule must fire only on cross-folder edges, not folder-internal ones.Ratchet: start at
warn, fix the violations in batches, flip toerroronce each folder is clean.Out of scope
packages/client/src/*.ts(x)top-level files — they're already the de-facto barrel. App.tsx pulls them directly; that's fine.packages/server/,packages/integrations/*— the layer rules already in.dependency-cruiser.mjs(anyagent-leaf,integrations-no-siblings,client-no-integration-runtime) cover the cross-package axis. This issue is specifically about feature folders inside a single package.Notes from #922
The
comments/folder is the worked example. Its actual public surface (from the PR-review investigation):CommentComposer,CommentsTray,CommentTextSurface,CommentIframeSurface,useCommentScrollRequestEverything else (
useComments,useTextSelection,highlightOverlay,formatMarkdown,SelectionPill,Comment/PersistedShapetypes,useComposer) is consumed only inside the folder. A barrel of 5 lines would lock that down — but only the dependency-cruiser rule turns it from convention into a CI gate.Why dependency-cruiser, not Biome
biome lint'snoRestrictedImportsonly restricts which modules are importable globally; it has no equivalent of ESLint'sno-restricted-paths(the importer-location axis), so it can't express "anything outsidecomments/may not deep-import into it." dependency-cruiser does this natively viafrom/topath patterns, and gets us the architecture graph (just depcruise-graph) as a side effect.