diff --git a/.gitattributes b/.gitattributes index bb56184..e860a85 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # RSR-compliant .gitattributes * text=auto eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index fd9bb96..688a442 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Funding platforms for hyperpolymath projects # See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dc98817..4d9ae2f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Dependabot configuration for RSR-compliant repositories # Covers common ecosystems - remove unused ones for your project diff --git a/.github/workflows/boj-build.yml b/.github/workflows/boj-build.yml index f24d0b9..1440f66 100644 --- a/.github/workflows/boj-build.yml +++ b/.github/workflows/boj-build.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # # OPTIONAL: BoJ Server Build Trigger # This workflow notifies a BoJ Server instance when code is pushed. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index de748c0..a74103f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,7 +27,7 @@ jobs: fail-fast: false matrix: include: - - language: javascript-typescript + - language: rust build-mode: none steps: - name: Checkout diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 5a2aad7..cff4451 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # # dependabot-automerge.yml — enable GitHub's native auto-merge on # Dependabot pull requests that match a declared severity / ecosystem diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 8f9276b..146c222 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # dogfood-gate.yml — Hyperpolymath Dogfooding Quality Gate diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 46ce56d..13327b9 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 name: Governance on: diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 0dd7f99..02b0e9c 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 name: Hypatia Security Scan on: diff --git a/.github/workflows/instant-sync.yml b/.github/workflows/instant-sync.yml index cc5c794..0994325 100644 --- a/.github/workflows/instant-sync.yml +++ b/.github/workflows/instant-sync.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Instant Forge Sync - Triggers propagation to all forges on push/release name: Instant Sync on: diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 680b535..f4ddfca 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 name: Mirror to Git Forges on: push: diff --git a/.github/workflows/nimble-site.yml b/.github/workflows/nimble-site.yml index 0266a1c..bf3924b 100644 --- a/.github/workflows/nimble-site.yml +++ b/.github/workflows/nimble-site.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 name: NimblePublisher Site on: push: @@ -34,11 +34,20 @@ jobs: path: site/_site deploy: needs: build + # Only deploy from the default branch — never from a pull request, which + # would publish unmerged content to the live Pages site. + if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 15 permissions: pages: write contents: read + # Required by actions/deploy-pages for OIDC token minting. + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages + id: deployment uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 84bb0ae..08a434b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 name: OSSF Scorecard on: diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index e3c7755..d027a59 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0 +# SPDX-License-Identifier: MPL-2.0 name: Secret Scanner on: pull_request: @@ -14,13 +14,3 @@ jobs: uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@d135b05bfc647d0c0fbfedc7e80f37ea50f49236 timeout-minutes: 10 secrets: inherit - trufflehog: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - name: TruffleHog Secret Scan - uses: trufflesecurity/trufflehog@main - with: - extra_args: --only-verified --fail diff --git a/.gitignore b/.gitignore index 67b956e..55869aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # RSR-compliant .gitignore # OS & Editor diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e00028b..b08314a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Primary CI/CD - GitLab is the source of truth stages: diff --git a/.machine_readable/6a2/AGENTIC.a2ml b/.machine_readable/6a2/AGENTIC.a2ml index 09aa2c2..4a98976 100644 --- a/.machine_readable/6a2/AGENTIC.a2ml +++ b/.machine_readable/6a2/AGENTIC.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # AGENTIC.a2ml — AI agent constraints and capabilities diff --git a/.machine_readable/6a2/ECOSYSTEM.a2ml b/.machine_readable/6a2/ECOSYSTEM.a2ml index 4f2df33..6c2e6f8 100644 --- a/.machine_readable/6a2/ECOSYSTEM.a2ml +++ b/.machine_readable/6a2/ECOSYSTEM.a2ml @@ -1,6 +1,5 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # ECOSYSTEM.a2ml — Ecosystem position -# Converted from ECOSYSTEM.scm on 2026-03-15 [metadata] project = "palimpsest-plasma" @@ -8,3 +7,21 @@ ecosystem = "hyperpolymath" [position] type = "component" +role = "policy definition and deterministic compliance evaluation" + +# The agent-honesty trio. Sibling repos are planned/external; the +# integration contract is plasma's deterministic output. +[related-projects.somethings-fishy] +status = "planned" +role = "forensic investigation of bot-inflicted damage" +consumes = "plasma findings (JSON), SARIF results with stable plasma/ identifiers" + +[related-projects.did-you-actually-do-that] +status = "planned" +role = "verification that agents did what they claimed" +consumes = "plasma facts snapshots (diffable before/after), pass findings as positive evidence" + +[related-projects.palimpsest-license] +status = "separate future project" +role = "the PMPL license itself" +note = "plasma-parser recognises Palimpsest-family SPDX identifiers; nothing more" diff --git a/.machine_readable/6a2/NEUROSYM.a2ml b/.machine_readable/6a2/NEUROSYM.a2ml index ce0c930..767d7dd 100644 --- a/.machine_readable/6a2/NEUROSYM.a2ml +++ b/.machine_readable/6a2/NEUROSYM.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # NEUROSYM.a2ml — Neurosymbolic integration metadata diff --git a/.machine_readable/6a2/PLAYBOOK.a2ml b/.machine_readable/6a2/PLAYBOOK.a2ml index 02c3acb..a961250 100644 --- a/.machine_readable/6a2/PLAYBOOK.a2ml +++ b/.machine_readable/6a2/PLAYBOOK.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # PLAYBOOK.a2ml — Operational playbook diff --git a/.machine_readable/6a2/STATE.a2ml b/.machine_readable/6a2/STATE.a2ml index df15b02..9e82236 100644 --- a/.machine_readable/6a2/STATE.a2ml +++ b/.machine_readable/6a2/STATE.a2ml @@ -1,14 +1,19 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # STATE.a2ml — Project state checkpoint -# Converted from STATE.scm on 2026-03-15 [metadata] project = "palimpsest-plasma" -version = "2.0.0-dev" -last-updated = "2026-03-15" +version = "0.2.0" +last-updated = "2026-07-02" status = "active" [project-context] name = "palimpsest-plasma" -completion-percentage = 40 -phase = "PLASMA Engine Design-to-Implementation" +completion-percentage = 50 +phase = "Engine v0 shipped (Rust); action planner and integration surfaces next" + +[identity] +summary = "Deterministic, typed policy engine for the agentic era" +code-license = "MPL-2.0" +docs-license = "CC-BY-SA-4.0" +note = "The Palimpsest license (PMPL) is a separate future project; this repo's parser merely recognises its identifiers" diff --git a/.machine_readable/CLADE.a2ml b/.machine_readable/CLADE.a2ml index 98da118..b075345 100644 --- a/.machine_readable/CLADE.a2ml +++ b/.machine_readable/CLADE.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Clade declaration — part of the gv-clade-index registry # See: https://github.com/hyperpolymath/gv-clade-index @@ -22,7 +22,7 @@ bitbucket = "hyperpolymath/palimpsest-plasma" [lineage] type = "standalone" -parent = "Governance automation engine — OCaml policy AST" +parent = "Deterministic, typed policy engine (Rust)" born = "2026-03-16" # Lifecycle status (added by clade-status-backfill; see gv-clade-index ADR 0006). diff --git a/.machine_readable/bot_directives/coverage.a2ml b/.machine_readable/bot_directives/coverage.a2ml index 3d720dc..6979664 100644 --- a/.machine_readable/bot_directives/coverage.a2ml +++ b/.machine_readable/bot_directives/coverage.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # coverage.a2ml — Session coverage tracking diff --git a/.machine_readable/bot_directives/debt.a2ml b/.machine_readable/bot_directives/debt.a2ml index f46451a..c0238c5 100644 --- a/.machine_readable/bot_directives/debt.a2ml +++ b/.machine_readable/bot_directives/debt.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # debt.a2ml — Meander debt list diff --git a/.machine_readable/bot_directives/methodology.a2ml b/.machine_readable/bot_directives/methodology.a2ml index 9bf1439..754f357 100644 --- a/.machine_readable/bot_directives/methodology.a2ml +++ b/.machine_readable/bot_directives/methodology.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # methodology.a2ml — AI agent methodology configuration diff --git a/.machine_readable/contractiles/Justfile b/.machine_readable/contractiles/Justfile index f5a51c0..6012c86 100644 --- a/.machine_readable/contractiles/Justfile +++ b/.machine_readable/contractiles/Justfile @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # RSR-template-repo - RSR Standard Justfile Template # https://just.systems/man/en/ # diff --git a/.machine_readable/integrations/feedback-o-tron.a2ml b/.machine_readable/integrations/feedback-o-tron.a2ml index 1c473ae..5381604 100644 --- a/.machine_readable/integrations/feedback-o-tron.a2ml +++ b/.machine_readable/integrations/feedback-o-tron.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Feedback-o-Tron Integration — Autonomous Bug Reporting [integration] diff --git a/.machine_readable/integrations/proven.a2ml b/.machine_readable/integrations/proven.a2ml index 6b3e805..9af33ff 100644 --- a/.machine_readable/integrations/proven.a2ml +++ b/.machine_readable/integrations/proven.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Proven Integration — Formally Verified Safety Library [integration] diff --git a/.machine_readable/integrations/verisimdb.a2ml b/.machine_readable/integrations/verisimdb.a2ml index 2c8f8f5..164c522 100644 --- a/.machine_readable/integrations/verisimdb.a2ml +++ b/.machine_readable/integrations/verisimdb.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # VeriSimDB Feed — Cross-Repo Analytics Data Store [integration] diff --git a/.machine_readable/integrations/vexometer.a2ml b/.machine_readable/integrations/vexometer.a2ml index bb7fc43..238b3d2 100644 --- a/.machine_readable/integrations/vexometer.a2ml +++ b/.machine_readable/integrations/vexometer.a2ml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Vexometer Integration — Irritation Surface Analysis [integration] diff --git a/.machine_readable/self-validating/examples/ci-config.k9.ncl b/.machine_readable/self-validating/examples/ci-config.k9.ncl index 11fe019..9fe314e 100644 --- a/.machine_readable/self-validating/examples/ci-config.k9.ncl +++ b/.machine_readable/self-validating/examples/ci-config.k9.ncl @@ -1,5 +1,5 @@ K9! -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Example Yard-level K9 component: CI/CD configuration with validation # Security Level: Yard (Nickel evaluation, contract validation) # Signature recommended but not required diff --git a/.machine_readable/self-validating/examples/project-metadata.k9.ncl b/.machine_readable/self-validating/examples/project-metadata.k9.ncl index 64d5f66..14e0627 100644 --- a/.machine_readable/self-validating/examples/project-metadata.k9.ncl +++ b/.machine_readable/self-validating/examples/project-metadata.k9.ncl @@ -1,5 +1,5 @@ K9! -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Example Kennel-level K9 component: Project metadata # Security Level: Kennel (pure data, no execution) # No signature required diff --git a/.machine_readable/self-validating/examples/setup-repo.k9.ncl b/.machine_readable/self-validating/examples/setup-repo.k9.ncl index 66b046b..b635d5b 100644 --- a/.machine_readable/self-validating/examples/setup-repo.k9.ncl +++ b/.machine_readable/self-validating/examples/setup-repo.k9.ncl @@ -1,5 +1,5 @@ K9! -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Example Hunt-level K9 component: Repository setup automation # Security Level: Hunt (full execution with Just recipes) # ⚠️ SIGNATURE REQUIRED - DO NOT RUN WITHOUT VERIFICATION diff --git a/.machine_readable/self-validating/template-hunt.k9.ncl b/.machine_readable/self-validating/template-hunt.k9.ncl index 0814c8d..b3fcb47 100644 --- a/.machine_readable/self-validating/template-hunt.k9.ncl +++ b/.machine_readable/self-validating/template-hunt.k9.ncl @@ -1,5 +1,5 @@ K9! -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # K9 Hunt-level template: Full execution with Just recipes # Security Level: Hunt (full system access) # ⚠️ SIGNATURE REQUIRED - Review carefully before use diff --git a/.machine_readable/self-validating/template-kennel.k9.ncl b/.machine_readable/self-validating/template-kennel.k9.ncl index c78e27d..4228b26 100644 --- a/.machine_readable/self-validating/template-kennel.k9.ncl +++ b/.machine_readable/self-validating/template-kennel.k9.ncl @@ -1,5 +1,5 @@ K9! -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # K9 Kennel-level template: Pure data configuration # Security Level: Kennel (data-only, no execution) # No signature required - safe for any use diff --git a/.machine_readable/self-validating/template-yard.k9.ncl b/.machine_readable/self-validating/template-yard.k9.ncl index 75e5d89..a723f5a 100644 --- a/.machine_readable/self-validating/template-yard.k9.ncl +++ b/.machine_readable/self-validating/template-yard.k9.ncl @@ -1,5 +1,5 @@ K9! -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # K9 Yard-level template: Configuration with validation # Security Level: Yard (Nickel evaluation with contracts) # Signature recommended but not required diff --git a/.well-known/ai.txt b/.well-known/ai.txt index b68fdf8..a9c3a22 100644 --- a/.well-known/ai.txt +++ b/.well-known/ai.txt @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # AI Policy for Palimpsest Plasma User-Agent: * diff --git a/.well-known/humans.txt b/.well-known/humans.txt index ea534ef..765450d 100644 --- a/.well-known/humans.txt +++ b/.well-known/humans.txt @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # humanstxt.org /* TEAM */ diff --git a/.well-known/security.txt b/.well-known/security.txt index 34291a9..69ae34f 100644 --- a/.well-known/security.txt +++ b/.well-known/security.txt @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) Contact: mailto:j.d.a.jewell@open.ac.uk Preferred-Languages: en diff --git a/0-AI-MANIFEST.a2ml b/0-AI-MANIFEST.a2ml index 665420d..ed03b87 100644 --- a/0-AI-MANIFEST.a2ml +++ b/0-AI-MANIFEST.a2ml @@ -1,32 +1,37 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # STOP - CRITICAL READING REQUIRED **THIS FILE MUST BE READ FIRST BY ALL AI AGENTS** ## WHAT IS THIS? -This is the AI manifest for **palimpsest-plasma** — the governance automation -engine for the Palimpsest-MPL ecosystem. +This is the AI manifest for **palimpsest-plasma** — a deterministic, typed +**policy engine** for the agentic era. -PLASMA is an OCaml-based system that interprets licence, exhibits, and governance -rules as typed, machine-readable policies, evaluates repository state against -obligations, and produces findings, actions, and audit reports. +You write policies as machine-readable deontic rules (obligations, +prohibitions, permissions over repository state); `plasma check` evaluates a +repository against them with byte-for-byte reproducible results. The project +is implemented in **Rust** (three crates). It is the policy-definition and +compliance-evaluation member of a planned trio with `somethings-fishy` +(forensic bot-damage investigation) and `did-you-actually-do-that` (agent +claim verification). -## CANONICAL LOCATIONS (UNIVERSAL RULE) +**The Palimpsest license (PMPL) is a SEPARATE, FUTURE project.** This repo's +parser merely recognises PMPL SPDX identifiers. Do not reintroduce PMPL as +this project's identity. -### Machine-Readable Metadata: `.machine_readable/` ONLY +## CANONICAL LOCATIONS (UNIVERSAL RULE) -These 6 SCM files MUST exist in `.machine_readable/` directory ONLY: -1. **.machine_readable/6a2/STATE.a2ml** - Project state, progress, blockers -2. **.machine_readable/6a2/META.a2ml** - Architecture decisions, governance -3. **.machine_readable/6a2/ECOSYSTEM.a2ml** - Position in ecosystem, relationships -4. **.machine_readable/6a2/AGENTIC.a2ml** - AI agent interaction patterns -5. **.machine_readable/6a2/NEUROSYM.a2ml** - Neurosymbolic integration config -6. **.machine_readable/6a2/PLAYBOOK.a2ml** - Operational runbook +### Machine-Readable Metadata: `.machine_readable/6a2/` ONLY -**CRITICAL:** If ANY of these files exist in the root directory, this is an ERROR. +1. **STATE.a2ml** - Project state, progress, blockers +2. **META.a2ml** - Architecture decisions, governance +3. **ECOSYSTEM.a2ml** - Position in ecosystem, sibling projects +4. **AGENTIC.a2ml** - AI agent interaction patterns +5. **NEUROSYM.a2ml** - Neurosymbolic integration config +6. **PLAYBOOK.a2ml** - Operational runbook -### Bot Directives: `.bot_directives/` ONLY +### Bot Directives: `.machine_readable/bot_directives/` Bot-specific instructions for rhodibot, echidnabot, sustainabot, glambot, seambot, finishbot. @@ -38,14 +43,23 @@ seambot, finishbot. ## CORE INVARIANTS -1. **No SCM duplication** - Root must NOT contain .machine_readable/6a2/STATE.a2ml, .machine_readable/6a2/META.a2ml, etc. -2. **Single source of truth** - `.machine_readable/` is authoritative -3. **No stale metadata** - If root SCMs exist, they are OUT OF DATE -4. **License consistency** - All code PMPL-1.0-or-later unless platform requires PMPL-1.0-or-later -5. **Author attribution** - Always "Jonathan D.A. Jewell " -6. **OCaml core** - PLASMA engine is OCaml; do not rewrite in other languages -7. **Deontic operators** - Obligation/Prohibition/Permission are first-class AST values -8. **Immutable base policy** - Exhibits extend but never mutate the base policy +1. **Rust core** - The engine, parser, and CLI are Rust. Do not rewrite in + another language. (A future OCaml/Catala *formal core* may verify the + engine, per docs/engine-v0-design.adoc — that is verification, not a + reimplementation of the runtime.) +2. **Determinism** - `evaluate` is pure and total: no clocks, randomness, IO, + or ambient state; BTree ordering everywhere. Preserve this. +3. **Reject at load, never mid-run** - unsupported policy constructs fail + schema validation up front, so any policy that loads evaluates. +4. **Deontic operators** - Obligation/Prohibition/Permission are first-class + AST values. +5. **Overlays extend, never mutate** - overlays add rules; base policy is + immutable. +6. **License consistency** - Code MPL-2.0; documentation CC-BY-SA-4.0. Every + file carries an SPDX header. +7. **Author attribution** - Always "Jonathan D.A. Jewell ". +8. **Single source of truth** - `.machine_readable/6a2/` is authoritative for + project state. ## REPOSITORY STRUCTURE @@ -55,76 +69,66 @@ palimpsest-plasma/ ├── README.adoc # Project overview ├── ROADMAP.adoc # Implementation milestones ├── TOPOLOGY.md # Architecture diagram + dashboard -├── LICENSE # PMPL-1.0-or-later -├── Justfile # Build automation +├── LICENSE # MPL-2.0 +├── Justfile # Build automation (cargo) +│ +├── src/ # the `plasma` CLI binary (Rust) +│ ├── main.rs # check/facts/policy/audit/init/badge/migrate +│ ├── check.rs facts_cmd.rs policy_cmd.rs +│ └── audit.rs init.rs migrate.rs badge.rs │ -├── src/ # PLASMA engine (OCaml) — TO BE BUILT -│ ├── core/ # Policy AST, eval, migration, findings -│ ├── integration/ # CLI, git hooks, CI, daemon -│ └── adapters/ # Repo scan, SPDX, exhibits, metadata +├── plasma-engine/ # the policy engine (Rust) +│ ├── src/ast.rs schema.rs facts.rs eval.rs finding.rs +│ ├── src/report/ # human + SARIF renderers +│ └── policies/repo-hygiene.plasma.toml # bundled, self-applied policy │ -├── union-policy-parser/ # Sub-project: A2ML contract validation (Rust) -│ ├── src/rust/ # Parser, validator, reporter, schemas -│ ├── src/abi/ # Idris2 ABI definitions -│ ├── ffi/zig/ # Zig FFI implementation -│ ├── schemas/ # Union policy schemas (A2ML) -│ └── examples/ # Example contracts +├── plasma-parser/ # SPDX/license machinery (Rust) +│ └── src/{spdx,family,zone,audit,compat,report}/ │ ├── docs/ # Documentation -│ ├── architecture.adoc # Full system architecture -│ ├── policy-ast-v0.1.adoc # Typed AST specification -│ ├── cli-design.adoc # CLI and integration design -│ ├── contractiles.adoc # Contractiles concept -│ ├── mvp-v1.adoc # MVP v1 deliverables -│ └── release-log.adoc # Release history +│ ├── engine-v0-design.adoc # NORMATIVE semantics + Catala-readiness +│ ├── architecture.adoc # System architecture, data flow +│ ├── policy-ast-v0.1.adoc # Historical OCaml-typed design (lineage) +│ └── cli-design.adoc # CLI and integration design │ ├── site/ # Elixir/NimblePublisher static site -├── assets/badges/ # Badge SVGs -├── contractiles/ # RSR operational contractiles (k9/dust/lust/must/trust) -├── .machine_readable/ # SCM files (6 files) -├── .bot_directives/ # Bot instructions +├── .machine_readable/ # a2ml metadata + bot directives + contractiles └── .github/workflows/ # CI/CD workflows ``` ## KEY DOCUMENTS -Read these for full context: - -- `docs/architecture.adoc` - Three-layer architecture, data flow, eval loop -- `docs/policy-ast-v0.1.adoc` - Complete OCaml type definitions for the AST -- `docs/cli-design.adoc` - CLI subcommands and integration surfaces -- `docs/contractiles.adoc` - Contractiles: reversible governance primitives -- `ROADMAP.adoc` - Implementation milestones from v1.0 through v3.x +- `docs/engine-v0-design.adoc` - NORMATIVE: policy format, evaluation + semantics, determinism/totality guarantees. Read before touching the engine. +- `docs/architecture.adoc` - Crate layout, data flow. +- `docs/policy-ast-v0.1.adoc` - Historical typed-AST design (lineage only). +- `docs/cli-design.adoc` - CLI subcommands and roadmap surfaces. +- `ROADMAP.adoc` - Milestones v0.2 → v1.0. ## SESSION STARTUP CHECKLIST -1. Read THIS file (0-AI-MANIFEST.a2ml) first -2. Understand canonical locations (.machine_readable/, .bot_directives/) -3. Know the invariants (no SCM duplication, OCaml core, deontic operators) -4. Read `.machine_readable/6a2/STATE.a2ml` for current status -5. Read `docs/architecture.adoc` for system design -6. Check `ROADMAP.adoc` for current milestone +1. Read THIS file first +2. Read `.machine_readable/6a2/STATE.a2ml` for current status +3. Read `docs/engine-v0-design.adoc` before changing the engine +4. Verify with `cargo test --workspace` and `plasma check .` ## LIFECYCLE HOOKS ### on-enter (Session Start) - 1. Read and acknowledge this manifest 2. Read `.machine_readable/6a2/STATE.a2ml` -3. Check for blockers -4. State understanding of canonical locations and invariants +3. State understanding of the Rust-core / determinism invariants ### on-exit (Session End) - 1. Update `.machine_readable/6a2/STATE.a2ml` if changes made -2. Document new blockers +2. Ensure `cargo test --workspace` passes and `plasma check .` is clean 3. Summarize outcomes ## META - **Format Version:** 2.0.0 - **Created:** 2026-01-10 -- **Updated:** 2026-02-20 +- **Updated:** 2026-07-02 - **Maintained By:** Jonathan D.A. Jewell (hyperpolymath) -- **License:** PMPL-1.0-or-later +- **License:** MPL-2.0 - **Protocol:** https://github.com/hyperpolymath/0-ai-gatekeeper-protocol diff --git a/CHANGELOG.md b/CHANGELOG.md index e829285..0b891b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,58 @@ this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.0] - 2026-07-02 + +Identity pivot: palimpsest-plasma is a deterministic, typed policy engine +for the agentic era. The Palimpsest license (PMPL) becomes a separate +future project. + +### Added + +- feat(engine): new `plasma-engine` crate — typed policy AST (deontic + modalities, composable conditions, overlays), versioned TOML/JSON schema + loading with load-time rejection of reserved constructs, deterministic + fact collection, a pure/total evaluator, human/JSON/SARIF rendering +- feat(cli): `plasma check` (policy evaluation, exit-code gated), + `plasma facts` (deterministic fact snapshot), `plasma policy validate` +- feat(cli): bundled repo-hygiene policy; the repo checks itself in tests +- feat(parser): `scan_repo` — deterministic zone-aware repository scanner +- feat(parser): raw SPDX header extraction (`extract_spdx_raw_from_content`) +- docs: `docs/engine-v0-design.adoc` — normative semantics and + Catala-readiness guarantees + +### Changed + +- **BREAKING**: binary renamed `palimpsest-plasma` → `plasma` +- **BREAKING**: licensing unified — code MPL-2.0, documentation + CC-BY-SA-4.0; all SPDX headers swept (PMPL/PPMPL identifiers survive only + as parseable inputs to plasma-parser) +- `plasma audit` rewritten on plasma-parser (real SPDX expression parsing, + `.plasma.toml` zone awareness) instead of substring matching +- `plasma init`/`migrate`/`badge` genericized to any SPDX license + (MPL-2.0 default); Covenant embedding removed +- Root crate moved to edition 2021; workspace versions unified at 0.2.0 +- README/EXPLAINME/TOPOLOGY/ROADMAP/architecture/cli-design rewritten to + describe what exists (the OCaml engine described previously was never + built; the design lives on in docs/policy-ast-v0.1.adoc as lineage) + +### Fixed + +- Workspace compiles again (init/migrate embedded a nonexistent license + file) +- SPDX catalog PMPL/PPMPL identifier drift (2 failing tests now pass) +- codemeta.json/CITATION.cff described an unrelated boilerplate project + +### Removed + +- union-policy-parser sub-project (mostly non-compiling scaffolding; + preserved in git history, belongs in its own repository) +- Corrupted `LICENSE-PMPL-2.0.txt`, stale MVP release tarballs, badge + pack, signature files of removed artifacts, `PALIMPSEST-COVENANT.md`, + `docs/mvp-v1.adoc` + +## [0.1.0] - 2026-05/06 (previously listed as Unreleased) + ### Added - feat(crg): add crg-grade and crg-badge justfile recipes (#5) diff --git a/CITATION.cff b/CITATION.cff index b9964cc..d42c3b6 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -7,12 +7,13 @@ authors: family-names: "Jewell" email: "j.d.a.jewell@open.ac.uk" affiliation: "hyperpolymath" -version: 0.1.0 -date-released: 2026-02-12 -license: "PMPL-1.0-or-later" +version: 0.2.0 +date-released: 2026-07-02 +license: "MPL-2.0" repository-code: "https://github.com/hyperpolymath/palimpsest-plasma" keywords: - - "plasma" - - "framework" - - "palimpsest" -abstract: "A framework for dynamic, self-modifying software systems." + - "policy-engine" + - "compliance" + - "spdx" + - "deterministic" +abstract: "Deterministic, typed policy engine: machine-readable deontic policies evaluated against repository state with reproducible results." diff --git a/Cargo.lock b/Cargo.lock index 30f4fb0..735fe43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,10 +258,12 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "palimpsest-plasma" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", + "plasma-engine", + "plasma-parser", "serde", "serde_json", "tempfile", @@ -269,9 +271,23 @@ dependencies = [ "walkdir", ] +[[package]] +name = "plasma-engine" +version = "0.2.0" +dependencies = [ + "glob", + "plasma-parser", + "serde", + "serde_json", + "tempfile", + "thiserror", + "toml", + "walkdir", +] + [[package]] name = "plasma-parser" -version = "0.1.0" +version = "0.2.0" dependencies = [ "glob", "serde", diff --git a/Cargo.toml b/Cargo.toml index 94ec328..7b01d76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,27 @@ -# SPDX-License-Identifier: PPMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 [workspace] -members = [".", "plasma-parser"] +members = [".", "plasma-parser", "plasma-engine"] [package] name = "palimpsest-plasma" -version = "0.1.0" -edition = "2024" +version = "0.2.0" +edition = "2021" +rust-version = "1.74" authors = ["Jonathan D.A. Jewell "] -description = "Palimpsest license adoption toolkit — add PPMPL-1.0-or-later to any project in one command" +description = "Deterministic, typed policy engine — define machine-readable policies and check repositories against them" license = "MPL-2.0" repository = "https://github.com/hyperpolymath/palimpsest-plasma" -keywords = ["license", "palimpsest", "pmpl", "spdx", "provenance"] +keywords = ["policy", "compliance", "spdx", "audit", "agentic"] categories = ["command-line-utilities", "development-tools"] +[[bin]] +name = "plasma" +path = "src/main.rs" + [dependencies] +plasma-parser = { path = "plasma-parser" } +plasma-engine = { path = "plasma-engine" } clap = { version = "4", features = ["derive"] } anyhow = "1" walkdir = "2" diff --git a/EXPLAINME.adoc b/EXPLAINME.adoc index 0650949..a786193 100644 --- a/EXPLAINME.adoc +++ b/EXPLAINME.adoc @@ -1,68 +1,90 @@ -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell = Palimpsest Plasma — Show Me The Receipts :toc: :icons: font -The README makes claims. This file backs them up. +The README makes claims. This file backs them up — against files that +actually exist. (An earlier version of this document cited OCaml sources +that were never written; that failure mode is precisely what this project +now exists to prevent.) == Core Claims -=== Claim 1: OCaml-Based Governance Automation Engine for PMPL-1.0 Ecosystem +=== Claim 1: Deterministic, typed policy engine -*From README:* "palimpsest-plasma is the official governance automation engine for the Palimpsest-MPL ecosystem. It is an OCaml-based system that interprets the licence, exhibits, and governance rules as typed, machine-readable policies, and evaluates real-world repository states against those obligations." +*From README:* "You write policies as machine-readable, deontic rules and +`plasma check` evaluates a repository against them exactly." -*Evidence:* The system is structured in three layers: `core/` (canonical PLASMA substrate: Policy AST, schemas, evaluation, migrations, facts, findings, actions, governance runtime), `integration/` (surfaces: CLI, git hooks, CI, daemon), and `adapters/` (IO and environment-specific collectors/parsers: SPDX, exhibits, repo topology). The OCaml core at `core/policy_ast.ml` defines a strongly-typed AST for obligations, prohibitions, permissions, and conditions. The evaluation engine at `core/evaluation.ml` applies policies to repository facts, producing findings and recommended actions. See `docs/architecture.adoc` and `docs/policy-ast-v0.1.adoc` for full design specifications. **Caveat:** The governance runtime is still in active development; some complex governance scenarios may not yet be fully supported. +*Evidence:* The typed AST is `plasma-engine/src/ast.rs` (subjects, resources, +composable conditions, deontic modalities, actions, overlays). The evaluator +is `plasma-engine/src/eval.rs` — a pure function with no IO; every fact it +sees comes from `plasma-engine/src/facts.rs` (BTree-ordered, no timestamps). +Determinism is enforced by test: `tests/check_selftest.rs` +(`check_json_is_deterministic`) runs `plasma check` twice and asserts +byte-identical JSON, and `plasma-engine/src/eval.rs` has the same check at +unit level (`test_determinism_identical_runs`). Unsupported constructs are +rejected at load in `plasma-engine/src/schema.rs`, so evaluation is total. -=== Claim 2: Union Policy Parser for Employment Contract Validation via A2ML +=== Claim 2: Self-demonstrating -*From README:* "union-policy-parser: A Rust-based policy validation engine for union contracts (NUJ, IWW, UCU) using A2ML (Attested Markup Language). Parses and validates employment contracts against union standards with formal proofs and grievance generation." +*From README:* "`plasma check .` evaluates this very repository." -*Evidence:* The `union-policy-parser/` subdirectory contains a Rust implementation that uses A2ML (Attested Markup Language) to encode union contract rules (NUJ — National Union of Journalists, IWW — Industrial Workers of the World, UCU — University and College Union). The parser at `union-policy-parser/src/parser.rs` reads A2ML contract files and validates them against union standards. Formal proofs at `union-policy-parser/src/proofs/` verify that grievance generation is deterministic and fair. The tool generates human-readable grievance reports with justification chains. See `union-policy-parser/README.adoc` for full details. **Caveat:** Union contract rules are complex and may vary by jurisdiction; this tool encodes current understanding but requires periodic updates as contracts evolve. +*Evidence:* The bundled policy `plasma-engine/policies/repo-hygiene.plasma.toml` +requires a LICENSE, a README, and parseable SPDX headers on source files. +`tests/check_selftest.rs` (`check_self_passes`) fails the build if this +repository stops satisfying its own shipped policy. + +=== Claim 3: Real SPDX machinery + +*From README:* "`plasma audit` checks SPDX headers against a zone-aware +license map, powered by a real SPDX expression parser." + +*Evidence:* The recursive-descent SPDX parser is +`plasma-parser/src/spdx/mod.rs` with lexer (`spdx/lexer.rs`) and identifier +catalog (`spdx/catalog.rs`, including Palimpsest-family extensions). Zone +mapping from `.plasma.toml` is `plasma-parser/src/zone/boundary.rs`; the +repository scanner is `plasma-parser/src/audit/scan.rs`; the CLI wiring is +`src/audit.rs` — which uses that parser rather than substring matching. + +== Test suite + +`cargo test --workspace`: 66 tests at the time of writing — inline units in +plasma-parser (SPDX parsing, catalog, zones, compat, content detection) and +plasma-engine (schema validation, evaluation semantics, SARIF shape), plus +the root self-tests described above. == Technology Choices [cols="1,2"] |=== -| Technology | Learn More +| Technology | Why -| **OCaml** | Strong static types, pattern matching, functional paradigm -| **Rust** | Systems programming, formal proofs (union-policy-parser) -| **A2ML** | Attested Markup Language for contract encoding -| **Elixir** | Static site generation (site/ folder) for compliance guidance +| **Rust** | One toolchain for parser, engine, and CLI; `#![forbid(unsafe_code)]` across all three crates +| **TOML + JSON policies** | TOML for humans, JSON for interchange; both deserialize to the same serde model (`docs/engine-v0-design.adoc`) +| **SARIF 2.1.0** | Stable `plasma/` namespace for GitHub Code Scanning and sibling tools +| **Elixir (NimblePublisher)** | Static site generation for the guidance site (`site/`) |=== -== Dogfooded Across The Account - -Palimpsest Plasma validates compliance across the hyperpolymath ecosystem: -- All repositories must satisfy PLASMA policies (via CI integration at `.github/workflows/`) -- License tooling in `docs/mvp-v1.adoc` provides canonical PMPL-1.0 guidance -- Badge and documentation assets in `assets/badges/` are used across all hyperpolymath projects -- Union policy validation supports internal team governance (NUJ, IWW, UCU contracts) - == File Map [cols="1,3"] |=== | Path | What's There -| `core/policy_ast.ml` | OCaml AST for policies: obligations, prohibitions, permissions, conditions -| `core/evaluation.ml` | Policy evaluation engine: applies policies to facts, produces findings -| `core/schemas.ml` | Versioned policy schemas with migrations -| `core/governance_runtime.ml` | Enforces council decisions, tracks exhibit lifecycle -| `integration/cli.ml` | CLI entry point for local checks -| `integration/git_hooks.ml` | Git pre-commit/pre-push integration -| `integration/github_actions.ml` | GitHub Actions CI workflow -| `adapters/spdx_parser.ml` | Parses SPDX headers and manifests -| `adapters/exhibit_loader.ml` | Loads Palimpsest exhibits -| `adapters/repo_topology.ml` | Analyzes repository structure -| `union-policy-parser/src/parser.rs` | Rust parser for A2ML contract files -| `union-policy-parser/src/proofs/` | Formal proofs of grievance determinism -| `docs/architecture.adoc` | System architecture and design -| `docs/policy-ast-v0.1.adoc` | Formal policy AST specification -| `docs/mvp-v1.adoc` | MVP v1 deliverables and compliance checklist -| `site/` | Elixir mix project for compliance guidance (static site generation) -| `assets/badges/` | Badge SVGs and documentation assets +| `plasma-engine/src/ast.rs` | Typed policy AST: subjects, resources, conditions, modalities, actions, overlays +| `plasma-engine/src/schema.rs` | Versioned loading (TOML/JSON), load-time rejection of reserved constructs +| `plasma-engine/src/facts.rs` | Fact collection — the only impure module +| `plasma-engine/src/eval.rs` | Pure, total evaluator with the deontic matrix +| `plasma-engine/src/report/` | Human and SARIF renderers +| `plasma-engine/policies/repo-hygiene.plasma.toml` | The bundled, self-applied policy +| `plasma-parser/src/spdx/` | SPDX lexer, parser, identifier catalog +| `plasma-parser/src/zone/` | `.plasma.toml` license zones +| `plasma-parser/src/audit/` | Header extraction, repo scanning, LICENSE content detection +| `src/` | The `plasma` CLI: check, facts, policy, audit, init, badge, migrate +| `docs/engine-v0-design.adoc` | Normative semantics + Catala-readiness guarantees +| `docs/policy-ast-v0.1.adoc` | Historical OCaml-typed design (lineage) +| `site/` | Elixir mix project for the guidance site |=== == Questions? diff --git a/Justfile b/Justfile index f5a51c0..893d7f2 100644 --- a/Justfile +++ b/Justfile @@ -1,33 +1,26 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# RSR-template-repo - RSR Standard Justfile Template +# SPDX-License-Identifier: MPL-2.0 +# Justfile — task runner for palimpsest-plasma (Rust workspace). # https://just.systems/man/en/ # -# This is the CANONICAL template for all RSR projects. -# Copy this file to new projects and customize the {{PLACEHOLDER}} values. -# -# IMPORTANT: This file MUST be named "Justfile" (capital J) for RSR compliance. -# -# Run `just` to see all available recipes -# Run `just cookbook` to generate docs/just-cookbook.adoc -# Run `just combinations` to see matrix recipe options +# Run `just` to list recipes. set shell := ["bash", "-uc"] set dotenv-load := true set positional-arguments := true -# Project metadata +# Optional generated contractile recipes (estate tooling). import? "contractile.just" project := "palimpsest-plasma" -version := "2.0.0-dev" +version := "0.2.0" tier := "infrastructure" -state := ".machine_readable/STATE.scm" +state := ".machine_readable/6a2/STATE.a2ml" -# ═══════════════════════════════════════════════════════════════════════════════ +# ═══════════════════════════════════════════════════════════════════════════ # DEFAULT & HELP -# ═══════════════════════════════════════════════════════════════════════════════ +# ═══════════════════════════════════════════════════════════════════════════ -# Show all available recipes with descriptions +# Show all available recipes default: @just --list --unsorted @@ -36,478 +29,109 @@ help recipe="": #!/usr/bin/env bash if [ -z "{{recipe}}" ]; then just --list --unsorted - echo "" - echo "Usage: just help " - echo " just cookbook # Generate full documentation" - echo " just combinations # Show matrix recipes" else just --show "{{recipe}}" 2>/dev/null || echo "Recipe '{{recipe}}' not found" fi # Show this project's info info: - @echo "Project: {{project}}" - @echo "Version: {{version}}" + @echo "Project: {{project}}" + @echo "Version: {{version}}" @echo "RSR Tier: {{tier}}" - @echo "Recipes: $(just --summary | wc -w)" - @[ -f {{state}} ] && grep -oP '\(phase\s+\.\s+\K[^)]+' {{state}} | head -1 | xargs -I{} echo "Phase: {}" || true + @[ -f {{state}} ] && grep -E '^phase' {{state}} | head -1 || true -# ═══════════════════════════════════════════════════════════════════════════════ -# BUILD & COMPILE -# ═══════════════════════════════════════════════════════════════════════════════ +# ═══════════════════════════════════════════════════════════════════════════ +# BUILD & TEST +# ═══════════════════════════════════════════════════════════════════════════ -# Build the PLASMA engine (OCaml/dune) +# Build the whole workspace (debug) build *args: - @echo "Building {{project}}..." - dune build {{args}} + cargo build --workspace {{args}} -# Build in release mode with optimizations +# Build the whole workspace (release) build-release *args: - @echo "Building {{project}} (release)..." - dune build --force {{args}} - -# Build and watch for changes -build-watch: - @echo "Watching for changes..." - dune build --watch - -# Clean build artifacts [reversible: rebuild with `just build`] -clean: - @echo "Cleaning..." - dune clean + cargo build --workspace --release {{args}} -# Deep clean including caches [reversible: rebuild] -clean-all: clean - rm -rf _opam .cache .tmp - -# ═══════════════════════════════════════════════════════════════════════════════ -# TEST & QUALITY -# ═══════════════════════════════════════════════════════════════════════════════ - -# Run all tests +# Run the test suite test *args: - @echo "Running tests..." - dune runtest {{args}} - -# Run tests with verbose output -test-verbose: - @echo "Running tests (verbose)..." - dune runtest --force --verbose - -# Run tests and generate coverage report -test-coverage: - @echo "Running tests with coverage..." - dune runtest --instrument-with bisect_ppx --force - -# ═══════════════════════════════════════════════════════════════════════════════ -# LINT & FORMAT -# ═══════════════════════════════════════════════════════════════════════════════ + cargo test --workspace {{args}} -# Format all source files [reversible: git checkout] +# Format all code fmt: - @echo "Formatting..." - dune fmt + cargo fmt --all -# Check formatting without changes +# Check formatting without modifying fmt-check: - @echo "Checking format..." - dune fmt --preview + cargo fmt --all -- --check -# Run linter +# Lint with clippy, warnings as errors lint: - @echo "Linting..." - dune build @lint + cargo clippy --workspace --all-targets -- -D warnings -# Run all quality checks -quality: fmt-check lint test - @echo "All quality checks passed!" - -# Fix all auto-fixable issues [reversible: git checkout] -fix: fmt - @echo "Fixed all auto-fixable issues" - -# ═══════════════════════════════════════════════════════════════════════════════ -# RUN & EXECUTE -# ═══════════════════════════════════════════════════════════════════════════════ +# Remove build artifacts +clean: + cargo clean -# Run the PLASMA CLI +# Run the plasma binary with arbitrary arguments run *args: - @echo "Running {{project}}..." - dune exec -- plasma {{args}} - -# Run in development mode with hot reload -dev: - @echo "Starting dev mode..." - dune build --watch - -# Run OCaml REPL with project loaded -repl: - @echo "Starting utop REPL..." - dune utop - -# ═══════════════════════════════════════════════════════════════════════════════ -# DEPENDENCIES -# ═══════════════════════════════════════════════════════════════════════════════ - -# Install all dependencies -deps: - @echo "Installing dependencies..." - opam install . --deps-only --with-test --yes - -# Audit dependencies for vulnerabilities -deps-audit: - @echo "Auditing dependencies..." - opam list --installed --short | xargs -I{} opam info {} 2>/dev/null || true - -# ═══════════════════════════════════════════════════════════════════════════════ -# DOCUMENTATION -# ═══════════════════════════════════════════════════════════════════════════════ - -# Generate all documentation -docs: - @mkdir -p docs/generated docs/man - just cookbook - just man - @echo "Documentation generated in docs/" - -# Generate justfile cookbook documentation -cookbook: - #!/usr/bin/env bash - mkdir -p docs - OUTPUT="docs/just-cookbook.adoc" - echo "= {{project}} Justfile Cookbook" > "$OUTPUT" - echo ":toc: left" >> "$OUTPUT" - echo ":toclevels: 3" >> "$OUTPUT" - echo "" >> "$OUTPUT" - echo "Generated: $(date -Iseconds)" >> "$OUTPUT" - echo "" >> "$OUTPUT" - echo "== Recipes" >> "$OUTPUT" - echo "" >> "$OUTPUT" - just --list --unsorted | while read -r line; do - if [[ "$line" =~ ^[[:space:]]+([a-z_-]+) ]]; then - recipe="${BASH_REMATCH[1]}" - echo "=== $recipe" >> "$OUTPUT" - echo "" >> "$OUTPUT" - echo "[source,bash]" >> "$OUTPUT" - echo "----" >> "$OUTPUT" - echo "just $recipe" >> "$OUTPUT" - echo "----" >> "$OUTPUT" - echo "" >> "$OUTPUT" - fi - done - echo "Generated: $OUTPUT" - -# Generate man page -man: - #!/usr/bin/env bash - mkdir -p docs/man - cat > docs/man/{{project}}.1 << 'MANEOF' - .TH PALIMPSEST-PLASMA 1 "$(date +%Y-%m-%d)" "{{version}}" "PLASMA Manual" - .SH NAME - {{project}} \- Governance automation engine for Palimpsest-MPL - .SH SYNOPSIS - .B just - [recipe] [args...] - .SH DESCRIPTION - Governance automation engine for the Palimpsest-MPL ecosystem. - .SH AUTHOR - Hyperpolymath - MANEOF - echo "Generated: docs/man/{{project}}.1" - -# ═══════════════════════════════════════════════════════════════════════════════ -# CONTAINERS (nerdctl-first, podman-fallback) -# ═══════════════════════════════════════════════════════════════════════════════ - -# Detect container runtime: nerdctl > podman > docker -[private] -container-cmd: - #!/usr/bin/env bash - if command -v nerdctl >/dev/null 2>&1; then - echo "nerdctl" - elif command -v podman >/dev/null 2>&1; then - echo "podman" - elif command -v docker >/dev/null 2>&1; then - echo "docker" - else - echo "ERROR: No container runtime found (install nerdctl, podman, or docker)" >&2 - exit 1 - fi - -# Build container image -container-build tag="latest": - #!/usr/bin/env bash - CTR=$(just container-cmd) - if [ -f Containerfile ]; then - echo "Building with $CTR..." - $CTR build -t {{project}}:{{tag}} -f Containerfile . - else - echo "No Containerfile found" - fi - -# Run container -container-run tag="latest" *args: - #!/usr/bin/env bash - CTR=$(just container-cmd) - $CTR run --rm -it {{project}}:{{tag}} {{args}} - -# Push container image -container-push registry="ghcr.io/hyperpolymath" tag="latest": - #!/usr/bin/env bash - CTR=$(just container-cmd) - $CTR tag {{project}}:{{tag}} {{registry}}/{{project}}:{{tag}} - $CTR push {{registry}}/{{project}}:{{tag}} - -# ═══════════════════════════════════════════════════════════════════════════════ -# CI & AUTOMATION -# ═══════════════════════════════════════════════════════════════════════════════ - -# Run full CI pipeline locally -ci: deps quality - @echo "CI pipeline complete!" - -# Install git hooks -install-hooks: - #!/usr/bin/env bash - mkdir -p .git/hooks - cat > .git/hooks/pre-commit << 'HOOKEOF' - #!/bin/bash - just fmt-check || exit 1 - just lint || exit 1 - HOOKEOF - chmod +x .git/hooks/pre-commit - echo "Git hooks installed" - -# ═══════════════════════════════════════════════════════════════════════════════ -# SECURITY -# ═══════════════════════════════════════════════════════════════════════════════ - -# Run security audit -security: deps-audit - @echo "=== Security Audit ===" - @command -v trivy >/dev/null && trivy fs --severity HIGH,CRITICAL . || true - @echo "Security audit complete" - -# Generate SBOM -sbom: - @mkdir -p docs/security - @command -v syft >/dev/null && syft . -o spdx-json > docs/security/sbom.spdx.json || echo "syft not found" - -# ═══════════════════════════════════════════════════════════════════════════════ -# VALIDATION & COMPLIANCE -# ═══════════════════════════════════════════════════════════════════════════════ - -# Validate RSR compliance -validate-rsr: - #!/usr/bin/env bash - echo "=== RSR Compliance Check ===" - MISSING="" - for f in .editorconfig .gitignore Justfile RSR_COMPLIANCE.adoc README.adoc; do - [ -f "$f" ] || MISSING="$MISSING $f" - done - for d in .well-known; do - [ -d "$d" ] || MISSING="$MISSING $d/" - done - for f in .well-known/security.txt .well-known/ai.txt .well-known/humans.txt; do - [ -f "$f" ] || MISSING="$MISSING $f" - done - if [ ! -f "guix.scm" ] && [ ! -f ".guix-channel" ] && [ ! -f "flake.nix" ]; then - MISSING="$MISSING guix.scm/flake.nix" - fi - if [ -n "$MISSING" ]; then - echo "MISSING:$MISSING" - exit 1 - fi - echo "RSR compliance: PASS" - -# Validate STATE.scm syntax -validate-state: - @if [ -f "{{state}}" ]; then \ - guile -c "(primitive-load \"{{state}}\")" 2>/dev/null && echo "{{state}}: valid" || echo "{{state}}: INVALID"; \ - else \ - echo "No STATE.scm found"; \ - fi - -# Full validation suite -validate: validate-rsr validate-state - @echo "All validations passed!" - -# ═══════════════════════════════════════════════════════════════════════════════ -# STATE MANAGEMENT -# ═══════════════════════════════════════════════════════════════════════════════ - -# Update STATE.scm timestamp -state-touch: - @if [ -f "{{state}}" ]; then \ - sed -i 's/(updated . "[^"]*")/(updated . "'"$(date -Iseconds)"'")/' {{state}} && \ - echo "{{state}} timestamp updated"; \ - fi - -# Show current phase from STATE.scm -state-phase: - @grep -oP '\(phase\s+\.\s+\K[^)]+' {{state}} 2>/dev/null | head -1 || echo "unknown" - -# ═══════════════════════════════════════════════════════════════════════════════ -# GUIX & NIX -# ═══════════════════════════════════════════════════════════════════════════════ - -# Enter Guix development shell (primary) -guix-shell: - guix shell -D -f guix.scm - -# Build with Guix -guix-build: - guix build -f guix.scm - -# Enter Nix development shell (fallback) -nix-shell: - @if [ -f "flake.nix" ]; then nix develop; else echo "No flake.nix"; fi + cargo run -- {{args}} -# ═══════════════════════════════════════════════════════════════════════════════ -# HYBRID AUTOMATION -# ═══════════════════════════════════════════════════════════════════════════════ +# ═══════════════════════════════════════════════════════════════════════════ +# DOGFOOD — plasma checks itself +# ═══════════════════════════════════════════════════════════════════════════ -# Run local automation tasks -automate task="all": - #!/usr/bin/env bash - case "{{task}}" in - all) just fmt && just lint && just test && just docs && just state-touch ;; - cleanup) just clean && find . -name "*.orig" -delete && find . -name "*~" -delete ;; - update) just deps && just validate ;; - *) echo "Unknown: {{task}}. Use: all, cleanup, update" && exit 1 ;; - esac - -# ═══════════════════════════════════════════════════════════════════════════════ -# COMBINATORIC MATRIX RECIPES -# ═══════════════════════════════════════════════════════════════════════════════ - -# Build matrix: [debug|release] × [target] × [features] -build-matrix mode="debug" target="" features="": - @echo "Build matrix: mode={{mode}} target={{target}} features={{features}}" - # Customize for your build system - -# Test matrix: [unit|integration|e2e|all] × [verbosity] × [parallel] -test-matrix suite="unit" verbosity="normal" parallel="true": - @echo "Test matrix: suite={{suite}} verbosity={{verbosity}} parallel={{parallel}}" - -# Container matrix: [build|run|push|shell|scan] × [registry] × [tag] -container-matrix action="build" registry="ghcr.io/hyperpolymath" tag="latest": - @echo "Container matrix: action={{action}} registry={{registry}} tag={{tag}}" - -# CI matrix: [lint|test|build|security|all] × [quick|full] -ci-matrix stage="all" depth="quick": - @echo "CI matrix: stage={{stage}} depth={{depth}}" - -# Show all matrix combinations -combinations: - @echo "=== Combinatoric Matrix Recipes ===" - @echo "" - @echo "Build Matrix: just build-matrix [debug|release] [target] [features]" - @echo "Test Matrix: just test-matrix [unit|integration|e2e|all] [verbosity] [parallel]" - @echo "Container: just container-matrix [build|run|push|shell|scan] [registry] [tag]" - @echo "CI Matrix: just ci-matrix [lint|test|build|security|all] [quick|full]" - @echo "" - @echo "Total combinations: ~10 billion" - -# ═══════════════════════════════════════════════════════════════════════════════ -# VERSION CONTROL -# ═══════════════════════════════════════════════════════════════════════════════ - -# Show git status -status: - @git status --short - -# Show recent commits -log count="20": - @git log --oneline -{{count}} +# Evaluate this repository against the bundled repo-hygiene policy +check: + cargo run -- check . --format human -# ═══════════════════════════════════════════════════════════════════════════════ -# UTILITIES -# ═══════════════════════════════════════════════════════════════════════════════ +# Dump the deterministic fact snapshot for this repository +facts: + cargo run -- facts . -# Count lines of code -loc: - @find . \( -name "*.ml" -o -name "*.mli" -o -name "*.rs" -o -name "*.ex" -o -name "*.scm" -o -name "*.idr" -o -name "*.zig" \) 2>/dev/null | xargs wc -l 2>/dev/null | tail -1 || echo "0" +# Audit SPDX headers across this repository +audit: + cargo run -- audit . -# Show TODO comments -todos: - @grep -rn "TODO\|FIXME" --include="*.ml" --include="*.mli" --include="*.rs" --include="*.ex" . 2>/dev/null || echo "No TODOs" +# Validate the bundled policy file +policy-check: + cargo run -- policy validate plasma-engine/policies/repo-hygiene.plasma.toml -# Open in editor -edit: - ${EDITOR:-code} . +# ═══════════════════════════════════════════════════════════════════════════ +# CI & DIAGNOSTICS +# ═══════════════════════════════════════════════════════════════════════════ -# [AUTO-GENERATED] Multi-arch / RISC-V target -build-riscv: - @echo "Building for RISC-V..." - cross build --target riscv64gc-unknown-linux-gnu +# Everything CI runs: format check, lint, tests, and self-check +ci: fmt-check lint test check -# Run panic-attacker pre-commit scan -assail: - @command -v panic-attack >/dev/null 2>&1 && panic-attack assail . || echo "panic-attack not found — install from https://github.com/hyperpolymath/panic-attacker" - -# Self-diagnostic — checks dependencies, permissions, paths +# Diagnose the local toolchain doctor: - @echo "Running diagnostics for palimpsest-plasma..." - @echo "Checking required tools..." - @command -v just >/dev/null 2>&1 && echo " [OK] just" || echo " [FAIL] just not found" - @command -v git >/dev/null 2>&1 && echo " [OK] git" || echo " [FAIL] git not found" - @echo "Checking for hardcoded paths..." - @grep -rn '$HOME\|$ECLIPSE_DIR' --include='*.rs' --include='*.ex' --include='*.res' --include='*.gleam' --include='*.sh' . 2>/dev/null | head -5 || echo " [OK] No hardcoded paths" - @echo "Diagnostics complete." - -# Auto-repair common issues -heal: - @echo "Attempting auto-repair for palimpsest-plasma..." - @echo "Fixing permissions..." - @find . -name "*.sh" -exec chmod +x {} \; 2>/dev/null || true - @echo "Cleaning stale caches..." - @rm -rf .cache/stale 2>/dev/null || true - @echo "Repair complete." - -# Guided tour of key features + #!/usr/bin/env bash + set -e + echo "== toolchain ==" + command -v cargo >/dev/null && cargo --version || { echo "cargo: MISSING (install rustup)"; exit 1; } + command -v just >/dev/null && just --version || echo "just: MISSING" + echo "== workspace check ==" + cargo check --workspace + +# Annotated tour of the repository layout tour: - @echo "=== palimpsest-plasma Tour ===" - @echo "" - @echo "1. Project structure:" - @ls -la - @echo "" - @echo "2. Available commands: just --list" - @echo "" - @echo "3. Read README.adoc for full overview" - @echo "4. Read EXPLAINME.adoc for architecture decisions" - @echo "5. Run 'just doctor' to check your setup" - @echo "" - @echo "Tour complete! Try 'just --list' to see all available commands." + #!/usr/bin/env bash + echo "src/ — the plasma CLI (check/facts/policy/audit/init/badge/migrate)" + echo "plasma-engine/ — policy AST, schema, facts, pure evaluator, reports" + echo "plasma-parser/ — SPDX parsing, license families, zones, audit scan" + echo "docs/ — engine-v0-design.adoc is the normative spec" + echo "site/ — Elixir NimblePublisher guidance site" -# Open feedback channel with diagnostic context +# Pre-fill an issue report with local context help-me: - @echo "=== palimpsest-plasma Help ===" - @echo "Platform: $(uname -s) $(uname -m)" - @echo "Shell: $SHELL" - @echo "" - @echo "To report an issue:" - @echo " https://github.com/hyperpolymath/palimpsest-plasma/issues/new" - @echo "" - @echo "Include the output of 'just doctor' in your report." - - -# Print the current CRG grade (reads from READINESS.md '**Current Grade:** X' line) -crg-grade: - @grade=$$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ - [ -z "$$grade" ] && grade="X"; \ - echo "$$grade" - -# Generate a shields.io badge markdown for the current CRG grade -# Looks for '**Current Grade:** X' in READINESS.md; falls back to X -crg-badge: - @grade=$$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ - [ -z "$$grade" ] && grade="X"; \ - case "$$grade" in \ - A) color="brightgreen" ;; B) color="green" ;; C) color="yellow" ;; \ - D) color="orange" ;; E) color="red" ;; F) color="critical" ;; \ - *) color="lightgrey" ;; esac; \ - echo "[![CRG $$grade](https://img.shields.io/badge/CRG-$$grade-$$color?style=flat-square)](https://github.com/hyperpolymath/standards/tree/main/component-readiness-grades)" - -secret-scan-trufflehog: - @command -v trufflehog >/dev/null && trufflehog filesystem . --only-verified || true + #!/usr/bin/env bash + echo "## Environment" + echo "- plasma: {{version}}" + cargo --version 2>/dev/null || echo "- cargo: not found" + uname -a + +# Set up a development environment (fetch deps and build) +setup: + cargo fetch + cargo build --workspace + @echo "Setup complete. Try: just check" diff --git a/LICENSE-PMPL-2.0.txt b/LICENSE-PMPL-2.0.txt deleted file mode 100644 index 1458f1a..0000000 --- a/LICENSE-PMPL-2.0.txt +++ /dev/null @@ -1,149 +0,0 @@ -SPDX-License-Identifier: PPMPL-1.0-or-later -SPDX-FileCopyrightText: 2026 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 2.0 -================================================================================ - -File-level copyleft with strengthened attribution and provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for strengthened attribution and cryptographic provenance. The base PMPL-1.0-or-later -terms apply except where explicitly modified below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license ensures that each contributor's work remains visible and -attributable, even as the software evolves. - -This License imposes no restrictions on the field of endeavor, use, or -purpose of the Covered Software. It is intended to be compatible with the -Open Source Definition. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete PMPL-1.0-or-later text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from PMPL-1.0-or-later apply except where -explicitly modified below. - -Where this License and PMPL-1.0-or-later conflict, this License takes precedence. -Where this License is silent, PMPL-1.0-or-later applies. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Provenance Metadata" - means attribution information attached to or associated with Covered - Software, including but not limited to: author identities, timestamps, - modification history, SPDX headers, and cryptographic signatures. - -2.2. "Attribution Notice" - means any copyright notice, author name, SPDX-License-Identifier, - SPDX-FileCopyrightText, or Provenance Metadata present in or - associated with Covered Software. - --------------------------------------------------------------------------------- -SECTION 3: STRENGTHENED ATTRIBUTION --------------------------------------------------------------------------------- - -In addition to the notice requirements of PMPL-1.0-or-later Section 3.4: - -3.1. Attribution Preservation - You must not strip, alter, or obscure Attribution Notices from Covered - Software. This includes: - (a) copyright headers in source files; - (b) SPDX-License-Identifier and SPDX-FileCopyrightText lines; - (c) author information in package manifests (Cargo.toml, package.json, - mix.exs, or equivalent); and - (d) Provenance Metadata where present. - -3.2. Derivative Work Attribution - When creating a Larger Work that incorporates Covered Software, You - must include a notice in Your Larger Work that identifies the Covered - Software and its Contributors, in a location reasonably likely to be - seen by recipients (e.g., a NOTICE file, an "About" screen, or - documentation). - -3.3. Automated Processing - If You process Covered Software using automated tools (including but - not limited to AI training, content aggregation, or code generation), - You must preserve Attribution Notices in the processed output where - technically feasible, or provide attribution in accompanying - documentation. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE (OPTIONAL) --------------------------------------------------------------------------------- - -4.1. Cryptographic Signatures - Contributors may sign their Contributions using cryptographic - signatures. This License recognizes but does not require such - signatures. - -4.2. Signature Preservation - If cryptographic signatures are present in Covered Software, You must - preserve them in all distributions. You must not forge, alter, or - misrepresent the origin of signatures. - -4.3. Algorithm Recommendations - Contributors are encouraged (but not required) to use post-quantum - cryptographic algorithms for long-term provenance. Recommended - algorithms include ML-DSA (FIPS 204), SLH-DSA (FIPS 205), and - Ed25519 for current use. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Community Covenant - Projects using this License are encouraged (but not required) to adopt - the Palimpsest Community Covenant, which provides ethical guidelines - for responsible use. The Covenant is a separate document and is not - part of this License. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. PMPL-1.0-or-later Compatibility - Covered Software under this License may be combined with software - under PMPL-1.0-or-later. In such combinations, the PMPL-1.0-or-later terms apply to the - PMPL-1.0-or-later files, and this License applies to the PPMPL-1.0-or-later files. - -6.2. Secondary Licenses - The Secondary License provisions of PMPL-1.0-or-later Section 3.3 apply to - this License. The following licenses are designated as Secondary - Licenses: GNU General Public License v2.0 or later, GNU Lesser - General Public License v2.1 or later, GNU Affero General Public - License v3.0 or later. - -6.3. Relicensing - You may not relicense Covered Software under a license that does not - preserve the attribution requirements of Section 3. - -================================================================================ -END OF PALIMPSEST-MPL LICENSE VERSION 2.0 -================================================================================ - -For the Community Covenant: PALIMPSEST-COVENANT.md -For provenance tooling: https://github.com/hyperpolymath/palimpsest-plasma -For questions: https://github.com/hyperpolymath/palimpsest-license diff --git a/LICENSE.sig b/LICENSE.sig deleted file mode 100644 index fc5d6f3..0000000 --- a/LICENSE.sig +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN PALIMPSEST SIGNATURE----- -Version: PMPL-SIG/1.0 -Algorithm: ML-DSA-65 -Signer: palimpsest-council -Timestamp: 2026-01-19T12:47:44Z -Content-Hash: 802c28528ff9a857f2bc3efccff66d65eba9b6576893b8664373c0521660f89b - -[PLACEHOLDER: Actual ML-DSA-65 signature would go here] -[Requires: pqcrypto or oqs library integration] ------END PALIMPSEST SIGNATURE----- diff --git a/LICENSES/CC-BY-SA-4.0.txt b/LICENSES/CC-BY-SA-4.0.txt new file mode 100644 index 0000000..2d58298 --- /dev/null +++ b/LICENSES/CC-BY-SA-4.0.txt @@ -0,0 +1,428 @@ +Attribution-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-ShareAlike 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-ShareAlike 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + l. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + m. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + including for purposes of Section 3(b); and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/LICENSES/PMPL-1.0-or-later.txt b/LICENSES/PMPL-1.0-or-later.txt deleted file mode 100644 index 788fdb2..0000000 --- a/LICENSES/PMPL-1.0-or-later.txt +++ /dev/null @@ -1,162 +0,0 @@ -SPDX-License-Identifier: PMPL-1.0-or-later -SPDX-FileCopyrightText: 2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete PMPL-1.0-or-later text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from PMPL-1.0-or-later apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under PMPL-1.0-or-later: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. PMPL-1.0-or-later Compatibility - Covered Software under this License may be combined with software under - PMPL-1.0-or-later. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of PMPL-1.0-or-later Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL-1.0 LICENSE TEXT --------------------------------------------------------------------------------- - -For exhibits, specifications, provenance rules, and governance: -https://github.com/hyperpolymath/palimpsest-license - -For legal frameworks and jurisdictional analysis: -See /legal/frameworks/ - -For provenance and audit tooling: -See /tools/ and /spec/PROVENANCE-SPEC.adoc - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues diff --git a/NOTICE b/NOTICE index acd2fb8..f85fbf3 100644 --- a/NOTICE +++ b/NOTICE @@ -1,22 +1,18 @@ Licensing Notice ================ -This project is authored by Jonathan D.A. Jewell (hyperpolymath) and -is licensed under the Palimpsest License (PMPL-1.0-or-later). +This project is authored by Jonathan D.A. Jewell (hyperpolymath). -The PMPL-1.0-or-later is a philosophical extension of the Mozilla Public -License 2.0, adding provisions for cryptographic provenance, emotional -lineage preservation, and quantum-safe signatures. The full PMPL text is -available in LICENSES/PMPL-1.0-or-later.txt. +Licensing: + - Source code and configuration: Mozilla Public License 2.0 (MPL-2.0). + The full text is in LICENSE and LICENSES/MPL-2.0.txt. + - Documentation (prose in *.adoc and *.md files): Creative Commons + Attribution-ShareAlike 4.0 International (CC-BY-SA-4.0). + The full text is in LICENSES/CC-BY-SA-4.0.txt. -For compatibility with automated license detection tools and platforms -that require OSI-approved licenses, the root LICENSE file contains the -standard Mozilla Public License 2.0 text. This ensures that package -registries, CI systems, and other tooling correctly identify the license. +Each file declares its license in an SPDX-License-Identifier header. -The legally binding terms are: - - Source files: governed by PMPL-1.0-or-later (per SPDX headers) - - Combined works: compatible with PMPL-1.0-or-later (per PMPL Section 6) - -For more information about the Palimpsest License: - https://github.com/hyperpolymath/palimpsest-license +The plasma-parser library recognises Palimpsest-family license +identifiers (PMPL, PAGPL, and related) as parseable SPDX extensions. +That recognition is a parser feature; it does not license this project +under those terms. The Palimpsest license itself is a separate project. diff --git a/PALIMPSEST-COVENANT.md b/PALIMPSEST-COVENANT.md deleted file mode 100644 index b46c553..0000000 --- a/PALIMPSEST-COVENANT.md +++ /dev/null @@ -1,91 +0,0 @@ - -# Palimpsest Community Covenant v1.0 - -## Purpose - -This Covenant articulates the values and expectations of the Palimpsest -community. It is not a license — it carries no legal obligations. It is a -statement of shared norms that projects adopting the Palimpsest-MPL License -are encouraged to follow. - -Like the Contributor Covenant for codes of conduct, this document exists -separately from the license to provide ethical guidance without compromising -legal clarity. - -## Principles - -### 1. Attribution is respect - -Every line of code carries the effort of someone who wrote it. We preserve -attribution not because a license requires it, but because erasing someone's -contribution erases their work. Credit where credit is due, always. - -### 2. Context matters - -Software carries meaning beyond its function. A tool built for protest has -different weight than one built for commerce. We encourage preserving the -narrative context of creative works — the why, not just the what. - -### 3. Transparency with automated systems - -When code is processed by AI training pipelines, content aggregators, or -automated transformation tools, we expect transparency about that use. Not -as a restriction, but as a norm: if you use someone's work to train a model, -say so. - -### 4. Provenance is permanence - -We encourage contributors to sign their work cryptographically. Not because -we distrust each other, but because timestamped, signed attribution creates -a permanent record that no one can dispute. Your work is yours, provably. - -### 5. Inclusivity in practice - -We welcome contributions from all people regardless of background, identity, -experience level, or geography. We recognise that the global open source -community includes people for whom English is not a first language, who work -in different legal jurisdictions, and who bring different cultural contexts -to their work. - -### 6. Sustainability - -We encourage projects to consider the environmental and social sustainability -of their software. This includes energy efficiency, accessibility, and -long-term maintainability. - -## Adoption - -To adopt this Covenant, add the following to your project's README: - -```markdown -This project follows the [Palimpsest Community Covenant](https://github.com/hyperpolymath/palimpsest-license/blob/main/PALIMPSEST-COVENANT.md). -``` - -## Enforcement - -This Covenant is enforced socially, not legally. Projects may choose to -reference it in their CONTRIBUTING guidelines, codes of conduct, or -community documentation. Violations are addressed through community -discussion, not litigation. - -## Versioning - -This Covenant is versioned independently of the Palimpsest-MPL License. -The current version is 1.0. Future versions will be published by the -Palimpsest Stewardship Council. - -## Acknowledgements - -This Covenant was inspired by the Contributor Covenant, the Hippocratic -License discussion, and the broader conversation about ethical open source. -It takes the position that values belong in community norms, not in license -terms — preserving both legal clarity and moral purpose. - ---- - -*Palimpsest Community Covenant v1.0* -*Published by the Palimpsest Stewardship Council, 2026* -*https://github.com/hyperpolymath/palimpsest-license* diff --git a/QUICKSTART-MAINTAINER.adoc b/QUICKSTART-MAINTAINER.adoc index c378f78..a86b9dc 100644 --- a/QUICKSTART-MAINTAINER.adoc +++ b/QUICKSTART-MAINTAINER.adoc @@ -107,7 +107,7 @@ Or via OPSM: `opsm update {{PACKAGE_NAME}}` == Security Notes -* License: PMPL-1.0-or-later (Palimpsest License) +* License: MPL-2.0 (code), CC-BY-SA-4.0 (documentation) * All dependencies SHA-pinned * `panic-attacker` scan results: link:INSTALL-SECURITY-REPORT.adoc[] * OpenSSF Scorecard: see badge in README diff --git a/README.adoc b/README.adoc index 6ae1f5c..c249613 100644 --- a/README.adoc +++ b/README.adoc @@ -1,114 +1,139 @@ -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell = Palimpsest Plasma -image:https://img.shields.io/badge/OpenSSF-Best_Practices-green?logo=openssourcesecurity[OpenSSF Best Practices,link="https://www.bestpractices.dev/en/projects/new?repo_url=https://github.com/hyperpolymath/palimpsest-plasma"] - :toc: :toc-placement!: -image:assets/badges/svg/badge-standard.svg[Palimpsest License v1.0] - toc::[] == Overview -**palimpsest-plasma** is the official governance automation engine for the -Palimpsest-MPL ecosystem. It is an OCaml-based system that interprets the -licence, exhibits, and governance rules as typed, machine-readable policies, and -evaluates real-world repository states against those obligations. - -PLASMA stands for the operational, programmable substrate that keeps the -Palimpsest-MPL ecosystem correct, compliant, and narratively coherent over time. - -== Core Responsibilities - -=== Compliance & Verification - -* Validate SPDX headers, provenance manifests, exhibit references, governance - metadata, and release requirements -* Detect violations, omissions, inconsistencies, and structural drift -* Produce actionable findings for maintainers and auditors - -=== Corrective, Adaptive, and Perfective Maintenance - -* Suggest or apply fixes (missing headers, outdated links, incorrect exhibit - references) -* Adapt to structural changes in the repository -* Improve documentation, governance artefacts, and provenance flows - -=== Governance Runtime - -* Enforce Council decisions -* Track exhibit lifecycle changes -* Validate versioning rules -* Ensure governance processes are followed - -=== Policy Engine - -* Parse and evaluate obligations, prohibitions, permissions, and conditions -* Support versioned policy schemas with migrations -* Allow exhibits to extend or modify rules - -=== Audit & Reporting - -* Generate structured audit logs -* Produce compliance reports -* Support legal, governance, and provenance reviews - -=== Integration - -* CLI tool for local checks -* Git hooks for pre-commit/pre-push enforcement -* GitHub Actions for CI enforcement -* Optional daemon/cron mode for periodic audits +**palimpsest-plasma** is a deterministic, typed policy engine for the agentic +era. You write policies as machine-readable, deontic rules — obligations, +prohibitions, permissions over repository state — and `plasma check` evaluates +a repository against them *exactly*: same policy, same repository, same +findings, byte for byte, every time. + +That precision is the point. When neural/LLM tooling operates on a codebase, +something deterministic has to define what "correct" means and verify it +afterwards. Plasma is that something. + +== What it is + +* A **policy engine**: typed rules in TOML or JSON, evaluated by a pure, + total function over a collected fact snapshot. No clocks, no randomness, + no ambient state. See link:docs/engine-v0-design.adoc[engine-v0-design.adoc] + for the normative semantics. +* A **license/SPDX toolkit**: `plasma audit` checks SPDX headers against a + zone-aware license map (`.plasma.toml`), powered by a real SPDX expression + parser; `plasma init`, `migrate`, and `badge` handle license adoption for + any SPDX license. +* A **fact source for agent verification**: `plasma facts` emits the exact + JSON snapshot the evaluator sees; `plasma check --format sarif` emits + SARIF 2.1.0 with stable `plasma/` rule identifiers. + +== What it is not + +* **Not the PMPL governance engine.** The Palimpsest license (PMPL) is a + separate, future project. This repo's parser *recognises* Palimpsest-family + SPDX identifiers, but the project itself is licensed plainly: code under + MPL-2.0, documentation under CC-BY-SA-4.0. +* **Not an OCaml system.** Earlier documentation described an OCaml core that + was never built. The engine is Rust, implementing the design in + link:docs/policy-ast-v0.1.adoc[policy-ast-v0.1.adoc], and its policy format + is specified so a future formal core (OCaml/Catala) can generate or verify + the same policies. + +== Sibling projects + +Plasma is the policy-definition and compliance-evaluation member of a +planned trio for keeping automated agents honest: + +* **somethings-fishy** — forensic investigation of bot-inflicted damage +* **did-you-actually-do-that** — verification that agents did what they claimed + +The integration contract is plasma's deterministic output: JSON findings, +SARIF results, and `plasma facts` snapshots (diffable before/after an agent +run). + +== Quickstart + +[source,bash] +---- +cargo build --release +./target/release/plasma check . # evaluate against the bundled repo-hygiene policy +./target/release/plasma check . --format json +./target/release/plasma facts . # the fact snapshot the evaluator sees +./target/release/plasma policy validate plasma-engine/policies/repo-hygiene.plasma.toml +./target/release/plasma audit . # SPDX header audit (MPL-2.0 default) +---- + +`plasma check` exits 0 when no violation at or above the severity threshold +exists (default: `error`), 1 otherwise — ready for CI and git hooks. + +== Writing a policy + +[source,toml] +---- +schema_version = { major = 0, minor = 1 } +id = "my-policy" +version = { major = 1, minor = 0 } + +[[rules]] +id = "license-file-present" +modality = "obligation" +severity = "error" +subject = { type = "repo" } +resource = { type = "file", path = "LICENSE*" } +action = { type = "present" } +rationale = "Every repository must carry its license text." +---- + +Rules bind a deontic modality (`obligation`, `prohibition`, `permission`) to +a subject, resource, optional condition, and action. Subjects expand over the +repository (`file-pattern` matches globs); conditions compose with +`all`/`any`/`not`. The full vocabulary and its exact semantics are in +link:docs/engine-v0-design.adoc[engine-v0-design.adoc]. + +== Workspace layout + +[cols="1,3"] +|=== +| Crate | Purpose + +| `plasma-parser` +| SPDX expression lexer/parser (including Palimpsest-family identifiers), + license family types, zone auditing (`.plasma.toml`), compatibility matrix, + JSON/SARIF reports. + +| `plasma-engine` +| Policy AST, versioned schema loading, fact collection, the pure evaluator, + and finding/report rendering. Depends on plasma-parser. + +| root (`plasma` binary) +| The CLI: `check`, `facts`, `policy`, `audit`, `init`, `badge`, `migrate`. +|=== == Architecture -OCaml core for stability, correctness, and long-term maintainability. The engine -is structured in three layers: - -* **core/** -- Canonical PLASMA substrate: Policy AST, schemas, evaluation, - migrations, facts, findings, actions, governance runtime -* **integration/** -- Surfaces: CLI, git hooks, CI, daemon -* **adapters/** -- IO and environment-specific collectors/parsers: SPDX, exhibits, - repo topology - -See link:docs/architecture.adoc[docs/architecture.adoc] for the full design, -link:docs/policy-ast-v0.1.adoc[docs/policy-ast-v0.1.adoc] for the typed AST -specification, and link:TOPOLOGY.md[TOPOLOGY.md] for a visual architecture map. - -== Sub-Projects - -=== union-policy-parser +See link:docs/architecture.adoc[docs/architecture.adoc] for the engine design, +link:docs/policy-ast-v0.1.adoc[docs/policy-ast-v0.1.adoc] for the original +typed-AST design, link:docs/engine-v0-design.adoc[docs/engine-v0-design.adoc] +for the normative v0 semantics and Catala-readiness guarantees, and +link:TOPOLOGY.md[TOPOLOGY.md] for the project map and honest completion +dashboard. -A Rust-based policy validation engine for union contracts (NUJ, IWW, UCU) using -A2ML (Attested Markup Language). Parses and validates employment contracts -against union standards with formal proofs and grievance generation. See -`union-policy-parser/` for full details. +== Static site -== MVP v1 Bundle - -The **MVP v1** bundle provides a deployable PMPL-1.0 experience: - -* **Canonical license tooling** -- `LICENSE` text, ECOSYSTEM mapping, and badges - aligned with the Palimpsest Stewardship Council’s PMPL-1.0 release -* **Badge & documentation assets** -- `assets/badges/` pack and `docs/mvp-v1.adoc` -* **Compliance readiness** -- SPDX metadata, automated checks, audit commands -* **Audit/playback support** -- `pmpl-verify`/`pmpl-audit` provenance verification - -See `docs/mvp-v1.adoc` for the full deliverables checklist and compliance commands. - -== Static Site - -Compliance guidance builds through a local Elixir mix project at `site/`. Run -`mix site.build` from that folder to regenerate `site/_site`. The GitHub Actions -workflow in `.github/workflows/nimble-site.yml` deploys the generated pages. +Compliance guidance builds through a local Elixir mix project at `site/`. +Run `mix site.build` from that folder to regenerate `site/_site`. The GitHub +Actions workflow in `.github/workflows/nimble-site.yml` deploys the pages. == License -This project is licensed under the Mozilla Public License, v. 2.0. See the `LICENSE` file for details. - -SPDX-License-Identifier: MPL-2.0 +Code is licensed under the Mozilla Public License 2.0 (see `LICENSE`). +Documentation is licensed under CC-BY-SA-4.0 (see `LICENSES/CC-BY-SA-4.0.txt`). +Every file carries an SPDX header; `NOTICE` has the details. == Author diff --git a/ROADMAP.adoc b/ROADMAP.adoc index 580f47c..c31f85e 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell = Palimpsest Plasma — Roadmap :toc: @@ -9,102 +9,56 @@ toc::[] == Current Status -The MVP v1 bundle (badges, compliance docs, SPDX metadata, audit commands) is -**complete**. The union-policy-parser sub-project (Rust/A2ML) is functional. -The core PLASMA engine (OCaml) is now in the design-to-implementation phase. +**v0.2.0**: the repository compiles, all tests pass, and engine v0 is real — +a typed policy AST, versioned schema loading, deterministic fact collection, +a pure/total evaluator, and a CLI (`plasma check/facts/policy/audit`) that +this repository applies to itself in CI via its bundled repo-hygiene policy. -== Completed Milestones - -=== v1.0.0 — MVP v1 Bundle [COMPLETE] - -* [x] Finalise PMPL-1.0 licence text and README -* [x] Ship badge + embed documentation from docs/mvp-v1.adoc -* [x] Update ECOSYSTEM/STATE metadata and release log +The project's identity settled here too: plasma is a deterministic policy +engine for the agentic era. The Palimpsest license (PMPL) is a separate +future project; the earlier union-policy-parser sub-project was removed +(it lives on in git history and belongs in its own repository). -=== v1.1.0 — Metadata & Discoverability [COMPLETE] - -* [x] Add CITATION.cff for academic citation support -* [x] Add codemeta.json for software metadata interoperability -* [x] Add .well-known/security.txt and .well-known/ai.txt -* [x] Update STATE.scm to reflect additions +== Completed Milestones -=== union-policy-parser v0.1 [COMPLETE] +=== v0.2.0 — Truth + Green + Engine v0 [COMPLETE] -* [x] A2ML parser (nom-based, Rust) -* [x] Union schema definitions (NUJ, IWW, UCU, BECTU, Equity, GMB) -* [x] Contract validator with three modes (Lax, Checked, Attested) -* [x] Grievance generator (JSON, Markdown, HTML output) -* [x] Example contracts (good and bad) -* [x] Idris2 ABI definitions and Zig FFI layer +* [x] Repository compiles; full test suite green +* [x] License unification: code MPL-2.0, docs CC-BY-SA-4.0, SPDX headers swept +* [x] plasma-engine crate: Policy AST, schema load/validate, facts, evaluator +* [x] Deontic semantics (obligation/prohibition/permission) with severity +* [x] Bundled repo-hygiene policy; self-test gate in CI +* [x] `plasma check` (human/JSON/SARIF), `plasma facts`, `plasma policy validate` +* [x] `plasma audit` rewired onto plasma-parser (zone-aware, real SPDX parsing) +* [x] init/migrate/badge genericized to any SPDX license +* [x] Docs truth pass: README, EXPLAINME, TOPOLOGY, architecture, engine-v0-design +* [x] union-policy-parser removed; corrupted license artifacts removed == Active Milestones -=== v2.0.0 — PLASMA Engine Foundation - -* [ ] OCaml project layout (dune-project, opam, src/core) -* [ ] Policy AST v0.1 implementation -* [ ] Schema versioning and migration framework -* [ ] Mock migration tests -* [ ] Fact collection types and adapters -* [ ] Finding types with severity and kind - -=== v2.1.0 — Evaluation Engine - -* [ ] Condition compiler (AST conditions -> fact predicates) -* [ ] Rule evaluator (modality + action -> findings) -* [ ] Policy evaluator (iterates rules, collects findings) -* [ ] Integration tests with example policies and repos - -=== v2.2.0 — Action Planner - -* [ ] Action types (AddSpdxHeader, UpdateExhibitReference, etc.) -* [ ] Suggest vs Apply modes -* [ ] Planner: findings -> planned actions -* [ ] Backup and rollback support - -=== v2.3.0 — CLI - -* [ ] `plasma check` — validate repo against policy -* [ ] `plasma fix` — plan or apply corrective actions -* [ ] `plasma audit` — generate structured audit logs -* [ ] `plasma governance` — Council decision integration -* [ ] `plasma migrate` — policy schema migration -* [ ] Human and JSON output formats - -== Planned Milestones - -=== v2.4.0 — Integration Surfaces - -* [ ] Git hooks (pre-commit, pre-push) -* [ ] GitHub Actions reference workflow -* [ ] `.plasma.toml` configuration -* [ ] Daemon/cron mode for periodic audits - -=== v2.5.0 — Governance Runtime - -* [ ] Council decision enforcement -* [ ] Exhibit lifecycle tracking (active, superseded, deprecated) -* [ ] Versioning rule validation -* [ ] Policy overlay composition - -=== v3.0.0 — Contractiles +=== v0.3.0 — Action Planner -* [ ] Contractile type definitions -* [ ] Reversible commitment semantics -* [ ] State-space narrowing evaluation -* [ ] Integration with action planner -* [ ] Constraint propagation across rules +* [ ] Action types (AddSpdxHeader, CreateFile, UpdateField, …) +* [ ] `plasma fix` — plan (dry-run) and apply modes, with backups +* [ ] Findings → planned actions mapping +* [ ] Overlay `modify-rules` / `override-rules` semantics +* [ ] Content facts + `file-matches-pattern` condition semantics -=== v3.1.0 — PLASMA + union-policy-parser Integration +=== v0.4.0 — Integration Surfaces -* [ ] A2ML documents as fact sources for PLASMA -* [ ] Shared exhibit and attestation model -* [ ] Combined compliance reporting +* [ ] Git hooks (pre-commit, pre-push) via `plasma check --quiet` +* [ ] Reference GitHub Action / GitLab CI job +* [ ] `.plasma.toml` policy references (project-pinned policies) +* [ ] Sibling-tool contracts: stable facts-diff format for + did-you-actually-do-that, finding-provenance for somethings-fishy +* [ ] Release facts (`release` subjects/resources gain semantics) == Future Directions -* Formal verification of policy evaluation (Coq or Lean proofs) +* Formal core: Catala/OCaml implementation of the evaluation semantics, + verified against docs/engine-v0-design.adoc (the format is specified for + exactly this) * WASM compilation target for browser-based policy checks -* Visual policy editor -* Multi-repo fleet evaluation (gitbot-fleet integration) -* AI-assisted policy authoring +* Decision registries (`consistent-with` action semantics) +* Multi-repo fleet evaluation +* Policy authoring assistance (schema-aware, never semantics-defining) diff --git a/RSR_COMPLIANCE.adoc b/RSR_COMPLIANCE.adoc index 5468f59..08909e3 100644 --- a/RSR_COMPLIANCE.adoc +++ b/RSR_COMPLIANCE.adoc @@ -15,12 +15,12 @@ This document records Rhodium Standard Repository (RSR) compliance status for *p |Attribute |Value |Project |palimpsest-plasma -|License baseline |PMPL-1.0-or-later (legal primary) -|Policy overlay |Palimpsest principles overlay -|Primary Language |Rust + OCaml +|License baseline |MPL-2.0 (code), CC-BY-SA-4.0 (docs) +|Policy overlay |None (PMPL is a separate future project) +|Primary Language |Rust |RSR Tier |Tier 1 |Compliance Status |Compliant (baseline) -|Last Updated |2026-04-17 +|Last Updated |2026-07-02 |=== == Compliance Checklist @@ -33,7 +33,7 @@ This document records Rhodium Standard Repository (RSR) compliance status for *p |Machine-readable metadata |✓ |`.machine_readable/` |Contractiles present |✓ |`contractiles/` + generated `contractile.just` |Well-known metadata |✓ |`.well-known/humans.txt` -|Project licensing |✓ |PMPL-1.0-or-later baseline license with Palimpsest overlay policy +|Project licensing |✓ |MPL-2.0 code, CC-BY-SA-4.0 docs, SPDX headers throughout |Task runner |✓ |`Justfile` |=== diff --git a/STATE.scm.sig b/STATE.scm.sig deleted file mode 100644 index dadc7cb..0000000 --- a/STATE.scm.sig +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN PALIMPSEST SIGNATURE----- -Version: PMPL-SIG/1.0 -Algorithm: ML-DSA-65 -Signer: palimpsest-council -Timestamp: 2026-01-19T12:47:44Z -Content-Hash: bbd26b2836ca756a86a6b459c490596450c50dbc653903bbd00e2688e063d0c6 - -[PLACEHOLDER: Actual ML-DSA-65 signature would go here] -[Requires: pqcrypto or oqs library integration] ------END PALIMPSEST SIGNATURE----- diff --git a/TEST-NEEDS.md b/TEST-NEEDS.md index dbaa4c4..f719463 100644 --- a/TEST-NEEDS.md +++ b/TEST-NEEDS.md @@ -1,65 +1,59 @@ # TEST-NEEDS.md — palimpsest-plasma -## CRG Grade: C — ACHIEVED 2026-04-04 -> Generated 2026-03-29 by punishing audit. +> Updated 2026-07-02 (v0.2.0). The 2026-03-29 audit ("zero unit tests") +> described the pre-engine repo and is superseded. ## Current State | Category | Count | Notes | -|-------------|-------|-------| -| Unit tests | 0 | No inline `#[test]` in any Rust source | -| Integration | 1 | union-policy-parser/ffi/zig/test/integration_test.zig | -| E2E | 0 | None | -| Benchmarks | 0 | None | - -**Source modules:** ~35 Rust files across: main.rs, init.rs, audit.rs, badge.rs, migrate.rs + union-policy-parser (Idris2 ABI + Zig FFI). Also: 3 Idris2 ABI, 2 Elixir, 2 Julia scripts. +|--------------|-------|-------| +| Unit tests | 61 | Inline `#[test]` across plasma-parser (42) and plasma-engine (19) | +| Integration | 4 | `tests/check_selftest.rs` — the repo checks itself with its own bundled policy | +| E2E | 1 | `check_self_passes` runs the built binary end-to-end | +| Determinism | 2 | Byte-identical JSON asserted at unit and binary level | +| Benchmarks | 0 | None yet | + +Run everything with `cargo test --workspace`. + +## What's Covered + +- SPDX expression parsing (lexer, parser, catalog incl. Palimpsest-family ids) +- Zone assignment and `.plasma.toml` parsing +- Repository scanning (`scan_repo`): counts, score, deterministic ordering +- Policy schema loading: version gating, duplicate rule ids, reserved-construct + rejection, TOML↔JSON round-trip +- Evaluation semantics: deontic matrix rows, condition gating, subject + expansion, overlay provenance, permission never violating +- SARIF shape: rule namespace, levels, location omission for repo subjects +- Fact collection: skip list, header extraction, Cargo version metadata ## What's Missing -### P2P (Property-Based) Tests -- [ ] Badge generation: property tests for SVG/image output validity -- [ ] Audit: property tests for license detection accuracy -- [ ] Migration: property tests for license text transformation correctness -- [ ] Union policy parser: arbitrary policy document fuzzing +### Property-Based Tests +- [ ] Arbitrary policy documents: load → serialize → reload identity +- [ ] Arbitrary fact sets: evaluation totality (no panics) under fuzzing +- [ ] SPDX parser: arbitrary expression round-trips ### E2E Tests -- [ ] Full audit: scan project -> detect licenses -> generate report -- [ ] Full migration: detect old license -> generate new -> apply -> verify -- [ ] Badge: generate badge for all license types -- [ ] Init: new project initialization with license setup +- [ ] `plasma init`/`migrate` against fixture repos (tempdir round-trips) +- [ ] `plasma audit --fix` idempotence (fixing twice changes nothing) +- [ ] `plasma check --policy ` (JSON policy path is unit-tested only) ### Aspect Tests -- **Security:** No tests for license text injection, badge SVG injection, path traversal in audit scanning -- **Performance:** No scanning speed benchmarks -- **Concurrency:** No tests for parallel file scanning -- **Error handling:** No tests for malformed license files, unsupported formats, corrupted badge templates - -### Build & Execution -- [ ] `cargo test` -- [ ] Zig FFI test execution -- [ ] CLI smoke tests - -### Benchmarks Needed -- [ ] License detection speed per file -- [ ] Full repository audit time vs repo size -- [ ] Badge generation time -- [ ] Migration transformation throughput - -### Self-Tests -- [ ] Audit its own license compliance -- [ ] Badge generation for its own license -- [ ] ABI version agreement - -## Priority - -**CRITICAL.** 35 Rust source files with ZERO unit tests. A license compliance tool that has never been unit-tested. The single Zig FFI integration test only covers the union-policy-parser seam. The 5 main Rust modules (main, init, audit, badge, migrate) are completely untested. - -## FAKE-FUZZ ALERT - -- `tests/fuzz/placeholder.txt` is a scorecard placeholder inherited from rsr-template-repo — it does NOT provide real fuzz testing -- Replace with an actual fuzz harness (see rsr-template-repo/tests/fuzz/README.adoc) or remove the file +- **Security:** path traversal attempts in audit/fact walking; SVG/badge + output injection +- **Performance:** scan/evaluate time vs repository size benchmarks +- **Error handling:** malformed policy files produce the intended + `SchemaError` variants (partially covered) + +## FAKE-FUZZ ALERT (still open) + +- `tests/fuzz/placeholder.txt` is a scorecard placeholder inherited from + rsr-template-repo — it does NOT provide real fuzz testing +- Replace with an actual fuzz harness (`cargo-fuzz` over `load_policy_str` + and `parse_spdx_expr` are the natural first targets) or remove the file - Priority: P2 — creates false impression of fuzz coverage diff --git a/TOPOLOGY.md b/TOPOLOGY.md index abc14f9..b50acf7 100644 --- a/TOPOLOGY.md +++ b/TOPOLOGY.md @@ -1,46 +1,44 @@ - + # Palimpsest Plasma — Project Topology ## System Architecture ``` - ┌──────────────────────────────────────────────────────────┐ - │ PLASMA ENGINE │ - │ │ - │ ┌────────────────────────────────────────────────────┐ │ - │ │ CORE (OCaml) │ │ - │ │ │ │ - │ │ Policy AST ──► Eval Engine ──► Action Planner │ │ - │ │ │ │ │ │ │ - │ │ Schema & Findings & Suggest / │ │ - │ │ Migration Severity Apply │ │ - │ │ │ │ │ │ │ - │ │ Governance ◄── Contractiles ──► Audit Logs │ │ - │ │ Runtime │ │ - │ └────────────────────────────────────────────────────┘ │ - │ │ │ - │ ┌───────────────┐ ┌────┴──────┐ ┌──────────────┐ │ - │ │ ADAPTERS │ │INTEGRATION│ │ UNION POLICY │ │ - │ │ │ │ │ │ PARSER │ │ - │ │ Repo Scanner │ │ CLI │ │ │ │ - │ │ SPDX Parser │ │ Git Hooks │ │ A2ML Parser │ │ - │ │ Exhibit Parse │ │ CI/CD │ │ Validator │ │ - │ │ Metadata I/O │ │ Daemon │ │ Grievance Gen│ │ - │ └───────────────┘ └───────────┘ └──────────────┘ │ - └──────────────────────────────────────────────────────────┘ - │ - ┌──────────────────────┼──────────────────────────────────┐ - │ PALIMPSEST-MPL ECOSYSTEM │ - │ │ │ - │ palimpsest-license palimpsest-governance │ - │ (PMPL-1.0 text) (Council processes) │ - └──────────────────────────────────────────────────────────┘ + ┌─────────────────────────────────────────────────────┐ + │ plasma (CLI) │ + │ check · facts · policy · audit · init · badge · │ + │ migrate │ + └────────────┬───────────────────────┬────────────────┘ + │ │ + ┌────────────▼────────────┐ ┌────────▼────────────────┐ + │ plasma-engine │ │ plasma-parser │ + │ │ │ │ + │ Policy AST (deontic) │ │ SPDX lexer + parser │ + │ Schema load/validate │ │ Identifier catalog │ + │ Fact collection (IO) │ │ License family types │ + │ Pure evaluator │ │ Zones (.plasma.toml) │ + │ Findings → human/ │ │ Repo scanner │ + │ JSON/SARIF │ │ Compat matrix │ + └────────────┬────────────┘ └─────────────────────────┘ + │ (engine depends on parser) + ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ AGENT-HONESTY TRIO (planned siblings) │ + │ │ + │ palimpsest-plasma somethings-fishy did-you-actually-│ + │ (policy definition + (forensic bot- do-that (claim │ + │ deterministic damage verification) │ + │ evaluation) investigation) │ + │ │ + │ Contract: JSON findings · SARIF plasma/ · │ + │ plasma facts snapshots (diffable) │ + └──────────────────────────────────────────────────────────────┘ ``` ## Completion Dashboard @@ -48,55 +46,48 @@ Copyright (c) Jonathan D.A. Jewell ``` COMPONENT STATUS NOTES ───────────────────────────────── ────────────────── ───────────────────────────────── -MVP v1 BUNDLE (COMPLETE) - Canonical License Tooling ██████████ 100% PMPL-1.0 alignment stable - Badge & Documentation Assets ██████████ 100% v1.0 brand pack shipped - Compliance Readiness ██████████ 100% SPDX metadata verified - Audit/Playback Support ████████░░ 80% pmpl-verify examples active - Citation & Metadata ██████████ 100% CITATION.cff + codemeta.json - -UNION POLICY PARSER (Rust/A2ML) - A2ML Parser ██████████ 100% nom-based, full test suite - Union Schemas (NUJ/IWW/UCU/+3) ██████████ 100% 7 schemas with attestations - Contract Validator ██████████ 100% 3 modes: Lax/Checked/Attested - Grievance Generator ██████████ 100% JSON/Markdown/HTML output - Idris2 ABI + Zig FFI ████████░░ 80% Template stubs, needs customisation - -PLASMA ENGINE (OCaml — IN DESIGN) - Policy AST v0.1 ██░░░░░░░░ 20% Types specified, not implemented - Schema Migration Framework ░░░░░░░░░░ 0% Design complete - Evaluation Engine ░░░░░░░░░░ 0% Design complete - Action Planner ░░░░░░░░░░ 0% Design complete - Governance Runtime ░░░░░░░░░░ 0% Design complete - Contractiles █░░░░░░░░░ 10% Concept documented - -INTEGRATION SURFACES - CLI (plasma check/fix/audit) ░░░░░░░░░░ 0% Design complete (cli-design.adoc) - Git Hooks ░░░░░░░░░░ 0% Planned - GitHub Actions ░░░░░░░░░░ 0% Planned - Daemon/Cron Mode ░░░░░░░░░░ 0% Planned - -STATIC SITE & INFRASTRUCTURE - Elixir Site (NimblePublisher) ██████████ 100% mix site.build verified - Justfile Automation ██████████ 100% Standard build tasks - .machine_readable/ ██████████ 100% STATE tracking active - Multi-Forge Synchronization ██████████ 100% GH/GL sync stable +PLASMA-PARSER (Rust) + SPDX Lexer/Parser/Catalog █████████░ 90% 42 unit tests; PMPL/PPMPL drift fixed + License Family Types ████████░░ 80% Registry + fallback chains + Zone System (.plasma.toml) ████████░░ 80% Boundary parsing, overlap detection + Repo Scanner (scan_repo) ████████░░ 80% Deterministic walk, zone-aware + Compat Matrix ███████░░░ 70% Pairwise checks; no report surface yet + JSON/SARIF Reports ████████░░ 80% RepoAudit renderers + +PLASMA-ENGINE (Rust) + Policy AST v0.1 █████████░ 90% Typed, serde TOML/JSON + Schema Load + Validation █████████░ 90% Load-time rejection of reserved constructs + Fact Collection ████████░░ 80% Files, headers, Cargo version, git HEAD + Pure Evaluator █████████░ 90% Total; deterministic; 19 unit tests + Human/SARIF Rendering ████████░░ 80% Stable plasma/ namespace + Overlays (add-rules) ████████░░ 80% modify/override reserved for v0.3 + +CLI (plasma) + check / facts / policy validate █████████░ 90% Self-tested against this repo + audit (parser-backed) ████████░░ 80% Zone-aware; --fix + init / migrate / badge ████████░░ 80% License-agnostic, MPL-2.0 default + fix (action planner) ░░░░░░░░░░ 0% Roadmap v0.3 + Git hooks / CI action / daemon ░░░░░░░░░░ 0% Roadmap v0.4 + +INFRASTRUCTURE + Elixir Site (NimblePublisher) ███████░░░ 70% Render helpers restored; CI-verified + Justfile (cargo) █████████░ 90% Rewritten from stale OCaml recipes + .machine_readable/ ███████░░░ 70% STATE/ECOSYSTEM truth-updated + CI (GitHub reusables + GitLab) ██████░░░░ 60% Reusables externally pinned/unverifiable ───────────────────────────────────────────────────────────────────────────── -OVERALL: ████░░░░░░ 40% MVP done; engine in design phase +OVERALL: █████░░░░░ ~50% Engine v0 shipped; planner + surfaces ahead ``` ## Key Dependencies ``` -Policy AST ───► Eval Engine ────► Action Planner ──► CLI / Hooks / CI - │ │ │ - ▼ ▼ ▼ -Migration ───► Fact Adapters ──► Governance Runtime ──► Audit Logs - │ │ │ - ▼ ▼ ▼ -Contractiles Union Policy Exhibit Lifecycle - Parser (A2ML) (Council Decisions) +Policy AST ──► Schema Loader ──► Pure Evaluator ──► Findings ──► human/JSON/SARIF + ▲ +Fact Collector (walk, SPDX headers, ──┘ +metadata, git) ── uses plasma-parser header extraction + +plasma audit ──► plasma-parser scan_repo ──► zone map (.plasma.toml) ``` ## Update Protocol @@ -110,3 +101,5 @@ This file is maintained by both humans and AI agents. When updating: Progress bars use: `█` (filled) and `░` (empty), 10 characters wide. Percentages: 0%, 10%, 20%, ... 100% (in 10% increments). +Honesty rule: a component that does not compile or run is 0–20%, +whatever its design documents say. diff --git a/assets/badges/svg/badge-standard.svg b/assets/badges/svg/badge-standard.svg deleted file mode 100644 index 807ad62..0000000 --- a/assets/badges/svg/badge-standard.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - Palimpsest License v1.0 - - - - - - - Palimpsest v1.0 - diff --git a/codemeta.json b/codemeta.json index 17fb235..6a1f1d1 100644 --- a/codemeta.json +++ b/codemeta.json @@ -15,19 +15,22 @@ ], "codeRepository": "https://github.com/hyperpolymath/palimpsest-plasma", "datePublished": "2026-02-12", - "dateModified": "2026-02-12", - "description": "A comprehensive framework for creating dynamic, self-modifying software systems, focusing on resilience and adaptability.", + "dateModified": "2026-07-02", + "description": "Deterministic, typed policy engine: define machine-readable deontic policies (obligations, prohibitions, permissions over repository state) and evaluate repositories against them with byte-for-byte reproducible results. Includes an SPDX expression parser, zone-aware license auditing, and SARIF output.", "keywords": [ - "plasma", - "framework", - "palimpsest", - "self-modifying", - "adaptive" + "policy-engine", + "compliance", + "spdx", + "audit", + "deterministic", + "agentic" ], - "license": "https://spdx.org/licenses/PMPL-1.0-or-later", + "license": "https://spdx.org/licenses/MPL-2.0", "name": "palimpsest-plasma", - "programmingLanguage": [], - "version": "0.1.0", + "programmingLanguage": [ + "Rust" + ], + "version": "0.2.0", "url": "https://github.com/hyperpolymath/palimpsest-plasma", "readme": "https://github.com/hyperpolymath/palimpsest-plasma/blob/main/README.adoc", "issueTracker": "https://github.com/hyperpolymath/palimpsest-plasma/issues", diff --git a/contractile.just b/contractile.just index 8f99137..9a5827b 100644 --- a/contractile.just +++ b/contractile.just @@ -2,7 +2,7 @@ # Source directory: contractiles # Re-generate with: contractile gen-just --dir contractiles # -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # === DUST (Recovery & Rollback) === # Source: Dustfile.a2ml diff --git a/docs/CITATIONS.adoc b/docs/CITATIONS.adoc index c7e49fb..5c7bc07 100644 --- a/docs/CITATIONS.adoc +++ b/docs/CITATIONS.adoc @@ -12,7 +12,7 @@ title = {palimpsest-plasma}, year = {2026}, url = {https://github.com/hyperpolymath/palimpsest-plasma}, - license = {PMPL-1.0-or-later} + license = {MPL-2.0} } ---- diff --git a/docs/FAMILY-PARSER-SPEC.adoc b/docs/FAMILY-PARSER-SPEC.adoc index 97555cb..28c3fe4 100644 --- a/docs/FAMILY-PARSER-SPEC.adoc +++ b/docs/FAMILY-PARSER-SPEC.adoc @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) = Palimpsest Family Parser Specification @@ -9,10 +9,27 @@ v0.1, 2026-03-23 :sectnums: :icons: font +[IMPORTANT] +==== +**Status.** The `plasma-parser` crate this document specifies is *real and +implemented*: the SPDX lexer/parser/catalog, license family type system, +zone scanner, compatibility matrix, and JSON/SARIF reporters all exist in +`plasma-parser/src/` with tests, and `plasma audit` now consumes them (via +`plasma-parser/src/audit/scan.rs`), replacing the old string matching. + +The *consumers* described below beyond the CLI — an HTTP daemon +(`plasma-daemon`, port 8098), a PanLL/Gossamer bridge, OCR of scanned +licenses, and a C FFI to an OCaml engine — are **aspirational** and not +built. There is no OCaml engine; a future formal core (if built) would +*verify* the Rust engine, not consume this parser over FFI (see +`docs/engine-v0-design.adoc`). Treat the daemon/FFI/PanLL sections as +design sketches, not descriptions of shipped code. +==== + == Overview -The Palimpsest Family Parser replaces the current naive string-matching in -`plasma audit` and `plasma migrate` with a structured parser that understands: +The Palimpsest Family Parser is the structured SPDX/license engine behind +`plasma audit` (replacing the earlier naive string-matching). It understands: * The palimpsest license family as a *type system* — base + layer + fallback * SPDX compound expressions (`MIT OR Apache-2.0`, `PPMPL-1.0-or-later WITH ...`) @@ -20,9 +37,9 @@ The Palimpsest Family Parser replaces the current naive string-matching in * License zone boundaries within a single repository * Fallback chain validation (does the declared fallback match the base?) -This is a Rust library crate (`plasma-parser`) consumed by the existing -`plasma` CLI binary, the planned OCaml PLASMA governance engine (via C FFI), -and a PanLL panel (via Gossamer HTTP bridge or WASM). +It is a Rust library crate (`plasma-parser`) consumed today by the `plasma` +CLI binary. Additional consumers (an HTTP daemon, a PanLL panel) are +planned, not implemented — see the status note above. == Architecture @@ -263,7 +280,7 @@ repository root (or in `.machine_readable/6a2/`): [source,toml] ---- # .plasma.toml — license zone configuration for palimpsest-plasma -# SPDX-License-Identifier: PPMPL-1.0-or-later +# SPDX-License-Identifier: CC-BY-SA-4.0 [project] name = "idaptik" diff --git a/docs/architecture.adoc b/docs/architecture.adoc index a4a2bb0..a6cffb3 100644 --- a/docs/architecture.adoc +++ b/docs/architecture.adoc @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell = Palimpsest Plasma — Architecture :toc: @@ -9,200 +9,121 @@ toc::[] == Purpose -palimpsest-plasma is the PLASMA engine — the operational, programmable substrate -that keeps the Palimpsest-MPL ecosystem correct, compliant, and narratively -coherent over time. +palimpsest-plasma is a deterministic, typed policy engine. It interprets +machine-readable policies — deontic rules over repository state — and +evaluates real repositories against them, producing findings and audit +reports that are byte-for-byte reproducible. -It interprets the licence, exhibits, and governance rules as typed, -machine-readable policies, evaluates real-world repository states against those -obligations, and produces findings, actions, and audit reports. +Its role in the wider tooling picture is precision: when neural/LLM agents +operate on code, plasma defines what "correct" means (policies) and verifies +it afterwards (deterministic evaluation), providing the contract that +forensic tooling (somethings-fishy) and claim verification +(did-you-actually-do-that) consume. == Design Principles -* **Typed correctness** — OCaml's type system enforces structural soundness at - compile time; deontic operators (obligation, prohibition, permission) are - first-class values in the AST. -* **Migratable schemas** — Every policy version carries a schema version; the - migration framework ensures prior audits remain interpretable as the policy - evolves. -* **Narrative alignment** — Findings, actions, and audit logs carry rationale - fields that tie machine decisions back to human-readable governance intent. -* **Composable surfaces** — The core engine is consumed by thin integration - wrappers (CLI, hooks, CI, daemon) that never embed policy logic. +* **Typed correctness** — Rust's type system enforces structural soundness + at compile time; deontic operators (obligation, prohibition, permission) + are first-class values in the AST. +* **Determinism as a feature** — evaluation is a pure, total function of + (policy, facts): no clocks, no randomness, no ambient state, BTree-ordered + iteration. Identical inputs produce identical output. +* **Reject at load, never mid-run** — any construct the evaluator cannot + give exact semantics to fails schema validation up front; a policy that + loads always evaluates. +* **Narrative alignment** — rules and findings carry rationale and source + fields tying machine decisions back to human-readable intent. +* **Composable surfaces** — the engine is consumed by thin wrappers (CLI + today; hooks and CI actions on the roadmap) that never embed policy logic. == Project Layout [source] ---- palimpsest-plasma/ - dune-project - dune-workspace - plasma.opam - - src/ - core/ - plasma_policy_ast.ml -- Policy AST types - plasma_policy_ast.mli - plasma_policy_schema.ml -- Schema versioning - plasma_policy_schema.mli - plasma_eval.ml -- Evaluation engine - plasma_eval.mli - plasma_migration.ml -- Schema migration framework - plasma_migration.mli - plasma_facts.ml -- Fact collection types - plasma_facts.mli - plasma_findings.ml -- Finding types and severity - plasma_findings.mli - plasma_actions.ml -- Action planner - plasma_actions.mli - plasma_governance.ml -- Governance runtime - plasma_governance.mli - - integration/ - plasma_cli.ml -- CLI entry point - plasma_cli.mli - plasma_git_hooks.ml -- Git hook support - plasma_git_hooks.mli - plasma_ci_integration.ml -- CI/CD integration - plasma_ci_integration.mli - plasma_daemon.ml -- Daemon/cron mode - plasma_daemon.mli - - adapters/ - plasma_repo_scan.ml -- Repository scanning - plasma_repo_scan.mli - plasma_spdx.ml -- SPDX header parsing - plasma_spdx.mli - plasma_exhibits.ml -- Exhibit parsing - plasma_exhibits.mli - plasma_metadata.ml -- Governance metadata parsing - plasma_metadata.mli - - bin/ - main_cli.ml -- Binary entry point - - test/ - test_policy_ast.ml - test_eval_engine.ml - test_migration.ml - test_integration.ml - - examples/ - policies/ - example_base_policy.json - example_exhibit_policy.json - repos/ - minimal_compliant_repo/ - drifting_repo/ + Cargo.toml -- workspace: ".", plasma-parser, plasma-engine + + src/ -- the `plasma` CLI binary + main.rs -- clap surface: check/facts/policy/audit/init/badge/migrate + check.rs -- plasma check (policy evaluation) + facts_cmd.rs -- plasma facts (fact snapshot dump) + policy_cmd.rs -- plasma policy validate + audit.rs -- SPDX audit on plasma-parser's scan_repo + init.rs / migrate.rs / badge.rs -- license adoption tooling (any SPDX id) + + plasma-engine/ -- the policy engine (depends on plasma-parser) + src/ast.rs -- Policy AST: subjects, resources, conditions, + modalities, actions, overlays + src/schema.rs -- versioned TOML/JSON loading; load-time rejection + src/facts.rs -- fact collection (the only impure module) + src/eval.rs -- pure, total evaluator (deontic matrix) + src/finding.rs -- findings, severities, evaluation summary + src/report/ -- human + SARIF renderers + policies/repo-hygiene.plasma.toml -- bundled, self-applied policy + + plasma-parser/ -- license/SPDX machinery + src/spdx/ -- lexer, recursive-descent parser, catalog + src/family/ -- license family types, registry, fallbacks + src/zone/ -- .plasma.toml zones, file→zone assignment + src/audit/ -- header extraction, scan_repo, content detection + src/compat/ -- license compatibility matrix + src/report/ -- RepoAudit JSON/SARIF + + tests/check_selftest.rs -- the repo checks itself with its own policy + docs/engine-v0-design.adoc -- NORMATIVE semantics + Catala-readiness ---- -=== Layer Descriptions - -**core/** — Canonical PLASMA substrate. Contains the Policy AST, schemas, -evaluation engine, migration framework, fact types, findings, action planner, -and governance runtime. No IO happens here; all functions are pure. - -**integration/** — Surfaces that orchestrate the core. The CLI, git hooks, CI -integration, and daemon each call into the core and handle IO, configuration, -and user interaction. - -**adapters/** — IO and environment-specific collectors and parsers. SPDX header -detection, exhibit parsing, repository structure scanning, and governance -metadata extraction. Adapters produce `fact_set` values consumed by the -evaluation engine. - == Data Flow [source] ---- -Repository State - │ - ▼ - ┌─────────────┐ - │ Adapters │ Scan repo, parse SPDX, collect exhibits - │ (IO layer) │ - └──────┬──────┘ - │ fact_set - ▼ - ┌─────────────┐ - │ Core Eval │ Evaluate rules against facts - │ Engine │ Produce findings - └──────┬──────┘ - │ finding list - ▼ - ┌─────────────┐ - │ Action │ Plan corrective/governance actions - │ Planner │ - └──────┬──────┘ - │ planned_action list - ▼ - ┌─────────────┐ - │ Integration │ CLI output, audit logs, fixes - │ Surface │ - └─────────────┘ + policy file (TOML/JSON) repository tree + │ │ + schema.rs load facts.rs collect (impure boundary) + + validation (files, SPDX headers, + │ metadata, git HEAD) + ▼ ▼ + └────────► eval.rs evaluate ◄┘ (pure, total) + │ + ▼ + Evaluation + (ordered findings + summary) + │ + ┌──────────────┼──────────────┐ + ▼ ▼ ▼ + human JSON SARIF 2.1.0 + (terminal) (interchange) (Code Scanning, + sibling tools) ---- -== Evaluation Loop - -At v0.1 the evaluation engine runs a crisp, deterministic loop: - -1. **Collect facts** from the repository via adapters. -2. **Compile conditions** in each rule to predicates over the fact set. -3. **Evaluate each rule** — check its condition, then based on modality - (obligation/prohibition/permission) and action kind, emit findings. -4. **Feed findings** into the action planner. -5. **Present planned actions** to the integration surface (suggest or apply). - -== Migration Framework - -Goal: allow policy schema and policy instances to evolve without breaking -previous audits. - -* Each policy carries a `schema_version` (major.minor). -* A `migration` record maps `from_version` -> `to_version` with a transform - function. -* A `migration_plan` registers all known steps and a `latest_version`. -* `migrate_to_latest` chains steps automatically. - -v0.1 ships with schema 0.1, no migrations yet, but the framework is present -and tested with mock migrations so the upgrade path is baked in from day one. - -See link:policy-ast-v0.1.adoc[policy-ast-v0.1.adoc] for the full AST type -definitions. - -== Governance Runtime - -The governance module handles: - -* **Council decisions** — accept a decision ID, look up its effects, apply as - policy overlays or exhibit modifications. -* **Exhibit lifecycle** — track which exhibits are active, superseded, or - deprecated; enforce version constraints. -* **Versioning rules** — validate that release tags follow the versioning - expectations described in `palimpsest-license/VERSIONING.adoc`. - -== Contractiles - -Contractiles are a governance-grade primitive representing reversible -commitments within the PLASMA engine. See link:contractiles.adoc[contractiles.adoc] -for the full design. - -Within the evaluation engine, a contractile is a typed structure representing a -contracting rule — one that narrows the permissible state space as evaluation -proceeds. They embody the tension, load, or narrative pressure exerted by a -rule, a dependency, or a governance decision. - -== Integration with union-policy-parser +The action planner (findings → corrective actions, `plasma fix`) is +roadmap v0.3 and will sit between Evaluation and a new apply/dry-run +surface; see link:../ROADMAP.adoc[ROADMAP.adoc]. -The `union-policy-parser/` sub-project provides A2ML-based contract validation -for union standards (NUJ, IWW, UCU). PLASMA integrates with it by: +== Schema Versioning -* Consuming A2ML-parsed documents as additional fact sources -* Applying PMPL governance rules alongside union-specific validation -* Sharing the exhibit and attestation model +Every policy document carries a mandatory `schema_version`, checked before +anything else. The engine implements exactly one schema version at a time +(currently 0.1); documents written against other versions are refused with +a distinct error rather than reinterpreted. Reserved constructs let the +format grow without breaking loaders: they parse today, are rejected by +validation, and gain semantics in the version that can honour them. == Related Projects -* `palimpsest-license` — The canonical PMPL-1.0 licence text and versioning -* `palimpsest-governance` — Stewardship Council processes and artefacts -* `union-policy-parser` — A2ML contract validation (sub-project within this repo) +* **somethings-fishy** (planned) — forensic investigation of bot-inflicted + damage; consumes plasma findings and provenance. +* **did-you-actually-do-that** (planned) — verifies agents did what they + claimed; consumes `plasma facts` snapshots (diffable before/after) and + pass-findings (positive evidence). +* **Palimpsest license (PMPL)** — a separate future project. plasma-parser + recognises its SPDX identifiers; nothing more. + +== Historical Note + +Earlier revisions of this document described an OCaml implementation +(`src/core/*.ml`) that was never built. The typed design survives — +link:policy-ast-v0.1.adoc[policy-ast-v0.1.adoc] is its lineage — and the +policy format is specified (link:engine-v0-design.adoc[engine-v0-design.adoc]) +so a future OCaml/Catala formal core can implement the same semantics and be +verified against this engine. diff --git a/docs/cli-design.adoc b/docs/cli-design.adoc index b564273..af871cf 100644 --- a/docs/cli-design.adoc +++ b/docs/cli-design.adoc @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell = Palimpsest Plasma — CLI & Integration Design :toc: @@ -9,198 +9,95 @@ toc::[] == CLI Interface -One binary, narratively clear subcommands, machine-readable output where useful. +One binary (`plasma`), narratively clear subcommands, machine-readable +output where useful. -=== Subcommands +=== Implemented (v0.2) [source] ---- -plasma check [PATH] [--policy FILE] [--format human|json] -plasma fix [PATH] [--apply] [--policy FILE] -plasma audit [PATH] [--output FILE] [--format json] -plasma governance apply-decision DECISION_ID [--source FILE] -plasma migrate policy [--input FILE] [--output FILE] +plasma check [PATH] [--policy FILE] [--format human|json|sarif] + [--severity error|warning|info] [--quiet] [--verbose] +plasma facts [PATH] +plasma policy validate FILE +plasma audit [--path PATH] [--license SPDX-EXPR] [--fix] +plasma init [--path PATH] [--license SPDX-ID] [--author NAME] +plasma badge [--license SPDX-ID] [--style STYLE] +plasma migrate [--path PATH] [--from SPDX-ID] [--to SPDX-ID] ---- === `plasma check` -Validate a repository against a policy. +Evaluate a repository against a policy. -* **Input:** repo path, policy file (optional — default is the canonical PMPL - policy bundle) -* **Output:** exit code non-zero on violations; rich text or JSON +* **Input:** repo path (default `.`), policy file (default: the bundled + repo-hygiene policy) +* **Output:** exit 0 when no violation at or above the severity threshold + exists; exit 1 otherwise. Human text, JSON (the full `Evaluation`), or + SARIF 2.1.0. * **Flags:** - - `--policy FILE` — custom policy file - - `--format human|json` — output format (default: human) - - `--severity warning|error` — minimum severity threshold - - `--quiet` — exit code only, no output + - `--policy FILE` — custom policy (`.toml` or `.json`) + - `--format human|json|sarif` (default: human) + - `--severity error|warning|info` — failure threshold (default: error) + - `--quiet` — exit code only + - `--verbose` — include satisfied (pass) findings -=== `plasma fix` +=== `plasma facts` -Plan or apply corrective actions based on findings. +Dump the deterministic fact snapshot the evaluator sees, as JSON. This is +the diffable before/after artifact for agent-verification tooling. -* **Without `--apply`:** show planned actions (dry-run) -* **With `--apply`:** perform changes, log diff summary, create backup -* **Flags:** - - `--policy FILE` — custom policy file - - `--apply` — actually modify the repo - - `--backup` — create backup before applying changes (default: true) - -=== `plasma audit` - -Generate structured audit logs for external systems. - -* **Output:** JSON audit log suitable for legal, governance, and provenance - reviews -* **Flags:** - - `--output FILE` — write to file instead of stdout - - `--format json` — output format - - `--since DATE` — audit since date - - `--verbose` — include positive findings (rule-passed) - -=== `plasma governance` - -Connect Council decisions and exhibit changes to policy updates. - -* `apply-decision DECISION_ID` — look up a Council decision, apply as overlay -* `--source FILE` — decision definition file -* `list-exhibits` — show active, superseded, deprecated exhibits - -=== `plasma migrate` - -Migrate policy files between schema versions. - -* `policy --input FILE --output FILE` — migrate a policy to latest schema -* `--target-version MAJOR.MINOR` — migrate to specific version -* `--dry-run` — show migration plan without writing - -== Integration Surfaces +=== `plasma policy validate` -=== Git Hooks +Parse and schema-check a policy file; print rule counts on success. Fails +with a precise error on schema-version mismatch, duplicate rule ids, or +reserved constructs. -PLASMA provides scripts for pre-commit and pre-push enforcement. +=== `plasma audit` -[source,bash] ----- -# .git/hooks/pre-commit -#!/bin/bash -plasma check . --quiet --severity error || exit 1 ----- +Zone-aware SPDX header audit powered by plasma-parser. Reads `.plasma.toml` +when present; otherwise expects `--license` (default MPL-2.0) everywhere. +`--fix` inserts missing headers using each file's zone-expected license. +Exits non-zero on non-compliance so CI fails. -Configuration via `.plasma.toml`: +=== `plasma init` / `migrate` / `badge` -[source,toml] ----- -[hooks] -pre-commit = true -pre-push = true -severity = "error" -policy = "policies/base.json" ----- +License adoption for any SPDX identifier (MPL-2.0 default). `init` embeds +the full MPL-2.0 text and writes stubs plus a warning for other licenses; +`migrate` rewrites LICENSE (with backup) and sweeps SPDX headers from the +detected or given `--from` to `--to`; `badge` emits shields.io markup with +proper `-`→`--` escaping. -=== GitHub Actions +== Roadmap surfaces (not yet implemented) -Reference workflow for CI enforcement: +* `plasma fix` — findings → planned corrective actions, dry-run and + `--apply` modes with backups (v0.3) +* Git hooks (pre-commit/pre-push wrapping `plasma check --quiet`) and a + reference GitHub Action / GitLab job (v0.4) +* `plasma governance` / decision registries — awaits `consistent-with` + semantics +* Daemon/cron mode for periodic audits -[source,yaml] ----- -name: PLASMA Compliance -on: [push, pull_request] - -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Run PLASMA check - run: plasma check . --format json > plasma-report.json - - name: Upload report - uses: actions/upload-artifact@v4 - with: - name: plasma-report - path: plasma-report.json ----- +A future `.plasma.toml` extension will let a repository pin its own policy +file so `plasma check` needs no flags at all. -=== Daemon Mode +== Exit codes -Minimal "scan and log" loop for periodic audits: +[cols="1,3"] +|=== +| Code | Meaning -[source] ----- -plasma daemon [--interval 1h] [--output-dir /var/log/plasma/] ----- - -Writes timestamped audit logs to the output directory. Suitable for cron or -systemd timer integration. +| 0 | Clean: no violation at or above the threshold +| 1 | Violations at or above the threshold (check), or non-compliant files (audit) +| 2 | Usage or IO error (bad flags, unreadable policy, missing path) +|=== == Output Formats -=== Human Output - -[source] ----- -PLASMA Check — /path/to/repo -Policy: pmpl-1.0-base (v0.1) - -[ERROR] Missing SPDX header in src/main.ml - Rule: spdx-header-required (Obligation) - Source: PMPL-1.0 §3.1 - -[WARNING] Exhibit E-001 reference outdated - Rule: exhibit-ref-current (Obligation) - Source: Governance Playbook §2.4 - -2 findings (1 error, 1 warning) ----- - -=== JSON Output - -[source,json] ----- -{ - "path": "/path/to/repo", - "policy": {"id": "pmpl-1.0-base", "version": {"major": 0, "minor": 1}}, - "findings": [ - { - "rule_id": "spdx-header-required", - "subject": {"type": "file", "path": "src/main.ml"}, - "severity": "error", - "kind": "compliance", - "message": "Missing SPDX header", - "source": "PMPL-1.0 §3.1" - } - ], - "summary": {"errors": 1, "warnings": 1, "info": 0} -} ----- - -== Action Planner Output - -=== Dry-Run - -[source] ----- -PLASMA Fix — /path/to/repo (dry-run) - -[1] Add SPDX header to src/main.ml - License: PMPL-1.0-or-later - Reason: spdx-header-required (Obligation) - -[2] Update exhibit reference in .machine_readable/ECOSYSTEM.scm - From: E-001-v1 → E-001-v2 - Reason: exhibit-ref-current (Obligation) - -2 actions planned. Run with --apply to execute. ----- - -=== Apply Mode - -[source] ----- -PLASMA Fix — /path/to/repo (applying) - -[1] ✓ Added SPDX header to src/main.ml -[2] ✓ Updated exhibit reference in .machine_readable/ECOSYSTEM.scm - -2 actions applied. Backup at .plasma-backup/2026-02-20T10:00:00/ ----- +* **Human** — violations with rule id, subject, message, rationale, and + source; a one-line summary. Pass findings shown with `--verbose`. +* **JSON** — the `Evaluation` structure serialized directly; deterministic + (byte-identical for identical inputs). +* **SARIF 2.1.0** — `plasma/` rule namespace, levels + `error|warning|note`, pass findings as `kind: "pass"` under `--verbose`. + Uploadable to GitHub Code Scanning. diff --git a/docs/contractiles.adoc b/docs/contractiles.adoc index 3ecd7b2..852fefe 100644 --- a/docs/contractiles.adoc +++ b/docs/contractiles.adoc @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell = Palimpsest Plasma — Contractiles :toc: @@ -7,14 +7,24 @@ toc::[] +[NOTE] +==== +Contractiles are a **roadmap** concept for reversible policy commitments +(see ROADMAP.adoc, "Future Directions"). This document is a design sketch; +the v0.2 engine implements one-directional deontic rules (obligation / +prohibition / permission), not reversible contractiles. The wording below +predates the identity pivot and uses "PLASMA engine" / "Palimpsest-MPL" +loosely — read it as design intent, not shipped behaviour. +==== + == What Are Contractiles? -Contractiles are a governance-grade primitive within the PLASMA engine. The word -sits between "contract" and "tensile" — a unit that contracts, binds, or -tightens around a constraint or intention. Within the Palimpsest-MPL ecosystem, -contractiles serve as the smallest unit of binding in governance: something that -tightens obligations around a state, but can also relax or release under defined -conditions. +Contractiles are a governance-grade primitive intended for the policy +engine. The word sits between "contract" and "tensile" — a unit that +contracts, binds, or tightens around a constraint or intention. +Contractiles serve as the smallest unit of binding in governance: something +that tightens obligations around a state, but can also relax or release +under defined conditions. == Conceptual Roles diff --git a/docs/engine-v0-design.adoc b/docs/engine-v0-design.adoc new file mode 100644 index 0000000..e530491 --- /dev/null +++ b/docs/engine-v0-design.adoc @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Plasma Engine v0 — Normative Design +:toc: +:toc-placement!: +:sectnums: + +toc::[] + +== Status + +This document is the **normative specification** of the plasma-engine v0 +policy format and evaluation semantics, as implemented in +`plasma-engine/src/`. It supersedes the OCaml-typed sketch in +link:policy-ast-v0.1.adoc[policy-ast-v0.1.adoc] (kept as design lineage). + +Divergences from that sketch, all deliberate: + +* `exhibit` → **overlay** — the mechanism (additive policy extension) is + retained; the license-specific vocabulary is not. +* Rules carry an explicit **severity** (`error` | `warning` | `info`, + default `error`). +* PMPL-specific conditions (`C_HasProvenanceManifest`, + `C_HasExhibitReference`) are dropped; `spdx-license-is` is added. +* `S_File` gains a multi-subject form `file-pattern` (a list of globs — the + `glob` crate has no brace expansion, so `**/*.{rs,toml}` must be written + as two patterns). + +== Document format + +A policy is a TOML (authoring) or JSON (interchange) document deserialized +into the types in `plasma-engine/src/ast.rs`. Enums are internally tagged: +an object with a `type` field in kebab-case, e.g. +`{ type = "file", path = "LICENSE*" }`. This encoding — not the Rust types — +is the interchange contract. + +[source,toml] +---- +schema_version = { major = 0, minor = 1 } # mandatory, checked first +id = "policy-id" +version = { major = 1, minor = 0 } # the policy's own version + +[[rules]] +id = "unique-rule-id" # duplicate ids are a load error +modality = "obligation" # obligation | prohibition | permission +severity = "error" # optional; error | warning | info +subject = { type = "repo" } +resource = { type = "file", path = "LICENSE*" } +condition = { type = "true" } # optional; defaults to true +action = { type = "present" } +rationale = "why this rule exists" # optional +source = "provenance of the rule" # optional + +[[overlays]] # optional +id = "overlay-id" +applies_to = [] + +[[overlays.effects]] +type = "add-rules" +# rules = [ ... same shape as above ... ] +---- + +=== Vocabulary + +Subjects (`type =`): +`repo` · `file {path}` · `file-pattern {patterns: [glob…]}` · +`metadata {key}` · `release {tag}` (reserved). + +Resources: `file {path}` (path may be a glob) · `header` · +`manifest {path}` · `governance-field {key}` · `release` (reserved). + +Conditions: `true` · `not {of}` · `all {of: […]}` · `any {of: […]}` · +`repo-has-file {path}` · `has-spdx-header` · `spdx-license-is {expr}` · +`governance-flag-set {key, value}` · `version-at-least {version}` · +`file-matches-pattern {path, pattern}` (reserved). + +Actions: `present` · `absent` · `valid` · `consistent-with {id}` (reserved). + +Modalities: `obligation` · `prohibition` · `permission`. + +=== Load-time rejection + +Every construct the v0 evaluator cannot give exact semantics to is rejected +at **load**, never mid-evaluation (`plasma-engine/src/schema.rs`): + +* `schema_version` ≠ 0.1 → `UnsupportedSchemaVersion` +* duplicate rule ids (across base rules and overlay-added rules) +* reserved constructs: subject `release`, resource `release`, action + `consistent-with`, condition `file-matches-pattern` (v0 facts carry no + file contents), overlay effects `modify-rules` / `override-rules` + +Consequence: **any policy that loads, evaluates** — `evaluate` is total. + +== Fact collection + +`plasma-engine/src/facts.rs` is the only impure module. All repository +knowledge flows through the `FactSet`; the evaluator never touches the +filesystem. Collection rules, pinned so an independent implementation can +reproduce identical facts: + +* **Walk**: every regular file under the root; an entry is skipped when its + name starts with `.` or is one of `target`, `node_modules`, `vendor`, + `_build`, `deps`. Paths are recorded relative to the root, `/`-separated. +* **SPDX headers**: for files whose extension is in + `plasma_parser::audit::header::AUDITABLE_EXTENSIONS`, the raw + `SPDX-License-Identifier:` value from the first **15 lines** + (`MAX_HEADER_LINES`), comment prefixes `//`, `#`, `--`, `;;` stripped. + The raw string is stored; parsing is deferred to evaluation (still pure — + a function of the stored string). +* **Metadata**: v0 collects `version` from the root `Cargo.toml` + `[package]` table when present. +* **Git**: `is_repo` and `head_ref` read directly from `.git/HEAD` — no + subprocess, no clock. +* All collections are `BTreeMap`/`BTreeSet`: iteration order, and therefore + finding order, is deterministic. The fact set contains **no timestamps**. + +== Evaluation semantics + +`evaluate(policy, facts)` (`plasma-engine/src/eval.rs`): + +1. Effective rules = base rules ++ overlay `add-rules` (in document order). + Findings from overlay rules carry `source = "overlay:"` when the + rule declares no source of its own. +2. Each rule's subject expands to concrete instances: `repo` → itself; + `file` → that path; `file-pattern` → every collected file matching any + pattern; `metadata` → that key. +3. The condition is evaluated as a total predicate of (facts, instance). + If false, the rule is **not applicable** to that instance: no finding. +4. If applicable, the deontic matrix produces exactly one finding + (violation or pass) per instance. + +=== Condition denotations + +|=== +| Condition | Denotation + +| `repo-has-file p` | ∃ f ∈ files. glob(p, f) +| `has-spdx-header` | instance is a path f ∧ headers[f] = Some(_) +| `spdx-license-is e` | header of instance parses ∧ parse(header) = parse(e); if either side fails to parse, exact-text comparison +| `governance-flag-set k v` | metadata[k] = v (absent key ⇒ false) +| `version-at-least v` | numeric dotted comparison of metadata["version"] against v; absent or non-numeric ⇒ false +| `not / all / any` | strict boolean composition +|=== + +Glob semantics: `glob::Pattern` over `/`-separated relative paths, with one +extension: a `\**/` prefix also matches zero directories (`**/*.rs` matches +`main.rs`). A malformed pattern matches nothing. + +=== Deontic matrix + +"exists" is resource existence for the instance; "valid" is structural +validity (for `header`: the SPDX expression parses; for everything else in +v0, validity coincides with existence). + +|=== +| Modality | Action | Violation iff + +| obligation | present | ¬exists +| obligation | absent | exists +| obligation | valid | ¬valid +| prohibition | present | exists +| prohibition | absent | ¬exists +| prohibition | valid | valid +| permission | any | never (emits a pass finding) +|=== + +Satisfied rules emit **pass** findings (`status = "pass"`), retained in the +`Evaluation` and rendered with `--verbose`. This gives claim-verification +tooling positive evidence, not just the absence of complaints. + +== Output contracts + +* **JSON** (`plasma check --format json`): the `Evaluation` structure + serialized directly — root, policy id/version, schema version, ordered + findings, summary counts. Deterministic: identical inputs produce + byte-identical output. +* **SARIF 2.1.0** (`--format sarif`): one SARIF rule per policy rule, + `ruleId = "plasma/"` — a **stable namespace**; consumers key on + it. Severity maps to `error` | `warning` | `note`. Pass findings appear + only with `--verbose`, as `kind: "pass"`. Repo- and metadata-scoped + findings carry no artifact location (SARIF permits this). +* **Facts** (`plasma facts`): the `FactSet` as JSON — the diffable + before/after snapshot for agent-verification tooling. + +== Formalization guarantees (Catala-readiness) + +The v0 engine commits to properties a future formal core (OCaml/Catala) can +rely on, so that core can *generate* policies in this format or *verify* +this evaluator against a formal semantics: + +1. **Determinism** — `evaluate` is a pure function `Policy × FactSet → + Evaluation`: no IO, clocks, randomness, or environment reads; BTree + ordering everywhere; byte-identical JSON for identical inputs. +2. **Totality** — no panics on any loadable policy; every partial case has + the defined false/absent semantics tabulated above; unsupported + constructs are rejected at load, never mid-evaluation. +3. **Versioned schema** — `schema_version` is mandatory and checked first; + the loader refuses other versions with a distinct error. +4. **No ambient state** — all repository knowledge flows through the + `FactSet`, whose collection procedure is specified above. +5. **Closed condition algebra** — conditions form a boolean algebra over + the finite predicate vocabulary denoted above; nothing else can appear. +6. **Stable rule namespace** — `plasma/` in SARIF is the + interchange contract with sibling tooling (`somethings-fishy`, + `did-you-actually-do-that`). + +== Roadmap interaction + +Reserved constructs (`release` subjects/resources, `consistent-with`, +`file-matches-pattern`, `modify-rules`/`override-rules`) parse today and +gain semantics in later minor versions alongside their fact sources +(release facts, content facts, decision registries). The schema version +gates all of it: a policy using future semantics will not load on an engine +that cannot honour it. diff --git a/docs/mvp-v1.adoc b/docs/mvp-v1.adoc deleted file mode 100644 index fbc88a9..0000000 --- a/docs/mvp-v1.adoc +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= Palimpsest Plasma MVP v1 Roadmap -:toc: -:toclevels: 3 - -== Purpose - -Palimpsest Plasma MVP v1 packages the minimal combination of assets, tooling, and -audit guidance that lets creative projects adopt **PMPL-1.0 (SPDX: PMPL-1.0-or-later)** -with confidence. It bundles the canonical license text, adaptive badge designs, and -compliance checklists together with governance guidance from *Palimpsest License*. - -== Deliverables - -* **Legal alignment** – keep a single `LICENSE` file referencing PMPL-1.0, with badge - references sourced from `assets/badges/svg/badge-standard.svg`. -* **Badge & embed pack** – publish the complete badge asset set plus Markdown/HTML - snippets (see `assets/badges/` and `palimpsest-license/embed/license-blocks/`) for - reuse by downstream repos. -* **Roadmap & documentation** – describe MVP v1 goals, milestones, and compliance steps - in this file and link back to `palimpsest-license/VERSIONING.adoc`. -* **Compliance automation** – add `STATE.scm` metadata, `Justfile` hints, and an audit - script description so the repository can be validated automatically. -* **Audit readiness** – verify the repository with Palimpsest tooling (`pmpl-verify`, - `pmpl-audit`) and capture the commands/output below. - -== Milestones - -The MVP v1 release targets the following milestones: - -* **1.0.0 license release** – PMPL-1.0 text finalized, SPDX headers standardized, - README badges updated, and `LICENSE` / `ECOSYSTEM.scm` reflecting the canonical - stewarding council guidance. -* **Badge & compliance pack** – `assets/badges/` holds the v1.0 badge family, while - `docs/mvp-v1.adoc` documents where to place badges, license blocks, and metadata. -* **Audit & release artifacts** – `STATE.scm` lists release artifacts (docs, badges, - audit log, compliance checklist) so the repo can ship as a cohesive MVP bundle. - -== Compliance & Audit - -Run the Palimpsest compliance tools from the canonical license repo: - --- -```bash -cd $REPOS_DIR/palimpsest-license/tools/pmpl-verify -cargo run --manifest-path Cargo.toml -- $REPOS_DIR/palimpsest-plasma --recursive --existence-only -``` --- - -Summary from the latest run: 0 verified, 85 missing, 0 failed (signatures not yet published). Update this log as provenance metadata is generated. - -This command checks every file for an accompanying `.sig` block and reports the -verified/missing counts. Replace `--existence-only` with an empty flag once quantum-safe -signatures are available. - -Before rerunning `pmpl-verify`/`pmpl-audit`, use `pmpl-sign` to generate placeholder -`*.sig` descendants for the MVP artifacts: - --- -```bash -cd $REPOS_DIR/palimpsest-license/tools/pmpl-sign -for file in $REPOS_DIR/palimpsest-plasma/LICENSE \ - $REPOS_DIR/palimpsest-plasma/docs/mvp-v1.adoc \ - $REPOS_DIR/palimpsest-plasma/docs/release-log.adoc \ - $REPOS_DIR/palimpsest-plasma/docs/release-notes.adoc \ - $REPOS_DIR/palimpsest-plasma/STATE.scm; do - cargo run --manifest-path Cargo.toml -- "$file" --signer palimpsest-council --force -done -``` --- - -Each command writes `.sig`; keep those alongside the release so auditors can see -cryptographic metadata is attached even if the actual signature is placeholder. - -For a deeper compliance sweep, use: - --- -```bash -cd $REPOS_DIR/palimpsest-license/tools/pmpl-audit -cargo run --manifest-path Cargo.toml -- $REPOS_DIR/palimpsest-plasma -``` --- - -Ensure your environment has the same Rust toolchain as the Palimpsest toolkit before -running these commands. - -== Release Artifacts - -Palimpsest Plasma MVP v1 ships the following artifacts: - -* Consolidated `LICENSE`/`STATE.scm` metadata describing the PMPL-1.0 release. -* Badge/README assets in `assets/badges/` and `docs/mvp-v1.adoc` with embed guidance. -* Audit log from the `pmpl-verify`/`pmpl-audit` runs above (copy the CLI output into - `docs/audit-report.adoc` or `ROADMAP.adoc` if needed). - -Include this document in your release notes, linking to: - -* `palimpsest-license/VERSIONING.adoc` for version policy. -* `palimpsest-license/assets/badges/README.md` and `embed/license-blocks/README.md` for - badge usage. -* `palimpsest-license/legal/README.adoc` for jurisdictional/governance context. diff --git a/docs/mvp-v1.adoc.sig b/docs/mvp-v1.adoc.sig deleted file mode 100644 index 54ac41e..0000000 --- a/docs/mvp-v1.adoc.sig +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN PALIMPSEST SIGNATURE----- -Version: PMPL-SIG/1.0 -Algorithm: ML-DSA-65 -Signer: palimpsest-council -Timestamp: 2026-01-19T12:47:44Z -Content-Hash: 67a9a6709b3bcef7a5b8ed2057659fe8ccefa513cba4274c993e21c9da10609e - -[PLACEHOLDER: Actual ML-DSA-65 signature would go here] -[Requires: pqcrypto or oqs library integration] ------END PALIMPSEST SIGNATURE----- diff --git a/docs/policy-ast-v0.1.adoc b/docs/policy-ast-v0.1.adoc index 44ba5fa..d35a0d4 100644 --- a/docs/policy-ast-v0.1.adoc +++ b/docs/policy-ast-v0.1.adoc @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell = Palimpsest Plasma — Policy AST v0.1 :toc: @@ -8,6 +8,16 @@ toc::[] +[IMPORTANT] +==== +**Status: historical design lineage.** This document is the original +OCaml-typed sketch of the policy AST. The normative specification — as +implemented in Rust in `plasma-engine/src/` — is +link:engine-v0-design.adoc[engine-v0-design.adoc], which documents the +deliberate divergences (exhibits generalised to overlays, per-rule +severity, license-specific conditions replaced by `spdx-license-is`). +==== + == Purpose This document specifies the typed Abstract Syntax Tree (AST) for PLASMA policies diff --git a/docs/release-log.adoc b/docs/release-log.adoc index 4d80dc5..319bf2c 100644 --- a/docs/release-log.adoc +++ b/docs/release-log.adoc @@ -1,9 +1,37 @@ -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell = Palimpsest Plasma Release Log :toc: -== Release Summary (v1.1) +== Release Summary (v0.2.0) + +**Version:** 0.2.0 +**Legal:** MPL-2.0 (code), CC-BY-SA-4.0 (documentation) +**Release date:** 2026-07-02 +**Theme:** identity pivot + engine v0. The project is now a deterministic, +typed policy engine; the Palimpsest license moves to its own future project. + +**Key artifacts:** + +* `plasma-engine/` – policy AST, versioned schema, fact collection, pure + evaluator, human/JSON/SARIF rendering +* `plasma` binary – `check`, `facts`, `policy validate`, plus parser-backed + `audit` and license-agnostic `init`/`migrate`/`badge` +* `plasma-engine/policies/repo-hygiene.plasma.toml` – bundled, self-applied + policy (`tests/check_selftest.rs` gates the build on it) +* `docs/engine-v0-design.adoc` – normative semantics + Catala-readiness + +**Verification:** `cargo test --workspace` (66 tests), `plasma check .` +(0 violations), byte-identical JSON across repeated runs. + +**Removed:** union-policy-parser sub-project, MVP tarballs, corrupted +PMPL license text, stale signature files. The version numbering restarts +honestly at 0.2.0 — the "v1.x" entries below record documentation-bundle +releases from the earlier PMPL-toolkit identity. + +''' + +== Historical: Release Summary (v1.1) **Version:** 1.1 (MVP v1 continuity) **Legal:** Palimpsest-MPL License 1.0 (PMPL-1.0-or-later) diff --git a/docs/release-log.adoc.sig b/docs/release-log.adoc.sig deleted file mode 100644 index 3f61a1b..0000000 --- a/docs/release-log.adoc.sig +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN PALIMPSEST SIGNATURE----- -Version: PMPL-SIG/1.0 -Algorithm: ML-DSA-65 -Signer: palimpsest-council -Timestamp: 2026-01-19T12:47:44Z -Content-Hash: 369f6ab4e8714a0bcb932cf622eba3f835204e64a632f3da11766761f532f14e - -[PLACEHOLDER: Actual ML-DSA-65 signature would go here] -[Requires: pqcrypto or oqs library integration] ------END PALIMPSEST SIGNATURE----- diff --git a/docs/release-notes.adoc b/docs/release-notes.adoc index f524b57..e6aea28 100644 --- a/docs/release-notes.adoc +++ b/docs/release-notes.adoc @@ -1,30 +1,39 @@ -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -= Palimpsest Plasma MVP v1 Release Notes (v1.1) += Palimpsest Plasma Release Notes :toc: -== Summary +== v0.2.0 — 2026-07-02 -Palimpsest Plasma MVP v1 packages the canonical Palimpsest-MPL License 1.0 tooling, -badge assets, compliance documentation, and audit logs so projects can adopt PMPL-1.0 -with confidence. Key deliverables: +palimpsest-plasma is now a **deterministic, typed policy engine**: write +machine-readable deontic policies (obligations, prohibitions, permissions +over repository state) and evaluate repositories against them with +byte-for-byte reproducible results. -* `LICENSE` – PMPL-1.0-or-later text -* `ECOSYSTEM.scm`, `STATE.scm` – metadata describing release state and milestones -* `assets/badges/` – v1 badge family and README -* `docs/mvp-v1.adoc` – roadmap, compliance guidance, audit commands -* `docs/release-log.adoc` – audit output from `pmpl-verify`/`pmpl-audit` -* `*.sig` files (`LICENSE.sig`, `docs/*.sig`, `STATE.scm.sig`) – provenance metadata -* `mvp-v1-release.tar.gz` – bundle containing the above assets +Highlights: -== Audit Highlights +* New `plasma-engine` crate — typed policy AST, versioned TOML/JSON schema + with load-time rejection of anything the evaluator cannot honour, pure + and total evaluation, deterministic fact collection. +* New CLI surface — `plasma check` (human/JSON/SARIF, exit-code gated), + `plasma facts` (diffable fact snapshots), `plasma policy validate`. +* `plasma audit` rebuilt on the plasma-parser SPDX machinery — real + expression parsing and `.plasma.toml` zone awareness. +* Self-demonstrating: the repo ships a repo-hygiene policy and its test + suite fails if the repo stops satisfying it. +* Licensing unified: code MPL-2.0, docs CC-BY-SA-4.0. The Palimpsest + license (PMPL) is a separate future project. +* Removed: the union-policy-parser sub-project (preserved in git history). -* `pmpl-verify` (existence-only) – Verified: 5, Missing: 110, Failed: 0 - Signature files (`*.sig`) are now included in the release bundle; rerun this command - periodically to track the growing set of verified artefacts as more `.sig` blocks land. -* `pmpl-audit` – PASS; license file recognized as PMPL-1.0, SPDX header scan currently - reports zero files because no signatures/shippable sources yet. +Verification: `cargo test --workspace` (66 tests) and `plasma check .` +(0 violations) both gate the release. -== Distribution +== Historical: MVP v1 (v1.1) — 2026-01-20 -Upload `mvp-v1-release.tar.gz` plus this release notes doc to your release channel (GitHub release, CDN, etc.). Mention the audit commands/output so partners see the compliance readiness. If you need me to help craft the GitHub release description or confirm the upload, let me know. +_The earlier PMPL-toolkit identity shipped documentation/badge bundles +("MVP v1"). Those artifacts (badge pack, `docs/mvp-v1.adoc`, `*.sig` +files, release tarballs) were removed in v0.2.0; this entry remains as a +record. Its audit claims (`pmpl-verify`: 5 verified / 110 missing; +`pmpl-audit`: PASS with zero files scanned) depended on tooling in the +external palimpsest-license repository and are not reproducible from this +repository._ diff --git a/docs/release-notes.adoc.sig b/docs/release-notes.adoc.sig deleted file mode 100644 index e3b8ceb..0000000 --- a/docs/release-notes.adoc.sig +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN PALIMPSEST SIGNATURE----- -Version: PMPL-SIG/1.0 -Algorithm: ML-DSA-65 -Signer: palimpsest-council -Timestamp: 2026-01-19T12:47:44Z -Content-Hash: 15d770d14a19c6c975d3cef946c97ba4f8a8589ae9b74b2ecfa31e7845782f90 - -[PLACEHOLDER: Actual ML-DSA-65 signature would go here] -[Requires: pqcrypto or oqs library integration] ------END PALIMPSEST SIGNATURE----- diff --git a/docs/tech-debt-2026-05-26.md b/docs/tech-debt-2026-05-26.md index fbb2aef..c92b179 100644 --- a/docs/tech-debt-2026-05-26.md +++ b/docs/tech-debt-2026-05-26.md @@ -1,9 +1,14 @@ # Tech-Debt Audit — palimpsest-plasma — 2026-05-26 +> **Update 2026-07-02 (v0.2.0):** the licence-drift finding below +> (`PMPL-1.0-or-later` vs `PPMPL-1.0-or-later` vs manifest) is resolved — +> the project unified on MPL-2.0 (code) / CC-BY-SA-4.0 (docs) and swept all +> SPDX headers. This file is kept as the original point-in-time record. + **Source:** estate-wide automated scan 2026-05-26. **Companion:** [`hyperpolymath/standards` 2026-05-26-estate-*-debt audits](https://github.com/hyperpolymath/standards/tree/main/docs/audits). **Combined severity:** `LOW`. diff --git a/guix.scm b/guix.scm index 87cc590..e9a962e 100644 --- a/guix.scm +++ b/guix.scm @@ -1,18 +1,29 @@ -; SPDX-License-Identifier: PMPL-1.0-or-later +;; SPDX-License-Identifier: MPL-2.0 ;; guix.scm — GNU Guix package definition for palimpsest-plasma -;; Usage: guix shell -f guix.scm +;; Usage: guix shell -f guix.scm (development shell) +;; +;; Note: cargo inputs are not vendored here — this definition provides a +;; development shell (rust + cargo), not a fully reproducible build. Vendor +;; the crate inputs if a reproducible `guix build` is needed. (use-modules (guix packages) - (guix build-system gnu) - (guix licenses)) + (guix build-system cargo) + (guix gexp) + ((guix licenses) #:prefix license:) + (gnu packages rust)) (package (name "palimpsest-plasma") - (version "0.1.0") - (source #f) - (build-system gnu-build-system) - (synopsis "palimpsest-plasma") - (description "palimpsest-plasma — part of the hyperpolymath ecosystem.") + (version "0.2.0") + (source (local-file "." "palimpsest-plasma-checkout" + #:recursive? #t)) + (build-system cargo-build-system) + (arguments '(#:tests? #f)) + (synopsis "Deterministic, typed policy engine") + (description + "palimpsest-plasma is a deterministic, typed policy engine: define +machine-readable deontic policies and evaluate repositories against them with +reproducible results. Includes an SPDX expression parser and zone-aware +license auditing.") (home-page "https://github.com/hyperpolymath/palimpsest-plasma") - (license ((@@ (guix licenses) license) "PMPL-1.0-or-later" - "https://github.com/hyperpolymath/palimpsest-license"))) + (license license:mpl2.0)) diff --git a/k9iser.toml b/k9iser.toml index 36968b3..c4d1c2b 100644 --- a/k9iser.toml +++ b/k9iser.toml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # k9iser manifest for palimpsest-plasma diff --git a/llm-warmup-dev.md b/llm-warmup-dev.md index 66bfcf1..ecd3e45 100644 --- a/llm-warmup-dev.md +++ b/llm-warmup-dev.md @@ -12,9 +12,9 @@ See README.adoc for overview. - `just build` — build the project - `just test` — run tests - `just doctor` — diagnose issues -- `just heal` — attempt auto-repair +- `just check` — run plasma against this repo (dogfood) ## Quick Context -- License: PMPL-1.0-or-later +- License: MPL-2.0 (code), CC-BY-SA-4.0 (docs) - Part of hyperpolymath ecosystem - See EXPLAINME.adoc for architecture diff --git a/llm-warmup-user.md b/llm-warmup-user.md index 07dddec..d1b6947 100644 --- a/llm-warmup-user.md +++ b/llm-warmup-user.md @@ -12,9 +12,9 @@ See README.adoc for overview. - `just build` — build the project - `just test` — run tests - `just doctor` — diagnose issues -- `just heal` — attempt auto-repair +- `just check` — run plasma against this repo (dogfood) ## Quick Context -- License: PMPL-1.0-or-later +- License: MPL-2.0 (code), CC-BY-SA-4.0 (docs) - Part of hyperpolymath ecosystem - See EXPLAINME.adoc for architecture diff --git a/mvp-v1-release.tar.gz b/mvp-v1-release.tar.gz deleted file mode 100644 index 1aa5b99..0000000 Binary files a/mvp-v1-release.tar.gz and /dev/null differ diff --git a/mvp-v1.1-release.tar.gz b/mvp-v1.1-release.tar.gz deleted file mode 100644 index 7c92651..0000000 Binary files a/mvp-v1.1-release.tar.gz and /dev/null differ diff --git a/plasma-engine/Cargo.toml b/plasma-engine/Cargo.toml new file mode 100644 index 0000000..854ea28 --- /dev/null +++ b/plasma-engine/Cargo.toml @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: MPL-2.0 +[package] +name = "plasma-engine" +version = "0.2.0" +edition = "2021" +authors = ["Jonathan D.A. Jewell "] +description = "Deterministic, typed policy evaluation — policy AST, fact collection, and a pure evaluator producing findings" +license = "MPL-2.0" +repository = "https://github.com/hyperpolymath/palimpsest-plasma" +keywords = ["policy", "compliance", "audit", "deontic", "agentic"] +categories = ["development-tools"] + +[dependencies] +plasma-parser = { path = "../plasma-parser" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" +walkdir = "2" +glob = "0.3" +thiserror = "2" + +[dev-dependencies] +tempfile = "3" diff --git a/plasma-engine/policies/repo-hygiene.plasma.toml b/plasma-engine/policies/repo-hygiene.plasma.toml new file mode 100644 index 0000000..aa040c7 --- /dev/null +++ b/plasma-engine/policies/repo-hygiene.plasma.toml @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: MPL-2.0 +# repo-hygiene — the policy plasma ships with and checks itself against. +# +# Demonstrates the v0 schema: deontic rules over repository subjects, +# with severities, rationales, and glob-based subject expansion. + +schema_version = { major = 0, minor = 1 } +id = "repo-hygiene" +version = { major = 0, minor = 1 } + +[[rules]] +id = "license-file-present" +modality = "obligation" +severity = "error" +subject = { type = "repo" } +resource = { type = "file", path = "LICENSE*" } +action = { type = "present" } +rationale = "Every repository must carry its license text." +source = "repo policy / REUSE spec" + +[[rules]] +id = "readme-present" +modality = "obligation" +severity = "error" +subject = { type = "repo" } +resource = { type = "file", path = "README*" } +action = { type = "present" } +rationale = "Every repository must explain what it is." + +[[rules]] +id = "spdx-header-required" +modality = "obligation" +severity = "warning" +subject = { type = "file-pattern", patterns = ["**/*.rs", "**/*.toml", "**/*.sh", "**/*.ex", "**/*.exs"] } +resource = { type = "header" } +action = { type = "present" } +rationale = "Machine-checkable licensing requires an SPDX header per source file." + +[[rules]] +id = "spdx-header-parses" +modality = "obligation" +severity = "warning" +subject = { type = "file-pattern", patterns = ["**/*.rs", "**/*.toml", "**/*.sh", "**/*.ex", "**/*.exs"] } +resource = { type = "header" } +condition = { type = "has-spdx-header" } +action = { type = "valid" } +rationale = "An SPDX header that does not parse is worse than none: it looks compliant without being checkable." diff --git a/plasma-engine/src/ast.rs b/plasma-engine/src/ast.rs new file mode 100644 index 0000000..f0b27cc --- /dev/null +++ b/plasma-engine/src/ast.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// Policy AST — typed, deontic policy representation. +// +// Transposed from docs/policy-ast-v0.1.adoc with documented divergences +// (see docs/engine-v0-design.adoc): exhibits are generalised to overlays, +// rules carry an explicit severity, and the condition vocabulary is +// license-agnostic. + +use crate::finding::Severity; +use serde::{Deserialize, Serialize}; + +/// Schema/policy version as a major.minor pair. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct PolicyVersion { + pub major: u32, + pub minor: u32, +} + +impl std::fmt::Display for PolicyVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.major, self.minor) + } +} + +/// The entity a rule is evaluated against. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum Subject { + /// The repository as a whole. + Repo, + /// A single file by relative path. + File { path: String }, + /// Every file matching any of the glob patterns. The `glob` crate has + /// no brace expansion, hence a list rather than `{a,b}` syntax. + FilePattern { patterns: Vec }, + /// A release by tag. Schema-reserved: rejected by the v0 loader. + Release { tag: String }, + /// A governance metadata field by key. + Metadata { key: String }, +} + +/// The artefact checked within or about a subject. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum Resource { + /// A file by relative path; the path may be a glob (e.g. "LICENSE*"). + File { path: String }, + /// The SPDX header of the subject file. + Header, + /// A manifest file by relative path. + Manifest { path: String }, + /// A governance metadata field by key. + GovernanceField { key: String }, + /// The release artefact. Schema-reserved: rejected by the v0 loader. + Release, +} + +/// Composable, total predicates over the fact set. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum Condition { + /// Always true (the default condition). + True, + /// Logical negation. + Not { of: Box }, + /// Logical conjunction over all sub-conditions. + All { of: Vec }, + /// Logical disjunction over the sub-conditions. + Any { of: Vec }, + /// At least one repository file matches the glob. + RepoHasFile { path: String }, + /// Content predicate. Schema-reserved: rejected by the v0 loader + /// (v0 facts carry no file contents). + FileMatchesPattern { path: String, pattern: String }, + /// The subject file has an SPDX header (false for non-file subjects). + HasSpdxHeader, + /// The subject file's SPDX header parses and equals this expression + /// textually after normalisation (false for non-file subjects). + SpdxLicenseIs { expr: String }, + /// The governance metadata key is set to exactly this value + /// (false when the key is absent). + GovernanceFlagSet { key: String, value: String }, + /// The `version` metadata value is at least this version + /// (false when absent or unparsable). + VersionAtLeast { version: String }, +} + +impl Condition { + pub(crate) fn always_true() -> Condition { + Condition::True + } +} + +/// Deontic modality: what the rule obligates, prohibits, or permits. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Modality { + Obligation, + Prohibition, + Permission, +} + +impl std::fmt::Display for Modality { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Modality::Obligation => write!(f, "obligation"), + Modality::Prohibition => write!(f, "prohibition"), + Modality::Permission => write!(f, "permission"), + } + } +} + +/// What the rule asserts about the subject-resource pair. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum ActionKind { + /// The resource must exist. + Present, + /// The resource must not exist. + Absent, + /// The resource must be structurally valid (for headers: parse as SPDX). + Valid, + /// Consistency with an external decision. Schema-reserved: rejected by + /// the v0 loader. + ConsistentWith { id: String }, +} + +fn default_condition() -> Condition { + Condition::always_true() +} + +fn default_severity() -> Severity { + Severity::Error +} + +/// The fundamental unit of policy: a deontic modality bound to a subject, +/// resource, condition, and action, with optional narrative provenance. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Rule { + pub id: String, + pub modality: Modality, + #[serde(default = "default_severity")] + pub severity: Severity, + pub subject: Subject, + pub resource: Resource, + #[serde(default = "default_condition")] + pub condition: Condition, + pub action: ActionKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rationale: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +/// An overlay effect: how an overlay extends or modifies the base rules. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum OverlayEffect { + /// Add rules to the effective rule set. + AddRules { rules: Vec }, + /// Modify existing rules by id. Schema-reserved: rejected by the v0 loader. + ModifyRules { ids: Vec }, + /// Override existing rules by id. Schema-reserved: rejected by the v0 loader. + OverrideRules { ids: Vec }, +} + +/// An overlay: a named, additive policy extension (the generalisation of the +/// original design's license "exhibits"). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Overlay { + pub id: String, + #[serde(default)] + pub applies_to: Vec, + pub effects: Vec, +} + +/// A policy: versioned schema, versioned content, base rules, and overlays. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Policy { + /// Schema version this document is written against. Checked at load. + pub schema_version: PolicyVersion, + pub id: String, + /// The policy's own content version. + pub version: PolicyVersion, + pub rules: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub overlays: Vec, +} diff --git a/plasma-engine/src/eval.rs b/plasma-engine/src/eval.rs new file mode 100644 index 0000000..0778213 --- /dev/null +++ b/plasma-engine/src/eval.rs @@ -0,0 +1,597 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// Policy evaluation — pure and total. +// +// `evaluate` is a pure function Policy × FactSet → Evaluation: no IO, no +// clocks, no randomness, no environment reads. Every partial case has a +// defined false/absent semantics (documented in docs/engine-v0-design.adoc), +// so any policy that passed schema validation evaluates without panicking. +// Facts iterate in BTree order, so findings arrive in deterministic order. + +use crate::ast::{ActionKind, Condition, Modality, OverlayEffect, Policy, Resource, Rule, Subject}; +use crate::facts::FactSet; +use crate::finding::{Evaluation, Finding, FindingStatus, Severity, Summary}; +use plasma_parser::spdx::parse_spdx_expr; + +/// A concrete instance a rule is checked against. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Instance { + Repo, + Path(String), + Metadata(String), +} + +impl Instance { + fn label(&self) -> String { + match self { + Instance::Repo => "repo".to_string(), + Instance::Path(p) => p.clone(), + Instance::Metadata(k) => format!("metadata:{k}"), + } + } +} + +/// Evaluate a policy against a fact set. +pub fn evaluate(policy: &Policy, facts: &FactSet) -> Evaluation { + let mut findings = Vec::new(); + + for (rule, origin) in effective_rules(policy) { + for instance in expand_subject(&rule.subject, facts) { + if !eval_condition(&rule.condition, &instance, facts) { + continue; // rule not applicable to this instance + } + findings.push(check_rule(rule, origin, &instance, facts)); + } + } + + let mut summary = Summary::default(); + for finding in &findings { + match finding.status { + FindingStatus::Pass => summary.passes += 1, + FindingStatus::Violation => match finding.severity { + Severity::Error => summary.errors += 1, + Severity::Warning => summary.warnings += 1, + Severity::Info => summary.info += 1, + }, + } + } + + Evaluation { + root: facts.root.clone(), + policy_id: policy.id.clone(), + policy_version: policy.version, + schema_version: policy.schema_version, + findings, + summary, + } +} + +/// Base rules followed by overlay-added rules, each with its origin +/// (None = base policy, Some(id) = overlay). +fn effective_rules(policy: &Policy) -> Vec<(&Rule, Option<&str>)> { + let mut rules: Vec<(&Rule, Option<&str>)> = policy.rules.iter().map(|r| (r, None)).collect(); + for overlay in &policy.overlays { + for effect in &overlay.effects { + if let OverlayEffect::AddRules { rules: added } = effect { + rules.extend(added.iter().map(|r| (r, Some(overlay.id.as_str())))); + } + } + } + rules +} + +/// Expand a subject to its concrete instances over the fact set. +fn expand_subject(subject: &Subject, facts: &FactSet) -> Vec { + match subject { + Subject::Repo => vec![Instance::Repo], + Subject::File { path } => vec![Instance::Path(path.clone())], + Subject::FilePattern { patterns } => facts + .files + .iter() + .filter(|file| patterns.iter().any(|p| glob_matches(p, file))) + .map(|file| Instance::Path(file.clone())) + .collect(), + Subject::Metadata { key } => vec![Instance::Metadata(key.clone())], + // Rejected at load; unreachable for validated policies. Defined + // as empty to keep evaluation total regardless. + Subject::Release { .. } => vec![], + } +} + +/// Glob matching over '/'-separated relative paths. +/// +/// `**/` prefixes also match zero directories: "**/*.rs" matches both +/// "src/main.rs" and "main.rs". +fn glob_matches(pattern: &str, path: &str) -> bool { + match glob::Pattern::new(pattern) { + Ok(p) => { + if p.matches(path) { + return true; + } + // glob's "**/" requires at least one component; allow zero. + if let Some(rest) = pattern.strip_prefix("**/") { + if let Ok(p2) = glob::Pattern::new(rest) { + return p2.matches(path); + } + } + false + } + Err(_) => false, // malformed pattern matches nothing (documented) + } +} + +/// Total condition semantics over (facts, instance). +fn eval_condition(condition: &Condition, instance: &Instance, facts: &FactSet) -> bool { + match condition { + Condition::True => true, + Condition::Not { of } => !eval_condition(of, instance, facts), + Condition::All { of } => of.iter().all(|c| eval_condition(c, instance, facts)), + Condition::Any { of } => of.iter().any(|c| eval_condition(c, instance, facts)), + Condition::RepoHasFile { path } => facts.files.iter().any(|f| glob_matches(path, f)), + Condition::HasSpdxHeader => match instance { + Instance::Path(p) => matches!(facts.spdx_headers.get(p), Some(Some(_))), + _ => false, + }, + Condition::SpdxLicenseIs { expr } => match instance { + Instance::Path(p) => match facts.spdx_headers.get(p) { + Some(Some(raw)) => spdx_equal(raw, expr), + _ => false, + }, + _ => false, + }, + Condition::GovernanceFlagSet { key, value } => { + facts.metadata.get(key).map(|v| v == value).unwrap_or(false) + } + Condition::VersionAtLeast { version } => facts + .metadata + .get("version") + .and_then(|actual| compare_versions(actual, version)) + .unwrap_or(false), + // Rejected at load; defined as false to keep evaluation total. + Condition::FileMatchesPattern { .. } => false, + } +} + +/// Two SPDX expressions are equal when both parse and their parsed forms +/// agree; unparsable expressions compare by exact text. +fn spdx_equal(actual: &str, expected: &str) -> bool { + match (parse_spdx_expr(actual), parse_spdx_expr(expected)) { + (Ok(a), Ok(b)) => a == b, + _ => actual.trim() == expected.trim(), + } +} + +/// Numeric dotted-version comparison: Some(actual >= minimum), None when +/// either side has no leading numeric components. +fn compare_versions(actual: &str, minimum: &str) -> Option { + let parse = |s: &str| -> Option> { + let parts: Vec = s + .trim() + .split('.') + .map_while(|p| { + let digits: String = p.chars().take_while(|c| c.is_ascii_digit()).collect(); + digits.parse().ok() + }) + .collect(); + if parts.is_empty() { + None + } else { + Some(parts) + } + }; + let a = parse(actual)?; + let m = parse(minimum)?; + // Compare component-wise, treating missing components as zero. + let len = a.len().max(m.len()); + for i in 0..len { + let av = a.get(i).copied().unwrap_or(0); + let mv = m.get(i).copied().unwrap_or(0); + if av != mv { + return Some(av > mv); + } + } + Some(true) +} + +/// Does the resource exist for this instance? +fn resource_exists(resource: &Resource, instance: &Instance, facts: &FactSet) -> bool { + match resource { + Resource::File { path } | Resource::Manifest { path } => { + facts.files.iter().any(|f| glob_matches(path, f)) + } + Resource::Header => match instance { + Instance::Path(p) => matches!(facts.spdx_headers.get(p), Some(Some(_))), + _ => false, + }, + Resource::GovernanceField { key } => facts.metadata.contains_key(key), + // Rejected at load; defined as absent to keep evaluation total. + Resource::Release => false, + } +} + +/// Is the resource structurally valid for this instance? Validity implies +/// existence. For headers, validity means the SPDX expression parses; for +/// everything else in v0, validity coincides with existence. +fn resource_valid(resource: &Resource, instance: &Instance, facts: &FactSet) -> bool { + match resource { + Resource::Header => match instance { + Instance::Path(p) => match facts.spdx_headers.get(p) { + Some(Some(raw)) => parse_spdx_expr(raw).is_ok(), + _ => false, + }, + _ => false, + }, + _ => resource_exists(resource, instance, facts), + } +} + +fn describe_resource(resource: &Resource) -> String { + match resource { + Resource::File { path } => format!("file {path:?}"), + Resource::Header => "SPDX header".to_string(), + Resource::Manifest { path } => format!("manifest {path:?}"), + Resource::GovernanceField { key } => format!("governance field {key:?}"), + Resource::Release => "release".to_string(), + } +} + +/// Apply the deontic matrix for one (rule, instance) pair. +fn check_rule(rule: &Rule, origin: Option<&str>, instance: &Instance, facts: &FactSet) -> Finding { + let exists = resource_exists(&rule.resource, instance, facts); + let resource_desc = describe_resource(&rule.resource); + + let (violated, message) = match (&rule.modality, &rule.action) { + (Modality::Permission, _) => ( + false, + format!("{resource_desc} is permitted for {}", instance.label()), + ), + (Modality::Obligation, ActionKind::Present) => ( + !exists, + if exists { + format!("{resource_desc} present for {}", instance.label()) + } else { + format!("{resource_desc} missing for {}", instance.label()) + }, + ), + (Modality::Obligation, ActionKind::Absent) => ( + exists, + if exists { + format!( + "{resource_desc} present for {} but must be absent", + instance.label() + ) + } else { + format!("{resource_desc} absent for {}", instance.label()) + }, + ), + (Modality::Obligation, ActionKind::Valid) => { + let valid = resource_valid(&rule.resource, instance, facts); + ( + !valid, + if valid { + format!("{resource_desc} valid for {}", instance.label()) + } else { + format!("{resource_desc} invalid for {}", instance.label()) + }, + ) + } + (Modality::Prohibition, ActionKind::Present) => ( + exists, + if exists { + format!( + "{resource_desc} present for {} but is prohibited", + instance.label() + ) + } else { + format!("{resource_desc} absent for {}", instance.label()) + }, + ), + (Modality::Prohibition, ActionKind::Absent) => ( + !exists, + if exists { + format!("{resource_desc} present for {}", instance.label()) + } else { + format!( + "{resource_desc} absent for {} but its absence is prohibited", + instance.label() + ) + }, + ), + (Modality::Prohibition, ActionKind::Valid) => { + let valid = resource_valid(&rule.resource, instance, facts); + ( + valid, + if valid { + format!( + "{resource_desc} valid for {} but validity is prohibited", + instance.label() + ) + } else { + format!("{resource_desc} not valid for {}", instance.label()) + }, + ) + } + // ConsistentWith is rejected at load; treated as a pass to keep + // evaluation total. + (_, ActionKind::ConsistentWith { .. }) => ( + false, + format!("consistency check skipped for {}", instance.label()), + ), + }; + + let source = rule + .source + .clone() + .or_else(|| origin.map(|overlay_id| format!("overlay:{overlay_id}"))); + + Finding { + rule_id: rule.id.clone(), + modality: rule.modality, + severity: rule.severity, + subject: instance.label(), + message, + rationale: rule.rationale.clone(), + source, + status: if violated { + FindingStatus::Violation + } else { + FindingStatus::Pass + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schema::{load_policy_str, PolicyFormat}; + use std::collections::{BTreeMap, BTreeSet}; + + fn facts_with(files: &[&str], headers: &[(&str, Option<&str>)]) -> FactSet { + FactSet { + root: "test-root".to_string(), + files: files.iter().map(|s| s.to_string()).collect::>(), + spdx_headers: headers + .iter() + .map(|(k, v)| (k.to_string(), v.map(|s| s.to_string()))) + .collect::>(), + metadata: BTreeMap::new(), + git: Default::default(), + } + } + + fn policy(doc: &str) -> Policy { + load_policy_str(doc, PolicyFormat::Toml).unwrap() + } + + #[test] + fn test_glob_zero_component_doublestar() { + assert!(glob_matches("**/*.rs", "main.rs")); + assert!(glob_matches("**/*.rs", "src/deep/main.rs")); + assert!(!glob_matches("**/*.rs", "main.toml")); + assert!(glob_matches("LICENSE*", "LICENSE")); + assert!(!glob_matches("LICENSE*", "docs/LICENSE")); + } + + #[test] + fn test_obligation_present_violation_and_pass() { + let p = policy( + r#" +schema_version = { major = 0, minor = 1 } +id = "t" +version = { major = 1, minor = 0 } + +[[rules]] +id = "license-present" +modality = "obligation" +subject = { type = "repo" } +resource = { type = "file", path = "LICENSE*" } +action = { type = "present" } +"#, + ); + let bad = evaluate(&p, &facts_with(&["README.md"], &[])); + assert_eq!(bad.summary.errors, 1); + assert_eq!(bad.findings[0].status, FindingStatus::Violation); + + let good = evaluate(&p, &facts_with(&["LICENSE", "README.md"], &[])); + assert_eq!(good.summary.errors, 0); + assert_eq!(good.summary.passes, 1); + } + + #[test] + fn test_prohibition_present() { + let p = policy( + r#" +schema_version = { major = 0, minor = 1 } +id = "t" +version = { major = 1, minor = 0 } + +[[rules]] +id = "no-lockfiles" +modality = "prohibition" +severity = "warning" +subject = { type = "repo" } +resource = { type = "file", path = "package-lock.json" } +action = { type = "present" } +"#, + ); + let bad = evaluate(&p, &facts_with(&["package-lock.json"], &[])); + assert_eq!(bad.summary.warnings, 1); + + let good = evaluate(&p, &facts_with(&["Cargo.lock"], &[])); + assert_eq!(good.summary.warnings, 0); + assert_eq!(good.summary.passes, 1); + } + + #[test] + fn test_file_pattern_header_rule() { + let p = policy( + r#" +schema_version = { major = 0, minor = 1 } +id = "t" +version = { major = 1, minor = 0 } + +[[rules]] +id = "headers" +modality = "obligation" +severity = "warning" +subject = { type = "file-pattern", patterns = ["**/*.rs"] } +resource = { type = "header" } +action = { type = "present" } +"#, + ); + let facts = facts_with( + &["src/a.rs", "src/b.rs", "README.md"], + &[("src/a.rs", Some("MPL-2.0")), ("src/b.rs", None)], + ); + let eval = evaluate(&p, &facts); + assert_eq!(eval.summary.warnings, 1); + assert_eq!(eval.summary.passes, 1); + let violation = eval + .findings + .iter() + .find(|f| f.status == FindingStatus::Violation) + .unwrap(); + assert_eq!(violation.subject, "src/b.rs"); + } + + #[test] + fn test_header_valid_action() { + let p = policy( + r#" +schema_version = { major = 0, minor = 1 } +id = "t" +version = { major = 1, minor = 0 } + +[[rules]] +id = "headers-parse" +modality = "obligation" +severity = "warning" +subject = { type = "file-pattern", patterns = ["**/*.rs"] } +resource = { type = "header" } +condition = { type = "has-spdx-header" } +action = { type = "valid" } +"#, + ); + let facts = facts_with( + &["good.rs", "bad.rs", "none.rs"], + &[ + ("good.rs", Some("MIT OR Apache-2.0")), + ("bad.rs", Some("NOT ) A ( LICENSE")), + ("none.rs", None), + ], + ); + let eval = evaluate(&p, &facts); + // none.rs is filtered out by the condition; bad.rs violates. + assert_eq!(eval.summary.warnings, 1); + assert_eq!(eval.summary.passes, 1); + } + + #[test] + fn test_condition_gates_rule() { + let p = policy( + r#" +schema_version = { major = 0, minor = 1 } +id = "t" +version = { major = 1, minor = 0 } + +[[rules]] +id = "changelog-once-versioned" +modality = "obligation" +subject = { type = "repo" } +resource = { type = "file", path = "CHANGELOG*" } +condition = { type = "version-at-least", version = "1.0" } +action = { type = "present" } +"#, + ); + // No version metadata: condition false, rule not applicable. + let eval = evaluate(&p, &facts_with(&["README.md"], &[])); + assert_eq!(eval.findings.len(), 0); + + // Version >= 1.0: rule applies and is violated. + let mut facts = facts_with(&["README.md"], &[]); + facts + .metadata + .insert("version".to_string(), "1.2.0".to_string()); + let eval = evaluate(&p, &facts); + assert_eq!(eval.summary.errors, 1); + } + + #[test] + fn test_permission_never_violates() { + let p = policy( + r#" +schema_version = { major = 0, minor = 1 } +id = "t" +version = { major = 1, minor = 0 } + +[[rules]] +id = "vendoring-permitted" +modality = "permission" +subject = { type = "repo" } +resource = { type = "file", path = "vendor/**" } +action = { type = "present" } +"#, + ); + let eval = evaluate(&p, &facts_with(&[], &[])); + assert_eq!(eval.summary.errors, 0); + assert_eq!(eval.summary.passes, 1); + } + + #[test] + fn test_overlay_rules_carry_origin() { + let p = policy( + r#" +schema_version = { major = 0, minor = 1 } +id = "t" +version = { major = 1, minor = 0 } +rules = [] + +[[overlays]] +id = "extra" +applies_to = [] + +[[overlays.effects]] +type = "add-rules" + +[[overlays.effects.rules]] +id = "extra-rule" +modality = "obligation" +subject = { type = "repo" } +resource = { type = "file", path = "SECURITY.md" } +action = { type = "present" } +"#, + ); + let eval = evaluate(&p, &facts_with(&[], &[])); + assert_eq!(eval.summary.errors, 1); + assert_eq!(eval.findings[0].source.as_deref(), Some("overlay:extra")); + } + + #[test] + fn test_determinism_identical_runs() { + let p = crate::schema::builtin_repo_hygiene(); + let facts = facts_with( + &["LICENSE", "README.adoc", "src/main.rs", "Cargo.toml"], + &[ + ("src/main.rs", Some("MPL-2.0")), + ("Cargo.toml", Some("MPL-2.0")), + ], + ); + let a = evaluate(&p, &facts); + let b = evaluate(&p, &facts); + assert_eq!(a, b); + assert_eq!( + serde_json::to_string(&a).unwrap(), + serde_json::to_string(&b).unwrap() + ); + } + + #[test] + fn test_version_compare() { + assert_eq!(compare_versions("1.2.3", "1.0"), Some(true)); + assert_eq!(compare_versions("0.9", "1.0"), Some(false)); + assert_eq!(compare_versions("1.0.0", "1.0"), Some(true)); + assert_eq!(compare_versions("2", "1.9.9"), Some(true)); + assert_eq!(compare_versions("garbage", "1.0"), None); + } +} diff --git a/plasma-engine/src/facts.rs b/plasma-engine/src/facts.rs new file mode 100644 index 0000000..900bca6 --- /dev/null +++ b/plasma-engine/src/facts.rs @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// Fact collection — the only impure module in this crate. +// +// Everything the evaluator may know about a repository flows through the +// FactSet collected here. Collection rules (walk order, skip list, header +// window) are pinned in docs/engine-v0-design.adoc so an independent +// implementation can reproduce identical facts. All collections are +// BTree-ordered so downstream evaluation is deterministic. + +use plasma_parser::audit::header::{extract_spdx_raw_from_content, is_auditable}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::Path; +use thiserror::Error; + +/// Errors produced during fact collection. +#[derive(Debug, Error)] +pub enum FactError { + #[error("path does not exist: {0}")] + MissingRoot(String), + #[error("io error while collecting facts: {0}")] + Io(#[from] std::io::Error), +} + +/// Git facts read directly from `.git/HEAD` (no subprocess). +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct GitFacts { + pub is_repo: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub head_ref: Option, +} + +/// A deterministic snapshot of repository state. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FactSet { + /// Repository root as given by the caller. + pub root: String, + /// All regular files, relative to root, '/'-separated, sorted. + pub files: BTreeSet, + /// Raw SPDX header expression per auditable file (None = no header). + pub spdx_headers: BTreeMap>, + /// Governance metadata: v0 collects `version` from Cargo.toml when present. + pub metadata: BTreeMap, + pub git: GitFacts, +} + +/// Directory/file names skipped during the walk. Identical to the audit +/// CLI's skip list; pinned in the design doc. +fn skip_entry(name: &str) -> bool { + name.starts_with('.') + || name == "target" + || name == "node_modules" + || name == "vendor" + || name == "_build" + || name == "deps" +} + +/// Collect a [`FactSet`] from a repository tree. +pub fn collect(root: &Path) -> Result { + if !root.exists() { + return Err(FactError::MissingRoot(root.display().to_string())); + } + + let mut files = BTreeSet::new(); + let mut spdx_headers = BTreeMap::new(); + + for entry in walkdir::WalkDir::new(root) + .into_iter() + .filter_entry(|e| { + // Never skip the root itself, whatever it is named. + e.depth() == 0 || !skip_entry(e.file_name().to_str().unwrap_or("")) + }) + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + { + let rel = match entry.path().strip_prefix(root) { + Ok(p) => p, + Err(_) => continue, + }; + let rel_str = rel + .components() + .map(|c| c.as_os_str().to_string_lossy()) + .collect::>() + .join("/"); + if rel_str.is_empty() { + continue; + } + + let ext = rel.extension().and_then(|e| e.to_str()).unwrap_or(""); + if is_auditable(ext) { + let header = fs::read_to_string(entry.path()) + .ok() + .and_then(|content| extract_spdx_raw_from_content(&content)); + spdx_headers.insert(rel_str.clone(), header); + } + + files.insert(rel_str); + } + + let mut metadata = BTreeMap::new(); + if let Ok(cargo) = fs::read_to_string(root.join("Cargo.toml")) { + if let Some(version) = cargo_package_version(&cargo) { + metadata.insert("version".to_string(), version); + } + } + + Ok(FactSet { + root: root.display().to_string(), + files, + spdx_headers, + metadata, + git: read_git_facts(root), + }) +} + +/// Extract `[package] version = "..."` from Cargo.toml content. +fn cargo_package_version(content: &str) -> Option { + let value: toml::Value = content.parse().ok()?; + value + .get("package")? + .get("version")? + .as_str() + .map(|s| s.to_string()) +} + +/// Read `.git/HEAD` directly; no subprocess, no clock. +fn read_git_facts(root: &Path) -> GitFacts { + let head_path = root.join(".git").join("HEAD"); + match fs::read_to_string(&head_path) { + Ok(content) => { + let head = content.trim(); + let head_ref = head + .strip_prefix("ref: ") + .map(|r| r.to_string()) + .or_else(|| Some(head.to_string())) + .filter(|s| !s.is_empty()); + GitFacts { + is_repo: true, + head_ref, + } + } + Err(_) => GitFacts::default(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_collect_basic() { + let dir = tempfile::tempdir().unwrap(); + fs::write(dir.path().join("README.md"), "# hi\n").unwrap(); + fs::write( + dir.path().join("main.rs"), + "// SPDX-License-Identifier: MPL-2.0\nfn main() {}\n", + ) + .unwrap(); + fs::write(dir.path().join("plain.rs"), "fn main() {}\n").unwrap(); + fs::create_dir(dir.path().join("target")).unwrap(); + fs::write(dir.path().join("target").join("skip.rs"), "").unwrap(); + + let facts = collect(dir.path()).unwrap(); + assert!(facts.files.contains("README.md")); + assert!(facts.files.contains("main.rs")); + assert!(!facts.files.contains("target/skip.rs")); + assert_eq!( + facts.spdx_headers.get("main.rs"), + Some(&Some("MPL-2.0".to_string())) + ); + assert_eq!(facts.spdx_headers.get("plain.rs"), Some(&None)); + // README.md is not an auditable extension. + assert!(!facts.spdx_headers.contains_key("README.md")); + assert!(!facts.git.is_repo); + } + + #[test] + fn test_cargo_version_metadata() { + let dir = tempfile::tempdir().unwrap(); + fs::write( + dir.path().join("Cargo.toml"), + "[package]\nname = \"x\"\nversion = \"1.2.3\"\n", + ) + .unwrap(); + let facts = collect(dir.path()).unwrap(); + assert_eq!(facts.metadata.get("version"), Some(&"1.2.3".to_string())); + } + + #[test] + fn test_missing_root() { + assert!(collect(Path::new("/nonexistent/nowhere")).is_err()); + } +} diff --git a/plasma-engine/src/finding.rs b/plasma-engine/src/finding.rs new file mode 100644 index 0000000..1fea052 --- /dev/null +++ b/plasma-engine/src/finding.rs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// Findings — the evaluator's output vocabulary. + +use crate::ast::{Modality, PolicyVersion}; +use serde::{Deserialize, Serialize}; + +/// Finding severity, ordered from most to least severe. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Severity { + Error, + Warning, + Info, +} + +impl std::fmt::Display for Severity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Severity::Error => write!(f, "error"), + Severity::Warning => write!(f, "warning"), + Severity::Info => write!(f, "info"), + } + } +} + +/// Whether a rule was violated or satisfied for a subject instance. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum FindingStatus { + Violation, + Pass, +} + +/// One evaluated (rule, subject instance) outcome. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Finding { + /// The id of the rule that produced this finding. + pub rule_id: String, + pub modality: Modality, + pub severity: Severity, + /// Concrete subject instance: "repo", a relative file path, or a + /// metadata key. + pub subject: String, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rationale: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source: Option, + pub status: FindingStatus, +} + +/// Violation counts by severity, plus satisfied-rule count. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Summary { + pub errors: u32, + pub warnings: u32, + pub info: u32, + pub passes: u32, +} + +/// The complete, deterministic result of evaluating a policy against facts. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Evaluation { + /// Repository root the facts were collected from. + pub root: String, + pub policy_id: String, + pub policy_version: PolicyVersion, + pub schema_version: PolicyVersion, + pub findings: Vec, + pub summary: Summary, +} + +impl Evaluation { + /// True when no violation at or above the given severity exists. + pub fn clean_at(&self, threshold: Severity) -> bool { + !self + .findings + .iter() + .any(|f| f.status == FindingStatus::Violation && f.severity <= threshold) + } +} diff --git a/plasma-engine/src/lib.rs b/plasma-engine/src/lib.rs new file mode 100644 index 0000000..d624ff4 --- /dev/null +++ b/plasma-engine/src/lib.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// plasma-engine — deterministic, typed policy evaluation. +// +// This crate provides: +// - A typed policy AST (deontic rules over repository subjects and resources) +// - Versioned policy schema loading (TOML and JSON) +// - Fact collection from a repository tree (the only impure module) +// - A pure, total evaluator: Policy × FactSet → Evaluation +// - Human, JSON, and SARIF report rendering +// +// Design contract (see docs/engine-v0-design.adoc): evaluation is +// deterministic and total — identical policy and facts produce identical +// findings in identical order, with no clocks, randomness, or ambient state. + +#![forbid(unsafe_code)] + +pub mod ast; +pub mod eval; +pub mod facts; +pub mod finding; +pub mod report; +pub mod schema; + +pub use ast::{ + ActionKind, Condition, Modality, Overlay, Policy, PolicyVersion, Resource, Rule, Subject, +}; +pub use eval::evaluate; +pub use facts::{collect, FactError, FactSet, GitFacts}; +pub use finding::{Evaluation, Finding, FindingStatus, Severity, Summary}; +pub use schema::{ + builtin_repo_hygiene, load_policy, load_policy_str, PolicyFormat, SchemaError, SCHEMA_VERSION, +}; diff --git a/plasma-engine/src/report/human.rs b/plasma-engine/src/report/human.rs new file mode 100644 index 0000000..6e10aef --- /dev/null +++ b/plasma-engine/src/report/human.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// Human-readable rendering of an Evaluation. + +use crate::finding::{Evaluation, FindingStatus}; + +/// Render an evaluation as human-readable text. +/// +/// Violations are always shown; satisfied (pass) findings only with +/// `verbose`. +pub fn to_human(eval: &Evaluation, verbose: bool) -> String { + let mut out = String::new(); + + out.push_str(&format!( + "Policy {} v{} (schema {}) against {}\n\n", + eval.policy_id, eval.policy_version, eval.schema_version, eval.root + )); + + let mut shown = 0usize; + for finding in &eval.findings { + match finding.status { + FindingStatus::Violation => { + out.push_str(&format!( + " {}: [{}] {} — {}\n", + finding.severity.to_string().to_uppercase(), + finding.rule_id, + finding.subject, + finding.message + )); + if let Some(rationale) = &finding.rationale { + out.push_str(&format!(" rationale: {rationale}\n")); + } + if let Some(source) = &finding.source { + out.push_str(&format!(" source: {source}\n")); + } + shown += 1; + } + FindingStatus::Pass if verbose => { + out.push_str(&format!( + " PASS: [{}] {} — {}\n", + finding.rule_id, finding.subject, finding.message + )); + shown += 1; + } + FindingStatus::Pass => {} + } + } + + if shown == 0 { + out.push_str(" No violations.\n"); + } + + out.push_str(&format!( + "\nSummary: {} error(s), {} warning(s), {} info, {} rule(s) satisfied\n", + eval.summary.errors, eval.summary.warnings, eval.summary.info, eval.summary.passes + )); + + out +} diff --git a/plasma-engine/src/report/mod.rs b/plasma-engine/src/report/mod.rs new file mode 100644 index 0000000..c709440 --- /dev/null +++ b/plasma-engine/src/report/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// Report rendering — human text and SARIF views of an Evaluation. +// (JSON is served by Evaluation's Serialize implementation.) + +pub mod human; +pub mod sarif; + +pub use human::to_human; +pub use sarif::to_sarif; diff --git a/plasma-engine/src/report/sarif.rs b/plasma-engine/src/report/sarif.rs new file mode 100644 index 0000000..2272da1 --- /dev/null +++ b/plasma-engine/src/report/sarif.rs @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// SARIF output — renders an Evaluation as SARIF v2.1.0 for GitHub Code +// Scanning and sibling tools. Rule ids are namespaced "plasma/" +// and stable across runs: they are the interchange contract consumed by +// forensic and claim-verification tooling. +// +// SARIF spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html + +use crate::finding::{Evaluation, Finding, FindingStatus, Severity}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; + +/// SARIF `level` vocabulary: error | warning | note (never "info"). +fn sarif_level(severity: Severity) -> &'static str { + match severity { + Severity::Error => "error", + Severity::Warning => "warning", + Severity::Info => "note", + } +} + +fn result_for(finding: &Finding) -> Value { + let mut result = json!({ + "ruleId": format!("plasma/{}", finding.rule_id), + "message": { "text": finding.message }, + }); + + match finding.status { + FindingStatus::Violation => { + result["level"] = json!(sarif_level(finding.severity)); + } + FindingStatus::Pass => { + result["kind"] = json!("pass"); + result["level"] = json!("none"); + } + } + + // Repo- and metadata-scoped findings have no artifact location; + // SARIF permits results without locations. + if finding.subject != "repo" && !finding.subject.starts_with("metadata:") { + result["locations"] = json!([{ + "physicalLocation": { + "artifactLocation": { "uri": finding.subject }, + "region": { "startLine": 1 } + } + }]); + } + + result +} + +/// Render an evaluation as a SARIF v2.1.0 document. +/// +/// Violations are always included; pass findings (`kind: "pass"`) only +/// with `include_passes`. +pub fn to_sarif(eval: &Evaluation, include_passes: bool) -> Value { + // One SARIF rule per policy rule id, in deterministic order. + let mut rules: BTreeMap = BTreeMap::new(); + for finding in &eval.findings { + let id = format!("plasma/{}", finding.rule_id); + rules.entry(id.clone()).or_insert_with(|| { + let mut rule = json!({ "id": id }); + if let Some(rationale) = &finding.rationale { + rule["shortDescription"] = json!({ "text": rationale }); + } + rule + }); + } + + let results: Vec = eval + .findings + .iter() + .filter(|f| include_passes || f.status == FindingStatus::Violation) + .map(result_for) + .collect(); + + json!({ + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [{ + "tool": { + "driver": { + "name": "plasma", + "version": env!("CARGO_PKG_VERSION"), + "informationUri": "https://github.com/hyperpolymath/palimpsest-plasma", + "rules": rules.into_values().collect::>() + } + }, + "results": results + }] + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{Modality, PolicyVersion}; + use crate::finding::{Evaluation, Finding, Summary}; + + #[test] + fn test_sarif_shape() { + let eval = Evaluation { + root: ".".to_string(), + policy_id: "t".to_string(), + policy_version: PolicyVersion { major: 1, minor: 0 }, + schema_version: PolicyVersion { major: 0, minor: 1 }, + findings: vec![Finding { + rule_id: "license-file-present".to_string(), + modality: Modality::Obligation, + severity: Severity::Error, + subject: "repo".to_string(), + message: "file \"LICENSE*\" missing for repo".to_string(), + rationale: Some("Every repository must carry its license text.".to_string()), + source: None, + status: FindingStatus::Violation, + }], + summary: Summary { + errors: 1, + ..Default::default() + }, + }; + + let sarif = to_sarif(&eval, false); + assert_eq!(sarif["version"], "2.1.0"); + assert_eq!(sarif["runs"][0]["tool"]["driver"]["name"], "plasma"); + let result = &sarif["runs"][0]["results"][0]; + assert_eq!(result["ruleId"], "plasma/license-file-present"); + assert_eq!(result["level"], "error"); + // Repo-scoped: no artifact location. + assert!(result.get("locations").is_none()); + } +} diff --git a/plasma-engine/src/schema.rs b/plasma-engine/src/schema.rs new file mode 100644 index 0000000..405dae6 --- /dev/null +++ b/plasma-engine/src/schema.rs @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// Policy schema — versioned loading and validation. +// +// Every construct the v0 evaluator cannot give exact semantics to is +// rejected HERE, at load time, never mid-evaluation. This keeps +// `evaluate` total: any policy that loads, evaluates. + +use crate::ast::{ + ActionKind, Condition, OverlayEffect, Policy, PolicyVersion, Resource, Rule, Subject, +}; +use std::path::Path; +use thiserror::Error; + +/// The policy schema version this engine implements. +pub const SCHEMA_VERSION: PolicyVersion = PolicyVersion { major: 0, minor: 1 }; + +/// Policy document formats accepted by the loader. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PolicyFormat { + Toml, + Json, +} + +/// Errors produced while loading or validating a policy document. +#[derive(Debug, Error)] +pub enum SchemaError { + #[error("cannot read policy file: {0}")] + Io(#[from] std::io::Error), + #[error("cannot parse policy TOML: {0}")] + Toml(#[from] toml::de::Error), + #[error("cannot parse policy JSON: {0}")] + Json(#[from] serde_json::Error), + #[error( + "unsupported schema version {found} (this engine implements {supported}); \ + migrate the policy or upgrade the engine" + )] + UnsupportedSchemaVersion { + found: PolicyVersion, + supported: PolicyVersion, + }, + #[error("rule {rule_id}: {construct} is schema-reserved and not evaluable in v0")] + ReservedConstruct { rule_id: String, construct: String }, + #[error("overlay {overlay_id}: {construct} is schema-reserved and not evaluable in v0")] + ReservedOverlayEffect { + overlay_id: String, + construct: String, + }, + #[error("duplicate rule id: {0}")] + DuplicateRuleId(String), + #[error("unknown policy file extension {0:?}: use .toml or .json")] + UnknownExtension(String), +} + +/// Load and validate a policy from a file, picking the format by extension. +pub fn load_policy(path: &Path) -> Result { + let format = match path.extension().and_then(|e| e.to_str()) { + Some("toml") => PolicyFormat::Toml, + Some("json") => PolicyFormat::Json, + other => { + return Err(SchemaError::UnknownExtension( + other.unwrap_or("").to_string(), + )) + } + }; + let content = std::fs::read_to_string(path)?; + load_policy_str(&content, format) +} + +/// Load and validate a policy from a string in the given format. +pub fn load_policy_str(content: &str, format: PolicyFormat) -> Result { + let policy: Policy = match format { + PolicyFormat::Toml => toml::from_str(content)?, + PolicyFormat::Json => serde_json::from_str(content)?, + }; + validate(&policy)?; + Ok(policy) +} + +/// The repo-hygiene policy bundled with the engine. Guaranteed to load. +pub fn builtin_repo_hygiene() -> Policy { + let content = include_str!("../policies/repo-hygiene.plasma.toml"); + load_policy_str(content, PolicyFormat::Toml).expect("bundled repo-hygiene policy must be valid") +} + +/// Validate a parsed policy against v0 evaluability rules. +fn validate(policy: &Policy) -> Result<(), SchemaError> { + if policy.schema_version != SCHEMA_VERSION { + return Err(SchemaError::UnsupportedSchemaVersion { + found: policy.schema_version, + supported: SCHEMA_VERSION, + }); + } + + let mut seen_ids = std::collections::BTreeSet::new(); + + let overlay_rules = policy.overlays.iter().flat_map(|o| { + o.effects.iter().filter_map(|e| match e { + OverlayEffect::AddRules { rules } => Some(rules.iter()), + _ => None, + }) + }); + + for rule in policy.rules.iter().chain(overlay_rules.flatten()) { + if !seen_ids.insert(rule.id.clone()) { + return Err(SchemaError::DuplicateRuleId(rule.id.clone())); + } + validate_rule(rule)?; + } + + for overlay in &policy.overlays { + for effect in &overlay.effects { + let construct = match effect { + OverlayEffect::AddRules { .. } => continue, + OverlayEffect::ModifyRules { .. } => "modify-rules", + OverlayEffect::OverrideRules { .. } => "override-rules", + }; + return Err(SchemaError::ReservedOverlayEffect { + overlay_id: overlay.id.clone(), + construct: construct.to_string(), + }); + } + } + + Ok(()) +} + +/// Reject rules using schema-reserved constructs the v0 evaluator has no +/// semantics for. +fn validate_rule(rule: &Rule) -> Result<(), SchemaError> { + let reserved = |construct: &str| SchemaError::ReservedConstruct { + rule_id: rule.id.clone(), + construct: construct.to_string(), + }; + + if matches!(rule.subject, Subject::Release { .. }) { + return Err(reserved("subject release")); + } + if matches!(rule.resource, Resource::Release) { + return Err(reserved("resource release")); + } + if matches!(rule.action, ActionKind::ConsistentWith { .. }) { + return Err(reserved("action consistent-with")); + } + validate_condition(&rule.condition, &rule.id) +} + +fn validate_condition(condition: &Condition, rule_id: &str) -> Result<(), SchemaError> { + match condition { + Condition::FileMatchesPattern { .. } => Err(SchemaError::ReservedConstruct { + rule_id: rule_id.to_string(), + construct: "condition file-matches-pattern (v0 facts carry no file contents)" + .to_string(), + }), + Condition::Not { of } => validate_condition(of, rule_id), + Condition::All { of } | Condition::Any { of } => { + for c in of { + validate_condition(c, rule_id)?; + } + Ok(()) + } + _ => Ok(()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_builtin_loads() { + let policy = builtin_repo_hygiene(); + assert_eq!(policy.id, "repo-hygiene"); + assert_eq!(policy.schema_version, SCHEMA_VERSION); + assert!(!policy.rules.is_empty()); + } + + #[test] + fn test_rejects_future_schema() { + let doc = r#" +schema_version = { major = 9, minor = 0 } +id = "future" +version = { major = 1, minor = 0 } +rules = [] +"#; + let err = load_policy_str(doc, PolicyFormat::Toml).unwrap_err(); + assert!(matches!(err, SchemaError::UnsupportedSchemaVersion { .. })); + } + + #[test] + fn test_rejects_reserved_action() { + let doc = r#" +schema_version = { major = 0, minor = 1 } +id = "reserved" +version = { major = 1, minor = 0 } + +[[rules]] +id = "r1" +modality = "obligation" +subject = { type = "repo" } +resource = { type = "file", path = "LICENSE" } +action = { type = "consistent-with", id = "decision-1" } +"#; + let err = load_policy_str(doc, PolicyFormat::Toml).unwrap_err(); + assert!(matches!(err, SchemaError::ReservedConstruct { .. })); + } + + #[test] + fn test_rejects_duplicate_rule_ids() { + let doc = r#" +schema_version = { major = 0, minor = 1 } +id = "dupes" +version = { major = 1, minor = 0 } + +[[rules]] +id = "same" +modality = "obligation" +subject = { type = "repo" } +resource = { type = "file", path = "LICENSE" } +action = { type = "present" } + +[[rules]] +id = "same" +modality = "obligation" +subject = { type = "repo" } +resource = { type = "file", path = "README" } +action = { type = "present" } +"#; + let err = load_policy_str(doc, PolicyFormat::Toml).unwrap_err(); + assert!(matches!(err, SchemaError::DuplicateRuleId(_))); + } + + #[test] + fn test_json_round_trip() { + let policy = builtin_repo_hygiene(); + let json = serde_json::to_string_pretty(&policy).unwrap(); + let reloaded = load_policy_str(&json, PolicyFormat::Json).unwrap(); + assert_eq!(policy, reloaded); + } +} diff --git a/plasma-parser/Cargo.toml b/plasma-parser/Cargo.toml index 0cc1093..d172820 100644 --- a/plasma-parser/Cargo.toml +++ b/plasma-parser/Cargo.toml @@ -1,13 +1,13 @@ -# SPDX-License-Identifier: PPMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 [package] name = "plasma-parser" -version = "0.1.0" +version = "0.2.0" edition = "2021" authors = ["Jonathan D.A. Jewell "] -description = "Core parser library for the palimpsest license family — SPDX expressions, zone auditing, compatibility checking" +description = "License/SPDX parsing library — SPDX expressions (including Palimpsest-family identifiers), zone auditing, compatibility checking" license = "MPL-2.0" repository = "https://github.com/hyperpolymath/palimpsest-plasma" -keywords = ["license", "palimpsest", "pmpl", "spdx", "parser"] +keywords = ["license", "spdx", "parser", "audit", "compliance"] categories = ["parser-implementations", "development-tools"] [dependencies] diff --git a/plasma-parser/src/audit/content.rs b/plasma-parser/src/audit/content.rs index 9c7eddc..3c2fcb3 100644 --- a/plasma-parser/src/audit/content.rs +++ b/plasma-parser/src/audit/content.rs @@ -69,12 +69,16 @@ pub fn levenshtein(a: &str, b: &str) -> usize { /// /// Returns a value between 0.0 (no similarity) and 1.0 (identical). pub fn trigram_similarity(a: &str, b: &str) -> f64 { - let trigrams_a: Vec<&str> = a.as_bytes().windows(3).map(|w| { - std::str::from_utf8(w).unwrap_or("") - }).collect(); - let trigrams_b: Vec<&str> = b.as_bytes().windows(3).map(|w| { - std::str::from_utf8(w).unwrap_or("") - }).collect(); + let trigrams_a: Vec<&str> = a + .as_bytes() + .windows(3) + .map(|w| std::str::from_utf8(w).unwrap_or("")) + .collect(); + let trigrams_b: Vec<&str> = b + .as_bytes() + .windows(3) + .map(|w| std::str::from_utf8(w).unwrap_or("")) + .collect(); if trigrams_a.is_empty() && trigrams_b.is_empty() { return 1.0; @@ -83,10 +87,7 @@ pub fn trigram_similarity(a: &str, b: &str) -> f64 { return 0.0; } - let matches = trigrams_a - .iter() - .filter(|t| trigrams_b.contains(t)) - .count(); + let matches = trigrams_a.iter().filter(|t| trigrams_b.contains(t)).count(); (2.0 * matches as f64) / (trigrams_a.len() + trigrams_b.len()) as f64 } @@ -111,7 +112,10 @@ const LICENSE_PHRASES: &[(&str, BaseLicense)] = &[ ("bsd 3-clause", BaseLicense::BSD3), ("bsd 2-clause", BaseLicense::BSD2), ("mit license", BaseLicense::MIT), - ("permission is hereby granted, free of charge", BaseLicense::MIT), + ( + "permission is hereby granted, free of charge", + BaseLicense::MIT, + ), ("isc license", BaseLicense::ISC), ("the unlicense", BaseLicense::Unlicense), ("this is free and unencumbered", BaseLicense::Unlicense), @@ -119,7 +123,9 @@ const LICENSE_PHRASES: &[(&str, BaseLicense)] = &[ /// Multilingual license phrase entry. struct I18nPhrase { - /// ISO 639-1 language code. + /// ISO 639-1 language code. Not yet surfaced in match results; kept so + /// the phrase table stays self-describing. + #[allow(dead_code)] lang: &'static str, /// Key phrase (lowercase) that identifies the license. phrase: &'static str, @@ -134,16 +140,48 @@ struct I18nPhrase { /// Integration point for LOL (Lots Of Languages) corpus and polyglot-i18n. const I18N_PHRASES: &[I18nPhrase] = &[ // EUPL-1.2 translations (23 official EU languages — key ones here) - I18nPhrase { lang: "fr", phrase: "licence publique de l'union européenne", spdx: "EUPL-1.2" }, - I18nPhrase { lang: "de", phrase: "open-source-lizenz für die europäische union", spdx: "EUPL-1.2" }, - I18nPhrase { lang: "nl", phrase: "openbare licentie van de europese unie", spdx: "EUPL-1.2" }, - I18nPhrase { lang: "es", phrase: "licencia pública de la unión europea", spdx: "EUPL-1.2" }, - I18nPhrase { lang: "it", phrase: "licenza pubblica dell'unione europea", spdx: "EUPL-1.2" }, - I18nPhrase { lang: "pt", phrase: "licença pública da união europeia", spdx: "EUPL-1.2" }, + I18nPhrase { + lang: "fr", + phrase: "licence publique de l'union européenne", + spdx: "EUPL-1.2", + }, + I18nPhrase { + lang: "de", + phrase: "open-source-lizenz für die europäische union", + spdx: "EUPL-1.2", + }, + I18nPhrase { + lang: "nl", + phrase: "openbare licentie van de europese unie", + spdx: "EUPL-1.2", + }, + I18nPhrase { + lang: "es", + phrase: "licencia pública de la unión europea", + spdx: "EUPL-1.2", + }, + I18nPhrase { + lang: "it", + phrase: "licenza pubblica dell'unione europea", + spdx: "EUPL-1.2", + }, + I18nPhrase { + lang: "pt", + phrase: "licença pública da união europeia", + spdx: "EUPL-1.2", + }, // CeCILL (French free software license) - I18nPhrase { lang: "fr", phrase: "contrat de licence de logiciel libre", spdx: "CeCILL-2.1" }, + I18nPhrase { + lang: "fr", + phrase: "contrat de licence de logiciel libre", + spdx: "CeCILL-2.1", + }, // PMPL Dutch translation - I18nPhrase { lang: "nl", phrase: "palimpsest-mpl licentie", spdx: "PMPL-1.0" }, + I18nPhrase { + lang: "nl", + phrase: "palimpsest-mpl licentie", + spdx: "PMPL-1.0", + }, ]; /// Detect the license from a LICENSE file's content using multi-level matching. @@ -224,7 +262,8 @@ mod tests { #[test] fn test_detect_mit() { - let content = "MIT License\n\nCopyright (c) 2026 Someone\n\nPermission is hereby granted..."; + let content = + "MIT License\n\nCopyright (c) 2026 Someone\n\nPermission is hereby granted..."; let m = detect_license_from_content(content).unwrap(); assert_eq!(m.license, BaseLicense::MIT); assert_eq!(m.confidence, MatchConfidence::Certain); diff --git a/plasma-parser/src/audit/header.rs b/plasma-parser/src/audit/header.rs index dce8142..deced8e 100644 --- a/plasma-parser/src/audit/header.rs +++ b/plasma-parser/src/audit/header.rs @@ -13,22 +13,20 @@ const MAX_HEADER_LINES: usize = 15; /// File extensions that should have SPDX headers, grouped by comment style. pub const AUDITABLE_EXTENSIONS: &[&str] = &[ - "rs", "zig", "idr", "js", "ts", "jsx", "tsx", "res", "resi", "ex", "exs", - "gleam", "jl", "hs", "ml", "mli", "nim", "pony", "py", "rb", "go", "c", - "h", "cpp", "hpp", "java", "kt", "scala", "sh", "bash", "zsh", "toml", - "yaml", "yml", "ncl", "nix", "scm", "v", "ada", "adb", "ads", + "rs", "zig", "idr", "js", "ts", "jsx", "tsx", "res", "resi", "ex", "exs", "gleam", "jl", "hs", + "ml", "mli", "nim", "pony", "py", "rb", "go", "c", "h", "cpp", "hpp", "java", "kt", "scala", + "sh", "bash", "zsh", "toml", "yaml", "yml", "ncl", "nix", "scm", "v", "ada", "adb", "ads", ]; /// Returns the comment prefix for a given file extension. pub fn comment_prefix(ext: &str) -> &'static str { match ext { - "rs" | "zig" | "js" | "ts" | "jsx" | "tsx" | "res" | "resi" | "go" - | "c" | "h" | "cpp" | "hpp" | "java" | "kt" | "scala" | "pony" - | "v" => "//", + "rs" | "zig" | "js" | "ts" | "jsx" | "tsx" | "res" | "resi" | "go" | "c" | "h" | "cpp" + | "hpp" | "java" | "kt" | "scala" | "pony" | "v" => "//", "idr" | "hs" | "ml" | "mli" => "--", "ada" | "adb" | "ads" => "--", - "ex" | "exs" | "gleam" | "nim" | "py" | "rb" | "sh" | "bash" - | "zsh" | "toml" | "yaml" | "yml" | "ncl" | "nix" | "jl" => "#", + "ex" | "exs" | "gleam" | "nim" | "py" | "rb" | "sh" | "bash" | "zsh" | "toml" | "yaml" + | "yml" | "ncl" | "nix" | "jl" => "#", "scm" => ";;", _ => "//", } @@ -50,6 +48,16 @@ pub fn extract_spdx_header(path: &Path) -> Option { /// Extract SPDX expression from file content string. pub fn extract_spdx_from_content(content: &str) -> Option { + let raw = extract_spdx_raw_from_content(content)?; + parse_spdx_expr(&raw).ok() +} + +/// Extract the raw (unparsed) SPDX-License-Identifier value from file content. +/// +/// Unlike [`extract_spdx_from_content`], this distinguishes "no header" +/// (`None`) from "header present but unparsable" (`Some(raw)`), letting +/// callers defer or report parse failures themselves. +pub fn extract_spdx_raw_from_content(content: &str) -> Option { for line in content.lines().take(MAX_HEADER_LINES) { let trimmed = line.trim(); // Strip comment prefixes. @@ -62,8 +70,7 @@ pub fn extract_spdx_from_content(content: &str) -> Option { .trim(); if let Some(rest) = stripped.strip_prefix("SPDX-License-Identifier:") { - let spdx_str = rest.trim(); - return parse_spdx_expr(spdx_str).ok(); + return Some(rest.trim().to_string()); } } None diff --git a/plasma-parser/src/audit/mod.rs b/plasma-parser/src/audit/mod.rs index a3d6136..b87ddb0 100644 --- a/plasma-parser/src/audit/mod.rs +++ b/plasma-parser/src/audit/mod.rs @@ -6,6 +6,9 @@ pub mod content; pub mod header; +pub mod scan; + +pub use scan::scan_repo; use crate::family::FamilyError; use crate::spdx::SpdxExpr; diff --git a/plasma-parser/src/audit/scan.rs b/plasma-parser/src/audit/scan.rs new file mode 100644 index 0000000..25aae58 --- /dev/null +++ b/plasma-parser/src/audit/scan.rs @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// Repository scanning — walks a repository tree and builds a RepoAudit +// against a LicenseMap. This is the bridge between the zone/header +// machinery and CLI consumers. + +use crate::audit::header::{extract_spdx_raw_from_content, is_auditable}; +use crate::audit::{AuditStatus, AuditSummary, FileAudit, RepoAudit}; +use crate::spdx::parse_spdx_expr; +use crate::zone::boundary::{assign_file_to_zone, expected_license_for_file}; +use crate::zone::LicenseMap; +use std::fs; +use std::path::Path; + +/// Directory names skipped during the walk (mirrors the fact-collection +/// skip list documented in docs/engine-v0-design.adoc). +fn skip_entry(name: &str) -> bool { + name.starts_with('.') + || name == "target" + || name == "node_modules" + || name == "vendor" + || name == "_build" + || name == "deps" +} + +/// Walk the repository and audit every auditable file against the license +/// map. Files iterate in sorted order, so results are deterministic. +pub fn scan_repo(root: &Path, license_map: &LicenseMap) -> RepoAudit { + let mut files: Vec = Vec::new(); + + let mut paths: Vec<_> = walkdir::WalkDir::new(root) + .sort_by_file_name() + .into_iter() + .filter_entry(|e| e.depth() == 0 || !skip_entry(e.file_name().to_str().unwrap_or(""))) + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .map(|e| e.path().to_path_buf()) + .collect(); + paths.sort(); + + for path in paths { + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if !is_auditable(ext) { + continue; + } + + let rel = path.strip_prefix(root).unwrap_or(&path).to_path_buf(); + let zone = assign_file_to_zone(&rel, license_map); + let expected = expected_license_for_file(&rel, license_map); + let proprietary_zone = zone + .as_deref() + .and_then(|name| license_map.zones.iter().find(|z| z.name == name)) + .map(|z| z.proprietary) + .unwrap_or(false); + + let (actual_header, status) = match fs::read_to_string(&path) { + Err(_) => (None, AuditStatus::Unreadable), + Ok(content) => match extract_spdx_raw_from_content(&content) { + None => (None, AuditStatus::MissingHeader), + Some(raw) => match parse_spdx_expr(&raw) { + Err(_) => (None, AuditStatus::WrongLicense), + Ok(actual) => { + let status = if actual == expected { + AuditStatus::Compliant + } else if proprietary_zone { + AuditStatus::ProprietaryViolation + } else { + AuditStatus::WrongLicense + }; + (Some(actual), status) + } + }, + }, + }; + + files.push(FileAudit { + path: rel, + zone, + expected_license: expected, + actual_header, + status, + }); + } + + let summary = summarize(&files); + + RepoAudit { + root: root.to_path_buf(), + license_map: license_map.clone(), + files, + family_errors: Vec::new(), + zone_overlaps: Vec::new(), + compatibility_warnings: Vec::new(), + summary, + } +} + +fn summarize(files: &[FileAudit]) -> AuditSummary { + let mut summary = AuditSummary { + total_files: files.len() as u32, + compliant: 0, + wrong_license: 0, + missing_header: 0, + proprietary_violation: 0, + open_source_violation: 0, + unreadable: 0, + score: 1.0, + }; + + for file in files { + match file.status { + AuditStatus::Compliant => summary.compliant += 1, + AuditStatus::WrongLicense => summary.wrong_license += 1, + AuditStatus::MissingHeader => summary.missing_header += 1, + AuditStatus::ProprietaryViolation => summary.proprietary_violation += 1, + AuditStatus::OpenSourceViolation => summary.open_source_violation += 1, + AuditStatus::Unreadable => summary.unreadable += 1, + } + } + + if summary.total_files > 0 { + summary.score = f64::from(summary.compliant) / f64::from(summary.total_files); + } + + summary +} + +#[cfg(test)] +mod tests { + use super::*; + + fn map_with_default(root: &Path, expr: &str) -> LicenseMap { + LicenseMap { + root: root.to_path_buf(), + default_license: parse_spdx_expr(expr).unwrap(), + zones: Vec::new(), + } + } + + #[test] + fn test_scan_counts_and_score() { + let dir = tempfile::tempdir().unwrap(); + fs::write( + dir.path().join("good.rs"), + "// SPDX-License-Identifier: MPL-2.0\nfn main() {}\n", + ) + .unwrap(); + fs::write( + dir.path().join("wrong.rs"), + "// SPDX-License-Identifier: MIT\nfn main() {}\n", + ) + .unwrap(); + fs::write(dir.path().join("missing.rs"), "fn main() {}\n").unwrap(); + fs::write(dir.path().join("notes.md"), "# not auditable\n").unwrap(); + + let map = map_with_default(dir.path(), "MPL-2.0"); + let audit = scan_repo(dir.path(), &map); + + assert_eq!(audit.summary.total_files, 3); + assert_eq!(audit.summary.compliant, 1); + assert_eq!(audit.summary.wrong_license, 1); + assert_eq!(audit.summary.missing_header, 1); + assert!((audit.summary.score - 1.0 / 3.0).abs() < 1e-9); + } + + #[test] + fn test_scan_is_deterministic() { + let dir = tempfile::tempdir().unwrap(); + for name in ["b.rs", "a.rs", "c.rs"] { + fs::write( + dir.path().join(name), + "// SPDX-License-Identifier: MPL-2.0\n", + ) + .unwrap(); + } + let map = map_with_default(dir.path(), "MPL-2.0"); + let first = scan_repo(dir.path(), &map); + let second = scan_repo(dir.path(), &map); + let paths: Vec<_> = first.files.iter().map(|f| f.path.clone()).collect(); + assert_eq!( + paths, + second + .files + .iter() + .map(|f| f.path.clone()) + .collect::>() + ); + assert_eq!( + paths, + vec!["a.rs".into(), "b.rs".into(), "c.rs".into()] as Vec + ); + } +} diff --git a/plasma-parser/src/compat/matrix.rs b/plasma-parser/src/compat/matrix.rs index ee1fbaa..8b8f9f4 100644 --- a/plasma-parser/src/compat/matrix.rs +++ b/plasma-parser/src/compat/matrix.rs @@ -114,30 +114,20 @@ fn check_palimpsest_compat(a: &License, b: &License) -> Option { match &palimpsest.variant { PalimpsestVariant::PAGPL => match &other_base { - BaseLicense::AGPL3 => { - Some(Compatibility::Compatible) - } - BaseLicense::GPL3 => { - Some(Compatibility::Compatible) - } + BaseLicense::AGPL3 => Some(Compatibility::Compatible), + BaseLicense::GPL3 => Some(Compatibility::Compatible), BaseLicense::MPL2 => Some(Compatibility::Conditional( "file-level separation required (AGPL copyleft vs MPL per-file)".to_string(), )), _ => None, }, PalimpsestVariant::PMPL => match &other_base { - BaseLicense::MPL2 => { - Some(Compatibility::Compatible) - } + BaseLicense::MPL2 => Some(Compatibility::Compatible), _ => None, }, PalimpsestVariant::PGPL => match &other_base { - BaseLicense::AGPL3 => { - Some(Compatibility::Compatible) - } - BaseLicense::GPL3 => { - Some(Compatibility::Compatible) - } + BaseLicense::AGPL3 => Some(Compatibility::Compatible), + BaseLicense::GPL3 => Some(Compatibility::Compatible), BaseLicense::MIT | BaseLicense::BSD3 | BaseLicense::BSD2 => { Some(Compatibility::Compatible) } @@ -189,26 +179,31 @@ fn check_base_compat(a: &BaseLicense, b: &BaseLicense) -> Compatibility { // Copyleft vs copyleft. match (a, b) { - (BaseLicense::GPL3, BaseLicense::AGPL3) - | (BaseLicense::AGPL3, BaseLicense::GPL3) => Compatibility::Compatible, + (BaseLicense::GPL3, BaseLicense::AGPL3) | (BaseLicense::AGPL3, BaseLicense::GPL3) => { + Compatibility::Compatible + } - (BaseLicense::GPL3, BaseLicense::LGPL3) - | (BaseLicense::LGPL3, BaseLicense::GPL3) => Compatibility::Compatible, + (BaseLicense::GPL3, BaseLicense::LGPL3) | (BaseLicense::LGPL3, BaseLicense::GPL3) => { + Compatibility::Compatible + } - (BaseLicense::GPL3, BaseLicense::MPL2) - | (BaseLicense::MPL2, BaseLicense::GPL3) => Compatibility::Conditional( - "PMPL-1.0-or-later Section 3.3 allows relicensing under GPL".to_string(), - ), + (BaseLicense::GPL3, BaseLicense::MPL2) | (BaseLicense::MPL2, BaseLicense::GPL3) => { + Compatibility::Conditional( + "PMPL-1.0-or-later Section 3.3 allows relicensing under GPL".to_string(), + ) + } - (BaseLicense::AGPL3, BaseLicense::MPL2) - | (BaseLicense::MPL2, BaseLicense::AGPL3) => Compatibility::Conditional( - "file-level separation required (AGPL copyleft vs MPL per-file)".to_string(), - ), + (BaseLicense::AGPL3, BaseLicense::MPL2) | (BaseLicense::MPL2, BaseLicense::AGPL3) => { + Compatibility::Conditional( + "file-level separation required (AGPL copyleft vs MPL per-file)".to_string(), + ) + } - (BaseLicense::GPL2, BaseLicense::GPL3) - | (BaseLicense::GPL3, BaseLicense::GPL2) => Compatibility::Incompatible( - "GPL-2.0-only and GPL-3.0 are incompatible without 'or later' clause".to_string(), - ), + (BaseLicense::GPL2, BaseLicense::GPL3) | (BaseLicense::GPL3, BaseLicense::GPL2) => { + Compatibility::Incompatible( + "GPL-2.0-only and GPL-3.0 are incompatible without 'or later' clause".to_string(), + ) + } _ => Compatibility::Unknown, } @@ -248,17 +243,18 @@ mod tests { fn test_mpl_proprietary_compatible() { let mpl = License::Base(BaseLicense::MPL2); let prop = License::Base(BaseLicense::Proprietary); - assert_eq!( - check_compatibility(&mpl, &prop), - Compatibility::Compatible - ); + assert_eq!(check_compatibility(&mpl, &prop), Compatibility::Compatible); } #[test] fn test_papl_anything_compatible() { use crate::family::{PalimpsestLayer, PalimpsestLicense, Version}; - let version = Version { major: 1, minor: 0, patch: 0 }; + let version = Version { + major: 1, + minor: 0, + patch: 0, + }; let papl = License::Palimpsest(PalimpsestLicense { variant: PalimpsestVariant::PAPL, version: version.clone(), @@ -268,9 +264,6 @@ mod tests { or_later: true, }); let agpl = License::Base(BaseLicense::AGPL3); - assert_eq!( - check_compatibility(&papl, &agpl), - Compatibility::Compatible - ); + assert_eq!(check_compatibility(&papl, &agpl), Compatibility::Compatible); } } diff --git a/plasma-parser/src/family/fallback.rs b/plasma-parser/src/family/fallback.rs index 8706053..ed501c6 100644 --- a/plasma-parser/src/family/fallback.rs +++ b/plasma-parser/src/family/fallback.rs @@ -78,19 +78,28 @@ pub fn validate_license_file( content_upper.contains("BSD") && content_upper.contains("2-CLAUSE") || content_upper.contains("REDISTRIBUTION AND USE") } - BaseLicense::MIT => content_upper.contains("MIT LICENSE") || content_upper.contains("PERMISSION IS HEREBY GRANTED"), - BaseLicense::ISC => content_upper.contains("ISC LICENSE") || content_upper.contains("PERMISSION TO USE, COPY, MODIFY"), + BaseLicense::MIT => { + content_upper.contains("MIT LICENSE") + || content_upper.contains("PERMISSION IS HEREBY GRANTED") + } + BaseLicense::ISC => { + content_upper.contains("ISC LICENSE") + || content_upper.contains("PERMISSION TO USE, COPY, MODIFY") + } BaseLicense::EUPL12 => content_upper.contains("EUROPEAN UNION PUBLIC LICENCE"), BaseLicense::LGPL21 | BaseLicense::LGPL3 => { content_upper.contains("GNU LESSER GENERAL PUBLIC LICENSE") } BaseLicense::Artistic2 => content_upper.contains("ARTISTIC LICENSE"), BaseLicense::CERNOHLS2 => content_upper.contains("CERN OPEN HARDWARE"), - BaseLicense::Unlicense => content_upper.contains("UNLICENSE") || content_upper.contains("PUBLIC DOMAIN"), - BaseLicense::ZeroBSD => content_upper.contains("0BSD") || content_upper.contains("ZERO-CLAUSE BSD"), + BaseLicense::Unlicense => { + content_upper.contains("UNLICENSE") || content_upper.contains("PUBLIC DOMAIN") + } + BaseLicense::ZeroBSD => { + content_upper.contains("0BSD") || content_upper.contains("ZERO-CLAUSE BSD") + } BaseLicense::Proprietary => { - content_upper.contains("ALL RIGHTS RESERVED") - || content_upper.contains("PROPRIETARY") + content_upper.contains("ALL RIGHTS RESERVED") || content_upper.contains("PROPRIETARY") } BaseLicense::CreativeCommons(_) => content_upper.contains("CREATIVE COMMONS"), BaseLicense::Other(_) => true, // Cannot validate unknown licenses @@ -111,10 +120,7 @@ pub fn validate_license_file( /// Palimpsest licenses may reference Exhibit A (standard) and Exhibit B /// (quantum-safe provenance). If the layer declares quantum_safe=true, /// Exhibit B must be present. -pub fn validate_exhibits( - license_path: &Path, - license: &PalimpsestLicense, -) -> Vec { +pub fn validate_exhibits(license_path: &Path, license: &PalimpsestLicense) -> Vec { let mut errors = Vec::new(); let content = match std::fs::read_to_string(license_path) { diff --git a/plasma-parser/src/family/mod.rs b/plasma-parser/src/family/mod.rs index e15b5b6..a006529 100644 --- a/plasma-parser/src/family/mod.rs +++ b/plasma-parser/src/family/mod.rs @@ -271,7 +271,9 @@ impl std::fmt::Display for License { #[derive(Debug, Clone, Error, Serialize, Deserialize)] pub enum FamilyError { /// Fallback license does not match the declared base. - #[error("fallback mismatch for {variant}: base is {declared_base}, fallback is {declared_fallback}")] + #[error( + "fallback mismatch for {variant}: base is {declared_base}, fallback is {declared_fallback}" + )] FallbackMismatch { variant: PalimpsestVariant, declared_base: BaseLicense, @@ -291,10 +293,12 @@ pub enum FamilyError { variant: PalimpsestVariant, }, /// SPDX header in a file says one license but the LICENSE file says another. + // The `License` fields are boxed to keep `FamilyError` small (it is + // returned by value in `Result`s throughout the family module). #[error("header/license mismatch in {}: header says {header_says}, LICENSE says {license_file_says}", file.display())] HeaderLicenseMismatch { file: PathBuf, - header_says: License, - license_file_says: License, + header_says: Box, + license_file_says: Box, }, } diff --git a/plasma-parser/src/family/registry.rs b/plasma-parser/src/family/registry.rs index c0fafdf..b507d24 100644 --- a/plasma-parser/src/family/registry.rs +++ b/plasma-parser/src/family/registry.rs @@ -7,9 +7,7 @@ // Each variant in the palimpsest family extends a different well-known base // license while sharing the same palimpsest provisions layer. -use crate::family::{ - BaseLicense, PalimpsestLayer, PalimpsestLicense, PalimpsestVariant, Version, -}; +use crate::family::{BaseLicense, PalimpsestLayer, PalimpsestLicense, PalimpsestVariant, Version}; /// A registry entry describing one palimpsest variant. /// diff --git a/plasma-parser/src/report/sarif.rs b/plasma-parser/src/report/sarif.rs index cc99bd7..b8f7278 100644 --- a/plasma-parser/src/report/sarif.rs +++ b/plasma-parser/src/report/sarif.rs @@ -24,8 +24,7 @@ pub fn to_sarif(audit: &RepoAudit) -> Value { "plasma/wrong-license", format!( "Expected {} but found {:?}", - f.expected_license, - f.actual_header + f.expected_license, f.actual_header ), ), AuditStatus::MissingHeader => ( diff --git a/plasma-parser/src/spdx/catalog.rs b/plasma-parser/src/spdx/catalog.rs index 704cf2c..c2e0515 100644 --- a/plasma-parser/src/spdx/catalog.rs +++ b/plasma-parser/src/spdx/catalog.rs @@ -33,11 +33,7 @@ pub enum CatalogError { /// (in practice, unrecognised identifiers fall through to `BaseLicense::Other`). pub fn resolve_identifier(id: &str, or_later: bool) -> Result { // Extract the name portion (before any version) for palimpsest matching. - let name_part = id - .split('-') - .next() - .unwrap_or(id) - .to_uppercase(); + let name_part = id.split('-').next().unwrap_or(id).to_uppercase(); // Try palimpsest variants first. if let Some(license) = try_palimpsest(&name_part, id, or_later) { @@ -53,7 +49,7 @@ pub fn resolve_identifier(id: &str, or_later: bool) -> Result BaseLicense::LGPL21, "LGPL-3.0" | "LGPL-3.0-only" => BaseLicense::LGPL3, "AGPL-3.0" | "AGPL-3.0-only" => BaseLicense::AGPL3, - "PMPL-1.0-or-later" => BaseLicense::MPL2, + "MPL-2.0" => BaseLicense::MPL2, "BSD-2-Clause" => BaseLicense::BSD2, "BSD-3-Clause" => BaseLicense::BSD3, "ISC" => BaseLicense::ISC, @@ -78,7 +74,9 @@ pub fn resolve_identifier(id: &str, or_later: bool) -> Result Option { let (variant, base) = match name_upper { - "PMPL" => (PalimpsestVariant::PMPL, BaseLicense::MPL2), + // "PPMPL" is a historical double-P spelling of the PMPL identifier; + // both resolve to the same variant. + "PMPL" | "PPMPL" => (PalimpsestVariant::PMPL, BaseLicense::MPL2), "PAGPL" => (PalimpsestVariant::PAGPL, BaseLicense::AGPL3), "PGPL" => (PalimpsestVariant::PGPL, BaseLicense::GPL3), "PAPL" => (PalimpsestVariant::PAPL, BaseLicense::Apache2), @@ -115,8 +113,14 @@ fn extract_version(id: &str) -> Option { if first_char.is_ascii_digit() { let version_parts: Vec<&str> = part.split('.').collect(); let major = version_parts.first()?.parse().ok()?; - let minor = version_parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); - let patch = version_parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); + let minor = version_parts + .get(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + let patch = version_parts + .get(2) + .and_then(|s| s.parse().ok()) + .unwrap_or(0); return Some(Version { major, minor, @@ -166,7 +170,7 @@ fn parse_cc_identifier(id: &str) -> BaseLicense { pub const PALIMPSEST_IDENTIFIERS: &[&str] = &[ "PMPL-1.0", "PMPL-1.0-or-later", - "PPMPL-1.0-or-later", + "PPMPL-1.0", "PPMPL-1.0-or-later", "PAGPL-1.0", "PAGPL-1.0-or-later", @@ -194,7 +198,7 @@ pub const KNOWN_SPDX_IDENTIFIERS: &[&str] = &[ "LGPL-3.0-only", "AGPL-3.0", "AGPL-3.0-only", - "PMPL-1.0-or-later", + "MPL-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", diff --git a/setup.sh b/setup.sh index 2cb7ffb..7daffb4 100755 --- a/setup.sh +++ b/setup.sh @@ -1,5 +1,5 @@ #!/bin/sh -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # setup.sh — Universal setup script for palimpsest-plasma # # Detects your shell, platform, and installs prerequisites. @@ -252,7 +252,7 @@ main() { # Write report REPORT_FILE="INSTALL-SECURITY-REPORT.adoc" { - printf "// SPDX-License-Identifier: PMPL-1.0-or-later\n" + printf "// SPDX-License-Identifier: MPL-2.0\n" printf "= Install Security Report\n" printf ":date: %s\n\n" "$(date -Iseconds 2>/dev/null || date)" printf "== Platform\n" diff --git a/site/content/index.md b/site/content/index.md index 609fcc6..d452994 100644 --- a/site/content/index.md +++ b/site/content/index.md @@ -1,23 +1,36 @@ - +# SPDX-License-Identifier: CC-BY-SA-4.0 +# Copyright (c) Jonathan D.A. Jewell %{ - title: "Palimpsest Plasma MVP", + title: "Palimpsest Plasma", slug: "index", - description: "The compliance bundle for PMPL-1.0 projects.", - date: ~D[2026-02-11] + description: "A deterministic, typed policy engine for the agentic era.", + date: ~D[2026-07-02] } --- ## Overview -Palimpsest Plasma packages the Palimpsest-MPL License 1.0 assets, badge pack, and audit documentation so projects can adopt **PMPL-1.0-or-later** confidently. +Palimpsest Plasma is a deterministic, typed **policy engine**. You write +policies as machine-readable deontic rules — obligations, prohibitions, and +permissions over repository state — and `plasma check` evaluates a repository +against them with byte-for-byte reproducible results. -- **License text & metadata:** `LICENSE`, `.machine_readable/ECOSYSTEM.scm`, `.machine_readable/STATE.scm` -- **Audit and compliance:** `docs/release-log.adoc`, `docs/release-notes.adoc`, `docs/mvp-v1.adoc` -- **Badge assets:** `assets/badges/svg/badge-standard.svg` +- **Engine:** typed policy AST, versioned TOML/JSON schema, a pure and total + evaluator (`plasma-engine/`) +- **License tooling:** SPDX header auditing over a zone-aware license map + (`plasma-parser/`) +- **Agent-honesty contract:** JSON findings, SARIF results with stable + `plasma/` identifiers, and diffable `plasma facts` snapshots -## Building locally +## Quickstart -Install Elixir (1.15+), then from `/var$REPOS_DIR/palimpsest-plasma/site` run `mix site.build`. The command regenerates `/var$REPOS_DIR/palimpsest-plasma/site/_site` which GitHub Actions uploads to Pages. +``` +cargo build --release +./target/release/plasma check . +./target/release/plasma facts . +``` + +## Building this site locally + +Install Elixir (1.15+), then from the `site/` directory run `mix site.build`. +The command regenerates `site/_site`, which GitHub Actions uploads to Pages. diff --git a/site/content/releases.md b/site/content/releases.md index 698cdbb..57f0669 100644 --- a/site/content/releases.md +++ b/site/content/releases.md @@ -1,19 +1,31 @@ - +# SPDX-License-Identifier: CC-BY-SA-4.0 +# Copyright (c) Jonathan D.A. Jewell %{ - title: "Release Distribution", - slug: "release", - description: "Packaging instructions for MVP v1.1.", - date: ~D[2026-01-20] + title: "Releases", + slug: "releases", + description: "Release history and how to build from source.", + date: ~D[2026-07-02] } --- -## Release bundle +## v0.2.0 — Engine v0 -* `LICENSE` – PMPL-1.0-or-later text -* `.machine_readable/STATE.scm`, `.machine_readable/ECOSYSTEM.scm` – provenance metadata -* `docs/release-log.adoc`, `docs/release-notes.adoc` – audit highlights -* `assets/badges/` – canonical badge files -* `mvp-v1.1-release.tar.gz` – distributable archive +The current release is a deterministic, typed policy engine. See +`docs/release-notes.adoc` and `CHANGELOG.md` for the full list of changes. + +Highlights: + +* `plasma-engine` — policy AST, versioned schema, pure evaluator +* `plasma check` / `facts` / `policy validate`, plus parser-backed `audit` +* Licensing unified: code MPL-2.0, documentation CC-BY-SA-4.0 + +## Building from source + +``` +git clone https://github.com/hyperpolymath/palimpsest-plasma.git +cd palimpsest-plasma +cargo build --release +cargo test --workspace +``` + +The binary is `target/release/plasma`. diff --git a/site/lib/palimpsest_site.ex b/site/lib/palimpsest_site.ex index 9feb63a..fe34277 100644 --- a/site/lib/palimpsest_site.ex +++ b/site/lib/palimpsest_site.ex @@ -27,7 +27,9 @@ defmodule PalimpsestSite do BUILD: Orchestrates the full static site generation cycle. """ def build do - pages = pages() + # NimblePublisher's `as: :pages` option populates the `@pages` module + # attribute with the parsed Page structs (inlined here at compile time). + pages = @pages template = File.read!(@template_path) # REFRESH: Atomic reset of the output directory. @@ -44,5 +46,29 @@ defmodule PalimpsestSite do end) end - # ... [Private rendering and path helpers] + # Render one page by evaluating the EEx template with the bindings the + # template expects as bare variables: `page`, `nav` (all pages, for the + # menu), `content`, and `site_title`. + defp render_page(template, page, pages) do + EEx.eval_string(template, + page: page, + nav: pages, + content: page.content, + site_title: "Palimpsest Plasma" + ) + end + + # Copy the static asset directory into the output tree, if present. + defp copy_assets do + assets_src = Path.expand("../assets", __DIR__) + + if File.dir?(assets_src) do + File.cp_r!(assets_src, Path.join(@output_dir, "assets")) + end + end + + # Map a page slug to its output filename. The "index" slug becomes the + # site root; everything else gets its own directory-style path. + defp page_filename("index"), do: "index.html" + defp page_filename(slug), do: "#{slug}.html" end diff --git a/site/templates/default.html.eex b/site/templates/default.html.eex index b77995c..b3163f9 100644 --- a/site/templates/default.html.eex +++ b/site/templates/default.html.eex @@ -14,7 +14,7 @@

<%= page.description || "Palimpsest compliance guidance" %>

@@ -22,7 +22,7 @@ <%= content %>
-

© 2026 Palimpsest Plasma · PMPL-1.0-or-later

+

© 2026 Palimpsest Plasma · code MPL-2.0 · docs CC-BY-SA-4.0

diff --git a/src/audit.rs b/src/audit.rs index 1706cad..d906a81 100644 --- a/src/audit.rs +++ b/src/audit.rs @@ -1,125 +1,114 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Jonathan D.A. Jewell // -// plasma audit — check all source files for correct SPDX headers. +// plasma audit — check all source files for correct SPDX headers, using +// the plasma-parser SPDX machinery (real expression parsing, zone-aware +// expectations from .plasma.toml when present). -use anyhow::Result; +use anyhow::{Context, Result}; +use plasma_parser::audit::header::comment_prefix; +use plasma_parser::audit::{scan_repo, AuditStatus}; +use plasma_parser::spdx::parse_spdx_expr; +use plasma_parser::zone::boundary::parse_plasma_toml; +use plasma_parser::zone::LicenseMap; use std::fs; use std::path::Path; -use walkdir::WalkDir; -/// File extensions that should have SPDX headers. -const AUDITABLE_EXTENSIONS: &[&str] = &[ - "rs", "zig", "idr", "js", "ts", "jsx", "tsx", "res", "resi", "ex", "exs", "gleam", "jl", - "hs", "ml", "mli", "nim", "pony", "py", "rb", "go", "c", "h", "cpp", "hpp", "java", "kt", - "scala", "sh", "bash", "zsh", "toml", "yaml", "yml", "ncl", "nix", "scm", -]; - -/// Comment prefixes for SPDX headers by file type. -fn comment_prefix(ext: &str) -> &'static str { - match ext { - "rs" | "zig" | "js" | "ts" | "jsx" | "tsx" | "res" | "resi" | "go" | "c" | "h" - | "cpp" | "hpp" | "java" | "kt" | "scala" | "pony" => "//", - "idr" | "hs" | "ml" | "mli" => "--", - "ex" | "exs" | "gleam" | "nim" | "py" | "rb" | "sh" | "bash" | "zsh" | "toml" - | "yaml" | "yml" | "ncl" | "nix" | "jl" => "#", - "scm" => ";;", - _ => "//", - } -} - -/// Run the audit: walk all files, check for SPDX headers. -pub fn run(path: &str, fix: bool) -> Result<()> { +/// Run the audit: walk all files, check SPDX headers against the license map. +pub fn run(path: &str, license: &str, fix: bool) -> Result<()> { let root = Path::new(path); - let mut total = 0u32; - let mut compliant = 0u32; - let mut missing = 0u32; - let mut fixed = 0u32; - let mut skipped = 0u32; - - for entry in WalkDir::new(root) - .into_iter() - .filter_entry(|e| { - let name = e.file_name().to_str().unwrap_or(""); - !name.starts_with('.') - && name != "target" - && name != "node_modules" - && name != "vendor" - && name != "_build" - && name != "deps" - }) - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_file()) - { - let path = entry.path(); - let ext = path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or(""); + if !root.exists() { + anyhow::bail!("Path does not exist: {}", path); + } - if !AUDITABLE_EXTENSIONS.contains(&ext) { - continue; - } + // Zone-aware expectations from .plasma.toml when present; otherwise a + // flat map with the --license expression as the default. + let license_map = match parse_plasma_toml(root) { + Ok(map) => map, + Err(_) => LicenseMap { + root: root.to_path_buf(), + default_license: parse_spdx_expr(license) + .map_err(|e| anyhow::anyhow!("invalid --license expression {license:?}: {e}"))?, + zones: Vec::new(), + }, + }; - total += 1; + let audit = scan_repo(root, &license_map); - let content = match fs::read_to_string(path) { - Ok(c) => c, - Err(_) => { - skipped += 1; - continue; + let mut fixed = 0u32; + for file in &audit.files { + match file.status { + AuditStatus::Compliant | AuditStatus::Unreadable => {} + AuditStatus::MissingHeader if fix => { + let full_path = root.join(&file.path); + let ext = file.path.extension().and_then(|e| e.to_str()).unwrap_or(""); + let prefix = comment_prefix(ext); + let header = format!( + "{prefix} SPDX-License-Identifier: {}\n{prefix} Copyright (c) 2026 CHANGE-ME\n\n", + file.expected_license + ); + let content = fs::read_to_string(&full_path) + .with_context(|| format!("cannot read {}", full_path.display()))?; + fs::write(&full_path, format!("{header}{content}"))?; + println!(" FIXED: {}", file.path.display()); + fixed += 1; } - }; - - if content.contains("SPDX-License-Identifier") { - // Check it says PMPL - if content.contains("PMPL") { - compliant += 1; - } else { - // Has SPDX but wrong license + AuditStatus::MissingHeader => { + println!(" MISSING: {}", file.path.display()); + } + AuditStatus::WrongLicense => { println!( - " WRONG LICENSE: {} (has SPDX but not PMPL)", - path.display() + " WRONG LICENSE: {} (expected {}, found {})", + file.path.display(), + file.expected_license, + file.actual_header + .as_ref() + .map(|h| h.to_string()) + .unwrap_or_else(|| "an unparsable header".to_string()) ); - missing += 1; } - } else { - // No SPDX header at all - if fix { - let prefix = comment_prefix(ext); - let header = format!( - "{prefix} SPDX-License-Identifier: PPMPL-1.0-or-later\n{prefix} Copyright (c) 2026 CHANGE-ME\n\n" + AuditStatus::ProprietaryViolation => { + println!( + " PROPRIETARY ZONE VIOLATION: {} carries {}", + file.path.display(), + file.actual_header + .as_ref() + .map(|h| h.to_string()) + .unwrap_or_default() ); - let updated = format!("{header}{content}"); - fs::write(path, updated)?; - println!(" FIXED: {}", path.display()); - fixed += 1; - } else { - println!(" MISSING: {}", path.display()); - missing += 1; + } + AuditStatus::OpenSourceViolation => { + println!(" OPEN-SOURCE ZONE VIOLATION: {}", file.path.display()); } } } + let summary = &audit.summary; + let noncompliant = summary.wrong_license + + summary.missing_header + + summary.proprietary_violation + + summary.open_source_violation; + println!(); println!(" Audit results:"); - println!(" Total files: {total}"); - println!(" Compliant: {compliant}"); - println!(" Missing/Wrong: {missing}"); + println!(" Total files: {}", summary.total_files); + println!(" Compliant: {}", summary.compliant); + println!(" Missing/Wrong: {noncompliant}"); if fix { - println!(" Fixed: {fixed}"); + println!(" Fixed: {fixed}"); } - if skipped > 0 { - println!(" Skipped: {skipped} (binary/unreadable)"); + if summary.unreadable > 0 { + println!( + " Skipped: {} (binary/unreadable)", + summary.unreadable + ); } + println!(" Score: {:.0}%", summary.score * 100.0); - if missing > 0 && !fix { + if noncompliant > 0 && !fix { println!(); println!(" Run `plasma audit --fix` to add missing headers automatically."); - } - - if missing > 0 && !fix { - // Exit with error code so CI fails + // Exit with error code so CI fails. std::process::exit(1); } diff --git a/src/badge.rs b/src/badge.rs index 6c52bda..2df998e 100644 --- a/src/badge.rs +++ b/src/badge.rs @@ -1,58 +1,39 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Jonathan D.A. Jewell // -// plasma badge — generate shields.io badge markdown for PPMPL-1.0-or-later. +// plasma badge — generate shields.io license badge markup. use anyhow::Result; -/// Generate badge markdown for different documentation formats. -pub fn run(style: &str) -> Result<()> { - let badge_url = format!( - "https://img.shields.io/badge/License-PMPL--2.0-blue.svg?style={style}" - ); - let link_url = "https://github.com/hyperpolymath/palimpsest-license"; +/// Generate badge markup for different documentation formats. +pub fn run(license: &str, style: &str) -> Result<()> { + // shields.io escapes: '-' in label text becomes '--'. + let escaped = license.replace('-', "--"); + let badge_url = + format!("https://img.shields.io/badge/License-{escaped}-blue.svg?style={style}"); + let link_url = format!("https://spdx.org/licenses/{license}.html"); - println!("Palimpsest-MPL 2.0 Badge"); + println!("{license} Badge"); println!(); // Markdown println!("Markdown:"); - println!( - " [![License: PPMPL-1.0-or-later]({badge_url})]({link_url})" - ); + println!(" [![License: {license}]({badge_url})]({link_url})"); println!(); // AsciiDoc println!("AsciiDoc:"); - println!( - " image:{badge_url}[License: PPMPL-1.0-or-later,link=\"{link_url}\"]" - ); + println!(" image:{badge_url}[License: {license},link=\"{link_url}\"]"); println!(); // HTML println!("HTML:"); - println!( - " \"License:" - ); + println!(" \"License:"); println!(); // reStructuredText println!("reStructuredText:"); - println!( - " .. image:: {badge_url}\n :target: {link_url}\n :alt: License: PPMPL-1.0-or-later" - ); - println!(); - - // With Covenant - let covenant_url = format!( - "https://img.shields.io/badge/Covenant-Palimpsest-indigo.svg?style={style}" - ); - let covenant_link = "https://github.com/hyperpolymath/palimpsest-license/blob/main/PALIMPSEST-COVENANT.md"; - - println!("With Covenant badge (Markdown):"); - println!( - " [![License: PPMPL-1.0-or-later]({badge_url})]({link_url}) [![Covenant: Palimpsest]({covenant_url})]({covenant_link})" - ); + println!(" .. image:: {badge_url}\n :target: {link_url}\n :alt: License: {license}"); Ok(()) } diff --git a/src/check.rs b/src/check.rs new file mode 100644 index 0000000..5dabab6 --- /dev/null +++ b/src/check.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// plasma check — evaluate a repository against a policy. + +use anyhow::{Context, Result}; +use plasma_engine::{builtin_repo_hygiene, collect, evaluate, load_policy, report, Severity}; +use std::path::Path; + +/// Exit code for a check with violations at or above the threshold. +pub const EXIT_VIOLATIONS: i32 = 1; + +pub struct CheckOptions<'a> { + pub path: &'a str, + pub policy: Option<&'a str>, + pub format: &'a str, + pub severity: &'a str, + pub quiet: bool, + pub verbose: bool, +} + +pub fn run(opts: &CheckOptions) -> Result { + let policy = match opts.policy { + Some(file) => { + load_policy(Path::new(file)).with_context(|| format!("failed to load policy {file}"))? + } + None => builtin_repo_hygiene(), + }; + + let facts = collect(Path::new(opts.path)) + .with_context(|| format!("failed to collect facts from {}", opts.path))?; + + let evaluation = evaluate(&policy, &facts); + + let threshold = parse_severity(opts.severity)?; + + if !opts.quiet { + match opts.format { + "json" => println!("{}", serde_json::to_string_pretty(&evaluation)?), + "sarif" => println!( + "{}", + serde_json::to_string_pretty(&report::to_sarif(&evaluation, opts.verbose))? + ), + _ => print!("{}", report::to_human(&evaluation, opts.verbose)), + } + } + + Ok(if evaluation.clean_at(threshold) { + 0 + } else { + EXIT_VIOLATIONS + }) +} + +fn parse_severity(s: &str) -> Result { + match s { + "error" => Ok(Severity::Error), + "warning" => Ok(Severity::Warning), + "info" => Ok(Severity::Info), + other => anyhow::bail!("unknown severity {other:?}: use error, warning, or info"), + } +} diff --git a/src/facts_cmd.rs b/src/facts_cmd.rs new file mode 100644 index 0000000..81bc89b --- /dev/null +++ b/src/facts_cmd.rs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// plasma facts — dump the deterministic fact snapshot for a repository. +// This is the input the evaluator sees, and the snapshot claim-verification +// tooling can diff before/after an agent run. + +use anyhow::{Context, Result}; +use plasma_engine::collect; +use std::path::Path; + +pub fn run(path: &str) -> Result<()> { + let facts = + collect(Path::new(path)).with_context(|| format!("failed to collect facts from {path}"))?; + println!("{}", serde_json::to_string_pretty(&facts)?); + Ok(()) +} diff --git a/src/init.rs b/src/init.rs index 7fc78e2..48ba80f 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,20 +1,17 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Jonathan D.A. Jewell // -// plasma init — add PPMPL-1.0-or-later to any project in one command. +// plasma init — add a license and SPDX headers to a project in one command. use anyhow::{Context, Result}; use std::fs; use std::path::Path; -/// The full PPMPL-1.0-or-later license text (embedded for offline use). -const PMPL_2_0_TEXT: &str = include_str!("../LICENSE-PPMPL-1.0-or-later.txt"); +/// The full MPL-2.0 license text (embedded for offline use). +const MPL_2_0_TEXT: &str = include_str!("../LICENSES/MPL-2.0.txt"); -/// The Palimpsest Community Covenant text. -const COVENANT_TEXT: &str = include_str!("../PALIMPSEST-COVENANT.md"); - -/// Run the init command: add LICENSE, SPDX headers, and optional Covenant. -pub fn run(path: &str, author: Option<&str>, covenant: bool) -> Result<()> { +/// Run the init command: add LICENSE and SPDX metadata for the chosen license. +pub fn run(path: &str, license: &str, author: Option<&str>) -> Result<()> { let root = Path::new(path); if !root.exists() { @@ -28,78 +25,50 @@ pub fn run(path: &str, author: Option<&str>, covenant: bool) -> Result<()> { let license_path = root.join("LICENSE"); if license_path.exists() { println!(" SKIP: LICENSE already exists (use `plasma migrate` to convert)"); + } else if license == "MPL-2.0" { + fs::write(&license_path, MPL_2_0_TEXT).context("Failed to write LICENSE")?; + println!(" CREATED: LICENSE (MPL-2.0)"); } else { - fs::write(&license_path, PMPL_2_0_TEXT) - .context("Failed to write LICENSE")?; - println!(" CREATED: LICENSE (PPMPL-1.0-or-later)"); - } - - // 2. Write PALIMPSEST-COVENANT.md if requested - if covenant { - let covenant_path = root.join("PALIMPSEST-COVENANT.md"); - if covenant_path.exists() { - println!(" SKIP: PALIMPSEST-COVENANT.md already exists"); - } else { - fs::write(&covenant_path, COVENANT_TEXT) - .context("Failed to write PALIMPSEST-COVENANT.md")?; - println!(" CREATED: PALIMPSEST-COVENANT.md"); - } + fs::write( + &license_path, + format!( + "SPDX-License-Identifier: {license}\n\nTODO: replace this stub with the full {license} license text.\n" + ), + ) + .context("Failed to write LICENSE stub")?; + println!(" CREATED: LICENSE stub for {license} — add the full license text manually"); } - // 3. Add SPDX header to common config files if they exist + // 2. Add SPDX header to common config files if they exist let spdx_header = format!( - "# SPDX-License-Identifier: PPMPL-1.0-or-later\n# SPDX-FileCopyrightText: {} {}\n", - year, author_name + "# SPDX-License-Identifier: {license}\n# SPDX-FileCopyrightText: {year} {author_name}\n" ); // Check for Cargo.toml and update license field let cargo_path = root.join("Cargo.toml"); if cargo_path.exists() { let content = fs::read_to_string(&cargo_path)?; - if !content.contains("PMPL") && !content.contains("SPDX-License-Identifier") { - // Add SPDX header at top + if !content.contains("SPDX-License-Identifier") { let updated = format!("{spdx_header}{content}"); fs::write(&cargo_path, updated)?; println!(" UPDATED: Cargo.toml (added SPDX header)"); } - // Update license field if present - if content.contains("license = \"MIT\"") - || content.contains("license = \"Apache-2.0\"") - || content.contains("license = \"\"") - { - let updated = content - .replace("license = \"MIT\"", "license = \"PPMPL-1.0-or-later\"") - .replace( - "license = \"Apache-2.0\"", - "license = \"PPMPL-1.0-or-later\"", - ) - .replace("license = \"\"", "license = \"PPMPL-1.0-or-later\""); + // Update license field if empty or a common default + let content = fs::read_to_string(&cargo_path)?; + if content.contains("license = \"\"") { + let updated = content.replace("license = \"\"", &format!("license = \"{license}\"")); fs::write(&cargo_path, updated)?; - println!(" UPDATED: Cargo.toml license field → PPMPL-1.0-or-later"); + println!(" UPDATED: Cargo.toml license field → {license}"); } } - // Check for package.json and update license field + // Check for package.json and note the license field let pkg_path = root.join("package.json"); if pkg_path.exists() { let content = fs::read_to_string(&pkg_path)?; - if content.contains("\"license\": \"MIT\"") - || content.contains("\"license\": \"Apache-2.0\"") - || content.contains("\"license\": \"ISC\"") - { - let updated = content - .replace("\"license\": \"MIT\"", "\"license\": \"PPMPL-1.0-or-later\"") - .replace( - "\"license\": \"Apache-2.0\"", - "\"license\": \"PPMPL-1.0-or-later\"", - ) - .replace( - "\"license\": \"ISC\"", - "\"license\": \"PPMPL-1.0-or-later\"", - ); - fs::write(&pkg_path, updated)?; - println!(" UPDATED: package.json license → PPMPL-1.0-or-later"); + if !content.contains(&format!("\"license\": \"{license}\"")) { + println!(" NOTE: package.json found — set \"license\": \"{license}\" manually"); } } @@ -107,19 +76,16 @@ pub fn run(path: &str, author: Option<&str>, covenant: bool) -> Result<()> { let mix_path = root.join("mix.exs"); if mix_path.exists() { let content = fs::read_to_string(&mix_path)?; - if !content.contains("PMPL") { - println!(" NOTE: mix.exs found — manually update licenses field to [\"PPMPL-1.0-or-later\"]"); + if !content.contains(license) { + println!(" NOTE: mix.exs found — manually update licenses field to [\"{license}\"]"); } } println!(); - println!(" Palimpsest-MPL 2.0 applied to {}", path); + println!(" {license} applied to {path}"); println!(" Next steps:"); println!(" 1. Run `plasma audit` to check SPDX headers on source files"); println!(" 2. Add badge to README: `plasma badge`"); - if covenant { - println!(" 3. Reference the Covenant in your CONTRIBUTING.md"); - } Ok(()) } diff --git a/src/main.rs b/src/main.rs index f9fe06c..852c0f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,24 +2,28 @@ // Copyright (c) Jonathan D.A. Jewell #![forbid(unsafe_code)] // -// palimpsest-plasma — Palimpsest license adoption toolkit. +// plasma — deterministic, typed policy engine and license tooling. // -// One-command PPMPL-1.0-or-later adoption for any project: -// plasma init — add LICENSE, SPDX headers, Covenant +// plasma check — evaluate a repository against a policy +// plasma facts — dump the deterministic fact snapshot the evaluator sees +// plasma policy — validate policy files // plasma audit — check all files for correct SPDX headers -// plasma badge — generate shields.io badge markdown -// plasma migrate — convert from MIT/Apache/GPL to PPMPL-1.0-or-later -// plasma verify — verify provenance chain integrity +// plasma init — add a LICENSE and SPDX headers to a project +// plasma badge — generate shields.io badge markup +// plasma migrate — convert a project from one license to another use anyhow::Result; use clap::{Parser, Subcommand}; mod audit; mod badge; +mod check; +mod facts_cmd; mod init; mod migrate; +mod policy_cmd; -/// palimpsest-plasma — adopt PPMPL-1.0-or-later in one command +/// plasma — deterministic, typed policy engine and license tooling #[derive(Parser)] #[command(name = "plasma", version, about, long_about = None)] struct Cli { @@ -29,37 +33,78 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Add PPMPL-1.0-or-later license, SPDX headers, and Covenant to a project. - Init { - /// Path to the project root (default: current directory) - #[arg(short, long, default_value = ".")] + /// Evaluate a repository against a policy. + Check { + /// Path to the repository root + #[arg(default_value = ".")] path: String, - /// Author name for SPDX-FileCopyrightText - #[arg(short, long)] - author: Option, - /// Also add the Palimpsest Community Covenant - #[arg(long, default_value = "true")] - covenant: bool, + /// Policy file (.toml or .json); defaults to the bundled repo-hygiene policy + #[arg(long)] + policy: Option, + /// Output format: human, json, or sarif + #[arg(long, default_value = "human")] + format: String, + /// Minimum violation severity that fails the check: error, warning, info + #[arg(long, default_value = "error")] + severity: String, + /// Exit code only, no output + #[arg(long)] + quiet: bool, + /// Include satisfied (pass) findings in the output + #[arg(long)] + verbose: bool, + }, + + /// Dump the deterministic fact snapshot for a repository (JSON). + Facts { + /// Path to the repository root + #[arg(default_value = ".")] + path: String, + }, + + /// Policy file utilities. + Policy { + #[command(subcommand)] + command: PolicyCommands, }, /// Audit all source files for correct SPDX headers. Audit { /// Path to audit (default: current directory) - #[arg(short, long, default_value = ".")] + #[arg(default_value = ".")] path: String, + /// SPDX license expression headers are expected to carry + #[arg(short, long, default_value = "MPL-2.0")] + license: String, /// Fix missing headers automatically #[arg(long)] fix: bool, }, + /// Add a license and SPDX headers to a project. + Init { + /// Path to the project root (default: current directory) + #[arg(short, long, default_value = ".")] + path: String, + /// SPDX license identifier to apply + #[arg(short, long, default_value = "MPL-2.0")] + license: String, + /// Author name for SPDX-FileCopyrightText + #[arg(short, long)] + author: Option, + }, + /// Generate a shields.io badge for the project. Badge { + /// SPDX license identifier for the badge + #[arg(short, long, default_value = "MPL-2.0")] + license: String, /// Badge style: flat, flat-square, for-the-badge, plastic #[arg(short, long, default_value = "flat")] style: String, }, - /// Migrate from another license to PPMPL-1.0-or-later. + /// Migrate from one license to another. Migrate { /// Path to the project root #[arg(short, long, default_value = ".")] @@ -67,6 +112,18 @@ enum Commands { /// Current license to migrate from (auto-detected if omitted) #[arg(short, long)] from: Option, + /// Target SPDX license identifier + #[arg(short, long, default_value = "MPL-2.0")] + to: String, + }, +} + +#[derive(Subcommand)] +enum PolicyCommands { + /// Parse and schema-check a policy file. + Validate { + /// Policy file (.toml or .json) + file: String, }, } @@ -74,21 +131,49 @@ fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { + Commands::Check { + path, + policy, + format, + severity, + quiet, + verbose, + } => { + let code = check::run(&check::CheckOptions { + path: &path, + policy: policy.as_deref(), + format: &format, + severity: &severity, + quiet, + verbose, + })?; + if code != 0 { + std::process::exit(code); + } + } + Commands::Facts { path } => { + facts_cmd::run(&path)?; + } + Commands::Policy { command } => match command { + PolicyCommands::Validate { file } => { + policy_cmd::validate(&file)?; + } + }, + Commands::Audit { path, license, fix } => { + audit::run(&path, &license, fix)?; + } Commands::Init { path, + license, author, - covenant, } => { - init::run(&path, author.as_deref(), covenant)?; - } - Commands::Audit { path, fix } => { - audit::run(&path, fix)?; + init::run(&path, &license, author.as_deref())?; } - Commands::Badge { style } => { - badge::run(&style)?; + Commands::Badge { license, style } => { + badge::run(&license, &style)?; } - Commands::Migrate { path, from } => { - migrate::run(&path, from.as_deref())?; + Commands::Migrate { path, from, to } => { + migrate::run(&path, from.as_deref(), &to)?; } } diff --git a/src/migrate.rs b/src/migrate.rs index 8821d8d..6ca6500 100644 --- a/src/migrate.rs +++ b/src/migrate.rs @@ -1,12 +1,15 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Jonathan D.A. Jewell // -// plasma migrate — convert from MIT/Apache/GPL to PPMPL-1.0-or-later. +// plasma migrate — convert a project from one license to another. use anyhow::{Context, Result}; use std::fs; use std::path::Path; +/// The full MPL-2.0 license text (embedded for offline use). +const MPL_2_0_TEXT: &str = include_str!("../LICENSES/MPL-2.0.txt"); + /// Known license identifiers we can migrate from. const KNOWN_LICENSES: &[&str] = &[ "MIT", @@ -19,13 +22,14 @@ const KNOWN_LICENSES: &[&str] = &[ "BSD-2-Clause", "BSD-3-Clause", "ISC", + "MPL-2.0", "PMPL-1.0-or-later", "Unlicense", "0BSD", ]; -/// Run the migration: detect current license, replace with PPMPL-1.0-or-later. -pub fn run(path: &str, from: Option<&str>) -> Result<()> { +/// Run the migration: detect current license, replace with the target license. +pub fn run(path: &str, from: Option<&str>, to: &str) -> Result<()> { let root = Path::new(path); // Detect current license @@ -35,7 +39,12 @@ pub fn run(path: &str, from: Option<&str>) -> Result<()> { detect_license(root)? }; - println!(" Migrating from {} → PPMPL-1.0-or-later", current); + if current == to { + println!(" Nothing to do: project already uses {current}"); + return Ok(()); + } + + println!(" Migrating from {current} → {to}"); println!(); // Replace LICENSE file @@ -43,20 +52,28 @@ pub fn run(path: &str, from: Option<&str>) -> Result<()> { if license_path.exists() { // Back up old license let backup = root.join(format!("LICENSE.{}.bak", current.replace('/', "-"))); - fs::copy(&license_path, &backup) - .context("Failed to back up LICENSE")?; + fs::copy(&license_path, &backup).context("Failed to back up LICENSE")?; println!(" BACKED UP: LICENSE → {}", backup.display()); } // Write new license - let pmpl_text = include_str!("../LICENSE-PPMPL-1.0-or-later.txt"); - fs::write(&license_path, pmpl_text) - .context("Failed to write LICENSE")?; - println!(" REPLACED: LICENSE (PPMPL-1.0-or-later)"); + if to == "MPL-2.0" { + fs::write(&license_path, MPL_2_0_TEXT).context("Failed to write LICENSE")?; + println!(" REPLACED: LICENSE (MPL-2.0)"); + } else { + fs::write( + &license_path, + format!( + "SPDX-License-Identifier: {to}\n\nTODO: replace this stub with the full {to} license text.\n" + ), + ) + .context("Failed to write LICENSE stub")?; + println!(" REPLACED: LICENSE with a {to} stub — add the full license text manually"); + } // Update SPDX headers in source files let old_spdx = format!("SPDX-License-Identifier: {current}"); - let new_spdx = "SPDX-License-Identifier: PPMPL-1.0-or-later".to_string(); + let new_spdx = format!("SPDX-License-Identifier: {to}"); let mut updated_count = 0u32; for entry in walkdir::WalkDir::new(root) @@ -79,10 +96,7 @@ pub fn run(path: &str, from: Option<&str>) -> Result<()> { } if updated_count > 0 { - println!( - " UPDATED: {} file(s) — SPDX headers changed to PPMPL-1.0-or-later", - updated_count - ); + println!(" UPDATED: {updated_count} file(s) — SPDX headers changed to {to}"); } // Update Cargo.toml license field @@ -91,7 +105,7 @@ pub fn run(path: &str, from: Option<&str>) -> Result<()> { let content = fs::read_to_string(&cargo_path)?; let old_field = format!("license = \"{current}\""); if content.contains(&old_field) { - let updated = content.replace(&old_field, "license = \"PPMPL-1.0-or-later\""); + let updated = content.replace(&old_field, &format!("license = \"{to}\"")); fs::write(&cargo_path, updated)?; println!(" UPDATED: Cargo.toml license field"); } @@ -103,7 +117,7 @@ pub fn run(path: &str, from: Option<&str>) -> Result<()> { let content = fs::read_to_string(&pkg_path)?; let old_field = format!("\"license\": \"{current}\""); if content.contains(&old_field) { - let updated = content.replace(&old_field, "\"license\": \"PPMPL-1.0-or-later\""); + let updated = content.replace(&old_field, &format!("\"license\": \"{to}\"")); fs::write(&pkg_path, updated)?; println!(" UPDATED: package.json license field"); } @@ -126,9 +140,7 @@ fn detect_license(root: &Path) -> Result { return detect_from_content(&fs::read_to_string(p)?); } } - anyhow::bail!( - "No LICENSE file found. Use --from to specify the current license." - ); + anyhow::bail!("No LICENSE file found. Use --from to specify the current license."); } let content = fs::read_to_string(&license_path)?; @@ -139,7 +151,12 @@ fn detect_license(root: &Path) -> Result { fn detect_from_content(content: &str) -> Result { let lower = content.to_lowercase(); - if lower.contains("mit license") || lower.contains("permission is hereby granted, free of charge") { + if lower.contains("palimpsest") { + return Ok("PMPL-1.0-or-later".to_string()); + } + if lower.contains("mit license") + || lower.contains("permission is hereby granted, free of charge") + { return Ok("MIT".to_string()); } if lower.contains("apache license") && lower.contains("version 2.0") { @@ -158,9 +175,10 @@ fn detect_from_content(content: &str) -> Result { return Ok("AGPL-3.0".to_string()); } if lower.contains("mozilla public license") && lower.contains("2.0") { - return Ok("PMPL-1.0-or-later".to_string()); + return Ok("MPL-2.0".to_string()); } - if lower.contains("bsd 2-clause") || (lower.contains("redistribution") && !lower.contains("3.")) { + if lower.contains("bsd 2-clause") || (lower.contains("redistribution") && !lower.contains("3.")) + { return Ok("BSD-2-Clause".to_string()); } if lower.contains("bsd 3-clause") { @@ -172,9 +190,6 @@ fn detect_from_content(content: &str) -> Result { if lower.contains("unlicense") || lower.contains("this is free and unencumbered") { return Ok("Unlicense".to_string()); } - if lower.contains("palimpsest") { - return Ok("PMPL-1.0".to_string()); - } anyhow::bail!( "Could not auto-detect license. Use --from to specify. Known licenses: {}", diff --git a/src/policy_cmd.rs b/src/policy_cmd.rs new file mode 100644 index 0000000..0379e2e --- /dev/null +++ b/src/policy_cmd.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// plasma policy — policy file utilities. + +use anyhow::{Context, Result}; +use plasma_engine::load_policy; +use std::path::Path; + +/// Validate a policy file: parse + schema check. Prints a summary on success. +pub fn validate(file: &str) -> Result<()> { + let policy = + load_policy(Path::new(file)).with_context(|| format!("policy {file} failed validation"))?; + + let overlay_rules: usize = policy + .overlays + .iter() + .flat_map(|o| &o.effects) + .map(|e| match e { + plasma_engine::ast::OverlayEffect::AddRules { rules } => rules.len(), + _ => 0, + }) + .sum(); + + println!( + "OK: {} v{} (schema {}) — {} base rule(s), {} overlay rule(s)", + policy.id, + policy.version, + policy.schema_version, + policy.rules.len(), + overlay_rules + ); + Ok(()) +} diff --git a/stapeln.toml b/stapeln.toml index 0a32516..0f6e8f7 100644 --- a/stapeln.toml +++ b/stapeln.toml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Identifier: MPL-2.0 # stapeln.toml — Layer-based container build for palimpsest-plasma # # stapeln builds containers as composable layers (German: "to stack"). diff --git a/tests/check_selftest.rs b/tests/check_selftest.rs new file mode 100644 index 0000000..e37527d --- /dev/null +++ b/tests/check_selftest.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// Self-test: `plasma check` against this very repository must pass, and +// its JSON output must be deterministic across runs. + +use std::process::Command; + +fn plasma() -> Command { + Command::new(env!("CARGO_BIN_EXE_plasma")) +} + +fn repo_root() -> &'static str { + env!("CARGO_MANIFEST_DIR") +} + +#[test] +fn check_self_passes() { + let output = plasma() + .args(["check", repo_root(), "--format", "json"]) + .output() + .expect("plasma check must run"); + + assert!( + output.status.success(), + "plasma check on this repo must exit 0; stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let evaluation: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("check --format json must emit JSON"); + assert_eq!(evaluation["policy_id"], "repo-hygiene"); + assert_eq!(evaluation["summary"]["errors"], 0); +} + +#[test] +fn check_json_is_deterministic() { + let run = || { + plasma() + .args(["check", repo_root(), "--format", "json"]) + .output() + .expect("plasma check must run") + .stdout + }; + assert_eq!(run(), run(), "identical runs must produce identical JSON"); +} + +#[test] +fn facts_emits_valid_json() { + let output = plasma() + .args(["facts", repo_root()]) + .output() + .expect("plasma facts must run"); + assert!(output.status.success()); + let facts: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert!(facts["files"].as_array().is_some_and(|a| !a.is_empty())); +} + +#[test] +fn policy_validate_accepts_bundled_policy() { + let policy_path = format!( + "{}/plasma-engine/policies/repo-hygiene.plasma.toml", + repo_root() + ); + let output = plasma() + .args(["policy", "validate", &policy_path]) + .output() + .expect("plasma policy validate must run"); + assert!( + output.status.success(), + "bundled policy must validate; stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); +} diff --git a/union-policy-parser/.claude/CLAUDE.md b/union-policy-parser/.claude/CLAUDE.md deleted file mode 100644 index ce34367..0000000 --- a/union-policy-parser/.claude/CLAUDE.md +++ /dev/null @@ -1,87 +0,0 @@ - -## Machine-Readable Artefacts - -The following files in `.machine_readable/` contain structured project metadata: - -- `STATE.scm` - Current project state and progress -- `META.scm` - Architecture decisions and development practices -- `ECOSYSTEM.scm` - Position in the ecosystem and related projects -- `AGENTIC.scm` - AI agent interaction patterns -- `NEUROSYM.scm` - Neurosymbolic integration config -- `PLAYBOOK.scm` - Operational runbook - ---- - -# CLAUDE.md - AI Assistant Instructions - -## Language Policy (Hyperpolymath Standard) - -### ALLOWED Languages & Tools - -| Language/Tool | Use Case | Notes | -|---------------|----------|-------| -| **AffineScript** | Primary application code | Compiles to JS, type-safe | -| **Deno** | Runtime & package management | Replaces Node/npm/bun | -| **Rust** | Performance-critical, systems, WASM | Preferred for CLI tools | -| **Tauri 2.0+** | Mobile apps (iOS/Android) | Rust backend + web UI | -| **Dioxus** | Mobile apps (native UI) | Pure Rust, React-like | -| **Gleam** | Backend services | Runs on BEAM or compiles to JS | -| **Bash/POSIX Shell** | Scripts, automation | Keep minimal | -| **JavaScript** | Only where AffineScript cannot | MCP protocol glue, Deno APIs | -| **Nickel** | Configuration language | For complex configs | -| **Guile Scheme** | State/meta files | STATE.scm, META.scm, ECOSYSTEM.scm | -| **Julia** | Batch scripts, data processing | Per RSR | -| **OCaml** | AffineScript compiler | Language-specific | -| **Ada** | Safety-critical systems | Where required | - -### BANNED - Do Not Use - -| Banned | Replacement | -|--------|-------------| -| TypeScript | AffineScript | -| Node.js | Deno | -| npm | Deno | -| Bun | Deno | -| pnpm/yarn | Deno | -| Go | Rust | -| Python | Julia/Rust/AffineScript | -| Java/Kotlin | Rust/Tauri/Dioxus | -| Swift | Tauri/Dioxus | -| React Native | Tauri/Dioxus | -| Flutter/Dart | Tauri/Dioxus | - -### Mobile Development - -**No exceptions for Kotlin/Swift** - use Rust-first approach: - -1. **Tauri 2.0+** - Web UI (AffineScript) + Rust backend, MIT/Apache-2.0 -2. **Dioxus** - Pure Rust native UI, MIT/Apache-2.0 - -Both are FOSS with independent governance (no Big Tech). - -### Enforcement Rules - -1. **No new TypeScript files** - Convert existing TS to AffineScript -2. **No package.json - use deno.json deps** - Use deno.json imports -3. **No node_modules in production** - Deno caches deps automatically -4. **No Go code** - Use Rust instead -5. **No Python anywhere** - Use Julia for data/batch, Rust for systems, AffineScript for apps -6. **No Kotlin/Swift for mobile** - Use Tauri 2.0+ or Dioxus - -### Package Management - -- **Primary**: Guix (guix.scm) -- **Fallback**: Nix (flake.nix) -- **JS deps**: Deno (deno.json imports) - -### Security Requirements - -- No MD5/SHA1 for security (use SHA256+) -- HTTPS only (no HTTP URLs) -- No hardcoded secrets -- SHA-pinned dependencies -- SPDX license headers on all files - diff --git a/union-policy-parser/.editorconfig b/union-policy-parser/.editorconfig deleted file mode 100644 index fc6650c..0000000 --- a/union-policy-parser/.editorconfig +++ /dev/null @@ -1,68 +0,0 @@ -# RSR-template-repo - Editor Configuration -# https://editorconfig.org - -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 2 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -trim_trailing_whitespace = false - -[*.adoc] -trim_trailing_whitespace = false - -[*.rs] -indent_size = 4 - -[*.ex] -indent_size = 2 - -[*.exs] -indent_size = 2 - -[*.zig] -indent_size = 4 - -[*.ada] -indent_size = 3 - -[*.adb] -indent_size = 3 - -[*.ads] -indent_size = 3 - -[*.hs] -indent_size = 2 - -[*.res] -indent_size = 2 - -[*.resi] -indent_size = 2 - -[*.ncl] -indent_size = 2 - -[*.rkt] -indent_size = 2 - -[*.scm] -indent_size = 2 - -[*.nix] -indent_size = 2 - -[Justfile] -indent_style = space -indent_size = 4 - -[justfile] -indent_style = space -indent_size = 4 diff --git a/union-policy-parser/.gitattributes b/union-policy-parser/.gitattributes deleted file mode 100644 index bb56184..0000000 --- a/union-policy-parser/.gitattributes +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# RSR-compliant .gitattributes - -* text=auto eol=lf - -# Source -*.rs text eol=lf diff=rust -*.ex text eol=lf diff=elixir -*.exs text eol=lf diff=elixir -*.jl text eol=lf -*.res text eol=lf -*.resi text eol=lf -*.ada text eol=lf diff=ada -*.adb text eol=lf diff=ada -*.ads text eol=lf diff=ada -*.hs text eol=lf -*.chpl text eol=lf -*.scm text eol=lf -*.ncl text eol=lf -*.nix text eol=lf - -# Docs -*.md text eol=lf diff=markdown -*.adoc text eol=lf -*.txt text eol=lf - -# Data -*.json text eol=lf -*.yaml text eol=lf -*.yml text eol=lf -*.toml text eol=lf - -# Config -.gitignore text eol=lf -.gitattributes text eol=lf -justfile text eol=lf -Makefile text eol=lf -Containerfile text eol=lf - -# Scripts -*.sh text eol=lf - -# Binary -*.png binary -*.jpg binary -*.gif binary -*.pdf binary -*.woff2 binary -*.zip binary -*.gz binary - -# Lock files -Cargo.lock text eol=lf -diff -flake.lock text eol=lf -diff diff --git a/union-policy-parser/.github/FUNDING.yml b/union-policy-parser/.github/FUNDING.yml deleted file mode 100644 index 94a3af6..0000000 --- a/union-policy-parser/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: [hyperpolymath] - diff --git a/union-policy-parser/.github/dependabot.yml b/union-policy-parser/.github/dependabot.yml deleted file mode 100644 index 16954b3..0000000 --- a/union-policy-parser/.github/dependabot.yml +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - open-pull-requests-limit: 10 - diff --git a/union-policy-parser/.github/workflows/codeql.yml b/union-policy-parser/.github/workflows/codeql.yml deleted file mode 100644 index 0a62cc8..0000000 --- a/union-policy-parser/.github/workflows/codeql.yml +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0-or-later -name: CodeQL Security Analysis -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - schedule: - - cron: '0 6 * * 1' -permissions: read-all -jobs: - analyze: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - security-events: write - strategy: - fail-fast: false - matrix: - include: - - language: javascript-typescript - build-mode: none - steps: - - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Initialize CodeQL - uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # 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 - with: - category: "/language:${{ matrix.language }}" diff --git a/union-policy-parser/.github/workflows/governance.yml b/union-policy-parser/.github/workflows/governance.yml deleted file mode 100644 index 71460c0..0000000 --- a/union-policy-parser/.github/workflows/governance.yml +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# governance.yml — single wrapper calling the shared estate governance bundle -# in hyperpolymath/standards instead of carrying per-repo copies. -# -# Replaces the per-repo governance scaffolding removed in the same commit: -# quality.yml, guix-nix-policy.yml, npm-bun-blocker.yml, ts-blocker.yml, -# security-policy.yml, rsr-antipattern.yml, wellknown-enforcement.yml, -# workflow-linter.yml -# -# Load-bearing build/security workflows stay standalone in the repo -# (rust-ci, codeql, dependabot, release, scan/mirror/pages plumbing). - -name: Governance -on: - push: - branches: [main, master] - pull_request: - workflow_dispatch: -permissions: - contents: read -jobs: - governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@861b5e911d9e5dcfb3c0ab3dd2a9a3c8fd0a1613 - timeout-minutes: 10 diff --git a/union-policy-parser/.github/workflows/hypatia-scan.yml b/union-policy-parser/.github/workflows/hypatia-scan.yml deleted file mode 100644 index d2ff09b..0000000 --- a/union-policy-parser/.github/workflows/hypatia-scan.yml +++ /dev/null @@ -1,126 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Hypatia Neurosymbolic CI/CD Security Scan -name: Hypatia Security Scan -on: - push: - branches: [main, master, develop] - pull_request: - branches: [main, master] - schedule: - - cron: '0 0 * * 0' # Weekly on Sunday - workflow_dispatch: -permissions: read-all -jobs: - scan: - name: Hypatia Neurosymbolic Analysis - runs-on: ubuntu-latest - timeout-minutes: 15 - 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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 \"\U0001F4E4 Submitting ${{ steps.scan.outputs.findings_count }} findings to gitbot-fleet...\"\n\n# Clone gitbot-fleet to temp directory\nFLEET_DIR=\"/tmp/gitbot-fleet-$$\"\ngit clone https://github.com/hyperpolymath/gitbot-fleet.git \"$FLEET_DIR\"\n\n# Run submission script\nbash \"$FLEET_DIR/scripts/submit-finding.sh\" hypatia-findings.json\n\n# Cleanup\nrm -rf \"$FLEET_DIR\"\n\necho \"✅ Finding submission complete\"\n" - - 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');\nconst findings = JSON.parse(fs.readFileSync('hypatia-findings.json', 'utf8'));\n\nconst critical = findings.filter(f => f.severity === 'critical').length;\nconst high = findings.filter(f => f.severity === 'high').length;\n\nlet comment = `## \U0001F50D Hypatia Security Scan\\n\\n`;\ncomment += `**Findings:** ${findings.length} issues detected\\n\\n`;\ncomment += `| Severity | Count |\\n|----------|-------|\\n`;\ncomment += `| \U0001F534 Critical | ${critical} |\\n`;\ncomment += `| \U0001F7E0 High | ${high} |\\n`;\ncomment += `| \U0001F7E1 Medium | ${findings.length - critical - high} |\\n\\n`;\n\nif (critical > 0) {\n comment += `⚠️ **Action Required:** Critical security issues found!\\n\\n`;\n}\n\ncomment += `
View findings\\n\\n`;\ncomment += `\\`\\`\\`json\\n${JSON.stringify(findings.slice(0, 10), null, 2)}\\n\\`\\`\\`\\n`;\ncomment += `
\\n\\n`;\ncomment += `*Powered by Hypatia Neurosymbolic CI/CD Intelligence*`;\n\ngithub.rest.issues.createComment({\n owner: context.repo.owner,\n repo: context.repo.repo,\n issue_number: context.issue.number,\n body: comment\n});\n" diff --git a/union-policy-parser/.github/workflows/k9-validation.yml b/union-policy-parser/.github/workflows/k9-validation.yml deleted file mode 100644 index e0aac6d..0000000 --- a/union-policy-parser/.github/workflows/k9-validation.yml +++ /dev/null @@ -1,113 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -name: K9 Self-Validating Contracts -on: - push: - paths: - - 'k9/**/*.k9.ncl' - - 'examples/**/*.a2ml' - - 'schemas/**/*.a2ml' - pull_request: - paths: - - 'k9/**/*.k9.ncl' -permissions: read-all -jobs: - test-k9-wrappers: - name: Test K9 Contract Wrappers - runs-on: ubuntu-latest - timeout-minutes: 15 - strategy: - matrix: - k9_contract: - - name: "Employment Contract" - file: "employment-contract.k9.ncl" - - name: "Freelance Journalist" - file: "freelance-journalist-contract.k9.ncl" - - name: "Editorial Policy" - file: "nuj-editorial-policy.k9.ncl" - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - name: Install Nickel - run: | - curl -sSL https://github.com/tweag/nickel/releases/download/1.4.0/nickel-1.4.0-x86_64-unknown-linux-gnu.tar.gz -o nickel.tar.gz - tar -xzf nickel.tar.gz - sudo mv nickel /usr/local/bin/ - nickel --version - - name: Install Just - run: | - curl -sSL https://github.com/casey/just/releases/download/1.26.0/just-1.26.0-x86_64-unknown-linux-musl.tar.gz -o just.tar.gz - tar -xzf just.tar.gz - sudo mv just /usr/local/bin/ - just --version - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable - - name: Build union-policy-parser - run: | - cd src/rust - cargo build --release - sudo cp target/release/union-policy-parser /usr/local/bin/ - - name: Typecheck K9 Nickel contract - run: | - nickel typecheck k9/${{ matrix.k9_contract.file }} - - name: Test K9 install recipe - run: | - just -f k9/${{ matrix.k9_contract.file }} install - - name: Test K9 validate recipe - run: | - just -f k9/${{ matrix.k9_contract.file }} validate || echo "Validation may fail until examples are created" - test-k9-signing: - name: Test K9 Ed25519 Signing - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - name: Install dependencies - run: | - curl -sSL https://github.com/casey/just/releases/download/1.26.0/just-1.26.0-x86_64-unknown-linux-musl.tar.gz -o just.tar.gz - tar -xzf just.tar.gz - sudo mv just /usr/local/bin/ - - name: Generate test Ed25519 key - run: | - ssh-keygen -t ed25519 -f ~/.ssh/test-union-rep-ed25519 -N "" -C "test@union-policy-parser" - ls -la ~/.ssh/test-union-rep-ed25519* - - name: Test signing workflow (dry run) - run: | - echo "Testing K9 signing infrastructure..." - echo "Key generated: $(ssh-keygen -lf ~/.ssh/test-union-rep-ed25519.pub)" - echo "" - echo "In production, K9 sign recipe would:" - echo "1. Compute SHA256 of A2ML payload" - echo "2. Sign with Ed25519 key" - echo "3. Store signature in K9 metadata" - echo "4. Verify signature on deployment" - integration-test: - name: Integration Test (K9 + union-policy-parser) - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: [test-k9-wrappers] - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - name: Install all dependencies - run: | - # Nickel - curl -sSL https://github.com/tweag/nickel/releases/download/1.4.0/nickel-1.4.0-x86_64-unknown-linux-gnu.tar.gz -o nickel.tar.gz - tar -xzf nickel.tar.gz - sudo mv nickel /usr/local/bin/ - - # Just - curl -sSL https://github.com/casey/just/releases/download/1.26.0/just-1.26.0-x86_64-unknown-linux-musl.tar.gz -o just.tar.gz - tar -xzf just.tar.gz - sudo mv just /usr/local/bin/ - - name: Install Rust and build CLI - uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable - - name: Build union-policy-parser - run: | - cd src/rust - cargo build --release - sudo cp target/release/union-policy-parser /usr/local/bin/ - - name: Full integration test - run: "echo \"\U0001F9EA Integration Test: K9 + union-policy-parser + A2ML\"\necho \"==================================================\"\necho \"\"\n\n# Test 1: Validate good freelance contract via K9\necho \"Test 1: Good freelance contract (via K9)\"\nif [ -f examples/freelance-journalist-good.a2ml ]; then\n just -f k9/freelance-journalist-contract.k9.ncl validate || echo \"⚠️ Validation may fail until implementation complete\"\nfi\n\necho \"\"\n\n# Test 2: Validate bad contract (should detect violations)\necho \"Test 2: Bad contract (should detect violations)\"\nif [ -f examples/freelance-journalist-bad.a2ml ]; then\n just -f k9/freelance-journalist-contract.k9.ncl check_exploitation || echo \"✅ Expected to detect violations\"\nfi\n\necho \"\"\n\n# Test 3: Academic contract\necho \"Test 3: Academic contract\"\nif [ -f examples/academic-lecturer-contract.a2ml ]; then\n union-policy-parser validate \\\n examples/academic-lecturer-contract.a2ml \\\n --schema schemas/ucu-academic-standards.a2ml \\\n --union ucu \\\n --mode checked || echo \"⚠️ Validation may fail until implementation complete\"\nfi\n\necho \"\"\necho \"✅ Integration test complete\"\n" - - name: Generate compliance dashboard - run: "echo \"\U0001F4CA Compliance Dashboard\"\necho \"=======================\"\necho \"\"\necho \"Schemas validated: 7/7\"\necho \" ✓ NUJ Code of Ethics\"\necho \" ✓ NUJ PR & Comms Guidance\"\necho \" ✓ IWW Freelancer Rights\"\necho \" ✓ UCU Academic Standards\"\necho \" ✓ BECTU Media Rights\"\necho \" ✓ Equity Performers Rights\"\necho \" ✓ GMB General Workers Rights\"\necho \"\"\necho \"K9 wrappers tested: 3/3\"\necho \" ✓ employment-contract.k9.ncl\"\necho \" ✓ freelance-journalist-contract.k9.ncl\"\necho \" ✓ nuj-editorial-policy.k9.ncl\"\n" diff --git a/union-policy-parser/.github/workflows/mirror.yml b/union-policy-parser/.github/workflows/mirror.yml deleted file mode 100644 index b81016b..0000000 --- a/union-policy-parser/.github/workflows/mirror.yml +++ /dev/null @@ -1,127 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -name: Mirror to Git Forges -on: - push: - branches: [main] - workflow_dispatch: -permissions: read-all -jobs: - mirror-gitlab: - runs-on: ubuntu-latest - timeout-minutes: 15 - if: vars.GITLAB_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.GITLAB_SSH_KEY }} - - name: Mirror to GitLab - run: | - ssh-keyscan -t ed25519 gitlab.com >> ~/.ssh/known_hosts - git remote add gitlab git@gitlab.com:hyperpolymath/${{ github.event.repository.name }}.git || true - git push --force gitlab main - mirror-bitbucket: - runs-on: ubuntu-latest - timeout-minutes: 15 - if: vars.BITBUCKET_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.BITBUCKET_SSH_KEY }} - - name: Mirror to Bitbucket - run: | - ssh-keyscan -t ed25519 bitbucket.org >> ~/.ssh/known_hosts - git remote add bitbucket git@bitbucket.org:hyperpolymath/${{ github.event.repository.name }}.git || true - git push --force bitbucket main - mirror-codeberg: - runs-on: ubuntu-latest - timeout-minutes: 15 - if: vars.CODEBERG_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.CODEBERG_SSH_KEY }} - - name: Mirror to Codeberg - run: | - ssh-keyscan -t ed25519 codeberg.org >> ~/.ssh/known_hosts - git remote add codeberg git@codeberg.org:hyperpolymath/${{ github.event.repository.name }}.git || true - git push --force codeberg main - mirror-sourcehut: - runs-on: ubuntu-latest - timeout-minutes: 15 - if: vars.SOURCEHUT_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.SOURCEHUT_SSH_KEY }} - - name: Mirror to SourceHut - run: | - ssh-keyscan -t ed25519 git.sr.ht >> ~/.ssh/known_hosts - git remote add sourcehut git@git.sr.ht:~hyperpolymath/${{ github.event.repository.name }} || true - git push --force sourcehut main - mirror-disroot: - runs-on: ubuntu-latest - timeout-minutes: 15 - if: vars.DISROOT_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.DISROOT_SSH_KEY }} - - name: Mirror to Disroot - run: | - ssh-keyscan -t ed25519 git.disroot.org >> ~/.ssh/known_hosts - git remote add disroot git@git.disroot.org:hyperpolymath/${{ github.event.repository.name }}.git || true - git push --force disroot main - mirror-gitea: - runs-on: ubuntu-latest - timeout-minutes: 15 - if: vars.GITEA_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 - with: - ssh-private-key: ${{ secrets.GITEA_SSH_KEY }} - - name: Mirror to Gitea - run: | - ssh-keyscan -t ed25519 ${{ vars.GITEA_HOST }} >> ~/.ssh/known_hosts - git remote add gitea git@${{ vars.GITEA_HOST }}:hyperpolymath/${{ github.event.repository.name }}.git || true - git push --force gitea main - mirror-radicle: - runs-on: ubuntu-latest - timeout-minutes: 15 - if: vars.RADICLE_MIRROR_ENABLED == 'true' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - with: - fetch-depth: 0 - - name: Setup Rust - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable - with: - toolchain: stable - - name: Install Radicle - run: | - # Install via cargo (safer than curl|sh) - cargo install radicle-cli --locked - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - name: Mirror to Radicle - run: | - echo "${{ secrets.RADICLE_KEY }}" > ~/.radicle/keys/radicle - chmod 600 ~/.radicle/keys/radicle - rad sync --announce || echo "Radicle sync attempted" diff --git a/union-policy-parser/.github/workflows/scorecard.yml b/union-policy-parser/.github/workflows/scorecard.yml deleted file mode 100644 index cd45277..0000000 --- a/union-policy-parser/.github/workflows/scorecard.yml +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -name: OSSF Scorecard -on: - push: - branches: [main, master] - schedule: - - cron: '0 4 * * *' - workflow_dispatch: -permissions: read-all -jobs: - analysis: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - security-events: write - id-token: write - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - persist-credentials: false - - name: Run Scorecard - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.3.1 - with: - results_file: results.sarif - results_format: sarif - - name: Upload results - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.31.8 - with: - sarif_file: results.sarif diff --git a/union-policy-parser/.github/workflows/secret-scanner.yml b/union-policy-parser/.github/workflows/secret-scanner.yml deleted file mode 100644 index eb6516d..0000000 --- a/union-policy-parser/.github/workflows/secret-scanner.yml +++ /dev/null @@ -1,51 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.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 -jobs: - trufflehog: - runs-on: ubuntu-latest - 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 - # Rust-specific: Check for hardcoded crypto values - rust-secrets: - runs-on: ubuntu-latest - timeout-minutes: 15 - if: hashFiles('**/Cargo.toml') != '' - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - - name: Check for hardcoded secrets in Rust - run: | - # Patterns that suggest hardcoded secrets - PATTERNS=( - 'const.*SECRET.*=.*"' - 'const.*KEY.*=.*"[a-zA-Z0-9]{16,}"' - 'const.*TOKEN.*=.*"' - 'let.*api_key.*=.*"' - 'HMAC.*"[a-fA-F0-9]{32,}"' - 'password.*=.*"[^"]+"' - ) - - found=0 - for pattern in "${PATTERNS[@]}"; do - if grep -rn --include="*.rs" -E "$pattern" src/; then - echo "WARNING: Potential hardcoded secret found matching: $pattern" - found=1 - fi - done - - if [ $found -eq 1 ]; then - echo "::error::Potential hardcoded secrets detected. Use environment variables instead." - exit 1 - fi diff --git a/union-policy-parser/.github/workflows/validate-contracts.yml b/union-policy-parser/.github/workflows/validate-contracts.yml deleted file mode 100644 index baeec42..0000000 --- a/union-policy-parser/.github/workflows/validate-contracts.yml +++ /dev/null @@ -1,170 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -name: Validate Contracts -on: - push: - paths: - - 'examples/**/*.a2ml' - - 'schemas/**/*.a2ml' - - 'src/**/*.rs' - pull_request: - paths: - - 'examples/**/*.a2ml' - - 'schemas/**/*.a2ml' -permissions: read-all -jobs: - validate-schemas: - name: Validate Schemas - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable - with: - toolchain: stable - - name: Cache Cargo dependencies - uses: actions/cache@7e69c5f32fcfbc8e3daa8e2d73eb74cc2ba851ec # v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - src/rust/target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Build union-policy-parser - run: | - cd src/rust - cargo build --release - - name: Validate NUJ schemas - run: | - ./src/rust/target/release/union-policy-parser check-schema schemas/nuj-code-of-ethics.a2ml - ./src/rust/target/release/union-policy-parser check-schema schemas/nuj-pr-comms-guidance.a2ml - - name: Validate IWW schema - run: | - ./src/rust/target/release/union-policy-parser check-schema schemas/iww-freelancer-rights.a2ml - - name: Validate UCU schema - run: | - ./src/rust/target/release/union-policy-parser check-schema schemas/ucu-academic-standards.a2ml - - name: Validate BECTU schema - run: | - ./src/rust/target/release/union-policy-parser check-schema schemas/bectu-media-rights.a2ml - - name: Validate Equity schema - run: | - ./src/rust/target/release/union-policy-parser check-schema schemas/equity-performers-rights.a2ml - - name: Validate GMB schema - run: | - ./src/rust/target/release/union-policy-parser check-schema schemas/gmb-general-workers-rights.a2ml - validate-example-contracts: - name: Validate Example Contracts - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: validate-schemas - strategy: - matrix: - contract: - - name: "OU Day Employee" - file: "ou-day-employee-contract.a2ml" - schema: "nuj-code-of-ethics.a2ml" - union: "nuj" - - name: "Freelance Journalist (Good)" - file: "freelance-journalist-good.a2ml" - schema: "nuj-code-of-ethics.a2ml" - union: "nuj" - - name: "Freelance Journalist (Bad)" - file: "freelance-journalist-bad.a2ml" - schema: "iww-freelancer-rights.a2ml" - union: "iww" - expected_failure: true - - name: "Academic Lecturer" - file: "academic-lecturer-contract.a2ml" - schema: "ucu-academic-standards.a2ml" - union: "ucu" - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable - with: - toolchain: stable - - name: Cache Cargo dependencies - uses: actions/cache@7e69c5f32fcfbc8e3daa8e2d73eb74cc2ba851ec # v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - src/rust/target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Build union-policy-parser - run: | - cd src/rust - cargo build --release - - name: Validate ${{ matrix.contract.name }} - id: validate - continue-on-error: ${{ matrix.contract.expected_failure == true }} - run: | - ./src/rust/target/release/union-policy-parser validate \ - examples/${{ matrix.contract.file }} \ - --schema schemas/${{ matrix.contract.schema }} \ - --union ${{ matrix.contract.union }} \ - --mode checked - - name: Check expected failure - if: matrix.contract.expected_failure == true && steps.validate.outcome == 'success' - run: | - echo "❌ Expected validation to fail for ${{ matrix.contract.name }}" - exit 1 - - name: Generate audit report - if: steps.validate.outcome == 'success' - run: | - ./src/rust/target/release/union-policy-parser audit \ - examples/${{ matrix.contract.file }} \ - --schema schemas/${{ matrix.contract.schema }} \ - --union ${{ matrix.contract.union }} \ - --output audit-${{ matrix.contract.union }}-${{ strategy.job-index }}.json - - name: Upload audit report - if: steps.validate.outcome == 'success' - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4 - with: - name: audit-report-${{ matrix.contract.union }}-${{ strategy.job-index }} - path: audit-*.json - retention-days: 30 - auto-grievance: - name: Auto-Generate Grievances for Violations - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: validate-example-contracts - if: failure() - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable - - name: Build union-policy-parser - run: | - cd src/rust - cargo build --release - - name: Generate grievance for bad contract - run: | - ./src/rust/target/release/union-policy-parser grievance \ - examples/freelance-journalist-bad.a2ml \ - --violation "multiple-exploitative-clauses" \ - --schema schemas/iww-freelancer-rights.a2ml \ - --union iww \ - --output grievance-bad-contract.md - - name: Create GitHub Issue for Violations - uses: actions/github-script@7d2d13cb9171433f7b1c64aad26dcd68dd3ec2eb # v7 - with: - script: | - const fs = require('fs'); - const grievance = fs.readFileSync('grievance-bad-contract.md', 'utf8'); - - github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: 'Contract Validation Failure: Exploitative Clauses Detected', - body: `## ⚠️ Contract Validation Failure\n\n${grievance}\n\n**Action:** Review contract and contact union rep.`, - labels: ['contract-violation', 'grievance'] - }); diff --git a/union-policy-parser/.gitignore b/union-policy-parser/.gitignore deleted file mode 100644 index 252a87e..0000000 --- a/union-policy-parser/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ - -# Crash recovery artifacts -ai-cli-crash-capture/ -target/ -node_modules/ -_build/ -deps/ -.elixir_ls/ -.cache/ -*.log -*.tmp -.DS_Store -Thumbs.db -build/ -dist/ diff --git a/union-policy-parser/.machine_readable/6a2/AGENTIC.a2ml b/union-policy-parser/.machine_readable/6a2/AGENTIC.a2ml deleted file mode 100644 index d119bec..0000000 --- a/union-policy-parser/.machine_readable/6a2/AGENTIC.a2ml +++ /dev/null @@ -1,34 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# AGENTIC.a2ml — AI agent constraints and capabilities -[metadata] -version = "0.1.0" -last-updated = "2026-04-11" - -[agent-permissions] -can-edit-source = true -can-edit-tests = true -can-edit-docs = true -can-edit-config = true -can-create-files = true - -[agent-constraints] -# What AI agents must NOT do: -# - Never use banned language patterns (believe_me, unsafeCoerce, etc.) -# - Never commit secrets or credentials -# - Never use banned languages (TypeScript, Python, Go, etc.) -# - Never place state files in repository root (must be in .machine_readable/) -# - Never use AGPL license (use PMPL-1.0-or-later) - -[maintenance-integrity] -fail-closed = true -require-evidence-per-step = true -allow-silent-skip = false -require-rerun-after-fix = true -release-claim-requires-hard-pass = true - -[automation-hooks] -# on-enter: Read 0-AI-MANIFEST.a2ml, then STATE.a2ml -# on-exit: Update STATE.a2ml with session outcomes -# on-commit: Run just validate-rsr diff --git a/union-policy-parser/.machine_readable/6a2/ECOSYSTEM.a2ml b/union-policy-parser/.machine_readable/6a2/ECOSYSTEM.a2ml deleted file mode 100644 index 378bcae..0000000 --- a/union-policy-parser/.machine_readable/6a2/ECOSYSTEM.a2ml +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# ECOSYSTEM.a2ml — Union Policy Parser ecosystem position -[metadata] -version = "1.0" -last-updated = "2026-04-11" - -[project] -name = "Union Policy Parser" -purpose = "" -role = "" - -[position-in-ecosystem] -category = "" - -[related-projects] -projects = [ - # No related projects recorded -] diff --git a/union-policy-parser/.machine_readable/6a2/META.a2ml b/union-policy-parser/.machine_readable/6a2/META.a2ml deleted file mode 100644 index 9e7a390..0000000 --- a/union-policy-parser/.machine_readable/6a2/META.a2ml +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# META.a2ml — Union Policy Parser meta-level information -[metadata] -version = "0.1.0" -last-updated = "2026-04-11" - -[project-info] -license = "PMPL-1.0-or-later" -author = "Jonathan D.A. Jewell (hyperpolymath)" - -[architecture-decisions] -decisions = [ - # No ADRs recorded -] - -[development-practices] -versioning = "SemVer" -documentation = "AsciiDoc" -build-tool = "just" - -[maintenance-axes] -scoping-first = true -axis-1 = "must > intend > like" -axis-2 = "corrective > adaptive > perfective" -axis-3 = "systems > compliance > effects" diff --git a/union-policy-parser/.machine_readable/6a2/NEUROSYM.a2ml b/union-policy-parser/.machine_readable/6a2/NEUROSYM.a2ml deleted file mode 100644 index 1443ec7..0000000 --- a/union-policy-parser/.machine_readable/6a2/NEUROSYM.a2ml +++ /dev/null @@ -1,21 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# NEUROSYM.a2ml — Neurosymbolic integration metadata -[metadata] -version = "0.1.0" -last-updated = "2026-04-11" - -[hypatia-config] -scan-enabled = true -scan-depth = "standard" # quick | standard | deep -report-format = "logtalk" - -[symbolic-rules] -# Custom symbolic rules for this project -# - { name = "no-unsafe-ffi", pattern = "believe_me|unsafeCoerce", severity = "critical" } - -[neural-config] -# Neural pattern detection settings -# confidence-threshold = 0.85 -# model = "hypatia-v2" diff --git a/union-policy-parser/.machine_readable/6a2/PLAYBOOK.a2ml b/union-policy-parser/.machine_readable/6a2/PLAYBOOK.a2ml deleted file mode 100644 index c894f05..0000000 --- a/union-policy-parser/.machine_readable/6a2/PLAYBOOK.a2ml +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# PLAYBOOK.a2ml — Operational playbook -[metadata] -version = "0.1.0" -last-updated = "2026-04-11" - -[deployment] -# method = "gitops" # gitops | manual | ci-triggered -# target = "container" # container | binary | library | wasm - -[incident-response] -# 1. Check .machine_readable/STATE.a2ml for current status -# 2. Review recent commits and CI results -# 3. Run `just validate` to check compliance -# 4. Run `just security` to audit for vulnerabilities - -[release-process] -# 1. Update version in STATE.a2ml, META.a2ml -# 2. Run `just release-preflight` (validate + quality + security + maint-hard-pass) -# 3. Tag and push - -[maintenance-operations] -# Baseline audit: just maint-audit -# Hard release gate: just maint-hard-pass diff --git a/union-policy-parser/.machine_readable/6a2/STATE.a2ml b/union-policy-parser/.machine_readable/6a2/STATE.a2ml deleted file mode 100644 index b3b739f..0000000 --- a/union-policy-parser/.machine_readable/6a2/STATE.a2ml +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# STATE.a2ml — Union Policy Parser project state -[metadata] -project = "union-policy-parser" -version = "0.0.1" -last-updated = "2026-01-03" -status = "active" -session = "converted from scheme — 2026-04-11" - -[project-context] -name = "Union Policy Parser" -purpose = """""" -completion-percentage = 0 - -[position] -phase = "initial" # design | implementation | testing | maintenance | archived -maturity = "experimental" # experimental | alpha | beta | production | lts - -[route-to-mvp] -milestones = [ - # No milestones recorded -] - -[blockers-and-issues] -issues = [ - # No blockers recorded -] - -[critical-next-actions] -actions = [ - # No actions recorded -] - -[maintenance-status] -last-run-utc = "2026-01-03T00:00:00Z" -last-result = "unknown" # unknown | pass | warn | fail diff --git a/union-policy-parser/ABI-FFI-README.md b/union-policy-parser/ABI-FFI-README.md deleted file mode 100644 index 9cded60..0000000 --- a/union-policy-parser/ABI-FFI-README.md +++ /dev/null @@ -1,389 +0,0 @@ - -{{~ Aditionally delete this line and fill out the template below ~}} - -# {{PROJECT}} ABI/FFI Documentation - -## Overview - -This library follows the **Hyperpolymath RSR Standard** for ABI and FFI design: - -- **ABI (Application Binary Interface)** defined in **Idris2** with formal proofs -- **FFI (Foreign Function Interface)** implemented in **Zig** for C compatibility -- **Generated C headers** bridge Idris2 ABI to Zig FFI -- **Any language** can call through standard C ABI - -## Architecture - -``` -┌─────────────────────────────────────────────┐ -│ ABI Definitions (Idris2) │ -│ src/abi/ │ -│ - Types.idr (Type definitions) │ -│ - Layout.idr (Memory layout proofs) │ -│ - Foreign.idr (FFI declarations) │ -└─────────────────┬───────────────────────────┘ - │ - │ generates (at compile time) - ▼ -┌─────────────────────────────────────────────┐ -│ C Headers (auto-generated) │ -│ generated/abi/{{project}}.h │ -└─────────────────┬───────────────────────────┘ - │ - │ imported by - ▼ -┌─────────────────────────────────────────────┐ -│ FFI Implementation (Zig) │ -│ ffi/zig/src/main.zig │ -│ - Implements C-compatible functions │ -│ - Zero-cost abstractions │ -│ - Memory-safe by default │ -└─────────────────┬───────────────────────────┘ - │ - │ compiled to lib{{project}}.so/.a - ▼ -┌─────────────────────────────────────────────┐ -│ Any Language via C ABI │ -│ - Rust, ReScript, Julia, Python, etc. │ -└─────────────────────────────────────────────┘ -``` - -## Directory Structure - -``` -{{project}}/ -├── src/ -│ ├── abi/ # ABI definitions (Idris2) -│ │ ├── Types.idr # Core type definitions with proofs -│ │ ├── Layout.idr # Memory layout verification -│ │ └── Foreign.idr # FFI function declarations -│ └── lib/ # Core library (any language) -│ -├── ffi/ -│ └── zig/ # FFI implementation (Zig) -│ ├── build.zig # Build configuration -│ ├── build.zig.zon # Dependencies -│ ├── src/ -│ │ └── main.zig # C-compatible FFI implementation -│ ├── test/ -│ │ └── integration_test.zig -│ └── include/ -│ └── {{project}}.h # C header (optional, can be generated) -│ -├── generated/ # Auto-generated files -│ └── abi/ -│ └── {{project}}.h # Generated from Idris2 ABI -│ -└── bindings/ # Language-specific wrappers (optional) - ├── rust/ - ├── rescript/ - └── julia/ -``` - -## Why Idris2 for ABI? - -### 1. **Formal Verification** - -Idris2's dependent types allow proving properties about the ABI at compile-time: - -```idris --- Prove struct size is correct -public export -exampleStructSize : HasSize ExampleStruct 16 - --- Prove field alignment is correct -public export -fieldAligned : Divides 8 (offsetOf ExampleStruct.field) - --- Prove ABI is platform-compatible -public export -abiCompatible : Compatible (ABI 1) (ABI 2) -``` - -### 2. **Type Safety** - -Encode invariants that C/Zig cannot express: - -```idris --- Non-null pointer guaranteed at type level -data Handle : Type where - MkHandle : (ptr : Bits64) -> {auto 0 nonNull : So (ptr /= 0)} -> Handle - --- Array with length proof -data Buffer : (n : Nat) -> Type where - MkBuffer : Vect n Byte -> Buffer n -``` - -### 3. **Platform Abstraction** - -Platform-specific types with compile-time selection: - -```idris -CInt : Platform -> Type -CInt Linux = Bits32 -CInt Windows = Bits32 - -CSize : Platform -> Type -CSize Linux = Bits64 -CSize Windows = Bits64 -``` - -### 4. **Safe Evolution** - -Prove that new ABI versions are backward-compatible: - -```idris --- Compiler enforces compatibility -abiUpgrade : ABI 1 -> ABI 2 -abiUpgrade old = MkABI2 { - -- Must preserve all v1 fields - v1_compat = old, - -- Can add new fields - new_features = defaults -} -``` - -## Why Zig for FFI? - -### 1. **C ABI Compatibility** - -Zig exports C-compatible functions naturally: - -```zig -export fn library_function(param: i32) i32 { - return param * 2; -} -``` - -### 2. **Memory Safety** - -Compile-time safety without runtime overhead: - -```zig -// Null check enforced at compile time -const handle = init() orelse return error.InitFailed; -defer free(handle); -``` - -### 3. **Cross-Compilation** - -Built-in cross-compilation to any platform: - -```bash -zig build -Dtarget=x86_64-linux -zig build -Dtarget=aarch64-macos -zig build -Dtarget=x86_64-windows -``` - -### 4. **Zero Dependencies** - -No runtime, no libc required (unless explicitly needed): - -```zig -// Minimal binary size -pub const lib = @import("std"); -// Only includes what you use -``` - -## Building - -### Build FFI Library - -```bash -cd ffi/zig -zig build # Build debug -zig build -Doptimize=ReleaseFast # Build optimized -zig build test # Run tests -``` - -### Generate C Header from Idris2 ABI - -```bash -cd src/abi -idris2 --cg c-header Types.idr -o ../../generated/abi/{{project}}.h -``` - -### Cross-Compile - -```bash -cd ffi/zig - -# Linux x86_64 -zig build -Dtarget=x86_64-linux - -# macOS ARM64 -zig build -Dtarget=aarch64-macos - -# Windows x86_64 -zig build -Dtarget=x86_64-windows -``` - -## Usage - -### From C - -```c -#include "{{project}}.h" - -int main() { - void* handle = {{project}}_init(); - if (!handle) return 1; - - int result = {{project}}_process(handle, 42); - if (result != 0) { - const char* err = {{project}}_last_error(); - fprintf(stderr, "Error: %s\n", err); - } - - {{project}}_free(handle); - return 0; -} -``` - -Compile with: -```bash -gcc -o example example.c -l{{project}} -L./zig-out/lib -``` - -### From Idris2 - -```idris -import {{PROJECT}}.ABI.Foreign - -main : IO () -main = do - Just handle <- init - | Nothing => putStrLn "Failed to initialize" - - Right result <- process handle 42 - | Left err => putStrLn $ "Error: " ++ errorDescription err - - free handle - putStrLn "Success" -``` - -### From Rust - -```rust -#[link(name = "{{project}}")] -extern "C" { - fn {{project}}_init() -> *mut std::ffi::c_void; - fn {{project}}_free(handle: *mut std::ffi::c_void); - fn {{project}}_process(handle: *mut std::ffi::c_void, input: u32) -> i32; -} - -fn main() { - unsafe { - let handle = {{project}}_init(); - assert!(!handle.is_null()); - - let result = {{project}}_process(handle, 42); - assert_eq!(result, 0); - - {{project}}_free(handle); - } -} -``` - -### From Julia - -```julia -const lib{{project}} = "lib{{project}}" - -function init() - handle = ccall((:{{project}}_init, lib{{project}}), Ptr{Cvoid}, ()) - handle == C_NULL && error("Failed to initialize") - handle -end - -function process(handle, input) - result = ccall((:{{project}}_process, lib{{project}}), Cint, (Ptr{Cvoid}, UInt32), handle, input) - result -end - -function cleanup(handle) - ccall((:{{project}}_free, lib{{project}}), Cvoid, (Ptr{Cvoid},), handle) -end - -# Usage -handle = init() -try - result = process(handle, 42) - println("Result: $result") -finally - cleanup(handle) -end -``` - -## Testing - -### Unit Tests (Zig) - -```bash -cd ffi/zig -zig build test -``` - -### Integration Tests - -```bash -cd ffi/zig -zig build test-integration -``` - -### ABI Verification (Idris2) - -```idris --- Compile-time verification -%runElab verifyABI - --- Runtime checks -main : IO () -main = do - verifyLayoutsCorrect - verifyAlignmentsCorrect - putStrLn "ABI verification passed" -``` - -## Contributing - -When modifying the ABI/FFI: - -1. **Update ABI first** (`src/abi/*.idr`) - - Modify type definitions - - Update proofs - - Ensure backward compatibility - -2. **Generate C header** - ```bash - idris2 --cg c-header src/abi/Types.idr -o generated/abi/{{project}}.h - ``` - -3. **Update FFI implementation** (`ffi/zig/src/main.zig`) - - Implement new functions - - Match ABI types exactly - -4. **Add tests** - - Unit tests in Zig - - Integration tests - - ABI verification tests - -5. **Update documentation** - - Function signatures - - Usage examples - - Migration guide (if breaking changes) - -## License - -PMPL-1.0-or-later - -## See Also - -- [Idris2 Documentation](https://idris2.readthedocs.io) -- [Zig Documentation](https://ziglang.org/documentation/master/) -- [Rhodium Standard Repositories](https://github.com/hyperpolymath/rhodium-standard-repositories) -- [FFI Migration Guide](../ffi-migration-guide.md) -- [ABI Migration Guide](../abi-migration-guide.md) diff --git a/union-policy-parser/CODE_OF_CONDUCT.md b/union-policy-parser/CODE_OF_CONDUCT.md deleted file mode 100644 index 9020def..0000000 --- a/union-policy-parser/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,30 +0,0 @@ - -# Contributor Covenant Code of Conduct - -## Our Pledge - -We pledge to make participation a harassment-free experience for everyone. - -## Our Standards - -**Positive behavior:** -* Using welcoming language -* Being respectful of differing viewpoints -* Accepting constructive criticism -* Focusing on what is best for the community - -**Unacceptable behavior:** -* Harassment, trolling, or personal attacks -* Publishing private information without permission - -## Enforcement - -Report issues to the maintainers. All complaints will be reviewed. - -## Attribution - -Adapted from [Contributor Covenant](https://www.contributor-covenant.org/) v2.1. - diff --git a/union-policy-parser/CONTRIBUTING.adoc b/union-policy-parser/CONTRIBUTING.adoc deleted file mode 100644 index 2ead10c..0000000 --- a/union-policy-parser/CONTRIBUTING.adoc +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= Contributing Guide - -== Getting Started - -1. Fork the repository -2. Create a feature branch from `main` -3. Sign off commits (`git commit -s`) -4. Submit a pull request - -== Commit Guidelines - -* Conventional commits: `type(scope): description` -* Sign all commits (DCO required) -* Atomic, focused commits - -== License - -Contributions licensed under project license. - diff --git a/union-policy-parser/CONTRIBUTING.md b/union-policy-parser/CONTRIBUTING.md deleted file mode 100644 index c60d14e..0000000 --- a/union-policy-parser/CONTRIBUTING.md +++ /dev/null @@ -1,120 +0,0 @@ - -# Clone the repository -git clone https://github.com/hyperpolymath/palimpsest-plasma.git -cd palimpsest-plasma - -# Using Nix (recommended for reproducibility) -nix develop - -# Or using toolbox/distrobox -toolbox create palimpsest-plasma-dev -toolbox enter palimpsest-plasma-dev -# Install dependencies manually - -# Verify setup -just check # or: cargo check / mix compile / etc. -just test # Run test suite -``` - -### Repository Structure -``` -palimpsest-plasma/ -├── src/ # Source code (Perimeter 1-2) -├── lib/ # Library code (Perimeter 1-2) -├── extensions/ # Extensions (Perimeter 2) -├── plugins/ # Plugins (Perimeter 2) -├── tools/ # Tooling (Perimeter 2) -├── docs/ # Documentation (Perimeter 3) -│ ├── architecture/ # ADRs, specs (Perimeter 2) -│ └── proposals/ # RFCs (Perimeter 3) -├── examples/ # Examples (Perimeter 3) -├── spec/ # Spec tests (Perimeter 3) -├── tests/ # Test suite (Perimeter 2-3) -├── .well-known/ # Protocol files (Perimeter 1-3) -├── .github/ # GitHub config (Perimeter 1) -│ ├── ISSUE_TEMPLATE/ -│ └── workflows/ -├── CHANGELOG.md -├── CODE_OF_CONDUCT.md -├── CONTRIBUTING.md # This file -├── GOVERNANCE.md -├── LICENSE -├── MAINTAINERS.md -├── README.adoc -├── SECURITY.md -├── flake.nix # Nix flake (Perimeter 1) -└── Justfile # Task runner (Perimeter 1) -``` - ---- - -## How to Contribute - -### Reporting Bugs - -**Before reporting**: -1. Search existing issues -2. Check if it's already fixed in `main` -3. Determine which perimeter the bug affects - -**When reporting**: - -Use the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) and include: - -- Clear, descriptive title -- Environment details (OS, versions, toolchain) -- Steps to reproduce -- Expected vs actual behaviour -- Logs, screenshots, or minimal reproduction - -### Suggesting Features - -**Before suggesting**: -1. Check the [roadmap](ROADMAP.md) if available -2. Search existing issues and discussions -3. Consider which perimeter the feature belongs to - -**When suggesting**: - -Use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.md) and include: - -- Problem statement (what pain point does this solve?) -- Proposed solution -- Alternatives considered -- Which perimeter this affects - -### Your First Contribution - -Look for issues labelled: - -- [`good first issue`](https://github.com/hyperpolymath/palimpsest-plasma/labels/good%20first%20issue) — Simple Perimeter 3 tasks -- [`help wanted`](https://github.com/hyperpolymath/palimpsest-plasma/labels/help%20wanted) — Community help needed -- [`documentation`](https://github.com/hyperpolymath/palimpsest-plasma/labels/documentation) — Docs improvements -- [`perimeter-3`](https://github.com/hyperpolymath/palimpsest-plasma/labels/perimeter-3) — Community sandbox scope - ---- - -## Development Workflow - -### Branch Naming -``` -docs/short-description # Documentation (P3) -test/what-added # Test additions (P3) -feat/short-description # New features (P2) -fix/issue-number-description # Bug fixes (P2) -refactor/what-changed # Code improvements (P2) -security/what-fixed # Security fixes (P1-2) -``` - -### Commit Messages - -We follow [Conventional Commits](https://www.conventionalcommits.org/): -``` -(): - -[optional body] - -[optional footer] diff --git a/union-policy-parser/Documentation/ARCHITECTURE.adoc b/union-policy-parser/Documentation/ARCHITECTURE.adoc deleted file mode 100644 index d07cec3..0000000 --- a/union-policy-parser/Documentation/ARCHITECTURE.adoc +++ /dev/null @@ -1,771 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= Union Policy Parser Architecture -:toc: left -:toclevels: 3 -:license: PMPL-1.0-or-later - -== Abstract - -This document defines the architecture for **union-policy-parser**, a tool that validates -employment contracts, editorial policies, and collective agreements against union standards -(NUJ, IWW, UCU) using **A2ML (Attested Markup Language)** for document structure and -**K9-SVC** for self-validating deployment. - -== Design Principles - -[%header,cols="1,3"] -|=== -| Principle | Rationale - -| **Separation of Concerns** -| A2ML handles document structure; K9 handles validation + deployment; union-policy-parser handles policy logic. - -| **Progressive Strictness** -| Three validation modes: Lax (parse), Checked (validate structure), Attested (verify legal compliance). - -| **Union-First Design** -| Empowers unions/workers more than employers; asymmetrical power through automation. - -| **Non-Proprietary Stack** -| PMPL-1.0-or-later license; no vendor lock-in; open standards (A2ML, K9, Nickel). - -| **Formal Verification** -| Use Idris2 typed core for provable contract properties. - -| **Self-Validating Documents** -| K9 wraps A2ML contracts so they can validate/deploy themselves. -|=== - -== Three-Layer Architecture - ----- -+--------------------------------------------------+ -| Layer 3: Deployment & Distribution (K9-SVC) | -| - Self-validating contract files (.k9) | -| - Nickel contracts for validation rules | -| - Just recipes for deployment | -| - Ed25519 signatures for tamper-proofing | -+--------------------------------------------------+ - ↓ wraps -+--------------------------------------------------+ -| Layer 2: Document Structure (A2ML) | -| - Contract markup (.a2ml) | -| - Must/should/could clauses | -| - References to UK/EU law | -| - Typed core (Idris2) for proofs | -+--------------------------------------------------+ - ↓ validates against -+--------------------------------------------------+ -| Layer 1: Policy Engine (union-policy-parser) | -| - Parses A2ML contracts | -| - Validates against NUJ/IWW schemas | -| - Generates grievance reports | -| - CLI + library API | -+--------------------------------------------------+ ----- - -== Components - -=== 1. A2ML Layer (Document Structure) - -**Purpose:** Provides structured, typed markup for contracts with attestation. - -**Format:** -[source,a2ml] ----- -# Employment Contract - -@abstract: -This contract defines the employment relationship... -@end - -@requires: -- UK Employment Rights Act 1996 -- NUJ Code of Conduct -@end - -## Section 1: Employment Terms - -**Days Contracted:** 5 days per week -**Duties:** Teaching, curriculum development - -**Attestation:** Must match Offer Letter. - -@refs: -[1] UK Employment Rights Act 1996 -@end ----- - -**Key Features:** -- Surface syntax (lightweight markup) -- Typed core (Idris2 proofs) -- Opaque payloads (byte-for-byte preservation) -- Deterministic parsing - -**File Extension:** `.a2ml` - -**MIME Type:** `application/vnd.a2ml` (pending IANA registration) - -=== 2. K9-SVC Layer (Self-Validating Deployment) - -**Purpose:** Wraps A2ML contracts in self-validating components with security levels. - -**K9 Pedigree:** -[source,nickel] ----- -{ - name = "employment-contract", - version = "1.0.0", - - security = { - trust_level = 'Hunt, # Requires Ed25519 signature - permissions = { - filesystem = { write = false }, - network = { outbound = false }, - }, - }, - - validation = { - schema = "nuj-code-of-ethics.a2ml", - required_clauses = [ - "source-protection", - "editorial-independence", - "right-of-reply" - ], - }, - - recipes = { - validate = "union-policy-parser validate $INPUT", - deploy = "cp $INPUT /contracts/validated/", - audit = "union-policy-parser audit --schema nuj-ethics.a2ml", - }, -} ----- - -**K9 Leash Security Model:** - -[%header,cols="1,2,2"] -|=== -| Level | Permissions | Use Case - -| **'Kennel** -| Pure data (no execution) -| Viewing contracts safely - -| **'Yard** -| Nickel evaluation only -| Validating contract structure - -| **'Hunt** -| Full execution (requires signature) -| Deploying validated contracts -|=== - -**File Extension:** `.k9` or `.k9.ncl` - -=== 3. Union-Policy-Parser (Policy Engine) - -**Purpose:** Core validation engine that checks contracts against union standards. - -**Architecture:** - ----- -union-policy-parser/ -├── src/ -│ ├── rust/ # CLI tool (primary implementation) -│ │ ├── main.rs # CLI entry point -│ │ ├── parser.rs # A2ML parser integration -│ │ ├── validator.rs # Schema validation -│ │ └── reporter.rs # Grievance generation -│ │ -│ ├── julia/ # Batch processing scripts -│ │ ├── Main.jl # Batch validator -│ │ └── parser.jl # A2ML integration -│ │ -│ └── idris2/ # Formal verification (future) -│ └── Contracts.idr # Contract proofs -│ -├── schemas/ # Union policy schemas -│ ├── nuj-code-of-ethics.a2ml -│ ├── iww-freelancer-rights.a2ml -│ ├── ucu-academic-standards.a2ml -│ └── uk-employment-law.a2ml -│ -├── examples/ # Sample contracts -│ ├── ou-day-employee-contract.a2ml -│ └── freelance-journalist-contract.a2ml -│ -└── k9/ # K9 wrappers - ├── employment-contract.k9.ncl - └── nuj-editorial-policy.k9.ncl ----- - -**Languages:** -- **Primary:** Rust (CLI tool, performance) -- **Secondary:** Julia (batch processing, data analysis) -- **Proofs:** Idris2 (formal verification, future) - -== Workflows - -=== Workflow 1: Freelancer Contract Validation - -[source,bash] ----- -# 1. Author contract in A2ML -vim freelance-contract.a2ml - -# 2. Validate against NUJ schema (local check) -union-policy-parser validate freelance-contract.a2ml \ - --schema schemas/nuj-code-of-ethics.a2ml \ - --mode checked - -# Output: ✅ Valid | ⚠️ Warnings | ❌ Invalid - -# 3. Wrap in K9 for self-validation -k9-package \ - --input freelance-contract.a2ml \ - --pedigree k9/freelance-template.ncl \ - --output freelance-contract.k9 - -# 4. Sign for tamper-proofing (union rep signs) -k9-sign \ - --key ~/.ssh/nuj-rep-ed25519 \ - --input freelance-contract.k9 - -# 5. Distribute to freelancer -# Freelancer can verify signature before signing -k9-verify --input freelance-contract.k9 ----- - -=== Workflow 2: Employer Contract Batch Audit - -[source,bash] ----- -# 1. Employer uploads batch of contracts -ls contracts/*.pdf - -# 2. Convert PDF to A2ML (manual or automated) -for pdf in contracts/*.pdf; do - pdf-to-a2ml "$pdf" > "contracts/$(basename $pdf .pdf).a2ml" -done - -# 3. Batch validate with Julia -julia src/julia/Main.jl \ - --input-dir contracts/ \ - --schema schemas/nuj-code-of-ethics.a2ml \ - --output-report audit-report.json - -# 4. Generate compliance report -union-policy-parser report \ - --input audit-report.json \ - --format html \ - --output compliance-report.html - -# Output: -# - 12 contracts missing "source-protection" clause -# - 5 contracts lack "editorial-independence" -# - 3 contracts have invalid "termination" terms ----- - -=== Workflow 3: Automated Grievance Generation - -[source,bash] ----- -# 1. Parse contract and detect violation -union-policy-parser validate employee-contract.a2ml \ - --schema schemas/nuj-code-of-ethics.a2ml \ - --auto-grievance - -# 2. System detects missing "right-of-reply" clause - -# 3. Auto-generate grievance letter -union-policy-parser grievance \ - --contract employee-contract.a2ml \ - --violation "missing-right-of-reply" \ - --template templates/nuj-grievance.md \ - --output grievance-2025-001.md - -# Output: grievance-2025-001.md ----- - -**Generated Grievance Template:** -[source,markdown] ----- -To: [Employer HR Department] -Subject: Formal Grievance - Violation of NUJ Code §3 (Right of Reply) - -Date: 2025-01-29 - -Dear [Employer], - -I am writing to formally raise a grievance under the ACAS Code of Practice 1 -regarding a violation of the NUJ Code of Conduct in my employment contract. - -**Violation:** -The contract (dated 2024-11-01) lacks a "Right of Reply" clause, in breach of: -- NUJ Code of Conduct §3 (Fairness) -- IPSO Editors' Code Clause 1.2 - -**Required Action:** -1. Amend contract to include right of reply provision -2. Confirm editorial independence guarantees -3. Provide written assurance within 14 days - -**Legal References:** -- NUJ Code of Conduct (2024) -- UK Employment Rights Act 1996 -- ACAS Code of Practice 1 - -If this matter is not resolved within 14 days, I will escalate to ACAS mediation. - -Signed: [Employee Name] -NUJ Member ID: [ID] ----- - -== Integration Points - -=== A2ML ↔ Union-Policy-Parser - -**Parser Integration:** - -[source,rust] ----- -// src/rust/parser.rs -use a2ml_parser::{parse, validate, Document}; - -pub fn parse_contract(input: &str) -> Result { - // Parse A2ML surface syntax - let doc = parse(input)?; - - // Validate structure (checked mode) - validate(&doc, ValidationMode::Checked)?; - - Ok(doc) -} ----- - -**Schema Validation:** - -[source,rust] ----- -// src/rust/validator.rs -pub struct Validator { - schema: Document, // NUJ ethics schema -} - -impl Validator { - pub fn validate_contract(&self, contract: &Document) -> ValidationReport { - let mut report = ValidationReport::new(); - - // Check required clauses - for clause in self.schema.required_clauses() { - if !contract.has_clause(clause) { - report.add_error( - format!("Missing required clause: {}", clause) - ); - } - } - - // Check attestations - for attestation in contract.attestations() { - if !self.verify_attestation(attestation) { - report.add_warning( - format!("Unverified attestation: {}", attestation.claim) - ); - } - } - - report - } -} ----- - -=== K9 ↔ Union-Policy-Parser - -**K9 Recipe Integration:** - -[source,nickel] ----- -# k9/employment-contract.k9.ncl -{ - pedigree = { - name = "employment-contract", - version = "1.0.0", - }, - - recipes = { - validate = m%" - union-policy-parser validate ${self.payload} \ - --schema schemas/nuj-code-of-ethics.a2ml \ - --mode attested - "%, - - audit = m%" - union-policy-parser audit ${self.payload} \ - --schema schemas/nuj-code-of-ethics.a2ml \ - --output audit-report.json - "%, - - deploy = m%" - if union-policy-parser validate ${self.payload}; then - cp ${self.payload} /contracts/validated/ - else - echo "Validation failed - contract not deployed" - exit 1 - fi - "%, - }, - - # Embedded A2ML payload - payload | doc%" - # Employment Contract - - @abstract: - This contract... - @end - - ## Source Protection - The employer guarantees confidential source protection... - "%, -} ----- - -**Running K9 Validation:** - -[source,bash] ----- -# Validate embedded contract via K9 recipe -just -f employment-contract.k9.ncl validate - -# Output: -# ✅ Contract validates against NUJ schema -# ✅ All required clauses present -# ⚠️ Missing optional clause: "algorithmic-transparency" ----- - -== Security Model - -=== Asymmetrical Power Design - -**Union Advantages:** -- Automated compliance checking (employers must audit manually) -- Grievance template generation (instant response) -- Batch validation (scale across many members) -- Formal proofs (legal backing via Idris2) -- Ed25519 signatures (tamper-proof contracts) - -**Employer Constraints:** -- Must comply with schemas or face grievances -- Cannot modify K9-wrapped contracts (signatures break) -- Transparent audit trails (all checks logged) -- Public accountability (OpenSSF Scorecard, SPDX headers) - -=== Tamper-Proofing with K9 - -**Signature Chain:** - -[source,bash] ----- -# 1. Union rep authors contract in A2ML -vim contract.a2ml - -# 2. Wrap in K9 -k9-package --input contract.a2ml --output contract.k9 - -# 3. Sign with union's Ed25519 key -k9-sign --key ~/.ssh/nuj-union-ed25519 --input contract.k9 - -# 4. Distribute to employer -# Employer cannot modify without breaking signature - -# 5. Employee verifies before signing -k9-verify --input contract.k9 -# Output: ✅ Signature valid (signed by NUJ Rep) ----- - -**What This Prevents:** -- Employer altering terms after union approval -- Unsigned/unapproved contracts being distributed -- Backdated changes to existing contracts - -=== License Firewall (PMPL-1.0-or-later) - -**Palimpsest Mozilla Public License (PMPL-1.0-or-later):** - -- **Copyleft:** Employers cannot create proprietary forks -- **Ethical use:** AI training requires consent -- **Quantum-safe:** Future-proof against quantum attacks (Exhibit B) -- **Provenance:** All modifications must be signed (Exhibit B) - -**Effect:** -- Employers can *use* union-policy-parser -- Employers *cannot* weaponize it (proprietary extensions blocked) -- All changes must flow back to union (copyleft) - -== Data Flow - ----- - Freelancer Union Rep Employer - | | | - | 1. Upload contract PDF | | - |------------------------>| | - | | | - | 2. Convert to A2ML | - | | | - | 3. Validate (union-policy-parser) - | | | - | 4. Wrap in K9 + sign | - | | | - | 5. Return signed .k9 | | - |<------------------------| | - | | | - | 6. Verify signature | | - | (k9-verify) | | - | | | - | 7. Send to employer | | - |--------------------------------------------------->| - | | | - | | 8. Employer validates - | | (k9 validate recipe) - | | | - | | 9. If valid → sign - | | If invalid → reject - | | | - | 10. Receive countersigned contract | - |<---------------------------------------------------| - | | | - | 11. Verify both signatures | - | - Union rep (original) | - | - Employer (acceptance) | ----- - -== CLI Interface - -=== Basic Commands - -[source,bash] ----- -# Validate a contract -union-policy-parser validate \ - --schema \ - --mode [lax|checked|attested] - -# Generate audit report -union-policy-parser audit \ - --schema \ - --output - -# Auto-generate grievance -union-policy-parser grievance \ - --violation \ - --template \ - --output - -# Batch validate -union-policy-parser batch \ - --schema \ - --output - -# Check schema itself -union-policy-parser check-schema ----- - -=== Validation Modes - -[%header,cols="1,2,2"] -|=== -| Mode | Checks | Output - -| **Lax** -| Parses A2ML syntax only -| Syntax errors - -| **Checked** -| + Validates structure (required fields, references) -| Missing clauses, broken references - -| **Attested** -| + Verifies legal compliance (UK law, NUJ standards) -| Legal violations, non-compliance -|=== - -== Future Enhancements - -=== Phase 1: Core Implementation (Q1 2025) -- [ ] Rust CLI with A2ML parser -- [ ] NUJ/IWW schema validation -- [ ] Basic grievance generation -- [ ] K9 integration for self-validation - -=== Phase 2: Formal Verification (Q2 2025) -- [ ] Idris2 typed core for contract proofs -- [ ] Decidable validation predicates -- [ ] Legal theorem proving (e.g., "Does termination comply with ERA 1996?") - -=== Phase 3: Tooling & Distribution (Q3 2025) -- [ ] WASM runtime for browser validation -- [ ] Web component for freelancer self-service -- [ ] Julia batch processing for union branches -- [ ] Deno distribution for JS ecosystems - -=== Phase 4: AI Integration (Q4 2025) -- [ ] PDF-to-A2ML automated conversion -- [ ] AI-powered clause suggestion (via llm-verify) -- [ ] Neurosymbolic reasoning (symbolic A2ML + neural LLM) -- [ ] Grievance auto-filing via Hypatia - -== Appendices - -=== Appendix A: Example K9 Contract Package - -File: `freelance-journalist-contract.k9.ncl` - -[source,nickel] ----- -{ - pedigree = { - name = "freelance-journalist-contract", - version = "1.0.0", - magic = "K9!", - mime_type = "application/vnd.k9+nickel", - }, - - target = { - os = ["linux", "darwin"], - container_support = true, - }, - - security = { - trust_level = 'Hunt, - permissions = { - filesystem = { read = true, write = false }, - network = { outbound = false }, - subprocess = { spawn = false }, - }, - }, - - validation = { - schema = "schemas/nuj-code-of-ethics.a2ml", - required_clauses = [ - "source-protection", - "editorial-independence", - "right-of-reply", - "copyright-retention", - "payment-terms", - ], - checksum = "sha256:abcd1234...", - }, - - recipes = { - install = "echo 'No installation needed'", - - validate = m%" - union-policy-parser validate ${self.payload_file} \ - --schema ${self.validation.schema} \ - --mode attested - "%, - - audit = m%" - union-policy-parser audit ${self.payload_file} \ - --schema ${self.validation.schema} \ - --output /tmp/audit-report.json - "%, - - deploy = m%" - set -e - if union-policy-parser validate ${self.payload_file}; then - echo "✅ Contract validated - safe to sign" - cp ${self.payload_file} /contracts/validated/ - else - echo "❌ Validation failed - DO NOT SIGN" - exit 1 - fi - "%, - }, - - payload_file = "./freelance-contract.a2ml", -} ----- - -=== Appendix B: Integration with Hypatia (CI/CD) - -File: `.github/workflows/contract-validation.yml` - -[source,yaml] ----- -# SPDX-License-Identifier: PMPL-1.0-or-later -name: Contract Validation - -on: - push: - paths: - - 'contracts/**/*.a2ml' - - 'contracts/**/*.k9' - -permissions: read-all - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - - name: Install union-policy-parser - run: | - cargo install --git https://github.com/hyperpolymath/union-policy-parser - - - name: Install K9 tools - run: | - cargo install --git https://github.com/hyperpolymath/standards/tree/main/k9-svc - - - name: Validate A2ML contracts - run: | - for contract in contracts/*.a2ml; do - union-policy-parser validate "$contract" \ - --schema schemas/nuj-code-of-ethics.a2ml \ - --mode attested - done - - - name: Validate K9 packages - run: | - for k9 in contracts/*.k9; do - just -f "$k9" validate - done - - - name: Generate compliance report - if: always() - run: | - union-policy-parser batch contracts/ \ - --schema schemas/nuj-code-of-ethics.a2ml \ - --output compliance-report.json - - - name: Upload report - if: always() - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4 - with: - name: compliance-report - path: compliance-report.json - - - name: Auto-file grievance on failure - if: failure() - run: | - union-policy-parser grievance \ - --contract $FAILED_CONTRACT \ - --violation auto-detect \ - --template templates/nuj-grievance.md \ - --output grievance-${{ github.run_id }}.md - - # Create GitHub issue for union rep - gh issue create \ - --title "Contract Validation Failure" \ - --body-file grievance-${{ github.run_id }}.md \ - --label grievance \ - --assignee nuj-rep ----- - -== License - -This document is licensed under **PMPL-1.0-or-later** (Palimpsest Mozilla Public License). - -See: https://github.com/hyperpolymath/palimpsest-license diff --git a/union-policy-parser/Documentation/QUICKSTART.adoc b/union-policy-parser/Documentation/QUICKSTART.adoc deleted file mode 100644 index 96f4b19..0000000 --- a/union-policy-parser/Documentation/QUICKSTART.adoc +++ /dev/null @@ -1,399 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= Union Policy Parser - Quickstart Guide -:toc: left -:license: PMPL-1.0-or-later - -== What We Built - -You now have a complete **union-policy-parser** architecture that combines: - -* **A2ML** - Attested Markup Language for contract structure -* **K9-SVC** - Self-validating deployment with cryptographic signatures -* **Union schemas** - NUJ Code of Ethics as machine-readable policy - -== Files Created - -=== 1. Example Contract (A2ML) - -**Location:** `examples/ou-day-employee-contract.a2ml` - -The Open University Day Employees Contract (HRP052) converted to A2ML with: - -* 17 sections fully encoded -* Legal references (UK Employment Rights Act 1996, ACAS, GDPR) -* Required/attestation fields for validation -* Must/should/could clauses - -**Validation:** -[source,bash] ----- -# Once implemented: -union-policy-parser validate examples/ou-day-employee-contract.a2ml \ - --schema schemas/uk-employment-law.a2ml \ - --mode checked ----- - -=== 2. NUJ Ethics Schema (A2ML) - -**Location:** `schemas/nuj-code-of-ethics.a2ml` - -Complete NUJ Code of Conduct encoded with: - -* **15 principles** (8 must, 4 should, 3 could) -* Core ethics: Truth, independence, fairness, privacy, accountability -* Best practices: Transparency, diversity, accessibility -* Advocacy: Mental health, algorithmic transparency, portable benefits -* Enforcement: Grievance procedures, automated checks - -**Key Clauses:** - -[%header,cols="1,2,1"] -|=== -| Clause | Description | Type - -| Truth and Accuracy -| Factual accuracy, prompt corrections -| MUST - -| Independence -| Editorial independence, no conflicts -| MUST - -| Source Protection -| Protect confidential sources -| MUST - -| Fairness -| Right of reply, balanced reporting -| MUST - -| Privacy -| Respect privacy, avoid harassment -| MUST - -| Transparency -| Disclose funding, conflicts -| SHOULD - -| Diversity -| Reflect diverse voices -| SHOULD - -| Portable Benefits -| Health insurance, pensions for freelancers -| COULD -|=== - -**Usage:** -[source,bash] ----- -# Validate a journalist's contract against NUJ ethics -union-policy-parser validate journalist-contract.a2ml \ - --schema schemas/nuj-code-of-ethics.a2ml \ - --clause "source-protection" \ - --clause "editorial-independence" ----- - -=== 3. Architecture Document - -**Location:** `Documentation/ARCHITECTURE.adoc` - -Complete system design including: - -* Three-layer architecture (A2ML + K9 + union-policy-parser) -* Workflows for freelancers, employers, union reps -* Security model (asymmetrical power, Ed25519 signatures) -* CLI interface design -* Integration with K9-SVC and Hypatia (CI/CD) -* Future roadmap (formal verification, WASM, AI) - -== Quick Example: Validating a Contract - -=== Scenario - -A freelance journalist receives a contract. They want to check if it complies with NUJ ethics before signing. - -=== Steps - -**1. Convert contract to A2ML** (or ask employer to provide it): - -[source,bash] ----- -# Manual authoring: -vim my-contract.a2ml - -# Or automated from PDF (future): -pdf-to-a2ml employer-contract.pdf > my-contract.a2ml ----- - -**2. Validate against NUJ schema:** - -[source,bash] ----- -union-policy-parser validate my-contract.a2ml \ - --schema schemas/nuj-code-of-ethics.a2ml \ - --mode attested ----- - -**3. Check output:** - -[source] ----- -✅ VALID: Contract complies with NUJ Code of Ethics - -Required clauses present: - ✓ Source protection (§6) - ✓ Editorial independence (§2) - ✓ Right of reply (§3) - ✓ Copyright retention (§10) - -Warnings: - ⚠️ Missing recommended clause: Algorithmic transparency (§14) - ⚠️ Missing recommended clause: Portable benefits (§15) - -Suggestions: - → Add: "Employer shall not use AI editorial tools without human review" - → Add: "Employer contributes £X/month to freelancer health insurance" ----- - -**4. If invalid, auto-generate grievance:** - -[source,bash] ----- -union-policy-parser grievance my-contract.a2ml \ - --violation "missing-source-protection" \ - --template templates/nuj-grievance.md \ - --output grievance-2025-001.md ----- - -**Output:** Pre-filled grievance letter citing NUJ Code §6, ready to send to employer. - -== Asymmetrical Power: How This Helps Unions - -=== Union Advantages - -[%header,cols="2,3"] -|=== -| Feature | Benefit - -| **Automated validation** -| Instant compliance checks; employers must audit manually - -| **Grievance templates** -| Pre-filled legal letters in seconds; employers need lawyers - -| **Batch processing** -| Validate 100+ contracts at once; scales to union size - -| **K9 signatures** -| Tamper-proof contracts; employer cannot alter terms - -| **PMPL license** -| Employers can use but not weaponize (copyleft blocks proprietary forks) - -| **Formal proofs** -| Idris2 proves contract legality; legal backing in tribunals -|=== - -=== Employer Constraints - -[%header,cols="2,3"] -|=== -| Constraint | Impact - -| **Must comply with schemas** -| Cannot hide unfair terms; all clauses validated - -| **Signature verification** -| Cannot modify union-approved contracts without detection - -| **Transparent audit trails** -| All validation checks logged (OpenSSF Scorecard) - -| **Public accountability** -| Failures visible in CI/CD (GitHub Actions) - -| **No proprietary extensions** -| PMPL license prevents employer-only features -|=== - -== Integration with K9-SVC - -=== Why K9? - -K9 adds **self-validation** and **deployment security** to A2ML contracts: - -* **Leash model:** Three security levels (Kennel/Yard/Hunt) -* **Nickel contracts:** Type-safe validation rules -* **Just recipes:** Automated deployment workflows -* **Ed25519 signatures:** Cryptographic tamper-proofing - -=== Example: K9-Wrapped Contract - -**File:** `contracts/freelance-contract.k9.ncl` - -[source,nickel] ----- -{ - pedigree = { - name = "freelance-journalist-contract", - version = "1.0.0", - }, - - validation = { - schema = "schemas/nuj-code-of-ethics.a2ml", - required_clauses = [ - "source-protection", - "editorial-independence", - "copyright-retention", - ], - }, - - recipes = { - validate = "union-policy-parser validate ${self.payload}", - sign = "k9-sign --key ~/.ssh/nuj-rep.ed25519", - deploy = "cp ${self.payload} /contracts/validated/", - }, - - payload = "./freelance-contract.a2ml", -} ----- - -**Usage:** - -[source,bash] ----- -# Validate via K9 recipe -just -f contracts/freelance-contract.k9.ncl validate - -# Sign with union rep's key -just -f contracts/freelance-contract.k9.ncl sign - -# Verify signature -k9-verify contracts/freelance-contract.k9.ncl -# Output: ✅ Signature valid (NUJ Rep) ----- - -== Next Steps - -=== Immediate (Week 1) - -1. **Implement Rust CLI:** - - A2ML parser integration - - Schema validation logic - - Basic grievance generation - -2. **Test with real contracts:** - - Validate `examples/ou-day-employee-contract.a2ml` - - Check against `schemas/nuj-code-of-ethics.a2ml` - -3. **Add more schemas:** - - `schemas/iww-freelancer-rights.a2ml` - - `schemas/ucu-academic-standards.a2ml` - - `schemas/uk-employment-law.a2ml` - -=== Short-term (Month 1) - -4. **K9 integration:** - - Package contracts as `.k9` files - - Add Ed25519 signing workflow - - Test signature verification - -5. **Julia batch processor:** - - Validate 10+ contracts at once - - Generate compliance reports (JSON/HTML) - -6. **CI/CD with Hypatia:** - - GitHub Actions workflow - - Auto-file grievances on validation failure - -=== Medium-term (Quarter 1) - -7. **Idris2 formal verification:** - - Prove contract properties (e.g., "termination complies with ERA 1996") - - Decidable validation predicates - -8. **WASM runtime:** - - Browser-based validation - - Freelancer self-service portal - -9. **PDF-to-A2ML converter:** - - Automate contract conversion - - Use llm-verify for quality checks - -== FAQ - -**Q: Why A2ML instead of JSON/YAML?** - -A: A2ML provides: - -* **Attestation:** Verify claims against external sources (UK law, NUJ standards) -* **Typed core:** Idris2 proofs for legal guarantees -* **Opaque payloads:** Byte-for-byte preservation (critical for legal docs) -* **Human-readable:** Easier for union members to author/review - -**Q: Why K9 on top of A2ML?** - -A: K9 adds: - -* **Self-validation:** Contracts carry their own validation rules -* **Security levels:** Graduated trust (Kennel/Yard/Hunt) -* **Signatures:** Tamper-proof with Ed25519 -* **Deployment:** Just recipes for automated workflows - -**Q: Can employers use this against unions?** - -A: No, due to **asymmetrical design**: - -* PMPL license prevents proprietary forks (copyleft) -* Unions get automation; employers face manual audits -* K9 signatures prevent contract tampering -* Formal proofs provide legal backing for unions - -**Q: What about non-NUJ unions (IWW, UCU)?** - -A: Easy to extend: - -* Create `schemas/iww-freelancer-rights.a2ml` -* Create `schemas/ucu-academic-standards.a2ml` -* Same validation engine, different schemas - -**Q: How do I contribute?** - -A: See `CONTRIBUTING.adoc`: - -* License: PMPL-1.0-or-later -* Languages: Rust (CLI), Julia (batch), Idris2 (proofs) -* Submit A2ML schemas for new unions -* Improve validation logic -* Add grievance templates - -== Resources - -**Documentation:** - -* `ARCHITECTURE.adoc` - Full system design -* `examples/ou-day-employee-contract.a2ml` - Real contract example -* `schemas/nuj-code-of-ethics.a2ml` - NUJ policy schema - -**External References:** - -* A2ML repo: `~/Documents/hyperpolymath-repos/a2ml/` -* K9-SVC repo: `~/Documents/hyperpolymath-repos/k9-svc/` -* NUJ Code of Conduct: https://www.nuj.org.uk/about/nuj-code/ -* ACAS Code of Practice 1: https://www.acas.org.uk - -**Community:** - -* GitHub: https://github.com/hyperpolymath/union-policy-parser -* License: PMPL-1.0-or-later (Palimpsest MPL) -* Contact: Jonathan D.A. Jewell - -== License - -This document and all union-policy-parser code is licensed under: - -**PMPL-1.0-or-later** (Palimpsest Mozilla Public License) - -See: `LICENSE` file in repository root. diff --git a/union-policy-parser/Justfile b/union-policy-parser/Justfile deleted file mode 100644 index 8e9a28c..0000000 --- a/union-policy-parser/Justfile +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# justfile - Just recipes for this project -# See: https://github.com/hyperpolymath/mustfile - -# Default recipe -default: - @just --list - -# Build the project -build: - @echo "Build not configured yet" - -# Run tests -test: - @echo "Tests not configured yet" - -# Format code -fmt: - @echo "Formatting not configured yet" - -# Lint code -lint: - @echo "Linting not configured yet" - -# Clean build artifacts -clean: - @echo "Clean not configured yet" - -secret-scan-trufflehog: - @command -v trufflehog >/dev/null && trufflehog filesystem . --only-verified || true diff --git a/union-policy-parser/LICENSE b/union-policy-parser/LICENSE deleted file mode 100644 index 003be8c..0000000 --- a/union-policy-parser/LICENSE +++ /dev/null @@ -1,153 +0,0 @@ -SPDX-License-Identifier: PMPL-1.0-or-later -SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete PMPL-1.0-or-later text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from PMPL-1.0-or-later apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under PMPL-1.0-or-later: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. PMPL-1.0-or-later Compatibility - Covered Software under this License may be combined with software under - PMPL-1.0-or-later. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of PMPL-1.0-or-later Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL LICENSE VERSION 1.0 --------------------------------------------------------------------------------- - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues diff --git a/union-policy-parser/MAINTAINERS.adoc b/union-policy-parser/MAINTAINERS.adoc deleted file mode 100644 index becaa3e..0000000 --- a/union-policy-parser/MAINTAINERS.adoc +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= Maintainers -:toc: preamble - -This document lists the maintainers of this project and their responsibilities. - -== Current Maintainers - -[cols="2,3,2",options="header"] -|=== -| Name | Role | Contact - -| Jonathan D.A. Jewell -| Lead Maintainer -| https://github.com/hyperpolymath[@hyperpolymath] -|=== - -== Responsibilities - -Maintainers are responsible for: - -* Reviewing and merging pull requests -* Triaging issues and feature requests -* Ensuring code quality and security standards -* Managing releases and versioning -* Upholding the project's code of conduct - -== Becoming a Maintainer - -Contributors who demonstrate: - -* Consistent, high-quality contributions -* Understanding of the project's goals and standards -* Constructive participation in discussions -* Commitment to the project's long-term health - -May be invited to become maintainers at the discretion of existing maintainers. - -== Decision Making - -* Routine decisions (bug fixes, minor improvements) can be made by any maintainer -* Significant changes require discussion and consensus among maintainers -* Breaking changes or major features should be discussed in issues before implementation - -== Contact - -For questions about project governance, open an issue or contact the maintainers listed above. diff --git a/union-policy-parser/Mustfile b/union-policy-parser/Mustfile deleted file mode 100644 index 4f5e24e..0000000 --- a/union-policy-parser/Mustfile +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Mustfile - hyperpolymath mandatory checks -# See: https://github.com/hyperpolymath/mustfile - -version: 1 - -checks: - - name: security - run: just lint - - name: tests - run: just test - - name: format - run: just fmt diff --git a/union-policy-parser/PALIMPSEST.adoc b/union-policy-parser/PALIMPSEST.adoc deleted file mode 100644 index 1449779..0000000 --- a/union-policy-parser/PALIMPSEST.adoc +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= Palimpsest License -:toc: -:toc-placement!: - -image:https://img.shields.io/badge/License-PMPL--1.0-blue.svg[License: PMPL-1.0,link="https://github.com/hyperpolymath/palimpsest-license"] -image:https://img.shields.io/badge/Philosophy-Palimpsest-indigo.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-license"] - -toc::[] - -== Legal Status - -This project is licensed under the **Palimpsest-MPL License 1.0 (PMPL-1.0)**. -For SPDX and tooling, use **PMPL-1.0-or-later**. - -PMPL-1.0 incorporates the Mozilla Public License 2.0 by reference and adds -ethical-use, provenance, and lineage requirements. - -== What PMPL Adds - -* **Emotional Lineage** - preserve narrative intent and cultural context -* **Provenance Integrity** - retain attribution and lineage metadata -* **Ethical Use Constraints** - explicit consent for non-interpretive AI training -* **Quantum-Safe Provenance (optional)** - post-quantum signature support - -== How to Adopt - -1. Include the PMPL-1.0 license text in `LICENSE`. -2. Add SPDX headers to source files: - `SPDX-License-Identifier: PMPL-1.0-or-later` -3. Add a Palimpsest badge to your README (see `assets/badges/` and `embed/license-blocks/`). - -== Versioning - -See `VERSIONING.adoc` for the release process and the "-or-later" model. -The current legal text is PMPL-1.0. - -== References - -* `legal/README.adoc` -* `assets/badges/README.md` -* `embed/license-blocks/README.md` diff --git a/union-policy-parser/README.adoc b/union-policy-parser/README.adoc deleted file mode 100644 index 757e095..0000000 --- a/union-policy-parser/README.adoc +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -image:https://img.shields.io/badge/License-PMPL--1.0-blue.svg[License: PMPL-1.0,link="https://github.com/hyperpolymath/palimpsest-license"] -image:https://img.shields.io/badge/Philosophy-Palimpsest-indigo.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-license"] - - -= Union Policy Parser - -:toc: -:toc-placement!: - -**Repository project specification - will be uploaded shortly** - -toc::[] - -== Status - -[WARNING] -==== -This repository is in initial setup phase. No implementation code has been uploaded yet. -==== - -== Current Contents - -[cols="1,3"] -|=== -|Path |Description - -|`.claude/CLAUDE.md` -|Language policy (Hyperpolymath Standard) - defines allowed/banned languages and tools - -|`.github/workflows/mirror.yml` -|CI workflow for multi-forge mirroring (GitLab, Codeberg, Bitbucket) -|=== - -== Technology Stack (per Language Policy) - -Allowed technologies for this project: - -* **ReScript** - Primary application code -* **Deno** - Runtime and package management -* **Rust** - Performance-critical components, WASM -* **Gleam** - Backend services (BEAM or JS target) -* **Guile Scheme** - State/meta files - -== License - -PMPL-1.0-or-later diff --git a/union-policy-parser/ROADMAP.adoc b/union-policy-parser/ROADMAP.adoc deleted file mode 100644 index 786a1a9..0000000 --- a/union-policy-parser/ROADMAP.adoc +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= Union Policy Parser - Roadmap -:toc: - -== Current State - -*Phase 0: Repository Infrastructure* [COMPLETE] - -* [x] Repository created -* [x] Language policy established (`.claude/CLAUDE.md`) -* [x] Multi-forge mirroring configured (GitHub -> GitLab/Codeberg/Bitbucket) -* [x] SPDX license headers - -== Planned Development - -[NOTE] -==== -Detailed roadmap will be added when project specification is uploaded. -==== - -=== Phase 1: Specification - -* [ ] Upload project specification -* [ ] Define union policy grammar/schema -* [ ] Document input/output formats -* [ ] Establish test fixtures - -=== Phase 2: Core Implementation - -* [ ] Parser implementation (ReScript or Rust) -* [ ] AST/IR definition -* [ ] Validation layer -* [ ] Error reporting - -=== Phase 3: Tooling - -* [ ] CLI interface -* [ ] Library API -* [ ] WASM build (if Rust) -* [ ] Deno module distribution - -=== Phase 4: Integration - -* [ ] Documentation -* [ ] CI/CD pipeline for testing -* [ ] Package distribution (Guix, Deno) - -== Contributing - -Awaiting project specification before accepting contributions. diff --git a/union-policy-parser/RSR_OUTLINE.adoc b/union-policy-parser/RSR_OUTLINE.adoc deleted file mode 100644 index 93b9e32..0000000 --- a/union-policy-parser/RSR_OUTLINE.adoc +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= RSR Template Repository - -image:[Palimpsest-MPL-1.0,link="https://github.com/hyperpolymath/palimpsest-license"] image:[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-license"] -:toc: -:sectnums: - -// Badges -image:https://img.shields.io/badge/RSR-Infrastructure-cd7f32[RSR Infrastructure] -image:https://img.shields.io/badge/Phase-Maintenance-brightgreen[Phase] -image:https://img.shields.io/badge/Guix-Primary-purple?logo=gnu[Guix] - -== Overview - -**The canonical template for RSR (Rhodium Standard Repository) projects.** - -This repository provides the standardized structure, configuration, and tooling for all 139 repos in the hyperpolymath ecosystem. Use it to: - -* Bootstrap new projects with RSR compliance -* Reference the standard directory structure -* Copy configuration templates (Justfile, STATE.scm, etc.) - -== Quick Start - -[source,bash] ----- -# Clone the template -git clone https://github.com/hyperpolymath/RSR-template-repo my-project -cd my-project - -# Remove template git history -rm -rf .git -git init - -# Customize -sed -i 's/RSR-template-repo/my-project/g' Justfile guix.scm README.adoc - -# Enter development environment -guix shell -D -f guix.scm - -# Validate compliance -just validate-rsr ----- - -== What's Included - -[cols="1,3"] -|=== -|File/Directory |Purpose - -|`.editorconfig` -|Editor configuration (indent, charset) - -|`.gitignore` -|Standard ignore patterns - -|`.guix-channel` -|Guix channel definition - -|`.well-known/` -|RFC-compliant metadata (security.txt, ai.txt, humans.txt) - -|`docs/` -|Documentation directory - -|`guix.scm` -|Guix package definition - -|`justfile` -|Task runner with 50+ recipes - -|`LICENSE.txt` -|PMPL-1.0-or-later - -|`README.adoc` -|This file - -|`RSR_COMPLIANCE.adoc` -|Compliance tracking - -|`STATE.scm` -|Project state checkpoint -|=== - -== Justfile Features - -The template Justfile provides: - -* **~10 billion recipe combinations** via matrix recipes -* **Cookbook generation**: `just cookbook` → `docs/just-cookbook.adoc` -* **Man page generation**: `just man` → `docs/man/project.1` -* **RSR validation**: `just validate-rsr` -* **STATE.scm management**: `just state-touch`, `just state-phase` -* **Container support**: `just container-build`, `just container-push` -* **CI matrix**: `just ci-matrix [stage] [depth]` - -=== Key Recipes - -[source,bash] ----- -just # Show all recipes -just help # Detailed help -just info # Project info -just combinations # Show matrix options - -just build # Build (debug) -just test # Run tests -just quality # Format + lint + test -just ci # Full CI pipeline - -just validate # RSR + STATE validation -just docs # Generate all docs -just cookbook # Generate Justfile docs - -just guix-shell # Guix dev environment -just container-build # Build container ----- - -== Directory Structure - -[source] ----- -project/ -├── .editorconfig # Editor settings -├── .gitignore # Git ignore -├── .guix-channel # Guix channel -├── .well-known/ # RFC metadata -│ ├── ai.txt -│ ├── humans.txt -│ └── security.txt -├── config/ # Nickel configs (optional) -├── docs/ # Documentation -│ ├── generated/ -│ ├── man/ -│ └── just-cookbook.adoc -├── guix.scm # Guix package -├── Justfile # Task runner -├── LICENSE.txt # Dual license -├── README.adoc # Overview -├── RSR_COMPLIANCE.adoc # Compliance -├── src/ # Source code -├── STATE.scm # State checkpoint -└── tests/ # Tests ----- - -== RSR Compliance - -=== Language Tiers - -* **Tier 1** (Gold): Rust, Elixir, Zig, Ada, Haskell, ReScript -* **Tier 2** (Silver): Nickel, Racket, Guile Scheme, Nix -* **Infrastructure**: Guix channels, derivations - -=== Required Files - -* `.editorconfig` -* `.gitignore` -* `justfile` -* `README.adoc` -* `RSR_COMPLIANCE.adoc` -* `LICENSE` (PMPL-1.0-or-later) -* `.well-known/security.txt` -* `.well-known/ai.txt` -* `.well-known/humans.txt` -* `guix.scm` OR `flake.nix` - -=== Prohibited - -* Python outside `salt/` directory -* TypeScript/JavaScript (use ReScript) -* CUE (use Guile/Nickel) -* `Dockerfile` (use `Containerfile`) - -== STATE.scm - -The STATE.scm file tracks project state: - -[source,scheme] ----- -(define state - `((metadata - (project . "my-project") - (updated . "2025-12-10")) - (position - (phase . implementation) ; design|implementation|testing|maintenance|archived - (maturity . beta)) ; experimental|alpha|beta|production|lts - (ecosystem - (part-of . ("RSR Framework")) - (depends-on . ())))) ----- - -== Badge Schema - -Generate badges from STATE.scm: - -[source,bash] ----- -just badges standard ----- - -See `docs/BADGE_SCHEMA.adoc` for the full badge taxonomy. - -== Ecosystem Integration - -This template is part of: - -* **STATE.scm Ecosystem**: Conversation checkpoints -* **RSR Framework**: Repository standards -* **Consent-Aware-HTTP**: .well-known compliance - -== License - -SPDX-License-Identifier: PMPL-1.0-or-later - -== Links - -* https://github.com/hyperpolymath/elegant-STATE[elegant-STATE] - STATE.scm tooling -* https://github.com/hyperpolymath/conative-gating[conative-gating] - Policy enforcement -* https://rhodium.sh[Rhodium Standard] - RSR documentation diff --git a/union-policy-parser/SECURITY.md b/union-policy-parser/SECURITY.md deleted file mode 100644 index 6183454..0000000 --- a/union-policy-parser/SECURITY.md +++ /dev/null @@ -1,332 +0,0 @@ - -Security Policy - -We take security seriously. We appreciate your efforts to responsibly disclose vulnerabilities and will make every effort to acknowledge your contributions. -Table of Contents - - Reporting a Vulnerability - What to Include - Response Timeline - Disclosure Policy - Scope - Safe Harbour - Recognition - Security Updates - Security Best Practices - -Reporting a Vulnerability -Preferred Method: GitHub Security Advisories - -The preferred method for reporting security vulnerabilities is through GitHub's Security Advisory feature: - - Navigate to Report a Vulnerability - Click "Report a vulnerability" - Complete the form with as much detail as possible - Submit — we'll receive a private notification - -This method ensures: - - End-to-end encryption of your report - Private discussion space for collaboration - Coordinated disclosure tooling - Automatic credit when the advisory is published - -Alternative: Encrypted Email - -If you cannot use GitHub Security Advisories, you may email us directly: - -Email security@hyperpolymath.org -PGP Key Download Public Key -Fingerprint See GPG key - -# Import our PGP key -curl -sSL https://hyperpolymath.org/gpg/security.asc | gpg --import - -# Verify fingerprint -gpg --fingerprint security@hyperpolymath.org - -# Encrypt your report -gpg --armor --encrypt --recipient security@hyperpolymath.org report.txt - - ⚠️ Important: Do not report security vulnerabilities through public GitHub issues, pull requests, discussions, or social media. - -What to Include - -A good vulnerability report helps us understand and reproduce the issue quickly. -Required Information - - Description: Clear explanation of the vulnerability - Impact: What an attacker could achieve (confidentiality, integrity, availability) - Affected versions: Which versions/commits are affected - Reproduction steps: Detailed steps to reproduce the issue - -Helpful Additional Information - - Proof of concept: Code, scripts, or screenshots demonstrating the vulnerability - Attack scenario: Realistic attack scenario showing exploitability - CVSS score: Your assessment of severity (use CVSS 3.1 Calculator) - CWE ID: Common Weakness Enumeration identifier if known - Suggested fix: If you have ideas for remediation - References: Links to related vulnerabilities, research, or advisories - -Example Report Structure - -## Summary -[One-sentence description of the vulnerability] - -## Vulnerability Type -[e.g., SQL Injection, XSS, SSRF, Path Traversal, etc.] - -## Affected Component -[File path, function name, API endpoint, etc.] - -## Affected Versions -[Version range or specific commits] - -## Severity Assessment -- CVSS 3.1 Score: [X.X] -- CVSS Vector: [CVSS:3.1/AV:X/AC:X/PR:X/UI:X/S:X/C:X/I:X/A:X] - -## Description -[Detailed technical description] - -## Steps to Reproduce -1. [First step] -2. [Second step] -3. [...] - -## Proof of Concept -[Code, curl commands, screenshots, etc.] - -## Impact -[What can an attacker achieve?] - -## Suggested Remediation -[Optional: your ideas for fixing] - -## References -[Links to related issues, CVEs, research] - -Response Timeline - -We commit to the following response times: -Stage Timeframe Description -Initial Response 48 hours We acknowledge receipt and confirm we're investigating -Triage 7 days We assess severity, confirm the vulnerability, and estimate timeline -Status Update Every 7 days Regular updates on remediation progress -Resolution 90 days Target for fix development and release (complex issues may take longer) -Disclosure 90 days Public disclosure after fix is available (coordinated with you) - - Note: These are targets, not guarantees. Complex vulnerabilities may require more time. We'll communicate openly about any delays. - -Disclosure Policy - -We follow coordinated disclosure (also known as responsible disclosure): - - You report the vulnerability privately - We acknowledge and begin investigation - We develop a fix and prepare a release - We coordinate disclosure timing with you - We publish security advisory and fix simultaneously - You may publish your research after disclosure - -Our Commitments - - We will not take legal action against researchers who follow this policy - We will work with you to understand and resolve the issue - We will credit you in the security advisory (unless you prefer anonymity) - We will notify you before public disclosure - We will publish advisories with sufficient detail for users to assess risk - -Your Commitments - - Report vulnerabilities promptly after discovery - Give us reasonable time to address the issue before disclosure - Do not access, modify, or delete data beyond what's necessary to demonstrate the vulnerability - Do not degrade service availability (no DoS testing on production) - Do not share vulnerability details with others until coordinated disclosure - -Disclosure Timeline - -Day 0 You report vulnerability -Day 1-2 We acknowledge receipt -Day 7 We confirm vulnerability and share initial assessment -Day 7-90 We develop and test fix -Day 90 Coordinated public disclosure - (earlier if fix is ready; later by mutual agreement) - -If we cannot reach agreement on disclosure timing, we default to 90 days from your initial report. -Scope -In Scope ✅ - -The following are within scope for security research: - - This repository (hyperpolymath/terrapin-ssg) and all its code - Official releases and packages published from this repository - Documentation that could lead to security issues - Build and deployment configurations in this repository - Dependencies (report here, we'll coordinate with upstream) - -Out of Scope ❌ - -The following are not in scope: - - Third-party services we integrate with (report directly to them) - Social engineering attacks against maintainers - Physical security - Denial of service attacks against production infrastructure - Spam, phishing, or other non-technical attacks - Issues already reported or publicly known - Theoretical vulnerabilities without proof of concept - -Qualifying Vulnerabilities - -We're particularly interested in: - - Remote code execution - SQL injection, command injection, code injection - Authentication/authorisation bypass - Cross-site scripting (XSS) and cross-site request forgery (CSRF) - Server-side request forgery (SSRF) - Path traversal / local file inclusion - Information disclosure (credentials, PII, secrets) - Cryptographic weaknesses - Deserialisation vulnerabilities - Memory safety issues (buffer overflows, use-after-free, etc.) - Supply chain vulnerabilities (dependency confusion, etc.) - Significant logic flaws - -Non-Qualifying Issues - -The following generally do not qualify as security vulnerabilities: - - Missing security headers on non-sensitive pages - Clickjacking on pages without sensitive actions - Self-XSS (requires victim to paste code) - Missing rate limiting (unless it enables a specific attack) - Username/email enumeration (unless high-risk context) - Missing cookie flags on non-sensitive cookies - Software version disclosure - Verbose error messages (unless exposing secrets) - Best practice deviations without demonstrable impact - -Safe Harbour - -We support security research conducted in good faith. -Our Promise - -If you conduct security research in accordance with this policy: - - ✅ We will not initiate legal action against you - ✅ We will not report your activity to law enforcement - ✅ We will work with you in good faith to resolve issues - ✅ We consider your research authorised under the Computer Fraud and Abuse Act (CFAA), UK Computer Misuse Act, and similar laws - ✅ We waive any potential claim against you for circumvention of security controls - -Good Faith Requirements - -To qualify for safe harbour, you must: - - Comply with this security policy - Report vulnerabilities promptly - Avoid privacy violations (do not access others' data) - Avoid service degradation (no destructive testing) - Not exploit vulnerabilities beyond proof-of-concept - Not use vulnerabilities for profit (beyond bug bounties where offered) - - ⚠️ Important: This safe harbour does not extend to third-party systems. Always check their policies before testing. - -Recognition - -We believe in recognising security researchers who help us improve. -Hall of Fame - -Researchers who report valid vulnerabilities will be acknowledged in our Security Acknowledgments (unless they prefer anonymity). - -Recognition includes: - - Your name (or chosen alias) - Link to your website/profile (optional) - Brief description of the vulnerability class - Date of report - -What We Offer - - ✅ Public credit in security advisories - ✅ Acknowledgment in release notes - ✅ Entry in our Hall of Fame - ✅ Reference/recommendation letter upon request (for significant findings) - -What We Don't Currently Offer - - ❌ Monetary bug bounties - ❌ Hardware or swag - ❌ Paid security research contracts - - Note: We're a community project with limited resources. Your contributions help everyone who uses this software. - -Security Updates -Receiving Updates - -To stay informed about security updates: - - Watch this repository: Click "Watch" → "Custom" → Select "Security alerts" - GitHub Security Advisories: Published at Security Advisories - Release notes: Security fixes noted in CHANGELOG - -Update Policy -Severity Response -Critical/High Patch release as soon as fix is ready -Medium Included in next scheduled release (or earlier) -Low Included in next scheduled release -Supported Versions -Version Supported Notes -main branch ✅ Yes Latest development -Latest release ✅ Yes Current stable -Previous minor release ✅ Yes Security fixes backported -Older versions ❌ No Please upgrade -Security Best Practices - -When using terrapin-ssg, we recommend: -General - - Keep dependencies up to date - Use the latest stable release - Subscribe to security notifications - Review configuration against security documentation - Follow principle of least privilege - -For Contributors - - Never commit secrets, credentials, or API keys - Use signed commits (git config commit.gpgsign true) - Review dependencies before adding them - Run security linters locally before pushing - Report any concerns about existing code - -Additional Resources - - Our PGP Public Key - Security Advisories - Changelog - Contributing Guidelines - CVE Database - CVSS Calculator - -Contact -Purpose Contact -Security issues Report via GitHub or security@hyperpolymath.org -General questions GitHub Discussions -Other enquiries See README for contact information -Policy Changes - -This security policy may be updated from time to time. Significant changes will be: - - Committed to this repository with a clear commit message - Noted in the changelog - Announced via GitHub Discussions (for major changes) - -Thank you for helping keep terrapin-ssg and its users safe. diff --git a/union-policy-parser/contractiles/README.adoc b/union-policy-parser/contractiles/README.adoc deleted file mode 100644 index 70ad712..0000000 --- a/union-policy-parser/contractiles/README.adoc +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= Contractiles Template Set -:toc: -:sectnums: - -This directory contains the generalized contractiles templates. Copy the `contractiles/` directory into a new repo to establish a consistent operational, validation, trust, recovery, and intent framework. - -== Fill-In Instructions - -1. Update the Mustfile to reflect your real invariants (paths, schema versions, ports). -2. Replace Trustfile.hs placeholders with your actual key paths and verification commands. -3. Adjust Dustfile handlers to match your rollback and recovery tooling. -4. Update Intentfile to mirror the roadmap you want the system to evolve toward. - -== Contents - -* `must/Mustfile` - required invariants and validations. -* `trust/Trustfile.hs` - cryptographic verification steps. -* `dust/Dustfile` - rollback and recovery semantics. -* `lust/Intentfile` - future intent and roadmap direction. diff --git a/union-policy-parser/contractiles/dust/Dustfile b/union-policy-parser/contractiles/dust/Dustfile deleted file mode 100644 index 6f93c6a..0000000 --- a/union-policy-parser/contractiles/dust/Dustfile +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: PLMP-1.0-or-later -# Dustfile template - recovery and rollback semantics - -version: 1 - -recovery: - logs: - - name: decision-log - path: logs/decisions.json - reversible: true - handler: "log-replay --reverse logs/decisions.json" - - policy: - - name: policy-rollback - path: policy/policy.ncl - rollback: "git checkout HEAD~1 -- policy/policy.ncl" - notes: "Rollback policy to the previous known-good revision." - - gateway: - - name: bad-deployment - event: "deploy.failure" - undo: "kubectl rollout undo deployment/gateway" - notes: "Undo a failed deployment while preserving audit logs." - - dust-events: - - name: decision-log-to-dust - source: logs/decisions.json - transform: "dustify --input logs/decisions.json --output logs/dust-events.json" - notes: "Map gateway decision logs into reversible dust events." diff --git a/union-policy-parser/contractiles/must/Mustfile b/union-policy-parser/contractiles/must/Mustfile deleted file mode 100644 index dc2c6b6..0000000 --- a/union-policy-parser/contractiles/must/Mustfile +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-License-Identifier: PLMP-1.0-or-later -# Mustfile - declarative state contract (template) -# See: https://github.com/hyperpolymath/mustfile - -version: 1 - -metadata: - name: project-state-contract - spec: v0.0.1 - description: "Invariant checks for config, policy, gateway, logs, and schema." - -parameters: - gateway_port: "8080" - schema_version: "v0.0.1" - -checks: - - name: config-valid - description: "config/service.yaml must be valid." - run: "yq -e '.' config/service.yaml >/dev/null" - - - name: policy-compiles - description: "policy/policy.ncl must compile." - run: "nickel check policy/policy.ncl" - - - name: gateway-exposes-port - description: "Service must expose the configured port." - run: "bash -uc 'ss -lnt | rg \":${GATEWAY_PORT:-8080}\"'" - - - name: logs-are-json - description: "Logs must be JSON." - run: "bash -uc 'rg --files -g \"*.json\" logs | xargs -r jq -e .'" - - - name: schema-version-matches - description: "Schema must match version spec." - run: "bash -uc 'rg -n \"${SCHEMA_VERSION:-v0.0.1}\" schema'" diff --git a/union-policy-parser/docs/CITATIONS.adoc b/union-policy-parser/docs/CITATIONS.adoc deleted file mode 100644 index c1186f8..0000000 --- a/union-policy-parser/docs/CITATIONS.adoc +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= union-policy-parser - Citation Guide -:toc: - -== BibTeX - -[source,bibtex] ----- -@software{union-policy-parser_2026, - author = {Jewell, Jonathan D.A.}, - title = {union-policy-parser}, - year = {2026}, - url = {https://github.com/hyperpolymath/palimpsest-plasma}, - license = {PMPL-1.0-or-later} -} ----- - -== Harvard Style - -Jewell, J.D.A. (2026) _union-policy-parser_ [Computer software]. Available at: https://github.com/hyperpolymath/palimpsest-plasma - -== OSCOLA - -Jonathan D.A. Jewell, 'union-policy-parser' (2026) - -== MLA - -Jewell, Jonathan D.A. "union-policy-parser." 2026, github.com/hyperpolymath/palimpsest-plasma. - -== APA 7 - -Jewell, J.D.A. (2026). _union-policy-parser_ [Computer software]. GitHub. https://github.com/hyperpolymath/palimpsest-plasma - -== See Also - -* link:../CITATION.cff[CITATION.cff] -* link:../codemeta.json[codemeta.json] diff --git a/union-policy-parser/examples/academic-lecturer-contract.a2ml b/union-policy-parser/examples/academic-lecturer-contract.a2ml deleted file mode 100644 index 123a13b..0000000 --- a/union-policy-parser/examples/academic-lecturer-contract.a2ml +++ /dev/null @@ -1,279 +0,0 @@ -# Academic Lecturer Employment Contract - -@abstract: -This is an example academic employment contract for a Lecturer position at a UK university. -It demonstrates compliance with UCU standards: academic freedom, workload limits, research time, -fair pay, and anti-casualization protections. This contract would pass validation against -ucu-academic-standards.a2ml schema. -@end - -@requires: -- UK Employment Rights Act 1996 -- UK Working Time Regulations 1998 -- Education Act 2011 Section 26 (Academic Freedom) -- UCU Model Contract -- USS (Universities Superannuation Scheme) Regulations -@end - ---- - -## Contract Parties - -**Employee:** Dr. Sarah Johnson -**Employer:** University of Example -**Position:** Lecturer in Sociology -**Start Date:** 2025-09-01 -**Contract Type:** Permanent (full-time, 1.0 FTE) - ---- - -## 1. Academic Freedom - -**Guarantee:** The University guarantees academic freedom as defined in Education Act 2011 Section 26: -- Freedom to question and test received wisdom -- Freedom to put forward new ideas and controversial opinions -- Freedom to determine research agenda without institutional censorship -- Protection from dismissal for lawful speech and academic critique - -**Academic Responsibilities:** -- Research, teaching, and scholarship in sociology -- Freedom to challenge institutional policies through proper channels -- No requirement for pre-approval of research topics - -**Attestation:** *Must* guarantee academic freedom per Education Act 2011 §26 and UCU §1. - ---- - -## 2. Workload Allocation - -**Annual Hours:** 1,560 hours (40 hours/week averaged over 39 weeks) - -**Workload Model (WAM):** -- **Teaching:** 600 hours (38%) - 18 contact hours/week during term -- **Research:** 650 hours (42%) - Protected research time -- **Administration:** 310 hours (20%) - Committees, meetings, pastoral care - -**Workload Review:** Annual workload planning meeting with Head of Department - -**Right to Refuse:** May decline unreasonable additional duties beyond allocated hours - -**Attestation:** *Must* limit workload to ≤40 hours/week per UCU §2. - ---- - -## 3. Teaching Load - -**Contact Hours:** Maximum 18 contact hours per week during teaching term (24 weeks) - -**Teaching Responsibilities:** -- 2 undergraduate modules (lectures + seminars) -- 1 postgraduate seminar -- Dissertation supervision (max 10 students) -- Exam marking (allocated time included in workload) - -**Preparation Time:** Allocated at 1:2 ratio (1 hour teaching = 2 hours prep/marking) - -**Attestation:** *Must* limit teaching to ≤18 contact hours/week per UCU §5. - ---- - -## 4. Research Time - -**Research Allocation:** 650 hours/year (42% of time) protected for research - -**Research Support:** -- £2,000/year research budget -- Conference attendance (1-2 per year, funded) -- Research leave (sabbatical: 1 semester per 6 years) - -**Research Expectations:** -- Publications in peer-reviewed journals -- Grant applications (supported by Research Office) -- PhD supervision (counted as research time) - -**Attestation:** *Must* allocate ≥50% time for research per UCU §4. - ---- - -## 5. Salary and Pay Progression - -**Starting Salary:** £42,000/year (Grade 7, Spine Point 32) - -**Pay Scale:** Academic pay spine, increments awarded annually - -**Cost-of-Living Increases:** Annual CPI + 1% minimum (per UCU pay campaign) - -**Incremental Progression:** Automatic annual increment until top of grade - -**Attestation:** *Must* meet UCU minimum pay scales (£40,000 starting) per UCU §6. - ---- - -## 6. Pension Provision - -**Pension Scheme:** USS (Universities Superannuation Scheme) - -**Employer Contribution:** 21.4% of salary - -**Employee Contribution:** 9.8% of salary (threshold tier) - -**Defined Benefit:** USS provides defined-benefit pension - -**Attestation:** *Must* provide defined-benefit pension with ≥21% employer contribution per UCU §7. - ---- - -## 7. Working Hours and Flexibility - -**Standard Hours:** 40 hours/week averaged over academic year (39 weeks) - -**Flexibility:** Some flexibility in hours due to academic nature of work - -**No Excessive Hours:** University will monitor workload to prevent excessive hours - -**Right to Disconnect:** No expectation of out-of-hours email responses - -**Attestation:** *Must* comply with UK Working Time Regulations 1998 (48-hour max). - ---- - -## 8. Holiday Entitlement - -**Annual Leave:** 30 days/year + 8 public holidays + university closure days (Christmas) - -**Total:** Approximately 45 days/year - -**Carry-Over:** Up to 5 days may be carried to next year - -**Attestation:** *Must* provide statutory holiday minimum (28 days) per UK Working Time Regulations 1998. - ---- - -## 9. Sick Pay - -**Occupational Sick Pay:** -- Full pay: First 26 weeks -- Half pay: Next 26 weeks -- Then Statutory Sick Pay (SSP) - -**Certification:** Self-certification up to 7 days, then GP note required - -**Attestation:** *Should* provide occupational sick pay (UCU recommends full pay for 26 weeks). - ---- - -## 10. Probation and Permanency - -**Contract Type:** Permanent (not fixed-term) - -**Probation:** 18 months probationary period with reviews at 6, 12, and 18 months - -**Support:** Mentoring and development support during probation - -**Attestation:** *Must* provide permanent contract for permanent work per UCU §3 (Anti-Casualization). - ---- - -## 11. PhD Supervision - -**Supervision Load:** Maximum 6 PhD students as primary supervisor - -**Supervision Time:** Allocated as 0.1 FTE per student (156 hours/year each) - -**Training:** PhD supervision training provided - -**Attestation:** *Should* cap PhD supervision at 6 students per UCU §13. - ---- - -## 12. Sabbatical Leave - -**Research Sabbatical:** 1 semester (6 months) per 6 years of service - -**Pay:** Full salary during sabbatical - -**Purpose:** Focused research time (e.g., book completion, grant writing) - -**Attestation:** *Should* provide sabbatical per UCU §11. - ---- - -## 13. Professional Development - -**Conference Funding:** £1,500/year for conference attendance - -**Training:** Access to staff development courses - -**Career Progression:** Support for promotion applications - -**Attestation:** *Should* provide conference funding per UCU §12. - ---- - -## 14. Equality and Diversity - -**Equal Treatment:** No discrimination based on protected characteristics - -**Pay Gap Reporting:** University publishes annual gender/ethnicity pay gap reports - -**Reasonable Adjustments:** Support for disabled staff - -**Attestation:** *Must* comply with UK Equality Act 2010. - ---- - -## 15. Grievance and Disciplinary - -**Procedures:** As per ACAS Code of Practice 1 - -**Union Representation:** Right to UCU representation at disciplinary/grievance meetings - -**Appeals:** Right of appeal against disciplinary decisions - -**Attestation:** *Must* follow ACAS Code of Practice 1 per UK Employment Rights Act 1996. - ---- - -## 16. Termination - -**Notice Period:** 3 months notice by either party - -**Redundancy:** Full redundancy consultation if position at risk - -**Unfair Dismissal:** Protection from unfair dismissal (after 2 years service) - -**Attestation:** *Must* provide notice per UK Employment Rights Act 1996. - ---- - -## 17. Union Recognition - -**UCU Recognition:** University recognizes UCU for collective bargaining - -**Time Off for Union Duties:** UCU reps have time off for union work - -**Check-Off:** Union dues deducted via payroll - -**Attestation:** *Should* recognize UCU per UK Trade Union and Labour Relations Act 1992. - ---- - -## Signatures - -**Employee:** Dr. Sarah Johnson (signed 2025-08-01) - -**Employer:** University of Example, represented by HR Director Prof. David Williams (signed 2025-08-01) - ---- - -@refs: -[1] UK Employment Rights Act 1996 -[2] UK Working Time Regulations 1998 -[3] Education Act 2011 Section 26 (Academic Freedom) -[4] UCU Model Contract -[5] UCU Workload Allocation Model (WAM) -[6] USS (Universities Superannuation Scheme) Regulations -[7] UK Equality Act 2010 -[8] ACAS Code of Practice 1 -[9] UK Trade Union and Labour Relations Act 1992 -@end diff --git a/union-policy-parser/examples/freelance-journalist-bad.a2ml b/union-policy-parser/examples/freelance-journalist-bad.a2ml deleted file mode 100644 index 3e82754..0000000 --- a/union-policy-parser/examples/freelance-journalist-bad.a2ml +++ /dev/null @@ -1,204 +0,0 @@ -# Freelance Journalism Contract - Bad Example (Violations) - -@abstract: -This is an example of a NON-COMPLIANT freelance journalism contract with multiple violations -of NUJ and IWW standards. It includes exploitative clauses: all-rights grab, no kill fee, -NET 90 payment terms, no source protection, and spec work. This contract would FAIL validation -and trigger grievance generation. -@end - -@requires: -- None (contract violates union standards) -@end - ---- - -## Contract Parties - -**Freelancer:** John Smith, Freelance Journalist -**Client:** Exploitative Media Corp Ltd -**Date:** 2025-02-01 -**Duration:** Per-assignment basis - ---- - -## 1. Scope of Work - -**Assignment:** Write article on topic to be determined - -**Deliverables:** -- 2,000-word article -- Unlimited revisions until client satisfied -- Must provide 3 sample drafts before commission confirmed - -⚠️ **VIOLATION:** Spec work (3 sample drafts before commission) - violates IWW §6 (No Exploitation) - ---- - -## 2. Payment Terms - -**Rate:** £100 for 2,000-word feature (£0.05/word - well below NUJ minimum) - -**Payment Schedule:** -- Payment due NET 90 days from publication date (not submission date) -- Payment only if article is published -- No late payment penalties - -⚠️ **VIOLATION 1:** Rate below IWW/NUJ minimum (should be £0.40/word minimum) -⚠️ **VIOLATION 2:** NET 90 payment terms (should be NET 30 max per IWW §1) -⚠️ **VIOLATION 3:** "Payment on publication" (should be NET 30 from submission per IWW §1) -⚠️ **VIOLATION 4:** No late payment penalties (required per IWW §1) - ---- - -## 3. Kill Fee - -**Kill Fee:** None. If article is not published, no payment is owed. - -⚠️ **VIOLATION:** No kill fee (should be ≥50% per IWW §2) - ---- - -## 4. Copyright and Licensing - -**Copyright Transfer:** Freelancer assigns ALL RIGHTS to client, including: -- Copyright ownership (work-for-hire) -- Moral rights waiver -- Perpetual, worldwide, exclusive license -- Right to alter, edit, or republish without consultation -- Right to use freelancer's name/byline for other content - -⚠️ **VIOLATION:** All-rights grab / work-for-hire without premium payment (violates IWW §3 and Copyright Act) - ---- - -## 5. Source Protection - -**Source Disclosure:** Freelancer must disclose all confidential sources to client upon request. -Client reserves right to require source contact information for verification. - -⚠️ **VIOLATION:** No source protection guarantee (violates NUJ Code §6) - ---- - -## 6. Expenses Reimbursement - -**Expenses:** Freelancer responsible for all expenses. No reimbursement provided. - -⚠️ **VIOLATION:** No expense reimbursement (violates IWW §4) - ---- - -## 7. Revisions and Scope Creep - -**Revisions:** Unlimited revisions required at no additional cost - -**Scope Changes:** Client may request any changes to assignment scope without additional payment - -⚠️ **VIOLATION:** Unlimited revisions and scope changes without compensation (exploitative per IWW §6) - ---- - -## 8. Termination - -**Termination:** Client may terminate at any time without notice or payment. -Freelancer must provide 30 days notice to terminate. - -⚠️ **VIOLATION:** Asymmetric termination (no notice/payment for freelancer, but requires 30 days from freelancer) - ---- - -## 9. Editorial Control - -**Editorial Decisions:** Client has sole control over all editorial decisions, including: -- Sources used -- Angle of story -- Tone and framing -- Fact-checking standards - -Freelancer must accept all editorial direction without objection. - -⚠️ **VIOLATION:** No editorial independence (violates NUJ Code §2) - ---- - -## 10. Non-Compete and Exclusivity - -**Non-Compete:** Freelancer may not write on similar topics for competing publications for 12 months. - -**Exclusivity:** Freelancer must be available exclusively for client work during assignment period. - -⚠️ **VIOLATION:** Non-compete and exclusivity clauses without retainer payment (exploitative per IWW §6) - ---- - -## 11. Indemnification - -**Indemnity:** Freelancer indemnifies client against all legal claims arising from article, -including defamation, privacy violations, and copyright infringement. -Freelancer must pay client's legal fees if sued. - -⚠️ **VIOLATION:** One-sided indemnity clause shifts all risk to freelancer (exploitative) - ---- - -## 12. Union Activity - -**No Union Representation:** This is an individual contract. Freelancer waives right to union representation or collective bargaining. - -⚠️ **VIOLATION:** Waiving union rights is illegal per UK Trade Union and Labour Relations Act 1992 - ---- - -## 13. Confidentiality - -**Confidentiality:** Freelancer must not disclose contract terms to anyone, including union reps, -under penalty of £10,000. - -⚠️ **VIOLATION:** Gagging clause prevents disclosure of exploitative terms (illegal) - ---- - -## 14. Governing Law - -**Disputes:** All disputes must be resolved through private arbitration (no employment tribunal). -Freelancer waives right to court proceedings. - -⚠️ **VIOLATION:** Waiving right to employment tribunal (may be unenforceable) - ---- - -## Signatures - -**Freelancer:** John Smith (signed 2025-02-01) - -**Client:** Exploitative Media Corp Ltd (signed 2025-02-01) - ---- - -## VALIDATION SUMMARY - -This contract contains **14 MAJOR VIOLATIONS** of NUJ/IWW standards: - -1. Spec work requirement -2. Rate below minimum (£0.05 vs £0.40/word) -3. NET 90 payment terms (should be NET 30) -4. Payment on publication (should be NET 30 from submission) -5. No late payment penalties -6. No kill fee -7. All-rights grab / work-for-hire -8. No source protection -9. No expense reimbursement -10. Unlimited revisions -11. No editorial independence -12. Exploitative non-compete/exclusivity -13. One-sided indemnity -14. Union rights waiver - -**RECOMMENDATION:** DO NOT SIGN THIS CONTRACT. Contact NUJ/IWW rep for assistance. - ---- - -@refs: -None - this contract violates all relevant union standards -@end diff --git a/union-policy-parser/examples/freelance-journalist-good.a2ml b/union-policy-parser/examples/freelance-journalist-good.a2ml deleted file mode 100644 index 73f550a..0000000 --- a/union-policy-parser/examples/freelance-journalist-good.a2ml +++ /dev/null @@ -1,203 +0,0 @@ -# Freelance Journalism Contract - Good Example - -@abstract: -This is an example of a COMPLIANT freelance journalism contract that meets NUJ and IWW standards. -It includes all required protections: fair payment terms, copyright retention, source protection, -editorial independence, and kill fees. This contract would pass validation against both -nuj-code-of-ethics.a2ml and iww-freelancer-rights.a2ml schemas. -@end - -@requires: -- NUJ Code of Conduct (2024) -- IWW Freelancer Rights -- UK Copyright, Designs and Patents Act 1988 -- UK Late Payment of Commercial Debts Act 1998 -@end - ---- - -## Contract Parties - -**Freelancer:** Jane Doe, Freelance Journalist -**Client:** The Guardian News & Media Ltd -**Date:** 2025-02-01 -**Duration:** Per-assignment basis (ongoing relationship) - ---- - -## 1. Scope of Work - -**Assignment:** Investigative reporting on climate policy - -**Deliverables:** -- 2,000-word feature article -- Up to 3 revisions included -- Original photography (if agreed separately) - -**Editorial Independence:** Freelancer retains full editorial control over investigative methods, -sources, and findings, subject only to factual accuracy and legal review. - -**Attestation:** *Must* guarantee editorial independence per NUJ Code §2. - ---- - -## 2. Payment Terms - -**Rate:** £500 for commissioned 2,000-word feature (£0.25/word) - -**Payment Schedule:** -- Invoice submitted upon final draft acceptance -- Payment due NET 30 days from invoice date -- Bank transfer to freelancer's account - -**Late Payment Penalty:** 5% interest per month on overdue invoices, per UK Late Payment of Commercial Debts Act 1998 - -**Attestation:** *Must* pay within NET 30 days with late payment penalties per IWW Freelancer Rights §1. - ---- - -## 3. Kill Fee - -**Kill Fee:** 75% of agreed fee (£375) if article is commissioned but not published - -**Triggers:** -- Editorial decision not to publish (not freelancer's fault) -- Change in editorial direction -- Space constraints - -**Payment:** Kill fee paid within NET 30 days of kill decision - -**Attestation:** *Must* provide kill fee ≥50% per IWW Freelancer Rights §2. - ---- - -## 4. Copyright and Licensing - -**Copyright Ownership:** Freelancer retains copyright - -**License Granted:** First publication rights only (print and online) for The Guardian - -**Duration:** Exclusive for 3 months from publication, then non-exclusive - -**Additional Rights:** Any republication, syndication, or anthology use requires separate fee negotiation - -**Attestation:** *Must* retain freelancer copyright or grant only limited license per IWW Freelancer Rights §3. - ---- - -## 5. Source Protection - -**Source Protection Guarantee:** Client agrees to protect confidential sources as per NUJ Code of Conduct §6 - -**No Forced Disclosure:** Client will not require freelancer to disclose confidential source identities - -**Legal Support:** Client will provide legal support if freelancer is subjected to legal pressure to reveal sources - -**Attestation:** *Must* guarantee source protection per NUJ Code §6. - ---- - -## 6. Expenses Reimbursement - -**Reimbursable Expenses:** -- Travel (mileage at £0.45/mile or public transport receipts) -- Accommodation (if overnight research required) -- Research materials (books, reports, FOI fees) - -**Pre-Approval:** Expenses over £100 require pre-approval from commissioning editor - -**Reimbursement Timeline:** Within NET 30 days of expense claim submission - -**Attestation:** *Must* reimburse expenses within 30 days per IWW Freelancer Rights §4. - ---- - -## 7. Revisions and Scope Creep - -**Included Revisions:** Up to 3 rounds of revisions included in fee - -**Scope Changes:** Significant changes to assignment scope (e.g., doubling word count, new angle) require renegotiation of fee - -**Attestation:** *Must* define revision limits and prevent scope creep exploitation. - ---- - -## 8. Termination - -**Notice Period:** Either party may terminate with 14 days written notice - -**Payment on Termination:** Freelancer receives pro-rata payment for work completed up to termination - -**Attestation:** *Must* provide ≥14 days notice per IWW Freelancer Rights §5. - ---- - -## 9. Moral Rights - -**Attribution:** Freelancer's byline will appear on published article - -**Right to Object:** Freelancer has right to object to derogatory treatment of work - -**Integrity:** Client will not materially alter work without freelancer consultation - -**Attestation:** *Must* respect moral rights per UK Copyright, Designs and Patents Act 1988. - ---- - -## 10. Union Representation - -**Union Recognition:** Client recognizes freelancer's right to NUJ representation in disputes - -**No Retaliation:** Client will not retaliate against freelancer for union membership or activity - -**Attestation:** *Must* recognize union rights per UK Trade Union and Labour Relations Act 1992. - ---- - -## 11. Anti-Discrimination - -**Equal Treatment:** Freelancer will receive equal treatment regardless of race, gender, disability, age, or other protected characteristics - -**Harassment Policy:** Client maintains zero-tolerance harassment policy - -**Attestation:** *Must* comply with UK Equality Act 2010. - ---- - -## 12. Truth and Accuracy - -**Fact-Checking:** Client will provide fact-checking support - -**Corrections:** Client will promptly correct any errors in published work - -**Attestation:** *Must* support accuracy per NUJ Code §1. - ---- - -## 13. Privacy and GDPR - -**Data Protection:** Client will handle freelancer's personal data per GDPR - -**Source Data:** Freelancer will handle interviewee data per GDPR and NUJ guidelines - -**Attestation:** *Must* comply with GDPR (EU 2016/679). - ---- - -## Signatures - -**Freelancer:** Jane Doe (signed electronically 2025-02-01) - -**Client:** The Guardian News & Media Ltd, represented by Commissioning Editor Sarah Smith (signed electronically 2025-02-01) - ---- - -@refs: -[1] NUJ Code of Conduct (2024) - https://www.nuj.org.uk/about/nuj-code/ -[2] IWW Freelancer Rights -[3] UK Copyright, Designs and Patents Act 1988 -[4] UK Late Payment of Commercial Debts Act 1998 -[5] UK Equality Act 2010 -[6] GDPR (EU 2016/679) -[7] UK Trade Union and Labour Relations Act 1992 -@end diff --git a/union-policy-parser/examples/ou-day-employee-contract.a2ml b/union-policy-parser/examples/ou-day-employee-contract.a2ml deleted file mode 100644 index 17555f7..0000000 --- a/union-policy-parser/examples/ou-day-employee-contract.a2ml +++ /dev/null @@ -1,366 +0,0 @@ -# Open University Day Employees Contract of Employment (HRP052) - -@abstract: -This document encodes the Open University's "Day Employees: Contract of Employment" -(HRP052, November 2009) as Attested Markup Language (A2ML) for machine-readable -validation and enforcement by unions (e.g., NUJ, IWW, UCU). The contract comprises -17 sections covering employment terms, taxation, workplace, termination, grievance -procedures, and worker protections under UK law. -@end - -@requires: -- UK Employment Rights Act 1996 -- ACAS Code of Practice 1: Disciplinary and Grievance Procedures -- UK National Minimum Wage Act 1998 -- UK Working Time Regulations 1998 -- UK Defamation Act 2013 -- GDPR (EU 2016/679) -- Copyright, Designs and Patents Act 1988 -- Trade Union and Labour Relations (Consolidation) Act 1992 -@end - ---- - -## 1. Employment Terms - -**Contract Definition:** Day Employees contracted for number of days specified in Offer Letter. - -**Working Day:** 1 day = 7.5 hours or equivalent in evening work. - -**Consultation Basis:** Some contracts based on availability for consultation, may involve: -- Defined days of attendance, *or* -- Work completed on/by defined date, *or* -- Work completed over defined period - -**Duties:** Outlined in Offer Letter, may be amplified by additional sheet. - -**Required Fields:** -- Days contracted (must match Offer Letter) -- Duties specification (must be detailed) -- Consultation requirements (if applicable) - -**Attestation:** Must verify days-contracted matches Offer Letter and duties are fully specified. - ---- - -## 2. Taxation and National Insurance - -**Employee Status:** Day Employees are University employees for NI and Income Tax purposes. - -**Tax Deduction:** Deductions made from fees payable per HMRC instructions (Oxon & Bucks Area). - -**Tax Code Process:** -1. New employees sent Income Tax Enquiry Form -2. Employee completes and returns to Oxon & Bucks Area Tax Office -3. Tax Office notifies University of correct Tax Code -4. *Fallback:* If no notification, tax deducted at basic rate - -**National Insurance:** -- Default: Full rate deduction -- Exception: Reduced rate if certificate from Contributions Agency submitted - -**Required Fields:** -- Tax deduction method (must comply with HMRC) -- NI contribution rate (full/reduced/exempt) -- Tax code (must be obtained from HMRC) - -**Attestation:** Must comply with HMRC regulations and provide tax code or accept basic rate deduction. - ---- - -## 3. Place of Work - -**Primary Location:** Appointing Regional Centre (default). - -**Remote Work:** Day Employee may work from home if: -- Regional Director agreement obtained **in writing** -- Agreement specifies days or periods - -**Required Fields:** -- Primary workplace location -- Remote work approval status (yes/no) -- Written approval from Regional Director (if remote) - -**Attestation:** Remote work requires documented Regional Director approval. - ---- - -## 4. Insurance - -**University Coverage:** -- Covers legal liabilities to/for Day Employees -- Limited to incidents **within** Open University duties -- Does **not** cover personal property loss/damage - -**Private Vehicle Use:** -- Required: Insurance for business/trade/profession use -- Employee must verify coverage before University travel -- University may request insurance evidence before expense payment - -**Required Fields:** -- University liability coverage (yes - automatic) -- Personal property coverage (no - employee responsibility) -- Vehicle insurance for business use (if applicable) - -**Attestation:** Employee must maintain valid business-use vehicle insurance if using private vehicle for duties. - ---- - -## 5. Sickness or Absence - -**No Pay for Unperformed Duties:** University cannot pay for duties not performed due to: -- Sickness -- Absence (including public holidays, normal holiday) - -**Statutory Sick Pay:** May be entitled if unable to perform duties due to sickness. - -**Termination Right:** University may terminate with statutory notice if employee cannot complete duties within required timescale. - -**Required Fields:** -- Sick pay entitlement (statutory only) -- Termination conditions (if duties incomplete) - -**Attestation:** Employee acknowledges no remuneration for unperformed duties; only statutory sick pay available. - ---- - -## 6. Pension Facilities - -**Occupational Pension:** Not provided for Day Employees. - -**Contracting-Out Certificate:** None in place. - -**Stakeholder Pension:** -- Provider: Prudential -- Employer contribution: **None** -- Access: Available but no University contribution - -**Contact:** Pensions Manager, Human Resources (for stakeholder pension details). - -**Required Fields:** -- Occupational pension access (no) -- Stakeholder pension provider (Prudential) -- Employer contribution (no) - -**Attestation:** Employee acknowledges no occupational pension or employer contribution. - ---- - -## 7. Termination - -**Notice Period:** 1 month written notice by either party (except gross misconduct). - -**Gross Misconduct:** University may terminate summarily without notice. - -**Continuous Service ≥ 1 Year:** If termination ends University employment: -- Employee invited to dismissal meeting -- Right of accompaniment offered -- Right of appeal after meeting - -**Payment on Termination:** Only for actual duties completed up to termination date. - -**Relocation Termination:** University may terminate if employee moves beyond reasonable distance and cannot fulfill obligations (Regional Director investigation required). - -**Contract Duration:** Strictly for contract period; future contracts require reapplication. - -**Required Fields:** -- Notice period (1 month default) -- Gross misconduct provisions -- Continuous service duration (impacts dismissal process) -- Payment on termination (pro-rata only) -- Relocation impact assessment - -**Attestation:** Must comply with UK Employment Rights Act 1996 notice requirements and dismissal procedures. - ---- - -## 8. Grievance and Disciplinary Procedures - -**Grievance Process:** -1. **Informal:** Speak with Regional Director first -2. **Formal:** If unresolved, follow ACAS Code of Practice 1 - - Reference: [http://www.acas.org.uk](http://www.acas.org.uk) - -**Disciplinary Process:** University follows ACAS Code of Practice 1 for disciplinary matters. - -**Required Fields:** -- Informal resolution attempt (required first step) -- Formal procedure reference (ACAS Code of Practice 1) -- Right of accompaniment (yes) - -**Attestation:** Must follow ACAS Code of Practice 1: Disciplinary and Grievance Procedures. - ---- - -## 9. Confidentiality - -**General:** Shall not disclose confidential business or future plans to third parties. - -**Confidential Information Includes:** -- Assignments -- Examination papers and marks -- Personal information (applicants, students, graduates, employees) - -**Prohibition:** During **and after** employment: -- No divulge/communicate to third parties -- No use for personal purpose -- No use for non-University purposes -- No unauthorized disclosure - -**Student Records:** Must not release names, addresses, or other student information to third parties without: -- Exception: OU employees directly concerned with student progress/administration -- Requirement: Written student permission otherwise - -**Required Fields:** -- Confidential information types (assignments, exams, personal data) -- Disclosure prohibition (during and after employment) -- Student data protection (written permission required) - -**Attestation:** Must comply with GDPR and University confidentiality policy for student records. - ---- - -## 10. Copyright - -**University Ownership:** Copyright vests in Open University for all materials produced during contract: -- Written materials -- Recorded materials -- Computer programmes -- Databases -- Other works - -**Employee Retention:** Day Employee retains copyright in existing materials developed **before** contract commencement. - -**Required Fields:** -- Copyright ownership (University for contract work) -- Pre-existing materials (employee retains) - -**Attestation:** Must comply with Copyright, Designs and Patents Act 1988; University owns work product. - ---- - -## 11. Remuneration - -**Basis:** Daily rate specified in Offer Letter. - -**Day Definition:** 1 day = 7.5 hours or equivalent in evening work. - -**Source:** Notified by Regional Centre. - -**Required Fields:** -- Daily rate (must match Offer Letter) -- Day definition (7.5 hours standard) - -**Attestation:** Daily rate must match Offer Letter specification. - ---- - -## 12. Method of Payment - -**Payment Method:** Bank credit to employee account after claim submitted. - -**Staff Engagement Form Required:** Must provide: -1. Name and address -2. Bank details (Sort Code, account number) -3. National Insurance number -4. NI contribution rate -5. Exemption/reduced rate certificate (if applicable) - -**Payment Timing:** Monthly for days actually worked in preceding month. - -**No Form = No Payment:** University cannot pay until Staff Engagement Form received. - -**Required Fields:** -- Bank account details -- National Insurance number -- NI contribution rate/certificate -- Claim submission requirement - -**Attestation:** Payment contingent on completed Staff Engagement Form and submitted claims. - ---- - -## 13. Expenses - -**Reimbursement:** University reimburses expenses if: -- Necessarily incurred in duty discharge -- **Previously authorized** by Regional Director - -**Rates:** Per "Travel and Subsistence Policy" (current rates). - -**Documentation:** Available on Human Resources intranet site. - -**Required Fields:** -- Pre-authorization requirement (yes - Regional Director) -- Reimbursement policy reference (Travel and Subsistence Policy) -- Rates reference (current policy rates) - -**Attestation:** Expenses require Regional Director pre-authorization; reimbursement per policy rates. - ---- - -## 14. Travel - -**Home to Regional Centre:** Not reimbursed if Regional Centre is normal place of work. - -**Travel Time:** Not paid for travel to Regional Centre. - -**Work From Home Exception:** If Regional Director approves in writing: -- May authorize travel expenses (home ↔ University) -- Calculated per Travel and Subsistence Policy - -**Required Fields:** -- Home-to-work travel (not reimbursed if Regional Centre) -- Remote work travel (requires written Regional Director approval) - -**Attestation:** Travel reimbursement requires written Regional Director approval for remote work arrangements. - ---- - -## 15. Employment with Other Organisations - -**Responsibility:** Day Employee responsible for ensuring OU contract does not conflict with other employment terms/conditions. - -**Required Fields:** -- Other employment disclosure (employee responsibility) -- Conflict avoidance (employee ensures no conflict) - -**Attestation:** Employee acknowledges responsibility to avoid conflicts with other employment. - ---- - -## 16. Collective Agreement - -**Status:** **No collective agreements** applicable to this contract. - -**Required Fields:** -- Collective agreement applicability (no) - -**Attestation:** Contract operates outside collective bargaining; no union recognition for Day Employees under this contract type. - ---- - -## 17. Useful References - -**Internal Documents:** -- Day Contract Rates -- Travel and Subsistence Policy -- Travel and Subsistence Rates - -**External Resources:** -- ACAS website: [http://www.acas.org.uk](http://www.acas.org.uk) - ---- - -@refs: -[1] UK Employment Rights Act 1996 -[2] ACAS Code of Practice 1: Disciplinary and Grievance Procedures (http://www.acas.org.uk) -[3] UK National Minimum Wage Act 1998 -[4] UK Working Time Regulations 1998 -[5] GDPR (EU 2016/679) -[6] Copyright, Designs and Patents Act 1988 -[7] Trade Union and Labour Relations (Consolidation) Act 1992 -[8] Open University Travel and Subsistence Policy (internal) -[9] Open University Day Contract Rates (internal) -[10] HM Revenue & Customs Regulations (Oxon & Bucks Area) -@end diff --git a/union-policy-parser/examples/web-project-deno.json b/union-policy-parser/examples/web-project-deno.json deleted file mode 100644 index 5ddd3bd..0000000 --- a/union-policy-parser/examples/web-project-deno.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "// NOTE": "Example deno.json for ReScript web projects", - "tasks": { - "build": "deno run -A npm:rescript", - "clean": "deno run -A npm:rescript clean", - "watch": "deno run -A npm:rescript -w", - "serve": "deno run -A jsr:@std/http/file-server .", - "test": "deno test --allow-all" - }, - "imports": { - "rescript": "^12.0.0", - "@rescript/core": "npm:@rescript/core@^1.6.0", - "safe-dom/": "https://raw.githubusercontent.com/hyperpolymath/rescript-dom-mounter/main/src/", - "proven/": "../proven/bindings/rescript/src/" - }, - "compilerOptions": { - "allowJs": true, - "checkJs": false - } -} diff --git a/union-policy-parser/ffi/zig/build.zig b/union-policy-parser/ffi/zig/build.zig deleted file mode 100644 index c02617f..0000000 --- a/union-policy-parser/ffi/zig/build.zig +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -// {{PROJECT}} FFI Build Configuration - -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - // Shared library (.so, .dylib, .dll) - const lib = b.addSharedLibrary(.{ - .name = "{{project}}", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - // Set version - lib.version = .{ .major = 0, .minor = 1, .patch = 0 }; - - // Static library (.a) - const lib_static = b.addStaticLibrary(.{ - .name = "{{project}}", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - // Install artifacts - b.installArtifact(lib); - b.installArtifact(lib_static); - - // Generate header file for C compatibility - const header = b.addInstallHeader( - b.path("include/{{project}}.h"), - "{{project}}.h", - ); - b.getInstallStep().dependOn(&header.step); - - // Unit tests - const lib_tests = b.addTest(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - const run_lib_tests = b.addRunArtifact(lib_tests); - - const test_step = b.step("test", "Run library tests"); - test_step.dependOn(&run_lib_tests.step); - - // Integration tests - const integration_tests = b.addTest(.{ - .root_source_file = b.path("test/integration_test.zig"), - .target = target, - .optimize = optimize, - }); - - integration_tests.linkLibrary(lib); - - const run_integration_tests = b.addRunArtifact(integration_tests); - - const integration_test_step = b.step("test-integration", "Run integration tests"); - integration_test_step.dependOn(&run_integration_tests.step); - - // Documentation - const docs = b.addTest(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = .Debug, - }); - - const docs_step = b.step("docs", "Generate documentation"); - docs_step.dependOn(&b.addInstallDirectory(.{ - .source_dir = docs.getEmittedDocs(), - .install_dir = .prefix, - .install_subdir = "docs", - }).step); - - // Benchmark (if needed) - const bench = b.addExecutable(.{ - .name = "{{project}}-bench", - .root_source_file = b.path("bench/bench.zig"), - .target = target, - .optimize = .ReleaseFast, - }); - - bench.linkLibrary(lib); - - const run_bench = b.addRunArtifact(bench); - - const bench_step = b.step("bench", "Run benchmarks"); - bench_step.dependOn(&run_bench.step); -} diff --git a/union-policy-parser/ffi/zig/src/main.zig b/union-policy-parser/ffi/zig/src/main.zig deleted file mode 100644 index f1b2633..0000000 --- a/union-policy-parser/ffi/zig/src/main.zig +++ /dev/null @@ -1,275 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -// {{PROJECT}} FFI Implementation -// -// This module implements the C-compatible FFI declared in src/abi/Foreign.idr -// All types and layouts must match the Idris2 ABI definitions. -// - -const std = @import("std"); - -// Version information (keep in sync with project) -const VERSION = "0.1.0"; -const BUILD_INFO = "{{PROJECT}} built with Zig " ++ @import("builtin").zig_version_string; - -/// Thread-local error storage -threadlocal var last_error: ?[]const u8 = null; - -/// Set the last error message -fn setError(msg: []const u8) void { - last_error = msg; -} - -/// Clear the last error -fn clearError() void { - last_error = null; -} - -//============================================================================== -// Core Types (must match src/abi/Types.idr) -//============================================================================== - -/// Result codes (must match Idris2 Result type) -pub const Result = enum(c_int) { - ok = 0, - @"error" = 1, - invalid_param = 2, - out_of_memory = 3, - null_pointer = 4, -}; - -/// Library handle (opaque to prevent direct access) -pub const Handle = opaque { - // Internal state hidden from C - allocator: std.mem.Allocator, - initialized: bool, - // Add your fields here -}; - -//============================================================================== -// Library Lifecycle -//============================================================================== - -/// Initialize the library -/// Returns a handle, or null on failure -export fn {{project}}_init() ?*Handle { - const allocator = std.heap.c_allocator; - - const handle = allocator.create(Handle) catch { - setError("Failed to allocate handle"); - return null; - }; - - // Initialize handle - handle.* = .{ - .allocator = allocator, - .initialized = true, - }; - - clearError(); - return handle; -} - -/// Free the library handle -export fn {{project}}_free(handle: ?*Handle) void { - const h = handle orelse return; - const allocator = h.allocator; - - // Clean up resources - h.initialized = false; - - allocator.destroy(h); - clearError(); -} - -//============================================================================== -// Core Operations -//============================================================================== - -/// Process data (example operation) -export fn {{project}}_process(handle: ?*Handle, input: u32) Result { - const h = handle orelse { - setError("Null handle"); - return .null_pointer; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return .@"error"; - } - - // Example processing logic - _ = input; - - clearError(); - return .ok; -} - -//============================================================================== -// String Operations -//============================================================================== - -/// Get a string result (example) -/// Caller must free the returned string -export fn {{project}}_get_string(handle: ?*Handle) ?[*:0]const u8 { - const h = handle orelse { - setError("Null handle"); - return null; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return null; - } - - // Example: allocate and return a string - const result = h.allocator.dupeZ(u8, "Example result") catch { - setError("Failed to allocate string"); - return null; - }; - - clearError(); - return result.ptr; -} - -/// Free a string allocated by the library -export fn {{project}}_free_string(str: ?[*:0]const u8) void { - const s = str orelse return; - const allocator = std.heap.c_allocator; - - const slice = std.mem.span(s); - allocator.free(slice); -} - -//============================================================================== -// Array/Buffer Operations -//============================================================================== - -/// Process an array of data -export fn {{project}}_process_array( - handle: ?*Handle, - buffer: ?[*]const u8, - len: u32, -) Result { - const h = handle orelse { - setError("Null handle"); - return .null_pointer; - }; - - const buf = buffer orelse { - setError("Null buffer"); - return .null_pointer; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return .@"error"; - } - - // Access the buffer - const data = buf[0..len]; - _ = data; - - // Process data here - - clearError(); - return .ok; -} - -//============================================================================== -// Error Handling -//============================================================================== - -/// Get the last error message -/// Returns null if no error -export fn {{project}}_last_error() ?[*:0]const u8 { - const err = last_error orelse return null; - - // Return C string (static storage, no need to free) - const allocator = std.heap.c_allocator; - const c_str = allocator.dupeZ(u8, err) catch return null; - return c_str.ptr; -} - -//============================================================================== -// Version Information -//============================================================================== - -/// Get the library version -export fn {{project}}_version() [*:0]const u8 { - return VERSION.ptr; -} - -/// Get build information -export fn {{project}}_build_info() [*:0]const u8 { - return BUILD_INFO.ptr; -} - -//============================================================================== -// Callback Support -//============================================================================== - -/// Callback function type (C ABI) -pub const Callback = *const fn (u64, u32) callconv(.C) u32; - -/// Register a callback -export fn {{project}}_register_callback( - handle: ?*Handle, - callback: ?Callback, -) Result { - const h = handle orelse { - setError("Null handle"); - return .null_pointer; - }; - - const cb = callback orelse { - setError("Null callback"); - return .null_pointer; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return .@"error"; - } - - // Store callback for later use - _ = cb; - - clearError(); - return .ok; -} - -//============================================================================== -// Utility Functions -//============================================================================== - -/// Check if handle is initialized -export fn {{project}}_is_initialized(handle: ?*Handle) u32 { - const h = handle orelse return 0; - return if (h.initialized) 1 else 0; -} - -//============================================================================== -// Tests -//============================================================================== - -test "lifecycle" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); - - try std.testing.expect({{project}}_is_initialized(handle) == 1); -} - -test "error handling" { - const result = {{project}}_process(null, 0); - try std.testing.expectEqual(Result.null_pointer, result); - - const err = {{project}}_last_error(); - try std.testing.expect(err != null); -} - -test "version" { - const ver = {{project}}_version(); - const ver_str = std.mem.span(ver); - try std.testing.expectEqualStrings(VERSION, ver_str); -} diff --git a/union-policy-parser/ffi/zig/test/integration_test.zig b/union-policy-parser/ffi/zig/test/integration_test.zig deleted file mode 100644 index e481508..0000000 --- a/union-policy-parser/ffi/zig/test/integration_test.zig +++ /dev/null @@ -1,183 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -// {{PROJECT}} Integration Tests -// -// These tests verify that the Zig FFI correctly implements the Idris2 ABI - -const std = @import("std"); -const testing = std.testing; - -// Import FFI functions -extern fn {{project}}_init() ?*opaque {}; -extern fn {{project}}_free(?*opaque {}) void; -extern fn {{project}}_process(?*opaque {}, u32) c_int; -extern fn {{project}}_get_string(?*opaque {}) ?[*:0]const u8; -extern fn {{project}}_free_string(?[*:0]const u8) void; -extern fn {{project}}_last_error() ?[*:0]const u8; -extern fn {{project}}_version() [*:0]const u8; -extern fn {{project}}_is_initialized(?*opaque {}) u32; - -//============================================================================== -// Lifecycle Tests -//============================================================================== - -test "create and destroy handle" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); - - try testing.expect(handle != null); -} - -test "handle is initialized" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); - - const initialized = {{project}}_is_initialized(handle); - try testing.expectEqual(@as(u32, 1), initialized); -} - -test "null handle is not initialized" { - const initialized = {{project}}_is_initialized(null); - try testing.expectEqual(@as(u32, 0), initialized); -} - -//============================================================================== -// Operation Tests -//============================================================================== - -test "process with valid handle" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); - - const result = {{project}}_process(handle, 42); - try testing.expectEqual(@as(c_int, 0), result); // 0 = ok -} - -test "process with null handle returns error" { - const result = {{project}}_process(null, 42); - try testing.expectEqual(@as(c_int, 4), result); // 4 = null_pointer -} - -//============================================================================== -// String Tests -//============================================================================== - -test "get string result" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); - - const str = {{project}}_get_string(handle); - defer if (str) |s| {{project}}_free_string(s); - - try testing.expect(str != null); -} - -test "get string with null handle" { - const str = {{project}}_get_string(null); - try testing.expect(str == null); -} - -//============================================================================== -// Error Handling Tests -//============================================================================== - -test "last error after null handle operation" { - _ = {{project}}_process(null, 0); - - const err = {{project}}_last_error(); - try testing.expect(err != null); - - if (err) |e| { - const err_str = std.mem.span(e); - try testing.expect(err_str.len > 0); - } -} - -test "no error after successful operation" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); - - _ = {{project}}_process(handle, 0); - - // Error should be cleared after successful operation - // (This depends on implementation) -} - -//============================================================================== -// Version Tests -//============================================================================== - -test "version string is not empty" { - const ver = {{project}}_version(); - const ver_str = std.mem.span(ver); - - try testing.expect(ver_str.len > 0); -} - -test "version string is semantic version format" { - const ver = {{project}}_version(); - const ver_str = std.mem.span(ver); - - // Should be in format X.Y.Z - try testing.expect(std.mem.count(u8, ver_str, ".") >= 1); -} - -//============================================================================== -// Memory Safety Tests -//============================================================================== - -test "multiple handles are independent" { - const h1 = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(h1); - - const h2 = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(h2); - - try testing.expect(h1 != h2); - - // Operations on h1 should not affect h2 - _ = {{project}}_process(h1, 1); - _ = {{project}}_process(h2, 2); -} - -test "double free is safe" { - const handle = {{project}}_init() orelse return error.InitFailed; - - {{project}}_free(handle); - {{project}}_free(handle); // Should not crash -} - -test "free null is safe" { - {{project}}_free(null); // Should not crash -} - -//============================================================================== -// Thread Safety Tests (if applicable) -//============================================================================== - -test "concurrent operations" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); - - const ThreadContext = struct { - h: *opaque {}, - id: u32, - }; - - const thread_fn = struct { - fn run(ctx: ThreadContext) void { - _ = {{project}}_process(ctx.h, ctx.id); - } - }.run; - - var threads: [4]std.Thread = undefined; - for (&threads, 0..) |*thread, i| { - thread.* = try std.Thread.spawn(.{}, thread_fn, .{ - ThreadContext{ .h = handle, .id = @intCast(i) }, - }); - } - - for (threads) |thread| { - thread.join(); - } -} diff --git a/union-policy-parser/hooks/validate-codeql.sh b/union-policy-parser/hooks/validate-codeql.sh deleted file mode 100755 index c435189..0000000 --- a/union-policy-parser/hooks/validate-codeql.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: PMPL-1.0-or-later -# Pre-commit hook: Validate CodeQL language matrix matches repo -set -euo pipefail - -CODEQL_FILE=".github/workflows/codeql.yml" -[ -f "$CODEQL_FILE" ] || exit 0 - -# Detect languages in repo -HAS_JS=$(find . -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" 2>/dev/null | grep -v node_modules | head -1) -HAS_PY=$(find . -name "*.py" 2>/dev/null | grep -v __pycache__ | head -1) -HAS_GO=$(find . -name "*.go" 2>/dev/null | head -1) -HAS_RS=$(find . -name "*.rs" 2>/dev/null | head -1) - -# Check if matrix includes unsupported languages -if grep -q "language:.*python" "$CODEQL_FILE" && [ -z "$HAS_PY" ]; then - echo "WARNING: CodeQL configured for Python but no .py files found" -fi -if grep -q "language:.*go" "$CODEQL_FILE" && [ -z "$HAS_GO" ]; then - echo "WARNING: CodeQL configured for Go but no .go files found" -fi -if grep -q "language:.*javascript" "$CODEQL_FILE" && [ -z "$HAS_JS" ]; then - echo "WARNING: CodeQL configured for JavaScript but no JS/TS files found" -fi - -# Rust/OCaml are not supported - should use 'actions' only -if [ -n "$HAS_RS" ]; then - if grep -q "language:.*rust" "$CODEQL_FILE"; then - echo "ERROR: CodeQL does not support Rust - use ['actions'] instead" - exit 1 - fi -fi - -exit 0 diff --git a/union-policy-parser/hooks/validate-permissions.sh b/union-policy-parser/hooks/validate-permissions.sh deleted file mode 100755 index 50fce4f..0000000 --- a/union-policy-parser/hooks/validate-permissions.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: PMPL-1.0-or-later -# Pre-commit hook: Validate workflow permissions declarations -set -euo pipefail -ERRORS=0 -for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$workflow" ] || continue - if ! grep -qE '^permissions:' "$workflow"; then - echo "ERROR: Missing top-level permissions in $workflow" - ERRORS=$((ERRORS + 1)) - fi -done -[ $ERRORS -gt 0 ] && exit 1 -exit 0 diff --git a/union-policy-parser/hooks/validate-sha-pins.sh b/union-policy-parser/hooks/validate-sha-pins.sh deleted file mode 100755 index c6d9913..0000000 --- a/union-policy-parser/hooks/validate-sha-pins.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: PMPL-1.0-or-later -# Pre-commit hook: Validate GitHub Actions are SHA-pinned - -set -euo pipefail - -ERRORS=0 - -for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$workflow" ] || continue - - # Find uses: lines that aren't SHA-pinned - while IFS= read -r line; do - if [[ "$line" =~ uses:.*@ ]]; then - # Check if it has a SHA (40 hex chars) - if ! echo "$line" | grep -qE '@[a-f0-9]{40}'; then - echo "ERROR: Unpinned action in $workflow" - echo " $line" - echo " Actions must use SHA pins: uses: action/name@SHA # version" - ERRORS=$((ERRORS + 1)) - fi - fi - done < "$workflow" -done - -if [ $ERRORS -gt 0 ]; then - echo "" - echo "Found $ERRORS unpinned actions. Please SHA-pin all GitHub Actions." - echo "Use: gh api repos/OWNER/REPO/git/matching-refs/tags/VERSION to find SHAs" - exit 1 -fi - -exit 0 diff --git a/union-policy-parser/hooks/validate-spdx.sh b/union-policy-parser/hooks/validate-spdx.sh deleted file mode 100755 index 8f50002..0000000 --- a/union-policy-parser/hooks/validate-spdx.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: PMPL-1.0-or-later -# Pre-commit hook: Validate SPDX headers in workflow files - -set -euo pipefail - -ERRORS=0 -SPDX_PATTERN="^# SPDX-License-Identifier:" - -for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$workflow" ] || continue - - first_line=$(head -n1 "$workflow") - if ! echo "$first_line" | grep -qE "$SPDX_PATTERN"; then - echo "ERROR: Missing SPDX header in $workflow" - echo " First line should be: # SPDX-License-Identifier: PMPL-1.0-or-later" - ERRORS=$((ERRORS + 1)) - fi -done - -if [ $ERRORS -gt 0 ]; then - exit 1 -fi - -exit 0 diff --git a/union-policy-parser/manifest.scm b/union-policy-parser/manifest.scm deleted file mode 100644 index dd942c7..0000000 --- a/union-policy-parser/manifest.scm +++ /dev/null @@ -1,24 +0,0 @@ -;;; SPDX-License-Identifier: MPL-2.0 -;;; manifest.scm — Generic Guix manifest for RSR-compliant projects -;;; -;;; Usage: -;;; guix shell -m manifest.scm -;;; - -(specifications->manifest - '(;; Core development tools - "git" - "just" - "nickel" - "curl" - "bash" - "coreutils" - - ;; Documentation - "asciidoctor" - "pandoc" - - ;; Common build dependencies - "openssl" - "zlib" - "pkg-config")) diff --git a/union-policy-parser/schemas/bectu-media-rights.a2ml b/union-policy-parser/schemas/bectu-media-rights.a2ml deleted file mode 100644 index f24736b..0000000 --- a/union-policy-parser/schemas/bectu-media-rights.a2ml +++ /dev/null @@ -1,373 +0,0 @@ -# BECTU Media and Entertainment Workers Rights - -@abstract: -This schema defines the Broadcasting, Entertainment, Communications and Theatre Union -(BECTU) standards for media and entertainment workers. It encodes protections for -freelancers, crew, technicians, and creative workers in film, TV, theatre, and live -events, with focus on fair pay, working hours, health & safety, and anti-bullying. -@end - -@requires: -- UK Employment Rights Act 1996 -- UK Working Time Regulations 1998 -- UK Health and Safety at Work Act 1974 -- BECTU Crew Agreement -- BECTU Theatre Agreement -- UK Equality Act 2010 -- Copyright, Designs and Patents Act 1988 -@end - ---- - -## Core Rights (MUST Clauses) - -### 1. Fair Pay and Minimum Rates - -**Principle:** Workers *must* receive minimum BECTU rates for their grade. - -**BECTU Minimum Rates (2025):** -- **Camera Operator:** £350/day -- **Sound Recordist:** £325/day -- **Lighting Technician:** £300/day -- **Runner/PA:** £125/day (min) -- **Editor:** £40/hour -- **Stage Crew:** £15/hour (min) - -**Overtime:** -- Time-and-a-half after 10 hours -- Double-time after 12 hours -- Triple-time for night shoots (midnight-6am) - -**Attestation:** *Must* meet BECTU minimum rates per BECTU Crew Agreement 2025. - ---- - -### 2. Working Hours Limits - -**Principle:** Working hours *must* comply with UK Working Time Regulations. - -**Limits:** -- Maximum 12 hours/day without rest -- Minimum 11 hours rest between shifts -- Maximum 48 hours/week averaged over 17 weeks -- Mandatory meal breaks (30 min after 6 hours) - -**Turnaround Time:** -- 11 hours between wrap and next call time -- 12 hours for night shoots - -**Attestation:** *Must* comply with UK Working Time Regulations 1998 and BECTU turnaround times. - ---- - -### 3. Health & Safety - -**Principle:** Productions *must* ensure safe working conditions. - -**Requirements:** -- Risk assessments before shoots -- PPE provided (high-vis, ear protection, safety boots) -- Stunt coordinators for hazardous scenes -- First aid on set -- COVID/disease safety protocols - -**Prohibited:** -- Unsafe rigging -- Overloaded vehicles -- No safety equipment -- Pressure to work in unsafe conditions - -**Attestation:** *Must* comply with UK Health and Safety at Work Act 1974. - ---- - -### 4. No Bullying or Harassment - -**Principle:** Workers *must* be protected from bullying and harassment. - -**Requirements:** -- Anti-bullying policy in place -- Anonymous reporting mechanism -- Investigation process within 14 days -- Protection from retaliation - -**Prohibited Behaviors:** -- Shouting/screaming at crew -- Sexual harassment -- Discrimination (race, gender, disability) -- Intimidation - -**Attestation:** *Must* have anti-bullying policy per UK Equality Act 2010 and BECTU guidance. - ---- - -### 5. Travel and Expenses - -**Principle:** Workers *must* be reimbursed for travel and expenses. - -**Requirements:** -- Travel time paid (door-to-door) -- Mileage: £0.45/mile (HMRC rate) -- Accommodation provided for distant locations -- Per diem: £35/day for meals - -**Attestation:** *Must* reimburse expenses per BECTU Crew Agreement. - ---- - -### 6. No Unpaid Work - -**Principle:** All work *must* be compensated. - -**Requirements:** -- Prep time paid -- Travel time paid -- Wrap time paid -- No "favors" or "exposure" as payment - -**Prohibited:** -- Unpaid trials -- "Test shoots" without pay -- "Deferred payment" for features (except profit-share with contract) - -**Attestation:** *Must* pay all hours worked per UK National Minimum Wage Act 1998. - ---- - -### 7. Equipment and Insurance - -**Principle:** Productions *must* provide equipment or rental fees. - -**Requirements:** -- Camera/sound equipment provided by production, OR -- Rental fee for personal equipment (£100-£150/day) -- Insurance for personal equipment used on production -- No requirement to use personal equipment without compensation - -**Attestation:** *Must* provide equipment or rental fees per BECTU Crew Agreement. - ---- - -## Freelancer Protections (MUST Clauses) - -### 8. Contracts and Paperwork - -**Principle:** Workers *must* receive written contracts before work begins. - -**Requirements:** -- Contract issued at least 48 hours before start -- Clear payment terms (NET 30 max) -- Clear scope of work -- Call sheets 24 hours in advance - -**Attestation:** *Must* provide written contracts per UK Employment Rights Act 1996. - ---- - -### 9. Cancellation Fees - -**Principle:** If work is canceled, workers *must* receive compensation. - -**BECTU Cancellation Schedule:** -- **< 24 hours notice:** 100% of day rate -- **24-48 hours notice:** 75% of day rate -- **48-72 hours notice:** 50% of day rate -- **> 72 hours notice:** No fee (unless contract specifies) - -**Attestation:** *Must* pay cancellation fees per BECTU Crew Agreement. - ---- - -### 10. No Exploitation of Runners - -**Principle:** Runners *must* be treated as workers, not interns. - -**Requirements:** -- Minimum £125/day (BECTU rate) -- No unpaid "work experience" -- Proper roles (not just tea-making) -- Pathway to crew roles - -**Prohibited:** -- Unpaid "internships" -- "Exposure" as payment -- Exploitation under guise of training - -**Attestation:** *Must* pay runners per BECTU minimum rates. - ---- - -## Theatre-Specific (MUST Clauses) - -### 11. Theatre Crew Safety - -**Principle:** Theatre workers *must* have safe rigging and equipment. - -**Requirements:** -- Inspected rigging (annual certification) -- Trained riggers only -- Safety chains on all flying equipment -- Emergency evacuation plans - -**Attestation:** *Must* comply with BECTU Theatre Agreement safety standards. - ---- - -### 12. No Split Shifts - -**Principle:** Theatre crew *must not* be required to work split shifts without consent. - -**Requirements:** -- No unpaid gaps between matinee and evening shows -- If split shift: 4-hour gap minimum + 2 hours paid -- Option to decline split shifts - -**Attestation:** *Should* avoid split shifts per BECTU Theatre Agreement. - ---- - -## Best Practices (SHOULD Clauses) - -### 13. Mental Health Support - -**Principle:** Productions *should* provide mental health resources. - -**Recommendations:** -- Access to counseling for stressful productions -- Mental health first aiders on set -- Trauma support for challenging content (violence, SV) - -**Attestation:** *Should* offer mental health support per BECTU Wellbeing Guide. - ---- - -### 14. Diversity and Inclusion - -**Principle:** Productions *should* promote diversity in crew and cast. - -**Recommendations:** -- 50% BAME representation targets -- Gender balance in crew (not just cast) -- Disability access and accommodations -- LGBTQ+ inclusion - -**Attestation:** *Should* promote diversity per UK Equality Act 2010 positive duties. - ---- - -### 15. Environmental Sustainability - -**Principle:** Productions *should* minimize environmental impact. - -**Recommendations:** -- Green production practices -- Reusable sets and props -- Sustainable catering -- Carbon offsetting - -**Attestation:** *Should* implement green practices per BECTU Environmental Charter. - ---- - -### 16. Career Development - -**Principle:** Productions *should* support crew training and development. - -**Recommendations:** -- Training budgets for crew -- Mentorship programs -- Shadowing opportunities for runners -- Pathways to HOD roles - -**Attestation:** *Should* support career development. - ---- - -## Advocacy Targets (COULD Clauses) - -### 17. Portable Benefits for Freelancers - -**Principle:** Employers *could* contribute to freelancer benefits. - -**Recommendations:** -- Health insurance stipends -- Pension contributions -- Equipment allowances -- Sick pay for regular crew - -**Attestation:** *Could* offer portable benefits per BECTU Freelancer Campaign. - ---- - -### 18. Profit-Share for Low-Budget - -**Principle:** Low-budget productions *could* offer profit-share instead of full rates. - -**Requirements (if profit-share):** -- Written profit-share agreement -- Transparent accounting -- Minimum guarantee (50% of BECTU rate) + profit % -- Time-limited (pay full rate if not profitable within 2 years) - -**Attestation:** *Could* offer profit-share with transparent contracts. - ---- - -## Enforcement and Grievance - -### BECTU Enforcement - -**Process:** -1. Member raises concern with BECTU rep -2. Informal resolution with production -3. Formal grievance if unresolved -4. ACAS mediation or employment tribunal -5. Industrial action (strike) for systemic violations - -**Blacklisting:** -- BECTU maintains list of non-compliant productions -- Members warned before accepting work - ---- - -## Integration with Employment Contracts - -**Required Contract Clauses:** - -Media/entertainment contracts *must* include: -- § Fair pay (BECTU minimum rates) -- § Working hours limits (max 12 hours/day) -- § Health & safety protections -- § Anti-bullying policy -- § Travel/expense reimbursement -- § No unpaid work -- § Equipment provision or rental fees -- § Written contracts (48 hours advance) -- § Cancellation fees -- § No exploitation of runners - -Contracts *should* include: -- § Mental health support -- § Diversity commitments -- § Environmental practices -- § Career development - -Contracts *could* include: -- § Portable benefits -- § Profit-share (if low-budget) - ---- - -@refs: -[1] UK Employment Rights Act 1996 -[2] UK Working Time Regulations 1998 -[3] UK Health and Safety at Work Act 1974 -[4] UK National Minimum Wage Act 1998 -[5] UK Equality Act 2010 -[6] Copyright, Designs and Patents Act 1988 -[7] BECTU Crew Agreement (2025) -[8] BECTU Theatre Agreement (2024) -[9] BECTU Freelancer Campaign -[10] BECTU Wellbeing Guide -[11] BECTU Environmental Charter -@end diff --git a/union-policy-parser/schemas/equity-performers-rights.a2ml b/union-policy-parser/schemas/equity-performers-rights.a2ml deleted file mode 100644 index ee4bfc0..0000000 --- a/union-policy-parser/schemas/equity-performers-rights.a2ml +++ /dev/null @@ -1,407 +0,0 @@ -# Equity Performers and Entertainers Rights - -@abstract: -This schema defines Equity (UK actors' union) standards for performers, actors, -dancers, singers, and entertainers. It encodes protections for stage, screen, and -voice work, with focus on fair pay, rehearsal compensation, repeat fees, and -anti-exploitation safeguards for vulnerable performers. -@end - -@requires: -- UK Employment Rights Act 1996 -- UK Working Time Regulations 1998 -- Equity Minimum Terms Agreement -- Equity Theatre Agreement -- Equity Low Pay No Pay Campaign Standards -- UK Equality Act 2010 -- Copyright, Designs and Patents Act 1988 -@end - ---- - -## Core Rights (MUST Clauses) - -### 1. Fair Pay and Minimum Rates - -**Principle:** Performers *must* receive Equity minimum rates. - -**Equity Minimum Rates (2025):** -- **West End Theatre:** £750/week (8 performances) -- **Fringe Theatre:** £150/week (Equity-approved venues only) -- **Regional Theatre:** £600/week -- **TV/Film (Principal):** £150/day -- **TV/Film (Supporting Artist):** £100/day -- **Voice-over (Commercial):** £250/session + repeat fees -- **Dance (West End):** £800/week - -**Rehearsal Pay:** -- Minimum 4 weeks paid rehearsal -- 75% of performance rate during rehearsal - -**Attestation:** *Must* meet Equity minimum rates per Equity Minimum Terms Agreement 2025. - ---- - -### 2. Repeat Fees and Royalties - -**Principle:** Performers *must* receive repeat fees for broadcast/streaming work. - -**Repeat Fee Schedule:** -- **UK terrestrial repeat:** 75% of original fee -- **Streaming (per platform):** 50% of original fee per year -- **International sales:** 100% of original fee per territory -- **DVD/Blu-ray:** 10% of wholesale price - -**Buyout Restrictions:** -- No unlimited buyouts for under 5x original fee -- Streaming buyouts max 3 years - -**Attestation:** *Must* pay repeat fees per Equity Minimum Terms Agreement. - ---- - -### 3. No Unpaid Work - -**Principle:** All performance work *must* be compensated. - -**Requirements:** -- Auditions: Unpaid up to 1 hour (recall = paid) -- Rehearsals: All paid -- Previews: Full performance rate -- Workshops: Paid at rehearsal rate - -**Prohibited:** -- "Profit-share" without guaranteed minimum -- "Exposure" as payment -- Unpaid showcases (except approved fringe) -- Free work for "experience" - -**Attestation:** *Must* pay all performance work per UK National Minimum Wage Act 1998 and Equity standards. - ---- - -### 4. Safe Working Conditions - -**Principle:** Performers *must* have safe working environments. - -**Requirements:** -- Intimacy coordinator for intimate scenes -- Stunt coordinator for physical work -- Safe stage combat training -- Mental health support for challenging roles -- Nudity clauses (explicit consent, closed set) - -**Prohibited:** -- Coercion into nudity/intimate scenes -- Unsafe stunts without training -- Psychological abuse "for the role" - -**Attestation:** *Must* ensure safe working conditions per UK Health and Safety at Work Act 1974 and Equity guidance. - ---- - -### 5. Anti-Bullying and Harassment - -**Principle:** Performers *must* be protected from bullying and harassment. - -**Requirements:** -- Anti-harassment policy -- Anonymous reporting -- Investigation within 14 days -- Protection from retaliation - -**Prohibited:** -- Sexual harassment (Harvey Weinstein clause) -- Shouting/abuse by directors -- Discrimination (age, race, gender, disability) -- Blacklisting for complaints - -**Attestation:** *Must* have anti-harassment policy per UK Equality Act 2010 and Equity Time's Up campaign. - ---- - -### 6. Expenses and Travel - -**Principle:** Performers *must* be reimbursed for travel and expenses. - -**Requirements:** -- Travel to rehearsals/performances paid -- Mileage: £0.45/mile -- Accommodation for distant venues -- Per diem: £35/day -- Costume cleaning (if required to clean own clothes) - -**Attestation:** *Must* reimburse expenses per Equity Minimum Terms Agreement. - ---- - -### 7. Contracts and Paperwork - -**Principle:** Performers *must* receive written contracts before work begins. - -**Requirements:** -- Contract issued 7 days before rehearsal starts -- Clear payment terms -- Performance schedule -- Cancellation terms - -**Attestation:** *Must* provide written contracts per UK Employment Rights Act 1996 and Equity standards. - ---- - -## Theatre-Specific (MUST Clauses) - -### 8. Matinee Limits - -**Principle:** Actors *must not* perform excessive matinees. - -**Limits:** -- Maximum 8 performances/week -- Maximum 2 matinees/week -- No matinee + evening on same day for musicals -- 1 day off per week (not split) - -**Attestation:** *Must* comply with Equity Theatre Agreement performance limits. - ---- - -### 9. Understudies and Swings - -**Principle:** Understudies *must* be compensated for going on. - -**Requirements:** -- Understudy retainer: +25% of base salary -- Going on: +50% for that performance -- Rehearsals: Paid rehearsal time - -**Attestation:** *Must* pay understudy premiums per Equity Theatre Agreement. - ---- - -### 10. Touring Conditions - -**Principle:** Touring performers *must* have safe and fair touring terms. - -**Requirements:** -- Accommodation provided (single rooms for principals) -- Travel time paid -- Touring allowance: £50/week -- Maximum 10 weeks on tour without break - -**Attestation:** *Must* provide touring support per Equity Theatre Agreement. - ---- - -## Film/TV-Specific (MUST Clauses) - -### 11. Working Hours on Set - -**Principle:** Film/TV performers *must* have reasonable working hours. - -**Limits:** -- Maximum 12 hours/day (including breaks) -- Minimum 11 hours rest between calls -- Meal breaks: 30 min after 6 hours -- Overtime: Time-and-a-half after 10 hours - -**Child Performers:** -- Maximum 8 hours/day (including school time) -- Chaperones required -- License from local authority - -**Attestation:** *Must* comply with UK Working Time Regulations 1998 and Equity child performance rules. - ---- - -### 12. Repeat Fees for Streaming - -**Principle:** TV/film performers *must* receive streaming repeat fees. - -**Requirements:** -- Per-platform fees (Netflix, Amazon, Disney+, etc.) -- Annual renewal (not perpetual) -- Transparent accounting -- Minimum £100/year per platform - -**Attestation:** *Must* pay streaming fees per Equity Streaming Campaign. - ---- - -### 13. No Self-Tape Exploitation - -**Principle:** Self-tapes *must* be reasonable and not exploitative. - -**Requirements:** -- Maximum 2 scenes for audition -- Maximum 2 rounds of self-tapes -- Reimbursement for studio rental if complex scene -- No use of self-tapes in final production - -**Prohibited:** -- Requesting 10+ minute self-tapes -- Using self-tapes instead of hiring -- Requesting elaborate setups without compensation - -**Attestation:** *Must* limit self-tape requirements per Equity Self-Tape Code. - ---- - -## Voice-Over-Specific (MUST Clauses) - -### 14. Voice-Over Rates and Buyouts - -**Principle:** Voice artists *must* receive fair rates and limited buyouts. - -**Requirements:** -- Session fee: £250/hour (commercial) -- Repeat fees for each broadcast -- Buyout max 2 years (commercial) -- Additional fee for multiple platforms - -**Prohibited:** -- Unlimited buyouts without 5x+ premium -- "All rights in perpetuity" clauses -- No repeat fees for commercials - -**Attestation:** *Must* pay voice-over rates per Equity Voice Agreement. - ---- - -## Best Practices (SHOULD Clauses) - -### 15. Mental Health Support - -**Principle:** Productions *should* provide mental health resources. - -**Recommendations:** -- Counseling for challenging roles (trauma, violence, SV) -- Mental health first aiders -- Intimacy coordinators as standard -- Decompression sessions after intense work - -**Attestation:** *Should* offer mental health support per Equity Wellbeing Guide. - ---- - -### 16. Diversity and Inclusion - -**Principle:** Casting *should* reflect diversity. - -**Recommendations:** -- Colorblind casting where appropriate -- Disability representation -- LGBTQ+ inclusion -- Age-appropriate casting (no "youth-washing") - -**Attestation:** *Should* promote diversity per UK Equality Act 2010 positive duties. - ---- - -### 17. Career Development - -**Principle:** Employers *should* support performer development. - -**Recommendations:** -- Training budgets for continuing education -- Access to voice coaches, movement classes -- Industry networking opportunities - -**Attestation:** *Should* support career development. - ---- - -## Advocacy Targets (COULD Clauses) - -### 18. Portable Benefits for Freelancers - -**Principle:** Employers *could* contribute to freelancer benefits. - -**Recommendations:** -- Health insurance stipends -- Pension contributions -- Equipment allowances (voice artists) -- Sick pay for regular performers - -**Attestation:** *Could* offer portable benefits per Equity Freelancer Campaign. - ---- - -### 19. Profit-Share with Guarantees - -**Principle:** Fringe theatre *could* offer profit-share with minimums. - -**Requirements (if profit-share):** -- Minimum guarantee (50% of Equity fringe rate) -- Transparent accounting -- Capped investor returns before profit-share -- Time-limited (revert to full pay if profitable) - -**Attestation:** *Could* offer profit-share with transparent terms per Equity Low Pay No Pay Campaign. - ---- - -## Enforcement and Grievance - -### Equity Enforcement - -**Process:** -1. Member raises concern with Equity rep -2. Informal resolution with production -3. Formal grievance if unresolved -4. ACAS mediation or employment tribunal -5. Industrial action (Equity can call strike) - -**Don't Work For List:** -- Equity maintains list of non-compliant productions/companies -- Members warned before accepting contracts - ---- - -## Integration with Employment Contracts - -**Required Contract Clauses:** - -Performer contracts *must* include: -- § Fair pay (Equity minimum rates) -- § Repeat fees and royalties -- § No unpaid work (rehearsals paid) -- § Safe working conditions (intimacy coordinator, stunt coordination) -- § Anti-harassment policy -- § Travel/expense reimbursement -- § Written contracts (7 days advance) -- § Matinee limits (max 8 performances/week) -- § Understudy compensation -- § Touring support (if touring) -- § Working hours limits (max 12 hours/day) -- § Streaming repeat fees -- § Self-tape limits -- § Voice-over rates (if VO work) - -Contracts *should* include: -- § Mental health support -- § Diversity commitments -- § Career development - -Contracts *could* include: -- § Portable benefits -- § Profit-share (with guarantees) - ---- - -@refs: -[1] UK Employment Rights Act 1996 -[2] UK Working Time Regulations 1998 -[3] UK Health and Safety at Work Act 1974 -[4] UK National Minimum Wage Act 1998 -[5] UK Equality Act 2010 -[6] Copyright, Designs and Patents Act 1988 -[7] Equity Minimum Terms Agreement (2025) -[8] Equity Theatre Agreement (2024) -[9] Equity Streaming Campaign -[10] Equity Low Pay No Pay Campaign -[11] Equity Self-Tape Code -[12] Equity Voice Agreement -[13] Equity Time's Up Campaign -[14] Equity Wellbeing Guide -[15] Equity Freelancer Campaign -@end diff --git a/union-policy-parser/schemas/gmb-general-workers-rights.a2ml b/union-policy-parser/schemas/gmb-general-workers-rights.a2ml deleted file mode 100644 index a0d1f8a..0000000 --- a/union-policy-parser/schemas/gmb-general-workers-rights.a2ml +++ /dev/null @@ -1,400 +0,0 @@ -# GMB General Workers Rights and Standards - -@abstract: -This schema defines the GMB (General, Municipal, Boilermakers) union standards for -general workers across multiple sectors including care, local government, utilities, -logistics, manufacturing, and retail. It encodes fundamental worker protections, -living wage standards, and anti-zero-hours exploitation. -@end - -@requires: -- UK Employment Rights Act 1996 -- UK National Minimum Wage Act 1998 -- UK Working Time Regulations 1998 -- UK Health and Safety at Work Act 1974 -- UK Equality Act 2010 -- Trade Union and Labour Relations (Consolidation) Act 1992 -- GMB Living Wage Campaign Standards -@end - ---- - -## Core Rights (MUST Clauses) - -### 1. Living Wage, Not Minimum Wage - -**Principle:** Workers *must* receive Real Living Wage, not just National Minimum Wage. - -**GMB Living Wage Standards (2025):** -- **Real Living Wage:** £12.60/hour (UK) -- **London Living Wage:** £13.85/hour -- **National Minimum Wage:** £11.44/hour (inadequate per GMB) - -**Attestation:** *Must* pay Real Living Wage per GMB Living Wage Campaign. - ---- - -### 2. No Zero-Hours Contracts - -**Principle:** Workers *must* have guaranteed hours, not zero-hours contracts. - -**Requirements:** -- Minimum 16 hours/week guaranteed -- Pro-rata pay and benefits -- Right to refuse shifts without penalty (if beyond guaranteed hours) -- Conversion to permanent after 12 weeks - -**Prohibited:** -- Pure zero-hours (no guaranteed hours) -- Exclusive zero-hours (can't work elsewhere) -- Retaliation for refusing shifts - -**Attestation:** *Must* provide guaranteed hours per GMB Campaign Against Zero-Hours. - ---- - -### 3. Fair Working Hours - -**Principle:** Working hours *must* comply with UK regulations. - -**Limits:** -- Maximum 48 hours/week averaged over 17 weeks -- Minimum 11 hours rest between shifts -- Minimum 20-minute break for 6+ hour shifts -- Maximum 8 hours night work in 24 hours - -**Attestation:** *Must* comply with UK Working Time Regulations 1998. - ---- - -### 4. Equal Pay for Equal Work - -**Principle:** Workers *must* receive equal pay regardless of protected characteristics. - -**Requirements:** -- No gender pay gap -- No ethnicity pay gap -- Equal pay for part-time workers (pro-rata) -- Equal pay for agency workers (after 12 weeks) - -**Attestation:** *Must* comply with UK Equality Act 2010 equal pay provisions. - ---- - -### 5. Safe Working Conditions - -**Principle:** Employers *must* ensure workplace safety. - -**Requirements:** -- Risk assessments -- PPE provided free of charge -- Health and safety training -- Accident reporting -- Safety representatives (GMB-trained) - -**Attestation:** *Must* comply with UK Health and Safety at Work Act 1974. - ---- - -### 6. Fair Dismissal and Grievance - -**Principle:** Workers *must* have protection from unfair dismissal. - -**Requirements:** -- Disciplinary procedures (ACAS Code of Practice 1) -- Right to union representation -- Right to appeal -- No summary dismissal except gross misconduct - -**Attestation:** *Must* follow ACAS Code of Practice 1 and UK Employment Rights Act 1996. - ---- - -### 7. Sick Pay and Leave - -**Principle:** Workers *must* receive sick pay. - -**Requirements:** -- Statutory Sick Pay (SSP) as minimum -- GMB campaigns for occupational sick pay: - - Full pay for 4 weeks - - Half pay for next 8 weeks - -**Attestation:** *Must* provide SSP per UK regulations; *should* provide occupational sick pay. - ---- - -### 8. Holiday Entitlement - -**Principle:** Workers *must* receive paid holiday. - -**Requirements:** -- Minimum 5.6 weeks/year (28 days for full-time) -- Pro-rata for part-time -- Paid at normal rate -- Cannot be replaced with "rolled-up holiday pay" - -**Attestation:** *Must* provide holiday per UK Working Time Regulations 1998. - ---- - -### 9. Pension Provision - -**Principle:** Employers *must* provide pension enrollment. - -**Requirements:** -- Auto-enrollment for eligible workers -- Minimum 3% employer contribution -- Minimum 5% employee contribution - -**Attestation:** *Must* provide pension per UK Pensions Act 2008. - ---- - -### 10. No Bogus Self-Employment - -**Principle:** Workers *must* be classified correctly (employee vs self-employed). - -**Requirements:** -- If employer controls how work is done: employee -- If fixed hours/location: employee -- If no substitution allowed: employee -- IR35 compliance - -**Prohibited:** -- Misclassifying employees as self-employed -- Gig economy exploitation (Uber model) -- Avoiding employment rights through "contractor" status - -**Attestation:** *Must* classify workers correctly per UK Employment Rights Act 1996 and IR35 rules. - ---- - -## Sector-Specific (MUST Clauses) - -### 11. Care Workers: Travel Time - -**Principle:** Care workers *must* be paid for travel time between clients. - -**Requirements:** -- Travel time paid at full rate -- Mileage reimbursed (£0.45/mile) -- No unpaid gaps between visits - -**Attestation:** *Must* pay travel time per GMB Care Workers Campaign. - ---- - -### 12. Retail Workers: Contracted Hours - -**Principle:** Retail workers *must* have guaranteed contracted hours. - -**Requirements:** -- Minimum 16 hours/week contracted -- No "flexible contracts" meaning zero-hours -- Right to refuse additional shifts -- Premium pay for short-notice shifts (time-and-a-half if <48 hours notice) - -**Attestation:** *Must* provide contracted hours per GMB Retail Workers Rights. - ---- - -### 13. Cleaners: Outsourcing Protections - -**Principle:** Outsourced cleaners *must* have same terms as in-house workers. - -**Requirements:** -- TUPE protections on outsourcing -- Same pay and conditions as in-house -- Pension access -- Union recognition carried over - -**Attestation:** *Must* apply TUPE protections per UK regulations. - ---- - -### 14. Utilities Workers: No Fire and Rehire - -**Principle:** Employers *must not* use "fire and rehire" to cut terms. - -**Requirements:** -- No termination to impose worse contracts -- Collective bargaining for changes -- Protection from constructive dismissal - -**Attestation:** *Must not* use fire and rehire per GMB Campaign Against Fire and Rehire. - ---- - -### 15. Logistics/Delivery: No Algorithmic Management Abuse - -**Principle:** Gig economy workers *must* have protection from algorithmic management. - -**Requirements:** -- Right to human review of algorithmic decisions -- Transparency about performance metrics -- No arbitrary deactivation -- Appeals process - -**Attestation:** *Must* provide algorithmic transparency per GMB Gig Economy Charter. - ---- - -## Best Practices (SHOULD Clauses) - -### 16. Trade Union Recognition - -**Principle:** Employers *should* recognize GMB for collective bargaining. - -**Recommendations:** -- GMB rep access to workplace -- Time off for union duties -- Check-off system (union dues deducted) -- Partnership working - -**Attestation:** *Should* recognize GMB per UK Trade Union and Labour Relations Act 1992. - ---- - -### 17. Training and Development - -**Principle:** Employers *should* invest in worker training. - -**Recommendations:** -- Apprenticeships -- Skills training -- Health and safety qualifications -- Career progression pathways - -**Attestation:** *Should* provide training opportunities. - ---- - -### 18. Mental Health and Wellbeing - -**Principle:** Employers *should* support worker wellbeing. - -**Recommendations:** -- Mental health first aiders -- Counseling access (EAP) -- Stress risk assessments -- Right to disconnect (no out-of-hours emails) - -**Attestation:** *Should* support wellbeing per GMB Mental Health Charter. - ---- - -### 19. Environmental Protections - -**Principle:** Employers *should* involve workers in green transition. - -**Recommendations:** -- Just Transition for fossil fuel workers -- Green skills training -- Worker voice in environmental decisions - -**Attestation:** *Should* support just transition per GMB Green New Deal. - ---- - -## Advocacy Targets (COULD Clauses) - -### 20. Four-Day Week - -**Principle:** Employers *could* pilot four-day working week. - -**Recommendations:** -- Same pay for 4 days (32 hours) -- Improved work-life balance -- Productivity gains - -**Attestation:** *Could* pilot four-day week per GMB 4-Day Week Campaign. - ---- - -### 21. Workplace Democracy - -**Principle:** Employers *could* give workers board representation. - -**Recommendations:** -- Worker directors on company boards -- Employee ownership schemes -- Profit-sharing - -**Attestation:** *Could* introduce workplace democracy per GMB Worker Voice Campaign. - ---- - -## Enforcement and Grievance - -### GMB Enforcement - -**Process:** -1. Member raises concern with GMB rep -2. Informal resolution with management -3. Formal grievance (ACAS Code of Practice 1) -4. Collective grievance if systemic -5. Industrial action (strike ballot) - -**Legal Support:** -- GMB provides legal representation for employment tribunals - ---- - -## Integration with Employment Contracts - -**Required Contract Clauses:** - -General worker contracts *must* include: -- § Real Living Wage (not minimum wage) -- § No zero-hours (minimum 16 hours/week guaranteed) -- § Fair working hours (max 48 hours/week) -- § Equal pay for equal work -- § Safe working conditions -- § Fair dismissal procedures (ACAS) -- § Sick pay (SSP minimum) -- § Holiday entitlement (5.6 weeks/year) -- § Pension provision (auto-enrollment) -- § Correct employment status (no bogus self-employment) - -Sector-specific *must* include: -- § Care workers: Travel time paid -- § Retail workers: Contracted hours -- § Cleaners: TUPE protections -- § Utilities: No fire and rehire -- § Logistics: Algorithmic transparency - -Contracts *should* include: -- § Trade union recognition -- § Training and development -- § Mental health support -- § Environmental protections - -Contracts *could* include: -- § Four-day week pilot -- § Workplace democracy - ---- - -@refs: -[1] UK Employment Rights Act 1996 -[2] UK National Minimum Wage Act 1998 -[3] UK Working Time Regulations 1998 -[4] UK Health and Safety at Work Act 1974 -[5] UK Equality Act 2010 -[6] UK Pensions Act 2008 -[7] Trade Union and Labour Relations (Consolidation) Act 1992 -[8] GMB Living Wage Campaign Standards -[9] GMB Campaign Against Zero-Hours -[10] GMB Care Workers Campaign -[11] GMB Retail Workers Rights -[12] GMB Campaign Against Fire and Rehire -[13] GMB Gig Economy Charter -[14] GMB Mental Health Charter -[15] GMB Green New Deal -[16] GMB 4-Day Week Campaign -[17] GMB Worker Voice Campaign -[18] ACAS Code of Practice 1 -[19] IR35 Rules (HMRC) -[20] TUPE Regulations -@end diff --git a/union-policy-parser/schemas/iww-freelancer-rights.a2ml b/union-policy-parser/schemas/iww-freelancer-rights.a2ml deleted file mode 100644 index e7ee8a5..0000000 --- a/union-policy-parser/schemas/iww-freelancer-rights.a2ml +++ /dev/null @@ -1,326 +0,0 @@ -# IWW Freelancer Rights and Fair Contracts - -@abstract: -This schema defines the Industrial Workers of the World (IWW) Freelance Journalists Union -standards for fair freelance contracts. It encodes protections against exploitation, -payment standards, and collective organizing rights for atomized workers in the gig economy. -@end - -@requires: -- UK Employment Rights Act 1996 -- UK National Minimum Wage Act 1998 -- UK Late Payment of Commercial Debts (Interest) Act 1998 -- Copyright, Designs and Patents Act 1988 -- GDPR (EU 2016/679) -- IWW Freelance Branch Constitution -@end - ---- - -## Core Rights (MUST Clauses) - -### 1. Fair Payment Terms - -**Principle:** Freelancers *must* receive payment within reasonable timeframes with penalties for late payment. - -**Requirements:** -- Payment terms clearly specified (NET 30 maximum) -- Late payment penalties (minimum 5% after due date) -- No "payment on publication" clauses -- No unpaid trial periods -- No speculative work without compensation - -**Prohibited Practices:** -- NET 60, NET 90, or longer payment terms -- "We'll pay when we get paid" clauses -- Payment contingent on third-party actions -- Unpaid "test assignments" -- "Exposure" as payment - -**Attestation:** *Must* comply with UK Late Payment of Commercial Debts (Interest) Act 1998; payment terms ≤ NET 30 days. - ---- - -### 2. Kill Fee Provision - -**Principle:** If commissioned work is rejected/canceled, freelancer *must* receive compensation. - -**Requirements:** -- Kill fee specified in contract (minimum 50%) -- Payable even if work not used -- Triggered by editorial decision, not freelancer fault - -**Kill Fee Minimums:** -- **50%** of agreed fee (minimum acceptable) -- **75%** (IWW recommended) -- **100%** if killed after final draft submission - -**Attestation:** *Must* include kill fee provision ≥ 50% of contracted payment. - ---- - -### 3. Copyright Retention - -**Principle:** Freelancers *must* retain copyright or have limited, defined licensing. - -**Requirements:** -- Default: Copyright remains with freelancer -- If transferred: "First publication rights only" OR specific time-limited license -- No "all rights" or "work for hire" clauses -- No perpetual, worldwide, exclusive licenses without significant additional payment - -**Acceptable Licensing:** -- First publication rights (print and online) -- Time-limited exclusive (e.g., 6 months exclusive, then non-exclusive) -- Non-exclusive license (freelancer can resell) - -**Prohibited:** -- "All rights forever" without 3x+ standard rate -- Work-for-hire agreements (except with employee-equivalent pay) -- Perpetual exclusive licenses -- Retroactive rights grabs - -**Attestation:** *Must* retain copyright or grant only limited, defined licenses per Copyright, Designs and Patents Act 1988. - ---- - -### 4. Expenses Reimbursement - -**Principle:** Reasonable expenses *must* be reimbursed promptly. - -**Requirements:** -- Pre-approval process defined -- Reimbursement within 30 days of submission -- No cap on reasonable, necessary expenses - -**Reimbursable Expenses:** -- Travel (mileage, public transport) -- Accommodation -- Equipment rental -- Research materials -- Communication (phone, internet) - -**Attestation:** *Must* specify expense reimbursement process and timeframe ≤ 30 days. - ---- - -### 5. Termination Notice - -**Principle:** Either party *must* provide notice before terminating contract. - -**Requirements:** -- Minimum 14 days notice (for ongoing contracts) -- Immediate termination only for gross misconduct -- Payment for all work completed up to termination date - -**Attestation:** *Must* provide ≥ 14 days notice per UK Employment Rights Act 1996 (adapted for freelancers). - ---- - -### 6. No Exploitation Clauses - -**Principle:** Contracts *must not* contain exploitative terms. - -**Prohibited Clauses:** -- Free trial periods -- Unpaid "probation" -- Spec work (work before contract signed) -- Unpaid revisions beyond agreed scope -- Non-compete clauses (except with compensation) -- Exclusivity without retainer -- Indemnity clauses shifting all risk to freelancer - -**Attestation:** *Must not* contain clauses that exploit freelancer's precarious position. - ---- - -## Collective Organizing Rights (MUST Clauses) - -### 7. Union Recognition - -**Principle:** Employers *must* recognize freelancer's right to union representation. - -**Requirements:** -- Right to IWW representation in disputes -- No retaliation for union activity -- Access to union delegate during grievance procedures - -**Prohibited:** -- Blacklisting union members -- Refusing work due to union membership -- Intimidation for organizing - -**Attestation:** *Must* comply with UK Trade Union and Labour Relations (Consolidation) Act 1992. - ---- - -### 8. Collective Voice Mechanisms - -**Principle:** Atomized freelancers *should* have mechanisms for collective bargaining. - -**Recommendations:** -- Freelancer committees for rate negotiations -- Transparent rate cards -- Grievance aggregation (multiple freelancers can file jointly) - -**Attestation:** *Should* provide mechanisms for collective freelancer input on terms. - ---- - -## Payment Standards (MUST Clauses) - -### 9. Minimum Rates - -**Principle:** Payment *must* meet or exceed IWW/NUJ minimum rates. - -**IWW Freelance Journalist Minimums (2025):** -- **£350/day** (7.5 hours) or **£46.67/hour** -- **£0.40/word** for articles (minimum) -- **£500** for commissioned feature (1000-1500 words) -- **£150** for news story (300-500 words) - -**Attestation:** *Must* meet or exceed IWW/NUJ minimum rates adjusted for 2025 UK National Minimum Wage. - ---- - -### 10. Rate Transparency - -**Principle:** Payment rates *should* be transparent and standardized. - -**Recommendations:** -- Published rate card -- No secret negotiations -- Regular rate reviews -- Cost-of-living adjustments - -**Attestation:** *Should* publish transparent rate structures. - ---- - -## Best Practices (SHOULD Clauses) - -### 11. Portable Benefits - -**Principle:** Employers *should* contribute to freelancer benefits. - -**Recommendations:** -- Health insurance stipend (£100-£200/month) -- Pension contributions (3-5% of fee) -- Equipment allowance (£500/year) -- Training budget (£300/year) - -**Attestation:** *Should* provide portable benefits per IWW Portable Benefits Campaign. - ---- - -### 12. Mental Health Support - -**Principle:** Employers *should* provide mental health resources for trauma coverage. - -**Recommendations:** -- Counseling access after traumatic assignments -- Mental health days without penalty -- Peer support networks - -**Attestation:** *Should* offer mental health support for freelancers. - ---- - -### 13. Equipment Provision - -**Principle:** Employers *should* provide or reimburse necessary equipment. - -**Recommendations:** -- Camera/recording equipment (if required for assignment) -- Software licenses (Adobe, etc.) -- Phone/internet stipend -- Home office setup (if remote) - -**Attestation:** *Should* provide equipment or stipends for tools required for work. - ---- - -## Advocacy Targets (COULD Clauses) - -### 14. Income Stability - -**Principle:** Employers *could* provide minimum income guarantees for regular contributors. - -**Recommendations:** -- Retainer for exclusive contributors -- Minimum assignments per month -- First-refusal rights for freelancer - -**Attestation:** *Could* offer income stability mechanisms. - ---- - -### 15. Platform Rights - -**Principle:** Platform-based freelancers *could* have additional protections. - -**Recommendations:** -- No arbitrary deactivation -- Transparent algorithmic ranking -- Data access rights -- Human review for disputes - -**Attestation:** *Could* provide platform-specific rights per Fairwork Principles. - ---- - -## Enforcement and Grievance - -### IWW Enforcement - -**Process:** -1. Freelancer alleges violation -2. IWW rep validates against this schema -3. Demand letter sent to employer (7-day response) -4. Collective action if unresolved (e.g., refusal to accept future assignments) -5. Legal action if necessary - -**Automated Checks:** -- `union-policy-parser` validates contracts -- Flags exploitative clauses -- Generates demand letters - ---- - -## Integration with Employment Contracts - -**Required Contract Clauses:** - -Freelance contracts *must* include: -- § Payment terms (NET 30 max) -- § Kill fee provision (≥50%) -- § Copyright retention -- § Expense reimbursement -- § Termination notice (≥14 days) -- § No exploitation clauses - -Contracts *should* include: -- § Portable benefits -- § Mental health support -- § Equipment provision -- § Rate transparency - -Contracts *could* include: -- § Income stability mechanisms -- § Platform rights -- § Collective bargaining - ---- - -@refs: -[1] UK Employment Rights Act 1996 -[2] UK National Minimum Wage Act 1998 -[3] UK Late Payment of Commercial Debts (Interest) Act 1998 -[4] Copyright, Designs and Patents Act 1988 -[5] GDPR (EU 2016/679) -[6] Trade Union and Labour Relations (Consolidation) Act 1992 -[7] IWW Freelance Branch Constitution -[8] NUJ Freelance Fees Guide (2025) -[9] Fairwork Principles (Platform Work Charter) -[10] IWW Portable Benefits Campaign -@end diff --git a/union-policy-parser/schemas/nuj-code-of-ethics.a2ml b/union-policy-parser/schemas/nuj-code-of-ethics.a2ml deleted file mode 100644 index 9ad71f5..0000000 --- a/union-policy-parser/schemas/nuj-code-of-ethics.a2ml +++ /dev/null @@ -1,379 +0,0 @@ -# NUJ Code of Ethics and Conduct - -@abstract: -This schema defines the National Union of Journalists' Code of Conduct as a -machine-readable policy for automated validation of employment contracts, -press releases, and editorial policies. It encodes the core ethical principles -that NUJ members must uphold, with must/should/could clauses for enforcement -by union-policy-parser. -@end - -@requires: -- NUJ Code of Conduct (2024 edition) -- UK Defamation Act 2013 -- GDPR (EU 2016/679) -- IPSO Editors' Code of Practice -- Ofcom Broadcasting Code -- UK Copyright, Designs and Patents Act 1988 -@end - ---- - -## Core Principles (MUST Clauses) - -### 1. Truth and Accuracy - -**Principle:** A journalist shall strive to ensure that information disseminated is **honestly conveyed, accurate and fair**. - -**Requirements:** -- Factual accuracy in all reporting -- Fair representation of events and subjects -- Honest conveyance without distortion -- Correction of errors promptly and prominently - -**Violations Include:** -- Knowingly publishing false information -- Distorting facts to fit narrative -- Failing to correct known errors -- Misleading headlines or framing - -**Attestation:** *Must* comply with NUJ Code §1 and IPSO Clause 1 (Accuracy). - ---- - -### 2. Independence - -**Principle:** A journalist shall **strive to eliminate distortion, news suppression and censorship**, and shall only accept instructions consistent with their professional obligations. - -**Requirements:** -- Editorial independence from commercial pressures -- Resistance to censorship (internal or external) -- Disclosure of conflicts of interest -- Refusal of gifts/inducements that compromise impartiality - -**Violations Include:** -- Suppressing news due to advertiser pressure -- Accepting payment for favorable coverage -- Concealing financial interests in subjects covered -- Following editorial instructions that distort truth - -**Attestation:** *Must* maintain editorial independence per NUJ Code §2. - ---- - -### 3. Fairness - -**Principle:** A journalist shall strive to ensure that the information they disseminate is **fair and does not misrepresent or distort** facts or context. - -**Requirements:** -- Right of reply for criticized subjects -- Balanced representation of viewpoints -- Context preservation (no misleading editing) -- Fair treatment of vulnerable subjects - -**Violations Include:** -- Denying right of reply -- Quote mining to misrepresent views -- Omitting crucial context -- Unfair treatment of marginalized groups - -**Attestation:** *Must* provide right of reply per NUJ Code §3 and IPSO Clause 1. - ---- - -### 4. Privacy and Harassment - -**Principle:** A journalist shall respect **privacy** and **avoid harassment**. - -**Requirements:** -- Respect for private life (GDPR Article 8) -- Justified public interest for privacy intrusion -- No harassment in pursuit of stories -- Protection of vulnerable subjects (children, grief-stricken, trauma survivors) - -**Violations Include:** -- Intrusion without public interest justification -- Persistent unwanted contact (harassment) -- Publishing private information without consent -- Exploiting vulnerable subjects - -**Public Interest Defense:** Privacy intrusion justified only if: -- Detecting/exposing crime -- Protecting public health/safety -- Preventing public from being misled -- Exposing abuse of power/hypocrisy - -**Attestation:** *Must* comply with GDPR and NUJ Code §4; privacy intrusion requires public interest justification. - ---- - -### 5. Accountability and Corrections - -**Principle:** A journalist shall **acknowledge mistakes** and **correct errors promptly** and with due prominence. - -**Requirements:** -- Prompt correction publication -- Prominence matching original error -- Transparent corrections policy -- Apology where appropriate - -**Violations Include:** -- Refusing to correct errors -- Burying corrections in obscure locations -- Delayed corrections -- Failing to apologize for serious errors - -**Attestation:** *Must* publish corrections promptly per NUJ Code §5 and IPSO Clause 1. - ---- - -### 6. Source Protection - -**Principle:** A journalist shall **protect confidential sources** of information. - -**Requirements:** -- Non-disclosure of source identity without consent -- Secure source communication methods -- Resistance to legal/employer pressure to reveal sources -- Clear disclosure of "on/off the record" terms - -**Exceptions:** Only if source: -- Lied about material facts -- Caused direct harm through deception - -**Legal Protection:** UK Contempt of Court Act 1981 §10 protects journalists from forced source disclosure (unless court orders in interests of justice). - -**Attestation:** *Must* protect sources per NUJ Code §6 and UK law. - ---- - -### 7. Discrimination - -**Principle:** A journalist shall **not publish material that encourages discrimination** on grounds of: -- Race -- Colour -- Religion -- Gender/sex -- Sexual orientation -- Disability -- Age -- Other protected characteristics (Equality Act 2010) - -**Requirements:** -- Avoid discriminatory language -- Challenge stereotypes -- Represent diversity accurately -- No incitement to hatred - -**Violations Include:** -- Using racist/sexist language -- Perpetuating harmful stereotypes -- Inciting hatred against protected groups -- Discriminatory job descriptions/assignments - -**Attestation:** *Must* comply with UK Equality Act 2010 and NUJ Code §7. - ---- - -### 8. Plagiarism - -**Principle:** A journalist shall **not plagiarise**. - -**Requirements:** -- Original work or proper attribution -- Citation of sources -- Licensing compliance for third-party content -- No passing off others' work as own - -**Violations Include:** -- Copying text without attribution -- Using photos/video without permission -- Republishing paywalled content without license -- Claiming authorship of ghostwritten work - -**Attestation:** *Must* respect copyright (Copyright, Designs and Patents Act 1988) and attribute sources. - ---- - -## Best Practices (SHOULD Clauses) - -### 9. Transparency and Disclosure - -**Principle:** A journalist *should* disclose funding sources, sponsorships, and potential conflicts of interest. - -**Recommendations:** -- Label advertorials/sponsored content clearly -- Disclose personal financial interests -- Identify PR-sourced material -- Transparency about algorithmic/AI assistance - -**Violations Include:** -- Hidden advertorials -- Undisclosed financial conflicts -- Presenting PR material as independent journalism -- AI-generated content without disclosure - -**Attestation:** *Should* disclose funding and conflicts per NUJ Transparency Guidelines. - ---- - -### 10. Diversity in Sources - -**Principle:** A journalist *should* reflect diverse voices and perspectives in reporting. - -**Recommendations:** -- Gender balance in expert sources -- Ethnic/racial diversity representation -- Include marginalized perspectives -- Avoid "single story" narratives - -**Violations Include:** -- All-male expert panels -- Exclusion of affected communities -- Reliance on elite sources only -- Perpetuating dominant narratives - -**Attestation:** *Should* reflect diversity per NUJ Equality Guidelines. - ---- - -### 11. Environmental Impact - -**Principle:** A journalist *should* consider the environmental impact of reporting and production. - -**Recommendations:** -- Disclose carbon footprint of major productions -- Sustainable travel choices -- Digital-first distribution to reduce print waste -- Environmental story coverage - -**Attestation:** *Should* minimize environmental impact per NUJ Green Journalism Guide. - ---- - -### 12. Accessibility - -**Principle:** A journalist *should* ensure content is accessible to disabled audiences. - -**Recommendations:** -- WCAG 2.1 AA compliance (minimum) -- Captions for video content -- Alt text for images -- Plain language summaries - -**Attestation:** *Should* comply with WCAG 2.1 for digital content. - ---- - -## Advocacy Targets (COULD Clauses) - -### 13. Mental Health Support - -**Principle:** Employers *could* provide mental health resources for journalists covering traumatic events. - -**Recommendations:** -- Counseling access post-trauma coverage -- Mental health days without stigma -- Peer support networks -- Trauma-informed editorial practices - -**Attestation:** *Could* include mental health support per NUJ Wellbeing Guide. - ---- - -### 14. Algorithmic Transparency - -**Principle:** Employers *could* explain AI-driven editorial decisions to journalists. - -**Recommendations:** -- Right to human review of AI recommendations -- Transparency about algorithmic news selection -- AI bias audits -- Journalist override rights - -**Attestation:** *Could* provide algorithmic transparency per NUJ AI Ethics Guide. - ---- - -### 15. Portable Benefits - -**Principle:** Employers *could* support portable benefits for freelance journalists. - -**Recommendations:** -- Health insurance stipends -- Pension contributions -- Training budgets -- Equipment allowances - -**Attestation:** *Could* offer portable benefits per NUJ Freelancer Campaign. - ---- - -## Enforcement and Grievance - -### Union Enforcement - -**Process:** -1. Member alleges NUJ Code violation -2. Union rep validates against this schema -3. Evidence gathering (contract, correspondence, published work) -4. Formal grievance filed with employer -5. ACAS mediation if unresolved -6. Employment tribunal if necessary - -**Automated Checks:** -- `union-policy-parser` validates contracts against this schema -- Flags missing clauses (e.g., source protection, diversity) -- Generates grievance templates for violations - ---- - -## Integration with Employment Contracts - -**Required Contract Clauses:** - -Contracts for NUJ members *must* include: -- § Source protection guarantees -- § Editorial independence provisions -- § Right to refuse unethical assignments -- § Whistleblower protection -- § Corrections policy - -Contracts *should* include: -- § Transparency requirements -- § Diversity commitments -- § Accessibility standards -- § Environmental policies - -Contracts *could* include: -- § Mental health support -- § Algorithmic transparency -- § Portable benefits - -**Validation:** -``` -union-policy-parser validate employment-contract.a2ml \ - --schema nuj-code-of-ethics.a2ml \ - --clause "source-protection" \ - --clause "editorial-independence" -``` - ---- - -@refs: -[1] NUJ Code of Conduct (2024 edition) - https://www.nuj.org.uk/about/nuj-code/ -[2] IPSO Editors' Code of Practice - https://www.ipso.co.uk/editors-code-of-practice/ -[3] Ofcom Broadcasting Code - https://www.ofcom.org.uk/tv-radio-and-on-demand/broadcast-codes/broadcast-code -[4] UK Defamation Act 2013 -[5] GDPR (EU 2016/679) -[6] UK Equality Act 2010 -[7] UK Copyright, Designs and Patents Act 1988 -[8] UK Contempt of Court Act 1981 §10 -[9] WCAG 2.1 Web Content Accessibility Guidelines -[10] NUJ Freelance Fees Guide (2024) -[11] NUJ Transparency Guidelines (2023) -[12] NUJ Equality Guidelines (2022) -[13] NUJ Green Journalism Guide (2020) -[14] NUJ AI Ethics Guide (2024) -[15] NUJ Wellbeing Guide (2021) -@end diff --git a/union-policy-parser/schemas/nuj-pr-comms-guidance.a2ml b/union-policy-parser/schemas/nuj-pr-comms-guidance.a2ml deleted file mode 100644 index c377a85..0000000 --- a/union-policy-parser/schemas/nuj-pr-comms-guidance.a2ml +++ /dev/null @@ -1,478 +0,0 @@ -# NUJ Guidance for PR & Communications Professionals - -@abstract: -This schema defines the National Union of Journalists' ethical standards specifically -for PR and communications professionals. It extends the NUJ Code of Conduct with -PR-specific guidance on transparency, client relationships, media ethics, and the -boundary between journalism and advocacy. -@end - -@requires: -- NUJ Code of Conduct (2024 edition) -- UK Communications Act 2003 -- UK Consumer Protection from Unfair Trading Regulations 2008 -- ASA CAP Code (Advertising Standards) -- CIPR Code of Conduct -- PRCA Professional Charter -- IPSO Editors' Code of Practice -@end - ---- - -## Core Principles (MUST Clauses) - -### 1. Truth and Transparency in PR - -**Principle:** PR professionals *must* be truthful and transparent about their role and client relationships. - -**Requirements:** -- Clear identification as PR/communications professional -- Disclosure of client/employer when placing stories -- No misrepresentation as independent journalist -- Factual accuracy in press releases and statements - -**Prohibited Practices:** -- Posing as independent journalist -- Hiding client identity from media -- "Astroturfing" (fake grassroots campaigns) -- Misleading journalists about story origins - -**Attestation:** *Must* disclose PR role and client relationships per NUJ PR Guidance §2.1. - ---- - -### 2. No Deception of Media - -**Principle:** PR professionals *must not* deceive journalists or media outlets. - -**Requirements:** -- Honest representation of facts -- No fabricated quotes or sources -- No manipulation of images/video -- Corrections issued promptly for errors - -**Prohibited:** -- Fake testimonials -- Staged events presented as spontaneous -- Misleading statistics -- Hidden product placement - -**Attestation:** *Must* maintain honesty with media per NUJ Code §1 (Truth and Accuracy). - ---- - -### 3. Separation from Editorial - -**Principle:** PR content *must* be clearly distinguishable from editorial content. - -**Requirements:** -- Advertorials labeled as "Advertisement" or "Sponsored Content" -- Native advertising clearly disclosed -- No attempts to blend PR with editorial -- Respect for editorial independence - -**Labeling Requirements:** -- "Sponsored by [Client]" -- "Advertisement Feature" -- "Paid Partnership" -- "Promotional Content" - -**Attestation:** *Must* label advertorials per ASA CAP Code and NUJ PR Guidance §3.1. - ---- - -### 4. Anti-Misinformation - -**Principle:** PR professionals *must* include correction policies for false statements. - -**Requirements:** -- Correction policy for client statements -- Prompt correction of errors -- No knowing distribution of false information -- Fact-checking before release - -**Correction Timeframe:** -- Social media: Within 24 hours -- Press releases: Within 48 hours -- Campaigns: Immediate withdrawal if false - -**Attestation:** *Must* include correction policy per NUJ PR Guidance §3.4. - ---- - -### 5. Data Protection in PR - -**Principle:** PR campaigns *must* comply with GDPR for media contacts and subjects. - -**Requirements:** -- Consent for use of personal data -- Right to erasure for press contacts -- No sharing of personal data without consent -- Secure storage of media databases - -**Media Lists:** -- Opt-in only (no bought lists) -- Easy unsubscribe -- Regular data audits -- No GDPR violations - -**Attestation:** *Must* comply with GDPR for PR data per EU 2016/679. - ---- - -### 6. No Bribery or Inducements - -**Principle:** PR professionals *must not* bribe journalists or offer inappropriate inducements. - -**Requirements:** -- No cash payments to journalists -- Gifts within reasonable limits (< £50) -- Transparent hospitality (press trips disclosed) -- No quid-pro-quo arrangements - -**Prohibited:** -- Cash for coverage -- Expensive gifts to journalists -- Hidden sponsorships -- "Pay-for-play" arrangements - -**Attestation:** *Must* avoid bribery per UK Bribery Act 2010 and NUJ Code §2. - ---- - -### 7. Respect for Source Protection - -**Principle:** PR professionals *must* respect journalists' source protection. - -**Requirements:** -- No pressure on journalists to reveal sources -- No retaliation against journalists for protecting sources -- Understanding of NUJ Code §6 (Source Protection) - -**Attestation:** *Must* respect source protection per NUJ Code §6. - ---- - -## Client Relationships (MUST Clauses) - -### 8. Client Confidentiality - -**Principle:** PR professionals *must* maintain client confidentiality (unless illegal). - -**Requirements:** -- Non-disclosure of sensitive client information -- Secure handling of confidential materials -- Exception: Illegal or unethical client activities - -**Exceptions to Confidentiality:** -- Criminal activity -- Public health/safety risks -- Fraud or deception - -**Attestation:** *Must* maintain client confidentiality except where illegal or unethical. - ---- - -### 9. Conflicts of Interest - -**Principle:** PR professionals *must* disclose conflicts of interest. - -**Requirements:** -- Disclosure of personal interests in client's industry -- No representation of competing clients without disclosure -- Transparent about financial relationships - -**Attestation:** *Must* disclose conflicts per NUJ Code §2 (Independence). - ---- - -### 10. Right to Refuse Unethical Clients - -**Principle:** PR professionals *must* have the right to refuse unethical work. - -**Requirements:** -- Right to decline clients engaged in illegal/harmful activities -- No retaliation for ethical refusals -- Whistleblower protection for reporting unethical practices - -**Prohibited Clients (examples):** -- Tobacco companies targeting children -- Climate denial campaigns -- Misinformation campaigns -- Discriminatory organizations - -**Attestation:** *Must* allow refusal of unethical clients per NUJ PR Guidance §4.1. - ---- - -## Best Practices (SHOULD Clauses) - -### 11. Diversity in PR Sources - -**Principle:** PR campaigns *should* reflect diverse voices and perspectives. - -**Recommendations:** -- Gender balance in spokespeople -- BAME representation -- Disability inclusion -- Avoid "manels" (all-male panels) - -**Attestation:** *Should* reflect diversity per NUJ Equality Guide. - ---- - -### 12. Environmental Impact of Campaigns - -**Principle:** PR campaigns *should* disclose environmental impact. - -**Recommendations:** -- Carbon footprint reporting for major campaigns -- Sustainable event planning -- Digital-first to reduce print waste -- No greenwashing - -**Attestation:** *Should* minimize environmental impact per NUJ Green PR Guide. - ---- - -### 13. Accessibility of PR Materials - -**Principle:** PR materials *should* be accessible to disabled audiences. - -**Recommendations:** -- WCAG 2.1 AA compliance for websites -- Captions for videos -- Alt text for images -- Plain language summaries - -**Attestation:** *Should* comply with WCAG 2.1 for digital content. - ---- - -### 14. Fact-Checking Infrastructure - -**Principle:** PR agencies *should* have robust fact-checking processes. - -**Recommendations:** -- Designated fact-checker on staff -- Third-party verification for statistics -- Source documentation for all claims -- Pre-release fact-check for major campaigns - -**Attestation:** *Should* implement fact-checking infrastructure. - ---- - -## Crisis Communications (MUST Clauses) - -### 15. Honesty in Crisis - -**Principle:** Crisis communications *must* prioritize truth over reputation management. - -**Requirements:** -- Acknowledge problems honestly -- No cover-ups or stonewalling -- Transparent about what's known/unknown -- Prompt updates as situation develops - -**Prohibited:** -- Lying to media during crisis -- Delaying bad news -- Blaming victims -- Attacking journalists - -**Attestation:** *Must* maintain honesty during crises per NUJ Code §1. - ---- - -### 16. Victim Sensitivity - -**Principle:** Crisis communications *must* show sensitivity to victims. - -**Requirements:** -- No victim-blaming -- Respectful language -- Offer support/resources -- Privacy protection for victims - -**Attestation:** *Must* treat victims sensitively per NUJ Code §4 (Privacy and Harassment). - ---- - -## Social Media & Digital PR (MUST Clauses) - -### 17. Disclosure on Social Media - -**Principle:** Social media PR *must* disclose paid partnerships and sponsorships. - -**Requirements:** -- #ad or #sponsored hashtags -- Clear disclosure of client relationships -- Transparency about influencer partnerships -- No stealth marketing - -**Platform-Specific:** -- Instagram: "Paid partnership with [Brand]" -- Twitter/X: #ad in first line -- TikTok: Branded content toggle -- LinkedIn: "Sponsored by [Client]" - -**Attestation:** *Must* disclose paid social media per ASA CAP Code and NUJ PR Guidance §5.2. - ---- - -### 18. No Fake Reviews/Testimonials - -**Principle:** PR campaigns *must not* use fake reviews or testimonials. - -**Requirements:** -- All testimonials genuine and verifiable -- No incentivized positive reviews without disclosure -- No suppression of negative reviews -- No astroturfing on review platforms - -**Attestation:** *Must* use genuine testimonials per UK Consumer Protection Regulations 2008. - ---- - -### 19. AI Disclosure in PR - -**Principle:** AI-generated PR content *must* be disclosed. - -**Requirements:** -- Label AI-generated text/images/video -- Human oversight for all AI content -- Disclosure of AI tools used -- No passing off AI as human-created - -**Labeling:** -- "Generated with AI assistance" -- "AI-created image" -- "Human-edited AI draft" - -**Attestation:** *Must* disclose AI-generated content per NUJ AI Ethics Guide §2.1. - ---- - -## Advocacy Targets (COULD Clauses) - -### 20. Mental Health Support for PR Professionals - -**Principle:** Employers *could* provide mental health resources for PR professionals. - -**Recommendations:** -- Counseling for crisis communications stress -- Mental health days without stigma -- Peer support networks -- Trauma-informed practices - -**Attestation:** *Could* include mental health support per NUJ Wellbeing Guide. - ---- - -### 21. Portable Benefits for Freelance PR - -**Principle:** Employers *could* support portable benefits for freelance PR professionals. - -**Recommendations:** -- Health insurance stipends -- Pension contributions -- Equipment allowances -- Training budgets - -**Attestation:** *Could* offer portable benefits per NUJ Freelancer Campaign. - ---- - -## Enforcement and Grievance - -### NUJ PR Branch Enforcement - -**Process:** -1. Member alleges PR ethics violation -2. NUJ PR rep validates against this schema -3. Evidence gathering (press releases, media coverage) -4. Formal complaint to employer or client -5. CIPR/PRCA referral if professional standards violated -6. Employment tribunal if employment rights breached - -**Automated Checks:** -- `union-policy-parser` validates PR contracts/policies -- Flags missing transparency clauses -- Generates complaint templates - ---- - -## Integration with Employment Contracts - -**Required Contract Clauses:** - -PR professional contracts *must* include: -- § Truth and transparency requirements -- § No deception of media -- § Advertorial labeling requirements -- § Anti-misinformation policy -- § GDPR compliance for PR data -- § No bribery/inducements clause -- § Right to refuse unethical clients - -Contracts *should* include: -- § Diversity commitments -- § Environmental impact disclosure -- § Accessibility standards -- § Fact-checking infrastructure - -Contracts *could* include: -- § Mental health support -- § Portable benefits (freelancers) - ---- - -## Boundary Between Journalism and PR - -### Key Distinctions - -**Journalist:** -- Independent editorial judgment -- Serves public interest -- Accountable to readers/viewers -- Protected by source protection - -**PR Professional:** -- Advocates for client -- Serves client interests -- Accountable to client -- Must disclose client relationship - -**Ethical Boundary:** -- NUJ members can work in PR **if**: - - They disclose their role honestly - - They don't pose as independent journalists - - They follow NUJ PR Guidance - - They don't undermine editorial independence - -**Prohibited:** -- NUJ journalist working PR for same outlet (conflict) -- PR professional presenting as journalist without disclosure - -**Attestation:** *Must* maintain clear boundary per NUJ PR Guidance §1.1. - ---- - -@refs: -[1] NUJ Code of Conduct (2024 edition) -[2] NUJ Guidance for PR & Communications (2023) -[3] UK Communications Act 2003 -[4] UK Consumer Protection from Unfair Trading Regulations 2008 -[5] UK Bribery Act 2010 -[6] ASA CAP Code (Advertising Standards) -[7] CIPR Code of Conduct -[8] PRCA Professional Charter -[9] IPSO Editors' Code of Practice -[10] GDPR (EU 2016/679) -[11] WCAG 2.1 (Web Content Accessibility Guidelines) -[12] NUJ AI Ethics Guide (2024) -[13] NUJ Equality Guide (2022) -[14] NUJ Green PR Guide (2023) -[15] NUJ Wellbeing Guide (2021) -@end diff --git a/union-policy-parser/schemas/ucu-academic-standards.a2ml b/union-policy-parser/schemas/ucu-academic-standards.a2ml deleted file mode 100644 index 532230b..0000000 --- a/union-policy-parser/schemas/ucu-academic-standards.a2ml +++ /dev/null @@ -1,370 +0,0 @@ -# UCU Academic Standards and Employment Rights - -@abstract: -This schema defines the University and College Union (UCU) standards for academic -employment contracts in UK higher education. It encodes protections against casualization, -workload limits, academic freedom guarantees, and research/teaching balance requirements. -@end - -@requires: -- UK Employment Rights Act 1996 -- UK Working Time Regulations 1998 -- UK Fixed-term Employees (Prevention of Less Favourable Treatment) Regulations 2002 -- UK Equality Act 2010 -- Education Act 2011 -- UCU Model Contract -- UCU Anti-Casualization Framework -@end - ---- - -## Core Principles (MUST Clauses) - -### 1. Academic Freedom - -**Principle:** Academics *must* have freedom to research, teach, and speak without institutional censorship. - -**Requirements:** -- Freedom to determine research agenda -- Freedom to challenge institutional policies -- Protection from dismissal for lawful speech -- No requirement to obtain pre-approval for research topics - -**Prohibited:** -- Censorship of research findings -- Punishment for criticizing university management -- Forced self-censorship -- Restrictions on conference presentations - -**Legal Protection:** Education Act 2011 Section 26 (academic freedom in HE) - -**Attestation:** *Must* guarantee academic freedom per Education Act 2011 §26. - ---- - -### 2. Workload Limits - -**Principle:** Academic workload *must* be sustainable and not exceed reasonable limits. - -**Requirements:** -- Maximum 40 hours/week averaged over academic year -- Transparent workload allocation model (WAM) -- Annual workload review with line manager -- Right to refuse unreasonable additional duties - -**Workload Components:** -- Teaching (lectures, seminars, tutorials) -- Research (including PhD supervision) -- Administration (committees, meetings) -- Pastoral care (student support) - -**UCU Recommended Allocation:** -- 40% teaching -- 40% research -- 20% administration - -**Attestation:** *Must* specify workload limits ≤ 40 hours/week per UK Working Time Regulations 1998. - ---- - -### 3. No Casualization - -**Principle:** Academics *must not* be exploited through precarious employment. - -**Requirements:** -- Permanent contracts for permanent work -- Fixed-term contracts only for genuinely temporary roles -- Conversion to permanent after 4 years of continuous service -- Pro-rata pay and benefits for part-time staff - -**Prohibited Practices:** -- Rolling short-term contracts for permanent teaching -- Zero-hours contracts -- Unpaid marking/preparation time -- "Voluntary" teaching without compensation - -**Attestation:** *Must* comply with UK Fixed-term Employees Regulations 2002; permanent work requires permanent contracts. - ---- - -### 4. Research Time - -**Principle:** Academics *must* have protected time for research. - -**Requirements:** -- Minimum 0.5 FTE (50% of time) for research for research-active staff -- Research time protected from encroachment by teaching/admin -- Annual research leave entitlement -- Support for conference attendance - -**Research Sabbaticals:** -- 1 semester sabbatical per 6 years of service -- Full pay during sabbatical -- No teaching duties during sabbatical - -**Attestation:** *Must* allocate ≥ 50% time for research per UCU Model Contract. - ---- - -### 5. Teaching Load - -**Principle:** Teaching load *must* be reasonable and allow for effective pedagogy. - -**Requirements:** -- Maximum 18 contact hours/week during term -- Maximum 120 students for marking-intensive courses -- Teaching preparation time allocated (1:2 ratio - 1 hour teaching = 2 hours prep/marking) -- No more than 3 different course preps per year (for new staff) - -**PhD Supervision:** -- Maximum 6 PhD students per supervisor -- PhD supervision counted as 0.1 FTE per student - -**Attestation:** *Must* limit teaching to ≤ 18 contact hours/week per UCU standards. - ---- - -### 6. Fair Pay - -**Principle:** Academics *must* receive fair pay with regular progression. - -**Requirements:** -- Starting salary on appropriate pay spine point -- Annual cost-of-living increases (minimum CPI + 1%) -- Incremental progression through pay spine -- Equal pay for equal work (no gender/race pay gaps) - -**UCU Pay Campaign (2025):** -- Minimum starting salary: £40,000 (Lecturer) -- Annual increment: ≥ 3% -- No unpaid hours - -**Attestation:** *Must* meet UCU minimum pay scales adjusted for 2025 cost of living. - ---- - -### 7. Pension Provision - -**Principle:** Academics *must* have access to defined-benefit pension schemes. - -**Requirements:** -- USS (Universities Superannuation Scheme) or TPS (Teachers' Pension Scheme) -- Employer contributions ≥ 21.4% (USS rate 2025) -- No pension cuts without consultation - -**Attestation:** *Must* provide defined-benefit pension with employer contributions ≥ 21%. - ---- - -## Anti-Casualization (MUST Clauses) - -### 8. No Hourly-Paid Contracts - -**Principle:** Teaching *must not* be paid on an hourly basis without employee rights. - -**Requirements:** -- Fractional contracts (0.2 FTE, 0.4 FTE, etc.) instead of hourly -- Pro-rata holiday pay, sick pay, pension -- Inclusion in department meetings and decision-making - -**Prohibited:** -- "Sessional" or "hourly-paid" lecturer contracts -- Payment per contact hour only (no prep/marking time) -- Exclusion from staff development - -**Attestation:** *Must* provide fractional contracts with full employee rights per UK Fixed-term Employees Regulations 2002. - ---- - -### 9. No Unpaid Labor - -**Principle:** All academic work *must* be compensated. - -**Requirements:** -- Marking time paid -- Preparation time paid -- Attendance at departmental meetings paid (if required) -- No "goodwill" exploitation - -**Attestation:** *Must* compensate all hours worked per UK National Minimum Wage Act 1998. - ---- - -### 10. Right to Permanent Contract - -**Principle:** After 4 years of continuous fixed-term contracts, *must* convert to permanent. - -**Requirements:** -- Automatic conversion at 4-year mark -- No requirement to reapply -- Continuity of service recognized - -**Attestation:** *Must* convert fixed-term to permanent after 4 years per UK Fixed-term Employees Regulations 2002. - ---- - -## Best Practices (SHOULD Clauses) - -### 11. Sabbatical Provision - -**Principle:** Academics *should* have regular sabbatical leave for research. - -**Recommendations:** -- 1 semester per 6 years (UCU standard) -- Full pay during sabbatical -- Research funding support - -**Attestation:** *Should* provide sabbatical leave per UCU Model Contract. - ---- - -### 12. Conference Funding - -**Principle:** Universities *should* fund conference attendance. - -**Recommendations:** -- £1,500/year conference budget per academic -- Travel, accommodation, registration covered -- PhD student conference support - -**Attestation:** *Should* provide annual conference funding. - ---- - -### 13. PhD Supervision Limits - -**Principle:** PhD supervision load *should* be capped. - -**Recommendations:** -- Maximum 6 PhD students per supervisor -- Additional time allocated for first-time supervisors -- Training for PhD supervision - -**Attestation:** *Should* cap PhD supervision at 6 students per UCU guidelines. - ---- - -### 14. Equality and Diversity - -**Principle:** Contracts *should* promote equality and tackle discrimination. - -**Recommendations:** -- Gender pay gap reporting -- BAME representation targets -- Disability accommodations -- Flexible working for carers - -**Attestation:** *Should* comply with UK Equality Act 2010 positive duty provisions. - ---- - -## Advocacy Targets (COULD Clauses) - -### 15. Four-Day Week - -**Principle:** Universities *could* adopt four-day working week. - -**Recommendations:** -- Compressed hours (9-day fortnight) -- Reduced working week with same pay -- Improved work-life balance - -**Attestation:** *Could* pilot four-day week per UCU campaign. - ---- - -### 16. Climate Justice - -**Principle:** Universities *could* adopt climate-conscious employment practices. - -**Recommendations:** -- Carbon budgets for international travel -- Remote work options to reduce commuting -- Funding for sustainable research - -**Attestation:** *Could* implement climate justice measures per UCU Environmental Framework. - ---- - -## Workload Calculation - -**UCU Workload Allocation Model (WAM):** - -**Teaching:** -- 1 hour lecture = 3 hours total (1 delivery + 2 prep/marking) -- 1 hour seminar = 2 hours total (1 delivery + 1 prep) -- 1 PhD student = 0.1 FTE (156 hours/year) - -**Research:** -- Grant application: 40 hours -- Journal article: 80 hours -- Book chapter: 60 hours - -**Administration:** -- Committee membership: 0.05 FTE -- Programme director: 0.2 FTE -- Head of department: 0.4 FTE - -**Total Annual Hours:** 1,560 hours (40 hours/week × 39 weeks) - -**Attestation:** *Must* use transparent workload model per UCU WAM guidelines. - ---- - -## Enforcement and Grievance - -### UCU Enforcement - -**Process:** -1. Member raises concern with UCU rep -2. Informal resolution attempted with line manager -3. Formal grievance if unresolved (ACAS Code of Practice 1) -4. UCU support for employment tribunal if needed -5. Industrial action (strike) if systemic violations - -**Automated Checks:** -- `union-policy-parser` validates academic contracts -- Flags casualization, workload violations -- Generates grievance letters - ---- - -## Integration with Employment Contracts - -**Required Contract Clauses:** - -Academic contracts *must* include: -- § Academic freedom guarantee -- § Workload limits (≤ 40 hours/week) -- § No casualization (permanent work = permanent contract) -- § Protected research time (≥ 50% for research-active) -- § Teaching load limits (≤ 18 contact hours/week) -- § Fair pay (UCU scales) -- § Pension provision (USS/TPS) - -Contracts *should* include: -- § Sabbatical provision -- § Conference funding -- § PhD supervision limits -- § Equality and diversity commitments - -Contracts *could* include: -- § Four-day week pilots -- § Climate justice measures - ---- - -@refs: -[1] UK Employment Rights Act 1996 -[2] UK Working Time Regulations 1998 -[3] UK Fixed-term Employees (Prevention of Less Favourable Treatment) Regulations 2002 -[4] UK Equality Act 2010 -[5] Education Act 2011 Section 26 (Academic Freedom) -[6] UCU Model Contract -[7] UCU Anti-Casualization Framework -[8] UCU Workload Allocation Model (WAM) -[9] UCU Pay Campaign 2025 -[10] USS (Universities Superannuation Scheme) Regulations -[11] ACAS Code of Practice 1: Disciplinary and Grievance Procedures -[12] UCU Environmental Framework -@end diff --git a/union-policy-parser/self-validating/employment-contract.k9.ncl b/union-policy-parser/self-validating/employment-contract.k9.ncl deleted file mode 100644 index 541f562..0000000 --- a/union-policy-parser/self-validating/employment-contract.k9.ncl +++ /dev/null @@ -1,274 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# K9 Self-Validating Employment Contract Wrapper -# Encapsulates A2ML contract with Nickel validation rules - -{ - pedigree = { - name = "employment-contract", - version = "1.0.0", - description = "Self-validating employment contract with union policy compliance", - magic = "K9!", - mime_type = "application/vnd.k9+nickel", - author = "Jonathan D.A. Jewell ", - }, - - target = { - os = ["linux", "darwin", "freebsd"], - edge = { - container = true, - wasm = false, # Future: WASM runtime for browser validation - }, - memory_min_mb = 10, - }, - - security = { - trust_level = 'Hunt, # Requires Ed25519 signature (tamper-proof) - - permissions = { - filesystem = { - read = true, # Read A2ML payload and schemas - write = false, # No modifications to prevent tampering - }, - network = { - outbound = false, # No phone-home; purely local validation - inbound = false, - }, - subprocess = { - spawn = true, # Needed for union-policy-parser invocation - shell = false, # No shell injection risk - }, - }, - }, - - validation = { - # Primary schema for validation - schema = "../schemas/nuj-code-of-ethics.a2ml", - - # Required clauses (must be present in contract) - required_clauses = [ - "employment.terms", - "taxation.ni", - "termination.conditions", - "grievance.procedure", - "confidentiality.obligations", - ], - - # Union-specific clauses (NUJ/IWW/UCU) - union_clauses = { - nuj = [ - "source-protection", - "editorial-independence", - "right-of-reply", - "copyright-retention", - ], - iww = [ - "payment-terms", - "late-payment-penalty", - "collective-voice", - ], - ucu = [ - "academic-freedom", - "workload-limits", - "research-time", - ], - }, - - # SHA256 checksum of A2ML payload (integrity) - checksum_algo = "sha256", - checksum | optional = null, # Computed during packaging - - # Signature verification - signature | optional = null, # Ed25519 signature - signed_by | optional = null, # Union rep's public key fingerprint - }, - - contracts = { - # Nickel contract: days-contracted must be positive integer - days_contracted = fun contract => - let days = contract.employment.terms.days_contracted in - std.contract.from_predicate (fun d => d > 0 && d <= 7), - - # Nickel contract: daily rate must meet minimum wage - daily_rate_minimum = fun contract => - let rate = contract.remuneration.daily_rate in - let min_wage_daily = 98.60 in # UK NMW 2025: £13.15/hr * 7.5hr - std.contract.from_predicate (fun r => r >= min_wage_daily), - - # Nickel contract: notice period must comply with UK law - notice_period_legal = fun contract => - let notice_months = contract.termination.conditions.notice_period in - std.contract.from_predicate (fun n => n >= 1), # ERA 1996: min 1 month - - # Nickel contract: ACAS grievance procedure required - grievance_acas = fun contract => - let procedure = contract.grievance.procedure.formal_procedure in - std.contract.from_predicate (fun p => - std.string.contains "ACAS" p || std.string.contains "acas" p - ), - }, - - recipes = { - # Install: No-op for contracts (already in place) - install = m%" - echo "✅ Employment contract ready for validation" - echo " Payload: ${self.payload_file}" - echo " Schema: ${self.validation.schema}" - "%, - - # Validate: Check contract against union schemas - validate = m%" - set -e - echo "🔍 Validating employment contract..." - - # Check if union-policy-parser is installed - if ! command -v union-policy-parser &> /dev/null; then - echo "❌ union-policy-parser not found" - echo " Install: cargo install --git https://github.com/hyperpolymath/union-policy-parser" - exit 1 - fi - - # Validate A2ML structure - union-policy-parser validate ${self.payload_file} \ - --schema ${self.validation.schema} \ - --mode attested \ - --required-clauses "${std.string.join "," self.validation.required_clauses}" - - # Verify checksum if present - if [ -n "${self.validation.checksum}" ]; then - echo "🔐 Verifying payload integrity..." - computed=$(sha256sum ${self.payload_file} | cut -d' ' -f1) - if [ "$computed" != "${self.validation.checksum}" ]; then - echo "❌ Checksum mismatch - contract may be tampered" - exit 1 - fi - echo "✅ Payload integrity verified" - fi - - echo "✅ Contract validation successful" - "%, - - # Audit: Generate detailed compliance report - audit = m%" - set -e - echo "📊 Generating compliance audit report..." - - union-policy-parser audit ${self.payload_file} \ - --schema ${self.validation.schema} \ - --output /tmp/contract-audit-$(date +%Y%m%d-%H%M%S).json \ - --format json - - echo "✅ Audit report saved to /tmp/" - "%, - - # Sign: Union rep signs contract with Ed25519 - sign = m%" - set -e - - if [ ! -f ~/.ssh/union-rep-ed25519 ]; then - echo "❌ Union rep signing key not found" - echo " Generate: ssh-keygen -t ed25519 -f ~/.ssh/union-rep-ed25519" - exit 1 - fi - - echo "🔏 Signing contract with union rep key..." - - # Compute checksum - checksum=$(sha256sum ${self.payload_file} | cut -d' ' -f1) - - # Sign checksum - signature=$(echo -n "$checksum" | ssh-keygen -Y sign -f ~/.ssh/union-rep-ed25519 -n contract | base64 -w0) - - echo "✅ Contract signed" - echo " Signature: $signature" - echo " Checksum: $checksum" - - # Update K9 metadata (requires editing this file) - echo "" - echo "⚠️ Manual step: Update this file with:" - echo " validation.checksum = \"$checksum\"" - echo " validation.signature = \"$signature\"" - echo " validation.signed_by = \"$(ssh-keygen -lf ~/.ssh/union-rep-ed25519.pub | awk '{print $2}')\"" - "%, - - # Verify: Check Ed25519 signature - verify = m%" - set -e - - if [ -z "${self.validation.signature}" ]; then - echo "⚠️ Contract not signed - cannot verify" - exit 0 - fi - - echo "🔍 Verifying contract signature..." - - # Compute checksum - computed=$(sha256sum ${self.payload_file} | cut -d' ' -f1) - - # Verify against stored checksum - if [ "$computed" != "${self.validation.checksum}" ]; then - echo "❌ Checksum mismatch - contract tampered after signing" - exit 1 - fi - - # Verify Ed25519 signature (requires union rep's public key) - # Note: ssh-keygen -Y verify requires allowed_signers file - echo "✅ Checksum verified" - echo " Signed by: ${self.validation.signed_by}" - - echo "" - echo "⚠️ Full signature verification requires union rep's public key" - echo " Add to ~/.ssh/allowed_signers:" - echo " contract ${self.validation.signed_by}" - "%, - - # Deploy: Copy validated contract to approved location - deploy = m%" - set -e - - # Validate first - just -f $0 validate - - # Check signature if present - if [ -n "${self.validation.signature}" ]; then - just -f $0 verify - fi - - # Deploy to approved contracts directory - deploy_dir="/var/union-contracts/validated" - mkdir -p "$deploy_dir" - - contract_id=$(basename ${self.payload_file} .a2ml) - timestamp=$(date +%Y%m%d-%H%M%S) - - cp ${self.payload_file} "$deploy_dir/${contract_id}-${timestamp}.a2ml" - - echo "✅ Contract deployed to $deploy_dir/" - echo " File: ${contract_id}-${timestamp}.a2ml" - "%, - - # Grievance: Auto-generate grievance for violations - grievance = m%" - set -e - - echo "⚖️ Checking for contract violations..." - - # Run validation and capture failures - if ! union-policy-parser validate ${self.payload_file} --schema ${self.validation.schema} 2>&1; then - echo "❌ Violations detected - generating grievance..." - - union-policy-parser grievance ${self.payload_file} \ - --schema ${self.validation.schema} \ - --violation auto-detect \ - --template ../templates/nuj-grievance.md \ - --output /tmp/grievance-$(date +%Y%m%d-%H%M%S).md - - echo "✅ Grievance letter generated in /tmp/" - else - echo "✅ No violations - grievance not needed" - fi - "%, - }, - - # A2ML payload file (relative path) - payload_file = "../examples/ou-day-employee-contract.a2ml", -} diff --git a/union-policy-parser/self-validating/freelance-journalist-contract.k9.ncl b/union-policy-parser/self-validating/freelance-journalist-contract.k9.ncl deleted file mode 100644 index e3877a5..0000000 --- a/union-policy-parser/self-validating/freelance-journalist-contract.k9.ncl +++ /dev/null @@ -1,294 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# K9 Self-Validating Freelance Journalist Contract -# NUJ-specific validation for freelance journalism contracts - -{ - pedigree = { - name = "freelance-journalist-contract", - version = "1.0.0", - description = "Self-validating freelance journalism contract with NUJ ethics compliance", - magic = "K9!", - mime_type = "application/vnd.k9+nickel", - author = "Jonathan D.A. Jewell ", - }, - - target = { - os = ["linux", "darwin", "freebsd"], - edge = { - container = true, - wasm = false, - }, - memory_min_mb = 10, - }, - - security = { - trust_level = 'Hunt, # Requires signature for deployment - - permissions = { - filesystem = { read = true, write = false }, - network = { outbound = false, inbound = false }, - subprocess = { spawn = true, shell = false }, - }, - }, - - validation = { - schema = "../schemas/nuj-code-of-ethics.a2ml", - - # Critical clauses for freelance journalists - required_clauses = [ - # NUJ Code of Ethics (MUST) - "source-protection", - "editorial-independence", - "right-of-reply", - "copyright-retention", - "truth-accuracy", - "fairness", - "privacy-harassment", - - # Freelancer-specific (MUST) - "payment-terms", - "expenses-reimbursement", - "termination-notice", - "ip-ownership", - - # Anti-exploitation (MUST) - "no-free-trials", - "no-spec-work", - "kill-fee-provision", - ], - - # Recommended clauses (SHOULD) - recommended_clauses = [ - "late-payment-penalty", - "portable-benefits", - "equipment-allowance", - "training-budget", - "mental-health-support", - ], - - checksum | optional = null, - signature | optional = null, - signed_by | optional = null, - }, - - contracts = { - # Payment terms: Must specify NET 30 or better - payment_terms_reasonable = fun contract => - let net_days = contract.payment.terms.net_days in - std.contract.from_predicate (fun d => d <= 30), - - # Late payment penalty: Minimum 5% after due date - late_penalty_minimum = fun contract => - let penalty_pct = contract.payment.late_penalty_percent in - std.contract.from_predicate (fun p => p >= 5.0), - - # Kill fee: Minimum 50% if article killed - kill_fee_minimum = fun contract => - let kill_fee_pct = contract.payment.kill_fee_percent in - std.contract.from_predicate (fun p => p >= 50.0), - - # Copyright: Freelancer retains or has first publication rights only - copyright_freelancer_friendly = fun contract => - let ownership = contract.ip_rights.copyright_ownership in - std.contract.from_predicate (fun o => - o == "freelancer" || o == "first-publication-only" - ), - - # Day rate: Must meet NUJ minimum - day_rate_nuj_minimum = fun contract => - let rate = contract.payment.day_rate_gbp in - let nuj_min_2025 = 350.00 in # NUJ Freelance Fees Guide 2025 - std.contract.from_predicate (fun r => r >= nuj_min_2025), - }, - - recipes = { - install = m%" - echo "✅ Freelance journalist contract ready" - echo " Payload: ${self.payload_file}" - echo " NUJ Schema: ${self.validation.schema}" - echo "" - echo "🔍 Key checks:" - echo " - Source protection guaranteed" - echo " - Editorial independence" - echo " - Fair payment terms (NET 30 max)" - echo " - Copyright retention" - echo " - Kill fee provision" - "%, - - validate = m%" - set -e - echo "🔍 Validating freelance contract (NUJ standards)..." - - # Check NUJ-specific clauses - union-policy-parser validate ${self.payload_file} \ - --schema ${self.validation.schema} \ - --mode attested \ - --union nuj \ - --required-clauses "${std.string.join "," self.validation.required_clauses}" - - # Check payment terms - echo "" - echo "💰 Payment terms check:" - union-policy-parser check-clause ${self.payload_file} \ - --clause "payment-terms.net-days" \ - --max 30 \ - --error-if-exceeds - - union-policy-parser check-clause ${self.payload_file} \ - --clause "payment.late-penalty-percent" \ - --min 5.0 \ - --warn-if-below - - # Check copyright - echo "" - echo "©️ Copyright check:" - union-policy-parser check-clause ${self.payload_file} \ - --clause "ip-rights.copyright-ownership" \ - --allowed "freelancer,first-publication-only" \ - --error-if-not - - echo "" - echo "✅ Freelance contract validates against NUJ standards" - "%, - - audit = m%" - set -e - echo "📊 NUJ Freelance Contract Audit" - echo "================================" - - union-policy-parser audit ${self.payload_file} \ - --schema ${self.validation.schema} \ - --union nuj \ - --check-exploitative-clauses \ - --check-nuj-minimums \ - --output /tmp/freelance-audit-$(date +%Y%m%d-%H%M%S).json - - echo "" - echo "Audit saved to /tmp/" - "%, - - # NUJ-specific: Check for exploitative clauses - check_exploitation = m%" - set -e - echo "⚠️ Checking for exploitative clauses..." - - # Red flags for freelancers - red_flags=$(union-policy-parser scan-red-flags ${self.payload_file} \ - --patterns \ - "all rights" \ - "work for hire" \ - "perpetual license" \ - "free trial" \ - "spec work" \ - "unpaid" \ - "no kill fee" \ - "payment on publication" \ - --case-insensitive) - - if [ -n "$red_flags" ]; then - echo "❌ EXPLOITATIVE CLAUSES DETECTED:" - echo "$red_flags" - echo "" - echo "⚠️ DO NOT SIGN THIS CONTRACT" - echo " Contact NUJ rep: freelance@nuj.org.uk" - exit 1 - else - echo "✅ No obvious exploitative clauses found" - fi - "%, - - # Auto-negotiate: Suggest improvements - negotiate = m%" - set -e - echo "💼 Contract Negotiation Suggestions" - echo "====================================" - - union-policy-parser negotiate ${self.payload_file} \ - --schema ${self.validation.schema} \ - --union nuj \ - --output /tmp/negotiation-points-$(date +%Y%m%d-%H%M%S).md - - echo "" - echo "📄 Negotiation points saved to /tmp/" - echo "" - echo "Common improvements for freelancers:" - echo " 1. Reduce NET days from 60 → 30" - echo " 2. Add late payment penalty (5-10%)" - echo " 3. Increase kill fee to 50-100%" - echo " 4. Retain copyright (or first-pub-only)" - echo " 5. Add expenses reimbursement clause" - "%, - - sign = m%" - set -e - - # Validate first - just -f $0 validate - - # Check exploitation - just -f $0 check_exploitation - - echo "" - echo "🔏 Signing freelance contract..." - - # NUJ rep signs to certify compliance - if [ ! -f ~/.ssh/nuj-freelance-rep-ed25519 ]; then - echo "❌ NUJ freelance rep key not found" - exit 1 - fi - - checksum=$(sha256sum ${self.payload_file} | cut -d' ' -f1) - signature=$(echo -n "$checksum" | ssh-keygen -Y sign -f ~/.ssh/nuj-freelance-rep-ed25519 -n freelance-contract | base64 -w0) - - echo "✅ Contract certified by NUJ freelance rep" - echo " Checksum: $checksum" - echo " Signature: $signature" - "%, - - deploy = m%" - set -e - - # Full validation pipeline - just -f $0 validate - just -f $0 check_exploitation - - if [ -n "${self.validation.signature}" ]; then - just -f $0 verify - fi - - deploy_dir="/var/nuj-contracts/freelance" - mkdir -p "$deploy_dir" - - contract_id=$(basename ${self.payload_file} .a2ml) - timestamp=$(date +%Y%m%d-%H%M%S) - - cp ${self.payload_file} "$deploy_dir/${contract_id}-${timestamp}.a2ml" - - echo "✅ Freelance contract deployed" - echo " Safe to sign and return to employer" - "%, - - grievance = m%" - set -e - - echo "⚖️ Checking NUJ Code violations..." - - if ! union-policy-parser validate ${self.payload_file} --schema ${self.validation.schema} 2>&1; then - echo "❌ NUJ Code violations detected" - - union-policy-parser grievance ${self.payload_file} \ - --schema ${self.validation.schema} \ - --union nuj \ - --template ../templates/nuj-freelance-grievance.md \ - --output /tmp/grievance-freelance-$(date +%Y%m%d-%H%M%S).md - - echo "" - echo "📧 Contact NUJ Freelance Office:" - echo " Email: freelance@nuj.org.uk" - echo " Phone: +44 (0)20 7843 3700" - fi - "%, - }, - - payload_file = "../examples/freelance-journalist-contract.a2ml", -} diff --git a/union-policy-parser/self-validating/nuj-editorial-policy.k9.ncl b/union-policy-parser/self-validating/nuj-editorial-policy.k9.ncl deleted file mode 100644 index c2ff2c5..0000000 --- a/union-policy-parser/self-validating/nuj-editorial-policy.k9.ncl +++ /dev/null @@ -1,359 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# K9 Self-Validating Editorial Policy -# For newsrooms/media organizations implementing NUJ ethical standards - -{ - pedigree = { - name = "nuj-editorial-policy", - version = "1.0.0", - description = "Self-validating editorial policy with NUJ Code compliance", - magic = "K9!", - mime_type = "application/vnd.k9+nickel", - author = "Jonathan D.A. Jewell ", - }, - - target = { - os = ["linux", "darwin", "freebsd"], - edge = { - container = true, - wasm = true, # Can run in browser for public transparency - }, - memory_min_mb = 10, - }, - - security = { - trust_level = 'Yard, # Public-facing, read-only validation - - permissions = { - filesystem = { read = true, write = false }, - network = { outbound = false, inbound = false }, - subprocess = { spawn = false, shell = false }, # Pure validation only - }, - }, - - validation = { - schema = "../schemas/nuj-code-of-ethics.a2ml", - - # Core NUJ editorial principles (MUST) - required_clauses = [ - # Truth & Accuracy - "corrections-policy", - "fact-checking-process", - "sources-verification", - - # Independence - "editorial-independence-guarantee", - "conflicts-of-interest-policy", - "gifts-policy", - "advertiser-separation", - - # Fairness - "right-of-reply-procedure", - "complaints-handling", - "impartiality-standards", - - # Privacy - "privacy-respect-policy", - "public-interest-criteria", - "vulnerable-subjects-protection", - - # Accountability - "transparency-about-errors", - "public-editor-contact", - "editorial-standards-contact", - - # Source Protection - "source-protection-guarantee", - "secure-communications-policy", - - # Discrimination - "anti-discrimination-policy", - "diversity-style-guide", - "hate-speech-policy", - - # Plagiarism - "attribution-requirements", - "copyright-compliance", - ], - - # Best practices (SHOULD) - recommended_clauses = [ - "transparency-funding-sources", - "ai-disclosure-policy", - "diversity-reporting-standards", - "accessibility-standards", - "environmental-policy", - ], - - checksum | optional = null, - signature | optional = null, - signed_by | optional = null, # Newsroom editor's signature - }, - - contracts = { - # Corrections must be prominent (not buried) - corrections_prominent = fun policy => - let prominence = policy.corrections.prominence_level in - std.contract.from_predicate (fun p => - p == "same-position" || p == "front-page" || p == "top-of-site" - ), - - # Right of reply must be timely (≤72 hours) - reply_timely = fun policy => - let hours = policy.fairness.right_of_reply_hours_max in - std.contract.from_predicate (fun h => h <= 72), - - # Source protection: no forced disclosure - source_protection_absolute = fun policy => - let forced_disclosure = policy.source_protection.forced_disclosure_allowed in - std.contract.from_predicate (fun f => f == false), - - # AI disclosure: Must label AI-generated content - ai_disclosure_required = fun policy => - let disclosure = policy.transparency.ai_disclosure_required in - std.contract.from_predicate (fun d => d == true), - - # Advertiser separation: Clear firewall - advertiser_firewall = fun policy => - let firewall = policy.independence.advertiser_editorial_firewall in - std.contract.from_predicate (fun f => f == true), - }, - - recipes = { - install = m%" - echo "✅ NUJ Editorial Policy loaded" - echo " Payload: ${self.payload_file}" - echo " Schema: ${self.validation.schema}" - echo "" - echo "📰 Editorial standards enforced:" - echo " ✓ Truth & Accuracy (corrections, fact-checking)" - echo " ✓ Editorial Independence (advertiser firewall)" - echo " ✓ Fairness (right of reply)" - echo " ✓ Privacy (public interest test)" - echo " ✓ Source Protection (no forced disclosure)" - echo " ✓ Anti-Discrimination" - "%, - - validate = m%" - set -e - echo "🔍 Validating editorial policy (NUJ Code)..." - - union-policy-parser validate ${self.payload_file} \ - --schema ${self.validation.schema} \ - --mode attested \ - --union nuj \ - --policy-type editorial \ - --required-clauses "${std.string.join "," self.validation.required_clauses}" - - # Check critical safeguards - echo "" - echo "🛡️ Checking critical safeguards:" - - # Source protection - union-policy-parser check-clause ${self.payload_file} \ - --clause "source-protection.forced-disclosure-allowed" \ - --expected false \ - --error-if-not - - # Advertiser firewall - union-policy-parser check-clause ${self.payload_file} \ - --clause "independence.advertiser-editorial-firewall" \ - --expected true \ - --error-if-not - - # Right of reply timeliness - union-policy-parser check-clause ${self.payload_file} \ - --clause "fairness.right-of-reply-hours-max" \ - --max 72 \ - --warn-if-exceeds - - echo "" - echo "✅ Editorial policy complies with NUJ Code" - "%, - - audit = m%" - set -e - echo "📊 NUJ Editorial Policy Audit" - echo "==============================" - echo "" - - union-policy-parser audit ${self.payload_file} \ - --schema ${self.validation.schema} \ - --union nuj \ - --check-nuj-code-alignment \ - --check-ipso-compliance \ - --check-ofcom-compliance \ - --output /tmp/editorial-audit-$(date +%Y%m%d-%H%M%S).json - - echo "" - echo "Audit covers:" - echo " - NUJ Code of Conduct compliance" - echo " - IPSO Editors' Code alignment" - echo " - Ofcom Broadcasting Code (if applicable)" - echo "" - echo "Report saved to /tmp/" - "%, - - # Public transparency: Publish policy for readers - publish = m%" - set -e - - # Validate first - just -f $0 validate - - echo "" - echo "📢 Publishing editorial policy for public transparency..." - - # Render to HTML - union-policy-parser render ${self.payload_file} \ - --format html \ - --template ../templates/editorial-policy-public.html \ - --output /var/www/about/editorial-standards.html - - # Render to plain text - union-policy-parser render ${self.payload_file} \ - --format markdown \ - --output /var/www/about/editorial-standards.md - - echo "" - echo "✅ Editorial policy published:" - echo " HTML: /var/www/about/editorial-standards.html" - echo " MD: /var/www/about/editorial-standards.md" - echo "" - echo "🔗 Make accessible at:" - echo " https://example.com/about/editorial-standards" - "%, - - # AI ethics check: Ensure AI disclosure - check_ai_ethics = m%" - set -e - echo "🤖 Checking AI ethics compliance..." - - # Does policy require AI disclosure? - ai_disclosure=$(union-policy-parser get-clause ${self.payload_file} \ - --clause "transparency.ai-disclosure-required") - - if [ "$ai_disclosure" != "true" ]; then - echo "❌ AI disclosure not required - violates NUJ AI Ethics" - echo "" - echo "⚠️ NUJ AI Ethics Guide (2024) requires:" - echo " - Disclosure of AI-generated content" - echo " - Human review for all AI suggestions" - echo " - No AI replacement of journalists" - exit 1 - fi - - # Does policy protect jobs from AI? - ai_replacement=$(union-policy-parser get-clause ${self.payload_file} \ - --clause "ai-policy.journalist-replacement-allowed") - - if [ "$ai_replacement" == "true" ]; then - echo "❌ Policy allows AI replacement of journalists" - echo " Violates NUJ AI Ethics (job protection)" - exit 1 - fi - - echo "✅ AI ethics compliant" - "%, - - # Diversity audit - check_diversity = m%" - set -e - echo "🌍 Checking diversity commitments..." - - union-policy-parser audit-diversity ${self.payload_file} \ - --check-source-diversity \ - --check-representation-standards \ - --check-style-guide-inclusive \ - --output /tmp/diversity-audit-$(date +%Y%m%d-%H%M%S).json - - echo "" - echo "Diversity audit complete (see /tmp/)" - echo "" - echo "NUJ Equality Guidelines checklist:" - echo " - Gender balance in expert sources" - echo " - BAME representation" - echo " - LGBTQ+ inclusion" - echo " - Disability representation" - echo " - Age diversity" - "%, - - sign = m%" - set -e - - # Validate first - just -f $0 validate - just -f $0 check_ai_ethics - just -f $0 check_diversity - - echo "" - echo "🔏 Signing editorial policy..." - - # Editor signs to certify NUJ compliance - if [ ! -f ~/.ssh/editor-ed25519 ]; then - echo "❌ Editor signing key not found" - exit 1 - fi - - checksum=$(sha256sum ${self.payload_file} | cut -d' ' -f1) - signature=$(echo -n "$checksum" | ssh-keygen -Y sign -f ~/.ssh/editor-ed25519 -n editorial-policy | base64 -w0) - - echo "✅ Editorial policy certified" - echo " Signed by: Editor-in-Chief" - echo " Checksum: $checksum" - echo "" - echo "⚠️ Update this file with:" - echo " validation.checksum = \"$checksum\"" - echo " validation.signature = \"$signature\"" - "%, - - deploy = m%" - set -e - - # Full validation pipeline - just -f $0 validate - just -f $0 check_ai_ethics - just -f $0 check_diversity - - # Publish for transparency - just -f $0 publish - - # Archive - policy_dir="/var/newsroom/editorial-policies" - mkdir -p "$policy_dir" - - timestamp=$(date +%Y%m%d-%H%M%S) - cp ${self.payload_file} "$policy_dir/policy-${timestamp}.a2ml" - - echo "" - echo "✅ Editorial policy deployed" - echo " Staff can access at /var/newsroom/editorial-policies/" - echo " Public can access at example.com/about/editorial-standards" - "%, - - # Staff training: Generate quiz - training_quiz = m%" - set -e - echo "📚 Generating NUJ Code training quiz..." - - union-policy-parser generate-quiz ${self.payload_file} \ - --schema ${self.validation.schema} \ - --questions 20 \ - --format interactive \ - --output /var/newsroom/training/nuj-code-quiz.html - - echo "" - echo "✅ Training quiz generated" - echo " Location: /var/newsroom/training/nuj-code-quiz.html" - echo "" - echo "Quiz covers:" - echo " - Source protection scenarios" - echo " - Right of reply procedures" - echo " - Privacy vs public interest" - echo " - Conflicts of interest" - echo " - Corrections policy" - "%, - }, - - payload_file = "../examples/nuj-editorial-policy.a2ml", -} diff --git a/union-policy-parser/src/abi/Foreign.idr b/union-policy-parser/src/abi/Foreign.idr deleted file mode 100644 index 1a5e5dd..0000000 --- a/union-policy-parser/src/abi/Foreign.idr +++ /dev/null @@ -1,218 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- Copyright (c) Jonathan D.A. Jewell -||| Foreign Function Interface Declarations -||| -||| This module declares all C-compatible functions that will be -||| implemented in the Zig FFI layer. -||| -||| All functions are declared here with type signatures and safety proofs. -||| Implementations live in ffi/zig/ - -module {{PROJECT}}.ABI.Foreign - -import {{PROJECT}}.ABI.Types -import {{PROJECT}}.ABI.Layout - -%default total - --------------------------------------------------------------------------------- --- Library Lifecycle --------------------------------------------------------------------------------- - -||| Initialize the library -||| Returns a handle to the library instance, or Nothing on failure -export -%foreign "C:{{project}}_init, lib{{project}}" -prim__init : PrimIO Bits64 - -||| Safe wrapper for library initialization -export -init : IO (Maybe Handle) -init = do - ptr <- primIO prim__init - pure (createHandle ptr) - -||| Clean up library resources -export -%foreign "C:{{project}}_free, lib{{project}}" -prim__free : Bits64 -> PrimIO () - -||| Safe wrapper for cleanup -export -free : Handle -> IO () -free h = primIO (prim__free (handlePtr h)) - --------------------------------------------------------------------------------- --- Core Operations --------------------------------------------------------------------------------- - -||| Example operation: process data -export -%foreign "C:{{project}}_process, lib{{project}}" -prim__process : Bits64 -> Bits32 -> PrimIO Bits32 - -||| Safe wrapper with error handling -export -process : Handle -> Bits32 -> IO (Either Result Bits32) -process h input = do - result <- primIO (prim__process (handlePtr h) input) - pure $ case result of - 0 => Left Error - n => Right n - --------------------------------------------------------------------------------- --- String Operations --------------------------------------------------------------------------------- - -||| Convert C string to Idris String -export -%foreign "support:idris2_getString, libidris2_support" -prim__getString : Bits64 -> String - -||| Free C string -export -%foreign "C:{{project}}_free_string, lib{{project}}" -prim__freeString : Bits64 -> PrimIO () - -||| Get string result from library -export -%foreign "C:{{project}}_get_string, lib{{project}}" -prim__getResult : Bits64 -> PrimIO Bits64 - -||| Safe string getter -export -getString : Handle -> IO (Maybe String) -getString h = do - ptr <- primIO (prim__getResult (handlePtr h)) - if ptr == 0 - then pure Nothing - else do - let str = prim__getString ptr - primIO (prim__freeString ptr) - pure (Just str) - --------------------------------------------------------------------------------- --- Array/Buffer Operations --------------------------------------------------------------------------------- - -||| Process array data -export -%foreign "C:{{project}}_process_array, lib{{project}}" -prim__processArray : Bits64 -> Bits64 -> Bits32 -> PrimIO Bits32 - -||| Safe array processor -export -processArray : Handle -> (buffer : Bits64) -> (len : Bits32) -> IO (Either Result ()) -processArray h buf len = do - result <- primIO (prim__processArray (handlePtr h) buf len) - pure $ case resultFromInt result of - Just Ok => Right () - Just err => Left err - Nothing => Left Error - where - resultFromInt : Bits32 -> Maybe Result - resultFromInt 0 = Just Ok - resultFromInt 1 = Just Error - resultFromInt 2 = Just InvalidParam - resultFromInt 3 = Just OutOfMemory - resultFromInt 4 = Just NullPointer - resultFromInt _ = Nothing - --------------------------------------------------------------------------------- --- Error Handling --------------------------------------------------------------------------------- - -||| Get last error message -export -%foreign "C:{{project}}_last_error, lib{{project}}" -prim__lastError : PrimIO Bits64 - -||| Retrieve last error as string -export -lastError : IO (Maybe String) -lastError = do - ptr <- primIO prim__lastError - if ptr == 0 - then pure Nothing - else pure (Just (prim__getString ptr)) - -||| Get error description for result code -export -errorDescription : Result -> String -errorDescription Ok = "Success" -errorDescription Error = "Generic error" -errorDescription InvalidParam = "Invalid parameter" -errorDescription OutOfMemory = "Out of memory" -errorDescription NullPointer = "Null pointer" - --------------------------------------------------------------------------------- --- Version Information --------------------------------------------------------------------------------- - -||| Get library version -export -%foreign "C:{{project}}_version, lib{{project}}" -prim__version : PrimIO Bits64 - -||| Get version as string -export -version : IO String -version = do - ptr <- primIO prim__version - pure (prim__getString ptr) - -||| Get library build info -export -%foreign "C:{{project}}_build_info, lib{{project}}" -prim__buildInfo : PrimIO Bits64 - -||| Get build information -export -buildInfo : IO String -buildInfo = do - ptr <- primIO prim__buildInfo - pure (prim__getString ptr) - --------------------------------------------------------------------------------- --- Callback Support --------------------------------------------------------------------------------- - -||| Callback function type (C ABI) -public export -Callback : Type -Callback = Bits64 -> Bits32 -> Bits32 - -||| Register a callback -export -%foreign "C:{{project}}_register_callback, lib{{project}}" -prim__registerCallback : Bits64 -> AnyPtr -> PrimIO Bits32 - -||| Safe callback registration -export -registerCallback : Handle -> Callback -> IO (Either Result ()) -registerCallback h cb = do - result <- primIO (prim__registerCallback (handlePtr h) (cast cb)) - pure $ case resultFromInt result of - Just Ok => Right () - Just err => Left err - Nothing => Left Error - where - resultFromInt : Bits32 -> Maybe Result - resultFromInt 0 = Just Ok - resultFromInt _ = Just Error - --------------------------------------------------------------------------------- --- Utility Functions --------------------------------------------------------------------------------- - -||| Check if library is initialized -export -%foreign "C:{{project}}_is_initialized, lib{{project}}" -prim__isInitialized : Bits64 -> PrimIO Bits32 - -||| Check initialization status -export -isInitialized : Handle -> IO Bool -isInitialized h = do - result <- primIO (prim__isInitialized (handlePtr h)) - pure (result /= 0) diff --git a/union-policy-parser/src/abi/Layout.idr b/union-policy-parser/src/abi/Layout.idr deleted file mode 100644 index a77341e..0000000 --- a/union-policy-parser/src/abi/Layout.idr +++ /dev/null @@ -1,178 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- Copyright (c) Jonathan D.A. Jewell -||| Memory Layout Proofs -||| -||| This module provides formal proofs about memory layout, alignment, -||| and padding for C-compatible structs. -||| -||| @see https://en.wikipedia.org/wiki/Data_structure_alignment - -module {{PROJECT}}.ABI.Layout - -import {{PROJECT}}.ABI.Types -import Data.Vect -import Data.So - -%default total - --------------------------------------------------------------------------------- --- Alignment Utilities --------------------------------------------------------------------------------- - -||| Calculate padding needed for alignment -public export -paddingFor : (offset : Nat) -> (alignment : Nat) -> Nat -paddingFor offset alignment = - if offset `mod` alignment == 0 - then 0 - else alignment - (offset `mod` alignment) - -||| Proof that alignment divides aligned size -public export -data Divides : Nat -> Nat -> Type where - DivideBy : (k : Nat) -> {n : Nat} -> {m : Nat} -> (m = k * n) -> Divides n m - -||| Round up to next alignment boundary -public export -alignUp : (size : Nat) -> (alignment : Nat) -> Nat -alignUp size alignment = - size + paddingFor size alignment - -||| Proof that alignUp produces aligned result -public export -alignUpCorrect : (size : Nat) -> (align : Nat) -> (align > 0) -> Divides align (alignUp size align) -alignUpCorrect size align prf = - -- Proof that (size + padding) is divisible by align - DivideBy ((size + paddingFor size align) `div` align) Refl - --------------------------------------------------------------------------------- --- Struct Field Layout --------------------------------------------------------------------------------- - -||| A field in a struct with its offset and size -public export -record Field where - constructor MkField - name : String - offset : Nat - size : Nat - alignment : Nat - -||| Calculate the offset of the next field -public export -nextFieldOffset : Field -> Nat -nextFieldOffset f = alignUp (f.offset + f.size) f.alignment - -||| A struct layout is a list of fields with proofs -public export -record StructLayout where - constructor MkStructLayout - fields : Vect n Field - totalSize : Nat - alignment : Nat - {auto 0 sizeCorrect : So (totalSize >= sum (map (\f => f.size) fields))} - {auto 0 aligned : Divides alignment totalSize} - -||| Calculate total struct size with padding -public export -calcStructSize : Vect n Field -> Nat -> Nat -calcStructSize [] align = 0 -calcStructSize (f :: fs) align = - let lastOffset = foldl (\acc, field => nextFieldOffset field) f.offset fs - lastSize = foldr (\field, _ => field.size) f.size fs - in alignUp (lastOffset + lastSize) align - -||| Proof that field offsets are correctly aligned -public export -data FieldsAligned : Vect n Field -> Type where - NoFields : FieldsAligned [] - ConsField : - (f : Field) -> - (rest : Vect n Field) -> - Divides f.alignment f.offset -> - FieldsAligned rest -> - FieldsAligned (f :: rest) - -||| Verify a struct layout is valid -public export -verifyLayout : (fields : Vect n Field) -> (align : Nat) -> Either String StructLayout -verifyLayout fields align = - let size = calcStructSize fields align - in case decSo (size >= sum (map (\f => f.size) fields)) of - Yes prf => Right (MkStructLayout fields size align) - No _ => Left "Invalid struct size" - --------------------------------------------------------------------------------- --- Platform-Specific Layouts --------------------------------------------------------------------------------- - -||| Struct layout may differ by platform -public export -PlatformLayout : Platform -> Type -> Type -PlatformLayout p t = StructLayout - -||| Verify layout is correct for all platforms -public export -verifyAllPlatforms : - (layouts : (p : Platform) -> PlatformLayout p t) -> - Either String () -verifyAllPlatforms layouts = - -- Check that layout is valid on all platforms - Right () - --------------------------------------------------------------------------------- --- C ABI Compatibility --------------------------------------------------------------------------------- - -||| Proof that a struct follows C ABI rules -public export -data CABICompliant : StructLayout -> Type where - CABIOk : - (layout : StructLayout) -> - FieldsAligned layout.fields -> - CABICompliant layout - -||| Check if layout follows C ABI -public export -checkCABI : (layout : StructLayout) -> Either String (CABICompliant layout) -checkCABI layout = - -- Verify C ABI rules - Right (CABIOk layout ?fieldsAlignedProof) - --------------------------------------------------------------------------------- --- Example Layouts --------------------------------------------------------------------------------- - -||| Example: Simple struct layout -public export -exampleLayout : StructLayout -exampleLayout = - MkStructLayout - [ MkField "x" 0 4 4 -- Bits32 at offset 0 - , MkField "y" 8 8 8 -- Bits64 at offset 8 (4 bytes padding) - , MkField "z" 16 8 8 -- Double at offset 16 - ] - 24 -- Total size: 24 bytes - 8 -- Alignment: 8 bytes - -||| Proof that example layout is valid -export -exampleLayoutValid : CABICompliant exampleLayout -exampleLayoutValid = CABIOk exampleLayout ?exampleFieldsAligned - --------------------------------------------------------------------------------- --- Offset Calculation --------------------------------------------------------------------------------- - -||| Calculate field offset with proof of correctness -public export -fieldOffset : (layout : StructLayout) -> (fieldName : String) -> Maybe (n : Nat ** Field) -fieldOffset layout name = - case findIndex (\f => f.name == name) layout.fields of - Just idx => Just (finToNat idx ** index idx layout.fields) - Nothing => Nothing - -||| Proof that field offset is within struct bounds -public export -offsetInBounds : (layout : StructLayout) -> (f : Field) -> So (f.offset + f.size <= layout.totalSize) -offsetInBounds layout f = ?offsetInBoundsProof diff --git a/union-policy-parser/src/abi/Types.idr b/union-policy-parser/src/abi/Types.idr deleted file mode 100644 index 9107480..0000000 --- a/union-policy-parser/src/abi/Types.idr +++ /dev/null @@ -1,64 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- Copyright (c) Jonathan D.A. Jewell -||| UNION-POLICY-PARSER — ABI Type Definitions -||| -||| This module defines the Application Binary Interface for the policy -||| orchestration engine. It ensures that complex "Union Policies" are -||| handled with strict type-safety across the proof and execution layers. - -module UNION_POLICY_PARSER.ABI.Types - -import Data.Bits -import Data.So -import Data.Vect - -%default total - --------------------------------------------------------------------------------- --- Platform Context --------------------------------------------------------------------------------- - -||| Supported targets for policy verification. -public export -data Platform = Linux | Windows | MacOS | BSD | WASM - -||| Resolves the target environment at compile time. -public export -thisPlatform : Platform -thisPlatform = - %runElab do - pure Linux - --------------------------------------------------------------------------------- --- Core Result Codes --------------------------------------------------------------------------------- - -||| Formal outcome of a policy parsing or validation operation. -public export -data Result : Type where - ||| Operation Successful - Ok : Result - ||| Operation Failed: Policy violation - Error : Result - ||| Malformed Policy: Syntax error in input - InvalidParam : Result - ||| System Error: Out of memory - OutOfMemory : Result - ||| Safety Error: Internal null pointer encountered - NullPointer : Result - --------------------------------------------------------------------------------- --- Opaque Policy Handles --------------------------------------------------------------------------------- - -||| Opaque handle to a Policy instance. -||| INVARIANT: The internal pointer is guaranteed to be non-null. -public export -data Handle : Type where - MkHandle : (ptr : Bits64) -> {auto 0 nonNull : So (ptr /= 0)} -> Handle - -||| Safe constructor for policy handles. -public export -createHandle : Bits64 -> Maybe Handle -createHandle 0 = Nothing -createHandle ptr = Just (MkHandle ptr) diff --git a/union-policy-parser/src/haskell/Main.hs b/union-policy-parser/src/haskell/Main.hs deleted file mode 100644 index e69de29..0000000 diff --git a/union-policy-parser/src/haskell/stack.yaml b/union-policy-parser/src/haskell/stack.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/union-policy-parser/src/julia/Main.jl b/union-policy-parser/src/julia/Main.jl deleted file mode 100644 index e69de29..0000000 diff --git a/union-policy-parser/src/julia/parser.jl b/union-policy-parser/src/julia/parser.jl deleted file mode 100644 index e69de29..0000000 diff --git a/union-policy-parser/src/rust/Cargo.lock b/union-policy-parser/src/rust/Cargo.lock deleted file mode 100644 index f97fc3f..0000000 --- a/union-policy-parser/src/rust/Cargo.lock +++ /dev/null @@ -1,1084 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "assert_cmd" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" -dependencies = [ - "anstyle", - "bstr", - "libc", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - -[[package]] -name = "cc" -version = "1.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "clap" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" - -[[package]] -name = "float-cmp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" -dependencies = [ - "num-traits", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "globset" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ignore" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jiff" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.180" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom_locate" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" -dependencies = [ - "bytecount", - "memchr", - "nom", -] - -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "portable-atomic" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "predicates" -version = "3.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" -dependencies = [ - "anstyle", - "difflib", - "float-cmp", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" - -[[package]] -name = "predicates-tree" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys", -] - -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "union-policy-parser" -version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "assert_cmd", - "chrono", - "clap", - "env_logger", - "hex", - "ignore", - "lazy_static", - "log", - "nom", - "nom_locate", - "predicates", - "regex", - "serde", - "serde_json", - "sha2", - "tempfile", - "thiserror", - "toml", - "walkdir", -] - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "zmij" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/union-policy-parser/src/rust/Cargo.toml b/union-policy-parser/src/rust/Cargo.toml deleted file mode 100644 index e731cc7..0000000 --- a/union-policy-parser/src/rust/Cargo.toml +++ /dev/null @@ -1,76 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later - -[package] -name = "union-policy-parser" -version = "0.1.0" -authors = ["Jonathan D.A. Jewell "] -edition = "2021" -license = "PMPL-1.0-or-later" -description = "Policy validation engine for union contracts (NUJ, IWW, UCU) using A2ML" -repository = "https://github.com/hyperpolymath/union-policy-parser" -keywords = ["union", "labor", "contracts", "a2ml", "policy"] -categories = ["command-line-utilities", "text-processing"] - -[dependencies] -# CLI framework -clap = { version = "4.5", features = ["derive", "env", "cargo"] } - -# Parsing -nom = "7.1" # Parser combinators for A2ML -nom_locate = "4.2" # Source location tracking - -# Serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -toml = "0.8" - -# Error handling -anyhow = "1.0" -thiserror = "1.0" - -# Logging -env_logger = "0.11" -log = "0.4" - -# Text processing -regex = "1.10" -aho-corasick = "1.1" # Fast multi-pattern matching -lazy_static = "1.4" # Static regex compilation - -# File I/O -walkdir = "2.5" -ignore = "0.4" # Respect .gitignore - -# Checksums -sha2 = "0.10" -hex = "0.4" - -# Date/time for grievance generation -chrono = "0.4" - -# Optional: PDF parsing (for pdf-to-a2ml) -# pdf-extract = { version = "0.7", optional = true } - -[dev-dependencies] -assert_cmd = "2.0" -predicates = "3.1" -tempfile = "3.10" -criterion = { version = "0.5", features = ["html_reports"] } - -[features] -default = [] -pdf = [] # Enable PDF parsing - -[[bin]] -name = "union-policy-parser" -path = "src/main.rs" - -[[bench]] -name = "parser_bench" -harness = false - -[profile.release] -opt-level = 3 -lto = true -codegen-units = 1 -strip = true diff --git a/union-policy-parser/src/rust/benches/parser_bench.rs b/union-policy-parser/src/rust/benches/parser_bench.rs deleted file mode 100644 index 89ee5f5..0000000 --- a/union-policy-parser/src/rust/benches/parser_bench.rs +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -// -//! Criterion Benchmarks — Union Policy Parser -//! -//! Measures the throughput of the A2ML parser across representative -//! workloads: a minimal policy document, a full multi-section contract, -//! and a large synthetic document constructed from 100 repeated sections. -//! -//! Run with: `cargo bench --bench parser_bench` - -use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput}; - -// --------------------------------------------------------------------------- -// Benchmark fixtures -// --------------------------------------------------------------------------- - -/// Minimal A2ML policy: @abstract block only. Represents the fastest-path case. -const MINIMAL_POLICY: &str = r#"@abstract -A minimal freelance engagement policy. -@end -"#; - -/// Full NUJ contract with all directive types and multiple sections. -/// Representative of a typical real-world input. -const FULL_NUJ_POLICY: &str = r#"@abstract -Full NUJ compliant freelance contract. -@end - -@requires -truth-accuracy -independence -source-protection -@end - -# Engagement Terms - -- Flat fee on acceptance -- No rights transfer -- Source identity protected under law - -# Payment Terms - -- NET-30 payment -- Late payment penalty: 10% per month compounded -- No speculative work required - -# Confidentiality - -- Sources never disclosed to third parties -- Notes retained by journalist for 7 years -- Publisher may not compel disclosure - -@refs -[NUJ Code of Conduct](https://www.nuj.org.uk/about-us/rules-and-guidance/code-of-conduct.html) -[NUJ Freelance Charter](https://www.nuj.org.uk/freelance/) -@end -"#; - -/// Large synthetic policy: 100 sections, stress-tests section parsing throughput. -fn large_policy() -> String { - let header = "@abstract\nSynthetic stress-test policy.\n@end\n\n@requires\ntruth-accuracy\n@end\n\n"; - let mut doc = String::from(header); - for i in 0..100 { - doc.push_str(&format!( - "# Section {i}: Terms and Conditions\n\n- Clause {i}.1: standard terms apply\n- Clause {i}.2: specific obligations\n- Clause {i}.3: governing law: England and Wales\n\n" - )); - } - doc.push_str("@refs\n[Reference](https://example.com/)\n@end\n"); - doc -} - -// --------------------------------------------------------------------------- -// Benchmark group: minimal input -// --------------------------------------------------------------------------- - -fn bench_minimal_parse(c: &mut Criterion) { - c.bench_function("parse_minimal_policy", |b| { - b.iter(|| { - let _ = union_policy_parser::parser::parse(black_box(MINIMAL_POLICY)); - }); - }); -} - -// --------------------------------------------------------------------------- -// Benchmark group: full NUJ document -// --------------------------------------------------------------------------- - -fn bench_full_nuj_parse(c: &mut Criterion) { - let mut group = c.benchmark_group("full_nuj"); - group.throughput(Throughput::Bytes(FULL_NUJ_POLICY.len() as u64)); - - group.bench_function("parse_full_nuj_policy", |b| { - b.iter(|| { - let _ = union_policy_parser::parser::parse(black_box(FULL_NUJ_POLICY)); - }); - }); - - group.finish(); -} - -// --------------------------------------------------------------------------- -// Benchmark group: large synthetic document -// --------------------------------------------------------------------------- - -fn bench_large_document_parse(c: &mut Criterion) { - let doc = large_policy(); - let byte_len = doc.len() as u64; - - let mut group = c.benchmark_group("large_document"); - group.throughput(Throughput::Bytes(byte_len)); - - group.bench_function("parse_100_section_policy", |b| { - b.iter(|| { - let _ = union_policy_parser::parser::parse(black_box(doc.as_str())); - }); - }); - - group.finish(); -} - -// --------------------------------------------------------------------------- -// Criterion registration -// --------------------------------------------------------------------------- - -criterion_group!( - benches, - bench_minimal_parse, - bench_full_nuj_parse, - bench_large_document_parse -); -criterion_main!(benches); diff --git a/union-policy-parser/src/rust/main.rs b/union-policy-parser/src/rust/main.rs deleted file mode 100644 index 2a0611d..0000000 --- a/union-policy-parser/src/rust/main.rs +++ /dev/null @@ -1,2 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell diff --git a/union-policy-parser/src/rust/src/error.rs b/union-policy-parser/src/rust/src/error.rs deleted file mode 100644 index d376c22..0000000 --- a/union-policy-parser/src/rust/src/error.rs +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -//! Union Policy Parser Error Space. -//! -//! This module defines the failure modes for the contract auditing tool. -//! It uses `thiserror` to provide semantic error reporting across -//! parsing, validation, and reporting stages. - -use thiserror::Error; -use std::path::PathBuf; - -#[derive(Error, Debug)] -pub enum PolicyError { - /// PARSE: The A2ML source file is syntactically invalid. - #[error("Failed to parse A2ML file: {0}")] - ParseError(String), - - /// VALIDATION: The contract violates a structural or logical rule. - #[error("Validation failed: {0}")] - ValidationError(String), - - /// MANDATORY: A required clause (e.g. 'source-protection') was not found. - #[error("Missing required clause: {0}")] - MissingClause(String), - - /// VALUE: A clause value (e.g. NET-90) violates union standards. - #[error("Invalid clause value for '{clause}': expected {expected}, got {actual}")] - InvalidClauseValue { - clause: String, - expected: String, - actual: String, - }, - - /// TEMPLATE: The grievance letter template is malformed. - #[error("Template error: {0}")] - TemplateError(String), - - /// SYSTEM: Wrapped IO and Serialization errors. - #[error("IO error: {0}")] - IoError(#[from] std::io::Error), -} - -pub type Result = std::result::Result; diff --git a/union-policy-parser/src/rust/src/main.rs b/union-policy-parser/src/rust/src/main.rs deleted file mode 100644 index eebcd36..0000000 --- a/union-policy-parser/src/rust/src/main.rs +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -//! Union Policy Parser — Verified Employment Contract Auditing (CLI). -//! -//! This tool provides a high-assurance interface for validating employment -//! contracts against ethical standards set by unions (NUJ, IWW, UCU) and -//! UK statutory law. It uses A2ML (Attested Markup Language) to ensure -//! that legal clauses are machine-readable and auditable. -//! -//! CORE CAPABILITIES: -//! 1. **Structural Validation**: Ensures mandatory clauses (e.g., payment -//! terms, source protection) are present and correctly formatted. -//! 2. **Attested Mode**: Performs deep legal compliance checks, matching -//! contract text against verified union policy schemas. -//! 3. **Grievance Engine**: Automatically generates Markdown-formatted -//! grievance letters when violations (e.g. "Work for Hire" traps) are found. -//! 4. **Red-Flag Scanning**: Heuristic search for exploitative keywords -//! (e.g. "moral rights waiver"). - -#![forbid(unsafe_code)] -use clap::{Parser, Subcommand}; -use anyhow::Result; -use std::path::PathBuf; -// ... [other imports] - -/// SUBCOMMAND DISPATCH: Orchestrates the different audit workflows. -#[derive(Subcommand)] -enum Commands { - /// VALIDATE: Checks a single contract against a specific union schema. - Validate { - contract: PathBuf, - schema: PathBuf, - mode: ValidationMode, - strict: bool, // Non-zero exit on ANY violation - }, - /// GRIEVANCE: Auto-generates formal correspondence for specific violations. - Grievance { - contract: PathBuf, - violation: String, - output: PathBuf, - }, - /// SCAN: Keyword-based discovery of exploitative or dangerous clauses. - ScanRedFlags { - contract: PathBuf, - patterns: Vec, - }, -} - -fn main() -> Result<()> { - // ... [CLI execution logic] - Ok(()) -} diff --git a/union-policy-parser/src/rust/src/parser.rs b/union-policy-parser/src/rust/src/parser.rs deleted file mode 100644 index 2bb5348..0000000 --- a/union-policy-parser/src/rust/src/parser.rs +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -//! A2ML Parser — Attested Markup Language Implementation. -//! -//! This module implements the "Module 0" parser for the A2ML format. It uses -//! the `nom` parser combinator library to provide a high-assurance, -//! zero-copy parsing pipeline for employment contracts and policy schemas. -//! -//! GRAMMAR PILLARS: -//! 1. **Directives**: Semantic blocks like `@abstract`, `@refs`, and `@requires`. -//! 2. **Structural**: Headings (#), Lists (-), and Tables. -//! 3. **Attestations**: Extracts formal claims and verification requirements -//! embedded within natural language paragraphs. - -use crate::error::{PolicyError, Result}; -use nom::{ - IResult, - branch::alt, - bytes::complete::{tag, take_until, take_while1}, - character::complete::{char, line_ending, multispace0, space0, space1}, - combinator::{map, opt}, - multi::{many0, many1}, - sequence::{preceded, terminated, tuple}, -}; -// ... [other imports] - -/// DOCUMENT MODEL: The logical representation of a parsed A2ML source. -#[derive(Debug, Clone, serde::Serialize)] -pub struct A2mlDocument { - pub abstract_text: Option, - pub sections: Vec
, - pub references: Vec, - pub requirements: Vec, - pub raw: String, -} - -/// PARSER KERNEL: Ingests a string and produces an `A2mlDocument`. -fn document(input: &str) -> IResult<&str, A2mlDocument> { - let (input, _) = multispace0(input)?; - - // SEQUENTIAL SCAN: Parses optional directives followed by section bodies. - let (input, abstract_text) = opt(abstract_directive)(input)?; - let (input, requirements) = opt(requires_directive)(input)?; - let (input, sections) = many0(section)(input)?; - let (input, references) = opt(refs_directive)(input)?; - - Ok((input, A2mlDocument { - abstract_text, - sections, - references: references.unwrap_or_default(), - requirements: requirements.unwrap_or_default(), - raw: input.to_string(), - })) -} - -/// ATTESTATION EXTRACTION: Heuristic search for verified claims. -/// Pattern: "**Attestation:** [Requirement] (Reference)" -fn extract_attestations(blocks: &[ContentBlock]) -> Vec { - // ... [Implementation identifying formal requirement keywords] -} diff --git a/union-policy-parser/src/rust/src/reporter.rs b/union-policy-parser/src/rust/src/reporter.rs deleted file mode 100644 index 43781cd..0000000 --- a/union-policy-parser/src/rust/src/reporter.rs +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -//! Grievance and Audit Reporting Engine. -//! -//! This module implements the "Actionable Feedback" layer of the policy -//! parser. It transforms abstract validation findings into concrete -//! audit reports and formal union grievance correspondence. -//! -//! OUTPUT FORMATS: -//! 1. **JSON**: For machine-readable integration with HR systems. -//! 2. **Markdown**: For high-fidelity human review and documentation. -//! 3. **Formal Letter**: Uses Markdown templates to generate letters -//! suitable for submission to employers. - -use crate::error::{PolicyError, Result}; -use crate::validator::ValidationReport; -use std::path::Path; -use std::fs; - -/// GRIEVANCE GENERATOR: Orchestrates the creation of formal correspondence. -pub struct GrievanceGenerator { - union: Option, // Contextual union (e.g. NUJ) - template: Option, // Markdown template with {{placeholders}} -} - -impl GrievanceGenerator { - /// DISPATCH: Generates a grievance letter for a specific `violation`. - /// - /// VARIABLES SUPPORTED: - /// - `{{violation}}`: The type of ethical breach detected. - /// - `{{contract_id}}`: Link to the parsed A2ML source. - /// - `{{union}}`: The relevant collective bargaining unit. - pub fn generate(&self, violation: &str, validation_report: &ValidationReport) -> Result { - // ... [Template substitution and formatting logic] - Ok("# GRIEVANCE LETTER\n".into()) - } -} - -/// REPORT RENDERER: High-level utility for serializing validation results. -pub struct ReportRenderer; - -impl ReportRenderer { - /// SERIALIZATION (JSON): Produces a structured audit trail. - pub fn render_json(report: &ValidationReport) -> Result { - // ... [Serde-based JSON generation] - Ok("{}".into()) - } -} diff --git a/union-policy-parser/src/rust/src/schemas.rs b/union-policy-parser/src/rust/src/schemas.rs deleted file mode 100644 index e49abdf..0000000 --- a/union-policy-parser/src/rust/src/schemas.rs +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -//! Union-Specific Policy Schemas. -//! -//! This module defines the authoritative standards for various trade unions. -//! It maps union identities to their required clauses and identifies -//! exploitative "Red Flag" patterns in employment contracts. - -use crate::error::{PolicyError, Result}; -use std::collections::HashMap; - -/// SUPPORTED UNIONS: -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Union { - Nuj, // National Union of Journalists - Iww, // Industrial Workers of the World - Ucu, // University and College Union -} - -impl Union { - /// POLICY: Returns the set of clauses that MUST be present for a - /// contract to be considered ethical by the specific union. - pub fn required_clauses(&self) -> Vec<&'static str> { - match self { - Union::Nuj => vec!["truth-accuracy", "independence", "source-protection"], - Union::Iww => vec!["payment-terms", "late-payment-penalty", "no-spec-work"], - Union::Ucu => vec!["academic-freedom", "workload-limits", "no-casualization"], - } - } - - /// AUDIT: Defines regex patterns for exploitative language. - /// Rejects contracts containing waivers of moral rights or - /// excessive payment delays. - pub fn red_flag_patterns(&self) -> Vec<&'static str> { - match self { - Union::Nuj => vec!["all rights", "work for hire", "no source protection"], - Union::Iww => vec!["unpaid", "payment on publication", "NET 90"], - Union::Ucu => vec!["zero hours", "unlimited hours", "no research time"], - } - } -} - -/// RULE ENGINE: Implements specific value checks for individual clauses. -pub struct UnionRules { - union: Union, -} - -impl UnionRules { - /// VALIDATION: Verifies that a clause value (e.g. "30") meets union - /// minimum standards (e.g. NET-30). - pub fn check_clause_value(&self, clause: &str, value: &str) -> Result { - match self.union { - Union::Iww => self.check_iww_clause(clause, value), - // ... [Dispatch to other union-specific checks] - _ => Ok(true), - } - } - - fn check_iww_clause(&self, clause: &str, value: &str) -> Result { - match clause { - "payment-terms.net-days" => { - // IWW MANDATE: NET-30 or better. - let days: u32 = value.parse().map_err(|_| PolicyError::ValidationError("Invalid days".into()))?; - Ok(days <= 30) - }, - _ => Ok(true), - } - } -} diff --git a/union-policy-parser/src/rust/src/validator.rs b/union-policy-parser/src/rust/src/validator.rs deleted file mode 100644 index da71f66..0000000 --- a/union-policy-parser/src/rust/src/validator.rs +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -//! Union Policy Validator — Legal Compliance Engine. -//! -//! This module implements the logical verification of employment contracts. -//! It evaluates a parsed `A2mlDocument` against a policy schema to identify -//! violations of union ethics or statutory law. - -use crate::error::{PolicyError, Result}; -use crate::parser::{A2mlDocument, Section}; -use std::collections::HashSet; - -/// ASSURANCE LEVELS: -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ValidationMode { - /// LAX: Validates A2ML syntax and basic document structure. - Lax, - /// CHECKED: Ensures all mandatory clauses (e.g. 'payment', 'copyright') are present. - Checked, - /// ATTESTED: Performs semantic verification of claims against external legal sources. - Attested, -} - -/// AUDIT REPORT: Consolidated results of a validation run. -#[derive(Debug, Clone)] -pub struct ValidationReport { - pub contract_path: String, - pub schema_path: String, - pub valid: bool, // Final pass/fail status - pub errors: Vec, - pub warnings: Vec, - pub required_clauses: Vec, -} - -impl Validator { - /// AUDIT: Executes the validation pipeline according to the chosen mode. - pub fn validate(&self, contract: &A2mlDocument, required_clauses: &[String]) -> ValidationReport { - // ... [Implementation of the validation dispatch] - match self.mode { - ValidationMode::Checked => { self.validate_structure(contract, &mut report); } - ValidationMode::Attested => { - self.validate_structure(contract, &mut report); - self.validate_attestations(contract, &mut report); // Deep legal check - } - _ => {} - } - report - } - - /// SEMANTIC CHECK: Verifies that formal attestations in the contract - /// (e.g. "Must comply with NUJ Code §1") are legally sound. - fn validate_attestations(&self, contract: &A2mlDocument, report: &mut ValidationReport) { - // ... [Logic to match claims against verified union policy data] - } -} diff --git a/union-policy-parser/src/rust/tests/aspect_test.rs b/union-policy-parser/src/rust/tests/aspect_test.rs deleted file mode 100644 index f15ba29..0000000 --- a/union-policy-parser/src/rust/tests/aspect_test.rs +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -// -//! Security / Aspect Tests — Union Policy Parser -//! -//! Verifies that the parser handles hostile, malformed, or edge-case inputs -//! without panicking. The critical invariant: the parser must never panic — -//! every input, however malformed, must return an Ok or Err, never abort. -//! -//! Also tests error-type correctness so that callers can handle failures -//! semantically (e.g., display a user-facing message rather than crashing). - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/// Parse and assert no panic. Returns true if Ok. -fn parses_without_panic(input: &str) -> bool { - // std::panic::catch_unwind is not needed here because Rust will report - // a panic as a test failure automatically. We simply call parse and - // observe the Result. - union_policy_parser::parser::parse(input).is_ok() -} - -// --------------------------------------------------------------------------- -// Security / Aspect tests -// --------------------------------------------------------------------------- - -#[test] -fn aspect_empty_string_does_not_panic() { - // The empty string is valid input — no directives, no sections. - let result = union_policy_parser::parser::parse(""); - // Must return Ok (empty document) or Err — never panic. - assert!( - result.is_ok() || result.is_err(), - "empty string must return Ok or Err, not panic" - ); -} - -#[test] -fn aspect_whitespace_only_does_not_panic() { - // Input consisting entirely of whitespace and newlines must not panic. - let result = union_policy_parser::parser::parse(" \n\n\t\n "); - assert!( - result.is_ok() || result.is_err(), - "whitespace-only input must not panic" - ); -} - -#[test] -fn aspect_unclosed_abstract_directive_does_not_panic() { - // A directive opened without a matching @end must not panic. - let input = "@abstract\nThis contract has no closing tag."; - let result = union_policy_parser::parser::parse(input); - assert!( - result.is_ok() || result.is_err(), - "unclosed @abstract must not panic" - ); -} - -#[test] -fn aspect_unknown_directive_does_not_panic() { - // An unrecognised @ directive must not panic; parser may ignore or error. - let input = "@unknown\nsome content\n@end\n# Section\n- item\n"; - let result = union_policy_parser::parser::parse(input); - assert!( - result.is_ok() || result.is_err(), - "unknown directive must not panic" - ); -} - -#[test] -fn aspect_special_chars_in_directive_do_not_panic() { - // Special characters including Unicode in directive bodies must be handled. - let input = "@abstract\n\n@end\n"; - let result = union_policy_parser::parser::parse(input); - assert!( - result.is_ok() || result.is_err(), - "special characters in @abstract must not panic" - ); -} - -#[test] -fn aspect_null_byte_in_input_does_not_panic() { - // A string containing a null byte (valid Rust str is NUL-safe) must not panic. - let input = "@abstract\nText with \x00 null byte.\n@end\n"; - let result = union_policy_parser::parser::parse(input); - assert!( - result.is_ok() || result.is_err(), - "null byte in input must not panic" - ); -} - -#[test] -fn aspect_extremely_long_line_does_not_panic() { - // A single line of 100,000 characters must not overflow the stack or panic. - let long_line = "a".repeat(100_000); - let input = format!("# Long Section\n\n- {}\n", long_line); - let result = union_policy_parser::parser::parse(&input); - assert!( - result.is_ok() || result.is_err(), - "extremely long line must not panic" - ); -} - -#[test] -fn aspect_deeply_nested_heading_markers_do_not_panic() { - // Many consecutive '#' characters at the start of a line must not panic. - let input = "##########################################################\nContent\n"; - let result = union_policy_parser::parser::parse(input); - assert!( - result.is_ok() || result.is_err(), - "deeply nested headings must not panic" - ); -} - -#[test] -fn aspect_malformed_refs_block_does_not_panic() { - // A @refs block with no valid link syntax must not panic. - let input = "@refs\n!!! not a valid link !!!\n@end\n"; - let result = union_policy_parser::parser::parse(input); - assert!( - result.is_ok() || result.is_err(), - "malformed @refs block must not panic" - ); -} - -#[test] -fn aspect_repeated_directives_do_not_panic() { - // Multiple @abstract blocks must not panic regardless of semantics. - let input = "@abstract\nFirst.\n@end\n@abstract\nSecond.\n@end\n"; - let result = union_policy_parser::parser::parse(input); - assert!( - result.is_ok() || result.is_err(), - "repeated @abstract blocks must not panic" - ); -} diff --git a/union-policy-parser/src/rust/tests/e2e_test.rs b/union-policy-parser/src/rust/tests/e2e_test.rs deleted file mode 100644 index d3e39cb..0000000 --- a/union-policy-parser/src/rust/tests/e2e_test.rs +++ /dev/null @@ -1,226 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -// -//! End-to-End Tests — Union Policy Parser -//! -//! These tests exercise the full A2ML parsing pipeline, verifying that complete -//! policy documents produce correctly structured `A2mlDocument` output. Each -//! test covers a realistic employment contract scenario relevant to union -//! compliance auditing (NUJ, IWW, UCU). - -use union_policy_parser::parser::A2mlDocument; -use union_policy_parser::schemas::Union; -use union_policy_parser::validator::ValidationMode; -use union_policy_parser::error::PolicyError; - -// --------------------------------------------------------------------------- -// Shared fixture helpers -// --------------------------------------------------------------------------- - -/// Minimal valid NUJ contract with all required clauses present. -fn nuj_minimal_policy() -> &'static str { - r#"@abstract -This agreement between the journalist and the publisher sets out the terms -of engagement under NUJ ethical standards. -@end - -@requires -truth-accuracy -independence -source-protection -@end - -# Assignment Terms - -- Payment on delivery within 30 days -- All rights retained by journalist -- Source identity protected under law - -@refs -[NUJ Code of Conduct](https://www.nuj.org.uk/about-us/rules-and-guidance/code-of-conduct.html) -@end -"# -} - -/// Minimal IWW contract fixture with mandatory payment clauses. -fn iww_minimal_policy() -> &'static str { - r#"@abstract -Freelance service agreement under IWW collective standards. -@end - -@requires -payment-terms -late-payment-penalty -no-spec-work -@end - -# Service Agreement - -- NET-30 payment terms -- 10% late payment penalty per month -- No speculative work required - -@refs -[IWW Freelance Contract](https://www.iww.org/) -@end -"# -} - -/// Minimal UCU contract for academic employment. -fn ucu_minimal_policy() -> &'static str { - r#"@abstract -Academic employment contract under UCU collective agreement. -@end - -@requires -academic-freedom -workload-limits -no-casualization -@end - -# Academic Terms - -- Guaranteed academic freedom in research and teaching -- Maximum 38-hour contracted week -- Permanent contract, no zero-hours clauses - -@refs -[UCU Framework Agreement](https://www.ucu.org.uk/) -@end -"# -} - -/// Policy document with a structural heading and a sub-list. -fn policy_with_sections() -> &'static str { - r#"@abstract -Multi-section policy for complex engagement. -@end - -# Section One: Payment - -- Delivery fee: £500 -- Expenses: reimbursed within 14 days - -# Section Two: Rights - -- Journalist retains copyright -- Publisher receives one-time print licence - -# Section Three: Confidentiality - -- Sources never disclosed -- Notes retained for 7 years - -@refs -[NUJ Freelance Charter](https://www.nuj.org.uk/) -@end -"# -} - -// --------------------------------------------------------------------------- -// E2E tests -// --------------------------------------------------------------------------- - -#[test] -fn e2e_parse_nuj_minimal_returns_ok() { - // A well-formed NUJ policy must parse without error. - let input = nuj_minimal_policy(); - let result = union_policy_parser::parser::parse(input); - assert!(result.is_ok(), "expected Ok, got: {:?}", result.err()); -} - -#[test] -fn e2e_parse_nuj_abstract_text_extracted() { - // The @abstract directive content must be captured. - let input = nuj_minimal_policy(); - let doc = union_policy_parser::parser::parse(input).unwrap(); - let abstract_text = doc.abstract_text.as_deref().unwrap_or(""); - assert!( - abstract_text.contains("journalist") || abstract_text.contains("NUJ"), - "abstract text should mention journalist or NUJ, got: {:?}", abstract_text - ); -} - -#[test] -fn e2e_parse_nuj_requirements_extracted() { - // @requires block must be parsed into the requirements vec. - let input = nuj_minimal_policy(); - let doc = union_policy_parser::parser::parse(input).unwrap(); - assert!( - doc.requirements.contains(&"truth-accuracy".to_string()) - || doc.requirements.iter().any(|r| r.contains("truth")), - "requirements should include truth-accuracy, got: {:?}", doc.requirements - ); -} - -#[test] -fn e2e_parse_nuj_references_extracted() { - // @refs block must produce at least one reference entry. - let input = nuj_minimal_policy(); - let doc = union_policy_parser::parser::parse(input).unwrap(); - assert!( - !doc.references.is_empty(), - "expected at least one reference, got empty vec" - ); -} - -#[test] -fn e2e_parse_iww_policy_ok() { - // IWW contract must parse without error. - let input = iww_minimal_policy(); - let result = union_policy_parser::parser::parse(input); - assert!(result.is_ok(), "IWW parse failed: {:?}", result.err()); -} - -#[test] -fn e2e_parse_iww_requirements_present() { - // IWW requirements must include payment-related clauses. - let input = iww_minimal_policy(); - let doc = union_policy_parser::parser::parse(input).unwrap(); - let reqs_str = doc.requirements.join(" "); - assert!( - reqs_str.contains("payment") || reqs_str.contains("no-spec"), - "IWW requirements missing payment or no-spec-work, got: {}", reqs_str - ); -} - -#[test] -fn e2e_parse_ucu_policy_ok() { - // UCU academic contract must parse without error. - let input = ucu_minimal_policy(); - let result = union_policy_parser::parser::parse(input); - assert!(result.is_ok(), "UCU parse failed: {:?}", result.err()); -} - -#[test] -fn e2e_parse_policy_with_multiple_sections() { - // Document with multiple # headings must produce multiple sections. - let input = policy_with_sections(); - let doc = union_policy_parser::parser::parse(input).unwrap(); - assert!( - doc.sections.len() >= 2, - "expected at least 2 sections, got {}", doc.sections.len() - ); -} - -#[test] -fn e2e_parse_raw_field_non_empty_on_valid_input() { - // The raw field on A2mlDocument must be populated for real input. - let input = nuj_minimal_policy(); - let doc = union_policy_parser::parser::parse(input).unwrap(); - // raw may capture trailing unparsed text or the full string — either way, - // parsing a non-empty document must not produce a completely empty raw field - // when the document has content beyond the parsed directives. - let _ = doc.raw; // field exists and is accessible -} - -#[test] -fn e2e_serialize_document_to_json() { - // A parsed document must be serializable to JSON via serde. - let input = nuj_minimal_policy(); - let doc = union_policy_parser::parser::parse(input).unwrap(); - let json = serde_json::to_string(&doc); - assert!(json.is_ok(), "JSON serialization failed: {:?}", json.err()); - let json_str = json.unwrap(); - assert!(json_str.contains("sections"), "JSON output missing 'sections' key"); -} diff --git a/union-policy-parser/src/rust/tests/property_test.rs b/union-policy-parser/src/rust/tests/property_test.rs deleted file mode 100644 index 02bbef1..0000000 --- a/union-policy-parser/src/rust/tests/property_test.rs +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -// -//! Property Tests — Union Policy Parser -//! -//! Each test represents a distinct policy input variant and verifies that: -//! 1. Valid input parses successfully (no panic, Ok result). -//! 2. Parsing is deterministic (same input → same output on repeated calls). -//! 3. The output structure is internally consistent. -//! -//! Eight input variants are exercised, with multiple assertions per variant. - -// --------------------------------------------------------------------------- -// Variant helpers -// --------------------------------------------------------------------------- - -/// Variant A: abstract only — no requirements, no sections, no refs. -fn variant_abstract_only() -> &'static str { - "@abstract\nBasic policy statement.\n@end\n" -} - -/// Variant B: requirements only — no abstract, no sections. -fn variant_requires_only() -> &'static str { - "@requires\nsource-protection\ntruth-accuracy\n@end\n" -} - -/// Variant C: single heading section, no directives. -fn variant_single_section() -> &'static str { - "# Payment Terms\n\n- NET-30 payment\n- No spec work\n" -} - -/// Variant D: refs only — minimal reference block. -fn variant_refs_only() -> &'static str { - "@refs\n[NUJ Code](https://nuj.org.uk/)\n@end\n" -} - -/// Variant E: full NUJ document with all blocks present. -fn variant_full_nuj() -> &'static str { - r#"@abstract -Full NUJ compliant freelance contract. -@end - -@requires -truth-accuracy -independence -source-protection -@end - -# Engagement Terms - -- Flat fee on acceptance -- No rights transfer - -@refs -[NUJ Code of Conduct](https://www.nuj.org.uk/about-us/rules-and-guidance/code-of-conduct.html) -@end -"# -} - -/// Variant F: document with special ASCII characters in content. -fn variant_special_characters() -> &'static str { - "@abstract\nPolicy: fees & costs — see §3(a)(i).\n@end\n# Terms\n- Rate: £500/day\n" -} - -/// Variant G: multi-paragraph abstract block. -fn variant_multi_paragraph_abstract() -> &'static str { - r#"@abstract -This agreement is entered into by the parties below. - -It supersedes all prior arrangements. -It is governed by the laws of England and Wales. -@end - -# Definitions - -- "Publisher" means the company named above -"# -} - -/// Variant H: deeply nested content with multiple list items. -fn variant_many_list_items() -> &'static str { - r#"# Deliverables - -- Article: 800 words -- Photographs: up to 12 -- Captions: per photo -- Audio clips: optional -- Video: excluded -- Revisions: 1 included -- Expenses: pre-approved only -- Deadlines: as agreed in writing -"# -} - -// --------------------------------------------------------------------------- -// Property tests -// --------------------------------------------------------------------------- - -#[test] -fn property_abstract_only_parses_ok() { - let result = union_policy_parser::parser::parse(variant_abstract_only()); - assert!(result.is_ok(), "abstract-only variant failed: {:?}", result.err()); -} - -#[test] -fn property_abstract_only_has_abstract_text() { - let doc = union_policy_parser::parser::parse(variant_abstract_only()).unwrap(); - assert!(doc.abstract_text.is_some(), "abstract_text must be Some for abstract-only variant"); - assert!( - !doc.abstract_text.as_deref().unwrap_or("").is_empty(), - "abstract_text must not be empty" - ); -} - -#[test] -fn property_requires_only_parses_ok() { - let result = union_policy_parser::parser::parse(variant_requires_only()); - assert!(result.is_ok(), "requires-only variant failed: {:?}", result.err()); -} - -#[test] -fn property_requires_only_has_requirements() { - let doc = union_policy_parser::parser::parse(variant_requires_only()).unwrap(); - assert!( - !doc.requirements.is_empty(), - "requirements must be non-empty for requires-only variant, got {:?}", doc.requirements - ); -} - -#[test] -fn property_single_section_parses_ok() { - let result = union_policy_parser::parser::parse(variant_single_section()); - assert!(result.is_ok(), "single-section variant failed: {:?}", result.err()); -} - -#[test] -fn property_refs_only_parses_ok() { - let result = union_policy_parser::parser::parse(variant_refs_only()); - assert!(result.is_ok(), "refs-only variant failed: {:?}", result.err()); -} - -#[test] -fn property_full_nuj_parses_ok() { - let result = union_policy_parser::parser::parse(variant_full_nuj()); - assert!(result.is_ok(), "full NUJ variant failed: {:?}", result.err()); -} - -#[test] -fn property_full_nuj_sections_non_empty() { - let doc = union_policy_parser::parser::parse(variant_full_nuj()).unwrap(); - assert!( - !doc.sections.is_empty(), - "full NUJ document must have at least one section" - ); -} - -#[test] -fn property_special_characters_parses_ok() { - let result = union_policy_parser::parser::parse(variant_special_characters()); - assert!(result.is_ok(), "special-characters variant failed: {:?}", result.err()); -} - -#[test] -fn property_special_characters_no_panic() { - // Calling parse twice on the same input must produce identical Ok/Err status. - let r1 = union_policy_parser::parser::parse(variant_special_characters()).is_ok(); - let r2 = union_policy_parser::parser::parse(variant_special_characters()).is_ok(); - assert_eq!(r1, r2, "parse result must be deterministic for special-characters variant"); -} - -#[test] -fn property_multi_paragraph_abstract_parses_ok() { - let result = union_policy_parser::parser::parse(variant_multi_paragraph_abstract()); - assert!(result.is_ok(), "multi-paragraph abstract variant failed: {:?}", result.err()); -} - -#[test] -fn property_many_list_items_parses_ok() { - let result = union_policy_parser::parser::parse(variant_many_list_items()); - assert!(result.is_ok(), "many-list-items variant failed: {:?}", result.err()); -} - -#[test] -fn property_determinism_full_nuj() { - // Parsing the same input twice must yield the same section count. - let doc1 = union_policy_parser::parser::parse(variant_full_nuj()).unwrap(); - let doc2 = union_policy_parser::parser::parse(variant_full_nuj()).unwrap(); - assert_eq!( - doc1.sections.len(), doc2.sections.len(), - "section count must be deterministic" - ); - assert_eq!( - doc1.requirements.len(), doc2.requirements.len(), - "requirements count must be deterministic" - ); -}