Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7dfd3cdc6c | |||
| c026c8930b |
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"phase": 1,
|
"phase": 2,
|
||||||
"stage": "complete",
|
"stage": "complete",
|
||||||
"milestone": "v0.3",
|
"milestone": "v0.3",
|
||||||
"phase_role": "execution",
|
"phase_role": "execution",
|
||||||
"project": "atelier",
|
"project": "atelier",
|
||||||
"attempts": 0,
|
"attempts": 0,
|
||||||
"updated_at": "2026-08-05T03:25:00Z",
|
"updated_at": "2026-08-05T03:35:00Z",
|
||||||
"phase_tag": "v0.2.1",
|
"phase_tag": "v0.2.2",
|
||||||
"release_id": 469
|
"release_id": 470
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ NFR milestone: no separate minor tag. The final patch (v0.1.5) IS the v0.2 deliv
|
|||||||
|-------|------|------|--------|------------------|
|
|-------|------|------|--------|------------------|
|
||||||
| 0 | Pre-Execution | docs | complete | Spec, clarify, research, ideate, plan, PERSONAS.md (extends platform-engineer, adds ml-engineer) — shipped v0.2.0 |
|
| 0 | Pre-Execution | docs | complete | Spec, clarify, research, ideate, plan, PERSONAS.md (extends platform-engineer, adds ml-engineer) — shipped v0.2.0 |
|
||||||
| 1 | GitOps + Operators Domain | docs | complete | domains/gitops-operators/{first-principles, argocd, flux, operators, progressive-delivery}.md — shipped v0.2.1 |
|
| 1 | GitOps + Operators Domain | docs | complete | domains/gitops-operators/{first-principles, argocd, flux, operators, progressive-delivery}.md — shipped v0.2.1 |
|
||||||
| 2 | AI/ML Domain | docs | pending | domains/ai-ml/{first-principles, data-versioning, model-evaluation, serving, monitoring-drift}.md |
|
| 2 | AI/ML Domain | docs | complete | domains/ai-ml/{first-principles, data-versioning, model-evaluation, serving, monitoring-drift}.md — shipped v0.2.2 |
|
||||||
| 3 | i18n + Compliance Domains | docs | pending | domains/i18n/{first-principles, locale-resources, formatting, rtl-bidi, testing-i18n}.md, domains/compliance/{first-principles, audit-logs, data-retention, policy-as-code, evidence}.md |
|
| 3 | i18n + Compliance Domains | docs | pending | domains/i18n/{first-principles, locale-resources, formatting, rtl-bidi, testing-i18n}.md, domains/compliance/{first-principles, audit-logs, data-retention, policy-as-code, evidence}.md |
|
||||||
| 4 | Matrix + Review Integration | docs | pending | matrix/principles-matrix.md (+40 mappings), matrix/domain-coverage.md (incl. C-rule coverage table update), review/{agent-checklist, peer-review-checklist, anti-patterns}.md, MANIFEST.md (+ examples/ listing per ATELIER-91) |
|
| 4 | Matrix + Review Integration | docs | pending | matrix/principles-matrix.md (+40 mappings), matrix/domain-coverage.md (incl. C-rule coverage table update), review/{agent-checklist, peer-review-checklist, anti-patterns}.md, MANIFEST.md (+ examples/ listing per ATELIER-91) |
|
||||||
| 5 | Examples + Cross-Links | docs | pending | examples/good + examples/bad for 4 domains, cross-links to devops/security/observability/data/k8s/iac |
|
| 5 | Examples + Cross-Links | docs | pending | examples/good + examples/bad for 4 domains, cross-links to devops/security/observability/data/k8s/iac |
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Audit Logs — Derived Rules
|
||||||
|
|
||||||
|
> Derives from `domains/compliance/first-principles.md`. Covers P1
|
||||||
|
> (Audit Logs are Append-Only), P2 (Every Significant Action is
|
||||||
|
> Logged), P7 (Identity is Attributable), P9 (Secrets Redacted in
|
||||||
|
> Audit), and P10 (Compliance Posture Observable). Referenced by
|
||||||
|
> `data-retention.md` (retention applies to audit logs themselves)
|
||||||
|
> and `evidence.md` (audit logs are evidence).
|
||||||
|
|
||||||
|
## Audit Logs are Append-Only (P1 Audit Logs are Append-Only)
|
||||||
|
|
||||||
|
- An audit record is immutable once written. The storage substrate
|
||||||
|
enforces this; policy alone does not. Write-once, append-only
|
||||||
|
sinks (WORM buckets, immutable log streams, hash-chained ledgers)
|
||||||
|
are the mechanism.
|
||||||
|
- Deletion or mutation of an audit record is itself an auditable
|
||||||
|
incident. The tampering is the signal, not just the underlying
|
||||||
|
event. A system that allows `DELETE FROM audit_log` is a system
|
||||||
|
whose audit log is a draft.
|
||||||
|
- The append-only guarantee is testable: attempt to write, then
|
||||||
|
attempt to overwrite, then attempt to delete. If the overwrite or
|
||||||
|
delete succeeds, the guarantee is absent and the design is a
|
||||||
|
violation.
|
||||||
|
|
||||||
|
## Structured Audit Events (P2 Every Significant Action is Logged)
|
||||||
|
|
||||||
|
- The set of auditable actions is defined a priori, in code, before
|
||||||
|
the action ships. The catalog is versioned and reviewed. An
|
||||||
|
auditable action with no log line is a violation, not a gap to
|
||||||
|
backfill later.
|
||||||
|
- Audit events are structured (JSON / protobuf / a typed schema),
|
||||||
|
not prose. A prose log line ("user logged in") is unqueryable and
|
||||||
|
unaggregatable; a structured event is both. The event schema is
|
||||||
|
the contract between the producer and the audit pipeline.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"timestamp": "2024-11-07T15:03:22Z",
|
||||||
|
"event": "auth.login",
|
||||||
|
"actor": { "kind": "user", "id": "u_8f3a", "session": "s_12b9" },
|
||||||
|
"action": "succeeded",
|
||||||
|
"target": { "kind": "service", "id": "billing-api" },
|
||||||
|
"source": { "ip": "203.0.113.42", "region": "us-east-1" },
|
||||||
|
"request_id": "req_91c2",
|
||||||
|
"version": "audit-schema/v2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- The catalog of significant actions typically includes:
|
||||||
|
authentication (success and failure), authorization decisions
|
||||||
|
(allow and deny), data access (read, write, delete), configuration
|
||||||
|
changes, policy changes, retention executions, and admin
|
||||||
|
operations. The exact set is declared per system; the discipline
|
||||||
|
is that it is declared.
|
||||||
|
|
||||||
|
## Cloud Audit Log Conventions (Prior Art, Abstracted)
|
||||||
|
|
||||||
|
- AWS CloudTrail, Google Cloud Audit Logs, and Azure Activity Log
|
||||||
|
share a common shape: immutable, time-ordered, queryable, with
|
||||||
|
actor / action / target / source / result fields. Atelier's
|
||||||
|
audit-logs doc adopts the shape, not the vendor.
|
||||||
|
- The shape is the contract; the sink is the implementation. A
|
||||||
|
self-hosted audit log that follows the same shape composes with
|
||||||
|
the same tooling (SIEM, query engines, evidence exporters) as the
|
||||||
|
cloud vendors'.
|
||||||
|
|
||||||
|
## Queryability (P10 Compliance Posture Observable)
|
||||||
|
|
||||||
|
- An audit log that cannot be queried is an audit log that cannot be
|
||||||
|
used. Queryability is a first-class design goal: the event schema
|
||||||
|
is typed, fields are indexed, and the common queries (who acted on
|
||||||
|
what when, what failed, what was denied) are cheap.
|
||||||
|
- "Who did X between T1 and T2" must be a single query, not a
|
||||||
|
forensics project. If the query requires a custom script per
|
||||||
|
investigation, the audit log is structured for storage, not for
|
||||||
|
use — a C7 (Observability) violation.
|
||||||
|
|
||||||
|
## Identity is Attributable (P7 Identity is Attributable)
|
||||||
|
|
||||||
|
- Every audit event records the authenticated principal that acted —
|
||||||
|
not a shared account, not a generic service, not "admin." The
|
||||||
|
actor field is populated at the time of the action from the
|
||||||
|
authenticated session, not resolved after the fact.
|
||||||
|
- A shared account in the actor field breaks accountability: an
|
||||||
|
event attributed to `svc-deploy` could be any of ten engineers.
|
||||||
|
This is the compliance angle on `domains/security/authorization.md`
|
||||||
|
and `domains/kubernetes/rbac.md`: bind actions to unique
|
||||||
|
principals, not to roles many can assume.
|
||||||
|
- Machine-to-machine actions record the workload identity (a service
|
||||||
|
account, a signed instance identity), not a human — but the
|
||||||
|
identity is still unique and attributable to a deployable unit.
|
||||||
|
|
||||||
|
## Redaction at the Boundary (P9 Secrets Redacted in Audit)
|
||||||
|
|
||||||
|
- Audit logs must not leak secrets, credentials, tokens, or PII.
|
||||||
|
Redaction is structural: applied at the logging boundary, before
|
||||||
|
the record is written to the append-only sink — not opportunistic
|
||||||
|
scrubbing after the fact. Once a secret is in an append-only log,
|
||||||
|
the remediation is expensive (rotate, rewrite access scope), so
|
||||||
|
redaction-at-source is the only sound position.
|
||||||
|
- The redaction policy is itself auditable: which fields are
|
||||||
|
redacted, by what rule, in which event type. A redaction rule
|
||||||
|
that lives in someone's head is a P9 violation waiting to happen.
|
||||||
|
|
||||||
|
```
|
||||||
|
// before redaction (DO NOT LOG)
|
||||||
|
{
|
||||||
|
"event": "config.read",
|
||||||
|
"target": { "kind": "secret", "id": "db-password" },
|
||||||
|
"value": "p@ssw0rd-plaintext-leaked" // VIOLATION
|
||||||
|
}
|
||||||
|
// after structural redaction
|
||||||
|
{
|
||||||
|
"event": "config.read",
|
||||||
|
"target": { "kind": "secret", "id": "db-password" },
|
||||||
|
"value": "[REDACTED:secret]",
|
||||||
|
"redaction": "secret-value-policy/v1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Never log request bodies, response bodies, headers like
|
||||||
|
`Authorization`, or environment variables that may carry secrets.
|
||||||
|
Log the *fact* of the action, not the *content* of the secret.
|
||||||
|
|
||||||
|
## Retention of Audit Logs Themselves
|
||||||
|
|
||||||
|
- Audit logs are subject to retention policy (cross `data-
|
||||||
|
retention.md`), but the floor is set by the accountability need,
|
||||||
|
not by storage economy. An audit log deleted before its retention
|
||||||
|
period is a P1 violation dressed as a P3 action.
|
||||||
|
- The retention rule for audit logs is itself logged (meta-audit):
|
||||||
|
when an audit log segment ages out and is deleted, the deletion is
|
||||||
|
recorded in a higher-tier audit log with the rule that authorized
|
||||||
|
it. The chain is observable end to end.
|
||||||
|
|
||||||
|
## What Violates Audit-Log Discipline
|
||||||
|
|
||||||
|
| Violation | Principle |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Audit log on a mutable filesystem with no write-once protection | P1 Audit Logs are Append-Only |
|
||||||
|
| `DELETE FROM audit_log WHERE timestamp < ...` as routine cleanup | P1 Audit Logs are Append-Only |
|
||||||
|
| An auth-success event with no audit record | P2 Every Significant Action is Logged |
|
||||||
|
| A prose log line ("user did a thing") instead of a structured event | P2 Every Significant Action is Logged |
|
||||||
|
| A shared `admin` account as the actor in audit events | P7 Identity is Attributable |
|
||||||
|
| An `Authorization: Bearer <token>` header logged in plaintext | P9 Secrets and Sensitive Data are Redacted in Audit |
|
||||||
|
| A redaction rule applied inconsistently across event types | P9 Secrets and Sensitive Data are Redacted in Audit |
|
||||||
|
| "Who did X?" requires a custom forensics script per investigation | P10 Compliance Posture is Observable |
|
||||||
|
| An audit log segment deleted with no meta-audit record | P1 Audit Logs are Append-Only |
|
||||||
|
|
||||||
|
## Relationship to Other Domains
|
||||||
|
|
||||||
|
- `domains/observability/logging.md` — audit logs are structured
|
||||||
|
logging with an append-only guarantee; the logging primitives
|
||||||
|
(levels, structured fields, correlation IDs) compose here.
|
||||||
|
- `domains/security/authorization.md` — the actor in an audit event
|
||||||
|
is the principal the authorization layer authenticated.
|
||||||
|
- `domains/security/secrets.md` — redaction at the logging boundary
|
||||||
|
is the audit-side complement of secret management.
|
||||||
|
- `domains/compliance/data-retention.md` — retention policy applies
|
||||||
|
to audit logs; the audit log's own deletion is meta-audited.
|
||||||
|
- `domains/compliance/evidence.md` — audit logs are a primary
|
||||||
|
evidence artifact; the append-only guarantee is what makes them
|
||||||
|
admissible.
|
||||||
|
- `domains/kubernetes/rbac.md` — workload identity in audit events
|
||||||
|
derives from the RBAC principal that acted.
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# Data Retention — Derived Rules
|
||||||
|
|
||||||
|
> Derives from `domains/compliance/first-principles.md`. Covers P3
|
||||||
|
> (Retention is Policy, Not Storage) and the data-shape angle on P8
|
||||||
|
> (Subject Access is Honored). Referenced by `audit-logs.md`
|
||||||
|
> (retention applies to audit logs) and `evidence.md` (evidence has
|
||||||
|
> a retention lifecycle). Framework-agnostic per D-024 — no
|
||||||
|
> regulation-specific retention periods.
|
||||||
|
|
||||||
|
## Retention is Policy, Not Storage (P3 Retention is Policy, Not Storage)
|
||||||
|
|
||||||
|
- Data lifetime is declared and enforced as policy, in code — not
|
||||||
|
left to the storage layer's defaults. The policy names what data
|
||||||
|
class is retained for how long, what action fires at end-of-life
|
||||||
|
(delete, archive, anonymize), and what exception path exists (a
|
||||||
|
legal hold suspends deletion).
|
||||||
|
- Deletion at end-of-life is a feature, not a failure. A system that
|
||||||
|
cannot delete on schedule is a system that over-retains, which is
|
||||||
|
the symmetric violation of a system that under-retains. Both are
|
||||||
|
P3 violations; the policy is the arbiter.
|
||||||
|
- "We kept it because the bucket was cheap" is a violation. "We
|
||||||
|
deleted it because the policy said to" is correct. Cost does not
|
||||||
|
override policy; policy is the contract.
|
||||||
|
|
||||||
|
## Retention Policy as Code
|
||||||
|
|
||||||
|
- Retention rules live as code: lifecycle rules on the storage
|
||||||
|
layer, scheduled deletion jobs, tiered storage transitions, and
|
||||||
|
anonymization transforms. The code is versioned, reviewed, and
|
||||||
|
auditable. A retention rule in a spreadsheet is a wishlist; the
|
||||||
|
same rule in a reviewed, deployable lifecycle policy is a control.
|
||||||
|
|
||||||
|
```
|
||||||
|
// illustrative lifecycle policy (abstracted, no vendor DSL)
|
||||||
|
// object-storage lifecycle
|
||||||
|
{
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"name": "user-events-90d",
|
||||||
|
"match": { "prefix": "events/" },
|
||||||
|
"transitions": [
|
||||||
|
{ "after": "30d", "to": "tier-cold" },
|
||||||
|
{ "after": "90d", "action": "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "audit-log-7y",
|
||||||
|
"match": { "prefix": "audit/" },
|
||||||
|
"transitions": [
|
||||||
|
{ "after": "365d", "to": "tier-archive" },
|
||||||
|
{ "after": "2555d", "action": "delete" }
|
||||||
|
],
|
||||||
|
"legal_hold": "suspends-action"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- The retention policy is itself auditable: which rule fired when,
|
||||||
|
against which objects, with what result. The deletion events are
|
||||||
|
logged (`audit-logs.md`) — deletion is a significant action.
|
||||||
|
|
||||||
|
## Retention vs. Backup — The Distinction
|
||||||
|
|
||||||
|
- A **backup** is a recovery mechanism: it exists to restore data
|
||||||
|
after loss. A **retention rule** is a deletion mechanism: it
|
||||||
|
exists to remove data at end-of-life. Conflating them produces
|
||||||
|
data that survives both the deletion policy and the disaster —
|
||||||
|
which is the opposite of compliance.
|
||||||
|
- A backup is governed by a recovery-point / recovery-time objective;
|
||||||
|
a retention rule is governed by a lifetime. They are independent
|
||||||
|
contracts. A backup that is also the retention store is a store
|
||||||
|
where nothing is ever deleted, which is a P3 violation.
|
||||||
|
- A legal hold suspends retention deletion for a defined data set
|
||||||
|
(e.g. data under investigation). The hold is itself a policy
|
||||||
|
action, auditable and time-bounded, not a manual override.
|
||||||
|
|
||||||
|
## Retention is Distinct per Data Class
|
||||||
|
|
||||||
|
- Different data classes have different lifetimes. The retention
|
||||||
|
policy enumerates the classes and their rules; it does not apply
|
||||||
|
one number to everything. Typical classes (the names are
|
||||||
|
abstract; the periods are policy decisions, not regulation-
|
||||||
|
specific):
|
||||||
|
- **Audit logs** — long, often multi-year, governed by
|
||||||
|
accountability needs (`audit-logs.md`).
|
||||||
|
- **User-generated content** — tied to the user's account
|
||||||
|
lifetime; deletion follows account deletion (cross P8 Subject
|
||||||
|
Access).
|
||||||
|
- **Telemetry / metrics** — short, governed by observability need
|
||||||
|
(`domains/observability/metrics.md`); high-resolution data ages
|
||||||
|
to downsampled aggregates.
|
||||||
|
- **Evidence artifacts** — tied to the audit cycle
|
||||||
|
(`evidence.md`); the cycle ends, the evidence ages out.
|
||||||
|
- A single retention rule for "all data" is a C3 (Simplicity)
|
||||||
|
violation of the wrong kind: it is simpler than the requirement
|
||||||
|
allows.
|
||||||
|
|
||||||
|
## Subject Access is Honored (P8 Subject Access is Honored)
|
||||||
|
|
||||||
|
- Data-subject rights — access (what do we have on this subject),
|
||||||
|
export (in a portable form), deletion (and prove it), correction
|
||||||
|
— are operations with defined contracts and audit trails, not
|
||||||
|
ad-hoc tickets. The system implements them as first-class
|
||||||
|
operations; a subject-access request that requires a forensics
|
||||||
|
team is a correctness defect.
|
||||||
|
- Retention and subject access interact at deletion: a subject
|
||||||
|
deletion request fires the deletion policy for that subject's
|
||||||
|
data, the deletion is audited, and the proof of deletion is
|
||||||
|
returned to the subject (and recorded). A subject deletion that
|
||||||
|
skips the audit is a P8 violation dressed as a P3 success.
|
||||||
|
- Cross `domains/data/schema-design.md`: subject access is only
|
||||||
|
computable if the schema tags which records belong to which
|
||||||
|
subject. A schema with no subject linkage cannot honor a subject
|
||||||
|
request — it cannot find the data to delete.
|
||||||
|
|
||||||
|
## Retention Migration Discipline
|
||||||
|
|
||||||
|
- Retention rules change. When the policy changes (a class's
|
||||||
|
lifetime shortens or lengthens), the change is a migration: the
|
||||||
|
new rule applies to data ingested after the cutover, and a
|
||||||
|
backfill applies the new rule to existing data where applicable.
|
||||||
|
Cross `domains/data/migrations.md` for the schema-lifecycle
|
||||||
|
discipline this mirrors.
|
||||||
|
- A retention rule change that is not versioned, not reviewed, and
|
||||||
|
not backfilled is a P3 violation: the policy is not actually the
|
||||||
|
policy if the storage layer does not reflect it.
|
||||||
|
|
||||||
|
## What Violates Retention Discipline
|
||||||
|
|
||||||
|
| Violation | Principle |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Data kept indefinitely because "storage is cheap" | P3 Retention is Policy, Not Storage |
|
||||||
|
| A retention rule in a spreadsheet, not in code | P3 Retention is Policy, Not Storage |
|
||||||
|
| A backup bucket used as the retention store (nothing ever deletes) | P3 Retention is Policy, Not Storage |
|
||||||
|
| A single retention period applied to all data classes | P3 Retention is Policy, Not Storage |
|
||||||
|
| A subject deletion with no audit record of the deletion | P8 Subject Access is Honored |
|
||||||
|
| A subject-access request that requires a forensics team | P8 Subject Access is Honored |
|
||||||
|
| A schema with no subject linkage (cannot find data to delete) | P8 Subject Access is Honored |
|
||||||
|
| A legal hold applied ad hoc, not as a policy action | P3 Retention is Policy, Not Storage |
|
||||||
|
| A retention rule change with no backfill to existing data | P3 Retention is Policy, Not Storage |
|
||||||
|
|
||||||
|
## Relationship to Other Domains
|
||||||
|
|
||||||
|
- `domains/data/schema-design.md` — retention requires the schema
|
||||||
|
to tag data class and subject linkage; subject access is only
|
||||||
|
computable over a schema that supports it.
|
||||||
|
- `domains/data/migrations.md` — retention rule changes are
|
||||||
|
migrations; the discipline (version, review, backfill) mirrors
|
||||||
|
schema migrations.
|
||||||
|
- `domains/compliance/audit-logs.md` — audit logs have their own
|
||||||
|
retention floor; deletion of an audit segment is meta-audited.
|
||||||
|
- `domains/compliance/evidence.md` — evidence artifacts have a
|
||||||
|
retention lifecycle tied to the audit cycle.
|
||||||
|
- `domains/observability/metrics.md` — telemetry retention is
|
||||||
|
governed by observability need; high-res data ages to aggregates.
|
||||||
|
- `domains/security/secrets.md` — secrets have a retention lifecycle
|
||||||
|
tied to rotation; a secret past its rotation date is overdue, not
|
||||||
|
retained.
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# Evidence — Derived Rules
|
||||||
|
|
||||||
|
> Derives from `domains/compliance/first-principles.md`. Covers P6
|
||||||
|
> (Evidence is Collected Continuously), P5 (Policy is a Gate, so
|
||||||
|
> decisions are evidence), P7 (Identity Attributable, so evidence
|
||||||
|
> has provenance), and P10 (Posture Observable, so evidence is
|
||||||
|
> queryable). Referenced by `audit-logs.md` (logs are evidence)
|
||||||
|
> and `data-retention.md` (evidence has a lifecycle).
|
||||||
|
|
||||||
|
## Evidence is Collected Continuously (P6 Evidence is Collected Continuously)
|
||||||
|
|
||||||
|
- Evidence of compliance — logs, configs, scans, attestations,
|
||||||
|
policy decisions, access reviews — is gathered as a byproduct of
|
||||||
|
operation, not assembled manually at audit time. The audit-time
|
||||||
|
scramble is the anti-pattern: it is expensive, it is incomplete,
|
||||||
|
and it produces evidence that is reconstructed rather than
|
||||||
|
recorded.
|
||||||
|
- Continuous evidence collection means the audit packet is a query
|
||||||
|
over already-collected artifacts, not a forensic reconstruction.
|
||||||
|
The auditor asks "show me the access reviews for Q3" and the
|
||||||
|
answer is a query against the evidence store, not a six-week
|
||||||
|
project.
|
||||||
|
- This is the compliance angle on `domains/observability/tracing.md`
|
||||||
|
for distributed evidence (a trace spans the request that produced
|
||||||
|
the evidence) and `domains/observability/metrics.md` for posture
|
||||||
|
signals (a metric is a continuous evidence stream).
|
||||||
|
|
||||||
|
## Evidence is a Byproduct, Not a Deliverable
|
||||||
|
|
||||||
|
- Evidence collected as a byproduct is trustworthy: it records what
|
||||||
|
happened, when it happened, recorded by the system that did it.
|
||||||
|
Evidence assembled at audit time is less trustworthy: it records
|
||||||
|
what someone remembered to write down, when they wrote it, after
|
||||||
|
the fact.
|
||||||
|
- The mechanism: every significant action (`audit-logs.md`) emits
|
||||||
|
its record to an evidence store; every policy decision
|
||||||
|
(`policy-as-code.md`) emits its decision; every deployment emits
|
||||||
|
its signed attestation; every access review emits its result. The
|
||||||
|
store is append-only (`audit-logs.md` P1), queryable (P10), and
|
||||||
|
retention-bound (`data-retention.md`).
|
||||||
|
|
||||||
|
## Provenance and Identity (P7 Identity is Attributable)
|
||||||
|
|
||||||
|
- Evidence has provenance: which system produced it, when, from what
|
||||||
|
input. An evidence artifact with no provenance is anecdote, not
|
||||||
|
evidence — it cannot be attributed to a source, so it cannot be
|
||||||
|
trusted.
|
||||||
|
- Provenance includes the identity of the producer (a workload
|
||||||
|
identity, a service account) and the chain of custody (who has
|
||||||
|
had access to the artifact since it was produced). Cross
|
||||||
|
`domains/security/authorization.md`: the producer's identity is
|
||||||
|
authenticated, not assumed.
|
||||||
|
|
||||||
|
## Signed Attestations (IDEATE-29)
|
||||||
|
|
||||||
|
- A signed attestation is evidence with a cryptographic signature
|
||||||
|
binding the artifact to its producer. The signature is the
|
||||||
|
provenance: it can be verified independently of the producer, and
|
||||||
|
it cannot be forged without the producer's key. Cross
|
||||||
|
`domains/security/supply-chain.md` for the supply-chain angle.
|
||||||
|
- Cosign (Sigstore) and in-toto are the canonical patterns: a
|
||||||
|
builder signs an artifact (container image, deployable, evidence
|
||||||
|
bundle) at production time; a verifier checks the signature at
|
||||||
|
consumption time. The signature is the evidence that the artifact
|
||||||
|
came from where it claims to have come from.
|
||||||
|
|
||||||
|
- **Illustrative signed attestation (Cosign / Sigstore format, NOT
|
||||||
|
a real signature — illustrative only, no live keys):**
|
||||||
|
|
||||||
|
```
|
||||||
|
// Cosign attest — bind an attestation to an image digest
|
||||||
|
// (illustrative; not a real signature)
|
||||||
|
$ cosign attest --type spdxjson \
|
||||||
|
--predicate sbom.spdx.json \
|
||||||
|
my-registry/app@sha256:5a3e1c...f9b2
|
||||||
|
|
||||||
|
// The attestation is stored as a signature in the registry,
|
||||||
|
// bound to the image digest. The payload is a DSSE envelope:
|
||||||
|
|
||||||
|
{
|
||||||
|
"payloadType": "application/vnd.in-toto+json",
|
||||||
|
"payload": "eyJfdHlwZSI6ImF0dGVzdGF0aW9uIn0...",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"sig": "MEUCIQDx...illustrative-base64-signature...==",
|
||||||
|
"keyid": "cosign-key-2024-q4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// The decoded payload (an in-toto statement binding the
|
||||||
|
// attestation to the image digest):
|
||||||
|
{
|
||||||
|
"_type": "https://in-toto.io/Statement/v0.1",
|
||||||
|
"predicateType": "https://spdx.dev/Document",
|
||||||
|
"subject": [
|
||||||
|
{
|
||||||
|
"name": "my-registry/app",
|
||||||
|
"digest": { "sha256": "5a3e1c...f9b2" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"predicate": {
|
||||||
|
"SPDXID": "SPDXRef-DOCUMENT",
|
||||||
|
"creationInfo": {
|
||||||
|
"created": "2024-11-07T15:03:22Z",
|
||||||
|
"creators": ["Tool: atelier-build-pipeline"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verification (independent of the producer):
|
||||||
|
$ cosign verify-attestation --type spdxjson \
|
||||||
|
--certificate-identity-regexp '.*atelier-build.*' \
|
||||||
|
my-registry/app@sha256:5a3e1c...f9b2
|
||||||
|
// Verification succeeded for: my-registry/app@sha256:5a3e1c...f9b2
|
||||||
|
// SBOM attestation found for subject
|
||||||
|
```
|
||||||
|
|
||||||
|
- The attestation is illustrative — the signatures and digests are
|
||||||
|
not real. The shape (DSSE envelope, in-toto statement, subject +
|
||||||
|
predicate, verify-by-identity) is what evidence-as-attestation
|
||||||
|
looks like. A real attestation carries a real signature from a
|
||||||
|
real key held by the builder.
|
||||||
|
|
||||||
|
## Audit-Ready Export
|
||||||
|
|
||||||
|
- The evidence store is queryable at any time, not only at audit
|
||||||
|
time. The audit packet is a query (a date range, a data class, a
|
||||||
|
subject) over the store; the export is a dump of the matching
|
||||||
|
artifacts with their provenance and signatures.
|
||||||
|
- An audit-ready export that requires six weeks of forensics is a
|
||||||
|
P6 violation dressed as a success: the evidence was not collected
|
||||||
|
continuously, it was reconstructed. The export should be a query
|
||||||
|
that runs in minutes, not a project that runs for weeks.
|
||||||
|
|
||||||
|
## Evidence Lifecycle
|
||||||
|
|
||||||
|
- Evidence has a retention lifecycle (`data-retention.md`): an
|
||||||
|
evidence artifact is retained for the audit cycle it supports,
|
||||||
|
then ages out. The retention rule for evidence is itself audited
|
||||||
|
(deletion of evidence is a meta-audited action, like deletion of
|
||||||
|
audit logs).
|
||||||
|
- A legal hold suspends evidence deletion for a defined set — the
|
||||||
|
same mechanism as audit-log holds.
|
||||||
|
|
||||||
|
## What Violates Evidence Discipline
|
||||||
|
|
||||||
|
| Violation | Principle |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Evidence assembled by hand the week before an audit | P6 Evidence is Collected Continuously |
|
||||||
|
| An evidence artifact with no provenance (no producer, no timestamp) | P7 Identity is Attributable |
|
||||||
|
| An audit packet that requires six weeks of forensics to produce | P6 Evidence is Collected Continuously |
|
||||||
|
| An attestation with no signature (provenance asserted, not proven) | P7 Identity is Attributable |
|
||||||
|
| Evidence store not queryable between audits | P10 Compliance Posture is Observable |
|
||||||
|
| Evidence deleted before its retention period with no meta-audit | P6 Evidence is Collected Continuously |
|
||||||
|
| Policy decisions not recorded as evidence | P5 Policy is Evaluated as a Gate |
|
||||||
|
| A deployment with no signed attestation of its build provenance | P7 Identity is Attributable |
|
||||||
|
|
||||||
|
## Relationship to Other Domains
|
||||||
|
|
||||||
|
- `domains/security/supply-chain.md` — signed attestations are the
|
||||||
|
supply-chain integrity primitive; evidence.md is the compliance
|
||||||
|
consumer of the same artifact.
|
||||||
|
- `domains/observability/metrics.md` — posture metrics are a
|
||||||
|
continuous evidence stream.
|
||||||
|
- `domains/observability/tracing.md` — distributed traces provide
|
||||||
|
evidence that spans a request across services.
|
||||||
|
- `domains/compliance/audit-logs.md` — audit logs are a primary
|
||||||
|
evidence artifact; the append-only guarantee is what makes them
|
||||||
|
admissible.
|
||||||
|
- `domains/compliance/policy-as-code.md` — policy decisions are
|
||||||
|
evidence of enforcement; the policy code itself is evidence of
|
||||||
|
the rule.
|
||||||
|
- `domains/compliance/data-retention.md` — evidence has a retention
|
||||||
|
lifecycle tied to the audit cycle.
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# Compliance — First Principles
|
||||||
|
|
||||||
|
> Framework-agnostic per D-024. These principles derive from core
|
||||||
|
> Security (a subset of C1 Correctness), Observability, and
|
||||||
|
> Reversibility. They apply across regulations — NIST CSF, SOC 2,
|
||||||
|
> GDPR, CCPA, HIPAA, PCI-DSS, ISO 27001 — without prescribing any
|
||||||
|
> regulation-specific implementation. Regulation names appear here
|
||||||
|
> only as examples of what the principles support; the principles
|
||||||
|
> themselves are engineering rules, not legal controls.
|
||||||
|
|
||||||
|
## 1. The Principles
|
||||||
|
|
||||||
|
### P1. Audit Logs are Append-Only
|
||||||
|
Audit records are immutable once written. Deletion or mutation of an
|
||||||
|
audit record is itself an auditable incident — the tampering is the
|
||||||
|
signal, not just the underlying event. An audit log that can be edited
|
||||||
|
is not an audit log; it is a draft. Append-only is enforced
|
||||||
|
structurally (write-once storage, immutable buckets, hash-chained
|
||||||
|
records), not by policy alone. This is the compliance angle on
|
||||||
|
`domains/observability/logging.md`: structured logs that cannot be
|
||||||
|
rewritten are the substrate of accountability.
|
||||||
|
|
||||||
|
### P2. Every Significant Action is Logged
|
||||||
|
The set of auditable actions is defined a priori, in code, before the
|
||||||
|
action ships — not retrofitted after an incident. Authentication
|
||||||
|
changes, authorization decisions, data access, configuration changes,
|
||||||
|
policy changes, and deletions are all significant. "We forgot to log
|
||||||
|
it" is a violation, not an excuse. The auditable-action catalog is
|
||||||
|
itself versioned and reviewed. A significant action with no log line
|
||||||
|
is a C7 (Observability) defect and a C1 (Correctness) defect: the
|
||||||
|
system's behavior is invisible, and accountability is impossible.
|
||||||
|
|
||||||
|
### P3. Retention is Policy, Not Storage
|
||||||
|
Data lifetime is declared and enforced as policy, not left to the
|
||||||
|
storage layer's defaults. Deletion at end-of-life is a feature, not a
|
||||||
|
failure. Retention rules live as code (lifecycle rules, scheduled
|
||||||
|
deletion jobs, tiered storage transitions), they are reviewed, and
|
||||||
|
they are auditable. "We kept it because the bucket was cheap" is a
|
||||||
|
violation; "we deleted it because the policy said to" is correct.
|
||||||
|
Retention is distinct from backup: a backup is a recovery mechanism,
|
||||||
|
a retention rule is a deletion mechanism. Keeping them conflated
|
||||||
|
produces data that survives both the deletion policy and the
|
||||||
|
disaster — which is the opposite of compliance. Cross
|
||||||
|
`domains/data/migrations.md` for the schema-lifecycle discipline.
|
||||||
|
|
||||||
|
### P4. Policy is Code
|
||||||
|
Compliance policy is expressed in versioned, reviewable, testable
|
||||||
|
code (OPA / Rego, AWS Cedar, HashiCorp Sentinel, Kyverno) — not in
|
||||||
|
spreadsheets, prose documents, or tribal knowledge. Policy in a
|
||||||
|
spreadsheet is untestable, unreviewable, and undeployable; it is a
|
||||||
|
wishlist, not a control. Policy-as-code inherits the disciplines of
|
||||||
|
`domains/infrastructure-as-code/P1 Declarative Intent`: declarative
|
||||||
|
intent, version control, review before merge, plan before apply. A
|
||||||
|
compliance rule that is not executable is a rule that cannot be
|
||||||
|
enforced, which is a rule that does not exist.
|
||||||
|
|
||||||
|
### P5. Policy is Evaluated as a Gate
|
||||||
|
Policy violations block before the action, not after the audit.
|
||||||
|
Enforcement happens at admission time (kubernetes admission), at
|
||||||
|
pipeline time (CI/CD gates), and at provisioning time (IaC plan
|
||||||
|
gates) — before the non-compliant state is realized. Detecting a
|
||||||
|
violation after it ships is detection, not enforcement. A policy that
|
||||||
|
is "logged but not blocked" is a postcard, not a gate. This is the
|
||||||
|
compliance angle on C5 (Reversibility): a blocked action is
|
||||||
|
reversible by construction; a shipped violation requires remediation,
|
||||||
|
which is more expensive than prevention.
|
||||||
|
|
||||||
|
### P6. Evidence is Collected Continuously
|
||||||
|
Evidence of compliance — logs, configs, scans, attestations, policy
|
||||||
|
decisions, access reviews — is gathered as a byproduct of operation,
|
||||||
|
not assembled manually at audit time. The audit-time scramble is the
|
||||||
|
anti-pattern: it is expensive, it is incomplete, and it produces
|
||||||
|
evidence that is reconstructed rather than recorded. Continuous
|
||||||
|
evidence collection means the audit packet is a query over
|
||||||
|
already-collected artifacts, not a forensic reconstruction. This is
|
||||||
|
the compliance angle on `domains/observability/tracing.md` for
|
||||||
|
distributed evidence and `domains/observability/metrics.md` for
|
||||||
|
posture signals.
|
||||||
|
|
||||||
|
### P7. Identity is Attributable
|
||||||
|
Every logged action traces to an authenticated, non-shared principal.
|
||||||
|
Shared accounts, generic service identities, and "admin" as an actor
|
||||||
|
are violations: an action with no attributable human or workload is
|
||||||
|
an action with no accountability. Identity is recorded in the audit
|
||||||
|
record at the time of the action, not resolved after the fact. This
|
||||||
|
is the compliance angle on `domains/security/authorization.md` and
|
||||||
|
`domains/kubernetes/rbac.md`: the audit subject must be the principal
|
||||||
|
that acted, not a role that many can assume.
|
||||||
|
|
||||||
|
### P8. Subject Access is Honored
|
||||||
|
Data-subject rights — access, export, deletion, correction — are
|
||||||
|
operations with defined contracts and audit trails, not ad-hoc
|
||||||
|
tickets. The system can answer "what do we have on this subject,"
|
||||||
|
"export it in a portable form," and "delete it and prove the
|
||||||
|
deletion" as first-class operations. These are not features bolted on
|
||||||
|
at the end; they are contracts the data layer implements from the
|
||||||
|
start. A subject-access request that requires a forensics team is a
|
||||||
|
correctness defect: the system does not know what it holds. Cross
|
||||||
|
`domains/data/schema-design.md` for the data shapes that make
|
||||||
|
subject access computable.
|
||||||
|
|
||||||
|
### P9. Secrets and Sensitive Data are Redacted in Audit
|
||||||
|
Audit logs themselves must not leak secrets, credentials, PII, or
|
||||||
|
other sensitive data. Redaction is structural — applied at the
|
||||||
|
logging boundary, before the record is written — not opportunistic
|
||||||
|
scrubbing after the fact. A secret that appears in an audit log is a
|
||||||
|
C1 (Correctness) violation (the log is now a secret store) and a
|
||||||
|
security violation (`domains/security/secrets.md`). The redaction
|
||||||
|
policy is itself auditable: which fields are redacted, by what rule,
|
||||||
|
in which log stream. Once a secret is in an append-only log, the
|
||||||
|
remediation is expensive — rotate the secret and rewrite the log's
|
||||||
|
access scope — so redaction-at-source is the only sound position.
|
||||||
|
|
||||||
|
### P10. Compliance Posture is Observable
|
||||||
|
The system reports its own compliance state: drift from policy, open
|
||||||
|
violations, retention status, evidence freshness, policy-evaluation
|
||||||
|
counts. Silent non-compliance is the bug. A compliance posture
|
||||||
|
metric is a first-class signal (`domains/observability/metrics.md`),
|
||||||
|
alertable, and dashboarded. "We didn't know we were non-compliant"
|
||||||
|
is not a defense; it is a C7 (Observability) defect. The posture is
|
||||||
|
queryable at any time, not only at audit time. This is the compliance
|
||||||
|
angle on `domains/infrastructure-as-code/P3 State is Truth`: the
|
||||||
|
compliance state is a versioned, queryable truth, not a vibe.
|
||||||
|
|
||||||
|
## 2. Core Principle Trace
|
||||||
|
|
||||||
|
Each compliance P-rule derives from one or more core C-rules (C1–C8).
|
||||||
|
The matrix extension lands in P4 of the v0.3 plan; the traces below
|
||||||
|
are authoritative.
|
||||||
|
|
||||||
|
| P-rule | Core | Why |
|
||||||
|
|--------|------|-----|
|
||||||
|
| P1 Audit Logs are Append-Only | C1, C5 | Correctness of the record; reversibility of tamper detection |
|
||||||
|
| P2 Every Significant Action is Logged | C7, C1 | Observability of behavior; correctness of a-priori audit scope |
|
||||||
|
| P3 Retention is Policy, Not Storage | C5, C8 | Reversibility of data lifetime; economy of storage as policy |
|
||||||
|
| P4 Policy is Code | C6, C2 | Composability of versioned policy; clarity of executable intent |
|
||||||
|
| P5 Policy is Evaluated as a Gate | C1, C5 | Correctness of pre-action enforcement; reversibility of blocked actions |
|
||||||
|
| P6 Evidence is Collected Continuously | C7, C3 | Observability of compliance state; simplicity of audit-by-query |
|
||||||
|
| P7 Identity is Attributable | C1, C7 | Correctness of accountability (security subset); observability of who acted |
|
||||||
|
| P8 Subject Access is Honored | C1, C5 | Correctness of the data-subject contract; reversibility of deletion |
|
||||||
|
| P9 Secrets and Sensitive Data are Redacted in Audit | C1, C3 | Correctness of not leaking (security subset); simplicity of structural redaction |
|
||||||
|
| P10 Compliance Posture is Observable | C7, C1 | Observability of posture; correctness of self-reported state |
|
||||||
|
|
||||||
|
## 3. What Violates These Principles
|
||||||
|
|
||||||
|
| Violation | Principle Breached |
|
||||||
|
|-----------|-------------------|
|
||||||
|
| An audit log stored on a mutable filesystem with no write-once protection | P1 Audit Logs are Append-Only |
|
||||||
|
| A `DELETE` on an audit record to "clean up a typo" | P1 Audit Logs are Append-Only |
|
||||||
|
| An auth change with no audit log line | P2 Every Significant Action is Logged |
|
||||||
|
| "We'll add logging after we ship the feature" | P2 Every Significant Action is Logged |
|
||||||
|
| Data kept indefinitely because "the bucket is cheap" | P3 Retention is Policy, Not Storage |
|
||||||
|
| A retention rule in a spreadsheet, not in code | P4 Policy is Code |
|
||||||
|
| A policy that logs violations but does not block the action | P5 Policy is Evaluated as a Gate |
|
||||||
|
| Evidence assembled by hand the week before an audit | P6 Evidence is Collected Continuously |
|
||||||
|
| A shared `admin` account as the audit actor | P7 Identity is Attributable |
|
||||||
|
| A subject-access request that requires a forensics team | P8 Subject Access is Honored |
|
||||||
|
| A secret visible in an audit log entry | P9 Secrets and Sensitive Data are Redacted in Audit |
|
||||||
|
| No dashboard for compliance posture between audits | P10 Compliance Posture is Observable |
|
||||||
|
|
||||||
|
## 4. Relationship to Other Domains
|
||||||
|
|
||||||
|
Compliance is the accountability layer that crosses
|
||||||
|
`domains/security/` (it audits security actions),
|
||||||
|
`domains/observability/` (audit logs are structured logging; posture
|
||||||
|
is metrics; evidence is traces), `domains/data/` (retention and
|
||||||
|
subject access are data-layer contracts), and
|
||||||
|
`domains/infrastructure-as-code/` (policy-as-code parallels
|
||||||
|
declarative IaC; compliance state parallels state-as-truth). Cross-
|
||||||
|
links are one-directional (per D-026 extended):
|
||||||
|
|
||||||
|
- `domains/security/authorization.md` ← P7 (attributable identity)
|
||||||
|
- `domains/security/secrets.md` ← P9 (redaction)
|
||||||
|
- `domains/security/supply-chain.md` ← P6 (signed attestations as evidence)
|
||||||
|
- `domains/observability/logging.md` ← P1, P2 (audit logs = structured logging)
|
||||||
|
- `domains/observability/metrics.md` ← P10 (compliance posture metrics)
|
||||||
|
- `domains/observability/tracing.md` ← P6 (evidence from distributed traces)
|
||||||
|
- `domains/data/schema-design.md` ← P3, P8 (retention and subject-access shapes)
|
||||||
|
- `domains/data/migrations.md` ← P3 (retention migration discipline)
|
||||||
|
- `domains/infrastructure-as-code/P1 Declarative Intent` ← P4 (policy-as-code)
|
||||||
|
- `domains/infrastructure-as-code/P3 State is Truth` ← P10 (compliance posture truth)
|
||||||
|
- `domains/kubernetes/rbac.md` ← P7 (audit subject identity)
|
||||||
|
- `domains/devops/ci-cd.md` ← P5 (policy as a pipeline gate)
|
||||||
|
- `domains/devops/first-principles.md` ← P4 (policy as configuration-as-code)
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Policy as Code — Derived Rules
|
||||||
|
|
||||||
|
> Derives from `domains/compliance/first-principles.md`. Covers P4
|
||||||
|
> (Policy is Code) and P5 (Policy is Evaluated as a Gate). Referenced
|
||||||
|
> by `audit-logs.md` (policy decisions are audited) and `evidence.md`
|
||||||
|
> (policy decisions are evidence). Framework-agnostic per D-024.
|
||||||
|
|
||||||
|
## Policy is Code (P4 Policy is Code)
|
||||||
|
|
||||||
|
- Compliance policy is expressed in versioned, reviewable, testable
|
||||||
|
code — not in spreadsheets, prose documents, or tribal knowledge.
|
||||||
|
Policy in a spreadsheet is untestable, unreviewable, and
|
||||||
|
undeployable; it is a wishlist, not a control.
|
||||||
|
- Policy-as-code inherits the disciplines of
|
||||||
|
`domains/infrastructure-as-code/P1 Declarative Intent`: declarative
|
||||||
|
intent, version control, review before merge, plan before apply.
|
||||||
|
A compliance rule that is not executable is a rule that cannot be
|
||||||
|
enforced, which is a rule that does not exist.
|
||||||
|
- Policy code is tested like any other code: unit tests for the rule
|
||||||
|
logic (given an input, the rule allows or denies as expected),
|
||||||
|
integration tests for the gate (the rule fires at the right point
|
||||||
|
in the pipeline), and versioning for the policy itself (a policy
|
||||||
|
change is a reviewed, merged, deployed change).
|
||||||
|
|
||||||
|
## Policy is Evaluated as a Gate (P5 Policy is Evaluated as a Gate)
|
||||||
|
|
||||||
|
- Policy violations block **before** the action, not after the
|
||||||
|
audit. Enforcement happens at:
|
||||||
|
- **Admission time** — a kubernetes admission webhook denies a
|
||||||
|
non-compliant resource before it is created
|
||||||
|
(`domains/kubernetes/rbac.md`).
|
||||||
|
- **Pipeline time** — a CI/CD gate denies a non-compliant change
|
||||||
|
before it merges (`domains/devops/ci-cd.md`).
|
||||||
|
- **Provisioning time** — an IaC plan gate denies a non-compliant
|
||||||
|
resource before `apply` (`domains/infrastructure-as-code/`).
|
||||||
|
- A policy that logs violations but does not block the action is a
|
||||||
|
postcard, not a gate. Detection is not enforcement. A logged
|
||||||
|
violation that the actor could ignore is a P5 violation — the
|
||||||
|
policy exists, but the system is not compliant by construction.
|
||||||
|
- The gate is the contract. The policy author writes the rule; the
|
||||||
|
gate operator wires the rule into the enforcement point; the
|
||||||
|
auditor verifies the gate fired. All three are auditable
|
||||||
|
(`audit-logs.md`).
|
||||||
|
|
||||||
|
## Engine Comparison (IDEATE-23)
|
||||||
|
|
||||||
|
| Engine | Policy Language | Evaluation Gate | Ecosystem | Notes |
|
||||||
|
|--------|-----------------|-----------------|-----------|-------|
|
||||||
|
| **OPA / Rego** | Rego (declarative, set-based, Datalog-inspired) | CI/CD, k8s admission (Gatekeeper), HTTP API, IaC plan (Terraform Sentinel-style), service mesh | Broadest ecosystem; CNCF graduated; library of reusable bundles | General-purpose; the default choice when the gate location varies |
|
||||||
|
| **AWS Cedar** | Cedar (declarative, authorization-focused, schema-typed) | k8s admission (via Cedar-agent), application authorization, AVP (Verified Permissions) | AWS-native; tight schema typing; separates policy from entities | Authorization-focused; strong where the policy is "who can do what on which resource" |
|
||||||
|
| **HashiCorp Sentinel** | Sentinel (declarative, restricted, policy-focused) | Terraform / TFE plan gate, Nomad, Vault | HashiCorp ecosystem; embedded in Terraform Enterprise / HCP | IaC-plan-gate native; the enforcement point is the `plan` output |
|
||||||
|
| **Kyverno** | Kyverno (YAML-declarative, k8s-native, no new DSL) | k8s admission (native), cluster-wide policy reports | Kubernetes-native; no separate language — policy is a CRD | k8s-cluster-gate native; the choice when the gate is admission and the team prefers YAML over a DSL |
|
||||||
|
|
||||||
|
- None is advocated over the others. The choice is (a) where the
|
||||||
|
gate fires, (b) the team's tolerance for a new policy language,
|
||||||
|
and (c) ecosystem fit. All four satisfy P4/P5 when wired
|
||||||
|
correctly.
|
||||||
|
- A gate is a gate regardless of engine: the rule is declarative,
|
||||||
|
the evaluation is pre-action, and the decision is allow-or-deny.
|
||||||
|
The engine difference is language and enforcement-point fit, not
|
||||||
|
correctness.
|
||||||
|
|
||||||
|
## Policy Testing
|
||||||
|
|
||||||
|
- Policy code is unit-tested like any other code. A test asserts
|
||||||
|
that a given input produces the expected decision (allow / deny /
|
||||||
|
warn). The test is versioned with the policy; a policy change
|
||||||
|
with no test change is a red flag.
|
||||||
|
|
||||||
|
```
|
||||||
|
// illustrative Rego policy + test
|
||||||
|
// policy: deny containers running as root
|
||||||
|
package k8s.admission
|
||||||
|
|
||||||
|
deny[msg] {
|
||||||
|
input.kind == "Pod"
|
||||||
|
c := input.spec.containers[_]
|
||||||
|
not c.securityContext.runAsNonRoot
|
||||||
|
msg := sprintf("container %s must set runAsNonRoot", [c.name])
|
||||||
|
}
|
||||||
|
|
||||||
|
// test (Rego unit test)
|
||||||
|
package k8s.admission
|
||||||
|
|
||||||
|
test_deny_root_container {
|
||||||
|
some msg in deny with input as {
|
||||||
|
"kind": "Pod",
|
||||||
|
"spec": { "containers": [ { "name": "app", "securityContext": {} } ] }
|
||||||
|
}
|
||||||
|
msg == "container app must set runAsNonRoot"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Integration tests assert the gate fires: a non-compliant resource
|
||||||
|
submitted to the admission endpoint is denied; a compliant one is
|
||||||
|
allowed. The integration test runs against the real gate, not a
|
||||||
|
mock, because the gate wiring is half the contract.
|
||||||
|
|
||||||
|
## Policy Versioning
|
||||||
|
|
||||||
|
- Policy is versioned in git. A policy change is a reviewed, merged,
|
||||||
|
deployed change — the same discipline as application code. A
|
||||||
|
policy that is edited in production without review is a P4
|
||||||
|
violation: the policy is code, but it is being treated as config.
|
||||||
|
- A policy change can break existing workloads (a new deny rule
|
||||||
|
blocks a previously-allowed resource). The rollout is staged:
|
||||||
|
warn-only mode first (log violations, do not block), then enforce
|
||||||
|
mode after the violation count is zero. This is the policy
|
||||||
|
analogue of `domains/devops/P5 Progressive Delivery`.
|
||||||
|
|
||||||
|
## What Violates Policy-as-Code Discipline
|
||||||
|
|
||||||
|
| Violation | Principle |
|
||||||
|
|-----------|-----------|
|
||||||
|
| A compliance rule in a spreadsheet | P4 Policy is Code |
|
||||||
|
| A policy that logs violations but does not block the action | P5 Policy is Evaluated as a Gate |
|
||||||
|
| A policy edited in production without review | P4 Policy is Code |
|
||||||
|
| A policy with no unit tests for the rule logic | P4 Policy is Code |
|
||||||
|
| A gate wired with a mock instead of the real engine | P5 Policy is Evaluated as a Gate |
|
||||||
|
| A new deny rule enforced without a warn-only rollout | P5 Policy is Evaluated as a Gate |
|
||||||
|
| A policy in prose ("the team should not use root containers") | P4 Policy is Code |
|
||||||
|
| A policy decision with no audit record | P5 Policy is Evaluated as a Gate |
|
||||||
|
|
||||||
|
## Relationship to Other Domains
|
||||||
|
|
||||||
|
- `domains/infrastructure-as-code/first-principles.md` — policy-as-
|
||||||
|
code inherits declarative intent, versioning, and plan-before-
|
||||||
|
apply from IaC.
|
||||||
|
- `domains/kubernetes/rbac.md` — k8s admission is a primary
|
||||||
|
enforcement gate; Kyverno and OPA Gatekeeper wire into it.
|
||||||
|
- `domains/devops/ci-cd.md` — CI/CD is a pipeline-time enforcement
|
||||||
|
gate; a policy step blocks a non-compliant change before merge.
|
||||||
|
- `domains/compliance/audit-logs.md` — every policy decision (allow
|
||||||
|
/ deny) is an audited significant action.
|
||||||
|
- `domains/compliance/evidence.md` — policy decisions and the
|
||||||
|
policy code itself are evidence of enforcement posture.
|
||||||
|
- `domains/security/authorization.md` — Cedar's authorization-
|
||||||
|
focused policy overlaps with authz; the split is that authz is
|
||||||
|
the runtime decision, policy-as-code is the reviewed rule that
|
||||||
|
drives it.
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# Internationalization (i18n) — First Principles
|
||||||
|
|
||||||
|
> Grounded in Unicode ICU + CLDR, W3C i18n WG, BCP 47 / RFC 5646,
|
||||||
|
> ICU MessageFormat / FormatJS / i18next / Mozilla Fluent, the
|
||||||
|
> JavaScript `Intl` API, and WCAG 2.1 AA. The developer's language is
|
||||||
|
> one locale among many, not the neutral form.
|
||||||
|
|
||||||
|
## 1. The Principles
|
||||||
|
|
||||||
|
### P1. Source Language is a Locale, Not the Default
|
||||||
|
The developer's own language is one locale among many — it is not the
|
||||||
|
"neutral" or "unlocalized" form of the product. Strings are extracted
|
||||||
|
from day one, addressed by key, and routed through a locale resource
|
||||||
|
layer even when only one locale is populated. Treating the source
|
||||||
|
language as the default produces hidden concatenations, hardcoded
|
||||||
|
grammar assumptions, and a translation debt that compounds until the
|
||||||
|
first second locale arrives — at which point the fix is a rewrite, not
|
||||||
|
a patch. The source locale is `en-US` (or whatever the team writes in);
|
||||||
|
it is not `null`. This is the i18n angle on `domains/uiux/copywriting.md`:
|
||||||
|
copy lives in resources, not in code.
|
||||||
|
|
||||||
|
### P2. Locale Identifiers are Standardized
|
||||||
|
Use BCP 47 language tags (`en-US`, `ar-EG`, `zh-Hans-CN`, `pt-BR`).
|
||||||
|
No ad-hoc locale codes, no two-letter-only hacks, no invented keys.
|
||||||
|
The tag carries language, script (when needed), and region (when
|
||||||
|
needed); it is the contract between the resource layer, the
|
||||||
|
formatting layer, and the runtime. A locale identifier that is not
|
||||||
|
BCP 47 is a key that cannot be resolved by any standard tool, which
|
||||||
|
is a correctness violation. Cross `domains/data/schema-design.md`:
|
||||||
|
locale identifiers are a data shape with a defined vocabulary.
|
||||||
|
|
||||||
|
### P3. Resources are External, Not Inline
|
||||||
|
User-facing strings live in locale resource files (`.po`, JSON,
|
||||||
|
Fluent `.ftl`, ICU Resource Bundle), never concatenated inline in
|
||||||
|
code. Inline strings are invisible to the translation pipeline,
|
||||||
|
unversionable as a unit, and untestable for completeness. String
|
||||||
|
concatenation in code (`"Welcome, " + name + "!"`) is the cardinal
|
||||||
|
violation: it bakes in source-language grammar and breaks for every
|
||||||
|
locale with different word order. Resources are the boundary; code
|
||||||
|
addresses strings by key, the resource layer resolves the key to the
|
||||||
|
locale. This is the i18n angle on C4 Locality: strings and their
|
||||||
|
locale-specific consequences live together in the resource, not
|
||||||
|
scattered across code.
|
||||||
|
|
||||||
|
### P4. Plural and Gender are Parameterized
|
||||||
|
Plural forms, gender, and select are expressed with ICU MessageFormat
|
||||||
|
(or an equivalent parameterized formatter), never with `if (n == 1)`
|
||||||
|
branching in code. Plural rules are locale-specific — English has
|
||||||
|
one/other, Arabic has six categories, Russian has three — and a
|
||||||
|
hand-rolled branch encodes exactly one locale's rules while pretending
|
||||||
|
to be universal. The formatter is the contract; the resource carries
|
||||||
|
the variants; the code passes the count and lets the formatter choose.
|
||||||
|
A `if (n == 1)` plural is a C1 (Correctness) violation masquerading as
|
||||||
|
a shortcut.
|
||||||
|
|
||||||
|
### P5. Formatting is Locale-Aware
|
||||||
|
Dates, times, numbers, currencies, units, and relative time are
|
||||||
|
formatted via ICU / CLDR / the JavaScript `Intl` API — never
|
||||||
|
hand-rolled. A hand-rolled date formatter encodes one locale's
|
||||||
|
conventions and silently produces wrong output for every other locale
|
||||||
|
(mm/dd/yyyy vs dd/mm/yyyy is the canonical failure). CLDR is the
|
||||||
|
source of truth for locale data; `Intl` is the runtime that exposes
|
||||||
|
it. Formatting correctness is observable: a misformatted date is a
|
||||||
|
wrong answer in the user's locale, even if it is "right" in the
|
||||||
|
developer's. Cross `domains/api/error-responses.md` for localized
|
||||||
|
error messages at API boundaries.
|
||||||
|
|
||||||
|
### P6. Text Direction is a Layout Primitive
|
||||||
|
RTL and bidi are first-class layout concerns, not a CSS afterthought.
|
||||||
|
Logical CSS properties (`margin-inline-start`, `padding-block-end`,
|
||||||
|
`inset-inline-end`) over physical (`margin-left`, `padding-top`). The
|
||||||
|
`dir` attribute is set on the document and on subtrees; the bidi
|
||||||
|
algorithm (UAX #9) handles inline reordering. A layout that assumes
|
||||||
|
LTR is a layout that is wrong for `ar`, `he`, `fa`, `ur`, and any
|
||||||
|
RTL-mixed context. Text direction is not a skin — it is a structural
|
||||||
|
property of the layout, and fixing it late is a rewrite. This is the
|
||||||
|
i18n angle on `domains/uiux/accessibility.md`: RTL support is an
|
||||||
|
accessibility concern for non-Latin-script users.
|
||||||
|
|
||||||
|
### P7. Layout Accommodates Expansion
|
||||||
|
Translated text expands and contracts — German is ~30% longer than
|
||||||
|
English, Japanese often shorter, RTL mirroring shifts every visual
|
||||||
|
anchor. Layouts are flexible: no fixed pixel widths for text, no
|
||||||
|
truncation without an ellipsis-and-title strategy, no
|
||||||
|
`white-space: nowrap` on translatable strings. A layout that breaks
|
||||||
|
on a 30% expansion is a layout that is wrong for most of the world's
|
||||||
|
locales. Designing for the worst-case expansion up front is cheaper
|
||||||
|
than reworking every screen when the first long-form locale ships.
|
||||||
|
|
||||||
|
### P8. Pseudo-Locales Test Early
|
||||||
|
Test with pseudo-locales (accented, lengthened, RTL-mirrored, brack-
|
||||||
|
enclosed) before real translations arrive. A pseudo-locale run
|
||||||
|
surfaces hardcoded strings, layout overflow, broken concatenation,
|
||||||
|
and LTR assumptions while the fix is still cheap — the translator
|
||||||
|
hasn't been paid yet, and the string freeze hasn't happened. Finding
|
||||||
|
these bugs after real translation is a C5 (Reversibility) violation:
|
||||||
|
the cost of undoing is now a re-translation. Cross
|
||||||
|
`domains/testing/fixtures.md` and `domains/testing/pyramid.md` for
|
||||||
|
where pseudo-locales sit in the testing pyramid.
|
||||||
|
|
||||||
|
### P9. Images and Icons are Cultural
|
||||||
|
Icons, colors, gestures, and imagery are locale-sensitive. A
|
||||||
|
mailbox icon means "email" in the US and "mail" in Japan — but a
|
||||||
|
green checkmark means "correct" in the West and "incorrect" in some
|
||||||
|
East Asian contexts. A thumbs-up is positive in much of the world
|
||||||
|
and an insult in parts of the Middle East. Avoid locale-bound symbols
|
||||||
|
as universal; parameterize imagery per locale where the symbol is not
|
||||||
|
globally neutral. Icons are not a universal language; they are a
|
||||||
|
locale with a picture. This is a C2 (Clarity) concern: an icon whose
|
||||||
|
meaning changes by locale is unclear to the reader it was not drawn
|
||||||
|
for.
|
||||||
|
|
||||||
|
### P10. Translation is Reversible and Versioned
|
||||||
|
Resource files are versioned alongside code; a bad translation is a
|
||||||
|
rollback, not a hot-patch. Every locale resource has a history
|
||||||
|
(what shipped when), a provenance (which translator / which service),
|
||||||
|
and a rollback path. A translation that breaks the UI is reverted to
|
||||||
|
the prior resource version, the same way a code regression is
|
||||||
|
reverted to the prior commit. Translations without version history
|
||||||
|
are anecdote, not artifact — you cannot tell what changed, when, or
|
||||||
|
why. This is the i18n angle on C5 Reversibility applied to the
|
||||||
|
resource layer.
|
||||||
|
|
||||||
|
## 2. Core Principle Trace
|
||||||
|
|
||||||
|
Each i18n P-rule derives from one or more core C-rules (C1–C8). The
|
||||||
|
matrix extension lands in P4 of the v0.3 plan; the traces below are
|
||||||
|
authoritative.
|
||||||
|
|
||||||
|
| P-rule | Core | Why |
|
||||||
|
|--------|------|-----|
|
||||||
|
| P1 Source Language is a Locale, Not the Default | C2, C1 | Clarity of locale intent; correctness of treating source as one-of-many |
|
||||||
|
| P2 Locale Identifiers are Standardized | C2, C6 | Clarity of a standard vocabulary; composability with standard tools |
|
||||||
|
| P3 Resources are External, Not Inline | C4, C6 | Locality of strings and their locale consequences; composability of the resource layer |
|
||||||
|
| P4 Plural and Gender are Parameterized | C1, C6 | Correctness of locale-specific plural rules; composability of the formatter contract |
|
||||||
|
| P5 Formatting is Locale-Aware | C1, C7 | Correctness of formatted output; observability of format correctness |
|
||||||
|
| P6 Text Direction is a Layout Primitive | C1, C4 | Correctness of layout for RTL; locality of direction with the text it governs |
|
||||||
|
| P7 Layout Accommodates Expansion | C8, C3 | Economy of rework; simplicity of flexible layouts over per-locale overrides |
|
||||||
|
| P8 Pseudo-Locales Test Early | C7, C5 | Observability of i18n defects early; reversibility of fixing before translation |
|
||||||
|
| P9 Images and Icons are Cultural | C1, C2 | Correctness of locale-appropriate symbols; clarity of meaning across locales |
|
||||||
|
| P10 Translation is Reversible and Versioned | C5 | Reversibility of the resource layer |
|
||||||
|
|
||||||
|
## 3. What Violates These Principles
|
||||||
|
|
||||||
|
| Violation | Principle Breached |
|
||||||
|
|-----------|-------------------|
|
||||||
|
| A user-facing string hardcoded in source | P3 Resources are External, Not Inline |
|
||||||
|
| `"Welcome, " + name + "!"` string concatenation | P3 Resources are External, Not Inline |
|
||||||
|
| `if (n == 1) { return "item"; } else { return "items"; }` | P4 Plural and Gender are Parameterized |
|
||||||
|
| A locale code like `en_us` or `english` instead of `en-US` | P2 Locale Identifiers are Standardized |
|
||||||
|
| A hand-rolled date formatter (`getMonth() + 1 + "/" + getDay()`) | P5 Formatting is Locale-Aware |
|
||||||
|
| `margin-left: 10px` on a translatable layout | P6 Text Direction is a Layout Primitive |
|
||||||
|
| A fixed-width text container that overflows on German | P7 Layout Accommodates Expansion |
|
||||||
|
| First i18n test runs against real translations, not pseudo-locales | P8 Pseudo-Locales Test Early |
|
||||||
|
| A thumbs-up icon shipped as universally positive | P9 Images and Icons are Cultural |
|
||||||
|
| Resource files with no git history or no rollback path | P10 Translation is Reversible and Versioned |
|
||||||
|
| The source language treated as the "unlocalized" default | P1 Source Language is a Locale, Not the Default |
|
||||||
|
|
||||||
|
## 4. Relationship to Other Domains
|
||||||
|
|
||||||
|
i18n is the locale-awareness layer that `domains/uiux/` consumes and
|
||||||
|
that `domains/api/` surfaces at boundaries. It borrows the testing
|
||||||
|
discipline of `domains/testing/` and the data-shape discipline of
|
||||||
|
`domains/data/`. Cross-links are one-directional (per D-026 extended):
|
||||||
|
|
||||||
|
- `domains/uiux/copywriting.md` ← P1, P3 (strings live in resources)
|
||||||
|
- `domains/uiux/accessibility.md` ← P6 (RTL is an a11y concern for non-Latin users)
|
||||||
|
- `domains/uiux/components.md` ← P6, P7 (layout primitives that survive direction and expansion)
|
||||||
|
- `domains/api/error-responses.md` ← P5 (localized error messages)
|
||||||
|
- `domains/data/schema-design.md` ← P2, P3 (locale data shapes)
|
||||||
|
- `domains/testing/fixtures.md` ← P8 (pseudo-locale fixtures)
|
||||||
|
- `domains/testing/pyramid.md` ← P8 (pseudo-locale tier mapping)
|
||||||
|
- `domains/testing/first-principles.md` ← P8 (testing discipline for locale)
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Formatting — Derived Rules
|
||||||
|
|
||||||
|
> Derives from `domains/i18n/first-principles.md`. Covers P2 (Locale
|
||||||
|
> Identifiers Standardized), P4 (Plural/Gender Parameterized), and P5
|
||||||
|
> (Formatting is Locale-Aware). Referenced by `locale-resources.md`
|
||||||
|
> (the formatter resolves the messages) and `testing-i18n.md` (the
|
||||||
|
> formatted output is what snapshots assert).
|
||||||
|
|
||||||
|
## Formatting is Locale-Aware (P5 Formatting is Locale-Aware)
|
||||||
|
|
||||||
|
- Dates, times, numbers, currencies, units, and relative time are
|
||||||
|
formatted via ICU / CLDR / the JavaScript `Intl` API — never
|
||||||
|
hand-rolled. CLDR is the source of truth for locale data; `Intl`
|
||||||
|
is the runtime that exposes it.
|
||||||
|
- A hand-rolled formatter encodes one locale's conventions and
|
||||||
|
silently produces wrong output for every other locale. The
|
||||||
|
canonical failure is date format: `mm/dd/yyyy` (US) vs
|
||||||
|
`dd/mm/yyyy` (most of the world) vs `yyyy-mm-dd` (ISO, sortable).
|
||||||
|
Picking one and calling it done is a correctness violation in
|
||||||
|
every locale it is wrong for.
|
||||||
|
|
||||||
|
## BCP 47 Tags Drive Formatting (P2 Locale Identifiers Standardized)
|
||||||
|
|
||||||
|
- Every formatter takes a BCP 47 locale tag. The tag is the contract
|
||||||
|
between the resource layer and the formatting layer: the same tag
|
||||||
|
that selects the resource selects the formatter.
|
||||||
|
- A locale tag that is not BCP 47 cannot be resolved by `Intl`, ICU,
|
||||||
|
or CLDR — the formatter returns the runtime default, which is the
|
||||||
|
developer's locale, not the user's. This is why P2 is a
|
||||||
|
prerequisite of P5: you cannot format for a locale you cannot name.
|
||||||
|
|
||||||
|
## The Intl Surface (ICU/CLDR in the Browser and Node)
|
||||||
|
|
||||||
|
| API | Formats | Notes |
|
||||||
|
|-----|---------|-------|
|
||||||
|
| `Intl.DateTimeFormat` | Dates, times, date+time, time zones | Calendar (`buddhist`, `hebrew`, `islamic`), numbering system (`arab`, `hanidec`) via locale tag extensions |
|
||||||
|
| `Intl.NumberFormat` | Numbers, currencies, units, percent | Notation (`compact`, `scientific`), grouping, sign display |
|
||||||
|
| `Intl.RelativeTimeFormat` | "3 days ago", "in 2 months" | Locale-specific phrasing; numeric vs auto |
|
||||||
|
| `Intl.PluralRules` | Plural category for a count | `one`, `few`, `many`, `other`, `zero`, `two` per CLDR — the engine ICU MessageFormat uses |
|
||||||
|
| `Intl.ListFormat` | "a, b, and c" | Conjunction / disjunction / unit lists, locale-specific separators |
|
||||||
|
| `Intl.Collator` | Locale-aware string sorting | Strength (`base`, `accent`, `case`); numeric collation |
|
||||||
|
|
||||||
|
- All of these are built on ICU/CLDR; they are the runtime baseline.
|
||||||
|
Use them. A `moment.js`-style hand-rolled format string
|
||||||
|
(`"MM/DD/YYYY"`) is a relic of the pre-`Intl` era and a P5
|
||||||
|
violation in any locale-aware code path.
|
||||||
|
|
||||||
|
## Dates and Times
|
||||||
|
|
||||||
|
```
|
||||||
|
// Correct — Intl, locale-aware
|
||||||
|
new Intl.DateTimeFormat("ar-EG", {
|
||||||
|
dateStyle: "full",
|
||||||
|
timeStyle: "short",
|
||||||
|
}).format(new Date());
|
||||||
|
// "الأربعاء، ٧ نوفمبر ٢٠٢٤، ٣:١٥ م"
|
||||||
|
|
||||||
|
// Wrong — hand-rolled, source-locale only
|
||||||
|
const d = new Date();
|
||||||
|
const s = (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear();
|
||||||
|
// "11/7/2024" — meaningless in most locales
|
||||||
|
```
|
||||||
|
|
||||||
|
- Time zones are not locales. A locale tells you *how to format* a
|
||||||
|
timestamp; a time zone tells you *what instant* it refers to. Do
|
||||||
|
not derive one from the other (`ar-EG` is not a time zone).
|
||||||
|
Format with the user's locale; render in the user's time zone;
|
||||||
|
store in UTC.
|
||||||
|
|
||||||
|
## Numbers, Currencies, Units
|
||||||
|
|
||||||
|
```
|
||||||
|
new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" })
|
||||||
|
.format(1234.56); // "1.234,56 €"
|
||||||
|
|
||||||
|
new Intl.NumberFormat("ar-EG", { style: "currency", currency: "EGP" })
|
||||||
|
.format(1234.56); // "١٬٢٣٤٫٥٦ ج.م."
|
||||||
|
|
||||||
|
new Intl.NumberFormat("en-US", { style: "unit", unit: "kilometer-per-hour" })
|
||||||
|
.format(100); // "100 km/h"
|
||||||
|
```
|
||||||
|
|
||||||
|
- The currency code (`EUR`, `EGP`, `USD`) is ISO 4217; the locale
|
||||||
|
determines the symbol, grouping, and placement. A hand-rolled
|
||||||
|
`"$" + amount` is wrong for `de-DE` (symbol, grouping, placement
|
||||||
|
all differ).
|
||||||
|
|
||||||
|
## Plural Rules (P4 Plural/Gender Parameterized)
|
||||||
|
|
||||||
|
- `Intl.PluralRules` returns the CLDR plural category for a count in
|
||||||
|
a given locale. ICU MessageFormat uses this category to select the
|
||||||
|
variant from the resource (`locale-resources.md`).
|
||||||
|
- Never branch on the raw count in code. The count goes to the
|
||||||
|
formatter; the formatter consults `PluralRules` for the locale;
|
||||||
|
the resource carries the variant for that category.
|
||||||
|
|
||||||
|
```
|
||||||
|
// ICU MessageFormat (FormatJS)
|
||||||
|
new Intl.MessageFormat(
|
||||||
|
"{count, plural, one {# item} other {# items}}",
|
||||||
|
"en-US"
|
||||||
|
).format({ count: 1 }); // "1 item"
|
||||||
|
|
||||||
|
// ar-EG — six categories; the code is identical, only the
|
||||||
|
// resource differs.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gender and Select
|
||||||
|
|
||||||
|
- ICU MessageFormat also supports `{gender, select, male {...} female {...} other {...}}`
|
||||||
|
for gendered agreement and `{case, select, ...}` for general
|
||||||
|
disjunction. These live in the resource, not in code branches.
|
||||||
|
- A `switch (gender)` in code that picks a string is the same
|
||||||
|
violation as `if (n == 1)`: it encodes one locale's grammar in
|
||||||
|
code and breaks for every locale with different agreement rules.
|
||||||
|
|
||||||
|
## What Violates Formatting Discipline
|
||||||
|
|
||||||
|
| Violation | Principle |
|
||||||
|
|-----------|-----------|
|
||||||
|
| `getMonth() + 1 + "/" + getDay()` hand-rolled date | P5 Formatting is Locale-Aware |
|
||||||
|
| `"$" + amount` hand-rolled currency | P5 Formatting is Locale-Aware |
|
||||||
|
| `if (n === 1) "item" else "items"` plural branch | P4 Plural and Gender are Parameterized |
|
||||||
|
| `moment("MM/DD/YYYY")` format string in locale-aware code | P5 Formatting is Locale-Aware |
|
||||||
|
| Deriving time zone from locale tag | P2 Locale Identifiers are Standardized |
|
||||||
|
| A non-BCP-47 tag passed to `Intl` (silently falls back) | P2 Locale Identifiers are Standardized |
|
||||||
|
| `switch (gender)` selecting strings in code | P4 Plural and Gender are Parameterized |
|
||||||
|
| Storing timestamps in local time, not UTC | P5 Formatting is Locale-Aware |
|
||||||
|
|
||||||
|
## Relationship to Other Domains
|
||||||
|
|
||||||
|
- `domains/api/error-responses.md` — API error messages are
|
||||||
|
formatted for the requesting locale; the error code is stable, the
|
||||||
|
message is locale-formatted.
|
||||||
|
- `domains/data/schema-design.md` — locale identifiers, currency
|
||||||
|
codes, and time zones are data contracts; treat them as schema
|
||||||
|
(`en-US`, `EUR`, `UTC`), not free text.
|
||||||
|
- `domains/i18n/locale-resources.md` — the resource layer carries
|
||||||
|
the parameterized messages this formatter resolves.
|
||||||
|
- `domains/testing/fixtures.md` — formatted output per locale is the
|
||||||
|
fixture; snapshot tests assert against it.
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# Locale Resources — Derived Rules
|
||||||
|
|
||||||
|
> Derives from `domains/i18n/first-principles.md`. Covers P1 (Source
|
||||||
|
> Language is a Locale), P2 (Locale Identifiers Standardized), P3
|
||||||
|
> (Resources External, Not Inline), P4 (Plural/Gender Parameterized),
|
||||||
|
> and P10 (Translation Reversible and Versioned). Referenced by
|
||||||
|
> `formatting.md` (strings the formatter resolves) and
|
||||||
|
> `rtl-bidi.md` (the `dir` the resource layer carries).
|
||||||
|
|
||||||
|
## Resources are the Boundary (P3 Resources are External, Not Inline)
|
||||||
|
|
||||||
|
- User-facing strings live in locale resource files, addressed by
|
||||||
|
key. Code references a key; the resource layer resolves the key to
|
||||||
|
the active locale. The source language is itself a locale
|
||||||
|
(`en-US`), not a fallback baked into code.
|
||||||
|
- String concatenation in code (`"Welcome, " + name + "!"`) is the
|
||||||
|
cardinal violation: it bakes in source-language word order and
|
||||||
|
breaks for every locale with different grammar. Replace every
|
||||||
|
concatenation with a parameterized message:
|
||||||
|
`t("welcome", { name })`.
|
||||||
|
- The resource is the single place a string lives. Editing a string
|
||||||
|
in code instead of the resource is a locality violation (C4): the
|
||||||
|
string and its locale consequences now live apart.
|
||||||
|
|
||||||
|
## Resource File Formats
|
||||||
|
|
||||||
|
| Format | Shape | When | Notes |
|
||||||
|
|--------|-------|------|-------|
|
||||||
|
| `.po` / `.pot` | gettext; msgid → msgstr, plural headers | Server-side, GNU ecosystem, PHP/Python/C | Mature tooling (`xgettext`, `msgmerge`); supports plural categories via header |
|
||||||
|
| JSON (flat or namespaced) | `{ "key": "value" }` per locale | JS/web, i18next, FormatJS | Simple, machine-readable, but no native plural support — wrap with ICU MessageFormat |
|
||||||
|
| Fluent `.ftl` | Mozilla FTL; asymmetric, resolver-driven | Browser-grade l10n, asymmetric translations | One message can resolve differently per locale without code changes; supports attributes, selectors |
|
||||||
|
| ICU Resource Bundle | ICU binary/text resources | ICU-native, JVM, C++ | Tightest integration with ICU formatting/CLDR; steeper tooling |
|
||||||
|
|
||||||
|
- None is advocated over the others. The choice is ecosystem fit,
|
||||||
|
not correctness. All four satisfy P3/P4 when used as the boundary.
|
||||||
|
- A custom format (a hand-rolled `.csv` of strings) is a violation:
|
||||||
|
it is unsupported by standard tooling, has no plural grammar, and
|
||||||
|
cannot compose with `formatting.md`'s ICU layer.
|
||||||
|
|
||||||
|
## Key Naming and Namespaces (P2 Locale Identifiers Standardized)
|
||||||
|
|
||||||
|
- Locale identifiers are BCP 47 tags (`en-US`, `ar-EG`, `zh-Hans-CN`).
|
||||||
|
No ad-hoc codes. The resource file is named for its locale:
|
||||||
|
`en-US.json`, `ar-EG.po`, `ftl/ar-EG/main.ftl`.
|
||||||
|
- Message keys are stable, semantic, and structured — not prose.
|
||||||
|
`checkout.cart.item_count` not `"You have 3 items in your cart"`.
|
||||||
|
A key that is the source string (`t("You have items")`) breaks the
|
||||||
|
moment the source copy is edited; the key must outlive the copy.
|
||||||
|
- Namespaces segment by surface (`checkout.*`, `errors.*`, `onboarding.*`)
|
||||||
|
so that a locale can be loaded incrementally and so that key
|
||||||
|
collisions across surfaces are impossible. A flat namespace with
|
||||||
|
thousands of keys is a C2 (Clarity) violation waiting to happen.
|
||||||
|
|
||||||
|
## Fallback Chains
|
||||||
|
|
||||||
|
- The fallback chain is explicit: requested locale → language-only
|
||||||
|
(`en` from `en-GB`) → default locale → key itself (last resort).
|
||||||
|
The default locale is declared once, not re-derived in every call
|
||||||
|
site.
|
||||||
|
- A missing key in the requested locale falling back silently to the
|
||||||
|
source locale is a P3 violation: the user is silently shown the
|
||||||
|
developer's locale, which is not the locale they asked for. Missing
|
||||||
|
keys must be observable (see `testing-i18n.md`).
|
||||||
|
- Fallback is a property of the resource layer, not of individual
|
||||||
|
components. A component that re-implements fallback is duplicating
|
||||||
|
a contract (C6 Composability violation).
|
||||||
|
|
||||||
|
## Plural and Gender in Resources (P4 Plural/Gender Parameterized)
|
||||||
|
|
||||||
|
- Plural variants live in the resource, selected by the formatter,
|
||||||
|
parameterized by the count. The code passes the count; the resource
|
||||||
|
carries the variants; the formatter picks the right one per the
|
||||||
|
locale's CLDR plural rules.
|
||||||
|
|
||||||
|
```
|
||||||
|
// JSON + ICU MessageFormat (FormatJS / i18next)
|
||||||
|
{
|
||||||
|
"cart.item_count": "{count, plural, one {# item} other {# items}}"
|
||||||
|
}
|
||||||
|
// ar-EG.json — six plural categories per CLDR
|
||||||
|
{
|
||||||
|
"cart.item_count": "{count, plural, zero {لا عناصر} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصرًا} other {# عنصر}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `if (n == 1)` branching in code is a violation regardless of
|
||||||
|
language. Arabic has six plural categories; Russian has three;
|
||||||
|
English has two. A two-branch `if` encodes exactly one locale's
|
||||||
|
rules and is wrong for every other.
|
||||||
|
|
||||||
|
## Extraction Tooling (P1, P3)
|
||||||
|
|
||||||
|
- Strings are extracted mechanically (e.g. `xgettext`, `i18next-
|
||||||
|
parser`, FormatJS babel plugin), not by hand-tagging. Mechanical
|
||||||
|
extraction produces a `.pot` template that translators work from;
|
||||||
|
the template is regenerated on every build.
|
||||||
|
- A string that cannot be extracted (built at runtime from
|
||||||
|
fragments) is a P3 violation: it is invisible to the pipeline. If
|
||||||
|
the extractor cannot see it, neither can the translator.
|
||||||
|
- The extracted template is versioned (`P10`): the diff between
|
||||||
|
templates is the change in translatable surface. A template that
|
||||||
|
is not committed is a contract that is not reviewable.
|
||||||
|
|
||||||
|
## Versioning and Rollback (P10 Translation Reversible and Versioned)
|
||||||
|
|
||||||
|
- Resource files are committed to git alongside code. A bad
|
||||||
|
translation is a `git revert` of the resource, not a hot-patch over
|
||||||
|
the translator's work. Every locale resource has history,
|
||||||
|
provenance (which translator / which service produced which
|
||||||
|
commit), and a rollback path.
|
||||||
|
- A locale resource that is generated by a translation service and
|
||||||
|
committed without review is a P10 violation: the resource is
|
||||||
|
versioned but the provenance is opaque. Review the diff the same
|
||||||
|
way you review a code diff.
|
||||||
|
|
||||||
|
## What Violates Locale-Resource Discipline
|
||||||
|
|
||||||
|
| Violation | Principle |
|
||||||
|
|-----------|-----------|
|
||||||
|
| `t("You have " + n + " items")` concatenation | P3 Resources are External, Not Inline |
|
||||||
|
| A custom `.csv` string store instead of a standard format | P3 Resources are External, Not Inline |
|
||||||
|
| Locale file named `english.json` not `en-US.json` | P2 Locale Identifiers are Standardized |
|
||||||
|
| `if (n == 1) { t("item") } else { t("items") }` in code | P4 Plural and Gender are Parameterized |
|
||||||
|
| A key equal to the source string (`t("Welcome back")`) | P2 / P10 — keys must outlive copy |
|
||||||
|
| Silent fallback to the source locale with no signal | P3 Resources are External, Not Inline |
|
||||||
|
| A runtime-built string the extractor cannot see | P3 Resources are External, Not Inline |
|
||||||
|
| Resource files committed by a bot with no human review | P10 Translation is Reversible and Versioned |
|
||||||
|
|
||||||
|
## Relationship to Other Domains
|
||||||
|
|
||||||
|
- `domains/uiux/copywriting.md` — copy lives in resources; UI
|
||||||
|
microcopy is the source content the resource layer carries.
|
||||||
|
- `domains/api/error-responses.md` — API error messages are locale-
|
||||||
|
resource keys resolved at the boundary, not inline strings.
|
||||||
|
- `domains/data/schema-design.md` — locale identifiers and resource
|
||||||
|
shapes are a data contract; treat them as schema.
|
||||||
|
- `domains/i18n/formatting.md` — the formatter resolves the
|
||||||
|
parameterized message this layer produces.
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# RTL and Bidi — Derived Rules
|
||||||
|
|
||||||
|
> Derives from `domains/i18n/first-principles.md`. Covers P6 (Text
|
||||||
|
> Direction is a Layout Primitive) and P7 (Layout Accommodates
|
||||||
|
> Expansion). Referenced by `testing-i18n.md` (RTL coverage is an
|
||||||
|
> e2e tier). Grounded in W3C i18n bidi authoring, UAX #9, and
|
||||||
|
> `domains/uiux/accessibility.md`.
|
||||||
|
|
||||||
|
## Text Direction is a Layout Primitive (P6 Text Direction is a Layout Primitive)
|
||||||
|
|
||||||
|
- RTL and bidi are first-class layout concerns, not a CSS
|
||||||
|
afterthought. The layout is designed for both directions from the
|
||||||
|
first commit, not retrofitted when an RTL locale ships.
|
||||||
|
- Logical CSS properties over physical properties, always. The
|
||||||
|
browser resolves logical → physical from the `dir` attribute; the
|
||||||
|
code never has to.
|
||||||
|
|
||||||
|
| Physical (LTR-only) | Logical (dir-aware) | Resolves to in RTL |
|
||||||
|
|---------------------|---------------------|--------------------|
|
||||||
|
| `margin-left` | `margin-inline-start` | `margin-right` |
|
||||||
|
| `margin-right` | `margin-inline-end` | `margin-left` |
|
||||||
|
| `padding-left` | `padding-inline-start` | `padding-right` |
|
||||||
|
| `left: 0` | `inset-inline-start: 0` | `right: 0` |
|
||||||
|
| `text-align: left` | `text-align: start` | `text-align: right` |
|
||||||
|
| `float: left` | use flexbox/grid + `inline-start` where supported | mirrored |
|
||||||
|
|
||||||
|
- The `dir` attribute is set on the document root (`<html dir="rtl">`)
|
||||||
|
and on subtrees whose direction differs from the document
|
||||||
|
(`<span dir="ltr">` for an embedded Latin run). `dir` is the
|
||||||
|
contract the bidi algorithm (UAX #9) reads; do not fake direction
|
||||||
|
with `text-align` alone.
|
||||||
|
|
||||||
|
## The Bidi Algorithm (UAX #9)
|
||||||
|
|
||||||
|
- The Unicode bidi algorithm resolves inline reordering of mixed-
|
||||||
|
direction runs. The browser applies it; the author's job is to
|
||||||
|
mark direction correctly, not to reorder by hand.
|
||||||
|
- A string like `"The price is 15 USD"` in an RTL context renders
|
||||||
|
with the Latin run `"15 USD"` in LTR within the RTL line — the
|
||||||
|
algorithm handles it *if* the container's `dir` is set. Without
|
||||||
|
`dir`, numbers and Latin fragments drift to the wrong edge.
|
||||||
|
- `dir="auto"` on a container infers direction from the first strong
|
||||||
|
directional character of its content — useful for user-generated
|
||||||
|
content whose direction is unknown. `dir="auto"` is not a
|
||||||
|
replacement for `dir="rtl"` on a known-RTL document.
|
||||||
|
|
||||||
|
## Mirroring (Icons, Controls, Diagrams)
|
||||||
|
|
||||||
|
- Direction-aware icons mirror in RTL: a "back" arrow pointing left
|
||||||
|
in LTR points right in RTL. A "refresh" circular arrow does not
|
||||||
|
mirror. The rule: icons that imply direction mirror; icons that
|
||||||
|
imply time or rotation do not.
|
||||||
|
- Use `[dir="rtl"]` selectors or logical icon variants — never
|
||||||
|
`transform: scaleX(-1)` as a one-off hack scattered across
|
||||||
|
components. Centralize the mirroring rule (a token, a component
|
||||||
|
prop) so it is auditable.
|
||||||
|
- Numbers do not mirror. `"15 USD"` in an RTL line is still
|
||||||
|
`"15 USD"` left-to-right inside the bidi run; mirroring it to
|
||||||
|
`"DSU 51"` is a correctness violation.
|
||||||
|
- Diagrams and flowcharts: a left-to-right process flow in LTR is a
|
||||||
|
right-to-left flow in RTL. Decide per diagram whether the flow
|
||||||
|
mirrors (most do) or is direction-neutral (some scientific
|
||||||
|
schematics).
|
||||||
|
|
||||||
|
## Layout Accommodates Expansion (P7 Layout Accommodates Expansion)
|
||||||
|
|
||||||
|
- Translated text expands. German is ~30% longer than English;
|
||||||
|
Japanese is often shorter but taller; RTL mirroring shifts every
|
||||||
|
visual anchor. Layouts are flexible:
|
||||||
|
- No fixed pixel widths on translatable text containers.
|
||||||
|
- No `white-space: nowrap` on translatable strings.
|
||||||
|
- No `text-overflow: ellipsis` without a `title` carrying the full
|
||||||
|
string.
|
||||||
|
- Buttons sized to fit their longest locale variant, not the
|
||||||
|
source.
|
||||||
|
- A layout that breaks at +30% width is a layout that is wrong for
|
||||||
|
most of the world's locales. Designing for the worst case up front
|
||||||
|
is cheaper than reworking every screen when the first long-form
|
||||||
|
locale ships.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
| Pitfall | Why it breaks | Fix |
|
||||||
|
|---------|---------------|-----|
|
||||||
|
| `margin-left` everywhere | In RTL the start is the right; `margin-left` leaves the right side unstyled | `margin-inline-start` |
|
||||||
|
| `text-align: left` for "default" alignment | In RTL the default is right; `left` pins content to the wrong edge | `text-align: start` |
|
||||||
|
| Icons hardcoded to LTR orientation | "Back" arrow points the wrong way in RTL | Mirror direction-implying icons via `[dir="rtl"]` |
|
||||||
|
| Numbers mirrored with the layout | Numbers are LTR inside RTL; mirroring produces garbage | Leave number runs LTR; the bidi algorithm handles embedding |
|
||||||
|
| Fixed `width: 120px` on a button | German button label overflows and truncates | `min-width` + `max-width` + flex; let content size |
|
||||||
|
| `position: absolute; left: 0` | Pins to the physical left in both directions | `inset-inline-start: 0` |
|
||||||
|
| Fake direction with `text-align` only | The bidi algorithm reads `dir`, not `text-align`; mixed runs reorder wrong | Set `dir` on the container |
|
||||||
|
|
||||||
|
## What Violates RTL/Bidi Discipline
|
||||||
|
|
||||||
|
| Violation | Principle |
|
||||||
|
|-----------|-----------|
|
||||||
|
| A layout with no `dir` attribute, assuming LTR | P6 Text Direction is a Layout Primitive |
|
||||||
|
| `margin-left` / `left: 0` / `text-align: left` throughout | P6 Text Direction is a Layout Primitive |
|
||||||
|
| A "back" arrow that points left in the RTL build | P6 Text Direction is a Layout Primitive |
|
||||||
|
| Numbers mirrored to read right-to-left | P6 Text Direction is a Layout Primitive |
|
||||||
|
| `width: 100px` on a text container that overflows in German | P7 Layout Accommodates Expansion |
|
||||||
|
| `white-space: nowrap` on a translated label | P7 Layout Accommodates Expansion |
|
||||||
|
| `dir` faked with `text-align` and no `dir` attribute | P6 Text Direction is a Layout Primitive |
|
||||||
|
| No RTL build until the first RTL locale ships | P6 Text Direction is a Layout Primitive |
|
||||||
|
|
||||||
|
## Relationship to Other Domains
|
||||||
|
|
||||||
|
- `domains/uiux/accessibility.md` — RTL support is an accessibility
|
||||||
|
concern for non-Latin-script users; WCAG 2.1 AA requires that
|
||||||
|
direction be set correctly.
|
||||||
|
- `domains/uiux/components.md` — components are built with logical
|
||||||
|
properties so they survive direction and expansion without per-
|
||||||
|
locale overrides.
|
||||||
|
- `domains/i18n/testing-i18n.md` — RTL coverage is an e2e-tier
|
||||||
|
test; pseudo-locale mirroring surfaces direction bugs early.
|
||||||
|
- `domains/i18n/locale-resources.md` — the `dir` is part of the
|
||||||
|
locale's metadata, carried alongside the resource bundle.
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# Testing i18n — Derived Rules
|
||||||
|
|
||||||
|
> Derives from `domains/i18n/first-principles.md`. Covers P8
|
||||||
|
> (Pseudo-Locales Test Early) and the testing-discipline angle on
|
||||||
|
> P3 (Resources External), P5 (Formatting Locale-Aware), P6 (Text
|
||||||
|
> Direction), and P10 (Translation Versioned). Referenced by
|
||||||
|
> `locale-resources.md` (missing-key detection) and `rtl-bidi.md`
|
||||||
|
> (RTL coverage tier).
|
||||||
|
|
||||||
|
## Pseudo-Locales Test Early (P8 Pseudo-Locales Test Early)
|
||||||
|
|
||||||
|
- A pseudo-locale is a synthetic locale that transforms the source
|
||||||
|
strings to surface i18n defects before real translations arrive.
|
||||||
|
Three transforms cover the three defect classes:
|
||||||
|
|
||||||
|
| Pseudo-locale | Transform | Surfaces |
|
||||||
|
|---------------|-----------|----------|
|
||||||
|
| `en-XA` (accented) | `Wêlcômê tô thê çhêckôût` | Strings not extracted (raw source appears), encoding bugs |
|
||||||
|
| `en-XB` (lengthened / "long") | `Wᴇʟᴄᴏᴍᴇ ᴛᴏ ᴛʜᴇ ᴄʜᴇᴄᴋᴏᴜᴛ──────` (~30% longer, bracketed) | Layout overflow, fixed widths, truncation |
|
||||||
|
| `en-XC` (RTL-mirrored) | Source rendered with `dir="rtl"` and a Latin-in-RTL run | LTR-only layout assumptions, physical CSS properties |
|
||||||
|
|
||||||
|
- Pseudo-locale tests are cheap: they run against source strings, no
|
||||||
|
translator involved, no string freeze required. A failing pseudo-
|
||||||
|
locale run is a bug found at the cheapest possible point in the
|
||||||
|
pipeline. Finding the same bug after real translation is a C5
|
||||||
|
(Reversibility) violation: the fix now costs a re-translation.
|
||||||
|
|
||||||
|
## Pseudo-Locale → Testing Pyramid Mapping (IDEATE-28)
|
||||||
|
|
||||||
|
- The testing pyramid (`domains/testing/pyramid.md`) has three tiers;
|
||||||
|
i18n tests map to each tier with a distinct signal. The mapping is
|
||||||
|
deliberate: each tier catches a different class of defect, and
|
||||||
|
skipping a tier leaves a blind spot.
|
||||||
|
|
||||||
|
| Pyramid Tier | i18n Test | Defect Caught | Tooling Shape |
|
||||||
|
|--------------|-----------|---------------|---------------|
|
||||||
|
| **Unit** | Missing-key detection | A key referenced in code but absent from the resource bundle; a key present in the source locale but missing from a target locale | Static scan over the resource bundle + code AST; runs per file, no runtime |
|
||||||
|
| **Integration** | Snapshot per locale | Formatted output for a fixture input differs across locales in a way that breaks the contract (wrong plural, wrong date, overflow) | Render a known fixture through the formatter per locale; snapshot-diff against the recorded baseline |
|
||||||
|
| **e2e** | RTL coverage | The app renders and is navigable in `dir="rtl"`; no layout overflow, no off-screen controls, no LTR-pinned anchors | Browser-driven run against the `en-XC` pseudo-locale (or a real RTL locale); assert on layout, not just text |
|
||||||
|
|
||||||
|
- Unit is the broad base (fast, runs on every commit), e2e is the
|
||||||
|
narrow top (slow, runs on PR merge). Integration sits between.
|
||||||
|
This mirrors `domains/testing/pyramid.md` exactly — i18n is not a
|
||||||
|
special case; it is a domain that uses the same tiers.
|
||||||
|
|
||||||
|
## Unit Tier — Missing-Key Detection (P3 Resources External)
|
||||||
|
|
||||||
|
- A static scan compares the set of keys referenced in code against
|
||||||
|
the keys present in each locale bundle. A key in code but not in
|
||||||
|
`en-US` is a P3 violation (the string is not in the resource
|
||||||
|
layer). A key in `en-US` but not in `ar-EG` is a coverage gap —
|
||||||
|
the missing-key scan flags it before the locale ships.
|
||||||
|
- Missing keys fail the build, not the runtime. A missing key that
|
||||||
|
surfaces only when a user switches locale is a defect found in
|
||||||
|
production, which is the most expensive place to find it.
|
||||||
|
|
||||||
|
```
|
||||||
|
// tool output (illustrative)
|
||||||
|
// missing-key scan
|
||||||
|
[FAIL] ar-EG: key "checkout.cart.item_count" referenced in code,
|
||||||
|
absent from ar-EG.json
|
||||||
|
[FAIL] en-US: key "checkout.cart.total" referenced in Checkout.tsx:42,
|
||||||
|
absent from en-US.json (not extracted)
|
||||||
|
[PASS] en-US, ar-EG, de-DE, zh-Hans-CN: all other keys present
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Tier — Snapshot per Locale (P5 Formatting Locale-Aware)
|
||||||
|
|
||||||
|
- For a fixed fixture input, render the formatted output per locale
|
||||||
|
and snapshot it. A change in the snapshot is either an intended
|
||||||
|
change (new CLDR data, new copy) or a regression.
|
||||||
|
- The snapshot is per locale, not per format string. The same
|
||||||
|
fixture (`{ count: 1, currency: "EUR", date: 2024-11-07 }`)
|
||||||
|
produces different snapshots for `en-US`, `de-DE`, `ar-EG` — and
|
||||||
|
that difference is the assertion. A locale whose snapshot matches
|
||||||
|
the source locale's is a red flag: the formatter is not actually
|
||||||
|
locale-aware.
|
||||||
|
|
||||||
|
```
|
||||||
|
// snapshot — checkout.cart (fixture: count=1, currency=EUR, date=2024-11-07)
|
||||||
|
// en-US
|
||||||
|
"1 item · €1,234.56 · 11/7/2024"
|
||||||
|
// de-DE
|
||||||
|
"1 Artikel · 1.234,56 € · 07.11.2024"
|
||||||
|
// ar-EG
|
||||||
|
"عنصر واحد · ١٬٢٣٤٫٥٦ € · ٧/١١/٢٠٢٤"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Snapshots are reviewed, not rubber-stamped. A snapshot diff that
|
||||||
|
changes the plural form for `ar-EG` is either a CLDR update (verify)
|
||||||
|
or a regression (revert).
|
||||||
|
|
||||||
|
## e2e Tier — RTL Coverage (P6 Text Direction is a Layout Primitive)
|
||||||
|
|
||||||
|
- A browser-driven run against `en-XC` (or a real RTL locale like
|
||||||
|
`ar-EG`) asserts that the app is navigable in RTL: no overflow, no
|
||||||
|
off-screen controls, no LTR-pinned anchors. The assertion is on
|
||||||
|
layout, not on text — text correctness is the integration tier's
|
||||||
|
job.
|
||||||
|
- RTL e2e is the narrow top of the i18n pyramid: it is slow, it
|
||||||
|
requires a browser, and it catches the defects the lower tiers
|
||||||
|
cannot (the interaction of `dir` with the real layout engine). It
|
||||||
|
runs on PR merge, not on every commit.
|
||||||
|
|
||||||
|
## Snapshot Discipline (P10 Translation Reversible and Versioned)
|
||||||
|
|
||||||
|
- Snapshots are versioned in git. A snapshot that changes because of
|
||||||
|
a real translation update is a committed diff, reviewed like a
|
||||||
|
code change. A snapshot that changes because of a regression is a
|
||||||
|
`git revert`.
|
||||||
|
- A snapshot that is regenerated and committed without review is a
|
||||||
|
P10 violation: the snapshot is versioned but the provenance is
|
||||||
|
opaque. The same discipline applies to snapshots as to resources
|
||||||
|
(`locale-resources.md`).
|
||||||
|
|
||||||
|
## What Violates i18n Testing Discipline
|
||||||
|
|
||||||
|
| Violation | Principle |
|
||||||
|
|-----------|-----------|
|
||||||
|
| First i18n test runs against real translations, not pseudo-locales | P8 Pseudo-Locales Test Early |
|
||||||
|
| No missing-key scan — gaps surface only at runtime in production | P3 Resources are External, Not Inline |
|
||||||
|
| Snapshot per locale that matches the source locale's snapshot | P5 Formatting is Locale-Aware |
|
||||||
|
| No RTL e2e — "we'll test RTL when we ship an RTL locale" | P6 Text Direction is a Layout Primitive |
|
||||||
|
| Snapshots regenerated and committed without review | P10 Translation is Reversible and Versioned |
|
||||||
|
| i18n tests only at e2e (no unit/integration tier) | pyramid inversion — `domains/testing/pyramid.md` |
|
||||||
|
| Pseudo-locale run skipped because "it's not a real locale" | P8 Pseudo-Locales Test Early |
|
||||||
|
|
||||||
|
## Relationship to Other Domains
|
||||||
|
|
||||||
|
- `domains/testing/pyramid.md` — the pseudo-locale → pyramid mapping
|
||||||
|
mirrors this domain's unit / integration / e2e tiers exactly.
|
||||||
|
- `domains/testing/fixtures.md` — locale fixtures (a fixed input
|
||||||
|
rendered per locale) are the fixture shape for the integration
|
||||||
|
tier.
|
||||||
|
- `domains/i18n/locale-resources.md` — missing-key detection is the
|
||||||
|
unit-tier scan over the resource bundle this doc defines.
|
||||||
|
- `domains/i18n/formatting.md` — the integration-tier snapshot
|
||||||
|
asserts against the formatter's output.
|
||||||
|
- `domains/i18n/rtl-bidi.md` — the e2e tier exercises the layout
|
||||||
|
rules this doc establishes.
|
||||||
|
- `domains/uiux/accessibility.md` — RTL coverage is an a11y
|
||||||
|
concern; an untested RTL build is an untested a11y surface.
|
||||||
Reference in New Issue
Block a user