Skip to content

Installation

Two installs, for the two ways the same policy is evaluated:

Terminal window
pnpm add -D @goodbones/oxlint # the plugin: editor feedback and `oxlint` in CI
pnpm add -D @goodbones/cli # the `architecture` CLI: check, baseline, coverage, and the graph family

Both pull in @goodbones/core (the manifest schema and the evaluators), @goodbones/typescript (the TypeScript language pack) and @goodbones/campaigns (the campaigns family, which the core does not own) transitively; you never install those directly until a second language pack exists. The pack parses files with oxc-parser and resolves modules with unrs-resolver; each ships its own native binary as a platform package, and there is nothing else to configure at the OS level.

oxlint loads JS plugins with a bare dynamic import(), so the plugin is registered by specifier and its five rules are enabled by id:

.oxlintrc.json
{
"jsPlugins": [{ "name": "architecture", "specifier": "@goodbones/oxlint" }],
"rules": {
"architecture/imports": "error",
"architecture/exports": "error",
"architecture/members": "error",
"architecture/structure": "error",
"architecture/surface": "error",
"import/no-cycle": "error"
}
}

import/no-cycle is oxlint’s own, and it owns the one check a per-file rule cannot make.

The plugin reads the manifest from the repository root at module load — architecture.yaml by name, or one of the other forms — which is also when it runs the probe check. A malformed manifest or a vacuous rule fails there, loudly, rather than lowering to a policy that matches nothing.

architecture init writes a starter: one open root, the adoption ceilings at zero, and a comment per section naming the page that explains it.

Terminal window
pnpm exec architecture init

Or start from the tree instead of a blank page: architecture infer describes what the repository does today as a manifest check already accepts, and the work is deleting from it. See Starting from the tree.

Or write one by hand. This is a complete policy:

architecture.yaml
# yaml-language-server: $schema=https://dataquail.github.io/goodbones/schema/architecture.schema.json
resolve:
scopes:
- files: ""
language: typescript
options: { tsconfig: tsconfig.json }
unresolved: error
aliases:
"@": src
tree:
"@/domain/":
message: "domain/ is the model. It reaches nothing but itself."
imports:
message: "domain/ may not reach the framework."
reset: true
external: [effect]
allow: ["@/domain/**"]
children:
"*.ts": {}

It says four things:

  • src/domain/ admits .ts files and nothing else.
  • Files there may import from src/domain/, and from effect.
  • Anything else — another folder, another package — is refused with the message given.
  • Every import specifier in the repository must resolve to something.

Before trusting a new rule, plant the violation it exists to catch and watch it fail:

Terminal window
echo 'import "express";' > src/domain/probe.ts
pnpm oxlint src
rm src/domain/probe.ts

This is the habit the whole package is built around, and it is automated two ways: the plugin’s own load-time probe check, and the architecture CLI.

The first line of the manifest names its JSON Schema. Any editor with a YAML language server — VS Code’s YAML extension, for one — completes every key from it and flags a misspelled one as you type, before the loader runs. The schema is generated from the codec that decodes the manifest, so what the editor accepts is what the loader accepts.

What the editor misses, the loader reports with a file, line and column, and every issue at once rather than the first. A repository that would rather write the manifest as JavaScript can — see Using a JavaScript manifest — and gives up both of those to do so.

If you consume the plugin from a workspace package rather than from npm, the compiled output must exist before oxlint runs — oxlint imports JavaScript, and a stale build enforces a stale policy while still linting green. Make the build a prelint step rather than a convention.