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
133 changes: 124 additions & 9 deletions .github/workflows/governance-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@
# jobs:
# governance:
# uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@main
#
# Hypatia baseline integration (added 2026-05-25):
# - A caller repo may carry `.hypatia-baseline.json` (array of
# acknowledged Hypatia findings; schema lives at
# `.machine_readable/hypatia-baseline.schema.json` in this repo and
# is documented in `docs/HYPATIA-BASELINE-FORMAT.adoc`).
# - `validate-baseline` (below) checks the file shape and reports
# stale entries — it never fails the gate when the baseline is
# absent or matches.
# - `language-policy` consults the baseline for
# `cicd_rules/banned_language_file` exemptions; the legacy
# `.hypatia-ignore` flat-file support is retained for
# backward-compat and will be retired in a follow-up PR once the
# estate has converged on `.hypatia-baseline.json`.
# - Rollout mode is controlled by `vars.HYPATIA_BASELINE_MODE`
# (`advisory` | `blocking`); the default is `advisory` for the
# one-week soak. Flip to `blocking` once the conversion is
# observed-clean.

name: Estate Governance (reusable)

Expand All @@ -37,6 +55,71 @@ permissions:
contents: read

jobs:
validate-baseline:
name: Validate Hypatia baseline
runs-on: ${{ inputs.runs-on }}
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}

