Status: Accepted (Phases 1–5 implemented. Detailed design for the direction decided in decisions/0009)
Related documents: decisions/0009, decisions/0008, git-native-model-for-test-knowledge-management.md
Intended audience: implementers of markharness (to be referenced when starting Phase 1)
Positioning: This document, building on markharness’s existing documentation and current Rust implementation, organizes an architecture that supports future feature additions, maintainability, testability, and applicability to large repositories. It does not introduce a web server, a resident process, a canonical database, or microservices, preserving the current nature of the tool: a Git repository as the canonical persistence layer, YAML/JSON as the exchange format, and the CLI plus CI as the user-facing interface. Relative to the original proposal supplied by the user (dated 2026-08-18), this document reflects the two corrections decided in decisions/0009: generalizing ChangeAnalyzer to CommitRef, and deferring introduction of the GitRepository trait.
The core of the proposal is to keep the current Git-native single-CLI nature while organizing the following processing into a consistent pipeline.
Test Knowledge
-> deterministic TestCase generation
-> ChangeEvent derivation between milestones
-> identification of impacted TestCases
-> reconciliation against execution evidence
-> derivation of pending / stale status
knowledge/, axes/, generated/, executions/, changes/ under Git.feature.yml’s id and the tree SHA of the whole Feature directory, not by path.Each Module keeps the interface callers must learn small, and hides complex implementation behind it.
flowchart TB
CLI["CLI / JSON output"]
APP["Application Use Cases"]
subgraph DOMAIN["Domain Modules"]
KW["KnowledgeWorkspace"]
TC["TestcaseCompiler"]
CA["ChangeAnalyzer"]
VE["VerificationEngine"]
BF["BackfillCoordinator"]
end
subgraph INFRA["Infrastructure"]
GIT["Git Adapter (git.rs)"]
KS["KnowledgeSource"]
FS["WorkspaceStore"]
SCHEMA["SchemaValidator"]
CACHE["Derived Index / Cache"]
end
subgraph DATA["Git-managed data"]
KNOW["knowledge / axes"]
GEN["generated"]
EXEC["executions"]
CHANGE["changes"]
NOTES["git notes"]
end
CLI --> APP
APP --> KW
APP --> TC
APP --> CA
APP --> VE
APP --> BF
KW --> FS
KW --> SCHEMA
KW --> KS
TC --> KW
CA --> GIT
CA --> KS
CA --> TC
VE --> GIT
VE --> FS
BF --> CA
BF --> GIT
KS --> GIT
FS --> KNOW
FS --> GEN
FS --> EXEC
FS --> CHANGE
GIT --> NOTES
CACHE -.reconstructible.-> KNOW
Dependencies flow in one direction as a rule: CLI to Application, Application to Domain, and Domain to the minimum necessary Infrastructure seam.
Reads knowledge/ and axes/ and provides a normalized Knowledge Snapshot.
impl KnowledgeWorkspace {
fn load(root: &Path) -> Result<Self>;
fn validate(&self) -> ValidationReport;
fn snapshot(&self) -> &KnowledgeSnapshot;
fn reconcile(&mut self, intent: &IntentDocument) -> Result<ReconcileOutcome>;
}
The following processing is hidden internally.
forked_from referencesToday, src/generate.rs and src/validate.rs each independently walk knowledge/ via fs::read_dir, duplicating traversal logic. Introducing KnowledgeWorkspace lets generation, validation, and index building share the same Snapshot within one command, eliminating this duplication.
Deterministically generates TestCases and the traceability index from a Knowledge Snapshot.
fn compile(snapshot: &KnowledgeSnapshot) -> Result<GeneratedArtifacts>;
GeneratedArtifacts includes:
traceability-index.jsonThe Compiler does not write files. The Application Use Case passes the result to the WorkspaceStore.
Invariants:
generate and verify must always use the same Compiler.
The core Module that compares Feature versions between two versions and derives ChangeEvents and impacted TestCases.
Per decisions/0009 Decision 3, version references are expressed as CommitRef rather than a MilestoneRef fixed to milestone tags.
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>;
}
struct ChangeOptions {
cache: CachePolicy,
impact_source: ImpactSource,
}
enum ImpactSource {
HistoricalTree,
CurrentWorkingTree,
}
Processing pipeline:
CommitRef to a commit (Milestone goes through tag resolution; Commit is used as-is).to’s Knowledge.true_divergences within the interval as needed.changes compute and backfill run use CommitRef::Milestone with the same ChangeAnalyzer. The PR Verification Plan feature added in decisions/0008 Stage 2 can reuse the same ChangeAnalyzer by passing CommitRef::Commit, without any interface redesign.
Derives re-verification status from ChangeEvents, TestCase correspondence, and execution evidence.
impl VerificationEngine {
fn trace(&self, input: TraceInput) -> TraceReport;
fn pending(&self, input: VerificationInput) -> PendingReport;
}
Status is expressed as a type, not a string.
enum VerificationStatus {
Current,
Pending,
Stale,
Unknown,
}
VerificationEngine does not read files or Git directly; it performs a pure judgment over already-loaded input. The Application layer collects ChangeEvent, Execution, and Feature version and passes them in. Today, src/verify.rs’s trace/pending functions call fs::read_to_string directly, so this separation does not yet exist.
Unknown is used when the basis for judgment is insufficient, such as an old-format execution record that lacks verified_feature_tree_shas.
Selects unprocessed milestone pairs, calls ChangeAnalyzer, and records progress.
fn run_once(&self, policy: BackfillPolicy) -> Result<BackfillSummary>;
Scope of responsibility:
CommitRef::Milestone)Not run as a resident worker; kept as a run-once design that CI or a scheduler can invoke repeatedly.
Holds Use Cases corresponding to CLI subcommands.
application/
init_project.rs
validate_knowledge.rs
apply_knowledge.rs
generate_testcases.rs
verify_generated.rs
compute_changes.rs
record_execution.rs
verify_pending.rs
run_backfill.rs
The Application layer’s responsibility is limited to:
CommandOutcomeIt does not handle exit codes, stdout, or stderr directly.
enum CommandOutcome {
Generated(GenerateSummary),
Validation(ValidationReport),
Changes(ChangeSummary),
Verification(PendingReport),
}
The CLI is responsible only for:
CommandOutcome to the PresenterHuman-readable output and JSON output are generated from the same result type.
trait Presenter {
fn present(&self, outcome: &CommandOutcome) -> PresentedResult;
}
struct PresentedResult {
stdout: String,
stderr: String,
exit_code: i32,
}
This eliminates println!, eprintln!, and std::process::exit from the Domain and Application layers. Today, src/cli.rs (2248 lines) contains 32 process::exit calls and 92 println!/eprintln! calls, so this separation does not yet exist.
Because Git is essential to markharness’s domain, it is not abstracted behind a generic Repository<T>. First, consolidate the direct git process calls currently scattered in src/changes.rs (five Command::new("git") call sites) into git.rs.
Trait abstraction is not in scope for this round (decisions/0009 Decision 4). While there is only one implementation (the git process Adapter), keep it as a plain function group in git.rs:
// git.rs — sketch of the consolidated function group (not a trait)
fn resolve_commit_ref(root: &Path, git_ref: &CommitRef) -> Result<CommitId>;
fn feature_trees(root: &Path, commit: &CommitId) -> Result<Vec<FeatureTree>>;
fn milestones(root: &Path) -> Result<Vec<Milestone>>;
fn merges_between(root: &Path, from: &CommitId, to: &CommitId) -> Result<Vec<MergeInfo>>;
fn read_note(root: &Path, key: &NoteKey) -> Result<Option<String>>;
fn write_note(root: &Path, key: &NoteKey, value: &str) -> Result<()>;
Trait abstraction (e.g. a GitRepository trait) is decided again once a concrete need arises — a fake implementation needed for tests, or multiple Adapters (e.g. other VCS support) becoming a requirement. Testing continues to favor integration tests that create a small real Git repository in a temp area.
For large-repository support, make the Knowledge source swappable via the following seam. Unlike 7.1, two concrete Adapters are needed from the start, so this one is trait-abstracted.
trait KnowledgeSource {
fn list(&self, prefix: &RepoPath) -> Result<Vec<KnowledgeEntry>>;
fn read(&self, path: &RepoPath) -> Result<Vec<u8>>;
}
Two Adapters are anticipated:
WorkingTreeKnowledgeSourceGitTreeKnowledgeSourceThis lets both the current working tree and a past commit’s Git tree feed the same Parser and Compiler. Today, historical_testcases_by_feature (src/changes.rs) runs git worktree add → generate_testcases → git worktree remove for every milestone; introducing GitTreeKnowledgeSource removes the need for this temporary worktree.
Keep the existing fs_safety, and consolidate the following:
For generate, add transactionality across the whole directory.
1. Generate all TestCases into a temp directory
2. Generate the traceability index
3. Confirm all output succeeded
4. Switch over generated/testcases
5. Switch over the traceability index
On mid-way failure, keep the existing generated artifacts.
.markharness-cache/ is not canonical data; it is a deletable, reconstructible derivative. This policy is already implemented in src/id_cache.rs today, and its cache key matches the following formula.
hash(
knowledge_tree_sha
+ canonicalization_rule_version
+ id_index_schema_version
+ tool_version
)
Under the same policy, the following indexes can be added in the future.
.markharness-cache/
feature-versions/ # existing (id_cache.rs)
testcase-by-feature/ # new
changeevent-by-feature/ # new
execution-by-case/ # new
Even if SQLite is used, it is not made canonical; it is limited to reconstructible local index use.
src/
main.rs
cli/
mod.rs
args.rs
presenter.rs
application/
mod.rs
commands/
domain/
knowledge/
mod.rs
model.rs
validation.rs
generation/
mod.rs
compiler.rs
artifact.rs
change/
mod.rs
analyzer.rs
model.rs # CommitRef, ChangeOptions, etc.
verification/
mod.rs
engine.rs
model.rs
backfill/
mod.rs
coordinator.rs
infrastructure/
git/
mod.rs # consolidated git calls (not a trait)
knowledge_source/
mod.rs
working_tree.rs
git_tree.rs
workspace/
mod.rs
yaml.rs
atomic_write.rs
schema/
mod.rs
cache/
mod.rs
safety/
paths.rs
File splitting is not a goal in itself. Do not create an excessive number of files holding only small types or functions; split at the granularity where a Module’s interface and responsibility become clear. Reorganize into this layout during Phase 4, as needed.
The current implementation already satisfies the following.
generate, changes, verify, backfill, gitgenerate and verifycompute_changes from backfillfs_safetyid_cache.rs, Section 7.4)This design is therefore not a full reimplementation, but a structural reorganization that preserves the current implementation’s strengths.
| Aspect | Current | Proposed |
|---|---|---|
| Overall | Single crate | Single crate maintained |
| Module layout | Flat, feature-named .rs files |
Domain / Application / Infrastructure |
| CLI | Handles parsing, execution, display, exit all at once | Limited to parsing and Presenter selection |
| Knowledge | Each feature walks as needed | Shares a normalized Snapshot |
| TestCase generation | A function that reads and generates given a path | A Compiler that takes a Snapshot |
| Change computation | A function taking a path and several bools, milestone-only | An Analyzer taking CommitRef and a config type (milestone and PR shared) |
| Verification | I/O and status judgment together | Data Loader separated from a pure Engine |
| Git | git.rs plus some direct calls |
Git calls consolidated into git.rs (no trait) |
| Generated-artifact updates | Safe per file | Atomic across the whole directory too |
| Type of scale | Degree of improvement | Reason |
|---|---|---|
| Feature additions | Large | Use Case and Domain responsibilities are separated |
| Code volume | Large | Change locality increases |
| Team size | Large | Avoids change concentration in a giant cli.rs |
| Number of tests | Large | Domain judgment can be tested without I/O |
| Adding output formats | Medium–large | A Presenter can be added |
| Adding importers | Medium–large | Can connect to KnowledgeWorkspace’s interface |
| Knowledge item count | Small–medium | Sharing a Snapshot reduces duplicate reads |
| Git history / milestone count | Small | The core algorithm is unchanged |
| Horizontal scale | None | The local CLI is preserved |
The main effect of this design is maintainability as code volume, feature count, and team size grow, more than raw execution speed.
Performance improvements for large data volumes require, in addition to the architectural reorganization, the following.
let snapshot = workspace.load_snapshot()?;
validate(&snapshot);
compile(&snapshot);
build_traceability(&snapshot);
Prevents validation, generation, and index building from re-reading YAML within the same process.
Knowledge tree SHA
-> changed Feature IDs
-> regenerate only those Features' TestCases
-> update the overall Manifest
To guarantee correctness, full generation remains the canonical operation.
generate full generation
generate --incremental incremental generation
CI periodically verified via full generation
GitTreeKnowledgeSource reads past Knowledge from a target commit’s blobs/trees without creating a temporary worktree. This is the replacement target for historical_testcases_by_feature.
Speed up the following lookups with reconstructible indexes.
Feature ID -> ChangeEvent
Feature ID -> TestCase
case_id -> Execution milestones
case_id -> verified tree SHA
--max-pairs 10
--time-budget 5m
--from-milestone <name>
Makes CI run time predictable. Parallelization is undertaken after designing conflict control for shared output files and Git notes.
Horizontal scale via a server, shared DB, or job queue is not adopted at this time.
markharness’s primary execution opportunities are local editing, PR-time CI, Change computation on tag push, and periodic backfill. Doing the following within a single process first is more consistent with the Git-native nature:
CommitRef::Milestone and CommitRef::Commitfeature.yml, still changes the tree SHA.true_divergences can be detected when merge commits are preserved.ChangeAnalyzer also works between two arbitrary non-tag commits (equivalent to a PR’s base/head).generate twice produces the same byte sequence.verify distinguishes additions, changes, and deletions.A summary of decisions/0009 Decision 8. Detailed work units for each Phase are managed via checklist-<task>.md when that Phase starts.
compute_changes’s boolean parameters with ChangeOptions.changes.rs into git.rs (no trait abstraction, Section 7.1).tests/fixtures/stage0/changes-m1-m2.golden.yml.Directory layout is unchanged at this stage.
CommandOutcome.generate, changes compute, and verify pending.generated/ only after success using a backup-assisted directory switch.KnowledgeSnapshot.Current/Pending/Stale/Unknown evaluator from Verification’s loading logic.ChangeAnalyzer on the CommitRef basis (Section 4.3).KnowledgeSource with working-tree and Git-tree adapters.GitTreeKnowledgeSource..markharness-cache/index/ as reconstructible derivatives.--max-pairs and --time-budget to Backfill.For local knowledge management inside a Git repository, the complexity of networking, authentication, distributed transactions, and operational infrastructure would be excessive. Not adopted.
Would create a second source of truth alongside Git’s history, review, and branch/tag workflow. Not adopted; SQLite and similar are limited to reconstructible index use.
Abstracting a dependency with only one implementation adds interfaces and reduces maintainability. Only consolidate Git operations (7.1) for now, and trait-abstract only seams where multiple Adapters are actually needed, such as working tree vs. Git tree (KnowledgeSource, 7.2).
Cache corruption, deletion detection, and inconsistencies from canonicalization-rule changes would be hard to detect. Not adopted; full generation remains canonical.
ChangeAnalyzer with a Fixed MilestoneRefConflicts with the direction decisions/0008 Stage 2 has already decided — treating PR base/head as a first-class version range — and would cause rework (a core interface redesign) when that stage starts. Not adopted; generalized to CommitRef instead (Section 4.3).
A modular monolith that keeps the current single Rust CLI and Git-native data model is the right fit for markharness.
The core Modules are the following five.
KnowledgeWorkspaceTestcaseCompilerChangeAnalyzer (CommitRef-based, handling both milestones and PR base/head)VerificationEngineBackfillCoordinatorThe current implementation already realizes important parts of this design: deterministic generation, reuse of Change computation, real-Git tests, safe file operations, and a content-addressed cache key. The highest-priority improvement is not a full rebuild, but separating the responsibilities of the now-large CLI, typed interfaces, consolidating Git operations, and atomicity of generated-artifact updates.
The architectural reorganization mainly improves scale with respect to feature count, code volume, and team size. Performance improvements for data volume are achieved by progressively introducing KnowledgeSource, reconstructible indexes, per-Feature processing, and Backfill throughput limits on top of this interface. Designing ChangeAnalyzer on a CommitRef basis means the extension to decisions/0008 Stage 2’s PR Verification Plan feature can also be built on this foundation without a backward-incompatible redesign.