Skip to content

Architecture quality: replace custom graph/layering code with maintained tools, close guardrail gaps, fix collocation (umbrella) #3276

Description

@thymikee

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 for scripts/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.
    • About a third are plain import-graph rules: R2, R4, R5, R6, R9, R77, R78, plus the path parts of R10, R11 and R65.
    • The rest are AST or symbol-ownership rules: R7, R13, R16, R18, R66–R70, R72–R76, R79.
  • Hand-rolled graph code:
    • Tarjan's cycle algorithm twice, plus a DFS that extracts a cycle path, in scripts/layering/model.ts
    • BFS walks in R73 (provider-snapshot-presentation-policy.ts) and R76 (daemon-platform-runtime-inventory.ts)
    • scripts/mutation/ownership.ts, which also has its own regex import extractor
    • src/__tests__/eager-import-closure.fixtures.ts

Workstreams

1. Hand-rolled graph code → @statelyai/graph (low risk; start here)

Replace every traversal listed above with library calls on the shared import-graph.ts construction path. mutation/ownership.ts should read the layering edge model instead of its regex extractor.
Done when:

  • grep -rnE "lowLink|queue\.(push|shift)" scripts/ finds no import-graph traversal.
  • Every gate produces identical output on the current tree.
  • Planted-failure tests still fail.

2. Boundary-tool spike: fallow boundaries vs dependency-cruiser

Choose an engine for the plain import-graph rules.

  • fallow is already a dependency. 3.x has boundaries.zones/rules with allowTypeOnly, coverage.requireAllFiles, calls.forbidden, and count or exact baselines. It has no dynamic-import distinction. Upgrading from 2.104.0 is a prerequisite.
  • dependency-cruiser 18.5 has type-only / dynamic-import dependency types, viaOnly on cycle rules, reachable rules, and a known-violations baseline. Its rules would be generated from TARGET_DAG_RANK.
  • A past cross-check (depcruise 3.1.1) missed 88 dynamic/type-only edges that the oxc resolver finds.
    Done when: an ADR records, for R2, R4, R5, R6, R77, R78, R14 and R71:
  • the edge-set diff against resolveImportEdges on the current tree
  • planted-violation parity
  • runtime
  • lines deleted

Then 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 declares references. Add project references so tsc -b enforces the package DAG and builds incrementally.
Done when:

  • References are declared and pnpm typecheck uses tsc -b.
  • The parts of R11 that resolution or compilation now enforces are deleted, with planted proof.
  • Typecheck wall time is recorded before and after.

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 from src/ai-sdk/index.ts.
Done when:

  • Either (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.
  • The zone-level value graph is acyclic, asserted by a gate.
  • Dynamic-import direction (currently watched by nothing) is either covered or explicitly scoped out.

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%) and host-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 (mostly daemon-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.ts

  • contracts (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.ts

  • device-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.ts

  • host-kit (4): host-kit/src/diagnostics.ts, host-kit/src/internal/request-cancel.ts, host-kit/src/request.ts, host-kit/src/session-paths.ts

  • kernel (1): kernel/src/device-isolation.ts

  • platform-android (1): platform-android/src/device-boot.ts

  • platform-apple (3): platform-apple/src/runner-owner-facade.ts, platform-apple/src/runner/legacy-xctest-device-set.ts, platform-apple/src/simulator-boot.ts

  • replay-port (1): replay-port/src/daemon-port/session-test-shard-devices.ts

  • selectors (2): selectors/src/parameterized-recorded-fill.ts, selectors/src/target-evidence.ts

  • cli (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.ts

  • sdk (2): src/sdk/limrun-runtime-types.ts → provider-limrun, src/sdk/limrun.ts → provider-limrun

  • Largest type/dynamic strongly connected components (allowed by R4, but they tie modules together):

    • 33 files / 6.8k LOC in src/daemon/interaction/internal/
    • 7 in provider-webdriver
    • 6 in src/cli/connection/
    • 5 each in src/commands/ (batch/projection) and commands/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.ts loads 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 built internal/daemon chunks:

  • request-binding.ts → replay-device-selection.ts pulls in all of maestro (7.3k LOC).
  • provider-device-runtimes.ts pulls in all of provider-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

  • Optionally add the dominator, zone-SCC and community summaries to pnpm depgraph as report-only fields.
  • File upstream issues for the @statelyai/graph 2.4.0 rough edges:
    • getLouvainCommunities returns ids, but getModularity requires node objects and throws on ids.
    • The docs name toMermaid, but the package exports toMermaidFlowchart.
    • Mermaid output doesn't escape ids like (root).
    • Importing /dot for toDOT requires the dotparser peer.

Non-goals

  • Nx and Turborepo boundaries: they only see package-level edges, and none of the zone rules are package-level. Turbo boundaries are still experimental.
  • eslint-plugin-boundaries / Sheriff: they would run under Oxlint, whose JS plugins are still alpha.
  • Symbol/AST ownership rules stay custom unless fallow calls.forbidden covers one exactly.

Activity

  1. thymikee commented on Oct 7, 2026

    @thymikee
    MemberAuthor

    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.

  2. thymikee commented on Oct 7, 2026

    @thymikee
    MemberAuthor

    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.

  3. thymikee commented on Oct 7, 2026

    @thymikee
    MemberAuthor

    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:

    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.

  4. thymikee commented on Oct 7, 2026

    @thymikee
    MemberAuthor

    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.

  5. thymikee commented on Oct 7, 2026

    @thymikee
    MemberAuthor

    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.

  6. thymikee commented on Oct 8, 2026

    @thymikee
    MemberAuthor

    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.

  7. thymikee commented on Oct 8, 2026

    @thymikee
    MemberAuthor

    Umbrella complete ✅

    All workstreams under this umbrella are delivered and merged. Final state:

    Merged PRs

    Closed issues: #3277, #3278, #3279, #3280, #3281, #3282, #3283, #3284, #3293, #3294, #2469.

    Notes:

  8. thymikee commented on Oct 8, 2026

    @thymikee
    MemberAuthor

    All child workstreams delivered and merged; all child issues closed. Closing the umbrella.

  9. thymikee commented on Oct 8, 2026

    @thymikee
    MemberAuthor

    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 the scripts/depgraph reports (#3285) have since become the report surface. If either measure is still wanted, add it as a workstream here, built on scripts/depgraph rather than as separate tooling. The full spec and measured evidence stay in #2677.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions