@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.
- package/bin/afm +4 -0
- package/content/.coupling-baseline.tsv +1 -0
- package/content/.pending-commands.txt +2 -0
- package/content/CHANGELOG-archive.md +806 -0
- package/content/CHANGELOG.md +4256 -0
- package/content/PRINCIPLES.md +121 -0
- package/content/README-light.md +160 -0
- package/content/README.md +152 -0
- package/content/code-style/_any/file-size.md +62 -0
- package/content/code-style/_any/imports.md +68 -0
- package/content/code-style/_any/naming.md +72 -0
- package/content/code-style/_any/test-placement.md +57 -0
- package/content/code-style/nextjs-app-router/imports.md +50 -0
- package/content/code-style/nextjs-app-router/naming.md +50 -0
- package/content/code-style/prisma-postgres/error-boundary.md +55 -0
- package/content/code-style/prisma-postgres/imports.md +61 -0
- package/content/code-style/shadcn-ui/naming.md +48 -0
- package/content/code-style/stripe-billing/naming.md +76 -0
- package/content/code-style/trigger-dev-v4/imports.md +58 -0
- package/content/code-style/trigger-dev-v4/naming.md +49 -0
- package/content/code-style/trigger-dev-v4/test-placement.md +61 -0
- package/content/code-style/trpc-v11/error-boundary.md +51 -0
- package/content/code-style/trpc-v11/file-size.md +44 -0
- package/content/code-style/trpc-v11/naming.md +60 -0
- package/content/code-style/trpc-v11/test-placement.md +52 -0
- package/content/core/ach.md +265 -0
- package/content/core/afm.md +496 -0
- package/content/core/gotchas.md +18 -0
- package/content/core/prd.md +143 -0
- package/content/core/protocol.md +160 -0
- package/content/core/ust.md +65 -0
- package/content/guardrails/README.md +55 -0
- package/content/guardrails/dsl-com-ordem-obrigatoria.md +47 -0
- package/content/guardrails/exaustividade-de-union.md +48 -0
- package/content/guardrails/fronteira-de-import.md +57 -0
- package/content/hooks/README.md +72 -0
- package/content/interview/bootstrap.md +415 -0
- package/content/interview/promotion.md +117 -0
- package/content/interview/refactor.md +381 -0
- package/content/interview/triggers.md +109 -0
- package/content/modules/nextjs-app-router/ach.patch.md +44 -0
- package/content/modules/nextjs-app-router/gotchas.seed.md +78 -0
- package/content/modules/nextjs-app-router/rules.patch.md +35 -0
- package/content/modules/prisma-postgres/ach.patch.md +65 -0
- package/content/modules/prisma-postgres/rules.patch.md +28 -0
- package/content/modules/shadcn-ui/ach.patch.md +42 -0
- package/content/modules/stripe-billing/ach.patch.md +46 -0
- package/content/modules/stripe-billing/gotchas.seed.md +73 -0
- package/content/modules/stripe-billing/rules.patch.md +30 -0
- package/content/modules/trigger-dev-v4/ach.patch.md +64 -0
- package/content/modules/trigger-dev-v4/gotchas.seed.md +53 -0
- package/content/modules/trigger-dev-v4/rules.patch.md +35 -0
- package/content/modules/trpc-v11/ach.patch.md +56 -0
- package/content/modules/trpc-v11/rules.patch.md +34 -0
- package/content/modules/vireya-ui/ach.patch.md +85 -0
- package/content/modules/vireya-ui/gotchas.seed.md +98 -0
- package/content/modules/vireya-ui/rules.patch.md +36 -0
- package/content/ops/adr.md +54 -0
- package/content/ops/analyze.md +129 -0
- package/content/ops/clarify.md +86 -0
- package/content/ops/deliver.md +211 -0
- package/content/ops/diagnose.md +146 -0
- package/content/ops/evolve.md +317 -0
- package/content/ops/export-template.md +97 -0
- package/content/ops/generalize.md +120 -0
- package/content/ops/gotcha.md +46 -0
- package/content/ops/plan.md +61 -0
- package/content/ops/policy.md +63 -0
- package/content/ops/promote.md +72 -0
- package/content/ops/protocol.md +51 -0
- package/content/ops/reconcile.md +44 -0
- package/content/ops/reflect.md +180 -0
- package/content/ops/remediate.md +146 -0
- package/content/ops/restructure.md +145 -0
- package/content/ops/rule.md +165 -0
- package/content/ops/specify.md +62 -0
- package/content/ops/tasks.md +67 -0
- package/content/ops/update.md +223 -0
- package/content/ops/verify.md +69 -0
- package/content/patterns/adapter.md +46 -0
- package/content/patterns/facade.md +47 -0
- package/content/patterns/factory.md +46 -0
- package/content/patterns/null-object.md +55 -0
- package/content/patterns/result-vs-exception.md +52 -0
- package/content/patterns/strategy.md +45 -0
- package/content/patterns/template-method-vs-composition.md +50 -0
- package/content/questions/adr.questions.yaml +14 -0
- package/content/questions/analyze.questions.yaml +12 -0
- package/content/questions/bootstrap-mapping.questions.yaml +14 -0
- package/content/questions/bootstrap-rules.questions.yaml +10 -0
- package/content/questions/bootstrap-stack.questions.yaml +23 -0
- package/content/questions/clarify.questions.yaml +10 -0
- package/content/questions/code-style.questions.yaml +12 -0
- package/content/questions/confirm.questions.yaml +12 -0
- package/content/questions/deliver.questions.yaml +23 -0
- package/content/questions/evolve.questions.yaml +15 -0
- package/content/questions/export-template-apply.questions.yaml +11 -0
- package/content/questions/export-template.questions.yaml +23 -0
- package/content/questions/gates.questions.yaml +27 -0
- package/content/questions/generalize.questions.yaml +13 -0
- package/content/questions/gotcha.questions.yaml +11 -0
- package/content/questions/init-template.questions.yaml +10 -0
- package/content/questions/init.questions.yaml +28 -0
- package/content/questions/plan.questions.yaml +14 -0
- package/content/questions/refactor.questions.yaml +23 -0
- package/content/questions/reflect.questions.yaml +13 -0
- package/content/questions/remediate.questions.yaml +27 -0
- package/content/questions/restructure.questions.yaml +15 -0
- package/content/questions/rf-mapping.questions.yaml +12 -0
- package/content/questions/rule-scope.questions.yaml +12 -0
- package/content/questions/rule.questions.yaml +12 -0
- package/content/questions/specify.questions.yaml +23 -0
- package/content/questions/update-artifacts.questions.yaml +10 -0
- package/content/questions/update-strings.questions.yaml +14 -0
- package/content/questions/update.questions.yaml +10 -0
- package/content/questions/verify.questions.yaml +9 -0
- package/content/roles/executor.md +18 -0
- package/content/roles/explorer.md +22 -0
- package/content/roles/reviewer.md +23 -0
- package/content/rubrics/enum-vs-union-vs-branded.md +40 -0
- package/content/rubrics/episodic-vs-semantic-boundary.md +37 -0
- package/content/rubrics/error-classification.md +98 -0
- package/content/rubrics/failure-classification.md +113 -0
- package/content/rubrics/negative-filters.md +38 -0
- package/content/rubrics/solid-triggers.md +121 -0
- package/content/rubrics/template-vs-streaming-precedence.md +34 -0
- package/content/rubrics/validation-boundary.md +94 -0
- package/content/rubrics/when-to-create-dsl.md +65 -0
- package/content/rubrics/when-to-create-lib.md +49 -0
- package/content/rubrics/when-to-create-module.md +39 -0
- package/content/rubrics/when-to-evolve-methodology.md +62 -0
- package/content/skills/adr.md +42 -0
- package/content/skills/analyze.md +34 -0
- package/content/skills/clarify.md +34 -0
- package/content/skills/deliver.md +42 -0
- package/content/skills/diagnose.md +50 -0
- package/content/skills/evolve.md +67 -0
- package/content/skills/export-template.md +40 -0
- package/content/skills/generalize.md +47 -0
- package/content/skills/gotcha.md +40 -0
- package/content/skills/init.md +82 -0
- package/content/skills/plan.md +34 -0
- package/content/skills/policy.md +31 -0
- package/content/skills/promote.md +41 -0
- package/content/skills/protocol.md +42 -0
- package/content/skills/reconcile.md +49 -0
- package/content/skills/refactor.md +84 -0
- package/content/skills/reflect.md +53 -0
- package/content/skills/remediate.md +39 -0
- package/content/skills/restructure.md +52 -0
- package/content/skills/rule.md +44 -0
- package/content/skills/specify.md +34 -0
- package/content/skills/tasks.md +34 -0
- package/content/skills/update.md +56 -0
- package/content/skills/verify.md +31 -0
- package/content/templates/adr.template.md +51 -0
- package/content/templates/evolution-report.template.md +129 -0
- package/content/templates/feature-plan.template.md +86 -0
- package/content/templates/feature-research.template.md +57 -0
- package/content/templates/feature-spec.template.md +86 -0
- package/content/templates/feature-tasks.induced.template.md +67 -0
- package/content/templates/feature-tasks.template.md +60 -0
- package/content/templates/focus.template.md +30 -0
- package/content/templates/gotcha.template.md +31 -0
- package/content/templates/guardrail-runner.template.md +81 -0
- package/content/templates/guardrail.template.md +63 -0
- package/content/templates/learning.template.md +40 -0
- package/content/templates/procedure.template.md +47 -0
- package/content/templates/protocols/p1.md +29 -0
- package/content/templates/protocols/p2.md +28 -0
- package/content/templates/protocols/p3.md +47 -0
- package/content/templates/research.template.md +50 -0
- package/content/templates/session-handoff.template.md +58 -0
- package/content/templates/us.template.md +39 -0
- package/content/tools/gate/core-rules/PLUGIN-13-secrets.sh +36 -0
- package/content/tools/gate/core-rules/PLUGIN-21-code-language.sh +27 -0
- package/content/tools/gate/core-rules/PLUGIN-35-gate-complete.sh +19 -0
- package/content/tools/gate/core-rules/PLUGIN-36-absolute-path.sh +38 -0
- package/content/tools/gate/core-rules/PLUGIN-37-prose-language.sh +56 -0
- package/content/tools/gate/core-rules/PLUGIN-38-parser-marker.sh +82 -0
- package/content/tools/gate/core-rules/PLUGIN-39-frontmatter-ops.sh +325 -0
- package/content/tools/gate/core-rules/PLUGIN-40-dead-reference.sh +74 -0
- package/content/tools/gate/core-rules/README.md +55 -0
- package/content/tools/gate/guardrails.sh +740 -0
- package/content/tools/gate/lib/ratchet.sh +119 -0
- package/content/tools/gate/lib/scope.sh +85 -0
- package/content/tools/gate/ratchet.seed.tsv +1 -0
- package/content/tools/gate/rules/README.md +50 -0
- package/package.json +19 -5
- package/plugin/agy/hooks.json +29 -0
- package/plugin/agy/mcp_config.json +7 -0
- package/plugin/agy/plugin.json +6 -0
- package/plugin/agy/skills/afm-adr/SKILL.md +44 -0
- package/plugin/agy/skills/afm-analyze/SKILL.md +36 -0
- package/plugin/agy/skills/afm-clarify/SKILL.md +36 -0
- package/plugin/agy/skills/afm-deliver/SKILL.md +44 -0
- package/plugin/agy/skills/afm-diagnose/SKILL.md +50 -0
- package/plugin/agy/skills/afm-evolve/SKILL.md +69 -0
- package/plugin/agy/skills/afm-export-template/SKILL.md +42 -0
- package/plugin/agy/skills/afm-generalize/SKILL.md +49 -0
- package/plugin/agy/skills/afm-gotcha/SKILL.md +42 -0
- package/plugin/agy/skills/afm-init/SKILL.md +84 -0
- package/plugin/agy/skills/afm-plan/SKILL.md +36 -0
- package/plugin/agy/skills/afm-policy/SKILL.md +31 -0
- package/plugin/agy/skills/afm-promote/SKILL.md +41 -0
- package/plugin/agy/skills/afm-protocol/SKILL.md +42 -0
- package/plugin/agy/skills/afm-reconcile/SKILL.md +49 -0
- package/plugin/agy/skills/afm-refactor/SKILL.md +86 -0
- package/plugin/agy/skills/afm-reflect/SKILL.md +55 -0
- package/plugin/agy/skills/afm-remediate/SKILL.md +41 -0
- package/plugin/agy/skills/afm-restructure/SKILL.md +54 -0
- package/plugin/agy/skills/afm-rule/SKILL.md +46 -0
- package/plugin/agy/skills/afm-specify/SKILL.md +36 -0
- package/plugin/agy/skills/afm-tasks/SKILL.md +34 -0
- package/plugin/agy/skills/afm-update/SKILL.md +58 -0
- package/plugin/agy/skills/afm-verify/SKILL.md +33 -0
- package/plugin/claude-code/.claude-plugin/marketplace.json +15 -0
- package/plugin/claude-code/.claude-plugin/plugin.json +9 -0
- package/plugin/claude-code/.mcp.json +7 -0
- package/plugin/claude-code/agents/afm-executor.md +18 -0
- package/plugin/claude-code/agents/afm-explorer.md +20 -0
- package/plugin/claude-code/agents/afm-reviewer.md +21 -0
- package/plugin/claude-code/hooks/hooks.json +85 -0
- package/plugin/claude-code/skills/adr/SKILL.md +44 -0
- package/plugin/claude-code/skills/analyze/SKILL.md +36 -0
- package/plugin/claude-code/skills/clarify/SKILL.md +36 -0
- package/plugin/claude-code/skills/deliver/SKILL.md +44 -0
- package/plugin/claude-code/skills/diagnose/SKILL.md +50 -0
- package/plugin/claude-code/skills/evolve/SKILL.md +69 -0
- package/plugin/claude-code/skills/export-template/SKILL.md +42 -0
- package/plugin/claude-code/skills/generalize/SKILL.md +49 -0
- package/plugin/claude-code/skills/gotcha/SKILL.md +42 -0
- package/plugin/claude-code/skills/init/SKILL.md +84 -0
- package/plugin/claude-code/skills/plan/SKILL.md +36 -0
- package/plugin/claude-code/skills/policy/SKILL.md +31 -0
- package/plugin/claude-code/skills/promote/SKILL.md +41 -0
- package/plugin/claude-code/skills/protocol/SKILL.md +42 -0
- package/plugin/claude-code/skills/reconcile/SKILL.md +49 -0
- package/plugin/claude-code/skills/refactor/SKILL.md +86 -0
- package/plugin/claude-code/skills/reflect/SKILL.md +55 -0
- package/plugin/claude-code/skills/remediate/SKILL.md +41 -0
- package/plugin/claude-code/skills/restructure/SKILL.md +54 -0
- package/plugin/claude-code/skills/rule/SKILL.md +46 -0
- package/plugin/claude-code/skills/specify/SKILL.md +36 -0
- package/plugin/claude-code/skills/tasks/SKILL.md +34 -0
- package/plugin/claude-code/skills/update/SKILL.md +58 -0
- package/plugin/claude-code/skills/verify/SKILL.md +33 -0
- package/plugin/opencode/afm.js +196 -0
- package/plugin/opencode/agents/afm-executor.md +31 -0
- package/plugin/opencode/agents/afm-explorer.md +47 -0
- package/plugin/opencode/agents/afm-reviewer.md +48 -0
- package/plugin/opencode/skills/afm-adr/SKILL.md +44 -0
- package/plugin/opencode/skills/afm-analyze/SKILL.md +36 -0
- package/plugin/opencode/skills/afm-clarify/SKILL.md +36 -0
- package/plugin/opencode/skills/afm-deliver/SKILL.md +44 -0
- package/plugin/opencode/skills/afm-diagnose/SKILL.md +50 -0
- package/plugin/opencode/skills/afm-evolve/SKILL.md +69 -0
- package/plugin/opencode/skills/afm-export-template/SKILL.md +42 -0
- package/plugin/opencode/skills/afm-generalize/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-gotcha/SKILL.md +42 -0
- package/plugin/opencode/skills/afm-init/SKILL.md +84 -0
- package/plugin/opencode/skills/afm-plan/SKILL.md +36 -0
- package/plugin/opencode/skills/afm-policy/SKILL.md +31 -0
- package/plugin/opencode/skills/afm-promote/SKILL.md +41 -0
- package/plugin/opencode/skills/afm-protocol/SKILL.md +42 -0
- package/plugin/opencode/skills/afm-reconcile/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-refactor/SKILL.md +86 -0
- package/plugin/opencode/skills/afm-reflect/SKILL.md +55 -0
- package/plugin/opencode/skills/afm-remediate/SKILL.md +41 -0
- package/plugin/opencode/skills/afm-restructure/SKILL.md +54 -0
- package/plugin/opencode/skills/afm-rule/SKILL.md +46 -0
- package/plugin/opencode/skills/afm-specify/SKILL.md +36 -0
- package/plugin/opencode/skills/afm-tasks/SKILL.md +34 -0
- package/plugin/opencode/skills/afm-update/SKILL.md +58 -0
- package/plugin/opencode/skills/afm-verify/SKILL.md +33 -0
- 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.
|