Skip to content

chore(arch): enforce layer boundaries in-repo and defer the multi-repo split #930

Description

@devlikebear

Part of #919. Phase 3. LP-011.

Prerequisite: #927 (LP-008) — the layer rules are not satisfiable until the tool package is split.

Summary

Record the decision not to split TARS into tars-app / tars-cli / tars-console repositories at this time, and get the discipline that split would have provided from a CI-enforced layering rule instead.

Problem

The motivation for splitting is real: keep the library clean, and stop application changes from breaking external consumers. But the evidence says a split addresses less than it appears to.

The vertical boundary already holds. go list -deps ./pkg/... contains no internal/tarsserver. The server does not leak into the core in the import direction. The actual coupling problem is horizontal — one oversized internal/tool package — which a repository split does not touch. #927 does.

There is no separable desktop application. No Tauri, no Electron. What is described as the desktop app is golang.design/x/hotkey, internal/tarsserver/notify.go, and the same binary serving an embedded console. There is no third codebase to extract.

The costs are concrete. frontend/console is 41k LOC embedded via go:embed; splitting it requires a new artifact or npm pipeline just to keep make build working. CI already runs security, windows-build, windows-test, pr-diff, three CodeQL analyses, and SonarCloud. For a single maintainer, every change crossing a new repo boundary becomes a multi-repo PR — and provider work currently spans internal/llm, internal/config, internal/tarsserver, and frontend/console routinely.

The problem a split really solves is API stability, and #928 plus #929 solve that directly and reversibly. A layering rule is cheap to add and cheap to remove; a repository split is neither.

Scope

  • Write an ADR under docs/ recording the decision, the evidence above, and the criteria for revisiting it.
  • Define the layers: cmd/internal/app/* (tarsserver, cron, pulse, reflection, ops) → internal/core/* (llm, tool-core, session, memory, skill, mcp, prompt) → pkg/.
  • Enforce with a depguard rule in .golangci.yml or a make arch-check target, alongside the existing static-analysis guards.
  • Fail CI on a reverse import.
  • Document the layer map in CLAUDE.md and .analysis/AI_CONTEXT.md so future work respects it by default.
  • State the re-evaluation criteria explicitly: two or more external consumers, plus two consecutive quarters with no breaking change to pkg/*, plus a release pipeline that no longer needs the console embedded at build time.

Acceptance Criteria

  • A reverse import from a core package to an app package fails CI with a message naming both packages.
  • The current tree passes the rule without exceptions, or every exception is listed with a reason and an owner.
  • The ADR is committed and linked from the README architecture section.
  • The re-evaluation criteria are specific enough to be checked without reopening the debate.

Validation

  • Add a deliberate reverse import locally and confirm CI rejects it
  • make test
  • make lint
  • make lint-diff
  • make ci-static-analysis-check
  • make build

Out of Scope

  • Moving any package to another repository.
  • Splitting the Go module. If pkg/* ever becomes its own module, that is a separate decision made against the criteria in this ADR.
  • Renaming packages beyond what the layer map requires.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or requestgoPull requests that update go codemlp-domain-platformMLP: Domain-agnostic agent platform

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions