# Capability manifest and auto-discovery contract

Offing publishes one deterministic capability manifest for humans, agents, and
integrations:

```bash
offing capabilities --json
```

The output uses the `offing.capabilities.v1` contract. Its JSON Schema is
[`schemas/capabilities.v1.schema.json`](../schemas/capabilities.v1.schema.json),
and a complete committed example is
[`examples/capabilities.json`](../examples/capabilities.json).

## What the manifest describes

- the Offing package and product version;
- supported, experimental, and unsupported capabilities;
- supported detector ecosystems and their maturity;
- CLI, MCP, Skill, plugin, and CI surfaces and their entry points;
- the shared capability identifiers available through each surface; and
- compatibility rules for consumers reading newer manifests.

## Detector maturity

Each ecosystem carries a `maturity` field next to its existing `status`:

| `maturity` | Shown in the docs as | What it promises users |
| --- | --- | --- |
| `experimental` | Experimental | The detector runs and is usable. Findings, rule ids, and unclassified output may change between releases without notice. |
| `beta` | Beta | Findings were measured against independent ground truth. Rule and finding ids are listed in the changelog when they change. |
| `stable` | Supported | The same promise as beta on a larger and more varied corpus, as part of the documented product contract. |

`maturity` is the detector's `level` in
[`benchmarks/thresholds.json`](../benchmarks/thresholds.json), which is the single
source of truth for [decision 0013](decisions/0013-detector-maturity.md). It is not
a second, independent judgment.

An ecosystem's `maturity` is the lowest level among the detectors listed in its
`detectorIds`. When several detectors serve one ecosystem, the ecosystem is only as
mature as its least mature detector. Today `nodejs` and `npm` both list the `npm`
detector, so both take its level. A contract test fails when the manifest disagrees
with `benchmarks/thresholds.json`.

The existing `status` field keeps its meaning and is not replaced: it says whether
the ecosystem is a supported product surface, while `maturity` records the evidence
level of the detectors behind it. A reader that ignores unknown fields can keep
using `status` exactly as before; `maturity` is an additive optional field in the
v1 manifest.

Capability identifiers describe behavior, not transport. For example,
`repository.scan` is the same capability when reached through the CLI, local MCP
server, Agent Skill, experimental host plugin, or CI. Integrations should compare
identifiers rather than infer behavior from command or tool names.

## Forward compatibility

Consumers of a v1 manifest must follow the compatibility object included in the
document:

1. Ignore fields they do not understand within schema major version 1.
2. Ignore unknown capability and surface identifiers rather than failing the whole
   manifest.
3. Treat any unlisted capability or ecosystem as unsupported. Absence never means
   experimental or supported.
4. Reject an unknown schema major version until the consumer explicitly supports
   it.
5. Use each entry's `status`; do not promote `experimental` or `unsupported`
   behavior based on a matching entry-point name.

New optional fields, identifiers, and surfaces using the defined statuses can be
added in a compatible v1 minor manifest update. Removing or redefining an existing
identifier, changing required field meaning or status vocabulary, or weakening the
rules above requires a new schema major version.

## JSON output schemas and policy for adding fields

Every command supporting `--json` (or `--format json`) produces output that strictly conforms
to its published JSON Schema in `schemas/*.v1.schema.json` (such as `scan.v1.schema.json`,
`check.v1.schema.json`, `finding.v1.schema.json`, `context.v1.schema.json`,
`doctor.v1.schema.json`, and `data-status.v1.schema.json`).

Because scripts, agents, and CI automations depend on exact JSON structures,
Offing locks all published schemas with strict contract tests:

1. **Strict property validation:** Output schemas enforce `additionalProperties: false`
   across all object definitions. CLI output must not contain properties undeclared in the schema.
2. **Deliberate schema updates:** Adding a field to any `--json` output is never done
   implicitly. It requires an explicit, reviewed update to the relevant JSON Schema under
   `schemas/`, defining the property's type, constraints, and intent.
