6 minutes read
Architecture
Shape
Offing is one TypeScript package with seven layers. A module belongs to exactly one layer and may import only the layers the table allows. Decision 0031 is the source of truth for the full allowed-imports matrix; this page explains the shape and where each piece lives. npm run check:architecture (tools/check-architecture.ts) enforces the rules that exist today and baselines known violations in tools/architecture-baseline.json, which may only shrink. tests/architecture-fitness.test.ts keeps these rules measured: it proves every rule rejects each forbidden import with a message naming the rule and the edge, pins the exported names of each public entry point (., ./sdk, ./conformance, ./mcp), and checks that detectors and the core cannot import network modules; tests/offline.test.ts covers the runtime offline guarantee.
A separate workspace package per layer is unnecessary while the interface and implementation share one release lifecycle. The public detector SDK is exported from @velella/offing/sdk.
Layers
| Layer | Lives in | Owns |
|---|---|---|
| Domain | domain.ts, errors.ts, paths.ts | Types, output contracts, pure helpers |
| Detector SDK | sdk.ts | API version, stable detector ID, one scan operation |
| Core | engine/, source-usage.ts, usage/, scan-cache.ts, detector-inputs.ts | Detector attribution, sorting, repository traversal, usage matching, rule loading |
| Detectors | detectors/ | Reading manifests and lockfiles for one ecosystem |
| Services | plan.ts, migration-packet.ts, fix/, sbom/, drift/, export/, inventory/, baseline.ts, diff.ts, receipt.ts, agent-tools.ts | Analysis built on a scan result. No argument parsing, no host protocol |
| Reporters | reporters.ts, reporters/, mermaid.ts | Text, JSON, SARIF, HTML, and other formats |
| Adapters | cli.ts, commands/, mcp.ts, lsp.ts, action.ts, github-action.ts, tui.ts | Translating one host interface to services and reporters |
Lifecycle rules, and the purl, VERS, registry, and forecast code that reads them, come from Ebb (@velella/ebb), a separate package. It is a dependency, not a layer: the domain and the core import it, and nothing in Ebb imports Offing.
Rules that follow from the table:
- The core never imports detectors. Built-in detectors reach the engine only through scan-builtins.ts, which an adapter or service calls.
- Detectors may use the shared usage matcher but may not import services, reporters, or adapters.
- Nothing imports an adapter except another adapter. Logic an adapter shares belongs in a service.
- Domain, SDK, and core are meant to do no file or network access; that rule lands with the pure-core work. Detectors and reporters never use the network.
How the pieces work
The detector SDK is the seam between the scan engine and detectors. Its interface is deliberately small. The engine owns detector attribution, sorting, and output contracts. See DETECTORS.md for the contract.
Source usage analysis is a shared batch module below the detector seam. A detector supplies declarative, language-specific match contracts; the module owns repository traversal, import binding resolution, call matching, configuration matching, deduplication, and impact calculation, and all matched package rules share one repository pass. Findings remain valid when no usages are found, because package metadata establishes the lifecycle risk independently of source usage.
JavaScript, TypeScript, and Python import and call matching use a vendored tree-sitter WebAssembly runtime under src/usage/vendor/tree-sitter/, limited to those four grammars. The WebAssembly modules are committed as base64 text with a manifest of their upstream source and SHA-256 hashes, and the loader decodes them in memory, so the repository carries no binary artifact. The package does not depend on @vscode/tree-sitter-wasm at install time: that package ships many unused grammars, and making it optional would fall back to the regex scanner and change scan results. When the parser cannot run (file too large, over budget, or runtime load failure), the shared module falls back to the regex scanner for that file.
The GitHub Action is an adapter over a single action-runner module. That module owns scanning, optional baseline comparison, severity-gate evaluation, annotation data, and report persistence behind one interface. The bundled entry point only translates environment inputs and outcomes to GitHub workflow commands; the composite wrapper uploads the already-generated report even when the gate fails.
Migration context is produced by a pure module whose interface accepts a scan result and finding ID. It owns lookup, affected-file derivation, constraints, ordered steps, sources, and verification commands. The CLI and local MCP server reuse the same interface.
The agent-tools module owns the complete scan-to-verification workflow behind a transport-independent interface. It remembers findings observed during one session so verification cannot mistake an arbitrary or unknown ID for a successful migration. Verification performs a fresh scan, compares the stable rule-and-path identity, and returns suggested commands without executing them. The MCP module is a thin adapter over this interface and contains no detector or migration logic. See MCP.md.
The local MCP entry point uses the official TypeScript SDK's stdio transport. It does not expose an HTTP listener, make outbound requests, or call a hosted Offing service. Repository analysis and session state stay on the user's machine.
The portable Agent Skill is a thin instruction adapter over the MCP and CLI interfaces. It owns no lifecycle facts, detector behavior, migration constraints, or permission grants. Codex and Claude plugins are generated host adapters over that Skill and the same local MCP launch contract; a deterministic assembly module pins both to the root package version and rejects drift in CI. See the plugin packaging architecture and Decision 0003.
Trust boundaries
- Repository contents are untrusted input.
- Registry entries are reviewed data, not executable code.
- The MCP server is stdio-only and has no network transport.
- Detectors cannot make network calls through the core contract.
- A finding must identify its detector, rule, evidence source, and affected location.
- AI-generated interpretation may enrich a finding but cannot create a core finding.
- Registry envelopes and every rule are validated before detector logic consumes them.
Hosting
No backend is required. The CLI and MCP server run locally, the GitHub Action runs on the repository's runner, and the rule registry ships with signed project releases. Documentation is a static GitHub Pages site: a workflow builds docs/ with Mizzen (Lateen theme) and deploys the _site artifact (pages.yml); it receives no repository source code.