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

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 (276) 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 +4256 -0
  6. package/content/PRINCIPLES.md +121 -0
  7. package/content/README-light.md +160 -0
  8. package/content/README.md +152 -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 +265 -0
  27. package/content/core/afm.md +496 -0
  28. package/content/core/gotchas.md +18 -0
  29. package/content/core/prd.md +143 -0
  30. package/content/core/protocol.md +160 -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 +72 -0
  37. package/content/interview/bootstrap.md +415 -0
  38. package/content/interview/promotion.md +117 -0
  39. package/content/interview/refactor.md +381 -0
  40. package/content/interview/triggers.md +109 -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 +129 -0
  60. package/content/ops/clarify.md +86 -0
  61. package/content/ops/deliver.md +211 -0
  62. package/content/ops/diagnose.md +146 -0
  63. package/content/ops/evolve.md +317 -0
  64. package/content/ops/export-template.md +97 -0
  65. package/content/ops/generalize.md +120 -0
  66. package/content/ops/gotcha.md +46 -0
  67. package/content/ops/plan.md +61 -0
  68. package/content/ops/policy.md +63 -0
  69. package/content/ops/promote.md +72 -0
  70. package/content/ops/protocol.md +51 -0
  71. package/content/ops/reconcile.md +44 -0
  72. package/content/ops/reflect.md +180 -0
  73. package/content/ops/remediate.md +146 -0
  74. package/content/ops/restructure.md +145 -0
  75. package/content/ops/rule.md +165 -0
  76. package/content/ops/specify.md +62 -0
  77. package/content/ops/tasks.md +67 -0
  78. package/content/ops/update.md +223 -0
  79. package/content/ops/verify.md +69 -0
  80. package/content/patterns/adapter.md +46 -0
  81. package/content/patterns/facade.md +47 -0
  82. package/content/patterns/factory.md +46 -0
  83. package/content/patterns/null-object.md +55 -0
  84. package/content/patterns/result-vs-exception.md +52 -0
  85. package/content/patterns/strategy.md +45 -0
  86. package/content/patterns/template-method-vs-composition.md +50 -0
  87. package/content/questions/adr.questions.yaml +14 -0
  88. package/content/questions/analyze.questions.yaml +12 -0
  89. package/content/questions/bootstrap-mapping.questions.yaml +14 -0
  90. package/content/questions/bootstrap-rules.questions.yaml +10 -0
  91. package/content/questions/bootstrap-stack.questions.yaml +23 -0
  92. package/content/questions/clarify.questions.yaml +10 -0
  93. package/content/questions/code-style.questions.yaml +12 -0
  94. package/content/questions/confirm.questions.yaml +12 -0
  95. package/content/questions/deliver.questions.yaml +23 -0
  96. package/content/questions/evolve.questions.yaml +15 -0
  97. package/content/questions/export-template-apply.questions.yaml +11 -0
  98. package/content/questions/export-template.questions.yaml +23 -0
  99. package/content/questions/gates.questions.yaml +27 -0
  100. package/content/questions/generalize.questions.yaml +13 -0
  101. package/content/questions/gotcha.questions.yaml +11 -0
  102. package/content/questions/init-template.questions.yaml +10 -0
  103. package/content/questions/init.questions.yaml +28 -0
  104. package/content/questions/plan.questions.yaml +14 -0
  105. package/content/questions/refactor.questions.yaml +23 -0
  106. package/content/questions/reflect.questions.yaml +13 -0
  107. package/content/questions/remediate.questions.yaml +27 -0
  108. package/content/questions/restructure.questions.yaml +15 -0
  109. package/content/questions/rf-mapping.questions.yaml +12 -0
  110. package/content/questions/rule-scope.questions.yaml +12 -0
  111. package/content/questions/rule.questions.yaml +12 -0
  112. package/content/questions/specify.questions.yaml +23 -0
  113. package/content/questions/update-artifacts.questions.yaml +10 -0
  114. package/content/questions/update-strings.questions.yaml +14 -0
  115. package/content/questions/update.questions.yaml +10 -0
  116. package/content/questions/verify.questions.yaml +9 -0
  117. package/content/roles/executor.md +18 -0
  118. package/content/roles/explorer.md +22 -0
  119. package/content/roles/reviewer.md +23 -0
  120. package/content/rubrics/enum-vs-union-vs-branded.md +40 -0
  121. package/content/rubrics/episodic-vs-semantic-boundary.md +37 -0
  122. package/content/rubrics/error-classification.md +98 -0
  123. package/content/rubrics/failure-classification.md +113 -0
  124. package/content/rubrics/negative-filters.md +38 -0
  125. package/content/rubrics/solid-triggers.md +121 -0
  126. package/content/rubrics/template-vs-streaming-precedence.md +34 -0
  127. package/content/rubrics/validation-boundary.md +94 -0
  128. package/content/rubrics/when-to-create-dsl.md +65 -0
  129. package/content/rubrics/when-to-create-lib.md +49 -0
  130. package/content/rubrics/when-to-create-module.md +39 -0
  131. package/content/rubrics/when-to-evolve-methodology.md +62 -0
  132. package/content/skills/adr.md +42 -0
  133. package/content/skills/analyze.md +34 -0
  134. package/content/skills/clarify.md +34 -0
  135. package/content/skills/deliver.md +42 -0
  136. package/content/skills/diagnose.md +50 -0
  137. package/content/skills/evolve.md +67 -0
  138. package/content/skills/export-template.md +40 -0
  139. package/content/skills/generalize.md +47 -0
  140. package/content/skills/gotcha.md +40 -0
  141. package/content/skills/init.md +82 -0
  142. package/content/skills/plan.md +34 -0
  143. package/content/skills/policy.md +31 -0
  144. package/content/skills/promote.md +41 -0
  145. package/content/skills/protocol.md +42 -0
  146. package/content/skills/reconcile.md +49 -0
  147. package/content/skills/refactor.md +84 -0
  148. package/content/skills/reflect.md +53 -0
  149. package/content/skills/remediate.md +39 -0
  150. package/content/skills/restructure.md +52 -0
  151. package/content/skills/rule.md +44 -0
  152. package/content/skills/specify.md +34 -0
  153. package/content/skills/tasks.md +34 -0
  154. package/content/skills/update.md +56 -0
  155. package/content/skills/verify.md +31 -0
  156. package/content/templates/adr.template.md +51 -0
  157. package/content/templates/evolution-report.template.md +129 -0
  158. package/content/templates/feature-plan.template.md +86 -0
  159. package/content/templates/feature-research.template.md +57 -0
  160. package/content/templates/feature-spec.template.md +86 -0
  161. package/content/templates/feature-tasks.induced.template.md +67 -0
  162. package/content/templates/feature-tasks.template.md +60 -0
  163. package/content/templates/focus.template.md +30 -0
  164. package/content/templates/gotcha.template.md +31 -0
  165. package/content/templates/guardrail-runner.template.md +81 -0
  166. package/content/templates/guardrail.template.md +63 -0
  167. package/content/templates/learning.template.md +40 -0
  168. package/content/templates/procedure.template.md +47 -0
  169. package/content/templates/protocols/p1.md +29 -0
  170. package/content/templates/protocols/p2.md +28 -0
  171. package/content/templates/protocols/p3.md +47 -0
  172. package/content/templates/research.template.md +50 -0
  173. package/content/templates/session-handoff.template.md +58 -0
  174. package/content/templates/us.template.md +39 -0
  175. package/content/tools/gate/core-rules/PLUGIN-13-secrets.sh +36 -0
  176. package/content/tools/gate/core-rules/PLUGIN-21-code-language.sh +27 -0
  177. package/content/tools/gate/core-rules/PLUGIN-35-gate-complete.sh +19 -0
  178. package/content/tools/gate/core-rules/PLUGIN-36-absolute-path.sh +38 -0
  179. package/content/tools/gate/core-rules/PLUGIN-37-prose-language.sh +56 -0
  180. package/content/tools/gate/core-rules/PLUGIN-38-parser-marker.sh +82 -0
  181. package/content/tools/gate/core-rules/PLUGIN-39-frontmatter-ops.sh +325 -0
  182. package/content/tools/gate/core-rules/PLUGIN-40-dead-reference.sh +74 -0
  183. package/content/tools/gate/core-rules/README.md +55 -0
  184. package/content/tools/gate/guardrails.sh +740 -0
  185. package/content/tools/gate/lib/ratchet.sh +119 -0
  186. package/content/tools/gate/lib/scope.sh +85 -0
  187. package/content/tools/gate/ratchet.seed.tsv +1 -0
  188. package/content/tools/gate/rules/README.md +50 -0
  189. package/package.json +19 -5
  190. package/plugin/agy/hooks.json +29 -0
  191. package/plugin/agy/mcp_config.json +7 -0
  192. package/plugin/agy/plugin.json +6 -0
  193. package/plugin/agy/skills/afm-adr/SKILL.md +44 -0
  194. package/plugin/agy/skills/afm-analyze/SKILL.md +36 -0
  195. package/plugin/agy/skills/afm-clarify/SKILL.md +36 -0
  196. package/plugin/agy/skills/afm-deliver/SKILL.md +44 -0
  197. package/plugin/agy/skills/afm-diagnose/SKILL.md +50 -0
  198. package/plugin/agy/skills/afm-evolve/SKILL.md +69 -0
  199. package/plugin/agy/skills/afm-export-template/SKILL.md +42 -0
  200. package/plugin/agy/skills/afm-generalize/SKILL.md +49 -0
  201. package/plugin/agy/skills/afm-gotcha/SKILL.md +42 -0
  202. package/plugin/agy/skills/afm-init/SKILL.md +84 -0
  203. package/plugin/agy/skills/afm-plan/SKILL.md +36 -0
  204. package/plugin/agy/skills/afm-policy/SKILL.md +31 -0
  205. package/plugin/agy/skills/afm-promote/SKILL.md +41 -0
  206. package/plugin/agy/skills/afm-protocol/SKILL.md +42 -0
  207. package/plugin/agy/skills/afm-reconcile/SKILL.md +49 -0
  208. package/plugin/agy/skills/afm-refactor/SKILL.md +86 -0
  209. package/plugin/agy/skills/afm-reflect/SKILL.md +55 -0
  210. package/plugin/agy/skills/afm-remediate/SKILL.md +41 -0
  211. package/plugin/agy/skills/afm-restructure/SKILL.md +54 -0
  212. package/plugin/agy/skills/afm-rule/SKILL.md +46 -0
  213. package/plugin/agy/skills/afm-specify/SKILL.md +36 -0
  214. package/plugin/agy/skills/afm-tasks/SKILL.md +34 -0
  215. package/plugin/agy/skills/afm-update/SKILL.md +58 -0
  216. package/plugin/agy/skills/afm-verify/SKILL.md +33 -0
  217. package/plugin/claude-code/.claude-plugin/marketplace.json +15 -0
  218. package/plugin/claude-code/.claude-plugin/plugin.json +9 -0
  219. package/plugin/claude-code/.mcp.json +7 -0
  220. package/plugin/claude-code/agents/afm-executor.md +18 -0
  221. package/plugin/claude-code/agents/afm-explorer.md +20 -0
  222. package/plugin/claude-code/agents/afm-reviewer.md +21 -0
  223. package/plugin/claude-code/hooks/hooks.json +85 -0
  224. package/plugin/claude-code/skills/adr/SKILL.md +44 -0
  225. package/plugin/claude-code/skills/analyze/SKILL.md +36 -0
  226. package/plugin/claude-code/skills/clarify/SKILL.md +36 -0
  227. package/plugin/claude-code/skills/deliver/SKILL.md +44 -0
  228. package/plugin/claude-code/skills/diagnose/SKILL.md +50 -0
  229. package/plugin/claude-code/skills/evolve/SKILL.md +69 -0
  230. package/plugin/claude-code/skills/export-template/SKILL.md +42 -0
  231. package/plugin/claude-code/skills/generalize/SKILL.md +49 -0
  232. package/plugin/claude-code/skills/gotcha/SKILL.md +42 -0
  233. package/plugin/claude-code/skills/init/SKILL.md +84 -0
  234. package/plugin/claude-code/skills/plan/SKILL.md +36 -0
  235. package/plugin/claude-code/skills/policy/SKILL.md +31 -0
  236. package/plugin/claude-code/skills/promote/SKILL.md +41 -0
  237. package/plugin/claude-code/skills/protocol/SKILL.md +42 -0
  238. package/plugin/claude-code/skills/reconcile/SKILL.md +49 -0
  239. package/plugin/claude-code/skills/refactor/SKILL.md +86 -0
  240. package/plugin/claude-code/skills/reflect/SKILL.md +55 -0
  241. package/plugin/claude-code/skills/remediate/SKILL.md +41 -0
  242. package/plugin/claude-code/skills/restructure/SKILL.md +54 -0
  243. package/plugin/claude-code/skills/rule/SKILL.md +46 -0
  244. package/plugin/claude-code/skills/specify/SKILL.md +36 -0
  245. package/plugin/claude-code/skills/tasks/SKILL.md +34 -0
  246. package/plugin/claude-code/skills/update/SKILL.md +58 -0
  247. package/plugin/claude-code/skills/verify/SKILL.md +33 -0
  248. package/plugin/opencode/afm.js +196 -0
  249. package/plugin/opencode/agents/afm-executor.md +31 -0
  250. package/plugin/opencode/agents/afm-explorer.md +47 -0
  251. package/plugin/opencode/agents/afm-reviewer.md +48 -0
  252. package/plugin/opencode/skills/afm-adr/SKILL.md +44 -0
  253. package/plugin/opencode/skills/afm-analyze/SKILL.md +36 -0
  254. package/plugin/opencode/skills/afm-clarify/SKILL.md +36 -0
  255. package/plugin/opencode/skills/afm-deliver/SKILL.md +44 -0
  256. package/plugin/opencode/skills/afm-diagnose/SKILL.md +50 -0
  257. package/plugin/opencode/skills/afm-evolve/SKILL.md +69 -0
  258. package/plugin/opencode/skills/afm-export-template/SKILL.md +42 -0
  259. package/plugin/opencode/skills/afm-generalize/SKILL.md +49 -0
  260. package/plugin/opencode/skills/afm-gotcha/SKILL.md +42 -0
  261. package/plugin/opencode/skills/afm-init/SKILL.md +84 -0
  262. package/plugin/opencode/skills/afm-plan/SKILL.md +36 -0
  263. package/plugin/opencode/skills/afm-policy/SKILL.md +31 -0
  264. package/plugin/opencode/skills/afm-promote/SKILL.md +41 -0
  265. package/plugin/opencode/skills/afm-protocol/SKILL.md +42 -0
  266. package/plugin/opencode/skills/afm-reconcile/SKILL.md +49 -0
  267. package/plugin/opencode/skills/afm-refactor/SKILL.md +86 -0
  268. package/plugin/opencode/skills/afm-reflect/SKILL.md +55 -0
  269. package/plugin/opencode/skills/afm-remediate/SKILL.md +41 -0
  270. package/plugin/opencode/skills/afm-restructure/SKILL.md +54 -0
  271. package/plugin/opencode/skills/afm-rule/SKILL.md +46 -0
  272. package/plugin/opencode/skills/afm-specify/SKILL.md +36 -0
  273. package/plugin/opencode/skills/afm-tasks/SKILL.md +34 -0
  274. package/plugin/opencode/skills/afm-update/SKILL.md +58 -0
  275. package/plugin/opencode/skills/afm-verify/SKILL.md +33 -0
  276. package/README.md +0 -3
