Skip to content

Commit ebf8cc4

Browse files
committed
feat(rsr): capability-gated template-applicability policy (presets + gates)
Replaces "every repo looks like the full template" with a rule + a test for which rsr-template-repo modules a repo should carry. Problem: scripts/check-root-shape.sh + .machine_readable/root-allow.txt encode the maximal template as one canonical shape for all repos — so a small Rust CLI is told abi.ipkg, verification/, container/, affinescript/ are "canonical" even with no ABI seam, no in-tree proofs, no container, no AffineScript. That empty scaffolding is vestigial cruft. Model (hybrid presets + gates): - Universal baseline (identity, community-health, machine-readable state, the licence invariant) — always carried. - Every other module is tagged with the capability it serves; a repo carries it iff it declares that capability. - Presets (rust-cli, rust-ffi-lib, rust-service, formal-proof-lib, docs-site, affinescript-app) are named capability bundles; a profile adds/removes on top. The test: carries(M) ⟺ M ∈ baseline ∨ gate(M) ⊆ profile.capabilities. VESTIGIAL = present without the gating capability (remove or declare it). MISSING = capability declared but module absent (add it). Crucial distinction: CONSUMING a prover (e.g. shelling out to Agda) is NOT the `formal-proofs` capability — only CONTAINING in-tree proofs is. Deliverables: - TEMPLATE-APPLICABILITY-POLICY.adoc — the human policy (taxonomy, gate table, presets, the test, the per-repo profile, how it evolves root-allow.txt). - .machine_readable/template-capability-gates.toml — machine-readable source of truth (capabilities, baseline, gates, presets). - scripts/check-rsr-profile.sh — reference checker: reads a repo's rsr-profile.a2ml, resolves effective capabilities, reports VESTIGIAL/MISSING. Validated against arghda-core (preset rust-cli): clean; planting a stray abi.ipkg + container/ is correctly flagged VESTIGIAL. Naming note: the file is rsr-profile.a2ml (PROFILE.a2ml already means an A2ML schema profile in k9-svc). DRAFT first pass. Follow-up: fold effective-allowlist into check-root-shape.sh; confirm the governance-tier boundary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GiSiEfgZCte35dyykgBHs
1 parent f290d4a commit ebf8cc4

3 files changed

Lines changed: 354 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# Machine-readable source of truth for the RSR template-applicability model.
5+
# Human policy: ../TEMPLATE-APPLICABILITY-POLICY.adoc
6+
# Reference checker: ../scripts/check-rsr-profile.sh
7+
#
8+
# A repo carries a gated module iff its rsr-profile's effective capability set
9+
# (preset capabilities + add - remove) contains the module's gating capability.
10+
# Arrays are kept single-line so the checker can parse them with grep.
11+
12+
[meta]
13+
version = "0.1.0"
14+
policy = "TEMPLATE-APPLICABILITY-POLICY.adoc"
15+
16+
[capabilities]
17+
# Every capability a profile may declare.
18+
known = ["rust", "zig", "agda", "idris2", "haskell", "gleam", "elixir", "affinescript", "julia", "ocaml", "bash", "cli", "library", "ffi", "abi", "api-service", "formal-proofs", "mobile", "web-ui", "docs-site", "published-package", "container", "reproducible-build", "governance-tier", "benchmarks"]
19+
20+
[baseline]
21+
# Always carried (gate = empty). Globs/dirs allowed; not capability-gated.
22+
paths = ["README.adoc", "EXPLAINME.adoc", "LICENSE", "SECURITY.md", "CONTRIBUTING.md", "CODE_OF_CONDUCT.md", "CHANGELOG.md", "0-AI-MANIFEST.a2ml", ".machine_readable/6a2/", ".machine_readable/rsr-profile.a2ml", ".well-known/", ".gitignore", "Justfile"]
23+
24+
[gates]
25+
# "module path (file, dir/, or glob)" = "gating capability"
26+
"Cargo.toml" = "rust"
27+
"Cargo.lock" = "rust"
28+
"src/**/*.rs" = "rust"
29+
".github/workflows/rust-ci.yml" = "rust"
30+
"src/interface/ffi/" = "ffi"
31+
"abi.ipkg" = "abi"
32+
"src/interface/Abi/" = "abi"
33+
"src/interface/generated/" = "abi"
34+
"verification/proofs/" = "formal-proofs"
35+
".github/workflows/e2e.yml" = "api-service"
36+
"container/" = "container"
37+
"Containerfile" = "container"
38+
"build/guix.scm" = "reproducible-build"
39+
"flake.nix" = "reproducible-build"
40+
"affinescript/" = "affinescript"
41+
"benches/" = "benchmarks"
42+
".github/workflows/release.yml" = "published-package"
43+
"AUDIT.adoc" = "governance-tier"
44+
"AFFIRMATION.adoc" = "governance-tier"
45+
"GOVERNANCE.adoc" = "governance-tier"
46+
"MAINTAINERS.adoc" = "governance-tier"
47+
48+
[presets]
49+
# preset name = [ base capabilities ]
50+
rust-cli = ["rust", "cli", "library"]
51+
rust-ffi-lib = ["rust", "zig", "ffi", "abi", "library"]
52+
rust-service = ["rust", "api-service", "container", "reproducible-build"]
53+
formal-proof-lib = ["formal-proofs", "library"]
54+
docs-site = ["docs-site"]
55+
affinescript-app = ["affinescript", "web-ui"]

TEMPLATE-APPLICABILITY-POLICY.adoc

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// SPDX-License-Identifier: CC-BY-SA-4.0
2+
= RSR Template-Applicability Policy
3+
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
:toc:
5+
:toc-placement: preamble
6+
7+
Canonical policy for *which parts of `rsr-template-repo` a given repository
8+
should carry*. It replaces "every repo looks like the full template" with a
9+
capability-gated model: a repo carries a template module only if it actually
10+
has the capability that module serves.
11+
12+
== Why
13+
14+
`rsr-template-repo` is maximal: it ships FFI/ABI seams, formal-proof trees,
15+
container scaffolding, service e2e, mobile shells, an AffineScript subtree, and
16+
the full governance trio — so that *some* repo can be scaffolded for *any* of
17+
them. The existing enforcement
18+
(`scripts/check-root-shape.sh` reading `.machine_readable/root-allow.txt`)
19+
encodes that maximum as **one canonical shape for all repos**. The result is
20+
over-scaffolding: a small Rust CLI is told `abi.ipkg`, `verification/`,
21+
`container/`, and `affinescript/` are "canonical" even though it has no ABI
22+
seam, no in-tree proofs, ships no container, and contains no AffineScript.
23+
24+
Empty scaffolding is not neutral — it is vestigial cruft that misleads readers,
25+
invites drift, and dilutes the signal of the parts that are real. This policy
26+
makes "carry it only if you need it" a *rule with a test*, not a judgement call.
27+
28+
== Model
29+
30+
Three layers:
31+
32+
. *Universal baseline* — modules every repo carries unconditionally (identity,
33+
community-health, machine-readable state, the licence invariant).
34+
. *Capability gates* — every other template module is tagged with the
35+
*capability* it serves. A repo carries the module **iff** it declares that
36+
capability.
37+
. *Presets* — named capability bundles (`rust-cli`, `rust-ffi-lib`,
38+
`rust-service`, `formal-proof-lib`, `docs-site`, …) a repo adopts as its base,
39+
then adjusts with explicit per-capability `add` / `remove`. (The "hybrid"
40+
layer: presets for ergonomics, gates for precision.)
41+
42+
A repo's *profile* (`.machine_readable/rsr-profile.a2ml`) declares its preset
43+
and any adjustments. Its effective capability set drives everything else.
44+
45+
== Capability taxonomy
46+
47+
[cols="1,2,3"]
48+
|===
49+
| Axis | Capability | A repo declares it when…
50+
51+
.4+| Language
52+
| `rust` | it contains Rust/SPARK source (`Cargo.toml`, `*.rs`).
53+
| `zig` | it contains Zig source (FFI/gateway/SDK layer).
54+
| `agda` / `idris2` / `haskell` / `gleam` / `elixir` / `affinescript` / `julia` / `ocaml`
55+
| it contains source in that language *as its own code* (not merely consumes a tool written in it).
56+
| `bash` | it ships non-trivial shell scripts (almost universal; rarely gating).
57+
58+
.5+| Interface / seam
59+
| `cli` | it ships a command-line binary.
60+
| `library` | it is consumed/published as a library.
61+
| `ffi` | it exposes or consumes a C-ABI FFI seam (Zig FFI scaffold).
62+
| `abi` | it has a *formally specified* ABI (Idris2 `abi.ipkg` + `src/interface/Abi/` proofs + `ffi/` + `generated/`).
63+
| `api-service` | it runs as a network service/daemon.
64+
65+
| Verification
66+
| `formal-proofs` | it *contains* mechanised proofs in-tree (`verification/proofs/`). **Consuming** a prover (e.g. a tool that shells out to Agda) is *not* this capability.
67+
68+
.3+| Surface
69+
| `mobile` | it ships a Tauri/Dioxus mobile app.
70+
| `web-ui` | it ships a browser UI.
71+
| `docs-site` | it publishes a static site (casket-ssg / Pages).
72+
73+
.3+| Distribution
74+
| `published-package` | it publishes to a registry (crates.io / Hackage / JSR).
75+
| `container` | it ships a container image.
76+
| `reproducible-build` | it provides a Guix/Nix reproducible build.
77+
78+
.2+| Governance tier
79+
| `governance-tier` | it is load-bearing/critical enough to carry the full `AUDIT` / `AFFIRMATION` / `GOVERNANCE` / `MAINTAINERS` trio. Small leaf repos may decline it.
80+
| `benchmarks` | it maintains a benchmark suite (`benches/`).
81+
|===
82+
83+
The machine-readable source of truth for capabilities, gates, and presets is
84+
`.machine_readable/template-capability-gates.toml`. The tables below mirror it
85+
for humans.
86+
87+
== The gate table (module → capability)
88+
89+
*Universal baseline* (gate = ∅; always carried):
90+
91+
`README.adoc`, `EXPLAINME.adoc`, `LICENSE`, `SECURITY.md`, `CONTRIBUTING.md`,
92+
`CODE_OF_CONDUCT.md`, `CHANGELOG.md`, `0-AI-MANIFEST.a2ml`,
93+
`.machine_readable/6a2/{STATE,META,ECOSYSTEM,AGENTIC,NEUROSYM,PLAYBOOK}.a2ml`,
94+
`.machine_readable/rsr-profile.a2ml`, `.well-known/`, `.gitignore`, `Justfile`,
95+
and the SPDX licence invariant (`LICENCE-POLICY.adoc`).
96+
97+
*Gated modules:*
98+
99+
[cols="2,1"]
100+
|===
101+
| Template module / path | Gating capability
102+
103+
| `Cargo.toml`, `Cargo.lock`, `src/**/*.rs`, `.github/workflows/rust-ci.yml` | `rust`
104+
| `src/interface/ffi/**` (Zig) | `ffi`
105+
| `abi.ipkg`, `src/interface/Abi/**` (Idris2), `src/interface/generated/**` | `abi`
106+
| `verification/proofs/**`, proof-CI | `formal-proofs`
107+
| service e2e, `.github/workflows/e2e.yml` | `api-service`
108+
| `container/`, `Containerfile` | `container`
109+
| `build/guix.scm`, `flake.nix`, `.guix-channel` | `reproducible-build`
110+
| mobile shell (Tauri/Dioxus) | `mobile`
111+
| `affinescript/` subtree | `affinescript`
112+
| `benches/` | `benchmarks`
113+
| `AUDIT.adoc`, `AFFIRMATION.adoc`, `GOVERNANCE.adoc`, `MAINTAINERS.adoc` | `governance-tier`
114+
| `.github/workflows/release.yml`, registry metadata | `published-package`
115+
|===
116+
117+
== Presets
118+
119+
[cols="1,3"]
120+
|===
121+
| Preset | Base capabilities
122+
123+
| `rust-cli` | `rust`, `cli`, `library`
124+
| `rust-ffi-lib` | `rust`, `zig`, `ffi`, `abi`, `library`
125+
| `rust-service` | `rust`, `api-service`, `container`, `reproducible-build`
126+
| `formal-proof-lib` | `agda` *or* `idris2`, `formal-proofs`, `library`
127+
| `docs-site` | `docs-site`
128+
| `affinescript-app` | `affinescript`, `web-ui`
129+
|===
130+
131+
A profile may `add` or `remove` individual capabilities on top of its preset
132+
(e.g. `rust-cli` + `add = ["published-package"]`).
133+
134+
== The test
135+
136+
Let `P` be a repo's effective capability set (`preset` ∪ `add` − `remove`).
137+
138+
----
139+
carries(M) ⟺ M ∈ baseline ∨ gate(M) ⊆ P
140+
----
141+
142+
For any tracked template-module path `X`:
143+
144+
* *OK* — `carries(X)` and `X` is present.
145+
* *MISSING* — `carries(X)` but `X` is absent → add it.
146+
* *VESTIGIAL* — `X` is a known template module, present, but `gate(X) ⊄ P` →
147+
remove it *or* declare the missing capability (with justification).
148+
* *UNKNOWN* — `X` is project-specific (not a template module) → allowed; it is
149+
the repo's own content, governed by the root-shape allowlist as before.
150+
151+
The key new verdict is **VESTIGIAL**: scaffolding present without the capability
152+
that justifies it. That is the thing this policy removes.
153+
154+
== Per-repo profile
155+
156+
`.machine_readable/rsr-profile.a2ml` (A2ML, TOML-like; SPDX `MPL-2.0`):
157+
158+
[source,toml]
159+
----
160+
[profile]
161+
preset = "rust-cli"
162+
add = [] # extra capabilities beyond the preset
163+
remove = [] # preset capabilities this repo does NOT have
164+
165+
[rationale]
166+
# One line per non-obvious add/remove or declined capability.
167+
no-abi = "language-agnostic engine; no C-ABI seam, no Idris2 ABI proofs"
168+
no-formal-proofs = "consumes Agda (shells out); contains no in-tree proofs"
169+
----
170+
171+
== Enforcement (how this evolves the existing machinery)
172+
173+
`root-allow.txt` becomes *derived*, not hand-maintained as one shape:
174+
175+
----
176+
effective-allowlist(repo) = baseline ∪ { M : gate(M) ⊆ profile(repo).capabilities }
177+
----
178+
179+
* `scripts/check-rsr-profile.sh` (reference implementation, this repo) reads a
180+
repo's `rsr-profile.a2ml` + `template-capability-gates.toml` and reports
181+
`MISSING` / `VESTIGIAL` modules.
182+
* `check-root-shape.sh` / `validate-template.sh` evolve to consume
183+
`effective-allowlist(repo)` instead of a static `root-allow.txt` (follow-up;
184+
the static file remains valid as the `governance-tier` + all-capabilities
185+
maximum until then).
186+
187+
A repo opts in by adding its `rsr-profile.a2ml` and running the check in CI.
188+
189+
== Worked example — arghda-core (`rust-cli`)
190+
191+
A small language-agnostic Rust CLI. Profile: `preset = "rust-cli"`,
192+
`remove = []`. Effective capabilities: `rust`, `cli`, `library`.
193+
194+
* *Carries* (gate ⊆ P): the universal baseline + `Cargo.toml`/`Cargo.lock`/
195+
`src/**/*.rs`/`rust-ci.yml` (`rust`).
196+
* *Correctly absent* (VESTIGIAL if present): `abi.ipkg` + `src/interface/`
197+
(`abi`/`ffi`), `verification/proofs/` (`formal-proofs` — arghda *consumes*
198+
Agda but contains no proofs), `container/` (`container`), `affinescript/`
199+
(`affinescript`), the `AUDIT`/`AFFIRMATION`/`GOVERNANCE`/`MAINTAINERS` trio
200+
(`governance-tier`, declined by a leaf tool).
201+
202+
That is precisely the lean shape arghda-core ships — now justified by a rule
203+
rather than a judgement call.
204+
205+
== Status
206+
207+
DRAFT (first pass). Open items: fold `effective-allowlist` into
208+
`check-root-shape.sh`; confirm the `governance-tier` boundary (which repos must
209+
carry the full trio); enumerate the remaining maximal-template modules into the
210+
gate table as they are profiled.

