Skip to content

Repository & Scope Model

How the single source of truth is serialized and laid out on disk — formats, the directory tree, identifiers, determinism rules — and how scopes, reference resolution, layering, promotion, the project lifecycle, and concurrency work.

Serialization formats

Data Format Notes
Tailoring Manifest (ea.manifest.yaml) YAML human-authored; schema-validated
Project metadata (project.yaml) YAML human-authored; schema-validated
Materialized definitions (definitions/…) YAML derived store; shape per packs.md
Artifacts (*.md) Markdown + YAML front-matter typed front-matter over a free-text body
CLI output JSON wire format only, on stdout — never written into the repo

JSON is used only as the CLI wire format. It never appears in the source of truth.

YAML profile

ea-core reads and writes a restricted, safe YAML subset:

  • Safe-load only. No custom tags, no !!python/..., no arbitrary object construction.
  • No anchors / aliases / merge keys (&, *, <<) — they defeat byte-stable diffs; rejected on read.
  • No bare timestamps (review: 2026-07-03) — safe-load would construct date/datetime objects, which fall outside JSON's model and cannot enter the CLI envelope; rejected on read. Quote the value ("2026-07-03") to keep it as a string. This is the read-side mirror of the quoting the canonical writer already applies to date-like scalars on emission.
  • Block style only on emission (no flow {}/[] collections except the empty cases).
  • Explicit scalar typing. Strings YAML would coerce (yes, no, on, off, null, ~, numbers, dates, Norway) are emitted quoted; the core never relies on implicit coercion.
  • UTF-8, no BOM. Non-ASCII is emitted verbatim (not \u-escaped).

Artifact = Markdown + YAML front-matter

---
domain: business
type: capability
id: billing
name: Billing
relates_to:
  - { type: pattern, id: retry-with-backoff }   # resolves locally, else Org (fall-through)
---
Billing handles invoicing, dunning, and revenue recognition for the platform.

## Context
…free Markdown…
  • The opening --- MUST be the first line; the front-matter is parsed under the YAML profile.
  • The body is opaque to the core: preserved verbatim, treated as a byte string for hashing/diffing. (Types may declare advisory body.sections an agent composes the body from — see packs.md — but the core never enforces body structure at commit.)
  • A file with no front-matter, an unterminated fence, or non-YAML front-matter is a structural validation error.

Validation layering

  1. Structural. JSON Schema (Draft 2020-12) documents shipped in ea-core validate the parsed tree of every manifest, project.yaml, materialized definition, and artifact front-matter envelope. Pure shape — never depends on the manifest or methods being loaded, so it runs even on a partially-broken repo.
  2. Semantic. The active types, relationship rules, domain vocabulary, and reference-layering rules validate on top ("domain must be one the manifest declares", "project → project references are illegal").

Encoding & determinism

Every file ea-core emits obeys these rules — identical inputs produce byte-identical files across runs and machines:

  • Encoding: UTF-8, no BOM. Newlines: LF only; single trailing newline; no trailing whitespace.
  • Indentation: 2 spaces; block style.
  • Key ordering: schema-declared order, else lexicographic (Unicode code-point order).
  • Sequences: a defined, stable order — schema-mandated or sorted by a documented key (e.g. a reference list by its (scope, domain, type, id) tuple).
  • No wall-clock or randomness. Any timestamp in the data (e.g. created) is an injected input supplied by the caller, never sampled by the core.
  • Idempotent round-trip. Reading a canonical file and re-emitting it yields identical bytes.

The content-bound confirmation_token (cli.md) hashes these canonical bytes — this is what guarantees the token is stable and drift-detecting.

Repository layout