- name: Detect baseline file
id: detect
run: |
if [ -f .hypatia-baseline.json ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
echo "::notice::Found .hypatia-baseline.json — entries will be honoured by the language-policy gate."
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::notice::No .hypatia-baseline.json — language-policy gate will treat every banned-language file as new."
fi

- name: Validate baseline against schema
if: steps.detect.outputs.present == 'true'
run: |
# Schema lives in this reusable workflow's repo so consumers
# don't need to bundle their own copy.
SCHEMA_URL="https://raw.githubusercontent.com/hyperpolymath/standards/main/.machine_readable/hypatia-baseline.schema.json"
curl -sSfL "$SCHEMA_URL" -o /tmp/hypatia-baseline.schema.json || {
echo "::warning::Could not fetch baseline schema from $SCHEMA_URL — skipping schema validation."
exit 0
}
# `ajv` is preferred but a pure-jq sanity check is enough for
# the advisory-mode rollout: array of objects, required keys
# present.
if command -v ajv >/dev/null 2>&1; then
ajv validate --spec=draft2020 \
-s /tmp/hypatia-baseline.schema.json \
-d .hypatia-baseline.json
else
jq -e 'type == "array"
and all(.[]; type == "object"
and has("severity")
and has("rule_module")
and has("type")
and (has("file") or has("file_pattern"))
)' .hypatia-baseline.json >/dev/null
echo "✅ baseline shape OK (jq fallback — install ajv for full schema validation)"
fi

- name: Detect stale baseline entries
if: steps.detect.outputs.present == 'true'
run: |
# Soft-fail: a baseline entry pointing at a file that no
# longer exists is a warning, not a hard failure. Flips to
# blocking once estate-wide convergence is observed.
STALE_COUNT=0
while IFS= read -r file; do
if [ -n "$file" ] && [ ! -e "$file" ]; then
echo "::warning file=.hypatia-baseline.json::Stale baseline entry: $file does not exist in the working tree."
STALE_COUNT=$((STALE_COUNT + 1))
fi
done < <(jq -r '.[].file // empty' .hypatia-baseline.json)
echo "Stale entries: $STALE_COUNT"

language-policy:
name: Language / package anti-pattern policy
runs-on: ${{ inputs.runs-on }}
Expand Down Expand Up @@ -138,20 +221,52 @@ jobs:
PYEOF

# Shared escape hatch for the banned-language-file checks below.
# Honours the estate's declared machine-readable exemption (standards#72,
# Explicit-Escape Principle): a file is exempt from the
# `cicd_rules/banned_language_file` rule if EITHER
# * `.hypatia-ignore` contains the exact line
# `cicd_rules/banned_language_file:<relpath>`, OR
# * the file carries an inline `# hypatia:ignore ...
# cicd_rules/banned_language_file` pragma in its first 8 lines
# — the same escape the Hypatia scanner itself honours.
# Honours three exemption mechanisms (see
# standards/docs/EXEMPTION-MECHANISMS.adoc):
# 1. `.hypatia-baseline.json` — array of acknowledged findings,
# shape mirrors the Hypatia findings themselves. Schema is
# `.machine_readable/hypatia-baseline.schema.json` in this
# repo. The validate-baseline job above schema-checks this.
# Added 2026-05-25 as part of the convergence on a single
# exemption mechanism.
# 2. `.hypatia-ignore` flat-file — legacy single-rule-per-line
# format (`cicd_rules/banned_language_file:<relpath>`).
# Will be retired in a follow-up PR once .hypatia-baseline.json
# is in active use across the estate.
# 3. Inline `# hypatia:ignore ...` pragma in the file's first
# 8 lines — the same escape the Hypatia scanner itself
# honours.
- name: Check for ReScript / Go / Python (banned language files)
run: |
rule="cicd_rules/banned_language_file"
rule_module="cicd_rules"
rule_type="banned_language_file"
rule="${rule_module}/${rule_type}"

# Baseline lookup: returns 0 (exempt) if the file appears in
# .hypatia-baseline.json with a matching rule_module + type.
# `file_pattern` glob match is intentionally NOT implemented
# here; the advisory-rollout window only honours exact `file`
# matches. Pattern support arrives with the blocking-mode
# flip and the apply-baseline.sh upgrade (see
# standards/scripts/apply-baseline.sh).
in_baseline() {
local target="$1"
[ -f .hypatia-baseline.json ] || return 1
command -v jq >/dev/null 2>&1 || return 1
jq -e \
--arg rm "$rule_module" \
--arg rt "$rule_type" \
--arg f "$target" \
'any(.[]; .rule_module == $rm and .type == $rt and .file == $f)' \
.hypatia-baseline.json >/dev/null 2>&1
}

is_exempt() {
f="${1#./}"
if in_baseline "$f"; then
echo "⏭️ exempt (baseline): $f"
return 0
fi
if [ -f .hypatia-ignore ] && grep -qxF "${rule}:${f}" .hypatia-ignore; then
return 0
fi
Expand Down
9 changes: 9 additions & 0 deletions .hypatia-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"severity": "critical",
"rule_module": "cicd_rules",
"type": "banned_language_file",
"file": "a2ml-templates/state-scm-to-v2.py",
"note": "One-shot v1 STATE.scm -> v2 STATE.a2ml directive-format migration script. Python is banned estate-wide (CLAUDE.md); this should be rewritten in Rust/AffineScript or retired once every estate repo has migrated to v2. Acknowledged here so the governance gate stops noise-flagging it on every PR."
}
]
67 changes: 67 additions & 0 deletions .machine_readable/hypatia-baseline.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/hyperpolymath/standards/blob/main/.machine_readable/hypatia-baseline.schema.json",
"title": "Hypatia per-repo baseline",
"description": "Authoritative schema for `.hypatia-baseline.json` files in hyperpolymath estate repos. A baseline is an array of acknowledged Hypatia findings that should be suppressed from blocking governance gates.",
"type": "array",
"items": {
"$ref": "#/$defs/BaselineEntry"
},
"$defs": {
"BaselineEntry": {
"type": "object",
"additionalProperties": false,
"required": ["severity", "rule_module", "type"],
"oneOf": [
{ "required": ["file"] },
{ "required": ["file_pattern"] }
],
"properties": {
"severity": {
"description": "Severity of the finding as reported by Hypatia. Must match the finding's severity exactly for the entry to apply.",
"type": "string",
"enum": ["critical", "high", "medium", "low", "info"]
},
"rule_module": {
"description": "Hypatia rule module that emitted the finding (e.g. `cicd_rules`, `code_safety`, `migration_rules`).",
"type": "string",
"pattern": "^[a-z][a-z0-9_]*$"
},
"type": {
"description": "Hypatia finding type within the rule module (e.g. `banned_language_file`, `obj_magic`, `deprecated_api`).",
"type": "string",
"pattern": "^[a-z][a-z0-9_]*$"
},
"file": {
"description": "Repo-relative path to a single file the entry exempts. Mutually exclusive with `file_pattern`.",
"type": "string",
"minLength": 1
},
"file_pattern": {
"description": "Glob pattern (gitignore-style) matching multiple files. Use to exempt a whole subtree without per-file enumeration. Mutually exclusive with `file`.",
"type": "string",
"minLength": 1
},
"severity_override": {
"description": "Optional. If set, the matched finding is downgraded to this severity in the gate (e.g. `low` for migration-window acknowledgement) instead of being silently suppressed. Useful for advisory-mode triage.",
"type": "string",
"enum": ["critical", "high", "medium", "low", "info", "advisory"]
},
"expires_at": {
"description": "Optional ISO-8601 date. After this date the entry is treated as expired and the gate fails again. Used to bound migration windows and prevent baseline rot.",
"type": "string",
"format": "date"
},
"note": {
"description": "Optional free-text human-readable rationale. Recommended for any non-obvious entry. Linked from the gate's summary output.",
"type": "string"
},
"tracking_issue": {
"description": "Optional GitHub issue reference (e.g. `hyperpolymath/gitbot-fleet#148`) that tracks the underlying work to remove the need for this exemption.",
"type": "string",
"pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+$"
}
}
}
}
}
136 changes: 136 additions & 0 deletions docs/EXEMPTION-MECHANISMS.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
= Exemption mechanisms in the hyperpolymath estate
:toc:

Three concentric exemption layers exist in the estate. Each addresses a
different question and lives in a different file. Mixing them up is the
single most common cause of "I added an ignore file but CI still fails"
confusion.

== Quick decision tree

