markharness

0009: Layer the CLI into Domain / Application / Infrastructure

Status

Accepted (Phases 1–5 were completed on 2026-08-18. Phase 5 introduced KnowledgeSource, direct Git-tree loading, reconstructible indexes, and Backfill throughput limits).

Context

The current implementation (a single Rust crate) is organized as a flat set of feature-named .rs files. As the codebase has grown, the following concrete problems have been confirmed.

A design proposal supplied by the user, “markharness Architecture Design Proposal” (dated 2026-08-18), presented a layering into Domain/Application/Infrastructure that addresses these. Review confirmed that its analysis of the current state matches the implementation (each item above was verified against the code), and that its Chapter 13 (“designs not adopted”) judgments align with CLAUDE.md’s operating rules (“reject compatibility-oriented design without assuming backward compatibility; always aim for the best product” and “do not count effort among a design’s downsides”). However, the proposal’s ChangeAnalyzer interface conflicted with the roadmap in decisions/0008 on one point, so this ADR corrects that and decides adoption.

Decision

1. Adopt five Domain Modules

KnowledgeWorkspace, TestcaseCompiler, ChangeAnalyzer, VerificationEngine, and BackfillCoordinator are the core of the Domain layer. The interface each Module exposes to callers, and the processing it hides internally, are governed by the design document.

2. Adopt a one-way dependency: CLI → Application → Domain → Infrastructure

Introduce a CommandOutcome type and a Presenter trait, and eliminate println!/eprintln!/std::process::exit from the Domain and Application layers. Human-readable output and JSON output are both generated from the same CommandOutcome.

3. Generalize ChangeAnalyzer’s version reference to CommitRef instead of a fixed MilestoneRef (a correction to the original proposal)

The original proposal specified ChangeAnalyzer::compute(from: MilestoneRef, to: MilestoneRef, options: ChangeOptions). However, decisions/0008 Stage 2 has already decided to “generalize the milestone-only UX into a common version range that adds PR base/head as first-class.” Fixing the type to MilestoneRef through Phase 4 would force a redesign of the core interface when Stage 2 starts, creating rework. This ADR therefore adopts the following instead.

enum CommitRef {
    Milestone(MilestoneId),  // a tag name; resolved to a commit internally via git tag resolution
    Commit(CommitId),        // an arbitrary commit (e.g. a PR's base/head SHA)
}

impl ChangeAnalyzer {
    fn compute(
        &self,
        from: CommitRef,
        to: CommitRef,
        options: ChangeOptions,
    ) -> Result<ChangeSet>;
}

The existing markharness changes compute and backfill run continue to use CommitRef::Milestone (behavior unchanged). The PR Verification Plan feature added in decisions/0008 Stage 2 can pass CommitRef::Commit to the same ChangeAnalyzer without any interface redesign.

4. Do not introduce a GitRepository trait yet; consolidate into git.rs first (a correction to the original proposal)

The original proposal’s Section 7.1 presented a complete GitRepository trait definition up front, which does not sit well with the proposal’s own Chapter 13 principle of “not abstracting a dependency that has only one implementation.” This ADR decides only as far as consolidating the direct git calls in changes.rs into git.rs, in Phase 1. Introducing a trait is deferred to the point where a concrete need arises — e.g. a fake implementation is needed for tests, or multiple Adapters become a requirement — and the trait’s shape is not fixed at this time.

5. Adopt a KnowledgeSource trait

Unlike item 4, two Adapters (WorkingTreeKnowledgeSource / GitTreeKnowledgeSource) are clearly needed from the start, so trait abstraction is warranted here. GitTreeKnowledgeSource replaces historical_testcases_by_feature’s git worktree add/remove with direct reads of the tree/blobs under a commit, reducing backfill’s scaling cost.

6. Adopt atomicity for generated-artifact updates

Make generate’s update of TestCases and the traceability index transactional across the whole directory (generate everything into a temp directory → verify → switch generated/ over). On mid-way failure, keep the existing generated artifacts.

7. Keep full generation as the canonical behavior; add incremental generation only as an optimization

Do not run on incremental generation alone from the start. Even after incremental generation is added, periodic full generation in CI remains the basis for verification (retaining the Chapter 13 judgment).

8. Adopt the staged migration plan

Phase Content
Phase 1 Replace compute_changes’s boolean parameters with ChangeOptions; consolidate direct git calls in changes.rs into git.rs. Pin existing behavior and the CLI contract with characterization tests. Directory layout unchanged.
Phase 2 Introduce CommandOutcome. Move std::process::exit from the CLI to the Presenter. Split human-readable and JSON Presenters. Extract Application Use Cases from generate, changes compute, and verify pending.
Phase 3 Add atomicity: generate TestCases and the traceability index into a temp area, then reflect into generated/ only on success.
Phase 4 Introduce KnowledgeSnapshot. Separate TestcaseCompiler from the filesystem. Separate the Data Loader from the Engine in Verification. Finalize ChangeAnalyzer on the CommitRef basis from Decision 3.
Phase 5 Introduce KnowledgeSource. Replace worktrees with GitTreeKnowledgeSource. Add reconstructible indexes for Feature/ChangeEvent/Execution. Add throughput limits to Backfill (--max-pairs, --time-budget, etc.).

See domain-application-infrastructure-layering-design.md for detailed interface definitions, the Mermaid diagram, code layout, and test strategy.

Consequences

Options considered but not adopted