my-ea-repo/
├── ea.manifest.yaml              # the one org-level Tailoring Manifest — marks the repo root
├── definitions/                  # active, MATERIALIZED subset — only what's in use
│   ├── methods/<method-id>.yaml  #   one file per selected method
│   ├── types/<type-id>.yaml      #   the artifact types those methods require
│   ├── rules/<rule-id>.yaml      #   the rules those methods require
│   └── index.yaml                #   integrity lock — each active pack@version + content digest
├── org/                          # the Org scope — shared, org-level architecture
│   └── <domain>/<type>/<id>.md   #   e.g. org/business/capability/billing-map.md
├── projects/                     # project scopes
│   └── <project-id>/
│       ├── project.yaml          #     project metadata (reserved name)
│       └── <domain>/<type>/<id>.md
└── .ea/                          # core working dir (cache) — gitignored, never authoritative
  • Repo root & discovery: a directory is an EAaC repository root iff it directly contains ea.manifest.yaml; the core discovers the root by walking up from the working directory.
  • definitions/ holds the materialized active subset: exactly the methods tailoring selects, plus the types/rules they require — never whole packs, never unused methods. The store is itself authoritative for the active set; every item carries source: provenance, and a consistency check verifies the dependency closure is complete. Materializing it is a guarded write. There is no templates/ tree — scaffold derives artifact stubs from types.
  • Scopes: exactly one Org scope (org/); zero or more projects (projects/<id>/, directory name = project.yaml's id). Every artifact belongs to exactly one scope by its subtree; a file outside both is a structural error.
  • Artifact path: <scope-root>/<domain>/<type>/<id>.md. The canonical key is the tuple (scope, domain, type, id) — the path is that key. The same id may recur under a different (scope, domain, type).
  • .ea/ holds non-authoritative working state only; deleting it is safe and lossless. Gitignore /.ea/.
  • Reserved names: at root — ea.manifest.yaml, definitions/, org/, projects/, .ea/; within definitions/methods/, types/, rules/, index.yaml; within a project — project.yaml. Domain/type names must not collide with a reserved name in their position.

project.yaml

id: payments              # MUST equal the parent directory name
name: Payments Platform   # human label
status: active            # active | archived
created: 2026-06-19       # injected timestamp input (never read from the clock)
description: >            # optional
  Solution architecture for the payments platform.

Identifiers

One rule for project ids, domain names, type names, artifact ids, and method ids:

  • Pattern ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ (lowercase kebab-case), length 1–64.
  • Compared byte-for-byte (only lowercase is legal).
  • Pack/method versions use SemVer; a pinned reference is written pack@version#method.
  • A filename's stem MUST equal the artifact id; the loader rejects a mismatch.

Artifact front-matter envelope

Envelope fields validated structurally for every artifact, regardless of type:

Field Required Rule
domain yes MUST equal the <domain> path segment
type yes MUST equal the <type> path segment
id yes MUST equal the filename stem
name yes human label; non-empty

All other front-matter fields are type-defined (packs.md) and validated in the semantic layer.

Reference target shape

Wherever an artifact references another, the target is an object — location-independent by design:

{ type: pattern, id: retry-with-backoff }                # common form
{ scope: org, type: pattern, id: retry-with-backoff }    # with an optional scope assertion
Field Required Meaning
type yes target artifact type
id yes target artifact id
scope no optional assertion (org or a project id); validated against where the target resolves — never part of identity
domain no optional assertion of the target's domain; validated if given

A reference carries no scope by default: resolution searches the source artifact's own scope, then Org (fall-through) — so the same {type, id} keeps resolving after its target is promoted project → Org, with no rewrite.

Reference resolution

Resolution is edge-agnostic: it operates on the reference target regardless of which type declared the relationship — a cross-pack edge added by the manifest relationship overlay (manifest.md) resolves through the same algorithm with no special case.

Visibility

Source scope Visible scopes (search order)
project P P, then org
org org only

Algorithm

To resolve {type, id, scope?, domain?} from a source artifact in scope Σ:

  1. If scope is asserted: it must be visible to Σ (else layering error); search only it.
  2. Otherwise search Σ's visible scopes in order; take the first (type, id) match.
  3. If domain is asserted, the resolved artifact's domain must equal it (else error).
  4. No match in any visible scope → dangling reference error.

Deterministic: visibility order is fixed, and no-shadowing guarantees at most one match per scope.

Uniqueness (no shadowing)

Within a project's visible set (own scope ∪ Org), (type, id) is unique — a project may not define an artifact whose (type, id) also exists in Org. Fall-through is therefore unambiguous, and promotion conflict-detection is trivial.

Layering, enforced by visibility

Reference Result
project → same project allowed
project → Org allowed
Org → Org allowed
Org → project impossible — project not visible → dangling error
project A → project B impossible — B not visible → dangling error

Consequence: only Org artifacts can have inbound references. Nothing outside a project can point into it — which is what makes project archive/delete safe (they can never break an external inbound link). An unresolvable reference is a validation failure, reported by the reference_resolves rule.

Promotion

Promotion moves a project artifact into Org and is a guarded, dry-run-by-default write with a reviewable plan:

  • Inbound references don't churn. Any {type, id} reference that resolved in-project now falls through to the Org copy automatically — no rewrite.
  • Outbound references must stay legal. After the move the artifact's visible set is Org only, so every outbound reference must resolve within Org. The plan pre-checks this; a reference that would dangle blocks promotion, and the plan reports it so the user can co-promote the target or remove the link.
  • No shadowing. If Org already holds the same (type, id), promotion is refused with the collision reported.
  • Single authoritative copy: the project copy is removed, not duplicated.

Project lifecycle

project.yaml carries status: active | archived. Three deterministic, guarded operations:

  • Create — confirmed write: creates projects/<id>/ with a project.yaml (status: active); id must match the pattern and not exist.
  • Archive (reversible) — sets status: archived. An archived project is retained and validatable on demand (--include-archived) but excluded from default validate/report runs; new artifacts may not be authored into it; unarchive reverses it. Non-destructive, so no delete guard; nothing external references into a project, so archiving never dangles anything.
  • Delete (destructive) — removes projects/<id>/ through the verify-before-delete guard. Self-contained by layering: no external inbound references can exist.

Verify-before-delete guard

A single tested guard backs every destructive file operation (project delete, artifact delete, definitions/ re-materialization overwrite):

  1. Plan (dry-run). Enumerate the exact target files and compute a fingerprint — a stable, sorted list of (path, content-hash) over the target set. Emit plan + fingerprint + confirmation_token (the token binds both).
  2. Apply (--confirm <token>). Recompute the current fingerprint; if it differs — the target changed since planning — refuse (drift guard, exit 4).
  3. Only on an exact match is the removal performed.

No destructive path bypasses it.

Concurrency: single-writer-per-worktree

One writer works on a git worktree/branch; git branches and merges are the collaboration model. The core takes no lock:

  • Reconciliation between writers is git's job — divergent edits surface a merge conflict, never a silent overwrite — so ea-core builds no locking/optimistic concurrency.
  • The confirm-token already guards drift within a tree: a plan computed against one state cannot be applied against a changed state (exit 4).
  • The one residual same-tree gap — create-create — is closed by binding a create's prior absence into the token, so a concurrent create trips the same drift guard.
  • Re-opens only if a non-git shared mutable store is ever built.

Worked example

org/
  data/pattern/retry-with-backoff.md
projects/
  payments/                      # status: active
    business/capability/billing.md   # relates_to: { type: pattern, id: retry-with-backoff }
  legacy-billing/                # status: archived
    business/capability/old-billing.md
  • billing.md's reference resolves: not found in payments, falls through to org. Legal (project → Org).
  • If retry-with-backoff were promoted from payments to org, the same reference would keep resolving with no edit.
  • A reference from payments into legacy-billing is unresolvable (other projects aren't visible) → validation failure.
  • Deleting legacy-billing runs the verify-before-delete guard; nothing references into it, so the delete is self-contained.