3. **Additive changes in v1:** New optional fields can be introduced in the current `v1`
   major schema release without breaking backward-compatible consumers.
4. **Breaking changes require a schema bump:** Removing existing properties, renaming
   fields, or altering requiredness / types requires a new schema major version (such as `v2`).
5. **Contract test enforcement:** Snapshot tests in `tests/schema-snapshots.test.ts`
   validate real execution output for every command against its schema and assert that any
   undeclared property triggers schema validation failure.

## Stability and privacy

The command reads a packaged constant and performs no repository scan, network
request, telemetry, or configuration write. Identical Offing versions produce
byte-for-byte identical JSON output. The package's contract tests validate both the
CLI output and committed example against the public JSON Schema and ensure every
surface references registered capability identifiers.

## Exit codes and error codes

Every CLI failure belongs to one class. The class decides the exit code, and with
`--json` (or `--format json`) the same class is written to stdout as an
`offing.error.v1` document, so an agent can branch without parsing messages.

| Class (`error.code`) | Exit code | Meaning |
| --- | --- | --- |
| *(none)* | 0 | The command succeeded. |
| `policy` | 1 | A gate you configured failed: `check` found an affected dependency at or above `--fail-on`, `doctor` reported a failed check, or `--on-expired fail` met an expired suppression. |
| `usage` | 2 | The command line is wrong: unknown command, flag, or value, or a missing argument. |
| `data` | 3 | The input is well formed but the data is not there: a finding ID that was not found, a pinned data version that is not available, or a data update that was refused. |
| `parse` | 4 | An input could not be parsed: an invalid Package URL, an unreadable or unsupported baseline, or an `AGENTS.md` section that is damaged. |
| `internal` | 5 | Offing failed unexpectedly. This is a bug; please report it. |

```json
{
  "schemaVersion": "offing.error.v1",
  "error": {
    "code": "usage",
    "reason": "unknown-command",
    "exitCode": 2,
    "message": "Unknown command: frobnicate"
  }
}
```

Each failure also carries one stable reason code, written as `error.reason` in the error document.

| Reason (`error.reason`) | Class | Raised when |
| --- | --- | --- |
| `policy-failed` | `policy` | A configured gate failed. No error document is written. |
| `invalid-argument` | `usage` | A flag, value, or operand is wrong or missing. |
| `invalid-config` | `usage` | The repository configuration file cannot be read or is invalid. |
| `invalid-data-version` | `usage` | `--data-version` is not a data version. |
| `unknown-command` | `usage` | The command name is not one Offing has. |
| `scaffold-failed` | `usage` | `new-rule` or `new-detector` could not create its files: an invalid id, a taken name, an existing file, or no contributor checkout. |
| `finding-not-found` | `data` | A finding ID matches no finding in the scan. |
| `finding-not-observed` | `data` | A finding ID was not observed earlier in this MCP session. |
| `data-unavailable` | `data` | A pinned data version or bundle is not available or does not verify. |
| `data-update-failed` | `data` | `offing data update` could not finish. |
| `invalid-purl` | `parse` | A Package URL is invalid. |
| `invalid-agent-file` | `parse` | An `AGENTS.md` section is damaged. |
| `invalid-sbom` | `parse` | An SBOM is not valid CycloneDX or SPDX. |
| `invalid-input` | `parse` | Another input could not be parsed. |
| `scan-memory-exceeded` | `internal` | The scan reached its heap limit. |
| `internal` | `internal` | Offing failed unexpectedly. |

Reason codes are stable: one is never renamed or moved to another class. The source is `ERROR_CODES` in `src/offing-error.ts`.

The message is also written to stderr in every mode. A `policy` failure is a
verdict on a result that was already printed, so stdout stays that single result
document and no error document is added. The set of classes and their exit codes
is stable; new classes would be added with new numbers, never by renumbering.
