Skip to content

feat: enforce import-direction DAG (Phase-5 layering lint)#984

Merged
thymikee merged 1 commit into
mainfrom
phase5-import-direction-lint
Jul 1, 2026
Merged

feat: enforce import-direction DAG (Phase-5 layering lint)#984
thymikee merged 1 commit into
mainfrom
phase5-import-direction-lint

Conversation

@thymikee

@thymikee thymikee commented Jul 1, 2026

Copy link
Copy Markdown
Member

What

Generalizes the inline CI Layering Guard grep (which only checked that src/daemon and src/platforms don't import src/commands/) into a structured import-direction lintscripts/layering/check.ts — over the resolved import graph. This is the Phase-5 capstone from plans/perfect-shape.md §5.5.

The DAG rule

Imports point DOWN toward the kernel/ sink:

kernel ◄ platforms ◄ core ◄ commands ◄ { cli, client, daemon/server }
client ◄ daemon/client     remote, metro ◄ daemon/client     sdk = barrels only

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/metro extraction, the daemon/server split, and the utils dissolution — 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:

Rule Invariant
R1 kernel-sink Nothing under src/kernel/ imports another zone, except the one type-only kernel→contracts re-export (documented allowance).
R2 commands-floor Nothing below the command surface (kernel, platforms, core, daemon) imports src/commands/. Generalizes the former guard, which covered only daemon + platforms.
R3 platforms-seam platforms/ is statically imported only at the core interactor seam (src/core/interactors/) and by the daemon server (src/daemon/ minus src/daemon/client/). Everywhere else must use a dynamic import() or a type-only import — preserving CLI cold-start.

Dynamic import('../platforms/*') lazy-loading and import type are always allowed by R3.

Allowances / exceptions

  • R1: one type-only src/kernel/contracts.ts → src/contracts/debug-symbols.ts re-export (export type { … }), the one seam the plan documents. Encoded narrowly (contracts + type-only only).
  • R3: the core interactor seam (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 already async/await, so each was fixed by converting to a dynamic import() — behavior-preserving and cold-start-improving, matching the plan's lazy-load principle:

  • src/client/client.tsdebug.symbols now lazy-imports symbolicateCrashArtifact.
  • src/cli/commands/web.tssetup/doctor now lazy-import agent-browser-tool (type kept as import type).
  • src/core/dispatch-interactions.tsrunner-sequence now lazy-imported inside runIosSequenceChunks, next to the file's existing await 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 Guard job now runs node --experimental-strip-types scripts/layering/check.ts (invoked directly, no deps install, so it stays fast). Added a check:layering package.json script (also folded into check:tooling). scripts/layering/** is excluded from fallow (untested CI script — a zero-coverage CRAP artifact, same treatment as scripts/perf/**).

Verification

  • node scripts/layering/check.tsOK, 623 source files satisfy R1/R2/R3
  • tsc -p tsconfig.json --noEmit → 0
  • oxlint . --deny-warnings → clean · oxfmt --check → clean
  • fallow audit --base origin/main → clean
  • rslib build → 0
  • targeted unit tests (client, dispatch-interactions, web, debug-symbols, agent-browser-tool) → 88 passed

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/**).
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-01 08:02 UTC

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.4 MB 1.4 MB +652 B
JS gzip 450.5 kB 451.9 kB +1.4 kB
npm tarball 549.9 kB 549.9 kB -16 B
npm unpacked 1.9 MB 1.9 MB +758 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.0 ms 29.6 ms +1.7 ms
CLI --help 49.0 ms 52.4 ms +3.4 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/8832.js +73.8 kB +23.7 kB
dist/src/debug-symbols.js +13.8 kB +5.5 kB
dist/src/495.js -13.6 kB -4.8 kB
dist/src/9722.js -2.2 kB -628 B
dist/src/interaction.js 0 B -3 B

@thymikee

thymikee commented Jul 1, 2026

Copy link
Copy Markdown
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 thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jul 1, 2026
@thymikee thymikee merged commit 3d70943 into main Jul 1, 2026
22 checks passed
@thymikee thymikee deleted the phase5-import-direction-lint branch July 1, 2026 08:02
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant