diff --git a/.clinerules b/.clinerules index b3b9ab7..41ee124 100644 --- a/.clinerules +++ b/.clinerules @@ -1,19 +1,20 @@ # SPDX-License-Identifier: MPL-2.0 -# Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # Authoritative source: docs/practice/AI-CONVENTIONS.adoc -# STARTUP: Read the repo deed (*_chora.deed at the repo root) first, then .machine_readable/descriptiles/STATE.a2ml. +# STARTUP: Read the repo deed (*_chora.deed at the repo root) first, then docs/status/ROADMAP.adoc. # LICENSE # All original code: MPL-2.0. # Never AGPL-3.0. MPL-2.0 only as platform-required fallback. # SPDX header required on every source file. -# Copyright: {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +# Copyright: Jonathan D.A. Jewell (hyperpolymath) -# STATE FILES (.machine_readable/ ONLY) -# Never create in repo root: STATE.a2ml, META.a2ml, ECOSYSTEM.a2ml, -# AGENTIC.a2ml, NEUROSYM.a2ml, PLAYBOOK.a2ml. -# The .machine_readable/ directory is the single source of truth. +# STATE FILES +# No .a2ml file may exist anywhere: the format is retired. State, metadata, +# (neurosym) and (playbook) are clauses of the repo deed (*_chora.deed); +# the journal is docs/status/ROADMAP.adoc. Never recreate them as files. +# The repo deed and .machine_readable/ are the single source of truth. # BANNED PATTERNS # Idris2: believe_me, assert_total, assert_smaller, unsafePerformIO @@ -23,15 +24,15 @@ # Lean: sorry # Rust: transmute (unless FFI with // SAFETY: comment) -# JS/TS RUNTIMES — ordered preference, reach for the first that can do the job -# (standards/3-practice/LANGUAGE-POLICY.adoc section 1, ruled 2026-07-29) -# 1. Bun default for all new work; runs .ts directly, no build step -# 2. Deno existing Deno projects are grandfathered; prefer over pnpm/npm -# 3. pnpm only where an upstream toolchain needs a node_modules layout -# 4. npm last resort; permitted, never preferred — a noted decision -# TypeScript IS PERMITTED under Bun. The old "use ReScript instead" rule is -# RETIRED: ReScript is no longer used in this estate, so that rule named a dead -# alternative. Do NOT migrate Bun to Deno — that inverts the current ruling. +# JS RUNTIME — Bun first (standards/3-practice/LANGUAGE-POLICY.adoc sections 1.3-1.4) +# 1. Bun tier 1: package.json + bun.lock, run with bun / bunx +# 2. Deno tier 2: permitted; runs the same package.json scripts (deno task) +# 3. pnpm tier 3 +# 4. npm last resort; permitted, never preferred; keeps no lock +# One package.json carries every runtime's lock (bun.lock, deno.lock, +# pnpm-lock.yaml); `js-runtimes regen` writes all three (owner ruling +# 2026-10-14). TypeScript and ReScript are banned; they migrate to +# AffineScript. Use plain JavaScript where JS is needed. # BANNED LANGUAGES # Go -> Rust diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..6342c2c --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MPL-2.0 +# CodeRabbit reviews changes; humans and authorised agents own changes. +# Canonical estate language policy: +# https://github.com/metadatastician/standards/blob/main/3-practice/LANGUAGE-POLICY.adoc +# This template must never permit unsolicited AI-generated fixes/commits. +reviews: + path_instructions: + - path: "**/*" + instructions: >- + Enforce the canonical estate language policy: Python is prohibited + for all NEW work, without exceptions. Do not suggest Python + implementations or tests, and flag newly added/modified Python + files or dependency manifests as policy violations. Use the owning + project's approved language and runtime. Pre-existing third-party + code is not permission to author new Python. Do not demand language + migrations of unrelated upstream dependencies as cosmetic PR work. + pre_merge_checks: + docstrings: + mode: warning + threshold: 80 + finishing_touches: + docstrings: + enabled: false + unit_tests: + enabled: false + simplify: + enabled: false + autofix: + enabled: false + fix_ci: + enabled: false + resolve_merge_conflict: + enabled: false diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 2357fd3..0000000 --- a/.cursorrules +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -# Authoritative source: docs/practice/AI-CONVENTIONS.adoc - -# Read the repo deed (*_chora.deed in the repo root) FIRST: canonical file locations -# live in its (ply ...) canonical-locations clauses. - -# LICENSE -# All original code: MPL-2.0 (SPDX header required on every file). -# Copyright: {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> - -# STATE FILES -# .deed metadata files go in .machine_readable/ ONLY. -# Never create STATE.a2ml, META.a2ml, ECOSYSTEM.a2ml, AGENTIC.a2ml, NEUROSYM.a2ml, or PLAYBOOK.a2ml. - -# BANNED PATTERNS -# Idris2: believe_me, assert_total, assert_smaller, unsafePerformIO -# Haskell: unsafeCoerce, unsafePerformIO, undefined, error -# OCaml: Obj.magic, Obj.repr, Obj.obj -# Coq: Admitted -# Lean: sorry -# Rust: transmute (unless FFI with // SAFETY: comment) - -# JS RUNTIMES — ordered preference, reach for the first that can do the job -# (standards/3-practice/LANGUAGE-POLICY.adoc section 1, ruled 2026-07-29) -# 1. Bun default for all new work; runs .ts directly, no build step -# 2. Deno existing Deno projects are grandfathered; prefer over pnpm/npm -# 3. pnpm only where an upstream toolchain needs a node_modules layout -# 4. npm last resort; permitted, never preferred — a noted decision -# TypeScript IS PERMITTED under Bun. The old "use ReScript instead" rule is -# RETIRED: ReScript is no longer used in this estate, so that rule named a dead -# alternative. Do NOT migrate Bun to Deno — that inverts the current ruling. - -# BANNED LANGUAGES -# Go -> use Rust -# Python -> use Julia or Rust - -# CONTAINERS -# Runtime: Podman (never Docker) -# File: Containerfile (never Dockerfile) -# Base: cgr.dev/chainguard/wolfi-base:latest - -# ABI/FFI STANDARD -# ABI definitions: Idris2 with dependent types (src/interface/abi/) -# FFI implementation: Zig with C ABI (src/interface/ffi/) -# Generated C headers: src/interface/generated/ - -# BUILD SYSTEM -# Use just (Justfile) for all build, test, lint, and format tasks. - -# CODE STYLE -# Use descriptive variable names. -# Annotate and document all files. -# Add SPDX-License-Identifier headers to every source file. diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 24f9f41..5126e69 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,11 +1,15 @@ # SPDX-License-Identifier: MPL-2.0 -# Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # -# Dev Container image for {{PROJECT_NAME}} +# Dev Container image for occupancy-types # Base: Chainguard Wolfi (minimal, supply-chain-secure) -# Build: podman build -t {{PROJECT_NAME}}-dev -f .devcontainer/Containerfile . +# Build: podman build -t occupancy-types-dev -f .devcontainer/Containerfile . # `:latest` suits a dev container (rebuilt daily); pin by digest # (...@sha256:) if you need a reproducible dev environment. +# +# Every tool comes from Wolfi or from a sha256-checked release binary, so the +# image builds with no dev-container features (registry-hosted features can +# vanish or turn private, and then nobody can start the container). FROM cgr.dev/chainguard/wolfi-base:latest @@ -14,19 +18,33 @@ RUN apk update && apk add --no-cache \ bash \ curl \ git \ + just \ openssh-client \ ca-certificates \ build-base \ posix-libc-utils \ - shadow \ && rm -rf /var/cache/apk/* -# Create non-root dev user (matches devcontainer.json remoteUser) -RUN groupadd -g 1000 nonroot || true \ - && useradd -m -u 1000 -g 1000 -s /bin/bash nonroot || true +# Nickel (the .machine_readable contracts): the same verified release CI uses +# (.github/workflows/estate-rules.yml). Bump the version and both digests together. +ARG NICKEL_VERSION=1.17.0 +RUN set -eu; \ + case "$(uname -m)" in \ + x86_64) asset=nickel-x86_64-linux; sum=afcdfa6e0fff31760cf229e85997456c02c00b8b3b84ff38f897ac7b3f39ae34 ;; \ + aarch64) asset=nickel-arm64-linux; sum=9d7e027fde829b5788c92231c27fcfc926ee841d0716d655cee202c9f582e0c0 ;; \ + *) echo "no Nickel release binary for $(uname -m)" >&2; exit 1 ;; \ + esac; \ + curl --proto '=https' --proto-redir '=https' -fsSL \ + "https://github.com/nickel-lang/nickel/releases/download/${NICKEL_VERSION}/${asset}" \ + -o /usr/local/bin/nickel; \ + echo "${sum} /usr/local/bin/nickel" | sha256sum -c -; \ + chmod 0755 /usr/local/bin/nickel + +# Dev user: wolfi-base already ships `nonroot` (uid/gid 65532, home /home/nonroot); +# devcontainer.json maps your host user onto it with --userns=keep-id. # Set workspace directory -WORKDIR /workspaces/{{PROJECT_NAME}} +WORKDIR /workspaces/occupancy-types # Default shell ENV SHELL=/bin/bash diff --git a/.devcontainer/README.adoc b/.devcontainer/README.adoc index 5eee18b..e950cc2 100644 --- a/.devcontainer/README.adoc +++ b/.devcontainer/README.adoc @@ -1,28 +1,61 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell = Dev Container Usage -:author: {{AUTHOR}} <{{AUTHOR_EMAIL}}> +:author: Jonathan D.A. Jewell == Overview -This dev container uses `cgr.dev/chainguard/wolfi-base` with git, curl, bash, and just pre-installed. Dev container features add git, just, and nickel automatically. +The image (`Containerfile`) is `cgr.dev/chainguard/wolfi-base` with bash, curl, +git, just, openssh-client and a C toolchain from Wolfi packages, plus the Nickel +release binary (SHA-256 checked). It uses no dev container *features*: the +feature images are pulled from ghcr.io and can fail anonymously, which would stop +anyone from opening the container. -== VS Code (Local) +You work as `nonroot`, the user built into wolfi-base (uid/gid 65532). + +There are two configurations for the same image: + +[cols="1,2,3"] +|=== +| Config | Engine | Why + +| `devcontainer.json` +| rootless Podman (estate default) +| Maps your host user onto `nonroot` with `--userns=keep-id:uid=65532,gid=65532`, + so the bind-mounted workspace is writable. `--security-opt=label=disable` + lets the mount work on SELinux-enforcing hosts. + +| `docker/devcontainer.json` +| Docker, GitHub Codespaces, Gitpod +| Docker rejects `--userns=keep-id`, so this one has no `runArgs`. +|=== + +== VS Code (local, Podman) . Install the *Dev Containers* extension (`ms-vscode-remote.remote-containers`). . Set `dev.containers.dockerPath` to `podman` in VS Code settings. -. Open the repo folder, then choose **Reopen in Container** from the command palette. +. Open the repo folder, then choose **Reopen in Container** and pick + **occupancy-types (Podman)**. + +== devcontainer CLI (Podman) + +[source,sh] +---- +bunx @devcontainers/cli up --workspace-folder . --docker-path podman +---- == GitHub Codespaces -. From the repository on GitHub, click **Code > Codespaces > New codespace**. -. The container builds automatically from this configuration. +. From the repository on GitHub, click **Code > Codespaces > ... > New with options**. +. Choose the **occupancy-types (Docker / Codespaces)** configuration. == Gitpod . Prefix the repo URL with `https://gitpod.io/#` to launch a workspace. -. Gitpod reads `devcontainer.json` and builds the environment. +. Point Gitpod at `.devcontainer/docker/devcontainer.json`. == Customization -Replace `{{PROJECT_NAME}}` placeholders in both `devcontainer.json` and `Containerfile` with your actual project name. Run `just deps` to verify the environment after first launch. +Replace the `occupancy-types` placeholders in both `devcontainer.json` files and +the `Containerfile` with your actual project name. `just deps` runs automatically +after the container is created. Run it again to check the environment. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a4b33e0..0871fbf 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,35 +1,22 @@ // SPDX-License-Identifier: MPL-2.0 -// Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) // -// Dev Container configuration for {{PROJECT_NAME}} -// Works with: VS Code Dev Containers, GitHub Codespaces, Gitpod -// Container runtime: Podman (recommended) or any OCI-compliant runtime +// Dev Container configuration for occupancy-types — rootless Podman (default). +// Works with: VS Code Dev Containers with `dev.containers.dockerPath` = podman, +// and the devcontainer CLI (`devcontainer up --docker-path podman`). +// Docker, GitHub Codespaces and Gitpod: use .devcontainer/docker/devcontainer.json +// instead — Docker rejects the Podman-only `--userns=keep-id` below. { - "name": "{{PROJECT_NAME}}", - "build": { - "dockerfile": "Containerfile", - "context": ".." + "context": "..", + "dockerfile": "Containerfile" }, - - "features": { - "ghcr.io/devcontainers/features/git:1": { - "ppa": false, - "version": "latest" - }, - "ghcr.io/jdx/devcontainer-features/just:1": {}, - "ghcr.io/nickel-lang/devcontainer-feature:0": {} - }, - - "postCreateCommand": "just deps", - - "remoteUser": "nonroot", - "containerEnv": { "EDITOR": "code --wait", + "HOME": "/home/nonroot", "LANG": "C.UTF-8" }, - + "containerUser": "nonroot", "customizations": { "vscode": { "extensions": [ @@ -45,25 +32,29 @@ "usernamehw.errorlens" ], "settings": { + "[makefile]": { + "editor.insertSpaces": false + }, "editor.formatOnSave": true, "editor.insertSpaces": true, "editor.tabSize": 2, - "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "files.trimFinalNewlines": true, - "[makefile]": { - "editor.insertSpaces": false - } + "files.trimTrailingWhitespace": true } - }, - "codespaces": { - "openFiles": [ - "README.adoc" - ] } }, - - "forwardPorts": [], - - "shutdownAction": "stopContainer" + "name": "occupancy-types (Podman)", + "postCreateCommand": "just deps", + "remoteUser": "nonroot", + // Rootless Podman maps your host user to root inside the container, so the + // bind-mounted workspace would be owned by root and unwritable for `nonroot`. + // keep-id maps your host user to uid/gid 65532 (`nonroot`, built into wolfi-base) instead. + // label=disable lets the workspace mount work on SELinux-enforcing hosts. + "runArgs": [ + "--userns=keep-id:uid=65532,gid=65532", + "--security-opt=label=disable" + ], + "shutdownAction": "stopContainer", + "updateRemoteUserUID": false } diff --git a/.devcontainer/docker/devcontainer.json b/.devcontainer/docker/devcontainer.json new file mode 100644 index 0000000..cca349d --- /dev/null +++ b/.devcontainer/docker/devcontainer.json @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +// +// Dev Container configuration for occupancy-types — Docker engines. +// Works with: GitHub Codespaces, Gitpod, and VS Code Dev Containers on Docker. +// The estate default is rootless Podman: .devcontainer/devcontainer.json. +// Same image (../Containerfile); only the user-namespace settings differ. +{ + "name": "occupancy-types (Docker / Codespaces)", + + "build": { + "dockerfile": "../Containerfile", + "context": "../.." + }, + + "postCreateCommand": "just deps", + + "remoteUser": "nonroot", + + "containerEnv": { + "EDITOR": "code --wait", + "LANG": "C.UTF-8" + }, + + "customizations": { + "vscode": { + "extensions": [ + "EditorConfig.EditorConfig", + "eamodio.gitlens", + "streetsidesoftware.code-spell-checker", + "timonwong.shellcheck", + "tamasfe.even-better-toml", + "skellock.just", + "redhat.vscode-yaml", + "DavidAnson.vscode-markdownlint", + "asciidoctor.asciidoctor-vscode", + "usernamehw.errorlens" + ], + "settings": { + "editor.formatOnSave": true, + "editor.insertSpaces": true, + "editor.tabSize": 2, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "[makefile]": { + "editor.insertSpaces": false + } + } + }, + "codespaces": { + "openFiles": [ + "README.adoc" + ] + } + }, + + "forwardPorts": [], + + "shutdownAction": "stopContainer" +} diff --git a/.editorconfig b/.editorconfig index 7767fc4..c5bae8d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 # Hyperpolymath estate canonical .editorconfig (standards#343 phase 1) -# Canon lives in rsr-template-repo; do not add a per-repo name to this header. +# Canon lives in occupancy-types; do not add a per-repo name to this header. # https://editorconfig.org root = true @@ -121,9 +121,6 @@ end_of_line = crlf # adopted for new work. These sections are inert where the files are absent and # keep surviving files from drifting. Removal is a per-repo judgement. -[*.a2ml] -indent_size = 2 - [*.go] indent_style = tab diff --git a/.envrc b/.envrc index 38d3f0c..fde7b5d 100644 --- a/.envrc +++ b/.envrc @@ -2,18 +2,18 @@ # Activate development environment # Install direnv: https://direnv.net/ -# Load .tool-versions if asdf is available +# Load the mise toolchain (mise.toml + mise.lock) if has mise; then use mise fi -# Load Guix shell if build/guix.scm exists -if has guix && [ -f build/guix.scm ]; then - use guix +# Load the Guix development shell when Guix is present (same manifest as `just dev-shell`) +if has guix && [ -f build/manifest.scm ]; then + use guix -m build/manifest.scm fi # Project environment variables -export PROJECT_NAME="{{PROJECT_NAME}}" +export PROJECT_NAME="occupancy-types" export RSR_TIER="infrastructure" # Add non-secret project env vars above (e.g. DATABASE_URL, service endpoints). # Real secrets belong in .env (gitignored) — never commit them to .envrc. diff --git a/.gitattributes b/.gitattributes index c85fa95..86298dc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ -# SPDX-License-Identifier: MPL-2.0 +# SPDX-License-Identifier: MPL-2.0 (code) +# SPDX-License-Identifier: CC-SA-BY-4.0 (prose) # Hyperpolymath estate canonical .gitattributes (standards#343 phase 1) -# Canon lives in rsr-template-repo; do not add a per-repo name to this header. +# Canon lives in occupancy-types; do not add a per-repo name to this header. # # Only diff drivers that git actually ships are used here. MEASURED on git # 2.47.3: diff=go and diff=zig are NOT drivers (git silently falls back to the diff --git a/.gitbot-fleet/enrolment.json b/.gitbot-fleet/enrolment.json new file mode 100644 index 0000000..28d108e --- /dev/null +++ b/.gitbot-fleet/enrolment.json @@ -0,0 +1 @@ +{"reporters":{"accessibilitybot":{"enrolled":false,"reason":"template default: enrol once the repository serves HTML"},"cipherbot":{"enrolled":false,"reason":"template default: enrol once the repository implements cryptography"},"echidnabot":{"enrolled":false,"reason":"template default: enrol once the repository carries proofs and an ECHIDNA engine is configured"},"finishingbot":{"enrolled":true,"title":"finishingbot (release readiness)"},"glambot":{"enrolled":true,"title":"glambot (presentation quality)"},"panicbot":{"enrolled":false,"reason":"panic-attack is already graded by the verdict grid's own panic-attack row, from static-analysis-gate.yml evidence; enrolling panicbot would report one scanner twice and give two rows that can disagree"},"rhodibot":{"enrolled":true,"title":"rhodibot (RSR compliance)"},"seambot":{"enrolled":false,"reason":"template default: enrol once a seam register exists at spec/seams/seam-register.json"},"sustainabot":{"enrolled":false,"reason":"reserved fleet slot; no adapter exists (gitbot-fleet bots/sustainabot/README.adoc)"}},"schema":1} diff --git a/.gitbot-fleet/report-backs/README.md b/.gitbot-fleet/report-backs/README.md new file mode 100644 index 0000000..526ba64 --- /dev/null +++ b/.gitbot-fleet/report-backs/README.md @@ -0,0 +1,35 @@ +# gitbot-fleet report-backs + +One JSON envelope per enrolled fleet member, `.json`, in the shape +metadatastician/standards `docs/VERDICT-GRID-SPEC.adoc` defines ("The +report-back envelope"). The Hypatia verdict grid gives every member named in +`../enrolment.json` one row; the row stays **orange** ("enrolled, but no +report-back was received") until an envelope is present here when the grid +renders. + +**This directory is empty in git on purpose, and stays so.** Envelopes are +produced per run by the `fleet` job of `.github/workflows/fork-governance.yml` +(gitbot-fleet `scripts/fleet-report-back.sh`), uploaded as the +`fleet-report-backs` artifact, and read from that artifact at render time by the +`verdict` job of the same workflow, which passes this directory to the grid +renderer. A committed envelope would be stale evidence about an older commit, so +none is ever committed. Do not add a placeholder to turn a row green; only a +real run may. + +Two related rules: + +* an enrolled member with **no** envelope renders **orange** ("enrolled, but no + report-back was received") — never green, and never grey + (`docs/HYPATIA-RULE-ORDER.adoc`); +* a member that reports back without an enrolment entry still gets a row from + its envelope alone. Enrolment decides what a *missing* envelope means; it does + not decide what a present one is allowed to say + (metadatastician/standards `docs/VERDICT-GRID-SPEC.adoc`). + +When instantiating a repository from this template, edit `../enrolment.json`: +enrol what is true of the repository, with a reason for every member that does +not apply. The template defaults enrol rhodibot, finishingbot and glambot, +which apply to every estate repository. Adoption guide: standards +`3-practice/fork-governance/ADOPTION.adoc` §8. + +Only files matching `*.json` are read, so this README is inert. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 611b5a8..6e4a4e2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,13 +2,14 @@ # CODEOWNERS - Define code review assignments # See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # -# Replace {{OWNER}} with your GitHub username or team +# Replace hyperpolymath with your GitHub username or team # Default owners for everything -* @{{OWNER}} +* @hyperpolymath # Security-sensitive files require explicit review -SECURITY.md @{{OWNER}} -.github/workflows/ @{{OWNER}} -Trustfile.a2ml @{{OWNER}} -.machine_readable/ @{{OWNER}} +SECURITY.md @hyperpolymath +.github/workflows/ @hyperpolymath +.machine_readable/contractiles/trust/ @hyperpolymath +*_chora.deed @hyperpolymath +.machine_readable/ @hyperpolymath diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index c7f72ae..0000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,338 +0,0 @@ - -# Code of Conduct - - - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in rsr-template-repo a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, colour, religion, or sexual identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. - -We recognise that a thriving open source community requires **psychological safety** — an environment where people can contribute, ask questions, make mistakes, and learn without fear of ridicule or retaliation. - ---- - -## Our Standards - -### Expected Behaviour - -The following behaviours contribute to a positive environment: - -**Communication** -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Giving and gracefully accepting constructive feedback -- Assuming good intent while addressing impact -- Communicating clearly and patiently, especially with newcomers - -**Collaboration** -- Focusing on what is best for the community -- Showing empathy and kindness toward other community members -- Being collaborative rather than competitive -- Mentoring and supporting less experienced contributors -- Celebrating others' contributions and successes - -**Professionalism** -- Accepting responsibility and apologising to those affected by our mistakes -- Learning from the experience and avoiding repetition -- Respecting others' time and attention -- Staying on topic in project spaces -- Following project guidelines and conventions - -**Accessibility** -- Using plain language and avoiding unnecessary jargon -- Providing alt text for images and transcripts for audio/video -- Being patient with those using assistive technologies -- Accommodating different communication styles and needs -- Recognising that not everyone communicates the same way - -### Unacceptable Behaviour - -The following behaviours are considered harassment and are unacceptable: - -**Harassment** -- The use of sexualised language or imagery, and sexual attention or advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Deliberate intimidation, stalking, or following (online or in-person) -- Unwelcome physical contact or simulated physical contact (e.g., emoji) -- Sustained disruption of talks, events, or online discussions - -**Discrimination** -- Discriminatory jokes and language -- Posting or threatening to post others' personally identifying information ("doxing") -- Advocating for, or encouraging, any of the above behaviour -- Microaggressions — subtle, often unintentional, discriminatory comments or actions - -**Professional Misconduct** -- Publishing others' private information without explicit permission -- Misrepresenting affiliation or contributions -- Plagiarism or claiming credit for others' work -- Retaliating against anyone who reports a Code of Conduct violation -- Other conduct which could reasonably be considered inappropriate in a professional setting - -### Grey Areas - -Some situations require judgement. When uncertain: - -- **Intent vs Impact**: Good intentions do not excuse harmful impact. Focus on making things right. -- **Power Dynamics**: Those with more power (maintainers, employers, experienced contributors) must be especially mindful of their impact. -- **Cultural Differences**: What's acceptable varies by culture. When in doubt, err on the side of caution and ask. -- **Humour**: Jokes at others' expense are rarely funny to everyone. Punch up, not down. - ---- - -## Scope - -This Code of Conduct applies within all community spaces, including: - -**Online Spaces** -- Repository discussions, issues, and pull/merge requests -- Project chat channels (Matrix, Discord, Slack, IRC) -- Mailing lists and forums -- Social media when representing the project -- Video calls and virtual meetings - -**In-Person Spaces** -- Conferences, meetups, and events -- Workshops and training sessions -- Any gathering where you represent the project - -**Representation** -This Code of Conduct also applies when an individual is officially representing the community in public spaces. Examples include: - -- Using an official project email address -- Posting via an official social media account -- Acting as an appointed representative at an event -- Speaking on behalf of the project - ---- - -## Enforcement - -### Reporting - -If you experience or witness unacceptable behaviour, or have any other concerns, please report it as soon as possible. - -**How to Report** - -| Method | Details | Best For | -|--------|---------|----------| -| **Email** | {{CONDUCT_EMAIL}} | Detailed reports, sensitive matters | -| **Private Message** | Contact any maintainer directly | Quick questions, minor issues | - -**What to Include** - -- Your contact information (unless anonymous) -- Names/usernames of those involved -- Description of what happened -- When and where it occurred -- Any witnesses -- Any supporting evidence (screenshots, links) -- How you would like us to respond (if you have a preference) - -**What Happens Next** - -1. You will receive acknowledgment within **{{RESPONSE_TIME}}** -2. The {{CONDUCT_TEAM}} will review the report -3. We may ask for additional information -4. We will determine appropriate action -5. We will inform you of the outcome (respecting others' privacy) - -### Confidentiality - -All reports will be handled with discretion: - -- Reporter identity is protected by default -- Details are shared only with those who need to know -- We will ask before naming you in any communication -- Anonymous reports are accepted and investigated - -### Conflicts of Interest - -If the {{CONDUCT_TEAM}} are themselves involved in an incident: - -- They will recuse themselves from the process -- Another maintainer or external party will handle the report -- We will disclose any potential conflicts - ---- - -## Enforcement Guidelines - -The {{CONDUCT_TEAM}} will follow these guidelines in determining consequences: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behaviour deemed unprofessional or unwelcome. - -**Consequence**: A private, written warning providing clarity around the nature of the violation and an explanation of why the behaviour was inappropriate. A public apology may be requested. - -**Duration**: Immediate - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of actions. - -**Consequence**: A warning with consequences for continued behaviour. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. - -**Duration**: 1-4 weeks - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including sustained inappropriate behaviour. - -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -**Duration**: 1-6 months - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behaviour, harassment of an individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the community. - -**Duration**: Permanent (with appeal rights after 12 months) - -### Enforcement Across Perimeters - -This project uses the estate's **Tri-Perimeter Contribution Framework (TPCF)**, a -graduated trust model defined in `hyperpolymath/standards` -(`rhodium-standard-repositories/README.adoc`): - -- 🔒 **Perimeter 1 (Core)** — maintainers only; shell runtime, build systems -- 🧠 **Perimeter 2 (Expert)** — trusted contributors; protocol extensions, validators -- 🌱 **Perimeter 3 (Community)** — open to all; docs, tests, proposals - -For contributors with elevated access (Perimeter 2 or 1): - -| Level | Additional Consequence | -|-------|----------------------| -| Correction | Noted in contributor record | -| Warning | Access privileges may be temporarily reduced | -| Temporary Ban | Access reduced to Perimeter 3 for ban duration | -| Permanent Ban | All access revoked | - ---- - -## Appeals - -If you believe an enforcement decision was made in error: - -1. **Wait 7 days** after the decision (cooling-off period) -2. **Email** {{CONDUCT_EMAIL}} with subject line "Appeal: [Original Report ID]" -3. **Explain** why you believe the decision should be reconsidered -4. **Provide** any new information not previously available - -**Appeals Process** - -- Appeals are reviewed by the {{CONDUCT_TEAM}}, excluding anyone involved in the original -- You will receive a response within 14 days -- The appeals decision is final -- You may only appeal once per incident - -**Grounds for Appeal** - -- Procedural errors in the original investigation -- New evidence not previously available -- Disproportionate response to the violation -- Misunderstanding of facts - ---- - -## Supporting Those Who Report - -We are committed to supporting those who report violations: - -**We Will** -- Believe and take all reports seriously -- Respect your privacy and confidentiality preferences -- Keep you informed of progress (if you wish) -- Take steps to protect you from retaliation -- Provide resources if you need support - -**We Will Not** -- Require you to confront the person directly -- Dismiss reports without investigation -- Reveal your identity without consent -- Tolerate retaliation against reporters -- Rush you to make decisions - ---- - -## Prevention - -Beyond enforcement, we actively work to prevent issues: - -**Onboarding** -- All contributors are expected to read this Code of Conduct -- Perimeter 2 applicants must confirm they've read and understood it -- Maintainers receive additional training on enforcement - -**Culture** -- We model the behaviour we expect -- We intervene early when we see potential issues -- We thank people for positive contributions -- We create opportunities for diverse voices - -**Review** -- This Code of Conduct is reviewed annually -- Community feedback is welcomed -- Changes are communicated clearly - ---- - -## Acknowledgments - -This Code of Conduct is adapted from: - -- [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1 -- [Django Code of Conduct](https://www.djangoproject.com/conduct/) -- [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) -- [Python Community Code of Conduct](https://www.python.org/psf/conduct/) - -We thank these communities for their leadership in creating welcoming spaces. - ---- - -## Questions? - -If you have questions about this Code of Conduct: - -- Open a [Discussion](https://github.com/hyperpolymath/rsr-template-repo/discussions) (for general questions) -- Email {{CONDUCT_EMAIL}} (for private questions) -- Contact any maintainer directly - ---- - -## Summary - -**Be kind. Be respectful. Be collaborative.** - -We're all here because we care about this project. Let's make it a place where everyone can do their best work. - ---- - -Last updated: {{CURRENT_YEAR}} · Based on Contributor Covenant 2.1 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 66f33b4..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,103 +0,0 @@ - -``` -# Clone the repository -git clone https://github.com/hyperpolymath/rsr-template-repo.git -cd rsr-template-repo - -# Using Guix (recommended for reproducibility) -guix shell -D -f build/guix.scm - -# Or using toolbox/distrobox -toolbox create rsr-template-repo-dev -toolbox enter rsr-template-repo-dev -# Install dependencies manually - -# Verify setup -just check # or: cargo check / mix compile / etc. -just test # Run test suite -``` - -### Repository Structure - -The authoritative map is **generated** from the tree and checked in CI, so it -cannot drift: -[`docs/architecture/REPOSITORY-MAP.adoc`](../docs/architecture/REPOSITORY-MAP.adoc). -Regenerate it with `just repo-map`. - -A hand-written tree used to live here. It described `lib/`, `extensions/`, -`plugins/` and `spec/` directories that this repository has never contained, -which is precisely why the map is now generated rather than typed. - ---- - -## How to Contribute - -### Reporting Bugs - -**Before reporting**: -1. Search existing issues -2. Check if it's already fixed in `{{MAIN_BRANCH}}` -3. Determine which perimeter the bug affects - -**When reporting**: - -Use the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) and include: - -- Clear, descriptive title -- Environment details (OS, versions, toolchain) -- Steps to reproduce -- Expected vs actual behaviour -- Logs, screenshots, or minimal reproduction - -### Suggesting Features - -**Before suggesting**: -1. Check the [roadmap](../docs/status/ROADMAP.adoc) if available -2. Search existing issues and discussions -3. Consider which perimeter the feature belongs to - -**When suggesting**: - -Use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.md) and include: - -- Problem statement (what pain point does this solve?) -- Proposed solution -- Alternatives considered -- Which perimeter this affects - -### Your First Contribution - -Look for issues labelled: - -- [`good first issue`](https://github.com/hyperpolymath/rsr-template-repo/labels/good%20first%20issue) — Simple Perimeter 3 tasks -- [`help wanted`](https://github.com/hyperpolymath/rsr-template-repo/labels/help%20wanted) — Community help needed -- [`documentation`](https://github.com/hyperpolymath/rsr-template-repo/labels/documentation) — Docs improvements -- [`perimeter-3`](https://github.com/hyperpolymath/rsr-template-repo/labels/perimeter-3) — Community sandbox scope - ---- - -## Development Workflow - -### Branch Naming -``` -docs/short-description # Documentation (P3) -test/what-added # Test additions (P3) -feat/short-description # New features (P2) -fix/issue-number-description # Bug fixes (P2) -refactor/what-changed # Code improvements (P2) -security/what-fixed # Security fixes (P1-2) -``` - -### Commit Messages - -We follow [Conventional Commits](https://www.conventionalcommits.org/): -``` -(): - -[optional body] - -[optional footer] -``` diff --git a/.github/GOVERNANCE.md b/.github/GOVERNANCE.adoc similarity index 93% rename from .github/GOVERNANCE.md rename to .github/GOVERNANCE.adoc index 4f88882..e09c73c 100644 --- a/.github/GOVERNANCE.md +++ b/.github/GOVERNANCE.adoc @@ -4,13 +4,13 @@ Copyright (c) Jonathan D.A. Jewell --> # Project Governance -This document describes the governance model for **{{PROJECT_NAME}}**. +This document describes the governance model for **occupancy-types**. --- ## Project Governance Model -{{PROJECT_NAME}} follows a **Benevolent Dictator For Life (BDFL)** governance model. +occupancy-types follows a **Benevolent Dictator For Life (BDFL)** governance model. This model is well-suited for solo maintainers and small project teams where rapid, consistent decision-making is more valuable than formal consensus processes. @@ -46,7 +46,7 @@ release schedules, contributor access, and community standards. - ADR statuses: `proposed`, `accepted`, `deprecated`, `superseded`, `rejected`. - ADRs provide a historical record of why decisions were made and what alternatives were considered. -- See `.machine_readable/descriptiles/META.a2ml` for the machine-readable ADR index. +- The ADR index is `docs/decisions/` itself; the `(meta)` clause of the repo deed (`*_chora.deed`) records the machine-readable architecture axes and practices. --- @@ -157,4 +157,4 @@ with the community before adoption, even though the BDFL retains final authority --- -Copyright (c) {{CURRENT_YEAR}} {{OWNER}}. Licensed under MPL-2.0. +Copyright (c) 2026 hyperpolymath. Licensed under MPL-2.0. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5e508a7..031f879 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,8 +6,8 @@ blank_issues_enabled: true contact_links: - name: Security vulnerability - url: https://{{FORGE}}/{{OWNER}}/{{REPO}}/security/advisories/new + url: https://github.com/hyperpolymath/occupancy-types/security/advisories/new about: Report privately via a security advisory. Do NOT open a public issue. - name: Question or support request - url: https://{{FORGE}}/{{OWNER}}/{{REPO}}/discussions + url: https://github.com/hyperpolymath/occupancy-types/discussions about: Ask a question. See .github/SUPPORT.md for what to expect. diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 9d376eb..c9a9073 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -33,7 +33,7 @@ We take security seriously. We appreciate your efforts to responsibly disclose v The preferred method for reporting security vulnerabilities is through GitHub's Security Advisory feature: -1. Navigate to [Report a Vulnerability](https://github.com/hyperpolymath/rsr-template-repo/security/advisories/new) +1. Navigate to [Report a Vulnerability](https://github.com/hyperpolymath/occupancy-types/security/advisories/new) 2. Click **"Report a vulnerability"** 3. Complete the form with as much detail as possible 4. Submit — we'll receive a private notification @@ -51,7 +51,7 @@ If you cannot use GitHub Security Advisories, you may email us directly: | | | |---|---| -| **Email** | {{SECURITY_EMAIL}} | +| **Email** | j.d.a.jewell@open.ac.uk | This mailbox is not encrypted. For anything sensitive enough to need encryption, prefer GitHub Security Advisories above — the report stays private @@ -187,7 +187,7 @@ If we cannot reach agreement on disclosure timing, we default to 90 days from yo The following are within scope for security research: -- This repository (`hyperpolymath/rsr-template-repo`) and all its code +- This repository (`hyperpolymath/occupancy-types`) and all its code - Official releases and packages published from this repository - Documentation that could lead to security issues - Build and deployment configurations in this repository @@ -306,7 +306,7 @@ Recognition includes: To stay informed about security updates: - **Watch this repository**: Click "Watch" → "Custom" → Select "Security alerts" -- **GitHub Security Advisories**: Published at [Security Advisories](https://github.com/hyperpolymath/rsr-template-repo/security/advisories) +- **GitHub Security Advisories**: Published at [Security Advisories](https://github.com/hyperpolymath/occupancy-types/security/advisories) - **Release notes**: Security fixes noted in [CHANGELOG](../CHANGELOG.md) ### Update Policy @@ -332,7 +332,7 @@ To stay informed about security updates: ## Security Best Practices -When using rsr-template-repo, we recommend: +When using occupancy-types, we recommend: ### General @@ -354,7 +354,7 @@ When using rsr-template-repo, we recommend: ## Additional Resources -- [Security Advisories](https://github.com/hyperpolymath/rsr-template-repo/security/advisories) +- [Security Advisories](https://github.com/hyperpolymath/occupancy-types/security/advisories) - [Changelog](../CHANGELOG.md) - [Contributing Guidelines](CONTRIBUTING.md) - [CVE Database](https://cve.mitre.org/) @@ -366,8 +366,8 @@ When using rsr-template-repo, we recommend: | Purpose | Contact | |---------|---------| -| **Security issues** | [Report via GitHub](https://github.com/hyperpolymath/rsr-template-repo/security/advisories/new) or {{SECURITY_EMAIL}} | -| **General questions** | [GitHub Discussions](https://github.com/hyperpolymath/rsr-template-repo/discussions) | +| **Security issues** | [Report via GitHub](https://github.com/hyperpolymath/occupancy-types/security/advisories/new) or j.d.a.jewell@open.ac.uk | +| **General questions** | [GitHub Discussions](https://github.com/hyperpolymath/occupancy-types/discussions) | | **Other enquiries** | See [README](../README.adoc) for contact information | --- @@ -382,8 +382,8 @@ This security policy may be updated from time to time. Significant changes will --- -*Thank you for helping keep rsr-template-repo and its users safe.* 🛡️ +*Thank you for helping keep occupancy-types and its users safe.* 🛡️ --- -Last updated: {{CURRENT_YEAR}} · Policy version: 1.0.0 +Last updated: 2026 · Policy version: 1.0.0 diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md deleted file mode 100644 index b06c59a..0000000 --- a/.github/SUPPORT.md +++ /dev/null @@ -1,7 +0,0 @@ -# Support - -For questions, help, and community discussion: - -- GitHub Discussions: https://github.com/{{OWNER}}/{{REPO}}/discussions -- GitHub Issues: https://github.com/{{OWNER}}/{{REPO}}/issues -- Documentation: See README.adoc in the root directory. 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/copilot-instructions.md b/.github/copilot-instructions.md index 4d831b6..a6d5be1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,7 +2,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0 Copyright (c) Jonathan D.A. Jewell --> - + # Copilot Instructions @@ -11,13 +11,13 @@ Copyright (c) Jonathan D.A. Jewell - Read the repo deed (`*_chora.deed` in the repo root) for canonical file locations (its `(ply ...)` clauses carry the canonical-locations tables). -- State files (.a2ml) live in `.machine_readable/` ONLY, never the root. +- No `.a2ml` file may exist anywhere: state and metadata are clauses of the repo deed. ## License - SPDX: `MPL-2.0` on all new files. - Never use AGPL-3.0. -- Copyright: `{{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}>` +- Copyright: `Jonathan D.A. Jewell (hyperpolymath) ` ## Code Style @@ -43,13 +43,17 @@ that can do the job: 1. **Bun** — default for all new work. Runs compiled ESM/JS directly, no bundler step. Uses an npm-compatible `package.json` plus `bun.lock`; both are expected, not anti-patterns. -2. **pnpm** — only where an upstream toolchain requires `node_modules`. -3. **npm** — last resort. Permitted, never preferred; a deliberate, noted choice. - -**Deno is being removed**, not grandfathered. Owner ruling 2026-08-26: *"deno is -to go and bun is the way we are going, put it first everywhere unless not -possible and explain why if not."* Existing Deno projects migrate to Bun; where -Bun genuinely cannot be used, document the reason in the repo. +2. **Deno** — permitted (tier 2). Deno 2 reads the same `package.json` and runs + its scripts with `deno task`. +3. **pnpm** — tier 3. +4. **npm** — last resort. Permitted, never preferred; it keeps no lock + (`package-lock.json` stays refused). + +**One manifest, every runtime** (owner ruling 2026-10-14, `LANGUAGE-POLICY.adoc` +§1.3–§1.4). `package.json` is the only manifest and carries every runtime's lock: +`bun.lock`, `deno.lock` and `pnpm-lock.yaml`. `js-runtimes regen` is the only +writer; run it after every `bun add`. Governance checks each runtime against its +lock. The ruling supersedes the 2026-08-26 one that removed Deno. **TypeScript is not the language for new application code — AffineScript is.** `LANGUAGE-POLICY.adoc` §1.2, ruled 2026-08-25, separates two questions the older @@ -64,7 +68,6 @@ migration destination is AffineScript. - No Go (use Rust) - No Python (use Julia or Rust) - No Nix (use Guix) -- No Deno for new work — being removed estate-wide; use Bun (owner ruling 2026-08-26) - No ReScript (`LANGUAGE-POLICY.adoc` §3) — migrate to AffineScript ## Containers @@ -81,6 +84,6 @@ migration destination is AffineScript. ## State Files -Never create these in the repo root: -STATE.a2ml, META.a2ml, ECOSYSTEM.a2ml, AGENTIC.a2ml, NEUROSYM.a2ml, PLAYBOOK.a2ml. -They belong in `.machine_readable/` only. +No `.a2ml` file may exist anywhere: the format is retired. State, metadata, +`(neurosym)` and `(playbook)` are clauses of the repo deed (`*_chora.deed`); +the journal is `docs/status/ROADMAP.adoc`. Never recreate them as separate files. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c58f7eb..51e4769 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,6 +14,20 @@ updates: patterns: - "*" open-pull-requests-limit: 2 + ignore: + # HOLD: github/codeql-action at v4.38.0 (SHA-pinned). v4.38.1 fails + # GitHub workflow-startup validation estate-wide (nexia-list#100; + # SHA-form re-bump bypassed versions-scoped ignores - see + # nexia-list#101/#104). Hold until upstream clears 4.38.1 or a + # newer release verifies green; revisit deliberately. + # + # The trailing * is load-bearing, as in standards/.github/dependabot.yml. + # Workflows reference the SUBPATH actions (github/codeql-action/init, + # /analyze) and Dependabot treats each subpath as its own dependency + # name, so a bare "github/codeql-action" matches NONE of them. That is + # how #210 bumped 4.38.0 -> 4.38.2 straight through this hold and left + # actions.lock out of sync (#217). + - dependency-name: "github/codeql-action*" # Rust/Cargo # @@ -27,50 +41,3 @@ updates: # "security only, not routine bumps". Pair with the # dependabot-automerge.yml workflow for low-touch security # maintenance. - ignore: - # HOLD: github/codeql-action at v4.38.0 (SHA-pinned). v4.38.1 fails - # GitHub workflow-startup validation estate-wide (nexia-list#100; - # SHA-form re-bump bypassed versions-scoped ignores - see - # nexia-list#101/#104). Hold until upstream clears 4.38.1 or a - # newer release verifies green; revisit deliberately. - - dependency-name: "github/codeql-action" - - - package-ecosystem: "cargo" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 0 - - # Elixir/Mix - - package-ecosystem: "mix" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 3 - groups: - dependency-updates: - patterns: ["*"] - - # Node.js/npm - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 3 - groups: - dependency-updates: - patterns: ["*"] - - # Python/pip - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 3 - groups: - dependency-updates: - patterns: ["*"] - - # NOTE: Nix is banned estate-wide — Guix is the sole package manager. - # Do not add a `nix` ecosystem here. Guix has no Dependabot ecosystem; - # pin Guix channels/manifests via build/ tooling instead. 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..c3b0ebb 100755 --- a/.github/hooks/scan-secrets.sh +++ b/.github/hooks/scan-secrets.sh @@ -7,13 +7,15 @@ # ── Why this is NOT duplicated work ───────────────────────────────────────── # The estate runs a TWO-TIER secret defence: # -# CI gitleaks, via the standards `secret-scanner-reusable.yml` -# (gitleaks + rust-secrets + shell-secrets). Catches what reaches the +# CI gitleaks, rust-secrets and shell-secrets, inline in +# `.github/workflows/secret-scanner.yml` (the same three scanner +# contracts the standards reusable publishes, kept inline so the three +# check names are this repository's own). Catches what reaches the # remote, on every pull_request and push to main. # LOCAL TruffleHog — this script — before the push leaves the machine. # -# Different detection engines at different checkpoints. The reusable's header -# retires TruffleHog "as redundant with gitleaks"; that judgement is scoped to +# Different detection engines at different checkpoints. The estate retired +# TruffleHog "as redundant with gitleaks"; that judgement is scoped to # CI, where running both cost twice over one population. It is not a reason to # leave the local tier unguarded. DO NOT delete this script as duplication — # see .github/workflows/README.adoc, "Secret scanning is single-sourced". @@ -162,7 +164,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 e644a73..733fa92 100755 --- a/.github/hooks/validate-deed.sh +++ b/.github/hooks/validate-deed.sh @@ -2,392 +2,82 @@ # 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 .a2ml and .deed files (dual-accept per owner ruling R-H2) 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) +# 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. +# +# A2ML is retired estate-wide (D99, D312); its facts belong in the repo deed. +# Without a ledger, any tracked A2ML file is an error on sight. A repository +# that still carries legacy A2ML lists each file, by exact path, in the +# shrink-only ledger .machine_readable/a2ml-legacy-allow.txt (owner decision +# 2026-11-04), and the A2ML check is then the canonical gate, +# scripts/check-a2ml-archived.sh from the same standards checkout: a file +# outside the ledger and outside 9-archive/ fails, and so does an entry whose +# file is gone. With A2ML_BASE set to a ref (CI sets the PR base), an entry +# the base ledger did not list also fails, so the ledger can only shrink. # # 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) +# A2ML_BASE — Ref whose ledger this one may not grow beyond (optional) # # Exit codes: -# 0 — All files valid (or only warnings in non-strict mode) -# 1 — Validation errors found +# 0 — Every tracked deed lints and the A2ML check passes, or no linter is +# available (skipped with a notice: CI still runs the lane) +# 1 — A deed failed, or an A2ML file is not accounted for 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_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) - # - project = "..." (for STATE.a2ml) - 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 = "..."`. - # - # `archetype` is the identity key of the ARCHETYPE.a2ml shape — the - # `just repo-init` scaffolding descriptors under archetypes/. It names the - # archetype exactly as `name` names a manifest, and is the fourth - # dialect this recogniser accommodates alongside TOML, s-expression and - # brace-block. Without it, archetypes/julia-library/ARCHETYPE.a2ml - # failed with "Missing required identity field" — on main, in this repo - # and in every repo instantiated from it. - # - # Recognising the shape is the right fix rather than adding a redundant - # `name = "julia-library"` beside `archetype = "julia-library"`: that - # file's own header states an archetype "must not write fiction", and - # duplicating its identity to satisfy a grep is exactly that. - 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" - - # 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 DEED; identity is carried by the parent repo. - *file.a2ml) - is_manifest=true - ;; - esac - - # Contractile-shape DEED 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 DEED 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 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: 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 +REPO="${INPUT_PATH:-.}" +STANDARDS_DIR="${STANDARDS_DIR:-$(cd "$REPO/.." && pwd)/standards}" +case "$STANDARDS_DIR" in + /*) ;; + *) STANDARDS_DIR="$PWD/$STANDARDS_DIR" ;; +esac +LINTER="$STANDARDS_DIR/1-formats/deed/tools/deed_lint.js" +A2ML_GATE="$STANDARDS_DIR/scripts/check-a2ml-archived.sh" +LEDGER=".machine_readable/a2ml-legacy-allow.txt" + +cd "$REPO" +errors=0 + +if [ -f "$LEDGER" ]; then + if [ -f "$A2ML_GATE" ]; then + echo " A2ML: checking against the shrink-only ledger $LEDGER" + if ! bash "$A2ML_GATE" ${A2ML_BASE:+--base "$A2ML_BASE"} .; then + errors=$((errors + 1)) 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::DEED Manifest Validation" -echo "Scanning ${SCAN_PATH} for .a2ml files..." -echo "" - -# Find all manifest files (.a2ml legacy + .deed — dual-accept; extension migration = standards #837, the DEED conversion campaign), excluding .git -mapfile -t deed_candidates < <(find "$SCAN_PATH" \( -name '*.a2ml' -o -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 + else + echo "::notice::A2ML: $LEDGER is present but the canonical gate is not ($A2ML_GATE); the deed-conformance lane checks it in CI" fi - deed_files+=("$_f") -done - -if [[ $SKIPPED -gt 0 ]]; then - echo "::notice::Skipped ${SKIPPED} file(s) matching paths-ignore" +else + mapfile -t a2ml < <(git ls-files | grep -E '\.a2ml(\.in|\.template|\.v[0-9]+\.template)?$' | grep -v '^9-archive/' || true) + for f in "${a2ml[@]}"; do + echo "::error file=$f::.a2ml is retired; move its facts into the repo deed and delete it, or list it in $LEDGER" + errors=$((errors + 1)) + done fi -if [[ ${#deed_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::" +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[@]} .a2ml 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..955c6c8 100755 --- a/.github/hooks/validate-k9.sh +++ b/.github/hooks/validate-k9.sh @@ -2,388 +2,65 @@ # 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}" +case "$STANDARDS_DIR" in + /*) ;; + *) STANDARDS_DIR="$PWD/$STANDARDS_DIR" ;; +esac +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/pull_request_template.md b/.github/pull_request_template.md index 2cca1e1..751230f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -21,7 +21,7 @@ Copyright (c) Jonathan D.A. Jewell - [ ] Tests pass (`just test` or equivalent) - [ ] Code is formatted (`just fmt` or equivalent) - [ ] Linter is clean (no new warnings or errors) -- [ ] No banned language patterns (no , no npm/bun, no Go/Python) +- [ ] No banned language patterns (no Python, Go, TypeScript or ReScript; JavaScript runs Bun > Deno > pnpm > npm) - [ ] No `unsafe` blocks without `// SAFETY:` comments - [ ] No banned functions (`believe_me`, `unsafeCoerce`, `Obj.magic`, `Admitted`, `sorry`) - [ ] SPDX license headers present on all new/modified source files @@ -29,9 +29,10 @@ Copyright (c) Jonathan D.A. Jewell ### As Applicable -- [ ] `.machine_readable/descriptiles/STATE.a2ml` updated (if project state changed) -- [ ] `.machine_readable/descriptiles/ECOSYSTEM.a2ml` updated (if integrations changed) -- [ ] `.machine_readable/descriptiles/META.a2ml` updated (if architectural decisions changed) +- [ ] Repo deed (`*_chora.deed`) `(status)` / `(maturity)` updated (if project state changed) +- [ ] Repo deed `(ecosystem)` updated (if integrations changed) +- [ ] Repo deed `(meta)` and/or `docs/decisions/` updated (if architectural decisions changed) +- [ ] `docs/status/ROADMAP.adoc` updated (if milestones or next actions changed) - [ ] Documentation updated for user-facing changes - [ ] `TOPOLOGY.md` updated (if architecture changed) - [ ] `CHANGELOG` or release notes updated diff --git a/.github/rulesets/Immutable-Tags.json b/.github/rulesets/Immutable-Tags.json index 75fe73a..05dbc8b 100644 --- a/.github/rulesets/Immutable-Tags.json +++ b/.github/rulesets/Immutable-Tags.json @@ -1,28 +1 @@ -{ - "name": "Immutable-Tags", - "target": "tag", - "enforcement": "active", - "conditions": { - "ref_name": { - "include": [ - "~ALL" - ], - "exclude": [] - } - }, - "bypass_actors": [], - "rules": [ - { - "type": "deletion" - }, - { - "type": "non_fast_forward" - }, - { - "type": "update" - }, - { - "type": "required_signatures" - } - ] -} +{"bypass_actors":[],"conditions":{"ref_name":{"exclude":[],"include":["~ALL"]}},"enforcement":"active","name":"Immutable-Tags","rules":[{"type":"deletion"},{"type":"non_fast_forward"},{"type":"update"},{"type":"required_signatures"}],"target":"tag"} diff --git a/.github/rulesets/README.adoc b/.github/rulesets/README.adoc index 1b79726..5b0fdf3 100644 --- a/.github/rulesets/README.adoc +++ b/.github/rulesets/README.adoc @@ -39,14 +39,144 @@ gh api --method PUT \ == Files `base.json`:: Canonical branch protection ruleset for the default branch (`~DEFAULT_BRANCH`). -Enforces required signatures, linear history, squash merge, and deletion/force-push protection -without unsatisfiable coverage or deadlocking approval requirements. +Enforces deleted-branch protection, linear history, squash merge, signatures and required +status checks, without unsatisfiable coverage or deadlocking approval requirements. +Sourced verbatim from `hyperpolymath/standards` `config/rulesets/base.json` (§7.3). + +`branch-floor.json`:: The irreducible branch floor for the default branch: deletion and +non-fast-forward protection only, with no bypass actors. This is the ruleset that keeps a +branch recoverable when the richer `Base` ruleset is unavailable or has been removed. +Sourced verbatim from `hyperpolymath/standards` `config/rulesets/branch-floor.json`. + +`require-signed-commits.json`:: Standalone, zero-bypass `required_signatures` on the default +branch (D215). Every new repo is born with it. See `hyperpolymath/standards` +`docs/SIGNING-POLICY.adoc`. Sourced verbatim from `config/rulesets/require-signed-commits.json`. + +`require-linear-history.json`:: Standalone, zero-bypass `required_linear_history` (D216). +Pair it with squash-only merge settings. A new repo starts with no merge commits, so it is +always linear-safe. Sourced verbatim from `config/rulesets/require-linear-history.json`. `Immutable-Tags.json`:: Makes release tags immutable — blocks tag deletion, non-fast-forward updates, and overwrites, and requires signed tags. +[NOTE] +==== +Files here are named to match the *live ruleset name*, which is not always the +upstream filename. Upstream ships `branch-floor.json` and `immutable-tags.json` +(lowercase, with a matching lowercase `name` inside the JSON); this repository's +`Immutable-Tags.json` declares `"name": "Immutable-Tags"`. Renaming a file here +changes nothing on GitHub, but it does change which upstream file it is diffed +against — keep that in mind when syncing. +==== + +== What is actually enforced on this repository + +Files are intent; this table is fact. Regenerate it with: + +[source,bash] +---- +gh api "/repos/{owner}/{repo}/rulesets" --jq '.[] | "\(.id)\t\(.name)\t\(.enforcement)\t\(.target)"' +---- + +|=== +| Live ruleset | Enforcement | Target | Rules reported by the API + +| `Require-Linear-History` (`24258228`) | active | branch | `required_linear_history` +| `Secret-Scan-Floor` (`24309812`) | active | branch | *none* — `rules: []` +| `Immutable-Tags` (`22959911`) | active | tag | creation, deletion, non-fast-forward, update, required signatures +|=== + +[IMPORTANT] +==== +Measured 2026-11-08. Two corrections against the previous version of this table: + +. `Base` (`23218684`) no longer exists. `GET /rulesets/23218684` returns +*404 Not Found*, and the repository's ruleset list does not include it. Every +claim in the drift notes below about what `Base` enforces — code scanning, +Copilot review, `deletion`, `non_fast_forward` — describes a ruleset that is +gone. `GET /rules/branches/main` now returns exactly one rule +(`required_linear_history`, from `24258228`). +. `Secret-Scan-Floor` (`24309812`) is `active` on `target: branch` with +`conditions.ref_name.include: ["~DEFAULT_BRANCH"]`, but it carries **zero +rules**. It therefore requires *no* status check. `scan / gitleaks` is +*produced* by `.github/workflows/secret-scanner.yml`, not required by anything. + +So as of 2026-11-08 `main` is protected only by `required_linear_history`. +This is a record of the live state, not an endorsement: a status-check floor +that enforces nothing is not a floor. Restoring it is an owner action on the +live ruleset and a deliberate decision about which contexts to require, not +something these files can do on their own — `base.json` is upstream-sourced and +its `required_status_checks` is deliberately empty. +==== + +=== 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-10-25, recorded rather than silently reconciled because +`base.json` is sourced verbatim from upstream and must not be forked here. + +*Live rulesets that no synced file describes* :: + +CORRECTED 2026-11-08: the previous text here stated that `code_scanning` (CodeQL +and Hypatia) and `copilot_code_review` are "enforced live in `Base` +(`23218684`)", and that `Secret-Scan-Floor` (`24309812`) "enforces +`required_status_checks` for `scan / gitleaks`". Both statements are now false. +`Base` returns 404 and is absent from the ruleset list; `Secret-Scan-Floor` is +active but has `rules: []`. No live branch ruleset carries a `code_scanning` +rule either, so CodeQL and Hypatia currently *report* results without gating +merges. What remains true is the file-vs-live gap: upstream's nearest +equivalent for code scanning, +`config/rulesets/Optimus-Extras.json`, declares `code_scanning` with an *empty* +`code_scanning_tools` list, so it would not describe a populated live rule +either. Conversely, `branch-floor.json` is retained here as the irreducible +recovery payload even though `Branch-Floor` is not mounted as a live ruleset and +nothing currently supplies its `deletion` / `non_fast_forward` coverage. + +*Removed from live `Base` on 2026-10-02 (owner decision)* :: + +NOTE (2026-11-08): the history below is retained, but `Base` was subsequently +deleted outright — the ruleset returns 404 and is absent from the ruleset list. +Every rule discussed here is now unenforced, including the `deletion` and +`non_fast_forward` coverage noted further down. + +`Scorecard` was dropped from `code_scanning_tools`, and the `code_coverage` rule +(minimum 80%, max drop 5) was deleted. Neither could ever be satisfied by a pull +request. `scorecard.yml` runs only on `schedule` and `workflow_dispatch`, and +Scorecard scores repository posture rather than a diff, so with +`alerts_threshold: all` any standing finding would have blocked every PR. Nothing +in this template produces a coverage report: `rust-ci / llvm-cov` is skipped +because the template has no Rust. The result was that every PR sat `BLOCKED` until +an admin bypass (#219). CodeQL and Hypatia both upload results on pull requests, +so they stay. Do not add either rule back without a producer that runs on +`pull_request`. + +*`base.json` rules that are not in force* :: + +`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` was +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. With `Base` deleted as of 2026-11-08, no live ruleset supplies `non_fast_forward` or `deletion` on branches.) + +*`Immutable-Tags.json` lags the live ruleset* :: + +The live tag ruleset includes a `creation` rule; this file does not. Upstream +`immutable-tags.json` also populates `bypass_actors` (repository-role 5 and an +integration) with `bypass_mode: always`, and this file declares none. + +[WARNING] +==== +`bypass_actors` cannot be diffed from a low-privilege token: the REST API returns +`null` for that field unless the caller has admin on the repository. Absence of +`bypass_actors` in the output above is *not* evidence that none are configured. +==== + == Verifying Do not infer that a ruleset is active because the JSON is present. Check with the `gh api ... /rulesets` list above. A ruleset that exists as a file but was never POSTed is the protection equivalent of a check that cannot fail. + +Conversely, do not infer that the estate is protected because a ruleset exists. +A ruleset with `enforcement: disabled` is inert, and a `pull_request` rule with +`allowed_merge_methods: []` can never be satisfied at all. 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 new file mode 100644 index 0000000..13b9d11 --- /dev/null +++ b/.github/rulesets/branch-floor.json @@ -0,0 +1 @@ +{"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 new file mode 100644 index 0000000..611b621 --- /dev/null +++ b/.github/rulesets/require-linear-history.json @@ -0,0 +1 @@ +{"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 new file mode 100644 index 0000000..6d78a18 --- /dev/null +++ b/.github/rulesets/require-signed-commits.json @@ -0,0 +1 @@ +{"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/settings.yml b/.github/settings.yml index 46df341..70133c7 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -12,10 +12,10 @@ # It carries NO `name`, `description`, `homepage` or `private` key, and it must # never gain one. The reason is a real incident, not a hypothetical: # -# This file previously read `name: "rsr-template-repo"`. probot/settings applies it on +# This file previously read `name: "occupancy-types"`. probot/settings applies it on # every push to the default branch, so it submitted the literal string -# `rsr-template-repo` as the repository name. GitHub sanitises an invalid name by -# collapsing each run of illegal characters to a dash — `rsr-template-repo` became +# `occupancy-types` as the repository name. GitHub sanitises an invalid name by +# collapsing each run of illegal characters to a dash — `occupancy-types` became # `-REPO-`. The template renamed itself on every push, its old URL 404'd, and # it was mistaken for a deleted repository. `description` was likewise left # reading the literal `Canonical RSR (Rhodium Standard Repository) template — governance, CI/CD, machine-readable metadata, ABI/FFI seam and formal-verification scaffolding that hyperpolymath projects are instantiated from.` on the live repo. diff --git a/.github/workflows/README.adoc b/.github/workflows/README.adoc index c7a2328..728bcf3 100644 --- a/.github/workflows/README.adoc +++ b/.github/workflows/README.adoc @@ -11,18 +11,36 @@ not "tidied away" into a real coverage gap. Independent; this is the repo's own Hypatia coverage. * `static-analysis-gate.yml` -> `hypatia-scan` job — runs Hypatia as part of the **gate**, then the `deposit-findings` job hands the results to the - **gitbot-fleet learning** pipeline. This job is also a *required status check*. + **gitbot-fleet learning** pipeline. This job is *documented here as a + required status check*, but as of 2026-11-08 no live rule requires it: + `GET /rules/branches/main` returns only `required_linear_history`. It is a + gate in the sense that it reddens a run; it is not a merge blocker until a + ruleset says so. Same tool, two different consumers (security scan vs fleet learning). Removing either loses real function — keep both. == Secret scanning is single-sourced *in CI* — and two-tier overall -`secret-scanner.yml` calls the standards reusable (gitleaks + a Rust-secrets -check). An inline TruffleHog job was removed: the reusable deliberately retired +`secret-scanner.yml` runs all three estate scanners inline: `scan / gitleaks`, +`scan / rust-secrets` and `scan / shell-secrets`. An inline TruffleHog job was +removed earlier: the standards reusable it was paired with deliberately retired TruffleHog as redundant with gitleaks, so re-adding it was duplicated work, not extra coverage. +The three jobs are inline rather than one `uses:` of the standards +`secret-scanner-reusable.yml`, and that is deliberate. A job-level reusable call +reports ` / `, so the wrapper's three public check names +would depend on job names inside a *remote* workflow: rename one there and three +required contexts move in every generated repository. Inline jobs whose `name:` +is the context make the names this repository's own, and the whole file stays +inside `gh actions-lock`'s model — every `uses:` is a step-level action the +official generator can lock, so regenerating never leaves a dangling edge. Do +not "simplify" this back into a single wrapper call; + +`metadatastician/standards` does it as a wrapper because it *owns* the reusable +and can keep `./`-local, so its own names cannot re-stale. + **That retirement is scoped to CI.** The estate runs a deliberate *two-tier* secret defence, and the second tier is not in this directory: @@ -32,7 +50,7 @@ secret defence, and the second tier is not in this directory: | CI | gitleaks (+ rust-secrets, shell-secrets) -| `secret-scanner.yml` -> standards reusable. Runs on pull_request and on push +| `secret-scanner.yml`, three inline jobs. Runs on pull_request and on push to main — i.e. on what has *already reached* the remote. | Local @@ -61,3 +79,185 @@ 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 `badges/k9.json` to the + `governance-badges` branch on eligible pushes to `main`; the README badge reads it + through the shields endpoint; the section "The README badges are + generated output" below states the contract that branch operates under. +* `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. + +== The README badges are generated output (`governance-badges`) + +Every repository minted from this template publishes one Shields-endpoint JSON +file per validation lane, on a *separate orphan branch* named +`governance-badges`: + +[cols="1,1,3",options="header"] +|=== +| Lane | File | Written by + +| K9 conformance +| `badges/k9.json` +| `dogfood-gate.yml` -> `badges` job, on eligible pushes to `main` + +|=== + +`README.adoc` reads that file through the shields endpoint at a URL that must +not change: + +---- +https://raw.githubusercontent.com///governance-badges/badges/k9.json +---- + +Both halves - rendering the badge and deciding whether to publish it - live in +`scripts/governance-badge.js`. The workflow only supplies the lane's result and +the lane's artifact. `tests/workflows/governance_badge_test.sh` exercises the +helper against a mock of the GitHub API and checks this directory's wiring. + +=== It is generated output, not source + +* `governance-badges` is an *orphan* branch. Its commits are created with + `parents: []`, so there is *no common ancestor with `main`* - not "a little + history", none. It is not a variant, a release branch or a staging area. +* *It must never be merged*, and there is nothing to merge: a pull request from + it would show unrelated histories. Do not open one, do not merge one, do not + rebase it onto `main`. Merging it would import a badges-only tree over the + repository's real content. +* *Recent pushes are normal.* Eligible pushes to `main` republish the badge. A run + whose badge is unchanged makes *no commit at all*, so the branch is quiet + when the lane result is stable and moves only when the state changes. +* It is safe to delete. The next eligible push to `main` recreates it, with no parent, + from scratch. +* It carries no workflows, no code and nothing that runs. Treat it like a + generated site deployment, not like a branch of the repository. + +=== What each colour means + +[cols="1,1,1,3",options="header"] +|=== +| Lane result | Badge message | Colour | Meaning + +| `success` +| `success` +| brightgreen +| The lane ran, passed, and produced its artifact. + +| `failure` +| `failure` +| red +| The lane ran and failed. + +| `skipped` +| `skipped` +| lightgrey +| The lane did not run for this commit. + +| `cancelled` +| `cancelled` +| lightgrey +| The lane started and was cancelled (superseded run, manual cancel, workflow + concurrency). + +| missing/malformed lane artifact +| `unavailable` +| lightgrey +| The lane claims success but its evidence is missing, unreadable or + self-contradictory (for example a `success` message paired with a red + colour). Nothing trustworthy happened, so nothing green is published. + +| anything else (`timed_out`, `neutral`, `action_required`, empty, a value from + a newer GitHub) +| `unknown` +| lightgrey +| Unrecognised. Treated as "no trustworthy result" rather than guessed at. +|=== + +*Grey means "this run did not produce a trustworthy success/failure result".* +It does not mean "pending", "cached" or "about to go green", and it is never a +stale green left behind by an earlier run: a workflow that runs with a skipped, +cancelled or unavailable lane *publishes grey*. + +The badge reflects the *lane job's* result (`needs.k9-validate.result`), with +the lane's artifact as a *witness*: a `success` result is only believed when the +`k9-grid` artifact is present, well formed, byte-identical to the canonical +render, and itself records `success`. An artifact can never turn a failed lane +green; it can only stop a green one being published. + +[NOTE] +==== +*Known limit.* If the whole workflow run is cancelled, GitHub does not start +the publisher at all, so the badge keeps its previous value until the next eligible push +to `main`. No job can publish a state for a run it was not allowed to execute. +Because the run that cancels a superseded one is itself publishing, the badge +converges on the newest run's result rather than staying stuck. +==== + +=== Identical content produces no commit + +Before writing anything, the publisher fetches the file that is currently on the +branch and compares it *byte for byte* with the badge it has just rendered. + +* Identical -> it prints `unchanged`, writes no commit, and exits 0. +* Different -> it publishes with the blob sha it just read, so a concurrent + publication is detected (HTTP 409) and re-evaluated instead of silently + overwriting the other run's commit. Publications are also serialised by a + per-repository `concurrency` group. +* The JSON is never re-serialised or canonicalised to make a comparison + succeed. Key order and spacing are fixed by the renderer, and a file that + differs only in layout *is* a difference: it gets republished. + +=== Missing is not the same as broken + +The publisher distinguishes the two, because treating every API error as +"missing" is how a badge pipeline quietly creates branches it should not: + +[cols="1,2",options="header"] +|=== +| HTTP | What happens + +| `404` on the file, `404` on the ref +| The branch really is absent: create it as an orphan (`parents: []`). + +| `404` on the file, `200` on the ref +| The branch exists but the file does not: add the file, no new branch. + +| `404` on the file, anything else on the ref +| Unknown. Fail the run; do not create a branch. + +| `401` / `403` +| Authentication or permission failure. Fail; do not write. + +| `429` / `5xx`, or a network error +| Transient. Retried a bounded number of times, then fail; nothing is treated + as missing. + +| Any other status +| Fail; do not write. +|=== + +A malformed file that is already on the branch (not JSON, not an object, no +`schemaVersion`, no message) is *refused*, not overwritten: it did not come from +this tool, and silently replacing it would hide a hand edit. Repair or delete it +and re-run. diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 0ab3e57..314015d 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@v4.38.3' - '.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,27 +28,34 @@ 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/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333' + - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' '.github/workflows/governance.yml': - - 'hyperpolymath/standards@94572e612a7bf11f8e6343d19adadf17ef2a3de3' + - '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@94572e612a7bf11f8e6343d19adadf17ef2a3de3' - '.github/workflows/k9-validate.yml': - - 'actions/checkout@v7.0.1' - - 'hyperpolymath/k9-ecosystem@main' + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' + - 'github/codeql-action@9b3254085494b55a0f7c4c2b3d2d52cc712ce370' '.github/workflows/label-triage.yml': [] '.github/workflows/labels.yml': [] '.github/workflows/lean4-manifest.yml': - 'actions/checkout@v7.0.1' '.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@94572e612a7bf11f8e6343d19adadf17ef2a3de3' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/ossf-best-practices.yml': - 'actions/checkout@v7.0.1' '.github/workflows/pages.yml': @@ -61,11 +67,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.1' '.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' @@ -73,30 +83,32 @@ workflows: - 'softprops/action-gh-release@v3.0.4' '.github/workflows/rsr-compliance-canary.yml': - 'actions/checkout@v7.0.1' - '.github/workflows/runtime-policy.yml': - - 'actions/checkout@v7.0.1' '.github/workflows/rust-ci.yml': - - 'hyperpolymath/standards@94572e612a7bf11f8e6343d19adadf17ef2a3de3' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/scorecard.yml': - - 'hyperpolymath/standards@94572e612a7bf11f8e6343d19adadf17ef2a3de3' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'github/codeql-action@9b3254085494b55a0f7c4c2b3d2d52cc712ce370' + - 'ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e' '.github/workflows/secret-scanner.yml': - - 'hyperpolymath/standards@94572e612a7bf11f8e6343d19adadf17ef2a3de3' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/security-policy.yml': - - 'actions/checkout@v7.0.1' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/session-ir.yml': - 'actions/checkout@v7.0.1' - 'julia-actions/setup-julia@v3.0.3' '.github/workflows/sonarqube.yml': - 'actions/checkout@v7.0.1' - 'sonarsource/sonarqube-scan-action@v8.4.1' + '.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' + '.github/workflows/verdict-grid-publish.yml': + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.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' @@ -110,11 +122,21 @@ dependencies: commit: 'sha1-508db95dd578ae2727ebd6217d5ba78e4fbda05d' owner_id: 44036562 repo_id: 760701061 + 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': + ref: 'v6.1.0' + commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + owner_id: 44036562 + repo_id: 215566462 'actions/cache@v6.1.0': ref: 'v6.1.0' commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' owner_id: 44036562 repo_id: 215566462 + 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': + ref: 'v7.0.1' + commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' + owner_id: 44036562 + repo_id: 197814629 'actions/checkout@v7.0.1': ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' @@ -130,16 +152,21 @@ 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 repo_id: 192626254 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f': - ref: 'bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' + ref: 'v7.0.0' commit: 'sha1-bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' owner_id: 44036562 repo_id: 192625955 + 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9': + ref: 'v7.0.2' + commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9' + owner_id: 44036562 + repo_id: 192625955 'actions/upload-artifact@v7.0.2': ref: 'v7.0.2' commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9' @@ -152,24 +179,24 @@ dependencies: repo_id: 496012378 uses: - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' - 'dependabot/fetch-metadata@v3.1.0': + 'dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98': ref: 'v3.1.0' commit: 'sha1-25dd0e34f4fe68f24cc83900b1fe3fe149efef98' owner_id: 27347476 repo_id: 371068214 - 'editorconfig-checker/action-editorconfig-checker@v3.0.0': + 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393': ref: 'v3.0.0' commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' owner_id: 26415196 repo_id: 297874902 - 'erlef/setup-beam@v1.24.1': + 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': ref: 'v1.24.1' commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 - 'github/codeql-action@v4.38.3': - ref: 'v4.38.3' - commit: 'sha1-24c54180a607b1449ed407dd24f251e4e9147c8d' + 'github/codeql-action@9b3254085494b55a0f7c4c2b3d2d52cc712ce370': + ref: 'v4.39.1' + commit: 'sha1-9b3254085494b55a0f7c4c2b3d2d52cc712ce370' owner_id: 9919 repo_id: 259445878 'haskell-actions/setup@v2.12.1': @@ -177,37 +204,22 @@ dependencies: commit: 'sha1-0f8e8c99d88aeb3fbfd523f1ef2c6f762d10d64d' owner_id: 75048950 repo_id: 623796603 - 'hyperpolymath/cicd-suite@55556cf5ba71ba744695861503c13148d3915a5d': - ref: '55556cf5ba71ba744695861503c13148d3915a5d' - commit: 'sha1-55556cf5ba71ba744695861503c13148d3915a5d' - owner_id: 6759885 - repo_id: 1326697643 - 'hyperpolymath/deed-ecosystem@main': - ref: 'main' - commit: 'sha1-eb89a3311e96835cdacad875e52118bed571880d' - owner_id: 6759885 - repo_id: 1275649586 - 'hyperpolymath/k9-ecosystem@main': - ref: 'main' - commit: 'sha1-89f3c2702f4f650a92aa7411502f38da06abd562' - owner_id: 6759885 - repo_id: 1275650185 'hyperpolymath/smtp-notify-action@v0.5.1': ref: 'v0.5.1' commit: 'sha1-e384daf6167a7a7567642d6a62060106244d2413' owner_id: 6759885 repo_id: 1352485172 - 'hyperpolymath/standards@94572e612a7bf11f8e6343d19adadf17ef2a3de3': - ref: '94572e612a7bf11f8e6343d19adadf17ef2a3de3' - commit: 'sha1-94572e612a7bf11f8e6343d19adadf17ef2a3de3' - owner_id: 6759885 - repo_id: 1116521501 'julia-actions/setup-julia@v3.0.3': ref: 'v3.0.3' commit: 'sha1-da908477b5db94c10ffce8cfea9f2b3b9d5db687' owner_id: 53965732 repo_id: 202020219 - 'oven-sh/setup-bun@v2.2.0': + 'ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e': + ref: 'v2.5.0' + commit: 'sha1-fa7117db83c52d787ae5a89f350999701505d78e' + owner_id: 67707773 + repo_id: 421101922 + 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6': ref: 'v2.2.0' commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' owner_id: 108928776 diff --git a/.github/workflows/build-notification.yml b/.github/workflows/build-notification.yml index e43da5b..0df5cc5 100644 --- a/.github/workflows/build-notification.yml +++ b/.github/workflows/build-notification.yml @@ -19,7 +19,7 @@ permissions: contents: read jobs: trigger-boj: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 if: ${{ vars.BOJ_SERVER_URL != '' }} steps: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ebcbfb9..1439940 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,9 +15,10 @@ permissions: contents: read jobs: analyze: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: + actions: read # upload-sarif calls get-a-workflow-run; private repos need this contents: read security-events: write strategy: @@ -37,11 +38,11 @@ jobs: with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@v4.38.3 + 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@v4.38.3 + 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 47cf344..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-latest - timeout-minutes: 15 - - steps: - - name: Checkout repository - uses: actions/checkout@v7.0.1 - - - name: Check for A2ML files - id: detect - run: | - COUNT=$(find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) - echo "count=$COUNT" >> "$GITHUB_OUTPUT" - if [ "$COUNT" -eq 0 ]; then - echo "::warning::No .a2ml/.deed manifest files found. Every RSR repo should have a repo deed (_chora.deed); legacy 0-AI-MANIFEST.a2ml accepted mid-migration — standards #837" - 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 .a2ml/.deed manifest files found.** Every RSR-compliant repo should have a repo deed (`_chora.deed`) at its root. - - Copy it from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo). Manifests are validated against the DEED grammar (standards `1-formats/deed/`); the `.a2ml` → `.deed` extension migration is tracked in standards #837 — the deed validator scans both. - EOF - else - echo "## DEED Manifest Validation" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Scanned **${MANIFEST_COUNT}** .a2ml/.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 31a7b6a..f1487d0 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -1,39 +1,21 @@ # 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, reviews and code-scanning rulesets to pass (`squabble != bypass`). +# Enabling auto-merge does not require submitting an approving review. Keep +# any required review pending for a maintainer instead of depending on the +# repository setting that permits GitHub Actions to approve pull requests. +# The merge request must match the head commit that triggered this run. name: Dependabot Auto-Merge on: @@ -44,84 +26,91 @@ 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: read 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 - runs-on: ubuntu-latest + 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" - is_security=false - [ -n "$GHSA_ID" ] && is_security=true + # 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 - # 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) - if: steps.policy.outputs.action == 'automerge' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_URL: ${{ github.event.pull_request.html_url }} - 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) + 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 + + { + echo "action=$action" + echo "severity=$severity" + echo "reason=$reason" + echo "update_type=$UPDATE_TYPE" + echo "ghsa=$GHSA_ID" + } >> "$GITHUB_OUTPUT" + + - name: Enable auto-merge (when fail-closed policy allows) + id: enable if: steps.policy.outputs.action == 'automerge' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} PR_URL: ${{ github.event.pull_request.html_url }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - gh pr merge --auto --squash "$PR_URL" + gh pr merge --auto --squash --match-head-commit "$PR_HEAD_SHA" "$PR_URL" + - name: Write decision to step summary + if: always() env: ACTION: ${{ steps.policy.outputs.action }} - IS_SECURITY: ${{ steps.policy.outputs.security }} + MERGE_OUTCOME: ${{ steps.enable.outcome }} + 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 +119,10 @@ 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 "| Auto-merge request | \`${MERGE_OUTCOME}\` |" + 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 925af4c..7c476e1 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,111 +22,213 @@ 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 - runs-on: ubuntu-latest - timeout-minutes: 15 - + if: github.event_name != 'pull_request' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 - - - name: Check for manifest files (.a2ml/.deed) - id: detect - run: | - COUNT=$(find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) - echo "count=$COUNT" >> "$GITHUB_OUTPUT" - if [ "$COUNT" -eq 0 ]; then - echo "::warning::No .a2ml/.deed manifest files found. Every RSR repo should have a repo deed (_chora.deed); legacy 0-AI-MANIFEST.a2ml accepted mid-migration — standards #837" - 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: e064539116c2dcfdf282a4749dbd58c69961c7e6 + path: .standards-deed + sparse-checkout: | + 1-formats/deed + scripts + 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 .a2ml/.deed manifest files found.** Every RSR-compliant repo should have a repo deed (`_chora.deed`) at its root. - - Copy it from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo). Manifests are validated against the DEED grammar (standards `1-formats/deed/`); the `.a2ml` → `.deed` extension migration is tracked in standards #837 — the deed validator scans both. - EOF - else - echo "## DEED Manifest Validation" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Scanned **${MANIFEST_COUNT}** .a2ml/.deed manifest(s). See step output for details." >> "$GITHUB_STEP_SUMMARY" + set -euo pipefail + rm -rf "$RUNNER_TEMP/standards-deed" + mv "$PWD/.standards-deed" "$RUNNER_TEMP/standards-deed" + if [ -f .machine_readable/a2ml-legacy-allow.txt ] && [ ! -f "$RUNNER_TEMP/standards-deed/scripts/check-a2ml-archived.sh" ]; then + echo "::error::A2ML ledger present but scripts/check-a2ml-archived.sh is missing from the standards checkout" + exit 1 fi + 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 - runs-on: ubuntu-latest + 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 + 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: e064539116c2dcfdf282a4749dbd58c69961c7e6 + 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: Setup Bun runtime + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: '1.3.12' - - name: Check for K9 files - id: detect + - name: Run K9 validation hook + id: validate 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." + set -euo pipefail + rm -rf "$RUNNER_TEMP/standards-k9" + mv "$PWD/.standards-k9" "$RUNNER_TEMP/standards-k9" + if [ ! -f "$RUNNER_TEMP/standards-k9/1-formats/k9/tools/k9-validate.sh" ]; then + echo "::error::K9 validator is missing from the standards checkout" + exit 1 + fi + if ! command -v nickel >/dev/null 2>&1; then + echo "::error::nickel is required for K9 layer L2 validation" + exit 1 fi + 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 + # The state -> badge mapping lives in scripts/governance-badge.js and + # nowhere else. It used to be this inline jq, which wrote `skipped` and + # `cancelled` in RED. The renderer is shared with the publisher, so the + # artifact and the published badge cannot drift apart. + - name: Generate K9 badge + if: always() + env: + K9_RESULT: ${{ steps.validate.outcome }} + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/k9-grid" + bun scripts/governance-badge.js render \ + --label K9 \ + --state "${K9_RESULT:-unknown}" \ + --out "$RUNNER_TEMP/k9-grid/badge.json" + cat "$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 - - 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 + # The live README badge: badges/k9.json (shields endpoint schema) published to + # the generated `governance-badges` ORPHAN branch on every push to main. + # + # Operational contract (the long form is in .github/workflows/README.adoc): + # * the branch is generated output - no common ancestor with main, no PR, + # never merged. That is why the branch is built with `parents: []` and + # never with main's head; + # * identical bytes produce NO commit; + # * only a `success` lane result may render brightgreen; + # * skipped, cancelled, unavailable and unrecognised states render + # lightgrey. Running with a skipped lane PUBLISHES grey; it must never + # leave a previous green badge in place, which is why this job no longer + # excludes `needs.k9-validate.result == 'skipped'`. + # + # scripts/governance-badge.js owns the whole decision, so it can be unit + # tested without a repository, a token or a network. + badges: + name: Publish the lane badges + needs: k9-validate + if: always() && github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: write + # One publisher at a time per repository. The contents API is ALSO + # sha-checked, so a race that slips past the queue is detected (409) and + # re-evaluated against what the other run wrote instead of blindly + # overwriting it. + concurrency: + group: governance-badges-${{ github.repository }} + cancel-in-progress: false + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - :warning: **No K9 contract files found.** Run `k9iser` to generate contracts. + - name: Setup Bun runtime + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: '1.3.12' + + # Deliberately non-fatal: a missing artifact IS a lane state ("we have no + # evidence"), and failing here would leave the previous badge - possibly + # a green one - in place. The publisher turns it into a grey + # "unavailable" badge and then fails the step, so the run is still red + # and the badge still stops claiming success. + - name: Download the lane grid + continue-on-error: true + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: k9-grid + path: ${{ runner.temp }}/k9-grid - 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 + - name: Publish badges/k9.json to the governance-badges branch + env: + GITHUB_TOKEN: ${{ github.token }} + LANE_RESULT: ${{ needs.k9-validate.result }} + ARTIFACT: ${{ runner.temp }}/k9-grid/badge.json + BRANCH: governance-badges + DEST: badges/k9.json + SOURCE_SHA: ${{ github.sha }} + run: | + set -euo pipefail + bun scripts/governance-badge.js publish \ + --label K9 \ + --lane-result "$LANE_RESULT" \ + --artifact "$ARTIFACT" \ + --branch "$BRANCH" \ + --path "$DEST" \ + --repo "$GITHUB_REPOSITORY" \ + --source-sha "$SOURCE_SHA" # --------------------------------------------------------------------------- # Job 3: Empty-linter — invisible character detection # --------------------------------------------------------------------------- empty-lint: name: "🔴 GATE: Empty-linter (invisible characters)" - runs-on: ubuntu-latest + 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 +275,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" @@ -196,23 +300,20 @@ jobs: # --------------------------------------------------------------------------- groove-check: name: "🟡 CHECK: Groove manifest check" - runs-on: ubuntu-latest + 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 +324,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 +333,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,32 +351,38 @@ 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 # --------------------------------------------------------------------------- eclexiaiser-validate: name: Validate eclexiaiser manifest - runs-on: ubuntu-latest + 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 +391,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 +402,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,250 +417,141 @@ 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 .machine_readable/rsr-profile.a2ml [canon]. - # - # 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. Read-only over the public internet needs no auth. # --------------------------------------------------------------------------- canon-lockstep: name: Canon lockstep - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: contents: read steps: - - name: Fetch the canon's released identity - id: canon + # The decision is scripts/canon-pin-check.sh (tested offline by + # tests/workflows/canon_pin_check_test.sh); these steps only fetch. + # Two-phase lockstep: the spine may adopt a canon AHEAD of its release + # by naming the open canon PR in the clause (:proposed-by + # "metadatastician/standards#N"); the pin must then equal canon.lock at + # that PR's head. Standards Gate A checks the other side. + - name: Checkout repository (for the decision script) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Compare this repo's declared pin with the canon + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + REPO: ${{ github.repository }} + # gh reads the token from the environment, so it never appears in a + # command line (where a curl -H would put it in the process list). + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail + CANON_REPO="metadatastician/standards" LOCK="$RUNNER_TEMP/canon.lock" - URL="https://raw.githubusercontent.com/hyperpolymath/standards/main/canon.lock" + URL="https://raw.githubusercontent.com/${CANON_REPO}/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 } - on { - line=$0; sub(/#.*/,"",line); rec = rec " " line - if (line ~ /}/) { on=0 } - } - END { print rec }' "$LOCK")" - want="$(printf '%s\n' "$rec" \ - | 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 ='." + DEED_NAME="${REPO#*/}_chora.deed" + DEED="$RUNNER_TEMP/repo.deed" + if ! gh api -H "Accept: application/vnd.github.raw" \ + "repos/${REPO}/contents/${DEED_NAME}?ref=${HEAD_SHA}" > "$DEED"; then + echo "::error::no $DEED_NAME at ${HEAD_SHA:0:7} — this repo cannot declare a canon pin" exit 1 fi - echo "want=$want" >> "$GITHUB_OUTPUT" - - - 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, and `uses ⊆ actions.lock` is already - # failing on main, so this job must not make it worse. That means no - # actions/checkout — and therefore NO WORKING TREE. Which is what - # broke this job: it read "machine-readable/rsr-profile.a2ml" from a - # directory that was never populated, so it failed 100% of the time - # with "no rsr-profile.a2ml", and would have whatever the pin said. - # - # Fetch the profile 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 }} - run: | - set -euo pipefail - PROFILE="$RUNNER_TEMP/rsr-profile.a2ml" - fetched="" - for p in ".machine_readable/rsr-profile.a2ml" "machine-readable/rsr-profile.a2ml"; do - URL="https://raw.githubusercontent.com/hyperpolymath/rsr-template-repo/${HEAD_SHA}/${p}" - if curl -fsSL --retry 3 --max-time 30 "$URL" -o "$PROFILE" 2>/dev/null; then - fetched="$p"; break - fi - done - if [ -z "$fetched" ]; then - echo "::error::no rsr-profile.a2ml at ${HEAD_SHA:0:7} — this repo cannot declare a canon pin" - echo "Looked for .machine_readable/rsr-profile.a2ml and machine-readable/rsr-profile.a2ml." + echo "deed: $DEED_NAME @ ${HEAD_SHA:0:7}" + OUT="$RUNNER_TEMP/released.md" + rc=0 + bash scripts/canon-pin-check.sh released "$DEED" "$LOCK" > "$OUT" || rc=$? + { echo "## Canon lockstep"; echo ""; grep -v '^PROPOSED_BY=' "$OUT" || true; echo ""; } >> "$GITHUB_STEP_SUMMARY" + cat "$OUT" + case "$rc" in + 0) echo ":white_check_mark: This template implements the released canon." >> "$GITHUB_STEP_SUMMARY"; exit 0 ;; + 1) echo "::error::canon lockstep broken — the template does not implement the canon at HEAD (adopt it, or name the canon PR with :proposed-by)"; exit 1 ;; + 3) ;; + *) echo "::error::canon lockstep could not compare (malformed pin or lock)"; exit 1 ;; + esac + # Two-phase: the pin names an unreleased canon PR. Check that PR. + ref="$(sed -n 's/^PROPOSED_BY=//p' "$OUT")" + num="${ref##*#}" + PR_JSON="$RUNNER_TEMP/pr.json" + if ! gh api "repos/${CANON_REPO}/pulls/${num}" > "$PR_JSON"; then + echo "::error::could not read ${CANON_REPO}#${num}, which this deed's :proposed-by names" exit 1 fi - echo "profile: $fetched @ ${HEAD_SHA:0:7}" - - # ALL THREE PINS, BOTH SIDES. - # - # This step used to compare only criteria_sha256, while this repo's own - # rsr-profile.a2ml stated that "these values ... equal canon.lock - # [canon.artifacts]" — plural. The claim was wider than the check, so - # `version` and `gates_sha256` could sit at stale values and nothing - # reported it. They did: canon 2.0.1 changed gates_sha256, and this repo - # kept asserting 2.0.0 and the superseded hash. A pin nothing verifies - # is not a binding. See hyperpolymath/standards docs/AUDIT.adoc F8. - # - # Both readers below are the technique the canon's own - # check-canon-lockstep.sh uses, so there is one idiom across both repos. - canon_key() { - awk -v key="$1" ' - /^[[:space:]]*#/ { next } - /^\[/ { f = ($0 == "[canon]") ? 1 : 0; next } - f && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { - sub(/^[^=]*=[[:space:]]*/, ""); gsub(/^["[:space:]]+|["[:space:]]+$/, ""); print; exit - }' "$2" - } - # The artefact records are inline tables spanning lines, so awk JOINS the - # record and grep extracts the hash. Deliberately not anchored to the - # sha256 being on any particular line of the record. - artefact_hash() { - awk -v key="$1" ' - $0 ~ "^" key "[[:space:]]*=" { f = 1 } - f { buf = buf " " $0 } - f && /sha256[[:space:]]*=/ { print buf; exit } - ' "$2" | grep -oE '[0-9a-f]{64}' | head -1 - } - - want_crit="$WANT" - want_gates="$(artefact_hash gates "$RUNNER_TEMP/canon.lock")" - want_ver="$(canon_key version "$RUNNER_TEMP/canon.lock")" - got_crit="$(canon_key criteria_sha256 "$PROFILE")" - got_gates="$(canon_key gates_sha256 "$PROFILE")" - got_ver="$(canon_key version "$PROFILE")" - - # An unreadable pin must not pass. Hashing or parsing nothing yields an - # empty string, and `"" = ""` is a green build with nothing behind it — - # the same class of bug as this job reading a file that was never there. - for pair in "want_crit:$want_crit" "want_gates:$want_gates" "want_ver:$want_ver" \ - "got_crit:$got_crit" "got_gates:$got_gates" "got_ver:$got_ver"; do - if [ -z "${pair#*:}" ]; then - echo "::error::could not read ${pair%%:*} — refusing to compare an empty value" - exit 1 - fi - done - - ok=1 - row() { - mark=":x:" - if [ "$2" = "$3" ]; then mark=":white_check_mark:"; else ok=0; fi - printf '| `%s` | `%s` | `%s` | %s |\n' "$1" "${2:0:16}" "${3:0:16}" "$mark" - } - { - echo "## Canon lockstep" - echo "" - echo "| pin | canon.lock asserts | this repo declares | |" - echo "|---|---|---|---|" - row version "$want_ver" "$got_ver" - row criteria_sha256 "$want_crit" "$got_crit" - 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." + state="$(jq -r '.state' "$PR_JSON")" + [ "$(jq -r '.merged' "$PR_JSON")" = "true" ] && state="merged" + head="$(jq -r '.head.sha' "$PR_JSON")" + if ! printf '%s' "$head" | grep -qxE '[0-9a-f]{40}'; then + echo "::error::${CANON_REPO}#${num} has no readable head commit" + exit 1 + fi + PLOCK="$RUNNER_TEMP/pending.lock" + if ! curl -fsSL --retry 3 --max-time 30 \ + "https://raw.githubusercontent.com/${CANON_REPO}/${head}/canon.lock" -o "$PLOCK"; then + echo "::error::could not fetch canon.lock at ${CANON_REPO}@${head:0:7}" + exit 1 + fi + POUT="$RUNNER_TEMP/pending.md" + prc=0 + bash scripts/canon-pin-check.sh pending "$DEED" "$PLOCK" "$state" > "$POUT" || prc=$? + { echo "### Adopted ahead of release: ${ref} (${state}, head ${head:0:7})"; echo ""; cat "$POUT"; echo ""; } >> "$GITHUB_STEP_SUMMARY" + cat "$POUT" + if [ "$prc" = 0 ]; then + echo ":white_check_mark: This template adopts ${ref} ahead of its release (spine-adopts-then-canon-releases)." >> "$GITHUB_STEP_SUMMARY" 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 \`[canon]\` in" - echo "\`.machine_readable/rsr-profile.a2ml\` to the new hashes and fix any" - echo "criteria this repo now fails, BEFORE the canon release lands." - } >> "$GITHUB_STEP_SUMMARY" + echo "::error::canon lockstep broken — the pin follows ${ref}, which is ${state} or no longer matches" exit 1 + # --------------------------------------------------------------------------- # Job 7: Dogfooding summary # --------------------------------------------------------------------------- dogfood-summary: name: "ℹ️ ADVISORY: Dogfooding compliance summary (non-gating)" - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 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? (.a2ml legacy extension accepted during standards #837 migration) - if find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then - SCORE=$((SCORE + 1)) - DEED_STATUS=":white_check_mark:" + if find . -name '*.deed' -not -path './.git/*' | head -1 | grep -q .; then + 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:" + if grep -rl 'verisimdb\|VeriSimDB' --include='*.toml' --include='*.yaml' --include='*.yml' --include='*.kyaml' --include='*.json' --include='*.rs' --include='*.ex' . 2>/dev/null | head -1 | grep -q .; then + 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 @@ -575,58 +559,60 @@ 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/rsr-template-repo) 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-latest + 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: e064539116c2dcfdf282a4749dbd58c69961c7e6 path: _standards - - uses: actions/checkout@v7.0.1 + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/hypatia + ref: be98f74ce93df200752f3798efa1305bfa3e0e10 path: _hypatia - - name: Setup Elixir for the oracle - uses: erlef/setup-beam@v1.24.1 + persist-credentials: false + # OTP 28.x (28.1 to 28.5.0.8) crashes this step with SIGSEGV in + # sweep_off_heap, called from the JIT's bs_init_bits, on trees whose + # workflow content trips it (rsr-template-repo#333). OTP 27.3 scored all + # of them cleanly. Do not move to 28.x until a pinned 28.x build scores + # the #333 trees (af8665c, a55669e, 6ad5378) with no crash. + - name: Setup Elixir 1.19.4 / OTP 27.3.4.19 for Hypatia oracle + uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 with: elixir-version: '1.19.4' - otp-version: '28.3' + otp-version: '27.3.4.19' + - 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 }}-otp27.3.4.19-elx1194-be98f74ce93df200752f3798efa1305bfa3e0e10 - 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 .. \ - --ssot ../_standards/0-canon/rsr/rsr-criteria-v2.a2ml \ - --write - - name: Show the scorecard - if: always() - run: | - if [ -f .machine_readable/descriptiles/SCORECARD.a2ml ]; then - cat .machine_readable/descriptiles/SCORECARD.a2ml - else - echo "::warning::no scorecard written — the oracle failed; see the step log" - fi - # 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. + mix hypatia.rsr_score ../subject \ + --ssot ../_standards/0-canon/rsr/rsr-criteria-v2.a2ml diff --git a/.github/workflows/dogfood-summary.yml b/.github/workflows/dogfood-summary.yml index 2df5c3d..e0c5cae 100644 --- a/.github/workflows/dogfood-summary.yml +++ b/.github/workflows/dogfood-summary.yml @@ -18,7 +18,7 @@ permissions: jobs: dogfood-summary: name: "Dogfooding compliance summary" - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 if: always() @@ -31,8 +31,8 @@ jobs: SCORE=0 MAX=6 - # DEED manifest present? (.a2ml legacy extension accepted during standards #837 migration) - if find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then + # DEED manifest present? + if find . -name '*.deed' -not -path './.git/*' | head -1 | grep -q .; then SCORE=$((SCORE + 1)) DEED_STATUS=":white_check_mark:" else @@ -64,7 +64,7 @@ jobs: 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 + if grep -rl 'verisimdb\|VeriSimDB' --include='*.toml' --include='*.yaml' --include='*.yml' --include='*.kyaml' --include='*.json' --include='*.rs' --include='*.ex' . 2>/dev/null | head -1 | grep -q .; then SCORE=$((SCORE + 1)) VSDB_STATUS=":white_check_mark:" else diff --git a/.github/workflows/dot-wellknown-enforcement.yml b/.github/workflows/dot-wellknown-enforcement.yml index 7ce166e..0fb8b4a 100644 --- a/.github/workflows/dot-wellknown-enforcement.yml +++ b/.github/workflows/dot-wellknown-enforcement.yml @@ -27,7 +27,7 @@ permissions: contents: read jobs: validate: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read diff --git a/.github/workflows/e2e.yml.template b/.github/workflows/e2e.yml.template index 9842451..92d238d 100644 --- a/.github/workflows/e2e.yml.template +++ b/.github/workflows/e2e.yml.template @@ -58,7 +58,7 @@ jobs: # repos either extend tests/e2e.sh or uncomment a stack block below. e2e: name: E2E — template smoke - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 steps: - name: Checkout @@ -77,7 +77,7 @@ jobs: ## === RUST E2E === # e2e: # name: E2E — Full Pipeline -# runs-on: ubuntu-latest +# runs-on: ubuntu-24.04 # timeout-minutes: 15 # steps: # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -90,7 +90,7 @@ jobs: ## === ZIG FFI E2E === # e2e: # name: E2E — FFI Pipeline -# runs-on: ubuntu-latest +# runs-on: ubuntu-24.04 # timeout-minutes: 15 # steps: # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -103,7 +103,7 @@ jobs: ## === ELIXIR E2E === # e2e: # name: E2E — Full Pipeline -# runs-on: ubuntu-latest +# runs-on: ubuntu-24.04 # timeout-minutes: 15 # steps: # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -114,24 +114,21 @@ jobs: # - run: mix deps.get && mix compile --warnings-as-errors # - run: mix test test/integration/e2e_test.exs --trace -## === DENO/ E2E === +## === BUN E2E === # e2e: # name: E2E — Full Pipeline -# runs-on: ubuntu-latest +# runs-on: ubuntu-24.04 # timeout-minutes: 15 # steps: # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 -# with: -# deno-version: v2.x -# - run: deno install --node-modules-dir=auto -# - run: deno task res:build # ReScript compile -# - run: deno test tests/e2e/ +# - uses: oven-sh/setup-bun@v2.2.0 # resolved by actions.lock +# - run: bun install --frozen-lockfile +# - run: bun test tests/e2e/ ## === PLAYWRIGHT (Browser E2E) === # e2e-playwright: # name: Playwright — ${{ matrix.project }} -# runs-on: ubuntu-latest +# runs-on: ubuntu-24.04 # timeout-minutes: 20 # strategy: # fail-fast: false @@ -139,12 +136,10 @@ jobs: # project: [chromium-1080p, firefox-1080p, webkit-1080p] # steps: # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 -# with: -# deno-version: v2.x -# - run: deno install --node-modules-dir=auto -# - run: npx playwright install --with-deps -# - run: npx playwright test --project=${{ matrix.project }} +# - uses: oven-sh/setup-bun@v2.2.0 # resolved by actions.lock +# - run: bun install --frozen-lockfile +# - run: bunx playwright install --with-deps +# - run: bunx playwright test --project=${{ matrix.project }} # - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 # if: failure() # with: @@ -155,7 +150,7 @@ jobs: ## === HASKELL E2E === # e2e: # name: E2E — Full Pipeline -# runs-on: ubuntu-latest +# runs-on: ubuntu-24.04 # timeout-minutes: 15 # steps: # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -172,7 +167,7 @@ jobs: # aspect-tests: # name: Aspect — Architectural Invariants -# runs-on: ubuntu-latest +# runs-on: ubuntu-24.04 # timeout-minutes: 10 # steps: # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -184,7 +179,7 @@ jobs: ## === RUST BENCH === # benchmarks: # name: Bench — Performance Regression -# runs-on: ubuntu-latest +# runs-on: ubuntu-24.04 # timeout-minutes: 15 # steps: # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -201,7 +196,7 @@ jobs: ## === ZIG BENCH === # benchmarks: # name: Bench — Performance Regression -# runs-on: ubuntu-latest +# runs-on: ubuntu-24.04 # timeout-minutes: 15 # steps: # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -215,7 +210,7 @@ jobs: # readiness: # name: Readiness — Grade D/C/B -# runs-on: ubuntu-latest +# runs-on: ubuntu-24.04 # timeout-minutes: 10 # steps: # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/.github/workflows/eclexiaiser-validate.yml b/.github/workflows/eclexiaiser-validate.yml index aae7481..e0e5e25 100644 --- a/.github/workflows/eclexiaiser-validate.yml +++ b/.github/workflows/eclexiaiser-validate.yml @@ -18,12 +18,14 @@ permissions: jobs: eclexiaiser-validate: name: "🟡 CHECK: Validate eclexiaiser manifest" - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 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 7bdf237..dcb890d 100644 --- a/.github/workflows/empty-linter.yml +++ b/.github/workflows/empty-linter.yml @@ -18,12 +18,14 @@ permissions: jobs: empty-lint: name: "🔴 GATE: Empty-linter (invisible characters)" - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 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 9dd2741..c94f510 100644 --- a/.github/workflows/estate-rules.yml +++ b/.github/workflows/estate-rules.yml @@ -23,11 +23,16 @@ permissions: contents: read jobs: estate-rules: - runs-on: ubuntu-latest - timeout-minutes: 15 + runs-on: ubuntu-24.04 + # Measured 2026-11-10 over the last 30 runs: passing jobs took 10m50s to + # 14m54s, and 10 jobs were killed at the old 15-minute limit while still + # running. 30 minutes is about twice the slowest pass. + timeout-minutes: 30 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. @@ -66,6 +71,15 @@ jobs: # Was written but wired to nothing, so it had never run in CI. - name: V-language gate self-test run: bash tests/workflows/check_no_vlang_test.sh + # .a2ml is retired estate-wide: facts are deed clauses, contracts are .k9. + - name: No .a2ml files + run: bash scripts/check-no-a2ml.sh . + - name: No-a2ml gate self-test (planted positives must fail) + run: bash tests/workflows/check_no_a2ml_test.sh + - name: Deed clause writer controls + run: bash tests/workflows/deed_clause_test.sh + - name: Coaptation families match the deed's descriptile clauses + run: bash tests/workflows/coaptation_families_test.sh - name: Install verified Nickel 1.17.0 run: | mkdir -p "$RUNNER_TEMP/nickel-bin" @@ -83,3 +97,219 @@ jobs: run: bash tests/workflows/session_contracts_test.sh - name: Verify mint cleanup controls run: bash tests/workflows/mint_cleanup_test.sh + - name: Self-name pass keeps issue references, provenance and detectors + run: bash tests/workflows/self_name_pass_test.sh + # The automated mint path (RSR_NON_INTERACTIVE) never completed, and no + # workflow exercised repo-init at all: tests/e2e runs nowhere in CI. + - name: Install verified just 1.43.0 + run: | + mkdir -p "$RUNNER_TEMP/just-bin" + curl --proto '=https' --proto-redir '=https' --fail --silent --show-error --location \ + https://github.com/casey/just/releases/download/1.43.0/just-1.43.0-x86_64-unknown-linux-musl.tar.gz \ + --output "$RUNNER_TEMP/just.tgz" + echo "a1bc93654f31669fd964ea3011a5e5e9676b9b6f8adcd762606e5140632ea72d $RUNNER_TEMP/just.tgz" | sha256sum --check --strict + tar -xzf "$RUNNER_TEMP/just.tgz" -C "$RUNNER_TEMP/just-bin" just + echo "$RUNNER_TEMP/just-bin" >> "$GITHUB_PATH" + # Installed before the mint steps: repo-init and container-init reformat the + # container KYAML they fill (scripts/format-container-kyaml.sh), which needs + # a yq that can write KYAML. The KYAML ratchet below uses the same install. + - 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/e064539116c2dcfdf282a4749dbd58c69961c7e6/scripts/kyaml-format.sh \ + --output "$RUNNER_TEMP/kyaml-format.sh" + echo "5fddd58fc8cbd2b68766ef5b4b5ba39f299da3f27252ae6cdcff0a641bbaef18 $RUNNER_TEMP/kyaml-format.sh" | sha256sum --check --strict + - name: Non-interactive mint resolves every prompt from the environment + run: bash tests/workflows/non_interactive_mint_test.sh + # repo-init recorded the CHILD's own HEAD as the template commit whenever + # a repo was created with "Use this template" (metadatastician/lintel). + - name: Template pin resolves by tree equality (controls) + run: bash tests/workflows/resolve_template_pin_test.sh + # A replay trusts RSR_PACKAGE_UUID; only recorded answers may supply it. + - name: Replay passes a package uuid only from recorded answers + run: bash tests/workflows/replay_mint_env_test.sh + # chain-mint reads checkouts through git: gitlinks kept, no ignored file read. + - name: Chain snapshots keep gitlinks and read no ignored file + run: bash tests/workflows/chain_tree_of_test.sh + # The repository map is line-based: a path holding a newline is refused by name. + - name: Repository map refuses a path holding a newline + run: bash tests/workflows/repo_map_newline_test.sh + # Allowlist comments reach the map whole: an apostrophe or quote once cut them. + - name: Repository map keeps allowlist comments whole + run: bash tests/workflows/repo_map_quotes_test.sh + # The repo uuid is derived the same on every machine (sha1sum, shasum or + # openssl), equal to uuidgen --sha1; with no SHA-1 tool it is refused. + - name: Repo uuid derivation is machine-independent + run: bash tests/workflows/uuid5_url_test.sh + # A divergent legacy .well-known/ file is quarantined under the MINT date, + # so a replay on a later day names it the same. + - name: Legacy .well-known quarantine is named by the mint date + run: bash tests/workflows/migrate_wellknown_date_test.sh + # The first CI job to run a FULL mint: a Use-this-template child, checked + # for provenance, untouched fixtures and ignored files, the map and the + # closing advice. A fresh mint of main aborted on 2026-10-20 and nothing + # here noticed. uuidgen is util-linux; installed only if the image lacks it. + - name: Use-this-template mint end to end + run: | + command -v uuidgen >/dev/null || { sudo apt-get update -q && sudo apt-get install -y -q uuid-runtime; } + bash tests/e2e/use_template_mint_test.sh + # A repo made from a MINTED repo: its parent's mint is replayed, the + # template is minted with the new answers, and the parent's own work is + # carried by a three-way merge (Codex review of #251; owner ruling). + - name: Chain mint (a minted repo minted again) end to end + run: bash tests/e2e/chain_mint_test.sh + # An archetype overlay once shipped its own actions.lock, which replaced + # the spine's at mint and left every Julia mint with Lock Sync red. + - name: Archetype lock fragments merge into a valid lock + run: bash tests/workflows/archetype_lock_merge_test.sh + # A campaign over a file with no {{TOKEN}}s once exited at that file. + - name: Campaign dry run finishes over a tokenless path + run: bash tests/workflows/rsr_campaign_tokenless_test.sh + # The CHECK half of the template contract. repo-init has always written an + # answer-file (now the child deed's (provenance) clause); nothing ever read it. + # This is the missing reader, and it covers the #200/#201/#203 class: + # self-parenting identity, unresolved pins, and leaked template branches. + # + # The template itself self-skips (archetypes/ present), so the real check + # runs in minted children. What THIS proves is that the instrument can + # fail — the only property that matters for a gate, and the one that was + # missing in #49, #64 and the conformance gate that "reported them + # 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 + # The README badge is generated output on an orphan branch. This proves + # the state -> colour table (grey, never a stale green), the byte-equal + # no-op, the orphan commit, the API-error classification and the workflow + # wiring - against a mock API, so no experiment is pushed to any branch. + # Behaviour tests run under the same Bun the step above installed, with a + # Node fallback; the wiring checks also run against a mutated workflow so + # they are proven able to fail. + - name: Governance badge contract (state mapping, no-op, orphan, errors) + run: bash tests/workflows/governance_badge_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. + # 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 . + # ── The scanner/reporting pipeline's own rules ──────────────────────── + # docs/HYPATIA-RULE-ORDER.adoc states the exemption layers, the clashes + # between them and what resolves each. These four steps are the parts of + # that document a machine can check; the rest is stated there as review. + - name: Hypatia exemption meta-rules (.hypatia-ignore) + run: | + set -euo pipefail + bash tests/workflows/hypatia_ignore_test.sh + bash scripts/check-hypatia-ignore.sh . + # The layer order, the ownership table and the clash resolutions are a + # contract (.machine_readable/policies/hypatia-rule-order.json), not a + # description: this step refuses a duplicate or gapped rank, an owner that + # does not exist, an unclassified exemption, and any pin that disagrees + # with the workflow that uses it. The suite proves each refusal fires. + - name: Hypatia rule order & clash resolution + run: | + set -euo pipefail + bash tests/workflows/hypatia_rule_order_test.sh + bash scripts/check-hypatia-rule-order.sh . + - name: Workflow edges (producer -> verdict grid) resolve + run: | + set -euo pipefail + bash tests/workflows/workflow_edges_test.sh + bash scripts/check-workflow-edges.sh . + # The grid itself is rendered by the pinned standards renderer, which the + # tests stub: this proves the lane's evidence collection, digest refusal, + # orange-not-green fallback and one-comment-per-PR behaviour, and that the + # workflow wiring is what the checks expect (mutated copies included). + - name: Verdict grid lane (collect, verify, render, comment) + run: bash tests/workflows/verdict_grid_test.sh + - 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 . + # The three scanner contexts, their least privilege and their pinning are + # this repository's own: a job-level reusable wrapper would make them the + # CALLEE's job names, and `secrets: inherit` would forward every secret to + # a scanner that declares none. Every property is mutated in the test. + - name: Secret scanner contract (three stable contexts) + run: bash tests/workflows/secret_scanner_contract_test.sh + # Behaviour and negative controls for the inline fixes to governance, + # hypatia-scan, main-estate-audit, rust-ci and secret-scanner. + - name: Inline workflow gap fixes (behaviour + negative controls) + run: bash tests/workflows/workflow_gap_fixes_test.sh diff --git a/.github/workflows/fork-governance.yml b/.github/workflows/fork-governance.yml new file mode 100644 index 0000000..b4d08bf --- /dev/null +++ b/.github/workflows/fork-governance.yml @@ -0,0 +1,501 @@ +# 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, the gitbot-fleet report-backs, and THE Hypatia verdict grid. +# Everything is pinned to metadatastician/standards@e064539116c2dcfdf282a4749dbd58c69961c7e6. +# +# ── HOW THIS FILE RELATES TO THE SCANNERS (read before editing) ────────────── +# +# The estate used to run scanners in three places with no statement of which +# one owned what, so the same Hypatia binary was built twice per pull request, +# two different jobs gated on "critical", and the verdict grid this file claimed +# to feed was produced nowhere. The layering is now explicitly: +# +# 1. PRODUCERS — each scanner runs in exactly one workflow: +# .github/workflows/hypatia-scan.yml -> artifact `hypatia-scan-findings` +# (Hypatia JSON, relativised; +# SARIF `category: hypatia`) +# .github/workflows/static-analysis-gate.yml -> artifact `static-analysis-findings` +# (panic-attack and pons status +# envelopes, patch bridge, unified) +# A producer states its own gate. Hypatia gates on critical; panic-attack +# gates on critical; pons is advisory. No other workflow repeats those +# decisions: this file never fails on a finding, so a pull request cannot be +# blocked twice for one finding, and no gate is suppressed here either — +# the producer's own check still goes red. +# +# 2. REPORTER — this workflow. It does not scan anything. It collects the +# producers' artifacts for one commit, renders the findings-family verdict +# grid (metadatastician/standards docs/VERDICT-GRID-SPEC.adoc) over Hypatia, +# panic-attack, pons and every enrolled gitbot-fleet member, uploads it as +# `verdict-grid`, and writes a job summary with read-only permissions. +# +# 3. SEQUENCING — this workflow runs on `pull_request` and `workflow_dispatch`. +# A pull-request run may render an orange row for a scanner that has not +# reported yet (orange, never green). verdict-grid-publish.yml runs on +# `workflow_run` when a producer finishes, re-renders with a trusted +# default-branch lane script, and maintains one comment per pull request. +# +# Two deliberate non-decisions: +# +# * A failing `fleet` job does NOT fail the verdict. Its rows go orange (no +# report-back evidence) and the `Fleet report-backs` check goes red, which is +# where a fleet failure is visible. Failing the verdict as well would report +# one failure twice and would block a pull request on infrastructure rather +# than on evidence about the pull request. The previous wiring reported the +# fleet result in a table and then ignored it; this comment is the statement +# that replaces that silence. +# * The grid is not a gate. Its state is published (job summary, artifact, +# comment) and the gating decisions stay with the producers. The spec's +# §Non-goals makes the same point: a grid is worst-of reporting, not a score. +name: Fork governance + +on: + pull_request: + workflow_dispatch: + +concurrency: + # Group by workflow and github.ref, keeping each pull request's runs separate. + group: ${{ github.workflow }}-${{ github.ref }} + # The newer run re-collects the producers' artifacts for this ref. + cancel-in-progress: true + +permissions: + contents: read + +env: + # The merge revision scanned by the fleet and producers, or the dispatch commit. + TARGET_SHA: ${{ github.sha }} + # Actions run discovery filters by the PR head rather than the merge revision. + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + # A manual dispatch has no pull request; its grid stays in the summary/artifact. + TARGET_PR: ${{ github.event.pull_request.number || '' }} + +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: e064539116c2dcfdf282a4749dbd58c69961c7e6 + path: .standards-deed + sparse-checkout: | + 1-formats/deed + scripts + 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 + env: + BASE_REF: ${{ github.base_ref }} + TOKEN: ${{ github.token }} + run: | + set -euo pipefail + STANDARDS_ABS="$PWD/.standards-deed" + rm -rf "$RUNNER_TEMP/standards-deed" + mv "$STANDARDS_ABS" "$RUNNER_TEMP/standards-deed" + # The lane is the gate: a ledger with no canonical gate to read it fails. + if [ -f .machine_readable/a2ml-legacy-allow.txt ] && [ ! -f "$RUNNER_TEMP/standards-deed/scripts/check-a2ml-archived.sh" ]; then + echo "::error::A2ML ledger present but scripts/check-a2ml-archived.sh is missing from the standards checkout" + exit 1 + fi + # Shrink-only: the base branch's ledger bounds this one. A base that + # cannot be fetched fails the step: skipping the comparison would let + # a new ledger entry through exactly when it cannot be checked. + A2ML_BASE="" + if [ -n "$BASE_REF" ]; then + # Authenticate the fetch. The checkout above sets persist-credentials: false, + # so an anonymous fetch works only for public repositories; this gate must + # also run on private ones. The header form keeps the token out of .git/config + # (the same pattern as lock-sync-gate.yml). + AUTH="AUTHORIZATION: basic $(printf 'x-access-token:%s' "${TOKEN}" | base64 -w0)" + if ! git -c http.extraheader="${AUTH}" fetch -q --depth=1 origin "$BASE_REF"; then + echo "::error::could not fetch the base branch $BASE_REF to bound the A2ML ledger" + exit 1 + fi + A2ML_BASE=FETCH_HEAD + fi + A2ML_BASE="$A2ML_BASE" 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: e064539116c2dcfdf282a4749dbd58c69961c7e6 + 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" + if [ ! -f "$RUNNER_TEMP/standards-k9/1-formats/k9/tools/k9-validate.sh" ]; then + echo "::error::K9 validator is missing from the standards checkout" + exit 1 + fi + if ! command -v nickel >/dev/null 2>&1; then + echo "::error::nickel is required for K9 layer L2 validation" + exit 1 + fi + STANDARDS_DIR="$RUNNER_TEMP/standards-k9" bash .github/hooks/validate-k9.sh + + # Fleet report-backs: the evidence behind each gitbot-fleet row of the verdict + # grid (.gitbot-fleet/enrolment.json says which members apply). Inline rather + # than `uses:`, because a repository instantiated from this template may live + # under an owner whose Actions policy refuses a reusable workflow from + # hyperomath at plan time (zero jobs, no error line). Checking out another + # repository is plain git. The build and run logic lives in the two + # gitbot-fleet scripts; standards 3-practice/fork-governance/ADOPTION.adoc §8 + # is the guide. + # + # The artifact name is what the verdict grid reads, and what + # hypatia-scan-reusable.yml downloads by default (`report-backs-artifact`), so + # a repository that later adopts the standards reusable needs no change here. + fleet: + name: Fleet report-backs + runs-on: ubuntu-24.04 + # Worst case, so a stalled phase never kills the job before the upload: the + # 2400 s build budget + seven adapters at 300 s + checkout, cache and upload. + timeout-minutes: 85 + permissions: + contents: read + env: + # hyperpolymath/gitbot-fleet commit whose driver and adapters run. + FLEET_REF: e44af8144789594fcce891ccc7b77fc9a464a520 + # panic-attack commit for panicbot. Deliberately the SAME commit + # static-analysis-gate.yml builds: panicbot is not enrolled today (the + # grid's own panic-attack row already grades that scanner, so a second + # row would report one tool twice), and if it ever is, both rows must + # describe the same binary or the grid would compare two tools and call + # it one. The standards Hypatia reusable pins b689a74 for its own row; + # scripts/check-workflow-edges.sh reports that divergence rather than + # letting it hide. + PANIC_ATTACK_SHA: b5530834a839734d1d181bfe6edb0d43d4233cf4 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Checkout gitbot-fleet at the pinned commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/gitbot-fleet + ref: ${{ env.FLEET_REF }} + path: .fleet-src + persist-credentials: false + + # The fleet checkout must not sit inside the tree being reported on: walk- + # based adapters would scan the fleet's own source and attribute its findings + # to this repository. actions/checkout only writes under the workspace, so + # the checkout is moved out to RUNNER_TEMP first; the lockfile hash for the + # cache key is taken before the move. + - name: Move the fleet checkout out of the scanned tree + id: fleet + run: | + set -euo pipefail + lockhash=$(cat .fleet-src/bots/*/Cargo.lock .fleet-src/shared-context/Cargo.lock 2>/dev/null | sha256sum | cut -c1-16) + mv .fleet-src "$RUNNER_TEMP/fleet-src" + echo "dir=$RUNNER_TEMP/fleet-src" >> "$GITHUB_OUTPUT" + echo "lockhash=$lockhash" >> "$GITHUB_OUTPUT" + + - name: Plan which adapters to build + id: plan + run: | + set -euo pipefail + plan=$(bash "$RUNNER_TEMP/fleet-src/scripts/fleet-report-back.sh" --plan --enrolment .gitbot-fleet/enrolment.json) + echo "plan:"; printf '%s\n' "$plan" | sed 's/^/ /' + crates=$(printf '%s\n' "$plan" | awk -F'\t' '$2 != "" {print $2}' | sort -u | tr '\n' ' ') + echo "crates=${crates% }" >> "$GITHUB_OUTPUT" + if printf '%s\n' "$plan" | awk -F'\t' '$1 == "panicbot" {f=1} END {exit !f}'; then echo panicbot=true; else echo panicbot=false; fi >> "$GITHUB_OUTPUT" + { echo "workspaces< target"; done; echo "$RUNNER_TEMP/fleet-src/shared-context -> target"; echo WS; } >> "$GITHUB_OUTPUT" + if [ -n "${crates% }" ]; then echo build=true; else echo build=false; fi >> "$GITHUB_OUTPUT" + + # Rust comes with the GitHub-hosted runner image; the cache is actions/cache + # keyed on the fleet commit's lockfiles. Only GitHub-owned, SHA-pinned + # actions appear in this job, as in gitbot-fleet's reusable. + - name: Cache adapter builds + if: steps.plan.outputs.build == 'true' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + ${{ runner.temp }}/fleet-src/bots/*/target + key: fleet-report-back-v3-${{ runner.os }}-${{ runner.arch }}-${{ steps.fleet.outputs.lockhash }}-${{ steps.plan.outputs.crates }} + restore-keys: | + fleet-report-back-v3-${{ runner.os }}-${{ runner.arch }}-${{ steps.fleet.outputs.lockhash }}- + fleet-report-back-v3-${{ runner.os }}-${{ runner.arch }}- + + - name: Report the Rust toolchain in use + if: steps.plan.outputs.build == 'true' || steps.plan.outputs.panicbot == 'true' + run: | + set -euo pipefail + command -v cargo >/dev/null || { echo "::error::cargo is not on this runner; the adapters cannot be built"; exit 1; } + cargo --version; rustc --version + + - name: Restore panic-attack build cache + if: steps.plan.outputs.panicbot == 'true' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.panic-attack + key: panic-attack-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.PANIC_ATTACK_SHA }} + + - name: Build the enrolled adapters + if: steps.plan.outputs.build == 'true' || steps.plan.outputs.panicbot == 'true' + env: + CRATES: ${{ steps.plan.outputs.crates }} + PANICBOT: ${{ steps.plan.outputs.panicbot }} + run: | + set -euo pipefail + args=(--fleet "$RUNNER_TEMP/fleet-src" --bin-dir "$RUNNER_TEMP/fleet-bin" --crates "$CRATES") + [ "$PANICBOT" = true ] && args+=(--panic-attack "$PANIC_ATTACK_SHA") + bash "$RUNNER_TEMP/fleet-src/scripts/fleet-report-back-build.sh" "${args[@]}" + + - name: Run the enrolled adapters and write the envelopes + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/fleet-bin" "$RUNNER_TEMP/report-backs" + bash "$RUNNER_TEMP/fleet-src/scripts/fleet-report-back.sh" --repo . --out "$RUNNER_TEMP/report-backs" \ + --bin-dir "$RUNNER_TEMP/fleet-bin" --enrolment .gitbot-fleet/enrolment.json \ + --fleet-ref "$FLEET_REF" --summary "$GITHUB_STEP_SUMMARY" + ls "$RUNNER_TEMP/report-backs"/*.json >/dev/null 2>&1 \ + || echo "No member is enrolled in .gitbot-fleet/enrolment.json." > "$RUNNER_TEMP/report-backs/NONE-ENROLLED.txt" + + - name: Upload the report-backs + if: ${{ always() }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: fleet-report-backs + path: ${{ runner.temp }}/report-backs + if-no-files-found: error + retention-days: 90 + + # Render and upload the PR-time grid with read-only permissions: actions for + # artifacts, contents for checkout, pull-requests for the previous comment. + # verdict-grid-publish.yml owns comment publication. + verdict: + name: Fork governance verdict + needs: [deed, k9, fleet] + if: always() + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + actions: read + contents: read + pull-requests: read + env: + DEED_RESULT: ${{ needs.deed.result }} + K9_RESULT: ${{ needs.k9.result }} + FLEET_RESULT: ${{ needs.fleet.result }} + steps: + # This job is read-only (actions/contents/pull-requests: read). It runs + # the merge-revision lane script, which is untrusted, so it must never receive a + # write-scoped token. Publishing lives in verdict-grid-publish.yml, which + # checks out the default branch. + - name: Checkout the scanned revision's lane script (read-only token) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ env.TARGET_SHA }} + persist-credentials: false + sparse-checkout: | + scripts/verdict-grid-lane.sh + .gitbot-fleet + sparse-checkout-cone-mode: false + + # The renderer is not copied into this repository: it is fetched from + # standards at the pinned commit and verified against a sha256 recorded in + # scripts/verdict-grid-lane.sh. A copy would be a second implementation of + # the estate's grid that could drift from the tested one. + - name: Checkout the verdict grid renderer at the pinned standards commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: metadatastician/standards + ref: e064539116c2dcfdf282a4749dbd58c69961c7e6 + path: .standards-grid + persist-credentials: false + sparse-checkout: | + scripts/verdict-grid.sh + scripts/lib/verdict-grid.jq + sparse-checkout-cone-mode: false + + # Discover producer runs by PR_HEAD_SHA; their default checkouts scan the + # merge revision reported as TARGET_SHA. A scanner with no run, or a run with + # no artifact, is recorded with its reason and renders orange — this step + # never turns "not yet" into "clean". + - name: Collect the producers' evidence + id: collect + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + bash scripts/verdict-grid-lane.sh collect \ + --repo "$GITHUB_REPOSITORY" \ + --sha "$PR_HEAD_SHA" \ + --out "$RUNNER_TEMP/evidence" \ + --source hypatia-scan.yml:hypatia-scan-findings \ + --source static-analysis-gate.yml:static-analysis-findings + + # The fleet's report-backs come from THIS run (the fleet job above), so a + # same-run download is enough. Missing evidence is not fatal here: an + # enrolled member without an envelope renders orange, which is the honest + # reading and the reason the grid can be trusted when it is green. + - name: Download the fleet report-backs + continue-on-error: true + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: fleet-report-backs + path: ${{ runner.temp }}/evidence/fleet-report-backs + + # The delta column compares against the grid this lane posted last time. + # Best effort: a missing previous comment only means "no comparable + # baseline", which the renderer says explicitly. + - name: Read the previous verdict grid + id: previous + if: ${{ always() && env.TARGET_PR != '' }} + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -uo pipefail + # An unreadable previous comment is not a failure of this run: the grid + # then says "— no comparable baseline", which is true. Written as an + # explicit branch rather than `cmd || :`, because a swallowed non-zero + # exit is exactly what RE005 exists to flag. + if ! bash scripts/verdict-grid-lane.sh previous \ + --repo "$GITHUB_REPOSITORY" --pr "$TARGET_PR" \ + --out "$RUNNER_TEMP/previous.md"; then + echo "no previous verdict grid could be read; the delta column will say so" + : > "$RUNNER_TEMP/previous.md" + fi + + - name: Check the evidence shapes before rendering + if: ${{ always() }} + run: | + set -euo pipefail + bash scripts/verdict-grid-lane.sh validate \ + --hypatia "$RUNNER_TEMP/evidence/hypatia-scan-findings/hypatia-findings.relativized.json" \ + --panic "$RUNNER_TEMP/evidence/static-analysis-findings/panic-attack-findings.json" \ + --pons "$RUNNER_TEMP/evidence/static-analysis-findings/pons-findings.json" \ + --enrolment .gitbot-fleet/enrolment.json + + - name: Render the verdict grid + id: grid + if: ${{ always() }} + env: + PREVIOUS: ${{ runner.temp }}/previous.md + run: | + set -euo pipefail + ev="$RUNNER_TEMP/evidence" + args=( + render --out "$RUNNER_TEMP/verdict-grid" + --renderer-dir .standards-grid + --repo "$GITHUB_REPOSITORY" + --sha "$TARGET_SHA" --run "$GITHUB_RUN_ID" --pr "${TARGET_PR:-local}" + --url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + --enrolment .gitbot-fleet/enrolment.json + --hypatia "$ev/hypatia-scan-findings/hypatia-findings.relativized.json" + --panic "$ev/static-analysis-findings/panic-attack-findings.json" + --pons "$ev/static-analysis-findings/pons-findings.json" + --hypatia-ref be98f74ce93df200752f3798efa1305bfa3e0e10 + --panic-ref b5530834a839734d1d181bfe6edb0d43d4233cf4 + --pons-ref 51b8920f541782ceb9465ea3f0d33c96104fc1fb + ) + [ -d "$ev/fleet-report-backs" ] && args+=(--report-backs "$ev/fleet-report-backs") + [ -s "$PREVIOUS" ] && args+=(--previous "$PREVIOUS") + bash scripts/verdict-grid-lane.sh "${args[@]}" + + - name: Upload the verdict grid + if: ${{ always() }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: verdict-grid + path: ${{ runner.temp }}/verdict-grid + if-no-files-found: error + retention-days: 90 + + # Publishing is deliberately handled by a separate default-branch + # workflow_run consumer. This PR-time workflow is read-only, so its + # untrusted head scripts never receive a write-scoped token. + + - name: Summarise the lanes and gate on conformance only + if: ${{ always() }} + env: + GRID_STATE: ${{ steps.grid.outputs.state }} + BADGE: ${{ steps.grid.outputs.badge }} + run: | + set -euo pipefail + { + echo "## Fork governance" + echo "" + echo "PR head (producer run lookup): \`$PR_HEAD_SHA\`" + echo "Scanned revision (merge revision for PRs): \`$TARGET_SHA\`" + echo "" + echo "| Lane | Result | Gates a merge? |" + echo "|------|--------|----------------|" + echo "| DEED syntax | \`${DEED_RESULT}\` | yes |" + echo "| K9-DEED conformance (L0–L2) | \`${K9_RESULT}\` | yes |" + echo "| Fleet report-backs (evidence, not a lane) | \`${FLEET_RESULT}\` | no — a failed fleet is orange rows, not a blocked PR |" + echo "| Verdict grid | \`${GRID_STATE:-missing}\` | no — see the producers for each scanner's gate |" + echo "" + echo "The grid itself is in the \`verdict-grid\` artifact and, on a pull request, in the" + echo "comment maintained by verdict-grid-publish.yml. A grid state of red or purple is reporting: Hypatia" + echo "and panic-attack gate on critical in their own workflows." + } >> "$GITHUB_STEP_SUMMARY" + failed="" + [ "$DEED_RESULT" = "success" ] || failed="${failed} deed=${DEED_RESULT}" + [ "$K9_RESULT" = "success" ] || failed="${failed} k9=${K9_RESULT}" + if [ -n "$failed" ]; then + echo "::error::Fork governance failed (${failed# })" + exit 1 + fi + if [ -z "${GRID_STATE:-}" ]; then + echo "::warning::No verdict grid state was rendered; see the Render step above." + fi diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 988d609..bbf6f84 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -6,16 +6,68 @@ 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@e064539116c2dcfdf282a4749dbd58c69961c7e6 +# (replacing the retired runtime-policy.yml workflow per PR #244). jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@94572e612a7bf11f8e6343d19adadf17ef2a3de3 + name: Estate Governance Policy + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + env: + STANDARDS_PIN: 'e064539116c2dcfdf282a4749dbd58c69961c7e6' + 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: e064539116c2dcfdf282a4749dbd58c69961c7e6 + 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 + # The pinned script is the sole TypeScript verdict: it honours the + # builtin allowlist, .claude/CLAUDE.md exemptions and + # .governance-allowlist. A second inline filter here would disagree + # with it. A missing script is a failure, never a silent skip. + test -f .standards-checkout/scripts/check-ts-allowlist.sh || { + echo "::error::Pinned check-ts-allowlist.sh missing at $STANDARDS_PIN" + exit 1 + } + bash .standards-checkout/scripts/check-ts-allowlist.sh + rm -rf .standards-checkout + + 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 f0e8785..605d365 100644 --- a/.github/workflows/groove-check.yml +++ b/.github/workflows/groove-check.yml @@ -18,12 +18,14 @@ permissions: jobs: groove-check: name: "🟡 CHECK: Groove manifest check" - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 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/guix-policy.yml b/.github/workflows/guix-policy.yml index 6363018..d6ac275 100644 --- a/.github/workflows/guix-policy.yml +++ b/.github/workflows/guix-policy.yml @@ -16,7 +16,7 @@ permissions: contents: read jobs: check: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index b107f98..f9a779f 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -1,18 +1,34 @@ # 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. +# +# This is the ONLY workflow that runs Hypatia (static-analysis-gate.yml used to +# run it a second time at the same pin, with a second cache, its own duplicate +# "fail on critical" gate and a differently-shaped output). See the header of +# fork-governance.yml for the layering. +# +# Two outputs matter downstream: +# * the SARIF, for code scanning; +# * `hypatia-findings.relativized.json` in the `hypatia-scan-findings` +# artifact, which the Fork governance verdict grid reads. The artifact name +# matches hypatia-scan-reusable.yml's, so adopting that reusable later needs +# no change in fork-governance.yml. +# +# `name:` is a cross-workflow EDGE: verdict-grid-publish.yml names "Hypatia +# Security Scan" in its `workflow_run.workflows`. Renaming this workflow without +# renaming it there silently stops the grid re-rendering; +# scripts/check-workflow-edges.sh fails the CICD Squabbler gate when the two +# disagree. 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,18 +36,147 @@ 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: - # secrets: inherit is required so HYPATIA_SCAN_PAT (org) reaches the - # reusable. It does not, and cannot, cure a Hypatia *scanner build* - # failure: this caller does not override hypatia-ref, so the reusable builds - # the scanner from its pinned default commit, not HEAD (mix escript.build). - # Scanner build failures are upstream failures, not consumer findings. - # See docs/status/HYPATIA-GATE.adoc. - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@94572e612a7bf11f8e6343d19adadf17ef2a3de3 - secrets: inherit + name: Hypatia Neurosymbolic Analysis + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + actions: read # upload-sarif calls get-a-workflow-run; private repos need this + contents: read + security-events: write + vulnerability-alerts: read # Dependabot alerts, for Hypatia's DA rules + env: + HYPATIA_SHA: 'be98f74ce93df200752f3798efa1305bfa3e0e10' + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + # OTP 28.5.0.8 crashes this job with SIGSEGV (exit 139 in hypatia-cli.sh + # scan) on some scanned trees, deterministically per tree + # (rsr-template-repo#339: 10 of 10 runs on #338's head, 0 of 10 on + # main's); OTP 27.3.4.19 scanned both trees cleanly (0 of 10 each). + # Same VM bug as the RSR oracle (#333, #335). Do not move to 28.x until a + # pinned 28.x build scans #338's head 7821940 with no crash. + - name: Setup Elixir 1.19.4 / OTP 27.3.4.19 for Hypatia scanner + uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + elixir-version: '1.19.4' + otp-version: '27.3.4.19' + + - 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 }}-otp27.3.4.19-elx1194-${{ env.HYPATIA_SHA }} + + - 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 + env: + # The API-backed rules (code-scanning, Dependabot, secret-scanning, + # branch-protection, baseline-health) return no findings without a + # token. Scoped to this step. The job token reaches code-scanning + # (security-events) and Dependabot (vulnerability-alerts) alerts; + # it cannot read secret-scanning alerts, which need an App or PAT. + GITHUB_TOKEN: ${{ github.token }} + 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 + + # Repo-relative paths, because the verdict grid's delta column compares + # findings against `git diff --name-only`, which is repo-relative: an + # absolute path would never match, and every finding would be counted + # as "standing" instead of "in this change". + jq --arg ws "$GITHUB_WORKSPACE/" ' + map(if (.file | type) == "string" then + .file |= (if startswith($ws) then .[($ws | length):] else . end) + else . end)' hypatia-findings.json > hypatia-findings.relativized.json + + # code_scanning_alerts findings are repository-wide historical GitHub + # alerts, not properties of this change. Hypatia's own SARIF render + # drops them (lib/hypatia/sarif.ex @meta_rule_modules) but its JSON + # keeps them, so exclude them from the BLOCKING count only; they stay + # in hypatia-findings.json and in the uploaded artifact. + CRITICAL="$(jq '[.[] | select(.severity == "critical" and .rule_module != "code_scanning_alerts")] | 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" + + # Evidence for the verdict grid. Uploaded even when the scan step failed: + # a missing artifact and a failed scan are different facts, and the grid + # can only say "orange" for the first. `if-no-files-found: warn` because a + # scan that never produced a file is already visible as a red step here. + - name: Upload Hypatia findings for the verdict grid + if: ${{ always() }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: hypatia-scan-findings + path: | + hypatia-findings.json + hypatia-findings.relativized.json + if-no-files-found: warn + retention-days: 90 + + - 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 548c3bb..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-latest - 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 b752226..3b8aa96 100644 --- a/.github/workflows/label-triage.yml +++ b/.github/workflows/label-triage.yml @@ -46,12 +46,12 @@ permissions: jobs: triage: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 # Hypatia workflow_audit/missing_timeout_minutes (alert #68) 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 31618e4..c496ffd 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -32,12 +32,12 @@ permissions: jobs: sync: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 # Hypatia workflow_audit/missing_timeout_minutes (alert #69) 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/lock-sync-gate.yml b/.github/workflows/lock-sync-gate.yml index db046cb..44a4595 100644 --- a/.github/workflows/lock-sync-gate.yml +++ b/.github/workflows/lock-sync-gate.yml @@ -35,7 +35,7 @@ concurrency: jobs: lock-sync: name: actions.lock is in sync with the workflow YAML - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - name: Check out without actions/checkout @@ -62,3 +62,9 @@ jobs: test -x scripts/check-lock-sync.sh \ || { echo "::error::scripts/check-lock-sync.sh missing or not executable"; exit 1; } ./scripts/check-lock-sync.sh + + - name: Test lockfile rejection cases + run: bash tests/workflows/check_lock_sync_test.sh + + - name: Test lockfile repair script + run: bash tests/workflows/lock_sync_fix_test.sh diff --git a/.github/workflows/main-estate-audit.yml b/.github/workflows/main-estate-audit.yml index c856ee3..6de4fd7 100644 --- a/.github/workflows/main-estate-audit.yml +++ b/.github/workflows/main-estate-audit.yml @@ -1,20 +1,78 @@ # 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 + + # These three enforcing gates are composite actions at the pinned + # suite (no check.sh), so run the `run:` block of the pinned + # action.yml itself rather than a copy. They can fail a build. + # manifest-check is NOT here: it delegates to the deed-ecosystem and + # k9-ecosystem validate-actions, which are `uses:` steps, not scripts. + for gate in required-files-check idris2-abi-check; do + echo "=== cicd-suite: $gate ===" + script="$(yq -r '.runs.steps[0].run' "$SUITE/$gate/action.yml")" + bash --noprofile --norc -e -o pipefail -c "$script" + 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 ===" + # git grep exits 1 when no tracked .zig file exists, so no guard is needed. + 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 + echo "Central Estate CI/CD Audit passed." diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 3dffb29..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@94572e612a7bf11f8e6343d19adadf17ef2a3de3 - # 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/ossf-best-practices.yml b/.github/workflows/ossf-best-practices.yml index b3aa90e..bd0cb62 100644 --- a/.github/workflows/ossf-best-practices.yml +++ b/.github/workflows/ossf-best-practices.yml @@ -16,7 +16,7 @@ permissions: contents: read jobs: openssf-compliance: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read @@ -44,11 +44,21 @@ jobs: echo "SECURITY file: OK ($SECFILE, $LINES lines)" - name: Check LICENSE exists run: | - if [ ! -f "LICENSE" ] && [ ! -f "LICENSE.txt" ] && [ ! -f "LICENSE.md" ]; then - echo "::error::LICENSE file is required for OpenSSF Best Practices" - exit 1 + # A root licence file in any of the usual spellings, or a REUSE + # LICENSES/ directory holding at least one licence text: a non-empty + # . file, per the REUSE spec (dotfiles and READMEs do not count). + for f in LICENSE LICENSE.txt LICENSE.md LICENSE.adoc; do + if [ -f "$f" ]; then + echo "LICENSE: OK ($f)" + exit 0 + fi + done + if [ -n "$(find LICENSES -maxdepth 1 -type f -size +0c -name '*.*' ! -name '.*' ! -iname 'README*' 2>/dev/null | head -n 1)" ]; then + echo "LICENSE: OK (REUSE LICENSES/)" + exit 0 fi - echo "LICENSE: OK" + echo "::error::LICENSE file is required for OpenSSF Best Practices" + exit 1 - name: Check CONTRIBUTING exists run: | if [ ! -f "CONTRIBUTING.md" ] && [ ! -f "CONTRIBUTING.adoc" ] \ @@ -64,18 +74,24 @@ jobs: exit 1 fi echo "README: OK" - - name: Check .machine_readable directory and STATE.a2ml + - name: Check .machine_readable directory and the repo deed run: | if [ ! -d ".machine_readable" ]; then echo "::error::.machine_readable/ directory is required" exit 1 fi - if [ ! -f ".machine_readable/descriptiles/STATE.a2ml" ]; then - echo "::error::.machine_readable/descriptiles/STATE.a2ml is required" + # Project state is the (status …) clause of the repo deed + # (rsr-template-repo#209), which replaced descriptiles/STATE.a2ml. + if ! DEED=$(bash scripts/deed-field.sh --find .); then + echo "::error::exactly one _chora.deed is required at the root" + exit 1 + fi + if ! bash scripts/deed-field.sh --has "$DEED" status; then + echo "::error::$DEED has no (status …) clause" exit 1 fi - echo ".machine_readable/descriptiles/STATE.a2ml: OK" + echo "$DEED (status): OK" - name: Check CHANGELOG exists run: | if [ ! -f "CHANGELOG.md" ] && [ ! -f "CHANGELOG.adoc" ] && [ ! -f "CHANGES.md" ]; then diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a176cb2..7fcc196 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -22,7 +22,7 @@ concurrency: jobs: build: timeout-minutes: 20 - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v7.0.1 @@ -83,7 +83,7 @@ jobs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: build steps: - name: Deploy to GitHub Pages diff --git a/.github/workflows/proven-tests.yml b/.github/workflows/proven-tests.yml new file mode 100644 index 0000000..40654bb --- /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 e064539116c2dcfdf282a4749dbd58c69961c7e6 + 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/push-email-notify.yml b/.github/workflows/push-email-notify.yml index 3b108c5..fff3546 100644 --- a/.github/workflows/push-email-notify.yml +++ b/.github/workflows/push-email-notify.yml @@ -35,7 +35,7 @@ jobs: notify: name: Email on push if: ${{ vars.PUSH_EMAIL_ENABLED == 'true' && github.event.deleted != true }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - name: Send push notification email diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c9c638c..0eb3270 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -17,12 +17,14 @@ permissions: contents: read jobs: lint: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 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,15 +58,49 @@ 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 + # The gates this template ships, and the proof that each one can fail. + # + # Nothing else in this workflow runs them: `just quality` is deliberately not + # wired here, because it needs the repository's own toolchain (zig for this + # tree) and a CI job that installs a toolchain to run a check is a job that + # gets skipped when the toolchain moves. These four gates need bash and git. + # + # tests/workflows/mint_gates_test.sh is the part that matters: it asserts that + # every gate above FAILS on a fixture built to fail it. A gate whose only + # exercised path is the passing one is the fake-gate class this estate hunts, + # and it cannot be caught by reading the gate. + mint-gates: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Gates + run: | + fail=0 + for g in check-mint-tokens check-spdx-headers check-mise-language check-container-pins; do + echo "── ${g} ──" + bash "scripts/${g}.sh" || fail=1 + done + exit "$fail" + - name: Every gate must fail on a fixture + run: bash tests/workflows/mint_gates_test.sh + - name: Language profiles (Julia and Rust scaffolds) + run: bash tests/e2e/language_profiles_mint_test.sh docs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 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 437afe0..8eba748 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ permissions: jobs: build: name: Build Artifacts - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read @@ -44,10 +44,6 @@ jobs: echo "::notice::Detected Zig project (build.zig)" echo "build_type=zig" >> "$GITHUB_OUTPUT" zig build -Doptimize=ReleaseSafe - elif [ -f "deno.json" ] || [ -f "deno.jsonc" ]; then - echo "::notice::Detected Deno project (deno.json)" - echo "build_type=deno" >> "$GITHUB_OUTPUT" - deno task build elif [ -f "gossamer.conf.json" ]; then echo "::notice::Detected Gossamer project (gossamer.conf.json)" echo "build_type=gossamer" >> "$GITHUB_OUTPUT" @@ -66,7 +62,7 @@ jobs: just build else echo "::error::No recognised build system found." - echo "Expected one of: mix.exs, Cargo.toml, build.zig, deno.json, gossamer.conf.json, gleam.toml, rebar.config, Justfile" + echo "Expected one of: mix.exs, Cargo.toml, build.zig, gossamer.conf.json, gleam.toml, rebar.config, Justfile" exit 1 fi # TODO: Upload build artifacts if needed (pin actions/upload-artifact @@ -77,7 +73,7 @@ jobs: # path: target/release/ changelog: name: Generate Changelog - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read @@ -118,7 +114,7 @@ jobs: release: name: Create GitHub Release needs: [build, changelog] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: write @@ -143,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/rsr-compliance-canary.yml b/.github/workflows/rsr-compliance-canary.yml index 13e0a55..5995bce 100644 --- a/.github/workflows/rsr-compliance-canary.yml +++ b/.github/workflows/rsr-compliance-canary.yml @@ -30,7 +30,7 @@ permissions: contents: read jobs: canary: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 steps: - name: Checkout @@ -54,11 +54,9 @@ jobs: for f in *-STATUS-*.md *-COMPLETION-*.md *-COMPLETE.md *-VERIFIED-*.md; do [ -f "$f" ] && warn "stale snapshot present: $f (would be deleted)" done - # would-RENAME: legacy manifest name (a repo deed at root is the - # deed-era equivalent — standards#837) - if [ -f "AI.a2ml" ] && [ ! -f "0-AI-MANIFEST.a2ml" ] && ! ls *_chora.deed >/dev/null 2>&1; then - warn "AI.a2ml present without 0-AI-MANIFEST.a2ml or a repo deed (would be renamed)" - fi + # retired format: every .a2ml file is drift (facts belong in the deed) + a2ml_left=$(find . -path ./.git -prune -o -type f -name '*.a2ml' -print | head -5) + [ -z "$a2ml_left" ] || warn "retired .a2ml files present (move their facts into the repo deed): $(echo $a2ml_left)" # would-DELETE: duplicate community files [ -f "CONTRIBUTING.md" ] && [ -f "CONTRIBUTING.adoc" ] && warn "duplicate CONTRIBUTING.md + CONTRIBUTING.adoc (one would be removed)" if [ -f "README.md" ] && [ -f "README.adoc" ] && [ "$(wc -l < README.md)" -lt 5 ]; then @@ -72,10 +70,14 @@ jobs: done # would-CREATE: missing required files [ -f "SECURITY.md" ] || [ -f ".github/SECURITY.md" ] || warn "no SECURITY.md (would be created)" - [ -f "CONTRIBUTING.md" ] || [ -f ".github/CONTRIBUTING.md" ] || warn "no CONTRIBUTING.md (would be created)" + # The estate writes its prose in AsciiDoc: CONTRIBUTING.adoc is the guide, + # not a gap (the .md was dropped in 1486d06 and the canary tripped on + # a rule that had diverged from the norm). Either form, root or .github/. + [ -f "CONTRIBUTING.md" ] || [ -f ".github/CONTRIBUTING.md" ] || [ -f "CONTRIBUTING.adoc" ] || [ -f ".github/CONTRIBUTING.adoc" ] \ + || warn "no CONTRIBUTING.adoc or CONTRIBUTING.md (would be created)" # --- unfixable compliance gaps (also drift) --- - [ -f "0-AI-MANIFEST.a2ml" ] || [ -f "AI.a2ml" ] || ls *_chora.deed >/dev/null 2>&1 || warn "missing AI manifest (0-AI-MANIFEST.a2ml or repo deed *_chora.deed)" + ls *_chora.deed >/dev/null 2>&1 || warn "missing repo deed (*_chora.deed at the root)" [ -f "LICENSE" ] || [ -f "LICENSE.md" ] || [ -f "LICENSE.txt" ] || warn "missing LICENSE file" [ -f "README.adoc" ] || [ -f "README.md" ] || warn "missing README" diff --git a/.github/workflows/runtime-policy.yml b/.github/workflows/runtime-policy.yml deleted file mode 100644 index 51ec04f..0000000 --- a/.github/workflows/runtime-policy.yml +++ /dev/null @@ -1,72 +0,0 @@ -# This workflow is managed by gh actions-lock. -# SPDX-License-Identifier: MPL-2.0 -# Runtime and package-manager policy check. -# -# Authority: hyperpolymath/standards LANGUAGE-POLICY.adoc §1. -# Ordering: Bun (1st) > Deno (2nd) > pnpm (3rd) > npm (last resort). -# -# REPLACES npm-bun-blocker.yml, which failed any build carrying `bun.lockb` with -# the message "npm/bun artifacts detected. Use Deno instead." That gate blocked -# what is now the FIRST-choice runtime and mandated the second. It was present in -# 55 repositories. -# -# What this fails on, deliberately: -# MIXED TOOLCHAINS -- two different package managers' lockfiles in one repo. -# That is real, actionable drift: two dependency graphs that can disagree. -# What it does NOT fail on: -# Using bun, pnpm or npm. Deno is RETIRED (2026-08-26). npm is LAST but PERMITTED; the check reports -# the tier in use so drift is visible without blocking legitimate work. -name: Runtime Policy -on: - push: - branches: [main, master] - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - runtime-policy: - name: Runtime Policy - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - steps: - - uses: actions/checkout@v7.0.1 - - - name: Report runtime tier and reject mixed toolchains - run: | - set -euo pipefail - - bun=0; deno=0; pnpm=0; npm=0 - [ -f bun.lockb ] || [ -f bun.lock ] && bun=1 || true - [ -f deno.lock ] || [ -f deno.json ] || [ -f deno.jsonc ] && deno=1 || true - [ -f pnpm-lock.yaml ] && pnpm=1 || true - [ -f package-lock.json ] && npm=1 || true - - total=$((bun + deno + pnpm + npm)) - - if [ "$total" -eq 0 ]; then - echo "::notice::No JS/TS package manager in use — nothing to check." - exit 0 - fi - - # Report the tier actually in use (LANGUAGE-POLICY.adoc §1). - [ "$bun" -eq 1 ] && echo "Bun — tier 1 (preferred)" - [ "$deno" -eq 1 ] && echo "::warning::Deno lockfile/config present. Deno is BEING REMOVED, not grandfathered (owner ruling 2026-08-26, canon standards#655). Migrate to Bun — package.json + bun.lock. This is a warning rather than an error only while the estate migration is in flight; it escalates to an error when that completes." - [ "$pnpm" -eq 1 ] && echo "pnpm — tier 3" - [ "$npm" -eq 1 ] && echo "::warning::npm lockfile present. npm is tier 4, the last resort — permitted, never preferred. See LANGUAGE-POLICY.adoc §1." - - if [ "$total" -gt 1 ]; then - echo "::error::Mixed toolchains: $total package managers have lockfiles in this repository." - echo "Two dependency graphs that can disagree is real drift. Pick one — preferring the" - echo "highest tier present — and delete the others' lockfiles." - exit 1 - fi - - echo "✅ Single package manager in use." diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 7bcd5e7..464b871 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -1,19 +1,54 @@ # 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@94572e612a7bf11f8e6343d19adadf17ef2a3de3 + 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 + # The runner's default toolchain may lack these components. + rustup component add rustfmt clippy + cargo fmt --all -- --check + cargo clippy --locked --all-targets -- -D warnings + cargo check --all-targets --locked + cargo test --locked + fi diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b9dcab2..57c9881 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -10,17 +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@94572e612a7bf11f8e6343d19adadf17ef2a3de3 - # 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 785d428..0c3a8eb 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -1,29 +1,377 @@ # 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@e064539116c2dcfdf282a4749dbd58c69961c7e6. +# Produces the `scan / gitleaks` status check that the `Secret-Scan-Floor` +# ruleset (24309812) was created to require on `main`. +# +# HONEST STATUS OF THAT RULE (measured 2026-11-08): the ruleset is `active` +# with `conditions.ref_name.include: ["~DEFAULT_BRANCH"]` but carries +# `rules: []` and a repository-role-5 `bypass_actors` entry with +# `bypass_mode: always`. It currently requires NOTHING, so this check is +# PRODUCED and reported, not enforced. `GET /rules/branches/main` returns only +# `required_linear_history`. Do not describe these contexts as required without +# re-reading the live ruleset; see `.github/rulesets/README.adoc`. +# +# ─── WHY THREE JOBS, AND WHY THEY ARE INLINE ───────────────────────────────── +# +# The estate canonical wraps all three scanners in ONE reusable workflow call: +# +# jobs: +# scan: +# uses: metadatastician/standards/.github/workflows/secret-scanner-reusable.yml@ +# +# That shape cannot satisfy this repository's contract, and the reason is +# measured, not theoretical. A job-level reusable call produces exactly one +# check, named ` / ` — so the wrapper above +# yields `scan / gitleaks`, `scan / rust-secrets` and `scan / shell-secrets` +# only while the called workflow happens to define jobs with those names. The +# wrapper's own job is named `scan`, so any rename inside the callee silently +# changes three public check names in every generated repository. Three +# required contexts that move when a remote repository is edited is not a +# contract. +# +# This repository therefore runs the same three scanners as three ordinary +# jobs whose `name:` IS the context: +# +# scan / gitleaks (gitleaks, estate baseline + repo config, +# working tree + full history + AsciiDoc mirror) +# scan / rust-secrets (hardcoded literals in .rs files; self-skips +# without a Cargo.toml) +# scan / shell-secrets (hardcoded literals in shell scripts) +# +# All three declare `permissions: contents: read` and NO `secrets:` line. The +# reusable declares no `workflow_call.secrets` and references no `secrets.*` +# context, so `secrets: inherit` on a caller buys nothing and forwards every +# repository and organisation secret to a scanner that needs none (CWE-250, +# standards#1108 §6b). Do not add it back. +# +# ─── actions.lock AND THIS FILE ────────────────────────────────────────────── +# +# Every `uses:` here is a step-level action that `gh actions-lock` knows how to +# lock, so the whole file is inside the generator's model: `gh actions-lock` +# records each ref and cannot produce a dangling edge for anything in it. That +# is deliberate. A job-level call to a remote reusable is NOT lockable by the +# tool, and a hand-added record for one is worse than no record: an entry that +# the runner cannot resolve fails the run at startup with zero jobs, while an +# absent one is merely unverified. +# +# Regenerate with the official generator only: +# +# gh actions-lock # writes .github/workflows/actions.lock +# gh actions-lock --verify # read-only check +# scripts/check-lock-sync.sh .github/workflows # transitive-closure gate +# +# The scanner's own pin is a full 40-hex SHA in `STANDARDS_SHA` below and in the +# estate-baseline checkout, and it must match the standards pin every other +# workflow in this directory uses. 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@94572e612a7bf11f8e6343d19adadf17ef2a3de3 + env: + GITLEAKS_VERSION: '8.18.4' + GITLEAKS_SHA256: 'ba6dbb656933921c775ee5a2d1c13a91046e7952e9d919f9bac4cec61d628e7d' + STANDARDS_SHA: 'e064539116c2dcfdf282a4749dbd58c69961c7e6' + 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: e064539116c2dcfdf282a4749dbd58c69961c7e6 + 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 + # The estate baseline always runs; a repository .gitleaks.toml adds a + # second pass and can never stand in for the baseline. + CONFIGS=(".gitleaks-estate.toml") + if [ -f .gitleaks.toml ]; then + CONFIGS+=(".gitleaks.toml") + fi + for CONFIG in "${CONFIGS[@]}"; do + "$RUNNER_TEMP/gitleaks" detect \ + --source . \ + --no-git \ + --redact \ + --no-banner \ + --verbose \ + --config "$CONFIG" \ + --exit-code 1 + done + + - 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 + # The estate baseline always runs; a repository .gitleaks.toml adds a + # second pass and can never stand in for the baseline. + CONFIGS=(".gitleaks-estate.toml") + if [ -f .gitleaks.toml ]; then + CONFIGS+=(".gitleaks.toml") + fi + for CONFIG in "${CONFIGS[@]}"; do + "$RUNNER_TEMP/gitleaks" detect \ + --source . \ + --redact \ + --no-banner \ + --verbose \ + --config "$CONFIG" \ + --exit-code 1 + done + + - 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 + # The estate baseline always runs; a repository .gitleaks.toml adds a + # second pass and can never stand in for the baseline. + CONFIGS=("$PWD/.gitleaks-estate.toml") + if [ -f .gitleaks.toml ]; then + CONFIGS+=("$PWD/.gitleaks.toml") + fi + for CONFIG in "${CONFIGS[@]}"; do + "$RUNNER_TEMP/gitleaks" detect \ + --source "$MIRROR" \ + --no-git \ + --redact \ + --no-banner \ + --config "$CONFIG" \ + --exit-code 1 + done + + rust-secrets: + name: "scan / rust-secrets" + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + env: + # Findings outside ./src (newly visible — see the scan-scope note in the + # step below) warn until this date, then block. Same warn-then-enforce + # idiom and same cutoff as the estate canonical reusable + # (metadatastician/standards .github/workflows/secret-scanner-reusable.yml + # at the pin named in this file's header), so this job and the reusable + # cannot disagree about when the widened scan starts blocking. + ENFORCE_RUST_WIDE_SCAN_FROM: '2026-08-21' + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Check for hardcoded secrets in Rust + run: | + set -euo pipefail + TODAY="${RUST_TODAY:-$(date -u +%Y-%m-%d)}" + + # An unparseable cutoff would pick the warn branch forever, silently + # disarming the widened scan. Refuse to run instead. + require_date() { + local name="$1" value="$2" + case "$value" in + [0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]) : ;; + *) echo "::error::rust-secrets: $name='$value' is not YYYY-MM-DD." + echo "Refusing to run: an unparseable cutoff would disarm this gate." + exit 1 ;; + esac + } + require_date ENFORCE_RUST_WIDE_SCAN_FROM "$ENFORCE_RUST_WIDE_SCAN_FROM" + require_date RUST_TODAY "$TODAY" + + # Self-skips when the tree has no Cargo.toml, so the job is safe on a + # repository that is not (yet) Rust. The check run still appears, which + # is what keeps the context stable across every generated repository. + if ! find . -name Cargo.toml -not -path './target/*' -print -quit | grep -q .; then + echo 'No Cargo.toml found — skipping Rust secrets check' + exit 0 + fi + + # Scan every .rs file, not just ./src. A Cargo workspace keeps its + # code in crates/*/src, and a `grep … src/` matches nothing there: + # grep exits 2 on a missing directory, which an `if` reads as "clean". + mapfile -t FILES < <(find . -name '*.rs' \ + -not -path './target/*' -not -path '*/target/*' \ + -not -path './vendor/*' -not -path '*/vendor/*' \ + -not -path './.git/*' -print) + if [ ${#FILES[@]} -eq 0 ]; then + echo 'Cargo.toml present but no .rs files — nothing to scan' + exit 0 + fi + echo "Scanning ${#FILES[@]} Rust file(s)." + + # Each pattern requires an assignment DIRECTLY to a string literal of + # meaningful length, so lookups, paths and demos do not match. A + # credential shorter than 8 characters is not a credential either. + # `static` is accepted alongside `const`: a `static AUTH_KEY: &str = + # "…"` is a genuine hardcoded secret. + PATTERNS=( + '(const|static)[[:space:]]+(mut[[:space:]]+)?[A-Za-z0-9_]*SECRET[A-Za-z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*b?"[^"]{8,}"' + '(const|static)[[:space:]]+(mut[[:space:]]+)?[A-Za-z0-9_]*KEY[A-Za-z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*b?"[a-zA-Z0-9]{16,}"' + '(const|static)[[:space:]]+(mut[[:space:]]+)?[A-Za-z0-9_]*TOKEN[A-Za-z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*b?"[^"]{8,}"' + '(let|static)[[:space:]]+(mut[[:space:]]+)?[a-z0-9_]*api_key[a-z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*b?"[^"]{8,}"' + 'HMAC.*"[a-fA-F0-9]{32,}"' + '[a-z0-9_]*password[a-z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*b?"[^"]{8,}"' + ) + + # Exemptions match on the VALUE or on an explicit pragma, never on a + # file path: a path-based exemption would blind the job to a genuine + # secret in the same file. + # 1. env-lookup RHS — `env::var("NAME")` IS the practice this job + # exists to enforce, so flagging it fails the correct code. + # 2. URL values — an OAuth endpoint is public, not a credential. + # 3. comment lines — documentation of a pattern is not a secret. + # 4. inline pragma `// scanner-allow: rust-secrets`. + COMMENT='^[^:]*:[0-9][0-9]*:[[:space:]]*(//|/\*|\*)' + ENV_RHS='=[^;]*(env::var|env::var_os|std::env|env!\(|option_env!\(|from_env|dotenv)' + URL_RHS='=[[:space:]]*b?"(https?|wss?|ftp)://' + PRAGMA='scanner-allow:[[:space:]]*rust-secrets' + + blocking=0 + advisory=0 + for pattern in "${PATTERNS[@]}"; do + hits="$(grep -nHE "$pattern" "${FILES[@]}" 2>/dev/null \ + | grep -vE "$COMMENT" \ + | grep -vE "$ENV_RHS" \ + | grep -vE "$URL_RHS" \ + | grep -vE "$PRAGMA" || [ $? -eq 1 ])" + [ -n "$hits" ] || continue + + # ./src hits keep blocking. Hits outside it are newly visible, so + # they warn until the cutoff rather than reddening repositories + # whose wider tree was never actually being scanned. + old_scope="$(printf '%s\n' "$hits" | grep -E '^\./src/' || [ $? -eq 1 ])" + new_scope="$(printf '%s\n' "$hits" | grep -vE '^\./src/' || [ $? -eq 1 ])" + + # Print file:line only. The matched source text is the credential + # itself; echoing it would publish it to the Actions log. + if [ -n "$old_scope" ]; then + printf '%s\n' "$old_scope" | cut -d: -f1,2 | sed 's/$/: hardcoded-secret pattern match (line text withheld)/' + echo "WARNING: Potential hardcoded secret found matching: $pattern" + blocking=1 + fi + if [ -n "$new_scope" ]; then + printf '%s\n' "$new_scope" | cut -d: -f1,2 | sed 's/$/: hardcoded-secret pattern match (line text withheld)/' + if [[ "$TODAY" < "$ENFORCE_RUST_WIDE_SCAN_FROM" ]]; then + echo "::warning::Potential hardcoded secret outside ./src matching:" \ + "$pattern — becomes BLOCKING on $ENFORCE_RUST_WIDE_SCAN_FROM (today is $TODAY)." + advisory=1 + else + echo "WARNING: Potential hardcoded secret found matching: $pattern" + blocking=1 + fi + fi + done + + if [ "$blocking" -eq 1 ]; then + echo "::error::Potential hardcoded secrets detected. Use environment variables instead." + exit 1 + fi + if [ "$advisory" -eq 1 ]; then + # Never claim a clean pass while findings are outstanding. + echo "NOT YET ENFORCED: findings outside ./src are advisory until $ENFORCE_RUST_WIDE_SCAN_FROM." + exit 0 + fi + echo 'No hardcoded secrets detected in Rust sources.' + + 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 + # Provider-specific patterns, plus assignment of a quoted literal to a + # *_TOKEN / *_KEY / *_SECRET / PASSWORD name (as in the estate canonical + # reusable, metadatastician/standards@e064539 + # .github/workflows/secret-scanner-reusable.yml). The value classes + # exclude '$', so ="${VAR}" and ="$VAR" never match. Only file names + # are listed, never the matched text. + SQ="'" + ASSIGN="[A-Z_]*(TOKEN|API_KEY|SECRET)[A-Z_]*=[\"${SQ}][A-Za-z0-9_./+=-]{16,}[\"${SQ}]|PASSWORD=[\"${SQ}][^\"${SQ}\$]{6,}[\"${SQ}]" + PROVIDER='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-----' + git grep -IlE "(${PROVIDER}|${ASSIGN})" > "$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 3c64369..a95cf6c 100644 --- a/.github/workflows/security-policy.yml +++ b/.github/workflows/security-policy.yml @@ -16,12 +16,14 @@ permissions: contents: read jobs: check: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 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 @@ -34,7 +36,7 @@ jobs: fi # Block HTTP URLs (except localhost) - HTTP_URLS=$(grep -rE 'http://[^l][^o][^c]' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.yaml" --include="*.yml" . 2>/dev/null | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5 || true) + HTTP_URLS=$(grep -rE 'http://[^l][^o][^c]' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.yaml" --include="*.yml" --include="*.kyaml" . 2>/dev/null | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5 || true) if [ -n "$HTTP_URLS" ]; then echo "⚠️ HTTP URLs found. Use HTTPS:" echo "$HTTP_URLS" diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 40b5318..358ba97 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -3,7 +3,7 @@ # SonarQube Cloud (SonarCloud) static analysis. Analysis scope + exclusions live # in sonar-project.properties. Requires the SONAR_TOKEN repository secret # (Settings -> Secrets and variables -> Actions) and a SonarCloud project: -# https://sonarcloud.io/project/overview?id=hyperpolymath_rsr-template-repo +# https://sonarcloud.io/project/overview?id=hyperpolymath_occupancy-types # Mirrors the boj-server arrangement. # # WHY THE TOKEN GUARD (added 2026-07-28) @@ -36,15 +36,19 @@ permissions: jobs: sonarqube: name: SonarQube - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 - # Secrets cannot be referenced from a job-level `if:`, so the presence test - # is carried through an env var set at job scope and read in a first step. - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: - name: Report configuration state id: cfg + # A job-level `if:` cannot read a secret. This step reads the token + # and gives the result to the steps after it. The token is in the + # environment of this step and of the scan step only. + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + # The script of this step is Bash. `shell: bash` says so in the step: + # a default shell or a runner with a different shell cannot change it. + shell: bash run: | if [ -z "${SONAR_TOKEN}" ]; then echo "configured=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/squabble.yml b/.github/workflows/squabble.yml new file mode 100644 index 0000000..ef85af2 --- /dev/null +++ b/.github/workflows/squabble.yml @@ -0,0 +1,93 @@ +# 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 . + # squabble follows CROSS-repository CI edges. The verdict grid added + # edges of the same kind INSIDE this repository (workflow_run on a + # workflow's `name:`, artifacts read by name, per-tool pins shared + # between a producer and the reporter), and a rename of any of them + # fails silently: no red check, just an orange grid row forever. + # This is the local half of the same audit. + bash scripts/check-workflow-edges.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 ffc406f..956e3d5 100644 --- a/.github/workflows/static-analysis-gate.yml +++ b/.github/workflows/static-analysis-gate.yml @@ -1,7 +1,33 @@ # 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 and pons-asinorum, executes real scans +# without silent fallback, and deposits one evidence artifact for the verdict +# grid. +# +# ── SCOPE (see the header of fork-governance.yml for the whole picture) ────── +# +# This workflow produces evidence; it does not render the verdict grid and it +# does not run Hypatia. Until 2026-10 it ran Hypatia a second time (same pin, +# different cache key, a second "fail on critical" gate, a differently-shaped +# JSON) while .github/workflows/hypatia-scan.yml ran it too. Hypatia now runs +# once, there. What is left here is what only this workflow does: panic-attack +# assail, pons asinorum deep recon, and patch-bridge CVE triage. +# +# The evidence artifact (`static-analysis-findings`, read by fork-governance.yml) +# carries each scanner's STATUS ENVELOPE, not a bare array: +# +# { "status": "ran" | "not_run" | "error", "tool": "...", "ref": "", +# "reason": null | "", "exit_code": 0 | , "report": {...} | null } +# +# A bare array cannot distinguish "ran clean" from "never ran", and the verdict +# grid renders a scanner that did not run as orange, never as a clean scan. This +# shape is the one metadatastician/standards docs/VERDICT-GRID-SPEC.adoc defines +# and hypatia-scan-reusable.yml already writes, so adopting that reusable would +# not change what the grid reads. +# +# GATES: panic-attack blocks on critical (its own decision, taken here once); +# pons and patch-bridge are advisory. The verdict grid never gates. name: Static Analysis Gate on: pull_request: @@ -13,288 +39,219 @@ concurrency: cancel-in-progress: true permissions: contents: read +env: + # One pin per tool, estate-wide, for this repository. fork-governance.yml's + # verdict job names the same commits when it identifies this evidence, and + # scripts/check-workflow-edges.sh fails the CICD Squabbler gate when a tool is + # pinned at two different commits in two workflows. + PANIC_ATTACK_SHA: b5530834a839734d1d181bfe6edb0d43d4233cf4 + PONS_SHA: 51b8920f541782ceb9465ea3f0d33c96104fc1fb jobs: # --------------------------------------------------------------------------- - # Job 1: panic-attack assail + # Job 1: panic-attack assail + pons-asinorum deep recon # --------------------------------------------------------------------------- panic-attack-assail: - name: panic-attack assail - runs-on: ubuntu-latest - timeout-minutes: 15 + name: panic-attack & pons-asinorum deep recon + runs-on: ubuntu-24.04 + 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-v3-${{ runner.os }}-pa-${{ env.PANIC_ATTACK_SHA }}-pons-${{ env.PONS_SHA }} + + - 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 --force \ + --git https://github.com/hyperpolymath/panic-attack.git \ + --rev "$PANIC_ATTACK_SHA" + cargo install --locked \ + --git https://github.com/hyperpolymath/pons-asinorum.git \ + --rev "$PONS_SHA" \ + --bin pons + + # Each scanner writes its envelope whichever way it ends, so the grid + # sees status "error" with the exit code and reason instead of an absent + # file that it could only call "not run". - 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 -uo pipefail + export PATH="$HOME/.cargo/bin:$PATH" + PA_RAW="$RUNNER_TEMP/pa-report.json" + PA_LOG="$RUNNER_TEMP/pa-assail.log" + out=panic-attack-findings.json + 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 - fi + envelope() { # envelope STATUS REASON + jq -n --arg s "$1" --arg r "$2" --arg ref "$PANIC_ATTACK_SHA" --argjson rc "$PA_EXIT" \ + --slurpfile doc "${3:-/dev/null}" \ + '{status: $s, tool: "panic-attack", ref: $ref, reason: ($r | if . == "" then null else . end), + exit_code: $rc, report: (if ($doc | length) > 0 then $doc[0] else null end)}' > "$out" + } - # 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 [ "$PA_EXIT" -ne 0 ] || [ ! -s "$PA_RAW" ]; then + cat "$PA_LOG" >&2 + envelope error "panic-attack assail failed or produced an empty report (exit ${PA_EXIT})" + echo "::error::panic-attack assail failed or produced empty report (exit ${PA_EXIT})" + elif ! jq -e 'type == "object" and (.weak_points | type == "array")' "$PA_RAW" >/dev/null 2>&1; then + cat "$PA_LOG" >&2 + envelope error "panic-attack assail produced no valid AssailReport JSON object" + echo "::error::panic-attack assail did not produce a valid AssailReport JSON object" + else + envelope ran "" "$PA_RAW" + # suppressed:true is panic-attack's own "likely false positive" mark; + # the grid does not count it either (VERDICT-GRID-SPEC §grading). + jq '[.weak_points[] | select(.suppressed != true) | .severity = ((.severity // "") | ascii_downcase)]' \ + "$PA_RAW" > "$RUNNER_TEMP/pa-findings.json" + TOTAL=$(jq 'length' "$RUNNER_TEMP/pa-findings.json") + CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' "$RUNNER_TEMP/pa-findings.json") + HIGH=$(jq '[.[] | select(.severity == "high")] | length' "$RUNNER_TEMP/pa-findings.json") + MEDIUM=$(jq '[.[] | select(.severity == "medium" or .severity == "warn")] | length' "$RUNNER_TEMP/pa-findings.json") + LOW=$(jq '[.[] | select(.severity == "low")] | length' "$RUNNER_TEMP/pa-findings.json") + { + echo "total=$TOTAL" + echo "critical=$CRITICAL" + echo "high=$HIGH" + echo "medium=$MEDIUM" + echo "low=$LOW" + } >> "$GITHUB_OUTPUT" + { + echo "## panic-attack assail" + echo "" + echo "| Severity | Count |" + echo "|----------|-------|" + echo "| Critical | ${CRITICAL} |" + echo "| High | ${HIGH} |" + echo "| Medium | ${MEDIUM} |" + echo "| Low | ${LOW} |" + } >> "$GITHUB_STEP_SUMMARY" + fi + jq -e '.status | type == "string"' "$out" >/dev/null + if [ "$(jq -r .status "$out")" != "ran" ]; then + echo "::error::Blocking scanner did not produce trustworthy results" + 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' - 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" - - name: Upload panic-attack findings - uses: actions/upload-artifact@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 - run: | - echo "::error::panic-attack found ${{ steps.assail.outputs.critical }} 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. - # --------------------------------------------------------------------------- - hypatia-scan: - name: Hypatia neurosymbolic scan - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout repository - uses: actions/checkout@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 - with: - elixir-version: '1.19.4' - otp-version: '28.3' - - name: Clone and build Hypatia - id: build + - name: Run pons-asinorum deep recon audit + id: pons 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" - fi - - name: Run Hypatia scan - id: scan - if: steps.build.outputs.ready == 'true' - run: | - set +e - HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json - 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" + set -uo pipefail + export PATH="$HOME/.cargo/bin:$PATH" + out=pons-findings.json + if ! command -v pons >/dev/null 2>&1; then + jq -n --arg ref "$PONS_SHA" \ + '{status: "not_run", tool: "pons-asinorum", ref: $ref, + reason: "pons binary not found on PATH after the pinned build", exit_code: null, report: null}' > "$out" + echo "::error::pons binary not found on PATH" exit 1 fi + 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=$? - 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) + envelope() { # envelope STATUS REASON + jq -n --arg s "$1" --arg r "$2" --arg ref "$PONS_SHA" --argjson rc "${PONS_RC:-0}" \ + --slurpfile doc "${3:-/dev/null}" \ + '{status: $s, tool: "pons-asinorum", ref: $ref, reason: ($r | if . == "" then null else . end), + exit_code: $rc, report: (if ($doc | length) > 0 then $doc[0] else null end)}' > "$out" + } - 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" - - 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 | - if .severity == "critical" then - "::error file=\($f),line=\(.line // 1)::[hypatia] \($m)" - elif .severity == "high" then - "::error file=\($f),line=\(.line // 1)::[hypatia] \($m)" - else - "::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 + if [ "$PONS_RC" -ne 0 ] || ! jq -e 'type == "object" and (.findings | type == "array")' "$PONS_JSON" >/dev/null 2>&1; then + cat "$PONS_LOG" >&2 + envelope error "pons scan failed (exit ${PONS_RC}) or produced no readable findings array" + echo "::error::pons scan failed (exit ${PONS_RC})" + exit 1 + fi + envelope ran "" "$PONS_JSON" + # ERROR counts only where pons itself does not call the finding + # speculative: a heuristic is never read as a proof. + 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" - | 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 + - name: Upload the scanner envelopes + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: hypatia-findings - path: hypatia-findings.json + name: static-analysis-findings + path: | + panic-attack-findings.json + pons-findings.json + if-no-files-found: error 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.assail.outputs.critical != '0' && steps.assail.outputs.critical != '' + env: + CRIT_COUNT: ${{ steps.assail.outputs.critical }} run: | - echo "::error::Hypatia found ${{ steps.scan.outputs.critical }} critical security issue(s) — blocking merge" + echo "::error::panic-attack found ${CRIT_COUNT} critical issue(s) — blocking merge" exit 1 + # --------------------------------------------------------------------------- - # Job 3: patch-bridge triage (CVE contextual assessment) + # Job 2: patch-bridge triage (CVE contextual assessment) # --------------------------------------------------------------------------- patch-bridge-triage: name: Patch Bridge CVE triage - runs-on: ubuntu-latest - timeout-minutes: 15 + runs-on: ubuntu-24.04 + 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 + + # One cache namespace only. Restoring the legacy binary without Cargo's + # install metadata made the subsequent pinned install abort rather than + # replace it, leaving patch-bridge permanently red on cache misses. + - name: Restore pinned patch-bridge scanner + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: cache-pa + with: + path: ~/.cargo/bin/panic-attack + key: patch-bridge-${{ runner.os }}-${{ env.PANIC_ATTACK_SHA }} + + - 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 "$PANIC_ATTACK_SHA" + - 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,149 +263,168 @@ 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' + env: + UNMIT: ${{ steps.triage.outputs.unmitigable }} + MIT: ${{ steps.triage.outputs.mitigated }} + CONCAT: ${{ steps.triage.outputs.concatenative }} + INFO: ${{ steps.triage.outputs.informational }} 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 }} | + { + 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" - 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' - 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" - 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) + # Job 3: deposit-findings (one unified report, beside the envelopes) # --------------------------------------------------------------------------- + # The unified report is for humans and for the fleet's own consumers. The + # verdict grid reads the ENVELOPES from `static-analysis-findings`, not this + # file: merging two scanners into one array discards each one's "did it run" + # fact, which is the single most important thing the grid reports. deposit-findings: name: Deposit findings for gitbot-fleet - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 - needs: [panic-attack-assail, hypatia-scan, patch-bridge-triage] - if: always() + needs: [panic-attack-assail, patch-bridge-triage] + # !cancelled(), not always(): deposit what the scans found even when one + # failed, but not after the run was cancelled (a superseding push), when + # the artifacts were never uploaded and the download step can only fail. + if: ${{ !cancelled() }} steps: - - name: Download panic-attack findings - uses: actions/download-artifact@v8.0.2 - with: - name: panic-attack-findings - path: findings/ - - name: Download hypatia findings - uses: actions/download-artifact@v8.0.2 + - name: Download panic-attack & pons envelopes + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + continue-on-error: true with: - name: hypatia-findings + name: static-analysis-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" + PONS_FILE="findings/pons-findings.json" - # Ensure both files exist and are valid JSON arrays - for f in "$PA_FILE" "$HYP_FILE"; do + # A missing envelope is recorded, not invented: "did not run" and + # "ran clean" must never collapse into the same empty array. + for f in "$PA_FILE" "$PONS_FILE"; do if [ ! -s "$f" ] || ! jq empty "$f" 2>/dev/null; then - echo "[]" > "$f" + echo '{"status": "not_run", "tool": "unknown", "ref": null, "reason": "no envelope artifact reached this job", "exit_code": null, "report": null}' > "$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 '[.report.weak_points[]? | select(.suppressed != true) | . + {"scanner": "panic-attack"}]' \ + "$PA_FILE" > "$RUNNER_TEMP/pa-tagged.json" + jq '[.report.findings[]? | . + {"scanner": "pons-asinorum"}]' \ + "$PONS_FILE" > "$RUNNER_TEMP/pons-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 pons "$RUNNER_TEMP/pons-tagged.json" \ --slurpfile bridge "$BRIDGE_FILE" \ + --slurpfile pa_env "$PA_FILE" \ + --slurpfile pn_env "$PONS_FILE" \ '{ - schema_version: "1.1.0", + schema_version: "1.2.0", repository: $repo, commit_sha: $sha, ref: $ref, run_id: $run_id, timestamp: $ts, - findings: ($pa[0] + $hyp[0]), + scanners: { + panic_attack: {status: $pa_env[0].status, ref: $pa_env[0].ref, reason: $pa_env[0].reason}, + pons_asinorum: {status: $pn_env[0].status, ref: $pn_env[0].ref, reason: $pn_env[0].reason} + }, + findings: ($pa[0] + $pons[0]), patch_bridge: $bridge[0] }' > findings/unified-findings.json TOTAL=$(jq '.findings | length' findings/unified-findings.json) - CRITICAL=$(jq '[.findings[] | select(.severity == "critical")] | length' findings/unified-findings.json) - HIGH=$(jq '[.findings[] | select(.severity == "high")] | length' findings/unified-findings.json) - 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) + CRITICAL=$(jq '[.findings[] | select((.severity // "" | ascii_downcase) == "critical")] | length' findings/unified-findings.json) + HIGH=$(jq '[.findings[] | select((.severity // "" | ascii_downcase) == "high" or (.severity // "" | ascii_upcase) == "ERROR")] | length' findings/unified-findings.json) + + { + echo "total=$TOTAL" + echo "critical=$CRITICAL" + echo "high=$HIGH" + } >> "$GITHUB_OUTPUT" - 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" - 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 }} + 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 (incl. pons ERROR) | ${HIGH_C} |" + echo "| **Total** | ${TOT} |" + echo "" + echo "Hypatia's findings are produced once, by \`.github/workflows/hypatia-scan.yml\`," + echo "and graded directly by the verdict grid — they are not repeated here." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/verdict-grid-publish.yml b/.github/workflows/verdict-grid-publish.yml new file mode 100644 index 0000000..d1be6e1 --- /dev/null +++ b/.github/workflows/verdict-grid-publish.yml @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: MPL-2.0 +# Trusted publisher for the read-only PR verdict-grid pipeline. +# This workflow_run file executes only when present on the default branch. +# Never check out PR-controlled code or run a downloaded artifact as a script. +name: Publish verdict grid + +on: + workflow_run: # zizmor: ignore[dangerous-triggers] + workflows: + - Fork governance + - Hypatia Security Scan + - Static Analysis Gate + types: [completed] + +permissions: {} +concurrency: + group: verdict-publish-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_sha }} + cancel-in-progress: true + +jobs: + publish: + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + actions: read + pull-requests: write + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Authenticate source run and identify current pull request + id: source + run: | + set -euo pipefail + run_id=$(jq -r '.workflow_run.id' "$GITHUB_EVENT_PATH") + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" > "$RUNNER_TEMP/source-run.json" + jq -e --slurpfile event "$GITHUB_EVENT_PATH" ' + .id == $event[0].workflow_run.id + and .event == "pull_request" and .status == "completed" + and .repository.full_name == $event[0].repository.full_name + and .head_sha == $event[0].workflow_run.head_sha + ' "$RUNNER_TEMP/source-run.json" >/dev/null + sha=$(jq -r '.head_sha' "$RUNNER_TEMP/source-run.json") + gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/commits/$sha/pulls" > "$RUNNER_TEMP/source-prs.json" + pr=$(jq -r --arg sha "$sha" --arg repo "$GITHUB_REPOSITORY" ' + [.[][] | select(.state == "open" and .head.sha == $sha + and .base.repo.full_name == $repo)] + | if length == 1 then .[0].number else empty end + ' "$RUNNER_TEMP/source-prs.json") + if [ -z "$pr" ]; then + echo "::notice::No unique current PR at the completed run SHA; refusing to publish a stale verdict." + exit 0 + fi + echo "pr=$pr" >> "$GITHUB_OUTPUT" + echo "sha=$sha" >> "$GITHUB_OUTPUT" + + - name: Checkout trusted default-branch reporter + if: steps.source.outputs.pr != '' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.repository.default_branch }} + persist-credentials: false + sparse-checkout: | + scripts/verdict-grid-lane.sh + .gitbot-fleet + sparse-checkout-cone-mode: false + + - name: Checkout pinned renderer + if: steps.source.outputs.pr != '' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: metadatastician/standards + ref: e064539116c2dcfdf282a4749dbd58c69961c7e6 + path: .standards-grid + persist-credentials: false + sparse-checkout: | + scripts/verdict-grid.sh + scripts/lib/verdict-grid.jq + sparse-checkout-cone-mode: false + + - name: Collect, validate and render diagnostics as data only + if: steps.source.outputs.pr != '' + env: + TARGET_SHA: ${{ steps.source.outputs.sha }} + TARGET_PR: ${{ steps.source.outputs.pr }} + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/evidence" + bash scripts/verdict-grid-lane.sh collect \ + --repo "$GITHUB_REPOSITORY" --sha "$TARGET_SHA" \ + --out "$RUNNER_TEMP/evidence" \ + --source hypatia-scan.yml:hypatia-scan-findings \ + --source static-analysis-gate.yml:static-analysis-findings + # Fleet evidence lives in the read-only Fork governance PR run, + # not in this privileged publisher's own workflow_run. + fleet_run=$(gh run list --repo "$GITHUB_REPOSITORY" \ + --workflow fork-governance.yml --commit "$TARGET_SHA" --event pull_request --limit 20 \ + --json databaseId,status,event | jq -r \ + '[.[] | select(.event == "pull_request" and .status == "completed")] | sort_by(.databaseId) | last | .databaseId // empty') + if [ -n "$fleet_run" ]; then + mkdir -p "$RUNNER_TEMP/evidence/fleet-report-backs" + gh run download "$fleet_run" --repo "$GITHUB_REPOSITORY" \ + --name fleet-report-backs \ + --dir "$RUNNER_TEMP/evidence/fleet-report-backs" || \ + echo "::warning::Fleet evidence unavailable; fleet rows will remain orange" + fi + ev="$RUNNER_TEMP/evidence" + evidence_ok=1 + if ! bash scripts/verdict-grid-lane.sh validate \ + --hypatia "$ev/hypatia-scan-findings/hypatia-findings.relativized.json" \ + --panic "$ev/static-analysis-findings/panic-attack-findings.json" \ + --pons "$ev/static-analysis-findings/pons-findings.json" \ + --enrolment .gitbot-fleet/enrolment.json; then + echo "::warning::Producer evidence is malformed; publishing an indeterminate (orange) report, never a stale green result." + evidence_ok=0 + fi + if ! gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$TARGET_PR/files?per_page=100" \ + --jq '.[].filename' > "$RUNNER_TEMP/changed-files.txt"; then + echo "::warning::Changed-file scope could not be established; refusing a misleading scoped verdict." + evidence_ok=0 + fi + if ! bash scripts/verdict-grid-lane.sh previous \ + --repo "$GITHUB_REPOSITORY" --pr "$TARGET_PR" \ + --out "$RUNNER_TEMP/previous.md"; then + echo "::warning::No trustworthy previous grid is available for a delta." + : > "$RUNNER_TEMP/previous.md" + fi + args=(render --out "$RUNNER_TEMP/verdict-grid" \ + --renderer-dir .standards-grid \ + --repo "$GITHUB_REPOSITORY" \ + --sha "$TARGET_SHA" --run "$GITHUB_RUN_ID" --pr "$TARGET_PR" \ + --url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + --enrolment .gitbot-fleet/enrolment.json \ + --hypatia "$ev/hypatia-scan-findings/hypatia-findings.relativized.json" \ + --panic "$ev/static-analysis-findings/panic-attack-findings.json" \ + --pons "$ev/static-analysis-findings/pons-findings.json" \ + --hypatia-ref be98f74ce93df200752f3798efa1305bfa3e0e10 \ + --panic-ref b5530834a839734d1d181bfe6edb0d43d4233cf4 \ + --pons-ref 51b8920f541782ceb9465ea3f0d33c96104fc1fb) + [ ! -d "$ev/fleet-report-backs" ] || args+=(--report-backs "$ev/fleet-report-backs") + [ ! -s "$RUNNER_TEMP/previous.md" ] || args+=(--previous "$RUNNER_TEMP/previous.md") + args+=(--changed "$RUNNER_TEMP/changed-files.txt") + if [ "$evidence_ok" -eq 1 ]; then + if ! bash scripts/verdict-grid-lane.sh "${args[@]}"; then + echo "::warning::Verdict rendering failed; replacing the report with an explicit indeterminate state." + evidence_ok=0 + fi + fi + if [ "$evidence_ok" -ne 1 ]; then + mkdir -p "$RUNNER_TEMP/verdict-grid" + cat > "$RUNNER_TEMP/verdict-grid/report.md" <<'REPORT' + + ### Verdict grid: indeterminate + + Producer evidence was malformed, unreadable or could not be rendered. + No green result or comparative delta can be asserted for this revision. + See the trusted publisher workflow run for details. + REPORT + printf '%s\n' '{"schemaVersion":1,"label":"Hypatia grid","message":"indeterminate","color":"orange"}' > "$RUNNER_TEMP/verdict-grid/badge.json" + printf '%s\n' '{"state":"orange","reason":"unverified producer evidence"}' > "$RUNNER_TEMP/verdict-grid/report.json" + fi + # Recheck freshness immediately before the write; do not publish + # a completed scan from a now-superseded head. + gh api "repos/$GITHUB_REPOSITORY/pulls/$TARGET_PR" | + jq -e --arg sha "$TARGET_SHA" '.state == "open" and .head.sha == $sha' >/dev/null + bash scripts/verdict-grid-lane.sh comment \ + --repo "$GITHUB_REPOSITORY" --pr "$TARGET_PR" \ + --report "$RUNNER_TEMP/verdict-grid" diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index 35dfd9d..ea906ab 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -6,6 +6,10 @@ name: Workflow Security Linter on: push: + # A PR push already runs this through pull_request; push runs only on + # main, so each PR push is linted once and main is still checked after merge. + branches: + - main paths: - '.github/workflows/**' pull_request: @@ -21,7 +25,7 @@ permissions: jobs: lint-workflows: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read @@ -38,7 +42,7 @@ jobs: [ -f "$file" ] || continue # actions-lock may prepend its own comment. Require the licence # in the leading comment block, before the workflow body. - if ! awk '/^# SPDX-License-Identifier:/ { found=1 } /^[^#[:space:]]/ { exit } END { exit !found }' "$file"; then + if ! awk '/^# SPDX-License-Identifier:/ { found=1 } /^[[:space:]]*[^#[:space:]]/ { exit } END { exit !found }' "$file"; then echo "ERROR: $file missing SPDX header" failed=1 fi @@ -67,12 +71,17 @@ jobs: echo "All workflows have permissions declared" # Bun is the estate's first-choice runtime (LANGUAGE-POLICY.adoc §1: - # Bun > Deno > pnpm > npm) and executes .ts directly (§1.2). 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/.gitignore b/.gitignore index 0998acf..11d867e 100644 --- a/.gitignore +++ b/.gitignore @@ -120,7 +120,7 @@ dist/ .machine_readable/arrival-pack/claude-md-data.json # Coaptation atomiser artifacts (regenerated from contractiles + descriptiles); -# the receipt itself (receipts/latest.a2ml) IS committed as the drift baseline. +# the receipt itself (receipts/latest.toml) IS committed as the drift baseline. .machine_readable/coaptation/clauses.json .machine_readable/coaptation/facts.json @@ -128,7 +128,7 @@ dist/ .machine_readable/coaptation/core/build/ # Coaptation re-anchor basis (occasional, generated on --reanchor when red; not a baseline) -.machine_readable/coaptation/receipts/reanchor-basis.a2ml +.machine_readable/coaptation/receipts/reanchor-basis.toml # Generated by `just cookbook` (has timestamp; non-deterministic) docs/just-cookbook.adoc diff --git a/.hypatia-ignore b/.hypatia-ignore index 1722a94..b4c79dc 100644 --- a/.hypatia-ignore +++ b/.hypatia-ignore @@ -13,18 +13,6 @@ # below carries a stated reason. Prefer fix-the-code or an inline directive # over an entry here; this file is for whole-file/directory exemptions. -# ─── build/docs-seed/ describes DOWNSTREAM repos, not this one ───────────── -# -# `build/docs-seed/` is the canonical template of documentation that gets -# copied into scaffolded repos. Its example paths (e.g. `src/middleware/…`, -# `src/main.rs`) describe a hypothetical generated project, so they -# legitimately do not resolve against this template's own tree. SD022 -# (stale `src//` reference) is therefore a structural false positive -# for everything under this directory — mirrors the built-in CHANGELOG.md -# and third_party/ carve-outs. Scaffolded repos inherit this file, so the -# exemption travels with the template. -structural_drift/SD022:build/docs-seed/ - # ─── Research-extension workflow findings ─────────────────────────────────────── # # RE001 is an advisory recommendation to add harden-runner whenever a workflow @@ -53,3 +41,10 @@ research_extensions/RE005:.github/workflows/static-analysis-gate.yml # Dependabot workflow uses the unprivileged pull_request event and also verifies # github.event.pull_request.user.login, so that threat model does not apply. research_extensions/RE008:.github/workflows/dependabot-automerge.yml + +# wrong_sha_pin compares 40-hex action pins against Hypatia's static May-2026 +# @sha_pins table (e.g. actions/checkout@v4 vs the current actions/checkout@v7.0.1 +# and ossf/scorecard-action@v2.5.0). Authoritative pin verification is enforced by +# .github/workflows/actions.lock + scripts/check-lock-sync.sh + GitHub's native +# lockfile verifier on every workflow run. +workflow_audit/wrong_sha_pin:.github/workflows/ diff --git a/.machine_readable/ENSAID_CONFIG.a2ml b/.machine_readable/ENSAID_CONFIG.a2ml deleted file mode 100644 index 1384822..0000000 --- a/.machine_readable/ENSAID_CONFIG.a2ml +++ /dev/null @@ -1,96 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -# -# ENSAID_CONFIG.a2ml — eNSAID Environment Configuration -# Per-repo configuration for PanLL and eNSAID-compatible tools. -# -# Canonical location: .machine_readable/ENSAID_CONFIG.a2ml -# Spec: https://github.com/hyperpolymath/standards/tree/main/ensaid-config -# -# Naming convention: -# - UPPERCASE + underscore = non-executable machine-readable file -# - Lives in .machine_readable/ alongside STATE.a2ml, META.a2ml, etc. - -# ───────────────────────────────────────────────────────────────── -# [ensaid] — Core eNSAID identity and version -# ───────────────────────────────────────────────────────────────── -[ensaid] -version = "1.0.0" -tool = "panll" - -# ───────────────────────────────────────────────────────────────── -# [workspace] — Workspace mode, protection, and execution policy -# ───────────────────────────────────────────────────────────────── -[workspace] -mode = "rhodium" # rhodium | gold | silver | bronze -protection = "open" # open | guarded | locked -execution = "live" # live | dry-run | approval-required - -# ───────────────────────────────────────────────────────────────── -# [preferences] — User/repo-level display and behaviour preferences -# ───────────────────────────────────────────────────────────────── -[preferences] -humidity = "medium" # high | medium | low (drift aura intensity) -default-arrangement = "default-3-panel" # workspace arrangement ID -auto-connect = true # auto-connect panels to backends on load - -# ───────────────────────────────────────────────────────────────── -# [panels] — Panel visibility, enablement, and isolation overrides -# ───────────────────────────────────────────────────────────────── -[panels] -version = "1.0.0" - -# By default, all panels are available. Uncomment to restrict: -# [[panels.enabled]] -# id = "valence-shell" -# isolation = "native" -# auto-connect = true -# -# [[panels.enabled]] -# id = "editor-bridge" -# isolation = "native" -# auto-connect = true - -# Panels to hide for this repo context: -# [panels.disabled] -# ids = [] - -# ───────────────────────────────────────────────────────────────── -# [workflows] — Automation Router event-driven cross-panel rules -# ───────────────────────────────────────────────────────────────── -[workflows] -version = "1.0.0" - -# Example: rebuild on file save -# [[workflows.rule]] -# name = "build-on-save" -# trigger = { event = "file-changed", pattern = "src/**/*.res" } -# condition = { panel = "build-dashboard", field = "watchMode", equals = true } -# action = { panel = "build-dashboard", message = "TriggerBuild", args = { target = "game" } } -# approval = "auto-fire" # auto-fire | require-approval | approve-once | dry-run-first - -# ───────────────────────────────────────────────────────────────── -# [clades] — Panel clade trait and capability overrides -# ───────────────────────────────────────────────────────────────── -[clades] -version = "1.0.0" - -# Example: add a custom capability to a panel clade -# [[clades.override]] -# id = "build-dashboard" -# traits = { has-work-items = true } -# capabilities-add = ["CustomCheck"] - -# ───────────────────────────────────────────────────────────────── -# [portfolios] — Custom panel bundles for this repo's workflow -# ───────────────────────────────────────────────────────────────── -[portfolios] -version = "1.0.0" - -# Example: a custom portfolio for this project -# [[portfolios.custom]] -# id = "{{project}}-dev" -# name = "{{PROJECT_NAME}} Development" -# description = "Panels for {{PROJECT_NAME}} development" -# panels = ["valence-shell", "editor-bridge", "build-dashboard"] -# default-isolation = "native" diff --git a/.machine_readable/README.adoc b/.machine_readable/README.adoc index fd6b719..d7489cd 100644 --- a/.machine_readable/README.adoc +++ b/.machine_readable/README.adoc @@ -16,16 +16,18 @@ document is the only place the syntax, semantics and typechecking rules are defined. Every other mention of the format across the estate — including every README in this tree — is a *pointer* to it, never a restatement. -The format formerly called A2ML is now *DEED* (`.deed`). Files here still -carry `.a2ml` pending a single atomic estate-wide rename; do not -hand-convert them. +The format formerly called A2ML is now *DEED* (`.deed`), and no `.a2ml` +file may exist in this repository. The descriptive records are clauses of +the repo deed (`*_chora.deed`) at the repo root, read with +`scripts/deed-field.sh` and written with `scripts/deed-clause.sh`. ==== == Contents `descriptiles/`:: What this repository *is* and what state it is *in* — -`META`, `STATE`, `ECOSYSTEM`, `PLAYBOOK`, `AGENTIC`, `NEUROSYM`, plus the -anchors and the facet record. See `descriptiles/README.adoc`. +the repo deed's `(meta)`, `(status)`, `(ecosystem)`, `(playbook)`, +`(agentic)` and `(neurosym)` clauses, plus the `(anchor)` clause and the +facet record. See `descriptiles/README.adoc`. `contractiles/`:: What this repository *ought* to do — the Must, Trust, Adjust, Intend, Bust and Dust verb sets, each with its declaration, its @@ -34,7 +36,8 @@ Nickel runner and its k9 service-automation component. See `arrival-pack/`:: The entry pack for agents visiting this repository. -`policies/`:: Maintenance axes and related policy records. +`policies/`:: Maintenance axes and related policy records, now the repo +deed's `(policies …)` clauses. -`rsr-profile.a2ml`:: This repository's declared RSR v2.0 capabilities, +`(rsr-profile)` (repo deed clause):: This repository's declared RSR v2.0 capabilities, which gate which conformance criteria apply to it. diff --git a/.machine_readable/ai/AI.a2ml b/.machine_readable/ai/AI.a2ml deleted file mode 100644 index d25039f..0000000 --- a/.machine_readable/ai/AI.a2ml +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 - -# AI Assistant Instructions - -## Repository Focus -- `rsr-template-repo` is treated as a Rhodium Standard Repository; obey the Rhodium policies and keep `.machine_readable/` authoritative. -- All machine-readable content lives under `.machine_readable/` — state files (a2ml), bot directives, and contractiles. -- Prefer to keep generated files out of source control, and regenerate them with the documented commands before committing. - -## Workflow -1. Inspect `.machine_readable/descriptiles/STATE.a2ml` for blockers and next actions. -2. Respect any constraints listed inside `.machine_readable/descriptiles/AGENTIC.a2ml` when tooling changes are requested. -3. After finishing edits, update STATE with your outcomes and commit with a concise, imperative message. - -## Delivery Promises -- Mention in summaries whether STATE, `.machine_readable/contractiles/`, or `.machine_readable/bot_directives/` changed. -- Keep this file in sync with the repository's status; update it if the governance changes. - -[foundational-integrations] -# These tools form the architectural floor for all hyperpolymath repos. -# Integrate them incrementally as the project matures. - -proven = "Formally verified safety library — replace raw string/JSON/URL/crypto ops" -panic-attacker = "Static analysis — run `just assail` before every commit" -feedback-o-tron = "Autonomous bug filing — auto-report upstream dependency failures" -verisimdb = "Cross-repo analytics — emit scan results and build metrics" -vexometer = "Irritation surface analysis — measure friction in CLI/UI tools" -hypatia = "Neurosymbolic CI/CD — safety-triangle routing for automated fixes" -boj-server = "MCP gateway — all external service integrations route through BoJ" - -[floor-raise-checklist] -step-1 = "Ensure the repo deed (_chora.deed) exists at repo root" -step-2 = "Ensure Mustfile.a2ml + Trustfile.a2ml + Dustfile.a2ml in .machine_readable/contractiles/" -step-3 = "Add `assail` recipe to Justfile and wire into pre-commit hook" -step-4 = "Add proven integration reference in .machine_readable/integrations/" -step-5 = "Add verisimdb feed configuration" -step-6 = "Add feedback-o-tron integration for upstream reporting" -step-7 = "Add vexometer hooks for friction measurement (CLI/UI repos)" diff --git a/.machine_readable/ai/PLACEHOLDERS.adoc b/.machine_readable/ai/PLACEHOLDERS.adoc index b10b863..58b7733 100644 --- a/.machine_readable/ai/PLACEHOLDERS.adoc +++ b/.machine_readable/ai/PLACEHOLDERS.adoc @@ -20,16 +20,16 @@ validates the result, and runs k9-svc checks if available. ```bash # If you prefer manual replacement (run from repo root) -sed -i 's/{{AUTHOR}}/Jane Doe/g' $(grep -rl '{{AUTHOR}}' .) -sed -i 's/{{AUTHOR_EMAIL}}/jane@example.org/g' $(grep -rl '{{AUTHOR_EMAIL}}' .) -sed -i 's/{{OWNER}}/my-org/g' $(grep -rl '{{OWNER}}' .) -sed -i 's/{{PROJECT_NAME}}/my-project/g' $(grep -rl '{{PROJECT_NAME}}' .) -sed -i 's/{{PROJECT}}/MY_PROJECT/g' $(grep -rl '{{PROJECT}}' .) -sed -i 's/{{project}}/my_project/g' $(grep -rl '{{project}}' .) -sed -i 's/{{REPO}}/my-project/g' $(grep -rl '{{REPO}}' .) -sed -i 's/{{FORGE}}/github.com/g' $(grep -rl '{{FORGE}}' .) -sed -i "s/{{CURRENT_YEAR}}/$(date +%Y)/g" $(grep -rl '{{CURRENT_YEAR}}' .) -sed -i "s/{{CURRENT_DATE}}/$(date +%Y-%m-%d)/g" $(grep -rl '{{CURRENT_DATE}}' .) +sed -i 's/Jonathan D.A. Jewell/Jane Doe/g' $(grep -rl 'Jonathan D.A. Jewell' .) +sed -i 's/j.d.a.jewell@open.ac.uk/jane@example.org/g' $(grep -rl 'j.d.a.jewell@open.ac.uk' .) +sed -i 's/hyperpolymath/my-org/g' $(grep -rl 'hyperpolymath' .) +sed -i 's/occupancy-types/My Project/g' $(grep -rl 'occupancy-types' .) +sed -i 's/OCCUPANCY_TYPES/MY_PROJECT/g' $(grep -rl 'OCCUPANCY_TYPES' .) +sed -i 's/occupancy_types/my_project/g' $(grep -rl 'occupancy_types' .) +sed -i 's/occupancy-types/my-project/g' $(grep -rl 'occupancy-types' .) +sed -i 's/github.com/github.com/g' $(grep -rl 'github.com' .) +sed -i "s/2026/$(date +%Y)/g" $(grep -rl '2026' .) +sed -i "s/2026-11-12/$(date +%Y-%m-%d)/g" $(grep -rl '2026-11-12' .) ``` ## Placeholder Reference @@ -38,58 +38,74 @@ sed -i "s/{{CURRENT_DATE}}/$(date +%Y-%m-%d)/g" $(grep -rl '{{CURRENT_DATE}}' .) | Placeholder | Description | Example | Files | |---|---|---|---| -| `{{AUTHOR}}` | Full legal name | `Jane Doe` | SPDX headers (all files), MAINTAINERS.md, .mailmap, .reuse/dep5, docs/AI-CONVENTIONS.md | -| `{{AUTHOR_EMAIL}}` | Primary contact email | `jane@example.org` | SPDX headers (all files), .mailmap, .reuse/dep5, www/.well-known/humans.txt | +| `Jonathan D.A. Jewell` | Full legal name | `Jane Doe` | SPDX headers (all files), MAINTAINERS.md, .mailmap, .reuse/dep5, docs/AI-CONVENTIONS.md | +| `j.d.a.jewell@open.ac.uk` | Primary contact email | `jane@example.org` | SPDX headers (all files), .mailmap, .reuse/dep5, www/.well-known/humans.txt | | `{{AUTHOR_EMAIL_ALT}}` | Previous/secondary email (for .mailmap) | `old@example.com` | .mailmap | -| `{{AUTHOR_ORG}}` | Author's organization/affiliation | `Acme University` | project-metadata.k9.ncl | -| `{{AUTHOR_LAST}}` | Author surname (for citations) | `Doe` | docs/CITATIONS.adoc | -| `{{AUTHOR_FIRST}}` | Author first name (for citations) | `Jane` | docs/CITATIONS.adoc | -| `{{AUTHOR_INITIALS}}` | Author initials (for citations) | `J.` | docs/CITATIONS.adoc | +| `` | Author's organization/affiliation | `Acme University` | project-metadata.k9.ncl | +| `Jewell` | Author surname (for citations) | `Doe` | docs/CITATIONS.adoc | +| `Jonathan D.A.` | Author first name (for citations) | `Jane` | docs/CITATIONS.adoc | +| `J.` | Author initials (for citations) | `J.` | docs/CITATIONS.adoc | +| `{{AUTHOR_ORCID}}` | Author ORCID iD, bare form; blank drops the line (default: `git config user.orcid`) | `0000-0002-1825-0097` | CITATION.cff | ### Project Identity +Three DIFFERENT things are named "the project" and they are three different +tokens. Conflating them is how a repository ends up with a display name where an +identifier belongs: + +* `occupancy-types` — what a human reads: spaces, capitals, punctuation. +* `OCCUPANCY_TYPES` / `occupancy_types` — what a compiler reads: an identifier, so no + spaces and no hyphens. `OCCUPANCY_TYPES` is the uppercase form (Idris2 modules, C + macros); `occupancy_types` is the lowercase form (C symbols, file names, crate + and module names). +* `occupancy-types` — what a forge reads: the repository slug. + +They may look alike today (`My Project` / `MY_PROJECT` / `my_project` / +`my-project`) and they are still four tokens, because each is filled from a +different answer in `just repo-init`. A file that needs all four needs all four. + | Placeholder | Description | Example | Files | |---|---|---|---| -| `{{PROJECT_NAME}}` | Human-readable project name | `My Project` | SECURITY.md, CODE_OF_CONDUCT.md, TOPOLOGY.md, STATE.a2ml, Justfile, GOVERNANCE.md, MAINTAINERS.md, devcontainer.json | -| `{{PROJECT_DESCRIPTION}}` | One-line description | `A tool for X` | README.adoc, build/container/Containerfile | -| `{{PROJECT}}` | Uppercase identifier (for Idris2 modules, C macros) | `MY_PROJECT` | ABI-FFI-README.md, src/interface/abi/*.idr, src/interface/ffi/*.zig | -| `{{project}}` | Lowercase identifier (for C symbols, filenames) | `my_project` | ABI-FFI-README.md, src/interface/ffi/*.zig | -| `{{REPO}}` | Repository name (slug) | `my-project` | CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, cliff.toml | -| `{{OWNER}}` | GitHub/GitLab org or username | `my-org` | SPDX headers, CONTRIBUTING.md, SECURITY.md, GOVERNANCE.md, MAINTAINERS.md, CODEOWNERS, mirror.yml, cliff.toml | -| `{{FORGE}}` | Git forge domain | `github.com` | CONTRIBUTING.md | +| `occupancy-types` | Human-readable project name | `My Project` | SECURITY.md, CODE_OF_CONDUCT.md, TOPOLOGY.md, docs/status/ROADMAP.adoc (via build/templates/ROADMAP.adoc.in), Justfile, GOVERNANCE.md, MAINTAINERS.md, devcontainer.json | +| `Early-stage research on compositional worst-case resource bounds, with a linear session-IR checker, occupancy-indexed state experiments in Idris 2, and a prototype stack-depth certificate checker. The long-term goal is predictable memory and network-buffer usage.` | One-line description | `A tool for X` | README.adoc, CITATION.cff, the repo deed | +| `OCCUPANCY_TYPES` | Uppercase identifier (for Idris2 modules, C macros) | `MY_PROJECT` | ABI-FFI-README.md, src/interface/abi/*.idr, src/interface/ffi/*.zig | +| `occupancy_types` | Lowercase identifier (for C symbols, filenames) | `my_project` | ABI-FFI-README.md, src/interface/ffi/*.zig | +| `occupancy-types` | Repository name (slug) | `my-project` | CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, cliff.toml | +| `hyperpolymath` | GitHub/GitLab org or username | `my-org` | SPDX headers, CONTRIBUTING.md, SECURITY.md, GOVERNANCE.md, MAINTAINERS.md, CODEOWNERS, mirror.yml, cliff.toml | +| `github.com` | Git forge domain | `github.com` | CONTRIBUTING.md | ### Dates | Placeholder | Description | Example | Files | |---|---|---|---| -| `{{CURRENT_YEAR}}` | Current year | `2026` | SPDX headers (all files), GOVERNANCE.md, MAINTAINERS.md | -| `{{CURRENT_DATE}}` | Current date (ISO) | `2026-02-14` | STATE.a2ml, MAINTAINERS.md | -| `{{DATE}}` | Last updated date | `2026-02-14` | TOPOLOGY.md, THREAT-MODEL.md | +| `2026` | Current year | `2026` | SPDX headers (all files), GOVERNANCE.md, MAINTAINERS.md | +| `2026-11-12` | Current date (ISO) | `2026-02-14` | `*_chora.deed` (via build/templates/identity.deed.in), MAINTAINERS.md | +| `2026-11-12` | Last updated date | `2026-02-14` | TOPOLOGY.md, THREAT-MODEL.md | ### Contact & Security | Placeholder | Description | Example | Files | |---|---|---|---| -| `{{SECURITY_EMAIL}}` | Security contact email | `security@example.org` | SECURITY.md | +| `j.d.a.jewell@open.ac.uk` | Security contact email | `security@example.org` | SECURITY.md | | `{{PGP_FINGERPRINT}}` | 40-char PGP fingerprint | `ABCD 1234 ...` | SECURITY.md | | `{{PGP_KEY_URL}}` | URL to public PGP key | `https://keys.openpgp.org/...` | SECURITY.md | -| `{{WEBSITE}}` | Project website | `https://example.org` | SECURITY.md | -| `{{CONDUCT_EMAIL}}` | Conduct reports email | `conduct@example.org` | CODE_OF_CONDUCT.md | -| `{{CONDUCT_TEAM}}` | Conduct committee name | `Code of Conduct Committee` | CODE_OF_CONDUCT.md | -| `{{RESPONSE_TIME}}` | SLA for initial response | `48 hours` | CODE_OF_CONDUCT.md | +| `https://github.com/hyperpolymath/occupancy-types` | Project website | `https://example.org` | SECURITY.md | +| `j.d.a.jewell@open.ac.uk` | Conduct reports email | `conduct@example.org` | CODE_OF_CONDUCT.md | +| `Code of Conduct Committee` | Conduct committee name | `Code of Conduct Committee` | CODE_OF_CONDUCT.md | +| `48 hours` | SLA for initial response | `48 hours` | CODE_OF_CONDUCT.md | ### Git | Placeholder | Description | Example | Files | |---|---|---|---| -| `{{MAIN_BRANCH}}` | Main branch name | `main` | CONTRIBUTING.md | +| `main` | Main branch name | `main` | CONTRIBUTING.md | ### Build | Placeholder | Description | Example | Files | |---|---|---|---| -| `{{LICENSE}}` | License name | `MPL-2.0` | ABI-FFI-README.md | -| `{{PROJECT_PURPOSE}}` | One-line project description | `FFI bridges between languages` | STATE.a2ml | +| `MPL-2.0` | License name | `MPL-2.0` | ABI-FFI-README.md | +| `Early-stage research on compositional worst-case resource bounds, with a linear session-IR checker, occupancy-indexed state experiments in Idris 2, and a prototype stack-depth certificate checker. The long-term goal is predictable memory and network-buffer usage.` | One-line project description | `FFI bridges between languages` | `*_chora.deed` `(anchor (identity))` :one-sentence (set from `Early-stage research on compositional worst-case resource bounds, with a linear session-IR checker, occupancy-indexed state experiments in Idris 2, and a prototype stack-depth certificate checker. The long-term goal is predictable memory and network-buffer usage.` by `just repo-init`) | | `{{OPENSSF_BP_ID}}` | OpenSSF Best Practices numeric project ID (from registering at bestpractices.dev). Optional — leave blank until registered; `just repo-init` then surfaces it in the post-init "remaining placeholders" notice as a reminder. | `1234` | README.adoc (OpenSSF Best Practices badge) | ### Project Content & Onboarding (manual-fill, post-init) @@ -102,20 +118,20 @@ surfaces any left unfilled. | Placeholder | Description | Example | Files | |---|---|---|---| -| `{{PACKAGE_NAME}}` | Package / module name (slug) | `my-project` | docs/onboarding/QUICKSTART-MAINTAINER.adoc | -| `{{DESCRIPTION}}` | Short repo description (forge metadata) | `A tool for X` | .github/settings.yml | -| `{{LANG_STACK}}` | Language / toolchain stack | `Idris2 + Zig` | docs/onboarding/QUICKSTART-DEV.adoc | -| `{{BUILD_CMD}}` | Build command | `just build` | docs/onboarding/QUICKSTART-DEV.adoc | -| `{{TEST_CMD}}` | Test command | `just test` | docs/onboarding/QUICKSTART-DEV.adoc | -| `{{BUILD_OUTPUT_PATH}}` | Build artefact output path | `build/` | docs/onboarding/QUICKSTART-MAINTAINER.adoc | -| `{{DEPS}}` | Project dependencies summary | `Idris2 0.7, Zig 0.13` | docs/onboarding/QUICKSTART-MAINTAINER.adoc | -| `{{MUST_INVARIANTS}}` | Required project invariants | `no believe_me; totality on` | docs/onboarding/QUICKSTART-DEV.adoc | -| `{{MAIN_FUNCTION}}` | Primary function / purpose | `FFI bridge generation` | machine-readable-design/canonical-directory-structure/Intentfile.a2ml | -| `{{TARGET_AUDIENCE}}` | Intended audience | `library authors` | machine-readable-design/canonical-directory-structure/Intentfile.a2ml | -| `{{EXPECTED_AUTHOR}}` | Expected author identity (trust anchor) | `hyperpolymath` | machine-readable-design/canonical-directory-structure/Trustfile.a2ml | -| `{{PROJECT_UNIQUE_STRENGTH}}` | What this project does better than alternatives | `proof-carrying FFI` | .machine_readable/bot_directives/methodology.a2ml | -| `{{PROJECT_KIND}}` | Project kind / category | `library` | .machine_readable/descriptiles/anchors/ANCHOR.a2ml | -| `{{PROJECT_DOMAIN}}` | Project domain | `formal-methods` | .machine_readable/descriptiles/anchors/ANCHOR.a2ml | +| `UNASSIGNED` | Package / module name (slug) | `my-project` | docs/onboarding/QUICKSTART-MAINTAINER.adoc | +| `Early-stage research on compositional worst-case resource bounds, with a linear session-IR checker, occupancy-indexed state experiments in Idris 2, and a prototype stack-depth certificate checker. The long-term goal is predictable memory and network-buffer usage.` | Short repo description (forge metadata) | `A tool for X` | .github/settings.yml | +| `UNASSIGNED` | Language / toolchain stack | `Idris2 + Zig` | docs/onboarding/QUICKSTART-DEV.adoc | +| `UNASSIGNED` | Build command | `just build` | docs/onboarding/QUICKSTART-DEV.adoc | +| `UNASSIGNED` | Test command | `just test` | docs/onboarding/QUICKSTART-DEV.adoc | +| `UNASSIGNED` | Build artefact output path | `build/` | docs/onboarding/QUICKSTART-MAINTAINER.adoc | +| `UNASSIGNED` | Project dependencies summary | `Idris2 0.7, Zig 0.13` | docs/onboarding/QUICKSTART-MAINTAINER.adoc | +| `UNASSIGNED` | Required project invariants | `no believe_me; totality on` | docs/onboarding/QUICKSTART-DEV.adoc | +| `UNASSIGNED` | Primary function / purpose | `FFI bridge generation` | no file in this template (the retired Intentfile is now `.machine_readable/contractiles/intend/intend.k9.ncl`, which does not carry the token) | +| `UNASSIGNED` | Intended audience | `library authors` | no file in this template (the retired Intentfile is now `.machine_readable/contractiles/intend/intend.k9.ncl`, which does not carry the token) | +| `{{EXPECTED_AUTHOR}}` | Expected author identity (trust anchor) | `hyperpolymath` | no file in this template (the retired Trustfile is now `.machine_readable/contractiles/trust/trust.k9.ncl`, which does not carry the token) | +| `UNASSIGNED` | What this project does better than alternatives | `proof-carrying FFI` | `*_chora.deed` `(bot-directives (methodology (methodology (unique-strength))))` :description | +| `UNASSIGNED` | Project kind / category | `library` | `*_chora.deed` `(anchor (identity))` :kind | +| `UNASSIGNED` | Project domain | `formal-methods` | `*_chora.deed` `(anchor (identity))` :domain | | `{{OPENSSF_PROJECT_ID}}` | OpenSSF Best Practices project ID (same value as `{{OPENSSF_BP_ID}}`) | `1234` | docs/governance/TEMPLATE-STANDARDS-AUDIT.adoc | ### Repo deed (folded family-7 allocation manifest) @@ -125,7 +141,7 @@ surfaces any left unfilled. | `[YOUR-REPO-NAME]` | Repository name | `my-project` | `*_chora.deed` (repo deed) | | `[YOUR-NAME/ORG]` | Maintainer name | `hyperpolymath` | `*_chora.deed` (repo deed) | -(`[DATE]` was listed here for 0-AI-MANIFEST.a2ml but no file ever carried the +(`[DATE]` was listed here for the root AI manifest but no file ever carried the literal token — row removed with the fold; standards#837 pilot.) ### AI Installation Guide @@ -162,7 +178,7 @@ Some files contain deletion instructions: After replacing all placeholders, verify none remain: ```bash -grep -rn '{{' . --include='*.md' --include='*.adoc' --include='*.a2ml' \ +grep -rn '{{' . --include='*.md' --include='*.adoc' --include='*.deed' \ --include='*.scm' --include='*.idr' --include='*.zig' --include='*.res' \ --include='Justfile' --include='*.nix' --include='*.toml' --include='*.yml' \ --include='*.yaml' --include='*.hs' --include='*.ncl' --include='*.txt' \ diff --git a/.machine_readable/ai/README.adoc b/.machine_readable/ai/README.adoc index a5dec59..628a0c5 100644 --- a/.machine_readable/ai/README.adoc +++ b/.machine_readable/ai/README.adoc @@ -2,23 +2,74 @@ // Copyright (c) Jonathan D.A. Jewell = AI Guidance Directory -Put AI-facing instructions in this folder. +Put AI-facing instructions in this folder (for example `CLAUDE.md`, +`COPILOT.md`, `GEMINI.md`, `AI.djot`). Avoid scattering agent instruction +files around the repo root. Machine-readable facts do not live here: they are +clauses of the repo deed (`*_chora.deed`) at the repo root, read with +`scripts/deed-field.sh`. There are no `.a2ml` files in this estate. -Examples: +== Recommended machine read order -* `CLAUDE.md` -* `COPILOT.md` -* `GEMINI.md` -* `AI.a2ml` -* `AI.djot` +* the repo deed's `(anchor)` clause — semantic authority and golden path +* the repo deed's `(policies)` clause — `(maintenance-axes)`, + `(maintenance-checklist)`, `(software-development-approach)`, + `(crg-criteria)`, `(tsdm)` +* the repo deed's `(status)`, `(maturity)` and `(meta)` clauses +* `docs/status/ROADMAP.adoc` -Avoid scattering agent instruction files around the repo root. +== AI assistant instructions -Recommended machine read order: +=== Repository focus -* `.machine_readable/descriptiles/anchors/ANCHOR.a2ml` -* `.machine_readable/policies/MAINTENANCE-AXES.a2ml` -* `.machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml` -* `.machine_readable/policies/SOFTWARE-DEVELOPMENT-APPROACH.a2ml` -* `.machine_readable/descriptiles/STATE.a2ml` -* `.machine_readable/descriptiles/META.a2ml` +* This repository is a Rhodium Standard Repository; obey the Rhodium policies + and keep the repo deed and `.machine_readable/` authoritative. +* Machine-readable content is the repo deed (`*_chora.deed`, at the root) plus + `.machine_readable/` (contractiles, bot-directive and integration clauses are + in the deed). +* Keep generated files out of source control, and regenerate them with the + documented commands before committing. + +=== Workflow + +. Read `docs/status/ROADMAP.adoc` for blockers and next actions, and the repo + deed's `(status)` clause for the phase. +. Respect the constraints in the repo deed's `(agentic)` clause when tooling + changes are requested. +. After finishing edits, update `docs/status/ROADMAP.adoc` with your outcomes + and commit with a concise, imperative message. + +=== Delivery promises + +* Say in summaries whether the repo deed, `docs/status/ROADMAP.adoc` or + `.machine_readable/contractiles/` changed. +* Keep this file in sync with the repository's status; update it if the + governance changes. + +== Foundational integrations + +These tools form the architectural floor for all hyperpolymath repos. Integrate +them incrementally as the project matures; their configuration is the repo +deed's `(integrations)` clause. + +[cols="1,3"] +|=== +| proven | Formally verified safety library — replace raw string/JSON/URL/crypto ops +| panic-attacker | Static analysis — run `just assail` before every commit +| feedback-o-tron | Autonomous bug filing — auto-report upstream dependency failures +| verisimdb | Cross-repo analytics — emit scan results and build metrics +| vexometer | Irritation surface analysis — measure friction in CLI/UI tools +| hypatia | Neurosymbolic CI/CD — safety-triangle routing for automated fixes +| boj-server | MCP gateway — all external service integrations route through BoJ +|=== + +== Floor-raise checklist + +. The repo deed (`_chora.deed`) exists at the repo root and lints. +. The contractiles exist: `.machine_readable/contractiles//.k9.ncl` + for must, trust and dust at least. +. The `assail` recipe is in the Justfile and wired into the pre-commit hook. +. The deed's `(integrations (proven))` clause is filled in. +. The deed's `(integrations (verisimdb))` feed is configured. +. The deed's `(integrations (feedback-o-tron))` clause is filled in for + upstream reporting. +. The deed's `(integrations (vexometer))` hooks are set (CLI/UI repos). diff --git a/.machine_readable/arrival-pack/README.adoc b/.machine_readable/arrival-pack/README.adoc index a48f9fd..c8ceb8a 100644 --- a/.machine_readable/arrival-pack/README.adoc +++ b/.machine_readable/arrival-pack/README.adoc @@ -10,9 +10,9 @@ the repo's own machine-readable truth. [source] ---- -a2ml (descriptiles) extract.sh arrival-pack.ncl CLAUDE.md -CLADE/ECOSYSTEM/ ──▶ (a2ml → JSON, ──▶ (Nickel projection ──▶ region between -AGENTIC/STATE/ANCHOR the thin reader) = the engine) BEGIN/END markers +_chora.deed extract.sh arrival-pack.ncl CLAUDE.md +(incl. its (anchor (deed → JSON, (Nickel projection region between + (golden-path))) ──▶ the thin reader) ──▶ = the engine) ──▶ BEGIN/END markers ---- Two halves of the generated region: @@ -20,17 +20,20 @@ Two halves of the generated region: * *Estate-common* — identical in every repo; the Manifesto doctrine + format-family orientation + canon pointers + language policy. Pinned to a Manifesto SHA once the wording is ratified (today: `DRAFT-unratified`). -* *Repo-specific* — projected from this repo's a2ml: identity, IS / IS-NOT, +* *Repo-specific* — projected from this repo's deed: identity, IS / IS-NOT, position, constraints, golden path, state. -The a2ml files remain the single source of truth. CLAUDE.md is a *view*. Content +The repo deed (`_chora.deed`) is the single source of truth; it took +over the former CLADE / ECOSYSTEM / AGENTIC / STATE records (rsr-template-repo#209), and +`extract.sh` reads it through `scripts/deed-field.sh`. The golden path +comes from its `(anchor (golden-path))` clause. CLAUDE.md is a *view*. Content outside the `ARRIVAL-PACK:BEGIN/END` markers is hand-authorable and preserved. == Files [cols="1,3"] |=== -| `extract.sh` | a2ml reader → deterministic `claude-md-data.json` (build artifact, git-ignored) +| `extract.sh` | deed reader → deterministic `claude-md-data.json` (build artifact, git-ignored) | `arrival-pack.ncl` | Nickel projection engine → the region string (`nickel export --format raw`) | `generate.sh` | orchestrator: extract → render → splice (Hunt-tier, writes CLAUDE.md) | `verify.sh` | drift check: regenerate + byte-compare committed region (Yard-tier) @@ -41,7 +44,7 @@ outside the `ARRIVAL-PACK:BEGIN/END` markers is hand-authorable and preserved. [source,console] ---- -just claude-md # (re)generate CLAUDE.md from a2ml +just claude-md # (re)generate CLAUDE.md from the repo deed just validate-claude-md # fail if the committed region drifted / was hand-edited ---- @@ -49,7 +52,10 @@ just validate-claude-md # fail if the committed region drifted / was hand-edit * Estate-common doctrine is a *DRAFT* in the agent's words — owner to ratify into his voice, then pin to a `hyperpolymath/manifesto` commit SHA. -* `ECOSYSTEM.what-this-is-not` is empty here, so IS-NOT renders as "not yet - declared". Populating it is the boundary-erosion fix (the `lith` lesson). -* `ANCHOR.a2ml` currently encodes semantic-authority + golden-path; the +* IS-NOT comes from the deed's `(ecosystem :not (…))`. When that list is empty, + IS-NOT renders as "not yet declared" and the deed should gain the list (the + boundary-erosion fix, the `lith` lesson). +* A repo's `(anchor (implementation-policy :forbidden (…)))` renders as one + "Anchor override" line in the repo-specific part. It is empty and hidden when absent. +* The `(anchor)` clause currently encodes semantic-authority + golden-path; the recognised-drift / re-anchor-ledger aspect is a pending standards change. diff --git a/.machine_readable/arrival-pack/arrival-pack.ncl b/.machine_readable/arrival-pack/arrival-pack.ncl index 938cc8d..fa83819 100644 --- a/.machine_readable/arrival-pack/arrival-pack.ncl +++ b/.machine_readable/arrival-pack/arrival-pack.ncl @@ -3,16 +3,16 @@ # # arrival-pack.ncl — the projection ENGINE for the CLAUDE.md arrival pack. # -# Pure Nickel. Imports the deterministic JSON produced by extract.sh (the a2ml +# Pure Nickel. Imports the deterministic JSON produced by extract.sh (the deed # reader) and renders the generated CLAUDE.md region as a single string. # Emit with: nickel export --format raw --file arrival-pack.ncl # # Two halves (see ARRIVAL-PACK-DESIGN): # A. estate-common — identical everywhere; DRAFT doctrine, pinned to a # Manifesto SHA once ratified (today: DRAFT-unratified). -# B. repo-specific — projected from this repo's a2ml (data.*). +# B. repo-specific — projected from this repo's deed (data.*). # -# The a2ml files remain the single source of truth; this is a VIEW. Hand-editing +# The deed remains the single source of truth; this is a VIEW. Hand-editing # the emitted region is a flagged error (see claude-md.k9.ncl drift check). let data = import "claude-md-data.json" in @@ -20,11 +20,16 @@ let data = import "claude-md-data.json" in # owner's voice. Until then the estate-common block is explicitly DRAFT. let manifesto_pin = "DRAFT-unratified" in -let provenance = "CLADE@%{data.h_clade} ECOSYSTEM@%{data.h_eco} AGENTIC@%{data.h_agentic} STATE@%{data.h_state} ANCHOR@%{data.h_anchor}" +let anchor_override = + if std.array.length data.anchor_forbidden == 0 then "" + else "\n- **Anchor override** (ANCHOR) — This repo's anchor forbids: %{std.string.join ", " data.anchor_forbidden}. The anchor overrides the estate policy above." +in + +let provenance = "DEED@%{data.h_deed} ANCHOR@%{data.h_anchor}" in m%" - + # You are in the hyperpolymath estate — orient before acting @@ -49,43 +54,53 @@ If you are unsure what something is, **read the canon; do not guess** (guessing 16. **Cheap models for upkeep**: CI/CD, workflow, template-drift and status-file work does not get a top-tier model. If it needs real effort, stop and say so, then open a thread on cheaper models (Sonnet and Haiku). Keep top-tier models for proof work and hard design. ## The machine-readable substrate (read in this order on arrival) -**CLADE** → **ANCHOR** → **AGENTIC** → **ECOSYSTEM** → **STATE** +**CHORA** → **ANCHOR** → **AGENTIC** → **ECOSYSTEM** → **STATE** -> **NEUROSYM** -> **PLAYBOOK** + +ANCHOR, AGENTIC, ECOSYSTEM, STATE, NEUROSYM and PLAYBOOK are clauses of the CHORA deed — `(anchor)`, `(agentic)`, `(ecosystem)`, `(status)` and `(maturity)`, `(neurosym)`, `(playbook)` — not files; STATE's journal is `docs/status/ROADMAP.adoc`. There are no `.a2ml` files in this estate. The descriptive family (working name *descriptiles*) describes what-is; the **contractiles** are the normative set-point. | File | Answers | |---|---| -| `CLADE.a2ml` | *Identity / lineage* — what this repo IS (registers into `gv-clade-index`). | -| `ANCHOR.a2ml` | *Semantic authority + golden path* — what downstream may extend-not-redefine; if a recognised-drift / re-anchor marker is present, it **supersedes** accumulated context — read it first. | -| `META.a2ml` | *Concept / constitutional authority* — ADRs, what's permitted. | -| `AGENTIC.a2ml` | *May I act now?* — permissions, risk gating, fail-safe-deny. | -| `ECOSYSTEM.a2ml` | *Where it sits* — estate + external relations, and `what-this-is-not`. | -| `NEUROSYM.a2ml` | *Meaning* of operations — proof obligations. | -| `PLAYBOOK.a2ml` | *How* permitted actions run. | -| `STATE.a2ml` | *Where things are now* — progress, blockers, next-actions. | +| `%{data.deed}` | **CHORA**, the repo deed. Its header `:repo-uuid` is the repo's one id, and `(identity)` and `(lineage)` say what this repo IS; a `(clade)` clause, when present, is only an outward finding aid (`gv-clade-index` lists it for discovery); `(meta)` the architecture axes; `(agentic)` *may I act now?*; `(ecosystem)` where it sits and what it is not; `(status)` and `(maturity)` where it is now. Read fields with `scripts/deed-field.sh`. | +| `(anchor)` | *Semantic authority + golden path* — what downstream may extend-not-redefine; if a recognised-drift / re-anchor marker is present, it **supersedes** accumulated context — read it first. | +| `(neurosym)` | *Meaning* of operations — proof obligations. | +| `(playbook)` | *How* permitted actions run. | +| `docs/status/ROADMAP.adoc` | *What is next* — milestones, blockers, next actions. | | contractiles | Normative doctrine: **Intend** (north-star) · **Must** (invariants) · **Trust** (security) · **Adjust** (accessibility / inclusive design) — the integral core that holds strong; plus **Dust** (exnovation drift) · **Bust** (failure / breakage, not drift). | | 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.** ## Estate language policy (overridable per-repo via AGENTIC) -Deny: **Nix, Python, Go, TypeScript, AGPL**. (Guix, not Nix.) -JavaScript tooling order: **Bun** (default) > Deno (grandfathered) > pnpm > npm (last resort, permitted). -Use plain JavaScript when this tooling is needed. The "use ReScript" rule is retired — ReScript is no longer used in this estate. Do not migrate Bun to Deno. +Deny: **Nix, Python, Go, TypeScript, ReScript, AGPL**. (Guix, not Nix.) +JavaScript runtime: **Bun** > Deno > pnpm > npm (last resort, permitted), from one `package.json` that carries every runtime's lock (`bun.lock`, `deno.lock`, `pnpm-lock.yaml`), all written by `js-runtimes regen` (owner ruling 2026-10-14, standards LANGUAGE-POLICY §1.3–§1.4). Deno is permitted again; `package-lock.json` stays refused. +Use plain JavaScript when this tooling is needed. TypeScript and ReScript migrate to AffineScript. + +## Asking the owner +Put every question that needs the owner's decision through the selector (the AskUserQuestion tool), not as prose at the end of a report. Batch up to four per call, offer concrete options with the recommended one first, and say what each option would cause. --- -# This repo: `%{data.canonical_name}` · clade `%{data.prefixed_name}` +# This repo: `%{data.canonical_name}` -- **Identity** — uuid `%{data.uuid}`; clade `%{data.clade_primary}` (secondary `%{data.clade_secondary}`); born %{data.born}; forge `%{data.forge_gh}`. +- **Identity** — chora id `%{data.uuid}`; clade: %{data.clade_aid}; born %{data.born}; forge `%{data.forge_gh}`. - **IS** — %{data.purpose} - **IS-NOT** — %{data.isnot} - **Where it sits** — pipeline position **%{data.pipeline_pos}**; chain `%{data.chain}`; coordination = `%{data.coordination}`. -- **Constraints here** (AGENTIC) — fail-closed; evidence-per-step; no-silent-skip; rerun-after-fix; release-claim-requires-hard-pass. Never: banned langs (above), secrets, state files in repo root, AGPL. Details: `.machine_readable/bot_directives/{methodology,coverage,debt}.a2ml`. -- **Golden path** (ANCHOR) — `%{data.golden_smoke}` → %{data.golden_crit}. -- **State** — phase %{data.phase}; maturity %{data.maturity}; %{data.completion}% complete; status %{data.status}. +- **Constraints here** (AGENTIC) — fail-closed; evidence-per-step; no-silent-skip; rerun-after-fix; release-claim-requires-hard-pass. Never: banned langs (above), secrets, state files in repo root, AGPL. Details: the deed's `(bot-directives (methodology) (coverage) (debt))` clauses. +- **Golden path** (ANCHOR) — `%{data.golden_smoke}` → %{data.golden_crit}.%{anchor_override} +- **State** — phase %{data.phase}; maturity %{data.maturity}. Milestones and next actions: `docs/status/ROADMAP.adoc`. "% diff --git a/.machine_readable/arrival-pack/claude-md.k9.ncl b/.machine_readable/arrival-pack/claude-md.k9.ncl index d3fbb1c..6685d81 100644 --- a/.machine_readable/arrival-pack/claude-md.k9.ncl +++ b/.machine_readable/arrival-pack/claude-md.k9.ncl @@ -5,13 +5,14 @@ K9! # claude-md.k9.ncl — k9 contract for the CLAUDE.md arrival-pack drift check. # # Yard-tier (pure evaluation / comparison; no exec, no network, no FS write): -# the check regenerates the arrival-pack region from this repo's a2ml and asserts +# the check regenerates the arrival-pack region from this repo's deed and asserts # the committed CLAUDE.md region byte-matches it. The runnable side is verify.sh; # writing (Hunt-tier) is generate.sh. This record documents and validates the # contract shape. { 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,12 +20,12 @@ 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", version = "1.0.0", - description = "CLAUDE.md arrival pack is a faithful projection of this repo's a2ml.", + description = "CLAUDE.md arrival pack is a faithful projection of this repo's deed.", paired_runner = "verify.sh", writer = "generate.sh", author = "Jonathan D.A. Jewell ", @@ -33,12 +34,10 @@ K9! # Inputs the projection reads (single source of truth). inputs = { + # The repo deed (_chora.deed) is the only input; the golden path is + # its (anchor (golden-path)) clause (rsr-template-repo#209). descriptiles = [ - ".machine_readable/descriptiles/CLADE.a2ml", - ".machine_readable/descriptiles/ECOSYSTEM.a2ml", - ".machine_readable/descriptiles/AGENTIC.a2ml", - ".machine_readable/descriptiles/STATE.a2ml", - ".machine_readable/descriptiles/anchors/ANCHOR.a2ml", + "_chora.deed", ], estate_common = "hyperpolymath/manifesto@", # DRAFT-unratified today target = "CLAUDE.md", @@ -47,7 +46,7 @@ K9! # Checks (all must hold). checks = [ - { id = "inputs-exist", desc = "every descriptiles input file exists and parses" }, + { id = "inputs-exist", desc = "exactly one repo deed exists, parses and carries (anchor (golden-path))" }, { id = "region-present", desc = "CLAUDE.md contains exactly one BEGIN/END region" }, { id = "no-drift", desc = "committed region byte-equals freshly generated region" }, ], diff --git a/.machine_readable/arrival-pack/extract.sh b/.machine_readable/arrival-pack/extract.sh index c0adf70..7e15507 100755 --- a/.machine_readable/arrival-pack/extract.sh +++ b/.machine_readable/arrival-pack/extract.sh @@ -2,88 +2,146 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # -# extract.sh — the thin a2ml *reader* for the arrival-pack compiler. +# extract.sh — the thin *reader* for the arrival-pack compiler. # -# Projects the scalar/array fields the CLAUDE.md arrival pack needs out of this -# repo's descriptive a2ml (the "descriptiles" family: CLADE/ECOSYSTEM/AGENTIC/STATE + -# anchors/ANCHOR) into a single deterministic JSON document on stdout. +# Projects the fields the CLAUDE.md arrival pack needs out of this repo's deed +# (_chora.deed: identity, clade, lineage, status, maturity, ecosystem, +# agentic, and the golden path in its (anchor) clause) into a single +# deterministic JSON document on stdout. Every deed read goes through +# scripts/deed-field.sh; there are no .a2ml inputs (rsr-template-repo#209). # -# This is a READER only — it authors nothing. The a2ml files remain the single +# This is a READER only — it authors nothing. The deed remains the single # source of truth; arrival-pack.ncl renders this JSON into the CLAUDE.md region. # Determinism (no timestamps) is required so the k9 drift check can byte-compare. set -euo pipefail +# A failed $(…) inside a function or assignment must stop the run, not read as "". +shopt -s inherit_errexit -DIR6A2="${1:-.machine_readable/descriptiles}" -CLADE="$DIR6A2/CLADE.a2ml" -ECO="$DIR6A2/ECOSYSTEM.a2ml" -AGENTIC="$DIR6A2/AGENTIC.a2ml" -STATE="$DIR6A2/STATE.a2ml" -ANCHOR="$DIR6A2/anchors/ANCHOR.a2ml" +ROOT="${1:-.}" +READ="$ROOT/scripts/deed-field.sh" -for f in "$CLADE" "$ECO" "$AGENTIC" "$STATE" "$ANCHOR"; do - [ -f "$f" ] || { echo "extract.sh: missing required a2ml: $f" >&2; exit 2; } -done +DEED="$(bash "$READ" --find "$ROOT")" \ + || { echo "extract.sh: need exactly one *_chora.deed in $ROOT" >&2; exit 2; } +bash "$READ" --has "$DEED" anchor/golden-path \ + || { echo "extract.sh: $(basename "$DEED") has no (anchor (golden-path)) clause" >&2; exit 2; } -# scalar KEY FILE -> first `KEY = "value"` or unquoted `KEY = value` (anchored) -scalar() { - local v - v="$(grep -oP "^$1 = \"\K[^\"]+" "$2" | head -1 || true)" - if [ -z "$v" ]; then - v="$(grep -oP "^$1 = \K[^\"#]+" "$2" | head -1 | sed 's/[[:space:]]*$//' || true)" - fi - printf '%s' "$v" +# field PATH KEY -> the deed value, or "" when the clause or key is absent +# (rc 1); an unreadable deed or a usage error (rc 2) fails the run instead +field() { + local rc=0 + bash "$READ" "$DEED" "$1" "$2" || rc=$? + [ "$rc" -le 1 ] || return "$rc" } -# arr KEY FILE SEP -> join all quoted strings in the `KEY = [ ... ]` block -# (inline `k = ["a","b"]` or multiline). SEP is a literal multi-char string. -arr() { - local key="$1" file="$2" sep="${3:-, }" out="" i - local -a items - mapfile -t items < <( - awk -v k="$key" ' - index($0, k" = [")==1 {grab=1} - grab {print} - grab && /\]/ {exit} - ' "$file" | grep -oP '"[^"]*"' | sed 's/^"//; s/"$//' || true - ) - for i in "${!items[@]}"; do - if [ "$i" -eq 0 ]; then out="${items[$i]}"; else out="$out$sep${items[$i]}"; fi - done +# items PATH KEY SEP -> the deed list :KEY ( … ) joined by the literal SEP, +# "" when absent; fails like field() when the deed cannot be read +items() { + local out="" item first=1 list rc=0 + list="$(bash "$READ" --list "$DEED" "$1" "$2")" || rc=$? + [ "$rc" -le 1 ] || return "$rc" + while IFS= read -r item; do + [ -n "$item" ] || continue + if [ "$first" -eq 1 ]; then out="$item"; first=0; else out="$out$3$item"; fi + done <<< "$list" + printf '%s' "$out" +} + +# lines PATH KEY -> the deed list :KEY ( … ), one item per line, "" when absent +lines() { + local out rc=0 + out="$(bash "$READ" --list "$DEED" "$1" "$2")" || rc=$? + [[ "$rc" -le 1 ]] || return "$rc" printf '%s' "$out" } -# short content hash (drift provenance) -sh() { sha256sum "$1" | cut -c1-12; } +# hash12 -> short content hash of stdin (drift provenance) +hash12() { sha256sum | cut -c1-12; } + +# The identity is the deed's own TYPED literal (#u5"", later #u8""), +# never a computed hex uuid. The uuid is derived from that name and never +# stored (deed.abnf); printing its hex form put a version-5 literal in +# CLAUDE.md, which ADR-008's strict gate (owner ruling D320, standards +# governance job uuid-v7-conformance) rejects in every minted repo. The +# typed literal says what the identifier is, as ADR-008 asks of legacy ids, +# and follows the deed when it moves to version 8. +uuid_name="$(field '' repo-uuid)" +uuid="" +if [[ -n "$uuid_name" ]]; then + # --typed keeps the tag and quotes, and reads the literal wherever the layout puts it + rc=0 + uuid="$(bash "$READ" --typed "$DEED" '' repo-uuid)" || rc=$? + [[ "$rc" -le 1 ]] || exit "$rc" + [[ "$uuid" =~ ^#u(uid|[0-9]+)\"[^\"]*\"$ ]] || uuid="" +fi +[ -z "$uuid_name" ] || [ -n "$uuid" ] \ + || { echo "extract.sh: $(basename "$DEED") :repo-uuid is not a typed #uuid\"...\" or #u\"...\" literal" >&2; exit 2; } + +# read_clauses -> the clauses this pack reads, one token per line. Hashing only +# these keeps an unrelated deed edit (say a canon-pin bump) from reading as drift. +read_clauses() { + local c + bash "$READ" "$DEED" '' canonical-name + printf '%s\n' "$uuid_name" + for c in identity clade lineage forges status maturity ecosystem agentic; do + bash "$READ" --clause "$DEED" "$c" || [ "$?" -eq 1 ] || return 2 + done +} -# --- IS-NOT: present only if ECOSYSTEM declares `what-this-is-not` ------------ -isnot="$(arr 'what-this-is-not' "$ECO" ' · ')" -[ -n "$isnot" ] || isnot="(not yet declared — add ECOSYSTEM.what-this-is-not)" +isnot="$(items ecosystem not ' · ')" +[ -n "$isnot" ] || isnot="(not yet declared — add (ecosystem :not (…)) to the deed)" -golden_smoke="$(arr 'smoke-test-command' "$ANCHOR" ' && ')" -golden_crit="$(arr 'success-criteria' "$ANCHOR" '; ')" +# Each value is its own assignment so that a failed deed read stops the run; +# as a bare `--arg x "$(…)"` its exit status would be discarded. +canonical_name="$(field '' canonical-name)" +[ -n "$canonical_name" ] \ + || { echo "extract.sh: $(basename "$DEED") has no :canonical-name" >&2; exit 2; } +# The clade is an optional outward finding aid (standards A1 §11), never identity. +clade_primary="$(field clade primary)" +clade_secondary="$(items clade secondary ', ')" +if [[ -z "$clade_primary" ]]; then + clade_aid="none declared (optional finding aid)" +elif [[ -n "$clade_secondary" ]]; then + clade_aid="$clade_primary (secondary $clade_secondary; finding aid)" +else + clade_aid="$clade_primary (finding aid)" +fi +born="$(field lineage born)" +forge_gh="$(field forges github)" +purpose="$(field ecosystem purpose)" +pipeline_pos="$(field ecosystem pipeline-position)" +chain="$(field ecosystem chain)" +coordination="$(field ecosystem coordination)" +phase="$(field status phase)" +maturity="$(field maturity level)" +golden_smoke="$(items anchor/golden-path smoke-test-command ' && ')" +golden_crit="$(items anchor/golden-path success-criteria '; ')" +# The anchor may override the estate language policy for this repo only. +anchor_forbidden="$(lines anchor/implementation-policy forbidden)" +anchor_allowed="$(lines anchor/implementation-policy allowed)" +anchor_clause="$(bash "$READ" --clause "$DEED" anchor)" +h_deed="$(read_clauses | hash12)" +h_anchor="$(printf '%s\n' "$anchor_clause" | hash12)" -jq -n \ - --arg canonical_name "$(scalar 'canonical-name' "$CLADE")" \ - --arg prefixed_name "$(scalar 'prefixed-name' "$CLADE")" \ - --arg uuid "$(scalar 'uuid' "$CLADE")" \ - --arg clade_primary "$(scalar 'primary' "$CLADE")" \ - --arg clade_secondary "$(arr 'secondary' "$CLADE" ', ')" \ - --arg born "$(scalar 'born' "$CLADE")" \ - --arg forge_gh "$(scalar 'github' "$CLADE")" \ - --arg purpose "$(scalar 'purpose' "$ECO")" \ +jq -cnS \ + --arg canonical_name "$canonical_name" \ + --arg clade_aid "$clade_aid" \ + --arg uuid "$uuid" \ + --arg born "$born" \ + --arg forge_gh "$forge_gh" \ + --arg purpose "$purpose" \ --arg isnot "$isnot" \ - --arg pipeline_pos "$(scalar 'position' "$ECO")" \ - --arg chain "$(scalar 'chain' "$ECO")" \ - --arg coordination "$(scalar 'coordination' "$ECO")" \ - --arg phase "$(scalar 'phase' "$STATE")" \ - --arg maturity "$(scalar 'maturity' "$STATE")" \ - --arg completion "$(scalar 'completion-percentage' "$STATE")" \ - --arg status "$(scalar 'status' "$STATE")" \ + --arg pipeline_pos "$pipeline_pos" \ + --arg chain "$chain" \ + --arg coordination "$coordination" \ + --arg phase "$phase" \ + --arg maturity "$maturity" \ + --arg deed "$(basename "$DEED")" \ --arg golden_smoke "$golden_smoke" \ --arg golden_crit "$golden_crit" \ - --arg h_clade "$(sh "$CLADE")" \ - --arg h_eco "$(sh "$ECO")" \ - --arg h_agentic "$(sh "$AGENTIC")" \ - --arg h_state "$(sh "$STATE")" \ - --arg h_anchor "$(sh "$ANCHOR")" \ - '$ARGS.named' + --arg h_deed "$h_deed" \ + --arg h_anchor "$h_anchor" \ + --arg anchor_forbidden "$anchor_forbidden" \ + --arg anchor_allowed "$anchor_allowed" \ + '$ARGS.named + | .anchor_forbidden |= (split("\n") | map(select(. != ""))) + | .anchor_allowed |= (split("\n") | map(select(. != "")))' diff --git a/.machine_readable/arrival-pack/generate.sh b/.machine_readable/arrival-pack/generate.sh index ac2a859..da27bed 100755 --- a/.machine_readable/arrival-pack/generate.sh +++ b/.machine_readable/arrival-pack/generate.sh @@ -3,7 +3,7 @@ # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # generate.sh — compile the repo's CLAUDE.md arrival pack. -# extract.sh (a2ml -> JSON) -> arrival-pack.ncl (Nickel projection) -> splice. +# extract.sh (deed -> JSON) -> arrival-pack.ncl (Nickel projection) -> splice. # # The generated region lives between the ARRIVAL-PACK:BEGIN/END markers. Anything # OUTSIDE the markers in CLAUDE.md is hand-authorable and is preserved verbatim. @@ -13,7 +13,7 @@ ROOT="$(git rev-parse --show-toplevel)" AP="$ROOT/.machine_readable/arrival-pack" TARGET="$ROOT/CLAUDE.md" -bash "$AP/extract.sh" "$ROOT/.machine_readable/descriptiles" > "$AP/claude-md-data.json" +bash "$AP/extract.sh" "$ROOT" > "$AP/claude-md-data.json" nickel export --format raw "$AP/arrival-pack.ncl" > "$AP/.region.tmp" if [ -f "$TARGET" ] && grep -q 'ARRIVAL-PACK:BEGIN' "$TARGET"; then @@ -35,7 +35,7 @@ else echo "-->" echo "" echo "" + echo " repo's deed by \`just claude-md\` — do not hand-edit it. -->" echo cat "$AP/.region.tmp" } > "$TARGET" diff --git a/.machine_readable/arrival-pack/verify.sh b/.machine_readable/arrival-pack/verify.sh index 1d90acf..c26fb58 100755 --- a/.machine_readable/arrival-pack/verify.sh +++ b/.machine_readable/arrival-pack/verify.sh @@ -3,7 +3,7 @@ # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # verify.sh — drift check for the CLAUDE.md arrival pack (the runnable side of -# claude-md.k9.ncl). Regenerates the region from a2ml and byte-compares it to the +# claude-md.k9.ncl). Regenerates the region from the deed and byte-compares it to the # committed region. Non-zero exit on drift or hand-edit. Wire into CI/pre-commit. set -euo pipefail @@ -13,12 +13,12 @@ TARGET="$ROOT/CLAUDE.md" [ -f "$TARGET" ] || { echo "DRIFT: CLAUDE.md missing — run \`just claude-md\`"; exit 1; } -bash "$AP/extract.sh" "$ROOT/.machine_readable/descriptiles" > "$AP/claude-md-data.json" +bash "$AP/extract.sh" "$ROOT" > "$AP/claude-md-data.json" fresh="$(nickel export --format raw "$AP/arrival-pack.ncl")" committed="$(awk '/` comment per pull request. It is triggered by + `pull_request` *and* by `workflow_run` on the two producer workflows, so it + re-renders when a scanner finishes instead of showing "not yet" forever. +* **Hypatia ran twice per pull request.** `hypatia-scan.yml` and a + `hypatia-scan` job of `static-analysis-gate.yml` each built the same scanner + at the same pin, with separate caches and two independent "fail on critical" + gates over two differently-shaped JSON files. Hypatia now runs once, in + `hypatia-scan.yml`, which also uploads `hypatia-findings.relativized.json` + (repo-relative paths, so the grid's delta column can match + `git diff --name-only`) as `hypatia-scan-findings`. `static-analysis-gate.yml` + keeps what is specific to it and uploads `static-analysis-findings`. +* **A failed fleet job was reported and then ignored.** The verdict job printed + the fleet result in a table and never failed on it, so a fleet failure was + invisible where a reader looked; its `jq '… | .findings | length'` loop could + also abort the whole summary on one malformed envelope and skip the final + check. The verdict now fails on DEED/K9 only (each producer owns its own + gate), states that the fleet's failure is orange rows plus its own red check, + and never parses fields it does not own. +* **`.hypatia-ignore` carried a dangling cross-reference**: `panicbot` was left + unenrolled because "the Hypatia lane's direct panic-attack row" would cover + it, and no such row existed. The grid now renders that row from + `static-analysis-gate.yml` evidence, and the enrolment reason states the rule + it is an instance of (one scanner, one row). + +* **Seven defects the review of this work found, each now covered by a control.** + None of these were reached by the tests as written, which is the point: +** `check-container-pins.sh` took the "nothing to pin" success path even after + failing on a `Dockerfile`, so the one repository shape the Dockerfile rule + exists to catch reported PASS; and it registered a stage name only after the + line that `continue`s, so the ordinary `FROM image@sha256:… AS build` + + `FROM build` pair was judged an unpinned remote image. Stage names are now + registered when parsed, before any branch can skip the rest of the line; +** `pin-container-digests.sh` rewrote every `FROM` line, turning `FROM scratch` + and `FROM build` into `scratch@sha256:…` and `build@sha256:…` — corrupting the + multi-stage files it should leave alone, while the companion gate accepted the + result. It now splits FROM into flags/reference/tail and rewrites only the + reference of a remote image; +** `check-spdx-headers.sh` judged emptiness per EXTENSION, so a valid Elixir tree + with `.ex` and no `.exs` (or OCaml `.ml` without `.mli`) failed for a language + it is written in. Emptiness is now judged per language, across all of its + extensions; +** `render-mise.sh --langs` only changed the label: the tool list still came from + the tree's own detection, so a fixture asking for Julia was handed the docs + toolchain. `mise-tools` now accepts the language list, and `--check` compares + the declared toolchain rather than the rendered prose — which made it + impossible to pass on any repository that wrote its own header, the template + included; +** `check-mise-language.sh` parsed only bare `[tools]` keys, and backend-qualified + tools MUST be quoted (`"npm:prettier"`), so the banned-tool check could never + see them; its `--detect` mirror also looked only at the root, calling a tree + with `src/interface/abi.ipkg` and `src/interface/ffi/build.zig` a docs + repository. Both now follow the engine, depths included, and the mirror lists + mise tools rather than each language's binaries; +** `check-mint-tokens.sh` treated "no build/just/repo-init.just" as proof of a + minted tree — but that recipe is carried verbatim into every child, so no child + was ever recognised and an unfilled `occupancy-types` passed (and + `survey-template-damage.sh` reported such repositories token-clean). The marker + is `archetypes/`, which the mint deletes; +** `archetypes/*/prune.txt` dropped only `build/container/`, so a minted Julia + package and a minted Rust CLI both carried the Idris2/Zig seam: three languages + detected, `idris2` and `zig` installed, `idris2 --build` in `just build`. Both + presets declare neither the abi nor the ffi capability, so both now prune + `src/interface/`; `validate-rsr` requires the seam only of a tree that carries + it. The archetype tests assert the toolchain is single-language. + +==== Added + +* `scripts/governance-badge.js` — the single implementation of the badge + scheme: the state -> (message, colour) table, canonical rendering, the + publication decision (create orphan branch / add file / update / no-op), + API-error classification, and guards that refuse `main`, `master`, a + repository default branch, or a path outside `badges/`. Runtime-agnostic + (Bun or Node, `node:` builtins only), so it is unit testable. +* `tests/workflows/governance_badge_test.sh` and + `tests/workflows/governance_badge_test.js` — hermetic tests wired into + Estate Rules: the state table, byte-equal no-op, changed-bytes publication, + orphan-branch creation with `parents: []`, grey states for missing/malformed + lane evidence, malformed existing content, 404-vs-outage classification, + conflict reconciliation and the source-branch guards, plus wiring checks run + against a mutated workflow so they are proven able to fail. They supersede + `tests/workflows/dogfood_badge_test.sh`, which asserted the old mapping and + was never wired into CI. A present but *unreadable* artifact (EACCES, or a + TOCTOU deletion between the stat and the read) is now evidence that cannot be + checked rather than an uncaught exception: it renders grey, names the errno, + and exits 3. The assertion is skipped, with the reason printed, when the + suite runs as root, where `chmod 000` cannot deny a read. +* `README.adoc` and `.github/workflows/README.adoc` now state the operational + contract of the generated `governance-badges` branch: it is an orphan with no + common ancestor with `main`, it is never merged and has no pull request, + recent pushes are normal refreshes, identical content makes no commit, and a + grey badge means the lane produced no trustworthy result. + * `www/` site-operations bundle (issue #53): canonical `.well-known/` metadata, publishable content, policy sources, error bodies, security-header templates, Caddy/nginx/Apache integration examples, an @@ -42,9 +453,139 @@ https://semver.org/spec/v2.0.0.html[Semantic Versioning]. template; they exist for running `repo-init` over a tree that predates #53. #106 described both as already present — neither was, which is a fair part of why the estate still carries root `.well-known/` directories. +* `scripts/merge-actions-lock.rs` — merges an archetype's lock fragment + (`archetypes//actions.lock`) into the spine's + `.github/workflows/actions.lock` at mint. A record that differs between + the two stops the mint. `tests/workflows/archetype_lock_merge_test.sh` + (wired into estate-rules) checks the merged lock with + `check-lock-sync.sh`, and checks that the old clobber, a missing record + and a conflicting record all fail. + +* `scripts/verdict-grid-lane.sh` — collect / validate / render / previous / + comment for the verdict grid lane. It refuses to execute a renderer whose + sha256 does not match the recorded digest, renders an explicit **orange + "indeterminate"** grid (never green) when the renderer or the evidence is + unavailable, refuses a bare panic/pons findings array (a bare array cannot + distinguish "ran clean" from "never ran"), and updates the comment it posted + last time instead of adding one per run. +* `scripts/check-workflow-edges.sh` — the intra-repository half of the CICD + Squabbler gate, run in both `squabble.yml` and `estate-rules.yml`. It fails + when a `workflow_run.workflows` entry names no workflow's `name:`, when two + workflows share a `name:`, when the verdict job reads an artifact its named + producer never uploads, when a producer stops running on `pull_request`, when + the verdict job's `--hypatia-ref`/`--panic-ref`/`--pons-ref` disagree with the + producer's pin, or when one tool is pinned at two different commits. +* `scripts/check-hypatia-ignore.sh` — the meta-rules for `.hypatia-ignore`: + every fragment must end on a path boundary (a substring fragment silences + more than it names), no entry may be subsumed by a broader one for the same + module, the path it names must exist, and it must belong to a commented + reason block. +* `docs/HYPATIA-RULE-ORDER.adoc` — the ordered statement of Hypatia's exemption + layers (total ban, universal excludes, built-in defaults, `.hypatia-ignore`, + inline directives, baseline, grid normalisation), the eight clashes found + between them, the rule that resolves each, the e/r (evidence/reason) + contract per reporter, the GitBot design rules, what is deliberately *not* + enforced, and the refusals that make the order a checked contract. +* `tests/workflows/verdict_grid_test.sh` (70 checks), `workflow_edges_test.sh` + (21) and `hypatia_ignore_test.sh` (22) — behaviour, wiring and firing controls + for the three scripts above, with stub `gh`/renderer and mutated workflow + copies, so every check is proven able to fail. +* `.machine_readable/policies/hypatia-rule-order.json` — the rule order as a + contract rather than prose: the seven layers with their rank, scope, owner and + what each may override; the eight concerns with exactly one owning workflow or + file each; the eight clashes with the rule that resolves each; the modules + `.hypatia-ignore` currently exempts and the layer each exemption lives in; and + the scanner, grid, renderer and jq-library pins the order was written against. +* `scripts/check-hypatia-rule-order.sh` — refuses a duplicate or gapped rank, a + layer that states no scope or no override, an owner outside the four declared + parties, a concern owned by a path that does not exist, a clash with no + resolution, a module exempted but unclassified (or classified but no longer + exempted), an exemption filed under a layer this repository may not change, + and any pin or digest that disagrees with `hypatia-scan.yml` or the lane + script. Exit 0 clean, 1 violation, 2 setup error. +* `tests/workflows/hypatia_rule_order_test.sh` (44 checks) — one mutation per + refusal above, run against a copy of the real policy. Two of its own first-draft + defects are documented in `docs/HYPATIA-RULE-ORDER.adoc`: `@tsv` collapses an + empty field (a missing `overrides` was reported as a missing `scope`), and + `grep` under `set -e` exits before a missing pin can be diagnosed. +* `.machine_readable/policies/README.adoc` — the registry now says what each + policy states, what checks it, and that adding a policy means adding the + checker and its lane. +* `scripts/run-proven-suite.sh` gains the lane *Hypatia rule order & clash + resolution*, so the policy cannot drift from the four files it describes + without a red run; `tests/run_proven_suite_test.sh` asserts the lane is + dispatched (16). ==== Changed +* KYAML, step 5 of standards YAML-POLICY §5 (standards#1024): the three + estate-owned YAML files that no third-party bot rewrites are now KYAML + content: `build/container/compose.yaml`, `compose.example.yaml` and + `stapeln/.gatekeeper.yaml`. Each keeps its name, because Compose, the + Containerfile label and svalinn find it by name (§3.4). They were rewritten + by standards' `kyaml-format.sh` alone, which refuses any change to the + parsed data. Every comment line survives, and `.gatekeeper.yaml` is now + byte-identical to standards' `_shared/container/.gatekeeper.yaml`. + Estate Rules gains the ratchet "Converted YAML stays KYAML", which runs + that script at standards `75bd599` with yq 4.53.3, both sha256-verified. + Container narrowing at mint can delete any of the three files, so the + template must keep all of them, while a minted repository checks the ones + it kept and names the ones narrowing removed. + `tests/workflows/kyaml_ratchet_test.sh` covers the template and each + narrowing, and `tests/workflows/container_kyaml_test.sh` checks that the + conversion kept every value in the template's configs. Estate Rules runs + both. + Workflows wait for step 6 (standards#1025). Files that Dependabot, + FUNDING, issue forms, Probot settings, CodeRabbit, Copilot, GitLab CI or + pre-commit read or rewrite are not converted, as in standards. +* The workflow linter installs Bun 1.4.2, the estate pin, from its release + archive and checks its sha256, as Estate Rules installs its tools. It ran + `oven-sh/setup-bun@v2.2.0` with `bun-version: latest`, so the runtime was + unpinned, and SonarCloud rated a third-party action pinned by tag "C" + ("Use full commit SHA hash"), even though `actions.lock` pins its commit. + The linter's lock edge to setup-bun goes. The `setup-bun@v2.2.0` record + stays, because the commented Bun steps in `e2e.yml.template` resolve + through it. +* `.github/workflows/runtime-policy.yml` is removed, folded into governance + as the standards cicd-regularisation spec §14 rules. Governance's + "Language / package anti-pattern policy" fails any tracked + `package-lock.json`, a stricter test than this file's mixed-toolchain error, + and its "JS runtimes" jobs check `bun.lock`, `deno.lock` and + `pnpm-lock.yaml` against `package.json`. Its `actions.lock` record goes with + it, and `validate-template.sh` and `validate_workflows_test.sh` require + `governance.yml` in its place. Repositories minted from the template stop + receiving their own copy of the check. Governance now runs on pull requests + to any branch, as the removed workflow did, so a stacked pull request keeps + the check. `tests/workflows/runtime_policy_fold_test.sh`, which Estate Rules + runs, checks that both validators reject a tree without `governance.yml` and + accept none of the retired gates' names in its place. + `tests/workflows/governance_policy_test.sh`, also in Estate Rules under a + sha256-verified Bun 1.4.2, checks that the workflow and its lock record stay + gone, that governance's pull request trigger has no filter, and that its + standards call matches the lock. +* JavaScript runtimes follow the 2026-10-14 ruling (standards + LANGUAGE-POLICY §1.3–§1.4): Bun > Deno > pnpm > npm, from one + `package.json` whose `bun.lock`, `deno.lock` and `pnpm-lock.yaml` are all + written by `js-runtimes regen`. +** `.github/workflows/runtime-policy.yml` used to fail any repository + carrying more than one lock, so it failed every repository that follows the + ruling ("Mixed toolchains: 3 package managers"). Those three locks now count + as one dependency graph. `package-lock.json` next to them still fails. A + `deno.json` is a notice, not a warning. +** The provisioning engine (`build/just/provision-lib.sh`, + `provision-modes.sh`, `provision-check.sh`) is re-synced to standards + `ed04132e` (engine 0.6.0, standards#1255). A `package.json` provisions bun, + deno and pnpm, and deno and pnpm are no longer banned tools. +** The same policy appears in the arrival pack (`arrival-pack.ncl`, and + CLAUDE.md regenerated from it), `mise.toml`, `docs/SETUP.adoc` PV-W23, + PV-W30 and PV-W34, the dev warm-up, `.cursorrules`, `.clinerules`, + `.windsurfrules`, `.github/copilot-instructions.md` and the PR template. +** The six standards reusables move from `ae36881` to `934b879`. At + `ae36881` governance's Actions lockfile verify installs gh-actions-lock + v0.1.6, which cannot read this lock's format v0.0.3 (exit 2, red on main). + `actions.lock` is re-keyed and closed at the new pin (setup-bun and + setup-deno records added), as resource-algebra#8 did. + * Canonical `.well-known/` location moved from the repository root to `www/.well-known/`. Well-known enforcement, Groove checks, the root-shape allowlist, the SSG bootstrap and the documentation now prefer the `www/` @@ -65,6 +606,69 @@ https://semver.org/spec/v2.0.0.html[Semantic Versioning]. ==== Fixed +* Governance, Hypatia Security Scan, Mirror to Git Forges, Rust CI and + Secret Scanner start again. GitHub's runner now rejects a lock record whose + `ref:` is the record's own SHA ("invalid lockfile pin"), and nine of this + lock's action records had one. The standards reusables move from `6a92510` + to `75bd599`, whose own lock carries tag refs + (hyperpolymath/standards#1271, hyperpolymath/standards#1285), and + `relock-standards-pin.sh` at `75bd599` gave the nine records the same tags. + No commit changes: each tag was checked to point at its recorded commit. +* The mint's self-name pass keeps issue references. It rewrote + `occupancy-types#N`, a reference to this template's own issue, into + `#N`, which names an issue of the child that does not exist or is + unrelated. resource-algebra carried such references after its mint. + `tests/workflows/self_name_pass_test.sh` (wired into Estate Rules) runs + the recipe's own rewrite program on fixtures. +* CLAUDE.md prints the deed's typed identity (`#u5""`) instead of + the derived uuid5 in hex, and the deed's identity comment no longer + spells it out. ADR-008's strict v8 gate (governance "UUID v7 + conformance") rejected both, in the template and in every minted repo. +* The standards reusables are pinned to `6a92510`, whose governance job + installs gh-actions-lock v0.2.0. At `ae36881` it installed v0.1.6, which + refuses lock format v0.0.3 ("newer than this binary supports"), so + Actions lockfile verify failed. `actions.lock` was relocked with + standards' `propagate-workflow-pins.sh` (hyperpolymath/standards#1254). + `6a92510` also scans the julia-library overlay's `Project.toml.in` as a + `Project.toml` (hyperpolymath/standards#1258), so its `[extras]` registry + UUIDs no longer fail the strict v8 gate. +* The non-interactive mint completes. `RSR_NON_INTERACTIVE=1` used to + stub `read` for the whole recipe, so an unanswered prompt killed the + mint under `set -u` and the substitution loops never ended. Prompts now + read answers from the environment; `tests/workflows/non_interactive_mint_test.sh` + drives that path in Estate Rules, and the docs that warned agents off it + now describe it. +* A mint leaves just's own interpolation of `project` alone, regenerates + CLAUDE.md and the repository map from the child's deed and tree, keeps + the deed's `reject-if-contains` detector, and no longer breaks the + OpenSSF audit table when no OpenSSF id is given. +* The standards reusables are pinned to standards main (`ae36881`), where + the Actions lockfile verify job pins its tool and treats `sha-as-ref` + as advisory. +* The julia-library lock fragment is at the spine's lockfile format + v0.0.3, so Julia mints merge it again. + +* Julia mints start with Lock Sync green. The julia-library overlay + shipped its own `actions.lock`, which replaced the spine's at mint, so + every spine workflow lost its records. The records now ship beside the + overlay and are merged in. The overlay workflows gain SPDX headers and + top-level permissions, and julia-docs builds with plain steps instead of + the allowlist-refused julia-buildpkg/julia-docdeploy. julia-actions/cache + moves to v3.5.0. The root allowlist admits `?Project.toml` and `?test/`. + The docs checkout sets `persist-credentials: false`, so package code run + while the docs environment is set up finds no stored token. deploydocs + still receives GITHUB_TOKEN. +* `just repo-init julia-library` accepts a `.jl` slug. The PROJECT + tokens map its dot to an underscore, and the default service name is the + slug in lower case. + +* Dev container starts for everyone: dropped the ghcr.io dev container + features (anonymous pulls returned 403), installing just from Wolfi and a + SHA-256-checked Nickel 1.17.0 binary instead. The default + `.devcontainer/devcontainer.json` now targets rootless Podman + (`--userns=keep-id:uid=65532,gid=65532`, `label=disable`) so the workspace + is writable as wolfi-base's built-in `nonroot`; Docker, Codespaces and + Gitpod use `.devcontainer/docker/devcontainer.json`. * `dot-wellknown-enforcement.yml` now drives the migrator that #106 described but never wired up: previously the workflow only printed advice to run it, so nothing verified that a repository would survive the migration. It now diff --git a/CITATION.cff b/CITATION.cff index e69aece..471c54c 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,11 +4,11 @@ authors: - family-names: "Jewell" given-names: "Jonathan D.A." orcid: "https://orcid.org/0000-0002-3078-6652" -title: "{{PROJECT_NAME}}" +title: "occupancy-types" version: 0.1.0 -date-released: "{{CURRENT_DATE}}" -url: "https://{{FORGE}}/{{OWNER}}/{{REPO}}" -repository-code: "https://{{FORGE}}/{{OWNER}}/{{REPO}}" +date-released: "2026-11-12" +url: "https://github.com/hyperpolymath/occupancy-types" +repository-code: "https://github.com/hyperpolymath/occupancy-types" license: MPL-2.0 keywords: - "rsr" diff --git a/CLAUDE.md b/CLAUDE.md index 01040b2..b2bac29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,9 +4,9 @@ SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell --> + repo's deed by `just claude-md` — do not hand-edit it. --> - + # You are in the hyperpolymath estate — orient before acting @@ -31,42 +31,146 @@ If you are unsure what something is, **read the canon; do not guess** (guessing 16. **Cheap models for upkeep**: CI/CD, workflow, template-drift and status-file work does not get a top-tier model. If it needs real effort, stop and say so, then open a thread on cheaper models (Sonnet and Haiku). Keep top-tier models for proof work and hard design. ## The machine-readable substrate (read in this order on arrival) -**CLADE** → **ANCHOR** → **AGENTIC** → **ECOSYSTEM** → **STATE** +**CHORA** → **ANCHOR** → **AGENTIC** → **ECOSYSTEM** → **STATE** -> **NEUROSYM** -> **PLAYBOOK** + +ANCHOR, AGENTIC, ECOSYSTEM, STATE, NEUROSYM and PLAYBOOK are clauses of the CHORA deed — `(anchor)`, `(agentic)`, `(ecosystem)`, `(status)` and `(maturity)`, `(neurosym)`, `(playbook)` — not files; STATE's journal is `docs/status/ROADMAP.adoc`. There are no `.a2ml` files in this estate. The descriptive family (working name *descriptiles*) describes what-is; the **contractiles** are the normative set-point. | File | Answers | |---|---| -| `CLADE.a2ml` | *Identity / lineage* — what this repo IS (registers into `gv-clade-index`). | -| `ANCHOR.a2ml` | *Semantic authority + golden path* — what downstream may extend-not-redefine; if a recognised-drift / re-anchor marker is present, it **supersedes** accumulated context — read it first. | -| `META.a2ml` | *Concept / constitutional authority* — ADRs, what's permitted. | -| `AGENTIC.a2ml` | *May I act now?* — permissions, risk gating, fail-safe-deny. | -| `ECOSYSTEM.a2ml` | *Where it sits* — estate + external relations, and `what-this-is-not`. | -| `NEUROSYM.a2ml` | *Meaning* of operations — proof obligations. | -| `PLAYBOOK.a2ml` | *How* permitted actions run. | -| `STATE.a2ml` | *Where things are now* — progress, blockers, next-actions. | +| `occupancy-types_chora.deed` | **CHORA**, the repo deed. Its header `:repo-uuid` is the repo's one id, and `(identity)` and `(lineage)` say what this repo IS; a `(clade)` clause, when present, is only an outward finding aid (`gv-clade-index` lists it for discovery); `(meta)` the architecture axes; `(agentic)` *may I act now?*; `(ecosystem)` where it sits and what it is not; `(status)` and `(maturity)` where it is now. Read fields with `scripts/deed-field.sh`. | +| `(anchor)` | *Semantic authority + golden path* — what downstream may extend-not-redefine; if a recognised-drift / re-anchor marker is present, it **supersedes** accumulated context — read it first. | +| `(neurosym)` | *Meaning* of operations — proof obligations. | +| `(playbook)` | *How* permitted actions run. | +| `docs/status/ROADMAP.adoc` | *What is next* — milestones, blockers, next actions. | | contractiles | Normative doctrine: **Intend** (north-star) · **Must** (invariants) · **Trust** (security) · **Adjust** (accessibility / inclusive design) — the integral core that holds strong; plus **Dust** (exnovation drift) · **Bust** (failure / breakage, not drift). | | 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.** ## Estate language policy (overridable per-repo via AGENTIC) -Deny: **Nix, Python, Go, TypeScript, AGPL**. (Guix, not Nix.) -JavaScript tooling order: **Bun** (default) > Deno (grandfathered) > pnpm > npm (last resort, permitted). -Use plain JavaScript when this tooling is needed. The "use ReScript" rule is retired — ReScript is no longer used in this estate. Do not migrate Bun to Deno. +Deny: **Nix, Python, Go, TypeScript, ReScript, AGPL**. (Guix, not Nix.) +JavaScript runtime: **Bun** > Deno > pnpm > npm (last resort, permitted), from one `package.json` that carries every runtime's lock (`bun.lock`, `deno.lock`, `pnpm-lock.yaml`), all written by `js-runtimes regen` (owner ruling 2026-10-14, standards LANGUAGE-POLICY §1.3–§1.4). Deno is permitted again; `package-lock.json` stays refused. +Use plain JavaScript when this tooling is needed. TypeScript and ReScript migrate to AffineScript. + +## Asking the owner +Put every question that needs the owner's decision through the selector (the AskUserQuestion tool), not as prose at the end of a report. Batch up to four per call, offer concrete options with the recommended one first, and say what each option would cause. --- -# This repo: `occupancy-types` · clade `rm-occupancy-types` +# This repo: `occupancy-types` -- **Identity** — uuid `311d18e0-650b-885e-b437-59bbe1f6ad3b`; clade `rm` (secondary ``); born 2026-03-16; forge `hyperpolymath/occupancy-types`. -- **IS** — Canonical RSR-compliant repository template: scaffolding (CI/CD, AI manifests, ABI/FFI standards, container ecosystem, governance) that new hyperpolymath projects are instantiated from. -- **IS-NOT** — a project in its own right · Scaffoldia (the full-featured repo designer) · standards (the canon source this template operationalises) -- **Where it sits** — pipeline position **foundation**; chain `standards → rsr-template-repo → (every estate repo)`; coordination = `standards`. -- **Constraints here** (AGENTIC) — fail-closed; evidence-per-step; no-silent-skip; rerun-after-fix; release-claim-requires-hard-pass. Never: banned langs (above), secrets, state files in repo root, AGPL. Details: `.machine_readable/bot_directives/{methodology,coverage,debt}.a2ml`. +- **Identity** — chora id `#u5"github.com/hyperpolymath/occupancy-types"`; clade: none declared (optional finding aid); born 2026-11-12; forge `hyperpolymath/occupancy-types`. +- **IS** — UNASSIGNED +- **IS-NOT** — (not yet declared — add (ecosystem :not (…)) to the deed) +- **Where it sits** — pipeline position **UNASSIGNED**; chain `UNASSIGNED`; coordination = `standards`. +- **Constraints here** (AGENTIC) — fail-closed; evidence-per-step; no-silent-skip; rerun-after-fix; release-claim-requires-hard-pass. Never: banned langs (above), secrets, state files in repo root, AGPL. Details: the deed's `(bot-directives (methodology) (coverage) (debt))` clauses. - **Golden path** (ANCHOR) — `just test && just quality` → Core tests pass; Quality gates pass; No unresolved critical security findings. -- **State** — phase maintenance; maturity production; 97% complete; status active. +- **Anchor override** (ANCHOR) — This repo's anchor forbids: Node.js, npm. The anchor overrides the estate policy above. +- **State** — phase incubating; maturity experimental. Milestones and next actions: `docs/status/ROADMAP.adoc`. + + + + +## Estate-Wide Format Migration Directives + +**IMPORTANT FOR ALL AGENTS (Claude, Codex, Gemini, Mistral, etc.):** + +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. +- 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. + KYAML is a subset of YAML; every YAML parser accepts it. + +**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` (`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`, `.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 +hand; that migration is done per repo (ADR-008 phase P3). In deed files use +`#u5` literals until the deed ABNF admits v8; a `#u7` literal is illegal. + +**.a2ml → deed:** A2ML is retired; never create or restore a `.a2ml` file. +- Grammars: `standards/1-formats/deed/spec/abnf/deed.abnf` (authoritative + over the deed README, D308) and `standards/1-formats/k9/` for k9. +- Descriptiles (anchor, meta, state, ecosystem, agentic, neurosym, playbook) + are clauses of `occupancy-types_chora.deed` (D312). +- Contractiles (intend, must, trust, adjust, bust, dust) are k9 contracts in + `.machine_readable/contractiles//`. The deed attests to them + (`:contractiles_required`, and a `(contractiles …)` clause per D312). +- Do not hand-convert A2ML in bulk: conversion is kyX's first front end (D313). + +**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** with **Pearlite** contracts, and **Verus**, by default and on the code (§1.2). 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) | + +Always name Creusot, Pearlite and Verus in Rust verification plans and in +`docs/status/PROOF-STATUS.adoc`. Pearlite is Creusot's specification +language, not a third verifier. Plan scopes for both verifiers, and record +any that is unsupported, inapplicable or not yet run, with its reason; never +present an unexecuted check as a pass. Using both does not make two +independent witnesses (policy §1.2). + +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. - +Proof grades are policy §8, *Proof Witness Grades*: `PWG-3` < `PWG-2i` < +`PWG-2ii` < `PWG-1`, where `PWG-1` is strongest. A grade is derived from +receipts, never asserted. A "verified" or "machine-checked" claim needs +current receipts. Report a census with its denominator, never a +percentage. Always write the `PWG-` prefix: CRG, TRG, ARG and FRG letters, +and ECHIDNA's `TrustLevel`, are different schemes. diff --git a/CODE_OF_CONDUCT.adoc b/CODE_OF_CONDUCT.adoc new file mode 100644 index 0000000..4af75b5 --- /dev/null +++ b/CODE_OF_CONDUCT.adoc @@ -0,0 +1,32 @@ +# Hyperpolymathy Estates Code of Conduct + +Like the technical community as a whole, the Hyperpolymathy Estates team and community is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people. + +Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance. + +This isn’t an exhaustive list of things that you can’t do. Rather, take it in the spirit in which it’s intended - a guide to make it easier to enrich all of us and the technical communities in which we participate. + +This code of conduct applies to all spaces managed by the Hyperpolymathy Estates project or The Metadatastician. This includes IRC, the mailing lists, the issue tracker, DSF events, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them. + +If you believe someone is violating the code of conduct, we ask that you report it by emailing [j.d.a.jewell@open.ac.uk](mailto:j.d.a.jewell@open.ac.uk). For more details please see our [Reporting guidelines](https://github.com/metadatastician/standards/conduct_guidelines) + +- **Be friendly and patient.** +- **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. +- **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. +- **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Hyperpolymathy Estates community should be respectful when dealing with other members as well as with people outside the Hyperpolymathy Estates community. +- **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: + - Violent threats or language directed against another person. + - Discriminatory jokes and language. + - Posting sexually explicit or violent material. + - Posting (or threatening to post) other people's personally identifying information ("doxing"). + - Personal insults, especially those using racist or sexist terms. + - Unwelcome sexual attention. + - Advocating for, or encouraging, any of the above behavior. + - Repeated harassment of others. In general, if someone asks you to stop, then stop. +- **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Hyperpolymathy Estates is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of Hyperpolymathy Estates comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. + +Original text courtesy of the [Speak Up! project](https://web.archive.org/web/20141109123859/speakup.io/coc.html). + +## Questions? + +If you have questions, please see [Faq](https://github.com/metadatastician/standards/conduct_guidelines/faq). If that doesn't answer your questions, feel free to [contact us](mailto:j.d.a.jewell@open.ac.uk). diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index 912c180..6aa4438 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // SPDX-FileCopyrightText: Jonathan D.A. Jewell (hyperpolymath) -= Contributing — rsr-template-repo += Contributing — occupancy-types :toc: left Contributors work on the *spine*: the template every RSR repo is minted @@ -12,15 +12,15 @@ is the estate's bar. [source,bash] ---- # Clone the repository -git clone https://github.com/hyperpolymath/rsr-template-repo.git -cd rsr-template-repo +git clone https://github.com/hyperpolymath/occupancy-types.git +cd occupancy-types # Using Guix (recommended for reproducibility) guix shell -D -f build/guix.scm # Or using toolbox/distrobox -toolbox create rsr-template-repo-dev -toolbox enter rsr-template-repo-dev +toolbox create occupancy-types-dev +toolbox enter occupancy-types-dev # Install dependencies manually # Verify setup @@ -80,10 +80,10 @@ and include: Look for issues labelled: -* https://github.com/hyperpolymath/rsr-template-repo/labels/good%20first%20issue[`good first issue`] — Simple Perimeter 3 tasks -* https://github.com/hyperpolymath/rsr-template-repo/labels/help%20wanted[`help wanted`] — Community help needed -* https://github.com/hyperpolymath/rsr-template-repo/labels/documentation[`documentation`] — Docs improvements -* https://github.com/hyperpolymath/rsr-template-repo/labels/perimeter-3[`perimeter-3`] — Community sandbox scope +* https://github.com/hyperpolymath/occupancy-types/labels/good%20first%20issue[`good first issue`] — Simple Perimeter 3 tasks +* https://github.com/hyperpolymath/occupancy-types/labels/help%20wanted[`help wanted`] — Community help needed +* https://github.com/hyperpolymath/occupancy-types/labels/documentation[`documentation`] — Docs improvements +* https://github.com/hyperpolymath/occupancy-types/labels/perimeter-3[`perimeter-3`] — Community sandbox scope == Development workflow @@ -112,6 +112,23 @@ We follow https://www.conventionalcommits.org/[Conventional Commits]: [optional footer] ---- +=== Signed commits + +Every commit that reaches the default branch must be signed; a ruleset refuses +unsigned pushes. Estate policy: +https://github.com/hyperpolymath/standards/blob/main/docs/SIGNING-POLICY.adoc[SIGNING-POLICY]. + +* **People and interactive agents** sign with an SSH key registered on GitHub + as a *signing* key (`gpg.format=ssh`, `user.signingkey=.pub`, + `commit.gpgsign=true`). The committer email must be verified on that account. +* **Apps, bots and workflows** never `git push` local commits. They write + through the API (`createCommitOnBranch` or the estate `signed-push` action) + so that GitHub signs each commit. +* Merge PRs with **squash**. The ruleset checks every commit on the PR branch, + not just the result, so one unsigned commit blocks the merge. Re-create such a + branch with signed commits (`git cherry-pick -S`) and open a new PR. + Rebase-merge replays commits unsigned and is disabled. + == Cross-references This root document exists because the estate docs gate diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index 417391d..0000000 --- a/GEMINI.md +++ /dev/null @@ -1,8 +0,0 @@ -# Pointer - -This repository has no `AGENTS.md` yet. Until it does, the instructions -for every coding agent live in **[CLAUDE.md](./CLAUDE.md)**. Read that -file, and skip anything in it that is specific to Claude Code tooling. -Do not duplicate rules here. - -When `AGENTS.md` lands in this repository, retarget this pointer at it. diff --git a/Justfile b/Justfile index 1e1ea5c..f035395 100644 --- a/Justfile +++ b/Justfile @@ -18,10 +18,13 @@ set positional-arguments := true # Re-generate with: contractile gen-just import? "build/contractile.just" +# Provisioning canon: setup, doctor, heal, dev-shell, eval, ai-setup, … (see PROVISIONING below) +mod provision 'build/just/provision.just' + # Project metadata — customize these -project := "rsr-template-repo" +project := "occupancy-types" OWNER := "hyperpolymath" -REPO := "rsr-template-repo" +REPO := "occupancy-types" version := "0.1.0" tier := "infrastructure" # 1 | 2 | infrastructure @@ -52,7 +55,7 @@ info: @echo "Version: {{version}}" @echo "RSR Tier: {{tier}}" @echo "Recipes: $(just --summary | wc -w)" - @[ -f ".machine_readable/descriptiles/STATE.a2ml" ] && grep -oP 'phase\s*=\s*"\K[^"]+' .machine_readable/descriptiles/STATE.a2ml | head -1 | xargs -I{} echo "Phase: {}" || true + @d=$(bash scripts/deed-field.sh --find . 2>/dev/null) && bash scripts/deed-field.sh "$d" status phase | xargs -I{} echo "Phase: {}" || true # Run Invariant Path overlay tools for this repository invariant-path *ARGS: @@ -85,26 +88,19 @@ import? "build/just/assess.just" # BUILD & COMPILE # ═══════════════════════════════════════════════════════════════════════════════ -# Build the project (debug mode) -build *args: - @echo "Building {{project}} (debug)..." - # TODO: Replace with your build command - # Examples: - # cargo build {{args}} # Rust - # mix compile {{args}} # Elixir - # zig build {{args}} # Zig - # deno task build {{args}} # Deno/ - @echo "Build complete" +# Build the project with every detected language's toolchain (here: pack/idris2 for +# the ABI, then `zig build` in src/interface/ffi). Replace the delegation with a +# body of your own when the project needs a different build. +build: provision::build # Build in release mode with optimizations build-release *args: - @echo "Building {{project}} (release)..." # TODO: Replace with your release build command # Examples: # cargo build --release {{args}} # MIX_ENV=prod mix compile {{args}} # zig build -Doptimize=ReleaseFast {{args}} - @echo "Release build complete" + @echo "FAIL: 'just build-release' is not wired yet — nothing was built. Edit the 'build-release' recipe." >&2; exit 1 # Build and watch for changes (requires entr or similar) build-watch: @@ -113,7 +109,7 @@ build-watch: # Examples: # find src -name '*.rs' | entr -c just build # mix compile --force --warnings-as-errors - # deno task dev + # bun run dev # Clean build artifacts [reversible: rebuild with `just build`] clean: @@ -152,67 +148,69 @@ check-ir: @bash scripts/check.sh --runnable-only # Run tests with verbose output -test-verbose: - @echo "Running tests (verbose)..." - # TODO: Replace with verbose test command +test-verbose: provision::test # Smoke test test-smoke: - @echo "Smoke test..." # TODO: Add basic sanity checks + @echo "FAIL: 'just test-smoke' is not wired yet — no smoke check ran. Edit the 'test-smoke' recipe." >&2; exit 1 # Run end-to-end tests (full pipeline: build → run → verify) e2e: - @echo "Running E2E tests..." - # TODO: Replace with your E2E test command. Examples: - # bash tests/e2e.sh # Shell-based E2E - # npx playwright test # Browser E2E - # mix test test/integration/e2e_test.exs # Elixir E2E - # cargo test --test end_to_end # Rust E2E - @echo "E2E tests passed!" + bash tests/e2e.sh # Run aspect tests (cross-cutting concern validation) aspect: - @echo "Running aspect tests..." - # TODO: Replace with your aspect test command. Examples: - # bash tests/aspect_tests.sh # Shell-based aspect tests - # cargo test --test aspects # Rust aspect tests # Aspect tests validate architectural invariants: # - Thread safety (mutex in FFI modules) # - ABI/FFI contract (declarations match exports) # - SPDX compliance (all files have license headers) # - No dangerous patterns (believe_me, assert_total, etc.) - @echo "Aspect tests passed!" - -# Run benchmarks (performance regression detection) -bench: - @echo "Running benchmarks..." - # TODO: Replace with your benchmark command. Examples: - # cargo bench # Rust criterion - # zig build bench # Zig benchmarks - # mix run bench/benchmarks.exs # Elixir benchee - # deno bench # Deno bench - @echo "Benchmarks complete!" + bash tests/aspect_tests.sh + +# Run benchmarks (performance regression detection). Reports N/A until a language +# declares some (e.g. a "bench" step in build.zig, [[bench]] in Cargo.toml). +bench: provision::bench + +# Run the proven-tests-and-benches 3-outcome suite (0 pass / 1 fail / 2 setup) +proven-tests *flags: + bash scripts/run-proven-suite.sh {{flags}} + +# Run the Idris2 proof escape-hatch & totality gate (proven-tests-and-benches) +escape-hatches: + bash scripts/check-escape-hatches.sh + +# Run template benchmarks and check Six-Sigma leave-one-out regression vs baseline +bench-check: + #!/usr/bin/env bash + set -euo pipefail + tmp="$(mktemp)" + trap 'rm -f "$tmp"' EXIT + bash benches/template_bench.sh --json . > "$tmp" + bash scripts/check-bench-regression.sh "$tmp" benches/baseline.json + +# Check RFC 7493 (I-JSON) + RFC 8785 (JCS) canonical form across tracked JSON +ijson-check: + bash scripts/check-ijson-jcs.sh --enforce . # Run readiness tests (Component Readiness Grade: D/C/B) readiness: - @echo "Running readiness tests..." # TODO: Replace with your readiness test command. Examples: # cargo test --test readiness -- --nocapture - @echo "Readiness tests complete!" + @echo "FAIL: 'just readiness' is not wired yet — no readiness test ran. Edit the 'readiness' recipe." >&2; exit 1 # Print the current CRG grade (reads from READINESS.md '**Current Grade:** X' line) crg-grade: - @grade=$$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ - [ -z "$$grade" ] && grade="X"; \ - echo "$$grade" + @grade=$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ + [ -z "$grade" ] && grade="X"; \ + echo "$grade" # Print a shields.io CRG badge for embedding in README files # Looks for '**Current Grade:** X' in READINESS.md; falls back to X crg-badge: - @grade=$$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ - [ -z "$$grade" ] && grade="X"; \ - case "$$grade" in \ + @grade=$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ + [ -z "$grade" ] && grade="X"; \ + case "$grade" in \ A) color="brightgreen" ;; \ B) color="green" ;; \ C) color="yellow" ;; \ @@ -221,16 +219,53 @@ crg-badge: F) color="critical" ;; \ *) color="lightgrey" ;; \ esac; \ - echo "[![CRG $$grade](https://img.shields.io/badge/CRG-$$grade-$$color?style=flat-square)](https://github.com/hyperpolymath/standards/tree/main/component-readiness-grades)" + echo "[![CRG $grade](https://img.shields.io/badge/CRG-$grade-$color?style=flat-square)](https://github.com/hyperpolymath/standards/tree/main/component-readiness-grades)" # Run the full merge-requirement test suite (ALL categories) # Per STANDING rule: P2P + E2E + aspect + execution + lifecycle + bench test-all: test e2e aspect bench readiness @echo "All test categories passed — safe to merge!" -# Run all quality checks -quality: fmt-check lint test - @echo "All quality checks passed!" +# Run all quality checks — and fail when one of them ran nothing. +# +# This used to be `quality: fmt-check lint test` plus `@echo "All quality checks +# passed!"`. The echo is the defect: the provisioning engine reports a verb with +# no command for any detected language as +# +# info fmt-check: N/A for julia (archetype: library) — nothing to run… +# +# and exits 0. That is honest output, but this recipe turned it into "All +# quality checks passed!" — so a Julia repo printed green having run only +# `Pkg.test()`, and a downstream repo shipped `fmt`, `fmt-check` and `lint` +# recipes that were `@echo` stubs for the same reason. +# +# N/A is not a pass. A gating verb that cannot run for this repo's languages is +# either wired up (the julia-library archetype wires fmt/fmt-check/lint with +# scripts/julia-quality.jl) or removed from this recipe deliberately — never +# echoed over. +quality: + #!/usr/bin/env bash + set -uo pipefail + rc=0 + for verb in fmt-check lint test; do + echo "" + echo "── ${verb} ──" + out="$(just "${verb}" 2>&1)"; st=$? + printf '%s\n' "$out" + if [ "$st" -ne 0 ]; then + echo "FAIL: '${verb}' exited ${st}" >&2 + rc=1 + continue + fi + if printf '%s\n' "$out" | grep -q 'N/A for'; then + echo "FAIL: '${verb}' ran nothing — N/A for every detected language." >&2 + echo " N/A is not a pass: wire the verb for this language, or drop" >&2 + echo " it from the 'quality' recipe on purpose." >&2 + rc=1 + fi + done + [ "$rc" -eq 0 ] && echo "" && echo "All quality checks ran and passed." + exit "$rc" # Fix all auto-fixable issues [reversible: git checkout] fix: fmt @@ -241,46 +276,24 @@ fix: fmt # ═══════════════════════════════════════════════════════════════════════════════ # Format all source files [reversible: git checkout] -fmt: - @echo "Formatting source files..." - # TODO: Replace with your formatter - # Examples: - # cargo fmt - # mix format - # gleam format - # deno fmt +fmt: provision::fmt # Check formatting without changes -fmt-check: - @echo "Checking formatting..." - # TODO: Replace with your format check - # Examples: - # cargo fmt --check - # mix format --check-formatted - # gleam format --check - -# Run linter -lint: - @echo "Linting source files..." - # TODO: Replace with your linter - # Examples: - # cargo clippy -- -D warnings - # mix credo --strict - # gleam check +fmt-check: provision::fmt-check + +# Run linter (`zig fmt --check` on src/interface/ffi; like `test`, it parses the +# FFI sources only after `just repo-init` has filled the template tokens) +lint: provision::lint # ═══════════════════════════════════════════════════════════════════════════════ # RUN & EXECUTE # ═══════════════════════════════════════════════════════════════════════════════ -# Run the application -run *args: build - # TODO: Replace with your run command - echo "Run not configured yet" +# Run the application (N/A for a library; wire a "run" step or executable to enable) +run: provision::run # Run with verbose output -run-verbose *args: build - # TODO: Replace with verbose run command - echo "Run not configured yet" +run-verbose: run # Install to user path install: build-release @@ -308,14 +321,15 @@ deps-audit: # Examples: # cargo audit # mix audit - @command -v trivy >/dev/null && trivy fs --severity HIGH,CRITICAL --quiet . || true + @command -v trivy >/dev/null || { echo "FAIL: trivy is not installed, so nothing was audited (mise install, or edit this recipe)." >&2; exit 1; } + trivy fs --severity HIGH,CRITICAL --exit-code 1 --quiet . @echo "Audit complete" # ═══════════════════════════════════════════════════════════════════════════════ -# ARRIVAL PACK — agent-facing CLAUDE.md, compiled from a2ml +# ARRIVAL PACK — agent-facing CLAUDE.md, compiled from the repo deed # ═══════════════════════════════════════════════════════════════════════════════ -# Compile CLAUDE.md (the agent arrival pack) from this repo's a2ml +# Compile CLAUDE.md (the agent arrival pack) from this repo's deed claude-md: @bash .machine_readable/arrival-pack/generate.sh @@ -339,7 +353,7 @@ validate-repo-map: rm -f "$before" echo "repository map: up to date" -# Fail if CLAUDE.md's generated region drifted from a2ml or was hand-edited +# Fail if CLAUDE.md's generated region drifted from the deed or was hand-edited validate-claude-md: @bash .machine_readable/arrival-pack/verify.sh @@ -462,16 +476,17 @@ import? "build/just/validate.just" # STATE MANAGEMENT # ═══════════════════════════════════════════════════════════════════════════════ -# Update STATE.a2ml timestamp -state-touch: - @if [ -f ".machine_readable/descriptiles/STATE.a2ml" ]; then \ - sed -i 's/last-updated = "[^"]*"/last-updated = "'"$(date +%Y-%m-%d)"'"/' .machine_readable/descriptiles/STATE.a2ml && \ - echo "STATE.a2ml timestamp updated"; \ - fi +# Repo state lives in the repo deed's (status …) and (maturity …) clauses +# (rsr-template-repo#209); the journal is docs/status/ROADMAP.adoc. There is +# no last-updated stamp to touch: git log is the record of when it changed. -# Show current phase from STATE.a2ml +# Show the lifecycle phase and maturity from the repo deed state-phase: - @sed -n 's/^[[:space:]]*phase[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' .machine_readable/descriptiles/STATE.a2ml 2>/dev/null | head -1 || echo "unknown" + #!/usr/bin/env bash + DEED=$(bash scripts/deed-field.sh --find . || true) + if [ -z "$DEED" ]; then echo "unknown (no *_chora.deed)"; exit 0; fi + echo "phase: $(bash scripts/deed-field.sh "$DEED" status phase || echo unknown)" + echo "maturity: $(bash scripts/deed-field.sh "$DEED" maturity level || echo unknown)" # ═══════════════════════════════════════════════════════════════════════════════ # GUIX @@ -493,7 +508,7 @@ guix-build: automate task="all": #!/usr/bin/env bash case "{{task}}" in - all) just fmt && just lint && just test && just docs && just state-touch ;; + all) just fmt && just lint && just test && just docs ;; cleanup) just clean && find . -name "*.orig" -delete && find . -name "*~" -delete ;; update) just deps && just validate ;; *) echo "Unknown: {{task}}. Use: all, cleanup, update" && exit 1 ;; @@ -589,19 +604,46 @@ assail: @command -v panic-attack >/dev/null 2>&1 && panic-attack assail . || echo "WARN: panic-attack not found — install from https://github.com/hyperpolymath/panic-attacker" -# Self-diagnostic — checks dependencies, permissions, paths -doctor: - @echo "Running diagnostics for rsr-template-repo..." - @echo "Checking required tools..." - @command -v just >/dev/null 2>&1 && echo " [OK] just" || echo " [FAIL] just not found" - @command -v git >/dev/null 2>&1 && echo " [OK] git" || echo " [FAIL] git not found" - @echo "Checking for hardcoded paths..." - @grep -rn '$HOME\|$ECLIPSE_DIR' --include='*.rs' --include='*.ex' --include='*.res' --include='*.gleam' --include='*.sh' . 2>/dev/null | head -5 || echo " [OK] No hardcoded paths" - @echo "Diagnostics complete." +# ═══════════════════════════════════════════════════════════════════════════════ +# PROVISIONING (standards 3-practice/provisioning/PROVISIONING-STANDARD.adoc) +# ═══════════════════════════════════════════════════════════════════════════════ +# The canon verbs live in the provision:: module; these delegations make +# `just ` and `just provision::` the same thing. Repository-specific +# checks go in a `doctor-local` / `setup-local` / `heal-local` recipe. + +# Install everything this repository needs, then run doctor +setup: provision::setup + +# Check the environment: PASS/WARN/FAIL with fix hints (exit 1 on any FAIL) +doctor: provision::doctor + +# Apply the safe fixes doctor knows, then re-run doctor +heal: provision::heal + +# Enter the Guix development shell (mise environment when Guix is absent) +dev-shell: provision::dev-shell + +# Bump mise to latest, re-lock mise.lock, re-pin the Guix channel +toolchain-refresh: provision::toolchain-refresh + +# Print the one sentence to give any AI assistant to set this repository up +ai-setup: provision::ai-setup + +# Print a warm-up to paste into any AI: user, dev or maintainer +ai-warmup who="user": (provision::ai-warmup who) + +# test + bench with timings, saved under .eval/ +eval: provision::eval + +# Where the toolchain and configuration are declared +config-show: provision::config-show + +# How to fetch this repository with OPSM +opsm: provision::opsm # Guided tour of key features tour: - @echo "=== rsr-template-repo Tour ===" + @echo "=== occupancy-types Tour ===" @echo "" @echo "1. Project structure:" @ls -la @@ -616,12 +658,12 @@ tour: # Open feedback channel with diagnostic context help-me: - @echo "=== rsr-template-repo Help ===" + @echo "=== occupancy-types Help ===" @echo "Platform: $(uname -s) $(uname -m)" @echo "Shell: $SHELL" @echo "" @echo "To report an issue:" - @echo " https://github.com/hyperpolymath/rsr-template-repo/issues/new" + @echo " https://github.com/hyperpolymath/occupancy-types/issues/new" @echo "" @echo "Include the output of 'just doctor' in your report." @@ -719,3 +761,13 @@ uncloak: else echo "Use 'ls -a' to view dotfiles on this OS." fi + +# Repair .github/workflows/actions.lock after a Dependabot github-actions bump. +# Dependabot rewrites `uses:` in the workflow YAML but cannot touch the lock, so +# the Lock Sync Gate goes red. This resolves each new ref (git ls-remote, peeled +# commit for annotated tags), rewrites the lock, and re-validates with +# scripts/check-lock-sync.sh. Fails loudly (lock untouched) on anything it cannot +# resolve. Run it on the Dependabot branch, then commit with a SIGNED commit. +lock-sync-fix: + ./scripts/lock-sync-fix.sh + ./scripts/check-lock-sync.sh diff --git a/LICENSE.adoc b/LICENSE.adoc new file mode 100644 index 0000000..14e2f77 --- /dev/null +++ b/LICENSE.adoc @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.adoc b/README.adoc index 558cf9e..07d2d53 100644 --- a/README.adoc +++ b/README.adoc @@ -12,17 +12,17 @@ image:https://img.shields.io/badge/Provenance-Quantum--Safe-blueviolet[Quantum-S // ── Standard & quality gates ──────────────────────────────────────────────── image:https://img.shields.io/badge/RSR-Rhodium_Standard-9C27B0[Rhodium Standard Repository,link="https://github.com/hyperpolymath/rhodium-standard-repositories"] -image:https://www.bestpractices.dev/projects/{{OPENSSF_BP_ID}}/badge[OpenSSF Best Practices,link="https://www.bestpractices.dev/projects/{{OPENSSF_BP_ID}}"] -image:https://api.scorecard.dev/projects/github.com/{{OWNER}}/{{REPO}}/badge[OpenSSF Scorecard,link="https://scorecard.dev/viewer/?uri=github.com/{{OWNER}}/{{REPO}}"] -image:https://sonarcloud.io/api/project_badges/measure?project={{OWNER}}_{{REPO}}&metric=alert_status[SonarQube Quality Gate,link="https://sonarcloud.io/summary/new_code?id={{OWNER}}_{{REPO}}"] -image:https://archive.softwareheritage.org/badge/origin/https://{{FORGE}}/{{OWNER}}/{{REPO}}/[Archived in Software Heritage,link="https://archive.softwareheritage.org/browse/origin/?origin_url=https://{{FORGE}}/{{OWNER}}/{{REPO}}"] +image:https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/hyperpolymath/occupancy-types/governance-badges/badges/k9.json[K9 conformance,link="https://github.com/hyperpolymath/occupancy-types/actions/workflows/dogfood-gate.yml"] +image:https://api.scorecard.dev/projects/github.com/hyperpolymath/occupancy-types/badge[OpenSSF Scorecard,link="https://scorecard.dev/viewer/?uri=github.com/hyperpolymath/occupancy-types"] +image:https://sonarcloud.io/api/project_badges/measure?project=hyperpolymath_occupancy-types&metric=alert_status[SonarQube Quality Gate,link="https://sonarcloud.io/summary/new_code?id=hyperpolymath_occupancy-types"] +image:https://archive.softwareheritage.org/badge/origin/https://github.com/hyperpolymath/occupancy-types/[Archived in Software Heritage,link="https://archive.softwareheritage.org/browse/origin/?origin_url=https://github.com/hyperpolymath/occupancy-types"] // ── Provenance & ecosystem ────────────────────────────────────────────────── image:https://img.shields.io/badge/Idris-Inside-5E5086?logo=idris&logoColor=white[Idris Inside,link="https://github.com/hyperpolymath/proven"] image:https://img.shields.io/badge/Zig-Zero--overhead-F7A41D?logo=zig&logoColor=black[Zig Zero-overhead FFI,link="https://github.com/hyperpolymath/typed-wasm"] -image:https://api.thegreenwebfoundation.org/greencheckimage/{{FORGE}}[Green Web,link="https://www.thegreenwebfoundation.org/green-web-check/?url={{FORGE}}"] +image:https://api.thegreenwebfoundation.org/greencheckimage/github.com[Green Web,link="https://www.thegreenwebfoundation.org/green-web-check/?url=github.com"] -{{PROJECT_DESCRIPTION}} +Early-stage research on compositional worst-case resource bounds, with a linear session-IR checker, occupancy-indexed state experiments in Idris 2, and a prototype stack-depth certificate checker. The long-term goal is predictable memory and network-buffer usage. == This repository: occupancy-types @@ -89,36 +89,86 @@ Replace this section with a description of *your* project once initialised. [source,bash] ---- # 1. Create a repo from this template (or clone it), then from the repo root: -just repo-init # interactive bootstrap: fills every {{PLACEHOLDER}} +mise trust # allow this repository's mise.toml (needed without a TTY) +mise install # the toolchain, including the yq repo-init needs +mise exec -- just repo-init # interactive bootstrap: fills every {{PLACEHOLDER}} # 2. See the available tasks: -just # lists all phases (build, test, validate, audit, ...) +mise exec -- just # lists all phases (build, test, validate, audit, ...) # 3. Check the repo still satisfies the RSR shape: -just validate # structure + metadata checks +mise exec -- just validate # structure + metadata checks ---- +`mise exec --` puts the toolchain on `PATH` for that one command. Once mise is +activated in your shell (`eval "$(mise activate bash)"`, or your shell's +equivalent), plain `just` works and the prefix can be dropped. + `just repo-init` prompts for the project name, owner, author, licence contact, and the other values listed in `.machine_readable/ai/PLACEHOLDERS.adoc`, substitutes them across the tree, validates the result, and (if available) runs the `k9-svc` checks. -== AI-Assisted Installation +[[ai-install]] +== AI-Assisted Installation (Recommended) + +=== Just say it + +Say this to any AI assistant that can read a URL and run commands (or give you +commands to paste): + +[source,text] +---- +Create a new repository from https://github.com/hyperpolymath/occupancy-types +---- + +The URL is the key: it leads the assistant to +link:docs/AI_INSTALLATION_GUIDE.adoc[`docs/AI_INSTALLATION_GUIDE.adoc`], the +complete step-by-step recipe. The assistant checks your system, installs the +prerequisites, instantiates the template with `just repo-init`, and verifies the +result with `just doctor`. -If you are an AI agent installing this project, read -link:docs/AI_INSTALLATION_GUIDE.adoc[the AI installation guide] first — it gives -the orientation order, the full prompt sequence, and the privacy notice. +* "Set up occupancy-types so I can work on the template itself — https://github.com/hyperpolymath/occupancy-types" +* `./launcher.sh --ai-setup` (or `just ai-setup`) prints the line and the guide path. + +=== What you will be asked + +. Your operating system (Linux, macOS, or Windows via WSL2). +. The new project's name, owner, author and licence contact (the values listed in + `.machine_readable/ai/PLACEHOLDERS.adoc`). +. To confirm the privacy notice below. + +=== Privacy and security + +[IMPORTANT] +==== +Setting up occupancy-types clones this repository and installs its developer +tools per-user with mise (no administrator rights, nothing sent anywhere). +`just repo-init` rewrites files only inside the new checkout; it creates no +remote repository and pushes nothing until you do. +==== -The one trap worth stating up front: **do not set `RSR_NON_INTERACTIVE=1`**. It -stubs the shell builtin `read`, which also disables the loops that perform token -substitution — the run never terminates and substitutes nothing. Pipe answers to -stdin instead. +=== Minting without a terminal + +Agents and CI can mint non-interactively: set `RSR_NON_INTERACTIVE=1` and pass +the answers as environment variables named like the prompts' variables in +`build/just/repo-init.just` (`PROJECT_NAME`, `REPO`, `OWNER`, `AUTHOR` and +`AUTHOR_EMAIL` are required; any other answer left unset is blank or takes its +default). Piping the answers to stdin in prompt order also works. + +=== Troubleshooting + +Run `just doctor`; each FAIL carries a code explained in +link:docs/SETUP.adoc#troubleshooting[`docs/SETUP.adoc` §Troubleshooting], and +`just heal` applies the automatic fixes. Prefer to do it yourself? The full +manual route is link:docs/SETUP.adoc[`docs/SETUP.adoc`]; developers and +maintainers can brief an AI with `just ai-warmup dev` / `just ai-warmup maintainer`. == What you get * *Machine-readable metadata* (`.machine_readable/descriptiles/`) — `STATE`, `META`, `ECOSYSTEM`, `PLAYBOOK`, `AGENTIC`, `NEUROSYM`, `CLADE`, and `anchors/ANCHOR`, in deed, so tools and agents can read the project's state and boundaries. -* *AI gatekeeper protocol* — `rsr-template-repo_chora.deed` (the repo deed) is the +* *AI gatekeeper protocol* — `occupancy-types_chora.deed` (the repo deed) is the universal entry point that tells an AI agent how to work in this repo before it touches anything; it carries the AI allocation policy and the ply directory tree. * *Typed ABI/FFI seam* — `src/interface/Abi/` (Idris2 type + layout proofs) over @@ -130,6 +180,41 @@ stdin instead. * *Governance* — `GOVERNANCE.adoc`, `MAINTAINERS.adoc`, `.github/` community health files, and a release `AUDIT.adoc` gate. +== Governance badges + +The *K9 conformance* badge in the row at the top becomes live after +`just repo-init` replaces the `hyperpolymath` and `occupancy-types` placeholders in its URL +and an eligible push to `main` publishes it. It is published by +`.github/workflows/dogfood-gate.yml` to a generated *orphan* branch called +`governance-badges`, and read back through the shields endpoint. + +[cols="1,3",options="header"] +|=== +| Badge | What it means + +| green `success` +| The K9 lane ran for that commit on `main` and passed. + +| red `failure` +| The K9 lane ran for that commit on `main` and failed. + +| grey `skipped`, `cancelled`, `unavailable`, `unknown` +| That commit produced *no trustworthy* K9 result. Grey is not "pending" and + not a stale green: a skipped, cancelled or unavailable lane replaces an + earlier green badge with grey. +|=== + +* `governance-badges` is *generated output*. It deliberately has *no common + ancestor with `main`*, it has no pull request, and it must *never be merged*. + Recent pushes to it are ordinary badge refreshes. +* A run that would publish the same bytes makes *no commit at all*. +* Nothing in the badge pipeline can turn a failing validation green: only a + lane that ran and passed, with its artifact present, renders brightgreen. + +The full operational contract - the exact state table, how API errors are +classified, how concurrent publications are serialised, and how to repair a bad +file - is in link:.github/workflows/README.adoc[`.github/workflows/README.adoc`]. + == Repository map The authoritative map is generated, so it cannot drift from the tree: diff --git a/SUPPORT.adoc b/SUPPORT.adoc new file mode 100644 index 0000000..4a41b06 --- /dev/null +++ b/SUPPORT.adoc @@ -0,0 +1,7 @@ +# Support + +For questions, help, and community discussion: + +- GitHub Discussions: https://github.com/hyperpolymath/occupancy-types/discussions +- GitHub Issues: https://github.com/hyperpolymath/occupancy-types/issues +- Documentation: See README.adoc in the root directory. diff --git a/archetypes/README.adoc b/archetypes/README.adoc deleted file mode 100644 index 8ed12ca..0000000 --- a/archetypes/README.adoc +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -= Archetypes — template-only overlay data for `just repo-init` - -An *archetype* specialises the spine for one kind of repository (a Julia -library, a WordPress plugin, …) **as data, not as a fork**. Each archetype is -a directory here: - - archetypes//ARCHETYPE.a2ml required — the contract (see below) - archetypes//overlay/ optional — files copied over the spine - -Decision record: `docs/decisions/0003-forge-and-sustain-lifecycle.adoc`. -Normative lifecycle: standards `spec/SCAFFOLD-LIFECYCLE.adoc`. - -== The contract (`ARCHETYPE.a2ml`) - -Single-line values, grep-parseable (same discipline as the canon's -`template-capability-gates.toml`): - -- `[metadata]` — `archetype`, `schema_version`. -- `[capability] preset = ""` — MUST name a preset in standards - `.machine_readable/template-capability-gates.toml [presets]`. The archetype - directory name and the preset name are the same string by convention. -- `[tokens]` — `TOKEN = "value"` lines. Only tokens the archetype can - honestly determine (`PROJECT_KIND`, `PROJECT_DOMAIN`, `LANG_STACK`, - `BUILD_CMD`, `TEST_CMD`, `DEPS`, `BUILD_OUTPUT_PATH`, `MAIN_FUNCTION`). - Values MUST NOT contain `|` (the init sed delimiter — init skips such - values with a warning). Judgement-only tokens - (`PROJECT_UNIQUE_STRENGTH`, `TARGET_AUDIENCE`, `MUST_INVARIANTS`, - per-repo names like `PACKAGE_NAME`) MUST be omitted so they stay - `UNASSIGNED` — an archetype never writes fiction. - -`just repo-init ` applies the overlay directory (if present), adds the -token substitutions, and writes the preset into the minted repo's -`.machine_readable/rsr-profile.a2ml`. This whole `archetypes/` directory is -template-only and is removed from the minted tree, exactly like -`build/templates/`. - -== Current archetypes - -- `julia-library` — reference instance, harvested from - rsr-julia-library-template-repo's `VARIANT.a2ml` (ADR-0002) and the - dormant scaffoldia registry's `julia/package.ncl`. The variant repo - remains the full hand-maintained reference until the scaffoldia - inversion; this archetype covers the `just repo-init` path. - -Declared, content to follow (presets already reserved in the canon): -`wordpress-plugin`, `zotero-plugin`, `userscript`. Do not stub them empty — -an archetype exists when its contract and overlay are real. diff --git a/archetypes/julia-library/ARCHETYPE.a2ml b/archetypes/julia-library/ARCHETYPE.a2ml deleted file mode 100644 index 4a5359d..0000000 --- a/archetypes/julia-library/ARCHETYPE.a2ml +++ /dev/null @@ -1,34 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# ARCHETYPE.a2ml — julia-library archetype for `just repo-init` (ADR-0003). -# -# Harvested 2026-07-21 from rsr-julia-library-template-repo (VARIANT.a2ml, -# ADR-0002 reference instance) and the dormant scaffoldia registry entry -# registry/julia/package.ncl. The variant repo remains the full -# hand-maintained reference (Project.toml [compat] closure, Aqua+JET, -# Documenter, flagship rubric); this archetype covers the `just repo-init` path. -# -# Contract rules: archetypes/README.adoc. Single-line values; no `|` in -# token values (init's sed delimiter). Tokens omitted here stay UNASSIGNED -# on purpose: PACKAGE_NAME and MUST_INVARIANTS are per-repo judgement, and -# an archetype must not write fiction. - -[metadata] -archetype = "julia-library" -name = "julia-library" -schema_version = "0.1.0" -harvested-from = "rsr-julia-library-template-repo VARIANT.a2ml + scaffoldia registry julia/package.ncl" - -[capability] -# Preset defined in standards .machine_readable/template-capability-gates.toml -preset = "julia-library" - -[tokens] -PROJECT_KIND = "library" -PROJECT_DOMAIN = "julia" -LANG_STACK = "Julia (1.9+)" -BUILD_CMD = "julia --project=. -e 'using Pkg; Pkg.instantiate()'" -TEST_CMD = "julia --project=. -e 'using Pkg; Pkg.test()'" -DEPS = "Julia General registry via Project.toml [deps] with [compat] closure" -BUILD_OUTPUT_PATH = "src/" diff --git a/archetypes/julia-library/overlay/.github/workflows/actions.lock b/archetypes/julia-library/overlay/.github/workflows/actions.lock deleted file mode 100644 index abfe3a3..0000000 --- a/archetypes/julia-library/overlay/.github/workflows/actions.lock +++ /dev/null @@ -1,18 +0,0 @@ -# This file is machine-generated by `gh actions-lock`. -# Do not edit by hand; run `gh actions-lock` to update. -# Docs: https://gh.io/actions-lockfile -# -# Minted with the julia-library archetype. Resolutions verified 2026-09-19 -# (see the workflow headers). Regenerate in the same PR as any uses: change. -version: 'v0.0.2' -workflows: - '.github/workflows/julia-ci.yml': - - 'actions/checkout@v7.0.1' - - 'julia-actions/cache@v2' - - 'julia-actions/setup-julia@v3.0.2' - '.github/workflows/julia-docs.yml': - - 'actions/checkout@v7.0.1' - - 'julia-actions/cache@v2' - - 'julia-actions/julia-buildpkg@v1' - - 'julia-actions/julia-docdeploy@v1' - - 'julia-actions/setup-julia@v3.0.2' diff --git a/archetypes/julia-library/overlay/.github/workflows/julia-ci.yml.in b/archetypes/julia-library/overlay/.github/workflows/julia-ci.yml.in deleted file mode 100644 index 9160ce7..0000000 --- a/archetypes/julia-library/overlay/.github/workflows/julia-ci.yml.in +++ /dev/null @@ -1,84 +0,0 @@ -# {{PROJECT_NAME}} — Julia CI (julia-library archetype) -# -# Pin posture (estate ruling R2, 2026-09-02; D38/D39 resolution 2026-09-19): -# symbolic refs here, pinned by .github/workflows/actions.lock (same dir). -# Refs resolved 2026-09-19 against the estate estate-pins: -# actions/checkout@v7.0.1 = 3d3c42e5aac5ba805825da76410c181273ba90b1 -# julia-actions/setup-julia@v3.0.2 = fa02766e078afaaf09b14210362cee14137e6a32 -# julia-actions/cache@v2 = a7bed9df697e5d7309d68afe7542a87621a8b6c8 -# Regenerate the lock in the SAME PR as any uses: change (estate MUST). - -name: Julia CI - -on: - push: - branches: [main, master] - pull_request: - -# No `continue-on-error` on a MUST check (R1–R9): a red job is the gate -# working. Coverage (R4) is reported by the estate's standard coverage -# upload where present; it is not a blocker at template level. -jobs: - fmt: - name: Format (JuliaFormatter) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7.0.1 - - uses: julia-actions/setup-julia@v3.0.2 - with: - version: "1" - - name: Check formatting (check mode — never writes in CI) - run: | - julia --project=. -e ' - using Pkg - Pkg.add("JuliaFormatter") - using JuliaFormatter - for f in filter(isfile, Iterators.flatten(( - [joinpath("src", x) for x in readdir("src")], - [joinpath("test", x) for x in readdir("test") if x != "cases"], - [joinpath("test", "cases", x) for x in readdir(joinpath("test", "cases"))], - ))) - text = read(f, String) - formatted = JuliaFormatter.format_string(text) - if text != formatted - @error "not formatted" file=f - exit(1) - end - end' - - jet: - name: Static analysis (JET) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7.0.1 - - uses: julia-actions/setup-julia@v3.0.2 - with: - version: "1" - - uses: julia-actions/cache@v2 - - name: JET.test_package - run: | - julia --project=. -e ' - using Pkg - Pkg.add("JET") - using JET - JET.test_package(path=".", julia_version="1")' - - test: - name: Test (Test + Aqua) - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - # The [compat] floor IS tested — an untested floor is an unproven - # floor (measured estate gap: Axiology-class repos declare julia 1.9 - # but test 1.10/1.11 only). - julia: ["1.9", "1"] - os: [ubuntu-latest] - steps: - - uses: actions/checkout@v7.0.1 - - uses: julia-actions/setup-julia@v3.0.2 - with: - version: ${{ matrix.julia }} - - uses: julia-actions/cache@v2 - - name: Pkg.test() - run: julia --project=. -e 'using Pkg; Pkg.test()' diff --git a/archetypes/julia-library/overlay/.github/workflows/julia-docs.yml.in b/archetypes/julia-library/overlay/.github/workflows/julia-docs.yml.in deleted file mode 100644 index d6e8c61..0000000 --- a/archetypes/julia-library/overlay/.github/workflows/julia-docs.yml.in +++ /dev/null @@ -1,41 +0,0 @@ -# {{PROJECT_NAME}} — docs build & deploy (julia-library archetype) -# -# Fails LOUD while docs/ is absent: a declared `docs-site` capability with -# no build is a defect (criterion 5.2.6, na-honesty). Once -# docs/Project.toml + docs/make.jl + docs/src/ exist, this builds and -# deploys via julia-actions/julia-docdeploy (canonical action, verified -# 2026-09-19: main = 9bff73f68f6a2aa952c8856564f3bd4aea155c15). -# -# Pin posture (estate ruling R2 / D38-D39): symbolic refs, pinned by -# .github/workflows/actions.lock. Regenerate the lock in the same PR as -# any uses: change. - -name: Docs - -on: - push: - branches: [main, master] - workflow_dispatch: - -jobs: - docs: - name: Build and deploy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7.0.1 - - name: Layout gate — fail loud while docs/ is absent - run: | - if [ ! -f docs/Project.toml ] || [ ! -f docs/make.jl ] || [ ! -d docs/src ]; then - echo "::error::docs-site declared but no Documenter build: docs/Project.toml, docs/make.jl and docs/src/ are required. Either add the build or drop the docs-site capability from rsr-profile.a2ml." - exit 1 - fi - - uses: julia-actions/setup-julia@v3.0.2 - with: - version: "1" - - uses: julia-actions/cache@v2 - # PIN-ME: resolve and lock this in the first estate pass (4.1.3 - # stays loud until pinned). Ref resolved 2026-09-19. - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-docdeploy@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/archetypes/julia-library/overlay/Project.toml.in b/archetypes/julia-library/overlay/Project.toml.in deleted file mode 100644 index ce0bc79..0000000 --- a/archetypes/julia-library/overlay/Project.toml.in +++ /dev/null @@ -1,32 +0,0 @@ -# minted by: just repo-init julia-library ({{CURRENT_YEAR}}) -# owner: {{OWNER}} -# -# uuid = "{{PACKAGE_UUID}}" is DERIVED at mint (UUIDv5 over the repo -# identity — see repo-init's julia-library rule); it is stable across -# re-mints of the same repo and is never hand-edited. If the mint could not -# derive it (no uuidgen AND no python3 on the operator machine) it is -# UNASSIGNED and Pkg refuses to load the package until the owner assigns one. - -name = "{{PROJECT_NAME}}" -uuid = "{{PACKAGE_UUID}}" -description = "{{PROJECT_DESCRIPTION}}" -authors = ["{{AUTHOR}} <{{AUTHOR_EMAIL}}"] -license = "MPL-2.0" -version = "0.1.0" - -[deps] - -# Test uuid = stdlib, resolved via Base.identify_package("Test") 2026-09-19 -[extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" - -# Aqua's deps_compat check requires a compat bound for EVERY extra — -# a template that omits these fails its own gate (measured 2026-09-19). -[targets] -test = ["Test", "Aqua"] - -[compat] -julia = "1.9" -Test = "1.9" -Aqua = "0.8" diff --git a/archetypes/julia-library/overlay/src/PACKAGE.jl.in b/archetypes/julia-library/overlay/src/PACKAGE.jl.in deleted file mode 100644 index 3084a24..0000000 --- a/archetypes/julia-library/overlay/src/PACKAGE.jl.in +++ /dev/null @@ -1,15 +0,0 @@ -module {{PROJECT_NAME}} - -# {{PROJECT_NAME}} — {{PROJECT_DESCRIPTION}} -# -# Forged from the hyperpolymath julia-library archetype ({{CURRENT_YEAR}}). -# Provenance: MINT-PROVENANCE.a2ml at the repo root. -# -# The public API is whatever is `export`ed below; Aqua's exports/ambiguity -# checks gate it from the first mint, so keep exports deliberate. - -# --- public API ---------------------------------------------------------- - -# --- implementation ------------------------------------------------------ - -end # module {{PROJECT_NAME}} diff --git a/archetypes/julia-library/overlay/test/cases/smoke.jl b/archetypes/julia-library/overlay/test/cases/smoke.jl deleted file mode 100644 index e3cddcb..0000000 --- a/archetypes/julia-library/overlay/test/cases/smoke.jl +++ /dev/null @@ -1,11 +0,0 @@ -# smoke.jl — the archetype's load-check case. -# -# This case proves the package LOADS (module resolution, no parse/load -# errors, top-level evaluation clean). It is deliberately not a behaviour -# test: a repo whose only case is this file has a NECESSARY-but-not- -# sufficient green. Replace it with real behaviour tests as they land — -# do not delete it (the Aqua suite already covers shape; this covers load). - -@testset "smoke — package loads" begin - @test @isdefined {{PROJECT_NAME}} -end diff --git a/archetypes/julia-library/overlay/test/runtests.jl b/archetypes/julia-library/overlay/test/runtests.jl deleted file mode 100644 index a8495e1..0000000 --- a/archetypes/julia-library/overlay/test/runtests.jl +++ /dev/null @@ -1,31 +0,0 @@ -# {{PROJECT_NAME}} — test entry point (julia-library archetype) -# -# Two suites, in this order: -# 1. Aqua package-shape gate — runs FIRST: a package that does not load -# cleanly, whose deps are not compat-closed, or whose exports are -# ambiguous is rejected before any behaviour test can pass. -# 2. Behaviour — every test/cases/*.jl is included. The shipped case -# (smoke.jl) is a LOAD check only; replace/extend it as real -# behaviour tests land. Until then, `Pkg.test()` passing is necessary -# but NOT sufficient — record that in STATE.a2ml (smoke-only suite). - -using Test -using {{PROJECT_NAME}} - -@testset "{{PROJECT_NAME}} — Aqua package shape" begin - using Aqua - Aqua.test_all({{PROJECT_NAME}}) -end - -@testset "{{PROJECT_NAME}} — behaviour" begin - # NOTE: no `continue`/`break` inside a `for` inside @testset — Test's - # loop-form detection re-parses the loop body at top level, where - # `continue` is a ParseError. Filter with a guarded call instead. - # (Measured trap, 2026-09-19 — see the Julia testing guide.) - cases = joinpath(@__DIR__, "cases") - if isdir(cases) - for path in sort(readdir(cases, join = true)) - endswith(path, ".jl") && include(path) - end - end -end diff --git a/benches/baseline.json b/benches/baseline.json new file mode 100644 index 0000000..8d31d6a --- /dev/null +++ b/benches/baseline.json @@ -0,0 +1 @@ +{"bench_schema_version":1,"kind":"proven-bench-baseline","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.","schema_version":1,"sources":[{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674333621"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674333622"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674333623"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674333624"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674336581"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674336582"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674336583"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674336584"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674337391"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674337392"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674337393"},{"commit":"89d8f68f2f990fa6055e2a40646c1ce3952dfdd0","run_id":"1309674337394"}],"statistic":"leave-one-out ratio: a workload median divided by the geometric mean of the other workloads medians in the same run","window":12,"workloads":{"escape_hatches":{"iterations":5,"mean_loo_ratio":6.823368787019621,"mean_median_ns":208714204.91666666},"ijson_jcs":{"iterations":5,"mean_loo_ratio":5.437949558375433,"mean_median_ns":176391924.41666666},"required_contexts":{"iterations":5,"mean_loo_ratio":0.16495587022425925,"mean_median_ns":12812400.25},"root_shape":{"iterations":5,"mean_loo_ratio":0.163910241446135,"mean_median_ns":12782074.083333334}}} diff --git a/benches/template_bench.sh b/benches/template_bench.sh index 5fb58a8..609b3e7 100755 --- a/benches/template_bench.sh +++ b/benches/template_bench.sh @@ -1,227 +1,140 @@ -#!/bin/bash +#!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # -# Template Benchmarks -# Measures performance characteristics of template validation and build system - +# Template Benchmarks (proven-tests-and-benches JSON v1 compatible) +# +# Usage: +# benches/template_bench.sh [--json | --csv] [REPO_ROOT] +# benches/template_bench.sh [REPO_ROOT] [human | json | csv] +# +# Exit contract (proven-tests-and-benches 3-outcome convention): +# 0 = all workloads succeeded (checksum == true for every workload) +# 1 = one or more benchmarked workloads failed (a broken workload is never timed as clean) +# 2 = setup error (missing repo root or required benchmark script) set -euo pipefail -REPO_ROOT="${1:-.}" -OUTPUT_FORMAT="${2:-human}" # human | json | csv - -# ANSI colors -BLUE='\033[0;34m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -log_info() { - echo -e "${BLUE}→${NC} $*" -} - -log_pass() { - echo -e "${GREEN}✓${NC} $*" -} - -# Ensure we have required commands -command -v /usr/bin/time >/dev/null 2>&1 || { - echo "Warning: /usr/bin/time not available, using built-in time" - TIME_CMD="time" -} - -TIME_CMD="/usr/bin/time -f %e" 2>/dev/null || TIME_CMD="time" - -echo "" -echo "═══════════════════════════════════════════════════════════════════════════════" -echo "Template Benchmarks" -echo "═══════════════════════════════════════════════════════════════════════════════" -echo "" - -declare -A results +OUTPUT_FORMAT="human" +REPO_ROOT="." -#============================================================================== -# BENCHMARK 1: Template Validation -#============================================================================== - -log_info "Running template validation benchmark" - -# Warm-up run -if [ -f "$REPO_ROOT/scripts/validate-template.sh" ]; then - bash "$REPO_ROOT/scripts/validate-template.sh" "$REPO_ROOT" 0 > /dev/null 2>&1 || true -fi - -# Timed runs -BENCH_RUNS=3 -TOTAL_TIME=0 - -for i in $(seq 1 $BENCH_RUNS); do - START=$(date +%s%N) - bash "$REPO_ROOT/scripts/validate-template.sh" "$REPO_ROOT" 0 > /dev/null 2>&1 || true - END=$(date +%s%N) - - # Convert to milliseconds - RUN_TIME=$(( (END - START) / 1000000 )) - TOTAL_TIME=$(( TOTAL_TIME + RUN_TIME )) - - [ "$OUTPUT_FORMAT" = "human" ] && echo " Run $i: ${RUN_TIME}ms" +while [ "$#" -gt 0 ]; do + case "$1" in + --json) OUTPUT_FORMAT="json"; shift ;; + --csv) OUTPUT_FORMAT="csv"; shift ;; + human|json|csv) OUTPUT_FORMAT="$1"; shift ;; + *) REPO_ROOT="$1"; shift ;; + esac done -AVG_VALIDATION_TIME=$(( TOTAL_TIME / BENCH_RUNS )) -results[validation]=$AVG_VALIDATION_TIME -log_pass "Validation: ${AVG_VALIDATION_TIME}ms average (${BENCH_RUNS} runs)" - -#============================================================================== -# BENCHMARK 2: Zig Build -#============================================================================== - -log_info "Running Zig build benchmark" - -if ! command -v zig &> /dev/null; then - echo " ⚠ Zig compiler not found - skipping Zig build benchmark" - results[zig_build]="skipped" -else - cd "$REPO_ROOT/src/interface/ffi" - - # Warm-up - zig build --summary off > /dev/null 2>&1 || true - - # Clean build - BENCH_RUNS=2 - TOTAL_TIME=0 +[ -d "$REPO_ROOT" ] || { echo "template_bench: repo root '$REPO_ROOT' does not exist" >&2; exit 2; } +REPO_ROOT="$(cd "$REPO_ROOT" && pwd)" - for i in $(seq 1 $BENCH_RUNS); do - rm -rf zig-cache - - START=$(date +%s%N) - zig build --summary off > /dev/null 2>&1 || true - END=$(date +%s%N) - - RUN_TIME=$(( (END - START) / 1000000 )) - TOTAL_TIME=$(( TOTAL_TIME + RUN_TIME )) - - [ "$OUTPUT_FORMAT" = "human" ] && echo " Run $i: ${RUN_TIME}ms" - done - - AVG_BUILD_TIME=$(( TOTAL_TIME / BENCH_RUNS )) - results[zig_build]=$AVG_BUILD_TIME - log_pass "Zig build: ${AVG_BUILD_TIME}ms average (clean build, ${BENCH_RUNS} runs)" - - cd - > /dev/null -fi - -#============================================================================== -# BENCHMARK 3: Zig Tests -#============================================================================== - -log_info "Running Zig test benchmark" - -if ! command -v zig &> /dev/null; then - echo " ⚠ Zig compiler not found - skipping Zig test benchmark" - results[zig_test]="skipped" -else - cd "$REPO_ROOT/src/interface/ffi" - - # Warm-up - zig build test --summary off > /dev/null 2>&1 || true - - START=$(date +%s%N) - TEST_OUTPUT=$(zig build test --summary off 2>&1 || true) - END=$(date +%s%N) - - TEST_TIME=$(( (END - START) / 1000000 )) - results[zig_test]=$TEST_TIME - log_pass "Zig tests: ${TEST_TIME}ms" - - # Count tests - TEST_COUNT=$(echo "$TEST_OUTPUT" | grep -c "^test " || echo "unknown") - echo " Test count: $TEST_COUNT" - - cd - > /dev/null -fi - -#============================================================================== -# BENCHMARK 4: Workflow Validation -#============================================================================== - -log_info "Running workflow validation benchmark" - -if [ -f "$REPO_ROOT/tests/workflows/validate_workflows_test.sh" ]; then - START=$(date +%s%N) - bash "$REPO_ROOT/tests/workflows/validate_workflows_test.sh" "$REPO_ROOT/.github/workflows" > /dev/null 2>&1 || true - END=$(date +%s%N) - - WORKFLOW_TIME=$(( (END - START) / 1000000 )) - results[workflow_validation]=$WORKFLOW_TIME - log_pass "Workflow validation: ${WORKFLOW_TIME}ms" -fi +for req in \ + "$REPO_ROOT/scripts/check-root-shape.sh" \ + "$REPO_ROOT/scripts/check-ijson-jcs.sh" \ + "$REPO_ROOT/scripts/check-required-contexts.sh" \ + "$REPO_ROOT/scripts/check-escape-hatches.sh" +do + [ -f "$req" ] || { echo "template_bench: required workload script missing: $req" >&2; exit 2; } +done -#============================================================================== -# BENCHMARK 5: Template Instantiation -#============================================================================== +# Print a message only in human output mode. +log_human() { + if [ "$OUTPUT_FORMAT" = "human" ]; then + printf '%s\n' "$*" + fi +} -log_info "Running template instantiation benchmark" +# median_of ... -> integer median on stdout +# Requires at least one integer sample; for even counts, truncate the average +# of the two middle samples toward zero. +median_of() { + local sorted count mid + mapfile -t sorted < <(printf '%s\n' "$@" | sort -n) + count="${#sorted[@]}" + mid=$(( count / 2 )) + if [ $(( count % 2 )) -eq 1 ]; then + printf '%s' "${sorted[$mid]}" + else + printf '%s' $(( (sorted[mid - 1] + sorted[mid]) / 2 )) + fi +} -if [ -f "$REPO_ROOT/tests/e2e/template_instantiation_test.sh" ]; then - START=$(date +%s%N) - bash "$REPO_ROOT/tests/e2e/template_instantiation_test.sh" "$REPO_ROOT" > /dev/null 2>&1 || true - END=$(date +%s%N) +declare -A MEDIAN_NS +declare -A ITERS +declare -A CHECKSUM +WORKLOADS=(escape_hatches ijson_jcs required_contexts root_shape) +ANY_FAILED=0 + +# run_workload NAME ITERATIONS COMMAND... — run one warm-up and ITERATIONS timed +# samples, recording median nanoseconds, iterations, and checksum by NAME. +# Any failed invocation sets ANY_FAILED and a false checksum; sampling continues. +run_workload() { + local name="$1" iterations="$2" + shift 2 + local samples=() ok="true" i start end elapsed + + # Warm-up run + if ! "$@" >/dev/null 2>&1; then + ok="false" + ANY_FAILED=1 + fi + + for i in $(seq 1 "$iterations"); do + start="$(date +%s%N)" + if ! "$@" >/dev/null 2>&1; then + ok="false" + ANY_FAILED=1 + fi + end="$(date +%s%N)" + elapsed=$(( end - start )) + [ "$elapsed" -gt 0 ] || elapsed=1 + samples+=("$elapsed") + log_human " [$name] sample $i/$iterations: $(( elapsed / 1000000 )) ms" + done + + MEDIAN_NS["$name"]="$(median_of "${samples[@]}")" + ITERS["$name"]="$iterations" + CHECKSUM["$name"]="$ok" +} - INSTANTIATION_TIME=$(( (END - START) / 1000000 )) - results[instantiation]=$INSTANTIATION_TIME - log_pass "Template instantiation: ${INSTANTIATION_TIME}ms" -fi +log_human "Template Benchmarks (proven-bench v1)" +log_human "=====================================" -#============================================================================== -# SUMMARY -#============================================================================== +run_workload escape_hatches 5 bash "$REPO_ROOT/scripts/check-escape-hatches.sh" \ + "$REPO_ROOT/src" "$REPO_ROOT/verification" "$REPO_ROOT/.machine_readable/coaptation" +# Clear any caller-supplied IJSON_JCS binary override so the benchmark always +# measures the same deterministic JS RFC 7493/8785 workload calibrated in +# benches/baseline.json (production gates still honour IJSON_JCS on PATH). +run_workload ijson_jcs 5 env -u IJSON_JCS IJSON_JCS_FORCE_JS=1 bash "$REPO_ROOT/scripts/check-ijson-jcs.sh" --enforce "$REPO_ROOT" +run_workload required_contexts 5 bash "$REPO_ROOT/scripts/check-required-contexts.sh" "$REPO_ROOT" +run_workload root_shape 5 bash "$REPO_ROOT/scripts/check-root-shape.sh" "$REPO_ROOT" -echo "" -echo "═══════════════════════════════════════════════════════════════════════════════" -echo "BENCHMARK RESULTS" -echo "═══════════════════════════════════════════════════════════════════════════════" -echo "" +COMMIT_SHA="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || printf 'unknown')" if [ "$OUTPUT_FORMAT" = "json" ]; then - echo "{" - echo " \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"," - echo " \"repo\": \"$REPO_ROOT\"," - echo " \"results\": {" - - count=0 - for key in "${!results[@]}"; do - value="${results[$key]}" - [ $count -gt 0 ] && echo "," - if [ "$value" = "skipped" ]; then - echo -n " \"$key\": \"skipped\"" - else - echo -n " \"$key\": $value" - fi - count=$((count + 1)) - done - echo "" - echo " }" - echo "}" + # Emit strict RFC 7493 / RFC 8785 (JCS) JSON v1 document for proven-bench tools. + printf '{"commit":"%s","results":[' "$COMMIT_SHA" + first=1 + for w in "${WORKLOADS[@]}"; do + [ "$first" -eq 1 ] || printf ',' + first=0 + printf '{"checksum":%s,"iterations":%d,"median_ns":%d,"name":"%s"}' \ + "${CHECKSUM[$w]}" "${ITERS[$w]}" "${MEDIAN_NS[$w]}" "$w" + done + printf '],"schema_version":1,"suite":"proven-bench"}\n' elif [ "$OUTPUT_FORMAT" = "csv" ]; then - echo "metric,value_ms,timestamp" - for key in "${!results[@]}"; do - value="${results[$key]}" - if [ "$value" != "skipped" ]; then - echo "$key,$value,$(date -u +%Y-%m-%dT%H:%M:%SZ)" - fi - done + printf 'workload,iterations,median_ns,checksum\n' + for w in "${WORKLOADS[@]}"; do + printf '%s,%d,%d,%s\n' "$w" "${ITERS[$w]}" "${MEDIAN_NS[$w]}" "${CHECKSUM[$w]}" + done else - # Human-readable format - for key in "${!results[@]}"; do - value="${results[$key]}" - if [ "$value" = "skipped" ]; then - printf " %-30s %s\n" "$key:" "SKIPPED" - else - printf " %-30s %5d ms\n" "$key:" "$value" - fi - done + log_human "" + for w in "${WORKLOADS[@]}"; do + printf ' %-20s %8d ms (median of %d, checksum=%s)\n' \ + "$w" "$(( MEDIAN_NS[$w] / 1000000 ))" "${ITERS[$w]}" "${CHECKSUM[$w]}" + done fi -echo "" -echo "Benchmark complete." -echo "" +[ "$ANY_FAILED" -eq 0 ] || exit 1 +exit 0 diff --git a/build/channels.scm b/build/channels.scm new file mode 100644 index 0000000..54ac61a --- /dev/null +++ b/build/channels.scm @@ -0,0 +1,18 @@ +;; SPDX-License-Identifier: MPL-2.0 +;; channels.scm — the Guix revision this repository's guix.scm and manifest.scm +;; were verified against. Reproduce that exact Guix with: +;; +;; guix time-machine -C channels.scm -- shell -m manifest.scm +;; +;; Refresh it (after re-verifying) with: just toolchain-refresh +;; Canon pin, verified 2026-09-30 (docker.io/metacall/guix, guix describe). +(list (channel + (name 'guix) + (url "https://codeberg.org/guix/guix.git") + (branch "master") + (commit "ae77aeb9543de2661d739104bc4d3803d8c6f38b") + (introduction + (make-channel-introduction + "9edb3f66fd807b096b48283debdcddccfea34bad" + (openpgp-fingerprint + "BBB0 2DDF 2CEA F6A8 0D1D E643 A2A0 6DF2 A33A 54FA"))))) diff --git a/build/container/Containerfile b/build/container/Containerfile deleted file mode 100644 index 6dbff80..0000000 --- a/build/container/Containerfile +++ /dev/null @@ -1,152 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# {{PROJECT_NAME}} Container Image -# -# Multi-stage build template for Chainguard Wolfi base images. -# Customise the builder stage for your language and copy the -# resulting binary/release into the minimal runtime stage. -# -# This is the Tier-A artifact: a plain OCI image, buildable by ANY -# OCI-compliant engine — podman (recommended), nerdctl, or docker. -# It carries no hard dependency on the stapeln tier; the cerro-torre / -# svalinn files under build/container/stapeln/ are referenced only as labels. -# -# Build (engine-agnostic — substitute podman | nerdctl | docker): -# podman build -t {{SERVICE_NAME}}:latest -f build/container/Containerfile . -# -# Run: -# podman run -p {{PORT}}:{{PORT}} {{SERVICE_NAME}}:latest -# -# Run with persistent volume: -# podman run -p {{PORT}}:{{PORT}} -v {{SERVICE_NAME}}-data:/data {{SERVICE_NAME}}:latest -# -# Via the Justfile (auto-detects engine, prefers cerro-torre when present): -# just container-build - -# ============================================================================ -# Stage 1: Builder -# ============================================================================ -# -# Install build tools and compile the application. -# This stage is discarded after the build — only the compiled output -# is copied into the runtime stage. -# -# Language-specific examples (uncomment the one you need): -# -# --- Rust --- -# RUN apk add --no-cache rust pkgconf build-base -# COPY Cargo.toml Cargo.lock ./ -# COPY src/ ./src/ -# RUN cargo build --release -# # Output: /build/target/release/{{SERVICE_NAME}} -# -# --- Elixir --- -# RUN apk add --no-cache erl27-elixir-1.18 erlang-27 erlang-27-dev git build-base -# COPY mix.exs mix.lock ./ -# COPY lib/ ./lib/ -# COPY config/ ./config/ -# ENV MIX_ENV=prod -# RUN mix local.hex --force && mix local.rebar --force && \ -# mix deps.get --only prod && mix compile && mix release -# # Output: /build/_build/prod/rel/{{SERVICE_NAME}}/ -# -# --- Zig --- -# RUN apk add --no-cache zig build-base -# COPY build.zig build.zig.zon ./ -# COPY src/ ./src/ -# RUN zig build -Doptimize=ReleaseFast -# # Output: /build/zig-out/bin/{{SERVICE_NAME}} -# -# Base image: `:latest` is right for a template scaffold (Chainguard rebuilds -# Wolfi daily). For PRODUCTION builds, pin by digest for reproducibility/supply- -# chain integrity, e.g.: -# FROM cgr.dev/chainguard/wolfi-base:latest@sha256: AS builder -# (cerro-torre's .ctp signing in build/container/stapeln/ records the resolved digest.) -FROM cgr.dev/chainguard/wolfi-base:latest AS builder - -# TODO: Install your language toolchain -RUN apk add --no-cache build-base - -WORKDIR /build - -# TODO: Copy source files and build -COPY . . -# RUN - -# ============================================================================ -# Stage 2: Runtime -# ============================================================================ -# -# Minimal production image. Only the compiled binary/release and runtime -# dependencies are included. No compilers, no source code, no build tools. -# -FROM cgr.dev/chainguard/wolfi-base:latest - -# OCI image labels (compatible with cerro-torre .ctp bundle metadata) -LABEL org.opencontainers.image.title="{{PROJECT_NAME}}" \ - org.opencontainers.image.description="{{PROJECT_DESCRIPTION}}" \ - org.opencontainers.image.url="https://{{FORGE}}/{{OWNER}}/{{REPO}}" \ - org.opencontainers.image.source="https://{{FORGE}}/{{OWNER}}/{{REPO}}" \ - org.opencontainers.image.vendor="{{OWNER}}" \ - org.opencontainers.image.licenses="{{LICENSE}}" \ - org.opencontainers.image.authors="{{AUTHOR}} <{{AUTHOR_EMAIL}}>" \ - dev.cerrotorre.manifest="build/container/stapeln/manifest.toml" \ - dev.cerrotorre.gatekeeper="build/container/stapeln/.gatekeeper.yaml" \ - dev.stapeln.compose="build/container/stapeln/compose.toml" - -# Install minimal runtime dependencies. -# Adjust this list for your application: -# - ca-certificates: TLS root certificates -# - curl: health check probe -# - libstdc++: C++ standard library (if needed by native deps) -# - ncurses: terminal UI (if needed, e.g. Elixir IEx) -RUN apk add --no-cache ca-certificates curl - -# Create non-root user for the application. -# Running as root inside containers is a security anti-pattern. -RUN addgroup -S appuser && adduser -S appuser -G appuser - -WORKDIR /app - -# TODO: Copy compiled binary/release from builder stage. -# Examples: -# COPY --from=builder /build/target/release/{{SERVICE_NAME}} /app/{{SERVICE_NAME}} -# COPY --from=builder /build/_build/prod/rel/{{SERVICE_NAME}} /app/release/ -# COPY --from=builder /build/zig-out/bin/{{SERVICE_NAME}} /app/{{SERVICE_NAME}} - -# Copy entrypoint script -COPY build/container/entrypoint.sh /app/entrypoint.sh -RUN chmod +x /app/entrypoint.sh - -# OPTIONAL (Tier C — stapeln). A pure OCI image does NOT need these baked in: -# svalinn reads .gatekeeper.yaml from a mounted volume (see stapeln/compose.toml), -# and cerro-torre reads manifest.toml at pack time. Uncomment only if you want -# the policy/manifest embedded in the image itself. -# COPY build/container/stapeln/.gatekeeper.yaml /etc/svalinn/gatekeeper.yaml -# COPY build/container/stapeln/manifest.toml /app/manifest.toml - -# Create data directory for persistent storage (mountable volume) -RUN mkdir -p /data && chown appuser:appuser /data - -# Set ownership of the application directory -RUN chown -R appuser:appuser /app - -# Environment variables — customise for your application -ENV APP_HOST=[::] -ENV APP_PORT={{PORT}} -ENV APP_LOG_FORMAT=json -ENV APP_DATA_DIR=/data - -# Declare /data as a volume for persistent storage -VOLUME ["/data"] - -# Run as non-root -USER appuser - -# Expose the application port -EXPOSE {{PORT}} - -# Health check — the application must respond 2xx at /health -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD curl -sf http://localhost:${APP_PORT}/health || exit 1 - -ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/build/container/README.adoc b/build/container/README.adoc index 3808b62..1b8b068 100644 --- a/build/container/README.adoc +++ b/build/container/README.adoc @@ -1,12 +1,32 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -= {{PROJECT_NAME}} Containerisation += occupancy-types Containerisation :toc: left :toclevels: 3 :sectnums: == Overview +[TIP] +==== +*This module ships no `Containerfile`.* It used to ship one, and that file had +two `FROM …:latest` lines and a body of TODO comments: it could not build, and +it failed `container-images-pinned` in every repository minted from this +template — including the ones that publish no image at all. + +A repository that publishes an image adds the file here, pinned: + +[source,bash] +---- +scripts/pin-container-digests.sh --root . --base --runtime \ + build/container/Containerfile +---- + +`scripts/check-container-pins.sh` fails the build until every `FROM` in it +carries an `@sha256:` digest, and `just container-build` says the same thing +rather than building an image from two moving tags. +==== + Containerisation in this template is organised in *three tiers*, from the most portable to the most sovereign. You can use only the tier you need and ignore — or delete — the rest. @@ -16,7 +36,7 @@ or delete — the rest. | Tier | What | Why | *A — OCI standard* -| `Containerfile`, `.containerignore` +| `Containerfile` (you add it — see above), `.containerignore` | A plain, multi-stage OCI image on https://www.chainguard.dev/[Chainguard] Wolfi. Builds with any OCI engine. No dependency on tiers B or C. @@ -102,9 +122,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 @@ -138,7 +160,7 @@ The sovereign tier wires together the stapeln container stack: (Kennel / Yard / Hunt). The request path in the full stack is: -`svalinn` (edge) → `rokur` (secrets gate) → `{{SERVICE_NAME}}` (app). +`svalinn` (edge) → `rokur` (secrets gate) → `occupancy-types` (app). == Initialise @@ -174,14 +196,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`. @@ -202,14 +236,14 @@ To enable it, set a repository (or organisation) variable: | `CONTAINER_RUNNER` | Optional JSON array of runner labels. Defaults to `["self-hosted","owned","container"]` — owned compute, so **no metered - GitHub Actions minutes**. Set e.g. `["ubuntu-latest"]` to use GitHub-hosted + GitHub Actions minutes**. Set e.g. `["ubuntu-24.04"]` to use GitHub-hosted runners instead. |=== [source,bash] ---- gh variable set CONTAINER_CI --body true -# optional: gh variable set CONTAINER_RUNNER --body '["ubuntu-latest"]' +# optional: gh variable set CONTAINER_RUNNER --body '["ubuntu-24.04"]' ---- The owned runner is expected to have `just` and a container engine installed; 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 4b5087f..ec94bf7 100644 --- a/build/container/compose.yaml +++ b/build/container/compose.yaml @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 # -# {{PROJECT_NAME}} — portable compose stack (Tier B) +# occupancy-types — portable compose stack (Tier B) # # This is the runtime-agnostic, OCI compose-spec file. It works unchanged with: # podman compose -f build/container/compose.yaml up -d @@ -16,84 +16,119 @@ # # See https://compose-spec.io for the full specification. -services: - # ────────────────────────────────────────────────────────────────────── - # Primary application service - # ────────────────────────────────────────────────────────────────────── - "{{SERVICE_NAME}}": - build: - context: .. - dockerfile: build/container/Containerfile - image: "{{REGISTRY}}/{{SERVICE_NAME}}:latest" - ports: - - "{{PORT}}:{{PORT}}" - environment: - APP_HOST: "[::]" - APP_PORT: "{{PORT}}" - APP_LOG_FORMAT: "json" - APP_DATA_DIR: "/data" - volumes: - - "{{SERVICE_NAME}}-data:/data" - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:{{PORT}}/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://{{SERVICE_NAME}}:{{PORT}}" - 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: - "{{SERVICE_NAME}}": - 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: - "{{SERVICE_NAME}}-data": - rokur-audit: +{ + services: { + # ────────────────────────────────────────────────────────────────────── + # Primary application service + # ────────────────────────────────────────────────────────────────────── + occupancy-types: { + build: { + context: "..", + dockerfile: "build/container/Containerfile", + }, + image: "ghcr.io/hyperpolymath/occupancy-types:latest", + ports: [ + "8080:8080", + ], + environment: { + APP_HOST: "[::]", + APP_PORT: "8080", + APP_LOG_FORMAT: "json", + APP_DATA_DIR: "/data", + }, + volumes: [ + "occupancy-types-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://occupancy-types: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: { + occupancy-types: { + 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: { + occupancy-types-data: null, + rokur-audit: null, + }, +} diff --git a/build/container/entrypoint.sh b/build/container/entrypoint.sh index a7a0369..4cea667 100755 --- a/build/container/entrypoint.sh +++ b/build/container/entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/sh # SPDX-License-Identifier: MPL-2.0 -# {{PROJECT_NAME}} container entrypoint +# occupancy-types container entrypoint # # Handles signal propagation, startup logging, and health check # preparation before exec-ing into the main application process. @@ -15,7 +15,7 @@ set -e # when Podman sends stop signals (e.g. `podman stop`, `selur-compose down`). cleanup() { - echo "Received shutdown signal — stopping {{SERVICE_NAME}}..." + echo "Received shutdown signal — stopping occupancy-types..." # If the main process is backgrounded, kill it here: # kill "$MAIN_PID" 2>/dev/null || true # wait "$MAIN_PID" 2>/dev/null || true @@ -27,9 +27,9 @@ trap cleanup TERM INT # Startup logging # --------------------------------------------------------------------------- -echo "Starting {{SERVICE_NAME}}..." +echo "Starting occupancy-types..." echo " Host: ${APP_HOST:-[::]}" -echo " Port: ${APP_PORT:-{{PORT}}}" +echo " Port: ${APP_PORT:-8080}" echo " Data: ${APP_DATA_DIR:-/data}" echo " Log: ${APP_LOG_FORMAT:-json}" @@ -56,8 +56,8 @@ fi # # TODO: Replace the command below with your application binary. # Examples: -# exec /app/{{SERVICE_NAME}} -# exec /app/release/bin/{{SERVICE_NAME}} start -# exec /app/{{SERVICE_NAME}} serve --host "${APP_HOST}" --port "${APP_PORT}" +# exec /app/occupancy-types +# exec /app/release/bin/occupancy-types start +# exec /app/occupancy-types serve --host "${APP_HOST}" --port "${APP_PORT}" exec "$@" diff --git a/build/container/stapeln/.gatekeeper.yaml b/build/container/stapeln/.gatekeeper.yaml index 4aac671..b53a6a3 100644 --- a/build/container/stapeln/.gatekeeper.yaml +++ b/build/container/stapeln/.gatekeeper.yaml @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 # -# Svalinn gatekeeper policy for {{PROJECT_NAME}} +# Svalinn gatekeeper policy for occupancy-types # # Controls which operations are permitted through the edge gateway. # This template provides moderate security defaults — not wide-open test @@ -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: "{{SERVICE_NAME}}-release" - algorithm: "Ed25519" - public_key_file: "/etc/svalinn/keys/{{SERVICE_NAME}}-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: "occupancy-types-release", + algorithm: "Ed25519", + public_key_file: "/etc/svalinn/keys/occupancy-types-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/compose.toml b/build/container/stapeln/compose.toml index db42fb9..67b549a 100644 --- a/build/container/stapeln/compose.toml +++ b/build/container/stapeln/compose.toml @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 # -# {{PROJECT_NAME}} selur-compose configuration +# occupancy-types selur-compose configuration # # Orchestrates the container stack as verified container bundles (.ctp). # Uses selur zero-copy IPC between services on the same host. @@ -10,7 +10,7 @@ # selur-compose up --detach # Start in background # selur-compose verify # Verify all .ctp signatures # selur-compose ps # Check status -# selur-compose logs -f {{SERVICE_NAME}} # Stream logs +# selur-compose logs -f occupancy-types # Stream logs # selur-compose down # Stop all services # # Fallback (when selur is not installed): this TOML is selur-native and is @@ -25,18 +25,18 @@ version = "1.0" # ============================================================================ # Primary application service -[services.{{SERVICE_NAME}}] -image = "{{REGISTRY}}/{{SERVICE_NAME}}:latest.ctp" -ports = ["{{PORT}}:{{PORT}}"] +[services.occupancy-types] +image = "ghcr.io/hyperpolymath/occupancy-types:latest.ctp" +ports = ["8080:8080"] environment = { APP_HOST = "[::]", - APP_PORT = "{{PORT}}", + APP_PORT = "8080", APP_LOG_FORMAT = "json", APP_DATA_DIR = "/data", } -volumes = ["{{SERVICE_NAME}}-data:/data"] +volumes = ["occupancy-types-data:/data"] restart = "always" -healthcheck = { test = "curl -sf http://localhost:{{PORT}}/health", interval = "30s", timeout = "5s", retries = 3 } +healthcheck = { test = "curl -sf http://localhost:8080/health", interval = "30s", timeout = "5s", retries = 3 } # Rokur secrets gate: refuses to start (and refuses to proxy) unless the # declared secrets are present; emits a structured audit log of every access. @@ -45,14 +45,14 @@ healthcheck = { test = "curl -sf http://localhost:{{PORT}}/health", interval = " image = "ghcr.io/hyperpolymath/rokur:latest.ctp" ports = ["8081:8081"] environment = { - ROKUR_BACKEND = "http://{{SERVICE_NAME}}:{{PORT}}", + ROKUR_BACKEND = "http://occupancy-types:8080", ROKUR_LISTEN = "[::]:8081", ROKUR_REQUIRED_SECRETS = "", ROKUR_AUDIT_LOG = "/var/log/rokur/audit.jsonl", ROKUR_LOG_FORMAT = "json", } volumes = ["rokur-audit:/var/log/rokur"] -depends_on = ["{{SERVICE_NAME}}"] +depends_on = ["occupancy-types"] restart = "always" healthcheck = { test = "curl -sf http://localhost:8081/health", interval = "30s", timeout = "5s", retries = 3 } @@ -75,7 +75,7 @@ healthcheck = { test = "curl -sf http://localhost:80/health", interval = "30s", # Volumes # ============================================================================ -[volumes.{{SERVICE_NAME}}-data] +[volumes.occupancy-types-data] driver = "local" [volumes.rokur-audit] diff --git a/build/container/stapeln/ct-build.sh b/build/container/stapeln/ct-build.sh index c825b84..19a358e 100755 --- a/build/container/stapeln/ct-build.sh +++ b/build/container/stapeln/ct-build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 # -# {{PROJECT_NAME}} — Cerro Torre build, sign, and verify pipeline +# occupancy-types — Cerro Torre build, sign, and verify pipeline # # Builds the container image, packages it as a verified .ctp bundle, # signs it with Ed25519, and verifies the result. Gracefully degrades @@ -18,8 +18,8 @@ # CT_KEY_ID=my-key ./ct-build.sh # Use specific signing key # # Environment variables: -# CT_KEY_ID — Signing key identifier (default: {{SERVICE_NAME}}-release) -# CT_REGISTRY — OCI registry to push to (default: {{REGISTRY}}) +# CT_KEY_ID — Signing key identifier (default: occupancy-types-release) +# CT_REGISTRY — OCI registry to push to (default: ghcr.io/hyperpolymath) # CT_TAG — Image tag (default: latest) set -euo pipefail @@ -44,15 +44,15 @@ for arg in "$@"; do fi done -CT_KEY_ID="${CT_KEY_ID:-{{SERVICE_NAME}}-release}" -CT_REGISTRY="${CT_REGISTRY:-{{REGISTRY}}}" +CT_KEY_ID="${CT_KEY_ID:-occupancy-types-release}" +CT_REGISTRY="${CT_REGISTRY:-ghcr.io/hyperpolymath}" CT_TAG="${CT_TAG:-latest}" -IMAGE_NAME="{{SERVICE_NAME}}" +IMAGE_NAME="occupancy-types" FULL_IMAGE="${CT_REGISTRY}/${IMAGE_NAME}:${CT_TAG}" CTP_FILE="${SCRIPT_DIR}/${IMAGE_NAME}-${CT_TAG}.ctp" -echo "=== {{PROJECT_NAME}} Cerro Torre Build Pipeline ===" +echo "=== occupancy-types Cerro Torre Build Pipeline ===" echo " Image: ${FULL_IMAGE}" echo " Key: ${CT_KEY_ID}" echo " Bundle: ${CTP_FILE}" diff --git a/build/container/stapeln/deploy.k9.ncl b/build/container/stapeln/deploy.k9.ncl deleted file mode 100644 index 7282738..0000000 --- a/build/container/stapeln/deploy.k9.ncl +++ /dev/null @@ -1,170 +0,0 @@ -K9! -# SPDX-License-Identifier: MPL-2.0 -# deploy.k9.ncl — {{PROJECT_NAME}} deployment component (Hunt level) -# -# k9-svc deployment specification with full pedigree (L1-L5). -# Security Level: 'Hunt (requires cryptographic handshake for execution). -# -# WARNING: This component can execute shell commands! -# It requires explicit authorisation via the Leash system. -# -# 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 - -# The component's pedigree (self-description across five layers) -let component_pedigree = { - # ───────────────────────────────────────────────────────────── - # L1: The Snout — Identity - # ───────────────────────────────────────────────────────────── - metadata = { - name = "{{SERVICE_NAME}}-deploy", - version = "{{VERSION}}", - breed = "application/vnd.k9+nickel", - magic_number = "K9!", - description = "{{PROJECT_NAME}} deployment component (Hunt level)", - }, - - # ───────────────────────────────────────────────────────────── - # L2: The Scent — Target Environment - # ───────────────────────────────────────────────────────────── - target = { - os = 'Linux, - is_edge = false, - requires_podman = true, - min_memory_mb = 256, - }, - - # ───────────────────────────────────────────────────────────── - # 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", - }, - - # ───────────────────────────────────────────────────────────── - # L4: The Gut — Self-Validation - # ───────────────────────────────────────────────────────────── - validation = { - checksum = "sha256:placeholder", - pedigree_version = "1.0.0", - hunt_authorized = false, # Must be set true after handshake - }, - - # ───────────────────────────────────────────────────────────── - # 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", - }, -} 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", - }, - production = { - replicas = 3, - memory = "1Gi", - cpu = "500m", - image_tag = "latest", - }, - }, - - # Container configuration - container = { - image = "{{REGISTRY}}/{{SERVICE_NAME}}", - port = {{PORT}}, - health_check = "/health", - readiness_check = "/ready", - }, - - # 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%" -#!/bin/sh -set -eu -echo "K9: Pre-deployment validation for {{SERVICE_NAME}}..." -cd build/container/stapeln && selur-compose verify || podman compose --file ../compose.yaml config -echo "K9: Validation passed." -"%, - - # Deployment script - deploy = m%" -#!/bin/sh -set -eu -ENV="${1:-dev}" -echo "K9: Deploying {{SERVICE_NAME}} to $ENV environment..." -cd build/container/stapeln -./ct-build.sh -selur-compose up --detach || podman compose --file ../compose.yaml up --detach -echo "K9: Deployment to $ENV complete." -"%, - - # Rollback script - rollback = m%" -#!/bin/sh -set -eu -echo "K9: Rolling back {{SERVICE_NAME}} deployment..." -cd build/container/stapeln -selur-compose down || podman compose --file ../compose.yaml down -echo "K9: Rollback complete." -"%, -} in - -# Export the component -{ - pedigree = component_pedigree & { name = "{{SERVICE_NAME}}-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 -"%, -} diff --git a/build/container/stapeln/deploy.k9.ncl.in b/build/container/stapeln/deploy.k9.ncl.in new file mode 100644 index 0000000..944fc88 --- /dev/null +++ b/build/container/stapeln/deploy.k9.ncl.in @@ -0,0 +1,189 @@ +K9! +# SPDX-License-Identifier: MPL-2.0 +# deploy.k9.ncl — occupancy-types deployment component (Hunt level) +# +# 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 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 (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 + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "deployment", + + metadata = { + name = "occupancy-types-deploy", + version = "0.1.0", + description = "occupancy-types deployment component (Hunt level)", + }, + + # Target environment (extension field; contract §5.5). + target = { + os = 'Linux, + is_edge = false, + requires_podman = true, + min_memory_mb = 256, + }, + + 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", + ], + }, + + 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/occupancy-types images", + "starts, updates and stops the occupancy-types 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", + }, + }, + + # 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", + }, + }, + + # Container configuration + container = { + image = "ghcr.io/hyperpolymath/occupancy-types", + port = 8080, + health_check = "/health", + readiness_check = "/ready", + }, + + # Deployment strategy + strategy = { + type = "rolling", + max_surge = 1, + max_unavailable = 0, + }, + }, + + # Deployment scripts (executed at Hunt level) + scripts = { + # Pre-deployment validation + pre_deploy = m%" +#!/bin/sh +set -eu +echo "K9: Pre-deployment validation for occupancy-types..." +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%" +#!/bin/sh +set -eu +ENV="${1:-dev}" +echo "K9: Deploying occupancy-types to $ENV environment..." +cd build/container/stapeln +./ct-build.sh +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%" +#!/bin/sh +set -eu +echo "K9: Rolling back occupancy-types deployment..." +cd build/container/stapeln +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." +"%, + }, + + # 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/container/stapeln/manifest.toml b/build/container/stapeln/manifest.toml index f55fcb3..45df7bf 100644 --- a/build/container/stapeln/manifest.toml +++ b/build/container/stapeln/manifest.toml @@ -1,27 +1,27 @@ # SPDX-License-Identifier: MPL-2.0 # -# Cerro Torre manifest for {{PROJECT_NAME}} .ctp bundle +# Cerro Torre manifest for occupancy-types .ctp bundle # # This manifest describes the container image for verified # container packaging. Used by `ct pack` to create .ctp bundles. [metadata] -name = "{{SERVICE_NAME}}" -version = "{{VERSION}}" +name = "occupancy-types" +version = "0.1.0" revision = 1 -summary = "{{PROJECT_DESCRIPTION}}" +summary = "Early-stage research on compositional worst-case resource bounds, with a linear session-IR checker, occupancy-indexed state experiments in Idris 2, and a prototype stack-depth certificate checker. The long-term goal is predictable memory and network-buffer usage." description = """ -{{PROJECT_NAME}} — containerised service packaged as a verified +occupancy-types — containerised service packaged as a verified cerro-torre .ctp bundle with Ed25519 signing and full provenance tracking. """ -license = "{{LICENSE}}" -homepage = "https://{{FORGE}}/{{OWNER}}/{{REPO}}" -maintainer = "{{AUTHOR}} <{{EMAIL}}>" +license = "MPL-2.0" +homepage = "https://github.com/hyperpolymath/occupancy-types" +maintainer = "Jonathan D.A. Jewell " [provenance] -upstream = "https://{{FORGE}}/{{OWNER}}/{{REPO}}" -import_date = {{CURRENT_DATE}}T00:00:00Z +upstream = "https://github.com/hyperpolymath/occupancy-types" +import_date = 2026-11-12T00:00:00Z [dependencies] runtime = ["ca-certificates", "curl"] @@ -32,10 +32,10 @@ system = "podman" [build.environment] APP_HOST = "[::]" -APP_PORT = "{{PORT}}" +APP_PORT = "8080" [outputs] -primary = "{{SERVICE_NAME}}" +primary = "occupancy-types" split = [] [attestations] @@ -54,7 +54,7 @@ drop = ["ALL"] add = ["NET_BIND_SERVICE"] [security.network] -listen_tcp = [{{PORT}}] +listen_tcp = [8080] [security.filesystem] read = ["/app/", "/data/"] diff --git a/build/container/stapeln/rokur.toml b/build/container/stapeln/rokur.toml index 361c1ea..e69aa47 100644 --- a/build/container/stapeln/rokur.toml +++ b/build/container/stapeln/rokur.toml @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 # -# Rokur secrets-gate configuration for {{PROJECT_NAME}} +# Rokur secrets-gate configuration for occupancy-types # # Rokur (stapeln container stack) is a secrets-management gate: a small # Deno service that sits in front of the application and refuses to pass @@ -16,8 +16,8 @@ # # or, in the stack, via compose (see compose.toml / compose.yaml) [metadata] -name = "{{SERVICE_NAME}}-gate" -version = "{{VERSION}}" +name = "occupancy-types-gate" +version = "0.1.0" # ============================================================================ # Listener / routing (env: ROKUR_LISTEN, ROKUR_BACKEND) @@ -27,7 +27,7 @@ version = "{{VERSION}}" # Address rokur listens on (inside the container). listen = "[::]:8081" # The upstream application rokur fronts once the gate is satisfied. -backend = "http://{{SERVICE_NAME}}:{{PORT}}" +backend = "http://occupancy-types:8080" # Health/readiness endpoints rokur exposes for vordr / orchestrators. health_path = "/health" ready_path = "/ready" diff --git a/build/container/stapeln/vordr.toml b/build/container/stapeln/vordr.toml index 90084b4..5811b1e 100644 --- a/build/container/stapeln/vordr.toml +++ b/build/container/stapeln/vordr.toml @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 # -# Vordr runtime monitoring configuration for {{PROJECT_NAME}} +# Vordr runtime monitoring configuration for occupancy-types # # Vordr watches container health, detects crashes, tracks resource usage, # and emits structured logs. It runs alongside the application stack and @@ -12,8 +12,8 @@ # vordr report [metadata] -name = "{{SERVICE_NAME}}" -version = "{{VERSION}}" +name = "occupancy-types" +version = "0.1.0" # ============================================================================ # Health Monitoring @@ -25,13 +25,13 @@ version = "{{VERSION}}" [health] # Primary health endpoint — must return 2xx. -endpoint = "http://localhost:{{PORT}}/health" +endpoint = "http://localhost:8080/health" interval = "30s" timeout = "5s" failure_threshold = 3 # Readiness endpoint — checked during startup and after restarts. -readiness_endpoint = "http://localhost:{{PORT}}/ready" +readiness_endpoint = "http://localhost:8080/ready" readiness_timeout = "10s" # Action on failure: "alert" (log + notify) or "restart" (alert + restart). @@ -96,7 +96,7 @@ format = "json" level = "info" # Write vordr logs to stdout (captured by Podman) and optionally to file. output = "stdout" -# file = "/var/log/vordr/{{SERVICE_NAME}}.log" +# file = "/var/log/vordr/occupancy-types.log" # ============================================================================ # Notifications (optional) @@ -111,7 +111,7 @@ output = "stdout" # on = ["failure", "recovery", "resource_critical"] # [notifications.email] -# to = "{{EMAIL}}" -# from = "vordr@{{SERVICE_NAME}}.local" +# to = "j.d.a.jewell@open.ac.uk" +# from = "vordr@occupancy-types.local" # smtp = "smtp://localhost:25" # on = ["failure", "resource_critical"] diff --git a/build/docs-seed/README.adoc b/build/docs-seed/README.adoc deleted file mode 100644 index c4e0266..0000000 --- a/build/docs-seed/README.adoc +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) -= Documentation Template -:revdate: 2026-05-26 -:status: SEED - -This directory is the *minimum heavy-wiki seed* for a hyperpolymath repo. - -[NOTE] -==== -*This is NOT a duplicate of `docs/`.* `docs/` holds *this* template's own, -fully-written documentation; `build/docs-seed/` is the un-filled *seed* (with -`{{PLACEHOLDER}}` tokens and illustrative example paths) that is copied into a -*newly scaffolded* repo's `docs/`. Example paths inside `build/docs-seed/` -describe a hypothetical downstream project, so they intentionally do not resolve -against this template's tree (see `.hypatia-ignore` for the matching SD022 -carve-out). -==== - -== How adoption works - -`just repo-init` does the first two steps for you. It was previously a manual -four-step procedure that nothing automated, with the result that minted repos -carried the un-filled seed *and* the spine's own rendered docs side by side. - -. **Automatic** — repo-init copies this seed over `docs/` before substitution. - It is an *overlay*: these files win on collision, and everything else the - spine ships under `docs/` survives (notably `docs/governance/` and - `docs/legal/`, which `build/just/validate.just` requires). This README is not - copied. The seed directory is then removed from your repo alongside - `archetypes/` and `build/templates/`. -. **Automatic** — `{{PROJECT_NAME}}`, `{{OWNER}}`, `{{AUTHOR}}`, - `{{AUTHOR_EMAIL}}` and `{{CURRENT_YEAR}}` are rendered by the same - substitution pass as the rest of the tree. -. **Yours to do** — delete any section that genuinely does not apply. Do not - leave empty scaffolding: the 2026-05-26 doc-debt audit counted 61 estate - repos with empty `docs/` dirs as its own category of debt. -. **Yours to do** — add repo-specific sections. - -== What's in here - -[cols="1,2", options="header"] -|=== -| File | Purpose - -| `README.adoc` | This file. NOT copied into your repo — it documents the seed itself. -| `architecture.adoc` | System overview, component diagram, data flow. -| `usage.adoc` | End-user / consumer perspective. How to invoke / consume / depend on this repo. -| `contributing.adoc` | Developer perspective. Local-dev setup, test running, PR conventions. -| `troubleshooting.adoc` | FAQ-style. Known failure modes + recovery paths. -| `decisions/0001-template.adoc` | ADR (Architecture Decision Record) template. Copy → `0002-...`, `0003-...` etc. -|=== - -== Why these five sections - -The 2026-05-26 estate documentation-debt audit -(link:https://github.com/hyperpolymath/standards/blob/main/docs/audits/2026-05-26-estate-documentation-debt.md[standards#197]) -identified four canonical perspectives every repo should answer: - -. **What is it** — README answers (lives at repo root, not here). -. **How is it built** — `architecture.adoc`. -. **How do I use it** — `usage.adoc`. -. **How do I help build it** — `contributing.adoc`. -. **What goes wrong** — `troubleshooting.adoc`. - -ADRs are the long-running historical record — they don't fit any of -the above but are essential for explaining *why* a particular shape -was chosen. - -== What's intentionally NOT in here - -This seed deliberately omits: - -* `proof-debt.md` — only relevant to proof-bearing repos; see - link:https://github.com/hyperpolymath/standards/blob/main/docs/TRUSTED-BASE-REDUCTION-POLICY.adoc[standards#203]. -* `CHANGELOG.md` — generated automatically by - link:https://github.com/hyperpolymath/standards/blob/main/.github/workflows/changelog-reusable.yml[`changelog-reusable.yml`] - (standards#206), don't write by hand. -* `tech-debt-YYYY-MM-DD.md` — generated by the periodic estate audit. - -== "Heavily-developed wiki" bar (audit definition) - -A repo meets the audit's bar when `docs/` contains ≥ 10 substantive -markdown / asciidoc / rst files with topical organisation. Adopting -this seed gets you to ~5 — add repo-specific deep-dives (typically -one per major component or invariant) to clear the bar. diff --git a/build/guix.scm b/build/guix.scm index 5a769d6..2cb2c67 100755 --- a/build/guix.scm +++ b/build/guix.scm @@ -1,20 +1,46 @@ ;; SPDX-License-Identifier: MPL-2.0 -;; Guix development environment template. -;; Usage: guix shell -D -f build/guix.scm +;; SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +;; +;; guix.scm — occupancy-types as a Guix package. +;; +;; guix build -f build/guix.scm # installs the source tree to share/occupancy-types +;; guix shell -D -f build/guix.scm # the full development toolchain +;; +;; This is a SOURCE package, and says so: a hermetic compiled build needs this +;; repository's idris2 and zig dependencies packaged in Guix, which they are not +;; (Guix builds offline). The toolchain below is real — `guix shell -D -f build/guix.scm` +;; then `just setup` gives a working environment. See docs/SETUP.adoc §Guix. +(use-modules (guix packages) (guix gexp) + (guix build-system copy) + (gnu packages) + ((guix licenses) #:prefix license:)) -(use-modules (guix packages) - (guix build-system gnu) - (guix licenses) - (gnu packages base) - (gnu packages bash)) +;; The repository root: this file sits at the root or in build/ (PROVISIONING-STANDARD §1). +(define %source-dir + (let ((d (dirname (current-filename)))) + (if (string=? (basename d) "build") (dirname d) d))) +;; A spec may name an output ("rust:cargo"); plain specification->package cannot. +(define (spec->input spec) + (call-with-values (lambda () (specification->package+output spec)) + (lambda (pkg out) (if (string=? out "out") pkg (list pkg out))))) + +(define %ignored + '(".git" "target" ".eval" "node_modules" "_build" "deps" "zig-out" ".zig-cache" "dist-newstyle")) (package - (name "rsr-template-repo") + (name "occupancy-types") (version "0.1.0") - (source #f) - (build-system gnu-build-system) - (inputs (list coreutils bash)) - (synopsis "rsr-template-repo") - (description "rsr-template-repo — part of the hyperpolymath ecosystem.") - (home-page "https://github.com/hyperpolymath/rsr-template-repo") - (license (@ (guix licenses) mpl2.0))) + (source (local-file %source-dir "occupancy-types-checkout" + #:recursive? #t + #:select? (lambda (file stat) + (not (member (basename file) %ignored))))) + (build-system copy-build-system) + (arguments (list #:install-plan #~'(("." "share/occupancy-types/")))) + (native-inputs (map spec->input (list "git" "bash" "coreutils" "nss-certs" "just" "mise" + "shellcheck" "chez-scheme" "gmp" "gcc-toolchain" "zig"))) + (home-page "https://github.com/hyperpolymath/occupancy-types") + (synopsis "Rhodium Standard Repository template") + (description "The estate's repository template: the RSR layout, an Idris2 ABI +seam with a Zig FFI, CI gates, and the provisioning set (launcher, Justfile, +mise, Guix, setup and AI-assisted installation guides).") + (license license:mpl2.0)) diff --git a/build/just/assess.just b/build/just/assess.just index 3c99031..a6a663b 100644 --- a/build/just/assess.just +++ b/build/just/assess.just @@ -43,7 +43,23 @@ self-assess: echo "── ESSENTIAL (removing breaks Rhodium Standard) ──────────" echo "" - for f in LICENSE .editorconfig .gitignore; do + # The licence may be LICENSE in any usual spelling (this template ships + # LICENSE.adoc) or a REUSE LICENSES/ directory holding a non-empty + # . licence text (dotfiles and READMEs do not count). + lic="" + for cand in LICENSE LICENSE.txt LICENSE.md LICENSE.adoc; do + [ -f "$cand" ] && { lic="$cand"; break; } + done + if [ -z "$lic" ] && [ -n "$(find LICENSES -maxdepth 1 -type f -size +0c -name '*.*' ! -name '.*' ! -iname 'README*' 2>/dev/null | head -n 1)" ]; then + lic="LICENSES/" + fi + if [ -n "$lic" ]; then + echo " ✓ $lic — KEEP (RSR required)" + else + echo " ✗ LICENSE — MISSING (RSR violation!)" + fi + + for f in .editorconfig .gitignore; do if [ -f "$f" ]; then echo " ✓ $f — KEEP (RSR required)" else @@ -101,15 +117,17 @@ self-assess: echo " ⚠ Consequence: no C-compatible FFI bridge." fi - if $HAS_API && [ -f ".machine_readable/integrations/groove.a2ml" ]; then - PORT=$(grep '(port ' .machine_readable/integrations/groove.a2ml 2>/dev/null | sed 's/.*(port \([0-9]*\)).*/\1/') + # The Groove manifest is the repo deed's (integrations (groove …)) clause. + GROOVE_DEED=$(bash scripts/deed-field.sh --find . 2>/dev/null || true) + if $HAS_API && [ -n "$GROOVE_DEED" ] && bash scripts/deed-field.sh --has "$GROOVE_DEED" integrations/groove; then + PORT=$(bash scripts/deed-field.sh "$GROOVE_DEED" integrations/groove port 2>/dev/null || true) if [ "$PORT" = "0" ]; then - echo " ⚠ groove.a2ml — Port not assigned. Run 'just groove-setup'." + echo " ⚠ (integrations (groove)) — Port not assigned. Run 'just groove-setup'." else - echo " ✓ groove.a2ml — KEEP (Groove discovery on port $PORT)" + echo " ✓ (integrations (groove)) — KEEP (Groove discovery on port $PORT)" fi elif $HAS_API; then - echo " ✗ groove.a2ml — MISSING. Your project has an API but no Groove manifest." + echo " ✗ (integrations (groove)) — MISSING. Your project has an API but no Groove clause in its deed." echo " → Run 'just groove-setup' to enable snap-on/snap-off discovery." fi @@ -244,13 +262,37 @@ verify: ERRORS=$((ERRORS + 1)) } + # The licence: a root licence file in any of the usual spellings (this + # template ships LICENSE.adoc), or a REUSE LICENSES/ directory holding at + # least one licence text: a non-empty . file, per the REUSE + # spec (dotfiles and READMEs do not count). + check_licence() { + local cand + for cand in LICENSE LICENSE.txt LICENSE.md LICENSE.adoc; do + if [ -f "$cand" ]; then + echo " OK: $cand" + return 0 + fi + done + if [ -n "$(find LICENSES -maxdepth 1 -type f -size +0c -name '*.*' ! -name '.*' ! -iname 'README*' 2>/dev/null | head -n 1)" ]; then + echo " OK: LICENSES/ (REUSE)" + return 0 + fi + echo " FAIL: LICENSE (or LICENSE.adoc, or REUSE LICENSES/) missing" + ERRORS=$((ERRORS + 1)) + } + check_community_file "SECURITY" - check_file "LICENSE" + check_licence check_community_file "CONTRIBUTING" check_either "README.adoc" "README.md" - check_file ".machine_readable/descriptiles/STATE.a2ml" - check_file ".machine_readable/descriptiles/META.a2ml" - check_file ".machine_readable/descriptiles/ECOSYSTEM.a2ml" + # Identity, state and ecosystem live in the repo deed (rsr-template-repo#209). + if DEED=$(bash scripts/deed-field.sh --find . 2>/dev/null) && bash scripts/deed-field.sh --has "$DEED" status; then + echo " OK: $DEED (status, meta, ecosystem)" + else + echo " FAIL: repo deed (*_chora.deed) with a (status …) clause missing" + ERRORS=$((ERRORS + 1)) + fi check_either "CHANGELOG.md" "CHANGELOG.adoc" # Check at least 1 workflow exists diff --git a/build/just/container.just b/build/just/container.just index a906c60..f56a856 100644 --- a/build/just/container.just +++ b/build/just/container.just @@ -46,6 +46,10 @@ container-init: exit 1 fi + # The configs are reformatted after their tokens are filled, which needs a + # KYAML-capable yq: check before anything is written. + bash scripts/format-container-kyaml.sh --preflight + echo "=== Container Template Initialisation ===" echo "" @@ -65,8 +69,11 @@ container-init: read -rp "Primary port [8080]: " _PORT PORT="${_PORT:-8080}" - read -rp "Container registry [ghcr.io/${OWNER:-{{OWNER}}}]: " _REGISTRY - REGISTRY="${_REGISTRY:-ghcr.io/${OWNER:-{{OWNER}}}}" + # Image paths are lower case; an owner login may not be. + _OWNER="${OWNER:-{{OWNER}}}"; _OWNER="${_OWNER,,}" + read -rp "Container registry [ghcr.io/${_OWNER}]: " _REGISTRY + REGISTRY="${_REGISTRY:-ghcr.io/${_OWNER}}" + bash scripts/format-container-kyaml.sh --check-values "$SERVICE_NAME" "$PORT" "$REGISTRY" echo "" echo " Service: $SERVICE_NAME" @@ -89,17 +96,53 @@ 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 + # Filling a token can change how KYAML quotes it; reformat the configs or + # Estate Rules' "Converted YAML stays KYAML" fails the child. + bash scripts/format-container-kyaml.sh + + # 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:" - echo " 1. Edit build/container/Containerfile — add your build commands (Tier A)" + echo " 1. Add build/container/Containerfile — Tier A; pin every FROM:" + echo " scripts/pin-container-digests.sh --root . build/container/Containerfile" + echo " (the template ships none: it shipped an unpinned, non-building one until 2026-10-30)" echo " 2. Edit build/container/entrypoint.sh — set your application binary" echo " 3. Review build/container/compose.yaml — portable stack (Tier B)" echo " 4. Review build/container/stapeln/ — sovereign stack (Tier C)" @@ -117,7 +160,10 @@ container-build *args: elif [ -f "Containerfile" ]; then "$ENGINE" build -t {{project}}:latest -f Containerfile . else - echo "No Containerfile found in build/container/ or project root" + echo "No Containerfile found in build/container/ or project root." + echo "This template ships none — the one it used to ship was unpinned and" + echo "built nothing. Add one, pin it, and the estate gate will accept it:" + echo " scripts/pin-container-digests.sh --root . build/container/Containerfile" exit 1 fi diff --git a/build/just/groove.just b/build/just/groove.just index d4cb734..c52b478 100644 --- a/build/just/groove.just +++ b/build/just/groove.just @@ -4,18 +4,20 @@ # GROOVE PROTOCOL # # Imported by ../../Justfile via `import? "build/just/groove.just"`. -# Recipes here configure and validate the Groove protocol manifest at -# .machine_readable/integrations/groove.a2ml — port assignment, API surface -# flags (REST/gRPC/GraphQL/WebSocket/SSE), and template-placeholder hygiene. -# The manifest itself is consumed by the Groove bridge / zig-unified-api-adapter. +# Recipes here configure and validate the Groove protocol manifest — the +# (integrations (groove …)) clause of the repo deed: port assignment, API +# surface flags (REST/gRPC/GraphQL/WebSocket/SSE), and template-placeholder +# hygiene. Every write goes through scripts/deed-clause.sh, one clause at a +# time. The manifest is consumed by the Groove bridge / zig-unified-api-adapter. # Configure Groove protocol manifest (port assignment, API surfaces) groove-setup: #!/usr/bin/env bash set -euo pipefail - MANIFEST=".machine_readable/integrations/groove.a2ml" - if [ ! -f "$MANIFEST" ]; then - echo "Error: $MANIFEST not found. Run 'just repo-init' first." + DEED="$(bash scripts/deed-field.sh --find .)" || { echo "Error: no single *_chora.deed here. Run 'just repo-init' first."; exit 1; } + MANIFEST="$DEED (integrations (groove))" + if ! bash scripts/deed-field.sh --has "$DEED" integrations/groove; then + echo "Error: $DEED has no (integrations (groove)) clause." exit 1 fi @@ -28,7 +30,7 @@ groove-setup: echo "" read -rp "Primary port for this service: " PORT - [ -z "$PORT" ] && echo "Error: port required" && exit 1 + [[ "$PORT" =~ ^[1-9][0-9]{0,4}$ ]] && [ "$PORT" -le 65535 ] || { echo "Error: port must be an integer 1-65535"; exit 1; } echo "" echo "Which API surfaces does this project expose?" @@ -38,14 +40,20 @@ groove-setup: read -rp " WebSocket? [y/N]: " WS read -rp " SSE (Server-Sent Events)? [y/N]: " SSE - # Update port in manifest - sed -i "s/(port 0)/(port ${PORT})/" "$MANIFEST" + bash scripts/deed-clause.sh set "$DEED" integrations/groove port "$PORT" - # Update API surface flags - [[ "${GRPC,,}" == "y" ]] && sed -i 's/(grpc.*enabled false)/(grpc (enabled true)/' "$MANIFEST" - [[ "${GRAPHQL,,}" == "y" ]] && sed -i 's/(graphql.*enabled false)/(graphql (enabled true)/' "$MANIFEST" - [[ "${WS,,}" == "y" ]] && sed -i 's/(websocket.*enabled false)/(websocket (enabled true)/' "$MANIFEST" - [[ "${SSE,,}" == "y" ]] && sed -i 's/(sse.*enabled false)/(sse (enabled true)/' "$MANIFEST" + # surface NAME ANSWER DEFAULT — write :enabled for one API surface from a + # y/n answer, DEFAULT (y or n) applying to an empty answer. + surface() { + local a="${2:-$3}" v='#f' + [[ "${a,,}" == y* ]] && v='#t' + bash scripts/deed-clause.sh set "$DEED" "integrations/groove/api-surfaces/$1" enabled "$v" + } + surface rest "$REST" y + surface grpc "$GRPC" n + surface graphql "$GRAPHQL" n + surface websocket "$WS" n + surface sse "$SSE" n echo "" echo "Groove manifest updated: $MANIFEST" @@ -59,7 +67,7 @@ verify-template: FOUND=0 # Check for double-brace placeholder patterns - HITS=$(grep -rn '{{'{{'}}[A-Z_]*{{'}}'}}' --include="*.adoc" --include="*.md" --include="*.a2ml" \ + HITS=$(grep -rn '{{'{{'}}[A-Z_]*{{'}}'}}' --include="*.adoc" --include="*.md" --include="*.deed" \ --include="*.scm" --include="*.toml" --include="*.yml" --include="*.yaml" \ . 2>/dev/null | grep -v 'node_modules\|\.git/' | grep -v 'PLACEHOLDERS.adoc' || true) if [ -n "$HITS" ]; then @@ -70,19 +78,20 @@ verify-template: fi # Check for template defaults still present - if grep -q 'rsr-template-repo' Justfile 2>/dev/null; then - echo "⚠ Justfile still references 'rsr-template-repo' — update project name" + if grep -q 'occupancy-types' Justfile 2>/dev/null; then + echo "⚠ Justfile still references 'occupancy-types' — update project name" FOUND=1 fi # Check for port 0 in Groove manifest - if grep -q '(port 0)' .machine_readable/integrations/groove.a2ml 2>/dev/null; then + DEED="$(bash scripts/deed-field.sh --find . 2>/dev/null || true)" + if [ -n "$DEED" ] && [ "$(bash scripts/deed-field.sh "$DEED" integrations/groove port 2>/dev/null)" = 0 ]; then echo "⚠ Groove manifest has port 0 — run 'just groove-setup' to assign a port" FOUND=1 fi - # Check for empty SCM files - for f in .machine_readable/descriptiles/STATE.a2ml .machine_readable/descriptiles/META.a2ml .machine_readable/descriptiles/ECOSYSTEM.a2ml; do + # Check the repo deed for unfilled template placeholders + for f in *_chora.deed; do if [ -f "$f" ] && grep -q '{{'{{'}}' "$f" 2>/dev/null; then echo "⚠ $f still has template placeholders" FOUND=1 diff --git a/build/just/provision-check.sh b/build/just/provision-check.sh new file mode 100755 index 0000000..36a855a --- /dev/null +++ b/build/just/provision-check.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# provision-check.sh — offline conformance check for the provisioning set +# (PROVISIONING-STANDARD.adoc §8, items 1–5). No network, no installs. +# +# provision-check.sh [--dev] [REPO_DIR] +# +# Exit 0 = conforms, 1 = does not. Every failure is printed; none is skipped. +# --dev downgrades repository-specific slot residue (__SPEC_X__) to a warning, +# for a checkout mid-specialisation. Mechanical residue (__X__) always fails. +set -uo pipefail + +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +LIB="$HERE/provision-lib.sh" +DEV=0 +[ "${1:-}" = "--dev" ] && { DEV=1; shift; } +REPO="${1:-.}" +cd "$REPO" || { echo "provision-check: no such directory: $REPO" >&2; exit 1; } + +MIN_JUST="1.42.0" +VERBS="setup doctor heal dev-shell toolchain-refresh ai-setup ai-warmup eval config-show opsm build test bench lint fmt fmt-check run deps" # hypatia:ignore eval_in_shell -- "eval" is a verb name, not the shell builtin +# Files the provisioning set owns; residue anywhere else is not ours to judge. +# The engine scripts under build/just/ are excluded: they name slot syntax in their +# own comments and code, and realign owns them. README.adoc is scanned only inside +# its [[ai-install]] section. +# Their locations come from provision-lib.sh (guix-dir, set-files): the engine and +# this check must never disagree about where the Guix files or warm-ups live. +[ -f "$LIB" ] || { echo "provision-check: engine missing: $LIB" >&2; exit 1; } +# Run the provisioning engine with the checked repository as its root. +lib() { PROVISION_ROOT="$PWD" bash "$LIB" "$@"; } +GDIR=$(lib guix-dir) +SET_FILES=$(lib set-files) +# Print the path to Guix filename $1 using the resolved Guix directory. +gp() { [ "$GDIR" = . ] && echo "$1" || echo "$GDIR/$1"; } + +fails=0 warns=0 +# Print a successful conformance check. +ok() { printf ' ok %s\n' "$*"; } +# Print a failed conformance check and increment the failure count. +bad() { printf ' FAIL %s\n' "$*"; fails=$((fails + 1)); } +# Print a conformance warning and increment the warning count. +warn() { printf ' warn %s\n' "$*"; warns=$((warns + 1)); } + +# Return success when version $1 is at least $2, using version sort order. +version_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; } + +echo "[1] launcher" +if [ ! -f launcher.sh ]; then bad "launcher.sh missing" +elif [ ! -x launcher.sh ]; then bad "launcher.sh is not executable (chmod +x; committed mode must be 100755)" +else + for m in --help --version; do + if ./launcher.sh "$m" >/dev/null 2>&1; then ok "launcher.sh $m exits 0" + else bad "launcher.sh $m exits non-zero"; fi + done +fi + +echo "[2] just" +f0=$fails +if ! command -v just >/dev/null 2>&1; then bad "just is not installed; cannot check the recipe contract" +else + jv=$(just --version 2>/dev/null | awk '{print $2}') + if version_ge "$jv" "$MIN_JUST"; then ok "just $jv >= $MIN_JUST" + else bad "just $jv < $MIN_JUST (module-recipe dependencies do not resolve)"; fi + if ! summary=$(just --summary 2>&1); then + bad "the Justfile does not parse: $(printf '%s' "$summary" | head -1)" + else + have=" $summary " + for v in $VERBS; do + case "$have" in *" $v "*) ;; *) bad "root recipe missing: $v" ;; esac + case "$have" in *" provision::$v "*) ;; *) bad "module recipe missing: provision::$v (engine drift)" ;; esac + done + [ "$fails" -eq "$f0" ] && ok "all ${VERBS// /, } present at root and in provision::" + fi +fi + +echo "[3] mise" +if [ ! -f mise.toml ]; then bad "mise.toml missing" +else + if hit=$(lib mise-banned); then ok "mise.toml names no banned tool"; else bad "mise.toml names banned tools: $hit"; fi + [ -f .mise.toml ] && bad "both mise.toml and .mise.toml exist" + if lg=$(lib mise-lock-gaps); then ok "mise.lock pins every mise.toml tool"; else bad "$lg (latest is not concrete; run: mise lock)"; fi +fi + +echo "[4] guix" +for g in "$(gp guix.scm)" "$(gp manifest.scm)" "$(gp channels.scm)"; do + if r=$(lib guix-stub "$g"); then ok "$g is not a stub"; else bad "$g: $r"; fi +done + +echo "[5] template residue" +f0=$fails +# Read text from stdin and report unfilled slots under label $1; +# repository-specific slots are warnings in --dev mode and failures otherwise. +# Mechanical slots always fail. Callers use the tallies, not the return status. +residue() { # $1 label; stdin = the text to judge + local text mech spec + text=$(cat) + # Mechanical slots (__X__ not starting SPEC_) are the generator's job: always fatal. + mech=$(printf '%s\n' "$text" | grep -noE '__[A-Z][A-Z_]*__' | grep -v ':__SPEC_' | head -3 | tr '\n' ' ') + spec=$(printf '%s\n' "$text" | grep -noE '__SPEC_[A-Z_]*__' | head -3 | tr '\n' ' ') + [ -n "$mech" ] && bad "$1: unfilled mechanical slots: $mech" + if [ -n "$spec" ]; then + if [ "$DEV" = 1 ]; then warn "$1: unfilled repository slots: $spec" + else bad "$1: unfilled repository slots: $spec"; fi + fi +} +for f in $SET_FILES; do + # shellcheck disable=SC2094 # $f is only the label; nothing writes it + [ -f "$f" ] && residue "$f" < "$f" +done +for r in README.adoc README.md; do + [ -f "$r" ] || continue + # From the [[ai-install]] anchor to the next level-2 heading after its own. + residue "$r [[ai-install]]" < <(awk '/^\[\[ai-install\]\]/{on=1; n=0} on && /^== /{n++; if (n>1) exit} on' "$r") +done +[ "$fails" -eq "$f0" ] && ok "no residue that fails this mode" + +echo +echo "provision-check: $fails FAIL, $warns WARN" +[ "$fails" -eq 0 ] diff --git a/build/just/provision-lib.sh b/build/just/provision-lib.sh new file mode 100755 index 0000000..31a23e2 --- /dev/null +++ b/build/just/provision-lib.sh @@ -0,0 +1,972 @@ +#!/usr/bin/env bash +# SC2015: `A && pass || fail` is safe here — pass/warn/fail/info always return 0. +# shellcheck disable=SC2015 +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# provision-lib.sh — the shared provisioning engine behind build/just/provision.just +# +# Canon: hyperpolymath/standards 3-practice/provisioning/PROVISIONING-STANDARD.adoc +# Vendored into every repo at build/just/provision-lib.sh by `provision-set mint`. +# DO NOT hand-edit a vendored copy: repo-specific facts live in +# .machine_readable/descriptiles/provisioning_praxis.deed, and `provision-set realign` +# overwrites this file from canon. +# +# Usage: provision-lib.sh [args] +# verbs: langs doctor setup heal dev-shell toolchain-refresh crates-scm ai-setup +# ai-warmup eval config-show opsm lang-run (hypatia:ignore eval_in_shell -- verb list) +# search version +# facts: guix-specs mise-tools langs guix-dir set-files guix-gaps tool-table +# system-deps +# predicates (print why and exit 1, or exit 0 silently): +# guix-stub FILE, mise-lock-gaps, mise-banned +# +# Exit: 0 ok | 1 a FAIL was found / a step failed | 2 usage error +set -uo pipefail + +PROVISION_LIB_VERSION="0.6.0" +ROOT="${PROVISION_ROOT:-$(pwd)}" +cd "$ROOT" || exit 2 +DEED=".machine_readable/descriptiles/provisioning_praxis.deed" +MIN_JUST="1.42.0" # root→module recipe deps (`doctor: provision::doctor`); 1.41 rejects them +# How to get mise: package managers first; the installer only as download, read, run. +MISE_INSTALL_HINT="brew install mise | Fedora: dnf copr enable jdxcode/mise, dnf install mise | winget install jdx.mise | others: https://mise.jdx.dev/installing-mise.html — or: curl -fsSLo mise-install.sh https://mise.run, read it, sh mise-install.sh" +T="timeout 15" # some --version probes hang (observed 2026-09-30); never probe unbounded + +if [ -t 1 ]; then R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[34m'; Z=$'\033[0m'; else R=; G=; Y=; B=; Z=; fi +PASS=0; WARN=0; FAIL=0 +# Increment the PASS tally and print the diagnostic message. +pass() { PASS=$((PASS+1)); printf ' %sPASS%s %s\n' "$G" "$Z" "$*"; } +# Increment the WARN tally and print the diagnostic message. +warn() { WARN=$((WARN+1)); printf ' %sWARN%s %s\n' "$Y" "$Z" "$*"; } +# Increment the FAIL tally and print the diagnostic message. +fail() { FAIL=$((FAIL+1)); printf ' %sFAIL%s %s\n' "$R" "$Z" "$*"; } +# Print an informational message without changing diagnostic tallies. +info() { printf ' %sinfo%s %s\n' "$B" "$Z" "$*"; } +# Print a section heading for provisioning output. +hdr() { printf '\n%s== %s ==%s\n' "$B" "$*" "$Z"; } +# Return success when command $1 is available in the current shell. +have() { command -v "$1" >/dev/null 2>&1; } + +# Where this repository keeps each part of the set. A file sits at the root only +# when something needs it there (launcher.sh, Justfile, mise.toml, mise.lock); +# the Guix trio and the warm-ups follow the layout the repository already has +# (occupancy-types keeps build/guix.scm and docs/onboarding/llm-warmup-*.adoc). +# This is the ONE answer: provision-check.sh asks it through `guix-dir` and +# `set-files` rather than keeping its own copy. +guix_dir() { if [ -f build/guix.scm ]; then echo build; else echo .; fi; } +# Print the first directory containing warm-up guides, falling back to the root. +warmup_dir() { + local d + for d in . docs/onboarding docs; do + compgen -G "$d/llm-warmup-*.adoc" >/dev/null && { echo "$d"; return; } + done + echo . +} +# Print the path to Guix filename $1 in the repository layout. +gpath() { local d; d=$(guix_dir); [ "$d" = . ] && echo "$1" || echo "$d/$1"; } +# Print the warm-up guide path for audience $1 in the repository layout. +wpath() { local d; d=$(warmup_dir); [ "$d" = . ] && echo "llm-warmup-$1.adoc" || echo "$d/llm-warmup-$1.adoc"; } +# List the provisioning-owned paths checked for unfilled template slots. +set_files() { + printf '%s\n' launcher.sh Justfile justfile mise.toml \ + "$(gpath guix.scm)" "$(gpath manifest.scm)" "$(gpath channels.scm)" \ + "$DEED" docs/SETUP.adoc docs/AI_INSTALLATION_GUIDE.adoc \ + "$(wpath user)" "$(wpath dev)" "$(wpath maintainer)" +} + +# --------------------------------------------------------------------------- +# Descriptor: flat `:key "value"` reads from provisioning_praxis.deed (s-expression). +# Use default $2 when the value is absent or empty; print without a trailing newline. +deed() { # $1 key, $2 default + local v="" + [ -f "$DEED" ] && v=$(grep -oE "\(:?$1[[:space:]]+\"[^\"]*\"" "$DEED" 2>/dev/null | head -1 | sed -E 's/^[^"]*"//; s/"$//') + [ -z "$v" ] && v=$(grep -oE ":$1[[:space:]]+\"[^\"]*\"" "$DEED" 2>/dev/null | head -1 | sed -E 's/^[^"]*"//; s/"$//') + printf '%s' "${v:-$2}" +} +# Derive the repository slug from origin, falling back to hyperpolymath/. +repo_slug() { + local u; u=$(git config --get remote.origin.url 2>/dev/null || true) + u=${u%.git}; u=${u#*github.com[:/]} + [ -n "$u" ] && printf '%s' "$u" || printf 'hyperpolymath/%s' "$(basename "$ROOT")" +} +REPO_SLUG="$(deed repo "$(repo_slug)")" +REPO_NAME="${REPO_SLUG#*/}" +ARCHETYPE="$(deed archetype "")" + +# --------------------------------------------------------------------------- +# Language detection. Markers at the root or one level down (workspaces). The +# estate ABI/FFI pattern nests deeper: Idris2 to two levels down (src/abi/*.ipkg), +# Zig to three (ffi/zig/build.zig, and occupancy-types's src/interface/ffi/build.zig: +# 74 repos have their only build.zig at that depth, measured 2026-09-30). +# Order matters only for display. `docs` is reported when nothing else is. +# Print the first path matching glob $1 within find depth $2 (default 2), +# excluding .git, node_modules and root target contents; no match prints nothing. +first() { find . -maxdepth "${2:-2}" -not -path './.git/*' -not -path '*/node_modules/*' -not -path './target/*' -name "$1" -print -quit 2>/dev/null; } +# Print detected languages, one per line; use docs when no language marker exists. +detect_langs() { + local out=() + [ -n "$(first Cargo.toml)" ] && out+=(rust) + [ -n "$(first '*.ipkg' 3)" ] && out+=(idris2) + [ -n "$(first Project.toml 1)" ] && out+=(julia) + [ -n "$(first build.zig 4)" ] && out+=(zig) + [ -n "$(first mix.exs)" ] && out+=(elixir) + [ -n "$(first gleam.toml)" ] && out+=(gleam) + [ -n "$(first dune-project)" ] && out+=(ocaml) + { [ -n "$(first '*.cabal')" ] || [ -n "$(first stack.yaml 1)" ]; } && out+=(haskell) + [ -n "$(first package.json)" ] && out+=(bun) + [ ${#out[@]} -eq 0 ] && out+=(docs) + printf '%s\n' "${out[@]}" +} +mapfile -t LANGS < <(detect_langs) +[ -z "$ARCHETYPE" ] && { [ "${LANGS[*]}" = docs ] && ARCHETYPE=docs || ARCHETYPE=library; } + +# Binaries each language needs, and how each is obtained. +# mise registry gaps (verified 2026-09-30 against mise 2026.7.5): ocaml, haskell, +# idris2 and guile are NOT mise tools. They come from opam / ghcup / pack / the +# system package manager (or Guix), and the doctor probes the binary itself. +lang_tools() { + case "$1" in + rust) echo "cargo rustc" ;; + idris2) echo "idris2" ;; + julia) echo "julia" ;; + zig) echo "zig" ;; + elixir) echo "elixir mix erl" ;; + gleam) echo "gleam erl" ;; + ocaml) echo "opam dune ocaml" ;; + haskell) echo "ghc cabal" ;; + # One package.json, every runtime (LANGUAGE-POLICY §1.4): bun runs it, and + # `js-runtimes regen` needs deno and pnpm to write their locks too. + bun) echo "bun deno pnpm" ;; + docs) echo "" ;; + esac +} +# Guix package specs per language (verified 2026-09-30 against guix ae77aeb in +# docker.io/metacall/guix). Guix has NO idris2, gleam, bun or lychee, and its +# julia is 1.8.5: those come from mise, which Guix itself ships ("mise"). deno +# and pnpm come from mise as well (their Guix packages were not measured). +GUIX_BASE="git bash coreutils nss-certs just mise shellcheck" +# Print the Guix package specifications for language $1, if available. +lang_guix() { + case "$1" in + rust) echo "rust rust:cargo gcc-toolchain pkg-config" ;; + idris2) echo "chez-scheme gmp gcc-toolchain" ;; + zig) echo "zig" ;; + elixir) echo "elixir erlang" ;; + gleam) echo "erlang" ;; + ocaml) echo "ocaml dune opam ocaml-findlib" ;; + haskell) echo "ghc cabal-install" ;; + docs) echo "ruby-asciidoctor" ;; + julia|bun) echo "" ;; + esac +} +# Tools a Guix shell cannot supply, so mise (inside that shell) does. +lang_guix_gap() { + case "$1" in + idris2) echo "idris2 (via pack)" ;; julia) echo julia ;; gleam) echo gleam ;; bun) echo "bun deno pnpm" ;; docs) echo lychee ;; + esac +} +# Print deduplicated base and detected-language Guix package specifications. +guix_specs() { + local l s=" $GUIX_BASE " + for l in "${LANGS[@]}"; do for p in $(lang_guix "$l"); do case "$s" in *" $p "*) ;; *) s="$s$p ";; esac; done; done + echo "$s" | xargs +} + +# mise tools per language (registry-checked 2026-09-30, mise 2026.7.5). idris2 comes +# from pack and haskell from ghcup or Guix: neither is a mise tool. The recipes +# themselves need just and shellcheck, so those two are always declared. +MISE_BASE="just shellcheck yq" # yq: scripts/kyaml-format.sh (KYAML configs; the mint formats the container ones) +# Tools a repo's own recipes call (e.g. `deps-audit` runs trivy): pinned only where used. +RECIPE_TOOLS="trivy" +# Print the mise tool names for language $1, if supported. +lang_mise() { case "$1" in + rust) echo "rust" ;; zig) echo "zig" ;; julia) echo "julia" ;; + elixir) echo "erlang elixir" ;; gleam) echo "erlang gleam" ;; ocaml) echo "opam" ;; + bun) echo "bun deno pnpm" ;; docs) echo "lychee" ;; esac; } +# Print known recipe tools referenced outside comments in the repository Justfiles. +recipe_tools() { + local t f body="" + for f in Justfile justfile build/just/*.just; do + [ -f "$f" ] && body+=$(grep -v '^[[:space:]]*#' "$f")$'\n' + done + for t in $RECIPE_TOOLS; do + [[ "$body" =~ (^|[^[:alnum:]_-])$t([^[:alnum:]_-]|$) ]] && echo "$t" + done +} + +# Print deduplicated mise tools needed by the base, detected languages and recipes. +# Usage: mise_tools [lang ...] +# The language list defaults to what the tree detects; callers that already know +# (scripts/render-mise.sh --langs) pass it, so an override actually changes the, +# toolchain rather than only the label above it. +mise_tools() { + local l t seen=" " out=() use=() + if [ "$#" -gt 0 ]; then use=("$@"); else use=("${LANGS[@]}"); fi + for t in $MISE_BASE $(for l in "${use[@]}"; do lang_mise "$l"; done) $(recipe_tools); do + case "$seen" in *" $t "*) ;; *) out+=("$t"); seen="$seen$t " ;; esac + done + printf "%s\n" "${out[*]}" +} + +# Print installation guidance for the toolchain of language $1. +lang_remedy() { + case "$1" in + rust) echo "mise use rust@latest (or rustup: https://rustup.rs)" ;; + idris2) echo "install pack: https://github.com/stefan-hoeck/idris2-pack#installation, then: pack install-app idris2" ;; + julia) echo "mise use julia@latest (or: https://julialang.org/install/ — juliaup)" ;; + zig) echo "mise use zig@latest" ;; + elixir) echo "mise use erlang@latest elixir@latest (erlang builds from source: allow ~10 min)" ;; + gleam) echo "mise use gleam@latest erlang@latest" ;; + ocaml) echo "mise use opam@latest && opam init -y && opam switch create . --deps-only -y" ;; + haskell) echo "ghcup: https://www.haskell.org/ghcup/ (or: just dev-shell, which uses Guix)" ;; + bun) echo "mise use bun@latest deno@latest pnpm@latest" ;; + esac +} + +# The per-language default for each contract verb. A repo overrides any of +# these simply by defining the root recipe itself; `provision-set` only adds +# `: provision::` where the root Justfile has no such recipe. +lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) + local l=$1 v=$2 ipkg + case "$l:$v" in + rust:deps) echo "cargo fetch" ;; + rust:build) echo "cargo build --all-targets" ;; + rust:test) echo "cargo test --all-targets" ;; + rust:bench) grep -rqs '\[\[bench\]\]\|criterion\|divan' --include=Cargo.toml . && echo "cargo bench" ;; + rust:lint) echo "cargo clippy --all-targets -- -D warnings" ;; + rust:fmt) echo "cargo fmt --all" ;; + rust:fmt-check) echo "cargo fmt --all -- --check" ;; + rust:run) echo "cargo run --release" ;; + + idris2:*) + ipkg=$(first '*.ipkg' 1); [ -z "$ipkg" ] && ipkg=$(first '*.ipkg'); [ -z "$ipkg" ] && ipkg=$(first '*.ipkg' 3); ipkg=${ipkg#./} + case "$v" in + deps) have pack && echo "pack install-deps $ipkg" ;; + build) have pack && echo "pack build $ipkg" || echo "idris2 --build $ipkg" ;; + test) local t; t=$(find . -maxdepth 3 -name 'test*.ipkg' -print -quit 2>/dev/null) + if [ -n "$t" ]; then have pack && echo "pack test ${t#./}" || echo "idris2 --build ${t#./}" + else have pack && echo "pack typecheck $ipkg" || echo "idris2 --typecheck $ipkg"; fi ;; + run) grep -qs '^[[:space:]]*executable' "$ipkg" && { have pack && echo "pack run $ipkg" || echo "idris2 --build $ipkg && ./build/exec/*"; } ;; + esac ;; + + julia:deps) echo "julia --project=. -e 'using Pkg; Pkg.instantiate()'" ;; + julia:build) echo "julia --project=. -e 'using Pkg; Pkg.precompile()'" ;; + julia:test) echo "julia --project=. -e 'using Pkg; Pkg.test()'" ;; + julia:bench) [ -f benchmark/benchmarks.jl ] && echo "julia --project=benchmark -e 'using Pkg; Pkg.develop(path=\".\"); Pkg.instantiate(); include(\"benchmark/benchmarks.jl\")'" ;; + + zig:*) + # build.zig may sit in ffi/zig/; run there, not at the root. + local zb zd; zb=$(first build.zig 1); [ -z "$zb" ] && zb=$(first build.zig); [ -z "$zb" ] && zb=$(first build.zig 3); [ -z "$zb" ] && zb=$(first build.zig 4) + zd=$(dirname "${zb#./}"); local cdz=""; [ "$zd" != . ] && cdz="cd '$zd' && " + case "$v" in + build) echo "${cdz}zig build" ;; + test) grep -qs '"test"' "$zb" && echo "${cdz}zig build test" ;; + bench) grep -qs '"bench"' "$zb" && echo "${cdz}zig build bench -Doptimize=ReleaseFast" ;; + fmt) echo "zig fmt $zd" ;; + fmt-check) echo "zig fmt --check $zd" ;; + lint) echo "zig fmt --check $zd" ;; + run) grep -qs '"run"' "$zb" && echo "${cdz}zig build run" ;; + esac ;; + + elixir:deps) echo "mix local.hex --force --if-missing && mix local.rebar --force --if-missing && mix deps.get" ;; + elixir:build) echo "mix compile --warnings-as-errors" ;; + elixir:test) echo "mix test" ;; + elixir:bench) ls bench/*.exs >/dev/null 2>&1 && echo "for f in bench/*.exs; do mix run \"\$f\"; done" ;; + elixir:lint) grep -qs ':credo' mix.exs && echo "mix credo --strict" || echo "mix compile --warnings-as-errors" ;; + elixir:fmt) echo "mix format" ;; + elixir:fmt-check) echo "mix format --check-formatted" ;; + elixir:run) echo "mix run --no-halt" ;; + + gleam:deps) echo "gleam deps download" ;; + gleam:build) echo "gleam build" ;; + gleam:test) echo "gleam test" ;; + gleam:fmt) echo "gleam format" ;; + gleam:fmt-check) echo "gleam format --check" ;; + gleam:lint) echo "gleam format --check" ;; + gleam:run) echo "gleam run" ;; + + ocaml:deps) echo "opam install . --deps-only --with-test -y" ;; + ocaml:build) echo "opam exec -- dune build" ;; + ocaml:test) echo "opam exec -- dune test" ;; + ocaml:fmt) echo "opam exec -- dune fmt" ;; + ocaml:fmt-check) echo "opam exec -- dune build @fmt" ;; + + haskell:deps) echo "cabal update && cabal build all --only-dependencies" ;; + haskell:build) echo "cabal build all" ;; + haskell:test) echo "cabal test all" ;; + haskell:bench) grep -qs '^benchmark' ./*.cabal && echo "cabal bench all" ;; + haskell:run) echo "cabal run" ;; + + bun:deps) [ -f bun.lock ] || [ -f bun.lockb ] && echo "bun install --frozen-lockfile" || echo "bun install" ;; + bun:build) grep -qs '"build"[[:space:]]*:' package.json && echo "bun run build" ;; + # `bun test` exits 0 when it finds no test files, so it runs only when some exist. + bun:test) if grep -qs '"test"[[:space:]]*:' package.json; then echo "bun run test" + elif git ls-files 2>/dev/null | grep -qE '(\.|_)(test|spec)\.(js|mjs|cjs|jsx)$'; then echo "bun test"; fi ;; + bun:bench) grep -qs '"bench"[[:space:]]*:' package.json && echo "bun run bench" ;; + bun:lint) grep -qs '"lint"[[:space:]]*:' package.json && echo "bun run lint" ;; + bun:fmt) grep -qs '"fmt"[[:space:]]*:' package.json && echo "bun run fmt" ;; + bun:fmt-check) grep -qs '"fmt-check"[[:space:]]*:' package.json && echo "bun run fmt-check" ;; + bun:run) grep -qs '"start"[[:space:]]*:' package.json && echo "bun run start" ;; + + docs:test) have asciidoctor && echo "for f in \$(git ls-files '*.adoc'); do asciidoctor -o /dev/null --failure-level=WARN \"\$f\" || exit 1; done" ;; + docs:lint) have lychee && echo "lychee --offline --no-progress \$(git ls-files '*.adoc' '*.md')" ;; + esac +} + +# Run a contract verb across every detected language. N/A is reported, not +# faked green: a verb with no command for any language exits 0 but SAYS so. +# A deed override runs alone and its status is returned. Otherwise all available +# language commands run; return the last failing status, or 0 if none failed. +lang_run() { # $1 verb + local verb=$1 ran=0 rc=0 l cmd override + override=$(deed "$verb" "") + if [ -n "$override" ]; then + info "$verb (from provisioning_praxis.deed): $override" + bash -c "$override"; return $? + fi + for l in "${LANGS[@]}"; do + cmd=$(lang_cmd "$l" "$verb") + [ -z "$cmd" ] && continue + ran=1 + info "$verb [$l]: $cmd" + bash -c "$cmd" || { rc=$?; printf ' %sFAIL%s %s [%s] exited %s\n' "$R" "$Z" "$verb" "$l" "$rc" >&2; } + done + [ $ran -eq 0 ] && info "$verb: N/A for ${LANGS[*]} (archetype: $ARCHETYPE) — nothing to run, and nothing was faked" + return $rc +} + +# --------------------------------------------------------------------------- +# A repo keeps its own checks as root recipes named doctor-local / setup-local / +# heal-local (provision-set renames a pre-existing custom doctor/setup/heal to +# these); the canon verbs run them, so `just doctor`, `./launcher.sh --doctor` and +# CI all see the same result. +has_recipe() { have just && just --summary 2>/dev/null | tr " " "\n" | grep -qx "$1"; } + +# Return success when version $1 is at least $2, using version sort order. +version_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; } + +# Tools that must never appear in a repo's toolchain (estate language policy). +# deno and pnpm are permitted runtimes since the 2026-10-14 ruling (Bun > Deno > +# pnpm > npm); node stays banned, so npm, the last resort, is never provisioned. +BANNED_TOOLS='python|node|nodejs|npm|yarn|typescript|rescript|make|black|ruff|pip|poetry|nix|go|golang|java|kotlin' +BANNED_BACKENDS='npm|pipx|pip|go' + +# --------------------------------------------------------------------------- +# Shared predicates. doctor and provision-check.sh both call THESE (the check via +# `provision-lib.sh guix-stub|mise-lock-gaps|mise-banned`): a gate with its own +# copy of a test passes what doctor warns about. + +# The tools every repository mise config declares, quotes stripped ("cargo:foo" +# stays cargo:foo): the [tools] keys of mise.toml and .mise.toml and the first +# word of each .tool-versions line. mise merges all three, so a predicate that +# read mise.toml alone would pass a banned tool pinned in the others. +mise_toml_tools() { + local f + for f in .tool-versions mise.toml .mise.toml; do + [ -f "$f" ] || continue + if [ "$f" = .tool-versions ]; then + awk '!/^[ \t]*(#|$)/{print $1}' "$f" + else + awk '/^\[tools\]/{t=1;next} /^\[/{t=0} t && /=/{sub(/[ \t]*=.*/,""); gsub(/["\x27 ]/,""); print}' "$f" + fi + done | awk '!seen[$0]++' +} +# True when a bare registry name resolves only to banned backends ("prettier" +# is only npm:prettier). Needs no network: `mise registry` reads the registry +# baked into mise. False when mise is absent or does not know the name, so an +# unknown tool is left to mise-lock to report, never called banned on a guess. +only_banned_backends() { + local b n=0 + command -v mise >/dev/null 2>&1 || return 1 + for b in $(mise registry "$1" 2>/dev/null); do + n=$((n + 1)) + [[ "$b" =~ ^($BANNED_BACKENDS): ]] || return 1 + done + [ "$n" -gt 0 ] +} +# Banned tools named in the mise configs. A backend prefix does not hide one +# ("aqua:denoland/deno" is deno), an npm:/pipx:/pip:/go: backend installs +# through a banned runtime whatever the package is ("npm:prettier"), and so does +# a bare name with no other backend ("prettier"). +# Print space-separated matches without a trailing newline; callers determine +# failure from non-empty output. +mise_banned() { + local t base hits="" + for t in $(mise_toml_tools); do + base=${t##*:}; base=${base%%@*}; base=${base##*/} + if [[ "$base" =~ ^($BANNED_TOOLS)$ ]] || [[ "$t" =~ ^($BANNED_BACKENDS): ]]; then + hits="$hits$t " + elif [[ "$t" != *:* ]] && only_banned_backends "$t"; then + hits="$hits$t " + fi + done + printf '%s' "${hits% }" +} +# Print the first category of lockfile gaps, or nothing if none is found. +# Skip checking when mise.toml is absent; otherwise check tools from all three +# config files for a non-empty version and platform tables for sha256 entries. +# Callers inspect stdout; version concreteness and checksum contents are not validated. +mise_lock_gaps() { + [ -f mise.toml ] || return 0 + [ -f mise.lock ] || { echo "mise.lock missing"; return; } + [ -s mise.lock ] || { echo "mise.lock is empty"; return; } + local t miss="" + # A tool is pinned when its [[tools.X]] block carries a concrete version line. + for t in $(mise_toml_tools); do + awk -v a="[[tools.$t]]" -v b="[[tools.\"$t\"]]" ' + $0 == a || $0 == b { inb = 1; next } + /^\[/ { inb = 0 } + inb && /^version = "[^"]+"/ { ok = 1 } + END { exit !ok }' mise.lock || miss="$miss$t " + done + [ -n "$miss" ] && { echo "mise.lock does not pin: ${miss% }"; return; } + # Checksums are per artefact: every [tools.X."platforms.P"] table needs its own + # sha256, so one checksummed tool cannot vouch for another. A tool with no + # platform tables has no artefact to checksum (core:rust installs through + # rustup, cargo: builds from source), which is what `mise lock` writes for it. + miss=$(awk ' + # Print the open platform table as tool/platform when it carried no sha256. + function close_table() { if (p != "" && !c) printf "%s ", p; p = "" } + /^\[tools\..*platforms\./ { close_table(); p = $0; c = 0 + gsub(/^\[tools\.|\]$|"/, "", p); sub(/\.platforms\./, "/", p); next } + /^\[/ { close_table() } + /^checksum = "sha256:[0-9a-f]+"/ { c = 1 } + END { close_table() }' mise.lock) + [ -z "$miss" ] || echo "mise.lock has no sha256 for: ${miss% }" +} +# Print why Guix file $1 appears to be a stub, or nothing if the textual checks +# pass. Check required package fields as well as known stub shapes; this does +# not evaluate Scheme or verify that the package builds. Callers inspect stdout. +# For example, `(package (name "x") (source (local-file ".")))` is a stub. +guix_stub_reason() { + local f=$1 k miss="" + [ -f "$f" ] || { echo "missing"; return; } + grep -qE '\{\{|__[A-Z][A-Z_]*__' "$f" && { echo "unfilled template slots"; return; } + grep -qE '\(inputs \(list\)\)|\(source #f\)' "$f" && { echo "empty inputs or no source"; return; } + case "${f##*/}" in + manifest.scm) grep -q 'specifications->manifest' "$f" || echo "lists no specifications"; return ;; + channels.scm) grep -qE '\(commit "[0-9a-f]{40}"\)' "$f" || echo "pins no commit"; return ;; + esac + for k in name version source build-system home-page synopsis description license; do + grep -qE "\\(${k}[[:space:]]" "$f" || miss="$miss$k " + done + [ -n "$miss" ] && { echo "no package field: ${miss% }"; return; } + if grep -q 'crates\.scm' "$f"; then + grep -qs 'define %crate-inputs' build/guix/crates.scm || echo "build/guix/crates.scm is missing or defines no %crate-inputs" + fi +} + +# --------------------------------------------------------------------------- +# Facts the generator writes into docs/SETUP.adoc and the AI guide. They live here, +# beside lang_tools and lang_remedy, so no second per-language table exists. +lang_title() { case "$1" in + rust) echo Rust ;; idris2) echo Idris2 ;; julia) echo Julia ;; zig) echo Zig ;; elixir) echo Elixir ;; + gleam) echo Gleam ;; ocaml) echo OCaml ;; haskell) echo Haskell ;; bun) echo Bun ;; docs) echo Docs ;; esac; } +# What a language's route needs from the OS: "fedora|debian|macos command|why", or nothing. +lang_sysdeps() { case "$1" in + rust) echo "gcc pkgconf-pkg-config|build-essential pkg-config|xcode-select --install|Rust links through the system C toolchain, and pkg-config finds C libraries" ;; + idris2) echo "chez-scheme gmp-devel|chezscheme libgmp-dev|brew install chezscheme gmp|pack builds Idris2 on top of Chez Scheme and GMP" ;; + elixir|gleam) echo "gcc gcc-c++ make autoconf ncurses-devel openssl-devel|build-essential autoconf m4 libncurses-dev libssl-dev|brew install autoconf openssl@3|mise builds Erlang/OTP from source (allow ~10 min); the make here is the system tool that build uses, not a Makefile in this repository" ;; + ocaml) echo "gcc make patch unzip bubblewrap|build-essential patch unzip bubblewrap|xcode-select --install|opam compiles OCaml and sandboxes its builds with bubblewrap" ;; + haskell) echo "gcc gcc-c++ gmp-devel make ncurses-devel xz perl|build-essential curl libffi-dev libgmp-dev libncurses-dev|xcode-select --install|ghcup installs GHC, which links through the C toolchain and GMP" ;; +esac; } +# Print a comma-separated list of tools Guix cannot supply, or none. +guix_gaps() { + local l g gaps="" + for l in "${LANGS[@]}"; do g=$(lang_guix_gap "$l"); [ -n "$g" ] && gaps="$gaps${gaps:+, }$g"; done + printf '%s\n' "${gaps:-none}" +} +# Print AsciiDoc table rows describing required tools and installation commands. +tool_table() { + local l t base + for l in "${LANGS[@]}"; do + if [ "$l" = docs ]; then echo "|docs |\`lychee\` |mise use lychee@latest"; continue; fi + t=$(lang_tools "$l"); echo "|$l |\`${t// /\`, \`}\` |$(lang_remedy "$l")" + done + base="$MISE_BASE $(recipe_tools | xargs)"; base=$(echo "$base" | xargs) + echo "|(recipes) |\`${base// /\`, \`}\` |mise install, or your OS package manager (e.g. \`dnf install just ShellCheck\`)" +} +# shellcheck disable=SC2016 # the backticks are AsciiDoc literals, not command substitution +# Print OS dependency guidance for detected languages; $1 selects adoc or ai output. +system_deps() { # $1 adoc|ai + local l d seen="" fed deb mac why any=0 + [ "$1" = adoc ] && printf '=== System packages\n' + for l in "${LANGS[@]}"; do + d=$(lang_sysdeps "$l"); [ -z "$d" ] && continue + case "$seen" in *"|$d|"*) continue ;; esac; seen="$seen|$d|"; any=1 + IFS='|' read -r fed deb mac why <<<"$d" + if [ "$1" = adoc ]; then + printf '\n%s: %s.\n\n* Fedora: `sudo dnf install %s`\n* Debian/Ubuntu: `sudo apt install %s`\n* macOS: `%s`\n' \ + "$(lang_title "$l")" "$why" "$fed" "$deb" "$mac" + else + printf '* %s needs OS packages (%s): Fedora `sudo dnf install %s` · Debian/Ubuntu `sudo apt install %s` · macOS `%s`.\n' \ + "$(lang_title "$l")" "$why" "$fed" "$deb" "$mac" + fi + done + if [ "$1" = adoc ]; then + if [ $any -eq 1 ]; then printf '\nThe Guix development shell (`%s`) provides these itself, so inside `just dev-shell` none of them is needed.\n' "$(gpath manifest.scm)" + else printf '\nNone beyond git and a shell: every tool this repository needs comes from mise, or from the Guix shell.\n'; fi + elif [ $any -eq 0 ]; then printf '* No other OS packages: every tool comes from mise.\n'; fi +} + +# Diagnose the provisioning set and toolchain, then the repository's own +# doctor-local checks. The PASS/WARN/FAIL tally is the last line of stdout; +# returns non-zero when anything FAILed. +# Warnings alone do not fail. An early doctor-local.sh exit or failed +# doctor-local recipe becomes a FAIL rather than propagating its exit status. +cmd_doctor() { + printf '%s doctor — %s (%s; languages: %s)\n' "$REPO_NAME" "$REPO_SLUG" "$ARCHETYPE" "${LANGS[*]}" + + hdr "Core toolchain" + have git && pass "git $($T git --version 2>/dev/null | awk '{print $3}')" || fail "PV-E01 git not found — install git from your OS package manager" + if have just; then + local jv; jv=$($T just --version 2>/dev/null | awk '{print $2}') + version_ge "$jv" "$MIN_JUST" && pass "just $jv (>= $MIN_JUST)" || fail "PV-E02 just $jv is older than $MIN_JUST — run: mise use just@latest" + else fail "PV-E02 just not found — run: mise use -g just@latest (or see docs/SETUP.adoc)"; fi + if have mise; then + pass "mise $($T mise --version 2>/dev/null | awk '{print $1}')" + if [ -f mise.toml ] || [ -f .mise.toml ]; then + # Only THIS repo's declarations: `mise ls` also lists the user's global config. + local here=${ROOT/#$HOME/\~} missing + missing=$($T mise ls --current --missing 2>/dev/null | grep -F -e "$here/mise.toml" -e "$here/.mise.toml" -e "$ROOT/mise.toml" | awk '{print $1"@"$2}' | tr '\n' ' ') + [ -z "${missing// /}" ] && pass "every mise tool is installed" || fail "PV-E03 mise tools not installed: $missing— run: just setup" + fi + else warn "PV-W01 mise not found — tools must then come from Guix or your OS; install: $MISE_INSTALL_HINT"; fi + if have guix; then pass "guix $($T guix --version 2>/dev/null | head -1 | awk '{print $NF}') (optional reproducible path)" + else info "guix not installed — optional; mise is the default path (docs/SETUP.adoc §Guix)"; fi + + hdr "Language toolchains" + local l t + for l in "${LANGS[@]}"; do + [ "$l" = docs ] && { info "no build language detected — docs/theory archetype"; continue; } + for t in $(lang_tools "$l"); do + if have "$t"; then pass "$l: $t ($(command -v "$t"))" + else fail "PV-E10 $l: '$t' not found — run: just setup (manual: $(lang_remedy "$l"))"; fi + done + done + local extra; extra=$(deed system-deps "") + for t in $extra; do have "$t" && pass "system dep: $t" || fail "PV-E11 system dependency '$t' not found (declared in $DEED) — see docs/SETUP.adoc §Prerequisites"; done + + hdr "Repository provisioning files" + [ -f mise.toml ] && pass "mise.toml" || fail "PV-E20 mise.toml missing — the toolchain is undeclared" + local lg; lg=$(mise_lock_gaps) + [ -z "$lg" ] && pass "mise.lock pins every mise.toml tool (latest → concrete, checksummed)" || warn "PV-W20 $lg — run: just toolchain-refresh" + [ -f .mise.toml ] && [ -f mise.toml ] && warn "PV-W21 both mise.toml and .mise.toml — mise merges them; keep only mise.toml" + [ -f .tool-versions ] && warn "PV-W22 .tool-versions present — a second toolchain source; fold it into mise.toml" + if [ -f mise.toml ] || [ -f .mise.toml ] || [ -f .tool-versions ]; then + local bad; bad=$(mise_banned) + [ -z "$bad" ] && pass "the mise configs pin no banned tool" || warn "PV-W23 a mise config pins banned tool(s): $bad (language policy: Bun > Deno > pnpm; no python/node/npm/make)" + fi + # hypatia guix_not_stub reads guix.scm AND build/guix.scm; an unfilled __PLACEHOLDER__ is a stub too. + local g r gstub="" + for g in guix.scm build/guix.scm "$(gpath manifest.scm)" "$(gpath channels.scm)"; do + [ -f "$g" ] || continue + r=$(guix_stub_reason "$g"); [ -n "$r" ] && gstub="$gstub$g ($r) " + done + local gs gm; gs=$(gpath guix.scm); gm=$(gpath manifest.scm) + [ -f guix.scm ] && [ -f build/guix.scm ] && warn "PV-W35 both guix.scm and build/guix.scm exist — two Guix sources; keep one (build/ is used)" + if [ -f "$gs" ]; then + if [ -n "$gstub" ]; then warn "PV-W24 template stub in: $gstub— the Guix path does not build this repo (just heal cannot fix this; provision-set realign does)" + else pass "$gs (non-stub)"; fi + else warn "PV-W25 guix.scm missing — no reproducible Guix path"; fi + [ -f "$gm" ] && pass "$gm (guix shell -m $gm)" || warn "PV-W26 $gm missing — 'just dev-shell' falls back to mise" + [ -x launcher.sh ] && pass "launcher.sh (executable)" || { [ -f launcher.sh ] && fail "PV-E27 launcher.sh not executable — run: just heal" || warn "PV-W27 launcher.sh missing"; } + local d + for d in docs/SETUP.adoc docs/AI_INSTALLATION_GUIDE.adoc "$(wpath user)" "$(wpath dev)" "$(wpath maintainer)"; do + if [ ! -f "$d" ]; then warn "PV-W28 $d missing" + elif grep -qE "__[A-Z][A-Z_]*__" "$d"; then warn "PV-W29 $d still has unfilled template slots (__SPEC_…__) — replace each with the facts for this repo" + else pass "$d"; fi + done + + hdr "Estate policy drift" + local f any=0 + # deno.lock is a generated lock every package carries (`js-runtimes regen`); + # authoring in Deno is permitted (tier 2), so its manifests are a notice only. + for f in deno.json deno.jsonc import_map.json; do + [ -f "$f" ] && info "PV-W30 $f — authored for Deno (permitted, tier 2); package.json stays the one manifest every runtime reads" + done + for f in flake.nix flake.lock shell.nix default.nix; do [ -f "$f" ] && { warn "PV-W31 $f — Nix is not an estate toolchain; Guix is"; any=1; }; done + for f in Makefile GNUmakefile makefile; do [ -f "$f" ] && { warn "PV-W32 $f — Justfile is the only task runner"; any=1; }; done + for f in requirements.txt pyproject.toml setup.py Pipfile; do [ -f "$f" ] && { warn "PV-W33 $f — Python is not an estate language"; any=1; }; done + for f in package-lock.json yarn.lock tsconfig.json; do [ -f "$f" ] && { warn "PV-W34 $f — npm/yarn/TypeScript leftover; package.json + bun.lock, deno.lock and pnpm-lock.yaml (js-runtimes regen)"; any=1; }; done + [ $any -eq 0 ] && pass "no nix / make / python / npm leftovers" + + local hook=build/just/doctor-local.sh + if [ -f "$hook" ]; then + hdr "Repo-specific checks ($hook)" + # Sourced so the hook can call pass/warn/fail, but in a subshell so an `exit` + # or `set -e` in it cannot end the doctor before the summary. The EXIT trap + # hands the hook's tally back; an early exit is itself a FAIL. + local tally qtally hp w f rc + tally=$(mktemp) + printf -v qtally '%q' "$tally" + # shellcheck disable=SC2030,SC2031 # the subshell's counts return via $tally + ( PASS=0 WARN=0 FAIL=0 hook_done=0 + # The path is baked in now: when `set -e` trips, bash unwinds this + # function's locals before the EXIT trap runs, so $tally is gone by then. + # shellcheck disable=SC2064 + trap "printf '%d %d %d %d\n' \"\$PASS\" \"\$WARN\" \"\$FAIL\" \"\$hook_done\" > $qtally" EXIT + # shellcheck source=/dev/null + . "$hook" + hook_done=1 ) + rc=$? + read -r hp w f hook_done < "$tally" || { hp=0 w=0 f=0 hook_done=0; } + rm -f "$tally" + # shellcheck disable=SC2031 + PASS=$((PASS+hp)) WARN=$((WARN+w)) FAIL=$((FAIL+f)) + [ "$hook_done" = 1 ] || fail "PV-E51 $hook exited (status $rc) before it finished; its later checks did not run" + fi + if has_recipe doctor-local; then + hdr "Repo-specific checks (just doctor-local)" + just doctor-local && pass "doctor-local" || fail "PV-E50 the repo-specific doctor-local recipe failed (output above)" + fi + + printf '\n%s: %s%d PASS%s, %s%d WARN%s, %s%d FAIL%s\n' "$REPO_NAME" "$G" "$PASS" "$Z" "$Y" "$WARN" "$Z" "$R" "$FAIL" "$Z" + if [ "$FAIL" -gt 0 ]; then + # stderr, so the tally above stays the last line of stdout on every outcome. + echo "Next: 'just heal' fixes what is safe to fix automatically; each FAIL code is explained in docs/SETUP.adoc §Troubleshooting." >&2 + return 1 + fi + return 0 +} + +# Install available toolchains and dependencies, run local setup hooks, then doctor; +# return non-zero when a tracked installation step or verification fails. +# May trust mise configuration, extend PATH, initialise opam and make the +# launcher executable. Installation and hook failures do not skip verification. +cmd_setup() { + printf '%s setup — installing everything this repository needs\n' "$REPO_NAME" + local rc=0 + if have mise; then + hdr "mise toolchain" + $T mise trust -q . 2>/dev/null || true + mise install || rc=1 + else + warn "mise not found. Install it ($MISE_INSTALL_HINT), or enter the Guix shell with 'just dev-shell', then re-run: just setup" + fi + local l + for l in "${LANGS[@]}"; do + case "$l" in + idris2) have pack || warn "pack not found — Idris2 comes from pack, not mise: $(lang_remedy idris2)" ;; + haskell) have ghcup || have ghc || warn "ghc not found — $(lang_remedy haskell)" ;; + ocaml) have opam && { opam switch show >/dev/null 2>&1 || opam init -y --bare; } ;; + esac + done + hdr "Project dependencies" + # Put this repo's mise tools on PATH for the dependency step. + if have mise; then PATH="$(mise bin-paths 2>/dev/null | paste -sd: -):$PATH"; export PATH; fi + lang_run deps || rc=1 + local hook=build/just/setup-local.sh + [ -f "$hook" ] && { hdr "Repo-specific setup ($hook)"; bash "$hook" || rc=1; } + has_recipe setup-local && { hdr "Repo-specific setup (just setup-local)"; just setup-local || rc=1; } + [ -f launcher.sh ] && chmod +x launcher.sh + hdr "Verification" + cmd_doctor || rc=1 + [ $rc -eq 0 ] && printf '\n%sReady.%s Try: just --list\n' "$G" "$Z" || printf '\n%sSetup incomplete%s — read the FAIL lines above; docs/SETUP.adoc has the manual route.\n' "$R" "$Z" + return $rc +} + +# Apply automatic environment repairs and local heal hooks, then return doctor status. +# Earlier repair and hook failures are not propagated independently of doctor. +cmd_heal() { + printf '%s heal — applying safe, reversible fixes, then re-checking\n' "$REPO_NAME" + hdr "Fixes" + [ -f launcher.sh ] && [ ! -x launcher.sh ] && chmod +x launcher.sh && info "made launcher.sh executable" + for f in build/just/*.sh scripts/*.sh; do [ -f "$f" ] && [ ! -x "$f" ] && chmod +x "$f" && info "made $f executable"; done + if have mise; then + $T mise trust -q . 2>/dev/null && info "mise config trusted" + mise install && info "mise tools installed" + [ -f mise.lock ] || { $T mise lock 2>/dev/null && info "mise.lock generated"; } + fi + if printf '%s\n' "${LANGS[@]}" | grep -qx elixir && have mix; then mix deps.get >/dev/null 2>&1 && info "mix deps fetched"; fi + if printf '%s\n' "${LANGS[@]}" | grep -qx bun && have bun; then bun install >/dev/null 2>&1 && info "bun deps installed"; fi + if printf '%s\n' "${LANGS[@]}" | grep -qx julia && have julia; then julia --project=. -e 'using Pkg; Pkg.instantiate()' >/dev/null 2>&1 && info "julia deps instantiated"; fi + local hook=build/just/heal-local.sh + [ -f "$hook" ] && { info "running $hook"; bash "$hook" || true; } + has_recipe heal-local && { info "running just heal-local"; just heal-local || true; } + echo "Not touched (never automatic): source files, git history, Deno/Nix/Make leftovers — those are migrations, tracked as issues." + hdr "Re-check" + cmd_doctor +} + +# Replace this process with Guix when its manifest exists and has no {{ residue; +# otherwise use mise and SHELL (default bash). Return 1 if neither route is +# available; a failed exec terminates the script without trying the other route. +cmd_dev_shell() { + local gm; gm=$(gpath manifest.scm) + if have guix && [ -f "$gm" ] && ! grep -q '{{' "$gm"; then + info "entering: guix shell -m $gm (exit to leave)" + exec guix shell -m "$gm" + elif have mise; then + info "guix/manifest.scm unavailable — entering the mise environment instead (exit to leave)" + exec mise exec -- "${SHELL:-bash}" + else + fail "PV-E40 neither guix nor mise is installed — see docs/SETUP.adoc"; return 1 + fi +} + +# Print the 40-hex commit of the `guix` channel in a channels list read from +# stdin (`guix describe --format=channels`), or nothing when there is none. +guix_channel_commit() { + awk '/\(name .guix\)/ { g = 1 } + g && match($0, /\(commit "[0-9a-f]{40}"\)/) { print substr($0, RSTART + 9, 40); exit }' +} + +# Re-pin only the `guix` channel's commit in channels file $1 to $2, keeping +# every other line (comments, other channels, introduction) as it is. Returns +# non-zero, leaving the file untouched, when it has no guix channel commit. +repin_guix_channel() { + local ch=$1 pin=$2 tmp + tmp=$(mktemp) || return 1 + if awk -v pin="$pin" ' + /\(name .guix\)/ { g = 1 } + g && !done && sub(/\(commit "[0-9a-f]{40}"\)/, "(commit \"" pin "\")") { done = 1 } + { print } + END { exit !done }' "$ch" > "$tmp"; then + cat "$tmp" > "$ch"; rm -f "$tmp" + else + rm -f "$tmp"; return 1 + fi +} + +# Weekly toolchain refresh: bump mise pins and the lock, re-pin the Guix channel, +# regenerate build/guix/crates.scm when guix.scm loads it, then show the diff +# for a signed commit. +# Return 1 for missing mise or failed mise update, locking or crate regeneration. +# Channel pin failures are warnings; unavailable Guix operations are skipped. +# Earlier updates are not rolled back. +cmd_toolchain_refresh() { + hdr "mise: bump 'latest' resolutions and re-lock" + have mise || { fail "mise not found"; return 1; } + mise up --bump || return 1 + $T mise lock 2>/dev/null || mise lock || return 1 + local ch; ch=$(gpath channels.scm) + if [ -f "$ch" ]; then + hdr "guix: channel pin" + if have guix; then + local pin + pin=$(guix describe --format=channels 2>/dev/null | guix_channel_commit) + if [ -z "$pin" ]; then + warn "could not read the current guix channel commit — $ch left unchanged" + elif repin_guix_channel "$ch" "$pin"; then + info "$ch: guix channel re-pinned to $pin" + else + warn "$ch has no (name 'guix) channel with a commit — left unchanged" + fi + else info "guix not installed — $ch left as-is (CI re-pins it)"; fi + fi + local cr="" + if [ -f Cargo.lock ] && grep -qs 'crates\.scm' "$(gpath guix.scm)"; then + hdr "guix: crate inputs from Cargo.lock" + cr=build/guix/crates.scm + if have "${GUIX%% *}"; then cmd_crates_scm || return 1 + else info "guix not installed — $cr left as-is (CI regenerates it)"; fi + fi + git --no-pager diff --stat -- mise.toml mise.lock "$ch" $cr 2>/dev/null + echo "Commit the diff above (signed) as: chore(toolchain): weekly refresh" +} + +# The guix command (default: guix), e.g. a wrapper that runs it in a container +# with this directory mounted. +GUIX="${GUIX:-guix}" + +# Write build/guix/crates.scm: every registry crate in Cargo.lock as a Guix +# origin, and %crate-inputs listing them for guix.scm. Written whole or not at +# all. Accept importer output when its rust- definition count matches the number +# of registry packages in Cargo.lock; the importer's exit status is not checked. +# GUIX may contain a command and arguments; import is limited to 1,800 seconds. +# Missing Cargo.lock is a successful no-op. Return 1 for temporary-file creation +# failure or a count mismatch; later write errors are not reliably propagated. +cmd_crates_scm() { + local dst=build/guix/crates.scm want got tmp spdx + [ -f Cargo.lock ] || { info "no Cargo.lock — no crate inputs"; return 0; } + want=$(grep -c '^source = "registry+' Cargo.lock) + tmp=$(mktemp) || return 1 + if [ "$want" -gt 0 ]; then + # GUIX is split on purpose: it may be a command with arguments. + # shellcheck disable=SC2086 + timeout 1800 $GUIX import crate --lockfile=Cargo.lock "$REPO_NAME" >"$tmp" 2>"$tmp.err" + got=$(grep -c '^(define rust-' "$tmp") + if [ "$got" != "$want" ]; then + fail "PV-E41 guix import crate defined $got of the $want registry crates in Cargo.lock; $dst left as-is: $(grep -m1 -i 'error' "$tmp.err" || grep -v '^+' "$tmp.err" | tail -1)" + rm -f "$tmp" "$tmp.err"; return 1 + fi + fi + # The licence line is guix.scm's own, so the file matches its repository. + spdx=$(grep -m1 'SPDX-License-Identifier' "$(gpath guix.scm)" 2>/dev/null) + mkdir -p build/guix + { + [ -n "$spdx" ] && printf '%s\n' "$spdx" + # shellcheck disable=SC2016 # the backticks are literal text + printf ';; Generated from Cargo.lock by `just toolchain-refresh` (guix import crate\n' + printf ';; --lockfile); never hand-edit it. guix.scm loads it for %%crate-inputs.\n\n' + grep -v '^guix (GNU Guix)' "$tmp" + printf '\n(define %%crate-inputs\n (list' + grep -o '^(define rust-[^ ]*' "$tmp" | awk '{printf "\n %s", $2}' + printf '))\n' + } >"$dst.new" && mv "$dst.new" "$dst" + rm -f "$tmp" "$tmp.err" + info "$dst: $want crate source(s)" +} + +# The sentence people are told to say lives in one place a reader sees: the first +# listing block of the README's [[ai-install]] section. The deed's ai-say-it and +# the generic line are fallbacks for a README without that section. +just_say_it() { + local line="" r + for r in README.adoc README.md; do + [ -f "$r" ] || continue + line=$(awk '/^\[\[ai-install\]\]/{on=1} on && /^----$/{if (inb) exit; inb=1; next} inb && NF{print; exit}' "$r") + [ -n "$line" ] && break + done + [ -z "$line" ] && line=$(deed ai-say-it "") + [ -z "$line" ] && line="Set up $REPO_NAME from https://github.com/$REPO_SLUG — follow docs/AI_INSTALLATION_GUIDE.adoc in that repo." + printf '%s' "$line" +} +# Copy stdin to an available clipboard with a three-second timeout; +# silently consume input and return 1 when no clipboard command is available. +# Otherwise return the timeout command's status; suppress clipboard output and errors. +clip() { # best-effort; silent when no clipboard exists (CI, SSH) + # wl-copy and xclip fork a daemon that inherits stdout; left attached, it + # holds any pipeline open forever (observed 2026-09-30). Detach and bound it. + local c=() + if have clip.exe; then c=(clip.exe) # WSL: the Windows clipboard + elif have pbcopy; then c=(pbcopy) + elif [ -n "${WAYLAND_DISPLAY:-}" ] && have wl-copy; then c=(wl-copy) + elif [ -n "${DISPLAY:-}" ] && have xclip; then c=(xclip -selection clipboard) + else cat >/dev/null; return 1; fi + timeout 3 "${c[@]}" >/dev/null 2>&1 +} +# Print AI-assisted setup instructions and attempt to copy the setup sentence. +# Clipboard failure is ignored. +cmd_ai_setup() { + cat <" >&2; return 2 ;; esac + for f in "$(wpath "$who")" "llm-warmup-$who.adoc" "docs/onboarding/llm-warmup-$who.adoc" "docs/llm-warmup-$who.adoc"; do + if [ -f "$f" ]; then + cat "$f"; clip < "$f" && echo "--- (copied $f to your clipboard — paste it as your first message to any AI)" >&2 || true + return 0 + fi + done + fail "llm-warmup-$who.adoc not found"; return 1 +} + +# Run test and bench recipes, saving output and timings under .eval/; (hypatia:ignore eval_in_shell -- directory name) +# report N/A for skipped work and return 1 if either recipe fails. +cmd_eval() { + local logf rc=0 s e v r o st + logf=".eval/$(date -u +%Y%m%dT%H%M%SZ).txt" # hypatia:ignore eval_in_shell -- .eval/ is a directory name + mkdir -p .eval # hypatia:ignore eval_in_shell -- .eval/ is a directory name + { echo "# $REPO_NAME evaluation — $(date -u +%FT%TZ) — $(git rev-parse --short HEAD 2>/dev/null || echo no-git)" + echo "# languages: ${LANGS[*]} archetype: $ARCHETYPE"; } > "$logf" + for v in test bench; do + # The root recipe if the repo defines one (its own override), else the module default. + if just --summary 2>/dev/null | tr " " "\n" | grep -qx "$v"; then r=$v; else r="provision::$v"; fi + s=$(date +%s); o=$(mktemp) + if just "$r" >"$o" 2>&1; then st=PASS; else st=FAIL; rc=1; fi + e=$(( $(date +%s) - s )); cat "$o" >>"$logf" + # A skip is not a pass: lang_run exits 0 on N/A (users see success), eval says N/A. (hypatia:ignore eval_in_shell -- verb name) + [ "$st" = PASS ] && grep -q "nothing was faked" "$o" && st=N/A + rm -f "$o"; echo "$v: $st (${e}s)" | tee -a "$logf" + done + echo "full log: $logf" + return $rc +} + +# Print resolved repository metadata and provisioning configuration paths. +cmd_config_show() { + echo "repo: $REPO_SLUG" + echo "archetype: $ARCHETYPE" + echo "languages: ${LANGS[*]}" + echo "descriptor: $DEED $([ -f "$DEED" ] && echo '(present)' || echo '(absent — defaults in use)')" + echo "config: $(deed config "none declared")" + echo "run: $(deed run "$(for l in "${LANGS[@]}"; do lang_cmd "$l" run; done | head -1)")" + echo "ports: $(deed ports "none")" + echo "mise: $([ -f mise.toml ] && echo mise.toml) $([ -f mise.lock ] && echo mise.lock)" + echo "guix: $(for f in guix.scm manifest.scm channels.scm; do f=$(gpath "$f"); [ -f "$f" ] && printf "%s " "$f"; done)" + echo "lib: provision-lib $PROVISION_LIB_VERSION" +} + +# Print repository-specific OPSM installation instructions. +cmd_opsm() { + cat <} + if have agrep; then git ls-files -z | xargs -0 agrep -n -1 -- "$p" 2>/dev/null + else git grep -n -i -- "$p"; fi +} + +case "${1:-}" in + guix-specs) guix_specs ;; + mise-tools) shift; mise_tools "$@" ;; + langs) printf '%s\n' "${LANGS[@]}" ;; + doctor) cmd_doctor ;; + setup) cmd_setup ;; + heal) cmd_heal ;; + dev-shell) cmd_dev_shell ;; + toolchain-refresh) cmd_toolchain_refresh ;; + crates-scm) cmd_crates_scm ;; + ai-setup) cmd_ai_setup ;; + ai-warmup) shift; cmd_ai_warmup "${1:-user}" ;; + eval) cmd_eval ;; # hypatia:ignore eval_in_shell -- the "eval" verb, not the builtin + config-show) cmd_config_show ;; + opsm) cmd_opsm ;; + search) shift; cmd_search "${1:-}" ;; + lang-run) shift; lang_run "${1:?verb}" ;; + version) echo "provision-lib $PROVISION_LIB_VERSION" ;; + guix-dir) guix_dir ;; + set-files) set_files ;; + guix-gaps) guix_gaps ;; + tool-table) tool_table ;; + system-deps) shift; system_deps "${1:-adoc}" ;; + # Predicates: print why, exit 1; print nothing, exit 0. + guix-stub) shift; r=$(guix_stub_reason "${1:?usage: guix-stub FILE}"); [ -z "$r" ] || { echo "$r"; exit 1; } ;; + mise-lock-gaps) r=$(mise_lock_gaps); [ -z "$r" ] || { echo "$r"; exit 1; } ;; + mise-banned) r=$(mise_banned); [ -z "$r" ] || { echo "$r"; exit 1; } ;; + *) sed -n '2,20p' "$0"; exit 2 ;; +esac diff --git a/build/just/provision-modes.sh b/build/just/provision-modes.sh new file mode 100755 index 0000000..eb9efa2 --- /dev/null +++ b/build/just/provision-modes.sh @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# shellcheck shell=bash +# +# provision-modes.sh — the launcher-standard 0.6.0 provisioning mode family, +# as a SOURCEABLE block shared by every launcher in the estate. +# +# New launchers (library/tool/theory/docs): templates/launcher.sh.tmpl sources +# this and calls hp_launcher_main. +# Existing app launchers: add ONE line before their mode switch — +# +# . "$REPO_DIR/build/just/provision-modes.sh" && hp_provision_or_return "$@" +# +# A provisioning mode runs and the launcher exits with its status (a failed +# --setup exits non-zero); any other mode returns, so the app's own switch +# handles it. hp_provision_dispatch itself returns 99 for "not mine". +# +# Canon: hyperpolymath/standards launcher/launcher-standard_praxis.deed +# (archetypes, provisioning-modes) and +# 3-practice/provisioning/PROVISIONING-STANDARD.adoc + +HP_PROVISION_MODES_VERSION="0.6.0" + +# Print the flat deed value for key $1, falling back to default $2. +hp__deed() { # $1 key, $2 default — flat (key "value") read from provisioning_praxis.deed + local f="$REPO_DIR/.machine_readable/descriptiles/provisioning_praxis.deed" v="" + [ -f "$f" ] && v=$(grep -oE "[(:]$1[[:space:]]+\"[^\"]*\"" "$f" | head -1 | sed -E 's/^[^"]*"//; s/"$//') + printf '%s' "${v:-$2}" +} + +# Print the declared repository archetype, defaulting to library. +hp_archetype() { hp__deed archetype "library"; } +# Without a deed, the origin remote names the repo (a worktree or renamed clone +# has another directory name); the directory is the last resort. +hp_app_name() { + local u; u=$(git -C "$REPO_DIR" config --get remote.origin.url 2>/dev/null || true) + u=${u%.git}; u=${u##*/} + hp__deed name "${u:-$(basename "$REPO_DIR")}" +} + +# Print linux, macos, windows or unknown based on the host kernel name. +hp_platform() { + case "$(uname -s)" in + Linux*) echo linux ;; + Darwin*) echo macos ;; + CYGWIN*|MINGW*|MSYS*|Windows_NT) echo windows ;; + *) echo unknown ;; + esac +} + +# Make sure `just` is runnable: PATH, then mise, then say exactly what to do. +# May install just globally via mise, extend PATH or define a just wrapper. +# Return 0 when available, otherwise 1 after printing installation guidance. +hp_ensure_just() { + command -v just >/dev/null 2>&1 && return 0 + if command -v mise >/dev/null 2>&1; then + echo "just is not installed — installing it with mise (mise use -g just@latest)..." >&2 + mise use -g just@latest >&2 && PATH="$(mise bin-paths 2>/dev/null | paste -sd: -):$PATH" && command -v just >/dev/null 2>&1 && return 0 + mise exec just@latest -- true >/dev/null 2>&1 && { just() { mise exec just@latest -- just "$@"; }; return 0; } + fi + cat >&2 <<'EOF' +Neither `just` nor `mise` is installed. Install mise (it then installs everything else): + + brew install mise # macOS + sudo dnf copr enable jdxcode/mise && sudo dnf install mise # Fedora + winget install jdx.mise # Windows + # anything else (Debian/Ubuntu apt, …): https://mise.jdx.dev/installing-mise.html + # or download the installer, read it, then run it — never pipe it into a shell: + curl -fsSLo mise-install.sh https://mise.run && less mise-install.sh && sh mise-install.sh + +then re-run this command. Manual route without mise: docs/SETUP.adoc +EOF + return 1 +} + +# Ensure just is available, then run it with the supplied arguments in REPO_DIR. +hp_just() { hp_ensure_just || return 1; (cd "$REPO_DIR" && just "$@"); } +# The provisioning modes call the engine directly, not a `just` recipe: a repo may +# define its own root `doctor`/`setup`/`heal`, and the launcher must still run the +# canon (which then runs that repo's *-local recipes). Only needs bash. +hp_lib() { (cd "$REPO_DIR" && bash build/just/provision-lib.sh "$@"); } + +# The one-line hook for existing launchers: exit with a provisioning mode's +# status, or return (0) so the caller's own mode switch runs. +hp_provision_or_return() { + hp_provision_dispatch "$@" + local rc=$? + [ "$rc" -eq 99 ] && return 0 + exit "$rc" +} + +# Run the provisioning mode named by $1 and return its status; +# return 99 when the mode is not handled here. +hp_provision_dispatch() { + case "${1:-}" in + --setup) hp_lib setup ;; + --doctor) hp_lib doctor ;; + --heal) hp_lib heal ;; + --ai-setup) hp_lib ai-setup ;; + *) return 99 ;; + esac +} + +# Print the launcher name, version, commit, platform and architecture. +hp_version_line() { + local sha ver + sha=$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown) + ver=$(hp__deed version "") + [ -z "$ver" ] && ver=$(git -C "$REPO_DIR" describe --tags --abbrev=0 2>/dev/null || echo 0.0.0) + printf '%s-launcher %s (%s) [%s-%s]\n' "$(hp_app_name)" "${ver#v}" "$sha" "$(hp_platform)" "$(uname -m)" +} + +# Print launcher usage and the modes applicable to the repository archetype. +hp_help() { + local name arch; name=$(hp_app_name); arch=$(hp_archetype) + cat < + +Provisioning (every repository): + --setup Install everything this repository needs, then run --doctor + --doctor Check the environment; PASS/WARN/FAIL with fix hints (exit 1 on FAIL) + --heal Apply safe fixes automatically, then re-run --doctor + --ai-setup Print the one line to give any AI assistant to set this up for you + +Meta: + --help This text + --version Machine-readable version line + +EOF + if [ "$arch" = app ]; then + echo "Runtime: --start --stop --status --auto (default) --integ --disinteg" + else + echo "Runtime modes (--start --stop --status --auto --integ --disinteg) do not apply" + echo "to a $arch repository; they are accepted and explain themselves." + fi + cat <&2 + return 1 + fi + echo "$(hp_app_name) is a $arch: there is nothing to ${mode#--}. Try --setup, --doctor or --help." + return 0 ;; + *) + echo "Unknown mode: $mode" >&2; hp_help >&2; return 2 ;; + esac +} diff --git a/build/just/provision.just b/build/just/provision.just new file mode 100644 index 0000000..9f1ac7c --- /dev/null +++ b/build/just/provision.just @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# provision.just — the estate provisioning contract, as a just MODULE. +# +# Canon: hyperpolymath/standards 3-practice/provisioning/PROVISIONING-STANDARD.adoc +# Loaded from the root Justfile with: +# +# mod provision 'build/just/provision.just' +# +# A module (not `import`) so these recipes live under `provision::` and never +# collide with a repo's own `doctor`, `heal` or `test`. The root Justfile then +# exposes each contract name ONCE — either its own recipe, or a one-line +# delegation such as `doctor: provision::doctor` that `provision-set mint` +# adds only when the root has no recipe of that name. +# +# requires: just >= 1.42.0 (root recipes depend on module recipes; measured 2026-09-30: 1.41 rejects `x: m::x`, 1.42 accepts) + +root := justfile_directory() +lib := "PROVISION_ROOT=" + quote(root) + " bash " + quote(source_directory() / "provision-lib.sh") + +# List the provisioning recipes +default: + @just --list provision + +# Install every tool and dependency this repo needs, then run doctor +setup: + @{{lib}} setup + +# Diagnose the environment: PASS/WARN/FAIL with codes; exits 1 on any FAIL +doctor: + @{{lib}} doctor + +# Apply safe, reversible fixes, then re-run doctor +heal: + @{{lib}} heal + +# Enter a shell with every tool on PATH (guix shell -m manifest.scm, else mise) +dev-shell: + @{{lib}} dev-shell + +# Weekly: bump mise 'latest' resolutions, re-lock, re-pin guix channels +toolchain-refresh: + @{{lib}} toolchain-refresh + +# Print (and copy) the one line to give any AI to install this repo +ai-setup: + @{{lib}} ai-setup + +# Print (and copy) the warm-up for an AI session: user, dev or maintainer +ai-warmup who="user": + @{{lib}} ai-warmup {{quote(who)}} + +# Run test + bench, time both, and write .eval/.txt +eval: + @{{lib}} eval + +# Show where this repo's configuration and toolchain are declared +config-show: + @{{lib}} config-show + +# How to install this repo with OPSM (odds-and-sods package manager) +opsm: + @{{lib}} opsm + +# Print the languages detected from marker files +langs: + @{{lib}} langs + +# Approximate search across tracked files (agrep, else git grep) +search pattern: + @{{lib}} search {{quote(pattern)}} + +# Per-language defaults for the contract verbs, reached as `just provision::` +# or through a root recipe that delegates (`build: provision::build`). A root +# recipe with its own body is the repo-specific override. + +# Build with every detected language's toolchain +build: + @{{lib}} lang-run build + +# Run every detected language's tests +test: + @{{lib}} lang-run test + +# Run every detected language's benchmarks +bench: + @{{lib}} lang-run bench + +# Lint every detected language +lint: + @{{lib}} lang-run lint + +# Format every detected language's sources +fmt: + @{{lib}} lang-run fmt + +# Check formatting without rewriting anything (the CI form of fmt) +fmt-check: + @{{lib}} lang-run fmt-check + +# Run the project (N/A for libraries) +run: + @{{lib}} lang-run run + +# Fetch every detected language's dependencies +deps: + @{{lib}} lang-run deps diff --git a/build/just/repo-init.just b/build/just/repo-init.just index ab12190..e3690a2 100644 --- a/build/just/repo-init.just +++ b/build/just/repo-init.just @@ -14,12 +14,200 @@ repo-init archetype="": #!/usr/bin/env bash set -euo pipefail - # Non-interactive support: if RSR_NON_INTERACTIVE is set, dummy-out 'read'. - # This prevents blocking on input and seamlessly uses environment variables. - if [ -n "${RSR_NON_INTERACTIVE:-}" ]; then - read() { return 0; } + # ask PROMPT VAR — every prompt below goes through this, never through a + # bare `read`. Interactive: read VAR from the terminal. Non-interactive + # (RSR_NON_INTERACTIVE set, the scaffoldia / CI path): touch no stdin and + # keep VAR's value from the environment, or empty when it has none. + # + # This replaced `read() { return 0; }`, which shadowed the builtin for the + # WHOLE recipe. Measured 2026-10-14 minting resource-algebra: + # * a prompt the environment did not answer left its variable unset, so + # `set -u` killed the mint at OPENSSF_BP_ID, after docs/ had already + # been overwritten by the seed; + # * every `while read -r` loop below inherited the no-op, which succeeds + # without reading, so the substitution pass spun forever on an unbound + # `file`. + # The non-interactive mint could therefore never complete, and the e2e test + # drove only the interactive path, so nothing noticed. + ask() { + if [ -n "${RSR_NON_INTERACTIVE:-}" ]; then + printf -v "$2" '%s' "${!2:-}" + else + read -rp "$1" "$2" + fi + } + + # The mint edits only what belongs to the repository, so it needs git to + # say what that is (see mint_files below). Checked first: nothing is + # asked, fetched or written for a tree that cannot be minted. + if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "Error: repo-init must run inside a git work tree (run 'git init' first)." + exit 1 + fi + # The mint runs git against other repositories too (replays, a template + # clone, the tests it runs in scratch repositories), and an inherited + # repository location (GIT_DIR, GIT_INDEX_FILE, … from a hook or another git + # caller) would redirect every one of them into this repository: the + # replay's commits, the tests' fixture commits (CodeRabbit review of #275). + # So the location is cleared, once, here, for this recipe and everything it + # starts; one that named a different repository than this work tree's own + # is refused, since clearing it would mint something else. + _git_loc=(GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY + GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_PREFIX GIT_NAMESPACE) + _git_set=""; for _v in "${_git_loc[@]}"; do [ -z "${!_v+x}" ] || _git_set="${_git_set} ${_v}"; done + if [ -n "$_git_set" ]; then + _here_gd="$(git rev-parse --absolute-git-dir 2>/dev/null || true)" + _bare_gd="$(cd "$(git rev-parse --show-toplevel)" && env $(printf -- '-u %s ' "${_git_loc[@]}") git rev-parse --absolute-git-dir 2>/dev/null || true)" + if [ -z "$_bare_gd" ] || [ "$_here_gd" != "$_bare_gd" ]; then + echo "Error: mint aborted before writing anything — git location variables are set" + echo " (${_git_set# }) and name a repository other than this work tree's own." + echo " Run repo-init from the repository's work tree without them." + exit 1 + fi + unset "${_git_loc[@]}" + fi + + # --- The template's own identity --------------------------------------- + # repo-init is carried into every minted repo UNCHANGED (it is listed in + # .machine_readable/mint-verbatim.txt: it is the generator, not generated + # content), so it cannot assume it is running in rsr-template-repo. By the + # second generation the deed on disk is _chora.deed, the literal to + # rewrite is , and the provenance parent is /. + # Hardcoding the original template made a mint from a minted repo rename + # nothing, rewrite nothing, and abort at the identity step. Measured on + # metadatastician/lintel 2026-10-19; first fixed there in lintel#9 (not + # merged), ported here, where the generator lives. + # + # Derived, never guessed: these are facts about the checkout, not choices. + # The root Justfile's OWNER/REPO lines are the literals written through the + # tree, so they are exactly what the rewrite has to match, and they are + # still the TEMPLATE's in a repository created with "Use this template". + # When the Justfile does not declare them, RSR_TEMPLATE_SLUG=owner/repo + # must say so, or the mint stops. There is deliberately no fallback to the + # git remote: in a template-created child the remote is the CHILD, so that + # fallback named the child as its own template, and the pin resolver then + # recorded the child's HEAD (Codex review of #251). Nor is there a default + # template name: a guess here is the hardcoding this block replaced. + TEMPLATE_OWNER="" + TEMPLATE_REPO="" + if [ -f Justfile ]; then + TEMPLATE_REPO="$(sed -n -E "s/^(export[[:space:]]+)?REPO[[:space:]]*:=[[:space:]]*[\"']([^\"']+)[\"'].*/\2/p" Justfile | head -1)" + TEMPLATE_OWNER="$(sed -n -E "s/^(export[[:space:]]+)?OWNER[[:space:]]*:=[[:space:]]*[\"']([^\"']+)[\"'].*/\2/p" Justfile | head -1)" + fi + if [ -z "$TEMPLATE_REPO" ] || [ -z "$TEMPLATE_OWNER" ]; then + if [[ "${RSR_TEMPLATE_SLUG:-}" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then + TEMPLATE_OWNER="${RSR_TEMPLATE_SLUG%%/*}" + TEMPLATE_REPO="${RSR_TEMPLATE_SLUG#*/}" + else + echo "Error: the root Justfile does not declare OWNER := and REPO :=, so the" + echo " template this tree came from is unknown. Set RSR_TEMPLATE_SLUG=owner/repo." + exit 1 + fi + fi + TEMPLATE_SLUG="${TEMPLATE_OWNER}/${TEMPLATE_REPO}" + + # --- A template is minted; a minted repo is minted as a chain ------------ + # A minted repo has already filled its tokens and deleted build/templates, + # so minting it like a template renders none of the new answers and skips + # the identity splice: the grandchild silently inherited its parent's + # author, contacts, clade and maturity (Codex review of #251). So it is + # never minted that way: it is chain-minted, or refused, before anything is + # asked or written. A variant template keeps build/templates and still mints. + # + # Chain minting (owner ruling 2026-11-04): a minted repo that recorded its + # (mint-answers) CAN be minted again. scripts/chain-mint.sh replays its + # mint, mints the same template commit with the new answers, and carries + # its own work across by a three-way merge. One minted before answers were + # recorded cannot be replayed, so it is still refused. + # + # Which it is, the deed's recorded mint state decides first: a template + # (or a variant template, which is maintained, never minted) records no + # (mint-answers). A minted repo that added build/templates/identity.deed.in + # back as its own work is still a minted repo (Codex review of #277). + # The recorded state alone decides; the chain script's presence does not: + # a minted repo that deleted scripts/chain-mint.sh is still minted, and + # minting it as a template would keep its author and contacts (Codex + # review of #277). It is refused instead. + CHAIN_MINT=0 + _prov_deed="$(bash scripts/deed-field.sh --find . 2>/dev/null || true)" + if [ -n "$_prov_deed" ] && bash scripts/deed-field.sh --has "$_prov_deed" mint-answers \ + && [ ! -f scripts/chain-mint.sh ]; then + echo "Error: this tree is a minted repo (its deed records (mint-answers)), but" + echo " scripts/chain-mint.sh, which mints a minted repo again, is absent." + echo " Restore it from the template, or mint from the template itself." + exit 1 + elif [ -n "$_prov_deed" ] && bash scripts/deed-field.sh --has "$_prov_deed" mint-answers; then + CHAIN_MINT=1 + echo "This tree is a minted repo (${TEMPLATE_SLUG}): it will be minted as a chain," + echo "from its template, carrying its own work since its mint." + echo "" + elif [ ! -f build/templates/identity.deed.in ]; then + echo "Error: this tree has already been minted (build/templates/identity.deed.in is" + echo " absent) and records no (mint-answers), so its mint cannot be replayed" + echo " and it cannot be minted again. Mint from the template it came from." + if [ -n "$_prov_deed" ]; then + _prov_from="$(bash scripts/deed-field.sh "$_prov_deed" provenance template-repo 2>/dev/null || true)" + [ -z "$_prov_from" ] || echo " This tree records its template as ${_prov_from}." + fi + exit 1 fi + # --- What the mint may edit --------------------------------------------- + # mint_files prints, one per line, every regular file that belongs to the + # repository: tracked, or untracked and not ignored (what `git add -A` + # would commit), minus the files .machine_readable/mint-verbatim.txt says + # the mint carries unchanged. Every rewrite pass and every completeness + # check below reads this list, and nothing else. + # + # The passes used to `find .` the whole directory. That walked git-ignored + # caches: minting lintel, the instruction-block pass edited the pinned + # sibling checkouts under _build/dependencies/, which broke those pins. It + # also rewrote template machinery whose fixtures must keep the template's + # literal name, so tests/workflows/self_name_pass_test.sh failed in the + # child, and stripped the very fixture blocks the leftover check below + # exempts. "Untracked and not ignored" is included so that a copy which + # was never committed (tests/e2e/template_instantiation_test.sh) still + # mints; in a repository created from the template every file is tracked. + # + # Paths are printed relative and without "./"; symlinks are skipped, as + # `find -type f` skipped them. + VERBATIM_LIST=".machine_readable/mint-verbatim.txt" + [ -f "$VERBATIM_LIST" ] || { echo "Error: $VERBATIM_LIST is missing; the mint cannot tell machinery from content."; exit 1; } + mapfile -t MINT_VERBATIM < <(sed -e 's/#.*//' -e 's/[[:space:]]*$//' "$VERBATIM_LIST" | grep -v '^$') + # These two preflights judge a TEMPLATE tree. A chain mint's tree is a + # minted repo whose own work (a deleted verbatim-listed file, an added file + # with a brace token) is the parent's business; the root template is + # checked when chain-mint.sh replays it, and the merged result passes the + # placeholder gate (Codex review of #277). + if [ "$CHAIN_MINT" != "1" ]; then + for _v in "${MINT_VERBATIM[@]}"; do + [ -f "$_v" ] || { echo "Error: $VERBATIM_LIST names $_v, which does not exist (stale entry)."; exit 1; } + done + fi + + # ── Can the template be minted at all? ────────────────────────────────── + # A brace token the recipe below has no answer for is a file that cannot be + # minted: it either survives into the child as a literal (measured: five + # files in every Julia repo minted before 2026-10-30) or trips the + # placeholder gate at the END of a mint that has already written the tree. + # Both are cheaper to refuse here, before a single byte is written. + if [ "$CHAIN_MINT" != "1" ] && [ -f scripts/check-mint-tokens.sh ]; then + if ! bash scripts/check-mint-tokens.sh; then + echo "ERROR: mint aborted before writing anything — the template ships a" + echo " token this recipe cannot fill. See the findings above." + exit 1 + fi + fi + mint_files() { + git ls-files -z --cached --others --exclude-standard \ + | while IFS= read -r -d '' _f; do + [ -f "$_f" ] && [ ! -L "$_f" ] || continue + printf '%s\n' "$_f" + done \ + | grep -vxF -f <(printf '%s\n' "${MINT_VERBATIM[@]}") \ + | sort -u || true + } + echo "═══════════════════════════════════════════════════" echo " RSR Project Bootstrap" echo "═══════════════════════════════════════════════════" @@ -29,14 +217,32 @@ repo-init archetype="": ARCHETYPE='{{archetype}}' ARCH_FILE="" ARCH_PRESET="" - if [ -n "$ARCHETYPE" ]; then - ARCH_FILE="archetypes/${ARCHETYPE}/ARCHETYPE.a2ml" + # A chain mint has no archetypes/ (the parent's mint removed it); it + # re-mints with the archetype the parent recorded, from the root template + # (scripts/chain-mint.sh). Naming that same archetype is accepted; any + # other is refused, since the parent's tree was built for its own. + # MINT_ARCH is the archetype the mint will actually apply: on a chain, the + # parent's, whether or not it was named (the slug rule below depends on it: + # a Julia package's .jl slug). + MINT_ARCH="$ARCHETYPE" + if [ "$CHAIN_MINT" = "1" ]; then + _p_arch="$(bash scripts/deed-field.sh "$_prov_deed" mint-answers archetype 2>/dev/null || true)" + if [ -n "$ARCHETYPE" ] && [ "$ARCHETYPE" != "$_p_arch" ]; then + echo "Error: this tree was minted as archetype '${_p_arch:-none}'; a chain mint keeps it." + echo " Run just repo-init${_p_arch:+ ${_p_arch}} (or mint from the template for another)." + exit 1 + fi + [ -z "$_p_arch" ] || { echo "Archetype: ${_p_arch} (the parent's, re-minted from the template)"; echo ""; } + MINT_ARCH="$_p_arch" + ARCHETYPE="" + elif [ -n "$ARCHETYPE" ]; then + ARCH_FILE="archetypes/${ARCHETYPE}/${ARCHETYPE}_praxis.deed" if [ ! -f "$ARCH_FILE" ]; then echo "Error: unknown archetype '${ARCHETYPE}'. Available:" ls -1 archetypes 2>/dev/null | grep -v 'README' || echo " (none)" exit 1 fi - ARCH_PRESET=$(sed -n 's/^preset = "\(.*\)"$/\1/p' "$ARCH_FILE" | head -1) + ARCH_PRESET=$(bash scripts/deed-field.sh "$ARCH_FILE" capability preset || true) echo "Archetype: ${ARCHETYPE}${ARCH_PRESET:+ (capability preset: $ARCH_PRESET)}" echo "" fi @@ -44,6 +250,7 @@ repo-init archetype="": # --- Load defaults from config (if exists) --- # Create yours: ~/.config/rsr/defaults # Format: OWNER=myorg AUTHOR="My Name" AUTHOR_EMAIL=me@example.org ... + # AUTHOR_ORCID=0000-0002-1825-0097 (optional) DEFAULTS="${XDG_CONFIG_HOME:-$HOME/.config}/rsr/defaults" if [ -f "$DEFAULTS" ]; then echo "Loading defaults from $DEFAULTS" @@ -53,75 +260,141 @@ repo-init archetype="": fi # --- Required values (pre-filled from defaults if available) --- - read -rp "Project name (human-readable, e.g. My Project): " PROJECT_NAME + ask "Project name (human-readable, e.g. My Project): " PROJECT_NAME [ -z "$PROJECT_NAME" ] && echo "Error: project name required" && exit 1 - read -rp "Repository slug (e.g. my-project): " REPO + ask "Repository slug (e.g. my-project): " REPO [ -z "$REPO" ] && echo "Error: repo slug required" && exit 1 - if [[ ! "$REPO" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then + # A Julia package repo is named after its package: `.jl`, where + # is the package's own identifier (UpperCamelCase by Julia + # convention). The julia-library archetype accepts that form as well as + # the estate slug; every other mint takes the estate slug only. + if [[ "$MINT_ARCH" = "julia-library" && "$REPO" =~ ^[A-Z][A-Za-z0-9]*\.jl$ ]]; then + : + elif [[ ! "$REPO" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then echo "Error: repo slug must be lowercase alphanumeric words separated by single hyphens" + [ "$MINT_ARCH" = "julia-library" ] && echo " (or, for a Julia package, .jl with in UpperCamelCase)" exit 1 fi - read -rp "Owner [${OWNER:-}]: " _OWNER + ask "Owner [${OWNER:-}]: " _OWNER OWNER="${_OWNER:-${OWNER:-}}" [ -z "$OWNER" ] && echo "Error: owner required" && exit 1 - read -rp "Author full name [${AUTHOR:-}]: " _AUTHOR + ask "Author full name [${AUTHOR:-}]: " _AUTHOR AUTHOR="${_AUTHOR:-${AUTHOR:-}}" [ -z "$AUTHOR" ] && echo "Error: author name required" && exit 1 - read -rp "Author email [${AUTHOR_EMAIL:-}]: " _AUTHOR_EMAIL + ask "Author email [${AUTHOR_EMAIL:-}]: " _AUTHOR_EMAIL AUTHOR_EMAIL="${_AUTHOR_EMAIL:-${AUTHOR_EMAIL:-}}" [ -z "$AUTHOR_EMAIL" ] && echo "Error: email required" && exit 1 # --- Optional values (pre-filled from defaults if available) --- - read -rp "Author organization [${AUTHOR_ORG:-none}]: " _AUTHOR_ORG + ask "Author organization [${AUTHOR_ORG:-none}]: " _AUTHOR_ORG AUTHOR_ORG="${_AUTHOR_ORG:-${AUTHOR_ORG:-}}" - read -rp "Previous/alt email [${AUTHOR_EMAIL_ALT:-none}]: " _AUTHOR_EMAIL_ALT + ask "Previous/alt email [${AUTHOR_EMAIL_ALT:-none}]: " _AUTHOR_EMAIL_ALT AUTHOR_EMAIL_ALT="${_AUTHOR_EMAIL_ALT:-${AUTHOR_EMAIL_ALT:-}}" - read -rp "Project description []: " PROJECT_DESCRIPTION + # ORCID iD for CITATION.cff. Defaults from AUTHOR_ORCID in the defaults + # file, then `git config user.orcid`, so an author sets it once: + # git config --global user.orcid 0000-0002-1825-0097 + # Blank drops the orcid line below. An all-zeros placeholder is worse than + # no line: citation tools resolve it and attribute the work to nobody. + AUTHOR_ORCID="${AUTHOR_ORCID:-$(git config --get user.orcid 2>/dev/null || true)}" + ask "Author ORCID iD [${AUTHOR_ORCID:-none}]: " _AUTHOR_ORCID + AUTHOR_ORCID="${_AUTHOR_ORCID:-${AUTHOR_ORCID:-}}" + AUTHOR_ORCID="${AUTHOR_ORCID#http*://orcid.org/}" + if [ -n "$AUTHOR_ORCID" ]; then + if [[ ! "$AUTHOR_ORCID" =~ ^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$ ]]; then + echo "Error: ORCID iD must look like 0000-0002-1825-0097 (got '${AUTHOR_ORCID}')" + exit 1 + fi + # ISO 7064 MOD 11-2 check digit, as ORCID specifies: a transposed + # digit is a valid-looking iD that belongs to someone else. + _orcid_digits="${AUTHOR_ORCID//-/}" + _orcid_total=0 + for ((i = 0; i < 15; i++)); do + _orcid_total=$(( (_orcid_total + ${_orcid_digits:i:1}) * 2 )) + done + _orcid_check=$(( (12 - _orcid_total % 11) % 11 )) + [ "$_orcid_check" = 10 ] && _orcid_check=X + if [ "$_orcid_check" != "${_orcid_digits:15:1}" ]; then + echo "Error: ORCID iD ${AUTHOR_ORCID} fails its check digit (expected ${_orcid_check})" + exit 1 + fi + fi + + ask "Project description []: " PROJECT_DESCRIPTION - read -rp "Forge domain [${FORGE:-github.com}]: " _FORGE + ask "Forge domain [${FORGE:-github.com}]: " _FORGE FORGE="${_FORGE:-${FORGE:-github.com}}" - read -rp "Security contact email [${SECURITY_EMAIL:-$AUTHOR_EMAIL}]: " _SECURITY_EMAIL + ask "Security contact email [${SECURITY_EMAIL:-$AUTHOR_EMAIL}]: " _SECURITY_EMAIL SECURITY_EMAIL="${_SECURITY_EMAIL:-${SECURITY_EMAIL:-$AUTHOR_EMAIL}}" - read -rp "Conduct contact email [${CONDUCT_EMAIL:-$AUTHOR_EMAIL}]: " _CONDUCT_EMAIL + ask "Conduct contact email [${CONDUCT_EMAIL:-$AUTHOR_EMAIL}]: " _CONDUCT_EMAIL CONDUCT_EMAIL="${_CONDUCT_EMAIL:-${CONDUCT_EMAIL:-$AUTHOR_EMAIL}}" - read -rp "Project type (library|binary|monorepo|service|website) [library]: " PROJECT_TYPE + ask "Project type (library|binary|monorepo|service|website) [library]: " PROJECT_TYPE PROJECT_TYPE="${PROJECT_TYPE:-library}" - read -rp "Website URL [https://${FORGE}/${OWNER}/${REPO}]: " WEBSITE + ask "Website URL [https://${FORGE}/${OWNER}/${REPO}]: " WEBSITE WEBSITE="${WEBSITE:-https://${FORGE}/${OWNER}/${REPO}}" - read -rp "OpenSSF Best Practices project ID (blank if not yet registered) []: " OPENSSF_BP_ID + ask "OpenSSF Best Practices project ID (blank if not yet registered) []: " OPENSSF_BP_ID # --- Container values (optional — only relevant if build/container/ exists) --- + # Derive the default for a DNS hostname and bare TOML service key, not + # only an OCI image name: Julia dots and slug underscores need hyphens. + # Explicit service answers still go through the validator unchanged. + DEFAULT_SERVICE_NAME="${REPO,,}" + DEFAULT_SERVICE_NAME="${DEFAULT_SERVICE_NAME//[._]/-}" if [ -d "build/container" ]; then echo "" echo "── Container configuration (optional) ─────────" - read -rp "Service name [${REPO}]: " _SERVICE_NAME - SERVICE_NAME="${_SERVICE_NAME:-${REPO}}" - read -rp "Primary port [8080]: " _PORT - PORT="${_PORT:-8080}" - read -rp "Container registry [ghcr.io/${OWNER}]: " _REGISTRY - REGISTRY="${_REGISTRY:-ghcr.io/${OWNER}}" + ask "Service name [${SERVICE_NAME:-$DEFAULT_SERVICE_NAME}]: " _SERVICE_NAME + SERVICE_NAME="${_SERVICE_NAME:-${SERVICE_NAME:-$DEFAULT_SERVICE_NAME}}" + ask "Primary port [${PORT:-8080}]: " _PORT + PORT="${_PORT:-${PORT:-8080}}" + ask "Container registry [${REGISTRY:-ghcr.io/${OWNER,,}}]: " _REGISTRY + REGISTRY="${_REGISTRY:-${REGISTRY:-ghcr.io/${OWNER,,}}}" + # The values fill double-quoted KYAML scalars: refuse a value that + # would break them now, before anything is written. + bash scripts/format-container-kyaml.sh --check-values "$SERVICE_NAME" "$PORT" "$REGISTRY" \ + || { echo "ERROR: mint aborted before writing anything — see above."; exit 1; } else - SERVICE_NAME="${REPO}" - PORT="8080" - REGISTRY="ghcr.io/${OWNER}" + SERVICE_NAME="${SERVICE_NAME:-$DEFAULT_SERVICE_NAME}" + PORT="${PORT:-8080}" + REGISTRY="${REGISTRY:-ghcr.io/${OWNER,,}}" fi # --- Derived values --- - PROJECT_UPPER=$(echo "$REPO" | tr '[:lower:]-' '[:upper:]_') - PROJECT_LOWER=$(echo "$REPO" | tr '[:upper:]-' '[:lower:]_') - CURRENT_YEAR=$(date +%Y) - CURRENT_DATE=$(date +%Y-%m-%d) + # The PROJECT and project tokens land in identifiers (Zig, Idris, C macros), + # so every character an identifier cannot hold maps to an underscore: the + # hyphens of a slug and the dot of a Julia slug alike. + PROJECT_UPPER=$(echo "$REPO" | tr '[:lower:]-.' '[:upper:]__') + PROJECT_LOWER=$(echo "$REPO" | tr '[:upper:]-.' '[:lower:]__') + # The mint date is an input like any answer: RSR_MINT_DATE replays a mint + # exactly (scripts/check-mint-drift.sh re-mints a child with the date it + # recorded). Otherwise it is today. The year always follows the date. + CURRENT_DATE="${RSR_MINT_DATE:-$(date +%Y-%m-%d)}" + # Validated in bash, not with `date -d`: that is GNU-only, and macOS's date + # rejected every date, today's included (Codex review of #275). + _dm=0 + if [[ "$CURRENT_DATE" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then + _yy=$((10#${BASH_REMATCH[1]})); _mm=$((10#${BASH_REMATCH[2]})); _dd=$((10#${BASH_REMATCH[3]})) + case "$_mm" in + 1|3|5|7|8|10|12) _dm=31 ;; + 4|6|9|11) _dm=30 ;; + 2) if (( (_yy % 4 == 0 && _yy % 100 != 0) || _yy % 400 == 0 )); then _dm=29; else _dm=28; fi ;; + esac + fi + if [ "$_dm" -eq 0 ] || [ "${_dd:-0}" -lt 1 ] || [ "${_dd:-0}" -gt "$_dm" ]; then + echo "Error: RSR_MINT_DATE must be a real date, YYYY-MM-DD (got '${CURRENT_DATE}')." + exit 1 + fi + CURRENT_YEAR="${CURRENT_DATE:0:4}" VERSION="0.1.0" # --- Derive the repo's uuid -------------------------------------------- @@ -131,11 +404,19 @@ repo-init archetype="": # We know OWNER and REPO, so this is computable here and must never be # guessed or copied. The owner segment is part of the derived name, so it is # part of the identity — a repo hosted elsewhere derives a different uuid. - if command -v uuidgen >/dev/null 2>&1; then - REPO_UUID=$(uuidgen --sha1 --namespace @url --name "${FORGE}/${OWNER}/${REPO}") - else - REPO_UUID="UNASSIGNED" - fi + # Derived here, the same way on every machine: it was UNASSIGNED without + # uuidgen, so a mint's bytes depended on the machine and the drift gate's + # replay (which has uuidgen) reported an unchanged child as drift (Codex + # review of #275); and macOS's uuidgen has no --sha1 at all. RFC 9562 + # §5.5: SHA-1 over the URL namespace's 16 bytes then the name, truncated + # to 16 bytes, version 5 and the RFC variant set. It equals + # `uuidgen --sha1 --namespace @url --name …` (scripts/uuid5-url.sh, tested by + # tests/workflows/uuid5_url_test.sh). + REPO_UUID="$(bash scripts/uuid5-url.sh "${FORGE}/${OWNER}/${REPO}")" || { + echo "Error: the repo uuid is derived (UUIDv5), and this machine has none of" + echo " sha1sum, shasum or openssl to derive it with." + exit 1 + } # Derive citation name parts (best-effort split on last space) AUTHOR_LAST="${AUTHOR##* }" @@ -154,21 +435,423 @@ repo-init archetype="": echo " Owner: $OWNER" echo " Author: $AUTHOR <$AUTHOR_EMAIL>" [ -n "$AUTHOR_ORG" ] && echo " Organization: $AUTHOR_ORG" + [ -n "$AUTHOR_ORCID" ] && echo " ORCID: $AUTHOR_ORCID" echo " Forge: $FORGE" echo " Year: $CURRENT_YEAR" echo "────────────────────────────────────────────────" echo "" - read -rp "Proceed? [Y/n] " CONFIRM + ask "Proceed? [Y/n] " CONFIRM [[ "${CONFIRM:-Y}" =~ ^[Nn] ]] && echo "Aborted." && exit 0 + # An answer is one line: no sed expression, deed string or prompt can carry + # a newline. Refused HERE, before the first write (the overlay, the prune + # and the docs seed all come next), so a bad answer leaves a clean tree. + for _ans in PROJECT_NAME PROJECT_DESCRIPTION OWNER AUTHOR AUTHOR_EMAIL AUTHOR_ORG \ + AUTHOR_LAST AUTHOR_FIRST FORGE SECURITY_EMAIL CONDUCT_EMAIL PROJECT_TYPE \ + WEBSITE SERVICE_NAME PORT REGISTRY AUTHOR_EMAIL_ALT AUTHOR_ORCID OPENSSF_BP_ID; do + case "${!_ans:-}" in + *$'\n'*) echo "Error: the answer for $_ans contains a newline; answers are one line."; exit 1 ;; + # 0x01 is the record separator deed-field.sh and deed-clause.sh read the + # deed with: an answer holding it renders everywhere, then cannot be + # recorded in (mint-answers) (Codex review of #275). + *$'\001'*) echo "Error: the answer for $_ans contains byte 0x01, which the deed cannot record."; exit 1 ;; + esac + done + + # Answers are free text, and the right-hand side of s|…|…| is not: `&` + # inserts the matched token, `\` escapes, `|` ends the expression. A + # description "Tools & proofs" rendered as "Tools proofs" and + # then failed the placeholder gate; a `|` broke the substitution. Every + # answer goes through sed_rhs. (A newline was refused before the first write.) + # Defined here, ahead of the rules below that use it. + sed_rhs() { printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'; } + + # The self-name rule, defined once: used by the self-name pass below and + # handed to scripts/chain-mint.sh, so a chain mint renames the parent by + # exactly the same rule (passed as data, never re-read from the recipe). + SELF_PROV='instantiated-from|parent|upstream|chain =|lineage|minted from|created from|Template: |reject-if-contains' + # The template's name and slug are LITERAL text, not patterns: a Julia + # mint's parent may be .jl, and `.` in a sed pattern matches any + # character. Escaped inline so self_name_pass_test.sh can evaluate this + # one assignment exactly as the recipe defines it. + # The name is matched only where it stands as a whole name: not inside + # a longer word or slug (Codex review of #251). Unbounded, a parent + # named `app` turned "application mapping" into "newlication mnewing". + # A neighbouring letter or digit blocks the match; anything else does + # not, so `_` (sonar.projectKey), `_chora.deed` and + # derived names such as `-checkout` or `rm-` still follow. Each pass + # loops (:label … t label) because a match consumes its right-hand + # neighbour, and marks through a sentinel so a new name that contains + # the old one (Geometry -> Geometry.jl) cannot loop forever. + SELF_NAME_SED="/${SELF_PROV}/!{:i;s@(^|[^A-Za-z0-9])${TEMPLATE_REPO//./\\.}#([0-9])@\\1\\x01I\\x01\\2@;ti;:s;s@(^|[^A-Za-z0-9])${TEMPLATE_SLUG//./\\.}([^A-Za-z0-9]|$)@\\1\\x01S\\x01\\2@;ts;:n;s@(^|[^A-Za-z0-9])${TEMPLATE_REPO//./\\.}([^A-Za-z0-9]|$)@\\1\\x01N\\x01\\2@;tn;s@\\x01S\\x01@${OWNER}/${REPO}@g;s@\\x01N\\x01@${REPO}@g;s@\\x01I\\x01@${TEMPLATE_REPO}#@g;}" + # The self-owner rule, defined once likewise (see the self-OWNER pass for + # why it is three exact identity lines and never a blanket rewrite): the + # Justfile OWNER declaration in each form the identity parser accepts, and + # the SonarCloud organization and project key. chain-mint.sh applies it to + # the parent's carried files too (Codex review of #277). + SELF_OWNER_SED="s|^((export[[:space:]]+)?OWNER[[:space:]]*:=[[:space:]]*)([\"'])${TEMPLATE_OWNER//./\\.}([\"'])|\\1\\3$(sed_rhs "${OWNER}")\\4|;s|^sonar\\.organization=${TEMPLATE_OWNER//./\\.}\$|sonar.organization=$(sed_rhs "${OWNER}")|;s|^sonar\\.projectKey=${TEMPLATE_OWNER//./\\.}_|sonar.projectKey=$(sed_rhs "${OWNER}")_|" + + # Chain mint: everything below renders a TEMPLATE tree, which this is not. + # chain-mint.sh renders the template afresh with these answers and carries + # this tree's own work across; it writes nothing unless it succeeds. + if [ "$CHAIN_MINT" = "1" ]; then + export PROJECT_NAME REPO OWNER AUTHOR AUTHOR_EMAIL AUTHOR_ORG AUTHOR_EMAIL_ALT \ + AUTHOR_ORCID PROJECT_DESCRIPTION FORGE SECURITY_EMAIL CONDUCT_EMAIL \ + PROJECT_TYPE WEBSITE OPENSSF_BP_ID SERVICE_NAME PORT REGISTRY \ + TEMPLATE_REPO TEMPLATE_OWNER + export RSR_MINT_DATE="$CURRENT_DATE" SELF_NAME_SED SELF_OWNER_SED + bash scripts/chain-mint.sh + echo "" + echo "Done! Chain mint complete. Review with git status, then commit." + exit 0 + fi + + # Template-only paths this recipe deletes, as they stand before it runs. + # Recorded in the (variant) clause's (paths :removed …) below, so the + # record says what the mint took away instead of leaving the drift gate to + # report each file as an undeclared removal (18 lines on a fresh mint). + MINT_REMOVES=() + for _d in archetypes build/docs-seed build/templates; do + [ -d "$_d" ] && MINT_REMOVES+=("$_d/") + done + + # --- Which template commit is in hand (provenance) --------------------- + # Resolved BEFORE anything is written, so an unresolvable parent stops the + # mint with the tree untouched instead of half-minted. The child's own HEAD + # is never recorded as the template's: in a repository created with "Use + # this template" it is a commit of the child (lintel recorded its own + # claude/mint-lintel tip, 2026-10-14). scripts/resolve-template-pin.sh owns + # the rule and says why; tests/workflows/resolve_template_pin_test.sh + # holds its controls. + # RSR_TEMPLATE_URL read the template's history from here instead + # RSR_TEMPLATE_COMMIT assert the template commit (verified to exist) + # RSR_ALLOW_UNPINNED=1 record UNASSIGNED instead of aborting (offline) + PIN_ARGS=(--slug "$TEMPLATE_SLUG") + [ -z "${RSR_TEMPLATE_URL:-}" ] || PIN_ARGS+=(--url "$RSR_TEMPLATE_URL") + [ -z "${RSR_TEMPLATE_COMMIT:-}" ] || PIN_ARGS+=(--commit "$RSR_TEMPLATE_COMMIT") + if PIN_OUT="$(bash scripts/resolve-template-pin.sh "${PIN_ARGS[@]}")"; then + PARENT_BRANCH="$(sed -n 's/^branch=//p' <<<"$PIN_OUT")" + PARENT_PIN="$(sed -n 's/^commit=//p' <<<"$PIN_OUT")" + PARENT_TREE="$(sed -n 's/^tree=//p' <<<"$PIN_OUT")" + echo "Template: ${TEMPLATE_SLUG}@${PARENT_PIN:0:12} ($(sed -n 's/^source=//p' <<<"$PIN_OUT"))" + elif [ "${RSR_ALLOW_UNPINNED:-0}" = "1" ]; then + PARENT_BRANCH=UNASSIGNED PARENT_PIN=UNASSIGNED PARENT_TREE=UNASSIGNED + echo "WARNING: template commit UNASSIGNED (RSR_ALLOW_UNPINNED=1)." + echo " scripts/check-template-conformance.sh fails until it is filled." + else + echo "ERROR: mint aborted before writing anything — the template commit this" + echo " tree came from cannot be named (reason above). Re-run with" + echo " RSR_ALLOW_UNPINNED=1 only if an unpinned mint is genuinely wanted." + exit 1 + fi + + # --- What this work tree holds beyond the pinned tree ------------------- + # The mint transforms the work tree as it stands; the drift gate replays + # from the pinned tree alone. Anything already changed here before the + # mint (an edit, an untracked file, a deletion) is this repo's own, not + # the mint's, so it is measured now and declared in (variant (paths)): + # the replay stays exact and the record says what happened (Codex review + # of #275). Measured in a scratch index and object store, so .git is not + # written. (A chain mint carries its parent's tree by its own rules.) + PREMINT_A=() PREMINT_M=() PREMINT_D=() + if [ "${CHAIN_MINT:-0}" != "1" ] && [ "$PARENT_TREE" != "UNASSIGNED" ]; then + # The work tree as `git add -A` would commit it, from the current index + # (so a tracked file an ignore rule matches stays), in a scratch index + # and object store. + _pm="$(mktemp -d)"; mkdir -p "$_pm/objects" + _gd="$(cd "$(git rev-parse --git-common-dir)" && pwd)" + _ix="$(git rev-parse --git-path index)" + [ ! -f "$_ix" ] || cp "$_ix" "$_pm/index" + _wt() { GIT_INDEX_FILE="$_pm/index" GIT_OBJECT_DIRECTORY="$_pm/objects" GIT_ALTERNATE_OBJECT_DIRECTORIES="$_gd/objects" git "$@"; } + _wt add -A . + # A gitlink (submodule) cannot be replayed (checkout-index writes no files + # for one), so the drift gate could never pass: refused by name here, + # whether or not anything else changed (Codex review of #275). + _gl="$(_wt ls-files -s | awk -F '\t' '$1 ~ /^160000 / { print " " $2 }')" + if [ -n "$_gl" ]; then + echo "ERROR: mint aborted before writing anything — this tree carries a submodule" + echo " (gitlink), which a replay of the mint cannot reproduce, so its drift gate" + echo " could never pass. Minting such a tree is not supported:" + printf '%s\n' "$_gl" + rm -rf "$_pm"; exit 1 + fi + if git cat-file -e "${PARENT_TREE}^{tree}" 2>/dev/null; then + while IFS= read -r -d '' _st && IFS= read -r -d '' _p; do + # A newline splits the deed's line-based lists; byte 0x01 is the deed + # reader's record separator (Codex review of #277). + case "$_p" in *$'\n'*|*$'\001'*) echo "Error: a path holding a newline or byte 0x01 cannot be declared: $(printf '%q' "$_p")"; rm -rf "$_pm"; exit 1 ;; esac + case "$_st" in + A) PREMINT_A+=("$_p") ;; + D) PREMINT_D+=("$_p") ;; + *) PREMINT_M+=("$_p") ;; + esac + done < <(_wt diff --cached -z --no-renames --name-status "$PARENT_TREE") + elif [ "$(_wt write-tree)" != "$PARENT_TREE" ]; then + # An asserted pin (RSR_TEMPLATE_COMMIT) whose tree this repository + # does not hold: the difference cannot even be listed, so it cannot + # be declared, and the replay would report it (Codex review of #275). + rm -rf "$_pm" + echo "ERROR: mint aborted before writing anything — this work tree differs from the" + echo " template tree ${PARENT_TREE:0:12}, which this repository does not hold, so" + echo " the difference cannot be declared. Fetch it first" + echo " (git fetch ${PARENT_PIN}) or mint from a matching tree." + exit 1 + fi + # The work tree's own tree: what a dry run of this mint starts from + # (below). Its objects stay in $_pm until the replays are done. + _WT_TREE="$(_wt write-tree)" + # The mint moves a root .well-known/ (the www/ migration, below), so a + # path there could not be declared where it ends up: refused by name. + for _p in "${PREMINT_A[@]}" "${PREMINT_M[@]}" "${PREMINT_D[@]}"; do + case "$_p" in + .well-known/*) + echo "ERROR: mint aborted before writing anything — $_p changed before the mint," + echo " and the mint moves .well-known/ into www/.well-known/. Commit that move" + echo " first (bash scripts/migrate-wellknown-to-www.sh), or revert the change." + rm -rf "$_pm"; exit 1 ;; + esac + done + # The mint writes new files of its own: the archetype overlay (before + # and after its .in rename), the docs seed, the deed. A pre-mint file + # at one of those paths would be overwritten (lost) and then declared + # as this repo's own addition, which the replay contradicts (Codex + # review of #275). Refused by name, before anything is written. + _writes=("${REPO}_chora.deed") + if [ -n "$ARCHETYPE" ] && [ -d "archetypes/${ARCHETYPE}/overlay" ]; then + while IFS= read -r -d '' _o; do + _o="${_o#./}"; _writes+=("$_o") + case "$_o" in + src/PACKAGE.jl.in) _writes+=("src/${PROJECT_NAME}.jl") ;; + *.in) _writes+=("${_o%.in}") ;; + esac + done < <(cd "archetypes/${ARCHETYPE}/overlay" && find . -type f -print0) + fi + if [ -d build/docs-seed ]; then + while IFS= read -r -d '' _o; do _writes+=("docs/${_o#./}"); done \ + < <(cd build/docs-seed && find . -type f ! -path ./README.adoc -print0) + fi + # An edited template file the overlay or seed replaces is lost the same way + # (Codex review of #275: provisioning_praxis.deed under julia-library). + for _p in "${PREMINT_A[@]}" "${PREMINT_M[@]}"; do + for _w in "${_writes[@]}"; do + if [ "$_p" = "$_w" ]; then + echo "ERROR: mint aborted before writing anything — $_p changed before the mint, and" + echo " the mint writes that path itself (the archetype overlay, the docs seed or" + echo " the deed), so your file would be overwritten. Move it aside, mint, then" + echo " bring it back as a change of your own." + rm -rf "$_pm"; exit 1 + fi + done + done + _n=$(( ${#PREMINT_A[@]} + ${#PREMINT_M[@]} + ${#PREMINT_D[@]} )) + if [ "$_n" -gt 0 ]; then + echo "NOTE: ${_n} path(s) here already differ from the template tree ${PARENT_TREE:0:12}" + echo " (${#PREMINT_A[@]} added, ${#PREMINT_M[@]} changed, ${#PREMINT_D[@]} removed before this mint). They are" + echo " this repo's own. What they change in the result is declared in" + echo " (variant (paths)), measured against a replay of this mint:" + for _p in "${PREMINT_A[@]}" "${PREMINT_M[@]}" "${PREMINT_D[@]}"; do echo " $_p"; done + fi + fi + + # --- The replay this mint is measured against, when the tree has changed --- + # A pre-mint change can reach the result in ways no rule over its own path + # predicts: the mint moves, overwrites and renders files, and reads its own + # inputs (the overlay, the docs seed, its scripts) from this tree (Codex and + # CodeRabbit review of #275). So this mint is replayed, here, before anything + # is written, from the pinned tree with the same answers and date, exactly + # as the drift gate will replay it; what this tree then differs by is + # declared below. The pinned tree is in this repository (checked above). + # The inner mint skips pin resolution (RSR_ALLOW_UNPINNED with no template + # source): it writes the pin only into its deed, which is not compared. + # + # And a dry run: the same mint of THIS tree, pre-mint changes and all. A + # change at a path where the dry run and the replay agree has no effect on + # the result: the mint writes that path itself (the roadmap, mise.toml, + # the map, the overlay, the docs seed, the deed) and the change + # would be silently discarded. It is refused by name, before anything is + # written (Codex review of #275), whatever writes it. + # _same A B — identical as git records them: content, executable bit, link target. + # As scripts/check-mint-drift.sh's same(): a link target is read with a + # sentinel (command substitution strips trailing newlines), and the + # executable bit is the owner's (0100), as git records it, not `-x` + # (effective access, true as root for a group-executable file) (Codex + # review of #275). + _ux() { [ -n "$(find "$1" -prune -perm -0100 2>/dev/null)" ]; } + _same() { + if [ -L "$1" ] || [ -L "$2" ]; then + [ -L "$1" ] && [ -L "$2" ] && [ "$(readlink "$1"; echo .)" = "$(readlink "$2"; echo .)" ] + return + fi + { _ux "$1" && _ux "$2"; } || { ! _ux "$1" && ! _ux "$2"; } || return 1 + cmp -s "$1" "$2" + } + _REPLAY="" + if [ $(( ${#PREMINT_A[@]} + ${#PREMINT_M[@]} + ${#PREMINT_D[@]} )) -gt 0 ]; then + # _rg — git with no user or system configuration and no hooks, and no + # inherited repository location (GIT_DIR, GIT_INDEX_FILE, … from a hook or + # another git caller), which would redirect the replay's commits into + # this repository (CodeRabbit review of #275). _rgi IDX — the same, on + # the scratch index IDX and object store $_pm (the work tree's objects). + _rgclean=(env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_INDEX_FILE -u GIT_OBJECT_DIRECTORY + -u GIT_ALTERNATE_OBJECT_DIRECTORIES -u GIT_PREFIX -u GIT_NAMESPACE GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1) + _rgflags=(-c user.name=replay -c user.email=replay@example.invalid -c commit.gpgsign=false -c core.hooksPath=/dev/null) + _rg() { "${_rgclean[@]}" git "${_rgflags[@]}" "$@"; } + _rgi() { local _i="$1"; shift + "${_rgclean[@]}" GIT_INDEX_FILE="$_i" GIT_OBJECT_DIRECTORY="$_pm/objects" GIT_ALTERNATE_OBJECT_DIRECTORIES="$_gd/objects" \ + git "${_rgflags[@]}" "$@"; } + _rp="$(mktemp -d)"; _REPLAY="$_rp/r" + mkdir -p "$_rp/cfg"; : > "$_rp/gitconfig" + # _mint_tree TREE DIR — TREE written into DIR as a checkout writes it + # (checkout-index, not git archive, whose export-ignore and export-subst + # attributes would change the tree (Codex review of #275)), committed as + # "Use this template" commits it, then minted with this mint's answers. + _mint_tree() { + mkdir -p "$2" + _rgi "$_rp/index" read-tree "$1" + # A gitlink has no files to write, so the copy would lose it: refused + # by name rather than replayed inexactly (Codex review of #277). + _gl="$(_rgi "$_rp/index" ls-files -s | awk -F '\t' '$1 ~ /^160000 / { print " " $2 }')" + if [ -n "$_gl" ]; then + echo "ERROR: the tree carries a submodule (gitlink), which the replay of this mint" + echo " cannot reproduce; minting such a tree is not supported:" + printf '%s\n' "$_gl" + rm -f "$_rp/index"; return 1 + fi + _rgi "$_rp/index" checkout-index -a -f --prefix="$2/" + rm -f "$_rp/index" + # Forced, then checked to BE the tree: a file the tree tracks although + # its .gitignore matches it is still copied by "Use this template" + # (Codex and CodeRabbit review of #277). + _rg -C "$2" init -q -b main; _rg -C "$2" add -f -A + if [ "$(_rg -C "$2" write-tree)" != "$(_rgi "$_rp/index" rev-parse "$1^{tree}")" ]; then + echo "ERROR: the checkout of the tree does not read back as itself (an attribute" + echo " rewrites a file), so this mint cannot be replayed exactly." + return 1 + fi + _rg -C "$2" commit -q -m "Initial commit" + _rg -C "$2" remote add origin "git@${FORGE}:${OWNER}/${REPO}.git" + (cd "$2" && env -i PATH="$PATH" HOME="$HOME" TMPDIR="${TMPDIR:-/tmp}" \ + ${RUSTUP_HOME:+RUSTUP_HOME="$RUSTUP_HOME"} ${CARGO_HOME:+CARGO_HOME="$CARGO_HOME"} \ + ${RSR_MINT_TOOL_CACHE:+RSR_MINT_TOOL_CACHE="$RSR_MINT_TOOL_CACHE"} \ + ${RSR_TEMPLATE_SLUG:+RSR_TEMPLATE_SLUG="$RSR_TEMPLATE_SLUG"} \ + ${STANDARDS_UUID_V8_JS:+STANDARDS_UUID_V8_JS="$STANDARDS_UUID_V8_JS"} \ + XDG_CONFIG_HOME="$_rp/cfg" GIT_CONFIG_GLOBAL="$_rp/gitconfig" GIT_CONFIG_NOSYSTEM=1 \ + RSR_NON_INTERACTIVE=1 RSR_MINT_REPLAY=1 RSR_ALLOW_UNPINNED=1 \ + RSR_TEMPLATE_URL="$_rp/no-template-source" RSR_MINT_DATE="$CURRENT_DATE" \ + PROJECT_NAME="$PROJECT_NAME" REPO="$REPO" OWNER="$OWNER" AUTHOR="$AUTHOR" \ + AUTHOR_EMAIL="$AUTHOR_EMAIL" AUTHOR_ORG="${AUTHOR_ORG:-}" AUTHOR_EMAIL_ALT="${AUTHOR_EMAIL_ALT:-}" \ + AUTHOR_ORCID="${AUTHOR_ORCID:-}" PROJECT_DESCRIPTION="${PROJECT_DESCRIPTION:-}" FORGE="$FORGE" \ + SECURITY_EMAIL="${SECURITY_EMAIL:-}" CONDUCT_EMAIL="${CONDUCT_EMAIL:-}" \ + PROJECT_TYPE="${PROJECT_TYPE:-}" WEBSITE="${WEBSITE:-}" OPENSSF_BP_ID="${OPENSSF_BP_ID:-}" \ + SERVICE_NAME="${SERVICE_NAME:-}" PORT="${PORT:-}" REGISTRY="${REGISTRY:-}" \ + just repo-init ${ARCHETYPE:+"$ARCHETYPE"} "$2.log" 2>&1 \ + || { tail -n 30 "$2.log"; return 1; } + } + echo " replaying this mint from the template tree, to measure the pre-mint changes..." + if ! _mint_tree "$PARENT_TREE" "$_REPLAY"; then + echo "ERROR: mint aborted before writing anything — the replay of this mint, needed to" + echo " measure the pre-mint changes above, failed (its log is above)." + rm -rf "$_pm"; exit 1 + fi + echo " and a dry run of it on this tree, to find a change the mint would discard..." + if ! _mint_tree "$_WT_TREE" "$_rp/dry"; then + echo "ERROR: mint aborted before writing anything — a dry run of this mint on this" + echo " tree failed (its log is above): a pre-mint change breaks the mint." + rm -rf "$_pm"; exit 1 + fi + _lost=() + _here() { [ -e "$1" ] || [ -L "$1" ]; } + # _result_tree DIR — the mint result in DIR, as the tree git would commit. + _result_tree() { _rg -C "$1" add -A >/dev/null && _rg -C "$1" write-tree; } + _DRY_T="" + _n=0 + for _p in "${PREMINT_A[@]}" "${PREMINT_M[@]}" "${PREMINT_D[@]}"; do + if _here "$_rp/dry/$_p" && _here "$_REPLAY/$_p"; then + if _same "$_rp/dry/$_p" "$_REPLAY/$_p"; then _lost+=("$_p"); fi + elif ! _here "$_rp/dry/$_p" && ! _here "$_REPLAY/$_p" \ + && { _wt ls-files --error-unmatch -- ":(literal)$_p" >/dev/null 2>&1; }; then + # Added or changed here, and gone from both results: an input + # the mint reads and deletes (the docs seed, the overlay), or a + # file in a directory it deletes. Its change had an effect only if + # the result differs without it: this mint, minus that one change, + # is minted again and compared whole (Codex review of #275: an + # edit to build/docs-seed/README.adoc, which is never copied, + # was destroyed without a trace). + [ -n "$_DRY_T" ] || _DRY_T="$(_result_tree "$_rp/dry")" + _n=$((_n + 1)) + _rgi "$_rp/index" read-tree "$_WT_TREE" + if _rgi "$_rp/index" cat-file -e "${PARENT_TREE}:$_p" 2>/dev/null; then + read -r _m _ _s _ <<<"$(_rgi "$_rp/index" ls-tree "$PARENT_TREE" -- "$_p")" + _rgi "$_rp/index" update-index --add --cacheinfo "$_m,$_s,$_p" + else + _rgi "$_rp/index" update-index --force-remove -- "$_p" + fi + _rev_t="$(_rgi "$_rp/index" write-tree)"; rm -f "$_rp/index" + echo " checking that the pre-mint change to $_p has an effect..." + if ! _mint_tree "$_rev_t" "$_rp/without.$_n"; then + echo "ERROR: mint aborted before writing anything — a dry run without the change" + echo " to $_p failed (its log is above)." + rm -rf "$_pm"; exit 1 + fi + if [ "$(_result_tree "$_rp/without.$_n")" = "$_DRY_T" ]; then _lost+=("$_p"); fi + fi + done + rm -rf "$_pm" + if [ "${#_lost[@]}" -gt 0 ]; then + echo "ERROR: mint aborted before writing anything — these paths changed before the mint," + echo " and the mint writes or deletes each itself, so the change would be discarded:" + for _p in "${_lost[@]}"; do echo " $_p"; done + echo " Revert them (or move them aside), mint, then make the change again." + exit 1 + fi + elif [ -n "${_pm:-}" ]; then + rm -rf "$_pm" + fi + echo "" echo "Replacing placeholders..." # Apply archetype overlay files (if any) BEFORE substitution, so overlay # files are rendered along with the rest of the tree. + # + # The overlay's file list is recorded first: the .in rename below must touch + # ONLY the files this archetype added. The spine ships .in templates of its + # own (build/templates/, the k9 examples) whose .in suffix is meaningful and + # must survive, and a blanket `find . -name '*.in' -exec mv` would rename + # them into broken files. + OVERLAY_FILES=() if [ -n "$ARCHETYPE" ] && [ -d "archetypes/${ARCHETYPE}/overlay" ]; then + mapfile -t OVERLAY_FILES < <( + cd "archetypes/${ARCHETYPE}/overlay" && find . -type f ! -name '*.orig' \ + | sed 's|^\./||' | sort + ) cp -a "archetypes/${ARCHETYPE}/overlay/." . - echo " applied overlay: archetypes/${ARCHETYPE}/overlay/" + echo " applied overlay: archetypes/${ARCHETYPE}/overlay/ (${#OVERLAY_FILES[@]} file(s))" + fi + + # An archetype's workflows bring their lock records as a fragment beside + # the overlay, never as an overlay file: an overlay actions.lock REPLACES + # the spine's lock, and every spine workflow then dies at startup with no + # record. Merge the fragment in; a conflicting record stops the mint. + if [ -n "$ARCHETYPE" ] && [ -f "archetypes/${ARCHETYPE}/actions.lock" ]; then + bash scripts/rust-tool.sh merge-actions-lock \ + .github/workflows/actions.lock "archetypes/${ARCHETYPE}/actions.lock" + fi + + # ── Archetype prune (ADR-0003) ─────────────────────────────────────────── + # The spine is polyglot on purpose: it carries the container module and the + # dev container so that SOME child can use them. A child carrying a module + # whose capability its preset does not declare is drift, and one of those + # modules is not harmless: `build/container/` is a DEPLOYED artifact, so a + # Julia package shipped it and failed `container-images-pinned` from birth + # over a container it does not need. The archetype says what it drops in + # archetypes//prune.txt; the removals are recorded in the child's + # (variant (paths :removed …)) so the drift gate does not report them. + if [ -n "$ARCHETYPE" ] && [ -f "archetypes/${ARCHETYPE}/prune.txt" ]; then + while IFS= read -r _pruned; do + [ -n "$_pruned" ] && MINT_REMOVES+=("$_pruned") + done < <(bash scripts/prune-archetype-modules.sh "archetypes/${ARCHETYPE}" .) + echo " pruned: archetypes/${ARCHETYPE}/prune.txt applied" fi # Seed docs/ from the documentation seed, BEFORE substitution so the seed's @@ -195,68 +878,78 @@ repo-init archetype="": LB=$(printf '\x7b\x7b') RB=$(printf '\x7d\x7d') + # Answers are free text, and the right-hand side of s|…|…| is not: `&` + # inserts the matched token, `\` escapes, `|` ends the expression. A + # description "Tools & proofs" rendered as "Tools proofs" and + # then failed the placeholder gate; a `|` broke the substitution. Every + # answer goes through sed_rhs. (A newline was refused before the first write.) + sed_rhs() { printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'; } + # Build the sed expression list # Note: using | as delimiter since URLs contain / SED_ARGS=( - -e "s|${LB}PROJECT_NAME${RB}|${PROJECT_NAME}|g" - -e "s|${LB}PROJECT_DESCRIPTION${RB}|${PROJECT_DESCRIPTION}|g" - -e "s|${LB}PROJECT${RB}|${PROJECT_UPPER}|g" - -e "s|${LB}project${RB}|${PROJECT_LOWER}|g" - -e "s|${LB}REPO${RB}|${REPO}|g" - -e "s|${LB}OWNER${RB}|${OWNER}|g" - -e "s|${LB}AUTHOR${RB}|${AUTHOR}|g" - -e "s|${LB}AUTHOR_EMAIL${RB}|${AUTHOR_EMAIL}|g" - -e "s|${LB}AUTHOR_ORG${RB}|${AUTHOR_ORG}|g" - -e "s|${LB}AUTHOR_LAST${RB}|${AUTHOR_LAST}|g" - -e "s|${LB}AUTHOR_FIRST${RB}|${AUTHOR_FIRST}|g" - -e "s|${LB}AUTHOR_INITIALS${RB}|${FIRST_INITIAL}|g" - -e "s|${LB}FORGE${RB}|${FORGE}|g" - -e "s|${LB}CURRENT_YEAR${RB}|${CURRENT_YEAR}|g" - -e "s|${LB}CURRENT_DATE${RB}|${CURRENT_DATE}|g" - -e "s|${LB}DATE${RB}|${CURRENT_DATE}|g" - -e "s|${LB}SECURITY_EMAIL${RB}|${SECURITY_EMAIL}|g" - -e "s|${LB}CONDUCT_EMAIL${RB}|${CONDUCT_EMAIL}|g" + -e "s|${LB}PROJECT_NAME${RB}|$(sed_rhs "${PROJECT_NAME}")|g" + -e "s|${LB}PROJECT_DESCRIPTION${RB}|$(sed_rhs "${PROJECT_DESCRIPTION}")|g" + -e "s|${LB}PROJECT${RB}|$(sed_rhs "${PROJECT_UPPER}")|g" + -e "s|${LB}project${RB}|$(sed_rhs "${PROJECT_LOWER}")|g" + -e "s|${LB}REPO${RB}|$(sed_rhs "${REPO}")|g" + -e "s|${LB}OWNER${RB}|$(sed_rhs "${OWNER}")|g" + -e "s|${LB}AUTHOR${RB}|$(sed_rhs "${AUTHOR}")|g" + -e "s|${LB}AUTHOR_EMAIL${RB}|$(sed_rhs "${AUTHOR_EMAIL}")|g" + -e "s|${LB}AUTHOR_ORG${RB}|$(sed_rhs "${AUTHOR_ORG}")|g" + -e "s|${LB}AUTHOR_LAST${RB}|$(sed_rhs "${AUTHOR_LAST}")|g" + -e "s|${LB}AUTHOR_FIRST${RB}|$(sed_rhs "${AUTHOR_FIRST}")|g" + -e "s|${LB}AUTHOR_INITIALS${RB}|$(sed_rhs "${FIRST_INITIAL}")|g" + -e "s|${LB}FORGE${RB}|$(sed_rhs "${FORGE}")|g" + -e "s|${LB}CURRENT_YEAR${RB}|$(sed_rhs "${CURRENT_YEAR}")|g" + -e "s|${LB}CURRENT_DATE${RB}|$(sed_rhs "${CURRENT_DATE}")|g" + -e "s|${LB}DATE${RB}|$(sed_rhs "${CURRENT_DATE}")|g" + -e "s|${LB}SECURITY_EMAIL${RB}|$(sed_rhs "${SECURITY_EMAIL}")|g" + -e "s|${LB}CONDUCT_EMAIL${RB}|$(sed_rhs "${CONDUCT_EMAIL}")|g" -e "s|${LB}LICENSE${RB}|MPL-2.0|g" -e "s|${LB}CONDUCT_TEAM${RB}|Code of Conduct Committee|g" -e "s|${LB}RESPONSE_TIME${RB}|48 hours|g" -e "s|${LB}MAIN_BRANCH${RB}|main|g" - -e "s|${LB}PROJECT_PURPOSE${RB}|${PROJECT_DESCRIPTION}|g" - -e "s|${LB}PROJECT_ROLE${RB}|${PROJECT_TYPE}|g" - -e "s|${LB}PROJECT_TYPE${RB}|${PROJECT_TYPE}|g" - -e "s|${LB}WEBSITE${RB}|${WEBSITE}|g" - -e "s|${LB}SERVICE_NAME${RB}|${SERVICE_NAME}|g" - -e "s|${LB}PORT${RB}|${PORT}|g" - -e "s|${LB}REGISTRY${RB}|${REGISTRY}|g" - -e "s|${LB}IMAGE${RB}|${REGISTRY}/${SERVICE_NAME}|g" - -e "s|${LB}VERSION${RB}|${VERSION}|g" - -e "s|${LB}EMAIL${RB}|${AUTHOR_EMAIL}|g" - -e "s|${LB}UUID${RB}|${REPO_UUID}|g" - -e "s|${LB}DESCRIPTION${RB}|${PROJECT_DESCRIPTION}|g" + -e "s|${LB}PROJECT_PURPOSE${RB}|$(sed_rhs "${PROJECT_DESCRIPTION}")|g" + -e "s|${LB}PROJECT_ROLE${RB}|$(sed_rhs "${PROJECT_TYPE}")|g" + -e "s|${LB}PROJECT_TYPE${RB}|$(sed_rhs "${PROJECT_TYPE}")|g" + -e "s|${LB}WEBSITE${RB}|$(sed_rhs "${WEBSITE}")|g" + -e "s|${LB}SERVICE_NAME${RB}|$(sed_rhs "${SERVICE_NAME}")|g" + -e "s|${LB}PORT${RB}|$(sed_rhs "${PORT}")|g" + -e "s|${LB}REGISTRY${RB}|$(sed_rhs "${REGISTRY}")|g" + -e "s|${LB}IMAGE${RB}|$(sed_rhs "${REGISTRY}")/$(sed_rhs "${SERVICE_NAME}")|g" + -e "s|${LB}VERSION${RB}|$(sed_rhs "${VERSION}")|g" + -e "s|${LB}EMAIL${RB}|$(sed_rhs "${AUTHOR_EMAIL}")|g" + -e "s|${LB}UUID${RB}|$(sed_rhs "${REPO_UUID}")|g" + -e "s|${LB}DESCRIPTION${RB}|$(sed_rhs "${PROJECT_DESCRIPTION}")|g" ) - [ -n "$AUTHOR_EMAIL_ALT" ] && SED_ARGS+=(-e "s|${LB}AUTHOR_EMAIL_ALT${RB}|${AUTHOR_EMAIL_ALT}|g") - [ -n "$OPENSSF_BP_ID" ] && SED_ARGS+=(-e "s|${LB}OPENSSF_BP_ID${RB}|${OPENSSF_BP_ID}|g") - [ -n "$OPENSSF_BP_ID" ] && SED_ARGS+=(-e "s|${LB}OPENSSF_PROJECT_ID${RB}|${OPENSSF_BP_ID}|g") + [ -n "$AUTHOR_EMAIL_ALT" ] && SED_ARGS+=(-e "s|${LB}AUTHOR_EMAIL_ALT${RB}|$(sed_rhs "${AUTHOR_EMAIL_ALT}")|g") + [ -n "$AUTHOR_ORCID" ] && SED_ARGS+=(-e "s|${LB}AUTHOR_ORCID${RB}|$(sed_rhs "${AUTHOR_ORCID}")|g") + [ -n "$OPENSSF_BP_ID" ] && SED_ARGS+=(-e "s|${LB}OPENSSF_BP_ID${RB}|$(sed_rhs "${OPENSSF_BP_ID}")|g") + [ -n "$OPENSSF_BP_ID" ] && SED_ARGS+=(-e "s|${LB}OPENSSF_PROJECT_ID${RB}|$(sed_rhs "${OPENSSF_BP_ID}")|g") # Archetype-determined tokens (ADR-0003): the archetype contract fills # what it can honestly determine. Added BEFORE the UNASSIGNED fallback # below — sed applies expressions in order, so a token the archetype # answers never reaches the fallback, and one it omits still fails loudly. if [ -n "$ARCH_FILE" ]; then - while IFS= read -r arch_line; do - arch_tok="${arch_line%%=*}"; arch_tok="${arch_tok// /}" - arch_val=$(printf '%s' "$arch_line" | sed -n 's/^[^=]*= *"\(.*\)" *$/\1/p') + # The archetype's (tokens) clause: every :KEY "value" pair, read through + # deed-field.sh so string escapes are decoded by the one reader. + while IFS= read -r arch_tok; do + arch_val=$(bash scripts/deed-field.sh "$ARCH_FILE" tokens "$arch_tok" || true) { [ -n "$arch_tok" ] && [ -n "$arch_val" ]; } || continue case "$arch_val" in *'|'*) echo "WARN: archetype token $arch_tok contains '|' (the sed delimiter) — skipped, stays UNASSIGNED" continue ;; esac SED_ARGS+=(-e "s|${LB}${arch_tok}${RB}|${arch_val}|g") - done < <(awk '$0=="[tokens]"{f=1;next} /^\[/{f=0} f && !/^[[:space:]]*#/ && NF' "$ARCH_FILE") + done < <(bash scripts/deed-field.sh --clause "$ARCH_FILE" tokens \ + | awk 'prev ~ /^A:/ && /^S/ { print substr(prev, 3) } { prev = $0 }') fi # Tokens only the author can answer. init cannot know a project's unique # strength or its language stack, and guessing would write fiction into - # ANCHOR.a2ml — the file the estate treats as semantic authority. They + # the deed's (anchor) clause — the estate's semantic authority. They # become UNASSIGNED, the same marker `just repo-init` already writes for an # unchosen clade: honest, greppable, and obviously unfinished, whereas a # leftover brace token just looks like the template broke. @@ -269,18 +962,39 @@ repo-init archetype="": SED_ARGS+=(-e "s|${LB}${tok}${RB}|UNASSIGNED|g") done - # julia-library archetype: derive the package uuid at mint (owner ruling - # 2026-09-19: generate at mint). Same DERIVABLE class as the REPO uuid - # above - computed, never guessed. Namespace = the repo's own derived - # uuid, name = julia:: stable across re-mints of the same repo, - # never collides with the repo uuid. No uuidgen AND no python3 on the - # operator machine -> UNASSIGNED (the honest fallback at :437); Pkg - # then refuses the package loudly until the owner assigns one. + # julia-library archetype: mint the package uuid as UUIDv8 profile C + # (ADR-008 amendment: the strict UUIDv8 gate rejects the UUIDv5 mint, + # and D320 requires unregistered packages at v8). Profile C is + # DETERMINISTIC: bytes 0..15 of SHA-256("julia-package-identity" ":" + # "github.com//"), version nibble 8, variant 10 -- stable + # across re-mints of the same repo, derivable by any operator machine + # with python3 (or bun over standards' scripts/uuid-v8.js), never + # colliding with the repo uuid. The domain is REGISTERED in standards' + # ADR-008-profile-c-domains.json. No python3 and no bun -> UNASSIGNED + # (the honest fallback at :437); Pkg then refuses the package loudly + # until the owner assigns one. if [ "$ARCHETYPE" = "julia-library" ]; then - if command -v uuidgen >/dev/null 2>&1; then - PACKAGE_UUID=$(uuidgen --sha1 --namespace "${REPO_UUID}" --name "julia:${REPO}") + # GitHub owner logins are case-insensitive; canonicalize to lowercase + # before the preimage so owner casing cannot fork the UUID. + OWNER_LC=$(printf '%s' "${OWNER}" | tr '[:upper:]' '[:lower:]') + # A replay supplies the uuid the mint recorded ((mint-answers + # :package-uuid)), so the drift gate does not depend on which of bun or + # python3 the checking host has (Codex review of #275). Only a replay: + # an ordinary mint always derives it, so a stray RSR_PACKAGE_UUID in the + # environment cannot write an unrelated identity into Project.toml. + if [ "${RSR_MINT_REPLAY:-0}" = "1" ] && [[ "${RSR_PACKAGE_UUID:-}" =~ ^(UNASSIGNED|[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$ ]]; then + PACKAGE_UUID="$RSR_PACKAGE_UUID" + elif command -v bun >/dev/null 2>&1 && [ -f "${STANDARDS_UUID_V8_JS:-}" ]; then + PACKAGE_UUID=$(bun "${STANDARDS_UUID_V8_JS}" c julia-package-identity "github.com/${OWNER_LC}/${REPO}") elif command -v python3 >/dev/null 2>&1; then - PACKAGE_UUID=$(python3 -c "import uuid; print(uuid.uuid5(uuid.UUID('${REPO_UUID}'), 'julia:${REPO}'))") + PACKAGE_UUID=$(python3 -c ' + import hashlib, sys + pre = ("julia-package-identity:github.com/%s/%s" % (sys.argv[1], sys.argv[2])).encode() + b = bytearray(hashlib.sha256(pre).digest()[:16]) + b[6] = (b[6] & 0x0F) | 0x80 # version 8 + b[8] = (b[8] & 0x3F) | 0x80 # variant 10 + h = b.hex() + print(f"{h[0:8]}-{h[8:12]}-{h[12:16]}-{h[16:20]}-{h[20:32]}")' "${OWNER_LC}" "${REPO}") else PACKAGE_UUID=UNASSIGNED fi @@ -294,9 +1008,21 @@ repo-init archetype="": # in the new repo's README, and it trips the placeholder gate on every # subsequent push, which trains people to ignore a red CI. No id yet means # no badge yet; add it when you register at bestpractices.dev. + # + # Line deletion is only safe where the line is the whole construct. It + # used to run on docs/governance/TEMPLATE-STANDARDS-AUDIT.adoc too, where + # the token's line is one CELL of a six-column table row: deleting it + # left an incomplete row, asciidoctor reported "dropping cells from + # incomplete row", and Estate Rules' render gate failed in every child + # minted without an OpenSSF id (measured 2026-10-14 on resource-algebra). + # That document names the README's tokens rather than using them, so it + # now spells them as plain names and needs no pass here. if [ -z "$OPENSSF_BP_ID" ]; then sed -i "/bestpractices\.dev\/projects\/${LB}OPENSSF_BP_ID${RB}/d" README.adoc 2>/dev/null || true - sed -i "/${LB}OPENSSF_PROJECT_ID${RB}/d" docs/governance/TEMPLATE-STANDARDS-AUDIT.adoc 2>/dev/null || true + fi + # Same rule for the ORCID iD: CFF makes `orcid` optional, a wrong one is not. + if [ -z "$AUTHOR_ORCID" ]; then + sed -i "/orcid\.org\/${LB}AUTHOR_ORCID${RB}/d" CITATION.cff 2>/dev/null || true fi # A .mailmap exists to map an alternate address onto the canonical one. # With no alternate address there is nothing to map, and the sole entry @@ -310,21 +1036,50 @@ repo-init archetype="": fi # Replace in all text files (skip .git, LICENSE text, and binaries) - find . -type f \ - -not -path './.git/*' \ - -not -name 'MPL-2.0.txt' \ - -not -name '*.png' -not -name '*.jpg' -not -name '*.gif' \ - -not -name '*.woff' -not -name '*.woff2' \ - | while read -r file; do - if file --brief "$file" | grep -qi 'text\|ascii\|utf'; then + # + # Skip just files too. In a Justfile or *.just module, double braces are + # just's OWN interpolation syntax, never a template token: every one in + # the template is a just variable or parameter. The lowercase project + # token is spelled exactly like just's interpolation of the `project` + # variable, so this pass used to freeze it into the identifier form. + # Measured 2026-10-14 minting resource-algebra: 7 Justfile lines, the + # contractile Justfile copy and container.just's image tag all became the + # literal `resource_algebra` instead of following `project := + # "resource-algebra"` (docs/man/resource_algebra.1, an image tagged with + # the underscored name). The self-name pass below still rewrites the + # template literal in these files; that is the edit they actually need. + # 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 + } + + mint_files | while IFS= read -r file; do + case "${file##*/}" in + MPL-2.0.txt|Justfile|*.just|*.png|*.jpg|*.gif|*.woff|*.woff2) continue ;; + esac + if is_text_file "$file"; then sed -i "${SED_ARGS[@]}" "$file" fi done - # Also replace [YOUR-REPO-NAME] and [YOUR-NAME/ORG] in the repo deed - # (the family-7 allocation manifest folded into it — standards#837 pilot). - sed -i "s|\[YOUR-REPO-NAME\]|${PROJECT_NAME}|g" rsr-template-repo_chora.deed 2>/dev/null || true - sed -i "s|\[YOUR-NAME/ORG\]|${OWNER}|g" rsr-template-repo_chora.deed 2>/dev/null || true + # The pass above fills the container tokens too (SERVICE_NAME, PORT, + # REGISTRY), which changes how KYAML quotes them; reformat the configs or + # Estate Rules' "Converted YAML stays KYAML" fails the child. + bash scripts/format-container-kyaml.sh + + # [YOUR-REPO-NAME] and [YOUR-NAME/ORG] (the family-7 allocation manifest + # folded into the deed — standards#837 pilot) are replaced in the deed, + # found by its shape rather than by the original template's file name: in + # a repository minted from a minted one it is _chora.deed, and the + # two seds that named rsr-template-repo_chora.deed silently no-opped there. + DEED_IN="$(bash scripts/deed-field.sh --find .)" \ + || { echo "Error: the tree must carry exactly one *_chora.deed at its root."; exit 1; } + sed -i -e "s|\[YOUR-REPO-NAME\]|$(sed_rhs "${PROJECT_NAME}")|g" -e "s|\[YOUR-NAME/ORG\]|$(sed_rhs "${OWNER}")|g" "$DEED_IN" # --- Instruction-block pass -------------------------------------------- # Delete the "TEMPLATE INSTRUCTIONS (delete this block before publishing)" @@ -342,7 +1097,7 @@ repo-init archetype="": if [ "$REPO" != "${REPO%-template-repo}" ]; then echo " instruction blocks: skipped (this IS a template repo)" else - bash scripts/rust-tool.sh strip-instruction-blocks . + mint_files | bash scripts/rust-tool.sh strip-instruction-blocks - fi # --- Dependabot ecosystem pass ----------------------------------------- @@ -366,10 +1121,10 @@ repo-init archetype="": # Justfile: project := "rsr-template-repo" # Justfile: REPO := "rsr-template-repo" # sonar-project.properties sonar.projectKey=hyperpolymath_rsr-template-repo - # STATE.a2ml / ECOSYSTEM.a2ml project = "rsr-template-repo" + # the deed's (status) / (ecosystem) project = "rsr-template-repo" # i.e. it declared that it WAS the template, and would have reported its # code analysis into the TEMPLATE's SonarCloud project. This is the same - # failure the CLADE.a2ml block below fixes for the uuid/canonical-name; + # failure the repo-identity splice below fixes for the uuid/canonical-name; # nothing generalised it to the rest of the tree. Confirmed in the wild: # hyperpolymath/cargo-zigbuild still carries both Justfile lines. # @@ -378,305 +1133,473 @@ repo-init archetype="": # (ADRs, its changelog, its own audit, and this recipe itself — which # holds PARENT_SLUG) are skipped wholesale, and within every other file # any line that names the parent AS a parent is left byte-for-byte alone. - if [ "$REPO" != "rsr-template-repo" ]; then - SELF_PROV='instantiated-from|parent|upstream|chain =|lineage|minted from|created from|Template: ' - find . -type f \ - -not -path './.git/*' \ - -not -path './build/just/repo-init.just' \ - -not -path './docs/decisions/*' \ - -not -path './.machine_readable/descriptiles/CLADE.a2ml' \ - -not -path './.machine_readable/descriptiles/VARIANT.a2ml' \ - -not -name 'CHANGELOG.md' \ - -not -name 'TEMPLATE-STANDARDS-AUDIT.adoc' \ - -not -name 'TEMPLATE-LINEAGE-AUDIT.adoc' \ - -not -name '*.png' -not -name '*.jpg' -not -name '*.gif' \ - -not -name '*.woff' -not -name '*.woff2' \ - | while read -r file; do - grep -q 'rsr-template-repo' "$file" 2>/dev/null || continue - file --brief "$file" | grep -qi 'text\|ascii\|utf' || continue + # + # A line that names the template as something to DETECT is protected too: + # the deed's state-validation `:reject-if-contains ("rsr-template-repo")` + # flags state files still carrying template text. Rewritten to the child's + # own slug it rejected every legitimate state file of the child (measured + # 2026-10-14 on resource-algebra) - the same "detector eaten by the + # filling" defect the :reject-if-matches comment in the deed records for + # tokens. + # + # An issue or PR reference `rsr-template-repo#N` names the TEMPLATE's own + # issue, and stays so. Rewritten it pointed at an issue of the child that + # does not exist, or worse, an unrelated one: resource-algebra carried 20 + # such references (`resource-algebra#209` for the deed-clause migration) + # after its mint on 2026-10-14. tests/workflows/self_name_pass_test.sh + # runs these two lines on fixtures. + if [ "$REPO" != "$TEMPLATE_REPO" ]; then + SELF_PROV='instantiated-from|parent|upstream|chain =|lineage|minted from|created from|Template: |reject-if-contains' + # The template's name and slug are LITERAL text, not patterns: a Julia + # mint's parent may be .jl, and `.` in a sed pattern matches any + # character. Escaped inline so self_name_pass_test.sh can evaluate this + # one assignment exactly as the recipe defines it. + # The name is matched only where it stands as a whole name: not inside + # a longer word or slug (Codex review of #251). Unbounded, a parent + # named `app` turned "application mapping" into "newlication mnewing". + # A neighbouring letter or digit blocks the match; anything else does + # not, so `_` (sonar.projectKey), `_chora.deed` and + # derived names such as `-checkout` or `rm-` still follow. Each pass + # loops (:label … t label) because a match consumes its right-hand + # neighbour, and marks through a sentinel so a new name that contains + # the old one (Geometry -> Geometry.jl) cannot loop forever. + SELF_NAME_SED="/${SELF_PROV}/!{:i;s@(^|[^A-Za-z0-9])${TEMPLATE_REPO//./\\.}#([0-9])@\\1\\x01I\\x01\\2@;ti;:s;s@(^|[^A-Za-z0-9])${TEMPLATE_SLUG//./\\.}([^A-Za-z0-9]|$)@\\1\\x01S\\x01\\2@;ts;:n;s@(^|[^A-Za-z0-9])${TEMPLATE_REPO//./\\.}([^A-Za-z0-9]|$)@\\1\\x01N\\x01\\2@;tn;s@\\x01S\\x01@${OWNER}/${REPO}@g;s@\\x01N\\x01@${REPO}@g;s@\\x01I\\x01@${TEMPLATE_REPO}#@g;}" + # Paths that are ABOUT the template keep its name wholesale: its ADRs, + # changelog and audits. Template machinery (this recipe, the tests whose + # fixtures must stay the template's literal name) is not in mint_files + # at all; it is listed in .machine_readable/mint-verbatim.txt. + mint_files | while IFS= read -r file; do + case "$file" in + docs/decisions/*|CHANGELOG.md|*/CHANGELOG.md) continue ;; + TEMPLATE-STANDARDS-AUDIT.adoc|*/TEMPLATE-STANDARDS-AUDIT.adoc) continue ;; + TEMPLATE-LINEAGE-AUDIT.adoc|*/TEMPLATE-LINEAGE-AUDIT.adoc) continue ;; + *.png|*.jpg|*.gif|*.woff|*.woff2) continue ;; + esac + grep -qF -- "$TEMPLATE_REPO" "$file" 2>/dev/null || continue + is_text_file "$file" || continue # sed -i preserves the mode bit; rewriting via a temp file would not. - sed -i -E "/${SELF_PROV}/!{s|hyperpolymath/rsr-template-repo|${OWNER}/${REPO}|g;s|rsr-template-repo|${REPO}|g;}" "$file" + sed -i -E "$SELF_NAME_SED" "$file" done - echo " self-name: rewrote template literal -> ${REPO} (provenance lines preserved)" + echo " self-name: rewrote ${TEMPLATE_REPO} -> ${REPO} (provenance lines preserved)" # The repo deed's FILENAME carries the repo slug (deed dispatch: # _chora.deed — deed.abnf v1.0.0). The self-name pass rewrote # the slug inside the file; the file itself is renamed here. Plain mv: # mint runs before the new repo's first commit. Renames (not tokens) - # are how filenames change — a {{REPO}} token in a FILENAME is the + # are how filenames change — a REPO token in a FILENAME is the # brace-collapse hazard class repo-init has already been burnt by. - if [ -f rsr-template-repo_chora.deed ]; then - mv rsr-template-repo_chora.deed "${REPO}_chora.deed" - echo " repo deed: renamed rsr-template-repo_chora.deed -> ${REPO}_chora.deed" - fi - - # ── self-OWNER pass ────────────────────────────────────────────────── - # - # Same class as the self-name leak above, and missed by it: the - # template's own OWNER is a plain literal, so nothing rewrote it. A repo - # minted under a different owner shipped: - # - # Justfile OWNER := "hyperpolymath" - # sonar-project.properties sonar.projectKey=hyperpolymath_ - # - # i.e. it declared the wrong owner and reported its code analysis into - # the wrong SonarCloud project. Measured on a metadatastician mint. - # - # This pass is deliberately NARROW — three exact, identity-bearing - # lines. A blanket `hyperpolymath` -> ${OWNER} rewrite would be WRONG: - # `hyperpolymath/standards`, `hyperpolymath/proven` and the other estate - # dependencies are real repos every child genuinely points at, and - # rewriting those would break the child's canon, CI and badges. - if [ "$OWNER" != "hyperpolymath" ]; then - sed -i -E 's|^(OWNER[[:space:]]*:=[[:space:]]*)"hyperpolymath"|\1"'"${OWNER}"'"|' Justfile - sed -i -E 's|^sonar\.organization=hyperpolymath$|sonar.organization='"${OWNER}"'|' sonar-project.properties - sed -i -E 's|^sonar\.projectKey=hyperpolymath_|sonar.projectKey='"${OWNER}"'_|' sonar-project.properties - echo " self-owner: rewrote owner literal -> ${OWNER} (estate dependency paths preserved)" - echo " NOTE: confirm the SonarCloud ORG is really '${OWNER}' before provisioning —" - echo " the GitHub owner and the SonarCloud org are not always the same." + # The deed was found by shape above (DEED_IN), never by the original + # template's name, so a mint from a minted repo renames it too. + if [ "${DEED_IN#./}" != "${REPO}_chora.deed" ]; then + mv -- "$DEED_IN" "${REPO}_chora.deed" + echo " repo deed: renamed ${DEED_IN#./} -> ${REPO}_chora.deed" fi fi + # ── self-OWNER pass ────────────────────────────────────────────────────── + # + # Same class as the self-name leak above, and missed by it: the template's + # own OWNER is a plain literal, so nothing rewrote it. A repo minted under a + # different owner shipped: + # + # Justfile OWNER := "hyperpolymath" + # sonar-project.properties sonar.projectKey=hyperpolymath_ + # + # i.e. it declared the wrong owner and reported its code analysis into the + # wrong SonarCloud project. Measured on a metadatastician mint. The owner + # compared against is the TEMPLATE's (derived above), so a repo minted from + # a metadatastician child is handled the same way. + # + # This pass is deliberately NARROW — three exact, identity-bearing lines. A + # blanket owner rewrite would be WRONG: `hyperpolymath/standards`, + # `hyperpolymath/proven` and the other estate dependencies are real repos + # every child genuinely points at, and rewriting those would break the + # child's canon, CI and badges. + if [ "$OWNER" != "$TEMPLATE_OWNER" ]; then + # The same declaration forms the identity parser accepts: optional + # `export`, either quote (Codex review of #274). + sed -i -E "$SELF_OWNER_SED" Justfile sonar-project.properties + echo " self-owner: rewrote ${TEMPLATE_OWNER} -> ${OWNER} (estate dependency paths preserved)" + echo " NOTE: confirm the SonarCloud ORG is really '${OWNER}' before provisioning —" + echo " the GitHub owner and the SonarCloud org are not always the same." + fi + echo "" - echo "── Identity (CLADE.a2ml) ────────────────────────" - - # Install THIS repo's CLADE.a2ml, replacing rsr-template-repo's own. - # - # The CLADE.a2ml shipped in the template is the TEMPLATE's real identity — a - # real uuid for a real repo. Nothing in SED_ARGS ever touched it, so every - # repo created from this template inherited uuid a5ea1382-… and - # canonical-name "rsr-template-repo" verbatim, and told every arriving agent - # it WAS the template. Nothing caught it: each value was individually valid. - # chronicles-of-slavia and scaffoldia were corrected for exactly this on - # 2026-07-16; paint-type, cargo-zigbuild, email-octad-experiment, llm-grace - # and rsr-template-how-to still carry it. - # - # build/templates/CLADE.a2ml.in has already had its brace tokens filled by - # the substitution loop above (including the UUID one, derived earlier). - # NB: literal double-brace tokens cannot be written in this recipe body — - # just interpolates them, which is why LB/RB are built with printf above. + echo "── Identity (repo deed) ─────────────────────────" + + # Write THIS repo's identity into its deed, replacing rsr-template-repo's own. + # + # The template ships its REAL identity — clade rm, the canonical-name + # rsr-template-repo, a production maturity, its own ecosystem relations — + # between the `>>> repo-identity` / `<<< repo-identity` markers of the deed. + # Before #209 the same facts sat in descriptiles/CLADE.a2ml, which nothing + # in SED_ARGS touched, so every repo created from this template inherited + # uuid a5ea1382-… and canonical-name "rsr-template-repo" verbatim and told + # every arriving agent it WAS the template. chronicles-of-slavia and + # scaffoldia were corrected for exactly this on 2026-07-16. + # + # build/templates/identity.deed.in has already had its brace tokens filled + # by the substitution loop above. It uses only tokens whose values cannot + # break a deed string (slug, owner, forge, date); free text such as the + # description stays UNASSIGNED for the author. # The split is the whole point: - # DERIVABLE -> computed, never guessed (uuid, owner, canonical-name) - # JUDGEMENT -> left UNASSIGNED, fails loudly until a human chooses (clade) - CLADE_IN="build/templates/CLADE.a2ml.in" - CLADE_OUT=".machine_readable/descriptiles/CLADE.a2ml" - if [ -f "$CLADE_IN" ]; then - mkdir -p "$(dirname "$CLADE_OUT")" - cp "$CLADE_IN" "$CLADE_OUT" + # DERIVABLE -> computed, never guessed (uuid name, owner, canonical-name) + # JUDGEMENT -> left to the author; the clade is an optional finding aid, never written here + ID_IN="build/templates/identity.deed.in" + # Whatever one deed is present (renamed for the child above, or already + # named for it): never a hardcoded template file name. + DEED="$(bash scripts/deed-field.sh --find .)" \ + || { echo "Error: the tree must carry exactly one *_chora.deed at its root."; exit 1; } + if [ -f "$ID_IN" ] && [ -f "$DEED" ]; then + if [ "$(grep -c '^ ; >>> repo-identity' "$DEED")" != 1 ] \ + || [ "$(grep -c '^ ; <<< repo-identity' "$DEED")" != 1 ]; then + echo "Error: $DEED must hold exactly one repo-identity marker pair." + exit 1 + fi + awk -v frag="$ID_IN" ' + /^ ; >>> repo-identity/ { while ((getline l < frag) > 0) print l; skip = 1; next } + /^ ; <<< repo-identity/ { skip = 0; next } + !skip + ' "$DEED" > "$DEED.splice" + cat "$DEED.splice" > "$DEED" # keeps the file's mode + rm -f "$DEED.splice" + # The uuid5 name. The self-name pass already rewrote owner and slug in + # it; the forge domain is set here, because nothing else knows it. + sed -i -E 's|^( :repo-uuid #u5")[^"]*"|\1'"${FORGE}/${OWNER}/${REPO}"'"|' "$DEED" + # The template's journal is not the child's: install the stub. + mkdir -p docs/status + cp build/templates/ROADMAP.adoc.in docs/status/ROADMAP.adoc rm -rf build/templates # template-only; not part of your repo if [ "$REPO_UUID" = "UNASSIGNED" ]; then echo " uuid: UNASSIGNED — uuidgen not found." - echo " Install util-linux (uuid-runtime), then run:" + echo " Install util-linux (uuid-runtime), then check it with:" echo " uuidgen --sha1 --namespace @url --name \"${FORGE}/${OWNER}/${REPO}\"" - echo " and put the result in $CLADE_OUT." else echo " uuid: $REPO_UUID" - echo " (uuid5 of ${FORGE}/${OWNER}/${REPO} — derived, recomputable, not invented)" + echo " (uuid5 of ${FORGE}/${OWNER}/${REPO} — derived from the deed's" + echo " :repo-uuid name, recomputable, not stored, not invented)" fi - echo " clade: UNASSIGNED — a human must choose one of the 12." - echo " CLADE-003/006 FAIL until you do. That is deliberate:" - echo " an unchosen clade must not look like a chosen one." - echo " The taxonomy is listed in $CLADE_OUT." + echo " clade: none written. Identity is the chora id above; a clade is an" + echo " optional finding aid. The 12 are listed in the identity comment" + echo " of $DEED; validate-rsr checks one only if you declare it." fi - echo "" echo "── Profile & provenance (ADR-0003) ──────────────" - # The template's own rsr-profile.a2ml is deliberately MAXIMAL (it is the - # dogfood target). A minted repo must NOT inherit it: declaring + # The template deed's own (rsr-profile) clause is deliberately MAXIMAL (it + # is the dogfood target). A minted repo must NOT inherit it: declaring # capabilities the tree lacks makes the oracle score modules that are - # absent (na-honesty, RSR-SPEC-v2 §scoring). Rewrite it minimally. - PROFILE_OUT=".machine_readable/rsr-profile.a2ml" + # absent (na-honesty, RSR-SPEC-v2 §scoring). Replace it minimally, below. # ── Canon pin (bind the canon) ────────────────────────────────────────── - # Read the canon identity from the template's [canon] block BEFORE that - # block is overwritten below. A minted repo must record WHICH canon release - # it was cut from; without it "which repos are on canon 1.x?" is a campaign - # across the whole estate instead of a grep. + # Read the canon identity from the template deed's `(canon …)` clause. A + # minted repo must record WHICH canon release it was cut from; without it + # "which repos are on canon 1.x?" is a campaign across the whole estate + # instead of a grep. + # + # The pin lives only in the deed (#215 moved it there from the retired + # profile file, whose copy could not follow canon 2.1.2). + # The deed was renamed to ${REPO}_chora.deed by the self-name pass above, + # and its (canon …) clause is carried into the child unchanged, so the + # child's deed and its PROVENANCE agree at mint. # # Count rather than guessed: a value here that silently became empty is the # same class of defect as an UNASSIGNED placeholder, and would let a repo # claim conformance to nothing. - # Section reader: `canon_key ` prints the value of from - # the [canon] section, or nothing. - # - # Deliberately escape-free: the first attempt spelled the section-header - # test as /^\[/ , which reached awk as a DOUBLE backslash followed by an - # unterminated bracket expression. awk does not error on that - it parses a - # character class that swallows the rest of the program and then matches - # nothing, so the pin came out silently EMPTY rather than failing loudly. - # `substr($0,1,1)=="["` cannot be mis-escaped. This is the same class of - # defect as an UNASSIGNED placeholder: a value that looks computed but is - # empty would let a minted repo claim conformance to nothing. - canon_key() { - awk -v want="[canon]" -v key="$1" ' - substr($0,1,1)=="[" { f = ($0==want) ? 1 : 0; next } - f && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { - sub(/^[^=]*=[[:space:]]*/, "") - gsub(/^["[:space:]]+|["[:space:]]+$/, "") - print; exit - } - ' "$2" + # + # Deliberately backslash-free. The previous TOML reader first spelled its + # section test as /^\[/ , which reached awk as a DOUBLE backslash followed + # by an unterminated bracket expression; awk parsed a character class that + # swallowed the rest of the program, matched nothing, and the pin came out + # silently EMPTY. Bracket classes such as [(] cannot be mis-escaped. + # + # 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. The SAME function is in + # scripts/canon-pin-check.sh 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)" + if [ "$_n" != "1" ]; then + echo "deed_canon: $2 carries ${_n:-0} (canon …) clauses, need exactly 1" >&2 + return 0 + fi + echo "$_clauses" | grep -oE ':'"$1"'[[:space:]]+"[^"]*"' | head -1 | sed -E 's/^[^"]*"//; s/"$//' || true } + DEED_OUT="${REPO}_chora.deed" CANON_VERSION="" CANON_CRITERIA_SHA="" CANON_GATES_SHA="" - if [ -f "$PROFILE_OUT" ]; then - CANON_VERSION="$(canon_key version "$PROFILE_OUT")" - CANON_CRITERIA_SHA="$(canon_key criteria_sha256 "$PROFILE_OUT")" - CANON_GATES_SHA="$(canon_key gates_sha256 "$PROFILE_OUT")" + if [ -f "$DEED_OUT" ]; then + CANON_VERSION="$(deed_canon version "$DEED_OUT")" + CANON_CRITERIA_SHA="$(deed_canon criteria-sha256 "$DEED_OUT")" + CANON_GATES_SHA="$(deed_canon gates-sha256 "$DEED_OUT")" fi CANON_VERSION="${CANON_VERSION:-UNASSIGNED}" CANON_CRITERIA_SHA="${CANON_CRITERIA_SHA:-UNASSIGNED}" CANON_GATES_SHA="${CANON_GATES_SHA:-UNASSIGNED}" + if [ ! -f "$DEED_OUT" ]; then + echo "Error: $DEED_OUT not found after the self-name pass — cannot write the" >&2 + echo " child's (rsr-profile) / (variant) / (provenance) clauses." >&2 + exit 1 + fi + + # put_clause NAME — write the clause read from stdin as the deed's + # top-level (NAME …) clause: replace it when present, append it otherwise, + # so a re-run of repo-init never leaves two. + put_clause() { + if bash scripts/deed-field.sh --has "$DEED_OUT" "$1"; then + bash scripts/deed-clause.sh replace "$DEED_OUT" "$1" - + else + bash scripts/deed-clause.sh append "$DEED_OUT" "" - + fi + } + + DECLARES_AGAINST=$(bash scripts/deed-field.sh "$DEED_OUT" rsr-profile declares-against 2>/dev/null || true) { - echo "# SPDX-License-Identifier: MPL-2.0" - echo "#" - echo "# rsr-profile.a2ml — this repo's declared RSR v2.0 capabilities." - echo "# Generated by just repo-init on ${CURRENT_DATE}. The template's maximal" - echo "# profile was removed at mint: declare only what this tree carries." - echo "" - echo "[rsr-profile]" - echo "version = \"1.0.0\"" - echo "spec = \"rsr-criteria-v2\"" - echo "declares-against = \"2.0.0-draft\"" + echo " ; rsr-profile — this repo's declared RSR v2.0 capabilities." + echo " ; Written by just repo-init on ${CURRENT_DATE}. The template's maximal" + echo " ; profile was replaced at mint: declare only what this tree carries." + echo " (rsr-profile" + echo " :version \"1.0.0\"" + echo " :spec \"rsr-criteria-v2\"" + echo " :declares-against \"${DECLARES_AGAINST:-2.0.0-draft}\"" if [ -n "$ARCH_PRESET" ]; then - echo "preset = \"${ARCH_PRESET}\"" + echo " :preset \"${ARCH_PRESET}\")" else - echo "# UNASSIGNED: declare capabilities (or a preset) — see standards" - echo "# .machine_readable/template-capability-gates.toml" - echo "capabilities = []" + echo " ; UNASSIGNED: declare capabilities (or a preset) — see standards" + echo " ; .machine_readable/template-capability-gates.toml" + echo " :capabilities ())" fi - } > "$PROFILE_OUT" - echo " profile: $PROFILE_OUT${ARCH_PRESET:+ (preset $ARCH_PRESET)}" + } | bash scripts/deed-clause.sh replace "$DEED_OUT" rsr-profile - + echo " profile: $DEED_OUT (rsr-profile)${ARCH_PRESET:+ (preset $ARCH_PRESET)}" # Provenance — the estate's answer-file (ADR-0002 contract shape, used - # per ADR-0003 with direction=generated-from-core). parent-pin resolves - # from the live template tip: correct for a fresh mint, UNASSIGNED when - # offline — honest, never guessed. - PARENT_SLUG="hyperpolymath/rsr-template-repo" - PARENT_PIN=$(git ls-remote "https://github.com/${PARENT_SLUG}.git" HEAD 2>/dev/null | cut -f1 || true) + # per ADR-0003 with direction=generated-from-core). + # + # PARENT_BRANCH / PARENT_PIN / PARENT_TREE were resolved before the first + # write (see "Which template commit is in hand" above). Reading the live + # remote tip recorded where the remote is NOW, not the tree in hand (#203); + # reading local HEAD recorded the CHILD's own commit whenever the repo was + # created with "Use this template" (lintel, 2026-10-14). Tree equality + # against the template's history is the rule that is right in both cases. + PARENT_SLUG="$TEMPLATE_SLUG" + + # Shape, not presence: a value that is not a 40-hex object name is not a pin. + [[ "$PARENT_PIN" =~ ^[0-9a-f]{40}$ ]] || PARENT_PIN="" + [[ "$PARENT_TREE" =~ ^[0-9a-f]{40}$ ]] || PARENT_TREE="" + [ "$PARENT_BRANCH" != "HEAD" ] || PARENT_BRANCH="" + PARENT_BRANCH="${PARENT_BRANCH:-UNASSIGNED}" PARENT_PIN="${PARENT_PIN:-UNASSIGNED}" - VARIANT_OUT=".machine_readable/descriptiles/VARIANT.a2ml" - mkdir -p "$(dirname "$VARIANT_OUT")" + PARENT_TREE="${PARENT_TREE:-UNASSIGNED}" + + # Recorded in the (provenance) clause. Non-interactive mints are the automated + # path (scaffoldia / CI); interactive ones are a human at a prompt. The + # distinction matters because #201 found a hand-minted instance whose + # generated identity was stale, and nothing recorded which path ran. + if [ -n "${RSR_NON_INTERACTIVE:-}" ]; then + MINT_MODE="non-interactive" + else + MINT_MODE="interactive" + fi + { - echo "# SPDX-License-Identifier: MPL-2.0" - echo "#" - echo "# VARIANT.a2ml — provenance contract (ADR-0002 shape; ADR-0003 use)." - echo "# Records what this repo was minted from so the drift gate" - echo "# (scripts/check-variant-drift.sh ) and future" - echo "# re-templating tooling have a defined input." - echo "#" - echo "# NOTE: placeholder rendering means many files differ from the" - echo "# parent by design; folding rendered answers into [normalise] is" - echo "# future work (ADR-0003), so the gate is informational for minted" - echo "# repos until then." - echo "" - echo "[metadata]" - echo "project = \"${REPO}\"" - echo "schema_version = \"0.1.0\"" - echo "last-updated = \"${CURRENT_DATE}\"" - echo "" - echo "[variant]" - echo "parent = \"${PARENT_SLUG}\"" - echo "parent-pin = \"${PARENT_PIN}\" # template tip at mint" - echo "role = \"minted-repo\"" - echo "stack = \"${ARCHETYPE:-unspecified}\"" - echo "direction = \"generated-from-core\"" - echo "" - echo "[normalise]" - echo "rules = \"action-pins self-name\"" - echo "" - echo "[paths.added]" - echo "paths = []" - echo "" - echo "[paths.removed]" - echo "paths = []" - echo "" - echo "[paths.diverged]" - echo "# Files a minted repo owns from birth." - echo "paths = [" - echo " \".machine_readable/descriptiles/CLADE.a2ml\"," - echo " \".machine_readable/descriptiles/VARIANT.a2ml\"," - echo " \".machine_readable/rsr-profile.a2ml\"," - echo "]" - } > "$VARIANT_OUT" + echo " ; variant — provenance contract (ADR-0002 shape; ADR-0003 use)." + echo " ; Records what this repo was minted from, for re-templating tooling and" + echo " ; the drift gate. For a minted repo the gate replays the mint from" + echo " ; (provenance) and (mint-answers) and diffs this tree against the" + echo " ; replay (scripts/check-mint-drift.sh), so only edits made after the" + echo " ; mint count. Declare those in (paths) below; the derived files are" + echo " ; declared already, since their own gates keep them fresh." + echo " (variant" + echo " :project \"${REPO}\"" + echo " :schema-version \"0.1.0\"" + echo " :last-updated \"${CURRENT_DATE}\"" + echo " :parent \"${PARENT_SLUG}\"" + echo " ; template tip at mint" + echo " :parent-pin \"${PARENT_PIN}\"" + echo " :role \"minted-repo\"" + echo " :stack \"${ARCHETYPE:-unspecified}\"" + echo " :direction \"generated-from-core\"" + echo " (normalise :rules \"action-pins self-name\")" + echo " ; Files a minted repo owns from birth are (paths :diverged …)." + echo " (paths" + # The paths this work tree changed before the mint are added to these + # lists once the mint has moved and deleted what it will (see "What + # this work tree held before the mint", below). + _list() { local _s="" _x; printf ' :%s (' "$1"; shift; for _x in "$@"; do printf '%s"%s"' "$_s" "$_x"; _s=" "; done; printf ')'; } + _removed=("${MINT_REMOVES[@]}") + [ "${DEED_IN#./}" = "${REPO}_chora.deed" ] || _removed+=("${DEED_IN#./}") + _list added; echo + _list removed "${_removed[@]}"; echo + _list diverged "${REPO}_chora.deed" docs/status/ROADMAP.adoc CLAUDE.md docs/architecture/REPOSITORY-MAP.adoc; echo "))" + } | put_clause variant if [ "$PARENT_PIN" = "UNASSIGNED" ]; then - echo " provenance: $VARIANT_OUT — parent-pin UNASSIGNED (offline?)" + echo " provenance: $DEED_OUT (variant) — parent-pin UNASSIGNED (offline?)" echo " Set it to the template commit you minted from:" echo " git ls-remote https://github.com/${PARENT_SLUG}.git HEAD" else - echo " provenance: $VARIANT_OUT (parent-pin ${PARENT_PIN:0:12})" + echo " provenance: $DEED_OUT (variant) (parent-pin ${PARENT_PIN:0:12})" fi - # ── PROVENANCE.a2ml — the answer-file for the canon binding ───────────── + # ── (provenance) — the answer-file for the canon binding ───────────────── # ADR-0003 named this the single highest-leverage gap: "a minted repo does # not know which template commit it came from, so template improvements # cannot be propagated and drift cannot be detected." Copier/Cruft solved # it with an answer-file + update + check; this estate hand-simulated it as # recurring standardisation-PR campaigns. This is the answer-file. # - # VARIANT.a2ml above records the TEMPLATE dimension (diffing against the + # (variant) above records the TEMPLATE dimension (diffing against the # parent). This records the CANON dimension (which law the repo was cut # under). They are different questions and both are needed. - PROVENANCE_OUT=".machine_readable/PROVENANCE.a2ml" + # Flat keys only: a nested form headed "canon" would be counted by + # deed_canon above and empty the pin. { - echo "# SPDX-License-Identifier: MPL-2.0" - echo "#" - echo "# PROVENANCE.a2ml — what this repo was minted FROM." - echo "# Written once, at mint, by build/just/repo-init.just. Not a hand-edited" - echo "# file: tools that want \"what is this repo\" read STATE.a2ml; tools that" - echo "# want \"where did this repo come from\" read this." - echo "" - echo "[provenance]" - echo "minted_at = \"${CURRENT_DATE}\"" - echo "" - echo "template_repo = \"${PARENT_SLUG}\"" - echo "template_commit = \"${PARENT_PIN}\"" - echo "" - echo "canon_version = \"${CANON_VERSION}\"" - echo "criteria_sha256 = \"${CANON_CRITERIA_SHA}\"" - echo "gates_sha256 = \"${CANON_GATES_SHA}\"" - echo "" - echo "archetype = \"${ARCHETYPE:-}\"" - echo "minted_by = \"hand\"" - echo "" - echo "[substitutions]" - echo "# ADR-0003 records the old state honestly: \"renders 34 substitutions and" - echo "# derives identity, but leaves 12 tokens UNASSIGNED\". Listing them here" - echo "# converts that from a known defect into a QUERYABLE one: a non-empty" - echo "# list means the mint did not fully render, and the validation step" - echo "# below already fails on a leftover token." - echo "rendered = 34" - echo "unassigned = []" - } > "$PROVENANCE_OUT" + echo " ; provenance — what this repo was minted FROM. Written once, at mint," + echo " ; by build/just/repo-init.just. Tools that want \"what is this repo\" read" + echo " ; the rest of the deed; tools that want \"where did it come from\" read this." + echo " (provenance" + echo " :minted-at \"${CURRENT_DATE}\"" + echo " :template-repo \"${PARENT_SLUG}\"" + echo " :template-branch \"${PARENT_BRANCH}\"" + echo " :template-commit \"${PARENT_PIN}\"" + echo " :template-tree \"${PARENT_TREE}\"" + echo " ; Contract: branches this repo is EXPECTED to carry beyond the default." + echo " ; Empty means default-only. A mint must never inherit template work" + echo " ; branches (coderabbit/, chore/, bot-task or stale branches) — that is" + echo " ; how knot-knot received a byte-identical copy of a template work branch" + echo " ; with no common ancestor (#203). Enforced by" + echo " ; scripts/check-template-conformance.sh." + echo " :extra-branches ()" + echo " :canon-version \"${CANON_VERSION}\"" + echo " :criteria-sha256 \"${CANON_CRITERIA_SHA}\"" + echo " :gates-sha256 \"${CANON_GATES_SHA}\"" + echo " :archetype \"${ARCHETYPE:-}\"" + echo " ; Which minting path actually ran (#201/#203 needed this signal)." + echo " :minted-by \"repo-init\"" + echo " :mint-mode \"${MINT_MODE}\"" + echo " ; A non-empty :unassigned list means the mint did not fully render;" + echo " ; the validation step below already fails on a leftover token." + echo " (substitutions :rendered 34 :unassigned ()))" + } | put_clause provenance + + # ── (mint-answers) — the answers, so the mint can be replayed ─────────── + # Provenance names the template commit; this names what it was rendered + # with. Together they make the mint a function that can be run again: + # scripts/check-mint-drift.sh re-mints the template at the pin with these + # answers and diffs the child against the result, so drift is measured + # exactly instead of through hand-written (normalise) rules (ADR-0003). + # Owner ruling (2026-11-04): record only values the mint already writes + # into the child's own files (names, contacts, description, URLs), so the + # record publishes nothing new. Deed strings escape `\` and `"`. + deed_str() { printf '%s' "$1" | sed -e 's/[\\"]/\\&/g'; } + # The ruling is checked here, not left to a hand-kept list (Codex review of + # #275 found project-type recorded, though no template file renders it): a + # non-empty answer is recorded only if its value appears verbatim in this + # repo's files. An answer left out replays as unanswered; it rendered + # nothing here, so the replay is unchanged. + # Not the template-only trees the mint deletes below (MINT_REMOVES: + # archetypes/, build/docs-seed/): a value found only there is in no file + # the child keeps (CodeRabbit review of #275). + mapfile -t _MINTED < <(mint_files | awk -v r="${MINT_REMOVES[*]:-}" \ + 'BEGIN { n = split(r, p, " ") } { for (i = 1; i <= n; i++) if (index($0, p[i]) == 1) next; print }') + _ANSWERS=() _UNRECORDED="" + for _pair in project-name:PROJECT_NAME repo:REPO owner:OWNER author:AUTHOR \ + author-email:AUTHOR_EMAIL author-org:AUTHOR_ORG \ + author-email-alt:AUTHOR_EMAIL_ALT author-orcid:AUTHOR_ORCID \ + description:PROJECT_DESCRIPTION forge:FORGE \ + security-email:SECURITY_EMAIL conduct-email:CONDUCT_EMAIL \ + project-type:PROJECT_TYPE website:WEBSITE \ + openssf-bp-id:OPENSSF_BP_ID service-name:SERVICE_NAME \ + port:PORT registry:REGISTRY archetype:ARCHETYPE \ + package-uuid:PACKAGE_UUID; do + _var="${_pair#*:}" + _val="${!_var:-}" + if [ -n "$_val" ] && ! grep -qsF -- "$_val" "${_MINTED[@]}" /dev/null; then + _UNRECORDED="${_UNRECORDED} ${_pair%%:*}" + continue + fi + _ANSWERS+=(" :${_pair%%:*} \"$(deed_str "$_val")\"") + done + { + echo " ; mint-answers — what just repo-init was answered on ${CURRENT_DATE}." + echo " ; Read by scripts/check-mint-drift.sh to replay the mint. Every value" + echo " ; here already appears in this repo's rendered files." + echo " (mint-answers" + printf '%s\n' "${_ANSWERS[@]}" + echo " :mint-date \"${CURRENT_DATE}\")" + } | put_clause mint-answers + echo " answers: $DEED_OUT (mint-answers) — replayable with scripts/check-mint-drift.sh" + [ -z "$_UNRECORDED" ] || echo " not recorded, written into no file here:${_UNRECORDED}" if [ "$CANON_VERSION" = "UNASSIGNED" ]; then - echo " provenance: $PROVENANCE_OUT — canon pin UNASSIGNED" - echo " The template carried no [canon] block, so this repo" + echo " provenance: $DEED_OUT (provenance) — canon pin UNASSIGNED" + echo " The template deed carried no (canon …) clause, so this repo" echo " cannot state which canon it conforms to. Fix by minting" echo " from a template commit that has one." else - echo " provenance: $PROVENANCE_OUT (canon v${CANON_VERSION} ${CANON_CRITERIA_SHA:0:12})" + echo " provenance: $DEED_OUT (provenance) (canon v${CANON_VERSION} ${CANON_CRITERIA_SHA:0:12})" + CANON_PROPOSED_BY="$(deed_canon proposed-by "$DEED_OUT")" + if [ -n "$CANON_PROPOSED_BY" ]; then + echo " WARNING: the template pins canon v${CANON_VERSION} ahead of its release" + echo " (:proposed-by \"${CANON_PROPOSED_BY}\"). This repo inherits that pin;" + echo " its Canon lockstep passes only while that canon PR is open and" + echo " unchanged, and once it merges. Re-mint or re-pin if it is closed." + fi fi # archetypes/ is template-only overlay data, not part of your repo # (same reason build/templates is removed above). rm -rf archetypes rm -rf build/docs-seed # template-only; its content now lives in docs/ + # Its SD022 exemption goes with it: a child keeping an exemption for a path + # it does not have fails check-hypatia-ignore.sh (stale exemption). The + # block runs from its section header to the entry, plus the blank after. + if [ -f .hypatia-ignore ]; then + awk '/^# ─── build\/docs-seed\/ describes DOWNSTREAM/ { skip = 1 } + skip && /^structural_drift\/SD022:build\/docs-seed\/$/ { skip = 0; blank = 1; next } + skip { next } + blank && /^$/ { blank = 0; next } + { blank = 0; print }' .hypatia-ignore > .hypatia-ignore.mint + mv .hypatia-ignore.mint .hypatia-ignore + fi + # The Hypatia rule-order policy classifies each module .hypatia-ignore + # exempts, and check-hypatia-rule-order.sh fails on a classification with + # no exemption behind it. With no structural_drift exemption left, drop its + # observed_modules entry. The policy is one line of JCS (RFC 8785), so + # removing one object and its comma keeps it canonical; no JSON tool needed. + POLICY=.machine_readable/policies/hypatia-rule-order.json + if [ -f "$POLICY" ] && ! grep -q '^structural_drift/' .hypatia-ignore 2>/dev/null; then + SD_OBJ='[{]"layer":[0-9]+,"module":"structural_drift","rationale":"([^"\\]|\\.)*"[}]' + sed -E -i -e "s/${SD_OBJ},//" -e "s/,${SD_OBJ}//" "$POLICY" + if grep -q '"module":"structural_drift"' "$POLICY"; then + echo "ERROR: could not drop structural_drift from $POLICY; the mint stops here." >&2 + exit 1 + fi + fi - # Overlay .in rule (julia-library archetype; the convention any future - # overlay can adopt): the overlay ships content templates as *.in so a - # substitution token never survives in a file with a final name. Rename - # the known set now that substitution has rendered them, before the - # placeholder gate below. - if [ "$ARCHETYPE" = "julia-library" ]; then - # the name is read from Project.toml.in until the loop below renames - # it - order matters. + # Overlay .in rule (every archetype): an overlay ships content templates as + # *.in so a substitution token never survives in a file whose final name is + # already the real one — a half-rendered `Project.toml` is a file `Pkg` + # reads, while a half-rendered `Project.toml.in` is obviously unfinished. + # Only files THIS overlay added are renamed: the spine's own .in templates + # (build/templates/, the k9 examples) keep their suffix. + # + # src/PACKAGE.jl.in is the one exception, handled after the loop: the Julia + # convention names the module file after the package, and the package name + # is only known once Project.toml.in has been rendered and renamed. + for _of in "${OVERLAY_FILES[@]}"; do + case "$_of" in + src/PACKAGE.jl.in) continue ;; + *.in) [ -f "$_of" ] && { mv "$_of" "${_of%.in}"; echo " overlay: ${_of%.in}"; } ;; + esac + done + if [ "$ARCHETYPE" = "julia-library" ] && [ -f src/PACKAGE.jl.in ]; then TOML_FILE=Project.toml - [ -f "$TOML_FILE" ] || TOML_FILE=Project.toml.in - if [ -f src/PACKAGE.jl.in ] && [ -f "$TOML_FILE" ]; then + if [ -f "$TOML_FILE" ]; then PKG_NAME=$(sed -n 's/^name = "\([^"]*\)".*/\1/p' "$TOML_FILE" | head -1) if [ -n "$PKG_NAME" ] && [ "$PKG_NAME" != "UNASSIGNED" ]; then mv "src/PACKAGE.jl.in" "src/${PKG_NAME}.jl" @@ -687,11 +1610,162 @@ repo-init archetype="": echo " mv src/PACKAGE.jl.in src/.jl" fi fi - for f in Project.toml.in \ - .github/workflows/julia-ci.yml.in \ - .github/workflows/julia-docs.yml.in; do - [ -f "$f" ] && { mv "$f" "${f%.in}"; echo " overlay: ${f%.in}"; } - done + fi + + # ── www/ site-operations bundle (issue #53, stage 5) ────────────────── + # + # A repository minted from this template already carries www/.well-known/ + # as the canonical location and has no root .well-known/, so in the + # ordinary case the migration below does nothing at all. It exists for the + # case that is not ordinary: repo-init run over a tree that predates #53 + # and still has a root .well-known/. The migrator moves it — quarantining + # anything divergent rather than overwriting it — and the bundle's own + # suite (under Validation) then proves the result. It runs HERE, before the + # map and the arrival pack are regenerated, because it moves files: the map + # must be the last thing written, or it describes a tree that no longer + # exists (lintel shipped a stale map and Estate Rules failed until lintel#8). + # + # #106 described this wiring as already present. It was not: nothing + # invoked the migrator or the suite at mint, which is why the estate + # still carries root .well-known/ directories years into the convention. + if [ -d .well-known ] && [ -f scripts/migrate-wellknown-to-www.sh ]; then + echo "" + echo "Migrating legacy root .well-known/ into www/.well-known/..." + bash scripts/migrate-wellknown-to-www.sh --date "$CURRENT_DATE" + fi + + # ── Toolchain (mise.toml) ─────────────────────────────────────────────── + # The provisioning standard says mise.toml is MINTED per repository (canon's + # mise.toml.tmpl carries __MISE_TOOLS_TOML__ for exactly that). Until now the + # mint copied the spine's file verbatim, so every child declared the + # TEMPLATE's toolchain: the spine once declared nine runtimes (python, node, + # rust, go, zig, java, bun, denojs, npm, pip, cargo, black, ruff, pytest …) + # and a minted Julia package therefore installed Python — banned estate-wide + # — declared no Julia, and ran `cargo build` from `just build`. + # + # Rendered here, after the overlay's manifests have their final names (so + # detection can see Project.toml / Cargo.toml) and after the archetype prune + # (so a dropped module does not keep its toolchain). + if [ -f scripts/render-mise.sh ]; then + echo "" + echo "── Toolchain ────────────────────────────────────" + bash scripts/render-mise.sh --root . || { + echo "ERROR: mise.toml could not be rendered from this tree's languages." + exit 1 + } + fi + + # ── What this work tree held before the mint ──────────────────────────── + # Declared here, after every step that moves or deletes files, as exactly + # what this tree now differs by from the replay taken before the first write + # (above): the comparison the drift gate makes, so the declaration cannot + # disagree with it, whatever the mint did to a pre-mint change on the way + # (moved, rendered, overwritten, or reached through an edited input) (Codex + # and CodeRabbit review of #275). The files this mint owns are declared + # already. Written into the (variant (paths)) lists by awk, which takes the + # paths as data and escapes \ and " in deed strings. + if [ -n "$_REPLAY" ]; then + _owned=("${REPO}_chora.deed" docs/status/ROADMAP.adoc CLAUDE.md docs/architecture/REPOSITORY-MAP.adoc) + _is_owned() { local _o; for _o in "${_owned[@]}"; do [ "$1" = "$_o" ] && return 0; done; return 1; } + # _files DIR — DIR's own files, one per line (tracked, or untracked and not + # ignored), as the drift gate lists them. This tree is read under its own + # git configuration, as `git add -A` will commit it (a file only a global + # excludesFile ignores is never committed, so never declared); the + # replay under none (CodeRabbit review of #275). + _files() { + { if [ "$1" = "$_REPLAY" ]; then _rg -C "$1" ls-files -z --cached --others --exclude-standard + else git -C "$1" ls-files -z --cached --others --exclude-standard; fi; } \ + | while IFS= read -r -d '' _f; do + if [ -e "$1/$_f" ] || [ -L "$1/$_f" ]; then printf '%s\n' "$_f"; fi + done | LC_ALL=C sort -u + } + _pml="$(mktemp -d)"; : > "$_pml/a"; : > "$_pml/r"; : > "$_pml/d" + _files . > "$_pml/child" + _files "$_REPLAY" > "$_pml/replay" + while IFS= read -r _p; do + _is_owned "$_p" && continue + if ! grep -qxF -- "$_p" "$_pml/replay"; then printf '%s\n' "$_p" >> "$_pml/a" + elif ! _same "$_p" "$_REPLAY/$_p"; then printf '%s\n' "$_p" >> "$_pml/d" + fi + done < "$_pml/child" + while IFS= read -r _p; do + _is_owned "$_p" && continue + grep -qxF -- "$_p" "$_pml/child" || printf '%s\n' "$_p" >> "$_pml/r" + done < "$_pml/replay" + awk -v fa="$_pml/a" -v fr="$_pml/r" -v fd="$_pml/d" ' + function load(file, s, p) { + s = "" + while ((getline p < file) > 0) { + gsub(/\\/, "\\\\", p); gsub(/"/, "\\\"", p) + s = s " \"" p "\"" + } + close(file); return s + } + # insert EXTRA before the ")" that closes the list opened at "(". + function extend(line, extra, i, c, q, open, out) { + open = index(line, "("); q = 0 + for (i = open + 1; i <= length(line); i++) { + c = substr(line, i, 1) + if (q && c == "\\") { i++; continue } + if (c == "\"") q = !q + else if (!q && c == ")") { + out = substr(line, 1, i - 1) extra substr(line, i) + sub(/\( "/, "(\"", out) + return out + } + } + return line + } + BEGIN { A = load(fa); R = load(fr); D = load(fd) } + /^ :added \(/ { $0 = extend($0, A) } + /^ :removed \(/ { $0 = extend($0, R) } + /^ :diverged \(/ { $0 = extend($0, D) } + { print } + ' "$DEED_OUT" > "$_pml/deed" && cat "$_pml/deed" > "$DEED_OUT" + echo " pre-mint: $(wc -l < "$_pml/a") added, $(wc -l < "$_pml/d") diverged, $(wc -l < "$_pml/r") removed;" + echo " declared in $DEED_OUT (variant (paths)) as this repo's own" + rm -rf "$_pml" "$(dirname "$_REPLAY")" + fi + + # ── Regenerate what is DERIVED from the deed and the tree ─────────────── + # CLAUDE.md's arrival pack and the repository map are generated files, and + # until now the mint left both describing the TEMPLATE. The self-name pass + # swaps the slug inside them, which is worse than leaving them alone: the + # pack then states the template's uuid and clade, maturity `production`, + # and pipeline position `foundation` with chain "standards -> + # -> (every estate repo)", under the child's name. Measured 2026-10-14 on + # resource-algebra. The map also kept the archetypes/ root entry, + # which this recipe deletes, so Estate Rules' map-freshness step failed in + # every minted child. Both are regenerated here, after every deed clause + # above is written and every template-only path is gone. Fail-closed: a + # generator that cannot run (no nickel on PATH) stops the mint rather than + # shipping a pack that names the wrong repository. + # + # This is the LAST step that writes the tree; everything after it only + # checks (tests/e2e/use_template_mint_test.sh commits the result and runs + # the freshness check on it). The map is read from the work tree + # (--worktree), not the index: the mint runs before the first commit, so + # the index still lists archetypes/ and the template's deed name, which is + # how lintel shipped a map that failed Estate Rules from birth. + if [ -f .machine_readable/arrival-pack/generate.sh ]; then + bash .machine_readable/arrival-pack/generate.sh + echo " arrival pack: CLAUDE.md regenerated from ${DEED_OUT}" + fi + if [ -f scripts/gen-repo-map.sh ]; then + bash scripts/gen-repo-map.sh --worktree . >/dev/null + echo " repo map: docs/architecture/REPOSITORY-MAP.adoc regenerated" + fi + + # Replay (scripts/check-mint-drift.sh): every tree-writing step has run, so + # the reconstruction is complete. What follows only checks the tree: the placeholder + # gate (a partial mint made with RSR_ALLOW_PLACEHOLDERS=1 would fail it on + # replay), the www suite (a past RSR_MINT_DATE fails it: security.txt + # expires at the end of the mint year) and the rest. The child's own CI + # runs those checks on the child; the replay exists only to be compared. + if [ "${RSR_MINT_REPLAY:-0}" = "1" ]; then + echo "" + echo "Replay complete (RSR_MINT_REPLAY=1): post-write checks left to the child's CI." + exit 0 fi echo "" @@ -727,17 +1801,24 @@ repo-init archetype="": # gate sensibly exempting metasyntax must let the block through. The two # checks here are therefore complementary, not redundant — the block can # only be caught by name, which the check below does. - # The four allowlisted paths are copied verbatim from - # scripts/check-no-placeholders.sh so the two gates cannot drift apart. - # Each legitimately names tokens: the token vocabulary itself, prose - # explaining that tokens exist, the checker's own pattern, and the e2e - # test's answer list. + # The allowlisted paths are copied from scripts/check-no-placeholders.sh so + # the two gates cannot drift apart. Each legitimately names tokens: the + # token vocabulary itself, prose explaining that tokens exist, and the + # checker's own pattern. Template machinery that names tokens in its + # fixtures (the e2e answer list, the Julia UUID test) is in + # .machine_readable/mint-verbatim.txt, so mint_files never lists it, and + # the checker reads the same manifest. PATTERN="${LB}[A-Z_]*${RB}" META='PLACEHOLDER|ANYTHING|TOKEN|UPPER_SNAKE' - REMAINING=$(grep -rl "$PATTERN" . --include='*.md' --include='*.adoc' --include='*.yml' --include='*.yaml' --include='*.a2ml' --include='*.toml' --include='*.scm' --include='*.ncl' --include='*.nix' --include='*.json' --include='*.sh' 2>/dev/null | grep -v '.git/' | while IFS= read -r f; do - case "${f#./}" in - .machine_readable/ai/PLACEHOLDERS.adoc|EXPLAINME.adoc|scripts/check-no-placeholders.sh|tests/e2e/template_instantiation_test.sh) continue ;; + REMAINING=$(mint_files | while IFS= read -r f; do + case "$f" in + *.md|*.adoc|*.yml|*.yaml|*.deed|*.toml|*.scm|*.ncl|*.nix|*.json|*.sh) ;; + *) continue ;; + esac + case "$f" in + .machine_readable/ai/PLACEHOLDERS.adoc|EXPLAINME.adoc|scripts/check-no-placeholders.sh) continue ;; esac + grep -q "$PATTERN" "$f" 2>/dev/null || continue # Bracketed [{]{2} rather than a doubled brace written literally, for # two independent reasons. First, `grep -E` is ugrep on some estate # machines and rejects a bare doubled brace as an "empty @@ -771,15 +1852,17 @@ repo-init archetype="": # token, and once its literal metasyntax is the only doubled-brace text left # the check above cannot distinguish the two. Fail on it by name. # - # The strip pass, this recipe and the e2e test are excluded because they - # necessarily NAME the marker in order to describe or assert on it — the fix - # for the defect would otherwise be flagged by the check for the defect, and - # so would the test that guards the fix, and the cleanup test whose - # fixture prose must keep the phrase to prove prose is not collateral. - # All four are tooling, not + # Read over mint_files, so the files that necessarily NAME the marker in + # order to describe or assert on it (the strip tool, this recipe, the e2e + # test, the cleanup test whose fixture must keep the phrase) are exempt by + # the same manifest that kept the strip pass out of them: they are listed + # in .machine_readable/mint-verbatim.txt. Before, this list exempted them + # here while the strip pass rewrote them anyway. All are tooling, not # community-health documents, so a mention in them cannot mislead a reader # about what this project's Code of Conduct says. - LEFTOVER_BLOCKS=$(grep -rl 'TEMPLATE INSTRUCTIONS' . --exclude-dir=.git 2>/dev/null | grep -vE '^\./(scripts/strip-instruction-blocks\.rs|build/just/repo-init\.just|tests/e2e/template_instantiation_test\.sh|tests/workflows/mint_cleanup_test\.sh)$' || true) + LEFTOVER_BLOCKS=$(mint_files | while IFS= read -r f; do + grep -q 'TEMPLATE INSTRUCTIONS' "$f" 2>/dev/null && echo "$f" + done || true) if [ -n "$LEFTOVER_BLOCKS" ]; then echo "" echo "Un-deleted TEMPLATE INSTRUCTIONS block in:" @@ -788,23 +1871,37 @@ repo-init archetype="": exit 1 fi - # ── www/ site-operations bundle (issue #53, stage 5) ────────────────── - # - # A repository minted from this template already carries www/.well-known/ - # as the canonical location and has no root .well-known/, so in the - # ordinary case both steps below do nothing at all. They exist for the - # case that is not ordinary: repo-init run over a tree that predates #53 - # and still has a root .well-known/. The migrator moves it — quarantining - # anything divergent rather than overwriting it — and the bundle's own - # suite then proves the result. - # - # #106 described this wiring as already present. It was not: nothing - # invoked the migrator or the suite at mint, which is why the estate - # still carries root .well-known/ directories years into the convention. - if [ -d .well-known ] && [ -f scripts/migrate-wellknown-to-www.sh ]; then + # ── The gates the template enforces on children, run on the child ──────── + # Each of these was once a check the template shipped that could not fail: + # * the SPDX check globbed one language's extensions, so in a Julia repo it + # examined nothing and reported PASS; + # * the container check asked "is cgr.dev named anywhere" and passed on a + # floating :latest, so a template shipping two of them passed itself; + # * the toolchain check did not exist, and the spine's mise.toml declared + # nine runtimes for repositories that use none of them. + # Running them HERE means a mint that would fail them fails at mint, in the + # template's own CI, instead of in N children after the fact. + echo "" + echo "── Language, licence and container gates ────────" + GATE_FAIL=0 + for _gate in check-spdx-headers check-mise-language check-container-pins; do + [ -f "scripts/${_gate}.sh" ] || continue echo "" - echo "Migrating legacy root .well-known/ into www/.well-known/..." - bash scripts/migrate-wellknown-to-www.sh + echo " ${_gate}.sh" + if bash "scripts/${_gate}.sh" --root .; then + echo " ${_gate}: OK" + else + echo " ${_gate}: FAILED" + GATE_FAIL=1 + fi + done + if [ "$GATE_FAIL" -ne 0 ]; then + echo "" + echo "ERROR: the minted tree fails a gate this template enforces on children." + echo " The tree above is complete but not conforming; fix it before" + echo " committing. (A mint that ships a failing tree teaches the child" + echo " that the gate is noise.)" + exit 1 fi # Fail-closed, like `just verify` below: the suite tolerates unminted @@ -853,5 +1950,7 @@ repo-init archetype="": echo " must be set to ci/.gitlab-ci.yml, or GitLab CI silently never" echo " runs - no pipeline and no error. See ci/README.adoc." fi - echo " 8. HARNESS — arm the drift gate against your recorded parent-pin:" - echo " scripts/check-variant-drift.sh " + echo " 8. HARNESS — check the recorded pin, then arm the drift gate, which replays" + echo " this mint from (provenance) and (mint-answers) and diffs the result:" + echo " scripts/check-template-conformance.sh" + echo " scripts/check-variant-drift.sh " diff --git a/build/just/validate.just b/build/just/validate.just index 5718a77..37fb553 100644 --- a/build/just/validate.just +++ b/build/just/validate.just @@ -18,36 +18,103 @@ validate-rsr: [ -f "$f" ] || MISSING="$MISSING $f" done # The repo deed's filename carries the repo slug (_chora.deed), so - # this is a glob; the folded replacement of the old 0-AI-MANIFEST.a2ml. + # this is a glob. ls *_chora.deed >/dev/null 2>&1 || MISSING="$MISSING repo-deed (*_chora.deed)" - for f in .machine_readable/descriptiles/STATE.a2ml .machine_readable/descriptiles/META.a2ml .machine_readable/descriptiles/ECOSYSTEM.a2ml .machine_readable/descriptiles/anchors/ANCHOR.a2ml .machine_readable/policies/MAINTENANCE-AXES.a2ml .machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml .machine_readable/policies/SOFTWARE-DEVELOPMENT-APPROACH.a2ml; do - [ -f "$f" ] || MISSING="$MISSING $f" - done + # .a2ml is retired: a repo carrying one is non-conformant, whatever else holds. + bash scripts/check-no-a2ml.sh . >/dev/null || MISSING="$MISSING no-a2ml(run scripts/check-no-a2ml.sh)" for f in docs/legal/EXHIBIT-A-ETHICAL-USE.txt docs/legal/EXHIBIT-B-QUANTUM-SAFE.txt LICENSES/MPL-2.0.txt; do [ -f "$f" ] || MISSING="$MISSING $f" done - if [ ! -d "src/interface/abi" ] && [ ! -d "src/interface/Abi" ]; then - MISSING="$MISSING src/interface/abi" + # The polyglot seam (Idris2 ABI over a Zig FFI) is INHERITED from the + # template's spine, not required of every repository: an archetype whose + # preset declares neither the abi nor the ffi capability prunes src/interface/ + # at mint (ADR-0003, archetypes//prune.txt). A minted Rust CLI is not + # incomplete for lacking a seam it did not ask for. So: a tree that carries + # the seam must carry all of it; a tree that carries none of it is complete + # without it. Deleting one part of a present seam still fails. + if [ -d src/interface ]; then + if [ ! -d "src/interface/abi" ] && [ ! -d "src/interface/Abi" ]; then + MISSING="$MISSING src/interface/abi" + fi + for f in src/interface/ffi src/interface/generated; do + [ -d "$f" ] || MISSING="$MISSING $f" + done fi - for f in src/interface/ffi src/interface/generated; do - [ -d "$f" ] || MISSING="$MISSING $f" - done for f in docs/governance/MAINTENANCE-CHECKLIST.adoc docs/governance/SOFTWARE-DEVELOPMENT-APPROACH.adoc; do [ -f "$f" ] || MISSING="$MISSING $f" done - if [ -f ".machine_readable/descriptiles/META.a2ml" ]; then - grep -q 'axis-1 = "must > intend > like"' .machine_readable/descriptiles/META.a2ml || MISSING="$MISSING META.a2ml:axis-1" - grep -q 'axis-2 = "corrective > adaptive > perfective"' .machine_readable/descriptiles/META.a2ml || MISSING="$MISSING META.a2ml:axis-2" - grep -q 'axis-3 = "systems > compliance > effects"' .machine_readable/descriptiles/META.a2ml || MISSING="$MISSING META.a2ml:axis-3" - grep -q 'scoping-first = true' .machine_readable/descriptiles/META.a2ml || MISSING="$MISSING META.a2ml:scoping-first" - grep -q 'idris-unsound-scan = "believe_me/assert_total"' .machine_readable/descriptiles/META.a2ml || MISSING="$MISSING META.a2ml:idris-unsound-scan" - grep -q 'audit-focus = "systems in place, documentation explains actual state, safety/security accounted for, observed effects reviewed"' .machine_readable/descriptiles/META.a2ml || MISSING="$MISSING META.a2ml:audit-focus" - grep -q 'compliance-focus = "seams/compromises/exception register, bounded exceptions, anti-drift checks"' .machine_readable/descriptiles/META.a2ml || MISSING="$MISSING META.a2ml:compliance-focus" - grep -q 'effects-evidence = "benchmark execution/results and maintainer status dialogue/review"' .machine_readable/descriptiles/META.a2ml || MISSING="$MISSING META.a2ml:effects-evidence" - grep -q 'compliance-tooling = "panic-attack"' .machine_readable/policies/MAINTENANCE-AXES.a2ml || MISSING="$MISSING MAINTENANCE-AXES.a2ml:compliance-tooling" - grep -q 'effects-tooling = "ecological checking with sustainabot guidance"' .machine_readable/policies/MAINTENANCE-AXES.a2ml || MISSING="$MISSING MAINTENANCE-AXES.a2ml:effects-tooling" - grep -q 'source-human = "docs/governance/MAINTENANCE-CHECKLIST.adoc"' .machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml || MISSING="$MISSING MAINTENANCE-CHECKLIST.a2ml:source-human" - grep -q 'source-human = "docs/governance/SOFTWARE-DEVELOPMENT-APPROACH.adoc"' .machine_readable/policies/SOFTWARE-DEVELOPMENT-APPROACH.a2ml || MISSING="$MISSING SOFTWARE-DEVELOPMENT-APPROACH.a2ml:source-human" + # Identity, lifecycle, maintenance axes, the anchor and the maintenance + # policies are all clauses of the repo deed (rsr-template-repo#209). + DEED=$(bash scripts/deed-field.sh --find . || true) + if [ -n "$DEED" ] && bash scripts/deed-field.sh --has "$DEED" status; then + # Prints the field, or nothing when it is absent. + field() { bash scripts/deed-field.sh "$DEED" "$1" "$2" || true; } + # Adds a MISSING entry unless deed field CLAUSE KEY equals the expected value. + want() { + [ "$(field "$1" "$2")" = "$3" ] || MISSING="$MISSING $DEED:$1:$2" + } + # Identity is the chora (standards A1, rulings R4, U1 to U4, V1): one + # :repo-uuid on the deed header, minted once from the repo's birth + # path and carried as the opaque literal #uuid"", + # version 5 or 8. The legacy name literals #u5"…" and #u8"…" are still + # read and are reported, not failed: their cutover date is the owner's. + RAW_UUID="$(sed -n -E 's/^[[:space:]]*:repo-uuid[[:space:]]+(#(uuid|u5|u8)"[^"]*").*/\1/p' "$DEED" | head -1)" + case "$RAW_UUID" in + '#uuid"'*) + printf '%s' "$RAW_UUID" | grep -Eq '^#uuid"[0-9a-f]{8}-[0-9a-f]{4}-[58][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"$' \ + || MISSING="$MISSING $DEED:repo-uuid(#uuid literal is not a lowercase v5/v8 RFC 9562 id)" ;; + '#u5"'?*|'#u8"'?*) + echo "NOTE: $DEED :repo-uuid is the legacy literal $RAW_UUID; the chora id is #uuid\"\" (standards A1, V1)." ;; + *) MISSING="$MISSING $DEED:repo-uuid" ;; + esac + bash scripts/deed-field.sh --has "$DEED" identity || MISSING="$MISSING $DEED:(identity)" + [ -n "$(field identity primary-forge)" ] || MISSING="$MISSING $DEED:identity:primary-forge" + # The clade is an outward finding aid, never identity (standards A1 §11, + # ruling R4): no clause and no prefixed name are required. A clade that + # IS declared must still be one of the 12 and agree with its name, so a + # finding aid cannot point at the wrong shelf. + if bash scripts/deed-field.sh --has "$DEED" clade; then + case "$(field clade primary)|$(field clade primary-name)" in + "fv|Formal Verification & Proofs"|"nl|Nextgen Languages"|"rm|Repo Management & Tooling"|\ + "gv|Governance & Standards"|"db|Databases"|"ap|Applications"|"ix|Infrastructure & Cloud"|\ + "dx|Developer Ecosystem"|"pt|Protocols & Interop"|"ax|AI & Neurosymbolic"|\ + "gm|Games & Interactive"|"sc|Security") ;; + "UNASSIGNED|UNASSIGNED"|"|") + echo "NOTE: $DEED (clade …) names no clade; it is optional, so choose one or delete the clause." ;; + *) MISSING="$MISSING $DEED:clade(primary/primary-name not one of the 12, or they disagree)" ;; + esac + fi + # CLADE-004/005: the lifecycle phase is a closed set. + case "$(field status phase)" in + reserved|incubating|active|dormant|merged|superseded|archived|extinct) ;; + *) MISSING="$MISSING $DEED:status:phase" ;; + esac + case "$(field maturity level)" in + experimental|alpha|beta|production|lts) ;; + *) MISSING="$MISSING $DEED:maturity:level" ;; + esac + # The ecosystem clause names the repo the deed's header names. + # Both empty would compare equal; an identity must be present. + [ -n "$(field "" canonical-name)" ] \ + && [ "$(field ecosystem project)" = "$(field "" canonical-name)" ] \ + || MISSING="$MISSING $DEED:ecosystem:project" + want meta axis-1 "must > intend > like" + want meta axis-2 "corrective > adaptive > perfective" + want meta axis-3 "systems > compliance > effects" + want meta scoping-first true + want meta/scoping idris-unsound-scan "believe_me/assert_total" + want meta/axis-3 audit-focus "systems in place, documentation explains actual state, safety/security accounted for, observed effects reviewed" + want meta/axis-3 compliance-focus "seams/compromises/exception register, bounded exceptions, anti-drift checks" + want meta/axis-3 effects-evidence "benchmark execution/results and maintainer status dialogue/review" + for c in anchor policies/maintenance-axes policies/maintenance-checklist policies/software-development-approach; do + bash scripts/deed-field.sh --has "$DEED" "$c" || MISSING="$MISSING $DEED:($c)" + done + want policies/maintenance-axes/axis-3-audit compliance-tooling "panic-attack" + want policies/maintenance-axes/axis-3-audit effects-tooling "ecological checking with sustainabot guidance" + want policies/maintenance-checklist/metadata source-human "docs/governance/MAINTENANCE-CHECKLIST.adoc" + want policies/software-development-approach/metadata source-human "docs/governance/SOFTWARE-DEVELOPMENT-APPROACH.adoc" + else + MISSING="$MISSING repo-deed:(status …)-clause" fi if [ -n "$MISSING" ]; then echo "MISSING:$MISSING" @@ -55,17 +122,31 @@ validate-rsr: fi echo "RSR compliance: PASS" -# Validate STATE.a2ml syntax +# Validate the repo's lifecycle state: the deed's (status …) and +# (maturity …) clauses. validate-state: - @cd "{{justfile_directory()}}" && if [ -f ".machine_readable/descriptiles/STATE.a2ml" ]; then \ - grep -q '^\[metadata\]' .machine_readable/descriptiles/STATE.a2ml && \ - grep -q 'project\s*=' .machine_readable/descriptiles/STATE.a2ml && \ - echo "STATE.a2ml: valid" || echo "STATE.a2ml: INVALID (missing required sections)"; \ - else \ - echo "No .machine_readable/descriptiles/STATE.a2ml found"; \ + #!/usr/bin/env bash + cd "{{justfile_directory()}}" + DEED=$(bash scripts/deed-field.sh --find . || true) + if [ -n "$DEED" ] && bash scripts/deed-field.sh --has "$DEED" status; then + phase=$(bash scripts/deed-field.sh "$DEED" status phase || true) + level=$(bash scripts/deed-field.sh "$DEED" maturity level || true) + # The same closed sets validate-rsr enforces (CLADE-004/005): a + # misspelt phase or level is not a state. + case "$phase" in + reserved|incubating|active|dormant|merged|superseded|archived|extinct) ;; + *) echo "$DEED: INVALID (status :phase '$phase' is not one of reserved|incubating|active|dormant|merged|superseded|archived|extinct)"; exit 1 ;; + esac + case "$level" in + experimental|alpha|beta|production|lts) ;; + *) echo "$DEED: INVALID (maturity :level '$level' is not one of experimental|alpha|beta|production|lts)"; exit 1 ;; + esac + echo "$DEED: valid (phase $phase, maturity $level)" + else + echo "No repo state found: the repo deed has no (status …) clause" + exit 1 fi -# Validate AI installation guide completeness (finishbot pre-release check) validate-ai-install: #!/usr/bin/env bash cd "{{justfile_directory()}}" @@ -123,6 +204,22 @@ validate-ai-install: fi fi + # The rest of the provisioning set's AI and manual-setup documents. Their + # locations come from the provisioning engine, so this check and `just doctor` + # agree on where the warm-ups live (root, docs/onboarding/ or docs/). + if [ -f build/just/provision-lib.sh ]; then + for d in $(bash build/just/provision-lib.sh set-files | grep -E '(^|/)(SETUP|llm-warmup-[a-z]+)\.adoc$'); do + if [ ! -f "$d" ]; then + echo "MISSING: $d" + ERRORS=$((ERRORS + 1)) + fi + done + if ! grep -q '^\[\[ai-install\]\]' "$README" 2>/dev/null; then + echo "MISSING: [[ai-install]] anchor in $README (see build/just/provision.just ai-setup)" + ERRORS=$((ERRORS + 1)) + fi + fi + if [ "$ERRORS" -gt 0 ]; then echo "" echo "AI install guide: FAIL ($ERRORS issues)" diff --git a/build/manifest.scm b/build/manifest.scm new file mode 100644 index 0000000..9a1d2d4 --- /dev/null +++ b/build/manifest.scm @@ -0,0 +1,15 @@ +;; SPDX-License-Identifier: MPL-2.0 +;; SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +;; +;; manifest.scm — the Guix development shell for occupancy-types. +;; +;; guix shell -m manifest.scm # or: just dev-shell +;; guix time-machine -C channels.scm -- shell -m manifest.scm # the pinned Guix +;; +;; Minted by provision-set from the languages detected here (idris2, zig). +;; Tools Guix does not package: idris2 (via pack). They come from mise, which this +;; shell provides: guix shell -m manifest.scm -- mise install +;; Canon: hyperpolymath/standards 3-practice/provisioning/PROVISIONING-STANDARD.adoc +(specifications->manifest + (list "git" "bash" "coreutils" "nss-certs" "just" "mise" + "shellcheck" "chez-scheme" "gmp" "gcc-toolchain" "zig")) diff --git a/build/templates/CLADE.a2ml.in b/build/templates/CLADE.a2ml.in deleted file mode 100644 index 1cfcb2a..0000000 --- a/build/templates/CLADE.a2ml.in +++ /dev/null @@ -1,133 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Clade declaration — part of the gv-clade-index registry -# See: https://github.com/hyperpolymath/gv-clade-index -# -# Installed by `just repo-init` from build/templates/CLADE.a2ml.in. -# The identity below is DERIVED. The clade is NOT — a human must choose it. - -[identity] -# THE UUID IS DERIVED, NOT ALLOCATED, NOT INVENTED. The standards spec -# (standards: docs/decisions/ADR-008-uuid-v8-estate-standard.adoc, profile C; -# docs/decisions/ADR-008-profile-c-domains.json): -# -# domain = "gv-clade-index", name = "github.com//" -# uuid = first 16 bytes of SHA-256 over UTF-8(domain + ":" + name), -# without a trailing newline, with the version nibble set to 8 -# and the RFC 9562 variant bits set to binary 10. -# -# "The same repo always produces the same UUID without a lookup table." It is a -# fact anyone can recompute and check — never copy one from another repo, and -# never make one up. Recompute this one in Bash with sha256sum: -# -# name="github.com/{{OWNER}}/{{REPO}}" -# hash=$(printf '%s' "gv-clade-index:$name" | sha256sum) -# hex=${hash:0:32} -# variant=$(printf '%x' "$(( (16#${hex:16:1} & 3) | 8 ))") -# printf '%s-%s-8%s-%s%s-%s\n' "${hex:0:8}" "${hex:8:4}" \ -# "${hex:13:3}" "$variant" "${hex:17:3}" "${hex:20:12}" -# -# The owner segment is part of the derived name, so it is part of the IDENTITY: -# the same repo hosted under a different owner has a different uuid. Record the -# owner that is TRUE TODAY, and re-derive if it ever moves. -uuid = "{{UUID}}" -primary-forge = "github" -primary-owner = "{{OWNER}}" -canonical-name = "{{REPO}}" -prefixed-name = "UNASSIGNED-{{REPO}}" # becomes -{{REPO}} once the clade is chosen - -[clade] -# --------------------------------------------------------------------------- -# CHOOSE ONE. A CLOSED TAXONOMY OF 12 CATEGORIES — NOT AN ABBREVIATION SCHEME. -# -# The two letters abbreviate the NAME OF THE CLADE. They NEVER abbreviate the -# name of your repo. Pick the category your repo belongs to, then write down -# that category's code. Do NOT look at your repo's name and hunt for two letters -# that fit it. -# -# fv Formal Verification & Proofs provable correctness, dependent types, theorem proving -# nl Nextgen Languages compilers, runtimes, language tooling -# rm Repo Management & Tooling scaffolding, graph analysis, bots, templates -# gv Governance & Standards licensing, compliance, policy enforcement -# db Databases database engines, query languages, storage -# ap Applications end-user apps, web, desktop, services -# ix Infrastructure & Cloud containers, deployment, sysadmin -# dx Developer Ecosystem dev tools, package managers, editors, bindings -# pt Protocols & Interop internet standards, protocol implementations -# ax AI & Neurosymbolic ML, neural proof synthesis, AI governance -# gm Games & Interactive game engines, interactive experiences -# sc Security scanning, vulnerability management, access control -# -# This is spelled out because the opposite was done, more than once: -# -# paint-type chose `pt`, reading it as "PainT-type". -# `pt` is Protocols & Interop. An image editor is `ap`. -# gossamer chose `gv`, reading it as "Graphical/Visual". -# `gv` is GoVernance & Standards. There is no graphical clade. -# -# Both codes were VALID, so every check passed, and both repos sat filed under a -# category they have nothing to do with. -# -# HOW TO CHOOSE (spec): "The primary clade reflects the project's core value -# proposition, not its implementation details." Ask what the repo is FOR, not -# what it is built with. A game written in Idris2 is `gm`, not `fv`. A webview -# shell with formal ABI proofs is what developers build on, not a proof project. -# -# Fill in BOTH `primary` and `primary-name`, and make them agree — CLADE-006 -# rejects a mismatched pair. The redundancy is deliberate: a code alone cannot -# express a wrong belief about what it means, so it cannot be checked for one. -# The pair can. -# -# Until you fill these in, CLADE-003 and CLADE-006 FAIL. That is deliberate: an -# unchosen clade must never be mistakable for a chosen one. Leaving a valid- -# looking default here is exactly how every repo built from this template ended -# up silently claiming "rm". -# --------------------------------------------------------------------------- -primary = "UNASSIGNED" -primary-name = "UNASSIGNED" -secondary = [] # [] unless there is a genuine SECOND core value proposition -assigned = "{{CURRENT_DATE}}" -rationale = "" # why THIS category — in value-proposition terms - -[forges] -github = "{{OWNER}}/{{REPO}}" -gitlab = "" # populated if/when mirrored -bitbucket = "" # populated if/when mirrored - -[lineage] -# type: standalone | monorepo | monorepo-child | inflated | deflated | hub | satellite -# hub — this repo COORDINATES an ecosystem family (its members carry -# type="satellite"). WHICH repos it coordinates is not recorded -# here: that lives in ECOSYSTEM.a2ml [pipeline] coordination. -# satellite — this repo is COORDINATED BY a hub. Its `parent` STAYS "": -# parent is a monorepo parent — never a description, and never -# the name of the coordinating hub. -# "hub"/"satellite" were added after the first hub ecosystem had to mint eight -# repos with the nearest-wrong value "standalone" (standards#726): lineage is -# the pointer, ECOSYSTEM.a2ml is the detail. Do not encode membership twice. -type = "standalone" -parent = "" # a monorepo PARENT — never a description of this repo -born = "{{CURRENT_DATE}}" -previous-names = [] -instantiated-from = "rsr-template-repo" - -[status] -# One of: reserved incubating active dormant | merged superseded archived extinct -phase = "incubating" # a repo created today has not proven anything yet -since = "{{CURRENT_DATE}}" -present = true -aliases = [] -merged-into = "" -superseded-by = "" -successors = [] -ended = "" - -[[status.history]] -phase = "incubating" -since = "{{CURRENT_DATE}}" -note = "created from rsr-template-repo; clade not yet chosen; not yet registered" - -# --------------------------------------------------------------------------- -# REGISTRATION IS NOT DERIVATION. Deriving the uuid above does not register this -# repo. Registration means an entry in gv-clade-index verisim/seed/repos.a2ml, -# it is outward-facing, and it is the OWNER's act — an agent must not do it. -# --------------------------------------------------------------------------- diff --git a/ci/.pre-commit-config.yaml b/ci/.pre-commit-config.yaml index 8069164..74eb6d8 100644 --- a/ci/.pre-commit-config.yaml +++ b/ci/.pre-commit-config.yaml @@ -23,25 +23,30 @@ repos: # upstream in the A2ML retirement (standards #836 owner ruling; R-H3), # leaving this pin as broken plumbing — pre-commit could not clone it. # Replaced with a local hook calling .github/hooks/validate-deed.sh, - # verified exit-0 against all 123 manifests in this repo. Dual-accept: - # scans both .a2ml (legacy extension; migration = standards #837, the - # DEED conversion campaign) and .deed. When the grammar-faithful .deed + # It validates every .deed file and rejects any .a2ml file (the format is + # retired; its facts live in the repo deed). When the grammar-faithful .deed # validator is wired per owner ruling R-H2, this hook is its natural home. - repo: local hooks: - id: validate-deed - name: Validate DEED manifests (dual-accept .a2ml/.deed) + name: Validate DEED manifests (and refuse retired .a2ml) entry: .github/hooks/validate-deed.sh language: system - files: '\.(a2ml|deed)$' + files: '\.(a2ml|A2ML|deed)$' pass_filenames: false # --- K9 contract validation --- - - repo: https://github.com/hyperpolymath/k9-pre-commit - rev: 9b82e1f7a6b6c0f99df72c145d7dee8851803c30 # k9-pre-commit @ main 2026-06-23 + # The canonical validator, through the same wrapper CI runs (needs a + # standards checkout beside this repo, or STANDARDS_DIR; skips with a + # notice otherwise). + - repo: local hooks: - id: validate-k9 name: Validate K9 contracts + entry: bash .github/hooks/validate-k9.sh + language: system + pass_filenames: false + files: '\.k9(\.(ncl|lock))?$' # --- Shell linting --- - repo: https://github.com/shellcheck-py/shellcheck-py @@ -62,12 +67,12 @@ repos: hooks: - id: gitleaks - # --- Arrival pack drift: CLAUDE.md must stay in sync with a2ml --- + # --- Arrival pack drift: CLAUDE.md must stay in sync with the repo deed --- - repo: local hooks: - id: validate-claude-md - name: CLAUDE.md arrival pack in sync with a2ml + name: CLAUDE.md arrival pack in sync with the repo deed entry: bash .machine_readable/arrival-pack/verify.sh language: system pass_filenames: false - files: '^(\.machine_readable/descriptiles/.*|CLAUDE\.md)$' + files: '^([^/]+_chora\.deed|\.machine_readable/arrival-pack/.*|CLAUDE\.md)$' 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/AFFIRMATION.adoc b/docs/AFFIRMATION.adoc index 90d321c..80c33f1 100644 --- a/docs/AFFIRMATION.adoc +++ b/docs/AFFIRMATION.adoc @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -= AFFIRMATION — {{PROJECT_NAME}}, as of += AFFIRMATION — occupancy-types, as of :toc: macro :toclevels: 2 :std-docs: https://github.com/hyperpolymath/standards/blob/main/docs @@ -37,12 +37,12 @@ toc::[] == What this is, and how it works -*What it is.* A short, dated, signed snapshot of what {{OWNER}} can honestly and -verifiably claim about *{{PROJECT_NAME}}* at one exact commit. Nothing here is +*What it is.* A short, dated, signed snapshot of what hyperpolymath can honestly and +verifiably claim about *occupancy-types* at one exact commit. Nothing here is marketing and nothing is a promise about the future — those live in the README. This file is the receipt. -*What the project is.* {{PROJECT_DESCRIPTION}} +*What the project is.* Early-stage research on compositional worst-case resource bounds, with a linear session-IR checker, occupancy-indexed state experiments in Idris 2, and a prototype stack-depth certificate checker. The long-term goal is predictable memory and network-buffer usage. *How it stays trustworthy.* Three moving parts: @@ -63,7 +63,7 @@ correctness. Treat it as a falsifiable claim, not gospel. == The epistemic contract (read this before you trust _or_ attack) -This document records {{OWNER}}'s *best belief* at the timestamp below. It is +This document records hyperpolymath's *best belief* at the timestamp below. It is *not a guarantee of correctness.* The only guarantee is *no intentional overclaim*: where something is proven we say "proven"; where it is a documented trust boundary, an experiment, or an unwired module, we say so; where a claim is @@ -95,19 +95,19 @@ file. Bring a counter-example, a failing run, or a contradicting source. | Field | Value | Project -| {{PROJECT_NAME}} +| occupancy-types | Repo -| `{{OWNER}}/{{REPO}}` +| `hyperpolymath/occupancy-types` | Branch -| `{{MAIN_BRANCH}}` +| `main` | Commit (HEAD) | `` | Permalink -| https://{{FORGE}}/{{OWNER}}/{{REPO}}/tree/ +| https://github.com/hyperpolymath/occupancy-types/tree/ | Verified (UTC) | `` @@ -120,7 +120,7 @@ file. Bring a counter-example, a failing run, or a contradicting source. | `` | Affirmed by -| {{AUTHOR}} <{{AUTHOR_EMAIL}}> +| Jonathan D.A. Jewell |=== [IMPORTANT] @@ -193,15 +193,15 @@ to prevent — mark it refuted and say what refuted it. [source,bash] ---- -git clone https://{{FORGE}}/{{OWNER}}/{{REPO}} -cd {{REPO}} +git clone https://github.com/hyperpolymath/occupancy-types +cd occupancy-types git checkout # ---- == One-line characterisation (quote this) -> `` +> `` == Joint attestation @@ -213,7 +213,7 @@ hidden. * *Engineering party (AI):* `` — ran the checks recorded here at `` and stands behind the wording above as a faithful report of those runs. -* *Owner / maintainer:* {{AUTHOR}} <{{AUTHOR_EMAIL}}> — _signs by committing +* *Owner / maintainer:* Jonathan D.A. Jewell — _signs by committing this file with `-S`; the git commit signature over this content, at the commit SHA recorded above, is the cryptographic form of this affirmation._ diff --git a/docs/AI-INSTALL-README-SECTION.adoc b/docs/AI-INSTALL-README-SECTION.adoc index 19ed463..8041c07 100644 --- a/docs/AI-INSTALL-README-SECTION.adoc +++ b/docs/AI-INSTALL-README-SECTION.adoc @@ -2,19 +2,20 @@ // Copyright (c) 2026 Jonathan D.A. Jewell = AI-Install README Section (canonical snippet) -The block below is the canonical `== AI-Assisted Installation` section. Paste it -into a repository's `README.adoc` verbatim; `just validate-ai-install` greps the -README for the heading text, so keep the heading exactly as written. +The canonical README section is now part of the estate provisioning set: +`hyperpolymath/standards` → +`3-practice/provisioning/templates/README-ai-install.adoc.tmpl`. +It starts with the `\[[ai-install]]` anchor and the heading +`== AI-Assisted Installation (Recommended)`, then a *Just say it* block whose first +listing line is the one sentence to give any AI. `just ai-setup` and +`./launcher.sh --ai-setup` print that same line, read from the README, so edit it +there and nowhere else. ----- -== AI-Assisted Installation +`just validate-ai-install` checks for the heading text, the `\[[ai-install]]` +anchor, `docs/AI_INSTALLATION_GUIDE.adoc`, `docs/SETUP.adoc` and the three +`llm-warmup-{user,dev,maintainer}.adoc` warm-ups. -If you are an AI agent installing this project, read -link:docs/AI_INSTALLATION_GUIDE.adoc[the AI installation guide] first — it gives -the orientation order, the full prompt sequence, and the privacy notice. - -The one trap worth stating up front: **do not set `RSR_NON_INTERACTIVE=1`**. It -stubs the shell builtin `read`, which also disables the loops that perform token -substitution — the run never terminates and substitutes nothing. Pipe answers to -stdin instead. ----- +This repository's own section (see `README.adoc`) is specialised for a template: +its sentence asks the assistant to create a new repository, and it keeps the +note on minting without a terminal (`RSR_NON_INTERACTIVE=1` with the answers in +the environment). diff --git a/docs/AI_INSTALLATION_GUIDE.adoc b/docs/AI_INSTALLATION_GUIDE.adoc index ceb61fe..f8b891c 100644 --- a/docs/AI_INSTALLATION_GUIDE.adoc +++ b/docs/AI_INSTALLATION_GUIDE.adoc @@ -18,7 +18,7 @@ Read, in this order, before running anything: . `*_chora.deed` (the repo deed) — the universal AI entry point at the repository root. . `CLAUDE.md` — the generated arrival pack (estate doctrine + repo identity). -. `.machine_readable/descriptiles/STATE.a2ml` — current phase and completion. +. `docs/status/ROADMAP.adoc` — milestones, blockers and next actions (phase and maturity are in the deed's `(status)` and `(maturity)` clauses; `just state-phase` prints them). Do not infer the repository's purpose from its name. If a fact is not written down, record it as `UNASSIGNED` rather than inventing it. @@ -30,7 +30,7 @@ local checkouts across this estate are routinely stale or carry sweep debris. [source,bash] ---- -git clone https://github.com/hyperpolymath/rsr-template-repo.git my-project +git clone https://github.com/hyperpolymath/occupancy-types.git my-project cd my-project rm -rf .git && git init ---- @@ -43,68 +43,87 @@ with no git ancestry; `just repo-init` recovers the template tip via [source,bash] ---- -just repo-init # or: just repo-init +mise trust # allow this repository's mise.toml (no prompt without a TTY) +mise install # the toolchain, including the yq repo-init needs +mise exec -- just repo-init # or: mise exec -- just repo-init ---- +When the mint keeps `build/container/`, `just repo-init` reformats its KYAML +configs after filling them, which needs a yq that writes KYAML (mise.toml +provides it). Without one the mint stops before writing anything. + `just repo-init` renders every `{{TOKEN}}` in the tree, derives the repository identity, writes the provenance files, and removes the template-only directories. -[WARNING] +[NOTE] ==== -**Do not set `RSR_NON_INTERACTIVE=1`.** It stubs the shell builtin `read` -globally, which also disables the two `while read -r file` loops that drive -substitution. The result is an infinite loop that performs zero substitution — -it does not fail, it never terminates. +**Without a terminal, set `RSR_NON_INTERACTIVE=1` and answer through the +environment.** Each prompt reads the variable named in the table below; the +recipe keeps that variable's value from the environment, or leaves it empty, and +never touches stdin. `PROJECT_NAME`, `REPO`, `OWNER`, `AUTHOR` and `AUTHOR_EMAIL` +are required; an unset optional answer takes its default. + +[source,bash] +---- +RSR_NON_INTERACTIVE=1 PROJECT_NAME="My Project" REPO=my-project \ + OWNER=my-org AUTHOR="My Name" AUTHOR_EMAIL=me@example.org \ + mise exec -- just repo-init +---- -Drive it non-interactively by **piping answers to stdin** instead. Real `read` -stays intact, so the data loops work. +Until 2026-10-14 this mode stubbed the `read` builtin for the whole recipe, so +the substitution loops never terminated, and this guide told agents to pipe +answers to stdin instead. Piping still works, in the order below. ==== The prompts, in order, are: -[cols="1,3",options="header"] +[cols="1,3,2",options="header"] |=== -| # | Prompt - -| 1 | Project name -| 2 | Repository slug — validated against `^[a-z0-9]+(-[a-z0-9]+)*$`; no capitals, spaces or underscores (it must be a valid Guix package name) -| 3 | Owner -| 4 | Author full name -| 5 | Author email -| 6 | Author organisation -| 7 | Previous/alternate email -| 8 | Project description -| 9 | Forge domain -| 10 | Security email -| 11 | Conduct email -| 12 | Project type -| 13 | Website URL (blank = default) -| 14 | OpenSSF Best Practices ID (blank = none) -| 15 | Service name (blank = default) -| 16 | Primary port (blank = 8080) -| 17 | Container registry (blank = default) -| 18 | Proceed? — answer `y` +| # | Prompt | Environment variable + +| 1 | Project name | `PROJECT_NAME` +| 2 | Repository slug — validated against `^[a-z0-9]+(-[a-z0-9]+)*$`; no capitals, spaces or underscores (it must be a valid Guix package name) | `REPO` +| 3 | Owner | `OWNER` +| 4 | Author full name | `AUTHOR` +| 5 | Author email | `AUTHOR_EMAIL` +| 6 | Author organisation | `AUTHOR_ORG` +| 7 | Previous/alternate email | `AUTHOR_EMAIL_ALT` +| 8 | Author ORCID iD (blank = none; checked against its check digit) | `AUTHOR_ORCID` +| 9 | Project description | `PROJECT_DESCRIPTION` +| 10 | Forge domain | `FORGE` +| 11 | Security email | `SECURITY_EMAIL` +| 12 | Conduct email | `CONDUCT_EMAIL` +| 13 | Project type | `PROJECT_TYPE` +| 14 | Website URL (blank = default) | `WEBSITE` +| 15 | OpenSSF Best Practices ID (blank = none) | `OPENSSF_BP_ID` +| 16 | Service name (blank = default) | `SERVICE_NAME` +| 17 | Primary port (blank = 8080) | `PORT` +| 18 | Container registry (blank = default) | `REGISTRY` +| 19 | Proceed? — answer `y` | `CONFIRM` |=== [IMPORTANT] ==== -Prompts 15–17 are gated on the presence of the container directory, **not** on -the project type. The template always ships one, so they always fire. Supplying -only 15 answers makes `read` hit EOF, which returns 1 and kills the recipe under -`set -e`. +When piping, prompts 16–18 are gated on the presence of the container +directory, **not** on the project type. The template always ships one, so they +always fire. Supplying fewer answers than prompts makes `read` hit EOF, which +returns 1 and kills the recipe under `set -e`. ==== === 4. Verify the result [source,bash] ---- -just --list # every recipe resolves (imports are silent on failure) -just check-root-shape # root matches the allowlist +mise exec -- just --list # every recipe resolves (imports are silent on failure) +mise exec -- just check-root-shape # root matches the allowlist bash scripts/check-no-placeholders.sh . -just verify +mise exec -- just verify ---- +An agent's shell is rarely mise-activated, and each command may run in a fresh +shell, so every `just` call here goes through `mise exec --`. + A successful instantiation leaves **no** `{{TOKEN}}` anywhere in the tree, and removes the template-only directories from the minted repository. If `scripts/validate-template.sh` or the end-to-end instantiation test are still diff --git a/docs/HYPATIA-RULE-ORDER.adoc b/docs/HYPATIA-RULE-ORDER.adoc new file mode 100644 index 0000000..8e57efe --- /dev/null +++ b/docs/HYPATIA-RULE-ORDER.adoc @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += Hypatia rule order, exemption layers and clash resolution +:toc: left +:toclevels: 3 +:sectnums: + +This is the repository's statement of *which* rule layer wins when two of them +speak about the same file, *which* clashes exist today, and *what resolves each +one*. It is local policy about tools that live elsewhere: Hypatia at +`hyperpolymath/hypatia@be98f74ce93df200752f3798efa1305bfa3e0e10` +(`lib/hypatia/scanner_suppression.ex`, `lib/rules/`), and the verdict grid at +`metadatastician/standards@e064539116c2dcfdf282a4749dbd58c69961c7e6` +(`docs/VERDICT-GRID-SPEC.adoc`). + +Nothing here changes what a scanner finds. It states the order the layers are +applied in, so that "the finding is exempt" and "the finding was never looked +for" cannot be confused for each other. + +== Why this document exists + +Three facts, each verified against the pinned sources, made the ordering +ambiguous: + +. **Hypatia's exemption layers are a flat OR.** `ScannerSuppression.suppressed?/4` + returns true when *any* of the universal excludes, the built-in + `@default_exemptions`, or the repository's `.hypatia-ignore` matches. There is + no "narrow wins" rule and no negative entry, so a broader layer silently + decides for a narrower one. +. **The verdict grid grades a *set* of reporters, not a pipeline.** Its + precedence (purple > red > orange > yellow > blue > green) settles what a + *row* becomes, but not which scanner is allowed to contribute a row. +. **Every workflow claimed a relationship it did not have.** `fork-governance.yml` + said the report-backs fed "the Hypatia lane", `.gitbot-fleet/report-backs/README.md` + named a lane that did not exist, and `.github/workflows/README.adoc` said + Hypatia ran "in two places (on purpose)". All three were wrong, and nothing + could fail when they went wrong. + +== The layers, in the order they are applied + +[cols="1,3,3",options="header"] +|=== +|Order |Layer |Owner and enforcement + +|1 +|**Rule-level total ban** — `cicd_rules/banned_language_file` (Python, Go, Makefile, Gemfile) is refused by a dedicated clause that no exemption can reach. +|Hypatia (`scanner_suppression.ex`). Not this repository's to change. A repository cannot exempt it, and must not try. + +|2 +|**Universal excludes** — `.git/`, `node_modules/`, `_build/`, `deps/`, `target/`, `dist/`, `.hypatia-baseline.json`. +|Hypatia. Applied to every rule. + +|3 +|**Built-in default exemptions** — `@training_corpus_paths` (`lib/rules/`, `test/`, `tests/`, `scripts/fix-scripts/`, `.audittraining/`, `integration/fixtures/`) keyed per module, plus `benches/` for `code_safety` only. +|Hypatia. A repository cannot narrow these (see clash C7). + +|4 +|**Repository exemptions** — `.hypatia-ignore`, `/:`. +|This repository. Checked by `scripts/check-hypatia-ignore.sh`. + +|5 +|**Inline directives** — `# hypatia: allow / -- `, scoped to the line or file that carries it. +|This repository. Narrower than every layer above; cannot widen anything. + +|6 +|**Baseline** — `.hypatia-baseline.json`, applied at gate time only. +|The gate, never the grid. A baselined finding is still counted by the verdict grid (spec §Non-goals), so standing debt cannot masquerade as a clean scan. + +|7 +|**Grid normalisation** — severity words, `suppressed: true`, and the row's colour. +|The verdict grid. Presentation and grading only; it never suppresses a finding, and nothing it does can change what a scanner reported. +|=== + +The first three layers belong to Hypatia; only layers 4 and 5 are this +repository's to get wrong, which is why the checks in this repository target +those two and *state* the rest rather than restating them as if they were ours. + +=== What "first match wins" does not mean + +Layers 2–5 are **not** a cascade and not a precedence chain: Hypatia ORs them. +"Order" here is the order in which a reader (and the checker) must interpret a +suppression, and the order in which a *new* reviewer should look for the reason +a finding is missing. Where two layers genuinely conflict, the conflict is one +of the clashes below, and each has an explicit rule. + +== Clashes, with the rule that resolves each + +=== C1 — one scanner, two workflows + +*What was happening.* Hypatia ran in `hypatia-scan.yml` **and** in a +`hypatia-scan` job of `static-analysis-gate.yml`: two builds, two caches, two +"fail on critical" gates, two different output shapes, and no artifact either +of them could hand to the grid. + +*Rule.* A scanner runs in exactly one workflow, and that workflow owns its gate. +A second workflow may *read* its artifact; it may not re-run the tool. + +*Enforced by.* The absence of a Hypatia job in `static-analysis-gate.yml`, and +`scripts/check-workflow-edges.sh`, which fails the CICD Squabbler gate when a +producer's `name:`, artifact or pin stops matching what the reporter expects. + +=== C2 — one tool, two pins + +*What was happening.* `static-analysis-gate.yml` built panic-attack at +`b5530834…`; the standards Hypatia reusable pins `b689a74d…` (it carries an +upstream severity fix, standards#1306). Both were defensible; together they +meant the grid could grade two different binaries and call the result comparable. + +*Rule.* One pin per tool per repository. The verdict job names the same commit +the producing workflow builds (`--hypatia-ref`, `--panic-ref`, `--pons-ref`), and +a pin that appears at two different commits in two workflows is a failure, not a +choice. The standards reusable's different panic-attack pin is *recorded here*, +not silently copied: a repository that adopts the reusable adopts its pins, and +must then change `scripts/check-workflow-edges.sh`'s expectation deliberately. + +*Enforced by.* `scripts/check-workflow-edges.sh` (identical-ref check plus the +duplicate-pin scan). + +=== C3 — panicbot versus the direct panic-attack row + +*What was happening.* `.gitbot-fleet/enrolment.json` kept `panicbot` unenrolled +with the reason "leave it to the Hypatia lane's direct panic-attack row" — and +no such row existed, in any workflow. + +*Rule.* A scanner gets one row. panic-attack is graded by the grid's own +`panic-attack assail` row from `static-analysis-gate.yml` evidence, so +enrolling `panicbot` would report one scanner twice and produce two rows that can +disagree. `panicbot` stays unenrolled, and its enrolment reason now says *that*. +The grid now does render the direct row, so the cross-reference is no longer a +promise about the future. + +*Enforced by.* The enrolment manifest's own wording (checked for shape by +`scripts/verdict-grid-lane.sh validate --enrolment`) and the grid spec's +one-row-per-reporter rule. + +=== C4 — substring fragments suppress more than they name + +*What was happening.* `.hypatia-ignore` fragments are **substring** matches. An +entry for `x.yml` also silences `x.yml.bak`; `build/docs-seed/` also silences +`build/docs-seed-old/`. The file's stated intent ("whole-file/directory +exemptions") was narrower than its effect. + +*Rule.* Every fragment must end on a path boundary: either `/` (a directory) or a +last segment containing a dot (a file name). A raw substring is refused. + +*Enforced by.* `scripts/check-hypatia-ignore.sh`. + +=== C5 — a broad entry makes narrower ones dead + +*What was happening.* `workflow_audit/wrong_sha_pin:.github/workflows/` exempts +the rule for every workflow, so any narrower `wrong_sha_pin` entry — added in +good faith by a later author — would be dead code that reads as a considered +decision. + +*Rule.* An entry whose (module, type, path) is already covered by a broader entry +for the same module (same type or `*`, and a path fragment the narrower one +contains) is refused. If the broad exemption is right, the narrow entry goes; if +the narrow exemption is right, the broad one goes. + +*Enforced by.* `scripts/check-hypatia-ignore.sh`. + +=== C6 — an exemption outlives the file it names + +*What was happening.* Nothing tied an entry to the path it named. After a rename +the entry kept suppressing a path that no longer existed — or, if the rename +produced the same prefix, a different file. + +*Rule.* The path a fragment names must exist in the tree. A stale entry is +refused, and its removal is a visible diff rather than silent rot. + +*Enforced by.* `scripts/check-hypatia-ignore.sh`. + +=== C7 — a repository cannot narrow a built-in default + +*What is happening.* Layers 3 and 4 are OR'd: the built-in +`@training_corpus_paths` exempts `tests/` from `security_errors`, and no +`.hypatia-ignore` entry can *un*-exempt it. This repository cannot express "scan +`tests/` for secrets" locally. + +*Rule.* Do not duplicate, and do not pretend. The repository must not add an +entry that restates a default (it would read as local policy when it is not), and +must not claim a narrowing it cannot make. Where a narrower exemption is +genuinely needed, use an inline directive (layer 5), which is line-scoped. +Where an *un-suppression* is needed, that is a change in Hypatia — an upstream +issue, not a local workaround. + +*Enforced by.* Review; recorded here because the checker cannot see the +other repository's defaults. + +=== C8 — the reporter must not re-gate + +*What was happening.* `fork-governance.yml` read the fleet job's result, printed +it in a table, and then failed on DEED/K9 only — so a fleet failure was +invisible in the one place a reader looked. Its `jq '… | .findings | length'` +loop could also abort the whole summary on a malformed envelope, skipping the +final check. + +*Rule.* The reporter never gates on findings: each producer owns its own gate, +and one finding must not be able to block a pull request twice. A failed +producer is visible as its own red check and as orange rows in the grid — those +two facts are the reporting, and the reporter does not repeat them as a third. +The lane writes its summary without parsing envelope fields it does not own. + +*Enforced by.* `fork-governance.yml`'s verdict job (which fails only on +DEED/K9), and the grid's own grading (`orange` for missing evidence). + +== Evidence and reason (e/r) across the pipeline + +A row is only gradeable when the evidence says *two* things: how bad the worst +finding is (the grade) and how that was established (the reason). The rule +adopted here, per scanner: + +[cols="1,2,2",options="header"] +|=== +|Reporter |Grade comes from |Reason/evidence carried + +|Hypatia +|`severity` on the relativised findings array +|`reason` (or `message`/`type`) per finding, `rule_module` and `type` for identity + +|panic-attack +|`report.weak_points[].severity`, `suppressed: true` excluded +|`description` per weak point; the envelope's `status`/`reason` say whether the scan ran at all + +|pons asinorum +|`report.findings[].severity` (`ERROR`/`WARN`/`INFO`) +|`evidence` class and `tier` per finding, carried into the message so a heuristic is never read as a proof + +|gitbot-fleet member +|`findings[].severity` in the report-back envelope +|`rule_id` and `message` per finding; `status`/`reason` at the envelope +|=== + +Three consequences, all enforced at the boundary rather than trusted: + +* **A status envelope is mandatory.** A bare findings array cannot distinguish + "ran and found nothing" from "never ran", so `panic-attack-findings.json` and + `pons-findings.json` are envelopes (`status`, `tool`, `ref`, `reason`, + `exit_code`, `report`). `scripts/verdict-grid-lane.sh validate` refuses a bare + array by name. +* **An unknown severity word is reported, not silently graded.** The grid makes + the row orange; the lane's `validate` step names the offending words first, so + the reason reaches the log even when the grid cannot render. +* **A finding without a reason is still a finding.** The severity decides the + colour; the missing reason is a warning attached to the evidence file, never a + reason to drop the finding. + +== GitBot design and operation + +The templates' rules for the fleet, each matching the grid spec: + +. **One row per reporter.** A member that is a wrapper around a scanner already + reporting (C3) stays unenrolled. +. **Enrolment is a claim about *this* repository**, with a reason for every + member that does not apply. `enrolled: false` without a reason is not + expressible in the manifest, and `scripts/verdict-grid-lane.sh validate` + warns when one appears. +. **No report-back is orange, never green.** An enrolled member whose envelope + is absent renders "enrolled, but no report-back was received". A committed + placeholder envelope would be stale evidence about an older commit, so + `.gitbot-fleet/report-backs/` stays empty in git and is filled only at render + time from the `fleet-report-backs` artifact. +. **A failed fleet job is not a failed verdict** (C8): its rows go orange and its + own check goes red. +. **Severity words normalise; unknown words make the row orange.** The member's + own vocabulary (`error`, `warning`, `info`, `suggestion`) is accepted. + +== What this document does *not* enforce + +Honesty about the limits, because each of these has been assumed "checked" +somewhere before: + +* the built-in default exemptions (layer 3) — they live in Hypatia, and C7 is + stated, not enforced; +* that a stated reason is *true*, only that one is present; +* that a fleet adapter writes the envelope shape the spec defines — the fleet is + a separate repository at a pinned commit; +* that the renderer's recorded sha256 stays current. A stale digest turns every + render orange with the mismatch named, which is loud on purpose: it is the + one failure mode where a wrong *renderer* would otherwise look like a clean + repository. + +== The order is a contract, not only a description + +The table above is prose, and prose does not fail a build. The order, the +ownership of each concern, the clash resolutions and the pins are therefore +restated in `.machine_readable/policies/hypatia-rule-order.json` and checked by +`scripts/check-hypatia-rule-order.sh` (lane: *Hypatia rule order & clash +resolution*, in `scripts/run-proven-suite.sh`). + +The checker refuses: + +[cols="1,3"] +|=== +|Refused |Because + +|two layers with one `rank` +|a duplicate rank leaves which of the two wins to the reader + +|a `rank` sequence with a hole, or written out of order +|the document *is* the sequence; a gap invites a layer to be inserted where it +cannot be seen + +|a layer with no `overrides` +|a layer that states nothing is an OR, not a cascade, and that has to be a +decision rather than an omission + +|a layer id used twice, a `concern` declared twice, a `clash` declared twice +|a duplicated row reads as two rules and is one + +|a `concern` with no owner, or owned by a path that does not exist +|this is exactly how Hypatia came to be run by two workflows (C1) + +|an `owner` outside `hypatia \| this-repository \| the-gate \| the-grid` +|the four parties are the whole vocabulary; a fifth name means the model has +changed and this document has not + +|a `clash` with no `resolution` +|C3 and C5 were both "somebody will notice"; nobody did + +|a module exempted in `.hypatia-ignore` that `observed_modules` does not +classify, or a classification for a module the ignore file no longer mentions +|the policy and the file it describes drift in both directions, and both are +silent + +|a module classified under a layer whose `repository_can_change` is `false` +|an exemption cannot live in a layer this repository may not edit (C7) + +|a `scanner.commit`, `grid.commit`, `renderer_sha256` or `library_sha256` that +disagrees with `hypatia-scan.yml` or `scripts/verdict-grid-lane.sh` +|a policy pin that agrees only with itself proves nothing + +|a workflow with no pin at all +|absent evidence is a failure, not a pass +|=== + +The suite that proves the checker fires is +`tests/workflows/hypatia_rule_order_test.sh`: 44 checks, one mutation per +refusal above, run against a copy of the real policy. Two harness defects it +found in its own first draft are worth recording, because both are the kind of +thing that makes a green test meaningless: + +* **`@tsv` cannot carry an empty field.** Tab is IFS whitespace, so an empty +`overrides` collapsed and the value that followed it shifted into its place — +the checker reported "no scope" for a missing `overrides` and would have passed +a layer that stated neither. The checker now reads one base64-encoded JSON +object per line and decodes each field by name. +* **`grep` in a `set -e` script exits before it can complain.** A workflow with +no `HYPATIA_SHA` made the extraction fail rather than the *check*, so a missing +pin exited 1 with no diagnostic — indistinguishable from the violations it was +supposed to report. The extractions are now `sed -nE ...p`, which succeeds on no +match and lets the failure be stated. + +=== Adding or changing a rule + +. Decide which layer it belongs to, by asking *who* may waive it: the scanner +(layers 1–3), this repository (4–6), or the grid (7). If two answers are +defensible, the rule is a clash and needs a `C` entry before it needs code. +. Add the layer entry to the policy with its `rank`, `overrides` and `owner`; +re-rank the ones below it so the sequence stays complete. +. If it replaces a mechanism, name the old one `duplicate_of` and make it +advisory — two advisory voices are noise, two blocking voices are C1. +. Run `bash scripts/check-hypatia-rule-order.sh .` and then the suite. A policy +change that no test exercises is a comment. diff --git a/docs/MAINTAINERS.adoc b/docs/MAINTAINERS.adoc index 458fc55..3f1fad4 100644 --- a/docs/MAINTAINERS.adoc +++ b/docs/MAINTAINERS.adoc @@ -12,9 +12,9 @@ earlier duplicates at `.github/MAINTAINERS` and `docs/attribution/MAINTAINERS.ad |=== | Name | Role | Contact -| {{AUTHOR}} +| Jonathan D.A. Jewell | Lead Maintainer -| https://github.com/{{OWNER}}[@{{OWNER}}] +| https://github.com/hyperpolymath[@hyperpolymath] |=== == Responsibilities diff --git a/docs/QUICKSTART.adoc b/docs/QUICKSTART.adoc index fc9fdf9..8e29a10 100644 --- a/docs/QUICKSTART.adoc +++ b/docs/QUICKSTART.adoc @@ -15,7 +15,7 @@ Get up and running in 60 seconds. [source,bash] ---- -git clone https://github.com/{{OWNER}}/rsr-template-repo my-project +git clone https://github.com/hyperpolymath/occupancy-types my-project cd my-project rm -rf .git && git init -b main just repo-init # interactive placeholder replacement diff --git a/docs/RSR-PHILOSOPHY.adoc b/docs/RSR-PHILOSOPHY.adoc index 05a1590..5f29561 100644 --- a/docs/RSR-PHILOSOPHY.adoc +++ b/docs/RSR-PHILOSOPHY.adoc @@ -51,7 +51,7 @@ Two obligations follow from this, and they are not optional: * *Find the source.* Before acting, trace the thing back to where it is actually defined — the template, the generator, the canon, the single point that everything else inherits from. The estate's structure is - `standards → rsr-template-repo → (every repo)`; a fix that belongs at the + `standards → occupancy-types → (every repo)`; a fix that belongs at the template does not belong in 380 leaves. * *Be mindful of every up- and down-stream.* A change at a source propagates. Before you make it, know what feeds into the thing you are changing (upstream) diff --git a/docs/RSR_OUTLINE.adoc b/docs/RSR_OUTLINE.adoc index 9bbe401..0341e18 100644 --- a/docs/RSR_OUTLINE.adoc +++ b/docs/RSR_OUTLINE.adoc @@ -32,7 +32,7 @@ This repository provides the standardized structure, configuration, and tooling * Bootstrap new projects with RSR compliance * Reference the standard directory structure -* Copy configuration templates (Justfile, STATE.a2ml, etc.) +* Copy configuration templates (Justfile, repo deed, `docs/status/ROADMAP.adoc`, etc.) == Quick Start @@ -78,7 +78,7 @@ just validate-rsr |Site-operations bundle; RFC-compliant metadata at `www/.well-known/` (security.txt, ai.txt, humans.txt) |`.machine_readable/` -|All machine-readable content: state files (6 a2ml), `bot_directives/`, `contractiles/` +|All machine-readable content: `ai/`, `bot_directives/`, `contractiles/` (state and descriptive facts are clauses of the root repo deed, `*_chora.deed`) |`docs/` |Documentation directory @@ -113,8 +113,8 @@ just validate-rsr |`*_chora.deed` (repo deed) |Universal AI agent entry point -|`AI.a2ml` -|Claude-specific instructions +|`.machine_readable/ai/README.adoc` +|AI agent instructions (including the Claude-specific ones) |`src/interface/Abi/` |Idris2 ABI definitions (Types, Layout, Foreign) @@ -134,7 +134,7 @@ The template Justfile provides: * **Cookbook generation**: `just cookbook` -> `docs/just-cookbook.adoc` * **Man page generation**: `just man` -> `docs/man/project.1` * **RSR validation**: `just validate-rsr` -* **STATE.a2ml management**: `just state-touch`, `just state-phase` +* **Project state**: `just state-phase` (prints the deed's phase and maturity) * **Container support**: `just container-build`, `just container-push` * **CI matrix**: `just ci-matrix [stage] [depth]` @@ -183,7 +183,8 @@ hand-written tree that used to sit here it cannot go stale. * `Justfile` * `README.adoc` * `LICENSE` (MPL-2.0) -* `.machine_readable/descriptiles/STATE.a2ml` +* `*_chora.deed` (repo deed) +* `docs/status/ROADMAP.adoc` * `www/.well-known/security.txt` * `www/.well-known/ai.txt` * `www/.well-known/humans.txt` @@ -198,37 +199,23 @@ hand-written tree that used to sit here it cannot go stale. * npm, Bun, pnpm, yarn (use ) * Go (use Rust) -== STATE.a2ml +== Project State -The STATE.a2ml file tracks project state: +Project state is split between the repo deed and a journal: -[source] ----- -# STATE — Project State Checkpoint -# Format: a2ml (AI-readable markup) - -project: v-graphql -version: 0.1.0 -last-updated: 2026-02-14 -status: active - -phase: implementation -maturity: beta - -ecosystem: - part-of: RSR Framework - depends-on: [] - -milestones: - - name: Initial setup - completion: 100 - - name: Core implementation - completion: 0 ----- +* **Repo deed** (`*_chora.deed`): the `(status …)` clause carries the + lifecycle phase (`:phase`, `:since`, and its `(history …)`); the + `(maturity …)` clause carries `:level`. Read them with + `scripts/deed-field.sh status phase` and + `scripts/deed-field.sh maturity level`, or `just state-phase`. +* **Journal** (`docs/status/ROADMAP.adoc`): milestones, blockers, next + actions and maintenance runs. + +There is no completion percentage: it is derivable from the milestones. == Badge Schema -Generate badges from STATE.a2ml: +Generate badges from the repo deed's `(maturity)` and `(status)` clauses: [source,bash] ---- @@ -241,7 +228,6 @@ See `docs/BADGE_SCHEMA.adoc` for the full badge taxonomy. This template is part of: -* **STATE.a2ml Ecosystem**: Conversation checkpoints * **RSR Framework**: Repository standards * **Consent-Aware-HTTP**: .well-known compliance * **Hypatia**: Neurosymbolic security scanning diff --git a/docs/SETUP.adoc b/docs/SETUP.adoc new file mode 100644 index 0000000..c1c43ab --- /dev/null +++ b/docs/SETUP.adoc @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += Setting up occupancy-types by hand +:toc: +:toclevels: 2 + +This is the complete manual route: every step, in order, with nothing assumed. +You do not need it if you use `./launcher.sh --setup` (or ask an AI, see +link:AI_INSTALLATION_GUIDE.adoc[AI_INSTALLATION_GUIDE.adoc]), but those do +exactly what is written here, so this page is also what to read when they stop. + +[horizontal] +Repository:: https://github.com/hyperpolymath/occupancy-types +Kind:: library (idris2, zig) +Toolchain declared in:: `mise.toml` (versions `latest`, made concrete in `mise.lock`), and +`build/manifest.scm` / `build/guix.scm` / `build/channels.scm` for Guix +Every command below is also a `just` recipe:: run `just` to list them + +[[prerequisites]] +== 1. Prerequisites + +You need three things from your operating system. Everything else is installed +for you in step 3. + +[cols="1,2,3"] +|=== +|Tool |Why |Install + +|git +|to fetch the repository +|Fedora `sudo dnf install git` · Debian/Ubuntu `sudo apt install git` · macOS `xcode-select --install` · Windows `winget install Git.Git` + +|a C toolchain +|some language toolchains link native code +|Fedora `sudo dnf install gcc` · Debian/Ubuntu `sudo apt install build-essential` · macOS (included with the Xcode tools above) · Windows: use WSL2 + +|mise +|installs every other tool at the version this repo declares +|macOS `brew install mise` · Fedora `sudo dnf copr enable jdxcode/mise && sudo dnf install mise` · Windows `winget install jdx.mise` · Debian/Ubuntu and the rest: https://mise.jdx.dev/installing-mise.html — or download the installer, read it, then run it: `curl -fsSLo mise-install.sh https://mise.run && less mise-install.sh && sh mise-install.sh`. Then follow the one line it prints to activate it in your shell +|=== + +NOTE: On Windows, use WSL2 (`wsl --install`) and follow the Linux steps inside +it. The launcher detects the platform and says so in `./launcher.sh --version`. + +=== System packages for Idris2 + +Idris2 is built by pack on top of Chez Scheme and GMP, which come from your OS: + +* Fedora: `sudo dnf install chez-scheme gmp-devel` +* Debian/Ubuntu: `sudo apt install chezscheme libgmp-dev` +* macOS: `brew install chezscheme gmp` + +The Guix development shell (`build/manifest.scm`) already includes both. + +== 2. Get the code + +[source,bash] +---- +git clone https://github.com/hyperpolymath/occupancy-types.git +cd occupancy-types +---- + +Or with OPSM, the odds-and-sods package manager (see <>): +`opsm install https://github.com/hyperpolymath/occupancy-types.git --registry git` + +TIP: To start a *new* project from this template, use GitHub's *Use this template* +button (or clone as above), install the toolchain (step 3), then run +`mise exec -- just repo-init` (plain `just repo-init` once mise is activated); +it fills every placeholder token, and needs the toolchain's yq to keep the container +configs KYAML (it stops before writing anything without one). The steps below work +the same afterwards. + +== 3. Install the toolchain + +[source,bash] +---- +mise trust # allow this repository's mise.toml +mise install # installs everything mise.toml lists, at mise.lock's versions +---- + +The `just` commands from here on expect the toolchain on `PATH`: activate mise in +your shell (`eval "$(mise activate bash)"`, or your shell's equivalent), or run each +one as `mise exec -- just …`. + +That installs: + +[cols="1,2,3"] +|=== +|Language |Tools |If mise cannot supply it + +|idris2 |`idris2` |not a mise tool: install pack (https://github.com/stefan-hoeck/idris2-pack#installation), then `pack install-app idris2` +|zig |`zig` |https://ziglang.org/download/ or Guix `zig` +|(recipes) |`just`, `shellcheck` |your OS package manager, e.g. `dnf install just ShellCheck` +|=== + +`just` itself is installed by `mise install`. Without mise, install just from your +OS instead (`dnf install just`, `brew install just`, `cargo install just`); this +repository needs just 1.42 or newer. + +== 4. Set it up + +[source,bash] +---- +just setup +---- + +`just setup` repeats step 3 (harmless), fetches this repository's own +dependencies (idris2, zig), and finishes by running `just doctor`. + +== 5. Check it + +[source,bash] +---- +just doctor # PASS / WARN / FAIL for every requirement, with the fix for each +---- + +`just doctor` exits 0 when nothing FAILs. A WARN is advice, not a failure. +Every code it prints is explained in <>. + +== 6. Use it + +[source,bash] +---- +just build # build +just test # run the tests +just bench # run the benchmarks (says N/A when there are none) +just eval # tests + benchmarks with timings, saved under .eval/ +just run # run it (says N/A for a library) +just config-show # where the toolchain and config are declared +---- + +Anything that does not apply to a library says so and exits 0; nothing is faked. + +== 7. Keep it current + +[source,bash] +---- +just toolchain-refresh # mise up --bump, re-lock mise.lock, re-pin build/channels.scm +just heal # re-apply safe fixes if something drifted +---- + +[[guix]] +== The Guix route (alternative to steps 3–4) + +With GNU Guix installed you can skip mise for everything Guix packages: + +[source,bash] +---- +guix shell -m build/manifest.scm # the development shell +guix time-machine -C build/channels.scm -- shell -m build/manifest.scm # the exact, pinned Guix +guix build -f build/guix.scm # the package +---- + +Tools Guix does not package: idris2 (via pack). The Guix shell includes mise, so +inside it run `mise install` for those. `just dev-shell` picks Guix when it is present and mise +when it is not. + +[[opsm]] +== OPSM (odds-and-sods package manager) + +[source,bash] +---- +opsm install https://github.com/hyperpolymath/occupancy-types.git --registry git +---- + +occupancy-types has no registry package, so OPSM fetches it through its git +adapter; then provision it with `./launcher.sh --setup`. To get OPSM itself +(Erlang/OTP 26+ and Elixir 1.16+ required): + +[source,bash] +---- +git clone https://github.com/hyperpolymath/odds-and-sods-package-manager.git +cd odds-and-sods-package-manager/opsm_ex +mix deps.get && mix escript.build +install -m 0755 opsm ~/.local/bin/opsm +opsm --version +---- + +`just opsm` prints the same instructions. + +[[troubleshooting]] +== Troubleshooting + +Run `just doctor`, find the code it printed, apply the fix. `just heal` applies +every fix marked *auto* for you. + +[cols="1,3,3,1"] +|=== +|Code |Meaning |Fix |Auto + +|PV-E01 |git is not installed |Install git (step 1) |no +|PV-E02 |just is missing or older than 1.42 |`mise use -g just@latest` |no +|PV-E03 |a tool in mise.toml is not installed |`just setup` (or `mise install`) |yes +|PV-E10 |a language toolchain binary is missing |`just setup`; the doctor line names the manual route |yes, via mise +|PV-E11 |a system dependency from the provisioning deed is missing |install it from your OS (see <>) |no +|PV-E20 |mise.toml is missing |restore it: `git checkout -- mise.toml` |no +|PV-E27 |launcher.sh is not executable |`chmod +x launcher.sh` |yes +|PV-E40 |neither Guix nor mise is installed, so there is no dev shell |install mise (step 1) |no +|PV-E41 |`just toolchain-refresh` could not regenerate `build/guix/crates.scm`: the Guix crate importer defined fewer crates than `Cargo.lock` lists (often a network failure or a crate not yet on crates.io) |re-run `just toolchain-refresh`; the error is printed after the code. The old file is kept, so the build is unaffected |no +|PV-E50 |this repository's own `doctor-local` checks failed |read the output above the summary line; the recipe is in the Justfile |no +|PV-E51 |`build/just/doctor-local.sh` exited before its end (an `exit` or a failing command under `set -e`) |the doctor names the exit status; fix the script so it reports with pass/warn/fail instead of exiting |no +|PV-W01 |mise is not installed |install mise (step 1) |no +|PV-W20 |mise.lock is missing, so `latest` is not concrete |`just toolchain-refresh` |yes +|PV-W21 |both mise.toml and .mise.toml exist |merge into mise.toml, delete .mise.toml |no +|PV-W22 |a .tool-versions file competes with mise.toml |fold it into mise.toml |no +|PV-W23 |a mise config (mise.toml, .mise.toml, .tool-versions) pins a tool the estate does not use, or an npm:/pipx:/pip:/go: backend |remove that line |no +|PV-W24 |a Guix file is an unfilled template |regenerate it (maintainers: `provision-set realign`) |no +|PV-W25 |guix.scm is missing |as PV-W24 |no +|PV-W26 |manifest.scm is missing |as PV-W24; mise still works |no +|PV-W27 |launcher.sh is missing |as PV-W24 |no +|PV-W28 |a setup document is missing |as PV-W24 |no +|PV-W29 |a setup document still holds unfilled template slots |replace each `__SPEC_…__` slot with the facts for this repository |no +|PV-W30 |a Deno manifest (`deno.json`, `deno.jsonc`, `import_map.json`) exists |a notice, not a fault: Deno is permitted (tier 2) and reads `package.json`, which stays the one manifest |no +|PV-W31 |Nix files remain |use Guix |no +|PV-W32 |a Makefile remains |use the Justfile |no +|PV-W33 |Python files remain |not an estate language |no +|PV-W34 |npm/yarn/TypeScript files remain (`package-lock.json`, `yarn.lock`, `tsconfig.json`) |keep `package.json`, and let `js-runtimes regen` write `bun.lock`, `deno.lock` and `pnpm-lock.yaml` |no +|PV-W35 |both guix.scm and build/guix.scm exist |keep one; the engine uses build/ |no +|=== + +Still stuck? `just doctor > doctor.txt 2>&1` and open an issue at +https://github.com/hyperpolymath/occupancy-types/issues with that file attached. diff --git a/build/docs-seed/architecture.adoc b/docs/architecture.adoc similarity index 93% rename from build/docs-seed/architecture.adoc rename to docs/architecture.adoc index 685b024..68d0016 100644 --- a/build/docs-seed/architecture.adoc +++ b/docs/architecture.adoc @@ -1,8 +1,8 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -= Architecture — {{PROJECT_NAME}} -:revdate: {{CURRENT_YEAR}}-MM-DD +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += Architecture — occupancy-types +:revdate: 2026-MM-DD == System overview diff --git a/docs/architecture/REPOSITORY-MAP.adoc b/docs/architecture/REPOSITORY-MAP.adoc index 5e13913..d1b74b2 100644 --- a/docs/architecture/REPOSITORY-MAP.adoc +++ b/docs/architecture/REPOSITORY-MAP.adoc @@ -23,11 +23,11 @@ predecessors did. | `.clinerules` | yes -| AI editor rules. Cline/Cursor/Windsurf read these from the project +| AI editor rules. Cline reads them from the project ROOT, so the copies formerly under .machine_readable/ai/ were inert (.cursorrules and .windsurfrules were retired 2026-10-26) -| `.cursorrules` -| yes -| ROOT, so the copies formerly under .machine_readable/ai/ were inert. +| `.coderabbit.yaml` +| optional +| CodeRabbit reads its config from the root only; pins Docstring Coverage to mode: warning (estate decision, not a vendor default). Optional: only the template and the repos whose rulesets require the CodeRabbit context carry it | `.devcontainer/` | optional @@ -45,6 +45,10 @@ predecessors did. | yes | - +| `.gitbot-fleet/` +| yes +| fleet enrolment manifest + report-backs/ (empty in git; filled per run by the fork-governance fleet job). Read by the Hypatia verdict grid at this path and no other + | `.github/` | yes | community health + workflows (GitHub reads this path and no other) + versioned git hooks (.github/hooks/, wired via core.hooksPath) @@ -73,14 +77,6 @@ predecessors did. | yes | git reads .mailmap from the worktree root only -| `.tool-versions` -| yes -| - - -| `.windsurfrules` -| yes -| - - | `CHANGELOG.adoc` | yes | AsciiDoc is the estate-standard documentation format @@ -91,23 +87,27 @@ predecessors did. | `CLAUDE.md` | yes -| generated arrival pack; the generator, pre-commit and Claude Code all read it from the ROOT (do not hand-edit; edit the a2ml source) +| generated arrival pack; the generator, pre-commit and Claude Code all read it from the ROOT (do not hand-edit; edit the repo deed) -| `CONTRIBUTING.adoc` +| `CODE_OF_CONDUCT.adoc` | optional -| estate docs gate (standards scripts/check-docs-presence.sh) requires CONTRIBUTING at the ROOT; this is the copy it verifies +| community health file; GitHub accepts it at root, docs/ or .github/ -| `GEMINI.md` +| `CONTRIBUTING.adoc` | optional -| pointer to CLAUDE.md for repos without AGENTS.md yet +| estate docs gate (standards scripts/check-docs-presence.sh) requires CONTRIBUTING at the ROOT; this is the copy it verifies | `Justfile` | yes | thin; delegates phases to build/just/*.just | `LICENSE` +| optional +| forge-facing copy of LICENSES/MPL-2.0.txt. Optional, not required: + +| `LICENSE.adoc` | yes -| - +| the licence (MPL-2.0); renamed from LICENSE to the estate AsciiDoc format 2026-10-26 | `LICENSES/` | yes @@ -121,21 +121,21 @@ predecessors did. | yes | - +| `SUPPORT.adoc` +| optional +| community health file; GitHub accepts it at root, docs/ or .github/ + | `ULTRAPLAN.md` | yes | project plan of record (occupancy-types): thesis, rung contract, ladder, kill criteria, decision log -| `archetypes/` -| optional -| template-only variation seam (ADR-0003); `just repo-init` removes it from minted repos - | `benches/` | optional | Cargo-conventional at package root | `build/` | yes -| build orchestration: just/ phase modules, container/, docs-seed/, templates/, guix.scm (canon 1.2.1 guix-primary template_ref = build/) +| build orchestration: just/ phase modules, container/, docs-seed/, templates/, guix.scm (canon 1.2.1 guix-primary template_ref = "build/") | `ci/` | yes @@ -157,15 +157,23 @@ predecessors did. | optional | optional feature packs +| `launcher.sh` +| yes +| --setup/--doctor/--heal/--ai-setup (launcher-standard 0.6.0); the one file a newcomer runs first + +| `mise.lock` +| yes +| makes "latest" concrete: version + per-platform checksum; mise reads it beside mise.toml + | `mise.toml` | yes -| pinned toolchains +| the toolchain: every tool at "latest" (PROVISIONING-STANDARD §4) | `occupancy-types.agda-lib` | yes | Agda library descriptor for the occupancy-types proof modules -| `rsr-template-repo_chora.deed` +| `occupancy-types_chora.deed` | yes | the repo deed: universal AI entry point; family-7 allocation manifest + ply tree folded in (standards#837 pilot). Filename carries the repo slug (deed dispatch _chora.deed), so repo-init renames it at mint @@ -182,8 +190,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 @@ -209,6 +217,8 @@ your own repo - but do not read their emptiness as neglect here. [cols="1"] |=== +| `.machine_readable/bot_directives/` +| `.machine_readable/descriptiles/anchors/` | `.machine_readable/scripts/verification/` | `docs/governance/audit/` | `docs/governance/audit/compliance/` @@ -222,7 +232,6 @@ your own repo - but do not read their emptiness as neglect here. | `docs/governance/planning/could/` | `docs/governance/planning/must/` | `docs/governance/planning/should/` -| `docs/reports/compliance/` | `docs/reports/maintenance/` | `docs/reports/performance/` | `docs/reports/quality/` diff --git a/docs/architecture/THREAT-MODEL.adoc b/docs/architecture/THREAT-MODEL.adoc index 76ca59a..8ea5304 100644 --- a/docs/architecture/THREAT-MODEL.adoc +++ b/docs/architecture/THREAT-MODEL.adoc @@ -2,19 +2,19 @@ // Copyright (c) Jonathan D.A. Jewell = Threat Model // SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -== Threat Model: {{PROJECT_NAME}} +== Threat Model: occupancy-types === Document Info |=== | Field | Value -| Project | {{PROJECT_NAME}} +| Project | occupancy-types | Version | 1.0 -| Last Reviewed | {{DATE}} -| Author | {{AUTHOR}} +| Last Reviewed | 2026-11-12 +| Author | Jonathan D.A. Jewell | Methodology | STRIDE |=== @@ -39,7 +39,7 @@ === System Overview -Brief description of {{PROJECT_NAME}} and its architecture. +Brief description of occupancy-types and its architecture. NOTE: See link:../STATE-VISUALIZER.adoc[STATE-VISUALIZER.adoc] for the full architecture diagram and completion dashboard. @@ -120,7 +120,7 @@ NOTE: See link:../STATE-VISUALIZER.adoc[STATE-VISUALIZER.adoc] for the full arch | Unlogged deployment | Build artifacts | Medium | Medium | Medium | SLSA provenance; deployment audit trail | Denied merge of vulnerable code | Source code | Low | Medium | Low | Git history is immutable; signed commits -| Secret rotation without record | CI/CD secrets | Low | Low | Low | Secret rotation logged in STATE.a2ml +| Secret rotation without record | CI/CD secrets | Low | Low | Low | Secret rotation logged in `docs/status/ROADMAP.adoc` (Maintenance runs) |=== diff --git a/docs/attribution/CITATIONS.adoc b/docs/attribution/CITATIONS.adoc index 0a8b419..72bcf7f 100644 --- a/docs/attribution/CITATIONS.adoc +++ b/docs/attribution/CITATIONS.adoc @@ -1,36 +1,36 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -= {{PROJECT_NAME}} - Citation Guide += occupancy-types - Citation Guide :toc: == BibTeX [source,bibtex] ---- -@software{{{PROJECT_NAME}}_{{CURRENT_YEAR}}, - author = {{{AUTHOR_LAST}}, {{AUTHOR_FIRST}}}, - title = {{{PROJECT_NAME}}}, - year = {{{CURRENT_YEAR}}}, - url = {https://github.com/{{OWNER}}/{{PROJECT_NAME}}}, +@software{occupancy-types_2026, + author = {Jewell, Jonathan D.A.}, + title = {occupancy-types}, + year = {2026}, + url = {https://github.com/hyperpolymath/occupancy-types}, license = {MPL-2.0} } ---- == Harvard Style -{{AUTHOR_LAST}}, {{AUTHOR_INITIALS}} ({{CURRENT_YEAR}}) _{{PROJECT_NAME}}_ [Computer software]. Available at: https://github.com/{{OWNER}}/{{PROJECT_NAME}} +Jewell, J. (2026) _occupancy-types_ [Computer software]. Available at: https://github.com/hyperpolymath/occupancy-types == OSCOLA -{{AUTHOR}}, '{{PROJECT_NAME}}' ({{CURRENT_YEAR}}) +Jonathan D.A. Jewell, 'occupancy-types' (2026) == MLA -{{AUTHOR_LAST}}, {{AUTHOR_FIRST}} "{{PROJECT_NAME}}." {{CURRENT_YEAR}}, github.com/{{OWNER}}/{{PROJECT_NAME}}. +Jewell, Jonathan D.A. "occupancy-types." 2026, github.com/hyperpolymath/occupancy-types. == APA 7 -{{AUTHOR_LAST}}, {{AUTHOR_INITIALS}} ({{CURRENT_YEAR}}). _{{PROJECT_NAME}}_ [Computer software]. GitHub. https://github.com/{{OWNER}}/{{PROJECT_NAME}} +Jewell, J. (2026). _occupancy-types_ [Computer software]. GitHub. https://github.com/hyperpolymath/occupancy-types == See Also diff --git a/build/docs-seed/contributing.adoc b/docs/contributing.adoc similarity index 88% rename from build/docs-seed/contributing.adoc rename to docs/contributing.adoc index eb22aac..e489d1b 100644 --- a/build/docs-seed/contributing.adoc +++ b/docs/contributing.adoc @@ -1,12 +1,12 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -= Contributing — {{PROJECT_NAME}} -:revdate: {{CURRENT_YEAR}}-MM-DD +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += Contributing — occupancy-types +:revdate: 2026-MM-DD == Audience -Developers working *on* `{{PROJECT_NAME}}`. For consumers (people calling +Developers working *on* `occupancy-types`. For consumers (people calling or depending on it) see link:./usage.adoc[usage.adoc]. == Local-dev setup @@ -23,8 +23,8 @@ One-shot setup: [source,bash] ---- -git clone git@github.com:{{OWNER}}/{{PROJECT_NAME}}.git -cd {{PROJECT_NAME}} +git clone git@github.com:hyperpolymath/occupancy-types.git +cd occupancy-types just setup # installs deps, sets up hooks just test # runs the full test suite ---- @@ -86,6 +86,6 @@ When you make a non-obvious design decision, write it down: == Reporting issues -* Bugs in `{{PROJECT_NAME}}`: file at `{{OWNER}}/{{PROJECT_NAME}}/issues`. +* Bugs in `occupancy-types`: file at `hyperpolymath/occupancy-types/issues`. * Estate-wide concerns (policy, conventions, CI): file at `hyperpolymath/standards/issues`. diff --git a/docs/decisions/0000-template.adoc b/docs/decisions/0000-template.adoc index 32c2c2a..0025b64 100644 --- a/docs/decisions/0000-template.adoc +++ b/docs/decisions/0000-template.adoc @@ -2,7 +2,7 @@ // Copyright (c) Jonathan D.A. Jewell = Architecture Decision Record: 0000-template // SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) == [NUMBER]. [TITLE] diff --git a/docs/decisions/0001-adopt-rsr-standard.adoc b/docs/decisions/0001-adopt-rsr-standard.adoc index 45ec441..6e0bfeb 100644 --- a/docs/decisions/0001-adopt-rsr-standard.adoc +++ b/docs/decisions/0001-adopt-rsr-standard.adoc @@ -2,7 +2,9 @@ // Copyright (c) Jonathan D.A. Jewell = Architecture Decision Record: 0001-adopt-rsr-standard // SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) + +NOTE: Superseded in part (2026-10-06, rsr-template-repo#209): the `.a2ml` files named below no longer exist; their facts are clauses of the repo deed (`*_chora.deed`). == 1. Adopt Rhodium Standard Repository (RSR) Template @@ -50,11 +52,13 @@ The RSR template provides: CODE_OF_CONDUCT.md, LICENSE (MPL-2.0) - **Architecture Decision Records** in `docs/decisions/` +NOTE: 2026-10-02 — rsr-template-repo#209 retired STATE/META/ECOSYSTEM/AGENTIC/CLADE.a2ml; their content is now in the repo deed (`*_chora.deed`) and `docs/status/ROADMAP.adoc`. + New repositories are created by cloning the template: [source,bash] ---- -git clone https://github.com/{{OWNER}}/rsr-template-repo new-repo-name +git clone https://github.com/hyperpolymath/rsr-template-repo new-repo-name cd new-repo-name rm -rf .git && git init ---- diff --git a/build/docs-seed/decisions/0001-template.adoc b/docs/decisions/0001-template.adoc similarity index 94% rename from build/docs-seed/decisions/0001-template.adoc rename to docs/decisions/0001-template.adoc index eff8bb8..b4070a8 100644 --- a/build/docs-seed/decisions/0001-template.adoc +++ b/docs/decisions/0001-template.adoc @@ -1,8 +1,8 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) = ADR-0001 — Use Architecture Decision Records -:revdate: {{CURRENT_YEAR}}-MM-DD +:revdate: 2026-MM-DD :status: ACCEPTED == Context diff --git a/docs/decisions/0002-variant-contract.adoc b/docs/decisions/0002-variant-contract.adoc index dfed0ec..80e1674 100644 --- a/docs/decisions/0002-variant-contract.adoc +++ b/docs/decisions/0002-variant-contract.adoc @@ -2,6 +2,8 @@ // Copyright (c) Jonathan D.A. Jewell = Architecture Decision Record: 0002-variant-contract +NOTE: Superseded in part (2026-10-06, rsr-template-repo#209): the `.a2ml` files named below no longer exist; their facts are clauses of the repo deed (`*_chora.deed`). The variant contract is the `(variant)` clause that `just repo-init` writes into a minted child's deed. + == 2. Variant templates declare a machine-readable contract with their parent Date: 2026-07-07 diff --git a/docs/decisions/0003-forge-and-sustain-lifecycle.adoc b/docs/decisions/0003-forge-and-sustain-lifecycle.adoc index 9726e04..540fffc 100644 --- a/docs/decisions/0003-forge-and-sustain-lifecycle.adoc +++ b/docs/decisions/0003-forge-and-sustain-lifecycle.adoc @@ -2,6 +2,8 @@ // Copyright (c) Jonathan D.A. Jewell = Architecture Decision Record: 0003-forge-and-sustain-lifecycle +NOTE: Superseded in part (2026-10-06, rsr-template-repo#209): the `.a2ml` files named below no longer exist; their facts are clauses of the repo deed (`*_chora.deed`). `just repo-init` writes the `(rsr-profile)`, `(variant)` and `(provenance)` clauses into a minted child's deed, and an archetype is `archetypes//_praxis.deed`. + == 3. One lifecycle, one contract: Forge (mint → provision → configure → harness) and Sustain (corrective / adaptive / perfective / preventive) Date: 2026-07-21 diff --git a/docs/developer/ABI-FFI-README.adoc b/docs/developer/ABI-FFI-README.adoc index 2a9ff08..96934a2 100644 --- a/docs/developer/ABI-FFI-README.adoc +++ b/docs/developer/ABI-FFI-README.adoc @@ -3,7 +3,7 @@ = ABI/FFI Standards {{~ Aditionally delete this line and fill out the template below ~}} -== {{PROJECT}} ABI/FFI Documentation +== OCCUPANCY_TYPES ABI/FFI Documentation === Overview @@ -29,7 +29,7 @@ This library follows the **Hyperpolymath RSR Standard** for ABI and FFI design: ▼ ┌─────────────────────────────────────────────┐ │ C Headers (auto-generated) │ -│ generated/abi/{{project}}.h │ +│ generated/abi/occupancy_types.h │ └─────────────────┬───────────────────────────┘ │ │ imported by @@ -42,7 +42,7 @@ This library follows the **Hyperpolymath RSR Standard** for ABI and FFI design: │ - Memory-safe by default │ └─────────────────┬───────────────────────────┘ │ - │ compiled to lib{{project}}.so/.a + │ compiled to liboccupancy_types.so/.a ▼ ┌─────────────────────────────────────────────┐ │ Any Language via C ABI │ @@ -53,7 +53,7 @@ This library follows the **Hyperpolymath RSR Standard** for ABI and FFI design: === Directory Structure ---- -{{project}}/ +occupancy_types/ ├── src/ │ ├── abi/ # ABI definitions (Idris2) │ │ ├── Types.idr # Core type definitions with proofs @@ -70,11 +70,11 @@ This library follows the **Hyperpolymath RSR Standard** for ABI and FFI design: │ ├── test/ │ │ └── integration_test.zig │ └── include/ -│ └── {{project}}.h # C header (optional, can be generated) +│ └── occupancy_types.h # C header (optional, can be generated) │ ├── generated/ # Auto-generated files │ └── abi/ -│ └── {{project}}.h # Generated from Idris2 ABI +│ └── occupancy_types.h # Generated from Idris2 ABI │ └── bindings/ # Language-specific wrappers (optional) ├── rust/ @@ -212,7 +212,7 @@ zig build test # Run tests [source,bash] ---- cd src/abi -idris2 --cg c-header Types.idr -o ../../generated/abi/{{project}}.h +idris2 --cg c-header Types.idr -o ../../generated/abi/occupancy_types.h ---- ==== Cross-Compile @@ -237,19 +237,19 @@ zig build -Dtarget=x86_64-windows [source,c] ---- -#include "{{project}}.h" +#include "occupancy_types.h" int main() { - void* handle = {{project}}_init(); + void* handle = occupancy_types_init(); if (!handle) return 1; - int result = {{project}}_process(handle, 42); + int result = occupancy_types_process(handle, 42); if (result != 0) { - const char* err = {{project}}_last_error(); + const char* err = occupancy_types_last_error(); fprintf(stderr, "Error: %s\n", err); } - {{project}}_free(handle); + occupancy_types_free(handle); return 0; } ---- @@ -257,14 +257,14 @@ int main() { Compile with: [source,bash] ---- -gcc -o example example.c -l{{project}} -L./zig-out/lib +gcc -o example example.c -loccupancy_types -L./zig-out/lib ---- ==== From Idris2 [source,idris] ---- -import {{PROJECT}}.ABI.Foreign +import OCCUPANCY_TYPES.ABI.Foreign main : IO () main = do @@ -282,22 +282,22 @@ main = do [source,rust] ---- -#[link(name = "{{project}}")] +#[link(name = "occupancy_types")] extern "C" { - fn {{project}}_init() -> *mut std::ffi::c_void; - fn {{project}}_free(handle: *mut std::ffi::c_void); - fn {{project}}_process(handle: *mut std::ffi::c_void, input: u32) -> i32; + fn occupancy_types_init() -> *mut std::ffi::c_void; + fn occupancy_types_free(handle: *mut std::ffi::c_void); + fn occupancy_types_process(handle: *mut std::ffi::c_void, input: u32) -> i32; } fn main() { unsafe { - let handle = {{project}}_init(); + let handle = occupancy_types_init(); assert!(!handle.is_null()); - let result = {{project}}_process(handle, 42); + let result = occupancy_types_process(handle, 42); assert_eq!(result, 0); - {{project}}_free(handle); + occupancy_types_free(handle); } } ---- @@ -306,21 +306,21 @@ fn main() { [source,julia] ---- -const lib{{project}} = "lib{{project}}" +const liboccupancy_types = "liboccupancy_types" function init() - handle = ccall((:{{project}}_init, lib{{project}}), Ptr{Cvoid}, ()) + handle = ccall((:occupancy_types_init, liboccupancy_types), Ptr{Cvoid}, ()) handle == C_NULL && error("Failed to initialize") handle end function process(handle, input) - result = ccall((:{{project}}_process, lib{{project}}), Cint, (Ptr{Cvoid}, UInt32), handle, input) + result = ccall((:occupancy_types_process, liboccupancy_types), Cint, (Ptr{Cvoid}, UInt32), handle, input) result end function cleanup(handle) - ccall((:{{project}}_free, lib{{project}}), Cvoid, (Ptr{Cvoid},), handle) + ccall((:occupancy_types_free, liboccupancy_types), Cvoid, (Ptr{Cvoid},), handle) end # Usage @@ -377,7 +377,7 @@ When modifying the ABI/FFI: 2. **Generate C header** ```bash - idris2 --cg c-header src/interface/Abi/Types.idr -o generated/abi/{{project}}.h + idris2 --cg c-header src/interface/Abi/Types.idr -o generated/abi/occupancy_types.h ``` 3. **Update FFI implementation** (`ffi/zig/src/main.zig`) @@ -396,10 +396,10 @@ When modifying the ABI/FFI: === License -{{LICENSE}} +MPL-2.0 === See Also - https://idris2.readthedocs.io[Idris2 Documentation] - https://ziglang.org/documentation/master/[Zig Documentation] -- https://github.com/{{OWNER}}/rhodium-standard-repositories[Rhodium Standard Repositories] +- https://github.com/hyperpolymath/rhodium-standard-repositories[Rhodium Standard Repositories] diff --git a/docs/governance/CRG-AUDIT-TEMPLATE.adoc b/docs/governance/CRG-AUDIT-TEMPLATE.adoc index e56cbff..88bdd28 100644 --- a/docs/governance/CRG-AUDIT-TEMPLATE.adoc +++ b/docs/governance/CRG-AUDIT-TEMPLATE.adoc @@ -1,8 +1,8 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -= {{PROJECT_NAME}} — CRG Audit ({{CURRENT_DATE}}) -{{AUTHOR}} -{{CURRENT_DATE}} += occupancy-types — CRG Audit (2026-11-12) +Jonathan D.A. Jewell +2026-11-12 :toc: :sectnums: @@ -18,17 +18,17 @@ == Scope and Standard -Evaluates the `{{REPO}}` repository against +Evaluates the `occupancy-types` repository against *Component Readiness Grades v2.0 (STRICT)*, 2026-03-30 revision. - Standard: `standards/component-readiness-grades/COMPONENT-READINESS-GRADES.md` -- Template (criteria boilerplate): `rsr-template-repo/docs/governance/CRG-CRITERIA.adoc` +- Template (criteria boilerplate): `occupancy-types/docs/governance/CRG-CRITERIA.adoc` - Self-declared grade (prior art): - * `.machine_readable/descriptiles/STATE.a2ml` → `grade = ""` + * `docs/status/ROADMAP.adoc` → `Declared CRG grade: ` line * `docs/READINESS.md` (if present) → per-component grades * Third-party badges (Glama, OpenSSF, etc.) if any — note date and scope -- Audit date: {{CURRENT_DATE}} -- Auditor: {{AUTHOR}} +- Audit date: 2026-11-12 +- Auditor: Jonathan D.A. Jewell The audit grades the repo *as-is today*, not aspirationally. @@ -49,7 +49,7 @@ The audit grades the repo *as-is today*, not aspirationally. Publication requires B+. Long alpha is discipline, not shame. -== Evidence Inventory (verified {{CURRENT_DATE}}) +== Evidence Inventory (verified 2026-11-12) === Structural compliance (Grade D floor) @@ -64,8 +64,8 @@ Do *not* mark PRESENT without citing the path. | PRESENT / PARTIAL (count) / ABSENT | `.github/workflows/` -| `.machine_readable/descriptiles/` canonical A2ML -| PRESENT (STATE, META, ECOSYSTEM, AGENTIC, NEUROSYM, PLAYBOOK) / ABSENT +| `.machine_readable/descriptiles/` +| PRESENT (NEUROSYM, PLAYBOOK) / ABSENT | Layout matches CLAUDE.md invariant | `*_chora.deed` (repo deed) @@ -101,7 +101,7 @@ Do *not* mark PRESENT without citing the path. | Build-system policy (RSR R-020) | Remote -| `git@{{FORGE}}:{{OWNER}}/{{REPO}}.git` (origin only) / drift +| `git@github.com:hyperpolymath/occupancy-types.git` (origin only) / drift | `git remote -v` |=== @@ -135,15 +135,15 @@ RSR compliance verdict: *met* / *partial* / *not met* — Grade D floor satisfie | Dangerous-pattern scan (`assert_total`, `Admitted`, `sorry`, `unsafeCoerce`, `Obj.magic`) | N -| Grepped {{CURRENT_DATE}}. +| Grepped 2026-11-12. |=== === Test matrix -Cite *from authoritative source* (STATE.a2ml or CI log). Do not re-count by -filesystem grep — grade the evidence that already exists. +Cite *from authoritative source* (`docs/status/ROADMAP.adoc` or CI log). Do not +re-count by filesystem grep — grade the evidence that already exists. -- `total-tests = N` (from `.machine_readable/descriptiles/STATE.a2ml` or CI). +- Test count N (from `docs/status/ROADMAP.adoc` or CI). - Breakdown by suite: list each named suite and its count. - CI: `.github/workflows/.yml` runs on push / tag / schedule. - Last green run: date + commit SHA. @@ -166,7 +166,7 @@ v2.0 C requires **per-file and per-directory orientation**. Evidence checklist: === Dogfooding / home-stable evidence -Cite STATE.a2ml `[dogfooding-status]`. For each line item: +Cite the `== Dogfooding status` section of `docs/status/ROADMAP.adoc`. For each line item: - WHAT was dogfooded (capability), - HOW (concrete use-path), - WHEN (date completed), @@ -180,15 +180,15 @@ Cite STATE.a2ml `[dogfooding-status]`. For each line item: === External validation (Grade B / A) Distinguish carefully — v2.0 B requires **real external users with feedback**, -not catalogue listings. Populate the canonical STATE.a2ml sections named -below; internal-capability items belong under `[grade-b-status]` (legacy) or -a roadmap section, *not* under `[external-targets]`. +not catalogue listings. Populate the `docs/status/ROADMAP.adoc` sections named +below; internal-capability items belong in another roadmap section, *not* +under `== External targets`. -- STATE.a2ml `[external-targets]` — at least 6 diverse entries for B. +- ROADMAP.adoc `== External targets` — at least 6 diverse entries for B. Target identity + date + outcome for each. If absent, grade is capped at C. -- STATE.a2ml `[issues-fed-back]` — closed-issue trail with external-reporter +- ROADMAP.adoc `== Issues fed back` — closed-issue trail with external-reporter label for B. -- STATE.a2ml `[field-signal]` — multi-source external confirmation for A. +- ROADMAP.adoc `== Field signal` — multi-source external confirmation for A. - Awesome-list PRs merged (N). *Catalogue listings, not dogfooders.* - Directory/registry listings (MCP directory, Glama, etc.) with automated assessment badges — *third-party automated assessment, not external usage*. @@ -196,7 +196,8 @@ a roadmap section, *not* under `[external-targets]`. NOTE: Legacy `[grade-b-status]` sections (pre-2026-04-18) often mix internal capabilities with external targets. During audit, re-classify each entry -and either move it to `[external-targets]` or drop it. See the boj-server +and either move it to `== External targets` in `docs/status/ROADMAP.adoc` +or drop it. See the boj-server audit for a worked example (six items all re-classified as internal). == Grade x Evidence Matrix @@ -222,7 +223,7 @@ audit for a worked example (six items all re-classified as internal). | D | ✓/✗ -| All RSR structural requirements met; N tests; scope documented in STATE.a2ml, ROADMAP.adoc +| All RSR structural requirements met; N tests; scope documented in the repo deed, ROADMAP.adoc | ... | C @@ -280,8 +281,9 @@ them. Examples: == Related Files - `docs/READINESS.md` — per-component table. Note v1.0-vs-v2.0 schema alignment. -- `.machine_readable/descriptiles/STATE.a2ml` — authoritative self-declared state. -- `docs/governance/CRG-LIFT-PLAN-{{CURRENT_DATE}}.adoc` — companion plan for +- `*_chora.deed` (repo deed) — authoritative phase and maturity (`(status)`, `(maturity)`). +- `docs/status/ROADMAP.adoc` — milestones, evidence and the declared CRG grade. +- `docs/governance/CRG-LIFT-PLAN-2026-11-12.adoc` — companion plan for D→C (and medium-term C→B) lifts. - `docs/governance/CRG-CRITERIA.adoc` — boilerplate criteria doc. diff --git a/docs/governance/CRG-CRITERIA.a2ml b/docs/governance/CRG-CRITERIA.a2ml deleted file mode 100644 index 6162585..0000000 --- a/docs/governance/CRG-CRITERIA.a2ml +++ /dev/null @@ -1,108 +0,0 @@ -; SPDX-License-Identifier: MPL-2.0 -; Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -; Component Readiness Grades (CRG) — Machine-readable specification -; Format: A2ML (AI-to-Machine Language) -; Standard: CRG v1.0 - -(standard - (name "Component Readiness Grades") - (abbreviation "CRG") - (version "1.0") - (date "2026-02-28") - (author "Jonathan D.A. Jewell ") - (license "MPL-2.0") - (family "RSR")) - -(grades - (grade - (code X) - (name "Untested") - (release-stage #f) - (ordinal 0) - (description "No testing has been performed. Status unknown.") - (evidence-required "none") - (minimum-for #f)) - (grade - (code F) - (name "Harmful / Wasteful") - (release-stage #f) - (ordinal 1) - (description "Actively harmful, wasteful, or better handled externally. Reject, deprecate, or delegate.") - (evidence-required "documented test results showing harm, waste, or redundancy; comparison with alternatives") - (minimum-for #f)) - (grade - (code E) - (name "Minimal / Salvageable") - (release-stage "pre-alpha") - (ordinal 2) - (description "Does something slight. Barely functional. Needs redesign or major work.") - (evidence-required "at least one successful test case; documented failures and limitations") - (minimum-for #f)) - (grade - (code D) - (name "Partial / Inconsistent") - (release-stage "alpha") - (ordinal 3) - (description "Works on some things but not systematically.") - (evidence-required "matrix of tested scenarios; documented scope vs actual capabilities") - (minimum-for "alpha")) - (grade - (code C) - (name "Self-Validated") - (release-stage "beta") - (ordinal 4) - (description "Tested on the tool/project itself (dogfooding). Reliable in home context.") - (evidence-required "active dogfooding; CI integration or equivalent; no known failures in home context") - (minimum-for "beta")) - (grade - (code B) - (name "Broadly Validated") - (release-stage "release-candidate") - (ordinal 5) - (description "Tested on at least 6 disparate, unrelated targets.") - (evidence-required "list of 6+ diverse targets with test results; evidence of feedback incorporation") - (minimum-for "release-candidate")) - (grade - (code A) - (name "Field-Proven") - (release-stage "stable") - (ordinal 6) - (description "Real-world external feedback confirms value. Does no harm in the wild.") - (evidence-required "real-world usage data; feedback incorporation evidence; no unresolved harm reports") - (minimum-for "stable"))) - -(transitions - (promotion - (from X) (to E) (requirement "Run at least one test. Document results.")) - (promotion - (from X) (to F) (requirement "Evaluate and determine harmful or wasteful.")) - (promotion - (from E) (to D) (requirement "Fix critical failures. Document scope.")) - (promotion - (from D) (to C) (requirement "Dogfood on own project. Fix what breaks.")) - (promotion - (from C) (to B) (requirement "Test on 6+ diverse external targets. Fix what breaks.")) - (promotion - (from B) (to A) (requirement "Ship. Collect external feedback. Demonstrate no harm.")) - (demotion - (from A) (to B) (trigger "External feedback dries up or reveals no longer useful.")) - (demotion - (from A) (to F) (trigger "External feedback reveals component causes harm.")) - (demotion - (from B) (to C) (trigger "Broad validation reveals unfixed failures.")) - (demotion - (from C) (to D) (trigger "Home context changes and component no longer reliable.")) - (demotion - (from C) (to F) (trigger "Dogfooding reveals net negative.")) - (demotion - (from D) (to E) (trigger "Scope narrows to barely functional.")) - (demotion - (from any) (to F) (trigger "Better external alternative makes this pure opportunity cost."))) - -(conformance - (rule "Each assessable component MUST have a grade from {X, F, E, D, C, B, A}.") - (rule "Each grade above X MUST be supported by evidence per section 4.") - (rule "Assessments MUST be recorded in a version-controlled location.") - (rule "Assessments MUST be reviewed at least once per release cycle.") - (rule "Release stages MUST respect minimum grade thresholds.")) diff --git a/docs/governance/MAINTENANCE-CHECKLIST.adoc b/docs/governance/MAINTENANCE-CHECKLIST.adoc index 48149d8..7550b1e 100644 --- a/docs/governance/MAINTENANCE-CHECKLIST.adoc +++ b/docs/governance/MAINTENANCE-CHECKLIST.adoc @@ -8,7 +8,7 @@ Use this as a repeatable maintenance runbook for any repo. Companion policy: - `docs/practice/SOFTWARE-DEVELOPMENT-APPROACH.adoc` (human-readable) -- `.machine_readable/policies/SOFTWARE-DEVELOPMENT-APPROACH.a2ml` (machine-readable) +- `*_chora.deed` `(policies (software-development-approach))` (machine-readable) ## Canonical Repo Baseline (Final) @@ -91,8 +91,8 @@ Run this pass at the end of a corrective/adaptive/perfective cycle: - `MAINTAINER` in root - `.github/CODEOWNERS` - [ ] Keep machine-readable canonical structure under `.machine_readable/`: - - state/meta/ecosystem files (`*.a2ml` or repo standard) - - `anchors/ANCHOR.a2ml` + - state/meta/ecosystem/anchor facts as clauses of the root repo deed + (`*_chora.deed`: `(status)`, `(meta)`, `(ecosystem)`, `(anchor)`); no `.a2ml` files - `contractiles/` (`must`, `trust`, `intend`, and related) - `ai/` for AI guidance files - `bot_directives/` for bot control files @@ -412,7 +412,7 @@ Add these checks for language/compiler repositories with formal ABI/FFI constrai - [x] README structure restored (index/TOC, audience paths, quickstart sanity). - [x] Wiki split by audience (laypeople/users/developers) and linked from docs index. - [x] Root-level clutter reduced (archive, analysis, reports relegated to `docs/` subtrees). -- [x] Machine-readable docs synchronized (`STATE.a2ml`, `META.a2ml`, `ECOSYSTEM.a2ml`, contractiles). +- [x] Machine-readable docs synchronized (repo deed `(status)`, `(meta)`, `(ecosystem)` clauses, contractiles). - [x] Human-readable docs synchronized (`README`, `QUICK_STATUS`, roadmap, wiki home). - [x] `Mustfile` invariants present and enforceable in CI. - [x] `Trustfile` and `Intentfile` present and complete. diff --git a/docs/governance/TEMPLATE-LINEAGE-AUDIT.adoc b/docs/governance/TEMPLATE-LINEAGE-AUDIT.adoc index ea01513..be81c71 100644 --- a/docs/governance/TEMPLATE-LINEAGE-AUDIT.adoc +++ b/docs/governance/TEMPLATE-LINEAGE-AUDIT.adoc @@ -5,6 +5,8 @@ :toclevels: 3 :sectnums: +NOTE: Superseded in part (2026-10-06, rsr-template-repo#209): the `.a2ml` files named below no longer exist; their facts are clauses of the repo deed (`*_chora.deed`). + *Measured 2026-09-15 against the live estate.* This document records what the template's self-identity repair passes do, what they provably did *not* do to repositories minted before they existed, and — just as importantly — which of @@ -47,7 +49,9 @@ that it WAS the template, and would have reported its code analysis into the TEMPLATE's SonarCloud project. ____ -The ordinary mint path rewrites `{{OWNER}}`, `{{REPO}}` and friends. Those +NOTE: 2026-10-02 — rsr-template-repo#209 retired STATE/META/ECOSYSTEM/AGENTIC/CLADE.a2ml; their content is now in the repo deed (`*_chora.deed`) and `docs/status/ROADMAP.adoc`. + +The ordinary mint path rewrites `hyperpolymath`, `occupancy-types` and friends. Those tokens *are* consumed widely — 56 files in this repository, including the README badge block, `CITATION.cff`, `.github/CODEOWNERS`, `.github/settings.yml` and the three AI-assistant rule files. The problem is @@ -188,8 +192,10 @@ Both `Justfile:24` and `.machine_readable/contractiles/Justfile:24` declare `canonical-name = "rsr-julia-library-template-repo"`. `OWNER :=` is correct in both files. +NOTE: 2026-10-02 — in this template, rsr-template-repo#209 retired CLADE.a2ml; the canonical name now lives in the repo deed header (`:canonical-name`). + *Severity is lower than it appears.* Across every `Justfile` and `*.just` in -that repository there are *zero* `{{OWNER}}` or `{{REPO}}` interpolations, so +that repository there are *zero* `hyperpolymath` or `occupancy-types` interpolations, so neither variable is consumed by any recipe. This is a wrong self-description with no runtime effect — worth fixing because a template propagates its own mistakes, not because anything currently misbehaves. diff --git a/docs/governance/TEMPLATE-STANDARDS-AUDIT.adoc b/docs/governance/TEMPLATE-STANDARDS-AUDIT.adoc index ea82302..079d2aa 100644 --- a/docs/governance/TEMPLATE-STANDARDS-AUDIT.adoc +++ b/docs/governance/TEMPLATE-STANDARDS-AUDIT.adoc @@ -7,6 +7,8 @@ v1.0, 2026-04-07 :toclevels: 3 :sectnums: +NOTE: Superseded in part (2026-10-06, rsr-template-repo#209): the `.a2ml` files named below no longer exist; their facts are clauses of the repo deed (`*_chora.deed`). + == Scope And Inventory Audit scope: @@ -28,7 +30,7 @@ Inventory snapshot at audit time: | Placeholder badge tokens in README | `README.adoc` -| `{{OPENSSF_PROJECT_ID}}`, `{{OWNER}}`, `{{REPO}}` +| `OPENSSF_PROJECT_ID`, `OWNER`, `REPO` (README badge token names) | placeholder | Template placeholders are intentional pre-bootstrap content. | Keep; document clearly as template tokens. @@ -155,7 +157,7 @@ Current status: rsr-template-repo/ README.adoc AUDIT.adoc - 0-AI-MANIFEST.a2ml + rsr-template-repo_chora.deed EXPLAINME.adoc Justfile coordination.k9 diff --git a/docs/governance/TSDM.a2ml b/docs/governance/TSDM.a2ml deleted file mode 100644 index f27036c..0000000 --- a/docs/governance/TSDM.a2ml +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 ---- -### [TSDM_SPEC] -id: "tsdm-standard" -version: "1.0.0" - -axes: - axis_1: - name: "Planning" - levels: ["must", "should", "could"] - axis_2: - name: "Maintenance" - levels: ["corrective", "adaptive", "perfective"] - axis_3: - name: "Audit" - levels: ["systems", "compliance", "effects"] - -invariants: - - "Every task MUST map to at least one TSDM coordinate" - - "Axis 1 priority governs resource allocation" - - "Axis 2 type governs commit categorisation" - - "Axis 3 focus governs audit depth" diff --git a/docs/governance/TSDM.adoc b/docs/governance/TSDM.adoc index 42899ec..bbb4ba1 100644 --- a/docs/governance/TSDM.adoc +++ b/docs/governance/TSDM.adoc @@ -25,4 +25,4 @@ TSDM is a three-dimensional governance framework designed for high-assurance, lo == Integration -TSDM is the operational core of the Rhodium Standard. Every task in the `Justfile` and every state change in `STATE.a2ml` should be justifiable within this framework. +TSDM is the operational core of the Rhodium Standard. Every task in the `Justfile` and every state change in the repo deed (`*_chora.deed` `(status)` / `(maturity)`) or `docs/status/ROADMAP.adoc` should be justifiable within this framework. diff --git a/docs/onboarding/QUICKSTART-DEV.adoc b/docs/onboarding/QUICKSTART-DEV.adoc index d2c4a5e..f66fd42 100644 --- a/docs/onboarding/QUICKSTART-DEV.adoc +++ b/docs/onboarding/QUICKSTART-DEV.adoc @@ -1,14 +1,14 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell // Template: QUICKSTART-DEV.adoc — clone → build → test → PR -// Replace rsr-template-repo, {{BUILD_CMD}}, {{TEST_CMD}}, {{LANG_STACK}} with actuals -= rsr-template-repo — Quick Start for Developers +// Replace occupancy-types, UNASSIGNED, UNASSIGNED, UNASSIGNED with actuals += occupancy-types — Quick Start for Developers :toc: :toclevels: 2 == Tech Stack -{{LANG_STACK}} +UNASSIGNED == Set Up Development Environment @@ -23,8 +23,8 @@ guix shell [source,bash] ---- -git clone https://github.com/hyperpolymath/rsr-template-repo.git -cd rsr-template-repo +git clone https://github.com/hyperpolymath/occupancy-types.git +cd occupancy-types just setup-dev ---- @@ -32,21 +32,21 @@ just setup-dev [source,bash] ---- -{{BUILD_CMD}} +UNASSIGNED ---- == Test [source,bash] ---- -{{TEST_CMD}} +UNASSIGNED ---- == Project Structure [source] ---- -rsr-template-repo/ +occupancy-types/ ├── src/ # Source code ├── src/interface/Abi/ # Idris2 ABI definitions (if applicable) ├── ffi/zig/ # Zig FFI bridge (if applicable) @@ -84,7 +84,7 @@ just panic-scan # No new security issues Read `.machine_readable/MUST.contractile` before making changes. Key invariants that must never be violated: -{{MUST_INVARIANTS}} +UNASSIGNED == LLM/AI Agent Development @@ -100,5 +100,5 @@ Or read the repo deed (`*_chora.deed` at root) and `.claude/CLAUDE.md` directly. == Get Help * **Architecture**: link:EXPLAINME.adoc[EXPLAINME.adoc] -* **Wiki**: https://github.com/hyperpolymath/rsr-template-repo/wiki +* **Wiki**: https://github.com/hyperpolymath/occupancy-types/wiki * **Report issue**: `just help-me` diff --git a/docs/onboarding/QUICKSTART-MAINTAINER.adoc b/docs/onboarding/QUICKSTART-MAINTAINER.adoc index aaaa348..94ac069 100644 --- a/docs/onboarding/QUICKSTART-MAINTAINER.adoc +++ b/docs/onboarding/QUICKSTART-MAINTAINER.adoc @@ -1,30 +1,30 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell // Template: QUICKSTART-MAINTAINER.adoc — packaging, deploying, and maintaining -// Replace rsr-template-repo, {{PACKAGE_NAME}}, {{DEPS}} with actuals -= rsr-template-repo — Quick Start for Platform Maintainers +// Replace occupancy-types, UNASSIGNED, UNASSIGNED with actuals += occupancy-types — Quick Start for Platform Maintainers :toc: :toclevels: 2 == Overview -This guide covers packaging, deploying, and maintaining rsr-template-repo for +This guide covers packaging, deploying, and maintaining occupancy-types for distribution on your platform. == Runtime Dependencies -{{DEPS}} +UNASSIGNED == Build from Source [source,bash] ---- -git clone https://github.com/hyperpolymath/rsr-template-repo.git -cd rsr-template-repo +git clone https://github.com/hyperpolymath/occupancy-types.git +cd occupancy-types just build-release ---- -Output: `{{BUILD_OUTPUT_PATH}}` +Output: `UNASSIGNED` == Packaging @@ -40,7 +40,7 @@ guix build -f build/guix.scm [source,bash] ---- just stapeln-export # Generates Containerfile -podman build -t rsr-template-repo . +podman build -t occupancy-types . ---- === Manual Package @@ -59,10 +59,10 @@ Files installed: | `$PREFIX/bin/` | Executables -| `$PREFIX/share/{{PACKAGE_NAME}}/` +| `$PREFIX/share/UNASSIGNED/` | Data files, assets -| `$PREFIX/share/doc/{{PACKAGE_NAME}}/` +| `$PREFIX/share/doc/UNASSIGNED/` | Documentation | `$PREFIX/share/applications/` @@ -74,9 +74,9 @@ Files installed: == Configuration -Default config location: `$XDG_CONFIG_HOME/{{PACKAGE_NAME}}/config.toml` +Default config location: `$XDG_CONFIG_HOME/UNASSIGNED/config.toml` -Fallback: `$HOME/.config/{{PACKAGE_NAME}}/config.toml` +Fallback: `$HOME/.config/UNASSIGNED/config.toml` == Health Checks @@ -96,7 +96,7 @@ just build-release just install --prefix=/usr/local ---- -Or via OPSM: `opsm update {{PACKAGE_NAME}}` +Or via OPSM: `opsm update UNASSIGNED` == Security Notes @@ -111,13 +111,13 @@ For deploying multiple instances (e.g., different users or tenants): [source,bash] ---- -just install --prefix=/opt/{{PACKAGE_NAME}}-instance1 --config=/etc/{{PACKAGE_NAME}}/instance1.toml -just install --prefix=/opt/{{PACKAGE_NAME}}-instance2 --config=/etc/{{PACKAGE_NAME}}/instance2.toml +just install --prefix=/opt/UNASSIGNED-instance1 --config=/etc/UNASSIGNED/instance1.toml +just install --prefix=/opt/UNASSIGNED-instance2 --config=/etc/UNASSIGNED/instance2.toml ---- Each instance has isolated config, data, and logs. == Reporting Issues -* Upstream: https://github.com/hyperpolymath/rsr-template-repo/issues +* Upstream: https://github.com/hyperpolymath/occupancy-types/issues * With diagnostic: `just help-me` (pre-fills context) diff --git a/docs/onboarding/QUICKSTART-USER.adoc b/docs/onboarding/QUICKSTART-USER.adoc index 0893e63..48273e5 100644 --- a/docs/onboarding/QUICKSTART-USER.adoc +++ b/docs/onboarding/QUICKSTART-USER.adoc @@ -1,12 +1,12 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell // Template: QUICKSTART-USER.adoc — 5-minute path to working software -// Replace rsr-template-repo, Rsr Template Repo — See README.adoc for details., just run, Rsr Template Repo started successfully. with actuals -= rsr-template-repo — Quick Start for Users +// Replace occupancy-types, Rsr Template Repo — See README.adoc for details., just run, Rsr Template Repo started successfully. with actuals += occupancy-types — Quick Start for Users :toc: :toclevels: 2 -== What is rsr-template-repo? +== What is occupancy-types? Rsr Template Repo — See README.adoc for details. @@ -38,8 +38,8 @@ Before you begin, ensure you have: [source,bash] ---- # Clone and set up -git clone https://github.com/hyperpolymath/rsr-template-repo.git -cd rsr-template-repo +git clone https://github.com/hyperpolymath/occupancy-types.git +cd occupancy-types just setup ---- @@ -62,7 +62,7 @@ just stapeln-run [source,bash] ---- -just install --portable --prefix=./rsr-template-repo-portable +just install --portable --prefix=./occupancy-types-portable ---- == First Run @@ -103,7 +103,7 @@ just heal * **In-app**: `just run --help` * **Guided tour**: `just tour` * **Report a problem**: `just help-me` (pre-fills diagnostic context) -* **Wiki**: https://github.com/hyperpolymath/rsr-template-repo/wiki +* **Wiki**: https://github.com/hyperpolymath/occupancy-types/wiki == Uninstall diff --git a/docs/onboarding/llm-warmup-dev.adoc b/docs/onboarding/llm-warmup-dev.adoc index f12ada8..9ed21c6 100644 --- a/docs/onboarding/llm-warmup-dev.adoc +++ b/docs/onboarding/llm-warmup-dev.adoc @@ -1,21 +1,87 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -= LLM Warmup — rsr-template-repo (Developer) +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += LLM warm-up — occupancy-types (developer) -== What is rsr-template-repo? +Paste this into an AI assistant before changing code in occupancy-types. -See `README.adoc` for overview. +== Orientation -== Key Commands +occupancy-types (https://github.com/hyperpolymath/occupancy-types): a library in idris2, zig. -* `just setup` — set up development environment -* `just build` — build the project -* `just test` — run tests -* `just doctor` — diagnose issues -* `just heal` — attempt auto-repair +The template's own code is the ABI/FFI seam that every minted repo inherits: -== Quick Context +* `src/interface/Abi/` + `src/interface/abi.ipkg`: Idris2 types and layout proofs + (the ABI owns the contract). +* `src/interface/ffi/`: the Zig implementation (`build.zig`, `src/main.zig`, + `test/integration_test.zig`), exporting C symbols named `_*`. +* `src/interface/generated/`: C headers generated from the ABI. -* License: MPL-2.0 -* Part of hyperpolymath ecosystem -* See `EXPLAINME.adoc` for architecture +Everything else is scaffolding: `.machine_readable/` (descriptiles, contractiles, +policies), `build/just/*.just` (the task phases the root `Justfile` imports), and +`.github/workflows/` (the CI a minted repo starts with). + +== Toolchain + +* Declared in `mise.toml` (specs `latest`), made concrete in `mise.lock`. Never + hand-edit versions; `just toolchain-refresh` bumps and re-locks. +* Guix alternative: `build/manifest.scm` (dev shell), `build/guix.scm` (package), + `build/channels.scm` (pinned Guix). `just dev-shell` picks whichever is present. +* `just` ≥ 1.42. The shared recipes live in `build/just/provision.just` + (`mod provision`); root recipes in `Justfile` override them. + +== The loop + +[source,bash] +---- +just setup # once +just build +just test +just bench # N/A when there are no benchmarks +just eval # test + bench with timings, logged under .eval/ +just lint && just fmt +just doctor # before you open a PR +---- + +Template-specific commands: + +[source,bash] +---- +just repo-init # instantiate: fills every placeholder token, then validates +just validate # RSR shape + metadata + AI-install completeness +just quality # fmt-check, lint, test +just test-all # test, e2e, aspect, bench, readiness +just repo-map # regenerate docs/architecture/REPOSITORY-MAP.adoc (CI checks it) +---- + +The golden path is `just test && just quality`. In the template itself `test` and +`lint` fail on the placeholder tokens in `src/interface/ffi/src/main.zig`; that is +deliberate. Test a change to the FFI in a scratch copy after `repo-init`. + +== Where things are + +* Top level: `archetypes/ benches/ build/ ci/ docs/ examples/ features/ LICENSES/ + scripts/ session/ src/ tests/ verification/ www/`. The root file list is fenced by + `.machine_readable/root-allow.txt`; a new root file needs a line there. +* `build/just/`: provisioning (`provision.just`, `provision-lib.sh`), `repo-init.just`, + `validate.just`, and the other imported phases. +* `build/guix.scm`, `build/manifest.scm`, `build/channels.scm`: the Guix route. +* `tests/`: e2e, aspect, shape and workflow checks. `benches/`: benchmark harness. +* `docs/`: SETUP, AI installation guide, onboarding warm-ups, architecture map. + +== Rules that bite + +* Languages: JavaScript runs Bun > Deno > pnpm > npm from one `package.json`; + after `bun add`, run `js-runtimes regen`. Python, TypeScript, ReScript, Nix, + Go and Makefiles are banned. New code follows the repository's existing languages. +* Every new file carries an SPDX header: `MPL-2.0` for code and config, + `CC-BY-SA-4.0` for prose docs. Never change an existing file's licence. +* Commits are signed; history is linear (squash merges). + +* `.machine_readable/contractiles/Justfile` must stay byte-identical to the root + `Justfile`; copy it after every edit. +* Never put literal placeholder tokens in prose that `repo-init` will process: it + substitutes them. Write "placeholder token" instead. +* `build/` is not an artefact directory here: `just clean` deliberately leaves it, + because `build/just/repo-init.just` lives there. +* Descriptile and policy facts are clauses of the repo deed (`*_chora.deed`), + read with `scripts/deed-field.sh`; A2ML is retired and no `.a2ml` file may exist. diff --git a/docs/onboarding/llm-warmup-maintainer.adoc b/docs/onboarding/llm-warmup-maintainer.adoc new file mode 100644 index 0000000..5403237 --- /dev/null +++ b/docs/onboarding/llm-warmup-maintainer.adoc @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += LLM warm-up — occupancy-types (maintainer) + +Paste this into an AI assistant before release, CI or governance work on +occupancy-types. Read `llm-warmup-dev.adoc` first; this adds what a maintainer owns. + +== The provisioning set + +This repository is provisioned from the estate canon +(https://github.com/hyperpolymath/standards, `3-practice/provisioning/`). +Repo-specific facts live in `.machine_readable/descriptiles/provisioning_praxis.deed`. +`provision-set realign` overwrites only the engine: `launcher.sh`, +`build/just/provision*.{just,sh}` and `channels.scm`; change behaviour through the +deed, never by hand-editing those. Everything else (`guix.scm` once real, +`manifest.scm`, `mise.toml`, the Justfile, `docs/` and these warm-ups) is minted +once and then belongs to this repository: keep it true as the code changes. + +== Keeping it current + +* `just toolchain-refresh` — `mise up --bump`, `mise lock`, re-pin `channels.scm`. + Commit the lock changes on their own. +* `just doctor` must be clean before a release; WARN codes are tracked, not ignored. +* The `provisioning-check` workflow reports drift from the canon. + +== Release + +A release is a signed tag `v` on `main`; pushing it runs `.github/workflows/release.yml`, which builds the release notes, publishes the GitHub release (`softprops/action-gh-release`) and attests build provenance. Bump `version` in the Justfile and `CITATION.cff` first, in the same PR. This repository is a GitHub *template*: a release changes what every new repository is born with, so re-run `./launcher.sh --doctor` and `bash build/just/provision-check.sh .` on a fresh clone before tagging. + +== CI and branch rules + +`main` is protected by a ruleset with these rule types: `required_signatures`, `required_linear_history`, `non_fast_forward`, `deletion`, `code_scanning`, `code_coverage` and `copilot_code_review` (read 2026-09-30; re-read with `gh api repos/hyperpolymath/rsr-template-repo/rules/branches/main`). Only squash merges are allowed and auto-merge is on. Every commit must be SSH-signed or the PR cannot land. The workflows under `.github/workflows/` are themselves the estate's reference copies: a change here propagates to every repository minted from the template, so change the canonical reusable in `hyperpolymath/standards` first and pin to it here. + +Rulesets can block a merge for reasons other than checks (review threads, +signatures, code owners): enumerate the ruleset's rule types before calling a PR +mergeable. + +== Decisions + +Owner rulings for the estate are recorded in `hyperpolymath/standards` +(issue #787 is the decision surface). Do not re-open a ruled question; cite it. +Rulings that bind this repository in particular: bun is the only JavaScript runtime (deno, TypeScript, ReScript, Python are banned), A2ML is retired (no `.a2ml` file may exist; descriptive and policy facts are clauses of the repo deed, `*_chora.deed`), and Justfile changes are mirrored in `.machine_readable/contractiles/Justfile`, which is kept byte-identical. diff --git a/docs/onboarding/llm-warmup-user.adoc b/docs/onboarding/llm-warmup-user.adoc index 2127530..ac47004 100644 --- a/docs/onboarding/llm-warmup-user.adoc +++ b/docs/onboarding/llm-warmup-user.adoc @@ -1,21 +1,55 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -= LLM Warmup — rsr-template-repo (User) +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += LLM warm-up — occupancy-types (user) -== What is rsr-template-repo? +Paste this whole file into any AI assistant before asking it about +occupancy-types. It tells the assistant what the project is, how a person gets it +running, and where to look when something goes wrong. -See `README.adoc` for overview. +== What it is -== Key Commands +occupancy-types (https://github.com/hyperpolymath/occupancy-types) is a library written in idris2, zig. -* `just setup` — set up development environment -* `just build` — build the project -* `just test` — run tests -* `just doctor` — diagnose issues -* `just heal` — attempt auto-repair +It is not a project in its own right: it is the template every new hyperpolymath +repository is created from. It ships CI/CD, machine-readable metadata +(`.machine_readable/descriptiles/`), an AI gatekeeper deed +(`occupancy-types_chora.deed`), a typed ABI/FFI seam (Idris2 over Zig), Guix and +mise toolchains, and governance files, all passing the RSR validators before you +add a line of your own. -== Quick Context +== Getting it running -* License: MPL-2.0 -* Part of hyperpolymath ecosystem -* See `EXPLAINME.adoc` for architecture +The supported routes, in order of ease: + +. *Ask an AI*: follow `docs/AI_INSTALLATION_GUIDE.adoc` in the repository. +. *The launcher*: `git clone https://github.com/hyperpolymath/occupancy-types.git && cd occupancy-types && ./launcher.sh --setup` +. *By hand*: `docs/SETUP.adoc`, every step explained. + +Prerequisites are git, a C toolchain and mise; everything else is installed by +`mise install`, at the versions pinned in `mise.lock`. + +== Using it + +A person uses it to start a new repository: + +. On GitHub, *Use this template* (or clone it). +. In the new checkout, run `just repo-init`. It asks for the project name, owner, + author and licence contact (listed in `.machine_readable/ai/PLACEHOLDERS.adoc`), + replaces every placeholder token, then validates the result. `just repo-init + julia-library` applies the one archetype overlay shipped in `archetypes/`. +. Run `just validate` (structure + metadata) and `just doctor` (toolchain). + +An AI running `repo-init` sets `RSR_NON_INTERACTIVE=1` and passes the answers as +environment variables (see `docs/AI_INSTALLATION_GUIDE.adoc`), or pipes them to stdin. + +== When something is wrong + +Run `just doctor`. Every FAIL line carries a code (PV-E…) that is explained, with +its fix, in `docs/SETUP.adoc` §Troubleshooting. `just heal` applies the automatic +fixes. Do not guess an installer that the repository does not document. + +== What it touches + +Nothing leaves the machine. `mise install` and `just setup` install tools per-user +under the mise data directory; `just repo-init` rewrites files only inside the +checkout. No remote repository is created and nothing is pushed until you do it. diff --git a/docs/practice/AI-CONVENTIONS.adoc b/docs/practice/AI-CONVENTIONS.adoc index d960221..d087446 100644 --- a/docs/practice/AI-CONVENTIONS.adoc +++ b/docs/practice/AI-CONVENTIONS.adoc @@ -2,7 +2,7 @@ // Copyright (c) Jonathan D.A. Jewell = AI Conventions // SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) == AI Conventions (Authoritative Source) @@ -12,12 +12,12 @@ Per-tool config files (.cursorrules, .clinerules, etc.) reference this document. === Session Startup 1. Read the repo deed (`*_chora.deed` at root) FIRST (mandatory gatekeeper). -2. Read `.machine_readable/descriptiles/STATE.a2ml` for current status and blockers. -3. Read `.machine_readable/descriptiles/anchors/ANCHOR.a2ml` for canonical authority boundaries. -4. Read `.machine_readable/policies/MAINTENANCE-AXES.a2ml` for maintenance/audit sequencing. -5. Read `.machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml` for baseline controls. -6. Read `.machine_readable/policies/SOFTWARE-DEVELOPMENT-APPROACH.a2ml` for execution order. -7. Read `.machine_readable/descriptiles/AGENTIC.a2ml` for agent constraints. +2. Read `docs/status/ROADMAP.adoc` for blockers and next actions (phase and maturity are in the deed's `(status)` and `(maturity)` clauses). +3. Read the deed's `(anchor)` clause for canonical authority boundaries. +4. Read the deed's `(policies (maintenance-axes))` clause for maintenance/audit sequencing. +5. Read the deed's `(policies (maintenance-checklist))` clause for baseline controls. +6. Read the deed's `(policies (software-development-approach))` clause for execution order. +7. Read the deed's `(agentic)` clause for agent constraints. === License @@ -38,16 +38,17 @@ Per estate policy (hyperpolymath/standards `LICENCE-POLICY.adoc` Rule 1 + Addend === Author Attribution -- Name: **{{AUTHOR}}** -- Email: **{{AUTHOR_EMAIL}}** -- Copyright: `Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}>` +- Name: **Jonathan D.A. Jewell** +- Email: **j.d.a.jewell@open.ac.uk** +- Copyright: `Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) ` === State Files -State/metadata files, anchors, and policies (.a2ml) belong in `.machine_readable/` ONLY. -NEVER create STATE.a2ml, META.a2ml, ECOSYSTEM.a2ml, AGENTIC.a2ml, -NEUROSYM.a2ml, PLAYBOOK.a2ml, ANCHOR.a2ml, MAINTENANCE-AXES.a2ml, -MAINTENANCE-CHECKLIST.a2ml, or SOFTWARE-DEVELOPMENT-APPROACH.a2ml in the repository root. +No `.a2ml` file may exist anywhere in this repository: the format is retired. +State, metadata, anchor and policy facts are clauses of the repo deed +(`*_chora.deed`) — `(status)`, `(maturity)`, `(meta)`, `(anchor)`, +`(neurosym)`, `(playbook)`, `(policies …)` — and the journal is +`docs/status/ROADMAP.adoc`. Never recreate them as separate files. === Banned Patterns @@ -94,9 +95,10 @@ Use `just` (Justfile) for all build, test, lint, and format tasks. === References - `*_chora.deed` (repo deed) -- universal AI entry point -- `.machine_readable/descriptiles/AGENTIC.a2ml` -- agent permissions and constraints -- `.machine_readable/descriptiles/STATE.a2ml` -- current project state -- `.machine_readable/descriptiles/anchors/ANCHOR.a2ml` -- canonical authority and policy boundary -- `.machine_readable/policies/MAINTENANCE-AXES.a2ml` -- canonical axis sequencing and audit requirements -- `.machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml` -- baseline maintenance checklist policy -- `.machine_readable/policies/SOFTWARE-DEVELOPMENT-APPROACH.a2ml` -- axis execution approach policy +- `*_chora.deed` `(agentic)` -- agent permissions and constraints +- `*_chora.deed` `(status)` / `(maturity)` -- current phase and maturity +- `docs/status/ROADMAP.adoc` -- milestones, blockers, next actions +- `*_chora.deed` `(anchor)` -- canonical authority and policy boundary +- `*_chora.deed` `(policies (maintenance-axes))` -- canonical axis sequencing and audit requirements +- `*_chora.deed` `(policies (maintenance-checklist))` -- baseline maintenance checklist policy +- `*_chora.deed` `(policies (software-development-approach))` -- axis execution approach policy diff --git a/docs/practice/STATE-VISUALIZER-GUIDE.adoc b/docs/practice/STATE-VISUALIZER-GUIDE.adoc index 48dda5f..fcc60a5 100644 --- a/docs/practice/STATE-VISUALIZER-GUIDE.adoc +++ b/docs/practice/STATE-VISUALIZER-GUIDE.adoc @@ -1,7 +1,7 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell = TOPOLOGY.md — Generation Guide -{{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +Jonathan D.A. Jewell (hyperpolymath) :toc: :sectnums: @@ -62,7 +62,7 @@ The file must contain exactly three sections: Add a header comment with SPDX-License-Identifier and Last updated date. End with an "Update Protocol" section explaining how to maintain the file. -Use the template at TOPOLOGY.md in rsr-template-repo as a structural reference, +Use the template at TOPOLOGY.md in occupancy-types as a structural reference, but make the content completely specific to THIS project. ---- @@ -70,7 +70,7 @@ but make the content completely specific to THIS project. [source,bash] ---- -cp /path/to/rsr-template-repo/TOPOLOGY.md ./TOPOLOGY.md +cp /path/to/occupancy-types/TOPOLOGY.md ./TOPOLOGY.md # Then edit: replace placeholders, draw the real architecture, fill the dashboard ---- @@ -143,14 +143,15 @@ Always 10 characters wide. Use full blocks only (no half-blocks). TOPOLOGY.md complements but does not replace: -- **STATE.a2ml** — machine-readable state (tasks, blockers, next actions) -- **ECOSYSTEM.a2ml** — position in the wider project ecosystem -- **META.a2ml** — architecture decisions and design rationale +- **docs/status/ROADMAP.adoc** — milestones, blockers, next actions +- **Repo deed `(status)` / `(maturity)`** — project phase and maturity +- **Repo deed `(ecosystem)`** — position in the wider project ecosystem +- **Repo deed `(meta)` and `docs/decisions/`** — architecture axes and decisions - ***_chora.deed (repo deed)** — AI agent entry point: allocation policy and directory invariants -TOPOLOGY.md is the _visual summary_ for humans; the a2ml files are the +TOPOLOGY.md is the _visual summary_ for humans; the deed and roadmap are the _structured data_ for tooling. Both should agree. == Copyright -Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> +Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) diff --git a/docs/practice/ci-cost-reduction.adoc b/docs/practice/ci-cost-reduction.adoc index 3f5e50c..beb2d29 100644 --- a/docs/practice/ci-cost-reduction.adoc +++ b/docs/practice/ci-cost-reduction.adoc @@ -199,7 +199,7 @@ Cap every job so a hung runner doesn't burn the budget: ---- jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 20 # fail-fast instead of 360 default ---- @@ -214,8 +214,8 @@ Recommendations: === 7. Reusable workflow (longer-term) Ship the above patterns in a single reusable workflow under -`rsr-template-repo/.github/workflows/rsr-ci-defaults.yml`, and adopt -it estate-wide via `uses: hyperpolymath/rsr-template-repo/.github/workflows/rsr-ci-defaults.yml@main` +`occupancy-types/.github/workflows/rsr-ci-defaults.yml`, and adopt +it estate-wide via `uses: hyperpolymath/occupancy-types/.github/workflows/rsr-ci-defaults.yml@main` in downstream repos. Single PR propagates improvements. === 8. Self-hosted runner for heavy work (long-term) @@ -274,5 +274,5 @@ frequency × job count). incident that motivated this spec. * `007-lang/audits/audit-dependabot-automation-gap-2026-04-17.md` — separate defect stack also addressed in the same session. -* `rsr-template-repo/.github/workflows/dependabot-automerge.yml` — +* `occupancy-types/.github/workflows/dependabot-automerge.yml` — canonical pattern for auto-merge pattern referenced above. diff --git a/docs/proposals/root-cleanup.adoc b/docs/proposals/root-cleanup.adoc index 84548e3..891cb17 100644 --- a/docs/proposals/root-cleanup.adoc +++ b/docs/proposals/root-cleanup.adoc @@ -37,7 +37,8 @@ one-shot relocation PR per project once the template lands. The full enumeration lives in `.machine_readable/root-allow.txt`. Summary: * *Authority files (root):* `README.adoc`, `AUDIT.adoc`, `EXPLAINME.adoc`, - `0-AI-MANIFEST.a2ml` (thin pointer), `LICENSE`, `CHANGELOG.md`. + `_chora.deed` (the repo deed, which is the AI manifest), `LICENSE`, + `CHANGELOG.md`. * *Build entry points (root):* `Justfile`, `coordination.k9`. * *Tool-required dotfiles (root):* `.editorconfig`, `.envrc`, `.gitattributes`, `.gitignore`, `.tool-versions`. @@ -161,19 +162,10 @@ across `Justfile`, `scripts/`, `.machine_readable/`). === AI manifest deduplication -`.machine_readable/0.1-AI-MANIFEST.a2ml` is the canonical AI manifest in both -the template and downstream repos (e.g. `the-nash-equilibrium` README line -244). The root `0-AI-MANIFEST.a2ml` becomes a thin pointer: - -[source,a2ml] ----- -# 0-AI-MANIFEST.a2ml — pointer file -authority = ".machine_readable/0.1-AI-MANIFEST.a2ml" -note = "AI agents MUST read the canonical manifest at the path above." ----- - -Once the canonical version is stable, decide whether to keep this pointer at -root or delete it entirely. +Resolved: the repo deed (`_chora.deed` at the root) is the single AI +manifest. The family-7 allocation manifest folded into it (standards#837 +pilot), and no separate manifest or pointer file remains: no `.a2ml` file +may exist in the template or its downstream repos (rsr-template-repo#209). === Template-only relocations diff --git a/docs/reports/audit/pillar-audit-2026-04-15.adoc b/docs/reports/audit/pillar-audit-2026-04-15.adoc index a99e61f..17b41e6 100644 --- a/docs/reports/audit/pillar-audit-2026-04-15.adoc +++ b/docs/reports/audit/pillar-audit-2026-04-15.adoc @@ -3,8 +3,10 @@ = Gemini Audit Report (M2: Pillar Repo Audits) :date: 2026-04-15 +NOTE: Superseded in part (2026-10-06, rsr-template-repo#209): the `.a2ml` files named below no longer exist; their facts are clauses of the repo deed (`*_chora.deed`). + Date: 2026-04-15 + -Repository: `/var/mnt/eclipse/repos/rsr-template-repo` +Repository: `/var/mnt/eclipse/repos/occupancy-types` == Audit Criteria diff --git a/docs/reports/compliance/RSR-TEMPLATE-DAMAGE-2026-10-30.adoc b/docs/reports/compliance/RSR-TEMPLATE-DAMAGE-2026-10-30.adoc new file mode 100644 index 0000000..463d23a --- /dev/null +++ b/docs/reports/compliance/RSR-TEMPLATE-DAMAGE-2026-10-30.adoc @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Template damage: what occupancy-types shipped, why it survived, and what now stops it +:toc: +:toclevels: 3 +:date: 2026-10-30 + +*Status*: remediation landed in this repository. Estate remediation is inventoried, +not performed — see <>. + +== Summary + +`JuliaPackageSpitter.jl` was reported as a broken Julia package: a `mise.toml` +declaring Python, Node, Go, Rust, Java, Bun and Cargo for a Julia library and no +Julia; a `Containerfile` built from two floating `:latest` stages and otherwise +consisting of TODO comments; `occupancy-types` and a `tier` token sitting in +committed files; an SPDX gate that passed without reading a single file; a +`Justfile` whose `fmt`, `fmt-check` and `lint` recipes are `@echo` statements, so +`just quality` printed "All quality checks passed!" having run only `Pkg.test()`. + +None of that was authored in that repository. It was authored here, and copied +into every repository minted while it was true. Fixing it in the downstream +repository fixes one repository; fixing it here is what stops the next mint. + +== The template, and how it was found + +[cols="1,3"] +|=== +| Search | Result + +| `gh search repos rsr-template --owner hyperpolymath --json fullName,description` +| `hyperpolymath/occupancy-types` — the only match. There is no separate + Julia template repository: `rsr-julia-library-template-repo` and its variants + return 404 (probed), so `JuliaPackageSpitter.jl` came from this template plus + the `julia-library` archetype. + +| `gh api -X GET search/code -f q='org:hyperpolymath "Juliapackage Reuse Audit"'` +| 13 files. (`gh search code` has no `--owner` flag; the `gh api search/code` + form is the one that works.) +|=== + +Provenance of the specific files, from this repository's own history: + +* `mise.toml` — introduced at `05fc319a`, corrected here at `cf15de59`. The + correction reached this repository only. Repositories minted between those + dates have the nine-runtime file, and nothing ever went back for them. +* `build/container/Containerfile` — introduced at `dbc4d67f`, corrected here at + `adb66f51`, with the same one-way reach. +* Everything else (unfilled tokens, the `@echo` quality verbs, the vacuous SPDX + glob) was never corrected at all — it was still true in this tree when this + investigation started. + +== The defects, and why each survived + +=== 1. The SPDX gate could not fail on a non-Rust repository + +`Mustfile.a2ml`'s `spdx-headers` check globbed `*.rs`, `*.res` and `*.gleam` and +capped the scan with `head -20`. In a Julia repository it matched zero files, +counted zero missing headers, and printed PASS. In a Rust repository with more +than 20 source files it graded the first 20. + +*A gate that examined nothing reported coverage.* It survived because it was +green everywhere it was read. + +=== 2. `container-images-pinned` asked the wrong question + +The probe was `grep -q 'cgr.dev\|@sha256:' Containerfile`. Any `cgr.dev` image +satisfied it, including `FROM cgr.dev/chainguard/wolfi-base:latest`, which is +what the template shipped. The check asked "is an approved registry named" while +reporting "images are pinned". + +=== 3. The mint never rendered the toolchain + +`mise.toml` was copied from the spine as-is. The spine's copy was one file for +every language, so every minted repository got the union of them all. This +repository eventually repaired its own copy by hand; the archetype never carried +a Julia `mise.toml` at all, so mints kept producing the union. + +=== 4. Nothing checked that `mise.toml` matched the languages present + +The estate had no gate for "the toolchain this repository declares is the +toolchain its languages need". Consequently the union file was not a red build +anywhere, in any repository, for two months. + +=== 5. Tokens survived because the checker skipped the files they survived in + +The placeholder gate excludes `Justfile` and `*.just` (they contain just's own +`{{...}}` interpolation), and the mint's token list is a fixed set. Between the +two, `Justfile project := "occupancy-types"`, `tier := "…"` (the `tier` brace token), +`.envrc`, `.reuse/dep5`, `Containerfile`, `devcontainer.json` and +`.devcontainer/Containerfile` kept their braces all the way into commits. + +Downstream, `JuliaPackageSpitter.jl` still carries *75* unfilled +token references; the 111-repository survey below found them in 105 of 111. + +=== 6. `just quality` echoed over verbs that ran nothing + +The provisioning engine prints `info fmt-check: N/A for julia (archetype: +library) — nothing to run…` and exits 0. That is honest. The `quality` recipe +then printed `All quality checks passed!`. A repo with no formatter, no linter +and no formatter-check reported "all quality checks passed". + +=== 7. The archetype could not remove what the preset does not declare + +The spine is polyglot on purpose, and an overlay can add files but not subtract +them. A Julia library that ships no image therefore inherited +`build/container/Containerfile` and failed `container-images-pinned` — or, before +this PR, passed it vacuously. + +== What changed here + +[cols="2,3"] +|=== +| File | Change + +| `scripts/check-spdx-headers.sh` | New. Derives the extension set from the + languages the repository declares (via `provision-lib.sh langs`), scans every + matched file with no cap, and fails when a declared language has no files or + when it matched nothing at all. Names banned-language source it finds. + +| `scripts/check-mise-language.sh` | New. Reads the languages and expected tools + from the provisioning engine, then requires that every declared tool is needed + and every needed tool is declared. Banned tools are findings wherever they + appear. `--detect` audits repositories minted before the engine shipped with + them. + +| `scripts/check-container-pins.sh` | New. Every `FROM` must carry a 64-hex + `@sha256:` digest or be `scratch`/a stage name. A pin behind `ARG` is a + finding, because the digest is then decided at build time. + +| `scripts/check-mint-tokens.sh` | New. Reads the answerable tokens *out of the + mint recipe* (not a copy) and fails on any brace token that is not in it. + Lowercase tokens included. In a minted tree with no recipe, every brace token + is a leak. + +| `scripts/render-mise.sh` | New, and now a mint step. Renders `mise.toml` from + the detected languages plus the recipe tools. Idempotent; `--check` reports + drift. + +| `scripts/pin-container-digests.sh` | New. Rewrites `FROM` lines to digest + pins. Refuses to run with a placeholder or malformed digest. + +| `build/container/Containerfile` | **Deleted.** It had two `FROM …:latest` + lines and a body of TODO comments — it could not build, and it failed + `container-images-pinned` in every repository that carried it, including the + ones that publish no image. Pinning it was not possible from here (no registry + access) and would have left a file that still builds nothing. "Pinned or not + at all" resolves to *not at all*: a repository that publishes an image adds + the file and pins it. `build/container/README.adoc` and `just container-build` + say so, and `just container-build` now explains this instead of exiting with + "No Containerfile found". + +| `scripts/prune-archetype-modules.sh` | New, and now a mint step. Deletes the + paths an archetype's `prune.txt` lists, refusing to continue on a path that + does not exist. + +| `scripts/survey-template-damage.sh` | New. Runs these gates against real + repositories (shallow clone each) to inventory the damage. + +| `build/just/repo-init.just` | Token gate before the first write; overlay files + recorded so only *their* `.in` names are resolved; prune step; + `render-mise.sh`; hard gate loop before handoff. + +| `archetypes/julia-library/` | `overlay/scripts/julia-quality.jl` (fmt, + fmt-check, lint in a private tools env — Aqua and JuliaFormatter must not enter + the package's own `[deps]`, where Aqua's own `deps_compat` check would fail + them), the `provisioning_praxis.deed` overrides that point the verbs at it, and + `prune.txt` removing `build/container/`. + +| `archetypes/rust-cli/` | New archetype: `rust-cli_praxis.deed`, + `Cargo.toml.in`, `src/{lib,main}.rs.in`, `tests/cli.rs.in`, `prune.txt`. + +| `Justfile` | `quality` runs the verbs, fails on a non-zero exit, and fails when + a verb reports `N/A for` every detected language. N/A is not a pass. + +| `.machine_readable/contractiles/must/must.k9.ncl` | `spdx-headers` now runs the + script instead of globbing three extensions; new `toolchain-matches-language` + invariant. + +| `.machine_readable/contractiles/trust/trust.k9.ncl` | `container-images-pinned` + now runs the script; severity raised to `critical`. + +| `scripts/validate-template.sh` | Phase 6 delegates to `check-spdx-headers.sh`; + the dead fixed-glob implementation is removed. + +| `scripts/check-no-placeholders.sh` | Fixed a silent failure: a repository with + no `origin` remote (a fresh `git init`, exactly what a mint produces before the + first push) made `git config` exit 1, which under `set -e` + `pipefail` killed + the gate with exit 1 and no output. + +| `.github/workflows/quality.yml` | New `mint-gates` job: the four gates plus + both test suites. No new action — `actions/checkout` at its existing pin. + +| `LICENSE` | Added — the MPL-2.0 text, identical to `LICENSES/MPL-2.0.txt`. + There was no file named `LICENSE`, so `validate-template.sh` failed Phase 1, + the Mustfile's `license-present` failed, and every minted repository's + `link:LICENSE[LICENSE]` was a dead link. (REUSE keeps `LICENSES/` authoritative; + the root copy exists because forges and readers look for it.) + +| `scripts/validate-template.sh` | Two more repairs: `SCRIPT_DIR` was referenced + but never set, so `set -u` aborted the run at Phase 6 and the last two phases + never ran; and the required-file check demanded `CONTRIBUTING.md` in a + repository whose `CONTRIBUTING.adoc` has been there all along. + +| `.machine_readable/ai/PLACEHOLDERS.adoc` | The Project Identity section now + separates the human name, the compiler identifier (upper and lower) and the + forge slug, and says they are four tokens filled from four answers. +|=== + +== Proof that the gates can fail + +`tests/workflows/mint_gates_test.sh` — 30 controls, each pair a positive and a +negative fixture: + +---- +$ bash tests/workflows/mint_gates_test.sh +── SPDX (language-derived) ──────────────────────────────── + PASS SPDX passes on the template tree + PASS SPDX fails on a Julia file with no header + PASS SPDX fails when a declared language has no files + PASS SPDX fails when it matches zero files + PASS SPDX fails on banned-language source (.py) + PASS SPDX passes on a well-formed Julia fixture +── Container image pins ─────────────────────────────────── + PASS pin gate fails on a floating :latest + PASS pin gate fails on a tag without a digest + PASS pin gate passes on a digest-pinned image + PASS pin gate fails when the pin lives in a build argument + PASS pin gate fails on a truncated digest + PASS pin gate allows a stage name and scratch + PASS pin gate fails on a Dockerfile + PASS pin gate passes when no Containerfile ships + PASS pinner refuses without a digest + PASS pinner rewrites both FROM lines + PASS pin gate passes after the pinner ran + PASS pinner removed the floating tag +── mise.toml matches the declared languages ─────────────── + PASS mise gate passes when the toolchain matches + PASS mise gate fails on a banned tool + PASS mise gate fails when the language tool is missing + PASS mise gate fails on a tool no language needs + PASS mise gate fails on an empty [tools] table + PASS mise gate fails when mise.toml is missing + PASS mise gate passes on the template tree +── Mint tokens ──────────────────────────────────────────── + PASS token gate passes on the template + PASS token gate ignores just's own interpolation + PASS token gate fails on a token with no answer +── Archetype prune ──────────────────────────────────────── + PASS prune script runs + PASS prune removed the listed path +mint gates: 30/30 controls hold +---- + +Two of these controls exist because the gates themselves were written badly the +first time and the tests caught it: + +* `check-spdx-headers.sh` read `git ls-files` paths relative to `--root` as if + they were relative to the caller's directory. On a fixture that shared a path + with the template it graded *the template's file*. Found when the Julia + scaffold reported four files missing that plainly carried headers. +* `check-mint-tokens.sh` piped answers into `grep -q`. `grep -q` exits on its + first match, the writer takes SIGPIPE, `pipefail` reports 141, and a *match* + became a non-match — a different one or two tokens reported unanswerable on + every run of an unchanged tree. Here-strings now. + +== A Julia package and a Rust CLI, minted and gated + +`tests/e2e/language_profiles_mint_test.sh` builds both from this tree and the +archetypes, using the recipe's own overlay/prune/rename ordering and its own +answer list, then runs the gates: + +---- +$ bash tests/e2e/language_profiles_mint_test.sh +── Julia scaffold ───────────────────────────────────────── + PASS Julia: Project.toml has its final name + PASS Julia: package name rendered from the archetype token + PASS Julia: module file named after the package + PASS Julia: build/container/ pruned (no unpinnable Containerfile ships) + PASS Julia: mise declares julia + PASS Julia: mise declares no banned or foreign tool + PASS Julia: quality verbs wired (scripts/julia-quality.jl) + PASS Julia: fmt/fmt-check/lint overrides shipped in the provisioning deed +── Rust scaffold ────────────────────────────────────────── + PASS Rust: Cargo.toml has its final name + PASS Rust: sources rendered from the overlay's .in files + PASS Rust: crate name is the repo slug + PASS Rust: mise declares rust + PASS Rust: mise declares no banned tool + PASS Rust: build/container/ pruned +── Gates on both scaffolds ──────────────────────────────── + PASS DemoPackage.jl: SPDX gate passes + PASS DemoPackage.jl: mise/language gate passes + PASS DemoPackage.jl: container pin gate passes + PASS DemoPackage.jl: no unanswered token survives the mint + PASS DemoPackage.jl: placeholder gate passes + PASS demo-rust: SPDX gate passes + PASS demo-rust: mise/language gate passes + PASS demo-rust: container pin gate passes + PASS demo-rust: no unanswered token survives the mint + PASS demo-rust: placeholder gate passes +── Replay the downstream damage ─────────────────────────── + PASS damage replay: the nine-runtime mise.toml is caught + PASS damage replay: a floating Containerfile is caught + PASS damage replay: an unanswered token is caught +language profiles: 27/27 controls hold +---- + +The Julia scaffold's rendered `mise.toml` declares `julia` and `just`, nothing +else. The Rust scaffold's declares `rust` and `just`. Neither declares Python, +Node, Go, Java, Cargo-for-Julia or Julia-for-Rust, and each is generated from the +languages the tree carries rather than from a list in a template. + +== The estate + +=== Method + +`scripts/survey-template-damage.sh` shallow-clones each repository and runs these +four gates against the clone — the same code a minted repository will run, not a +grep. Reproducible: + +---- +gh api -X GET search/code -f q='org:hyperpolymath "occupancy-types"' \ + --jq '[.items[].repository.full_name] | unique | .[]' +scripts/survey-template-damage.sh --tsv owner/name owner/name … +---- + +Code search totals for the whole organisation, at the time of writing (each is a +separate query, so the populations overlap and none is a subset of another): + +[cols="1,1,3"] +|=== +| Query | Files | What it establishes + +| `org:hyperpolymath "occupancy-types"` | 1536 | Unfilled tokens are not one + repository's accident. +| `org:hyperpolymath filename:mise.toml python` | 175 | 175 `mise.toml` files + mention Python. Python is banned estate-wide. +| `org:hyperpolymath filename:Containerfile ":latest"` | 421 | 421 `Containerfile` + files carry a floating tag somewhere. +| `org:hyperpolymath filename:Justfile "occupancy-types"` | 33 | The mint's + own skipped-file class, in 33 `Justfile`s. +|=== + +=== What the survey found + +111 repositories surveyed (the union of the queries above). Result per gate: + +[cols="2,1,3"] +|=== +| Gate | Repositories failing | Reading + +| Unfilled tokens | 105 / 111 | The mint left braces in committed files. +| Banned tool declared in `mise.toml` | 84 / 111 | Includes every + `.jl` repository in the set. +| SPDX headers incomplete | 78 / 111 | The gate that shipped could not + have reported this. +| Floating container base image | 68 / 111 | `cgr.dev/chainguard`:latest. +| *All four* | 55 / 111 | The full template signature. +| Clean | 0 / 111 | — +|=== + +The 55 repositories carrying all four, in full: + +* `InvestigativeJournalism.jl` +* `JuliaKids.jl` +* `JuliaPackageSpitter.jl` +* `aerie` +* `alloyiser` +* `boinc-boinc` +* `bunsenite` +* `cafescripto` +* `cicd-squabbler` +* `cloud-sync-tuner` +* `conative-gating` +* `conflow` +* `cookie-rebound` +* `dafniser` +* `deed-core` +* `deed-ecosystem` +* `developer-ecosystem` +* `echidna` +* `excel-economic-numbers-tool` +* `filesoup` +* `formatrix-docs` +* `fraying-model-computational-testbed` +* `gnpl` +* `gv-clade-index` +* `http-capability-gateway` +* `ideas-to-alphas` +* `idrisiser` +* `iseriser` +* `jtv-lang` +* `julia-ecosystem` +* `k9-ecosystem` +* `kitchenspeak` +* `lithoglyph` +* `lucidscript` +* `lustreiser` +* `natsci-studio` +* `nesy-solver` +* `otpiser` +* `panll` +* `patallm-gallery` +* `patch-bridge` +* `polyglot-i18n` +* `proof-burrower` +* `proven-tests-and-benches` +* `pseudoscript` +* `rattlescript` +* `scaffoldia` +* `session-sentinel` +* `snifs` +* `somethings-fishy` +* `squeakwell` +* `trigger` +* `ubicity` +* `vocarium` +* `wokelang` + +These are the repositories where the four defects coexist, which is the +signature of a single mint from the damaged template. The remaining 56 +have one or more of them, usually the token leak alone. + +Examples of individual rows, verbatim from the survey: + +---- +JuliaPackageSpitter.jl mise: banned tool(s) declared: black go isort java jest make node npm pip + prettier pytest python ruff vitest yarn + spdx: SPDX headers: 26/27 files carry a header + pins: Containerfile: floating base image 'cgr.dev/chainguard/wolfi-base:latest' + tokens: 75 unfilled token(s) in a tree that has no generator left +---- + +== Remediation + +The template is fixed here; the 111 repositories are not. Each needs a +per-repository change, and the shape of it is now mechanical: + +. Copy `scripts/render-mise.sh` and `build/just/provision-lib.sh` into the repository from the same revision of `hyperpolymath/occupancy-types`. Older mints do not contain `scripts/render-mise.sh`, and the renderer exits with an error when `build/just/provision-lib.sh` is missing. +. Re-render the toolchain: `scripts/render-mise.sh --root .` in the repository, + then delete the `[alias]` block (it routes `build`/`test`/`lint` to + cargo/npm/go/ruff/prettier/black, which is how a Julia package came to have a + `lint` that ran `ruff`). +. Pin or delete the `Containerfile`. A repository that builds no image should not + ship one; `scripts/pin-container-digests.sh` is for the ones that do. +. Fill the tokens from `PLACEHOLDERS.adoc`, or delete the lines that need them. + `scripts/check-mint-tokens.sh` lists every one. +. Add the SPDX headers `scripts/check-spdx-headers.sh` names. +. Replace the `@echo` `fmt`/`fmt-check`/`lint` recipes with real ones — for a + Julia package, `archetypes/julia-library/overlay/scripts/julia-quality.jl` is + the file to copy. + +That is a migration, not a merge request, and it is deliberately not attempted +here: a template PR that also rewrites 111 repositories is a PR nobody can +review. + +== What this PR does not prove + +* `just repo-init` was not run end to end. The recipe needs `rustc` (its mint + tooling is single-file Rust compiled on demand), `nickel` (the arrival pack) + and `uuidgen`; this environment has none of the three and installing them from + the package indexes available would have meant an installer outside the + estate's provenance. What was verified instead: the recipe body parses as bash + (`just --show repo-init | bash -n`), and `tests/e2e/language_profiles_mint_test.sh` + reproduces its overlay → prune → substitute → rename → render ordering and runs + the gates on the result. The remaining risk is in the parts of the recipe this + test does not touch, and the CI job that runs the real mint + (`tests/e2e/use_template_mint_test.sh`) is the place that closes it. +* The contractile probes changed here (`must.k9.ncl`, `trust.k9.ncl`) were not + executed: they need `nickel`/`contractile`. The scripts they now call are + tested; the plumbing from the contractile to the script is not. +* Nothing was pushed to any of the 111 repositories, and no issue was opened + against them. + +== Reproducing this report + +---- +bash scripts/check-spdx-headers.sh # the template: 174/174 +bash scripts/check-mise-language.sh # the template: consistent +bash scripts/check-container-pins.sh # the template: pass +bash scripts/check-mint-tokens.sh # 51 answered, none leaked +bash tests/workflows/mint_gates_test.sh # 30/30 +bash tests/e2e/language_profiles_mint_test.sh # 27/27 +bash scripts/survey-template-damage.sh --tsv owner/name … +---- diff --git a/docs/status/PROOF-NEEDS.adoc b/docs/status/PROOF-NEEDS.adoc index aabe3c2..31756ed 100644 --- a/docs/status/PROOF-NEEDS.adoc +++ b/docs/status/PROOF-NEEDS.adoc @@ -2,7 +2,7 @@ // Copyright (c) Jonathan D.A. Jewell // Template: rsr-template-repo/docs/status/PROOF-NEEDS.adoc // Authoritative master list: ~/Desktop/PROOF-REQUIREMENTS-MASTER.adoc -= Proof Requirements — {{PROJECT}} += Proof Requirements — OCCUPANCY_TYPES == Proof Tier @@ -26,7 +26,9 @@ == Mandatory Proofs (All RSR Repos) -These proofs come from the rsr-template-repo and MUST be present in every repo: +These proofs are inherited from the RSR template spine and MUST be present in every repo. +(Name no repository here: the mint's self-name pass rewrites the template's +name into the child's, which once made this line credit the child.) === ABI/FFI Boundary Proofs (Idris2) @@ -48,7 +50,7 @@ These proofs come from the rsr-template-repo and MUST be present in every repo: | # | Proof | Status | File | TP-1 | Core data type well-formedness | Needed | `verification/proofs/idris2/Types.idr` -| TP-2 | Public API type safety (exported functions) | Needed | `verification/proofs/lean4/ApiTypes.lean` +| TP-2 | Public API type safety (exported functions). PROOF-POLICY §1 assigns APIs to Idris2; the Lean file is the shipped example until a project ports it | Needed | `verification/proofs/lean4/ApiTypes.lean` |=== == Project-Specific Proofs @@ -85,18 +87,24 @@ CI will reject any PR introducing these patterns (enforced by `panic-attack assa == Prover Selection Guide +The mapping is set by `3-practice/PROOF-POLICY.adoc` §1 in +`metadatastician/standards`, not by this table: where they differ, the policy +wins. `verification/proofs/README.adoc` maps it onto this tree. + [cols="2,2,3", options="header"] |=== -| Use Case | Recommended Prover | Why - -| ABI/FFI boundaries | *Idris2* | Dependent types model layouts precisely -| Type system proofs | *Coq* or *Lean4* | Mature proof assistants for metatheory -| Algebraic properties | *Lean4* | Good mathlib support -| Inductive/coinductive | *Agda* | Native support for (co)induction -| Distributed systems | *TLA+* | Model checking for protocols -| Numerical properties | *Isabelle* | Strong real analysis library +| Code or obligation | Prover | Notes + +| Theory (type systems, lattices, metatheory, algebra) | *Agda*, rechecked in *Rocq* and *Lean* | Canonical invocation `agda --safe --without-K --cubical-compatible` (§2.1). The Rocq leg is Dedukti transport first, hand re-proof as the fallback; the Lean leg is re-proved until a released Lambdapi Lean exporter exists (§2). *No Mathlib*: a missing lemma is proved in Lean core. +| APIs, ABIs, engineering properties | *Idris2* | `%default total`; no `believe_me` or `assert_total`. +| Rust code | *Creusot* with *Pearlite* contracts, and *Verus* | On the code. Kani for `unsafe` and FFI seams, and Aeneas where Rust implements an Agda theorem, alongside and never instead (§1.2, §4.1). +| Ada code | *SPARK* | GNATprove. +| Concurrency protocols | *TLA+* (a §4 exception) | A finite model check is `tested-only`, with its bounds, not `proven`. |=== +Grades are §8 Proof Witness Grades (`PWG-3` … `PWG-1`), recorded per claim in +`PROOF-STATUS.adoc` and derived from receipts. + == Proof File Locations ---- @@ -104,11 +112,11 @@ verification/proofs/ ├── idris2/ # Idris2 proofs (ABI, dependent types) │ ├── ABI/ # ABI-specific proofs │ └── *.idr # Project-specific Idris2 proofs -├── lean4/ # Lean4 proofs (algebra, lattices) +├── lean4/ # Lean recheck legs of Agda theory (core only, no Mathlib) │ └── *.lean -├── agda/ # Agda proofs (induction, metatheory) +├── agda/ # Agda theory proofs (the source of truth, §2) │ └── *.agda -├── coq/ # Coq proofs (type systems, compilation) +├── coq/ # Rocq recheck legs of Agda theory │ └── *.v └── tlaplus/ # TLA+ specs (distributed protocols) └── *.tla diff --git a/docs/status/PROOF-STATUS.adoc b/docs/status/PROOF-STATUS.adoc index aca702c..7572c76 100644 --- a/docs/status/PROOF-STATUS.adoc +++ b/docs/status/PROOF-STATUS.adoc @@ -1,101 +1,69 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell // Template: rsr-template-repo/docs/status/PROOF-STATUS.adoc -// Tracks proof completion. Requirements defined in PROOF-NEEDS.adoc -= Proof Status — {{PROJECT}} - -== Summary - -[cols="2,1,1,1,1,1", options="header"] -|=== -| Category | Total | Done | In Progress | Blocked | Remaining - -| ABI/FFI (ABI) | 5 | 0 | 0 | 0 | 5 -| Typing (TP) | 2 | 0 | 0 | 0 | 2 -| Invariant (INV) | 0 | 0 | 0 | 0 | 0 -| Security (SEC) | 0 | 0 | 0 | 0 | 0 -| Concurrency (CONC) | 0 | 0 | 0 | 0 | 0 -| Algorithm (ALG) | 0 | 0 | 0 | 0 | 0 -| Domain (DOM) | 0 | 0 | 0 | 0 | 0 -| *Total* | *7* | *0* | *0* | *0* | *7* -|=== - -*Overall*: 0% proven - -== Proofs Done - -// Format: -// | ID | Proof | Prover | File | Date | Verified By | -// | ABI-1 | Non-null pointer proofs | Idris2 | verification/proofs/idris2/ABI/Pointers.idr | 2026-XX-XX | idris2 --check | - -[cols="1,3,1,3,1,2", options="header"] -|=== -| ID | Proof | Prover | File | Date | Verified By - -| — | No proofs completed yet | — | — | — | — -|=== - -== Proofs In Progress - -[cols="1,3,1,2,1,2", options="header"] +// The per-claim proof record. Obligations are in PROOF-NEEDS.adoc. += Proof Status — OCCUPANCY_TYPES + +This is the repository's proof-status file in the sense of +`standards/3-practice/PROOF-POLICY.adoc` §8.5: one row per claim, with its status +token, its Proof Witness Grade (PWG) and its recheck legs. The estate census +(hyperpolymath/mathesis) reads this file. A row says only what a receipt or a +gate supports. + +A freshly minted repository has no claims, so the table is empty. That is the +honest state. Do not replace it with a percentage or an "overall" figure: +§8.4 forbids percentages, averages and single scores for proofs. + +== Vocabulary + +* *Status token* (§5): `proven`, `proven-with-postulates`, `tested-only`, + `argued`, `open`. +* *PWG* (§8), always written with its prefix, never as a bare "Grade": +** `PWG-3`: the Agda proof is `proven` under the canonical §2.1 invocation, + `agda --safe --without-K --cubical-compatible`; +** `PWG-2i`: one recheck leg also counts (Rocq or Lean); +** `PWG-2ii`: both legs count; +** `PWG-1`: every counted witness has a current receipt for the exact commit; +** `ungraded`: a theory theorem below `PWG-3`; +** `n/a`: not a theory theorem (a test, a refusal fixture, an assumption, or an + engineering proof in Idris2, SPARK, Creusot or Verus). §8.1 puts these off the + ladder. +* *Recheck legs* (§2): `transported`, `re-proved`, `waived` or `unavailable`, + each with its §2.2 correspondence state, for example `BRIDGE-PENDING` or + `RECONCILED`. A leg counts towards a grade only when it is `RECONCILED`. + +A grade is derived from receipts, not asserted (§8.4). Name the receipt in the +table, and a claim that has none stays `ungraded`. `PWG-3` is not a discharge: +a theorem at `PWG-3` still owes both legs. + +The template's own sample proofs under `verification/proofs/` are boilerplate. +They are never rows here: replace them with this repository's proofs, or delete +them. + +== Claims + +[cols="1,3,1,1,1,1,2",options="header"] |=== -| ID | Proof | Prover | Assignee | Started | Blocker +| ID | Claim (and the artefact that proves or tests it) | Token | PWG | Rocq leg | Lean leg | Receipt -| — | — | — | — | — | — +| — | No claims yet | — | — | — | — | — |=== -== Proofs Blocked +== Trust boundary -[cols="1,3,2,3", options="header"] -|=== -| ID | Proof | Blocked By | Notes +List every axiom, `ASSUMED` boundary specification (third-party code, PROOF-POLICY +§1.1) and recorded exception (§4) here, with the claims that rest on it. Write +"none" when there are none, so an empty section cannot read as an oversight. -| — | — | — | — -|=== +none -== Proofs Remaining - -[cols="1,3,1,1,1,1", options="header"] -|=== -| ID | Proof | Category | Prover | Priority | Est. Effort - -| ABI-1 | Non-null pointer proofs | ABI | Idris2 | P1 | 2h -| ABI-2 | Memory layout correctness | ABI | Idris2 | P1 | 4h -| ABI-3 | Platform type size proofs | ABI | Idris2 | P1 | 2h -| ABI-4 | FFI function return type proofs | ABI | Idris2 | P1 | 2h -| ABI-5 | C ABI compliance | ABI | Idris2 | P1 | 4h -| TP-1 | Core data type well-formedness | TP | Idris2 | P1 | 4h -| TP-2 | Public API type safety | TP | Lean4 | P2 | 4h -|=== - -== Verification Commands +== Verification commands [source,bash] ---- -# Check all Idris2 proofs -just proof-check-idris2 - -# Check all Lean4 proofs -just proof-check-lean4 - -# Check all Agda proofs -just proof-check-agda - -# Check all Coq proofs -just proof-check-coq - -# Run all proof checks -just proof-check-all - -# Scan for dangerous patterns -panic-attack assail --proofs-only +just proof-check-agda # Agda: agda --safe --without-K --cubical-compatible +just proof-check-coq # Rocq recheck legs +just proof-check-lean4 # Lean recheck legs (core only, no Mathlib) +just proof-check-idris2 # Idris2 engineering proofs +just proof-check-all # every prover; fatal if a toolchain is absent ---- - -== Changelog - -[cols="1,3,1", options="header"] -|=== -| Date | Change | By - -| 2026-04-04 | Initial proof status tracking | Template -|=== diff --git a/docs/status/READINESS.adoc b/docs/status/READINESS.adoc index effb64e..3f99ea0 100644 --- a/docs/status/READINESS.adoc +++ b/docs/status/READINESS.adoc @@ -1,10 +1,10 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -= {{PROJECT_NAME}} Component Readiness Assessment += occupancy-types Component Readiness Assessment -*Standard:* link:https://{{FORGE}}/{{OWNER}}/standards/tree/main/component-readiness-grades[Component Readiness Grades (CRG) v2.0 STRICT] + -*Assessed:* {{CURRENT_DATE}} + -*Assessor:* {{AUTHOR}} + +*Standard:* link:https://github.com/hyperpolymath/standards/tree/main/component-readiness-grades[Component Readiness Grades (CRG) v2.0 STRICT] + +*Assessed:* 2026-11-12 + +*Assessor:* Jonathan D.A. Jewell + *Previous assessment:* __ *Current Grade:* X @@ -40,34 +40,34 @@ evidence doesn't support the claim. *Evidence gates (v2.0 is stricter than v1.0):* * *D*: RSR-compliant + per-capability tests + documented scope. Test matrix - must cite counts and live in `.machine_readable/descriptiles/STATE.a2ml` or CI. + must cite counts and live in `docs/status/ROADMAP.adoc` or CI. * *C*: All of D, plus active dogfooding in home context with *no known home-context failures over an evidence window*, plus *deep per-file and per-directory annotation* (purpose, boundaries, invariants, execution/test/proof surfaces, per-directory orientation READMEs). - STATE.a2ml `[dogfooding-status]` populated with concrete "done — " - entries. + `docs/status/ROADMAP.adoc` `== Dogfooding status` populated with concrete + "done — " entries. * *B*: All of C, plus *six genuinely diverse external targets* with - feedback fed back. STATE.a2ml `[external-targets]` holds target identities - + dates + outcomes; `[issues-fed-back]` holds the closed-issue trail. + feedback fed back. ROADMAP.adoc `== External targets` holds target identities + + dates + outcomes; `== Issues fed back` holds the closed-issue trail. Internal-capability items do *not* count. * *A*: All of B, plus multi-source real-world external feedback confirming - value, no harm in the wild. STATE.a2ml `[field-signal]` populated. + value, no harm in the wild. ROADMAP.adoc `== Field signal` populated. *Publication gate:* non-abstract implementation claims require *B+*. Below B, any publication must be explicitly abstract or provisional. ''' -== 2. Headline Evidence (as of {{CURRENT_DATE}}) +== 2. Headline Evidence (as of 2026-11-12) [cols="2,3,3", options="header"] |=== | Metric | Value | Source -| Test count | __ | `.machine_readable/descriptiles/STATE.a2ml` or CI +| Test count | __ | `docs/status/ROADMAP.adoc` or CI | Formal-verification posture | __ | grep of proof dirs -| Dangerous patterns (`assert_total`, `unsafeCoerce`, `Obj.magic`) | __ | grep {{CURRENT_DATE}} +| Dangerous patterns (`assert_total`, `unsafeCoerce`, `Obj.magic`) | __ | grep 2026-11-12 | Per-unit README coverage | __ | filesystem scan of unit dirs | CI status | __ | `.github/workflows/` | RSR mandatory workflows | __ | `.github/workflows/` @@ -87,9 +87,9 @@ unless noted. |=== | Component | Grade | Posture | Evidence Summary | Promotion blocker | Last Assessed -| __ | X/F/E/D/C/B/A | __ | __ | __ | {{CURRENT_DATE}} -| __ | … | … | … | … | {{CURRENT_DATE}} -| __ | … | … | … | … | {{CURRENT_DATE}} +| __ | X/F/E/D/C/B/A | __ | __ | __ | 2026-11-12 +| __ | … | … | … | … | 2026-11-12 +| __ | … | … | … | … | 2026-11-12 |=== *Rules of thumb for populating this table:* @@ -113,8 +113,8 @@ CRG v2.0 requires two new pieces of evidence on top of D: ** Home context: __. ** "No known failures" is a moving claim — must hold continuously across the evidence window (recommended: 4 weeks, daily use). -** Populate `STATE.a2ml [dogfooding-status]` with one entry per capability: - `capability = "done — "`. +** Populate `== Dogfooding status` in `docs/status/ROADMAP.adoc` with one entry + per capability: ` — done — `. . *Deep code and folder annotation.* ** Per-directory orientation READMEs in every non-trivial source subtree. @@ -142,21 +142,21 @@ component. Candidate diversity axes: * Different topology (star, mesh, peer-to-peer). * Different trust model (same-org, federated, adversarial). -Populate `STATE.a2ml`: +Populate `docs/status/ROADMAP.adoc`: -* `[external-targets]` — target-id → `" — — "`. -* `[issues-fed-back]` — issue-id → `" — — "`. +* `== External targets` — target-id → ` — — `. +* `== Issues fed back` — issue-id → ` — — `. Six variations of the same use case do *not* count. Re-classify any items -currently under `[grade-b-status]` (legacy) and move external-eligible ones -into `[external-targets]`. +currently under other roadmap sections and move external-eligible ones +into `== External targets`. ''' == 6. What's Needed for B → A Real-world external feedback confirming value. Populate -`STATE.a2ml [field-signal]` with multi-source entries: +`== Field signal` in `docs/status/ROADMAP.adoc` with multi-source entries: * External users beyond the six B-targets. * Third-party writeups, talks, papers, or testimonials. @@ -164,7 +164,7 @@ Real-world external feedback confirming value. Populate ''' -== 7. Summary ({{CURRENT_DATE}}) +== 7. Summary (2026-11-12) * Project grade: __. Why: _<1-2 line justification>_. * Delta since last assessment: __. @@ -177,10 +177,12 @@ Real-world external feedback confirming value. Populate * `docs/governance/CRG-CRITERIA.adoc` — grade definitions (boilerplate). * `docs/governance/CRG-AUDIT-.adoc` — formal audit (populate from - `rsr-template-repo/docs/governance/CRG-AUDIT-TEMPLATE.adoc`). + `occupancy-types/docs/governance/CRG-AUDIT-TEMPLATE.adoc`). * `docs/practice/DOGFOOD-LOG.adoc` — dated dogfood evidence (required for C). -* `.machine_readable/descriptiles/STATE.a2ml` — authoritative state (canonical keys - `[dogfooding-status]`, `[external-targets]`, `[issues-fed-back]`, - `[field-signal]`). +* `docs/status/ROADMAP.adoc` — evidence journal (sections + `== Dogfooding status`, `== External targets`, `== Issues fed back`, + `== Field signal`, and a `Declared CRG grade:` line). +* `*_chora.deed` (repo deed) — authoritative phase and maturity (`(status)`, + `(maturity)`). _Run `just crg-badge` to generate the shields.io badge for your README._ diff --git a/docs/status/ROADMAP.adoc b/docs/status/ROADMAP.adoc index bafe0ce..30a46f6 100644 --- a/docs/status/ROADMAP.adoc +++ b/docs/status/ROADMAP.adoc @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -= YOUR Template Repo Roadmap += occupancy-types Roadmap == Current Status diff --git a/docs/status/TEST-NEEDS.adoc b/docs/status/TEST-NEEDS.adoc index c6d2e51..0770436 100644 --- a/docs/status/TEST-NEEDS.adoc +++ b/docs/status/TEST-NEEDS.adoc @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -= TEST-NEEDS: rsr-template-repo += TEST-NEEDS: occupancy-types == CRG Grade: C — ACHIEVED 2026-04-04 @@ -65,7 +65,7 @@ output only. === Zig FFI Tests ✅ * [x] `src/interface/ffi/test/integration_test.zig` — template with examples -** Converted from `{{project}}` placeholders to "template" namespace +** Converted from `occupancy_types` placeholders to "template" namespace ** Added comprehensive comments for how to instantiate ** Tests grouped by category (lifecycle, operations, strings, errors, version, memory safety, threading) ** Compiles and passes placeholder test diff --git a/build/docs-seed/troubleshooting.adoc b/docs/troubleshooting.adoc similarity index 84% rename from build/docs-seed/troubleshooting.adoc rename to docs/troubleshooting.adoc index 8c62e69..c2f480a 100644 --- a/build/docs-seed/troubleshooting.adoc +++ b/docs/troubleshooting.adoc @@ -1,8 +1,8 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -= Troubleshooting — {{PROJECT_NAME}} -:revdate: {{CURRENT_YEAR}}-MM-DD +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += Troubleshooting — occupancy-types +:revdate: 2026-MM-DD This file is a *living FAQ* — known failure modes and recovery paths. Add a new entry every time you debug something that took more than 15 @@ -54,5 +54,5 @@ just log-tail # last N lines of logs * `docs/architecture.adoc` — internals. * `docs/decisions/` — historical record of why things are this shape. -* `https://github.com/{{OWNER}}/{{PROJECT_NAME}}/issues?q=is%3Aissue+` — has anyone hit this before? +* `https://github.com/hyperpolymath/occupancy-types/issues?q=is%3Aissue+` — has anyone hit this before? * `https://github.com/hyperpolymath/standards/issues` — for estate-wide problems. diff --git a/build/docs-seed/usage.adoc b/docs/usage.adoc similarity index 87% rename from build/docs-seed/usage.adoc rename to docs/usage.adoc index 8ebc533..99149f0 100644 --- a/build/docs-seed/usage.adoc +++ b/docs/usage.adoc @@ -1,12 +1,12 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -= Usage — {{PROJECT_NAME}} -:revdate: {{CURRENT_YEAR}}-MM-DD +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += Usage — occupancy-types +:revdate: 2026-MM-DD == Audience -This document is for *consumers* of `{{PROJECT_NAME}}` — people who depend +This document is for *consumers* of `occupancy-types` — people who depend on it, call it, or include it. For developers working *on* it, see link:./contributing.adoc[contributing.adoc]. @@ -31,7 +31,7 @@ Expected output: [source] ---- -{{PROJECT_NAME}} v0.0.0 +occupancy-types v0.0.0 Listening on 127.0.0.1:8080 ---- diff --git a/launcher.sh b/launcher.sh new file mode 100755 index 0000000..52ad7d7 --- /dev/null +++ b/launcher.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# @launcher-deed begin +# ;; SPDX-License-Identifier: MPL-2.0 +# (praxis-deed +# :schema-version "1.0.0" +# :canonical-name "occupancy-types-launcher" +# :beholding-chora #u5"estate/chora" +# (artefact :type "launcher" :version "0.1.0" +# :generator "provision-set") +# (app :name "occupancy-types" :display "occupancy-types" +# :url "https://github.com/hyperpolymath/occupancy-types" :archetype "library") +# (compliance :standard-version "0.6.0" +# :standards ("launcher-standard.adoc" +# "PROVISIONING-STANDARD.adoc")) +# (modes :accepted ("--setup" "--doctor" "--heal" "--ai-setup" "--help" "--version" +# "--start" "--stop" "--status" "--auto" "--integ" "--disinteg")) +# (platforms :supported ("linux" "macos" "windows")) +# (lifecycle-phases :covered ("provision" "diagnose" "heal") +# :deferred ("install" "run"))) +# @launcher-deed end +# +# The launcher for a library / tool / theory / docs repository +# (launcher-standard 0.6.0, archetype profile). Every mode is implemented in +# build/just/provision-modes.sh, and every provisioning mode delegates to the +# Justfile. This file is minted by `provision-set`; do not hand-edit it. +# Repo-specific facts belong in .machine_readable/descriptiles/provisioning_praxis.deed. +set -uo pipefail + +REPO_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" + +if [ ! -f "$REPO_DIR/build/just/provision-modes.sh" ]; then + echo "launcher.sh: build/just/provision-modes.sh is missing — this checkout is incomplete." >&2 + echo "Re-clone, or restore it: git checkout -- build/just/provision-modes.sh" >&2 + exit 1 +fi +# shellcheck source=build/just/provision-modes.sh +. "$REPO_DIR/build/just/provision-modes.sh" + +hp_launcher_main "$@" diff --git a/mise.lock b/mise.lock new file mode 100644 index 0000000..504562b --- /dev/null +++ b/mise.lock @@ -0,0 +1,157 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +[[tools.just]] +version = "1.56.0" +backend = "aqua:casey/just" + +[tools.just."platforms.linux-arm64"] +checksum = "sha256:c8c1d656e9f47569ec1ae2bf8779af2621cdeea6bbbba3b0cacd64f951d25e2b" +url = "https://github.com/casey/just/releases/download/1.56.0/just-1.56.0-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/casey/just/releases/assets/472074960" + +[tools.just."platforms.linux-arm64-musl"] +checksum = "sha256:c8c1d656e9f47569ec1ae2bf8779af2621cdeea6bbbba3b0cacd64f951d25e2b" +url = "https://github.com/casey/just/releases/download/1.56.0/just-1.56.0-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/casey/just/releases/assets/472074960" + +[tools.just."platforms.linux-x64"] +checksum = "sha256:fa2a8ec1015d9df5330941ade12437488fc40d33f9c9f8cd4eb70a26de11b639" +url = "https://github.com/casey/just/releases/download/1.56.0/just-1.56.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/casey/just/releases/assets/472074624" + +[tools.just."platforms.linux-x64-musl"] +checksum = "sha256:fa2a8ec1015d9df5330941ade12437488fc40d33f9c9f8cd4eb70a26de11b639" +url = "https://github.com/casey/just/releases/download/1.56.0/just-1.56.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/casey/just/releases/assets/472074624" + +[tools.just."platforms.macos-arm64"] +checksum = "sha256:f35798d4bcdc4db020eef7d2853ad98bbfb97a4d29ee695ba042f18e7fedcc11" +url = "https://github.com/casey/just/releases/download/1.56.0/just-1.56.0-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/casey/just/releases/assets/472074635" + +[tools.just."platforms.macos-x64"] +checksum = "sha256:09b35ff6d17023ffae37ce408d1a78a976d9e001cae54b88e238f7f40db9b783" +url = "https://github.com/casey/just/releases/download/1.56.0/just-1.56.0-x86_64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/casey/just/releases/assets/472074650" + +[tools.just."platforms.windows-x64"] +checksum = "sha256:804f5b2fe94291d0df38fd8dfc5620afbf3496f8f11c1915b42a87323234f0ba" +url = "https://github.com/casey/just/releases/download/1.56.0/just-1.56.0-x86_64-pc-windows-msvc.zip" +url_api = "https://api.github.com/repos/casey/just/releases/assets/472075917" + +[[tools.shellcheck]] +version = "0.11.0" +backend = "aqua:koalaman/shellcheck" + +[tools.shellcheck."platforms.linux-arm64"] +checksum = "sha256:12b331c1d2db6b9eb13cfca64306b1b157a86eb69db83023e261eaa7e7c14588" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.aarch64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056934" + +[tools.shellcheck."platforms.linux-arm64-musl"] +checksum = "sha256:12b331c1d2db6b9eb13cfca64306b1b157a86eb69db83023e261eaa7e7c14588" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.aarch64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056934" + +[tools.shellcheck."platforms.linux-x64"] +checksum = "sha256:8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056942" + +[tools.shellcheck."platforms.linux-x64-musl"] +checksum = "sha256:8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056942" + +[tools.shellcheck."platforms.macos-arm64"] +checksum = "sha256:56affdd8de5527894dca6dc3d7e0a99a873b0f004d7aabc30ae407d3f48b0a79" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.darwin.aarch64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056932" + +[tools.shellcheck."platforms.macos-x64"] +checksum = "sha256:3c89db4edcab7cf1c27bff178882e0f6f27f7afdf54e859fa041fca10febe4c6" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.darwin.x86_64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056930" + +[tools.shellcheck."platforms.windows-x64"] +checksum = "sha256:8a4e35ab0b331c85d73567b12f2a444df187f483e5079ceffa6bda1faa2e740e" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.zip" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056944" + +[[tools.trivy]] +version = "0.74.0" +backend = "aqua:aquasecurity/trivy" + +[tools.trivy."platforms.linux-arm64"] +checksum = "sha256:b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5" +url = "https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_Linux-ARM64.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/514339429" +provenance = "cosign" + +[tools.trivy."platforms.linux-arm64-musl"] +checksum = "sha256:b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5" +url = "https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_Linux-ARM64.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/514339429" +provenance = "cosign" + +[tools.trivy."platforms.linux-x64"] +checksum = "sha256:2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a" +url = "https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_Linux-64bit.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/514339431" +provenance = "cosign" + +[tools.trivy."platforms.linux-x64-musl"] +checksum = "sha256:2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a" +url = "https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_Linux-64bit.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/514339431" +provenance = "cosign" + +[tools.trivy."platforms.macos-arm64"] +checksum = "sha256:1caada5e0e2091909357c7525d3aa76f4b660b13821bc143b190c7483e31cc11" +url = "https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_macOS-ARM64.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/514339464" +provenance = "cosign" + +[tools.trivy."platforms.macos-x64"] +checksum = "sha256:472816f6888dda689d075c30254d4210b4d1035acf365aa72332f584c2f60485" +url = "https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_macOS-64bit.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/514339388" +provenance = "cosign" + +[tools.trivy."platforms.windows-x64"] +checksum = "sha256:94c40e0696e4b907a74b7b2e1438d5d72ebaca83115817407f568a002d520842" +url = "https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_windows-64bit.zip" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/514339469" +provenance = "cosign" + +[[tools.zig]] +version = "0.16.0" +backend = "core:zig" + +[tools.zig."platforms.linux-arm64"] +checksum = "sha256:ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17" +url = "https://ziglang.org/download/0.16.0/zig-aarch64-linux-0.16.0.tar.xz" + +[tools.zig."platforms.linux-arm64-musl"] +checksum = "sha256:ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17" +url = "https://ziglang.org/download/0.16.0/zig-aarch64-linux-0.16.0.tar.xz" + +[tools.zig."platforms.linux-x64"] +checksum = "sha256:70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00" +url = "https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz" + +[tools.zig."platforms.linux-x64-musl"] +checksum = "sha256:70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00" +url = "https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz" + +[tools.zig."platforms.macos-arm64"] +checksum = "sha256:b23d70deaa879b5c2d486ed3316f7eaa53e84acf6fc9cc747de152450d401489" +url = "https://ziglang.org/download/0.16.0/zig-aarch64-macos-0.16.0.tar.xz" + +[tools.zig."platforms.macos-x64"] +checksum = "sha256:0387557ed1877bc6a2e1802c8391953baddba76081876301c522f52977b52ba7" +url = "https://ziglang.org/download/0.16.0/zig-x86_64-macos-0.16.0.tar.xz" + +[tools.zig."platforms.windows-x64"] +checksum = "sha256:68659eb5f1e4eb1437a722f1dd889c5a322c9954607f5edcf337bc3684a75a7e" +url = "https://ziglang.org/download/0.16.0/zig-x86_64-windows-0.16.0.zip" diff --git a/mise.toml b/mise.toml index be48009..9044a9e 100644 --- a/mise.toml +++ b/mise.toml @@ -1,88 +1,25 @@ -# Every tool name below was checked against `mise registry` AND resolved with -# `mise ls-remote` before being written. The previous version of this file named -# 13 tools that do not exist in the registry, so mise emitted ~25 lines of -# warnings on every shell entry while installing none of them. -# -# This file is a TEMPLATE. Repos minted from it inherit it verbatim, so add -# your project's actual language toolchain here after minting — pinned to the -# same versions your CI uses, so local and CI agree. +# SPDX-License-Identifier: MPL-2.0 +# +# The toolchain for this repository (idris2 zig). Rendered at mint by +# scripts/render-mise.sh from the languages the tree declares; the language→tool +# table is build/just/provision-lib.sh's, so this file and `just doctor` cannot +# disagree. Specs are `latest`; mise.lock records the concrete version and +# per-platform checksum of each. Refresh both with: just toolchain-refresh +# Canon: hyperpolymath/standards 3-practice/provisioning (TRUST-DEFAULTS §toolchain). +# Estate policy: no python, node/npm/yarn, make or nix here. JavaScript runs +# Bun > Deno > pnpm from one package.json (LANGUAGE-POLICY §1.4). +# +# Hand-edited project tools belong below the table's end, never by replacing it: +# `scripts/check-mise-language.sh` fails when a tool matches no detected +# language, and when a detected language's tool is missing. + +[settings] +lockfile = true [tools] -# Language runtimes -node = "latest" -julia = "1.10" -rust = "latest" -go = "latest" +just = "latest" +shellcheck = "latest" # the shell scripts +yq = "latest" # scripts/kyaml-format.sh (KYAML configs) zig = "latest" -java = "latest" -bun = "latest" -deno = "latest" # was `denojs` — not a registry name - -# Package managers / task runner -npm = "latest" -yarn = "latest" -pnpm = "latest" -just = "latest" # the estate's task runner (Justfile) - -# Formatting & linting -black = "latest" -ruff = "latest" -prettier = "latest" -shfmt = "latest" -stylua = "latest" - -# Build tools -cmake = "latest" -make = "latest" -ninja = "latest" - -# --------------------------------------------------------------------------- -# REMOVED — none of these resolve in the mise registry, so they installed -# nothing and only produced warnings. Grouped by why they were wrong: -# -# Already provided by a tool that IS listed above: -# cargo -> ships with `rust` -# gofmt -> ships with `go` -# (no pip: the stepper is Julia, standard library only) -# -# Project-level dependencies, not system tools. These belong in -# package.json / pyproject.toml / mix.exs, not in a toolchain manager: -# vitest, jest, pytest, isort -# -# Base system binaries that mise should not be shadowing on Linux: -# git, gnu-sed, gnu-grep, gnu-tar -# (If a GNU-vs-BSD coreutils difference ever actually bites on macOS, add -# `coreutils`, which does exist in the registry, rather than these names.) -# -# Task runner that never installed: -# go-task -> not a registry name. NB the bare name `task` does not work -# either: `mise registry` LISTS it as `aqua:go-task/task`, but resolving -# `task@latest` still fails. Appearing in `mise registry` output is NOT -# sufficient to conclude a name resolves — check with `mise ls-remote`. -# `just` is listed above instead. -# --------------------------------------------------------------------------- - -[env] -NODE_ENV = "development" -PYTHONDONTWRITEBYTECODE = "1" -PYTHONUNBUFFERED = "1" - -# --------------------------------------------------------------------------- -# REMOVED — an `[alias]` section holding what were clearly meant to be tasks: -# -# [alias] -# task = "go-task" -# build = "cargo build --release || npm run build || go build" -# test = "cargo test || npm test || go test ./..." -# lint = "ruff check . || prettier --check . || black --check ." -# fmt = "ruff format . || prettier --write . || black ." -# -# mise's `[alias]` maps an alias to a TOOL PLUGIN — it does not define tasks, -# so none of these ever ran. mise also warns that `[alias]` is deprecated in -# favour of `[tool_alias]`. -# -# They are not re-added as `[tasks]`: the estate drives builds through the -# Justfile, and a second task runner sharing the verbs `build`/`test`/`lint`/ -# `fmt` — one of which silently falls through `||` chains into a different -# language's build — is worse than one. Use `just `. -# --------------------------------------------------------------------------- +julia = "1.10" # src/julia (the Julia package); detection fix pending upstream +trivy = "latest" # just deps-audit diff --git a/occupancy-types_chora.deed b/occupancy-types_chora.deed new file mode 100644 index 0000000..8503323 --- /dev/null +++ b/occupancy-types_chora.deed @@ -0,0 +1,1159 @@ +;; SPDX-License-Identifier: MPL-2.0 +; +; The repo deed of occupancy-types: one-deed-per-repo doctrine (standards#837, +; owner ruling 2026-09-19, Option A). Folds the former ply manifest tree +; (82 per-directory manifests) and the family-7 AI allocation manifest +; into deed clauses. +; The ply, manifest and canon clauses come from the #837 pilot emitter. The +; identity, clade, forges, lineage, status, maturity, ecosystem, meta and +; agentic clauses carry the former CLADE, META, ECOSYSTEM, AGENTIC and STATE +; records (#209), translated field for field under the standards mapping +; specs (1-formats/deed/mappings/). STATE's journal is docs/status/ROADMAP.adoc. +; The anchor, neurosym, playbook, ensaid, bot-directives, integrations, +; policies and rsr-profile clauses at the end carry every other record this +; repo used to keep under .machine_readable/: the deed is the one +; machine-readable description of the repo. scripts/deed-field.sh reads any +; field; scripts/deed-clause.sh is the one writer. +(repo-deed + :schema-version "1.0.0" + :canonical-name "occupancy-types" + :beholding-chora #u5"estate/chora" + :repo-uuid #u5"github.com/hyperpolymath/occupancy-types" + ; The canon pin: which release of hyperpolymath/standards canon.lock this + ; repo implements. It is the ONLY canon pin in this deed (deed_canon + ; refuses zero or two); the (rsr-profile) clause below declares + ; capabilities and carries no pin. It moved here so the pin could follow a + ; canon release (#215). + ; Read by: dogfood-gate.yml `Canon lockstep` (reverse direction), + ; standards scripts/check-canon-lockstep.sh assertion 3 (forward), and + ; build/just/repo-init.just (writes the child's (provenance) clause at mint). Every hash MUST + ; equal canon.lock [canon.artifacts]; canon.lock [canon.lockstep].order is + ; spine-adopts-then-canon-releases, so this clause moves FIRST. + ; Two-phase: to adopt a canon AHEAD of its release, pin the open canon + ; PR's canon.lock and name it with :proposed-by "metadatastician/standards#N". + ; scripts/canon-pin-check.sh then checks the pin against that PR's head + ; while it is open; once it merges the field is spent and can be dropped. + (canon + :version "2.1.5" + :criteria-sha256 "6a5aa8857bd0d0d58ef48827938ca17c251b6b854dacf59d306388d61694d82a" + :gates-sha256 "5c510bba025be718deebb12d0333bfe7db2da43c66578b728b1a334df65290b1" + :lockstep-since "2026-09-17" + ; A canon change this repo cannot satisfy is a change made in the WRONG + ; ORDER, not a rejected change: land the spine's adoption first, then the + ; canon change is a one-line bump. + :order "spine-adopts-then-canon-releases") + ; >>> repo-identity + ; Written by `just repo-init` from build/templates/identity.deed.in, in + ; place of the block the template repo ships (template issue #209). It replaces + ; descriptiles/CLADE.a2ml and ECOSYSTEM.a2ml; the mapping is standards + ; 1-formats/deed/mappings/clade-to-repo-deed.adoc and + ; meta-ecosystem-to-repo-deed.adoc. + ; + ; THE UUID IS DERIVED, NOT ALLOCATED, NOT INVENTED. It is not stored: it is + ; uuid5(namespace = URL, name = the header :repo-uuid string) + ; and anyone can recompute it (gv-clade-index: + ; docs/SPEC-clade-verisim-portal.adoc §Identity Model): + ; uuidgen --sha1 --namespace @url --name "github.com/hyperpolymath/occupancy-types" + ; The owner is part of the name, so part of the identity: record the owner + ; that is true today, and re-derive if the repo ever moves. + ; + ; IDENTITY IS THE CHORA, NOT THE CLADE (standards A1, ruling R4). The + ; deed header's :repo-uuid is the repo's id. A clade is an OPTIONAL outward finding + ; aid: no check requires one, and no name prefix is required. When you do + ; choose one, add a clause beside (identity), for example + ; (clade :primary gm :primary-name "Games & Interactive" :secondary () + ; :assigned "YYYY-MM-DD" :rationale "why") + ; and `just validate-rsr` then checks that the code is one of the 12 and + ; agrees with its name. The 12, read off the RIGHT-HAND column, never off + ; the repository name (two real mis-choices read `pt` and `gv` as letters of + ; the slug): + ; fv Formal Verification & Proofs provable correctness, dependent types, theorem proving + ; nl Nextgen Languages compilers, runtimes, language tooling + ; rm Repo Management & Tooling scaffolding, graph analysis, bots, templates + ; gv Governance & Standards licensing, compliance, policy enforcement + ; db Databases database engines, query languages, storage + ; ap Applications end-user apps, web, desktop, services + ; ix Infrastructure & Cloud containers, deployment, sysadmin + ; dx Developer Ecosystem dev tools, package managers, editors, bindings + ; pt Protocols & Interop internet standards, protocol implementations + ; ax AI & Neurosymbolic ML, neural proof synthesis, AI governance + ; gm Games & Interactive game engines, interactive experiences + ; sc Security scanning, vulnerability management, access control + ; The primary clade reflects the project's core value proposition, not its + ; implementation: a game written in Idris2 is `gm`. + ; + ; REGISTRATION IS NOT DERIVATION. Registering means an entry in the + ; gv-clade-index registry seed. It is outward-facing and it is the OWNER's + ; act; an agent must not do it. + (identity + :primary-forge github + :owner "hyperpolymath") + (forges + :github "hyperpolymath/occupancy-types") + ; :type is one of standalone monorepo monorepo-child inflated deflated hub + ; satellite. A hub COORDINATES an ecosystem family; a satellite is + ; coordinated by one and keeps :parent "" (parent names a monorepo parent, + ; never the coordinating hub). + (lineage + :type standalone + :parent "" + :born "2026-11-12" + :previous-names () + :instantiated-from "rsr-template-repo") + ; :phase is one of reserved incubating active dormant merged superseded + ; archived extinct. A repo created today has not proven anything yet. + (status + :phase incubating + :since "2026-11-12" + :present #t + :aliases () + :merged-into "" + :superseded-by "" + :successors () + :ended "" + (history + (entry :phase incubating :since "2026-11-12" :note "created from rsr-template-repo; not yet registered"))) + ; :level is one of experimental alpha beta production lts (standards + ; 1-formats/deed/vocabulary/maturity.adoc). + (maturity + :level experimental + :since "2026-11-12") + ; Fill in what this repo IS and IS NOT, and the repos it relates to. + ; :relationship is one of standard-source build-tooling validation-tooling + ; signing-tooling verification-tooling minted-from registry. + (ecosystem + :project "occupancy-types" + :ecosystem "hyperpolymath" + :position-type "UNASSIGNED" + :purpose "UNASSIGNED" + :not () + :pipeline-position "UNASSIGNED" + :chain "UNASSIGNED" + :coordination standards + (related :name "rsr-template-repo" :relationship minted-from :notes "the template this repo was minted from by just repo-init")) + ; <<< repo-identity + ; Meta: mapping meta-ecosystem-to-repo-deed. A minted repo keeps this + ; clause (self-name pass applied) and edits it as its own. + (meta + :version "0.1.0" + :last-updated "2026-04-11" + :type library + :languages () + :license MPL-2.0 + :author "Jonathan D.A. Jewell (hyperpolymath)" + :build-tool just + :container-runtime podman + :ci-platform github-actions + :package-manager guix + :scoping-first #t + :execution-order "axis-1 > axis-2 > axis-3" + :axis-1 "must > intend > like" + :axis-2 "corrective > adaptive > perfective" + :axis-3 "systems > compliance > effects" + (scoping + :sources "README, roadmap, status docs, maintenance checklist, CI/security docs" + :marker-scan "TODO/FIXME/XXX/HACK/STUB/PARTIAL" + :idris-unsound-scan "believe_me/assert_total") + :corrective-first #t + :adaptive-second #t + :adaptive-focus "scope-change reconciliation, stale-reference removal, obsolete-work culling" + :perfective-third #t + :perfective-source "axis-1 honest state after corrective/adaptive updates" + (axis-3 + :audit-focus "systems in place, documentation explains actual state, safety/security accounted for, observed effects reviewed" + :compliance-focus "seams/compromises/exception register, bounded exceptions, anti-drift checks" + :drift-risk-example "single exception broadening into policy violation (e.g. ->TypeScript spread)" + :effects-evidence "benchmark execution/results and maintainer status dialogue/review")) + ; Agent permissions and integrity: mapping agentic-neurosym-playbook-to-repo-deed. + ; What agents must NOT do: + ; - Never use banned language patterns (believe_me, unsafeCoerce, etc.) + ; - Never commit secrets or credentials + ; - Never use banned languages (TypeScript, Python, Go, etc.) + ; - Never place state files in repository root (must be in .machine_readable/) + ; - Never relicense an existing file, and never run an automated licence + ; sweep (LICENCE-POLICY.adoc A2). New files get correct SPDX from birth. + ; - Never assume a licence. Read standards/3-practice/LICENCE-POLICY.adoc: Rule 1 + ; defaults to MPL-2.0 (code) / CC-BY-SA-4.0 (prose), but Rule 3 + ; (co-developed), Rule 4 (network-deployed services) and Rule 5 + ; (games) are AGPL-3.0-or-later, and Rule 2 names the PMPL register. + ; This clause declares WHAT agents may do; the (bot-directives) clause of + ; this deed declares HOW they work (methodology, coverage, debt, rra). + ; Hooks: on enter, read this deed, then docs/status/ROADMAP.adoc; on exit, + ; update ROADMAP.adoc and the (bot-directives (coverage)) and + ; (bot-directives (debt)) clauses with scripts/deed-clause.sh; on commit, + ; run `just validate-rsr`. + (agentic + :version "0.1.0" + :last-updated "2026-04-11" + (permissions :source #t :tests #t :docs #t :config #t :create-files #t) + (integrity + :fail-closed #t + :require-evidence-per-step #t + :allow-silent-skip #f + :require-rerun-after-fix #t + :release-claim-requires-hard-pass #t) + (methodology :instructions-clause "bot-directives" :default-mode hybrid)) + (manifest + :version "0.1.0" + :last-updated "2026-11-12" + :project "occupancy-types" + :purpose "Canonical RSR (Rhodium Standard Repository) template — governance, CI/CD, machine-readable metadata, ABI/FFI seam and formal-verification scaffolding that hyperpolymath projects are instantiated from." + (agent :name CLAUDE :role "proofs, compilers, repo-local implementation, CI/CD, formal verification, Rust/Idris2/Zig") + (agent :name CHATGPT :role "prose, papers, publication review, standards docs, outreach drafts, letters") + (agent :name GEMINI :role "estate audits, cross-repo sweeps, long-context triage, pattern detection") + (agent :name VIBE :role "UI/frontend, PanLL panels, components, theming, rapid prototyping") + (policy :rules ("Do not duplicate tasks across sections. If a task needs multiple LLMs, note the handoff." "Update THIS file during sessions. Do NOT recreate per-repo TODO files.")) + (work :agent CLAUDE :task "proofs, compilers, repo-local implementation, CI/CD, formal verification, Rust/Idris2/Zig") + (work :agent CHATGPT :task "prose, papers, publication review, standards docs, outreach drafts, letters") + (work :agent GEMINI :task "estate audits, cross-repo sweeps, long-context triage, pattern detection") + (work :agent VIBE :task "UI/frontend, PanLL panels, components, theming, rapid prototyping") + ) + (ply + (directory :ply 1 :path ".github") + (directory :id "machine-readable-pillar" :ply 1 :path ".machine_readable" :description "Registry for all machine-readable metadata, policies, and internal\nautomation scripts." + (canonical-locations :ai_configs "ai/" :anchor "occupancy-types_chora.deed (anchor)" :compliance "compliance/" :neurosym "occupancy-types_chora.deed (neurosym)" :playbook "occupancy-types_chora.deed (playbook)" :policies "occupancy-types_chora.deed (policies)" :scripts "scripts/") + :invariants ("Repo metadata MUST live in the repo deed (occupancy-types_chora.deed) and follow the DEED grammar" "Internal automation MUST live in scripts/ subfolder") + (directory :id "ai-registry" :ply 2 :path ".machine_readable/ai" :description "Sub-registry for ai metadata.") + (directory :id "configs-registry" :ply 2 :path ".machine_readable/configs" :description "Sub-registry for configs metadata.") + (directory :ply 0 :path ".machine_readable/descriptiles" :description "This manifest declares the AI-assistant context for the descriptiles machine-readable metadata directory." + :invariants ("No duplicate files in root directory" "Single source of truth: this directory is authoritative" "No stale metadata")) + (directory :id "policies-registry" :ply 2 :path ".machine_readable/policies" :description "Sub-registry for policies metadata.") + (directory :id "automation-scripts-unit" :ply 2 :path ".machine_readable/scripts" :description "Internal automation logic for the project lifecycle, forge sync,\nverification triggers, and maintenance." + (canonical-locations :forge "forge/" :lifecycle "lifecycle/" :maintenance "maintenance/" :verification "verification/") + (directory :id "automation-unit-forge" :ply 3 :path ".machine_readable/scripts/forge" :description "Internal automation logic for project forge.") + (directory :id "automation-unit-lifecycle" :ply 3 :path ".machine_readable/scripts/lifecycle" :description "Internal automation logic for project lifecycle.") + (directory :id "automation-unit-verification" :ply 3 :path ".machine_readable/scripts/verification" :description "Internal automation logic for project verification."))) + (directory :id "container-templates" :ply 1 :path "build/container" :version "1.0.0" :description "Container templates for the stapeln container ecosystem. This directory\nprovides Podman-Chainguard-stapeln templates that are customised via\n`just container-init` or `just repo-init` during project bootstrap.\n\nAll files use {{PLACEHOLDER}} tokens that are substituted with project-\nspecific values during initialisation." :purpose "Provide a complete, security-first container deployment story for any\nRSR-compliant repository. The templates cover the full lifecycle:\nbuild, sign, verify, deploy, monitor, and govern." :overview "The stapeln container ecosystem comprises six tools:\n\nselur — Container orchestration with zero-copy IPC. Reads compose.toml.\ncerro-torre — Verified container packaging (.ctp bundles), Ed25519 signing.\nsvalinn — Policy-driven edge gateway (auth, rate limits, CORS, trust).\nvordr — Runtime monitoring (health, crashes, resources, logs).\nrokur — Secrets management (runtime injection, no baked secrets).\nk9-svc — Nickel deployment components (Kennel/Yard/Hunt trust levels)." + :context ("https://stapeln.dev/ns/v1") + (canonical-locations :build_pipeline "build/container/stapeln/ct-build.sh" :compose "build/container/stapeln/compose.toml" :compose_example "build/container/stapeln/compose.example.toml" :compose_portable "build/container/compose.yaml" :compose_portable_example "build/container/compose.example.yaml" :containerfile "build/container/Containerfile" :containerignore "build/container/.containerignore" :deployment "build/container/stapeln/deploy.k9.ncl" :entrypoint "build/container/entrypoint.sh" :gatekeeper "build/container/stapeln/.gatekeeper.yaml" :just_module "build/just/container.just" :manifest "build/container/stapeln/manifest.toml" :monitoring "build/container/stapeln/vordr.toml" :secrets_gate "build/container/stapeln/rokur.toml") + :invariants ("Base images MUST be cgr.dev/chainguard/wolfi-base or cgr.dev/chainguard/static" "Container runtime is Podman — never Docker" "Containerfile — never Dockerfile" "All images run as non-root (appuser or project-specific user)" ".ctp bundles are signed with Ed25519 via cerro-torre" "Health endpoints (/health, /ready) must always be public (no auth)") + (file-relationships + (file :name "compose.toml" :role "Orchestration" :description "selur-compose stack definition. Declares services, volumes, networks,\nand health checks. References the Containerfile for image builds and\n.gatekeeper.yaml for svalinn policy." :depends-on ("Containerfile" ".gatekeeper.yaml")) + (file :name "Containerfile" :role "Image Build (Tier A)" :description "Multi-stage OCI container build. Stage 1 compiles the application on\nwolfi-base; Stage 2 copies the binary into a minimal runtime image and\ncopies entrypoint.sh. Engine-agnostic (podman/nerdctl/docker). The\nstapeln files (.gatekeeper.yaml, manifest.toml) are referenced only as\nOCI labels and copied in only if you uncomment those COPY lines, so the\nimage has no hard dependency on tier C." :depends-on ("entrypoint.sh")) + (file :name "manifest.toml" :role "Bundle Metadata" :description "Cerro-torre .ctp bundle manifest. Describes provenance, dependencies,\nattestations, and runtime security profile. Used by `ct pack` and\n`ct verify`." :depends-on ()) + (file :name ".gatekeeper.yaml" :role "Gateway Policy" :description "Svalinn edge gateway policy. Controls authentication, rate limiting,\ncontainer trust, request validation, CORS, and audit logging." :depends-on ()) + (file :name "ct-build.sh" :role "Build Pipeline" :description "Shell script implementing the 5-stage pipeline: build (Podman),\npack (cerro-torre .ctp), sign (Ed25519), verify, push (optional).\nDegrades gracefully when cerro-torre tools are not installed." :depends-on ("Containerfile" "manifest.toml")) + (file :name "entrypoint.sh" :role "Container Entrypoint" :description "Startup script with signal handling (SIGTERM, SIGINT), logging, and\nexec into the main application process." :depends-on ()) + (file :name "vordr.toml" :role "Runtime Monitoring" :description "Vordr monitoring configuration. Health endpoint probing, crash\ndetection, resource thresholds, and structured log output." :depends-on ()) + (file :name "deploy.k9.ncl.in" :role "Deployment Component (template)" :description "k9-svc deployment component at the Hunt leash, K9 contract v1.0.0:\npedigree with capability grant, side effects and signature block;\nenvironment configs, container config, rolling deployment strategy,\ndeploy scripts and Just recipes. `just container-init` renders it to\ndeploy.k9.ncl once no placeholder token is left." :depends-on ("compose.toml" "ct-build.sh")) + (file :name "compose.example.toml" :role "Example" :description "Fully-commented multi-service example (Rust API + Elixir worker +\nsvalinn gateway). Copy to compose.toml and customise." :depends-on ()) + )) + (directory :id "docs-pillar" :ply 1 :path "docs" :description "Technical documentation hub. The root contains high-level orientation\n(README, Quickstart, State-Visualizer). Specialized tracks live in\nsubdirectories." + (canonical-locations :architecture "architecture/" :attribution "attribution/" :decisions "decisions/" :developer "developer/" :governance "governance/" :legal "legal/" :practice "practice/" :quickstart "QUICKSTART.adoc" :reports "reports/" :standards "standards/" :state_visualizer "STATE-VISUALIZER.adoc" :theory "theory/" :whitepapers "whitepapers/" :wikis "wikis/") + :invariants ("Primary documentation format MUST be AsciiDoc (.adoc)" "Root docs/ MUST only contain pillar entry points") + (directory :id "architecture-track" :ply 2 :path "docs/architecture" :description "Documentation track for system architecture and threat models." + (canonical-locations :threat_model "THREAT-MODEL.adoc") + :invariants ("Visual diagrams MUST include ASCII or Mermaid representations")) + (directory :id "attribution-unit" :ply 2 :path "docs/attribution" :description "Sub-unit of the docs pillar focusing on attribution.") + (directory :id "decisions-unit" :ply 2 :path "docs/decisions" :description "Sub-unit of the docs pillar focusing on decisions.") + (directory :id "developer-unit" :ply 2 :path "docs/developer" :description "Sub-unit of the docs pillar focusing on developer.") + (directory :id "governance-pillar" :ply 1 :path "docs/governance" :description "Primary governance pillar implementing the Triaxial Software Development\nMethodology (TSDM). Contains planning, maintenance, and audit tracks." + (canonical-locations :approach "SOFTWARE-DEVELOPMENT-APPROACH.adoc" :audit "audit/" :checklist "MAINTENANCE-CHECKLIST.adoc" :crg "CRG-CRITERIA.adoc" :maintenance "maintenance/" :planning "planning/" :tsdm_spec "TSDM.adoc") + (directory :id "governance-axis-audit" :ply 2 :path "docs/governance/audit" :description "TSDM Audit track." + (directory :id "governance-unit-compliance" :ply 3 :path "docs/governance/audit/compliance" :description "TSDM compliance unit within the Audit axis.") + (directory :id "governance-unit-effects" :ply 3 :path "docs/governance/audit/effects" :description "TSDM effects unit within the Audit axis.") + (directory :id "governance-unit-systems" :ply 3 :path "docs/governance/audit/systems" :description "TSDM systems unit within the Audit axis.")) + (directory :id "governance-axis-maintenance" :ply 2 :path "docs/governance/maintenance" :description "TSDM Maintenance track." + (directory :id "governance-unit-adaptive" :ply 3 :path "docs/governance/maintenance/adaptive" :description "TSDM adaptive unit within the Maintenance axis.") + (directory :id "governance-unit-corrective" :ply 3 :path "docs/governance/maintenance/corrective" :description "TSDM corrective unit within the Maintenance axis.") + (directory :id "governance-unit-perfective" :ply 3 :path "docs/governance/maintenance/perfective" :description "TSDM perfective unit within the Maintenance axis.")) + (directory :id "governance-axis-planning" :ply 2 :path "docs/governance/planning" :description "TSDM Planning track." + (directory :id "governance-unit-could" :ply 3 :path "docs/governance/planning/could" :description "TSDM could unit within the Planning axis.") + (directory :id "governance-unit-must" :ply 3 :path "docs/governance/planning/must" :description "TSDM must unit within the Planning axis.") + (directory :id "governance-unit-should" :ply 3 :path "docs/governance/planning/should" :description "TSDM should unit within the Planning axis."))) + (directory :id "legal-track" :ply 2 :path "docs/legal" :description "Sub-unit for legal and licensing documentation. Contains framework\nexhibits and archival license texts." + (canonical-locations :exhibits "exhibits/" :texts "texts/")) + (directory :id "practice-unit" :ply 2 :path "docs/practice" :description "Sub-unit of the docs pillar focusing on practice.") + (directory :id "reports-unit" :ply 2 :path "docs/reports" :description "Documentation unit for all automated and manual audit reports. Classified\nby domain." + (canonical-locations :compliance "compliance/" :maintenance "maintenance/" :performance "performance/" :quality "quality/" :security "security/") + (directory :id "report-unit-compliance" :ply 3 :path "docs/reports/compliance" :description "Specialised repository for compliance findings and evidence.") + (directory :id "report-unit-maintenance" :ply 3 :path "docs/reports/maintenance" :description "Maintenance reports.") + (directory :id "report-unit-performance" :ply 3 :path "docs/reports/performance" :description "Specialised repository for performance findings and evidence.") + (directory :id "report-unit-quality" :ply 3 :path "docs/reports/quality" :description "Specialised repository for quality findings and evidence.") + (directory :id "report-unit-security" :ply 3 :path "docs/reports/security" :description "Specialised repository for security findings and evidence.")) + (directory :id "standards-unit" :ply 2 :path "docs/standards" :description "Standards unit for high-rigor verification.") + (directory :id "theory-track" :ply 2 :path "docs/theory" :description "Documentation track for domain-specific theory and research foundations.\nCategorised by discipline." + (canonical-locations :computing "computing/" :formalisms "formalisms/" :mathematics "mathematics/" :ontologies "ontologies/" :other "other/" :socio_technical "socio-technical/") + :invariants ("Theoretical claims MUST reference established academic or technical formalisms") + (directory :id "theory-unit-computing" :ply 3 :path "docs/theory/computing" :description "Theoretical foundation for computing.") + (directory :id "theory-unit-formalisms" :ply 3 :path "docs/theory/formalisms" :description "Theoretical foundation for formalisms.") + (directory :id "theory-unit-mathematics" :ply 3 :path "docs/theory/mathematics" :description "Theoretical foundation for mathematics.") + (directory :id "theory-unit-ontologies" :ply 3 :path "docs/theory/ontologies" :description "Theoretical foundation for ontologies.") + (directory :id "theory-unit-other" :ply 3 :path "docs/theory/other" :description "Theoretical foundation for other.") + (directory :id "theory-unit-socio-technical" :ply 3 :path "docs/theory/socio-technical" :description "Theoretical foundation for socio technical.")) + (directory :id "whitepapers-track" :ply 2 :path "docs/whitepapers" :description "Unit for strategic publications and whitepapers. Categorised by target\naudience: Academic, Industry, and Outreach." + (canonical-locations :academic "academic/" :industry "industry/" :outreach "outreach/") + :invariants ("Each sub-track MUST have a clear audience definition in its README") + (directory :id "academic-unit" :ply 3 :path "docs/whitepapers/academic" :description "Academic logic at level 3.") + (directory :id "industry-unit" :ply 3 :path "docs/whitepapers/industry" :description "Industry logic at level 3.") + (directory :id "whitepapers-track-outreach" :ply 3 :path "docs/whitepapers/outreach" :description "Documentation track for outreach, education, and general-audience\nengagement. Focuses on accessibility and high-level conceptual clarity." + :invariants ("Language MUST be accessible to non-technical audiences" "Avoid deep technical jargon without providing clear definitions"))) + (directory :id "wikis-track" :ply 2 :path "docs/wikis" :description "Long-form collaborative documentation and project knowledge base.\nThis directory mirrors the content structure of the project wiki." + :invariants ("Primary wiki format MUST be Markdown (.md) — owner ruling 2026-09-19: wikis are the .md home (berrywiki); everything outside wikis stays .adoc"))) + (directory :ply 1 :path "examples") + (directory :id "features-pillar" :ply 1 :path "features" :description "Optional project features and ecosystem integrations. Provides bootstrap\nguides for high-rigor tools (Panic-Attacker, BoJ-Server, SSGs)." + (canonical-locations :boj_server "boj-server/" :panic_attacker "panic-attacker/" :ssg "ssg/") + (directory :id "feature-unit-boj-server" :ply 2 :path "features/boj-server" :description "Bootstrap and integration logic for the boj-server ecosystem component.") + (directory :id "feature-unit-panic-attacker" :ply 2 :path "features/panic-attacker" :description "Bootstrap and integration logic for the panic-attacker ecosystem component.") + (directory :id "feature-unit-ssg" :ply 2 :path "features/ssg" :description "Bootstrap and integration logic for the ssg ecosystem component.")) + (directory :id "source-pillar" :ply 1 :path "src" :description "Primary source code directory. Organized by role and architectural\naspect." + (canonical-locations :aspects "aspects/" :bridges "bridges/" :contracts "contracts/" :core "core/" :definitions "definitions/" :errors "errors/" :interface "interface/") + :invariants ("Core logic MUST reside in core/" "Verified seams MUST reside in interface/" "Safety constraints MUST reside in contracts/" "Failure dictionaries MUST reside in errors/") + (directory :id "source-unit-aspects" :ply 2 :path "src/aspects" :description "Cross-cutting concerns and domain-specific aspects (Security,\nObservability, Integrity)." + (canonical-locations :integrity "integrity/" :observability "observability/" :security "security/") + (directory :id "aspect-unit-integrity" :ply 3 :path "src/aspects/integrity" :description "Implementation logic for the integrity aspect.") + (directory :id "aspect-unit-observability" :ply 3 :path "src/aspects/observability" :description "Implementation logic for the observability aspect.") + (directory :id "aspect-unit-security" :ply 3 :path "src/aspects/security" :description "Implementation logic for the security aspect.")) + (directory :id "source-unit-bridges" :ply 2 :path "src/bridges" :description "Integration logic for external systems (API, Database, RPC, etc.).") + (directory :id "source-unit-contracts" :ply 2 :path "src/contracts" :description "Contracts unit for high-rigor source code.") + (directory :id "source-unit-core" :ply 2 :path "src/core" :description "Primary application logic and core domain models.") + (directory :id "source-unit-definitions" :ply 2 :path "src/definitions" :description "Definitions unit for high-rigor source code.") + (directory :id "source-unit-errors" :ply 2 :path "src/errors" :description "Errors unit for high-rigor source code.") + (directory :id "interface-seams-unit" :ply 2 :path "src/interface" :description "Consolidated \"Verified Interface Seams\" unit. This directory unifies the\nformal specification (ABI), the bridge implementation (FFI), and the\nresulting artifacts (Generated)." + (canonical-locations :abi "abi/" :ffi "ffi/" :generated "generated/") + :invariants ("ABI MUST be Idris2 (.idr)" "FFI MUST be Zig (.zig)" "Generated artifacts MUST be C-compatible" "The 'Truth' lives in abi/; the 'Implementation' lives in ffi/") + (directory :id "abi-logic" :ply 3 :path "src/interface/Abi" :description "Specialised Level 3 logic for abi.") + (directory :id "ffi-logic" :ply 3 :path "src/interface/ffi" :description "Specialised Level 3 logic for ffi." + (directory :id "src-unit" :ply 4 :path "src/interface/ffi/src" :description "Src logic at level 4.") + (directory :id "test-unit" :ply 4 :path "src/interface/ffi/test" :description "Test logic at level 4.")) + (directory :id "generated-logic" :ply 3 :path "src/interface/generated" :description "Specialised Level 3 logic for generated." + (directory :id "abi-unit" :ply 4 :path "src/interface/generated/abi" :description "Abi logic at level 4.")))) + (directory :id "verification-pillar" :ply 1 :path "verification" :description "Primary verification pillar. Contains evidence for correctness,\nperformance, formal proofs, randomized testing, and aerospace-grade\nhigh-assurance metrics (MC/DC coverage, traceability, safety cases)." + (canonical-locations :benchmarks "benchmarks/" :coverage "coverage/" :fuzzing "fuzzing/" :proofs "proofs/" :safety_case "safety_case/" :simulations "simulations/" :tests "tests/" :traceability "traceability/") + :invariants ("Evidence MUST be reproducible and documented" "High-assurance deployments MUST satisfy traceability and safety_case requirements") + (directory :id "benches-pillar" :ply 2 :path "verification/benchmarks" :description "Benches pillar.") + (directory :id "verification-unit-coverage" :ply 2 :path "verification/coverage" :description "High-assurance verification unit for coverage. \nCritical for safety-of-life and aerospace-grade deployment standards.") + (directory :id "fuzzing-unit" :ply 2 :path "verification/fuzzing" :description "Fuzzing unit for high-rigor verification.") + (directory :id "verification-unit-proofs" :ply 2 :path "verification/proofs" :description "Sub-unit focusing on proofs.") + (directory :id "verification-unit-safety_case" :ply 2 :path "verification/safety_case" :description "High-assurance verification unit for safety case. \nCritical for safety-of-life and aerospace-grade deployment standards.") + (directory :id "simulations-unit" :ply 2 :path "verification/simulations" :description "Simulations unit for high-rigor verification.") + (directory :ply 2 :path "verification/tests") + (directory :id "verification-unit-traceability" :ply 2 :path "verification/traceability" :description "High-assurance verification unit for traceability. \nCritical for safety-of-life and aerospace-grade deployment standards.")) + ) + ; ANCHOR — the authoritative anchor: semantic authority, implementation policy + ; and the golden path. The CLAUDE.md arrival pack reads anchor/golden-path and hashes this clause (ANCHOR@…). + (anchor + :schema "hyperpolymath.anchor/1" + :repo "hyperpolymath/occupancy-types" + :authority "upstream-canonical" + :purpose ( + "Define canonical semantics and policy boundaries for this repository." + "Declare what downstream/satellite repos can extend but not redefine." + "Provide a stable golden path and invariant contract for release readiness." + ) + (metadata + :version "1.0.0" + :last-updated "2026-11-12") + (identity + :project "occupancy-types" + ; language | library | service | tool + :kind "UNASSIGNED" + :one-sentence "Early-stage research on compositional worst-case resource bounds, with a linear session-IR checker, occupancy-indexed state experiments in Idris 2, and a prototype stack-depth certificate checker. The long-term goal is predictable memory and network-buffer usage." + :domain "UNASSIGNED") + (semantic-authority + :policy "canonical" + :owns ( + "Project semantics and specification" + "Invariant definitions and contractiles" + "Reference implementation behavior" + )) + (implementation-policy + :allowed ("Rust" "Idris2" "Zig" "Scheme" "Shell" "Just" "AsciiDoc" "Markdown") + :forbidden ("Node.js" "npm")) + (golden-path + :smoke-test-command ("just test" "just quality") + :success-criteria ( + "Core tests pass" + "Quality gates pass" + "No unresolved critical security findings" + )) + (satellite-policy + :must-pin-upstream #t + :must-declare-authority #t + :must-have-anchor #t + :must-have-golden-path #t) + (semantic-authority-files + :language-spec "SPECIFICATION.md" + :formal-proofs "docs/proofs/PROOFS.adoc" + :type-theory "docs/theory/THEORY.adoc" + :algorithms "docs/theory/ALGORITHMS.adoc")) + ; NEUROSYM — neurosymbolic integration metadata: the meaning of operations. + ; Configuration for Hypatia scanning and symbolic reasoning. + (neurosym + (metadata + :version "0.1.0" + :last-updated "2026-04-11") + (hypatia-config + :scan-enabled #t + ; quick | standard | deep + :scan-depth "standard" + :report-format "logtalk") + (symbolic-rules + ; Custom symbolic rules for this project + ; (rule :name "no-unsafe-ffi" :pattern "believe_me|unsafeCoerce" :severity "critical") + ) + (neural-config + ; Neural pattern detection settings + ; confidence-threshold = 0.85 + ; model = "hypatia-v2" + )) + ; PLAYBOOK — operational playbook: how permitted actions run. + ; Runbooks, incident response, deployment procedures. + (playbook + (metadata + :version "0.1.0" + :last-updated "2026-04-11") + (deployment + ; :method "gitops" ; gitops | manual | ci-triggered + ; :target "container" ; container | binary | library | wasm + ) + (incident-response + ; 1. Check the repo deed (*_chora.deed) (status) clause and docs/status/ROADMAP.adoc for current status + ; 2. Review recent commits and CI results + ; 3. Run `just validate` to check compliance + ; 4. Run `just security` to audit for vulnerabilities + ) + (release-process + ; 1. Update version in the repo deed (meta :version), Justfile + ; 2. Run `just release-preflight` (validate + quality + security + maint-hard-pass) + ; 3. Optional local permission hardening: `just perms-snapshot && just perms-lock` + ; 4. Tag and push + ; 5. Restore local permissions if needed: `just perms-restore` + ; 6. Run `just container-push` if applicable + ) + (maintenance-operations + ; Baseline audit: + ; just maint-audit + ; Hard release gate: + ; just maint-hard-pass + ; Permission audit: + ; just perms-audit + ) + (rsr-repo-skeleton + ; Canonical organisation of any RSR-derived repository. + ; Used by tooling, human onboarding, and the scheduled downstream sweep agent. + ; The 5-PR cleanup pattern (below) brings a non-conforming repository into + ; compliance with this skeleton. + ; + ; This section is the single source of truth for "what does an RSR repo look + ; like?". Other docs (TOPOLOGY, AUDIT, etc.) describe the repo at hand; + ; this describes the canonical shape that all RSR repos share. + :skeleton-version "1.0" + :last-updated "2026-04-30" + :authority-allowlist ".machine_readable/root-allow.txt" + :enforcement-workflow ".github/workflows/estate-rules.yml")) + ; eNSAID environment configuration — per-repo configuration for PanLL and + ; eNSAID-compatible tools. + ; Spec: https://github.com/hyperpolymath/standards/tree/main/1-formats/sub-specs/ensaid-config + (ensaid + :version "1.0.0" + :tool "panll" + ; Workspace mode, protection, and execution policy + (workspace + ; rhodium | gold | silver | bronze + :mode "rhodium" + ; open | guarded | locked + :protection "open" + ; live | dry-run | approval-required + :execution "live") + ; User/repo-level display and behaviour preferences + (preferences + ; high | medium | low (drift aura intensity) + :humidity "medium" + ; workspace arrangement ID + :default-arrangement "default-3-panel" + ; auto-connect panels to backends on load + :auto-connect #t) + ; Panel visibility, enablement, and isolation overrides + (panels + :version "1.0.0" + ; By default, all panels are available. Uncomment to restrict: + ; (enabled :id "valence-shell" :isolation "native" :auto-connect #t) + ; (enabled :id "editor-bridge" :isolation "native" :auto-connect #t) + ; Panels to hide for this repo context: + ; (disabled :ids ()) + ) + ; Automation Router event-driven cross-panel rules + (workflows + :version "1.0.0" + ; Example: rebuild on file save + ; (rule :name "build-on-save" + ; (trigger :event "file-changed" :pattern "src/**/*.res") + ; (condition :panel "build-dashboard" :field "watchMode" :equals #t) + ; (action :panel "build-dashboard" :message "TriggerBuild" (args :target "game")) + ; :approval "auto-fire") ; auto-fire | require-approval | approve-once | dry-run-first + ) + ; Panel clade trait and capability overrides + (clades + :version "1.0.0" + ; Example: add a custom capability to a panel clade + ; (override :id "build-dashboard" (traits :has-work-items #t) :capabilities-add ("CustomCheck")) + ) + ; Custom panel bundles for this repo's workflow + (portfolios + :version "1.0.0")) + ; Bot directives — what agents and bots read at session start and update at + ; session end. Human guide: + ; .machine_readable/bot_directives/README.adoc. + (bot-directives + ; methodology — AI agent methodology configuration + ; Declares how agents should approach work in this repository. + ; Read at session start by any AI agent (Claude, Gemini, Copilot, etc.) + ; + ; Reference: ADR-002 (methodology layer) in hyperpolymath/standards + (methodology + (metadata + :version "1.0.0" + :last-updated "2026-11-12" + :spec "https://github.com/hyperpolymath/standards/blob/main/1-formats/a2ml/agentic/docs/ADR-002-methodology-layer.adoc") + ; ============================================================================ + ; MODE SELECTION + ; ============================================================================ + ; convergent: find gaps, fill them, build infrastructure (default for ops/infra) + ; divergent: find what's strongest, push it further (for research/creative) + ; hybrid: audit 20% of budget, then focus 80% on top MUSTs (default for most) + (methodology + :default-mode "hybrid" + ; Hard ceiling for ring expansion (0-3) + :ring-ceiling 2 + ; Max waves before requiring user "keep going" + :wave-cap 2 + ; Every session must ship code, not just designs + :spike-required #t + ; ============================================================================ + ; PRIORITY WEIGHTS + ; ============================================================================ + ; MUST (3x): Blocking the current work → fix immediately + ; SHOULD (2x): Degrading quality of current work → fix if in zone + ; COULD (1x): Improving quality of adjacent work → add to debt list + (priority-weights + :must 3 + :should 2 + :could 1) + ; ============================================================================ + ; CONVERGENT BUDGET (when mode = convergent or hybrid) + ; ============================================================================ + ; How to allocate effort across work types. + ; Prevents over-polishing docs while structural work waits. + (convergent-budget + ; % for new modules, compilation fixes, wiring, integration + :structural 70 + ; % for bugs found, broken imports, stale references + :corrective 20 + ; % for SPDX headers, doc updates, formatting, style + :perfective 10) + ; ============================================================================ + ; UNIQUE STRENGTH (when mode = divergent) + ; ============================================================================ + ; What makes this project special. Agents should DEEPEN this, not broaden it. + ; Customise this per project — the template default is generic. + (unique-strength + :description "UNASSIGNED" + :deepen-not-broaden #t) + ; ============================================================================ + ; DIVERGENT INVARIANTS + ; ============================================================================ + ; Constraints that divergent mode must NOT violate. + ; These are the riverbanks — diverge within them, not across. + ; "Amplify uniqueness" means deepen, not broaden. + ; + ; Test before any divergent action: + ; "Does this deepen the existing strength, or add a parallel strength?" + ; If parallel → stop. Note as cross-project insight. + (divergent-invariants + ; Customise per project. Examples: + ; "Idris2 only for formal verification — no Lean4, Coq, Agda", + ; "believe_me count must remain zero", + ; "FFI architecture: Idris2 → RefC → Zig → C ABI (no shortcuts)", + :rules ()) + ; Optional: language invariant for the core strength + ; If set, divergent mode will not introduce other languages for this purpose + ; language-invariant = "idris2" + ; ============================================================================ + ; CONSTRAINT HINTS + ; ============================================================================ + ; Help Phase 0 find the critical chain faster. + ; Updated at session end with newly discovered constraints. + (known-constraints + ; Customise per project. Examples: + ; "End-to-end build has never been verified", + ; "libproject.so does not exist yet — all bindings call stubs", + :constraints ()) + ; ============================================================================ + ; STATE FILE VALIDATION + ; ============================================================================ + ; Phase 0 reads the repo deed (status) clause and docs/status/ROADMAP.adoc first, + ; but they may be broken. + ; These rules detect corrupt/template/stale state files. + (state-validation + ; A regex, not a literal list. The list form named the exact strings it was + ; meant to catch ("OCCUPANCY_TYPES"), so `just repo-init` substituted the detector along + ; with everything else and shipped reject-if-contains = ["MY_PROJECT", ...] — + ; a guard against unfilled tokens that had been eaten by the filling. The + ; escaped pattern below contains no token for init to match, and covers every + ; upper-snake token rather than the three anyone remembered to enumerate. + ; Enforced by scripts/check-no-placeholders.sh. + :reject-if-matches ("\\{\\{[A-Z][A-Z0-9_]*\\}\\}") + :reject-if-contains ("rsr-template-repo") + :reject-if-project-name-mismatch #t + :staleness-threshold-days 90 + :fallback-files ("TODO.md" "TODO.adoc" "ROADMAP.adoc" "README.adoc")))) + ; coverage — session coverage tracking + ; Updated at the end of each AI agent session. + ; Persists what was visited, what was skipped, and what has MUSTs. + ; + ; Reference: ADR-002 (methodology layer) in hyperpolymath/standards + (coverage + (metadata + :version "1.0.0" + :last-updated "2026-11-12") + ; ============================================================================ + ; COVERAGE STATE + ; ============================================================================ + ; Updated by agents at session end. Tracks which components have been + ; visited and which have known MUSTs that were skipped. + (coverage + :total-components 0 + :visited-components 0 + :coverage-percent 0)) + ; debt — Meander debt list + ; Things found but not fixed. Carried between sessions. + ; Becomes the next session's Phase 0 input. + ; + ; Reference: ADR-002 (methodology layer) in hyperpolymath/standards + (debt + (metadata + :version "1.0.0" + :last-updated "2026-11-12")) + ; rra — directives for Robot Repo Automaton (RRA) enforcing the Platonic Ideal root. + (rra + :target "robot-repo-automaton" + (module + :name "root-cleaner" + ; mode can be "enforce" (auto-commit) or "putative" (write to /.auto-rr/ for human review) + (policy + :mode "putative" + :strictness "high") + ; Files that are strictly allowed to exist at the root. CLAUDE.md is the + ; generated arrival pack; git/Claude/pre-commit all read it from the ROOT. + ; The pattern ".*" allows hidden dotfiles implicitly; RRA checks specific + ; dotfile rules below. + (allow-list + :files ("README.adoc" "CHANGELOG.adoc" "Justfile" "LICENSE" "occupancy-types_chora.deed" "CITATION.cff" "CLAUDE.md") + :patterns (".*")) + ; Relocation rules: move heavy prose out of the root + (relocations + (move :pattern "*.adoc" :dest "docs/" :except ("README.adoc" "CHANGELOG.adoc")) + (move :pattern "GOVERNANCE.*" :dest "docs/") + (move :pattern "ARCHITECTURE.*" :dest "docs/") + (move :file "MAINTAINERS.adoc" :dest "docs/")) + ; Consolidation rules: reduce the dotfile "wall of noise" + (consolidations + ; Merge legacy environment trackers into mise + (merge :sources (".envrc" ".tool-versions") :engine "mise-translator" :dest ".config/mise.toml") + ; Ensure standard git attributes are consolidated + (merge :sources (".gitattributes-legacy" ".gitattributes-os") :dest ".gitattributes")) + ; Explicitly ignore certain paths from being cleaned + (ignores + :dirs ("src/" "tests/" ".git/" "docs/" ".machine_readable/"))))) + ; Optional ecosystem integrations. + ; Each clause is opt-in; delete the ones your project does not use. + (integrations + ; OPTIONAL: Feedback-o-Tron Integration — Autonomous Bug Reporting + ; Delete this clause if your project does not use feedback-o-tron. + (feedback-o-tron + (integration + :name "feedback-o-tron" + :type "bug-reporter" + :repository "https://github.com/hyperpolymath/feedback-o-tron") + (reporting-config + :platforms ("github" "gitlab" "bugzilla") + :deduplication #t + :audit-logging #t + :auto-file-upstream "on-external-dependency-failure")) + ; OPTIONAL: Proven Integration — Formally Verified Safety Library + ; Delete this clause if your project does not use the proven library. + ; See https://github.com/hyperpolymath/proven for details. + (proven + (integration + :name "proven" + :type "safety-library" + :repository "https://github.com/hyperpolymath/proven" + :version "1.2.0") + (binding-policy + :approach "thin-ffi-wrapper" + :unsafe-patterns "replace-with-proven-equivalent" + :modules-available ( + "SafeMath" + "SafeString" + "SafeJSON" + "SafeURL" + "SafeRegex" + "SafeSQL" + "SafeFile" + "SafeTemplate" + "SafeCrypto" + )) + (adoption-guidance + :priority "high" + :scope "all-string-json-url-crypto-operations" + :migration "incremental — replace unsafe patterns as encountered")) + ; OPTIONAL: VeriSimDB Feed — Cross-Repo Analytics Data Store + ; Delete this clause if your project does not feed data to VeriSimDB. + ; See https://github.com/hyperpolymath/nextgen-databases for details. + (verisimdb + (integration + :name "verisimdb" + :type "data-feed" + :repository "https://github.com/hyperpolymath/nextgen-databases" + :data-store "verisimdb-data") + (feed-config + :emit-scan-results #t + :emit-build-metrics #t + :emit-dependency-graph #t + :format "hexad" + :destination "verisimdb-data/feeds/")) + ; OPTIONAL: Vexometer Integration — Irritation Surface Analysis + ; Delete this clause if your project does not use vexometer. + (vexometer + (integration + :name "vexometer" + :type "friction-measurement" + :repository "https://github.com/hyperpolymath/vexometer") + (measurement-config + :dimensions 10 + :emit-isa-reports #t + :lazy-eliminator #t + :satellite-interventions #t) + (hooks + :cli-tools "measure-on-error" + :ui-panels "measure-on-interaction" + :build-failures "measure-on-failure")) + ; groove — Groove Protocol manifest: the API surfaces this project exposes + ; Consumed by the Groove bridge / + ; zig-unified-api-adapter for snap-on/snap-off service discovery. Edit it to + ; match your project's actual APIs; `just groove-setup` writes :port and the + ; :enabled flags through scripts/deed-clause.sh. + ; See: https://github.com/hyperpolymath/standards/tree/main/groove-protocol + (groove + :version "1.0" + ; Service identity — replace occupancy-types with your project name + :service "occupancy-types" + :service-version "0.1.0" + ; Primary port — MUST be unique across the ecosystem. Check PORT-REGISTRY.md + ; in the standards repo before assigning: + ; https://github.com/hyperpolymath/standards/blob/main/PORT-REGISTRY.md + ; 0 = not assigned yet — run `just groove-setup` to assign + :port 0 + ; Health endpoint — used by Groove discovery + :health "/health" + ; Capabilities — what this service can do for others + :capabilities () + ; Dependencies — what this service needs from others + :dependencies () + ; API surfaces this project exposes (dodeca-API). Remove the ones you don't use. + (api-surfaces + (rest :enabled #t :path "/api/v1") + (grpc :enabled #f :port-offset 1) + (graphql :enabled #f :path "/graphql") + (websocket :enabled #f :path "/ws") + (sse :enabled #f :path "/events") + (groove :enabled #t :path "/.well-known/groove")))) + ; Maintenance and development policies, machine-readable canonical. + ; Human companions live in + ; docs/governance/. + (policies + ; Canonical maintenance governance model + (maintenance-axes + (metadata + :version "1.0.0" + :last-updated "2026-11-12" + :scope "repo") + (discovery + :human-entrypoints ( + "README.adoc" + "docs/governance/MAINTENANCE-CHECKLIST.adoc" + "docs/governance/SOFTWARE-DEVELOPMENT-APPROACH.adoc" + ) + :machine-entrypoints ( + "*_chora.deed (policies (maintenance-axes))" + "*_chora.deed (policies (maintenance-checklist))" + "*_chora.deed (policies (software-development-approach))" + "*_chora.deed" + ".machine_readable/ai/README.adoc" + "*_chora.deed (bot-directives)" + ) + :bots ("hypatia" "gitbot-fleet" "repo visitors")) + (axes + :axis-1 "must > intend > like" + :axis-2 "corrective > adaptive > perfective" + :axis-3 "systems > compliance > effects" + :execution-order "axis-1 > axis-2 > axis-3") + (axis-1-scoping + :required #t + :sources "README, roadmap, status docs, maintenance checklist, CI/security docs" + :markers "TODO/FIXME/XXX/HACK/STUB/PARTIAL" + :idris-unsound-markers "believe_me/assert_total" + :output "scoped work assembly in must/intend/like buckets") + (axis-2-maintenance + :corrective-first #t + :adaptive-second #t + :adaptive-focus "scope changes, stale references, obsolete work culling" + :perfective-third #t + :perfective-source "honest state from axis-1 after corrective/adaptive updates") + (axis-3-audit + :systems-check #t + :compliance-check #t + :effects-check #t + :compliance-focus "seams/compromises/exception register and anti-drift" + :compliance-tooling "panic-attack" + :effects-tooling "ecological checking with sustainabot guidance" + :effects-evidence "benchmark evidence and maintainer dialogue/status review")) + ; Cross-repo maintenance baseline (machine-readable canonical) + (maintenance-checklist + (metadata + :version "1.1.0" + :last-updated "2026-02-24" + :scope "cross-repo" + :source-human "docs/governance/MAINTENANCE-CHECKLIST.adoc" + :companion-human "docs/governance/SOFTWARE-DEVELOPMENT-APPROACH.adoc" + :companion-machine "*_chora.deed (policies (software-development-approach))") + (policy + :single-source #t + :notes "Use this clause as canonical machine policy and keep the AsciiDoc synchronized.") + (maintenance-axes + :scoping-first #t + :execution-order ("scoping" "axis-1" "axis-2" "axis-3") + :axis-1 "must > intend > like" + :axis-2 "corrective > adaptive > perfective" + :axis-3 "systems > compliance > effects") + (scoping + :inputs_required ( + "README" + "roadmap" + "status-docs" + "maintenance-checklist" + "ci-and-security-docs" + ) + :marker_scan_required ("TODO" "FIXME" "XXX" "HACK" "STUB" "PARTIAL") + :idris_unsound_scan_required ("believe_me" "assert_total") + :scope_assembly_buckets ("must" "intend" "like")) + (axis-2-maintenance-rules + :corrective-first #t + :adaptive-second #t + :adaptive_examples ( + "scope-change reconciliation" + "stale-reference removal" + "obsolete-work culling" + ) + :perfective-third #t + :perfective_source "axis-1 honest state after corrective/adaptive updates") + (axis-3-audit-rules + :systems-check #t + :documentation-honesty-check #t + :safety-security-accounted-check #t + :effects-review-check #t + :benchmark-evidence-required #t + :maintainer-dialogue-review-required #t + :compliance-seams-check #t + :exception-register-required #t + :exception-bounded-scope-required #t + :policy-drift-contamination-check #t + :example-drift-risk "single TypeScript exception causing broad ->TypeScript migration" + :compliance-tooling "panic-attack" + :effects-tooling "ecological checking with sustainabot guidance") + (generic-cleanup-finish-off + :root-cleanup-required #t + :stale-work-cull-required #t + :docs-parity-required #t + :machine-human-sync-required #t + :compliance-finish-off-required #t + :effects-finish-off-required #t + :release-prep-summary-required #t + :next-actions-required ("corrective" "adaptive" "perfective")) + (must + :root_control_files ( + ".gitignore" + ".gitattributes" + ".editorconfig" + ".tool-versions" + "Containerfile" + "Justfile" + ) + :root_hosting_files ("CNAME" ".nojekyll") + :ownership_files ("MAINTAINER" ".github/CODEOWNERS") + :machine_readable_required ( + "*_chora.deed (anchor)" + ".machine_readable/contractiles/" + ".machine_readable/ai/" + ".machine_readable/bot_directives/" + ) + :contractiles_required ( + ".machine_readable/contractiles/must/" + ".machine_readable/contractiles/trust/" + ".machine_readable/contractiles/intend/" + ) + :security_required ("www/.well-known/security.txt" "ci-security-scan") + :quality_gate_required ( + "format" + "lint" + "unit-tests" + "integration-tests" + "p2p-tests" + "e2e-tests" + "bench-smoke" + "docs-check" + "security-scan" + ) + :abi_ffi_policy ("ABI Idris2 in src/interface/abi/*.idr" "FFI Zig in ffi/**/*.zig")) + (should + :docs_primary_format "adoc" + :docs_structure ( + "docs/theory" + "docs/practice" + "docs/whitepapers/academic" + "docs/whitepapers/industry" + "docs/proofs" + "docs/reports" + ) + :root_minimization #t + :well_known_metadata #t + :roadmap_honesty_with_dates #t + :ci_doc_format_policy #t) + (could + :generate_human_from_machine #t + :mode_aware_bots ("corrective" "adaptive" "perfective" "audit") + :topology_dashboard #t + :exception_registry #t)) + ; General software development approach (machine-readable) + (software-development-approach + (metadata + :version "1.0.0" + :last-updated "2026-02-24" + :scope "cross-repo" + :source-human "docs/governance/SOFTWARE-DEVELOPMENT-APPROACH.adoc") + (execution + :order ("axis-1" "axis-2" "axis-3")) + (axis-1 + :name "scope" + :priority "must > intend > like" + :inputs ("README" "roadmap" "status-docs" "ci-and-security-docs") + :marker-scan ("TODO" "FIXME" "XXX" "HACK" "STUB" "PARTIAL") + :idris-unsound-scan ("believe_me" "assert_total") + :output "scoped-work-assembly") + (axis-2 + :name "maintenance" + :priority "corrective > adaptive > perfective" + :corrective "defect/regression/safety/security fixes" + :adaptive "scope reconciliation, stale-reference removal, obsolete-work culling" + :perfective "quality improvements derived from axis-1 honest state") + (axis-3 + :name "audit" + :priority "systems > compliance > effects" + :systems "required systems present and operating" + :compliance "exceptions explicit, bounded, and drift-resistant" + :effects "benchmark/operational impact evidence captured and reviewed" + :compliance-tooling "panic-attack" + :effects-tooling "ecological checking with sustainabot guidance") + (cleanup-finish-off + :root-cleanup #t + :stale-work-cull #t + :docs-sync-human-machine #t + :compliance-audit #t + :effects-audit #t + :release-summary ("must" "should" "could") + :next-actions ("corrective" "adaptive" "perfective")) + (collaboration + :maintainer-dialogue-required #t + :dialogue-topics ("what changed" "why" "remaining risks"))) + ; Component Readiness Grades (CRG v1.0) — the machine-readable grade + ; standard; the human companion is docs/governance/CRG-CRITERIA.adoc. + (crg-criteria + (standard + :name "Component Readiness Grades" + :abbreviation "CRG" + :version "1.0" + :date "2026-02-28" + :author "Jonathan D.A. Jewell " + :license "MPL-2.0" + :family "RSR") + (grades + (grade :code "X" :name "Untested" :release-stage #f :ordinal 0 + :description "No testing has been performed. Status unknown." + :evidence-required "none" + :minimum-for #f) + (grade :code "F" :name "Harmful / Wasteful" :release-stage #f :ordinal 1 + :description "Actively harmful, wasteful, or better handled externally. Reject, deprecate, or delegate." + :evidence-required "documented test results showing harm, waste, or redundancy; comparison with alternatives" + :minimum-for #f) + (grade :code "E" :name "Minimal / Salvageable" :release-stage "pre-alpha" :ordinal 2 + :description "Does something slight. Barely functional. Needs redesign or major work." + :evidence-required "at least one successful test case; documented failures and limitations" + :minimum-for #f) + (grade :code "D" :name "Partial / Inconsistent" :release-stage "alpha" :ordinal 3 + :description "Works on some things but not systematically." + :evidence-required "matrix of tested scenarios; documented scope vs actual capabilities" + :minimum-for "alpha") + (grade :code "C" :name "Self-Validated" :release-stage "beta" :ordinal 4 + :description "Tested on the tool/project itself (dogfooding). Reliable in home context." + :evidence-required "active dogfooding; CI integration or equivalent; no known failures in home context" + :minimum-for "beta") + (grade :code "B" :name "Broadly Validated" :release-stage "release-candidate" :ordinal 5 + :description "Tested on at least 6 disparate, unrelated targets." + :evidence-required "list of 6+ diverse targets with test results; evidence of feedback incorporation" + :minimum-for "release-candidate") + (grade :code "A" :name "Field-Proven" :release-stage "stable" :ordinal 6 + :description "Real-world external feedback confirms value. Does no harm in the wild." + :evidence-required "real-world usage data; feedback incorporation evidence; no unresolved harm reports" + :minimum-for "stable")) + (transitions + (promotion :from "X" :to "E" :requirement "Run at least one test. Document results.") + (promotion :from "X" :to "F" :requirement "Evaluate and determine harmful or wasteful.") + (promotion :from "E" :to "D" :requirement "Fix critical failures. Document scope.") + (promotion :from "D" :to "C" :requirement "Dogfood on own project. Fix what breaks.") + (promotion :from "C" :to "B" :requirement "Test on 6+ diverse external targets. Fix what breaks.") + (promotion :from "B" :to "A" :requirement "Ship. Collect external feedback. Demonstrate no harm.") + (demotion :from "A" :to "B" :trigger "External feedback dries up or reveals no longer useful.") + (demotion :from "A" :to "F" :trigger "External feedback reveals component causes harm.") + (demotion :from "B" :to "C" :trigger "Broad validation reveals unfixed failures.") + (demotion :from "C" :to "D" :trigger "Home context changes and component no longer reliable.") + (demotion :from "C" :to "F" :trigger "Dogfooding reveals net negative.") + (demotion :from "D" :to "E" :trigger "Scope narrows to barely functional.") + (demotion :from "any" :to "F" :trigger "Better external alternative makes this pure opportunity cost.")) + (conformance + :rules ( + "Each assessable component MUST have a grade from {X, F, E, D, C, B, A}." + "Each grade above X MUST be supported by evidence per section 4." + "Assessments MUST be recorded in a version-controlled location." + "Assessments MUST be reviewed at least once per release cycle." + "Release stages MUST respect minimum grade thresholds."))) + ; Triaxial Software Development Methodology — the machine-readable axes; + ; the human companion is docs/governance/TSDM.adoc. + (tsdm + :id "tsdm-standard" + :version "1.0.0" + (axes + (axis :ordinal 1 :name "Planning" :levels ("must" "should" "could")) + (axis :ordinal 2 :name "Maintenance" :levels ("corrective" "adaptive" "perfective")) + (axis :ordinal 3 :name "Audit" :levels ("systems" "compliance" "effects"))) + :invariants ( + "Every task MUST map to at least one TSDM coordinate" + "Axis 1 priority governs resource allocation" + "Axis 2 type governs commit categorisation" + "Axis 3 focus governs audit depth"))) + ; rsr-profile — this repo's declared RSR v2.0 capabilities. + ; Written by just repo-init on 2026-11-12. The template's maximal + ; profile was replaced at mint: declare only what this tree carries. + (rsr-profile + :version "1.0.0" + :spec "rsr-criteria-v2" + :declares-against "2.1.1-draft" + ; UNASSIGNED: declare capabilities (or a preset) — see standards + ; .machine_readable/template-capability-gates.toml + :capabilities ()) + ; Egress attestation (standards ADR-009; grammar + ; 1-formats/deed/spec/abnf/clauses/egress.abnf). Every job that reads a real + ; secret (anything but GITHUB_TOKEN) is listed with exactly the secrets it + ; reads, the endpoints outside GitHub it reaches, and what holds it to them. + ; Hypatia RE001 accepts an entry here in place of step-security/harden-runner + ; and reports drift between an entry and its workflow. A minted repo inherits + ; these entries with the workflows; when you add secrets.* to a job, or + ; delete a workflow listed here, change this clause in the same commit. + (egress + :policy "standards ADR-009" + (job + :workflow ".github/workflows/push-email-notify.yml" + :job "notify" + :secrets ("SMTP_HOST" "SMTP_PORT" "SMTP_USER" "SMTP_PASS") + :endpoints ("secret:SMTP_HOST") + :enforcement none + :reason "Sends the push notification by SMTP submission to the org mail host, whose name is itself a secret.") + (job + :workflow ".github/workflows/sonarqube.yml" + :job "sonarqube" + :secrets ("SONAR_TOKEN") + :endpoints ("sonarcloud.io" "*.sonarcloud.io" "binaries.sonarsource.com" "keyserver.ubuntu.com" "keys.openpgp.org") + :enforcement none + :reason "Uploads the CI-based analysis to SonarQube Cloud. Skipped when SONAR_TOKEN is unset. SonarSource/sonarqube-scan-action v8.3.0 downloads the scanner from binaries.sonarsource.com and, with signature verification on (its default), fetches the signing key from keyserver.ubuntu.com, falling back to keys.openpgp.org.") + (job + :workflow ".github/workflows/build-notification.yml" + :job "trigger-boj" + :secrets ("BOJ_SERVER_URL") + :endpoints ("secret:BOJ_SERVER_URL" "var:BOJ_SERVER_URL") + :enforcement none + :reason "Notifies the BoJ server of a push. The URL is the secret when set, else the variable that also gates the job.")) + ; variant — provenance contract (ADR-0002 shape; ADR-0003 use). + ; Records what this repo was minted from, for re-templating tooling and + ; the drift gate. For a minted repo the gate replays the mint from + ; (provenance) and (mint-answers) and diffs this tree against the + ; replay (scripts/check-mint-drift.sh), so only edits made after the + ; mint count. Declare those in (paths) below; the derived files are + ; declared already, since their own gates keep them fresh. + (variant + :project "occupancy-types" + :schema-version "0.1.0" + :last-updated "2026-11-12" + :parent "hyperpolymath/rsr-template-repo" + ; template tip at mint + :parent-pin "840ac2509f28a176fe7e2c991b909375654164dc" + :role "minted-repo" + :stack "unspecified" + :direction "generated-from-core" + (normalise :rules "action-pins self-name") + ; Files a minted repo owns from birth are (paths :diverged …). + (paths + :added ("examples/session_ir/" "src/OccupancyTypes/" "src/julia/" "src/occ/" "src/stackcert/" "tests/reject/" ".github/workflows/lean4-manifest.yml" ".github/workflows/proofs.yml" ".github/workflows/session-ir.yml" "PROOF-STATUS.adoc" "ULTRAPLAN.md" "docs/DECISION-001-python-stepper.adoc" "docs/OPERATIONAL-MODEL.md" "docs/glossary.adoc" "docs/recon/A1-ACTIONS-RUNBOOK.adoc" "docs/recon/COORDINATED-PLAN-2026-10-04.adoc" "docs/recon/TYPE-FAMILY-POSITION-2026-10-04.adoc" "docs/recon/ULTRA-PLAN-2026-10-04.adoc" "docs/reports/session-2026-09-27.adoc" "docs/retraction-ledger.adoc" "docs/status/HYPATIA-GATE.adoc" "occupancy-types.agda-lib" "scripts/check-agda.sh" "scripts/check.sh" "scripts/fetch-siblings.sh" "scripts/kill-test-audit.jl" "scripts/scan-postulates.pl" "tests/check_agda_find_failure_test.sh" "tests/run_stackcert.sh" "tests/scan_postulates_test.sh" "verification/proofs/lean4/CostStateSeparation.lean") + :removed ("archetypes/" "build/docs-seed/" "build/templates/" "rsr-template-repo_chora.deed") + :diverged ("occupancy-types_chora.deed" "docs/status/ROADMAP.adoc" "CLAUDE.md" "docs/architecture/REPOSITORY-MAP.adoc" "README.adoc" "CITATION.cff" "CHANGELOG.adoc" "LICENSE" "Justfile" ".gitignore" ".machine_readable/root-allow.txt" ".github/workflows/actions.lock" ".github/workflows/push-email-notify.yml" ".github/workflows/release.yml" ".github/workflows/sonarqube.yml" "docs/EXPLAINME.adoc" "scripts/check-no-md-in-docs.sh" "verification/proofs/idris2/MANIFEST" "verification/proofs/lean4/MANIFEST"))) + ; provenance — what this repo was minted FROM. Written once, at mint, + ; by build/just/repo-init.just. Tools that want "what is this repo" read + ; the rest of the deed; tools that want "where did it come from" read this. + (provenance + :minted-at "2026-11-12" + :template-repo "hyperpolymath/rsr-template-repo" + :template-branch "main" + :template-commit "840ac2509f28a176fe7e2c991b909375654164dc" + :template-tree "a7e2cb8e1d6e868ed56f00effefb6417b396f6a7" + ; Contract: branches this repo is EXPECTED to carry beyond the default. + ; Empty means default-only. A mint must never inherit template work + ; branches (coderabbit/, chore/, bot-task or stale branches) — that is + ; how knot-knot received a byte-identical copy of a template work branch + ; with no common ancestor (#203). Enforced by + ; scripts/check-template-conformance.sh. + :extra-branches () + :canon-version "2.1.5" + :criteria-sha256 "6a5aa8857bd0d0d58ef48827938ca17c251b6b854dacf59d306388d61694d82a" + :gates-sha256 "5c510bba025be718deebb12d0333bfe7db2da43c66578b728b1a334df65290b1" + :archetype "" + ; Which minting path actually ran (#201/#203 needed this signal). + :minted-by "repo-init" + :mint-mode "interactive" + ; A non-empty :unassigned list means the mint did not fully render; + ; the validation step below already fails on a leftover token. + (substitutions :rendered 34 :unassigned ())) + ; mint-answers — what just repo-init was answered on 2026-11-12. + ; Read by scripts/check-mint-drift.sh to replay the mint. Every value + ; here already appears in this repo's rendered files. + (mint-answers + :project-name "occupancy-types" + :repo "occupancy-types" + :owner "hyperpolymath" + :author "Jonathan D.A. Jewell" + :author-email "j.d.a.jewell@open.ac.uk" + :author-org "" + :author-email-alt "" + :author-orcid "" + :description "Early-stage research on compositional worst-case resource bounds, with a linear session-IR checker, occupancy-indexed state experiments in Idris 2, and a prototype stack-depth certificate checker. The long-term goal is predictable memory and network-buffer usage." + :forge "github.com" + :security-email "j.d.a.jewell@open.ac.uk" + :conduct-email "j.d.a.jewell@open.ac.uk" + :project-type "library" + :website "https://github.com/hyperpolymath/occupancy-types" + :openssf-bp-id "" + :service-name "occupancy-types" + :port "8080" + :registry "ghcr.io/hyperpolymath" + :archetype "" + :package-uuid "" + :mint-date "2026-11-12") +) diff --git a/rsr-template-repo_chora.deed b/rsr-template-repo_chora.deed deleted file mode 100644 index 4248ef8..0000000 --- a/rsr-template-repo_chora.deed +++ /dev/null @@ -1,152 +0,0 @@ -;; SPDX-License-Identifier: MPL-2.0 -; -; The repo deed of rsr-template-repo: one-deed-per-repo doctrine (standards#837, -; owner ruling 2026-09-19, Option A). Folds the former ply manifest tree -; (0.N-AI-MANIFEST.a2ml, 82 directories) and the family-7 AI -; allocation manifest (root 0-AI-MANIFEST.a2ml) into deed clauses. -; Generated by the #837 pilot emitter; every byte gated by deed_lint.py. -(repo-deed - :schema-version "1.0.0" - :canonical-name "rsr-template-repo" - :beholding-chora #u5"estate/chora" - :repo-uuid #u5"github.com/hyperpolymath/rsr-template-repo" - (manifest - :version "0.1.0" - :last-updated "{{CURRENT_DATE}}" - :project "[YOUR-REPO-NAME]" - :purpose "Canonical RSR (Rhodium Standard Repository) template — governance, CI/CD, machine-readable metadata, ABI/FFI seam and formal-verification scaffolding that hyperpolymath projects are instantiated from." - (agent :name CLAUDE :role "proofs, compilers, repo-local implementation, CI/CD, formal verification, Rust/Idris2/Zig") - (agent :name CHATGPT :role "prose, papers, publication review, standards docs, outreach drafts, letters") - (agent :name GEMINI :role "estate audits, cross-repo sweeps, long-context triage, pattern detection") - (agent :name VIBE :role "UI/frontend, PanLL panels, components, theming, rapid prototyping") - (policy :rules ("Do not duplicate tasks across sections. If a task needs multiple LLMs, note the handoff." "Update THIS file during sessions. Do NOT recreate per-repo TODO files.")) - (work :agent CLAUDE :task "proofs, compilers, repo-local implementation, CI/CD, formal verification, Rust/Idris2/Zig") - (work :agent CHATGPT :task "prose, papers, publication review, standards docs, outreach drafts, letters") - (work :agent GEMINI :task "estate audits, cross-repo sweeps, long-context triage, pattern detection") - (work :agent VIBE :task "UI/frontend, PanLL panels, components, theming, rapid prototyping") - ) - (ply - (directory :ply 1 :path ".github") - (directory :id "machine-readable-pillar" :ply 1 :path ".machine_readable" :description "Registry for all machine-readable metadata, policies, and internal\nautomation scripts." - (canonical-locations :agentic "descriptiles/AGENTIC.a2ml" :ai_configs "ai/" :anchors "descriptiles/anchors/" :clade "descriptiles/CLADE.a2ml" :compliance "compliance/" :ecosystem "descriptiles/ECOSYSTEM.a2ml" :meta "descriptiles/META.a2ml" :neurosym "descriptiles/NEUROSYM.a2ml" :playbook "descriptiles/PLAYBOOK.a2ml" :policies "policies/" :scripts "scripts/" :state "descriptiles/STATE.a2ml") - :invariants ("Metadata files MUST follow a2ml format" "Internal automation MUST live in scripts/ subfolder") - (directory :id "ai-registry" :ply 2 :path ".machine_readable/ai" :description "Sub-registry for ai metadata.") - (directory :id "configs-registry" :ply 2 :path ".machine_readable/configs" :description "Sub-registry for configs metadata.") - (directory :ply 0 :path ".machine_readable/descriptiles" :description "This manifest declares the AI-assistant context for the descriptiles machine-readable metadata directory." - :invariants ("No duplicate files in root directory" "Single source of truth: this directory is authoritative" "No stale metadata") - (directory :ply 0 :path ".machine_readable/descriptiles/anchors" :description "This manifest declares the AI-assistant context for the anchor machine-readable metadata directory." - :invariants ("Multiple versions with different dates are permitted" "No other A2ML files in this directory" "Single source of truth for anchor documents"))) - (directory :id "policies-registry" :ply 2 :path ".machine_readable/policies" :description "Sub-registry for policies metadata.") - (directory :id "automation-scripts-unit" :ply 2 :path ".machine_readable/scripts" :description "Internal automation logic for the project lifecycle, forge sync,\nverification triggers, and maintenance." - (canonical-locations :forge "forge/" :lifecycle "lifecycle/" :maintenance "maintenance/" :verification "verification/") - (directory :id "automation-unit-forge" :ply 3 :path ".machine_readable/scripts/forge" :description "Internal automation logic for project forge.") - (directory :id "automation-unit-lifecycle" :ply 3 :path ".machine_readable/scripts/lifecycle" :description "Internal automation logic for project lifecycle.") - (directory :id "automation-unit-verification" :ply 3 :path ".machine_readable/scripts/verification" :description "Internal automation logic for project verification."))) - (directory :id "container-templates" :ply 1 :path "build/container" :version "1.0.0" :description "Container templates for the stapeln container ecosystem. This directory\nprovides Podman-Chainguard-stapeln templates that are customised via\n`just container-init` or `just repo-init` during project bootstrap.\n\nAll files use {{PLACEHOLDER}} tokens that are substituted with project-\nspecific values during initialisation." :purpose "Provide a complete, security-first container deployment story for any\nRSR-compliant repository. The templates cover the full lifecycle:\nbuild, sign, verify, deploy, monitor, and govern." :overview "The stapeln container ecosystem comprises six tools:\n\nselur — Container orchestration with zero-copy IPC. Reads compose.toml.\ncerro-torre — Verified container packaging (.ctp bundles), Ed25519 signing.\nsvalinn — Policy-driven edge gateway (auth, rate limits, CORS, trust).\nvordr — Runtime monitoring (health, crashes, resources, logs).\nrokur — Secrets management (runtime injection, no baked secrets).\nk9-svc — Nickel deployment components (Kennel/Yard/Hunt trust levels)." - :context ("https://a2ml.org/ns/v2" "https://stapeln.dev/ns/v1") - (canonical-locations :build_pipeline "build/container/stapeln/ct-build.sh" :compose "build/container/stapeln/compose.toml" :compose_example "build/container/stapeln/compose.example.toml" :compose_portable "build/container/compose.yaml" :compose_portable_example "build/container/compose.example.yaml" :containerfile "build/container/Containerfile" :containerignore "build/container/.containerignore" :deployment "build/container/stapeln/deploy.k9.ncl" :entrypoint "build/container/entrypoint.sh" :gatekeeper "build/container/stapeln/.gatekeeper.yaml" :just_module "build/just/container.just" :manifest "build/container/stapeln/manifest.toml" :monitoring "build/container/stapeln/vordr.toml" :secrets_gate "build/container/stapeln/rokur.toml") - :invariants ("Base images MUST be cgr.dev/chainguard/wolfi-base or cgr.dev/chainguard/static" "Container runtime is Podman — never Docker" "Containerfile — never Dockerfile" "All images run as non-root (appuser or project-specific user)" ".ctp bundles are signed with Ed25519 via cerro-torre" "Health endpoints (/health, /ready) must always be public (no auth)") - (file-relationships - (file :name "compose.toml" :role "Orchestration" :description "selur-compose stack definition. Declares services, volumes, networks,\nand health checks. References the Containerfile for image builds and\n.gatekeeper.yaml for svalinn policy." :depends-on ("Containerfile" ".gatekeeper.yaml")) - (file :name "Containerfile" :role "Image Build (Tier A)" :description "Multi-stage OCI container build. Stage 1 compiles the application on\nwolfi-base; Stage 2 copies the binary into a minimal runtime image and\ncopies entrypoint.sh. Engine-agnostic (podman/nerdctl/docker). The\nstapeln files (.gatekeeper.yaml, manifest.toml) are referenced only as\nOCI labels and copied in only if you uncomment those COPY lines, so the\nimage has no hard dependency on tier C." :depends-on ("entrypoint.sh")) - (file :name "manifest.toml" :role "Bundle Metadata" :description "Cerro-torre .ctp bundle manifest. Describes provenance, dependencies,\nattestations, and runtime security profile. Used by `ct pack` and\n`ct verify`." :depends-on ()) - (file :name ".gatekeeper.yaml" :role "Gateway Policy" :description "Svalinn edge gateway policy. Controls authentication, rate limiting,\ncontainer trust, request validation, CORS, and audit logging." :depends-on ()) - (file :name "ct-build.sh" :role "Build Pipeline" :description "Shell script implementing the 5-stage pipeline: build (Podman),\npack (cerro-torre .ctp), sign (Ed25519), verify, push (optional).\nDegrades gracefully when cerro-torre tools are not installed." :depends-on ("Containerfile" "manifest.toml")) - (file :name "entrypoint.sh" :role "Container Entrypoint" :description "Startup script with signal handling (SIGTERM, SIGINT), logging, and\nexec into the main application process." :depends-on ()) - (file :name "vordr.toml" :role "Runtime Monitoring" :description "Vordr monitoring configuration. Health endpoint probing, crash\ndetection, resource thresholds, and structured log output." :depends-on ()) - (file :name "deploy.k9.ncl" :role "Deployment Component" :description "k9-svc deployment specification at Hunt trust level. Full pedigree\n(L1-L5), environment configs, container config, and rolling\ndeployment strategy." :depends-on ("compose.toml" "ct-build.sh")) - (file :name "compose.example.toml" :role "Example" :description "Fully-commented multi-service example (Rust API + Elixir worker +\nsvalinn gateway). Copy to compose.toml and customise." :depends-on ()) - )) - (directory :id "docs-pillar" :ply 1 :path "docs" :description "Technical documentation hub. The root contains high-level orientation\n(README, Quickstart, State-Visualizer). Specialized tracks live in\nsubdirectories." - (canonical-locations :architecture "architecture/" :attribution "attribution/" :decisions "decisions/" :developer "developer/" :governance "governance/" :legal "legal/" :practice "practice/" :quickstart "QUICKSTART.adoc" :reports "reports/" :standards "standards/" :state_visualizer "STATE-VISUALIZER.adoc" :theory "theory/" :whitepapers "whitepapers/" :wikis "wikis/") - :invariants ("Primary documentation format MUST be AsciiDoc (.adoc)" "Root docs/ MUST only contain pillar entry points") - (directory :id "architecture-track" :ply 2 :path "docs/architecture" :description "Documentation track for system architecture and threat models." - (canonical-locations :threat_model "THREAT-MODEL.adoc") - :invariants ("Visual diagrams MUST include ASCII or Mermaid representations")) - (directory :id "attribution-unit" :ply 2 :path "docs/attribution" :description "Sub-unit of the docs pillar focusing on attribution.") - (directory :id "decisions-unit" :ply 2 :path "docs/decisions" :description "Sub-unit of the docs pillar focusing on decisions.") - (directory :id "developer-unit" :ply 2 :path "docs/developer" :description "Sub-unit of the docs pillar focusing on developer.") - (directory :id "governance-pillar" :ply 1 :path "docs/governance" :description "Primary governance pillar implementing the Triaxial Software Development\nMethodology (TSDM). Contains planning, maintenance, and audit tracks." - (canonical-locations :approach "SOFTWARE-DEVELOPMENT-APPROACH.adoc" :audit "audit/" :checklist "MAINTENANCE-CHECKLIST.adoc" :crg "CRG-CRITERIA.adoc" :maintenance "maintenance/" :planning "planning/" :tsdm_spec "TSDM.adoc") - (directory :id "governance-axis-audit" :ply 2 :path "docs/governance/audit" :description "TSDM Audit track." - (directory :id "governance-unit-compliance" :ply 3 :path "docs/governance/audit/compliance" :description "TSDM compliance unit within the Audit axis.") - (directory :id "governance-unit-effects" :ply 3 :path "docs/governance/audit/effects" :description "TSDM effects unit within the Audit axis.") - (directory :id "governance-unit-systems" :ply 3 :path "docs/governance/audit/systems" :description "TSDM systems unit within the Audit axis.")) - (directory :id "governance-axis-maintenance" :ply 2 :path "docs/governance/maintenance" :description "TSDM Maintenance track." - (directory :id "governance-unit-adaptive" :ply 3 :path "docs/governance/maintenance/adaptive" :description "TSDM adaptive unit within the Maintenance axis.") - (directory :id "governance-unit-corrective" :ply 3 :path "docs/governance/maintenance/corrective" :description "TSDM corrective unit within the Maintenance axis.") - (directory :id "governance-unit-perfective" :ply 3 :path "docs/governance/maintenance/perfective" :description "TSDM perfective unit within the Maintenance axis.")) - (directory :id "governance-axis-planning" :ply 2 :path "docs/governance/planning" :description "TSDM Planning track." - (directory :id "governance-unit-could" :ply 3 :path "docs/governance/planning/could" :description "TSDM could unit within the Planning axis.") - (directory :id "governance-unit-must" :ply 3 :path "docs/governance/planning/must" :description "TSDM must unit within the Planning axis.") - (directory :id "governance-unit-should" :ply 3 :path "docs/governance/planning/should" :description "TSDM should unit within the Planning axis."))) - (directory :id "legal-track" :ply 2 :path "docs/legal" :description "Sub-unit for legal and licensing documentation. Contains framework\nexhibits and archival license texts." - (canonical-locations :exhibits "exhibits/" :texts "texts/")) - (directory :id "practice-unit" :ply 2 :path "docs/practice" :description "Sub-unit of the docs pillar focusing on practice.") - (directory :id "reports-unit" :ply 2 :path "docs/reports" :description "Documentation unit for all automated and manual audit reports. Classified\nby domain." - (canonical-locations :compliance "compliance/" :maintenance "maintenance/" :performance "performance/" :quality "quality/" :security "security/") - (directory :id "report-unit-compliance" :ply 3 :path "docs/reports/compliance" :description "Specialised repository for compliance findings and evidence.") - (directory :id "report-unit-maintenance" :ply 3 :path "docs/reports/maintenance" :description "Maintenance reports.") - (directory :id "report-unit-performance" :ply 3 :path "docs/reports/performance" :description "Specialised repository for performance findings and evidence.") - (directory :id "report-unit-quality" :ply 3 :path "docs/reports/quality" :description "Specialised repository for quality findings and evidence.") - (directory :id "report-unit-security" :ply 3 :path "docs/reports/security" :description "Specialised repository for security findings and evidence.")) - (directory :id "standards-unit" :ply 2 :path "docs/standards" :description "Standards unit for high-rigor verification.") - (directory :id "theory-track" :ply 2 :path "docs/theory" :description "Documentation track for domain-specific theory and research foundations.\nCategorised by discipline." - (canonical-locations :computing "computing/" :formalisms "formalisms/" :mathematics "mathematics/" :ontologies "ontologies/" :other "other/" :socio_technical "socio-technical/") - :invariants ("Theoretical claims MUST reference established academic or technical formalisms") - (directory :id "theory-unit-computing" :ply 3 :path "docs/theory/computing" :description "Theoretical foundation for computing.") - (directory :id "theory-unit-formalisms" :ply 3 :path "docs/theory/formalisms" :description "Theoretical foundation for formalisms.") - (directory :id "theory-unit-mathematics" :ply 3 :path "docs/theory/mathematics" :description "Theoretical foundation for mathematics.") - (directory :id "theory-unit-ontologies" :ply 3 :path "docs/theory/ontologies" :description "Theoretical foundation for ontologies.") - (directory :id "theory-unit-other" :ply 3 :path "docs/theory/other" :description "Theoretical foundation for other.") - (directory :id "theory-unit-socio-technical" :ply 3 :path "docs/theory/socio-technical" :description "Theoretical foundation for socio technical.")) - (directory :id "whitepapers-track" :ply 2 :path "docs/whitepapers" :description "Unit for strategic publications and whitepapers. Categorised by target\naudience: Academic, Industry, and Outreach." - (canonical-locations :academic "academic/" :industry "industry/" :outreach "outreach/") - :invariants ("Each sub-track MUST have a clear audience definition in its README") - (directory :id "academic-unit" :ply 3 :path "docs/whitepapers/academic" :description "Academic logic at level 3.") - (directory :id "industry-unit" :ply 3 :path "docs/whitepapers/industry" :description "Industry logic at level 3.") - (directory :id "whitepapers-track-outreach" :ply 3 :path "docs/whitepapers/outreach" :description "Documentation track for outreach, education, and general-audience\nengagement. Focuses on accessibility and high-level conceptual clarity." - :invariants ("Language MUST be accessible to non-technical audiences" "Avoid deep technical jargon without providing clear definitions"))) - (directory :id "wikis-track" :ply 2 :path "docs/wikis" :description "Long-form collaborative documentation and project knowledge base.\nThis directory mirrors the content structure of the project wiki." - :invariants ("Primary wiki format MUST be Markdown (.md) — owner ruling 2026-09-19: wikis are the .md home (berrywiki); everything outside wikis stays .adoc"))) - (directory :ply 1 :path "examples") - (directory :id "features-pillar" :ply 1 :path "features" :description "Optional project features and ecosystem integrations. Provides bootstrap\nguides for high-rigor tools (Panic-Attacker, BoJ-Server, SSGs)." - (canonical-locations :boj_server "boj-server/" :panic_attacker "panic-attacker/" :ssg "ssg/") - (directory :id "feature-unit-boj-server" :ply 2 :path "features/boj-server" :description "Bootstrap and integration logic for the boj-server ecosystem component.") - (directory :id "feature-unit-panic-attacker" :ply 2 :path "features/panic-attacker" :description "Bootstrap and integration logic for the panic-attacker ecosystem component.") - (directory :id "feature-unit-ssg" :ply 2 :path "features/ssg" :description "Bootstrap and integration logic for the ssg ecosystem component.")) - (directory :id "source-pillar" :ply 1 :path "src" :description "Primary source code directory. Organized by role and architectural\naspect." - (canonical-locations :aspects "aspects/" :bridges "bridges/" :contracts "contracts/" :core "core/" :definitions "definitions/" :errors "errors/" :interface "interface/") - :invariants ("Core logic MUST reside in core/" "Verified seams MUST reside in interface/" "Safety constraints MUST reside in contracts/" "Failure dictionaries MUST reside in errors/") - (directory :id "source-unit-aspects" :ply 2 :path "src/aspects" :description "Cross-cutting concerns and domain-specific aspects (Security,\nObservability, Integrity)." - (canonical-locations :integrity "integrity/" :observability "observability/" :security "security/") - (directory :id "aspect-unit-integrity" :ply 3 :path "src/aspects/integrity" :description "Implementation logic for the integrity aspect.") - (directory :id "aspect-unit-observability" :ply 3 :path "src/aspects/observability" :description "Implementation logic for the observability aspect.") - (directory :id "aspect-unit-security" :ply 3 :path "src/aspects/security" :description "Implementation logic for the security aspect.")) - (directory :id "source-unit-bridges" :ply 2 :path "src/bridges" :description "Integration logic for external systems (API, Database, RPC, etc.).") - (directory :id "source-unit-contracts" :ply 2 :path "src/contracts" :description "Contracts unit for high-rigor source code.") - (directory :id "source-unit-core" :ply 2 :path "src/core" :description "Primary application logic and core domain models.") - (directory :id "source-unit-definitions" :ply 2 :path "src/definitions" :description "Definitions unit for high-rigor source code.") - (directory :id "source-unit-errors" :ply 2 :path "src/errors" :description "Errors unit for high-rigor source code.") - (directory :id "interface-seams-unit" :ply 2 :path "src/interface" :description "Consolidated \"Verified Interface Seams\" unit. This directory unifies the\nformal specification (ABI), the bridge implementation (FFI), and the\nresulting artifacts (Generated)." - (canonical-locations :abi "abi/" :ffi "ffi/" :generated "generated/") - :invariants ("ABI MUST be Idris2 (.idr)" "FFI MUST be Zig (.zig)" "Generated artifacts MUST be C-compatible" "The 'Truth' lives in abi/; the 'Implementation' lives in ffi/") - (directory :id "abi-logic" :ply 3 :path "src/interface/Abi" :description "Specialised Level 3 logic for abi.") - (directory :id "ffi-logic" :ply 3 :path "src/interface/ffi" :description "Specialised Level 3 logic for ffi." - (directory :id "src-unit" :ply 4 :path "src/interface/ffi/src" :description "Src logic at level 4.") - (directory :id "test-unit" :ply 4 :path "src/interface/ffi/test" :description "Test logic at level 4.")) - (directory :id "generated-logic" :ply 3 :path "src/interface/generated" :description "Specialised Level 3 logic for generated." - (directory :id "abi-unit" :ply 4 :path "src/interface/generated/abi" :description "Abi logic at level 4.")))) - (directory :id "verification-pillar" :ply 1 :path "verification" :description "Primary verification pillar. Contains evidence for correctness,\nperformance, formal proofs, randomized testing, and aerospace-grade\nhigh-assurance metrics (MC/DC coverage, traceability, safety cases)." - (canonical-locations :benchmarks "benchmarks/" :coverage "coverage/" :fuzzing "fuzzing/" :proofs "proofs/" :safety_case "safety_case/" :simulations "simulations/" :tests "tests/" :traceability "traceability/") - :invariants ("Evidence MUST be reproducible and documented" "High-assurance deployments MUST satisfy traceability and safety_case requirements") - (directory :id "benches-pillar" :ply 2 :path "verification/benchmarks" :description "Benches pillar.") - (directory :id "verification-unit-coverage" :ply 2 :path "verification/coverage" :description "High-assurance verification unit for coverage. \nCritical for safety-of-life and aerospace-grade deployment standards.") - (directory :id "fuzzing-unit" :ply 2 :path "verification/fuzzing" :description "Fuzzing unit for high-rigor verification.") - (directory :id "verification-unit-proofs" :ply 2 :path "verification/proofs" :description "Sub-unit focusing on proofs.") - (directory :id "verification-unit-safety_case" :ply 2 :path "verification/safety_case" :description "High-assurance verification unit for safety case. \nCritical for safety-of-life and aerospace-grade deployment standards.") - (directory :id "simulations-unit" :ply 2 :path "verification/simulations" :description "Simulations unit for high-rigor verification.") - (directory :ply 2 :path "verification/tests") - (directory :id "verification-unit-traceability" :ply 2 :path "verification/traceability" :description "High-assurance verification unit for traceability. \nCritical for safety-of-life and aerospace-grade deployment standards.")) - ) -) 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/campaigns/repo-map-trim.campaign b/scripts/campaigns/repo-map-trim.campaign new file mode 100644 index 0000000..9520750 --- /dev/null +++ b/scripts/campaigns/repo-map-trim.campaign @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# repo-map-trim.campaign: carry the gen-repo-map.sh comment-trim fix to the +# estate repositories that already hold a template copy of the script. +# +# TARGETED, NOT ESTATE-WIDE. A dry run on 2026-11-10 over the 325 repositories +# in gv-clade-index found the script in 6 of them; the other 263 would have had +# it ADDED, which this campaign must not do. Run it only with this repos file: +# +# catachresis-colloquium/enterglide-ncc-137-k +# catachresis-colloquium/planer-1 +# catachresis-colloquium/stealth-glider-ecosystem +# catachresis-colloquium/tardis-rule-disruption +# catachresis-colloquium/x15-rocket-glider +# +# (These 5 moved from metadatastician to catachresis-colloquium. Use the new +# names: a repo-scoped GitHub proxy (as in Claude Code cloud sessions) does +# not follow the redirect, so pushes to the old names are refused.) +# +# Each holds a copy that matches an earlier template version (no local edits), +# and regenerating its map with the fixed script changes nothing, so no gate +# goes red. hyperpolymath/oblibeny is left out on purpose: its map changes by +# 2 lines and its CI checks the map, so it needs the script and a +# `just repo-map` in the same commit, which a campaign cannot do. +[meta] +name = chore/rsr-repo-map-trim +description = keep allowlist comments whole in the repository map (occupancy-types PR 316) +[paths] +scripts/gen-repo-map.sh diff --git a/scripts/campaigns/www-bundle.campaign b/scripts/campaigns/www-bundle.campaign index c1424ce..ec045c8 100644 --- a/scripts/campaigns/www-bundle.campaign +++ b/scripts/campaigns/www-bundle.campaign @@ -30,7 +30,7 @@ # # The rest of www/ is deliberately NOT propagated: # -# www/public/ per-repo placeholders ({{PROJECT_NAME}} and friends) +# www/public/ per-repo placeholders (occupancy-types and friends) # www/policies/ per-repo placeholders # www/dns/ a DNS scaffold most repositories do not operate # www/tls/ as above diff --git a/scripts/canon-pin-check.sh b/scripts/canon-pin-check.sh new file mode 100755 index 0000000..59e6927 --- /dev/null +++ b/scripts/canon-pin-check.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# canon-pin-check.sh — decide whether a repo deed's (canon …) pin implements a +# canon.lock. The decision only: no network. dogfood-gate.yml `Canon lockstep` +# fetches the locks and the canon PR's state and calls this; the tests in +# tests/workflows/canon_pin_check_test.sh drive it with fixtures. +# +# Two-phase lockstep. canon.lock [canon.lockstep].order is +# "spine-adopts-then-canon-releases", and the spine's adoption necessarily pins +# hashes that exist only in the unmerged canon PR. So the spine may adopt a +# canon AHEAD of its release by naming that PR in the clause: +# +# (canon :version "2.1.6" :criteria-sha256 "…" :gates-sha256 "…" +# :proposed-by "metadatastician/standards#1440" …) +# +# The pin then has to equal canon.lock at that PR's head, and the PR has to be +# open. When the PR merges, the pin equals the released canon.lock and the +# field is spent (a notice asks for it to be dropped). When the PR is closed +# unmerged or amended, the pin names a canon that is not released, and this +# fails. Standards Gate A checks the other side: a canon PR that changes +# canon.lock is not mergeable until the spine's main pins it. +# +# Usage: +# canon-pin-check.sh released +# 0 the pin equals canon.lock (a spent :proposed-by gives a notice) +# 3 the pin differs and the clause names a canon PR: the caller must +# check that PR; stdout ends with the line PROPOSED_BY= +# 1 the pin differs and names no canon PR +# 2 malformed input (no single clause, a pin of the wrong shape, a +# malformed :proposed-by, an unreadable lock) +# canon-pin-check.sh pending +# 0 is "open" and the pin equals that lock +# 1 otherwise +# 2 malformed input +# Both modes print a Markdown table of the comparison to stdout. + +set -euo pipefail + +usage() { sed -n '25,38p' "$0" >&2; exit 2; } + +# 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. The SAME function is in build/just/repo-init.just and +# standards scripts/check-canon-lockstep.sh; keep them 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)" + if [ "$_n" != "1" ]; then + echo "deed_canon: $2 carries ${_n:-0} (canon …) clauses, need exactly 1" >&2 + return 0 + fi + echo "$_clauses" | grep -oE ':'"$1"'[[:space:]]+"[^"]*"' | head -1 | sed -E 's/^[^"]*"//; s/"$//' || true +} + +# canon_key : a scalar under [canon]. +canon_key() { + awk -v key="$1" ' + /^[[:space:]]*#/ { next } + /^\[/ { f = ($0 == "[canon]") ? 1 : 0; next } + f && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { + sub(/^[^=]*=[[:space:]]*/, ""); sub(/[[:space:]]*#.*/, ""); gsub(/^["[:space:]]+|["[:space:]]+$/, ""); print; exit + }' "$2" +} + +# artefact_hash : the sha256 of [canon.artifacts] . +artefact_hash() { + awk -v key="$1" ' + /^[[:space:]]*#/ { next } + $0 ~ "^" key "[[:space:]]*=" { f = 1 } + f { buf = buf " " $0 } + f && /sha256[[:space:]]*=/ { print buf; exit } + ' "$2" | grep -oE '[0-9a-f]{64}' | head -1 || true +} + +HEX='[0-9a-f]{64}' +SEMVER='[0-9]+\.[0-9]+\.[0-9]+' +PR_REF='(hyperpolymath|metadatastician)/standards#[1-9][0-9]*' + +# compare