diff --git a/MANIFEST.md b/MANIFEST.md
index 93a1556..f98fb09 100644
--- a/MANIFEST.md
+++ b/MANIFEST.md
@@ -43,29 +43,29 @@
## Examples
-> Examples are illustrative markdown with fenced code only (no standalone runtime artifacts per D-020 / D-025). The `examples/` directory listing closes the v0.2 ESC-002 drift (IDEATE-17, ATELIER-91). P5 examples populate this section.
+> Examples are illustrative markdown with fenced code only (no standalone runtime artifacts per D-020 / D-025). The `examples/` directory listing closes the v0.2 ESC-002 drift (IDEATE-17, ATELIER-91). P5 authored the v0.3 examples and promoted all entries from `pending` to `✓` (verified — every listed file exists).
| Path | Status | Notes |
|------|--------|-------|
-| `examples/good/` | (pending P5) | Good-example directory — to be populated by v0.3 P5 |
-| `examples/bad/` | (pending P5) | Bad-example directory — to be populated by v0.3 P5 |
-| `examples/good/api-endpoint.md` | (pending P5) | v0.1 example — to be listed when P5 back-fills |
-| `examples/good/react-component.md` | (pending P5) | v0.1 example — to be listed when P5 back-fills |
-| `examples/good/db-schema.md` | (pending P5) | v0.1 example — to be listed when P5 back-fills |
-| `examples/good/error-handler.md` | (pending P5) | v0.1 example — to be listed when P5 back-fills |
-| `examples/bad/god-object.md` | (pending P5) | v0.1 example — to be listed when P5 back-fills |
-| `examples/bad/silent-error.md` | (pending P5) | v0.1 example — to be listed when P5 back-fills |
-| `examples/bad/leaky-abstraction.md` | (pending P5) | v0.1 example — to be listed when P5 back-fills |
-| `examples/good/terraform-module.md` | (pending P5) | v0.2 example — to be listed when P5 back-fills |
-| `examples/good/k8s-deployment.md` | (pending P5) | v0.2 example — to be listed when P5 back-fills |
-| `examples/bad/terraform-unlocked-state.md` | (pending P5) | v0.2 example — to be listed when P5 back-fills |
-| `examples/bad/k8s-bare-pod-no-resources.md` | (pending P5) | v0.2 example — to be listed when P5 back-fills |
-| `examples/good/gitops-pr.md` | (pending P5) | v0.3 example — to be authored in P5 |
-| `examples/good/ai-ml-reproducibility.md` | (pending P5) | v0.3 example — to be authored in P5 |
-| `examples/bad/i18n-string-concat.md` | (pending P5) | v0.3 example — to be authored in P5 |
-| `examples/bad/compliance-audit-log.md` | (pending P5) | v0.3 example — to be authored in P5 |
+| `examples/good/` | ✓ | Good-example directory — 8 examples (v0.1 + v0.2 + v0.3) |
+| `examples/bad/` | ✓ | Bad-example directory — 7 examples (v0.1 + v0.2 + v0.3) |
+| `examples/good/api-endpoint.md` | ✓ | v0.1 example — good REST endpoint |
+| `examples/good/react-component.md` | ✓ | v0.1 example — good React component |
+| `examples/good/db-schema.md` | ✓ | v0.1 example — good DB schema |
+| `examples/good/error-handler.md` | ✓ | v0.1 example — good error handler |
+| `examples/bad/god-object.md` | ✓ | v0.1 example — bad god object |
+| `examples/bad/silent-error.md` | ✓ | v0.1 example — bad silent error |
+| `examples/bad/leaky-abstraction.md` | ✓ | v0.1 example — bad leaky abstraction |
+| `examples/good/terraform-module.md` | ✓ | v0.2 example — good IaC module |
+| `examples/good/k8s-deployment.md` | ✓ | v0.2 example — good k8s deployment |
+| `examples/bad/terraform-unlocked-state.md` | ✓ | v0.2 example — bad unlocked state |
+| `examples/bad/k8s-bare-pod-no-resources.md` | ✓ | v0.2 example — bad bare pod |
+| `examples/good/gitops-pr.md` | ✓ | v0.3 example — good GitOps PR |
+| `examples/good/ai-ml-reproducibility.md` | ✓ | v0.3 example — good reproducible training run |
+| `examples/bad/i18n-string-concat.md` | ✓ | v0.3 example — bad i18n string concat |
+| `examples/bad/compliance-audit-log.md` | ✓ | v0.3 example — bad audit log (P1 + P9 breaches) |
-> **Note:** The `examples/` section structure is established here (P4) so P5 can populate it. Files marked "pending P5" do not yet exist; they will be authored in Phase 5 and promoted from `pending` to `✓` upon completion. Listing them here as `pending` makes the manifest authoritative about what *will* exist and prevents drift.
+> **Note:** The `examples/` section was established in P4 with entries pre-listed as `pending P5`. P5 authored the 4 v0.3 examples and promoted all entries to `✓` after verifying every listed file exists on disk. The manifest remains authoritative — unlisted = not part of framework.
## Cross-Cutting
diff --git a/examples/bad/compliance-audit-log.md b/examples/bad/compliance-audit-log.md
new file mode 100644
index 0000000..ab48cf0
--- /dev/null
+++ b/examples/bad/compliance-audit-log.md
@@ -0,0 +1,222 @@
+# Bad Example: Compliance Audit Log (Two Breaches)
+
+> An audit logging implementation that violates **two** Atelier
+> compliance principles in one example (per IDEATE-26, D-044):
+> **P1** (Audit Logs are Append-Only) — a mutable audit log with
+> routine `DELETE`/`UPDATE` "cleanup" — and **P9** (Secrets and
+> Sensitive Data are Redacted in Audit) — a database password leaked
+> into an audit record. Each violation is cited, then fixed.
+
+## The Code
+
+```python
+# audit_log.py — the audit sink, stored in a mutable Postgres table
+import psycopg2, datetime
+
+# P1 VIOLATION: the audit log is a regular mutable table. There is no
+# write-once protection, no immutable bucket, no hash-chaining.
+# Any DB user with UPDATE/DELETE can rewrite history.
+CREATE_TABLE = """
+CREATE TABLE audit_log (
+ id BIGSERIAL PRIMARY KEY,
+ timestamp TIMESTAMPTZ NOT NULL,
+ event TEXT NOT NULL,
+ actor TEXT NOT NULL,
+ target TEXT,
+ payload JSONB,
+ request_id TEXT
+);
+-- No row-level immutability. No trigger preventing UPDATE/DELETE.
+"""
+
+def write_event(event, actor, target=None, payload=None, request_id=None):
+ conn = psycopg2.connect(os.environ["DATABASE_URL"])
+ conn.execute(
+ "INSERT INTO audit_log (timestamp, event, actor, target, payload, request_id) "
+ "VALUES (%s, %s, %s, %s, %s, %s)",
+ (datetime.datetime.utcnow(), event, actor, target,
+ json.dumps(payload), request_id),
+ )
+
+# P1 VIOLATION (continued): "cleanup" that mutates the audit log.
+# A routine job deletes records older than 30 days to "save space"
+# and updates records to "fix typos in the actor field."
+def cleanup_audit_log():
+ conn = psycopg2.connect(os.environ["DATABASE_URL"])
+ # DELETE — an audit record is destroyed. This is tampering,
+ # dressed as housekeeping.
+ conn.execute("DELETE FROM audit_log WHERE timestamp < NOW() - INTERVAL '30 days'")
+ # UPDATE — an audit record is rewritten. The "fix" is the
+ # violation; the original actor is lost.
+ conn.execute("UPDATE audit_log SET actor = 'admin' WHERE actor LIKE 'svc-%'")
+```
+
+```python
+# The call site that leaks a secret into the audit log.
+def read_config(key):
+ # ... fetches a secret from the secrets manager ...
+ value = secrets_manager.get(key) # e.g. the raw DB password
+ # P9 VIOLATION: the raw secret value is written into the audit
+ # payload. The append-only log is now a secret store.
+ write_event(
+ event="config.read",
+ actor="api-server",
+ target={"kind": "secret", "id": key},
+ payload={"value": value}, # <- the secret, in plaintext
+ request_id=req.id,
+ )
+ return value
+```
+
+The resulting audit record:
+
+```json
+{
+ "id": 48213,
+ "timestamp": "2026-08-05T09:12:03Z",
+ "event": "config.read",
+ "actor": "api-server",
+ "target": {"kind": "secret", "id": "db-password"},
+ "payload": {"value": "p@ssw0rd-sup3r-s3cr3t-plaintext"},
+ "request_id": "req_91c2"
+}
+```
+
+A week later, the `cleanup_audit_log` job `DELETE`s this record (it
+is older than 30 days in the team's "retention" — which is actually a
+storage-economy decision, not a policy), and `UPDATE`s every
+`svc-*` actor to `admin`. The secret was in the log for a week,
+readable by anyone with `SELECT` on the table; now the record of it
+having been there is gone.
+
+## What Makes It Bad
+
+### Breach 1 — Mutable Audit Log (Compliance P1 Audit Logs are Append-Only)
+- The audit log is a regular mutable Postgres table. `DELETE FROM
+ audit_log WHERE timestamp < ...` and `UPDATE audit_log SET actor =
+ ...` both succeed. The log is a draft, not a record.
+- Routine `DELETE` as "cleanup" is the cardinal P1 violation: the
+ deletion of an audit record is itself an auditable incident, not a
+ housekeeping task. "We deleted old records to save space" is a P3
+ (Retention is Policy, Not Storage) violation *and* a P1 violation —
+ the retention decision is driven by storage cost, and the
+ mechanism is tampering.
+- The `UPDATE` that rewrites `svc-deploy` → `admin` destroys
+ attribution (a P7 violation stacked on the P1 violation): the
+ original actor is lost, and the replacement (`admin`) is a shared
+ identity that could be any of ten engineers.
+- **Fix:** the audit sink is append-only *by construction*, not by
+ policy. Write-once storage (WORM bucket, immutable log stream,
+ hash-chained ledger) enforces immutability at the substrate.
+ Retention is a declared policy with a meta-audit of deletions; a
+ human does not run ad-hoc `DELETE` jobs.
+
+ ```python
+ # Fix: write to an append-only sink (illustrative — S3 Object Lock,
+ # WORM bucket, or a hash-chained ledger). The API has no update /
+ # delete path; the storage refuses mutation.
+ def write_event(event, actor, target=None, payload=None, request_id=None):
+ record = {
+ "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
+ "event": event,
+ "actor": actor, # the authenticated principal, not "admin"
+ "target": target,
+ "payload": redact(payload), # see Breach 2 fix
+ "request_id": request_id,
+ "prev_hash": last_hash(), # hash-chaining: tampering breaks the chain
+ }
+ record["hash"] = sha256(canonical_json(record))
+ append_only_sink.write(record) # WORM storage; no update/delete API exists
+
+ # Fix: retention is a declared, reviewed policy — not an ad-hoc DELETE.
+ # When an audit segment ages out, the deletion is itself meta-audited
+ # in a higher-tier log with the rule that authorized it.
+ # (See domains/compliance/data-retention.md and audit-logs.md.)
+ ```
+- See `domains/compliance/audit-logs.md` (Audit Logs are Append-Only)
+ and `domains/compliance/first-principles.md` P1.
+
+### Breach 2 — Secret Leaked in Audit Log (Compliance P9 Secrets and Sensitive Data are Redacted in Audit)
+- `payload={"value": value}` writes the raw DB password into the
+ audit record. The append-only log is now a secret store: anyone
+ with `SELECT` on `audit_log` can read production credentials. The
+ log is harder to secure than the secrets manager it read from.
+- Once the secret is in an append-only log, the remediation is
+ expensive — rotate the secret *and* rewrite the log's access scope
+ (you cannot edit the record; it is append-only). Redaction must
+ happen *at the logging boundary, before the record is written*, not
+ by opportunistic scrubbing after the fact.
+- The redaction policy here is "nothing" — there is no rule for
+ which fields are redacted, by what mechanism, in which event type.
+ A redaction rule that lives in no one's head and no code is a P9
+ violation waiting to happen (and it happened).
+- **Fix:** redaction is structural, applied at the logging boundary
+ before the record reaches the append-only sink. The policy is
+ itself auditable (which fields, by what rule, in which event).
+
+ ```python
+ # Fix: redaction at the boundary. Log the FACT of the action
+ # (a secret was read), never the CONTENT of the secret.
+ REDACTED_FIELDS = {"value", "token", "password", "authorization", "secret"}
+
+ def redact(payload):
+ if not isinstance(payload, dict):
+ return "[REDACTED:non-object]"
+ out = {}
+ for k, v in payload.items():
+ if k.lower() in REDACTED_FIELDS or "secret" in k.lower():
+ out[k] = "[REDACTED:secret]"
+ else:
+ out[k] = v
+ out["_redaction"] = "secret-value-policy/v1" # the rule is auditable
+ return out
+
+ # The fixed audit record:
+ # {
+ # "event": "config.read",
+ # "actor": "api-server", # the authenticated principal
+ # "target": {"kind": "secret", "id": "db-password"},
+ # "payload": {"value": "[REDACTED:secret]"},
+ # "_redaction": "secret-value-policy/v1",
+ # "request_id": "req_91c2"
+ # }
+ # The fact of the read is logged; the secret never enters the log.
+ ```
+- See `domains/compliance/audit-logs.md` (Redaction at the Boundary)
+ and `domains/compliance/first-principles.md` P9. Cross
+ `domains/security/secrets.md` — the audit-side redaction is the
+ complement of secret management.
+
+## The Cascade (Two Breaches Compound)
+
+The two violations compound destructively. The secret enters the
+mutable log (P9 breach), where it sits readable by any `SELECT`-holder
+for a week. Then the `cleanup` job `DELETE`s the record (P1 breach) —
+destroying the evidence that the secret was ever logged, while the
+secret itself has already been exposed to every reader of the table.
+The `UPDATE` that rewrites `svc-deploy` → `admin` (a P7 attribution
+breach stacked on the P1 breach) means that even if a copy of the
+record survived, the actor who triggered the secret read is no longer
+identifiable. The team cannot answer "who read the DB password and
+when" — the log that would answer it was mutated, and the secret it
+leaked is now in the wild. This is the worst-case interaction of P1
+and P9: a secret leak with no attributable actor and no surviving
+record.
+
+## Cross-Domain Links
+
+- `domains/compliance/audit-logs.md` — the append-only guarantee and
+ the redaction-at-boundary rule this code violates.
+- `domains/compliance/first-principles.md` — P1 (Append-Only) and P9
+ (Redacted) are the two breached principles; P7 (Attributable) is
+ breached by the `UPDATE` rewrite.
+- `domains/compliance/evidence.md` — an audit log that can be
+ `DELETE`d is not admissible evidence; the append-only guarantee is
+ what makes it admissible.
+- `domains/compliance/data-retention.md` — retention is a declared
+ policy with meta-audited deletions, not an ad-hoc `DELETE` job.
+- `domains/security/secrets.md` — redaction at the logging boundary
+ is the audit-side complement of secret management.
+- `domains/observability/logging.md` — audit logs are structured
+ logging with an append-only guarantee; the logging primitives
+ compose here.
\ No newline at end of file
diff --git a/examples/bad/i18n-string-concat.md b/examples/bad/i18n-string-concat.md
new file mode 100644
index 0000000..1e497b2
--- /dev/null
+++ b/examples/bad/i18n-string-concat.md
@@ -0,0 +1,175 @@
+# Bad Example: i18n String Concatenation
+
+> A checkout component that violates Atelier's i18n principles. Each
+> violation is cited, then fixed.
+
+## The Code
+
+```typescript
+// Checkout.tsx — the cardinal i18n violation
+function CartSummary({ itemCount, name, total, currency, date }) {
+ // P3 VIOLATION: inline string concatenation. The source-language
+ // word order ("Welcome, {name}! You have {n} items") is baked into
+ // code. Every locale with different word order is broken.
+ const welcome = "Welcome, " + name + "!";
+
+ // P4 VIOLATION: hand-rolled plural branching. `if (n === 1)` encodes
+ // exactly English's one/other rule. Arabic (six categories), Russian
+ // (three), Polish (three) are all wrong.
+ const items =
+ itemCount === 1 ? "1 item" : itemCount + " items";
+
+ // P5 VIOLATION: hand-rolled currency + date formatting. "$" + total
+ // is wrong for de-DE (symbol, grouping, placement). The date
+ // `getMonth() + 1 + "/" + getDay()` is US-only (mm/dd/yyyy).
+ const price = "$" + total.toFixed(2);
+ const d = new Date(date);
+ const dateStr = (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear();
+
+ return (
+
+
{welcome}
+
{items} · {price} · {dateStr}
+
+ );
+}
+```
+
+```typescript
+// The "resource" file — a custom CSV the team hand-rolled.
+// locale,en_us
+// welcome_prefix,Welcome,
+// item_singular,item
+// item_plural,items
+//
+// This is a P3 violation on its own: a custom format no standard
+// tool (xgettext, i18next, FormatJS) can extract from or compose with.
+```
+
+The team runs their first i18n test against real Arabic translations —
+after the string freeze, after the translator was paid. The Arabic
+build renders `"Welcome, محمد!"` with the name on the wrong side of
+the comma, `"1 items"` for a single item (Arabic has six plural
+categories, not two), and the price as `"$1,234.56"` (Arabic-Egypt
+formats as `"١٬٢٣٤٫٥٦ ج.م."`). Every screen is a rewrite, not a patch.
+
+## What Makes It Bad
+
+### Inline String Concatenation (i18n P3 Resources are External, Not Inline)
+- `"Welcome, " + name + "!"` bakes English word order into code. In
+ Japanese the name comes first (`ようこそ、محمدさん!`); in Arabic the
+ structure differs again. The concatenation is invisible to the
+ extraction pipeline (`xgettext`, `i18next-parser`) — the translator
+ never sees it as a unit, and the string cannot be versioned or
+ rolled back as a whole.
+- The custom `.csv` "resource" store is a second P3 violation: no
+ standard tool reads it, it carries no plural grammar, and it cannot
+ compose with the ICU formatting layer.
+- **Fix:** strings live in a standard locale resource file, addressed
+ by key. Code calls `t("welcome", { name })`; the resource carries
+ the parameterized message.
+
+ ```json
+ // en-US.json (ICU MessageFormat)
+ {
+ "checkout.welcome": "Welcome, {name}!",
+ "checkout.cart.summary": "{count, plural, one {# item} other {# items}} · {price} · {date}"
+ }
+ ```
+
+ ```json
+ // ar-EG.json — six plural categories per CLDR; the code is identical
+ {
+ "checkout.welcome": "أهلاً بك، {name}!",
+ "checkout.cart.summary": "{count, plural, zero {لا عناصر} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصرًا} other {# عنصر}} · {price} · {date}"
+ }
+ ```
+- See `domains/i18n/locale-resources.md` (Resources are the Boundary)
+ and `domains/i18n/first-principles.md` P3.
+
+### Hand-Rolled Plural Branching (i18n P4 Plural and Gender are Parameterized)
+- `itemCount === 1 ? "1 item" : itemCount + " items"` encodes
+ English's one/other rule and nothing else. Arabic has six
+ categories (zero, one, two, few, many, other); Russian has three
+ (one, few, many); Polish has three with different boundaries. A
+ two-branch `if` is a C1 (Correctness) violation masquerading as a
+ shortcut — it returns a wrong answer for every non-English locale.
+- **Fix:** the count goes to ICU MessageFormat; the formatter
+ consults `Intl.PluralRules` for the active locale; the resource
+ carries the variant for that category. The code passes the count,
+ nothing more.
+
+ ```typescript
+ // The code passes the count; the resource + formatter pick the form.
+ t("checkout.cart.summary", { count: itemCount, price, date });
+ // Intl.PluralRules("ar-EG").select(1) === "one" -> "عنصر واحد"
+ // Intl.PluralRules("ar-EG").select(2) === "two" -> "عنصران"
+ // Intl.PluralRules("ar-EG").select(5) === "few" -> "٥ عناصر"
+ ```
+- See `domains/i18n/locale-resources.md` (Plural and Gender in
+ Resources) and `domains/i18n/formatting.md` (Plural Rules).
+
+### Hand-Rolled Currency and Date Formatting (i18n P5 Formatting is Locale-Aware)
+- `"$" + total.toFixed(2)` hardcodes the US dollar symbol, US
+ grouping (`,`), and US placement (symbol before the number). In
+ `de-DE` the euro formats as `"1.234,56 €"` (symbol after, dot
+ grouping). In `ar-EG` the pound formats as `"١٬٢٣٤٫٥٦ ج.م."`
+ (Arabic-Indic digits, different grouping).
+- `(d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear()`
+ produces `11/7/2024` — US `mm/dd/yyyy`. Most of the world reads
+ `dd/mm/yyyy`; ISO is `yyyy-mm-dd`. A hand-rolled date formatter
+ encodes one locale's convention and silently produces wrong output
+ for every other.
+- **Fix:** `Intl.NumberFormat` and `Intl.DateTimeFormat` with a BCP
+ 47 locale tag. CLDR is the source of truth; `Intl` is the runtime.
+
+ ```typescript
+ new Intl.NumberFormat("ar-EG", { style: "currency", currency: "EGP" })
+ .format(1234.56); // "١٬٢٣٤٫٥٦ ج.م."
+ new Intl.DateTimeFormat("ar-EG", { dateStyle: "medium" })
+ .format(new Date(date)); // "٧ نوفمبر ٢٠٢٤"
+ ```
+- See `domains/i18n/formatting.md` (the Intl surface, dates, numbers,
+ currencies) and `domains/i18n/first-principles.md` P5.
+
+### Source Language Treated as the Default (i18n P1 Source Language is a Locale)
+- The component has no resource layer at all for the source locale —
+ English is "just the strings in the code." When the first second
+ locale arrives, the fix is a rewrite (extract every string,
+ restructure every concatenation), not a patch. The source language
+ is `en-US`, a locale among many — it is not `null`.
+- **Fix:** extract source strings into `en-US.json` from day one,
+ even before a second locale exists. The resource layer is the
+ boundary from the first commit.
+- See `domains/i18n/first-principles.md` P1 and
+ `domains/uiux/copywriting.md`.
+
+## The Cascade
+
+The violations compound. Inline concatenation makes strings invisible
+to the extraction pipeline, so the translator never receives them as
+units — they reconstruct them by reading the code. Hand-rolled
+plurals return wrong answers for every non-English locale, so the
+Arabic build ships `"1 items"` for a single item. Hand-rolled
+formatting produces US-shaped output everywhere, so the price and
+date are wrong for `de-DE`, `ar-EG`, `zh-Hans-CN`, and every other
+locale. And because the first i18n test ran against real translations
+(a P8 violation — pseudo-locales should have surfaced all of this
+while the fix was still cheap), the defects are found after the
+string freeze, after the translator was paid, and after the release
+date was promised. The fix is now a re-translation and a re-release,
+not a commit.
+
+## Cross-Domain Links
+
+- `domains/i18n/locale-resources.md` — the resource layer this code
+ lacks; the standard formats (`.po`, JSON, Fluent, ICU Resource
+ Bundle) it should have used.
+- `domains/i18n/formatting.md` — the `Intl`/ICU/CLDR formatting this
+ code should call instead of hand-rolling.
+- `domains/i18n/first-principles.md` — P3, P4, P5, and P8 (pseudo-
+ locales test early).
+- `domains/uiux/copywriting.md` — copy lives in resources, not in
+ code.
+- `domains/api/error-responses.md` — the same parameterized-message
+ discipline applies to localized API errors.
\ No newline at end of file
diff --git a/examples/good/ai-ml-reproducibility.md b/examples/good/ai-ml-reproducibility.md
new file mode 100644
index 0000000..fa08125
--- /dev/null
+++ b/examples/good/ai-ml-reproducibility.md
@@ -0,0 +1,226 @@
+# Good Example: AI/ML Reproducible Training Run
+
+> A training run that follows Atelier's AI/ML principles. Each aspect
+> cites the principle it satisfies. Scope per D-023: this is
+> engineering discipline (reproducibility, versioning, lineage,
+> serving), **not** algorithm or model design — no architecture
+> choice, hyperparameter tuning, or model-family comparison appears
+> here.
+
+## The Run
+
+A training run `2026-08-05T09:12:00Z#run-42` produces model
+`registry/payments-fraud@sha256:b5e1...aa0`. Every input that shaped
+the model is pinned, named, and recoverable; the eval was declared
+before training; the model is an addressed artifact in a registry;
+the rollback path names the prior model and the prior dataset.
+
+### The Reproducibility Contract
+
+```yaml
+# lineage/run-42.yaml — the lineage root, committed alongside the code
+run_id: 2026-08-05T09:12:00Z#run-42
+dataset: s3://ml-data/train@sha256:7f3a...e21
+splits: dvc.yaml@commit a1b2c4d
+code: git@a1b2c4d
+config: configs/train.yaml@commit a1b2c4d
+environment: ghcr.io/org/train-img@sha256:9c2d...f88
+eval_spec: configs/eval.yaml@commit a1b2c4d
+model_digest: registry/payments-fraud@sha256:b5e1...aa0
+status: passed # eval gate passed -> eligible for promotion
+```
+
+- Lose any line and the run is anecdote, not evidence. The record is
+ the lineage root: a prediction cites the `model_digest`, which
+ cites this `run_id`, which cites everything above.
+
+### Data is Versioned (DVC, content-hashed)
+
+```ini
+# dvc.yaml — the split config is versioned in git, the data in the
+# content-addressed object store. Both are pinned by commit + hash.
+stages:
+ prepare:
+ cmd: python src/prepare.py --input data/raw --out data/splits
+ deps:
+ - data/raw
+ - src/prepare.py
+ outs:
+ - data/splits/train.parquet
+ - data/splits/val.parquet
+ - data/splits/test.parquet
+ # The dataset hash (sha256:7f3a...e21) is recorded in the lineage
+ # contract above. "s3://ml-data/latest" would be a P2 violation.
+```
+
+```bash
+# The dataset is pinned by content hash, not by a mutable path.
+$ dvc get s3://ml-data/train --rev sha256:7f3a...e21
+# The split is a deterministic function of (dataset version, split
+# config, random seed). Two runs on the same pinned inputs produce
+# the same splits.
+```
+
+### Code and Config are Versioned (git)
+
+```yaml
+# configs/train.yaml@commit a1b2c4d — versioned with the code
+# (No algorithm/hyperparameter content is illustrated here — this is
+# the engineering discipline of pinning the config, not the model
+# design inside it. Per D-023, algorithm choice is out of scope.)
+seed: 42
+splits:
+ train: data/splits/train.parquet
+ val: data/splits/val.parquet
+ test: data/splits/test.parquet # held out, never touched by training
+```
+
+### Environment is Pinned (container digest)
+
+```dockerfile
+# The training environment is an image addressed by digest, not :latest.
+# ghcr.io/org/train-img@sha256:9c2d...f88
+FROM python:3.11-slim
+# dependencies pinned in requirements.txt with hashes
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+```
+
+```text
+# requirements.txt — pinned + hash-pinned (pip-compile / pip-audit)
+dvc==3.50.2 \
+ --hash=sha256:1c8a...e7
+mlflow==2.16.0 \
+ --hash=sha256:9b2f...a1
+# No unpinned ranges. A rerun pulls the exact same wheels.
+```
+
+### Evaluation is Defined Before Training (P4)
+
+```yaml
+# configs/eval.yaml@commit a1b2c4d — committed BEFORE training runs.
+# The metrics, splits, and pass/fail thresholds are a-priori; they
+# are the contract the model must satisfy to leave the experiment.
+metrics:
+ - name: precision_at_threshold
+ threshold: ">= 0.92"
+ - name: recall_at_threshold
+ threshold: ">= 0.85"
+ - name: false_positive_rate
+ threshold: "<= 0.03"
+split: data/splits/test.parquet # held out, never in training
+gate: all_metrics_pass # AND of all thresholds; no cherry-pick
+# The eval schema equals the serving input contract (serving.md P8):
+# feature names, types, ranges match the production boundary exactly.
+```
+
+- Metrics chosen after seeing scores would be a P4 violation: the eval
+ would be rationalizing, not measuring. See
+ `domains/ai-ml/model-evaluation.md`.
+
+### The Model is a Versioned Artifact (MLflow registry)
+
+```bash
+# After the eval gate passes, the model is registered as an immutable
+# artifact addressed by digest, then promoted by stage.
+$ mlflow models register \
+ --name payments-fraud \
+ --model-uri runs:/run-42/model \
+ --description "run-42, dataset sha256:7f3a...e21, eval passed"
+# registry/payments-fraud@sha256:b5e1...aa0
+# Stages: None -> Staging -> Production. Promotion is a registry
+# operation, not a file copy. Never "latest".
+```
+
+### The Pipeline Composes (P9)
+
+```text
+# The training flow is a pipeline with explicit stages and contracts,
+# not a notebook. Each stage has named inputs and named outputs.
+prepare(dataset@hash) -> split(dvc.yaml) -> train(config, env@digest)
+ -> eval(eval.yaml, test@hash) -> [gate: pass] -> register(model@digest)
+ |
+ +-> [gate: fail] -> abort, no promote
+# A notebook in this path would be a P9 violation: implicit state,
+# human-dependent order, unreproducible.
+```
+
+## What Makes It Good
+
+### Reproducibility is First Class (AI/ML P1, C1, C5)
+- data + code + config + environment are all pinned. A second
+ engineer on a second laptop checks out commit `a1b2c4d`, pulls the
+ dataset by hash, pulls the image by digest, and reproduces the run
+ bit-for-bit. The run is reviewable because it is recreatable.
+- See `domains/ai-ml/first-principles.md` P1 and
+ `domains/devops/first-principles.md` P1 Reproducibility.
+
+### Data is Versioned, Not Just Code (AI/ML P2, C5, C7)
+- The dataset is `s3://ml-data/train@sha256:7f3a...e21`, not
+ `s3://ml-data/latest`. A model trained on "the data" is a model
+ trained on an unknown input — a C1 violation. DVC pins the data the
+ way git pins the code.
+- See `domains/ai-ml/data-versioning.md` (dataset hashing, the DVC /
+ Delta Lake / LakeFS comparison) and `domains/data/migrations.md`.
+
+### Lineage is Traceable End-to-End (AI/ML P3, C7, C1)
+- prediction → model → run-42 → dataset → source. Every edge is
+ named; no orphan model. A serving regression traces back to the
+ exact dataset and code that built the model, which is how drift is
+ diagnosed (data drift vs concept drift vs prediction drift).
+- See `domains/ai-ml/data-versioning.md` (lineage record) and
+ `domains/observability/logging.md`.
+
+### Evaluation Defined Before Training (AI/ML P4, C1, C2)
+- `eval.yaml` was committed before `train` ran. The gate is
+ `all_metrics_pass`; a failing metric aborts promotion. Cherry-
+ picking a metric post-hoc is a correctness violation — the eval
+ would no longer measure the model.
+- See `domains/ai-ml/model-evaluation.md` (eval-as-a-gate) and
+ `domains/testing/first-principles.md` (tests as specification).
+
+### Models are Versioned Artifacts (AI/ML P5, C5, C6)
+- The model is `registry/payments-fraud@sha256:b5e1...aa0`, promoted
+ Staging → Production. A serving endpoint that pulled `latest` would
+ be serving an unknown model with no rollback. The registry is to
+ models what a container registry is to images.
+- See `domains/ai-ml/serving.md` (the model is an addressed artifact)
+ and `domains/devops/first-principles.md` P7 Immutability.
+
+### Rollback Includes the Model (AI/ML P10, C5)
+- If production regresses, the rollback restores the prior model
+ digest `registry/payments-fraud@sha256:a1c4...f09` AND the prior
+ serving code. A rollback that redeploys old code but keeps the new
+ model has not rolled back — the model was the thing that regressed.
+- See `domains/ai-ml/serving.md` (Rollback Includes the Model) and
+ `domains/devops/first-principles.md` P4 Rollback First.
+
+## What This Example Does NOT Do (And Why That's Good)
+
+- Does **not** reference the dataset by a mutable path —
+ `s3://ml-data/latest` would be a P2 violation.
+- Does **not** choose metrics after seeing scores — that is a P4
+ violation (rationalizing, not measuring).
+- Does **not** pull `latest` from the model registry — that is a P5
+ violation (unknown model, no rollback).
+- Does **not** contain algorithm/architecture/hyperparameter content
+ — per D-023, those are research choices, not engineering
+ principles, and have no derivation in the core C-rules.
+- Does **not** run from a notebook — a notebook in the pipeline path
+ is a P9 violation (implicit state, unreproducible).
+
+## Cross-Domain Links
+
+- `domains/ai-ml/data-versioning.md` — the DVC pinning, the lineage
+ record, the tool comparison (DVC / Delta Lake / LakeFS).
+- `domains/ai-ml/serving.md` — the model is promoted as an addressed
+ artifact; the serving boundary validates inputs against the same
+ schema as the eval.
+- `domains/ai-ml/model-evaluation.md` — the eval-as-a-gate that this
+ run must pass before promotion.
+- `domains/devops/first-principles.md` P1 Reproducibility — the
+ non-negotiable this run inherits.
+- `domains/data/migrations.md` — data versioning parallels schema
+ migration discipline.
+- `domains/observability/logging.md` — the lineage record is a
+ structured, append-only log of provenance.
\ No newline at end of file
diff --git a/examples/good/gitops-pr.md b/examples/good/gitops-pr.md
new file mode 100644
index 0000000..4fdffc8
--- /dev/null
+++ b/examples/good/gitops-pr.md
@@ -0,0 +1,197 @@
+# Good Example: GitOps Pull Request
+
+> A pull request that changes ArgoCD Application manifests following
+> Atelier's GitOps + Operators principles. Each aspect cites the
+> principle it satisfies.
+
+## The PR
+
+A PR titled `promote payments-api 1.2.3 to prod` opened against the
+GitOps repo `platform/gitops`. It changes the `targetRevision` of the
+payments-api Application from `1.2.2` to `1.2.3`, adds a sync-wave
+annotation to a new migration Job, and tightens the AppProject's
+destination allow-list. CI runs plan/diff; nothing pushes to the
+cluster.
+
+### The Commit
+
+```yaml
+# manifests/prod/payments-api.yaml — the only file changed
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: payments-api
+ namespace: argocd
+ finalizers:
+ - resources-finalizer.argocd.argoproj.io
+spec:
+ source:
+ repoURL: https://git.example.com/platform/payments
+ targetRevision: 1.2.3 # was 1.2.2 — pinned, not latest
+ path: manifests/prod
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: payments
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=false
+ - PrunePropagationPolicy=foreground
+```
+
+```yaml
+# manifests/prod/payments-db-migration.yaml — new file, wave-ordered
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: payments-db-migrate-1.2.3
+ namespace: payments
+ annotations:
+ argocd.argoproj.io/sync-wave: "-1" # PreSync: run before the app
+spec:
+ backoffLimit: 0
+ ttlSecondsAfterFinished: 86400
+ template:
+ spec:
+ restartPolicy: OnFailure
+ serviceAccountName: payments-migrator
+ containers:
+ - name: migrate
+ image: registry.example.com/payments/migrate:1.2.3
+ args: ["up", "--to", "1.2.3"]
+```
+
+```yaml
+# projects/payments.yaml — AppProject tightened in the same PR
+apiVersion: argoproj.io/v1alpha1
+kind: AppProject
+metadata:
+ name: payments
+ namespace: argocd
+spec:
+ sourceRepos:
+ - https://git.example.com/platform/payments
+ destinations:
+ - server: https://kubernetes.default.svc
+ namespace: payments # prod-app can only deploy to payments ns
+ clusterResourceWhitelist:
+ - group: ""
+ kind: Namespace # allowed to create its own namespace
+ roles:
+ - name: payments-team
+ policies:
+ - p, proj:payments:payments-team, applications, sync, payments/*, allow
+```
+
+### The CI Pipeline (runs on the PR, before merge)
+
+```text
+# .github/workflows/gitops-plan.yml (illustrative steps)
+- name: validate manifests
+ run: argocd app manifests manifests/prod/ | kubeconform -strict
+
+- name: diff against live cluster (read-only, no apply)
+ run: argocd app diff payments-api --server $ARGOCD_SERVER --auth-token $READ_ONLY_TOKEN
+ # CI holds a READ-ONLY ArgoCD token. It never holds kubectl rights.
+ # A non-empty diff is the PR's proposed change, rendered for review.
+
+- name: opa gate (admission policy pre-check)
+ run: opa eval -i manifests/prod/ -d policies/ "data.k8s.admission.deny"
+ # Policy violations fail the PR before merge, not after deploy.
+```
+
+## What Makes It Good
+
+### Git is the Source of Truth (GitOps P1, C1 Correctness)
+- The promotion is a commit. The cluster's desired state is a
+ derivative of this repo; the repo is the authority. If the change is
+ wrong, `git revert` is the rollback — the recovery path is the
+ history.
+- See `domains/gitops-operators/first-principles.md` P1 and
+ `domains/gitops-operators/argocd.md` (Application CRD).
+
+### Pull, Don't Push (GitOps P3, C4 Locality)
+- CI holds a **read-only** ArgoCD token for `app diff`. It holds no
+ `kubectl` rights against the production cluster. The cluster's
+ ArgoCD controller pulls the merged commit; nothing pushes to the
+ cluster. A compromised CI token can read, not deploy.
+- See `domains/gitops-operators/argocd.md` (RBAC and SSO) and
+ `domains/gitops-operators/flux.md` for the same pull boundary from
+ the Flux side.
+
+### State is Immutable and Versioned (GitOps P5, C5 Reversibility)
+- `targetRevision: 1.2.3` — the Application pins a specific chart
+ revision, not `latest`. The commit that changed it is a permanent
+ record; `git revert` restores `1.2.2` and ArgoCD's `selfHeal`
+ converges the cluster back. No force-push; history is the audit
+ trail.
+- See `domains/gitops-operators/first-principles.md` P5 and
+ `domains/infrastructure-as-code/state.md` (State is Truth).
+
+### Sync Waves Order Correctness (GitOps P4, C1)
+- The migration Job carries `argocd.argoproj.io/sync-wave: "-1"` so
+ it runs in `PreSync` before the payments-api Deployment that
+ depends on the new schema. Wave ordering is a correctness
+ mechanism, not performance — the app starting before its migration
+ is a correctness bug.
+- See `domains/gitops-operators/argocd.md` (Sync Waves and Hooks).
+
+### Reconcile, Don't Mutate by Hand (GitOps P8)
+- `selfHeal: true` + `prune: true` means a hand-edited drift on a
+ managed resource is overwritten on the next loop. The fix for drift
+ is a new commit, not `kubectl edit`. The PR author does not SSH into
+ the cluster to "fix" anything.
+- See `domains/gitops-operators/argocd.md` (Diff and Drift) and
+ `domains/gitops-operators/first-principles.md` P8.
+
+### Least Privilege Reconciliation (GitOps P10, C8 Economy)
+- The AppProject `payments` restricts the Application to the
+ `payments` namespace and the `payments` repo. The controller's
+ ServiceAccount (not shown) is bound to a namespace-scoped Role, not
+ `cluster-admin`. The PR *tightens* the allow-list — least privilege
+ is a direction, not a one-time setting.
+- See `domains/gitops-operators/argocd.md` (RBAC and SSO) and
+ `domains/kubernetes/rbac.md`.
+
+### Policy is a Gate (Compliance P5, cross-link)
+- The `opa eval` step runs the admission policy against the proposed
+ manifests before merge. A violation fails the PR; the non-compliant
+ state is never realized. Detection is not enforcement; this is
+ enforcement.
+- See `domains/compliance/policy-as-code.md` and
+ `domains/devops/ci-cd.md`.
+
+### Failure is Observable (GitOps P9)
+- A sync failure or health degradation on `payments-api` emits
+ ArgoCD status (`Degraded` / `OutOfSync`) and a notification. Silent
+ drift is the bug; this PR does not disable notifications.
+- See `domains/gitops-operators/argocd.md` (Health and Status) and
+ `domains/observability/metrics.md`.
+
+## What This PR Does NOT Do (And Why That's Good)
+
+- Does **not** run `kubectl apply` from CI — that is the push pattern,
+ a P3 violation (see `examples/bad/` for the anti-pattern).
+- Does **not** use `argocd app set` as the steady state — the change
+ is in git, not in an imperative command's history.
+- Does **not** store raw Secrets in the GitOps repo — secrets arrive
+ via Sealed Secrets / SOPS / External Secrets, encrypted in git.
+- Does **not** float `targetRevision: latest` — the Application pins
+ a version; "latest" is an unknown model of the system.
+
+## Cross-Domain Links
+
+- `domains/gitops-operators/argocd.md` — the Application CRD, sync
+ waves, RBAC/AppProjects, and the pull model.
+- `domains/gitops-operators/flux.md` — the same PR pattern from the
+ Flux side (Kustomization CRD, per-cluster autonomy).
+- `domains/kubernetes/workloads.md` — the Deployment/Job the
+ Application reconciles.
+- `domains/kubernetes/rbac.md` — the ServiceAccount + Role the
+ controller and the migration Job run as.
+- `domains/compliance/policy-as-code.md` — the OPA gate is a
+ compliance-as-a-gate enforcement point.
+- `domains/devops/P4 Rollback First` — `git revert` is the rollback;
+ `selfHeal` is the convergence.
\ No newline at end of file