Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .machine_readable/template-capability-gates.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# Machine-readable source of truth for the RSR template-applicability model.
# Human policy: ../TEMPLATE-APPLICABILITY-POLICY.adoc
# Reference checker: ../scripts/check-rsr-profile.sh
#
# A repo carries a gated module iff its rsr-profile's effective capability set
# (preset capabilities + add - remove) contains the module's gating capability.
# Arrays are kept single-line so the checker can parse them with grep.

[meta]
version = "0.1.0"
policy = "TEMPLATE-APPLICABILITY-POLICY.adoc"

[capabilities]
# Every capability a profile may declare.
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"]

[baseline]
# Always carried (gate = empty). Globs/dirs allowed; not capability-gated.
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"]

[gates]
# "module path (file, dir/, or glob)" = "gating capability"
"Cargo.toml" = "rust"
"Cargo.lock" = "rust"
"src/**/*.rs" = "rust"
".github/workflows/rust-ci.yml" = "rust"
"src/interface/ffi/" = "ffi"
"abi.ipkg" = "abi"
"src/interface/Abi/" = "abi"
"src/interface/generated/" = "abi"
"verification/proofs/" = "formal-proofs"
".github/workflows/e2e.yml" = "api-service"
"container/" = "container"
"Containerfile" = "container"
"build/guix.scm" = "reproducible-build"
"flake.nix" = "reproducible-build"
"affinescript/" = "affinescript"
"benches/" = "benchmarks"
".github/workflows/release.yml" = "published-package"
"AUDIT.adoc" = "governance-tier"
"AFFIRMATION.adoc" = "governance-tier"
"GOVERNANCE.adoc" = "governance-tier"
"MAINTAINERS.adoc" = "governance-tier"

[presets]
# preset name = [ base capabilities ]
rust-cli = ["rust", "cli", "library"]
rust-ffi-lib = ["rust", "zig", "ffi", "abi", "library"]
rust-service = ["rust", "api-service", "container", "reproducible-build"]
formal-proof-lib = ["formal-proofs", "library"]
docs-site = ["docs-site"]
affinescript-app = ["affinescript", "web-ui"]
210 changes: 210 additions & 0 deletions TEMPLATE-APPLICABILITY-POLICY.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
= RSR Template-Applicability Policy
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
:toc:
:toc-placement: preamble

Canonical policy for *which parts of `rsr-template-repo` a given repository
should carry*. It replaces "every repo looks like the full template" with a
capability-gated model: a repo carries a template module only if it actually
has the capability that module serves.

== Why

`rsr-template-repo` is maximal: it ships FFI/ABI seams, formal-proof trees,
container scaffolding, service e2e, mobile shells, an AffineScript subtree, and
the full governance trio — so that *some* repo can be scaffolded for *any* of
them. The existing enforcement
(`scripts/check-root-shape.sh` reading `.machine_readable/root-allow.txt`)
encodes that maximum as **one canonical shape for all repos**. The result is
over-scaffolding: a small Rust CLI is told `abi.ipkg`, `verification/`,
`container/`, and `affinescript/` are "canonical" even though it has no ABI
seam, no in-tree proofs, ships no container, and contains no AffineScript.

Empty scaffolding is not neutral — it is vestigial cruft that misleads readers,
invites drift, and dilutes the signal of the parts that are real. This policy
makes "carry it only if you need it" a *rule with a test*, not a judgement call.

== Model

Three layers:

. *Universal baseline* — modules every repo carries unconditionally (identity,
community-health, machine-readable state, the licence invariant).
. *Capability gates* — every other template module is tagged with the
*capability* it serves. A repo carries the module **iff** it declares that
capability.
. *Presets* — named capability bundles (`rust-cli`, `rust-ffi-lib`,
`rust-service`, `formal-proof-lib`, `docs-site`, …) a repo adopts as its base,
then adjusts with explicit per-capability `add` / `remove`. (The "hybrid"
layer: presets for ergonomics, gates for precision.)

