From a75df6921510ab4e96364e770839be7f62bfd996 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 26 May 2026 12:25:22 +0100 Subject: [PATCH 01/10] chore(fixtures): delete stale SafeDOMExample.res ReScript fixture (Refs gitbot-fleet#148, #208; affinescript#229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes 1 copies of the stale SafeDOMExample.res ReScript fixture from this repo. One of 1,267 byte-clustered copies across the estate (129 repos). ReScript is fully banned in new code (2026-04-30 policy refresh). Current-grammar AffineScript replacement lives at gitbot-fleet/bots/*/examples/SafeDOMExample.affine (PR gitbot-fleet#210 MERGED). The example fixtures are not load-bearing — propagated from an earlier template-instantiation script. Refs hyperpolymath/gitbot-fleet#148, #208 Refs hyperpolymath/affinescript#57, #229 Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/SafeDOMExample.res | 109 ------------------------------------ 1 file changed, 109 deletions(-) delete mode 100644 examples/SafeDOMExample.res diff --git a/examples/SafeDOMExample.res b/examples/SafeDOMExample.res deleted file mode 100644 index e5c9046..0000000 --- a/examples/SafeDOMExample.res +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Example: Using SafeDOM for formally verified DOM mounting - -open SafeDOM - -// Example 1: Basic mounting with error handling -let mountApp = () => { - mountSafe( - "#app", - "

Hello, World!

Mounted safely with proofs.

", - ~onSuccess=el => { - Console.log("✓ App mounted successfully!") - Console.log("Element:", el) - }, - ~onError=err => { - Console.error("✗ Mount failed:", err) - } - ) -} - -// Example 2: Wait for DOM ready before mounting -let mountWhenDOMReady = () => { - mountWhenReady( - "#app", - "

App Title

", - ~onSuccess=_ => Console.log("✓ Mounted after DOM ready"), - ~onError=err => Console.error("✗ Failed:", err) - ) -} - -// Example 3: Batch mounting (atomic - all or nothing) -let mountMultiple = () => { - let specs = [ - {selector: "#header", html: "

Site Title

"}, - {selector: "#nav", html: ""}, - {selector: "#main", html: "

Content here

"}, - {selector: "#footer", html: ""} - ] - - switch mountBatch(specs) { - | Ok(elements) => { - Console.log(`✓ Successfully mounted ${Array.length(elements)} elements`) - elements->Array.forEach(el => Console.log(" -", el)) - } - | Error(err) => { - Console.error("✗ Batch mount failed:", err) - Console.error(" (None were mounted - atomic operation)") - } - } -} - -// Example 4: Explicit validation before mounting -let mountWithValidation = () => { - // Validate selector first - switch ProvenSelector.validate("#my-app") { - | Error(e) => Console.error(`Invalid selector: ${e}`) - | Ok(validSelector) => { - // Validate HTML - switch ProvenHTML.validate("
Content
") { - | Error(e) => Console.error(`Invalid HTML: ${e}`) - | Ok(validHtml) => { - // Now mount with proven safety - switch mount(validSelector, validHtml) { - | Mounted(el) => Console.log("✓ Mounted with validated inputs:", el) - | MountPointNotFound(s) => Console.error(`✗ Element not found: ${s}`) - | InvalidSelector(_) => Console.error("Impossible - already validated") - | InvalidHTML(_) => Console.error("Impossible - already validated") - } - } - } - } -} - -// Example 5: Integration with TEA -module MyApp = { - type model = {message: string} - type msg = NoOp - - let init = () => {message: "Hello from TEA"} - let update = (model, _msg) => model - let view = model => `

${model.message}

` -} - -let mountTEAApp = () => { - let model = MyApp.init() - let html = MyApp.view(model) - - mountWhenReady( - "#tea-app", - html, - ~onSuccess=el => { - Console.log("✓ TEA app mounted") - // Set up event handlers, subscriptions here - }, - ~onError=err => Console.error(`✗ TEA mount failed: ${err}`) - ) -} - -// Entry point -let main = () => { - Console.log("SafeDOM Examples") - Console.log("================\n") - - // Choose which example to run - mountWhenDOMReady() // Run on DOM ready -} - -// Auto-execute when module loads -main() From 758e2ff564afd0a0c63a1015abdcbbfb8784fdb0 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 26 May 2026 12:52:40 +0100 Subject: [PATCH 02/10] docs: record tech-debt audit findings (2026-05-26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docs/tech-debt-2026-05-26.md with this repo's findings from the estate-wide tech-debt scan: proof debt, licence debt, documentation debt. This file records the findings only — it does not close the debt. Cross-references: - hyperpolymath/standards#195 (estate proof-debt audit) - hyperpolymath/standards#196 (estate licence-debt audit) - hyperpolymath/standards#197 (estate documentation-debt audit) Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/tech-debt-2026-05-26.md | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/tech-debt-2026-05-26.md diff --git a/docs/tech-debt-2026-05-26.md b/docs/tech-debt-2026-05-26.md new file mode 100644 index 0000000..6da7bc3 --- /dev/null +++ b/docs/tech-debt-2026-05-26.md @@ -0,0 +1,57 @@ + + +# Tech-Debt Audit — conative-gating — 2026-05-26 + +**Source:** estate-wide automated scan 2026-05-26. +**Companion:** [`hyperpolymath/standards` 2026-05-26-estate-*-debt audits](https://github.com/hyperpolymath/standards/tree/main/docs/audits). +**Combined severity:** `LOW`. + +This file records the *raw findings* — it does not by itself fix the debt. Each section ends with a 'Recommended next move' line; closing the debt is follow-up work. + +## 1. Proof debt + +No proof-bearing files (`*.v`, `*.lean`, `*.agda`, `*.idr`, `*.idr2`, `*.fst`, `*.dfy`, `*.tla`, `*.ads`, `*.adb`) found in this repo. + +**Recommended next move:** none. + +## 2. Licence debt + +| Field | Value | +|---|---| +| LICENSE file | `LICENSE` | +| SPDX header | `MPL-2.0` | +| Manifest licence | `MPL-2.0` | +| Body classifier | `Palimp-MPL-2.0` | +| Severity | `ok` | + +**Recommended next move:** none for licence. + +## 3. Documentation debt + +| Field | Value | +|---|---| +| README lines | 329 | +| `docs/` files | 14 | +| `docs/` LoC | 5903 | +| CHANGELOG.md | N | +| CONTRIBUTING.md | Y | +| CODE_OF_CONDUCT.md | Y | +| SECURITY.md | Y | +| Severity | `LOW` | + +**Recommended next move:** `docs/` has only 14 file(s). Aim for ≥10 organised docs (architecture, usage, contributing-guide, troubleshooting, design-decisions). The user's bar for a "heavily-developed and well-organised wiki" is ≥10 files with topical organisation. + +Additionally: **CHANGELOG.md is missing.** 65% of estate repos lack one — adopting a CHANGELOG (or auto-generating via `git-cliff`) is a recommended estate-wide follow-up. + +## Cross-references + +- Estate proof-debt audit: `hyperpolymath/standards/docs/audits/2026-05-26-estate-proof-debt.md` +- Estate licence-debt audit: `hyperpolymath/standards/docs/audits/2026-05-26-estate-licence-debt.md` +- Estate documentation-debt audit: `hyperpolymath/standards/docs/audits/2026-05-26-estate-documentation-debt.md` + +--- + +🤖 Generated by Claude Code estate-wide tech-debt scan (2026-05-26). This file is informational — closing the debt is follow-up work owned by the maintainer. From ec6679d3a18ca89092144667ebbcba819d545598 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 26 May 2026 18:40:58 +0100 Subject: [PATCH 03/10] docs: seed CHANGELOG.md (Keep-a-Changelog format) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes Row-2 Phase 3 of the 2026-05-26 estate tech-debt audit chain for this repo. The 2026-05-26 documentation-debt audit (hyperpolymath/standards#197) flagged that 180 of 279 estate repos lacked a CHANGELOG.md (65% gap) — this seed closes that finding here. The seed: - Uses Keep-a-Changelog format with an [Unreleased] section. - Buckets the most recent 100 commits by conventional-commit prefix (feat/fix/refactor/docs/ci/build) into Added/Fixed/Changed/ Documentation/CI sections. - References standards#206's changelog-reusable.yml + the canonical templates/cliff.toml for full-regeneration via git-cliff. The file is initial — the maintainer can adopt changelog-reusable.yml in this repo's CI to keep it auto-regenerated, or regenerate manually. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e5d6df4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,64 @@ + + +# Changelog + +All notable changes to `conative-gating` will be documented in this file. + +This file is generated from conventional commits by the +[`changelog-reusable.yml`](https://github.com/hyperpolymath/standards/blob/main/.github/workflows/changelog-reusable.yml) +workflow (`hyperpolymath/standards#206`). Adopt the workflow in this repo's CI to keep this file in sync automatically — see +[`templates/cliff.toml`](https://github.com/hyperpolymath/standards/blob/main/templates/cliff.toml) +for the canonical config. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); +this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- feat(crg): add crg-grade and crg-badge justfile recipes +- feat: add stapeln.toml container definition +- feat: deploy UX Manifesto infrastructure +- feat: add CLADE.a2ml — clade taxonomy declaration + +### Fixed + +- fix(ci): bump a2ml/k9-validate-action pins to canonical (#50) +- fix(ci): sync hypatia-scan.yml to canonical (#49) +- fix(ci): adopt canonical hypatia-scan.yml (#47) +- fix: remove stale banned gen-v-connector V-lang recipe (#46) +- fix(ci): Phase-2 fleet submission must not fail the security gate (#45) +- fix(ci): hypatia-scan workdir (${{ env.HOME }} resolves empty) (#44) +- fix(ci): hypatia-scan.yml -- --exit-zero + GITHUB_TOKEN (hyperpolymath/hypatia#213) (#39) +- fix(ci): rsr-antipattern duplicate heredoc + setup-beam ubuntu24 (#40) +- fix(ci): repair YAML block-scalar in workflow-linter Check Permissions step (#41) +- fix(ci): move secret-scanner Cargo.toml gate from job-level if: to step-level (#42) + +### Changed + +- refactor: migrate 6SCM → 6A2 (.scm → .a2ml format) + +### Documentation + +- docs: substantive CRG C annotation (EXPLAINME.adoc) +- docs: add EXPLAINME.adoc — prove-it file backing README claims + +### CI + +- ci(rust): convert rust-ci.yml to thin wrapper (standards#174) (#54) +- ci: redistribute concurrency-cancel guard to read-only check workflows (#52) +- ci(secret-scanner): drop duplicate --fail from trufflehog extra_args (#38) +- ci: SHA-pin hyperpolymath validate-actions in dogfood-gate +- ci(antipattern): fix top-level dir + benchmark/lsp filename matching (#37) + +## Pre-history + +Prior commits to this file's introduction are recorded in git history but not formally classified into Keep-a-Changelog sections. To backfill, run `git cliff -o CHANGELOG.md` locally using the canonical [`cliff.toml`](https://github.com/hyperpolymath/standards/blob/main/templates/cliff.toml) — this is one-shot mechanical work. + +--- + + From 6e1956f7305a2539e9cb77d7dbaa1078e357b94f Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:56:12 +0100 Subject: [PATCH 04/10] fix(ci): drop the beta-Rust CodeQL leg, keep an actions leg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL Rust support is beta — that matrix leg cancels/hangs and never resolves (the recurring "CodeQL checks 2 things forever" symptom), giving zero real coverage. Removed it; ensured an actions leg remains so the matrix is non-empty (no zero-jobs startup_failure). Rust security belongs in cargo-audit/clippy. Verified with actionlint: no parse/syntax errors; actions leg present, no active rust leg. Part of the estate-wide CI cleanup (13 repos; reference hyperpolymath/gitbot-fleet#375). Co-Authored-By: Claude Fable 5 --- .github/workflows/codeql.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index dc5d46b..7653eea 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,8 +27,6 @@ jobs: fail-fast: false matrix: include: - - language: rust - build-mode: none - language: actions build-mode: none steps: From 7bdb6883ae552a8ef82d90caffec5c42dfe8a784 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:30:43 +0100 Subject: [PATCH 05/10] ci: vendor validation scripts and remove remote action pins --- .githooks/validate-a2ml.sh | 350 ++++++++++++++++++++++++++++ .githooks/validate-k9.sh | 357 +++++++++++++++++++++++++++++ .github/workflows/dogfood-gate.yml | 10 +- 3 files changed, 709 insertions(+), 8 deletions(-) create mode 100755 .githooks/validate-a2ml.sh create mode 100755 .githooks/validate-k9.sh diff --git a/.githooks/validate-a2ml.sh b/.githooks/validate-a2ml.sh new file mode 100755 index 0000000..b053676 --- /dev/null +++ b/.githooks/validate-a2ml.sh @@ -0,0 +1,350 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# validate-a2ml.sh — A2ML manifest validation script +# +# Scans for .a2ml files and validates: +# 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) +# +# Environment variables: +# INPUT_PATH — Directory to scan (default: .) +# INPUT_STRICT — Promote warnings to errors (default: false) +# +# Exit codes: +# 0 — All files valid (or only warnings in non-strict mode) +# 1 — Validation errors found + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +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 + +# --------------------------------------------------------------------------- +# 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 .a2ml file +# --------------------------------------------------------------------------- +validate_a2ml() { + 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 --- + # A2ML files must contain either: + # - agent-id = "..." or agent_id = "..." + # - pedigree block with name field + # - name = "..." at top level (for AI manifests) + # - project = "..." (for STATE.a2ml) + local has_identity=false + local has_version=false + line_num=0 + + while IFS= read -r line; do + line_num=$((line_num + 1)) + + # Check for identity fields (various A2ML patterns) + # TOML/kv form: `name = "..."`, `project = "..."`, `agent-id = "..."` + if [[ "$line" =~ ^[[:space:]]*(agent[-_]id|name|project)[[:space:]]*= ]]; then + has_identity=true + fi + # S-expression form: `(name "...")`, `(project "...")`, + # `(agent-id "...")`. Some A2ML 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 A2ML 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 + # 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 + done < "$file" + + # AI manifest files (0-AI-MANIFEST.a2ml, 0.1-AI-MANIFEST.a2ml, etc.) + # use markdown-style headers and free text, so identity check is relaxed + local basename + basename="$(basename "$file")" + local is_manifest=false + if [[ "$basename" == *"AI-MANIFEST"* ]]; then + is_manifest=true + fi + # Canonical typed manifests under .machine_readable/descriptiles/ — identity comes + # from the enclosing directory + filename, not an in-file field. Sibling + # files in the same directory (ECOSYSTEM.a2ml, STATE.a2ml) DO carry their + # own $name/project and continue to be validated normally. + case "$basename" in + AGENTIC.a2ml|META.a2ml|NEUROSYM.a2ml|PLAYBOOK.a2ml|AI.a2ml) + # AI.a2ml = free-text "AI Assistant Instructions" manifest, the same + # doc type as 0-AI-MANIFEST.a2ml but with the bare name; identity is + # carried by the enclosing repo/plugin dir, not an in-file field. + is_manifest=true + ;; + # Dockerfile-style top-level typed manifests (Intentfile, Trustfile, …) + # use markdown-flavoured A2ML; identity is carried by the parent repo. + *file.a2ml) + is_manifest=true + ;; + esac + + # Contractile-shape A2ML files use `@directive:` syntax instead of + # TOML `key = value`. Trustfile.a2ml, Intentfile.a2ml, Mustfile.a2ml, + # Adjustfile.a2ml etc. are policy / trust / intent / abstract files + # whose identity is implicit in their @-prefixed directives + # (`@trust-level`, `@intent`, ...) rather than a TOML name/version + # pair. Treating them as manifest-shape produces 100% false positives — + # they're a different A2ML doc type. Detected by the presence of any + # contractile directive in the file body. + local is_contractile_shape=false + if grep -qE '^@(abstract|trust-level|trust-boundary|trust-actions|trust-deny|intent|must|adjust|end)([[:space:]]*:|$)' "$file"; then + is_contractile_shape=true + fi + + # Canonical structured A2ML 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: rsr-template-repo 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_manifest" == "false" && "$is_contractile_shape" == "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_manifest" == "false" && "$is_contractile_shape" == "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: discover and validate .a2ml files +# --------------------------------------------------------------------------- + +echo "::group::A2ML Manifest Validation" +echo "Scanning ${SCAN_PATH} for .a2ml files..." +echo "" + +# Find all .a2ml files, excluding .git directory +mapfile -t a2ml_candidates < <(find "$SCAN_PATH" -name '*.a2ml' -not -path '*/.git/*' -type f | sort) + +# Apply paths-ignore filter +a2ml_files=() +SKIPPED=0 +for _f in "${a2ml_candidates[@]}"; do + if path_ignored "$_f"; then + SKIPPED=$((SKIPPED + 1)) + continue + fi + a2ml_files+=("$_f") +done + +if [[ $SKIPPED -gt 0 ]]; then + echo "::notice::Skipped ${SKIPPED} file(s) matching paths-ignore" +fi + +if [[ ${#a2ml_files[@]} -eq 0 ]]; then + echo "::notice::No .a2ml 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::" + exit 0 +fi + +echo "Found ${#a2ml_files[@]} .a2ml file(s)" +echo "" + +for file in "${a2ml_files[@]}"; do + echo " Validating: ${file}" + validate_a2ml "$file" +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::A2ML validation failed with ${ERRORS} error(s)" + exit 1 +fi + +echo "A2ML validation passed." +exit 0 diff --git a/.githooks/validate-k9.sh b/.githooks/validate-k9.sh new file mode 100755 index 0000000..c83e290 --- /dev/null +++ b/.githooks/validate-k9.sh @@ -0,0 +1,357 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# validate-k9.sh — K9 configuration file validation script +# +# 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 +# +# Environment variables: +# INPUT_PATH — Directory to scan (default: .) +# INPUT_STRICT — Promote warnings to errors (default: false) +# +# Exit codes: +# 0 — All files valid (or only warnings in non-strict mode) +# 1 — Validation errors found + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +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 + + 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 + + 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 [[ $SKIPPED -gt 0 ]]; then + echo "::notice::Skipped ${SKIPPED} file(s) matching paths-ignore" +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::" + exit 0 +fi + +echo "Found ${#k9_files[@]} K9 file(s)" +echo "" + +for file in "${k9_files[@]}"; do + echo " Validating: ${file}" + validate_k9 "$file" +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 validation passed." +exit 0 diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index c0e1f8e..8e08387 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -33,10 +33,7 @@ jobs: fi - name: Validate A2ML manifests if: steps.detect.outputs.count > 0 - uses: hyperpolymath/a2ml-ecosystem/validate-action@aa4b836bd969df2bc58128cb8e3d20bbc88d5e79 # main - with: - path: '.' - strict: 'false' + run: bash .githooks/validate-a2ml.sh - name: Write summary run: | A2ML_COUNT="${{ steps.detect.outputs.count }}" @@ -77,10 +74,7 @@ jobs: fi - name: Validate K9 contracts if: steps.detect.outputs.k9_count > 0 - uses: hyperpolymath/k9-ecosystem/validate-action@89f3c2702f4f650a92aa7411502f38da06abd562 # main - with: - path: '.' - strict: 'false' + run: bash .githooks/validate-k9.sh - name: Write summary run: | K9_COUNT="${{ steps.detect.outputs.k9_count }}" From c5aa0da4d6d62a6d7c712d3eb1df446673596122 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:52:57 +0100 Subject: [PATCH 06/10] fix(ci): update secret-scanner SHA to @7fdc2705df74b4e352d2a1cde3e87a5923fdf329 + simplify permissions + remove squisher-corpus placeholders --- .github/workflows/secret-scanner.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 7bd0955..6a2e53a 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -13,7 +13,5 @@ jobs: scan: permissions: contents: read - pull-requests: write - actions: read - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@d135b05bfc647d0c0fbfedc7e80f37ea50f49236 + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 secrets: inherit From d9d991336223d1b6462de2cafe585535d7f1c666 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:33:48 +0100 Subject: [PATCH 07/10] fix(ci): update reusable workflow SHAs to @7fdc2705df74b4e352d2a1cde3e87a5923fdf329 Part of estate-wide standards#426 remediation - Batch 11 SHA update. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .github/workflows/mirror.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 6bd847d..11ae526 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -8,5 +8,5 @@ permissions: contents: read jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@d135b05bfc647d0c0fbfedc7e80f37ea50f49236 + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 secrets: inherit From b8499fd6c9f612b086ceefbf7bb0fde5e4d58fe2 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:57:08 +0100 Subject: [PATCH 08/10] fix(ci): update reusable workflow SHAs to @7fdc2705df74b4e352d2a1cde3e87a5923fdf329 Part of estate-wide standards#426 remediation - Batch 13 SHA update. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 8776de0..884848a 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -13,4 +13,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 \ No newline at end of file + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 \ No newline at end of file diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 9dde27a..d4fbdeb 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -16,4 +16,4 @@ permissions: jobs: scan: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 \ No newline at end of file + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 \ No newline at end of file diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b97e2cb..d0c06e2 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -13,7 +13,7 @@ permissions: jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 permissions: contents: read security-events: write From e5b6f7092b6067b892fdf49bd11e42ce3a75da15 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:17:39 +0100 Subject: [PATCH 09/10] fix(ci): add required permissions for reusable workflows (Bug B) Add security-events: write and id-token: write to workflow-level permissions in scorecard.yml for scorecard-reusable.yml calls. Ensure contents: read at workflow-level for secret-scanner.yml. Part of hyperpolymath/standards#426 remediation - Batch 2. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .github/workflows/scorecard.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index d0c06e2..3449a81 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -10,7 +10,8 @@ on: permissions: contents: read - + security-events: write + id-token: write jobs: scorecard: uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 From c49cd0ae4dadc1ae102671f401c6c7dfed7ced97 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:00:11 +0100 Subject: [PATCH 10/10] chore(toolchain): keep .tool-versions -> .mise.toml pin conversion (R-16) Owner ruling 2026-08-28 (R-16/R-20/R-21): keep the pin conversion from the template-sync sweep, revert the rest. Pin content verified against HEAD:.tool-versions before commit. Co-Authored-By: Claude Fable 5 --- .mise.toml | 2 ++ .tool-versions | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .mise.toml delete mode 100644 .tool-versions diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..3156a04 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +rust = "stable" diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 3cd07d4..0000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -rust stable