scripts/check-rsr-profile.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
4+
#
5+
# Reference checker for the RSR template-applicability model.
6+
# Policy: ../TEMPLATE-APPLICABILITY-POLICY.adoc
7+
# Data: ../.machine_readable/template-capability-gates.toml
8+
#
9+
# Reads a target repo's .machine_readable/rsr-profile.a2ml, resolves its
10+
# effective capability set (preset capabilities + add - remove) against the
11+
# gate table, and reports gated modules that are:
12+
# VESTIGIAL — present but the repo does not declare the gating capability.
13+
# MISSING — the capability is declared but the module is absent.
14+
#
15+
# Usage: check-rsr-profile.sh [REPO_DIR] (default: current directory)
16+
# Exit: 0 OK | 1 violations | 2 setup error
17+
set -euo pipefail
18+
19+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20+
GATES="${RSR_GATES:-$SCRIPT_DIR/../.machine_readable/template-capability-gates.toml}"
21+
REPO="${1:-.}"
22+
PROFILE="$REPO/.machine_readable/rsr-profile.a2ml"
23+
24+
[ -f "$GATES" ] || { echo "ERROR: gates file not found: $GATES" >&2; exit 2; }
25+
[ -f "$PROFILE" ] || { echo "ERROR: no profile at $PROFILE" >&2; exit 2; }
26+
27+
# Body lines of a [section] (comments and blanks stripped).
28+
section() { awk -v s="[$1]" '$0==s{f=1;next} /^\[/{f=0} f && !/^[[:space:]]*#/ && NF' "$2"; }
29+
# "quoted" tokens on the line whose key is $1 (reads section body from stdin).
30+
quoted_on_key() { grep -E "^[[:space:]]*$1[[:space:]]*=" | grep -oE '"[^"]+"' | tr -d '"' || true; }
31+
32+
# --- target profile ---
33+
PBODY="$(section profile "$PROFILE")"
34+
PRESET="$(printf '%s\n' "$PBODY" | quoted_on_key preset | sed -n 1p)"
35+
ADD="$(printf '%s\n' "$PBODY" | quoted_on_key add)"
36+
REMOVE="$(printf '%s\n' "$PBODY" | quoted_on_key remove)"
37+
[ -n "$PRESET" ] || { echo "ERROR: profile declares no preset" >&2; exit 2; }
38+
39+
# --- preset's base capabilities (from the gate data) ---
40+
PRESETCAPS="$(section presets "$GATES" | quoted_on_key "$PRESET")"
41+
[ -n "$PRESETCAPS" ] || { echo "ERROR: unknown preset '$PRESET' (not in $GATES [presets])" >&2; exit 2; }
42+
43+
# --- effective capabilities = presetcaps + add - remove ---
44+
EFFECTIVE="$(printf '%s\n%s\n' "$PRESETCAPS" "$ADD" | sort -u | sed '/^$/d')"
45+
if [ -n "$REMOVE" ]; then
46+
EFFECTIVE="$(comm -23 <(printf '%s\n' "$EFFECTIVE") <(printf '%s\n' "$REMOVE" | sort -u))"
47+
fi
48+
has_cap() { printf '%s\n' "$EFFECTIVE" | grep -qx "$1"; }
49+
50+
# Presence of a module path: file, dir/ (trailing slash), or glob (contains *).
51+
present() {
52+
local key="$1"
53+
case "$key" in
54+
*/) [ -d "$REPO/${key%/}" ] ;;
55+
*'*'*) ( shopt -s globstar nullglob; local m=("$REPO"/$key); [ ${#m[@]} -gt 0 ] ) ;;
56+
*) [ -e "$REPO/$key" ] ;;
57+
esac
58+
}
59+
60+
echo "repo: $REPO"
61+
echo "preset: $PRESET"
62+
echo "effective capabilities: $(printf '%s ' $EFFECTIVE)"
63+
echo
64+
65+
fail=0
66+
while IFS= read -r line; do
67+
path="$(printf '%s' "$line" | grep -oE '"[^"]+"' | sed -n 1p | tr -d '"')"
68+
cap="$(printf '%s' "$line" | grep -oE '"[^"]+"' | sed -n 2p | tr -d '"')"
69+
[ -n "$path" ] && [ -n "$cap" ] || continue
70+
if has_cap "$cap"; then
71+
present "$path" || { echo " MISSING ($cap): $path"; fail=1; }
72+
else
73+
present "$path" && { echo " VESTIGIAL (no '$cap' capability): $path"; fail=1; }
74+
fi
75+
done < <(section gates "$GATES")
76+
77+
if [ "$fail" -ne 0 ]; then
78+
cat >&2 <<'MSG'
79+
80+
rsr-profile check: FAIL — scaffold does not match declared capabilities.
81+
Fix one of:
82+
* remove the vestigial module, OR
83+
* declare the capability in .machine_readable/rsr-profile.a2ml (with a
84+
[rationale] line), OR
85+
* add the missing module.
86+
MSG
87+
exit 1
88+
fi
89+
echo "rsr-profile check: OK — scaffold matches declared capabilities."

0 commit comments

Comments
 (0)