
# Decision records

These records explain the choices behind Offing's architecture, data format, contracts, and
policy. Each one is accepted unless a later record supersedes it, or unless its status says
otherwise. Records are numbered in the order they were written; this page groups them by theme.

## Detectors

| # | Title | Decision |
| --- | --- | --- |
| [0002](0002-go-ecosystem-support.md) | Add Go as a first-class detector ecosystem | Add evidence-backed Go runtime, toolchain, and direct-module lifecycle detection through the existing detector SDK, without requiring the Go toolchain or network access at scan time. |
| [0007](0007-container-detector.md) | Detect end-of-life container base images from Dockerfiles and Compose files | Add a `container` detector that reports base-image references whose release line has a vendor-published end of life. |
| [0008](0008-kubernetes-detector.md) | Detect removed and deprecated Kubernetes APIs in manifests, Helm charts, and kustomizations | Add a `kubernetes` detector that reports `apiVersion`/`kind` pairs the Kubernetes project has documented as deprecated or no longer served. |
| [0009](0009-java-detector.md) | Detect end-of-support JDK releases and relocated Maven dependencies in JVM projects | Add a `java` detector for pinned or targeted JDKs and Maven coordinates that a vendor or maintainer has documented as out of support or moved. |
| [0011](0011-rust-detector.md) | Detect unsupported Rust toolchains and abandoned or yanked crates | Add a `rust` detector that reads Cargo and toolchain files and reports lifecycle risks an authoritative source states. |
| [0012](0012-dotnet-detector.md) | Detect unsupported .NET targets and deprecated NuGet packages | Add a `dotnet` detector for unsupported .NET SDK/runtime lines and deprecated NuGet packages with publisher evidence. |
| [0013](0013-detector-maturity.md) | Detector maturity levels and the evidence to move between them | Use three levels — `experimental`, `beta`, and `stable` — with fixed evidence floors (fixtures, corpus size, precision, recall) to move between them. |
| [0014](0014-ci-images-detector.md) | Detect retired CI runner images | Add a `ci-images` detector that reports only image lifecycle risks the platform's authoritative documentation explicitly states. |
| [0015](0015-corpus-fetch-strategy.md) | Corpus fetch strategy: sparse checkout, concurrency 16 | Default the public-repository corpus to sparse checkout at concurrency 16, with cache invalidation keyed to detector input declarations. |

## Data and contracts

| # | Title | Decision |
| --- | --- | --- |
| [0004](0004-registry-v2.md) | Key lifecycle rules by PURL and VERS with provenance | Move every ecosystem to one `offing.registry.v2` format keyed by Package URL and VERS, with provenance on each rule. |
| [0005](0005-lifecycle-importers.md) | Draft lifecycle rules with importers; never write the registry | Let importers draft rules from one authoritative source; a person reviews every draft and changes `registry/` in a pull request. |
| [0006](0006-data-bundle.md) | Ship lifecycle data as a versioned, content-addressed bundle | Publish and consume lifecycle data as an `offing.data-bundle.v1` document separate from the code release. |
| [0010](0010-scheduled-end-of-life.md) | Record announced end-of-life dates that have not happened yet | Add a `scheduled-end-of-life` event for announced future ends, reported at low severity and promoted by a reviewed change after the date. |
| [0016](0016-cli-exit-codes.md) | CLI exit codes and error codes | Map failures to five fixed classes and exit codes: policy 1, usage 2, data 3, parse 4, internal 5. |
| [0017](0017-openeox-ingestion.md) | Do not read OpenEoX documents yet; keep CLE as the interchange target | Scans never read OpenEoX; do not ingest it until the Core Schema is final, and keep CLE as the standards interchange path. |
| [0020](0020-data-license.md) | License the lifecycle dataset under CC BY 4.0 | License `registry/` and first-party schemas in the data-only package under CC BY 4.0; keep the rest of the repository Apache-2.0. |
| [0022](0022-python-method-shape.md) | Match Python `TestCase` method calls with a `method` shape | Add a Python-only `method` symbol shape that matches a call by name on any receiver inside a class that directly subclasses `unittest.TestCase` or an alias of it. |
| [0028](0028-mcp-schema-inlining-fails-closed.md) | Publish a self-contained MCP output schema | Inline every cross-file `$ref` before the MCP server advertises a tool's schema and throw when one cannot resolve; same-document references stay, and sibling keywords win over the inlined target. |
| [0032](0032-split-mcp-tools-into-one-file-per-tool.md) | Split the MCP server into one file per tool | Give each MCP tool its own file under `src/mcp/tools/`, list them in a small registry, and prove with a `tools/list` snapshot that no advertised name, description, input schema, output schema, or annotation changed. |
| [0033](0033-required-source-access-date.md) | Require the retrieval date on every lifecycle source | Make `sources[].accessedAt` required in registry v2, backfill every bundled record, and let the importer runner stamp the run date so every finding shows when its source was read. |
| [0037](0037-recommend-version-from-bundled-data.md) | Recommend a release line from bundled data only | Add `recommend_version` and `offing recommend`: the newest unended line whose end date is at least 180 days away, from bundled rules only; `no-data` is explicit and never says "latest". |
| [0036](0036-corpus-schema-deno-elixir.md) | Add Deno and Elixir to the public validation schema | Add `deno` and `elixir` to the published corpus ecosystem enum when those detectors gain a public corpus. The change is additive. |
| [0034](0034-detector-limits-in-the-sdk.md) | Publish the detector limits in the SDK | Export `DETECTOR_LIMITS` and its `DetectorLimits` type so the file-size, depth, and vendored-directory bounds are part of the typed detector contract and conformance reads them from there. |

