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
2 changes: 1 addition & 1 deletion .machine_readable/REGISTRY.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ name = "Standards Hypatia Rules"
stream = "integration"
home = "hypatia-rules/"
canonical_doc = "hypatia-rules/README.adoc"
source_hash = "sha256:1cfcad4499645de9b99d6689ec22fa6d290dc741b1d426ac8e64f4a77017f75e"
source_hash = "sha256:d6df48bcd28acc10dc67feb2b39c9415ce975a8b35131545dd5b82d0bbc571e8"
route = "the dogfooding rules that scan THIS repo (incl. drift detection)"

[[spec]]
Expand Down
17 changes: 14 additions & 3 deletions hypatia-rules/README.adoc
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
= Standards-Specific Hypatia Rules
:status: Draft v0.2.0
:updated: 2026-04-18
:status: Draft v0.3.0
:updated: 2026-08-24

Eight Hypatia rules specific to the standards-repo dogfooding loop.
Nine Hypatia rules specific to the standards-repo dogfooding loop.
Each rule is defined in A2ML, consumes VeriSimDB octads or the repo file
tree, and emits Groove `compliance.finding.new` signals.

Expand All @@ -19,6 +19,7 @@ tree, and emits Groove `compliance.finding.new` signals.
| HYP-S006 | `registry-staleness` | Alert when REGISTRY.a2ml source hashes go stale or a DERIVED doc (TOPOLOGY.md) drifts |
| HYP-S007 | `profile-drift-detector` | Flag a 6a2 file whose content drifts from its declared A2ML `@profile` |
| HYP-S008 | `workflow-allowlist-gap` | Flag a workflow `uses:` an action/reusable not permitted by the repo Actions allowlist (would `startup_failure`) |
| HYP-S009 | `implementation-inside-canon` | Flag product/build manifests below a local (non-external) canonical spec home |

The CRG rule pair (S001 + S005) together enforce grade-honesty: S001
catches backwards moves, S005 catches forwards-overshoots. Both read from
Expand Down Expand Up @@ -50,6 +51,15 @@ so the gap is a legible finding, not a dark run. It is FLAG-ONLY
workflow `GITHUB_TOKEN`; its recipe runs `scripts/check-allowed-actions.sh` and
the paired `scripts/set-allowed-actions.sh` performs the admin fix.

HYP-S009 is the recurrence guard for the standards carve-out campaign. It uses
`.machine_readable/REGISTRY.a2ml` as the authoritative canon boundary and scans
only local entries (those not marked `kind = "external"`); it does not guess
from broad directory names or the mere presence of an A2ML file. Product/build
manifests below a local spec home emit review-tier findings. The rule is
deliberately flag-only:
moving an implementation can cross history, ownership, and licence boundaries
and therefore requires a reviewed carve-out rather than scanner autofix.

== Implementation

Rules live as `.a2ml` files in this directory. They are consumed by
Expand All @@ -76,6 +86,7 @@ Rules read from:
- `a2ml/profiles/REGISTRY.a2ml` + `a2ml/profiles/*/PROFILE.a2ml` and the 6a2
files under `.machine_readable/` (HYP-S007)
- `.github/workflows/*` and `rhodium-standard-repositories/actions-allowlist/allowed-actions.json` (HYP-S008)
- `.machine_readable/REGISTRY.a2ml` LOCAL homes and their product/build manifests (HYP-S009)

And emit:
- Groove `compliance.finding.new` signals with the rule's ID
110 changes: 110 additions & 0 deletions hypatia-rules/implementation-inside-canon.a2ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# SPDX-License-Identifier: MPL-2.0
# HYP-S009 — Implementation Inside Canon
#
# A REGISTRY.a2ml entry whose home is LOCAL declares that the directory is the
# in-repository source of truth for a standard. Product/build manifests below
# that home are therefore architectural drift: an implementation has started
# growing inside the canon again. This is the recurrence guard for the
# standards carve-out campaign (#479 / #498).
#
# This rule deliberately does NOT infer canon from "contains an .a2ml file" or
# from a broad directory name such as docs/. Those heuristics made the earlier
# shell prototype flag legitimate examples, migrations, and documentation
# tooling. REGISTRY.a2ml is the authoritative boundary.

@rule(version="1.0"):
id: HYP-S009
name: "Implementation inside canon"
description: "Flag product/build manifests below a local (non-external) REGISTRY.a2ml spec home"
severity: medium
category: StandardsCompliance
auto_fixable: false
source: standards/hypatia-rules
@end

@parameters:
registry: .machine_readable/REGISTRY.a2ml
implementation_basenames:
- Cargo.toml
- deno.json
- package.json
- Containerfile
- compose.yaml
- CNAME
- ads.txt
conditional_basenames:
- guix.scm
path_exemptions:
- "**/examples/**"
- "**/templates/**"
- "**/test/fixtures/**"
- "**/tests/fixtures/**"
@end

@scanner(type="file-tree"):
find:
- glob: ".machine_readable/REGISTRY.a2ml"
- glob: "**/Cargo.toml"
- glob: "**/deno.json"
- glob: "**/package.json"
- glob: "**/Containerfile"
- glob: "**/compose.yaml"
- glob: "**/guix.scm"
- glob: "**/CNAME"
- glob: "**/ads.txt"
@end

@logic(engine="built-in"):
steps:
- parse_registry: ".machine_readable/REGISTRY.a2ml"
extract:
- key: "id"
as: spec_id
- key: "home"
as: home
- key: "kind"
as: kind
# Local entries omit `kind`; external pointers explicitly use
# `kind = external`. This matches the registry generator contract.
- for_each_spec where "kind != external":
require: "home is a normalized repository-relative directory"
reject_if: "home is absolute or contains a '..' path component"
scan_descendants: home
emit_if: "path not in path_exemptions && basename in implementation_basenames"
finding_kind: implementation-inside-canon
emit_if: "path not in path_exemptions && basename == guix.scm && file is not a documented non-building stub"
finding_kind: implementation-inside-canon
- deduplicate_by: "spec_id, normalized_path"
@end

@router:
default_strategy: review
# Moving an implementation changes repository ownership/topology and may cross
# licence boundaries. It is never safe for a scanner to perform that move.
strategy_caps:
- when: "finding_kind == implementation-inside-canon"
cap: review
reason: "Carve-out requires history, ownership, licence, and destination-repo review"
@end

@action:
emit_signal: compliance.finding.new
message_template: >-
Implementation manifest {normalized_path} is inside LOCAL canonical spec
home '{home}' ({spec_id}). Move the product/build surface to its owning repo
and leave only normative spec, policy, registry, or template-as-spec content.
recipe: review-canon-carve-out
halt_on_violation: false
dedupe_window_days: 7
@end

@recipe(id="review-canon-carve-out"):
description: "Produce an audit-only carve-out report; never move or delete files automatically"
command: "bash scripts/check-implementation-inside-canon.sh ."
auto_applicable: false
@end

@sibling_rules:
- HYP-S006 registry-staleness — verifies registry pointers and derived docs
- HYP-S008 workflow-allowlist-gap — reifies zero-job Actions failures
@end
Loading
Loading