Architecture¶
This page maps the Rhiza CLI's source layout and traces a rhiza sync call
through the modules end to end. It is the entry point for a new contributor: read
it first, then follow the Architecture Decision Records for the
why behind the structure.
Package layout¶
The package lives under src/rhiza/ and splits cleanly into three layers:
| Layer | Location | Responsibility |
|---|---|---|
| CLI | cli.py, __main__.py |
Typer app: argument parsing, option wiring, error-to-exit-code translation. No business logic. |
| Commands | commands/ |
One module per subcommand — the orchestration layer. |
| Models | models/ |
Data + git engine: config/lock parsing, bundle resolution, and the git driver. |
Commands (src/rhiza/commands/)¶
| Module | Subcommand | Role |
|---|---|---|
sync.py |
rhiza sync |
Primary command — orchestrates the 3-way template merge. |
_sync_helpers.py |
— | Internal helpers for sync: lock I/O, orphan cleanup, workflow-file warnings. |
init.py |
rhiza init |
Scaffold .rhiza/template.yml in a new project; verifies the template repo is reachable. |
validate.py |
rhiza validate |
Validate project structure and template.yml against the schema. |
status.py |
rhiza status |
Report the current sync state from the lock file. |
tree.py |
rhiza tree |
Show the resolved file tree a sync would manage. |
list_repos.py |
rhiza list |
List bundles/profiles available in the template repo. |
summarise.py |
rhiza summarise |
Summarise template/bundle contents. |
migrate.py |
rhiza migrate |
Migrate older config layouts to the current schema. |
uninstall.py |
rhiza uninstall |
Remove Rhiza-managed files from a project. |
Models (src/rhiza/models/)¶
| Module | Public surface | Role |
|---|---|---|
template.py |
RhizaTemplate, GitHost |
Parsed template.yml: repository, ref, profiles, templates, include/exclude. |
lock.py |
TemplateLock |
Parsed/serialised template.lock: synced SHA, tracked files, metadata. |
bundle.py |
RhizaBundles, ProfileDefinition, BundleDefinition |
Resolves profiles → bundle names → concrete file paths. |
_git_utils.py |
GitContext, get_git_executable |
The git engine — clone/sparse-checkout, snapshot prep, diff, and git apply -3 3-way merge. See ADR-0004 for why it is one module. |
_base.py |
YamlSerializable, load_model |
Shared YAML (de)serialisation base for the models. |
language_validators.py sits at the package root and provides per-language
project-structure checks used by validate.
How a rhiza sync flows through the modules¶
sync is the heart of the tool and exercises every layer. The numbered steps
below correspond to the diagram.
sequenceDiagram
participant U as User
participant CLI as cli.sync<br/>(Typer)
participant Cmd as commands.sync.sync
participant T as RhizaTemplate / RhizaBundles
participant G as GitContext<br/>(_git_utils)
participant H as _sync_helpers
participant L as TemplateLock
U->>CLI: rhiza sync [--strategy merge]
CLI->>Cmd: sync(target, branch, strategy, …)
Cmd->>G: assert_status_clean(target)
Cmd->>G: handle_target_branch(target, …)
Cmd->>T: validate() + RhizaTemplate.from_yaml()
Cmd->>G: _clone_template() — sparse clone @ ref
G->>T: resolve profiles → bundles → paths
Cmd->>L: read base SHA from template.lock
Cmd->>G: _prepare_snapshot() — materialise upstream files
Cmd->>L: build new TemplateLock
alt strategy == "diff"
Cmd->>G: sync_diff() — dry-run, show changes
else strategy == "merge"
Cmd->>G: sync_merge()
G->>G: _merge_with_base() — clone base @ base_sha,<br/>get_diff(), _apply_diff() via git apply -3
G->>G: _scan_conflict_artifacts() — detect *.rej / markers
G->>H: _clean_orphaned_files()
G->>H: _write_lock() — atomic, fcntl-locked
end
Cmd-->>U: success, or RuntimeError on conflicts
Step by step:
cli.sync(cli.py:197) parses arguments, rejects unknown strategies, and calls the command inside_exit_on_error(...), which convertsCalledProcessError/RuntimeError/ValueErrorinto a clean exit code.commands.sync.sync(commands/sync.py:195) orchestrates the rest:GitContext.default()builds the git driver.assert_status_cleanrefuses to run on a dirty tree;handle_target_branchoptionally creates/checks out a working branch._load_template_from_projectrunsvalidate()thenRhizaTemplate.from_yaml()to get a validated config._clone_templatesparse-clones the template repo at the configuredref, and (in profile/template mode) resolves profiles → bundle names → concrete paths viaRhizaBundles, then narrows the sparse checkout to exactly those paths.- The previously-synced base SHA is read from
TemplateLock, and_prepare_snapshotmaterialises the upstream files (applyingexcludepaths and any path remaps) into a temp snapshot. - The merge engine —
GitContext.sync_merge(models/_git_utils.py:471) does the 3-way merge:_merge_with_baseclones the base snapshot atbase_sha,get_diffcomputes base→upstream, and_apply_diffapplies it withgit apply -3so local edits survive._scan_conflict_artifactsdetects any leftover*.rejfiles or conflict markers. On first sync (no base), it falls back to a plain copy. - Finalisation — back in
_sync_helpers,_clean_orphaned_filesremoves files the template no longer tracks, and_write_lockwrites the refreshedtemplate.lockatomically under anfcntllock (see ADR-0003).sync_mergereturnsFalseif any conflict remains, which the command surfaces as aRuntimeError.
The dependency direction is commands → models, with one deliberate
exception: GitContext.sync_merge imports _sync_helpers lazily (a function-local
import) to reuse the lock/orphan helpers without creating an import cycle.
Why this shape — the ADRs¶
The non-obvious structural choices are recorded as Architecture Decision Records:
- ADR-0001 — the diff/patch
engine (
get_diff+git apply -3) is inlined into the codebase rather than depending oncruft. This is why_git_utils.pyowns a diff routine. - ADR-0002 —
repositoryandrefare the canonical keys intemplate.yml, parsed byRhizaTemplate. - ADR-0003 — lock-file writes use
fcntl+ atomic rename, implemented in_sync_helpers._write_lock. - ADR-0004 —
_git_utils.pystays a single ~1000-LOC module: it is one cohesiveGitContextclass plus helpers with no independent consumers. Split it only when distinct responsibilities with their own consumers emerge.
What this repo owns vs. what Rhiza owns¶
This project syncs its own dev infrastructure from the
jebel-quant/rhiza template — the same
mechanism described above, applied to this repo. Roughly:
- Locally owned (the subject of this page):
src/,tests/,pyproject.toml,README.md, the docs underdocs/(including this file and the ADRs),mkdocs.yml, and.rhiza/template.yml. - Rhiza-managed (synced, not edited in place):
.github/workflows/*, theMakefileand.rhiza/make.d/*,.pre-commit-config.yaml,pytest.ini,ruff.toml, and the.rhiza/engine. The authoritative list is thefiles:block of.rhiza/template.lock; seeCLAUDE.mdfor the ownership split.