@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,325 @@
1
+ #!/usr/bin/env bash
2
+ # Hard rule 39 — every op carries frontmatter, and what it declares is true.
3
+ #
4
+ # A gate, not a preference, because the failure is SILENT in the way that matters: the
5
+ # index reads frontmatter and nothing else, so an op with none is simply invisible to it.
6
+ # The agent asks "who writes spec.md", gets an answer that omits the file it needed, and
7
+ # has no signal that anything was left out. Missing metadata does not look like an error.
8
+ # It looks like a shorter list.
9
+ #
10
+ # A dangling `composes:` fails the same way from the other side — it names an op that no
11
+ # longer exists, and the reader trusts it because the frontmatter is the cheap thing it
12
+ # read INSTEAD of the body.
13
+ set -u
14
+ # COUNTING rule: the scope is the whole repository, not the diff. Rule 36 declares the same, for
15
+ # the same reason — the question "is every indexed file readable" is about the TREE. A staged-only
16
+ # answer would report a bundle as conformant because this commit happened not to touch the broken
17
+ # file. The argument is accepted and deliberately unused.
18
+ MODE="${1:-worktree}"; : "$MODE"
19
+ REPO="${AFM_GATE_REPO:-$PWD}"
20
+ . "${AFM_GATE_LIB:?}/scope.sh"
21
+
22
+ # Resolved through $0, not $REPO (see README "Resolve a sibling tool through $0"): this file
23
+ # sits three levels below the source root both in the plugin (framework-template/tools/gate/
24
+ # core-rules/) and in a materialized consumer (docs/tools/gate/core-rules/), so the same
25
+ # expression reaches framework-template/ in one and docs/ in the other. $REPO/plugins/afm/
26
+ # framework-template only exists when this very repository is being dogfooded through ./test/ —
27
+ # never in a project bootstrapped by afm init. There $OPS never existed either way, but the old
28
+ # `exit 2` short-circuited the WHOLE runner on that, so passes 1c/1d below — the ones that check
29
+ # the consumer's own docs/ instance and need no $FT at all — silently never ran in any bootstrapped
30
+ # project. Each $FT-dependent pass already guards its own directory with `[ -d ... ] || continue`,
31
+ # so there is nothing left here to gate on.
32
+ FT="$(cd "$(dirname "$0")/../../.." && pwd)"
33
+ OPS="$FT/ops"
34
+
35
+ # Every directory the index reads. Keep in sync with `INDEXED` in tools/afm-index.sh: a
36
+ # directory the index reads but the gate does not check is a directory where frontmatter
37
+ # can silently go missing, and missing frontmatter is invisible by construction.
38
+ # `modules` (3.31.0) is the one entry whose unit is a DIRECTORY per stack, so the loop below
39
+ # descends one level for it. Keep this list in sync with `INDEXED` in tools/afm-index.sh: a
40
+ # directory the index reads and the gate does not check is a directory where frontmatter can
41
+ # silently go missing, which is exactly how these 17 files shipped unindexed until now.
42
+ #
43
+ # `patterns` and `code-style` (3.40.0) are the two newest entries. `code-style/<stack>/` is
44
+ # directory-unit like `modules`, and reaches the loop below the same way — its `*/*.md` glob
45
+ # already covers a directory one level deep, `code-style/_any/` included. `patterns` is flat.
46
+ # Neither adds new required keys here: `smell`, `when_to_use`, `when_not_to_use`, `stack`,
47
+ # `concern` and `detect` all stay OPTIONAL, on purpose — requiring them would fail every
48
+ # consumer pinned below 3.40.0 the moment this release reaches it.
49
+ INDEXED="ops core rubrics templates templates/protocols interview modules patterns code-style"
50
+
51
+ RC=0
52
+
53
+ # Reads one frontmatter key. Same block definition the index uses: line 1 is `---`, the
54
+ # block ends at the next `---`.
55
+ fmkey() {
56
+ awk -v k="$2" '
57
+ NR == 1 { next }
58
+ /^---[[:space:]]*$/ { exit }
59
+ $0 ~ "^" k ":" { print; found = 1; exit }
60
+ END { exit !found }
61
+ ' "$1" 2>/dev/null
62
+ }
63
+
64
+ # Pass 0 — the frontmatter is PARSEABLE YAML with a non-empty `type`.
65
+ #
66
+ # This is clause 1 and clause 2 of OKF conformance (SPEC v0.2 11), and until 3.10.1 nothing
67
+ # checked either one. The repository declared conformance in ADR 0018 and had 59 files that no
68
+ # YAML parser could read: a `description: Session record: name` (a colon inside a plain scalar)
69
+ # and a `trigger_grep: "awk.*\."` (a backslash escape inside a double-quoted scalar) both parse
70
+ # fine to a grep and not at all to YAML.
71
+ #
72
+ # The whole point of a shared format is that a tool nobody here wrote can read the files. A
73
+ # grep-based reader hides exactly this class of break, because grep never parses.
74
+ #
75
+ # python3 is the parser. When it is absent the check SKIPS AUDIBLY, the same discipline the
76
+ # shellcheck skip follows: a silent skip is a gate that reports coverage it does not have.
77
+ if command -v python3 >/dev/null 2>&1; then
78
+ YAML_OUT="$(python3 - "$REPO" <<'PYEOF' 2>/dev/null
79
+ import sys, os, glob
80
+ try:
81
+ import yaml
82
+ except ImportError:
83
+ sys.exit(3)
84
+ repo = sys.argv[1]
85
+ roots = ["docs/**/*.md", "plugins/afm/framework-template/**/*.md"]
86
+ for pat in roots:
87
+ for p in glob.glob(os.path.join(repo, pat), recursive=True):
88
+ if os.path.basename(p) in ("index.md", "log.md", "README.md"):
89
+ continue
90
+ s = open(p, encoding="utf-8", errors="replace").read()
91
+ if not s.startswith("---\n"):
92
+ continue
93
+ end = s.find("\n---", 4)
94
+ if end < 0:
95
+ print("%s\tfrontmatter block never closes" % os.path.relpath(p, repo)); continue
96
+ try:
97
+ d = yaml.safe_load(s[4:end])
98
+ except Exception as e:
99
+ print("%s\tfrontmatter is not parseable YAML: %s" % (os.path.relpath(p, repo), str(e).split("\n")[0][:90])); continue
100
+ if not isinstance(d, dict):
101
+ print("%s\tfrontmatter is not a mapping" % os.path.relpath(p, repo)); continue
102
+ if not d.get("type"):
103
+ print("%s\tno non-empty `type:` (OKF requires exactly this one key)" % os.path.relpath(p, repo))
104
+ PYEOF
105
+ )"
106
+ RCP=$?
107
+ if [ "$RCP" = 3 ]; then
108
+ # stderr, never stdout. The runner contract says one stdout line per FINDING, and the
109
+ # aggregator counts them — so a skip printed there is reported as a rule 39 violation on
110
+ # every machine without the module. An audible skip must be audible, not a false failure.
111
+ printf 'note: rule 39 pass 0 SKIPPED — python3 has no yaml module, OKF clause 1 unchecked\n' >&2
112
+ elif [ -n "$YAML_OUT" ]; then
113
+ printf '%s\n' "$YAML_OUT" | while IFS="$(printf '\t')" read -r f m; do
114
+ printf '39\t%s\t%s\n' "$f" "$m"
115
+ done
116
+ RC=1
117
+ fi
118
+ else
119
+ printf 'note: rule 39 pass 0 SKIPPED — no python3, OKF clause 1 unchecked\n' >&2
120
+ fi
121
+
122
+ # Pass 1 — every indexed file carries the OKF profile contract.
123
+ #
124
+ # `type` is the one key OKF requires (SPEC v0.2 4.1). It replaced `kind`, which said the same
125
+ # thing under a name no other tool knows. `description` is what the generated index prints
126
+ # INSTEAD of opening the body, so a file without one costs a full read to answer "what is this".
127
+ # `tier` is the AFM extension that tells the index what it may skip: a `cold` file is never
128
+ # loaded at run time, and the index must not pay to open it.
129
+ #
130
+ # All three fail the same silent way. The index reads frontmatter and nothing else, so the file
131
+ # is not reported as broken. It is simply absent from the answer.
132
+ for d in $INDEXED; do
133
+ [ -d "$FT/$d" ] || continue
134
+ for f in "$FT/$d"/*.md "$FT/$d"/*/*.md; do
135
+ # `templates` and `templates/protocols` are BOTH in INDEXED, so the */*.md glob re-scans
136
+ # what the deeper entry already scanned. Harmless while the files conform; the day one
137
+ # loses `load:` the gate prints the same finding twice and reads as two defects.
138
+ _sub="$(dirname "${f#"$FT/"}")"
139
+ case " $INDEXED " in *" $_sub "*) [ "$_sub" = "$d" ] || continue ;; esac
140
+ [ -f "$f" ] || continue
141
+ case "$(basename "$f")" in README.md|index.md|log.md) continue ;; esac
142
+ rel="${f#"$REPO"/}"
143
+ if [ "$(head -n1 "$f")" != "---" ]; then
144
+ printf '39\t%s\tno frontmatter — invisible to the index\n' "$rel"
145
+ RC=1
146
+ continue
147
+ fi
148
+ if fmkey "$f" kind >/dev/null; then
149
+ printf '39\t%s\t`kind:` is the retired name — OKF requires `type:`\n' "$rel"
150
+ RC=1
151
+ fi
152
+ fmkey "$f" type >/dev/null || {
153
+ printf '39\t%s\tfrontmatter has no `type:` — the one key OKF requires\n' "$rel"; RC=1; }
154
+ fmkey "$f" description >/dev/null || {
155
+ printf '39\t%s\tno `description:` — the index would have to open the body\n' "$rel"; RC=1; }
156
+ load="$(fmkey "$f" load | sed 's/^load:[[:space:]]*//')"
157
+ case "$load" in
158
+ hot|warm|cold) ;;
159
+ "") printf '39\t%s\tno `load:` — the index cannot know what to skip\n' "$rel"; RC=1 ;;
160
+ *) printf '39\t%s\t`load: %s` outside the closed vocabulary (hot|warm|cold)\n' "$rel" "$load"; RC=1 ;;
161
+ esac
162
+ done
163
+ done
164
+
165
+ # Pass 1b — a rubric declares HOW it fires and WHAT it says.
166
+ #
167
+ # Same failure as a missing `kind:`, one family over: `afm-index.sh --triggers` and
168
+ # `afm-detect.sh` both read frontmatter and nothing else, so a rubric without `fires_on` is
169
+ # invisible to the index and never fires at write time. The reader gets a shorter list and no
170
+ # signal that anything was left out. A rubric that nothing can trigger is a document nobody
171
+ # will ever be pointed at, which is the exact condition this whole line of releases exists to
172
+ # remove.
173
+ RUBRICS="$FT/rubrics"
174
+ if [ -d "$RUBRICS" ]; then
175
+ for f in "$RUBRICS"/*.md; do
176
+ [ -f "$f" ] || continue
177
+ case "$(basename "$f")" in README.md|index.md|log.md) continue ;; esac
178
+ rel="${f#"$REPO"/}"
179
+ [ "$(head -n1 "$f")" = "---" ] || continue # pass 1 already reported this
180
+ fo="$(fmkey "$f" fires_on)"
181
+ if [ -z "$fo" ]; then
182
+ printf '39\t%s\trubric with no `fires_on:` — invisible to the index and never fires\n' "$rel"
183
+ RC=1
184
+ continue
185
+ fi
186
+ case "$fo" in
187
+ *file-shape|*repo-shape|*op-call|*event|*cadence) ;;
188
+ *) printf '39\t%s\t`fires_on:` outside the closed vocabulary (file-shape|repo-shape|op-call|event|cadence)\n' "$rel"; RC=1 ;;
189
+ esac
190
+ fmkey "$f" fires_when >/dev/null || {
191
+ printf '39\t%s\trubric with no `fires_when:` — nothing to name when it fires\n' "$rel"; RC=1; }
192
+ done
193
+ fi
194
+
195
+ # Pass 1c — a procedure and a learning declare when they start.
196
+ #
197
+ # Not to make them FIRE — an `event` trigger cannot fire and is not supposed to. To make them
198
+ # DISCOVERABLE: the condition that starts a runbook used to live in a `When to run it` bullet
199
+ # inside the body, so answering "is there a runbook for this?" meant opening every file in the
200
+ # family. One line in the index answers it instead. This repository reached 2026-08 with 26
201
+ # gotchas and 3 procedures, and the family that nobody creates is the family nobody can find.
202
+ #
203
+ # An `event` or `cadence` entry must carry NO `trigger_*` key. Fabricating a mechanical field
204
+ # for a human judgment is the failure this vocabulary exists to prevent: it would dilute the
205
+ # confidence that rubrics and guardrails earned by being genuinely mechanical.
206
+ for d in docs/procedures docs/learnings; do
207
+ [ -d "$REPO/$d" ] || continue
208
+ for f in "$REPO/$d"/*.md; do
209
+ [ -f "$f" ] || continue
210
+ case "$(basename "$f")" in README.md|index.md|log.md) continue ;; esac
211
+ rel="${f#"$REPO"/}"
212
+ if [ "$(head -n1 "$f")" != "---" ]; then
213
+ printf '39\t%s\tno frontmatter — invisible to the trigger index\n' "$rel"; RC=1; continue
214
+ fi
215
+ fo="$(fmkey "$f" fires_on)"
216
+ [ -n "$fo" ] || { printf '39\t%s\tno `fires_on:` — the index cannot say how this starts\n' "$rel"; RC=1; }
217
+ fmkey "$f" fires_when >/dev/null || {
218
+ printf '39\t%s\tno `fires_when:` — nothing to show in the index\n' "$rel"; RC=1; }
219
+ case "$fo" in
220
+ *event|*cadence)
221
+ if grep -qE '^trigger_' "$f"; then
222
+ printf '39\t%s\t`%s` declares a trigger_* key — a human-recognized moment has no mechanical trigger\n' "$rel" "$fo"
223
+ RC=1
224
+ fi ;;
225
+ esac
226
+ done
227
+ done
228
+
229
+ # Pass 1d — the INSTANCE families carry the same contract as the plugin ones.
230
+ #
231
+ # Since 3.9.1 a gotcha, a hard rule, an ADR and a rubric are all concept files with frontmatter.
232
+ # The generated index reads `description` from them, so a file without one costs a full read to
233
+ # answer "what is this", which is the exact cost this whole layer exists to remove. A feature,
234
+ # an ADR, research, a protocol and an evolution report carry this SAME base contract — the NEW
235
+ # routing keys each family gained (`status`, `for_paths`, `trigger_path`, `fires_on`, …) are
236
+ # checked nowhere below, and that is deliberate: they are OPTIONAL, and requiring them here would
237
+ # fail every consumer pinned at 3.6.0 or below the moment this release reaches them.
238
+ #
239
+ # `check_base_contract` is shared with Pass 1e below, because a feature folder needs the exact
240
+ # same three checks one directory level deeper than a flat family — two copies of this body
241
+ # would let a fix land in one and rot silently in the other.
242
+ check_base_contract() {
243
+ f="$1"; rel="${f#"$REPO"/}"
244
+ if [ "$(head -n1 "$f")" != "---" ]; then
245
+ printf '39\t%s\tno frontmatter — invisible to the index\n' "$rel"; RC=1; return
246
+ fi
247
+ fmkey "$f" type >/dev/null || { printf '39\t%s\tno `type:`\n' "$rel"; RC=1; }
248
+ fmkey "$f" description >/dev/null || { printf '39\t%s\tno `description:`\n' "$rel"; RC=1; }
249
+ load="$(fmkey "$f" load | sed 's/^load:[[:space:]]*//')"
250
+ case "$load" in hot|warm|cold) ;; *) printf '39\t%s\t`load:` absent or outside hot|warm|cold\n' "$rel"; RC=1 ;; esac
251
+ }
252
+
253
+ for d in docs docs/gotchas docs/rules docs/adr docs/rubrics docs/guardrails docs/protocols \
254
+ docs/sessions docs/evolution docs/research docs/measurements \
255
+ docs/code-style docs/patterns; do
256
+ [ -d "$REPO/$d" ] || continue
257
+ for f in "$REPO/$d"/*.md; do
258
+ [ -f "$f" ] || continue
259
+ case "$(basename "$f")" in README.md|index.md|log.md) continue ;; esac
260
+ check_base_contract "$f"
261
+ done
262
+ done
263
+
264
+ # Pass 1e — a feature folder carries the same base contract, one level deeper than every other
265
+ # instance family. `docs/features/NNN-slug/{spec,plan,tasks}.md` is the one shape Pass 1d's flat
266
+ # `*.md` glob cannot reach — a feature's three files sit inside their own folder. Skipping it
267
+ # would leave every feature file invisible to the gate while `afm-index.sh --triggers` and
268
+ # `--for` already read them, the exact silent gap this whole rule exists to close.
269
+ if [ -d "$REPO/docs/features" ]; then
270
+ for f in "$REPO"/docs/features/*/*.md; do
271
+ [ -f "$f" ] || continue
272
+ case "$(basename "$f")" in README.md|index.md|log.md) continue ;; esac
273
+ check_base_contract "$f"
274
+ done
275
+ fi
276
+
277
+ # Pass 2 — an op declares more, and what it declares must be true.
278
+ for f in "$OPS"/*.md; do
279
+ [ -f "$f" ] || continue
280
+ rel="${f#"$REPO"/}"
281
+ base="$(basename "$f" .md)"
282
+
283
+ [ "$(head -n1 "$f")" = "---" ] || continue
284
+
285
+ for key in writes composes; do
286
+ if ! fmkey "$f" "$key" >/dev/null; then
287
+ printf '39\t%s\tfrontmatter has no `%s:`\n' "$rel" "$key"
288
+ RC=1
289
+ fi
290
+ done
291
+
292
+ # A `composes:` entry naming an op that does not exist is worse than no entry: the
293
+ # reader trusts it precisely because it read the frontmatter instead of the body.
294
+ deps="$(fmkey "$f" composes | sed 's/^composes:[[:space:]]*//; s/^\[//; s/\]$//; s/"//g; s/,/ /g')"
295
+ for d in $deps; do
296
+ [ -n "$d" ] || continue
297
+ if [ ! -f "$OPS/$d.md" ]; then
298
+ printf '39\t%s\tcomposes `%s`, which is not an op\n' "$rel" "$d"
299
+ RC=1
300
+ fi
301
+ if [ "$d" = "$base" ]; then
302
+ printf '39\t%s\tcomposes itself\n' "$rel"
303
+ RC=1
304
+ fi
305
+ done
306
+ done
307
+
308
+ # Pass 4 — the generated indexes match the directories they describe.
309
+ #
310
+ # OKF endorses generating index.md (SPEC 8) and its conformance never checks an index against
311
+ # its own directory (11 is structural only). That check is left to the producer, so it lives
312
+ # here. A stale index is the worst kind of doc: it is the cheap thing a reader consults INSTEAD
313
+ # of listing the directory, so when it lies the reader never finds out.
314
+ IDX="$FT/tools/afm-index.sh"
315
+ if [ -x "$IDX" ] && [ -d "$REPO/docs" ]; then
316
+ OUT="$(cd "$REPO" && "$IDX" --check-index 2>/dev/null)"
317
+ if [ -n "$OUT" ]; then
318
+ printf '%s\n' "$OUT" | while IFS= read -r l; do
319
+ printf '39\t%s\tgenerated index is stale — run afm-index.sh --materialize\n' "${l#stale index: }"
320
+ done
321
+ RC=1
322
+ fi
323
+ fi
324
+
325
+ exit $RC
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env bash
2
+ # Hard rule PLUGIN-40 — a doc under `docs/` cites no path that does not exist.
3
+ #
4
+ # Why it is a GATE and not prose. The lesson "a rename is finished when the WRITERS and the
5
+ # READERS both move" was already written down, in `_focus.md` and in a gotcha, and on 2026-08-30
6
+ # it failed twice inside the release that exists because of it: a runner glob kept matching only
7
+ # the old name shape, and then seven rule files kept showing a label pointing at files that had
8
+ # just moved. `afm-health.sh` DETECTED the second one and reported it at SessionStart — where it
9
+ # was read and passed over. Detection at the start of a session is not the same as refusal at the
10
+ # commit, and only one of the two arrives while the change is still in the hand of whoever made it.
11
+ #
12
+ # The check that makes this cheap to keep honest: a cited path resolves from the REPOSITORY ROOT
13
+ # **or** from the directory of the file that cites it. Both conventions are alive in a real
14
+ # instance, and a checker that knows only one invents findings — which is how a gate gets
15
+ # switched off.
16
+ set -u
17
+ : "${1:-staged}" # the rule is about the CONTENT of the docs, not about the diff
18
+ REPO="${AFM_GATE_REPO:-$PWD}"
19
+ . "${AFM_GATE_LIB:?}/scope.sh"
20
+ . "${AFM_GATE_LIB:?}/ratchet.sh"
21
+
22
+ [ -d "$REPO/docs" ] || exit 0
23
+ TRACKED="$(cd "$REPO" && git ls-files 2>/dev/null || true)"
24
+
25
+ # The findings are collected, not streamed. Every loop below runs inside a pipeline subshell, so
26
+ # a counter incremented in there dies with it — a runner that prints its findings and still exits
27
+ # 0 is a verifier that reports and never refuses, which is the exact failure this rule is about.
28
+ FOUND="$(
29
+ while IFS= read -r f; do
30
+ # NORMATIVE families only. The scope is not "every doc" and that is deliberate: a rubric
31
+ # teaches with an ILLUSTRATIVE path (`src/lib/stripe/checkout.ts`), a research dossier quotes a
32
+ # hypothetical one, and a feature's `tasks.md` is a historical plan that names paths as they
33
+ # were on the day it was written. Pointing the gate at those produces 44 findings that are all
34
+ # correct notation, and a gate that starts red is switched off the next day — which would take
35
+ # every other rule in the same aggregator with it.
36
+ case "$f" in
37
+ docs/afm.md|docs/ach.md|docs/prd.md|docs/ust.md|docs/_focus.md) ;;
38
+ docs/adr/*|docs/rules/*|docs/gotchas/*|docs/learnings/*|docs/guardrails/*|docs/procedures/*) ;;
39
+ *) continue ;;
40
+ esac
41
+ [ -f "$REPO/$f" ] || continue
42
+ ratchet_exempt PLUGIN-40 "$f" && continue
43
+ dir="$(dirname "$f")"
44
+
45
+ while IFS= read -r line; do
46
+ # A line that ANNOUNCES a removal is a historical record, not a reference. An ADR's job is
47
+ # to cite what stopped existing.
48
+ printf '%s' "$line" | grep -qiE 'remov|deprecat|legacy|no longer|used to|until [0-9]' && continue
49
+ printf '%s\n' "$line" | grep -oE '`[A-Za-z0-9_./-]+\.[A-Za-z]+`' | tr -d '`' | while IFS= read -r p; do
50
+ case "$p" in
51
+ */*) ;; # only a PATH; a bare filename in prose is not a reference
52
+ *) continue ;;
53
+ esac
54
+ case "$p" in
55
+ http*|*..*) continue ;;
56
+ .*/*) continue ;; # a dot-dir path is the notation for a materialized instance
57
+ /docs/*|/*) continue ;; # `/docs/x.md` is AFM notation for the consumer, not an absolute path
58
+ *.*.*/*) continue ;; # a domain, not a path
59
+ *NNN*|*NN-*|*YYYY*|*MM.md|*'{{'*) continue ;; # a placeholder shape, not a path
60
+ */_shared/*) continue ;; # a proposed layout, not one that exists
61
+ esac
62
+ [ -e "$REPO/$p" ] && continue # from the repository root
63
+ [ -e "$REPO/$dir/$p" ] && continue # from the citing file's own directory
64
+ esc="$(printf '%s' "$p" | sed 's/[.[*^$\\]/\\&/g')"
65
+ printf '%s\n' "$TRACKED" | grep -qE "(^|/)${esc}\$" && continue
66
+ printf 'PLUGIN-40\t%s\t%s does not exist, from the repository root or from %s/\n' "$f" "$p" "$dir"
67
+ done
68
+ done < "$REPO/$f"
69
+ done < <(scope_all '*.md')
70
+ )"
71
+
72
+ [ -z "$FOUND" ] && exit 0
73
+ printf '%s\n' "$FOUND"
74
+ exit 1
@@ -0,0 +1,55 @@
1
+ # Core rule runners — the ones the plugin ships
2
+
3
+ Every runner here enforces a rule about **the `docs/` instance that AFM materializes**, never
4
+ about a consumer's own code. That is why these travel and the ones in
5
+ [`../rules/`](../rules/README.md) do not.
6
+
7
+ | Runner | Rule |
8
+ | --- | --- |
9
+ | `PLUGIN-13-secrets.sh` | a token or a secret never leaks, the `docs/sessions/` narrative included |
10
+ | `PLUGIN-21-code-language.sh` | INALIENABLE — code and tests are written in English |
11
+ | `PLUGIN-35-gate-complete.sh` | a rule with a 0/1 trigger has a versioned runner in the gate |
12
+ | `PLUGIN-36-absolute-path.sh` | a doc under `docs/` carries no filesystem absolute path |
13
+ | `PLUGIN-37-prose-language.sh` | the prose of an artifact is English |
14
+ | `PLUGIN-38-parser-marker.sh` | a Portuguese literal never survives in a script or a quoted marker |
15
+ | `PLUGIN-39-frontmatter-ops.sh` | an indexed file carries the OKF contract |
16
+
17
+ ## The namespace
18
+
19
+ The filename is `PLUGIN-<n>-<slug>.sh`, and `<n>` matches the `PLUGIN-<n>.` entry in
20
+ `docs/afm.md § 3`. The aggregator reads the whole `PLUGIN-<n>` as the rule ID, and `--audit`
21
+ cross-checks rule and runner in both directions inside each namespace.
22
+
23
+ A project runner with the SAME number wins over the shipped one, so a consumer overrides a core
24
+ rule by dropping `<n>-<slug>.sh` into `../rules/` — no fork of the plugin.
25
+
26
+ ## Exemptions go in the ratchet, never in the runner
27
+
28
+ A runner here must not carry a `case ... continue ;;` arm that names a path. A path exists in one
29
+ repository; a shipped runner runs in all of them. Write the exemption as a ratchet row instead:
30
+
31
+ ```
32
+ rule region count note
33
+ PLUGIN-38 tools/lang-detector.sh exempt the accented alternation IS the detector
34
+ ```
35
+
36
+ `exempt` is not a budget of zero. A budget says "this many violations are tolerated and the
37
+ number may only fall"; an exemption says "this file is not what the rule is about". The ratchet
38
+ also makes the exemption **visible**: it sits in a versioned table a reviewer reads, instead of
39
+ inside code nobody re-opens.
40
+
41
+ A **shape** that holds in every instance — an archived `CHANGELOG`, `core/afm.md` quoting a bad
42
+ identifier as the example of what rule PLUGIN-21 forbids — stays in the runner. It is not a path.
43
+
44
+ ## Contract
45
+
46
+ Identical to a project runner. See [`../rules/README.md`](../rules/README.md). Two additions,
47
+ both learned the hard way on 2026-08-30:
48
+
49
+ - **Never derive the repository from `$0`.** Every runner used to run
50
+ `REPO="$(cd "$(dirname "$0")/../.." && pwd)"`, which is only correct while the runner sits two
51
+ levels below the root. The moment one shipped — five levels down — that expression pointed at
52
+ `tools/`, and rule PLUGIN-37 reported "cannot be decided" against a file that was right there.
53
+ Read `AFM_GATE_REPO`, which the aggregator exports.
54
+ - **Resolve a sibling tool through `$0`, not through a repository path.** The aggregator ships
55
+ beside this directory in the plugin and in a materialized `docs/tools/gate/` alike.