@@ -0,0 +1,223 @@
1
+ ---
2
+ type: op
3
+ description: Brings a materialized instance up to the current plugin version.
4
+ load: warm
5
+ writes: ["docs/.afm-version", "docs/evolution/*.md"]
6
+ composes: []
7
+ ---
8
+ # Sub-flow: sync of the materialized instance with the current plugin version (`update`)
9
+
10
+ > **In this file:** What update is / is not · Procedure · What the codemods can do · Instances older than 4.0 · 1.6 Language migration · Opt-in deltas · Post-conditions · Suppression contract · Anti-patterns
11
+
12
+ > **Form** — effect: deltas in the instance + bump · trigger: `afm update`, composition · precondition: pin ≠ the shipped version · composed by: `evolve` 2.2 · **degree of freedom: low**
13
+
14
+ Triggers: `afm update`, or natural language for **version bump / sync with the plugin**. Examples:
15
+ "update AFM here", "bump the methodology version", "sync `docs/` with the new plugin", "migrate to the new
16
+ AFM version", "my `.afm-version` is behind".
17
+
18
+ > **The write target is the local docs (`docs/`), NEVER the plugin.** `afm update` reads the shipped
19
+ > content only to **discover the deltas**. It **materializes** the consumer-facing ones
20
+ > in the local instance. It never edits the plugin (bright line #7). See `afm path ops/evolve` § The
21
+ > target of evolution.
22
+ >
23
+ > **Sync mechanism, not orchestrator.** `update` resolves the **instance-to-plugin version drift** (axis
24
+ > A3, which `afm diagnose` *detects* but does not fix). It runs à la carte (this doc, with its own gate)
25
+ > or inside the `evolve` cycle, which composes it when `afm diagnose` flags A3, with the own gate
26
+ > suppressed (see § Suppression contract).
27
+
28
+ The command owns everything mechanical: reading the pin, comparing versions, running the **codemod
29
+ registry** of every release between the pin and the binary (ADR-0013), proving each op, keeping the manifest
30
+ and moving the pin at the end. This op owns the three things it cannot do: the agent tasks, the language
31
+ migration and the post-conditions.
32
+
33
+ ## When it is another op instead
34
+
35
+ - `init` / `refactor` (bootstrap) **create** the instance from zero. `update` assumes the instance
36
+ **already exists** and only brings it forward. `afm refactor` refuses a project with `.afm-version`;
37
+ `update` is the correct path for that case.
38
+ - `diagnose` only **detects** A3 (read-only). `update` **resolves** it. Diagnose finds it, update fixes it.
39
+ - `evolve` is the full health cycle. `update` is **one lens** (version sync), composed when A3 fires.
40
+ - `promote` **logs** a finding meant for the plugin. `update` goes plugin to **instance**. Neither op
41
+ touches the shipped content: an absolute boundary (#7).
42
+ - Not a resolver of `[TBD]`. Not an inventor of product content (#4/#5). It materializes only what the
43
+ plugin already provides: stack-agnostic rubrics, hygiene rules, template sections. It never fills in a
44
+ product decision.
45
+
46
+ ## Procedure
47
+
48
+ 1. **Preview:** `afm update --dry-run`. It runs the pre-check and prints every codemod with its result
49
+ against an overlay of the docs, writing nothing:
50
+ - Exit 3: no `docs/.afm-version`. Stop and run `afm init` (greenfield) or `afm refactor` (brownfield).
51
+ - Exit 5: the pin and the binary differ by a major version. Run `afm migrate` (the 3.x to 4.0 bridge),
52
+ then `afm update` again.
53
+ - *already up to date*: no drift means nothing to do, and an empty diagnosis is a valid result.
54
+ - *anomaly*: the pin is **ahead** of the binary. Someone edited the instance by hand, or the binary
55
+ is old. Report it, upgrade the binary, and never roll the pin back.
56
+ 2. **Read the preview as one package, not N loose items.** The tally counts what each op did:
57
+ - *applied*: the op wrote (a literal replacement, a move, a created or synced file, a frontmatter key,
58
+ a section, a regenerated surface).
59
+ - *already applied or absent*: the proof of need failed (the old text is not there), so the delta was
60
+ already applied or never existed in this instance. Silent, and not work done.
61
+ - *held*: `[TBD]` content protects the text (#4), or the owner edited a file a codemod would delete.
62
+ **Stop and report it**: the owner's stance wins.
63
+ - *failed*: a proof of application did not hold. The pin stays; fix the cause and run again.
64
+ - *agent task(s) pending*: work only judgment can do (below).
65
+ - a release with nothing to materialize counts toward the bump and prints no line. Do not invent work
66
+ where the plugin declared there is none.
67
+ 3. **Own gate (à la carte).** Show the preview as evidence (which version to which version, what each
68
+ codemod changes in the instance), then ask `afm ask update` (questions in
69
+ `content/questions/update.questions.yaml`). Under the `evolve` cycle this gate is **suppressed**: the
70
+ migration item moves up to the orchestrator's single gate.
71
+ 4. **Apply:** `afm update`. It runs every codemod, proves each one, prints the tally and **moves
72
+ `docs/.afm-version` only at the end**. The exit code tells what is left:
73
+ - Exit 0: done, the pin moved.
74
+ - Exit 1: a codemod failed (or the pin is ahead). The pin stays (marker honesty: never bump as if the
75
+ migration were complete). Fix it and run again.
76
+ - **Exit 6: agent tasks are pending.** Do them (step 5), then run `afm update` again. The pin stays until
77
+ every task passes its check.
78
+ - **Protected surfaces** are never materialized by an update: the shipped content itself,
79
+ `PRINCIPLES`, `[TBD]` content, the format the hooks parse, the rubric that decides when the evolution
80
+ cycle runs (`when-to-evolve-methodology`) and the `triggers` entries that fire the evolution
81
+ mechanisms. That is self-reference: the sync does not materialize the document that decides when
82
+ the cycle runs. New stack-agnostic rubrics, yes.
83
+ 5. **Do each agent task** listed after the tally. A line reads `agent <codemod>: <brief> (scope: <paths>)`
84
+ followed by the lines of its check that still fail. Work only inside the scope, through the write ops
85
+ (`afm adr`, `afm gotcha`, `afm rule`, `afm new <type>`), suppressing each op's own per-artifact
86
+ question. Run `afm lint <path>` to see the frontmatter findings and `afm lint --fix <path>` to fix
87
+ the ones that have one exact answer. Never invent content: what the operator did not say stays `[TBD]`.
88
+ A `[TBD]` placeholder alone lints clean and does not hold the pin.
89
+ 6. **Close a sync on an edited file** with `afm update --accept <path>`. When a codemod would overwrite a
90
+ materialized file the owner edited (its hash in the manifest no longer matches), it hands the merge to
91
+ the agent instead. Merge the new shipped text into the owner's file, then run
92
+ `afm update --accept docs/<path>`: it records the file as it is now as the accepted version. It never
93
+ edits the file.
94
+ 7. **Run `afm update` again** until it prints `pin: <from> -> <to>` (or *already up to date*). Then the
95
+ **post-conditions** (below) and the **log**.
96
+
97
+ ## What the codemods can do
98
+
99
+ A release is a module in `packages/core/src/features/codemod-releases/data/` with an ordered list of typed ops. The rules that
100
+ make them safe are 0/1 and none depends on judgment:
101
+
102
+ | Op | What it does | Proof of need | When it hands over |
103
+ | --- | --- | --- | --- |
104
+ | `replace` | literal substitution in one file | the old text is in the file | `[TBD]` there: *held* |
105
+ | `move` | renames a file or a folder | the source exists | a target that already exists |
106
+ | `create` | writes a shipped file the instance lacks | the file is absent | never overwrites |
107
+ | `sync` | overwrites a materialized file with the new shipped text | the manifest hash still equals the file | an edited file becomes an agent task (`--accept`) |
108
+ | `delete` | removes a file the plugin retired | the manifest hash still equals the file | an edited file is *held* |
109
+ | `frontmatter` | sets or renames a key | the key is missing or old | `[TBD]` there: *held* |
110
+ | `section` | adds a section to a doc | the heading is absent | never rewrites an existing one |
111
+ | `regenerate` | rebuilds a generated surface (agents block, stubs, index) | the surface is stale | none |
112
+ | `custom` | a program for what the others cannot say (the OKF lift) | its own | its own agent tasks |
113
+ | `agent` | what only judgment can do (a rewrite, a merge) | its own | always, with a mechanical check |
114
+
115
+ **The rules:**
116
+
117
+ 1. **Proof of need before a file is touched.** Zero matches means already applied: skip it silently.
118
+ 2. **Literal substitution, never a regular expression.** A pattern on an instance the owner edited by
119
+ hand is how a sync destroys text nobody asked to change.
120
+ 3. **Proof of application afterwards.** The old text is gone **and** the new text appears at least as often.
121
+ 4. **A file missing from the instance is skipped.** A doc the consumer never materialized does not arrive
122
+ through the back door, except through an explicit `create`.
123
+ 5. **`[TBD]` is untouchable** (#4): text inside a block the owner marked this way is *held*.
124
+ 6. **The manifest is the memory of what AFM wrote.** `docs/.afm-manifest.json` maps each file AFM wrote to
125
+ the hash of the text it wrote. `sync` and `delete` act only on a file whose hash still matches. A file
126
+ the owner changed is never overwritten or removed silently: the old "update never deletes" is now
127
+ "update never deletes what the owner changed". Commit the manifest with the docs.
128
+ 7. **The proof of need decides, not only the pin.** A codemod whose precondition still holds runs even when
129
+ the pin is past its release.
130
+ 8. **The pin moves last**, and only when every agent task passes its check.
131
+
132
+ > **Discover-first (#2):** the proof of need is the discover-first rule. A delta the operator already copied
133
+ > by hand never reappears.
134
+
135
+ ## Instances older than 4.0
136
+
137
+ `afm update` refuses to cross a major version (exit 5). A 3.x instance goes through `afm migrate` first
138
+ (`--dry-run` to preview): the bridge to 4.0, the split of `docs/gotchas.md` into `docs/gotchas/` and of
139
+ `afm.md` section 3 into `docs/rules/`, and the report names. Run it once and **commit before anything
140
+ else** (a structural migration mixed with a content edit hides both in the diff). Then run `afm update`
141
+ to receive the codemods of the 4.x releases (the OKF frontmatter lift among them), and
142
+ `afm index --materialize` (hard rule 39 fails on a missing or stale index).
143
+
144
+ ## 1.6 Language migration (runs when the instance is not English)
145
+
146
+ Artifacts are English: the docs, the code, the comments, the tests, the rule text. The ONE exception is
147
+ the strings the product shows its own users, and that is a product decision.
148
+
149
+ **The order below is the mechanism, and reversing it is the defect.** Read the field. Derive the excluded
150
+ scope. Only then read a file. "Scan first, remember to skip" makes the exclusion depend on the agent
151
+ noticing during the work, and that is the branch that damages a product.
152
+
153
+ 1. **Read `docs/prd.md` § 11.1 FIRST.** It holds the app-facing string language. An instance
154
+ materialized before this field existed has no such section, which reads the same as `[TBD]`.
155
+ 2. **`[TBD]` or absent: stop and ask `afm ask update-strings`** (questions in
156
+ `content/questions/update-strings.questions.yaml`). Never choose a language. The irreducible mistake
157
+ is "no field, so nothing is a product string, so migrate everything". A translated UI is not something
158
+ `git revert` gives back to the user who saw it.
159
+ 3. **Derive the excluded scope from the answer, before any file is read.** The exclusion is a file and
160
+ directory list computed once, not a judgment made per file. An English app language makes the excluded
161
+ scope empty, and the rest is unchanged. `afm lang` reports the language each file is written in.
162
+ 4. **Ask ONE question about the artifacts: `afm ask update-artifacts`** (questions in
163
+ `content/questions/update-artifacts.questions.yaml`). Ask it once, never per file. `Keep the current language` records the choice, because the next update must not ask again.
164
+ 5. **On migrate, fan out over the artifacts outside the excluded scope**: the local `docs/`, the code
165
+ comments, the test names. Each file gets a rewrite. Then an adversarial check asks what fact, number,
166
+ path, marker or caveat disappeared. The checker reports; it never rewrites.
167
+ 6. **Never touch the excluded scope.** Not the UI strings, not the user-facing error messages, not the
168
+ email copy, not the seed data a user reads. An ambiguous file stays.
169
+
170
+ **Post-condition.** `prd.md` § 11.1 is unchanged by this step. The migration reads that field and never
171
+ writes it. The product language is the owner's decision, not a consequence of a version bump.
172
+
173
+ ## Opt-in deltas: a release that adds a capability, not a requirement
174
+
175
+ A capability delta is not a migration. Nothing breaks when the operator ignores it, and `update` must not
176
+ apply it silently, because opting in is the operator's decision. The release notes (`content/CHANGELOG.md`) name it, and no codemod applies it.
177
+ Report an opt-in delta, and never apply it. A project that has not opted in has not decided against the
178
+ feature: it has not been asked.
179
+
180
+ | Release | Capability | How to opt in | What happens if you do nothing |
181
+ | --- | --- | --- | --- |
182
+ | 3.10.0 | organisation policy | set `[org] dir` in the global config, pointing at a bundle that holds `policy.md` that your user owns (it was the `AFM_GLOBAL_DIR` variable before 4.0) | nothing. Every reader returns before any file test when no bundle is set |
183
+ | 3.11.0 | `policy_blocking` is REMOVED | delete the key if an instance added it | nothing. It decided nothing while it existed |
184
+
185
+ ## Post-conditions
186
+
187
+ 1. **Parseable format.** Re-extraction by the hooks over the touched docs must preserve the format:
188
+ `afm lint` exits 0, `afm index --check` returns 0 and `afm gate --audit` is no worse than before. Otherwise roll back the
189
+ delta and report it. Never fail silently.
190
+ 2. **The instance/plugin boundary is MECHANICAL.** Run the two-branch check in `afm path ops/evolve`
191
+ § Phase 4.3 at the end. The boundary must come back clean: any change in the shipped content means
192
+ **rollback + report CRITICAL**. `update` only reads the plugin.
193
+
194
+ ## Log and handoff
195
+
196
+ - **À la carte:** report in chat what migrated (applied deltas, pending deltas, the newly pinned version).
197
+ A simple sync does not need its own `docs/evolution/` entry.
198
+ - **Under `evolve`:** it feeds the cycle report (the A3 migration becomes one line in
199
+ `docs/evolution/<date>-NNN.md`).
200
+ - **Handoff to `afm reconcile`** to watch, in the following sessions, whether the materialized delta now
201
+ guides well.
202
+
203
+ ## Suppression contract (back-pointer)
204
+
205
+ `evolve` **composes `update`** on axis A3. Under the cycle, `evolve` suppresses the gate of step 3 and
206
+ moves the item up to its single gate; the log feeds the cycle report. The authoritative table lives in
207
+ `afm path ops/evolve` § Suppression contract. À la carte, `update` runs on its own, with its own gate.
208
+
209
+ ## Anti-patterns
210
+
211
+ - ❌ **Editing the shipped content.** `update` only **reads** the plugin to discover deltas. It writes
212
+ only to the instance.
213
+ - ❌ **Materializing a consumer-noop release.** A plugin-internal patch with no content to materialize
214
+ only bumps the version. Do not invent work.
215
+ - ❌ **Re-materializing a delta that is already in the instance.** The proof of need exists for this.
216
+ - ❌ **Overwriting or deleting a file the owner edited.** The manifest hash is the guard: merge by hand and close it with `afm update --accept <path>`.
217
+ - ❌ **Materializing a protected surface:** the evolution cycle's own rubric, the evolution `triggers`,
218
+ and `[TBD]`. This is self-reference, and it is the operator's own stance.
219
+ - ❌ **Bumping `.afm-version` as if the migration were complete.** If a delta was left out, the marker
220
+ must show the gap honestly.
221
+ - ❌ **Inventing product content.** `update` materializes only what the plugin already provides
222
+ (stack-agnostic). A missing product decision stays `[TBD]`, for `afm reconcile` to handle (#4/#5).
223
+ - ❌ **Running as bootstrap.** A missing instance calls for `init` or `refactor`, not `update`.
@@ -0,0 +1,69 @@
1
+ ---
2
+ type: op
3
+ description: Stamps a human verification on a doc, the only writer of the OKF `verified` field.
4
+ load: warm
5
+ writes: ["docs/**/*.md"]
6
+ composes: []
7
+ ---
8
+ # Sub-flow: stamp a human verification (`verify`)
9
+
10
+ > **In this file:** What verify is / is not · Why only a human · Procedure · What it must never do · Invariants
11
+
12
+ > **Form** — effect: adds `verified:` to the frontmatter of one file · trigger: `afm verify <path>` · precondition: `.afm-version` · **degree of freedom: low**
13
+
14
+ Triggers: `afm verify <path>`. Also natural language that states a person checked a document.
15
+ Examples: "I reviewed this rule", "mark this gotcha as verified", "sign off on this ADR".
16
+
17
+ ## What `verify` is, and is not
18
+
19
+ OKF v0.2 §5.2 keeps two facts apart. `generated` records what PRODUCED the content. `verified`
20
+ records who CONFIRMED it. They are separate because the writer and the checker are different
21
+ actors, and a document can change without re-confirmation.
22
+
23
+ From those two fields OKF derives a trust tier (§5.3):
24
+
25
+ | `verified` | Tier |
26
+ | --- | --- |
27
+ | absent | **unverified** |
28
+ | present, no `human:` actor | **machine-confirmed** |
29
+ | present with a `human:<id>` actor | **human-reviewed** |
30
+
31
+ AFM is autonomy-first. `reflect`, `reconcile` and `remediate` all WRITE documentation. Without this
32
+ operation, "an agent wrote this and no person read it" and "a person approved it" look the same.
33
+
34
+ ## Why only a human, and why only here
35
+
36
+ An automatic writer would destroy the field. The agent commits under the operator's git identity. A
37
+ hook that stamps `verified` on every commit would claim human review of every agent commit. That is
38
+ worse than no field: it converts an honest absence into a confident lie.
39
+
40
+ So the rule is one line. **Absence means unverified, and only an explicit human stamp writes it.**
41
+ The command enforces it: it refuses when `AFM_ACTOR` names a role or a machine instead of a person.
42
+
43
+ ## Procedure
44
+
45
+ 1. `afm preflight`. Exit 3: no instance, stop.
46
+ 2. **Confirm that a person read the file.** Show it, or ask `afm ask verify` (questions in
47
+ `content/questions/verify.questions.yaml`). Never infer review from ordinary work. `No` stops.
48
+ 3. **Stamp it:** `afm verify <path under docs/>`. The actor is `human:<id>`, where `<id>` comes from
49
+ `--as <id>`, from `AFM_ACTOR=human:<id>` or from the git `user.name` in lowercase with no space.
50
+ The command appends `verified: [{ by: "human:<id>", at: <ISO 8601 UTC> }]`, keeps every other
51
+ key, and adds an entry when `verified` is already a list. A second check is a second event; the
52
+ tier reads the latest `at`.
53
+ 4. **Report the tier that resulted**, and say what it means. The command prints it.
54
+
55
+ A file with no frontmatter carries no contract to stamp, and the command says so.
56
+
57
+ ## What it must never do
58
+
59
+ - Never stamp a file the person did not read.
60
+ - Never write `verified` from a hook, a gate, a commit, or another op.
61
+ - Never remove a verification. A doc that changed after a verification stays verified at that date,
62
+ and the date is the signal. Silently dropping it hides that the content moved.
63
+ - Never write `generated` here. That belongs to whoever produced the content.
64
+
65
+ ## Invariant principles
66
+
67
+ The single canonical source is `afm path PRINCIPLES`. Universals §1-8 apply. #5 applies hardest:
68
+ this operation records a fact about a person. Inventing one is the worst kind of invention, because
69
+ nothing downstream can tell.
@@ -0,0 +1,46 @@
1
+ ---
2
+ type: pattern
3
+ description: Wraps a vendor SDK type or payload shape at the boundary so domain and lib code depend on an owned interface instead of the vendor's.
4
+ load: cold
5
+ smell: 'a vendor SDK type (a Stripe object, an S3 response, an ORM row) is imported and used directly inside domain or lib code, past the boundary it arrived at'
6
+ when_to_use: the vendor type crosses more than one file, or it is expected to change independently of the domain model that consumes it
7
+ when_not_to_use: the vendor call happens in exactly one file and its result is consumed nowhere else
8
+ ---
9
+
10
+ # Pattern — Adapter
11
+
12
+ > Suggestion only. This pattern does not become project convention by existing here — it
13
+ > materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
14
+ > this codebase.
15
+
16
+ Continues DIP in `rubrics/solid-triggers.md` (domain must not import infra) and
17
+ `rubrics/validation-boundary.md` (the parser lives at exactly 2 places). Adapter is what sits at
18
+ that same boundary for a *type*, not a schema: it converts the vendor's shape to an owned one on
19
+ the way in, once.
20
+
21
+ ## Decision
22
+
23
+ | Situation | Verdict | Anti-pattern |
24
+ | --- | --- | --- |
25
+ | `Stripe.Customer` (or equivalent) is read in 2+ files | Adapter: one function maps it to an owned `Customer` type at the boundary, everything past that line uses the owned type | Passing the SDK type through domain and re-reading its fields in each consumer |
26
+ | The SDK's field names or nesting are unstable across major versions (checked: it has broken before) | Adapter absorbs the next breaking change in 1 file | The breaking change ships and touches every call site that read the SDK type directly |
27
+ | The SDK call and its one consumer live in the same file, never exported | No adapter yet — see `when-to-create-lib.md`, one caller does not earn extraction | A `StripeCustomerAdapter` class for a value read once and discarded |
28
+ | The domain needs to call back into the vendor (not just read its output) | Wrap the vendor client behind a port your domain defines (this is DIP, Adapter's usual partner) | Domain importing the vendor SDK client directly to make the call itself |
29
+
30
+ ## Why this works
31
+
32
+ A vendor's shape is out of your control; a domain model is not. Every field of the vendor type
33
+ read directly in domain code is a place a vendor upgrade can break — with the adapter, an upgrade
34
+ that renames or restructures the vendor type is absorbed in the 1 function that does the mapping,
35
+ never in the N places that use the resulting owned type.
36
+
37
+ ## Anti-patterns
38
+
39
+ - ❌ An adapter that just renames fields 1:1 and adds no real translation, guarding a type used in
40
+ exactly one place. That's indirection with no boundary to protect — see `when-to-create-lib.md`.
41
+ - ❌ The adapter returns the vendor's own error type unchanged. Errors need the same translation as
42
+ data — see `error-classification.md` for what the domain-facing shape should be.
43
+ - ❌ Two adapters for the same vendor type, one per module, silently drifting. Vendor type → owned
44
+ type is a boundary fact, not a per-module preference; it belongs in one shared place.
45
+ - ❌ Calling the adapter function conditionally, so some call sites still see the raw vendor type.
46
+ A boundary that is optional is not a boundary.
@@ -0,0 +1,47 @@
1
+ ---
2
+ type: pattern
3
+ description: Bundles a repeated sequence of calls across 3 or more collaborators, that always represents one business step, behind a single function.
4
+ load: cold
5
+ smell: 'a call site wires 3 or more collaborators in the same order, in more than one place, to carry out one coherent business step'
6
+ when_to_use: the same sequence of calls across 3+ collaborators repeats at 2+ call sites and always represents one step
7
+ when_not_to_use: the collaborators are used independently at each call site — a different subset, or a different order, each time
8
+ ---
9
+
10
+ # Pattern — Facade
11
+
12
+ > Suggestion only. This pattern does not become project convention by existing here — it
13
+ > materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
14
+ > this codebase.
15
+
16
+ Continues ISP in `rubrics/solid-triggers.md`, from the other direction: ISP narrows what a single
17
+ function receives; Facade narrows what a *caller* must know to perform one step, by hiding the
18
+ fan-out to several collaborators behind one call.
19
+
20
+ ## Decision
21
+
22
+ | Situation | Verdict | Anti-pattern |
23
+ | --- | --- | --- |
24
+ | 2+ call sites each wire the same 3+ collaborators, in the same order, for the same outcome | Extract a facade function; the call sites call it once and stop knowing the collaborators exist | Each call site re-derives the correct order, one of them getting it wrong under time pressure |
25
+ | One of the wired collaborators is itself a vendor SDK crossing a boundary | Apply Adapter to that one collaborator first, then wrap the sequence in the Facade | The Facade re-exports the raw vendor type through its own return value, leaking the boundary it should hide |
26
+ | The call sites use *different* subsets or orders of the same collaborators | Do not extract — that variation is the actual logic, and a facade would hide it, not simplify it | A facade with a boolean flag per caller's variant — the flags are the switch this pattern was meant to avoid |
27
+ | Only one call site exists | No facade yet — inline the sequence until a 2nd real caller needs the same one | A `CheckoutFacade` built for a single call site "to keep the handler short" |
28
+
29
+ ## Why this works
30
+
31
+ A caller wiring C collaborators must know all C APIs, their call order, and how errors from each
32
+ propagate — that knowledge is repeated at every call site that performs the same step. A facade
33
+ moves it to 1 place: each call site's coupling drops from C collaborators to 1 function, and a
34
+ change to the internal wiring (a 4th collaborator added, the order changed) touches that 1 place.
35
+
36
+ ## Anti-patterns
37
+
38
+ - ❌ A facade over collaborators that are each already simple to call alone. If wiring them takes
39
+ 1 line each, the facade adds a name to remember without reducing real complexity.
40
+ - ❌ A "God facade" that grows to wrap every collaborator in the module, becoming the module's only
41
+ entry point for unrelated steps. Facade wraps *one* coherent business step, not the whole module
42
+ — see `rubrics/solid-triggers.md` § SRP if it starts doing 2+ things.
43
+ - ❌ Swallowing or generically catching every collaborator's error inside the facade. Each
44
+ collaborator's failure still needs its own classification — see `error-classification.md`;
45
+ a facade changes who calls, not how errors are represented.
46
+ - ❌ Building the facade before the 2nd call site exists, on the belief the sequence will repeat.
47
+ That is the same premature-extraction risk `when-to-create-lib.md` names for helpers in general.
@@ -0,0 +1,46 @@
1
+ ---
2
+ type: pattern
3
+ description: Centralizes a multi-step object construction that is copy-pasted at 2 or more call sites into one named function.
4
+ load: cold
5
+ smell: 'the same multi-step construction (apply defaults, validate, wire 2+ collaborators) is copy-pasted at 2 or more call sites to build the same kind of object'
6
+ when_to_use: constructing the object takes more than one step, and that construction already repeats at 2+ call sites
7
+ when_not_to_use: the value is a single-field object literal, or only one call site builds it today
8
+ ---
9
+
10
+ # Pattern — Factory
11
+
12
+ > Suggestion only. This pattern does not become project convention by existing here — it
13
+ > materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
14
+ > this codebase.
15
+
16
+ Continues the Rule-of-Three trigger already used in `rubrics/when-to-create-lib.md`: that rubric
17
+ extracts a helper on the 3rd caller. Factory is the same threshold applied specifically to
18
+ *construction* logic — defaults, validation, wiring 2+ collaborators into one object.
19
+
20
+ ## Decision
21
+
22
+ | Situation | Verdict | Anti-pattern |
23
+ | --- | --- | --- |
24
+ | 2+ call sites build the same kind of object with the same defaults and the same wiring steps | Extract a `create<X>()` factory function; call sites pass only what varies | Each call site re-applies the same defaults, one of them drifting when a default changes |
25
+ | The object needs 1+ collaborator injected (a clock, an id generator, a config) to be testable | Factory takes the collaborators as parameters, with production defaults | The object reaches for `Date.now()` or a singleton internally — untestable without mocking modules |
26
+ | Construction is a single field literal (`{ id, name }`) | No factory. A function that returns its own argument is ceremony | `createUser({ id, name })` that does nothing `{ id, name }` didn't already do |
27
+ | Only one call site exists today | No factory yet — wait for the 2nd real caller, per `when-to-create-lib.md` | A `UserFactory` class built ahead of the 2nd caller, on the promise it is coming |
28
+
29
+ ## Why this works
30
+
31
+ Construction logic duplicated at K call sites means a changed default, a new required field, or a
32
+ new invariant must be applied K times — and K-1 of those edits are typically missed. A factory
33
+ collapses that to 1 place: the invariant is enforced once, and every call site that changes still
34
+ compiles because they pass arguments to a function, not fields to a literal.
35
+
36
+ ## Anti-patterns
37
+
38
+ - ❌ A factory with a config object with 12 optional fields, most unused by every caller. That is
39
+ the object's own complexity leaking into the factory's signature — fix the object first.
40
+ - ❌ A factory that also performs I/O (saves to the DB, calls an API) as a side effect of
41
+ "constructing." Construction builds a value; persistence is a separate step the caller invokes.
42
+ - ❌ A factory function per call site (`createUserForSignup`, `createUserForAdmin`) that each
43
+ duplicate 90% of the same logic. That is the smell this pattern exists to remove, reintroduced
44
+ one layer down — collapse them into one factory with a parameter for the 10% that differs.
45
+ - ❌ Naming it `UserFactory.getInstance()` to imitate a singleton when nothing requires only one
46
+ instance to exist. A plain exported function is the smaller design (KISS).
@@ -0,0 +1,55 @@
1
+ ---
2
+ type: pattern
3
+ description: Replaces a null guard repeated at 3 or more call sites with a default implementation that behaves like the absent case's known, safe outcome.
4
+ load: cold
5
+ smell: 'the same `if (x == null) { ...default behavior... }` guard, followed by using `x`, is repeated at 3 or more call sites'
6
+ when_to_use: the absent case has one well-known default behavior and every caller currently branches on null to reach it
7
+ when_not_to_use: callers must react differently to absence, or must distinguish absent from present-but-default
8
+ ---
9
+
10
+ # Pattern — Null Object
11
+
12
+ > Suggestion only. This pattern does not become project convention by existing here — it
13
+ > materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
14
+ > this codebase.
15
+
16
+ A narrower cousin of Strategy in this same library: instead of one implementation per real case,
17
+ it adds exactly one extra implementation standing in for "nothing here" — so the caller's `switch`
18
+ or `if (x == null)` collapses to zero branches, not one fewer.
19
+
20
+ ## Decision
21
+
22
+ - ✅ 3+ call sites do `if (x == null) return fallbackValue` (or an equivalent guard) right before
23
+ using `x`, and `fallbackValue` is the same at every one → give the absent case a real
24
+ implementation of the same interface (an empty cart, a no-op logger, a zero-total invoice) and
25
+ stop returning `null` in the first place.
26
+ - ✅ The type already has a real interface with 1+ methods (a `Logger`, a `Cart`) → the Null Object
27
+ implements that interface with safe, inert behavior (`NullLogger.log()` does nothing).
28
+ - ❌ Fewer than 3 call sites guard the same way, or two callers want a *different* fallback → a
29
+ shared Null Object would force one fallback where callers legitimately need their own; keep the
30
+ guards local, or pass the fallback in per caller instead.
31
+ - ❌ The caller must tell "absent" apart from "present with the default value" (e.g. a form field
32
+ the user explicitly cleared vs. one never touched) → collapsing both into one Null Object erases
33
+ that distinction; `Result`/`Option` is the honest fit — see `result-vs-exception.md`.
34
+
35
+ ## Why this works
36
+
37
+ Every `if (x == null)` guard is a branch a reader and the compiler must both track; N call sites
38
+ with the same guard is N places that branch identically and N places a future change to the
39
+ fallback must be kept in sync. A Null Object moves the branch inside the 1 place that decides
40
+ "use the real thing or the null thing" (usually the factory or lookup that produces `x`), so the
41
+ N call sites go back to unconditional code — 1 branch total instead of N.
42
+
43
+ ## Anti-patterns
44
+
45
+ - ❌ A Null Object that throws when any of its methods is called. That is not a null object, it is
46
+ a differently-shaped exception — it defeats the purpose, which is *safe* default behavior.
47
+ - ❌ Using Null Object where the caller actually needs to know absence happened (an audit log that
48
+ must record "no discount was found," not silently apply a $0 discount and look identical to a
49
+ real $0 discount). Prefer an explicit `Option`/`Result` there instead.
50
+ - ❌ A Null Object with mutable shared state (`NullCart` that collects items across unrelated
51
+ calls). The point of the pattern is a stateless, inert stand-in — shared mutable state turns it
52
+ into a hidden singleton bug.
53
+ - ❌ Silently swapping `null` for a Null Object across a serialization boundary (DB, API response).
54
+ Null Object is an in-process convenience; a stored `null` still means "absent" to every reader
55
+ outside this process, so translate at that boundary instead of upstream.
@@ -0,0 +1,52 @@
1
+ ---
2
+ type: pattern
3
+ description: Replaces a thrown exception for an expected, listable failure with a typed Result value the immediate caller must handle.
4
+ load: cold
5
+ smell: 'a function throws for an outcome the caller already anticipates and branches on (validation failure, not-found, conflict), and the caller wraps the call in try/catch to recover that one branch'
6
+ when_to_use: the failure modes are known, finite, and the immediate caller needs to react differently to each one
7
+ when_not_to_use: the failure is unexpected or unrecoverable — a network drop, a timeout, a bug
8
+ ---
9
+
10
+ # Pattern — Result / Either over exception-as-control-flow
11
+
12
+ > Suggestion only. This pattern does not become project convention by existing here — it
13
+ > materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
14
+ > this codebase.
15
+
16
+ `rubrics/error-classification.md` already prescribes the shape (`Result<T, E>` with a literal
17
+ `as const` code) for the Domain layer specifically. This pattern generalizes the same call: apply
18
+ it to *any* function, domain or not, where a `try { } catch { }` exists only to recover one
19
+ specific, anticipated branch — not to handle a genuine surprise.
20
+
21
+ ## Decision
22
+
23
+ - ✅ The function has 2 or more named, expected outcomes (`EMAIL_TAKEN`, `NOT_FOUND`,
24
+ `WEAK_PASSWORD`) → return `Result<T, E>` with `E` a literal union, and force the caller to
25
+ `switch` with an exhaustive `never` default (see `error-classification.md`).
26
+ - ✅ Today's code throws a custom error class only to `catch (e) { if (e instanceof X) }` one line
27
+ above the call → that `catch` is the tell; replace both sides with `Result`.
28
+ - ❌ The failure is a bug, a timeout, or anything the immediate caller cannot meaningfully act on
29
+ → let it rise. Wrapping every infra call in `Result` just relocates the `try/catch`, it does not
30
+ remove it — see `error-classification.md`'s Infra-error row.
31
+ - ❌ The function is called in exactly one place and that place already has a correct
32
+ `try/catch` → converting it buys nothing; the try/catch already IS the handling.
33
+
34
+ ## Why this works
35
+
36
+ An exception's type is invisible at the call site — nothing in a function's signature says it can
37
+ throw, so the caller either wraps every call defensively or misses a case entirely. A `Result<T,
38
+ E>` return type puts the failure in the signature: TypeScript's exhaustiveness check on the
39
+ `switch` (the `never` default) then makes an unhandled case a compile error instead of a runtime
40
+ surprise, at zero added `try/catch` blocks.
41
+
42
+ ## Anti-patterns
43
+
44
+ - ❌ Wrapping infra failures (DB down, network drop) in `Result` "for consistency." Those are not
45
+ anticipated outcomes — see `error-classification.md`: infra errors rise and get caught once at
46
+ the process boundary, they do not become a case in every caller's switch.
47
+ - ❌ A `Result<T, string>` with a free-text message instead of a literal code. The caller cannot
48
+ `switch` on a string; the whole benefit (exhaustiveness) is gone. The code must be `as const`.
49
+ - ❌ Mixing `Result` and exceptions in the same function — return `Result` for the expected branch,
50
+ then also `throw` for another branch of the same function. Callers must handle two mechanisms.
51
+ - ❌ A generic `try/catch` around a `Result`-returning call "just in case." If the function no
52
+ longer throws for its expected failures, the surrounding `try` should not exist either.
@@ -0,0 +1,45 @@
1
+ ---
2
+ type: pattern
3
+ description: Replaces a branching conditional keyed on a kind, type, or provider field with one interface and one implementation per case, selected through a registry.
4
+ load: cold
5
+ smell: 'a `switch` or `if`/`else if` chain keyed on a kind, type, or provider field, duplicated across 2+ files or functions'
6
+ when_to_use: a new case of the same kind arrives on a predictable cadence and every case can share one call shape
7
+ when_not_to_use: only one implementation exists today, or the cases would return incompatible shapes
8
+ ---
9
+
10
+ # Pattern — Strategy
11
+
12
+ > Suggestion only. This pattern does not become project convention by existing here — it
13
+ > materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
14
+ > this codebase.
15
+
16
+ Continues the OCP trigger in `rubrics/solid-triggers.md`: that rubric tells you a `switch` on a
17
+ provider is a problem the moment a 2nd `switch` on the same field appears elsewhere. Strategy is
18
+ the concrete extraction once that trigger fires.
19
+
20
+ ## Decision
21
+
22
+ | Situation | Verdict | Anti-pattern |
23
+ | --- | --- | --- |
24
+ | A `switch(kind)` selects behavior, and a second `switch(kind)` on the same field lives in another function or file | Apply Strategy: one file per case implementing a shared interface, wired through a lookup table | Adding a 4th `case` to both switches and hoping they stay in sync |
25
+ | Every case already returns the same shape (LSP, see `solid-triggers.md`) | Safe to extract now — the interface is exactly that shared shape | Extracting before the shapes agree — the interface degrades to `any` or a union of incompatibles |
26
+ | Only one case exists and no second is concretely planned | Do not extract. A single `if` is not a smell | A `StrategyRegistry` with one entry — OCP paid for a case that never happened |
27
+ | The active case is fixed for the process lifetime (an env-driven backend chosen once at boot) | Plain dependency injection of the one active implementation | A registry of N strategies when exactly 1 is ever selected at runtime |
28
+
29
+ ## Why this works
30
+
31
+ A `switch` on a variant duplicates its list of cases every time the branch is copy-pasted into a
32
+ new function: N call sites × M cases means up to N×M places that drift when case M+1 arrives.
33
+ Strategy collapses that to N call sites each holding one lookup, plus M single-purpose files —
34
+ adding case M+1 then touches exactly 1 new file and 1 registry line, never the N call sites.
35
+
36
+ ## Anti-patterns
37
+
38
+ - ❌ A `StrategyRegistry` holding one entry "for when we add the second provider." Inline the one
39
+ implementation. Extract on the second real case, not the imagined one.
40
+ - ❌ Interfaces that leak the framework in the port (`RegistryAdapter.publish(ctx: TRPCContext)`).
41
+ The port must be as narrow as ISP allows — see `solid-triggers.md` § ISP.
42
+ - ❌ A registry keyed by a raw `string` instead of the closed literal union of kinds. A typo in the
43
+ key then fails at runtime instead of at compile time.
44
+ - ❌ Keeping the old `switch` alongside the new registry "just in case." Delete it in the same
45
+ change — two selection mechanisms for the same kind is worse than the one you started with.
@@ -0,0 +1,50 @@
1
+ ---
2
+ type: pattern
3
+ description: Chooses between an inherited algorithm skeleton with overridable steps and a plain function that takes the varying step as a parameter.
4
+ load: cold
5
+ smell: 'two or more functions or classes repeat the same ordered sequence of steps, differing only in 1-2 of those steps'
6
+ when_to_use: the skeleton has 3+ fixed steps and only 1-2 vary across callers
7
+ when_not_to_use: the varying step is the only step, or fewer than 3 callers share the skeleton
8
+ ---
9
+
10
+ # Pattern — Template Method vs. composition
11
+
12
+ > Suggestion only. This pattern does not become project convention by existing here — it
13
+ > materializes into `code-style/`, `ach.md`, or an ADR only when the operator accepts it for
14
+ > this codebase.
15
+
16
+ Shares the Rule-of-Three threshold from `rubrics/when-to-create-lib.md`, applied to a *sequence*
17
+ of steps instead of a single helper — and it decides between two ways to remove the duplication,
18
+ not just whether to remove it.
19
+
20
+ ## Decision
21
+
22
+ | Situation | Verdict | Anti-pattern |
23
+ | --- | --- | --- |
24
+ | 3+ callers share an ordered skeleton (validate → transform → persist → notify), 1-2 steps vary, and the language has first-class functions | Composition: one function takes the varying step(s) as parameters/callbacks | Copy-pasting the whole skeleton per caller so the 1 varying line can differ |
25
+ | Same skeleton, but the varying steps need to read or mutate protected state shared across steps (a running total, an accumulator built up across the sequence) | Template Method: a base class fixes the sequence, subclasses override only the varying steps | Threading that shared state through 4 separate callback parameters to fake what inheritance gives for free |
26
+ | Only 1-2 callers share the skeleton today | Do not extract either way — inline duplication for 2 callers costs less than an abstraction (Rule of Three, `when-to-create-lib.md`) | A base class with exactly one subclass, built ahead of the 2nd real caller |
27
+ | The "skeleton" is actually just the 1 step that varies, wrapped in ceremony | There is no skeleton to extract — this is a Strategy situation instead, see `strategy.md` | Template Method with a base class whose every method is abstract — nothing is actually shared |
28
+
29
+ ## Why this works
30
+
31
+ A skeleton with S total steps where V of them vary means V×N points of difference across N
32
+ callers when duplicated in full. Extracting the skeleton — either way — reduces that to N call
33
+ sites providing only the V varying steps, plus 1 shared definition of the S−V fixed ones.
34
+ Composition and Template Method both hit that number; the choice between them is only about
35
+ where the shared state between steps needs to live — a closure (composition) or `this`
36
+ (inheritance).
37
+
38
+ ## Anti-patterns
39
+
40
+ - ❌ Reaching for inheritance by default. Composition is the smaller design (KISS) whenever no
41
+ step needs shared mutable state across the sequence — prefer it, and use Template Method only
42
+ when composition would require threading that state through every callback.
43
+ - ❌ A Template Method base class with a step marked `abstract` that every subclass overrides
44
+ identically. That step isn't varying — fold it back into the fixed skeleton.
45
+ - ❌ Composition where the "varying step" callback secretly depends on 3+ pieces of the caller's
46
+ outer scope. At that point the callback IS the logic and the shared skeleton is the smaller
47
+ part — question whether extracting the skeleton was the right cut at all.
48
+ - ❌ Extracting the skeleton before the 3rd caller exists. Two callers sharing steps can be
49
+ coincidence, not a real pattern — the same Rule-of-Three reasoning `when-to-create-lib.md` uses
50
+ for helpers applies here.