@tenkara/afm-linux-arm64 0.0.0-stage → 4.0.0-alpha.11

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 (283) hide show
  1. package/bin/afm +4 -0
  2. package/content/.coupling-baseline.tsv +1 -0
  3. package/content/.pending-commands.txt +2 -0
  4. package/content/CHANGELOG-archive.md +806 -0
  5. package/content/CHANGELOG.md +4330 -0
  6. package/content/PRINCIPLES.md +119 -0
  7. package/content/README-light.md +158 -0
  8. package/content/README.md +150 -0
  9. package/content/code-style/_any/file-size.md +62 -0
  10. package/content/code-style/_any/imports.md +68 -0
  11. package/content/code-style/_any/naming.md +72 -0
  12. package/content/code-style/_any/test-placement.md +57 -0
  13. package/content/code-style/nextjs-app-router/imports.md +50 -0
  14. package/content/code-style/nextjs-app-router/naming.md +50 -0
  15. package/content/code-style/prisma-postgres/error-boundary.md +55 -0
  16. package/content/code-style/prisma-postgres/imports.md +61 -0
  17. package/content/code-style/shadcn-ui/naming.md +48 -0
  18. package/content/code-style/stripe-billing/naming.md +76 -0
  19. package/content/code-style/trigger-dev-v4/imports.md +58 -0
  20. package/content/code-style/trigger-dev-v4/naming.md +49 -0
  21. package/content/code-style/trigger-dev-v4/test-placement.md +61 -0
  22. package/content/code-style/trpc-v11/error-boundary.md +51 -0
  23. package/content/code-style/trpc-v11/file-size.md +44 -0
  24. package/content/code-style/trpc-v11/naming.md +60 -0
  25. package/content/code-style/trpc-v11/test-placement.md +52 -0
  26. package/content/core/ach.md +260 -0
  27. package/content/core/afm.md +514 -0
  28. package/content/core/gotchas.md +18 -0
  29. package/content/core/prd.md +141 -0
  30. package/content/core/protocol.md +158 -0
  31. package/content/core/ust.md +65 -0
  32. package/content/guardrails/README.md +55 -0
  33. package/content/guardrails/dsl-com-ordem-obrigatoria.md +47 -0
  34. package/content/guardrails/exaustividade-de-union.md +48 -0
  35. package/content/guardrails/fronteira-de-import.md +57 -0
  36. package/content/hooks/README.md +70 -0
  37. package/content/interview/bootstrap.md +417 -0
  38. package/content/interview/promotion.md +115 -0
  39. package/content/interview/refactor.md +379 -0
  40. package/content/interview/triggers.md +107 -0
  41. package/content/modules/nextjs-app-router/ach.patch.md +44 -0
  42. package/content/modules/nextjs-app-router/gotchas.seed.md +78 -0
  43. package/content/modules/nextjs-app-router/rules.patch.md +35 -0
  44. package/content/modules/prisma-postgres/ach.patch.md +65 -0
  45. package/content/modules/prisma-postgres/rules.patch.md +28 -0
  46. package/content/modules/shadcn-ui/ach.patch.md +42 -0
  47. package/content/modules/stripe-billing/ach.patch.md +46 -0
  48. package/content/modules/stripe-billing/gotchas.seed.md +73 -0
  49. package/content/modules/stripe-billing/rules.patch.md +30 -0
  50. package/content/modules/trigger-dev-v4/ach.patch.md +64 -0
  51. package/content/modules/trigger-dev-v4/gotchas.seed.md +53 -0
  52. package/content/modules/trigger-dev-v4/rules.patch.md +35 -0
  53. package/content/modules/trpc-v11/ach.patch.md +56 -0
  54. package/content/modules/trpc-v11/rules.patch.md +34 -0
  55. package/content/modules/vireya-ui/ach.patch.md +85 -0
  56. package/content/modules/vireya-ui/gotchas.seed.md +98 -0
  57. package/content/modules/vireya-ui/rules.patch.md +36 -0
  58. package/content/ops/adr.md +54 -0
  59. package/content/ops/analyze.md +130 -0
  60. package/content/ops/clarify.md +86 -0
  61. package/content/ops/deliver.md +209 -0
  62. package/content/ops/diagnose.md +143 -0
  63. package/content/ops/evolve.md +366 -0
  64. package/content/ops/export-template.md +97 -0
  65. package/content/ops/generalize.md +117 -0
  66. package/content/ops/gotcha.md +46 -0
  67. package/content/ops/obligations.md +145 -0
  68. package/content/ops/plan.md +61 -0
  69. package/content/ops/policy.md +61 -0
  70. package/content/ops/promote.md +72 -0
  71. package/content/ops/protocol.md +51 -0
  72. package/content/ops/reconcile.md +44 -0
  73. package/content/ops/reflect.md +182 -0
  74. package/content/ops/remediate.md +143 -0
  75. package/content/ops/restructure.md +142 -0
  76. package/content/ops/rule.md +170 -0
  77. package/content/ops/specify.md +62 -0
  78. package/content/ops/tasks.md +67 -0
  79. package/content/ops/update.md +252 -0
  80. package/content/ops/verify.md +67 -0
  81. package/content/patterns/adapter.md +46 -0
  82. package/content/patterns/facade.md +47 -0
  83. package/content/patterns/factory.md +46 -0
  84. package/content/patterns/null-object.md +55 -0
  85. package/content/patterns/result-vs-exception.md +52 -0
  86. package/content/patterns/strategy.md +45 -0
  87. package/content/patterns/template-method-vs-composition.md +50 -0
  88. package/content/questions/adr.questions.yaml +14 -0
  89. package/content/questions/analyze.questions.yaml +12 -0
  90. package/content/questions/bootstrap-mapping.questions.yaml +14 -0
  91. package/content/questions/bootstrap-rules.questions.yaml +10 -0
  92. package/content/questions/bootstrap-stack.questions.yaml +23 -0
  93. package/content/questions/clarify.questions.yaml +10 -0
  94. package/content/questions/code-style.questions.yaml +12 -0
  95. package/content/questions/confirm.questions.yaml +12 -0
  96. package/content/questions/deliver.questions.yaml +23 -0
  97. package/content/questions/evolve.questions.yaml +15 -0
  98. package/content/questions/export-template-apply.questions.yaml +11 -0
  99. package/content/questions/export-template.questions.yaml +23 -0
  100. package/content/questions/gates.questions.yaml +27 -0
  101. package/content/questions/generalize.questions.yaml +13 -0
  102. package/content/questions/gotcha.questions.yaml +11 -0
  103. package/content/questions/init-template.questions.yaml +10 -0
  104. package/content/questions/init.questions.yaml +41 -0
  105. package/content/questions/obligations.questions.yaml +17 -0
  106. package/content/questions/plan.questions.yaml +14 -0
  107. package/content/questions/refactor.questions.yaml +23 -0
  108. package/content/questions/reflect.questions.yaml +13 -0
  109. package/content/questions/remediate.questions.yaml +27 -0
  110. package/content/questions/restructure.questions.yaml +15 -0
  111. package/content/questions/rf-mapping.questions.yaml +12 -0
  112. package/content/questions/rule-scope.questions.yaml +12 -0
  113. package/content/questions/rule.questions.yaml +12 -0
  114. package/content/questions/specify.questions.yaml +23 -0
  115. package/content/questions/update-artifacts.questions.yaml +10 -0
  116. package/content/questions/update-strings.questions.yaml +14 -0
  117. package/content/questions/update.questions.yaml +10 -0
  118. package/content/questions/verify.questions.yaml +9 -0
  119. package/content/roles/executor.md +18 -0
  120. package/content/roles/explorer.md +22 -0
  121. package/content/roles/reviewer.md +23 -0
  122. package/content/rubrics/enum-vs-union-vs-branded.md +40 -0
  123. package/content/rubrics/episodic-vs-semantic-boundary.md +37 -0
  124. package/content/rubrics/error-classification.md +98 -0
  125. package/content/rubrics/failure-classification.md +111 -0
  126. package/content/rubrics/negative-filters.md +38 -0
  127. package/content/rubrics/solid-triggers.md +119 -0
  128. package/content/rubrics/template-vs-streaming-precedence.md +34 -0
  129. package/content/rubrics/validation-boundary.md +94 -0
  130. package/content/rubrics/when-to-create-dsl.md +65 -0
  131. package/content/rubrics/when-to-create-lib.md +49 -0
  132. package/content/rubrics/when-to-create-module.md +39 -0
  133. package/content/rubrics/when-to-evolve-methodology.md +62 -0
  134. package/content/skills/adr.md +42 -0
  135. package/content/skills/analyze.md +28 -0
  136. package/content/skills/clarify.md +28 -0
  137. package/content/skills/deliver.md +26 -0
  138. package/content/skills/diagnose.md +42 -0
  139. package/content/skills/evolve.md +43 -0
  140. package/content/skills/export-template.md +37 -0
  141. package/content/skills/generalize.md +31 -0
  142. package/content/skills/gotcha.md +40 -0
  143. package/content/skills/init.md +77 -0
  144. package/content/skills/obligations.md +27 -0
  145. package/content/skills/plan.md +28 -0
  146. package/content/skills/policy.md +25 -0
  147. package/content/skills/promote.md +39 -0
  148. package/content/skills/protocol.md +41 -0
  149. package/content/skills/reconcile.md +45 -0
  150. package/content/skills/refactor.md +79 -0
  151. package/content/skills/reflect.md +43 -0
  152. package/content/skills/remediate.md +33 -0
  153. package/content/skills/restructure.md +46 -0
  154. package/content/skills/rule.md +43 -0
  155. package/content/skills/specify.md +28 -0
  156. package/content/skills/tasks.md +24 -0
  157. package/content/skills/update.md +58 -0
  158. package/content/skills/verify.md +29 -0
  159. package/content/templates/adr.template.md +51 -0
  160. package/content/templates/evolution-report.template.md +132 -0
  161. package/content/templates/feature-plan.template.md +84 -0
  162. package/content/templates/feature-research.template.md +56 -0
  163. package/content/templates/feature-spec.template.md +86 -0
  164. package/content/templates/feature-tasks.induced.template.md +91 -0
  165. package/content/templates/feature-tasks.template.md +82 -0
  166. package/content/templates/focus.template.md +30 -0
  167. package/content/templates/gotcha.template.md +31 -0
  168. package/content/templates/guardrail-runner.template.md +97 -0
  169. package/content/templates/guardrail.template.md +63 -0
  170. package/content/templates/learning.template.md +40 -0
  171. package/content/templates/procedure.template.md +47 -0
  172. package/content/templates/protocols/p1.md +29 -0
  173. package/content/templates/protocols/p2.md +28 -0
  174. package/content/templates/protocols/p3.md +47 -0
  175. package/content/templates/research.template.md +50 -0
  176. package/content/templates/session-handoff.template.md +58 -0
  177. package/content/templates/us.template.md +39 -0
  178. package/content/tools/gate/core-rules/PLUGIN-13-secrets.sh +36 -0
  179. package/content/tools/gate/core-rules/PLUGIN-21-code-language.sh +27 -0
  180. package/content/tools/gate/core-rules/PLUGIN-35-gate-complete.sh +19 -0
  181. package/content/tools/gate/core-rules/PLUGIN-36-absolute-path.sh +38 -0
  182. package/content/tools/gate/core-rules/PLUGIN-37-prose-language.sh +56 -0
  183. package/content/tools/gate/core-rules/PLUGIN-38-parser-marker.sh +82 -0
  184. package/content/tools/gate/core-rules/PLUGIN-39-frontmatter-ops.sh +325 -0
  185. package/content/tools/gate/core-rules/PLUGIN-40-dead-reference.sh +74 -0
  186. package/content/tools/gate/core-rules/README.md +55 -0
  187. package/content/tools/gate/guardrails.sh +740 -0
  188. package/content/tools/gate/lib/ratchet.sh +119 -0
  189. package/content/tools/gate/lib/scope.sh +85 -0
  190. package/content/tools/gate/ratchet.seed.tsv +1 -0
  191. package/content/tools/gate/rules/README.md +50 -0
  192. package/package.json +19 -5
  193. package/plugin/agy/hooks.json +29 -0
  194. package/plugin/agy/mcp_config.json +7 -0
  195. package/plugin/agy/plugin.json +6 -0
  196. package/plugin/agy/skills/afm-adr/SKILL.md +48 -0
  197. package/plugin/agy/skills/afm-analyze/SKILL.md +34 -0
  198. package/plugin/agy/skills/afm-clarify/SKILL.md +34 -0
  199. package/plugin/agy/skills/afm-deliver/SKILL.md +30 -0
  200. package/plugin/agy/skills/afm-diagnose/SKILL.md +46 -0
  201. package/plugin/agy/skills/afm-evolve/SKILL.md +49 -0
  202. package/plugin/agy/skills/afm-export-template/SKILL.md +43 -0
  203. package/plugin/agy/skills/afm-generalize/SKILL.md +37 -0
  204. package/plugin/agy/skills/afm-gotcha/SKILL.md +46 -0
  205. package/plugin/agy/skills/afm-init/SKILL.md +81 -0
  206. package/plugin/agy/skills/afm-obligations/SKILL.md +31 -0
  207. package/plugin/agy/skills/afm-plan/SKILL.md +34 -0
  208. package/plugin/agy/skills/afm-policy/SKILL.md +29 -0
  209. package/plugin/agy/skills/afm-promote/SKILL.md +43 -0
  210. package/plugin/agy/skills/afm-protocol/SKILL.md +45 -0
  211. package/plugin/agy/skills/afm-reconcile/SKILL.md +49 -0
  212. package/plugin/agy/skills/afm-refactor/SKILL.md +83 -0
  213. package/plugin/agy/skills/afm-reflect/SKILL.md +49 -0
  214. package/plugin/agy/skills/afm-remediate/SKILL.md +39 -0
  215. package/plugin/agy/skills/afm-restructure/SKILL.md +52 -0
  216. package/plugin/agy/skills/afm-rule/SKILL.md +49 -0
  217. package/plugin/agy/skills/afm-specify/SKILL.md +34 -0
  218. package/plugin/agy/skills/afm-tasks/SKILL.md +28 -0
  219. package/plugin/agy/skills/afm-update/SKILL.md +64 -0
  220. package/plugin/agy/skills/afm-verify/SKILL.md +35 -0
  221. package/plugin/claude-code/.claude-plugin/marketplace.json +15 -0
  222. package/plugin/claude-code/.claude-plugin/plugin.json +9 -0
  223. package/plugin/claude-code/.mcp.json +7 -0
  224. package/plugin/claude-code/agents/afm-executor.md +18 -0
  225. package/plugin/claude-code/agents/afm-explorer.md +20 -0
  226. package/plugin/claude-code/agents/afm-reviewer.md +21 -0
  227. package/plugin/claude-code/hooks/hooks.json +85 -0
  228. package/plugin/claude-code/skills/adr/SKILL.md +48 -0
  229. package/plugin/claude-code/skills/analyze/SKILL.md +34 -0
  230. package/plugin/claude-code/skills/clarify/SKILL.md +34 -0
  231. package/plugin/claude-code/skills/deliver/SKILL.md +30 -0
  232. package/plugin/claude-code/skills/diagnose/SKILL.md +46 -0
  233. package/plugin/claude-code/skills/evolve/SKILL.md +49 -0
  234. package/plugin/claude-code/skills/export-template/SKILL.md +43 -0
  235. package/plugin/claude-code/skills/generalize/SKILL.md +37 -0
  236. package/plugin/claude-code/skills/gotcha/SKILL.md +46 -0
  237. package/plugin/claude-code/skills/init/SKILL.md +81 -0
  238. package/plugin/claude-code/skills/obligations/SKILL.md +31 -0
  239. package/plugin/claude-code/skills/plan/SKILL.md +34 -0
  240. package/plugin/claude-code/skills/policy/SKILL.md +29 -0
  241. package/plugin/claude-code/skills/promote/SKILL.md +43 -0
  242. package/plugin/claude-code/skills/protocol/SKILL.md +45 -0
  243. package/plugin/claude-code/skills/reconcile/SKILL.md +49 -0
  244. package/plugin/claude-code/skills/refactor/SKILL.md +83 -0
  245. package/plugin/claude-code/skills/reflect/SKILL.md +49 -0
  246. package/plugin/claude-code/skills/remediate/SKILL.md +39 -0
  247. package/plugin/claude-code/skills/restructure/SKILL.md +52 -0
  248. package/plugin/claude-code/skills/rule/SKILL.md +49 -0
  249. package/plugin/claude-code/skills/specify/SKILL.md +34 -0
  250. package/plugin/claude-code/skills/tasks/SKILL.md +28 -0
  251. package/plugin/claude-code/skills/update/SKILL.md +64 -0
  252. package/plugin/claude-code/skills/verify/SKILL.md +35 -0
  253. package/plugin/opencode/afm/index.js +245 -0
  254. package/plugin/opencode/afm/tui.js +21 -0
  255. package/plugin/opencode/agents/afm-executor.md +39 -0
  256. package/plugin/opencode/agents/afm-explorer.md +92 -0
  257. package/plugin/opencode/agents/afm-reviewer.md +93 -0
  258. package/plugin/opencode/skills/afm-adr/SKILL.md +48 -0
  259. package/plugin/opencode/skills/afm-analyze/SKILL.md +34 -0
  260. package/plugin/opencode/skills/afm-clarify/SKILL.md +34 -0
  261. package/plugin/opencode/skills/afm-deliver/SKILL.md +30 -0
  262. package/plugin/opencode/skills/afm-diagnose/SKILL.md +46 -0
  263. package/plugin/opencode/skills/afm-evolve/SKILL.md +49 -0
  264. package/plugin/opencode/skills/afm-export-template/SKILL.md +43 -0
  265. package/plugin/opencode/skills/afm-generalize/SKILL.md +37 -0
  266. package/plugin/opencode/skills/afm-gotcha/SKILL.md +46 -0
  267. package/plugin/opencode/skills/afm-init/SKILL.md +81 -0
  268. package/plugin/opencode/skills/afm-obligations/SKILL.md +31 -0
  269. package/plugin/opencode/skills/afm-plan/SKILL.md +34 -0
  270. package/plugin/opencode/skills/afm-policy/SKILL.md +29 -0
  271. package/plugin/opencode/skills/afm-promote/SKILL.md +43 -0
  272. package/plugin/opencode/skills/afm-protocol/SKILL.md +45 -0
  273. package/plugin/opencode/skills/afm-reconcile/SKILL.md +49 -0
  274. package/plugin/opencode/skills/afm-refactor/SKILL.md +83 -0
  275. package/plugin/opencode/skills/afm-reflect/SKILL.md +49 -0
  276. package/plugin/opencode/skills/afm-remediate/SKILL.md +39 -0
  277. package/plugin/opencode/skills/afm-restructure/SKILL.md +52 -0
  278. package/plugin/opencode/skills/afm-rule/SKILL.md +49 -0
  279. package/plugin/opencode/skills/afm-specify/SKILL.md +34 -0
  280. package/plugin/opencode/skills/afm-tasks/SKILL.md +28 -0
  281. package/plugin/opencode/skills/afm-update/SKILL.md +64 -0
  282. package/plugin/opencode/skills/afm-verify/SKILL.md +35 -0
  283. package/README.md +0 -3
