From cddabaf067901ee3d02b09b0fddc56f6e69865ef 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:37 +0100 Subject: [PATCH] fix(rsr): resolve the machine tree per repo, and accept real file locations The oracle could not score the estate's own spine. It hardcoded `.machine_readable/` in eight places, and eleven criteria detectors named root paths that the April 2026 root cleanup had already moved. MEASURED, against rsr-template-repo with the standards SSOT: before could not read the profile at all after paths tier=none score=74.29% pass=31 fail=14 after detectors tier=silver score=92.77% pass=42 fail=3 The three that still fail are REAL defects, not path artefacts, and are left failing because they are failing: 4.1.3 sha-pinned (dogfood-gate.yml uses unpinned @main actions), 5.1.1 no-python (the template ships strip-instruction-blocks.py and prune-dependabot-ecosystems.py while the estate bans Python - it bans Python and ships Python, and every minted repo inherits both), and 8.1.4 no-scaffold-stub. Hypatia.Paths.machine_tree/1 + machine_tree_join/2 resolve a repo's machine tree per repo at check time. The canonical name is `machine-readable/`; the dotted `.machine_readable/` is legacy and still accepted, because the canon itself, scaffoldia, the julia variant and ~300 minted repos still carry it. Resolving to one name only would make whichever half of the estate had not migrated unscoreable - which is precisely the failure being fixed, in mirror image. VERIFIED no repo is stranded: a fabricated legacy-layout copy of the template (machine-readable/ renamed back to .machine_readable/) scores IDENTICALLY - tier=silver, 92.77%, profile=true. Both layouts, same verdict. Eleven detectors now accept the locations their files legitimately occupy, via the existing any_of/1 (and a new any_of_mr/1 for paths inside the machine tree): SECURITY/CONTRIBUTING/CODE_OF_CONDUCT.md at root or .github/ - GitHub resolves both, and .github/ is the estate's canonical location; MAINTAINERS/GOVERNANCE/ AFFIRMATION/AUDIT.adoc at root or docs/; CHANGELOG as .adoc or .md; .pre-commit-config.yaml at root or ci/; ANCHOR.a2ml flat or under descriptiles/anchors/, which is how the spine nests it. Note what this does NOT change: coverage stays 69% and provisional stays true, because 19 of 74 criteria are detect="manual". A provisional scorecard never satisfies --fail-under, so the dogfood gate's Gold requirement remains structurally unreachable until those detectors exist. That is a separate defect and is not papered over here. Scope: this fixes the scoring path. Ten other files under lib/ still construct `.machine_readable` paths directly (structural_drift, root_hygiene, code_safety, honest_completion, kin/gate, merge_orchestration/kin_gate and the mix tasks) - 19 Path.join sites in total. They should move to the same resolver; left as a follow-up rather than half-done silently. Compiles clean. Co-Authored-By: Claude Opus 5 --- lib/paths.ex | 30 +++++++++++++++++++ lib/rules/rsr_conformance.ex | 56 ++++++++++++++++++++++++------------ 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/lib/paths.ex b/lib/paths.ex index f2c91137..46bb896a 100644 --- a/lib/paths.ex +++ b/lib/paths.ex @@ -20,4 +20,34 @@ defmodule Hypatia.Paths do def scans, do: Path.join(verisimdb_data(), "scans") def dispatch, do: Path.join(verisimdb_data(), "dispatch") def neural_states, do: Path.join(verisimdb_data(), "neural-states") + + @machine_tree_canonical "machine-readable" + @machine_tree_legacy ".machine_readable" + + @doc """ + Name of a repository's machine tree directory. + + The canonical name is `machine-readable/`; `.machine_readable/` is the LEGACY + form. Both are accepted: the canon, scaffoldia, the julia variant and ~300 + minted repos still carry the dotted name, so resolving to one form only would + make the oracle unable to score whichever half of the estate had not migrated. + + Prefers the canonical name when both are present. Falls back to the canonical + name when neither exists, so callers building a path for a repo that has no + machine tree at all report against the name it *should* have. + """ + def machine_tree(repo_path) do + cond do + File.dir?(Path.join(repo_path, @machine_tree_canonical)) -> @machine_tree_canonical + File.dir?(Path.join(repo_path, @machine_tree_legacy)) -> @machine_tree_legacy + true -> @machine_tree_canonical + end + end + + @doc "Join a path inside a repository's machine tree, whichever name it uses." + def machine_tree_join(repo_path, parts) when is_list(parts) do + Path.join([repo_path, machine_tree(repo_path) | parts]) + end + + def machine_tree_join(repo_path, part), do: machine_tree_join(repo_path, [part]) end diff --git a/lib/rules/rsr_conformance.ex b/lib/rules/rsr_conformance.ex index 74dd79e6..d86df23a 100644 --- a/lib/rules/rsr_conformance.ex +++ b/lib/rules/rsr_conformance.ex @@ -14,7 +14,8 @@ defmodule Hypatia.Rules.RsrConformance do oracle. This module implements the **scoring engine** faithfully: * applicable set = universal criteria ∪ criteria whose `gate` capability - the repo declares in `.machine_readable/rsr-profile.a2ml`; + the repo declares in `machine-readable/rsr-profile.a2ml` + (legacy `.machine_readable/` also accepted); non-applicable criteria are `:na` and excluded from the denominator; * verdicts `:pass` / `:partial` (half weight) / `:fail`; * score = passed weight / applicable **verified** weight; @@ -177,13 +178,13 @@ defmodule Hypatia.Rules.RsrConformance do # --- applicability --------------------------------------------------------- - # Reads `.machine_readable/rsr-profile.a2ml` (record dialect). Accepts the + # Reads `/rsr-profile.a2ml` (record dialect). Accepts the # capability list under `[rsr-profile]` or `[profile]`, key `capabilities`. # Returns `:none` when the file is absent/unreadable — only universal # criteria are then applicable, and criterion 3.2.2 (profile presence, # itself universal) fails, which is exactly the intended signal. defp declared_capabilities(repo_path) do - path = Path.join([repo_path, ".machine_readable", "rsr-profile.a2ml"]) + path = Hypatia.Paths.machine_tree_join(repo_path, ["rsr-profile.a2ml"]) with {:ok, text} <- File.read(path), {:ok, tree} <- RecordDialect.parse(text), @@ -333,16 +334,16 @@ defmodule Hypatia.Rules.RsrConformance do "1.1.2" => any_of(["Justfile", "justfile"]), "1.1.3" => absent("Makefile"), "1.1.4" => present(".editorconfig"), - "1.2.2" => present(".pre-commit-config.yaml"), + "1.2.2" => any_of([".pre-commit-config.yaml", "ci/.pre-commit-config.yaml"]), "1.2.4" => present(".tool-versions"), "2.1.1" => present("README.adoc"), "2.1.2" => all_graded(["LICENSE", "LICENSES"]), - "2.1.3" => present("SECURITY.md"), - "2.1.4" => present("CODE_OF_CONDUCT.md"), - "2.1.5" => present("CONTRIBUTING.md"), - "2.1.6" => present("CHANGELOG.md"), - "2.1.7" => present("MAINTAINERS.adoc"), - "2.1.8" => present("GOVERNANCE.adoc"), + "2.1.3" => any_of(["SECURITY.md", ".github/SECURITY.md"]), + "2.1.4" => any_of(["CODE_OF_CONDUCT.md", ".github/CODE_OF_CONDUCT.md"]), + "2.1.5" => any_of(["CONTRIBUTING.md", ".github/CONTRIBUTING.md"]), + "2.1.6" => any_of(["CHANGELOG.adoc", "CHANGELOG.md"]), + "2.1.7" => any_of(["MAINTAINERS.adoc", "docs/MAINTAINERS.adoc"]), + "2.1.8" => any_of(["GOVERNANCE.adoc", "docs/GOVERNANCE.adoc", ".github/GOVERNANCE.md"]), "2.1.9" => any_of(["FUNDING.yml", ".github/FUNDING.yml"]), "2.1.10" => all_graded([".gitignore", ".gitattributes"]), "2.2.1" => @@ -352,17 +353,17 @@ defmodule Hypatia.Rules.RsrConformance do ".well-known/humans.txt" ]), "2.3.1" => present("0-AI-MANIFEST.a2ml"), - "3.1.1" => present(".machine_readable/descriptiles"), + "3.1.1" => present_mr("descriptiles"), "3.1.2" => descriptile("STATE"), "3.1.3" => descriptile("META"), "3.1.4" => descriptile("ECOSYSTEM"), "3.1.5" => descriptile("AGENTIC"), "3.1.6" => descriptile("NEUROSYM"), "3.1.7" => descriptile("PLAYBOOK"), - "3.1.8" => descriptile("ANCHOR"), + "3.1.8" => any_of_mr(["descriptiles/ANCHOR.a2ml", "descriptiles/anchors/ANCHOR.a2ml"]), "3.1.9" => descriptile("CLADE"), "3.2.1" => &descriptiles_parse/1, - "3.2.2" => present(".machine_readable/rsr-profile.a2ml"), + "3.2.2" => present_mr("rsr-profile.a2ml"), "6.1.1" => &workflows_present/1, "6.1.2" => present(".github/workflows/hypatia-scan.yml"), "6.1.3" => present(".github/workflows/governance.yml"), @@ -377,20 +378,37 @@ defmodule Hypatia.Rules.RsrConformance do "5.1.6" => no_file_named("package-lock.json"), "7.1.2" => present("LICENSES"), "8.1.4" => &guix_not_stub/1, - "10.1.1" => present("GOVERNANCE.adoc"), - "10.1.3" => present("AFFIRMATION.adoc"), - "11.1.1" => present("AUDIT.adoc") + "10.1.1" => any_of(["GOVERNANCE.adoc", "docs/GOVERNANCE.adoc", ".github/GOVERNANCE.md"]), + "10.1.3" => any_of(["AFFIRMATION.adoc", "docs/AFFIRMATION.adoc"]), + "11.1.1" => any_of(["AUDIT.adoc", "docs/AUDIT.adoc"]) } end defp present(rel), do: fn repo -> if exists?(repo, rel), do: :pass, else: :fail end + # Presence of a path INSIDE the repo's machine tree. The directory is named + # `machine-readable/` canonically and `.machine_readable/` in the legacy + # layout; this resolves per repo at check time so the oracle can score both + # while the estate migrates. Hardcoding either name made whichever half had + # not migrated unscoreable. + defp present_mr(rel) do + fn repo -> if exists?(repo, Path.join(Hypatia.Paths.machine_tree(repo), rel)), do: :pass, else: :fail end + end + defp absent(rel), do: fn repo -> if exists?(repo, rel), do: :fail, else: :pass end defp any_of(rels) do fn repo -> if Enum.any?(rels, &exists?(repo, &1)), do: :pass, else: :fail end end + # any_of within the repo's machine tree, whichever name that tree uses. + defp any_of_mr(rels) do + fn repo -> + mr = Hypatia.Paths.machine_tree(repo) + if Enum.any?(rels, &exists?(repo, Path.join(mr, &1))), do: :pass, else: :fail + end + end + # All present -> :pass, some -> :partial, none -> :fail. defp all_graded(rels) do fn repo -> @@ -403,7 +421,7 @@ defmodule Hypatia.Rules.RsrConformance do end defp descriptile(name), - do: present(Path.join([".machine_readable", "descriptiles", name <> ".a2ml"])) + do: present_mr(Path.join(["descriptiles", name <> ".a2ml"])) # 3.2.1: every RECORD-DIALECT .a2ml under the substrate parses. Markup-dialect # files (the 0-AI-MANIFEST manifest and friends, which open with `@directive` @@ -414,9 +432,9 @@ defmodule Hypatia.Rules.RsrConformance do # valid. defp descriptiles_parse(repo) do candidates = - Path.wildcard(Path.join([repo, ".machine_readable", "descriptiles", "*.a2ml"])) ++ + Path.wildcard(Hypatia.Paths.machine_tree_join(repo, ["descriptiles", "*.a2ml"])) ++ Enum.filter( - [Path.join([repo, ".machine_readable", "rsr-profile.a2ml"])], + [Hypatia.Paths.machine_tree_join(repo, ["rsr-profile.a2ml"])], &File.exists?/1 )