[source]
----
Are you trying to stop bots from WRITING to a path?
-> use the estate-wide bot_exclusion_registry.a2ml

Are you trying to stop the governance gate from FAILING on a
pre-existing finding?
-> use the per-repo .hypatia-baseline.json

Are you trying to make a finding GO AWAY for one specific PR only?
-> currently unsupported. See "per-PR exemptions" below.
----

== Layer 1: Estate-wide bot write denylist

**File:** `hyperpolymath/standards/.machine_readable/bot_exclusion_registry.a2ml`

**Scope:** All bots, all repos. Authoritative across the entire estate.

**Purpose:** Tells bots what they may not *write* to. Read-only scanning
is always allowed; only write actions (Write, Commit, CreatePr,
CreateBranch, etc.) are gated.

**Format:** A2ML (TOML-shaped). Three axes:
1. `external-repos` — exact `owner/repo` matches that bots must skip.
2. `vendored-directory-patterns` — globs that should never be edited.
3. `remote-origin-patterns` — bail-if-origin-matches patterns.

**Consumer:** Every bot via `shared-context::ExclusionRegistry::check()`.
The registry is fail-closed: if the file fails to load, all bot writes
are denied.

**Edit when:** A new repo should be off-limits to bots, or a new
vendored subtree should never be modified.

**Don't use for:** Suppressing CI findings. This layer doesn't touch
CI at all.

== Layer 2: Per-repo Hypatia finding baseline

**File:** `.hypatia-baseline.json` at the calling repo's root.

**Scope:** One repo. Acknowledged findings only — not a "ignore these
forever" file.

**Purpose:** Tells the governance gate which Hypatia findings have been
seen, triaged, and accepted as known debt. Findings matched by a baseline
entry don't block CI; they're surfaced in the run summary as
"acknowledged."

**Format:** JSON array of `{severity, rule_module, type, file | file_pattern, ...}`
objects. See `docs/HYPATIA-BASELINE-FORMAT.adoc` for the full schema.

**Consumer:** `apply-baseline.sh` invoked from
`hyperpolymath/standards/.github/workflows/governance-reusable.yml`.
Suppresses matching findings from the gate, downgrades severity if
`severity_override` is set, fails on expired entries.

**Edit when:**
* Pre-existing debt is tracked for migration (link a `tracking_issue`).
* Vendored / third-party code triggers a rule the team can't fix.
* Test fixtures intentionally violate a rule (with `note` explaining).

**Don't use for:**
* Newly-introduced findings on freshly-authored code. The rule exists
for a reason; baselining the new file defeats it. Fix the code.
* One-off PR-scoped suppression. Baseline edits are merged to main; they
affect all subsequent PRs. See Layer 3.

== Layer 3: Per-PR exemptions (NOT YET IMPLEMENTED)

There is currently no supported per-PR exemption mechanism. PR authors
attempting one (e.g. by inventing a `.hypatia-ignore` file) will find
nothing reads it, and the gate will still fail. **Do not invent new
file conventions.** If you need per-PR exemption, file an issue against
`hyperpolymath/standards` proposing a designed mechanism.

If a mechanism is added, it should be one of:

1. **PR-body marker**, e.g. a fenced block:
+
[source,markdown]
----
```hypatia-exempt
- rule: code_safety/obj_magic
file: bots/sustainabot/bot-integration/src/Analysis.res
reason: Pre-migration debt; tracked in #148
```
----
+
Pros: lives with the PR, vanishes when the PR closes, reviewable in
the same UI as the change. Cons: requires a PR-body parser.

2. **Label-driven**, e.g. apply `gate:exempt:code_safety` and require an
exemption-justification comment from a CODEOWNER.

3. **Sidecar file under `.github/pr-exemptions/`** — committed for one
PR's lifetime, deleted at merge by a workflow.

A formal proposal should pick one and document it explicitly.

== Anti-patterns to reject

These have been attempted and should be refused at review:

* `.hypatia-ignore` (any format) — never read by anything. Reject; point
the author at `.hypatia-baseline.json`.
* Adding `pragma: ignore-rule` comments in source. Hypatia scans by AST
and ignores comments; suppression has to be data-driven, not
source-comment driven.
* Setting `continue-on-error: true` on the governance gate. This makes
CI lie. Use a baseline entry with `severity_override: advisory`
instead — keeps the finding visible while unblocking the gate.
* Adding the offending path to `.gitignore`. The file still exists in
history; Hypatia and other estate scanners still see it.
* Forking `hyperpolymath/standards` to disable the rule for one repo.
If a rule is wrong for one repo, the rule needs scoping in the
upstream, not a fork.

== Cross-references

* `docs/HYPATIA-BASELINE-FORMAT.adoc` — the baseline file format.
* `.machine_readable/hypatia-baseline.schema.json` — machine schema.
* `hyperpolymath/standards#????` — proposal that landed this consumer.
* `hyperpolymath/hypatia` — the scanner that emits findings.
Loading
Loading