14. Diffs
Every Archlang model lives in git. Branches are future states, commits are architectural decisions, the working tree is now. That much you get for free. The interesting part is how Archlang turns git’s line-level diffs into structural diffs — diffs that talk about modules, interfaces, and processes rather than added and deleted lines.
This chapter is about how that works, what you can do with it, and what the limits are.
What a structural diff is
A git diff of two .arch files shows added and removed lines. That’s useful for reading the file but useless for reading the architecture. Rename a module from Payments to PaymentsService and the git diff shows:
service #pay001 Payments {service #pay001 PaymentsService {A reviewer can read that and figure out it’s a rename. But automated tooling can’t trivially say “the rename of Payments introduces no new dependencies” — that requires knowing it’s a rename, not delete-plus-add.
A structural diff says, in words like these:
- Renamed:
Payments→PaymentsService(same#pay001). - Added label:
criticality: HighonPaymentsService. - Added interface:
PaymentsService.Void. - Added module:
FraudCheck(#frd001). - Removed module:
LegacyBilling(#leg001). - Modified process:
BasicPayment— one step inserted.
Chapter 3 walked through one of these end to end. This chapter focuses on the mechanics and the tooling.
How identity is matched
The diff engine matches old to new in this order:
- By stable ID. Anything with a
#idmatches by ID. A module with#pay001in the old file matches the module with#pay001in the new file, regardless of name. Renames are detected here. - By structural path inside a stable parent. Facets and interfaces don’t carry IDs (Chapter 13). Their identity is the dot-path inside a parent that does —
Payments.AuthorizematchesPaymentsService.Authorizebecause the parent#pay001matched first. - By contract-shape heuristics. Within a matched parent, interfaces with different names are tested for similarity — kind, fields, description, name similarity. Strong matches are reported as renames; weak matches fall back to delete-plus-add.
Special diff views
The viewer’s diff mode renders the structural delta directly on the diagram. The viewer’s UI offers several saved diff views:
- Before / After — paint added nodes green, removed red, modified yellow.
- What Changed — list-only view, no diagram, just the bullet form of the structural delta.
- New Dependencies — focus on edges introduced by the diff. Useful when a refactor adds cross-team calls and you want to make sure they’re intentional.
- Removed Dependencies — the inverse. Surface things that used to talk and no longer do.
Each is just a configured diff render — same underlying engine.
What gets compared
A diff has two sides. They can be:
- Two git revisions in one branch — for example, “what changed in the last commit?”
- Two branches — what a feature branch introduces over
main. - The working tree vs. HEAD — uncommitted changes.
- Two arbitrary directories — the Chapter 3 example.
All four resolve to two parsed snapshots; the same engine compares them. Different tooling surfaces (the hosted viewer, editor plugins, scripts using the core diff library) expose different convenience entry points for picking the two sides.
What replaces TO-BE / AS-IS
Older modeling tools (RSM, Sparx) use marker fields — state: as-is, state: to-be, new, changed, existing — to indicate that the model contains both current and proposed structure. Archlang doesn’t need these.
Branches play the role. Your main branch is the current architecture. A feature branch is a proposed architecture. The diff between them is “what changes if we ship this proposal.” Reviewers see the structural delta and decide.
No flags. No second copy of the same module annotated differently. No risk of forgetting to remove a state: to-be marker after shipping.
Limits
- Interface renames combined with kind changes can defeat the heuristic.
command Authorize→query Validatein the same commit looks like delete-plus-add. Mitigate by splitting the change across two commits (rename first, kind change second). - Cross-module interface moves are currently treated as delete-plus-add. If you move
OrderEventsfromOrderstoEventBus, the diff says one event was removed and another added. A future iteration may track these. - Whole-package renames require manual stewardship. The package name in
package.archspacedoesn’t carry an ID; if you renameacme.shoptoacme.commerce, the loader treats the result as a new package. Most renames in practice happen on directories and dependency paths, where the path-following dependency resolution accommodates them.
Stability through refactors
Because the diff engine respects stable IDs and dot-paths, large refactors stay reviewable:
- Splitting a module into two: move half the interfaces into a new module with a new ID. The diff shows: one interface removed from
OldModule, one new module added containing those interfaces. Reviewers see the move; the interface-shape heuristic links the moved interfaces to their old definitions. - Reorganizing nesting: change
service Orders { ... }toservice Orders in CommerceSystem { ... }. The module’s ID stays the same; the diff reports a parent change, not a delete. - Renaming labels: change
domain: Paymentstodomain: PaymentDomainacross many files. The diff reports field-value changes per module; the structural shape is unaltered.
Beyond the viewer
The diff engine is exposed as a library function in @archlang/core. Pipelines and review tools consume its structured output — a list of renames, additions, removals, and modifications — and feed it into:
- Code review automation (block PRs that introduce cross-team dependencies without sign-off).
- Architecture decision records (auto-generate “what changed” sections in ADRs).
- Compliance reports (any modification touching a
security.zone: PCImodule flagged for review).
Casual reader checkpoint
If you only consume the kinds defined by your stdlib or your platform team, you have the full picture now:
- You know what a module, interface, facet, process, and view are.
- You know how fields, labels, and descriptions describe and classify them.
- You know how packages, stable IDs, and diffs make change manageable.
You can stop here. The remaining chapters are about defining the metamodel — creating your own kinds, controlling how their fields propagate, and building custom widgets. They’re essential for platform teams and language-extension authors. They’re optional for application teams that consume stdlib kinds.
Summary
- Archlang’s diff is structural — it speaks of renamed modules and added interfaces, not added lines.
- Stable IDs let the diff recognize renames; interfaces use contract-shape heuristics.
- The viewer ships several diff views: Before/After, What Changed, New/Removed Dependencies.
- Branches replace TO-BE / AS-IS markers; reviewing a PR is reviewing an architecture delta.
- The diff engine is exposed as a library function for pipelines and review tools.
What’s next
Chapter 15: Why Types? → — the first chapter of Part IV, and the mindset shift that makes the rest of the language make sense.