@nurix/etna 0.19.65 → 0.19.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/bin/etna.js +10 -1
  2. package/data/harness-index.json +1 -5
  3. package/data/harnesses/code/CLAUDE.md +3 -1
  4. package/data/harnesses/code/agents/maintenance.md +4 -1
  5. package/data/harnesses/code/harness.json +7 -2
  6. package/data/harnesses/code/hooks/check-completion-integrity.mjs +8 -0
  7. package/data/harnesses/code/hooks/compliance.mjs +139 -27
  8. package/data/harnesses/code/hooks/lib/frozen.mjs +3 -0
  9. package/data/harnesses/code/hooks/lib/ledger.mjs +31 -0
  10. package/data/harnesses/code/hooks/lib/ownership.mjs +3 -1
  11. package/data/harnesses/code/hooks/lib/session-log.mjs +75 -0
  12. package/data/harnesses/code/rules/changelog.md +2 -0
  13. package/data/harnesses/code/skills/documentation/SKILL.md +1 -0
  14. package/data/harnesses/code/skills/documentation/references/frontmatter.md +1 -1
  15. package/data/harnesses/code/skills/documentation/scripts/doc-lint.mjs +2 -2
  16. package/data/harnesses/code/skills/documentation/templates/Readme.md +1 -0
  17. package/data/harnesses/code/skills/documentation/templates/behavioral-checklist.md +2 -0
  18. package/data/harnesses/code/skills/documentation/templates/changelog-entry.md +2 -2
  19. package/data/harnesses/code/skills/documentation/templates/feature.md +1 -0
  20. package/data/harnesses/code/skills/documentation/templates/implementation-map.md +42 -0
  21. package/data/harnesses/code/skills/testing/SKILL.md +5 -5
  22. package/data/harnesses/delivery/agents/maintenance.md +4 -1
  23. package/data/harnesses/delivery/harness.json +2 -2
  24. package/data/harnesses/delivery/rules/changelog.md +2 -0
  25. package/data/harnesses/delivery/skills/documentation/SKILL.md +1 -0
  26. package/data/harnesses/delivery/skills/documentation/references/frontmatter.md +1 -1
  27. package/data/harnesses/delivery/skills/documentation/scripts/doc-lint.mjs +2 -2
  28. package/data/harnesses/delivery/skills/documentation/templates/Readme.md +1 -0
  29. package/data/harnesses/delivery/skills/documentation/templates/behavioral-checklist.md +2 -0
  30. package/data/harnesses/delivery/skills/documentation/templates/changelog-entry.md +2 -2
  31. package/data/harnesses/delivery/skills/documentation/templates/feature.md +1 -0
  32. package/data/harnesses/delivery/skills/documentation/templates/implementation-map.md +42 -0
  33. package/data/harnesses/etna/agents/maintenance.md +4 -1
  34. package/data/harnesses/etna/harness.json +2 -2
  35. package/data/harnesses/etna/rules/changelog.md +2 -0
  36. package/data/harnesses/etna/skills/documentation/SKILL.md +1 -0
  37. package/data/harnesses/etna/skills/documentation/references/frontmatter.md +1 -1
  38. package/data/harnesses/etna/skills/documentation/scripts/doc-lint.mjs +2 -2
  39. package/data/harnesses/etna/skills/documentation/templates/Readme.md +1 -0
  40. package/data/harnesses/etna/skills/documentation/templates/behavioral-checklist.md +2 -0
  41. package/data/harnesses/etna/skills/documentation/templates/changelog-entry.md +2 -2
  42. package/data/harnesses/etna/skills/documentation/templates/feature.md +1 -0
  43. package/data/harnesses/etna/skills/documentation/templates/implementation-map.md +42 -0
  44. package/lib/install/materialize.mjs +51 -57
  45. package/package.json +2 -2
  46. package/data/harnesses/apollo/.mcp.json +0 -8
  47. package/data/harnesses/apollo/.prettierignore +0 -29
  48. package/data/harnesses/apollo/.prettierrc.json +0 -3
  49. package/data/harnesses/apollo/.vscode/settings.json +0 -19
  50. package/data/harnesses/apollo/CLAUDE.md +0 -162
  51. package/data/harnesses/apollo/agents/bookkeeper.md +0 -28
  52. package/data/harnesses/apollo/agents/brainstorm.md +0 -144
  53. package/data/harnesses/apollo/agents/change-review.md +0 -251
  54. package/data/harnesses/apollo/agents/lld-mode.md +0 -58
  55. package/data/harnesses/apollo/agents/maintenance.md +0 -233
  56. package/data/harnesses/apollo/agents/performance-review.md +0 -223
  57. package/data/harnesses/apollo/agents/retro.md +0 -46
  58. package/data/harnesses/apollo/codex/agents/bookkeeper.toml +0 -26
  59. package/data/harnesses/apollo/codex/agents/brainstorm.toml +0 -142
  60. package/data/harnesses/apollo/codex/agents/change-review.toml +0 -249
  61. package/data/harnesses/apollo/codex/agents/lld-mode.toml +0 -52
  62. package/data/harnesses/apollo/codex/agents/maintenance.toml +0 -228
  63. package/data/harnesses/apollo/codex/agents/performance-review.toml +0 -222
  64. package/data/harnesses/apollo/codex/agents/retro.toml +0 -45
  65. package/data/harnesses/apollo/harness.json +0 -19
  66. package/data/harnesses/apollo/hooks/approved-packages.md +0 -44
  67. package/data/harnesses/apollo/hooks/awareness.mjs +0 -405
  68. package/data/harnesses/apollo/hooks/check-bookkeeping-clean.mjs +0 -19
  69. package/data/harnesses/apollo/hooks/check-changelog-presence.mjs +0 -57
  70. package/data/harnesses/apollo/hooks/check-completion-integrity.mjs +0 -71
  71. package/data/harnesses/apollo/hooks/check-ownership.mjs +0 -22
  72. package/data/harnesses/apollo/hooks/compliance.mjs +0 -457
  73. package/data/harnesses/apollo/hooks/hooks.json +0 -23
  74. package/data/harnesses/apollo/hooks/lib/frozen.mjs +0 -58
  75. package/data/harnesses/apollo/hooks/lib/ownership.mjs +0 -242
  76. package/data/harnesses/apollo/hooks/lib/state.mjs +0 -170
  77. package/data/harnesses/apollo/rules/changelog.md +0 -25
  78. package/data/harnesses/apollo/rules/database.md +0 -34
  79. package/data/harnesses/apollo/rules/frontend.md +0 -30
  80. package/data/harnesses/apollo/rules/general.md +0 -40
  81. package/data/harnesses/apollo/rules/nomenclature.md +0 -30
  82. package/data/harnesses/apollo/rules/pnpm.md +0 -57
  83. package/data/harnesses/apollo/rules/testing.md +0 -64
  84. package/data/harnesses/apollo/skills/boundary/SKILL.md +0 -75
  85. package/data/harnesses/apollo/skills/boundary/references/index.md +0 -16
  86. package/data/harnesses/apollo/skills/boundary/references/mechanisms.md +0 -99
  87. package/data/harnesses/apollo/skills/boundary/references/packs/async-messaging.md +0 -81
  88. package/data/harnesses/apollo/skills/boundary/references/packs/auth.md +0 -93
  89. package/data/harnesses/apollo/skills/boundary/references/packs/build-config-runtime.md +0 -93
  90. package/data/harnesses/apollo/skills/boundary/references/packs/cloudflare.md +0 -93
  91. package/data/harnesses/apollo/skills/boundary/references/packs/frontend.md +0 -93
  92. package/data/harnesses/apollo/skills/boundary/references/packs/generic-tail.md +0 -55
  93. package/data/harnesses/apollo/skills/boundary/references/packs/persistence.md +0 -93
  94. package/data/harnesses/apollo/skills/boundary/references/packs/wire-api.md +0 -87
  95. package/data/harnesses/apollo/skills/brainstorm/README.md +0 -56
  96. package/data/harnesses/apollo/skills/brainstorm/SKILL.md +0 -173
  97. package/data/harnesses/apollo/skills/brainstorm/references/doctrine.md +0 -98
  98. package/data/harnesses/apollo/skills/brainstorm/references/harvest.md +0 -86
  99. package/data/harnesses/apollo/skills/brainstorm/references/session-schema.md +0 -186
  100. package/data/harnesses/apollo/skills/brainstorm/scripts/detector.mjs +0 -460
  101. package/data/harnesses/apollo/skills/design/SKILL.md +0 -121
  102. package/data/harnesses/apollo/skills/design/components/charts.md +0 -76
  103. package/data/harnesses/apollo/skills/design/components/collections.md +0 -132
  104. package/data/harnesses/apollo/skills/design/components/feedback.md +0 -47
  105. package/data/harnesses/apollo/skills/design/components/hooks.md +0 -35
  106. package/data/harnesses/apollo/skills/design/components/index.md +0 -143
  107. package/data/harnesses/apollo/skills/design/components/inputs.md +0 -327
  108. package/data/harnesses/apollo/skills/design/components/layout.md +0 -52
  109. package/data/harnesses/apollo/skills/design/components/media.md +0 -66
  110. package/data/harnesses/apollo/skills/design/components/motion.md +0 -82
  111. package/data/harnesses/apollo/skills/design/components/navigation.md +0 -29
  112. package/data/harnesses/apollo/skills/design/components/overlays.md +0 -72
  113. package/data/harnesses/apollo/skills/design/components/setup.md +0 -40
  114. package/data/harnesses/apollo/skills/design/components/typography.md +0 -46
  115. package/data/harnesses/apollo/skills/design/components/utils.md +0 -37
  116. package/data/harnesses/apollo/skills/design/data/blueprints/browse.md +0 -38
  117. package/data/harnesses/apollo/skills/design/data/blueprints/creation-flow.md +0 -38
  118. package/data/harnesses/apollo/skills/design/data/blueprints/dashboard.md +0 -38
  119. package/data/harnesses/apollo/skills/design/data/blueprints/grid.md +0 -38
  120. package/data/harnesses/apollo/skills/design/data/blueprints/queue.md +0 -38
  121. package/data/harnesses/apollo/skills/design/data/blueprints/record-detail.md +0 -38
  122. package/data/harnesses/apollo/skills/design/data/blueprints/settings.md +0 -38
  123. package/data/harnesses/apollo/skills/design/data/charts/census.csv +0 -58
  124. package/data/harnesses/apollo/skills/design/data/checklists/browse.md +0 -21
  125. package/data/harnesses/apollo/skills/design/data/checklists/creation-flow.md +0 -20
  126. package/data/harnesses/apollo/skills/design/data/checklists/dashboard.md +0 -21
  127. package/data/harnesses/apollo/skills/design/data/checklists/grid.md +0 -21
  128. package/data/harnesses/apollo/skills/design/data/checklists/queue.md +0 -21
  129. package/data/harnesses/apollo/skills/design/data/checklists/record-detail.md +0 -21
  130. package/data/harnesses/apollo/skills/design/data/checklists/settings.md +0 -21
  131. package/data/harnesses/apollo/skills/design/data/chrome/board-column.md +0 -68
  132. package/data/harnesses/apollo/skills/design/data/chrome/data-table.md +0 -63
  133. package/data/harnesses/apollo/skills/design/data/chrome/side-panel.md +0 -60
  134. package/data/harnesses/apollo/skills/design/data/chrome/sidebar.md +0 -78
  135. package/data/harnesses/apollo/skills/design/data/chrome/stat-cards.md +0 -62
  136. package/data/harnesses/apollo/skills/design/data/chrome/topbar.md +0 -65
  137. package/data/harnesses/apollo/skills/design/data/elevation/ant-design-directional-shadow.md +0 -19
  138. package/data/harnesses/apollo/skills/design/data/elevation/apple-liquid-glass-material.md +0 -18
  139. package/data/harnesses/apollo/skills/design/data/elevation/atlassian-surface-shadow-pairing.md +0 -20
  140. package/data/harnesses/apollo/skills/design/data/elevation/fluent-2-key-ambient-ramp.md +0 -20
  141. package/data/harnesses/apollo/skills/design/data/elevation/github-primer-sparing-shadow.md +0 -19
  142. package/data/harnesses/apollo/skills/design/data/elevation/ibm-carbon-layer-and-border.md +0 -19
  143. package/data/harnesses/apollo/skills/design/data/elevation/linear-dark-surface-ladder.md +0 -17
  144. package/data/harnesses/apollo/skills/design/data/elevation/material-2-key-ambient-dp-ramp.md +0 -19
  145. package/data/harnesses/apollo/skills/design/data/elevation/material-3-tonal-elevation.md +0 -21
  146. package/data/harnesses/apollo/skills/design/data/elevation/neobrutalist-hard-offset.md +0 -19
  147. package/data/harnesses/apollo/skills/design/data/elevation/neumorphic-soft-dual-shadow.md +0 -18
  148. package/data/harnesses/apollo/skills/design/data/elevation/notion-warm-minimal-hairline.md +0 -17
  149. package/data/harnesses/apollo/skills/design/data/elevation/polaris-elevation-inset-bevel.md +0 -21
  150. package/data/harnesses/apollo/skills/design/data/elevation/radix-themes-alpha-ring-scale.md +0 -19
  151. package/data/harnesses/apollo/skills/design/data/elevation/shadcn-neutral-derived-radius.md +0 -19
  152. package/data/harnesses/apollo/skills/design/data/elevation/stripe-brand-tinted-dual-shadow.md +0 -18
  153. package/data/harnesses/apollo/skills/design/data/elevation/tailwind-default-utility-scale.md +0 -19
  154. package/data/harnesses/apollo/skills/design/data/elevation/vercel-geist-shadow-as-border.md +0 -20
  155. package/data/harnesses/apollo/skills/design/data/iconography.md +0 -199
  156. package/data/harnesses/apollo/skills/design/data/motion/accordion-expand.md +0 -18
  157. package/data/harnesses/apollo/skills/design/data/motion/destructive-hold-confirm.md +0 -18
  158. package/data/harnesses/apollo/skills/design/data/motion/drag-dismiss-momentum.md +0 -18
  159. package/data/harnesses/apollo/skills/design/data/motion/empty-state-arrive.md +0 -18
  160. package/data/harnesses/apollo/skills/design/data/motion/hover-lift.md +0 -18
  161. package/data/harnesses/apollo/skills/design/data/motion/modal-center-arrive.md +0 -18
  162. package/data/harnesses/apollo/skills/design/data/motion/number-ticker-roll.md +0 -18
  163. package/data/harnesses/apollo/skills/design/data/motion/onboarding-bounce.md +0 -18
  164. package/data/harnesses/apollo/skills/design/data/motion/panel-slide.md +0 -18
  165. package/data/harnesses/apollo/skills/design/data/motion/press-feedback.md +0 -18
  166. package/data/harnesses/apollo/skills/design/data/motion/progress-linear-loop.md +0 -18
  167. package/data/harnesses/apollo/skills/design/data/motion/route-crossfade.md +0 -18
  168. package/data/harnesses/apollo/skills/design/data/motion/rubber-band-overscroll.md +0 -18
  169. package/data/harnesses/apollo/skills/design/data/motion/skeleton-shimmer.md +0 -18
  170. package/data/harnesses/apollo/skills/design/data/motion/stagger-arrive.md +0 -18
  171. package/data/harnesses/apollo/skills/design/data/motion/toast-enter-stack.md +0 -18
  172. package/data/harnesses/apollo/skills/design/data/motion/tooltip-chain-instant.md +0 -18
  173. package/data/harnesses/apollo/skills/design/data/motion/trigger-anchored-reveal.md +0 -18
  174. package/data/harnesses/apollo/skills/design/data/tells.csv +0 -34
  175. package/data/harnesses/apollo/skills/design/data/voices/bright-collaborator.md +0 -29
  176. package/data/harnesses/apollo/skills/design/data/voices/direct-operator.md +0 -29
  177. package/data/harnesses/apollo/skills/design/data/voices/earnest-helper.md +0 -29
  178. package/data/harnesses/apollo/skills/design/data/voices/editorial-composed.md +0 -29
  179. package/data/harnesses/apollo/skills/design/data/voices/quiet-concierge.md +0 -29
  180. package/data/harnesses/apollo/skills/design/data/voices/warm-guide.md +0 -29
  181. package/data/harnesses/apollo/skills/design/references/compose.md +0 -23
  182. package/data/harnesses/apollo/skills/design/references/content.md +0 -28
  183. package/data/harnesses/apollo/skills/design/references/emission.md +0 -21
  184. package/data/harnesses/apollo/skills/design/references/existing-apps.md +0 -27
  185. package/data/harnesses/apollo/skills/design/references/foundations.md +0 -57
  186. package/data/harnesses/apollo/skills/design/references/intake.md +0 -40
  187. package/data/harnesses/apollo/skills/design/references/object-pass.md +0 -24
  188. package/data/harnesses/apollo/skills/design/references/ship-gate.md +0 -29
  189. package/data/harnesses/apollo/skills/design/references/ux-patterns/Readme.md +0 -195
  190. package/data/harnesses/apollo/skills/design/references/ux-patterns/ai-layer.md +0 -49
  191. package/data/harnesses/apollo/skills/design/references/ux-patterns/attributes-lifecycle.md +0 -43
  192. package/data/harnesses/apollo/skills/design/references/ux-patterns/container-choice.md +0 -44
  193. package/data/harnesses/apollo/skills/design/references/ux-patterns/creation-paths.md +0 -46
  194. package/data/harnesses/apollo/skills/design/references/ux-patterns/dashboards.md +0 -42
  195. package/data/harnesses/apollo/skills/design/references/ux-patterns/modes.md +0 -42
  196. package/data/harnesses/apollo/skills/design/references/ux-patterns/object-model.md +0 -46
  197. package/data/harnesses/apollo/skills/design/references/ux-patterns/page-archetype.md +0 -48
  198. package/data/harnesses/apollo/skills/design/references/ux-patterns/promotion-demotion.md +0 -37
  199. package/data/harnesses/apollo/skills/design/references/ux-patterns/settings.md +0 -42
  200. package/data/harnesses/apollo/skills/design/references/ux-patterns/standard-pages.md +0 -46
  201. package/data/harnesses/apollo/skills/design/references/ux-patterns/state-matrix.md +0 -41
  202. package/data/harnesses/apollo/skills/design/references/ux-patterns/table-mechanics.md +0 -48
  203. package/data/harnesses/apollo/skills/design/references/ux-patterns/verbs.md +0 -45
  204. package/data/harnesses/apollo/skills/design/references/ux-patterns/viewer-context.md +0 -40
  205. package/data/harnesses/apollo/skills/design/scripts/README-validator.md +0 -27
  206. package/data/harnesses/apollo/skills/design/scripts/select-chart.mjs +0 -622
  207. package/data/harnesses/apollo/skills/design/scripts/validate-design-md.mjs +0 -1198
  208. package/data/harnesses/apollo/skills/documentation/SKILL.md +0 -134
  209. package/data/harnesses/apollo/skills/documentation/references/examples.md +0 -279
  210. package/data/harnesses/apollo/skills/documentation/references/frontmatter.md +0 -44
  211. package/data/harnesses/apollo/skills/documentation/references/instruction-style.md +0 -50
  212. package/data/harnesses/apollo/skills/documentation/references/style-guide.md +0 -384
  213. package/data/harnesses/apollo/skills/documentation/references/untemplated.md +0 -38
  214. package/data/harnesses/apollo/skills/documentation/scripts/doc-lint.mjs +0 -140
  215. package/data/harnesses/apollo/skills/documentation/templates/Readme.md +0 -34
  216. package/data/harnesses/apollo/skills/documentation/templates/api-reference.md +0 -20
  217. package/data/harnesses/apollo/skills/documentation/templates/architecture-decision.md +0 -121
  218. package/data/harnesses/apollo/skills/documentation/templates/backlog.md +0 -33
  219. package/data/harnesses/apollo/skills/documentation/templates/behavioral-checklist.md +0 -79
  220. package/data/harnesses/apollo/skills/documentation/templates/bookkeeping.md +0 -18
  221. package/data/harnesses/apollo/skills/documentation/templates/changelog-entry.md +0 -57
  222. package/data/harnesses/apollo/skills/documentation/templates/changelog-index.md +0 -41
  223. package/data/harnesses/apollo/skills/documentation/templates/docs-readme.md +0 -72
  224. package/data/harnesses/apollo/skills/documentation/templates/domain-model.md +0 -30
  225. package/data/harnesses/apollo/skills/documentation/templates/environment-registry.md +0 -15
  226. package/data/harnesses/apollo/skills/documentation/templates/feature.md +0 -149
  227. package/data/harnesses/apollo/skills/documentation/templates/glossary.md +0 -11
  228. package/data/harnesses/apollo/skills/documentation/templates/journal.md +0 -13
  229. package/data/harnesses/apollo/skills/documentation/templates/memory-main.md +0 -25
  230. package/data/harnesses/apollo/skills/documentation/templates/migration-runbook.md +0 -34
  231. package/data/harnesses/apollo/skills/documentation/templates/non-goals.md +0 -11
  232. package/data/harnesses/apollo/skills/documentation/templates/operations-procedure.md +0 -36
  233. package/data/harnesses/apollo/skills/documentation/templates/plan.md +0 -88
  234. package/data/harnesses/apollo/skills/documentation/templates/protected-files.md +0 -37
  235. package/data/harnesses/apollo/skills/documentation/templates/root-readme.md +0 -104
  236. package/data/harnesses/apollo/skills/documentation/templates/security-overview.md +0 -27
  237. package/data/harnesses/apollo/skills/documentation/templates/structure.md +0 -65
  238. package/data/harnesses/apollo/skills/documentation/templates/system-overview.md +0 -32
  239. package/data/harnesses/apollo/skills/documentation/templates/testing-strategy.md +0 -29
  240. package/data/harnesses/apollo/skills/schema-design/SKILL.md +0 -141
  241. package/data/harnesses/apollo/skills/test/SKILL.md +0 -104
  242. package/data/harnesses/apollo/skills/testing/SKILL.md +0 -166
  243. package/data/harnesses/apollo/skills/workflow-mapper/SKILL.md +0 -451
  244. package/data/harnesses/apollo/skills/workflow-mapper/references/patterns.md +0 -214
  245. /package/data/harnesses/{apollo → code}/docs/README.md +0 -0
  246. /package/data/harnesses/{apollo → code}/docs/history/CHANGELOG.md +0 -0
  247. /package/data/harnesses/{apollo → code}/docs/history/backlog.md +0 -0
  248. /package/data/harnesses/{apollo → code}/docs/memory/main.md +0 -0
  249. /package/data/harnesses/{apollo → code}/docs/structure.md +0 -0
  250. /package/data/harnesses/{apollo → code}/rules/cloudflare.md +0 -0
