diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 2958123..19bd936 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,3 +1,4 @@
+# SPDX-License-Identifier: MPL-2.0-or-later
# Dependabot configuration for poly-ssg
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates
@@ -6,6 +7,8 @@ updates:
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
+ cooldown:
+ default-days: 7
schedule:
interval: "weekly"
day: "monday"
@@ -19,6 +22,8 @@ updates:
# OCaml/opam dependencies (rescript-wasm)
- package-ecosystem: "npm"
directory: "/engines/rescript-wasm"
+ cooldown:
+ default-days: 7
schedule:
interval: "weekly"
day: "monday"
@@ -34,6 +39,8 @@ updates:
# Forth Estate (if any npm deps added)
- package-ecosystem: "npm"
directory: "/engines/forth-estate"
+ cooldown:
+ default-days: 7
schedule:
interval: "weekly"
commit-message:
@@ -45,6 +52,8 @@ updates:
# Root project dependencies
- package-ecosystem: "npm"
directory: "/"
+ cooldown:
+ default-days: 7
schedule:
interval: "weekly"
commit-message:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1083502..396dc80 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,275 +1,42 @@
# SPDX-License-Identifier: MPL-2.0-or-later
-# Continuous Integration - Strict Mode
-# All warnings treated as errors, all tests must pass
-name: CI
-
+# Preservation gates for the extracted, unqualified source archive.
+name: Preservation CI
on:
- push:
- branches: [main, develop]
pull_request:
+ push:
branches: [main]
-
-permissions: read-all
-
-env:
- OCAML_VERSION: "4.14.x"
- # Treat warnings as errors
- OCAML_COLOR: always
-
+ workflow_dispatch:
+permissions:
+ contents: read
+concurrency:
+ group: preservation-${{ github.event_name }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
- # OCaml/ReScript-WASM build and test - STRICT MODE
- rescript-wasm:
- name: ReScript WASM-GC Backend
- runs-on: ${{ matrix.os }}
- permissions:
- contents: read
-
- strategy:
- fail-fast: true # Fail immediately on first error
- matrix:
- os: [ubuntu-latest, macos-latest]
- ocaml-version: ["4.14.x", "5.1.x"]
-
+ preservation:
+ name: Source history and preservation controls
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
steps:
- - name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Set up OCaml ${{ matrix.ocaml-version }}
- uses: ocaml/setup-ocaml@7c185f622998a008ba1b296d2d7c64e235629c23 # v3.6.1
- with:
- ocaml-compiler: ${{ matrix.ocaml-version }}
- dune-cache: true
-
- - name: Install dependencies
- run: opam install . --deps-only --with-test -y
- working-directory: engines/rescript-wasm
-
- - name: Build with strict warnings
- run: |
- opam exec -- dune build --force
- working-directory: engines/rescript-wasm
-
- - name: Run all tests (must pass)
- run: |
- opam exec -- dune test
- echo "All tests passed successfully"
- working-directory: engines/rescript-wasm
-
- - name: Generate sample WAT
- run: |
- opam exec -- dune exec rescript_wasm -- add > add.wat
- opam exec -- dune exec rescript_wasm -- max > max.wat
- opam exec -- dune exec rescript_wasm -- square > square.wat
- opam exec -- dune exec rescript_wasm -- combined > combined.wat
- echo "=== add.wat ==="
- cat add.wat
- working-directory: engines/rescript-wasm
-
- - name: Install wasm-tools
- if: runner.os == 'Linux'
- run: |
- curl -L https://github.com/bytecodealliance/wasm-tools/releases/download/v1.219.1/wasm-tools-1.219.1-x86_64-linux.tar.gz | tar xz
- sudo mv wasm-tools-1.219.1-x86_64-linux/wasm-tools /usr/local/bin/
-
- - name: Validate WAT output (strict)
- if: runner.os == 'Linux'
- run: |
- # Validate all generated WAT files
- for wat in add.wat max.wat square.wat combined.wat; do
- echo "Validating $wat..."
- wasm-tools validate --features gc "$wat" || echo "Note: WAT validation requires WASM-GC support"
- done
- working-directory: engines/rescript-wasm
-
- - name: Upload WAT artifacts
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
- name: wat-output-${{ matrix.os }}-${{ matrix.ocaml-version }}
- path: engines/rescript-wasm/*.wat
-
- # Forth Estate build
- forth-estate:
- name: Forth Estate SSG
- runs-on: ubuntu-latest
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Install Gforth
- run: sudo apt-get update && sudo apt-get install -y gforth
-
- - name: Check Forth syntax
- run: |
- if [ -f forth-estate.fs ]; then
- gforth forth-estate.fs -e bye
- fi
- working-directory: engines/forth-estate
-
- # Ddraig (Idris 2) build and test
- ddraig-ssg:
- name: Ddraig SSG
- runs-on: ubuntu-latest
- container: ghcr.io/idris-community/idris2:latest
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Build Ddraig executable
- run: |
- cd engines/ddraig-ssg
- idris2 Ddraig.idr -o ddraig
-
- - name: Run Ddraig parser tests
- run: |
- cd engines/ddraig-ssg
- ./build/exec/ddraig test-markdown
- ./build/exec/ddraig test-frontmatter
- ./build/exec/ddraig test-full
-
- - name: Build site with Ddraig
- run: |
- cd engines/ddraig-ssg
- ./build/exec/ddraig build ../../content ../../_site
-
- # Strict lint and format checks - ALL MUST PASS
- lint:
- name: Lint & Format (Strict)
- runs-on: ubuntu-latest
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Set up OCaml
- uses: ocaml/setup-ocaml@7c185f622998a008ba1b296d2d7c64e235629c23 # v3.6.1
- with:
- ocaml-compiler: "4.14.x"
-
- - name: Check for unsafe patterns (MUST PASS)
- run: |
- echo "=== Checking for unsafe patterns ==="
- ERRORS=0
-
- # Check for Obj.magic (type safety violation)
- if grep -rn "Obj\.magic" engines/rescript-wasm/lib/; then
- echo "ERROR: Obj.magic found - this violates type safety"
- ERRORS=$((ERRORS + 1))
- fi
-
- # Check for Marshal (code execution risk)
- if grep -rn "Marshal\." engines/rescript-wasm/lib/; then
- echo "ERROR: Marshal found - this is a code execution risk"
- ERRORS=$((ERRORS + 1))
- fi
-
- # Check for shell execution
- if grep -rn "Unix\.system\|Unix\.open_process\|Sys\.command" engines/rescript-wasm/lib/; then
- echo "ERROR: Shell execution found - security risk"
- ERRORS=$((ERRORS + 1))
- fi
-
- if [ $ERRORS -gt 0 ]; then
- echo "Found $ERRORS security issues"
- exit 1
- fi
- echo "No unsafe patterns found"
-
- - name: Check for catch-all patterns (MUST PASS)
- run: |
- echo "=== Checking for dangerous catch-all patterns ==="
- # Look for standalone _ patterns in match statements (potential bug hiding)
- # This is a heuristic check - not perfect but catches common issues
- if grep -En "^\s*\|\s*_\s*->" engines/rescript-wasm/lib/*.ml | grep -v "failwith\|error\|assert\|raise"; then
- echo "WARNING: Found catch-all patterns without explicit error handling"
- echo "Consider using explicit pattern matching to avoid hiding bugs"
- fi
- echo "Pattern check complete"
-
- - name: Report TODOs/FIXMEs
- run: |
- echo "=== TODOs and FIXMEs (informational) ==="
- grep -rn "TODO\|FIXME\|XXX\|HACK" engines/rescript-wasm/lib/ || echo "None found"
-
- - name: Check line lengths (soft limit 100)
- run: |
- echo "=== Lines over 100 characters (warnings) ==="
- LONG_LINES=$(find engines/rescript-wasm/lib -name "*.ml" -exec awk 'length > 100 {count++} END {print count+0}' {} \; | awk '{s+=$1} END {print s}')
- echo "Found $LONG_LINES lines over 100 characters"
- if [ "$LONG_LINES" -gt 50 ]; then
- echo "WARNING: Many long lines found, consider refactoring"
- fi
-
- # Security scanning job
- security:
- name: Security Checks
- runs-on: ubuntu-latest
- permissions:
- security-events: write
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Set up OCaml
- uses: ocaml/setup-ocaml@7c185f622998a008ba1b296d2d7c64e235629c23 # v3.6.1
- with:
- ocaml-compiler: "4.14.x"
-
- - name: Check dependencies for known vulnerabilities
- run: |
- echo "=== Checking for vulnerable dependencies ==="
- opam list --installed --safe 2>/dev/null || true
-
- - name: Scan for hardcoded secrets
- run: |
- echo "=== Scanning for hardcoded secrets ==="
- # Check for common secret patterns
- if grep -rEn "(password|secret|api_key|apikey|auth_token|private_key)\s*=\s*['\"][^'\"]+['\"]" engines/rescript-wasm/; then
- echo "ERROR: Potential hardcoded secrets found"
- exit 1
- fi
- echo "No hardcoded secrets found"
-
- - name: Check .gitignore coverage
- run: |
- echo "=== Checking .gitignore coverage ==="
- # Verify important patterns are ignored
- if [ -f engines/rescript-wasm/.gitignore ]; then
- for pattern in "_build/" "*.cmo" "*.cmi" ".env"; do
- if grep -q "$pattern" engines/rescript-wasm/.gitignore; then
- echo "OK: $pattern is ignored"
- else
- echo "WARNING: $pattern should be in .gitignore"
- fi
- done
- fi
-
- # Final validation job - runs after all others
- validate:
- name: Final Validation
- runs-on: ubuntu-latest
- permissions:
- contents: read
- needs: [rescript-wasm, forth-estate, ddraig-ssg, lint, security]
-
- steps:
- - name: All checks passed
- run: |
- echo "========================================="
- echo "All CI checks passed successfully!"
- echo "========================================="
- echo "- ReScript-WASM: Build & Tests PASSED"
- echo "- Forth Estate: Syntax PASSED"
- echo "- Ddraig SSG: Build & Tests PASSED"
- echo "- Lint & Format: PASSED"
- echo "- Security Checks: PASSED"
- echo "========================================="
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Verify preserved source identity and ancestry
+ run: bash scripts/verify-preservation.sh
+ - name: Reject corrupt provenance and unrelated history
+ run: bash tests/preservation-controls.sh
+ - name: Verify retired workflow bytes and reject changed archives
+ run: |
+ bash scripts/verify-archived-workflows.sh
+ bash tests/archived-workflow-controls.sh
+ - name: Validate output checker with independent positive and negative inputs
+ run: bash tests/output-validation-controls.sh
+ - name: Record qualification scope
+ run: |
+ printf '%s\n' \
+ '## Preservation receipt' \
+ "Subject: $(git rev-parse HEAD)" \
+ 'Source history and output-checker regressions were exercised.' \
+ 'Generator builds, BoJ parity, deployment and overall security qualification remain open in issue #2.' \
+ >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index e8b27d0..8b6c37d 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -1,6 +1,5 @@
# SPDX-License-Identifier: MPL-2.0-or-later
name: CodeQL Security Analysis
-
on:
push:
branches: [main, master]
@@ -8,14 +7,16 @@ on:
branches: [main, master]
schedule:
- cron: '0 6 * * 1'
-
-permissions: read-all
-
+permissions:
+ contents: read
+ actions: read
jobs:
analyze:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
permissions:
contents: read
+ actions: read
security-events: write
strategy:
fail-fast: false
@@ -23,18 +24,34 @@ jobs:
include:
- language: javascript-typescript
build-mode: none
-
steps:
- - name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
+ - name: Checkout immutable subject and retained ancestry
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 0
+ persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v3.28.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
-
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.28.1
+ uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
+ with:
+ category: /language:${{ matrix.language }}
+ output: ${{ runner.temp }}/codeql-sarif
+ # This private repository has no hosted code-scanning entitlement.
+ # Keep actual analysis and complete SARIF; do not turn upload errors into success.
+ upload: never
+ upload-database: false
+ - name: Classify retained findings and block current findings
+ run: bash scripts/review-preservation-sarif.sh codeql "$RUNNER_TEMP/codeql-sarif/javascript.sarif" "$RUNNER_TEMP/codeql-sarif/disposition"
+ - name: Retain complete analysis and disposition
+ if: always()
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
- category: "/language:${{ matrix.language }}"
+ name: codeql-${{ matrix.language }}-${{ github.event.pull_request.head.sha || github.sha }}
+ path: ${{ runner.temp }}/codeql-sarif
+ if-no-files-found: error
+ retention-days: 90
diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml
index f2bf132..80239b5 100644
--- a/.github/workflows/hypatia-scan.yml
+++ b/.github/workflows/hypatia-scan.yml
@@ -1,179 +1,27 @@
# SPDX-License-Identifier: PMPL-1.0-or-later
-# Hypatia Neurosymbolic CI/CD Security Scan
name: Hypatia Security Scan
-
on:
push:
- branches: [ main, master, develop ]
+ branches: [main, master, develop]
pull_request:
- branches: [ main, master ]
+ branches: [main, master]
schedule:
- - cron: '0 0 * * 0' # Weekly on Sunday
+ - cron: '0 0 * * 0'
workflow_dispatch:
-
-permissions: read-all
-
+permissions:
+ actions: read
+ contents: read
jobs:
scan:
- name: Hypatia Neurosymbolic Analysis
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- with:
- fetch-depth: 0 # Full history for better pattern analysis
-
- - name: Setup Elixir for Hypatia scanner
- uses: erlef/setup-beam@2f0cc07b4b9bea248ae098aba9e1a8a1de5ec24c # v1.18.2
- with:
- elixir-version: '1.19.4'
- otp-version: '28.3'
-
- - name: Clone Hypatia
- run: |
- if [ ! -d "$HOME/hypatia" ]; then
- git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia"
- fi
-
- - name: Build Hypatia scanner (if needed)
- working-directory: ${{ env.HOME }}/hypatia
- run: |
- if [ ! -f hypatia-v2 ]; then
- echo "Building hypatia-v2 scanner..."
- cd scanner
- mix deps.get
- mix escript.build
- mv hypatia ../hypatia-v2
- fi
-
- - name: Run Hypatia scan
- id: scan
- run: |
- echo "Scanning repository: ${{ github.repository }}"
-
- # Run scanner
- HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json
-
- # Count findings
- FINDING_COUNT=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0)
- echo "findings_count=$FINDING_COUNT" >> $GITHUB_OUTPUT
-
- # Extract severity counts
- CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' hypatia-findings.json)
- HIGH=$(jq '[.[] | select(.severity == "high")] | length' hypatia-findings.json)
- MEDIUM=$(jq '[.[] | select(.severity == "medium")] | length' hypatia-findings.json)
-
- echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
- echo "high=$HIGH" >> $GITHUB_OUTPUT
- echo "medium=$MEDIUM" >> $GITHUB_OUTPUT
-
- echo "## Hypatia Scan Results" >> $GITHUB_STEP_SUMMARY
- echo "- Total findings: $FINDING_COUNT" >> $GITHUB_STEP_SUMMARY
- echo "- Critical: $CRITICAL" >> $GITHUB_STEP_SUMMARY
- echo "- High: $HIGH" >> $GITHUB_STEP_SUMMARY
- echo "- Medium: $MEDIUM" >> $GITHUB_STEP_SUMMARY
-
- - name: Upload findings artifact
- uses: actions/upload-artifact@65c79d7f54e76e4e3c7a8f34db0f4ac8b515c478 # v4
- with:
- name: hypatia-findings
- path: hypatia-findings.json
- retention-days: 90
-
- - name: Submit findings to gitbot-fleet (Phase 2)
- if: steps.scan.outputs.findings_count > 0
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_SHA: ${{ github.sha }}
- run: |
- echo "📤 Submitting ${{ steps.scan.outputs.findings_count }} findings to gitbot-fleet..."
-
- # Clone gitbot-fleet to temp directory
- FLEET_DIR="/tmp/gitbot-fleet-$$"
- git clone https://github.com/hyperpolymath/gitbot-fleet.git "$FLEET_DIR"
-
- # Run submission script
- bash "$FLEET_DIR/scripts/submit-finding.sh" hypatia-findings.json
-
- # Cleanup
- rm -rf "$FLEET_DIR"
-
- echo "✅ Finding submission complete"
-
- - name: Check for critical issues
- if: steps.scan.outputs.critical > 0
- run: |
- echo "⚠️ Critical security issues found!"
- echo "Review hypatia-findings.json for details"
- # Don't fail the build yet - just warn
- # exit 1
-
- - name: Generate scan report
- run: |
- cat << EOF > hypatia-report.md
- # Hypatia Security Scan Report
-
- **Repository:** ${{ github.repository }}
- **Scan Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")
- **Commit:** ${{ github.sha }}
-
- ## Summary
-
- | Severity | Count |
- |----------|-------|
- | Critical | ${{ steps.scan.outputs.critical }} |
- | High | ${{ steps.scan.outputs.high }} |
- | Medium | ${{ steps.scan.outputs.medium }} |
- | **Total**| ${{ steps.scan.outputs.findings_count }} |
-
- ## Next Steps
-
- 1. Review findings in the artifact: hypatia-findings.json
- 2. Auto-fixable issues will be addressed by robot-repo-automaton (Phase 3)
- 3. Manual review required for complex issues
-
- ## Learning
-
- These findings feed Hypatia's learning engine to improve future rules.
-
- ---
- *Powered by [Hypatia](https://github.com/hyperpolymath/hypatia) - Neurosymbolic CI/CD Intelligence*
- EOF
-
- cat hypatia-report.md >> $GITHUB_STEP_SUMMARY
-
- - name: Comment on PR with findings
- if: github.event_name == 'pull_request' && steps.scan.outputs.findings_count > 0
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- with:
- script: |
- const fs = require('fs');
- const findings = JSON.parse(fs.readFileSync('hypatia-findings.json', 'utf8'));
-
- const critical = findings.filter(f => f.severity === 'critical').length;
- const high = findings.filter(f => f.severity === 'high').length;
-
- let comment = `## 🔍 Hypatia Security Scan\n\n`;
- comment += `**Findings:** ${findings.length} issues detected\n\n`;
- comment += `| Severity | Count |\n|----------|-------|\n`;
- comment += `| 🔴 Critical | ${critical} |\n`;
- comment += `| 🟠 High | ${high} |\n`;
- comment += `| 🟡 Medium | ${findings.length - critical - high} |\n\n`;
-
- if (critical > 0) {
- comment += `⚠️ **Action Required:** Critical security issues found!\n\n`;
- }
-
- comment += `View findings
\n\n`;
- comment += `\`\`\`json\n${JSON.stringify(findings.slice(0, 10), null, 2)}\n\`\`\`\n`;
- comment += ` \n\n`;
- comment += `*Powered by Hypatia Neurosymbolic CI/CD Intelligence*`;
-
- github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body: comment
- });
+ uses: metadatastician/standards/.github/workflows/hypatia-scan-reusable.yml@d3e4b485a2af90c92a8e18af0888cd19cb132cd9
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+ with:
+ # The canonical grid reports all evidence and unavailable integrations.
+ # No broad path exclusions or caller-local scanner implementation.
+ scope-excludes: ''
+ report-backs-artifact: ''
+ secrets:
+ HYPATIA_SCAN_PAT: ${{ secrets.HYPATIA_SCAN_PAT }}
diff --git a/.github/workflows/language-policy.yml b/.github/workflows/language-policy.yml
index 1191cd2..72a0218 100644
--- a/.github/workflows/language-policy.yml
+++ b/.github/workflows/language-policy.yml
@@ -1,82 +1,23 @@
# SPDX-License-Identifier: MPL-2.0-or-later
+# Source preservation is distinct from permission to introduce new runtime code.
name: Language Policy Enforcement
-
on:
push:
- branches: ["*"]
+ branches: [main, master, develop]
pull_request:
- branches: ["*"]
-
-permissions: read-all
-
+permissions:
+ contents: read
jobs:
check-policy:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
-
- - name: Setup Deno
- uses: denoland/setup-deno@e95548e56ddc3f63d0e1eab7bed41d4031f02b36 # v2
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
- deno-version: v2.x
-
- - name: Check for banned TypeScript files
- run: |
- echo "Checking for banned TypeScript files..."
- VIOLATIONS=$(find . -type f \( -name "*.ts" -o -name "*.tsx" \) \
- ! -name "*.d.ts" \
- ! -path "./.git/*" \
- ! -path "./node_modules/*" \
- ! -path "./_site/*" | head -20)
- if [ -n "$VIOLATIONS" ]; then
- echo "ERROR: TypeScript files found (banned - use ReScript instead):"
- echo "$VIOLATIONS"
- exit 1
- fi
- echo "No TypeScript violations found"
-
- - name: Check for banned Go files
- run: |
- echo "Checking for banned Go files..."
- VIOLATIONS=$(find . -type f -name "*.go" \
- ! -path "./.git/*" | head -20)
- if [ -n "$VIOLATIONS" ]; then
- echo "ERROR: Go files found (banned - use Rust instead):"
- echo "$VIOLATIONS"
- exit 1
- fi
- echo "No Go violations found"
-
- - name: Check for banned Node.js lock files
- run: |
- echo "Checking for banned lock files..."
- VIOLATIONS=""
- for f in package-lock.json yarn.lock pnpm-lock.yaml bun.lockb; do
- if [ -f "$f" ]; then
- VIOLATIONS="$VIOLATIONS $f"
- fi
- done
- if [ -n "$VIOLATIONS" ]; then
- echo "ERROR: Node.js lock files found (banned - use Deno instead):"
- echo "$VIOLATIONS"
- exit 1
- fi
- echo "No Node.js lock file violations found"
-
- - name: Check for banned Python files
- run: |
- echo "Checking for banned Python files..."
- VIOLATIONS=$(find . -type f -name "*.py" \
- ! -path "./.git/*" \
- ! -path "./salt/*" \
- ! -path "./saltstack/*" \
- ! -path "./_salt/*" | head -20)
- if [ -n "$VIOLATIONS" ]; then
- echo "ERROR: Python files found outside SaltStack (banned - use ReScript/Rust):"
- echo "$VIOLATIONS"
- exit 1
- fi
- echo "No Python violations found"
-
- - name: Run full policy check
- run: deno run --allow-read scripts/check-language-policy.js
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Reject new prohibited source and identify exact retained objects
+ run: bash scripts/check-preserved-language-policy.sh
+ - name: Reject changed, relocated and fabricated historical evidence
+ run: bash tests/preservation-policy-controls.sh
diff --git a/.github/workflows/rsr-antipattern.yml b/.github/workflows/rsr-antipattern.yml
index 641deed..bbc8a24 100644
--- a/.github/workflows/rsr-antipattern.yml
+++ b/.github/workflows/rsr-antipattern.yml
@@ -1,92 +1,23 @@
# SPDX-License-Identifier: MPL-2.0-or-later
-# RSR Anti-Pattern CI Check
-# SPDX-License-Identifier: MPL-2.0-or-later
-#
-# Enforces: No TypeScript, No Go, No Python (except SaltStack), No npm
-# Allows: ReScript, Deno, WASM, Rust, OCaml, Haskell, Guile/Scheme
-
+# Source preservation is distinct from permission to introduce new runtime code.
name: RSR Anti-Pattern Check
-
on:
push:
branches: [main, master, develop]
pull_request:
- branches: [main, master, develop]
-
-
-permissions: read-all
-
+permissions:
+ contents: read
jobs:
antipattern-check:
- runs-on: ubuntu-latest
- permissions:
- contents: read
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Check for TypeScript
- run: |
- # Exclude bindings/deno/ - those are Deno FFI files using Deno.dlopen, not plain TypeScript
- # Exclude .d.ts files - those are TypeScript type declarations for ReScript FFI
- TS_FILES=$(find . \( -name "*.ts" -o -name "*.tsx" \) | grep -v node_modules | grep -v 'bindings/deno' | grep -v '\.d\.ts$' || true)
- if [ -n "$TS_FILES" ]; then
- echo "❌ TypeScript files detected - use ReScript instead"
- echo "$TS_FILES"
- exit 1
- fi
- echo "✅ No TypeScript files (Deno FFI bindings excluded)"
-
- - name: Check for Go
- run: |
- if find . -name "*.go" | grep -q .; then
- echo "❌ Go files detected - use Rust/WASM instead"
- find . -name "*.go"
- exit 1
- fi
- echo "✅ No Go files"
-
- - name: Check for Python (non-SaltStack)
- run: |
- PY_FILES=$(find . -name "*.py" | grep -v salt | grep -v _states | grep -v _modules | grep -v pillar | grep -v venv | grep -v __pycache__ || true)
- if [ -n "$PY_FILES" ]; then
- echo "❌ Python files detected - only allowed for SaltStack"
- echo "$PY_FILES"
- exit 1
- fi
- echo "✅ No non-SaltStack Python files"
-
- - name: Check for npm lockfiles
- run: |
- if [ -f "package-lock.json" ] || [ -f "yarn.lock" ]; then
- echo "❌ npm/yarn lockfile detected - use Deno instead"
- exit 1
- fi
- echo "✅ No npm lockfiles"
-
- - name: Check for tsconfig
- run: |
- if [ -f "tsconfig.json" ]; then
- echo "❌ tsconfig.json detected - use ReScript instead"
- exit 1
- fi
- echo "✅ No tsconfig.json"
-
- - name: Verify Deno presence (if package.json exists)
- run: |
- if [ -f "package.json" ]; then
- if [ ! -f "deno.json" ] && [ ! -f "deno.jsonc" ]; then
- echo "⚠️ Warning: package.json without deno.json - migration recommended"
- fi
- fi
- echo "✅ Deno configuration check complete"
-
- - name: Summary
- run: |
- echo "╔════════════════════════════════════════════════════════════╗"
- echo "║ RSR Anti-Pattern Check Passed ✅ ║"
- echo "║ ║"
- echo "║ Allowed: ReScript, Deno, WASM, Rust, OCaml, Haskell, ║"
- echo "║ Guile/Scheme, SaltStack (Python) ║"
- echo "║ ║"
- echo "║ Blocked: TypeScript, Go, npm, Python (non-Salt) ║"
- echo "╚════════════════════════════════════════════════════════════╝"
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Reject new prohibited source and identify exact retained objects
+ run: bash scripts/check-preserved-language-policy.sh
+ - name: Reject changed, relocated and fabricated historical evidence
+ run: bash tests/preservation-policy-controls.sh
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 6a82602..44b8f1d 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -3,30 +3,41 @@ name: OSSF Scorecard
on:
push:
branches: [main, master]
+ pull_request:
+ branches: [main]
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
-
-permissions: read-all
-
+permissions:
+ contents: read
+ actions: read
jobs:
analysis:
- runs-on: ubuntu-latest
- permissions:
- security-events: write
- id-token: write
+ runs-on: ubuntu-24.04
+ timeout-minutes: 15
steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 0
persist-credentials: false
-
- name: Run Scorecard
- uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.3.1
+ uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0
with:
results_file: results.sarif
results_format: sarif
-
- - name: Upload results
- uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.31.8
+ publish_results: false
+ - name: Classify repository findings with explicit preservation scope
+ env:
+ SCORECARD_ANALYSIS_SCOPE: ${{ github.event_name == 'pull_request' && 'checked-out-subject' || 'remote-default-branch' }}
+ run: bash scripts/review-preservation-sarif.sh scorecard results.sarif "$RUNNER_TEMP/scorecard/disposition"
+ - name: Retain complete SARIF without unavailable hosted upload
+ if: always()
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
- sarif_file: results.sarif
+ name: scorecard-${{ github.event.pull_request.head.sha || github.sha }}
+ path: |
+ results.sarif
+ ${{ runner.temp }}/scorecard
+ if-no-files-found: error
+ retention-days: 90
diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml
index 59e5571..a17754d 100644
--- a/.github/workflows/secret-scanner.yml
+++ b/.github/workflows/secret-scanner.yml
@@ -1,33 +1,31 @@
# SPDX-License-Identifier: MPL-2.0-or-later
-# Prevention workflow - scans for hardcoded secrets before they reach main
name: Secret Scanner
-
on:
pull_request:
push:
branches: [main]
-
-permissions: read-all
-
+permissions:
+ contents: read
jobs:
+ scan:
+ uses: hyperpolymath/cicd-suite/.github/workflows/secret-scanner-reusable.yml@52a90c24bf183437b887dd182c5a9406af02c5dd
+ with:
+ fast-pr: false
+ permissions:
+ contents: read
trufflehog:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
+ timeout-minutes: 15
steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
- with:
- fetch-depth: 0 # Full history for scanning
-
- - name: TruffleHog Secret Scan
- uses: trufflesecurity/trufflehog@ef6e76c3c4023279497fab4721ffa071a722fd05 # v3
- with:
- extra_args: --only-verified --fail
-
- gitleaks:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
+ persist-credentials: false
+ - name: TruffleHog verified-credential scan
+ uses: trufflesecurity/trufflehog@ef6e76c3c4023279497fab4721ffa071a722fd05 # v3.92.4
+ with:
+ # The pinned action supplies --fail; repeating it aborts before scanning.
+ extra_args: --only-verified
- name: Gitleaks Secret Scan
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 9ae77bd..4c3ea84 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -1,6 +1,5 @@
# SPDX-License-Identifier: MPL-2.0-or-later
name: Semgrep SAST
-
on:
push:
branches: [main, master]
@@ -9,27 +8,36 @@ on:
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
-
-permissions: read-all
-
+permissions:
+ contents: read
jobs:
semgrep:
- runs-on: ubuntu-latest
- permissions:
- security-events: write
- contents: read
- container:
- image: semgrep/semgrep
+ runs-on: ubuntu-24.04
+ timeout-minutes: 15
steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Run Semgrep
- run: semgrep scan --sarif --output=semgrep.sarif --config=auto .
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Run pinned Semgrep against the complete subject
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
-
- - name: Upload SARIF
- uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
- with:
- sarif_file: semgrep.sarif
+ run: |
+ set -euo pipefail
+ mkdir -p "$RUNNER_TEMP/semgrep"
+ docker run --rm --entrypoint sh \
+ -v "$PWD:/src:ro" -v "$RUNNER_TEMP/semgrep:/out" -w /src \
+ -e SEMGREP_APP_TOKEN \
+ semgrep/semgrep@sha256:451b3d875c00dac7ed5db5293e584a07de11449a409c10b3c2d2652f429630b6 \
+ -c 'git config --global --add safe.directory /src && exec semgrep scan --sarif --output=/out/semgrep.sarif --config=auto .'
+ - name: Classify retained findings and block current findings
+ run: bash scripts/review-preservation-sarif.sh semgrep "$RUNNER_TEMP/semgrep/semgrep.sarif" "$RUNNER_TEMP/semgrep/disposition"
+ - name: Retain complete SARIF without unavailable hosted upload
if: always()
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+ with:
+ name: semgrep-${{ github.event.pull_request.head.sha || github.sha }}
+ path: ${{ runner.temp }}/semgrep
+ if-no-files-found: error
+ retention-days: 90
diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml
index b6050d7..0e31695 100644
--- a/.github/workflows/workflow-linter.yml
+++ b/.github/workflows/workflow-linter.yml
@@ -1,157 +1,22 @@
# SPDX-License-Identifier: MPL-2.0-or-later
-# workflow-linter.yml - Validates GitHub workflows against RSR security standards
-# This workflow can be copied to other repos for consistent enforcement
name: Workflow Security Linter
-
on:
push:
- paths:
- - '.github/workflows/**'
+ paths: ['.github/workflows/**', 'scripts/lint-workflow-contracts.sh', 'tests/workflow-contract-controls.sh']
pull_request:
- paths:
- - '.github/workflows/**'
+ paths: ['.github/workflows/**', 'scripts/lint-workflow-contracts.sh', 'tests/workflow-contract-controls.sh']
workflow_dispatch:
-
-permissions: read-all
-
+permissions:
+ contents: read
jobs:
lint-workflows:
- runs-on: ubuntu-latest
- permissions:
- contents: read
-
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
steps:
- - name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Check SPDX Headers
- run: |
- echo "=== Checking SPDX License Headers ==="
- failed=0
- for file in .github/workflows/*.yml .github/workflows/*.yaml; do
- [ -f "$file" ] || continue
- if ! head -1 "$file" | grep -q "^# SPDX-License-Identifier:"; then
- echo "ERROR: $file missing SPDX header"
- failed=1
- fi
- done
- if [ $failed -eq 1 ]; then
- echo "Add '# SPDX-License-Identifier: PMPL-1.0-or-later' as first line"
- exit 1
- fi
- echo "All workflows have SPDX headers"
-
- - name: Check Permissions Declaration
- run: |
- echo "=== Checking Permissions ==="
- failed=0
- for file in .github/workflows/*.yml .github/workflows/*.yaml; do
- [ -f "$file" ] || continue
- if ! grep -q "^permissions:" "$file"; then
- echo "ERROR: $file missing top-level 'permissions:' declaration"
- failed=1
- fi
- done
- if [ $failed -eq 1 ]; then
- echo "Add 'permissions: read-all' at workflow level"
- exit 1
- fi
- echo "All workflows have permissions declared"
-
- - name: Check SHA-Pinned Actions
- run: |
- echo "=== Checking Action Pinning ==="
- # Find any uses: lines that don't have @SHA format
- # Exclude: comment lines, local actions, docker actions, github-script
- unpinned=$(grep -rn "uses:" .github/workflows/ | \
- grep -v "^\s*#" | \
- grep -v "@[a-f0-9]\{40\}" | \
- grep -v "uses: \./\|uses: docker://\|uses: actions/github-script" | \
- grep -v "# Pattern:\|# Find any" || true)
-
- if [ -n "$unpinned" ]; then
- echo "ERROR: Found unpinned actions:"
- echo "$unpinned"
- echo ""
- echo "Replace version tags with SHA pins, e.g.:"
- echo " uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.1.1"
- exit 1
- fi
- echo "All actions are SHA-pinned"
-
- - name: Check for Duplicate Workflows
- run: |
- echo "=== Checking for Duplicates ==="
- # Known duplicate patterns
- if [ -f .github/workflows/codeql.yml ] && [ -f .github/workflows/codeql-analysis.yml ]; then
- echo "ERROR: Duplicate CodeQL workflows found"
- echo "Delete codeql-analysis.yml (keep codeql.yml)"
- exit 1
- fi
- if [ -f .github/workflows/rust.yml ] && [ -f .github/workflows/rust-ci.yml ]; then
- echo "WARNING: Potential duplicate Rust workflows"
- echo "Consider consolidating rust.yml and rust-ci.yml"
- fi
- echo "No critical duplicates found"
-
- - name: Check CodeQL Language Matrix
- run: |
- echo "=== Checking CodeQL Configuration ==="
- if [ ! -f .github/workflows/codeql.yml ]; then
- echo "No CodeQL workflow found (optional)"
- exit 0
- fi
-
- # Detect repo languages
- has_js=$(find . -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -path "*/src/*" -o -path "*/lib/*" 2>/dev/null | head -1)
- has_py=$(find . -name "*.py" -path "*/src/*" -o -path "*/lib/*" 2>/dev/null | head -1)
- has_go=$(find . -name "*.go" -path "*/src/*" -o -path "*/cmd/*" -o -path "*/pkg/*" 2>/dev/null | head -1)
- has_rs=$(find . -name "*.rs" -path "*/src/*" 2>/dev/null | head -1)
- has_java=$(find . -name "*.java" -path "*/src/*" 2>/dev/null | head -1)
- has_rb=$(find . -name "*.rb" -path "*/lib/*" -o -path "*/app/*" 2>/dev/null | head -1)
-
- echo "Detected languages:"
- [ -n "$has_js" ] && echo " - javascript-typescript"
- [ -n "$has_py" ] && echo " - python"
- [ -n "$has_go" ] && echo " - go"
- [ -n "$has_rs" ] && echo " - rust (note: CodeQL rust is limited)"
- [ -n "$has_java" ] && echo " - java-kotlin"
- [ -n "$has_rb" ] && echo " - ruby"
-
- # Check for over-reach
- if grep -q "language:.*'go'" .github/workflows/codeql.yml && [ -z "$has_go" ]; then
- echo "WARNING: CodeQL configured for Go but no Go files found"
- fi
- if grep -q "language:.*'python'" .github/workflows/codeql.yml && [ -z "$has_py" ]; then
- echo "WARNING: CodeQL configured for Python but no Python files found"
- fi
- if grep -q "language:.*'java'" .github/workflows/codeql.yml && [ -z "$has_java" ]; then
- echo "WARNING: CodeQL configured for Java but no Java files found"
- fi
- if grep -q "language:.*'ruby'" .github/workflows/codeql.yml && [ -z "$has_rb" ]; then
- echo "WARNING: CodeQL configured for Ruby but no Ruby files found"
- fi
-
- echo "CodeQL check complete"
-
- - name: Check Secrets Guards
- run: |
- echo "=== Checking Secrets Usage ==="
- # Look for secrets without conditional guards in mirror workflows
- if [ -f .github/workflows/mirror.yml ]; then
- if grep -q "secrets\." .github/workflows/mirror.yml; then
- if ! grep -q "if:.*vars\." .github/workflows/mirror.yml; then
- echo "WARNING: mirror.yml uses secrets without vars guard"
- echo "Add 'if: vars.FEATURE_ENABLED == true' to jobs"
- fi
- fi
- fi
- echo "Secrets check complete"
-
- - name: Summary
- run: |
- echo ""
- echo "=== Workflow Linter Summary ==="
- echo "All critical checks passed."
- echo ""
- echo "For more info, see: robot-repo-bot/ERROR-CATALOG.scm"
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: Parse workflow contracts and complete action pins
+ run: bash scripts/lint-workflow-contracts.sh
+ - name: Reject floating refs without matching comments or script strings
+ run: bash tests/workflow-contract-controls.sh
diff --git a/README.adoc b/README.adoc
index f3562ee..a897c41 100644
--- a/README.adoc
+++ b/README.adoc
@@ -6,21 +6,26 @@ image:https://img.shields.io/badge/Idris-Inside-5E5086[Idris Inside,link="https:
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
= Poly SSG Comparison Site
+
+[CAUTION]
+====
+*Preservation staging, not an accepted migration.* This repository is a history-preserving extraction of `hyperpolymath/polystack/poly-ssg`. It is currently private; the preservation workflow checks retained Git history and output-checker regressions; runtime qualification remains open. No BoJ backend, build parity, deployment, security or proof claims are independently validated by the extraction. See link:docs/PRESERVATION.adoc[provenance and qualification ledger] and link:https://github.com/hyperpolymath/poly-ssg/issues/2[qualification issue #2]. Polystack remains the fallback while link:https://github.com/hyperpolymath/polystack/issues/30[Polystack #30] is open.
+====
:toc: auto
image:https://img.shields.io/badge/RSR-compliant-blue[RSR Compliant]
-image:https://img.shields.io/badge/status-active-green[Status: Active]
+image:https://img.shields.io/badge/status-unqualified-lightgrey[Status: Unqualified preservation staging]
Interactive comparison of SSG engines (Casket, Hackenbush, Bunsenite, Cobalt) built with ReScript, Deno, and formally verified components from proven.
-== Architecture
+== Historical architecture claims
* *ReScript* - Type-safe frontend compilation
* *Deno* - Modern runtime (no Node.js per RSR)
* *proven* - Formally verified string operations
* *TEA* - The Elm Architecture for state management
-=== Formally Verified Components
+=== Historical verification claims
This site uses *ProvenSafeString* from the proven library for safe config generation, ensuring:
@@ -28,7 +33,7 @@ This site uses *ProvenSafeString* from the proven library for safe config genera
* Memory-safe concatenation
* Proven correctness of output format
-== Development
+== Historical development instructions
=== Build
@@ -53,7 +58,7 @@ deno run -A jsr:@std/http/file-server .
python3 -m http.server 8000
----
-=== GitHub Pages Deployment (Ddraig)
+=== Historical GitHub Pages deployment instructions (unqualified)
Primary Pages deploy now uses `ddraig-ssg` (Idris 2) via:
@@ -78,7 +83,7 @@ echo "axiom.metadatasticaian.art" > ../../_site/CNAME
* *Feature Comparison* - Understand engine capabilities
* *Safe String Operations* - Formally verified config building
-== Project Status (2026-01-30)
+== Historical Project Status (2026-01-30 — not a current verification)
* ✅ ReScript compilation with Deno
* ✅ proven integration for safe strings
@@ -88,6 +93,10 @@ echo "axiom.metadatasticaian.art" > ../../_site/CNAME
* ⏳ Visual comparison table
* ⏳ Interactive feature filtering
+== Retention and retirement
+
+See link:docs/RETENTION-PLAN.adoc[the retention and retirement plan]. Curation belongs in `stuff-of-awe/awesome-ssgs`; implementation history stays in source repositories. The source snapshot has been checked by matching Git tree IDs. A passing preservation workflow does not close the runtime or security findings in qualification issue #2.
+
== Related Projects
* link:https://github.com/hyperpolymath/casket-ssg[Casket SSG] - Haskell implementation
@@ -101,3 +110,4 @@ echo "axiom.metadatasticaian.art" > ../../_site/CNAME
PMPL-1.0-or-later (Palimpsest-MPL)
Philosophy: _One contract. Many paradigms. Every paradigm teaches._
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..3269b5b
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,40 @@
+
+# Security policy
+
+## Supported scope
+
+This repository is private source-preservation staging for historical Poly SSG
+experiments. It offers no supported executable release, hosted service or
+production deployment. The retained source has known, unqualified security
+findings. A passing preservation check establishes only the claim named by
+that check; it does not establish that an engine is safe to run.
+
+Current preservation scripts, workflow definitions and documentation are
+maintained within the qualification work tracked in issue #2. An implementation
+selected for a maintained successor needs its own security and runtime review.
+
+## Reporting a vulnerability
+
+Contact the repository owner at **jonathan.jewell@open.ac.uk**, the reporting
+address already published in this repository's `.well-known/security.txt`.
+Identify the repository, exact commit, affected path, impact and a small
+reproduction where possible. Do not include live credentials or disclose private
+source in a public issue or public pull request. No response-time or remediation
+SLA is promised by this preservation project.
+
+Reports about current automation and exposed credentials require review even
+when the associated source is historical. Preserved-source findings remain
+recorded in complete scanner artifacts and are not described as fixed or false
+positives solely because the source is unsupported.
+
+## Analysis records
+
+GitHub hosted code scanning was unavailable for this private repository when
+the workflow repair was prepared. CodeQL, Semgrep and Scorecard retain complete
+SARIF artifacts and explicit disposition reports. Missing or failed analysis
+cannot become a passing finding gate. Artifact retention is finite; qualification
+issue #2 must retain the accepted evidence needed for final closure.
+
+## Acknowledgments
+
+Reporter attribution will be agreed with the reporter before publication.
diff --git a/docs/PRESERVATION.adoc b/docs/PRESERVATION.adoc
new file mode 100644
index 0000000..35d8b37
--- /dev/null
+++ b/docs/PRESERVATION.adoc
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: CC-BY-SA-4.0
+= Poly SSG — provenance, qualification and release gate
+:toc: left
+
+*Current disposition:* PRIVATE SOURCE-PRESERVATION STAGING. Do not use as a production dependency solely because this repository exists.
+
+== Exact provenance
+
+* Original repository: https://github.com/hyperpolymath/polystack[hyperpolymath/polystack]
+* Original source commit: `e1af43bb102106b08cb694402bd1c04b96f1677c` (main, inspected 2026-11-03)
+* Original prefix: `poly-ssg/`
+* Git history extraction: `git subtree split --prefix=poly-ssg --branch preservation-poly-ssg` in Debian WSL (Git for Windows' shell failed, so WSL was used)
+* Extracted initial commit: `9302dfcc3aedb7c52edf2867a2fcbf2c735878b4`
+* Original retained in Polystack unchanged. Independent Git API reads on 2026-11-04 confirmed that the original `poly-ssg/` subtree and extracted initial commit both name tree `6a8aa93222c8261c3e81b37acd7d1bd59f42bdc4`. The active preservation job also verifies the import remains an ancestor and its objects remain connected.
+
+The extracted history is not a new implementation. The original prefix path has become this repository root. This preserves previous authors/commit history for the subtree; it does not establish its executable behaviour.
+
+== Important environmental warnings
+
+* The Windows source checkout flagged case-sensitive path collisions in `poly-ssg/satellites/rats-ssg/{Justfile,justfile}`, which cannot both appear correctly on a case-insensitive checkout. Compare Git trees, not just Windows worktree files.
+* `.gitmodules` refers to multiple individual historical SSG repositories; many are now consolidated into https://github.com/hyperpolymath/ssg-collection[ssg-collection]. The references and whether they are actual gitlinks or merely stale metadata need review before automated checkouts.
+* `sites-build/` includes historical DNS, security-policy and generator outputs, not an approved current deployment configuration. Do not deploy or distribute generated DNS zones unreviewed.
+* The historical README still contains claims originating in early 2026. Those claims are not current assurance.
+* Actions were initially disabled before import. Runs inspected on 2026-11-04 establish that inherited scheduled workflows are executing again. The current preservation CI exercises Git provenance and positive/negative output-checker controls; it does not establish generator or consumer qualification. The former root build matrix is retained at `docs/legacy-workflows/ci.yml.txt` because it referenced absent Ddraig/Forth implementations and could report tests without running them. Do not use zero jobs as a successful gate.
+
+== Successor relationships are still open
+
+* https://github.com/hyperpolymath/ssg-collection[ssg-collection] contains overlapping experimental engines, variants and scaffolds; the file-level differences are catalogued at https://github.com/hyperpolymath/polystack/pull/31[Polystack PR #31].
+* https://github.com/hyperpolymath/boj-server-cartridges/tree/main/cartridges/domains/web/ssg-mcp[BoJ ssg-mcp] currently declares five operations and delegates to a separate HTTP backend (default `127.0.0.1:7742`). Backend presence and runtime equivalence remain unverified.
+* https://github.com/hyperpolymath/boj-server-cartridges/tree/main/cartridges/domains/web/ssg-lsp[BoJ ssg-lsp] is described as scaffolded; it preserves the original Elixir LSP source as source material, not a finished port.
+* https://github.com/stuff-of-awe/awesome-ssgs[stuff-of-awe/awesome-ssgs] is an independent **curation collection**, not an SSG framework or backend.
+
+== Release acceptance
+
+https://github.com/hyperpolymath/poly-ssg/issues/2[Qualification issue #2] is the authoritative evidence checklist. The minimum gate is correct source correspondence; a tracked ownership/licence and dependency inventory; clean, non-vacuous CI on the latest SHA; one fully demonstrated site build with negative controls; an accepted consumer adapter; and all applicable app reviews and conversations resolved.
+
+Only after those tests and reviews may the owner decide on publication, supported status or closing https://github.com/hyperpolymath/polystack/issues/30[Polystack's archive blocker].
+
+*No archival, deployment, or release approval is implied by this document.*
+
+
+== Workflow recovery receipt (2026-11-04)
+
+* Semgrep run 41777623670 scanned 2,351 targets with 503 rules and reported 313 findings. Its final SARIF upload failed because the job omitted `actions: read`; adding that read permission repairs report publication. The scan remains a findings report, not a clean-security attestation. Its image is pinned to the digest observed in that executed run. Findings remain to be triaged or covered by an explicit historical-source disposition.
+* Scorecard run 41771377426 failed before scanning because its old GCR image could not be pulled. The replacement is upstream v2.5.0 at `fa7117db83c52d787ae5a89f350999701505d78e`, which uses GHCR; its tag was verified by GitHub. Hosted execution must still establish this repair works.
+* The output checker previously treated every nonempty file as containing NUL, exited on the first zero-valued arithmetic increment, lost directory counters in a subshell, and could lose early grep matches under pipefail. Positive and negative controls now cover those failures, including large input, empty populations and malformed encoding. The checker remains a heuristic for listed patterns.
+* Polystack PR #31 has merged at `218ccc9b9e458c36a8ff783fd762941abd4a4439`; its structural comparison is retained evidence, not runtime parity.
diff --git a/docs/RETENTION-PLAN.adoc b/docs/RETENTION-PLAN.adoc
new file mode 100644
index 0000000..1bf9dc4
--- /dev/null
+++ b/docs/RETENTION-PLAN.adoc
@@ -0,0 +1,344 @@
+// SPDX-License-Identifier: CC-BY-SA-4.0
+= Poly SSG: preserve the useful work and retire the original cleanly
+:revdate: 2026-11-04
+:toc:
+:toclevels: 2
+
+*Status: proposed component dispositions, prepared for Poly SSG PR #3.*
+This plan records a finite route to retirement. The independent Git tree
+comparison is recorded in `PRESERVATION.adoc`: the original `poly-ssg/` subtree
+and the extracted root both identify tree
+`6a8aa93222c8261c3e81b37acd7d1bd59f42bdc4`. That establishes source-tree
+correspondence, including file modes; it does not report an archive, release,
+working replacement, or completed runtime qualification. The preservation gate
+checks the imported commit's tree identity, ancestry and object connectivity.
+
+== Intended outcome
+
+Retain the original Git history and useful experiments in an identified source
+snapshot; publish useful, original educational and discovery material in
+`stuff-of-awe/awesome-ssgs`; assign any selected runtime work to a maintained
+successor; and explicitly discontinue the remaining experiments. Close the
+retirement work when those decisions and their evidence are recorded.
+
+There is no requirement to finish every proposed generator, implement every
+historical feature, or make every archived experiment build. A functioning
+successor needs execution evidence. A discontinued experiment needs preserved
+source, an honest status, and removal of any claim that a live consumer can
+depend on it. These are different outcomes and should be recorded separately.
+
+The proposed end state uses GitHub archival, with history and a clear successor
+notice retained. Repository deletion is outside this plan. Retirement of Poly
+SSG and archival of its parent Polystack are separate repository decisions;
+the SSG work alone cannot establish that every other Polystack domain is ready.
+
+== Inspection baseline and source references
+
+The following refs were read on 2026-11-04. Freeze the final refs again when
+recording completion; these values are evidence baselines, not moving-branch
+guarantees.
+
+[cols="2,3,3",options="header"]
+|===
+|Repository or item |Observed ref/state |Meaning
+|Original Polystack source
+|`e1af43bb102106b08cb694402bd1c04b96f1677c`, prefix `poly-ssg/`
+|Frozen extraction source recorded in the preservation documents.
+|Extracted `hyperpolymath/poly-ssg` main
+|`9302dfcc3aedb7c52edf2867a2fcbf2c735878b4`
+|Existing private source-preservation snapshot. Its root tree matches the
+frozen original prefix tree; see `PRESERVATION.adoc`. Runtime and full historical
+authorship qualification remain separate from that comparison.
+|Poly SSG PR #3, initial inspection
+|`7ab68dddd6bfd01f3f4d62fff9ed6804835ea2e0`
+|Documentation branch before the present workflow repairs. Use its eventual
+final head for CI and review evidence.
+|Polystack current main / PR #31
+|`218ccc9b9e458c36a8ff783fd762941abd4a4439`; PR #31 merged
+|The initial source-hash preservation ledger is already on main. It is no
+longer a draft PR, and merging it did not complete migration.
+|`hyperpolymath/ssg-collection` main
+|`e5b7e7fd9af511a1c339cfbd2824e0cccd03fada`
+|Existing experiments collection used by the structural comparison.
+|`hyperpolymath/boj-server-cartridges` main
+|`2fd1fa3b18fbe613f21397506acf09f7d07b072f`
+|Consumer snapshot referenced by the public migration tracker.
+|`stuff-of-awe/awesome-ssgs` main
+|`2e9ac1af7988638f58ff8c17c9d074a3931f81cb`
+|Existing public starter collection, contribution rules, source map and local
+curation checks.
+|===
+
+Primary records:
+
+* https://github.com/hyperpolymath/poly-ssg/pull/3[Poly SSG PR #3].
+* https://github.com/hyperpolymath/poly-ssg/issues/2[Poly SSG qualification #2].
+* https://github.com/hyperpolymath/polystack/issues/30[Polystack preservation blocker #30].
+* https://github.com/hyperpolymath/polystack/blob/218ccc9b9e458c36a8ff783fd762941abd4a4439/docs/SSG-PRESERVATION-LEDGER.adoc[Merged structural comparison ledger].
+* https://github.com/hyperpolymath/boj-server-cartridges/issues/165[Public BoJ migration tracker #165].
+* https://github.com/stuff-of-awe/awesome-ssgs/blob/2e9ac1af7988638f58ff8c17c9d074a3931f81cb/docs/SOURCE-MAP.md[Public curation source map].
+
+== Three claims, with separate evidence
+
+[cols="2,4,3",options="header"]
+|===
+|Claim |Minimum evidence |What the claim establishes
+|Verified preserved snapshot
+|Original commit and prefix; extracted commit; matching Git tree identity or
+an explicit file/mode/blob comparison; extraction method; retained authorship
+and history; recorded licences and any unresolved notices; a durable ref.
+|The material can be recovered with its provenance. It establishes no runtime
+or integration guarantee.
+|Working successor
+|Named repository and exact commit; supported operations; pinned toolchain;
+executed input-to-output tests; negative cases; output inspection; applicable
+reviews and real CI; consumer ownership and documented limitations.
+|Only the tested functions and environments have a working replacement.
+|Deliberately discontinued function or experiment
+|Explicit disposition and rationale; preserved source ref; dependency and
+consumer review; corrected availability/status documentation; any live caller
+receives a clear unsupported/unavailable result rather than an implied service.
+|The function has been retired intentionally. No implementation is promised
+merely to close the preservation task.
+|===
+
+Every source area receives a preservation state and an operational disposition.
+Public curation is an additional decision. For example, an experiment may be
+fully preserved, deliberately discontinued, and still useful in a historical
+learning section.
+
+== Source-area disposition map
+
+The preferred default is to retain the complete extracted snapshot, then move
+only selected, understood material into maintained destinations. This allows
+unique source to remain recoverable without manufacturing new maintenance
+obligations or forcing incomplete material into an active codebase.
+
+[cols="3,4,4",options="header"]
+|===
+|Source area |Proposed destination and disposition |Evidence needed before retirement
+|`src/`, `lib/`, `public/`, `content/`, `config.ncl`, `deno.lock`, `Justfile`, `Mustfile`
+|Preserve framework, comparison UI, generated artefacts and build configuration
+in the extracted source snapshot. Continue the application only if an actual
+consumer is identified; otherwise record it as historical and discontinued.
+|Inventory authored versus generated files. Record the intended entry point
+and manifest gaps. A successor claiming to run the application needs a real
+build; historical retention does not require repairing the application.
+|`engines/`, root `casket-ssg/`, embedded Casket and Ddraig satellite copies
+|Preserve all copies first. Send selected fixes to the independently maintained
+engine repositories after comparison. Keep unresolved or obsolete variants in
+the historical snapshot, with links to current canonical engines.
+|Compare paths, blob hashes, licences and unique source/tests. Check renames
+and semantic changes before declaring a copy redundant. Test only changes
+adopted by the maintained engine.
+|`satellites/`
+|Preserve all 50 directories. Reconcile selected useful changes with
+`ssg-collection/{implementations,variants,stubs}`. Record unselected ideas as
+historical experiments without a future implementation promise.
+|Use the existing frozen comparison as a starting point. For every changed or
+unique source file selected for migration, record the accepted destination
+commit. Every unselected unique file remains covered by the preserved snapshot.
+|`docs/PHILOSOPHY.adoc`, `docs/DESIGN-SPEC-TEMPLATE.adoc`, `docs/specs/`
+|Retain originals as history. Publish newly written comparison and educational
+notes in Awesome SSGs. Link public implementation evidence where available.
+|Separate design intent from executed capabilities. Correct or omit unsupported
+historical toolchain, proof, maturity and effort claims. Record attribution
+without copying private documents into the public collection.
+|`docs/ADDING-ENGINE.adoc`, `test-corpus/`, `scripts/`
+|Keep the intended build contract and fixtures in source history. Adopt useful
+fixtures into a selected runtime or engine successor; curated notes may explain
+the comparison method using original prose and small synthetic examples.
+|Check stale command/path references, input/output expectations, draft handling,
+exit codes and negative cases. Only adopted fixtures need to become executable
+successor tests; abandoned harnesses can remain explicitly historical.
+|`mcp/`
+|Preserve the original implementation. Track any maintained consumer contract
+in BoJ #165. Select a narrow backend/engine set, or explicitly discontinue
+unprovided operations and correct consumer availability claims.
+|Inventory the historical operations against the actual BoJ adapter. A working
+replacement requires backend ownership and real load/build/output tests.
+Retirement requires an explicit supported/unavailable decision for each
+advertised operation and no hidden dependency on the old source location.
+|`lsp/`
+|Preserve the Elixir implementation and compare it with BoJ's existing source
+archive. Either maintain a narrow, tested LSP successor or mark the LSP effort
+discontinued; scaffold preservation alone is not a functioning port.
+|Check source correspondence and licences. If retained as a service, test
+protocol framing and the selected handlers. If discontinued, remove claims of
+working handlers and record that no replacement service is supplied.
+|`sites-build/`, `poly-ssg.org.zone`, `_headers`, `.well-known/`
+|Retain historical outputs and deployment context privately. Assign any live
+site or domain to its actual maintained owner; retire unneeded deployment
+paths. Do not copy environment-specific configuration into the curated list.
+|Identify live consumers and domain references, intended operators, source
+inputs and generated outputs. Review any configuration that is adopted for
+current use. Record old reports as historical; no live deployment is needed
+to demonstrate archival preservation.
+|`.github/`, `hooks/`, `config/`, `contractiles/`
+|Use the PR #3 repair to establish a small, meaningful preservation and
+documentation gate. Keep old deployment workflows recoverable in history.
+Each maintained successor owns its own applicable CI and contract checks.
+|Execute positive and negative controls on the final PR SHA; inspect workflow
+permissions and triggers; resolve substantive review findings. Documentation
+checks must state their limited scope. Stop obsolete recurring automation as
+part of actual retirement, after the final repair and receipt are recorded.
+|`.gitmodules`, `.editorconfig`, `.gitattributes`, `.gitignore`, `.bot_directives/`, `.claude/`
+|Preserve repository and historical automation context with the source. Adapt
+only genuinely applicable metadata in maintained successors.
+|The inspected extracted tree is untruncated and contains no `160000` gitlinks,
+despite `.gitmodules` listing satellite entries. Record this distinction;
+do not initiate recursive legacy checkouts merely because metadata remains.
+Keep both case-colliding `rats-ssg/Justfile` and `justfile` in the Git snapshot.
+|`license/`, `PALIMPSEST.adoc`, file-level notices and `README.adoc`
+|Preserve original notices and provenance. Put an accurate historical/successor
+notice at the retained repository entry point. New curation text follows
+Awesome SSGs' CC BY-SA 4.0 rules; source code retains its own notices.
+|Record the existing MPL/PMPL notice inconsistency rather than silently
+relicensing material. Check the actual files selected for reuse. Unknown
+redistribution terms keep a copy in private preservation pending resolution.
+|===
+
+The structural ledger records 46 directly matching satellite basenames and four
+unmatched source basenames: `anvil`, `casket`, `ddraig`, `obli`. Its five
+destination-only names are `befunge`, `cloudanvil`, `ephapax`, `shift`,
+`wokelang`. In particular, decide the `anvil`/`cloudanvil` relationship through
+comparison, not name similarity. The ledger's file-count categories are not
+runtime qualification levels.
+
+The historical design index also needs editorial care: its summary advertises
+14 engine proposals and five archive proposals, while the detailed tables
+enumerate 12 and seven respectively. Retain that original document as history;
+use a corrected, dated account for any new public explanation.
+
+== Public curation and private preservation
+
+`stuff-of-awe/awesome-ssgs` already supplies the intended public home. Its scope
+is useful discovery, comparative evidence, unusual-language research and honest
+maturity labels. It has a curation gate, not an SSG runtime qualification gate.
+
+Recommended bounded addition:
+
+. Add `docs/COMPARISON-NOTES.md`, with original guidance on comparing the same
+ publishing task across implementations and recording reproducible results.
+. Link it from the existing "Compare generators usefully" section.
+. Retain the existing starter status and distinction between inspected source,
+ executable examples and verified integrations.
+. Update the public source map only when the underlying disposition changes.
+ Use public tracker #165 for public migration status; keep private source URLs,
+ source excerpts, environment details and private review discussion out of the
+ public article.
+. Run the existing local catalogue validator and tests. A passing curation gate
+ establishes link/notice/status checks within its stated scope; it does not
+ verify the linked generators.
+
+The destination contribution policy and validator were inspected. Every public
+Markdown document needs the CC-BY-SA-4.0 marker. Local links must resolve inside
+the repository, and the validator rejects private Polystack/Poly SSG source
+URLs. Third-party code must remain in its appropriately licensed source home.
+
+== Ordered completion work
+
+=== 1. Finish the bounded workflow and documentation repair
+
+Finish PR #3 on its existing branch. Establish the preservation/documentation
+checks, execute them on the final head, address applicable review findings and
+merge once the required checks genuinely run and pass. Record the resulting
+merge SHA and check-run URLs. This is a completed documentation/CI repair even
+if separate runtime questions remain open.
+
+=== 2. Attach a preservation receipt
+
+Record the original source commit, prefix tree, extracted commit/tree and the
+commands/results of the independent correspondence check. Verify history and
+authorship retention separately from current-tree equality. Preserve the exact
+snapshot using a durable Git ref and, where needed, an independently recoverable
+bundle or mirror; verify its object integrity. Do not create a public release
+of private material as an incidental backup action.
+
+The receipt must distinguish verified results from documented claims. It must
+cover executable file modes and the case-colliding paths, and identify any
+external dependencies or unavailable historical objects. No success field is
+pre-filled by this plan.
+
+=== 3. Record a finite component decision ledger
+
+For each source-area row, record: preservation receipt; proposed canonical
+destination; maintained / discontinued / unresolved operational disposition;
+licence state; current consumers; and evidence or remaining task.
+
+Prioritise live consumers and unique executable source/tests. Select only work
+that is useful enough to maintain. Unselected material can be closed as a
+preserved, discontinued experiment. Avoid creating a new issue for every
+historical sketch. A selected unfinished runtime task belongs in its current
+owner's repository and does not become a false completed-migration claim.
+
+=== 4. Preserve public educational value
+
+Land the focused Awesome SSGs article and index link with the destination's
+existing checks. Add or update individual experimental entries only when their
+canonical source, language, licence and status have been assessed. Keep the
+historical source snapshot intact while this public curation improves.
+
+=== 5. Resolve consumer availability and archival scope
+
+For each claimed BoJ operation, record working replacement or deliberate
+unavailability. A selected working build path needs actual output and negative
+controls. Unsupported deployment, preview, clean or LSP functionality can be
+explicitly retired without being implemented, provided consumers and
+documentation are corrected and the retirement decision is recorded.
+
+Update qualification #2 and public tracker #165 to reflect the adopted scope.
+If an original checklist item has become a deliberate discontinuation, record
+that outcome and its reason rather than ticking it as successfully implemented.
+Keep any actively promised runtime work open in its appropriate tracker.
+
+For Polystack #30, record source preservation and the exact SSG successor or
+discontinuation mapping. Check the separate disposition of other Polystack
+domains before treating that as authorisation or evidence to archive the
+entire parent repository.
+
+=== 6. Retire and verify
+
+Replace active-status claims with an accurate retirement notice, canonical
+successor links, unsupported-function notes and the retained snapshot ref.
+Reconcile open issues and PRs: completed preservation, superseded work,
+deliberately declined experiments, and transferred maintained tasks each need
+their correct outcome. Keep useful review history accessible.
+
+Once the scoped retirement decision is recorded and live dependencies are
+resolved, archive the identified original repository and stop obsolete
+recurring automation. Verify that the repository is archived, the preserved
+ref remains readable, and the public guide and live consumers point to the
+intended destinations. Record these observations in a final closure receipt.
+
+== Completion criteria
+
+* The exact source snapshot and provenance are verified and recoverable.
+* The repaired documentation/workflow PR has real passing checks at its final
+ head and an observed merge result.
+* Each broad source area is assigned a destination or preserved historical
+ disposition; no unique source is discarded because a similarly named repo exists.
+* Public curation retains useful explanations with accurate status and licence
+ notices, without exposing private source material.
+* Each actual consumer has a working successor or a recorded, explicit
+ unavailability/discontinuation decision.
+* Open runtime promises remain tracked honestly; discontinued experiments do
+ not require new implementations to permit preservation closure.
+* The exact repository being retired, its archival result and stopped obsolete
+ automation are recorded separately from preservation and runtime testing.
+
+== Decisions still to record
+
+* Which repository is the final archival target: the extracted historical Poly
+ SSG repository, parent Polystack, or both after their respective dependencies
+ are resolved?
+* Which concrete runtime functions, if any, are selected for continuing
+ maintenance, and which are deliberately discontinued?
+* Who owns the retained source snapshot and the continuing curated collection?
+* What is the resolution of file-level licence ambiguities for any source chosen
+ for redistribution or active reuse?
+
+These decisions refine the already requested preservation-and-retirement work.
+They do not invalidate completed workflow repair, source verification or public
+curation, and they do not authorise deletion or incidental publication of private
+source.
diff --git a/docs/WORKFLOW-DISPOSITION.adoc b/docs/WORKFLOW-DISPOSITION.adoc
new file mode 100644
index 0000000..5bfda1a
--- /dev/null
+++ b/docs/WORKFLOW-DISPOSITION.adoc
@@ -0,0 +1,209 @@
+// SPDX-License-Identifier: CC-BY-SA-4.0
+= Preservation workflow and findings disposition
+:revdate: 2026-11-04
+
+This repository retains unqualified historical source. The active work is source
+preservation, review and explicit retirement; it supplies no supported generator
+release. `PRESERVATION.adoc`, `RETENTION-PLAN.adoc` and qualification issue #2
+remain the scope records. This document records workflow decisions, not a claim
+that their new hosted executions have already passed.
+
+== Identity before historical classification
+
+The immutable import is `9302dfcc3aedb7c52edf2867a2fcbf2c735878b4`, with tree
+`6a8aa93222c8261c3e81b37acd7d1bd59f42bdc4`. The active checks verify that tree,
+its object connectivity and its ancestry in the exact checked-out subject.
+
+The language gate enumerates the complete committed subject tree. Prohibited
+source is retained only when the exact path, regular-file mode and Git blob
+match that import and the path is in an explicitly retained source area.
+Changed modes, relocated copies and newly added files cannot inherit the
+historical disposition. TypeScript implementation, Go, Python, Java/Kotlin,
+Swift, Dart, npm/yarn/pnpm lockfiles and tsconfig are checked; declaration-only
+`.d.ts` files retain the existing FFI exception. Bun manifests are allowed by
+the current repository language instructions. The obsolete Deno policy runner
+remains available in source history but is not the active policy authority.
+
+At the inspected PR subject `f82227bcbd13f27eab207ff791aa79639decefd4`, the two
+prohibited-language objects were:
+
+[cols="3,1,3",options="header"]
+|===
+|Path |Mode |Imported and current blob
+|`engines/milk-ssg/milk-ssg.py` |`100755` |`1024c994bf5ea704d2c9c791815b34ddba7e133e`
+|`engines/terrapin-ssg/terrapin-ssg.py` |`100755` |`b6ee989e6176c775c7b95aa56f2fa32df800a8e6`
+|===
+
+They remain unsupported historical experiments. This is not permission to
+introduce or develop Python runtime code, and neither file is deleted to
+make a language check pass.
+
+This gate checks a *source-object delta*, not execution or activation. An added
+shell wrapper could invoke an unchanged historical file while the exact
+path/blob/mode comparison still passes. A regression fixture records that
+boundary, and both console and job-summary output state it explicitly. Reviews
+of workflow, script, build and package entry points remain a separate activation
+boundary: retaining an object does not authorize running it. No automated
+whole-program non-activation guarantee is claimed. A maintained successor must
+qualify its selected runtime and dependencies before enabling those entry points.
+
+== Complete security evidence
+
+The previous runs successfully produced analysis, then failed to upload it to
+GitHub code scanning, which is unavailable for this private repository:
+
+* https://github.com/hyperpolymath/poly-ssg/actions/runs/41878049639[CodeQL]
+ exported JavaScript/TypeScript SARIF before the integration API rejected it.
+* https://github.com/hyperpolymath/poly-ssg/actions/runs/41878049561[Semgrep]
+ ran 503 rules against 2,355 targets and reported 313 findings. Those findings
+ were not thereby fixed or established as false positives.
+* https://github.com/hyperpolymath/poly-ssg/actions/runs/41878049542[Scorecard]
+ reported `SecurityPolicyID` and `FuzzingID`; its later upload was rejected.
+
+The repaired jobs continue the real analyses and retain complete raw SARIF in
+90-day artifacts. CodeQL uses upstream v4.39.1 and its documented `upload:
+never` and `upload-database: false` controls. Semgrep retains the exact image
+digest observed in the executed scan and the existing `--config=auto` rule
+selection. That rule registry can change; the artifact describes its actual
+run, not a permanently frozen rule corpus. Scorecard publishing remains off.
+No private source or result is published to a public destination by this change.
+
+The disposition script does not remove results from SARIF. It records each
+original result with one of three decisions:
+
+. *Retained historical source*: every reported location is in an enumerated
+ unsupported source area and has the exact imported path, blob and file mode.
+ The finding remains present and unremediated. The areas are `engines/`,
+ `satellites/`, `casket-ssg/`, `src/`, `lib/`, `public/`, `content/`, `mcp/`,
+ `lsp/`, `test-corpus/` and `sites-build/`. A prefix alone is insufficient.
+. *Preservation advisory*: only Scorecard's exact observed `FuzzingID` condition
+ that no fuzzing integration exists. This repository provides no supported
+ executable release. A selected runtime successor must qualify its own fuzzing;
+ this disposition does not claim that fuzzing ran.
+. *Actionable*: every other finding, including current automation, new or
+ changed source, missing source locations, unrecognised paths and missing
+ security policy. These findings fail the job and remain in the artifact.
+
+Location collection follows the source-bearing result structures in the
+https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json[SARIF 2.1.0 schema].
+It includes primary and related locations, the analysis target, nested code-flow
+locations and stacks, result stacks, graph nodes and children, fixes,
+attachments, suppression locations and conversion-source provenance. Every
+collected artifact must independently satisfy the exact imported-object check.
+A retained primary location cannot cover a current secondary location.
+
+The supported form is an explicit source-relative URI, optionally based at
+`%SRCROOT%`. Missing and logical-only locations, cached indices, unresolved
+location relationships or graph traversals, address-based/runtime evidence,
+unknown source bases and opaque extension fields remain actionable. The script
+does not guess their referents. Externalized report data is rejected as an
+incomplete analysis. The narrow `FuzzingID` advisory requires a nonempty primary
+location population and every collected location to be the observed no-file
+placeholder; an added real-file location prevents that advisory. Each receipt
+retains both the original result and its collected location evidence.
+
+The newly supplied root `SECURITY.md` resolves the missing reporting-policy
+document by using the contact already recorded in `.well-known/security.txt`.
+Hosted execution must confirm the resulting Scorecard finding set. No response
+SLA or supported historical release is invented.
+
+Missing, malformed or failed analyses return an infrastructure/configuration
+failure rather than a clean result. Each disposition binds the raw report's
+SHA-256 and the checked-out subject SHA. CodeQL and Semgrep analyze that checkout.
+Upstream Scorecard v2.5.0 scans the local checkout on pull requests but queries
+the remote default branch for other events. The latter reports explicitly leave
+the analyzed SHA unknown and do not classify source findings against a possibly
+different checkout. The reports explicitly say hosted
+upload is unavailable and make no clean-security claim. Accepted evidence needed
+for permanent closure must be retained beyond the finite artifact lifetime in
+qualification issue #2's closure record.
+
+== Other scanner and policy repairs
+
+The Secret Scanner directly calls the published CI-suite implementation at
+`52a90c24bf183437b887dd182c5a9406af02c5dd`, with full history and `fast-pr:
+false`. Its required contexts are `scan / gitleaks`, `scan / rust-secrets` and
+`scan / shell-secrets`; the latter two validate actual detector receipts.
+Independent TruffleHog verified-credential scanning is retained. Secret findings
+are not covered by the historical SARIF disposition and still block.
+
+Hypatia uses the canonical standards reusable at
+`d3e4b485a2af90c92a8e18af0888cd19cb132cd9`, with its declared permissions and
+only its declared optional scan secret. There is no caller-local scanner clone,
+floating scanner implementation, jq-to-zero fallback or unsupported fleet
+submission. The canonical verdict grid and artifacts report missing evidence
+and findings. Its default advisory mode is not a clean-security attestation.
+
+The workflow linter parses actual YAML job and step `uses` keys, job containers
+in string or object form, and every service container image. Full action SHAs,
+literal SHA-256 image digests, explicit permissions and SPDX headers are required.
+Missing, malformed, floating or expression-derived image references fail the
+contract; a dynamic expression is not treated as proof of a digest pin.
+Comments and script string literals cannot masquerade as action references;
+`actions/github-script` no longer receives an implicit pinning exemption.
+
+== Retired deployment and propagation effects
+
+The following exact files, including their notices, are retained as inert text:
+
+[cols="3,3,4",options="header"]
+|===
+|Former active workflow |Retained path |Disposition
+|`.github/workflows/ci.yml`
+|`docs/legacy-workflows/ci.yml.txt`
+|The former unqualified runtime build is retained as an inert record. The active
+replacement checks preservation and independent regression controls.
+|`.github/workflows/deploy-casket.yml`
+|`docs/legacy-workflows/deploy-casket.yml.txt`
+|Unsupported manual deployment referenced missing `casket-simple`. Retain the
+source record; no current deployment is supplied.
+|`.github/workflows/deploy-ddraig.yml`
+|`docs/legacy-workflows/deploy-ddraig.yml.txt`
+|Unsupported automatic/manual deployment used an unqualified Ddraig build and
+a floating image. Retain the source record; no current deployment is supplied.
+|`.github/workflows/instant-sync.yml`
+|`docs/legacy-workflows/instant-sync.yml.txt`
+|Push/release dispatch into `.git-private-farm` is retired for this preservation
+repository. No propagation event is emitted by the archival workflow surface.
+|`.github/workflows/mirror.yml`
+|`docs/legacy-workflows/mirror.yml.txt`
+|Seven-forge propagation, including force-push jobs and an unqualified Radicle
+installer, is retired. Its exact original source remains available for review.
+|===
+
+This removes unsupported deployment and propagation effects from the active workflow surface.
+It does not remove source history or claim that the former deployments worked.
+Any maintained successor must establish its own deployment and runtime gates.
+No remote forge history or mirror is deleted or rewritten by this change.
+
+`scripts/verify-archived-workflows.sh` checks exact archive bytes against the
+original Git blobs. The CI original is taken from
+`7ab68dddd6bfd01f3f4d62fff9ed6804835ea2e0`, the parent of its archive-creation
+commit. The other four originals are taken from
+`f82227bcbd13f27eab207ff791aa79639decefd4`, the parent of their archival commit.
+These are identity checks, not use of Git's object hash as a security attestation.
+
+[cols="2,4",options="header"]
+|===
+|Original workflow |Exact original blob retained
+|`ci.yml` |`10835026204a248fd671748b2215b1bccc38a771`
+|`deploy-casket.yml` |`a865df41062cb761bbeb7a7f04b9e7e91e75deb5`
+|`deploy-ddraig.yml` |`8d4ed07ca40fb96dd5be39622a9614256d9b42a1`
+|`instant-sync.yml` |`3c784538bf4c1b434ec913874ba4b45e7f91b185`
+|`mirror.yml` |`a7b1547af5637dacea5690deb0c1d492d7b56370`
+|===
+
+== Validation and remaining evidence
+
+Local controls exercise actual Git object comparisons and reject changed source,
+copied/relocated paths, changed modes, wrong trees, malformed reports, failed
+analysis and wrong reporter identities. Mixed current/historical secondary
+locations and unresolved references cannot inherit a historical disposition.
+Parser controls reject floating action and job/service image refs while accepting
+inert comments. Archive controls reject appended bytes, absent files and symlinks.
+Those synthetic controls validate gate
+behaviour; they do not substitute for real scans of the final PR SHA.
+
+The final branch still needs hosted checks, complete findings review,
+required-context consistency and applicable app reviews. Actual repository
+archival and permanent closure remain separately recorded actions.
diff --git a/docs/legacy-workflows/ci.yml.txt b/docs/legacy-workflows/ci.yml.txt
new file mode 100644
index 0000000..1083502
--- /dev/null
+++ b/docs/legacy-workflows/ci.yml.txt
@@ -0,0 +1,275 @@
+# SPDX-License-Identifier: MPL-2.0-or-later
+# Continuous Integration - Strict Mode
+# All warnings treated as errors, all tests must pass
+name: CI
+
+on:
+ push:
+ branches: [main, develop]
+ pull_request:
+ branches: [main]
+
+permissions: read-all
+
+env:
+ OCAML_VERSION: "4.14.x"
+ # Treat warnings as errors
+ OCAML_COLOR: always
+
+jobs:
+ # OCaml/ReScript-WASM build and test - STRICT MODE
+ rescript-wasm:
+ name: ReScript WASM-GC Backend
+ runs-on: ${{ matrix.os }}
+ permissions:
+ contents: read
+
+ strategy:
+ fail-fast: true # Fail immediately on first error
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ ocaml-version: ["4.14.x", "5.1.x"]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Set up OCaml ${{ matrix.ocaml-version }}
+ uses: ocaml/setup-ocaml@7c185f622998a008ba1b296d2d7c64e235629c23 # v3.6.1
+ with:
+ ocaml-compiler: ${{ matrix.ocaml-version }}
+ dune-cache: true
+
+ - name: Install dependencies
+ run: opam install . --deps-only --with-test -y
+ working-directory: engines/rescript-wasm
+
+ - name: Build with strict warnings
+ run: |
+ opam exec -- dune build --force
+ working-directory: engines/rescript-wasm
+
+ - name: Run all tests (must pass)
+ run: |
+ opam exec -- dune test
+ echo "All tests passed successfully"
+ working-directory: engines/rescript-wasm
+
+ - name: Generate sample WAT
+ run: |
+ opam exec -- dune exec rescript_wasm -- add > add.wat
+ opam exec -- dune exec rescript_wasm -- max > max.wat
+ opam exec -- dune exec rescript_wasm -- square > square.wat
+ opam exec -- dune exec rescript_wasm -- combined > combined.wat
+ echo "=== add.wat ==="
+ cat add.wat
+ working-directory: engines/rescript-wasm
+
+ - name: Install wasm-tools
+ if: runner.os == 'Linux'
+ run: |
+ curl -L https://github.com/bytecodealliance/wasm-tools/releases/download/v1.219.1/wasm-tools-1.219.1-x86_64-linux.tar.gz | tar xz
+ sudo mv wasm-tools-1.219.1-x86_64-linux/wasm-tools /usr/local/bin/
+
+ - name: Validate WAT output (strict)
+ if: runner.os == 'Linux'
+ run: |
+ # Validate all generated WAT files
+ for wat in add.wat max.wat square.wat combined.wat; do
+ echo "Validating $wat..."
+ wasm-tools validate --features gc "$wat" || echo "Note: WAT validation requires WASM-GC support"
+ done
+ working-directory: engines/rescript-wasm
+
+ - name: Upload WAT artifacts
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ with:
+ name: wat-output-${{ matrix.os }}-${{ matrix.ocaml-version }}
+ path: engines/rescript-wasm/*.wat
+
+ # Forth Estate build
+ forth-estate:
+ name: Forth Estate SSG
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Install Gforth
+ run: sudo apt-get update && sudo apt-get install -y gforth
+
+ - name: Check Forth syntax
+ run: |
+ if [ -f forth-estate.fs ]; then
+ gforth forth-estate.fs -e bye
+ fi
+ working-directory: engines/forth-estate
+
+ # Ddraig (Idris 2) build and test
+ ddraig-ssg:
+ name: Ddraig SSG
+ runs-on: ubuntu-latest
+ container: ghcr.io/idris-community/idris2:latest
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Build Ddraig executable
+ run: |
+ cd engines/ddraig-ssg
+ idris2 Ddraig.idr -o ddraig
+
+ - name: Run Ddraig parser tests
+ run: |
+ cd engines/ddraig-ssg
+ ./build/exec/ddraig test-markdown
+ ./build/exec/ddraig test-frontmatter
+ ./build/exec/ddraig test-full
+
+ - name: Build site with Ddraig
+ run: |
+ cd engines/ddraig-ssg
+ ./build/exec/ddraig build ../../content ../../_site
+
+ # Strict lint and format checks - ALL MUST PASS
+ lint:
+ name: Lint & Format (Strict)
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Set up OCaml
+ uses: ocaml/setup-ocaml@7c185f622998a008ba1b296d2d7c64e235629c23 # v3.6.1
+ with:
+ ocaml-compiler: "4.14.x"
+
+ - name: Check for unsafe patterns (MUST PASS)
+ run: |
+ echo "=== Checking for unsafe patterns ==="
+ ERRORS=0
+
+ # Check for Obj.magic (type safety violation)
+ if grep -rn "Obj\.magic" engines/rescript-wasm/lib/; then
+ echo "ERROR: Obj.magic found - this violates type safety"
+ ERRORS=$((ERRORS + 1))
+ fi
+
+ # Check for Marshal (code execution risk)
+ if grep -rn "Marshal\." engines/rescript-wasm/lib/; then
+ echo "ERROR: Marshal found - this is a code execution risk"
+ ERRORS=$((ERRORS + 1))
+ fi
+
+ # Check for shell execution
+ if grep -rn "Unix\.system\|Unix\.open_process\|Sys\.command" engines/rescript-wasm/lib/; then
+ echo "ERROR: Shell execution found - security risk"
+ ERRORS=$((ERRORS + 1))
+ fi
+
+ if [ $ERRORS -gt 0 ]; then
+ echo "Found $ERRORS security issues"
+ exit 1
+ fi
+ echo "No unsafe patterns found"
+
+ - name: Check for catch-all patterns (MUST PASS)
+ run: |
+ echo "=== Checking for dangerous catch-all patterns ==="
+ # Look for standalone _ patterns in match statements (potential bug hiding)
+ # This is a heuristic check - not perfect but catches common issues
+ if grep -En "^\s*\|\s*_\s*->" engines/rescript-wasm/lib/*.ml | grep -v "failwith\|error\|assert\|raise"; then
+ echo "WARNING: Found catch-all patterns without explicit error handling"
+ echo "Consider using explicit pattern matching to avoid hiding bugs"
+ fi
+ echo "Pattern check complete"
+
+ - name: Report TODOs/FIXMEs
+ run: |
+ echo "=== TODOs and FIXMEs (informational) ==="
+ grep -rn "TODO\|FIXME\|XXX\|HACK" engines/rescript-wasm/lib/ || echo "None found"
+
+ - name: Check line lengths (soft limit 100)
+ run: |
+ echo "=== Lines over 100 characters (warnings) ==="
+ LONG_LINES=$(find engines/rescript-wasm/lib -name "*.ml" -exec awk 'length > 100 {count++} END {print count+0}' {} \; | awk '{s+=$1} END {print s}')
+ echo "Found $LONG_LINES lines over 100 characters"
+ if [ "$LONG_LINES" -gt 50 ]; then
+ echo "WARNING: Many long lines found, consider refactoring"
+ fi
+
+ # Security scanning job
+ security:
+ name: Security Checks
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Set up OCaml
+ uses: ocaml/setup-ocaml@7c185f622998a008ba1b296d2d7c64e235629c23 # v3.6.1
+ with:
+ ocaml-compiler: "4.14.x"
+
+ - name: Check dependencies for known vulnerabilities
+ run: |
+ echo "=== Checking for vulnerable dependencies ==="
+ opam list --installed --safe 2>/dev/null || true
+
+ - name: Scan for hardcoded secrets
+ run: |
+ echo "=== Scanning for hardcoded secrets ==="
+ # Check for common secret patterns
+ if grep -rEn "(password|secret|api_key|apikey|auth_token|private_key)\s*=\s*['\"][^'\"]+['\"]" engines/rescript-wasm/; then
+ echo "ERROR: Potential hardcoded secrets found"
+ exit 1
+ fi
+ echo "No hardcoded secrets found"
+
+ - name: Check .gitignore coverage
+ run: |
+ echo "=== Checking .gitignore coverage ==="
+ # Verify important patterns are ignored
+ if [ -f engines/rescript-wasm/.gitignore ]; then
+ for pattern in "_build/" "*.cmo" "*.cmi" ".env"; do
+ if grep -q "$pattern" engines/rescript-wasm/.gitignore; then
+ echo "OK: $pattern is ignored"
+ else
+ echo "WARNING: $pattern should be in .gitignore"
+ fi
+ done
+ fi
+
+ # Final validation job - runs after all others
+ validate:
+ name: Final Validation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ needs: [rescript-wasm, forth-estate, ddraig-ssg, lint, security]
+
+ steps:
+ - name: All checks passed
+ run: |
+ echo "========================================="
+ echo "All CI checks passed successfully!"
+ echo "========================================="
+ echo "- ReScript-WASM: Build & Tests PASSED"
+ echo "- Forth Estate: Syntax PASSED"
+ echo "- Ddraig SSG: Build & Tests PASSED"
+ echo "- Lint & Format: PASSED"
+ echo "- Security Checks: PASSED"
+ echo "========================================="
diff --git a/.github/workflows/deploy-casket.yml b/docs/legacy-workflows/deploy-casket.yml.txt
similarity index 100%
rename from .github/workflows/deploy-casket.yml
rename to docs/legacy-workflows/deploy-casket.yml.txt
diff --git a/.github/workflows/deploy-ddraig.yml b/docs/legacy-workflows/deploy-ddraig.yml.txt
similarity index 100%
rename from .github/workflows/deploy-ddraig.yml
rename to docs/legacy-workflows/deploy-ddraig.yml.txt
diff --git a/.github/workflows/instant-sync.yml b/docs/legacy-workflows/instant-sync.yml.txt
similarity index 100%
rename from .github/workflows/instant-sync.yml
rename to docs/legacy-workflows/instant-sync.yml.txt
diff --git a/.github/workflows/mirror.yml b/docs/legacy-workflows/mirror.yml.txt
similarity index 100%
rename from .github/workflows/mirror.yml
rename to docs/legacy-workflows/mirror.yml.txt
diff --git a/scripts/check-preserved-language-policy.sh b/scripts/check-preserved-language-policy.sh
new file mode 100755
index 0000000..5941ac7
--- /dev/null
+++ b/scripts/check-preserved-language-policy.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: MPL-2.0-or-later
+# Source-object delta check; execution references are outside this gate's scope.
+set -euo pipefail
+(( $# == 0 || $# == 2 )) || { echo 'usage: check-preserved-language-policy.sh [import-commit expected-tree]' >&2; exit 2; }
+source "$(dirname -- "${BASH_SOURCE[0]}")/preservation-scope.sh"
+preservation_scope_init "$@" || exit 2
+historical=0
+violations=0
+for path in "${!SUBJECT_OBJECTS[@]}"; do
+ case "$path" in
+ *.d.ts) continue ;;
+ *.ts|*.tsx|*.go|*.py|*.java|*.kt|*.kts|*.swift|*.dart|package-lock.json|*/package-lock.json|yarn.lock|*/yarn.lock|pnpm-lock.yaml|*/pnpm-lock.yaml|tsconfig.json|*/tsconfig.json) ;;
+ *) continue ;;
+ esac
+ if preserved_source_matches "$path"; then
+ printf 'HISTORICAL unsupported source: %s [%s]\n' "$path" "${SUBJECT_OBJECTS["$path"]}"
+ historical=$((historical + 1))
+ else
+ printf 'VIOLATION: prohibited source object differs in path, blob, mode or retained area: %s\n' "$path" >&2
+ violations=$((violations + 1))
+ fi
+done
+printf 'Language preservation check: subject=%s historical=%s violations=%s\n' "$SUBJECT_SHA" "$historical" "$violations"
+echo 'Historical means exact retained source, not current language approval or runtime/security qualification.'
+echo 'Execution references and activation are not analyzed by this source-object delta check.'
+if [[ -n ${GITHUB_STEP_SUMMARY:-} ]]; then
+ printf '### Language preservation scope\n\nSubject: `%s`\n\nRetained prohibited-language objects: **%s**. Object-delta violations: **%s**. Historical objects remain unsupported. Execution references and activation are not analyzed by this check.\n' \
+ "$SUBJECT_SHA" "$historical" "$violations" >> "$GITHUB_STEP_SUMMARY"
+fi
+(( violations == 0 ))
diff --git a/scripts/lint-workflow-contracts.sh b/scripts/lint-workflow-contracts.sh
new file mode 100755
index 0000000..7af7220
--- /dev/null
+++ b/scripts/lint-workflow-contracts.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: MPL-2.0-or-later
+set -euo pipefail
+command -v yq >/dev/null || { echo 'yq is required to parse actual workflow keys' >&2; exit 2; }
+command -v jq >/dev/null || { echo 'jq is required to inspect parsed container definitions' >&2; exit 2; }
+shopt -s nullglob
+files=(.github/workflows/*.yml .github/workflows/*.yaml)
+(( ${#files[@]} > 0 )) || { echo 'No workflow population' >&2; exit 2; }
+failed=0
+refs=$(mktemp)
+parsed=$(mktemp)
+images=$(mktemp)
+trap 'rm -f -- "$refs" "$parsed" "$images"' EXIT
+for file in "${files[@]}"; do
+ IFS= read -r first < "$file"
+ [[ "$first" == '# SPDX-License-Identifier:'* ]] || { echo "Missing SPDX header: $file" >&2; failed=1; }
+ if ! yq -e 'has("permissions") and (.jobs | tag == "!!map")' "$file" >/dev/null; then
+ echo "Malformed workflow or missing permission declaration: $file" >&2; failed=1; continue
+ fi
+ if ! yq -r '([.jobs[] | select(has("uses")) | .uses] + [.jobs[].steps[]? | select(has("uses")) | .uses]) | .[]' "$file" > "$refs"; then
+ echo "Cannot enumerate workflow uses: $file" >&2; failed=1; continue
+ fi
+ while IFS= read -r ref; do
+ if [[ "$ref" == ./* ]]; then
+ case "/${ref#./}/" in
+ */../*|*/./*|*//*) echo "Invalid local action path: $file" >&2; failed=1 ;;
+ *) ;; # A canonical local path requires no remote version pin.
+ esac
+ elif [[ "$ref" == docker://* ]]; then
+ [[ "$ref" =~ ^docker://[^[:space:]@]+@sha256:[0-9a-f]{64}$ ]] || { echo "Unpinned container action: $file" >&2; failed=1; }
+ elif [[ ! "$ref" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(/[A-Za-z0-9_./-]+)?@[0-9a-f]{40}$ ]]; then
+ printf 'Unpinned action or reusable workflow in %s: %s\n' "$file" "$ref" >&2; failed=1
+ fi
+ done < "$refs"
+ if ! yq -o=json '.' "$file" > "$parsed" ||
+ ! jq -r '
+ def image:
+ if type == "string" and length > 0 and (test("[\u0000-\u001f\u007f]") | not) then .
+ else error("container image is missing or malformed") end;
+ def service_image:
+ if type == "object" then .image | image
+ else error("service container is not an object") end;
+ [.jobs[] |
+ (if has("container") then .container |
+ if type == "string" then image else service_image end
+ else empty end),
+ (if has("services") then .services |
+ if type == "object" then .[] | service_image
+ else error("services is not a map") end
+ else empty end)] | .[]
+ ' "$parsed" > "$images"; then
+ echo "Malformed job or service container: $file" >&2; failed=1; continue
+ fi
+ while IFS= read -r image; do
+ if [[ ! "$image" =~ ^[A-Za-z0-9][A-Za-z0-9._:/-]*@sha256:[0-9a-f]{64}$ ]]; then
+ echo "Unpinned job or service container image: $file" >&2; failed=1
+ fi
+ done < "$images"
+done
+if [[ -f .github/workflows/codeql.yml && -f .github/workflows/codeql-analysis.yml ]]; then
+ echo 'Duplicate CodeQL entry points require review' >&2; failed=1
+fi
+(( failed == 0 )) || exit 1
+printf 'Parsed %s workflows: SPDX, explicit permissions, action pins and container image digests passed.\n' "${#files[@]}"
diff --git a/scripts/preservation-sarif-locations.jq b/scripts/preservation-sarif-locations.jq
new file mode 100644
index 0000000..8bb3c9d
--- /dev/null
+++ b/scripts/preservation-sarif-locations.jq
@@ -0,0 +1,108 @@
+# SPDX-License-Identifier: MPL-2.0-or-later
+# SARIF 2.1.0 result source locations, including secondary evidence.
+# Unsupported references remain actionable; this does not resolve cached,
+# external, logical-only or extension-defined locations by guessing a file.
+
+def object_value:
+ if type == "object" then . else error("location object is missing or malformed") end;
+
+def entries($key):
+ if has($key) then
+ .[$key] | if type == "array" then .[] else error($key + " is not an array") end
+ else empty end;
+
+def required_entries($key):
+ if has($key) and (.[$key] | type == "array" and length > 0)
+ then .[$key][] else error($key + " has no explicit entries") end;
+
+def optional($key; parser):
+ if has($key) then .[$key] | parser else empty end;
+
+def no_cached_index:
+ if has("index") and .index != -1 then error("cached location index is unresolved") else . end;
+
+def artifact:
+ object_value | no_cached_index |
+ if (.uri | type == "string" and length > 0 and (test("[\u0000-\u001f\u007f]") | not)) and
+ ((has("uriBaseId") | not) or .uriBaseId == "%SRCROOT%")
+ then . else error("artifact URI or source base is missing or unsupported") end;
+
+def physical:
+ object_value |
+ if has("address") then error("address-based source evidence is unsupported")
+ else .artifactLocation | artifact end;
+
+def location:
+ object_value |
+ # A logical name cannot qualify a physical file. Cached logical references
+ # are also not silently resolved against a potentially different run.
+ if any(entries("logicalLocations"); type != "object" or
+ (has("index") and .index != -1) or (has("parentIndex") and .parentIndex != -1))
+ then error("logical location reference is unresolved")
+ elif has("relationships") and (.relationships | type != "array" or length > 0)
+ then error("location relationship references are unresolved")
+ else .physicalLocation | physical end;
+
+def stack:
+ object_value | required_entries("frames") | object_value | .location | location;
+
+def flow_location:
+ object_value | no_cached_index |
+ (.location | location), optional("stack"; stack);
+
+def code_flow:
+ object_value | required_entries("threadFlows") | object_value |
+ required_entries("locations") | flow_location;
+
+def node:
+ object_value |
+ if has("location") or (.children | type == "array" and length > 0) then
+ optional("location"; location), (entries("children") | node)
+ else error("graph node has no explicit source location") end;
+
+def graph:
+ object_value | required_entries("nodes") | node;
+
+def fix:
+ object_value | required_entries("artifactChanges") | object_value | .artifactLocation | artifact;
+
+def provenance:
+ object_value | entries("conversionSources") | physical;
+
+def extension_boundary:
+ # Standard property-bag tags carry no location. Other opaque extensions,
+ # runtime request/response evidence and graph-index traversals need review.
+ if any(.. | objects;
+ (has("properties") and
+ (.properties | type != "object" or ((keys - ["tags"]) | length > 0) or
+ (has("tags") and (.tags | type != "array" or any(.[]; type != "string"))))) or
+ has("webRequest") or has("webResponse"))
+ then error("opaque extension or runtime location evidence is unsupported")
+ elif has("graphTraversals") and (.graphTraversals | type != "array" or length > 0)
+ then error("graph traversal references are unresolved")
+ else . end;
+
+def result_locations:
+ object_value | extension_boundary |
+ # Unknown result fields are not assumed to be location-free extensions.
+ if (keys - ["ruleId","ruleIndex","rule","kind","level","message","analysisTarget",
+ "locations","guid","correlationGuid","occurrenceCount","partialFingerprints",
+ "fingerprints","stacks","codeFlows","graphs","graphTraversals","relatedLocations",
+ "suppressions","baselineState","rank","attachments","hostedViewerUri","workItemUris",
+ "provenance","fixes","taxa","webRequest","webResponse","properties"] | length > 0)
+ then error("unknown result field requires location review") else . end |
+ (entries("locations") | location),
+ (entries("relatedLocations") | location),
+ optional("analysisTarget"; artifact),
+ (entries("codeFlows") | code_flow),
+ (entries("stacks") | stack),
+ (entries("graphs") | graph),
+ (entries("fixes") | fix),
+ (entries("attachments") | object_value | .artifactLocation | artifact),
+ (entries("suppressions") | object_value | optional("location"; location)),
+ optional("provenance"; provenance);
+
+try ([result_locations] |
+ if length == 0 then error("result has no explicit source locations")
+ else {complete:true, artifact_locations:., reason:null} end)
+catch {complete:false, artifact_locations:[], reason:.}
diff --git a/scripts/preservation-scope.sh b/scripts/preservation-scope.sh
new file mode 100755
index 0000000..08ed19f
--- /dev/null
+++ b/scripts/preservation-scope.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: MPL-2.0-or-later
+# Shared source-identity checks. Source this file; it never executes retained code.
+
+preservation_scope_init() {
+ local checker_dir entry metadata path
+ checker_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
+ PRESERVED_IMPORT=${1:-9302dfcc3aedb7c52edf2867a2fcbf2c735878b4}
+ PRESERVED_TREE=${2:-6a8aa93222c8261c3e81b37acd7d1bd59f42bdc4}
+ bash "$checker_dir/verify-preservation.sh" "$PRESERVED_IMPORT" "$PRESERVED_TREE" || return 2
+ SUBJECT_SHA=$(git rev-parse --verify HEAD) || return 2
+ declare -gA PRESERVED_OBJECTS=() SUBJECT_OBJECTS=()
+ local inventory
+ inventory=$(mktemp) || return 2
+ if ! git ls-tree -rz --full-tree "$PRESERVED_IMPORT" > "$inventory"; then
+ rm -f -- "$inventory"; return 2
+ fi
+ while IFS= read -r -d '' entry; do
+ metadata=${entry%%$'\t'*}; path=${entry#*$'\t'}
+ PRESERVED_OBJECTS["$path"]=$metadata
+ done < "$inventory"
+ if ! git ls-tree -rz --full-tree "$SUBJECT_SHA" > "$inventory"; then
+ rm -f -- "$inventory"; return 2
+ fi
+ while IFS= read -r -d '' entry; do
+ metadata=${entry%%$'\t'*}; path=${entry#*$'\t'}
+ SUBJECT_OBJECTS["$path"]=$metadata
+ done < "$inventory"
+ rm -f -- "$inventory"
+ (( ${#PRESERVED_OBJECTS[@]} > 0 && ${#SUBJECT_OBJECTS[@]} > 0 ))
+}
+
+preserved_source_area() {
+ # Explicit unsupported source areas from docs/RETENTION-PLAN.adoc.
+ # Current automation, scripts, policy, documentation and configuration do
+ # not acquire an exemption merely because an old copy exists in the import.
+ case "$1" in
+ engines/*|satellites/*|casket-ssg/*|src/*|lib/*|public/*|content/*|mcp/*|lsp/*|test-corpus/*|sites-build/*) return 0 ;;
+ *) return 1 ;;
+ esac
+}
+
+preserved_source_matches() {
+ local path=$1 old current
+ [[ -n "$path" && "$path" != /* && "$path" != *$'\n'* && "$path" != *$'\r'* ]] || return 1
+ case "/$path/" in
+ */../*|*/./*|*//*|*%*|*\\*) return 1 ;;
+ *) ;; # A canonical relative path proceeds to exact object comparison.
+ esac
+ preserved_source_area "$path" || return 1
+ old=${PRESERVED_OBJECTS["$path"]-}; current=${SUBJECT_OBJECTS["$path"]-}
+ [[ -n "$old" && "$old" == "$current" && "$old" =~ ^100(644|755)\ blob\ [0-9a-f]{40}$ ]]
+}
diff --git a/scripts/review-preservation-sarif.sh b/scripts/review-preservation-sarif.sh
new file mode 100755
index 0000000..2a32eae
--- /dev/null
+++ b/scripts/review-preservation-sarif.sh
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: MPL-2.0-or-later
+# Keep complete SARIF. Classify findings; never rewrite or suppress a result.
+set -euo pipefail
+if (( $# != 3 && $# != 5 )); then
+ echo 'usage: review-preservation-sarif.sh reporter input.sarif output-prefix [import-commit expected-tree]' >&2
+ exit 2
+fi
+reporter=$1; input=$2; output=$3; shift 3
+case "$reporter" in codeql|semgrep|scorecard) ;; *) echo 'Unknown reporter' >&2; exit 2 ;; esac
+analysis_scope=checked-out-subject
+if [[ "$reporter" == scorecard ]]; then
+ case "${SCORECARD_ANALYSIS_SCOPE:-}" in
+ checked-out-subject|remote-default-branch) analysis_scope=$SCORECARD_ANALYSIS_SCOPE ;;
+ *) echo 'Scorecard analysis scope must be declared from the workflow event' >&2; exit 2 ;;
+ esac
+fi
+command -v jq >/dev/null || { echo 'jq is required' >&2; exit 2; }
+checker_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
+source "$checker_dir/preservation-scope.sh"
+preservation_scope_init "$@" || exit 2
+[[ -f "$input" && ! -L "$input" && -s "$input" ]] || { echo 'Missing or unsafe SARIF input' >&2; exit 2; }
+if ! jq -se --arg reporter "$reporter" '
+ length == 1 and (.[0] |
+ .version == "2.1.0" and (.runs | type == "array" and length > 0) and
+ (.inlineExternalProperties == null or .inlineExternalProperties == []) and
+ all(.runs[];
+ (.tool.driver.name | type == "string" and length > 0) and
+ (if $reporter == "codeql" then .tool.driver.name == "CodeQL"
+ elif $reporter == "scorecard" then .tool.driver.name == "Scorecard"
+ else (.tool.driver.name | ascii_downcase | startswith("semgrep")) end) and
+ (.results | type == "array") and
+ (.externalPropertyFileReferences == null or .externalPropertyFileReferences == {}) and
+ all(.invocations[]?; .executionSuccessful == true and
+ all(.toolExecutionNotifications[]?; .level != "error")) and
+ all(.results[];
+ (.ruleId | type == "string" and length > 0) and
+ (.message | type == "object") and
+ ((.message.text // .message.markdown) | type == "string") and
+ (.level == null or (.level as $l | ["error","warning","note","none"] | index($l) != null)) and
+ (.locations == null or (.locations | type == "array")))))
+' "$input" >/dev/null; then
+ echo 'Malformed, failed or mismatched SARIF analysis; no passing result can be inferred' >&2
+ exit 2
+fi
+mkdir -p -- "$(dirname -- "$output")"
+rows=$(mktemp); results=$(mktemp)
+trap 'rm -f -- "$rows" "$results"' EXIT
+jq -c '.runs[] | .results[]' "$input" > "$results"
+total=0; historical=0; advisory=0; actionable=0
+while IFS= read -r result; do
+ total=$((total + 1))
+ rule=$(jq -r '.ruleId' <<< "$result")
+ disposition=actionable
+ reason='Current, changed, unlocated or unqualified source requires review.'
+ if ! locations=$(jq -c -f "$checker_dir/preservation-sarif-locations.jq" <<< "$result"); then
+ echo 'Cannot evaluate complete SARIF location evidence' >&2; exit 2
+ fi
+ location_count=$(jq '.artifact_locations | length' <<< "$locations")
+ if [[ $(jq -r '.complete' <<< "$locations") != true ]]; then
+ reason="Unqualified location evidence: $(jq -r '.reason' <<< "$locations")"
+ fi
+ # Only this observed repository-level condition has a staging disposition.
+ # Missing security policy and every other Scorecard rule remain actionable.
+ if [[ "$reporter" == scorecard && "$rule" == FuzzingID ]] &&
+ jq -e '(.message.text | startswith("score is 0: project is not fuzzed:")) and
+ (.locations | type == "array" and length > 0)' <<< "$result" >/dev/null &&
+ jq -e '.complete and (.artifact_locations | length > 0 and all(.[]; .uri == "no file associated with this alert"))' <<< "$locations" >/dev/null; then
+ disposition=preservation-advisory
+ reason='No supported executable release is provided by this preservation repository; no fuzzing qualification is claimed. A selected runtime successor must qualify its own fuzzing.'
+ advisory=$((advisory + 1))
+ elif [[ "$analysis_scope" == checked-out-subject ]] && (( location_count > 0 )); then
+ all_preserved=true
+ for (( index=0; index> "$rows"
+done < "$results"
+jq -s --arg reporter "$reporter" --arg subject "$SUBJECT_SHA" --arg import "$PRESERVED_IMPORT" \
+ --arg analysis_scope "$analysis_scope" \
+ --arg input_sha256 "$(sha256sum "$input" | cut -d ' ' -f 1)" \
+ --argjson total "$total" --argjson historical "$historical" --argjson advisory "$advisory" --argjson actionable "$actionable" \
+ '{schema_version:1,reporter:$reporter,checked_out_subject_sha:$subject,import_sha:$import,
+ analysis_scope:$analysis_scope,analysis_subject_sha:(if $analysis_scope == "checked-out-subject" then $subject else null end),
+ raw_sarif_sha256:$input_sha256,hosted_upload:"unavailable-private-repository",
+ security_clean_claim:false,counts:{total:$total,retained_historical:$historical,preservation_advisory:$advisory,actionable:$actionable},findings:.}' \
+ "$rows" > "$output.json"
+{
+ printf '### %s analysis disposition\n\n' "$reporter"
+ printf 'Checkout: `%s`. Analysis scope: `%s`. Raw SARIF is retained unchanged in the workflow artifact.\n\n' "$SUBJECT_SHA" "$analysis_scope"
+ if [[ "$analysis_scope" == remote-default-branch ]]; then
+ printf 'Scorecard queried the remote default branch; its precise analyzed commit is not supplied by this action. Checkout identity is not asserted as analysis identity, and source findings cannot receive the historical-file disposition.\n\n'
+ fi
+ printf '| Total | Exact retained source | Preservation advisory | Current or unqualified |\n| ---: | ---: | ---: | ---: |\n| %s | %s | %s | %s |\n\n' "$total" "$historical" "$advisory" "$actionable"
+ printf 'Hosted code-scanning upload is unavailable for this private repository. This is a preservation disposition, not a clean-security or supported-runtime attestation.\n'
+} > "$output.md"
+cat "$output.md"
+[[ -z ${GITHUB_STEP_SUMMARY:-} ]] || cat "$output.md" >> "$GITHUB_STEP_SUMMARY"
+(( actionable == 0 )) || { echo "::error::$reporter has $actionable current or unqualified finding(s); inspect the complete SARIF and disposition artifact" >&2; exit 1; }
diff --git a/scripts/validate-output.sh b/scripts/validate-output.sh
index 475bdec..2b34fc7 100755
--- a/scripts/validate-output.sh
+++ b/scripts/validate-output.sh
@@ -1,6 +1,7 @@
#!/bin/bash
+# SPDX-License-Identifier: MPL-2.0-or-later
# Output Validation Script
-# Validates generated HTML against security and correctness requirements
+# Heuristic checks for selected unsafe output patterns; not a complete HTML/XSS validator.
#
# Usage: validate-output.sh
# validate-output.sh
@@ -15,60 +16,67 @@ NC='\033[0m'
ERRORS=0
WARNINGS=0
-log_error() { echo -e "${RED}[ERROR]${NC} $*"; ((ERRORS++)); }
-log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; ((WARNINGS++)); }
+log_error() { printf '%b[ERROR]%b %s\n' "$RED" "$NC" "$*"; ERRORS=$((ERRORS + 1)); }
+log_warn() { printf '%b[WARN]%b %s\n' "$YELLOW" "$NC" "$*"; WARNINGS=$((WARNINGS + 1)); }
log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
validate_file() {
local file="$1"
- local filename=$(basename "$file")
+ local filename
+ filename=$(basename -- "$file")
echo "Validating: $filename"
# Check file exists and is not empty
- if [[ ! -f "$file" ]]; then
- log_error "File not found: $file"
- return 1
+ if [[ ! -f "$file" || ! -r "$file" ]]; then
+ log_error "File missing or unreadable: $file"
+ return 0
fi
if [[ ! -s "$file" ]]; then
- log_warn "File is empty: $file"
+ log_error "File is empty: $file"
+ return 0
fi
- local content
- content=$(cat "$file")
+ # Bash strings cannot contain NUL; check the file before command substitution.
+ local nul_count
+ nul_count=$(LC_ALL=C tr -cd '\000' < "$file" | wc -c)
+ if (( nul_count > 0 )); then
+ log_error "$filename: Contains null bytes"
+ return 0
+ fi
# =========================================================================
# SECURITY CHECKS - These are ERRORS (treat as failures)
# =========================================================================
# Check for unescaped script tags
- if echo "$content" | grep -qi '
\n' > "$scratch/script.html"
+check 1 "$scratch/script.html"
+printf '
\n' > "$scratch/event.html"
+check 1 "$scratch/event.html"
+printf '
\n' > "$scratch/event-spaced.html"
+check 1 "$scratch/event-spaced.html"
+printf 'link
\n' > "$scratch/url.html"
+check 1 "$scratch/url.html"
+{ printf ''; head -c 262144 /dev/zero | tr '\000' x; printf '
\n'; } > "$scratch/large.html"
+check 1 "$scratch/large.html"
+printf 'nul\000byte
\n' > "$scratch/nul.html"
+check 1 "$scratch/nul.html"
+printf 'invalid\377
\n' > "$scratch/encoding.html"
+check 1 "$scratch/encoding.html"
+: > "$scratch/empty.html"
+check 1 "$scratch/empty.html"
+check 1 "$scratch/absent.html"
+mkdir "$scratch/empty-directory"
+check 1 "$scratch/empty-directory"
+mkdir "$scratch/mixed-directory"
+cp "$scratch/safe.html" "$scratch/mixed-directory/first safe.html"
+cp "$scratch/script.html" "$scratch/mixed-directory/second unsafe.html"
+check 1 "$scratch/mixed-directory"
+grep -Fq '1 errors' "$scratch/result"
+printf 'PASS: safe output accepted; empty, missing, injection, NUL, UTF-8 and directory controls rejected\n'
diff --git a/tests/preservation-controls.sh b/tests/preservation-controls.sh
new file mode 100755
index 0000000..efa5492
--- /dev/null
+++ b/tests/preservation-controls.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: MPL-2.0-or-later
+set -euo pipefail
+root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
+scratch=$(mktemp -d)
+trap 'rm -rf -- "$scratch"' EXIT
+git -C "$scratch" init -q
+git -C "$scratch" config user.name 'Preservation regression fixture'
+git -C "$scratch" config user.email 'fixture@example.invalid'
+git -C "$scratch" config commit.gpgsign false
+printf 'retained source\n' > "$scratch/source.txt"
+git -C "$scratch" add source.txt
+git -C "$scratch" commit -qm 'fixture import'
+import_commit=$(git -C "$scratch" rev-parse HEAD)
+tree=$(git -C "$scratch" rev-parse 'HEAD^{tree}')
+printf 'new documentation\n' > "$scratch/README"
+git -C "$scratch" add README
+git -C "$scratch" commit -qm 'fixture successor'
+(cd "$scratch" && bash "$root/scripts/verify-preservation.sh" "$import_commit" "$tree")
+reject() {
+ local label=$1 expected=$2
+ shift 2
+ local rc=0
+ (cd "$scratch" && bash "$root/scripts/verify-preservation.sh" "$@") >"$scratch/result" 2>&1 || rc=$?
+ if (( rc != 1 )) || ! grep -Fq "$expected" "$scratch/result"; then
+ printf 'FAIL: %s did not produce the expected rejection\n' "$label" >&2
+ cat "$scratch/result" >&2
+ exit 1
+ fi
+ printf 'PASS: rejected %s\n' "$label"
+}
+reject 'mutable import ref' 'immutable full Git object IDs' main "$tree"
+reject 'missing import' 'preserved import is absent' 0000000000000000000000000000000000000000 "$tree"
+reject 'wrong source tree' 'imported tree differs' "$import_commit" 0000000000000000000000000000000000000000
+git -C "$scratch" checkout --orphan unrelated -q
+git -C "$scratch" rm -rfq .
+printf 'unrelated\n' > "$scratch/other"
+git -C "$scratch" add other
+git -C "$scratch" commit -qm 'unrelated history'
+reject 'unrelated history' 'does not retain the preserved import' "$import_commit" "$tree"
+
diff --git a/tests/preservation-policy-controls.sh b/tests/preservation-policy-controls.sh
new file mode 100755
index 0000000..35b0910
--- /dev/null
+++ b/tests/preservation-policy-controls.sh
@@ -0,0 +1,174 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: MPL-2.0-or-later
+set -euo pipefail
+root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
+tmp=$(mktemp -d)
+trap 'rm -rf -- "$tmp"' EXIT
+export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
+export GIT_AUTHOR_NAME=Fixture GIT_COMMITTER_NAME=Fixture
+export GIT_AUTHOR_EMAIL=fixture@example.invalid GIT_COMMITTER_EMAIL=fixture@example.invalid
+git init -q "$tmp/repo"
+cd "$tmp/repo"
+mkdir -p engines/example scripts
+retained_path=engines/example/archive.py
+active_path=scripts/active.sh
+printf '%s\n' '# retained source fixture' > "$retained_path"
+printf '%s\n' '# active automation fixture' > "$active_path"
+git add .; git commit -qm import
+base=$(git rev-parse HEAD); tree=$(git rev-parse HEAD^{tree})
+object_violation='VIOLATION: prohibited source object differs in path, blob, mode or retained area'
+current_finding='current or unqualified finding(s)'
+no_file_location='no file associated with this alert'
+expect() {
+ local wanted=$1 marker=$2; shift 2
+ local actual=0
+ "$@" > "$tmp/output" 2>&1 || actual=$?
+ if [[ "$actual" != "$wanted" ]] || ! grep -Fq -- "$marker" "$tmp/output"; then
+ cat "$tmp/output" >&2; printf 'Expected status %s and marker %s, got %s\n' "$wanted" "$marker" "$actual" >&2; exit 1
+ fi
+ printf 'ok: %s\n' "$marker"
+}
+policy=(bash "$root/scripts/check-preserved-language-policy.sh" "$base" "$tree")
+expect 0 'historical=1 violations=0' "${policy[@]}"
+printf '%s\n' '# changed historical source' >> "$retained_path"
+git add .; git commit -qm changed
+expect 1 "$object_violation" "${policy[@]}"
+git reset --hard -q "$base"
+cp "$retained_path" engines/example/new.py
+git add .; git commit -qm copied
+expect 1 "$object_violation" "${policy[@]}"
+git reset --hard -q "$base"
+chmod +x "$retained_path"
+git add .; git commit -qm mode
+expect 1 "$object_violation" "${policy[@]}"
+git reset --hard -q "$base"
+git mv "$retained_path" scripts/relocated.py
+git commit -qm moved
+expect 1 "$object_violation" "${policy[@]}"
+git reset --hard -q "$base"
+expect 2 'imported tree differs' bash "$root/scripts/check-preserved-language-policy.sh" "$base" "0000000000000000000000000000000000000000"
+# Record the actual limit: an allowed wrapper does not alter a retained source
+# object. This gate must not claim it proved the wrapper cannot execute it.
+printf '%s\n' '#!/usr/bin/env bash' 'python engines/example/archive.py' > scripts/wrapper.sh
+git add .; git commit -qm wrapper
+expect 0 'Execution references and activation are not analyzed' "${policy[@]}"
+grep -Fq 'historical=1 violations=0' "$tmp/output"
+git reset --hard -q "$base"
+
+sarif="$tmp/result.sarif"
+make_result() {
+ local tool=$1 rule=$2 uri=$3 message=${4:-fixture finding}
+ jq -n --arg tool "$tool" --arg rule "$rule" --arg uri "$uri" --arg message "$message" \
+ '{version:"2.1.0",runs:[{tool:{driver:{name:$tool}},results:[{ruleId:$rule,level:"warning",message:{text:$message},locations:[{physicalLocation:{artifactLocation:{uri:$uri}}}]}]}]}' > "$sarif"
+}
+review=(bash "$root/scripts/review-preservation-sarif.sh")
+export SCORECARD_ANALYSIS_SCOPE=checked-out-subject
+make_result CodeQL fixture "$retained_path"
+original=$(sha256sum "$sarif")
+expect 0 'Exact retained source' "${review[@]}" codeql "$sarif" "$tmp/receipt" "$base" "$tree"
+[[ $(sha256sum "$sarif") == "$original" ]]
+jq -e '.counts.total == 1 and .counts.retained_historical == 1 and .security_clean_claim == false' "$tmp/receipt.json" >/dev/null
+make_result CodeQL fixture "$active_path"
+expect 1 "$current_finding" "${review[@]}" codeql "$sarif" "$tmp/receipt" "$base" "$tree"
+make_result CodeQL fixture ../engines/example/archive.py
+expect 1 "$current_finding" "${review[@]}" codeql "$sarif" "$tmp/receipt" "$base" "$tree"
+make_result CodeQL fixture "$retained_path"
+printf '%s\n' '# current mutation' >> "$retained_path"
+git add .; git commit -qm current
+expect 1 "$current_finding" "${review[@]}" codeql "$sarif" "$tmp/receipt" "$base" "$tree"
+git reset --hard -q "$base"
+make_result Scorecard FuzzingID "$no_file_location" 'score is 0: project is not fuzzed: fixture'
+expect 0 'Preservation advisory' "${review[@]}" scorecard "$sarif" "$tmp/receipt" "$base" "$tree"
+jq -e '.counts.preservation_advisory == 1 and .counts.actionable == 0' "$tmp/receipt.json" >/dev/null
+make_result Scorecard SecurityPolicyID "$no_file_location"
+expect 1 "$current_finding" "${review[@]}" scorecard "$sarif" "$tmp/receipt" "$base" "$tree"
+make_result Scorecard FuzzingID "$no_file_location" 'a different failure'
+expect 1 "$current_finding" "${review[@]}" scorecard "$sarif" "$tmp/receipt" "$base" "$tree"
+make_result Scorecard fixture "$retained_path"
+SCORECARD_ANALYSIS_SCOPE=remote-default-branch expect 1 "$current_finding" "${review[@]}" scorecard "$sarif" "$tmp/receipt" "$base" "$tree"
+jq -e '.analysis_subject_sha == null and .analysis_scope == "remote-default-branch"' "$tmp/receipt.json" >/dev/null
+
+extend_result() {
+ local expression=$1 uri=${2:-$active_path}
+ jq --arg uri "$uri" 'def loc: {physicalLocation:{artifactLocation:{uri:$uri}}};
+ .runs[0].results[0] |= ('"$expression"')' "$sarif" > "$tmp/extended.sarif"
+ mv "$tmp/extended.sarif" "$sarif"
+}
+review_source_fixture() {
+ local status=$1 marker=$2
+ local before
+ before=$(sha256sum "$sarif")
+ expect "$status" "$marker" "${review[@]}" codeql "$sarif" "$tmp/receipt" "$base" "$tree"
+ [[ $(sha256sum "$sarif") == "$before" ]]
+ jq -e --slurpfile raw "$sarif" \
+ '[.findings[].result] == [$raw[0].runs[].results[]]' "$tmp/receipt.json" >/dev/null
+}
+
+# Every supported source-bearing shape gets both a retained-source positive
+# and a mixed current/historical negative. Primary locations alone are not enough.
+while IFS= read -r expression; do
+ printf 'location control: %s\n' "$expression"
+ make_result CodeQL fixture "$retained_path"
+ extend_result "$expression" "$retained_path"
+ review_source_fixture 0 'Exact retained source'
+ jq -e '.findings[0].location_evidence.complete and (.findings[0].location_evidence.artifact_locations | length > 1)' "$tmp/receipt.json" >/dev/null
+ make_result CodeQL fixture "$retained_path"
+ extend_result "$expression" "$active_path"
+ review_source_fixture 1 "$current_finding"
+done <<'JQ'
+.relatedLocations=[loc]
+.analysisTarget={uri:$uri}
+.codeFlows=[{threadFlows:[{locations:[{location:loc}]}]}]
+.codeFlows=[{threadFlows:[{locations:[{location:.locations[0],stack:{frames:[{location:loc}]}}]}]}]
+.stacks=[{frames:[{location:loc}]}]
+.graphs=[{nodes:[{id:"parent",children:[{id:"child",location:loc}]}]}]
+.fixes=[{artifactChanges:[{artifactLocation:{uri:$uri},replacements:[]}]}]
+.attachments=[{artifactLocation:{uri:$uri}}]
+.suppressions=[{kind:"inSource",location:loc}]
+.provenance={conversionSources:[loc.physicalLocation]}
+JQ
+
+# An unsupported/missing secondary reference must not disappear from traversal.
+while IFS= read -r expression; do
+ printf 'unresolved location control: %s\n' "$expression"
+ make_result CodeQL fixture "$retained_path"
+ extend_result "$expression"
+ review_source_fixture 1 "$current_finding"
+ jq -e '.findings[0].location_evidence.complete == false' "$tmp/receipt.json" >/dev/null
+done <<'JQ'
+.relatedLocations=[{}]
+.relatedLocations=[{logicalLocations:[{name:"unknown"}]}]
+.relatedLocations=[{physicalLocation:{artifactLocation:{index:0}}}]
+.relatedLocations=[{physicalLocation:{artifactLocation:{uri:$uri,uriBaseId:"UNKNOWN"}}}]
+.relatedLocations=null
+.locations[0].physicalLocation.artifactLocation.index=0
+.locations[0].physicalLocation.address={index:0}
+.locations[0].relationships=[{target:9}]
+.codeFlows=[{threadFlows:[{locations:[{index:0}]}]}]
+.codeFlows=[{threadFlows:[]}]
+.stacks=[{frames:[{}]}]
+.fixes=[{artifactChanges:[{}]}]
+.attachments=[{}]
+.graphs=[{nodes:[{id:"unknown"}]}]
+.graphTraversals=[{runGraphIndex:0}]
+.properties={sourceLocation:loc}
+.customLocation=loc
+.relatedLocations=[{physicalLocation:{artifactLocation:{uri:"engines/example/archive.py\nother"}}}]
+JQ
+
+make_result Scorecard FuzzingID "$no_file_location" 'score is 0: project is not fuzzed: fixture'
+extend_result '.relatedLocations=[loc]'
+expect 1 "$current_finding" "${review[@]}" scorecard "$sarif" "$tmp/receipt" "$base" "$tree"
+make_result Scorecard FuzzingID "$no_file_location" 'score is 0: project is not fuzzed: fixture'
+extend_result '.locations=[]'
+expect 1 "$current_finding" "${review[@]}" scorecard "$sarif" "$tmp/receipt" "$base" "$tree"
+make_result CodeQL fixture "$retained_path"
+jq '.runs[0].invocations=[{executionSuccessful:false}]' "$sarif" > "$tmp/bad.sarif"
+expect 2 'Malformed, failed or mismatched' "${review[@]}" codeql "$tmp/bad.sarif" "$tmp/receipt" "$base" "$tree"
+expect 2 'Malformed, failed or mismatched' "${review[@]}" semgrep "$sarif" "$tmp/receipt" "$base" "$tree"
+jq '.runs[0].externalPropertyFileReferences={results:[{location:{uri:"external.sarif"}}]}' "$sarif" > "$tmp/bad.sarif"
+expect 2 'Malformed, failed or mismatched' "${review[@]}" codeql "$tmp/bad.sarif" "$tmp/receipt" "$base" "$tree"
+printf '{' > "$tmp/bad.sarif"
+expect 2 'Malformed, failed or mismatched' "${review[@]}" codeql "$tmp/bad.sarif" "$tmp/receipt" "$base" "$tree"
+expect 2 'Missing or unsafe SARIF input' "${review[@]}" codeql "$tmp/absent.sarif" "$tmp/receipt" "$base" "$tree"
+echo 'Preservation policy and SARIF controls passed; synthetic fixtures do not qualify actual repository analysis.'
diff --git a/tests/workflow-contract-controls.sh b/tests/workflow-contract-controls.sh
new file mode 100755
index 0000000..1fff6ae
--- /dev/null
+++ b/tests/workflow-contract-controls.sh
@@ -0,0 +1,93 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: MPL-2.0-or-later
+set -euo pipefail
+root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
+tmp=$(mktemp -d)
+trap 'rm -rf -- "$tmp"' EXIT
+mkdir -p "$tmp/.github/workflows"
+cd "$tmp"
+fixture=.github/workflows/fixture.yml
+cat > "$fixture" <<'YAML'
+# SPDX-License-Identifier: MPL-2.0-or-later
+name: fixture
+on: push
+permissions:
+ contents: read
+jobs:
+ fixture:
+ runs-on: ubuntu-24.04
+ steps:
+ # uses: fabricated/comment@main
+ # image: fabricated/container:latest
+ - run: |
+ echo 'uses: string/not-an-action@main'
+ echo 'container: string/not-an-image:latest'
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+ - uses: ./local-action
+YAML
+bash "$root/scripts/lint-workflow-contracts.sh" > "$tmp/good"
+grep -Fq 'Parsed 1 workflows' "$tmp/good"
+cp "$fixture" "$tmp/original"
+reject() {
+ local marker=${1:-Unpinned action}
+ local actual=0
+ bash "$root/scripts/lint-workflow-contracts.sh" > "$tmp/result" 2>&1 || actual=$?
+ [[ "$actual" == 1 ]] && grep -Fq "$marker" "$tmp/result" || { cat "$tmp/result" >&2; exit 1; }
+ printf 'rejected: %s\n' "$marker"
+ cp "$tmp/original" "$fixture"
+}
+accept() {
+ bash "$root/scripts/lint-workflow-contracts.sh" > "$tmp/result" 2>&1 || { cat "$tmp/result" >&2; exit 1; }
+ grep -Fq 'container image digests passed' "$tmp/result"
+ cp "$tmp/original" "$fixture"
+}
+sed -i 's/@3d3c42e5aac5ba805825da76410c181273ba90b1/@main/' "$fixture"
+reject
+sed -i 's/@3d3c42e5aac5ba805825da76410c181273ba90b1/@3d3c42e5aac5ba805825da76410c181273ba90b1extra/' "$fixture"
+reject
+sed -i 's|actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1|actions/github-script@main|' "$fixture"
+reject
+cat >> "$fixture" <<'YAML'
+ reusable:
+ uses: example/source/.github/workflows/check.yml@main
+YAML
+reject
+
+# The literals are fixtures for pin syntax, not claims about real image content.
+export WORKFLOW_TEST_IMAGE='example/tool:V1@sha256:1111111111111111111111111111111111111111111111111111111111111111'
+yq -i '.jobs.fixture.container = strenv(WORKFLOW_TEST_IMAGE)' "$fixture"
+accept
+yq -i '.jobs.fixture.container = {"image":strenv(WORKFLOW_TEST_IMAGE)}' "$fixture"
+accept
+yq -i '.jobs.fixture.services = {"db":{"image":strenv(WORKFLOW_TEST_IMAGE)}}' "$fixture"
+accept
+yq -i '.jobs.fixture.services = {}' "$fixture"
+accept
+for expression in \
+ '.jobs.fixture.container = "example/tool:latest"' \
+ '.jobs.fixture.container = {"image":"example/tool:v1"}' \
+ '.jobs.fixture.services = {"db":{"image":"example/db:latest"}}' \
+ '.jobs.fixture.container = "${{ matrix.image }}"' \
+ '.jobs.fixture.container = "__container_population_complete__"' \
+ '.jobs.fixture.container = "example/tool@sha256:abcd"'; do
+ yq -i "$expression" "$fixture"
+ reject 'Unpinned job or service container image'
+done
+for expression in \
+ '.jobs.fixture.container = null' \
+ '.jobs.fixture.container = {}' \
+ '.jobs.fixture.container = {"image":[]}' \
+ '.jobs.fixture.services = null' \
+ '.jobs.fixture.services = {"db":{}}' \
+ '.jobs.fixture.services = {"db":strenv(WORKFLOW_TEST_IMAGE)}' \
+ '.jobs.fixture.container = strenv(WORKFLOW_TEST_IMAGE) + "\n" + strenv(WORKFLOW_TEST_IMAGE)'; do
+ yq -i "$expression" "$fixture"
+ reject 'Malformed job or service container'
+done
+yq -i '.jobs.fixture.steps += [{"uses":"docker://example/tool:latest"}]' "$fixture"
+reject 'Unpinned container action'
+yq -i '.jobs.fixture.steps += [{"uses":"docker://" + strenv(WORKFLOW_TEST_IMAGE)}]' "$fixture"
+accept
+yq -i '.jobs.fixture.steps += [{"uses":"./a/../b"}]' "$fixture"
+reject 'Invalid local action path'
+echo 'Workflow parser controls passed: action refs and job/service image pins checked; comments and run strings remain inert.'