From d8878e592393845c819bd4d672601d587ddee515 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 01:21:37 +0000 Subject: [PATCH 1/3] ci: follow standards to metadatastician; port the template's CI hyperpolymath/standards moved to metadatastician/standards. A reusable `uses:` does not follow the rename, and a hyperpolymath repository cannot call a reusable in metadatastician's repository (rsr-template-repo#253 measured it: every caller failed at startup). Governance, Rust CI, Hypatia and Secret Scanner had stopped loading ("workflow was not found"). Ports rsr-template-repo's CI from 60cedf1 to 8034fc9, its last green main commit, by a three-way apply of the template's own diff over .github, scripts, tests, .machine_readable, verification, build/container and the K9 records, then the mint's self-name pass, as a mint would: - self-contained workflows checking out metadatastician/standards@8e48b1c; - squabble, proven-tests and fork-governance workflows; deed and K9 validation move into Dogfood Gate; - KYAML container config (data unchanged, rendered with this repo's values and canonicalised by kyaml-format.sh), K9 contract v1.0.0 records, a JCS-canonical aibdp schema; - CLAUDE.md regenerated by the arrival pack; the template's lock plus proofs.yml. Not ported: the generator tests (use-this-template mint, template-pin resolver, e2e mint tests), which exercise repo-init.just. Known gap: the self-contained governance runs one policy step where the reusable ran 19 jobs (UUID v8 conformance, licence consistency, lockfile verify, ratchets, JS runtimes); they return when standards is callable again. tests/workflows/actions_lock_sha_refs_test.sh now checks every SHA-keyed record in the lock against an exact table, keeps all its negative controls, and refuses any reusable call into the moved repository. ffi_strings_test.sh and container_kyaml_test.sh join mint-verbatim.txt: they are token fixtures the placeholder gate otherwise rejects. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LiQzWJdcPfRU6YMDGRG8Xi --- .github/actions/squabble/README.adoc | 123 +++ .github/actions/squabble/action.yml | 68 ++ .github/actions/squabble/install.sh | 146 ++++ .github/actions/squabble/run.sh | 39 + .github/hooks/pre-push | 7 +- .github/hooks/scan-secrets.sh | 2 +- .github/hooks/validate-deed.sh | 365 +-------- .github/hooks/validate-k9.sh | 407 +--------- .github/label-classifier.json | 740 +----------------- .github/labels.json | 261 +----- .github/rulesets/README.adoc | 23 +- .github/rulesets/base.json | 103 +-- .github/rulesets/branch-floor.json | 17 +- .github/rulesets/require-linear-history.json | 20 +- .github/rulesets/require-signed-commits.json | 20 +- .github/workflows/README.adoc | 27 + .github/workflows/actions.lock | 163 ++-- .github/workflows/codeql.yml | 4 +- .github/workflows/deed-validate.yml | 59 -- .github/workflows/dependabot-automerge.yml | 162 ++-- .github/workflows/docker-build.yml | 8 +- .github/workflows/dogfood-gate.yml | 438 +++++------ .github/workflows/eclexiaiser-validate.yml | 6 +- .github/workflows/empty-linter.yml | 10 +- .github/workflows/estate-rules.yml | 97 ++- .github/workflows/fork-governance.yml | 124 +++ .github/workflows/governance.yml | 59 +- .github/workflows/groove-check.yml | 22 +- .github/workflows/hypatia-scan.yml | 110 ++- .github/workflows/k9-validate.yml | 64 -- .github/workflows/label-triage.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/main-estate-audit.yml | 64 +- .github/workflows/mirror.yml | 34 +- .github/workflows/proven-tests.yml | 108 +++ .github/workflows/quality.yml | 10 +- .github/workflows/release.yml | 2 +- .github/workflows/rust-ci.yml | 41 +- .github/workflows/scorecard.yml | 28 +- .github/workflows/secret-scanner.yml | 160 +++- .github/workflows/security-policy.yml | 4 +- .github/workflows/squabble.yml | 86 ++ .github/workflows/static-analysis-gate.yml | 509 ++++++------ .github/workflows/workflow-linter.yml | 17 +- .../arrival-pack/arrival-pack.ncl | 8 + .../arrival-pack/claude-md.k9.ncl | 3 +- .machine_readable/arrival-pack/extract.sh | 2 +- .machine_readable/coaptation/coapt.k9.ncl | 3 +- .../coaptation/core/Coaptation.idr | 2 + .../coaptation/receipts/latest.toml | 2 +- .machine_readable/contractiles/README.adoc | 8 +- .../contractiles/adjust/adjust.k9.ncl | 14 +- .../contractiles/bust/bust.k9.ncl | 20 +- .../contractiles/dust/dust.k9.ncl | 14 +- .../contractiles/intend/intend.k9.ncl | 24 +- .../contractiles/must/must.k9.ncl | 24 +- .../contractiles/trust/trust.k9.ncl | 24 +- .machine_readable/mint-verbatim.txt | 36 + .machine_readable/required-contexts-allow.txt | 11 + .machine_readable/root-allow.txt | 2 +- .../scripts/maintenance/maint-assault.sh | 10 +- .machine_readable/self-validating/README.adoc | 10 +- ...setup-repo.k9.ncl => setup-repo.k9.ncl.in} | 0 .../self-validating/methodology-guard.k9.ncl | 1 + ...te-hunt.k9.ncl => template-hunt.k9.ncl.in} | 0 ...ennel.k9.ncl => template-kennel.k9.ncl.in} | 0 ...te-yard.k9.ncl => template-yard.k9.ncl.in} | 0 .../testing/adversarial-content.k9.ncl | 39 + CHANGELOG.adoc | 27 + CLAUDE.md | 49 +- build/container/README.adoc | 24 +- build/container/compose.example.yaml | 233 +++--- build/container/compose.yaml | 197 +++-- build/container/stapeln/.gatekeeper.yaml | 257 +++--- build/container/stapeln/deploy.k9.ncl | 279 ++++--- build/just/container.just | 34 +- coordination.k9.ncl | 1 + docs/architecture/REPOSITORY-MAP.adoc | 4 +- scripts/bench-stats.jq | 96 +++ scripts/check-bench-regression.sh | 157 ++++ scripts/check-escape-hatches.sh | 210 +++++ scripts/check-ijson-jcs.sh | 152 ++++ scripts/check-lock-sync.sh | 87 +- scripts/check-no-placeholders.sh | 34 +- scripts/check-required-contexts.sh | 143 ++++ scripts/check-variant-drift.sh | 13 + scripts/compute-bench-baseline.sh | 118 +++ scripts/emit-aggregate-inputs.sh | 91 +++ scripts/gen-repo-map.sh | 29 +- scripts/ijson-jcs-cli.js | 408 ++++++++++ scripts/kyaml-format.sh | 126 +++ scripts/merge-actions-lock.rs | 2 + scripts/prune-dependabot-ecosystems.rs | 2 + scripts/rsr-campaign.sh | 4 +- scripts/run-proven-suite.sh | 207 +++++ scripts/strip-instruction-blocks.rs | 112 +-- scripts/sweep-wellknown.sh | 15 +- session/custom-checks.k9.ncl | 1 + tests/ffi_strings_test.sh | 111 +++ tests/proven_suite_test.sh | 452 +++++++++++ tests/run_proven_suite_test.sh | 137 ++++ tests/workflows/actions_lock_sha_refs_test.sh | 118 ++- tests/workflows/check_ijson_jcs_test.sh | 279 +++++++ tests/workflows/check_lock_sync_test.sh | 130 +++ tests/workflows/container_kyaml_test.sh | 143 ++++ tests/workflows/dependabot_automerge_test.sh | 109 +++ tests/workflows/dogfood_badge_test.sh | 66 ++ tests/workflows/governance_policy_test.sh | 199 +++++ tests/workflows/kyaml_format_test.sh | 126 +++ tests/workflows/kyaml_ratchet_test.sh | 256 ++++++ tests/workflows/mint_cleanup_test.sh | 20 +- tests/workflows/runtime_policy_fold_test.sh | 169 ++++ tests/workflows/squabble_gate_test.sh | 390 +++++++++ verification/proofs/README.adoc | 94 ++- verification/proofs/idris2/ABI/Compliance.idr | 2 +- verification/proofs/idris2/ABI/Foreign.idr | 2 +- verification/proofs/idris2/ABI/Layout.idr | 2 +- verification/proofs/idris2/ABI/Platform.idr | 2 +- verification/proofs/idris2/ABI/Pointers.idr | 2 +- verification/proofs/idris2/Types.idr | 2 +- www/schemas/aibdp-schema-v0.2.json | 378 +-------- 121 files changed, 7516 insertions(+), 3803 deletions(-) create mode 100644 .github/actions/squabble/README.adoc create mode 100644 .github/actions/squabble/action.yml create mode 100755 .github/actions/squabble/install.sh create mode 100755 .github/actions/squabble/run.sh delete mode 100644 .github/workflows/deed-validate.yml create mode 100644 .github/workflows/fork-governance.yml delete mode 100644 .github/workflows/k9-validate.yml create mode 100644 .github/workflows/proven-tests.yml create mode 100644 .github/workflows/squabble.yml create mode 100644 .machine_readable/mint-verbatim.txt create mode 100644 .machine_readable/required-contexts-allow.txt rename .machine_readable/self-validating/examples/{setup-repo.k9.ncl => setup-repo.k9.ncl.in} (100%) rename .machine_readable/self-validating/{template-hunt.k9.ncl => template-hunt.k9.ncl.in} (100%) rename .machine_readable/self-validating/{template-kennel.k9.ncl => template-kennel.k9.ncl.in} (100%) rename .machine_readable/self-validating/{template-yard.k9.ncl => template-yard.k9.ncl.in} (100%) create mode 100644 .machine_readable/testing/adversarial-content.k9.ncl create mode 100644 scripts/bench-stats.jq create mode 100755 scripts/check-bench-regression.sh create mode 100755 scripts/check-escape-hatches.sh create mode 100755 scripts/check-ijson-jcs.sh create mode 100755 scripts/check-required-contexts.sh create mode 100755 scripts/compute-bench-baseline.sh create mode 100755 scripts/emit-aggregate-inputs.sh create mode 100755 scripts/ijson-jcs-cli.js create mode 100755 scripts/kyaml-format.sh create mode 100755 scripts/run-proven-suite.sh create mode 100644 tests/ffi_strings_test.sh create mode 100755 tests/proven_suite_test.sh create mode 100644 tests/run_proven_suite_test.sh create mode 100755 tests/workflows/check_ijson_jcs_test.sh create mode 100644 tests/workflows/check_lock_sync_test.sh create mode 100644 tests/workflows/container_kyaml_test.sh create mode 100644 tests/workflows/dependabot_automerge_test.sh create mode 100644 tests/workflows/dogfood_badge_test.sh create mode 100644 tests/workflows/governance_policy_test.sh create mode 100644 tests/workflows/kyaml_format_test.sh create mode 100644 tests/workflows/kyaml_ratchet_test.sh create mode 100644 tests/workflows/runtime_policy_fold_test.sh create mode 100755 tests/workflows/squabble_gate_test.sh diff --git a/.github/actions/squabble/README.adoc b/.github/actions/squabble/README.adoc new file mode 100644 index 0000000..65e5425 --- /dev/null +++ b/.github/actions/squabble/README.adoc @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) 2026 Jonathan D.A. Jewell += squabble action — install the pinned, attested squabble binary +:toc: preamble + +A composite action that installs one exact squabble release binary in a +workflow and, optionally, runs it. It refuses any file whose sha256 is not +the pinned digest, and any file whose build provenance does not trace back +to this repository's release workflow at the pinned tag and source commit. +Nothing is executed until both checks pass. + +== Usage + +Pin the action to a full commit SHA of this repository, never to a branch or +tag: + +[source,yaml] +---- +# Install only: squabble is on PATH from the next step on. +- uses: hyperpolymath/cicd-squabbler/.github/actions/squabble@<40-hex sha> + id: squabble +- run: squabble --version +---- + +Run mode passes `args` to squabble, one argument per line, taken literally +(no shell parsing, no globbing; blank lines are dropped). The step fails +when squabble exits non-zero, and `run.sh` records the exit code as the +`exit-code` output (see <<_not_yet_tested>> for what CI has not yet shown). + +[source,yaml] +---- +- uses: hyperpolymath/cicd-squabbler/.github/actions/squabble@<40-hex sha> + with: + args: | + verify-satisfied + ${{ github.repository }} + ${{ github.event.pull_request.number }} + token: ${{ steps.app-token.outputs.token }} +---- + +=== Inputs and outputs + +[cols="1,3"] +|=== +| `args` (input) | squabble's arguments, one per line. Empty (the default) means install only. +| `token` (input) | `GH_TOKEN` for squabble in run mode. Defaults to `github.token`. `verify-satisfied` reads branch rules, which need *Administration: read*; `github.token` cannot hold that, so pass a GitHub App installation token for it. +| `path` (output) | Absolute path of the installed binary. +| `exit-code` (output) | squabble's exit code in run mode; empty when `args` is empty. +|=== + +The download and the attestation check always use the workflow's own +`github.token`, never `token`: an App token minted for the caller's +repository need not reach this one. This repository is public, so +`github.token` is enough for both. + +== What is checked, in order + +`install.sh` does all of this before the binary is made executable: + +. The runner is Linux on x86_64. That is the only asset published. +. The release asset is downloaded into a fresh directory. +. Its sha256 must equal the pinned digest. +. `gh attestation verify --format json` must accept it with this repository, + the signer workflow `.github/workflows/release.yml`, the source ref + `refs/tags/`, the pinned source commit, the SLSA provenance v1 + predicate, and `--deny-self-hosted-runners`. The JSON is then read + back: it must be a non-empty array in which every result names the asset + with the pinned digest. The JSON form is used because the plain-text output + has been seen empty, with exit 0, when stdout is not a terminal. +. Only then is the file made executable, and it must report the pinned + version (`squabble 0.1.0`). + +== Pins + +[cols="1,3"] +|=== +| Release tag | `v0.1.0` (a locator, not a trust anchor) +| Asset | `squabble-x86_64-linux-musl` +| Asset sha256 | `6cbeb4577d83ccf2dea3405a3afb0d34b7fee422af73f2113d413ea7edba026c` +| Source commit | `b854d17abc0dce296719a349ebe856ff945cf03e` +| Signer workflow | `hyperpolymath/cicd-squabbler/.github/workflows/release.yml` +|=== + +== Bumping to a new release + +Read every value from the release itself, never from a tag name alone. + +. `gh release download --repo hyperpolymath/cicd-squabbler --pattern + squabble-x86_64-linux-musl --pattern SHA256SUMS`, then `sha256sum -c + SHA256SUMS --ignore-missing`. +. Find the commit the tag points to: `git rev-parse ^{commit}`. +. Run `gh attestation verify` on the asset with the flags `install.sh` uses + and that commit as `--source-digest`. It must pass. +. Set `SQUABBLE_TAG`, `SQUABBLE_SHA256` and `SQUABBLE_SOURCE_DIGEST` in + `install.sh`, and update any version reference in + `.github/workflows/squabble.yml`. +. Run `bash tests/workflows/squabble_gate_test.sh`. It must end with every + planned case run and none failed. + +== How it is tested + +`tests/workflows/squabble_gate_test.sh` exercises the action guards and the +companion required-context gate with Silence + Firing + Setup cases: the +`verify_digest` SHA256 mismatch check, the `check_attestation_json` SLSA +attestation checks (empty array, matching digest, mismatched digest), +`run.sh` exit-code propagation (`0` and blocking exit `4`) against a stand-in +binary, and `scripts/check-required-contexts.sh` against missing producers, +path-filtered workflows, unmapped ruleset contexts, and `gh pr merge --admin` +bypass invocations. + +`.github/workflows/squabble.yml` runs `install.sh` and `run.sh` on a real +runner against this repository's `.github/workflows` directory and verifies +`scripts/check-required-contexts.sh`. + +=== Not yet tested + +GitHub's own evaluation of `action.yml` (composite inputs, outputs and the +`if:` on the run step) is not exercised here. This repository has an +`actions.lock`, and the lockfile does not support local-path actions, so a +workflow here cannot call the action as `uses: ./.github/actions/squabble`. +It is first exercised by a caller that pins the action at a merged commit of +`main`, which the lockfile does support. That caller should check in +particular that `exit-code` is readable when the run step fails. diff --git a/.github/actions/squabble/action.yml b/.github/actions/squabble/action.yml new file mode 100644 index 0000000..a42a408 --- /dev/null +++ b/.github/actions/squabble/action.yml @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# squabble: install the pinned, attested squabble release binary and, +# optionally, run it. Call it pinned to a commit of this repository: +# +# uses: hyperpolymath/cicd-squabbler/.github/actions/squabble@<40-hex sha> +# +# The binary itself is pinned inside install.sh by sha256 and by its +# build-provenance attestation. See README.adoc next to this file. +# +# This file is KYAML (standards YAML-POLICY, D280). Regenerate it with +# standards/scripts/kyaml-format.sh after an edit; keep each run: to one line +# that calls a script, so the KYAML stays readable. +{ + name: "squabble", + description: "Install the pinned squabble release (sha256 and build attestation checked), and optionally run it.", + author: "hyperpolymath", + inputs: { + args: { + description: "Arguments for squabble, one per line, taken literally. Empty means install only.", + required: false, + default: "", + }, + token: { + description: "GH_TOKEN for squabble itself in run mode. verify-satisfied needs Administration:read, which github.token cannot hold, so pass an App token for it. Installing always uses github.token.", + required: false, + default: "${{ github.token }}", + }, + }, + outputs: { + path: { + description: "Absolute path of the installed squabble binary.", + value: "${{ steps.install.outputs.path }}", + }, + exit-code: { + description: "squabble's exit code in run mode; empty when args is empty.", + value: "${{ steps.run.outputs.exit-code }}", + }, + }, + runs: { + using: "composite", + steps: [ + # Install with the workflow's own token, never the caller's run-mode token: + # an App installation token for the caller's repo need not reach this one. + { + name: "Install pinned squabble (sha256 + attestation)", + id: "install", + shell: "bash", + env: { + GH_TOKEN: "${{ github.token }}", + }, + run: "bash \"${GITHUB_ACTION_PATH}/install.sh\"", + }, + { + name: "Run squabble", + id: "run", + if: "inputs.args != ''", + shell: "bash", + env: { + GH_TOKEN: "${{ inputs.token }}", + SQUABBLE_ARGS: "${{ inputs.args }}", + SQUABBLE_BIN: "${{ steps.install.outputs.path }}", + }, + run: "bash \"${GITHUB_ACTION_PATH}/run.sh\"", + }, + ], + }, +} diff --git a/.github/actions/squabble/install.sh b/.github/actions/squabble/install.sh new file mode 100755 index 0000000..8c9de84 --- /dev/null +++ b/.github/actions/squabble/install.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# install.sh — install the pinned squabble release binary, or refuse. +# +# The order is the whole point of this file: +# +# 1. download the release asset into a fresh directory; +# 2. refuse it unless its sha256 equals SQUABBLE_SHA256, before anything +# else reads or runs the file; +# 3. refuse it unless `gh attestation verify` proves it was built by this +# repository's release.yml, from the pinned tag AND the pinned source +# commit, on a GitHub-hosted runner, and the attested subject for the +# asset carries that same digest; +# 4. only then make it executable, smoke-test its version and put it on PATH. +# +# The tag is a locator, not a trust anchor: the digest is the anchor, and the +# attestation ties that digest to the source commit. The plain-text output of +# `gh attestation verify` has been seen EMPTY with exit 0 when stdout is not a +# terminal (2026-10-09), so the JSON form is read and checked here instead. +# +# Bumping squabble means changing SQUABBLE_TAG, SQUABBLE_SHA256 and +# SQUABBLE_SOURCE_DIGEST to a new release's values, each read from that +# release itself, never copied from a tag name; README.adoc next to this file +# gives the steps. +# +# Sourcing this file defines the functions without running main, so +# tests/squabble_action_test.sh can exercise each guard on its own. + +set -euo pipefail + +readonly SQUABBLE_REPO="hyperpolymath/cicd-squabbler" +readonly SQUABBLE_TAG="v0.1.0" +readonly SQUABBLE_ASSET="squabble-x86_64-linux-musl" +readonly SQUABBLE_SHA256="6cbeb4577d83ccf2dea3405a3afb0d34b7fee422af73f2113d413ea7edba026c" +readonly SQUABBLE_SOURCE_DIGEST="b854d17abc0dce296719a349ebe856ff945cf03e" +readonly SQUABBLE_SIGNER_WORKFLOW="hyperpolymath/cicd-squabbler/.github/workflows/release.yml" + +# die MESSAGE... — print MESSAGE as a GitHub Actions error annotation and exit 1. +die() { + printf '::error::%s\n' "$*" >&2 + exit 1 +} + +# require_platform — refuse any machine that cannot execute the x86_64 Linux asset. +require_platform() { + local os arch + os="$(uname -s)" + arch="$(uname -m)" + if [[ "$os" != "Linux" || "$arch" != "x86_64" ]]; then + die "squabble action supports only x86_64 Linux runners (this is ${os}/${arch})" + fi +} + +# verify_digest FILE — succeed only if FILE exists and its sha256 equals SQUABBLE_SHA256. +verify_digest() { + local file="$1" actual + if [[ ! -f "$file" ]]; then + printf '::error::%s does not exist\n' "$file" >&2 + return 1 + fi + actual="$(sha256sum -- "$file")" + actual="${actual%% *}" + if [[ "$actual" != "$SQUABBLE_SHA256" ]]; then + printf '::error::%s has sha256 %s, expected %s; refusing it\n' \ + "$file" "$actual" "$SQUABBLE_SHA256" >&2 + return 1 + fi +} + +# check_attestation_json FILE — succeed only if FILE holds a non-empty JSON array +# of verification results, each of which attests SQUABBLE_ASSET, and only with +# the digest SQUABBLE_SHA256. Other subjects in the same statement (the release +# also attests SHA256SUMS) are allowed; an empty array is a refusal, not a pass. +check_attestation_json() { + local json="$1" + jq -e --arg name "$SQUABBLE_ASSET" --arg sha "$SQUABBLE_SHA256" ' + type == "array" and length > 0 and all(.[]; + [.verificationResult.statement.subject[]? + | select(.name == $name) | .digest.sha256] as $d + | ($d | length) > 0 and all($d[]; . == $sha)) + ' "$json" >/dev/null +} + +# verify_attestation FILE — succeed only if GitHub's build-provenance attestation +# for FILE was signed by SQUABBLE_SIGNER_WORKFLOW at SQUABBLE_TAG, built from +# SQUABBLE_SOURCE_DIGEST on a GitHub-hosted runner, and names the pinned digest. +verify_attestation() { + local file="$1" json rc=0 + json="${file}.attestation.json" + gh attestation verify "$file" \ + --repo "$SQUABBLE_REPO" \ + --signer-workflow "$SQUABBLE_SIGNER_WORKFLOW" \ + --source-ref "refs/tags/${SQUABBLE_TAG}" \ + --source-digest "$SQUABBLE_SOURCE_DIGEST" \ + --predicate-type "https://slsa.dev/provenance/v1" \ + --deny-self-hosted-runners \ + --format json >"$json" || rc=$? + if (( rc != 0 )); then + printf '::error::gh attestation verify refused %s (exit %d)\n' "$file" "$rc" >&2 + return 1 + fi + if ! check_attestation_json "$json"; then + printf '::error::the attestation for %s does not name %s with sha256 %s\n' \ + "$file" "$SQUABBLE_ASSET" "$SQUABBLE_SHA256" >&2 + return 1 + fi +} + +# main — download, verify and install squabble; on success append its directory +# to GITHUB_PATH and write `path=` to GITHUB_OUTPUT when those are set. +main() { + local work asset bin version + require_platform + work="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/squabble.XXXXXX")" + mkdir -p "${work}/bin" + gh release download "$SQUABBLE_TAG" \ + --repo "$SQUABBLE_REPO" \ + --pattern "$SQUABBLE_ASSET" \ + --dir "${work}/download" + asset="${work}/download/${SQUABBLE_ASSET}" + + verify_digest "$asset" || die "squabble ${SQUABBLE_TAG}: download does not match the pinned sha256" + verify_attestation "$asset" || die "squabble ${SQUABBLE_TAG}: attestation check failed" + + bin="${work}/bin/squabble" + mv -- "$asset" "$bin" + chmod 0755 "$bin" + version="$("$bin" --version)" + if [[ "$version" != "squabble ${SQUABBLE_TAG#v}" ]]; then + die "installed binary reports '${version}', expected 'squabble ${SQUABBLE_TAG#v}'" + fi + + if [[ -n "${GITHUB_PATH:-}" ]]; then + printf '%s\n' "${work}/bin" >>"$GITHUB_PATH" + fi + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + printf 'path=%s\n' "$bin" >>"$GITHUB_OUTPUT" + fi + printf 'installed %s at %s (sha256:%s, attestation verified)\n' \ + "$version" "$bin" "$SQUABBLE_SHA256" >&2 +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi diff --git a/.github/actions/squabble/run.sh b/.github/actions/squabble/run.sh new file mode 100755 index 0000000..339805f --- /dev/null +++ b/.github/actions/squabble/run.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# run.sh — run the installed squabble with the arguments in SQUABBLE_ARGS, +# record its exit code as the step output `exit-code`, and exit with it. +# +# SQUABBLE_ARGS holds one argument per line, taken literally: no shell +# parsing, no globbing, no word splitting. Blank lines are dropped, so the +# trailing newline of a YAML block scalar adds no empty argument. +# +# The exit code is captured with `|| rc=$?`. The runner's shell is +# `bash -e`, under which `cmd; rc=$?` never reaches the assignment. + +set -euo pipefail + +# main — split SQUABBLE_ARGS on newlines, run SQUABBLE_BIN with them, write +# exit-code to GITHUB_OUTPUT when it is set, and exit with squabble's code. +main() { + local line rc=0 + local -a args=() + if [[ ! -x "${SQUABBLE_BIN:-}" ]]; then + printf '::error::SQUABBLE_BIN (%s) is not an executable file\n' "${SQUABBLE_BIN:-}" >&2 + exit 1 + fi + while IFS= read -r line; do + if [[ -n "$line" ]]; then + args+=("$line") + fi + done <<<"${SQUABBLE_ARGS:-}" + + "$SQUABBLE_BIN" "${args[@]}" || rc=$? + + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + printf 'exit-code=%d\n' "$rc" >>"$GITHUB_OUTPUT" + fi + exit "$rc" +} + +main "$@" diff --git a/.github/hooks/pre-push b/.github/hooks/pre-push index 7e00686..7174c81 100755 --- a/.github/hooks/pre-push +++ b/.github/hooks/pre-push @@ -6,9 +6,10 @@ # # Runs the SAME DEED + K9 validators the CI Dogfood Gate runs, but here on # your machine before the push leaves, so format drift is caught in seconds -# instead of surfacing as red CI minutes later. The vendored validators in -# this directory are byte-identical to the pinned CI action scripts and are -# refreshed by the estate `refresh-githooks` sweep. +# instead of surfacing as red CI minutes later. The two scripts here are thin +# wrappers over the canonical linter and validator in a hyperpolymath/standards +# checkout (STANDARDS_DIR, default ../standards); without one they skip with a +# notice and the CI lanes remain the gate. # # Enable once per clone: # git config core.hooksPath .github/hooks diff --git a/.github/hooks/scan-secrets.sh b/.github/hooks/scan-secrets.sh index b26dd40..3ecb23f 100755 --- a/.github/hooks/scan-secrets.sh +++ b/.github/hooks/scan-secrets.sh @@ -162,7 +162,7 @@ scan() { } if [ -n "${K9_PUSH_RANGES:-}" ]; then - while read -r local_ref local_sha remote_ref remote_sha; do + while read -r local_ref local_sha _remote_ref remote_sha; do [ -z "${local_sha:-}" ] && continue [ "$local_sha" = "$ZERO" ] && continue # branch deletion if [ "${remote_sha:-$ZERO}" = "$ZERO" ]; then diff --git a/.github/hooks/validate-deed.sh b/.github/hooks/validate-deed.sh index 226a27a..770ba69 100755 --- a/.github/hooks/validate-deed.sh +++ b/.github/hooks/validate-deed.sh @@ -2,351 +2,56 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # -# validate-deed.sh — DEED manifest validation script +# validate-deed.sh — pre-push deed check, through the canonical linter. # -# Scans for .deed files and validates the checks below. A .a2ml file is an -# error on sight: the format is retired estate-wide and its facts belong in -# the repo deed. Checks: -# 1. Required fields: agent-id or pedigree name, version -# 2. SPDX-License-Identifier header presence -# 3. Attestation block structure (if present) -# 4. Section heading syntax ([section] or ## section) +# One validator per family (hyperpolymath/standards, +# docs/CONTRACTILE-DESCRIPTILE-VALIDATION.adoc): the grep checks that used to +# live here were a second validator. This runs 1-formats/deed/tools/deed_lint.js +# (Bun) from a standards checkout over the tracked .deed files, as the +# deed-conformance lane does in CI (.github/workflows/dogfood-gate.yml). The +# lane is the gate; this is the early warning. A tracked .a2ml file is an +# error on sight: the format is retired estate-wide (D99, D312) and its facts +# belong in the repo deed. # # Environment variables: -# INPUT_PATH — Directory to scan (default: .) -# INPUT_STRICT — Promote warnings to errors (default: false) +# INPUT_PATH — Repository to check (default: .) +# STANDARDS_DIR — A hyperpolymath/standards checkout +# (default: ../standards beside the repository) # # Exit codes: -# 0 — All files valid (or only warnings in non-strict mode) -# 1 — Validation errors found +# 0 — Every tracked deed lints, or no linter is available (skipped with a +# notice: CI still runs the lane) +# 1 — A deed failed, or a .a2ml file is tracked set -euo pipefail -# --------------------------------------------------------------------------- -# Configuration -# --------------------------------------------------------------------------- +REPO="${INPUT_PATH:-.}" +STANDARDS_DIR="${STANDARDS_DIR:-$(cd "$REPO/.." && pwd)/standards}" +LINTER="$STANDARDS_DIR/1-formats/deed/tools/deed_lint.js" -SCAN_PATH="${INPUT_PATH:-.}" -STRICT="${INPUT_STRICT:-false}" -PATHS_IGNORE_RAW="${INPUT_PATHS_IGNORE:-}" -GITHUB_OUTPUT_FILE="${GITHUB_OUTPUT:-/dev/null}" +cd "$REPO" +errors=0 -# Parse paths-ignore: newline-separated fragments, blank lines and # comments -# stripped. Each fragment is a substring match against the file path. Pattern -# adopted from hyperpolymath/hypatia#243 — content-pattern validators must -# distinguish a target from a vendored / fixture file that legitimately -# contains the very pattern being checked. -PATHS_IGNORE=() -while IFS= read -r _frag; do - # Strip leading and trailing whitespace (canonical bash idiom). - _frag="${_frag#"${_frag%%[![:space:]]*}"}" - _frag="${_frag%"${_frag##*[![:space:]]}"}" - [[ -z "$_frag" || "$_frag" == \#* ]] && continue - PATHS_IGNORE+=("$_frag") -done <<< "$PATHS_IGNORE_RAW" - -# Returns 0 if path should be skipped (matches any ignore fragment) -path_ignored() { - local p="$1" frag - for frag in "${PATHS_IGNORE[@]}"; do - [[ "$p" == *"$frag"* ]] && return 0 - done - return 1 -} - -# Counters -FILES_SCANNED=0 -ERRORS=0 -WARNINGS=0 - -# --------------------------------------------------------------------------- -# Helper: emit GitHub annotation -# --------------------------------------------------------------------------- -# Usage: annotate -# level: error | warning | notice -annotate() { - local level="$1" file="$2" line="$3" message="$4" - echo "::${level} file=${file},line=${line}::${message}" -} - -# --------------------------------------------------------------------------- -# Helper: report issue (respects strict mode) -# --------------------------------------------------------------------------- -# Usage: report_issue -# severity: error | warning -report_issue() { - local severity="$1" file="$2" line="$3" message="$4" - - if [[ "$severity" == "warning" && "$STRICT" == "true" ]]; then - severity="error" - fi - - annotate "$severity" "$file" "$line" "$message" - - if [[ "$severity" == "error" ]]; then - ERRORS=$((ERRORS + 1)) - else - WARNINGS=$((WARNINGS + 1)) - fi -} - -# --------------------------------------------------------------------------- -# Validator: check a single .deed file -# --------------------------------------------------------------------------- -validate_deed() { - local file="$1" - FILES_SCANNED=$((FILES_SCANNED + 1)) - - # --- Check 1: SPDX header --- - # The SPDX-License-Identifier should appear in the first 10 lines - local has_spdx=false - local line_num=0 - while IFS= read -r line; do - line_num=$((line_num + 1)) - if [[ $line_num -gt 10 ]]; then - break - fi - if [[ "$line" == *"SPDX-License-Identifier"* ]]; then - has_spdx=true - break - fi - done < "$file" - - if [[ "$has_spdx" == "false" ]]; then - report_issue "warning" "$file" 1 \ - "Missing SPDX-License-Identifier in first 10 lines" - fi - - # --- Check 2: Required identity fields --- - # DEED files must contain either: - # - agent-id = "..." or agent_id = "..." - # - pedigree block with name field - # - name = "..." at top level (for AI manifests) - local has_identity=false - local has_version=false - local first_form_seen=false - line_num=0 - - while IFS= read -r line; do - line_num=$((line_num + 1)) - - # Check for identity fields (various DEED patterns) - # TOML/kv form: `name = "..."`, `project = "..."`, `agent-id = "..."`. - - if [[ "$line" =~ ^[[:space:]]*(agent[-_]id|name|project|archetype)[[:space:]]*= ]]; then - has_identity=true - fi - # S-expression form: `(name "...")`, `(project "...")`, - # `(agent-id "...")`. Some DEED dialects (audit registries, - # classification stores) use Lisp-style s-expressions for the - # metadata block instead of TOML. Identity carries the same - # semantics; only the syntax differs. Match at any indent so it - # also picks up entries nested under `(metadata ...)`. - if [[ "$line" =~ ^[[:space:]]*\([[:space:]]*(agent[-_]id|name|project)[[:space:]]+\" ]]; then - has_identity=true - fi - # Colon / brace-block form: `name: "..."`, `id: "..."`, `project: "..."`. - # YAML-ish and brace-block DEED dialects (e.g. `Trust { name: "..." }`, - # `id: "tsdm-standard"`) carry the same identity semantics; only the - # delimiter (`:` vs `=`) differs. `id` is the brace-block spelling of an - # identity key. - if [[ "$line" =~ ^[[:space:]]*(agent[-_]id|name|project|id)[[:space:]]*: ]]; then - has_identity=true - fi - # DEED s-expression head form: `(estate-deed`, `(repo-deed`, - # `(estate-atlas-deed`, `(praxis-deed`. Per DEED-GRAMMAR-SPEC - # <>, a file whose first form is one of the four declared - # heads is a deed of that kind, and the head satisfies the structural - # half of identity. Only the FIRST form is eligible — checking every - # line would let a malformed file open with some other form and append - # a deed head lower down to buy identity. Ported from - # hyperpolymath/deed-ecosystem validate-action/validate-a2ml.sh so the - # local hook and the CI action agree on what a deed is. - if [[ "$first_form_seen" == "false" && "$line" =~ ^[[:space:]]*\( ]]; then - first_form_seen=true - if [[ "$line" =~ ^[[:space:]]*\((estate-deed|repo-deed|estate-atlas-deed|praxis-deed)([[:space:]]|$) ]]; then - has_identity=true - fi - fi - # DEED keyword identity form: `:canonical-name "..."` and the two other - # identity keywords the spec names. Leading colon: none of the forms - # above match it, because they test the bare words. - if [[ "$line" =~ ^[[:space:]]*:(canonical-name|estate-authority|agent-id)[[:space:]] ]]; then - has_identity=true - fi - # Check for version field — TOML form - if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*= ]]; then - has_version=true - fi - # Version field — s-expression form - if [[ "$line" =~ ^[[:space:]]*\([[:space:]]*(version|schema_version)[[:space:]]+\" ]]; then - has_version=true - fi - # Version field — colon / brace-block form - if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*: ]]; then - has_version=true - fi - # DEED keyword version form: `:schema-version "1.0.0"` — leading colon, - # hyphenated, REQUIRED on all four deed heads. The three patterns above - # spell it `schema_version` with no leading colon, so a conforming deed - # matched none of them. `:registry-version` is a distinct, optional - # atlas field and never satisfies the version requirement. - if [[ "$line" =~ ^[[:space:]]*:schema-version[[:space:]] ]]; then - has_version=true - fi - done < "$file" - - # Canonical structured DEED tree. Everything under a `.machine_readable/` - # directory is a typed agent-readable doc (CLADE, ANCHOR, STATE, - # ECOSYSTEM, bot_directives/{debt,coverage,methodology}, ai/AI, - # policies/*, integrations/*, …). Per the RSR convention these carry - # identity structurally — owning repo + path + filename — not via an - # in-file `name`/`agent-id`. This generalises the `.machine_readable/descriptiles/` - # rationale above to the whole tree: resource-algebra itself ships these - # files without an in-file identity key, so requiring one produces - # estate-wide false positives on every repo built from the canonical - # template. Files outside `.machine_readable/` are still validated. - local is_structural_identity=false - if [[ "$file" == *"/.machine_readable/"* || "$file" == "./.machine_readable/"* || "$file" == ".machine_readable/"* ]]; then - is_structural_identity=true - fi - - if [[ "$has_identity" == "false" && "$is_structural_identity" == "false" ]]; then - report_issue "error" "$file" 1 \ - "Missing required identity field (agent-id, name, or project)" - fi - - if [[ "$has_version" == "false" && "$is_structural_identity" == "false" ]]; then - report_issue "warning" "$file" 1 \ - "Missing version or schema_version field" - fi - - # --- Check 3: Attestation block structure --- - # If file contains [attestation] or ## ATTESTATION, validate it has - # required sub-fields: proof or signature - local in_attestation=false - local attestation_line=0 - local attestation_has_content=false - line_num=0 - - while IFS= read -r line; do - line_num=$((line_num + 1)) - - # Detect attestation section start - if [[ "$line" =~ ^\[attestation\] ]] || [[ "$line" =~ ^##[[:space:]]+[Aa]ttestation ]] || [[ "$line" =~ ^##[[:space:]]+ATTESTATION ]]; then - in_attestation=true - attestation_line=$line_num - continue - fi - - # Detect next section (ends attestation block) - if [[ "$in_attestation" == "true" ]]; then - if [[ "$line" =~ ^\[.+\] ]] || [[ "$line" =~ ^##[[:space:]] ]]; then - in_attestation=false - continue - fi - # Check for content in attestation block - if [[ "$line" =~ (proof|signature|verified|hash)[[:space:]]*= ]]; then - attestation_has_content=true - fi - fi - done < "$file" - - if [[ $attestation_line -gt 0 && "$attestation_has_content" == "false" ]]; then - report_issue "warning" "$file" "$attestation_line" \ - "Attestation block found but missing proof/signature/hash fields" - fi - - # --- Check 4: Section heading syntax --- - # Validate that [section] headings are well-formed (no unclosed brackets) - line_num=0 - while IFS= read -r line; do - line_num=$((line_num + 1)) - # Lines starting with [ should have a matching ] - if [[ "$line" =~ ^\[ && ! "$line" =~ ^\[.+\] ]]; then - # Exclude markdown-style links and multi-line values - if [[ ! "$line" =~ ^\[.*\]\( && ! "$line" =~ ^\[TODO && ! "$line" =~ ^\[YOUR ]]; then - report_issue "warning" "$file" "$line_num" \ - "Possibly malformed section heading: unclosed bracket" - fi - fi - done < "$file" -} - -# --------------------------------------------------------------------------- -# Main: refuse retired .a2ml files, then discover and validate .deed files -# --------------------------------------------------------------------------- - -echo "::group::DEED Manifest Validation" -echo "Scanning ${SCAN_PATH} for .deed files..." -echo "" - -# The .a2ml format is retired: every such file is an error, whatever its content. -mapfile -t a2ml_files < <(find "$SCAN_PATH" -iname '*.a2ml' -not -path '*/.git/*' -type f | sort) -for _f in "${a2ml_files[@]}"; do - path_ignored "$_f" && continue - report_issue "error" "$_f" 1 "Retired .a2ml file: move its facts into the repo deed and delete it" +mapfile -t a2ml < <(git ls-files -- '*.a2ml') +for f in "${a2ml[@]}"; do + echo "::error file=$f::.a2ml is retired; move its facts into the repo deed and delete it" + errors=$((errors + 1)) done -# Find all deed manifests, excluding .git -mapfile -t deed_candidates < <(find "$SCAN_PATH" -name '*.deed' -not -path '*/.git/*' -type f | sort) - -# Apply paths-ignore filter -deed_files=() -SKIPPED=0 -for _f in "${deed_candidates[@]}"; do - if path_ignored "$_f"; then - SKIPPED=$((SKIPPED + 1)) - continue - fi - deed_files+=("$_f") -done - -if [[ $SKIPPED -gt 0 ]]; then - echo "::notice::Skipped ${SKIPPED} file(s) matching paths-ignore" -fi - -if [[ ${#deed_files[@]} -eq 0 ]]; then - echo "::notice::No .deed files found in ${SCAN_PATH}" - echo "files_scanned=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true - echo "errors=${ERRORS}" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true - echo "warnings=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true - echo "::endgroup::" - # A retired .a2ml file is still an error when no deed is present. - [[ $ERRORS -eq 0 ]] || exit 1 +if [ ! -f "$LINTER" ] || ! command -v bun >/dev/null 2>&1; then + echo "::notice::DEED: no deed linter here (needs Bun and a standards checkout at $STANDARDS_DIR; set STANDARDS_DIR); the deed-conformance lane lints in CI" + [ "$errors" -eq 0 ] || exit 1 exit 0 fi -echo "Found ${#deed_files[@]} .deed file(s)" -echo "" - -for file in "${deed_files[@]}"; do - echo " Validating: ${file}" - validate_deed "$file" +mapfile -t deeds < <(git ls-files -- '*.deed' | sort) +for f in "${deeds[@]}"; do + echo " Linting: $f" + if ! bun "$LINTER" "$f"; then + errors=$((errors + 1)) + fi done -echo "" -echo "────────────────────────────────────────" -echo "Files scanned: ${FILES_SCANNED}" -echo "Errors: ${ERRORS}" -echo "Warnings: ${WARNINGS}" -echo "Strict mode: ${STRICT}" -echo "────────────────────────────────────────" - -# Write outputs for GitHub Actions -{ - echo "files_scanned=${FILES_SCANNED}" - echo "errors=${ERRORS}" - echo "warnings=${WARNINGS}" -} >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true - -echo "::endgroup::" - -# Exit with failure if errors were found -if [[ $ERRORS -gt 0 ]]; then - echo "::error::DEED validation failed with ${ERRORS} error(s)" - exit 1 -fi - +echo "DEED: ${#deeds[@]} file(s), ${errors} failing (linter: $LINTER)" +[ "$errors" -eq 0 ] || { echo "::error::DEED validation failed for ${errors} file(s)"; exit 1; } echo "DEED validation passed." -exit 0 diff --git a/.github/hooks/validate-k9.sh b/.github/hooks/validate-k9.sh index 02845a4..4fff253 100755 --- a/.github/hooks/validate-k9.sh +++ b/.github/hooks/validate-k9.sh @@ -2,388 +2,61 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # -# validate-k9.sh — K9 configuration file validation script +# validate-k9.sh — pre-push K9 check, through the canonical validator. # -# Scans for .k9 and .k9.ncl files and validates: -# 1. K9! magic number on line 1 -# 2. Pedigree block presence with required fields (name, version) -# 3. Security level is one of: kennel, yard, hunt (case-insensitive) -# 4. Hunt-level files must have a signature or signature_required field -# 5. SPDX-License-Identifier header presence +# One validator per family (hyperpolymath/standards, +# docs/CONTRACTILE-DESCRIPTILE-VALIDATION.adoc): the grep checks that used to +# live here were a second validator, and they accepted files that contract +# v1.0.0 (D173) rejects. This runs 1-formats/k9/tools/k9-validate.sh from a +# standards checkout over the tracked K9 files, strict, layers L0–L2 (L0–L1 +# when nickel is not installed), as the k9-conformance lane does in CI (.github/workflows/dogfood-gate.yml). The lane +# is the gate; this is the early warning. # # Environment variables: -# INPUT_PATH — Directory to scan (default: .) -# INPUT_STRICT — Promote warnings to errors (default: false) +# INPUT_PATH — Repository to check (default: .) +# STANDARDS_DIR — A hyperpolymath/standards checkout +# (default: ../standards beside the repository) # # Exit codes: -# 0 — All files valid (or only warnings in non-strict mode) -# 1 — Validation errors found +# 0 — Every tracked K9 file passes, or no validator is available (skipped +# with a notice: CI still runs the lane) +# 1 — A file failed set -euo pipefail -# --------------------------------------------------------------------------- -# Configuration -# --------------------------------------------------------------------------- +REPO="${INPUT_PATH:-.}" +STANDARDS_DIR="${STANDARDS_DIR:-$(cd "$REPO/.." && pwd)/standards}" +VALIDATOR="$STANDARDS_DIR/1-formats/k9/tools/k9-validate.sh" -SCAN_PATH="${INPUT_PATH:-.}" -STRICT="${INPUT_STRICT:-false}" -PATHS_IGNORE_RAW="${INPUT_PATHS_IGNORE:-}" -GITHUB_OUTPUT_FILE="${GITHUB_OUTPUT:-/dev/null}" - -# Parse paths-ignore: newline-separated fragments, blank lines and # comments -# stripped. Each fragment is a substring match against the file path. Pattern -# adopted from hyperpolymath/hypatia#243 — content-pattern validators must -# distinguish a target from a vendored / fixture file that legitimately -# contains the very pattern being checked. -PATHS_IGNORE=() -while IFS= read -r _frag; do - # Strip leading and trailing whitespace (canonical bash idiom). - _frag="${_frag#"${_frag%%[![:space:]]*}"}" - _frag="${_frag%"${_frag##*[![:space:]]}"}" - [[ -z "$_frag" || "$_frag" == \#* ]] && continue - PATHS_IGNORE+=("$_frag") -done <<< "$PATHS_IGNORE_RAW" - -# Returns 0 if path should be skipped (matches any ignore fragment) -path_ignored() { - local p="$1" frag - for frag in "${PATHS_IGNORE[@]}"; do - [[ "$p" == *"$frag"* ]] && return 0 - done - return 1 -} - -# Counters -FILES_SCANNED=0 -ERRORS=0 -WARNINGS=0 - -# Valid security levels (the leash metaphor) -VALID_LEVELS="kennel yard hunt" - -# --------------------------------------------------------------------------- -# Helper: emit GitHub annotation -# --------------------------------------------------------------------------- -annotate() { - local level="$1" file="$2" line="$3" message="$4" - echo "::${level} file=${file},line=${line}::${message}" -} - -# --------------------------------------------------------------------------- -# Helper: report issue (respects strict mode) -# --------------------------------------------------------------------------- -report_issue() { - local severity="$1" file="$2" line="$3" message="$4" - - if [[ "$severity" == "warning" && "$STRICT" == "true" ]]; then - severity="error" - fi - - annotate "$severity" "$file" "$line" "$message" - - if [[ "$severity" == "error" ]]; then - ERRORS=$((ERRORS + 1)) - else - WARNINGS=$((WARNINGS + 1)) - fi -} - -# --------------------------------------------------------------------------- -# Helper: normalise a security level string -# --------------------------------------------------------------------------- -# Strips quotes, leading/trailing whitespace, Nickel enum tick prefix -normalise_level() { - local raw="$1" - # Remove surrounding quotes, tick prefix ('Kennel -> Kennel), whitespace - raw="${raw#*=}" # Remove everything before = - raw="${raw//\"/}" # Remove double quotes - raw="${raw//\'/}" # Remove single quotes (Nickel tick) - raw="${raw//,/}" # Remove trailing commas - raw="${raw## }" # Trim leading space - raw="${raw%% }" # Trim trailing space - raw="${raw%%#*}" # Remove inline comments - raw="${raw## }" # Trim again - raw="${raw%% }" - echo "${raw,,}" # Lowercase -} - -# --------------------------------------------------------------------------- -# Validator: check a single K9 file -# --------------------------------------------------------------------------- -validate_k9() { - local file="$1" - FILES_SCANNED=$((FILES_SCANNED + 1)) - - # --- Check 1: K9! magic number on first non-empty line --- - local first_content_line="" - local first_content_line_num=0 - local line_num=0 - - while IFS= read -r line; do - line_num=$((line_num + 1)) - # Skip empty lines - if [[ -z "${line// /}" ]]; then - continue - fi - first_content_line="$line" - first_content_line_num=$line_num - break - done < "$file" - - if [[ "$first_content_line" != "K9!" ]]; then - report_issue "error" "$file" "$first_content_line_num" \ - "Missing K9! magic number. First non-empty line must be exactly 'K9!'" - fi - - # --- Check 2: SPDX header --- - local has_spdx=false - line_num=0 - while IFS= read -r line; do - line_num=$((line_num + 1)) - if [[ $line_num -gt 10 ]]; then - break - fi - if [[ "$line" == *"SPDX-License-Identifier"* ]]; then - has_spdx=true - break - fi - done < "$file" - - if [[ "$has_spdx" == "false" ]]; then - report_issue "warning" "$file" 1 \ - "Missing SPDX-License-Identifier in first 10 lines" - fi - - # --- Check 3: Pedigree block with required fields --- - local has_pedigree=false - local has_pedigree_name=false - local has_pedigree_version=false - local has_security_level=false - local security_level_value="" - local security_level_line=0 - local has_signature_field=false - local in_pedigree=false - local pedigree_depth=0 - - # Resolve a one-hop `let` indirection before scanning. - # - # Nickel lets you build the pedigree separately and attach it by name: - # - # let component_pedigree = { - # metadata = { name = "verisimdb-test-infra", ... }, - # } in - # { pedigree = component_pedigree, ... } - # - # The brace-depth scanner below only sees `pedigree = component_pedigree,` - # — a line with no braces — so depth never rises, the block appears empty, - # and `name` is invisible. The file is valid; the grep could not follow the - # reference. Observed on verisimdb/connectors/test-infra/deploy.k9.ncl, - # which does declare metadata.name at its line 23. - # - # If `pedigree = ` names a binding, treat `let = {` - # as the block opener too. One hop only: chased aliases would need a real - # Nickel evaluator, and `nickel typecheck` cannot be used here because the - # mandatory `K9!` magic line on line 1 is not valid Nickel. - local pedigree_alias="" - pedigree_alias=$(sed -nE 's/^[[:space:]]*pedigree[[:space:]]*=[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*,?[[:space:]]*$/\1/p' "$file" | head -1) - - line_num=0 - while IFS= read -r line; do - line_num=$((line_num + 1)) - - # Detect pedigree block start. Note: do NOT `continue` here — the - # `pedigree = {` line itself contains the opening brace that - # establishes the block. Falling through to the brace counter - # below makes depth start at 1, so a subsequent `security = {…},` - # closing brace correctly takes depth to 1 (not 0), keeping us - # inside the pedigree block when later fields (name/version/leash) - # are checked. Previously the `continue` skipped this opening - # brace, depth started at 0, and the first nested block's close - # prematurely terminated the validator's view of the pedigree — - # making `pedigree.metadata.name` invisible. - if [[ "$line" =~ ^[[:space:]]*pedigree[[:space:]]*= ]]; then - has_pedigree=true - in_pedigree=true - pedigree_depth=0 - # fall through - fi - - # `let = {` where is what `pedigree =` points at. - # See the pedigree_alias note above. - if [[ -n "$pedigree_alias" ]] && \ - [[ "$line" =~ ^[[:space:]]*let[[:space:]]+${pedigree_alias}[[:space:]]*= ]]; then - has_pedigree=true - in_pedigree=true - pedigree_depth=0 - # fall through — this line carries the opening brace - fi - - if [[ "$in_pedigree" == "true" ]]; then - # Track brace depth to know when pedigree block ends - local opens closes - opens="${line//[^\{]/}" - closes="${line//[^\}]/}" - pedigree_depth=$(( pedigree_depth + ${#opens} - ${#closes} )) - - if [[ $pedigree_depth -le 0 && "$has_pedigree" == "true" ]]; then - # Check this final line too before leaving - : - fi - - # Check for name field within pedigree.metadata or pedigree directly. - # Two patterns cover both multi-line and single-line pedigrees: - # 1. ^[[:space:]]+name[[:space:]]*= — the normal multi-line case where - # `name = "..."` appears on its own indented line. - # 2. [[:space:]]name[[:space:]]*= — inline within a single-line - # pedigree assignment such as: - # pedigree = component_pedigree & { name = "foo" } - # (root cause: developer-ecosystem@baab1534 — single-line form - # was missed entirely because the pedigree block opened and - # closed in one line, never reaching the ^[[:space:]]+ check on - # a subsequent iteration.) - if [[ "$line" =~ ^[[:space:]]+name[[:space:]]*= ]] || \ - [[ "$line" =~ [[:space:]]name[[:space:]]*= ]]; then - has_pedigree_name=true - fi - - # Check for version field - if [[ "$line" =~ ^[[:space:]]+(version|schema_version)[[:space:]]*= ]] || \ - [[ "$line" =~ [[:space:]](version|schema_version)[[:space:]]*= ]]; then - has_pedigree_version=true - fi - - # Check for security level (leash field) - if [[ "$line" =~ ^[[:space:]]+(leash|security_level)[[:space:]]*= ]]; then - has_security_level=true - security_level_value="$(normalise_level "$line")" - security_level_line=$line_num - fi - - # Check for signature fields - if [[ "$line" =~ ^[[:space:]]+(signature|signature_required)[[:space:]]*= ]]; then - has_signature_field=true - fi - - # End of pedigree block - if [[ $pedigree_depth -le 0 && "$has_pedigree" == "true" && "$line" == *"}"* ]]; then - in_pedigree=false - fi - fi - - # Also check for signature fields outside pedigree (top-level) - if [[ "$line" =~ ^[[:space:]]*(signature)[[:space:]]*= ]]; then - has_signature_field=true - fi - done < "$file" - - if [[ "$has_pedigree" == "false" ]]; then - report_issue "error" "$file" 1 \ - "Missing pedigree block. K9 files must contain a 'pedigree = { ... }' section" - else - if [[ "$has_pedigree_name" == "false" ]]; then - report_issue "error" "$file" 1 \ - "Pedigree block missing 'name' field (in pedigree.metadata.name or pedigree.name)" - fi - - if [[ "$has_pedigree_version" == "false" ]]; then - report_issue "warning" "$file" 1 \ - "Pedigree block missing 'version' or 'schema_version' field" - fi - fi - - # --- Check 4: Security level validation --- - if [[ "$has_security_level" == "true" ]]; then - local level_valid=false - for valid in $VALID_LEVELS; do - if [[ "$security_level_value" == "$valid" ]]; then - level_valid=true - break - fi - done - - if [[ "$level_valid" == "false" ]]; then - report_issue "error" "$file" "$security_level_line" \ - "Invalid security level '${security_level_value}'. Must be one of: kennel, yard, hunt" - fi - else - if [[ "$has_pedigree" == "true" ]]; then - report_issue "warning" "$file" 1 \ - "No security level (leash/security_level) found in pedigree block" - fi - fi - - # --- Check 5: Hunt-level signature requirement --- - if [[ "$security_level_value" == "hunt" && "$has_signature_field" == "false" ]]; then - report_issue "error" "$file" "$security_level_line" \ - "Hunt-level K9 file must include a 'signature' or 'signature_required' field" - fi -} - -# --------------------------------------------------------------------------- -# Main: discover and validate K9 files -# --------------------------------------------------------------------------- - -echo "::group::K9 Configuration Validation" -echo "Scanning ${SCAN_PATH} for K9 files (.k9, .k9.ncl)..." -echo "" - -# Find all K9 files, excluding .git directory -mapfile -t k9_candidates < <(find "$SCAN_PATH" \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path '*/.git/*' -type f | sort) - -# Apply paths-ignore filter -k9_files=() -SKIPPED=0 -for _f in "${k9_candidates[@]}"; do - if path_ignored "$_f"; then - SKIPPED=$((SKIPPED + 1)) - continue - fi - k9_files+=("$_f") -done +if [ ! -f "$VALIDATOR" ]; then + echo "::notice::K9: no standards checkout at $STANDARDS_DIR (set STANDARDS_DIR to run the validator locally); the k9-conformance lane validates in CI" + exit 0 +fi -if [[ $SKIPPED -gt 0 ]]; then - echo "::notice::Skipped ${SKIPPED} file(s) matching paths-ignore" +# L2 is the Nickel evaluation against the contract; without nickel the +# validator reports those checks as SKIPPED and refuses to call that a pass, +# so stop at L1 here and say so. The lane checks L2 in CI either way. +LAYER=L2 +if ! command -v nickel >/dev/null 2>&1; then + echo "::notice::K9: nickel is not installed, so layer L2 (Nickel semantics) is left to the CI lane; checking L0–L1" + LAYER=L1 fi -if [[ ${#k9_files[@]} -eq 0 ]]; then - echo "::notice::No K9 files found in ${SCAN_PATH}" - echo "files_scanned=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true - echo "errors=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true - echo "warnings=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true - echo "::endgroup::" +cd "$REPO" +mapfile -t files < <(git ls-files -- '*.k9.ncl' '*.k9' '*.k9.lock' | sort) +if [ "${#files[@]}" -eq 0 ]; then + echo "::notice::K9: no tracked K9 file" exit 0 fi -echo "Found ${#k9_files[@]} K9 file(s)" -echo "" - -for file in "${k9_files[@]}"; do - echo " Validating: ${file}" - validate_k9 "$file" +errors=0 +for f in "${files[@]}"; do + echo " Validating: $f" + if ! bash "$VALIDATOR" --strict --layer "$LAYER" "$f"; then + errors=$((errors + 1)) + fi done -echo "" -echo "────────────────────────────────────────" -echo "Files scanned: ${FILES_SCANNED}" -echo "Errors: ${ERRORS}" -echo "Warnings: ${WARNINGS}" -echo "Strict mode: ${STRICT}" -echo "────────────────────────────────────────" - -# Write outputs for GitHub Actions -{ - echo "files_scanned=${FILES_SCANNED}" - echo "errors=${ERRORS}" - echo "warnings=${WARNINGS}" -} >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true - -echo "::endgroup::" - -# Exit with failure if errors were found -if [[ $ERRORS -gt 0 ]]; then - echo "::error::K9 validation failed with ${ERRORS} error(s)" - exit 1 -fi - +echo "K9: ${#files[@]} file(s), ${errors} failing (validator: $VALIDATOR)" +[ "$errors" -eq 0 ] || { echo "::error::K9 validation failed for ${errors} file(s)"; exit 1; } echo "K9 validation passed." -exit 0 diff --git a/.github/label-classifier.json b/.github/label-classifier.json index d349eaa..92e2557 100644 --- a/.github/label-classifier.json +++ b/.github/label-classifier.json @@ -1,739 +1 @@ -{ - "_generated_from": ".github/label-classifier.yml + .github/labels.yml in hyperpolymath/.git-private-farm", - "_do_not_edit": "regenerate with scripts/gen-classifier-json.py", - "version": 1, - "prefix_split_on": "/", - "title_prefix": { - "docs": { - "type": "documentation" - }, - "ci": { - "type": "chore", - "areas": [ - "cicd" - ] - }, - "governance": { - "type": "chore", - "areas": [ - "governance" - ] - }, - "roadmap": { - "type": "enhancement", - "meta": "meta:roadmap" - }, - "chore": { - "type": "chore" - }, - "build": { - "type": "chore", - "areas": [ - "cicd" - ] - }, - "security": { - "type": "chore", - "areas": [ - "security" - ] - }, - "proof": { - "type": "chore", - "areas": [ - "proofs" - ] - }, - "proofs": { - "type": "chore", - "areas": [ - "proofs" - ] - }, - "proof-debt": { - "type": "tech-debt", - "areas": [ - "proofs" - ] - }, - "epic": { - "type": "enhancement", - "meta": "meta:umbrella" - }, - "umbrella": { - "type": "enhancement", - "meta": "meta:umbrella" - }, - "tracking": { - "type": "chore", - "meta": "meta:umbrella" - }, - "campaign": { - "type": "enhancement", - "meta": "meta:campaign" - }, - "hygiene": { - "type": "tech-debt" - }, - "audit": { - "type": "research" - }, - "estate": { - "type": "chore", - "scope": "scope:estate" - }, - "automation": { - "type": "enhancement", - "areas": [ - "automation" - ] - }, - "research": { - "type": "research" - }, - "refactor": { - "type": "refactor" - }, - "test": { - "type": "testing" - }, - "tests": { - "type": "testing" - }, - "feat": { - "type": "enhancement" - }, - "fix": { - "type": "bug" - }, - "bug": { - "type": "bug" - }, - "perf": { - "type": "enhancement", - "areas": [ - "performance" - ] - }, - "codegen": { - "type": "enhancement", - "areas": [ - "architecture" - ] - }, - "packaging": { - "type": "chore", - "areas": [ - "packaging" - ] - }, - "policy": { - "type": "chore", - "areas": [ - "governance" - ] - }, - "ops": { - "type": "chore", - "areas": [ - "automation" - ] - }, - "standard": { - "type": "chore", - "areas": [ - "governance" - ] - }, - "migration": { - "type": "refactor", - "areas": [ - "migration" - ] - }, - "drift": { - "type": "tech-debt" - }, - "corrective": { - "type": "bug" - }, - "adaptive": { - "type": "enhancement" - }, - "perfective": { - "type": "enhancement" - }, - "preventive": { - "type": "tech-debt" - }, - "machine-readable": { - "type": "tech-debt" - }, - "parser": { - "type": "bug" - }, - "lang": { - "type": "bug" - }, - "clippy": { - "type": "tech-debt" - }, - "release": { - "type": "chore" - }, - "upstream": { - "type": "chore" - }, - "hardening": { - "type": "chore", - "areas": [ - "security" - ] - }, - "deps": { - "type": "chore" - }, - "rustsec": { - "type": "chore", - "areas": [ - "security" - ] - }, - "track": { - "type": "chore", - "meta": "meta:umbrella" - }, - "tracker": { - "type": "chore", - "meta": "meta:umbrella" - }, - "wiki": { - "type": "documentation" - }, - "reclassify": { - "type": "refactor" - }, - "backlog": { - "type": "chore" - }, - "core": { - "type": "enhancement", - "areas": [ - "design" - ] - }, - "evidence": { - "type": "enhancement", - "areas": [ - "design" - ] - }, - "manifest": { - "type": "enhancement", - "areas": [ - "design" - ] - }, - "backends": { - "type": "enhancement", - "areas": [ - "design" - ] - } - }, - "bracket_tag": { - "campaign": { - "meta": "meta:campaign" - }, - "umbrella": { - "meta": "meta:umbrella" - }, - "gov": { - "areas": [ - "governance" - ] - }, - "proofs/a": { - "areas": [ - "proofs" - ] - }, - "proofs/b": { - "areas": [ - "proofs" - ] - }, - "proofs/c": { - "areas": [ - "proofs" - ] - }, - "estate": { - "scope": "scope:estate" - }, - "repo": { - "scope": "scope:repo" - }, - "feature": { - "type": "enhancement" - }, - "integration": { - "areas": [ - "conformance" - ] - }, - "reference": { - "type": "documentation" - }, - "register": { - "type": "documentation" - }, - "p0": { - "priority": "priority:p0" - }, - "p1": { - "priority": "priority:p1" - }, - "p2": { - "priority": "priority:p2" - }, - "et-l2": { - "areas": [ - "conformance" - ] - }, - "et-l4": { - "areas": [ - "conformance" - ] - } - }, - "keyword_area": { - "proofs": [ - "agda", - "coq", - "rocq", - "idris", - "lean", - "isabelle", - "hol", - "mizar", - "why3", - "tla", - "alloy", - "dafny", - "acl2", - "pvs", - "metamath", - "z3", - "smt", - "prover", - "provers", - "theorem", - "theorems", - "axiom", - "axioms", - "postulate", - "postulates", - "believe_me", - "sorry", - "proof obligation", - "proof obligations", - "proof hole", - "proof holes", - "proof suite", - "proof-pipeline", - "proof debt", - "proof-debt", - "metatheory", - "mechanize", - "qed" - ], - "cicd": [ - "workflow", - "github action", - "actions.lock", - "lockfile", - "runner", - "startup_failure", - "dependabot", - "check run", - "required context", - "scorecard", - "codeql", - "ci/cd" - ], - "licensing": [ - "spdx", - "licence", - "license", - "reuse", - "copyright", - "attribution", - "agpl", - "mpl" - ], - "security": [ - "gitleaks", - "secret", - "vulnerabilit", - "advisory", - "supply chain", - "cve" - ], - "bindings": [ - "abi", - "ffi", - "wasm", - "jni", - "c api", - "interop", - "extern \"c\"", - "nif", - "snif" - ], - "packaging": [ - "guix", - "nix", - "container", - "containerfile", - "docker", - "flatpak", - "oci image" - ], - "scaffolding": [ - "rsr", - "scaffold", - "template", - "repo-init", - "instantiat", - "placeholder" - ], - "governance": [ - "ruleset", - "policy", - "compliance", - "governance", - "branch protection", - "codeowners", - "code of conduct" - ], - "migration": [ - "rescript", - "to-affinescript", - "\u2192 affinescript", - "port", - "deno", - "bun" - ], - "automation": [ - "bot", - "gitbot", - "hypatia", - "sustainabot", - "oikosbot", - "fan-out", - "fanout", - "dispatch", - "self-heal" - ], - "performance": [ - "latency", - "throughput", - "binary size", - "memory", - "hot path", - "regression" - ] - }, - "keyword_type": { - "tech-debt": [ - "debt", - "drift", - "hygiene", - "stale", - "cleanup", - "follow-up", - "clean up", - "left over", - "leftover", - "anti-pattern", - "inconsistency", - "inconsistent", - "placeholder", - "placeholders", - "tbd", - "todo", - "todos", - "unfilled" - ], - "documentation": [ - "document", - "docs", - "readme", - "adoc", - "prose", - "docs/", - "changelog", - "explainme", - "quickstart", - "wiki", - "docstring", - "doc tree" - ], - "testing": [ - "test", - "tests", - "fuzz", - "bench", - "coverage", - "crash-consistency", - "linearizability", - "equivalence", - "property-correspondence", - "property-based", - "test suite", - "proptest" - ], - "bug": [ - "broken", - "fails", - "failing", - "crash", - "oom", - "regression", - "incorrect", - "does not", - "panic", - "panics", - "unreachable", - "mangled", - "never run", - "never ran", - "never succeeded", - "never fires", - "cannot fail", - "deadlock", - "hangs" - ], - "refactor": [ - "refactor", - "restructure", - "consolidate", - "consolidation", - "reconcile", - "reconciliation", - "unify", - "dedupe", - "re-point", - "repoint", - "extract", - "retire", - "retire duplicate", - "deduplicate", - "reclassify", - "migrate" - ], - "research": [ - "investigat", - "explore", - "spike", - "work out", - "triage", - "assess", - "survey", - "gap analysis", - "self-audit", - "inventory", - "weakness list", - "theory", - "synthesis", - "prioritised weakness", - "feasibility" - ], - "decision": [ - "ruling", - "decide", - "decision", - "adjudicat", - "which of" - ], - "enhancement": [ - "add", - "implement", - "support", - "introduce", - "enable", - "expand", - "expansion", - "extend", - "wire", - "complete", - "build", - "create", - "port" - ] - }, - "meta_signal": { - "meta:umbrella": [ - "umbrella", - "epic", - "master issue", - "parent issue", - "sub-issues", - "child issues" - ], - "meta:campaign": [ - "campaign" - ], - "meta:roadmap": [ - "roadmap", - "capability-expansion", - "future work" - ], - "meta:recurring": [ - "recurring", - "recurrence", - "standing", - "every run", - "each week" - ] - }, - "status_signal": { - "status:blocked": [ - "blocked on", - "blocked:", - "(blocked", - "is blocked", - "gated on", - "waiting on upstream", - "needs upstream" - ], - "status:needs-owner": [ - "unassigned", - "needs an owner", - "no owner" - ], - "status:needs-ruling": [ - "needs a ruling", - "awaiting ruling", - "owner decision needed" - ] - }, - "scope_signal": { - "scope:estate": [ - "estate-wide", - "estate wide", - "across the estate", - "all repos", - "fleet-wide" - ] - }, - "tier_of": { - "bug": "type", - "enhancement": "type", - "documentation": "type", - "refactor": "type", - "tech-debt": "type", - "testing": "type", - "chore": "type", - "research": "type", - "decision": "type", - "question": "type", - "cicd": "area", - "security": "area", - "proofs": "area", - "governance": "area", - "design": "area", - "architecture": "area", - "performance": "area", - "bindings": "area", - "migration": "area", - "packaging": "area", - "licensing": "area", - "automation": "area", - "scaffolding": "area", - "conformance": "area", - "priority:p0": "priority", - "priority:p1": "priority", - "priority:p2": "priority", - "priority:p3": "priority", - "status:blocked": "status", - "status:ready": "status", - "status:needs-owner": "status", - "status:needs-ruling": "status", - "status:do-not-automate": "status", - "meta:umbrella": "meta", - "meta:campaign": "meta", - "meta:roadmap": "meta", - "meta:recurring": "meta", - "scope:estate": "scope", - "scope:repo": "scope" - }, - "tier_max": { - "type": 1, - "area": null, - "priority": 1, - "status": 1, - "meta": 1, - "scope": 1 - }, - "types": [ - "bug", - "enhancement", - "documentation", - "refactor", - "tech-debt", - "testing", - "chore", - "research", - "decision", - "question" - ], - "frozen": [ - "dependencies", - "duplicate", - "elixir", - "gitar-approved", - "github_actions", - "good first issue", - "help wanted", - "invalid", - "javascript", - "never-stale", - "nix", - "pinned", - "python", - "rust", - "security", - "stale", - "wontfix" - ], - "precedence": { - "meta:campaign": 0, - "meta:umbrella": 1, - "meta:recurring": 2, - "meta:roadmap": 3, - "priority:p0": 0, - "priority:p1": 1, - "priority:p2": 2, - "priority:p3": 3, - "status:blocked": 0, - "status:needs-ruling": 1, - "status:needs-owner": 2, - "status:do-not-automate": 3, - "status:ready": 4, - "scope:estate": 0, - "scope:repo": 1, - "bug": 0, - "decision": 1, - "tech-debt": 2, - "testing": 3, - "documentation": 4, - "refactor": 5, - "research": 6, - "enhancement": 7, - "chore": 8, - "question": 9 - } -} +{"_do_not_edit":"regenerate with scripts/gen-classifier-json.py","_generated_from":".github/label-classifier.yml + .github/labels.yml in hyperpolymath/.git-private-farm","bracket_tag":{"campaign":{"meta":"meta:campaign"},"estate":{"scope":"scope:estate"},"et-l2":{"areas":["conformance"]},"et-l4":{"areas":["conformance"]},"feature":{"type":"enhancement"},"gov":{"areas":["governance"]},"integration":{"areas":["conformance"]},"p0":{"priority":"priority:p0"},"p1":{"priority":"priority:p1"},"p2":{"priority":"priority:p2"},"proofs/a":{"areas":["proofs"]},"proofs/b":{"areas":["proofs"]},"proofs/c":{"areas":["proofs"]},"reference":{"type":"documentation"},"register":{"type":"documentation"},"repo":{"scope":"scope:repo"},"umbrella":{"meta":"meta:umbrella"}},"frozen":["dependencies","duplicate","elixir","gitar-approved","github_actions","good first issue","help wanted","invalid","javascript","never-stale","nix","pinned","python","rust","security","stale","wontfix"],"keyword_area":{"automation":["bot","gitbot","hypatia","sustainabot","oikosbot","fan-out","fanout","dispatch","self-heal"],"bindings":["abi","ffi","wasm","jni","c api","interop","extern \"c\"","nif","snif"],"cicd":["workflow","github action","actions.lock","lockfile","runner","startup_failure","dependabot","check run","required context","scorecard","codeql","ci/cd"],"governance":["ruleset","policy","compliance","governance","branch protection","codeowners","code of conduct"],"licensing":["spdx","licence","license","reuse","copyright","attribution","agpl","mpl"],"migration":["rescript","to-affinescript","→ affinescript","port","deno","bun"],"packaging":["guix","nix","container","containerfile","docker","flatpak","oci image"],"performance":["latency","throughput","binary size","memory","hot path","regression"],"proofs":["agda","coq","rocq","idris","lean","isabelle","hol","mizar","why3","tla","alloy","dafny","acl2","pvs","metamath","z3","smt","prover","provers","theorem","theorems","axiom","axioms","postulate","postulates","believe_me","sorry","proof obligation","proof obligations","proof hole","proof holes","proof suite","proof-pipeline","proof debt","proof-debt","metatheory","mechanize","qed"],"scaffolding":["rsr","scaffold","template","repo-init","instantiat","placeholder"],"security":["gitleaks","secret","vulnerabilit","advisory","supply chain","cve"]},"keyword_type":{"bug":["broken","fails","failing","crash","oom","regression","incorrect","does not","panic","panics","unreachable","mangled","never run","never ran","never succeeded","never fires","cannot fail","deadlock","hangs"],"decision":["ruling","decide","decision","adjudicat","which of"],"documentation":["document","docs","readme","adoc","prose","docs/","changelog","explainme","quickstart","wiki","docstring","doc tree"],"enhancement":["add","implement","support","introduce","enable","expand","expansion","extend","wire","complete","build","create","port"],"refactor":["refactor","restructure","consolidate","consolidation","reconcile","reconciliation","unify","dedupe","re-point","repoint","extract","retire","retire duplicate","deduplicate","reclassify","migrate"],"research":["investigat","explore","spike","work out","triage","assess","survey","gap analysis","self-audit","inventory","weakness list","theory","synthesis","prioritised weakness","feasibility"],"tech-debt":["debt","drift","hygiene","stale","cleanup","follow-up","clean up","left over","leftover","anti-pattern","inconsistency","inconsistent","placeholder","placeholders","tbd","todo","todos","unfilled"],"testing":["test","tests","fuzz","bench","coverage","crash-consistency","linearizability","equivalence","property-correspondence","property-based","test suite","proptest"]},"meta_signal":{"meta:campaign":["campaign"],"meta:recurring":["recurring","recurrence","standing","every run","each week"],"meta:roadmap":["roadmap","capability-expansion","future work"],"meta:umbrella":["umbrella","epic","master issue","parent issue","sub-issues","child issues"]},"precedence":{"bug":0,"chore":8,"decision":1,"documentation":4,"enhancement":7,"meta:campaign":0,"meta:recurring":2,"meta:roadmap":3,"meta:umbrella":1,"priority:p0":0,"priority:p1":1,"priority:p2":2,"priority:p3":3,"question":9,"refactor":5,"research":6,"scope:estate":0,"scope:repo":1,"status:blocked":0,"status:do-not-automate":3,"status:needs-owner":2,"status:needs-ruling":1,"status:ready":4,"tech-debt":2,"testing":3},"prefix_split_on":"/","scope_signal":{"scope:estate":["estate-wide","estate wide","across the estate","all repos","fleet-wide"]},"status_signal":{"status:blocked":["blocked on","blocked:","(blocked","is blocked","gated on","waiting on upstream","needs upstream"],"status:needs-owner":["unassigned","needs an owner","no owner"],"status:needs-ruling":["needs a ruling","awaiting ruling","owner decision needed"]},"tier_max":{"area":null,"meta":1,"priority":1,"scope":1,"status":1,"type":1},"tier_of":{"architecture":"area","automation":"area","bindings":"area","bug":"type","chore":"type","cicd":"area","conformance":"area","decision":"type","design":"area","documentation":"type","enhancement":"type","governance":"area","licensing":"area","meta:campaign":"meta","meta:recurring":"meta","meta:roadmap":"meta","meta:umbrella":"meta","migration":"area","packaging":"area","performance":"area","priority:p0":"priority","priority:p1":"priority","priority:p2":"priority","priority:p3":"priority","proofs":"area","question":"type","refactor":"type","research":"type","scaffolding":"area","scope:estate":"scope","scope:repo":"scope","security":"area","status:blocked":"status","status:do-not-automate":"status","status:needs-owner":"status","status:needs-ruling":"status","status:ready":"status","tech-debt":"type","testing":"type"},"title_prefix":{"adaptive":{"type":"enhancement"},"audit":{"type":"research"},"automation":{"areas":["automation"],"type":"enhancement"},"backends":{"areas":["design"],"type":"enhancement"},"backlog":{"type":"chore"},"bug":{"type":"bug"},"build":{"areas":["cicd"],"type":"chore"},"campaign":{"meta":"meta:campaign","type":"enhancement"},"chore":{"type":"chore"},"ci":{"areas":["cicd"],"type":"chore"},"clippy":{"type":"tech-debt"},"codegen":{"areas":["architecture"],"type":"enhancement"},"core":{"areas":["design"],"type":"enhancement"},"corrective":{"type":"bug"},"deps":{"type":"chore"},"docs":{"type":"documentation"},"drift":{"type":"tech-debt"},"epic":{"meta":"meta:umbrella","type":"enhancement"},"estate":{"scope":"scope:estate","type":"chore"},"evidence":{"areas":["design"],"type":"enhancement"},"feat":{"type":"enhancement"},"fix":{"type":"bug"},"governance":{"areas":["governance"],"type":"chore"},"hardening":{"areas":["security"],"type":"chore"},"hygiene":{"type":"tech-debt"},"lang":{"type":"bug"},"machine-readable":{"type":"tech-debt"},"manifest":{"areas":["design"],"type":"enhancement"},"migration":{"areas":["migration"],"type":"refactor"},"ops":{"areas":["automation"],"type":"chore"},"packaging":{"areas":["packaging"],"type":"chore"},"parser":{"type":"bug"},"perf":{"areas":["performance"],"type":"enhancement"},"perfective":{"type":"enhancement"},"policy":{"areas":["governance"],"type":"chore"},"preventive":{"type":"tech-debt"},"proof":{"areas":["proofs"],"type":"chore"},"proof-debt":{"areas":["proofs"],"type":"tech-debt"},"proofs":{"areas":["proofs"],"type":"chore"},"reclassify":{"type":"refactor"},"refactor":{"type":"refactor"},"release":{"type":"chore"},"research":{"type":"research"},"roadmap":{"meta":"meta:roadmap","type":"enhancement"},"rustsec":{"areas":["security"],"type":"chore"},"security":{"areas":["security"],"type":"chore"},"standard":{"areas":["governance"],"type":"chore"},"test":{"type":"testing"},"tests":{"type":"testing"},"track":{"meta":"meta:umbrella","type":"chore"},"tracker":{"meta":"meta:umbrella","type":"chore"},"tracking":{"meta":"meta:umbrella","type":"chore"},"umbrella":{"meta":"meta:umbrella","type":"enhancement"},"upstream":{"type":"chore"},"wiki":{"type":"documentation"}},"types":["bug","enhancement","documentation","refactor","tech-debt","testing","chore","research","decision","question"],"version":1} diff --git a/.github/labels.json b/.github/labels.json index 78786d4..8e8c403 100644 --- a/.github/labels.json +++ b/.github/labels.json @@ -1,260 +1 @@ -{ - "_generated_from": ".github/labels.yml in hyperpolymath/.git-private-farm", - "_do_not_edit": "regenerate with scripts/gen-labels-json.py", - "version": 1, - "labels": [ - { - "name": "bug", - "color": "d73a4a", - "description": "Something is broken or behaves incorrectly", - "tier": "type" - }, - { - "name": "enhancement", - "color": "a2eeef", - "description": "New capability or improvement to existing behaviour", - "tier": "type" - }, - { - "name": "documentation", - "color": "0075ca", - "description": "Docs, prose, diagrams, READMEs, ADRs", - "tier": "type" - }, - { - "name": "refactor", - "color": "c5def5", - "description": "Restructuring that preserves observable behaviour", - "tier": "type" - }, - { - "name": "tech-debt", - "color": "fbca04", - "description": "Known shortcut, drift, or hygiene owed - includes cleanup", - "tier": "type" - }, - { - "name": "testing", - "color": "bfd4f2", - "description": "Tests, benchmarks, fuzzing, property checks, coverage", - "tier": "type" - }, - { - "name": "chore", - "color": "ededed", - "description": "Routine maintenance with no behaviour change", - "tier": "type" - }, - { - "name": "research", - "color": "d4c5f9", - "description": "Open investigation; the outcome is knowledge, not code", - "tier": "type" - }, - { - "name": "decision", - "color": "8b5cf6", - "description": "A ruling is required before work can proceed", - "tier": "type" - }, - { - "name": "question", - "color": "d876e3", - "description": "Further information is requested", - "tier": "type" - }, - { - "name": "cicd", - "color": "006b75", - "description": "CI/CD: workflows, actions, lockfiles, pins, runners, release gates", - "tier": "area" - }, - { - "name": "security", - "color": "006b75", - "description": "Security posture, secrets, scanning, advisories, supply chain", - "tier": "area" - }, - { - "name": "proofs", - "color": "006b75", - "description": "Formal verification: Agda, Coq, Idris, Lean, Z3/SMT, axiom debt", - "tier": "area" - }, - { - "name": "governance", - "color": "006b75", - "description": "Policy, rulesets, standards, compliance, and their enforcement", - "tier": "area" - }, - { - "name": "design", - "color": "006b75", - "description": "Design of an interface, protocol, grammar, or type theory", - "tier": "area" - }, - { - "name": "architecture", - "color": "006b75", - "description": "Structural/system-level shape and runtime behaviour", - "tier": "area" - }, - { - "name": "performance", - "color": "006b75", - "description": "Throughput, latency, memory, binary size", - "tier": "area" - }, - { - "name": "bindings", - "color": "006b75", - "description": "ABI, FFI, WASM, and cross-language interop surfaces", - "tier": "area" - }, - { - "name": "migration", - "color": "006b75", - "description": "Porting between languages or toolchains (e.g. -> AffineScript)", - "tier": "area" - }, - { - "name": "packaging", - "color": "006b75", - "description": "Guix, Nix, containers, distribution artefacts", - "tier": "area" - }, - { - "name": "licensing", - "color": "006b75", - "description": "Licences, SPDX headers, REUSE compliance, attribution", - "tier": "area" - }, - { - "name": "automation", - "color": "006b75", - "description": "Bots, schedulers, dispatch, self-healing, fan-out", - "tier": "area" - }, - { - "name": "scaffolding", - "color": "006b75", - "description": "RSR templates, repo init, instantiation, project skeletons", - "tier": "area" - }, - { - "name": "conformance", - "color": "006b75", - "description": "Conformance to an external or internal specification", - "tier": "area" - }, - { - "name": "priority:p0", - "color": "b60205", - "description": "Critical - drop other work", - "tier": "priority" - }, - { - "name": "priority:p1", - "color": "d93f0b", - "description": "High - schedule next", - "tier": "priority" - }, - { - "name": "priority:p2", - "color": "e99695", - "description": "Normal - queue it", - "tier": "priority" - }, - { - "name": "priority:p3", - "color": "f9d0c4", - "description": "Low - nice to have", - "tier": "priority" - }, - { - "name": "status:blocked", - "color": "fbca04", - "description": "Cannot proceed until a dependency clears", - "tier": "status" - }, - { - "name": "status:ready", - "color": "fbca04", - "description": "Fully specified and ready to be picked up", - "tier": "status" - }, - { - "name": "status:needs-owner", - "color": "fbca04", - "description": "Unassigned and needs someone to take it", - "tier": "status" - }, - { - "name": "status:needs-ruling", - "color": "fbca04", - "description": "Awaiting an owner decision", - "tier": "status" - }, - { - "name": "status:do-not-automate", - "color": "fbca04", - "description": "Bots and sweeps must not touch this issue", - "tier": "status" - }, - { - "name": "meta:umbrella", - "color": "5319e7", - "description": "Parent issue aggregating child issues", - "tier": "meta" - }, - { - "name": "meta:campaign", - "color": "5319e7", - "description": "Coordinated multi-repo push with a defined end state", - "tier": "meta" - }, - { - "name": "meta:roadmap", - "color": "5319e7", - "description": "Forward planning; not yet actionable work", - "tier": "meta" - }, - { - "name": "meta:recurring", - "color": "5319e7", - "description": "Recurs on a schedule or by trigger; never finally closed", - "tier": "meta" - }, - { - "name": "scope:estate", - "color": "bfdadc", - "description": "Affects many or all repos across the estate", - "tier": "scope" - }, - { - "name": "scope:repo", - "color": "bfdadc", - "description": "Confined to this repository", - "tier": "scope" - } - ], - "frozen": [ - "dependencies", - "duplicate", - "elixir", - "gitar-approved", - "github_actions", - "good first issue", - "help wanted", - "invalid", - "javascript", - "never-stale", - "nix", - "pinned", - "python", - "rust", - "security", - "stale", - "wontfix" - ] -} +{"_do_not_edit":"regenerate with scripts/gen-labels-json.py","_generated_from":".github/labels.yml in hyperpolymath/.git-private-farm","frozen":["dependencies","duplicate","elixir","gitar-approved","github_actions","good first issue","help wanted","invalid","javascript","never-stale","nix","pinned","python","rust","security","stale","wontfix"],"labels":[{"color":"d73a4a","description":"Something is broken or behaves incorrectly","name":"bug","tier":"type"},{"color":"a2eeef","description":"New capability or improvement to existing behaviour","name":"enhancement","tier":"type"},{"color":"0075ca","description":"Docs, prose, diagrams, READMEs, ADRs","name":"documentation","tier":"type"},{"color":"c5def5","description":"Restructuring that preserves observable behaviour","name":"refactor","tier":"type"},{"color":"fbca04","description":"Known shortcut, drift, or hygiene owed - includes cleanup","name":"tech-debt","tier":"type"},{"color":"bfd4f2","description":"Tests, benchmarks, fuzzing, property checks, coverage","name":"testing","tier":"type"},{"color":"ededed","description":"Routine maintenance with no behaviour change","name":"chore","tier":"type"},{"color":"d4c5f9","description":"Open investigation; the outcome is knowledge, not code","name":"research","tier":"type"},{"color":"8b5cf6","description":"A ruling is required before work can proceed","name":"decision","tier":"type"},{"color":"d876e3","description":"Further information is requested","name":"question","tier":"type"},{"color":"006b75","description":"CI/CD: workflows, actions, lockfiles, pins, runners, release gates","name":"cicd","tier":"area"},{"color":"006b75","description":"Security posture, secrets, scanning, advisories, supply chain","name":"security","tier":"area"},{"color":"006b75","description":"Formal verification: Agda, Coq, Idris, Lean, Z3/SMT, axiom debt","name":"proofs","tier":"area"},{"color":"006b75","description":"Policy, rulesets, standards, compliance, and their enforcement","name":"governance","tier":"area"},{"color":"006b75","description":"Design of an interface, protocol, grammar, or type theory","name":"design","tier":"area"},{"color":"006b75","description":"Structural/system-level shape and runtime behaviour","name":"architecture","tier":"area"},{"color":"006b75","description":"Throughput, latency, memory, binary size","name":"performance","tier":"area"},{"color":"006b75","description":"ABI, FFI, WASM, and cross-language interop surfaces","name":"bindings","tier":"area"},{"color":"006b75","description":"Porting between languages or toolchains (e.g. -> AffineScript)","name":"migration","tier":"area"},{"color":"006b75","description":"Guix, Nix, containers, distribution artefacts","name":"packaging","tier":"area"},{"color":"006b75","description":"Licences, SPDX headers, REUSE compliance, attribution","name":"licensing","tier":"area"},{"color":"006b75","description":"Bots, schedulers, dispatch, self-healing, fan-out","name":"automation","tier":"area"},{"color":"006b75","description":"RSR templates, repo init, instantiation, project skeletons","name":"scaffolding","tier":"area"},{"color":"006b75","description":"Conformance to an external or internal specification","name":"conformance","tier":"area"},{"color":"b60205","description":"Critical - drop other work","name":"priority:p0","tier":"priority"},{"color":"d93f0b","description":"High - schedule next","name":"priority:p1","tier":"priority"},{"color":"e99695","description":"Normal - queue it","name":"priority:p2","tier":"priority"},{"color":"f9d0c4","description":"Low - nice to have","name":"priority:p3","tier":"priority"},{"color":"fbca04","description":"Cannot proceed until a dependency clears","name":"status:blocked","tier":"status"},{"color":"fbca04","description":"Fully specified and ready to be picked up","name":"status:ready","tier":"status"},{"color":"fbca04","description":"Unassigned and needs someone to take it","name":"status:needs-owner","tier":"status"},{"color":"fbca04","description":"Awaiting an owner decision","name":"status:needs-ruling","tier":"status"},{"color":"fbca04","description":"Bots and sweeps must not touch this issue","name":"status:do-not-automate","tier":"status"},{"color":"5319e7","description":"Parent issue aggregating child issues","name":"meta:umbrella","tier":"meta"},{"color":"5319e7","description":"Coordinated multi-repo push with a defined end state","name":"meta:campaign","tier":"meta"},{"color":"5319e7","description":"Forward planning; not yet actionable work","name":"meta:roadmap","tier":"meta"},{"color":"5319e7","description":"Recurs on a schedule or by trigger; never finally closed","name":"meta:recurring","tier":"meta"},{"color":"bfdadc","description":"Affects many or all repos across the estate","name":"scope:estate","tier":"scope"},{"color":"bfdadc","description":"Confined to this repository","name":"scope:repo","tier":"scope"}],"version":1} diff --git a/.github/rulesets/README.adoc b/.github/rulesets/README.adoc index 572f934..a7794bc 100644 --- a/.github/rulesets/README.adoc +++ b/.github/rulesets/README.adoc @@ -81,24 +81,30 @@ gh api "/repos/{owner}/{repo}/rulesets" --jq '.[] | "\(.id)\t\(.name)\t\(.enforc |=== | Live ruleset | Enforcement | Target | Covers -| `Base` | active | branch | deletion, required signatures, code scanning (CodeQL/Hypatia), Copilot review, non-fast-forward -| `Branch-Floor` | active | branch | deletion, non-fast-forward -| `Immutable-Tags` | active | tag | creation, deletion, non-fast-forward, update, required signatures +| `Base` (`23218684`) | active | branch | deletion, required signatures, code scanning (CodeQL/Hypatia), Copilot review, non-fast-forward +| `Require-Linear-History` (`24258228`) | active | branch | required linear history +| `Secret-Scan-Floor` (`24309812`) | active | branch | required status checks (`scan / gitleaks`) +| `Immutable-Tags` (`22959911`) | active | tag | creation, deletion, non-fast-forward, update, required signatures |=== === Known drift between these files and what is live The files below are *not* a transcript of the live rulesets. This is the -divergence as of 2026-09-28, recorded rather than silently reconciled because +divergence as of 2026-10-25, recorded rather than silently reconciled because `base.json` is sourced verbatim from upstream and must not be forked here. -*Live `Base` rule that no file describes* :: +*Live rulesets that no synced file describes* :: `code_scanning` (with CodeQL and Hypatia tools) and `copilot_code_review` are -enforced live but appear in neither `base.json` nor any upstream file synced into -this directory. Upstream's nearest equivalent, +enforced live in `Base` (`23218684`), and `Secret-Scan-Floor` (`24309812`) +enforces `required_status_checks` for `scan / gitleaks` (`integration_id: 15368`), +yet neither appears in `base.json` nor any upstream file synced into this +directory. Upstream's nearest equivalent for code scanning, `config/rulesets/Optimus-Extras.json`, declares `code_scanning` with an *empty* `code_scanning_tools` list, so it does not describe what is live here either. +Conversely, `branch-floor.json` is retained here as the irreducible recovery +payload even though the separate `Branch-Floor` live ruleset is not currently +mounted (`Base` covers both `deletion` and `non_fast_forward`). *Removed from live `Base` on 2026-10-02 (owner decision)* :: @@ -118,7 +124,8 @@ so they stay. Do not add either rule back without a producer that runs on `required_linear_history` is in `base.json` but not in live `Base`; it is enforced by the separate `Require-Linear-History` ruleset. `required_status_checks` (an empty list, so a no-op) and the `pull_request` rule are likewise absent from live `Base`. (`non_fast_forward` is -live in `Base` as of 2026-10-02, and is also covered by `Branch-Floor`.) +live in `Base` as of 2026-10-02; `branch-floor.json` also specifies it as a +retained recovery payload, though `Branch-Floor` is not mounted as a separate live ruleset.) *`Immutable-Tags.json` lags the live ruleset* :: diff --git a/.github/rulesets/base.json b/.github/rulesets/base.json index 136c0a0..5c3eaf0 100644 --- a/.github/rulesets/base.json +++ b/.github/rulesets/base.json @@ -1,102 +1 @@ -{ - "name": "Base", - "target": "branch", - "enforcement": "active", - "conditions": { - "ref_name": { - "include": [ - "~DEFAULT_BRANCH" - ], - "exclude": [] - } - }, - "bypass_actors": [ - { - "actor_id": 5, - "actor_type": "RepositoryRole", - "bypass_mode": "pull_request" - }, - { - "actor_id": 1236702, - "actor_type": "Integration", - "bypass_mode": "pull_request" - }, - { - "actor_id": 29110, - "actor_type": "Integration", - "bypass_mode": "pull_request" - }, - { - "actor_id": 15368, - "actor_type": "Integration", - "bypass_mode": "pull_request" - }, - { - "actor_id": 347564, - "actor_type": "Integration", - "bypass_mode": "pull_request" - }, - { - "actor_id": 46505, - "actor_type": "Integration", - "bypass_mode": "pull_request" - }, - { - "actor_id": 1143301, - "actor_type": "Integration", - "bypass_mode": "pull_request" - }, - { - "actor_id": 1144995, - "actor_type": "Integration", - "bypass_mode": "pull_request" - }, - { - "actor_id": 12526, - "actor_type": "Integration", - "bypass_mode": "pull_request" - }, - { - "actor_id": 2538504, - "actor_type": "Integration", - "bypass_mode": "pull_request" - } - ], - "rules": [ - { - "type": "deletion" - }, - { - "type": "non_fast_forward" - }, - { - "type": "required_signatures" - }, - { - "type": "required_linear_history" - }, - { - "type": "pull_request", - "parameters": { - "required_approving_review_count": 0, - "dismiss_stale_reviews_on_push": true, - "require_code_owner_review": false, - "require_last_push_approval": false, - "required_review_thread_resolution": true, - "require_extra_approval_for_unattributed_changes": false, - "required_reviewers": [], - "allowed_merge_methods": [ - "squash" - ] - } - }, - { - "type": "required_status_checks", - "parameters": { - "strict_required_status_checks_policy": true, - "do_not_enforce_on_create": false, - "required_status_checks": [] - } - } - ] -} +{"bypass_actors":[{"actor_id":5,"actor_type":"RepositoryRole","bypass_mode":"pull_request"},{"actor_id":1236702,"actor_type":"Integration","bypass_mode":"pull_request"},{"actor_id":29110,"actor_type":"Integration","bypass_mode":"pull_request"},{"actor_id":15368,"actor_type":"Integration","bypass_mode":"pull_request"},{"actor_id":347564,"actor_type":"Integration","bypass_mode":"pull_request"},{"actor_id":46505,"actor_type":"Integration","bypass_mode":"pull_request"},{"actor_id":1143301,"actor_type":"Integration","bypass_mode":"pull_request"},{"actor_id":1144995,"actor_type":"Integration","bypass_mode":"pull_request"},{"actor_id":12526,"actor_type":"Integration","bypass_mode":"pull_request"},{"actor_id":2538504,"actor_type":"Integration","bypass_mode":"pull_request"}],"conditions":{"ref_name":{"exclude":[],"include":["~DEFAULT_BRANCH"]}},"enforcement":"active","name":"Base","rules":[{"type":"deletion"},{"type":"non_fast_forward"},{"type":"required_signatures"},{"type":"required_linear_history"},{"parameters":{"allowed_merge_methods":["squash"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":false,"require_extra_approval_for_unattributed_changes":false,"require_last_push_approval":false,"required_approving_review_count":0,"required_review_thread_resolution":true,"required_reviewers":[]},"type":"pull_request"},{"parameters":{"do_not_enforce_on_create":false,"required_status_checks":[],"strict_required_status_checks_policy":true},"type":"required_status_checks"}],"target":"branch"} diff --git a/.github/rulesets/branch-floor.json b/.github/rulesets/branch-floor.json index 465908a..13b9d11 100644 --- a/.github/rulesets/branch-floor.json +++ b/.github/rulesets/branch-floor.json @@ -1,16 +1 @@ -{ - "name": "Branch-Floor", - "target": "branch", - "enforcement": "active", - "bypass_actors": [], - "conditions": { - "ref_name": { - "include": ["~DEFAULT_BRANCH"], - "exclude": [] - } - }, - "rules": [ - { "type": "deletion" }, - { "type": "non_fast_forward" } - ] -} +{"bypass_actors":[],"conditions":{"ref_name":{"exclude":[],"include":["~DEFAULT_BRANCH"]}},"enforcement":"active","name":"Branch-Floor","rules":[{"type":"deletion"},{"type":"non_fast_forward"}],"target":"branch"} diff --git a/.github/rulesets/require-linear-history.json b/.github/rulesets/require-linear-history.json index 188cca9..611b621 100644 --- a/.github/rulesets/require-linear-history.json +++ b/.github/rulesets/require-linear-history.json @@ -1,19 +1 @@ -{ - "name": "Require-Linear-History", - "target": "branch", - "enforcement": "active", - "bypass_actors": [], - "conditions": { - "ref_name": { - "include": [ - "~DEFAULT_BRANCH" - ], - "exclude": [] - } - }, - "rules": [ - { - "type": "required_linear_history" - } - ] -} +{"bypass_actors":[],"conditions":{"ref_name":{"exclude":[],"include":["~DEFAULT_BRANCH"]}},"enforcement":"active","name":"Require-Linear-History","rules":[{"type":"required_linear_history"}],"target":"branch"} diff --git a/.github/rulesets/require-signed-commits.json b/.github/rulesets/require-signed-commits.json index 01d8875..6d78a18 100644 --- a/.github/rulesets/require-signed-commits.json +++ b/.github/rulesets/require-signed-commits.json @@ -1,19 +1 @@ -{ - "name": "Require-Signed-Commits", - "target": "branch", - "enforcement": "active", - "bypass_actors": [], - "conditions": { - "ref_name": { - "include": [ - "~DEFAULT_BRANCH" - ], - "exclude": [] - } - }, - "rules": [ - { - "type": "required_signatures" - } - ] -} +{"bypass_actors":[],"conditions":{"ref_name":{"exclude":[],"include":["~DEFAULT_BRANCH"]}},"enforcement":"active","name":"Require-Signed-Commits","rules":[{"type":"required_signatures"}],"target":"branch"} diff --git a/.github/workflows/README.adoc b/.github/workflows/README.adoc index c7a2328..2a57751 100644 --- a/.github/workflows/README.adoc +++ b/.github/workflows/README.adoc @@ -61,3 +61,30 @@ pins), and gitleaks in CI catches a genuine SonarCloud token as `generic-api-key Each targets a different language/surface; together they cover what no single analyser does. + +== Deed and K9 conformance come from the canon, not from this tree + +`dogfood-gate.yml` validates the repo deed and the K9 contracts by calling the +two reusable lanes in hyperpolymath/standards, `deed-conformance.yml` and +`k9-conformance.yml`, at the one standards commit every `uses:` in this +directory pins. That commit supplies the linter, the validator, the K9 contract +(v1.0.0, D173) and the verdict-grid renderer, so what CI accepts is exactly +what the canon says (standards `docs/CONTRACTILE-DESCRIPTILE-VALIDATION.adoc`). + +* `k9-validate` (reusable) renders the K9 grid into its job summary and the + `k9-grid` artifact; it asks for `pull-requests: read` only. `k9-verdict` + downloads that artifact and posts `report.md` to the pull request under + ``, one comment per PR, + updated in place. `badges` publishes `badge.json` to `badges/k9.json` on the + `governance-badges` branch on every push to `main`; the README badge reads it + through the shields endpoint. +* `hypatia-scan.yml` -> `verdict` job posts the Hypatia grid the same way + (artifact `verdict-grid`, marker ``). + +There is deliberately no second validator here: the former `k9-validate.yml` +and `deed-validate.yml` (the `k9-ecosystem` / `deed-ecosystem` actions at +`@main`) and the grep checks in `.github/hooks/validate-{k9,deed}.sh` accepted +files the contract rejects. The hooks are now thin wrappers over the same +canonical tools, run from a standards checkout (`STANDARDS_DIR`), and skip +with a notice when none is present. k9iser is retired estate-wide: contracts +are authored to `1-formats/k9/spec/K9-CONTRACT-SPEC.adoc`, never generated. diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 0288335..5bc98d3 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -7,19 +7,18 @@ workflows: - 'actions/checkout@v7.0.1' '.github/workflows/codeql.yml': - 'actions/checkout@v7.0.1' - - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' - '.github/workflows/deed-validate.yml': - - 'actions/checkout@v7.0.1' - - 'hyperpolymath/deed-ecosystem@main' + - 'github/codeql-action@9b3254085494b55a0f7c4c2b3d2d52cc712ce370' '.github/workflows/dependabot-automerge.yml': - - 'dependabot/fetch-metadata@v3.1.0' + - 'dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98' '.github/workflows/docker-build.yml': - 'actions/checkout@v7.0.1' '.github/workflows/dogfood-gate.yml': - - 'actions/checkout@v7.0.1' - - 'erlef/setup-beam@v1.24.1' - - 'hyperpolymath/deed-ecosystem@main' - - 'hyperpolymath/k9-ecosystem@main' + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333' + - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' '.github/workflows/dogfood-summary.yml': - 'actions/checkout@v7.0.1' '.github/workflows/dot-wellknown-enforcement.yml': @@ -29,25 +28,28 @@ workflows: '.github/workflows/empty-linter.yml': - 'actions/checkout@v7.0.1' '.github/workflows/estate-rules.yml': - - 'actions/checkout@v7.0.1' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + '.github/workflows/fork-governance.yml': + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' '.github/workflows/governance.yml': - - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/groove-check.yml': - 'actions/checkout@v7.0.1' '.github/workflows/guix-policy.yml': - 'actions/checkout@v7.0.1' '.github/workflows/hypatia-scan.yml': - - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' - '.github/workflows/k9-validate.yml': - - 'actions/checkout@v7.0.1' - - 'hyperpolymath/k9-ecosystem@main' + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' + - 'github/codeql-action@9b3254085494b55a0f7c4c2b3d2d52cc712ce370' '.github/workflows/label-triage.yml': [] '.github/workflows/labels.yml': [] '.github/workflows/lock-sync-gate.yml': [] '.github/workflows/main-estate-audit.yml': - - 'hyperpolymath/cicd-suite@55556cf5ba71ba744695861503c13148d3915a5d' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/mirror.yml': - - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/ossf-best-practices.yml': - 'actions/checkout@v7.0.1' '.github/workflows/pages.yml': @@ -59,11 +61,15 @@ workflows: - 'haskell-actions/setup@v2.12.1' '.github/workflows/proofs.yml': - 'actions/checkout@v7.0.1' + '.github/workflows/proven-tests.yml': + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' '.github/workflows/push-email-notify.yml': - 'hyperpolymath/smtp-notify-action@v0.5.0' '.github/workflows/quality.yml': - - 'actions/checkout@v7.0.1' - - 'editorconfig-checker/action-editorconfig-checker@v3.0.0' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393' '.github/workflows/release.yml': - 'actions/attest-build-provenance@v4.2.2' - 'actions/checkout@v7.0.1' @@ -72,24 +78,27 @@ workflows: '.github/workflows/rsr-compliance-canary.yml': - 'actions/checkout@v7.0.1' '.github/workflows/rust-ci.yml': - - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/scorecard.yml': - - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'github/codeql-action@9b3254085494b55a0f7c4c2b3d2d52cc712ce370' + - 'ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e' '.github/workflows/secret-scanner.yml': - - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/security-policy.yml': - - 'actions/checkout@v7.0.1' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/sonarqube.yml': - 'actions/checkout@v7.0.1' - 'sonarsource/sonarqube-scan-action@v8.3.0' + '.github/workflows/squabble.yml': [] '.github/workflows/static-analysis-gate.yml': - - 'actions/checkout@v7.0.1' - - 'actions/download-artifact@v8.0.2' - - 'actions/upload-artifact@v7.0.2' - - 'erlef/setup-beam@v1.24.1' + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333' + - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' '.github/workflows/workflow-linter.yml': - 'actions/checkout@v7.0.1' - - 'oven-sh/setup-bun@v2.2.0' dependencies: 'actions/attest-build-provenance@v4.2.2': ref: 'v4.2.2' @@ -133,7 +142,7 @@ dependencies: commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' owner_id: 44036562 repo_id: 438112499 - 'actions/download-artifact@v8.0.2': + 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333': ref: 'v8.0.2' commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333' owner_id: 44036562 @@ -160,106 +169,39 @@ dependencies: repo_id: 496012378 uses: - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed': - ref: 'v2.0.5' - commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' - owner_id: 42048915 - repo_id: 356423100 - 'dependabot/fetch-metadata@v3.1.0': + 'dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98': ref: 'v3.1.0' commit: 'sha1-25dd0e34f4fe68f24cc83900b1fe3fe149efef98' owner_id: 27347476 repo_id: 371068214 - 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f': - ref: 'v1' - commit: 'sha1-e2a55d2ffb04f378e9626c28d38b36d230d1e12f' - owner_id: 1940490 - repo_id: 260749683 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393': ref: 'v3.0.0' commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' owner_id: 26415196 repo_id: 297874902 - 'editorconfig-checker/action-editorconfig-checker@v3.0.0': - ref: 'v3.0.0' - commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' - owner_id: 26415196 - repo_id: 297874902 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': ref: 'v1.24.1' commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 - 'erlef/setup-beam@v1.24.1': - ref: 'v1.24.1' - commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' - owner_id: 47606891 - repo_id: 331103973 - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63': - ref: 'v4.38.0' - commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' + 'github/codeql-action@9b3254085494b55a0f7c4c2b3d2d52cc712ce370': + ref: 'v4.39.1' + commit: 'sha1-9b3254085494b55a0f7c4c2b3d2d52cc712ce370' owner_id: 9919 repo_id: 259445878 - 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406': - ref: 'v2.2.1' - commit: 'sha1-abea47f85e598557f500fa1fd2ab7464fcb39406' - owner_id: 1006268 - repo_id: 212984112 'haskell-actions/setup@v2.12.1': ref: 'v2.12.1' commit: 'sha1-0f8e8c99d88aeb3fbfd523f1ef2c6f762d10d64d' owner_id: 75048950 repo_id: 623796603 - 'hyperpolymath/cicd-suite@0da816c05ae7486671d863e07935f93981c0c2d5': - ref: '0da816c05ae7486671d863e07935f93981c0c2d5' - commit: 'sha1-0da816c05ae7486671d863e07935f93981c0c2d5' - owner_id: 6759885 - repo_id: 1326697643 - 'hyperpolymath/cicd-suite@55556cf5ba71ba744695861503c13148d3915a5d': - ref: '55556cf5ba71ba744695861503c13148d3915a5d' - commit: 'sha1-55556cf5ba71ba744695861503c13148d3915a5d' - owner_id: 6759885 - repo_id: 1326697643 - uses: - - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'hyperpolymath/cicd-suite@0da816c05ae7486671d863e07935f93981c0c2d5' - 'hyperpolymath/deed-ecosystem@main': - ref: 'main' - commit: 'sha1-44f9c9fc42901a55e0f489a40eabf4173e550d1d' - owner_id: 6759885 - repo_id: 1275649586 - 'hyperpolymath/k9-ecosystem@main': - ref: 'main' - commit: 'sha1-2155aa26a21758f2ba119f61bc7e0e1981c106fb' - owner_id: 6759885 - repo_id: 1275650185 'hyperpolymath/smtp-notify-action@v0.5.0': ref: 'v0.5.0' commit: 'sha1-c1c9fa07992a02c1fd3d67a0dc1b08cccb852aef' owner_id: 6759885 repo_id: 1352485172 - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66': - ref: '75bd5994470661a099df562d6d5ad2fe744dbd66' - commit: 'sha1-75bd5994470661a099df562d6d5ad2fe744dbd66' - owner_id: 6759885 - repo_id: 1116521501 - uses: - - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' - - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' - - 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f' - - 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393' - - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' - - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' - - 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406' - - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' - - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' - - 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' - - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': - ref: 'v2.4.4' - commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' + 'ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e': + ref: 'v2.5.0' + commit: 'sha1-fa7117db83c52d787ae5a89f350999701505d78e' owner_id: 67707773 repo_id: 421101922 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6': @@ -267,11 +209,6 @@ dependencies: commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' owner_id: 108928776 repo_id: 512644635 - 'oven-sh/setup-bun@v2.2.0': - ref: 'v2.2.0' - commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' - owner_id: 108928776 - repo_id: 512644635 'softprops/action-gh-release@v3.0.3': ref: 'v3.0.3' commit: 'sha1-efb35369e0ad2afab669f228072c1b0d510eae64' @@ -282,13 +219,3 @@ dependencies: commit: 'sha1-d209202bc7d53ff1cc128f7f907dac145c9d6ae9' owner_id: 545988 repo_id: 366408409 - 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6': - ref: 'v2.9.2' - commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' - owner_id: 580492 - repo_id: 298565987 - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': - ref: 'v0.10.0' - commit: 'sha1-e83874834305fe9a4a2997156cb26c5de65a8555' - owner_id: 135788 - repo_id: 208510314 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a84257d..4b76561 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,11 +37,11 @@ jobs: with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 (4.38.1 blocked estate-wide; nexia-list#100) + uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.38.0 (4.38.1 blocked estate-wide; nexia-list#100) with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 (4.38.1 blocked estate-wide; nexia-list#100) + uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.38.0 (4.38.1 blocked estate-wide; nexia-list#100) with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/deed-validate.yml b/.github/workflows/deed-validate.yml deleted file mode 100644 index 0206245..0000000 --- a/.github/workflows/deed-validate.yml +++ /dev/null @@ -1,59 +0,0 @@ -# This workflow is managed by gh actions-lock. -# SPDX-License-Identifier: MPL-2.0 -name: "🟡 CHECK: DEED Manifest Validation" - -on: - pull_request: - branches: ['**'] - push: - branches: [main, master] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deed-validate: - name: Validate DEED manifests - runs-on: ubuntu-24.04 - timeout-minutes: 15 - - steps: - - name: Checkout repository - uses: actions/checkout@v7.0.1 - - - name: Check for deed manifests - id: detect - run: | - COUNT=$(find . -name '*.deed' -not -path './.git/*' | wc -l) - echo "count=$COUNT" >> "$GITHUB_OUTPUT" - if [ "$COUNT" -eq 0 ]; then - echo "::warning::No .deed manifest found. Every RSR repo should have a repo deed (_chora.deed) at its root." - fi - - - name: "🟡 CHECK: Validate DEED manifests" - if: steps.detect.outputs.count > 0 - uses: hyperpolymath/deed-ecosystem/validate-action@main - with: - path: '.' - strict: 'false' - - - name: Write summary - run: | - MANIFEST_COUNT="${{ steps.detect.outputs.count }}" - if [ "$MANIFEST_COUNT" -eq 0 ]; then - cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" - ## DEED Manifest Validation - - :warning: **No .deed manifest found.** Every RSR-compliant repo should have a repo deed (`_chora.deed`) at its root. - - Copy it from [resource-algebra](https://github.com/hyperpolymath/resource-algebra). Manifests are validated against the DEED grammar (standards `1-formats/deed/`). The `.a2ml` format is retired; no `.a2ml` file may exist. - EOF - else - echo "## DEED Manifest Validation" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Scanned **${MANIFEST_COUNT}** .deed manifest(s). See step output for details." >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index d164a43..cd5ca8d 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -1,39 +1,17 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # -# dependabot-automerge.yml — enable GitHub's native auto-merge on -# Dependabot pull requests that match a declared severity / ecosystem -# policy. Pairs with `.github/dependabot.yml`'s -# `open-pull-requests-limit: 0` + security-only pattern (see the -# cargo block there). +# dependabot-automerge.yml — enable GitHub's native auto-merge on Dependabot +# pull requests that satisfy the fail-closed severity + semver policy (#204): +# 1. PR author is Dependabot (verified by immutable numeric user.id 49699333 +# AND user.login == 'dependabot[bot]', never spoofable github.actor); +# 2. Security update (GHSA ID present) whose GitHub Advisory severity is +# verified via the GitHub Advisory API as `low` or `medium` (CVSS < 7.0; +# `high`, `critical`, or unreadable advisories fail closed to human review); +# 3. Semver update is `patch` or `minor` (never `major`). # -# What this does: -# - Triggers on every Dependabot PR. -# - Reads the PR's update-type metadata via the dependabot/fetch-metadata -# action (no free-text parsing). -# - Requires CI to be green before merge (GitHub's auto-merge enforces -# required status checks). -# - Gates merge behind a severity+ecosystem policy table. Default is -# low+medium security updates only. -# -# Why auto-merge on GitHub (not via a bot like rhodibot) is the right -# layer: GitHub enforces branch protection + required checks natively, -# and the PR author is already `dependabot[bot]`. Rhodibot doesn't need -# to know anything about ecosystems — GitHub handles the merge mechanics -# once we approve. -# -# Threat model: -# - A compromised upstream package with a bogus security advisory -# could propose a malicious version bump. Mitigation: require at -# least one non-automated reviewer for HIGH+CRITICAL severity -# (done below — we explicitly refuse to auto-approve those). -# - A compromised Dependabot itself is an Akerlof claim-grounder -# problem. Not in scope here; track under -# `project_claim_grounders_dual_use_akerlof.md`. -# -# Dogfooding: this workflow template is itself subject to the same -# Dependabot config via the github-actions ecosystem block, so SHA -# bumps for dependabot/fetch-metadata flow through the same path. +# Never invokes `--admin`: GitHub's native `--auto` waits for all required +# status checks and code-scanning rulesets to pass (`squabble != bypass`). name: Dependabot Auto-Merge on: @@ -44,84 +22,99 @@ concurrency: cancel-in-progress: false permissions: contents: read - pull-requests: write # needed to approve - # NB: keep narrow — do NOT add secrets: read or id-token: write here. - # The write scope auto-merge needs is elevated PER JOB below, not here: a - # job-level permissions: block REPLACES this one, so the job restates both. + pull-requests: write jobs: automerge: - # Only run for PRs actually authored by Dependabot. - if: github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]' + if: github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.user.id == 49699333 permissions: - contents: write # needed to enable auto-merge (gh pr merge --auto) - pull-requests: write # needed to approve + contents: write + pull-requests: write runs-on: ubuntu-24.04 timeout-minutes: 15 steps: - name: Fetch Dependabot metadata id: meta - uses: dependabot/fetch-metadata@v3.1.0 + uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - # --- Policy gate ------------------------------------------------------- - # Outputs from fetch-metadata we care about: - # update-type → version-update:semver-{patch,minor,major} - # dependency-type → direct:{development,production} | indirect - # alert-state → AUTO_DISMISSED | DISMISSED | FIXED | OPEN - # ghsa-id → GHSA-... if this is a security PR - # --- Policy ------------------------------------------------------------- - # AUTO-APPROVE + AUTO-MERGE when: - # 1. This is a SECURITY update (ghsa-id present), AND - # 2. Update is patch or minor, AND - # 3. Severity ≤ moderate (Dependabot doesn't expose severity - # directly in fetch-metadata; infer from the absence of - # HIGH/CRITICAL labels added by Dependabot). - # Otherwise: do nothing. Human reviews HIGH+CRITICAL security - # updates and all non-security bumps. - - name: Decide policy outcome + github-token: ${{ github.token }} + + - name: Decide policy outcome (fail-closed on severity & semver) id: policy env: - GHSA_ID: ${{ steps.meta.outputs.ghsa-id }} + GH_TOKEN: ${{ github.token }} + META_GHSA_ID: ${{ steps.meta.outputs.ghsa-id }} + DEP_NAMES: ${{ steps.meta.outputs.dependency-names }} UPDATE_TYPE: ${{ steps.meta.outputs.update-type }} - PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }} + PR_BODY: ${{ github.event.pull_request.body }} run: | set -euo pipefail + action="manual_review" + severity="n/a" + reason="routine or non-qualifying update" + + # Populate GHSA_ID from fetch-metadata when available; otherwise extract + # the GHSA identifier from Dependabot's security PR body and cross-verify + # against the authoritative GitHub Advisory API + dependency name (never + # trusting PR text alone, and requiring no PAT secret). + GHSA_ID="$META_GHSA_ID" + if [ -z "$GHSA_ID" ] && [ -n "$PR_BODY" ]; then + GHSA_ID="$(printf '%s\n' "$PR_BODY" | grep -oE 'GHSA-[23456789cfghjmpqrvwx]{4}-[23456789cfghjmpqrvwx]{4}-[23456789cfghjmpqrvwx]{4}' | head -n 1 || true)" + fi - is_security=false - [ -n "$GHSA_ID" ] && is_security=true + if [ -n "$GHSA_ID" ]; then + if [[ "$UPDATE_TYPE" = "version-update:semver-patch" || "$UPDATE_TYPE" = "version-update:semver-minor" ]]; then + adv_json="$(gh api "/advisories/${GHSA_ID}" 2>/dev/null || echo '{}')" + severity="$(printf '%s' "$adv_json" | jq -r '.severity // "unknown"')" + cvss="$(printf '%s' "$adv_json" | jq -r 'if (.cvss.score | type) == "number" then (.cvss.score | tostring) else "invalid" end')" + pkg_match="$(printf '%s' "$adv_json" | jq -r --arg deps "$DEP_NAMES" ' + ($deps | split(",") | map(gsub("^\\s+|\\s+$"; "") | ascii_downcase) | map(select(length > 0))) as $dlist + | [ (.vulnerabilities // [])[].package.name // "" | ascii_downcase ] as $alist + | if ($dlist | length) > 0 and any($dlist[]; . as $d | any($alist[]; . == $d)) then "true" else "false" end + ')" + if [ "$pkg_match" = "true" ] && [[ "$severity" = "low" || "$severity" = "medium" ]] && [[ "$cvss" =~ ^[0-9]+(\.[0-9]+)?$ ]] && awk -v s="$cvss" 'BEGIN { exit !(s + 0 < 7.0) }'; then + action="automerge" + reason="verified ${severity} security ${UPDATE_TYPE#version-update:} (CVSS ${cvss} < 7.0)" + else + reason="security advisory ${GHSA_ID} (severity='${severity}', CVSS=${cvss}, pkg_match=${pkg_match}) — human review required" + fi + else + reason="security update ${GHSA_ID} is '${UPDATE_TYPE}' (major or unknown) — human review required" + fi + fi - # Owner policy (deliberate: velocity over caution). Auto-merge EVERY - # Dependabot update — patch, minor AND major, security or routine. - # Safe because GitHub's auto-merge only COMPLETES once the required - # checks (secret-scanner, codeql, hypatia-scan, openssf-compliance, - # build/test) are green: a bump that breaks fails CI and the PR stays - # OPEN with an email ("oi, it broke") instead of landing on a red - # main; a bump that passes lands within minutes with no chasing. - # This is preferred over making Dependabot a ruleset BYPASS actor, - # which would let bumps skip those very checks (no gate, no signal). - echo "action=automerge" >> "$GITHUB_OUTPUT" - echo "security=$is_security" >> "$GITHUB_OUTPUT" - echo "update_type=$UPDATE_TYPE" >> "$GITHUB_OUTPUT" - echo "ghsa=$GHSA_ID" >> "$GITHUB_OUTPUT" - - name: Approve PR (if policy allows) + { + echo "action=$action" + echo "severity=$severity" + echo "reason=$reason" + echo "update_type=$UPDATE_TYPE" + echo "ghsa=$GHSA_ID" + } >> "$GITHUB_OUTPUT" + + - name: Approve PR (when fail-closed policy allows) if: steps.policy.outputs.action == 'automerge' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} PR_URL: ${{ github.event.pull_request.html_url }} + GHSA_ID: ${{ steps.policy.outputs.ghsa }} + SEVERITY: ${{ steps.policy.outputs.severity }} + UPDATE_TYPE: ${{ steps.policy.outputs.update_type }} run: | gh pr review --approve "$PR_URL" \ - --body "Auto-approving Dependabot security update (${{ steps.policy.outputs.ghsa }}, ${{ steps.policy.outputs.update_type }}). Policy: low/moderate security patches/minors only." - - name: Enable auto-merge (if policy allows) + --body "Auto-approving Dependabot security update (${GHSA_ID}, severity=${SEVERITY}, ${UPDATE_TYPE}). Policy: verified low/medium security patch/minor only." + + - name: Enable auto-merge (when fail-closed policy allows) if: steps.policy.outputs.action == 'automerge' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} PR_URL: ${{ github.event.pull_request.html_url }} run: | gh pr merge --auto --squash "$PR_URL" + - name: Write decision to step summary env: ACTION: ${{ steps.policy.outputs.action }} - IS_SECURITY: ${{ steps.policy.outputs.security }} + SEVERITY: ${{ steps.policy.outputs.severity }} + REASON: ${{ steps.policy.outputs.reason }} UPDATE_TYPE: ${{ steps.policy.outputs.update_type }} GHSA: ${{ steps.policy.outputs.ghsa }} run: | @@ -130,8 +123,9 @@ jobs: echo "" echo "| Field | Value |" echo "|-------|-------|" - echo "| Policy action | \`$ACTION\` |" - echo "| Security update | \`$IS_SECURITY\` |" - echo "| Update type | \`$UPDATE_TYPE\` |" + echo "| Policy action | \`${ACTION}\` |" + echo "| Reason | ${REASON} |" + echo "| Advisory severity | \`${SEVERITY}\` |" + echo "| Update type | \`${UPDATE_TYPE}\` |" echo "| GHSA ID | \`${GHSA:-n/a}\` |" } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index f795d2e..819043d 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -6,7 +6,7 @@ on: paths: - 'build/container/**' - 'build/just/container.just' - - '.github/workflows/container-build.yml' + - '.github/workflows/docker-build.yml' push: tags: ['v*'] workflow_dispatch: @@ -32,6 +32,8 @@ jobs: contents: read steps: - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Tooling check run: | @@ -46,9 +48,11 @@ jobs: run: just container-verify - name: Scan image (trivy, best-effort) + env: + REPO_NAME: ${{ github.event.repository.name }} run: | if command -v trivy >/dev/null 2>&1; then - trivy image --severity HIGH,CRITICAL --exit-code 0 "${{ github.event.repository.name }}:latest" || true + trivy image --severity HIGH,CRITICAL --exit-code 0 "${REPO_NAME}:latest" || true else echo "trivy not installed on this runner — skipping image scan" fi diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 675e3fd..5d3a400 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -1,11 +1,11 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 -# This workflow is managed by gh actions-lock. # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # dogfood-gate.yml — Hyperpolymath Dogfooding Quality Gate # Validates that the repo uses hyperpolymath's own formats and tools. -# Companion to static-analysis-gate.yml (security) — this is for format compliance. +# Companion to static-analysis-gate.yml (security) and fork-governance.yml (PR +# DEED/K9/Hypatia lanes + unified grid comment). name: Dogfood Gate on: @@ -22,99 +22,155 @@ permissions: jobs: # --------------------------------------------------------------------------- - # Job 1: DEED manifest validation + # Job 1: DEED conformance on push (pull_request runs in fork-governance.yml + # per metadatastician/standards 3-practice/fork-governance/ADOPTION.adoc §3). # --------------------------------------------------------------------------- deed-validate: name: Validate DEED manifests + if: github.event_name != 'pull_request' runs-on: ubuntu-24.04 - timeout-minutes: 15 - + timeout-minutes: 10 + permissions: + contents: read steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 - - - name: Check for deed manifests - id: detect - run: | - COUNT=$(find . -name '*.deed' -not -path './.git/*' | wc -l) - echo "count=$COUNT" >> "$GITHUB_OUTPUT" - if [ "$COUNT" -eq 0 ]; then - echo "::warning::No .deed manifest found. Every RSR repo should have a repo deed (_chora.deed) at its root." - fi - - - name: Validate DEED manifests - if: steps.detect.outputs.count > 0 - uses: hyperpolymath/deed-ecosystem/validate-action@main + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - path: '.' - strict: 'false' - - - name: Write summary + persist-credentials: false + - name: Checkout canonical DEED validator + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: metadatastician/standards + ref: 8e48b1caade08a552646adeab66558ac43b4360d + path: .standards-deed + sparse-checkout: 1-formats/deed + sparse-checkout-cone-mode: true + persist-credentials: false + - name: Setup Bun runtime + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: '1.3.12' + - name: Run DEED validation hook run: | - MANIFEST_COUNT="${{ steps.detect.outputs.count }}" - if [ "$MANIFEST_COUNT" -eq 0 ]; then - cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" - ## DEED Manifest Validation - - :warning: **No .deed manifest found.** Every RSR-compliant repo should have a repo deed (`_chora.deed`) at its root. - - Copy it from [resource-algebra](https://github.com/hyperpolymath/resource-algebra). Manifests are validated against the DEED grammar (standards `1-formats/deed/`). The `.a2ml` format is retired; no `.a2ml` file may exist. - EOF - else - echo "## DEED Manifest Validation" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Scanned **${MANIFEST_COUNT}** .deed manifest(s). See step output for details." >> "$GITHUB_STEP_SUMMARY" - fi + set -euo pipefail + rm -rf "$RUNNER_TEMP/standards-deed" + mv "$PWD/.standards-deed" "$RUNNER_TEMP/standards-deed" + STANDARDS_DIR="$RUNNER_TEMP/standards-deed" bash .github/hooks/validate-deed.sh # --------------------------------------------------------------------------- - # Job 2: K9 contract validation + # Job 2: K9 conformance on push (pull_request runs in fork-governance.yml). # --------------------------------------------------------------------------- k9-validate: name: Validate K9 contracts + if: github.event_name != 'pull_request' runs-on: ubuntu-24.04 timeout-minutes: 15 - + permissions: + contents: read + env: + NICKEL_VERSION: '1.18.0' + NICKEL_SHA256: '9cba4dd65ae9915ec61f73033aafcff307a377665a83fd8f530df086763318cb' steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 - - - name: Check for K9 files - id: detect + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Checkout canonical K9 validator + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: metadatastician/standards + ref: 8e48b1caade08a552646adeab66558ac43b4360d + path: .standards-k9 + sparse-checkout: 1-formats/k9 + sparse-checkout-cone-mode: true + persist-credentials: false + - name: Install sha256-verified Nickel CLI run: | - COUNT=$(find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | wc -l) - CONFIG_COUNT=$(find . \( -name '*.toml' -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' \) \ - -not -path './.git/*' -not -path './node_modules/*' -not -path './.deno/*' \ - -not -name 'package-lock.json' -not -name 'Cargo.lock' -not -name 'deno.lock' | wc -l) - echo "k9_count=$COUNT" >> "$GITHUB_OUTPUT" - echo "config_count=$CONFIG_COUNT" >> "$GITHUB_OUTPUT" - if [ "$COUNT" -eq 0 ] && [ "$CONFIG_COUNT" -gt 0 ]; then - echo "::warning::Found $CONFIG_COUNT config files but no K9 contracts. Run k9iser to generate contracts." - fi + set -euo pipefail + URL="https://github.com/tweag/nickel/releases/download/${NICKEL_VERSION}/nickel-x86_64-linux" + curl -sSfL --proto '=https' --proto-redir '=https' --tlsv1.2 --retry 3 -o "$RUNNER_TEMP/nickel" "$URL" + echo "${NICKEL_SHA256} ${RUNNER_TEMP}/nickel" | sha256sum -c - + chmod +x "$RUNNER_TEMP/nickel" + echo "$RUNNER_TEMP" >> "$GITHUB_PATH" + - name: Run K9 validation hook + id: validate + run: | + set -euo pipefail + rm -rf "$RUNNER_TEMP/standards-k9" + mv "$PWD/.standards-k9" "$RUNNER_TEMP/standards-k9" + STANDARDS_DIR="$RUNNER_TEMP/standards-k9" bash .github/hooks/validate-k9.sh - - name: Validate K9 contracts - if: steps.detect.outputs.k9_count > 0 - uses: hyperpolymath/k9-ecosystem/validate-action@main + - name: Generate K9 badge + if: always() + env: + K9_RESULT: ${{ steps.validate.outcome }} + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/k9-grid" + jq -cn --arg result "$K9_RESULT" '{ + schemaVersion: 1, + label: "K9", + message: $result, + color: (if $result == "success" then "brightgreen" else "red" end) + }' > "$RUNNER_TEMP/k9-grid/badge.json" + + - name: Upload K9 grid + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - path: '.' - strict: 'false' + name: k9-grid + path: ${{ runner.temp }}/k9-grid/badge.json + if-no-files-found: error + + # The live README badge: badge.json (shields endpoint schema) published to + # the governance-badges branch on every push to main, through the contents + # API so the commit is GitHub-signed and no checkout is written to. + badges: + name: Publish the lane badges + needs: k9-validate + if: always() && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.k9-validate.result != 'skipped' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: write + steps: + - name: Download the grid + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: k9-grid + path: ${{ runner.temp }}/k9-grid - - name: Write summary + - name: Publish badges/k9.json to the governance-badges branch + env: + GH_TOKEN: ${{ github.token }} + BADGE: ${{ runner.temp }}/k9-grid/badge.json + BRANCH: governance-badges + DEST: badges/k9.json run: | - K9_COUNT="${{ steps.detect.outputs.k9_count }}" - CFG_COUNT="${{ steps.detect.outputs.config_count }}" - if [ "$K9_COUNT" -eq 0 ]; then - cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" - ## K9 Contract Validation - - :warning: **No K9 contract files found.** Run `k9iser` to generate contracts. - - Generate contracts with: `k9iser generate .` - EOF + set -euo pipefail + if [ ! -s "$BADGE" ]; then + echo "::error::the k9-grid artifact holds no badge.json" + exit 1 + fi + jq -e 'type == "object" and has("schemaVersion")' "$BADGE" >/dev/null + content="$(base64 -w0 "$BADGE")" + message="badges: K9 grid at ${GITHUB_SHA:0:12}" + if gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/$BRANCH" >/dev/null 2>&1; then + sha="$(gh api "repos/$GITHUB_REPOSITORY/contents/$DEST?ref=$BRANCH" --jq .sha 2>/dev/null || true)" + jq -n --arg c "$content" --arg s "$sha" --arg b "$BRANCH" --arg m "$message" \ + '{message: $m, content: $c, branch: $b} + (if $s != "" then {sha: $s} else {} end)' > "$RUNNER_TEMP/put.json" + gh api -X PUT "repos/$GITHUB_REPOSITORY/contents/$DEST" --input "$RUNNER_TEMP/put.json" >/dev/null else - echo "## K9 Contract Validation" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Validated **${K9_COUNT}** K9 contract(s) against **${CFG_COUNT}** config file(s)." >> "$GITHUB_STEP_SUMMARY" + jq -n --arg c "$content" '{content: $c, encoding: "base64"}' > "$RUNNER_TEMP/blob.json" + blob="$(gh api -X POST "repos/$GITHUB_REPOSITORY/git/blobs" --input "$RUNNER_TEMP/blob.json" --jq .sha)" + jq -n --arg b "$blob" --arg p "$DEST" '{tree: [{path: $p, mode: "100644", type: "blob", sha: $b}]}' > "$RUNNER_TEMP/tree.json" + tree="$(gh api -X POST "repos/$GITHUB_REPOSITORY/git/trees" --input "$RUNNER_TEMP/tree.json" --jq .sha)" + jq -n --arg t "$tree" --arg m "$message" '{message: $m, tree: $t, parents: []}' > "$RUNNER_TEMP/commit.json" + commit="$(gh api -X POST "repos/$GITHUB_REPOSITORY/git/commits" --input "$RUNNER_TEMP/commit.json" --jq .sha)" + jq -n --arg s "$commit" --arg r "refs/heads/$BRANCH" '{ref: $r, sha: $s}' > "$RUNNER_TEMP/ref.json" + gh api -X POST "repos/$GITHUB_REPOSITORY/git/refs" --input "$RUNNER_TEMP/ref.json" >/dev/null fi + echo "published $DEST on $BRANCH" # --------------------------------------------------------------------------- # Job 3: Empty-linter — invisible character detection @@ -123,10 +179,11 @@ jobs: name: "🔴 GATE: Empty-linter (invisible characters)" runs-on: ubuntu-24.04 timeout-minutes: 15 - steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Scan for invisible characters id: lint @@ -173,10 +230,12 @@ jobs: fi - name: Write summary + env: + READY: ${{ steps.lint.outputs.ready }} + FINDINGS: ${{ steps.lint.outputs.findings }} run: | - if [ "${{ steps.lint.outputs.ready }}" = "true" ]; then - FINDINGS="${{ steps.lint.outputs.findings }}" - if [ "$FINDINGS" -gt 0 ] 2>/dev/null; then + if [ "$READY" = "true" ]; then + if [ "${FINDINGS:-0}" -gt 0 ] 2>/dev/null; then echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "Found **${FINDINGS}** invisible character issue(s). See annotations above." >> "$GITHUB_STEP_SUMMARY" @@ -198,21 +257,18 @@ jobs: name: "🟡 CHECK: Groove manifest check" runs-on: ubuntu-24.04 timeout-minutes: 15 - steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Check for Groove manifest id: groove run: | - # Check for static or dynamic Groove endpoints HAS_MANIFEST="false" HAS_GROOVE_CODE="false" - # Canonical manifest location is www/.well-known/groove/ (issue #53); - # the repository-root path is accepted, with a warning, during the - # migration window. MANIFEST="" if [ -f "www/.well-known/groove/manifest.json" ]; then MANIFEST="www/.well-known/groove/manifest.json" @@ -223,12 +279,7 @@ jobs: if [ -n "$MANIFEST" ]; then HAS_MANIFEST="true" - # Validate the manifest JSON if ! jq empty "$MANIFEST" 2>/dev/null; then - # Gate, don't annotate: an unparseable manifest is a real error, - # not a warning — same behaviour as the standalone - # groove-check.yml (this job had drifted to annotation-only, - # the class of "check that cannot fail" from nexia-list#49). echo "::error file=$MANIFEST::Invalid JSON in Groove manifest" exit 1 else @@ -237,18 +288,10 @@ jobs: fi fi - # Check for Groove endpoint code (Rust, Elixir, Zig, V) if grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' --include='*.res' . 2>/dev/null | head -1 | grep -q .; then HAS_GROOVE_CODE="true" fi - # Check if this repo likely serves HTTP in production. Key on - # production HTTP-server framework markers only. A bare `TcpListener` - # is deliberately NOT a signal: it is dominated by test/utility use - # (e.g. a #[cfg(test)] loopback fake), so matching it produced a - # spurious groove nudge on client-only apps. A real hand-rolled Rust - # server still pairs the listener with `axum::serve`/`hyper::Server`, - # which are matched here. HAS_SERVER="false" if grep -rl 'Bandit\|Plug.Cowboy\|httpz\|vweb\|axum::serve\|actix_web\|hyper::Server\|rocket::build\|warp::serve' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' . 2>/dev/null | head -1 | grep -q .; then HAS_SERVER="true" @@ -263,14 +306,20 @@ jobs: fi - name: Write summary + env: + HAS_MANIFEST: ${{ steps.groove.outputs.has_manifest }} + HAS_GROOVE_CODE: ${{ steps.groove.outputs.has_groove_code }} + HAS_SERVER: ${{ steps.groove.outputs.has_server }} run: | - echo "## Groove Protocol Check" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "| Check | Status |" >> "$GITHUB_STEP_SUMMARY" - echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Static manifest (www/.well-known/groove/manifest.json) | ${{ steps.groove.outputs.has_manifest }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Groove endpoint in code | ${{ steps.groove.outputs.has_groove_code }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Has HTTP server code | ${{ steps.groove.outputs.has_server }} |" >> "$GITHUB_STEP_SUMMARY" + { + echo "## Groove Protocol Check" + echo "" + echo "| Check | Status |" + echo "|-------|--------|" + echo "| Static manifest (www/.well-known/groove/manifest.json) | ${HAS_MANIFEST} |" + echo "| Groove endpoint in code | ${HAS_GROOVE_CODE} |" + echo "| Has HTTP server code | ${HAS_SERVER} |" + } >> "$GITHUB_STEP_SUMMARY" # --------------------------------------------------------------------------- # Job 5: eclexiaiser manifest validation @@ -279,16 +328,16 @@ jobs: name: Validate eclexiaiser manifest runs-on: ubuntu-24.04 timeout-minutes: 15 - steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Check and validate eclexiaiser manifest id: eclex run: | if [ ! -f "eclexiaiser.toml" ]; then - # Check if repo has a Containerfile — if so, recommend eclexiaiser if [ -f "Containerfile" ]; then echo "::warning::Containerfile present but no eclexiaiser.toml. Run \`eclexiaiser init\` to scaffold energy/carbon budgets." fi @@ -297,9 +346,6 @@ jobs: fi echo "has_manifest=true" >> "$GITHUB_OUTPUT" - - # Validate eclexiaiser.toml structure (bash + grep; NO Python per estate policy). - # Structural presence checks only — deep schema validation is eclexiaiser's own job. err=0 grep -qE '^[[:space:]]*\[project\]' eclexiaiser.toml || { echo "::error file=eclexiaiser.toml::[project] section is required"; err=1; } grep -qE '^[[:space:]]*name[[:space:]]*=[[:space:]]*"[^"]+"' eclexiaiser.toml || { echo "::error file=eclexiaiser.toml::a non-empty name is required"; err=1; } @@ -311,8 +357,10 @@ jobs: echo "Valid: eclexiaiser.toml structure present (${fns} function block(s))" - name: Write summary + env: + HAS_MANIFEST: ${{ steps.eclex.outputs.has_manifest }} run: | - if [ "${{ steps.eclex.outputs.has_manifest }}" = "true" ]; then + if [ "$HAS_MANIFEST" = "true" ]; then echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo ":white_check_mark: **eclexiaiser.toml** present and valid." >> "$GITHUB_STEP_SUMMARY" @@ -324,32 +372,6 @@ jobs: # --------------------------------------------------------------------------- # Job 6: Canon lockstep — does this template still implement the canon? - # - # The canon<->spine binding. hyperpolymath/standards publishes canon.lock: the - # released identity of the law, with a sha256 per law artefact. This repo - # declares the SAME hashes in the `(canon …)` clause of its repo deed, - # resource-algebra_chora.deed. - # - # The pin used to live in a retired .a2ml profile, which D43 froze, so it - # could not follow canon 2.1.2 and this job went red on every PR (#215). The - # deed is the one-deed-per-repo home (#837), so the pin lives only there. - # - # Before this job, the declaration was two free-text strings - # (`spec = "rsr-criteria-v2"`, `declares-against = "2.0.0-draft"`) and nothing - # failed when the canon changed. This makes it a comparison. - # - # Deliberately a bare `run:` step with NO `uses:`. Adding an action reference - # would have required an actions.lock row, and this repo's lock records - # FLOATING refs ('actions/checkout@v7.0.1') where the canon's records SHAs - - # so a pinned `uses:` here would mismatch the lock and fail at LOAD time with - # `jobs=0` and no annotation. - # - # Auth: canon.lock lives in the public hyperpolymath/standards repo and is - # fetched anonymously (the job token is scoped to THIS repo and could not - # read another one anyway). This repo's own deed is fetched through the - # contents API with the job token, because raw.githubusercontent.com without - # a credential returns 404 for every PRIVATE repo minted from this template - # (measured 2026-10-06 on a private descendant: anonymous 404, token 200). # --------------------------------------------------------------------------- canon-lockstep: name: Canon lockstep @@ -363,25 +385,11 @@ jobs: run: | set -euo pipefail LOCK="$RUNNER_TEMP/canon.lock" - URL="https://raw.githubusercontent.com/hyperpolymath/standards/main/canon.lock" + URL="https://raw.githubusercontent.com/metadatastician/standards/main/canon.lock" if ! curl -fsSL --retry 3 --max-time 30 "$URL" -o "$LOCK"; then echo "::error::could not fetch canon.lock from $URL" - echo "The canon does not publish a released identity, so this repo" - echo "cannot be shown to implement it. Fix at source: add canon.lock." exit 1 fi - # Read (not compute) the criteria hash canon.lock asserts. - # - # awk only JOINS the record — the artefact entries are inline tables - # that span lines — then grep -oE extracts the hash. Deliberately the - # same two-step the canon's own check-canon-lockstep.sh uses, so - # there is one technique across both repos. - # - # Two traps this avoids, both found by running it rather than reading - # it. (1) An earlier version used `gsub(/.*"|"/,"",s)` to strip the - # quotes; the `.*"` is GREEDY, so it ate the entire string and the - # hash came out EMPTY. (2) awk interval expressions are not portable - # across mawk/gawk builds; grep -oE is. rec="$(awk ' /^[[:space:]]*#/ { next } /^criteria[[:space:]]*=/ { on=1 } @@ -394,8 +402,7 @@ jobs: | grep -oE 'sha256[[:space:]]*=[[:space:]]*"[0-9a-f]{64}"' \ | grep -oE '[0-9a-f]{64}' | head -1)" if [ -z "$want" ]; then - echo "::error::canon.lock carries no criteria sha256 — malformed, or" - echo "the criteria record no longer starts a line with 'criteria ='." + echo "::error::canon.lock carries no criteria sha256" exit 1 fi echo "want=$want" >> "$GITHUB_OUTPUT" @@ -403,20 +410,8 @@ jobs: - name: Compare this repo's declared pin env: WANT: ${{ steps.canon.outputs.want }} - # This job has NO `uses:` steps, deliberately: any added action would - # have to be pinned in actions.lock. That means no actions/checkout — - # and therefore NO WORKING TREE. An earlier version read a path from - # a directory that was never populated and failed 100% of the time. - # - # Fetch the deed by SHA instead, exactly as canon.lock is fetched - # above. Deliberately NOT github.sha: on pull_request that is the - # ephemeral MERGE commit, which is not on the remote and would 404. - # The event's head sha is the commit that actually exists there. HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} REPO: ${{ github.repository }} - # The job token (contents: read) authenticates the deed fetch, so the - # gate also works in private repositories. Passed as a header, never - # in the URL, so it cannot leak into a log line. TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -433,18 +428,6 @@ jobs: fi echo "deed: $DEED_NAME @ ${HEAD_SHA:0:7}" - # ALL THREE PINS, BOTH SIDES. Comparing criteria alone once let - # `version` and `gates_sha256` sit stale while the pin claimed all - # three (standards docs/AUDIT.adoc F8). A pin nothing verifies is not - # a binding. - - # deed_canon : print the value of : from the - # deed's one (canon …) clause, or nothing when the clause or key is - # absent or the clause is not unique. A ";" outside a string starts a - # comment that runs to line-end (deed.abnf), whole-line or inline. Backslash-free on purpose (see - # build/just/repo-init.just canon_key). The SAME function is in - # build/just/repo-init.just, this workflow, and standards - # scripts/check-canon-lockstep.sh; keep the three identical. deed_canon() { _clauses="$(awk 'BEGIN { bs = sprintf("%c", 92); dq = sprintf("%c", 34) } { o = ""; q = 0; for (i = 1; i <= length($0); i++) { c = substr($0, i, 1); if (q && c == bs) { o = o c substr($0, i + 1, 1); i++; continue } if (c == dq) q = !q; else if (!q && c == ";") break; o = o c } printf "%s ", o }' "$2" | grep -oE '[(]canon[[:space:]][^()]*[)]')" || true _n="$(echo "$_clauses" | grep -c '(canon' || true)" @@ -454,8 +437,6 @@ jobs: fi echo "$_clauses" | grep -oE ':'"$1"'[[:space:]]+"[^"]*"' | head -1 | sed -E 's/^[^"]*"//; s/"$//' || true } - # canon_key : print from the [canon] section. - # Reads canon.lock. canon_key() { awk -v key="$1" ' /^[[:space:]]*#/ { next } @@ -464,9 +445,6 @@ jobs: sub(/^[^=]*=[[:space:]]*/, ""); gsub(/^["[:space:]]+|["[:space:]]+$/, ""); print; exit }' "$2" } - # artefact_hash : the sha256 of one artefact - # record. The records are inline tables spanning lines, so awk JOINS - # the record and grep extracts the hash. artefact_hash() { awk -v key="$1" ' $0 ~ "^" key "[[:space:]]*=" { f = 1 } @@ -482,9 +460,6 @@ jobs: got_gates="$(deed_canon gates-sha256 "$DEED")" got_ver="$(deed_canon version "$DEED")" - # An unreadable pin must not pass: `"" = ""` is a green build with - # nothing behind it. Validate SHAPE, not mere presence — a reader - # that captured the wrong token would otherwise compare garbage. for pair in "want_crit:$want_crit" "want_gates:$want_gates" \ "got_crit:$got_crit" "got_gates:$got_gates"; do if ! printf '%s' "${pair#*:}" | grep -qxE '[0-9a-f]{64}'; then @@ -515,23 +490,13 @@ jobs: row gates-sha256 "$want_gates" "$got_gates" } >> "$GITHUB_STEP_SUMMARY" - if [ "$ok" = "1" ]; then echo ":white_check_mark: This template implements the canon at HEAD." exit 0 fi echo "::error::canon lockstep broken — the template does not implement the canon at HEAD" - { - echo "" - echo ":x: **Lockstep broken.**" - echo "" - echo "The canon's law changed and this template has not adopted it." - echo "Order matters (\`canon.lock [canon.lockstep].order\` is" - echo "\`spine-adopts-then-canon-releases\`): re-pin the \`(canon …)\` clause" - echo "in \`$DEED_NAME\` to the new hashes and fix any criteria this repo" - echo "now fails, BEFORE the canon release lands." - } >> "$GITHUB_STEP_SUMMARY" exit 1 + # --------------------------------------------------------------------------- # Job 7: Dogfooding summary # --------------------------------------------------------------------------- @@ -541,64 +506,46 @@ jobs: timeout-minutes: 15 needs: [deed-validate, k9-validate, empty-lint, groove-check, eclexiaiser-validate, canon-lockstep] if: always() - steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Generate dogfooding scorecard run: | SCORE=0 MAX=6 - - # DEED manifest present? if find . -name '*.deed' -not -path './.git/*' | head -1 | grep -q .; then - SCORE=$((SCORE + 1)) - DEED_STATUS=":white_check_mark:" + SCORE=$((SCORE + 1)); DEED_STATUS=":white_check_mark:" else DEED_STATUS=":x:" fi - - # K9 contracts present? if find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | head -1 | grep -q .; then - SCORE=$((SCORE + 1)) - K9_STATUS=":white_check_mark:" + SCORE=$((SCORE + 1)); K9_STATUS=":white_check_mark:" else K9_STATUS=":x:" fi - - # .editorconfig present? if [ -f ".editorconfig" ]; then - SCORE=$((SCORE + 1)) - EC_STATUS=":white_check_mark:" + SCORE=$((SCORE + 1)); EC_STATUS=":white_check_mark:" else EC_STATUS=":x:" fi - - # Groove manifest or code? if [ -f "www/.well-known/groove/manifest.json" ] || [ -f ".well-known/groove/manifest.json" ] || grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' . 2>/dev/null | head -1 | grep -q .; then - SCORE=$((SCORE + 1)) - GROOVE_STATUS=":white_check_mark:" + SCORE=$((SCORE + 1)); GROOVE_STATUS=":white_check_mark:" else GROOVE_STATUS=":ballot_box_with_check:" fi - - # VeriSimDB integration? if grep -rl 'verisimdb\|VeriSimDB' --include='*.toml' --include='*.yaml' --include='*.yml' --include='*.json' --include='*.rs' --include='*.ex' . 2>/dev/null | head -1 | grep -q .; then - SCORE=$((SCORE + 1)) - VSDB_STATUS=":white_check_mark:" + SCORE=$((SCORE + 1)); VSDB_STATUS=":white_check_mark:" else VSDB_STATUS=":ballot_box_with_check:" fi - - # eclexiaiser energy tracking? if [ -f "eclexiaiser.toml" ]; then - SCORE=$((SCORE + 1)) - ECLEX_STATUS=":white_check_mark:" + SCORE=$((SCORE + 1)); ECLEX_STATUS=":white_check_mark:" else ECLEX_STATUS=":ballot_box_with_check:" fi - cat <> "$GITHUB_STEP_SUMMARY" ## Dogfooding Scorecard @@ -606,52 +553,55 @@ jobs: | Tool/Format | Status | Notes | |-------------|--------|-------| - | DEED repo deed (`_chora.deed`) | ${DEED_STATUS} | Required for all RSR repos | + | DEED repo deed (\`_chora.deed\`) | ${DEED_STATUS} | Required for all RSR repos | | K9 contracts | ${K9_STATUS} | Required for repos with config files | | .editorconfig | ${EC_STATUS} | Required for all repos | | Groove endpoint | ${GROOVE_STATUS} | Required for service repos | | VeriSimDB integration | ${VSDB_STATUS} | Required for stateful repos | | eclexiaiser | ${ECLEX_STATUS} | Energy/carbon budgets for container services | - - --- - *Generated by the [Dogfood Gate](https://github.com/hyperpolymath/resource-algebra) workflow.* - *Dogfooding is guinea pig fooding — we test our tools on ourselves.* EOF rsr-score-self: name: RSR oracle — dogfood this repo runs-on: ubuntu-24.04 + timeout-minutes: 20 steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + path: subject fetch-depth: 0 - - uses: actions/checkout@v7.0.1 + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: hyperpolymath/standards + repository: metadatastician/standards + ref: 8e48b1caade08a552646adeab66558ac43b4360d path: _standards - - uses: actions/checkout@v7.0.1 + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/hypatia + ref: d73f76b5cb7a35e1aa1024c916aee7cb4f9b141e path: _hypatia - - name: Setup Elixir for the oracle - uses: erlef/setup-beam@v1.24.1 + persist-credentials: false + - name: Setup Elixir 1.19.4 / OTP 28.3 for Hypatia oracle + uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 with: elixir-version: '1.19.4' otp-version: '28.3' + - name: Cache Hypatia Mix deps and build + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + _hypatia/deps + _hypatia/_build + ~/.hex + ~/.mix + key: hypatia-oracle-${{ runner.os }}-otp28-elx1194-d73f76b5cb7a35e1aa1024c916aee7cb4f9b141e - name: Compile the oracle working-directory: _hypatia - run: mix deps.get && mix compile + run: mix local.hex --force && mix local.rebar --force && mix deps.get && mix compile - name: Score this repo against the canon working-directory: _hypatia run: | - # --ssot is explicit until the hypatia M-3 fix lands (the - # --standards default still points at the pre-reorg path). - mix hypatia.rsr_score .. \ + mix hypatia.rsr_score ../subject \ --ssot ../_standards/0-canon/rsr/rsr-criteria-v2.a2ml - # No --write: the oracle prints the scorecard to this log, and --write - # would put a .a2ml file in the tree (the format is retired; the criteria - # file named by --ssot is the canon's, in hyperpolymath/standards). - # Deliberately NO --fail-under: the point is to publish the honest - # scorecard (provisional=true until the coverage worklist is triaged), - # not to gate the spine on itself. Tightens in the PR that lands the - # first coverage tranche. diff --git a/.github/workflows/eclexiaiser-validate.yml b/.github/workflows/eclexiaiser-validate.yml index b748b94..e0e5e25 100644 --- a/.github/workflows/eclexiaiser-validate.yml +++ b/.github/workflows/eclexiaiser-validate.yml @@ -24,6 +24,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Check and validate eclexiaiser manifest id: eclex @@ -52,8 +54,10 @@ jobs: echo "Valid: eclexiaiser.toml structure present (${fns} function block(s))" - name: "Write summary" + env: + HAS_MANIFEST: ${{ steps.eclex.outputs.has_manifest }} run: | - if [ "${{ steps.eclex.outputs.has_manifest }}" = "true" ]; then + if [ "$HAS_MANIFEST" = "true" ]; then echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo ":white_check_mark: **eclexiaiser.toml** present and valid." >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/empty-linter.yml b/.github/workflows/empty-linter.yml index f622c49..dcb890d 100644 --- a/.github/workflows/empty-linter.yml +++ b/.github/workflows/empty-linter.yml @@ -24,6 +24,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Scan for invisible characters id: lint @@ -70,10 +72,12 @@ jobs: fi - name: "Write summary" + env: + READY: ${{ steps.lint.outputs.ready }} + FINDINGS: ${{ steps.lint.outputs.findings }} run: | - if [ "${{ steps.lint.outputs.ready }}" = "true" ]; then - FINDINGS="${{ steps.lint.outputs.findings }}" - if [ "$FINDINGS" -gt 0 ] 2>/dev/null; then + if [ "$READY" = "true" ]; then + if [ "${FINDINGS:-0}" -gt 0 ] 2>/dev/null; then echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "Found **${FINDINGS}** invisible character issue(s). See annotations above." >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/estate-rules.yml b/.github/workflows/estate-rules.yml index 1726316..d52526d 100644 --- a/.github/workflows/estate-rules.yml +++ b/.github/workflows/estate-rules.yml @@ -27,7 +27,9 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false # Prove the instrument works before trusting its reading. This gate was # one-directional for months, which is indistinguishable from a working # gate until you try to make it fail. @@ -118,3 +120,96 @@ jobs: # conforming" upstream. - name: Template conformance gate self-test (must fail on drift) run: bash tests/workflows/template_conformance_test.sh + # runtime-policy.yml folded into governance (#244). Both validators must + # require governance.yml, and a retired gate's name must not stand in. + - name: Validators require governance in place of the runtime gate + run: bash tests/workflows/runtime_policy_fold_test.sh + # Bun is the estate's first-choice runtime; the governance test below + # parses the workflow and its lock with Bun.YAML. Pinned and verified, + # as the other tools here are. + - name: Install verified Bun 1.4.2 + run: | + curl --proto '=https' --proto-redir '=https' --fail --silent --show-error --location \ + https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64.zip \ + --output "$RUNNER_TEMP/bun.zip" + echo "36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913 $RUNNER_TEMP/bun.zip" | sha256sum --check --strict + unzip -q "$RUNNER_TEMP/bun.zip" -d "$RUNNER_TEMP" + echo "$RUNNER_TEMP/bun-linux-x64" >> "$GITHUB_PATH" + # The runtime gate stays retired, governance runs for pull requests to + # any branch, and it calls the standards reusable at the commit its lock + # records. + - name: Governance replaces the runtime gate for every pull request + run: bash tests/workflows/governance_policy_test.sh + # KYAML ratchet (standards YAML-POLICY §3.4, §5 step 5, standards#1024): + # every file listed here has been converted and must stay KYAML. A file + # joins the list in the PR that converts it and never leaves it. The + # formatter is standards' own kyaml-format.sh at a pinned commit, the one + # rewriter the step-2 comment-preservation proof covers, so this repo + # carries no copy that can drift. yq is pinned too: kyaml-format.sh exits + # 2 (red) on a yq that cannot write KYAML, never green. Workflows wait for + # step 6 (standards#1025); files a third-party bot or tool rewrites or + # finds by schema (Dependabot, FUNDING, issue forms, settings, CodeRabbit, + # Copilot, GitLab CI, pre-commit) are not listed, as in standards. + - name: Install verified yq 4.53.3 and kyaml-format.sh (standards 8e48b1c) + run: | + mkdir -p "$RUNNER_TEMP/yq-bin" + curl --proto '=https' --proto-redir '=https' --fail --silent --show-error --location \ + https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_linux_amd64 \ + --output "$RUNNER_TEMP/yq-bin/yq" + echo "fa52a4e758c63d38299163fbdd1edfb4c4963247918bf9c1c5d31d84789eded4 $RUNNER_TEMP/yq-bin/yq" | sha256sum --check --strict + chmod +x "$RUNNER_TEMP/yq-bin/yq" + echo "$RUNNER_TEMP/yq-bin" >> "$GITHUB_PATH" + curl --proto '=https' --proto-redir '=https' --fail --silent --show-error --location \ + https://raw.githubusercontent.com/metadatastician/standards/8e48b1caade08a552646adeab66558ac43b4360d/scripts/kyaml-format.sh \ + --output "$RUNNER_TEMP/kyaml-format.sh" + echo "5fddd58fc8cbd2b68766ef5b4b5ba39f299da3f27252ae6cdcff0a641bbaef18 $RUNNER_TEMP/kyaml-format.sh" | sha256sum --check --strict + # Container narrowing at mint (build/just/container.just) can delete any + # of these files. The template (archetypes/ present) must keep all of + # them; a minted repository checks the ones it kept and names the rest. + - name: Converted YAML stays KYAML + run: | + set -euo pipefail + converted=( + build/container/compose.yaml + build/container/compose.example.yaml + build/container/stapeln/.gatekeeper.yaml + ) + present=() + for file in "${converted[@]}"; do + if [ -f "$file" ]; then + present+=("$file") + elif [ -d archetypes ]; then + echo "::error::$file is missing from the template" + exit 1 + else + echo "::notice::$file was removed at mint by container narrowing; not checked" + fi + done + if [ "${#present[@]}" -eq 0 ]; then + echo "No converted YAML is left in this repository; nothing to check." + exit 0 + fi + bash "$RUNNER_TEMP/kyaml-format.sh" --check "${present[@]}" + - name: Estate composite actions stay KYAML + run: | + set -euo pipefail + if [ -f .github/actions/squabble/action.yml ]; then + bash "$RUNNER_TEMP/kyaml-format.sh" --check .github/actions/squabble/action.yml + if [ -f scripts/kyaml-format.sh ]; then + bash scripts/kyaml-format.sh --check .github/actions/squabble/action.yml + fi + fi + - name: Converted container configs keep their data (template only) + run: bash tests/workflows/container_kyaml_test.sh + - name: KYAML ratchet self-test (installs verify, gate propagates, narrowing) + run: bash tests/workflows/kyaml_ratchet_test.sh + - name: I-JSON (RFC 7493) + JCS (RFC 8785) canonical gate & self-test + run: | + set -euo pipefail + bash tests/workflows/check_ijson_jcs_test.sh + bash scripts/check-ijson-jcs.sh --enforce . + - name: Required status contexts & squabble anti-bypass gate + run: | + set -euo pipefail + bash tests/workflows/squabble_gate_test.sh + bash scripts/check-required-contexts.sh . diff --git a/.github/workflows/fork-governance.yml b/.github/workflows/fork-governance.yml new file mode 100644 index 0000000..e44e3ed --- /dev/null +++ b/.github/workflows/fork-governance.yml @@ -0,0 +1,124 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# fork-governance.yml — PR-time entry point for DEED syntax, K9-DEED L0–L2 +# conformance, and unified summary reporting, pinned to +# metadatastician/standards@8e48b1caade08a552646adeab66558ac43b4360d. +name: Fork governance + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + deed: + name: DEED syntax + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Checkout canonical DEED validator + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: metadatastician/standards + ref: 8e48b1caade08a552646adeab66558ac43b4360d + path: .standards-deed + sparse-checkout: 1-formats/deed + sparse-checkout-cone-mode: true + persist-credentials: false + + - name: Setup Bun runtime + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: '1.3.12' + + - name: Validate DEED manifests and block retired .a2ml files + run: | + set -euo pipefail + STANDARDS_ABS="$PWD/.standards-deed" + rm -rf "$RUNNER_TEMP/standards-deed" + mv "$STANDARDS_ABS" "$RUNNER_TEMP/standards-deed" + STANDARDS_DIR="$RUNNER_TEMP/standards-deed" bash .github/hooks/validate-deed.sh + + k9: + name: K9-DEED conformance + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + env: + NICKEL_VERSION: '1.18.0' + NICKEL_SHA256: '9cba4dd65ae9915ec61f73033aafcff307a377665a83fd8f530df086763318cb' + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Checkout canonical K9 validator and contract + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: metadatastician/standards + ref: 8e48b1caade08a552646adeab66558ac43b4360d + path: .standards-k9 + sparse-checkout: 1-formats/k9 + sparse-checkout-cone-mode: true + persist-credentials: false + + - name: Install sha256-verified Nickel CLI + run: | + set -euo pipefail + URL="https://github.com/tweag/nickel/releases/download/${NICKEL_VERSION}/nickel-x86_64-linux" + curl -sSfL --proto '=https' --proto-redir '=https' --tlsv1.2 --retry 3 -o "$RUNNER_TEMP/nickel" "$URL" + echo "${NICKEL_SHA256} ${RUNNER_TEMP}/nickel" | sha256sum -c - + chmod +x "$RUNNER_TEMP/nickel" + echo "$RUNNER_TEMP" >> "$GITHUB_PATH" + + - name: Validate K9 contracts (L0–L2) + run: | + set -euo pipefail + rm -rf "$RUNNER_TEMP/standards-k9" + mv "$PWD/.standards-k9" "$RUNNER_TEMP/standards-k9" + STANDARDS_DIR="$RUNNER_TEMP/standards-k9" bash .github/hooks/validate-k9.sh + + verdict: + name: Fork governance verdict + needs: [deed, k9] + if: always() + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + env: + DEED_RESULT: ${{ needs.deed.result }} + K9_RESULT: ${{ needs.k9.result }} + steps: + - name: Render fork governance summary + run: | + set -euo pipefail + { + echo "## Fork Governance Conformance" + echo "" + echo "| Lane | Result |" + echo "|------|--------|" + echo "| DEED syntax | \`${DEED_RESULT}\` |" + echo "| K9-DEED conformance (L0–L2) | \`${K9_RESULT}\` |" + } >> "$GITHUB_STEP_SUMMARY" + if [ "$DEED_RESULT" != "success" ] || [ "$K9_RESULT" != "success" ]; then + echo "::error::Fork governance failed (deed=${DEED_RESULT}, k9=${K9_RESULT})" + exit 1 + fi diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 4609772..030c7c8 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -6,16 +6,69 @@ on: push: branches: [main, master] pull_request: - branches: [main, master] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + permissions: - actions: read # required by the reusable workflow (staleness check reads workflow runs) contents: read +# Self-contained execution of the shared estate governance policies pinned to +# metadatastician/standards@8e48b1caade08a552646adeab66558ac43b4360d +# (replacing the retired runtime-policy.yml workflow per PR #244). jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + name: Estate Governance Policy + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + env: + STANDARDS_PIN: '8e48b1caade08a552646adeab66558ac43b4360d' + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Checkout pinned standards policy scripts + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: metadatastician/standards + ref: 8e48b1caade08a552646adeab66558ac43b4360d + path: .standards-checkout + sparse-checkout: | + scripts + 0-canon + sparse-checkout-cone-mode: true + persist-credentials: false + + - name: Enforce TypeScript and npm/yarn runtime policy + run: | + set -euo pipefail + if [ -f .standards-checkout/scripts/check-ts-allowlist.sh ]; then + bash .standards-checkout/scripts/check-ts-allowlist.sh + fi + rm -rf .standards-checkout + + bad_ts="$(git ls-files '*.ts' '*.tsx' | grep -vE '^(archetypes/|templates/|tests/fixtures/)' || true)" + if [ -n "$bad_ts" ]; then + echo "::error::Unallowlisted TypeScript files detected:" + printf '%s\n' "$bad_ts" + exit 1 + fi + + bad_locks="$(git ls-files 'package-lock.json' '**/package-lock.json' 'yarn.lock' '**/yarn.lock' 'pnpm-lock.yaml' '**/pnpm-lock.yaml' || true)" + if [ -n "$bad_locks" ]; then + echo "::error::Banned npm/yarn/pnpm lockfiles detected (use Deno or Bun per LANGUAGE-POLICY):" + printf '%s\n' "$bad_locks" + exit 1 + fi + + if [ -f tsconfig.json ] || [ -f Makefile ] || [ -f GNUmakefile ]; then + echo "::error::Banned root config (tsconfig.json / Makefile) detected" + exit 1 + fi + echo "Governance runtime and language anti-pattern checks passed." diff --git a/.github/workflows/groove-check.yml b/.github/workflows/groove-check.yml index 09a6fcf..605d365 100644 --- a/.github/workflows/groove-check.yml +++ b/.github/workflows/groove-check.yml @@ -24,6 +24,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Check for Groove manifest id: groove @@ -81,11 +83,17 @@ jobs: fi - name: "Write summary" + env: + HAS_MANIFEST: ${{ steps.groove.outputs.has_manifest }} + HAS_GROOVE_CODE: ${{ steps.groove.outputs.has_groove_code }} + HAS_SERVER: ${{ steps.groove.outputs.has_server }} run: | - echo "## Groove Protocol Check" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "| Check | Status |" >> "$GITHUB_STEP_SUMMARY" - echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Static manifest (www/.well-known/groove/manifest.json) | ${{ steps.groove.outputs.has_manifest }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Groove endpoint in code | ${{ steps.groove.outputs.has_groove_code }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Has HTTP server code | ${{ steps.groove.outputs.has_server }} |" >> "$GITHUB_STEP_SUMMARY" + { + echo "## Groove Protocol Check" + echo "" + echo "| Check | Status |" + echo "|-------|--------|" + echo "| Static manifest (www/.well-known/groove/manifest.json) | ${HAS_MANIFEST} |" + echo "| Groove endpoint in code | ${HAS_GROOVE_CODE} |" + echo "| Has HTTP server code | ${HAS_SERVER} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 734f602..e7d9108 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -1,18 +1,16 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # -# Standalone Hypatia security scan (push / PR / weekly). This is NOT a duplicate -# of the `hypatia-scan` job in `static-analysis-gate.yml`: that job exists to -# feed the gitbot-fleet LEARNING pipeline (via `deposit-findings`), whereas this -# workflow is the repo's independent security scan. Same tool, two consumers. -# See .github/workflows/README.adoc. +# Self-contained Hypatia neurosymbolic security scan + SARIF upload (`category: +# hypatia`) satisfying the `Hypatia` code-scanning tool requirement in the +# `Base` branch ruleset on `main`, as well as push, weekly schedule, and manual +# dispatch runs. name: Hypatia Security Scan on: + pull_request: push: branches: [main, master, develop] - pull_request: - branches: [main, master] schedule: - cron: '0 0 * * 0' workflow_dispatch: @@ -20,11 +18,103 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + permissions: - actions: read # required by the reusable workflow (staleness check reads workflow runs) contents: read - security-events: write jobs: hypatia: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + name: Hypatia Neurosymbolic Analysis + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + contents: read + security-events: write + env: + HYPATIA_SHA: 'd73f76b5cb7a35e1aa1024c916aee7cb4f9b141e' + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Elixir 1.19.4 / OTP 28.3 for Hypatia scanner + uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + elixir-version: '1.19.4' + otp-version: '28.3' + + - name: Cache pinned Hypatia build + id: cache-hypatia + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/hypatia + ~/.hex + ~/.mix + key: hypatia-sarif-${{ runner.os }}-otp28-elx1194-d73f76b5cb7a35e1aa1024c916aee7cb4f9b141e + + - name: Build pinned Hypatia scanner + run: | + set -euo pipefail + if [ ! -x "$HOME/hypatia/hypatia" ] || [ ! -x "$HOME/hypatia/hypatia-cli.sh" ]; then + rm -rf "$HOME/hypatia" + git init "$HOME/hypatia" + git -C "$HOME/hypatia" remote add origin https://github.com/hyperpolymath/hypatia.git + git -C "$HOME/hypatia" fetch --depth=1 origin "$HYPATIA_SHA" + git -C "$HOME/hypatia" checkout --detach FETCH_HEAD + ( + cd "$HOME/hypatia" + mix local.hex --force + mix local.rebar --force + mix deps.get + MIX_NO_PUBSUB=1 mix escript.build + ) + chmod +x "$HOME/hypatia/hypatia" "$HOME/hypatia/hypatia-cli.sh" + fi + + - name: Run Hypatia JSON and SARIF analysis + id: scan + run: | + set -euo pipefail + HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json + jq -e 'type == "array"' hypatia-findings.json >/dev/null + + HYPATIA_FORMAT=sarif "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia.sarif + jq -e '.runs | type == "array" and length > 0' hypatia.sarif >/dev/null + + CRITICAL="$(jq '[.[] | select(.severity == "critical")] | length' hypatia-findings.json)" + HIGH="$(jq '[.[] | select(.severity == "high")] | length' hypatia-findings.json)" + TOTAL="$(jq 'length' hypatia-findings.json)" + { + echo "critical=$CRITICAL" + echo "high=$HIGH" + echo "total=$TOTAL" + } >> "$GITHUB_OUTPUT" + + - name: Upload Hypatia SARIF to GitHub Code Scanning + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 + with: + sarif_file: hypatia.sarif + category: hypatia + + - name: Enforce zero critical Hypatia findings + env: + CRITICAL_COUNT: ${{ steps.scan.outputs.critical }} + HIGH_COUNT: ${{ steps.scan.outputs.high }} + TOTAL_COUNT: ${{ steps.scan.outputs.total }} + run: | + set -euo pipefail + { + echo "## Hypatia Neurosymbolic Scan" + echo "" + echo "- Critical: ${CRITICAL_COUNT}" + echo "- High: ${HIGH_COUNT}" + echo "- Total findings: ${TOTAL_COUNT}" + } >> "$GITHUB_STEP_SUMMARY" + if [ "$CRITICAL_COUNT" != "0" ]; then + echo "::error::Hypatia reported ${CRITICAL_COUNT} critical finding(s)" + exit 1 + fi diff --git a/.github/workflows/k9-validate.yml b/.github/workflows/k9-validate.yml deleted file mode 100644 index 56f0029..0000000 --- a/.github/workflows/k9-validate.yml +++ /dev/null @@ -1,64 +0,0 @@ -# This workflow is managed by gh actions-lock. -# SPDX-License-Identifier: MPL-2.0 -name: "🟡 CHECK: K9 Contract Validation" - -on: - pull_request: - branches: ['**'] - push: - branches: [main, master] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - k9-validate: - name: "🟡 CHECK: Validate K9 contracts" - runs-on: ubuntu-24.04 - timeout-minutes: 15 - - steps: - - name: Checkout repository - uses: actions/checkout@v7.0.1 - - - name: Check for K9 files - id: detect - run: | - COUNT=$(find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | wc -l) - CONFIG_COUNT=$(find . \( -name '*.toml' -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' \) \ - -not -path './.git/*' -not -path './node_modules/*' -not -path './.deno/*' \ - -not -name 'package-lock.json' -not -name 'Cargo.lock' -not -name 'deno.lock' | wc -l) - echo "k9_count=$COUNT" >> "$GITHUB_OUTPUT" - echo "config_count=$CONFIG_COUNT" >> "$GITHUB_OUTPUT" - if [ "$COUNT" -eq 0 ] && [ "$CONFIG_COUNT" -gt 0 ]; then - echo "::warning::Found $CONFIG_COUNT config files but no K9 contracts. Run k9iser to generate contracts." - fi - - - name: "🟡 CHECK: Validate K9 contracts" - if: steps.detect.outputs.k9_count > 0 - uses: hyperpolymath/k9-ecosystem/validate-action@main - with: - path: '.' - strict: 'false' - - - name: Write summary - run: | - K9_COUNT="${{ steps.detect.outputs.k9_count }}" - CFG_COUNT="${{ steps.detect.outputs.config_count }}" - if [ "$K9_COUNT" -eq 0 ]; then - cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" - ## K9 Contract Validation - - :warning: **No K9 contract files found.** Repos with configuration files should have K9 contracts. - - Generate contracts with: `k9iser generate .` - EOF - else - echo "## K9 Contract Validation" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Validated **${K9_COUNT}** K9 contract(s) against **${CFG_COUNT}** config file(s)." >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/.github/workflows/label-triage.yml b/.github/workflows/label-triage.yml index 1bb0c35..3b8aa96 100644 --- a/.github/workflows/label-triage.yml +++ b/.github/workflows/label-triage.yml @@ -51,7 +51,7 @@ jobs: steps: - name: Classify and label env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} NUM: ${{ github.event.issue.number || inputs.issue }} run: | set -uo pipefail diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 06c8579..c496ffd 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Apply canonical labels env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} # ⚠ LOAD-BEARING. This workflow deliberately does not check the repo # out (no `uses:`, so no actions.lock entry can drift), which means # `gh label create` / `gh label edit` have no git remote to infer a diff --git a/.github/workflows/main-estate-audit.yml b/.github/workflows/main-estate-audit.yml index c856ee3..758b056 100644 --- a/.github/workflows/main-estate-audit.yml +++ b/.github/workflows/main-estate-audit.yml @@ -1,20 +1,68 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 -# This workflow is managed by gh actions-lock. name: Central Estate CI/CD Audit -# The called reusable declares `permissions: contents: read`; a caller must grant -# at least what the reusable declares, and this grants exactly that — no more. permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] - workflow_call: + branches: ["main"] + workflow_dispatch: +# Executes the enforcing gates from hyperpolymath/cicd-suite pinned at a full +# 40-hex commit SHA without a cross-repository workflow_call (avoiding the +# GitHub runner 0-job rejection on onboarded cross-repo workflow_call callers). jobs: - call-estate-audit: - uses: hyperpolymath/cicd-suite/.github/workflows/main-estate-audit.yml@55556cf5ba71ba744695861503c13148d3915a5d # cicd-suite MAIN (branch-reachable): remote-form callee — 27 full-SHA refs, de-onboarded; witnessed cross-repo in rsr runs 35282081912 + 35283168495 (jobs spawn; gates audit THIS repo). Prior f9e173a pin (via #137) = deleted PR head: pull-ref-only reachability -> 0-job creation rejection; its onboarded @main/tag form was separately proven startup_failure cross-repo (35280642055). Repin on callee updates; never to PR-head commits. + estate-audit: + name: Central Estate CI/CD Audit + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Checkout pinned cicd-suite gates + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/cicd-suite + ref: bf7dd95cf49733019343ff485c2f45c6b261839c + path: .cicd-suite-tmp + persist-credentials: false + + - name: Run enforcing cicd-suite governance, hygiene, and language gates + env: + REPO_OWNER: ${{ github.repository_owner }} + REPO_NAME: ${{ github.repository }} + run: | + set -euo pipefail + rm -rf "$RUNNER_TEMP/cicd-suite" + mv .cicd-suite-tmp "$RUNNER_TEMP/cicd-suite" + SUITE="$RUNNER_TEMP/cicd-suite/actions" + + for gate in affirmation-check referencing-check code-hygiene-check secrets-check boj-cartridge-check; do + echo "=== cicd-suite: $gate ===" + bash "$SUITE/$gate/check.sh" . + done + + echo "=== cicd-suite: spdx-license-check ===" + test -f LICENSE.txt || test -f LICENSE || test -f LICENSE.md || test -f LICENSE.adoc + + echo "=== cicd-suite: idris2-abi & zig-unified-api-adapter ===" + if find . -name '*.zig' -not -path './.git/*' | grep -q .; then + if git grep -E 'std\.fs|std\.io' -- '*.zig'; then + echo "::error::Zig FFI modules contain prohibited side-effects (std.fs/std.io)" + exit 1 + fi + fi + echo "Central Estate CI/CD Audit passed." diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 5e19042..5f0d3cf 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -1,28 +1,34 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 name: Mirror to Git Forges + on: push: branches: [main] workflow_dispatch: + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false + permissions: - actions: read # required by the reusable workflow (staleness check reads workflow runs) contents: read + jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 - # Explicit secrets map — no `secrets: inherit` (Hypatia WH008, alert #131). - # All seven are the callee's complete optional contract (standards - # mirror-reusable.yml@da2c748); behaviour is unchanged, future secrets - # are no longer shared implicitly. - secrets: - GITLAB_SSH_KEY: ${{ secrets.GITLAB_SSH_KEY }} - BITBUCKET_SSH_KEY: ${{ secrets.BITBUCKET_SSH_KEY }} - CODEBERG_SSH_KEY: ${{ secrets.CODEBERG_SSH_KEY }} - SOURCEHUT_SSH_KEY: ${{ secrets.SOURCEHUT_SSH_KEY }} - DISROOT_SSH_KEY: ${{ secrets.DISROOT_SSH_KEY }} - GITEA_SSH_KEY: ${{ secrets.GITEA_SSH_KEY }} - RADICLE_KEY: ${{ secrets.RADICLE_KEY }} + name: Forge mirror readiness + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout repository with full history + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Verify mirror ref integrity + run: | + set -euo pipefail + echo "Verified commit $(git rev-parse HEAD) (${GITHUB_REF:-HEAD}) ready for downstream forge pull mirrors." diff --git a/.github/workflows/proven-tests.yml b/.github/workflows/proven-tests.yml new file mode 100644 index 0000000..8135e18 --- /dev/null +++ b/.github/workflows/proven-tests.yml @@ -0,0 +1,108 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# +# proven-tests.yml — Full 3-outcome testing, proof honesty, data-format, and +# Six-Sigma leave-one-out benchmark regression suite grounded in +# hyperpolymath/proven-tests-and-benches and hyperpolymath/ijson-jcs. +name: Proven Tests and Benches + +on: + pull_request: + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: proven-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + proven-suite: + name: Proven 3-outcome test & bench suite + runs-on: ubuntu-24.04 + timeout-minutes: 20 + env: + NICKEL_VERSION: '1.18.0' + NICKEL_SHA256: '9cba4dd65ae9915ec61f73033aafcff307a377665a83fd8f530df086763318cb' + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Checkout canon validators into RUNNER_TEMP (metadatastician/standards) + run: | + set -euo pipefail + git init -q "$RUNNER_TEMP/standards" + git -C "$RUNNER_TEMP/standards" remote add origin https://github.com/metadatastician/standards.git + git -C "$RUNNER_TEMP/standards" config core.sparseCheckout true + printf '1-formats/deed/\n1-formats/k9/\n' > "$RUNNER_TEMP/standards/.git/info/sparse-checkout" + git -C "$RUNNER_TEMP/standards" fetch -q --depth 1 origin 8e48b1caade08a552646adeab66558ac43b4360d + git -C "$RUNNER_TEMP/standards" checkout -q FETCH_HEAD + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + - name: Install verified Nickel 1.18.0 (K9 L2 semantics) + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/nickel-bin" + curl --proto '=https' --proto-redir '=https' --tlsv1.2 --fail --silent --show-error --location \ + "https://github.com/tweag/nickel/releases/download/${NICKEL_VERSION}/nickel-x86_64-linux" \ + --output "$RUNNER_TEMP/nickel-bin/nickel" + echo "${NICKEL_SHA256} $RUNNER_TEMP/nickel-bin/nickel" | sha256sum --check --strict + chmod +x "$RUNNER_TEMP/nickel-bin/nickel" + echo "$RUNNER_TEMP/nickel-bin" >> "$GITHUB_PATH" + - name: Install verified Zig 0.16.0 (matching mise.lock) + run: | + set -euo pipefail + curl --proto '=https' --proto-redir '=https' --fail --silent --show-error --location \ + https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz \ + --output "$RUNNER_TEMP/zig.tar.xz" + echo "70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00 $RUNNER_TEMP/zig.tar.xz" | sha256sum --check --strict + mkdir -p "$RUNNER_TEMP/zig" + tar -xJf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP/zig" --strip-components=1 + echo "$RUNNER_TEMP/zig" >> "$GITHUB_PATH" + + - name: Install verified Just 1.56.0 (matching mise.lock) + run: | + set -euo pipefail + curl --proto '=https' --proto-redir '=https' --fail --silent --show-error --location \ + https://github.com/casey/just/releases/download/1.56.0/just-1.56.0-x86_64-unknown-linux-musl.tar.gz \ + --output "$RUNNER_TEMP/just.tar.gz" + echo "fa2a8ec1015d9df5330941ade12437488fc40d33f9c9f8cd4eb70a26de11b639 $RUNNER_TEMP/just.tar.gz" | sha256sum --check --strict + mkdir -p "$RUNNER_TEMP/just-bin" + tar -xzf "$RUNNER_TEMP/just.tar.gz" -C "$RUNNER_TEMP/just-bin" just + chmod +x "$RUNNER_TEMP/just-bin/just" + echo "$RUNNER_TEMP/just-bin" >> "$GITHUB_PATH" + + - name: Cache pinned ijson-jcs and panic-attack binaries + id: cache-ijson + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.cargo/bin/ijson-jcs + ~/.cargo/bin/panic-attack + key: proven-tools-${{ runner.os }}-ijson-2c880594c8b1-pa-b5530834a839 + + - name: Build pinned hyperpolymath/ijson-jcs & panic-attack when uncached + if: steps.cache-ijson.outputs.cache-hit != 'true' + run: | + set -euo pipefail + cargo install --locked \ + --git https://github.com/hyperpolymath/ijson-jcs.git \ + --rev 2c880594c8b1f823a7ab2a8a830c894975a1792e \ + --bin ijson-jcs + cargo install --locked \ + --git https://github.com/hyperpolymath/panic-attack.git \ + --rev b5530834a839734d1d181bfe6edb0d43d4233cf4 + + - name: Run 3-outcome proven test, proof honesty, FFI, E2E & bench suite + env: + IJSON_JCS: ijson-jcs + run: | + set -euo pipefail + export PATH="$HOME/.cargo/bin:$PATH" + STANDARDS_DIR="$RUNNER_TEMP/standards" bash scripts/run-proven-suite.sh --require-zig --bench diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 931704f..a5d3307 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -22,7 +22,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Check file permissions run: | find . -type f -perm /111 -name "*.sh" | head -10 || true @@ -56,7 +58,7 @@ jobs: run: | find . -type f -size +1M -not -path "./.git/*" | head -10 || echo "No large files" - name: EditorConfig check - uses: editorconfig-checker/action-editorconfig-checker@v3.0.0 + uses: editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393 # v3.0.0 continue-on-error: true docs: runs-on: ubuntu-24.04 @@ -64,7 +66,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Check documentation run: | MISSING="" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d7c8ea..7522976 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -139,7 +139,7 @@ jobs: # files: | # artifacts/* env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} # GitHub native artifact attestation (build provenance). Generates a # signed, verifiable claim binding each released artifact to this build # (commit, workflow, runner) via OIDC + Sigstore — verify with diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index d4d39e5..a63415c 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -1,19 +1,50 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 -# Rust CI — thin wrapper calling the shared estate reusable in -# hyperpolymath/standards. Configure once, propagate everywhere. -# See: docs/CI-REUSABLE-WORKFLOWS.adoc in standards. +# Rust CI — compiles standalone scripts/*.rs utilities with -D warnings and +# runs cargo check/test/clippy when a Cargo.toml is present. name: Rust CI + on: push: branches: [main, master] pull_request: + workflow_dispatch: + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + permissions: - actions: read contents: read + jobs: rust-ci: - uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + name: Rust standalone tools and workspace gate + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Compile standalone Rust helpers and run tests + run: | + set -euo pipefail + rustc --version + mkdir -p "$RUNNER_TEMP/rust-bins" + for src in scripts/*.rs; do + [ -f "$src" ] || continue + bin_name="$(basename "$src" .rs)" + echo "Compiling $src (-D warnings)..." + rustc --edition 2021 -D warnings "$src" -o "$RUNNER_TEMP/rust-bins/$bin_name" + done + if [ -f tests/workflows/archetype_lock_merge_test.sh ]; then + bash tests/workflows/archetype_lock_merge_test.sh + fi + if [ -f Cargo.toml ]; then + cargo check --all-targets --locked + cargo test --locked + fi diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index bd93efe..57c9881 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -10,18 +10,36 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + permissions: - actions: read # required by the reusable workflow (staleness check reads workflow runs) contents: read jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 - # Reusable called-workflow permissions are CAPPED by the caller's grants; - # without security-events: write here the scorecard SARIF upload fails with - # startup_failure (hypatia WF018). id-token: write enables OIDC publish. + if: github.event_name != 'pull_request' + name: OSSF Scorecard analysis + runs-on: ubuntu-24.04 + timeout-minutes: 15 permissions: actions: read contents: read security-events: write id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run OSSF Scorecard + uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0 + with: + results_file: results.sarif + results_format: sarif + publish_results: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + + - name: Upload Scorecard SARIF to GitHub Code Scanning + uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 + with: + sarif_file: results.sarif + category: scorecard diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 2adeddc..eaea280 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -1,29 +1,161 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 name: Secret Scanner + on: pull_request: push: branches: [main] + workflow_dispatch: + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + permissions: - actions: read contents: read -# Single secret scanner. The standards reusable runs gitleaks (+ a Rust-secrets -# check). An inline TruffleHog job previously lived here, but the reusable -# DELIBERATELY retired TruffleHog as redundant (gitleaks gives sufficient -# coverage at lower cost — see the reusable's header). Re-adding it was -# duplicated work, not extra coverage, so it has been removed. See -# .github/workflows/README.adoc. + +# Self-contained execution of the estate secret-scanning contract pinned to +# metadatastician/standards@8e48b1caade08a552646adeab66558ac43b4360d. +# Produces the exact `scan / gitleaks` status check required by the +# Secret-Scan-Floor ruleset on `main`. jobs: - scan: - # The reusable installs and executes a pinned gitleaks binary directly. - # Since standards#500 it neither comments on PRs nor reads workflow-run - # metadata, so contents: read is the complete permission contract. Keep - # this job-level block explicit: it replaces the workflow-level grant and - # is therefore the cap GitHub applies to the called workflow. + gitleaks: + name: "scan / gitleaks" + runs-on: ubuntu-24.04 + timeout-minutes: 15 permissions: contents: read - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + env: + GITLEAKS_VERSION: '8.18.4' + GITLEAKS_SHA256: 'ba6dbb656933921c775ee5a2d1c13a91046e7952e9d919f9bac4cec61d628e7d' + STANDARDS_SHA: '8e48b1caade08a552646adeab66558ac43b4360d' + steps: + - name: Checkout repository with full history + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Checkout estate gitleaks baseline + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: metadatastician/standards + ref: 8e48b1caade08a552646adeab66558ac43b4360d + path: .standards-baseline + sparse-checkout: config/gitleaks + sparse-checkout-cone-mode: true + persist-credentials: false + + - name: Stage estate baseline and install pinned gitleaks + run: | + set -euo pipefail + cp .standards-baseline/config/gitleaks/estate-baseline.toml .gitleaks-estate.toml + rm -rf .standards-baseline + test -s .gitleaks-estate.toml + + TARBALL="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + URL="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${TARBALL}" + curl -sSfL --proto '=https' --proto-redir '=https' --tlsv1.2 \ + --retry 3 --retry-delay 2 \ + -o "$RUNNER_TEMP/$TARBALL" "$URL" + echo "${GITLEAKS_SHA256} ${RUNNER_TEMP}/${TARBALL}" | sha256sum -c - + tar -xzf "$RUNNER_TEMP/$TARBALL" -C "$RUNNER_TEMP" gitleaks + chmod +x "$RUNNER_TEMP/gitleaks" + rm -f "$RUNNER_TEMP/$TARBALL" + + - name: Gitleaks working-tree pass + run: | + set -euo pipefail + CONFIG=".gitleaks-estate.toml" + if [ -f .gitleaks.toml ]; then + CONFIG=".gitleaks.toml" + fi + "$RUNNER_TEMP/gitleaks" detect \ + --source . \ + --no-git \ + --redact \ + --no-banner \ + --verbose \ + --config "$CONFIG" \ + --exit-code 1 + + - name: Gitleaks full-history pass + run: | + set -euo pipefail + if [ "$(git rev-parse --is-shallow-repository)" != "false" ]; then + echo "::error::checkout is shallow; refusing vacuous history scan" + exit 1 + fi + CONFIG=".gitleaks-estate.toml" + if [ -f .gitleaks.toml ]; then + CONFIG=".gitleaks.toml" + fi + "$RUNNER_TEMP/gitleaks" detect \ + --source . \ + --redact \ + --no-banner \ + --verbose \ + --config "$CONFIG" \ + --exit-code 1 + + - name: Gitleaks AsciiDoc mirror pass + run: | + set -euo pipefail + MIRROR="$RUNNER_TEMP/adoc-mirror" + rm -rf "$MIRROR" + mkdir -p "$MIRROR" + found=0 + while IFS= read -r -d '' f; do + dest="$MIRROR/$(dirname "$f")" + mkdir -p "$dest" + cp "$f" "$dest/$(basename "$f").txt" + found=$((found + 1)) + done < <(find . -path ./.git -prune -o \( -name '*.adoc' -o -name '*.asciidoc' \) -type f -print0) + + if [ "$found" -eq 0 ]; then + echo "No AsciiDoc files present." + exit 0 + fi + CONFIG="$PWD/.gitleaks-estate.toml" + if [ -f .gitleaks.toml ]; then + CONFIG="$PWD/.gitleaks.toml" + fi + "$RUNNER_TEMP/gitleaks" detect \ + --source "$MIRROR" \ + --no-git \ + --redact \ + --no-banner \ + --config "$CONFIG" \ + --exit-code 1 + + shell-secrets: + name: "scan / shell-secrets" + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run repository secret pattern hook + run: | + set -euo pipefail + rc=0 + git grep -IlE '(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82}|glpat-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----)' > "$RUNNER_TEMP/hits.txt" || rc=$? + # git grep exits 1 when nothing matches; above 1 is an error. + if [ "$rc" -gt 1 ]; then + echo "::error::Secret pattern scan failed with exit ${rc}" + exit 2 + fi + # Exclude intentional scanner rules and adversarial negative test fixtures. + filtered="$(grep -vE '^(\.github/hooks/scan-secrets\.sh|\.github/workflows/secret-scanner\.yml|\.machine_readable/testing/adversarial-content\.k9\.ncl|tests/)' "$RUNNER_TEMP/hits.txt" || :)" + if [ -n "$filtered" ]; then + echo "::error::Hardcoded credential material detected:" + printf '%s\n' "$filtered" + exit 1 + fi + echo "No hardcoded shell/source credentials detected." diff --git a/.github/workflows/security-policy.yml b/.github/workflows/security-policy.yml index 8d80728..6d9e43f 100644 --- a/.github/workflows/security-policy.yml +++ b/.github/workflows/security-policy.yml @@ -21,7 +21,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Security checks run: | FAILED=false diff --git a/.github/workflows/squabble.yml b/.github/workflows/squabble.yml new file mode 100644 index 0000000..290abe3 --- /dev/null +++ b/.github/workflows/squabble.yml @@ -0,0 +1,86 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# +# squabble.yml — CICD-Squabbler gate & cross-repo CI dependency chain verifier +# (hyperpolymath/cicd-squabbler; Doctrine rule 5: "Squabble, don't bypass"). +# +# Carries zero external `uses:` references (lockfile entry `[]`, matching +# cicd-squabbler's own squabble-action.yml pattern) so it can never be felled +# by a third-party action tag move or lockfile mismatch: +# 1. Checks out the exact commit under test via git HTTP header auth; +# 2. Installs Bun 1.4.2 (sha256-verified) and runs offline Silence + Firing +# self-tests (`squabble_gate_test.sh`) plus `check-required-contexts.sh`; +# 3. Installs the pinned `squabble` v0.1.0 release binary through +# `.github/actions/squabble/install.sh` (sha256 + GitHub SLSA provenance +# attestation verified before chmod +x); +# 4. Runs `squabble chains .= --follow 1` through +# `.github/actions/squabble/run.sh`, failing loudly (exit 4) on any +# cross-repo CI dependency cycle or dead upstream action/workflow. +name: CICD Squabbler + +on: + pull_request: + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + attestations: read + +concurrency: + group: squabble-${{ github.ref }} + cancel-in-progress: true + +jobs: + squabble: + name: Squabble CI chains & required-context gate + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Check out commit without external uses + env: + REPO: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + TOKEN: ${{ github.token }} + run: | + set -euo pipefail + AUTH="AUTHORIZATION: basic $(printf 'x-access-token:%s' "${TOKEN}" | base64 -w0)" + git init -q . + git remote add origin "https://github.com/${REPO}.git" + git -c http.extraheader="${AUTH}" fetch -q --depth 1 origin "${SHA}" + git checkout -q FETCH_HEAD + + - name: Install verified Bun 1.4.2 + run: | + set -euo pipefail + curl --proto '=https' --proto-redir '=https' --fail --silent --show-error --location \ + https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64.zip \ + --output "$RUNNER_TEMP/bun.zip" + echo "36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913 $RUNNER_TEMP/bun.zip" | sha256sum --check --strict + unzip -q "$RUNNER_TEMP/bun.zip" -d "$RUNNER_TEMP" + echo "$RUNNER_TEMP/bun-linux-x64" >> "$GITHUB_PATH" + + - name: Squabble guards & required-context self-tests + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + bash tests/workflows/squabble_gate_test.sh + bash scripts/check-required-contexts.sh . + + - name: Install pinned & SLSA-attested squabble v0.1.0 + id: install + env: + GH_TOKEN: ${{ github.token }} + run: bash .github/actions/squabble/install.sh + + - name: Audit cross-repo CI dependency chains (cycles & dead upstreams) + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + SQUABBLE_BIN: ${{ steps.install.outputs.path }} + run: | + set -euo pipefail + SQUABBLE_ARGS="$(printf 'chains\n.=%s\n--follow\n1\n' "${REPO}")" \ + bash .github/actions/squabble/run.sh diff --git a/.github/workflows/static-analysis-gate.yml b/.github/workflows/static-analysis-gate.yml index 0d27daf..4f009ca 100644 --- a/.github/workflows/static-analysis-gate.yml +++ b/.github/workflows/static-analysis-gate.yml @@ -1,7 +1,8 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 -# Static Analysis Gate — Required by branch protection rules. -# Runs panic-attack and hypatia, deposits findings for gitbot-fleet learning. +# Static Analysis Gate — Deep recon & fleet-learning deposit. +# Builds and caches pinned panic-attack, pons-asinorum, and Hypatia (OTP 27), +# executes real scans without silent fallback, and deposits unified findings. name: Static Analysis Gate on: pull_request: @@ -15,165 +16,166 @@ permissions: contents: read jobs: # --------------------------------------------------------------------------- - # Job 1: panic-attack assail + # Job 1: panic-attack assail + pons-asinorum deep recon # --------------------------------------------------------------------------- panic-attack-assail: - name: panic-attack assail + name: panic-attack & pons-asinorum deep recon runs-on: ubuntu-24.04 - timeout-minutes: 15 + timeout-minutes: 25 steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - - name: Install panic-attack (if available) - id: install + persist-credentials: false + + - name: Cache pinned panic-attack & pons-asinorum binaries + id: cache-recon + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.cargo/bin/panic-attack + ~/.cargo/bin/pons + key: deep-recon-v2-${{ runner.os }}-pa-b5530834a839-pons-51b8920f5417 + + - name: Build pinned panic-attack & pons-asinorum when uncached + if: steps.cache-recon.outputs.cache-hit != 'true' run: | - # Try to fetch the latest release binary from the org - PA_URL="https://github.com/hyperpolymath/panic-attack/releases/latest/download/panic-attack-linux-x86_64" - if curl -fsSL --head "$PA_URL" >/dev/null 2>&1; then - curl -fsSL -o /usr/local/bin/panic-attack "$PA_URL" - chmod +x /usr/local/bin/panic-attack - echo "installed=true" >> "$GITHUB_OUTPUT" - else - echo "::notice::panic-attack binary not available — skipping assail" - echo "installed=false" >> "$GITHUB_OUTPUT" - fi + set -euo pipefail + cargo install --locked \ + --git https://github.com/hyperpolymath/panic-attack.git \ + --rev b5530834a839734d1d181bfe6edb0d43d4233cf4 + cargo install --locked \ + --git https://github.com/hyperpolymath/pons-asinorum.git \ + --rev 51b8920f541782ceb9465ea3f0d33c96104fc1fb \ + --bin pons + - name: Run panic-attack assail id: assail - if: steps.install.outputs.installed == 'true' run: | - set +e - panic-attack assail --format json . > panic-attack-findings.json - PA_EXIT=$? - set -e + set -euo pipefail + export PATH="$HOME/.cargo/bin:$PATH" + PA_RAW="$RUNNER_TEMP/pa-report.json" + PA_LOG="$RUNNER_TEMP/pa-assail.log" + PA_EXIT=0 + panic-attack assail . --output "$PA_RAW" > "$PA_LOG" 2>&1 || PA_EXIT=$? - # Same defect class as the Hypatia job below: `2>&1` folded the - # scanner's stderr into the JSON payload, so every jq parse failed, - # every count silently became 0 via `|| echo 0`, and "Fail on critical - # findings" could never fire on any input. Keep stderr on the log. - if [ ! -s panic-attack-findings.json ]; then - echo "[]" > panic-attack-findings.json + if [ "$PA_EXIT" -ne 0 ] || [ ! -s "$PA_RAW" ]; then + cat "$PA_LOG" >&2 || : + echo "::error::panic-attack assail failed or produced empty report (exit ${PA_EXIT})" + exit 1 fi - # Deliberately a WARNING, not a failure. panic-attack is a downloaded - # release binary whose exit-code and output contract are not verified - # here, and it has no confirmed --exit-zero equivalent, so we surface a - # malformed payload in the log rather than block on an unverified tool. - # Promote to `exit 1` (as the Hypatia job does) once that contract is - # confirmed -- see the follow-up issue linked from this PR. - if ! jq -e 'type == "array"' panic-attack-findings.json >/dev/null 2>&1; then - echo "::warning::panic-attack output is not a JSON array (exit ${PA_EXIT}); counts below are unreliable" + if ! jq -e 'type == "object" and (.weak_points | type == "array")' "$PA_RAW" >/dev/null 2>&1; then + cat "$PA_LOG" >&2 || : + echo "::error::panic-attack assail did not produce a valid AssailReport JSON object" + exit 1 fi - # Parse finding counts - TOTAL=$(jq '. | length' panic-attack-findings.json 2>/dev/null || echo 0) - CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' panic-attack-findings.json 2>/dev/null || echo 0) - HIGH=$(jq '[.[] | select(.severity == "high")] | length' panic-attack-findings.json 2>/dev/null || echo 0) - MEDIUM=$(jq '[.[] | select(.severity == "medium" or .severity == "warn")] | length' panic-attack-findings.json 2>/dev/null || echo 0) - LOW=$(jq '[.[] | select(.severity == "low")] | length' panic-attack-findings.json 2>/dev/null || echo 0) - - echo "total=$TOTAL" >> "$GITHUB_OUTPUT" - echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT" - echo "high=$HIGH" >> "$GITHUB_OUTPUT" - echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT" - echo "low=$LOW" >> "$GITHUB_OUTPUT" - echo "exit_code=$PA_EXIT" >> "$GITHUB_OUTPUT" - - name: Emit check annotations - if: steps.install.outputs.installed == 'true' - run: | - # Convert JSON findings into GitHub Actions annotations - # Findings carry no `.message` (keys: action,file,line,reason,rule_module, - # severity,type), so every annotation read "null". `.file` is an absolute - # runner path, which GitHub cannot anchor to the diff, so it is made - # workspace-relative here. - jq -r --arg ws "$GITHUB_WORKSPACE" '.[] | select(.file != null) | - (.file | ltrimstr($ws + "/")) as $f | - (.reason // .message // .type // "finding") as $m | - if .severity == "critical" then - "::error file=\($f),line=\(.line // 1)::[panic-attack] \($m)" - elif .severity == "high" then - "::error file=\($f),line=\(.line // 1)::[panic-attack] \($m)" - else - "::warning file=\($f),line=\(.line // 1)::[panic-attack] \($m)" - end - ' panic-attack-findings.json || true - - name: Write step summary - if: steps.install.outputs.installed == 'true' - run: | - cat <> "$GITHUB_STEP_SUMMARY" - ## panic-attack assail Results - - | Severity | Count | - |----------|-------| - | Critical | ${{ steps.assail.outputs.critical }} | - | High | ${{ steps.assail.outputs.high }} | - | Medium | ${{ steps.assail.outputs.medium }} | - | Low | ${{ steps.assail.outputs.low }} | - | **Total**| ${{ steps.assail.outputs.total }} | - EOF - - name: Create stub findings (when panic-attack unavailable) - if: steps.install.outputs.installed != 'true' + jq '[.weak_points[] | select(.suppressed != true) | .severity = ((.severity // "") | ascii_downcase)]' \ + "$PA_RAW" > panic-attack-findings.json + + TOTAL=$(jq '. | length' panic-attack-findings.json) + CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' panic-attack-findings.json) + HIGH=$(jq '[.[] | select(.severity == "high")] | length' panic-attack-findings.json) + MEDIUM=$(jq '[.[] | select(.severity == "medium" or .severity == "warn")] | length' panic-attack-findings.json) + LOW=$(jq '[.[] | select(.severity == "low")] | length' panic-attack-findings.json) + + { + echo "total=$TOTAL" + echo "critical=$CRITICAL" + echo "high=$HIGH" + echo "medium=$MEDIUM" + echo "low=$LOW" + } >> "$GITHUB_OUTPUT" + + - name: Run pons-asinorum deep recon audit run: | - echo "[]" > panic-attack-findings.json - echo "## panic-attack assail" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY" + set -euo pipefail + export PATH="$HOME/.cargo/bin:$PATH" + command -v pons >/dev/null 2>&1 || { echo "::error::pons binary not found on PATH"; exit 1; } + pons --version + PONS_JSON="$RUNNER_TEMP/pons-report.json" + PONS_LOG="$RUNNER_TEMP/pons.log" + PONS_RC=0 + pons scan --format json . > "$PONS_JSON" 2> "$PONS_LOG" || PONS_RC=$? + if [ "$PONS_RC" -ne 0 ] || ! jq -e 'type == "object" and (.findings | type == "array")' "$PONS_JSON" >/dev/null 2>&1; then + cat "$PONS_LOG" >&2 || : + echo "::error::pons scan failed (exit ${PONS_RC})" + exit 1 + fi + ERR_COUNT="$(jq '[.findings[] | select((.evidence // "" | ascii_upcase) != "SPECULATIVE" and (.severity // "" | ascii_upcase) == "ERROR")] | length' "$PONS_JSON")" + WARN_COUNT="$(jq '[.findings[] | select((.severity // "" | ascii_upcase) == "WARN")] | length' "$PONS_JSON")" + INFO_COUNT="$(jq '[.findings[] | select((.severity // "" | ascii_upcase) == "INFO")] | length' "$PONS_JSON")" + { + echo "## pons-asinorum Deep Recon" + echo "" + echo "| Severity | Count |" + echo "|----------|-------|" + echo "| ERROR (non-speculative) | ${ERR_COUNT} |" + echo "| WARN | ${WARN_COUNT} |" + echo "| INFO | ${INFO_COUNT} |" + } >> "$GITHUB_STEP_SUMMARY" + - name: Upload panic-attack findings - uses: actions/upload-artifact@v7.0.2 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: panic-attack-findings path: panic-attack-findings.json retention-days: 90 + - name: Fail on critical findings - if: steps.install.outputs.installed == 'true' && steps.assail.outputs.critical > 0 + if: steps.assail.outputs.critical != '0' && steps.assail.outputs.critical != '' + env: + CRIT_COUNT: ${{ steps.assail.outputs.critical }} run: | - echo "::error::panic-attack found ${{ steps.assail.outputs.critical }} critical issue(s) — blocking merge" + echo "::error::panic-attack found ${CRIT_COUNT} critical issue(s) — blocking merge" exit 1 + # --------------------------------------------------------------------------- - # Job 2: hypatia-scan - # - # NOTE — this is NOT a duplicate of the standalone `hypatia-scan.yml`. - # * THIS job runs Hypatia inside the gate and hands its findings to the - # `deposit-findings` job below, which feeds the gitbot-fleet LEARNING - # pipeline (artifact -> fleet). It is a REQUIRED status check. - # * `hypatia-scan.yml` runs Hypatia standalone as the repo's security scan, - # independent of the fleet-learning deposit. - # Same tool, two different consumers — keep both. See .github/workflows/README.adoc. + # Job 2: hypatia-scan (pinned Elixir 1.19.4 / OTP 28.3 + cached escript) # --------------------------------------------------------------------------- hypatia-scan: name: Hypatia neurosymbolic scan runs-on: ubuntu-24.04 - timeout-minutes: 15 + timeout-minutes: 20 steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - - name: Setup Elixir for Hypatia scanner - id: beam - continue-on-error: true - uses: erlef/setup-beam@v1.24.1 + persist-credentials: false + + - name: Setup Elixir 1.19.4 / OTP 28.3 for Hypatia scanner + uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 with: elixir-version: '1.19.4' otp-version: '28.3' - - name: Clone and build Hypatia + + - name: Cache pinned Hypatia build + id: cache-hypatia + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/hypatia + ~/.hex + ~/.mix + key: hypatia-escript-${{ runner.os }}-otp28-elx1194-d73f76b5cb7a35e1aa1024c916aee7cb4f9b141e + + - name: Clone and build pinned Hypatia id: build - continue-on-error: true run: | - git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia" 2>/dev/null || true - if [ -f "$HOME/hypatia/mix.exs" ]; then - cd "$HOME/hypatia" - # Build escript if neither hypatia nor hypatia-v2 exists - if [ ! -f hypatia ] && [ ! -f hypatia-v2 ]; then - mix deps.get - mix escript.build - fi - echo "ready=true" >> "$GITHUB_OUTPUT" - else - echo "::notice::Hypatia scanner not available — skipping scan" - echo "ready=false" >> "$GITHUB_OUTPUT" + set -euo pipefail + if [ ! -f "$HOME/hypatia/hypatia" ] && [ ! -f "$HOME/hypatia/hypatia-v2" ]; then + rm -rf "$HOME/hypatia" + git init -q "$HOME/hypatia" + git -C "$HOME/hypatia" remote add origin https://github.com/hyperpolymath/hypatia.git + git -C "$HOME/hypatia" fetch -q --depth 1 origin d73f76b5cb7a35e1aa1024c916aee7cb4f9b141e + git -C "$HOME/hypatia" checkout -q FETCH_HEAD + (cd "$HOME/hypatia" && mix local.hex --force && mix local.rebar --force && mix deps.get && MIX_NO_PUBSUB=1 mix escript.build) fi + echo "ready=true" >> "$GITHUB_OUTPUT" + - name: Run Hypatia scan id: scan if: steps.build.outputs.ready == 'true' @@ -183,44 +185,32 @@ jobs: HYP_EXIT=$? set -e - # --exit-zero is Hypatia's own documented CI recipe (lib/hypatia/cli.ex), - # for exactly this case: "use in CI when a downstream step gates on - # severity counts". Findings go to stdout, the one-line summary to - # stderr, and the process exits 0 unless the SCANNER itself failed. - # - # Do NOT redirect stderr into the payload with `2>&1`: that folds the - # summary line into the JSON, so every parse fails, the old `[]` - # fallback substituted a clean result, CRITICAL was always 0, and the - # gate below could never fire on any input. Keep stderr on the log. if [ "$HYP_EXIT" -ne 0 ]; then echo "::error::Hypatia scanner execution failed with exit ${HYP_EXIT}" exit "$HYP_EXIT" fi - # `jq empty` is NOT sufficient -- it succeeds on any valid JSON, - # including a bare string, object or null. Assert the array. if [ ! -s hypatia-findings.json ] || ! jq -e 'type == "array"' hypatia-findings.json >/dev/null; then echo "::error::Hypatia did not produce a valid JSON findings array" exit 1 fi - TOTAL=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0) - CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' hypatia-findings.json 2>/dev/null || echo 0) - HIGH=$(jq '[.[] | select(.severity == "high")] | length' hypatia-findings.json 2>/dev/null || echo 0) - MEDIUM=$(jq '[.[] | select(.severity == "medium" or .severity == "warn")] | length' hypatia-findings.json 2>/dev/null || echo 0) - LOW=$(jq '[.[] | select(.severity == "low")] | length' hypatia-findings.json 2>/dev/null || echo 0) - - echo "total=$TOTAL" >> "$GITHUB_OUTPUT" - echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT" - echo "high=$HIGH" >> "$GITHUB_OUTPUT" - echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT" - echo "low=$LOW" >> "$GITHUB_OUTPUT" + TOTAL=$(jq '. | length' hypatia-findings.json) + CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' hypatia-findings.json) + HIGH=$(jq '[.[] | select(.severity == "high")] | length' hypatia-findings.json) + MEDIUM=$(jq '[.[] | select(.severity == "medium" or .severity == "warn")] | length' hypatia-findings.json) + LOW=$(jq '[.[] | select(.severity == "low")] | length' hypatia-findings.json) + + { + echo "total=$TOTAL" + echo "critical=$CRITICAL" + echo "high=$HIGH" + echo "medium=$MEDIUM" + echo "low=$LOW" + } >> "$GITHUB_OUTPUT" + - name: Emit check annotations if: steps.build.outputs.ready == 'true' run: | - # Findings carry no `.message` (keys: action,file,line,reason,rule_module, - # severity,type), so every annotation read "null". `.file` is an absolute - # runner path, which GitHub cannot anchor to the diff, so it is made - # workspace-relative here. jq -r --arg ws "$GITHUB_WORKSPACE" '.[] | select(.file != null) | (.file | ltrimstr($ws + "/")) as $f | (.reason // .message // .type // "finding") as $m | @@ -232,69 +222,57 @@ jobs: "::warning file=\($f),line=\(.line // 1)::[hypatia] \($m)" end ' hypatia-findings.json || true - - name: Write step summary - if: steps.build.outputs.ready == 'true' - run: | - cat <> "$GITHUB_STEP_SUMMARY" - ## Hypatia Scan Results - - | Severity | Count | - |----------|-------| - | Critical | ${{ steps.scan.outputs.critical }} | - | High | ${{ steps.scan.outputs.high }} | - | Medium | ${{ steps.scan.outputs.medium }} | - | Low | ${{ steps.scan.outputs.low }} | - | **Total**| ${{ steps.scan.outputs.total }} | - EOF - - name: Create stub findings (when Hypatia unavailable) - if: steps.build.outputs.ready != 'true' - run: | - echo "[]" > hypatia-findings.json - echo "## Hypatia Scan" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Skipped: Hypatia scanner not available in this environment." >> "$GITHUB_STEP_SUMMARY" + - name: Upload hypatia findings - uses: actions/upload-artifact@v7.0.2 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: hypatia-findings path: hypatia-findings.json retention-days: 90 - - name: Fail on critical security findings - if: steps.build.outputs.ready == 'true' && steps.scan.outputs.critical > 0 + + - name: Fail on critical findings + if: steps.build.outputs.ready == 'true' && steps.scan.outputs.critical != '0' && steps.scan.outputs.critical != '' + env: + CRIT_COUNT: ${{ steps.scan.outputs.critical }} run: | - echo "::error::Hypatia found ${{ steps.scan.outputs.critical }} critical security issue(s) — blocking merge" + echo "::error::Hypatia found ${CRIT_COUNT} critical security issue(s) — blocking merge" exit 1 + # --------------------------------------------------------------------------- # Job 3: patch-bridge triage (CVE contextual assessment) # --------------------------------------------------------------------------- patch-bridge-triage: name: Patch Bridge CVE triage runs-on: ubuntu-24.04 - timeout-minutes: 15 + timeout-minutes: 20 steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - - name: Install panic-attack (if available) - id: install + persist-credentials: false + + - name: Cache pinned panic-attack binary + id: cache-pa + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cargo/bin/panic-attack + key: panic-attack-${{ runner.os }}-b5530834a839734d1d181bfe6edb0d43d4233cf4 + + - name: Build pinned panic-attack when uncached + if: steps.cache-pa.outputs.cache-hit != 'true' run: | - PA_URL="https://github.com/hyperpolymath/panic-attack/releases/latest/download/panic-attack-linux-x86_64" - if curl -fsSL --head "$PA_URL" >/dev/null 2>&1; then - curl -fsSL -o /usr/local/bin/panic-attack "$PA_URL" - chmod +x /usr/local/bin/panic-attack - echo "installed=true" >> "$GITHUB_OUTPUT" - else - echo "::notice::panic-attack binary not available — skipping Patch Bridge" - echo "installed=false" >> "$GITHUB_OUTPUT" - fi + set -euo pipefail + cargo install --locked \ + --git https://github.com/hyperpolymath/panic-attack.git \ + --rev b5530834a839734d1d181bfe6edb0d43d4233cf4 + - name: Run Patch Bridge triage id: triage - if: steps.install.outputs.installed == 'true' run: | + export PATH="$HOME/.cargo/bin:$PATH" set +e - panic-attack bridge triage --format json . > bridge-report.json 2>&1 - PB_EXIT=$? + panic-attack bridge triage --format json . > bridge-report.json 2>"$RUNNER_TEMP/pb.err" set -e if [ ! -s bridge-report.json ] || ! jq empty bridge-report.json 2>/dev/null; then @@ -306,46 +284,45 @@ jobs: CONCATENATIVE=$(jq '.concatenative // 0' bridge-report.json) INFORMATIONAL=$(jq '.informational // 0' bridge-report.json) - echo "unmitigable=$UNMITIGABLE" >> "$GITHUB_OUTPUT" - echo "mitigated=$MITIGATED" >> "$GITHUB_OUTPUT" - echo "concatenative=$CONCATENATIVE" >> "$GITHUB_OUTPUT" - echo "informational=$INFORMATIONAL" >> "$GITHUB_OUTPUT" + { + echo "unmitigable=$UNMITIGABLE" + echo "mitigated=$MITIGATED" + echo "concatenative=$CONCATENATIVE" + echo "informational=$INFORMATIONAL" + } >> "$GITHUB_OUTPUT" + - name: Write step summary - if: steps.install.outputs.installed == 'true' - run: | - cat <> "$GITHUB_STEP_SUMMARY" - ## Patch Bridge CVE Triage - - | Classification | Count | - |----------------|-------| - | Unmitigable | ${{ steps.triage.outputs.unmitigable }} | - | Mitigated | ${{ steps.triage.outputs.mitigated }} | - | Concatenative | ${{ steps.triage.outputs.concatenative }} | - | Informational | ${{ steps.triage.outputs.informational }} | - - Unmitigable CVEs require dependency replacement or rearchitecture. - Mitigated CVEs have active controls with soundness proofs. - Concatenative risks are CVE combinations that multiply severity. - EOF - - name: Create stub report (when unavailable) - if: steps.install.outputs.installed != 'true' + env: + UNMIT: ${{ steps.triage.outputs.unmitigable }} + MIT: ${{ steps.triage.outputs.mitigated }} + CONCAT: ${{ steps.triage.outputs.concatenative }} + INFO: ${{ steps.triage.outputs.informational }} run: | - echo '{"cves":[],"mitigated":0,"unmitigable":0,"concatenative":0,"informational":0}' > bridge-report.json - echo "## Patch Bridge CVE Triage" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY" + { + echo "## Patch Bridge CVE Triage" + echo "" + echo "| Classification | Count |" + echo "|----------------|-------|" + echo "| Unmitigable | ${UNMIT} |" + echo "| Mitigated | ${MIT} |" + echo "| Concatenative | ${CONCAT} |" + echo "| Informational | ${INFO} |" + } >> "$GITHUB_STEP_SUMMARY" + - name: Upload bridge report - uses: actions/upload-artifact@v7.0.2 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: bridge-report path: bridge-report.json retention-days: 90 - - name: Fail on unmitigable CVEs in critical paths - if: steps.install.outputs.installed == 'true' && steps.triage.outputs.unmitigable > 0 + + - name: Warn on unmitigable CVEs in critical paths + if: steps.triage.outputs.unmitigable != '0' && steps.triage.outputs.unmitigable != '' + env: + UNMIT: ${{ steps.triage.outputs.unmitigable }} run: | - echo "::warning::Patch Bridge found ${{ steps.triage.outputs.unmitigable }} unmitigable CVE(s) — review required" - # Warning only, not blocking. Unmitigable means the developer needs - # to make an architectural decision, not that the PR is wrong. + echo "::warning::Patch Bridge found ${UNMIT} unmitigable CVE(s) — review required" + # --------------------------------------------------------------------------- # Job 4: deposit-findings (combines + archives for gitbot-fleet) # --------------------------------------------------------------------------- @@ -357,52 +334,57 @@ jobs: if: always() steps: - name: Download panic-attack findings - uses: actions/download-artifact@v8.0.2 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + continue-on-error: true with: name: panic-attack-findings path: findings/ - name: Download hypatia findings - uses: actions/download-artifact@v8.0.2 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + continue-on-error: true with: name: hypatia-findings path: findings/ - name: Download bridge report - uses: actions/download-artifact@v8.0.2 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + continue-on-error: true with: name: bridge-report path: findings/ - name: Combine findings into unified report id: combine + env: + REPO_SLUG: ${{ github.repository }} + COMMIT_SHA: ${{ github.sha }} + GIT_REF: ${{ github.ref }} + RUN_ID: ${{ github.run_id }} run: | + mkdir -p findings PA_FILE="findings/panic-attack-findings.json" HYP_FILE="findings/hypatia-findings.json" - # Ensure both files exist and are valid JSON arrays for f in "$PA_FILE" "$HYP_FILE"; do if [ ! -s "$f" ] || ! jq empty "$f" 2>/dev/null; then echo "[]" > "$f" fi done - # Tag each finding with its source scanner - jq '[.[] | . + {"scanner": "panic-attack"}]' "$PA_FILE" > /tmp/pa-tagged.json - jq '[.[] | . + {"scanner": "hypatia"}]' "$HYP_FILE" > /tmp/hyp-tagged.json + jq '[.[] | . + {"scanner": "panic-attack"}]' "$PA_FILE" > "$RUNNER_TEMP/pa-tagged.json" + jq '[.[] | . + {"scanner": "hypatia"}]' "$HYP_FILE" > "$RUNNER_TEMP/hyp-tagged.json" - # Read bridge report (CVE triage, not findings array) BRIDGE_FILE="findings/bridge-report.json" if [ ! -s "$BRIDGE_FILE" ] || ! jq empty "$BRIDGE_FILE" 2>/dev/null; then echo '{"cves":[],"mitigated":0,"unmitigable":0,"concatenative":0,"informational":0}' > "$BRIDGE_FILE" fi - # Build unified report envelope jq -n \ - --arg repo "${{ github.repository }}" \ - --arg sha "${{ github.sha }}" \ - --arg ref "${{ github.ref }}" \ - --arg run_id "${{ github.run_id }}" \ + --arg repo "$REPO_SLUG" \ + --arg sha "$COMMIT_SHA" \ + --arg ref "$GIT_REF" \ + --arg run_id "$RUN_ID" \ --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - --slurpfile pa /tmp/pa-tagged.json \ - --slurpfile hyp /tmp/hyp-tagged.json \ + --slurpfile pa "$RUNNER_TEMP/pa-tagged.json" \ + --slurpfile hyp "$RUNNER_TEMP/hyp-tagged.json" \ --slurpfile bridge "$BRIDGE_FILE" \ '{ schema_version: "1.1.0", @@ -421,34 +403,43 @@ jobs: MEDIUM=$(jq '[.findings[] | select(.severity == "medium" or .severity == "warn")] | length' findings/unified-findings.json) LOW=$(jq '[.findings[] | select(.severity == "low")] | length' findings/unified-findings.json) - echo "total=$TOTAL" >> "$GITHUB_OUTPUT" - echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT" - echo "high=$HIGH" >> "$GITHUB_OUTPUT" - echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT" - echo "low=$LOW" >> "$GITHUB_OUTPUT" + { + echo "total=$TOTAL" + echo "critical=$CRITICAL" + echo "high=$HIGH" + echo "medium=$MEDIUM" + echo "low=$LOW" + } >> "$GITHUB_OUTPUT" + - name: Upload unified findings (fleet scanner picks these up) - uses: actions/upload-artifact@v7.0.2 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: unified-findings path: findings/unified-findings.json retention-days: 90 + - name: Write deposit summary + env: + REPO_SLUG: ${{ github.repository }} + COMMIT_SHA: ${{ github.sha }} + CRIT: ${{ steps.combine.outputs.critical }} + HIGH_C: ${{ steps.combine.outputs.high }} + MED: ${{ steps.combine.outputs.medium }} + LOW_C: ${{ steps.combine.outputs.low }} + TOT: ${{ steps.combine.outputs.total }} run: | - cat <> "$GITHUB_STEP_SUMMARY" - ## Unified Findings Deposit - - **Repository:** ${{ github.repository }} - **Commit:** \`${{ github.sha }}\` - **Deposited at:** $(date -u +"%Y-%m-%d %H:%M:%S UTC") - - | Severity | Count | - |----------|-------| - | Critical | ${{ steps.combine.outputs.critical }} | - | High | ${{ steps.combine.outputs.high }} | - | Medium | ${{ steps.combine.outputs.medium }} | - | Low | ${{ steps.combine.outputs.low }} | - | **Total**| ${{ steps.combine.outputs.total }} | - - Findings saved as \`unified-findings\` artifact. - The gitbot-fleet scanner will ingest these on its next pass. - EOF + { + echo "## Unified Findings Deposit" + echo "" + echo "**Repository:** ${REPO_SLUG}" + echo "**Commit:** \`${COMMIT_SHA}\`" + echo "**Deposited at:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")" + echo "" + echo "| Severity | Count |" + echo "|----------|-------|" + echo "| Critical | ${CRIT} |" + echo "| High | ${HIGH_C} |" + echo "| Medium | ${MED} |" + echo "| Low | ${LOW_C} |" + echo "| **Total** | ${TOT} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index ef8483a..ea89dfa 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -67,12 +67,17 @@ jobs: echo "All workflows have permissions declared" # Bun is the estate's first-choice runtime (LANGUAGE-POLICY.adoc §1: - # Bun > pnpm > npm; Deno retired). Tag form - # matches every other ref in this repo and is resolved by actions.lock. - - name: Set up Bun - uses: oven-sh/setup-bun@v2.2.0 - with: - bun-version: latest + # Bun > Deno > pnpm > npm). It is the estate pin 1.4.2, downloaded and + # sha256-checked as Estate Rules installs its tools, so this gate runs + # no third-party action and no unpinned runtime. + - name: Install verified Bun 1.4.2 + run: | + curl --proto '=https' --proto-redir '=https' --fail --silent --show-error --location \ + https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64.zip \ + --output "$RUNNER_TEMP/bun.zip" + echo "36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913 $RUNNER_TEMP/bun.zip" | sha256sum --check --strict + unzip -q "$RUNNER_TEMP/bun.zip" -d "$RUNNER_TEMP" + echo "$RUNNER_TEMP/bun-linux-x64" >> "$GITHUB_PATH" - name: Check SHA-Pinned Actions run: | diff --git a/.machine_readable/arrival-pack/arrival-pack.ncl b/.machine_readable/arrival-pack/arrival-pack.ncl index 8f8f378..988cfb2 100644 --- a/.machine_readable/arrival-pack/arrival-pack.ncl +++ b/.machine_readable/arrival-pack/arrival-pack.ncl @@ -65,6 +65,14 @@ The descriptive family (working name *descriptiles*) describes what-is; the **co | k9 | *Validation*. Kennel (data) / Yard (pure eval) / Hunt (guarded exec). | ## Canon pointers +Estate data-format route: **kyq** handles exactly **TOML, Dhall, KYAML, +I-JSON/JCS**. Preserve TOML/Dhall; migrate YAML/YML to KYAML and JSON variants +through kyq using **hyperpolymath/ijson-jcs**, the actual estate implementation +repository. **kyX** owns wider projection. DEED/K9/coordination keep their own +grammars. Canon: `metadatastician/standards/3-practice/DATA-FORMAT-ROUTING.adoc`. +Planned support is not operational evidence: retain explicit transitional +checks until a pinned kyq executable passes positive and negative tests. + - `hyperpolymath/standards` — the canon source. · `hyperpolymath/gv-clade-index` — the estate map (identity registry). · `hyperpolymath/manifesto` — this doctrine. - **Before you invent, rename, or consolidate anything: STOP and check the map + IS-NOT.** diff --git a/.machine_readable/arrival-pack/claude-md.k9.ncl b/.machine_readable/arrival-pack/claude-md.k9.ncl index d4af629..6685d81 100644 --- a/.machine_readable/arrival-pack/claude-md.k9.ncl +++ b/.machine_readable/arrival-pack/claude-md.k9.ncl @@ -12,6 +12,7 @@ K9! { pedigree = { schema_version = "1.0.0", + component_type = "drift-check:claude-md-arrival-pack", contractile_verb = "trust", # provenance: the view must equal its source semantics = "projection-fidelity", security = { @@ -19,7 +20,7 @@ K9! trust_level = "read-only comparison", allow_network = false, allow_filesystem_write = false, - allow_subprocess = true, # runs extract.sh + nickel to reproduce the view + allow_subprocess = false, # verify.sh runs extract.sh + nickel; this record does not }, metadata = { name = "claude-md-arrival-pack", diff --git a/.machine_readable/arrival-pack/extract.sh b/.machine_readable/arrival-pack/extract.sh index c6e9a0d..9c108fe 100755 --- a/.machine_readable/arrival-pack/extract.sh +++ b/.machine_readable/arrival-pack/extract.sh @@ -98,7 +98,7 @@ anchor_clause="$(bash "$READ" --clause "$DEED" anchor)" h_deed="$(read_clauses | hash12)" h_anchor="$(printf '%s\n' "$anchor_clause" | hash12)" -jq -n \ +jq -cnS \ --arg canonical_name "$canonical_name" \ --arg prefixed_name "$prefixed_name" \ --arg uuid "$uuid" \ diff --git a/.machine_readable/coaptation/coapt.k9.ncl b/.machine_readable/coaptation/coapt.k9.ncl index d15eace..22b354a 100644 --- a/.machine_readable/coaptation/coapt.k9.ncl +++ b/.machine_readable/coaptation/coapt.k9.ncl @@ -13,6 +13,7 @@ K9! { pedigree = { schema_version = "1.0.0", + component_type = "drift-check:coaptation-receipt", contractile_verb = "trust", # provenance: the reading must equal its sources semantics = "coaptation-fidelity", security = { @@ -20,7 +21,7 @@ K9! trust_level = "read-only comparison", allow_network = false, allow_filesystem_write = false, - allow_subprocess = true, # runs the extractors + nickel to reproduce the reading + allow_subprocess = false, # verify.sh runs the extractors + nickel; this record does not }, metadata = { name = "coaptation", diff --git a/.machine_readable/coaptation/core/Coaptation.idr b/.machine_readable/coaptation/core/Coaptation.idr index 5593535..4314a13 100644 --- a/.machine_readable/coaptation/core/Coaptation.idr +++ b/.machine_readable/coaptation/core/Coaptation.idr @@ -21,6 +21,8 @@ module Coaptation import Data.Vect import Data.Fin +%default total + ||| The six normative contractile verbs. public export data Verb = Intend | Must | Trust | Adjust | Dust | Bust diff --git a/.machine_readable/coaptation/receipts/latest.toml b/.machine_readable/coaptation/receipts/latest.toml index 1e4f433..e271ff9 100644 --- a/.machine_readable/coaptation/receipts/latest.toml +++ b/.machine_readable/coaptation/receipts/latest.toml @@ -98,5 +98,5 @@ bust.sync-drift-between-repos = "latent" bust.contractile-parse-error = "latent" [provenance] -contractiles = "intend@feaaa58c4690 must@6a15dd3ccc3a trust@71566e6b64fc adjust@d7e4225fe5a8 dust@448489330834 bust@ea93d531c781" +contractiles = "intend@53c58b12670b must@df17f095d038 trust@62cd1ad0fb54 adjust@cfd661debafd dust@a3de1d6db05d bust@72320ddbc891" descriptiles = "deed@ab944221ee82 anchor@51194a4e21e8" diff --git a/.machine_readable/contractiles/README.adoc b/.machine_readable/contractiles/README.adoc index 5a4075e..299b4ad 100644 --- a/.machine_readable/contractiles/README.adoc +++ b/.machine_readable/contractiles/README.adoc @@ -150,16 +150,16 @@ enforces or validates concern declarations. k9 provides three trust-tier |=== | File | Trust tier | Description -| `../self-validating/template-kennel.k9.ncl` +| `../self-validating/template-kennel.k9.ncl.in` | Kennel | Pure data. No subprocess, no filesystem write, no network. Safe for metadata and declarative settings. -| `../self-validating/template-yard.k9.ncl` +| `../self-validating/template-yard.k9.ncl.in` | Yard | Nickel evaluation with contracts and validation. No side effects. -| `../self-validating/template-hunt.k9.ncl` +| `../self-validating/template-hunt.k9.ncl.in` | Hunt | Full execution surface. Must declare side effects, support dry-run, and be signed before the estate treats it as trustworthy automation. @@ -167,7 +167,7 @@ enforces or validates concern declarations. k9 provides three trust-tier The templates live in `.machine_readable/self-validating/`. Each verb's `.k9.ncl` instantiates the Hunt tier's shape; it takes its `pedigree` -contract from `_base.ncl` (the old `../k9/template-hunt.k9.ncl` import named +contract from `_base.ncl` (the old `../k9/template-hunt.k9.ncl.in` import named a path that no longer exists). === Why the naming rule does not apply diff --git a/.machine_readable/contractiles/adjust/adjust.k9.ncl b/.machine_readable/contractiles/adjust/adjust.k9.ncl index 48fc252..d4ce031 100644 --- a/.machine_readable/contractiles/adjust/adjust.k9.ncl +++ b/.machine_readable/contractiles/adjust/adjust.k9.ncl @@ -22,11 +22,23 @@ K9! # erosion causing gradual parameter drift). Where must flags "broken # now", adjust flags "drifting toward broken". +# +# 2026-10-19 (K9 contract v1.0.0, D173, standards#1058): this record is a +# DECLARATION evaluated at 'Yard. The probes it lists are run by the paired +# runner (adjust.ncl and the scripts/run-*.sh runners), which is the execution +# surface; this file spawns nothing. A 'Hunt record needs signature_required, +# a signature block and a verified signature (K9-S008/S009/C001); no +# contractile carries one, so 'Hunt here was a claim the gate could never honour. + let base = import "../_base.ncl" in { pedigree = base.pedigree_schema & { contractile_verb = "adjust", + schema_version = "1.0.0", + component_type = "contractile:adjust", + semantics = "drift tolerances + corrective actions", + side_effects = [], paired_xfile = "../adjust/adjust.k9.ncl#declaration", paired_runner = "../adjust/adjust.ncl", @@ -47,7 +59,7 @@ let base = import "../_base.ncl" in trust_level = "tolerance measurement + declared auto-fix", allow_network = false, allow_filesystem_write_conditional = true, # auto_fix_when_available may edit - allow_subprocess = true, + allow_subprocess = false, # the paired runner spawns; this record does not probe_scope = 'measurement_plus_declared_fix, }, diff --git a/.machine_readable/contractiles/bust/bust.k9.ncl b/.machine_readable/contractiles/bust/bust.k9.ncl index 113663e..4f4df68 100644 --- a/.machine_readable/contractiles/bust/bust.k9.ncl +++ b/.machine_readable/contractiles/bust/bust.k9.ncl @@ -23,15 +23,27 @@ K9! # past their expiry), and the "it works, ship it" pattern where a # caller silently starts using a must-not-run API. +# +# 2026-10-19 (K9 contract v1.0.0, D173, standards#1058): this record is a +# DECLARATION evaluated at 'Yard. The probes it lists are run by the paired +# runner (bust.ncl and the scripts/run-*.sh runners), which is the execution +# surface; this file spawns nothing. A 'Hunt record needs signature_required, +# a signature block and a verified signature (K9-S008/S009/C001); no +# contractile carries one, so 'Hunt here was a claim the gate could never honour. + let base = import "../_base.ncl" in { pedigree = base.pedigree_schema & { contractile_verb = "bust", + schema_version = "1.0.0", + component_type = "contractile:bust", + semantics = "hard-stop / expiry / must-not-run declarations", + side_effects = [], paired_xfile = "../bust/bust.k9.ncl#declaration", paired_runner = "../bust/bust.ncl", - tier = 'Hunt, + tier = 'Yard, authority = 'blocking, metadata = { @@ -44,12 +56,12 @@ let base = import "../_base.ncl" in }, security = { - leash = 'Hunt, - signature_required = true, + leash = 'Yard, + signature_required = false, trust_level = "read-only expiry + state-check with subprocess", allow_network = false, allow_filesystem_write = false, - allow_subprocess = true, + allow_subprocess = false, # the paired runner spawns; this record does not probe_scope = 'read_only, }, diff --git a/.machine_readable/contractiles/dust/dust.k9.ncl b/.machine_readable/contractiles/dust/dust.k9.ncl index 7a8cea8..247262c 100644 --- a/.machine_readable/contractiles/dust/dust.k9.ncl +++ b/.machine_readable/contractiles/dust/dust.k9.ncl @@ -25,11 +25,23 @@ K9! # rollback path; anything retired must preserve the evidence of its # previous existence). +# +# 2026-10-19 (K9 contract v1.0.0, D173, standards#1058): this record is a +# DECLARATION evaluated at 'Yard. The probes it lists are run by the paired +# runner (dust.ncl and the scripts/run-*.sh runners), which is the execution +# surface; this file spawns nothing. A 'Hunt record needs signature_required, +# a signature block and a verified signature (K9-S008/S009/C001); no +# contractile carries one, so 'Hunt here was a claim the gate could never honour. + let base = import "../_base.ncl" in { pedigree = base.pedigree_schema & { contractile_verb = "dust", + schema_version = "1.0.0", + component_type = "contractile:dust", + semantics = "rollback / recovery / deprecation / audit-trail preservation", + side_effects = [], paired_xfile = "../dust/dust.k9.ncl#declaration", paired_runner = "../dust/dust.ncl", @@ -53,7 +65,7 @@ let base = import "../_base.ncl" in # execute declared rollback/removal — but only behind explicit # --apply flag + per-item approval. Default is dry-run. allow_filesystem_write_conditional = true, - allow_subprocess = true, + allow_subprocess = false, # the paired runner spawns; this record does not destructive_action_gating = { default_mode = 'dry_run, requires_flag = "--apply", diff --git a/.machine_readable/contractiles/intend/intend.k9.ncl b/.machine_readable/contractiles/intend/intend.k9.ncl index bb74185..92030af 100644 --- a/.machine_readable/contractiles/intend/intend.k9.ncl +++ b/.machine_readable/contractiles/intend/intend.k9.ncl @@ -28,21 +28,33 @@ K9! # * Evidence sinks: VeriSimDB (queryable) + descriptiles/drift_praxis.deed (repo-local). # * Failure-mode defenses cross-referenced to the AI failure catalog. +# +# 2026-10-19 (K9 contract v1.0.0, D173, standards#1058): this record is a +# DECLARATION evaluated at 'Yard. The probes it lists are run by the paired +# runner (intend.ncl and the scripts/run-*.sh runners), which is the execution +# surface; this file spawns nothing. A 'Hunt record needs signature_required, +# a signature block and a verified signature (K9-S008/S009/C001); no +# contractile carries one, so 'Hunt here was a claim the gate could never honour. + let base = import "../_base.ncl" in { pedigree = base.pedigree_schema & { contractile_verb = "intend", + schema_version = "1.0.0", + component_type = "contractile:intend", + semantics = "north-star (commitments + aspirations)", + side_effects = [], paired_xfile = "../intend/intend.k9.ncl#declaration", paired_runner = "../intend/intend.ncl", # α two-axis declaration — capability × authority. - # intend is Hunt-capable (probes shell out) but reporting-authority - # (never blocks). must/trust/bust will declare (Hunt, blocking); + # intend is Yard-tier as a record (intend.ncl shells out for the probes) + # and reporting-authority (never blocks). must/trust/bust declare (Yard, blocking); # adjust/dust will declare (Yard, advisory). Splitting the axes # means "I'm Hunt-tier so I can override everything" is structurally # impossible — authority is a separate field. - tier = 'Hunt, + tier = 'Yard, authority = 'reporting, metadata = { @@ -59,12 +71,12 @@ let base = import "../_base.ncl" in }, security = { - leash = 'Hunt, - signature_required = true, + leash = 'Yard, + signature_required = false, trust_level = "subprocess + filesystem-read", allow_network = false, allow_filesystem_write = false, # evidence sinks are indirected - allow_subprocess = true, + allow_subprocess = false, # the paired runner spawns; this record does not }, # Trident manifest — folded in from the retired .manifest.a2ml. diff --git a/.machine_readable/contractiles/must/must.k9.ncl b/.machine_readable/contractiles/must/must.k9.ncl index 4f70b0d..5e49e4b 100644 --- a/.machine_readable/contractiles/must/must.k9.ncl +++ b/.machine_readable/contractiles/must/must.k9.ncl @@ -28,18 +28,30 @@ K9! # fabrication of success "facts" — invariants ground truth against # marketing copy) and D1 (lore fabrication about what the repo contains). +# +# 2026-10-19 (K9 contract v1.0.0, D173, standards#1058): this record is a +# DECLARATION evaluated at 'Yard. The probes it lists are run by the paired +# runner (must.ncl and the scripts/run-*.sh runners), which is the execution +# surface; this file spawns nothing. A 'Hunt record needs signature_required, +# a signature block and a verified signature (K9-S008/S009/C001); no +# contractile carries one, so 'Hunt here was a claim the gate could never honour. + let base = import "../_base.ncl" in { pedigree = base.pedigree_schema & { contractile_verb = "must", + schema_version = "1.0.0", + component_type = "contractile:must", + semantics = "invariant assertion — release-blocking", + side_effects = [], paired_xfile = "../must/must.k9.ncl#declaration", paired_runner = "../must/must.ncl", - # α two-axis: Hunt tier (subprocess for grep/test/etc.) but - # restricted to read-only operations. Blocking authority because + # α two-axis: Yard tier (the runner shells out for grep/test/etc.; + # this record only declares) and read-only operations. Blocking authority because # must is the canonical gating verb. - tier = 'Hunt, + tier = 'Yard, authority = 'blocking, metadata = { @@ -52,12 +64,12 @@ let base = import "../_base.ncl" in }, security = { - leash = 'Hunt, - signature_required = true, + leash = 'Yard, + signature_required = false, trust_level = "read-only invariant verification with subprocess", allow_network = false, allow_filesystem_write = false, - allow_subprocess = true, + allow_subprocess = false, # the paired runner spawns; this record does not probe_scope = 'read_only, # must probes NEVER mutate probe_kinds_allowed = [ 'file_existence, diff --git a/.machine_readable/contractiles/trust/trust.k9.ncl b/.machine_readable/contractiles/trust/trust.k9.ncl index feb2ebc..ef7a916 100644 --- a/.machine_readable/contractiles/trust/trust.k9.ncl +++ b/.machine_readable/contractiles/trust/trust.k9.ncl @@ -44,21 +44,33 @@ K9! # * Failure-mode defenses cross-referenced — trust carries the most # defenses of any verb because the threat surface is widest. +# +# 2026-10-19 (K9 contract v1.0.0, D173, standards#1058): this record is a +# DECLARATION evaluated at 'Yard. The probes it lists are run by the paired +# runner (trust.ncl and the scripts/run-*.sh runners), which is the execution +# surface; this file spawns nothing. A 'Hunt record needs signature_required, +# a signature block and a verified signature (K9-S008/S009/C001); no +# contractile carries one, so 'Hunt here was a claim the gate could never honour. + let base = import "../_base.ncl" in { pedigree = base.pedigree_schema & { contractile_verb = "trust", + schema_version = "1.0.0", + component_type = "contractile:trust", + semantics = "security + provenance + safe-hacking", + side_effects = [], paired_xfile = "../trust/trust.k9.ncl#declaration", paired_runner = "../trust/trust.ncl", # α two-axis declaration — capability × authority. - # trust is Hunt-capable (active probes shell out, safe-hacking section - # runs real fuzz/injection/auth-bypass attempts scoped to the repo) + # trust is Yard-tier as a record (the runner, trust.ncl, shells out for the + # active probes and the safe-hacking attempts scoped to the repo) # AND blocking-authority (failed verification = failed CI). # Contrast with intend = (Hunt, reporting). The two verbs exercise # the full α surface. - tier = 'Hunt, + tier = 'Yard, authority = 'blocking, metadata = { @@ -71,12 +83,12 @@ let base = import "../_base.ncl" in }, security = { - leash = 'Hunt, - signature_required = true, + leash = 'Yard, + signature_required = false, trust_level = "verification + authorised-probe + hard-gate", allow_network = false, # verifications offline by default allow_filesystem_write = false, # evidence sinks are indirected - allow_subprocess = true, + allow_subprocess = false, # the paired runner spawns; this record does not authorised_probes_only = true, # probe section explicitly lists allowed targets + probe classes probe_scope_enforcement = 'this_repo_only, # probes NEVER hit external systems }, diff --git a/.machine_readable/mint-verbatim.txt b/.machine_readable/mint-verbatim.txt new file mode 100644 index 0000000..cd6ffd7 --- /dev/null +++ b/.machine_readable/mint-verbatim.txt @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# mint-verbatim.txt — files `just repo-init` carries into a child UNCHANGED. +# +# These are the mint's own machinery: the generator, and the tests whose +# fixtures must keep the template's literal name, its brace tokens or its +# instruction-block marker, because those fixtures are the INPUT the test feeds +# the generator. No rewrite pass touches them (placeholder substitution, the +# instruction-block strip, the self-name pass), and neither completeness check +# reads them, since an unrendered fixture is not a half-minted file. +# +# Measured minting metadatastician/lintel (2026-10-14): the self-name pass +# rewrote self_name_pass_test.sh's inputs to `lintel`, and the test then +# FAILED in the child; the strip pass removed mint_cleanup_test.sh's fixture +# block although the leftover check exempted that very file. On 2026-10-20 a +# fresh mint of main aborted outright, because julia_library_test.sh's +# PACKAGE_UUID brace-token fixture read as an unfilled token. +# +# Read by build/just/repo-init.just (mint_files) and by +# scripts/check-no-placeholders.sh. One repository-relative path per line; `#` +# starts a comment. A listed path that does not exist stops the mint: a stale +# entry is a hole, not a no-op. + +build/just/repo-init.just # the generator itself +scripts/strip-instruction-blocks.rs # names the marker it strips +scripts/resolve-template-pin.sh # documents the template it resolves +tests/e2e/template_instantiation_test.sh # token answer list; marker exemptions +tests/e2e/use_template_mint_test.sh # mints a template copy: template literals +tests/workflows/mint_cleanup_test.sh # instruction-block fixture +tests/workflows/self_name_pass_test.sh # template-name fixtures +tests/workflows/template_conformance_test.sh # template-slug fixtures +tests/workflows/julia_library_test.sh # PACKAGE_UUID brace-token fixture +tests/workflows/resolve_template_pin_test.sh # template-slug fixtures +tests/ffi_strings_test.sh # FFI token fixture, filled in a scratch copy +tests/workflows/container_kyaml_test.sh # container token fixtures (template only) diff --git a/.machine_readable/required-contexts-allow.txt b/.machine_readable/required-contexts-allow.txt new file mode 100644 index 0000000..1f429f7 --- /dev/null +++ b/.machine_readable/required-contexts-allow.txt @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# Live required status check contexts on default branch (`main`) and the +# workflow files in `.github/workflows/` that produce them on `pull_request`. +# +# Format: => <.github/workflows/file.yml> +# +# Verified by `scripts/check-required-contexts.sh` (3-outcome 0/1/2) so a +# renamed or path-filtered workflow can never silently strand pull requests +# behind an unsatisfiable ruleset check (issue #219 class). +scan / gitleaks => .github/workflows/secret-scanner.yml diff --git a/.machine_readable/root-allow.txt b/.machine_readable/root-allow.txt index e98fc5f..1b2d06b 100644 --- a/.machine_readable/root-allow.txt +++ b/.machine_readable/root-allow.txt @@ -68,10 +68,10 @@ build/ # build orchestration: just/ phase modules, container ci/ # .gitlab-ci.yml + .pre-commit-config.yaml; ci/README.adoc records the out-of-band GitLab project setting these require scripts/ # repo helper scripts session/ # dispatch.sh, custom-checks.k9.ncl, local-hooks.sh -src/ tests/ # ─── Optional: capability-gated or template-only ───────────────────────────── +?src/ # source tree; optional for non-code/specification/governance repositories (issue #233, e.g. standards) ?archetypes/ # template-only variation seam (ADR-0003); `just repo-init` removes it from minted repos ?.devcontainer/ # VS Code dev container spec; present only where the container capability is declared ?sonar-project.properties # only where the repo is analysed by SonarCloud diff --git a/.machine_readable/scripts/maintenance/maint-assault.sh b/.machine_readable/scripts/maintenance/maint-assault.sh index 4c630f1..9431514 100644 --- a/.machine_readable/scripts/maintenance/maint-assault.sh +++ b/.machine_readable/scripts/maintenance/maint-assault.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 # -# maint-assault.sh — High-rigor stress testing using panic-attacker +# maint-assault.sh — High-rigor stress testing using panic-attack # # This script runs a full assault (static + dynamic) on the project binary # to detect logic-based bug signatures and environmental vulnerabilities. @@ -31,10 +31,10 @@ fi echo "Initiating full assault on $BINARY_NAME..." mkdir -p "$(dirname "$REPORT_PATH")" -"$PA_BIN" assault "target/release/$BINARY_NAME" - --source . - --intensity medium - --duration 10 +"$PA_BIN" assault "target/release/$BINARY_NAME" \ + --source . \ + --intensity medium \ + --duration 10 \ --output "$REPORT_PATH" echo "" diff --git a/.machine_readable/self-validating/README.adoc b/.machine_readable/self-validating/README.adoc index 778ab2e..59d93df 100644 --- a/.machine_readable/self-validating/README.adoc +++ b/.machine_readable/self-validating/README.adoc @@ -51,7 +51,7 @@ Configuration with Nickel contracts for runtime validation. Evaluated safely wit === Hunt Level (Full Execution) -**File:** `examples/setup-repo.k9.ncl` +**File:** `examples/setup-repo.k9.ncl.in` Full execution with Just recipes. Can run shell commands and modify filesystem. @@ -78,7 +78,7 @@ cp .machine_readable/contractiles/k9/examples/project-metadata.k9.ncl config/met cp .machine_readable/contractiles/k9/examples/ci-config.k9.ncl .github/ci.k9.ncl # Hunt: Full automation -cp .machine_readable/contractiles/k9/examples/setup-repo.k9.ncl scripts/setup.k9.ncl +cp .machine_readable/contractiles/k9/examples/setup-repo.k9.ncl.in scripts/setup.k9.ncl ---- === 2. Validate Components @@ -137,9 +137,9 @@ K9 contractiles integrate with other RSR standards: Use these as starting points for your own K9 components: -- `template-kennel.k9.ncl` - Pure data template -- `template-yard.k9.ncl` - Validated config template -- `template-hunt.k9.ncl` - Full execution template +- `template-kennel.k9.ncl.in` - Pure data template +- `template-yard.k9.ncl.in` - Validated config template +- `template-hunt.k9.ncl.in` - Full execution template == Dependencies diff --git a/.machine_readable/self-validating/examples/setup-repo.k9.ncl b/.machine_readable/self-validating/examples/setup-repo.k9.ncl.in similarity index 100% rename from .machine_readable/self-validating/examples/setup-repo.k9.ncl rename to .machine_readable/self-validating/examples/setup-repo.k9.ncl.in diff --git a/.machine_readable/self-validating/methodology-guard.k9.ncl b/.machine_readable/self-validating/methodology-guard.k9.ncl index cefb839..f58d5d1 100644 --- a/.machine_readable/self-validating/methodology-guard.k9.ncl +++ b/.machine_readable/self-validating/methodology-guard.k9.ncl @@ -16,6 +16,7 @@ let methodology_guard = { pedigree = { schema_version = "1.0.0", + component_type = "validator:methodology-guard", metadata = { name = "methodology-guard", version = "1.0.0", diff --git a/.machine_readable/self-validating/template-hunt.k9.ncl b/.machine_readable/self-validating/template-hunt.k9.ncl.in similarity index 100% rename from .machine_readable/self-validating/template-hunt.k9.ncl rename to .machine_readable/self-validating/template-hunt.k9.ncl.in diff --git a/.machine_readable/self-validating/template-kennel.k9.ncl b/.machine_readable/self-validating/template-kennel.k9.ncl.in similarity index 100% rename from .machine_readable/self-validating/template-kennel.k9.ncl rename to .machine_readable/self-validating/template-kennel.k9.ncl.in diff --git a/.machine_readable/self-validating/template-yard.k9.ncl b/.machine_readable/self-validating/template-yard.k9.ncl.in similarity index 100% rename from .machine_readable/self-validating/template-yard.k9.ncl rename to .machine_readable/self-validating/template-yard.k9.ncl.in diff --git a/.machine_readable/testing/adversarial-content.k9.ncl b/.machine_readable/testing/adversarial-content.k9.ncl new file mode 100644 index 0000000..d461af6 --- /dev/null +++ b/.machine_readable/testing/adversarial-content.k9.ncl @@ -0,0 +1,39 @@ +K9! +# SPDX-License-Identifier: MPL-2.0 +# Adversarial and negative-control test fixtures (proven-tests-and-benches +# Silence + Firing doctrine). Documents why negative test suites name banned +# patterns or feed malformed inputs into gates so human reviewers and scanners +# do not mistake Firing controls for production defects. + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "testing:adversarial-controls", + metadata = { + name = "rsr-adversarial-controls", + version = "0.1.0", + }, + security = { + leash = 'Kennel, + }, + }, + + adversarial_suites = [ + { + path = "tests/proven_suite_test.sh", + purpose = "Generates ephemeral Idris2 escape-hatch mutants and benchmark regressions inside mktemp to prove check-escape-hatches.sh, check-bench-regression.sh, compute-bench-baseline.sh, and emit-aggregate-inputs.sh fire with exact 0/1/2 exit codes.", + }, + { + path = "tests/workflows/check_ijson_jcs_test.sh", + purpose = "Feeds duplicate keys, integers outside +/-(2^53-1), lone UTF-16 surrogates, Unicode noncharacters, and non-JCS formatting into check-ijson-jcs.sh.", + }, + { + path = "tests/workflows/squabble_gate_test.sh", + purpose = "Plants admin-override strings, continue-on-error masks, and phantom ruleset contexts into ephemeral workflow fixtures to prove cicd-squabbler and check-required-contexts.sh fire.", + }, + { + path = "www/tests/controls", + purpose = "Planted open-recursion BIND9, over-broad Caddyfile, and invalid AIBDP status controls.", + }, + ], +} diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index b467ed8..46bc008 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -1,5 +1,32 @@ == Changelog +== 2026-10-27 — CI follows standards to metadatastician/standards + +* `hyperpolymath/standards` moved to `metadatastician/standards`. A + reusable-workflow `uses:` does not follow the rename, and a hyperpolymath + repository cannot call a reusable in metadatastician's repository, so + Governance, Rust CI, Hypatia and Secret Scanner stopped loading ("workflow + was not found"). +* Ported rsr-template-repo's CI from `60cedf1` to `8034fc9`, its last green + main commit (rsr-template-repo#244, rsr-template-repo#245, + rsr-template-repo#247, rsr-template-repo#249, rsr-template-repo#251, + rsr-template-repo#253 and rsr-template-repo#256), with a three-way apply + that keeps this repository's own tests. Workflows run + self-contained, checking out `metadatastician/standards@8e48b1c`; new + squabble, proven-tests and fork-governance workflows, KYAML container + config, K9 contract v1.0.0 records and an I-JSON schema come with it. + `actions.lock` is the template's plus `proofs.yml`. +* Not ported: the template's generator tests (use-this-template mint, + template-pin resolver, e2e mint tests), which exercise + `build/just/repo-init.just` and this port does not bring the generator. +* Known gap: the self-contained governance runs one policy step. The + reusable ran 19 jobs, among them UUID v8 conformance, licence consistency, + actions-lockfile verify and the debt and exemption ratchets. They return + when standards can be called from hyperpolymath again. +* `tests/workflows/actions_lock_sha_refs_test.sh` now checks every SHA-keyed + record in the lock, and that no workflow calls a reusable from the moved + repository. + == 2026-10-26 — F-3 and H-3 proved; template proof removed * `Morphism.peak-not-strict`: the peak does not preserve ⊗ (peaks 2 and 3 diff --git a/CLAUDE.md b/CLAUDE.md index 6fc3d6f..0434f22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,14 @@ The descriptive family (working name *descriptiles*) describes what-is; the **co | k9 | *Validation*. Kennel (data) / Yard (pure eval) / Hunt (guarded exec). | ## Canon pointers +Estate data-format route: **kyq** handles exactly **TOML, Dhall, KYAML, +I-JSON/JCS**. Preserve TOML/Dhall; migrate YAML/YML to KYAML and JSON variants +through kyq using **hyperpolymath/ijson-jcs**, the actual estate implementation +repository. **kyX** owns wider projection. DEED/K9/coordination keep their own +grammars. Canon: `metadatastician/standards/3-practice/DATA-FORMAT-ROUTING.adoc`. +Planned support is not operational evidence: retain explicit transitional +checks until a pinned kyq executable passes positive and negative tests. + - `hyperpolymath/standards` — the canon source. · `hyperpolymath/gv-clade-index` — the estate map (identity registry). · `hyperpolymath/manifesto` — this doctrine. - **Before you invent, rename, or consolidate anything: STOP and check the map + IS-NOT.** @@ -80,12 +88,28 @@ Put every question that needs the owner's decision through the selector (the Ask This estate enforces strict format standards. Failure to follow these causes estate drift and broken tooling. These rules are NOT optional. +**Estate route (owner clarification, 2026-10-25):** `metadatastician/kyq` +handles repository gates, queries and migration for exactly **TOML, Dhall, +KYAML and I-JSON/JCS**. Preserve TOML and Dhall. YAML/YML content migrates to +KYAML; JSON variants, including NDJSON/JSONL and JSON-LD, use the profile in +**`hyperpolymath/ijson-jcs`** through kyq. That name means the owner's actual +repository, which must track standards through tested update proposals. +Preserve framing, consumer filenames and JSON-LD semantics. Wider projection +belongs to **kyX**, not extra kyq formats. DEED, K9 and coordination retain +their own grammars/contracts. Canon: +`metadatastician/standards/3-practice/DATA-FORMAT-ROUTING.adoc`. + +kyq's build and validation are still incomplete at `4370c76`; never claim a +placeholder is a working gate. The yq/ijson-jcs commands below are explicit +transitional enforcement until a pinned kyq executable passes the applicable +positive and negative tests. They are not the destination architecture. + **KYAML (KEP-5295, owner rulings D280/D307):** YAML content is KYAML. - Estate-owned config uses the `.kyaml` extension. - `.yml`/`.yaml` only where a tool demands the name: GitHub workflows, dependabot. Workflows migrate to KYAML content only once the lock-coverage shim is a required context (D280); leave them alone until then. -- Use `yq` (Go version) and the estate scripts `kyaml-format.sh`, +- Transitional tooling: use `yq` (Go version) and the estate scripts `kyaml-format.sh`, `kyaml-migrate.sh`. No Python YAML parsers (D293). - KYAML format: `{}` for maps, `[]` for lists, double-quoted strings, unquoted keys where unambiguous, trailing commas OK, 2-space indent. @@ -93,9 +117,10 @@ estate drift and broken tooling. These rules are NOT optional. **JSON (owner ruling D306):** every `.json`/`.jsonl` is I-JSON (RFC 7493), serialised by JCS (RFC 8785), one record per line for JSON Lines. Extensions -do not change. Tool: `hyperpolymath/ijson-jcs`. Rule and gate: +do not change. Tool: `hyperpolymath/ijson-jcs` (`scripts/check-ijson-jcs.sh --enforce`, +wired in `estate-rules.yml`). Rule and gate: `standards/3-practice/JSON-POLICY.adoc`. Tool-demanded JSONC -(`devcontainer.json`) is carved out. +(`devcontainer.json`, `.vscode/*.json`) is carved out. **UUIDv8 (owner ruling D305, standards ADR-008):** mint every new identifier as UUIDv8 in the ADR-008 layout. Do not rewrite existing v4/v7 identifiers by @@ -115,3 +140,21 @@ hand; that migration is done per repo (ADR-008 phase P3). In deed files use **ENFORCEMENT:** these rulings are recorded on `standards#787`. Some are ruled but not yet gated: check the gate on `standards` main before relying on it. + + +## Proofs — which prover for which obligation + +Normative source: `3-practice/PROOF-POLICY.adoc` in `hyperpolymath/standards`. +Read it there; `verification/proofs/README.adoc` maps it onto this repo. + +| Code or obligation | Verifier | +|---|---| +| Rust code | **Creusot**, on the code (Kani for `unsafe`/FFI seams and Aeneas for Rust that implements an Agda theorem run alongside, never instead) | +| Ada code | **SPARK** | +| APIs, ABIs, engineering properties | **Idris2** | +| Theory | **Agda**, rechecked in **Rocq** and **Lean** (no Mathlib) | + +Third-party code (crates such as Bevy, libraries, forks) is **not** converted +or re-proved. State what a proof relies on as an `ASSUMED` boundary +specification, and when choosing between dependencies that fit equally well, +prefer one that carries its own verification. A preference, not a gate. diff --git a/build/container/README.adoc b/build/container/README.adoc index fc76578..cbd56d7 100644 --- a/build/container/README.adoc +++ b/build/container/README.adoc @@ -102,9 +102,11 @@ The container engine is *auto-detected* (`podman`, then `nerdctl`, then | *cerro-torre* build → pack → sign (Ed25519) → verify → push pipeline. Engine-agnostic; degrades gracefully when cerro-torre tools are absent. -| `deploy.k9.ncl` -| *k9-svc* deployment component at Hunt trust level: full pedigree (L1–L5), - dev/staging/prod configs, rolling deployment strategy. +| `deploy.k9.ncl.in` +| *k9-svc* deployment component at the Hunt leash (K9 contract v1.0.0): + pedigree with capability grant, side effects and signature block; + dev/staging/prod configs, rolling deployment strategy. Rendered to + `deploy.k9.ncl` by `just container-init`. |=== == The stapeln ecosystem @@ -174,14 +176,26 @@ just container-push # push the signed bundle cd build/container/stapeln && selur-compose up --detach ---- -k9-svc managed deployment: +k9-svc managed deployment. The deployment component ships as the template +`build/container/stapeln/deploy.k9.ncl.in`, in the K9 contract v1.0.0 shape. +`just repo-init` fills its `PROJECT_NAME` and `VERSION` placeholders and +`just container-init` fills `SERVICE_NAME`, `PORT` and `REGISTRY`. Once no +placeholder token is left, `just container-init` renders it to +`build/container/stapeln/deploy.k9.ncl`; until then it keeps the `.in` file +and names the tokens still missing. The rendered file is a K9 component, so +the K9 lane validates it like any other: [source,bash] ---- -nickel typecheck build/container/stapeln/deploy.k9.ncl +k9-validate.sh --strict --layer L2 build/container/stapeln/deploy.k9.ncl k9-svc deploy build/container/stapeln/deploy.k9.ncl --env production ---- +It is a `'Hunt` component, so it carries a signature block. That block is a +claim until you sign the rendered file: the validator reports it +`'Present_Unverified` (K9-C001), and Hunt stays unauthorised until a verifier +accepts the signature. + == Continuous integration A `container build` workflow ships in `.github/workflows/container-build.yml`. diff --git a/build/container/compose.example.yaml b/build/container/compose.example.yaml index cbd43aa..6e84892 100644 --- a/build/container/compose.example.yaml +++ b/build/container/compose.example.yaml @@ -12,97 +12,142 @@ # # For the sovereign selur path see build/container/stapeln/compose.example.toml. -services: - # Rust API — primary HTTP/gRPC backend. - rust-api: - build: - context: .. - dockerfile: build/container/Containerfile - image: ghcr.io/hyperpolymath/myproject-api:latest - ports: - - "8080:8080" - environment: - RUST_LOG: "info" - APP_HOST: "[::]" - APP_PORT: "8080" - APP_LOG_FORMAT: "json" - APP_DATA_DIR: "/data" - volumes: - - api-data:/data - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:8080/health"] - interval: 30s - timeout: 5s - retries: 3 - - # Elixir worker — background processing, talks to the API over the network. - elixir-worker: - image: ghcr.io/hyperpolymath/myproject-worker:latest - ports: - - "4000:4000" - environment: - API_URL: "http://rust-api:8080/api/v1" - MIX_ENV: "prod" - APP_LOG_FORMAT: "json" - POOL_SIZE: "10" - depends_on: - rust-api: - condition: service_healthy - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:4000/health"] - interval: 30s - timeout: 5s - retries: 3 - - # rokur — secrets gate. Refuses to come up unless required secrets are present. - rokur: - image: ghcr.io/hyperpolymath/rokur:latest - ports: - - "8081:8081" - environment: - ROKUR_BACKEND: "http://rust-api:8080" - ROKUR_LISTEN: "[::]:8081" - ROKUR_REQUIRED_SECRETS: "DATABASE_URL,JWT_SIGNING_KEY" - ROKUR_AUDIT_LOG: "/var/log/rokur/audit.jsonl" - ROKUR_LOG_FORMAT: "json" - volumes: - - rokur-audit:/var/log/rokur - depends_on: - rust-api: - condition: service_healthy - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:8081/health"] - interval: 30s - timeout: 5s - retries: 3 - - # svalinn — edge gateway. TLS, auth, rate limiting, audit. The only - # externally exposed service. - svalinn: - image: ghcr.io/hyperpolymath/svalinn:latest - ports: - - "443:443" - - "80:80" - environment: - SVALINN_BACKEND: "http://rokur:8081" - SVALINN_WORKER_BACKEND: "http://elixir-worker:4000" - SVALINN_POLICY_FILE: "/etc/svalinn/gatekeeper.yaml" - SVALINN_TLS_AUTO: "true" - volumes: - - ./stapeln/.gatekeeper.yaml:/etc/svalinn/gatekeeper.yaml:ro - depends_on: - - rokur - - elixir-worker - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:80/health"] - interval: 30s - timeout: 5s - retries: 3 - -volumes: - api-data: - rokur-audit: +{ + services: { + # Rust API — primary HTTP/gRPC backend. + rust-api: { + build: { + context: "..", + dockerfile: "build/container/Containerfile", + }, + image: "ghcr.io/hyperpolymath/myproject-api:latest", + ports: [ + "8080:8080", + ], + environment: { + RUST_LOG: "info", + APP_HOST: "[::]", + APP_PORT: "8080", + APP_LOG_FORMAT: "json", + APP_DATA_DIR: "/data", + }, + volumes: [ + "api-data:/data", + ], + restart: "unless-stopped", + healthcheck: { + test: [ + "CMD", + "curl", + "-sf", + "http://localhost:8080/health", + ], + interval: "30s", + timeout: "5s", + retries: 3, + }, + }, + # Elixir worker — background processing, talks to the API over the network. + elixir-worker: { + image: "ghcr.io/hyperpolymath/myproject-worker:latest", + ports: [ + "4000:4000", + ], + environment: { + API_URL: "http://rust-api:8080/api/v1", + MIX_ENV: "prod", + APP_LOG_FORMAT: "json", + POOL_SIZE: "10", + }, + depends_on: { + rust-api: { + condition: "service_healthy", + }, + }, + restart: "unless-stopped", + healthcheck: { + test: [ + "CMD", + "curl", + "-sf", + "http://localhost:4000/health", + ], + interval: "30s", + timeout: "5s", + retries: 3, + }, + }, + # rokur — secrets gate. Refuses to come up unless required secrets are present. + rokur: { + image: "ghcr.io/hyperpolymath/rokur:latest", + ports: [ + "8081:8081", + ], + environment: { + ROKUR_BACKEND: "http://rust-api:8080", + ROKUR_LISTEN: "[::]:8081", + ROKUR_REQUIRED_SECRETS: "DATABASE_URL,JWT_SIGNING_KEY", + ROKUR_AUDIT_LOG: "/var/log/rokur/audit.jsonl", + ROKUR_LOG_FORMAT: "json", + }, + volumes: [ + "rokur-audit:/var/log/rokur", + ], + depends_on: { + rust-api: { + condition: "service_healthy", + }, + }, + restart: "unless-stopped", + healthcheck: { + test: [ + "CMD", + "curl", + "-sf", + "http://localhost:8081/health", + ], + interval: "30s", + timeout: "5s", + retries: 3, + }, + }, + # svalinn — edge gateway. TLS, auth, rate limiting, audit. The only + # externally exposed service. + svalinn: { + image: "ghcr.io/hyperpolymath/svalinn:latest", + ports: [ + "443:443", + "80:80", + ], + environment: { + SVALINN_BACKEND: "http://rokur:8081", + SVALINN_WORKER_BACKEND: "http://elixir-worker:4000", + SVALINN_POLICY_FILE: "/etc/svalinn/gatekeeper.yaml", + SVALINN_TLS_AUTO: "true", + }, + volumes: [ + "./stapeln/.gatekeeper.yaml:/etc/svalinn/gatekeeper.yaml:ro", + ], + depends_on: [ + "rokur", + "elixir-worker", + ], + restart: "unless-stopped", + healthcheck: { + test: [ + "CMD", + "curl", + "-sf", + "http://localhost:80/health", + ], + interval: "30s", + timeout: "5s", + retries: 3, + }, + }, + }, + volumes: { + api-data: null, + rokur-audit: null, + }, +} diff --git a/build/container/compose.yaml b/build/container/compose.yaml index 4112547..dfa1c28 100644 --- a/build/container/compose.yaml +++ b/build/container/compose.yaml @@ -16,84 +16,119 @@ # # See https://compose-spec.io for the full specification. -services: - # ────────────────────────────────────────────────────────────────────── - # Primary application service - # ────────────────────────────────────────────────────────────────────── - "resource-algebra": - build: - context: .. - dockerfile: build/container/Containerfile - image: "ghcr.io/hyperpolymath/resource-algebra:latest" - ports: - - "8080:8080" - environment: - APP_HOST: "[::]" - APP_PORT: "8080" - APP_LOG_FORMAT: "json" - APP_DATA_DIR: "/data" - volumes: - - "resource-algebra-data:/data" - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:8080/health"] - interval: 30s - timeout: 5s - retries: 3 - start_period: 10s - - # ────────────────────────────────────────────────────────────────────── - # rokur — secrets-management gate (stapeln). Fronts the app and refuses - # to start the stack unless required secrets are present. on Wolfi. - # Remove this service if you do not use secret gating. - # ────────────────────────────────────────────────────────────────────── - rokur: - image: ghcr.io/hyperpolymath/rokur:latest - environment: - ROKUR_BACKEND: "http://resource-algebra:8080" - ROKUR_LISTEN: "[::]:8081" - ROKUR_REQUIRED_SECRETS: "" # comma-separated env names that MUST be set - ROKUR_AUDIT_LOG: "/var/log/rokur/audit.jsonl" - ROKUR_LOG_FORMAT: "json" - ports: - - "8081:8081" - volumes: - - rokur-audit:/var/log/rokur - depends_on: - "resource-algebra": - condition: service_healthy - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:8081/health"] - interval: 30s - timeout: 5s - retries: 3 - - # ────────────────────────────────────────────────────────────────────── - # svalinn — edge gateway (stapeln). TLS termination, auth, rate limiting. - # Reads the gatekeeper policy from build/container/stapeln/.gatekeeper.yaml. - # Remove this service for a bare app with no gateway. - # ────────────────────────────────────────────────────────────────────── - svalinn: - image: ghcr.io/hyperpolymath/svalinn:latest - ports: - - "443:443" - - "80:80" - environment: - SVALINN_BACKEND: "http://rokur:8081" - SVALINN_POLICY_FILE: "/etc/svalinn/gatekeeper.yaml" - SVALINN_TLS_AUTO: "true" - volumes: - - ./stapeln/.gatekeeper.yaml:/etc/svalinn/gatekeeper.yaml:ro - depends_on: - - rokur - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:80/health"] - interval: 30s - timeout: 5s - retries: 3 - -volumes: - "resource-algebra-data": - rokur-audit: +{ + services: { + # ────────────────────────────────────────────────────────────────────── + # Primary application service + # ────────────────────────────────────────────────────────────────────── + resource-algebra: { + build: { + context: "..", + dockerfile: "build/container/Containerfile", + }, + image: "ghcr.io/hyperpolymath/resource-algebra:latest", + ports: [ + "8080:8080", + ], + environment: { + APP_HOST: "[::]", + APP_PORT: "8080", + APP_LOG_FORMAT: "json", + APP_DATA_DIR: "/data", + }, + volumes: [ + "resource-algebra-data:/data", + ], + restart: "unless-stopped", + healthcheck: { + test: [ + "CMD", + "curl", + "-sf", + "http://localhost:8080/health", + ], + interval: "30s", + timeout: "5s", + retries: 3, + start_period: "10s", + }, + }, + # ────────────────────────────────────────────────────────────────────── + # rokur — secrets-management gate (stapeln). Fronts the app and refuses + # to start the stack unless required secrets are present. on Wolfi. + # Remove this service if you do not use secret gating. + # ────────────────────────────────────────────────────────────────────── + rokur: { + image: "ghcr.io/hyperpolymath/rokur:latest", + environment: { + ROKUR_BACKEND: "http://resource-algebra:8080", + ROKUR_LISTEN: "[::]:8081", + ROKUR_REQUIRED_SECRETS: "", # comma-separated env names that MUST be set + ROKUR_AUDIT_LOG: "/var/log/rokur/audit.jsonl", + ROKUR_LOG_FORMAT: "json", + }, + ports: [ + "8081:8081", + ], + volumes: [ + "rokur-audit:/var/log/rokur", + ], + depends_on: { + resource-algebra: { + condition: "service_healthy", + }, + }, + restart: "unless-stopped", + healthcheck: { + test: [ + "CMD", + "curl", + "-sf", + "http://localhost:8081/health", + ], + interval: "30s", + timeout: "5s", + retries: 3, + }, + }, + # ────────────────────────────────────────────────────────────────────── + # svalinn — edge gateway (stapeln). TLS termination, auth, rate limiting. + # Reads the gatekeeper policy from build/container/stapeln/.gatekeeper.yaml. + # Remove this service for a bare app with no gateway. + # ────────────────────────────────────────────────────────────────────── + svalinn: { + image: "ghcr.io/hyperpolymath/svalinn:latest", + ports: [ + "443:443", + "80:80", + ], + environment: { + SVALINN_BACKEND: "http://rokur:8081", + SVALINN_POLICY_FILE: "/etc/svalinn/gatekeeper.yaml", + SVALINN_TLS_AUTO: "true", + }, + volumes: [ + "./stapeln/.gatekeeper.yaml:/etc/svalinn/gatekeeper.yaml:ro", + ], + depends_on: [ + "rokur", + ], + restart: "unless-stopped", + healthcheck: { + test: [ + "CMD", + "curl", + "-sf", + "http://localhost:80/health", + ], + interval: "30s", + timeout: "5s", + retries: 3, + }, + }, + }, + volumes: { + resource-algebra-data: null, + rokur-audit: null, + }, +} diff --git a/build/container/stapeln/.gatekeeper.yaml b/build/container/stapeln/.gatekeeper.yaml index 1144fab..8542307 100644 --- a/build/container/stapeln/.gatekeeper.yaml +++ b/build/container/stapeln/.gatekeeper.yaml @@ -9,114 +9,149 @@ # # See: stapeln/container-stack/svalinn/ -version: "1.0" - -# ============================================================================ -# Authentication -# ============================================================================ -# -# Define which endpoints require authentication and at what level. - -auth: - # Public endpoints — no authentication required. - # Health and readiness probes must always be public so that - # orchestrators (selur, Podman, k8s) can check service status. - public: - - path: "/health" - methods: ["GET"] - - path: "/ready" - methods: ["GET"] - - path: "/metrics" - methods: ["GET"] - - # Endpoints requiring JWT or OAuth2 authentication. - # Svalinn validates the token before forwarding the request. - authenticated: - - path: "/api/v1/*" - methods: ["GET", "POST", "PUT", "DELETE"] - -# ============================================================================ -# Rate Limiting -# ============================================================================ -# -# Protects backend services from overload. Values here are moderate -# defaults — adjust based on your service capacity. - -rate_limits: - # Global limit: applied to all authenticated clients. - global: - requests_per_second: 500 - burst: 1000 - - # Write operations: stricter limit to protect data stores. - writes: - paths: ["/api/v1/*"] - methods: ["POST", "PUT", "DELETE"] - requests_per_second: 100 - burst: 200 - -# ============================================================================ -# Container Trust -# ============================================================================ -# -# Svalinn verifies that all .ctp bundles in the stack are signed by -# trusted keys and carry the required attestations. - -trust: - # Only accept .ctp bundles signed by these keys. - trusted_signers: - - key_id: "resource-algebra-release" - algorithm: "Ed25519" - public_key_file: "/etc/svalinn/keys/resource-algebra-release.pub" - - # Require these attestations on all .ctp bundles. - required_attestations: - - "source-signature" - - "sbom-complete" - - # Reject unsigned or untrusted images. - reject_unsigned: true - -# ============================================================================ -# Request Validation -# ============================================================================ -# -# Input validation at the gateway layer — catches malformed requests -# before they reach the application. - -validation: - # Maximum request body size. - max_body_size: "8MB" - - # Reject requests with NaN or Infinity in numeric fields. - reject_nan_inf: true - - # Maximum result limit per list/search query. - max_result_limit: 500 - -# ============================================================================ -# CORS -# ============================================================================ -# -# Cross-Origin Resource Sharing policy. The defaults below allow all -# origins — restrict to your frontend domain(s) in production. - -cors: - allow_origins: ["*"] - allow_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] - allow_headers: ["Content-Type", "Authorization"] - max_age: 3600 - -# ============================================================================ -# Logging -# ============================================================================ -# -# Structured logging for svalinn itself. Audit paths log all requests -# (including body hashes) for post-incident investigation. - -logging: - format: "json" - level: "info" - # Log all write operations for audit trail. - audit_paths: - - "/api/v1/*" +{ + version: "1.0", + # ============================================================================ + # Authentication + # ============================================================================ + # + # Define which endpoints require authentication and at what level. + auth: { + # Public endpoints — no authentication required. + # Health and readiness probes must always be public so that + # orchestrators (selur, Podman, k8s) can check service status. + public: [ + { + path: "/health", + methods: [ + "GET", + ], + }, + { + path: "/ready", + methods: [ + "GET", + ], + }, + { + path: "/metrics", + methods: [ + "GET", + ], + }, + ], + # Endpoints requiring JWT or OAuth2 authentication. + # Svalinn validates the token before forwarding the request. + authenticated: [ + { + path: "/api/v1/*", + methods: [ + "GET", + "POST", + "PUT", + "DELETE", + ], + }, + ], + }, + # ============================================================================ + # Rate Limiting + # ============================================================================ + # + # Protects backend services from overload. Values here are moderate + # defaults — adjust based on your service capacity. + rate_limits: { + # Global limit: applied to all authenticated clients. + global: { + requests_per_second: 500, + burst: 1000, + }, + # Write operations: stricter limit to protect data stores. + writes: { + paths: [ + "/api/v1/*", + ], + methods: [ + "POST", + "PUT", + "DELETE", + ], + requests_per_second: 100, + burst: 200, + }, + }, + # ============================================================================ + # Container Trust + # ============================================================================ + # + # Svalinn verifies that all .ctp bundles in the stack are signed by + # trusted keys and carry the required attestations. + trust: { + # Only accept .ctp bundles signed by these keys. + trusted_signers: [ + { + key_id: "resource-algebra-release", + algorithm: "Ed25519", + public_key_file: "/etc/svalinn/keys/resource-algebra-release.pub", + }, + ], + # Require these attestations on all .ctp bundles. + required_attestations: [ + "source-signature", + "sbom-complete", + ], + # Reject unsigned or untrusted images. + reject_unsigned: true, + }, + # ============================================================================ + # Request Validation + # ============================================================================ + # + # Input validation at the gateway layer — catches malformed requests + # before they reach the application. + validation: { + # Maximum request body size. + max_body_size: "8MB", + # Reject requests with NaN or Infinity in numeric fields. + reject_nan_inf: true, + # Maximum result limit per list/search query. + max_result_limit: 500, + }, + # ============================================================================ + # CORS + # ============================================================================ + # + # Cross-Origin Resource Sharing policy. The defaults below allow all + # origins — restrict to your frontend domain(s) in production. + cors: { + allow_origins: [ + "*", + ], + allow_methods: [ + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS", + ], + allow_headers: [ + "Content-Type", + "Authorization", + ], + max_age: 3600, + }, + # ============================================================================ + # Logging + # ============================================================================ + # + # Structured logging for svalinn itself. Audit paths log all requests + # (including body hashes) for post-incident investigation. + logging: { + format: "json", + level: "info", + # Log all write operations for audit trail. + audit_paths: [ + "/api/v1/*", + ], + }, +} diff --git a/build/container/stapeln/deploy.k9.ncl b/build/container/stapeln/deploy.k9.ncl index 5c4288b..3dddb5e 100644 --- a/build/container/stapeln/deploy.k9.ncl +++ b/build/container/stapeln/deploy.k9.ncl @@ -2,169 +2,188 @@ K9! # SPDX-License-Identifier: MPL-2.0 # deploy.k9.ncl — Resource Algebra deployment component (Hunt level) # -# k9-svc deployment specification with full pedigree (L1-L5). -# Security Level: 'Hunt (requires cryptographic handshake for execution). +# k9-svc deployment component, K9 contract v1.0.0 (D173). Shipped as +# deploy.k9.ncl.in because a template is not a component: `just repo-init` +# fills PROJECT_NAME and VERSION, `just container-init` fills SERVICE_NAME, +# PORT and REGISTRY, and once no brace token is left `just container-init` +# renders it to build/container/stapeln/deploy.k9.ncl. # -# WARNING: This component can execute shell commands! -# It requires explicit authorisation via the Leash system. +# WARNING: this component executes shell commands. It is 'Hunt, so a host +# runs it only with all five Hunt preconditions met, a verified signature +# among them. The signature block below is a claim until the owner signs the +# rendered file; until then the validator reports it 'Present_Unverified +# (K9-C001) and Hunt stays unauthorised. # -# Usage: -# nickel typecheck build/container/deploy.k9.ncl -# k9-svc validate build/container/deploy.k9.ncl -# k9-svc deploy build/container/deploy.k9.ncl --env production +# Usage (on the rendered file): +# k9-validate.sh --strict --layer L2 build/container/stapeln/deploy.k9.ncl +# k9-svc validate build/container/stapeln/deploy.k9.ncl +# k9-svc deploy build/container/stapeln/deploy.k9.ncl --env production -# The component's pedigree (self-description across five layers) -let component_pedigree = { - # ───────────────────────────────────────────────────────────── - # L1: The Snout — Identity - # ───────────────────────────────────────────────────────────── - metadata = { - name = "resource-algebra-deploy", - version = "0.1.0", - breed = "application/vnd.k9+nickel", - magic_number = "K9!", - description = "Resource Algebra deployment component (Hunt level)", - }, - - # ───────────────────────────────────────────────────────────── - # L2: The Scent — Target Environment - # ───────────────────────────────────────────────────────────── - target = { - os = 'Linux, - is_edge = false, - requires_podman = true, - min_memory_mb = 256, - }, +{ + pedigree = { + schema_version = "1.0.0", + component_type = "deployment", + + metadata = { + name = "resource-algebra-deploy", + version = "0.1.0", + description = "Resource Algebra deployment component (Hunt level)", + }, - # ───────────────────────────────────────────────────────────── - # L3: The Leash — Security - # ───────────────────────────────────────────────────────────── - security = { - trust_level = 'Hunt, - # Named field the k9 validators grep for (leash/security_level); - # value mirrors trust_level — this component is documented Hunt-level. - security_level = 'Hunt, - allow_network = true, - allow_filesystem_write = true, - allow_subprocess = true, - # In production, replace with a real Ed25519 signature. - signature = "PLACEHOLDER-SIGNATURE-REQUIRED-FOR-HUNT", - }, + # Target environment (extension field; contract §5.5). + target = { + os = 'Linux, + is_edge = false, + requires_podman = true, + min_memory_mb = 256, + }, - # ───────────────────────────────────────────────────────────── - # L4: The Gut — Self-Validation - # ───────────────────────────────────────────────────────────── - validation = { - checksum = "sha256:placeholder", - pedigree_version = "1.0.0", - hunt_authorized = false, # Must be set true after handshake - }, + security = { + leash = 'Hunt, + trust_level = "controlled-execution", + allow_network = true, + allow_filesystem_write = true, + allow_subprocess = true, + signature_required = true, + # Default-deny: every flag above is paid for here (contract §8.4), plus + # the deploy and rollback actions the scripts below perform. + capabilities = [ + "net.fetch", + "fs.write", + "process.spawn", + "container.run", + "deploy.apply", + "rollback.apply", + ], + }, - # ───────────────────────────────────────────────────────────── - # L5: The Muscle — Deployment Recipes - # ───────────────────────────────────────────────────────────── - recipes = { - install = "just container-build", - validate = "just container-verify", - deploy = "just container-up", - migrate = "just container-build && just container-up", + warnings = [ + "This is a Hunt-level component: it executes shell commands and changes the system.", + "Review the deployment scripts before running it.", + "A signature block being present is not a signature being verified.", + "Authorise Hunt explicitly: k9-svc authorize build/container/stapeln/deploy.k9.ncl", + ], + + side_effects = [ + "spawns build/container/stapeln/ct-build.sh, which builds the image and packs and signs a .ctp bundle beside it", + "pulls and pushes ghcr.io/hyperpolymath/resource-algebra images", + "starts, updates and stops the resource-algebra stack with selur-compose, else podman compose over build/container/compose.yaml", + "publishes the service on port 8080", + ], + + # Ed25519, presence only (K9-S009). The signing step adds payload_hash + # and signature to the rendered file; verification is K9-C001 (L3). + signature = { + algorithm = "Ed25519", + key_id = "primary", + }, }, -} in -# Deployment configuration -let deployment = { - # Target environments (dev / staging / production) - environments = { - dev = { - replicas = 1, - memory = "256Mi", - cpu = "100m", - image_tag = "dev", - }, - staging = { - replicas = 2, - memory = "512Mi", - cpu = "250m", - image_tag = "staging", + # Deployment configuration + deployment = { + # Target environments (dev / staging / production) + environments = { + dev = { + replicas = 1, + memory = "256Mi", + cpu = "100m", + image_tag = "dev", + }, + staging = { + replicas = 2, + memory = "512Mi", + cpu = "250m", + image_tag = "staging", + }, + production = { + replicas = 3, + memory = "1Gi", + cpu = "500m", + image_tag = "latest", + }, }, - production = { - replicas = 3, - memory = "1Gi", - cpu = "500m", - image_tag = "latest", - }, - }, - # Container configuration - container = { - image = "ghcr.io/hyperpolymath/resource-algebra", - port = 8080, - health_check = "/health", - readiness_check = "/ready", - }, + # Container configuration + container = { + image = "ghcr.io/hyperpolymath/resource-algebra", + port = 8080, + health_check = "/health", + readiness_check = "/ready", + }, - # Deployment strategy - strategy = { - type = "rolling", - max_surge = 1, - max_unavailable = 0, + # Deployment strategy + strategy = { + type = "rolling", + max_surge = 1, + max_unavailable = 0, + }, }, -} in -# Deployment scripts (executed at Hunt level) -let scripts = { - # Pre-deployment validation - pre_deploy = m%" + # Deployment scripts (executed at Hunt level) + scripts = { + # Pre-deployment validation + pre_deploy = m%" #!/bin/sh set -eu echo "K9: Pre-deployment validation for resource-algebra..." -cd build/container/stapeln && selur-compose verify || podman compose --file ../compose.yaml config +cd build/container/stapeln +if command -v selur-compose >/dev/null 2>&1; then + selur-compose verify +else + podman compose --file ../compose.yaml config +fi echo "K9: Validation passed." "%, - # Deployment script - deploy = m%" + # Deployment script + deploy = m%" #!/bin/sh set -eu ENV="${1:-dev}" echo "K9: Deploying resource-algebra to $ENV environment..." cd build/container/stapeln ./ct-build.sh -selur-compose up --detach || podman compose --file ../compose.yaml up --detach +if command -v selur-compose >/dev/null 2>&1; then + selur-compose up --detach +else + podman compose --file ../compose.yaml up --detach +fi echo "K9: Deployment to $ENV complete." "%, - # Rollback script - rollback = m%" + # Rollback script + rollback = m%" #!/bin/sh set -eu echo "K9: Rolling back resource-algebra deployment..." cd build/container/stapeln -selur-compose down || podman compose --file ../compose.yaml down +if command -v selur-compose >/dev/null 2>&1; then + selur-compose down +else + podman compose --file ../compose.yaml down +fi echo "K9: Rollback complete." "%, -} in - -# Export the component -{ - pedigree = component_pedigree & { name = "resource-algebra-deploy" }, - deployment = deployment, - scripts = scripts, - - # Security check: this component requires Hunt level - required_level = 'Hunt, - - # Warning for users - warning = m%" -WARNING: This is a Hunt-level component. - -It can execute shell commands and modify your system. -Before running, ensure you have: - -1. Reviewed the deployment scripts above -2. Verified the signature (when implemented) -3. Explicitly authorised Hunt-level execution + }, -Run with: k9-svc authorize build/container/deploy.k9.ncl && k9-svc deploy build/container/deploy.k9.ncl -"%, + # Just recipes: an execution surface, which is why the leash is 'Hunt + # (K9-S011). + recipes = { + install = { + description = "Build the container image", + commands = ["just container-build"], + }, + validate = { + description = "Verify the compose configuration", + commands = ["just container-verify"], + }, + deploy = { + description = "Start the container stack", + commands = ["just container-up"], + }, + migrate = { + description = "Rebuild the image and restart the stack", + commands = ["just container-build", "just container-up"], + }, + }, } diff --git a/build/just/container.just b/build/just/container.just index a906c60..f880f64 100644 --- a/build/just/container.just +++ b/build/just/container.just @@ -89,13 +89,43 @@ container-init: -e "s|${LB}REGISTRY${RB}|${REGISTRY}|g" ) + # Return success when $1 is classified as text, using a byte-content probe + # (which accepts empty files) when the file command is unavailable. + is_text_file() { + if command -v file >/dev/null 2>&1; then + file --brief "$1" | grep -qi 'text\|ascii\|utf' + else + [ ! -s "$1" ] || LC_ALL=C grep -Iq . "$1" + fi + } + # Recurses into build/container/stapeln/ as well. - find build/container/ -type f | while read -r file; do - if file --brief "$file" | grep -qi 'text\|ascii\|utf'; then + find build/container/ -type f | while IFS= read -r file; do + if is_text_file "$file"; then sed -i "${SED_ARGS[@]}" "$file" fi done + # The k9-svc deploy component ships as a template (deploy.k9.ncl.in): a + # file with brace tokens is not a K9 component. Render it once nothing is + # left to fill (repo-init fills PROJECT_NAME and VERSION), so the K9 lane + # validates the real thing. The bracket spellings keep just from reading + # the token pattern as an interpolation. + DEPLOY_IN="build/container/stapeln/deploy.k9.ncl.in" + DEPLOY_OUT="${DEPLOY_IN%.in}" + if [ -f "$DEPLOY_IN" ]; then + LEFT=$(grep -oE '[{][{][A-Za-z_]+[}][}]' "$DEPLOY_IN" | sort -u | tr '\n' ' ' || true) + if [ -n "$LEFT" ]; then + echo "Kept $DEPLOY_IN: unfilled tokens ${LEFT}remain." + echo " Run 'just repo-init', then 'just container-init' again to render it." + elif [ -e "$DEPLOY_OUT" ]; then + echo "Kept $DEPLOY_IN: $DEPLOY_OUT already exists, so it was not overwritten." + else + mv "$DEPLOY_IN" "$DEPLOY_OUT" + echo "Rendered $DEPLOY_OUT (K9 component, contract v1.0.0)." + fi + fi + echo "Container templates initialised." echo "" echo "Next steps:" diff --git a/coordination.k9.ncl b/coordination.k9.ncl index 6f4f987..69721f8 100644 --- a/coordination.k9.ncl +++ b/coordination.k9.ncl @@ -5,6 +5,7 @@ K9! { pedigree = { schema_version = "1.0.0", + component_type = "session:coordination-binding", metadata = { name = "session-coordination", version = "0.1.0", diff --git a/docs/architecture/REPOSITORY-MAP.adoc b/docs/architecture/REPOSITORY-MAP.adoc index 9f34afd..b923a6c 100644 --- a/docs/architecture/REPOSITORY-MAP.adoc +++ b/docs/architecture/REPOSITORY-MAP.adoc @@ -178,8 +178,8 @@ predecessors did. | only where the repo is analysed by SonarCloud | `src/` -| yes -| - +| optional +| source tree; optional for non-code/specification/governance repositories (issue #233, e.g. standards) | `tests/` | yes diff --git a/scripts/bench-stats.jq b/scripts/bench-stats.jq new file mode 100644 index 0000000..55f62eb --- /dev/null +++ b/scripts/bench-stats.jq @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Shared statistics for the benchmark regression gate. +# +# ONE definition, included (`jq -L scripts 'include "bench-stats"; ...'`) by +# both scripts/compute-bench-baseline.sh and scripts/check-bench-regression.sh, +# so the baseline and the check cannot drift into computing different +# statistics. Inputs are proven-bench JSON v1 documents as written by +# `proven-bench --json` (benchmarks/Benchmark.idr, `runJSON`). + +# bench_errors: the list of shape errors in a proven-bench v1 run document. +# An empty list means the document is usable. Shape is checked, not merely +# presence: `log` of a zero or negative median is silently -inf/nan in jq, so +# a median that is not a positive number must be refused here. +def bench_errors: + if type != "object" then ["not a JSON object"] + else + [ (if .schema_version != 1 + then "schema_version is \(.schema_version | tojson), expected 1" else empty end), + (if .suite != "proven-bench" + then "suite is \(.suite | tojson), expected \"proven-bench\"" else empty end), + (if (.results | type) != "array" or (.results | length) < 3 + then "results must be an array of at least 3 workloads (each ratio needs >= 2 others)" + else empty end) + ] + + (if (.results | type) == "array" then + [ .results[] + | if type != "object" then "a result is not a JSON object" + elif (.name | type) != "string" or .name == "" then "a result has no name" + elif (.median_ns | type) != "number" or .median_ns <= 0 + then "\(.name): median_ns \(.median_ns | tojson) is not a positive number" + elif (.iterations | type) != "number" or .iterations <= 0 + then "\(.name): iterations \(.iterations | tojson) is not a positive number" + else empty end ] + + ([ .results[] | .name? | select(type == "string") ] + | group_by(.) | map(select(length > 1) | "duplicate workload name \(.[0] | tojson)")) + else [] end) + end; + +# checksum_errors: one error per workload whose checksum is not true. Applied +# to the CURRENT run only. It is deliberately not part of bench_errors, which +# also vets baseline inputs: before 2026-10-06 (#85) the checksum was an XOR +# parity that read False on every run whatever the predicates did, so the +# historical artifacts a baseline is cut from carry a checksum that means +# nothing. Since #85 the bench asserts it and exits non-zero itself; this is +# the second line of defence for a JSON that reached the gate anyway. +def checksum_errors: + [ .results[] + | select(.checksum != true) + | "\(.name): checksum is \(.checksum | tojson) - the workload is broken, its timing measures nothing" ]; + +# medians: {workload name: median_ns} for a valid run document. +def medians: .results | map({key: .name, value: .median_ns}) | from_entries; + +# iterations_map: {workload name: iterations} for a valid run document. +def iterations_map: .results | map({key: .name, value: .iterations}) | from_entries; + +# loo_ratios: {workload name: leave-one-out ratio} for a valid run document. +# Each workload's median divided by the GEOMETRIC mean of the medians of every +# OTHER workload in the same run. Leaving the workload itself out of its own +# denominator means a regression in workload W raises only W's ratio (and +# lowers the others'), so a slowdown can only ever be blamed on the workload +# that slowed down. +def loo_ratios: + medians as $m + | ($m | keys) as $ks + | reduce $ks[] as $k ({}; + .[$k] = $m[$k] / ([ $ks[] | select(. != $k) | $m[.] | log ] | add / length | exp)); + +# baseline_errors: the list of shape errors in a baseline document written by +# scripts/compute-bench-baseline.sh. An empty list means it is usable. +def baseline_errors: + if type != "object" then ["not a JSON object"] + else + [ (if .kind != "proven-bench-baseline" + then "kind is \(.kind | tojson), expected \"proven-bench-baseline\"" else empty end), + (if .schema_version != 1 + then "schema_version is \(.schema_version | tojson), expected 1" else empty end), + (if (.workloads | type) != "object" or (.workloads | length) < 3 + then "workloads must be an object holding at least 3 workloads" else empty end) + ] + + (if (.workloads | type) == "object" then + [ .workloads | to_entries[] + | .key as $n | .value + | if type != "object" then "\($n): entry is not a JSON object" + elif (.mean_median_ns | type) != "number" or .mean_median_ns <= 0 + then "\($n): mean_median_ns \(.mean_median_ns | tojson) is not a positive number" + elif (.mean_loo_ratio | type) != "number" or .mean_loo_ratio <= 0 + then "\($n): mean_loo_ratio \(.mean_loo_ratio | tojson) is not a positive number" + elif (.iterations | type) != "number" or .iterations <= 0 + then "\($n): iterations \(.iterations | tojson) is not a positive number" + else empty end ] + else [] end) + end; diff --git a/scripts/check-bench-regression.sh b/scripts/check-bench-regression.sh new file mode 100755 index 0000000..724f5d1 --- /dev/null +++ b/scripts/check-bench-regression.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Benchmark regression gate (ultraplan Phase 3b; ROADMAP 2a; DEBT I-1). +# +# Usage: scripts/check-bench-regression.sh +# +# Compares one proven-bench JSON v1 run against benchmarks/baseline.json and +# classifies each workload on the taxonomy's Six Sigma scale. +# +# WHY RATIOS, NOT NANOSECONDS +# --------------------------- +# CI runs on shared GitHub runners whose CPU class varies from run to run. +# Over 28 green main runs (2026-08-10 .. 2026-10-06) the same workload's +# absolute median ranged from 0.44x to 1.65x the baseline mean (a 3.8x +# spread); a gate on absolute time at the taxonomy's 50% would have failed +# green runs routinely, and a gate that fails randomly gets disabled. Much of +# that spread is common-mode — a slower machine slows every workload — so the +# primary statistic divides it out: each workload's median over the geometric +# mean of the OTHER workloads' medians in the same run (leave-one-out, see +# scripts/bench-stats.jq). Its spread over the same runs is 0.67x .. 1.51x. +# It does not vanish entirely, because runner classes do not slow every +# workload by the same factor (effectStackValid and ceremonyEndsProperly move +# together; relationTransitive and coveredCatAspect move together). +# +# THE TOLERANCES, AND HOW THEY WERE CHOSEN (manual override, stated) +# ------------------------------------------------------------------ +# * LOO_FAIL = 2.0. Max observed leave-one-out ratio vs the baseline: 1.515 +# over 28 main runs (2nd highest 1.431), 1.527 over 27 branch/PR runs. The +# taxonomy's 1.5x hard-fail would have failed green runs, so this departs +# from it (taxonomy Part IV permits a manual override with a reason; this is +# the reason). Margin: 2.0 / 1.527 = 1.31x. A 3x slowdown of one workload +# is caught even on the most favourable observed runner (3 x 0.67 = 2.02). +# * ABS_FAIL = 2.5, on absolute median vs baseline mean. The ratio statistic +# is BLIND to a uniform slowdown (every workload 3x slower leaves every ratio +# at 1.0), so this backstop catches what ratios cannot. Max observed 1.65 +# (main) / 1.62 (branches); margin 2.5 / 1.65 = 1.52x. A uniform 3x +# slowdown is caught; a uniform slowdown under 2.5x is NOT (stated blind +# spot, recorded in TEST-NEEDS.adoc Gap C). Likewise a CORRELATED slowdown +# of two of the four workloads partly hides itself: both 2.5x slower gives +# each a ratio of 2.5 / 2.5^(1/3) = 1.84x, under LOO_FAIL. +# * EXTRA = 0.5 on the ratio: below it a workload is EXTRAORDINARY — passed, +# but flagged for investigation (real improvement or measurement artefact). +# Min observed 0.67. +# * There is no separate "Acceptable" (soft-fail, 20-50%) band: the measured +# noise envelope already spans it, so a warning there would fire on green +# runs and carry no information. +# +# Exit codes (check-toolchain-pins.sh convention): +# 0 every workload Ordinary or Extraordinary +# 1 at least one workload Unacceptable (named on stdout) +# 2 NO CHECK WAS PERFORMED: a file is missing or malformed, or the workload +# sets / iteration counts disagree. A skip is not a pass. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" || { echo "FAIL: cannot locate script directory" >&2; exit 2; } + +LOO_FAIL=2.0 +ABS_FAIL=2.5 +EXTRA=0.5 + +# inert: report that no check was performed, and why, then exit 2. +inert() { + echo "bench-regression: $*" >&2 + echo "bench-regression: NO CHECK WAS PERFORMED - a skip is not a pass." >&2 + if [ -n "${GITHUB_ACTIONS:-}" ]; then + echo "::error title=Bench regression gate inert::$*" + fi + exit 2 +} + +# validate: refuse a file that is missing, unparseable, or ill-shaped. $1 is +# a label, $2 the path, $3 the bench-stats.jq error function to apply. +validate() { + local label="$1" f="$2" fn="$3" errs + [ -f "$f" ] || inert "$label '$f' does not exist" + if ! errs="$(jq -L "$SCRIPT_DIR" -r "include \"bench-stats\"; ${fn}[]" "$f")"; then + inert "$label '$f' is not parseable JSON" + fi + if [ -n "$errs" ]; then + printf '%s\n' "$errs" | sed "s|^| $label: |" >&2 + inert "$label '$f' is malformed" + fi +} + +[ "$#" -eq 2 ] || inert "usage: $0 " +CURRENT="$1" +BASELINE="$2" + +validate "baseline" "$BASELINE" baseline_errors +validate "current run" "$CURRENT" bench_errors +validate "current run" "$CURRENT" checksum_errors + +# A workload present on only one side is not a pass: either the bench lost a +# workload (nothing measured it) or the baseline is for a different workload +# set (nothing to compare it to). Same for a changed iteration count, which +# redefines the workload (Benchmark.idr: workloads are FROZEN). +mismatch="$(jq -L "$SCRIPT_DIR" -r --slurpfile b "$BASELINE" ' + include "bench-stats"; + ($b[0].workloads) as $base + | iterations_map as $cur + | ( [ $base | keys[] | select($cur[.] == null) | "MISSING from current run: \(.)" ] + + [ $cur | keys[] | select($base[.] == null) | "MISSING from baseline: \(.)" ] + + [ $cur | to_entries[] | select($base[.key] != null and $base[.key].iterations != .value) + | "iterations changed for \(.key): baseline \($base[.key].iterations), current \(.value) - cut a new baseline" ] + )[] +' "$CURRENT")" || inert "jq failed while comparing workload sets" +if [ -n "$mismatch" ]; then + printf '%s\n' "$mismatch" | sed 's/^/bench-regression: /' + inert "the current run and the baseline do not measure the same workloads" +fi + +# One line per workload: name, ratio-vs-baseline, absolute-vs-baseline, class. +verdicts="$(jq -L "$SCRIPT_DIR" -r --slurpfile b "$BASELINE" \ + --argjson loo_fail "$LOO_FAIL" --argjson abs_fail "$ABS_FAIL" --argjson extra "$EXTRA" ' + include "bench-stats"; + ($b[0].workloads) as $base + | loo_ratios as $loo + | medians as $med + | $base | keys[] + | . as $n + | ($loo[$n] / $base[$n].mean_loo_ratio) as $rx + | ($med[$n] / $base[$n].mean_median_ns) as $ax + | [ $n, + ($rx * 1000 | round / 1000 | tostring), + ($ax * 1000 | round / 1000 | tostring), + (if $rx > $loo_fail or $ax > $abs_fail then "UNACCEPTABLE" + elif $rx < $extra then "EXTRAORDINARY" + else "ORDINARY" end) ] + | @tsv +' "$CURRENT")" || inert "jq failed while classifying workloads" +[ -n "$verdicts" ] || inert "classification produced no verdicts" + +echo "bench-regression: $(basename "$CURRENT") vs $(basename "$BASELINE")" +echo "bench-regression: fail if ratio > ${LOO_FAIL}x baseline ratio, or absolute > ${ABS_FAIL}x baseline mean" +regressed=() +while IFS=$'\t' read -r name rx ax class; do + printf ' %-22s ratio %6sx absolute %6sx %s\n' "$name" "$rx" "$ax" "$class" + case "$class" in + UNACCEPTABLE) + regressed+=("$name") + if [ -n "${GITHUB_ACTIONS:-}" ]; then + echo "::error title=Benchmark regression::$name is Unacceptable: ratio ${rx}x (limit ${LOO_FAIL}x), absolute ${ax}x (limit ${ABS_FAIL}x)" + fi ;; + EXTRAORDINARY) + echo " ^ Extraordinary: real improvement or measurement artefact? Re-cut the baseline only if confirmed." ;; + esac +done <<<"$verdicts" + +if [ "${#regressed[@]}" -gt 0 ]; then + echo "bench-regression: FAIL - Unacceptable regression in: ${regressed[*]}" + exit 1 +fi +echo "bench-regression: OK - no workload regressed beyond tolerance" diff --git a/scripts/check-escape-hatches.sh b/scripts/check-escape-hatches.sh new file mode 100755 index 0000000..9577ac7 --- /dev/null +++ b/scripts/check-escape-hatches.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# The honesty lint (from hyperpolymath/proven-tests-and-benches): fail if any +# proof escape hatch appears in Idris2 sources or if '%default total' is absent +# or placed after a declaration. +# +# Exit codes (proven-tests-and-benches 3-outcome convention): +# 0 no escape hatches found across >= 1 .idr file +# 1 at least one escape hatch or missing/misplaced '%default total' found +# 2 the scan could not be performed (missing/unreadable directory or 0 .idr +# files — a skip is NOT a pass) +set -uo pipefail + +if [ "$#" -gt 0 ]; then + SCOPE=("$@") +else + SCOPE=(src verification .machine_readable/coaptation) +fi + +LITERAL_PATTERNS='believe_me|assert_total|assert_smaller|%partial|%default (partial|covering)|unsafePerformIO|idris_crash' +PARTIAL_DECL='^[[:space:]]*((public )?export[[:space:]]+|private[[:space:]]+)?partial([[:space:]]|$)' +COVERING_DECL='^[[:space:]]*((public )?export[[:space:]]+|private[[:space:]]+)?covering([[:space:]]|$)' +COVERING_ALLOWED=() +HOLE_PATTERN='(^|[[:space:](=,[])\?[a-zA-Z_][a-zA-Z0-9_'"'"']*' + +missing=0 +for dir in "${SCOPE[@]}"; do + if [ ! -d "$dir" ]; then + echo "check-escape-hatches: scope directory '$dir' does not exist" >&2 + missing=1 + fi +done +if [ "$missing" -ne 0 ]; then + echo "FAIL: NO SCAN WAS PERFORMED — the scan scope is incomplete." >&2 + echo " A missing directory is not a clean result." >&2 + exit 2 +fi + +FIND_OUT="$(mktemp)" +trap 'rm -f "$FIND_OUT"' EXIT +if ! find "${SCOPE[@]}" -name '*.idr' -type f > "$FIND_OUT"; then + echo "check-escape-hatches: find failed while scanning scope (${SCOPE[*]})" >&2 + echo "FAIL: NO SCAN WAS PERFORMED — an unreadable directory is not a clean result." >&2 + exit 2 +fi +mapfile -t IDR_FILES < <(LC_ALL=C sort "$FIND_OUT") +if [ "${#IDR_FILES[@]}" -eq 0 ]; then + echo "check-escape-hatches: 0 .idr files found in scope (${SCOPE[*]})" >&2 + echo "FAIL: NO SCAN WAS PERFORMED — an empty proof corpus is not a pass." >&2 + exit 2 +fi + +echo "Scanning ${#IDR_FILES[@]} Idris2 file(s) for proof escape hatches in: ${SCOPE[*]}" + +hits=0 + +# Strip line and nested block comments once per file (preserving quoted comment +# markers inside Idris strings and exact line numbers) for all passes. +STRIP_DIR="$(mktemp -d)" +trap 'rm -f "$FIND_OUT"; rm -rf "$STRIP_DIR"' EXIT + +# strip_idr_comments FILE — print source without line/doc or nested block comments, +# preserving line numbers and quoted comment markers in string literals. +strip_idr_comments() { + awk ' + BEGIN { bs = sprintf("%c", 92); dq = sprintf("%c", 34); depth = 0 } + depth == 0 && /^[[:space:]]*\|\|\|/ { print ""; next } + depth == 0 && index($0, "--") == 0 && index($0, "{-") == 0 { print; next } + { + out = ""; in_str = 0; n = length($0) + for (i = 1; i <= n; i++) { + c = substr($0, i, 1) + if (depth > 0) { + if (c == "{" && i < n && substr($0, i + 1, 1) == "-") { depth++; i++ } + else if (c == "-" && i < n && substr($0, i + 1, 1) == "}") { depth--; i++ } + out = out " " + } else if (in_str) { + out = out c + if (c == bs && i < n) { + i++ + out = out substr($0, i, 1) + } else if (c == dq) { + in_str = 0 + } + } else { + if (c == "{" && i < n && substr($0, i + 1, 1) == "-") { + depth++ + i++ + out = out " " + } else if (c == dq) { + in_str = 1 + out = out c + } else if (c == "-" && i < n && substr($0, i + 1, 1) == "-") { + break + } else { + out = out c + } + } + } + print out + } + ' "$1" +} + +declare -A STRIPPED_FILE=() +idx_f=0 +for f in "${IDR_FILES[@]}"; do + idx_f=$((idx_f + 1)) + sf="$STRIP_DIR/$idx_f.idr" + strip_idr_comments "$f" > "$sf" + STRIPPED_FILE["$f"]="$sf" +done + +# scan_idr_code REGEX — print matching stripped lines as file:line:code. +# Scan IDR_FILES via STRIPPED_FILE; return 0 if any line matches, 1 otherwise. +scan_idr_code() { + local pattern="$1" f line_no=0 found=0 + for f in "${IDR_FILES[@]}"; do + line_no=0 + while IFS= read -r code || [ -n "$code" ]; do + line_no=$((line_no + 1)) + if [[ "$code" =~ $pattern ]]; then + printf '%s:%d:%s\n' "$f" "$line_no" "$code" + found=1 + fi + done < "${STRIPPED_FILE[$f]}" + done + return $(( 1 - found )) +} + +if scan_idr_code "$LITERAL_PATTERNS"; then + hits=1 +fi + +if scan_idr_code "$HOLE_PATTERN"; then + hits=1 +fi + +if scan_idr_code "$PARTIAL_DECL"; then + hits=1 +fi + +for f in "${IDR_FILES[@]}"; do + sf="${STRIPPED_FILE[$f]}" + mapfile -t cov_lines < "$sf" + for (( idx = 0; idx < ${#cov_lines[@]}; idx++ )); do + code="${cov_lines[$idx]}" + if [[ "$code" =~ $COVERING_DECL ]]; then + line_no=$((idx + 1)) + next_line="${cov_lines[$((idx + 1))]:-}" + name="$(printf '%s\n' "$next_line" | sed -E 's/^[[:space:]]*([^[:space:]:]+).*/\1/')" + allowed=0 + for pair in "${COVERING_ALLOWED[@]}"; do + [ "$pair" = "$f:$name" ] && allowed=1 + done + if [ "$allowed" -eq 0 ]; then + echo "$f:$line_no: covering $name is not on the I/O allowlist" + hits=1 + fi + fi + done +done + +# Require '%default total' to appear and precede all value/type declarations +# (only blank lines, comments, module headers, imports, namespaces, or benign +# compiler pragmas %hide/%logging/%unbound_implicits may precede it). +for f in "${IDR_FILES[@]}"; do + status="$(awk ' + /^[[:space:]]*$/ { next } + /^[[:space:]]*(module|import|namespace)[[:space:]]/ { next } + /^[[:space:]]*%(hide|logging|unbound_implicits|auto_implicit_depth)([[:space:]]|$)/ { next } + /^[[:space:]]*%default total([[:space:]]|$)/ { + if (saw_decl) { print "after_decl"; printed = 1; exit } + saw_total = 1 + next + } + { if (!saw_total) saw_decl = 1 } + END { + if (!printed) { + if (!saw_total) print "missing" + else print "ok" + } + } + ' "${STRIPPED_FILE[$f]}")" + case "$status" in + missing) + echo "$f: missing '%default total'" + hits=1 + ;; + after_decl) + echo "$f: '%default total' appears after a declaration (earlier definitions default to covering)" + hits=1 + ;; + esac +done + +if [ "$hits" -ne 0 ]; then + echo + echo "FAIL: proof escape hatch found. Actually-Proven content may not use these." + echo " Banned: believe_me, assert_total, assert_smaller, %partial," + echo " %default partial|covering, partial declarations," + echo " covering outside the I/O allowlist," + echo " unsafePerformIO, idris_crash, and hole syntax (?name)." + echo " Required: '%default total' before all declarations in every .idr file." + exit 1 +fi + +echo "OK: no proof escape hatches across ${#IDR_FILES[@]} file(s) in ${SCOPE[*]}" diff --git a/scripts/check-ijson-jcs.sh b/scripts/check-ijson-jcs.sh new file mode 100755 index 0000000..9c0345a --- /dev/null +++ b/scripts/check-ijson-jcs.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Report or block JSON and JSON Lines files that are not I-JSON (RFC 7493) + +# JCS (RFC 8785) canonical, per metadatastician/standards 3-practice/JSON-POLICY.adoc +# (owner ruling D306) and DATA-FORMAT-ROUTING.adoc (transitional enforcement via +# hyperpolymath/ijson-jcs until a pinned metadatastician/kyq executable passes +# positive and negative tests). +# +# Usage: scripts/check-ijson-jcs.sh [--enforce] [PATH...] +# +# Exit contract (proven-tests-and-benches 3-outcome convention): +# 0 = pass (or report-only scan completed) +# 1 = --enforce found one or more non-canonical or invalid files +# 2 = setup/tooling error (missing binary, missing scan path): never a pass +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +enforce=0 +if [ "${1:-}" = "--enforce" ]; then + enforce=1 + shift +fi +if [ "$#" -eq 0 ]; then + set -- . +fi + +# Resolve the runner: +# - When IJSON_JCS is explicitly provided by the caller, honour it strictly +# (so a test passing IJSON_JCS=missing-bin gets exit 2, never a fallback). +# - Otherwise prefer the compiled hyperpolymath/ijson-jcs binary on PATH, and +# fall back to the local Bun RFC 7493/8785 implementation when offline. +RUNNER=() +RUNNER_STDIN=0 +if [ -n "${IJSON_JCS:-}" ]; then + if ! command -v "$IJSON_JCS" >/dev/null 2>&1; then + printf '%s: not found; install hyperpolymath/ijson-jcs or set IJSON_JCS\n' "$IJSON_JCS" >&2 + exit 2 + fi + RUNNER=("$IJSON_JCS") +elif [ "${IJSON_JCS_FORCE_JS:-0}" = "1" ] && command -v bun >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/ijson-jcs-cli.js" ]; then + RUNNER=(bun "$SCRIPT_DIR/ijson-jcs-cli.js") + RUNNER_STDIN=1 +elif command -v ijson-jcs >/dev/null 2>&1; then + RUNNER=(ijson-jcs) +elif command -v bun >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/ijson-jcs-cli.js" ]; then + RUNNER=(bun "$SCRIPT_DIR/ijson-jcs-cli.js") + RUNNER_STDIN=1 +else + printf 'ijson-jcs: neither ijson-jcs nor bun (for %s/ijson-jcs-cli.js) found\n' "$SCRIPT_DIR" >&2 + exit 2 +fi + +# A mistyped path must not turn into an empty, passing scan. +for path in "$@"; do + if [ ! -e "$path" ]; then + printf '%s: scan path does not exist\n' "$path" >&2 + exit 2 + fi +done + +# list_files PATH... — emit NUL-delimited .json/.jsonl paths, pruning .git, +# node_modules, target, and _build directories. +list_files() { + find "$@" \( -name .git -o -name node_modules -o -name target -o -name _build \) -prune \ + -o -type f \( -name '*.json' -o -name '*.jsonl' \) -print0 +} + +# is_jsonc_carve_out PATH — return 0 for devcontainer.json or .vscode JSON paths; +# return 1 otherwise. +is_jsonc_carve_out() { + case "$1" in + */devcontainer.json | devcontainer.json | */.vscode/*.json | .vscode/*.json) + return 0 + ;; + esac + return 1 +} + +# classify_json FILE — print CANONICAL, NOT_CANONICAL, or INVALID using RUNNER. +# Canonical files require a final LF. Checker errors become INVALID verdicts. +classify_json() { + local rc=0 + if [ "$RUNNER_STDIN" -eq 1 ]; then + "${RUNNER[@]}" check <"$1" >/dev/null 2>&1 || rc=$? + else + "${RUNNER[@]}" check "$1" >/dev/null 2>&1 || rc=$? + fi + if [ "$rc" = 0 ] && [ "$(tail -c 1 "$1" | od -An -c | tr -d ' ')" != '\n' ]; then + rc=1 + fi + case "$rc" in + 0) echo CANONICAL ;; + 1) echo NOT_CANONICAL ;; + *) echo INVALID ;; + esac +} + +# classify_jsonl FILE — print CANONICAL, NOT_CANONICAL, or INVALID for JSON Lines. +# Empty files are canonical; nonempty files require a final LF. Blank records +# or failed canonicalization yield INVALID; valid records must match JCS exactly. +classify_jsonl() { + local file="$1" line canon verdict=CANONICAL + if [ -s "$file" ] && [ "$(tail -c 1 "$file" | od -An -c | tr -d ' ')" != '\n' ]; then + verdict=NOT_CANONICAL + fi + while IFS= read -r line || [ -n "$line" ]; do + if [ -z "$line" ]; then + echo INVALID + return + fi + if ! canon="$(printf '%s' "$line" | "${RUNNER[@]}" canon 2>/dev/null)"; then + echo INVALID + return + fi + if [ "$canon" != "$line" ]; then + verdict=NOT_CANONICAL + fi + done <"$file" + echo "$verdict" +} + +total=0 canonical=0 noncanonical=0 invalid=0 skipped=0 +while IFS= read -r -d '' file; do + file="${file#./}" + if is_jsonc_carve_out "$file"; then + skipped=$((skipped + 1)) + continue + fi + total=$((total + 1)) + case "$file" in + *.jsonl) verdict="$(classify_jsonl "$file")" ;; + *) verdict="$(classify_json "$file")" ;; + esac + case "$verdict" in + CANONICAL) canonical=$((canonical + 1)) ;; + NOT_CANONICAL) noncanonical=$((noncanonical + 1)); printf 'NOT CANONICAL %s\n' "$file" ;; + *) invalid=$((invalid + 1)); printf 'INVALID %s\n' "$file" ;; + esac +done < <(list_files "$@" | sort -z) + +summary="ijson-jcs: ${total} file(s) checked, ${canonical} canonical, ${noncanonical} not canonical, ${invalid} invalid; ${skipped} JSONC carve-out(s) skipped" +echo "$summary" +if [ -n "${GITHUB_ACTIONS:-}" ] && [ "$enforce" -eq 0 ]; then + echo "::notice title=ijson-jcs (report-only)::${summary}. Fix with: ijson-jcs fix " +fi +if [ "$enforce" -eq 1 ] && [ $((noncanonical + invalid)) -gt 0 ]; then + exit 1 +fi +exit 0 diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index 3088bbd..dd030d2 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -47,20 +47,16 @@ set -euo pipefail WF_DIR="${1:-.github/workflows}" LOCK="$WF_DIR/actions.lock" -# gawk is required: the parser uses 3-argument match(), a GNU extension. mawk -# (the Debian/Ubuntu default `awk`) does not support it, and a silent parse -# failure here would read as a clean pass - the exact failure mode this script -# exists to prevent. Probe it rather than trusting the name. +# Portable POSIX awk: avoids GNU gawk's 3-argument match() extension so the +# gate runs identically on Debian/Ubuntu minimal images (mawk) and gawk. AWK="" -for cand in gawk awk; do - if command -v "$cand" >/dev/null 2>&1 \ - && echo x | "$cand" '{ if (match($0, /(x)/, m) && m[1] == "x") exit 0; exit 1 }' 2>/dev/null; then +for cand in gawk mawk awk; do + if command -v "$cand" >/dev/null 2>&1; then AWK="$cand"; break fi done if [ -z "$AWK" ]; then - echo "check-lock-sync: FATAL: no awk supporting 3-argument match() (need gawk)" >&2 - echo "check-lock-sync: install it with: sudo apt-get install -y gawk" >&2 + echo "check-lock-sync: FATAL: no awk found on PATH" >&2 exit 1 fi @@ -105,21 +101,44 @@ function ck(r, at, s) { # ---------- pass 1: the lockfile ---------- FILENAME == lockfile { + if ($0 ~ /^version:[[:space:]]*/) { + verline = $0 + sub(/^version:[[:space:]]*/, "", verline) + sub(/[[:space:]]*$/, "", verline) + has_version = 1 + lock_version = verline + next + } if ($0 ~ /^workflows:[[:space:]]*$/) { inwf = 1; indep = 0; next } if ($0 ~ /^dependencies:[[:space:]]*$/) { inwf = 0; indep = 1; next } if ($0 ~ /^[a-z_]+:/) { inwf = 0; indep = 0; next } - # --- the dependencies: section, for clause 3 --- + # --- the dependencies: section, for clause 3 & self-SHA ref guard --- if (indep) { # " 'owner/repo@ref':" -- a top-level dependency record - if (match($0, /^ '([^']+)':/, m)) { - depkey = m[1] + if ($0 ~ /^ \047[^\047]+\047:/) { + depkey = $0 + sub(/^ \047/, "", depkey) + sub(/\047:.*$/, "", depkey) haverec[ck(depkey)] = 1; disp[ck(depkey)] = depkey next } + # " ref: '...'" -- ban 40-hex self-SHA ref (standards#1271) + if ($0 ~ /^ ref:[[:space:]]*\047[0-9a-fA-F]+\047[[:space:]]*$/ && depkey != "") { + rval = $0 + sub(/^ ref:[[:space:]]*\047/, "", rval) + sub(/\047[[:space:]]*$/, "", rval) + if (length(rval) == 40) { + selfsha[depkey] = rval + } + next + } # " - 'owner/repo@ref'" -- a nested uses: of that record - if (match($0, /^ - '([^']+)'/, m) && depkey != "") { - r = ck(m[1]); disp[r] = m[1] + if ($0 ~ /^ - \047[^\047]+\047/ && depkey != "") { + val = $0 + sub(/^ - \047/, "", val) + sub(/\047.*$/, "", val) + r = ck(val); disp[r] = val want[r] = 1 wantsrc[r] = wantsrc[r] " dependencies:" depkey next @@ -130,13 +149,18 @@ FILENAME == lockfile { if (!inwf) next # " '.github/workflows/x.yml':" or "... : []" - if (match($0, /^ '([^']+)':/, m)) { - cur = m[1] + if ($0 ~ /^ \047[^\047]+\047:/) { + cur = $0 + sub(/^ \047/, "", cur) + sub(/\047:.*$/, "", cur) seen_path[cur] = 1 next } - if (match($0, /^ - '([^']+)'[[:space:]]*$/, m) && cur != "") { - lr = ck(m[1]); disp[lr] = m[1]; lock[cur, lr] = 1 + if ($0 ~ /^ - \047[^\047]+\047[[:space:]]*$/ && cur != "") { + val = $0 + sub(/^ - \047/, "", val) + sub(/\047[[:space:]]*$/, "", val) + lr = ck(val); disp[lr] = val; lock[cur, lr] = 1 lockcount[cur]++ want[lr] = 1 wantsrc[lr] = wantsrc[lr] " " cur @@ -150,9 +174,10 @@ FNR == 1 { wf = FILENAME } { line = $0 sub(/[[:space:]]+#.*$/, "", line) # strip trailing comment - if (match(line, /^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*(.+)$/, m)) { - raw = m[1] - gsub(/^["']|["']$/, "", raw) + if (line ~ /^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*.+$/) { + raw = line + sub(/^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*/, "", raw) + gsub(/^["\047]|["\047]$/, "", raw) gsub(/[[:space:]]+$/, "", raw) if (raw ~ /^\$\//) { dollar[wf] = dollar[wf] " " raw; next } # known corruption n = norm(raw) @@ -257,6 +282,24 @@ END { bad = 1 } + # --- clause 5: NO SELF-SHA REFS. GitHub rejects any dependencies: record whose + # `ref:` is a 40-hex commit SHA ("invalid lockfile pin", standards#1271). --- + nself = 0; selflist = "" + for (s in selfsha) { + nself++ + selflist = selflist sprintf("\n %s (ref: %s)", s, selfsha[s]) + } + if (nself > 0) { + printf "FAIL actions.lock: SELF-SHA REFS\n" + printf " %d dependencies: record(s) use a 40-hex SHA as ref: (rejected by GitHub runner):%s\n", nself, selflist + bad = 1 + } + + if (!has_version || lock_version != "\047v0.0.3\047") { + printf "FAIL actions.lock: INVALID VERSION (%s; expected \047v0.0.3\047)\n", (has_version ? lock_version : "missing") + bad = 1 + } + # --- a dependencies: record nothing names is dead weight, not fatal: report only --- nunref = 0 for (d in haverec) if (!(d in want)) nunref++ @@ -279,7 +322,7 @@ END { print " github/codeql-action/upload-sarif@ is REJECTED by the schema; collapse it" print " to github/codeql-action@." print " 4. For any UNLISTED WORKFLOWS above, add the path as a lockfile key. A workflow" - print " with no uses: takes an empty list: \x27.github/workflows/x.yml\x27: []" + print " with no uses: takes an empty list: \047.github/workflows/x.yml\047: []" print " `gh actions-lock` has been observed to OMIT such a workflow entirely; that" print " omission is itself the defect, so re-running the tool may not add it." exit 1 diff --git a/scripts/check-no-placeholders.sh b/scripts/check-no-placeholders.sh index 7e95baf..28854f6 100755 --- a/scripts/check-no-placeholders.sh +++ b/scripts/check-no-placeholders.sh @@ -124,8 +124,17 @@ ALLOWED=( ".machine_readable/ai/PLACEHOLDERS.adoc" # the token vocabulary itself "EXPLAINME.adoc" # prose explaining that tokens exist "scripts/check-no-placeholders.sh" # this file (the pattern above) - "tests/e2e/template_instantiation_test.sh" # names tokens in its answer list ) +# Template machinery the mint carries unchanged, whose fixtures name tokens on +# purpose (the e2e answer list, the Julia UUID test). One manifest, read by +# repo-init too, so the mint and this gate cannot disagree about what is a +# fixture: see .machine_readable/mint-verbatim.txt. +VERBATIM_LIST="$REPO_ROOT/.machine_readable/mint-verbatim.txt" +if [ -f "$VERBATIM_LIST" ]; then + while IFS= read -r v; do + [ -n "$v" ] && ALLOWED+=("$v") + done < <(sed -e 's/#.*//' -e 's/[[:space:]]*$//' "$VERBATIM_LIST") +fi is_allowed() { local rel="$1" @@ -147,6 +156,26 @@ is_allowed() { # teach people that this gate cries wolf. Real tokens name a real init variable. META_TOKENS='PLACEHOLDER|ANYTHING|TOKEN|UPPER_SNAKE' +# repo_files — every file of the repository holding an upper-snake token, as +# "$REPO_ROOT/". In a git work tree, "of the repository" means tracked or +# untracked-and-not-ignored, the same set `just repo-init` edits: a git-ignored +# cache such as _build/dependencies/ holds OTHER repositories' files, whose +# tokens are not this repo's leak. Outside git, every file is scanned. +repo_files() { + if git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + git -C "$REPO_ROOT" ls-files -z --cached --others --exclude-standard \ + | while IFS= read -r -d '' rel; do + [ -f "$REPO_ROOT/$rel" ] && [ ! -L "$REPO_ROOT/$rel" ] || continue + grep -qIE '\{\{[A-Z][A-Z0-9_]*\}\}' "$REPO_ROOT/$rel" 2>/dev/null \ + && printf '%s\n' "$REPO_ROOT/$rel" + done + else + grep -rlE '\{\{[A-Z][A-Z0-9_]*\}\}' "$REPO_ROOT" \ + --exclude-dir=.git --binary-files=without-match 2>/dev/null + fi + return 0 +} + LEAKS=() while IFS= read -r hit; do rel="${hit#"$REPO_ROOT"/}" @@ -156,8 +185,7 @@ while IFS= read -r hit; do | grep -qvE "^\{\{($META_TOKENS)\}\}$"; then LEAKS+=("$rel") fi -done < <(grep -rlE '\{\{[A-Z][A-Z0-9_]*\}\}' "$REPO_ROOT" \ - --exclude-dir=.git --binary-files=without-match 2>/dev/null | sort) +done < <(repo_files | sort) if [ ${#LEAKS[@]} -eq 0 ]; then echo "PASS: no unfilled {{PLACEHOLDER}} tokens" diff --git a/scripts/check-required-contexts.sh b/scripts/check-required-contexts.sh new file mode 100755 index 0000000..235f28b --- /dev/null +++ b/scripts/check-required-contexts.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Verify that: +# 1. Every required status check context mapped in +# .machine_readable/required-contexts-allow.txt (and any non-empty +# required_status_checks context in .github/rulesets/*.json) resolves to +# an existing workflow under .github/workflows/ that runs on pull_request +# without path-filtering that could strand a PR (#219 defect class). +# 2. No workflow in .github/workflows/ invokes an admin bypass (`gh pr merge +# --admin`) — enforcing cicd-squabbler's core `squabble != bypass` +# invariant statically alongside the runtime binary. +# +# Exit contract (proven-tests-and-benches 3-outcome convention): +# 0 = all mapped required contexts are producible on pull_request and zero +# bypass commands exist in workflows +# 1 = phantom/path-stranded required context or admin-bypass workflow found +# 2 = setup error (missing repo directory, workflows directory, or allowfile) +set -euo pipefail + +REPO_ROOT="${1:-.}" +if [ ! -d "$REPO_ROOT" ]; then + echo "check-required-contexts: repo directory '$REPO_ROOT' does not exist" >&2 + exit 2 +fi +WORKFLOWS_DIR="$REPO_ROOT/.github/workflows" +ALLOW_FILE="$REPO_ROOT/.machine_readable/required-contexts-allow.txt" + +if [ ! -d "$WORKFLOWS_DIR" ]; then + echo "check-required-contexts: workflows directory '$WORKFLOWS_DIR' does not exist" >&2 + exit 2 +fi +if [ ! -f "$ALLOW_FILE" ]; then + echo "check-required-contexts: mapping file '$ALLOW_FILE' does not exist" >&2 + exit 2 +fi + +command -v bun >/dev/null 2>&1 || { + echo "check-required-contexts: bun is required to parse workflow YAML" >&2 + exit 2 +} + +bun run - "$REPO_ROOT" <<'JS' +import { readdirSync, readFileSync, existsSync } from "node:fs"; +import { join, resolve } from "node:path"; + +const root = resolve(process.argv[2]); +const wfDir = join(root, ".github/workflows"); +const allowPath = join(root, ".machine_readable/required-contexts-allow.txt"); +const rulesetsDir = join(root, ".github/rulesets"); + +let errors = 0; +/** Report a violation and increment the shared error count without exiting. */ +function fail(msg) { + console.error(`FAIL: ${msg}`); + errors++; +} + +// 1. Parse mapping file: " => <.github/workflows/foo.yml>" +const mapped = new Map(); +const lines = readFileSync(allowPath, "utf8").split(/\r?\n/); +for (let idx = 0; idx < lines.length; idx++) { + const trimmed = lines[idx].trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const parts = trimmed.split("=>").map((s) => s.trim()); + if (parts.length !== 2 || !parts[0] || !parts[1]) { + console.error(`check-required-contexts: malformed mapping at line ${idx + 1}: ${lines[idx]}`); + process.exit(2); + } + mapped.set(parts[0], parts[1]); +} +if (mapped.size === 0) { + console.error("check-required-contexts: 0 contexts mapped in required-contexts-allow.txt"); + process.exit(2); +} + +// 2. Also collect any required_status_checks declared in .github/rulesets/*.json +if (existsSync(rulesetsDir)) { + for (const name of readdirSync(rulesetsDir).sort()) { + if (!name.endsWith(".json")) continue; + const rs = JSON.parse(readFileSync(join(rulesetsDir, name), "utf8")); + for (const rule of rs.rules ?? []) { + if (rule.type === "required_status_checks") { + for (const check of rule.parameters?.required_status_checks ?? []) { + if (check.context && !mapped.has(check.context)) { + fail(`ruleset ${name} requires context '${check.context}', which has no producer mapping in .machine_readable/required-contexts-allow.txt`); + } + } + } + } + } +} + +// 3. Verify each mapped producer workflow exists, triggers on pull_request, and +// has no pull_request.paths / paths-ignore filter that can strand a PR. +for (const [ctx, relWf] of mapped.entries()) { + const fullWf = join(root, relWf); + if (!existsSync(fullWf)) { + fail(`required context '${ctx}' maps to missing workflow '${relWf}'`); + continue; + } + const doc = Bun.YAML.parse(readFileSync(fullWf, "utf8")); + const onBlock = doc?.on ?? doc?.true; + const triggers = + typeof onBlock === "string" + ? [onBlock] + : Array.isArray(onBlock) + ? onBlock.map(String) + : onBlock && typeof onBlock === "object" + ? Object.keys(onBlock) + : []; + if (!triggers.includes("pull_request")) { + fail(`required context '${ctx}' producer '${relWf}' does not trigger on pull_request`); + continue; + } + const prCfg = + onBlock && typeof onBlock === "object" && !Array.isArray(onBlock) + ? onBlock.pull_request + : null; + if (prCfg && typeof prCfg === "object") { + if (Object.hasOwn(prCfg, "paths") || Object.hasOwn(prCfg, "paths-ignore")) { + fail(`required context '${ctx}' producer '${relWf}' filters pull_request by paths — strands PRs that do not touch those paths`); + } + } +} + +// 4. Enforce squabble != bypass across all workflows: no workflow may call +// `gh pr merge ... --admin` (joining backslash line continuations first). +for (const name of readdirSync(wfDir).sort()) { + if (!name.endsWith(".yml") && !name.endsWith(".yaml")) continue; + const raw = readFileSync(join(wfDir, name), "utf8").replace(/\\\r?\n\s*/g, " "); + if (/gh\s+pr\s+merge\b[^\n]*--admin\b/.test(raw)) { + fail(`.github/workflows/${name} invokes 'gh pr merge --admin' (violates squabble != bypass)`); + } +} + +if (errors > 0) { + console.error(`check-required-contexts: ${errors} violation(s) found`); + process.exit(1); +} +console.log(`OK: ${mapped.size} required status context(s) verified producible on pull_request; 0 admin-bypass workflows`); +JS diff --git a/scripts/check-variant-drift.sh b/scripts/check-variant-drift.sh index 5a2839e..0b28991 100755 --- a/scripts/check-variant-drift.sh +++ b/scripts/check-variant-drift.sh @@ -36,6 +36,19 @@ PARENT_NAME="${PARENT_SLUG##*/}" PIN=$(bash "$READ" "$DEED" variant parent-pin || true) [[ "$PIN" =~ ^[0-9a-f]{40}$ ]] || PIN="" +# A minted repo is not yet a target for this gate: the mint renders tokens and +# rewrites files, so most spine files differ from the parent by design, and the +# (normalise) rules do not fold the mint's answers (ADR-0003). repo-init no +# longer recommends arming it there. Say so before the findings, rather than +# leave a reader to "fix" ~100 expected lines by declaring them diverged; the +# verdict itself is unchanged (a gate that turns itself green is no gate). +ROLE=$(bash "$READ" "$DEED" variant role 2>/dev/null || true) +if [ "$ROLE" = "minted-repo" ]; then + echo "NOTE: $DEED records :role \"minted-repo\". Rendered files are expected to differ" + echo " from the parent until (normalise) folds the mint's answers; check the" + echo " template pin with scripts/check-template-conformance.sh instead." +fi + # section_paths SECTION — the path list :SECTION of the (variant (paths …)) # clause, one per line; nothing when the key is absent. section_paths() { diff --git a/scripts/compute-bench-baseline.sh b/scripts/compute-bench-baseline.sh new file mode 100755 index 0000000..d4bb004 --- /dev/null +++ b/scripts/compute-bench-baseline.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Compute the benchmark baseline from proven-bench JSON v1 run files and emit +# RFC 7493 (I-JSON) + RFC 8785 (JCS) canonical JSON. +# +# Usage: scripts/compute-bench-baseline.sh ... > benches/baseline.json +# +# Exit codes: 0 baseline written to stdout; 2 no baseline could be computed. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" || { echo "FAIL: cannot locate script directory" >&2; exit 2; } +MIN_RUNS=10 + +# die MESSAGE... — abort baseline generation with exit status 2. +die() { + echo "compute-bench-baseline: $*" >&2 + echo "compute-bench-baseline: NO BASELINE WRITTEN." >&2 + exit 2 +} + +# run_id_of FILE — print the parent directory's name if it contains only digits; +# print nothing otherwise. +run_id_of() { + local parent + parent="$(basename "$(dirname "$1")")" + if [[ "$parent" =~ ^[0-9]+$ ]]; then + printf '%s' "$parent" + fi +} + +# validate_run FILE — require exactly one proven-bench v1 document, exiting 2 on +# missing, unparseable, or malformed input. Historical checksums are not validated. +validate_run() { + local f="$1" errs + [ -f "$f" ] || die "run file '$f' does not exist" + if ! errs="$(jq -L "$SCRIPT_DIR" -sr 'include "bench-stats"; + if length != 1 then ["expected exactly one benchmark document"] + else .[0] | bench_errors end | .[]' "$f")"; then + die "run file '$f' is not parseable JSON" + fi + if [ -n "$errs" ]; then + printf '%s\n' "$errs" | sed "s|^| $f: |" >&2 + die "run file '$f' is not a well-formed proven-bench v1 document" + fi +} + +[ "$#" -ge 1 ] || die "usage: $0 ... (at least $MIN_RUNS)" +if [ "$#" -lt "$MIN_RUNS" ]; then + die "BASELINE BOOTSTRAPPING: $#/$MIN_RUNS runs - the gate would be INERT; supply at least $MIN_RUNS" +fi + +declare -A SEEN_PATHS=() +declare -A SEEN_RUN_IDS=() +ids_json='[]' +for f in "$@"; do + validate_run "$f" + canon_path="$(realpath "$f" 2>/dev/null || printf '%s' "$f")" + if [ -n "${SEEN_PATHS[$canon_path]:-}" ]; then + die "duplicate source run file '$f' - supply $MIN_RUNS distinct runs" + fi + SEEN_PATHS["$canon_path"]=1 + + rid="$(run_id_of "$f")" + if [ -n "$rid" ]; then + if [ -n "${SEEN_RUN_IDS[$rid]:-}" ]; then + die "duplicate source run_id '$rid' - supply $MIN_RUNS distinct runs" + fi + SEEN_RUN_IDS["$rid"]=1 + fi + + ids_json="$(jq -c --arg id "$rid" '. + [ if $id == "" then null else $id end ]' <<<"$ids_json")" \ + || die "could not record provenance for '$f'" +done + +if ! jq -L "$SCRIPT_DIR" -s -e 'include "bench-stats"; + (map(iterations_map) | unique | length) == 1' "$@" >/dev/null; then + die "the run files disagree on the workload set or iteration counts - they measure different experiments" +fi + +RAW_JSON="$(jq -L "$SCRIPT_DIR" -cS -s --argjson ids "$ids_json" ' + include "bench-stats"; + (if length != ($ids | length) then error("expected one benchmark document per source file") else . end) as $runs + | ($runs[0] | iterations_map) as $iters + | ($runs | map(loo_ratios)) as $loo + | ($runs | map(medians)) as $med + | { + schema_version: 1, + kind: "proven-bench-baseline", + bench_schema_version: 1, + statistic: "leave-one-out ratio: a workload median divided by the geometric mean of the other workloads medians in the same run", + method: "arithmetic mean over the source runs (taxonomy: mean of the last 10 CI runs on main)", + note: "Manual tolerance override (taxonomy Part IV permits one with a stated reason): the checker fails at 2.0x the baseline ratio or 2.5x the baseline absolute median, not the taxonomy 1.5x, because 1.5x would have failed green runs on shared GitHub runners (max observed ratio 1.53x, absolute 1.65x). Measured numbers are in benchmarks/README.adoc and scripts/check-bench-regression.sh.", + window: ($runs | length), + sources: [ range(0; $runs | length) as $i + | { run_id: $ids[$i], commit: $runs[$i].commit } ], + workloads: ( $iters | keys | map({ + key: ., + value: { + iterations: $iters[.], + mean_median_ns: ([ $med[][.] ] | add / length), + mean_loo_ratio: ([ $loo[][.] ] | add / length) + } }) | from_entries ) + } +' "$@")" || die "jq failed while computing the baseline" + +if command -v ijson-jcs >/dev/null 2>&1; then + printf '%s' "$RAW_JSON" | ijson-jcs canon || die "ijson-jcs failed to canonicalize baseline" + printf '\n' +elif command -v bun >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/ijson-jcs-cli.js" ]; then + printf '%s' "$RAW_JSON" | bun "$SCRIPT_DIR/ijson-jcs-cli.js" canon || die "ijson-jcs-cli.js failed to canonicalize baseline" + printf '\n' +else + die "neither ijson-jcs nor bun is available to canonicalize baseline" +fi diff --git a/scripts/emit-aggregate-inputs.sh b/scripts/emit-aggregate-inputs.sh new file mode 100755 index 0000000..f44be80 --- /dev/null +++ b/scripts/emit-aggregate-inputs.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Feed the proof-ladder files cited by Actually-Proven tests (from the JSON run +# report) into panic-attack's `aggregate`, which classifies each proof artifact +# (Closed/Holes/Refuted/Indeterminate) and folds it into a trust-tagged report. +# +# Exit contract (proven-tests-and-benches 3-outcome convention): +# 0 = all Actually-Proven ladder files classified Closed (or 0 Actually-Proven +# entries in a valid report) +# 1 = panic-attack aggregate reported a non-Closed verdict (Holes / Refuted) +# 2 = setup error: missing/unparseable report, missing jq, or missing +# panic-attack binary when Actually-Proven files require aggregation +# +# Usage: emit-aggregate-inputs.sh [panic-attack-binary] +set -euo pipefail + +REPORT="${1:-build/report/proven-tests-report.json}" +PANIC_ATTACK="${2:-panic-attack}" + +if [ ! -f "$REPORT" ]; then + echo "error: run report not found: $REPORT" >&2 + echo " run: ./build/exec/proven-tests --report $REPORT" >&2 + exit 2 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "error: jq is required to filter Actually-Proven entries in $REPORT" >&2 + exit 2 +fi + +WORK_DIR="$(mktemp -d)" +EXTRACT_TMP="$WORK_DIR/files" +trap 'rm -rf "$WORK_DIR"' EXIT +if ! jq -er ' + if (type != "object") or (.entries | type != "array") then + error("report must be an object with an entries array") + else + [ .entries[] + | select(.provenance == "Actually-Proven") + | (.proof_ladder // [])[] + | .proof_file + | select(type == "string" and length > 0) + ] | unique | .[] + end +' "$REPORT" > "$EXTRACT_TMP" 2>/dev/null; then + # Note: `jq -e` exits 4 when output is empty (`unique | .[]` produces zero + # items); distinguish malformed JSON/schema from a valid empty selection. + if ! jq -e 'type == "object" and (.entries | type == "array")' "$REPORT" >/dev/null 2>&1; then + echo "error: malformed or invalid run report: $REPORT" >&2 + exit 2 + fi + : > "$EXTRACT_TMP" +fi + +mapfile -t LADDER_FILES < "$EXTRACT_TMP" + +if [ "${#LADDER_FILES[@]}" -eq 0 ]; then + echo "no Actually-Proven proof-ladder files cited in $REPORT — nothing to aggregate" + exit 0 +fi + +echo "Actually-Proven proof-ladder files to aggregate:" +printf ' %s\n' "${LADDER_FILES[@]}" + +if ! command -v "$PANIC_ATTACK" >/dev/null 2>&1; then + echo "error: '$PANIC_ATTACK' not on PATH — cannot classify ${#LADDER_FILES[@]} Actually-Proven proof file(s)" >&2 + exit 2 +fi + +status=0 +idx=0 +for f in "${LADDER_FILES[@]}"; do + idx=$((idx + 1)) + output="$WORK_DIR/$idx.json" + label="$(basename "$f" .idr)-ladder" + echo "--- aggregate: $f (label=$label) ---" + if ! "$PANIC_ATTACK" aggregate --proof "$f" --label "$f=$label" --output "$output"; then + echo "error: aggregate failed for $f" >&2 + status=1 + elif ! jq -e ' + (.aggregated_proofs | type == "array" and length > 0) + and all(.aggregated_proofs[]; .verdict == "closed") + ' "$output" >/dev/null 2>&1; then + echo "error: aggregate reported a non-Closed verdict for $f" >&2 + status=1 + fi +done +exit "$status" diff --git a/scripts/gen-repo-map.sh b/scripts/gen-repo-map.sh index a9cdb64..befe940 100755 --- a/scripts/gen-repo-map.sh +++ b/scripts/gen-repo-map.sh @@ -13,7 +13,15 @@ # was a second whole-repo README contradicting the first. They rotted because # nothing checked them. A map that is regenerated and diffed in CI cannot. # -# Usage: bash scripts/gen-repo-map.sh [repo_root] +# Usage: bash scripts/gen-repo-map.sh [--worktree] [repo_root] +# +# --worktree maps the files `git add -A` would commit (tracked or untracked, +# not ignored, still on disk) instead of the index. `just repo-init` needs it: +# the mint runs before the child's first commit, so the index still lists +# what the mint removed (archetypes/, the template's deed name) and lacks what +# it created. Generated from the index there, the map was stale from birth, and +# Estate Rules failed at its freshness step on every run of lintel until +# lintel#8 regenerated it. On a committed tree the two modes agree. set -euo pipefail # Byte order, everywhere. `sort` is LOCALE-DEPENDENT: under en_US.UTF-8 it @@ -25,16 +33,29 @@ set -euo pipefail # locale, which is what tests/shape/repo_map_determinism_test.sh now does. export LC_ALL=C +MODE=index +if [ "${1:-}" = "--worktree" ]; then MODE=worktree; shift; fi REPO_ROOT="${1:-.}" cd "$REPO_ROOT" ALLOW=".machine_readable/root-allow.txt" OUT="docs/architecture/REPOSITORY-MAP.adoc" +# repo_files [PATH] — the files this map describes, under PATH when given. +repo_files() { + if [ "$MODE" = worktree ]; then + git ls-files -z --cached --others --exclude-standard -- "$@" \ + | while IFS= read -r -d '' f; do [ -e "$f" ] || [ -L "$f" ] && printf '%s\n' "$f"; done \ + | sort -u + else + git ls-files -- "$@" + fi +} + [ -f "$ALLOW" ] || { echo "ERROR: $ALLOW not found" >&2; exit 2; } mkdir -p "$(dirname "$OUT")" # Root entries, tracked shape only (mirrors check-root-shape.sh). -mapfile -t ENTRIES < <(git ls-files | awk -F/ '{print $1}' | sort -u) +mapfile -t ENTRIES < <(repo_files | awk -F/ '{print $1}' | sort -u) # name -> annotation, harvested from the allowlist's own comments. declare -A NOTE=() OPTIONAL=() @@ -60,7 +81,7 @@ done < "$ALLOW" # these counts were folded into the repo deed — standards#837 pilot.) declared_only() { local d="$1" n - n=$(git ls-files "$d" | awk -F/ '{print $NF}' \ + n=$(repo_files "$d" | awk -F/ '{print $NF}' \ | grep -vxE 'README\.adoc|\.gitkeep' | wc -l) [ "$n" -eq 0 ] } @@ -106,7 +127,7 @@ declared_only() { echo "your own repo - but do not read their emptiness as neglect here." echo first=1 - for d in $(git ls-files | grep '/' | sed 's|/[^/]*$||' | sort -u); do + for d in $(repo_files | grep '/' | sed 's|/[^/]*$||' | sort -u); do if declared_only "$d"; then [ $first -eq 1 ] && { echo "[cols=\"1\"]"; echo '|==='; first=0; } echo "| \`$d/\`" diff --git a/scripts/ijson-jcs-cli.js b/scripts/ijson-jcs-cli.js new file mode 100755 index 0000000..36cbfdd --- /dev/null +++ b/scripts/ijson-jcs-cli.js @@ -0,0 +1,408 @@ +#!/usr/bin/env bun +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +// +// ijson-jcs-cli.js — local Bun fallback matching hyperpolymath/ijson-jcs's CLI +// contract (`check`, `fix`, `canon`) for environments where the compiled Rust +// binary is not yet installed. CI installs and runs hyperpolymath/ijson-jcs +// directly at a pinned commit; both enforce RFC 7493 (I-JSON) + RFC 8785 (JCS) +// as transitional tooling until metadatastician/kyq passes positive/negative +// tests across its four-format scope. + +import { readFileSync } from "node:fs"; + +const EXIT_OK = 0; +const EXIT_NOT_CANONICAL = 1; +const EXIT_INVALID = 2; +const SAFE_MAX_INT = 9007199254740991n; // 2^53 - 1 (RFC 7493 §2.2) + +const USAGE = `ijson-jcs-cli.js - I-JSON (RFC 7493) parsing and JCS (RFC 8785) canonicalisation (stdin filter) + +USAGE: + ijson-jcs-cli.js check Read stdin; exit 0 if canonical, 1 if valid I-JSON but non-canonical, 2 if invalid + ijson-jcs-cli.js canon Read stdin; write canonical JCS (no trailing newline) to stdout +`; + +/** + * Decode a buffer strictly as UTF-8 without BOM (RFC 7493 §2.1). + * @param {Uint8Array} bytes + * @returns {string} + * @throws {Error} If a BOM is present. + * @throws {TypeError} If the bytes are not valid UTF-8. + */ +function decodeStrictUtf8(bytes) { + if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) { + throw new Error("UTF-8 BOM is forbidden in I-JSON"); + } + const decoder = new TextDecoder("utf-8", { fatal: true }); + return decoder.decode(bytes); +} + +/** + * Reject Unicode noncharacters and lone surrogates (RFC 7493 §2.1). + * @param {string} s + * @throws {Error} If the string contains a noncharacter or lone surrogate. + */ +function checkStringCodePoints(s) { + for (let k = 0; k < s.length; k++) { + const c = s.charCodeAt(k); + if (c >= 0xd800 && c <= 0xdfff) { + if (c <= 0xdbff && k + 1 < s.length) { + const c2 = s.charCodeAt(k + 1); + if (c2 >= 0xdc00 && c2 <= 0xdfff) { + const cp = ((c - 0xd800) << 10) + (c2 - 0xdc00) + 0x10000; + if ((cp & 0xfffe) === 0xfffe) { + throw new Error(`Unicode noncharacter U+${cp.toString(16).toUpperCase()}`); + } + k++; + continue; + } + } + throw new Error(`lone UTF-16 surrogate 0x${c.toString(16)}`); + } + if ((c >= 0xfdd0 && c <= 0xfdef) || c === 0xfffe || c === 0xffff) { + throw new Error(`Unicode noncharacter U+${c.toString(16).toUpperCase()}`); + } + } +} + +/** + * Parse `raw` strictly as RFC 7493 I-JSON, rejecting duplicate keys, + * integer tokens outside +/-(2^53-1), non-finite floats, lone surrogates, + * and Unicode noncharacters. + * @param {string} raw + * @returns {unknown} The parsed value, with objects represented as Maps. + * @throws {Error} If JSON syntax or the above validation rules are violated. + */ +function parseIJson(raw) { + let i = 0; + const n = raw.length; + + /** Advance the shared offset past any JSON whitespace. */ + function ws() { + while (i < n) { + const ch = raw[i]; + if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") i++; + else break; + } + } + + /** + * Read a quoted string at the shared offset and advance past its closing quote. + * Throws on malformed escapes, unterminated strings, or invalid code points. + */ + function parseString() { + if (raw[i] !== '"') throw new Error(`expected '"' at byte ${i}`); + i++; + let out = ""; + while (i < n) { + const ch = raw[i++]; + if (ch === '"') { + checkStringCodePoints(out); + return out; + } + if (ch.charCodeAt(0) < 0x20) { + throw new Error(`unescaped control character at offset ${i - 1}`); + } + if (ch === "\\") { + if (i >= n) throw new Error("unterminated escape sequence"); + const esc = raw[i++]; + if (esc === '"' || esc === "\\" || esc === "/") out += esc; + else if (esc === "b") out += "\b"; + else if (esc === "f") out += "\f"; + else if (esc === "n") out += "\n"; + else if (esc === "r") out += "\r"; + else if (esc === "t") out += "\t"; + else if (esc === "u") { + const hex = raw.slice(i, i + 4); + if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new Error("invalid \\u escape"); + i += 4; + const cp = parseInt(hex, 16); + if (cp >= 0xd800 && cp <= 0xdbff) { + if (raw.slice(i, i + 2) !== "\\u") throw new Error("unpaired high surrogate"); + const hex2 = raw.slice(i + 2, i + 6); + if (!/^[0-9a-fA-F]{4}$/.test(hex2)) throw new Error("invalid low surrogate escape"); + const cp2 = parseInt(hex2, 16); + if (cp2 < 0xdc00 || cp2 > 0xdfff) throw new Error("unpaired high surrogate"); + i += 6; + out += String.fromCodePoint(((cp - 0xd800) << 10) + (cp2 - 0xdc00) + 0x10000); + } else if (cp >= 0xdc00 && cp <= 0xdfff) { + throw new Error("unpaired low surrogate"); + } else { + out += String.fromCharCode(cp); + } + } else { + throw new Error(`invalid escape '\\${esc}'`); + } + } else { + out += ch; + } + } + throw new Error("unterminated string"); + } + + /** + * Read a JSON number at the shared offset, advancing past it and converting -0 to 0. + * Throws on malformed numbers, unsafe integer tokens, or non-finite results. + */ + function parseNumber() { + const start = i; + if (raw[i] === "-") i++; + if (i >= n) throw new Error("incomplete number"); + if (raw[i] === "0") { + i++; + if (i < n && raw[i] >= "0" && raw[i] <= "9") { + throw new Error("leading zero in number"); + } + } else if (raw[i] >= "1" && raw[i] <= "9") { + while (i < n && raw[i] >= "0" && raw[i] <= "9") i++; + } else { + throw new Error(`invalid number at offset ${start}`); + } + let isFloat = false; + if (i < n && raw[i] === ".") { + isFloat = true; + i++; + if (i >= n || raw[i] < "0" || raw[i] > "9") throw new Error("missing fraction digits"); + while (i < n && raw[i] >= "0" && raw[i] <= "9") i++; + } + if (i < n && (raw[i] === "e" || raw[i] === "E")) { + isFloat = true; + i++; + if (i < n && (raw[i] === "+" || raw[i] === "-")) i++; + if (i >= n || raw[i] < "0" || raw[i] > "9") throw new Error("missing exponent digits"); + while (i < n && raw[i] >= "0" && raw[i] <= "9") i++; + } + const token = raw.slice(start, i); + if (!isFloat) { + const mag = BigInt(token.startsWith("-") ? token.slice(1) : token); + if (mag > SAFE_MAX_INT) { + throw new Error(`integer ${token} exceeds IEEE-754 safe range +/-(2^53-1)`); + } + } + const num = Number(token); + if (!Number.isFinite(num)) { + throw new Error(`number ${token} overflows IEEE-754 double`); + } + return Object.is(num, -0) ? 0 : num; + } + + /** + * Read the next value after whitespace, advancing the shared offset. + * Objects become Maps; arrays retain their order. Throws on invalid syntax, + * duplicate decoded keys, or errors propagated from string or number parsing. + */ + function parseValue() { + ws(); + if (i >= n) throw new Error("unexpected end of input"); + const ch = raw[i]; + if (ch === '"') return parseString(); + if (ch === "-" || (ch >= "0" && ch <= "9")) return parseNumber(); + if (ch === "{") { + i++; + ws(); + const obj = new Map(); + if (raw[i] === "}") { + i++; + return obj; + } + let closedObj = false; + while (i < raw.length) { + ws(); + const key = parseString(); + if (obj.has(key)) { + throw new Error(`duplicate object key ${JSON.stringify(key)}`); + } + ws(); + if (raw[i] !== ":") throw new Error(`expected ':' after key ${JSON.stringify(key)}`); + i++; + const val = parseValue(); + obj.set(key, val); + ws(); + if (raw[i] === ",") { + i++; + continue; + } + if (raw[i] === "}") { + i++; + closedObj = true; + break; + } + throw new Error(`expected ',' or '}' at offset ${i}`); + } + if (!closedObj) throw new Error("unterminated object"); + return obj; + } + if (ch === "[") { + i++; + ws(); + const arr = []; + if (raw[i] === "]") { + i++; + return arr; + } + let closedArr = false; + while (i < raw.length) { + arr.push(parseValue()); + ws(); + if (raw[i] === ",") { + i++; + continue; + } + if (raw[i] === "]") { + i++; + closedArr = true; + break; + } + throw new Error(`expected ',' or ']' at offset ${i}`); + } + if (!closedArr) throw new Error("unterminated array"); + return arr; + } + if (raw.slice(i, i + 4) === "true") { + i += 4; + return true; + } + if (raw.slice(i, i + 5) === "false") { + i += 5; + return false; + } + if (raw.slice(i, i + 4) === "null") { + i += 4; + return null; + } + throw new Error(`unexpected token at offset ${i}`); + } + + const val = parseValue(); + ws(); + if (i !== n) throw new Error(`trailing characters at offset ${i}`); + return val; +} + +/** + * Compare two strings by UTF-16 code units (RFC 8785 §3.2.3). + * @param {string} a + * @param {string} b + */ +function compareUtf16(a, b) { + const len = Math.min(a.length, b.length); + for (let idx = 0; idx < len; idx++) { + const diff = a.charCodeAt(idx) - b.charCodeAt(idx); + if (diff !== 0) return diff; + } + return a.length - b.length; +} + +/** + * Serialize a validated value into RFC 8785 (JCS) canonical form. + * @param {unknown} val - A parsed value whose objects are Maps with string keys. + * @returns {string} + * @throws {Error} If a value is neither a JSON primitive, an array, nor a Map. + */ +function serializeJcs(val) { + if (val === null || typeof val === "boolean" || typeof val === "number" || typeof val === "string") { + return JSON.stringify(val); + } + if (Array.isArray(val)) { + return "[" + val.map(serializeJcs).join(",") + "]"; + } + if (val instanceof Map) { + const keys = [...val.keys()].sort(compareUtf16); + return "{" + keys.map((k) => JSON.stringify(k) + ":" + serializeJcs(val.get(k))).join(",") + "}"; + } + throw new Error("unsupported JSON value"); +} + +/** + * Return JCS text without a trailing newline from BOM-free UTF-8 I-JSON bytes. + * Propagates decoding and I-JSON validation errors. + */ +function canonicalString(bytes) { + const text = decodeStrictUtf8(bytes); + const parsed = parseIJson(text); + return serializeJcs(parsed); +} + +const encoder = new TextEncoder(); + +/** + * Check stdin against JCS, allowing a single trailing LF. + * Return 0 for canonical input, 1 for valid but non-canonical input, or 2 for + * caught read or validation errors, which are reported to stderr. + */ +function cmdCheckStdin() { + let bytes; + try { + bytes = readFileSync(0); + } catch (err) { + console.error(`INVALID : cannot read: ${err.message}`); + return EXIT_INVALID; + } + try { + const canon = encoder.encode(canonicalString(bytes)); + const stripped = + bytes.length > 0 && bytes[bytes.length - 1] === 0x0a + ? bytes.subarray(0, bytes.length - 1) + : bytes; + if (stripped.length !== canon.length) { + return EXIT_NOT_CANONICAL; + } + for (let idx = 0; idx < canon.length; idx++) { + if (stripped[idx] !== canon[idx]) { + return EXIT_NOT_CANONICAL; + } + } + return EXIT_OK; + } catch (err) { + console.error(`INVALID : ${err.message}`); + return EXIT_INVALID; + } +} + +/** + * Write stdin as JCS without a trailing newline and return 0. + * Return 2 and report caught read, validation, or write errors to stderr. + */ +function cmdCanon() { + let bytes; + try { + bytes = readFileSync(0); + } catch (err) { + console.error(`INVALID : cannot read: ${err.message}`); + return EXIT_INVALID; + } + try { + process.stdout.write(canonicalString(bytes)); + return EXIT_OK; + } catch (err) { + console.error(`INVALID : ${err.message}`); + return EXIT_INVALID; + } +} + +const args = process.argv.slice(2); +if (args.includes("-h") || args.includes("--help")) { + process.stdout.write(USAGE); + process.exit(EXIT_OK); +} +if (args.includes("-V") || args.includes("--version")) { + console.log("ijson-jcs-cli.js 0.1.0"); + process.exit(EXIT_OK); +} +if (args.length === 0) { + console.error("ijson-jcs: missing command"); + process.exit(EXIT_INVALID); +} +const [cmd, ...rest] = args; +if (rest.length > 0) { + console.error(`ijson-jcs: '${cmd}' reads stdin and takes no arguments`); + process.exit(EXIT_INVALID); +} +if (cmd === "check") process.exit(cmdCheckStdin()); +if (cmd === "canon") { + process.exitCode = cmdCanon(); +} else { + console.error(`ijson-jcs: unknown command '${cmd}'`); + process.exitCode = EXIT_INVALID; +} diff --git a/scripts/kyaml-format.sh b/scripts/kyaml-format.sh new file mode 100755 index 0000000..f348034 --- /dev/null +++ b/scripts/kyaml-format.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# kyaml-format.sh — format arbitrary YAML as KYAML, or check that it already is +# (YAML-POLICY Y-3; KYAML adoption step 3, standards#1022). +# +# The rewriter is `yq -p yaml -o kyaml '.'`, verbatim — the one rewriter the +# step-2 comment-preservation proof (tools/yaml-comment-proof, standards#1021) +# covers. That proof does not generalise to any other rewriter, so this script +# deliberately adds none: no post-processing, no re-indenting, no sed. +# +# Usage: +# kyaml-format.sh [--check] FILE... +# +# (default) rewrite each FILE in place as KYAML when it is not already. +# --check rewrite nothing; exit 1 if any FILE is not already KYAML. +# +# Exit contract: +# 0 = every FILE is KYAML (after the rewrite, in format mode) +# 1 = --check: at least one FILE is not KYAML (each one is named) +# 2 = refused: no FILE given, a FILE is missing or unparseable, a FILE has no +# final newline, is a symlink in format mode, or the rewrite would change +# the parsed data. A refusal +# never rewrites that FILE and never counts as clean. +# +# Why the final-newline refusal: a `|` block scalar that ends at EOF with no +# line break is read differently by go-yaml (yq) and eemeli/yaml — measured on +# k9-contractile.yml, the trailing "\n" of its last run: body exists for one +# parser and not the other. Formatting such a file would silently pick one +# reading, so the script refuses it and names the fix instead. +set -uo pipefail + +MODE=format +if [ "${1:-}" = "--check" ]; then + MODE=check + shift +fi + +if [ "$#" -eq 0 ]; then + echo "kyaml-format: no files given (usage: kyaml-format.sh [--check] FILE...)" >&2 + exit 2 +fi + +command -v yq >/dev/null 2>&1 || { echo "kyaml-format: yq is not on PATH" >&2; exit 2; } + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# canonical_data — prints the file's parsed value as key-sorted JSON; +# fails when the file does not parse. +canonical_data() { + yq -p yaml -o json -I 0 'sort_keys(..)' "$1" +} + +# kyaml_of — writes the KYAML rendering of to ; +# fails when does not parse. +kyaml_of() { + yq -p yaml -o kyaml '.' "$1" > "$2" +} + +# refuse — reports a file this script will not judge. +refuse() { + echo "::error file=$1::kyaml-format: REFUSED $1 — $2" >&2 + refused=$((refused + 1)) +} + +refused=0 drift=0 clean=0 rewritten=0 +i=0 +for f in "$@"; do + i=$((i + 1)) + out="$TMP/$i.kyaml" + if [ "$MODE" = format ] && [ -L "$f" ]; then + refuse "$f" "symlink; format its target explicitly"; continue + fi + if [ ! -f "$f" ]; then + refuse "$f" "no such file"; continue + fi + if [ -s "$f" ] && [ -n "$(tail -c1 "$f")" ]; then + refuse "$f" "no final newline; add one first (a trailing block scalar is ambiguous without it)"; continue + fi + if ! kyaml_of "$f" "$out" 2>"$TMP/err"; then + refuse "$f" "does not parse: $(head -1 "$TMP/err")"; continue + fi + if cmp -s "$f" "$out"; then + clean=$((clean + 1)); continue + fi + # The rewrite is only acceptable when it preserves the data exactly. + if ! before=$(canonical_data "$f") || ! after=$(canonical_data "$out") || [ "$before" != "$after" ]; then + refuse "$f" "the KYAML rendering does not parse to the same data"; continue + fi + if [ "$MODE" = check ]; then + echo "::error file=$f::kyaml-format: $f is not KYAML — run scripts/kyaml-format.sh $f" >&2 + drift=$((drift + 1)) + else + staged="" + if ! staged="$(mktemp "$(dirname "$f")/.kyaml.XXXXXX")" || ! cp -p "$f" "$staged" 2>/dev/null && ! : > "$staged"; then + [ -n "$staged" ] && rm -f "$staged" + refuse "$f" "could not stage temporary file in $(dirname "$f")" + continue + fi + if ! cat "$out" > "$staged" || ! cmp -s "$out" "$staged" || ! mv -f "$staged" "$f"; then + rm -f "$staged" + refuse "$f" "failed to atomically replace $f" + continue + fi + echo "kyaml-format: rewrote $f" + rewritten=$((rewritten + 1)) + fi +done + +total=$# +if [ "$refused" -gt 0 ]; then + echo "kyaml-format: $refused of $total file(s) REFUSED — not judged, not clean" >&2 + exit 2 +fi +if [ "$drift" -gt 0 ]; then + echo "kyaml-format: $drift of $total file(s) are not KYAML" >&2 + exit 1 +fi +if [ "$MODE" = check ]; then + echo "kyaml-format: all $total file(s) are KYAML" +else + echo "kyaml-format: $total file(s) KYAML ($rewritten rewritten, $clean already clean)" +fi +exit 0 diff --git a/scripts/merge-actions-lock.rs b/scripts/merge-actions-lock.rs index 332b493..53cd139 100644 --- a/scripts/merge-actions-lock.rs +++ b/scripts/merge-actions-lock.rs @@ -23,6 +23,8 @@ // are a conflict for `gh actions-lock` to settle, not for this tool; // * the output keeps the base's header and sorts each section by key in byte // order, as `gh actions-lock` does, so a second merge changes nothing. + +#![forbid(unsafe_code)] // // The base file is rewritten in place. // diff --git a/scripts/prune-dependabot-ecosystems.rs b/scripts/prune-dependabot-ecosystems.rs index fb0cacf..f232adf 100644 --- a/scripts/prune-dependabot-ecosystems.rs +++ b/scripts/prune-dependabot-ecosystems.rs @@ -17,6 +17,8 @@ // // Usage: prune-dependabot-ecosystems.rs +#![forbid(unsafe_code)] + use std::env; use std::fs; use std::path::Path; diff --git a/scripts/rsr-campaign.sh b/scripts/rsr-campaign.sh index 920e2b4..834f321 100644 --- a/scripts/rsr-campaign.sh +++ b/scripts/rsr-campaign.sh @@ -281,7 +281,7 @@ excluded() { # path -> 0 if excluded by the manifest REPORT="${REPORT:-$WORK_DIR/campaign-report.tsv}" printf 'repo\tpath\toutcome\tdetail\n' > "$REPORT" -ADDED=0; UPDATED=0; UNCHANGED=0; SKIPPED=0; REFUSED=0; NOCHANGE=0; FAILED=0 +ADDED=0; UPDATED=0; SKIPPED=0; REFUSED=0; NOCHANGE=0; FAILED=0 for repo in "${SELECTED[@]}"; do [ -z "$repo" ] && continue @@ -328,7 +328,7 @@ for repo in "${SELECTED[@]}"; do while IFS= read -r rel; do [ -z "$rel" ] && continue case "$rel" in - *.gitkeep|*/.gitkeep) continue ;; + *.gitkeep) continue ;; esac if excluded "$rel"; then printf '%s\t%s\tEXCLUDED\tmanifest exclude\n' "$repo" "$rel" >> "$REPORT" diff --git a/scripts/run-proven-suite.sh b/scripts/run-proven-suite.sh new file mode 100755 index 0000000..b67ee9f --- /dev/null +++ b/scripts/run-proven-suite.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Unified 3-outcome test & proof runner grounded in hyperpolymath/proven-tests-and-benches. +# +# Usage: scripts/run-proven-suite.sh [--require-zig] [--bench] [--report FILE] +# +# Exit contract: +# 0 = every tier passed (Silence on clean tree + Firing on every negative mutant) +# 1 = a subject check, proof honesty lint, or contract test failed +# 2 = setup / cannot-look failure (missing tool or missing suite — never a pass) +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REQUIRE_ZIG=0 +RUN_BENCH=0 +REPORT_PATH="" +OWNED_REPORT_TMP="" +BENCH_TMP="" + +# Remove the runner-owned temporary report directory and benchmark file, if set. +cleanup() { + [ -n "$OWNED_REPORT_TMP" ] && rm -rf "$OWNED_REPORT_TMP" + [ -n "$BENCH_TMP" ] && rm -f "$BENCH_TMP" +} +trap cleanup EXIT + +while [ "$#" -gt 0 ]; do + case "$1" in + --require-zig) REQUIRE_ZIG=1; shift ;; + --bench) RUN_BENCH=1; shift ;; + --report) + [ "$#" -ge 2 ] || { echo "run-proven-suite: --report needs a path" >&2; exit 2; } + REPORT_PATH="$2" + shift 2 + ;; + *) echo "run-proven-suite: unknown option '$1'" >&2; exit 2 ;; + esac +done + +cd "$REPO_ROOT" || exit 2 + +if [ -z "$REPORT_PATH" ]; then + OWNED_REPORT_TMP="$(mktemp -d)" + REPORT_PATH="$OWNED_REPORT_TMP/proven-tests-report.json" +else + mkdir -p "$(dirname "$REPORT_PATH")" || { echo "run-proven-suite: cannot create report parent directory" >&2; exit 2; } +fi + +subject_failures=0 +setup_failures=0 + +# run_lane LABEL COMMAND... — run a lane and accumulate its outcome without +# propagating its exit status: 2 increments setup_failures; other nonzero +# statuses increment subject_failures. +run_lane() { + local label="$1" rc=0 + shift + printf '==> [proven-suite] %s\n' "$label" + "$@" || rc=$? + case "$rc" in + 0) printf ' PASS (0): %s\n' "$label" ;; + 2) + printf ' INERT/SETUP (2): %s\n' "$label" >&2 + setup_failures=$((setup_failures + 1)) + ;; + *) + printf ' FAIL (%d): %s\n' "$rc" "$label" >&2 + subject_failures=$((subject_failures + 1)) + ;; + esac +} + +# require_file PATH — return 0 for an existing regular file; otherwise increment +# setup_failures and return 1. +require_file() { + if [ ! -f "$1" ]; then + printf ' INERT/SETUP (2): missing required gate script: %s\n' "$1" >&2 + setup_failures=$((setup_failures + 1)) + return 1 + fi + return 0 +} + +# ── Tier 1: Idris2 honesty lint + proof-ladder report ──────────────────────── +if require_file scripts/check-escape-hatches.sh; then + run_lane "Idris2 proof escape-hatch & totality gate" \ + bash scripts/check-escape-hatches.sh src verification .machine_readable/coaptation +fi + +mapfile -t PROOF_FILES < <(find src verification .machine_readable/coaptation -name '*.idr' -type f | LC_ALL=C sort) + +# Only claim "Actually-Proven" when the Idris2 typechecker discharges every +# module in the corpus; static escape-hatch totality verification without the +# Idris2 kernel is recorded honestly as "Static-Checked" (never overclaiming). +PROOF_PROVENANCE="Static-Checked" +if command -v idris2 >/dev/null 2>&1; then + idris_ok=1 + for pf in "${PROOF_FILES[@]}"; do + if ! idris2 --check "$pf" >/dev/null 2>&1; then + idris_ok=0 + subject_failures=$((subject_failures + 1)) + printf ' FAIL (1): idris2 --check %s\n' "$pf" >&2 + fi + done + if [ "$idris_ok" -eq 1 ] && [ "${#PROOF_FILES[@]}" -gt 0 ]; then + PROOF_PROVENANCE="Actually-Proven" + fi +fi + +{ + printf '{"entries":[{"name":"rsr-idris2-proof-corpus","proof_ladder":[' + first=1 + for pf in "${PROOF_FILES[@]}"; do + [ "$first" -eq 1 ] || printf ',' + first=0 + printf '{"proof_file":"%s"}' "$pf" + done + printf '],"provenance":"%s"}],"schema_version":1}\n' "$PROOF_PROVENANCE" +} > "$REPORT_PATH" + +if require_file scripts/emit-aggregate-inputs.sh; then + run_lane "Proof-ladder aggregation (emit-aggregate-inputs)" \ + bash scripts/emit-aggregate-inputs.sh "$REPORT_PATH" +fi + +# ── Tier 2: Repository invariant & estate data-format gates ────────────────── +require_file scripts/check-root-shape.sh && \ + run_lane "Root shape bidirectional allowlist" bash scripts/check-root-shape.sh . +require_file scripts/check-ijson-jcs.sh && \ + run_lane "I-JSON (RFC 7493) + JCS (RFC 8785) canonical gate" bash scripts/check-ijson-jcs.sh --enforce . +require_file scripts/check-lock-sync.sh && \ + run_lane "Workflow actions.lock integrity" bash scripts/check-lock-sync.sh .github/workflows +require_file scripts/check-required-contexts.sh && \ + run_lane "Required status contexts producibility gate" bash scripts/check-required-contexts.sh . +require_file .github/hooks/validate-deed.sh && \ + run_lane "CHORA deed validation & retired .a2ml refusal" bash .github/hooks/validate-deed.sh +require_file .github/hooks/validate-k9.sh && \ + run_lane "K9 pedigree & contract validation" bash .github/hooks/validate-k9.sh + +# ── Tier 3: Silence + Firing contract & E2E test suites ────────────────────── +require_file tests/proven_suite_test.sh && \ + run_lane "Proven tools Silence + Firing self-tests" bash tests/proven_suite_test.sh +require_file tests/workflows/check_ijson_jcs_test.sh && \ + run_lane "I-JSON/JCS Silence + Firing self-tests" bash tests/workflows/check_ijson_jcs_test.sh +require_file tests/workflows/squabble_gate_test.sh && \ + run_lane "CICD-Squabbler & required-context Silence + Firing self-tests" bash tests/workflows/squabble_gate_test.sh +require_file tests/shape/check_root_shape_test.sh && \ + run_lane "Root shape Silence + Firing self-tests" bash tests/shape/check_root_shape_test.sh +require_file www/tests/run-all.sh && \ + run_lane "Site & .well-known boundary + planted controls" bash www/tests/run-all.sh + +# ── Tier 4: Zig FFI unit tests + E2E mint instantiation ────────────────────── +# Run Zig FFI tests in a temporary copy with project placeholders filled in. +# Remove the copy on return and propagate the zig build test exit status. +run_zig_ffi_tests() { + local work tok_lc tok_uc + work="$(mktemp -d)" + # shellcheck disable=SC2064 + trap "rm -rf '$work'" RETURN + cp -R src/interface/ffi "$work/ffi" + tok_lc="{{""project}}" + tok_uc="{{""PROJECT}}" + sed -i -e "s/${tok_lc}/test_project/g" -e "s/${tok_uc}/TEST_PROJECT/g" \ + "$work/ffi/build.zig" "$work/ffi/src/main.zig" "$work/ffi/test/integration_test.zig" 2>/dev/null || true + (cd "$work/ffi" && zig build test) +} + +if command -v zig >/dev/null 2>&1; then + run_lane "Zig FFI ABI layout & memory-leak unit tests" run_zig_ffi_tests +elif [ "$REQUIRE_ZIG" -eq 1 ]; then + printf ' INERT/SETUP (2): zig compiler required (--require-zig) but missing on PATH\n' >&2 + setup_failures=$((setup_failures + 1)) +else + printf '==> [proven-suite] Zig FFI unit tests: skipped locally (pass --require-zig in CI to fail-closed)\n' +fi + +if command -v rustc >/dev/null 2>&1 && command -v just >/dev/null 2>&1; then + require_file tests/e2e/template_instantiation_test.sh && \ + run_lane "Template mint instantiation E2E" bash tests/e2e/template_instantiation_test.sh . +fi + +# ── Tier 5: Benchmark regression gate (optional --bench) ───────────────────── +if [ "$RUN_BENCH" -eq 1 ]; then + if ! command -v jq >/dev/null 2>&1; then + printf ' INERT/SETUP (2): jq required for --bench regression gate\n' >&2 + setup_failures=$((setup_failures + 1)) + elif require_file benches/template_bench.sh && require_file scripts/check-bench-regression.sh && require_file benches/baseline.json; then + BENCH_TMP="$(mktemp)" + run_lane "Template benchmark execution (--json)" \ + bash -c "bash benches/template_bench.sh --json . > '$BENCH_TMP'" + run_lane "Six-Sigma leave-one-out benchmark regression gate" \ + bash scripts/check-bench-regression.sh "$BENCH_TMP" benches/baseline.json + fi +fi + +if [ "$setup_failures" -gt 0 ]; then + printf 'proven-suite: SETUP/INERT FAILURE (%d setup, %d subject)\n' "$setup_failures" "$subject_failures" >&2 + exit 2 +fi +if [ "$subject_failures" -gt 0 ]; then + printf 'proven-suite: SUBJECT FAILURE (%d failing lane(s))\n' "$subject_failures" >&2 + exit 1 +fi +printf 'proven-suite: ALL LANES PASSED\n' +exit 0 diff --git a/scripts/strip-instruction-blocks.rs b/scripts/strip-instruction-blocks.rs index eb2060d..b33f5ed 100644 --- a/scripts/strip-instruction-blocks.rs +++ b/scripts/strip-instruction-blocks.rs @@ -12,19 +12,20 @@ // terminator of a preceding comment. That is what keeps an SPDX comment that // sits immediately above an instructions block intact. // -// Usage: strip-instruction-blocks.rs [ROOT] (default ROOT = .) +// Usage: strip-instruction-blocks.rs FILE... or ... - < newline-separated paths + +#![forbid(unsafe_code)] use std::env; use std::fs; +use std::io::{self, Read}; use std::path::{Path, PathBuf}; +use std::process; const MARKER: &str = "TEMPLATE INSTRUCTIONS"; const OPEN: &str = ""; -/// Directories never descended into. Mirrors the Ruby original. -const SKIP_DIRS: [&str; 5] = [".git", "node_modules", ".venv", "target", "dist"]; - /// Remove every HTML comment whose body contains MARKER, then collapse runs of /// three or more newlines to two. /// @@ -99,70 +100,81 @@ fn collapse_blank_runs(s: &str) -> String { out } -fn process(path: &Path, changed: &mut usize) { - let bytes = match fs::read(path) { - Ok(b) => b, - Err(_) => return, - }; - // Non-UTF-8 and unreadable files are skipped, never rewritten blind. +fn process(path: &Path, changed: &mut usize) -> Result<(), String> { + let bytes = fs::read(path).map_err(|e| format!("{}: {}", path.display(), e))?; + // Non-UTF-8 files are skipped, never rewritten blind. let text = match String::from_utf8(bytes) { Ok(t) => t, - Err(_) => return, + Err(_) => return Ok(()), }; if !text.contains(MARKER) { - return; + return Ok(()); } let updated = strip_blocks(&text); if updated == text { - return; - } - if fs::write(path, updated).is_err() { - return; + return Ok(()); } + fs::write(path, updated).map_err(|e| format!("{}: {}", path.display(), e))?; println!(" instruction block: stripped from {}", path.display()); *changed += 1; + Ok(()) } -fn walk(root: &Path, changed: &mut usize) { - // Explicit stack rather than recursion: deep trees must not blow the stack. - let mut stack: Vec = vec![root.to_path_buf()]; - while let Some(dir) = stack.pop() { - let entries = match fs::read_dir(&dir) { - Ok(e) => e, - Err(_) => continue, - }; - for entry in entries.flatten() { - let path = entry.path(); - let ft = match entry.file_type() { - Ok(ft) => ft, - Err(_) => continue, - }; - if ft.is_symlink() { - continue; - } - if ft.is_dir() { - let skip = path - .file_name() - .and_then(|n| n.to_str()) - .map(|n| SKIP_DIRS.contains(&n)) - .unwrap_or(false); - if skip { - continue; - } - stack.push(path); - continue; - } - if ft.is_file() { - process(&path, changed); - } +/// The files to clean: every argument, where `-` reads one path per line +/// from stdin. A directory is refused rather than walked. This tool used to +/// walk its ROOT argument, and so entered git-ignored caches: minting +/// metadatastician/lintel it edited the pinned sibling checkouts under +/// _build/dependencies/, breaking their pins, and it stripped the fixture +/// blocks of the template's own tests, which the mint's leftover check +/// exempts. Which files belong to a repository is the caller's question +/// (repo-init asks git: its mint_files), not a guess made here. +fn targets(args: &[String]) -> Result, String> { + let mut out = Vec::new(); + for a in args { + if a == "-" { + let mut buf = String::new(); + io::stdin() + .read_to_string(&mut buf) + .map_err(|e| format!("stdin: {}", e))?; + out.extend(buf.lines().filter(|l| !l.is_empty()).map(PathBuf::from)); + } else { + out.push(PathBuf::from(a)); + } + } + for p in &out { + if p.is_dir() { + return Err(format!( + "{} is a directory; pass files (e.g. `git ls-files | {} -`)", + p.display(), + "strip-instruction-blocks" + )); } } + Ok(out) } fn main() { - let root = env::args().nth(1).unwrap_or_else(|| ".".to_string()); + let args: Vec = env::args().skip(1).collect(); + if args.is_empty() { + eprintln!("usage: strip-instruction-blocks FILE... | strip-instruction-blocks - < list"); + process::exit(2); + } + let files = match targets(&args) { + Ok(f) => f, + Err(e) => { + eprintln!("strip-instruction-blocks: {}", e); + process::exit(2); + } + }; let mut changed = 0usize; - walk(Path::new(&root), &mut changed); + for f in &files { + if let Err(e) = process(f, &mut changed) { + // Fail loudly: a file the caller named that cannot be read or + // written is not "clean", it is unchecked. + eprintln!("strip-instruction-blocks: {}", e); + process::exit(1); + } + } if changed > 0 { println!(" instruction blocks: {} file(s) cleaned", changed); } else { diff --git a/scripts/sweep-wellknown.sh b/scripts/sweep-wellknown.sh index 1dc6db7..671898f 100755 --- a/scripts/sweep-wellknown.sh +++ b/scripts/sweep-wellknown.sh @@ -57,7 +57,6 @@ TREES_DIR="" WORK="" BRANCH="chore/well-known-to-www" MIGRATOR="" -SUITE="" usage() { cat <<'EOF' @@ -204,12 +203,12 @@ fi if [ -n "$BATCH" ]; then start=$(( (BATCH - 1) * BATCH_SIZE )) - REPOS=(${REPOS[@]:$start:$BATCH_SIZE}) + REPOS=("${REPOS[@]:$start:$BATCH_SIZE}") [ ${#REPOS[@]} -gt 0 ] || die "batch $BATCH is empty" note "batch $BATCH of size $BATCH_SIZE: ${#REPOS[@]} repository(ies)" fi if [ -n "$LIMIT" ] && [ "$LIMIT" -lt ${#REPOS[@]} ]; then - REPOS=(${REPOS[@]:0:$LIMIT}) + REPOS=("${REPOS[@]:0:$LIMIT}") fi note "selected ${#REPOS[@]} repository(ies) from $OWNER; work dir $WORK" @@ -222,6 +221,10 @@ gh_api() { # path... "$@" } +# Classify OWNER/$1 for migration: Pages/CDN markers (review-pages) take precedence +# over RSR markers (sweep); repositories with neither need review-other. +# API failures yield unknown and truncated trees yield review-other, both with +# success status. root_wk and www_wk indicate .well-known file presence. classify_repo() { # repo -> "class|root_wk|www_wk|notes"; paths cached in $WORK/trees/ local repo="$1" local json paths="" @@ -287,11 +290,11 @@ classify_repo() { # repo -> "class|root_wk|www_wk|notes"; paths cached in $WORK/ local class if [ "$pages" = yes ]; then - class=review-pages + class="review-pages" elif [ "$rsr" = yes ]; then - class=sweep + class="sweep" else - class=review-other + class="review-other" notes="${notes:+$notes; }no RSR marker found" fi [ -n "$marker" ] && notes="${notes:+$notes; }marker: $marker" diff --git a/session/custom-checks.k9.ncl b/session/custom-checks.k9.ncl index 6f6f36b..7f6c01a 100644 --- a/session/custom-checks.k9.ncl +++ b/session/custom-checks.k9.ncl @@ -5,6 +5,7 @@ K9! { pedigree = { schema_version = "1.0.0", + component_type = "session:custom-checks", metadata = { name = "custom-session-checks", version = "0.1.0", diff --git a/tests/ffi_strings_test.sh b/tests/ffi_strings_test.sh new file mode 100644 index 0000000..4879ed1 --- /dev/null +++ b/tests/ffi_strings_test.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Regression tests for the dupeZ compatibility helper and its two callers. +# Instantiate only the FFI fixture, using the repository's zig build test. +# Usage: bash tests/ffi_strings_test.sh (requires Zig 0.15.2+) +set -euo pipefail +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +command -v zig >/dev/null || { echo 'FAIL: Zig is required' >&2; exit 1; } +zig_bin=$(command -v zig) +# Resolve mise shims while still inside the configured repository. +if command -v mise >/dev/null 2>&1; then + zig_bin=$(mise which zig 2>/dev/null || printf '%s' "$zig_bin") +fi +cp -R "$repo/src/interface/ffi" "$work/ffi" +sed -i -e 's/{{project}}/test_project/g' -e 's/{{PROJECT}}/TEST_PROJECT/g' \ + "$work/ffi/build.zig" "$work/ffi/src/main.zig" "$work/ffi/test/integration_test.zig" +# Tests share the production module so the private helper can be exercised +# with the leak-detecting allocator without changing the public FFI surface. +cat >> "$work/ffi/src/main.zig" <<'ZIG' + +test "dupeZ owns a sentinel terminated copy including empty and binary inputs" { + for ([_][]const u8{ "", "abc", "héllo 😀", "a\x00b" }) |input| { + const copy = try dupeZ(std.testing.allocator, input); + defer std.testing.allocator.free(copy); + try std.testing.expectEqualSlices(u8, input, copy); + try std.testing.expectEqual(@as(u8, 0), copy.ptr[copy.len]); + } + var input = [_]u8{ 'a', 'b' }; + const copy = try dupeZ(std.testing.allocator, &input); + defer std.testing.allocator.free(copy); + input[0] = 'z'; + try std.testing.expectEqualStrings("ab", copy); +} + +test "dupeZ propagates allocation failure without leaking" { + var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); + try std.testing.expectError(error.OutOfMemory, dupeZ(failing.allocator(), "data")); +} + +test "get_string returns independent owned C strings and clears an old error" { + const handle = test_project_init() orelse return error.InitFailed; + defer test_project_free(handle); + _ = test_project_process(null, 0); + const first = test_project_get_string(handle) orelse return error.StringFailed; + defer test_project_free_string(first); + const second = test_project_get_string(handle) orelse return error.StringFailed; + defer test_project_free_string(second); + try std.testing.expect(first != second); + try std.testing.expectEqualStrings("Example result", std.mem.span(first)); + try std.testing.expectEqual(@as(u8, 0), first[14]); + try std.testing.expect(test_project_last_error() == null); +} + +test "last_error copies survive subsequent error clearing" { + const handle = test_project_init() orelse return error.InitFailed; + defer test_project_free(handle); + try std.testing.expect(test_project_get_string(null) == null); + const first = test_project_last_error() orelse return error.ExpectedError; + defer test_project_free_string(first); + const second = test_project_last_error() orelse return error.ExpectedError; + defer test_project_free_string(second); + try std.testing.expect(first != second); + try std.testing.expectEqualStrings("Null handle", std.mem.span(first)); + try std.testing.expectEqual(Result.ok, test_project_process(handle, 0)); + try std.testing.expect(test_project_last_error() == null); + try std.testing.expectEqualStrings("Null handle", std.mem.span(second)); +} + +test "get_string reports allocation failure on an initialized handle" { + var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); + var handle = Handle{ .allocator = failing.allocator(), .initialized = true }; + defer clearError(); + try std.testing.expect(test_project_get_string(&handle) == null); + const message = test_project_last_error() orelse return error.ExpectedError; + defer test_project_free_string(message); + try std.testing.expectEqualStrings("Failed to allocate string", std.mem.span(message)); +} + +test "an empty C string still needs allocation for its sentinel" { + var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); + try std.testing.expectError(error.OutOfMemory, dupeZ(failing.allocator(), "")); +} + +test "get_string refuses an uninitialized handle before allocating" { + var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); + var handle = Handle{ .allocator = failing.allocator(), .initialized = false }; + defer clearError(); + try std.testing.expect(test_project_get_string(&handle) == null); + const message = test_project_last_error() orelse return error.ExpectedError; + defer test_project_free_string(message); + try std.testing.expectEqualStrings("Handle not initialized", std.mem.span(message)); + try std.testing.expect(!failing.has_induced_failure); +} + +test "freeing one error copy preserves the stored error and other copies" { + defer clearError(); + try std.testing.expect(test_project_get_string(null) == null); + const first = test_project_last_error() orelse return error.ExpectedError; + test_project_free_string(first); + const second = test_project_last_error() orelse return error.ExpectedError; + defer test_project_free_string(second); + try std.testing.expectEqualStrings("Null handle", std.mem.span(second)); + test_project_free_string(null); + const third = test_project_last_error() orelse return error.ExpectedError; + defer test_project_free_string(third); + try std.testing.expectEqualStrings("Null handle", std.mem.span(third)); +} +ZIG +(cd "$work/ffi" && "$zig_bin" build test) diff --git a/tests/proven_suite_test.sh b/tests/proven_suite_test.sh new file mode 100755 index 0000000..b3317d8 --- /dev/null +++ b/tests/proven_suite_test.sh @@ -0,0 +1,452 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Silence + Firing + Setup-error test suite for the proven-tests-and-benches +# toolchain wired in scripts/: +# - scripts/check-escape-hatches.sh +# - scripts/compute-bench-baseline.sh +# - scripts/check-bench-regression.sh +# - scripts/emit-aggregate-inputs.sh +# +# Every negative fixture is generated ephemerally in $WORK (mktemp -d) so no +# static escape-hatch token ever resides in a tracked .idr file. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +pass=0 +fail=0 + +assert_rc() { # assert_rc