# Conformance coverage

Authoritative map of JSON Schemas in [`../schemas/`](../schemas/) to the conformance cases that exercise them. Every schema MUST have at least one case before spec v1.0.0 ships, missing case → missing release ([`../../context/spec.md`](../../context/spec.md) §Analyzers for AI agents editing spec/).

Hand-maintained. A CI check before spec release compares the schema inventory against this table and fails if any schema lacks a case.

## Coverage matrix

| # | Schema | Case(s) | Status | Notes |
|---|---|---|---|---|
| 1 | `node.schema.json` | `kernel-empty-boot` (indirect) | 🟢 covered | `scan-result-schema` runs over a corpus with populated `nodes`, and `scan-result.schema.json` `$ref`s this schema, so validating the whole document validates each row against it end-to-end. The case asserts the array is non-empty precisely so the transitive coverage cannot silently evaporate if the fixture changes. |
| 2 | `link.schema.json` | `points-root-fallback`, `markdown-link-rooted` | 🟢 covered | `points-root-fallback` and `markdown-link-rooted` cover the dual-base resolution values; `scan-result-schema` additionally validates every emitted `Link` row against this schema transitively (populated `links`, asserted non-empty). `invokes` + `mentions` kinds remain uncovered by value, which is a fixture gap rather than a schema one. |
| 3 | `issue.schema.json` | `reference-broken-existence` | 🟢 covered | `reference-broken-existence` asserts the populated `Issue` values; `scan-result-schema` shares that fixture and validates every emitted row against this schema transitively (populated `issues`, asserted non-empty). `reference-broken-ignored` covers the inverse contract: the operator's `ignored-references` match-list (one literal, one regex, one glob entry) suppresses both the issue and the confidence penalty, so genuinely broken links emit zero issues and exit 0. A `name-collision` fixture is still worth adding for a second analyzer's shape. |
| 4 | `scan-result.schema.json` | `kernel-empty-boot`, `orphan-markdown-fallback` | 🟢 covered | Zero-filled case via empty-boot. `orphan-markdown-fallback` (spec 0.18.0) asserts a populated `ScanResult` over a multi-Provider corpus where one node lands via the universal `core/markdown` fallback and another via vendor-specific claude classification, locks the orchestrator's path-dedup contract. Populated rename / orphan cases live under `provider:claude` (`basic-scan` / `rename-high` / `orphan-detection`). |
| 5 | `execution-record.schema.json` | `history-record-schema` | 🟢 covered | Closed by the `each` assertion: `sm history --json` emits an ARRAY, and `each: true` validates every element against the record schema (an empty array fails by contract, so a broken chain cannot pass vacuously). The case drives the full round trip (submit, claim, record) using `capture` to thread the claim-issued nonce. |
| 6 | `project-config.schema.json` | `project-config-schema` | 🟢 covered | `sm config list --json` emits the effective merged configuration and the case validates the whole document via `stdout-matches-schema`, so a silently added or renamed key fails rather than slipping past a field probe. The layered-merge VALUES (a partial `settings.json` overlaid by `settings.local.json`) remain worth a follow-up case; the shape contract itself is covered. |
| 7 | `plugins-registry.schema.json` | `plugin-manifest-schema` | 🟢 covered | Closed by `schemaPointer`. The ROOT still models `.skill-map/plugins.json`, an aggregate no implementation writes, so the case targets `#/$defs/PluginManifest`, which describes the `plugin.json` every plugin-bearing scope carries. Verified the pointer is load-bearing: the same manifest validated against the permissive root PASSES vacuously. |
| 8 | `job.schema.json` | `job-document-schema`, `jobs-list-omits-nonce`, `extension-mode-routing` | 🟢 covered | BOTH projections are asserted whole-document, against the `$defs` that matches each surface. `job-document-schema` validates `sm jobs submit --json` against `#/$defs/CredentialedJob` (the credentialed direction); `jobs-list-omits-nonce` validates every row of `sm jobs list --json` against `#/$defs/PublicJob`, which ASSERTS the credential's absence, so a leaking implementation fails. That second direction was unexpressible while the root schema required `nonce`: the read surfaces the spec mandates could not validate against it at all, so the security invariant of §Nonce exposure had no conformance case and a leak would have passed the suite. |
| 9 | `report-base.schema.json` | `record-report-schema-gate` | 🟢 covered | Closed directly rather than transitively: the fixture `report-invalid.json` differs from its valid sibling only in `safety`, a field report-base OWNS, so the exit-2 rejection is this schema being enforced. The record path is where it becomes normative. |
| 10 | `conformance-case.schema.json` | (runner load gate + bundled-case sweep) | 🟢 covered | Closed at the only place a self-referential schema can be: the runner MUST validate every case document against this schema before executing it (`case-invalid` on failure, normative in the README), and a unit sweep validates all bundled cases across every scope. A CASE can never assert it, since the documents live outside the provisioned scope and a case invoking the suite would recurse; the load gate is the point every case necessarily passes through, so nothing can run without this schema having held. |
| 11 | `frontmatter/base.schema.json` | `orphan-markdown-fallback` | 🟢 covered | Universal frontmatter shape, `name` + `description` only, `additionalProperties: true`. Per-kind schemas live with the Provider that emits them: vendor kinds (`skill` / `agent` / `command`) under `src/plugins/{claude,codex,agent-skills}/providers/<id>/schemas/`; the format-named generic `markdown` kind under `src/plugins/core/providers/core-markdown/schemas/` (spec 0.18.0, markdown is provider-agnostic). All extend this base via `$ref`-by-`$id`. `orphan-markdown-fallback` exercises base-only frontmatter end-to-end via the `ARCHITECTURE.md` fixture file (no kind-specific extras). |
| 13 | `enrichments/github.schema.json` | `github-enrichment-recorded` | 🟢 covered | The recorded-transport objection is answered by `setup.staticServe`: the runner serves the recorded API + raw responses over loopback, so the fetch path is REAL (the case redirects the extension's project-local-only base-URL settings at it) and the scope stays offline. Covered through the gate the schema governs, same doctrine as row 12: `sm enrich` validates the enricher's report against the action's report schema, which `allOf`-extends this canonical schema, BEFORE persisting; a non-conforming report records a `report-invalid` FAILED execution and no row. The case asserts the `completed` execution plus the report values (`verified: true`, `method: api-ref`, the recorded SHA) via `sm history --json`, whose `reportPath` carries the report as a JSON string, the only surface that re-emits it today. |
| 12 | `summaries/markdown.schema.json` | `record-report-schema-gate`, `history-record-schema` | 🟢 covered | Covered through the gate the schema actually governs: `sm record` validates the submitted report against the contract the job names, accepting the conforming fixture (in `history-record-schema`) and rejecting the one-field-off sibling with exit 2. Validating a hand-authored document against the schema would have proven the fixture conforms, not the implementation. |
| 15 | `tags/markdown.schema.json` | `record-tags-report` | 🟢 covered | A recorded tagger report validates against this schema AND its tags surface as a proposal rather than being written to the node, which is the half that matters: tags are human curation, so an implementation that silently persisted them would satisfy the schema while violating the storage rule. |
| 14 | `findings/report.schema.json` | `record-findings-report`, `record-findings-envelope-gate`, `findings-contract`, `findings-contract-kind` | 🟢 covered | Both sides now covered. `findings-contract` / `findings-contract-kind` assert the SUBMIT side (the rendered job embeds the finder report schema with its `$ref` to this envelope). `capture` closed the record side that previously lived only in implementation integration tests: a recorded report writes its rows through with `type` / `severity` / `confidence` intact, and a sibling differing only in an out-of-enum `severity` is rejected with exit 2 and writes nothing. |
| 17 | `extensions/base.schema.json` | `extension-kind-manifests` | 🟢 covered | The meta-case asked for here now exists in the form the runner can express: `extension-kind-manifests` ships one valid extension of EVERY kind, and loading is the observable proof each manifest passed its kind schema, which extends base via `allOf`. |
| 38 | `extensions/extension-manifest.schema.json` | `extension-manifest-enable-gate` | 🟢 covered | The fixture's only extension declares `stability: 'experimental'` and its module THROWS at top level, so an implementation that imports a ships-disabled extension surfaces `load-error`; reaching `enabled` with an empty `extensions` array is the proof the enable decision preceded the import. The case validates the `extension.json` document itself via `file-matches-schema` alongside the behaviour it exists for. |
| 18 | `extensions/provider.schema.json` | `plugin-missing-ui-rejected` | 🟢 covered | `plugin-missing-ui-rejected` covers the negative direction (a `kinds/<name>/kind.json` missing `ui` fails AJV); `extension-kind-manifests` now covers the positive one (a valid provider manifest plus its `kinds/` catalog loads and registers). Direct case for a missing `kinds/` directory is still pending. |
| 19 | `extensions/extractor.schema.json` | `extension-kind-manifests` | 🟢 covered | Positive direction covered by `extension-kind-manifests` (a valid extractor manifest loads). The `precondition-kind-unknown` doctor warning for an unknown qualified kind is still uncovered. |
| 20 | `extensions/analyzer.schema.json` | `score-phase-confidence` | 🟢 covered | `score-phase-confidence` exercises the `phase` enum end-to-end; `extension-kind-manifests` adds the positive manifest-validation direction (a valid deterministic analyzer loads). The `detect` / `aggregate` defaults and the `precondition` / `ui` blocks remain uncovered by value. |
| 21 | `extensions/action.schema.json` | `extension-kind-manifests` | 🟢 covered | Positive direction covered by `extension-kind-manifests` (a valid deterministic action, with its mandatory `report.schema.json` sibling, loads). The probabilistic branch (missing `prompt.md` surfaces `load-error`) is still uncovered. |
| 22 | `extensions/formatter.schema.json` | `extension-kind-manifests` | 🟢 covered | Covered by `extension-kind-manifests`: a valid formatter manifest loads and registers. |
| 23 | `history-stats.schema.json` | `history-stats-schema` | 🟢 covered | Closed with a plain whole-document assertion. The prior note deferred this on the per-element limitation governing `sm history --json`, which was the wrong diagnosis: the stats surface is a single OBJECT, not a list. The chain records a real execution so the totals are populated. |
| 24 | `extensions/hook.schema.json` | `extension-kind-manifests` | 🟢 covered | Positive direction covered by `extension-kind-manifests` (a hook declaring `triggers: ['scan.completed']` loads). The unknown-trigger rejection is still uncovered. |
| 36 | `extensions/provider-kind.schema.json` | `extension-kind-manifests` | 🟢 covered | Covered by `extension-kind-manifests`, which validates the fixture's `kinds/demo/kind.json` against this schema directly via `file-matches-schema` on top of loading the provider that consumes it. |
| 25 | `api/rest-envelope.schema.json` | `rest-envelope-schema` | 🟢 covered | Closed by the server-capable runner: `setup.serve` boots the implementation's server on an ephemeral port inside the scope, and `http-matches-schema` issues `GET /api/nodes` against the port resolved from `serve.json`, validating the 200 body against the envelope (the list variant with `items` + `counts` + `filters` + the three registries). Implementation-side coverage remains at `server/routes/__tests__/`. |
| 26 | `sidecar.schema.json` | `sidecar-end-to-end` | 🟢 covered | Co-located YAML sidecar (`<basename>.sm`) root shape: reserved blocks `for` / `annotations` / `settings` / `audit` plus opt-in plugin namespacing. Step 9.6.2 (2026-05-05) shipped the kernel reader; Step 9.6.3 (2026-05-05) formalised the `audit:` sub-shape populated by the built-in `bump` Action; Step 9.6.6 (2026-05-06) flips this row 🟢 with the end-to-end `sidecar-end-to-end` case (fixture `sidecar-end-to-end/`): a scan over a stale-`.sm` + orphan-`.sm` corpus produces a populated `Node.sidecar` overlay with `present: true` and `status: stale-*`, denormalises `annotations.version` into the node row, and emits the `annotation-orphan` issue from the built-in core analyzers (drift is icon-only since 2026-07-20: `annotation-stale` surfaces `sidecar.status` via card contributions, no issue). Structural sample (untouched) at `fixtures/sidecar-example/agent-example.sm`. |
| 27 | `annotations.schema.json` | `sidecar-end-to-end` | 🟢 covered | Curated catalog of 15 conventional skill-map annotation fields (versioning, supersession, provenance, lifecycle, taxonomy, display, docs). `additionalProperties: true` so users / plugins extend without coordination; the `unknown-field` Tier-1 analyzer shipped in Step 9.6.6 emits warnings on truly unrecognized keys. Step 9.6.2 (2026-05-05) wired the kernel reader; Step 9.6.6 (2026-05-06) flips this row 🟢 via `sidecar-end-to-end`, which asserts that an `annotations.version: 7` value round-trips through `state_scan_nodes.annotations_json` and surfaces in the node's `sidecar.annotations` overlay AND in the denormalised `Node.version` column. Structural sample at `fixtures/sidecar-example/agent-example.sm`. Catalog trimmed from 31 to 15 fields on 2026-05-07 after UX review; the dismissal surfaces `suppressions` (finding classes) and `issueSuppressions` (deterministic issue values, 2026-07-27) joined later, current catalog is the 10 fields the schema lists. |
| 28 | `bump-report.schema.json` | `bump-report-schema` | 🟢 covered | `sm bump <node> --json` validated in full. The case reaches the report only through BOTH consent gates (`core/node-bump` ships `defaultEnabled: false` and must be enabled; the `.sm` write needs `--yes`), so it pins the gates alongside the shape: an implementation emitting this report without them would still satisfy the schema alone. |
| 29 | `report-base-deterministic.schema.json` | `bump-report-schema` (transitive) | 🟢 covered | Covered transitively and non-vacuously: `bump-report.schema.json` extends this base via `allOf` + `$ref`, so validating a real bump report validates the base, and the case additionally asserts `$.ok`, the one field the base OWNS. The base is `additionalProperties: true`, so `required: ["ok"]` is essentially all it contributes, and that is what the case pins. |
| 30 | `view-slots.schema.json` | `view-action-button`, `view-contribution-payloads`, `view-slots-all` | 🟢 covered | Closed catalog of 14 view slots + the `IViewContribution` manifest declaration shape + per-slot payload schemas. `view-action-button` covers the positive load of the unified `inspector.header.badge` slot and the new `inspector.action.button` dispatch slot AND the negative path: an analyzer declaring the retired `inspector.header.badge.counter` slot (folded into `inspector.header.badge`) rejects with `invalid-manifest` while the good plugin and the markdown node survive (exit 0). `view-contribution-payloads` covers the renamed list-payload fields (breakdown `bars`, key-values `pairs`, link-list `links`) loading + scanning clean, plus two visible off-shape rejections, an AJV payload failure and an undeclared-ref spread copy, each surfacing an `extension.error` on stderr. `view-slots-all` declares a contribution to every one of the 14 catalog slots and asserts `sm plugins doctor` loads it clean (`ok`), locking that every slot id is a valid manifest declaration. |
| 31 | `input-types.schema.json` | `catalog-slots-input-types` | 🟢 covered | `sm plugins slots list --json` validated in full against the catalog schema. The catalog is closed by design, so an implementation that quietly adds or drops an entry is indistinguishable from one that does not until a plugin author picks the missing name; validating the emitted catalog is what stops that drift (the 12th member, `match-list`, is guarded the same way; its runtime matching semantics are exercised by `reference-broken-ignored`). |
| 32 | `enrich-report.schema.json` | `enrich-report-schema` | 🟢 covered | `sm enrich <node> --json` validated in full. Exercises the ZERO case deliberately (no network-declaring Action installed): a conforming envelope is required even when nothing was enriched, not only on the happy path. |
| 33 | `plugins-doctor.schema.json` | `plugins-doctor-schema` | 🟢 covered | `sm plugins doctor --json` validated in full via `stdout-matches-schema`. The case runs against a corpus with NO drop-in plugins, so the built-in set alone must already produce a conforming report and a green result never depends on a fixture plugin happening to load. |
| 34 | `conformance-result.schema.json` | `conformance-result-schema` | 🟢 covered | Closed by the new `sm conformance run --case <id>` selector. The row was deferred because a case invoking the suite would run the whole suite including itself; filtering the inner run to ONE other case (`kernel-empty-boot`, fixture-free and state-free so it cannot fail for unrelated reasons) breaks the recursion by construction. The case also pins the envelope internally: `totals.scopes` must agree with the length of the `scopes` array it summarises. |
| 35 | `user-settings.schema.json` | (indirect via `no-global-scope`) | ⚪ exempt | PERMANENT exemption, decided rather than postponed. The file lives at `~/.skill-map/settings.json`, the single documented `$HOME` exception, so direct validation means asserting a path OUTSIDE the provisioned scope, which the runner's containment guard refuses, correctly. A home-scope primitive (the runner provisioning an isolated fake `HOME`) IS technically feasible and was weighed: it was rejected because a language-neutral suite would impose that machinery on EVERY conforming implementation for one schema, and because `no-global-scope` already asserts the stronger property, that skill-map never reads `$HOME` by default. This file stays implementation-tested. Revisit only if a home-scope primitive earns its place for other reasons. |
| 36 | `serve-info.schema.json` | `serve-info-schema` | 🟢 covered | Closed by `setup.serve`'s ordering guarantee: the server stays up through the main `invoke` AND assertion evaluation, so `file-matches-schema` observes `.skill-map/serve.json` in the only window it exists (written on boot, removed on clean shutdown). The recorded `port` is the RESOLVED ephemeral one, so the shape validates end-to-end against a real listener. |
| 39 | `skill-actions/report.schema.json` | `skill-actions-catalog` | 🟢 covered | Canonical report contract for skill actions (`skill-actions.md`): every `skill:`-prefixed job records against this ONE schema, resolved as a constant at record time. The case validates the documented example report against it directly (`file-matches-schema`, pinning the `allOf` chain to `report-base` and the required `summary`), and the same case's `http-matches-schema` on `GET /api/nodes/:pathB64/prob-extensions` covers the discovery side (a boot-seeded catalog skill surfacing through the envelope's `skills` bucket / `SkillActionEntry`). The ENFORCEMENT direction (the record gate rejecting a summary-less report with exit 2) is implementation-tested for now: v1 submits are BFF-only (`skill:` is reserved-but-refused on the CLI) and the runner has no HTTP body primitive, so a chain case cannot create a skill job; lifting the CLI deferral is the moment to add the `record-report-schema-gate`-shaped sibling. |
| 37 | `signal.schema.json` | `extractor-emits-signal`, `extractor-collision-detection` | 🟢 covered | Intermediate Representation (IR) emitted by extractors via `ctx.emitSignal()`; the kernel resolver phase consumes Signals and materialises Links. Opt-in: the existing `ctx.emitLink()` path coexists. Phase 2.A wired the resolver end-to-end (filter -> rank -> overlap -> materialise + annotate); Phase 2.B + 2.C migrated all six link-emitter extractors (`claude/at-directive`, `claude/slash-command`, `core/markdown-link`, `core/annotations`, `core/mcp-tools`, `core/external-url-counter`); Phase 2.D added the `core/extractor-collision` analyzer + the two cases. The cases cover (a) `extractor-emits-signal`, a markdown body with one `[text](path)` link materialises one Link via the Signal IR path; (b) `extractor-collision-detection`, a body with both `[label](./api.md)` AND `@./api.md` at overlapping byte ranges triggers a cross-extractor collision, the resolver materialises ONE Link (markdown-link wins on confidence) and the loser's `resolution.rejectedBy` reaches the `core/extractor-collision` analyzer which emits a `warn` issue naming WHO won, WHO lost, and WHY. |

