magus v0.4.3 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
3 min read

Abstract at the right layer, or not at all

Over-abstraction is the failure I watch for most, in my own code and in what an agent hands me. Engineers are trained to abstract, and the instinct is usually right, which is why it goes wrong so quietly. A layer built on top of another team's abstraction, under a third team's, is three models of a thing nobody at the top can see. Each layer looks reasonable to whoever wrote it, and working on the top one means holding all three in your head.

It happens inside one repository too, and TypeScript is where I see it most: a directory of one-function modules, a barrel file re-exporting them, and an import chain five files long before you reach the line that does anything. To learn what one call does, you open every file in its lineage. Nothing was hidden by any of those boundaries; they only moved the code further from the reader.

A boundary earns its place by what it hides. A package with one exported function over four unexported helpers is real encapsulation at any size. A package that is one file and its test, exporting one function nothing else needed separated, is a rename with an import path. So is a Go interface with one implementation and no second on the way. The deliberate exception is a package stubbed for work that is planned and named, and it says so.

You cannot abstract a system you do not understand, and you cannot redesign one either. Before proposing a boundary, a rewrite or a new architecture, learn what drove the current design, where it hurts today and what it is for. A fence whose purpose you never learned looks like an obstacle, and taking it down is how the problem it held back comes back worse. Engineering for its own sake, a redesign that serves the person proposing it more than the people using the thing, is not worth the time it costs. In this repository the record is there to read: the decisions, A record of refusals, and magus explain on the code itself.

None of this is against structure. A directory tree that mirrors the domain, nested where the domain nests, with each package deep enough to justify its name, is one of the best things a codebase can have: you can read the architecture off ls. Done halfway, with packages too thin to hold anything, the same nesting becomes a maze. Hierarchy is the goal; thin layers are its counterfeit.

The same discipline reaches the words. magus names one concept once (target, spell, charm, op) and reuses it everywhere, rather than wrapping the same idea in a new term at each layer.

Enforced by

  • The file-split conformance metric keeps a concept in one file until that file passes 1500 lines, so a package does not fan out into thin files early.
  • The export-scope and dep-fanout conformance metrics, ratcheted in hack/policy/conformance.jsonl, flag an export nothing outside its package uses and a package that imports too much.
  • The architecture review skill names the too-thin shapes to check before proposing a boundary: a package imported only from inside its own subtree, a package with exactly one importer, and a single file with a single exported symbol.
  • Nothing yet fails a one-file package outright; that metric is a debt in Where this is strained.

Good

One package, deep enough to hold its concept, its files named for the parts of it (internal/guard):

internal/guard/
  catalog.go   every rule, its name and what it catches
  shell.go     judging a shell command
  push.go      the push rules
  write.go     judging a file write
  lease.go     who owns a path right now

Bad

src/utils/index.ts           export * from "./format";
src/utils/format/index.ts    export * from "./formatDate";
src/utils/format/formatDate.ts
                             export const formatDate = (d: Date) => toIso(d);
src/utils/format/toIso.ts    export const toIso = (d: Date) => d.toISOString();

Four files and two barrels to reach toISOString(). Every boundary is a rename, none of them hides anything, and a reader or an agent opens all four to learn that.

See also

designprincipleabstractionpackages
Last updated (2df714e8)
Glossary

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. Target in full.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). Op in full.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. Spell in full.

Charm

An execution modifier attached with : (lint:rw) that changes how a target runs, not which one; the built-in rw flips a check-only target to mutate in place, and ci always strips it. Charm in full.

Module

A magus stdlib namespace a magusfile imports for host capabilities: filesystem, exec, vcs, and more. Module in full.

Engine

The interpreter a magusfile runs on; magus embeds the Buzz engine. Engine in full.

CI

An ordinary target you compose yourself with ctx.needs; magus does not hardcode its stages. It is the anchor magus affected ci keys off, and ci strips the rw charm. CI in full.

Concurrency

How many targets run at once. It is bounded by the pool's capacity and set with --concurrency, MAGUS_CONCURRENCY, or the concurrency config key. Concurrency in full.

Agent

A model working through an agent host such as Claude Code, Codex, Cursor or OpenCode. magus treats an agent as one more caller of its CLI, MCP tools and console, held to the same checks as a person. Agent in full.

Tool

An action a model can call through its host: a name, a description, a typed input and a result. magus serves its tools over MCP, each a read of the workspace model or a run of the CLI it already has. Tool in full.

Skill

A packaged set of instructions an agent host loads when a task matches its description: a directory holding a SKILL.md. magus ships its magus-* skills in the binary and writes them with magus agent install. Skill in full.

Invocation

One magus process's recorded facts (the targets it finished, their outcomes, the lease it acted as, and the session it ran in when a host delivered one), kept in a repo-scoped store every worktree shares. magus session lists them; the store prunes itself by last-fact age. Invocation in full.

Lease

The grant a holder takes on a job: the write and read paths that job declared, enforced in the checkout that took it with magus job exec. A job is the piece of work; a lease is permission over it. Lease in full.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.