## Adoption

| # | Title | Decision |
| --- | --- | --- |
| [0018](0018-adoption-before-v1.md) | Ship v0.8.0 (adoption) and v0.9.0 (visibility) before v1 | Ship adoption then visibility before v1, with usage before attention, and record post-v1 ideas as unscheduled epics. |
| [0019](0019-privacy-first-adoption-metrics.md) | Privacy-first adoption metrics; opt-in telemetry only after v1 | Keep product telemetry off by default forever as the default; any opt-in comes only after a separate post-v1 decision. |
| [0021](0021-one-release-before-v1.md) | Ship adoption and visibility as one release, with one launch | Merge v0.9.0 into v0.8.0 and replace the three mini-launches with one launch that follows the release. |

## Quality

| # | Title | Decision |
| --- | --- | --- |
| [0023](0023-measurable-quality-criteria.md) | Define reference quality as measurable, enforced criteria | **Proposed.** Define eight quality criteria, each enforced by a failing check; zero new debt; existing debt allowlisted with an owner and an expiry of 2027-01-26. |
| [0025](0025-linter-and-formatter.md) | Choose the linter and formatter | Lint with ESLint and typescript-eslint (type-aware); format with Biome; do not use Biome for typed lint or Prettier for format. |
| [0026](0026-public-api-doc-comments.md) | Document the public API and enforce it with lint | Put a behavior-focused doc comment on every public declaration and enforce it with a local ESLint rule; no contract semantics change. |
| [0027](0027-fixtures-invisible-to-scanners.md) | Keep fixtures out of scanners' sight with neutral names | Commit manifest-shaped fixtures under a `.fixture` suffix and restore the real name in a temporary directory at test time; reject archives and dismissals; scanner config is a narrow fallback. |
| [0029](0029-cut-scanner-complexity-by-extracting-the-line-masker.md) | Cut scanner complexity by extracting the line masker and replacing branch chains with tables | Move the per-language literal scanner to `src/usage/masking.ts`, dispatch readers on tables, and prove parity differentially against a build of `main`; the SDK export surface is unchanged. |
| [0030](0030-corpus-selection-and-200-target.md) | Select corpus repositories independently of results; target 200 per ecosystem | Freeze eligibility and stack variety for all 23 buckets before scanning; retain poor results and publish honest shortfalls against 200 reviewed repositories per bucket. Accepted on maintainer merge of the PR for #2116. |

## Operations

| # | Title | Decision |
| --- | --- | --- |
| [0024](0024-ci-lanes.md) | CI lanes: pull request, merge queue, main, nightly, release | **Proposed.** Five CI lanes with wall-clock budgets; one required check (`ci-ok`); the merge queue is the proof that lands on `main`. |

## Policy

| # | Title | Decision |
| --- | --- | --- |
| [0001](0001-typescript-first.md) | TypeScript-first, local-first architecture | Build v0.1 in TypeScript on Node.js 22 or newer, sharing one language across CLI, engine, detectors, Action, and MCP, with no hosted backend. |
| [0003](0003-shared-plugin-packaging.md) | Generate host plugins from shared Offing assets | Keep Skill, runtime, rules, and migration behavior as canonical shared sources and generate Codex and Claude plugins from them so CI fails on drift. |
| [0031](0031-layered-architecture-and-allowed-imports.md) | Layered architecture and the allowed-imports table | **Proposed.** Seven layers (domain, SDK, core, detectors, services, reporters, adapters) with one allowed-imports table and runtime-access rules; each epic #1990 follow-up adds its rule to the checker. |
| [0035](0035-one-error-model-with-stable-codes.md) | One error model with stable reason codes | Every failure carries one reason code that fixes its class and exit code; the CLI error document gains `error.reason` and MCP failures carry the code in `_meta.offing`. |
