diff --git a/domains/compliance/audit-logs.md b/domains/compliance/audit-logs.md new file mode 100644 index 0000000..6b26f66 --- /dev/null +++ b/domains/compliance/audit-logs.md @@ -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 ` 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. \ No newline at end of file diff --git a/domains/compliance/data-retention.md b/domains/compliance/data-retention.md new file mode 100644 index 0000000..dfdd896 --- /dev/null +++ b/domains/compliance/data-retention.md @@ -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. \ No newline at end of file diff --git a/domains/compliance/evidence.md b/domains/compliance/evidence.md new file mode 100644 index 0000000..218cd66 --- /dev/null +++ b/domains/compliance/evidence.md @@ -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. \ No newline at end of file diff --git a/domains/compliance/first-principles.md b/domains/compliance/first-principles.md new file mode 100644 index 0000000..e92fa67 --- /dev/null +++ b/domains/compliance/first-principles.md @@ -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) \ No newline at end of file diff --git a/domains/compliance/policy-as-code.md b/domains/compliance/policy-as-code.md new file mode 100644 index 0000000..7e9aa0a --- /dev/null +++ b/domains/compliance/policy-as-code.md @@ -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. \ No newline at end of file diff --git a/domains/i18n/first-principles.md b/domains/i18n/first-principles.md new file mode 100644 index 0000000..a9ed7ff --- /dev/null +++ b/domains/i18n/first-principles.md @@ -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) \ No newline at end of file diff --git a/domains/i18n/formatting.md b/domains/i18n/formatting.md new file mode 100644 index 0000000..a29789f --- /dev/null +++ b/domains/i18n/formatting.md @@ -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. \ No newline at end of file diff --git a/domains/i18n/locale-resources.md b/domains/i18n/locale-resources.md new file mode 100644 index 0000000..d091622 --- /dev/null +++ b/domains/i18n/locale-resources.md @@ -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. \ No newline at end of file diff --git a/domains/i18n/rtl-bidi.md b/domains/i18n/rtl-bidi.md new file mode 100644 index 0000000..9dc1a9f --- /dev/null +++ b/domains/i18n/rtl-bidi.md @@ -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 (``) + and on subtrees whose direction differs from the document + (`` 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. \ No newline at end of file diff --git a/domains/i18n/testing-i18n.md b/domains/i18n/testing-i18n.md new file mode 100644 index 0000000..28702ed --- /dev/null +++ b/domains/i18n/testing-i18n.md @@ -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. \ No newline at end of file