@nurix/etna 0.18.10 → 0.18.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +13 -13
  2. package/bin/etna.js +98 -2
  3. package/data/harness-index.json +9 -1
  4. package/data/harnesses/code/CLAUDE.md +9 -10
  5. package/data/harnesses/code/agents/brainstorm.md +3 -3
  6. package/data/harnesses/code/agents/change-review.md +4 -4
  7. package/data/harnesses/code/agents/maintenance.md +177 -59
  8. package/data/harnesses/code/agents/spec-mode.md +3 -3
  9. package/data/harnesses/code/harness.json +2 -2
  10. package/data/harnesses/code/hooks/generate-ownership-index.mjs +148 -0
  11. package/data/harnesses/code/hooks/hooks.json +12 -0
  12. package/data/harnesses/code/skills/boundary/references/boundary-taxonomy.md +3 -3
  13. package/data/harnesses/code/skills/brainstorm/README.md +11 -9
  14. package/data/harnesses/code/skills/brainstorm/SKILL.md +2 -2
  15. package/data/harnesses/code/skills/brainstorm/harvest.md +5 -6
  16. package/data/harnesses/code/skills/distiller/SKILL.md +3 -3
  17. package/data/harnesses/code/skills/distiller/references/docs-mode.md +7 -7
  18. package/data/harnesses/code/skills/documentation/SKILL.md +23 -23
  19. package/data/harnesses/code/skills/documentation/references/templates.md +26 -64
  20. package/data/harnesses/etna/.prettierignore +12 -0
  21. package/data/harnesses/etna/.prettierrc.json +3 -0
  22. package/data/harnesses/etna/.vscode/settings.json +10 -0
  23. package/data/harnesses/etna/CLAUDE.md +151 -0
  24. package/data/harnesses/etna/agents/brainstorm.md +35 -0
  25. package/data/harnesses/etna/agents/change-review.md +251 -0
  26. package/data/harnesses/etna/agents/maintenance.md +240 -0
  27. package/data/harnesses/etna/agents/performance-review.md +223 -0
  28. package/data/harnesses/etna/agents/spec-mode.md +56 -0
  29. package/data/harnesses/etna/harness.json +11 -0
  30. package/data/harnesses/etna/hooks/generate-ownership-index.mjs +148 -0
  31. package/data/harnesses/etna/hooks/hooks.json +39 -0
  32. package/data/harnesses/etna/hooks/lib/redact.mjs +35 -0
  33. package/data/harnesses/etna/hooks/prompt-recall.mjs +155 -0
  34. package/data/harnesses/etna/hooks/session-reflect.mjs +285 -0
  35. package/data/harnesses/etna/rules/general.md +86 -0
  36. package/data/harnesses/etna/rules/kb.md +13 -0
  37. package/data/harnesses/etna/rules/nomenclature.md +41 -0
  38. package/data/harnesses/etna/rules/npm-cli.md +88 -0
  39. package/data/harnesses/etna/skills/boundary/SKILL.md +49 -0
  40. package/data/harnesses/etna/skills/boundary/references/boundary-taxonomy.md +796 -0
  41. package/data/harnesses/etna/skills/brainstorm/README.md +138 -0
  42. package/data/harnesses/etna/skills/brainstorm/SKILL.md +135 -0
  43. package/data/harnesses/etna/skills/brainstorm/harvest.md +25 -0
  44. package/data/harnesses/etna/skills/brainstorm/templates/backlog.md +39 -0
  45. package/data/harnesses/etna/skills/brainstorm/templates/build.md +37 -0
  46. package/data/harnesses/etna/skills/brainstorm/templates/decisions.md +29 -0
  47. package/data/harnesses/etna/skills/brainstorm/templates/doctrine.md +31 -0
  48. package/data/harnesses/etna/skills/brainstorm/templates/domain-model.md +25 -0
  49. package/data/harnesses/etna/skills/brainstorm/templates/glossary.md +20 -0
  50. package/data/harnesses/etna/skills/brainstorm/templates/milestones.md +43 -0
  51. package/data/harnesses/etna/skills/brainstorm/templates/non-goals.md +17 -0
  52. package/data/harnesses/etna/skills/brainstorm/templates/plan-doc.md +61 -0
  53. package/data/harnesses/etna/skills/brainstorm/templates/questions.md +36 -0
  54. package/data/harnesses/etna/skills/brainstorm/templates/references.md +28 -0
  55. package/data/harnesses/etna/skills/brainstorm/templates/rejected.md +29 -0
  56. package/data/harnesses/etna/skills/brainstorm/templates/state.md +1 -0
  57. package/data/harnesses/etna/skills/brainstorm/templates/tech.md +23 -0
  58. package/data/harnesses/etna/skills/brainstorm/templates/thesis.md +21 -0
  59. package/data/harnesses/etna/skills/distiller/SKILL.md +90 -0
  60. package/data/harnesses/etna/skills/distiller/references/docs-mode.md +299 -0
  61. package/data/harnesses/etna/skills/distiller/references/method.md +94 -0
  62. package/data/harnesses/etna/skills/documentation/SKILL.md +124 -0
  63. package/data/harnesses/etna/skills/documentation/references/examples.md +162 -0
  64. package/data/harnesses/etna/skills/documentation/references/style-guide.md +308 -0
  65. package/data/harnesses/etna/skills/documentation/references/templates.md +853 -0
  66. package/data/harnesses/etna/skills/workflow-mapper/SKILL.md +451 -0
  67. package/data/harnesses/etna/skills/workflow-mapper/references/patterns.md +214 -0
  68. package/data/harnesses/hr/harness.json +1 -1
  69. package/data/harnesses/hr/skills/evaluator/criteria/testing.md +4 -4
  70. package/data/harnesses/mobile/.prettierignore +12 -0
  71. package/data/harnesses/mobile/.prettierrc.json +3 -0
  72. package/data/harnesses/mobile/.vscode/settings.json +10 -0
  73. package/data/harnesses/mobile/CLAUDE.md +128 -0
  74. package/data/harnesses/mobile/agents/brainstorm.md +35 -0
  75. package/data/harnesses/mobile/agents/change-review.md +251 -0
  76. package/data/harnesses/mobile/agents/maintenance.md +240 -0
  77. package/data/harnesses/mobile/agents/performance-review.md +223 -0
  78. package/data/harnesses/mobile/agents/spec-mode.md +56 -0
  79. package/data/harnesses/mobile/harness.json +11 -0
  80. package/data/harnesses/mobile/hooks/generate-ownership-index.mjs +148 -0
  81. package/data/harnesses/mobile/hooks/hooks.json +39 -0
  82. package/data/harnesses/mobile/hooks/lib/redact.mjs +35 -0
  83. package/data/harnesses/mobile/hooks/prompt-recall.mjs +155 -0
  84. package/data/harnesses/mobile/hooks/session-reflect.mjs +285 -0
  85. package/data/harnesses/mobile/rules/general.md +86 -0
  86. package/data/harnesses/mobile/rules/nomenclature.md +41 -0
  87. package/data/harnesses/mobile/rules/react-native.md +132 -0
  88. package/data/harnesses/mobile/rules/typescript.md +40 -0
  89. package/data/harnesses/mobile/skills/boundary/SKILL.md +49 -0
  90. package/data/harnesses/mobile/skills/boundary/references/boundary-taxonomy.md +796 -0
  91. package/data/harnesses/mobile/skills/brainstorm/README.md +138 -0
  92. package/data/harnesses/mobile/skills/brainstorm/SKILL.md +135 -0
  93. package/data/harnesses/mobile/skills/brainstorm/harvest.md +25 -0
  94. package/data/harnesses/mobile/skills/brainstorm/templates/backlog.md +39 -0
  95. package/data/harnesses/mobile/skills/brainstorm/templates/build.md +37 -0
  96. package/data/harnesses/mobile/skills/brainstorm/templates/decisions.md +29 -0
  97. package/data/harnesses/mobile/skills/brainstorm/templates/doctrine.md +31 -0
  98. package/data/harnesses/mobile/skills/brainstorm/templates/domain-model.md +25 -0
  99. package/data/harnesses/mobile/skills/brainstorm/templates/glossary.md +20 -0
  100. package/data/harnesses/mobile/skills/brainstorm/templates/milestones.md +43 -0
  101. package/data/harnesses/mobile/skills/brainstorm/templates/non-goals.md +17 -0
  102. package/data/harnesses/mobile/skills/brainstorm/templates/plan-doc.md +61 -0
  103. package/data/harnesses/mobile/skills/brainstorm/templates/questions.md +36 -0
  104. package/data/harnesses/mobile/skills/brainstorm/templates/references.md +28 -0
  105. package/data/harnesses/mobile/skills/brainstorm/templates/rejected.md +29 -0
  106. package/data/harnesses/mobile/skills/brainstorm/templates/state.md +1 -0
  107. package/data/harnesses/mobile/skills/brainstorm/templates/tech.md +23 -0
  108. package/data/harnesses/mobile/skills/brainstorm/templates/thesis.md +21 -0
  109. package/data/harnesses/mobile/skills/distiller/SKILL.md +90 -0
  110. package/data/harnesses/mobile/skills/distiller/references/docs-mode.md +299 -0
  111. package/data/harnesses/mobile/skills/distiller/references/method.md +94 -0
  112. package/data/harnesses/mobile/skills/documentation/SKILL.md +124 -0
  113. package/data/harnesses/mobile/skills/documentation/references/examples.md +162 -0
  114. package/data/harnesses/mobile/skills/documentation/references/style-guide.md +308 -0
  115. package/data/harnesses/mobile/skills/documentation/references/templates.md +853 -0
  116. package/data/harnesses/mobile/skills/workflow-mapper/SKILL.md +451 -0
  117. package/data/harnesses/mobile/skills/workflow-mapper/references/patterns.md +214 -0
  118. package/package.json +1 -1
  119. package/data/harnesses/code/agents/harness-manager.md +0 -184
