Skip to content

14. Diffs

Every ArchLang model lives in git, and that makes change first-class: branches are future states, commits are architectural decisions, the working tree is now. Stable IDs (Chapter 13) anchor identity through all of it, so a refactor reads as renames and moves rather than “everything deleted and re-added.” 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.

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 #k7m2qx Payments {
service #k7m2qx 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: PaymentsPaymentsService (same #k7m2qx).
  • Added aspect: criticality: "High" on PaymentsService.
  • Added interface: PaymentsService.void.
  • Added module: FraudCheck (#r3n8wt).
  • Removed module: LegacyBilling (#b9x4cd).
  • 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.

The diff engine matches old to new in this order:

  1. By stable ID. Anything with a #id matches by ID. A module with #k7m2qx in the old file matches the module with #k7m2qx in the new file, regardless of name. Renames are detected here.
  2. By structural path inside a stable parent. Surfaces and interfaces don’t carry IDs (Chapter 13). Their identity is the dot-path inside a parent that does — Payments.authorize matches PaymentsService.authorize because the parent #k7m2qx matched first.
  3. By contract-shape heuristics. Within a matched parent, interfaces with different names are tested for similarity — type, fields, description, name similarity. Strong matches are reported as renames; weak matches fall back to delete-plus-add.

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.

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.

Older modeling tools 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.

  • Interface renames combined with type changes can defeat the heuristic. rest_create authorizerest_read validate in the same commit looks like delete-plus-add. Mitigate by splitting the change across two commits (rename first, type change second).
  • Cross-module interface moves are currently treated as delete-plus-add. If you move OrderEvents from Orders to EventBus, 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.archspace doesn’t carry an ID; if you rename acme.shop to acme.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.

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 { ... } to in CommerceSystem service Orders { ... }. The module’s ID stays the same; the diff reports a parent change, not a delete.
  • Renaming aspects: change domain: "Payments" to domain: "PaymentDomain" across many files. The diff reports field-value changes per module; the structural shape is unaltered.

The diff engine is exposed as a library function in @archlang/engine. 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: PCI module flagged for review).

If you only consume the types defined by your stdlib or your platform team, you have the full picture now:

  • You know what a module, interface, surface, process, and view are.
  • You know how fields, aspects, 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 types, 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 types.

  • 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.

Chapter 15: Why Types? → — the first chapter of Part IV, and the mindset shift that makes the rest of the language make sense.