diff --git a/.envrc b/.envrc index ef739ef..03b910a 100644 --- a/.envrc +++ b/.envrc @@ -20,7 +20,7 @@ fi export PROJECT_NAME="{{PROJECT_NAME}}" export RSR_TIER="infrastructure" # export DATABASE_URL="..." -# export API_KEY="..." +# Set secrets like API keys via .env (gitignored), never inline here. # Source .env if it exists (gitignored) dotenv_if_exists diff --git a/.github/workflows/static-analysis-gate.yml b/.github/workflows/static-analysis-gate.yml index cc38471..2ca8807 100644 --- a/.github/workflows/static-analysis-gate.yml +++ b/.github/workflows/static-analysis-gate.yml @@ -45,14 +45,28 @@ jobs: if: steps.install.outputs.installed == 'true' run: | set +e - panic-attack assail --format json . > panic-attack-findings.json 2>&1 + panic-attack assail --format json . > panic-attack-findings.json PA_EXIT=$? set -e + # 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 + # 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" + 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) @@ -71,13 +85,19 @@ jobs: if: steps.install.outputs.installed == 'true' run: | # Convert JSON findings into GitHub Actions annotations - jq -r '.[] | select(.file != null) | + # 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=\(.file),line=\(.line // 1)::[panic-attack] \(.message)" + "::error file=\($f),line=\(.line // 1)::[panic-attack] \($m)" elif .severity == "high" then - "::error file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)" + "::error file=\($f),line=\(.line // 1)::[panic-attack] \($m)" else - "::warning file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)" + "::warning file=\($f),line=\(.line // 1)::[panic-attack] \($m)" end ' panic-attack-findings.json || true @@ -160,12 +180,28 @@ jobs: if: steps.build.outputs.ready == 'true' run: | set +e - HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json 2>&1 + HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json HYP_EXIT=$? set -e - if [ ! -s hypatia-findings.json ] || ! jq empty hypatia-findings.json 2>/dev/null; then - echo "[]" > hypatia-findings.json + # --exit-zero is Hypatia's own documented CI recipe (lib/hypatia/cli.ex), + # for exactly this case: "use in CI when a downstream step gates on + # severity counts". Findings go to stdout, the one-line summary to + # stderr, and the process exits 0 unless the SCANNER itself failed. + # + # Do NOT redirect stderr into the payload with `2>&1`: that folds the + # summary line into the JSON, so every parse fails, the old `[]` + # fallback substituted a clean result, CRITICAL was always 0, and the + # gate below could never fire on any input. Keep stderr on the log. + if [ "$HYP_EXIT" -ne 0 ]; then + echo "::error::Hypatia scanner execution failed with exit ${HYP_EXIT}" + exit "$HYP_EXIT" + fi + # `jq empty` is NOT sufficient -- it succeeds on any valid JSON, + # including a bare string, object or null. Assert the array. + if [ ! -s hypatia-findings.json ] || ! jq -e 'type == "array"' hypatia-findings.json >/dev/null; then + echo "::error::Hypatia did not produce a valid JSON findings array" + exit 1 fi TOTAL=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0) @@ -183,13 +219,19 @@ jobs: - name: Emit check annotations if: steps.build.outputs.ready == 'true' run: | - jq -r '.[] | select(.file != null) | + # 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=\(.file),line=\(.line // 1)::[hypatia] \(.message)" + "::error file=\($f),line=\(.line // 1)::[hypatia] \($m)" elif .severity == "high" then - "::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)" + "::error file=\($f),line=\(.line // 1)::[hypatia] \($m)" else - "::warning file=\(.file),line=\(.line // 1)::[hypatia] \(.message)" + "::warning file=\($f),line=\(.line // 1)::[hypatia] \($m)" end ' hypatia-findings.json || true diff --git a/.machine_readable/6a2/AGENTIC.a2ml b/.machine_readable/descriptiles/AGENTIC.a2ml similarity index 100% rename from .machine_readable/6a2/AGENTIC.a2ml rename to .machine_readable/descriptiles/AGENTIC.a2ml diff --git a/.machine_readable/6a2/ECOSYSTEM.a2ml b/.machine_readable/descriptiles/ECOSYSTEM.a2ml similarity index 100% rename from .machine_readable/6a2/ECOSYSTEM.a2ml rename to .machine_readable/descriptiles/ECOSYSTEM.a2ml diff --git a/.machine_readable/6a2/META.a2ml b/.machine_readable/descriptiles/META.a2ml similarity index 100% rename from .machine_readable/6a2/META.a2ml rename to .machine_readable/descriptiles/META.a2ml diff --git a/.machine_readable/6a2/NEUROSYM.a2ml b/.machine_readable/descriptiles/NEUROSYM.a2ml similarity index 100% rename from .machine_readable/6a2/NEUROSYM.a2ml rename to .machine_readable/descriptiles/NEUROSYM.a2ml diff --git a/.machine_readable/6a2/PLAYBOOK.a2ml b/.machine_readable/descriptiles/PLAYBOOK.a2ml similarity index 94% rename from .machine_readable/6a2/PLAYBOOK.a2ml rename to .machine_readable/descriptiles/PLAYBOOK.a2ml index 501c50c..435a7b9 100644 --- a/.machine_readable/6a2/PLAYBOOK.a2ml +++ b/.machine_readable/descriptiles/PLAYBOOK.a2ml @@ -13,7 +13,7 @@ method = "ci-triggered" target = "binary" # Rust CLI binary; Zig FFI shared library [incident-response] -# 1. Check .machine_readable/6a2/STATE.a2ml for current status +# 1. Check .machine_readable/descriptiles/STATE.a2ml 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 diff --git a/.machine_readable/6a2/STATE.a2ml b/.machine_readable/descriptiles/STATE.a2ml similarity index 100% rename from .machine_readable/6a2/STATE.a2ml rename to .machine_readable/descriptiles/STATE.a2ml diff --git a/.machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml b/.machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml index 698f4d0..ace72da 100644 --- a/.machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml +++ b/.machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml @@ -2,6 +2,7 @@ # Cross-repo maintenance baseline (machine-readable canonical) [metadata] +project = "dafniser" version = "1.1.0" last-updated = "2026-02-24" scope = "cross-repo" diff --git a/0-AI-MANIFEST.a2ml b/0-AI-MANIFEST.a2ml index d799752..1417e23 100644 --- a/0-AI-MANIFEST.a2ml +++ b/0-AI-MANIFEST.a2ml @@ -23,7 +23,7 @@ Part of the hyperpolymath -iser family (https://github.com/hyperpolymath/iserise ### Machine-Readable Metadata: `.machine_readable/` ONLY -These 6 a2ml files MUST exist in `.machine_readable/6a2/` directory ONLY: +These 6 a2ml files MUST exist in `.machine_readable/descriptiles/` directory ONLY: 1. **STATE.a2ml** - Project state, progress, blockers 2. **META.a2ml** - Architecture decisions, governance 3. **ECOSYSTEM.a2ml** - Position in ecosystem, relationships @@ -111,7 +111,7 @@ dafniser/ ├── docs/ │ └── architecture/ │ └── TOPOLOGY.md # Module topology and data flow diagram -└── .machine_readable/ # ALL machine-readable metadata (6a2/ subdirectory) +└── .machine_readable/ # ALL machine-readable metadata (descriptiles/ subdirectory) ``` ## CORE INVARIANTS @@ -131,7 +131,7 @@ dafniser/ 1. Read THIS file (0-AI-MANIFEST.a2ml) first 2. Understand canonical location: `.machine_readable/` -3. Read `.machine_readable/6a2/STATE.a2ml` for current project state +3. Read `.machine_readable/descriptiles/STATE.a2ml` for current project state 4. State understanding of canonical locations ## ATTESTATION PROOF diff --git a/ROADMAP.adoc b/ROADMAP.adoc index 6637240..60827c7 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -12,7 +12,7 @@ * [x] Idris2 ABI module stubs (Types, Layout, Foreign) * [x] Zig FFI bridge stubs with build.zig * [x] README with architecture overview -* [x] Machine-readable metadata (6a2 files) +* [x] Machine-readable metadata (descriptiles) == Phase 1: Specification Parser * [ ] Define `dafniser.toml` schema for function specs (pre/postconditions, invariants) diff --git a/container/deploy.k9.ncl b/container/deploy.k9.ncl index 0ad0d04..cba76d5 100644 --- a/container/deploy.k9.ncl +++ b/container/deploy.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # deploy.k9.ncl — {{PROJECT_NAME}} deployment component (Hunt level) # @@ -12,61 +13,6 @@ # k9-svc validate container/deploy.k9.ncl # k9-svc deploy 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, - 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) @@ -143,7 +89,61 @@ echo "K9: Rollback complete." # Export the component { - pedigree = component_pedigree, + # The component's pedigree (self-description across five layers) + 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, + 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", + }, + }, + deployment = deployment, scripts = scripts, diff --git a/docs/governance/MAINTENANCE-CHECKLIST.a2ml b/docs/governance/MAINTENANCE-CHECKLIST.a2ml index 698f4d0..ace72da 100644 --- a/docs/governance/MAINTENANCE-CHECKLIST.a2ml +++ b/docs/governance/MAINTENANCE-CHECKLIST.a2ml @@ -2,6 +2,7 @@ # Cross-repo maintenance baseline (machine-readable canonical) [metadata] +project = "dafniser" version = "1.1.0" last-updated = "2026-02-24" scope = "cross-repo"