markharness

0032: Introduce a CLI read-model seam

Status

Accepted (decided 2026-09-16).

Background

markharness v2 leaves Change Impact and Release Coverage views/dashboards to a separate repository, markharness-view (working name). To support that split, docs/en/design/cli-read-model-design.md (Draft) proposed an external read contract (a “read model”) for the CLI, assuming a new record_kind/schema_version envelope and new CommandOutcome variants.

A grilling session that inspected the actual implementation (src/) found this premise wrong. Two independent output paths already exist.

  1. The CommandOutcome/Presenter path (src/presentation.rs): represents the result of three write commands — canonical_imported, generated, and changes_computed (the result of markharness changes compute, which writes to .markharness/changes/). JsonPresenter has emitted the flat shape {"schema_version": 1, "outcome": "<tag>", ...fields} since v0.6.1.
  2. The per-command-module path (src/impact.rs, src/coverage.rs): the read-only commands impact and coverage never go through CommandOutcome/Presenter at all. Each module owns its own struct (ChangeImpact, ReleaseCoverage) that already carries schema_version and record_kind (values "change_impact" and "release_coverage" respectively), serialized directly from cli.rs via serde_json::to_string_pretty. --format currently has only one value, Json; human-readable output is not implemented.

In other words, what the draft set out to design — a read model carrying record_kind/schema_version, produced in the Application layer and handed to the JSON Presenter — is already implemented for impact/coverage. And record_kind itself is not a new name the draft invented; it’s the existing convention markharness-v2-design.md §9.1 and 0025 already established for persistent records under .markharness/, and impact/coverage’s CLI output already follows it.

The only genuinely new thing needed is a traceability command (not yet implemented) that follows this same existing pattern.

There is a further wrinkle: a file named src/traceability.rs already exists, implementing TraceabilityIndex/build_index/serialize_index. This is a generation artifact: generate builds it and writes it to .markharness/generated/traceability-index.json (application.rs), and verify uses it to check regeneration is deterministic (verify.rs). It’s a flat, per-TestCase list (case_id, requirement_ids, feature, behavior, scenario, axis), carries no record_kind/schema_version, and is not something markharness-view reads. The new TraceabilityReadModel (Node arrays per entity type plus Relations, computed on demand for a given --at and returned over stdout as a read-only query result) differs from it in every respect that matters, including whether it’s a persisted artifact or a query result. Reusing the same file name for two different responsibilities would leave readers unsure which module a reference to “traceability” means, so this ADR resolves the naming collision.

Decision

1. traceability follows the same output pattern as impact/coverage

The new traceability command does not go through CommandOutcome/Presenter. Like impact.rs/coverage.rs, it gets its own module defining a TraceabilityReadModel struct with schema_version: u32 and record_kind: &'static str (value "traceability"), serialized directly from cli.rs via serde_json.

No new CommandOutcome variants (ImpactComputed, CoverageComputed, TraceabilityComputed) are added. impact and coverage never used CommandOutcome to begin with, and traceability matches that.

2. Rename the existing src/traceability.rs to src/traceability_index.rs; reuse src/traceability.rs for the new CLI command

TraceabilityIndex/build_index/serialize_index (the generate-owned artifact) move to src/traceability_index.rs. Callers (application.rs, verify.rs) update their use paths accordingly. The type names, function names, and the artifact’s on-disk name (.markharness/generated/traceability-index.json) are unchanged — the artifact’s file name is an existing external detail independent of the Rust module name, and there’s no reason to touch it.

The now-free src/traceability.rs becomes the module for the new traceability CLI command (TraceabilityReadModel). This keeps the “module name matches its command” pattern impact.rs/impact and coverage.rs/coverage already establish, extended to traceability.

This rename is a breaking change to the public Rust API path markharness::traceability::TraceabilityIndex (src/lib.rs makes every module pub, including pub mod traceability;), which becomes markharness::traceability_index::TraceabilityIndex. This crate’s pub mod set exists so the integration tests under tests/ can reach crate internals; it is not offered or guaranteed as a stable API for external library consumers (Cargo.toml’s description reads “Git-native test knowledge management CLI” — use as a library is not a design goal). No pub use re-export is added to keep the old path working. CLAUDE.md’s “no backward-compatibility designs” policy applies here too, to the Rust public API.

3. impact and coverage’s output contracts are unchanged

ChangeImpact and ReleaseCoverage, record_kind/schema_version included, are already in reasonable shape as an external contract. The draft’s proposed ChangeImpactReadModel/ReleaseCoverageReadModel are reinterpreted as referring to the existing ChangeImpact/ReleaseCoverage. No breaking change to their existing fields is made within this ADR’s scope.

4. The CommandOutcome/Presenter path (the outcome field) is out of scope for the read models

canonical_imported, generated, and changes_computed are results of write commands, unrelated to what markharness-view reads (traceability/impact/coverage). Unifying their outcome field to record_kind has no concrete motivation and is out of this ADR’s scope (YAGNI). If a concrete need arises later to tidy up write commands’ output as an external contract, that gets its own ADR.

5. This ADR’s scope is limited to “add traceability following the existing read-command pattern”

TraceabilityReadModel’s detailed field layout (per-type Node arrays, how Relations are represented, etc.) is not decided by this ADR; it remains the responsibility of docs/en/design/cli-read-model-design.md (the design doc).

6. This ADR records a design decision; implementation is separate

The src/traceability.rs rename, its new implementation, and wiring traceability into cli.rs are out of scope for this ADR. They are a separate implementation task, done with TDD (Red-Green-Refactor).

Consequences

Not yet done (deferred to the implementation phase): the following existing, git-tracked documents still reference src/traceability.rs as of this ADR, deliberately left unchanged. The rename in decision 2 has not landed in code yet — src/traceability.rs still holds TraceabilityIndex today — so rewriting them to say src/traceability_index.rs now would describe a file that does not yet exist. They are updated in the same commit that lands the actual Rust rename (decision 6).

Options considered and rejected