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

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 +173 -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,56 @@
1
+ #!/usr/bin/env bash
2
+ # Hard rule 37 — the prose of an artifact is English.
3
+ #
4
+ # It complements rule 21, which covers only an IDENTIFIER and a TEST NAME carrying a
5
+ # non-ASCII character. Rule 21 is blind to prose: a `.md` written entirely in Portuguese
6
+ # passes it clean, and that is exactly how this repo reached 2026-08 with 175 Portuguese
7
+ # doc files while the gate reported green.
8
+ #
9
+ # The measurement itself lives in `framework-template/tools/afm-lang.sh` and NOT here.
10
+ # Rule 34: one canonical source. The same threshold has to decide at `PreToolUse`, where
11
+ # the text is still a proposal, and at commit, where it is already a fact — two copies of
12
+ # a measured threshold drift apart in silence, and the commit gate would then contradict
13
+ # the hook that had just approved the write.
14
+ set -u
15
+ MODE="${1:-staged}"; : "$MODE" # COUNTING rule: the scope is the whole repo
16
+ REPO="${AFM_GATE_REPO:-$PWD}"
17
+ . "${AFM_GATE_LIB:?}/scope.sh"
18
+ . "${AFM_GATE_LIB:?}/ratchet.sh"
19
+
20
+ # Resolved through $0, not $REPO (see README "Resolve a sibling tool through $0"): the tool
21
+ # ships beside this directory both in the plugin (framework-template/tools/, two levels up from
22
+ # tools/gate/core-rules/) and in a materialized consumer (docs/tools/, same two levels up from
23
+ # docs/tools/gate/core-rules/). $REPO/plugins/afm/framework-template/tools/afm-lang.sh only
24
+ # exists when this very repository is being dogfooded through ./test/ — in a project bootstrapped
25
+ # by afm init it never exists, so this used to report "missing" against a tool that was right
26
+ # there at docs/tools/afm-lang.sh.
27
+ LANG_TOOL="$(dirname "$0")/../../afm-lang.sh"
28
+ if [ ! -x "$LANG_TOOL" ]; then
29
+ printf 'PLUGIN-37\t%s\tthe language tool is missing or not executable — rule 37 cannot be decided\n' \
30
+ "$LANG_TOOL"
31
+ exit 1
32
+ fi
33
+
34
+ RC=0
35
+ while IFS= read -r f; do
36
+ [ -f "$REPO/$f" ] || continue
37
+ # Exemptions live in the ratchet, never in this file. A `case` arm naming a path is an
38
+ # exclusion that only one repository can have, invisible to any reviewer who does not read
39
+ # the runner. See `ratchet_exempt` in `lib/ratchet.sh`.
40
+ ratchet_exempt PLUGIN-37 "$f" && continue
41
+ # An archived CHANGELOG is a frozen historical record everywhere, not a per-project choice,
42
+ # so it stays in code: it is a SHAPE, not a path.
43
+ case "$f" in */CHANGELOG-archive.md) continue ;; esac
44
+ D="$("$LANG_TOOL" --density "$REPO/$f" 2>/dev/null)" || continue
45
+ # Exit 1 is the accusation; exit 2 is "too short to be a ratio" and has to read as a pass.
46
+ # Collapsing the two reported 83 extra files on the first run of this delegation — every
47
+ # short eval fixture — because `if ! cmd` cannot tell an accusation from an abstention.
48
+ "$LANG_TOOL" --verdict --path "$f" "$REPO/$f" >/dev/null 2>&1; V=$?
49
+ if [ "$V" -eq 1 ]; then
50
+ printf '37\t%s\tPortuguese prose (density %s, %s hits in %s words)\n' \
51
+ "$f" "$(printf '%s' "$D" | cut -f3)" "$(printf '%s' "$D" | cut -f2)" "$(printf '%s' "$D" | cut -f1)"
52
+ RC=1
53
+ fi
54
+ done < <(scope_all '*.md')
55
+
56
+ exit $RC
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ # Hard rule 38 — a Portuguese literal inside a script or inside a quoted marker.
3
+ #
4
+ # Why it exists: "the data was translated and the parser was not" was the MOST FREQUENT failure
5
+ # of the english-first migration, and NONE of the occurrences was caught by a gate. Three of
6
+ # them killed a mechanism in silence:
7
+ #
8
+ # - afm-health.sh matched the Portuguese "superseded by ADR-" heading while the ADRs said
9
+ # something else, so vital sign A4 (broken ADR chain) found zero, always.
10
+ # - ops/restructure.md read the Portuguese bite and last-bite fields while the template wrote
11
+ # `Bites:`/`last:`, so the dead-gotcha prune never found anything.
12
+ # - guardrails/README.md documented the Portuguese key names while the files used `signal:`
13
+ # and `trigger:`, so a new guardrail would be born invisible.
14
+ #
15
+ # The symptom never varies: the literal stays Portuguese and the target has already changed.
16
+ # Because the repo is english-first (rule 37), the HEURISTIC is cheap and precise — an accented
17
+ # word inside a `.sh`, or inside backticks in a plugin `.md`, is a candidate. It does not prove
18
+ # the drift; it points at where the drift lives. The human triages.
19
+ #
20
+ # It complements rule 37, which looks only at prose DENSITY in `.md` and is therefore blind both
21
+ # to `.sh` and to an isolated literal inside an English doc.
22
+ #
23
+ # Since 2026-08-27 `test/` is NO LONGER exempt. It used to be, by an explicit decision that the
24
+ # gates of this repo speak to the maintainer and are not distributed surface. Translating them
25
+ # found three dead mechanisms this exemption had been hiding for months — rule 34's check (a),
26
+ # the plugin-effect boundary in refs-lint, and the protocol assert in update-rehearsal — each one
27
+ # a parser left on a spelling nothing produces any more. The exemption was protecting exactly the
28
+ # files that most needed this rule.
29
+ set -u
30
+ MODE="${1:-staged}"; : "$MODE" # COUNTING rule: the scope is the whole repo
31
+ REPO="${AFM_GATE_REPO:-$PWD}"
32
+ . "${AFM_GATE_LIB:?}/scope.sh"
33
+ . "${AFM_GATE_LIB:?}/ratchet.sh"
34
+
35
+ # LC_ALL=C: the [À-ÿ] range is INVALID outside a UTF-8 locale and makes grep abort.
36
+ # The prototype of this runner swallowed that error with 2>/dev/null and reported "0 findings"
37
+ # over a corpus with 6 known cases — a gate that swallows its own error is indistinguishable
38
+ # from one that passed, which is exactly the bug hunted here.
39
+ # Hence: an explicit alternation, no range, and the grep NEVER goes to /dev/null.
40
+ export LC_ALL=C
41
+ PT='ã|ç|õ|á|é|í|ó|ú|â|ê|ô|à|Ã|Ç|Õ|Á|É|Í|Ó|Ú|Â|Ê|Ô|À|ambos|planejada|gatilho|Gatilho'
42
+
43
+ RC=0
44
+ report() { printf '38\t%s\t%s\n' "$1" "$2"; RC=1; }
45
+
46
+ # --- 1. shell: any accented word (a comment included: it is instruction) ------
47
+ while IFS= read -r f; do
48
+ [ -f "$REPO/$f" ] || continue
49
+ # Exemptions live in the ratchet, never in this file. A `case` arm naming a path is an
50
+ # exclusion that only one repository can have, invisible to any reviewer who does not read
51
+ # the runner. See `ratchet_exempt` in `lib/ratchet.sh`.
52
+ ratchet_exempt PLUGIN-38 "$f" && continue
53
+ n=$(grep -cE "$PT" "$REPO/$f") || n=0
54
+ [ "$n" -gt 0 ] && report "$f" "${n} line(s) with a Portuguese literal in a script"
55
+ done < <(scope_all '*.sh')
56
+
57
+ # --- 2. plugin markdown: a backticked literal (marker candidate) -------------
58
+ while IFS= read -r f; do
59
+ [ -f "$REPO/$f" ] || continue
60
+ # Exemptions live in the ratchet, never in this file. A `case` arm naming a path is an
61
+ # exclusion that only one repository can have, invisible to any reviewer who does not read
62
+ # the runner. See `ratchet_exempt` in `lib/ratchet.sh`.
63
+ ratchet_exempt PLUGIN-38 "$f" && continue
64
+ # These three are SHAPES that hold in any AFM instance, not paths one project happens to
65
+ # have, so they stay in code: a changelog is a historical record; `core/afm.md` quotes a
66
+ # Portuguese identifier as the example of what rule 21 forbids, and the example has to be
67
+ # wrong to be an example; `ops/deliver.md` lists the override phrases, which are USER SPEECH.
68
+ case "$f" in
69
+ *CHANGELOG.md|*CHANGELOG-archive.md) continue ;;
70
+ */core/afm.md|*/ops/deliver.md|*/ops/update.md) continue ;;
71
+ esac
72
+ # REAL backtick pairing: splitting on the backtick leaves the EVEN fields inside the code
73
+ # span. A naive grep for a backtick, anything, a Portuguese word, anything, a backtick also
74
+ # matches the SPACE BETWEEN two spans — that is how the first version reported a French loan
75
+ # phrase as a Portuguese marker.
76
+ n=$(awk -v pat="$PT" '
77
+ { k=split($0, S, "`"); for (i=2; i<=k; i+=2) if (S[i] ~ pat) c++ }
78
+ END { print c+0 }' "$REPO/$f")
79
+ [ "$n" -gt 0 ] && report "$f" "${n} backticked Portuguese literal(s)"
80
+ done < <(scope_all 'plugins/*/**/*.md' 'plugins/*/*.md')
81
+
82
+ exit $RC
@@ -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.