@@ -0,0 +1,72 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Decides the shape of an export name — prefix or suffix, and action-first or entity-first.'
4
+ load: cold
5
+ concern: naming
6
+ ---
7
+
8
+ # Code style — Export naming
9
+
10
+ ## The decision
11
+
12
+ This is not about which word to use — it is about the SHAPE the decision takes. Two independent
13
+ axes decide the shape of an exported name, and a codebase needs an answer on each:
14
+
15
+ 1. **Where does the layer tag sit** — as a prefix (`domain_createUser`), as a suffix
16
+ (`createUserDomain`), or nowhere in the name at all, left to the folder the file lives in?
17
+ 2. **What comes first inside the name** — the action (`createUser`) or the entity
18
+ (`userCreate`)?
19
+
20
+ ## Axis 1 — Prefix, suffix, or no tag
21
+
22
+ - **Prefix** (`domain_<action><Entity>`): sorted alphabetically, every export in a layer clusters
23
+ together — `grep '^export const domain_'` finds the whole layer's public surface in one
24
+ command, independent of file location. **Cost:** the identifier now carries information the
25
+ file's own path already carries, and the two can drift — a file that moves to a different layer
26
+ keeps a prefix that no longer describes it until someone renames the export too.
27
+ - **Suffix** (`<action><Entity>Domain`): reads as a noun phrase — "createUserDomain" parses as
28
+ "the Domain [operation] for creating a User" the way English adjectives-after-noun sometimes
29
+ read more naturally in code completion, where the base name (`createUser`) still autocompletes
30
+ first, and the layer qualifier appears as a refinement. **Cost:** same drift risk as a prefix,
31
+ and alphabetical clustering by layer is lost — sorting now clusters by action or entity instead.
32
+ - **No tag, layer signaled by path alone**: the shortest name, and the file's directory is the
33
+ single source of truth for its layer, so there is nothing in the identifier that can drift out
34
+ of sync with reality. **Cost:** nothing greppable-by-name marks the boundary. Enforcing "domain
35
+ code never imports the transport layer" now needs a path-based rule instead of a name-based one
36
+ — which is not necessarily worse, but it is a different mechanism, and re-exports through a
37
+ barrel file can make the path itself ambiguous.
38
+
39
+ ## Axis 2 — Action-first or entity-first
40
+
41
+ - **Action-first** (`createUser`, `deleteUser`, `createOrder`): matches how the code reads at the
42
+ call site — a verb phrase. Typing the action ("create…") surfaces every creator across every
43
+ entity, which is how "what CRUD operations exist" gets scanned.
44
+ - **Entity-first** (`userCreate`, `userDelete`, `orderCreate`): typing the entity ("user…")
45
+ surfaces every operation on that one entity, which is how "what can I do to a User" gets
46
+ scanned. This is the axis that matters more when most work is "go deep on one entity" rather
47
+ than "go wide across one action."
48
+
49
+ Neither axis has a universally right answer — the right answer is whichever one matches how this
50
+ team actually searches its own codebase more often, which the measurement below settles instead
51
+ of guessing.
52
+
53
+ ## How to measure which shape this codebase already follows
54
+
55
+ Look at the exports that already exist before proposing a shape:
56
+
57
+ ```sh
58
+ grep -rhoE '^export (async )?function [a-zA-Z_]+' src \
59
+ | sed 's/.* //' | sort | uniq -c | sort -rn | head -20
60
+ ```
61
+
62
+ Read the output for a dominant prefix (`^[a-z]+_`), a dominant suffix, or neither. If a majority
63
+ already follows one shape, propose codifying it — do not introduce a third shape into a codebase
64
+ that has already, even informally, converged on one.
65
+
66
+ ## Consequence of mixing both
67
+
68
+ A codebase with `domain_createUser` next to `createOrderDomain` next to `handleWebhook` (no tag at
69
+ all) cannot be greped for "every domain export" by any single pattern — the search has to know,
70
+ file by file, which shape that file happened to use. The mechanical boundary check that a
71
+ consistent shape would give for free (one `grep`, one hard rule) degrades into a manual read of
72
+ every file, which is the exact cost a naming convention exists to avoid.
@@ -0,0 +1,57 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Decides where a test file lives relative to the source file it tests.'
4
+ load: cold
5
+ concern: test-placement
6
+ ---
7
+
8
+ # Code style — Test placement
9
+
10
+ ## The decision
11
+
12
+ A test file needs a fixed address relative to the source it tests. Two shapes answer that, and a
13
+ codebase drifts the moment both are in use at once.
14
+
15
+ ## Option A — Neighbor file
16
+
17
+ `<name>.test.ts` sits in the same directory as `<name>.ts`.
18
+
19
+ - **For:** the test is one file-tree hop from the code, so opening one strongly suggests the
20
+ other. Deleting or moving the source and forgetting the test is rare — the two sit side by side
21
+ in every directory listing and every diff.
22
+ - **Against:** a directory that holds ten source files now holds twenty entries. A quick `ls` of
23
+ "what does this module do" is diluted by an equal count of test files.
24
+
25
+ ## Option B — A `__tests__/` directory
26
+
27
+ Tests for a module collect under a sibling `__tests__/` folder instead of sitting next to the
28
+ source.
29
+
30
+ - **For:** the source directory stays a pure listing of what the module does. Tooling that treats
31
+ "test" as a separate concern (a coverage report, an editor's file-tree filter) has one folder to
32
+ point at instead of a naming pattern to match.
33
+ - **Against:** the link between a source file and its test is now a path convention, not
34
+ proximity. Renaming `foo.ts` to `bar.ts` and forgetting `__tests__/foo.test.ts` produces an
35
+ orphaned test that nothing flags — the file still runs, just against code that no longer exists
36
+ under that name.
37
+
38
+ ## How to measure which one this codebase already follows
39
+
40
+ Count both shapes before proposing either one — the codebase has usually already chosen:
41
+
42
+ ```sh
43
+ find src -name "*.test.*" -o -name "*.spec.*" | grep -v '__tests__' | wc -l # neighbor count
44
+ find src -type d -name "__tests__" | wc -l # __tests__ dirs present
45
+ ```
46
+
47
+ A clear majority on one side settles it. Propose codifying what already dominates, not the
48
+ option either of you prefers in the abstract — a rule that fights the existing grain gets
49
+ routed around at the first deadline.
50
+
51
+ ## Consequence of mixing both
52
+
53
+ A codebase with both shapes present can't grep its way to "did this file get a test" — the search
54
+ has to check two locations, and a script that checks only one silently under-reports coverage.
55
+ Worse, a reviewer scanning for the test of a changed file checks the neighbor first, finds
56
+ nothing, and assumes untested when the test is one level away in `__tests__/`. Pick one, and let
57
+ the measurement above decide which one costs less to enforce.
@@ -0,0 +1,50 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Proposes a rule that a route handler stays thin and delegates logic to a server/ layer, instead of embedding business rules in the App Router route file.'
4
+ load: cold
5
+ stack: nextjs-app-router
6
+ concern: imports
7
+ detect: |
8
+ if command -v jq >/dev/null 2>&1; then jq -e '(.dependencies//{})+(.devDependencies//{})+(.peerDependencies//{})|has("next")' package.json >/dev/null 2>&1; else grep -q '"next"' package.json 2>/dev/null; fi
9
+ ---
10
+
11
+ # Code style — Next.js route handlers delegate to server/
12
+
13
+ > This is a **proposal**, not a fact about Next.js. Next.js has no opinion on whether a
14
+ > `route.ts` file contains business logic or calls out to another layer — this is a
15
+ > layer-direction choice, the same kind covered generally in `code-style/_any/imports.md`.
16
+ > Materialize it into `docs/ach.md` and `docs/afm.md` §3 only if the operator accepts it. Nothing
17
+ > here is `(mandatory)`.
18
+
19
+ ## The convention
20
+
21
+ - A route handler under `src/app/api/.../route.ts` validates input, calls into a `server/` layer,
22
+ and returns the response. It contains no inline business rule.
23
+ - The `server/` layer (or whatever a project already calls its business/domain code) owns the
24
+ actual logic; the route file is a thin adapter between the HTTP boundary and that layer.
25
+
26
+ ## Why this works
27
+
28
+ A route handler that only adapts input to a call and a result to a response stays small enough to
29
+ read in one pass, and the same business logic becomes reachable from a second entry point (a cron
30
+ job, a CLI, another route) without duplicating it. Keeping the rule qualitative — "delegate, don't
31
+ embed" — rather than a line-count threshold avoids the trap the line-count version of this rule
32
+ fell into: a fixed number with no measurement behind it. A project that wants a mechanical
33
+ backstop for file size can add one from `code-style/_any/file-size.md` instead, with a threshold
34
+ it actually measured against its own files.
35
+
36
+ ## The trade-off
37
+
38
+ "Delegate to server/" assumes a project already has, or wants, a server/domain layer distinct from
39
+ the route file — a small project with three routes and no other consumer of that logic gets pure
40
+ indirection for no reuse benefit. And the rule itself is not mechanically checkable: nothing greps
41
+ "this route file contains a business rule" reliably, so it stays a review-time principle unless a
42
+ project also has file-size or complexity tooling that catches a route file growing past what
43
+ adapting alone should need.
44
+
45
+ ## If the operator accepts this
46
+
47
+ Materialize it into `docs/ach.md` §6 as a §1.3 principle (review, not a grep trigger) — there is
48
+ no mechanical check for "no inline business rule." A project that also wants a numeric backstop
49
+ should adopt `code-style/_any/file-size.md` separately, with a threshold measured against its own
50
+ route files, not an imported constant.
@@ -0,0 +1,50 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Proposes rooting the App Router at src/app/, a src/components/ location for UI primitives, and a naming convention for route groups.'
4
+ load: cold
5
+ stack: nextjs-app-router
6
+ concern: naming
7
+ detect: |
8
+ if command -v jq >/dev/null 2>&1; then jq -e '(.dependencies//{})+(.devDependencies//{})+(.peerDependencies//{})|has("next")' package.json >/dev/null 2>&1; else grep -q '"next"' package.json 2>/dev/null; fi
9
+ ---
10
+
11
+ # Code style — Next.js App Router folder naming
12
+
13
+ > This is a **proposal**, not a fact about Next.js. Next.js accepts the App Router rooted at
14
+ > `app/` **or** `src/app/` — either works with no config. Where UI primitives live and how a route
15
+ > group is named are house choices on top of that, not a framework requirement. Materialize any
16
+ > part of this into `docs/ach.md` and `docs/afm.md` §3 only if the operator accepts it — item by
17
+ > item, at bootstrap. Nothing here is `(mandatory)`.
18
+
19
+ ## The convention
20
+
21
+ - Root the App Router at `src/app/`, not the repo-root `app/` — keeps every source file under one
22
+ `src/` tree, alongside `src/lib/`, `src/server/`, and the like.
23
+ - Put UI primitives (shadcn-style components, feature components) in `src/components/`. No direct
24
+ fetch — data arrives through the project's transport client, never a component-level `fetch`.
25
+ - Group authenticated routes under a named route group, for example `(protected)/`, and
26
+ unauthenticated ones under a plain folder, for example `auth/` — so the URL path and the auth
27
+ boundary read the same way in the file tree.
28
+
29
+ ## Why this works
30
+
31
+ `src/` as a single root means a contributor never has to ask "is this file at the repo root or
32
+ under `src/`" — there is one answer, and tooling (a `tsconfig.json` `baseUrl`, an ESLint
33
+ `import/no-relative-parent-imports` rule) can assume it. A named route group for the auth boundary
34
+ makes the split visible in a file listing, before opening any file.
35
+
36
+ ## The trade-off
37
+
38
+ Next.js's own default is `app/` at the repo root, with no `src/` at all — a project that starts
39
+ there and later adds a `src/` tree for everything else pays a one-time move, and any doc or
40
+ tutorial written against the plain-`app/` default needs a mental translation. Route-group naming
41
+ is invisible to the URL (Next.js strips the parens), so it only pays off if the team actually
42
+ reads folder names during review; a team that navigates by IDE search alone gets no benefit from
43
+ the convention and only the extra folder nesting.
44
+
45
+ ## If the operator accepts this
46
+
47
+ Materialize the tree into `docs/ach.md` §2 and the route-group convention into §6. There is no
48
+ mechanical check for "is this the right root" — Next.js accepts either — so this stays a §1.3
49
+ principle, not a §3 hard rule, unless the project also adds a lint rule that fails on the
50
+ unwanted root.
@@ -0,0 +1,55 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Proposes a soft-delete convention — a deletedAt column, a filter-by-default query rule, and a scheduled hard-delete job — as one boundary a query must respect, not a Prisma requirement.'
4
+ load: cold
5
+ stack: prisma-postgres
6
+ concern: error-boundary
7
+ detect: |
8
+ test -f prisma/schema.prisma
9
+ ---
10
+
11
+ # Code style — Prisma + Postgres soft-delete boundary
12
+
13
+ > Soft delete is a **design choice**, not a Prisma fact — Prisma has no opinion on whether a row
14
+ > ever leaves the table. This is a proposal for one way to implement it, framed as a boundary a
15
+ > query must respect: a soft-deleted row must not cross from storage into application code unless
16
+ > the caller explicitly asked for it. Materialize it into `docs/ach.md` and `docs/afm.md` §3 only
17
+ > if the operator accepts it. Nothing here is `(mandatory)`.
18
+
19
+ ## The convention
20
+
21
+ - Add `deletedAt: DateTime?` to a model that needs soft delete.
22
+ - Mark the retention SLA with a schema comment next to the column, for example
23
+ `// hard-delete after 180d`.
24
+ - Every query against a soft-deletable model filters `deletedAt: null` by default. An admin or
25
+ audit path may opt out explicitly — never silently.
26
+ - A scheduled job performs the actual hard delete once the SLA elapses, separate from the request
27
+ path that sets `deletedAt`.
28
+
29
+ ## Why this works
30
+
31
+ Without a default filter, a soft-deleted row is indistinguishable from a live one to every query
32
+ that does not remember to exclude it — and that memory is exactly what a hard rule replaces. Two
33
+ grep-able boundaries make it mechanical: which models declare `deletedAt` at all, and whether
34
+ every read against them filters it.
35
+
36
+ ## The trade-off
37
+
38
+ A default filter on every query is easy to forget for a *new* soft-deletable model — the check
39
+ below only fires once a model already has `deletedAt`, so nothing enforces adding the column in
40
+ the first place. It also adds a `WHERE` clause and often a partial index to every query against
41
+ that model, for a feature many projects never use (a hard delete plus an audit-log row is simpler
42
+ when no product requirement asks for undo or a retention window). A project without that
43
+ requirement should skip this entirely rather than add the column speculatively.
44
+
45
+ ## If the operator accepts this
46
+
47
+ Materialize the convention into `docs/ach.md` §6 and add a hard rule to `docs/afm.md` §3, verified
48
+ by:
49
+ ```sh
50
+ grep -rn "deletedAt" {{src-path}}/server/modules/
51
+ ```
52
+ Every match is a query against a soft-deletable model; each one either filters `deletedAt: null`
53
+ or is an admin/audit path that says so in a comment. List the models that actually declare
54
+ `deletedAt` first (`grep -n "deletedAt" prisma/schema.prisma` or the project's schema files) —
55
+ the check only means something once at least one model has the column.
@@ -0,0 +1,61 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Proposes a schema-file split, a generated-client output path, and three layer-direction import rules for a Prisma + Postgres project.'
4
+ load: cold
5
+ stack: prisma-postgres
6
+ concern: imports
7
+ detect: |
8
+ test -f prisma/schema.prisma
9
+ ---
10
+
11
+ # Code style — Prisma + Postgres schema layout and import direction
12
+
13
+ > This is a **proposal**, not a fact about Prisma. Prisma itself only requires one
14
+ > `schema.prisma` file that a `generator client` block points at some output path — where split
15
+ > files live, what the output folder is called, and which layer may import the generated client
16
+ > directly are all house choices. Materialize any part of this into `docs/ach.md` and
17
+ > `docs/afm.md` §3 only if the operator accepts it — item by item, at bootstrap. Nothing here is
18
+ > `(mandatory)`.
19
+
20
+ ## The convention
21
+
22
+ **Schema layout**
23
+ - Split the schema by domain: `prisma/schemas/<module>.prisma`, one file per domain, merged by a
24
+ `prisma.config.ts` (Prisma 5.15+ multi-file schema).
25
+ - Output the generated client to `.prisma/` at the repo root (gitignored). Reference it with a
26
+ relative import: `../../.prisma/client`, `../../.prisma/enums`.
27
+
28
+ **Import direction**
29
+ - `{{src-path}}/server/modules/<f>/domain/*` may import types from `.prisma/enums`. It does not
30
+ import the Prisma client directly — it receives `db` through `ctx`.
31
+ - `{{src-path}}/lib/*` never imports `.prisma/*` — `lib/` is publishable outside this project, and
32
+ a generated-client import would tie it back to this schema.
33
+ - `{{src-path}}/trigger/*` may import `.prisma/*` through a context-constructor helper, if the
34
+ project has one.
35
+
36
+ ## Why this works
37
+
38
+ A fixed schema-split location and a single generated-client path give every contributor the same
39
+ two answers ("where do I add a model", "where does the client come from") without asking. The
40
+ import-direction rules turn "domain code should not couple to Prisma's client type, and `lib/`
41
+ should stay portable" from a review-time reminder into something a hard rule can check by path.
42
+
43
+ ## The trade-off
44
+
45
+ None of this is required by Prisma itself. A project with a single-schema file, a different
46
+ output path (Prisma's own default is `node_modules/.prisma/client`), or no `lib/`/`trigger/`
47
+ split at all gets nothing from importing this layout — the value is internal consistency, not a
48
+ Prisma constraint. The `db`-through-`ctx` rule also asks every domain function to accept a `ctx`
49
+ argument even where it feels like ceremony for a one-line query; a smaller project may prefer the
50
+ simplicity of importing the client directly and accept the harder-to-mock trade-off instead.
51
+
52
+ ## If the operator accepts this
53
+
54
+ Materialize the layout into `docs/ach.md` §2 (the folder tree) and the import rules into
55
+ `docs/afm.md` §3, verified per rule by:
56
+ ```sh
57
+ grep -rnE "from.*prisma/client|new PrismaClient" {{src-path}}/server/modules/*/domain/
58
+ grep -rn "\.prisma/" {{src-path}}/lib/
59
+ ```
60
+ Both must return 0 for the rule to hold; fill `{{src-path}}` with the project's actual root
61
+ before pasting either grep — an unresolved placeholder matches nothing and passes vacuously.
@@ -0,0 +1,48 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Proposes separate directories for copied shadcn primitives and for feature components that compose them.'
4
+ load: cold
5
+ stack: shadcn-ui
6
+ concern: naming
7
+ detect: |
8
+ test -f components.json
9
+ ---
10
+
11
+ # Code style — shadcn primitive vs. feature component layout
12
+
13
+ > This is a **proposal**, not a fact about shadcn. Materialize it into `docs/ach.md` only if the
14
+ > operator accepts it — item by item, at bootstrap. Nothing here is `(mandatory)`.
15
+
16
+ ## The convention
17
+
18
+ ```
19
+ src/
20
+ components/
21
+ ui/ # shadcn primitives — CLI-copied, then edited in place
22
+ <feature>/ # components that compose primitives with feature logic
23
+ ```
24
+
25
+ `components/ui/` holds only what `npx shadcn add` copied in, plus your in-place edits to it.
26
+ Anything that combines a primitive with data, feature logic, or app-specific composition goes in
27
+ its own `components/<feature>/` directory instead.
28
+
29
+ ## Why this works
30
+
31
+ Keeping the copied primitives in one directory makes it obvious, at a glance, which files are
32
+ "vendored" shadcn code you have edited versus app code written from scratch — useful the day you
33
+ want to diff a primitive against an upstream shadcn update.
34
+
35
+ ## The trade-off
36
+
37
+ A project with only a handful of components total does not need the split yet — one flat
38
+ `components/` directory is simpler until the primitive/feature distinction starts to matter. A
39
+ design system that already dictates its own component directory convention (the `vireya-ui`
40
+ module, for one) should not have this layered on top of it — the two conventions are for
41
+ different stacks and are not meant to combine.
42
+
43
+ ## If the operator accepts this
44
+
45
+ Materialize the directory split into `docs/ach.md`, under the UI primitive component type. This
46
+ convention has no mechanical check of its own — `components.json`'s `aliases.ui` path is the
47
+ only part shadcn itself enforces, and nothing distinguishes a "feature" component from a
48
+ misplaced primitive except review.
@@ -0,0 +1,76 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Proposes a file layout and naming scheme for a Stripe billing module: one way to organize it, not a Stripe requirement.'
4
+ load: cold
5
+ stack: stripe-billing
6
+ concern: naming
7
+ detect: |
8
+ if command -v jq >/dev/null 2>&1; then jq -e '(.dependencies//{})+(.devDependencies//{})+(.peerDependencies//{})|has("stripe")' package.json >/dev/null 2>&1; else grep -q '"stripe"' package.json 2>/dev/null; fi
9
+ ---
10
+
11
+ # Code style — Stripe billing naming and layout
12
+
13
+ > Seed for `docs/code-style/stripe-billing/naming.md`. A proposal, materialized only if the
14
+ > operator accepts it during the interview. Stripe imposes none of this; it is one way to
15
+ > organize a multi-plan billing module, offered so a project does not have to invent the layout
16
+ > from zero.
17
+
18
+ ## Proposal: an SDK-wrapper directory with a prefixed export name
19
+
20
+ - Put every direct call into the Stripe SDK behind a thin wrapper, in its own directory (for
21
+ example `src/lib/stripe/`), so a reader can tell at a glance which functions cross the network
22
+ and which are local logic.
23
+ - Name every exported wrapper function with the same lowercase prefix (for example
24
+ `stripe_createCustomer`, `stripe_verifyWebhook`), so a call site reads as "this touches Stripe"
25
+ without opening the import.
26
+ - Export one client-getter function (for example `stripe_getClient()`) instead of a module-level
27
+ singleton instance. A getter is easier to mock in a test and easier to reconfigure per request
28
+ (multi-tenant API keys, test mode) than a value captured at import time.
29
+
30
+ **Trade-off.** The prefix is redundant once the directory name already says "stripe" — a project
31
+ that values shorter names over grep-ability may skip it and rely on the import path alone. The
32
+ getter-over-singleton choice costs one extra function call at every use site; a project with a
33
+ single, static API key and no test doubles to satisfy may prefer the plain singleton.
34
+
35
+ ## Proposal: a dedicated billing domain with a capability matrix
36
+
37
+ For a subscription product with more than one paid plan, keep the mapping from plan to
38
+ capabilities in one place instead of scattering `if (plan === 'pro')` checks through the
39
+ codebase:
40
+
41
+ - A **plan-to-price config**: one constant per paid plan (lookup key, amount, interval), plus a
42
+ map from the app's own plan identifier to that config, and the reverse lookup (a Stripe Price
43
+ lookup key back to the app's plan identifier) that a webhook handler needs.
44
+ - A **capability matrix**: one file that is the single source of truth for which capability each
45
+ plan unlocks — a table or map from plan to a set of boolean flags, one flag per gated feature.
46
+ - A **billing-state resolver**: a pure function from the current subscription (status, plan) to
47
+ the resolved capability flags. A subscription in good standing (active, trialing, or a
48
+ grace-period past-due) carries its plan's capabilities; a suspended subscription (unpaid,
49
+ incomplete, paused) drops to the free tier's capabilities even while the stored plan label
50
+ still shows what the customer was nominally on, so an account page can say "you were on the
51
+ Pro plan" instead of silently forgetting it.
52
+ - A **webhook dispatcher** that resolves the plan from the event's Price lookup key and calls the
53
+ resolver.
54
+
55
+ **Trade-off.** This earns the indirection once there are two or more paid plans with different
56
+ capabilities — below that, a single `isPaid: boolean` flag is simpler, and the matrix is YAGNI.
57
+ The four-file split also assumes a `domain/` layer already exists in this codebase's module
58
+ convention; a project without one should collapse it to fewer files rather than manufacture the
59
+ layer just to hold this pattern.
60
+
61
+ ## Proposal: gate every capability check through one accessor shape, server-side
62
+
63
+ Expose the resolved capability flags on the request context under one dot-path (for example
64
+ `ctx.user.billing.canX`), and have every procedure that performs a gated action check that flag
65
+ before it runs — never only in the frontend, since a client-side check is UX, not a boundary an
66
+ attacker cannot cross.
67
+
68
+ *Suggested verification, once a project adopts this shape (point the glob at where this
69
+ project's procedures actually live):* `grep -rnE "billing\.can" <procedures-dir>` should show
70
+ every gated procedure checking the flag before it acts.
71
+
72
+ **Trade-off.** A single dot-path accessor is convenient once many procedures gate on it, but it
73
+ couples every caller to that exact context shape. A project that already has its own
74
+ authorization layer (a policy object, a decorator, a middleware) should express the same
75
+ "check server-side, never trust the client" principle through its own shape instead of adopting
76
+ this one.
@@ -0,0 +1,58 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Proposes an import boundary for task files and a shared context helper for their DB access.'
4
+ load: cold
5
+ stack: trigger-dev-v4
6
+ concern: imports
7
+ detect: |
8
+ if command -v jq >/dev/null 2>&1; then jq -e '(.dependencies//{})+(.devDependencies//{})+(.peerDependencies//{})|has("@trigger.dev/sdk")' package.json >/dev/null 2>&1; else grep -q '"@trigger.dev/sdk"' package.json 2>/dev/null; fi
9
+ ---
10
+
11
+ # Code style — task import boundary and context helper
12
+
13
+ > This is a **proposal**, not a fact about Trigger.dev. Materialize it into `docs/ach.md` and
14
+ > `docs/afm.md` §3 only if the operator accepts it — item by item, at bootstrap. Nothing here is
15
+ > `(mandatory)`.
16
+
17
+ ## The convention
18
+
19
+ A task file may import `lib/`, `domain/`, and the ORM client, but not the request-layer
20
+ procedures (tRPC routers, REST handlers, and so on) — a task is triggered, not called, so
21
+ importing a procedure the same way an HTTP handler would defeats the boundary the task runs
22
+ behind.
23
+
24
+ To avoid every task constructing its own DB client by hand, route the ones that need one through
25
+ a single helper that wraps the ORM singleton, for example:
26
+
27
+ ```ts
28
+ // src/lib/trigger.ts
29
+ export function createTriggerContext() {
30
+ return { db: prismaClient /* , ...whatever else every task needs */ };
31
+ }
32
+ ```
33
+
34
+ ## Why this works
35
+
36
+ One place constructs the client, so pool sizing and connection reuse are decided once instead of
37
+ once per task file. It also gives every task the same starting shape, which lowers the cost of
38
+ reading the next one.
39
+
40
+ ## The trade-off
41
+
42
+ A single task, or a set of tasks with no DB access at all, does not need a shared helper — a
43
+ direct import is simpler, and the helper would be a speculative abstraction (YAGNI). A project
44
+ that already has a context-building convention for its request layer gains nothing from a second,
45
+ task-only version — reuse the existing one instead. The import boundary itself is also only as
46
+ good as the discipline behind it: nothing in Trigger.dev enforces it, so a project that adopts it
47
+ needs a mechanical check, not just a convention nobody reads.
48
+
49
+ ## If the operator accepts this
50
+
51
+ - Materialize the import boundary and the context-helper pattern into `docs/ach.md`, under the
52
+ Task component type.
53
+ - Add a hard rule to `docs/afm.md` §3, verified by (point the glob at where this project's tasks
54
+ actually live):
55
+ ```sh
56
+ grep -rnE "prismaClient|new PrismaClient" {{src-path}}/trigger/
57
+ ```
58
+ Zero results required, except the file that **defines** the context helper.
@@ -0,0 +1,49 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Proposes a directory tree and file-naming pattern for Trigger.dev v4 tasks.'
4
+ load: cold
5
+ stack: trigger-dev-v4
6
+ concern: naming
7
+ detect: |
8
+ if command -v jq >/dev/null 2>&1; then jq -e '(.dependencies//{})+(.devDependencies//{})+(.peerDependencies//{})|has("@trigger.dev/sdk")' package.json >/dev/null 2>&1; else grep -q '"@trigger.dev/sdk"' package.json 2>/dev/null; fi
9
+ ---
10
+
11
+ # Code style — Trigger.dev v4 task layout
12
+
13
+ > This is a **proposal**, not a fact about Trigger.dev. Materialize it into `docs/ach.md` only if
14
+ > the operator accepts it — item by item, at bootstrap. Nothing here is `(mandatory)`.
15
+
16
+ ## The convention
17
+
18
+ ```
19
+ src/
20
+ trigger/ # Trigger.dev v4 tasks
21
+ cron-<X>.ts # cron / schedules
22
+ <stage>-<entity>.ts # subtasks
23
+ start-<entity>-workflow.ts # orchestrators
24
+ ```
25
+
26
+ One directory, one file per task, and the file name states the task's role (`cron-`,
27
+ `start-…-workflow`, or a bare `<stage>-<entity>`) before you open it.
28
+
29
+ ## Why this works
30
+
31
+ Grepping `src/trigger/` lists every task the project runs, in one place, sorted by what kind
32
+ each one is — no need to open a file to learn whether it is a cron, an orchestrator, or a
33
+ subtask.
34
+
35
+ ## The trade-off
36
+
37
+ A monorepo that already splits tasks per package has its own layout, and forcing this tree onto
38
+ it adds a migration with no payoff. A project with only one or two tasks does not need a
39
+ dedicated subtree yet — inline them where they are triggered from, and adopt this once a third
40
+ task shows up (Rule of Three). The prefixes themselves are also redundant with the `id` passed to
41
+ `tasks.task(...)`, which already states the same thing at the call site.
42
+
43
+ ## If the operator accepts this
44
+
45
+ Materialize the tree and the three file-name prefixes into `docs/ach.md`, under the Task
46
+ component type. This convention has no mechanical check of its own beyond the directory existing
47
+ — naming a task file correctly is check-able only by parsing each file's exported task `id`
48
+ against its filename, which duplicates the kebab-case convention already stated as fact in
49
+ `docs/ach.md`. Do not add a redundant hard rule for the prefixes alone.
@@ -0,0 +1,61 @@
1
+ ---
2
+ type: code-style
3
+ description: 'Proposes co-locating an idempotency test with each task, and an atomic-upsert pattern for the state it guards.'
4
+ load: cold
5
+ stack: trigger-dev-v4
6
+ concern: test-placement
7
+ detect: |
8
+ if command -v jq >/dev/null 2>&1; then jq -e '(.dependencies//{})+(.devDependencies//{})+(.peerDependencies//{})|has("@trigger.dev/sdk")' package.json >/dev/null 2>&1; else grep -q '"@trigger.dev/sdk"' package.json 2>/dev/null; fi
9
+ ---
10
+
11
+ # Code style — Trigger.dev v4 idempotency test placement
12
+
13
+ > This is a **proposal**, not a fact about Trigger.dev. Materialize it into `docs/ach.md` and
14
+ > `docs/afm.md` §3 only if the operator accepts it — item by item, at bootstrap. Nothing here is
15
+ > `(mandatory)`.
16
+
17
+ ## The convention
18
+
19
+ **Neighbor test.** Every task a rerun could touch twice (a webhook retry, a `triggerAndWait`
20
+ failure that reruns its parent, a manual replay from the dashboard) gets a test co-located with
21
+ it — `<task>.test.ts` in the same directory as `<task>.ts` — asserting that a second run does not
22
+ corrupt state: an early return on a terminal state, or a no-op on an already-applied write.
23
+
24
+ **One way to make the write itself safe:** an atomic upsert guarded by the field that tracks the
25
+ run:
26
+
27
+ ```ts
28
+ await db.thing.updateMany({
29
+ where: { id, runId: null },
30
+ data: { runId },
31
+ });
32
+ ```
33
+
34
+ `runId` is written once; a rerun with the same `runId` finds no row where it is still `null`, so
35
+ it updates nothing.
36
+
37
+ ## Why this works
38
+
39
+ A task that silently double-applies its write on a retry is a production incident, not a style
40
+ nit — Trigger.dev retries by design, so idempotency is the one thing every task must get right.
41
+ Testing it next to the task keeps the two in sync when either one changes.
42
+
43
+ ## The trade-off
44
+
45
+ A task with no side effect a rerun could double-apply (a pure fan-out, a read-only report) does
46
+ not need this test — write one only where a second run has something to corrupt. A project whose
47
+ test layout is centralized instead of co-located should keep the idempotency test there too — the
48
+ co-location is the suggestion, the idempotency test itself is not. The `updateMany`/`runId`
49
+ pattern also assumes a DB row already exists to guard; a task whose idempotency depends on an
50
+ external system (an upstream API that is itself idempotent by request key) needs a different
51
+ guard entirely.
52
+
53
+ ## If the operator accepts this
54
+
55
+ - Materialize the neighbor-test convention into `docs/ach.md`, under the Task component type.
56
+ - Add a hard rule to `docs/afm.md` §3, verified by (point the glob at where this project's tasks
57
+ actually live):
58
+ ```sh
59
+ grep -rLE "rerun|retry|already.*processed" {{src-path}}/trigger/*.test.ts
60
+ ```
61
+ Lists tasks with no idempotency test — review the list, it is not a pass/fail count.