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 constructdate/datetimeobjects, 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.sectionsan 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¶
- Structural. JSON Schema (Draft 2020-12) documents shipped in
ea-corevalidate 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. - Semantic. The active types, relationship rules, domain vocabulary, and reference-layering
rules validate on top ("
domainmust 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 carriessource:provenance, and a consistency check verifies the dependency closure is complete. Materializing it is a guarded write. There is notemplates/tree —scaffoldderives artifact stubs from types.- Scopes: exactly one Org scope (
org/); zero or more projects (projects/<id>/, directory name =project.yaml'sid). 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 sameidmay 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/; withindefinitions/—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 Σ:
- If
scopeis asserted: it must be visible toΣ(else layering error); search only it. - Otherwise search
Σ's visible scopes in order; take the first(type, id)match. - If
domainis asserted, the resolved artifact's domain must equal it (else error). - 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 aproject.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 defaultvalidate/reportruns; new artifacts may not be authored into it;unarchivereverses 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):
- 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). - Apply (
--confirm <token>). Recompute the current fingerprint; if it differs — the target changed since planning — refuse (drift guard, exit 4). - 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 inpayments, falls through toorg. Legal (project → Org).- If
retry-with-backoffwere promoted frompaymentstoorg, the same reference would keep resolving with no edit. - A reference from
paymentsintolegacy-billingis unresolvable (other projects aren't visible) → validation failure. - Deleting
legacy-billingruns the verify-before-delete guard; nothing references into it, so the delete is self-contained.