feat: enforce import-direction DAG (Phase-5 layering lint)#984
Merged
Conversation
Generalize the inline CI "Layering Guard" grep into a structured
import-direction lint (scripts/layering/check.ts) over the resolved
import graph, per plans/perfect-shape.md §5.5.
The full target DAG (kernel ◄ platforms ◄ core ◄ commands ◄ {cli,
client, daemon/server}; client ◄ daemon/client) is only partly realized
— the client/remote/metro extraction, the daemon/server split, and the
utils dissolution are still pending Phase-5 moves, so the tree still
holds legitimate back-edges (platforms→core, commands→cli, utils→*).
Enforcing the whole DAG today would need a mass import rewrite that
Phase 5 defers. The lint therefore enforces the three invariants the
completed moves (kernel/, daemon/client/) already guarantee and that are
green today:
R1 kernel-sink — nothing under src/kernel/ imports another zone,
except the one type-only kernel→contracts re-export.
R2 commands-floor — nothing below the command surface (kernel,
platforms, core, daemon) imports src/commands/.
Generalizes the former guard (daemon + platforms).
R3 platforms-seam — platforms/ is statically imported only at the
core interactor seam (src/core/interactors/) and by
the daemon server; elsewhere use a dynamic import()
or a type-only import, preserving CLI cold-start.
Dynamic import('../platforms/*') and `import type` stay allowed.
Fixes the three pre-existing R3 violations by converting static
platforms value imports to dynamic imports (all in already-async call
sites, behavior-preserving and cold-start-improving):
- src/client/client.ts debug.symbols → lazy symbolicateCrashArtifact
- src/cli/commands/web.ts setup/doctor → lazy agent-browser-tool
- src/core/dispatch-interactions.ts runner-sequence → lazy (matches the
file's own dynamic-import pattern)
Wire the check into the Layering Guard CI job and add a check:layering
package.json script (also folded into check:tooling). scripts/layering/**
is excluded from fallow (untested CI script, like scripts/perf/**).
|
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
Member
Author
|
Review pass for head 64b52a5 found no actionable blockers. Checked the new layering script and the three runtime import changes. The script enforces the documented current invariants only: kernel sink, commands floor, and platforms seam, while preserving dynamic imports and type-only imports. The changed call sites were already async and now lazy-load platform code, matching the cold-start/layering direction. The CI Layering Guard wiring and check:layering/check:tooling integration are consistent, and all reported checks are green. |
thymikee
added a commit
that referenced
this pull request
Jul 1, 2026
…985) Phase-5 §5.5 folder move (server side; the daemon/client/ split shipped in #962). Extracts the process-bootstrap / server-runtime cluster into src/daemon/server/ as a pure, behaviorless path codemod — no logic changes. Moved (server bootstrap/runtime — the layer that spins up the daemon and owns the platform graph; each imported only by the bootstrap layer + each other): src/daemon-runtime.ts -> src/daemon/server/daemon-runtime.ts src/daemon/http-server.ts -> src/daemon/server/http-server.ts src/daemon/transport.ts -> src/daemon/server/transport.ts src/daemon/server-lifecycle.ts -> src/daemon/server/server-lifecycle.ts src/daemon/server-shutdown.ts -> src/daemon/server/server-shutdown.ts Left in src/daemon/ root (request core / shared wire helpers, out of scope): request-router.ts, handlers/, session-store.ts, lease-registry.ts, context.ts (the daemon's request layer) and http-contract.ts / http-health.ts / http-errors.ts / config.ts (HTTP wire contract + daemon config shared across client, remote, and cli — not server-only). Left: src/daemon.ts (the thin process entry) stays at src/ with the other package entrypoints; it is coupled to its physical path by four non-import string references (rslib entry, config dev-mode sentinel, process-identity detection regex, daemon-client launch srcPath), so moving it is beyond a pure import codemod. Rewrote every from/import/import()/type-only specifier per importer (resolve-based path.relative recompute) across src and test, and renamed the fallow health-baseline key for http-server.ts. daemon-runtime's static platforms/ import is now inside the daemon-server seam the layering lint (#984 R3) allows. Verification: tsc --noEmit 0; layering check (branch script) unchanged (3 pre-existing R3 violations, 0 new); oxfmt clean; oxlint --deny-warnings 0; fallow audit --base origin/main clean (14 files); rslib build 0 (internal/daemon entry still emits); vitest 17 passed (daemon-entrypoint, http-server-rpc-validation, server-shutdown + 3 provider-integration).
thymikee
added a commit
that referenced
this pull request
Jul 1, 2026
…#987) #984 added the R3 platforms-seam layering rule and #986 moved the public SDK entry barrels into src/sdk/; the two merged mutually inconsistent, so the Layering Guard is failing on main. sdk/ are public re-export barrels that legitimately expose platform symbols and are off the CLI cold path (not imported by bin.ts), so they are a correct R3 exemption alongside core/interactors and the daemon server — not a cold-start regression.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Generalizes the inline CI Layering Guard grep (which only checked that
src/daemonandsrc/platformsdon't importsrc/commands/) into a structured import-direction lint —scripts/layering/check.ts— over the resolved import graph. This is the Phase-5 capstone fromplans/perfect-shape.md§5.5.The DAG rule
Imports point DOWN toward the
kernel/sink:Why only three invariants (not the whole DAG)
The full DAG is a target. Several Phase-5 folder moves are still pending — the
client/remote/metroextraction, thedaemon/serversplit, and theutilsdissolution — so the tree still contains legitimate back-edges between folders that have not yet been separated (e.g.platforms→core,commands→cli,utils→*). Enforcing the whole DAG today would require a mass import rewrite that Phase 5 explicitly defers to "quiet windows."So the lint enforces exactly the three invariants that the already-completed moves (
kernel/,daemon/client/) guarantee and that hold green today:src/kernel/imports another zone, except the one type-onlykernel→contractsre-export (documented allowance).kernel,platforms,core,daemon) importssrc/commands/. Generalizes the former guard, which covered onlydaemon+platforms.platforms/is statically imported only at the core interactor seam (src/core/interactors/) and by the daemon server (src/daemon/minussrc/daemon/client/). Everywhere else must use a dynamicimport()or a type-only import — preserving CLI cold-start.Dynamic
import('../platforms/*')lazy-loading andimport typeare always allowed by R3.Allowances / exceptions
src/kernel/contracts.ts → src/contracts/debug-symbols.tsre-export (export type { … }), the one seam the plan documents. Encoded narrowly (contracts + type-only only).src/core/interactors/**) and the daemon server. No file-level allowlist was needed — the three pre-existing violations are fixed, not waived (below).Violations found + how handled
The lint surfaced three pre-existing R3 violations (static
platforms/value imports outside the seam). All three call sites were alreadyasync/await, so each was fixed by converting to a dynamicimport()— behavior-preserving and cold-start-improving, matching the plan's lazy-load principle:src/client/client.ts—debug.symbolsnow lazy-importssymbolicateCrashArtifact.src/cli/commands/web.ts—setup/doctornow lazy-importagent-browser-tool(type kept asimport type).src/core/dispatch-interactions.ts—runner-sequencenow lazy-imported insiderunIosSequenceChunks, next to the file's existingawait import('../platforms/...')calls.R1 and R2 were already green (0 violations).
Mechanism
Matches the existing guard's mechanism — a CI-invoked check over the source tree, not a new oxlint plugin. The
Layering Guardjob now runsnode --experimental-strip-types scripts/layering/check.ts(invoked directly, no deps install, so it stays fast). Added acheck:layeringpackage.json script (also folded intocheck:tooling).scripts/layering/**is excluded from fallow (untested CI script — a zero-coverage CRAP artifact, same treatment asscripts/perf/**).Verification
node scripts/layering/check.ts→ OK, 623 source files satisfy R1/R2/R3tsc -p tsconfig.json --noEmit→ 0oxlint . --deny-warnings→ clean ·oxfmt --check→ cleanfallow audit --base origin/main→ cleanrslib build→ 0