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
21 changes: 21 additions & 0 deletions bots/rhodibot/canon/pin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,24 @@ sha256 = "37cb5f679b414f6ee99c6bb62c460fd5349ff7d50cc1dab25b1e1a8e8d3c7bb9"
categories = 11
criteria = 74
weight_sum = 88

# The release's second artefact: the gate table. It says which capabilities a
# profile may declare, what a preset expands to, and which module paths belong
# to which capability. Same rule as the copy above -- taken verbatim, pinned by
# digest, and pinned by shape. `slot` is the name `canon.lock` gives it; the
# drift script checks that all three agree, because a pin nothing verifies is
# not a binding.
[gates]
repo = "hyperpolymath/standards"
path = ".machine_readable/template-capability-gates.toml"
slot = "gates"
version = "0.2.0"
released = "2026-09-17"
pinned = "2026-09-19"
sha256 = "b65ce75438c42d01bedf0325b9a97f1a575064866158b4340d7427fca911dd1b"

# The shape of the pinned table. A vocabulary that shrank would leave profiles
# declaring the lost words unparseable, and criteria gated on them permanently
# inapplicable -- the failure mode is quiet, so the numbers are asserted.
known = 28
presets = 11
139 changes: 139 additions & 0 deletions bots/rhodibot/canon/template-capability-gates.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# 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.2.0"
policy = "0-canon/TEMPLATE-APPLICABILITY-POLICY.adoc"

[capabilities]
# Every capability a profile may declare.
# "plugin" = code hosted inside a third-party extension host (WordPress,
# Zotero, a userscript manager): the host owns the runtime and the packaging
# rules, so host-mandated files are exempt from the usual language gates the
# way interop-target bindings are.
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", "plugin", "deno",
"canon"] # "deno": criterion 4.3.1 gates on it; without it here that criterion could never become applicable.
# "canon": the ROLE of hyperpolymath/standards itself (see [canon] below).
# Without it in `known`, a profile declaring role = "canon" is rejected as an
# unknown CAPABILITY rather than accepted as an unknown role.