@@ -0,0 +1,796 @@
1
+ # Application Boundary Taxonomy
2
+
3
+ > The shared knowledge root for the `boundary` skill. Every mode — `check`, `generate`, `create`, `init` — reads this file. It is the single source of truth for _what an application boundary is_ and _which kinds exist_, so the four modes can never drift from each other. Extend the catalog here and all four modes inherit it at once.
4
+
5
+ ---
6
+
7
+ ## What an application boundary is
8
+
9
+ An **application boundary** is a horizontal, cross-cutting contract — an _edge_ between modules, layers, services, or deploy units — where an implicit agreement must hold on both sides, but is enforced in _different places_ on each side. The defining hazard: change one side and it compiles/lints/tests clean, then fails only at runtime — in production, often silently, sometimes with data loss.
10
+
11
+ The dangerous boundaries are the **invisible** ones. A team documents "auth" but never "the Redux store shape" or "the entity model," because those read as _just how the app is built_. This taxonomy names them so they can't hide.
12
+
13
+ The recurring failure signature across every category is the same: **two sides that are never statically linked** — a reader and a provider, a producer and a consumer, a schema and its generated client, a host and a remote, a config and its bundler — so a change on one side surfaces only at boot, install, deploy, replay, or never-loudly-at-all (a leaked secret, a stale cache, a silent cross-tenant read). The universal mitigation: **make the implicit contract explicit and CI-checkable**, and treat the identifier (column, key, route, tag, topic, token, header, binding) as a versioned API.
14
+
15
+ ## The two categories
16
+
17
+ | Category | Sourced from | Examples |
18
+ | --- | --- | --- |
19
+ | **technology** | stack detection (`package.json` + lockfile + source) | db-schema, wire-response-envelope, store-shape, edge-binding |
20
+ | **domain** | the product's information architecture (ADRs, doctrine, contracts pkg) — _not_ stack detection | the entity model + topology, tenancy |
21
+
22
+ The **domain** boundaries are stack-independent and the most catastrophic when broken; they come from the IA, not from `package.json`. Everything else is **technology** — detected from the deps and source.
23
+
24
+ ## `point` vs `transcribe` — the doc-mode
25
+
26
+ Each boundary type carries a **doc-mode** that decides how its file records the shape:
27
+
28
+ - **`point`** — _self-locating_ shapes a reader finds unaided: a DB schema lives in `schema.prisma`; routes in the router file. Record a **pointer (`file:line`) + invariants + safe-change checklist**. Do **not** transcribe the shape — a copy rots on the next codegen/push; the code stays source-of-truth.
29
+ - **`transcribe`** — _synthesized_ contracts with no single code home: the composed Redux store tree (spread across slices), the API response envelope (a convention), the IA topology, tenancy. Transcribe these canonically into the boundary file; feature docs own only their slice and link up.
30
+
31
+ Docs never outrank code. For `point` boundaries you mirror with `file:line`, you do not fork.
32
+
33
+ ## Greenfield / pre-code boundaries
34
+
35
+ When `generate` runs on a repo whose contract is **decided but has no code yet** (greenfield-after-brainstorm: ADRs/domain-model exist, modules aren't scaffolded), a `point` boundary has no `file:line` to mirror — but the boundary is real and the conventions are decided. Don't skip it and don't fabricate a pointer. Instead:
36
+
37
+ - **Record the decided conventions inline** (the invariants + safe-change essentials the ADRs/domain-model settle), the same content a transcribed shape would carry.
38
+ - **Carry a planned pointer** to where the canonical source _will_ land — `planned packages/api/src/db/schema.ts` — so the pointer converts to a real `file:line` when the module lands (a `create`/update refreshes it).
39
+ - **Mark it `point*`** in the index (a `transcribe` boundary whose canonical source is likewise pre-code is `transcribe*`) — the `*` reads against an index footnote: _"point-mode boundary whose pointer lands at implementation — converts to (or gains) a `file:line` pointer when the module lands."_
40
+ - The last safe-change checklist item is "replace the planned pointer with the real `file:line` once code lands."
41
+
42
+ A safe-change item that hinges on an **unresolved decision** links to its open-question or ADR destination rather than asserting a resolution (kb.md: link, don't fabricate).
43
+
44
+ The index also carries a **Skipped (premature for a pre-code repo)** block: boundary types not yet exhibited, one line each on why (e.g. _"workspace-package-graph — packages not scaffolded; add at scaffold time"_). See the index spec below.
45
+
46
+ ## Boundary file anatomy
47
+
48
+ Every file the skill writes to `docs/boundaries/` follows this shape — the format that `check` and `init` parse:
49
+
50
+ ```markdown
51
+ # <Boundary name>
52
+
53
+ [BOUNDARY: <type> · <CRITICAL|HIGH|MED|LOW>]
54
+
55
+ **Bounds.** <the two sides of this contract — two modules, two layers (app↔DB, render↔hydrate, producer↔consumer), or a module↔external-system>
56
+
57
+ ## Shape
58
+
59
+ <transcribed contract, OR a pointer: `packages/.../schema.ts:NN` with one-line orientation>
60
+
61
+ ## Invariants
62
+
63
+ <what must hold>
64
+
65
+ ## Blast radius
66
+
67
+ <what breaks, and where, if violated>
68
+
69
+ ## Safe-change checklist
70
+
71
+ - [ ] <step>
72
+ - [ ] <step>
73
+ ```
74
+
75
+ `docs/boundaries/README.md` is the index. Its table is one row per boundary with a **doc-mode column**, so the fold/skip decisions are auditable from the index alone:
76
+
77
+ ```markdown
78
+ | Boundary | Type | Severity | Doc-mode | Bounds (summary) |
79
+ ```
80
+
81
+ Below the table, three standing blocks:
82
+
83
+ - **Active technology packs** — which packs (persistence, wire, auth, async, config, …) the repo exhibits.
84
+ - **Folds applied** — which catalog types collapsed into which file, surfacing the per-entry fold-notes (below) as a ledger: e.g. _"orm-mapping/relations/enums/indexes → db-schema · error-envelope/status-codes → wire-contracts."_
85
+ - **Skipped** — types deliberately omitted, one line each on why (not exhibited, or premature for a pre-code repo: _"workspace-package-graph — packages not scaffolded; add at scaffold time"_).
86
+
87
+ If any boundary is pre-code (`point*`/`transcribe*`), add the footnote: _"\* point-mode boundary whose pointer lands at implementation — converts to (or gains) a `file:line` pointer when the module lands."_
88
+
89
+ **Name the bounding code units** (paths / package names) in every `Bounds` line — the distinct vertices named across all `Bounds` lines are the **module-discovery feed** the `maintenance` agent (baseline mode) reads to propose modules, so name them precisely. A `Bounds` line is not restricted to two modules: it may name two **layers** within one module (app↔DB, render↔hydrate, producer↔consumer) or a module and an **external system** — so a flat / single-module repo still has real boundaries to file (as the intro states).
90
+
91
+ **The clustering key — a convergence heuristic, not a hard count:** _one boundary file per (source-of-truth × doc-mode)_. Types that point at the same source-of-truth in the same doc-mode collapse into one file — all persistence types reading one relational schema fold into the `db-schema` file; split only where the source-of-truth or the doc-mode differs (a `transcribe` shape over the same schema, or a different store). Each catalog entry below carries a fold note: **Folds into `x`** (this type lands in `x`'s file), **Canonical home** (this type _is_ the file others fold into), or **Own home** (a standalone source-of-truth × doc-mode). Two `generate` runs converge on the same file set only where this fold-map names the home — where it doesn't, the count is a judgment call, not a guarantee.
92
+
93
+ ## Severity is load-bearing
94
+
95
+ Severity (`CRITICAL` / `HIGH` / `MED` / `LOW`) sets `check`-mode review depth and `init`-mode scan priority. Each entry below carries a default; adjust from the concrete repo + project memory. The pattern that holds across all categories: **CRITICAL belongs to shared, single-source, persisted, or trust-crossing contracts** (the IA model, db-schema, the wire envelope, auth/tenancy, event schemas, secrets, federation/remotes) — the ones touched by everything or that break _historical/in-flight_ state, not just live traffic.
96
+
97
+ The default is the type's **intrinsic** severity: the skill may **down-adjust** a single type with a reason logged in `docs/memory/boundary.md`, but there is **no relative rank and no up-or-down quota** across the repo's boundary set — three genuine CRITICALs in a micro repo is correct signal, not over-inflation to be capped.
98
+
99
+ ---
100
+
101
+ # The Catalog
102
+
103
+ Organized by the seven boundary categories. Each entry: **type-name — SEVERITY · doc-mode**, then what it is · where it appears · what breaks · how to detect · the safe-change essentials. Only instantiate the types the detected stack/IA actually exhibits; `log()` what you skip and why.
104
+
105
+ ---
106
+
107
+ ## 0 · DOMAIN (stack-independent — sourced from the IA, not `package.json`)
108
+
109
+ #### `information-architecture` — CRITICAL · transcribe
110
+
111
+ The entity model + topology (e.g. Brief→Plan→Persona→Audience→Match→Campaign→Run; service planes). **Where:** synthesized from ADRs / the domain model / a `@org/contracts` package — never `package.json`. **Breaks:** re-relating or renaming an entity ripples across _every_ module, migration, and wire type. **Detect:** `docs/architecture/` (domain-model, `adr/`), a contracts package, `schema.ts`/`models.py` entity defs. **Safe-change:** map every consumer before re-relating; coordinate migrations + wire types + selectors in one change. _(Own home — the IA model is its own transcribed source-of-truth; never folds into a technology file.)_ _The flagship domain boundary._
112
+
113
+ #### `tenancy` — CRITICAL · transcribe
114
+
115
+ The invariant that every request carries a non-spoofable tenant identity and all data access is scoped to it across every layer. **Where:** `tenant_id`/`org_id` columns, subdomain/JWT-claim resolution, `SET LOCAL app.current_tenant`, Better Auth organization plugin. **Breaks:** resolving tenant from a request body (vs validated JWT/subdomain/session) → cross-tenant read; under transaction-pooled PgBouncer a non-`SET LOCAL` GUC leaks tenant context across pooled clients. **Detect:** `tenant_id`/`organizationId` columns, `SET LOCAL`/`set_config(...,true)`, tenant middleware, `req.body.tenantId` (red flag). **Safe-change:** resolve tenant only from non-spoofable sources; transaction-scoped GUCs under pooling; never trust a body tenant id; keep RLS as a backstop (see `row-level-security`). _(Own home — the cross-layer tenant-isolation invariant (`transcribe`); the DB-layer enforcement is auth `row-level-security` (with persistence `row-level-multi-tenancy` folded in).)_
116
+
117
+ ---
118
+
119
+ ## 1 · PERSISTENCE & DATA MODEL
120
+
121
+ #### `db-schema` — CRITICAL · point
122
+
123
+ Tables, columns, names, types, nullability, defaults physically in the relational DB. **Where:** Postgres 17/MySQL/SQLite via Prisma 7 (`schema.prisma`), Drizzle (`schema.ts`), TypeORM, raw DDL. **Breaks:** a renamed/dropped/retyped column breaks every query, ORM model, serializer, report; nullability changes break INSERTs; a narrowed type truncates/rejects existing data. **Detect:** `schema.prisma`, `schema.ts` (`pgTable`/`mysqlTable`), `entities/**` (`@Entity`/`@Column`), `migrations/**`, `CREATE TABLE`, `ALTER TABLE`, `@db.`. **Safe-change:** expand-then-contract (add nullable → backfill → switch reads → drop); never reuse a dropped column name with a new type in a release; update every reader (ORM, raw SQL, BI/ETL, replicas) in lockstep. _(Canonical home for the relational-schema × `point` cluster — `orm-model-mapping`, `foreign-key-and-relations`, `index-and-constraint`, `db-enum`, `db-view-and-stored-procedure` all fold in unless the repo splits one out.)_
124
+
125
+ #### `orm-model-mapping` — HIGH · point
126
+
127
+ The mapping binding app objects/types to tables — the contract that the model still matches the live DB. **Where:** Prisma `model`/`@@map`, Drizzle table objects, TypeORM `@Entity`, SQLAlchemy, Ecto, generated clients. **Breaks:** DB and model drift independently; a wrong-typed column or stale `@@map` fails at runtime, not compile; the generated client lies to TS callers. **Detect:** ORM schema files, generated clients (`.prisma/client`, `drizzle/`), `@map(`, `@relation`, `prisma generate`, `drizzle-kit`. **Safe-change:** regenerate + recompile after every schema change; keep explicit column names in sync; run a drift check (`prisma migrate diff`, `drizzle-kit check`) in CI; treat the generated client as build output. _(Folds into `db-schema` — same relational-schema source-of-truth, same `point` doc-mode.)_
128
+
129
+ #### `db-migration` — CRITICAL · point
130
+
131
+ The ordered, immutable history of DDL/DML transforming one schema version to the next. **Where:** Prisma Migrate, Drizzle Kit, Flyway, Liquibase, Alembic, Knex, Atlas. **Breaks:** editing an applied migration, a non-idempotent step, or a `db push` that bypasses history causes **drift** — the runner's recorded hash no longer matches the live schema and refuses (or applies partially). **Detect:** `prisma/migrations/**`, `drizzle/**`, `migrations/**`, `_prisma_migrations`/`schema_migrations` tables, checksum columns, `migrate deploy`. **Safe-change:** never edit an applied migration — add a new one; never mix `db push` with `migrate dev` on one DB; test forward against a prod-shaped snapshot; verify the from-hash before deploy. _(Own home — the migration history is a distinct source-of-truth from the live schema; do not fold into `db-schema`.)_ _(This repo's doctrine: drop-&-push the declared schema locally; never author migration SQL — see project memory.)_
132
+
133
+ #### `foreign-key-and-relations` — HIGH · point
134
+
135
+ Referential-integrity edges (FK constraints) + the ORM relations on top. **Where:** `REFERENCES … ON DELETE/UPDATE`, Prisma `@relation`, Drizzle `references()`, TypeORM `@ManyToOne`. **Breaks:** changing a cascade silently changes delete semantics — `ON DELETE CASCADE` wipes child rows; `RESTRICT` blocks assumed deletes; mismatched ORM cardinality returns wrong shapes. **Detect:** `REFERENCES`, `FOREIGN KEY`, `ON DELETE`, `@relation(`, `onDelete:`, `relations(`. **Safe-change:** audit every cascade's blast radius before changing; change DB FK + ORM relation together; clean orphans before adding an FK; index the FK column. _(Folds into `db-schema` — same relational-schema source-of-truth, same `point` doc-mode.)_
136
+
137
+ #### `index-and-constraint` — HIGH · point
138
+
139
+ Unique/check/exclusion/partial/composite indexes the app's correctness and latency depend on. **Where:** Postgres `CREATE UNIQUE INDEX`/`CHECK`/`EXCLUDE`, Prisma `@@unique`/`@@index`, Drizzle `uniqueIndex()`. **Breaks:** code relies on a unique constraint for upsert/idempotency (`ON CONFLICT`) — dropping it lets duplicates in; dropping a hot-query index is a silent latency cliff; composite column-order changes regress range/tenant filters. **Detect:** `CREATE INDEX`, `UNIQUE`, `CHECK (`, `@@unique(`, `ON CONFLICT`, `uniqueIndex(`, `WHERE` (partial). **Safe-change:** `CONCURRENTLY` to avoid locks; grep `ON CONFLICT`/`findUnique` before dropping a unique index; dedupe before adding UNIQUE; preserve leading-column order. _(Folds into `db-schema` — same relational-schema source-of-truth, same `point` doc-mode.)_
140
+
141
+ #### `db-enum` — MED · point
142
+
143
+ A closed value set enforced at DB level, mirrored in code. **Where:** Postgres `CREATE TYPE … AS ENUM`, Prisma `enum`, Drizzle `pgEnum`, MySQL `ENUM`. **Breaks:** Postgres can't remove an enum value and restricts adding inside a txn; app switch/exhaustiveness and the DB set drift — writing an unknown value throws, reading an unhandled one falls through. **Detect:** `CREATE TYPE`, `AS ENUM`, `enum {`, `pgEnum(`, TS unions mirroring DB enums. **Safe-change:** add values, never reorder/remove; add to DB before deploying code that writes it; keep the app union in sync and handle unknowns defensively. _(Folds into `db-schema` — same relational-schema source-of-truth, same `point` doc-mode.)_
144
+
145
+ #### `jsonb-column-shape` — HIGH · transcribe
146
+
147
+ The implicit structure of a JSON/JSONB column read/written by key path. **Where:** Postgres `jsonb`, MySQL `JSON`, Prisma `Json`, Drizzle `jsonb()`, validated (or not) by Zod at the edge. **Breaks:** the DB enforces _nothing_ inside, so the contract lives entirely in code on both sides; a renamed inner key or nesting change breaks readers with no DDL, no migration, no compile error — and old rows keep the old shape forever. **Detect:** `jsonb`/`Json`, accessors `->`/`->>`/`#>`, `jsonb_path_query`, Zod schemas on JSON columns, GIN indexes. **Safe-change:** version the embedded shape (`_v`) and read all live versions; validate with Zod on write AND read; data-migrate old rows; index only after the shape is stable. _(Own home — same schema source-of-truth as `db-schema` but `transcribe` doc-mode (the shape lives only in code), so it splits into its own file.)_
148
+
149
+ #### `fulltext-search-index` — MED · point
150
+
151
+ Lexical search structures + their tokenization/config contract. **Where:** Postgres `tsvector`/`to_tsvector`/GIN, MySQL `FULLTEXT`, Elasticsearch/Meilisearch/Typesense mappings. **Breaks:** index-time config (language/stemming/weights) must match query-time or recall silently degrades; a config or stored-`tsvector` change needs a full reindex; queries against a stale index return wrong results with no error. **Detect:** `tsvector`, `to_tsvector(`, `plainto_tsquery`, `@@`, `setweight`, ES `analyzer`. **Safe-change:** keep index- and query-time config identical; reindex fully after any change; pin the language/dictionary explicitly. _(Own home — the lexical-index config is a distinct source-of-truth from the relational schema; do not fold into `db-schema`.)_
152
+
153
+ #### `vector-search-index` — HIGH · point
154
+
155
+ Embedding columns + their ANN index, where dimension, metric, and index type are a hard contract. **Where:** pgvector (`vector(N)`, HNSW, IVFFlat), Pinecone, Qdrant, Cloudflare Vectorize. **Breaks:** dimension is fixed per column and must match the embedding model — switching models breaks every insert; the **distance operator must match the index operator class** (`<->` L2 / `<=>` cosine / `<#>` IP) or Postgres silently seq-scans; pgvector caps at 2000 dims. **Detect:** `vector(`, `USING hnsw`/`ivfflat`, `vector_cosine_ops`/`_l2_ops`/`_ip_ops`, `<->`/`<=>`/`<#>`, `CREATE EXTENSION vector`, `embedding` columns. **Safe-change:** re-embed + reindex when changing model/dimension; match query operator to index operator class; store the embedding-model identity as provenance; mind the 2000-dim cap and `m`/`ef_construction`. _(Own home — the embedding/ANN-index contract is a distinct source-of-truth (and its IA/embedding-provenance facet can be CRITICAL); do not fold into `db-schema`.)_ _(This repo: install pgvector in db-init so a from-scratch push succeeds — project memory.)_
156
+
157
+ #### `cache-key-schema` — MED · transcribe
158
+
159
+ The naming convention, key structure, value serialization, and embedded version of cached entries. **Where:** Redis 7, Memcached, Cloudflare KV, ioredis/node-redis, framework caches. **Breaks:** producer and consumer of a key are often different deploys — if the value serialization changes but the key doesn't, a new reader deserializes an old value and crashes/misreads; key-pattern changes orphan old keys. **Detect:** `redis.set(`/`get(`, `:`-delimited key builders, `cache:` prefixes, `JSON.stringify`/`msgpack` around cache, `EXPIRE`/`SETEX`, `:v{n}` in keys. **Safe-change:** bump a version segment when value shape changes (old keys miss and re-populate); make deserializers version-aware; always set a TTL; namespace hierarchically, invalidate by version bump not `KEYS` wildcards. _(Canonical home for the cache-store × `transcribe` cluster — `cache-ttl-and-consistency` folds in (the freshness facet of the same cache contract).)_
160
+
161
+ #### `cache-ttl-and-consistency` — MED · transcribe
162
+
163
+ The expiry/eviction + freshness contract — how stale is acceptable, when entries invalidate. **Where:** Redis TTL/`maxmemory-policy`, HTTP `Cache-Control`/`s-maxage`, Next.js `revalidate`/`tags`, write-through/-behind. **Breaks:** TTL/invalidation live on the writer; readers assume a freshness SLA — lowering TTL spikes DB load (stampede); raising it serves stale data; an eviction-policy change can evict keys assumed durable. **Detect:** `EXPIRE`/`SETEX`/`ttl:`, `maxmemory-policy`, `revalidate:`, `staleWhileRevalidate`, invalidation hooks on writes. **Safe-change:** coordinate TTL with the freshness expectation + origin capacity; add jitter to avoid synchronized expiry; invalidate on every source write; document durable vs evictable keys. _(Folds into `cache-key-schema` — same cache-store source-of-truth, same `transcribe` doc-mode.)_
164
+
165
+ #### `row-level-multi-tenancy` — CRITICAL · transcribe
166
+
167
+ Data-isolation enforced at the row level (see domain `tenancy` for the cross-layer invariant; this is its DB-layer enforcement). **Where:** Postgres RLS (`CREATE POLICY`, `FORCE ROW LEVEL SECURITY`, `current_setting('app.tenant_id')`), `tenant_id` columns, schema/DB-per-tenant. **Breaks:** a new table without the tenant column or RLS policy is invisible until tenant A reads tenant B; RLS is silently bypassed by the table owner (without `FORCE`), `BYPASSRLS` roles, superusers, `SECURITY DEFINER` views. **Detect:** `CREATE POLICY`, `ENABLE/FORCE ROW LEVEL SECURITY`, `current_setting('app.`, `BYPASSRLS`, `tenant_id` columns. **Safe-change:** every new tenant-scoped table gets column + policy + `FORCE` in one change; never run app traffic as a `BYPASSRLS`/owner/superuser role; `SET LOCAL` under poolers; tenant_id as leading composite-index column; `security_invoker = true` views. _(Folds into auth `row-level-security` — same RLS-policy source-of-truth, same `transcribe` doc-mode; this is its persistence-layer view. Backstops domain `tenancy`.)_
168
+
169
+ #### `soft-delete-convention` — HIGH · transcribe
170
+
171
+ "Deleted" rows are flagged, not removed, and every query must respect the flag. **Where:** `deleted_at` columns, Prisma soft-delete extensions, TypeORM `@DeleteDateColumn`, Sequelize `paranoid`, Rails `acts_as_paranoid`. **Breaks:** the "don't show deleted" rule is in app/ORM scope filters, not the DB — a new query/raw SQL/report that forgets it resurrects deleted rows; a UNIQUE constraint collides with a soft-deleted row and blocks legit re-creation. **Detect:** `deleted_at`/`is_deleted`/`archived_at`, `WHERE deleted_at IS NULL`, `paranoid`, global ORM scopes, partial unique indexes. **Safe-change:** enforce the filter centrally (global scope/middleware); make unique constraints partial (`WHERE deleted_at IS NULL`); audit raw SQL/reports; define hard-purge retention. _(Own home — the "respect the flag" rule is a transcribed app/ORM-scope convention, not the relational schema; do not fold into `db-schema`.)_
172
+
173
+ #### `audit-and-event-sourcing-tables` — HIGH · point
174
+
175
+ Append-only stores of facts/changes — audit logs, event stores, the outbox — where the event schema is a long-lived contract with future readers. **Where:** EventStoreDB, Marten, Axon, Kafka + outbox tables, `audit_log`/`events` tables, bitemporal tables. **Breaks:** events are immutable and replayed — a projector built today must understand events written years ago; changing an event's payload breaks replay/projection rebuilds; you can never migrate past events, only version them. **Detect:** `events`/`event_store`/`outbox`/`audit_log` tables, `event_type`/`aggregate_id`/`sequence`/`version` columns, `append`/`replay`/`project`, upcasters, Debezium. **Safe-change:** version event types + add upcasters; never mutate/delete history; keep projectors tolerant of all versions; test a full replay; audit columns are compliance-frozen (additive only). _(Own home — the append-only event/audit store is a distinct source-of-truth from the live schema; do not fold into `db-schema`. Cross-ref async `event-schema` for the event wire-shape.)_
176
+
177
+ #### `read-model-projection-and-materialized-view` — MED · point
178
+
179
+ Denormalized derived stores — materialized views, CQRS read models, rollups — contracted between the refresher and the reader. **Where:** Postgres `MATERIALIZED VIEW` (+ `REFRESH … CONCURRENTLY`), CQRS read models, dbt models, D1 derived tables. **Breaks:** two contracts — the source schema it's derived from (a source column change breaks the refresh) and the read-model schema consumers query; `CONCURRENTLY` needs a unique index; stale projections serve wrong data with no error. **Detect:** `CREATE/REFRESH MATERIALIZED VIEW`, projection handlers, `*_summary`/`*_read_model` tables, dbt `models/`, refresh jobs. **Safe-change:** update the view + rebuild when a source column changes; keep a unique index for `CONCURRENTLY`; monitor refresh failures; version read-model schema separately. _(Own home — the derived/read-model store is a distinct source-of-truth from the base schema it projects; do not fold into `db-schema`.)_
180
+
181
+ #### `db-view-and-stored-procedure` — HIGH · point
182
+
183
+ Server-side DB logic — views, procedures/functions, triggers — that code and other objects depend on as an interface. **Where:** Postgres `CREATE VIEW`/`FUNCTION`/`TRIGGER` (PL/pgSQL), Supabase RPC, `SECURITY DEFINER`. **Breaks:** a proc is an API with a signature; a trigger embeds invisible business logic that fires on INSERT; `SECURITY DEFINER` silently bypasses RLS; changing a signature breaks callers; dropping a selected column breaks the view. **Detect:** `CREATE VIEW`/`OR REPLACE FUNCTION`/`TRIGGER`, `LANGUAGE plpgsql`, `SECURITY DEFINER`, `supabase.rpc(`, `BEFORE/AFTER INSERT`. **Safe-change:** treat proc signatures as public API; recreate dependent views on column change; audit triggers for side effects; set `security_invoker` deliberately so RLS holds. _(Folds into `db-schema` — same relational-DDL source-of-truth, same `point` doc-mode; split out only if procs/triggers carry enough business logic to warrant their own file.)_
184
+
185
+ #### `blob-object-storage-key-convention` — MED · point
186
+
187
+ The path/key scheme + metadata contract for object storage, plus the DB pointers to it. **Where:** S3, GCS, Azure Blob, Cloudflare R2, MinIO; `object_key`/`storage_path` columns; presigned URLs. **Breaks:** the key scheme is a contract among uploader, downloader, lifecycle rules, and DB rows — changing the template orphans existing objects; a prefix change breaks lifecycle/CDN paths; there's no FK between DB and bucket. **Detect:** `s3.putObject`/`getSignedUrl`, `r2.put`, key-template builders, `object_key`/`file_url` columns, `${tenant}/${id}/` patterns, lifecycle config. **Safe-change:** never change the template for existing objects without a copy/re-point migration; store the full key in the DB; coordinate prefix changes with lifecycle + CDN; namespace by tenant. _(Own home — the object-store key scheme is a distinct source-of-truth from the relational schema; do not fold into `db-schema`.)_
188
+
189
+ #### `connection-pool-and-isolation-level` — HIGH · point
190
+
191
+ Assumptions about pooling mode, transaction isolation, statement timeouts, and session-state lifetime. **Where:** PgBouncer/Pgpool (session/transaction/statement pooling), `pg`/Prisma/Drizzle pool config, RDS Proxy, `SET TRANSACTION ISOLATION LEVEL`. **Breaks:** code relying on session state (`SET`, prepared statements, advisory locks, `SET ROLE`, RLS context) breaks under transaction-mode pooling where a "session" is shared across clients; lowering from SERIALIZABLE introduces write-skew/phantoms under concurrency. **Detect:** `pgbouncer`, `pool_mode = transaction`, `?pgbouncer=true`, `SET TRANSACTION ISOLATION`, `SET LOCAL`, `pg_advisory_lock`, `statement_timeout`. **Safe-change:** `SET LOCAL` only + disable server-side prepared statements under transaction pooling; don't lower isolation below the app's invariants (retry on serialization failure); set explicit timeouts; size the pool against `max_connections` + pooler mode. _(Own home — the pooler/driver config is a distinct source-of-truth from the schema; do not fold into `db-schema`.)_
192
+
193
+ #### `db-extension-and-collation` — HIGH · point
194
+
195
+ DB-level dependencies — installed extensions, server encoding, `LC_COLLATE`/ICU — that objects and sort/compare semantics rely on. **Where:** Postgres `CREATE EXTENSION` (`pgvector`, `pgcrypto`, `uuid-ossp`, `postgis`, `citext`), collations, `COLLATE`, init scripts. **Breaks:** a schema object needs its extension installed first (a from-scratch deploy fails otherwise); a collation-version change (glibc/ICU upgrade) silently changes string sort order, corrupting B-tree index ordering → wrong results / "duplicate" unique-index rows. **Detect:** `CREATE EXTENSION`, `db-init`/`init.sql`, `shared_preload_libraries`, `COLLATE`, `citext`, `pg_trgm`. **Safe-change:** install extensions in DB-init before the migration that uses them; reindex collation-dependent indexes after any ICU/glibc change; pin encoding/collation across primary/replicas/CI. _(Own home — DB-init / cluster-level deps are a distinct source-of-truth from the schema objects that rely on them; do not fold into `db-schema`.)_
196
+
197
+ #### `auth-and-framework-managed-tables` — HIGH · point
198
+
199
+ Tables whose schema is owned by a framework/library, not your app migrations — and which your schema tooling can clobber. **Where:** Better Auth tables (`user`, `session`, `account`, `verification`), Auth.js adapter tables, Lucia, Temporal/job-queue tables. **Breaks:** two migration authorities target one DB — `drizzle-kit push`/`prisma migrate` can **drop the Better Auth tables**, breaking login until the library's own migration is re-run; the library reads its tables by exact column names, so an app-side rename breaks auth. **Detect:** `better-auth`, `@better-auth/cli migrate`, `auth.ts`, `user`/`session`/`account` tables, two tools writing one DB. **Safe-change:** after any push/migrate touching the shared DB, re-run `@better-auth/cli migrate`; keep framework tables reconciled with (or out of) your ORM source; never rename framework table/column names; verify login after every auth-table change. _(Own home — the framework library owns this schema authority, a distinct source-of-truth your app migrations must not clobber; do not fold into `db-schema`. Cross-ref auth `auth-contract` for the session-table shape.)_ _(This repo's exact failure mode — project memory.)_
200
+
201
+ ---
202
+
203
+ ## 2 · API & WIRE / SYNCHRONOUS CONTRACTS
204
+
205
+ #### `wire-response-envelope` — CRITICAL · transcribe
206
+
207
+ The top-level shape wrapping every response body (e.g. `{status, message, data}`), independent of the per-endpoint payload. **Where:** Express 5 REST (this repo's `{status, message, data}`), Axios interceptors that unwrap `.data.data`. **Breaks:** every client unwraps identically — renaming `data`, flipping `status`'s type (1/0 vs boolean), or moving errors out of the envelope breaks every consumer that destructures it. **Detect:** `res.json({ status`, shared response builders (`sendSuccess`, `apiResponse.js`), client interceptors reading `response.data.data`. **Safe-change:** change the envelope in a versioned namespace, never in place; update the shared builder + client interceptor together; keep `status` semantics stable. _(Canonical home for the REST-wire-convention × `transcribe` cluster — `error-envelope-and-error-codes` and `http-status-code-convention` fold in (the failure-shape and status-mapping facets of the same envelope contract).)_ _(This repo's documented envelope — `.claude/rules/general.md`.)_
208
+
209
+ #### `error-envelope-and-error-codes` — HIGH · transcribe
210
+
211
+ The structured failure shape + stable code taxonomy (`{status:0,…}`, RFC 9457 `problem+json`, `{error:{code}}`). **Where:** Express centralized error handler, gRPC `google.rpc.Status`, GraphQL `extensions.code`, Stripe-style `error.code`. **Breaks:** clients branch on `error.code` — renaming a code, changing its HTTP status, or restructuring `details` breaks retry logic, user messages, and alerts, silently (transport still "succeeds"). **Detect:** the last `app.use((err,req,res,next)` handler, error-code enums, `extensions.code`, `problem+json`. **Safe-change:** treat codes as append-only; keep the status↔code mapping stable; version alongside the success envelope; document every code consumers branch on. _(Folds into `wire-response-envelope` — same REST-wire-convention source-of-truth, same `transcribe` doc-mode; the failure facet of the one envelope contract.)_
212
+
213
+ #### `rest-endpoint-shape` — HIGH · point
214
+
215
+ Method + path + params + request/response body of one REST endpoint. **Where:** Express 5 routers, Fastify, Next.js route handlers. **Breaks:** renaming a route, changing `:id`→`:userId`, making an optional field required, or removing a response field breaks every caller across the HTTP gap with no compile signal. **Detect:** `**/routes/**`, `**/*.routes.*`, `router.(get|post|…)`, client `axios.get('/...')`/`fetch`. **Safe-change:** add fields, don't rename/remove; deprecate before deletion; keep query params optional; update client call sites + typed SDK together. _(Canonical home for the REST-endpoint × `point` cluster — `request-validation-schema` and `response-dto-shape` fold in (the inbound and outbound shapes of the same endpoint), unless the repo files validation/DTO contracts separately.)_
216
+
217
+ #### `http-status-code-convention` — HIGH · transcribe
218
+
219
+ The team's outcome→status mapping (`200/201/400/401=unauth/403=forbidden/404/409/422/429/500`). **Where:** Express `res.status(...)`, client interceptors branching on `err.response.status`. **Breaks:** clients route behavior off status (401→refresh+retry, 409→conflict UI, 429→backoff) — switching 400↔422 or confusing 401/403 breaks retry/redirect logic. **Detect:** `res.status(`, interceptors checking `status === 401`, `retryableStatusCodes`. **Safe-change:** keep 401=unauthenticated / 403=unauthorized fixed; don't migrate validation 400↔422 without updating clients; keep 429 paired with retry headers. _(Folds into `wire-response-envelope` — same REST-wire-convention source-of-truth, same `transcribe` doc-mode; the status-mapping facet.)_ _(This repo's documented convention — `.claude/rules/general.md`.)_
220
+
221
+ #### `openapi-spec-contract` — HIGH · point
222
+
223
+ A machine-readable OpenAPI/Swagger doc as source of truth for generated clients/mocks/tests. **Where:** `openapi.{yaml,json}`, `zod-to-openapi`, `tsoa`, NestJS Swagger, openapi-generator SDKs. **Breaks:** generated SDKs, mocks, and docs derive from the spec — spec↔server drift makes clients send/expect wrong shapes, visible only at runtime. **Detect:** `**/openapi.{yaml,json}`, `openapi: 3.x`, generator config, spec-validation in CI. **Safe-change:** regenerate spec-from-code (or code-from-spec) in one change; run an OpenAPI breaking-change linter (`oasdiff`, Redocly) in CI; bump `info.version`; republish SDKs. _(Own home — where the repo is spec-first, the spec file is the REST source-of-truth and `rest-endpoint-shape` cross-refs it; do not fold the spec into per-endpoint files.)_
224
+
225
+ #### `request-validation-schema` — HIGH · point
226
+
227
+ The runtime schema (Zod/yup/Joi/class-validator) defining a valid inbound request. **Where:** Zod 4 `z.object()` in Express middleware, `safeParse`, tRPC `.input()`, NestJS DTOs. **Breaks:** the schema _is_ the request contract — tightening it (new required field, narrowed enum, `.strict()`) silently 400s previously-valid clients; loosening lets malformed data through. **Detect:** `z.object`, `.parse(`/`.safeParse(`, `zodResolver`, `@IsString`, `schemas/`/`validators/`. **Safe-change:** new body fields optional/defaulted; avoid `.strict()` regressions; share the schema with the client so both move together; enums append-only. _(Folds into `rest-endpoint-shape` — same endpoint/handler source-of-truth, same `point` doc-mode; the inbound-shape facet. Cross-ref frontend `form-schema-contract`.)_
228
+
229
+ #### `response-dto-shape` — HIGH · point
230
+
231
+ The field set, types, and nullability of an endpoint's serialized response (the DTO inside the envelope). **Where:** Express JSON responses, ORM serializers, DTO mappers, GraphQL object types. **Breaks:** removing/renaming a field, changing number↔string, or flipping nullability breaks every reader — including TS clients whose types were derived once and now lie. **Detect:** response serializers, `res.json({...})` literals, `select`/`pick` projections, client `interface *Response`. **Safe-change:** add freely, deprecate before removing; never change a field's JSON type in place; keep date/number serialization stable; sync client types from one source. _(Folds into `rest-endpoint-shape` — same endpoint/handler source-of-truth, same `point` doc-mode; the outbound-shape facet (the DTO inside the envelope).)_
232
+
233
+ #### `graphql-schema-contract` — CRITICAL · point
234
+
235
+ The SDL — types, fields, args, enums, directives — resolvers fulfill and clients query. **Where:** Apollo Server, GraphQL Yoga, `schema.graphql`, code-first (Pothos/Nexus), `@graphql-codegen`. **Breaks:** removing/renaming a field, tightening nullability, removing an enum value, or changing an arg breaks existing client queries at execution; codegen'd types + persisted-query docs go stale. **Detect:** `**/*.graphql`, `typeDefs`, `gql\``, `buildSchema`/`makeExecutableSchema`, `codegen.yml`. **Safe-change:** run a schema-diff check (GraphQL Inspector, `rover graph check`, Hive) in CI; `@deprecated`+ usage metrics before removal; don't tighten output nullability or remove in-use enum values; regenerate client types every change. *(Canonical home for the GraphQL-SDL ×`point`cluster —`graphql-federation-subgraph-contract`folds in (its directives live in the SDL);`graphql-persisted-query-contract` keeps its own file (a separate registry).)\*
236
+
237
+ #### `graphql-federation-subgraph-contract` — CRITICAL · point
238
+
239
+ The federation entity/key contract — `@key`, `@external`, `@requires`, `@provides`, `@shareable`, `@tag` — between a subgraph and the composed supergraph. **Where:** Apollo Federation v2, WunderGraph Cosmo, GraphQL Hive, `@apollo/subgraph`. **Breaks:** changing an entity `@key`, removing an `@external` field another subgraph `@requires`, or breaking a `@shareable` type fails _supergraph composition_ — taking down fields owned by other teams. **Detect:** `@key(`, `@external`, `@requires`, `@provides`, `@shareable`, `extend type`, `@tag`, `supergraph.yaml`. **Safe-change:** `rover subgraph check`/Cosmo/Hive composition checks before publish; coordinate entity-key changes across referencing subgraphs; don't remove depended-on `@external`/`@provides`. _(Folds into `graphql-schema-contract` — the federation directives live in the same SDL source-of-truth, same `point` doc-mode; split out a `supergraph.yaml`-level file only in a dedicated federation gateway.)_
240
+
241
+ #### `graphql-persisted-query-contract` — HIGH · point
242
+
243
+ The registry of allowlisted/hashed operation documents shared between client build output and the server allowlist. **Where:** Apollo APQ/persisted queries, Hive persisted documents, Relay persisted queries. **Breaks:** the server only executes registered hashes — editing an operation without republishing the allowlist (or changing the hash algorithm) → `PersistedQueryNotFound`, blocking all traffic for that operation. **Detect:** `persistedQueries`, `automaticPersistedQueries`, `sha256Hash`, `extensions.persistedQuery`, the persisted-query manifest. **Safe-change:** republish the manifest whenever a query changes; pin the hashing algorithm across client/server; roll out new manifests before dependent clients; stabilize the schema first. _(Own home — the persisted-query manifest/registry is a distinct source-of-truth from the SDL; do not fold into `graphql-schema-contract`.)_
244
+
245
+ #### `grpc-protobuf-wire-contract` — CRITICAL · point
246
+
247
+ Protobuf message/service defs whose field numbers + wire types are a binary-compatibility contract. **Where:** gRPC, `.proto` (proto2/3), `buf`, `protoc` stubs, Connect-RPC, gRPC-Web. **Breaks:** field numbers identify fields on the wire and can never be reused — changing a number/type/`repeated`-ness, or reusing a deleted number, silently corrupts data across services on different `.proto` versions, no exception. **Detect:** `**/*.proto`, `= 1;` field numbers, `reserved`, `service { rpc }`, `buf.yaml`. **Safe-change:** never reuse/renumber field numbers (`reserved` removed ones); add fields with new numbers only; `buf breaking` in CI; never change wire type or cardinality in place. _(Canonical home for the `.proto` × `point` cluster — `grpc-service-method-contract` folds in (the service surface lives in the same `.proto`).)_
248
+
249
+ #### `grpc-service-method-contract` — HIGH · point
250
+
251
+ The gRPC service surface — RPC names, request/response types, streaming mode. **Where:** gRPC `service` blocks, generated stubs, reflection, Connect/gRPC-Web. **Breaks:** renaming an RPC, changing its messages, or switching streaming mode (unary→stream) breaks generated clients with `UNIMPLEMENTED`/deserialization errors; the method path is a routing contract. **Detect:** `rpc \w+\(`, `stream` in `.proto`, `*ServiceClient`/`*ServiceServer`, `UNIMPLEMENTED`. **Safe-change:** add new RPCs rather than mutate; keep the fully-qualified path stable; don't change streaming mode of an in-use RPC; regenerate + republish stubs. _(Folds into `grpc-protobuf-wire-contract` — same `.proto` source-of-truth, same `point` doc-mode; the service-surface facet.)_
252
+
253
+ #### `trpc-router-contract` — HIGH · point
254
+
255
+ The tRPC router's procedures, `.input()`/`.output()` schemas, and the exported `AppRouter` type flowing end-to-end. **Where:** tRPC v11, `createTRPCClient<AppRouter>`, Next.js + tRPC monorepos. **Breaks:** the contract is the TS type shared by import — caught at compile only if client/server build the same types; deploy-skew (server ahead of client) breaks at runtime, and mismatched package versions throw on connect. **Detect:** `initTRPC`, `router({`, `.input(`, `publicProcedure`, `createTRPCClient<`, `export type AppRouter`. **Safe-change:** deploy server + client from the same `AppRouter` version; keep tRPC package versions identical; version procedures via `v1`/`v2` sub-routers; exported-type changes are semver-major. _(Own home — the exported `AppRouter` type is its own end-to-end source-of-truth, distinct from REST endpoints.)_
256
+
257
+ #### `api-versioning-strategy` — HIGH · transcribe
258
+
259
+ How the API expresses version — URL path, header, media-type param, query — and what each version freezes. **Where:** Express prefixes (`/v1`), header routing, proto `package myapi.v1`, GraphQL field-level versioning. **Breaks:** clients pin a version — changing the default, dropping a version, or moving version header→URL silently routes clients to incompatible behavior. **Detect:** `/v1`/`/v2` prefixes, `Accept-Version`/`X-API-Version` reads, `package *.v1`, version-routing middleware. **Safe-change:** keep old versions alive until consumers migrate (announce sunset); don't change default/unversioned behavior; keep the selection mechanism stable. _(Own home — the version-selection convention is a distinct cross-cutting `transcribe` contract, not the per-endpoint shape.)_
260
+
261
+ #### `pagination-filtering-sorting-contract` — MED · transcribe
262
+
263
+ The paging (offset/cursor/keyset), filter, sort param convention + page-metadata shape (`hasMore`, `nextCursor`, `total`). **Where:** REST list endpoints, GraphQL Relay connections (`edges`/`pageInfo`), `?limit=&offset=`/`?cursor=`. **Breaks:** clients loop on exact param names + metadata fields — switching offset→cursor, renaming `nextCursor`, changing cursor encoding, or altering default page size breaks iteration (drops rows / infinite loops). **Detect:** `limit`/`offset`/`page`/`cursor`/`after`, `pageInfo`/`hasNextPage`/`endCursor`, `sort=`/`filter[`. **Safe-change:** keep param names + default page size stable; treat cursors as opaque; add filter/sort options additively; keep page-metadata field names fixed. _(Own home — the paging/filter/sort convention spans list endpoints as one transcribed contract, not a single endpoint's shape.)_
264
+
265
+ #### `cors-preflight-contract` — HIGH · transcribe
266
+
267
+ The CORS contract — allowed origins/methods/headers, credentials flag, and which custom headers trigger preflight. **Where:** Express `cors({ origin: FRONTEND_URL })`, gateway/edge CORS, browser `OPTIONS` preflight. **Breaks:** the browser enforces at request-shape level — adding a custom header (`X-Tenant-Id`) without `Access-Control-Allow-Headers`, tightening `origin`, or enabling credentials without `allow-credentials` silently blocks requests in the browser only (server tests pass). **Detect:** `cors(`, `Access-Control-Allow-`, `allowedHeaders`/`credentials`, `origin:`, custom client headers. **Safe-change:** add new custom headers to `allowedHeaders`; keep `origin` allowlist synced to deployed frontends (every env); match `credentials:true` with a non-wildcard origin; list read headers in `exposedHeaders`. _(Canonical home; runtime `cors-policy` folds in — same CORS contract, infra-layer view.)_
268
+
269
+ #### `content-negotiation-media-type` — MED · point
270
+
271
+ The `Content-Type`/`Accept` contract — which media types the server produces/consumes and how it parses by type. **Where:** Express `express.json()`/`urlencoded()`, `res.format()`, vendor media types (`application/vnd.app.v2+json`), `problem+json`. **Breaks:** a client sending `application/json` to an endpoint that switched to `multipart/form-data`, or a server dropping a vendor type, breaks parsing — empty bodies or 415s. **Detect:** `Content-Type`/`Accept`, `res.format(`, `express.json`/`urlencoded`/`text`, `+json`/`vnd.`, multer. **Safe-change:** keep accepted/produced types stable per endpoint; don't switch body encoding without client updates; return 406/415 correctly. _(Own home — the media-type parse contract is a distinct request-shape convention; do not fold into `rest-endpoint-shape`.)_
272
+
273
+ #### `file-upload-multipart-contract` — MED · point
274
+
275
+ The multipart field-name, count, size-limit, MIME contract for uploads (or presigned-URL flows). **Where:** multer (`upload.single('avatar')`), busboy, GraphQL multipart, S3/R2 presigned PUT/POST. **Breaks:** the server keys off exact field names + size/MIME limits — renaming `file`→`upload`, tightening `fileSize`, or changing allowed MIME silently rejects uploads; presigned policy mismatch rejects the PUT. **Detect:** `multer`, `upload.single`/`array`/`fields`, `multipart/form-data`, `fileSize`/`limits`, `createPresignedPost`. **Safe-change:** keep field names stable; don't tighten size/MIME without notifying clients; keep presigned-POST policy aligned with the client form. _(Own home — the multipart field/size/MIME contract is distinct; do not fold into `rest-endpoint-shape`. Cross-ref persistence `blob-object-storage-key-convention` for presigned-URL flows.)_
276
+
277
+ #### `inbound-webhook-contract` — HIGH · point
278
+
279
+ The contract for webhooks your service _receives_ — payload schema, headers, signature scheme, idempotency. **Where:** Stripe (`stripe-signature`), GitHub (`X-Hub-Signature-256`), Shopify HMAC, Express raw-body handlers. **Breaks:** (1) the provider changes payload/version/signing → your verifier rejects/misparses; (2) JSON middleware re-serializes the body → HMAC over parsed body no longer matches the raw-body signature — both fail silently or 400 every delivery. **Detect:** `X-Hub-Signature`, `stripe-signature`, `constructEvent`, `createHmac`, `timingSafeEqual`, `express.raw`, `req.rawBody`. **Safe-change:** verify signatures over the **raw** body before JSON parsing; `timingSafeEqual` not `===`; make handlers idempotent on delivery IDs; track the provider's webhook API version. _(Own home — the inbound contract is owned by the external provider (`point` at their docs); distinct from `outbound-webhook-contract` you emit.)_
280
+
281
+ #### `outbound-webhook-contract` — HIGH · transcribe
282
+
283
+ The payload schema, event-type taxonomy, signature, and retry semantics your service _emits_ to subscribers. **Where:** webhook dispatchers, Svix, `POST` to subscriber URLs with `X-Signature`. **Breaks:** subscribers parse your payload + verify your signature — renaming an event type, changing the payload, rotating the secret without notice, or altering the signature header silently breaks every (external, uncontrolled) subscriber. **Detect:** webhook-send code, event-type enums, signature construction, retry/backoff config. **Safe-change:** version the event payload (`api_version`); event-type names append-only; rotate secrets with overlap windows + notice; keep signature header + algorithm stable. _(Own home — the payload you emit is a transcribed contract you own, distinct from the inbound provider contract.)_
284
+
285
+ #### `auth-token-wire-contract` — CRITICAL · transcribe
286
+
287
+ How credentials travel per request and what the server expects — `Authorization: Bearer` vs cookie session, claim shape, header name, 401-challenge format. **Where:** Express auth middleware, `jose` verification, Better Auth cookie/session, `WWW-Authenticate`, `X-API-Key`. **Breaks:** the request-shape edge between client and auth — moving header→cookie, renaming the header, changing required claims, or altering the 401 challenge silently 401s every authenticated request; interacts with CORS credentials. **Detect:** `Authorization`, `Bearer`, `req.headers.authorization`, `jose`/`jwtVerify`, `X-API-Key`, cookie-session reads, `WWW-Authenticate`. **Safe-change:** keep transport (header vs cookie) + header name stable; required claims additive; coordinate issuance + verification together; align with CORS `credentials`/`allowedHeaders`. _(Folds into auth `auth-contract` — same credential source-of-truth, same `transcribe` doc-mode; this is its wire/transport view. Cross-ref auth `jwt-claims-contract` for the claim set.)_
288
+
289
+ #### `conditional-request-caching-contract` — MED · point
290
+
291
+ The HTTP caching/concurrency contract — `ETag`/`If-None-Match`, `Last-Modified`/`If-Modified-Since`, `If-Match` for optimistic concurrency. **Where:** Express `etag`, CDN/proxy caches, `304`/`412 Precondition Failed` on `PUT`/`PATCH`. **Breaks:** clients/proxies cache on `ETag` and gate writes on `If-Match` — changing the ETag algorithm (weak↔strong), dropping it, or altering `If-Match` enforcement → stale reads or lost-update writes returning unexpected `412`. **Detect:** `ETag`/`etag`, `If-None-Match`/`If-Match`, `Last-Modified`, `304`/`412` handling. **Safe-change:** keep ETag generation deterministic/stable; preserve `If-Match` enforcement on writes; keep `304`/`412` semantics consistent. _(Own home — the ETag/`If-Match` concurrency contract (`point`) is distinct from the `Cache-Control` TTL/CDN facet; cross-ref runtime `cache-header-contract`.)_
292
+
293
+ #### `idempotency-key-contract` — HIGH · transcribe
294
+
295
+ The dedup contract for safe retries of non-idempotent operations — an `Idempotency-Key` header (HTTP) or a dedup id (messaging) → cached/deduped result. **Where:** Stripe-style `Idempotency-Key`, payment/order APIs; and (messaging) SQS FIFO `MessageDeduplicationId`, Kafka `enable.idempotence`, BullMQ `jobId`. **Breaks:** client + server must agree on the key recipe + dedup window — changing how the key is derived makes previously-deduped ops look distinct → duplicate side effects (double charge/email); shortening the TTL re-executes late retries. **Detect:** `Idempotency-Key`/`idempotency_key`, dedup-store lookups, `MessageDeduplicationId`, `jobId:`, Powertools `@idempotent`. **Safe-change:** document exactly which fields compose the key; never change the recipe without a migration; keep the dedup window ≥ max retry horizon; persist the key with the result; co-version on both sides. _(Canonical home for both the HTTP-header and message-dedup facets.)_
296
+
297
+ ---
298
+
299
+ ## 3 · FRONTEND, CLIENT-STATE & UI
300
+
301
+ > Recurring failure mode in this category: **untyped string identifiers** — slice paths, cache tags, route paths, param keys, i18n/flag/event/storage/query keys — where producer and consumer drift with _no compile error_, surfacing only as silent runtime degradation. Universal fix: centralize each identifier set in one typed module and CI-gate drift.
302
+
303
+ #### `store-shape` — HIGH · transcribe
304
+
305
+ The serializable shape of the global client state tree (slice names, nesting, key paths) every reader/writer agrees on. **Where:** Redux Toolkit 2 `configureStore`+`createSlice`, Zustand, Jotai, Pinia. **Breaks:** renaming a slice or moving a key returns `undefined` to every consuming selector with no compile error if selectors use string paths; persisted state rehydrates into the wrong shape. **Detect:** `configureStore({ reducer: {…} })` (the reducer map _is_ the top-level shape), `createSlice`, `**/store.*`, `**/slices/**`. **Safe-change:** grep all `state.<slice>`/`useSelector` before renaming; bump `redux-persist` version + migration if persisted; slice names additive; co-locate selectors with the slice. _(Canonical home for the client-store tree (`transcribe`) — `selector-contract` keeps its own file (`point` read-API over the same store, different doc-mode); persisted-store rehydration cross-refs `client-storage-schema`.)_
306
+
307
+ #### `selector-contract` — MED · point
308
+
309
+ Memoized selectors whose input deps + output shape are an implicit API between store and components. **Where:** Reselect/RTK `createSelector`, `entityAdapter.getSelectors`, `useSelector`. **Breaks:** changing output shape (array→keyed object, +/- fields) breaks every reader; changing memo inputs causes stale results or re-render storms; an unstable return triggers infinite re-renders. **Detect:** `createSelector(`, `getSelectors(`, `**/selectors.*`, `select*` exports. **Safe-change:** treat exported selectors' return type as public; verify memo inputs cover added fields; never return a fresh reference unmemoized. _(Own home — the selector read-API is `point` over the same store as `store-shape` but a different doc-mode, so it files separately.)_
310
+
311
+ #### `rtk-query-cache-tags` — HIGH · point
312
+
313
+ The tag taxonomy (`providesTags`/`invalidatesTags`) wiring queries to mutation invalidation. **Where:** RTK Query `createApi`, `tagTypes`, `injectEndpoints`. **Breaks:** a tag typo or a mutation that forgets to invalidate → stale UI with no error; renaming a tag in one endpoint but not another breaks consistency; over-broad tags cause refetch storms. **Detect:** `createApi(`, `tagTypes:`, `providesTags:`, `invalidatesTags:`, `**/*Api.*`. **Safe-change:** define `tagTypes` as one const enum referenced everywhere (no inline literals); audit which queries provide an affected tag when adding a mutation; use `{type,id}` granularity. _(Own home — the RTK Query tag taxonomy is its own source-of-truth; the non-RTK equivalent is `query-cache-key-contract`.)_
314
+
315
+ #### `route-table` — HIGH · point
316
+
317
+ The route definition tree mapping URL patterns to components/loaders — the contract between the address bar and the app. **Where:** react-router 7 (`createBrowserRouter`, `routes.ts`, `<Route>`), TanStack Router, Next.js App Router. **Breaks:** renaming/restructuring a path breaks bookmarks, deep links, redirects, sitemaps, inbound `<Link>`/`navigate()`; framework-mode typegen + loader wiring break. **Detect:** `createBrowserRouter(`, `routes.{ts,tsx}`, `<Route path=`, `app/**/page.*`, `.react-router/types`. **Safe-change:** add a redirect from old→new on a public URL change; grep `<Link to=`/`navigate(`/`href=`; regenerate route types; keep route IDs stable if loaders reference them. _(Canonical home for the route tree (`point`) — `url-param-contract` folds in (the params the route table defines), unless the repo files param contracts separately.)_
318
+
319
+ #### `url-param-contract` — HIGH · point
320
+
321
+ The path/query/search-param names + types components read from the URL. **Where:** react-router `useParams`/`useSearchParams`, loader `params`, Next.js `searchParams`, nuqs. **Breaks:** renaming `:userId`→`:id` or `?tab=`→`?view=` yields `undefined` in reads while old links carry the old key; string-vs-number parsing diverges. **Detect:** `useParams(`, `useSearchParams(`, `params.<name>`, `searchParams.get(`, `:` segments, `useQueryState`. **Safe-change:** match the param name in route def + every read; validate with Zod at the boundary; support old + new keys during a deprecation window. _(Folds into `route-table` — the params are defined by the same router source-of-truth, same `point` doc-mode.)_
322
+
323
+ #### `component-prop-contract` — HIGH · point
324
+
325
+ The public props interface (+ defaults) of a reusable/published component. **Where:** React 19 `interface Props`, design-system / `@nurix/components` exports, headless-UI render props. **Breaks:** renaming/removing a prop, tightening a type, changing a default, or flipping controlled↔uncontrolled breaks every consumer; for a published package the blast radius is every importing app (semver). **Detect:** `interface *Props`, `React.FC<`, `**/components/**`, barrel `index.ts`, package `exports`. **Safe-change:** treat exported props as semver-governed; add props optional with safe defaults; deprecate don't delete; grep all `<ComponentName` usages before changing required props. _(Own home — a component's props interface is its own source-of-truth; for a published component cross-ref config `package-public-api` (the install-boundary view).)_
326
+
327
+ #### `react-context-contract` — MED · point
328
+
329
+ The value shape a Provider supplies + the expectation a matching Provider wraps every consumer. **Where:** React 19 `createContext`/`use`/`useContext`, theme/auth/router providers, Redux/RTK/i18next providers. **Breaks:** changing the value shape breaks all `useContext` readers; a consumer outside its provider gets the default (often `undefined` → crash) with no compile error; duplicate providers diverge silently. **Detect:** `createContext(`, `useContext(`, `*Provider`, `*Context`, `**/providers/**`. **Safe-change:** keep the value type stable (add, don't repurpose); throw a clear error in the hook if context is `undefined`; verify provider placement when moving routes/layouts. _(Own home — each Provider's value shape is its own source-of-truth.)_
330
+
331
+ #### `design-token-contract` — MED · transcribe
332
+
333
+ Named design tokens (colors/spacing/radii/type) exposed as utility classes + CSS custom properties, plus the theme-mode selector and required style imports. **Where:** Tailwind 4 `@theme` (CSS-first), `--color-*` custom properties, `dark:` variant / `data-theme`, `@nurix/components` styles import, Style Dictionary. **Breaks:** in Tailwind 4 every `@theme` token _is_ a CSS var + generates a utility — renaming a token removes the utility (`bg-brand` stops applying) and breaks `var(--color-brand)`; a mis-namespaced var is silently dropped; changing the mode selector (`.dark`→`[data-theme]`) disables every `dark:` style; forgetting the styles import renders components unstyled. **Detect:** `@theme {`, `--color-`/`--spacing-`, `var(--`, `tailwind.config.*`, `@apply`, `dark:`, `import '@nurix/components/styles'`. **Safe-change:** grep utility usages AND `var(--token)` before renaming; keep namespaces valid; provide alias tokens during a rename; keep one token source; verify rendered UI. _(Canonical home; cross-ref config `css-design-token-contract` for the build/import facet.)_
334
+
335
+ #### `i18n-message-keys` — MED · point
336
+
337
+ Translation keys (+ namespaces + interpolation placeholders) shared between code lookups and locale catalogs. **Where:** i18next/react-i18next `t('key')`, `Trans`, FormatJS `defineMessages`, `locales/{lng}/{ns}.json`. **Breaks:** renaming a key in code but not catalogs (or vice versa) shows the raw key/`missingKey`; missing interpolation vars render literal `{{name}}`; namespace load failure drops a screen's text. **Detect:** `t(`, `useTranslation(`, `<Trans`, `i18nKey=`, `**/locales/**`, `i18next-parser` config. **Safe-change:** run `i18next-parser --ci` to fail on code keys missing from catalogs; generate TS key types; keep placeholder names in sync across locales; configure `fallbackLng`. _(Own home — the translation-key set + catalogs are their own source-of-truth.)_
338
+
339
+ #### `form-schema-contract` — HIGH · point
340
+
341
+ Field names/types/validation shared between a form's fields, its schema, and the submit payload. **Where:** React Hook Form `register`+`zodResolver`, Zod 4, Formik+Yup, TanStack Form, `@nurix/components` form fields. **Breaks:** renaming a field in the schema but not `register('name')` leaves it unvalidated/unsubmitted silently; schema types diverge from the API payload; `defaultValues` keys must match field names or RHF drops them. **Detect:** `useForm(`, `register('`, `Controller name=`, `zodResolver(`, `defaultValues:`, `**/*.schema.ts`. **Safe-change:** derive field names from the Zod schema (`z.infer`) so a rename is a type error; align `defaultValues` keys; verify the submit payload still matches the server. _(Own home — the form-field↔schema↔payload contract is its own source-of-truth; cross-ref API `request-validation-schema` (the server side it must match).)_
342
+
343
+ #### `client-storage-schema` — HIGH · transcribe
344
+
345
+ The versioned shape of browser-persisted data future code must read back. **Where:** `localStorage`/`sessionStorage`, IndexedDB, localforage, `redux-persist` (`version`+`createMigrate`), Zustand `persist`, TanStack Query persisters. **Breaks:** changing the persisted shape without a migration means returning users rehydrate stale/incompatible data into new code — crashes on first load after deploy; localforage driver/db-name changes orphan data. **Detect:** `localStorage.`/`sessionStorage.`, `localforage.`, `indexedDB.open(`, `persistReducer(`, `createMigrate(`, `version:`, `persist(`, key literals. **Safe-change:** bump persist `version` + add a `createMigrate` migration per shape change; whitelist which slices persist; keep keys/DB names stable; guard rehydration with schema validation, reset on parse failure. _(Own home — the browser-persisted shape is its own versioned source-of-truth; cross-ref frontend `store-shape` when the persisted data is the store.)_
346
+
347
+ #### `feature-flag-gating` — MED · point
348
+
349
+ The flag keys + expected defaults gating client UI/behavior. **Where:** LaunchDarkly `useFlags`/`variation`, OpenFeature, Statsig, GrowthBook, Cloudflare Flagship. **Breaks:** a flag key is permanent — renaming/archiving without removing all code refs returns the default (often `false`), silently hiding a shipped feature; missing defaults cause load flicker; stale flags accumulate as dead branches. **Detect:** `useFlags(`, `variation(`, `getFlag(`, flag-key literals, `**/flags.*`, SDK init. **Safe-change:** centralize keys in one typed module; supply explicit "off"-safe defaults; archive only after grepping the key to zero refs. _(Folds into config `feature-flag-contract` — same flag-key source-of-truth; this is its client-gating facet.)_
350
+
351
+ #### `asset-public-path-contract` — MED · point
352
+
353
+ The base/public path + asset URL resolution mapping imports and `/public` refs to deployed URLs. **Where:** Vite `base` + `import.meta.env.BASE_URL`, `/public`, hashed filenames, CDN origins. **Breaks:** changing `base` (sub-path deploy) breaks hardcoded `/asset` URLs; `import.meta.env.BASE_URL` is statically replaced and must appear verbatim; moving to a CDN breaks relative refs. **Detect:** `base:` in `vite.config`, `import.meta.env.BASE_URL`, `/public/`, hardcoded `/assets/`, `new URL('...', import.meta.url)`. **Safe-change:** use `BASE_URL` verbatim for dynamic URLs; audit hardcoded paths when changing `base`/CDN; let the bundler rewrite imported assets. _(Own home — the public-path/asset-URL resolution is its own contract; cross-ref config `bundler-entry-output-externals` for output naming.)_
354
+
355
+ #### `ssr-hydration-contract` — HIGH · point
356
+
357
+ The requirement that server-rendered markup exactly matches the client's first render, and transferred state is serializable. **Where:** React 19 `hydrateRoot`, Next.js App Router, react-router 7 framework mode, Remix, Astro islands. **Breaks:** any divergence (locale dates, `Date.now()`, `window` access, random IDs, `typeof window` branches) throws a hydration mismatch and discards the server tree; a text mismatch is a hard error in React 19. **Detect:** `hydrateRoot(`, `renderToString`/`renderToPipeableStream`, `useSyncExternalStore`, `typeof window`, `new Date()` in render, `suppressHydrationWarning`. **Safe-change:** move client-only/non-deterministic logic into `useEffect`/`useSyncExternalStore`; render the same first-paint content on both sides; keep date/number formatting locale-stable. _(Own home — the markup-match requirement is distinct from the serializability rule in `state-serialization-contract`.)_
358
+
359
+ #### `state-serialization-contract` — HIGH · transcribe
360
+
361
+ The rule that data crossing server→client (or worker/postMessage) must be JSON-serializable; structured types are lost. **Where:** Next.js Server→Client props/RSC payload, TanStack Query `dehydrate`, react-router loader data, `postMessage`, `structuredClone`. **Breaks:** `Date`, `Map`, `Set`, `BigInt`, `undefined`, class instances, functions don't survive default serialization — a `Date` arrives as a string and consumers expecting a `Date` prototype diverge/throw; methods are stripped. **Detect:** `dehydrate(`, `HydrationBoundary`, `superjson`/`devalue`, `JSON.stringify`/`parse` across a boundary, Server→Client `props`, `postMessage(`. **Safe-change:** pass only JSON-safe values or use superjson/devalue; serialize `Date`/`Map`/`Set` explicitly + rehydrate; don't pass functions/class instances Server→Client. _(Own home — the server→client serialization rule; cross-ref runtime `worker-thread-boundary` for the structured-clone facet across threads.)_
362
+
363
+ #### `custom-element-contract` — MED · point
364
+
365
+ A custom element's public surface — tag name, observed attributes, properties, events, slots, CSS shadow parts. **Where:** Web Components (`customElements.define`, `observedAttributes`), Lit, Stencil; React 19 (now sets custom-element properties), Vue `defineCustomElement`. **Breaks:** renaming a tag breaks all usages + registration (double-register throws); attribute↔property reflection mismatches; renaming a slot leaves projected content unrendered; changing a `part` name breaks external `::part()` styling. **Detect:** `customElements.define(`, `observedAttributes`, `attributeChangedCallback`, `static properties`, `<slot name=`, `::part(`. **Safe-change:** keep tag/observed-attribute/slot/part names stable (public API); maintain attribute↔property symmetry; guard `define` against double-registration; treat emitted CustomEvent names/`detail` as contract. _(Canonical home for the custom-element public surface (`point`) — `dom-event-contract` folds in (the emitted-event facet of the same element).)_
366
+
367
+ #### `dom-event-contract` — LOW · point
368
+
369
+ Custom DOM/CustomEvent names + `detail` shapes components dispatch and listeners consume. **Where:** `dispatchEvent(new CustomEvent('name',{detail}))`, web-component events, Vue `emits`, `@dnd-kit` events. **Breaks:** renaming an event or changing `detail` silently breaks listeners (handler never fires / reads `undefined`); `bubbles`/`composed` affect shadow-DOM delivery. **Detect:** `new CustomEvent(`, `dispatchEvent(`, `addEventListener('`, `emit(`/`emits:`. **Safe-change:** treat event names + `detail` shapes as public (version them); keep `bubbles`/`composed` consistent with where listeners attach. _(Folds into `custom-element-contract` — same component DOM-surface source-of-truth, same `point` doc-mode; the emitted-event facet. Stands alone only for framework-agnostic events with no custom element.)_
370
+
371
+ #### `deep-link-scheme-contract` — HIGH · point
372
+
373
+ URL scheme / universal-link / app-link patterns routing external entry points (email, push, OAuth callbacks, mobile) into in-app destinations. **Where:** custom schemes (`myapp://`), iOS Universal Links / Android App Links (`apple-app-site-association`, `assetlinks.json`), OAuth redirect URIs, magic-links, react-router targets. **Breaks:** changing a path/scheme breaks links already sent in emails/push (irrecoverable) and OAuth provider allowlists; a missing AASA/assetlinks falls back to browser; redirect-URI mismatch breaks auth. **Detect:** `://` schemes, `apple-app-site-association`, `.well-known/assetlinks.json`, OAuth `redirectUri`/`callbackURL` (Better Auth `trustedOrigins`), `**/.well-known/**`. **Safe-change:** keep old deep-link/redirect paths alive with redirects; update OAuth allowlists + AASA/assetlinks in lockstep; never repurpose a path's meaning. _(Own home — the deep-link/scheme/AASA config is its own source-of-truth; cross-ref auth `redirect-uri-allowlist` for the OAuth-callback overlap.)_
374
+
375
+ #### `analytics-event-contract` — MED · transcribe
376
+
377
+ The event names + property keys + value types (the "tracking plan") shared between client instrumentation and downstream analytics/warehouse. **Where:** Segment `track`, Snowplow self-describing events, GA4 `gtag('event')`, Amplitude/Mixpanel/PostHog `capture`. **Breaks:** a tracking plan is a producer→consumer data contract — renaming an event/property or changing a type silently breaks dashboards/funnels and fragments historical continuity, with no app error. **Detect:** `track(`, `gtag('event'`, `capture(`, `logEvent(`, event-name literals, `**/analytics/**`. **Safe-change:** centralize names/keys in a typed module/schema; version events (add new, deprecate); validate against the tracking-plan schema in CI; property types additive. _(Own home — the analytics tracking plan is its own producer→consumer source-of-truth, distinct from domain `event-schema`.)_
378
+
379
+ #### `query-cache-key-contract` — HIGH · point
380
+
381
+ Cache-key arrays/structure identifying cached server-state entries + driving invalidation (non-RTK). **Where:** TanStack Query `queryKey`/`invalidateQueries`, SWR keys/`mutate`, Apollo `typePolicies`/`keyFields`/`__typename`. **Breaks:** a query key + its invalidation target must match exactly — a key-shape change in one place but not the invalidator → stale UI or permanent cache miss; Apollo normalization relies on stable `id`/`keyFields`/`__typename`. **Detect:** `queryKey:`, `useQuery(`, `invalidateQueries(`, `useSWR(`, `mutate(`, `typePolicies`, `keyFields`, `**/queries/**`. **Safe-change:** centralize query-key factories (never hand-write at call sites); keep invalidation keys structurally aligned; preserve Apollo `keyFields`/`__typename`. _(Own home — the TanStack/SWR/Apollo cache-key source-of-truth; the RTK equivalent is `rtk-query-cache-tags`.)_
382
+
383
+ ---
384
+
385
+ ## 4 · IDENTITY, AUTH & TENANCY
386
+
387
+ > See domain `tenancy` for the cross-layer tenant-isolation invariant. The CRITICAL tier here is anything whose failure is a _silent auth bypass or data disclosure_, not a loud error.
388
+
389
+ #### `auth-contract` — CRITICAL · transcribe
390
+
391
+ The credential a client presents + the server validates — session cookie vs bearer token, and each one's structure. **Where:** Better Auth `session` table (`token`, `userId`, `expiresAt`, `ipAddress`), jose-signed JWTs, Express auth middleware. **Breaks:** flipping stateful-session↔stateless-JWT makes every validator, logout/revocation path, and middleware silently fail or accept stale credentials; Better Auth uses `expiresAt` (not `expires`) and issues a _session token_ for email+password — assuming an access/refresh pair breaks consumers. **Detect:** `auth.ts`/`auth.config.*`, `betterAuth({…})`, `jose` (`jwtVerify`/`SignJWT`), `req.cookies`/`Set-Cookie`, session-table schema. **Safe-change:** confirm every validator agrees stateful vs stateless before flipping; keep field/cookie names byte-stable (renames = migrations); verify logout/revocation still invalidates; re-run Better Auth CLI migrate after a schema push. _(Canonical home for the credential contract (`transcribe`) — API `auth-token-wire-contract` folds in (its wire/transport view); `jwt-claims-contract` keeps its own file (the claim set, `point`).)_
392
+
393
+ #### `jwt-claims-contract` — CRITICAL · point
394
+
395
+ The set + meaning of claims (`iss`, `aud`, `sub`, `exp`, `nbf`, `scope`, custom) issuer and verifier must agree on. **Where:** jose/`jsonwebtoken`, RFC 9068 access-token JWTs, Keycloak/Auth0/WorkOS, gateway validators. **Breaks:** a valid token from a trusted issuer is still wrong if `aud` isn't enforced; `sub` is unique only within an `iss`, so keying on `sub` alone collides identities; adding/renaming a custom claim silently drops authz on old-name readers; loosening `iss`/`aud` opens token-substitution. **Detect:** `jwtVerify(token, key, { issuer, audience })`, claim string reads (`payload.role`), JWKS URL config. **Safe-change:** always enforce `iss` AND `aud`; key identity on `(iss, sub)`; treat custom claims as versioned; bounded clock skew (≤60s); claims additive. _(Own home — the claim set is `point` source-of-truth, distinct from the `transcribe` credential contract `auth-contract`; the verifier's key edge is `signing-key-rotation`.)_
396
+
397
+ #### `signing-key-rotation` — CRITICAL · point
398
+
399
+ The trust edge where the verifier fetches + caches the issuer's public keys (JWKS). **Where:** `/.well-known/jwks.json`, `kid` header, jose `createRemoteJWKSet`, Keycloak/Auth0, SAML signing certs. **Breaks:** on rotation, verifiers caching stale JWKS reject all fresh tokens — teams then "temporarily" disable validation forever; `alg` confusion (`none`, RS↔HS) is a classic bypass. **Detect:** `createRemoteJWKSet`, `jwks_uri`, `kid`, hardcoded PEMs, `algorithms: [...]`, cache TTL. **Safe-change:** force a JWKS re-fetch on verify-failure-with-cached-key before rejecting; pin an explicit `algorithms` allowlist (never `none`); overlap old+new keys until old tokens expire; match cache TTL to rotation cadence. _(Own home — the JWKS/key-rotation trust edge is a distinct source-of-truth from the claim set.)_
400
+
401
+ #### `refresh-token-rotation` — HIGH · point
402
+
403
+ The exchange contract for a refresh token → new access token: single-use rotation, grace windows, replay revocation. **Where:** OAuth2 token endpoint, Better Auth `oauthRefreshToken` table (`token`, `clientId`, `revoked`, `scopes`, `expiresAt`). **Breaks:** if rotation marks old tokens `revoked` and a client races two requests, the second is a replay and the whole family is revoked; changing TTL/grace/single-use semantics silently logs users out or leaves stolen tokens valid. **Detect:** `oauthRefreshToken`/`refresh_tokens` schema, `revoked` columns, `grant_type=refresh_token`, rotation config. **Safe-change:** decide single-use vs grace-window explicitly; scope replay revocation to the family not the user (unless intended); store hashed; test concurrent-refresh races. _(Own home — the refresh-token exchange/rotation table is its own source-of-truth; cross-ref `oauth-oidc-flow` for the grant.)_
404
+
405
+ #### `session-cookie-attributes` — HIGH · point
406
+
407
+ The flags on the session cookie — `SameSite`, `Secure`, `HttpOnly`, `Domain`, `Path`, `Max-Age`. **Where:** `Set-Cookie`, Better Auth cookie config, Express `res.cookie`/`cookie-session`, proxy cookie rewriting. **Breaks:** `SameSite=Lax→None` disables CSRF defense; dropping `Secure` allows interception; changing `Domain` breaks cross-subdomain auth or over-shares to siblings; `HttpOnly=false` exposes the session to XSS — all silent (the cookie still "works"). **Detect:** `res.cookie(...{sameSite,secure,httpOnly,domain})`, `Set-Cookie`, `cookiePrefix`/`crossSubDomainCookies`, `proxy_cookie_domain`. **Safe-change:** keep `HttpOnly`+`Secure` always; don't widen `Domain` without auditing siblings; pair `SameSite=None` with explicit CSRF; treat cookie name/prefix as cross-service contract. _(Own home — the cookie-flag set is its own source-of-truth; cross-ref `csrf-token-contract` (the `SameSite`/double-submit interaction).)_
408
+
409
+ #### `csrf-token-contract` — HIGH · point
410
+
411
+ The anti-CSRF mechanism (synchronizer token, double-submit cookie, origin check) + where the token is read/validated. **Where:** `csurf`/custom middleware, Better Auth CSRF + trusted-origins, `X-CSRF-Token` header + double-submit cookie. **Breaks:** SameSite alone doesn't cover mutating GETs/sibling-domain vulns — if the frontend stops sending the token, or the cookie becomes `HttpOnly` (breaking double-submit read), state-changing requests fail or lose protection; changing trusted-origins silently blocks/admits callers. **Detect:** `csrf`/`csurf`, `X-CSRF-Token`/`X-XSRF-Token`, `trustedOrigins`/`allowedOrigins`, double-submit cookie not `HttpOnly`. **Safe-change:** keep header + cookie names in lockstep FE/BE; double-submit CSRF cookie must be JS-readable; update `trustedOrigins` on every new frontend origin. _(Own home — the anti-CSRF mechanism is its own source-of-truth; cross-ref `session-cookie-attributes` for the `SameSite` interaction.)_
412
+
413
+ #### `row-level-security` — CRITICAL · transcribe
414
+
415
+ DB-enforced policies backstopping app-layer tenant/authz logic. **Where:** Postgres `CREATE POLICY`/`ENABLE`/`FORCE ROW LEVEL SECURITY`, GUC `app.current_tenant`, Supabase RLS. **Breaks:** RLS only works as well as the context fed it — a new table without a policy, a `BYPASSRLS` role, a missing `FORCE`, or a forgotten `SET LOCAL` silently returns all rows; a query path using a superuser/owner connection bypasses every policy. **Detect:** `ENABLE/FORCE ROW LEVEL SECURITY`, `CREATE POLICY`, `current_setting('app.current_tenant')`, `BYPASSRLS` grants, migrations lacking policies on new tenant tables. **Safe-change:** add a policy to every new tenant table in the same migration; `FORCE` so owners are constrained too; run app queries under a non-`BYPASSRLS` role; verify the tenant GUC on every connection path. _(Canonical home for the RLS-policy contract (`transcribe`) — persistence `row-level-multi-tenancy` folds in (its persistence-layer view); backstops domain `tenancy` (the cross-layer invariant, its own home).)_
416
+
417
+ #### `object-level-authorization` — CRITICAL · point
418
+
419
+ Per-object/per-instance permission checks (the IDOR surface), tying a subject to a specific resource via relationships. **Where:** Google Zanzibar model, OpenFGA/SpiceDB/Permify (`object#relation@user`), Oso, app `can(user, action, resource)`. **Breaks:** endpoints that authorize the action but not the specific object id are IDOR-vulnerable; changing a relationship schema silently grants/denies access to existing objects; cache-staleness serves outdated decisions. **Detect:** OpenFGA/SpiceDB schema (`.fga`), `Check`/`ListObjects`, tuple-writes, route handlers looking up `:id` without an ownership check, Oso policies. **Safe-change:** authorize the specific resource id, not just the action/role; treat relationship-schema edits as access-changing migrations; invalidate authz caches on relationship writes; keep tuple-writes transactional with the resource mutation. _(Own home — the relationship/ReBAC authz model is its own source-of-truth, distinct from `rbac-permission-model` and `abac-policy-engine`.)_
420
+
421
+ #### `rbac-permission-model` — HIGH · transcribe
422
+
423
+ The role/permission taxonomy — role names, permission strings, role→permission maps — shared between policy and enforcement. **Where:** Better Auth `admin`/`organization` plugins, permission strings (`billing:read`), enum role columns, middleware guards. **Breaks:** renaming a role or permission string (`admin`→`owner`, `post:write`→`posts:create`) breaks every guard checking the old string — silently denying, or granting if a default-allow exists; a new role without seeded permissions is powerless or over-powered. **Detect:** permission-string literals, `hasPermission`/`requireRole`, role enums, `ac.newRole(...)`, seed files. **Safe-change:** centralize permission strings in one module; migrate role assignments on rename; unknown permission = default-deny; grep all enforcement points on any change. _(Own home — the role/permission taxonomy is its own `transcribe` source-of-truth, distinct from the per-object (`object-level-authorization`) and attribute-policy (`abac-policy-engine`) models.)_
424
+
425
+ #### `abac-policy-engine` — HIGH · point
426
+
427
+ Attribute/condition policies evaluated by an external engine against a structured input. **Where:** OPA/Rego, AWS Cedar/Verified Permissions, Oso, Cerbos. **Breaks:** the engine + caller share an _input schema_ (the attributes shipped per decision) — if the app stops sending an attribute the policy relies on, it silently denies (or default-allows); Rego/Cedar semantic changes alter outcomes with no app code change. **Detect:** `.rego`/`.cedar`/Cerbos YAML, OPA `data`/`input` calls, `authorize(input)`, default-decision rules. **Safe-change:** version the decision-input schema + validate before sending; explicit `default allow = false`; deploy policy + app in lockstep on input changes; test missing/null attributes. _(Own home — the external policy + decision-input schema is its own source-of-truth, distinct from `rbac-permission-model` and `object-level-authorization`.)_
428
+
429
+ #### `oauth-oidc-flow` — HIGH · point
430
+
431
+ The OAuth2/OIDC flow contract — grant type, PKCE, `state`/`nonce`, token endpoint, provider metadata. **Where:** Better Auth social/OIDC providers, OAuth 2.1 plugin, Auth.js, Keycloak/Auth0/Entra, `.well-known/openid-configuration`. **Breaks:** switching grants (implicit→code+PKCE), dropping `state`/`nonce`, or changing scopes breaks the callback and can reopen CSRF/replay; provider metadata changes (endpoints, algs) silently break the flow. **Detect:** `response_type`/`grant_type`, `code_verifier`/`code_challenge`, `state`/`nonce`, provider config, callback handlers. **Safe-change:** use auth-code + PKCE, enforce `state` + (OIDC) `nonce`; keep scopes + callback URL synced to provider registration; validate the ID token on callback; refresh provider metadata. _(Own home — the grant/flow contract; the callback allowlist (`redirect-uri-allowlist`) and consent records (`consent-scope-grant`) keep their own files.)_
432
+
433
+ #### `redirect-uri-allowlist` — CRITICAL · point
434
+
435
+ The set of pre-registered, exact-match redirect/callback URIs an auth server will redirect to. **Where:** OAuth/OIDC client registration, Better Auth `redirectURLs`/`trustedOrigins`, Google/Auth0/Entra app config. **Breaks:** loose matching (wildcard/prefix/regex) enables authorization-code exfiltration + open-redirect token theft; adding a frontend origin without registering its callback silently breaks login. **Detect:** `redirect_uri`/`redirectURLs`/`callbackURL`, wildcard/`startsWith` URI matching, provider registration, `trustedOrigins`. **Safe-change:** exact 1:1 string comparison (no wildcards/regex/prefix); require HTTPS absolute URIs; register every new callback before deploying the client; on unmatched URI, error without redirecting. _(Own home — a CRITICAL security surface kept separate from `oauth-oidc-flow`; cross-ref frontend `deep-link-scheme-contract` for the in-app deep-link overlap.)_
436
+
437
+ #### `api-key-auth` — HIGH · point
438
+
439
+ Format, storage, scoping, validation of API keys / service tokens for machine-to-machine auth. **Where:** Better Auth `apiKey` plugin, `X-API-Key`, mTLS client certs, signed service JWTs, HMAC request signing. **Breaks:** changing the key prefix, hashing scheme, or scope encoding silently invalidates live integrations or accepts forged keys; plaintext-stored keys turn a DB leak into total compromise; rotating a shared key without a dual-validity window breaks every caller at once. **Detect:** `X-API-Key`/`Authorization: ApiKey`, `apiKey` config, key-hashing code, prefix constants, mTLS/cert config, HMAC verification. **Safe-change:** store hashed + constant-time compare; scope keys least-privilege + tie to a principal; rotate with an overlap window; keep prefix/format stable (changes = migration). _(Own home — the M2M API-key format/storage/scoping is its own source-of-truth.)_
440
+
441
+ #### `sso-saml-assertion` — HIGH · point
442
+
443
+ The SAML SP↔IdP trust contract — assertion signing, `NameID` format, attribute mapping, `entityID`, metadata. **Where:** SAML 2.0 IdPs (Entra, Okta, authentik), SP libraries, metadata XML, signing certs. **Breaks:** if the IdP rotates its signing cert without the SP re-importing metadata, all assertions fail signature validation; changing `NameID` format re-keys identity and can duplicate accounts; renaming an IdP attribute drops role/group mapping. **Detect:** IdP metadata XML/URLs, `entityID`/issuer, `NameIDFormat`, attribute mapping, X.509 fingerprints, ACS URL. **Safe-change:** re-import metadata on cert rotation; pin `NameID` format + key on a stable identifier; validate the signature on the right XML element; keep `entityID`/ACS synced; overlap IdP certs during rotation. _(Own home — the SAML SP↔IdP trust contract is its own source-of-truth, distinct from the OIDC flow.)_
444
+
445
+ #### `impersonation` — HIGH · transcribe
446
+
447
+ The act-as / admin-assume mechanism + its audit/scope guarantees. **Where:** Better Auth admin `impersonateUser`, support "log in as", Entra `user_impersonation` scope, act-as session claims. **Breaks:** an impersonation session not carrying the _real actor_ breaks audit attribution; missing read-only constraints/auto-expiry/separate audit turns support tooling into undetectable privilege escalation. **Detect:** `impersonate`/`actAs`/`onBehalfOf`, `impersonatedBy`/`actorId` session fields, admin-plugin config, impersonation audit writes + TTL. **Safe-change:** carry both real-actor + target identity; log every start/stop to an immutable trail; auto-expire (~30 min) + tightly scope; revoke active sessions on suspicious grants. _(Own home — the act-as mechanism + its audit/scope guarantees are their own source-of-truth.)_
448
+
449
+ #### `consent-scope-grant` — HIGH · transcribe
450
+
451
+ The record of which scopes a user consented to for a client + how it's enforced/revoked. **Where:** OAuth consent screens, Better Auth OAuth provider consent + `oauthAccessToken` scopes, Entra consent grants. **Breaks:** adding a scope to a client without re-prompting silently grants access the user never approved (or fails enforcement); illicit consent grants are an attack vector; no revocation leaves a compromised app with standing access. **Detect:** `scopes`/`scope` columns, consent handlers, `prompt=consent`, consent-grant records, revocation endpoints. **Safe-change:** re-prompt consent when adding scopes; enforce the granted scope set on every token (not just issuance); provide + test revocation propagating to live tokens; audit unusual scope/client pairs. _(Own home — the consent-grant record + enforcement is its own source-of-truth; cross-ref `oauth-oidc-flow` for the scope-issuance flow.)_
452
+
453
+ #### `per-identity-rate-limit-quota` — MED · transcribe
454
+
455
+ Rate limits + usage quotas keyed per identity/tenant/API-key. **Where:** `express-rate-limit`, Better Auth rate-limit, Redis token-bucket, gateway/Cloudflare per-key limits, plan quotas. **Breaks:** changing the limit _key_ (IP→user→tenant) lets one tenant exhaust a shared bucket (noisy-neighbor) or throttles a whole tenant for one user; a non-shared counter store breaks limits across instances. **Detect:** `rateLimit({ keyGenerator })`, Redis `INCR`/token-bucket keys, `windowMs`/`max`, quota tables, `429` handling. **Safe-change:** pick the limit key deliberately + document; shared store (Redis) across instances; keep quota config synced to billing plans; run auth before rate-limit keying. _(Own home — the per-identity rate-limit/quota keying is its own source-of-truth.)_
456
+
457
+ #### `encryption-at-rest-key-boundary` — CRITICAL · point
458
+
459
+ The boundary between encrypted data, its DEKs, and the KEKs/root keys in a KMS. **Where:** AWS/GCP KMS envelope encryption, field/column-level encryption, Confluent CSFLE. **Breaks:** rotating/losing the KEK without re-wrapping DEKs renders data permanently unreadable; widening who can `Decrypt` silently expands PII access; changing the DEK-per-record scheme breaks decryption of historical data. **Detect:** KMS calls (`GenerateDataKey`/`Decrypt`/`Encrypt`), `keyId`/KEK ARNs, envelope-wrap code, encrypted-column markers, KMS key policies. **Safe-change:** rotate KEKs not DEKs; restrict `Decrypt` least-privilege + audit; store `keyId`/version with ciphertext; never drop a KEK while any DEK is wrapped under it. _(Own home — the KEK/DEK/KMS key hierarchy is its own source-of-truth; cross-ref config `secrets-management` for non-data secrets.)_
460
+
461
+ #### `pii-classification` — HIGH · transcribe
462
+
463
+ Labeling of which fields are PII/PHI/financial + the handling rules (encryption, masking, logging, retention) it drives. **Where:** classification tags in schema, masking middleware, `pino` redact paths, DLP rules, retention policies. **Breaks:** adding a PII field without classifying it lets it flow into logs/analytics/unencrypted columns; changing what counts as PII without updating redaction/encryption opens a quiet leak — classification is the input _other_ boundaries consume. **Detect:** `pino` `redact`, masking utils, classification tags, encrypted-column lists, DLP/retention config, log statements emitting user fields. **Safe-change:** classify every new personal-data field at schema time; wire classification to redaction + encryption + retention; re-audit redaction on rename/add; one canonical classification source. _(Own home — the PII/PHI classification is the input other boundaries consume (encryption, masking, logging, retention); keep it canonical and singular.)_
464
+
465
+ #### `account-linking-identity-resolution` — HIGH · point
466
+
467
+ The rule merging multiple credentials (social + password, multiple IdPs) into one user. **Where:** Better Auth `account` table + `accountLinking` config, OAuth `provider`+`providerAccountId`, email-based auto-linking. **Breaks:** auto-linking by _unverified_ email enables account takeover; changing the linking key (email vs `(provider, providerAccountId)`) silently merges/splits accounts; toggling auto-link changes who can claim an existing account. **Detect:** `account` schema (`providerId`/`accountId`/`userId`), `accountLinking`/`trustedProviders`, email-match merge logic, find-or-create handlers. **Safe-change:** only auto-link on a _verified_ email from trusted providers; key external identities on `(provider, providerAccountId)` not email; require re-auth before merging; audit linking-config changes. _(Own home — the credential-merge/identity-resolution rule is its own source-of-truth.)_
468
+
469
+ #### `mfa-step-up-contract` — HIGH · point
470
+
471
+ Which actions require MFA, the factor types, and how "verified-recently" is represented. **Where:** Better Auth `twoFactor`/`passkey`, TOTP/WebAuthn, `amr`/`acr` claims, AAL levels, step-up gates. **Breaks:** a sensitive endpoint checking "is authenticated" but not "passed MFA / recently re-verified" silently allows it without step-up; changing the factor, freshness window, or `acr`/`amr` claim names weakens/breaks the gate. **Detect:** `twoFactor`/`webauthn`/`passkey` config, `acr`/`amr`/`aal` reads, step-up/`requireFreshSession` guards, `mfa_verified_at` fields. **Safe-change:** gate sensitive actions on factor level + recency, not just "logged in"; keep `acr`/`amr`/AAL names stable; enforce a freshness window; recovery factors without weakening the primary gate. _(Own home — the MFA/step-up gate + freshness representation is its own source-of-truth.)_
472
+
473
+ #### `service-account-workload-identity` — HIGH · point
474
+
475
+ Non-human principal identity — service accounts, workload-identity federation, instance/pod IAM roles — + the trust minting their tokens. **Where:** K8s ServiceAccount/OIDC, GCP workload identity, AWS IRSA/instance profiles, SPIFFE/SPIRE, GitHub Actions OIDC. **Breaks:** changing a workload token's audience/subject, or the trust-policy `sub`/`aud` condition (e.g. GitHub `repo:org/name:ref`), silently breaks federation or — if loosened — lets an untrusted workload assume the role. **Detect:** IAM trust policies with OIDC conditions, `audience`/`sub` conditions, K8s `serviceAccountName`, SPIFFE IDs, workload-identity-pool config. **Safe-change:** scope trust conditions tightly (avoid wildcards); keep token audience synced between issuer + trusting resource; short-TTL tokens; review trust edits as privilege-grant changes. _(Own home — non-human workload identity + its trust-minting is its own source-of-truth, distinct from human-user credential contracts.)_
476
+
477
+ #### `password-credential-policy` — HIGH · point
478
+
479
+ The password hashing scheme + credential-handling rules (algorithm, params, breach checks, reset-token shape). **Where:** Better Auth `emailAndPassword` (scrypt default) / custom `password.hash`, argon2/bcrypt, reset/verification token tables. **Breaks:** changing the hash algorithm/params without a rehash-on-verify path silently locks out every existing user; weakening params lowers crack resistance; reset/verification-token shape + TTL is its own contract. **Detect:** `password.hash`/`verify` config, argon2/bcrypt/scrypt imports, credential columns, reset-token tables + TTL. **Safe-change:** rehash transparently on next login when changing algorithm; params ≥ current OWASP guidance; reset/verification tokens single-use + expiring; never log raw credentials/reset tokens. _(Own home — the password-hashing scheme + credential-handling rules are their own source-of-truth.)_
480
+
481
+ ---
482
+
483
+ ## 5 · ASYNCHRONOUS & MESSAGING
484
+
485
+ > The CRITICAL tier here (`event-schema`, `cdc-stream-contract`, `workflow-determinism-contract`, `schema-registry-contract`) shares one trait: failure breaks **persisted / in-flight / replayed** state — silent _and_ historical, not just live. The universal defense: producer/consumer version skew survives a rolling deploy only if payloads are additive + versioned and both sides share one validated schema.
486
+
487
+ #### `message-queue-payload` — HIGH · transcribe
488
+
489
+ The shape of a job/message body enqueued by a producer + deserialized by a worker. **Where:** BullMQ/Bull (`queue.add`→`Worker`), Redis streams, SQS, RabbitMQ, Kafka, Sidekiq/Celery. **Breaks:** producer + consumer deploy/version independently — add/rename/retype a field and **in-flight jobs already on the queue** carry the old shape while the new worker expects the new (or vice versa during rolling deploys); JSON has no queue-layer schema enforcement. **Detect:** `queue.add(`, `new Queue(`/`new Worker(`, `.process(`, `sqs.sendMessage`, `channel.sendToQueue`, a shared DTO imported by both sides (absent = high-risk). **Safe-change:** payload changes additive (optional + default); tag with `schemaVersion` the worker branches on, or drain before a breaking change; share one validated schema validated on enqueue AND dequeue; account for the mixed-version deploy window. _(Canonical home for the job/message-body contract (`transcribe`) — `job-name-contract` keeps its own file (the routing string, `point`).)_
490
+
491
+ #### `job-name-contract` — MED · point
492
+
493
+ The string job/task name routing a queued item to a processor. **Where:** BullMQ named jobs (`job.name`), Celery task names, Sidekiq classes, Temporal Activity Type names. **Breaks:** rename the job string on the producer without the worker and the job lands in the queue but **no processor matches** — it sits unprocessed or hits a default/throw; the compiler sees two unrelated literals. **Detect:** the literal on both sides, `switch (job.name)`, `@Process('name')`. **Safe-change:** centralize names in a shared enum; on rename, handle both old + new until the old queue drains; assert an exhaustive handler (default throws/logs). _(Own home — the job-routing string is `point`, a different doc-mode from the `transcribe` payload `message-queue-payload`, so it files separately.)_
494
+
495
+ #### `event-schema` — CRITICAL · transcribe
496
+
497
+ A domain/integration event's fields/types/semantics shared between one publisher and many subscribers. **Where:** Kafka topics, NATS subjects, EventBridge, Redis pub/sub, event-sourced domain events, the `@nurix/chat` wire envelope. **Breaks:** a breaking field change ripples to every subscriber, each failing at different deploy times; events are often **persisted** (event store/Kafka log) so old-shape events must remain readable forever (replay). **Detect:** `emit(`/`publish(`, topic/subject constants, event-type discriminators (`type`/`eventType`), `*.avsc`/`*.proto`/`events/*.ts`. **Safe-change:** events are append-only — add fields, never remove/retype; new variants get new discriminators; keep old-version deserialization while old events exist; carry an explicit `version`; use a schema registry / shared package with CI compatibility checks. _(Canonical home for the domain/integration event shape (`transcribe`) — `command-event-boundary` folds in (the CQRS command/event split over the same events); `topic-subject-naming` keeps its own file (the rendezvous string, `point`); cross-ref `schema-registry-contract` when a registry enforces it.)_
498
+
499
+ #### `topic-subject-naming` — HIGH · point
500
+
501
+ The string identifying a topic/subject/exchange/routing-key/channel producers write to and consumers subscribe to. **Where:** Kafka topics, NATS subjects (wildcards), RabbitMQ exchange + routing keys, MQTT, Debezium CDC topics, SNS ARNs, Redis pub/sub channels. **Breaks:** the string is the rendezvous point — renaming a topic, changing an env prefix, or altering a routing-key wildcard makes producers/consumers silently stop meeting (publish into the void); Debezium derives topic names from table names, so a table rename relocates the stream. **Detect:** topic/subject literals, `subscribe(`, `routingKey`, `bindQueue`, `topic.prefix`, `KAFKA_TOPIC_PREFIX`. **Safe-change:** centralize names + derive env prefixes from one helper; dual-publish to old + new on rename; pin Debezium/CDC topic naming explicitly; verify wildcard patterns after segment changes. _(Own home — the topic/subject rendezvous string is `point`, a different doc-mode from the `transcribe` `event-schema`, so it files separately.)_
502
+
503
+ #### `command-event-boundary` — HIGH · transcribe
504
+
505
+ The CQRS split: commands (imperative, single handler, may reject) vs events (factual, past-tense, fan-out), and each one's contract. **Where:** CQRS/event-sourced systems, Axon, EventStoreDB, Kafka command/event topics, NestJS CQRS. **Breaks:** mislabeling or reshaping one breaks read-model projections (events) or write-side validation (commands); read models rebuild by replaying events, so an event-contract change corrupts every projection. **Detect:** `commands/`/`events/`, `*Command`/`*Event`, `CommandHandler`/`EventHandler`, `aggregate.apply(`, past-tense vs imperative naming. **Safe-change:** events immutable + additive (version, don't mutate); projections handle old versions (upcasting); don't conflate command + event shapes; test projection rebuild from the full stream. _(Folds into `event-schema` — same event source-of-truth, same `transcribe` doc-mode; the CQRS command/event split facet. Stands alone in a dedicated CQRS module.)_
506
+
507
+ #### `cdc-stream-contract` — CRITICAL · point
508
+
509
+ A change-data-capture stream's row-shape envelope (before/after/op), topic mapping, and source DB schema downstream consumers depend on. **Where:** Debezium, Kafka Connect, AWS DMS, Postgres logical replication, Snowflake Streams, Materialize. **Breaks:** the stream's shape is **derived from the source DB schema** owned by a different team that may not know consumers exist — a column rename/type change/table rename silently reshapes or relocates the stream; the DB migration looks innocuous on the producing side. **Detect:** `debezium`, connector JSON, `slot.name`, `publication`, `REPLICA IDENTITY`, `transforms` SMT, `before`/`after`/`op` envelope fields, `pgoutput`. **Safe-change:** treat CDC-source columns as a published contract + coordinate migrations with consumers; pin topic routing + `REPLICA IDENTITY`; schema-registry with compatibility enforcement; dual-emit before dropping. _(Own home — the CDC stream envelope is its own source-of-truth (derived from, but distinct from, `db-schema`); cross-ref `schema-registry-contract` when a registry enforces it.)_
510
+
511
+ #### `cron-schedule-contract` — MED · point
512
+
513
+ A scheduled job's cron expression/interval, timezone assumption, expected frequency, and the idempotency that frequency implies. **Where:** BullMQ repeatable jobs/`JobScheduler`, node-cron, Temporal Schedules, K8s CronJobs, EventBridge Scheduler, Cloudflare Cron Triggers, GitHub Actions `schedule`, pg_cron. **Breaks:** the schedule string + job logic are an implicit contract — tightening "daily"→"hourly" duplicates side effects; a timezone (UTC vs local) shift changes the run instant; BullMQ repeat-key changes spawn duplicate schedulers; DST silently shifts run times. **Detect:** cron strings, `repeat: { pattern/every }`, `Cron(`, `schedule:`, `kind: CronJob`, `ScheduleExpression`, `tz`/`timezone`, `[triggers] crons`. **Safe-change:** make jobs idempotent so frequency/overlap changes don't double-apply; pin + document timezone (prefer UTC, account for DST); remove the old repeatable job when changing a BullMQ repeat key; add overlap guards. _(Canonical home for the cron contract — runtime `cron-timezone-contract` folds in (the scheduler-TZ/infra facet of the same schedule).)_
514
+
515
+ #### `workflow-determinism-contract` — CRITICAL · point
516
+
517
+ The requirement that workflow code replays deterministically against persisted event history, making any logic change a versioning event. **Where:** Temporal, Cadence, Step Functions, Azure Durable Functions, Restate, Inngest. **Breaks:** a running workflow's state is reconstructed by **replaying its history** through current code — adding/removing/reordering an `await` on a command-producing API, changing branching, or reordering activities makes replay diverge → non-determinism error on already-running workflows; the change passes new-workflow tests but bricks in-flight executions. **Detect:** `proxyActivities`, `executeActivity`, `workflow.sleep`, `condition(`, `patched(`/`getVersion(`, `defineSignal`/`defineQuery`, Worker Versioning Build IDs, task queue names. **Safe-change:** gate logic changes with `patched()`/`getVersion()` (or Build IDs) so old histories replay on old code; keep I/O/randomness/time in Activities; replay against real production histories in CI; don't remove a patch marker until pre-patch workflows complete. _(Canonical home for the workflow contract (`point`) — `workflow-signal-query-contract` and `workflow-activity-contract` fold in (the signal/query and activity surfaces of the same worker), unless the repo files them separately.)_ _(This repo ran Temporal — project memory notes the cf-realtime migration away from it.)_
518
+
519
+ #### `workflow-signal-query-contract` — HIGH · point
520
+
521
+ Names + arg/return shapes of Temporal Signals (async input), Queries (sync read), Updates external callers send into a running workflow. **Where:** Temporal `defineSignal`/`defineQuery`/`defineUpdate`, Cadence signals, Durable Functions external events. **Breaks:** rename a signal or change its args and senders silently send signals **no handler matches** (dropped) or with the wrong shape; workflows already running with the old registration won't see the new name. **Detect:** `defineSignal('name')`, `setHandler(`, `workflow.signal(`, `handle.query(`, name literals on both sides. **Safe-change:** centralize signal/query/update names + arg types in a shared module; add new names alongside old (keep old handlers until pre-change workflows finish); validate arg shapes in the handler; version payloads like event schemas. _(Folds into `workflow-determinism-contract` — same workflow-worker source-of-truth, same `point` doc-mode; the signal/query/update surface.)_
522
+
523
+ #### `workflow-activity-contract` — HIGH · point
524
+
525
+ The Activity Type name + input/output shapes + timeout/retry policy between a workflow and its activity worker. **Where:** Temporal/Cadence Activities, Step Functions tasks, Durable Functions activities. **Breaks:** activities run on separately-deployed workers polling a task queue — change an activity's shape or rename its type and workflows scheduling the old shape hit workers expecting the new (or "activity not registered"); changing retry/timeout changes failure semantics the workflow assumes. **Detect:** `proxyActivities<…>`, `@activityMethod`, `registerActivities`, `ActivityOptions`, `startToCloseTimeout`, `RetryPolicy`, activity-type + task-queue constants. **Safe-change:** share activity input/output types via one interface module; register new versions before removing old (keep old workers until old workflows drain); treat timeout/retry as part of the contract. _(Folds into `workflow-determinism-contract` — same workflow-worker source-of-truth, same `point` doc-mode; the activity-type surface.)_
526
+
527
+ #### `delivery-semantics-contract` — HIGH · transcribe
528
+
529
+ The shared assumption about delivery guarantee — at-most-once / at-least-once / effectively-once — between the messaging layer and the handler. **Where:** SQS standard vs FIFO, Kafka (at-least-once default; EOS with transactions), RabbitMQ acks, NATS JetStream, BullMQ. **Breaks:** a handler assuming exactly-once on an at-least-once queue double-applies side effects; switching a queue type (FIFO→standard, Kafka EOS on/off) silently flips the guarantee under handlers assuming the old; auto-ack vs manual-ack changes whether a crash loses or redelivers. **Detect:** `.fifo` suffix, `ContentBasedDeduplication`, `enable.idempotence`/`isolation.level=read_committed`, `noAck`/`autoAck`, `visibilityTimeout`. **Safe-change:** make handlers idempotent regardless of the claimed guarantee (assume at-least-once); document the guarantee + re-audit on queue-type change; treat manual-ack/visibility-timeout as contract. _(Own home — the delivery-guarantee assumption is its own transcribed contract.)_
530
+
531
+ #### `message-ordering-partition-key` — HIGH · point
532
+
533
+ The key/strategy determining ordering + which partition/shard/group a message lands in. **Where:** Kafka partition key, SQS FIFO `MessageGroupId`, Kinesis partition key, RabbitMQ consistent-hash, Pub/Sub ordering keys. **Breaks:** ordering is only guaranteed _within_ a partition/group — changing the key derivation scatters related messages across partitions, silently losing ordering (events processed out of order corrupt state machines); changing partition _count_ also remaps keys. **Detect:** `partitionKey`, `MessageGroupId`, `key:` in `producer.send`, `partitioner`, `num.partitions`, consumers assuming sequential per-entity processing. **Safe-change:** keep the partition/group key stable + document the entity it orders by; avoid changing partition count for keyed topics (plan a re-key migration); verify consumers don't assume cross-partition global ordering. _(Own home — the ordering/partition-key strategy is its own source-of-truth.)_
534
+
535
+ #### `outbox-pattern-contract` — HIGH · point
536
+
537
+ The contract making local DB writes + message publishing atomic via an outbox table a relay publishes. **Where:** transactional outbox with Debezium/polling relays, Kafka Connect outbox SMT, NestJS/Spring outbox libs. **Breaks:** the outbox **table schema** is the contract between the writing transaction + the relay — changing columns (aggregate id/type/payload/sequence) breaks the relay's row→message mapping; events go nowhere; the relay also encodes the event-schema contract (inherits its risks). **Detect:** `outbox`/`event_outbox` tables, outbox SMT config, relay polling (`WHERE published = false`), `aggregateid`/`payload` columns, Debezium event router. **Safe-change:** treat the outbox schema as co-owned by writer + relay; columns additive + keep relay mapping synced; preserve ordering/sequence columns; test committed-write → published-message end-to-end. _(Own home — the outbox-table schema is co-owned by writer + relay, its own source-of-truth; cross-ref `event-schema` (the relay also encodes it).)_
538
+
539
+ #### `saga-step-contract` — HIGH · transcribe
540
+
541
+ The ordered sequence of local transactions + their compensating (undo) transactions, plus the events/commands linking each step. **Where:** orchestration sagas (Temporal/orchestrator) + choreography sagas, Axon Saga, MassTransit, Camunda/Zeebe. **Breaks:** each forward step implies a compensation contract — adding/reordering a step without adjusting its compensation leaves a mid-saga failure **partially committed with no rollback** (money reserved never released); the compensation + linking event/command names are an invisible cross-service contract. **Detect:** `compensate`/`Saga`/`@SagaStep`/`compensation`, orchestrator state machines, choreography handlers triggering the next step, Temporal try/catch + rollback activities. **Safe-change:** every new forward step gets its compensating action wired into the failure path; keep compensations idempotent + tolerant of nothing-to-undo; preserve linking event/command names (version them); failure-inject each step. _(Own home — the saga step/compensation sequence is its own source-of-truth.)_
542
+
543
+ #### `dead-letter-queue-contract` — MED · point
544
+
545
+ Where/when/in-what-shape failed messages route after exhausting retries + who drains/replays them. **Where:** SQS redrive + DLQ, BullMQ failed set, RabbitMQ `x-dead-letter-exchange`, Kafka DLQ topics, Pub/Sub dead-letter topics. **Breaks:** the redrive `maxReceiveCount`, DLQ name, and whether anything monitors/replays it are an implicit contract — changing `maxReceiveCount` or the binding makes poison messages loop forever (no DLQ) or get discarded (DLQ with no consumer); replaying assumes the consumer's payload contract hasn't drifted. **Detect:** `RedrivePolicy`, `maxReceiveCount`, `deadLetterTargetArn`, `x-dead-letter-exchange`, `.dlq` suffix, `getFailed()`, DLQ alarm config. **Safe-change:** pair every queue with a monitored DLQ + alarm/consumer; set `maxReceiveCount` consciously; verify the current consumer accepts replayed (possibly stale) payloads; keep DLQ names stable + namespaced. _(Own home — the redrive/DLQ contract is its own source-of-truth; cross-ref `retry-backoff-contract` (the attempts it exhausts before redrive).)_
546
+
547
+ #### `retry-backoff-contract` — MED · transcribe
548
+
549
+ The retry policy — attempts, backoff curve, jitter, what's retryable — between the messaging layer/SDK and the side-effecting handler. **Where:** BullMQ `attempts`/`backoff`, SQS visibility-timeout retries, Temporal `RetryPolicy`, Kafka retry topics, Step Functions retriers. **Breaks:** the handler's side effects must survive the policy — more attempts or shorter backoff multiplies side effects on a non-idempotent handler; a visibility timeout shorter than processing time lets another consumer pick up the same message mid-flight (concurrent duplicate). **Detect:** `attempts:`, `backoff: { type: 'exponential' }`, `RetryPolicy`, `maximumAttempts`, `visibilityTimeout`, `nonRetryableErrorTypes`. **Safe-change:** keep visibility/lock timeout > worst-case processing; classify retryable vs terminal explicitly (don't retry validation failures); re-verify idempotency when attempts/backoff change; jitter to avoid thundering herds. _(Own home — the retry/backoff policy is its own source-of-truth; cross-ref `dead-letter-queue-contract` (the terminal redrive).)_
550
+
551
+ #### `schema-registry-contract` — CRITICAL · point
552
+
553
+ The centrally-registered, versioned serialization schema for a topic's messages + its enforced compatibility mode. **Where:** Confluent Schema Registry, Apicurio, AWS Glue, Karapace, Buf, with Avro/Protobuf/JSON-Schema serdes. **Breaks:** the **compatibility mode** (BACKWARD/FORWARD/FULL/TRANSITIVE) defines which producer/consumer upgrade orders are safe — Protobuf field-number reuse or Avro fields without defaults break silently (reused Protobuf number deserializes garbage, no error); the wrong mode breaks consumers fleet-wide. **Detect:** `*.avsc`/`*.proto`, `buf.yaml`, `schema.registry.url`, `KafkaAvroSerializer`/`KafkaProtobufSerializer`, `compatibility` config, subject-naming strategy, `reserved`, Avro `default`. **Safe-change:** set + CI-enforce a compatibility mode (BACKWARD or FULL_TRANSITIVE); Avro: every new field gets a default; Protobuf: never reuse numbers, `reserved` removed ones, use `optional`; co-version producer + consumer serdes. _(Own home — the registry + its compatibility mode is its own source-of-truth; `event-schema` and `cdc-stream-contract` cross-ref it as the enforcer.)_
554
+
555
+ #### `realtime-channel-naming` — HIGH · transcribe
556
+
557
+ The convention for naming/subscribing to real-time channels/rooms, including auth scoping. **Where:** Cloudflare Realtime / Durable Object rooms (this repo's `cf-realtime` L1), Pusher (`private-`/`presence-` prefixes), Ably, socket.io rooms/namespaces, Supabase Realtime, Phoenix Channels. **Breaks:** the channel name is the meeting point between client subscribe + server publish, and prefixes encode **authorization** (Pusher `private-`/`presence-` require auth) — rename a channel or change its templating (`chat:{tenantId}:{sessionId}`) and clients subscribe to a channel the server never publishes to (no messages, no error); drop an auth prefix and a private channel becomes public. **Detect:** channel-name templates, `subscribe('private-...')`, `presence-`, socket.io `.to(room)`/namespace, DO `idFromName`, presence/auth callbacks, `@nurix/chat` channel config. **Safe-change:** centralize channel-name templating shared client + server; preserve auth-prefix conventions; keep tenant/session scoping consistent across publish + subscribe; dual-publish on rename. _(Canonical home for the realtime channel-naming + auth-scoping rendezvous (`transcribe`) — `presence-state-contract` folds in (member-state within the same channels); the per-message envelope is `websocket-wire-envelope`.)_
558
+
559
+ #### `presence-state-contract` — MED · transcribe
560
+
561
+ The shape + semantics of presence/member-state payloads synced across clients in a channel. **Where:** Pusher presence channels, Ably presence, socket.io presence, Phoenix Presence, Supabase Realtime presence, Liveblocks, Yjs awareness. **Breaks:** presence member-info is a payload contract among all connected clients, which are **mixed versions in the wild** (old app versions linger far longer than backend deploys) — change member-info fields and old clients render `undefined`; change the user-id key and dedup/avatar logic breaks. **Detect:** `presence-` channels, `members.each`, `member.info`, `presence.enter(`, `Presence.list`, `awareness.setLocalState`. **Safe-change:** add presence fields additively + keep old ones; keep the identity key stable; make client rendering tolerant of missing fields; version on a breaking change. _(Folds into `realtime-channel-naming` — same realtime-channel source-of-truth, same `transcribe` doc-mode; the member-state facet. Cross-ref `websocket-wire-envelope` for the message envelope.)_
562
+
563
+ #### `websocket-wire-envelope` — HIGH · transcribe
564
+
565
+ The message envelope/framing on a long-lived socket — the `type`/`event` discriminator + payload — multiplexing many message kinds over one connection. **Where:** socket.io events, raw WebSocket JSON frames, this repo's `@nurix/chat` wire envelope over cf-realtime, `graphql-ws`, STOMP, Phoenix Channels. **Breaks:** one socket carries many message types keyed by a discriminator — add/rename a `type` or change a payload and the **mixed-version client population** can't parse it (old clients ignore new types / crash on changed payloads); unlike request/response there's no per-call negotiation, the envelope is global to the connection; a socket.io major-version bump is a protocol mismatch (HTTP 400 on connect). **Detect:** `socket.emit(`/`socket.on(`, `JSON.parse(event.data)`+`switch (msg.type)`, discriminated-union message types, `graphql-ws`, `@nurix/chat` inbound/outbound handling, `EIO=`/protocol version. **Safe-change:** discriminated `type` field, add types additively; clients ignore unknown types gracefully; share the envelope type def client + server; negotiate a protocol/envelope version at handshake for breaking changes; keep socket.io client/server majors compatible. _(Canonical home; cross-ref frontend/API `websocket-message-protocol`.)_
566
+
567
+ #### `pubsub-channel-contract` — MED · point
568
+
569
+ Channel name + payload contract for fire-and-forget pub/sub with no persistence or schema enforcement. **Where:** Redis `PUBLISH`/`SUBSCRIBE`/`PSUBSCRIBE`, Postgres `LISTEN`/`NOTIFY`, in-process EventEmitter, `BroadcastChannel`, MQTT. **Breaks:** no broker schema/persistence/registration — publisher + subscriber agree only by convention on the channel string + JSON shape; mismatch = silent drop (Redis pub/sub doesn't buffer for absent subscribers); Postgres `NOTIFY` payloads are 8000-byte-capped (growing one silently truncates/fails). **Detect:** `redis.publish(`, `subscribe(`/`psubscribe(`, `LISTEN`/`NOTIFY`, `emitter.emit(`, `new BroadcastChannel(`. **Safe-change:** centralize channel names + validate payload on both ends; keep `NOTIFY` payloads small (pass an id, re-fetch); don't rely on pub/sub for delivery guarantees. _(Own home — broker-less fire-and-forget pub/sub (`point`) is distinct from the persisted-stream `event-schema` and the socket `websocket-wire-envelope`.)_
570
+
571
+ #### `stream-consumer-offset-contract` — HIGH · point
572
+
573
+ Consumer groups, offset/cursor management, and consumer-group naming for replayable log streams. **Where:** Kafka consumer groups + offsets, Kinesis shard iterators, Redis Streams consumer groups (`XREADGROUP`/`XACK`), Pulsar subscriptions, NATS JetStream durables. **Breaks:** the consumer-group/subscription **name** owns the offset — rename it and it starts from `auto.offset.reset` (earliest = reprocess everything; latest = skip the backlog), both silently catastrophic; changing commit timing changes whether a crash reprocesses or loses messages; a renamed Redis Streams group strands un-acked PEL messages. **Detect:** `group.id`/`groupId:`, `auto.offset.reset`, `enable.auto.commit`, `XGROUP CREATE`/`XACK`, durable subscription names. **Safe-change:** treat group/subscription names as stable identifiers; set `auto.offset.reset` deliberately + know the fresh-group consequence; match commit timing to handler idempotency; rewind via explicit offset reset, not a group rename. _(Own home — the consumer-group/offset contract is its own source-of-truth.)_
574
+
575
+ #### `batch-window-contract` — MED · point
576
+
577
+ Batch size, max wait/window, and partial-failure semantics between a stream/queue and a batch handler. **Where:** Lambda event-source mapping (`BatchSize`, `MaximumBatchingWindow`, `ReportBatchItemFailures`), Kafka `max.poll.records`, SQS batch-of-10. **Breaks:** increasing `BatchSize` makes a handler that throws on one bad record fail the **whole batch** (and reprocess all of it); toggling partial-batch-failure reporting flips redrive behavior — without it, one poison message blocks the whole batch. **Detect:** `BatchSize`, `MaximumBatchingWindow`, `ReportBatchItemFailures`/`functionResponseType`, `max.poll.records`, `partialBatchResponse`. **Safe-change:** enable partial-batch-failure reporting; make per-record processing idempotent (whole batch may redeliver); re-test poison-message handling when batch size/window changes. _(Own home — the batch-size/window/partial-failure contract is its own source-of-truth.)_
578
+
579
+ ---
580
+
581
+ ## 6 · CONFIG, ENV & BUILD
582
+
583
+ > Recurring signature: two sides never statically linked (reader/provider, schema/generated, host/remote, tsconfig/bundler, package.json/lockfile) — a change on one passes compilation and surfaces only at boot/install/deploy, or never-loudly (leaked secret, stale cache, wrong DI scope). The highest-severity ones cross a **trust or deploy boundary**.
584
+
585
+ #### `env-var-contract` — HIGH · transcribe
586
+
587
+ The agreement that named env vars (expected names/types/required-ness) are present at runtime for code that reads them. **Where:** `process.env.X`, `import.meta.env`, `os.environ`; `.env*`, dotenv; deploy manifests (Helm, ECS, Vercel, `wrangler.toml [vars]`). **Breaks:** reader (code) + provider (deploy env) never reference each other statically — rename `DATABASE_URL`, drop a var, or change `"true"`→`1` and the consumer fails as `undefined` coercions / silent `NaN` / a default-port fallback. **Detect:** `process.env.`/`import.meta.env.`, `.env*`, `.env.example`, a Zod/envalid/`@t3-oss/env` schema (`env.ts`), `wrangler.toml`, compose `environment:`. **Safe-change:** update `.env.example` + the validation schema in the same commit; rename in _all_ environments (CI secrets, prod dashboard, IaC); fail-fast at boot over silent defaults; document type coercion (env values are strings). _(Canonical home for the env-var key set (`transcribe`) — `env-parity` folds in (the same key set across environments).)_
588
+
589
+ #### `env-parity` — HIGH · transcribe
590
+
591
+ The contract that dev/CI/staging/prod expose the _same_ config keys (differing only in values). **Where:** `.env.{development,production}`, Vite modes, `NODE_ENV`, CI secret stores, Vercel/Netlify per-env vars, K8s ConfigMaps/Secrets per namespace, Twelve-Factor. **Breaks:** a var present in dev but absent in prod (or vice versa) is a "works on my machine" gap that surfaces only post-deploy; drift in _which keys exist_ silently changes code paths. **Detect:** diff key-sets across `.env.*` + `.env.example`, CI `env:`/`secrets:` vs the schema, `--mode`/`NODE_ENV` branches. **Safe-change:** keep one canonical key list + assert every env satisfies it in CI; when adding a var set it in _every_ env before merge; avoid `NODE_ENV`-conditional reads of partially-set vars; treat `.env.example` as a tracked contract. _(Folds into `env-var-contract` — same env-var key-set source-of-truth, same `transcribe` doc-mode; the cross-environment-parity facet.)_
592
+
593
+ #### `build-time-vs-runtime-config` — HIGH · point
594
+
595
+ The line between config baked into the bundle at build (immutable per artifact) vs read at runtime (changeable per deploy). **Where:** Vite `import.meta.env`/`define`, Next.js `NEXT_PUBLIC_*` vs server env, Webpack `DefinePlugin`, esbuild `--define`, promoted Docker images, SSG. **Breaks:** build-time config is frozen into the artifact — promoting the same image to prod won't pick up new values, and an env-specific value gets hard-coded for all; code expecting a runtime var finds a stale build-time literal; both look identical in source (`process.env.X`). **Detect:** `define`/`DefinePlugin`/`--define`, `NEXT_PUBLIC_`/`VITE_` reads, CI building with env-specific vars, one image tag deployed to multiple envs. **Safe-change:** per-var, decide build-time vs runtime; never bake env-specific values into promoted images; document which prefix inlines at build; for SSG, rebuild per env or fetch config client-side. _(Own home — the build-vs-runtime line is `point` at the bundler config, a different doc-mode/concern from the `transcribe` env-var key set.)_
596
+
597
+ #### `client-exposed-env` — CRITICAL · point
598
+
599
+ The contract that only vars under a designated public prefix ship to the browser, and nothing secret is ever placed under it. **Where:** Vite `VITE_*`, Next.js `NEXT_PUBLIC_*`, Expo `EXPO_PUBLIC_*`, Gatsby `GATSBY_*`. **Breaks:** the prefix is the _only_ gate between server secret + public bundle — misnaming `VITE_STRIPE_SECRET` leaks a key into shipped JS; in some Vite versions referencing a missing var dumps the _entire_ env into the bundle; the leak is silent (the app works, the secret is public). **Detect:** secret-shaped names under public prefixes (`VITE_.*(SECRET|KEY|TOKEN|PASSWORD)`), bundle-output audit, `.env` secrets next to prefixed vars. **Safe-change:** never put secrets under a client prefix; audit the built bundle (grep output) in CI; always reference defined vars; treat the public prefix as a forever-public surface. _(Own home — the public-prefix leak gate is a distinct CRITICAL surface, kept separate from `env-var-contract`; cross-ref frontend `client-env-var-contract` (its consuming-side view).)_
600
+
601
+ #### `secrets-management` — CRITICAL · point
602
+
603
+ The contract between where secrets are stored (vault/SSM/manager) + how code retrieves them (paths, IAM scope, mounts). **Where:** Vault, AWS Secrets Manager/SSM, GCP Secret Manager, Azure Key Vault, Doppler, K8s Secrets/External Secrets, Docker `secrets:`, BuildKit `--mount=type=secret`. **Breaks:** the secret path + the IAM permission to read it are a contract code can't validate at build — rename a path, rotate without dual-writing, or tighten IAM and the app fails to boot or uses a stale cached value; rotation breaks long-lived connections holding old credentials. **Detect:** SDK calls (`SecretsManagerClient`, `vault read`), `external-secrets` CRDs, `secrets:` in compose, IAM `secretsmanager:GetSecretValue`, init-container secret fetches. **Safe-change:** treat secret paths as versioned (dual-write old+new during rename/rotation); co-change the IAM granting read; re-fetch on auth failure not cache-forever; never bake secrets into images (use mounts); scan history. _(Own home — the secret-store path + IAM-read contract is its own source-of-truth; cross-ref auth `encryption-at-rest-key-boundary` for data-encryption keys.)_
604
+
605
+ #### `feature-flag-contract` — HIGH · transcribe
606
+
607
+ The agreement on a flag's key, default value, and the off=legacy/on=new convention. **Where:** LaunchDarkly, Flagsmith, Unleash, ConfigCat, GrowthBook, PostHog, Statsig; in-house `flags.ts`; `process.env.FEATURE_X`. **Breaks:** SDK config (defaults, targeting) + the gated branch are two sides — changing a default, deleting a still-referenced flag, or inverting off/on semantics flips behavior silently; in distributed systems removing a flag one service still reads creates partial-rollout failure. **Detect:** flag init + `getFlag`/`variation`/`isEnabled('key')`, flag-key constants, stale dead branches, SDK fallback defaults. **Safe-change:** preserve off=previous-safe-behavior so disabling is rollback; two-step removal in distributed systems (make safe unconditionally → deploy everywhere → delete); remove dead branches + SDK defaults + targeting together; sane SDK fallbacks. _(Canonical home for the flag-key contract (`transcribe`) — frontend `feature-flag-gating` folds in (its client-UI-gating facet).)_
608
+
609
+ #### `workspace-package-graph` — HIGH · point
610
+
611
+ The contract that a monorepo package may only import siblings it explicitly declares as workspace deps — no phantom/hoisted imports. **Where:** pnpm workspaces (`pnpm-workspace.yaml`, `workspace:*`, strict `node_modules`), npm/yarn/Bun workspaces, Nx, Turborepo. **Breaks:** under hoisting a package can import an undeclared dep hoisted to root (works locally, breaks on publish or layout change); pnpm's strict layout forbids it; removing/renaming a workspace package breaks every undeclared importer silently. **Detect:** `pnpm-workspace.yaml`/root `workspaces`, `workspace:*` deps, imports of package names not in that package's `dependencies`, `nx graph`/`turbo` graph. **Safe-change:** declare every cross-package import as a `workspace:` dep; on rename/remove update all dependents + lockfile; pin internal deps `workspace:*`; CI dependency-graph check for undeclared imports. _(Own home — the workspace dependency graph is its own source-of-truth.)_ _(A workspace is NOT a module — see how the `maintenance` agent reads boundary `Bounds` vertices.)_
612
+
613
+ #### `import-layering-rule` — MED · point
614
+
615
+ Architectural rules constraining which layers/modules may import which (domain ⊥ UI; feature A ⊥ feature B internals). **Where:** ESLint `no-restricted-imports`/`no-restricted-paths`, `eslint-plugin-boundaries`, Nx `enforce-module-boundaries` (tags), `dependency-cruiser`, Sheriff. **Breaks:** the rule lives in lint config not the type system — a violating import compiles + runs fine but erodes architecture (circular deps, leaked internals); refactoring a path silently makes blocked imports allowed (or vice versa). **Detect:** `.eslintrc*`/`eslint.config.*` restricted-import blocks, `.dependency-cruiser.js`, Nx `tags`+`depConstraints`, `eslint-plugin-boundaries` `elements`. **Safe-change:** verify moved files still match the layer globs; keep lint rules synced to the conceptual architecture; run boundary lint in CI; prefer barrel/public-entry imports. _(Own home — the layer/import-restriction rule (lint config) is its own source-of-truth.)_ _(CRITICAL if the rule guards a security/tenancy boundary.)_
616
+
617
+ #### `tsconfig-paths` — MED · point
618
+
619
+ Import-alias mapping (`@app/*`) declared in `tsconfig.json` `paths`+`baseUrl`. **Where:** TS `paths`; mirrored in Vite (`resolve.alias`/`vite-tsconfig-paths`), Jest (`moduleNameMapper`), Webpack, Node (`tsconfig-paths`), Vitest. **Breaks:** `paths` only affects _type-check_ resolution — the bundler/runtime needs the same aliases separately; if they drift, type-check passes but build/tests fail to resolve (or resolve to the wrong file) silently. **Detect:** `tsconfig*.json` `paths`/`baseUrl`, vs `vite.config`/`webpack` `alias`, `jest` `moduleNameMapper`; `vite-tsconfig-paths` plugin. **Safe-change:** change an alias in all resolvers at once or use a plugin deriving them from tsconfig; prefer one source of truth; verify build + tests, not just `tsc`. _(Canonical home for the tsconfig × `point` cluster — `tsconfig-project-references` folds in (the build-graph facet of the same tsconfig), unless the monorepo files the references graph separately.)_
620
+
621
+ #### `tsconfig-project-references` — MED · point
622
+
623
+ The build-graph contract between composite TS projects — `references` wiring, `composite: true`, emitted `.d.ts`/`.tsbuildinfo`. **Where:** TS project references (`tsc --build`), monorepo tsconfig graphs, `declarationMap`. **Breaks:** a referenced project must emit declarations the referencing one reads; `composite` changes `rootDir` defaults — break the graph, forget `composite`, or change a referenced output layout and downstream builds use stale `.d.ts`, skip rebuilds, or fail to find outputs (incremental builds make staleness silent). **Detect:** `references` arrays, `composite`/`declaration`/`tsBuildInfoFile`, `tsc -b`, `.tsbuildinfo`, per-project `outDir`/`rootDir`. **Safe-change:** keep `composite`+`declaration` on every referenced project; update the `references` graph on dep changes (`tsc -b`); don't change a referenced `outDir`/`rootDir` without updating consumers; clear `.tsbuildinfo` to debug staleness. _(Folds into `tsconfig-paths` — same tsconfig source-of-truth, same `point` doc-mode; the build-graph facet. Stands alone in a large composite-project monorepo.)_
624
+
625
+ #### `package-public-api` — CRITICAL · point
626
+
627
+ A package's published entry points (what consumers may import) + the semver promise that breaking them bumps major. **Where:** `package.json` `exports`/`main`/`module`/`types`/`typesVersions`, subpath + conditional exports, `files`, API Extractor, `@nurix/*` scopes; barrel `index.ts`. **Breaks:** `exports` _encapsulates_ the package — anything mapped is public API (changing it is breaking), anything unmapped throws `ERR_PACKAGE_PATH_NOT_EXPORTED` for deep imports consumers rely on; adding `exports`, removing a subpath, reordering conditions, or changing `types` silently breaks imports/type resolution across the install boundary. **Detect:** `package.json` `exports`/`main`/`types`/`files`, conditional-export maps, deep-import patterns in consumers, `publishConfig`, barrel exports. **Safe-change:** every mapped subpath is public (removal = major bump); don't expose internals you'll want to change; keep `types`/`typesVersions` aligned with runtime `exports` (dual ESM/CJS); verify consumers can import after publish. _(Canonical home; cross-ref frontend `barrel-export-contract`.)_
628
+
629
+ #### `peer-dependency-contract` — HIGH · point
630
+
631
+ The agreement that a package's host supplies a compatible version of a shared dep (React, a plugin's framework) rather than bundling its own. **Where:** `peerDependencies`+`peerDependenciesMeta`, plugin ecosystems (React, ESLint, Vite/Webpack plugins, `@nurix/components`), pnpm strict peers. **Breaks:** package + host independently declare versions that must overlap — a widened/narrowed range or a host upgrading past it produces duplicate-instance bugs (two React copies → broken hooks/context), `ERR_PEER_DEP_*`, or crashes; pnpm enforces strictly, npm/yarn historically ignore silently. **Detect:** `peerDependencies`/`peerDependenciesMeta`, pnpm peer warnings, duplicate singleton copies in the lockfile, `dedupe`/`overrides`/`resolutions` hacks. **Safe-change:** keep peer ranges as wide as compatible (major bump to narrow); for singletons ensure exactly one copy (`dedupe`/`resolutions`/federation `singleton`); test lowest + highest supported; don't move a peer into `dependencies`. _(Own home — the peer-dependency overlap contract is its own source-of-truth; cross-ref `lockfile-resolution-contract` (the resolved tree).)_
632
+
633
+ #### `module-federation-remotes` — CRITICAL · point
634
+
635
+ The runtime contract between a host + remotely-loaded micro-frontends — remote URLs/entry names, exposed-module map, `shared` versions/singletons. **Where:** Webpack/Rspack Module Federation, `@module-federation/enhanced`, Native Federation, Vite `@originjs/vite-plugin-federation`. **Breaks:** host + remote build/deploy independently yet must agree at runtime on exposed-module names + shared-lib versions — rename an exposed path, change a remote URL, or let a shared singleton drift and the host fails to load the remote / loads duplicate React breaking `useState`/`useContext` silently; version postfixes (`-rc.1`) can break singleton sharing. **Detect:** `ModuleFederationPlugin` (`remotes`/`exposes`/`shared`/`singleton`/`requiredVersion`/`strictVersion`), `remoteEntry.js` URLs, `import('remote/Module')`. **Safe-change:** treat exposed names + URLs as a versioned runtime API; mark framework libs `singleton: true`+`requiredVersion`+`strictVersion`; avoid prerelease singletons; coordinate host+remote deploys + add a runtime version/health check. _(Own home — the host↔remote federation contract is its own runtime source-of-truth.)_
636
+
637
+ #### `di-container-registration` — HIGH · point
638
+
639
+ The contract between DI tokens/keys + their registered providers consumers resolve at runtime. **Where:** NestJS providers + `@Inject(TOKEN)`, InversifyJS bindings, tsyringe, Angular DI/`InjectionToken`, typedi, Awilix. **Breaks:** tokens + registrations match at runtime not via the type system (string/symbol tokens especially) — rename a token, forget to register a provider, or change its scope (singleton↔request) and resolution throws at boot or injects the wrong instance silently (a shared singleton where per-request was assumed leaks state across tenants). **Detect:** `@Inject(`/`@injectable`/`container.register`/`bind(...).to(...)`/`InjectionToken`/`provide:`, token constants, module `providers`/`exports`, `forwardRef`. **Safe-change:** use typed/`InjectionToken` tokens over strings; register every injected token + import the providing module where resolved; preserve scope semantics (state-sharing); update registration + injection sites together on rename. _(Own home — the DI token↔provider registration is its own source-of-truth.)_
640
+
641
+ #### `code-generation-io` — HIGH · point
642
+
643
+ The contract that generated code stays in sync with its source-of-truth schema (and consumers import the generated output, not edit it). **Where:** Prisma (`prisma generate`→`@prisma/client`), GraphQL Code Generator, Protobuf/gRPC (`buf`), OpenAPI generators, tRPC inference, Drizzle, `wrangler types`. **Breaks:** schema + generated artifact must regenerate together — forget to generate and types drift while the DB/API changed (code compiles against stale types, mismatches at runtime); hand-editing generated files is clobbered next run; cross-project, frontend types lag a backend schema change with no compile error until regen. **Detect:** `schema.prisma`, `codegen.{yml,ts}`, `*.proto`, `buf.gen.yaml`, `openapi.{yaml,json}`, generated dirs (`@prisma/client`, `__generated__/`, `*.gen.ts`), `generate`/`postinstall` scripts. **Safe-change:** regenerate in the same commit + CI `git diff --exit-code`; never hand-edit generated files; wire generate into `postinstall`/build; version + coordinate consumer regen for cross-service schemas. _(Own home — the schema↔generated-artifact sync is its own cross-cutting contract; the schema-specific types (`orm-model-mapping`, `openapi-spec-contract`, `graphql-schema-contract`, `grpc-protobuf-wire-contract`) cross-ref it.)_
644
+
645
+ #### `bundler-entry-output-externals` — MED · point
646
+
647
+ A bundle's entry points, output filenames/format, and which deps are externalized (expected at runtime). **Where:** Vite `build.rollupOptions`/`build.lib`, Rollup, esbuild, Webpack `entry`/`output`/`externals`, tsup. **Breaks:** output filenames/formats + externals are an implicit contract with whatever loads the bundle — change an entry name/hashing/format and the HTML/loader references a missing file; mark a dep external that isn't provided → `Cannot find module`; stop externalizing a peer → it's duplicated. **Detect:** `vite.config`/`rollup.config`/`webpack` `entry`/`input`/`output`/`external`/`build.lib`, `format`, manifest plugins, HTML/CDN refs to output names. **Safe-change:** keep externals consistent with what the runtime provides; coordinate filename/format changes with consumers; externalize peer deps in library builds; verify the consumer loads the artifact. _(Own home — the bundle entry/output/externals contract is its own source-of-truth; cross-ref frontend `asset-public-path-contract` for output URLs.)_
648
+
649
+ #### `docker-build-arg-env` — HIGH · point
650
+
651
+ The distinction between Dockerfile `ARG` (build-time only, gone from the container but in image history) + `ENV` (baked into image + container). **Where:** Dockerfile `ARG`/`ENV`, `--build-arg`, compose `build.args` vs `environment`, BuildKit `--mount=type=secret`. **Breaks:** `ARG` + `ENV` look interchangeable but differ on whether the value survives into the container + leaks into history — a secret via `--build-arg` is baked into image history (extractable); relying on an `ARG` at runtime finds it undefined; promoting an image with `ENV` baked in carries that value everywhere. **Detect:** `ARG`/`ENV` lines, `--build-arg` in CI, compose `build.args`/`environment`, `--mount=type=secret`, `docker history`. **Safe-change:** never pass secrets via `--build-arg`/`ENV` (use secret mounts); `ARG` only for build-time choices, `ENV` only for every-promotion-target runtime values; scan image history; rotate any secret ever passed as a build-arg. _(Own home — the Dockerfile `ARG`/`ENV` distinction is its own source-of-truth; cross-ref runtime `container-image-contract` (the baked image interface) and `secrets-management`.)_
652
+
653
+ #### `config-schema-defaults` — MED · point
654
+
655
+ The contract a validated config schema carries — required vs optional keys, types, and _default values_ filling absent keys. **Where:** Zod/`@t3-oss/env`/envalid/convict schemas, `node-config` JSON hierarchies, framework config (`next.config`), `cosmiconfig` `.*rc`. **Breaks:** defaults are the silent fallback between operator-set + code-assumed — change a default (timeout, page size) and behavior shifts everywhere the key was unset (no error); make an optional key required and existing deploys fail at boot. **Detect:** `z.object({…}).default(...)`, envalid/convict `default`, `.optional()`/`.required()`, `cosmiconfig`/`*.config.*`, the config-loading module. **Safe-change:** treat defaults as observable behavior (changelog a change); a new required key without a default breaks deploys (provide a default or migrate first); keep schema types aligned with consumers; validate + fail-fast at boot. _(Own home — the validated config schema + its defaults are `point` source-of-truth; cross-ref `env-var-contract` (the env keys it validates).)_
656
+
657
+ #### `ci-pipeline-config-contract` — MED · point
658
+
659
+ The contract between CI workflows + the repo — required secrets/vars, expected scripts, runner/Node versions, cache keys, matrix. **Where:** GitHub Actions (`.github/workflows/*.yml`), GitLab CI, CircleCI, Turborepo/Nx remote cache, `engines`. **Breaks:** the workflow references repo scripts (`pnpm build`), secrets, + tool versions independently — rename a script, drop a secret, or bump the runner's Node and pipelines fail or (with caching) produce stale artifacts; cache-key drift silently serves wrong outputs. **Detect:** `.github/workflows/`, `.gitlab-ci.yml`, `secrets.`/`vars.`, `setup-node` `node-version`, `engines`/scripts referenced in CI, cache `key:`. **Safe-change:** grep workflows when renaming `package.json` scripts; add required secrets/vars before the workflow merges; keep CI Node/tool versions aligned with `engines`+lockfile; invalidate cache keys when inputs change. _(Own home — the CI-workflow↔repo contract is its own source-of-truth; cross-ref `node-engines-runtime-contract` and `lockfile-resolution-contract`.)_
660
+
661
+ #### `node-engines-runtime-contract` — MED · point
662
+
663
+ The contract that the runtime/package-manager present matches what the code + lockfile were built against. **Where:** `package.json` `engines`+`packageManager` (Corepack), `.nvmrc`/`.node-version`, volta, `.tool-versions`, Docker base tags, CI `setup-node`. **Breaks:** declared engine + installed runtime are separate — a mismatched Node major breaks native addons or uses changed built-ins; a different package-manager version regenerates the lockfile differently; `engines` is advisory by default so drift is silent unless `engine-strict`. **Detect:** `engines`/`packageManager`, `.nvmrc`/`.tool-versions`/volta, Docker `FROM node:` tag, CI `node-version`, `engine-strict=true`. **Safe-change:** keep `engines`, `.nvmrc`, Docker tag, CI version in lockstep; pin the package manager via Corepack `packageManager`; set `engine-strict` if load-bearing; test on the lowest supported runtime. _(Own home — the declared-engine↔installed-runtime contract is its own source-of-truth; cross-ref `lockfile-resolution-contract` and `ci-pipeline-config-contract`.)_ _(This repo: Node 22 LTS, pnpm/yarn/npm detected from lockfile — `.claude/rules/general.md`.)_
664
+
665
+ #### `lockfile-resolution-contract` — HIGH · point
666
+
667
+ The contract that the committed lockfile pins the exact, reproducible dependency tree everyone + CI installs. **Where:** `pnpm-lock.yaml`/`package-lock.json`/`yarn.lock`/`bun.lockb`, `npm ci`/`--frozen-lockfile`, `overrides`/`resolutions`, integrity hashes. **Breaks:** the lockfile is the contract between what ranges allow + what installs — editing `package.json` without updating the lock, mixing managers, or CI installing without `--frozen-lockfile` drifts the installed tree from what was tested (untested transitive versions, silently). **Detect:** exactly one lockfile (multiple = mixed-manager hazard), `--frozen-lockfile`/`npm ci` in CI, `overrides`/`resolutions`, `lockfileVersion`, out-of-sync `package.json` vs lock. **Safe-change:** commit the lockfile with every `package.json` change (never hand-edit); `--frozen-lockfile`/`npm ci` in CI; one manager per repo (delete stray lockfiles); encode forced versions in `overrides`/`resolutions` + re-lock. _(Own home — the lockfile-pinned dependency tree is its own source-of-truth; cross-ref `peer-dependency-contract` and `node-engines-runtime-contract`.)_
668
+
669
+ #### `monorepo-task-pipeline` — HIGH · point
670
+
671
+ The contract describing inter-package task deps, inputs/outputs, and cache keys determining build/test ordering + cache validity. **Where:** Turborepo (`turbo.json` `pipeline`/`dependsOn`/`inputs`/`outputs`), Nx (`nx.json` `targetDefaults`/`namedInputs`), remote caching. **Breaks:** the config declares what depends on what + which files invalidate cache — mis-declared `inputs`/`outputs` means a changed file doesn't bust the cache (a stale artifact served as fresh) or a downstream task runs before its dependency; the dep graph is config, decoupled from the import graph. **Detect:** `turbo.json`/`nx.json`/`project.json` `targets`, `dependsOn`/`inputs`/`outputs`/`namedInputs`, `cache: true`, `.turbo`/`.nx/cache`. **Safe-change:** declare all real inputs (source globs + env via `env`/`passThroughEnv`); declare `outputs` accurately; keep `dependsOn` aligned with the package dep graph; treat env-var inputs explicitly. _(Own home — the task-pipeline/cache-key graph is its own source-of-truth; cross-ref `workspace-package-graph` (the import graph it tracks).)_
672
+
673
+ #### `css-design-token-contract` — MED · transcribe
674
+
675
+ The build/import facet of design tokens — shared CSS custom-property names, Tailwind theme config, and style-import order components depend on. **Where:** Tailwind 4 `@theme`/CSS-first config, CSS custom properties, CSS Modules, `@nurix/components` styles import, PostCSS, Style Dictionary. **Breaks:** components reference token names defined elsewhere + the app must import the styles + provide the tokens — rename a token, change Tailwind config, or forget the styles import and components render unstyled with no JS error; Tailwind v4 content-scanning silently drops unseen classes. **Detect:** `@theme`/`tailwind.config`/`@tailwind`/`@import` of a package's styles, `var(--*)`, CSS Module imports, `import '@nurix/components/styles'`. **Safe-change:** treat token names as public (deprecate before rename); ensure consuming apps import the library styles + Tailwind sees the classes; one token source generated per platform; verify rendered UI. _(Folds into frontend `design-token-contract` — same design-token source-of-truth, same `transcribe` doc-mode; this is its build/import facet.)_
676
+
677
+ ---
678
+
679
+ ## 7 · RUNTIME, PROCESS, NETWORK & INFRA
680
+
681
+ #### `edge-binding` — CRITICAL · point
682
+
683
+ The named-capability contract between an edge/serverless function + its platform resources (KV, R2, D1, queues, secrets, service bindings), declared at deploy. **Where:** Cloudflare Workers/Pages (`wrangler.toml` `[[kv_namespaces]]`/`[[r2_buckets]]`/`[[d1_databases]]`/`[[services]]`/`[vars]`/`[[durable_objects.bindings]]`), accessed as `env.BINDING_NAME`. **Breaks:** code references `env.MY_KV`/`env.MY_DO` — rename/remove the binding in config and the runtime hands `undefined`, calls throw at request time with no compile error; bindings are _not_ inherited across environments, so one present in `[env.staging]` but missing in production silently 500s only in prod. **Detect:** `**/wrangler.{toml,jsonc,json}`, `env\.[A-Z_]+` in `src/`, cross-referenced against declared bindings, `[env.*]` sub-tables. **Safe-change:** rename the binding in config AND every `env.X` ref together; verify the binding in _every_ named environment; for service bindings confirm the target Worker exists; for DO bindings match `class_name` to an exported class. _(Own home — the wrangler binding↔`env.X` contract is its own source-of-truth; cross-ref `do-class-contract` for DO bindings.)_
684
+
685
+ #### `do-class-contract` — CRITICAL · point
686
+
687
+ The binding between a Durable Object's exported class name, RPC method surface, and persisted storage shape across deploys. **Where:** Cloudflare Durable Objects — `wrangler.toml` `[[migrations]]` (`new_classes`/`renamed_classes`/`deleted_classes`), `class MyDO extends DurableObject`, `state.storage`/SQLite. **Breaks:** renaming/deleting a DO class without a `[[migrations]]` entry orphans live stateful objects (stored data unreachable); changing an RPC signature breaks callers holding stubs; changing persisted storage schema breaks objects that outlive a deploy. **Detect:** `extends DurableObject`, `idFromName`/`get(id)`, the `[[migrations]]` tag sequence, `state.storage.get/put` keys + DO-internal SQL. **Safe-change:** every class rename/delete needs a matching `[[migrations]]` entry with an incremented tag; never reuse a migration tag (append-only, ordered); treat persisted keys/SQL as versioned + read-tolerate old shapes; roll RPC changes additively. _(Own home — the DO class↔RPC↔storage contract is its own source-of-truth; cross-ref `edge-binding` (the binding that resolves the class) and async `realtime-channel-naming` (DO rooms).)_ _(This repo's cf-realtime L1 uses Durable Object rooms.)_
688
+
689
+ #### `lambda-handler-contract` — HIGH · point
690
+
691
+ The fixed handler signature + event/context payload shape the FaaS platform invokes. **Where:** AWS Lambda (`exports.handler = async (event, context)`), API Gateway proxy event (v1 vs v2 payload format), SQS/SNS/S3/EventBridge trigger envelopes; Azure/GCP equivalents. **Breaks:** the platform owns one side — switching API Gateway payload v1→v2 moves `event.httpMethod` to `event.requestContext.http.method` (existing parsing reads `undefined`); changing the trigger type changes the whole envelope; renaming the handler without updating the `Handler` config makes it un-invocable. **Detect:** `exports.handler`/`def handler`, IaC `Handler:`/`handler =` strings, `event.requestContext`/`event.Records`/`event.httpMethod`. **Safe-change:** pin the API Gateway payload format version; validate the event shape with Zod at the handler; keep the `Handler:` string in lockstep with the exported symbol; treat each new trigger's envelope as a new contract. _(Own home — the FaaS handler signature + event-envelope contract is its own source-of-truth.)_
692
+
693
+ #### `cold-start-assumption` — HIGH · transcribe
694
+
695
+ The implicit contract about what runs at module/init scope vs per-invocation, and the latency/state budget of a fresh execution environment. **Where:** Lambda init phase (top-level `await`, provisioned concurrency, SnapStart), Cloudflare Workers global scope (no persistent globals guaranteed; isolate reuse not guaranteed), serverless containers. **Breaks:** code assuming a module-scope DB pool/secret persists across invocations breaks when the platform recycles isolates or scales out; un-awaited init work runs on the _first_ invocation (latency); mutating module-global state assuming single-tenancy leaks data across requests in a reused isolate. **Detect:** top-level mutable state/pools/caches in handler modules, `let cache = {}` at module scope, missing top-level `await`, `globalThis` mutation tied to a request. **Safe-change:** treat module-global state as per-isolate cache never request storage; `await` init side-effects at module top-level (Lambda); don't assume warm reuse for correctness; bound init work against the init timeout. _(Own home — the init-scope-vs-per-invocation assumption is its own transcribed contract.)_
696
+
697
+ #### `worker-thread-boundary` — MED · transcribe
698
+
699
+ The structured-clone / transferable / `SharedArrayBuffer` contract for data crossing a thread or process boundary. **Where:** Node `worker_threads` (`postMessage`, `transferList`), browser Web Workers, `Atomics`/`SharedArrayBuffer`, `child_process` IPC (`process.send`). **Breaks:** only structured-cloneable values cross — functions, class instances (methods lost), closures don't survive `postMessage`; passing a non-transferable as transferable throws; mutating after transfer touches a detached buffer; prototype/class identity is lost (`instanceof` fails). **Detect:** `new Worker(`, `postMessage(`, `parentPort`, `MessageChannel`, `process.send(`, `Atomics.`, `SharedArrayBuffer`. **Safe-change:** pass plain data + reconstruct rich objects on receipt; version the message envelope (`{type,payload}`) + validate; don't reuse a buffer after `transferList`; share a TS type for the protocol. _(Own home — the thread/process structured-clone contract; cross-ref frontend `state-serialization-contract` (the server→client facet of the same hazard).)_
700
+
701
+ #### `container-image-contract` — HIGH · point
702
+
703
+ The runtime contract baked into a container image — entrypoint/CMD, exposed ports, workdir, user, env defaults, required volumes. **Where:** Dockerfile (`ENTRYPOINT`/`CMD`/`EXPOSE`/`USER`/`WORKDIR`/`VOLUME`/`ENV`), OCI metadata, consuming compose/K8s pod spec. **Breaks:** the orchestrator + other containers depend on these — changing `EXPOSE`/the listen port without updating compose/K8s `containerPort`/Service breaks routing; switching `USER` to non-root breaks mounted-volume writes; `ENTRYPOINT` shell vs exec form changes signal delivery; removing a `VOLUME` loses persistence. **Detect:** `**/Dockerfile*`, `**/docker-compose*.yml`, diff `EXPOSE`/`ENV`/`USER`/`ENTRYPOINT` against compose `ports:`/`environment:` + K8s `containerPort`/`securityContext`. **Safe-change:** keep `EXPOSE` == listen port == compose/K8s `containerPort` == Service target; prefer exec-form `ENTRYPOINT` so signals reach PID 1; document required volumes + env as the interface; verify volume permissions if changing `USER`. _(Own home — the image runtime interface (entrypoint/ports/user/volumes) is its own source-of-truth; cross-ref `exposed-port-contract`, `graceful-shutdown`, `process-lifecycle-contract`, `docker-build-arg-env`.)_
704
+
705
+ #### `exposed-port-contract` — HIGH · point
706
+
707
+ The agreement on which port a process listens on + how upstream routers reach it. **Where:** app `listen(PORT)`, `Dockerfile EXPOSE`, compose `ports:`/`expose:`, K8s `containerPort`+Service `targetPort`, reverse-proxy `upstream`/`proxy_pass`. **Breaks:** a port is referenced in 3–5 disconnected places — change the app's listen port + forget the Service `targetPort` and traffic black-holes with connection-refused, no app-log error. **Detect:** `listen(`, `PORT`, `EXPOSE`, `targetPort`, `containerPort`, `proxy_pass`, `upstream`. **Safe-change:** source the port from one env var everywhere; update proxy/Service/compose mappings in the same change; verify the health-check probe targets the new port. _(Own home — the listen-port↔router wiring is its own source-of-truth; cross-ref `container-image-contract` (`EXPOSE`), `ingress-routing-rule`, `health-check-contract`.)_
708
+
709
+ #### `ingress-routing-rule` — HIGH · point
710
+
711
+ The host/path → backend mapping enforced by an ingress/reverse-proxy/gateway. **Where:** nginx `location`/`server_name`, K8s `Ingress`/Gateway API `HTTPRoute`, Envoy `route_config`, Traefik `IngressRoute`, Cloudflare/Caddy, API Gateway paths. **Breaks:** route matching is order- + specificity-sensitive — renaming an app route prefix without updating the ingress rule → 404 at the edge while the app is healthy; a path-rewrite/strip-prefix mismatch sends the backend an unrecognized path; a broader catch-all above a specific route shadows it. **Detect:** `**/*.conf` (nginx), `kind: Ingress|HTTPRoute|VirtualService`, Envoy `route` YAML, Traefik labels; match `path`/`prefix`/`rewrite` against mounted routes. **Safe-change:** change app route prefix + ingress rule together (verify rewrite/strip-prefix); check rule ordering for shadowing; confirm host + TLS SNI align. _(Own home — the host/path→backend mapping is its own source-of-truth; cross-ref `exposed-port-contract`, `dns-tls-san`, `service-discovery-contract`.)_
712
+
713
+ #### `cors-policy` — HIGH · transcribe
714
+
715
+ The allowed-origins/methods/headers/credentials contract the server advertises + the browser enforces (infra-layer view of `cors-preflight-contract`). **Where:** Express `cors({ origin })`, server `Access-Control-Allow-*`, preflight `OPTIONS`, CDN/edge CORS rules, K8s ingress annotations. **Breaks:** the browser is the enforcer — move the frontend to a new domain + forget the allow-list and all XHR/fetch fails CORS in the browser only (curl/server-to-server works, so backend tests pass); adding `Authorization` without `Allow-Headers` breaks preflight; `Allow-Credentials: true` is incompatible with `Allow-Origin: *`. **Detect:** `cors(`, `Access-Control-Allow-`, `FRONTEND_URL`, `origin:`, edge/CDN + ingress CORS. **Safe-change:** update the allow-list on any frontend-origin change (every env); list every custom request header in `Allow-Headers`; never pair credentials with wildcard origin; verify preflight `OPTIONS` returns 2xx. _(Folds into API `cors-preflight-contract` — same CORS contract, same `transcribe` doc-mode; this is its infra-layer view.)_
716
+
717
+ #### `cache-header-contract` — HIGH · transcribe
718
+
719
+ The `Cache-Control`/`ETag`/`Expires`/`Last-Modified` directives deciding how long every cache (browser/CDN/proxy) holds a response. **Where:** origin response headers, CDN config (Cloudflare/Fastly/CloudFront), `s-maxage`/`stale-while-revalidate`, asset-hashing strategy. **Breaks:** the cache obeys the header you sent — ship a bugfix while a `max-age=300` response is cached and users see the broken version for 5 min; `immutable` on a non-hashed asset means users never update; caching a personalized response publicly leaks one user's data to another. **Detect:** `Cache-Control`, `setHeader('ETag'`, `s-maxage`, `immutable`, `stale-while-revalidate`, CDN config + asset hashing. **Safe-change:** `immutable`/long-max-age only on content-hashed assets, short TTL for HTML/API; never cache personalized responses in a shared/CDN cache; add a cache-purge step to deploys; lower TTL before a risky release. _(Own home — the `Cache-Control`/TTL/CDN-duration contract (`transcribe`); the ETag/`If-Match` concurrency facet is API `conditional-request-caching-contract` (`point`), the cache-key facet is `vary-header-contract`.)_
720
+
721
+ #### `vary-header-contract` — HIGH · point
722
+
723
+ The request headers a cache must factor into its cache key, declared via `Vary` (+ CDN custom keys). **Where:** `Vary: Accept-Encoding, Accept-Language`, CDN cache-key config (Cloudflare Cache Rules, Fastly VCL), content negotiation. **Breaks:** return language- or auth-dependent content without listing the header in `Vary` and the CDN serves a French user the cached English page — or one logged-in user's response to another; conversely `Vary: Authorization` on a public CDN explodes cache cardinality. **Detect:** `Vary`, `Accept-Language`/`Accept-Encoding` branching, CDN cache-key settings, responses branching on request headers. **Safe-change:** any header the response branches on must appear in `Vary` (or the CDN key); never `Vary: Authorization`/cache personalized content on a shared cache; keep `Vary` + CDN key config consistent. _(Own home — the cache-key (`Vary`) contract is `point`, a different doc-mode/concern from the `transcribe` `cache-header-contract`.)_
724
+
725
+ #### `dns-tls-san` — HIGH · point
726
+
727
+ The binding between hostnames clients dial, DNS records, and the SANs/CN on the presented TLS cert. **Where:** DNS records, Let's Encrypt/ACM/cert-manager `Certificate` `dnsNames`, ingress `tls.hosts`, SNI routing, service hostnames. **Breaks:** a cert is valid only for its SANs — add a subdomain/route a service under a new host without adding it to the cert and the TLS handshake fails (`ERR_CERT_COMMON_NAME_INVALID`) for that host only; rename a service's DNS without updating consumers → NXDOMAIN; SNI mismatch routes to the wrong backend. **Detect:** `dnsNames`, `tls:`/`hosts:`, `server_name`, hardcoded hostnames in client config, cert-manager `Certificate`, ingress `tls`. **Safe-change:** add any new hostname to the cert SAN list before routing traffic; keep DNS + ingress `tls.hosts` + cert `dnsNames` synced; verify SNI/Host alignment; mind wildcard scope (`*.example.com` ≠ `a.b.example.com`). _(Own home — the hostname↔DNS↔cert-SAN binding is its own source-of-truth; cross-ref `ingress-routing-rule`.)_
728
+
729
+ #### `iac-output-contract` — HIGH · point
730
+
731
+ The named outputs of one infra stack consumed by another stack or app config. **Where:** Terraform `output` + `terraform_remote_state`/`tfe_outputs`, Pulumi `StackReference`, CloudFormation `Exports`/`Fn::ImportValue`, CDK cross-stack refs. **Breaks:** a consuming stack reads `data.terraform_remote_state.x.outputs.vpc_id` — rename/remove that output + every consumer fails to plan/apply; changing the producer's backend bucket/key/region means consumers can't find the state; `terraform_remote_state` exposes the _entire_ state (secret leak). **Detect:** `**/*.tf`, `output "`, `terraform_remote_state`, `tfe_outputs`, `StackReference`, `Fn::ImportValue`. **Safe-change:** treat output names as public (deprecate, don't rename); prefer SSM/`tfe_outputs` over full-state reads (least privilege); don't move the producer's backend without coordinating; grep all consumers before removing. _(Own home — the cross-stack IaC output contract is its own source-of-truth.)_
732
+
733
+ #### `inter-service-resilience-contract` — HIGH · transcribe
734
+
735
+ The agreed timeout/retry/circuit-breaker/connection-pool budget between a caller + a downstream service. **Where:** Istio `DestinationRule`+`VirtualService`, Envoy `retry_policy`/`circuit_breakers`, client libs (axios timeout, gRPC deadlines), resilience4j/Polly. **Breaks:** timeouts must form a coherent hierarchy — a caller timeout shorter than the callee's makes the callee finish work the caller abandoned; lowering a downstream timeout while callers retry 3× triggers a retry storm that takes down a recovering service; tightening `maxConnections` trips the breaker under normal load; these configs live in a different repo/layer than the calling code. **Detect:** `timeout`, `retries`/`retry_policy`, `circuit_breaker`/`outlierDetection`, `maxConnections`, `deadline`, mesh `DestinationRule`/`VirtualService`. **Safe-change:** keep caller timeout > callee timeout + retries; cap total retry amplification + jittered backoff; tune breaker/pool limits against measured load; change mesh policy + client expectations together. _(Own home — the timeout/retry/breaker budget between services is its own transcribed contract.)_
736
+
737
+ #### `trace-context-propagation` — MED · point
738
+
739
+ The wire headers carrying distributed-trace identity across a hop (`traceparent`/`tracestate`, B3, X-Ray). **Where:** W3C Trace Context, OpenTelemetry propagators, Zipkin B3, `baggage`, AWS `X-Amzn-Trace-Id`. **Breaks:** both services must agree on the format — A injects W3C `traceparent` but B's propagator only reads B3 and the trace breaks into two disconnected traces (observability silently degrades, no error); a proxy dropping the header (allow-lists, size limits) severs the trace; misconfigured propagators break `baggage`-based tenant/feature context. **Detect:** `traceparent`/`tracestate`, `X-B3-`, `baggage`, `propagator`/`TextMapPropagator`, OTel setup, proxy/ingress header allow-lists, CORS `Allow-Headers`. **Safe-change:** standardize on one propagator (W3C) across services; ensure proxies forward `traceparent`/`tracestate`/`baggage`; add trace headers to browser CORS `Allow-Headers`; don't put security-sensitive data in `baggage`. _(Own home — the trace-context wire-header contract is its own source-of-truth.)_ _(HIGH if `baggage` carries functional context.)_
740
+
741
+ #### `log-schema` — MED · transcribe
742
+
743
+ The structured-log field-name/type contract every producer emits + every consumer (queries/dashboards/alerts) depends on. **Where:** pino/winston/zap/structlog JSON, shared logger wrappers, Loki/Elasticsearch/Datadog parsing, alert rules keyed on field names. **Breaks:** dashboards, saved queries, + alerts are pinned to exact field names — rename `userId`→`user_id` in one service + the cross-service "all logs for a user" query silently returns nothing; the alert on `level: "error"` goes quiet if you switch to `severity`; a type change (string→object) breaks the index mapping. **Detect:** `logger.info(`/`log.error(` field keys, the shared logger config (pino `base`/`formatters`/`redact`), cross-referenced with alert/dashboard defs. **Safe-change:** treat field names/types as versioned (add new, deprecate old); standardize core fields (`level`/`timestamp`/`service`/`trace_id`/`user_id`); update dashboards/alerts in the same change as a rename; keep `pino.redact` paths synced to renamed fields. _(Own home — the structured-log field contract (`transcribe`) is distinct from the metric-name contract.)_ _(This repo uses pino — `.claude/rules/general.md`.)_
744
+
745
+ #### `metric-name-contract` — MED · point
746
+
747
+ Metric names, label/tag keys, and cardinality emitters publish + dashboards/alerts/SLOs consume. **Where:** Prometheus metric names+labels, OpenTelemetry instruments, StatsD/Datadog tags, Grafana dashboards, alerting/recording rules, SLOs. **Breaks:** rename `http_requests_total`→`http_request_count` + every dashboard panel/alert referencing the old name goes blank/silent (no error); adding a high-cardinality label (user-id) explodes the time-series count + can OOM the backend; changing a label key breaks `by(label)` aggregations. **Detect:** `Counter(`/`Histogram(`/`Gauge(`, `meter.create`, metric-name literals, label/tag maps, vendored dashboard/alert JSON. **Safe-change:** treat metric + label names as a contract (deprecate, don't rename); never put unbounded-cardinality values in labels; update dashboards/alerts/SLOs alongside a rename; follow OTel/Prometheus naming. _(Own home — the metric/label-name contract (`point`) is distinct from the structured-log field contract.)_
748
+
749
+ #### `health-check-contract` — HIGH · point
750
+
751
+ The path, status semantics, and timing of liveness vs readiness vs startup endpoints orchestrators poll. **Where:** K8s `livenessProbe`/`readinessProbe`/`startupProbe`, LB health checks, `/healthz`/`/readyz`/`/livez`, gRPC health service, Spring Actuator. **Breaks:** the orchestrator owns polling, the app owns the endpoint — rename `/healthz`→`/health` without updating the probe + K8s thinks the pod is dead and CrashLoopBackOffs a healthy app; conflating liveness with readiness (checking DB in liveness) kills the pod on a transient DB blip; a readiness probe that doesn't fail fast on shutdown causes 502s during rolling deploys. **Detect:** `/healthz`/`/readyz`/`/livez`/`/health`, `livenessProbe`/`readinessProbe`/`startupProbe`, `app.get('/health'`. **Safe-change:** keep probe path/port in lockstep with the route; liveness = process-only/cheap, readiness = critical deps; readiness fails immediately on SIGTERM; tune `initialDelaySeconds`/`failureThreshold` to real startup. _(Own home — the liveness/readiness/startup-probe contract is its own source-of-truth; cross-ref `graceful-shutdown`, `exposed-port-contract`.)_
752
+
753
+ #### `graceful-shutdown` — HIGH · point
754
+
755
+ The contract for how a process receives termination signals + drains in-flight work before force-kill. **Where:** K8s `terminationGracePeriodSeconds`+`preStop`+SIGTERM→SIGKILL, `process.on('SIGTERM')`, Docker exec-form ENTRYPOINT (PID 1 signal delivery), connection draining. **Breaks:** the platform sends SIGTERM then SIGKILLs after the grace period — shell-form `ENTRYPOINT` means signals hit the shell not your app (PID 1), so SIGTERM is swallowed + the app is hard-killed; failing readiness fast without a `preStop` sleep races kube-proxy still routing to the terminating pod → 502s; exiting before finishing in-flight requests drops them. **Detect:** `SIGTERM`/`SIGINT`, `process.on(`, `terminationGracePeriodSeconds`, `preStop`, `server.close(`, Dockerfile ENTRYPOINT form, `tini`/`dumb-init`. **Safe-change:** exec-form ENTRYPOINT (or `tini`) so SIGTERM reaches the app as PID 1; on SIGTERM fail readiness → stop new conns → drain → exit; `terminationGracePeriodSeconds` ≥ max request + `preStop` sleep; add a `preStop` sleep for endpoint-propagation delay. _(Own home — the SIGTERM/drain contract is its own source-of-truth; cross-ref `health-check-contract`, `process-lifecycle-contract`, `container-image-contract` (ENTRYPOINT form).)_
756
+
757
+ #### `resource-limit-contract` — HIGH · point
758
+
759
+ The CPU/memory (+ ephemeral-storage) requests/limits the platform enforces against actual consumption. **Where:** K8s `resources.requests`/`limits`, Docker `--memory`/`--cpus`, Lambda `MemorySize`(+CPU)+`timeout`, Cloudflare Workers CPU-time limit + 128MB cap, OOMKiller. **Breaks:** the limit is enforced by the kernel/platform not the app — raise heap past the memory `limit` (bigger cache, new dep) → OOMKilled (exit 137) with no app error; JVM/Node heap ignoring the cgroup limit OOMs under load; a Worker adding CPU-heavy work exceeds the CPU-ms limit; CPU `limit` throttling adds silent latency. **Detect:** `resources:`/`limits:`/`requests:`, `--memory`/`--cpus`, `MemorySize`, `cpu_ms`, `--max-old-space-size`, `NODE_OPTIONS`. **Safe-change:** re-evaluate limits when adding caches/deps/concurrency; make heap settings cgroup-aware; set requests/limits from measured P99; budget against CPU-time/timeout caps for Workers/Lambda. _(Own home — the CPU/memory requests/limits contract is its own source-of-truth.)_
760
+
761
+ #### `ipc-protocol-contract` — MED · point
762
+
763
+ The message framing/serialization protocol over an in-host IPC transport (sockets/pipes/shared memory), distinct from a public network API. **Where:** Unix domain sockets, named pipes, gRPC over UDS, Node `cluster`/`child_process` IPC, sidecar↔app sockets, Electron `ipcMain`/`ipcRenderer`. **Breaks:** both ends must agree on framing + message shape but are often versioned independently (a sidecar updated separately) — change the schema/channel name/socket path on one side + messages are dropped/misparsed with no network error; Electron `ipcRenderer.invoke('channel')` silently no-ops if `ipcMain.handle('channel')` was renamed. **Detect:** `unix:`/`.sock`, `ipcMain`/`ipcRenderer`, `child.send`/`process.on('message'`, `createServer` on a socket path, gRPC over UDS. **Safe-change:** version the IPC envelope + validate on receipt; keep channel names/socket paths in a shared constant; coordinate sidecar/app upgrades; don't reuse a socket path across protocol versions. _(Own home — the in-host IPC framing/transport is its own source-of-truth; cross-ref `worker-thread-boundary` (the thread variant).)_
764
+
765
+ #### `cron-timezone-contract` — MED · point
766
+
767
+ The schedule expression + its assumed timezone at the infra/scheduler layer + the runtime TZ. **Where:** K8s `CronJob.spec.schedule`(+`timeZone`), Cloudflare Workers Cron Triggers (always UTC), EventBridge `ScheduleExpression`, system crontab, `TZ` env. **Breaks:** a cron expression has no meaning without a timezone, + the scheduler's TZ often differs from the developer's + the container's — `0 0 * * *` runs at UTC midnight not local, so DST shifts + date-boundary logic (daily rollups, billing cutoffs) run on the wrong day; Cloudflare cron is always UTC; unset container `TZ` defaults to UTC so date-based filenames drift. **Detect:** `schedule:`/`crons`/`ScheduleExpression`/cron expressions, `timeZone`, `TZ=`, `new Date()`/`toISOString` in scheduled jobs. **Safe-change:** pin an explicit `timeZone` on CronJobs; remember most cloud schedulers fire in UTC; set `TZ` explicitly if logic is local-time-sensitive (else standardize UTC end-to-end); re-check date-boundary jobs across DST. _(Folds into async `cron-schedule-contract` — same cron contract; this is its scheduler-TZ/infra facet.)_
768
+
769
+ #### `process-lifecycle-contract` — MED · point
770
+
771
+ The init-system/supervisor contract around PID 1, zombie reaping, restart policy, exit-code semantics. **Where:** Docker PID 1 (`tini`/`dumb-init`), K8s `restartPolicy`+exit-code handling, systemd `Restart=`, supervisord, pm2. **Breaks:** PID 1 in a container has special signal semantics + must reap zombies — a Node/Python app as PID 1 without an init shim leaks zombies + mishandles signals; the orchestrator interprets exit codes (0=success, 137=SIGKILL/OOM, 143=SIGTERM) to decide restart, so exiting 0 on a fatal error makes K8s think the job succeeded; changing `restartPolicy` changes crash-recovery silently. **Detect:** `tini`/`dumb-init`/`ENTRYPOINT`, `restartPolicy`, `process.exit(`, systemd `Restart=`, `pm2`, apps spawning children as PID 1. **Safe-change:** use an init shim if the main process spawns children; exit non-zero on fatal errors; match `restartPolicy` to workload type (`Always` services, `OnFailure`/`Never` jobs); don't swallow SIGTERM/SIGCHLD as PID 1. _(Own home — the PID-1/reaping/restart-policy contract is its own source-of-truth; cross-ref `graceful-shutdown`, `container-image-contract`.)_
772
+
773
+ #### `proxy-header-forwarding-contract` — MED · point
774
+
775
+ The forwarded/trust headers (`X-Forwarded-For`/`-Proto`/`-Host`, `Forwarded`, `X-Real-IP`) + the `trust proxy` setting binding a proxy to the app. **Where:** nginx/Envoy/ALB/Cloudflare adding `X-Forwarded-*`, Express `app.set('trust proxy', …)`, rate-limiters/auth keyed on client IP, HTTPS-redirect reading `X-Forwarded-Proto`. **Breaks:** the app trusts headers the proxy sets — misconfigured `trust proxy` makes the app see the proxy's IP for everyone (rate-limit-by-IP becomes global; audit logs wrong); trusting `X-Forwarded-For` without a trusted proxy lets clients spoof their IP; adding/removing a proxy hop shifts which XFF entry is the real client. **Detect:** `X-Forwarded-`, `trust proxy`, `req.ip`/`req.ips`, `X-Real-IP`, `proxy_set_header`, proxy-hop count. **Safe-change:** set `trust proxy` to the exact hop count/CIDR (never blanket-trust); re-verify client-IP extraction when adding/removing a proxy/CDN; ensure the edge strips client-supplied `X-Forwarded-*`; keep HTTPS-redirect reading the proxy-set `X-Forwarded-Proto`. _(Own home — the forwarded/trust-header + `trust proxy` contract is its own source-of-truth.)_ _(HIGH if it enables spoofing of security controls.)_
776
+
777
+ #### `service-discovery-contract` — HIGH · point
778
+
779
+ The name/address resolution by which a client locates a service instance (DNS name, env-injected URL, registry entry). **Where:** K8s Service DNS (`svc.namespace.svc.cluster.local`), Consul/Eureka, Docker Compose service names, env-injected `*_SERVICE_HOST`/`_PORT`, connection strings. **Breaks:** clients hardcode/env-reference a service name — rename the K8s Service or compose service and consumers get DNS resolution failures; moving to a new namespace changes the FQDN; changing a connection-string env-var name breaks every consumer reading the old name. **Detect:** service hostnames, `*.svc.cluster.local`, `_SERVICE_HOST`, compose `depends_on`/service names, connection-string env vars. **Safe-change:** treat Service names + FQDNs as a stable contract; coordinate namespace moves with all consumers; keep connection-string env-var names stable (deprecate before rename); verify `depends_on`/startup-order assumptions. _(Own home — the service name/address resolution contract is its own source-of-truth; cross-ref `dns-tls-san`, `ingress-routing-rule`, `env-var-contract`.)_
780
+
781
+ ---
782
+
783
+ ## Default severity, at a glance
784
+
785
+ | Tier | Boundary types (defaults — adjust per repo) |
786
+ | --- | --- |
787
+ | **CRITICAL** | information-architecture, tenancy, db-schema, db-migration, row-level-_ / RLS, vector-search-index_, encryption-at-rest-key-boundary, wire-response-envelope, graphql-schema / federation, grpc-protobuf-wire, auth-token-wire / auth-contract / jwt-claims / signing-key-rotation, redirect-uri-allowlist, object-level-authorization, event-schema, cdc-stream, workflow-determinism, schema-registry, client-exposed-env, secrets-management, package-public-api, module-federation-remotes, edge-binding, do-class-contract |
788
+ | **HIGH** | orm-mapping, foreign-keys, index-and-constraint, jsonb-shape, soft-delete, audit/event-sourcing tables, db-view/proc, connection-pool, db-extension/collation, auth-managed-tables, rest-endpoint, error-envelope, http-status, openapi, request-validation, response-dto, persisted-query, grpc-method, trpc-router, api-versioning, cors, inbound/outbound-webhook, store-shape, rtk-query-tags, route-table, url-param, component-props, form-schema, client-storage, ssr-hydration, state-serialization, deep-link, query-cache-key, rbac, abac, oauth-oidc, api-key, sso-saml, impersonation, consent-scope, pii-classification, account-linking, mfa-step-up, workload-identity, password-policy, message-queue-payload, topic-naming, command-event, workflow-signal/activity, delivery-semantics, ordering-key, outbox, saga-step, stream-offset, realtime-channel, websocket-envelope, env-var, env-parity, build-vs-runtime, feature-flag, workspace-graph, peer-dependency, di-container, code-generation, lockfile, monorepo-pipeline, container-image, exposed-port, ingress, cache-header, vary, dns-tls-san, iac-output, inter-service-resilience, health-check, graceful-shutdown, resource-limit, service-discovery, lambda-handler, cold-start |
789
+ | **MED** | db-enum, fulltext-index, cache-key/ttl, read-model/matview, blob-key, selector, react-context, design-token, i18n-keys, feature-flag-gating, asset-path, custom-element, analytics-event, per-identity-rate-limit, content-negotiation, file-upload, conditional-request-caching, idempotency-key, pagination, job-name, cron-schedule, dead-letter, retry-backoff, presence-state, pubsub-channel, batch-window, import-layering, tsconfig-paths/refs, bundler-externals, config-defaults, ci-pipeline, node-engines, css-design-token, worker-thread, trace-context, log-schema, metric-name, ipc-protocol, cron-timezone, process-lifecycle, proxy-header |
790
+ | **LOW** | hateoas-links, dom-event, theme-mode (folded into design-token), shadcn/ui generated primitives |
791
+
792
+ \* vector-search-index defaults HIGH but the IA/embedding-provenance facet can be CRITICAL.
793
+
794
+ ## Extending the taxonomy
795
+
796
+ Add a row here — and all four modes inherit it. For a **new technology**, add a short pack: where its boundaries live + the safe-change checklist. Keep instructions **technology-specific, never vague** — "middleware order is parsers→custom→routes→error-handler-last" beats "be careful with middleware." Only instantiate the types the detected stack/IA actually exhibits; `log()` what you skip and why. When a type genuinely spans two categories, give it one **canonical home** and a one-line cross-reference in the other (as done above for `cors`, `idempotency-key`, `websocket-envelope`, `design-token`, `package-public-api`, `row-level-security`).