A repo's *profile* (`.machine_readable/rsr-profile.a2ml`) declares its preset
and any adjustments. Its effective capability set drives everything else.

== Capability taxonomy

[cols="1,2,3"]
|===
| Axis | Capability | A repo declares it when…

.4+| Language
| `rust` | it contains Rust/SPARK source (`Cargo.toml`, `*.rs`).
| `zig` | it contains Zig source (FFI/gateway/SDK layer).
| `agda` / `idris2` / `haskell` / `gleam` / `elixir` / `affinescript` / `julia` / `ocaml`
| it contains source in that language *as its own code* (not merely consumes a tool written in it).
| `bash` | it ships non-trivial shell scripts (almost universal; rarely gating).

.5+| Interface / seam
| `cli` | it ships a command-line binary.
| `library` | it is consumed/published as a library.
| `ffi` | it exposes or consumes a C-ABI FFI seam (Zig FFI scaffold).
| `abi` | it has a *formally specified* ABI (Idris2 `abi.ipkg` + `src/interface/Abi/` proofs + `ffi/` + `generated/`).
| `api-service` | it runs as a network service/daemon.

| Verification
| `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.

.3+| Surface
| `mobile` | it ships a Tauri/Dioxus mobile app.
| `web-ui` | it ships a browser UI.
| `docs-site` | it publishes a static site (casket-ssg / Pages).

.3+| Distribution
| `published-package` | it publishes to a registry (crates.io / Hackage / JSR).
| `container` | it ships a container image.
| `reproducible-build` | it provides a Guix/Nix reproducible build.

.2+| Governance tier
| `governance-tier` | it is load-bearing/critical enough to carry the full `AUDIT` / `AFFIRMATION` / `GOVERNANCE` / `MAINTAINERS` trio. Small leaf repos may decline it.
| `benchmarks` | it maintains a benchmark suite (`benches/`).
|===

The machine-readable source of truth for capabilities, gates, and presets is
`.machine_readable/template-capability-gates.toml`. The tables below mirror it
for humans.

== The gate table (module → capability)

*Universal baseline* (gate = ∅; always carried):

`README.adoc`, `EXPLAINME.adoc`, `LICENSE`, `SECURITY.md`, `CONTRIBUTING.md`,
`CODE_OF_CONDUCT.md`, `CHANGELOG.md`, `0-AI-MANIFEST.a2ml`,
`.machine_readable/6a2/{STATE,META,ECOSYSTEM,AGENTIC,NEUROSYM,PLAYBOOK}.a2ml`,
`.machine_readable/rsr-profile.a2ml`, `.well-known/`, `.gitignore`, `Justfile`,
and the SPDX licence invariant (`LICENCE-POLICY.adoc`).

*Gated modules:*

[cols="2,1"]
|===
| Template module / path | Gating capability

| `Cargo.toml`, `Cargo.lock`, `src/**/*.rs`, `.github/workflows/rust-ci.yml` | `rust`
| `src/interface/ffi/**` (Zig) | `ffi`
| `abi.ipkg`, `src/interface/Abi/**` (Idris2), `src/interface/generated/**` | `abi`
| `verification/proofs/**`, proof-CI | `formal-proofs`
| service e2e, `.github/workflows/e2e.yml` | `api-service`
| `container/`, `Containerfile` | `container`
| `build/guix.scm`, `flake.nix`, `.guix-channel` | `reproducible-build`
| mobile shell (Tauri/Dioxus) | `mobile`
| `affinescript/` subtree | `affinescript`
| `benches/` | `benchmarks`
| `AUDIT.adoc`, `AFFIRMATION.adoc`, `GOVERNANCE.adoc`, `MAINTAINERS.adoc` | `governance-tier`
| `.github/workflows/release.yml`, registry metadata | `published-package`
|===

== Presets

[cols="1,3"]
|===
| Preset | Base capabilities

| `rust-cli` | `rust`, `cli`, `library`
| `rust-ffi-lib` | `rust`, `zig`, `ffi`, `abi`, `library`
| `rust-service` | `rust`, `api-service`, `container`, `reproducible-build`
| `formal-proof-lib` | `agda` *or* `idris2`, `formal-proofs`, `library`
| `docs-site` | `docs-site`
| `affinescript-app` | `affinescript`, `web-ui`
|===

A profile may `add` or `remove` individual capabilities on top of its preset
(e.g. `rust-cli` + `add = ["published-package"]`).

== The test

Let `P` be a repo's effective capability set (`preset` ∪ `add` − `remove`).

----
carries(M) ⟺ M ∈ baseline ∨ gate(M) ⊆ P
----

For any tracked template-module path `X`:

* *OK* — `carries(X)` and `X` is present.
* *MISSING* — `carries(X)` but `X` is absent → add it.
* *VESTIGIAL* — `X` is a known template module, present, but `gate(X) ⊄ P` →
remove it *or* declare the missing capability (with justification).
* *UNKNOWN* — `X` is project-specific (not a template module) → allowed; it is
the repo's own content, governed by the root-shape allowlist as before.

The key new verdict is **VESTIGIAL**: scaffolding present without the capability
that justifies it. That is the thing this policy removes.

== Per-repo profile

`.machine_readable/rsr-profile.a2ml` (A2ML, TOML-like; SPDX `MPL-2.0`):

[source,toml]
----
[profile]
preset = "rust-cli"
add = [] # extra capabilities beyond the preset
remove = [] # preset capabilities this repo does NOT have

[rationale]
# One line per non-obvious add/remove or declined capability.
no-abi = "language-agnostic engine; no C-ABI seam, no Idris2 ABI proofs"
no-formal-proofs = "consumes Agda (shells out); contains no in-tree proofs"
----

== Enforcement (how this evolves the existing machinery)

`root-allow.txt` becomes *derived*, not hand-maintained as one shape:

----
effective-allowlist(repo) = baseline ∪ { M : gate(M) ⊆ profile(repo).capabilities }
----

* `scripts/check-rsr-profile.sh` (reference implementation, this repo) reads a
repo's `rsr-profile.a2ml` + `template-capability-gates.toml` and reports
`MISSING` / `VESTIGIAL` modules.
* `check-root-shape.sh` / `validate-template.sh` evolve to consume
`effective-allowlist(repo)` instead of a static `root-allow.txt` (follow-up;
the static file remains valid as the `governance-tier` + all-capabilities
maximum until then).

A repo opts in by adding its `rsr-profile.a2ml` and running the check in CI.

== Worked example — arghda-core (`rust-cli`)

A small language-agnostic Rust CLI. Profile: `preset = "rust-cli"`,
`remove = []`. Effective capabilities: `rust`, `cli`, `library`.

* *Carries* (gate ⊆ P): the universal baseline + `Cargo.toml`/`Cargo.lock`/
`src/**/*.rs`/`rust-ci.yml` (`rust`).
* *Correctly absent* (VESTIGIAL if present): `abi.ipkg` + `src/interface/`
(`abi`/`ffi`), `verification/proofs/` (`formal-proofs` — arghda *consumes*
Agda but contains no proofs), `container/` (`container`), `affinescript/`
(`affinescript`), the `AUDIT`/`AFFIRMATION`/`GOVERNANCE`/`MAINTAINERS` trio
(`governance-tier`, declined by a leaf tool).

That is precisely the lean shape arghda-core ships — now justified by a rule
rather than a judgement call.

== Status

DRAFT (first pass). Open items: fold `effective-allowlist` into
`check-root-shape.sh`; confirm the `governance-tier` boundary (which repos must
carry the full trio); enumerate the remaining maximal-template modules into the
gate table as they are profiled.
89 changes: 89 additions & 0 deletions scripts/check-rsr-profile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# Reference checker for the RSR template-applicability model.
# Policy: ../TEMPLATE-APPLICABILITY-POLICY.adoc
# Data: ../.machine_readable/template-capability-gates.toml
#
# Reads a target repo's .machine_readable/rsr-profile.a2ml, resolves its
# effective capability set (preset capabilities + add - remove) against the
# gate table, and reports gated modules that are:
# VESTIGIAL — present but the repo does not declare the gating capability.
# MISSING — the capability is declared but the module is absent.
#
# Usage: check-rsr-profile.sh [REPO_DIR] (default: current directory)
# Exit: 0 OK | 1 violations | 2 setup error
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GATES="${RSR_GATES:-$SCRIPT_DIR/../.machine_readable/template-capability-gates.toml}"
REPO="${1:-.}"
PROFILE="$REPO/.machine_readable/rsr-profile.a2ml"

[ -f "$GATES" ] || { echo "ERROR: gates file not found: $GATES" >&2; exit 2; }
[ -f "$PROFILE" ] || { echo "ERROR: no profile at $PROFILE" >&2; exit 2; }

# Body lines of a [section] (comments and blanks stripped).
section() { awk -v s="[$1]" '$0==s{f=1;next} /^\[/{f=0} f && !/^[[:space:]]*#/ && NF' "$2"; }
# "quoted" tokens on the line whose key is $1 (reads section body from stdin).
quoted_on_key() { grep -E "^[[:space:]]*$1[[:space:]]*=" | grep -oE '"[^"]+"' | tr -d '"' || true; }

# --- target profile ---
PBODY="$(section profile "$PROFILE")"
PRESET="$(printf '%s\n' "$PBODY" | quoted_on_key preset | sed -n 1p)"
ADD="$(printf '%s\n' "$PBODY" | quoted_on_key add)"
REMOVE="$(printf '%s\n' "$PBODY" | quoted_on_key remove)"
[ -n "$PRESET" ] || { echo "ERROR: profile declares no preset" >&2; exit 2; }

# --- preset's base capabilities (from the gate data) ---
PRESETCAPS="$(section presets "$GATES" | quoted_on_key "$PRESET")"
[ -n "$PRESETCAPS" ] || { echo "ERROR: unknown preset '$PRESET' (not in $GATES [presets])" >&2; exit 2; }

# --- effective capabilities = presetcaps + add - remove ---
EFFECTIVE="$(printf '%s\n%s\n' "$PRESETCAPS" "$ADD" | sort -u | sed '/^$/d')"
if [ -n "$REMOVE" ]; then
EFFECTIVE="$(comm -23 <(printf '%s\n' "$EFFECTIVE") <(printf '%s\n' "$REMOVE" | sort -u))"
fi
has_cap() { printf '%s\n' "$EFFECTIVE" | grep -qx "$1"; }

# Presence of a module path: file, dir/ (trailing slash), or glob (contains *).
present() {
local key="$1"
case "$key" in
*/) [ -d "$REPO/${key%/}" ] ;;
*'*'*) ( shopt -s globstar nullglob; local m=("$REPO"/$key); [ ${#m[@]} -gt 0 ] ) ;;
*) [ -e "$REPO/$key" ] ;;
esac
}

echo "repo: $REPO"
echo "preset: $PRESET"
echo "effective capabilities: $(printf '%s ' $EFFECTIVE)"
echo

fail=0
while IFS= read -r line; do
path="$(printf '%s' "$line" | grep -oE '"[^"]+"' | sed -n 1p | tr -d '"')"
cap="$(printf '%s' "$line" | grep -oE '"[^"]+"' | sed -n 2p | tr -d '"')"
[ -n "$path" ] && [ -n "$cap" ] || continue
if has_cap "$cap"; then
present "$path" || { echo " MISSING ($cap): $path"; fail=1; }
else
present "$path" && { echo " VESTIGIAL (no '$cap' capability): $path"; fail=1; }
fi
done < <(section gates "$GATES")

if [ "$fail" -ne 0 ]; then
cat >&2 <<'MSG'

rsr-profile check: FAIL — scaffold does not match declared capabilities.
Fix one of:
* remove the vestigial module, OR
* declare the capability in .machine_readable/rsr-profile.a2ml (with a
[rationale] line), OR
* add the missing module.
MSG
exit 1
fi
echo "rsr-profile check: OK — scaffold matches declared capabilities."
Loading