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
30 changes: 30 additions & 0 deletions lib/paths.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
hyperpolymath marked this conversation as resolved.
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
56 changes: 37 additions & 19 deletions lib/rules/rsr_conformance.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -177,13 +178,13 @@ defmodule Hypatia.Rules.RsrConformance do

# --- applicability ---------------------------------------------------------

# Reads `.machine_readable/rsr-profile.a2ml` (record dialect). Accepts the
# Reads `<machine tree>/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),
Expand Down Expand Up @@ -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" =>
Expand All @@ -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"),
Expand All @@ -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 ->
Expand All @@ -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`
Expand All @@ -414,9 +432,9 @@ defmodule Hypatia.Rules.RsrConformance do
# valid.
defp descriptiles_parse(repo) do
Comment thread
hyperpolymath marked this conversation as resolved.
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
)

Expand Down
Loading