Repository navigation
Architecture quality: replace custom graph/layering code with maintained tools, close guardrail gaps, fix collocation (umbrella) #3276
Description
Activity
Child issues created: WS1 #3277 (graph traversals, stacked on #3275), WS2 #3278 (boundary-tool spike ADR), WS3 #3279 (project references / tsc -b), WS4 #3280 (R5 (root)-zone blindness), WS5 #3281 (collocation decisions + first move batch), WS6 #3282 (daemon cold start), WS7 #3283 (depgraph reports + upstream issues). WS1 and WS7 stack on #3275; the rest are independent. Work is being dispatched to agents with hourly PR babysitting.
Progress: #3275 merged, which unblocked the stack. Merged workstreams: WS6 (#3284 → #3282 closed) and WS7's depgraph reports (#3285; #3283 stays open only for its upstream-issue deliverable, in flight). Reviewer-closed and merge-ready: WS2 #3286 (ADR 0032), WS5 #3287 (ADR 0033 + move batch), WS4 #3288 (one test-coverage finding being addressed), WS3 #3289 (project references; green after fixing a branch-specific freerange OOM). WS1 #3277 handled separately by the maintainer.
Architecture review follow-up: the merged library traversal work (#3275), report-only metrics (#3285), and targeted Maestro loading change (#3284) support the intended direction. The remaining design adjustments are concentrated here:
- ADR 0032 feedback: keep one import model without freezing its implementation; distinguish semantic blockers from migration costs; explicitly track the nine missed type-import pairs.
- Root-zone feedback: distinguish logical classification from physical collocation, and avoid making the per-file mapping the permanent ownership model.
- Collocation ADR feedback: reconcile the three promised “move (root pass)” rows with refactor(layering): rank every root module so R5 sees through (root) #3288, and distinguish sound ownership from constraints on a mechanical move.
- Prioritize existing gates: let the eager-closure budget tolerate a module split without admitting new eager work #2469 alongside this umbrella: ADR 0027's file-splitting conflict remains unresolved, and perf(daemon): lazy-load Maestro in request binding; budget the daemon entry #3284 expands the eager-count guard's reach. The guard must admit a healthy split while rejecting new eager work; replacing it with another large inference framework would miss the simplification goal.
Please carry these into the existing workstream tracking. The changes requested before merging the ADRs are scope/rationale corrections; the parser and guard work should remain focused follow-ups. Judge collocation by ownership and fewer independently maintained declarations per change. Keep community/modularity scores advisory, and remove custom checks only where the replacement proves the same intended invariant.
- added a commit that references this issue
on Oct 7, 2026 Reconciled the design follow-up (see the three PR comments): (a) #3286 ADR narrowed per suggestion — one import model, replaceable implementation, semantic blockers vs migration costs separated, maintainer's decision wording adopted (worker dispatched); (b) #3288 scope wording — (root) reduction is logical classification only, mapping framed as a bridge with directory/package-derived ownership as the end state (worker dispatched); (c) #3287 'keep' rows reworded (retain-for-batch vs sound ownership) and the three 'move (root pass)' rows reconciled with #3288: physical moves confirmed OUTSTANDING, tracked in new child issue #3294; the nine missed type-import pairs get child issue #3293 (parse static imports on the existing OXC AST, no second extractor); (d) #2469 (eager-closure split tolerance) prioritized and assigned to the daemon cold-start worker who owns that gate seam.
Progress: all review rounds from yesterday's design follow-ups and the R11/eager-closure reviews are closed. Merge-ready & green: #3289 (WS3), #3298 (#2469 eager-split rule). WS9 (#3294) delivered as two move PRs: #3297 (daemon-diagnostics-scope) + #3299 (runtime-command-surface/factory pair); answering cubic's ADR-provenance P3s now. Remaining open: #3283 (closure only), #3293 (held behind #3277). Maintainer merges pending on #3289/#3297/#3298/#3299.
Progress: maintainer approvals now cover all four open PRs (#3289, #3297, #3298, #3299). Final rounds closed since: #3289 confirmed the freerange cwd/config question in its body (plus took the reference-compaction and tsbuildinfo-uniformity items, -265 diff lines); #3298's gate rule passed the maintainer's novelty review with the addedModulesAreNew + deleted-source probes and closed the last probe-limit wording P2. All four reviewer-closed, 0 unresolved threads, checks green/in-flight. Program awaits merges; after #3297 merges, #3299 retargets to main. #3283 awaits closure only; #3293 held behind #3277 per plan.
Umbrella complete ✅
All workstreams under this umbrella are delivered and merged. Final state:
Merged PRs
- refactor(depgraph): run graph traversals on @statelyai/graph #3275, perf(daemon): lazy-load Maestro in request binding; budget the daemon entry #3284, feat(depgraph): dominator, zone-SCC, and cohesion report summaries #3285, docs(adr): retain the current layering engine (ADR 0032) #3286, refactor(move): collocation batch 1 — allocator contract into managed-allocation #3287, refactor(layering): rank every root module so R5 sees through (root) #3288, refactor(typescript): project references + tsc -b typecheck; retire the R11 branches tsc enforces #3289, refactor(move): daemon diagnostics scope into daemon-contracts (#3294) #3297, refactor(eager-closure): tolerate a pure split in the no-growth rule (#2469) #3298, refactor(move): runtime assembly pair into command-runtime (#3294) #3299 (WS2–WS7 + follow-ups)
- refactor(graph): use shared engine for gate traversals #3316 — shared graph engine for gate traversals (R73 BFS, R76 reverse reachability, mutation-ownership regex-extractor removal) —
Closes #3277 - fix(layering): parse static imports from OXC AST #3317 — OXC AST static-import parity in the layering parser —
Closes #3293
Closed issues: #3277, #3278, #3279, #3280, #3281, #3282, #3283, #3284, #3293, #3294, #2469.
Notes:
- The final traversals/parity work (refactor(graph): use shared engine for gate traversals #3316/fix(layering): parse static imports from OXC AST #3317) supersedes the stale
refactor/ws1-graph-traversalsbranch, which was never merged; its one unmerged correctness fix (NUL-joined edge ids,f1a64ef17) was absorbed and re-landed inside refactor(graph): use shared engine for gate traversals #3316 as its own tested commit. - The WS2 spike harness is pinned out-of-tree at
d9959f510(retrievable viagit show); ADR 0032 merged as ADR-only per the maintainer's decision.
All child workstreams delivered and merged; all child issues closed. Closing the umbrella.
Folding #2677 into this umbrella. Its measures (placement legibility via Jev, and change-coupling modularity from git history) never landed on main. They exist only on the stale branch
apex/module-shape-measures(86741f2, last touched 09-19), and thescripts/depgraphreports (#3285) have since become the report surface. If either measure is still wanted, add it as a workstream here, built onscripts/depgraphrather than as separate tooling. The full spec and measured evidence stay in #2677.
Purpose
Raise architecture quality and replace custom guardrail code with maintained tools wherever one does the job. This issue collects findings from a graph analysis of the production import graph: 1,837 files, 9,775 collapsed edges, 41 zones, at
ff685ad80. The analysis ran on@statelyai/graph(adopted forscripts/depgraph/in #3275). Each workstream below is meant to become its own child issue and PR.Principle: off-the-shelf first. Custom code stays only where no tool expresses the rule. Today that means symbol/AST ownership rules and ratchets measured against the merge-base.
Current custom surface
scripts/layering/: 75 files, about 13.9k lines (about 6k of them tests), enforcing 29 rule ids through its own oxc-based import resolver.scripts/layering/model.tsprovider-snapshot-presentation-policy.ts) and R76 (daemon-platform-runtime-inventory.ts)scripts/mutation/ownership.ts, which also has its own regex import extractorsrc/__tests__/eager-import-closure.fixtures.tsWorkstreams
1. Hand-rolled graph code →
@statelyai/graph(low risk; start here)Replace every traversal listed above with library calls on the shared
import-graph.tsconstruction path.mutation/ownership.tsshould read the layering edge model instead of its regex extractor.Done when:
grep -rnE "lowLink|queue\.(push|shift)" scripts/finds no import-graph traversal.2. Boundary-tool spike: fallow
boundariesvs dependency-cruiserChoose an engine for the plain import-graph rules.
boundaries.zones/ruleswithallowTypeOnly,coverage.requireAllFiles,calls.forbidden, and count or exact baselines. It has no dynamic-import distinction. Upgrading from 2.104.0 is a prerequisite.type-only/dynamic-importdependency types,viaOnlyon cycle rules,reachablerules, and a known-violations baseline. Its rules would be generated fromTARGET_DAG_RANK.Done when: an ADR records, for R2, R4, R5, R6, R77, R78, R14 and R71:
resolveImportEdgeson the current treeThen a follow-up PR migrates the chosen rules and deletes their custom code.
3. Workspace-level enforcement
All 25 packages set
composite: true, but no tsconfig declaresreferences. Add project references sotsc -benforces the package DAG and builds incrementally.Done when:
pnpm typecheckusestsc -b.4. Guardrail gap: R5 is blind through unranked zones
At zone level, value imports are not a DAG. Nine zones form one cycle:
cli,commands,core,daemon-client,daemon-server,remote,sdk,plugins,(root). Every closing edge goes through the unranked(root)zone, plus one same-rank pair,remote ⇄ daemon-server. At file level this hides two real inversions:src/sdk/index.ts(rank 4) →src/agent-device-client.ts(root) →src/daemon-client/daemon-client-transport.ts(rank 5), and the same path fromsrc/ai-sdk/index.ts.Done when:
(root)holds only true entry points and composition roots and the rest are ranked, or R5 also rejects ranked→higher-rank reachability through unranked files.5. Collocation
Louvain modularity is 0.564 for detected communities versus 0.373 for declared zones. The least cohesive zones are
kernel(26%),contracts(29%),sdk(31%),(root)(37%) andhost-kit(38%). Each figure is the share of the zone's files that land in its largest detected community. 48 files sit in a community that is at least 80% another zone (mostlydaemon-server); candidates, not a work list:capture-kit (8):
capture-kit/src/capture-admission/audio-probe-admission-ledger.ts,capture-kit/src/capture-admission/durable-capture-admission-ledger.ts,capture-kit/src/capture-admission/perf-capture-admission-ledger.ts,capture-kit/src/capture-admission/screen-recording-admission-ledger.ts,capture-kit/src/durable-json.ts→ managed-allocation,capture-kit/src/snapshot/snapshot-evidence.ts,capture-kit/src/snapshot/snapshot-freshness/index.ts,capture-kit/src/snapshot/touch-reference-frame.tscontracts (9):
contracts/src/android-observation.ts,contracts/src/android-system-chrome.ts,contracts/src/app-events.ts,contracts/src/app-state.ts,contracts/src/device-boot.ts,contracts/src/facades/device.ts,contracts/src/interaction-guarantees.ts,contracts/src/managed-device-allocation.ts→ managed-allocation,contracts/src/wait-runtime-plan.tsdevice-selection (4):
device-selection/src/device-inventory-context.ts,device-selection/src/device-selection-resolver.ts,device-selection/src/dispatch-resolve.ts,device-selection/src/open-target.tshost-kit (4):
host-kit/src/diagnostics.ts,host-kit/src/internal/request-cancel.ts,host-kit/src/request.ts,host-kit/src/session-paths.tskernel (1):
kernel/src/device-isolation.tsplatform-android (1):
platform-android/src/device-boot.tsplatform-apple (3):
platform-apple/src/runner-owner-facade.ts,platform-apple/src/runner/legacy-xctest-device-set.ts,platform-apple/src/simulator-boot.tsreplay-port (1):
replay-port/src/daemon-port/session-test-shard-devices.tsselectors (2):
selectors/src/parameterized-recorded-fill.ts,selectors/src/target-evidence.tscli (1):
src/cli/commands/device-release.ts(root) (12):
src/daemon-diagnostics-scope.ts,src/daemon-policy-file.ts,src/daemon.ts,src/platform-runtime-apple-runner-owner.ts,src/platform-runtime-daemon-lifecycle.ts,src/platform-runtime-device-boot.ts,src/platform-runtime-resource-cleanup.ts,src/provider-credential-fingerprint.ts,src/provider-limrun-runtime.ts→ provider-limrun,src/request-progress-protocol.ts,src/runtime-command-surface.ts,src/runtime-factory.tssdk (2):
src/sdk/limrun-runtime-types.ts→ provider-limrun,src/sdk/limrun.ts→ provider-limrunLargest type/dynamic strongly connected components (allowed by R4, but they tie modules together):
src/daemon/interaction/internal/provider-webdriversrc/cli/connection/src/commands/(batch/projection) andcommands/interaction/runtime/wait-*Related: Add report-only module-shape measures: placement legibility (Jev) and change-coupling modularity #2677 (change-coupling modularity) is the git-history counterpart of this static measure.
Done when: each item has a decision (move, merge, or keep with a reason), and moves land as
refactor(move)PRs.6. Daemon cold start (low priority)
src/daemon.tsloads 621 of the 1,435 files it can reach before the first request. The dominator tree of that eager closure shows the two biggest avoidable branches, both confirmed in the builtinternal/daemonchunks:request-binding.ts → replay-device-selection.tspulls in all ofmaestro(7.3k LOC).provider-device-runtimes.tspulls in all ofprovider-webdriver(5.4k LOC).The measured gain is about 10–15 ms of roughly 47 ms total module load.
Done when: both branches load through
import(), or the issue records why not.7. Report and upstream
pnpm depgraphas report-only fields.@statelyai/graph2.4.0 rough edges:getLouvainCommunitiesreturns ids, butgetModularityrequires node objects and throws on ids.toMermaid, but the package exportstoMermaidFlowchart.(root)./dotfortoDOTrequires thedotparserpeer.Non-goals
calls.forbiddencovers one exactly.