From 846b82914cf20d1babbd970175181e5085269580 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:12:16 +0100 Subject: [PATCH 1/2] fix(gates): make the path contract resolvable, and give the table alternation The canon named module paths that no repo has had for months, and had no way to express an alternative, so conforming repos were reported as drift. VERIFIED BEFORE: running this repo's own scripts/check-rsr-profile.sh against rsr-template-repo exited 2 at the hardcoded profile path before evaluating a single gate. VERIFIED AFTER: exit 0, "scaffold matches declared capabilities". Alternation. Gate rows may now list alternatives separated by '|', satisfied if ANY exists. Without it every row was an independent AND, which is why the table demanded BOTH build/guix.scm AND flake.nix for one capability - contradicting criterion 1.2.1 ("Nix fallback only") and making reproducible-build unsatisfiable for any repo that had correctly retired Nix. Those two rows are now one. Alternation is what the table always meant; it had no way to say it. Paths corrected - five of these were already stale before the 2026-08 template reorganisation, and would have been wrong regardless of it: * governance set AUDIT/AFFIRMATION/GOVERNANCE/MAINTAINERS.adoc -> docs/ (moved by the April 2026 root cleanup) * community health SECURITY/CONTRIBUTING/CODE_OF_CONDUCT.md -> .github/ (GitHub resolves these from .github/ as readily as the root; the estate's canonical location IS .github/) * CHANGELOG.md -> CHANGELOG.adoc (AsciiDoc is the estate standard; canon's criteria contradicted canon's own practice here) * EXPLAINME.adoc -> docs/ * src/interface/Abi/ - both cases accepted and three from the reorganisation: * machine tree .machine_readable/ -> machine-readable/ * container/ -> build/container/ * build/guix.scm -> guix.scm Machine tree: BOTH names accepted. The canon itself, scaffoldia, the julia variant and ~300 minted repos still carry the dotted form, so flipping in one move would strand every one of them on the same day. Verified: a legacy-layout repo is still read via the fallback (exit 1, violations - not exit 2, setup error). Remove the legacy branch once the estate migration completes. Vocabulary: "deno" added to [capabilities].known. Criterion 4.3.1 gates on it, but it was never declared, so that criterion could never become applicable to any repo - a gate that cannot fire. New [carrier] section. A spine repo may carry capability-gated modules it does not declare, because it carries them for the repos minted from it: rsr-template-repo ships rust-ci.yml so a Rust project minted from it has one, while itself carrying no Rust. The model could not express that, so it reported the template's entire purpose as VESTIGIAL - while 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 doing the same is still drift. template_ref traceability: RSR-SPEC-v2 says "a dangling template_ref is a defect in either the spec or the template". 20 of 74 dangled. All 44 distinct refs now resolve against the template - verified by checking every one. Includes repointing npm-bun-blocker.yml and ts-blocker.yml at runtime-policy.yml, which states in its own header that it REPLACES the former. Left deliberately unresolved: criterion 2.1.7 gates MAINTAINERS.adoc as "universal" while this table gates it as "governance-tier". That contradiction is canon-internal and needs an owner ruling rather than a silent pick; noted in place. Co-Authored-By: Claude Opus 5 --- .../template-capability-gates.toml | 43 +++++++++---- .../spec/rsr-criteria-v2.a2ml | 62 +++++++++---------- scripts/check-rsr-profile.sh | 46 ++++++++++++-- 3 files changed, 105 insertions(+), 46 deletions(-) diff --git a/.machine_readable/template-capability-gates.toml b/.machine_readable/template-capability-gates.toml index 275be7a6..5ad2299b 100644 --- a/.machine_readable/template-capability-gates.toml +++ b/.machine_readable/template-capability-gates.toml @@ -19,11 +19,15 @@ policy = "TEMPLATE-APPLICABILITY-POLICY.adoc" # 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"] +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"] # "deno": criterion 4.3.1 gates on it; without it here that criterion could never become applicable [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/descriptiles/", ".machine_readable/rsr-profile.a2ml", ".well-known/", ".gitignore", "Justfile"] +# '|' 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" @@ -33,21 +37,38 @@ paths = ["README.adoc", "EXPLAINME.adoc", "LICENSE", "SECURITY.md", "CONTRIBUTIN ".github/workflows/rust-ci.yml" = "rust" "src/interface/ffi/" = "ffi" "abi.ipkg" = "abi" -"src/interface/Abi/" = "abi" +"src/interface/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" +"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" -"AUDIT.adoc" = "governance-tier" -"AFFIRMATION.adoc" = "governance-tier" -"GOVERNANCE.adoc" = "governance-tier" -"MAINTAINERS.adoc" = "governance-tier" +"docs/AUDIT.adoc|AUDIT.adoc" = "governance-tier" +"docs/AFFIRMATION.adoc|AFFIRMATION.adoc" = "governance-tier" +"docs/GOVERNANCE.adoc|GOVERNANCE.adoc|.github/GOVERNANCE.md" = "governance-tier" +"docs/MAINTAINERS.adoc|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"] [presets] # OPTIONAL shorthands. A profile may declare `capabilities = [...]` directly diff --git a/rhodium-standard-repositories/spec/rsr-criteria-v2.a2ml b/rhodium-standard-repositories/spec/rsr-criteria-v2.a2ml index 7238658e..b5102502 100644 --- a/rhodium-standard-repositories/spec/rsr-criteria-v2.a2ml +++ b/rhodium-standard-repositories/spec/rsr-criteria-v2.a2ml @@ -32,7 +32,7 @@ model = "semver-with-errata" rule-major = "A MAJOR bump is an era change: it MAY remove or redefine criteria and MAY move a criterion between tiers. Cutting a major freezes the prior major's rsr-criteria as an immutable, hash-pinned artefact under archive/." rule-minor = "A MINOR bump MAY ADD criteria or ADD a capability gate, but MUST NOT make a previously-conforming repo non-conforming at the same tier. Additive only." rule-patch = "A PATCH bump is editorial (wording, detection-rule id corrections, template_ref fixes) with no criteria-set change." -freeze-mechanism = "A released major is frozen by pinning this file's byte-hash in .machine_readable/REGISTRY.a2ml (or the spec's own VERSIONS ledger), NOT by a prose 'FROZEN' banner. A freeze guard in CI (see [oracle].freeze-guard) fails any PR that mutates a frozen major's criteria file." +freeze-mechanism = "A released major is frozen by pinning this file's byte-hash in machine-readable/REGISTRY.a2ml (or the spec's own VERSIONS ledger), NOT by a prose 'FROZEN' banner. A freeze guard in CI (see [oracle].freeze-guard) fails any PR that mutates a frozen major's criteria file." errata = "Corrections that do not change criteria semantics are recorded in RSR-SPEC-v2.adoc Appendix E (Errata) and shipped as PATCH bumps." [tiers] @@ -46,12 +46,12 @@ scale = "X F E D C B A maps onto the readiness-grade scale; RSR tiers are the re [scoring] # The capability-gated model (replaces v1.0's flat 'every repo needs all 11 categories'). -applicable-set = "A criterion is APPLICABLE to a repo iff its `gate` is `universal` OR the repo's .machine_readable/rsr-profile.a2ml declares the gating capability (per TEMPLATE-APPLICABILITY-POLICY.adoc). Non-applicable criteria are scored `na` and excluded from the denominator." +applicable-set = "A criterion is APPLICABLE to a repo iff its `gate` is `universal` OR the repo's machine-readable/rsr-profile.a2ml declares the gating capability (per TEMPLATE-APPLICABILITY-POLICY.adoc). Non-applicable criteria are scored `na` and excluded from the denominator." denominator = "sum of weights of APPLICABLE criteria" numerator = "sum of weights of APPLICABLE criteria whose `detect` returns pass" partial = "A criterion MAY return `partial` (e.g. some but not all files present); partial contributes half its weight and is reported explicitly." na-honesty = "A repo MUST NOT reach a tier by declaring away (via rsr-profile) a capability it actually has. hypatia cross-checks declared capabilities against detected ones (structural_drift) and flags under-declaration as :review." -report = "The oracle writes a per-repo scorecard to verisim-data (.machine_readable/scorecards/.scorecard.a2ml shape) with per-criterion verdicts, the applicable set, and the resulting tier." +report = "The oracle writes a per-repo scorecard to verisim-data (machine-readable/scorecards/.scorecard.a2ml shape) with per-criterion verdicts, the applicable set, and the resulting tier." [oracle] # ONE normative checker. RSR v1.0 shipped five divergent checkers; v2.0 designates @@ -90,8 +90,8 @@ criteria = [ { id = "1.1.3", name = "no-makefile", desc = "No Makefile (Mustfile/justfile only)", tier = "bronze", gate = "universal", detect = "cicd_rules/makefile_detected", template_ref = "-" }, { id = "1.1.4", name = "editorconfig", desc = ".editorconfig present", tier = "silver", gate = "universal", detect = "manual", template_ref = ".editorconfig" }, { id = "1.2.1", name = "guix-primary", desc = "Guix manifest (guix.scm) as primary reproducible build; Nix fallback only", tier = "silver", gate = "reproducible-build", detect = "build_system_rules/guix_primary", template_ref = "build/" }, - { id = "1.2.2", name = "git-hooks", desc = ".pre-commit-config.yaml with real hooks", tier = "silver", gate = "universal", detect = "manual", template_ref = ".pre-commit-config.yaml" }, - { id = "1.2.3", name = "container-rootless", desc = "Container config uses rootless Podman + Chainguard/Wolfi base", tier = "gold", gate = "container", detect = "cicd_rules/containerfile_base", template_ref = "container/" }, + { id = "1.2.2", name = "git-hooks", desc = ".pre-commit-config.yaml with real hooks", tier = "silver", gate = "universal", detect = "manual", template_ref = "ci/.pre-commit-config.yaml" }, + { id = "1.2.3", name = "container-rootless", desc = "Container config uses rootless Podman + Chainguard/Wolfi base", tier = "gold", gate = "container", detect = "cicd_rules/containerfile_base", template_ref = "build/container/" }, { id = "1.2.4", name = "tool-versions", desc = ".tool-versions pins toolchain versions", tier = "silver", gate = "universal", detect = "manual", template_ref = ".tool-versions" }, ] @@ -103,13 +103,13 @@ weight = 12 criteria = [ { id = "2.1.1", name = "readme-adoc", desc = "README.adoc present (.adoc primary per estate doc policy)", tier = "bronze", gate = "universal", detect = "root_hygiene/readme", template_ref = "README.adoc" }, { id = "2.1.2", name = "license-file", desc = "LICENSE present + LICENSES/ REUSE texts (NOT LICENSE.txt)", tier = "bronze", gate = "universal", detect = "root_hygiene/license", template_ref = "LICENSE, LICENSES/" }, - { id = "2.1.3", name = "security-md", desc = "SECURITY.md with a vulnerability-disclosure policy", tier = "bronze", gate = "universal", detect = "root_hygiene/security", template_ref = "SECURITY.md" }, - { id = "2.1.4", name = "coc-md", desc = "CODE_OF_CONDUCT.md", tier = "silver", gate = "universal", detect = "root_hygiene/coc", template_ref = "CODE_OF_CONDUCT.md" }, - { id = "2.1.5", name = "contributing-md", desc = "CONTRIBUTING.md", tier = "silver", gate = "universal", detect = "root_hygiene/contributing", template_ref = "CONTRIBUTING.md" }, - { id = "2.1.6", name = "changelog", desc = "CHANGELOG.md (Keep a Changelog)", tier = "silver", gate = "universal", detect = "root_hygiene/changelog", template_ref = "CHANGELOG.md" }, - { id = "2.1.7", name = "maintainers", desc = "MAINTAINERS.adoc", tier = "silver", gate = "universal", detect = "root_hygiene/maintainers", template_ref = "MAINTAINERS.adoc" }, - { id = "2.1.8", name = "governance", desc = "GOVERNANCE.adoc", tier = "gold", gate = "governance-tier", detect = "root_hygiene/governance", template_ref = "GOVERNANCE.adoc" }, - { id = "2.1.9", name = "funding", desc = "FUNDING.yml / .github/FUNDING.yml", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = "FUNDING.yml" }, + { id = "2.1.3", name = "security-md", desc = "SECURITY.md with a vulnerability-disclosure policy", tier = "bronze", gate = "universal", detect = "root_hygiene/security", template_ref = ".github/SECURITY.md" }, + { id = "2.1.4", name = "coc-md", desc = "CODE_OF_CONDUCT.md", tier = "silver", gate = "universal", detect = "root_hygiene/coc", template_ref = ".github/CODE_OF_CONDUCT.md" }, + { id = "2.1.5", name = "contributing-md", desc = "CONTRIBUTING.md", tier = "silver", gate = "universal", detect = "root_hygiene/contributing", template_ref = ".github/CONTRIBUTING.md" }, + { id = "2.1.6", name = "changelog", desc = "CHANGELOG.md (Keep a Changelog)", tier = "silver", gate = "universal", detect = "root_hygiene/changelog", template_ref = "CHANGELOG.adoc" }, + { id = "2.1.7", name = "maintainers", desc = "MAINTAINERS.adoc", tier = "silver", gate = "universal", detect = "root_hygiene/maintainers", template_ref = "docs/MAINTAINERS.adoc" }, + { id = "2.1.8", name = "governance", desc = "GOVERNANCE.adoc", tier = "gold", gate = "governance-tier", detect = "root_hygiene/governance", template_ref = "docs/GOVERNANCE.adoc" }, + { id = "2.1.9", name = "funding", desc = "FUNDING.yml / .github/FUNDING.yml", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = ".github/FUNDING.yml" }, { id = "2.1.10", name = "gitignore", desc = ".gitignore and .gitattributes present", tier = "bronze", gate = "universal", detect = "manual", template_ref = ".gitignore, .gitattributes" }, { id = "2.2.1", name = "wellknown-core", desc = ".well-known/{security.txt,ai.txt,humans.txt}", tier = "silver", gate = "universal", detect = "structural_drift/SD020", template_ref = ".well-known/" }, { id = "2.2.2", name = "wellknown-ext", desc = ".well-known/{consent-required.txt,provenance.json}", tier = "gold", gate = "web-ui", detect = "manual", template_ref = "-" }, @@ -122,17 +122,17 @@ key = "machine-readable-substrate" name = "Machine-Readable Substrate (Descriptiles)" weight = 14 criteria = [ - { id = "3.1.1", name = "descriptiles-dir", desc = ".machine_readable/descriptiles/ present (NOT 6a2/, which is deprecated 2026-06-30)", tier = "gold", gate = "universal", detect = "structural_drift/SD003", template_ref = ".machine_readable/descriptiles/" }, - { id = "3.1.2", name = "state", desc = "STATE.a2ml — current state/progress (valid record dialect)", tier = "gold", gate = "universal", detect = "structural_drift/SD004", template_ref = ".machine_readable/descriptiles/STATE.a2ml" }, - { id = "3.1.3", name = "meta", desc = "META.a2ml — ADRs / constitutional authority", tier = "gold", gate = "universal", detect = "structural_drift/SD005", template_ref = ".machine_readable/descriptiles/META.a2ml" }, - { id = "3.1.4", name = "ecosystem", desc = "ECOSYSTEM.a2ml — estate position + what-this-is-not", tier = "gold", gate = "universal", detect = "structural_drift/SD006", template_ref = ".machine_readable/descriptiles/ECOSYSTEM.a2ml" }, - { id = "3.1.5", name = "agentic", desc = "AGENTIC.a2ml — agent permissions / risk gating", tier = "gold", gate = "universal", detect = "structural_drift/SD007", template_ref = ".machine_readable/descriptiles/AGENTIC.a2ml" }, - { id = "3.1.6", name = "neurosym", desc = "NEUROSYM.a2ml — proof obligations / meaning of ops", tier = "gold", gate = "universal", detect = "structural_drift/SD008", template_ref = ".machine_readable/descriptiles/NEUROSYM.a2ml" }, - { id = "3.1.7", name = "playbook", desc = "PLAYBOOK.a2ml — operational runbook", tier = "gold", gate = "universal", detect = "structural_drift/SD009", template_ref = ".machine_readable/descriptiles/PLAYBOOK.a2ml" }, - { id = "3.1.8", name = "anchor", desc = "ANCHOR.a2ml — semantic authority + golden path", tier = "gold", gate = "universal", detect = "structural_drift/SD010", template_ref = ".machine_readable/descriptiles/ANCHOR.a2ml" }, - { id = "3.1.9", name = "clade", desc = "CLADE.a2ml — identity/lineage (registers into gv-clade-index)", tier = "gold", gate = "governance-tier", detect = "structural_drift/SD011", template_ref = ".machine_readable/descriptiles/CLADE.a2ml" }, + { id = "3.1.1", name = "descriptiles-dir", desc = "machine-readable/descriptiles/ present (NOT 6a2/, which is deprecated 2026-06-30)", tier = "gold", gate = "universal", detect = "structural_drift/SD003", template_ref = "machine-readable/descriptiles/" }, + { id = "3.1.2", name = "state", desc = "STATE.a2ml — current state/progress (valid record dialect)", tier = "gold", gate = "universal", detect = "structural_drift/SD004", template_ref = "machine-readable/descriptiles/STATE.a2ml" }, + { id = "3.1.3", name = "meta", desc = "META.a2ml — ADRs / constitutional authority", tier = "gold", gate = "universal", detect = "structural_drift/SD005", template_ref = "machine-readable/descriptiles/META.a2ml" }, + { id = "3.1.4", name = "ecosystem", desc = "ECOSYSTEM.a2ml — estate position + what-this-is-not", tier = "gold", gate = "universal", detect = "structural_drift/SD006", template_ref = "machine-readable/descriptiles/ECOSYSTEM.a2ml" }, + { id = "3.1.5", name = "agentic", desc = "AGENTIC.a2ml — agent permissions / risk gating", tier = "gold", gate = "universal", detect = "structural_drift/SD007", template_ref = "machine-readable/descriptiles/AGENTIC.a2ml" }, + { id = "3.1.6", name = "neurosym", desc = "NEUROSYM.a2ml — proof obligations / meaning of ops", tier = "gold", gate = "universal", detect = "structural_drift/SD008", template_ref = "machine-readable/descriptiles/NEUROSYM.a2ml" }, + { id = "3.1.7", name = "playbook", desc = "PLAYBOOK.a2ml — operational runbook", tier = "gold", gate = "universal", detect = "structural_drift/SD009", template_ref = "machine-readable/descriptiles/PLAYBOOK.a2ml" }, + { id = "3.1.8", name = "anchor", desc = "ANCHOR.a2ml — semantic authority + golden path", tier = "gold", gate = "universal", detect = "structural_drift/SD010", template_ref = "machine-readable/descriptiles/anchors/ANCHOR.a2ml" }, + { id = "3.1.9", name = "clade", desc = "CLADE.a2ml — identity/lineage (registers into gv-clade-index)", tier = "gold", gate = "governance-tier", detect = "structural_drift/SD011", template_ref = "machine-readable/descriptiles/CLADE.a2ml" }, { id = "3.2.1", name = "a2ml-valid", desc = "All .a2ml files parse + validate against their record-dialect profile", tier = "gold", gate = "universal", detect = "rsr-conformance/a2ml_valid", template_ref = "-" }, - { id = "3.2.2", name = "rsr-profile", desc = ".machine_readable/rsr-profile.a2ml declares capabilities (drives applicable set)", tier = "silver", gate = "universal", detect = "rsr-conformance/profile_present", template_ref = ".machine_readable/rsr-profile.a2ml" }, + { id = "3.2.2", name = "rsr-profile", desc = "machine-readable/rsr-profile.a2ml declares capabilities (drives applicable set)", tier = "silver", gate = "universal", detect = "rsr-conformance/profile_present", template_ref = "machine-readable/rsr-profile.a2ml" }, ] [[category]] @@ -150,7 +150,7 @@ criteria = [ { id = "4.2.2", name = "scorecard-9", desc = "OpenSSF Scorecard >= 9.0", tier = "gold", gate = "universal", detect = "scorecard_compliance/aggregate", template_ref = ".github/workflows/scorecard.yml" }, { id = "4.3.1", name = "deno-perms", desc = "Explicit Deno permission flags (no bare --allow-all)", tier = "silver", gate = "deno", detect = "cicd_rules/deno_permissions", template_ref = "-" }, { id = "4.3.2", name = "sbom", desc = "SBOM generated on release", tier = "gold", gate = "published-package", detect = "supply_chain/sbom", template_ref = ".github/workflows/release.yml" }, - { id = "4.3.3", name = "rootless", desc = "Containers run rootless", tier = "gold", gate = "container", detect = "cicd_rules/rootless_container", template_ref = "container/" }, + { id = "4.3.3", name = "rootless", desc = "Containers run rootless", tier = "gold", gate = "container", detect = "cicd_rules/rootless_container", template_ref = "build/container/" }, ] [[category]] @@ -160,11 +160,11 @@ name = "Language Policy Conformance" weight = 12 criteria = [ { id = "5.1.1", name = "no-python", desc = "No Python (fully banned)", tier = "bronze", gate = "universal", detect = "cicd_rules/python_detected", template_ref = ".github/workflows/estate-rules.yml" }, - { id = "5.1.2", name = "no-typescript", desc = "No new TypeScript outside approved carve-outs", tier = "bronze", gate = "universal", detect = "cicd_rules/typescript_detected", template_ref = ".github/workflows/ts-blocker.yml" }, + { id = "5.1.2", name = "no-typescript", desc = "No new TypeScript outside approved carve-outs", tier = "bronze", gate = "universal", detect = "cicd_rules/typescript_detected", template_ref = ".github/workflows/runtime-policy.yml" }, { id = "5.1.3", name = "no-rescript", desc = "No new ReScript outside approved carve-outs", tier = "bronze", gate = "universal", detect = "cicd_rules/rescript_detected", template_ref = "-" }, { id = "5.1.4", name = "no-vlang", desc = "No V-lang (migration completed 2026-05-28)", tier = "bronze", gate = "universal", detect = "cicd_rules/vlang_detected", template_ref = "-" }, { id = "5.1.5", name = "no-go", desc = "No Go (use Rust/SPARK)", tier = "bronze", gate = "universal", detect = "cicd_rules/go_detected", template_ref = "-" }, - { id = "5.1.6", name = "no-node-npm", desc = "No Node/npm/bun runtime deps (use Deno)", tier = "bronze", gate = "universal", detect = "cicd_rules/nodejs_detected", template_ref = ".github/workflows/npm-bun-blocker.yml" }, + { id = "5.1.6", name = "no-node-npm", desc = "No Node/npm/bun runtime deps (use Deno)", tier = "bronze", gate = "universal", detect = "cicd_rules/nodejs_detected", template_ref = ".github/workflows/runtime-policy.yml" }, { id = "5.2.1", name = "spark-ready", desc = "Rust projects designed to admit SPARK/Ada modules", tier = "rhodium", gate = "rust", detect = "manual", template_ref = "-" }, { id = "5.2.2", name = "proofs-clean", desc = "No believe_me / sorry / Admitted in load-bearing proofs", tier = "gold", gate = "formal-proofs", detect = "proof_obligation/no_holes", template_ref = "verification/" }, ] @@ -192,7 +192,7 @@ criteria = [ { id = "7.1.1", name = "licence-classified", desc = "Licence matches the estate 5-way classification (detection FLAG-ONLY / :review; never auto-edited)", tier = "bronze", gate = "universal", detect = "cicd_rules/license_finding", template_ref = "LICENSE" }, { id = "7.1.2", name = "reuse-compliant", desc = "REUSE-style LICENSES/ dir with full texts", tier = "silver", gate = "universal", detect = "cicd_rules/reuse_lint", template_ref = "LICENSES/" }, { id = "7.1.3", name = "spdx-audit", desc = "SPDX identifiers resolve to real licences (no scrambled ids)", tier = "silver", gate = "universal", detect = "cicd_rules/spdx_valid", template_ref = "-" }, - { id = "7.2.1", name = "dco", desc = "DCO sign-off or CLA on contributions", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = "CONTRIBUTING.md" }, + { id = "7.2.1", name = "dco", desc = "DCO sign-off or CLA on contributions", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = ".github/CONTRIBUTING.md" }, ] [[category]] @@ -201,7 +201,7 @@ key = "lifecycle-management" name = "Lifecycle Management" weight = 4 criteria = [ - { id = "8.1.1", name = "semver", desc = "Semantic versioning for releases", tier = "silver", gate = "published-package", detect = "manual", template_ref = "CHANGELOG.md" }, + { id = "8.1.1", name = "semver", desc = "Semantic versioning for releases", tier = "silver", gate = "published-package", detect = "manual", template_ref = "CHANGELOG.adoc" }, { id = "8.1.2", name = "pinned-deps", desc = "Dependency versions pinned/locked", tier = "silver", gate = "universal", detect = "supply_chain/unpinned_dep", template_ref = "-" }, { id = "8.1.3", name = "deprecation", desc = "Deprecation + sunset policy documented", tier = "gold", gate = "published-package", detect = "manual", template_ref = "-" }, { id = "8.1.4", name = "no-scaffold-stub", desc = "No scaffold-stub guix.scm (placeholders / empty inputs / source #f)", tier = "silver", gate = "reproducible-build", detect = "structural_drift/SD021", template_ref = "-" }, @@ -225,9 +225,9 @@ key = "community-governance" name = "Community & Governance" weight = 2 criteria = [ - { id = "10.1.1", name = "governance-model", desc = "Governance model documented (not just a stub)", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = "GOVERNANCE.adoc" }, - { id = "10.1.2", name = "succession", desc = "Succession / bus-factor plan", tier = "rhodium", gate = "governance-tier", detect = "manual", template_ref = "MAINTAINERS.adoc" }, - { id = "10.1.3", name = "affirmation", desc = "AFFIRMATION.adoc (no-overclaim ethos) present", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = "AFFIRMATION.adoc" }, + { id = "10.1.1", name = "governance-model", desc = "Governance model documented (not just a stub)", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = "docs/GOVERNANCE.adoc" }, + { id = "10.1.2", name = "succession", desc = "Succession / bus-factor plan", tier = "rhodium", gate = "governance-tier", detect = "manual", template_ref = "docs/MAINTAINERS.adoc" }, + { id = "10.1.3", name = "affirmation", desc = "AFFIRMATION.adoc (no-overclaim ethos) present", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = "docs/AFFIRMATION.adoc" }, ] [[category]] @@ -236,7 +236,7 @@ key = "accountability-provenance" name = "Accountability & Provenance" weight = 1 criteria = [ - { id = "11.1.1", name = "audit-doc", desc = "AUDIT.adoc — audit trail / evidence ledger", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = "AUDIT.adoc" }, + { id = "11.1.1", name = "audit-doc", desc = "AUDIT.adoc — audit trail / evidence ledger", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = "docs/AUDIT.adoc" }, { id = "11.1.2", name = "provenance-json", desc = ".well-known/provenance.json provenance chain", tier = "rhodium", gate = "web-ui", detect = "manual", template_ref = "-" }, { id = "11.1.3", name = "signed-commits", desc = "Commits signed (advisory in agent envs; see estate note)", tier = "rhodium", gate = "universal", detect = "git_state/unsigned_commit", template_ref = "-" }, ] diff --git a/scripts/check-rsr-profile.sh b/scripts/check-rsr-profile.sh index 8da883ed..c3d0fb55 100755 --- a/scripts/check-rsr-profile.sh +++ b/scripts/check-rsr-profile.sh @@ -22,7 +22,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" GATES="${RSR_GATES:-$SCRIPT_DIR/../.machine_readable/template-capability-gates.toml}" REPO="${1:-.}" LIVE_REPOSITORY="${2:-${RSR_REPOSITORY:-}}" -PROFILE="$REPO/.machine_readable/rsr-profile.a2ml" +# The machine tree is `machine-readable/` (canonical since 2026-08). The dotted +# `.machine_readable/` form is the LEGACY location and is still accepted, because +# the canon, scaffoldia, the julia variant and ~300 minted repos all still carry +# it; flipping in one move would strand every one of them on the same day. +# Remove the legacy branch once the estate migration completes. +PROFILE="$REPO/machine-readable/rsr-profile.a2ml" +[ -f "$PROFILE" ] || 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; } @@ -73,8 +79,23 @@ fi has_cap() { printf '%s\n' "$EFFECTIVE" | grep -qx "$1"; } # Presence of a module path: file, dir/ (trailing slash), or glob (contains *). +# +# A row may list ALTERNATIVES separated by '|', satisfied if ANY of them exists. +# Without this every row was an independent AND, which is why the table demanded +# both build/guix.scm AND flake.nix for one capability - contradicting criterion +# 1.2.1 ("Nix fallback only") and making `reproducible-build` unsatisfiable for +# any repo that had correctly retired Nix. Alternation is what that table always +# meant; it just had no way to say it. present() { - local key="$1" + local key="$1" alt + if [[ "$key" == *"|"* ]]; then + local -a alts + IFS='|' read -r -a alts <<< "$key" + for alt in "${alts[@]}"; do + present "$alt" && return 0 + done + return 1 + fi case "$key" in */) [ -d "$REPO/${key%/}" ] ;; *'*'*) ( shopt -s globstar nullglob; compgen -G "$REPO/$key" >/dev/null ) ;; @@ -87,6 +108,17 @@ echo "profile: ${PRESET:+preset=$PRESET }${DIRECT:+direct-capabilities}" echo "effective capabilities: $(printf '%s\n' "$EFFECTIVE" | paste -sd ' ' -)" echo +# A spine (template) repo legitimately carries capability-gated modules it does +# not declare - it ships them for the repos minted from it. [carrier] lists them. +ROLE="$(section rsr-profile "$PROFILE" | quoted_on_key role | sed -n 1p || true)" +CARRIER="" +if [ "$ROLE" = "spine" ]; then + CARRIER="$(section carrier "$GATES" | quoted_on_key paths || true)" + echo "role: spine - [carrier] paths exempt from VESTIGIAL" + echo +fi +is_carrier() { [ -n "$CARRIER" ] && printf '%s\n' "$CARRIER" | grep -qx "$1"; } + fail=0 while IFS= read -r line; do path="$(printf '%s' "$line" | grep -oE '"[^"]+"' | sed -n 1p | tr -d '"')" @@ -95,7 +127,13 @@ while IFS= read -r line; do if has_cap "$cap"; then present "$path" || { echo " MISSING ($cap): $path"; fail=1; } else - present "$path" && { echo " VESTIGIAL (no '$cap' capability): $path"; fail=1; } + if present "$path"; then + if is_carrier "$path"; then + echo " carried for downstream (no '$cap' capability, spine): $path" + else + echo " VESTIGIAL (no '$cap' capability): $path"; fail=1 + fi + fi fi done < <(section gates "$GATES") @@ -105,7 +143,7 @@ if [ "$fail" -ne 0 ]; then 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 + * declare the capability in machine-readable/rsr-profile.a2ml (with a [rationale] line), OR * add the missing module. MSG From b04e8c35535c87cf8b7f325f1d851837e7832a7d Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:32:11 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix(gates):=20address=20review=20=E2=80=94?= =?UTF-8?q?=20role=20lookup,=20carrier=20alternation,=20changelog=20desc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review findings, all verified against the code before acting, all real and all gaps in the previous commit rather than in the reviewers. 1. is_carrier() exact-matched the whole gate row (Codacy). Gate rows may now be alternations ("a|b"), while a [carrier] entry names one path, so an alternation row whose alternatives are carried was silently NOT exempted. Latent today (no carrier path is currently an alternation) but a trap for whoever adds one. Now tests each alternative in turn. Proved with a control: with the fix, exit=0 and 0 VESTIGIAL findings; reverting is_carrier to the old form gives exit=1 and 1 VESTIGIAL on the same fixture. 2. The ROLE lookup re-read [rsr-profile] directly (CodeRabbit), bypassing the $PBODY that already falls back from [rsr-profile] to [profile] — the two spellings this script explicitly supports. A repo using [profile] therefore lost its role, and with it the carrier exemption. Now reads $PBODY. Verified on a fixture using the [profile] spelling: role is read and the exemption applies. 3. Criterion 2.1.6 desc said "CHANGELOG.md" while the previous commit changed its template_ref to "CHANGELOG.adoc" (CodeRabbit) — an inconsistency I introduced. Both now name the same thing, with .md still accepted. Still deliberately NOT resolved: criterion 2.1.7 gates MAINTAINERS.adoc as "universal" while the gates table gates it as "governance-tier". Codacy is right that this can produce a validation catch-22, but which side is correct is a canon-governance decision for the owner, not something to settle silently in a path-contract PR. It remains flagged in place. Verified: the checker still exits 0 against rsr-template-repo. Co-Authored-By: Claude Opus 5 --- .../spec/rsr-criteria-v2.a2ml | 2 +- scripts/check-rsr-profile.sh | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/rhodium-standard-repositories/spec/rsr-criteria-v2.a2ml b/rhodium-standard-repositories/spec/rsr-criteria-v2.a2ml index b5102502..b56a8c0a 100644 --- a/rhodium-standard-repositories/spec/rsr-criteria-v2.a2ml +++ b/rhodium-standard-repositories/spec/rsr-criteria-v2.a2ml @@ -106,7 +106,7 @@ criteria = [ { id = "2.1.3", name = "security-md", desc = "SECURITY.md with a vulnerability-disclosure policy", tier = "bronze", gate = "universal", detect = "root_hygiene/security", template_ref = ".github/SECURITY.md" }, { id = "2.1.4", name = "coc-md", desc = "CODE_OF_CONDUCT.md", tier = "silver", gate = "universal", detect = "root_hygiene/coc", template_ref = ".github/CODE_OF_CONDUCT.md" }, { id = "2.1.5", name = "contributing-md", desc = "CONTRIBUTING.md", tier = "silver", gate = "universal", detect = "root_hygiene/contributing", template_ref = ".github/CONTRIBUTING.md" }, - { id = "2.1.6", name = "changelog", desc = "CHANGELOG.md (Keep a Changelog)", tier = "silver", gate = "universal", detect = "root_hygiene/changelog", template_ref = "CHANGELOG.adoc" }, + { id = "2.1.6", name = "changelog", desc = "CHANGELOG.adoc, or .md (Keep a Changelog)", tier = "silver", gate = "universal", detect = "root_hygiene/changelog", template_ref = "CHANGELOG.adoc" }, { id = "2.1.7", name = "maintainers", desc = "MAINTAINERS.adoc", tier = "silver", gate = "universal", detect = "root_hygiene/maintainers", template_ref = "docs/MAINTAINERS.adoc" }, { id = "2.1.8", name = "governance", desc = "GOVERNANCE.adoc", tier = "gold", gate = "governance-tier", detect = "root_hygiene/governance", template_ref = "docs/GOVERNANCE.adoc" }, { id = "2.1.9", name = "funding", desc = "FUNDING.yml / .github/FUNDING.yml", tier = "gold", gate = "governance-tier", detect = "manual", template_ref = ".github/FUNDING.yml" }, diff --git a/scripts/check-rsr-profile.sh b/scripts/check-rsr-profile.sh index c3d0fb55..7baabb9c 100755 --- a/scripts/check-rsr-profile.sh +++ b/scripts/check-rsr-profile.sh @@ -110,14 +110,29 @@ echo # A spine (template) repo legitimately carries capability-gated modules it does # not declare - it ships them for the repos minted from it. [carrier] lists them. -ROLE="$(section rsr-profile "$PROFILE" | quoted_on_key role | sed -n 1p || true)" +# Read role from $PBODY, which already falls back from [rsr-profile] to +# [profile]. Re-reading [rsr-profile] directly lost the role for any repo +# using the [profile] spelling the script otherwise supports. +ROLE="$(printf '%s\n' "$PBODY" | quoted_on_key role | sed -n 1p || true)" CARRIER="" if [ "$ROLE" = "spine" ]; then CARRIER="$(section carrier "$GATES" | quoted_on_key paths || true)" echo "role: spine - [carrier] paths exempt from VESTIGIAL" echo fi -is_carrier() { [ -n "$CARRIER" ] && printf '%s\n' "$CARRIER" | grep -qx "$1"; } +# A gate row may be an alternation ("a|b"); a carrier entry names ONE path. +# Exact-matching the whole row would silently fail to exempt an alternation row +# whose alternatives are carried, so test each alternative in turn. +is_carrier() { + [ -n "$CARRIER" ] || return 1 + local alt + local -a alts + IFS='|' read -r -a alts <<< "$1" + for alt in "${alts[@]}"; do + printf '%s\n' "$CARRIER" | grep -qx "$alt" && return 0 + done + return 1 +} fail=0 while IFS= read -r line; do