@nurix/etna 0.18.0 → 0.18.2
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.
- package/README.md +6 -5
- package/data/harness-index.json +1 -5
- package/data/harnesses/code/CLAUDE.md +8 -19
- package/data/harnesses/code/agents/brainstorm.md +3 -3
- package/data/harnesses/code/agents/change-review.md +13 -10
- package/data/harnesses/code/agents/harness-manager.md +15 -15
- package/data/harnesses/code/agents/maintenance.md +10 -3
- package/data/harnesses/code/agents/performance-review.md +5 -0
- package/data/harnesses/code/harness.json +1 -1
- package/data/harnesses/code/rules/general.md +40 -36
- package/data/harnesses/code/skills/boundary/SKILL.md +15 -42
- package/data/harnesses/code/skills/boundary/references/boundary-taxonomy.md +366 -304
- package/data/harnesses/code/skills/brainstorm/README.md +9 -9
- package/data/harnesses/code/skills/brainstorm/SKILL.md +15 -15
- package/data/harnesses/code/skills/brainstorm/harvest.md +3 -3
- package/data/harnesses/code/skills/brainstorm/templates/backlog.md +2 -2
- package/data/harnesses/code/skills/brainstorm/templates/build.md +6 -6
- package/data/harnesses/code/skills/brainstorm/templates/decisions.md +3 -3
- package/data/harnesses/code/skills/brainstorm/templates/doctrine.md +2 -2
- package/data/harnesses/code/skills/brainstorm/templates/domain-model.md +2 -2
- package/data/harnesses/code/skills/brainstorm/templates/glossary.md +2 -1
- package/data/harnesses/code/skills/brainstorm/templates/non-goals.md +3 -7
- package/data/harnesses/code/skills/brainstorm/templates/questions.md +6 -5
- package/data/harnesses/code/skills/brainstorm/templates/state.md +1 -8
- package/data/harnesses/code/skills/brainstorm/templates/tech.md +6 -6
- package/data/harnesses/code/skills/brainstorm/templates/thesis.md +2 -2
- package/data/harnesses/code/skills/distiller/SKILL.md +48 -177
- package/data/harnesses/code/skills/distiller/references/docs-mode.md +116 -343
- package/data/harnesses/code/skills/distiller/references/method.md +37 -109
- package/data/harnesses/code/skills/documentation/SKILL.md +9 -6
- package/data/harnesses/code/skills/documentation/references/examples.md +30 -61
- package/data/harnesses/code/skills/documentation/references/style-guide.md +59 -62
- package/data/harnesses/code/skills/documentation/references/templates.md +26 -34
- package/data/harnesses/code/skills/workflow-mapper/SKILL.md +32 -11
- package/data/harnesses/code/skills/workflow-mapper/references/patterns.md +1 -1
- package/data/harnesses/discover/CLAUDE.md +60 -113
- package/data/harnesses/discover/agents/api.md +11 -28
- package/data/harnesses/discover/agents/codebase.md +12 -30
- package/data/harnesses/discover/agents/community.md +9 -21
- package/data/harnesses/discover/agents/deployed-client-bundle.md +16 -67
- package/data/harnesses/discover/agents/distribution-artifacts.md +21 -59
- package/data/harnesses/discover/agents/docs.md +11 -20
- package/data/harnesses/discover/agents/infra-backend-fingerprint.md +28 -68
- package/data/harnesses/discover/agents/packages.md +12 -28
- package/data/harnesses/discover/agents/session.md +18 -50
- package/data/harnesses/discover/agents/website.md +9 -19
- package/data/harnesses/discover/agents/wire-capture.md +22 -72
- package/data/harnesses/discover/harness.json +1 -1
- package/data/harnesses/discover/rules/cartography-coverage.md +94 -0
- package/data/harnesses/discover/rules/cartography-flows.md +99 -0
- package/data/harnesses/discover/rules/cartography-ia.md +88 -0
- package/data/harnesses/discover/rules/cartography.md +109 -0
- package/data/harnesses/discover/rules/discovery.md +73 -182
- package/data/harnesses/discover/rules/evaluation.md +92 -98
- package/data/harnesses/discover/rules/ingestion.md +123 -322
- package/data/harnesses/discover/rules/self-correction.md +94 -107
- package/data/harnesses/discover/rules/tradecraft.md +201 -172
- package/data/harnesses/hr/CLAUDE.md +21 -51
- package/data/harnesses/hr/harness.json +1 -1
- package/data/harnesses/hr/skills/interview-eval/SKILL.md +23 -11
- package/data/harnesses/hr/skills/interview-eval/TEMPLATE.md +49 -29
- package/data/harnesses/hr/skills/interview-eval/rubric.md +30 -30
- package/data/harnesses/kb-agi/CLAUDE.md +8 -8
- package/data/harnesses/kb-agi/agents/kb-agi-init.md +31 -22
- package/data/harnesses/kb-agi/agents/kb-catch-stale.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-find-gaps.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-fix-links.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-refresh-summaries.md +2 -2
- package/data/harnesses/kb-agi/agents/kb-sync-metadata.md +2 -2
- package/data/harnesses/kb-agi/harness.json +1 -1
- package/package.json +1 -1
- package/data/harnesses/code-fable/.prettierignore +0 -12
- package/data/harnesses/code-fable/.prettierrc.json +0 -3
- package/data/harnesses/code-fable/.vscode/settings.json +0 -10
- package/data/harnesses/code-fable/CLAUDE.md +0 -135
- package/data/harnesses/code-fable/agents/brainstorm.md +0 -35
- package/data/harnesses/code-fable/agents/change-review.md +0 -248
- package/data/harnesses/code-fable/agents/harness-manager.md +0 -181
- package/data/harnesses/code-fable/agents/maintenance.md +0 -115
- package/data/harnesses/code-fable/agents/performance-review.md +0 -218
- package/data/harnesses/code-fable/agents/spec-mode.md +0 -56
- package/data/harnesses/code-fable/harness.json +0 -12
- package/data/harnesses/code-fable/hooks/hooks.json +0 -27
- package/data/harnesses/code-fable/hooks/lib/redact.mjs +0 -35
- package/data/harnesses/code-fable/hooks/prompt-recall.mjs +0 -155
- package/data/harnesses/code-fable/hooks/session-reflect.mjs +0 -285
- package/data/harnesses/code-fable/rules/general.md +0 -79
- package/data/harnesses/code-fable/rules/nomenclature.md +0 -30
- package/data/harnesses/code-fable/rules/typescript.md +0 -19
- package/data/harnesses/code-fable/skills/boundary/SKILL.md +0 -76
- package/data/harnesses/code-fable/skills/boundary/references/boundary-taxonomy.md +0 -734
- package/data/harnesses/code-fable/skills/brainstorm/README.md +0 -136
- package/data/harnesses/code-fable/skills/brainstorm/SKILL.md +0 -135
- package/data/harnesses/code-fable/skills/brainstorm/harvest.md +0 -26
- package/data/harnesses/code-fable/skills/brainstorm/templates/backlog.md +0 -39
- package/data/harnesses/code-fable/skills/brainstorm/templates/build.md +0 -37
- package/data/harnesses/code-fable/skills/brainstorm/templates/decisions.md +0 -29
- package/data/harnesses/code-fable/skills/brainstorm/templates/doctrine.md +0 -31
- package/data/harnesses/code-fable/skills/brainstorm/templates/domain-model.md +0 -25
- package/data/harnesses/code-fable/skills/brainstorm/templates/glossary.md +0 -19
- package/data/harnesses/code-fable/skills/brainstorm/templates/milestones.md +0 -43
- package/data/harnesses/code-fable/skills/brainstorm/templates/non-goals.md +0 -21
- package/data/harnesses/code-fable/skills/brainstorm/templates/plan-doc.md +0 -61
- package/data/harnesses/code-fable/skills/brainstorm/templates/questions.md +0 -35
- package/data/harnesses/code-fable/skills/brainstorm/templates/references.md +0 -28
- package/data/harnesses/code-fable/skills/brainstorm/templates/rejected.md +0 -29
- package/data/harnesses/code-fable/skills/brainstorm/templates/state.md +0 -8
- package/data/harnesses/code-fable/skills/brainstorm/templates/tech.md +0 -23
- package/data/harnesses/code-fable/skills/brainstorm/templates/thesis.md +0 -21
- package/data/harnesses/code-fable/skills/distiller/SKILL.md +0 -219
- package/data/harnesses/code-fable/skills/distiller/references/docs-mode.md +0 -526
- package/data/harnesses/code-fable/skills/distiller/references/method.md +0 -166
- package/data/harnesses/code-fable/skills/documentation/SKILL.md +0 -125
- package/data/harnesses/code-fable/skills/documentation/references/examples.md +0 -193
- package/data/harnesses/code-fable/skills/documentation/references/style-guide.md +0 -288
- package/data/harnesses/code-fable/skills/documentation/references/templates.md +0 -865
- package/data/harnesses/code-fable/skills/workflow-mapper/SKILL.md +0 -430
- package/data/harnesses/code-fable/skills/workflow-mapper/references/patterns.md +0 -214
|
@@ -1,92 +1,51 @@
|
|
|
1
1
|
# Application Boundary Taxonomy
|
|
2
2
|
|
|
3
|
-
> The shared knowledge root for the `boundary` skill. Every mode —
|
|
4
|
-
> `check`, `generate`, `create`, `init` — reads this file. It is the single
|
|
5
|
-
> source of truth for *what an application boundary is* and *which kinds exist*,
|
|
6
|
-
> so the four modes can never drift from each other. Extend the catalog here and
|
|
7
|
-
> all four modes inherit it at once.
|
|
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.
|
|
8
4
|
|
|
9
5
|
---
|
|
10
6
|
|
|
11
7
|
## What an application boundary is
|
|
12
8
|
|
|
13
|
-
An **application boundary** is a horizontal, cross-cutting contract — an
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
only at
|
|
18
|
-
|
|
19
|
-
The dangerous boundaries are the **invisible** ones. A team documents "auth" but
|
|
20
|
-
never "the Redux store shape" or "the entity model," because those read as *just
|
|
21
|
-
how the app is built*. This taxonomy names them so they can't hide.
|
|
22
|
-
|
|
23
|
-
The recurring failure signature across every category is the same: **two sides
|
|
24
|
-
that are never statically linked** — a reader and a provider, a producer and a
|
|
25
|
-
consumer, a schema and its generated client, a host and a remote, a config and
|
|
26
|
-
its bundler — so a change on one side surfaces only at boot, install, deploy,
|
|
27
|
-
replay, or never-loudly-at-all (a leaked secret, a stale cache, a silent
|
|
28
|
-
cross-tenant read). The universal mitigation: **make the implicit contract
|
|
29
|
-
explicit and CI-checkable**, and treat the identifier (column, key, route, tag,
|
|
30
|
-
topic, token, header, binding) as a versioned API.
|
|
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.
|
|
31
14
|
|
|
32
15
|
## The two categories
|
|
33
16
|
|
|
34
17
|
| Category | Sourced from | Examples |
|
|
35
|
-
|
|
18
|
+
| --- | --- | --- |
|
|
36
19
|
| **technology** | stack detection (`package.json` + lockfile + source) | db-schema, wire-response-envelope, store-shape, edge-binding |
|
|
37
|
-
| **domain** | the product's information architecture (ADRs, doctrine, contracts pkg) —
|
|
20
|
+
| **domain** | the product's information architecture (ADRs, doctrine, contracts pkg) — _not_ stack detection | the entity model + topology, tenancy |
|
|
38
21
|
|
|
39
|
-
The **domain** boundaries are stack-independent and the most catastrophic when
|
|
40
|
-
broken; they come from the IA, not from `package.json`. Everything else is
|
|
41
|
-
**technology** — detected from the deps and source.
|
|
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.
|
|
42
23
|
|
|
43
24
|
## `point` vs `transcribe` — the doc-mode
|
|
44
25
|
|
|
45
|
-
Each boundary type carries a **doc-mode** that decides how its file records the
|
|
46
|
-
shape:
|
|
26
|
+
Each boundary type carries a **doc-mode** that decides how its file records the shape:
|
|
47
27
|
|
|
48
|
-
- **`point`** —
|
|
49
|
-
|
|
50
|
-
(`file:line`) + invariants + safe-change checklist**. Do **not** transcribe the
|
|
51
|
-
shape — a copy rots on the next codegen/push; the code stays source-of-truth.
|
|
52
|
-
- **`transcribe`** — *synthesized* contracts with no single code home: the
|
|
53
|
-
composed Redux store tree (spread across slices), the API response envelope (a
|
|
54
|
-
convention), the IA topology, tenancy. Transcribe these canonically into the
|
|
55
|
-
boundary file; feature docs own only their slice and link up.
|
|
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.
|
|
56
30
|
|
|
57
|
-
Docs never outrank code. For `point` boundaries you mirror with `file:line`, you
|
|
58
|
-
do not fork.
|
|
31
|
+
Docs never outrank code. For `point` boundaries you mirror with `file:line`, you do not fork.
|
|
59
32
|
|
|
60
33
|
## Greenfield / pre-code boundaries
|
|
61
34
|
|
|
62
|
-
When `generate` runs on a repo whose contract is **decided but has no code yet**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- **Mark it `point*`** in the index (a `transcribe` boundary whose canonical source
|
|
73
|
-
is likewise pre-code is `transcribe*`) — the `*` reads against an index footnote:
|
|
74
|
-
*"point-mode boundary whose pointer lands at implementation — converts to (or
|
|
75
|
-
gains) a `file:line` pointer when the module lands."*
|
|
76
|
-
- The last safe-change checklist item is "replace the planned pointer with the real
|
|
77
|
-
`file:line` once code lands."
|
|
78
|
-
|
|
79
|
-
A safe-change item that hinges on an **unresolved decision** links to its open-question
|
|
80
|
-
or ADR destination rather than asserting a resolution (kb.md: link, don't fabricate).
|
|
81
|
-
|
|
82
|
-
The index also carries a **Skipped (premature for a pre-code repo)** block: boundary
|
|
83
|
-
types not yet exhibited, one line each on why (e.g. *"workspace-package-graph —
|
|
84
|
-
packages not scaffolded; add at scaffold time"*). See the index spec below.
|
|
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.
|
|
85
45
|
|
|
86
46
|
## Boundary file anatomy
|
|
87
47
|
|
|
88
|
-
Every file the skill writes to `docs/boundaries/` follows this shape — the format
|
|
89
|
-
that `check` and `init` parse:
|
|
48
|
+
Every file the skill writes to `docs/boundaries/` follows this shape — the format that `check` and `init` parse:
|
|
90
49
|
|
|
91
50
|
```markdown
|
|
92
51
|
# <Boundary name>
|
|
@@ -96,21 +55,24 @@ that `check` and `init` parse:
|
|
|
96
55
|
**Bounds.** <the two sides of this contract — two modules, two layers (app↔DB, render↔hydrate, producer↔consumer), or a module↔external-system>
|
|
97
56
|
|
|
98
57
|
## Shape
|
|
58
|
+
|
|
99
59
|
<transcribed contract, OR a pointer: `packages/.../schema.ts:NN` with one-line orientation>
|
|
100
60
|
|
|
101
61
|
## Invariants
|
|
62
|
+
|
|
102
63
|
<what must hold>
|
|
103
64
|
|
|
104
65
|
## Blast radius
|
|
66
|
+
|
|
105
67
|
<what breaks, and where, if violated>
|
|
106
68
|
|
|
107
69
|
## Safe-change checklist
|
|
70
|
+
|
|
108
71
|
- [ ] <step>
|
|
109
72
|
- [ ] <step>
|
|
110
73
|
```
|
|
111
74
|
|
|
112
|
-
`docs/boundaries/README.md` is the index. Its table is one row per boundary with a
|
|
113
|
-
**doc-mode column**, so the fold/skip decisions are auditable from the index alone:
|
|
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:
|
|
114
76
|
|
|
115
77
|
```markdown
|
|
116
78
|
| Boundary | Type | Severity | Doc-mode | Bounds (summary) |
|
|
@@ -118,603 +80,711 @@ that `check` and `init` parse:
|
|
|
118
80
|
|
|
119
81
|
Below the table, three standing blocks:
|
|
120
82
|
|
|
121
|
-
- **Active technology packs** — which packs (persistence, wire, auth, async, config,
|
|
122
|
-
|
|
123
|
-
- **
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
If any boundary is pre-code (`point*`/`transcribe*`), add the footnote: *"\* point-mode
|
|
131
|
-
boundary whose pointer lands at implementation — converts to (or gains) a `file:line`
|
|
132
|
-
pointer when the module lands."*
|
|
133
|
-
|
|
134
|
-
**Name the bounding code units** (paths /
|
|
135
|
-
package names) in every `Bounds` line — the distinct vertices named across all
|
|
136
|
-
`Bounds` lines are the **module-discovery feed** `harness-manager` reads to
|
|
137
|
-
propose modules, so name them precisely. A `Bounds` line is not restricted to two
|
|
138
|
-
modules: it may name two **layers** within one module (app↔DB, render↔hydrate,
|
|
139
|
-
producer↔consumer) or a module and an **external system** — so a flat /
|
|
140
|
-
single-module repo still has real boundaries to file (as the intro states).
|
|
141
|
-
|
|
142
|
-
**The clustering key — a convergence heuristic, not a hard count:** *one boundary
|
|
143
|
-
file per (source-of-truth × doc-mode)*. Types that point at the same
|
|
144
|
-
source-of-truth in the same doc-mode collapse into one file — all persistence
|
|
145
|
-
types reading one relational schema fold into the `db-schema` file; split only
|
|
146
|
-
where the source-of-truth or the doc-mode differs (a `transcribe` shape over the
|
|
147
|
-
same schema, or a different store). Each catalog entry below carries a fold note:
|
|
148
|
-
**Folds into `x`** (this type lands in `x`'s file), **Canonical home** (this type
|
|
149
|
-
*is* the file others fold into), or **Own home** (a standalone source-of-truth ×
|
|
150
|
-
doc-mode). Two `generate` runs converge on the same file set only where this
|
|
151
|
-
fold-map names the home — where it doesn't, the count is a judgment call, not a
|
|
152
|
-
guarantee.
|
|
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** `harness-manager` 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.
|
|
153
92
|
|
|
154
93
|
## Severity is load-bearing
|
|
155
94
|
|
|
156
|
-
Severity (`CRITICAL` / `HIGH` / `MED` / `LOW`) sets `check`-mode review depth and
|
|
157
|
-
`init`-mode scan priority. Each entry below carries a default; adjust from the
|
|
158
|
-
concrete repo + project memory. The pattern that holds across all categories:
|
|
159
|
-
**CRITICAL belongs to shared, single-source, persisted, or trust-crossing
|
|
160
|
-
contracts** (the IA model, db-schema, the wire envelope, auth/tenancy, event
|
|
161
|
-
schemas, secrets, federation/remotes) — the ones touched by everything or that
|
|
162
|
-
break *historical/in-flight* state, not just live traffic.
|
|
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.
|
|
163
96
|
|
|
164
|
-
The default is the type's **intrinsic** severity: the skill may **down-adjust** a
|
|
165
|
-
single type with a reason logged in `docs/memory/boundary.md`, but there is **no
|
|
166
|
-
relative rank and no up-or-down quota** across the repo's boundary set — three
|
|
167
|
-
genuine CRITICALs in a micro repo is correct signal, not over-inflation to be
|
|
168
|
-
capped.
|
|
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.
|
|
169
98
|
|
|
170
99
|
---
|
|
171
100
|
|
|
172
101
|
# The Catalog
|
|
173
102
|
|
|
174
|
-
Organized by the seven boundary categories. Each entry: **type-name —
|
|
175
|
-
SEVERITY · doc-mode**, then what it is · where it appears · what breaks · how to
|
|
176
|
-
detect · the safe-change essentials. Only instantiate the types the detected
|
|
177
|
-
stack/IA actually exhibits; `log()` what you skip and why.
|
|
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.
|
|
178
104
|
|
|
179
105
|
---
|
|
180
106
|
|
|
181
107
|
## 0 · DOMAIN (stack-independent — sourced from the IA, not `package.json`)
|
|
182
108
|
|
|
183
109
|
#### `information-architecture` — CRITICAL · transcribe
|
|
184
|
-
|
|
110
|
+
|
|
111
|
+
The entity model + topology (e.g. Brief→Plan→Persona→Audience→Match→Campaign→Run; service planes). **Where:** synthesized from ADRs / module pillars / 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, ADRs), `docs/modules/*/pillars/`, 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._
|
|
185
112
|
|
|
186
113
|
#### `tenancy` — CRITICAL · transcribe
|
|
187
|
-
|
|
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).)_
|
|
188
116
|
|
|
189
117
|
---
|
|
190
118
|
|
|
191
119
|
## 1 · PERSISTENCE & DATA MODEL
|
|
192
120
|
|
|
193
121
|
#### `db-schema` — CRITICAL · point
|
|
194
|
-
|
|
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.)_
|
|
195
124
|
|
|
196
125
|
#### `orm-model-mapping` — HIGH · point
|
|
197
|
-
|
|
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.)_
|
|
198
128
|
|
|
199
129
|
#### `db-migration` — CRITICAL · point
|
|
200
|
-
|
|
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.)_
|
|
201
132
|
|
|
202
133
|
#### `foreign-key-and-relations` — HIGH · point
|
|
203
|
-
|
|
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.)_
|
|
204
136
|
|
|
205
137
|
#### `index-and-constraint` — HIGH · point
|
|
206
|
-
|
|
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.)_
|
|
207
140
|
|
|
208
141
|
#### `db-enum` — MED · point
|
|
209
|
-
|
|
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.)_
|
|
210
144
|
|
|
211
145
|
#### `jsonb-column-shape` — HIGH · transcribe
|
|
212
|
-
|
|
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.)_
|
|
213
148
|
|
|
214
149
|
#### `fulltext-search-index` — MED · point
|
|
215
|
-
|
|
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`.)_
|
|
216
152
|
|
|
217
153
|
#### `vector-search-index` — HIGH · point
|
|
218
|
-
|
|
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.)_
|
|
219
156
|
|
|
220
157
|
#### `cache-key-schema` — MED · transcribe
|
|
221
|
-
|
|
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).)_
|
|
222
160
|
|
|
223
161
|
#### `cache-ttl-and-consistency` — MED · transcribe
|
|
224
|
-
|
|
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.)_
|
|
225
164
|
|
|
226
165
|
#### `row-level-multi-tenancy` — CRITICAL · transcribe
|
|
227
|
-
|
|
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`.)_
|
|
228
168
|
|
|
229
169
|
#### `soft-delete-convention` — HIGH · transcribe
|
|
230
|
-
|
|
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`.)_
|
|
231
172
|
|
|
232
173
|
#### `audit-and-event-sourcing-tables` — HIGH · point
|
|
233
|
-
|
|
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.)_
|
|
234
176
|
|
|
235
177
|
#### `read-model-projection-and-materialized-view` — MED · point
|
|
236
|
-
|
|
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`.)_
|
|
237
180
|
|
|
238
181
|
#### `db-view-and-stored-procedure` — HIGH · point
|
|
239
|
-
|
|
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.)_
|
|
240
184
|
|
|
241
185
|
#### `blob-object-storage-key-convention` — MED · point
|
|
242
|
-
|
|
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`.)_
|
|
243
188
|
|
|
244
189
|
#### `connection-pool-and-isolation-level` — HIGH · point
|
|
245
|
-
|
|
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`.)_
|
|
246
192
|
|
|
247
193
|
#### `db-extension-and-collation` — HIGH · point
|
|
248
|
-
|
|
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`.)_
|
|
249
196
|
|
|
250
197
|
#### `auth-and-framework-managed-tables` — HIGH · point
|
|
251
|
-
|
|
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.)_
|
|
252
200
|
|
|
253
201
|
---
|
|
254
202
|
|
|
255
203
|
## 2 · API & WIRE / SYNCHRONOUS CONTRACTS
|
|
256
204
|
|
|
257
205
|
#### `wire-response-envelope` — CRITICAL · transcribe
|
|
258
|
-
|
|
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`.)_
|
|
259
208
|
|
|
260
209
|
#### `error-envelope-and-error-codes` — HIGH · transcribe
|
|
261
|
-
|
|
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.)_
|
|
262
212
|
|
|
263
213
|
#### `rest-endpoint-shape` — HIGH · point
|
|
264
|
-
|
|
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.)_
|
|
265
216
|
|
|
266
217
|
#### `http-status-code-convention` — HIGH · transcribe
|
|
267
|
-
|
|
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`.)_
|
|
268
220
|
|
|
269
221
|
#### `openapi-spec-contract` — HIGH · point
|
|
270
|
-
|
|
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.)_
|
|
271
224
|
|
|
272
225
|
#### `request-validation-schema` — HIGH · point
|
|
273
|
-
|
|
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`.)_
|
|
274
228
|
|
|
275
229
|
#### `response-dto-shape` — HIGH · point
|
|
276
|
-
|
|
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).)_
|
|
277
232
|
|
|
278
233
|
#### `graphql-schema-contract` — CRITICAL · point
|
|
279
|
-
|
|
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).)\*
|
|
280
236
|
|
|
281
237
|
#### `graphql-federation-subgraph-contract` — CRITICAL · point
|
|
282
|
-
|
|
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.)_
|
|
283
240
|
|
|
284
241
|
#### `graphql-persisted-query-contract` — HIGH · point
|
|
285
|
-
|
|
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`.)_
|
|
286
244
|
|
|
287
245
|
#### `grpc-protobuf-wire-contract` — CRITICAL · point
|
|
288
|
-
|
|
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`).)_
|
|
289
248
|
|
|
290
249
|
#### `grpc-service-method-contract` — HIGH · point
|
|
291
|
-
|
|
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.)_
|
|
292
252
|
|
|
293
253
|
#### `trpc-router-contract` — HIGH · point
|
|
294
|
-
|
|
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.)_
|
|
295
256
|
|
|
296
257
|
#### `api-versioning-strategy` — HIGH · transcribe
|
|
297
|
-
|
|
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.)_
|
|
298
260
|
|
|
299
261
|
#### `pagination-filtering-sorting-contract` — MED · transcribe
|
|
300
|
-
|
|
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.)_
|
|
301
264
|
|
|
302
265
|
#### `cors-preflight-contract` — HIGH · transcribe
|
|
303
|
-
|
|
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.)_
|
|
304
268
|
|
|
305
269
|
#### `content-negotiation-media-type` — MED · point
|
|
306
|
-
|
|
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`.)_
|
|
307
272
|
|
|
308
273
|
#### `file-upload-multipart-contract` — MED · point
|
|
309
|
-
|
|
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.)_
|
|
310
276
|
|
|
311
277
|
#### `inbound-webhook-contract` — HIGH · point
|
|
312
|
-
|
|
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.)_
|
|
313
280
|
|
|
314
281
|
#### `outbound-webhook-contract` — HIGH · transcribe
|
|
315
|
-
|
|
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.)_
|
|
316
284
|
|
|
317
285
|
#### `auth-token-wire-contract` — CRITICAL · transcribe
|
|
318
|
-
|
|
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.)_
|
|
319
288
|
|
|
320
289
|
#### `conditional-request-caching-contract` — MED · point
|
|
321
|
-
|
|
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`.)_
|
|
322
292
|
|
|
323
293
|
#### `idempotency-key-contract` — HIGH · transcribe
|
|
324
|
-
|
|
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.)_
|
|
325
296
|
|
|
326
297
|
---
|
|
327
298
|
|
|
328
299
|
## 3 · FRONTEND, CLIENT-STATE & UI
|
|
329
300
|
|
|
330
|
-
> Recurring failure mode in this category: **untyped string identifiers** — slice
|
|
331
|
-
> paths, cache tags, route paths, param keys, i18n/flag/event/storage/query keys
|
|
332
|
-
> — where producer and consumer drift with *no compile error*, surfacing only as
|
|
333
|
-
> silent runtime degradation. Universal fix: centralize each identifier set in one
|
|
334
|
-
> typed module and CI-gate drift.
|
|
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.
|
|
335
302
|
|
|
336
303
|
#### `store-shape` — HIGH · transcribe
|
|
337
|
-
|
|
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`.)_
|
|
338
306
|
|
|
339
307
|
#### `selector-contract` — MED · point
|
|
340
|
-
|
|
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.)_
|
|
341
310
|
|
|
342
311
|
#### `rtk-query-cache-tags` — HIGH · point
|
|
343
|
-
|
|
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`.)_
|
|
344
314
|
|
|
345
315
|
#### `route-table` — HIGH · point
|
|
346
|
-
|
|
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.)_
|
|
347
318
|
|
|
348
319
|
#### `url-param-contract` — HIGH · point
|
|
349
|
-
|
|
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.)_
|
|
350
322
|
|
|
351
323
|
#### `component-prop-contract` — HIGH · point
|
|
352
|
-
|
|
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).)_
|
|
353
326
|
|
|
354
327
|
#### `react-context-contract` — MED · point
|
|
355
|
-
|
|
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.)_
|
|
356
330
|
|
|
357
331
|
#### `design-token-contract` — MED · transcribe
|
|
358
|
-
|
|
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.)_
|
|
359
334
|
|
|
360
335
|
#### `i18n-message-keys` — MED · point
|
|
361
|
-
|
|
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.)_
|
|
362
338
|
|
|
363
339
|
#### `form-schema-contract` — HIGH · point
|
|
364
|
-
|
|
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).)_
|
|
365
342
|
|
|
366
343
|
#### `client-storage-schema` — HIGH · transcribe
|
|
367
|
-
|
|
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.)_
|
|
368
346
|
|
|
369
347
|
#### `feature-flag-gating` — MED · point
|
|
370
|
-
|
|
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.)_
|
|
371
350
|
|
|
372
351
|
#### `asset-public-path-contract` — MED · point
|
|
373
|
-
|
|
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.)_
|
|
374
354
|
|
|
375
355
|
#### `ssr-hydration-contract` — HIGH · point
|
|
376
|
-
|
|
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`.)_
|
|
377
358
|
|
|
378
359
|
#### `state-serialization-contract` — HIGH · transcribe
|
|
379
|
-
|
|
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.)_
|
|
380
362
|
|
|
381
363
|
#### `custom-element-contract` — MED · point
|
|
382
|
-
|
|
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).)_
|
|
383
366
|
|
|
384
367
|
#### `dom-event-contract` — LOW · point
|
|
385
|
-
|
|
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.)_
|
|
386
370
|
|
|
387
371
|
#### `deep-link-scheme-contract` — HIGH · point
|
|
388
|
-
|
|
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.)_
|
|
389
374
|
|
|
390
375
|
#### `analytics-event-contract` — MED · transcribe
|
|
391
|
-
|
|
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`.)_
|
|
392
378
|
|
|
393
379
|
#### `query-cache-key-contract` — HIGH · point
|
|
394
|
-
|
|
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`.)_
|
|
395
382
|
|
|
396
383
|
---
|
|
397
384
|
|
|
398
385
|
## 4 · IDENTITY, AUTH & TENANCY
|
|
399
386
|
|
|
400
|
-
> See domain `tenancy` for the cross-layer tenant-isolation invariant. The
|
|
401
|
-
> CRITICAL tier here is anything whose failure is a *silent auth bypass or data
|
|
402
|
-
> disclosure*, not a loud error.
|
|
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.
|
|
403
388
|
|
|
404
389
|
#### `auth-contract` — CRITICAL · transcribe
|
|
405
|
-
|
|
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`).)_
|
|
406
392
|
|
|
407
393
|
#### `jwt-claims-contract` — CRITICAL · point
|
|
408
|
-
|
|
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`.)_
|
|
409
396
|
|
|
410
397
|
#### `signing-key-rotation` — CRITICAL · point
|
|
411
|
-
|
|
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.)_
|
|
412
400
|
|
|
413
401
|
#### `refresh-token-rotation` — HIGH · point
|
|
414
|
-
|
|
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.)_
|
|
415
404
|
|
|
416
405
|
#### `session-cookie-attributes` — HIGH · point
|
|
417
|
-
|
|
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).)_
|
|
418
408
|
|
|
419
409
|
#### `csrf-token-contract` — HIGH · point
|
|
420
|
-
|
|
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.)_
|
|
421
412
|
|
|
422
413
|
#### `row-level-security` — CRITICAL · transcribe
|
|
423
|
-
|
|
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).)_
|
|
424
416
|
|
|
425
417
|
#### `object-level-authorization` — CRITICAL · point
|
|
426
|
-
|
|
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`.)_
|
|
427
420
|
|
|
428
421
|
#### `rbac-permission-model` — HIGH · transcribe
|
|
429
|
-
|
|
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.)_
|
|
430
424
|
|
|
431
425
|
#### `abac-policy-engine` — HIGH · point
|
|
432
|
-
|
|
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`.)_
|
|
433
428
|
|
|
434
429
|
#### `oauth-oidc-flow` — HIGH · point
|
|
435
|
-
|
|
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.)_
|
|
436
432
|
|
|
437
433
|
#### `redirect-uri-allowlist` — CRITICAL · point
|
|
438
|
-
|
|
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.)_
|
|
439
436
|
|
|
440
437
|
#### `api-key-auth` — HIGH · point
|
|
441
|
-
|
|
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.)_
|
|
442
440
|
|
|
443
441
|
#### `sso-saml-assertion` — HIGH · point
|
|
444
|
-
|
|
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.)_
|
|
445
444
|
|
|
446
445
|
#### `impersonation` — HIGH · transcribe
|
|
447
|
-
|
|
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
448
|
|
|
449
449
|
#### `consent-scope-grant` — HIGH · transcribe
|
|
450
|
-
|
|
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.)_
|
|
451
452
|
|
|
452
453
|
#### `per-identity-rate-limit-quota` — MED · transcribe
|
|
453
|
-
|
|
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.)_
|
|
454
456
|
|
|
455
457
|
#### `encryption-at-rest-key-boundary` — CRITICAL · point
|
|
456
|
-
|
|
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.)_
|
|
457
460
|
|
|
458
461
|
#### `pii-classification` — HIGH · transcribe
|
|
459
|
-
|
|
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.)_
|
|
460
464
|
|
|
461
465
|
#### `account-linking-identity-resolution` — HIGH · point
|
|
462
|
-
|
|
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.)_
|
|
463
468
|
|
|
464
469
|
#### `mfa-step-up-contract` — HIGH · point
|
|
465
|
-
|
|
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.)_
|
|
466
472
|
|
|
467
473
|
#### `service-account-workload-identity` — HIGH · point
|
|
468
|
-
|
|
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.)_
|
|
469
476
|
|
|
470
477
|
#### `password-credential-policy` — HIGH · point
|
|
471
|
-
|
|
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.)_
|
|
472
480
|
|
|
473
481
|
---
|
|
474
482
|
|
|
475
483
|
## 5 · ASYNCHRONOUS & MESSAGING
|
|
476
484
|
|
|
477
|
-
> The CRITICAL tier here (`event-schema`, `cdc-stream-contract`,
|
|
478
|
-
> `workflow-determinism-contract`, `schema-registry-contract`) shares one trait:
|
|
479
|
-
> failure breaks **persisted / in-flight / replayed** state — silent *and*
|
|
480
|
-
> historical, not just live. The universal defense: producer/consumer version
|
|
481
|
-
> skew survives a rolling deploy only if payloads are additive + versioned and
|
|
482
|
-
> both sides share one validated schema.
|
|
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.
|
|
483
486
|
|
|
484
487
|
#### `message-queue-payload` — HIGH · transcribe
|
|
485
|
-
|
|
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`).)_
|
|
486
490
|
|
|
487
491
|
#### `job-name-contract` — MED · point
|
|
488
|
-
|
|
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.)_
|
|
489
494
|
|
|
490
495
|
#### `event-schema` — CRITICAL · transcribe
|
|
491
|
-
|
|
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.)_
|
|
492
498
|
|
|
493
499
|
#### `topic-subject-naming` — HIGH · point
|
|
494
|
-
|
|
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.)_
|
|
495
502
|
|
|
496
503
|
#### `command-event-boundary` — HIGH · transcribe
|
|
497
|
-
|
|
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.)_
|
|
498
506
|
|
|
499
507
|
#### `cdc-stream-contract` — CRITICAL · point
|
|
500
|
-
|
|
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.)_
|
|
501
510
|
|
|
502
511
|
#### `cron-schedule-contract` — MED · point
|
|
503
|
-
|
|
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).)_
|
|
504
514
|
|
|
505
515
|
#### `workflow-determinism-contract` — CRITICAL · point
|
|
506
|
-
|
|
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.)_
|
|
507
518
|
|
|
508
519
|
#### `workflow-signal-query-contract` — HIGH · point
|
|
509
|
-
|
|
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.)_
|
|
510
522
|
|
|
511
523
|
#### `workflow-activity-contract` — HIGH · point
|
|
512
|
-
|
|
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.)_
|
|
513
526
|
|
|
514
527
|
#### `delivery-semantics-contract` — HIGH · transcribe
|
|
515
|
-
|
|
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.)_
|
|
516
530
|
|
|
517
531
|
#### `message-ordering-partition-key` — HIGH · point
|
|
518
|
-
|
|
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.)_
|
|
519
534
|
|
|
520
535
|
#### `outbox-pattern-contract` — HIGH · point
|
|
521
|
-
|
|
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).)_
|
|
522
538
|
|
|
523
539
|
#### `saga-step-contract` — HIGH · transcribe
|
|
524
|
-
|
|
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.)_
|
|
525
542
|
|
|
526
543
|
#### `dead-letter-queue-contract` — MED · point
|
|
527
|
-
|
|
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).)_
|
|
528
546
|
|
|
529
547
|
#### `retry-backoff-contract` — MED · transcribe
|
|
530
|
-
|
|
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).)_
|
|
531
550
|
|
|
532
551
|
#### `schema-registry-contract` — CRITICAL · point
|
|
533
|
-
|
|
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.)_
|
|
534
554
|
|
|
535
555
|
#### `realtime-channel-naming` — HIGH · transcribe
|
|
536
|
-
|
|
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`.)_
|
|
537
558
|
|
|
538
559
|
#### `presence-state-contract` — MED · transcribe
|
|
539
|
-
|
|
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.)_
|
|
540
562
|
|
|
541
563
|
#### `websocket-wire-envelope` — HIGH · transcribe
|
|
542
|
-
|
|
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`.)_
|
|
543
566
|
|
|
544
567
|
#### `pubsub-channel-contract` — MED · point
|
|
545
|
-
|
|
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`.)_
|
|
546
570
|
|
|
547
571
|
#### `stream-consumer-offset-contract` — HIGH · point
|
|
548
|
-
|
|
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.)_
|
|
549
574
|
|
|
550
575
|
#### `batch-window-contract` — MED · point
|
|
551
|
-
|
|
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.)_
|
|
552
578
|
|
|
553
579
|
---
|
|
554
580
|
|
|
555
581
|
## 6 · CONFIG, ENV & BUILD
|
|
556
582
|
|
|
557
|
-
> Recurring signature: two sides never statically linked (reader/provider,
|
|
558
|
-
> schema/generated, host/remote, tsconfig/bundler, package.json/lockfile) — a
|
|
559
|
-
> change on one passes compilation and surfaces only at boot/install/deploy, or
|
|
560
|
-
> never-loudly (leaked secret, stale cache, wrong DI scope). The highest-severity
|
|
561
|
-
> ones cross a **trust or deploy boundary**.
|
|
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**.
|
|
562
584
|
|
|
563
585
|
#### `env-var-contract` — HIGH · transcribe
|
|
564
|
-
|
|
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).)_
|
|
565
588
|
|
|
566
589
|
#### `env-parity` — HIGH · transcribe
|
|
567
|
-
|
|
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.)_
|
|
568
592
|
|
|
569
593
|
#### `build-time-vs-runtime-config` — HIGH · point
|
|
570
|
-
|
|
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.)_
|
|
571
596
|
|
|
572
597
|
#### `client-exposed-env` — CRITICAL · point
|
|
573
|
-
|
|
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).)_
|
|
574
600
|
|
|
575
601
|
#### `secrets-management` — CRITICAL · point
|
|
576
|
-
|
|
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.)_
|
|
577
604
|
|
|
578
605
|
#### `feature-flag-contract` — HIGH · transcribe
|
|
579
|
-
|
|
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).)_
|
|
580
608
|
|
|
581
609
|
#### `workspace-package-graph` — HIGH · point
|
|
582
|
-
|
|
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 `harness-manager` reads boundary `Bounds` vertices.)_
|
|
583
612
|
|
|
584
613
|
#### `import-layering-rule` — MED · point
|
|
585
|
-
|
|
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.)_
|
|
586
616
|
|
|
587
617
|
#### `tsconfig-paths` — MED · point
|
|
588
|
-
|
|
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.)_
|
|
589
620
|
|
|
590
621
|
#### `tsconfig-project-references` — MED · point
|
|
591
|
-
|
|
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.)_
|
|
592
624
|
|
|
593
625
|
#### `package-public-api` — CRITICAL · point
|
|
594
|
-
|
|
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`.)_
|
|
595
628
|
|
|
596
629
|
#### `peer-dependency-contract` — HIGH · point
|
|
597
|
-
|
|
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).)_
|
|
598
632
|
|
|
599
633
|
#### `module-federation-remotes` — CRITICAL · point
|
|
600
|
-
|
|
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.)_
|
|
601
636
|
|
|
602
637
|
#### `di-container-registration` — HIGH · point
|
|
603
|
-
|
|
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.)_
|
|
604
640
|
|
|
605
641
|
#### `code-generation-io` — HIGH · point
|
|
606
|
-
|
|
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.)_
|
|
607
644
|
|
|
608
645
|
#### `bundler-entry-output-externals` — MED · point
|
|
609
|
-
|
|
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.)_
|
|
610
648
|
|
|
611
649
|
#### `docker-build-arg-env` — HIGH · point
|
|
612
|
-
|
|
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`.)_
|
|
613
652
|
|
|
614
653
|
#### `config-schema-defaults` — MED · point
|
|
615
|
-
|
|
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).)_
|
|
616
656
|
|
|
617
657
|
#### `ci-pipeline-config-contract` — MED · point
|
|
618
|
-
|
|
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`.)_
|
|
619
660
|
|
|
620
661
|
#### `node-engines-runtime-contract` — MED · point
|
|
621
|
-
|
|
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`.)_
|
|
622
664
|
|
|
623
665
|
#### `lockfile-resolution-contract` — HIGH · point
|
|
624
|
-
|
|
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`.)_
|
|
625
668
|
|
|
626
669
|
#### `monorepo-task-pipeline` — HIGH · point
|
|
627
|
-
|
|
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).)_
|
|
628
672
|
|
|
629
673
|
#### `css-design-token-contract` — MED · transcribe
|
|
630
|
-
|
|
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.)_
|
|
631
676
|
|
|
632
677
|
---
|
|
633
678
|
|
|
634
679
|
## 7 · RUNTIME, PROCESS, NETWORK & INFRA
|
|
635
680
|
|
|
636
681
|
#### `edge-binding` — CRITICAL · point
|
|
637
|
-
|
|
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.)_
|
|
638
684
|
|
|
639
685
|
#### `do-class-contract` — CRITICAL · point
|
|
640
|
-
|
|
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.)_
|
|
641
688
|
|
|
642
689
|
#### `lambda-handler-contract` — HIGH · point
|
|
643
|
-
|
|
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.)_
|
|
644
692
|
|
|
645
693
|
#### `cold-start-assumption` — HIGH · transcribe
|
|
646
|
-
|
|
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.)_
|
|
647
696
|
|
|
648
697
|
#### `worker-thread-boundary` — MED · transcribe
|
|
649
|
-
|
|
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).)_
|
|
650
700
|
|
|
651
701
|
#### `container-image-contract` — HIGH · point
|
|
652
|
-
|
|
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`.)_
|
|
653
704
|
|
|
654
705
|
#### `exposed-port-contract` — HIGH · point
|
|
655
|
-
|
|
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`.)_
|
|
656
708
|
|
|
657
709
|
#### `ingress-routing-rule` — HIGH · point
|
|
658
|
-
|
|
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`.)_
|
|
659
712
|
|
|
660
713
|
#### `cors-policy` — HIGH · transcribe
|
|
661
|
-
|
|
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.)_
|
|
662
716
|
|
|
663
717
|
#### `cache-header-contract` — HIGH · transcribe
|
|
664
|
-
|
|
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`.)_
|
|
665
720
|
|
|
666
721
|
#### `vary-header-contract` — HIGH · point
|
|
667
|
-
|
|
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`.)_
|
|
668
724
|
|
|
669
725
|
#### `dns-tls-san` — HIGH · point
|
|
670
|
-
|
|
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`.)_
|
|
671
728
|
|
|
672
729
|
#### `iac-output-contract` — HIGH · point
|
|
673
|
-
|
|
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.)_
|
|
674
732
|
|
|
675
733
|
#### `inter-service-resilience-contract` — HIGH · transcribe
|
|
676
|
-
|
|
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.)_
|
|
677
736
|
|
|
678
737
|
#### `trace-context-propagation` — MED · point
|
|
679
|
-
|
|
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.)_
|
|
680
740
|
|
|
681
741
|
#### `log-schema` — MED · transcribe
|
|
682
|
-
|
|
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`.)_
|
|
683
744
|
|
|
684
745
|
#### `metric-name-contract` — MED · point
|
|
685
|
-
|
|
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.)_
|
|
686
748
|
|
|
687
749
|
#### `health-check-contract` — HIGH · point
|
|
688
|
-
|
|
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`.)_
|
|
689
752
|
|
|
690
753
|
#### `graceful-shutdown` — HIGH · point
|
|
691
|
-
|
|
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).)_
|
|
692
756
|
|
|
693
757
|
#### `resource-limit-contract` — HIGH · point
|
|
694
|
-
|
|
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.)_
|
|
695
760
|
|
|
696
761
|
#### `ipc-protocol-contract` — MED · point
|
|
697
|
-
|
|
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).)_
|
|
698
764
|
|
|
699
765
|
#### `cron-timezone-contract` — MED · point
|
|
700
|
-
|
|
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.)_
|
|
701
768
|
|
|
702
769
|
#### `process-lifecycle-contract` — MED · point
|
|
703
|
-
|
|
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`.)_
|
|
704
772
|
|
|
705
773
|
#### `proxy-header-forwarding-contract` — MED · point
|
|
706
|
-
|
|
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.)_
|
|
707
776
|
|
|
708
777
|
#### `service-discovery-contract` — HIGH · point
|
|
709
|
-
|
|
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`.)_
|
|
710
780
|
|
|
711
781
|
---
|
|
712
782
|
|
|
713
783
|
## Default severity, at a glance
|
|
714
784
|
|
|
715
785
|
| Tier | Boundary types (defaults — adjust per repo) |
|
|
716
|
-
|
|
717
|
-
| **CRITICAL** | information-architecture, tenancy, db-schema, db-migration, row-level
|
|
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 |
|
|
718
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 |
|
|
719
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 |
|
|
720
790
|
| **LOW** | hateoas-links, dom-event, theme-mode (folded into design-token), shadcn/ui generated primitives |
|
|
@@ -723,12 +793,4 @@ The name/address resolution by which a client locates a service instance (DNS na
|
|
|
723
793
|
|
|
724
794
|
## Extending the taxonomy
|
|
725
795
|
|
|
726
|
-
Add a row here — and all four modes inherit it. For a **new technology**, add a
|
|
727
|
-
short pack: where its boundaries live + the safe-change checklist. Keep
|
|
728
|
-
instructions **technology-specific, never vague** — "middleware order is
|
|
729
|
-
parsers→custom→routes→error-handler-last" beats "be careful with middleware."
|
|
730
|
-
Only instantiate the types the detected stack/IA actually exhibits; `log()` what
|
|
731
|
-
you skip and why. When a type genuinely spans two categories, give it one
|
|
732
|
-
**canonical home** and a one-line cross-reference in the other (as done above for
|
|
733
|
-
`cors`, `idempotency-key`, `websocket-envelope`, `design-token`,
|
|
734
|
-
`package-public-api`, `row-level-security`).
|
|
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`).
|