> **Note on Provider-owned schemas.** Per-kind frontmatter schemas (`agent`, `command`, `skill` and `mcp` for the built-in Claude Provider; other Providers MAY declare different kinds) live with the Provider that emits them, for the built-in Claude Provider under `src/plugins/claude/providers/claude/schemas/`. Those schemas are NOT counted in the coverage matrix above; they belong to the Provider's own conformance suite at `src/plugins/claude/providers/claude/conformance/coverage.md`. The same split applies to the cases exercising Provider-specific kinds (`basic-scan`, `rename-high`, `orphan-detection`): they live in the Provider's `cases/` directory.

| 40 | `map-view.schema.json` | (none yet) | 🟠 deferred | Committed map-view documents (`.skill-map/views/<slug>.json`, `map-views.md`) are written and validated by the BFF only (`PUT /api/map-views/:slug`); no CLI verb emits or consumes them, and the runner has no HTTP write primitive, so the enforcement direction (an invalid body rejected, an invalid on-disk file skipped) cannot be expressed as a case today. What would unblock it: an HTTP body/json-path assertion primitive (then a case plants one valid and one invalid view file under the fixture's `.skill-map/views/` and asserts via `GET /api/map-views` that the valid one lists in `views` and the invalid one lands in `skipped`, exercising the schema gate in both directions), or a future `sm views` CLI verb. Until then the guard lives implementation-side: the route spec AJV-validates the server's written bytes against THIS schema file, so inline-validator drift fails there. |
| 41 | `session-recording.schema.json` | (none yet) | 🟠 deferred | Session-journal files (`.skill-map/sessions/*.json`, `provider-activity.md` §Session journal) are produced only by a live serve boot ingesting real activity events: a case would need `setup.serve` PLUS a token-authenticated `POST /api/activity` write primitive plus a debounce wait before `file-matches-schema` could observe a journal file, and the runner has no HTTP write primitive (same blocker as row 40). What would unblock it: an HTTP POST primitive with header support (then a case boots the server over a scanned fixture, posts one synthetic provider event with the `serve.json` token, waits out the flush, and validates the produced session file against THIS schema). Until then the guard lives implementation-side: the journal writer's unit suite validates its written bytes against this schema file, and the scan-side reader AJV-validates every file against it (off-shape files skipped), so drift fails there. |
Status legend: 🟢 covered (at least one case asserts the schema end-to-end) · 🟡 partial (covered only indirectly or via a sub-shape) · 🟠 deferred to v1.1 (the row states WHAT would unblock it, not merely that it is postponed) · ⚪ exempt (a DECIDED permanent exclusion: the row states why conformance coverage is the wrong tool for it, and what was weighed and rejected) · 🔴 missing.

## Non-schema normative artifacts

These have their own conformance cases even though they are not JSON Schemas.

| # | Artifact | Case | Status | Notes |
|---|---|---|---|---|
| A | Preamble verbatim text | `preamble-bitwise-match` | 🟢 covered | Submits a `ai-summarizer-action` job over a scanned markdown node (`setup.priorInvokes`), then `sm jobs preview --last` MUST print content containing `fixtures/preamble-v2.txt` byte-for-byte (`stdout-contains-verbatim`). |
| B | Kernel empty-boot invariant | `kernel-empty-boot` | 🟢 covered | All extensions disabled → empty ScanResult. |
| C | Atomic-claim race safety | `claim-race-atomicity` | 🟢 covered | The invariant is only observable as a race, and `invoke.parallel` exists for exactly this: two IDENTICAL `sm jobs claim --json` processes started before either is awaited, over one queued job. `parallel-exit-codes [0, 1]` is the atomicity proof (two zeros would be a double handout, two ones a lost job), and `parallel-json-path-count` pins exactly one `$.nonce` handover across the pair. |
| D | Duplicate detection | `duplicate-submit-rejected` | 🟢 covered | A second submit over the same `(extensionId, extensionVersion, nodeId, contentHash)` while the first is queued is refused with exit 3 and names the covering job. No staged-failure primitive was needed after all: the rejected submit IS the main invoke, so its non-zero exit is asserted directly rather than staged. |
| E | `--force` bypass | `force-does-not-duplicate-live-job` | 🟢 covered | Uses `expectExit: 3` to stage the forced submit as a REFUSAL, then asserts the queue still holds exactly one job. The row reads as `--force` behaving like a plain submit, which is the contract: the flag skips the pre-check, but the unique partial index over `queued`/`running` is the hard invariant, so forcing past a live job must fail rather than enqueue a twin. |
| F | Nonce mismatch | `record-nonce-mismatch` | 🟢 covered | Closed by `capture`: the case binds the REAL job id from `sm jobs claim --json` and supplies a wrong nonce, so exit 4 can only come from the credential check. Supplying a bogus id instead would have exited 5 and proven something else. |
| G | Reap | `ttl-reap-abandoned` | 🟢 covered | Closed by `sleepAfterMs`: submit with `--ttl 1`, claim, sleep 3000ms, then a second claim whose ride-along reap fails the expired job (`abandoned`) and finds the queue empty (exit 1). The 3x-TTL sleep bounds the timing risk; the old objection that a sleep would make the suite timing-dependent is answered by proportionality, 3000ms against a 1000ms TTL. Asserted through the EFFECT (`sm jobs list --json` shows the abandoned row) rather than any message, since the claim-side reap is silent by contract. |
| H | `run.*` event envelope for Skill agent | `record-run-envelope` | 🟢 covered | The prior deferral mis-diagnosed the surface as websocket-blocked. `run.*` events deliberately never travel the server push leg (`job-events.md` names `sm record --json`'s synthetic envelope as the record surface), so the contract was a STDOUT ndjson one all along; what was missing was only the ndjson-aware `ndjson-line` assertion, which now pins the five-event sequence line by line. No server involved. |
| I | Rename heuristic | `rename-high`, `orphan-detection` (Provider-owned) | 🟢 covered | High-confidence rename emits no issue and the new path is the sole node. Orphan branch emits exactly one `orphan` issue (severity `info`) when a deleted node has no replacement. Cases live with the Claude Provider (they reach a Provider's `kinds` catalog by construction); see [`src/plugins/claude/providers/claude/conformance/`](../../src/plugins/claude/providers/claude/conformance/). Medium / ambiguous branches are exercised by `src/test/rename-heuristic.test.ts` until the conformance schema grows richer assertions. |
| L | Elapsed-time reporting | `elapsed-time-reporting` | 🟢 covered | `sm enrich --json` exercises both halves: the `done in <formatted>` stderr line and the top-level `elapsedMs` in the document. The prior note said "blocked by Step 4"; Step 4 shipped long ago, so the block was stale rather than real. |
| M | No global / user scope (no `-g/--global` flag) | `no-global-scope` | 🟢 covered | Skill-map operates exclusively on the project scope (see `cli-contract.md` §Scope is always project-local). Implementations MUST reject `-g/--global` on every verb as an unknown option (exit `2`). The case invokes `sm scan -g --json` and asserts exit code `2` plus a stderr line matching `/(?i)unknown option/`. The matching implementation-side coverage lives at `src/test/global-flag-removed.test.ts` (regression guard) and `src/test/no-implicit-home-reads.test.ts` (no implicit `$HOME` reads from any verb on the default path). |

## Release gates

- **spec v0.x**: partial coverage acceptable. Every case added as the reference impl lands the verb that makes it runnable.
- **spec v1.0.0 release**: all rows above MUST be 🟢 covered, explicitly 🟠 deferred to v1.1 with a linked issue, or ⚪ exempt with the reasoning recorded in the row. There are no deferred rows: the single non-green row (`user-settings.schema.json`) is exempt by decision, not postponed, so the gate carries no open debt into 1.0.0.
- **CI check**: [`spec/scripts/check-coverage.js`](../scripts/check-coverage.js) compares `spec/schemas/**/*.schema.json` against the matrix above on every PR. A schema without a row here, or a row pointing at a missing schema, fails CI (exit 1 with a `::error::` annotation). Wired into the spec workspace's `spec:check` script (invoked by `npm run validate` from the repo root, picked up by CI).

## Stability

The **coverage matrix gating policy** (every shipped schema MUST have a row; every row MUST be 🟢 covered, 🟠 deferred-to-v1.1, or ⚪ exempt-by-decision before the spec v1.0.0 tag) is stable as of spec v1.0.0. Relaxing the v1.0 release gate (e.g. allowing 🔴 missing rows to ship) is a major bump. Tightening it further (e.g. forbidding 🟠 deferrals) is a minor bump.

The **assertion-type vocabulary** (`exit-code`, `json-path`, `file-exists`, `file-contains-verbatim`, `stdout-contains-verbatim`, `file-matches-schema`, `stdout-matches-schema`, `http-matches-schema`, `ndjson-line`, `stderr-matches`, `parallel-exit-codes`, `parallel-json-path-count`) is stable as of spec v1.0.0. Adding a new assertion type is a minor bump; renaming or removing one is a major bump. This list previously omitted `stdout-contains-verbatim`, which had shipped in both the schema and the runner, so the enumeration is authoritative only as of the correction that added `stdout-matches-schema`.

`file-matches-schema` and `stdout-matches-schema` are the pair that make a 🟢 row mean what it says. A row can technically be closed with `json-path` probes alone, but those assert the fields whoever wrote the case remembered, not the contract; validating the payload against the schema is what turns "these keys looked right" into "this output conforms". Prefer the schema assertion for any row whose subject IS a schema, and reach for `json-path` to pin specific VALUES the schema cannot express (a count, a resolved path, an enum choice).

The **per-row schema and case set** above is mutable through every spec release: rows are added when a new schema or normative artifact lands, marked deferred when the runtime that exercises them ships in a later step, and flipped to 🟢 covered when the case file is committed. Adding rows is non-breaking; deleting a row MUST coincide with deleting the corresponding schema (itself a major bump).
