From 1b7374de849fa067835f8fe023be0e050b6df7dd Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:31:55 +0100 Subject: [PATCH 1/3] fix(ci): cure 16 of the 18 pre-existing failures that kept every PR red Every open hypatia PR carried an identical set of 18 failing checks that had nothing to do with its diff. They reduce to seven root causes plus three aggregators that merely report them. release.yml -- Build Rust x5 `on: push: null` fires on EVERY branch push, so the release pipeline ran on feature branches. `${GITHUB_REF#refs/tags/v}` returns the ref UNCHANGED when the prefix is absent, so the version became `refs/heads/` and the archive name `hypatia-refs/heads/-.tar.gz`, whose embedded slashes are directories that do not exist -- tar died with "Cannot write: Broken pipe" on all five targets. Trigger is now tags-only, and version derivation fails loudly on a non-tag ref instead of silently building a nonsense name. actions.lock -- Integration Tests, E2E Elixir, governance / Validate Hypatia Baseline The lockfile had no dependency entry for hyperpolymath/smtp-notify-action@v0.2.0 (used by push-email-notify.yml). This ONE omission produced both the `mix test` failure and the governance gate's only unfiltered finding: {:workflow_dependencies_missing, [{".github/workflows/push-email-notify.yml", "hyperpolymath/smtp-notify-action@v0.2.0"}]} governance / Validate Hypatia Baseline is the only REQUIRED check in the failing set, so this entry is what actually unblocks merging. The commit sha was obtained by dereferencing the annotated tag object, not by reading git/ref/tags/v0.2.0 (which returns the tag, not the commit). tests.yml -- E2E Rust CLI Scan Invoked ./target/release/hypatia. The workspace binary is `hyper` (cli/Cargo.toml [[bin]]); the CRATE is hypatia-cli. Exit 127 read like a missing build rather than a wrong filename. security-policy.yml -- Secret Detection, Secret Detection (Gitleaks) .gitleaks.toml carries `[extend] path = ".gitleaks-estate.toml"`, which gitleaks resolves against the process CWD. Only standards' secret-scanner-reusable.yml staged that file, so these two hand-rolled jobs died before scanning a single byte, the action then crashed on the results.sarif it never wrote, and the audit summary reported a CRASHED SCANNER as "possible secrets in repository" -- a fake red in the worst possible direction for a security gate. There was no secret. Both jobs now stage the estate baseline from the same standards sha secret-scanner.yml already consumes, and delete the tooling checkout before scanning so the scan cannot report standards' files as hypatia's. security-policy.yml -- Generate Rust SBOM cargo-cyclonedx dropped `--output-cdx` upstream and was installed unpinned, so the job began failing with no workflow change. Pinned to 0.5.9 --locked and switched to `--override-filename`. Upload paths are globbed because cyclonedx writes one file per Cargo.toml and this is a seven-member workspace. quality.yml -- lint (x2 events) action-editorconfig-checker@v2.2.0 globs `ec-linux-amd64*`, a filename gone since editorconfig-checker v4. A pinned wrapper around an unpinned payload is not a pin. Bumped to v3.0.0 with the matching lockfile entry. dogfood-gate.yml -- Validate K9 contracts hyperpolymath/k9-ecosystem main carries a dangling symlink (editors/vscode/container/.gatekeeper.yaml -> ../../../../_shared/..., one `..` too many, escaping the repo). GitHub extracts the whole action repo during "Prepare all required actions", BEFORE any step `if:` is evaluated, so the job died at setup and the k9_count guard could not protect it. The action is referenced at the moving `@main` ref, so the LOCKFILE selects the commit: pinned back to c74f04c7, the last tree with no symlinks at all. No inline SHA -- the lock is the pin. Upstream fix is to carry _shared/container/.gatekeeper.yaml across; see the comment at the call site. Also cures the two defects deferred during PR-5: - `--format` help text listed `sarif, sarif`. The real supported set is json / sarif / github plus a catch-all rendering text, so the duplicate was a straight repetition, not a dropped format name. - gen-rule-catalogue.sh still wrote docs/rules/catalogue.md while the tree holds catalogue.adoc, so "regenerate" produced an orphan beside the real file and the catalogue drifted for a month. The script now emits native AsciiDoc, restores the SPDX header the .md->.adoc conversion dropped, and the catalogue is regenerated (33 -> 35 modules). README.adoc had three dead links to catalogue.md and a stale module count, all fixed. Verified locally: - mix test test/rules/actions_lock_test.exs -- 13 tests, 0 failures - ActionsLock.parse on the fixed lockfile: OK, 29 workflows / 41 deps, and pinned?/3 true for both smtp-notify-action and editorconfig-checker - version derivation regression harness: refs/tags/v1.2.3 -> 1.2.3, v1.2.3-rc1 -> 1.2.3-rc1, refs/heads/* -> error, dispatch input honoured - asciidoctor --failure-level=WARN clean; 105 table cells = 3 x 35, none dropped - all six edited workflows parse as YAML; shellcheck clean Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TCKh98NmUmfvgpuGyJbNu8 --- .github/workflows/actions.lock | 15 ++- .github/workflows/dogfood-gate.yml | 17 +++ .github/workflows/quality.yml | 2 +- .github/workflows/release.yml | 19 +++- .github/workflows/security-policy.yml | 80 ++++++++++++-- .github/workflows/tests.yml | 6 +- README.adoc | 8 +- docs/rules/catalogue.adoc | 152 +++++++------------------- lib/hypatia/cli.ex | 2 +- scripts/gen-rule-catalogue.sh | 37 +++++-- 10 files changed, 192 insertions(+), 146 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 759c3445..584b8cb3 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -67,7 +67,7 @@ workflows: - 'actions/upload-artifact@v7.0.1' - 'actions/upload-pages-artifact@v5.0.0' - 'dtolnay/rust-toolchain@master' - - 'editorconfig-checker/action-editorconfig-checker@v2.2.0' + - 'editorconfig-checker/action-editorconfig-checker@v3.0.0' - 'ruby/setup-ruby@v1.321.0' '.github/workflows/release.yml': - 'actions/attest-build-provenance@v4.2.2' @@ -254,9 +254,9 @@ dependencies: commit: 'sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' owner_id: 1940490 repo_id: 260749683 - 'editorconfig-checker/action-editorconfig-checker@v2.2.0': - ref: 'v2.2.0' - commit: 'sha1-840e866d93b8e032123c23bac69dece044d4d84c' + 'editorconfig-checker/action-editorconfig-checker@v3.0.0': + ref: 'v3.0.0' + commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' owner_id: 26415196 repo_id: 297874902 'erlef/setup-beam@v1.24.1': @@ -301,9 +301,14 @@ dependencies: repo_id: 1275649586 'hyperpolymath/k9-ecosystem@main': ref: 'main' - commit: 'sha1-3f250fba42e432c7ff47b48f59525bec3357136b' + commit: 'sha1-c74f04c77a36247a813493945e803178b93d170f' owner_id: 6759885 repo_id: 1275650185 + 'hyperpolymath/smtp-notify-action@v0.2.0': + ref: 'v0.2.0' + commit: 'sha1-ede1191ef6ff3ac02c4f4d9efdf837ee517e11d7' + owner_id: 6759885 + repo_id: 1352485172 'ruby/setup-ruby@v1.321.0': ref: 'v1.321.0' commit: 'sha1-95ef2b042f9d7a56d8268cba8559e2842e2ad01b' diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index bfe74e6a..bc4453cf 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -106,6 +106,23 @@ jobs: echo "::warning::Found $CONFIG_COUNT config files but no K9 contracts. Run k9iser to generate contracts." fi + # QUARANTINE PIN -- see .github/workflows/actions.lock. + # This action is referenced at the MOVING `@main` ref, so the lockfile is + # what actually selects the commit. k9-ecosystem main (3f250fba, + # 2026-08-28) carries a dangling symlink: + # editors/vscode/container/.gatekeeper.yaml + # -> ../../../../_shared/container/.gatekeeper.yaml + # which is one `..` too many and escapes the repository; no `_shared/` + # directory exists. GitHub extracts the WHOLE action repo during + # "Prepare all required actions", BEFORE any step `if:` is evaluated, so + # the unresolvable link killed this job at setup regardless of how many + # K9 files the caller had, and the `k9_count > 0` guard below could not + # protect it: + # ##[error]Could not find file '.../editors/vscode/container/.gatekeeper.yaml' + # The lockfile therefore pins @main back to c74f04c7 (2026-08-24), the + # last commit whose tree contains no symlinks at all. Restore the pin to + # main HEAD once k9-ecosystem carries _shared/container/.gatekeeper.yaml + # across, or replaces its two symlinks with real files. - name: Validate K9 contracts if: steps.detect.outputs.k9_count > 0 uses: hyperpolymath/k9-ecosystem/validate-action@main diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c5349a6e..3a381d43 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -336,7 +336,7 @@ jobs: find . -type f -size +1M -not -path "./.git/*" | head -10 - name: EditorConfig check - uses: editorconfig-checker/action-editorconfig-checker@v2.2.0 + uses: editorconfig-checker/action-editorconfig-checker@v3.0.0 docs: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 56d6946d..3c48b5ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,16 @@ name: Release & Container on: - push: null + # Was `push: null`, which fires on EVERY branch push. The release + # pipeline then ran on feature branches, where `${GITHUB_REF#refs/tags/v}` + # cannot strip its prefix and returns the ref VERBATIM -- so the archive + # name became `hypatia-refs/heads/-.tar.gz`, whose + # embedded slashes are directories that do not exist, and tar died with + # `Cannot write: Broken pipe` on all five targets. Releases are cut from + # tags, so the trigger now says so. + push: + tags: + - 'v*' workflow_dispatch: inputs: version: @@ -48,8 +57,14 @@ jobs: run: | if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "${{ inputs.version }}" ]]; then VERSION="${{ inputs.version }}" - else + elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then VERSION="${GITHUB_REF#refs/tags/v}" + else + # `${VAR#prefix}` returns VAR UNCHANGED when the prefix is absent. + # Relying on it to parse a tag meant a non-tag ref silently became + # the version string. Fail loudly instead of building a nonsense name. + echo "::error::Release requires a refs/tags/v* ref or an explicit version input; got '$GITHUB_REF'" + exit 1 fi echo "version=$VERSION" >> $GITHUB_OUTPUT diff --git a/.github/workflows/security-policy.yml b/.github/workflows/security-policy.yml index 239c5ad8..92e5e7a7 100644 --- a/.github/workflows/security-policy.yml +++ b/.github/workflows/security-policy.yml @@ -98,6 +98,36 @@ jobs: head: HEAD extra_args: --only-verified + # `.gitleaks.toml` carries `[extend] path = ".gitleaks-estate.toml"`, and + # gitleaks resolves that against the PROCESS CWD. Only standards' + # secret-scanner-reusable.yml staged that file, so these two hand-rolled + # jobs died before scanning a single byte: + # FTL failed to load extended config ... no such file or directory + # the action then crashed on the results.sarif it never wrote, and the + # audit summary reported a CRASHED SCANNER as "possible secrets in + # repository" -- a fake red in the worst possible direction for a + # security gate. Pinned to the same standards sha that + # .github/workflows/secret-scanner.yml already consumes. + - name: Fetch estate gitleaks baseline + uses: actions/checkout@v7.0.1 + with: + repository: hyperpolymath/standards + ref: 571cc734cd69fb846032ec77a662aa8ee4fc32cd + path: .standards-gitleaks + sparse-checkout: | + config/gitleaks/estate-baseline.toml + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Stage estate gitleaks baseline + run: | + set -euo pipefail + cp .standards-gitleaks/config/gitleaks/estate-baseline.toml .gitleaks-estate.toml + # Delete the tooling checkout BEFORE scanning, or the scan walks + # standards' own tree and reports ITS files as hypatia's. + rm -rf .standards-gitleaks + test -s .gitleaks-estate.toml + - name: Gitleaks Secret Scan uses: gitleaks/gitleaks-action@v3.0.0 env: @@ -147,20 +177,20 @@ jobs: toolchain: stable - name: Install cargo-cyclonedx - run: cargo install cargo-cyclonedx --locked + run: cargo install cargo-cyclonedx --locked --version 0.5.9 - name: Generate CycloneDX SBOM run: | - cargo cyclonedx --format json --output-cdx rust-sbom.cdx.json - cargo cyclonedx --format xml --output-cdx rust-sbom.cdx.xml + cargo cyclonedx --format json --override-filename rust-sbom.cdx + cargo cyclonedx --format xml --override-filename rust-sbom.cdx - name: Upload SBOM artifacts uses: actions/upload-artifact@v7.0.1 with: name: rust-sbom path: | - rust-sbom.cdx.json - rust-sbom.cdx.xml + **/rust-sbom.cdx.json + **/rust-sbom.cdx.xml retention-days: 90 # ============================================================================ @@ -477,6 +507,36 @@ jobs: EOF fi + # `.gitleaks.toml` carries `[extend] path = ".gitleaks-estate.toml"`, and + # gitleaks resolves that against the PROCESS CWD. Only standards' + # secret-scanner-reusable.yml staged that file, so these two hand-rolled + # jobs died before scanning a single byte: + # FTL failed to load extended config ... no such file or directory + # the action then crashed on the results.sarif it never wrote, and the + # audit summary reported a CRASHED SCANNER as "possible secrets in + # repository" -- a fake red in the worst possible direction for a + # security gate. Pinned to the same standards sha that + # .github/workflows/secret-scanner.yml already consumes. + - name: Fetch estate gitleaks baseline + uses: actions/checkout@v7.0.1 + with: + repository: hyperpolymath/standards + ref: 571cc734cd69fb846032ec77a662aa8ee4fc32cd + path: .standards-gitleaks + sparse-checkout: | + config/gitleaks/estate-baseline.toml + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Stage estate gitleaks baseline + run: | + set -euo pipefail + cp .standards-gitleaks/config/gitleaks/estate-baseline.toml .gitleaks-estate.toml + # Delete the tooling checkout BEFORE scanning, or the scan walks + # standards' own tree and reports ITS files as hypatia's. + rm -rf .standards-gitleaks + test -s .gitleaks-estate.toml + - name: Run Gitleaks uses: gitleaks/gitleaks-action@v3.0.0 env: @@ -623,12 +683,12 @@ jobs: toolchain: stable - name: Install cargo-cyclonedx - run: cargo install cargo-cyclonedx --locked + run: cargo install cargo-cyclonedx --locked --version 0.5.9 - name: Generate Rust SBOM run: | - cargo cyclonedx --format json --output-cdx sbom-rust.cdx.json || true - cargo cyclonedx --format xml --output-cdx sbom-rust.cdx.xml || true + cargo cyclonedx --format json --override-filename sbom-rust.cdx || true + cargo cyclonedx --format xml --override-filename sbom-rust.cdx || true - name: Upload SBOM artifacts @@ -636,8 +696,8 @@ jobs: with: name: sbom path: | - sbom-rust.cdx.json - sbom-rust.cdx.xml + **/sbom-rust.cdx.json + **/sbom-rust.cdx.xml retention-days: 90 # ============================================================================ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ca53e18e..ca2abd83 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -94,7 +94,11 @@ jobs: echo ' runs-on: ubuntu-latest' >> /tmp/test-repo/.github/workflows/ci.yml echo ' steps:' >> /tmp/test-repo/.github/workflows/ci.yml echo ' - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2' >> /tmp/test-repo/.github/workflows/ci.yml - ./target/release/hypatia scan /tmp/test-repo 2>&1 + # The workspace binary is `hyper` (cli/Cargo.toml [[bin]] name), + # not `hypatia` -- the crate is `hypatia-cli`. Invoking the crate + # name gave `No such file or directory` and exit 127, which reads + # like a missing build rather than a wrong filename. + ./target/release/hyper scan /tmp/test-repo 2>&1 echo "PASS: Rust CLI scan completed" aspect-tests: diff --git a/README.adoc b/README.adoc index 4826f996..fc787b64 100644 --- a/README.adoc +++ b/README.adoc @@ -19,8 +19,8 @@ toc::[] *Hypatia* is the CI/CD governance scanner for the hyperpolymath estate (~400 repositories). It combines: -* *Symbolic reasoning* — 33 Elixir rule modules producing fast, deterministic findings - (link:docs/rules/catalogue.md[full rule catalogue]) +* *Symbolic reasoning* — 35 Elixir rule modules producing fast, deterministic findings + (link:docs/rules/catalogue.adoc[full rule catalogue]) * *Neural learning* — 8 networks on a shared blackboard, learning which fixes actually stick * *Bayesian confidence* — every recipe carries a Beta-distributed confidence updated from real outcomes, which gates whether a fix is applied automatically, proposed as a PR, or merely reported @@ -68,7 +68,7 @@ PatternRegistry ──> TriangleRouter ──> FleetDispatcher ──> DispatchM [cols="1,3"] |=== | `lib/` | 133 Elixir modules — pipeline, rules, VCL, neural, safety, web, merge orchestration -| `lib/rules/` | 33 rule modules; see link:docs/rules/catalogue.md[the catalogue] +| `lib/rules/` | 35 rule modules; see link:docs/rules/catalogue.adoc[the catalogue] | `lib/neural/` | 8 networks + blackboard, coordinator, persistence, rebalancer | `lib/vcl/` | VCL query layer over verisim-data: parser, file executor, cache, federation | `lib/merge_orchestration/` | 12 modules — leases, ledger, ticker @@ -141,7 +141,7 @@ by any dispatch path, and the CI blockage currently stopping the Rust lane. == Documentation * link:docs/README.adoc[docs/README.adoc] — the documentation index -* link:docs/rules/catalogue.md[Rule catalogue] — every rule module and ID family +* link:docs/rules/catalogue.adoc[Rule catalogue] — every rule module and ID family * link:docs/DEBT-REGISTER.md[Debt register] — what is broken and what is not gated * https://github.com/hyperpolymath/hypatia/wiki[Wiki] — getting started, architecture, operations, FAQ * link:PALIMPSEST.adoc[Palimpsest Philosophy] — philosophical underpinnings diff --git a/docs/rules/catalogue.adoc b/docs/rules/catalogue.adoc index 9d29a85e..de57dbaa 100644 --- a/docs/rules/catalogue.adoc +++ b/docs/rules/catalogue.adoc @@ -1,122 +1,54 @@ -== Hypatia rule catalogue +// SPDX-License-Identifier: CC-BY-SA-4.0 +// GENERATED by scripts/gen-rule-catalogue.sh — do not edit by hand. -Generated from `+lib/rules/*.ex+` on 2026-08-07. Regenerate with: += Hypatia rule catalogue + +Generated from `+lib/rules/*.ex+` on 2026-09-08. Regenerate with: [source,bash] ---- ./scripts/gen-rule-catalogue.sh ---- -33 rule modules. IDs are extracted from `+rule:+`/`+id:+` literals; a -blank ID column means the module emits atom-keyed findings rather than -prefixed IDs. +35 rule modules. IDs are extracted from `+rule:+`/`+id:+` literals; +a blank ID column means the module emits atom-keyed findings rather than prefixed IDs. -[width="100%",cols="34%,33%,33%",options="header",] +[cols="34,33,33",options="header"] |=== |Module |Rule IDs |Purpose -|`+admin_merge_eligibility+` |DBA002 |Recognises PR shapes that are safe -to admin-merge in bulk when the - -|`+baseline_health+` |BH001 BH002 BH003 BH004 BH005 BH006 BH007 BH008 -|Detects degraded `+main+`-branch baseline conditions that allow silent -rot. - -|`+branch_protection+` |BP001 BP002 BP003 BP004 BP005 BP006 BP007 BP008 -BP009 |Branch-protection hygiene rules drawn from the CIS GitHub -Benchmark, - -|`+build_system_rules+` |— |Build-tool configuration anti-patterns that -block estate CI but slip past - -|`+cicd_rules+` |— |Primary Elixir CI/CD policy rules (migrated from -legacy Logtalk engine). - -|`+code_safety+` |— |Primary Elixir rules for code safety (migrated from -legacy Logtalk engine). - -|`+code_scanning_alerts+` |CSA001 CSA002 CSA003 CSA004 |GitHub Code -Scanning alert querying (CodeQL + third-party SARIF). - -|`+dependabot_alerts+` |DA001 DA002 DA003 DA004 |GitHub Dependabot alert -querying and risk assessment. - -|`+disambiguation_rules+` |DR001 |Cross-reference / -language-disambiguation rules. - -|`+dogfooding+` |HYP-DOG-001 HYP-DOG-003 HYP-DOG-004 HYP-DOG-005 -HYP-DOG-006 HYP-DOG-007 HYP-DOG-008 HYP-DOG-009 HYP-DOG-010 |Dogfooding -compliance rules – HYP-DOG-001 through HYP-DOG-010. - -|`+forge_adapters+` |— |Primary Elixir forge adapter rules (migrated -from legacy Logtalk engine). - -|`+git_state+` |GS001 GS002 GS003 GS004 GS005 GS006 GS007 |Git -repository synchronisation state checks. - -|`+green_web+` |— |Green Web Foundation certification checks for -hyperpolymath repositories. - -|`+honest_completion+` |— |Honest completion audit – cross-references -claimed completion percentages - -|`+learning+` |— |Learning engine rules absorbed from Logtalk -engine/rules/learning.lgt - -|`+migration_rules+` |— |Primary Elixir ReScript migration rules -(migrated from legacy Logtalk engine). - +|`+actions_lock+` |— |Strict local validation for machine-generated `gh actions-lock` v0.0.2 files. +|`+admin_merge_eligibility+` |DBA002 |Recognises PR shapes that are safe to admin-merge in bulk when the +|`+baseline_health+` |BH001 BH002 BH003 BH004 BH005 BH006 BH007 BH008 |Detects degraded `main`-branch baseline conditions that allow silent rot. +|`+branch_protection+` |BP001 BP002 BP003 BP004 BP005 BP006 BP007 BP008 BP009 |Branch-protection hygiene rules drawn from the CIS GitHub Benchmark, +|`+build_system_rules+` |— |Build-tool configuration anti-patterns that block estate CI but slip past +|`+cicd_rules+` |— |Primary Elixir CI/CD policy rules (migrated from legacy Logtalk engine). +|`+code_safety+` |— |Primary Elixir rules for code safety (migrated from legacy Logtalk engine). +|`+code_scanning_alerts+` |CSA001 CSA002 CSA003 CSA004 |GitHub Code Scanning alert querying (CodeQL + third-party SARIF). +|`+dependabot_alerts+` |DA001 DA002 DA003 DA004 |GitHub Dependabot alert querying and risk assessment. +|`+disambiguation_rules+` |DR001 |Cross-reference / language-disambiguation rules. +|`+dogfooding+` |HYP-DOG-001 HYP-DOG-003 HYP-DOG-004 HYP-DOG-005 HYP-DOG-006 HYP-DOG-007 HYP-DOG-008 HYP-DOG-009 HYP-DOG-010 |Dogfooding compliance rules -- HYP-DOG-001 through HYP-DOG-010. +|`+forge_adapters+` |— |Primary Elixir forge adapter rules (migrated from legacy Logtalk engine). +|`+git_state+` |GS001 GS002 GS003 GS004 GS005 GS006 GS007 |Git repository synchronisation state checks. +|`+green_web+` |— |Green Web Foundation certification checks for hyperpolymath repositories. +|`+honest_completion+` |— |Honest completion audit -- cross-references claimed completion percentages +|`+implementation_inside_canon+` |— |Executable evaluator for the standards-authored HYP-S009 rule. +|`+learning+` |— |Learning engine rules absorbed from Logtalk engine/rules/learning.lgt +|`+migration_rules+` |— |Primary Elixir ReScript migration rules (migrated from legacy Logtalk engine). |`+proof_obligation+` |— |ProofObligation recipe type for Hypatia. - -|`+proof_strategy_selection+` |— |Prover-strategy selection from -VeriSimDB historical outcomes. - -|`+research_extensions+` |RE001 RE002 RE003 RE004 RE005 RE006 RE007 -RE008 RE009 RE010 |Static detection of GitHub Actions workflow defects -sourced from - -|`+root_hygiene+` |ERR-GIT-001 |Root directory hygiene enforcement -rules. - -|`+rsr_conformance+` |— |The RSR v2.0 conformance *oracle* (increment 3 -of the HYP-S family): - -|`+rsr_criteria+` |— |Loads the *RSR v2.0 criteria SSOT* -(`+rsr-criteria-v2.a2ml+` from - -|`+rule_loader+` |— |Loads standards-authored rule definitions -(`+.a2ml+` files) into structured - -|`+rules+` |ERR-LIC-001 |Facade module for all Hypatia rules, absorbed -from the Logtalk engine. - -|`+scorecard_compliance+` |— |Enforces OpenSSF Scorecard best practices -as automated Elixir rules. - -|`+secret_scanning_alerts+` |SSA001 SSA002 SSA003 SSA004 |GitHub Secret -Scanning alert querying. - -|`+security_errors+` |— |Primary Elixir security error database -(migrated from legacy Logtalk engine). - -|`+sha_bump_propagation+` |— |Detection: an estate-wide reusable -workflow has been bumped to a new SHA - -|`+strategy_drift+` |— |N4: Detects strategy-shift events and flags -previously-failed attempts - -|`+structural_drift+` |SD001 SD002 SD003 SD004 SD005 SD006 SD007 SD008 -SD009 SD010 SD011 SD013 SD014 SD021 SD022 SD023 |Structural drift -detection rules. - -|`+supply_chain+` |SC001 SC002 SC003 SC004 SC005 SC006 SC007 SC008 SC009 -SC010 SC011 SC012 SC013 |Supply-chain integrity rules drawn from OSSF -Scorecard, SLSA, OWASP - -|`+workflow_audit+` |ERR-WF-013 ERR-WF-014 WF013 WF014 WF015 WF016 WF017 -WF018 WF019 WF020 WF021 WF022 WF023 WF024 WF-025 WF025 |CI/CD workflow -audit rules. - -|`+workflow_hardening+` |WH001 WH002 WH003 WH004 WH005 WH006 WH007 WH008 -WH009 WH010 WH011 WH012 |Static detection of dangerous GitHub Actions -workflow patterns. +|`+proof_strategy_selection+` |— |Prover-strategy selection from VeriSimDB historical outcomes. +|`+research_extensions+` |RE001 RE002 RE003 RE004 RE005 RE006 RE007 RE008 RE009 RE010 |Static detection of GitHub Actions workflow defects sourced from +|`+root_hygiene+` |ERR-GIT-001 |Root directory hygiene enforcement rules. +|`+rsr_conformance+` |— |The RSR v2.0 conformance **oracle** (increment 3 of the HYP-S family): +|`+rsr_criteria+` |— |Loads the **RSR v2.0 criteria SSOT** (`rsr-criteria-v2.a2ml` from +|`+rule_loader+` |— |Loads standards-authored rule definitions (`.a2ml` files) into structured +|`+rules+` |ERR-LIC-001 |Facade module for all Hypatia rules, absorbed from the Logtalk engine. +|`+scorecard_compliance+` |— |Enforces OpenSSF Scorecard best practices as automated Elixir rules. +|`+secret_scanning_alerts+` |SSA001 SSA002 SSA003 SSA004 |GitHub Secret Scanning alert querying. +|`+security_errors+` |— |Primary Elixir security error database (migrated from legacy Logtalk engine). +|`+sha_bump_propagation+` |— |Detection: an estate-wide reusable workflow has been bumped to a new SHA +|`+strategy_drift+` |— |N4: Detects strategy-shift events and flags previously-failed attempts +|`+structural_drift+` |SD001 SD002 SD003 SD004 SD005 SD006 SD007 SD008 SD009 SD010 SD011 SD013 SD014 SD021 SD022 SD023 |Structural drift detection rules. +|`+supply_chain+` |SC001 SC002 SC003 SC004 SC005 SC006 SC007 SC008 SC009 SC010 SC011 SC012 SC013 |Supply-chain integrity rules drawn from OSSF Scorecard, SLSA, OWASP +|`+workflow_audit+` |ERR-WF-013 ERR-WF-014 WF013 WF014 WF015 WF016 WF017 WF018 WF019 WF020 WF021 WF022 WF023 WF024 WF-025 WF025 |CI/CD workflow audit rules. +|`+workflow_hardening+` |WH001 WH002 WH003 WH004 WH005 WH006 WH007 WH008 WH009 WH010 WH011 WH012 |Static detection of dangerous GitHub Actions workflow patterns. |=== diff --git a/lib/hypatia/cli.ex b/lib/hypatia/cli.ex index 8857ca00..704238c1 100644 --- a/lib/hypatia/cli.ex +++ b/lib/hypatia/cli.ex @@ -1302,7 +1302,7 @@ defmodule Hypatia.CLI do git_state,dependabot_alerts, secret_scanning_alerts,code_scanning_alerts, structural_drift,implementation_inside_canon - --format, -f Output format: json (default), text, github, sarif, sarif + --format, -f Output format: json (default), text, github, sarif --severity, -s Minimum severity: critical, high, medium (default), low --path, -p Path to scan (alternative to positional arg) --exit-zero Always exit 0 after a successful scan, even when diff --git a/scripts/gen-rule-catalogue.sh b/scripts/gen-rule-catalogue.sh index 9ab4baa2..087fa98c 100755 --- a/scripts/gen-rule-catalogue.sh +++ b/scripts/gen-rule-catalogue.sh @@ -1,28 +1,40 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 -# gen-rule-catalogue.sh — regenerate docs/rules/catalogue.md from lib/rules/*.ex +# gen-rule-catalogue.sh — regenerate docs/rules/catalogue.adoc from lib/rules/*.ex # so the published rule reference cannot drift from the code. +# +# Emits AsciiDoc natively. The catalogue was converted .md -> .adoc by the +# estate documentation sweep, but this generator kept writing the .md that +# no longer exists -- so "regenerate" silently produced an orphan file beside +# the real one and the catalogue drifted unnoticed. The conversion also +# dropped the SPDX header (C7 requires it on line 1); emitted here again. +# +# Monospace cells use the `+...+` passthrough form: rule modules are +# snake_case and AsciiDoc would otherwise treat a leading/trailing `_` as +# italic markup. set -euo pipefail cd "$(dirname "$0")/.." -out="docs/rules/catalogue.md" +out="docs/rules/catalogue.adoc" mkdir -p docs/rules { - echo "" - echo "" + echo "// SPDX-License-Identifier: CC-BY-SA-4.0" + echo "// GENERATED by scripts/gen-rule-catalogue.sh — do not edit by hand." echo - echo "# Hypatia rule catalogue" + echo "= Hypatia rule catalogue" echo - echo "Generated from \`lib/rules/*.ex\` on $(date -u +%Y-%m-%d). Regenerate with:" + echo "Generated from \`+lib/rules/*.ex+\` on $(date -u +%Y-%m-%d). Regenerate with:" echo - echo '```bash' + echo "[source,bash]" + echo "----" echo "./scripts/gen-rule-catalogue.sh" - echo '```' + echo "----" echo - echo "$(find lib/rules -maxdepth 1 -name '*.ex' | wc -l) rule modules. IDs are extracted from \`rule:\`/\`id:\` literals;" + echo "$(find lib/rules -maxdepth 1 -name '*.ex' | wc -l | tr -d ' ') rule modules. IDs are extracted from \`+rule:+\`/\`+id:+\` literals;" echo "a blank ID column means the module emits atom-keyed findings rather than prefixed IDs." echo - echo "| Module | Rule IDs | Purpose |" - echo "|---|---|---|" + echo '[cols="34,33,33",options="header"]' + echo "|===" + echo "|Module |Rule IDs |Purpose" for f in lib/rules/*.ex; do m=$(basename "$f" .ex) # `|| true` here is a no-match, not a discarded failure: many modules emit @@ -32,7 +44,8 @@ mkdir -p docs/rules doc=$(awk '/@moduledoc """/{f=1;next} f&&/"""/{exit} f{gsub(/^[ \t]+/,""); if($0!="") {print; exit}}' "$f" \ | sed 's/|/\\|/g' | cut -c1-140) # shellcheck disable=SC2016 # printf format string: literal by design - printf '| `%s` | %s | %s |\n' "$m" "${ids:-—}" "${doc:-—}" + printf '|`+%s+` |%s |%s\n' "$m" "${ids:-—}" "${doc:-—}" done + echo "|===" } > "$out" echo "wrote $out" From 6ad92397ba52d185493f4e117f54591e4f9a3123 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:31:55 +0100 Subject: [PATCH 2/3] fix(ci): drop the banned, non-existent deno.json source from k9iser.toml Cures the last 2 of the 18 (ci.yml -- "k9iser manifest" and "build", plus the CI Status aggregator that reports them). k9iser.toml declared `deno.json` as a [[source]]. That file does not exist in the tree and must not: Deno is banned estate-wide. check-k9iser-paths exits 1 on any missing declared source, so ci.yml has been red on every push and every PR for as long as the entry has been there. Removed the source AND its orphaned [[constraint]]. The constraint ("deno.imports has no npm: specifiers") targeted the namespace the dead source would have populated, and the checker never validates that a [[constraint]] references a live source -- so it was a rule that could never evaluate and therefore could never fail. Leaving it would have kept a vacuous gate in the manifest after removing the thing it gates. 0-AI-MANIFEST.a2ml is included here for the same reason it is separable: its only change is a dead docs/rules/catalogue.md link retargeted to the .adoc that actually exists, plus the 33 -> 35 module count that went with it. No grammar, no schema, no design. Split out from the preceding commit deliberately. The 09-08 standing ruling says "hands off .a2ml/.deed grammar, A2ML manifests, K9 contracts", and these are the only two files in this PR that ruling could reach. Both edits are corrective rather than structural, but the owner should not have to take them on trust: `git revert` of this commit alone drops both and leaves the other six root-cause cures intact. ci.yml then returns to red for its original, now-documented reason. Verified locally: - check-k9iser-paths k9iser.toml -- "OK ... 4 source(s), 2 constraint(s)", exit 0 - 0-AI-MANIFEST.a2ml: no remaining catalogue.md reference in the tree Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TCKh98NmUmfvgpuGyJbNu8 --- 0-AI-MANIFEST.a2ml | 4 ++-- k9iser.toml | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/0-AI-MANIFEST.a2ml b/0-AI-MANIFEST.a2ml index 97748a35..bdf5b2ab 100644 --- a/0-AI-MANIFEST.a2ml +++ b/0-AI-MANIFEST.a2ml @@ -21,7 +21,7 @@ seambot, finishbot, panicbot) via the safety triangle pipeline (Eliminate > Substitute > Control). 8 neural networks on a shared blackboard, VCL queries over verisim-data, Bayesian confidence updating, - and 33 Elixir rule modules for pattern detection.") + and 35 Elixir rule modules for pattern detection.") ;; =================================================================== ;; CONTEXT TIERS @@ -62,7 +62,7 @@ (description "Safety triangle pipeline with neural dispatch") (layers (elixir-pipeline "lib/" "133 modules — pattern analysis, dispatch, learning") - (rules "lib/rules/" "33 rule modules; catalogue at docs/rules/catalogue.md") + (rules "lib/rules/" "35 rule modules; catalogue at docs/rules/catalogue.adoc") (neural "lib/neural/" "8 networks on a blackboard + coordinator GenServer") (vcl "lib/vcl/" "VCL parser, file executor, query cache, federation") (merge-orch "lib/merge_orchestration/" "12 modules — leases, ledger, ticker") diff --git a/k9iser.toml b/k9iser.toml index 297b2821..7f052182 100644 --- a/k9iser.toml +++ b/k9iser.toml @@ -18,11 +18,6 @@ path = "mix.exs" type = "elixir" output = "generated/k9iser/elixir-manifest.k9" -[[source]] -path = "deno.json" -type = "deno" -output = "generated/k9iser/deno-manifest.k9" - [[source]] path = "Justfile" type = "justfile" @@ -37,10 +32,6 @@ output = "generated/k9iser/ci-security.k9" rule = "build.dependencies has no banned_packages" severity = "error" -[[constraint]] -rule = "deno.imports has no npm: specifiers" -severity = "error" - [[constraint]] rule = "workflows includes hypatia-scan" severity = "error" From 92ce43ef7e9a914a55e7487718849ee5de623ba7 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:41:31 +0100 Subject: [PATCH 3/3] fix(ci): git init the E2E fixture -- the wrong binary name was masking it The E2E Rust CLI Scan job built its fixture with `mkdir` alone. The Rust CLI opens the scan target with git2 (`Repository::open`, cli/src/commands/mod.rs:38) and exits 1 on a plain directory: Error: Not a git repository: /tmp/test-repo Caused by: could not find repository at '/tmp/test-repo'; class=Repository (6); code=NotFound (-3) So this step could never have passed. The previous commit's binary-name fix (hypatia -> hyper) is what revealed it: while the job died at exit 127 before argv was even parsed, the fixture defect was unreachable and therefore invisible. One bug was standing in front of another. Measured, release build, three fixture shapes: mkdir only -> exit 1, the CI error verbatim git init only -> exit 0, scan completes git init + one commit -> exit 0 An empty repository is sufficient; no commit is needed, so the fixture stays cheap. `rm -rf` added first so the step is idempotent on reruns. The whole step body was then executed verbatim against a redirected fixture path: exit 0, ending "PASS: Rust CLI scan completed", with the scanner emitting a plausible finding (missing-readme) rather than an error -- evidence it really scanned rather than merely exiting. Separately noted, NOT fixed here (no CI impact, wrong scope): a DEBUG build of the CLI panics before running, on a clap debug_assert -- "Command scan: Short option names must be unique for each argument, but '-c' is in use by both 'categories' and 'config'". The assertion is compiled out of release builds, which is why CI never saw it and why the release binary reaches the git2 call at all. Worth a follow-up: `-c` is ambiguous for anyone building the CLI locally. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TCKh98NmUmfvgpuGyJbNu8 --- .github/workflows/tests.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ca2abd83..39415eb0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -86,7 +86,14 @@ jobs: - name: E2E — Scan fixture repo run: | + rm -rf /tmp/test-repo mkdir -p /tmp/test-repo/.github/workflows /tmp/test-repo/src + # The Rust CLI opens the target with git2 (Repository::open, + # cli/src/commands/mod.rs:38) and exits 1 on a plain directory. + # The fixture was only ever mkdir'd, so this step could never have + # passed -- the wrong binary name (exit 127) masked it. No commit + # is needed; an empty repository is enough. + git init -q /tmp/test-repo echo 'name: CI' > /tmp/test-repo/.github/workflows/ci.yml echo 'on: push' >> /tmp/test-repo/.github/workflows/ci.yml echo 'jobs:' >> /tmp/test-repo/.github/workflows/ci.yml