@tenkara/afm-linux-arm64 0.0.0-stage → 4.0.0-alpha.10
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 +4320 -0
- package/content/PRINCIPLES.md +119 -0
- package/content/README-light.md +158 -0
- package/content/README.md +150 -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 +260 -0
- package/content/core/afm.md +514 -0
- package/content/core/gotchas.md +18 -0
- package/content/core/prd.md +141 -0
- package/content/core/protocol.md +158 -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 +70 -0
- package/content/interview/bootstrap.md +417 -0
- package/content/interview/promotion.md +115 -0
- package/content/interview/refactor.md +379 -0
- package/content/interview/triggers.md +107 -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 +130 -0
- package/content/ops/clarify.md +86 -0
- package/content/ops/deliver.md +209 -0
- package/content/ops/diagnose.md +143 -0
- package/content/ops/evolve.md +366 -0
- package/content/ops/export-template.md +97 -0
- package/content/ops/generalize.md +117 -0
- package/content/ops/gotcha.md +46 -0
- package/content/ops/plan.md +61 -0
- package/content/ops/policy.md +61 -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 +182 -0
- package/content/ops/remediate.md +143 -0
- package/content/ops/restructure.md +142 -0
- package/content/ops/rule.md +170 -0
- package/content/ops/specify.md +62 -0
- package/content/ops/tasks.md +67 -0
- package/content/ops/update.md +252 -0
- package/content/ops/verify.md +67 -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 +41 -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 +111 -0
- package/content/rubrics/negative-filters.md +38 -0
- package/content/rubrics/solid-triggers.md +119 -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 +28 -0
- package/content/skills/clarify.md +28 -0
- package/content/skills/deliver.md +26 -0
- package/content/skills/diagnose.md +42 -0
- package/content/skills/evolve.md +43 -0
- package/content/skills/export-template.md +37 -0
- package/content/skills/generalize.md +31 -0
- package/content/skills/gotcha.md +40 -0
- package/content/skills/init.md +77 -0
- package/content/skills/plan.md +28 -0
- package/content/skills/policy.md +25 -0
- package/content/skills/promote.md +39 -0
- package/content/skills/protocol.md +41 -0
- package/content/skills/reconcile.md +45 -0
- package/content/skills/refactor.md +79 -0
- package/content/skills/reflect.md +43 -0
- package/content/skills/remediate.md +33 -0
- package/content/skills/restructure.md +46 -0
- package/content/skills/rule.md +43 -0
- package/content/skills/specify.md +28 -0
- package/content/skills/tasks.md +24 -0
- package/content/skills/update.md +58 -0
- package/content/skills/verify.md +29 -0
- package/content/templates/adr.template.md +51 -0
- package/content/templates/evolution-report.template.md +132 -0
- package/content/templates/feature-plan.template.md +84 -0
- package/content/templates/feature-research.template.md +56 -0
- package/content/templates/feature-spec.template.md +86 -0
- package/content/templates/feature-tasks.induced.template.md +91 -0
- package/content/templates/feature-tasks.template.md +82 -0
- package/content/templates/focus.template.md +30 -0
- package/content/templates/gotcha.template.md +31 -0
- package/content/templates/guardrail-runner.template.md +97 -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 +48 -0
- package/plugin/agy/skills/afm-analyze/SKILL.md +34 -0
- package/plugin/agy/skills/afm-clarify/SKILL.md +34 -0
- package/plugin/agy/skills/afm-deliver/SKILL.md +30 -0
- package/plugin/agy/skills/afm-diagnose/SKILL.md +46 -0
- package/plugin/agy/skills/afm-evolve/SKILL.md +49 -0
- package/plugin/agy/skills/afm-export-template/SKILL.md +43 -0
- package/plugin/agy/skills/afm-generalize/SKILL.md +37 -0
- package/plugin/agy/skills/afm-gotcha/SKILL.md +46 -0
- package/plugin/agy/skills/afm-init/SKILL.md +81 -0
- package/plugin/agy/skills/afm-plan/SKILL.md +34 -0
- package/plugin/agy/skills/afm-policy/SKILL.md +29 -0
- package/plugin/agy/skills/afm-promote/SKILL.md +43 -0
- package/plugin/agy/skills/afm-protocol/SKILL.md +45 -0
- package/plugin/agy/skills/afm-reconcile/SKILL.md +49 -0
- package/plugin/agy/skills/afm-refactor/SKILL.md +83 -0
- package/plugin/agy/skills/afm-reflect/SKILL.md +49 -0
- package/plugin/agy/skills/afm-remediate/SKILL.md +39 -0
- package/plugin/agy/skills/afm-restructure/SKILL.md +52 -0
- package/plugin/agy/skills/afm-rule/SKILL.md +49 -0
- package/plugin/agy/skills/afm-specify/SKILL.md +34 -0
- package/plugin/agy/skills/afm-tasks/SKILL.md +28 -0
- package/plugin/agy/skills/afm-update/SKILL.md +64 -0
- package/plugin/agy/skills/afm-verify/SKILL.md +35 -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 +48 -0
- package/plugin/claude-code/skills/analyze/SKILL.md +34 -0
- package/plugin/claude-code/skills/clarify/SKILL.md +34 -0
- package/plugin/claude-code/skills/deliver/SKILL.md +30 -0
- package/plugin/claude-code/skills/diagnose/SKILL.md +46 -0
- package/plugin/claude-code/skills/evolve/SKILL.md +49 -0
- package/plugin/claude-code/skills/export-template/SKILL.md +43 -0
- package/plugin/claude-code/skills/generalize/SKILL.md +37 -0
- package/plugin/claude-code/skills/gotcha/SKILL.md +46 -0
- package/plugin/claude-code/skills/init/SKILL.md +81 -0
- package/plugin/claude-code/skills/plan/SKILL.md +34 -0
- package/plugin/claude-code/skills/policy/SKILL.md +29 -0
- package/plugin/claude-code/skills/promote/SKILL.md +43 -0
- package/plugin/claude-code/skills/protocol/SKILL.md +45 -0
- package/plugin/claude-code/skills/reconcile/SKILL.md +49 -0
- package/plugin/claude-code/skills/refactor/SKILL.md +83 -0
- package/plugin/claude-code/skills/reflect/SKILL.md +49 -0
- package/plugin/claude-code/skills/remediate/SKILL.md +39 -0
- package/plugin/claude-code/skills/restructure/SKILL.md +52 -0
- package/plugin/claude-code/skills/rule/SKILL.md +49 -0
- package/plugin/claude-code/skills/specify/SKILL.md +34 -0
- package/plugin/claude-code/skills/tasks/SKILL.md +28 -0
- package/plugin/claude-code/skills/update/SKILL.md +64 -0
- package/plugin/claude-code/skills/verify/SKILL.md +35 -0
- package/plugin/opencode/afm/index.js +245 -0
- package/plugin/opencode/afm/tui.js +21 -0
- package/plugin/opencode/agents/afm-executor.md +39 -0
- package/plugin/opencode/agents/afm-explorer.md +92 -0
- package/plugin/opencode/agents/afm-reviewer.md +93 -0
- package/plugin/opencode/skills/afm-adr/SKILL.md +48 -0
- package/plugin/opencode/skills/afm-analyze/SKILL.md +34 -0
- package/plugin/opencode/skills/afm-clarify/SKILL.md +34 -0
- package/plugin/opencode/skills/afm-deliver/SKILL.md +30 -0
- package/plugin/opencode/skills/afm-diagnose/SKILL.md +46 -0
- package/plugin/opencode/skills/afm-evolve/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-export-template/SKILL.md +43 -0
- package/plugin/opencode/skills/afm-generalize/SKILL.md +37 -0
- package/plugin/opencode/skills/afm-gotcha/SKILL.md +46 -0
- package/plugin/opencode/skills/afm-init/SKILL.md +81 -0
- package/plugin/opencode/skills/afm-plan/SKILL.md +34 -0
- package/plugin/opencode/skills/afm-policy/SKILL.md +29 -0
- package/plugin/opencode/skills/afm-promote/SKILL.md +43 -0
- package/plugin/opencode/skills/afm-protocol/SKILL.md +45 -0
- package/plugin/opencode/skills/afm-reconcile/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-refactor/SKILL.md +83 -0
- package/plugin/opencode/skills/afm-reflect/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-remediate/SKILL.md +39 -0
- package/plugin/opencode/skills/afm-restructure/SKILL.md +52 -0
- package/plugin/opencode/skills/afm-rule/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-specify/SKILL.md +34 -0
- package/plugin/opencode/skills/afm-tasks/SKILL.md +28 -0
- package/plugin/opencode/skills/afm-update/SKILL.md +64 -0
- package/plugin/opencode/skills/afm-verify/SKILL.md +35 -0
- package/README.md +0 -3
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Mechanical gate for the hard rules of `docs/afm.md` § 3.
|
|
3
|
+
#
|
|
4
|
+
# guardrails.sh # look at the INDEX (what becomes the commit); exit 1 on a violation
|
|
5
|
+
# guardrails.sh --worktree # look at the working tree
|
|
6
|
+
# guardrails.sh --audit # prove that rule and runner are in sync, both ways
|
|
7
|
+
# guardrails.sh --self-test # prove the aggregator catches what it claims to catch
|
|
8
|
+
# guardrails.sh --baseline # measure HEAD and record the debt, so the gate starts green
|
|
9
|
+
# guardrails.sh --install # arm the pre-commit hook (opt-in, never automatic)
|
|
10
|
+
#
|
|
11
|
+
# Why it exists: hard rules with an executable trigger written in PROSE never run. A rule that
|
|
12
|
+
# holds only when the agent remembers to run the grep fails exactly when context gets tight.
|
|
13
|
+
#
|
|
14
|
+
# Runner contract (`<rules>/<NN>-<slug>.sh`):
|
|
15
|
+
# argv[1] : "staged" | "worktree"
|
|
16
|
+
# env RATCHET : path of the ratchet TSV
|
|
17
|
+
# env AFM_GATE_LIB : directory holding scope.sh and ratchet.sh
|
|
18
|
+
# stdout : one line per finding — "<NN>\t<path>[:<line>]\t<message>"
|
|
19
|
+
# exit 0 : compliant · exit 1 : violated · exit 2 : COULD NOT decide
|
|
20
|
+
#
|
|
21
|
+
# Exit 2 is deliberately distinct from 1: "I could not verify" must never become "it failed".
|
|
22
|
+
# A runner can depend on a tool the machine does not have. The first machine without it would
|
|
23
|
+
# turn the gate into a permanent block, and a gate like that gets disabled — together with
|
|
24
|
+
# every other rule in the same aggregator.
|
|
25
|
+
set -u
|
|
26
|
+
|
|
27
|
+
V="${V:-}"
|
|
28
|
+
MODE=staged
|
|
29
|
+
ACTION=run
|
|
30
|
+
REPO=""
|
|
31
|
+
RULES_DIR=""
|
|
32
|
+
CORE_RULES_DIR=""
|
|
33
|
+
AFM_DOC=""
|
|
34
|
+
RATCHET_ARG=""
|
|
35
|
+
|
|
36
|
+
usage() {
|
|
37
|
+
printf 'usage: %s [-v] [--staged|--worktree] [--audit|--self-test|--install|--baseline]\n' "$0" >&2
|
|
38
|
+
printf ' [--repo <dir>] [--rules <dir>] [--core-rules <dir>]\n' >&2
|
|
39
|
+
printf ' [--ratchet <tsv>] [--afm-doc <md>]\n' >&2
|
|
40
|
+
exit 2
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
while [ $# -gt 0 ]; do
|
|
44
|
+
case "$1" in
|
|
45
|
+
-v|--verbose) V=1 ;;
|
|
46
|
+
--staged) MODE=staged ;;
|
|
47
|
+
--worktree) MODE=worktree ;;
|
|
48
|
+
--audit) ACTION=audit ;;
|
|
49
|
+
--self-test) ACTION=selftest ;;
|
|
50
|
+
--install) ACTION=install ;;
|
|
51
|
+
--baseline) ACTION=baseline ;;
|
|
52
|
+
--repo) shift; [ $# -gt 0 ] || usage; REPO="$1" ;;
|
|
53
|
+
--rules) shift; [ $# -gt 0 ] || usage; RULES_DIR="$1" ;;
|
|
54
|
+
--core-rules) shift; [ $# -gt 0 ] || usage; CORE_RULES_DIR="$1" ;;
|
|
55
|
+
--ratchet) shift; [ $# -gt 0 ] || usage; RATCHET_ARG="$1" ;;
|
|
56
|
+
--afm-doc) shift; [ $# -gt 0 ] || usage; AFM_DOC="$1" ;;
|
|
57
|
+
*) usage ;;
|
|
58
|
+
esac
|
|
59
|
+
shift
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
# --- Where things are -------------------------------------------------------
|
|
63
|
+
# The aggregator is canonical in the plugin and materialized in the consumer, so it cannot
|
|
64
|
+
# derive the repository from its own location. It asks git, and falls back to the working
|
|
65
|
+
# directory when git has nothing to say.
|
|
66
|
+
if [ -z "$REPO" ]; then
|
|
67
|
+
REPO="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# The library travels next to this script, whichever copy is running.
|
|
71
|
+
AFM_GATE_LIB="${AFM_GATE_LIB:-$(cd "$(dirname "$0")" && pwd)/lib}"
|
|
72
|
+
export AFM_GATE_LIB
|
|
73
|
+
# The mode reaches a runner as $1, but `scope_all` takes no mode argument and cannot grow one
|
|
74
|
+
# without breaking every runner a consumer already wrote. It reads this instead.
|
|
75
|
+
export AFM_GATE_MODE="$MODE"
|
|
76
|
+
# shellcheck source=lib/ratchet.sh
|
|
77
|
+
. "$AFM_GATE_LIB/ratchet.sh"
|
|
78
|
+
|
|
79
|
+
# Two candidates, in order. A consumer that took the materialized layout needs no flag; a
|
|
80
|
+
# project that already keeps its gates somewhere else passes --rules and --ratchet.
|
|
81
|
+
first_existing() {
|
|
82
|
+
local c
|
|
83
|
+
for c in "$@"; do [ -e "$c" ] && { printf '%s' "$c"; return 0; }; done
|
|
84
|
+
printf '%s' "$1"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[ -n "$RULES_DIR" ] || RULES_DIR="${AFM_GATE_RULES:-$(first_existing "$REPO/docs/tools/gate/rules" "$REPO/test/rules")}"
|
|
88
|
+
|
|
89
|
+
# The gate has TWO classes of runner, and conflating them was a real defect (ADR 0021).
|
|
90
|
+
#
|
|
91
|
+
# PROJECT rules — `$RULES_DIR`. One project's rules about its own CODE. The plugin ships
|
|
92
|
+
# none of these, and the README in `gate/rules/` explains why: a consumer
|
|
93
|
+
# that inherits a rule nobody chose bypasses it first.
|
|
94
|
+
# ARTIFACT rules — `$CORE_RULES_DIR`. The contract the plugin already imposes on the `docs/`
|
|
95
|
+
# it materializes: English prose, no absolute path, the OKF frontmatter, a
|
|
96
|
+
# versioned runner behind every 0/1 trigger. Nobody "chooses" these — they
|
|
97
|
+
# are what an AFM instance IS. Until 3.20.0 they lived in one repository and
|
|
98
|
+
# every consumer got an `afm.md` that stopped at rule 21.
|
|
99
|
+
#
|
|
100
|
+
# A project runner with the same NN WINS over the shipped one, so a consumer can override
|
|
101
|
+
# without forking the plugin.
|
|
102
|
+
[ -n "$CORE_RULES_DIR" ] || CORE_RULES_DIR="${AFM_GATE_CORE_RULES:-$(first_existing "$REPO/docs/tools/gate/core-rules" "$REPO/test/core-rules")}"
|
|
103
|
+
[ -n "$AFM_DOC" ] || AFM_DOC="${AFM_GATE_DOC:-$REPO/docs/afm.md}"
|
|
104
|
+
if [ -n "$RATCHET_ARG" ]; then
|
|
105
|
+
RATCHET="$RATCHET_ARG"
|
|
106
|
+
else
|
|
107
|
+
RATCHET="${RATCHET:-$(first_existing "$REPO/docs/tools/gate/ratchet.tsv" "$REPO/test/baseline/ratchet.tsv")}"
|
|
108
|
+
fi
|
|
109
|
+
export RATCHET
|
|
110
|
+
|
|
111
|
+
# Absolute before the `cd` below, because a relative path passed on the command line is
|
|
112
|
+
# relative to where the operator stood, not to the repository.
|
|
113
|
+
abspath() {
|
|
114
|
+
case "$1" in
|
|
115
|
+
/*) printf '%s' "$1" ;;
|
|
116
|
+
*) printf '%s/%s' "$(pwd)" "$1" ;;
|
|
117
|
+
esac
|
|
118
|
+
}
|
|
119
|
+
RULES_DIR="$(abspath "$RULES_DIR")"
|
|
120
|
+
CORE_RULES_DIR="$(abspath "$CORE_RULES_DIR")"
|
|
121
|
+
# A runner may invoke the aggregator again (rule PLUGIN-35 does exactly that). Without these,
|
|
122
|
+
# the nested call re-resolves the directories from the DEFAULTS and reports every shipped rule
|
|
123
|
+
# as a phantom gate — a failure invented by the check itself.
|
|
124
|
+
export AFM_GATE_RULES="$RULES_DIR"
|
|
125
|
+
export AFM_GATE_CORE_RULES="$CORE_RULES_DIR"
|
|
126
|
+
AFM_DOC="$(abspath "$AFM_DOC")"
|
|
127
|
+
RATCHET="$(abspath "$RATCHET")"
|
|
128
|
+
export RATCHET
|
|
129
|
+
|
|
130
|
+
# Stand in the repository before running anything. `scope_all` and `scope_files` ask git, and
|
|
131
|
+
# git answers about the CURRENT DIRECTORY: from a subdirectory it lists only that subdirectory,
|
|
132
|
+
# and from another repository it lists that other repository. Measured 2026-08-26 while testing
|
|
133
|
+
# the refutation of ADR 0017 — the gate was invoked with `--repo <other project>` from this
|
|
134
|
+
# repository and reported findings in `plugins/afm/`, under the other project's name. A count
|
|
135
|
+
# rule that measures the wrong tree does not fail; it reports a number that looks right.
|
|
136
|
+
cd "$REPO" || { printf 'cannot enter %s\n' "$REPO" >&2; exit 2; }
|
|
137
|
+
|
|
138
|
+
# A runner must NOT derive the repository from its own path. Until 3.20.0 every one of them ran
|
|
139
|
+
# `REPO="$(cd "$(dirname "$0")/../.." && pwd)"`, which is only correct while the runner sits
|
|
140
|
+
# exactly two levels below the root. The moment a runner shipped with the plugin — five levels
|
|
141
|
+
# down — that expression pointed at `tools/`, and rule 37 reported "cannot be decided" against a
|
|
142
|
+
# file that was right there. The aggregator already stands in the repository, so it hands the
|
|
143
|
+
# answer down instead of letting each runner guess.
|
|
144
|
+
export AFM_GATE_REPO="$REPO"
|
|
145
|
+
|
|
146
|
+
# --- Scoreboard -------------------------------------------------------------
|
|
147
|
+
# Silent when green, loud when red. A gate that talks while everything is fine trains the
|
|
148
|
+
# operator to ignore its output, and then the red line disappears in the noise.
|
|
149
|
+
PASS=0; FAIL=0; NOTE=0
|
|
150
|
+
sec() { [ -n "$V" ] && printf '%s\n' "$1"; return 0; }
|
|
151
|
+
ok() { PASS=$((PASS+1)); [ -n "$V" ] && printf ' ok — %s\n' "$1"; return 0; }
|
|
152
|
+
bad() { FAIL=$((FAIL+1)); printf ' FAIL — %s\n' "$1"; }
|
|
153
|
+
# A note is KNOWN DEBT or "could not decide". Neither is actionable at commit time, so it only
|
|
154
|
+
# shows under -v, or under --audit, which is where you look at debt on purpose.
|
|
155
|
+
note() { NOTE=$((NOTE+1)); [ -n "$V" ] && printf ' note — %s\n' "$1"; return 0; }
|
|
156
|
+
|
|
157
|
+
summary() {
|
|
158
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
159
|
+
printf '\n%d ok, %d fail, %d note (known debt: %s --audit)\n' "$PASS" "$FAIL" "$NOTE" "$0"
|
|
160
|
+
elif [ -n "$V" ]; then
|
|
161
|
+
printf '\n%d ok, %d note\n' "$PASS" "$NOTE"
|
|
162
|
+
fi
|
|
163
|
+
[ "$FAIL" -eq 0 ]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# A rule ID has TWO namespaces, and that is what removed the number reservation (ADR 0021).
|
|
167
|
+
#
|
|
168
|
+
# PLUGIN-<n> a rule the plugin ships. It is the contract of the `docs/` AFM materializes.
|
|
169
|
+
# <n> a rule this project wrote about its own code.
|
|
170
|
+
#
|
|
171
|
+
# Before 3.20.0 the template reserved 18-29 for "universal rules, which grow with each release"
|
|
172
|
+
# and told a project to start at 30. A reservation is a promise about numbers nobody can keep:
|
|
173
|
+
# this repository had already spent 35-39 on rules that turned out to be universal, and the
|
|
174
|
+
# moment they shipped, any consumer's own rule 35 collided with them. A namespace cannot
|
|
175
|
+
# collide, and it also says where a rule came from, which the number alone never did.
|
|
176
|
+
#
|
|
177
|
+
# An organisation policy rule stays numbered 100 or higher — a third namespace, already
|
|
178
|
+
# distinct, and left alone here.
|
|
179
|
+
rule_number() { basename "$1" | sed -E 's/^((PLUGIN-)?[0-9]+)-.*/\1/'; }
|
|
180
|
+
|
|
181
|
+
# --- Core: run one runner and decide against the ratchet --------------------
|
|
182
|
+
# A runner only knows how to find a violation. The budget belongs to the aggregator. That keeps
|
|
183
|
+
# the ratchet rule in one place and keeps runners dumb, which is what makes them cheap to write
|
|
184
|
+
# and cheap to audit.
|
|
185
|
+
run_rule() {
|
|
186
|
+
local f="$1" nn out rc line path region
|
|
187
|
+
nn="$(rule_number "$f")"
|
|
188
|
+
|
|
189
|
+
out="$("$f" "$MODE" 2>/dev/null)"
|
|
190
|
+
rc=$?
|
|
191
|
+
|
|
192
|
+
if [ "$rc" = "2" ]; then
|
|
193
|
+
note "rule ${nn}: the runner could not decide (missing dependency) — this is not a violation"
|
|
194
|
+
return 0
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
if [ -z "$out" ]; then
|
|
198
|
+
ok "rule ${nn}"
|
|
199
|
+
return 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# Group the findings by the most specific configured region.
|
|
203
|
+
local -A tally=()
|
|
204
|
+
while IFS= read -r line; do
|
|
205
|
+
[ -n "$line" ] || continue
|
|
206
|
+
path="$(printf '%s' "$line" | cut -f2 | cut -d: -f1)"
|
|
207
|
+
region="$(ratchet_region "$nn" "$path")"
|
|
208
|
+
tally["$region"]=$(( ${tally["$region"]:-0} + 1 ))
|
|
209
|
+
done <<< "$out"
|
|
210
|
+
|
|
211
|
+
local budget r n
|
|
212
|
+
for r in "${!tally[@]}"; do
|
|
213
|
+
n="${tally[$r]}"
|
|
214
|
+
budget="$(ratchet_budget "$nn" "$r")"
|
|
215
|
+
if [ "$n" -le "$budget" ]; then
|
|
216
|
+
note "rule ${nn} in '${r}': ${n}/${budget} within the ratchet$( [ -n "$(ratchet_note "$nn" "$r")" ] && printf ' (%s)' "$(ratchet_note "$nn" "$r")" )"
|
|
217
|
+
else
|
|
218
|
+
bad "rule ${nn} in '${r}': ${n} violation(s), budget ${budget}"
|
|
219
|
+
printf '%s\n' "$out" | awk -F'\t' '{ printf " %s — %s\n", $2, $3 }'
|
|
220
|
+
fi
|
|
221
|
+
done
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# The one place that decides WHICH runners exist. Project rules win over shipped ones on a
|
|
225
|
+
# number collision, so a consumer overrides by dropping a same-numbered file into its own dir.
|
|
226
|
+
# Every caller that used to glob a single directory goes through here — a second glob left
|
|
227
|
+
# behind is a rule that runs in `run_all` and is invisible to `--audit`, which is the shape of
|
|
228
|
+
# defect this whole release is about.
|
|
229
|
+
rule_files() {
|
|
230
|
+
local d f nn
|
|
231
|
+
local -A seen=()
|
|
232
|
+
for d in "$RULES_DIR" "$CORE_RULES_DIR"; do
|
|
233
|
+
[ -d "$d" ] || continue
|
|
234
|
+
# BOTH shapes. `[0-9]*.sh` alone silently skipped every shipped runner, because a namespaced
|
|
235
|
+
# name starts with `P`. The glob matched nothing, and a glob that matches nothing reports
|
|
236
|
+
# nothing — the audit went green while the entire core-rules directory was invisible.
|
|
237
|
+
for f in "$d"/[0-9]*.sh "$d"/PLUGIN-[0-9]*.sh; do
|
|
238
|
+
[ -f "$f" ] || continue
|
|
239
|
+
nn="$(rule_number "$f")"
|
|
240
|
+
[ -n "${seen[$nn]:-}" ] && continue
|
|
241
|
+
seen[$nn]="$f"
|
|
242
|
+
done
|
|
243
|
+
done
|
|
244
|
+
for nn in $(printf '%s\n' "${!seen[@]}" | sort -n); do printf '%s\n' "${seen[$nn]}"; done
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
run_all() {
|
|
248
|
+
local f found=0
|
|
249
|
+
while IFS= read -r f; do
|
|
250
|
+
[ -n "$f" ] || continue
|
|
251
|
+
[ -x "$f" ] || { bad "runner is not executable: $f"; continue; }
|
|
252
|
+
found=1
|
|
253
|
+
run_rule "$f"
|
|
254
|
+
done < <(rule_files)
|
|
255
|
+
[ "$found" = 1 ] || note "no runner in ${RULES_DIR} or ${CORE_RULES_DIR} — the gate is empty"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# --- Baseline: measure HEAD so the gate can start green ---------------------
|
|
259
|
+
# `ops/rule.md` step 3 already requires this measurement before a rule is turned on. Doing it by
|
|
260
|
+
# hand means it does not get done, and the rule is then turned on in a repository that already
|
|
261
|
+
# breaks it. A gate that starts red is disabled the next day.
|
|
262
|
+
#
|
|
263
|
+
# It only ever APPENDS, and only for a rule that has no row yet. Overwriting would erase the one
|
|
264
|
+
# thing in the ratchet that cost real work: a region boundary somebody decided on.
|
|
265
|
+
baseline() {
|
|
266
|
+
sec "== baseline: measuring HEAD =="
|
|
267
|
+
local f nn out rc n added=0
|
|
268
|
+
|
|
269
|
+
if [ ! -f "$RATCHET" ]; then
|
|
270
|
+
mkdir -p "$(dirname "$RATCHET")" || { printf 'cannot create %s\n' "$RATCHET" >&2; return 1; }
|
|
271
|
+
printf 'rule\tregion\tcount\tnote\n' > "$RATCHET"
|
|
272
|
+
printf 'created %s\n' "$RATCHET"
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
local stamp
|
|
276
|
+
stamp="$(date +%Y-%m-%d)"
|
|
277
|
+
|
|
278
|
+
while IFS= read -r f; do
|
|
279
|
+
[ -n "$f" ] && [ -x "$f" ] || continue
|
|
280
|
+
nn="$(rule_number "$f")"
|
|
281
|
+
|
|
282
|
+
if ratchet_has_rule "$nn"; then
|
|
283
|
+
printf ' skip — rule %s already has a row; a measured decision is never overwritten\n' "$nn"
|
|
284
|
+
continue
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
out="$("$f" worktree 2>/dev/null)"
|
|
288
|
+
rc=$?
|
|
289
|
+
if [ "$rc" = "2" ]; then
|
|
290
|
+
printf ' skip — rule %s could not decide; nothing to record\n' "$nn"
|
|
291
|
+
continue
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
n="$(printf '%s' "$out" | grep -c . || true)"
|
|
295
|
+
printf '%s\t.\t%s\tmeasured at baseline on %s\n' "$nn" "$n" "$stamp" >> "$RATCHET"
|
|
296
|
+
added=$((added+1))
|
|
297
|
+
if [ "$n" -gt 0 ]; then
|
|
298
|
+
printf ' debt — rule %s: %s finding(s) recorded as pre-existing\n' "$nn" "$n"
|
|
299
|
+
else
|
|
300
|
+
printf ' clean — rule %s: born at 0, and growth now fails\n' "$nn"
|
|
301
|
+
fi
|
|
302
|
+
done < <(rule_files)
|
|
303
|
+
|
|
304
|
+
printf '\n%d rule(s) recorded in %s\n' "$added" "$RATCHET"
|
|
305
|
+
[ "$added" -gt 0 ] && printf 'The debt is a starting point, not a target. It only decreases.\n'
|
|
306
|
+
return 0
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# --- Audit: rule and runner, both ways --------------------------------------
|
|
310
|
+
# A rule with a 0/1 trigger and no runner is a PHANTOM GATE: the doc promises a check nobody
|
|
311
|
+
# runs, which is worse than promising nothing — it buys trust it cannot back up.
|
|
312
|
+
rules_declared() {
|
|
313
|
+
[ -f "$AFM_DOC" ] || return 0
|
|
314
|
+
awk '/^## 3\./{f=1} /^## [0-9]/&&!/^## 3\./{if(f)f=0} f' "$AFM_DOC" \
|
|
315
|
+
| sed -nE 's/^((PLUGIN-)?[0-9]+)\. \*\*.*/\1/p'
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
# Which surface enforces a rule: `pre-commit`, `ci`, or `agent` (judgment, no runner).
|
|
319
|
+
#
|
|
320
|
+
# It reads the *Verification* line of the rule. That line used to live inside § 3 of `afm.md`,
|
|
321
|
+
# and since 3.9.1 the rules are one file each under `rules/` while § 3 is only an index. This
|
|
322
|
+
# function kept reading `afm.md` alone, found no *Verification* line for any rule, and answered
|
|
323
|
+
# `agent` for all of them — so the phantom-gate branch below could never fire. Measured on
|
|
324
|
+
# 2026-08-30: deleting rule 37's runner produced no finding at all, from the very check whose
|
|
325
|
+
# whole purpose is to catch a rule that promises a gate nobody runs.
|
|
326
|
+
#
|
|
327
|
+
# Both layouts are read, inline first, because an instance that has not migrated still has the
|
|
328
|
+
# line in `afm.md` and must not lose the check on the way to the split layout.
|
|
329
|
+
rule_surface() {
|
|
330
|
+
local id="$1" body out
|
|
331
|
+
# The ID can hold a `-`, which is not a regex metacharacter, so a literal anchor is enough.
|
|
332
|
+
out="$(awk -v r="^${id}\\. " '
|
|
333
|
+
$0 ~ r {f=1}
|
|
334
|
+
f && /\*Verification/ {
|
|
335
|
+
if (match($0, /\*Verification \(([a-z-]+)\)/, m)) { print m[1]; exit }
|
|
336
|
+
print "agent"; exit
|
|
337
|
+
}
|
|
338
|
+
' "$AFM_DOC" 2>/dev/null)"
|
|
339
|
+
[ -n "$out" ] && { printf '%s' "$out"; return 0; }
|
|
340
|
+
|
|
341
|
+
for body in "$(dirname "$AFM_DOC")/rules/${id}-"*.md; do
|
|
342
|
+
[ -f "$body" ] || continue
|
|
343
|
+
out="$(awk '
|
|
344
|
+
/\*Verification/ {
|
|
345
|
+
if (match($0, /\*Verification \(([a-z-]+)\)/, m)) { print m[1]; exit }
|
|
346
|
+
print "agent"; exit
|
|
347
|
+
}' "$body" 2>/dev/null)"
|
|
348
|
+
[ -n "$out" ] && { printf '%s' "$out"; return 0; }
|
|
349
|
+
done
|
|
350
|
+
printf 'agent'
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
audit_debt() {
|
|
354
|
+
[ -f "$RATCHET" ] || return 0
|
|
355
|
+
# A note that opens with `PERMANENT:` is a DECLARATION, not debt. Some regions never decrease
|
|
356
|
+
# by their nature — episodic history that only grows, and fixtures whose assertions match their
|
|
357
|
+
# own content — and listing them as debt forever makes the debt list mean less every time it is
|
|
358
|
+
# read. The count still holds the ceiling, so a real increase is still caught.
|
|
359
|
+
awk -F'\t' 'NR>1 && $3+0 > 0 {
|
|
360
|
+
if ($4 ~ /^PERMANENT:/) { sub(/^PERMANENT:[[:space:]]*/, "", $4)
|
|
361
|
+
printf " declared — rule %s in %s: %s, permanent (%s)\n", $1, $2, $3, $4 }
|
|
362
|
+
else printf " debt — rule %s in %s: %s (%s)\n", $1, $2, $3, $4
|
|
363
|
+
}' "$RATCHET"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
audit() {
|
|
367
|
+
local nn f have
|
|
368
|
+
for nn in $(rules_declared); do
|
|
369
|
+
have=""
|
|
370
|
+
while IFS= read -r f; do
|
|
371
|
+
[ "$(rule_number "$f")" = "$nn" ] && have="$f"
|
|
372
|
+
done < <(rule_files)
|
|
373
|
+
if [ -n "$have" ]; then
|
|
374
|
+
ok "rule ${nn} has a runner ($(basename "$have"))"
|
|
375
|
+
else
|
|
376
|
+
case "$(rule_surface "$nn")" in
|
|
377
|
+
pre-commit|ci) bad "rule ${nn} declares a gate but has no runner in ${RULES_DIR} or ${CORE_RULES_DIR} — phantom gate" ;;
|
|
378
|
+
*) note "rule ${nn} is an 'agent' surface (no runner, by design)" ;;
|
|
379
|
+
esac
|
|
380
|
+
fi
|
|
381
|
+
done
|
|
382
|
+
while IFS= read -r f; do
|
|
383
|
+
[ -n "$f" ] || continue
|
|
384
|
+
nn="$(rule_number "$f")"
|
|
385
|
+
rules_declared | grep -qx "$nn" \
|
|
386
|
+
&& ok "runner ${nn} matches a declared rule" \
|
|
387
|
+
|| bad "orphan runner: $(basename "$f") matches no rule in § 3 of ${AFM_DOC}"
|
|
388
|
+
done < <(rule_files)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
# --- Self-test --------------------------------------------------------------
|
|
392
|
+
mk_runner() { # $1=dir $2=NN $3=exit $4=finding lines (separated by §)
|
|
393
|
+
local p="$1/$2-fixture.sh"
|
|
394
|
+
{ printf '#!/usr/bin/env bash\n'
|
|
395
|
+
[ -n "$4" ] && printf '%s\n' "$4" | tr '§' '\n' | while IFS= read -r l; do
|
|
396
|
+
[ -n "$l" ] && printf 'printf "%%b\\n" "%s"\n' "$l"
|
|
397
|
+
done
|
|
398
|
+
printf 'exit %s\n' "$3"
|
|
399
|
+
} > "$p"
|
|
400
|
+
chmod +x "$p"
|
|
401
|
+
printf '%s' "$p"
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
SPASS=0; SFAIL=0
|
|
405
|
+
sok() { SPASS=$((SPASS+1)); [ -n "$V" ] && printf ' ok — %s\n' "$1"; return 0; }
|
|
406
|
+
snote() { [ -n "$V" ] && printf " note — %s\n" "$1"; return 0; }
|
|
407
|
+
sbad() { SFAIL=$((SFAIL+1)); printf ' FAIL — %s\n' "$1"; }
|
|
408
|
+
|
|
409
|
+
selftest() {
|
|
410
|
+
sec "== aggregator self-test (synthetic fixtures) =="
|
|
411
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
412
|
+
mkdir -p "$TMP/rules"
|
|
413
|
+
local saved_dir="$RULES_DIR" saved_core="$CORE_RULES_DIR" saved_ratchet="$RATCHET" saved_doc="$AFM_DOC"
|
|
414
|
+
|
|
415
|
+
RULES_DIR="$TMP/rules"
|
|
416
|
+
# The self-test builds its own fixtures. A real shipped runner leaking in would make the
|
|
417
|
+
# aggregator prove itself against rules the fixture never declared.
|
|
418
|
+
CORE_RULES_DIR="$TMP/core-rules"; mkdir -p "$CORE_RULES_DIR"
|
|
419
|
+
RATCHET="$TMP/ratchet.tsv"; export RATCHET
|
|
420
|
+
printf 'rule\tregion\tcount\tnote\n' > "$RATCHET"
|
|
421
|
+
|
|
422
|
+
# 1. a runner that exits 1 fails the aggregator, and the rule number reaches the output.
|
|
423
|
+
mk_runner "$TMP/rules" 99 1 "99\tsrc/a.ts:3\tviolated" >/dev/null
|
|
424
|
+
PASS=0; FAIL=0; NOTE=0
|
|
425
|
+
run_all > "$TMP/out" 2>&1
|
|
426
|
+
[ "$FAIL" -gt 0 ] && sok "a runner that exits 1 fails the aggregator" || sbad "the aggregator ignored a red runner"
|
|
427
|
+
grep -q '99' "$TMP/out" \
|
|
428
|
+
&& sok "the output names the violated rule" \
|
|
429
|
+
|| sbad "the output does not name the rule"
|
|
430
|
+
|
|
431
|
+
# 2. a compliant repository: exit 0 AND silence.
|
|
432
|
+
rm -f "$TMP/rules"/*.sh
|
|
433
|
+
mk_runner "$TMP/rules" 98 0 "" >/dev/null
|
|
434
|
+
PASS=0; FAIL=0; NOTE=0
|
|
435
|
+
local keepv="$V"; V=""
|
|
436
|
+
run_all > "$TMP/out" 2>&1
|
|
437
|
+
V="$keepv"
|
|
438
|
+
[ "$FAIL" -eq 0 ] && sok "a compliant repository does not fail" || sbad "the aggregator failed a compliant repository"
|
|
439
|
+
[ ! -s "$TMP/out" ] && sok "a compliant repository prints nothing" || sbad "the aggregator spoke while green: $(cat "$TMP/out")"
|
|
440
|
+
|
|
441
|
+
# 3. exit 2 becomes a note, never a failure.
|
|
442
|
+
rm -f "$TMP/rules"/*.sh
|
|
443
|
+
mk_runner "$TMP/rules" 97 2 "" >/dev/null
|
|
444
|
+
PASS=0; FAIL=0; NOTE=0
|
|
445
|
+
run_all >/dev/null 2>&1
|
|
446
|
+
[ "$FAIL" -eq 0 ] && [ "$NOTE" -gt 0 ] \
|
|
447
|
+
&& sok "exit 2 becomes a note, not a failure" \
|
|
448
|
+
|| sbad "exit 2 was treated as a violation (FAIL=$FAIL NOTE=$NOTE)"
|
|
449
|
+
|
|
450
|
+
# 4. ratchet: inside the budget passes, growth fails.
|
|
451
|
+
rm -f "$TMP/rules"/*.sh
|
|
452
|
+
printf 'rule\tregion\tcount\tnote\n96\t.\t3\tmeasured debt\n' > "$RATCHET"
|
|
453
|
+
mk_runner "$TMP/rules" 96 1 "96\ta.ts:1\tx§96\tb.ts:1\tx§96\tc.ts:1\tx" >/dev/null
|
|
454
|
+
PASS=0; FAIL=0; NOTE=0
|
|
455
|
+
run_all >/dev/null 2>&1
|
|
456
|
+
[ "$FAIL" -eq 0 ] && sok "3 findings against a budget of 3 pass (ratchet)" || sbad "the ratchet failed inside its budget"
|
|
457
|
+
rm -f "$TMP/rules"/*.sh
|
|
458
|
+
mk_runner "$TMP/rules" 96 1 "96\ta.ts:1\tx§96\tb.ts:1\tx§96\tc.ts:1\tx§96\td.ts:1\tx" >/dev/null
|
|
459
|
+
PASS=0; FAIL=0; NOTE=0
|
|
460
|
+
run_all >/dev/null 2>&1
|
|
461
|
+
[ "$FAIL" -gt 0 ] && sok "4 findings against a budget of 3 fail (the ratchet only decreases)" || sbad "the ratchet let the debt grow"
|
|
462
|
+
|
|
463
|
+
# 5. the most specific configured region wins over the generic one.
|
|
464
|
+
rm -f "$TMP/rules"/*.sh
|
|
465
|
+
printf 'rule\tregion\tcount\tnote\n95\t.\t0\t-\n95\tsrc/legacy\t2\tlegacy\n' > "$RATCHET"
|
|
466
|
+
mk_runner "$TMP/rules" 95 1 "95\tsrc/legacy/a.ts:1\tx§95\tsrc/legacy/b.ts:1\tx" >/dev/null
|
|
467
|
+
PASS=0; FAIL=0; NOTE=0
|
|
468
|
+
run_all >/dev/null 2>&1
|
|
469
|
+
[ "$FAIL" -eq 0 ] && sok "a finding lands in the most specific configured region" || sbad "the specific region was ignored"
|
|
470
|
+
|
|
471
|
+
# 5b. a region is a PATH, so it only matches at a path boundary. `index(p,$2)==1` is a literal
|
|
472
|
+
# character prefix, and `docs/research-extra/x.md` starts with the characters of
|
|
473
|
+
# `docs/research`. The neighbour's debt therefore landed in this region's budget: found by an
|
|
474
|
+
# external probe (3.9.0-rc.7). A budget that absorbs a directory nobody measured is a budget
|
|
475
|
+
# that stops meaning anything, and it fails toward green.
|
|
476
|
+
rm -f "$TMP/rules"/*.sh
|
|
477
|
+
printf 'rule\tregion\tcount\tnote\n94\t.\t0\t-\n94\tdocs/research\t2\tpre-existing\n' > "$RATCHET"
|
|
478
|
+
mk_runner "$TMP/rules" 94 1 "94\tdocs/research-extra/a.md:1\tx§94\tdocs/research-extra/b.md:1\tx" >/dev/null
|
|
479
|
+
PASS=0; FAIL=0; NOTE=0
|
|
480
|
+
run_all >/dev/null 2>&1
|
|
481
|
+
[ "$FAIL" -gt 0 ] \
|
|
482
|
+
&& sok "a sibling directory does not spend the neighbouring region's budget" \
|
|
483
|
+
|| sbad "docs/research-extra was absorbed by the docs/research budget (prefix without a boundary)"
|
|
484
|
+
|
|
485
|
+
# ...and the region must still cover its own subtree, and itself.
|
|
486
|
+
rm -f "$TMP/rules"/*.sh
|
|
487
|
+
mk_runner "$TMP/rules" 94 1 "94\tdocs/research/a.md:1\tx§94\tdocs/research/deep/b.md:1\tx" >/dev/null
|
|
488
|
+
PASS=0; FAIL=0; NOTE=0
|
|
489
|
+
run_all >/dev/null 2>&1
|
|
490
|
+
[ "$FAIL" -eq 0 ] \
|
|
491
|
+
&& sok "the region covers its own subtree" \
|
|
492
|
+
|| sbad "the boundary broke the legitimate match inside the region"
|
|
493
|
+
|
|
494
|
+
# A budget bash cannot compare is malformed, not a budget. The ratchet TSV is written by hand,
|
|
495
|
+
# and an all-digit value beyond the 64-bit range survives awk (which prints it back as
|
|
496
|
+
# 100000000000000000000) and then breaks `[ -le ]` itself with "integer expression expected" —
|
|
497
|
+
# which evaluates falsy, so the finding counts as over budget AND stderr fills up. Same defect
|
|
498
|
+
# class the Stop hook already paid for on the cadence threshold.
|
|
499
|
+
printf 'rule\tregion\tcount\tnote\n93\t.\t99999999999999999999\tuncomparable\n' > "$RATCHET"
|
|
500
|
+
B="$(ratchet_budget 93 .)"
|
|
501
|
+
ERR="$( { [ 1 -le "$B" ]; } 2>&1 )"
|
|
502
|
+
[ -z "$(printf '%s' "$ERR" | grep -iE 'integer|inteiro')" ] \
|
|
503
|
+
&& sok "an uncomparable budget never reaches [ -le ] raw" \
|
|
504
|
+
|| sbad "ratchet_budget returned a value bash cannot compare: [$B]"
|
|
505
|
+
printf 'rule\tregion\tcount\tnote\n93\t.\t7\tnormal\n' > "$RATCHET"
|
|
506
|
+
[ "$(ratchet_budget 93 .)" = "7" ] \
|
|
507
|
+
&& sok "a sane budget is unaffected by the clamp" \
|
|
508
|
+
|| sbad "the clamp broke a normal budget"
|
|
509
|
+
|
|
510
|
+
# 6. --audit catches a rule with no runner and a runner with no rule.
|
|
511
|
+
rm -f "$TMP/rules"/*.sh
|
|
512
|
+
AFM_DOC="$TMP/afm.md"
|
|
513
|
+
printf '## 3. Rules\n\n77. **A rule with a gate.**\n\n *Verification (pre-commit):* rules/77-x.sh\n\n78. **A rule of judgment.**\n\n *Verification:* eye to eye\n\n## 4. Other\n' > "$AFM_DOC"
|
|
514
|
+
PASS=0; FAIL=0; NOTE=0
|
|
515
|
+
audit >/dev/null 2>&1
|
|
516
|
+
[ "$FAIL" -gt 0 ] && sok "--audit reports a pre-commit rule with no runner (phantom gate)" || sbad "--audit let a phantom gate through"
|
|
517
|
+
[ "$NOTE" -gt 0 ] && sok "--audit does not demand a runner for an 'agent' rule" || sbad "--audit demanded a runner for a judgment rule"
|
|
518
|
+
mk_runner "$TMP/rules" 77 0 "" >/dev/null
|
|
519
|
+
mk_runner "$TMP/rules" 66 0 "" >/dev/null
|
|
520
|
+
PASS=0; FAIL=0; NOTE=0
|
|
521
|
+
audit >/dev/null 2>&1
|
|
522
|
+
[ "$FAIL" -gt 0 ] && sok "--audit reports an orphan runner" || sbad "--audit let a runner with no rule through"
|
|
523
|
+
|
|
524
|
+
# 7. --baseline turns a red gate green, and only by recording what it measured.
|
|
525
|
+
rm -f "$TMP/rules"/*.sh
|
|
526
|
+
printf 'rule\tregion\tcount\tnote\n' > "$RATCHET"
|
|
527
|
+
mk_runner "$TMP/rules" 94 1 "94\ta.ts:1\tx§94\tb.ts:1\tx§94\tc.ts:1\tx" >/dev/null
|
|
528
|
+
PASS=0; FAIL=0; NOTE=0
|
|
529
|
+
run_all >/dev/null 2>&1
|
|
530
|
+
[ "$FAIL" -gt 0 ] && sok "a rule with no row starts red" || sbad "a rule with no row did not start red"
|
|
531
|
+
baseline >/dev/null 2>&1
|
|
532
|
+
awk -F'\t' 'NR>1 && $1=="94" && $3=="3"{f=1} END{exit !f}' "$RATCHET" \
|
|
533
|
+
&& sok "--baseline records the measured count" \
|
|
534
|
+
|| sbad "--baseline did not record the measured count: $(cat "$RATCHET")"
|
|
535
|
+
PASS=0; FAIL=0; NOTE=0
|
|
536
|
+
run_all >/dev/null 2>&1
|
|
537
|
+
[ "$FAIL" -eq 0 ] && sok "after --baseline the gate starts green" || sbad "the gate stayed red after --baseline"
|
|
538
|
+
rm -f "$TMP/rules"/*.sh
|
|
539
|
+
mk_runner "$TMP/rules" 94 1 "94\ta.ts:1\tx§94\tb.ts:1\tx§94\tc.ts:1\tx§94\td.ts:1\tx" >/dev/null
|
|
540
|
+
PASS=0; FAIL=0; NOTE=0
|
|
541
|
+
run_all >/dev/null 2>&1
|
|
542
|
+
[ "$FAIL" -gt 0 ] && sok "growth past the baseline still fails" || sbad "the baseline became a licence to grow"
|
|
543
|
+
|
|
544
|
+
# 8. --baseline never overwrites a row a human decided on.
|
|
545
|
+
printf 'rule\tregion\tcount\tnote\n93\tsrc/legacy\t2\thuman decision\n' > "$RATCHET"
|
|
546
|
+
rm -f "$TMP/rules"/*.sh
|
|
547
|
+
mk_runner "$TMP/rules" 93 1 "93\ta.ts:1\tx§93\tb.ts:1\tx§93\tc.ts:1\tx" >/dev/null
|
|
548
|
+
baseline > "$TMP/out" 2>&1
|
|
549
|
+
awk -F'\t' 'NR>1 && $1=="93" && $2=="src/legacy" && $4=="human decision"{f=1} END{exit !f}' "$RATCHET" \
|
|
550
|
+
&& sok "--baseline leaves a registered row untouched" \
|
|
551
|
+
|| sbad "--baseline overwrote measured debt"
|
|
552
|
+
[ "$(awk -F'\t' 'NR>1 && $1=="93"' "$RATCHET" | wc -l)" = "1" ] \
|
|
553
|
+
&& sok "--baseline adds no second row for a registered rule" \
|
|
554
|
+
|| sbad "--baseline duplicated a registered rule"
|
|
555
|
+
grep -q 'skip' "$TMP/out" \
|
|
556
|
+
&& sok "--baseline says out loud which rule it skipped" \
|
|
557
|
+
|| sbad "--baseline skipped in silence"
|
|
558
|
+
|
|
559
|
+
# 9. an empty gate is empty, not failed.
|
|
560
|
+
rm -f "$TMP/rules"/*.sh
|
|
561
|
+
PASS=0; FAIL=0; NOTE=0
|
|
562
|
+
run_all >/dev/null 2>&1
|
|
563
|
+
[ "$FAIL" -eq 0 ] && [ "$NOTE" -gt 0 ] \
|
|
564
|
+
&& sok "an empty gate reports empty, and does not fail" \
|
|
565
|
+
|| sbad "an empty gate did not behave as empty (FAIL=$FAIL NOTE=$NOTE)"
|
|
566
|
+
|
|
567
|
+
# 10. the runner receives AFM_GATE_LIB, and the library is really there.
|
|
568
|
+
printf '#!/usr/bin/env bash\n. "${AFM_GATE_LIB:?}/scope.sh"\nexit 0\n' > "$TMP/rules/92-lib.sh"
|
|
569
|
+
chmod +x "$TMP/rules/92-lib.sh"
|
|
570
|
+
PASS=0; FAIL=0; NOTE=0
|
|
571
|
+
run_all >/dev/null 2>&1
|
|
572
|
+
[ "$FAIL" -eq 0 ] \
|
|
573
|
+
&& sok "a runner resolves its library through AFM_GATE_LIB" \
|
|
574
|
+
|| sbad "AFM_GATE_LIB did not reach the runner"
|
|
575
|
+
|
|
576
|
+
# --- 3.20.0: the two-class gate. Each block below is one defect that reported GREEN ---
|
|
577
|
+
sec "== self-test: PLUGIN- namespace and exemptions =="
|
|
578
|
+
|
|
579
|
+
mkdir -p "$TMP/core-rules"
|
|
580
|
+
CORE_RULES_DIR="$TMP/core-rules"
|
|
581
|
+
printf '#!/usr/bin/env bash\nprintf "PLUGIN-81\\tsrc/a.ts\\tfinding\\n"\nexit 1\n' > "$TMP/core-rules/PLUGIN-81-x.sh"
|
|
582
|
+
chmod +x "$TMP/core-rules/PLUGIN-81-x.sh"
|
|
583
|
+
|
|
584
|
+
# 1. the glob. `[0-9]*.sh` alone never matches a name that starts with `P`, and a glob that
|
|
585
|
+
# matches nothing reports nothing — the whole shipped directory was invisible.
|
|
586
|
+
if rule_files | grep -q 'PLUGIN-81-x.sh'; then
|
|
587
|
+
sok "a PLUGIN- runner is found by rule_files"
|
|
588
|
+
else
|
|
589
|
+
sbad "rule_files skipped the namespaced runner — the core-rules directory is invisible"
|
|
590
|
+
fi
|
|
591
|
+
[ "$(rule_number "$TMP/core-rules/PLUGIN-81-x.sh")" = "PLUGIN-81" ] \
|
|
592
|
+
&& sok "rule_number keeps the namespace" \
|
|
593
|
+
|| sbad "rule_number stripped the namespace (got $(rule_number "$TMP/core-rules/PLUGIN-81-x.sh"))"
|
|
594
|
+
|
|
595
|
+
# 2. a project runner with the same number wins, so a consumer overrides without a fork.
|
|
596
|
+
printf '#!/usr/bin/env bash\nexit 0\n' > "$TMP/rules/82-mine.sh"; chmod +x "$TMP/rules/82-mine.sh"
|
|
597
|
+
printf '#!/usr/bin/env bash\nexit 1\n' > "$TMP/core-rules/82-theirs.sh"; chmod +x "$TMP/core-rules/82-theirs.sh"
|
|
598
|
+
rule_files | grep -q '82-mine.sh' && ! rule_files | grep -q '82-theirs.sh' \
|
|
599
|
+
&& sok "a project runner overrides a shipped one on the same number" \
|
|
600
|
+
|| sbad "the shipped runner won over the project's — an override is impossible"
|
|
601
|
+
rm -f "$TMP/rules/82-mine.sh" "$TMP/core-rules/82-theirs.sh"
|
|
602
|
+
|
|
603
|
+
# 3. an exempt row is a SCOPE statement, not a region with a budget of zero. Treating it as a
|
|
604
|
+
# region made it absorb every finding under the path and fail there — the exemption became
|
|
605
|
+
# its own opposite.
|
|
606
|
+
printf 'rule\tregion\tcount\tnote\nPLUGIN-83\tsrc/vendor\texempt\tnot ours\n' > "$TMP/ratchet.tsv"
|
|
607
|
+
RATCHET="$TMP/ratchet.tsv"; export RATCHET
|
|
608
|
+
ratchet_exempt PLUGIN-83 src/vendor/lib.ts \
|
|
609
|
+
&& sok "an exempt row takes a path out of scope" \
|
|
610
|
+
|| sbad "ratchet_exempt did not match its own row"
|
|
611
|
+
ratchet_exempt PLUGIN-83 src/vendor-extra/lib.ts \
|
|
612
|
+
&& sbad "the exemption leaked past the path boundary" \
|
|
613
|
+
|| sok "an exemption stops at the path boundary"
|
|
614
|
+
[ "$(ratchet_region PLUGIN-83 src/vendor/lib.ts)" = "." ] \
|
|
615
|
+
&& sok "an exempt row is not a region" \
|
|
616
|
+
|| sbad "an exempt row became a region with budget 0 — the exemption inverted"
|
|
617
|
+
|
|
618
|
+
# 4. rule_surface must read the SPLIT layout. Since 3.9.1 the *Verification* line lives in
|
|
619
|
+
# rules/<id>-*.md and § 3 is an index; reading afm.md alone answered `agent` for every rule,
|
|
620
|
+
# which silently disabled the phantom-gate branch.
|
|
621
|
+
mkdir -p "$TMP/rules-doc"
|
|
622
|
+
AFM_DOC="$TMP/afm.md"
|
|
623
|
+
printf '## 3. Rules\n\nPLUGIN-84. **Split layout.** — [`rules/PLUGIN-84-x.md`](./rules/PLUGIN-84-x.md)\n\n## 4. Other\n' > "$AFM_DOC"
|
|
624
|
+
mkdir -p "$TMP/rules"; mkdir -p "$(dirname "$AFM_DOC")/rules"
|
|
625
|
+
printf -- '---\ntype: rule\n---\n\n# Hard rule PLUGIN-84\n\n*Verification (pre-commit):* rules/PLUGIN-84-x.sh\n' \
|
|
626
|
+
> "$(dirname "$AFM_DOC")/rules/PLUGIN-84-x.md"
|
|
627
|
+
[ "$(rule_surface PLUGIN-84)" = "pre-commit" ] \
|
|
628
|
+
&& sok "rule_surface reads the split layout" \
|
|
629
|
+
|| sbad "rule_surface answered '$(rule_surface PLUGIN-84)' — the phantom-gate branch is dead"
|
|
630
|
+
rules_declared | grep -qx 'PLUGIN-84' \
|
|
631
|
+
&& sok "rules_declared reads a namespaced index entry" \
|
|
632
|
+
|| sbad "rules_declared skipped the namespaced rule"
|
|
633
|
+
|
|
634
|
+
# 5. PLUGIN-40 resolves a cited path from the repo root OR from the citing file's directory.
|
|
635
|
+
# A checker that knows only one convention invents findings, and a gate that invents
|
|
636
|
+
# findings is switched off — the same end as a gate that finds nothing.
|
|
637
|
+
mkdir -p "$TMP/repo/docs/rules" "$TMP/repo/docs/adr" "$TMP/repo/src"
|
|
638
|
+
: > "$TMP/repo/src/real.ts"
|
|
639
|
+
printf -- '*Verification:* [`tools/gate/ghost.sh`](x)\n' > "$TMP/repo/docs/rules/PLUGIN-99-x.md"
|
|
640
|
+
printf -- 'The file `src/real.ts` is fine.\n' > "$TMP/repo/docs/adr/0001-y.md"
|
|
641
|
+
# Resolves ONLY from the citing file's own directory. Without this case the assertion above
|
|
642
|
+
# passes with the directory branch deleted, which is a test of nothing.
|
|
643
|
+
printf -- 'See `notes/sibling.md` next to me.\n' > "$TMP/repo/docs/adr/0002-z.md"
|
|
644
|
+
# `scope_all` asks git, and git answers about the CURRENT directory — a fixture that is not a
|
|
645
|
+
# repository yields an empty file list, and the runner then finds nothing for the wrong reason.
|
|
646
|
+
( cd "$TMP/repo" && git init -q . && git add -A ) >/dev/null 2>&1
|
|
647
|
+
# Created AFTER `git add`, so it is UNTRACKED. That is the case where the directory-relative
|
|
648
|
+
# branch is the only thing that saves it: the `git ls-files` fallback matches by suffix and
|
|
649
|
+
# would cover a tracked sibling on its own, which made the first version of this assertion
|
|
650
|
+
# pass with the branch deleted — a test of nothing.
|
|
651
|
+
mkdir -p "$TMP/repo/docs/adr/notes"; : > "$TMP/repo/docs/adr/notes/sibling.md"
|
|
652
|
+
P40="$(dirname "$0")/core-rules/PLUGIN-40-dead-reference.sh"
|
|
653
|
+
if [ -x "$P40" ]; then
|
|
654
|
+
P40OUT="$(cd "$TMP/repo" && AFM_GATE_REPO="$TMP/repo" AFM_GATE_LIB="$AFM_GATE_LIB" AFM_GATE_MODE=worktree RATCHET=/dev/null bash "$P40" worktree 2>/dev/null)"
|
|
655
|
+
printf '%s' "$P40OUT" | grep -q 'ghost.sh' \
|
|
656
|
+
&& sok "PLUGIN-40 finds a path that exists under neither convention" \
|
|
657
|
+
|| sbad "PLUGIN-40 missed a dead reference"
|
|
658
|
+
printf '%s' "$P40OUT" | grep -q 'real.ts' \
|
|
659
|
+
&& sbad "PLUGIN-40 flagged a path that resolves from the repository root" \
|
|
660
|
+
|| sok "PLUGIN-40 accepts a path that resolves from the repository root"
|
|
661
|
+
printf '%s' "$P40OUT" | grep -q 'notes/sibling.md' \
|
|
662
|
+
&& sbad "PLUGIN-40 flagged a path that resolves from the citing file's own directory" \
|
|
663
|
+
|| sok "PLUGIN-40 accepts a path relative to the citing file"
|
|
664
|
+
else
|
|
665
|
+
snote "PLUGIN-40 runner absent — skipped"
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
rm -rf "$TMP/core-rules"
|
|
669
|
+
CORE_RULES_DIR=""
|
|
670
|
+
|
|
671
|
+
RULES_DIR="$saved_dir"; CORE_RULES_DIR="$saved_core"; RATCHET="$saved_ratchet"; AFM_DOC="$saved_doc"; export RATCHET
|
|
672
|
+
printf '\n%d ok, %d fail (self-test)\n' "$SPASS" "$SFAIL"
|
|
673
|
+
[ "$SFAIL" -eq 0 ]
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
# --- Install ----------------------------------------------------------------
|
|
677
|
+
install_gate() {
|
|
678
|
+
# A repository CANNOT declare its own hooks. If it could, `git clone` would be remote code
|
|
679
|
+
# execution. That is why every hook manager (husky, lefthook, pre-commit) charges a one-time
|
|
680
|
+
# opt-in step. This is ours.
|
|
681
|
+
#
|
|
682
|
+
# Running a command called `--install` IS the authorization. What this mode never does is
|
|
683
|
+
# install itself from a SessionStart or from another hook. An opt-in that installs itself is
|
|
684
|
+
# not an opt-in, and the config is local, so every clone decides again.
|
|
685
|
+
local hooks_dir="$REPO/.githooks"
|
|
686
|
+
local gate_cmd
|
|
687
|
+
gate_cmd="$(python3 - "$REPO" "$0" 2>/dev/null <<'PY' || printf '%s' "$0"
|
|
688
|
+
import os, sys
|
|
689
|
+
print(os.path.relpath(os.path.abspath(sys.argv[2]), os.path.abspath(sys.argv[1])))
|
|
690
|
+
PY
|
|
691
|
+
)"
|
|
692
|
+
|
|
693
|
+
if [ ! -f "$hooks_dir/pre-commit" ]; then
|
|
694
|
+
mkdir -p "$hooks_dir"
|
|
695
|
+
{ printf '#!/usr/bin/env sh\n'
|
|
696
|
+
printf '# Gate for the hard rules — the surface that makes docs/afm.md § 3 executable.\n'
|
|
697
|
+
printf '#\n'
|
|
698
|
+
printf '# Deliberate escape: `git commit --no-verify`. That is not a failure mode. Friction is the\n'
|
|
699
|
+
printf '# intended effect, and a gate with no escape becomes a gate that gets uninstalled.\n'
|
|
700
|
+
printf 'exec ./%s --staged\n' "$gate_cmd"
|
|
701
|
+
} > "$hooks_dir/pre-commit"
|
|
702
|
+
chmod +x "$hooks_dir/pre-commit"
|
|
703
|
+
printf 'wrote %s\n' ".githooks/pre-commit"
|
|
704
|
+
fi
|
|
705
|
+
|
|
706
|
+
local cur
|
|
707
|
+
cur="$(git config --get core.hooksPath 2>/dev/null || true)"
|
|
708
|
+
if [ "$cur" = ".githooks" ]; then
|
|
709
|
+
printf 'The gate is already armed (core.hooksPath = .githooks).\n'
|
|
710
|
+
return 0
|
|
711
|
+
fi
|
|
712
|
+
if [ -n "$cur" ]; then
|
|
713
|
+
# Overwriting another manager's config in silence would disarm THEIR gate with nobody
|
|
714
|
+
# noticing — the exact failure mode this gate exists to prevent.
|
|
715
|
+
printf 'core.hooksPath already points at "%s" (another hook manager?).\n' "$cur" >&2
|
|
716
|
+
printf 'I do not overwrite that in silence. Resolve the conflict and run again, or point\n' >&2
|
|
717
|
+
printf 'that pre-commit at: ./%s --staged\n' "$gate_cmd" >&2
|
|
718
|
+
return 1
|
|
719
|
+
fi
|
|
720
|
+
git config core.hooksPath .githooks || { printf 'failed to write core.hooksPath\n' >&2; return 1; }
|
|
721
|
+
printf 'Gate armed: core.hooksPath = .githooks\n'
|
|
722
|
+
printf 'Every `git commit` now goes through ./%s --staged.\n' "$gate_cmd"
|
|
723
|
+
printf 'Deliberate escape: `git commit --no-verify`. Disarm: `git config --unset core.hooksPath`.\n'
|
|
724
|
+
return 0
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
case "$ACTION" in
|
|
728
|
+
selftest) selftest; exit ;;
|
|
729
|
+
baseline) baseline; exit ;;
|
|
730
|
+
install) install_gate; exit ;;
|
|
731
|
+
audit)
|
|
732
|
+
sec "== audit: hard rule and runner =="
|
|
733
|
+
audit
|
|
734
|
+
printf '\n== debt recorded in the ratchet ==\n'
|
|
735
|
+
audit_debt
|
|
736
|
+
summary; exit ;;
|
|
737
|
+
run)
|
|
738
|
+
sec "== hard rule gate (mode: $MODE) =="
|
|
739
|
+
run_all; summary; exit ;;
|
|
740
|
+
esac
|