Skip to content

ADR-0053: Ubiquitous Language as a Living Artifact

Status

Accepted

Date

2026-05-04

Enforced by

tests/test_ubiquitous_language.py, tests/test_seed_terms.py

Context

HydraFlow has accumulated 293 wiki entries across 13 topic files (docs/wiki/) and 50+ ADRs (docs/adr/). CLAUDE.md P2.9 names ~14 load-bearing terms ("Names are load-bearing — don't paraphrase") as a flat bullet list. This list is implicit, unenforced, and drifts: terms get renamed in code, new load-bearing classes appear without entering the language, and entries paraphrase canonical names ("repo wiki loop" instead of RepoWikiLoop).

Wiki entries are evidence and policy — they describe rules and rationale. They are not the language itself. A flat glossary is fragile: it doesn't disambiguate the same word across bounded contexts ("review" in ReviewRunner vs code-review skill vs GitHub PR review), it has no resolvable code anchor, and there is no mechanism for code↔language drift to fail CI.

DDD provides the load-bearing pattern: a ubiquitous language shared between the system, its docs, and the people working on it. It is structured (Aggregates, Entities, Value Objects, Services, Domain Events, Bounded Contexts), it has explicit context boundaries, and it evolves alongside the code. HydraFlow already has implicit bounded contexts — caretaker, builder, ai-dev-team, plus a shared kernel — established by ADR-0029, ADR-0001, and ADR-0045.

Decision

Term as a first-class artifact

A new entity type, Term, lives in docs/wiki/terms/ (one markdown file per term, YAML frontmatter + prose body). Term is sibling to WikiEntry, not a replacement. Wiki entries continue to capture rules and evidence; terms own canonical names, definitions, invariants, code anchors, and typed edges to other terms.

Vocabulary kinds (closed set)

aggregate, entity, value_object, domain_event,
service, port, adapter, bounded_context,
invariant, policy, loop, runner

The last two (loop, runner) are HydraFlow-specific kinds not present in canonical DDD; they reflect names that are load-bearing in this codebase. The set is closed — adding a kind requires a new ADR superseding this one.

Bounded contexts (closed set)

caretaker, builder, ai-dev-team, shared-kernel

Each term lives in exactly one context. A surface name shared across contexts (e.g., "review") becomes multiple terms — same name, different contexts — and that disambiguation is the explicit value.

Typed edges between terms

is_a, part_of, publishes, consumes,
guarded_by, implements, depends_on, contradicts

Edges are stored on the source term as related: list[TermRel]. The set is closed for the same reason as kinds.

Three CI lint rules (governance)

  1. Anchor resolution — every term's code_anchor (e.g., src/repo_wiki_loop.py:RepoWikiLoop) MUST resolve to a live symbol. Failure is a hard CI error. Renamed symbols are handled via the term's aliases list, which the lint also reads.
  2. Paraphrase detector — when wiki entry prose mentions a term's aliases value instead of its canonical name, lint flags it. Hard CI error after grace period (initial: warn-only for one cycle).
  3. Reverse coverage — load-bearing code symbols (classes matching *Loop, *Runner, *Port, *Adapter, plus the explicit list in CLAUDE.md) without a corresponding Term produce a warn-only signal in make quality. Promoted to hard-fail in a follow-up ADR after the first migration cycle.

Storage and rendering

  • Terms: docs/wiki/terms/<slug>.md, YAML frontmatter + prose definition + invariants list
  • Glossary view: docs/arch/generated/ubiquitous-language.md (alphabetical, regenerated by make quality and arch-regen.yml)
  • Context map: docs/arch/generated/ubiquitous-language-context-map.md (Mermaid, one subgraph per bounded context, typed edges between terms)
  • Both generated files have a "DO NOT EDIT" header; the term files in docs/wiki/terms/ are the source of truth

Authoring discipline

  • New terms in this slice are hand-authored. LLM-assisted authoring is out of scope until a follow-up ADR establishes guardrails.
  • Every term has confidence: proposed | accepted | deprecated. Seed terms ship as accepted. New terms enter as proposed; promotion to accepted requires explicit human review.
  • Supersession follows the same pattern as WikiEntry (superseded_by + superseded_reason).

Consequences

  • CLAUDE.md's flat UL list is replaced by a pointer to docs/wiki/terms/ and this ADR — single source of truth.
  • Drift between code names and documented language becomes a CI failure, not a vibe.
  • Adding a new load-bearing class (loop, runner, port) creates an explicit prompt to author a Term, surfaced as a warn in make quality.
  • Existing 293 wiki entries are unchanged in this slice. A future ADR will define how entries reference terms via evidence / applies_to once the seed glossary is proven valuable.
  • The glossary becomes the canonical onboarding document for the system's vocabulary, replacing CLAUDE.md's bullet list.

Alternatives considered

  • Free-form glossary in markdown. Rejected: no enforcement, drifts within two PRs.
  • Annotate every WikiEntry with a terms field. Rejected: inverts the DDD relationship — terms become decoration on rules, when they should be the language.
  • OWL/RDF ontology with subPropertyOf and reasoner. Rejected: heavyweight, no clear payoff for a 14-term seed glossary, indistinguishable from the lint rules' value.
  • Defer until after wiki graph (applies_to / related on entries). Considered, but the graph plan would be authoring symbol references against a non-existent vocabulary. Terms-first inverts the dependency.
  • ADR-0032 — repo wiki knowledge base
  • ADR-0044 §P2.9 — names are load-bearing
  • ADR-0029, ADR-0001, ADR-0045 — bounded context boundaries
  • src/repo_wiki.pyRepoWikiStore, WikiEntry, LintResult
  • docs/wiki/terms/ — the seed glossary