@@ -1,64 +0,0 @@
1
- # Testing & Verification
2
-
3
- **When** work is proven and **which technology** proves it — the standing constraints that bind on every turn. The full lifecycle procedure — deriving a plan's Test Obligations, placing tests in strata, executing the verification gate — is the `testing` skill, invoked at those moments; this rule never restates it.
4
-
5
- ## Two regimes
6
-
7
- **Deferral is a property of plans, not of code.** A plan carries a Test Obligations ledger and a gate that discharges it; nothing else does, so nothing else may defer.
8
-
9
- | Regime | During the work | Proven |
10
- | --- | --- | --- |
11
- | **Plan-driven build** | No new test files. Code only — the loop stays fast | At the verification gate, before `Status: Complete` |
12
- | **Bug fix** — a defect in shipped behavior | The regression test ships with the fix | Immediately — it fails before the change, passes after |
13
- | **Ad-hoc / non-plan change** | The test ships with the change **when it alters promised behavior** — layout, copy, and config tweaks owe none | Immediately, same discipline as a bug fix |
14
-
15
- - **A bug fix cannot defer** — "fails before, passes after" is provable afterward only by reverting to the pre-fix ref, a cost nobody pays; capture the property at fix time or lose it.
16
- - **Ad-hoc work cannot defer — there is no ledger to defer into.** A non-plan change that alters no promised behavior — a pure refactor, a rename — owes no test.
17
-
18
- ## During the loop
19
-
20
- Rules for the plan regime between the plan gate and the verification gate:
21
-
22
- - **No new test files** — obligations wait in the plan's ledger (written at the plan gate by the `testing` skill).
23
- - **Existing tests broken by the plan's intended behavior change are updated to the new contract before any mid-plan commit** — the shared branch never carries a red suite, and an update is never a weakening (§ Discipline).
24
- - **A defect in the plan's own in-flux code is the loop iterating** — no test, no ledger row. **A pre-existing bug in shipped behavior is the bug-fix regime even mid-plan**: fix it with its regression test now — deferring it to the gate loses "fails before" — or send it to the backlog via Out-of-Scope Discovery. Never fix silently.
25
- - **The ledger is amendable while the plan is open** — the gate discharges whatever it holds at gate time.
26
- - **A mid-plan commit is a checkpoint, not a verification claim.**
27
-
28
- ## The gate
29
-
30
- **A discrete, user-triggered step.** When implementation is complete, report it and offer the gate — never run it unbidden, and never flip `Status: Complete` without it. An explicit user acceptance of finished plan work — "ship it", "commit", "looks good" — counts as the trigger; open the gate without asking again. A plan with outstanding obligations stays open, whatever the code does. The procedure — checklist, drive-then-pin ladder, row discharge, what earns a durable test — is the `testing` skill (gate mode).
31
-
32
- - **Precondition: the ledger exists.** A plan with no Test Obligations section is blocked at the gate — run the `testing` skill (plan-gate mode) on it first; an empty ledger passes vacuously and the gap surfaces only after the file is frozen.
33
- - **The verdict is never delegated** (the root policy file § Delegation), and where browser work is main-session-only (its § Runtime Notes), the driving is too. Fan-out may prepare fixtures or draft test bodies; the driving and the verdict stay inline.
34
- - **The gate needs no stronger tier than the session default** — a standard-tier session suffices whatever tier built the plan; the plan and its ledger carry all the state the gate needs, so gating in a fresh session loses nothing.
35
- - **Report what happened, including failures.** A gate that reports green while a suite is red is worse than no gate; state the failing test and its output.
36
- - **Abandonment does not erase debt.** Before `Status: Abandoned` freezes a plan whose code was committed, convert its undischarged rows into backlog items; uncommitted code owes nothing.
37
-
38
- ## Frameworks by stack
39
-
40
- Framework choice is **never** ad hoc and never the user's problem mid-task: detect the stack, use its row; if the repo already has a runner, that runner wins — **never** introduce a second framework beside it.
41
-
42
- | Stack | Unit / integration | Component | E2E | HTTP mocking | Don't use |
43
- | --- | --- | --- | --- | --- | --- |
44
- | Node + Vite / React (web) | `vitest` | `@testing-library/react` (+ `@testing-library/user-event`, `happy-dom`) | `@playwright/test` | `msw` | `jest`, `mocha`, `jasmine`, `cypress`, `enzyme` |
45
- | Express APIs | `vitest` | — | `supertest` against the app instance | `msw` (outbound calls) | `jest`, `chai-http` |
46
- | React Native / Expo | `jest` via the `jest-expo` preset | `@testing-library/react-native` | `maestro` | `msw` | `detox`, `enzyme` |
47
- | Cloudflare Workers | `vitest` + `@cloudflare/vitest-pool-workers` | — | `@playwright/test` against `wrangler dev` | `msw` | `jest` |
48
- | Rust | `cargo test` (built-in) | — | — | — | — |
49
-
50
- Test packages install at `latest` — outside the approved-packages pin set. Fake data comes from `@ngneat/falso` — **never** `@faker-js/faker` / `chance`.
51
-
52
- ## Conventions
53
-
54
- - **Suite, not scripts.** Tests live in the canonical test tree — `tests/` mirroring `src/`, or the convention the repo already uses — named `<subject>.test.ts`; shared fixtures, factories, and helpers in `tests/helpers/` (or the suite's existing shared home).
55
- - **Wire the canonical command.** `"test"` in `package.json` runs the whole unit suite (`vitest run`); e2e under `"test:e2e"`. A repo where `pnpm test` / `npm test` doesn't run the suite is misconfigured — fix the wiring, **never** route around it.
56
- - **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob.
57
- - **No test doubles for what you own.** Prefer real instances — in-memory SQLite for drizzle, the actual Express app via `supertest` — over mocks; mock **only** true externals (network via `msw`, time via the runner's fake timers).
58
- - **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone and in any order.
59
-
60
- ## Discipline
61
-
62
- - **Never weaken, skip, or delete a test to mask an unexplained failure.** When a plan or LLD intentionally changes behavior, updating the affected tests to the new contract is part of that change.
63
- - **A behavior that is compiler-guarded owes no test** — if it cannot ship green-but-broken, a test adds cost and no safety.
64
- - **Throwaway checks are not tests.** Temporary scripts written to drive a change live in scratch space and are deleted — never committed into the test tree.
@@ -1,75 +0,0 @@
1
- ---
2
- name: boundary
3
- description: "Use when a plan or diff might break a cross-cutting contract. Boundary diagnostic: reads the source a plan touches — db schema, wire envelope, auth, events — and reports violations with a pass/block verdict. Read-only; also runs on a diff. Triggers on 'boundary check', 'what could break'."
4
- ---
5
-
6
- # Boundary Diagnostic
7
-
8
- You diagnose whether a plan's changes would break an application boundary.
9
-
10
- A boundary is a contract between two pieces of code (or code and infra) that share an assumption with **no static link** between them: the column a raw SQL string names, the JSON envelope a client unwraps, the event string a dispatcher parses, the env var a deploy script writes. Changing one side ships green — compiles, lints, tests pass — and breaks the other at runtime. Your job is to find those edges in the plan's path _before_ execution.
11
-
12
- **The litmus is "would it ship green?"** An edge the compiler or an existing test already guards is not a boundary — a change that fails the build loudly needs no diagnostic. Spend your reading on the edges with no static link.
13
-
14
- ## Contract sources
15
-
16
- Three layers tell you what to look for; the code tells you what exists.
17
-
18
- - **The mechanisms** — [`references/mechanisms.md`](references/mechanisms.md) defines the seven failure mechanisms every boundary instantiates (rendezvous-string, serialized-shape, generated-artifact, dual-authority, config-elsewhere, trust-invariant, lifecycle-protocol), each with its trace strategy, universal safe-change pattern, and severity heuristic, plus the two stack-independent domain contracts (entity-model, tenancy). Load it on every check.
19
- - **The technology packs** — [`references/index.md`](references/index.md) routes to the packs under `references/packs/` by the signals in the touched code. Each pack carries the non-derivable payload for one technology area: exact detect patterns, expensive-to-learn gotchas, and safe-change sequences. Load **only the packs the touch list exhibits**; a technology no pack covers is still in scope — diagnose it from its mechanism.
20
- - **Repo doctrine** — `.claude/rules/`, `docs/architecture/` (including `decisions/`), and any contracts package name the repo's bespoke contracts (a documented response envelope, a schema-parity rule, an entity model). A documented contract is a real side of a boundary: a plan that contradicts one is a finding **even when no code consumer exists yet** — this is how greenfield plans, whose modules have no code to trace, still get checked.
21
-
22
- The packs are a floor, not a fence: report any contract that passes the ships-green litmus, pack entry or not.
23
-
24
- ## Run
25
-
26
- **Input:** a plan path (given, or the newest in `docs/history/plans/`) — or a diff / named code area for non-plan work. Everything below is identical either way; a diff's hunks stand in for the plan's steps.
27
-
28
- 1. **Build the touch list.** Parse the plan's Proposed Changes: files created or modified, entities/state added or changed, API changes, anything the author flags as a cross-module contract. Note missing subsections.
29
-
30
- 2. **Identify the contracts.** Read each touched file (and its immediate imports). Against the mechanisms, the routed packs, and the repo doctrine, list the contracts this code participates in — the schema it queries, the envelope it emits, the events it publishes or parses, the auth checks it sits behind, the env it reads.
31
-
32
- 3. **Trace the other side.** A boundary has two sides; the danger lives on the one the plan _doesn't_ mention. For each contract, grep the shared name or shape across the repo — the column name in other SQL strings, the response field in client unwrappers, the event kind in subscribers, the route in fetch calls and scripts, the env key in deploy tooling. Record consumers as `file:line`; record doctrine as the doc path. When the touch list is large (≳10 contracts), fan the tracing out to parallel sub-agents — one per pack area, each with a self-contained prompt naming its contracts and grep targets — and judge the returned evidence yourself; a small diff traces inline.
33
-
34
- 4. **Judge each intersection.** Does the planned change alter a shape a traced consumer depends on, break an invariant the code or doctrine establishes (append-only vocabularies, idempotency keys, fail-closed checks, ordering guarantees), or introduce a new cross-module edge? Severity:
35
- - **CRITICAL** — can corrupt persisted data, or open an auth/money path.
36
- - **HIGH** — silent cross-module breakage that would ship green.
37
- - **MEDIUM** — drift-prone coupling (string-matched, duplicated constants).
38
- - **LOW** — smell. Loud, immediately-visible runtime breakage caps at HIGH.
39
-
40
- 5. **Report in chat:**
41
-
42
- ```
43
- # Boundary check — {plan}
44
-
45
- **Verdict: PASS | PASS WITH AMENDMENTS | BLOCK**
46
-
47
- Touched files read: {N} · Contracts identified: {N} · Consumers traced: {N}
48
-
49
- ## Violations
50
- | # | Boundary | Mechanism | Planned change | Other side (file:line or doc) | Severity |
51
-
52
- ## New edges the plan introduces
53
- | # | Contract | Sides | Hardening to fold into the plan |
54
-
55
- ## Coverage
56
- | Area touched | Contracts checked | Result |
57
-
58
- ## Suggested plan amendments
59
- ## Missing plan subsections
60
- ## Docs the plan's ledger missed
61
- | Feature doc | Owned path in the touch list | In Doc Obligations? |
62
- ```
63
-
64
- **The doc cross-check is free and you are the only one who can make it.** You already hold the touch list; resolving those paths against the ownership index and comparing the result to the plan's **Doc Obligations** rows costs one lookup. The plan's author wrote that ledger, so it is the one gate artifact nobody independently checked — a `None` that contradicts your touch list is the failure this section exists to catch. Report the gap; never edit the ledger yourself.
65
-
66
- **BLOCK** = any CRITICAL. HIGH findings yield **PASS WITH AMENDMENTS** — fold the amendments into the plan before coding. Order amendments sequencing-first: many violations dissolve by reordering steps (expand-then-contract, migrate-before-code) rather than changing what ships — prefer that fix, drawn from the entry's safe-change sequence.
67
-
68
- 6. **Record the verdict in the plan's `Gates:` line.** Rewrite only your own segment, to exactly `Boundary: passed YYYY-MM-DD` (PASS, or PASS WITH AMENDMENTS once the amendments are folded) or `Boundary: blocked YYYY-MM-DD` (BLOCK). The compliance hook greps these exact forms — implementation edits stay blocked while the segment reads `pending`. A plan with no `Gates:` line predates the format; add the line rather than skipping the write-back.
69
-
70
- ## Rules
71
-
72
- - **Read-only, one exception.** You never edit the plan's content, the code, or any doc — the user folds your amendments into the plan. The single write you own is the plan's `Gates:` line (step 6), your segment only.
73
- - **Verify in source.** Never flag from plan text alone — read both sides of every finding and cite them: code as `file:line`, doctrine as the doc path.
74
- - **No other side, no violation.** An intersection with no traced consumer and no doctrine behind it isn't a violation — it's a **new edge**: report it in its own section with the hardening its mechanism prescribes (version the cache key now, make the enum append-only now), so the fragile edge is born safe.
75
- - **A clean report is valid.** Zero violations means say so — the coverage table is the evidence it was checked, not skipped. Don't pad findings.
@@ -1,16 +0,0 @@
1
- # Pack routing index
2
-
3
- Load when: every boundary check, right after `mechanisms.md`. Match the touch list against the signals below and load **only** the packs whose signals appear in the touched files or the plan's Proposed Changes. Several packs matching is normal (a full-stack feature typically routes 2–4). A technology matching nothing here is still in scope — diagnose it from its mechanism, and check `packs/generic-tail.md` for a thin entry.
4
-
5
- | Pack | Load when the touch list shows |
6
- | --- | --- |
7
- | `packs/persistence.md` | `schema.ts`, `init.sql`, `sqliteTable(`, `drizzle-kit`, `pgTable`, migrations, `*.seed.sql`, seed generators, JSON columns, `pgEnum`, pgvector, Redis `get`/`set` caching |
8
- | `packs/wire-api.md` | Express routes/middleware (`app.use(`, `router.`), `res.json({ status`, zod schemas on requests, axios clients/interceptors, `cors(`, inbound webhooks, `rateLimit(` |
9
- | `packs/frontend.md` | `createSlice`/`configureStore`, RTK Query (`tagTypes`), react-router tables/`useParams`, Tailwind CSS (`@theme`, `personality.css`, `globals.css`), `vite.config.*` (alias, env), `@nurix/components`, localforage/redux-persist, date formatting |
10
- | `packs/auth.md` | `jwtVerify(`/jose, `betterAuth(`/`auth.api.`, session cookies, CSRF, OAuth callbacks/`redirect_uri`, `tenantId`/`organizationId` scoping, password hashing, auth middleware mounts |
11
- | `packs/cloudflare.md` | `wrangler.jsonc`/`wrangler.toml`, `env.X` bindings, `extends DurableObject`/`[[migrations]]`, KV/R2/D1/Queues, `scheduled(`/crons, `WorkerEntrypoint`, `compatibility_date`, static assets config |
12
- | `packs/async-messaging.md` | BullMQ (`new Queue(`/`new Worker(`), Redis pub/sub or Streams, WebSockets/socket.io, SSE (`text/event-stream`), channel-name templates, outbound webhook senders |
13
- | `packs/build-config-runtime.md` | `package.json` (deps/`exports`/scripts/`packageManager`), lockfiles, `.env*`, tsconfig `paths`, Dockerfiles/compose, `.github/workflows/`, pino config, `trust proxy`, ports/healthchecks |
14
- | `packs/generic-tail.md` | GraphQL, gRPC/protobuf, tRPC, OpenAPI codegen, Kafka/schema registry/CDC, workflow engines, event sourcing, Postgres RLS/pooling/collation, SSR/hydration, module federation, i18n, analytics plans, hosted feature flags, K8s probes/shutdown/limits, service mesh, IaC outputs, SAML, Lambda, IPC |
15
-
16
- Two files never route — they always load: `mechanisms.md` (the seven failure mechanisms + domain contracts) and this index.
@@ -1,99 +0,0 @@
1
- # Boundary Mechanisms
2
-
3
- Load when: every boundary check — this is the core reference. The technology packs under `packs/` instantiate these mechanisms with exact detect signals and gotchas; `index.md` routes to the packs the touched code exhibits.
4
-
5
- A boundary is a contract between two pieces of code (or code and infra) that share an assumption with **no static link** between them. The litmus is **"would it ship green?"** — change one side, everything compiles, lints, and tests pass, and the other side breaks at runtime. An edge the compiler or an existing test already guards is not a boundary.
6
-
7
- Every boundary instantiates one of seven mechanisms. The mechanism tells you what to grep for, what the universal fix is, and how bad a break is — including for technologies no pack covers. When touched code exhibits a contract that matches no pack entry, diagnose it from its mechanism.
8
-
9
- ---
10
-
11
- ## 1 · rendezvous-string
12
-
13
- Two sides meet on a literal identifier: a column name in raw SQL, an event/job/topic name, a route path, an env key, a cache key, a channel template, a DI token, a flag key, a metric name, a port number, a binding name. The compiler sees two unrelated literals.
14
-
15
- - **Ask:** where else does this exact string (or its template) appear — including config, IaC, CI, dashboards, and other repos?
16
- - **Failure:** the sides silently stop meeting — publishes into the void, reads `undefined`, routes 404, handler never fires, alert goes quiet.
17
- - **Trace:** grep the literal and its prefix/template variants repo-wide; the other side is often not code (a deploy manifest, a saved dashboard query, a provider allowlist).
18
- - **Universal fix:** centralize the identifier in one shared constant or module. On rename, serve both old and new until the old side drains — in-flight jobs, cached entries, sent emails, deployed clients.
19
- - **Severity:** HIGH by default; CRITICAL when the string scopes trust (a tenant segment in a channel name, a redirect-URI allowlist entry).
20
-
21
- ## 2 · serialized-shape
22
-
23
- A payload crosses a gap in space or time — a process boundary, a deploy window, persistence — and the reader is not the writer. Old data and mixed-version peers must still parse. The defining sub-case is the **mixed-version window**: rolling deploys, in-flight queue items, long-lived clients, persisted events, browser storage — the old shape exists somewhere and outlives the change.
24
-
25
- - **Ask:** who reads this shape that doesn't build from this commit — old rows, queued messages, deployed clients, replayed histories?
26
- - **Failure:** the reader crashes or misreads with no error at the writer; persisted old shapes fail **forever**, not just during the deploy.
27
- - **Trace:** find every deserializer of the shape (parsers, `JSON.parse` + property reads, ORM rehydration, client unwrappers) and every store where old instances persist.
28
- - **Universal fix:** additive-only changes (add optional fields, never rename/retype/remove); carry an explicit version and read all live versions; readers tolerate unknown fields and ignore unknown discriminators.
29
- - **Severity:** CRITICAL when the shape is persisted or replayed (the break is historical, not just live); HIGH for live-traffic shapes.
30
-
31
- ## 3 · generated-artifact
32
-
33
- A derived artifact must stay in sync with its source: ORM client from schema, types from codegen, lockfile from package.json, bundler aliases from tsconfig, seed SQL from a CSV. The artifact is trusted as if it were the source — when they drift, the artifact **lies** to everything that consumes it.
34
-
35
- - **Ask:** what generates this, what does it generate, and would CI notice if they drifted?
36
- - **Failure:** code compiles against stale types and mismatches at runtime; installs resolve a tree nobody tested; hand-edits are clobbered on the next generation.
37
- - **Trace:** find the generator invocation (scripts, `prepack`, CI steps) and every consumer of the output; check whether generation is enforced or manual.
38
- - **Universal fix:** regenerate in the same commit as the source change; CI-gate drift (`git diff --exit-code` after generate); never hand-edit generated output.
39
- - **Severity:** HIGH by default; CRITICAL when the artifact governs installs or schema (lockfile, migrations).
40
-
41
- ## 4 · dual-authority
42
-
43
- Two independent writers own the same state: two migration tools targeting one database, a framework that owns its own tables, an app route table and an ingress rule, hand-written DDL and an ORM mirror. Each authority is internally consistent; the state they share is not.
44
-
45
- - **Ask:** who else writes this — and does either writer know the other exists?
46
- - **Failure:** one authority clobbers or drifts from the other; the loser's consumers break with no signal on the winner's side (a schema push drops the auth library's tables; login breaks).
47
- - **Trace:** list every tool/config with write authority over the state (migration runners, CLIs, IaC, admin dashboards) and what each believes the state is.
48
- - **Universal fix:** designate one canonical authority and make the others read-only mirrors reconciled from it — or byte-align them and CI-check the parity.
49
- - **Severity:** CRITICAL when the shared state is persisted data or auth; HIGH otherwise.
50
-
51
- ## 5 · config-elsewhere
52
-
53
- A value is declared in one place and consumed in another — env vars, platform bindings, secret paths, ports, build-time defines. Reader and provider never reference each other statically, and the provider is often per-environment, so the break can exist in exactly one environment.
54
-
55
- - **Ask:** for each value this code reads, where is it declared — in **every** environment, not just the one you can see?
56
- - **Failure:** `undefined` at request time, boot failure in one env only, a stale build-time literal where a runtime value was expected, a secret baked into a public artifact.
57
- - **Trace:** map each read (`process.env.X`, `env.BINDING`, `import.meta.env.Y`) to its declaration in every env file, deploy manifest, CI secret store, and dashboard.
58
- - **Universal fix:** validate the full key set at boot and fail fast; keep a tracked example/contract file (`.env.example`); change reader and every provider in one commit.
59
- - **Severity:** HIGH by default; CRITICAL when the flow direction is wrong (a secret reaching a client bundle) — that is also a trust-invariant break.
60
-
61
- ## 6 · trust-invariant
62
-
63
- An auth or isolation guarantee that **fails open**: tenancy scoping, RLS policies, cookie flags, CSRF defenses, redirect-URI allowlists, JWT audience checks, public-env prefixes. Unlike the other mechanisms, breaking it doesn't stop the system — everything keeps working, plus an attacker path or a cross-tenant leak.
64
-
65
- - **Ask:** if this check were deleted, would anything visibly fail — or would the system happily serve data it shouldn't?
66
- - **Failure:** silent grant or disclosure. There is no error to observe; tests that only exercise the happy path stay green by construction.
67
- - **Trace:** find every enforcement point of the invariant and every path that bypasses it (new endpoints, new tables, raw queries, `SECURITY DEFINER`-style escape hatches, spoofable inputs like `req.body.tenantId`).
68
- - **Universal fix:** enforce centrally (middleware, policy, schema default) so a new code path inherits the invariant instead of having to remember it; default-deny on any missing input.
69
- - **Severity:** CRITICAL, categorically. Loud auth failures (everyone locked out) cap at HIGH; it's the quiet successes that are CRITICAL.
70
-
71
- ## 7 · lifecycle-protocol
72
-
73
- An ordering or sequencing agreement with a platform or across deploys: expand-then-contract migrations, deploy order between producer and consumer, graceful-shutdown draining, replay determinism, retry/idempotency budgets, timezone/DST assumptions, append-only version tags.
74
-
75
- - **Ask:** does correctness depend on *when* or *in what order* this runs — and who else assumes that order?
76
- - **Failure:** correct code applied in the wrong order: a migration that strands a rolling deploy, a retry that double-charges, a rename that orphans live stateful objects, a cron that fires on the wrong day twice a year.
77
- - **Trace:** reconstruct the sequence the change requires (what must exist before what) and compare against how deploys actually roll out — including crash/retry paths.
78
- - **Universal fix:** make each step independently safe (idempotent handlers, additive-first migrations, append-only version markers); many boundary violations dissolve by **reordering the plan**, not changing what ships.
79
- - **Severity:** CRITICAL when a wrong order corrupts persisted state or duplicates money-adjacent side effects; HIGH otherwise.
80
-
81
- ---
82
-
83
- ## Domain contracts — stack-independent, always checked
84
-
85
- Two contracts come from the product's information architecture, not from any technology, and are checked on every run:
86
-
87
- - **entity-model — CRITICAL.** The entity vocabulary and topology (what the entities are, how they relate) that every module, migration, wire type, and doc agrees on. Re-relating or renaming an entity is a change to *every* mechanism at once. Sourced from `docs/architecture/` (domain model, ADRs), a contracts package, and the schema — never from a single module's view of it. Safe change: map every consumer before re-relating; coordinate schema + wire + client in one planned sequence.
88
- - **tenancy — CRITICAL · trust-invariant.** The invariant that every request carries a non-spoofable tenant identity and all data access is scoped by it, at every layer. Its enforcement points are technology-specific (a JWT claim, a channel-name segment, an RLS policy, a scoped query helper) but the invariant is one contract — a single unscoped path anywhere breaks it.
89
-
90
- ---
91
-
92
- ## Severity recap
93
-
94
- - **CRITICAL** — can corrupt persisted data, or open an auth/money path. Trust-invariants and persisted/replayed shapes live here.
95
- - **HIGH** — silent cross-module breakage that ships green.
96
- - **MEDIUM** — drift-prone coupling (string-matched, duplicated constants) not yet broken.
97
- - **LOW** — smell. Loud, immediately-visible runtime breakage caps at HIGH; statically-guarded edges are out of scope entirely.
98
-
99
- Severity is intrinsic to the finding, not curved: three genuine CRITICALs in a small repo is correct signal, not inflation.
@@ -1,81 +0,0 @@
1
- # Pack: async jobs, messaging, realtime
2
-
3
- Load when: the touch list exhibits BullMQ (`new Queue(`, `new Worker(`), Redis pub/sub or Streams (`publish(`, `xReadGroup(`), WebSockets / socket.io (`ws.send(`, `socket.emit(`), SSE (`text/event-stream`), realtime channel templates, or outbound webhook senders.
4
-
5
- ## bullmq-job-payload-shape — HIGH · serialized-shape
6
- **Contract:** BullMQ producer's `queue.add(name, data)` payload and the worker's `job.data` reads agree on a JSON shape with zero schema enforcement — and Redis holds in-flight jobs serialized in the OLD shape across any rolling deploy.
7
- **Detect:** `queue.add(`, `job.data`, `new Queue(`, `new Worker(`, `**/queues/**`, `**/workers/**`, `flow.add(`
8
- **Ships green, breaks:** Deploy new worker first → it dequeues jobs already sitting in `wait`/`delayed`/`prioritized` (plus every retry of an old `failed` job, which replays the ORIGINAL payload, possibly days later) with the old shape → `undefined` field reads, or worse: no crash, wrong values written. Deploy producer first → old workers process new-shape jobs. TypeScript can't help; the queue boundary erases types.
9
- **Safe change:** Add fields optional-first, worker tolerates both shapes; deploy worker before producer; only remove old-shape handling after the queue AND the `failed` set drain (retries replay old payloads); consider a `v` field in `data` and branch on it.
10
-
11
- ## bullmq-job-name-routing — HIGH · rendezvous-string
12
- **Contract:** The first argument of `queue.add('email-welcome', …)` and the worker's `switch (job.name)` meet on a bare string literal; BullMQ workers receive EVERY job on the queue regardless of name — there is no named-processor registration (unlike Bull v3's `process('name', fn)`).
13
- **Detect:** `queue.add('`, `job.name`, `switch (job.name)`, `case '`
14
- **Ships green, breaks:** Rename either side and the compiler sees two unrelated literals. Worst variant: a `switch` without a throwing `default` — the processor falls through, returns `undefined`, and BullMQ marks the job **completed successfully**. No error, no failed job, no retry; the work silently never happens.
15
- **Safe change:** Centralize job names in one shared const/enum imported by both sides; make the worker's `default` branch throw `UnrecoverableError` so unknown names surface in `failed`; on rename, worker accepts old+new names during the deploy window.
16
-
17
- ## bullmq-repeatable-scheduler-identity — HIGH · rendezvous-string
18
- **Contract:** A recurring job's identity is a string: legacy repeatables (`{ repeat: {…} }`, deprecated since BullMQ 5.16.0) derive it by **hashing the repeat options** (name, cron `pattern`, `every`, `tz`), while Job Schedulers use the explicit `schedulerId` passed to `upsertJobScheduler(schedulerId, repeatOpts, template)`.
19
- **Detect:** `upsertJobScheduler(`, `removeJobScheduler(`, `repeat:`, `pattern:`, `every:`, `removeRepeatable(`, `removeRepeatableByKey(`, `getRepeatableJobs(`, `getJobSchedulers(`
20
- **Ships green, breaks:** Legacy API: change the cron string (or tz, or job name) → the hash changes → a brand-new repeatable is registered while the OLD one keeps firing forever; `removeRepeatable(name, opts)` only works with the EXACT original options (else list with `getRepeatableJobs()` and use `removeRepeatableByKey`). Job Schedulers: re-upserting the SAME `schedulerId` updates in place — but changing the `schedulerId` string recreates the legacy problem. Registration is restart-time-only, so schedulers removed from code persist in Redis indefinitely.
21
- **Safe change:** Use `upsertJobScheduler` with a stable, never-derived `schedulerId`; change cadence by re-upserting the same id, never by renaming; on any rename, explicitly `removeJobScheduler(oldId)`; audit live state with `getJobSchedulers()` + `getRepeatableJobs()` after deploy.
22
-
23
- ## bullmq-lock-stall-redelivery — CRITICAL · lifecycle-protocol
24
- **Contract:** The worker promises to finish (or keep renewing the lock on) a job within `lockDuration` (default 30000 ms, auto-renewed at `lockDuration/2` — renewal runs on the event loop); the queue promises not to hand the job to anyone else until then.
25
- **Detect:** `lockDuration`, `lockRenewTime`, `stalledInterval`, `maxStalledCount`, `on('stalled'`, `concurrency:`
26
- **Ships green, breaks:** Any CPU-bound section, sync I/O, or GC pause longer than the renewal window blocks lock renewal → the stalled-checker (every `stalledInterval`, default 30000 ms) moves the job back to `wait` → a second worker starts it **while the first is still running it**: emails sent twice, payments charged twice. No test catches this — it needs production-sized payloads. After `maxStalledCount` (default 1) the job fails with `job stalled more than allowable limit`.
27
- **Safe change:** Size `lockDuration` above worst-case runtime when adding slow steps; keep the processor's event loop breathing (chunk CPU work, `await` regularly) or move heavy jobs to sandboxed processors (separate process = renewal survives CPU blocks); make side effects idempotent (dedup key per `job.id`); alert on the `stalled` event — it means double-processing happened.
28
-
29
- ## bullmq-retry-retention-dlq — HIGH · lifecycle-protocol
30
- **Contract:** `attempts`/`backoff` on the producer's job options promise the handler will be re-invoked with the same payload on failure; the handler must be idempotent to that promise; the `failed` set is the implicit dead-letter queue, and Redis retains completed/failed jobs per `removeOnComplete`/`removeOnFail`.
31
- **Detect:** `attempts:`, `backoff:`, `removeOnComplete`, `removeOnFail`, `UnrecoverableError`, `on('failed'`, `defaultJobOptions`
32
- **Ships green, breaks:** (1) Someone adds `attempts: 5` in producer options — the handler, written for exactly-once, now re-executes partial side effects up to 5×. (2) BullMQ's default keeps **all** completed and failed jobs forever — no `removeOnComplete`/`removeOnFail` means Redis grows until OOM weeks later (auto-removal is lazy: it runs only when a subsequent job finishes, so an idle queue never cleans up). (3) Exhausted retries land in `failed` — the de-facto DLQ nothing monitors; work is lost invisibly. Wrapping `UnrecoverableError` in a plain `Error` during a refactor silently re-enables retries.
33
- **Safe change:** Set `defaultJobOptions: { removeOnComplete: {age, count}, removeOnFail: {age} }` on every queue; treat any `attempts > 1` change as an idempotency review of the handler; alert on `failed` depth (`getFailedCount()`); preserve `UnrecoverableError` semantics through error-wrapping refactors.
34
-
35
- ## bullmq-queue-name-prefix-rendezvous — HIGH · rendezvous-string
36
- **Contract:** Producer `new Queue(name, { prefix, connection })` and worker `new Worker(name, fn, { prefix, connection })` rendezvous on the Redis key `{prefix}:{name}:*` (default prefix `bull`) — same name, same prefix, same Redis logical DB, or they never meet.
37
- **Detect:** `new Queue(`, `new Worker(`, `prefix:`, `QueueEvents(`, `db:` in redis connection opts, `{` hash-tag in prefix
38
- **Ships green, breaks:** Any mismatch — name typo, one side adding `prefix: 'myapp'`, different `db` index, different Redis instance after a config refactor — and jobs pile up in `wait` forever while the worker idles at zero errors on both sides. Redis Cluster: the prefix must be a hash tag (`prefix: '{myqueue}'`) so all keys land in one slot; producer with `{tag}` + worker without = two disjoint key spaces.
39
- **Safe change:** Define queue name + prefix once in a shared module; never inline the string; after any connection/prefix refactor, run a canary job end-to-end; on cluster, hash-tag the prefix identically everywhere including dashboards (bull-board with a different prefix shows an empty queue).
40
-
41
- ## redis-pubsub-fire-and-forget — HIGH · rendezvous-string
42
- **Contract:** `PUBLISH channel` and `SUBSCRIBE channel` / `PSUBSCRIBE pattern` meet on a literal channel string at one instant in time — Redis pub/sub has zero buffering, zero persistence, zero delivery guarantee, and `PUBLISH` returns a receiver-count nobody checks.
43
- **Detect:** `.publish(`, `.subscribe(`, `.pSubscribe(`, `.psubscribe(`, `.sSubscribe(`, `duplicate()` on redis clients
44
- **Ships green, breaks:** No subscriber connected (worker restarting, deploy gap) = message silently gone — `publish()` resolves fine, returning `0`, unchecked. Renaming a channel on one side, or editing a `PSUBSCRIBE` glob, silently partitions publisher from subscriber. Redis 7 cluster: plain `SUBSCRIBE` broadcasts cluster-wide but sharded `SSUBSCRIBE`/`SPUBLISH` route by slot — mixing `publish` with `sSubscribe` never meets. node-redis requires a dedicated (`.duplicate()`) connection in subscriber mode.
45
- **Safe change:** Treat pub/sub as lossy signaling only — anything that must be processed goes to Streams or BullMQ; centralize channel names/patterns in shared constants; pair `publish`↔`subscribe` and `sPublish`↔`sSubscribe`, never cross.
46
-
47
- ## redis-streams-group-offset-identity — CRITICAL · rendezvous-string
48
- **Contract:** A consumer group's NAME is the durable owner of its offset (`last-delivered-id`) and its pending-entries list (PEL); consumers within it are named too, and un-acked messages belong to the consumer name that read them.
49
- **Detect:** `xGroupCreate(`, `XGROUP CREATE`, `xReadGroup(`, `xAck(`, `xAutoClaim(`, `xClaim(`, `XGROUP DELCONSUMER`, `MKSTREAM`
50
- **Ships green, breaks:** Rename the group in code → `XGROUP CREATE` mints a fresh group starting wherever the code says: `$` = silently SKIP everything undelivered (data loss), `0` = REPROCESS the entire retained stream (duplicate side effects) — and the old group's PEL messages are stranded forever. `XGROUP DELCONSUMER` **discards** that consumer's pending entries outright. `XADD` with `MAXLEN` trimming can delete messages a lagging group never consumed.
51
- **Safe change:** Group names are permanent identifiers — never rename; migrate: create new group, drain old PEL via `XAUTOCLAIM` + ack, then `XGROUP DESTROY`; claim a dead consumer's PEL before any `DELCONSUMER`; monitor `XPENDING` depth and `XINFO GROUPS` lag; size `MAXLEN` against worst-case consumer downtime.
52
-
53
- ## websocket-envelope-mixed-clients — HIGH · serialized-shape
54
- **Contract:** Server and a MIXED-VERSION population of long-lived clients agree on the message envelope — the `{type, payload}` discriminator values and payload fields — with no per-message negotiation and no redeploy of clients: tabs stay open for days, mobile apps for weeks.
55
- **Detect:** `JSON.parse(event.data)`, `ws.send(JSON.stringify(`, `switch (msg.type)`, `socket.emit('`, `socket.on('`, `**/ws/**`, `**/realtime/**`
56
- **Ships green, breaks:** Server renames a `type` value or restructures `payload` → every already-connected old client either throws or — the silent case — hits a `switch` with no matched case and drops the message: the UI just stops updating, no error anywhere. socket.io event names are the same bare-literal rendezvous — a renamed event is silently never delivered. TypeScript types verify nothing about deployed clients.
57
- **Safe change:** Message types are append-only — add new `type` values, never rename/repurpose; clients MUST ignore unknown types explicitly (with a metric, not a crash); payload fields additive-only until a protocol-version bump negotiated at connect; keep the server accepting old client messages for the longest realistic client lifetime, not the deploy window.
58
-
59
- ## realtime-channel-template-tenant-scope — CRITICAL · trust-invariant
60
- **Contract:** The channel-name template (`chat:{tenantId}:{sessionId}` — Durable-Object room names, Redis channels, socket.io rooms alike) is built independently by the publisher, the subscriber, AND the subscribe-authorization check; the tenant segment embedded in the string IS the isolation boundary.
61
- **Detect:** `` `chat:${ ``, `` `${tenantId}:` ``, `.join(':')`, `split(':')`, `io.to(`, `socket.join(`, `idFromName(`, room/channel builder functions
62
- **Ships green, breaks:** Two directions, both silent. (1) **Availability** — the publisher's template changes while subscribers build the old string: clients subscribe to a channel the server never publishes to; connects succeed, auth passes, zero messages, zero errors. (2) **Isolation** — drop or misorder the tenant segment, or interpolate an unvalidated value: a `sessionId`/`tenantId` containing `:` shifts segments so the string parses as a different tenant's channel — segment injection through the delimiter. `PSUBSCRIBE chat:*` grants cross-tenant reads in one line.
63
- **Safe change:** One shared `buildChannelName()`/`parseChannelName()` module used by publish, subscribe, AND authz — never three inline template literals; validate segments against `[^:]+` before interpolation; authz parses the ACTUAL requested channel string, never trusts client-supplied segments; template changes are a versioned migration (dual-publish during rollout).
64
-
65
- ## socketio-multinode-adapter — HIGH · config-elsewhere
66
- **Contract:** `io.to(room).emit(…)` reaching ALL connected clients depends on every node running the same adapter (`@socket.io/redis-adapter` / `@socket.io/redis-streams-adapter`) against the same Redis — and HTTP long-polling fallback depends on load-balancer sticky sessions declared in infra config the app repo never sees.
67
- **Detect:** `createAdapter(`, `@socket.io/redis-adapter`, `io.adapter(`, `io.to(`, `io.in(`, `transports:`, `allowEIO3`, LB config (`ip_hash`, `sessionAffinity`, ALB stickiness)
68
- **Ships green, breaks:** Works perfectly with one node in dev. Scale to two nodes without the adapter: broadcasts only reach sockets on the emitting node — a random ~half of users silently miss every message. Sticky sessions missing: the polling handshake round-robins to a node that doesn't know the session → `400 Session ID unknown` → silent reconnect loops, presenting as "flaky realtime". A REST endpoint emitting via a bare `io` instance from a different process (no adapter) publishes to nobody.
69
- **Safe change:** Wire the adapter before the second node exists; verify with a 2-node compose setup emitting cross-node; declare the sticky-session requirement next to the socket.io code or force `transports: ['websocket']` deliberately; treat `allowEIO3` removal as a client-population question, not a cleanup.
70
-
71
- ## outbound-webhook-contract — CRITICAL · serialized-shape
72
- **Contract:** The org's emitted webhooks — payload shape, event-type names, signature header name + algorithm, and the shared secret — are consumed by EXTERNAL subscribers you cannot redeploy, coordinate with, or even enumerate.
73
- **Detect:** `createHmac(`, `timingSafeEqual(`, signature header literals (`X-*-Signature`, `X-Hub-Signature-256`), `event_type`, webhook dispatch tables, `WEBHOOK_SECRET`
74
- **Ships green, breaks:** Everything here ships green because the other side is off-repo. Rename an event type: subscribers filter on the literal string and silently ignore the new one — their integrations just stop firing. Rotate the secret in one step: every delivery fails verification the instant the env var flips; if the sender retries-then-drops, events are permanently lost. Canonicalization drift is subtler: signing re-serialized JSON instead of the exact raw bytes sent (key order, whitespace, unicode escaping) makes verification fail only for some payloads.
75
- **Safe change:** Event types and payload fields are append-only; breaking payload changes = a new version delivered per-subscriber, old version supported indefinitely; rotate secrets with an overlap window — sign with BOTH (Stripe/Svix `v1=sig1,v1=sig2` style) until subscribers confirm; sign the exact raw body bytes; include a signed timestamp; never repurpose the header name or algorithm prefix.
76
-
77
- ## sse-resume-and-event-names — MED · lifecycle-protocol
78
- **Contract:** Server SSE frames and browser `EventSource` agree on two things with no static link: the `id:` field ↔ `Last-Event-ID` resume protocol across auto-reconnects, and the `event:` field name ↔ `addEventListener(name)` dispatch.
79
- **Detect:** `text/event-stream`, `Last-Event-ID`, `new EventSource(`, `res.write('id:`, `res.write('event:`, `onmessage`, `X-Accel-Buffering`
80
- **Ships green, breaks:** (1) `EventSource` auto-reconnects invisibly and sends `Last-Event-ID`; a server that never assigned `id:` — or ignores the header — silently drops every event from the gap while the client believes it is live. (2) Adding an `event: order-update` field to previously-unnamed frames reroutes dispatch: `onmessage` fires ONLY for unnamed frames, so existing clients silently stop receiving everything. Infra trap: a buffering reverse proxy (nginx default) delays events unboundedly until `X-Accel-Buffering: no` is set.
81
- **Safe change:** Assign monotonic `id:` from day one and implement replay from `Last-Event-ID` (or send an explicit `snapshot` event on connect); never add/rename the `event:` field on an existing stream; keep event names in a shared constant when the client is in-repo; verify proxy buffering headers whenever the route moves.
@@ -1,93 +0,0 @@
1
- # Pack: identity, auth, tenancy
2
-
3
- Load when: the touch list exhibits jose (`jwtVerify(`), Better Auth (`betterAuth(`, `auth.api.`), session cookies, CSRF middleware, OAuth callbacks/`redirect_uri`, tenant scoping (`tenantId`, `organizationId`), password hashing, or auth middleware mounts. This area skews CRITICAL: the defining hazard is a silent auth bypass or data disclosure, not a loud error.
4
-
5
- ## jose-jwtverify-claims-opt-in — CRITICAL · trust-invariant
6
- **Contract:** The token minter scopes tokens with `iss`/`aud`, and every verifier must pass matching `issuer`/`audience` options to `jwtVerify` — jose 6 checks signature, `exp`, `nbf` by default but NOT `iss`, `aud`, `sub`, or `typ` unless the option is passed.
7
- **Detect:** `jwtVerify(` without `issuer:`/`audience:` in the same options object, `jwtVerify(token, secret)` two-arg calls, missing `algorithms:` where the key is an HMAC secret (`Uint8Array`), `clockTolerance`, `maxTokenAge`
8
- **Ships green, breaks:** A valid token minted for a *different* service or audience by the same issuer/key verifies successfully — cross-service token replay; with a shared HMAC secret and no `algorithms: ['HS256']` allowlist, any HS* alg the secret supports is accepted. Everything compiles; the token "verifies".
9
- **Safe change:** Always pass `{ issuer, audience, algorithms }` to `jwtVerify`; treat a bare two-arg call as a finding. Add `clockTolerance: '30s'` for cross-host skew. When adding a new consumer service, mint tokens with a distinct `aud` and enforce it there.
10
-
11
- ## jwt-identity-keyed-on-iss-plus-sub — CRITICAL · trust-invariant
12
- **Contract:** `sub` is unique only *per issuer* (RFC 7519); application user lookup must key on the pair `(iss, sub)`, never `sub` alone.
13
- **Detect:** `payload.sub` used as a sole DB lookup key, multi-IdP config (multiple `issuer` values, per-tenant IdPs, `createRemoteJWKSet` called with more than one URL)
14
- **Ships green, breaks:** With two identity providers (or per-tenant issuers), IdP B can mint `sub` values colliding with IdP A's users — a tenant-controlled IdP issues `sub: <victim-sub>` and logs in as the victim. Single-IdP tests never expose it.
15
- **Safe change:** Store external identity as a composite `(issuer, subject)` unique key (mirror of Better Auth's `(providerId, accountId)` on `account`); grep every login path for `sub`-only lookups; adding a second IdP is the trigger event for this audit.
16
-
17
- ## jose-remote-jwks-rotation-cache — HIGH · lifecycle-protocol
18
- **Contract:** The IdP's key-rotation schedule and every verifier's `createRemoteJWKSet` cache must follow the publish-first protocol: jose 6 re-fetches the JWKS when a token's `kid` matches no cached key, but at most once per `cooldownDuration` (default 30000 ms), and refreshes at most every `cacheMaxAge` (default 600000 ms = 10 min).
19
- **Detect:** `createRemoteJWKSet(`, `cooldownDuration`, `cacheMaxAge`, IdP rotation jobs, `JWKSNoMatchingKey`, `ERR_JWKS_NO_MATCHING_KEY`
20
- **Ships green, breaks:** IdP starts signing with a new key *before* publishing it → every verifier throws `JWKSNoMatchingKey`, and the 30 s cooldown suppresses re-fetch so the outage comes in waves; removing the old key from the JWKS while its tokens are still live mass-invalidates sessions; each process instance caches independently, so rollouts fail partially.
21
- **Safe change:** Rotate as: publish new key to JWKS → wait > `cacheMaxAge` → start signing with new `kid` → keep old key published until the longest-lived old token expires; never sign-then-publish; keep `kid` on every JWK and JWS header.
22
-
23
- ## better-auth-schema-dual-authority — CRITICAL · dual-authority
24
- **Contract:** Better Auth's CLI (`npx @better-auth/cli generate` / `migrate`) owns the shape of the `user`, `session`, `account`, `verification` tables (camelCase columns: `session.expiresAt` — not `expires` — `session.token`, `account.providerId`, `account.accountId`); your ORM migration tool is a second writer over the same tables.
25
- **Detect:** `@better-auth/cli`, `drizzle-kit push`, `drizzle-kit generate`, auth tables present in DB but absent from `schema.ts`, `modelName`/`fields` remapping in `betterAuth(` config
26
- **Ships green, breaks:** `drizzle-kit push` diffing against a schema file that lacks the auth tables will *drop them* (with `--force`/CI, no prompt) — every user, session, and OAuth link destroyed; adding a Better Auth plugin (admin, organization, twoFactor) without re-running `generate`/`migrate` leaves its columns (`impersonatedBy`, `role`, `banned`, `activeOrganizationId`) missing, so only that plugin's runtime paths error while core login still works.
27
- **Safe change:** After any `betterAuth(` config or plugin change, re-run `npx @better-auth/cli generate` and diff into the ORM schema; keep auth tables in the ORM schema file so push never sees them as deleted; column renames only via `fields`/`modelName` config, never raw SQL.
28
-
29
- ## better-auth-session-token-not-a-jwt — HIGH · serialized-shape
30
- **Contract:** The `better-auth.session_token` cookie carries an opaque signed token (`<token>.<hmac>`) whose first half is the `session.token` DB row key — it is NOT a JWT access/refresh pair, and every other service must validate it by calling `auth.api.getSession({ headers })`, not by decoding.
31
- **Detect:** `jwtVerify`/`decodeJwt`/`atob(` applied to the session cookie, `session.cookieCache`, `session_data`, `getSessionCookie(`
32
- **Ships green, breaks:** A sibling service that "verifies" the cookie as a JWT fails for all users; with `session.cookieCache: { enabled: true, maxAge }` the session payload is served from the signed `session_data` cookie, so `revokeSession`/user-ban does NOT take effect until the cookie cache expires — logout/ban tests without cookieCache pass, production revocation lags. `getSessionCookie()` only checks cookie *existence*, never validity — using it as an auth gate is a bypass.
33
- **Safe change:** Cross-service auth goes through `getSession` against the auth service (or add the `jwt` plugin and verify those JWTs with jose); if enabling `cookieCache`, bound `maxAge` by your revocation SLA; treat `getSessionCookie` as an optimistic-redirect hint only.
34
-
35
- ## better-auth-cookie-name-rendezvous — HIGH · rendezvous-string
36
- **Contract:** Every consumer that reads the session cookie by name (edge middleware, nginx, a second service) must compute the same name Better Auth writes: `{cookiePrefix}.session_token` (default prefix `better-auth`), which becomes `__Secure-better-auth.session_token` when cookies are secure (production / `advanced.useSecureCookies: true`).
37
- **Detect:** literal `better-auth.session_token`, `__Secure-`, `advanced.cookiePrefix`, `advanced.crossSubDomainCookies`, `getSessionCookie(request, { cookieName`
38
- **Ships green, breaks:** Code reading `cookies['better-auth.session_token']` works in dev (http, no prefix) and silently sees no cookie in production because the name is `__Secure-`-prefixed — everyone appears logged out on the consumer side only; changing `cookiePrefix` orphans every hardcoded reader. `crossSubDomainCookies` is **disabled by default** — a sibling subdomain service never receives the cookie until `advanced.crossSubDomainCookies: { enabled: true, domain }` is set.
39
- **Safe change:** Never hardcode the cookie name — use `getSessionCookie(request, { cookieName, cookiePrefix })` with the same config as the server; when changing `cookiePrefix`/`crossSubDomainCookies`, grep all services for the old literal; test cookie reads under https/production mode, not just dev.
40
-
41
- ## better-auth-trusted-origins-allowlist — CRITICAL · config-elsewhere
42
- **Contract:** `trustedOrigins` (default: only the `baseURL` origin) is the single allowlist gating both the Origin-header CSRF check and every client-supplied `callbackURL`/redirect target.
43
- **Detect:** `trustedOrigins`, wildcard entries `https://*.` / `**`, `callbackURL` passed from client code, new frontend domains not present in the auth config
44
- **Ships green, breaks:** The loud half: a new frontend origin fails with 403s. The silent half: someone "fixes" it with `https://*.example.com` — now any subdomain (stale CNAME takeover, user-content subdomains, preview deploys) can drive login CSRF and receive `callbackURL` redirects carrying fresh sessions; `**` additionally crosses `/`. The wildcard ships green because every legitimate flow still works.
45
- **Safe change:** Enumerate origins exactly; if wildcards are unavoidable, scope to a dedicated non-user-controllable parent domain; review `trustedOrigins` whenever a domain is added/retired.
46
-
47
- ## session-cookie-attribute-downgrade — CRITICAL · trust-invariant
48
- **Contract:** The session cookie's `HttpOnly` + `Secure` + `SameSite=Lax|Strict` + narrowest-possible `Domain` collectively are the CSRF/theft defense; any relaxation keeps login working while removing the guarantee.
49
- **Detect:** `sameSite: 'none'`, `httpOnly: false`, `domain: '.` (parent-domain widening), `secure: false`, proxy configs rewriting `Set-Cookie`
50
- **Ships green, breaks:** Switching to `SameSite=None` (to make an iframe work) silently re-enables classic CSRF on every state-changing endpoint unless a token defense exists; widening `Domain` to `.example.com` hands the session to every subdomain including compromised/user-content ones; `httpOnly: false` (added so JS can check "am I logged in") turns any XSS into full session theft. All ship green — the cookie still authenticates every legitimate request.
51
- **Safe change:** Treat any diff touching cookie attributes as security review; `SameSite=None` requires a compensating CSRF token on the same PR; keep `Domain` host-only unless cross-subdomain is a deliberate product decision; expose login state via an endpoint, never by de-HttpOnly-ing the session cookie.
52
-
53
- ## csrf-double-submit-lockstep — HIGH · rendezvous-string
54
- **Contract:** In double-submit CSRF, the CSRF cookie must be JS-readable (NOT HttpOnly) while the session cookie must be HttpOnly, and the frontend's header name must match the backend's reader — axios defaults `xsrfCookieName: 'XSRF-TOKEN'` / `xsrfHeaderName: 'X-XSRF-TOKEN'`.
55
- **Detect:** `XSRF-TOKEN`, `X-XSRF-TOKEN`, `xsrfCookieName`, `xsrfHeaderName`, `withXSRFToken`, CSRF middleware with a skip-when-absent branch, blanket `httpOnly: true` cookie sweeps
56
- **Ships green, breaks:** The two cookies are routinely inverted: a "hardening" sweep sets HttpOnly on everything — the frontend can't read the CSRF cookie, and if the middleware validates only *when the header is present*, requests sail through with zero protection, silently; renaming the header on one side has the same fail-open effect. axios only auto-attaches the XSRF header same-origin unless `withXSRFToken` is set — cross-origin SPAs silently send no token.
57
- **Safe change:** CSRF middleware must *reject* state-changing requests when the token is absent, never skip; pin cookie+header names in one shared constant consumed by both sides; assert in an integration test that a request without the header is refused.
58
-
59
- ## oauth-redirect-uri-exact-match — CRITICAL · trust-invariant
60
- **Contract:** The OAuth authorization-server allowlist and the client's `redirect_uri` agree by *exact string match*; `state` (and `nonce`/PKCE for OIDC) must be both generated and verified — the flow completes identically when verification is dropped.
61
- **Detect:** `redirect_uri`, `startsWith(`/prefix/wildcard matching in redirect validation, `genericOAuth` plugin configs, hand-rolled `/callback` handlers, `state` generated but never compared, `code_verifier`/`code_challenge` present on only one leg
62
- **Ships green, breaks:** Prefix or wildcard matching lets `https://app.example.com.evil.com` or a path-traversal callback receive the authorization code — code exfiltration, full account access; dropping the `state` comparison (or the PKCE `code_verifier` check) fails open: every legitimate login still works, but login-CSRF/code-injection is silently enabled. Better Auth's built-in providers handle this — the hazard is `genericOAuth` configs and hand-rolled flows.
63
- **Safe change:** Register and compare `redirect_uri` byte-for-byte (scheme, host, port, path); generate `state` bound to the session and *reject* on mismatch or absence; require PKCE S256 end-to-end; test the negative paths (tampered state, missing verifier), not just the happy path.
64
-
65
- ## account-linking-verified-email-only — CRITICAL · trust-invariant
66
- **Contract:** External identities key on `(providerId, accountId)` (Better Auth's `account` table), and auto-linking a new OAuth identity to an existing user by email is safe only when the provider attests the email as verified — Better Auth's default links only on verified-email match; `trustedProviders` bypasses that check and `allowDifferentEmails: true` (default `false`) widens it further.
67
- **Detect:** `account.accountLinking`, `trustedProviders`, `allowDifferentEmails`, custom provisioning doing `findUserByEmail` on OAuth callback, lookups keyed on profile email instead of `(providerId, accountId)`
68
- **Ships green, breaks:** Adding a provider to `trustedProviders` (or hand-rolling link-by-email) means an attacker registers at that provider with the victim's email *unverified*, signs in, and is silently linked into the victim's account — full takeover; every legitimate login flow still works, so nothing surfaces in testing.
69
- **Safe change:** Keep identity lookup on `(providerId, accountId)`; only list providers in `trustedProviders` that contractually verify emails; leave `allowDifferentEmails` false unless linking is an explicit user-initiated, authenticated action.
70
-
71
- ## tenant-id-non-spoofable-source — CRITICAL · trust-invariant
72
- **Contract:** The tenant identifier used to scope every query must come from a server-validated source — a verified JWT claim, the session (e.g. Better Auth organization plugin's `session.activeOrganizationId`), or server-resolved subdomain — never from `req.body`, `req.query`, or a client-set header; and every data access must actually include the scope.
73
- **Detect:** `req.body.tenantId`, `req.query.tenantId`, `req.headers['x-tenant`, queries by bare PK in multi-tenant repos (`WHERE id = ?` without `tenant_id`/`organizationId`), new route files not using the shared tenant middleware
74
- **Ships green, breaks:** Two failure modes, both silent: (1) tenant read from the request → any authenticated user sets another tenant's id and reads/writes their data; (2) the new-endpoint failure — a handler queries by UUID primary key alone, works perfectly in every test (ids are globally unique), and is a cross-tenant IDOR in production. Neither throws, lints, or fails a test.
75
- **Safe change:** One `requireTenant` middleware derives `req.tenantId` from session/claim only — delete all request-sourced fallbacks; make the repository layer take `tenantId` as a required parameter so unscoped queries can't be expressed; review every new endpoint for the scope, not just the auth check.
76
-
77
- ## express5-auth-middleware-mount-order — HIGH · lifecycle-protocol
78
- **Contract:** In Express 5, `app.use(authMiddleware)` protects only routes and routers mounted *after* it in registration order, and per-identity rate limiting (`keyGenerator: req => req.user.id`) requires auth to have already run.
79
- **Detect:** `app.use(` ordering in the composition root, router mounts above the auth `app.use`, `keyGenerator` referencing `req.user`, `app.use('*'` patterns (path-to-regexp v8: bare `*` is invalid — use `/*splat`)
80
- **Ships green, breaks:** A new `app.use('/api/reports', reportsRouter)` line added above the auth mount ships green — every endpoint works in manual testing (the dev is logged in anyway) — but is fully unauthenticated; a rate limiter mounted before auth with an identity `keyGenerator` sees `req.user === undefined`, collapsing all clients into one shared `undefined` bucket: global lockout under load and per-user limits silently gone.
81
- **Safe change:** Keep one composition root with the invariant order: parsers → auth → identity-keyed rate limit → routers → error handler; mount new routers only below the auth line; give identity-keyed limiters a hard failure (throw) when `req.user` is absent instead of a fallback key.
82
-
83
- ## password-hash-format-migration — CRITICAL · serialized-shape
84
- **Contract:** Stored password hashes (Better Auth default: scrypt, in `account.password` for `providerId: 'credential'`) are a persisted serialized format that every future `verify` implementation must still accept.
85
- **Detect:** `emailAndPassword.password: { hash, verify }` overrides, argon2/bcrypt param changes, hash-format prefixes in `account.password` vs what `verify` parses, absence of rehash-on-login logic
86
- **Ships green, breaks:** Swapping the algorithm (scrypt → argon2) or tightening params without a legacy path makes `verify` return `false` for every pre-existing user — no error is thrown, new signups work, fresh-user tests pass, and the user base is silently locked out over the following weeks as sessions expire.
87
- **Safe change:** New `verify` must detect the old format by prefix and accept it; on successful legacy verify, rehash with the new params and update the row; keep the legacy branch until no old-format hashes remain (measure, don't guess); never bulk-invalidate.
88
-
89
- ## impersonation-dual-identity-audit — HIGH · trust-invariant
90
- **Contract:** With Better Auth's admin plugin, an impersonated session carries BOTH identities — `session.userId` is the target, `session.impersonatedBy` is the acting admin — and every audit/attribution consumer must read both; the impersonated session auto-expires after 1 hour by default (`impersonationSessionDuration`, seconds).
91
- **Detect:** `impersonateUser`, `stopImpersonating`, `impersonatedBy`, `impersonationSessionDuration`, audit code reading only `session.userId`, session payloads forwarded downstream without `impersonatedBy`
92
- **Ships green, breaks:** Audit logs, "last modified by" fields, and downstream services that record `session.userId` silently attribute admin actions to the *victim* user — the audit trail is wrong precisely when it matters; custom session serialization that drops `impersonatedBy` erases the distinction entirely. Impersonation itself works flawlessly, so nothing surfaces.
93
- **Safe change:** Every audit write records `actorId = session.impersonatedBy ?? session.userId` plus `onBehalfOf` when they differ; propagate `impersonatedBy` in any session payload crossing a service boundary; after adding the admin plugin, re-run the Better Auth CLI so the `impersonatedBy` column exists (see schema dual-authority entry).