[baseline]
# Always carried (gate = empty). Globs/dirs allowed; not capability-gated.
# '|' offers alternatives; a path is satisfied if ANY alternative exists.
# GitHub resolves community-health files from .github/ as readily as the root,
# and the estate's canonical location for them IS .github/ - naming only the
# root form marked every conforming repo as missing them.
paths = ["README.adoc", "EXPLAINME.adoc|docs/EXPLAINME.adoc", "LICENSE", "SECURITY.md|.github/SECURITY.md", "CONTRIBUTING.md|.github/CONTRIBUTING.md", "CODE_OF_CONDUCT.md|.github/CODE_OF_CONDUCT.md", "CHANGELOG.adoc|CHANGELOG.md", "0-AI-MANIFEST.a2ml", ".machine_readable/descriptiles/|machine-readable/descriptiles/", ".machine_readable/rsr-profile.a2ml|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/|src/interface/Abi/" = "abi"
"src/interface/generated/" = "abi"
"verification/proofs/" = "formal-proofs"
".github/workflows/e2e.yml" = "api-service"
"build/container/|container/" = "container"
"build/container/Containerfile|container/Containerfile|Containerfile" = "container"
# Guix is primary; Nix is fallback-only (criterion 1.2.1). As two separate rows
# this demanded BOTH, so every repo that correctly retired Nix failed the gate.
"guix.scm|build/guix.scm|flake.nix" = "reproducible-build"
"affinescript/" = "affinescript"
"benches/" = "benchmarks"
".github/workflows/release.yml" = "published-package"
"docs/AUDIT.adoc|AUDIT.adoc" = "governance-tier"
"docs/AFFIRMATION.adoc|AFFIRMATION.adoc" = "governance-tier"
"docs/GOVERNANCE.adoc|0-canon/GOVERNANCE.adoc|.github/GOVERNANCE.md" = "governance-tier"
"docs/MAINTAINERS.adoc|MAINTAINERS.adoc|3-practice/MAINTAINERS.adoc" = "governance-tier"

[carrier]
# Paths a SPINE (template) repo may carry WITHOUT declaring the gating
# capability, because it carries them for the repos minted from it.
#
# The spine is the one repo for which "present but not declared" is correct
# rather than drift: rsr-template-repo ships rust-ci.yml so that a Rust project
# minted from it has one, while itself carrying no Rust. Before this section the
# model had no way to express that, so it reported the template's entire reason
# for existing as VESTIGIAL - and the template's own profile [notes] had already
# argued, correctly, that declaring capabilities it lacks would be worse.
#
# Applies ONLY where the profile declares role = "spine". A minted repo carrying
# these without the capability is still drift, which is the behaviour that
# matters for the other ~300 repos.
paths = [".github/workflows/rust-ci.yml", ".github/workflows/release.yml", ".github/workflows/e2e.yml", ".github/workflows/boj-build.yml"]

[canon]
# Paths a CANON repo may carry WITHOUT declaring the gating capability, because
# they are the LAW the capabilities are defined BY, not an instance of them.
#
# The [carrier] section above exists because the spine legitimately carries
# modules whose capability it does not have: rsr-template-repo ships
# rust-ci.yml so a Rust project minted from it has one, while itself carrying
# no Rust. Before [carrier] existed the model reported the template's entire
# reason for existing as VESTIGIAL.
#
# The canon has the same shape of problem one layer up, and it has had it for
# longer. hyperpolymath/standards cannot currently be scored by the checker it
# ships (scripts/check-rsr-profile.sh exits 2 on this repo), because:
#
# * it carries the criteria SSOT, which no capability gates — the criteria
# are not an instance of `formal-proofs` or anything else, they are the
# document that DEFINES those criteria;
# * it carries docs/proofs/ (283 files) but `formal-proofs` means "contains
# mechanised proofs IN TREE, of its own code" — the canon is prose and has
# no code to prove, so it fails a criterion that is meaningless for it;
# * it carries .github/workflows/*-reusable.yml, which every other repo CALLS
# and the canon SERVES, which no capability describes.
#
# Applies ONLY where the profile declares role = "canon"
# (.machine_readable/rsr-profile.a2ml here and in the spine — the root rename
# landed 2026-09-17; machine-readable/rsr-profile.a2ml is the minority spelling
# and is still resolved).
#
# A minted repo declaring role = "canon" to evade a gate is still drift, which
# is the behaviour that matters for the other ~441 repos.
paths = [
"0-canon/rsr/rsr-criteria-v2.a2ml", # the criteria SSOT
"0-canon/rsr/archive/", # frozen prior majors
"0-canon/rsr/RSR-SPEC-v2.adoc", # prose authority
".machine_readable/template-capability-gates.toml", # this file
"0-canon/TEMPLATE-APPLICABILITY-POLICY.adoc",
"0-canon/constitution/",
".github/workflows/governance-reusable.yml", # gates other repos CALL
".machine_readable/REGISTRY.a2ml", # generated index
"docs/proofs/", # proof artefacts OF THE ESTATE
"canon.lock", # the release identity
"standards-map.toml",
]

[presets]
# OPTIONAL shorthands. A profile may declare `capabilities = [...]` directly
# instead of a preset; presets are pure sugar that expands to a capability set.
# preset name = [ base capabilities ]
# canon = the role of hyperpolymath/standards itself. A one-line shorthand for
# the honest capability set of a prose-and-tooling repo that owns the criteria.
canon = ["bash", "docs-site", "governance-tier"]
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"]
# Archetype presets (rsr-template-repo archetypes/ carry the matching name;
# see spec/SCAFFOLD-LIFECYCLE.adoc and rsr-template-repo ADR-0003).
julia-library = ["julia", "library", "docs-site", "published-package"]
wordpress-plugin = ["plugin", "web-ui"]
zotero-plugin = ["plugin"]
userscript = ["plugin"]
68 changes: 68 additions & 0 deletions bots/rhodibot/src/canon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
//! The failure mode to avoid is the quiet one: a rule set that silently shrinks
//! and reports every repository as compliant.

pub mod profile;
pub mod requirement;

use std::collections::HashSet;
Expand All @@ -56,6 +57,14 @@ use sha2::{Digest, Sha256};
/// whatever the filesystem happens to contain.
pub const VENDORED_CRITERIA: &str = include_str!("../canon/rsr-criteria-v2.a2ml");

/// The canon's gate table, copied verbatim from the same release.
///
/// This is what says which capabilities exist, what a profile's preset expands
/// to, and which module paths belong to which capability. Its pin sits beside
/// the criteria pin in `canon/pin.toml`, under `[gates]`, and
/// `scripts/check-canon-drift.sh` checks both against the canon.
pub const VENDORED_GATES: &str = include_str!("../canon/template-capability-gates.toml");

/// The pin describing which canon revision [`VENDORED_CRITERIA`] came from.
const VENDORED_PIN: &str = include_str!("../canon/pin.toml");

Expand Down Expand Up @@ -378,6 +387,25 @@ impl Canon {
#[derive(Debug, Clone, Deserialize)]
pub struct Pin {
pub source: PinSource,
pub gates: GatePin,
}

/// The gate table's pin.
///
/// Deliberately does not repeat `canon_version` and `released`: those belong to
/// the release, and a second copy of them can only ever disagree with the
/// first. `slot` names the artefact in `canon.lock` whose hash this must match.
#[derive(Debug, Clone, Deserialize)]
pub struct GatePin {
pub repo: String,
pub path: String,
pub slot: String,
pub version: String,
pub sha256: String,
/// How many capabilities the pinned table defines.
pub known: usize,
/// How many presets it defines.
pub presets: usize,
}

#[derive(Debug, Clone, Deserialize)]
Expand Down Expand Up @@ -437,6 +465,46 @@ impl Pin {
Ok(())
}

/// Check the vendored gate table against its pin.
pub fn verify_gates(&self, source: &str) -> Result<()> {
let actual = digest_of(source);
let expected = self.gates.sha256.to_lowercase();
if actual != expected {
bail!(
"vendored gate table does not match its pin: {} (slot {:?}) pins {}, this copy \
hashes {}. Either re-pin (copy {}, update [gates] sha256 in canon/pin.toml) or \
restore the copy.",
self.gates.path,
self.gates.slot,
expected,
actual,
self.gates.path
);
}
Ok(())
}

/// Check a parsed gate table against the shape this pin describes.
///
/// The vocabulary is what makes a profile's declaration checkable. If it
/// silently shrinks, profiles declaring the lost words stop parsing and
/// criteria gated on them can never apply -- so its size is pinned too.
pub fn verify_gate_counts(&self, table: &profile::GateTable) -> Result<()> {
ensure!(
table.known_count() == self.gates.known,
"pinned gate table defines {} capabilities, this copy defines {}",
self.gates.known,
table.known_count()
);
ensure!(
table.preset_count() == self.gates.presets,
"pinned gate table defines {} presets, this copy defines {}",
self.gates.presets,
table.preset_count()
);
Ok(())
}

/// Check a canon source against this pin.
pub fn verify(&self, source: &str) -> Result<()> {
let actual = digest_of(source);
Expand Down
Loading
Loading