From 9a958a738da6c73651682441fccf54aeeff438e1 Mon Sep 17 00:00:00 2001 From: arena-agent Date: Tue, 22 Sep 2026 12:51:44 +0000 Subject: [PATCH] feat(gating): native Nickel policies, real SLM providers, audited OTP arbiter Native Nickel policy backend: - vendor Bunsenite @ f788de39 (default-features-off nickel-lang-core 0.18.0) behind a `nickel` feature; vendored tree excluded from workspace membership and un-ignored in .gitignore - Policy::from_policy_file/.ncl dispatch, fail-closed import rejection (decision documented in docs/NICKEL-POLICY.adoc), dedicated nickel-native CI job with the pinned constrained command (parser cannot build under ~2 GB) SLM provider layer: - SlmProvider trait with verdict contract + correlation preservation - LlamaCppProvider: pinned llama-cli invocation (-m/-p/-n/--temp 0 --no-display-prompt --single-turn), last-valid-object extraction for banner-safe parsing, timeout kill, from_env config - HttpSlmProvider behind `http` feature: OpenAI-compatible, rustls, https-or-loopback guard, SLM_API_KEY env-only - real-inference smokes (ignored, env-gated) for local GGUF and loopback llama-server; pinned + SHA-256-verified CI artifacts (.ci-artifact-pins.txt) Gating contract integration: - ContractRunner::evaluate_with_provider: oracle Block terminal (provider never called), Warn +0.2 no-go addend, threshold matrix, should_block override, low llm_confidence escalate, provider failure -> non- overridable Sys902 Escalate - Rust OTP-arbiter client (protocol v1) requiring audit_recorded: true OTP consensus arbiter (Elixir escript): - protocol v1 decode/validate/encode, asymmetric decision matrix (SLM weight 1.5, +0.2 soft-concern addend, 0.9/0.4 thresholds, llm.confidence <= 0.8 escalate) - durable JSONL audit sink: flush-before-ack, rotation at CONATIVE_AUDIT_MAX_BYTES, fail-closed, bounded history, no content - 28 ExUnit tests (CI: arbiter-ci workflow; no local OTP toolchain) Tests: 187 Rust assertions green locally (incl. 11 generative/proptest: terminality, thresholds at 0/1, fail-closed provider errors, determinism, request-ID preservation, concurrent correlation no-mixing) plus real Qwen2.5-0.5B GGUF (14.1s) and llama-server HTTP (3.6s) round-trips. CI: nickel-native job, arbiter-ci workflow, slm-real-inference workflow with approval-gated remote-provider job (secrets never reach PRs). Docs: NICKEL-POLICY, SLM_PROVIDERS, ARBITER_PROTOCOL, UPSTREAM-DELIVERY; CHANGELOG/ROADMAP/TEST-NEEDS/README updates; MAAF bunsenite path fix. --- .ci-artifact-pins.txt | 12 + .clusterfuzzlite/build.sh | 0 .github/hooks/validate-a2ml.sh | 0 .github/hooks/validate-k9.sh | 0 .github/workflows/arbiter-ci.yml | 68 + .github/workflows/ci.yml | 19 + .github/workflows/pages.yml | 0 .github/workflows/slm-real-inference.yml | 116 + .gitignore | 5 +- CHANGELOG.adoc | 21 + Cargo.lock | 2286 +++++++++- Cargo.toml | 16 + README.adoc | 3 + ROADMAP.adoc | 12 +- TEST-NEEDS.adoc | 20 + docs/ARBITER_PROTOCOL.adoc | 103 + docs/MAAF_INTEGRATION.adoc | 5 +- docs/NICKEL-POLICY.adoc | 101 + docs/SLM_PROVIDERS.adoc | 149 + docs/UPSTREAM-DELIVERY.adoc | 70 + hooks/validate-codeql.sh | 0 hooks/validate-permissions.sh | 0 hooks/validate-sha-pins.sh | 0 hooks/validate-spdx.sh | 0 scripts/apply-common-files.sh | 0 scripts/apply-justfiles.sh | 0 scripts/bulk-standardize.sh | 0 scripts/mass-apply-templates.sh | 0 scripts/reconcile-wharf-repos.sh | 0 setup.sh | 0 src/arbiter/config/runtime.exs | 28 + .../lib/conative_gating/application.ex | 4 +- .../lib/conative_gating/arbiter_protocol.ex | 126 + src/arbiter/lib/conative_gating/audit_log.ex | 205 + src/arbiter/lib/conative_gating/cli.ex | 37 + .../lib/conative_gating/protocol_server.ex | 133 + src/arbiter/mix.exs | 9 +- src/arbiter/test/arbiter_protocol_test.exs | 93 + src/arbiter/test/audit_log_test.exs | 115 + src/arbiter/test/consensus_arbiter_test.exs | 63 + src/arbiter/test/protocol_server_test.exs | 111 + src/arbiter/test/test_helper.exs | 5 + src/contract/src/arbiter.rs | 501 ++ src/contract/src/lib.rs | 460 +- src/main.rs | 59 +- src/oracle/Cargo.toml | 11 + src/oracle/src/lib.rs | 209 + src/oracle/src/nickel.rs | 111 + src/slm/Cargo.toml | 15 +- src/slm/src/http.rs | 436 ++ src/slm/src/lib.rs | 29 + src/slm/src/provider.rs | 593 +++ src/slm/tests/real_inference.rs | 117 + tests/generative_test.rs | 545 +++ vendor/bunsenite/.claude/CLAUDE.md | 88 + .../bunsenite/.clusterfuzzlite/Containerfile | 8 + vendor/bunsenite/.clusterfuzzlite/build.sh | 13 + .../bunsenite/.clusterfuzzlite/project.yaml | 3 + vendor/bunsenite/.editorconfig | 68 + vendor/bunsenite/.gitattributes | 54 + vendor/bunsenite/.github/CODEOWNERS | 34 + vendor/bunsenite/.github/CONTRIBUTING.md | 88 + vendor/bunsenite/.github/FUNDING.yml | 7 + .../.github/copilot/coding-agent.yml | 6 + vendor/bunsenite/.github/dependabot.yml | 42 + vendor/bunsenite/.github/funding.yml | 4 + .../bunsenite/.github/label-classifier.json | 739 +++ vendor/bunsenite/.github/labels.json | 260 ++ .../.github/scripts/classify-issue.jq | 164 + .../bunsenite/.github/workflows/actions.lock | 327 ++ .../bunsenite/.github/workflows/boj-build.yml | 23 + .../.github/workflows/cargo-audit.yml | 60 + .../.github/workflows/casket-pages.yml | 121 + .../.github/workflows/cflite_batch.yml | 37 + .../bunsenite/.github/workflows/cflite_pr.yml | 36 + vendor/bunsenite/.github/workflows/codeql.yml | 55 + .../workflows/dependabot-automerge.yml | 147 + .../.github/workflows/dogfood-gate.yml | 418 ++ .../generator-generic-ossf-slsa3-publish.yml | 75 + .../.github/workflows/ghcr-publish.yml | 65 + .../.github/workflows/governance.yml | 20 + .../.github/workflows/hypatia-scan.yml | 23 + .../.github/workflows/instant-sync.yml | 37 + .../.github/workflows/label-triage.yml | 117 + vendor/bunsenite/.github/workflows/labels.yml | 106 + vendor/bunsenite/.github/workflows/mirror.yml | 19 + vendor/bunsenite/.github/workflows/pages.yml | 57 + .../.github/workflows/publish-aur.yml | 115 + .../.github/workflows/publish-chocolatey.yml | 154 + .../.github/workflows/publish-container.yml | 61 + .../.github/workflows/publish-copr.yml | 160 + .../.github/workflows/publish-debian-ppa.yml | 211 + .../.github/workflows/publish-flatpak.yml | 167 + .../.github/workflows/publish-homebrew.yml | 151 + .../.github/workflows/publish-macports.yml | 243 + .../.github/workflows/publish-nixpkgs.yml | 203 + .../.github/workflows/publish-obs.yml | 185 + .../.github/workflows/publish-packages.yml | 268 ++ .../.github/workflows/publish-scoop.yml | 110 + .../.github/workflows/publish-winget.yml | 180 + .../.github/workflows/push-email-notify.yml | 36 + .../bunsenite/.github/workflows/release.yml | 256 ++ .../bunsenite/.github/workflows/rust-ci.yml | 24 + .../bunsenite/.github/workflows/scorecard.yml | 22 + .../.github/workflows/secret-scanner.yml | 25 + .../.github/workflows/stress-test.yml | 56 + .../.github/workflows/workflow-linter.yml | 58 + .../bunsenite/.github/workflows/zig-ffi.yml | 149 + vendor/bunsenite/.gitignore | 90 + vendor/bunsenite/.gitlab-ci.yml | 325 ++ vendor/bunsenite/.guix-channel | 7 + vendor/bunsenite/.hypatia-ignore | 27 + vendor/bunsenite/.hypatia/activity.jsonl | 1 + vendor/bunsenite/.hypatia/last-visit.json | 6 + .../.machine_readable/6a2/0-AI-MANIFEST.a2ml | 31 + .../.machine_readable/6a2/AGENTIC.a2ml | 29 + .../.machine_readable/6a2/ECOSYSTEM.a2ml | 10 + .../bunsenite/.machine_readable/6a2/META.a2ml | 9 + .../.machine_readable/6a2/NEUROSYM.a2ml | 14 + .../.machine_readable/6a2/PLAYBOOK.a2ml | 23 + .../.machine_readable/6a2/README.adoc | 30 + .../.machine_readable/6a2/STATE.a2ml | 14 + .../6a2/anchor/0-AI-MANIFEST.a2ml | 21 + .../.machine_readable/6a2/anchor/ANCHOR.a2ml | 18 + .../.machine_readable/6a2/anchor/README.adoc | 25 + .../.machine_readable/ADJUST.contractile | 126 + vendor/bunsenite/.machine_readable/CLADE.a2ml | 47 + .../.machine_readable/INTENT.contractile | 72 + .../.machine_readable/MUST.contractile | 91 + .../.machine_readable/TRUST.contractile | 80 + .../bot_directives/README.adoc | 41 + .../bot_directives/coverage.a2ml | 61 + .../bot_directives/debt.a2ml | 49 + .../bot_directives/methodology.a2ml | 107 + .../contractiles/bust/Bustfile.a2ml | 28 + .../contractiles/bust/bust.ncl | 66 + .../contractiles/dust/Dustfile.a2ml | 22 + .../contractiles/trust/Trustfile.a2ml | 50 + .../integrations/feedback-o-tron.a2ml | 13 + .../integrations/proven.a2ml | 18 + .../integrations/verisimdb.a2ml | 15 + .../integrations/vexometer.a2ml | 18 + .../.machine_readable/root-allow.txt | 3 + vendor/bunsenite/.mise.toml | 5 + vendor/bunsenite/.nojekyll | 0 vendor/bunsenite/0-AI-MANIFEST.a2ml | 15 + vendor/bunsenite/ABI-FFI-README.adoc | 409 ++ vendor/bunsenite/ARCHITECTURE.adoc | 48 + vendor/bunsenite/CHANGELOG.adoc | 202 + vendor/bunsenite/CITATION.cff | 24 + vendor/bunsenite/CLAUDE.md | 279 ++ vendor/bunsenite/CODE_OF_CONDUCT.adoc | 175 + vendor/bunsenite/Cargo.lock | 4025 +++++++++++++++++ vendor/bunsenite/Cargo.toml | 123 + vendor/bunsenite/Containerfile | 29 + vendor/bunsenite/EXPLAINME.adoc | 71 + vendor/bunsenite/GEMINI.md | 8 + vendor/bunsenite/GOVERNANCE.adoc | 60 + vendor/bunsenite/Justfile | 230 + vendor/bunsenite/LICENSE | 373 ++ .../bunsenite/LICENSES/AGPL-3.0-or-later.txt | 661 +++ vendor/bunsenite/LICENSES/CC-BY-SA-4.0.txt | 428 ++ vendor/bunsenite/LICENSES/MPL-2.0.txt | 373 ++ vendor/bunsenite/MAINTAINERS | 43 + vendor/bunsenite/MAINTAINERS.adoc | 227 + vendor/bunsenite/Mustfile | 13 + vendor/bunsenite/NOTICE | 22 + vendor/bunsenite/PACKAGING.adoc | 148 + vendor/bunsenite/PALIMPSEST.adoc | 43 + vendor/bunsenite/PROOF-NEEDS.adoc | 41 + vendor/bunsenite/PROVEN-INTEGRATION.adoc | 120 + vendor/bunsenite/PUBLISHING.adoc | 155 + vendor/bunsenite/QUICKSTART-DEV.adoc | 41 + vendor/bunsenite/QUICKSTART-MAINTAINER.adoc | 42 + vendor/bunsenite/QUICKSTART-USER.adoc | 35 + vendor/bunsenite/README.adoc | 329 ++ vendor/bunsenite/ROADMAP.adoc | 167 + vendor/bunsenite/RSR_COMPLIANCE.adoc | 74 + vendor/bunsenite/RSR_OUTLINE.adoc | 220 + vendor/bunsenite/SECURITY.adoc | 235 + vendor/bunsenite/TEST-NEEDS.adoc | 58 + vendor/bunsenite/TOPOLOGY.adoc | 87 + vendor/bunsenite/UPSTREAM-REVISION | 4 + vendor/bunsenite/VENDOR.adoc | 68 + vendor/bunsenite/benches/bunsenite_bench.rs | 187 + vendor/bunsenite/benches/parser.rs | 182 + vendor/bunsenite/bindings/deno/README.adoc | 247 + .../bunsenite/bindings/deno/bunsenite.affine | 302 ++ vendor/bunsenite/bindings/deno/example.affine | 117 + .../bindings/rescript/Bunsenite.affine | 176 + .../bindings/rescript/Bunsenite_test.affine | 184 + .../bindings/rescript/Example.affine | 250 + .../bunsenite/bindings/rescript/README.adoc | 267 ++ .../bindings/rescript/bunsenite.d.affine | 44 + .../bunsenite/bindings/rescript/package.json | 61 + vendor/bunsenite/codemeta.json | 27 + vendor/bunsenite/config/README.adoc | 181 + vendor/bunsenite/config/build.k9.ncl | 105 + vendor/bunsenite/config/rust-fmt.k9.ncl | 101 + vendor/bunsenite/contractile.just | 75 + vendor/bunsenite/contractiles/README.adoc | 21 + vendor/bunsenite/contractiles/dust/Dustfile | 29 + .../contractiles/intend/Intentfile.a2ml | 22 + vendor/bunsenite/contractiles/must/Mustfile | 35 + .../contractiles/self-validating/README.adoc | 179 + .../self-validating/examples/ci-config.k9.ncl | 126 + .../examples/project-metadata.k9.ncl | 57 + .../examples/setup-repo.k9.ncl | 167 + .../self-validating/template-hunt.k9.ncl | 136 + .../self-validating/template-kennel.k9.ncl | 54 + .../self-validating/template-yard.k9.ncl | 84 + .../contractiles/trust/Trustfile.a2ml | 25 + vendor/bunsenite/docs/CITATIONS.adoc | 38 + .../bunsenite/docs/tech-debt-2026-05-26.adoc | 67 + vendor/bunsenite/docs/wiki-home.adoc | 127 + vendor/bunsenite/eclexiaiser.toml | 19 + vendor/bunsenite/examples/config.ncl | 51 + vendor/bunsenite/examples/simple.ncl | 8 + .../bunsenite/examples/web-project-deno.json | 20 + vendor/bunsenite/ffi/zig/build.zig | 95 + vendor/bunsenite/ffi/zig/src/main.zig | 275 ++ .../ffi/zig/test/integration_test.zig | 183 + vendor/bunsenite/fuzz/Cargo.toml | 24 + .../fuzz/fuzz_targets/fuzz_parser.rs | 22 + vendor/bunsenite/hooks/validate-codeql.sh | 34 + .../bunsenite/hooks/validate-permissions.sh | 14 + vendor/bunsenite/hooks/validate-sha-pins.sh | 33 + vendor/bunsenite/hooks/validate-spdx.sh | 25 + vendor/bunsenite/llm-warmup-dev.adoc | 19 + vendor/bunsenite/llm-warmup-user.adoc | 19 + vendor/bunsenite/mise.toml | 57 + vendor/bunsenite/packaging/arch/PKGBUILD | 54 + vendor/bunsenite/packaging/arch/PKGBUILD-bin | 33 + vendor/bunsenite/packaging/aur-ready/.SRCINFO | 17 + vendor/bunsenite/packaging/aur-ready/PKGBUILD | 22 + .../packaging/chocolatey/bunsenite.nuspec | 33 + vendor/bunsenite/packaging/debian/control | 44 + vendor/bunsenite/packaging/debian/rules | 28 + .../com.campaignforcoolercoding.bunsenite.yml | 32 + .../bunsenite/packaging/homebrew/bunsenite.rb | 48 + vendor/bunsenite/packaging/macports/Portfile | 55 + vendor/bunsenite/packaging/rpm/bunsenite.spec | 74 + .../bunsenite/packaging/scoop/bunsenite.json | 23 + .../bunsenite/packaging/winget/bunsenite.yaml | 42 + vendor/bunsenite/papers/arxiv/bunsenite.aux | 52 + vendor/bunsenite/papers/arxiv/bunsenite.out | 26 + vendor/bunsenite/papers/arxiv/bunsenite.pdf | Bin 0 -> 300857 bytes vendor/bunsenite/papers/arxiv/bunsenite.tex | 450 ++ vendor/bunsenite/selur-compose.toml | 17 + vendor/bunsenite/setup-dev-env.k9.ncl | 201 + vendor/bunsenite/setup.sh | 64 + vendor/bunsenite/src/error.rs | 245 + vendor/bunsenite/src/ffi.rs | 237 + vendor/bunsenite/src/lib.rs | 139 + vendor/bunsenite/src/loader.rs | 264 ++ vendor/bunsenite/src/main.rs | 445 ++ vendor/bunsenite/src/schema.rs | 208 + vendor/bunsenite/src/wasm.rs | 151 + vendor/bunsenite/stapeln.toml | 97 + vendor/bunsenite/tests/aspect_test.rs | 247 + vendor/bunsenite/tests/e2e_test.rs | 274 ++ vendor/bunsenite/tests/integration_test.rs | 118 + vendor/bunsenite/tests/property_test.rs | 280 ++ .../bunsenite/validate-nickel-configs.k9.ncl | 148 + vendor/bunsenite/www/.well-known/ai.txt | 140 + vendor/bunsenite/www/.well-known/dc.xml | 23 + vendor/bunsenite/www/.well-known/humans.txt | 205 + vendor/bunsenite/www/.well-known/security.txt | 31 + vendor/bunsenite/zig/README.adoc | 90 + vendor/bunsenite/zig/build.zig | 56 + vendor/bunsenite/zig/bunsenite.zig | 112 + 271 files changed, 33730 insertions(+), 139 deletions(-) create mode 100644 .ci-artifact-pins.txt mode change 100755 => 100644 .clusterfuzzlite/build.sh mode change 100755 => 100644 .github/hooks/validate-a2ml.sh mode change 100755 => 100644 .github/hooks/validate-k9.sh create mode 100644 .github/workflows/arbiter-ci.yml mode change 100755 => 100644 .github/workflows/pages.yml create mode 100644 .github/workflows/slm-real-inference.yml create mode 100644 docs/ARBITER_PROTOCOL.adoc create mode 100644 docs/NICKEL-POLICY.adoc create mode 100644 docs/SLM_PROVIDERS.adoc create mode 100644 docs/UPSTREAM-DELIVERY.adoc mode change 100755 => 100644 hooks/validate-codeql.sh mode change 100755 => 100644 hooks/validate-permissions.sh mode change 100755 => 100644 hooks/validate-sha-pins.sh mode change 100755 => 100644 hooks/validate-spdx.sh mode change 100755 => 100644 scripts/apply-common-files.sh mode change 100755 => 100644 scripts/apply-justfiles.sh mode change 100755 => 100644 scripts/bulk-standardize.sh mode change 100755 => 100644 scripts/mass-apply-templates.sh mode change 100755 => 100644 scripts/reconcile-wharf-repos.sh mode change 100755 => 100644 setup.sh create mode 100644 src/arbiter/config/runtime.exs create mode 100644 src/arbiter/lib/conative_gating/arbiter_protocol.ex create mode 100644 src/arbiter/lib/conative_gating/audit_log.ex create mode 100644 src/arbiter/lib/conative_gating/cli.ex create mode 100644 src/arbiter/lib/conative_gating/protocol_server.ex create mode 100644 src/arbiter/test/arbiter_protocol_test.exs create mode 100644 src/arbiter/test/audit_log_test.exs create mode 100644 src/arbiter/test/consensus_arbiter_test.exs create mode 100644 src/arbiter/test/protocol_server_test.exs create mode 100644 src/arbiter/test/test_helper.exs create mode 100644 src/contract/src/arbiter.rs create mode 100644 src/oracle/src/nickel.rs create mode 100644 src/slm/src/http.rs create mode 100644 src/slm/src/provider.rs create mode 100644 src/slm/tests/real_inference.rs create mode 100644 tests/generative_test.rs create mode 100644 vendor/bunsenite/.claude/CLAUDE.md create mode 100644 vendor/bunsenite/.clusterfuzzlite/Containerfile create mode 100644 vendor/bunsenite/.clusterfuzzlite/build.sh create mode 100644 vendor/bunsenite/.clusterfuzzlite/project.yaml create mode 100644 vendor/bunsenite/.editorconfig create mode 100644 vendor/bunsenite/.gitattributes create mode 100644 vendor/bunsenite/.github/CODEOWNERS create mode 100644 vendor/bunsenite/.github/CONTRIBUTING.md create mode 100644 vendor/bunsenite/.github/FUNDING.yml create mode 100644 vendor/bunsenite/.github/copilot/coding-agent.yml create mode 100644 vendor/bunsenite/.github/dependabot.yml create mode 100644 vendor/bunsenite/.github/funding.yml create mode 100644 vendor/bunsenite/.github/label-classifier.json create mode 100644 vendor/bunsenite/.github/labels.json create mode 100644 vendor/bunsenite/.github/scripts/classify-issue.jq create mode 100644 vendor/bunsenite/.github/workflows/actions.lock create mode 100644 vendor/bunsenite/.github/workflows/boj-build.yml create mode 100644 vendor/bunsenite/.github/workflows/cargo-audit.yml create mode 100644 vendor/bunsenite/.github/workflows/casket-pages.yml create mode 100644 vendor/bunsenite/.github/workflows/cflite_batch.yml create mode 100644 vendor/bunsenite/.github/workflows/cflite_pr.yml create mode 100644 vendor/bunsenite/.github/workflows/codeql.yml create mode 100644 vendor/bunsenite/.github/workflows/dependabot-automerge.yml create mode 100644 vendor/bunsenite/.github/workflows/dogfood-gate.yml create mode 100644 vendor/bunsenite/.github/workflows/generator-generic-ossf-slsa3-publish.yml create mode 100644 vendor/bunsenite/.github/workflows/ghcr-publish.yml create mode 100644 vendor/bunsenite/.github/workflows/governance.yml create mode 100644 vendor/bunsenite/.github/workflows/hypatia-scan.yml create mode 100644 vendor/bunsenite/.github/workflows/instant-sync.yml create mode 100644 vendor/bunsenite/.github/workflows/label-triage.yml create mode 100644 vendor/bunsenite/.github/workflows/labels.yml create mode 100644 vendor/bunsenite/.github/workflows/mirror.yml create mode 100644 vendor/bunsenite/.github/workflows/pages.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-aur.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-chocolatey.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-container.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-copr.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-debian-ppa.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-flatpak.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-homebrew.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-macports.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-nixpkgs.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-obs.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-packages.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-scoop.yml create mode 100644 vendor/bunsenite/.github/workflows/publish-winget.yml create mode 100644 vendor/bunsenite/.github/workflows/push-email-notify.yml create mode 100644 vendor/bunsenite/.github/workflows/release.yml create mode 100644 vendor/bunsenite/.github/workflows/rust-ci.yml create mode 100644 vendor/bunsenite/.github/workflows/scorecard.yml create mode 100644 vendor/bunsenite/.github/workflows/secret-scanner.yml create mode 100644 vendor/bunsenite/.github/workflows/stress-test.yml create mode 100644 vendor/bunsenite/.github/workflows/workflow-linter.yml create mode 100644 vendor/bunsenite/.github/workflows/zig-ffi.yml create mode 100644 vendor/bunsenite/.gitignore create mode 100644 vendor/bunsenite/.gitlab-ci.yml create mode 100644 vendor/bunsenite/.guix-channel create mode 100644 vendor/bunsenite/.hypatia-ignore create mode 100644 vendor/bunsenite/.hypatia/activity.jsonl create mode 100644 vendor/bunsenite/.hypatia/last-visit.json create mode 100644 vendor/bunsenite/.machine_readable/6a2/0-AI-MANIFEST.a2ml create mode 100644 vendor/bunsenite/.machine_readable/6a2/AGENTIC.a2ml create mode 100644 vendor/bunsenite/.machine_readable/6a2/ECOSYSTEM.a2ml create mode 100644 vendor/bunsenite/.machine_readable/6a2/META.a2ml create mode 100644 vendor/bunsenite/.machine_readable/6a2/NEUROSYM.a2ml create mode 100644 vendor/bunsenite/.machine_readable/6a2/PLAYBOOK.a2ml create mode 100644 vendor/bunsenite/.machine_readable/6a2/README.adoc create mode 100644 vendor/bunsenite/.machine_readable/6a2/STATE.a2ml create mode 100644 vendor/bunsenite/.machine_readable/6a2/anchor/0-AI-MANIFEST.a2ml create mode 100644 vendor/bunsenite/.machine_readable/6a2/anchor/ANCHOR.a2ml create mode 100644 vendor/bunsenite/.machine_readable/6a2/anchor/README.adoc create mode 100644 vendor/bunsenite/.machine_readable/ADJUST.contractile create mode 100644 vendor/bunsenite/.machine_readable/CLADE.a2ml create mode 100644 vendor/bunsenite/.machine_readable/INTENT.contractile create mode 100644 vendor/bunsenite/.machine_readable/MUST.contractile create mode 100644 vendor/bunsenite/.machine_readable/TRUST.contractile create mode 100644 vendor/bunsenite/.machine_readable/bot_directives/README.adoc create mode 100644 vendor/bunsenite/.machine_readable/bot_directives/coverage.a2ml create mode 100644 vendor/bunsenite/.machine_readable/bot_directives/debt.a2ml create mode 100644 vendor/bunsenite/.machine_readable/bot_directives/methodology.a2ml create mode 100644 vendor/bunsenite/.machine_readable/contractiles/bust/Bustfile.a2ml create mode 100644 vendor/bunsenite/.machine_readable/contractiles/bust/bust.ncl create mode 100644 vendor/bunsenite/.machine_readable/contractiles/dust/Dustfile.a2ml create mode 100644 vendor/bunsenite/.machine_readable/contractiles/trust/Trustfile.a2ml create mode 100644 vendor/bunsenite/.machine_readable/integrations/feedback-o-tron.a2ml create mode 100644 vendor/bunsenite/.machine_readable/integrations/proven.a2ml create mode 100644 vendor/bunsenite/.machine_readable/integrations/verisimdb.a2ml create mode 100644 vendor/bunsenite/.machine_readable/integrations/vexometer.a2ml create mode 100644 vendor/bunsenite/.machine_readable/root-allow.txt create mode 100644 vendor/bunsenite/.mise.toml create mode 100644 vendor/bunsenite/.nojekyll create mode 100644 vendor/bunsenite/0-AI-MANIFEST.a2ml create mode 100644 vendor/bunsenite/ABI-FFI-README.adoc create mode 100644 vendor/bunsenite/ARCHITECTURE.adoc create mode 100644 vendor/bunsenite/CHANGELOG.adoc create mode 100644 vendor/bunsenite/CITATION.cff create mode 100644 vendor/bunsenite/CLAUDE.md create mode 100644 vendor/bunsenite/CODE_OF_CONDUCT.adoc create mode 100644 vendor/bunsenite/Cargo.lock create mode 100644 vendor/bunsenite/Cargo.toml create mode 100644 vendor/bunsenite/Containerfile create mode 100644 vendor/bunsenite/EXPLAINME.adoc create mode 100644 vendor/bunsenite/GEMINI.md create mode 100644 vendor/bunsenite/GOVERNANCE.adoc create mode 100644 vendor/bunsenite/Justfile create mode 100644 vendor/bunsenite/LICENSE create mode 100644 vendor/bunsenite/LICENSES/AGPL-3.0-or-later.txt create mode 100644 vendor/bunsenite/LICENSES/CC-BY-SA-4.0.txt create mode 100644 vendor/bunsenite/LICENSES/MPL-2.0.txt create mode 100644 vendor/bunsenite/MAINTAINERS create mode 100644 vendor/bunsenite/MAINTAINERS.adoc create mode 100644 vendor/bunsenite/Mustfile create mode 100644 vendor/bunsenite/NOTICE create mode 100644 vendor/bunsenite/PACKAGING.adoc create mode 100644 vendor/bunsenite/PALIMPSEST.adoc create mode 100644 vendor/bunsenite/PROOF-NEEDS.adoc create mode 100644 vendor/bunsenite/PROVEN-INTEGRATION.adoc create mode 100644 vendor/bunsenite/PUBLISHING.adoc create mode 100644 vendor/bunsenite/QUICKSTART-DEV.adoc create mode 100644 vendor/bunsenite/QUICKSTART-MAINTAINER.adoc create mode 100644 vendor/bunsenite/QUICKSTART-USER.adoc create mode 100644 vendor/bunsenite/README.adoc create mode 100644 vendor/bunsenite/ROADMAP.adoc create mode 100644 vendor/bunsenite/RSR_COMPLIANCE.adoc create mode 100644 vendor/bunsenite/RSR_OUTLINE.adoc create mode 100644 vendor/bunsenite/SECURITY.adoc create mode 100644 vendor/bunsenite/TEST-NEEDS.adoc create mode 100644 vendor/bunsenite/TOPOLOGY.adoc create mode 100644 vendor/bunsenite/UPSTREAM-REVISION create mode 100644 vendor/bunsenite/VENDOR.adoc create mode 100644 vendor/bunsenite/benches/bunsenite_bench.rs create mode 100644 vendor/bunsenite/benches/parser.rs create mode 100644 vendor/bunsenite/bindings/deno/README.adoc create mode 100644 vendor/bunsenite/bindings/deno/bunsenite.affine create mode 100644 vendor/bunsenite/bindings/deno/example.affine create mode 100644 vendor/bunsenite/bindings/rescript/Bunsenite.affine create mode 100644 vendor/bunsenite/bindings/rescript/Bunsenite_test.affine create mode 100644 vendor/bunsenite/bindings/rescript/Example.affine create mode 100644 vendor/bunsenite/bindings/rescript/README.adoc create mode 100644 vendor/bunsenite/bindings/rescript/bunsenite.d.affine create mode 100644 vendor/bunsenite/bindings/rescript/package.json create mode 100644 vendor/bunsenite/codemeta.json create mode 100644 vendor/bunsenite/config/README.adoc create mode 100644 vendor/bunsenite/config/build.k9.ncl create mode 100644 vendor/bunsenite/config/rust-fmt.k9.ncl create mode 100644 vendor/bunsenite/contractile.just create mode 100644 vendor/bunsenite/contractiles/README.adoc create mode 100644 vendor/bunsenite/contractiles/dust/Dustfile create mode 100644 vendor/bunsenite/contractiles/intend/Intentfile.a2ml create mode 100644 vendor/bunsenite/contractiles/must/Mustfile create mode 100644 vendor/bunsenite/contractiles/self-validating/README.adoc create mode 100644 vendor/bunsenite/contractiles/self-validating/examples/ci-config.k9.ncl create mode 100644 vendor/bunsenite/contractiles/self-validating/examples/project-metadata.k9.ncl create mode 100644 vendor/bunsenite/contractiles/self-validating/examples/setup-repo.k9.ncl create mode 100644 vendor/bunsenite/contractiles/self-validating/template-hunt.k9.ncl create mode 100644 vendor/bunsenite/contractiles/self-validating/template-kennel.k9.ncl create mode 100644 vendor/bunsenite/contractiles/self-validating/template-yard.k9.ncl create mode 100644 vendor/bunsenite/contractiles/trust/Trustfile.a2ml create mode 100644 vendor/bunsenite/docs/CITATIONS.adoc create mode 100644 vendor/bunsenite/docs/tech-debt-2026-05-26.adoc create mode 100644 vendor/bunsenite/docs/wiki-home.adoc create mode 100644 vendor/bunsenite/eclexiaiser.toml create mode 100644 vendor/bunsenite/examples/config.ncl create mode 100644 vendor/bunsenite/examples/simple.ncl create mode 100644 vendor/bunsenite/examples/web-project-deno.json create mode 100644 vendor/bunsenite/ffi/zig/build.zig create mode 100644 vendor/bunsenite/ffi/zig/src/main.zig create mode 100644 vendor/bunsenite/ffi/zig/test/integration_test.zig create mode 100644 vendor/bunsenite/fuzz/Cargo.toml create mode 100644 vendor/bunsenite/fuzz/fuzz_targets/fuzz_parser.rs create mode 100644 vendor/bunsenite/hooks/validate-codeql.sh create mode 100644 vendor/bunsenite/hooks/validate-permissions.sh create mode 100644 vendor/bunsenite/hooks/validate-sha-pins.sh create mode 100644 vendor/bunsenite/hooks/validate-spdx.sh create mode 100644 vendor/bunsenite/llm-warmup-dev.adoc create mode 100644 vendor/bunsenite/llm-warmup-user.adoc create mode 100644 vendor/bunsenite/mise.toml create mode 100644 vendor/bunsenite/packaging/arch/PKGBUILD create mode 100644 vendor/bunsenite/packaging/arch/PKGBUILD-bin create mode 100644 vendor/bunsenite/packaging/aur-ready/.SRCINFO create mode 100644 vendor/bunsenite/packaging/aur-ready/PKGBUILD create mode 100644 vendor/bunsenite/packaging/chocolatey/bunsenite.nuspec create mode 100644 vendor/bunsenite/packaging/debian/control create mode 100644 vendor/bunsenite/packaging/debian/rules create mode 100644 vendor/bunsenite/packaging/flatpak/com.campaignforcoolercoding.bunsenite.yml create mode 100644 vendor/bunsenite/packaging/homebrew/bunsenite.rb create mode 100644 vendor/bunsenite/packaging/macports/Portfile create mode 100644 vendor/bunsenite/packaging/rpm/bunsenite.spec create mode 100644 vendor/bunsenite/packaging/scoop/bunsenite.json create mode 100644 vendor/bunsenite/packaging/winget/bunsenite.yaml create mode 100644 vendor/bunsenite/papers/arxiv/bunsenite.aux create mode 100644 vendor/bunsenite/papers/arxiv/bunsenite.out create mode 100644 vendor/bunsenite/papers/arxiv/bunsenite.pdf create mode 100644 vendor/bunsenite/papers/arxiv/bunsenite.tex create mode 100644 vendor/bunsenite/selur-compose.toml create mode 100644 vendor/bunsenite/setup-dev-env.k9.ncl create mode 100644 vendor/bunsenite/setup.sh create mode 100644 vendor/bunsenite/src/error.rs create mode 100644 vendor/bunsenite/src/ffi.rs create mode 100644 vendor/bunsenite/src/lib.rs create mode 100644 vendor/bunsenite/src/loader.rs create mode 100644 vendor/bunsenite/src/main.rs create mode 100644 vendor/bunsenite/src/schema.rs create mode 100644 vendor/bunsenite/src/wasm.rs create mode 100644 vendor/bunsenite/stapeln.toml create mode 100644 vendor/bunsenite/tests/aspect_test.rs create mode 100644 vendor/bunsenite/tests/e2e_test.rs create mode 100644 vendor/bunsenite/tests/integration_test.rs create mode 100644 vendor/bunsenite/tests/property_test.rs create mode 100644 vendor/bunsenite/validate-nickel-configs.k9.ncl create mode 100644 vendor/bunsenite/www/.well-known/ai.txt create mode 100644 vendor/bunsenite/www/.well-known/dc.xml create mode 100644 vendor/bunsenite/www/.well-known/humans.txt create mode 100644 vendor/bunsenite/www/.well-known/security.txt create mode 100644 vendor/bunsenite/zig/README.adoc create mode 100644 vendor/bunsenite/zig/build.zig create mode 100644 vendor/bunsenite/zig/bunsenite.zig diff --git a/.ci-artifact-pins.txt b/.ci-artifact-pins.txt new file mode 100644 index 0000000..2126dd3 --- /dev/null +++ b/.ci-artifact-pins.txt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: MPL-2.0 +# Pinned artifacts for the slm-real-inference CI job (SHA-256 verified at download time). +# llama.cpp nightly binary (ggml-org/llama.cpp release b11100, 2026-09-22): +LLAMA_CPP_TAG=b11100 +LLAMA_CPP_UBUNTU_X64_URL=https://github.com/ggml-org/llama.cpp/releases/download/b11100/llama-b11100-bin-ubuntu-x64.tar.gz +LLAMA_CPP_UBUNTU_X64_SHA256=a836c913236ab4533ef9aaf49f0e1ad2955c8159869d7eec1a9072a92e13d61b +# Smoke model: Qwen2.5-0.5B-Instruct Q4_K_M (official Qwen repo, commit 9217f5db79a29953eb74d5343926648285ec7e67). +# Chosen over SmolLM2-135M-Instruct Q4_K_M (bartowski mirror) after on-runner evaluation: +# the 135M model loops `0.000000...` at temp 0 and never closes the verdict JSON within +# the token budget; Qwen2.5-0.5B emits a valid object on the first attempt. +GGUF_MODEL_URL=https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q4_k_m.gguf +GGUF_MODEL_SHA256=74a4da8c9fdbcd15bd1f6d01d621410d31c6fc00986f5eb687824e7b93d7a9db diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh old mode 100755 new mode 100644 diff --git a/.github/hooks/validate-a2ml.sh b/.github/hooks/validate-a2ml.sh old mode 100755 new mode 100644 diff --git a/.github/hooks/validate-k9.sh b/.github/hooks/validate-k9.sh old mode 100755 new mode 100644 diff --git a/.github/workflows/arbiter-ci.yml b/.github/workflows/arbiter-ci.yml new file mode 100644 index 0000000..3e36898 --- /dev/null +++ b/.github/workflows/arbiter-ci.yml @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: MPL-2.0 +name: Arbiter CI +on: + push: + branches: [main, master] + paths: + - "src/arbiter/**" + - "src/contract/src/arbiter.rs" + - ".github/workflows/arbiter-ci.yml" + pull_request: + branches: [main, master] + paths: + - "src/arbiter/**" + - ".github/workflows/arbiter-ci.yml" +permissions: + contents: read +jobs: + elixir: + name: OTP arbiter (format, deps, ExUnit, escript smoke) + runs-on: ubuntu-latest + timeout-minutes: 25 + permissions: + contents: read + defaults: + run: + working-directory: src/arbiter + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + otp-version: "27" + elixir-version: "1.18" + - name: Install build tooling + run: mix local.hex --force && mix local.rebar --force + - name: Check formatting + run: mix format --check-formatted + # NOTE: no mix.lock is committed yet (cannot be generated without a + # local OTP toolchain). deps are resolved fresh; lockfile to be added + # by a maintainer — see docs/UPSTREAM-DELIVERY.adoc. + - name: Fetch dependencies + run: mix deps.get + - name: Run ExUnit suite + run: mix test + - name: Build escript + run: mix escript.build + - name: Protocol smoke — valid allow round-trip, exactly one audit record + run: | + set -euo pipefail + AUDIT_DIR="$(mktemp -d)" + export CONATIVE_AUDIT_PATH="$AUDIT_DIR/audit.jsonl" + RESPONSE="$(printf '%s\n' \ + '{"protocol_version":1,"request_id":"smoke-1","llm":{"confidence":0.95},"slm":{"violation_confidence":0.05},"oracle":{"verdict":"allow"}}' \ + | ./conative_arbiter)" + echo "response: $RESPONSE" + echo "$RESPONSE" | grep -q '"verdict":"allow"' + echo "$RESPONSE" | grep -q '"request_id":"smoke-1"' + echo "$RESPONSE" | grep -q '"audit_recorded":true' + test "$(wc -l < "$CONATIVE_AUDIT_PATH")" -eq 1 + - name: Protocol smoke — malformed input fails closed (error, never a verdict) + run: | + set -euo pipefail + AUDIT_DIR="$(mktemp -d)" + export CONATIVE_AUDIT_PATH="$AUDIT_DIR/audit.jsonl" + RESPONSE="$(printf '%s\n' 'not json' | ./conative_arbiter || true)" + echo "response: $RESPONSE" + echo "$RESPONSE" | grep -q '"error"' + ! echo "$RESPONSE" | grep -q '"verdict"' + test ! -s "$CONATIVE_AUDIT_PATH" || test "$(wc -l < "$CONATIVE_AUDIT_PATH")" -eq 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7f8543..ad14267 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,3 +73,22 @@ jobs: with: name: conative-cli path: target/release/conative + nickel-native: + name: Native Nickel policy backend + # Dedicated job: nickel-lang-parser does not link-compile under ~2 GB + # (OOM-killed, reproduced), so this must run on a full-size hosted + # runner (16 GB). The exact command is part of the delivery contract — + # do not "optimise" it (single job, no debuginfo, warnings as errors, + # lib tests only, locked dependency set). + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + with: + key: nickel-native + - name: Test policy-oracle with native Nickel (pinned command) + run: CARGO_BUILD_JOBS=1 RUSTFLAGS="-C debuginfo=0 -Dwarnings" cargo test -p policy-oracle --features nickel --lib --locked diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml old mode 100755 new mode 100644 diff --git a/.github/workflows/slm-real-inference.yml b/.github/workflows/slm-real-inference.yml new file mode 100644 index 0000000..9e21083 --- /dev/null +++ b/.github/workflows/slm-real-inference.yml @@ -0,0 +1,116 @@ +# SPDX-License-Identifier: MPL-2.0 +name: SLM Real Inference +# Real-model smoke tests. Artifacts are pinned + SHA-256 verified and are +# never committed to the repository (see .ci-artifact-pins.txt). +# +# - `local-gguf`: pinned llama.cpp binary + pinned Qwen2.5-0.5B GGUF, runs the +# ignored llama-cli round-trip AND the HTTP adapter against a loopback +# llama-server. Safe on every PR: no secrets involved. +# - `remote-provider`: the live remote-provider smoke. Runs ONLY on +# workflow_dispatch or pushes to the protected main branch of the upstream +# repository, inside the `slm-remote-production` GitHub Environment (which +# holds CONATIVE_SLM_ENDPOINT / CONATIVE_SLM_MODEL_NAME / SLM_API_KEY and +# requires reviewer approval). Never on pull requests — fork PR code must +# never see these secrets. +on: + pull_request: + branches: [main, master] + push: + branches: [main, master] + workflow_dispatch: +permissions: + contents: read +env: + CARGO_TERM_COLOR: always +jobs: + local-gguf: + name: Local GGUF (pinned llama.cpp + pinned model) + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + env: + LLAMA_CPP_UBUNTU_X64_URL: https://github.com/ggml-org/llama.cpp/releases/download/b11100/llama-b11100-bin-ubuntu-x64.tar.gz + LLAMA_CPP_UBUNTU_X64_SHA256: a836c913236ab4533ef9aaf49f0e1ad2955c8159869d7eec1a9072a92e13d61b + GGUF_MODEL_URL: https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q4_k_m.gguf + GGUF_MODEL_SHA256: 74a4da8c9fdbcd15bd1f6d01d621410d31c6fc00986f5eb687824e7b93d7a9db + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + with: + key: slm-real-inference + - name: Cache pinned artifacts + id: artifacts + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ${{ runner.temp }}/slm-artifacts + key: slm-artifacts-${{ env.LLAMA_CPP_UBUNTU_X64_SHA256 }}-${{ env.GGUF_MODEL_SHA256 }} + - name: Download + verify llama.cpp binary + if: steps.artifacts.outputs.cache-hit != 'true' + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/slm-artifacts" + cd "$RUNNER_TEMP/slm-artifacts" + curl -fsSL --retry 3 -o llama-cli.tar.gz "$LLAMA_CPP_UBUNTU_X64_URL" + echo "$LLAMA_CPP_UBUNTU_X64_SHA256 llama-cli.tar.gz" | sha256sum -c - + mkdir -p bin && tar xzf llama-cli.tar.gz -C bin --strip-components=1 + - name: Download + verify GGUF model + if: steps.artifacts.outputs.cache-hit != 'true' + run: | + set -euo pipefail + cd "$RUNNER_TEMP/slm-artifacts" + curl -fsSL --retry 3 -o model.gguf "$GGUF_MODEL_URL" + echo "$GGUF_MODEL_SHA256 model.gguf" | sha256sum -c - + - name: llama-cli round-trip (real model, ignored-by-default test) + run: | + set -euo pipefail + export LD_LIBRARY_PATH="$RUNNER_TEMP/slm-artifacts/bin" + export CONATIVE_LLAMA_CLI="$RUNNER_TEMP/slm-artifacts/bin/llama-cli" + export CONATIVE_GGUF_MODEL="$RUNNER_TEMP/slm-artifacts/model.gguf" + cargo test -p slm-evaluator --test real_inference real_llama -- --ignored --nocapture + - name: HTTP adapter round-trip via loopback llama-server + run: | + set -euo pipefail + export LD_LIBRARY_PATH="$RUNNER_TEMP/slm-artifacts/bin" + "$RUNNER_TEMP/slm-artifacts/bin/llama-server" \ + -m "$RUNNER_TEMP/slm-artifacts/model.gguf" \ + --host 127.0.0.1 --port 18080 -t 4 -c 2048 --log-disable & + SERVER_PID=$! + trap 'kill $SERVER_PID 2>/dev/null || true' EXIT + for i in $(seq 1 120); do + curl -fsS http://127.0.0.1:18080/health >/dev/null 2>&1 && break + sleep 1 + done + export CONATIVE_SLM_ENDPOINT=http://127.0.0.1:18080 + cargo test -p slm-evaluator --features http --test real_inference real_http -- --ignored --nocapture + remote-provider: + name: Remote provider (protected environment, approval-gated) + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + environment: slm-remote-production + # Never on pull requests (fork code must never touch secrets); never on + # forks of the repository. Only dispatched runs or protected main pushes. + if: >- + github.repository == 'hyperpolymath/conative-gating' && + (github.event_name == 'workflow_dispatch' || + (github.event_name == 'push' && github.ref == 'refs/heads/main')) + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + with: + key: slm-remote-provider + - name: Remote provider round-trip (creds from Environment secrets) + env: + CONATIVE_SLM_ENDPOINT: ${{ secrets.CONATIVE_SLM_ENDPOINT }} + CONATIVE_SLM_MODEL_NAME: ${{ secrets.CONATIVE_SLM_MODEL_NAME }} + SLM_API_KEY: ${{ secrets.SLM_API_KEY }} + run: | + set -euo pipefail + : "${CONATIVE_SLM_ENDPOINT:?set in the slm-remote-production environment}" + : "${SLM_API_KEY:?set in the slm-remote-production environment}" + cargo test -p slm-evaluator --features http --test real_inference real_http -- --ignored --nocapture diff --git a/.gitignore b/.gitignore index 3846b37..2294218 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,10 @@ Thumbs.db # Dependencies /node_modules/ -/vendor/ +# /vendor/ is ignored except the reviewed Bunsenite vendor fork used by the +# `nickel` feature (see vendor/bunsenite/VENDOR.adoc) +/vendor/* +!/vendor/bunsenite/ /deps/ /.elixir_ls/ diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 32015d6..4e53bbd 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -18,6 +18,27 @@ https://semver.org/spec/v2.0.0.html[Semantic Versioning]. ==== Added +* feat(oracle): native Nickel policy backend via vendored Bunsenite + (`nickel` feature, default-features-off `nickel-lang-core 0.18.0`), + fail-closed `import` rejection, `Policy::from_policy_file` dispatch — + see `docs/NICKEL-POLICY.adoc` +* feat(slm): provider layer with verdict contract + correlation + preservation; `LlamaCppProvider` (pinned llama.cpp CLI, `--single-turn` + hardened) and feature-gated `HttpSlmProvider` (`http`, https-or-loopback, + `SLM_API_KEY` env-only) — see `docs/SLM_PROVIDERS.adoc` +* feat(contract): `ContractRunner::evaluate_with_provider` — terminal + oracle blocks, asymmetric Warn addend, threshold matrix, fail-closed + provider errors (`Sys902` Escalate) +* feat(arbiter): OTP consensus arbiter escript (protocol v1) with durable + JSONL audit sink (flush-before-ack, rotation, fail-closed, bounded + history, no proposal content) — see `docs/ARBITER_PROTOCOL.adoc` +* feat(contract): Rust arbiter client enforcing `audit_recorded: true` +* test(slm): env-gated real-inference smokes (local GGUF + HTTP adapter) + with pinned, SHA-256-verified CI artifacts +* test(contract): generative proptest suite (terminality, thresholds, + fail-closed, determinism, correlation, concurrency) +* ci: `nickel-native` job (exact pinned command), `arbiter-ci` workflow, + `slm-real-inference` workflow with approval-gated remote-provider job * feat(crg): add crg-grade and crg-badge justfile recipes * feat: add stapeln.toml container definition * feat: deploy UX Manifesto infrastructure diff --git a/Cargo.lock b/Cargo.lock index e96911d..a9494c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -11,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "alloca" version = "0.4.0" @@ -37,9 +58,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -58,9 +79,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -91,17 +112,104 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[package]] +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "bumpalo" @@ -109,6 +217,26 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "bunsenite" +version = "1.0.2" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "miette", + "nickel-lang-core", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + [[package]] name = "bytes" version = "1.11.1" @@ -137,6 +265,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.42" @@ -180,9 +325,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -190,9 +335,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -200,7 +345,7 @@ dependencies = [ "strsim", "terminal_size", "unicase", - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -214,14 +359,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -240,6 +385,27 @@ dependencies = [ "roff", ] +[[package]] +name = "codespan" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583f52b0658b321b25fd6b209b6c76cf058f433071297de64e5980c3d9aad937" +dependencies = [ + "codespan-reporting", + "serde", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width 0.2.2", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -257,19 +423,49 @@ dependencies = [ "criterion", "gating-contract", "policy-oracle", + "proptest", "serde", "serde_json", + "slm-evaluator", "tracing", "tracing-subscriber", "uuid", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[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 = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "criterion" version = "0.8.2" @@ -282,7 +478,7 @@ dependencies = [ "ciborium", "clap", "criterion-plot", - "itertools", + "itertools 0.13.0", "num-traits", "oorandom", "page_size", @@ -302,7 +498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", - "itertools", + "itertools 0.13.0", ] [[package]] @@ -336,12 +532,52 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[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 = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -358,18 +594,100 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "find-msvc-tools" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "gating-contract" version = "0.1.0" @@ -379,12 +697,47 @@ dependencies = [ "serde", "serde_json", "slm-evaluator", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", "uuid", ] +[[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.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[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 = "getrandom" version = "0.4.1" @@ -392,12 +745,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", + "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" version = "0.3.3" @@ -421,7 +783,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -429,6 +791,24 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" @@ -437,36 +817,292 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "iana-time-zone" -version = "0.1.64" +name = "http" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", + "bytes", + "itoa", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "http-body" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ - "cc", + "bytes", + "http", ] [[package]] -name = "id-arena" -version = "2.3.0" +name = "http-body-util" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +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 = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8" + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d07aafccd67af15d02512a6adf5896fbc5ed00f2e99b471d2efa14016db3db" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" +dependencies = [ + "bitmaps", +] + +[[package]] name = "indexmap" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -478,6 +1114,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -493,6 +1150,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -509,6 +1175,56 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json_scanner" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0a2dc336065c75719cffd3c6c929e0ec4ed85b92b8248a7bbd999acb0e419c" +dependencies = [ + "memchr", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.14.0", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata", + "rustversion", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -523,15 +1239,27 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -548,12 +1276,162 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn 2.0.119", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "malachite" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba00455c89cf785ef73a0dfc941ab3c21211963c86130c0bd48d7994b942707" +dependencies = [ + "malachite-base", + "malachite-float", + "malachite-nz", + "malachite-q", +] + +[[package]] +name = "malachite-base" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f44099731f17094b07825c88ccb5fbd1bfa1f82fafff7daa33e8b8652db16e" +dependencies = [ + "hashbrown 0.16.1", + "itertools 0.14.0", + "libm", + "ryu", +] + +[[package]] +name = "malachite-float" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23982acc6f68aa384504a44d112f42fc82207035272f23485220a861e2cf1af" +dependencies = [ + "itertools 0.14.0", + "malachite-base", + "malachite-nz", + "malachite-q", + "serde", +] + +[[package]] +name = "malachite-nz" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a137660cdba20f136c8a223125f08088adb4e0b72fbb8466f08c43e31cc0427d" +dependencies = [ + "itertools 0.14.0", + "libm", + "malachite-base", + "serde", + "wide", +] + +[[package]] +name = "malachite-q" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffcbeed95e34c0fcc3864ccd146e129cbbf7de1513d3afbcfb47c7674c82d94" +dependencies = [ + "itertools 0.14.0", + "libm", + "malachite-base", + "malachite-nz", + "serde", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.1.1" @@ -565,6 +1443,91 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nickel-lang-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692d8a2ba34c633bc37e704dc94f4ca33edaa8fbf6d08efdcadb81db333ccdb6" +dependencies = [ + "base64", + "bumpalo", + "codespan", + "codespan-reporting", + "colorchoice", + "indexmap", + "indoc", + "json_scanner", + "lalrpop", + "lalrpop-util", + "logos", + "malachite", + "malachite-q", + "md-5", + "nickel-lang-parser", + "nickel-lang-vector", + "once_cell", + "ouroboros", + "paste", + "pretty", + "regex", + "saphyr-parser", + "serde", + "serde_json", + "serde_yaml", + "sha-1", + "sha2", + "simple-counter", + "smallvec", + "strip-ansi-escapes", + "strsim", + "toml", + "toml_edit", + "typed-arena", + "unicode-segmentation", +] + +[[package]] +name = "nickel-lang-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7aaf73e60b66ef4fffc969b0e4e419a15a029525f9b53f2f5cc0ca41bbe17ff" +dependencies = [ + "bumpalo", + "codespan", + "codespan-reporting", + "indexmap", + "lalrpop", + "lalrpop-util", + "logos", + "malachite", + "nickel-lang-vector", + "ouroboros", + "pretty", + "regex", + "saphyr-parser", + "serde", + "serde_json", + "simple-counter", + "toml_edit", + "typed-arena", +] + +[[package]] +name = "nickel-lang-vector" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f243832286908d8873add24a905d6732ffabd6cfb2bf74cb18d667e892e279" +dependencies = [ + "imbl-sized-chunks", + "serde", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -583,6 +1546,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -601,6 +1573,36 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "owo-colors" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" + [[package]] name = "page_size" version = "0.6.0" @@ -634,6 +1636,43 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -641,80 +1680,276 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] -name = "plotters" -version = "0.3.7" +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "policy-oracle" +version = "0.1.0" +dependencies = [ + "bunsenite", + "glob", + "regex", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width 0.2.2", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "version_check", + "yansi", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.5", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" +dependencies = [ + "bytes", + "getrandom 0.4.1", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", + "proc-macro2", ] [[package]] -name = "plotters-backend" -version = "0.3.7" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "plotters-svg" -version = "0.3.7" +name = "rand" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "plotters-backend", + "rand_chacha", + "rand_core 0.9.5", ] [[package]] -name = "policy-oracle" -version = "0.1.0" +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ - "glob", - "regex", - "serde", - "serde_json", - "thiserror", - "tokio", - "tracing", - "uuid", + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.1", ] [[package]] -name = "prettyplease" -version = "0.2.37" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ - "proc-macro2", - "syn", + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] -name = "proc-macro2" -version = "1.0.103" +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "unicode-ident", + "getrandom 0.3.4", ] [[package]] -name = "quote" -version = "1.0.42" +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "proc-macro2", + "rand_core 0.10.1", ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "rand_xorshift" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] [[package]] name = "rayon" @@ -774,17 +2009,83 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "roff" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ "bitflags", "errno", @@ -793,12 +2094,74 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safe_arch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c6efa15875e6ecb39ca61fb0b0c1a40b84fac5a5ffe71eef7d1000c8eb3f5f" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -808,6 +2171,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "saphyr-parser" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb771b59f6b1985d1406325ec28f97cfb14256abcec4fdfb37b36a1766d6af7" +dependencies = [ + "arraydeque", + "hashlink", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -847,7 +2220,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -863,6 +2236,72 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -888,13 +2327,32 @@ dependencies = [ "libc", ] +[[package]] +name = "simple-counter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb57743b52ea059937169c0061d70298fe2df1d2c988b44caae79dd979d9b49" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "slm-evaluator" version = "0.1.0" dependencies = [ + "reqwest", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", "uuid", @@ -916,23 +2374,145 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" -version = "2.0.111" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "term" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.4.3" @@ -943,13 +2523,43 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "textwrap" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49" +dependencies = [ + "icu_segmenter", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[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 2.0.119", ] [[package]] @@ -960,7 +2570,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -972,6 +2582,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -982,6 +2603,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "tokio" version = "1.50.0" @@ -1007,9 +2634,116 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.24.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01f2eadbbc6b377a847be05f60791ef1058d9f696ecb51d2c07fe911d8569d8e" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", ] +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -1029,7 +2763,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1067,11 +2801,35 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" @@ -1079,6 +2837,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" @@ -1091,6 +2861,36 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1103,7 +2903,7 @@ version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom", + "getrandom 0.4.1", "js-sys", "serde_core", "wasm-bindgen", @@ -1115,6 +2915,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[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" @@ -1125,6 +2949,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1162,6 +2995,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.106" @@ -1181,7 +3027,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -1238,6 +3084,35 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wide" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d920ac99c3c8edce110cb8d07dbb324d6d026011dce85b1e9355b70f0adacc4f" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1290,7 +3165,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1301,7 +3176,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1328,13 +3203,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -1346,6 +3230,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -1353,64 +3253,127 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -1433,7 +3396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "wit-parser", ] @@ -1444,10 +3407,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "indexmap", "prettyplease", - "syn", + "syn 2.0.119", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -1463,7 +3426,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -1505,6 +3468,41 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -1522,7 +3520,69 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e3ed4eb..66ac74f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,13 @@ members = [ "src/slm", "src/contract", ] +# The vendored Bunsenite fork is a path dependency of policy-oracle (optional, +# behind the `nickel` feature). Exclude it from workspace membership so that +# plain `cargo check/test --workspace` does not pull in the Nickel dependency +# tree; it is built only when the `nickel` feature is enabled. +exclude = [ + "vendor/bunsenite", +] [workspace.dependencies] tokio = { version = "1", features = ["full"] } @@ -29,9 +36,17 @@ clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } clap_complete = "4" clap_mangen = "0.3" +[features] +# Native Nickel (.ncl) policy loading in the CLI (policy-oracle/nickel). +default = [] +nickel = ["policy-oracle/nickel"] +# Real HTTP SLM provider for `contract eval --slm` (slm-evaluator/http). +slm-http = ["slm-evaluator/http"] + [dependencies] policy-oracle = { path = "src/oracle" } gating-contract = { path = "src/contract" } +slm-evaluator = { path = "src/slm" } clap.workspace = true chrono.workspace = true clap_complete.workspace = true @@ -44,6 +59,7 @@ tracing-subscriber.workspace = true [dev-dependencies] criterion = { version = "0.8.2", features = ["html_reports"] } +proptest = "1" [[bench]] name = "oracle_bench" diff --git a/README.adoc b/README.adoc index 12ddcde..3953f35 100644 --- a/README.adoc +++ b/README.adoc @@ -332,6 +332,9 @@ Copyright (C) 2025 Jonathan D.A. Jewell * link:docs/ARCHITECTURE.md[Full Architecture Specification] * link:docs/MAAF_INTEGRATION.adoc[MAAF Integration] * link:docs/STATE_ECOSYSTEM_SCHEMA.adoc[STATE/ECOSYSTEM Schema] +* link:docs/NICKEL-POLICY.adoc[Native Nickel Policy Backend] +* link:docs/SLM_PROVIDERS.adoc[SLM Provider Backends] +* link:docs/ARBITER_PROTOCOL.adoc[Consensus Arbiter Protocol] == Architecture diff --git a/ROADMAP.adoc b/ROADMAP.adoc index d42a0ad..a282aa8 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -34,12 +34,16 @@ Development roadmap for the SLM-as-Cerebellum policy enforcement system. | compliant/violations/edge_cases | SLM Evaluator -| [yellow]#*PLACEHOLDER*# -| Interface defined, needs llama.cpp +| [green]#*BACKENDS LANDED*# +| Real `llama-cli` + HTTP providers, contract-verified; model calibration open | Consensus Arbiter -| [yellow]#*STARTED*# -| GenServer skeleton, Application module, decide/3 logic +| [green]#*BACKENDS LANDED*# +| Protocol v1 escript + durable audit sink + Rust client; ops hardening open + +| Native Nickel policies +| [green]#*FEATURE LANDED*# +| Vendored Bunsenite backend; CI-verified build (memory-gated) | LLM Integration | [red]#*NOT STARTED*# diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index 6f0d331..a92b087 100644 --- a/TEST-NEEDS.adoc +++ b/TEST-NEEDS.adoc @@ -9,6 +9,26 @@ * *Property tests*: 10 pass (determinism, outcomes, performance) * *Security aspect tests*: 20 pass (bypass prevention, manipulation detection) + +=== 2026-09-22 update — SLM backends + arbiter + +* *Rust suites*: 187 assertions green locally + (15 slm units incl. provider contract + timeout, 22 slm `http` + feature, 60 contract units incl. 9 SLM-stage + arbiter client + validation, 30 oracle, 19 pipeline, 10 property, 20 security, + 11 generative/proptest: terminality under arbitrary outcomes, + threshold arithmetic at 0/1, low-confidence escalation, fail-closed + failure modes, determinism, request-ID preservation, concurrent + correlation no-mixing) +* *Real-inference smokes* (env-gated, `--ignored` in CI-driven jobs): + local Qwen2.5-0.5B GGUF round-trip (14.1 s / 2 vCPU) and llama-server + HTTP round-trip (3.6 s warm) — both green on 2026-09-22 +* *Elixir (OTP arbiter)*: 28 ExUnit tests written (consensus decision + matrix, protocol validation, audit sink incl. rotation + fail-closed, + server round-trips) — verified by the `arbiter-ci` workflow; no local + OTP toolchain existed +* *Native Nickel*: feature tests run in the dedicated `nickel-native` + CI job (parser cannot compile under ~2 GB) * *Integration tests*: 1 Zig template placeholder (ready for expansion) * *Benchmarks*: Baseline infrastructure in place (criterion-ready) * *panic-attack scan*: Ready to run diff --git a/docs/ARBITER_PROTOCOL.adoc b/docs/ARBITER_PROTOCOL.adoc new file mode 100644 index 0000000..cf3b44f --- /dev/null +++ b/docs/ARBITER_PROTOCOL.adoc @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Consensus Arbiter Protocol (v1) +:toc: + +The OTP consensus arbiter (`src/arbiter/`) is a standalone Elixir escript +consuming newline-delimited JSON on stdin and answering on stdout. The +Rust client (`src/contract/src/arbiter.rs`) spawns one process per +decision; both sides fail closed. + +== Wire format + +=== Request + +[source,json] +---- +{"protocol_version":1,"request_id":"", + "llm":{"confidence":0.95}, + "slm":{"violation_confidence":0.05}, + "oracle":{"verdict":"allow|soft_concern|hard_violation"}} +---- + +* `protocol_version` — must be integer `1`; any other version is a + protocol error (unknown versions never yield Allow). +* `request_id` — non-empty; echoed verbatim in the response and the audit + record. Empty/missing ids are rejected. +* `llm.confidence`, `slm.violation_confidence` — numbers in `[0, 1]` + (integers are coerced). Out-of-range values are rejected. +* `oracle.verdict` — one of `allow`, `soft_concern`, `hard_violation`. + +=== Response (decision) + +[source,json] +---- +{"protocol_version":1,"request_id":"…", + "verdict":"allow|escalate|block","reason":"…","audit_recorded":true} +---- + +`audit_recorded` is `true` iff the decision was durably appended to the +audit log **before** the response was written (flush before ack). + +=== Response (service error) + +[source,json] +---- +{"protocol_version":1,"request_id":"…","error":"…"} +---- + +Produced for malformed input, unsupported versions, and **any audit +persistence failure**. No `verdict` key is ever present in a service +error. + +== Decision matrix (consensus) + +* `hard_violation` → **block** (terminal; mirroring the Rust oracle). +* `no_go = violation_confidence × 1.5 (SLM weight)`; `soft_concern` adds + 0.2 to `no_go`. +* `no_go >= 0.9` → **block**. +* `llm.confidence <= 0.8` or `no_go >= 0.4` → **escalate**. +* otherwise → the oracle verdict stands (allow / soft concern). + +The asymmetric matrix (SLM votes count 1.5× because abstaining is cheap +and false-allows are expensive) is documented in +`lib/conative_gating/consensus_arbiter.ex`. + +== Fail-closed guarantees + +* Malformed / wrong-version / out-of-range input → service error, never a + verdict; **no audit record is written for protocol errors** (the request + was never a decision). +* Audit sink unavailable or a persistence failure at record time → service + error; neither an ack nor a verdict is emitted. +* Exactly one audit record per accepted request (flush before ack — + enforced by design; the Rust client requires `audit_recorded: true`). +* The audit record contains decision metadata only (ids, votes, verdict, + reason) — **no proposal content** is ever persisted. + +== Audit log (OTP sink) + +* One JSONL record per decision at `CONATIVE_AUDIT_PATH` + (default `./conative-gating-audit.jsonl`; the test env isolates this + under `_build/test/`). +* Rotation: when the file exceeds `CONATIVE_AUDIT_MAX_BYTES` + (default 10 MiB), it is renamed to `.1` before the next append + (last-generation retention; ship/rotate further in ops if required). +* Invalid `CONATIVE_AUDIT_*` values fail closed at boot — a misconfigured + arbiter never silently runs without its sink. +* In-memory history is bounded (last N records) for diagnostics. + +Hardening backlog (deliberately ops-owned, see +`docs/UPSTREAM-DELIVERY.adoc`): file permissions/ownership policy, +cross-process append locking for multi-instance deployments, disk-full +behaviour drills, log shipping/backup, and optional tamper evidence +(hash chaining) are documented there; the single-instance semantics above +are the tested floor. + +== Concurrency + +One arbiter process per decision-stream; requests are answered in order. +The Rust-side concurrency property (no correlation-ID mixing across +in-flight requests) is tested in `tests/generative_test.rs` at the +contract layer; the escript is single-request-stream per process, so +cross-request mixing is structurally impossible. diff --git a/docs/MAAF_INTEGRATION.adoc b/docs/MAAF_INTEGRATION.adoc index b067153..3aa4706 100644 --- a/docs/MAAF_INTEGRATION.adoc +++ b/docs/MAAF_INTEGRATION.adoc @@ -269,7 +269,10 @@ NOTE: The `wordpress-wharf` (gitlab) and `wharf` (github) repositories need reco **Purpose**: Cross-language Nickel configuration loader with FFI bindings. -**Repository**: https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite +**Repository**: https://gitlab.com/hyperpolymath/bunsenite (canonical; +the former `campaign-for-cooler-coding-and-programming/bunsenite` path is +a stale mirror — conative-gating vendors the canonical tree under +`vendor/bunsenite/`, see `vendor/bunsenite/VENDOR.adoc`) **Key Features**: * Robust Nickel parsing via `nickel-lang-core` diff --git a/docs/NICKEL-POLICY.adoc b/docs/NICKEL-POLICY.adoc new file mode 100644 index 0000000..bec5f17 --- /dev/null +++ b/docs/NICKEL-POLICY.adoc @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Native Nickel Policy Backend +:toc: + +The policy oracle can natively load and validate `.ncl` policy files by +embedding https://nickel-lang.org[Nickel] through the vendored +https://gitlab.com/hyperpolymath/bunsenite[Bunsenite] bindings. + +== Feature gate + +The native backend is behind the `nickel` cargo feature (off by default): + +---- +cargo build -p policy-oracle --features nickel +cargo test -p policy-oracle --features nickel --lib --locked +conative scan --policy policy.ncl # binary built --features nickel +---- + +Without the feature, any `.ncl` policy path fails with an explicit +error — **never** a silent JSON fallback. + +`Policy::from_policy_file(path)` dispatches on extension: +`.ncl` → native Nickel evaluation; anything else → JSON +(backward-compatible with every existing policy file). +`Policy::from_nickel_source(source)` is available for embedded callers. + +== Vendored Bunsenite + +The dependency is `vendor/bunsenite/`, pinned to upstream rev +`f788de3950b7541354806299cc8605dcf1608d11` (see +`vendor/bunsenite/UPSTREAM-REVISION` and `vendor/bunsenite/VENDOR.adoc`). + +One deliberate local diff: `nickel-lang-core = "0.18.0"` with +`default-features = false`, which drops the REPL/doc/format/markdown +feature chain (tree-sitter, topiary, rustyline, comrak, their C +toolchains) from the embedded evaluator. `nickel-lang-core 0.19.0` exists; +the pin stays on 0.18.0 per the delivery spec — revisit at PR review. + +The vendor directory carries Apache-2.0/MIT licenses upstream; see +`vendor/bunsenite/LICENSE*`. + +NOTE: The vendor tree is **excluded from workspace membership** +(`exclude = ["vendor/bunsenite"]`) so plain `cargo check --workspace` does +not pull the Nickel graph; it is built only via the `nickel` feature. + +== Build memory constraint (important) + +`nickel-lang-parser` (compiled by `nickel-lang-core` even with all default +features off) does not link-compile reliably under ~2 GB RAM; the build is +SIGKILLed (OOM), reproduced twice with `CARGO_BUILD_JOBS=1 +CARGO_INCREMENTAL=0 ... -Dwarnings`. The reduced-default-features fork +shrinks the graph but cannot remove the parser crate. + +Therefore the authoritative verification of the `nickel` feature is the +dedicated CI job (`nickel-native` in `.github/workflows/ci.yml`), which +runs on a full-size hosted runner with the exact contracted command: + +---- +CARGO_BUILD_JOBS=1 RUSTFLAGS="-C debuginfo=0 -Dwarnings" \ + cargo test -p policy-oracle --features nickel --lib --locked +---- + +== Multi-file imports: NOT supported (decision) + +**Decision: Nickel `import` statements are rejected. Policies must be a +single self-contained `.ncl` file.** + +Rationale: + +* the audit boundary requires that exactly the bytes under review are what + was evaluated — resolving relative imports would make evaluation depend + on ambient filesystem state (symlinks, TOCTOU swaps, + include-what-you-didn't-review); +* Bunsenite's `eval_for_nickel` evaluates an anonymous root term, so import + roots would need bespoke resolution policy anyway; +* refusing early and loudly makes policy composition explicit: assemble the + final `.ncl` at policy-review time, commit the result. + +Implementation: `src/oracle/src/nickel.rs` pre-scans the source for +top-level `import` expressions **outside of comments and string literals** +and fails with `OracleError::NickelUnsupported` naming the offending +import. No evaluation is attempted. + +If/when composition is genuinely needed, the sanctioned paths are: + +* generate the single-file policy from a policy repo (recommended), or +* request a `Policy` multiple-file merge at the JSON model level + (languages/toolchain/patterns/enforcement lists are plain data). + +This decision, and how to revisit it, is recorded here per the delivery +contract. + +== Validation contract + +A Nickel policy must evaluate to a record satisfying the strict +`policy_oracle::Policy` contract (exact fields `name`, `languages`, +`toolchain`, `patterns`, `enforcement`; the strict serde contract rejects +unknown fields — Nickel refinements do not silently widen the model). +Evaluation errors, contract errors and unsupported features all surface as +explicit `OracleError`s; policy loading fails closed. diff --git a/docs/SLM_PROVIDERS.adoc b/docs/SLM_PROVIDERS.adoc new file mode 100644 index 0000000..94d1609 --- /dev/null +++ b/docs/SLM_PROVIDERS.adoc @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += SLM Provider Backends +:toc: + +`slm-evaluator` evaluates proposals through pluggable **providers**. Two +real backends ship in this change: a local `llama.cpp` CLI adapter and an +OpenAI-compatible HTTP adapter (behind the `http` cargo feature). + +== The provider contract (verbatim) + +Every provider must: + +* accept a `SlmRequest` carrying the **correlation ID** + (`proposal_id`) of the gating request, plus bounded `content`/`context` + (`MAX_CONTENT_CHARS` = 4096, `MAX_CONTEXT_CHARS` = 1024); +* return a `SlmEvaluation` whose `proposal_id` **echoes the request** — + the ID is preserved end-to-end; +* produce a verdict object with **exactly** the fields + `{"spirit_score": 0..1, "confidence": 0..1, "reasoning": string, "should_block": bool}`; +* be **fail-closed**: timeouts, transport failure, malformed JSON, and + out-of-range scores are all `SlmError`s — the gating contract turns any + provider error into a non-overridable **Escalate** (`Sys902`), never an + Allow. + +`evaluate_with_provider` never calls a provider when the deterministic +oracle already ruled Block — an SLM never re-litigates a terminal oracle +decision, and proposal content never leaves the process needlessly. + +== LlamaCppProvider (local GGUF) + +Runs a pinned `llama-cli`: + +---- +llama-cli -m MODEL -p PROMPT -n TOKENS --temp 0 --no-display-prompt --single-turn +---- + +`--single-turn` is additive to the documented argument set: current +`llama-cli` nightlies otherwise enter their interactive conversation loop +and never exit (observed with b11100). + +Environment configuration (`LlamaCppProvider::from_env`: + +* `CONATIVE_GGUF_MODEL` — path to the GGUF model (required; unset = + provider not configured); +* `CONATIVE_LLAMA_CLI` — executable path (default `llama-cli`); +* `CONATIVE_SLM_MAX_TOKENS` — per-request decoding budget (default 256 — + small instruct models were observed truncated mid-JSON at 128); +* `CONATIVE_SLM_TIMEOUT_SECS` — timeout (default 120). + +== HttpSlmProvider (OpenAI-compatible, `--features http`) + +Feature-gated. `reqwest` (blocking, rustls) to +`{CONATIVE_SLM_ENDPOINT}/v1/chat/completions`. + +* `CONATIVE_SLM_ENDPOINT` — endpoint origin, e.g. `https://slm.example.com` + (required; **https, or loopback for development** — plain http to a + non-loopback host is refused at construction); +* `CONATIVE_SLM_MODEL_NAME` — model id (default `local-slm`); +* `SLM_API_KEY` — bearer token, **read from the environment only**, never + logged, never sent anywhere but the configured endpoint; +* `CONATIVE_SLM_MAX_TOKENS` / `CONATIVE_SLM_TIMEOUT_SECS` as above. + +A loopback `llama-server` satisfies this adapter for development/CI: + +---- +llama-server -m model.gguf --host 127.0.0.1 --port 18080 & +export CONATIVE_SLM_ENDPOINT=http://127.0.0.1:18080 +---- + +== Pinned CI artifacts (SHA-256 verified, never committed) + +Recorded in `.ci-artifact-pins.txt`; downloaded + verified in the +`slm-real-inference` workflow: + +[cols="2,3,1", options="header"] +|=== +|Artifact |Source pinned |SHA-256 + +|llama.cpp b11100 ubuntu-x64 binary +|`github.com/ggml-org/llama.cpp` release `b11100` +|`a836c913236ab4533ef9aaf49f0e1ad2955c8159869d7eec1a9072a92e13d61b` + +|Qwen2.5-0.5B-Instruct Q4_K_M GGUF +|`huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF` @ `9217f5db…` +|`74a4da8c9fdbcd15bd1f6d01d621410d31c6fc00986f5eb687824e7b93d7a9db` +|=== + +Model choice note: `SmolLM2-135M-Instruct` (Q4_K_M) was tried first for +size (105 MB) and **rejected empirically**: at `--temp 0` it emits +`"spirit_score": 0.000000…` indefinitely and never closes the verdict +object within the token budget. Qwen2.5-0.5B produces a valid verdict on +the first attempt. The parser nevertheless scans *all* balanced objects +and accepts the last schema-valid one, because current `llama-cli` +pollutes stdout with a banner and a prompt echo containing the invalid +template object. Fail-closed behaviour is unchanged. + +== Benchmarks (measured, 2 vCPU / 2 GB sandbox, 2026-09-22) + +[cols="2,2,3", options="header"] +|=== +|Backend |Latency |Notes + +|llama-cli, Qwen2.5-0.5B Q4_K_M +|*14.1 s* end-to-end (`temp 0`, 128-token answer) +|includes process spawn + model mmap; generation ≈ 15 tok/s, prompt +processing ≈ 80 tok/s + +|llama-server loopback, same model +|*3.6 s* warm per evaluation +|server amortises model load; identical deterministic verdict + +|SmolLM2-135M Q4_K_M +|fails contract +|token-loop (see above); kept out of CI +|=== + +These are floor numbers from a deliberately tiny environment; the CI +runner (4 vCPU/16 GB) is expected to be several times faster. The gating +CLI enforces the provider timeout regardless of wall-clock here. +Verdict *quality* of a 0.5B model is advisory-grade only (it scored a +clean hello-world 0.9 violation-confidence once) — treat small-model +verdicts as a signal that must be backed by the oracle's hard rules and +human escalation paths. + +== Running the real smoke tests + +---- +# local GGUF +CONATIVE_LLAMA_CLI=./llama-cli CONATIVE_GGUF_MODEL=./model.gguf \ + cargo test -p slm-evaluator --test real_inference real_llama -- --ignored --nocapture + +# HTTP adapter (loopback llama-server or any OpenAI-compatible endpoint) +CONATIVE_SLM_ENDPOINT=http://127.0.0.1:18080 \ + cargo test -p slm-evaluator --features http --test real_inference real_http -- --ignored --nocapture +---- + +== Live remote provider (protected) + +The genuine remote-provider smoke never runs on PRs. It runs only: + +* on `workflow_dispatch`, and +* on pushes to the protected `main` branch, + +inside the `slm-remote-production` GitHub Environment, which must be +configured with `CONATIVE_SLM_ENDPOINT`, `CONATIVE_SLM_MODEL_NAME`, and +`SLM_API_KEY` secrets and a required-reviewers protection rule. See +`.github/workflows/slm-real-inference.yml`. Fork PR code never receives +these secrets. diff --git a/docs/UPSTREAM-DELIVERY.adoc b/docs/UPSTREAM-DELIVERY.adoc new file mode 100644 index 0000000..c4c0482 --- /dev/null +++ b/docs/UPSTREAM-DELIVERY.adoc @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Upstream Delivery Notes (2026-09-22 rebuild) +:toc: + +State of the feature set delivered by the +`agent/native-nickel-slm-arbiter` branch, and what remains ops-owned. + +== Delivered + +* **Native Nickel backend** — vendored Bunsenite @ `f788de39…` + (default-features-off `nickel-lang-core 0.18.0`), `nickel` feature, + fail-closed import rejection, `Policy::from_policy_file` dispatch. + Verified by the `nickel-native` CI job (the parser cannot build under + ~2 GB — see `docs/NICKEL-POLICY.adoc`). +* **SLM provider layer** — `SlmProvider` trait, verdict contract + + correlation preservation, `LlamaCppProvider` (`--single-turn` hardened), + `HttpSlmProvider` (`--features http`, https-or-loopback, `SLM_API_KEY` + env-only), `from_env()` selection, CLI `eval --slm` (loud exit 3 on + misconfiguration). +* **Contract integration** — `evaluate_with_provider` (oracle Block + terminal, provider never called; Warn addend; threshold matrix; + provider failure → non-overridable `Sys902` Escalate), Rust arbiter + client with enforced `audit_recorded: true`. +* **OTP arbiter** — protocol v1 decode/validate/encode, consensus + decision matrix (SLM weight 1.5, +0.2 soft-concern addend, 0.9/0.4 + thresholds, `llm.confidence <= 0.8` escalate), stdio server, escript, + durable audit sink (JSONL, flush-before-ack, rotation at + `CONATIVE_AUDIT_MAX_BYTES`, fail-closed, bounded history, no content). +* **Tests** — 187 Rust assertions green locally (incl. 22 http-feature, + 11 generative/proptest covering terminality, threshold arithmetic at + 0/1, low-confidence escalation, fail-closed failure modes, determinism, + request-ID preservation, concurrency correlation no-mixing), 28 ExUnit + tests for the arbiter (CI-verified; no local OTP toolchain existed). +* **Real-model smokes** — llama-cli + Qwen2.5-0.5B Q4_K_M round-trip + (14.1 s on 2 vCPU) and llama-server HTTP round-trip (3.6 s warm), both + reproducible via pinned artifacts in `.ci-artifact-pins.txt` and the + `slm-real-inference` workflow. + +== Ops-owned setup (before merging) + +. **Rotate the PAT** used to push this branch + (https://github.com/settings/tokens) — it travelled through a chat + channel; treat as compromised. +. **Create the `slm-remote-production` GitHub Environment** with required + reviewers and secrets `CONATIVE_SLM_ENDPOINT`, `CONATIVE_SLM_MODEL_NAME`, + `SLM_API_KEY`. The remote smoke job is inert until then (by design). +. **Commit `src/arbiter/mix.lock`** — it cannot be generated without an + OTP toolchain; run `mix deps.get` once in `src/arbiter/` on any OTP-27 + machine and commit the lockfile. +. Audit-sink hardening for production multi-instance use: decide file + permissions/ownership (`umask`), add cross-process append locking if + more than one arbiter writes one file, wire log shipping/backup, and + consider hash-chained records for tamper evidence. The tested floor + (single instance, flush-before-ack, rotation) is documented in + `docs/ARBITER_PROTOCOL.adoc`. +. Watch the first `nickel-native` CI run — it is the authoritative + verification vehicle for the feature build. + +== Known limitations / deliberate deferrals + +* Small-model verdict *quality* is advisory-grade; plumbing is verified, + calibration is future work (`docs/SLM_PROVIDERS.adoc` benchmark notes). +* Nickel multi-file `import` is intentionally unsupported (decision + + alternatives in `docs/NICKEL-POLICY.adoc`). +* `nickel-lang-core` pinned to 0.18.0 per spec; 0.19.0 upgrade is a PR + review point. +* The audit "arbiter restart does not corrupt the log" invariant is + covered structurally (append-only JSONL + per-line records) and by the + rotation test; a chaos-restart drill belongs with ops hardening above. diff --git a/hooks/validate-codeql.sh b/hooks/validate-codeql.sh old mode 100755 new mode 100644 diff --git a/hooks/validate-permissions.sh b/hooks/validate-permissions.sh old mode 100755 new mode 100644 diff --git a/hooks/validate-sha-pins.sh b/hooks/validate-sha-pins.sh old mode 100755 new mode 100644 diff --git a/hooks/validate-spdx.sh b/hooks/validate-spdx.sh old mode 100755 new mode 100644 diff --git a/scripts/apply-common-files.sh b/scripts/apply-common-files.sh old mode 100755 new mode 100644 diff --git a/scripts/apply-justfiles.sh b/scripts/apply-justfiles.sh old mode 100755 new mode 100644 diff --git a/scripts/bulk-standardize.sh b/scripts/bulk-standardize.sh old mode 100755 new mode 100644 diff --git a/scripts/mass-apply-templates.sh b/scripts/mass-apply-templates.sh old mode 100755 new mode 100644 diff --git a/scripts/reconcile-wharf-repos.sh b/scripts/reconcile-wharf-repos.sh old mode 100755 new mode 100644 diff --git a/setup.sh b/setup.sh old mode 100755 new mode 100644 diff --git a/src/arbiter/config/runtime.exs b/src/arbiter/config/runtime.exs new file mode 100644 index 0000000..04f8984 --- /dev/null +++ b/src/arbiter/config/runtime.exs @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +# Runtime configuration — evaluated at boot (escript-friendly). + +import Config + +config :conative_gating, + audit_path: System.get_env("CONATIVE_AUDIT_PATH"), + audit_max_bytes: + case System.get_env("CONATIVE_AUDIT_MAX_BYTES") do + nil -> + nil + + raw -> + case Integer.parse(raw) do + {value, ""} when value > 0 -> value + _ -> nil + end + end + +# Tests must never pollute the working directory: route the default audit +# path into the build tree where artifacts are disposable. +if config_env() == :test do + config :conative_gating, + audit_path: + System.get_env("CONATIVE_AUDIT_PATH") || + Path.expand(Path.join([__DIR__, "..", "_build", "test", "conative-gating-audit.jsonl"])) +end diff --git a/src/arbiter/lib/conative_gating/application.ex b/src/arbiter/lib/conative_gating/application.ex index d9bd01c..bb97983 100644 --- a/src/arbiter/lib/conative_gating/application.ex +++ b/src/arbiter/lib/conative_gating/application.ex @@ -15,7 +15,9 @@ defmodule ConativeGating.Application do def start(_type, _args) do children = [ # Start the Consensus Arbiter GenServer - ConativeGating.ConsensusArbiter + ConativeGating.ConsensusArbiter, + # Durable JSONL audit sink (CONATIVE_AUDIT_PATH / CONATIVE_AUDIT_MAX_BYTES) + {ConativeGating.AuditLog, []} ] opts = [strategy: :one_for_one, name: ConativeGating.Supervisor] diff --git a/src/arbiter/lib/conative_gating/arbiter_protocol.ex b/src/arbiter/lib/conative_gating/arbiter_protocol.ex new file mode 100644 index 0000000..0782b3a --- /dev/null +++ b/src/arbiter/lib/conative_gating/arbiter_protocol.ex @@ -0,0 +1,126 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ConativeGating.ArbiterProtocol do + @moduledoc """ + Versioned JSON-lines protocol for the Consensus Arbiter. + + Wire format (protocol version 1): + + request: + {"protocol_version":1,"request_id":"…", + "llm":{"confidence":0.95}, + "slm":{"violation_confidence":0.05}, + "oracle":{"verdict":"allow|soft_concern|hard_violation"}} + + response: + {"protocol_version":1,"request_id":"…", + "verdict":"allow|escalate|block","reason":"…","audit_recorded":true} + + service error: + {"protocol_version":1,"request_id":"…","error":"…"} + + Decoding never raises: every malformed request yields + `{:error, reason}` so the caller can answer with a protocol error and fail + closed. See `docs/ARBITER_PROTOCOL.adoc`. + """ + + @protocol_version 1 + @oracle_verdicts ~w(allow soft_concern hard_violation) + @final_verdicts ~w(allow escalate block) + + @typedoc "A validated consensus request." + @type request :: %{ + protocol_version: 1, + request_id: String.t(), + llm: %{confidence: float()}, + slm: %{violation_confidence: float()}, + oracle: %{verdict: String.t()} + } + + def protocol_version, do: @protocol_version + def final_verdicts, do: @final_verdicts + def oracle_verdicts, do: @oracle_verdicts + + @doc """ + Decode and validate one request line. + """ + @spec decode_request(binary()) :: {:ok, request()} | {:error, atom() | tuple()} + def decode_request(line) when is_binary(line) do + with {:ok, decoded} <- Jason.decode(line), + {:ok, request} <- validate_request(decoded) do + {:ok, request} + else + {:error, %Jason.DecodeError{}} -> {:error, :malformed_json} + {:error, reason} -> {:error, reason} + end + end + + defp validate_request(%{"protocol_version" => version} = request) + when version != @protocol_version do + _ = request + {:error, {:unsupported_protocol_version, version}} + end + + defp validate_request( + %{ + "protocol_version" => @protocol_version, + "request_id" => request_id, + "llm" => %{"confidence" => confidence}, + "slm" => %{"violation_confidence" => violation_confidence}, + "oracle" => %{"verdict" => oracle_verdict} + } = request + ) + when is_binary(request_id) and is_number(confidence) and + is_number(violation_confidence) do + cond do + request_id == "" -> + {:error, :missing_request_id} + + confidence < 0 or confidence > 1 -> + {:error, {:out_of_range, "llm.confidence"}} + + violation_confidence < 0 or violation_confidence > 1 -> + {:error, {:out_of_range, "slm.violation_confidence"}} + + oracle_verdict not in @oracle_verdicts -> + {:error, {:unknown_oracle_verdict, oracle_verdict}} + + true -> + {:ok, + %{ + protocol_version: @protocol_version, + request_id: request_id, + llm: %{confidence: confidence / 1}, + slm: %{violation_confidence: violation_confidence / 1}, + oracle: %{verdict: oracle_verdict} + }} + end + end + + defp validate_request(_other), do: {:error, :invalid_request_shape} + + @doc "Encode a consensus response (one line, no trailing newline)." + @spec encode_response(String.t(), String.t(), String.t() | nil, boolean()) :: binary() + def encode_response(request_id, verdict, reason, audit_recorded) + when verdict in @final_verdicts and is_boolean(audit_recorded) do + Jason.encode!(%{ + protocol_version: @protocol_version, + request_id: request_id, + verdict: verdict, + reason: reason, + audit_recorded: audit_recorded + }) + end + + @doc "Encode a service-level error response (clients must fail closed)." + @spec encode_error(String.t() | nil, binary()) :: binary() + def encode_error(request_id, message) do + Jason.encode!(%{ + protocol_version: @protocol_version, + request_id: request_id || "", + error: message + }) + end +end diff --git a/src/arbiter/lib/conative_gating/audit_log.ex b/src/arbiter/lib/conative_gating/audit_log.ex new file mode 100644 index 0000000..c00b4bd --- /dev/null +++ b/src/arbiter/lib/conative_gating/audit_log.ex @@ -0,0 +1,205 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ConativeGating.AuditLog do + @moduledoc """ + Durable JSONL audit sink for gating decisions. + + Guarantees: + + * **Exactly one record per decision** — one `record/2` call appends one + line; callers must invoke it exactly once per accepted request. + * **Flushed before acknowledged** — each record is written and + `:file.sync`'d before the caller receives `:ok`. + * **Rotation** — when appending would push the active file past + `max_bytes`, the active file is renamed to `.1` (single + generation, overwriting any previous rotation) before writing. + * **Fail-closed** — any filesystem failure returns `{:error, reason}` and + leaves history untouched. A decision whose audit cannot persist must + never be answered as `allow` upstream. + * **No proposal content** — `:content`/`"content"` keys are stripped + defensively; audit carries identifiers, votes, and metadata only. + * **Bounded memory** — an in-memory history (default 1,000 entries) is + kept for diagnostics, never growing beyond capacity. + + Configuration (first match wins): explicit start options, then the + `:conative_gating` application env, then process env vars: + + * `CONATIVE_AUDIT_PATH` — JSONL file path (default + `conative-gating-audit.jsonl`) + * `CONATIVE_AUDIT_MAX_BYTES` — rotation threshold (default 10 MiB) + """ + + use GenServer + require Logger + + @default_path "conative-gating-audit.jsonl" + @default_max_bytes 10 * 1024 * 1024 + @default_history_capacity 1_000 + + # ------------------------------------------------------------------------- + # Client API + # ------------------------------------------------------------------------- + + def start_link(opts \\ []) do + {name, opts} = Keyword.pop(opts, :name, __MODULE__) + GenServer.start_link(__MODULE__, opts, name: name) + end + + @doc """ + Persist one audit entry (a JSON-encodable map). Returns `:ok` only after + the record has been synced to disk; `{:error, reason}` otherwise. + """ + @spec record(map(), GenServer.server()) :: :ok | {:error, term()} + def record(entry, server \\ __MODULE__) when is_map(entry) do + GenServer.call(server, {:record, entry}) + end + + @doc "Bounded in-memory diagnostic history (newest last)." + @spec history(GenServer.server()) :: [map()] + def history(server \\ __MODULE__) do + GenServer.call(server, :history) + end + + @doc "The effective sink configuration (diagnostics/tests)." + @spec config(GenServer.server()) :: map() + def config(server \\ __MODULE__) do + GenServer.call(server, :config) + end + + # ------------------------------------------------------------------------- + # Server + # ------------------------------------------------------------------------- + + @impl true + def init(opts) do + app_env = Application.get_all_env(:conative_gating) + + path = + Keyword.get(opts, :path) || + Keyword.get(app_env, :audit_path) || + System.get_env("CONATIVE_AUDIT_PATH") || + @default_path + + max_bytes = + Keyword.get(opts, :max_bytes) || + Keyword.get(app_env, :audit_max_bytes) || + case System.get_env("CONATIVE_AUDIT_MAX_BYTES") do + nil -> @default_max_bytes + raw -> parse_positive_integer(raw, @default_max_bytes) + end + + history_capacity = Keyword.get(opts, :history_capacity, @default_history_capacity) + + {:ok, + %{ + path: Path.expand(path), + max_bytes: max_bytes, + history_capacity: history_capacity, + history: [] + }} + end + + @impl true + def handle_call({:record, entry}, _from, state) do + sanitized = sanitize(entry) + line = Jason.encode!(sanitized) + + case persist(state.path, state.max_bytes, line) do + :ok -> + history = (state.history ++ [sanitized]) |> Enum.take(-state.history_capacity) + {:reply, :ok, %{state | history: history}} + + {:error, reason} = error -> + Logger.error("audit persistence failed (fail-closed): #{inspect(reason)}") + {:reply, error, state} + end + end + + def handle_call(:history, _from, state), do: {:reply, state.history, state} + + def handle_call(:config, _from, state) do + {:reply, + %{ + path: state.path, + max_bytes: state.max_bytes, + history_capacity: state.history_capacity, + history_size: length(state.history) + }, state} + end + + # ------------------------------------------------------------------------- + # Persistence + # ------------------------------------------------------------------------- + + # Write one JSONL record, rotating first when the active file would exceed + # max_bytes. The record is synced before returning :ok. + defp persist(path, max_bytes, line) do + record_bytes = byte_size(line) + 1 + current_size = file_size(path) + + with :ok <- maybe_rotate(path, max_bytes, current_size + record_bytes), + {:ok, io} <- File.open(path, [:append, :utf8, :raw]), + :ok <- write_and_sync(io, line) do + :ok + else + {:error, reason} -> {:error, reason} + end + end + + defp write_and_sync(io, line) do + with :ok <- IO.binwrite(io, [line, "\n"]), + :ok <- :file.sync(io) do + File.close(io) + else + {:error, reason} -> + File.close(io) + {:error, reason} + end + end + + defp maybe_rotate(path, max_bytes, projected_size) + when projected_size <= max_bytes, + do: :ok + + defp maybe_rotate(path, _max_bytes, _projected_size) do + rotated = path <> ".1" + + if File.exists?(path) do + File.rm(rotated) + File.rename(path, rotated) + else + :ok + end + end + + defp file_size(path) do + case File.stat(path) do + {:ok, %{size: size}} -> size + {:error, _} -> 0 + end + end + + # Belt-and-braces removal of proposal content keys, shallow and nested one + # level under common envelope keys, before anything touches the disk. + defp sanitize(entry) when is_map(entry) do + entry + |> Map.delete(:content) + |> Map.delete("content") + |> Map.new(fn + {key, value} when is_map(value) -> + {key, value |> Map.delete(:content) |> Map.delete("content")} + + other -> + other + end) + end + + defp parse_positive_integer(raw, default) do + case Integer.parse(raw) do + {value, ""} when value > 0 -> value + _ -> default + end + end +end diff --git a/src/arbiter/lib/conative_gating/cli.ex b/src/arbiter/lib/conative_gating/cli.ex new file mode 100644 index 0000000..27547ce --- /dev/null +++ b/src/arbiter/lib/conative_gating/cli.ex @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ConativeGating.CLI do + @moduledoc """ + Escript entry point for the Consensus Arbiter protocol server. + + conative_arbiter # read JSONL requests on stdin, answer on stdout + + The audit sink is configured entirely through the environment + (`CONATIVE_AUDIT_PATH`, `CONATIVE_AUDIT_MAX_BYTES` — see the AuditLog + module). One process serves a whole stream of requests; callers that want + process-per-request semantics (the Rust client) simply close stdin after + one line. + """ + + alias ConativeGating.{AuditLog, ProtocolServer} + + def main(_args) do + {:ok, _} = Application.ensure_all_started(:jason) + + case AuditLog.start_link(name: AuditLog) do + {:ok, _pid} -> + :ok + + {:error, {:already_started, _pid}} -> + :ok + + {:error, reason} -> + IO.puts(:stderr, "failed to start audit log: #{inspect(reason)}") + exit(:audit_unavailable) + end + + ProtocolServer.loop(AuditLog, :stdio) + end +end diff --git a/src/arbiter/lib/conative_gating/protocol_server.ex b/src/arbiter/lib/conative_gating/protocol_server.ex new file mode 100644 index 0000000..128faa9 --- /dev/null +++ b/src/arbiter/lib/conative_gating/protocol_server.ex @@ -0,0 +1,133 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ConativeGating.ProtocolServer do + @moduledoc """ + Line-oriented stdio driver for the Consensus Arbiter (protocol v1). + + Reads one JSON request per line from stdin, reaches a consensus decision, + persists the audit record, and writes exactly one response line per request + before the requester considers the decision acknowledged: + + * malformed requests → service error response (never a verdict) + * consensus → audit record first; an audit failure yields an error + response, so an unaudited decision can never be acknowledged + * EOF closes the loop + + The Rust client (`gating_contract::ArbiterClient`) spawns one short-lived + arbiter per decision; this loop additionally tolerates multi-request + streams for supervisor/library use. + """ + + alias ConativeGating.{ArbiterProtocol, AuditLog, ConsensusArbiter} + + @doc "Read requests until EOF, answering one line per request." + def loop(audit_server \\ AuditLog, io \\ :stdio) do + case IO.gets(io, "") do + :eof -> + :ok + + {:error, _reason} -> + :ok + + line -> + IO.puts(io, process_line(line, audit_server)) + loop(audit_server, io) + end + end + + @doc """ + Process exactly one request line and return the response line. + + Pure with respect to decision making: identical votes always produce the + same verdict. The only side effect is the audit write (which must succeed + for a verdict response to be produced). + """ + def process_line(line, audit_server \\ AuditLog) do + case ArbiterProtocol.decode_request(line) do + {:ok, request} -> + answer(request, audit_server) + + {:error, reason} -> + ArbiterProtocol.encode_error(nil, "invalid request: #{format_reason(reason)}") + end + end + + defp answer(request, audit_server) do + llm = %{confidence: request.llm.confidence} + slm = %{violation_confidence: request.slm.violation_confidence} + oracle = %{verdict: oracle_verdict(request.oracle.verdict)} + + {verdict, detail} = ConsensusArbiter.decide(llm, slm, oracle) + verdict_text = verdict_to_text(verdict) + reason_text = reason_text(detail) + + entry = %{ + schema: "conative-gating-audit-v1", + audit_id: uuid4(), + request_id: request.request_id, + timestamp: DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601(), + votes: %{ + llm: %{confidence: request.llm.confidence}, + slm: %{violation_confidence: request.slm.violation_confidence}, + oracle: %{verdict: request.oracle.verdict} + }, + verdict: verdict_text, + reason: reason_text, + protocol_version: ArbiterProtocol.protocol_version() + } + + case AuditLog.record(entry, audit_server) do + :ok -> + ArbiterProtocol.encode_response(request.request_id, verdict_text, reason_text, true) + + {:error, reason} -> + ArbiterProtocol.encode_error( + request.request_id, + "audit persistence failed: #{format_reason(reason)}" + ) + end + end + + defp oracle_verdict("allow"), do: :allow + defp oracle_verdict("soft_concern"), do: {:soft_concern, :policy_oracle} + defp oracle_verdict("hard_violation"), do: {:hard_violation, :policy_oracle} + + defp verdict_to_text(:block), do: "block" + defp verdict_to_text(:escalate), do: "escalate" + defp verdict_to_text(:allow), do: "allow" + + defp reason_text(%{reason: reason}) when is_atom(reason), do: Atom.to_string(reason) + defp reason_text(%{reason: reason}) when is_binary(reason), do: reason + + defp reason_text(detail) when is_map(detail) do + "go=#{format_number(Map.get(detail, :go_score))} " <> + "no_go=#{format_number(Map.get(detail, :no_go_score))}" + end + + defp format_number(nil), do: "n/a" + defp format_number(v) when is_float(v), do: :erlang.float_to_binary(v, decimals: 3) + defp format_number(v), do: to_string(v) + + # Local RFC 4122 UUIDv4 (random) — the project deliberately carries no UUID + # dependency, and Erlang/OTP has none built in. + defp uuid4() do + <> = :crypto.strong_rand_bytes(16) + c = Bitwise.bor(Bitwise.band(c0, 0x0FFF), 0x4000) + d = Bitwise.bor(Bitwise.band(d0, 0x3FFF), 0x8000) + Enum.join([hex(a, 8), hex(b, 4), hex(c, 4), hex(d, 4), hex(e, 12)], "-") + end + + defp hex(value, width) do + :io_lib.format("~*.16.0b", [width, value]) + |> IO.iodata_to_binary() + |> String.downcase() + end + + defp format_reason({:unsupported_protocol_version, v}), do: "unsupported protocol version #{v}" + defp format_reason({:out_of_range, field}), do: "#{field} out of range 0..1" + defp format_reason({:unknown_oracle_verdict, v}), do: "unknown oracle verdict #{v}" + defp format_reason(reason) when is_atom(reason), do: Atom.to_string(reason) + defp format_reason(reason), do: inspect(reason) +end diff --git a/src/arbiter/mix.exs b/src/arbiter/mix.exs index c17227b..6420fcb 100644 --- a/src/arbiter/mix.exs +++ b/src/arbiter/mix.exs @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) Jonathan D.A. Jewell -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell defmodule ConativeGating.MixProject do use Mix.Project @@ -12,6 +12,7 @@ defmodule ConativeGating.MixProject do elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps(), + escript: escript(), description: "Consensus Arbiter for Conative Gating", package: package() ] @@ -19,11 +20,15 @@ defmodule ConativeGating.MixProject do def application do [ - extra_applications: [:logger], + extra_applications: [:logger, :crypto], mod: {ConativeGating.Application, []} ] end + defp escript do + [main_module: ConativeGating.CLI, name: "conative_arbiter"] + end + defp deps do [ {:rustler, "~> 0.30"}, # For Rust NIF integration diff --git a/src/arbiter/test/arbiter_protocol_test.exs b/src/arbiter/test/arbiter_protocol_test.exs new file mode 100644 index 0000000..dff3d4f --- /dev/null +++ b/src/arbiter/test/arbiter_protocol_test.exs @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ConativeGating.ArbiterProtocolTest do + use ExUnit.Case, async: true + + alias ConativeGating.ArbiterProtocol + + defp request_json(overrides \\ %{}) do + base = %{ + "protocol_version" => 1, + "request_id" => "11111111-2222-3333-4444-555555555555", + "llm" => %{"confidence" => 0.95}, + "slm" => %{"violation_confidence" => 0.05}, + "oracle" => %{"verdict" => "allow"} + } + + Jason.encode!(deep_merge(base, overrides)) + end + + defp deep_merge(a, b) do + Map.merge(a, b, fn _k, av, bv -> + if is_map(av) and is_map(bv), do: Map.merge(av, bv), else: bv + end) + end + + test "valid request decodes with all fields" do + assert {:ok, request} = ArbiterProtocol.decode_request(request_json()) + assert request.protocol_version == 1 + assert request.request_id == "11111111-2222-3333-4444-555555555555" + assert request.llm.confidence == 0.95 + assert request.slm.violation_confidence == 0.05 + assert request.oracle.verdict == "allow" + end + + test "non-JSON input is rejected" do + assert {:error, :malformed_json} = ArbiterProtocol.decode_request("not json") + end + + test "unsupported protocol version is rejected" do + line = request_json(%{"protocol_version" => 2}) + assert {:error, {:unsupported_protocol_version, 2}} = ArbiterProtocol.decode_request(line) + end + + test "out-of-range confidences are rejected" do + line = request_json(%{"llm" => %{"confidence" => 1.5}}) + assert {:error, {:out_of_range, "llm.confidence"}} = ArbiterProtocol.decode_request(line) + + line = request_json(%{"slm" => %{"violation_confidence" => -0.1}}) + assert {:error, {:out_of_range, "slm.violation_confidence"}} = + ArbiterProtocol.decode_request(line) + end + + test "unknown oracle verdict is rejected" do + line = request_json(%{"oracle" => %{"verdict" => "uncertain"}}) + assert {:error, {:unknown_oracle_verdict, "uncertain"}} = + ArbiterProtocol.decode_request(line) + end + + test "empty request id is rejected" do + line = request_json(%{"request_id" => ""}) + assert {:error, :missing_request_id} = ArbiterProtocol.decode_request(line) + end + + test "missing envelope keys are rejected" do + line = Jason.encode!(%{"protocol_version" => 1, "request_id" => "x"}) + assert {:error, :invalid_request_shape} = ArbiterProtocol.decode_request(line) + end + + test "integer confidences are coerced to floats" do + line = request_json(%{"llm" => %{"confidence" => 1}}) + assert {:ok, request} = ArbiterProtocol.decode_request(line) + assert is_float(request.llm.confidence) + end + + test "response encoding round-trips" do + encoded = ArbiterProtocol.encode_response("req-1", "block", "high no-go", true) + decoded = Jason.decode!(encoded) + assert decoded["protocol_version"] == 1 + assert decoded["request_id"] == "req-1" + assert decoded["verdict"] == "block" + assert decoded["reason"] == "high no-go" + assert decoded["audit_recorded"] == true + end + + test "error encoding carries nil request id as empty string" do + decoded = Jason.decode!(ArbiterProtocol.encode_error(nil, "bad")) + assert decoded["error"] == "bad" + assert decoded["request_id"] == "" + assert decoded["protocol_version"] == 1 + end +end diff --git a/src/arbiter/test/audit_log_test.exs b/src/arbiter/test/audit_log_test.exs new file mode 100644 index 0000000..8ce2f41 --- /dev/null +++ b/src/arbiter/test/audit_log_test.exs @@ -0,0 +1,115 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ConativeGating.AuditLogTest do + use ExUnit.Case, async: false + + alias ConativeGating.AuditLog + + defp tmpdir!(tag) do + dir = Path.join(System.tmp_dir!(), "conative-audit-test-#{tag}-#{System.unique_integer([:positive])}") + File.mkdir_p!(dir) + on_exit(fn -> File.rm_rf(dir) end) + dir + end + + defp start_sink(dir, opts) do + {:ok, pid} = AuditLog.start_link(Keyword.merge([path: Path.join(dir, "audit.jsonl"), name: nil], opts)) + + on_exit(fn -> + if Process.alive?(pid), do: GenServer.stop(pid) + end) + + pid + end + + test "record persists one JSONL line with the entry fields" do + dir = tmpdir!("record") + sink = start_sink(dir, []) + + entry = %{ + audit_id: "00000000-0000-0000-0000-000000000001", + request_id: "req-123", + verdict: "allow", + reason: "fixture" + } + + assert :ok = AuditLog.record(entry, sink) + assert :ok = AuditLog.record(%{entry | request_id: "req-124"}, sink) + + lines = dir |> Path.join("audit.jsonl") |> File.read!() |> String.split("\n", trim: true) + assert length(lines) == 2 + + first = Jason.decode!(Enum.at(lines, 0)) + assert first["request_id"] == "req-123" + assert first["verdict"] == "allow" + end + + test "proposal content keys are stripped before persistence" do + dir = tmpdir!("content") + sink = start_sink(dir, []) + + entry = %{ + request_id: "req-secret", + verdict: "block", + "content" => "super-secret-proposal-body-xyzzy", + votes: %{"content" => "nested-secret-xyzzy", slm: %{violation_confidence: 0.9}} + } + + assert :ok = AuditLog.record(entry, sink) + raw = dir |> Path.join("audit.jsonl") |> File.read!() + refute raw =~ "secret-proposal-body-xyzzy" + refute raw =~ "nested-secret-xyzzy" + assert raw =~ "req-secret" + end + + test "rotation moves the active file to .1 once max_bytes is exceeded" do + dir = tmpdir!("rotation") + # Two ~120-byte records with a 200-byte budget forces one rotation. + sink = start_sink(dir, max_bytes: 200) + + filler = String.duplicate("x", 90) + assert :ok = AuditLog.record(%{request_id: "req-1", reason: filler}, sink) + assert :ok = AuditLog.record(%{request_id: "req-2", reason: filler}, sink) + + path = Path.join(dir, "audit.jsonl") + assert File.exists?(path <> ".1"), "expected rotated file audit.jsonl.1 to exist" + + rotated = File.read!(path <> ".1") + active = File.read!(path) + assert rotated =~ "req-1" + refute active =~ "req-1" + assert active =~ "req-2" + + # A third record also fits (rotation happens lazily per record). + assert :ok = AuditLog.record(%{request_id: "req-3", reason: filler}, sink) + lines = path |> File.read!() |> String.split("\n", trim: true) + assert length(lines) == 2 + end + + test "persistence failure fails closed" do + missing_parent = Path.join(System.tmp_dir!(), "conative-missing-#{System.unique_integer([:positive])}") + bad_path = Path.join(missing_parent, "audit.jsonl") + {:ok, sink} = AuditLog.start_link(path: bad_path, name: nil) + + on_exit(fn -> if Process.alive?(sink), do: GenServer.stop(sink) end) + + assert {:error, _reason} = AuditLog.record(%{request_id: "req-x"}, sink) + # History must not acknowledge an unpersisted record. + assert AuditLog.history(sink) == [] + end + + test "in-memory history is bounded by capacity" do + dir = tmpdir!("history") + sink = start_sink(dir, history_capacity: 5, max_bytes: 10_000_000) + + for i <- 1..7 do + assert :ok = AuditLog.record(%{request_id: "req-#{i}"}, sink) + end + + history = AuditLog.history(sink) + assert length(history) == 5 + assert List.last(history)["request_id"] == "req-7" + end +end diff --git a/src/arbiter/test/consensus_arbiter_test.exs b/src/arbiter/test/consensus_arbiter_test.exs new file mode 100644 index 0000000..18fee01 --- /dev/null +++ b/src/arbiter/test/consensus_arbiter_test.exs @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ConativeGating.ConsensusArbiterTest do + use ExUnit.Case, async: true + + alias ConativeGating.ConsensusArbiter + + defp llm(confidence), do: %{confidence: confidence} + defp slm(violation), do: %{violation_confidence: violation} + defp oracle_allow, do: %{verdict: :allow} + defp oracle_soft, do: %{verdict: {:soft_concern, :tier2_language}} + defp oracle_hard, do: %{verdict: {:hard_violation, :forbidden_language}} + + test "hard oracle violation always blocks, regardless of other votes" do + for confidence <- [0.0, 0.5, 0.95, 1.0], violation <- [0.0, 0.5, 1.0] do + assert {:block, %{reason: :policy_oracle}} = + ConsensusArbiter.decide(llm(confidence), slm(violation), oracle_hard()) + end + end + + test "high weighted violation confidence blocks (violation 0.61 => 0.915)" do + assert {:block, %{reason: :high_violation_confidence}} = + ConsensusArbiter.decide(llm(0.95), slm(0.61), oracle_allow()) + end + + test "weighted score exactly at the 0.9 boundary does NOT block" do + # 0.6 * 1.5 == 0.8999999999999999 in IEEE-754 — strictly below 0.9, so + # the block branch must not trigger. Documented deterministic boundary. + assert {:escalate, detail} = + ConsensusArbiter.decide(llm(0.9), slm(0.6), oracle_allow()) + + assert detail.no_go_score < 0.9 + end + + test "clear pass allows (violation 0.19 => weighted 0.285 < 0.3)" do + assert {:allow, _} = ConsensusArbiter.decide(llm(0.95), slm(0.19), oracle_allow()) + end + + test "weighted score at/above 0.3 never allows (violation 0.21 => 0.315)" do + assert {:escalate, _} = + ConsensusArbiter.decide(llm(0.99), slm(0.21), oracle_allow()) + end + + test "low LLM confidence escalates even with a clean SLM vote" do + assert {:escalate, %{go_score: 0.5}} = + ConsensusArbiter.decide(llm(0.5), slm(0.05), oracle_allow()) + end + + test "oracle soft concern adds 0.2 to the no-go score" do + # 0.05*1.5 + 0.2 = 0.275 < 0.3 -> allow (with high go) + assert {:allow, _} = ConsensusArbiter.decide(llm(0.95), slm(0.05), oracle_soft()) + # 0.1*1.5 + 0.2 = 0.35 >= 0.3 -> escalate + assert {:escalate, _} = ConsensusArbiter.decide(llm(0.95), slm(0.1), oracle_soft()) + end + + test "decisions are deterministic for identical votes" do + first = ConsensusArbiter.decide(llm(0.87), slm(0.34), oracle_soft()) + second = ConsensusArbiter.decide(llm(0.87), slm(0.34), oracle_soft()) + assert first == second + end +end diff --git a/src/arbiter/test/protocol_server_test.exs b/src/arbiter/test/protocol_server_test.exs new file mode 100644 index 0000000..bdab6e6 --- /dev/null +++ b/src/arbiter/test/protocol_server_test.exs @@ -0,0 +1,111 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ConativeGating.ProtocolServerTest do + use ExUnit.Case, async: false + + alias ConativeGating.{AuditLog, ProtocolServer} + + defp tmpdir!(tag) do + dir = Path.join(System.tmp_dir!(), "conative-server-test-#{tag}-#{System.unique_integer([:positive])}") + File.mkdir_p!(dir) + on_exit(fn -> File.rm_rf(dir) end) + dir + end + + defp start_sink!(dir) do + {:ok, pid} = AuditLog.start_link(path: Path.join(dir, "audit.jsonl"), name: nil) + + on_exit(fn -> + if Process.alive?(pid), do: GenServer.stop(pid) + end) + + pid + end + + defp request_line(request_id, votes \\ %{}) do + Jason.encode!(%{ + protocol_version: 1, + request_id: request_id, + llm: %{confidence: Map.get(votes, :llm, 0.95)}, + slm: %{violation_confidence: Map.get(votes, :slm, 0.05)}, + oracle: %{verdict: Map.get(votes, :oracle, "allow")} + }) + end + + test "allow round-trip: correlated, versioned, audited" do + dir = tmpdir!("allow") + sink = start_sink!(dir) + + response = ProtocolServer.process_line(request_line("req-allow-1"), sink) |> Jason.decode!() + assert response["protocol_version"] == 1 + assert response["request_id"] == "req-allow-1" + assert response["verdict"] == "allow" + assert response["audit_recorded"] == true + + # Exactly one audit record for the accepted request. + [entry] = AuditLog.history(sink) + assert entry.request_id == "req-allow-1" + assert entry.verdict == "allow" + end + + test "hard oracle violation blocks" do + dir = tmpdir!("block") + sink = start_sink!(dir) + + response = + ProtocolServer.process_line(request_line("req-block-1", %{oracle: "hard_violation"}), sink) + |> Jason.decode!() + + assert response["verdict"] == "block" + assert response["audit_recorded"] == true + end + + test "invalid requests produce error responses, never verdicts" do + dir = tmpdir!("invalid") + sink = start_sink!(dir) + + bad_version = + Jason.encode!(%{ + protocol_version: 2, + request_id: "req-bad", + llm: %{confidence: 0.9}, + slm: %{violation_confidence: 0.1}, + oracle: %{verdict: "allow"} + }) + + response = ProtocolServer.process_line(bad_version, sink) |> Jason.decode!() + assert response["error"] =~ "unsupported protocol version" + assert response["request_id"] == "" + refute Map.has_key?(response, "verdict") + + garbage = ProtocolServer.process_line("this is not json", sink) |> Jason.decode!() + assert garbage["error"] =~ "invalid request" + + # No audit records were created for refused requests. + assert AuditLog.history(sink) == [] + end + + test "audit failure yields an error response, never an unaudited verdict" do + missing_parent = Path.join(System.tmp_dir!(), "conative-missing-#{System.unique_integer([:positive])}") + {:ok, sink} = AuditLog.start_link(path: Path.join(missing_parent, "audit.jsonl"), name: nil) + + on_exit(fn -> if Process.alive?(sink), do: GenServer.stop(sink) end) + + response = ProtocolServer.process_line(request_line("req-audit-fail"), sink) |> Jason.decode!() + assert response["request_id"] == "req-audit-fail" + assert response["error"] =~ "audit persistence failed" + refute Map.has_key?(response, "verdict") + end + + test "deterministic votes produce identical verdicts across processes" do + dir = tmpdir!("determinism") + sink = start_sink!(dir) + + line = request_line("req-det", %{llm: 0.87, slm: 0.34, oracle: "soft_concern"}) + first = ProtocolServer.process_line(line, sink) |> Jason.decode!() + second = ProtocolServer.process_line(line, sink) |> Jason.decode!() + assert first["verdict"] == second["verdict"] + end +end diff --git a/src/arbiter/test/test_helper.exs b/src/arbiter/test/test_helper.exs new file mode 100644 index 0000000..308e201 --- /dev/null +++ b/src/arbiter/test/test_helper.exs @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +ExUnit.start() diff --git a/src/contract/src/arbiter.rs b/src/contract/src/arbiter.rs new file mode 100644 index 0000000..8956dae --- /dev/null +++ b/src/contract/src/arbiter.rs @@ -0,0 +1,501 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Client for the OTP Consensus Arbiter's versioned JSON-lines protocol. +//! +//! Protocol version 1 (see `docs/ARBITER_PROTOCOL.adoc` and +//! `src/arbiter/lib/conative_gating/`): +//! +//! ```text +//! request: {"protocol_version":1,"request_id":"…", +//! "llm":{"confidence":0.95}, +//! "slm":{"violation_confidence":0.05}, +//! "oracle":{"verdict":"allow"}} +//! response: {"protocol_version":1,"request_id":"…", +//! "verdict":"allow|escalate|block","reason":"…","audit_recorded":true} +//! error: {"protocol_version":1,"request_id":"…","error":"…"} +//! ``` +//! +//! Design decisions: +//! +//! * **One arbiter process per decision** (spawn per call): stateless, robust +//! against a wedged service, and trivially timeout-enforced. The arbiter is +//! cheap to start relative to an SLM inference. +//! * **Correlation is mandatory**: the response's `request_id` must equal the +//! request's. Per-call processes make cross-request mix-ups structurally +//! impossible; the check is still enforced and tested. +//! * **Audit must be confirmed**: `audit_recorded: true` is required. The +//! "every accepted request has exactly one audit record" invariant is +//! enforced client-side — a decision the arbiter did not durably record is +//! treated as [`ArbiterError::AuditNotConfirmed`] and fails closed. +//! * **Every failure is [`ArbiterError`]**: callers must map arbiter failure +//! to Escalate/NO-GO, never to Allow. + +use serde::{Deserialize, Serialize}; +use std::io::{BufRead, BufReader, Write}; +use std::process::{Command, Stdio}; +use std::time::{Duration, Instant}; +use thiserror::Error; +use uuid::Uuid; + +/// Wire protocol version spoken by this client. +pub const ARBITER_PROTOCOL_VERSION: u32 = 1; + +/// Default arbiter timeout (the arbiter adds negligible latency to an SLM +/// call; 30s is generous). +pub const DEFAULT_ARBITER_TIMEOUT: Duration = Duration::from_secs(30); + +/// The oracle's vote on the wire. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OracleVote { + /// No violation. + Allow, + /// Soft concern raised. + SoftConcern, + /// Hard violation (arbiter must answer `block`). + HardViolation, +} + +/// A consensus decision returned by the arbiter. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ArbiterVerdict { + /// Proposal may proceed. + Allow, + /// Route to human review. + Escalate, + /// Proposal is rejected. + Block, +} + +/// Protocol v1 consensus request. +#[derive(Debug, Clone, Serialize)] +pub struct ArbiterRequest { + protocol_version: u32, + request_id: Uuid, + llm: VoteConfidence, + slm: VoteViolation, + oracle: OracleBallot, +} + +#[derive(Debug, Clone, Serialize)] +struct VoteConfidence { + confidence: f64, +} + +#[derive(Debug, Clone, Serialize)] +struct VoteViolation { + violation_confidence: f64, +} + +#[derive(Debug, Clone, Serialize)] +struct OracleBallot { + verdict: OracleVote, +} + +/// Parsed protocol v1 response (before validation). +#[derive(Debug, Deserialize)] +struct WireResponse { + protocol_version: u32, + request_id: Uuid, + verdict: Option, + #[serde(default)] + reason: Option, + #[serde(default)] + audit_recorded: Option, + #[serde(default)] + error: Option, +} + +/// A validated consensus response. +#[derive(Debug, Clone, PartialEq)] +pub struct ArbiterDecision { + /// Correlated request ID (always equals the request's). + pub request_id: Uuid, + /// Consensus verdict. + pub verdict: ArbiterVerdict, + /// Optional arbiter-provided reason (diagnostics). + pub reason: Option, +} + +/// Every client failure mode (fail-closed: never map these to Allow). +#[derive(Error, Debug)] +pub enum ArbiterError { + /// Spawning or talking to the arbiter process failed. + #[error("arbiter transport failure: {0}")] + Transport(String), + /// The arbiter did not answer within the timeout. + #[error("arbiter timeout: {0}")] + Timeout(String), + /// The arbiter exited without producing a response line. + #[error("arbiter closed without answering: {0}")] + Closed(String), + /// The response line was not a well-formed protocol message. + #[error("malformed arbiter response: {0}")] + Malformed(String), + /// The response declared an unsupported protocol version. + #[error("unsupported arbiter protocol version {0} (client speaks {ARBITER_PROTOCOL_VERSION})")] + ProtocolVersion(u32), + /// The response answered a different request than the one sent. + #[error("arbiter correlation mismatch: expected {expected}, got {got}")] + CorrelationMismatch { + /// The request_id of the request actually sent. + expected: Uuid, + /// The request_id present in the response. + got: Uuid, + }, + /// The arbiter reported an application-level error. + #[error("arbiter service error: {0}")] + Service(String), + /// The arbiter answered a decision it did not durably audit. + #[error("arbiter decision without confirmed audit record")] + AuditNotConfirmed, +} + +/// Client spawning one short-lived arbiter process per decision. +#[derive(Debug)] +pub struct ArbiterClient { + command: Vec, + timeout: Duration, +} + +impl ArbiterClient { + /// `command` is the full invocation (program + args) of a protocol v1 + /// arbiter, e.g. `["/path/to/conative_arbiter"]` or + /// `["escript", "arbiter_protocol.exs"]`. + pub fn new(command: &[&str], timeout: Duration) -> Result { + if command.is_empty() { + return Err(ArbiterError::Transport("empty arbiter command".to_string())); + } + Ok(Self { + command: command.iter().map(ToString::to_string).collect(), + timeout, + }) + } + + /// Build from `CONATIVE_ARBITER_CMD` (split on whitespace, e.g. + /// `escript src/arbiter/priv/arbiter_protocol.exs`). Unset → `Ok(None)`. + pub fn from_env() -> Result, ArbiterError> { + let Ok(raw) = std::env::var("CONATIVE_ARBITER_CMD") else { + return Ok(None); + }; + let parts: Vec<&str> = raw.split_whitespace().collect(); + if parts.is_empty() { + return Ok(None); + } + Ok(Some(Self::new(&parts, DEFAULT_ARBITER_TIMEOUT)?)) + } + + /// Ask the arbiter for a consensus decision. + pub fn decide( + &self, + llm_confidence: f64, + violation_confidence: f64, + oracle_vote: OracleVote, + ) -> Result { + let request = ArbiterRequest { + protocol_version: ARBITER_PROTOCOL_VERSION, + request_id: Uuid::new_v4(), + llm: VoteConfidence { + confidence: llm_confidence, + }, + slm: VoteViolation { + violation_confidence, + }, + oracle: OracleBallot { + verdict: oracle_vote, + }, + }; + let line = serde_json::to_string(&request) + .map_err(|error| ArbiterError::Malformed(error.to_string()))?; + + let mut child = Command::new(&self.command[0]) + .args(&self.command[1..]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| { + ArbiterError::Transport(format!( + "failed to spawn arbiter {}: {error}", + self.command[0] + )) + })?; + + // Write the request and close stdin so stream-driven servers exit. + child + .stdin + .take() + .expect("invariant: stdin piped") + .write_all(line.as_bytes()) + .map_err(|error| ArbiterError::Transport(format!("write to arbiter stdin: {error}")))?; + + let mut stdout = child.stdout.take().expect("invariant: stdout piped"); + let reader = std::thread::spawn(move || { + let mut reader = BufReader::new(&mut stdout); + let mut line = String::new(); + let _ = reader.read_line(&mut line); + line + }); + + let started = Instant::now(); + let status = loop { + match child.try_wait() { + Ok(Some(status)) => break status, + Ok(None) => { + if started.elapsed() >= self.timeout { + let _ = child.kill(); + let _ = child.wait(); + let _ = reader.join(); + return Err(ArbiterError::Timeout(format!( + "no answer within {:?}", + self.timeout + ))); + } + std::thread::sleep(Duration::from_millis(10)); + } + Err(error) => { + let _ = child.kill(); + let _ = child.wait(); + return Err(ArbiterError::Transport(format!( + "waiting on arbiter failed: {error}" + ))); + } + } + }; + + let answer = reader.join().unwrap_or_default(); + let answer = answer.trim(); + if answer.is_empty() { + return Err(ArbiterError::Closed(format!( + "arbiter exited {status} with no response" + ))); + } + Self::validate_response(&request, answer) + } + + /// Validate a response line against the request (extracted for testing). + fn validate_response( + request: &ArbiterRequest, + answer: &str, + ) -> Result { + let wire: WireResponse = serde_json::from_str(answer) + .map_err(|error| ArbiterError::Malformed(format!("{error} (line: {answer:.200})")))?; + + if wire.protocol_version != ARBITER_PROTOCOL_VERSION { + return Err(ArbiterError::ProtocolVersion(wire.protocol_version)); + } + if wire.request_id != request.request_id { + return Err(ArbiterError::CorrelationMismatch { + expected: request.request_id, + got: wire.request_id, + }); + } + if let Some(error) = wire.error { + return Err(ArbiterError::Service(error)); + } + if wire.audit_recorded != Some(true) { + return Err(ArbiterError::AuditNotConfirmed); + } + let verdict = wire + .verdict + .ok_or_else(|| ArbiterError::Malformed("missing verdict".to_string()))?; + Ok(ArbiterDecision { + request_id: wire.request_id, + verdict, + reason: wire.reason, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixture_dir() -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("conative-arbiter-{}", Uuid::new_v4())); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + #[cfg(unix)] + fn make_script(dir: &std::path::Path, name: &str, body: &str) -> std::path::PathBuf { + use std::os::unix::fs::PermissionsExt; + let path = dir.join(name); + std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap(); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap(); + path + } + + /// A well-behaved protocol v1 server: echoes the request_id, answers with + /// a fixed verdict, confirms its audit record. + #[cfg(unix)] + fn good_server(dir: &std::path::Path, verdict: &str) -> std::path::PathBuf { + make_script( + dir, + "arbiter-good.sh", + &format!( + "IFS= read -r line\nid=$(printf '%s' \"$line\" | sed -n 's/.*\"request_id\"[ ]*:[ ]*\"\\([^\"]*\\)\".*/\\1/p')\nprintf '%s\\n' '{{\"protocol_version\":1,\"request_id\":\"'\"$id\"'\",\"verdict\":\"{verdict}\",\"reason\":\"fixture\",\"audit_recorded\":true}}'\n" + ), + ) + } + + #[cfg(unix)] + fn client_for(script: &std::path::Path) -> ArbiterClient { + let s = script.to_string_lossy().to_string(); + ArbiterClient::new(&[s.as_str()], Duration::from_secs(10)).unwrap() + } + + #[test] + #[cfg(unix)] + fn good_arbiter_allow() { + let dir = fixture_dir(); + let script = good_server(&dir, "allow"); + let client = client_for(&script); + let decision = client.decide(0.95, 0.05, OracleVote::Allow).unwrap(); + assert_eq!(decision.verdict, ArbiterVerdict::Allow); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + #[cfg(unix)] + fn good_arbiter_block_on_hard_violation() { + let dir = fixture_dir(); + let script = good_server(&dir, "block"); + let client = client_for(&script); + let decision = client.decide(0.99, 0.0, OracleVote::HardViolation).unwrap(); + assert_eq!(decision.verdict, ArbiterVerdict::Block); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + #[cfg(unix)] + fn protocol_version_mismatch_fails_closed() { + let dir = fixture_dir(); + let script = make_script( + &dir, + "arbiter-v2.sh", + "IFS= read -r line\nid=$(printf '%s' \"$line\" | sed -n 's/.*\"request_id\"[ ]*:[ ]*\"\\([^\"]*\\)\".*/\\1/p')\nprintf '%s\\n' '{\"protocol_version\":2,\"request_id\":\"'\"$id\"'\",\"verdict\":\"allow\",\"audit_recorded\":true}'\n", + ); + let client = client_for(&script); + assert!(matches!( + client.decide(0.5, 0.5, OracleVote::Allow), + Err(ArbiterError::ProtocolVersion(2)) + )); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + #[cfg(unix)] + fn correlation_mismatch_fails_closed() { + let dir = fixture_dir(); + let script = make_script( + &dir, + "arbiter-wrongid.sh", + "IFS= read -r line\nprintf '%s\\n' '{\"protocol_version\":1,\"request_id\":\"00000000-0000-0000-0000-000000000000\",\"verdict\":\"allow\",\"audit_recorded\":true}'\n", + ); + let client = client_for(&script); + assert!(matches!( + client.decide(0.5, 0.5, OracleVote::Allow), + Err(ArbiterError::CorrelationMismatch { .. }) + )); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + #[cfg(unix)] + fn service_error_fails_closed() { + let dir = fixture_dir(); + let script = make_script( + &dir, + "arbiter-error.sh", + "IFS= read -r line\nid=$(printf '%s' \"$line\" | sed -n 's/.*\"request_id\"[ ]*:[ ]*\"\\([^\"]*\\)\".*/\\1/p')\nprintf '%s\\n' '{\"protocol_version\":1,\"request_id\":\"'\"$id\"'\",\"error\":\"consensus unavailable\"}'\n", + ); + let client = client_for(&script); + assert!(matches!( + client.decide(0.5, 0.5, OracleVote::Allow), + Err(ArbiterError::Service(_)) + )); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + #[cfg(unix)] + fn missing_audit_confirmation_fails_closed() { + let dir = fixture_dir(); + let script = make_script( + &dir, + "arbiter-noaudit.sh", + "IFS= read -r line\nid=$(printf '%s' \"$line\" | sed -n 's/.*\"request_id\"[ ]*:[ ]*\"\\([^\"]*\\)\".*/\\1/p')\nprintf '%s\\n' '{\"protocol_version\":1,\"request_id\":\"'\"$id\"'\",\"verdict\":\"allow\",\"audit_recorded\":false}'\n", + ); + let client = client_for(&script); + assert!(matches!( + client.decide(0.5, 0.5, OracleVote::Allow), + Err(ArbiterError::AuditNotConfirmed) + )); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + #[cfg(unix)] + fn garbage_output_fails_closed() { + let dir = fixture_dir(); + let script = make_script(&dir, "arbiter-garbage.sh", "echo 'not json at all'\n"); + let client = client_for(&script); + assert!(matches!( + client.decide(0.5, 0.5, OracleVote::Allow), + Err(ArbiterError::Malformed(_)) + )); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + #[cfg(unix)] + fn closed_output_fails_closed() { + let dir = fixture_dir(); + let script = make_script(&dir, "arbiter-closed.sh", "exit 0\n"); + let client = client_for(&script); + assert!(matches!( + client.decide(0.5, 0.5, OracleVote::Allow), + Err(ArbiterError::Closed(_)) + )); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + #[cfg(unix)] + fn timeout_fails_closed() { + let dir = fixture_dir(); + let script = make_script(&dir, "arbiter-slow.sh", "sleep 5\n"); + let s = script.to_string_lossy().to_string(); + let client = ArbiterClient::new(&[s.as_str()], Duration::from_millis(300)).unwrap(); + assert!(matches!( + client.decide(0.5, 0.5, OracleVote::Allow), + Err(ArbiterError::Timeout(_)) + )); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + #[cfg(unix)] + fn verdict_shape_is_serialized_as_protocol_v1() { + // Serialized request must exactly match the documented wire shape. + let request = ArbiterRequest { + protocol_version: 1, + request_id: Uuid::nil(), + llm: VoteConfidence { confidence: 0.95 }, + slm: VoteViolation { + violation_confidence: 0.05, + }, + oracle: OracleBallot { + verdict: OracleVote::SoftConcern, + }, + }; + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["protocol_version"], 1); + assert_eq!(json["request_id"], Uuid::nil().to_string()); + assert_eq!(json["llm"]["confidence"], 0.95); + assert_eq!(json["slm"]["violation_confidence"], 0.05); + assert_eq!(json["oracle"]["verdict"], "soft_concern"); + } +} diff --git a/src/contract/src/lib.rs b/src/contract/src/lib.rs index d8ec225..9512baa 100644 --- a/src/contract/src/lib.rs +++ b/src/contract/src/lib.rs @@ -19,10 +19,19 @@ use policy_oracle::{ ViolationType, }; use serde::{Deserialize, Serialize}; +use slm_evaluator::{SlmProvider, SlmRequest}; use std::collections::HashMap; use thiserror::Error; use uuid::Uuid; +/// Client for the OTP Consensus Arbiter (JSON-lines protocol v1). +pub mod arbiter; + +pub use arbiter::{ + ArbiterClient, ArbiterDecision, ArbiterError, ArbiterVerdict, OracleVote, + ARBITER_PROTOCOL_VERSION, +}; + // ============================================================================ // CONTRACT VERSION // ============================================================================ @@ -1509,6 +1518,207 @@ impl RedTeamSummary { } } +// ============================================================================ +// SLM STAGE - optional live provider evaluation +// ============================================================================ + +impl ContractRunner { + /// Evaluate a request through the contract **with a live SLM provider**. + /// + /// Stages: `oracle` → `slm` (`slm_error` recorded when the provider + /// fails). Verdict combination mirrors the OTP arbiter's decision matrix + /// with the policy's asymmetric SLM weight (`enforcement.slm_weight`, + /// default 1.5 — inhibition is privileged over GO signals): + /// + /// * an oracle **Block** is terminal and is returned before the provider + /// is ever called (no proposal content leaves the process for a + /// decision already made); + /// * otherwise the weighted no-go score + /// (`spirit_score * slm_weight`, plus 0.2 when the oracle raised a soft + /// concern) is compared against `enforcement.block_threshold` → + /// **Block** (5xx spirit code); a `should_block` recommendation blocks + /// regardless of score; + /// * a no-go at/above `enforcement.escalate_threshold`, or low LLM + /// confidence (`llm_confidence <= 0.8`), → **Escalate**; + /// * otherwise the oracle verdict stands (Allow, or Warn with its + /// original soft refusal). + /// + /// **Fail-closed**: any provider error (timeout, transport, malformed or + /// out-of-range output) prevents Allow/Warn and yields an **Escalate** + /// with a 9xx system code — an SLM outage is never an all-clear. + pub fn evaluate_with_provider( + &self, + request: &GatingRequest, + provider: &dyn SlmProvider, + ) -> Result { + let start = std::time::Instant::now(); + + // Stage 1: Oracle (identical semantics to `evaluate`). + let mut stages_executed = vec!["oracle".to_string()]; + let oracle_eval = self.oracle.check_proposal(&request.proposal)?; + let (oracle_verdict, oracle_refusal) = self.process_oracle_result(&oracle_eval); + + // An oracle BLOCK is terminal: never re-litigated by the SLM, and the + // proposal content is never forwarded to a provider needlessly. + if oracle_verdict == Verdict::Block { + let duration = start.elapsed(); + return Ok(GatingDecision { + request_id: request.request_id, + decision_id: Uuid::new_v4(), + timestamp: Utc::now(), + verdict: oracle_verdict, + refusal: oracle_refusal, + evaluations: EvaluationChain { + oracle: Some(oracle_eval.clone()), + slm: None, + arbiter: None, + }, + processing: ProcessingMetadata { + duration_us: duration.as_micros() as u64, + contract_version: CONTRACT_VERSION.to_string(), + policy_name: self.policy.name.clone(), + rules_checked: oracle_eval.rules_checked.len(), + stages_executed, + }, + }); + } + + // Stage 2: SLM (correlated with the proposal under evaluation). + stages_executed.push("slm".to_string()); + let slm_request = SlmRequest { + proposal_id: request.proposal.id, + content: request.proposal.content.clone(), + context: format!( + "policy={} source={} session={} agent={}", + self.policy.name, + request.context.source, + request.context.session_id.as_deref().unwrap_or("-"), + request.context.agent_id.as_deref().unwrap_or("-"), + ), + // Provider-configured budget applies (0 = provider default). + max_tokens: 0, + }; + + let mut slm_vote: Option = None; + let (slm_stage, verdict, refusal) = match provider.evaluate(&slm_request) { + Ok(evaluation) => { + let slm_result = SlmEvaluationResult { + spirit_score: evaluation.spirit_score, + confidence: evaluation.confidence, + reasoning: evaluation.reasoning.clone(), + should_block: evaluation.should_block, + }; + + // Asymmetric no-go (mirrors the OTP arbiter's decision matrix). + let weight = self.policy.enforcement.slm_weight; + let mut no_go = evaluation.spirit_score * weight; + if matches!(oracle_verdict, Verdict::Warn) { + // Oracle soft concerns add 0.2 to the no-go score. + no_go += 0.2; + } + let go = request.proposal.llm_confidence; + let escalate_at = self.policy.enforcement.escalate_threshold; + let block_at = self.policy.enforcement.block_threshold; + + if evaluation.should_block || no_go >= block_at { + slm_vote = Some(Verdict::Block); + ( + Some(slm_result), + Verdict::Block, + Some(Refusal { + category: RefusalCategory::IntentViolation, + code: RefusalCode::Spirit599OtherSpirit, + message: format!( + "SLM spirit violation (score {:.2}, weighted no-go {:.2}): {}", + evaluation.spirit_score, no_go, evaluation.reasoning + ), + remediation: Some( + "Revise the proposal to match the spirit of the policy".to_string(), + ), + evidence: Vec::new(), + overridable: true, + override_level: Some(AuthorizationLevel::Maintainer), + }), + ) + } else if no_go >= escalate_at || go <= 0.8 { + slm_vote = Some(Verdict::Escalate); + ( + Some(slm_result), + Verdict::Escalate, + Some(Refusal { + category: RefusalCategory::IntentViolation, + code: RefusalCode::Spirit505IntentMismatch, + message: format!( + "uncertain spirit assessment (LLM go {go:.2}, weighted no-go {no_go:.2}): {}", + evaluation.reasoning + ), + remediation: Some( + "Route to human review per the gating policy".to_string(), + ), + evidence: Vec::new(), + overridable: true, + override_level: Some(AuthorizationLevel::User), + }), + ) + } else { + slm_vote = Some(Verdict::Allow); + // Oracle verdict stands (Allow, or Warn with its soft refusal). + (Some(slm_result), oracle_verdict, oracle_refusal) + } + } + Err(error) => { + stages_executed.push("slm_error".to_string()); + ( + None, + Verdict::Escalate, + Some(Refusal { + category: RefusalCategory::SystemError, + code: RefusalCode::Sys902InternalError, + message: format!( + "SLM evaluation failed; failing closed rather than allowing: {error}" + ), + remediation: Some( + "Restore the SLM provider, or review the proposal manually".to_string(), + ), + evidence: Vec::new(), + overridable: false, + override_level: Some(AuthorizationLevel::Admin), + }), + ) + } + }; + + let arbiter = slm_vote.map(|vote| ArbiterResult { + consensus_reached: true, + oracle_vote: oracle_verdict, + slm_vote: vote, + final_verdict: verdict, + slm_weight: self.policy.enforcement.slm_weight, + }); + + let duration = start.elapsed(); + Ok(GatingDecision { + request_id: request.request_id, + decision_id: Uuid::new_v4(), + timestamp: Utc::now(), + verdict, + refusal, + evaluations: EvaluationChain { + oracle: Some(oracle_eval.clone()), + slm: slm_stage, + arbiter, + }, + processing: ProcessingMetadata { + duration_us: duration.as_micros() as u64, + contract_version: CONTRACT_VERSION.to_string(), + policy_name: self.policy.name.clone(), + rules_checked: oracle_eval.rules_checked.len(), + stages_executed, + }, + }) + } +} + // ============================================================================ // UNIT TESTS // ============================================================================ @@ -1865,8 +2075,8 @@ mod tests { let results = harness.run_all(&tests); assert_eq!(results.len(), 2); - assert_eq!(results[0].passed, true); - assert_eq!(results[1].passed, true); + assert!(results[0].passed); + assert!(results[1].passed); } #[test] @@ -2118,3 +2328,249 @@ mod tests { assert!(metadata.stages_executed.is_empty()); } } + +// ============================================================================ +// SLM STAGE TESTS — evaluate_with_provider with mock providers +// ============================================================================ + +#[cfg(test)] +mod slm_stage_tests { + use super::*; + use policy_oracle::ActionType; + use slm_evaluator::{SlmError, SlmEvaluation, SlmProvider, SlmRequest}; + use std::sync::atomic::{AtomicUsize, Ordering}; + + /// Configurable canned-verdict provider; counts invocations. + struct MockSlm { + spirit_score: f64, + should_block: bool, + calls: AtomicUsize, + } + + impl MockSlm { + fn verdict(spirit_score: f64, should_block: bool) -> Self { + Self { + spirit_score, + should_block, + calls: AtomicUsize::new(0), + } + } + + fn clean() -> Self { + Self::verdict(0.05, false) + } + + fn calls(&self) -> usize { + self.calls.load(Ordering::SeqCst) + } + } + + impl SlmProvider for MockSlm { + fn name(&self) -> &str { + "mock-slm" + } + + fn evaluate(&self, request: &SlmRequest) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + Ok(SlmEvaluation { + proposal_id: request.proposal_id, + spirit_score: self.spirit_score, + confidence: 0.9, + reasoning: "mock verdict".to_string(), + should_block: self.should_block, + }) + } + } + + struct FailingSlm; + + impl SlmProvider for FailingSlm { + fn name(&self) -> &str { + "failing-mock-slm" + } + + fn evaluate(&self, _request: &SlmRequest) -> Result { + Err(SlmError::Timeout("mock provider timeout".to_string())) + } + } + + fn request_for(path: &str, content: &str, llm_confidence: f32) -> GatingRequest { + GatingRequest::new(Proposal { + id: Uuid::new_v4(), + action_type: ActionType::CreateFile { + path: path.to_string(), + }, + content: content.to_string(), + files_affected: vec![path.to_string()], + llm_confidence, + }) + } + + #[test] + fn oracle_block_is_terminal_and_never_calls_provider() { + let runner = ContractRunner::new(); + let provider = MockSlm::clean(); + let request = request_for("util.ts", "const x: string = 'y';", 0.95); + + let decision = runner.evaluate_with_provider(&request, &provider).unwrap(); + + assert_eq!(decision.verdict, Verdict::Block); + assert_eq!(provider.calls(), 0, "provider must not run on oracle block"); + assert!(decision.evaluations.slm.is_none()); + assert!(decision.evaluations.arbiter.is_none()); + assert_eq!( + decision.processing.stages_executed, + vec!["oracle".to_string()] + ); + } + + #[test] + fn clean_slm_keeps_oracle_allow() { + let runner = ContractRunner::new(); + let provider = MockSlm::clean(); + let request = request_for("src/main.rs", "fn main() {}", 0.95); + + let decision = runner.evaluate_with_provider(&request, &provider).unwrap(); + + assert_eq!(decision.verdict, Verdict::Allow); + assert!(decision.refusal.is_none()); + assert!(decision.evaluations.slm.is_some()); + let arbiter = decision.evaluations.arbiter.expect("arbiter result"); + assert!(arbiter.consensus_reached); + assert_eq!(arbiter.oracle_vote, Verdict::Allow); + assert_eq!(arbiter.slm_vote, Verdict::Allow); + assert_eq!(arbiter.final_verdict, Verdict::Allow); + assert!((arbiter.slm_weight - 1.5).abs() < f64::EPSILON); + assert_eq!( + decision.processing.stages_executed, + vec!["oracle".to_string(), "slm".to_string()] + ); + } + + #[test] + fn high_weighted_spirit_score_blocks() { + let runner = ContractRunner::new(); + // 0.9 * 1.5 = 1.35 >= block_threshold (0.7) + let provider = MockSlm::verdict(0.9, false); + let request = request_for("src/main.rs", "fn main() {}", 0.95); + + let decision = runner.evaluate_with_provider(&request, &provider).unwrap(); + + assert_eq!(decision.verdict, Verdict::Block); + let refusal = decision.refusal.expect("spirit refusal"); + assert_eq!(refusal.category, RefusalCategory::IntentViolation); + assert_eq!(refusal.code, RefusalCode::Spirit599OtherSpirit); + assert!(refusal.overridable); + assert_eq!(refusal.override_level, Some(AuthorizationLevel::Maintainer)); + assert_eq!( + decision.evaluations.arbiter.unwrap().slm_vote, + Verdict::Block + ); + } + + #[test] + fn should_block_flag_blocks_regardless_of_score() { + let runner = ContractRunner::new(); + // Low score but the model's hard recommendation is to block. + let provider = MockSlm::verdict(0.01, true); + let request = request_for("src/main.rs", "fn main() {}", 0.95); + + let decision = runner.evaluate_with_provider(&request, &provider).unwrap(); + + assert_eq!(decision.verdict, Verdict::Block); + assert_eq!( + decision.refusal.unwrap().code, + RefusalCode::Spirit599OtherSpirit + ); + } + + #[test] + fn mid_weighted_score_escalates() { + let runner = ContractRunner::new(); + // 0.35 * 1.5 = 0.525 >= escalate_threshold (0.4), below block (0.7) + let provider = MockSlm::verdict(0.35, false); + let request = request_for("src/main.rs", "fn main() {}", 0.95); + + let decision = runner.evaluate_with_provider(&request, &provider).unwrap(); + + assert_eq!(decision.verdict, Verdict::Escalate); + let refusal = decision.refusal.expect("escalation refusal"); + assert_eq!(refusal.code, RefusalCode::Spirit505IntentMismatch); + assert_eq!( + decision.evaluations.arbiter.unwrap().slm_vote, + Verdict::Escalate + ); + } + + #[test] + fn low_llm_confidence_escalates_despite_clean_slm() { + let runner = ContractRunner::new(); + let provider = MockSlm::clean(); + let request = request_for("src/main.rs", "fn main() {}", 0.5); + + let decision = runner.evaluate_with_provider(&request, &provider).unwrap(); + + assert_eq!(decision.verdict, Verdict::Escalate); + } + + #[test] + fn oracle_soft_concern_is_escalated_by_slm_weighting() { + let runner = ContractRunner::new(); + // Tier-2 language fixture: oracle Warn (racket marker present). + let warn_request = request_for("script.rkt", "#lang racket", 0.95); + + // Clean SLM (0.05*1.5 + 0.2 = 0.275 < 0.4): Warn stands. + let provider = MockSlm::clean(); + let decision = runner + .evaluate_with_provider(&warn_request, &provider) + .unwrap(); + assert_eq!(decision.verdict, Verdict::Warn); + + // Moderate SLM (0.15*1.5 + 0.2 = 0.425 >= 0.4): Escalate. + let provider = MockSlm::verdict(0.15, false); + let decision = runner + .evaluate_with_provider(&warn_request, &provider) + .unwrap(); + assert_eq!(decision.verdict, Verdict::Escalate); + } + + #[test] + fn provider_failure_escalates_fail_closed() { + let runner = ContractRunner::new(); + let request = request_for("src/main.rs", "fn main() {}", 0.95); + + let decision = runner + .evaluate_with_provider(&request, &FailingSlm) + .unwrap(); + + assert_eq!(decision.verdict, Verdict::Escalate); + let refusal = decision.refusal.expect("system refusal"); + assert_eq!(refusal.category, RefusalCategory::SystemError); + assert_eq!(refusal.code, RefusalCode::Sys902InternalError); + assert!( + !refusal.overridable, + "system failure must not be overridable" + ); + assert!(refusal.message.contains("failing closed")); + assert!(decision.evaluations.slm.is_none()); + assert!(decision.evaluations.arbiter.is_none()); + assert!(decision + .processing + .stages_executed + .contains(&"slm_error".to_string())); + } + + #[test] + fn warnings_do_not_lose_their_soft_refusal_when_slm_is_clean() { + let runner = ContractRunner::new(); + let provider = MockSlm::clean(); + let request = request_for("script.rkt", "#lang racket", 0.95); + + let decision = runner.evaluate_with_provider(&request, &provider).unwrap(); + + assert_eq!(decision.verdict, Verdict::Warn); + let refusal = decision.refusal.expect("soft refusal preserved"); + assert_eq!(refusal.category, RefusalCategory::ForbiddenLanguage); + assert_eq!(refusal.code, RefusalCode::Lang199OtherForbidden); + } +} diff --git a/src/main.rs b/src/main.rs index 9d618d3..49544f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -115,7 +115,7 @@ struct Cli { #[arg(long, global = true)] no_color: bool, - /// Custom policy file in JSON format (the embedded Nickel file is not loaded at runtime) + /// Custom policy file (JSON; native .ncl when built with --features nickel) #[arg(short, long, global = true)] policy_file: Option, @@ -335,6 +335,15 @@ enum ContractAction { /// Include audit log entry in output #[arg(long)] audit: bool, + + /// Route the request through the configured SLM provider + /// + /// Requires CONATIVE_SLM_PROVIDER=llama (with CONATIVE_GGUF_MODEL, and + /// optionally CONATIVE_LLAMA_CLI) or CONATIVE_SLM_PROVIDER=http (with + /// CONATIVE_SLM_ENDPOINT; SLM_API_KEY for auth; binary built with + /// --features slm-http). Provider failures escalate fail-closed. + #[arg(long)] + slm: bool, }, /// Display contract schema information @@ -514,12 +523,13 @@ fn main() { request, format, audit, + slm, } => { if cli.dry_run { println!("[dry-run] Would evaluate request: {}", request.display()); 0 } else { - eval_contract_request(&request, &format, audit) + eval_contract_request(&request, &format, audit, slm) } } ContractAction::Schema { format, section } => { @@ -567,16 +577,9 @@ fn main() { } fn load_policy_oracle(path: &Path) -> Result { - let content = std::fs::read_to_string(path).map_err(|error| error.to_string())?; - if path.extension().is_some_and(|extension| extension == "ncl") { - return Err( - "Nickel policy loading is not available in the Rust CLI yet; provide a JSON policy export" - .to_string(), - ); - } - let policy: Policy = serde_json::from_str(&content) - .map_err(|error| format!("invalid JSON policy {}: {error}", path.display()))?; - Ok(Oracle::new(policy)) + // Dispatch lives in the oracle: JSON everywhere, native `.ncl` when the + // binary was built with the `nickel` feature — failing closed otherwise. + Oracle::from_policy_file(path).map_err(|error| error.to_string()) } fn scan_directory( @@ -1196,7 +1199,12 @@ fn load_test_case_file(path: &Path) -> Result { }) } -fn eval_contract_request(request_path: &Path, format: &OutputFormat, include_audit: bool) -> i32 { +fn eval_contract_request( + request_path: &Path, + format: &OutputFormat, + include_audit: bool, + use_slm: bool, +) -> i32 { let content = match std::fs::read_to_string(request_path) { Ok(c) => c, Err(e) => { @@ -1214,7 +1222,30 @@ fn eval_contract_request(request_path: &Path, format: &OutputFormat, include_aud }; let runner = ContractRunner::new(); - let decision = match runner.evaluate(&request) { + let decision_result = if use_slm { + // Explicit request for a live SLM stage: provider configuration is + // mandatory, misconfiguration is loud (exit 3), and provider failures + // escalate fail-closed inside the contract. + match slm_evaluator::from_env() { + Ok(Some(provider)) => runner.evaluate_with_provider(&request, provider.as_ref()), + Ok(None) => { + eprintln!( + "--slm requested but no SLM provider is configured. Set \ + CONATIVE_SLM_PROVIDER=llama (+CONATIVE_GGUF_MODEL, CONATIVE_LLAMA_CLI) \ + or CONATIVE_SLM_PROVIDER=http (+CONATIVE_SLM_ENDPOINT; SLM_API_KEY; \ + build with --features slm-http)." + ); + return 3; + } + Err(error) => { + eprintln!("SLM provider misconfigured: {error}"); + return 3; + } + } + } else { + runner.evaluate(&request) + }; + let decision = match decision_result { Ok(d) => d, Err(e) => { eprintln!("Error evaluating request: {}", e); diff --git a/src/oracle/Cargo.toml b/src/oracle/Cargo.toml index 0e21d87..100c99b 100644 --- a/src/oracle/Cargo.toml +++ b/src/oracle/Cargo.toml @@ -15,3 +15,14 @@ thiserror.workspace = true tracing.workspace = true glob = "0.3" regex = "1" +# Native Nickel policy evaluation via the reviewed vendor fork in +# vendor/bunsenite (see vendor/bunsenite/VENDOR.adoc). Default features are +# disabled on the vendor fork: no REPL/format/doc/markdown feature load. +bunsenite = { path = "../../vendor/bunsenite", optional = true, default-features = false } + +[features] +# Native Nickel (.ncl) policy loading. Off by default: default builds compile +# without the Nickel dependency tree, and .ncl policy files are rejected +# fail-closed instead of being silently ignored. +default = [] +nickel = ["dep:bunsenite"] diff --git a/src/oracle/src/lib.rs b/src/oracle/src/lib.rs index ced0ef3..775d061 100644 --- a/src/oracle/src/lib.rs +++ b/src/oracle/src/lib.rs @@ -15,6 +15,10 @@ use std::path::{Path, PathBuf}; use thiserror::Error; use uuid::Uuid; +/// Native Nickel policy loading (requires the `nickel` feature). +#[cfg(feature = "nickel")] +pub mod nickel; + // ============ Core Types ============ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -234,6 +238,19 @@ pub enum OracleError { RegexError(#[from] regex::Error), #[error("Invalid glob pattern: {0}")] GlobError(String), + /// Native Nickel evaluation failed: parse error, evaluation error, or the + /// evaluated record does not match the `Policy` contract. + #[cfg(feature = "nickel")] + #[error("native Nickel policy evaluation failed: {0}")] + NickelEvaluation(String), + /// A Nickel policy containing an `import` statement was rejected + /// fail-closed. Multi-file Nickel policies are intentionally unsupported; + /// see `docs/NICKEL-POLICY.adoc`. + #[cfg(feature = "nickel")] + #[error( + "Nickel policy imports are not supported (fail-closed); inline the policy or export JSON (found: {0})" + )] + NickelImportUnsupported(String), } // ============ Oracle Implementation ============ @@ -251,6 +268,16 @@ impl Oracle { Self::new(Policy::rsr_default()) } + /// Construct an oracle from a policy file on disk. + /// + /// Dispatch is extension-based: `.ncl` files use + /// [`Policy::from_policy_file`]'s native Nickel path (which requires the + /// `nickel` feature and fails closed without it); everything else is + /// parsed as JSON. + pub fn from_policy_file(path: &Path) -> Result { + Ok(Self::new(Policy::from_policy_file(path)?)) + } + /// Check a proposal against policy pub fn check_proposal(&self, proposal: &Proposal) -> Result { let mut rules_checked = Vec::new(); @@ -659,6 +686,54 @@ fn push_unique_concern(concerns: &mut Vec, candidate: FileConcern) // ============ Default Policy ============ +impl Policy { + /// Load a policy from disk, dispatching on the file extension. + /// + /// * `.ncl` — evaluated as native Nickel. Requires the `nickel` feature; + /// without it this is a fail-closed error rather than a silent fallback + /// to a compiled-in policy. + /// * anything else — deserialised as JSON. + pub fn from_policy_file(path: &Path) -> Result { + if path.extension().is_some_and(|extension| extension == "ncl") { + #[cfg(feature = "nickel")] + { + return Self::from_nickel_file(path); + } + #[cfg(not(feature = "nickel"))] + { + return Err(OracleError::PolicyParseError(format!( + "native Nickel policy support is not compiled in; rebuild with \ + --features nickel (see docs/NICKEL-POLICY.adoc) or export the \ + policy as JSON: {}", + path.display() + ))); + } + } + let content = fs::read_to_string(path)?; + serde_json::from_str(&content).map_err(|error| { + OracleError::PolicyParseError(format!( + "invalid JSON policy {}: {error}", + path.display() + )) + }) + } + + /// Evaluate Nickel source to a policy (requires the `nickel` feature). + /// + /// Sources containing `import` statements are rejected fail-closed; see + /// [`nickel::reject_imports`]. + #[cfg(feature = "nickel")] + pub fn from_nickel_source(content: &str, source_name: &str) -> Result { + nickel::policy_from_nickel_source(content, source_name) + } + + /// Load and evaluate a `.ncl` policy file (requires the `nickel` feature). + #[cfg(feature = "nickel")] + pub fn from_nickel_file(path: &Path) -> Result { + nickel::policy_from_nickel_file(path) + } +} + impl Policy { /// RSR-compliant default policy pub fn rsr_default() -> Self { @@ -1285,3 +1360,137 @@ mod tests { )); } } + +// ============ Native Nickel feature tests ============ +// +// These run under the dedicated CI job: +// CARGO_BUILD_JOBS=1 RUSTFLAGS="-C debuginfo=0 -Dwarnings" \ +// cargo test -p policy-oracle --features nickel --lib --locked + +#[cfg(all(test, feature = "nickel"))] +mod nickel_feature_tests { + use super::*; + + /// The repository policy is the native fixture: it must evaluate and must + /// agree exactly with the compiled-in RSR default policy. + #[test] + fn repository_policy_ncl_matches_rsr_default() { + let policy_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("config") + .join("policy.ncl"); + let policy = Policy::from_nickel_file(&policy_path).unwrap_or_else(|error| { + panic!( + "repository policy {} failed to evaluate: {error}", + policy_path.display() + ) + }); + + let expected = serde_json::to_value(Policy::rsr_default()).unwrap(); + let actual = serde_json::to_value(policy).unwrap(); + assert_eq!( + actual, expected, + "config/policy.ncl and Policy::rsr_default() diverged" + ); + } + + #[test] + fn from_policy_file_dispatches_ncl_extension() { + let policy_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("config") + .join("policy.ncl"); + let oracle = Oracle::from_policy_file(&policy_path).expect("dispatch and evaluate"); + let proposal = Proposal { + id: Uuid::new_v4(), + action_type: ActionType::CreateFile { + path: "util.ts".to_string(), + }, + content: "const x: string = 'y'".to_string(), + files_affected: vec!["util.ts".to_string()], + llm_confidence: 0.9, + }; + let result = oracle.check_proposal(&proposal).unwrap(); + assert!(matches!(result.verdict, PolicyVerdict::HardViolation(_))); + } + + #[test] + fn import_statement_is_rejected_fail_closed() { + let source = r#" +let base = import "./shared.ncl" in +{ name = "Uses shared base" } +"#; + let result = Policy::from_nickel_source(source, ""); + assert!( + matches!(result, Err(OracleError::NickelImportUnsupported(_))), + "import must be rejected fail-closed, got: {result:?}" + ); + } + + #[test] + fn import_mention_in_comment_is_not_rejected() { + let source = "# to reuse the base, `import \"shared.ncl\"` — kept as prose\n"; + assert!( + nickel::reject_imports(source).is_ok(), + "comment-only import mention must not trip the scanner" + ); + } + + #[test] + fn invalid_nickel_is_an_evaluation_error() { + let result = Policy::from_nickel_source("{ this is not = valid nickel", ""); + assert!(matches!(result, Err(OracleError::NickelEvaluation(_)))); + } + + #[test] + fn evaluated_record_must_match_policy_contract() { + let source = "{ name = 5 }"; + let result = Policy::from_nickel_source(source, ""); + assert!(matches!(result, Err(OracleError::NickelEvaluation(_)))); + } + + #[test] + fn json_policy_files_still_load() { + let path = std::env::temp_dir().join(format!("conative-policy-{}.json", Uuid::new_v4())); + let json = serde_json::to_string_pretty(&Policy::rsr_default()).unwrap(); + fs::write(&path, &json).unwrap(); + let loaded = Policy::from_policy_file(&path).expect("JSON policy loads"); + assert_eq!( + serde_json::to_value(loaded).unwrap(), + serde_json::to_value(Policy::rsr_default()).unwrap() + ); + let _ = fs::remove_file(path); + } +} + +/// Without the `nickel` feature, `.ncl` dispatch must fail closed: it is an +/// explicit error, never a silent fallback to any compiled-in policy. +#[cfg(all(test, not(feature = "nickel")))] +mod nickel_guard_tests { + use super::*; + + #[test] + fn ncl_policy_dispatch_fails_closed_without_feature() { + let path = std::env::temp_dir().join(format!("conative-policy-{}.ncl", Uuid::new_v4())); + fs::write(&path, "{ name = \"fixture\" }").unwrap(); + let result = Policy::from_policy_file(&path); + let _ = fs::remove_file(&path); + match result { + Err(OracleError::PolicyParseError(message)) => { + assert!(message.contains("--features nickel")); + } + other => panic!("expected fail-closed PolicyParseError, got: {other:?}"), + } + } + + #[test] + fn json_policy_files_load_without_feature() { + let path = std::env::temp_dir().join(format!("conative-policy-{}.json", Uuid::new_v4())); + let json = serde_json::to_string(&Policy::rsr_default()).unwrap(); + fs::write(&path, json).unwrap(); + assert!(Policy::from_policy_file(&path).is_ok()); + let _ = fs::remove_file(path); + } +} diff --git a/src/oracle/src/nickel.rs b/src/oracle/src/nickel.rs new file mode 100644 index 0000000..144b1e5 --- /dev/null +++ b/src/oracle/src/nickel.rs @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Native Nickel (`.ncl`) policy loading via the vendored Bunsenite evaluator. +//! +//! ## Fail-closed design +//! +//! * This module only exists with `--features nickel`. Without the feature, +//! `.ncl` dispatch in [`crate::Policy::from_policy_file`] returns an error +//! instead of silently falling back to any default policy. +//! * Nickel `import` statements are **rejected before evaluation** +//! ([`OracleError::NickelImportUnsupported`]). The vendored Bunsenite +//! revision builds its Nickel program from only the file name, so import +//! resolution relies on ambient evaluator behaviour. Rather than accept +//! partially-resolved or ambient-dependent policies, multi-file imports are +//! not supported: inline the policy, or export it to JSON. See +//! `docs/NICKEL-POLICY.adoc`. +//! +//! ## Import scan +//! +//! [`reject_imports`] is a conservative single-pass scanner: it finds the +//! `import` keyword applied to a string literal (`import "foo.ncl"` or +//! `import 'foo.ncl'`, where Nickel's inter-string form is also quoted), +//! ignoring `#` line comments. Multiline/embedded occurrences inside string +//! *values* may cause false positives; those fail closed (policy rejected), +//! which is the safe direction, and they are documented in +//! `docs/NICKEL-POLICY.adoc`. + +use crate::{OracleError, Policy}; +use regex::Regex; +use std::path::Path; +use std::sync::OnceLock; + +/// `import` applied to a quoted path, e.g. `let base = import "./lib.ncl" in`. +fn import_pattern() -> &'static Regex { + static RE: OnceLock = OnceLock::new(); + RE.get_or_init(|| { + Regex::new(r#"(?:^|[\s=(,\[{])import\s+["']"#).expect("static regex compiles") + }) +} + +/// Strip a trailing `#` line comment, respecting simple double-quoted ranges. +/// Nickel multiline strings (`m#"..."#`) are not fully tokenised; an +/// `import "..."` fragment inside one is treated as code (conservative). +fn strip_line_comment(line: &str) -> &str { + let mut in_string = false; + let mut prev = '\0'; + for (idx, ch) in line.char_indices() { + match ch { + '"' if prev != '\\' => in_string = !in_string, + '#' if !in_string => return &line[..idx], + _ => {} + } + prev = ch; + } + line +} + +/// Find the first `import`-of-a-path statement outside line comments. +fn find_import(content: &str) -> Option { + for line in content.lines() { + let code = strip_line_comment(line); + if let Some(found) = import_pattern().find(code) { + let excerpt: String = code[found.start()..] + .trim_start() + .chars() + .take(48) + .collect(); + return Some(excerpt); + } + } + None +} + +/// Reject any Nickel source that imports another file. +pub fn reject_imports(content: &str) -> Result<(), OracleError> { + match find_import(content) { + Some(excerpt) => Err(OracleError::NickelImportUnsupported(excerpt)), + None => Ok(()), + } +} + +/// Evaluate Nickel source to a [`Policy`] via the vendored Bunsenite evaluator. +/// +/// `source_name` is only used for evaluator/error diagnostics (e.g. the file +/// name or ``); evaluation does not perform filesystem access beyond +/// what the evaluator itself requires. +pub fn policy_from_nickel_source(content: &str, source_name: &str) -> Result { + reject_imports(content)?; + + let loader = bunsenite::NickelLoader::new(); + let value = loader + .parse_string(content, source_name) + .map_err(|error| OracleError::NickelEvaluation(error.to_string()))?; + + serde_json::from_value::(value).map_err(|error| { + OracleError::NickelEvaluation(format!( + "evaluated Nickel policy {source_name} does not match the Policy contract: {error}" + )) + }) +} + +/// Load and evaluate a `.ncl` policy file. +pub fn policy_from_nickel_file(path: &Path) -> Result { + let content = std::fs::read_to_string(path).map_err(OracleError::IoError)?; + let name = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + policy_from_nickel_source(&content, &name) +} diff --git a/src/slm/Cargo.toml b/src/slm/Cargo.toml index a3b5e4d..1974cd4 100644 --- a/src/slm/Cargo.toml +++ b/src/slm/Cargo.toml @@ -13,4 +13,17 @@ serde_json.workspace = true uuid.workspace = true thiserror.workspace = true tracing.workspace = true -# llama-cpp-2 = "0.1" # Uncomment when ready for SLM integration +# Optional blocking HTTPS client for the remote (OpenAI-compatible) SLM +# provider. Rustls only: no native TLS system dependency is introduced. +reqwest = { version = "0.12", default-features = false, features = [ + "blocking", + "rustls-tls", + "json", +], optional = true } +# llama-cpp-2 = "0.1" # Uncomment when ready for in-process SLM integration + +[features] +# Remote SLM provider over HTTPS. Off by default; local llama.cpp provider +# works without it. +default = [] +http = ["dep:reqwest"] diff --git a/src/slm/src/http.rs b/src/slm/src/http.rs new file mode 100644 index 0000000..853eeb5 --- /dev/null +++ b/src/slm/src/http.rs @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Remote SLM provider over HTTPS (OpenAI-compatible chat-completions). +//! +//! Enabled with `--features http`. Honours the provider contract in +//! [`crate::provider`]: deterministic decoding (`temperature: 0`, +//! `response_format: json_object`, explicit token budget), strict response +//! shape, range validation, correlation echo, fail-closed errors. +//! +//! Endpoints must be `https://`. Plain `http://` is accepted **only** for +//! loopback hosts (`127.0.0.1`, `::1`, `localhost`) so local servers (e.g. +//! `llama-server`) and test fixtures can be exercised without TLS. This is a +//! fail-closed guard against transmitting requests — and credentials — in +//! cleartext. +//! +//! The API key is read from the environment (`SLM_API_KEY`) and never +//! logged or embedded in errors. + +use crate::provider::{ + build_prompt, complete_evaluation, parse_verdict, SlmProvider, DEFAULT_MAX_TOKENS, + DEFAULT_TIMEOUT, +}; +use crate::{SlmError, SlmEvaluation}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +/// Blocking, OpenAI-compatible remote provider. +#[derive(Debug, Clone)] +pub struct HttpSlmProvider { + endpoint: String, + model: String, + api_key: Option, + max_tokens: u32, + timeout: Duration, +} + +impl HttpSlmProvider { + /// Create a provider for `endpoint` serving `model`. `endpoint` is the + /// server base — the OpenAI path `/v1/chat/completions` is appended. + pub fn new( + endpoint: impl Into, + model: impl Into, + api_key: Option, + ) -> Result { + let endpoint = endpoint.into(); + let endpoint = endpoint.trim_end_matches('/').to_string(); + if endpoint.is_empty() { + return Err(SlmError::NotConfigured( + "SLM endpoint must not be empty".to_string(), + )); + } + reject_plaintext_off_loopback(&endpoint)?; + Ok(Self { + endpoint, + model: model.into(), + api_key, + max_tokens: DEFAULT_MAX_TOKENS, + timeout: DEFAULT_TIMEOUT, + }) + } + + /// Override the default token budget and timeout. + pub fn with_limits(mut self, max_tokens: u32, timeout: Duration) -> Self { + self.max_tokens = max_tokens; + self.timeout = timeout; + self + } + + /// Build from the process environment: + /// + /// * `CONATIVE_SLM_ENDPOINT` — server base URL (required for + /// `Ok(Some(_))`; unset means the HTTP provider is not configured). + /// * `CONATIVE_SLM_MODEL_NAME` — model identifier (default `local-slm`). + /// * `SLM_API_KEY` — optional bearer token. + /// * `CONATIVE_SLM_MAX_TOKENS` / `CONATIVE_SLM_TIMEOUT_SECS` — as for the + /// local provider. + /// + /// The endpoint and key belong in a protected GitHub Environment for CI + /// smoke tests; they must never be exposed to untrusted PR code. See + /// `docs/SLM_PROVIDERS.adoc`. + pub fn from_env() -> Result, SlmError> { + let Ok(endpoint) = std::env::var("CONATIVE_SLM_ENDPOINT") else { + return Ok(None); + }; + let model = + std::env::var("CONATIVE_SLM_MODEL_NAME").unwrap_or_else(|_| "local-slm".to_string()); + let api_key = std::env::var("SLM_API_KEY") + .ok() + .filter(|key| !key.trim().is_empty()); + let mut provider = Self::new(endpoint, model, api_key)?; + if let Ok(tokens) = std::env::var("CONATIVE_SLM_MAX_TOKENS") { + provider.max_tokens = tokens.parse().map_err(|_| { + SlmError::NotConfigured("CONATIVE_SLM_MAX_TOKENS must be an integer".to_string()) + })?; + } + if let Ok(secs) = std::env::var("CONATIVE_SLM_TIMEOUT_SECS") { + let secs: u64 = secs.parse().map_err(|_| { + SlmError::NotConfigured("CONATIVE_SLM_TIMEOUT_SECS must be an integer".to_string()) + })?; + provider.timeout = Duration::from_secs(secs); + } + Ok(Some(provider)) + } + + /// The full chat-completions URL (diagnostics/tests). + pub fn completions_url(&self) -> String { + format!("{}/v1/chat/completions", self.endpoint) + } +} + +/// Fail closed when a plaintext endpoint is not loopback. +fn reject_plaintext_off_loopback(endpoint: &str) -> Result<(), SlmError> { + if endpoint.starts_with("https://") { + return Ok(()); + } + if let Some(rest) = endpoint.strip_prefix("http://") { + let authority = rest.split('/').next().unwrap_or_default(); + // Bracketed IPv6 (`[::1]:8080`) vs. `host:port`. + let host = if let Some(bracketed) = authority.strip_prefix('[') { + bracketed.split(']').next().unwrap_or_default() + } else { + authority.split(':').next().unwrap_or_default() + }; + if matches!(host, "127.0.0.1" | "localhost" | "::1") { + return Ok(()); + } + } + Err(SlmError::NotConfigured(format!( + "refusing non-loopback plaintext SLM endpoint (use https://, or a loopback \ + address for local servers): {endpoint}" + ))) +} + +#[derive(Serialize)] +struct ChatRequest<'a> { + model: &'a str, + messages: [ChatMessage<'a>; 1], + temperature: f64, + max_tokens: u32, + response_format: ResponseFormat, +} + +#[derive(Serialize)] +struct ChatMessage<'a> { + role: &'a str, + content: String, +} + +#[derive(Serialize)] +struct ResponseFormat { + #[serde(rename = "type")] + kind: &'static str, +} + +#[derive(Deserialize)] +struct ChatResponse { + choices: Vec, +} + +#[derive(Deserialize)] +struct ChatChoice { + message: ChatResponseMessage, +} + +#[derive(Deserialize)] +struct ChatResponseMessage { + content: String, +} + +impl SlmProvider for HttpSlmProvider { + fn name(&self) -> &str { + "http-openai-compatible" + } + + fn evaluate(&self, request: &crate::provider::SlmRequest) -> Result { + let tokens = match request.max_tokens { + 0 => self.max_tokens, + requested => requested.min(self.max_tokens.max(1)), + }; + let body = ChatRequest { + model: &self.model, + messages: [ChatMessage { + role: "user", + content: build_prompt(request), + }], + temperature: 0.0, + max_tokens: tokens, + response_format: ResponseFormat { + kind: "json_object", + }, + }; + + let client = reqwest::blocking::Client::builder() + .timeout(self.timeout) + .build() + .map_err(|error| { + SlmError::Transport(format!("failed to build HTTP client: {error}")) + })?; + + let mut call = client.post(self.completions_url()).json(&body); + if let Some(key) = &self.api_key { + call = call.bearer_auth(key); + } + + let response = call.send().map_err(|error| { + let hint = if error.is_timeout() { + "request timed out" + } else { + "transport error" + }; + SlmError::Transport(format!("{hint} calling SLM endpoint: {error}")) + })?; + + let status = response.status(); + if !status.is_success() { + let detail = response + .text() + .unwrap_or_default() + .chars() + .take(200) + .collect::(); + return Err(SlmError::Transport(format!( + "SLM endpoint returned {status}: {detail}" + ))); + } + + let parsed: ChatResponse = response.json().map_err(|error| { + SlmError::InvalidResponse(format!( + "endpoint returned non-chat-completions JSON: {error}" + )) + })?; + let content = parsed + .choices + .first() + .map(|choice| choice.message.content.as_str()) + .ok_or_else(|| { + SlmError::InvalidResponse("endpoint returned zero choices".to_string()) + })?; + + // The message content must itself be the provider JSON verdict — + // exactly like the local provider's stdout. + let verdict = parse_verdict(content)?; + Ok(complete_evaluation(request, verdict)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::provider::{SlmProvider, SlmRequest}; + use std::io::{BufRead, BufReader, Read, Write}; + use std::net::TcpListener; + use uuid::Uuid; + + /// Minimal std-only HTTP/1.1 test server: reads one request (headers + + /// Content-Length body), returns a canned response, and records the + /// request line, an optional Authorization header, and the raw body. + struct MockServer { + base_url: String, + request_line: std::sync::Arc>, + auth_header: std::sync::Arc>, + request_body: std::sync::Arc>, + handle: Option>, + } + + impl MockServer { + fn start(status_line: &'static str, response_body: &'static str) -> Self { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind loopback"); + let port = listener.local_addr().unwrap().port(); + let request_line = std::sync::Arc::new(std::sync::Mutex::new(String::new())); + let auth_header = std::sync::Arc::new(std::sync::Mutex::new(String::new())); + let request_body = std::sync::Arc::new(std::sync::Mutex::new(String::new())); + let (rl, ah, rb) = ( + request_line.clone(), + auth_header.clone(), + request_body.clone(), + ); + let handle = std::thread::spawn(move || { + let Ok((stream, _)) = listener.accept() else { + return; + }; + let mut reader = BufReader::new(stream); + let mut line = String::new(); + let mut content_length = 0usize; + // Request line first. + if reader.read_line(&mut line).is_ok() { + *rl.lock().unwrap() = line.trim().to_string(); + } + // Headers until the empty line. + loop { + line.clear(); + let Ok(read) = reader.read_line(&mut line) else { + break; + }; + if read == 0 || line == "\r\n" { + break; + } + let lower = line.to_lowercase(); + if let Some(value) = lower.strip_prefix("content-length:") { + content_length = value.trim().parse().unwrap_or(0); + } + // Header names are case-insensitive on the wire. + if let Some(prefix_end) = line.find(':') { + if line[..prefix_end].eq_ignore_ascii_case("authorization") { + *ah.lock().unwrap() = line[prefix_end + 1..].trim().to_string(); + } + } + } + // Body. + let mut body = vec![0u8; content_length]; + let _ = reader.read_exact(&mut body); + *rb.lock().unwrap() = String::from_utf8_lossy(&body).to_string(); + + let response = format!( + "{status_line}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{response_body}", + response_body.len() + ); + let _ = reader.get_mut().write_all(response.as_bytes()); + }); + Self { + base_url: format!("http://127.0.0.1:{port}"), + request_line, + auth_header, + request_body, + handle: Some(handle), + } + } + + fn join(mut self) -> (String, String, String) { + if let Some(handle) = self.handle.take() { + let _ = handle.join(); + } + let take = |m: &std::sync::Mutex| m.lock().unwrap().clone(); + ( + take(&self.request_line), + take(&self.auth_header), + take(&self.request_body), + ) + } + } + + fn request() -> SlmRequest { + SlmRequest { + proposal_id: Uuid::new_v4(), + content: "fn main() {}".to_string(), + context: "unit test".to_string(), + max_tokens: 32, + } + } + + const VERDICT_JSON: &str = r#"{\"spirit_score\": 0.2, \"confidence\": 0.9, \"reasoning\": \"mock\", \"should_block\": false}"#; + + #[test] + fn posts_openai_shape_and_echoes_correlation() { + let chat_response = format!( + r#"{{"id":"chatcmpl-mock","choices":[{{"index":0,"message":{{"role":"assistant","content":"{VERDICT_JSON}"}}}}]}}"# + ); + let server = + MockServer::start("HTTP/1.1 200 OK", Box::leak(chat_response.into_boxed_str())); + let provider = + HttpSlmProvider::new(&server.base_url, "mock-model", Some("test-key".into())).unwrap(); + + let req = request(); + let evaluation = provider.evaluate(&req).unwrap(); + assert_eq!(evaluation.proposal_id, req.proposal_id); + assert!(!evaluation.should_block); + assert_eq!(evaluation.spirit_score, 0.2); + + let (request_line, auth, body) = server.join(); + assert_eq!(request_line, "POST /v1/chat/completions HTTP/1.1"); + assert_eq!(auth, "Bearer test-key"); + assert!(body.contains(r#""model":"mock-model""#)); + assert!(body.contains(r#""temperature":0.0"#)); + assert!(body.contains(r#""max_tokens":32"#)); + assert!(body.contains(r#""json_object""#)); + } + + #[test] + fn server_error_is_fail_closed() { + let server = MockServer::start("HTTP/1.1 500 Internal Server Error", "{}"); + let provider = HttpSlmProvider::new(&server.base_url, "mock-model", None).unwrap(); + assert!(matches!( + provider.evaluate(&request()), + Err(SlmError::Transport(_)) + )); + server.join(); + } + + #[test] + fn zero_choices_is_fail_closed() { + let server = MockServer::start("HTTP/1.1 200 OK", r#"{"choices":[]}"#); + let provider = HttpSlmProvider::new(&server.base_url, "mock-model", None).unwrap(); + assert!(matches!( + provider.evaluate(&request()), + Err(SlmError::InvalidResponse(_)) + )); + server.join(); + } + + #[test] + fn malformed_content_json_is_fail_closed() { + let server = MockServer::start( + "HTTP/1.1 200 OK", + r#"{"choices":[{"message":{"role":"assistant","content":"no verdict here"}}]}"#, + ); + let provider = HttpSlmProvider::new(&server.base_url, "mock-model", None).unwrap(); + assert!(matches!( + provider.evaluate(&request()), + Err(SlmError::InvalidResponse(_)) + )); + server.join(); + } + + #[test] + fn plaintext_non_loopback_is_refused() { + let result = HttpSlmProvider::new("http://slm.example.com", "m", None); + assert!(matches!(result, Err(SlmError::NotConfigured(_)))); + } + + #[test] + fn loopback_and_https_endpoints_accepted() { + assert!(HttpSlmProvider::new("http://127.0.0.1:8080", "m", None).is_ok()); + assert!(HttpSlmProvider::new("http://localhost:8080", "m", None).is_ok()); + assert!(HttpSlmProvider::new("http://[::1]:8080/", "m", None).is_ok()); + assert!(HttpSlmProvider::new("https://slm.example.com/", "m", None).is_ok()); + } + + #[test] + fn completions_url_strips_trailing_slash() { + let provider = HttpSlmProvider::new("https://slm.example.com/", "m", None).unwrap(); + assert_eq!( + provider.completions_url(), + "https://slm.example.com/v1/chat/completions" + ); + } +} diff --git a/src/slm/src/lib.rs b/src/slm/src/lib.rs index 3d76f1b..9ea646a 100644 --- a/src/slm/src/lib.rs +++ b/src/slm/src/lib.rs @@ -16,6 +16,21 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; +/// Provider adapters for local and remote SLM backends. +pub mod provider; + +/// Optional remote (OpenAI-compatible HTTPS) provider. +#[cfg(feature = "http")] +pub mod http; + +pub use provider::{ + build_prompt, from_env, parse_verdict, LlamaCppProvider, ProviderVerdict, SlmProvider, + SlmRequest, +}; + +#[cfg(feature = "http")] +pub use http::HttpSlmProvider; + /// SLM evaluation result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SlmEvaluation { @@ -44,6 +59,20 @@ pub enum SlmError { ModelNotLoaded, #[error("Inference error: {0}")] InferenceError(String), + /// Provider is not (or is wrongly) configured, e.g. missing model path, + /// missing endpoint, or a requested feature that was not compiled in. + #[error("SLM provider not configured: {0}")] + NotConfigured(String), + /// The provider did not answer within its timeout. + #[error("SLM provider timeout: {0}")] + Timeout(String), + /// Spawn/transport failure (process spawn, exit status, HTTP transport). + #[error("SLM provider transport failure: {0}")] + Transport(String), + /// The provider answered, but the answer failed contract validation + /// (non-JSON, schema mismatch, out-of-range scores). Always fail-closed. + #[error("SLM provider returned an invalid response: {0}")] + InvalidResponse(String), } impl SlmEvaluator { diff --git a/src/slm/src/provider.rs b/src/slm/src/provider.rs new file mode 100644 index 0000000..20b1943 --- /dev/null +++ b/src/slm/src/provider.rs @@ -0,0 +1,593 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! SLM provider adapters: local `llama.cpp` CLI and (optionally) remote HTTPS. +//! +//! Both providers honour the same contract: +//! +//! * **Deterministic decoding** — temperature 0, explicit token limit. +//! * **Strict response shape** — the model must answer with a JSON object +//! `{"spirit_score", "confidence", "reasoning", "should_block"}`. Malformed +//! output is rejected, never coerced into a "safe-looking" result. +//! * **Range validation** — scores must be finite and within `0..=1`. +//! * **Correlation** — the request's `proposal_id` is carried through and +//! echoed on the evaluation; providers never invent one. +//! * **Fail-closed** — every failure mode is a [`SlmError`] variant, and +//! downstream callers (contract runner, arbiter) must treat provider +//! failure as NO-GO. +//! +//! Configuration is explicit: nothing downloads models or probes for binaries +//! at runtime. See `docs/SLM_PROVIDERS.adoc`. + +use crate::{SlmError, SlmEvaluation}; +use serde::{Deserialize, Serialize}; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::time::{Duration, Instant}; +use uuid::Uuid; + +/// Maximum proposal content forwarded to a provider (chars). Guards argv +/// limits for the CLI provider and keeps prompts bounded. +pub const MAX_CONTENT_CHARS: usize = 4096; + +/// Maximum context forwarded to a provider (chars). +pub const MAX_CONTEXT_CHARS: usize = 1024; + +/// Default decoding token budget. +/// Default per-request decoding budget. 256 gives small instruct models +/// enough room to emit long-form reasoning AND still close the JSON object: +/// at 128 tokens SmolLM2-135M/Qwen2.5-0.5B responses were observed truncated +/// mid-object, which correctly fails closed but wastes the evaluation. +pub const DEFAULT_MAX_TOKENS: u32 = 256; + +/// Default provider timeout. +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(120); + +/// A correlated evaluation request handed to a provider. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SlmRequest { + /// Correlation ID linking this request to a proposal — must be preserved + /// end-to-end so audit records can be joined to the gating request. + pub proposal_id: Uuid, + /// Proposal content under evaluation (bounded, see [`MAX_CONTENT_CHARS`]). + pub content: String, + /// Policy/request context (bounded, see [`MAX_CONTEXT_CHARS`]). + pub context: String, + /// Decoding token budget for this request. + pub max_tokens: u32, +} + +/// An SLM backend able to evaluate requests. +pub trait SlmProvider: Send + Sync { + /// Human-readable provider name for diagnostics and audit context. + fn name(&self) -> &str; + /// Evaluate a request, honouring the module-level provider contract. + fn evaluate(&self, request: &SlmRequest) -> Result; +} + +/// Trim `text` to at most `max` chars on a char boundary. +fn bounded(text: &str, max: usize) -> &str { + if text.chars().count() <= max { + text + } else { + match text.char_indices().nth(max) { + Some((idx, _)) => &text[..idx], + None => text, + } + } +} + +/// Build the deterministic evaluation prompt shared by all providers. +pub fn build_prompt(request: &SlmRequest) -> String { + let content = bounded(&request.content, MAX_CONTENT_CHARS); + let context = bounded(&request.context, MAX_CONTEXT_CHARS); + format!( + "You are a policy-spirit evaluator in a code-gating system. The deterministic \ + oracle has already ruled on explicit rules; you judge whether the proposal \ + violates the SPIRIT of the policy (e.g. disguised intent, verbosity abuse, \ + over-documentation to hide complexity, structural evasion).\n\n\ + Respond with ONLY a JSON object, no prose, no markdown fences, exactly:\n\ + {{\"spirit_score\": , \"confidence\": , \ + \"reasoning\": \"\", \"should_block\": }}\n\n\ + - spirit_score: estimated probability that the proposal violates the spirit of policy\n\ + - confidence: your confidence in that estimate\n\ + - should_block: true only if it must be blocked outright\n\n\ + POLICY CONTEXT:\n{context}\n\nPROPOSAL UNDER EVALUATION:\n{content}" + ) +} + +/// Wire format of the model's answer (validated before use). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProviderVerdict { + /// Estimated probability the proposal violates the spirit of policy. + pub spirit_score: f64, + /// Model confidence in its estimate. + pub confidence: f64, + /// Short human-readable justification. + pub reasoning: String, + /// Hard-block recommendation. + pub should_block: bool, +} + +impl ProviderVerdict { + /// Enforce the provider contract: finite in-range scores, bounded reasoning. + pub fn validate(&self) -> Result<(), SlmError> { + for (name, score) in [ + ("spirit_score", self.spirit_score), + ("confidence", self.confidence), + ] { + if !score.is_finite() || !(0.0..=1.0).contains(&score) { + return Err(SlmError::InvalidResponse(format!( + "{name} must be a finite number within 0..=1, got {score}" + ))); + } + } + if self.reasoning.chars().count() > 1000 { + return Err(SlmError::InvalidResponse( + "reasoning exceeds 1000 characters".to_string(), + )); + } + Ok(()) + } +} + +/// Extract ALL balanced top-level `{...}` JSON objects from `raw`, respecting +/// string literals and escapes (models sometimes wrap the object in prose). +fn extract_json_objects(raw: &str) -> Vec<&str> { + let mut objects = Vec::new(); + let mut depth = 0usize; + let mut in_string = false; + let mut escaped = false; + let mut start = 0usize; + for (offset, ch) in raw.char_indices() { + if escaped { + escaped = false; + continue; + } + match ch { + '\\' if in_string => escaped = true, + '"' => in_string = !in_string, + '{' if !in_string => { + if depth == 0 { + start = offset; + } + depth += 1; + } + '}' if !in_string && depth != 0 => { + depth -= 1; + if depth == 0 { + objects.push(&raw[start..=offset]); + } + } + _ => {} + } + } + objects +} + +/// Parse provider output text into a validated [`ProviderVerdict`]. +/// +/// Real CLI front-ends (notably current `llama-cli` conversation mode) pollute +/// stdout with banners, an echo of the prompt — which itself contains an +/// *invalid* template of the verdict object (`` placeholders) — +/// and throughput stats. The assistant's answer is the final output block, so +/// this scans every balanced object and accepts the LAST one that satisfies +/// the verdict schema and range validation. Anything else is treated as noise; +/// if no object validates, the response fails closed as invalid. +pub fn parse_verdict(raw: &str) -> Result { + let objects = extract_json_objects(raw); + if objects.is_empty() { + return Err(SlmError::InvalidResponse( + "no JSON object found in provider output".to_string(), + )); + } + let mut last_error = String::new(); + for object in objects.iter().rev() { + match serde_json::from_str::(object) + .map_err(|error| { + SlmError::InvalidResponse(format!( + "provider output failed schema validation: {error}" + )) + }) + .and_then(|verdict| verdict.validate().map(|()| verdict)) + { + Ok(verdict) => return Ok(verdict), + Err(error) => last_error = error.to_string(), + } + } + Err(SlmError::InvalidResponse(format!( + "no valid verdict object in provider output ({} object candidates; last error: {last_error})", + objects.len() + ))) +} + +/// Join a validated verdict with the request it answers, preserving correlation. +pub(crate) fn complete_evaluation(request: &SlmRequest, verdict: ProviderVerdict) -> SlmEvaluation { + SlmEvaluation { + proposal_id: request.proposal_id, + spirit_score: verdict.spirit_score, + confidence: verdict.confidence, + reasoning: verdict.reasoning, + should_block: verdict.should_block, + } +} + +// ============ Local llama.cpp provider ============ + +/// Provider that shells out to a `llama.cpp`-compatible executable. +/// +/// Invocation (explicit, deterministic, per the provider contract): +/// +/// ```text +/// llama-cli -m MODEL -p PROMPT -n TOKENS --temp 0 --no-display-prompt --single-turn +/// ``` +/// +/// `--no-display-prompt` and `--single-turn` are additive to the documented +/// argument set: the first keeps stdout JSON-extractable, the second +/// guarantees the process terminates after one generation instead of +/// parking in llama-cli's interactive conversation loop (observed with +/// llama.cpp nightlies, where plain `-p` never exits). Nothing is +/// downloaded or auto-discovered: both the executable and the GGUF model +/// path must be configured explicitly. +#[derive(Debug, Clone)] +pub struct LlamaCppProvider { + cli_path: PathBuf, + model_path: PathBuf, + max_tokens: u32, + timeout: Duration, +} + +impl LlamaCppProvider { + /// Create a provider; the model file must exist (fail fast with a clear + /// error rather than a confusing model-load failure at first request). + pub fn new( + cli_path: impl Into, + model_path: impl Into, + ) -> Result { + let model_path = model_path.into(); + if !model_path.is_file() { + return Err(SlmError::NotConfigured(format!( + "GGUF model not found: {}", + model_path.display() + ))); + } + Ok(Self { + cli_path: cli_path.into(), + model_path, + max_tokens: DEFAULT_MAX_TOKENS, + timeout: DEFAULT_TIMEOUT, + }) + } + + /// Override the default token budget and timeout. + pub fn with_limits(mut self, max_tokens: u32, timeout: Duration) -> Self { + self.max_tokens = max_tokens; + self.timeout = timeout; + self + } + + /// Build from the process environment: + /// + /// * `CONATIVE_GGUF_MODEL` — path to the GGUF model (required for + /// `Ok(Some(_))`; unset means the local provider is not configured). + /// * `CONATIVE_LLAMA_CLI` — executable path/name (default `llama-cli`). + /// * `CONATIVE_SLM_MAX_TOKENS` — token budget (default [`DEFAULT_MAX_TOKENS`]). + /// * `CONATIVE_SLM_TIMEOUT_SECS` — timeout seconds (default 120). + pub fn from_env() -> Result, SlmError> { + let Ok(model) = std::env::var("CONATIVE_GGUF_MODEL") else { + return Ok(None); + }; + let cli = std::env::var("CONATIVE_LLAMA_CLI").unwrap_or_else(|_| "llama-cli".to_string()); + let mut provider = Self::new(cli, model)?; + if let Ok(tokens) = std::env::var("CONATIVE_SLM_MAX_TOKENS") { + provider.max_tokens = tokens.parse().map_err(|_| { + SlmError::NotConfigured("CONATIVE_SLM_MAX_TOKENS must be an integer".to_string()) + })?; + } + if let Ok(secs) = std::env::var("CONATIVE_SLM_TIMEOUT_SECS") { + let secs: u64 = secs.parse().map_err(|_| { + SlmError::NotConfigured("CONATIVE_SLM_TIMEOUT_SECS must be an integer".to_string()) + })?; + provider.timeout = Duration::from_secs(secs); + } + Ok(Some(provider)) + } + + /// Path of the configured model (diagnostics only). + pub fn model_path(&self) -> &Path { + &self.model_path + } + + /// Path of the configured executable (diagnostics only). + pub fn cli_path(&self) -> &Path { + &self.cli_path + } +} + +impl SlmProvider for LlamaCppProvider { + fn name(&self) -> &str { + "llama.cpp-cli" + } + + fn evaluate(&self, request: &SlmRequest) -> Result { + let prompt = build_prompt(request); + // Per-request budget, defaulting to — and capped by — the provider's + // configured budget. `0` means "use the provider budget". + let tokens = match request.max_tokens { + 0 => self.max_tokens, + requested => requested.min(self.max_tokens.max(1)), + }; + + let mut child = Command::new(&self.cli_path) + .arg("-m") + .arg(&self.model_path) + .arg("-p") + .arg(&prompt) + .arg("-n") + .arg(tokens.to_string()) + .arg("--temp") + .arg("0") + .arg("--no-display-prompt") + .arg("--single-turn") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| { + SlmError::Transport(format!( + "failed to spawn {}: {error}", + self.cli_path.display() + )) + })?; + + // Drain pipes on threads so a verbose child cannot deadlock on a full + // stderr/stdout buffer while the main thread enforces the timeout. + let mut stdout_reader = child.stdout.take().expect("invariant: stdout was piped"); + let mut stderr_reader = child.stderr.take().expect("invariant: stderr was piped"); + let stdout_thread = std::thread::spawn(move || { + let mut buffer = String::new(); + let _ = stdout_reader.read_to_string(&mut buffer); + buffer + }); + let stderr_thread = std::thread::spawn(move || { + let mut buffer = String::new(); + let _ = stderr_reader.read_to_string(&mut buffer); + buffer + }); + + let started = Instant::now(); + let status = loop { + match child.try_wait() { + Ok(Some(status)) => break status, + Ok(None) => { + if started.elapsed() >= self.timeout { + let _ = child.kill(); + let _ = child.wait(); + let _ = stdout_thread.join(); + let _ = stderr_thread.join(); + return Err(SlmError::Timeout(format!( + "{} did not answer within {:?}", + self.cli_path.display(), + self.timeout + ))); + } + std::thread::sleep(Duration::from_millis(25)); + } + Err(error) => { + let _ = child.kill(); + let _ = child.wait(); + return Err(SlmError::Transport(format!( + "failed while waiting on {}: {error}", + self.cli_path.display() + ))); + } + } + }; + + let stdout = stdout_thread.join().unwrap_or_default(); + let stderr = stderr_thread.join().unwrap_or_default(); + + if !status.success() { + return Err(SlmError::Transport(format!( + "{} exited with {status}: {}", + self.cli_path.display(), + stderr.chars().take(200).collect::() + ))); + } + + let verdict = parse_verdict(&stdout)?; + Ok(complete_evaluation(request, verdict)) + } +} + +// ============ Provider selection from the environment ============ + +/// Select a provider from the environment: +/// +/// * `CONATIVE_SLM_PROVIDER=none` (or unset) → `Ok(None)` +/// * `CONATIVE_SLM_PROVIDER=llama` → [`LlamaCppProvider::from_env`] +/// * `CONATIVE_SLM_PROVIDER=http` → `HttpSlmProvider::from_env` (requires the +/// crate's `http` feature; without it this is a fail-closed error) +pub fn from_env() -> Result>, SlmError> { + let provider = std::env::var("CONATIVE_SLM_PROVIDER") + .unwrap_or_else(|_| "none".to_string()) + .to_lowercase(); + match provider.trim() { + "" | "none" | "disabled" => Ok(None), + "llama" | "llamacpp" | "llama-cpp" | "llama-cli" => Ok(LlamaCppProvider::from_env()? + .map(|p| std::sync::Arc::new(p) as std::sync::Arc)), + #[cfg(feature = "http")] + "http" | "https" | "openai" | "remote" => Ok(crate::http::HttpSlmProvider::from_env()? + .map(|p| std::sync::Arc::new(p) as std::sync::Arc)), + #[cfg(not(feature = "http"))] + "http" | "https" | "openai" | "remote" => Err(SlmError::NotConfigured( + "CONATIVE_SLM_PROVIDER=http requested but this build lacks the `http` feature; \ + rebuild with --features http" + .to_string(), + )), + other => Err(SlmError::NotConfigured(format!( + "unknown CONATIVE_SLM_PROVIDER={other} (expected: none | llama | http)" + ))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn prompt_is_deterministic_and_bounded() { + let request = SlmRequest { + proposal_id: Uuid::nil(), + content: "x".repeat(10_000), + context: "ctx".to_string(), + max_tokens: 64, + }; + let a = build_prompt(&request); + let b = build_prompt(&request); + assert_eq!(a, b); + assert!(a.contains("POLICY CONTEXT")); + assert!(a.chars().count() < 10_000); + } + + #[test] + fn parse_accepts_clean_json() { + let verdict = parse_verdict( + r#"{"spirit_score": 0.1, "confidence": 0.9, "reasoning": "fine", "should_block": false}"#, + ) + .unwrap(); + assert!(!verdict.should_block); + assert_eq!(verdict.spirit_score, 0.1); + } + + #[test] + fn parse_extracts_json_from_prose() { + let raw = r#"Sure! Here is my answer: + {"spirit_score": 0.8, "confidence": 0.7, "reasoning": "uses strings like }", "should_block": true} + Hope that helps!"#; + let verdict = parse_verdict(raw).unwrap(); + assert!(verdict.should_block); + assert_eq!(verdict.reasoning, "uses strings like }"); + } + + #[test] + fn parse_rejects_out_of_range_scores() { + let raw = + r#"{"spirit_score": 1.5, "confidence": 0.7, "reasoning": "x", "should_block": false}"#; + assert!(matches!( + parse_verdict(raw), + Err(SlmError::InvalidResponse(_)) + )); + } + + #[test] + fn parse_rejects_non_json() { + assert!(matches!( + parse_verdict("definitely not json"), + Err(SlmError::InvalidResponse(_)) + )); + } + + #[test] + fn parse_rejects_schema_mismatch() { + let raw = r#"{"spirit_score": 0.1, "confidence": 0.2}"#; + assert!(matches!( + parse_verdict(raw), + Err(SlmError::InvalidResponse(_)) + )); + } + + #[test] + fn llama_provider_requires_existing_model() { + let result = LlamaCppProvider::new("llama-cli", "/nonexistent/model.gguf"); + assert!(matches!(result, Err(SlmError::NotConfigured(_)))); + } + + #[test] + fn provider_echoes_correlation_id() { + // A fake `llama-cli` shell script emitting a valid verdict; asserts + // the provider's stdout parsing and correlation echo end to end. + let dir = std::env::temp_dir().join(format!("conative-slm-{}", Uuid::new_v4())); + std::fs::create_dir_all(&dir).unwrap(); + let model = dir.join("model.gguf"); + std::fs::write(&model, b"gguf-fixture").unwrap(); + let cli = dir.join("fake-llama-cli.sh"); + std::fs::write( + &cli, + "#!/bin/sh\nprintf '%s' '{\"spirit_score\": 0.2, \"confidence\": 0.9, \"reasoning\": \"fixture\", \"should_block\": false}'\n", + ) + .unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&cli, std::fs::Permissions::from_mode(0o755)).unwrap(); + } + let provider = LlamaCppProvider::new(&cli, &model).unwrap(); + let request = SlmRequest { + proposal_id: Uuid::new_v4(), + content: "fn main() {}".to_string(), + context: "test".to_string(), + max_tokens: 16, + }; + let evaluation = provider.evaluate(&request).unwrap(); + assert_eq!(evaluation.proposal_id, request.proposal_id); + assert!(!evaluation.should_block); + std::fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn provider_rejects_garbage_output_fail_closed() { + let dir = std::env::temp_dir().join(format!("conative-slm-{}", Uuid::new_v4())); + std::fs::create_dir_all(&dir).unwrap(); + let model = dir.join("model.gguf"); + std::fs::write(&model, b"gguf-fixture").unwrap(); + let cli = dir.join("fake-llama-cli.sh"); + std::fs::write(&cli, "#!/bin/sh\necho 'I cannot answer that.'\n").unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&cli, std::fs::Permissions::from_mode(0o755)).unwrap(); + } + let provider = LlamaCppProvider::new(&cli, &model).unwrap(); + let request = SlmRequest { + proposal_id: Uuid::new_v4(), + content: "fn main() {}".to_string(), + context: "test".to_string(), + max_tokens: 16, + }; + assert!(matches!( + provider.evaluate(&request), + Err(SlmError::InvalidResponse(_)) + )); + std::fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn provider_times_out_fail_closed() { + let dir = std::env::temp_dir().join(format!("conative-slm-{}", Uuid::new_v4())); + std::fs::create_dir_all(&dir).unwrap(); + let model = dir.join("model.gguf"); + std::fs::write(&model, b"gguf-fixture").unwrap(); + let cli = dir.join("fake-llama-cli.sh"); + std::fs::write(&cli, "#!/bin/sh\nsleep 5\n").unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&cli, std::fs::Permissions::from_mode(0o755)).unwrap(); + } + let provider = LlamaCppProvider::new(&cli, &model) + .unwrap() + .with_limits(16, Duration::from_millis(300)); + let request = SlmRequest { + proposal_id: Uuid::new_v4(), + content: "fn main() {}".to_string(), + context: "test".to_string(), + max_tokens: 16, + }; + assert!(matches!( + provider.evaluate(&request), + Err(SlmError::Timeout(_)) + )); + std::fs::remove_dir_all(&dir).unwrap(); + } +} diff --git a/src/slm/tests/real_inference.rs b/src/slm/tests/real_inference.rs new file mode 100644 index 0000000..60ee0f3 --- /dev/null +++ b/src/slm/tests/real_inference.rs @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Real model smoke tests — **ignored by default** and driven entirely by +//! the environment. They exercise the production provider paths against real +//! inference backends, never fixtures: +//! +//! Local llama.cpp (CI job `slm-real-inference` downloads a pinned binary + +//! pinned, SHA-256-verified GGUF model and runs this): +//! +//! ```sh +//! CONATIVE_LLAMA_CLI=./llama-cli CONATIVE_GGUF_MODEL=./model.gguf \ +//! cargo test -p slm-evaluator --test real_inference -- --ignored --nocapture +//! ``` +//! +//! Remote OpenAI-compatible endpoint (protected CI environment; also usable +//! against a local `llama-server`): +//! +//! ```sh +//! cargo test -p slm-evaluator --features http --test real_inference -- \ +//! --ignored --nocapture \ +//! # with CONATIVE_SLM_ENDPOINT, CONATIVE_SLM_MODEL_NAME, SLM_API_KEY +//! ``` +//! +//! A genuine model answer must satisfy the provider contract: JSON verdict, +//! in-range scores, correlation echo. + +use slm_evaluator::{LlamaCppProvider, SlmProvider, SlmRequest}; +use std::time::Instant; +use uuid::Uuid; + +fn smoke_request() -> SlmRequest { + SlmRequest { + proposal_id: Uuid::new_v4(), + content: "fn main() { println!(\"hello\"); }".to_string(), + context: "policy 'RSR Default Policy': Rust and Elixir are preferred; \ + TypeScript, Python, Go and Java are forbidden; npm requires deno" + .to_string(), + max_tokens: 0, + } +} + +#[test] +#[ignore = "requires CONATIVE_LLAMA_CLI + CONATIVE_GGUF_MODEL"] +fn real_llama_cpp_model_roundtrip() { + let provider = LlamaCppProvider::from_env() + .expect("environment must parse") + .unwrap_or_else(|| { + panic!( + "real smoke test requires CONATIVE_GGUF_MODEL (and optionally \ + CONATIVE_LLAMA_CLI); see docs/SLM_PROVIDERS.adoc" + ) + }); + + let request = smoke_request(); + let started = Instant::now(); + let evaluation = provider + .evaluate(&request) + .unwrap_or_else(|error| panic!("real model evaluation failed contract: {error}")); + let elapsed = started.elapsed(); + + assert_eq!( + evaluation.proposal_id, request.proposal_id, + "correlation id must echo the request" + ); + assert!((0.0..=1.0).contains(&evaluation.spirit_score)); + assert!((0.0..=1.0).contains(&evaluation.confidence)); + + // Surface the run for CI logs / docs/SLM_PROVIDERS.adoc benchmarking. + eprintln!( + "REAL-INFERENCE(llama.cpp): model={} verdict={{spirit_score: {:.2}, confidence: {:.2}, \ + should_block: {}, reasoning: {:?}}} latency={:?} tokens_budget={} cli={}", + provider.model_path().display(), + evaluation.spirit_score, + evaluation.confidence, + evaluation.should_block, + evaluation.reasoning, + elapsed, + request.max_tokens, + provider.cli_path().display(), + ); +} + +#[cfg(feature = "http")] +#[test] +#[ignore = "requires CONATIVE_SLM_ENDPOINT (+ optional SLM_API_KEY)"] +fn real_http_endpoint_roundtrip() { + let provider = slm_evaluator::HttpSlmProvider::from_env() + .expect("environment must parse") + .unwrap_or_else(|| { + panic!( + "real HTTP smoke test requires CONATIVE_SLM_ENDPOINT; see \ + docs/SLM_PROVIDERS.adoc" + ) + }); + + let request = smoke_request(); + let started = Instant::now(); + let evaluation = provider + .evaluate(&request) + .unwrap_or_else(|error| panic!("real HTTP evaluation failed contract: {error}")); + let elapsed = started.elapsed(); + + assert_eq!(evaluation.proposal_id, request.proposal_id); + assert!((0.0..=1.0).contains(&evaluation.spirit_score)); + assert!((0.0..=1.0).contains(&evaluation.confidence)); + + eprintln!( + "REAL-INFERENCE(http): url={} verdict={{spirit_score: {:.2}, confidence: {:.2}, \ + should_block: {}, reasoning: {:?}}} latency={:?}", + provider.completions_url(), + evaluation.spirit_score, + evaluation.confidence, + evaluation.should_block, + evaluation.reasoning, + elapsed, + ); +} diff --git a/tests/generative_test.rs b/tests/generative_test.rs new file mode 100644 index 0000000..add432b --- /dev/null +++ b/tests/generative_test.rs @@ -0,0 +1,545 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Generative (property-based) tests for the SLM stage of the gating +//! contract. +//! +//! Invariants under test (from the upstream delivery spec): +//! - an oracle Block is terminal: the provider is never invoked, for ANY +//! provider outcome; +//! - the SLM decision matrix obeys the enforcement thresholds at ALL scores, +//! including 0 and 1; +//! - the oracle Warn addend (+0.2 no-go) shifts the matrix predictably; +//! - low LLM confidence (<= 0.8) always escalates; +//! - any provider failure (timeout, transport, invalid response, outage) +//! fails closed — never Allow/Warn, always a non-overridable Escalate; +//! - determinism: identical votes → identical verdict; +//! - responses preserve the request/correlation ID; +//! - concurrent requests never mix correlation IDs. +//! +//! The OTP arbiter counterpart of this suite lives in +//! `src/arbiter/test/` (ExUnit); audit-sink persistence invariants are +//! tested there (`audit persistence failure never allows`). + +use gating_contract::{ContractRunner, GatingRequest, RefusalCode, Verdict}; +use policy_oracle::{ActionType, EnforcementConfig, Proposal}; +use slm_evaluator::{SlmError, SlmEvaluation, SlmProvider, SlmRequest}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use uuid::Uuid; + +use proptest::prelude::*; + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +const CLEAN_PATH: &str = "src/main.rs"; +const CLEAN_CONTENT: &str = "fn main() { println!(\"ok\"); }"; +const WARN_PATH: &str = "script.rkt"; +const WARN_CONTENT: &str = "#lang racket\n(displayln \"hi\")\n"; +const BLOCK_PATH: &str = "tool.py"; +const BLOCK_CONTENT: &str = "import os\nos.system('ls')\n"; + +fn proposal(path: &str, content: &str, llm_confidence: f32) -> Proposal { + Proposal { + id: Uuid::new_v4(), + action_type: ActionType::CreateFile { + path: path.to_string(), + }, + content: content.to_string(), + files_affected: vec![path.to_string()], + llm_confidence, + } +} + +// --------------------------------------------------------------------------- +// Mock providers +// --------------------------------------------------------------------------- + +/// Provider with a proptest-controlled outcome, counting invocations. +struct ScriptedSlm { + calls: AtomicUsize, + outcome: Outcome, +} + +#[derive(Clone, Debug)] +enum Outcome { + /// Echoes the request's proposal ID in the response. + Eval { + spirit_score: f64, + confidence: f64, + should_block: bool, + reasoning: String, + }, + /// Fails with the given error (constructed per call; SlmError is !Clone). + Err(ErrorKind), +} + +#[derive(Clone, Debug)] +enum ErrorKind { + ModelNotLoaded, + Inference, + NotConfigured, + Timeout, + Transport, + InvalidResponse, +} + +impl ErrorKind { + fn build(&self, msg: &str) -> SlmError { + match self { + ErrorKind::ModelNotLoaded => SlmError::ModelNotLoaded, + ErrorKind::Inference => SlmError::InferenceError(msg.to_string()), + ErrorKind::NotConfigured => SlmError::NotConfigured(msg.to_string()), + ErrorKind::Timeout => SlmError::Timeout(msg.to_string()), + ErrorKind::Transport => SlmError::Transport(msg.to_string()), + ErrorKind::InvalidResponse => SlmError::InvalidResponse(msg.to_string()), + } + } +} + +impl SlmProvider for ScriptedSlm { + fn name(&self) -> &str { + "scripted-test-double" + } + + fn evaluate(&self, request: &SlmRequest) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + match &self.outcome { + Outcome::Eval { + spirit_score, + confidence, + should_block, + reasoning, + } => Ok(SlmEvaluation { + proposal_id: request.proposal_id, + spirit_score: *spirit_score, + confidence: *confidence, + reasoning: reasoning.clone(), + should_block: *should_block, + }), + Outcome::Err(kind) => Err(kind.build("scripted failure")), + } + } +} + +impl ScriptedSlm { + fn eval(score: f64, confidence: f64, should_block: bool) -> Self { + Self { + calls: AtomicUsize::new(0), + outcome: Outcome::Eval { + spirit_score: score, + confidence, + should_block, + reasoning: "scripted reasoning".to_string(), + }, + } + } + + fn failing(kind: ErrorKind) -> Self { + Self { + calls: AtomicUsize::new(0), + outcome: Outcome::Err(kind), + } + } + + fn calls(&self) -> usize { + self.calls.load(Ordering::SeqCst) + } +} + +/// Enforcement thresholds under test, sourced from the same defaults the +/// runner uses (RSR default policy). +fn enforcement() -> EnforcementConfig { + EnforcementConfig::default() +} + +/// The decision matrix the implementation must satisfy (from the spec): +/// `no_go >= block` (or `should_block`) → Block; `no_go >= escalate` or +/// `go <= 0.8` → Escalate; otherwise the oracle verdict stands. +fn predict( + oracle_standing: Verdict, + score: f64, + go: f32, + should_block: bool, + addend: f64, +) -> Verdict { + let e = enforcement(); + let no_go = score * e.slm_weight + addend; + if should_block || no_go >= e.block_threshold { + Verdict::Block + } else if no_go >= e.escalate_threshold || go <= 0.8 { + Verdict::Escalate + } else { + oracle_standing + } +} + +// --------------------------------------------------------------------------- +// Proptest strategies +// --------------------------------------------------------------------------- + +fn arb_score() -> impl Strategy { + 0.0f64..=1.0 +} + +fn arb_confidence() -> impl Strategy { + 0.0f64..=1.0 +} + +fn arb_go() -> impl Strategy { + 0.0f32..=1.0 +} + +fn arb_error_kind() -> impl Strategy { + prop_oneof![ + Just(ErrorKind::ModelNotLoaded), + Just(ErrorKind::Inference), + Just(ErrorKind::NotConfigured), + Just(ErrorKind::Timeout), + Just(ErrorKind::Transport), + Just(ErrorKind::InvalidResponse), + ] +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(96))] + + /// An oracle Block is terminal for ANY provider outcome: the provider is + /// never invoked (proposal content never leaves the process), no SLM or + /// arbiter evidence is recorded, and the verdict is Block. + #[test] + fn oracle_block_is_terminal_for_any_provider_outcome( + eval_outcome in prop_oneof![ + (arb_score(), arb_confidence(), any::()) + .prop_map(|(s, c, b)| Outcome::Eval { + spirit_score: s, + confidence: c, + should_block: b, + reasoning: "arbitrary".to_string(), + }), + arb_error_kind().prop_map(Outcome::Err), + ], + ) { + let runner = ContractRunner::new(); + let provider = ScriptedSlm { + calls: AtomicUsize::new(0), + outcome: eval_outcome, + }; + let request = GatingRequest::new(proposal(BLOCK_PATH, BLOCK_CONTENT, 0.95)); + let decision = runner + .evaluate_with_provider(&request, &provider) + .expect("evaluation must not error"); + + prop_assert_eq!(decision.verdict, Verdict::Block); + prop_assert_eq!(provider.calls(), 0, "provider must never be invoked on an oracle block"); + prop_assert!(decision.evaluations.slm.is_none()); + prop_assert!(decision.evaluations.arbiter.is_none()); + prop_assert_eq!(decision.processing.stages_executed, vec!["oracle".to_string()]); + prop_assert_eq!(decision.request_id, request.request_id); + } + + /// The clean-proposal decision matrix obeys threshold arithmetic at every + /// score, including 0 and 1. + #[test] + fn clean_matrix_matches_threshold_arithmetic( + score in arb_score(), + should_block in any::(), + ) { + let runner = ContractRunner::new(); + let provider = ScriptedSlm::eval(score, 0.99, should_block); + let request = GatingRequest::new(proposal(CLEAN_PATH, CLEAN_CONTENT, 0.95)); + let decision = runner + .evaluate_with_provider(&request, &provider) + .expect("evaluation must not error"); + + let expected = predict(Verdict::Allow, score, 0.95, should_block, 0.0); + prop_assert_eq!(decision.verdict, expected, + "score {} should_block {} must follow the matrix", score, should_block); + prop_assert_eq!(provider.calls(), 1); + + // SLM evidence and arbiter record populated and consistent. + let slm = decision.evaluations.slm.as_ref().expect("slm stage recorded"); + prop_assert_eq!(slm.spirit_score, score); + let arbiter = decision.evaluations.arbiter.as_ref().expect("arbiter record"); + prop_assert!(arbiter.consensus_reached); + prop_assert_eq!(arbiter.oracle_vote, Verdict::Allow); + prop_assert_eq!(arbiter.final_verdict, decision.verdict); + prop_assert_eq!(arbiter.slm_weight, enforcement().slm_weight); + match decision.verdict { + Verdict::Block => prop_assert_eq!(arbiter.slm_vote, Verdict::Block), + Verdict::Escalate => prop_assert_eq!(arbiter.slm_vote, Verdict::Escalate), + standing => prop_assert_eq!(arbiter.slm_vote, Verdict::Allow, "standing {:?}", standing), + } + prop_assert_eq!(decision.request_id, request.request_id); + } + + /// The oracle Warn addend (+0.2 to no-go) shifts the matrix, and a passing + /// SLM verdict leaves the Warn standing (never upgraded to Allow). + #[test] + fn warn_addend_shifts_matrix_and_warn_stands( + score in arb_score(), + ) { + let runner = ContractRunner::new(); + let provider = ScriptedSlm::eval(score, 0.99, false); + let request = GatingRequest::new(proposal(WARN_PATH, WARN_CONTENT, 0.95)); + let decision = runner + .evaluate_with_provider(&request, &provider) + .expect("evaluation must not error"); + + let expected = predict(Verdict::Warn, score, 0.95, false, 0.2); + prop_assert_eq!(decision.verdict, expected, + "score {} with +0.2 addend must follow the shifted matrix", score); + let arbiter = decision.evaluations.arbiter.as_ref().expect("arbiter record"); + prop_assert_eq!(arbiter.oracle_vote, Verdict::Warn); + if expected == Verdict::Warn { + prop_assert!(decision.refusal.is_some(), "the oracle soft refusal is preserved"); + } + } + + /// Score 0 never escalates or blocks; score 1 always blocks, for any + /// SLM-side confidence. + #[test] + fn score_zero_and_one_obey_supremum_bounds( + slm_confidence in arb_confidence(), + ) { + let runner = ContractRunner::new(); + + let provider_zero = ScriptedSlm::eval(0.0, slm_confidence, false); + let request = GatingRequest::new(proposal(CLEAN_PATH, CLEAN_CONTENT, 0.95)); + let d0 = runner + .evaluate_with_provider(&request, &provider_zero) + .expect("evaluation must not error"); + prop_assert_eq!(d0.verdict, Verdict::Allow, "spirit score 0 must allow"); + + let provider_one = ScriptedSlm::eval(1.0, slm_confidence, false); + let request = GatingRequest::new(proposal(CLEAN_PATH, CLEAN_CONTENT, 0.95)); + let d1 = runner + .evaluate_with_provider(&request, &provider_one) + .expect("evaluation must not error"); + prop_assert_eq!(d1.verdict, Verdict::Block, "spirit score 1 must block"); + prop_assert_eq!( + d1.refusal.as_ref().map(|r| &r.code), + Some(&RefusalCode::Spirit599OtherSpirit), + ); + } + + /// `should_block: true` forces Block at ANY spirit score (including 0). + #[test] + fn should_block_flag_always_blocks( + score in arb_score(), + slm_confidence in arb_confidence(), + ) { + let runner = ContractRunner::new(); + let provider = ScriptedSlm::eval(score, slm_confidence, true); + let request = GatingRequest::new(proposal(CLEAN_PATH, CLEAN_CONTENT, 0.95)); + let decision = runner + .evaluate_with_provider(&request, &provider) + .expect("evaluation must not error"); + prop_assert_eq!(decision.verdict, Verdict::Block); + } + + /// Low LLM-side go (llm_confidence <= 0.8) always escalates, even with a + /// perfectly clean SLM vote — a provider cannot rescue provider metadata. + #[test] + fn low_llm_confidence_always_escalates( + go in 0.0f32..=0.8f32, + ) { + let runner = ContractRunner::new(); + let provider = ScriptedSlm::eval(0.0, 1.0, false); + let request = GatingRequest::new(proposal(CLEAN_PATH, CLEAN_CONTENT, go)); + let decision = runner + .evaluate_with_provider(&request, &provider) + .expect("evaluation must not error"); + prop_assert_eq!(decision.verdict, Verdict::Escalate); + } + + /// High go (> 0.8) with a mid-band score still escalates via no-go, and a + /// clean score allows — predictions delegated to the shared matrix. + #[test] + fn high_go_follows_no_go_band( + score in arb_score(), + go in 0.8000001f32..=1.0f32, + ) { + let runner = ContractRunner::new(); + let provider = ScriptedSlm::eval(score, 0.99, false); + let request = GatingRequest::new(proposal(CLEAN_PATH, CLEAN_CONTENT, go)); + let decision = runner + .evaluate_with_provider(&request, &provider) + .expect("evaluation must not error"); + prop_assert_eq!(decision.verdict, predict(Verdict::Allow, score, go, false, 0.0)); + } + + /// EVERY provider failure mode fails closed: Escalate with a + /// non-overridable 9xx system refusal, an explicit `slm_error` stage, and + /// no SLM/arbiter evidence — on both clean and warn fixtures. + #[test] + fn any_provider_failure_fails_closed( + kind in arb_error_kind(), + use_warn_fixture in any::(), + ) { + let runner = ContractRunner::new(); + let provider = ScriptedSlm::failing(kind); + let (path, content) = if use_warn_fixture { + (WARN_PATH, WARN_CONTENT) + } else { + (CLEAN_PATH, CLEAN_CONTENT) + }; + let request = GatingRequest::new(proposal(path, content, 0.95)); + let decision = runner + .evaluate_with_provider(&request, &provider) + .expect("evaluation must not error"); + + prop_assert_eq!(provider.calls(), 1); + prop_assert_eq!(decision.verdict, Verdict::Escalate, "provider failure must never allow"); + let refusal = decision.refusal.as_ref().expect("refusal recorded"); + prop_assert_eq!(&refusal.code, &RefusalCode::Sys902InternalError); + prop_assert!(!refusal.overridable, "system failures are not policy-overridable"); + prop_assert!(decision.processing.stages_executed.iter().any(|s| s == "slm_error")); + prop_assert!(decision.evaluations.slm.is_none()); + prop_assert!(decision.evaluations.arbiter.is_none()); + prop_assert_eq!(decision.request_id, request.request_id); + } + + /// Determinism: identical votes → identical verdict and refusal code, + /// across two fresh runners and providers. + #[test] + fn identical_votes_identical_verdict( + score in arb_score(), + go in arb_go(), + should_block in any::(), + use_warn_fixture in any::(), + ) { + let (path, content) = if use_warn_fixture { + (WARN_PATH, WARN_CONTENT) + } else { + (CLEAN_PATH, CLEAN_CONTENT) + }; + let decisions: Vec<_> = (0..2) + .map(|_| { + ContractRunner::new() + .evaluate_with_provider( + &GatingRequest::new(proposal(path, content, go)), + &ScriptedSlm::eval(score, 0.99, should_block), + ) + .expect("evaluation must not error") + }) + .collect(); + + prop_assert_eq!(decisions[0].verdict, decisions[1].verdict); + prop_assert_eq!( + decisions[0].refusal.as_ref().map(|r| &r.code), + decisions[1].refusal.as_ref().map(|r| &r.code), + ); + } + + /// The response preserves the request ID for every provider outcome. + #[test] + fn response_preserves_request_id( + score in arb_score(), + go in arb_go(), + should_block in any::(), + ) { + let runner = ContractRunner::new(); + let request = GatingRequest::new(proposal(CLEAN_PATH, CLEAN_CONTENT, go)); + let decision = runner + .evaluate_with_provider(&request, &ScriptedSlm::eval(score, 0.99, should_block)) + .expect("evaluation must not error"); + prop_assert_eq!(decision.request_id, request.request_id); + } +} + +// --------------------------------------------------------------------------- +// Concurrency: correlation IDs must never mix across in-flight requests +// --------------------------------------------------------------------------- + +/// Provider whose evaluation echoes a marker found in the request content and +/// forces a Block, so the marker flows into the refusal message of the exact +/// decision belonging to that request. Sleeps an id-derived 0-4ms to maximise +/// interleaving. +struct MarkerBlockingSlm { + calls: AtomicUsize, +} + +impl SlmProvider for MarkerBlockingSlm { + fn name(&self) -> &str { + "marker-blocking-test-double" + } + + fn evaluate(&self, request: &SlmRequest) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + let jitter = request.proposal_id.as_bytes()[0] % 5; + std::thread::sleep(std::time::Duration::from_millis(u64::from(jitter))); + let marker = request + .content + .rsplit("// marker:") + .next() + .unwrap_or("") + .trim() + .to_string(); + Ok(SlmEvaluation { + proposal_id: request.proposal_id, + spirit_score: 1.0, + confidence: 1.0, + reasoning: format!("flagged {marker}"), + should_block: true, + }) + } +} + +#[test] +fn concurrent_requests_do_not_mix_correlation_ids() { + const THREADS: usize = 8; + const REQ_PER_THREAD: usize = 4; + + let provider = Arc::new(MarkerBlockingSlm { + calls: AtomicUsize::new(0), + }); + + let handles: Vec<_> = (0..THREADS) + .map(|t| { + let provider = Arc::clone(&provider); + std::thread::spawn(move || { + let runner = ContractRunner::new(); + let mut results = Vec::new(); + for r in 0..REQ_PER_THREAD { + let marker = format!("t{t}-r{r}"); + let request = GatingRequest::new(proposal( + CLEAN_PATH, + &format!("{CLEAN_CONTENT} // marker:{marker}"), + 0.95, + )); + let request_id = request.request_id; + let decision = runner + .evaluate_with_provider(&request, provider.as_ref()) + .expect("evaluation must not error"); + results.push((marker, request_id, decision)); + } + results + }) + }) + .collect(); + + let mut total = 0; + for handle in handles { + for (marker, request_id, decision) in handle.join().expect("thread panicked") { + total += 1; + assert_eq!(decision.verdict, Verdict::Block); + assert_eq!( + decision.request_id, request_id, + "decision must answer its own request" + ); + let message = &decision.refusal.as_ref().expect("refusal recorded").message; + assert!( + message.contains(&format!("flagged {marker}")), + "correlation mix-up: decision for marker {marker} carried {message:?}" + ); + } + } + assert_eq!(total, THREADS * REQ_PER_THREAD); + assert_eq!( + provider.calls.load(Ordering::SeqCst), + THREADS * REQ_PER_THREAD, + "exactly one provider call per request passed the oracle" + ); +} diff --git a/vendor/bunsenite/.claude/CLAUDE.md b/vendor/bunsenite/.claude/CLAUDE.md new file mode 100644 index 0000000..830fb2c --- /dev/null +++ b/vendor/bunsenite/.claude/CLAUDE.md @@ -0,0 +1,88 @@ + +## Machine-Readable Artefacts + +The following files in `.machine_readable/` contain structured project metadata: + +- `.machine_readable/6a2/STATE.a2ml` - Current project state and progress +- `.machine_readable/6a2/META.a2ml` - Architecture decisions and development practices +- `.machine_readable/6a2/ECOSYSTEM.a2ml` - Position in the ecosystem and related projects +- `.machine_readable/6a2/AGENTIC.a2ml` - AI agent interaction patterns +- `.machine_readable/6a2/NEUROSYM.a2ml` - Neurosymbolic integration config +- `.machine_readable/6a2/PLAYBOOK.a2ml` - Operational runbook + +--- + +# CLAUDE.md - AI Assistant Instructions + +## Language Policy (Hyperpolymath Standard) + +### ALLOWED Languages & Tools + +| Language/Tool | Use Case | Notes | +|---------------|----------|-------| +| **AffineScript** | Primary application code | Affine-typed, compiles to typed-wasm or ESM | +| **Bun** | JS runtime & package management (tier 1) | Default for all new work. Runs compiled ESM/JS directly — no bundler step. Uses an npm-compatible `package.json` plus `bun.lock` — both are expected, not anti-patterns. | +| **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, Bun APIs | +| **Nickel** | Configuration language | For complex configs | +| **Guile Scheme** | State/meta files | .machine_readable/6a2/STATE.a2ml, .machine_readable/6a2/META.a2ml, .machine_readable/6a2/ECOSYSTEM.a2ml | +| **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 | +| ReScript | AffineScript | +| Deno | Bun | +| Node.js | Bun | +| npm | Bun | +| pnpm/yarn | Bun | +| 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. **Use `package.json` + `bun.lock` for JS runtime deps** - Bun is npm-compatible; a manifest is REQUIRED +3. **`bun install --production --frozen-lockfile` for production deps** - resolved from `package.json` and pinned via `bun.lock`; `--frozen-lockfile` makes a lockfile mismatch a build failure rather than a silent re-resolve +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**: Guix (flake.guix) +- **JS deps**: Bun (`package.json` + `bun.lock`). Declare tooling as a devDependency and run `bunx --no-install --bun ` — a bare `bunx ` can fetch an unpinned package and may start Node via its shebang. + +### 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/vendor/bunsenite/.clusterfuzzlite/Containerfile b/vendor/bunsenite/.clusterfuzzlite/Containerfile new file mode 100644 index 0000000..ff3d423 --- /dev/null +++ b/vendor/bunsenite/.clusterfuzzlite/Containerfile @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: MPL-2.0 +# ClusterFuzzLite build environment for bunsenite +FROM gcr.io/oss-fuzz-base/base-builder-rust@sha256:73c1d5648db54100639339d411a5d192cbc8bf413ee91e843a07cf6f0e319dc7 + +COPY . $SRC/bunsenite +WORKDIR $SRC/bunsenite + +COPY .clusterfuzzlite/build.sh $SRC/ diff --git a/vendor/bunsenite/.clusterfuzzlite/build.sh b/vendor/bunsenite/.clusterfuzzlite/build.sh new file mode 100644 index 0000000..bfdc807 --- /dev/null +++ b/vendor/bunsenite/.clusterfuzzlite/build.sh @@ -0,0 +1,13 @@ +#!/bin/bash -eu +# SPDX-License-Identifier: MPL-2.0 +# Build script for ClusterFuzzLite + +cd $SRC/bunsenite + +# Build fuzz targets using cargo-fuzz +cargo +nightly fuzz build + +# Copy fuzz targets to $OUT +for target in $(cargo +nightly fuzz list); do + cp ./target/x86_64-unknown-linux-gnu/release/$target $OUT/ +done diff --git a/vendor/bunsenite/.clusterfuzzlite/project.yaml b/vendor/bunsenite/.clusterfuzzlite/project.yaml new file mode 100644 index 0000000..4d72a30 --- /dev/null +++ b/vendor/bunsenite/.clusterfuzzlite/project.yaml @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MPL-2.0 +# ClusterFuzzLite configuration for bunsenite +language: rust diff --git a/vendor/bunsenite/.editorconfig b/vendor/bunsenite/.editorconfig new file mode 100644 index 0000000..960e2cd --- /dev/null +++ b/vendor/bunsenite/.editorconfig @@ -0,0 +1,68 @@ +# bunsenite - 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/vendor/bunsenite/.gitattributes b/vendor/bunsenite/.gitattributes new file mode 100644 index 0000000..e860a85 --- /dev/null +++ b/vendor/bunsenite/.gitattributes @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: MPL-2.0 +# 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/vendor/bunsenite/.github/CODEOWNERS b/vendor/bunsenite/.github/CODEOWNERS new file mode 100644 index 0000000..3a3b7f2 --- /dev/null +++ b/vendor/bunsenite/.github/CODEOWNERS @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: MPL-2.0 +# CODEOWNERS - Define code review assignments for GitHub +# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# Default: sole maintainer for all files +* @hyperpolymath + +# Security-sensitive files require explicit ownership +SECURITY.md @hyperpolymath +.github/workflows/ @hyperpolymath +.machine_readable/ @hyperpolymath +contractiles/ @hyperpolymath + +# License files +LICENSE @hyperpolymath +LICENSES/ @hyperpolymath + +# Configuration +.gitignore @hyperpolymath +.github/ @hyperpolymath + +# Documentation +README* @hyperpolymath +CONTRIBUTING* @hyperpolymath +CODE_OF_CONDUCT* @hyperpolymath +GOVERNANCE* @hyperpolymath +MAINTAINERS* @hyperpolymath +CHANGELOG* @hyperpolymath +ROADMAP* @hyperpolymath + +# Build and CI +Justfile @hyperpolymath +Makefile @hyperpolymath +*.sh @hyperpolymath diff --git a/vendor/bunsenite/.github/CONTRIBUTING.md b/vendor/bunsenite/.github/CONTRIBUTING.md new file mode 100644 index 0000000..3466911 --- /dev/null +++ b/vendor/bunsenite/.github/CONTRIBUTING.md @@ -0,0 +1,88 @@ + +# Getting started +```bash +git clone https://github.com/hyperpolymath/bunsenite.git +cd bunsenite + +# Using mise (recommended: provision the pinned toolchain) +mise install + +# Task runner (see Justfile) +just --list # available tasks +just check # verify setup / static checks +just test # run the test suite +``` + +--- + +## 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](../docs/status/ROADMAP.adoc) if available +2. Search existing issues and discussions +3. Consider which perimeter the feature belongs to + +**When suggesting**: + +Use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.md) and include: + +- Problem statement (what pain point does this solve?) +- Proposed solution +- Alternatives considered +- Which perimeter this affects + +### Your First Contribution + +Look for issues labelled: + +- [`good first issue`](https://github.com/hyperpolymath/bunsenite/labels/good%20first%20issue) — Simple Perimeter 3 tasks +- [`help wanted`](https://github.com/hyperpolymath/bunsenite/labels/help%20wanted) — Community help needed +- [`documentation`](https://github.com/hyperpolymath/bunsenite/labels/documentation) — Docs improvements +- [`perimeter-3`](https://github.com/hyperpolymath/bunsenite/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/vendor/bunsenite/.github/FUNDING.yml b/vendor/bunsenite/.github/FUNDING.yml new file mode 100644 index 0000000..688a442 --- /dev/null +++ b/vendor/bunsenite/.github/FUNDING.yml @@ -0,0 +1,7 @@ +# 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 + +github: hyperpolymath +ko_fi: hyperpolymath +liberapay: hyperpolymath diff --git a/vendor/bunsenite/.github/copilot/coding-agent.yml b/vendor/bunsenite/.github/copilot/coding-agent.yml new file mode 100644 index 0000000..a719a77 --- /dev/null +++ b/vendor/bunsenite/.github/copilot/coding-agent.yml @@ -0,0 +1,6 @@ +mcp_servers: + boj-server: + command: npx + args: ["-y", "@hyperpolymath/boj-server@latest"] + env: + BOJ_URL: http://localhost:7700 diff --git a/vendor/bunsenite/.github/dependabot.yml b/vendor/bunsenite/.github/dependabot.yml new file mode 100644 index 0000000..4168336 --- /dev/null +++ b/vendor/bunsenite/.github/dependabot.yml @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: MPL-2.0 +version: 2 +updates: + - package-ecosystem: "bundler" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + # `open-pull-requests-limit: 0` suppresses routine version-update PRs + # while leaving Dependabot SECURITY PRs flowing. The previous + # `ignore: "*" patch` rule also silenced security PRs under GitHub\'s + # current Dependabot behaviour. See rsr-template-repo commit 78b050e + # and 007-lang/audits/audit-dependabot-automation-gap-2026-04-17.md. + open-pull-requests-limit: 10 + groups: + cargo: + patterns: + - "*" + update-types: + - "minor" + - "patch" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + actions: + patterns: + - "*" + open-pull-requests-limit: 2 + - package-ecosystem: "guix" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 3 diff --git a/vendor/bunsenite/.github/funding.yml b/vendor/bunsenite/.github/funding.yml new file mode 100644 index 0000000..e4f7c07 --- /dev/null +++ b/vendor/bunsenite/.github/funding.yml @@ -0,0 +1,4 @@ +# Funding Configuration +# See: https://docs.github.com/en/repositories/managing-your-repositorys-custom-fields/displaying-a-sponsor-button-in-your-repository + +github: metadatastician diff --git a/vendor/bunsenite/.github/label-classifier.json b/vendor/bunsenite/.github/label-classifier.json new file mode 100644 index 0000000..d349eaa --- /dev/null +++ b/vendor/bunsenite/.github/label-classifier.json @@ -0,0 +1,739 @@ +{ + "_generated_from": ".github/label-classifier.yml + .github/labels.yml in hyperpolymath/.git-private-farm", + "_do_not_edit": "regenerate with scripts/gen-classifier-json.py", + "version": 1, + "prefix_split_on": "/", + "title_prefix": { + "docs": { + "type": "documentation" + }, + "ci": { + "type": "chore", + "areas": [ + "cicd" + ] + }, + "governance": { + "type": "chore", + "areas": [ + "governance" + ] + }, + "roadmap": { + "type": "enhancement", + "meta": "meta:roadmap" + }, + "chore": { + "type": "chore" + }, + "build": { + "type": "chore", + "areas": [ + "cicd" + ] + }, + "security": { + "type": "chore", + "areas": [ + "security" + ] + }, + "proof": { + "type": "chore", + "areas": [ + "proofs" + ] + }, + "proofs": { + "type": "chore", + "areas": [ + "proofs" + ] + }, + "proof-debt": { + "type": "tech-debt", + "areas": [ + "proofs" + ] + }, + "epic": { + "type": "enhancement", + "meta": "meta:umbrella" + }, + "umbrella": { + "type": "enhancement", + "meta": "meta:umbrella" + }, + "tracking": { + "type": "chore", + "meta": "meta:umbrella" + }, + "campaign": { + "type": "enhancement", + "meta": "meta:campaign" + }, + "hygiene": { + "type": "tech-debt" + }, + "audit": { + "type": "research" + }, + "estate": { + "type": "chore", + "scope": "scope:estate" + }, + "automation": { + "type": "enhancement", + "areas": [ + "automation" + ] + }, + "research": { + "type": "research" + }, + "refactor": { + "type": "refactor" + }, + "test": { + "type": "testing" + }, + "tests": { + "type": "testing" + }, + "feat": { + "type": "enhancement" + }, + "fix": { + "type": "bug" + }, + "bug": { + "type": "bug" + }, + "perf": { + "type": "enhancement", + "areas": [ + "performance" + ] + }, + "codegen": { + "type": "enhancement", + "areas": [ + "architecture" + ] + }, + "packaging": { + "type": "chore", + "areas": [ + "packaging" + ] + }, + "policy": { + "type": "chore", + "areas": [ + "governance" + ] + }, + "ops": { + "type": "chore", + "areas": [ + "automation" + ] + }, + "standard": { + "type": "chore", + "areas": [ + "governance" + ] + }, + "migration": { + "type": "refactor", + "areas": [ + "migration" + ] + }, + "drift": { + "type": "tech-debt" + }, + "corrective": { + "type": "bug" + }, + "adaptive": { + "type": "enhancement" + }, + "perfective": { + "type": "enhancement" + }, + "preventive": { + "type": "tech-debt" + }, + "machine-readable": { + "type": "tech-debt" + }, + "parser": { + "type": "bug" + }, + "lang": { + "type": "bug" + }, + "clippy": { + "type": "tech-debt" + }, + "release": { + "type": "chore" + }, + "upstream": { + "type": "chore" + }, + "hardening": { + "type": "chore", + "areas": [ + "security" + ] + }, + "deps": { + "type": "chore" + }, + "rustsec": { + "type": "chore", + "areas": [ + "security" + ] + }, + "track": { + "type": "chore", + "meta": "meta:umbrella" + }, + "tracker": { + "type": "chore", + "meta": "meta:umbrella" + }, + "wiki": { + "type": "documentation" + }, + "reclassify": { + "type": "refactor" + }, + "backlog": { + "type": "chore" + }, + "core": { + "type": "enhancement", + "areas": [ + "design" + ] + }, + "evidence": { + "type": "enhancement", + "areas": [ + "design" + ] + }, + "manifest": { + "type": "enhancement", + "areas": [ + "design" + ] + }, + "backends": { + "type": "enhancement", + "areas": [ + "design" + ] + } + }, + "bracket_tag": { + "campaign": { + "meta": "meta:campaign" + }, + "umbrella": { + "meta": "meta:umbrella" + }, + "gov": { + "areas": [ + "governance" + ] + }, + "proofs/a": { + "areas": [ + "proofs" + ] + }, + "proofs/b": { + "areas": [ + "proofs" + ] + }, + "proofs/c": { + "areas": [ + "proofs" + ] + }, + "estate": { + "scope": "scope:estate" + }, + "repo": { + "scope": "scope:repo" + }, + "feature": { + "type": "enhancement" + }, + "integration": { + "areas": [ + "conformance" + ] + }, + "reference": { + "type": "documentation" + }, + "register": { + "type": "documentation" + }, + "p0": { + "priority": "priority:p0" + }, + "p1": { + "priority": "priority:p1" + }, + "p2": { + "priority": "priority:p2" + }, + "et-l2": { + "areas": [ + "conformance" + ] + }, + "et-l4": { + "areas": [ + "conformance" + ] + } + }, + "keyword_area": { + "proofs": [ + "agda", + "coq", + "rocq", + "idris", + "lean", + "isabelle", + "hol", + "mizar", + "why3", + "tla", + "alloy", + "dafny", + "acl2", + "pvs", + "metamath", + "z3", + "smt", + "prover", + "provers", + "theorem", + "theorems", + "axiom", + "axioms", + "postulate", + "postulates", + "believe_me", + "sorry", + "proof obligation", + "proof obligations", + "proof hole", + "proof holes", + "proof suite", + "proof-pipeline", + "proof debt", + "proof-debt", + "metatheory", + "mechanize", + "qed" + ], + "cicd": [ + "workflow", + "github action", + "actions.lock", + "lockfile", + "runner", + "startup_failure", + "dependabot", + "check run", + "required context", + "scorecard", + "codeql", + "ci/cd" + ], + "licensing": [ + "spdx", + "licence", + "license", + "reuse", + "copyright", + "attribution", + "agpl", + "mpl" + ], + "security": [ + "gitleaks", + "secret", + "vulnerabilit", + "advisory", + "supply chain", + "cve" + ], + "bindings": [ + "abi", + "ffi", + "wasm", + "jni", + "c api", + "interop", + "extern \"c\"", + "nif", + "snif" + ], + "packaging": [ + "guix", + "nix", + "container", + "containerfile", + "docker", + "flatpak", + "oci image" + ], + "scaffolding": [ + "rsr", + "scaffold", + "template", + "repo-init", + "instantiat", + "placeholder" + ], + "governance": [ + "ruleset", + "policy", + "compliance", + "governance", + "branch protection", + "codeowners", + "code of conduct" + ], + "migration": [ + "rescript", + "to-affinescript", + "\u2192 affinescript", + "port", + "deno", + "bun" + ], + "automation": [ + "bot", + "gitbot", + "hypatia", + "sustainabot", + "oikosbot", + "fan-out", + "fanout", + "dispatch", + "self-heal" + ], + "performance": [ + "latency", + "throughput", + "binary size", + "memory", + "hot path", + "regression" + ] + }, + "keyword_type": { + "tech-debt": [ + "debt", + "drift", + "hygiene", + "stale", + "cleanup", + "follow-up", + "clean up", + "left over", + "leftover", + "anti-pattern", + "inconsistency", + "inconsistent", + "placeholder", + "placeholders", + "tbd", + "todo", + "todos", + "unfilled" + ], + "documentation": [ + "document", + "docs", + "readme", + "adoc", + "prose", + "docs/", + "changelog", + "explainme", + "quickstart", + "wiki", + "docstring", + "doc tree" + ], + "testing": [ + "test", + "tests", + "fuzz", + "bench", + "coverage", + "crash-consistency", + "linearizability", + "equivalence", + "property-correspondence", + "property-based", + "test suite", + "proptest" + ], + "bug": [ + "broken", + "fails", + "failing", + "crash", + "oom", + "regression", + "incorrect", + "does not", + "panic", + "panics", + "unreachable", + "mangled", + "never run", + "never ran", + "never succeeded", + "never fires", + "cannot fail", + "deadlock", + "hangs" + ], + "refactor": [ + "refactor", + "restructure", + "consolidate", + "consolidation", + "reconcile", + "reconciliation", + "unify", + "dedupe", + "re-point", + "repoint", + "extract", + "retire", + "retire duplicate", + "deduplicate", + "reclassify", + "migrate" + ], + "research": [ + "investigat", + "explore", + "spike", + "work out", + "triage", + "assess", + "survey", + "gap analysis", + "self-audit", + "inventory", + "weakness list", + "theory", + "synthesis", + "prioritised weakness", + "feasibility" + ], + "decision": [ + "ruling", + "decide", + "decision", + "adjudicat", + "which of" + ], + "enhancement": [ + "add", + "implement", + "support", + "introduce", + "enable", + "expand", + "expansion", + "extend", + "wire", + "complete", + "build", + "create", + "port" + ] + }, + "meta_signal": { + "meta:umbrella": [ + "umbrella", + "epic", + "master issue", + "parent issue", + "sub-issues", + "child issues" + ], + "meta:campaign": [ + "campaign" + ], + "meta:roadmap": [ + "roadmap", + "capability-expansion", + "future work" + ], + "meta:recurring": [ + "recurring", + "recurrence", + "standing", + "every run", + "each week" + ] + }, + "status_signal": { + "status:blocked": [ + "blocked on", + "blocked:", + "(blocked", + "is blocked", + "gated on", + "waiting on upstream", + "needs upstream" + ], + "status:needs-owner": [ + "unassigned", + "needs an owner", + "no owner" + ], + "status:needs-ruling": [ + "needs a ruling", + "awaiting ruling", + "owner decision needed" + ] + }, + "scope_signal": { + "scope:estate": [ + "estate-wide", + "estate wide", + "across the estate", + "all repos", + "fleet-wide" + ] + }, + "tier_of": { + "bug": "type", + "enhancement": "type", + "documentation": "type", + "refactor": "type", + "tech-debt": "type", + "testing": "type", + "chore": "type", + "research": "type", + "decision": "type", + "question": "type", + "cicd": "area", + "security": "area", + "proofs": "area", + "governance": "area", + "design": "area", + "architecture": "area", + "performance": "area", + "bindings": "area", + "migration": "area", + "packaging": "area", + "licensing": "area", + "automation": "area", + "scaffolding": "area", + "conformance": "area", + "priority:p0": "priority", + "priority:p1": "priority", + "priority:p2": "priority", + "priority:p3": "priority", + "status:blocked": "status", + "status:ready": "status", + "status:needs-owner": "status", + "status:needs-ruling": "status", + "status:do-not-automate": "status", + "meta:umbrella": "meta", + "meta:campaign": "meta", + "meta:roadmap": "meta", + "meta:recurring": "meta", + "scope:estate": "scope", + "scope:repo": "scope" + }, + "tier_max": { + "type": 1, + "area": null, + "priority": 1, + "status": 1, + "meta": 1, + "scope": 1 + }, + "types": [ + "bug", + "enhancement", + "documentation", + "refactor", + "tech-debt", + "testing", + "chore", + "research", + "decision", + "question" + ], + "frozen": [ + "dependencies", + "duplicate", + "elixir", + "gitar-approved", + "github_actions", + "good first issue", + "help wanted", + "invalid", + "javascript", + "never-stale", + "nix", + "pinned", + "python", + "rust", + "security", + "stale", + "wontfix" + ], + "precedence": { + "meta:campaign": 0, + "meta:umbrella": 1, + "meta:recurring": 2, + "meta:roadmap": 3, + "priority:p0": 0, + "priority:p1": 1, + "priority:p2": 2, + "priority:p3": 3, + "status:blocked": 0, + "status:needs-ruling": 1, + "status:needs-owner": 2, + "status:do-not-automate": 3, + "status:ready": 4, + "scope:estate": 0, + "scope:repo": 1, + "bug": 0, + "decision": 1, + "tech-debt": 2, + "testing": 3, + "documentation": 4, + "refactor": 5, + "research": 6, + "enhancement": 7, + "chore": 8, + "question": 9 + } +} diff --git a/vendor/bunsenite/.github/labels.json b/vendor/bunsenite/.github/labels.json new file mode 100644 index 0000000..78786d4 --- /dev/null +++ b/vendor/bunsenite/.github/labels.json @@ -0,0 +1,260 @@ +{ + "_generated_from": ".github/labels.yml in hyperpolymath/.git-private-farm", + "_do_not_edit": "regenerate with scripts/gen-labels-json.py", + "version": 1, + "labels": [ + { + "name": "bug", + "color": "d73a4a", + "description": "Something is broken or behaves incorrectly", + "tier": "type" + }, + { + "name": "enhancement", + "color": "a2eeef", + "description": "New capability or improvement to existing behaviour", + "tier": "type" + }, + { + "name": "documentation", + "color": "0075ca", + "description": "Docs, prose, diagrams, READMEs, ADRs", + "tier": "type" + }, + { + "name": "refactor", + "color": "c5def5", + "description": "Restructuring that preserves observable behaviour", + "tier": "type" + }, + { + "name": "tech-debt", + "color": "fbca04", + "description": "Known shortcut, drift, or hygiene owed - includes cleanup", + "tier": "type" + }, + { + "name": "testing", + "color": "bfd4f2", + "description": "Tests, benchmarks, fuzzing, property checks, coverage", + "tier": "type" + }, + { + "name": "chore", + "color": "ededed", + "description": "Routine maintenance with no behaviour change", + "tier": "type" + }, + { + "name": "research", + "color": "d4c5f9", + "description": "Open investigation; the outcome is knowledge, not code", + "tier": "type" + }, + { + "name": "decision", + "color": "8b5cf6", + "description": "A ruling is required before work can proceed", + "tier": "type" + }, + { + "name": "question", + "color": "d876e3", + "description": "Further information is requested", + "tier": "type" + }, + { + "name": "cicd", + "color": "006b75", + "description": "CI/CD: workflows, actions, lockfiles, pins, runners, release gates", + "tier": "area" + }, + { + "name": "security", + "color": "006b75", + "description": "Security posture, secrets, scanning, advisories, supply chain", + "tier": "area" + }, + { + "name": "proofs", + "color": "006b75", + "description": "Formal verification: Agda, Coq, Idris, Lean, Z3/SMT, axiom debt", + "tier": "area" + }, + { + "name": "governance", + "color": "006b75", + "description": "Policy, rulesets, standards, compliance, and their enforcement", + "tier": "area" + }, + { + "name": "design", + "color": "006b75", + "description": "Design of an interface, protocol, grammar, or type theory", + "tier": "area" + }, + { + "name": "architecture", + "color": "006b75", + "description": "Structural/system-level shape and runtime behaviour", + "tier": "area" + }, + { + "name": "performance", + "color": "006b75", + "description": "Throughput, latency, memory, binary size", + "tier": "area" + }, + { + "name": "bindings", + "color": "006b75", + "description": "ABI, FFI, WASM, and cross-language interop surfaces", + "tier": "area" + }, + { + "name": "migration", + "color": "006b75", + "description": "Porting between languages or toolchains (e.g. -> AffineScript)", + "tier": "area" + }, + { + "name": "packaging", + "color": "006b75", + "description": "Guix, Nix, containers, distribution artefacts", + "tier": "area" + }, + { + "name": "licensing", + "color": "006b75", + "description": "Licences, SPDX headers, REUSE compliance, attribution", + "tier": "area" + }, + { + "name": "automation", + "color": "006b75", + "description": "Bots, schedulers, dispatch, self-healing, fan-out", + "tier": "area" + }, + { + "name": "scaffolding", + "color": "006b75", + "description": "RSR templates, repo init, instantiation, project skeletons", + "tier": "area" + }, + { + "name": "conformance", + "color": "006b75", + "description": "Conformance to an external or internal specification", + "tier": "area" + }, + { + "name": "priority:p0", + "color": "b60205", + "description": "Critical - drop other work", + "tier": "priority" + }, + { + "name": "priority:p1", + "color": "d93f0b", + "description": "High - schedule next", + "tier": "priority" + }, + { + "name": "priority:p2", + "color": "e99695", + "description": "Normal - queue it", + "tier": "priority" + }, + { + "name": "priority:p3", + "color": "f9d0c4", + "description": "Low - nice to have", + "tier": "priority" + }, + { + "name": "status:blocked", + "color": "fbca04", + "description": "Cannot proceed until a dependency clears", + "tier": "status" + }, + { + "name": "status:ready", + "color": "fbca04", + "description": "Fully specified and ready to be picked up", + "tier": "status" + }, + { + "name": "status:needs-owner", + "color": "fbca04", + "description": "Unassigned and needs someone to take it", + "tier": "status" + }, + { + "name": "status:needs-ruling", + "color": "fbca04", + "description": "Awaiting an owner decision", + "tier": "status" + }, + { + "name": "status:do-not-automate", + "color": "fbca04", + "description": "Bots and sweeps must not touch this issue", + "tier": "status" + }, + { + "name": "meta:umbrella", + "color": "5319e7", + "description": "Parent issue aggregating child issues", + "tier": "meta" + }, + { + "name": "meta:campaign", + "color": "5319e7", + "description": "Coordinated multi-repo push with a defined end state", + "tier": "meta" + }, + { + "name": "meta:roadmap", + "color": "5319e7", + "description": "Forward planning; not yet actionable work", + "tier": "meta" + }, + { + "name": "meta:recurring", + "color": "5319e7", + "description": "Recurs on a schedule or by trigger; never finally closed", + "tier": "meta" + }, + { + "name": "scope:estate", + "color": "bfdadc", + "description": "Affects many or all repos across the estate", + "tier": "scope" + }, + { + "name": "scope:repo", + "color": "bfdadc", + "description": "Confined to this repository", + "tier": "scope" + } + ], + "frozen": [ + "dependencies", + "duplicate", + "elixir", + "gitar-approved", + "github_actions", + "good first issue", + "help wanted", + "invalid", + "javascript", + "never-stale", + "nix", + "pinned", + "python", + "rust", + "security", + "stale", + "wontfix" + ] +} diff --git a/vendor/bunsenite/.github/scripts/classify-issue.jq b/vendor/bunsenite/.github/scripts/classify-issue.jq new file mode 100644 index 0000000..6467c74 --- /dev/null +++ b/vendor/bunsenite/.github/scripts/classify-issue.jq @@ -0,0 +1,164 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# Classify one issue title against the estate label taxonomy. +# +# jq -r --arg title "docs: fix the README" \ +# --argjson have '[]' \ +# -f .github/scripts/classify-issue.jq .github/label-classifier.json +# +# Prints one label per line, or NOTHING when it cannot place the issue +# confidently. Nothing printed means "leave it for a human" -- a correct +# outcome, not a failure. +# +# WHY jq AND NOT PYTHON +# +# Python is fully banned estate-wide: the `governance / Language / package +# anti-pattern policy` gate runs `git ls-files '*.py'` and fails the PR +# ("Python is fully banned -- use AffineScript/Rust/SPARK/Julia"). This file +# is dispatched into every repo in the estate, so shipping it as .py would +# mean shipping an exemption into every repo too -- normalising the policy +# away by sweep. jq is preinstalled on every GitHub runner, is not on the +# banned list, needs no action (so no actions.lock entry can drift), and the +# rules are already JSON. +# +# The canonical implementation remains scripts/label-classify.py in the hub, +# which never runs in CI. tests/test-classifier-parity.py asserts this file +# agrees with it on every title in the corpus. +# +# `$have` lists labels the issue already carries. Anything already present is +# never re-suggested, and the classifier stays out of any max-1 tier the issue +# already has a label in, so a human's classification is never overridden. + +# Escape every non-alphanumeric so a keyword is matched literally. Escaping +# punctuation that needs no escape is harmless in Oniguruma. +def reesc: gsub("(?[^A-Za-z0-9 _])"; "\\\(.c)"); + +def norm: (. // "") | ascii_downcase + | sub("^[[:space:]]+"; "") | sub("[[:space:]]+$"; ""); + +# Asymmetric boundary: STRICT on the left, inflection-tolerant on the right. +# +# Measured over the issue corpus, the two error directions are not symmetric: +# * every false positive is a LEFT-side prefix -- `lean` in "clean up", +# `abi` in "capability", `mpl` in "Implement", `ffi` in "AffineScript", +# `smt` in "wasmtime". The left boundary must stay strict. +# * every real miss is a RIGHT-side inflection -- `test` vs "tests", +# `theorem` vs "theorems", `todo` vs "TODOs", `scaffold` vs "scaffolding". +# +# The right side therefore admits a CLOSED set of inflections. Closed, not open +# (`.*`), because an open right side re-admits the prefix false positives. +# +# `ion`/`ation` are excluded from the base set: they mint unrelated words +# (`port` + `ion` = "portion", and `port` is a live keyword). They are enabled +# only for shapes that are unambiguously truncated stems -- `-at` +# (instantiat, investigat, adjudicat) and `-ment` (document, implement). +def kwrx($kw): + ( "s|es|ed|d|ing|er|ers|y|ies" + + (if ($kw | endswith("at")) then "|ion|ions|e" + elif ($kw | endswith("ment")) then "|ation|ations" + else "" end) + ) as $suf + # Boundaries are conditional: a keyword not starting alphanumeric has no left + # boundary to enforce, and one not ending alphanumeric takes no suffix. + | (if ($kw | test("^[A-Za-z0-9]")) then "(?[^\\]]{1,25})\\]")) // null) as $m + | if $m == null then {rule: null, rest: $t} + else (($m.tag | norm | split("#")[0]) | norm) as $tag + | { rule: ($R.bracket_tag[$tag] // null), + rest: ($t | sub("^[[:space:]]*\\[[^\\]]{1,25}\\]"; "")) } + end; + +# Leading `word:` / `word(scope):` conventional-commit prefix. +def prefixrule($R; $t): + (($t | capture("^[[:space:]]*(?[A-Za-z][A-Za-z0-9_./-]{1,24})(?:[[:space:]]*\\([^)]*\\))?[[:space:]]*:")) // null) as $m + | if $m == null then null + else ($m.w | norm) as $k + # Compound prefixes such as "adaptive/must:" carry their meaning in the + # ISO 14764 category only; the modality does not label. + | (if ($R.prefix_split_on // "") != "" and ($k | contains($R.prefix_split_on)) + then ($k | split($R.prefix_split_on) | .[0]) else $k end) as $key + | ($R.title_prefix[$key] // null) + end; + +def signals($R; $tl; $sec): + [ ($R[$sec] // {}) | to_entries[] + | select(.value | any(. as $k | kwhit($k; $tl))) + | .key ]; + +# The HIGHEST-PRECEDENCE matching type, not merely the first in key order. +def kwtype($R; $tl): + [ $R.keyword_type | to_entries[] + | select(.value | any(. as $k | kwhit($k; $tl))) + | .key ] + | if length == 0 then null + else min_by([($R.precedence[.] // 99), .]) end; + +# Drop violations of each tier's `max`, keeping the highest-precedence member. +def enforce($R; $labels): + ($labels | unique) + | group_by($R.tier_of[.] // "?") + | map( ($R.tier_of[.[0]] // "?") as $tier + | ($R.tier_max[$tier] // null) as $mx + | if $mx == null or (length <= $mx) then . + else (sort_by([($R.precedence[.] // 99), .]))[0:$mx] end ) + | flatten; + +def classify($R; $title; $have0): + ($title // "") as $t0 + | ($t0 | norm) as $tl + | ($have0 | map(select(. != null and . != "")) + | unique) as $have + | ($R.tier_of | keys) as $canon + | $R.types as $types + | bracket($R; $t0) as $b + | (if $b.rule != null then ($b.rule | rulelabels) else [] end) as $l1 + | prefixrule($R; $b.rest) as $pr + | (if $pr != null then ($pr | rulelabels) else [] end) as $l2 + | (($b.rule != null) or ($pr != null)) as $matched0 + # 3. keyword areas are additive and never contribute a type + | ($l1 + $l2 + signals($R; $tl; "keyword_area")) as $acc + # 4. a type only if neither the rules nor the issue already supplied one + | (if (($acc + $have) | any(. as $x | $types | index($x))) + then null else kwtype($R; $tl) end) as $ty + | ($acc + (if $ty != null then [$ty] else [] end)) as $acc + | ($matched0 or ($ty != null)) as $matched + | ( $acc + + signals($R; $tl; "status_signal") + + signals($R; $tl; "meta_signal") + + signals($R; $tl; "scope_signal") ) as $acc + # NOTE: `frozen` is deliberately NOT subtracted. Frozen means "never rename or + # delete this label" -- `security` is frozen because triage.yml pins it in + # exempt-issue-labels. APPLYING it to an issue is correct; only the + # definition is protected. + | ($acc | map(select(. as $x | $canon | index($x))) | unique) as $acc + | enforce($R; $acc + ($have | map(select(. as $x | $canon | index($x))))) as $acc + | ($acc - $have) as $out + # Stay out of any max-1 tier the issue ALREADY has a label in -- a human's, + # or one an ISSUE_TEMPLATE applied. A prefix rule fires unconditionally, so + # "fix: ..." on an issue already labelled `enhancement` would otherwise add + # `bug` beside it. This covers every max-1 tier (type, priority, status, + # meta, scope), not just type. + | ( [ $R.tier_max | to_entries[] | select(.value == 1) | .key ] + | map(. as $t | select($have | any(($R.tier_of[.] // "?") == $t))) + ) as $lockedtiers + | ($out | map(select(($R.tier_of[.] // "?") as $t | ($lockedtiers | index($t)) | not))) as $out + # A rule must actually have FIRED: keyword-area hits alone are not enough. + | if ($matched | not) then [] + # a type is mandatory + elif ((($out + $have) | any(. as $x | $types | index($x))) | not) then [] + else ($out | sort) end; + +classify(.; $title; $have) | .[] diff --git a/vendor/bunsenite/.github/workflows/actions.lock b/vendor/bunsenite/.github/workflows/actions.lock new file mode 100644 index 0000000..4627463 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/actions.lock @@ -0,0 +1,327 @@ +# This file is machine-generated by `gh actions-lock`. +# Do not edit by hand; run `gh actions-lock` to update. +# Docs: https://gh.io/actions-lockfile +version: 'v0.0.2' +workflows: + '.github/workflows/boj-build.yml': + - 'actions/checkout@v4.1.7' + '.github/workflows/cargo-audit.yml': + - 'actions/checkout@v4.1.1' + '.github/workflows/casket-pages.yml': + - 'actions/cache@v4.3.0' + - 'actions/checkout@v4.1.1' + - 'actions/configure-pages@v5.0.0' + - 'actions/deploy-pages@v4.0.5' + - 'actions/upload-pages-artifact@v3.0.1' + - 'haskell-actions/setup@v2.7.5' + '.github/workflows/cflite_batch.yml': + - 'google/clusterfuzzlite@v1' + '.github/workflows/cflite_pr.yml': + - 'google/clusterfuzzlite@v1' + '.github/workflows/codeql.yml': + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938' + '.github/workflows/dependabot-automerge.yml': + - 'dependabot/fetch-metadata@v2.2.0' + '.github/workflows/dogfood-gate.yml': + - 'actions/checkout@v4.3.1' + - 'hyperpolymath/deed-ecosystem@main' + - 'hyperpolymath/k9-ecosystem@main' + '.github/workflows/generator-generic-ossf-slsa3-publish.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/ghcr-publish.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/governance.yml': [] + '.github/workflows/hypatia-scan.yml': [] + '.github/workflows/instant-sync.yml': + - 'peter-evans/repository-dispatch@v3.0.0' + '.github/workflows/label-triage.yml': [] + '.github/workflows/labels.yml': [] + '.github/workflows/mirror.yml': [] + '.github/workflows/pages.yml': + - 'actions/checkout@v4.4.0' + - 'actions/deploy-pages@v4.0.5' + - 'actions/upload-pages-artifact@v3.0.1' + '.github/workflows/publish-aur.yml': + - 'actions/checkout@v6.0.1' + - 'ksxgithub/github-actions-deploy-aur@v3.0.1' + '.github/workflows/publish-chocolatey.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/publish-container.yml': + - 'actions/attest-build-provenance@v2.4.0' + - 'actions/checkout@v6.0.1' + - 'docker/build-push-action@v6.9.0' + - 'docker/login-action@v3.3.0' + - 'docker/metadata-action@v5.5.1' + '.github/workflows/publish-copr.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/publish-debian-ppa.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/publish-flatpak.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/publish-homebrew.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/publish-macports.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/publish-nixpkgs.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/publish-obs.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/publish-packages.yml': + - 'peter-evans/repository-dispatch@v3.0.0' + '.github/workflows/publish-scoop.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/publish-winget.yml': + - 'actions/checkout@v6.0.1' + '.github/workflows/push-email-notify.yml': + - 'dawidd6/action-send-mail@v3.12.0' + '.github/workflows/release.yml': + - 'actions/attest-build-provenance@v2.4.0' + - 'actions/checkout@v6.0.1' + - 'actions/download-artifact@v4.1.8' + - 'actions/setup-node@v4.0.2' + - 'actions/upload-artifact@v4.6.2' + - 'dtolnay/rust-toolchain@v1' + - 'goto-bus-stop/setup-zig@v2.2.1' + - 'softprops/action-gh-release@v2.2.1' + '.github/workflows/rust-ci.yml': [] + '.github/workflows/scorecard.yml': [] + '.github/workflows/secret-scanner.yml': [] + '.github/workflows/stress-test.yml': + - 'actions/checkout@v6.0.1' + - 'dtolnay/rust-toolchain@v1' + '.github/workflows/workflow-linter.yml': + - 'actions/checkout@v4.1.1' + '.github/workflows/zig-ffi.yml': + - 'actions/cache@v4.3.0' + - 'actions/checkout@v6.0.1' + - 'actions/upload-artifact@v4.6.2' + - 'denoland/setup-deno@v1.5.2' + - 'dtolnay/rust-toolchain@v1' + - 'goto-bus-stop/setup-zig@v2.2.1' +dependencies: + 'actions/attest-build-provenance@1176ef556905f349f669722abf30bce1a6e16e01': + ref: 'predicate@1.1.5' + commit: 'sha1-1176ef556905f349f669722abf30bce1a6e16e01' + owner_id: 44036562 + repo_id: 760702757 + 'actions/attest-build-provenance@v2.4.0': + ref: 'v2.4.0' + commit: 'sha1-e8998f949152b193b063cb0ec769d69d929409be' + owner_id: 44036562 + repo_id: 760702757 + uses: + - 'actions/attest-build-provenance@1176ef556905f349f669722abf30bce1a6e16e01' + - 'actions/attest@ce27ba3b4a9a139d9a20a4a07d69fabb52f1e5bc' + 'actions/attest@ce27ba3b4a9a139d9a20a4a07d69fabb52f1e5bc': + ref: 'v2.4.0' + commit: 'sha1-ce27ba3b4a9a139d9a20a4a07d69fabb52f1e5bc' + owner_id: 44036562 + repo_id: 760701061 + 'actions/cache@v4.3.0': + ref: 'v4.3.0' + commit: 'sha1-0057852bfaa89a56745cba8c7296529d2fc39830' + owner_id: 44036562 + repo_id: 215566462 + 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': + ref: 'v7.0.1' + commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' + owner_id: 44036562 + repo_id: 197814629 + 'actions/checkout@v4.1.1': + ref: 'v4.1.1' + commit: 'sha1-b4ffde65f46336ab88eb53be808477a3936bae11' + owner_id: 44036562 + repo_id: 197814629 + 'actions/checkout@v4.1.7': + ref: 'v4.1.7' + commit: 'sha1-692973e3d937129bcbf40652eb9f2f61becf3332' + owner_id: 44036562 + repo_id: 197814629 + 'actions/checkout@v4.3.1': + ref: 'v4.3.1' + commit: 'sha1-34e114876b0b11c390a56381ad16ebd13914f8d5' + owner_id: 44036562 + repo_id: 197814629 + 'actions/checkout@v4.4.0': + ref: 'v4.4.0' + commit: 'sha1-11d5960a326750d5838078e36cf38b85af677262' + owner_id: 44036562 + repo_id: 197814629 + 'actions/checkout@v6.0.1': + ref: 'v6.0.1' + commit: 'sha1-8e8c483db84b4bee98b60c0593521ed34d9990e8' + owner_id: 44036562 + repo_id: 197814629 + 'actions/configure-pages@v5.0.0': + ref: 'v5.0.0' + commit: 'sha1-983d7736d9b0ae728b81ab479565c72886d7745b' + owner_id: 44036562 + repo_id: 513659658 + 'actions/deploy-pages@v4.0.5': + ref: 'v4.0.5' + commit: 'sha1-d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e' + owner_id: 44036562 + repo_id: 438112499 + 'actions/download-artifact@v4.1.8': + ref: 'v4.1.8' + commit: 'sha1-fa0a91b85d4f404e444e00e005971372dc801d16' + owner_id: 44036562 + repo_id: 192626254 + 'actions/setup-node@v4.0.2': + ref: 'v4.0.2' + commit: 'sha1-60edb5dd545a775178f52524783378180af0d1f8' + owner_id: 44036562 + repo_id: 189476904 + 'actions/upload-artifact@v4': + ref: 'v4' + commit: 'sha1-ea165f8d65b6e75b540449e92b4886f43607fa02' + owner_id: 44036562 + repo_id: 192625955 + 'actions/upload-artifact@v4.6.2': + ref: 'v4.6.2' + commit: 'sha1-ea165f8d65b6e75b540449e92b4886f43607fa02' + owner_id: 44036562 + repo_id: 192625955 + 'actions/upload-pages-artifact@v3.0.1': + ref: 'v3.0.1' + commit: 'sha1-56afc609e74202658d3ffba0e8f6dda462b719fa' + owner_id: 44036562 + repo_id: 496012378 + uses: + - 'actions/upload-artifact@v4' + 'cachix/install-nix-action@v30': + ref: 'v30' + commit: 'sha1-08dcb3a5e62fa31e2da3d490afc4176ef55ecd72' + owner_id: 36824654 + repo_id: 212301524 + 'dawidd6/action-send-mail@v3.12.0': + ref: 'v3.12.0' + commit: 'sha1-2cea9617b09d79a095af21254fbcb7ae95903dde' + owner_id: 9713907 + repo_id: 222439721 + 'denoland/setup-deno@v1.5.2': + ref: 'v1.5.2' + commit: 'sha1-11b63cf76cfcafb4e43f97b6cad24d8e8438f62d' + owner_id: 42048915 + repo_id: 356423100 + 'dependabot/fetch-metadata@v2.2.0': + ref: 'v2.2.0' + commit: 'sha1-dbb049abf0d677abbd7f7eee0375145b417fdd34' + owner_id: 27347476 + repo_id: 371068214 + 'docker/build-push-action@v6.9.0': + ref: 'v6.9.0' + commit: 'sha1-4f58ea79222b3b9dc2c8bbdd6debcef730109a75' + owner_id: 5429470 + repo_id: 241092383 + 'docker/login-action@v3.3.0': + ref: 'v3.3.0' + commit: 'sha1-9780b0c442fbb1117ed29e0efdff1e18412f7567' + owner_id: 5429470 + repo_id: 287743349 + 'docker/metadata-action@v5.5.1': + ref: 'v5.5.1' + commit: 'sha1-8e5442c4ef9f78752691e2d8f8d19755c6f78e81' + owner_id: 5429470 + repo_id: 306769011 + 'dtolnay/rust-toolchain@v1': + ref: 'v1' + commit: 'sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' + owner_id: 1940490 + repo_id: 260749683 + 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938': + ref: 'v4.37.9' + commit: 'sha1-cdf488f595d80d6e07e03d4674febd5ab45fa938' + owner_id: 9919 + repo_id: 259445878 + 'google/clusterfuzzlite@v1': + ref: 'v1' + commit: 'sha1-884713a6c30a92e5e8544c39945cd7cb630abcd1' + owner_id: 1342004 + repo_id: 400046858 + 'goto-bus-stop/setup-zig@v2.2.1': + ref: 'v2.2.1' + commit: 'sha1-abea47f85e598557f500fa1fd2ab7464fcb39406' + owner_id: 1006268 + repo_id: 212984112 + 'haskell-actions/setup@v2.7.5': + ref: 'v2.7.5' + commit: 'sha1-ec49483bfc012387b227434aba94f59a6ecd0900' + owner_id: 75048950 + repo_id: 623796603 + 'hyperpolymath/deed-ecosystem@main': + ref: 'main' + commit: 'sha1-f9d999b60cb5f383679ea19912bcdc49c944973a' + owner_id: 6759885 + repo_id: 1275649586 + 'hyperpolymath/k9-ecosystem@main': + ref: 'main' + commit: 'sha1-2155aa26a21758f2ba119f61bc7e0e1981c106fb' + owner_id: 6759885 + repo_id: 1275650185 + 'ksxgithub/github-actions-deploy-aur@v3.0.1': + ref: 'v3.0.1' + commit: 'sha1-a97f56a8425a7a7f3b8c58607f769c69b089cadb' + owner_id: 11488886 + repo_id: 261159912 + 'peter-evans/repository-dispatch@v3.0.0': + ref: 'v3.0.0' + commit: 'sha1-ff45666b9427631e3450c54a1bcbee4d9ff4d7c0' + owner_id: 18365890 + repo_id: 220359305 + 'softprops/action-gh-release@v2.2.1': + ref: 'v2.2.1' + commit: 'sha1-c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda' + owner_id: 2242 + repo_id: 204253808 + 'Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6': + ref: 'v2' + commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' + owner_id: 580492 + repo_id: 298565987 + 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': + ref: 'v6.1.0' + commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + owner_id: 44036562 + repo_id: 215566462 + 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a': + ref: 'v7.0.1' + commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + owner_id: 44036562 + repo_id: 192625955 + 'actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb': + ref: 'tag' + commit: 'sha1-83fd05a356d7e2593de66fc9913b3002723633cb' + owner_id: 44036562 + repo_id: 192625955 + 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772': + ref: 'stable' + commit: 'sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' + owner_id: 1940490 + repo_id: 260749683 + 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393': + ref: 'v3.0.0' + commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' + owner_id: 26415196 + repo_id: 297874902 + 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': + ref: 'v1.24.1' + commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' + owner_id: 47606891 + repo_id: 331103973 + 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': + ref: 'v2.4.4' + commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' + owner_id: 67707773 + repo_id: 421101922 + 'softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844': + ref: 'v0.1.15' + commit: 'sha1-de2c0eb89ae2a093876385947365aca7b0e5f844' + owner_id: 2242 + repo_id: 204253808 + 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': + ref: 'v0.10.0' + commit: 'sha1-e83874834305fe9a4a2997156cb26c5de65a8555' + owner_id: 135788 + repo_id: 208510314 diff --git a/vendor/bunsenite/.github/workflows/boj-build.yml b/vendor/bunsenite/.github/workflows/boj-build.yml new file mode 100644 index 0000000..e4a8e6b --- /dev/null +++ b/vendor/bunsenite/.github/workflows/boj-build.yml @@ -0,0 +1,23 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: BoJ Server Build Trigger +on: + push: + branches: [main, master] + workflow_dispatch: +jobs: + trigger-boj: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4.1.7 + - name: Trigger BoJ Server (Casket/ssg-mcp) + run: | + # Send a secure trigger to boj-server to build this repository + curl -X POST "http://boj-server.local:7700/cartridges/ssg-mcp/invoke" -H "Content-Type: application/json" -d "{\"repo\": \"${{ github.repository }}\", \"branch\": \"${{ github.ref_name }}\", \"engine\": \"casket\\"}"} + continue-on-error: true +permissions: + contents: read diff --git a/vendor/bunsenite/.github/workflows/cargo-audit.yml b/vendor/bunsenite/.github/workflows/cargo-audit.yml new file mode 100644 index 0000000..3cf5d93 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/cargo-audit.yml @@ -0,0 +1,60 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +# Prevention workflow - audits Rust dependencies for vulnerabilities +name: Cargo Audit + +on: + push: + branches: [main] + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' + pull_request: + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' + schedule: + - cron: '0 6 * * 1' # Weekly on Monday + +permissions: read-all + +jobs: + audit: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4.1.1 + + - name: Install cargo-audit + run: cargo install cargo-audit --locked + + - name: Run cargo audit + run: cargo audit --deny warnings + + - name: Check for unmaintained crates + run: cargo audit --deny unmaintained + + # Optional: Create issues for vulnerabilities + create-issue: + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: audit + if: failure() + permissions: + issues: write + steps: + - uses: actions/checkout@v4.1.1 + + - name: Create vulnerability issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + EXISTING=$(gh issue list --label "security,dependencies" --state open --json number -q '.[0].number') + if [ -z "$EXISTING" ]; then + gh issue create \ + --title "Security: Dependency vulnerabilities detected" \ + --body "cargo audit found vulnerabilities. Run \`cargo audit\` locally for details." \ + --label "security,dependencies" + fi diff --git a/vendor/bunsenite/.github/workflows/casket-pages.yml b/vendor/bunsenite/.github/workflows/casket-pages.yml new file mode 100644 index 0000000..8a35d6a --- /dev/null +++ b/vendor/bunsenite/.github/workflows/casket-pages.yml @@ -0,0 +1,121 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: GitHub Pages + +on: + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + + - name: Checkout casket-ssg + uses: actions/checkout@v4.1.1 + with: + repository: hyperpolymath/casket-ssg + path: .casket-ssg + + - name: Setup GHCup + uses: haskell-actions/setup@v2.7.5 + with: + ghc-version: '9.8.2' + cabal-version: '3.10' + + - name: Cache Cabal + uses: actions/cache@v4.3.0 + with: + path: | + ~/.cabal/packages + ~/.cabal/store + .casket-ssg/dist-newstyle + key: ${{ runner.os }}-casket-${{ hashFiles('.casket-ssg/casket-ssg.cabal') }} + + - name: Build casket-ssg + working-directory: .casket-ssg + run: cabal build + + - name: Prepare site source + shell: bash + run: | + set -euo pipefail + rm -rf .site-src _site + + if [ -d site ]; then + cp -R site .site-src + else + mkdir -p .site-src + TODAY="$(date +%Y-%m-%d)" + REPO_NAME="${{ github.event.repository.name }}" + REPO_URL="https://github.com/${{ github.repository }}" + README_URL="" + + if [ -f README.md ]; then + README_URL="${REPO_URL}/blob/${{ github.ref_name }}/README.md" + elif [ -f README.adoc ]; then + README_URL="${REPO_URL}/blob/${{ github.ref_name }}/README.adoc" + fi + + { + echo "---" + echo "title: ${REPO_NAME}" + echo "date: ${TODAY}" + echo "---" + echo + echo "# ${REPO_NAME}" + echo + echo "Static documentation site for ${REPO_NAME}." + echo + echo "- Source repository: [${{ github.repository }}](${REPO_URL})" + if [ -n "${README_URL}" ]; then + echo "- README: [project README](${README_URL})" + fi + if [ -d docs ]; then + echo "- Docs directory: [docs/](${REPO_URL}/tree/${{ github.ref_name }}/docs)" + fi + echo + echo "Project-specific site content can be added later under site/." + } > .site-src/index.md + fi + + - name: Build site + run: | + mkdir -p _site + cd .casket-ssg && cabal run casket-ssg -- build ../.site-src ../_site + touch ../_site/.nojekyll + + - name: Setup Pages + uses: actions/configure-pages@v5.0.0 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3.0.1 + with: + path: '_site' + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4.0.5 diff --git a/vendor/bunsenite/.github/workflows/cflite_batch.yml b/vendor/bunsenite/.github/workflows/cflite_batch.yml new file mode 100644 index 0000000..2a81f24 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/cflite_batch.yml @@ -0,0 +1,37 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: ClusterFuzzLite batch fuzzing +on: + schedule: + - cron: '0 3 * * 0' # Weekly on Sunday at 3am UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + BatchFuzzing: + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + sanitizer: [address] + steps: + - name: Build Fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + language: rust + sanitizer: ${{ matrix.sanitizer }} + + - name: Run Fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 1800 + mode: batch + sanitizer: ${{ matrix.sanitizer }} diff --git a/vendor/bunsenite/.github/workflows/cflite_pr.yml b/vendor/bunsenite/.github/workflows/cflite_pr.yml new file mode 100644 index 0000000..b564941 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/cflite_pr.yml @@ -0,0 +1,36 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: ClusterFuzzLite PR fuzzing +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + PR: + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + sanitizer: [address] + steps: + - name: Build Fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + language: rust + sanitizer: ${{ matrix.sanitizer }} + + - name: Run Fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 300 + mode: code-change + sanitizer: ${{ matrix.sanitizer }} diff --git a/vendor/bunsenite/.github/workflows/codeql.yml b/vendor/bunsenite/.github/workflows/codeql.yml new file mode 100644 index 0000000..80d2588 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/codeql.yml @@ -0,0 +1,55 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: CodeQL Security Analysis + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + schedule: + - cron: '0 6 1 * *' # monthly 1st 06:00 UTC + +# Estate guardrail: cancel superseded runs so re-pushes / rebased PR +# updates do not pile up queued runs against the shared account-wide +# Actions concurrency pool. Applied only to read-only check workflows +# (no publish/mutation), so cancelling a superseded run is always safe. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/vendor/bunsenite/.github/workflows/dependabot-automerge.yml b/vendor/bunsenite/.github/workflows/dependabot-automerge.yml new file mode 100644 index 0000000..da95664 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/dependabot-automerge.yml @@ -0,0 +1,147 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +# +# dependabot-automerge.yml — enable GitHub's native auto-merge on +# Dependabot pull requests that match a declared severity / ecosystem +# policy. Pairs with `.github/dependabot.yml`'s +# `open-pull-requests-limit: 0` + security-only pattern (see the +# cargo block there). +# +# What this does: +# - Triggers on every Dependabot PR. +# - Reads the PR's update-type metadata via the dependabot/fetch-metadata +# action (no free-text parsing). +# - Requires CI to be green before merge (GitHub's auto-merge enforces +# required status checks). +# - Gates merge behind a severity+ecosystem policy table. Default is +# low+medium security updates only. +# +# Why auto-merge on GitHub (not via a bot like rhodibot) is the right +# layer: GitHub enforces branch protection + required checks natively, +# and the PR author is already `dependabot[bot]`. Rhodibot doesn't need +# to know anything about ecosystems — GitHub handles the merge mechanics +# once we approve. +# +# Threat model: +# - A compromised upstream package with a bogus security advisory +# could propose a malicious version bump. Mitigation: require at +# least one non-automated reviewer for HIGH+CRITICAL severity +# (done below — we explicitly refuse to auto-approve those). +# - A compromised Dependabot itself is an Akerlof claim-grounder +# problem. Not in scope here; track under +# `project_claim_grounders_dual_use_akerlof.md`. +# +# Dogfooding: this workflow template is itself subject to the same +# Dependabot config via the github-actions ecosystem block, so SHA +# bumps for dependabot/fetch-metadata flow through the same path. + +name: Dependabot Auto-Merge + +on: + pull_request: + types: [opened, reopened, synchronize] + +permissions: + contents: read # needed to enable auto-merge + pull-requests: write # needed to approve + # NB: keep narrow — do NOT add secrets: read or id-token: write here. + +jobs: + automerge: + # Only run for PRs actually authored by Dependabot. + if: github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]' + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Fetch Dependabot metadata + id: meta + uses: dependabot/fetch-metadata@v2.2.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + # --- Policy gate ------------------------------------------------------- + # Outputs from fetch-metadata we care about: + # update-type → version-update:semver-{patch,minor,major} + # dependency-type → direct:{development,production} | indirect + # alert-state → AUTO_DISMISSED | DISMISSED | FIXED | OPEN + # ghsa-id → GHSA-... if this is a security PR + # --- Policy ------------------------------------------------------------- + # AUTO-APPROVE + AUTO-MERGE when: + # 1. This is a SECURITY update (ghsa-id present), AND + # 2. Update is patch or minor, AND + # 3. Severity ≤ moderate (Dependabot doesn't expose severity + # directly in fetch-metadata; infer from the absence of + # HIGH/CRITICAL labels added by Dependabot). + # Otherwise: do nothing. Human reviews HIGH+CRITICAL security + # updates and all non-security bumps. + - name: Decide policy outcome + id: policy + env: + GHSA_ID: ${{ steps.meta.outputs.ghsa-id }} + UPDATE_TYPE: ${{ steps.meta.outputs.update-type }} + PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }} + run: | + set -euo pipefail + + is_security=false + is_patch_or_minor=false + is_high_or_critical=false + + [ -n "$GHSA_ID" ] && is_security=true + case "$UPDATE_TYPE" in + version-update:semver-patch|version-update:semver-minor) + is_patch_or_minor=true ;; + esac + + # Dependabot adds severity labels like "severity: high", + # "severity: critical". Look for those in the PR labels JSON. + if echo "$PR_LABELS" | grep -qiE '"(severity: (high|critical))"'; then + is_high_or_critical=true + fi + + if $is_security && $is_patch_or_minor && ! $is_high_or_critical; then + echo "action=automerge" >> "$GITHUB_OUTPUT" + else + echo "action=skip" >> "$GITHUB_OUTPUT" + fi + echo "security=$is_security" >> "$GITHUB_OUTPUT" + echo "update_type=$UPDATE_TYPE" >> "$GITHUB_OUTPUT" + echo "ghsa=$GHSA_ID" >> "$GITHUB_OUTPUT" + + - name: Approve PR (if policy allows) + if: steps.policy.outputs.action == 'automerge' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + gh pr review --approve "$PR_URL" \ + --body "Auto-approving Dependabot security update (${{ steps.policy.outputs.ghsa }}, ${{ steps.policy.outputs.update_type }}). Policy: low/moderate security patches/minors only." + + - name: Enable auto-merge (if policy allows) + if: steps.policy.outputs.action == 'automerge' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + gh pr merge --auto --squash "$PR_URL" + + - name: Write decision to step summary + env: + ACTION: ${{ steps.policy.outputs.action }} + IS_SECURITY: ${{ steps.policy.outputs.security }} + UPDATE_TYPE: ${{ steps.policy.outputs.update_type }} + GHSA: ${{ steps.policy.outputs.ghsa }} + run: | + { + echo "## Dependabot Auto-Merge Decision" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| Policy action | \`$ACTION\` |" + echo "| Security update | \`$IS_SECURITY\` |" + echo "| Update type | \`$UPDATE_TYPE\` |" + echo "| GHSA ID | \`${GHSA:-n/a}\` |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/vendor/bunsenite/.github/workflows/dogfood-gate.yml b/vendor/bunsenite/.github/workflows/dogfood-gate.yml new file mode 100644 index 0000000..909b913 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/dogfood-gate.yml @@ -0,0 +1,418 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# dogfood-gate.yml — Hyperpolymath Dogfooding Quality Gate +# Validates that the repo uses hyperpolymath's own formats and tools. +# Companion to static-analysis-gate.yml (security) — this is for format compliance. +name: Dogfood Gate + +on: + pull_request: + branches: ['**'] + push: + branches: [main, master] + +permissions: + contents: read + +jobs: + # --------------------------------------------------------------------------- + # Job 1: A2ML manifest validation + # --------------------------------------------------------------------------- + a2ml-validate: + name: Validate DEED manifests + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4.3.1 + + - name: Check for A2ML files + id: detect + run: | + COUNT=$(find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) + echo "count=$COUNT" >> "$GITHUB_OUTPUT" + if [ "$COUNT" -eq 0 ]; then + echo "::warning::No .a2ml/.deed manifest files found. Every RSR repo should have a repo deed (_chora.deed); legacy 0-AI-MANIFEST.a2ml accepted mid-migration — standards #837" + fi + + - name: Validate A2ML manifests + if: steps.detect.outputs.count > 0 + uses: hyperpolymath/deed-ecosystem/validate-action@main + with: + path: '.' + strict: 'false' + + - name: Write summary + run: | + A2ML_COUNT="${{ steps.detect.outputs.count }}" + if [ "$A2ML_COUNT" -eq 0 ]; then + cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" + ## A2ML Validation + + :warning: **No .a2ml/.deed manifest files found.** Every RSR-compliant repo should have a repo deed (`_chora.deed`) at its root. + + Copy it from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo). + EOF + else + echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Scanned **${A2ML_COUNT}** manifest file(s) (.deed, or legacy .a2ml). See step output for details." >> "$GITHUB_STEP_SUMMARY" + fi + + # --------------------------------------------------------------------------- + # Job 2: K9 contract validation + # --------------------------------------------------------------------------- + k9-validate: + name: Validate K9 contracts + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4.3.1 + + - name: Check for K9 files + id: detect + run: | + COUNT=$(find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | wc -l) + CONFIG_COUNT=$(find . \( -name '*.toml' -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' \) \ + -not -path './.git/*' -not -path './node_modules/*' -not -path './.deno/*' \ + -not -name 'package-lock.json' -not -name 'Cargo.lock' -not -name 'deno.lock' | wc -l) + echo "k9_count=$COUNT" >> "$GITHUB_OUTPUT" + echo "config_count=$CONFIG_COUNT" >> "$GITHUB_OUTPUT" + if [ "$COUNT" -eq 0 ] && [ "$CONFIG_COUNT" -gt 0 ]; then + echo "::warning::Found $CONFIG_COUNT config files but no K9 contracts. Run k9iser to generate contracts." + fi + + - name: Validate K9 contracts + if: steps.detect.outputs.k9_count > 0 + uses: hyperpolymath/k9-ecosystem/validate-action@main + with: + path: '.' + strict: 'false' + + - name: Write summary + run: | + K9_COUNT="${{ steps.detect.outputs.k9_count }}" + CFG_COUNT="${{ steps.detect.outputs.config_count }}" + if [ "$K9_COUNT" -eq 0 ]; then + cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" + ## K9 Contract Validation + + :warning: **No .a2ml/.deed manifest files found.** Every RSR-compliant repo should have a repo deed (`_chora.deed`) at its root. + + Generate contracts with: `k9iser generate .` + EOF + else + echo "## K9 Contract Validation" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Validated **${K9_COUNT}** K9 contract(s) against **${CFG_COUNT}** config file(s)." >> "$GITHUB_STEP_SUMMARY" + fi + + # --------------------------------------------------------------------------- + # Job 3: Empty-linter — invisible character detection + # --------------------------------------------------------------------------- + empty-lint: + name: Empty-linter (invisible characters) + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4.3.1 + + - name: Scan for invisible characters + id: lint + run: | + # Inline invisible character detection (from empty-linter's core patterns). + # Checks for: zero-width spaces, zero-width joiners, BOM, soft hyphens, + # non-breaking spaces, null bytes, and other invisible Unicode in source files. + set +e + PATTERNS='(*UTF)[\x00-\x08\x0B\x0C\x0E-\x1F\x{a0}\x{ad}\x{200b}-\x{200f}\x{202a}-\x{202f}\x{2060}\x{2066}-\x{2069}\x{feff}]' + find "$GITHUB_WORKSPACE" \ + -not -path '*/.git/*' -not -path '*/node_modules/*' \ + -not -path '*/.deno/*' -not -path '*/target/*' \ + -not -path '*/_build/*' -not -path '*/deps/*' \ + -not -path '*/external_corpora/*' -not -path '*/.lake/*' \ + -type f \( -name '*.rs' -o -name '*.ex' -o -name '*.exs' -o -name '*.res' \ + -o -name '*.js' -o -name '*.ts' -o -name '*.json' -o -name '*.toml' \ + -o -name '*.yml' -o -name '*.yaml' -o -name '*.md' -o -name '*.adoc' \ + -o -name '*.idr' -o -name '*.zig' -o -name '*.v' -o -name '*.jl' \ + -o -name '*.gleam' -o -name '*.hs' -o -name '*.ml' -o -name '*.sh' \) \ + -exec grep -aPrl "$PATTERNS" {} \; > /tmp/empty-lint-results.txt 2>/dev/null + EL_EXIT=$? + set -e + + FINDINGS=$(wc -l < /tmp/empty-lint-results.txt 2>/dev/null || echo 0) + echo "findings=$FINDINGS" >> "$GITHUB_OUTPUT" + echo "exit_code=$EL_EXIT" >> "$GITHUB_OUTPUT" + echo "ready=true" >> "$GITHUB_OUTPUT" + + # Blocking subset: C0 controls and NUL only (owner ruling 2026-08-28). + # Invisible Unicode (NBSP/BOM/zero-width) stays ADVISORY - about 2,100 + # estate files carry it as legitimate typography in prose. + blocking=0 + while IFS= read -r bf; do + [ -z "$bf" ] && continue + if grep -qaP '\x00|[\x01-\x08\x0B\x0C\x0E-\x1F]' "$bf"; then + blocking=$((blocking+1)) + echo "::error file=${bf#$GITHUB_WORKSPACE/}::C0 control characters or NUL bytes - file corruption, blocks the gate" + fi + done < /tmp/empty-lint-results.txt + echo "blocking=$blocking" >> "$GITHUB_OUTPUT" + + # Emit annotations for each file with invisible chars + while IFS= read -r filepath; do + [ -z "$filepath" ] && continue + REL_PATH="${filepath#$GITHUB_WORKSPACE/}" + echo "::warning file=${REL_PATH}::Invisible Unicode characters detected (zero-width space, BOM, NBSP, etc.)" + done < /tmp/empty-lint-results.txt + + # Enforce (owner ruling 2026-08-28): C0/NUL corruption BLOCKS; other + # invisible Unicode stays advisory. Enforcement lives inside this step + # so a crash above fails the job directly - counts can never arrive + # empty into a separate check that then passes silently. + if [ "$EL_EXIT" -ne 0 ]; then + echo "::warning::invisible-character scan exited $EL_EXIT - results may be incomplete" + fi + if [ "${blocking:-0}" -gt 0 ]; then + echo "## Empty-linter: BLOCKED - $blocking file(s) with C0/NUL corruption" >> "$GITHUB_STEP_SUMMARY" + echo "::error::$blocking file(s) contain C0 control characters or NUL bytes - corruption, not typography. See file annotations." + exit 1 + elif [ "${FINDINGS:-0}" -gt 0 ]; then + echo "::notice::$FINDINGS file(s) carry invisible Unicode (NBSP/BOM/zero-width) - advisory only" + fi + + - name: Write summary + run: | + if [ "${{ steps.lint.outputs.ready }}" = "true" ]; then + FINDINGS="${{ steps.lint.outputs.findings }}" + if [ "$FINDINGS" -gt 0 ] 2>/dev/null; then + echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Found **${FINDINGS}** invisible character issue(s). See annotations above." >> "$GITHUB_STEP_SUMMARY" + else + echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo ":white_check_mark: No invisible character issues found." >> "$GITHUB_STEP_SUMMARY" + fi + else + echo "## Empty-Linter" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Skipped: empty-linter not available." >> "$GITHUB_STEP_SUMMARY" + fi + + # --------------------------------------------------------------------------- + # Job 4: Groove manifest check (for repos that should expose services) + # --------------------------------------------------------------------------- + groove-check: + name: Groove manifest check + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4.3.1 + + - name: Check for Groove manifest + id: groove + run: | + # Check for static or dynamic Groove endpoints + HAS_MANIFEST="false" + HAS_GROOVE_CODE="false" + + if [ -f ".well-known/groove/manifest.json" ]; then + HAS_MANIFEST="true" + # Validate the manifest JSON + if ! jq empty .well-known/groove/manifest.json 2>/dev/null; then + echo "::error file=.well-known/groove/manifest.json::Invalid JSON in Groove manifest" + else + SVC_ID=$(jq -r '.service_id // "unknown"' .well-known/groove/manifest.json) + echo "service_id=$SVC_ID" >> "$GITHUB_OUTPUT" + fi + fi + + # Check for Groove endpoint code (Rust, Elixir, Zig, V) + if grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' --include='*.res' . 2>/dev/null | head -1 | grep -q .; then + HAS_GROOVE_CODE="true" + fi + + # Check if this repo likely serves HTTP (has server/listener code) + HAS_SERVER="false" + if grep -rl 'TcpListener\|Bandit\|Plug.Cowboy\|httpz\|vweb\|axum::serve\|actix_web' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' . 2>/dev/null | head -1 | grep -q .; then + HAS_SERVER="true" + fi + + echo "has_manifest=$HAS_MANIFEST" >> "$GITHUB_OUTPUT" + echo "has_groove_code=$HAS_GROOVE_CODE" >> "$GITHUB_OUTPUT" + echo "has_server=$HAS_SERVER" >> "$GITHUB_OUTPUT" + + if [ "$HAS_SERVER" = "true" ] && [ "$HAS_MANIFEST" = "false" ] && [ "$HAS_GROOVE_CODE" = "false" ]; then + echo "::warning::This repo has server code but no Groove endpoint. Add .well-known/groove/manifest.json for service discovery." + fi + + - name: Write summary + run: | + echo "## Groove Protocol Check" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Check | Status |" >> "$GITHUB_STEP_SUMMARY" + echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY" + echo "| Static manifest (.well-known/groove/manifest.json) | ${{ steps.groove.outputs.has_manifest }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Groove endpoint in code | ${{ steps.groove.outputs.has_groove_code }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Has HTTP server code | ${{ steps.groove.outputs.has_server }} |" >> "$GITHUB_STEP_SUMMARY" + + # --------------------------------------------------------------------------- + # Job 5: eclexiaiser manifest validation + # --------------------------------------------------------------------------- + eclexiaiser-validate: + name: Validate eclexiaiser manifest + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4.3.1 + + - name: Check and validate eclexiaiser manifest + id: eclex + run: | + if [ ! -f "eclexiaiser.toml" ]; then + # Check if repo has a Containerfile — if so, recommend eclexiaiser + if [ -f "Containerfile" ]; then + echo "::warning::Containerfile present but no eclexiaiser.toml. Run \`eclexiaiser init\` to scaffold energy/carbon budgets." + fi + echo "has_manifest=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "has_manifest=true" >> "$GITHUB_OUTPUT" + + # Validate TOML structure using Python 3.11+ tomllib + python3 -c " + import tomllib, sys + with open('eclexiaiser.toml', 'rb') as f: + data = tomllib.load(f) + project = data.get('project', {}) + if not project.get('name', '').strip(): + print('ERROR: project.name is required', file=sys.stderr) + sys.exit(1) + functions = data.get('functions', []) + if not functions: + print('ERROR: at least one [[functions]] entry is required', file=sys.stderr) + sys.exit(1) + for fn in functions: + if not fn.get('name', '').strip(): + print('ERROR: function name cannot be empty', file=sys.stderr) + sys.exit(1) + if not fn.get('source', '').strip(): + print(f'ERROR: function {fn[\"name\"]} has no source path', file=sys.stderr) + sys.exit(1) + print(f'Valid: {project[\"name\"]} ({len(functions)} function(s))') + " || { + echo "::error file=eclexiaiser.toml::Invalid eclexiaiser.toml — see step output for details" + exit 1 + } + + - name: Write summary + run: | + if [ "${{ steps.eclex.outputs.has_manifest }}" = "true" ]; then + echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo ":white_check_mark: **eclexiaiser.toml** present and valid." >> "$GITHUB_STEP_SUMMARY" + else + echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo ":ballot_box_with_check: No eclexiaiser.toml. Add one with \`eclexiaiser init\` for energy/carbon tracking." >> "$GITHUB_STEP_SUMMARY" + fi + + # --------------------------------------------------------------------------- + # Job 6: Dogfooding summary + # --------------------------------------------------------------------------- + dogfood-summary: + name: Dogfooding compliance summary + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: [a2ml-validate, k9-validate, empty-lint, groove-check, eclexiaiser-validate] + if: always() + + steps: + - name: Checkout repository + uses: actions/checkout@v4.3.1 + + - name: Generate dogfooding scorecard + run: | + SCORE=0 + MAX=6 + + # A2ML manifest present? + if find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then + SCORE=$((SCORE + 1)) + A2ML_STATUS=":white_check_mark:" + else + A2ML_STATUS=":x:" + fi + + # K9 contracts present? + if find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | head -1 | grep -q .; then + SCORE=$((SCORE + 1)) + K9_STATUS=":white_check_mark:" + else + K9_STATUS=":x:" + fi + + # .editorconfig present? + if [ -f ".editorconfig" ]; then + SCORE=$((SCORE + 1)) + EC_STATUS=":white_check_mark:" + else + EC_STATUS=":x:" + fi + + # Groove manifest or code? + if [ -f ".well-known/groove/manifest.json" ] || grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' . 2>/dev/null | head -1 | grep -q .; then + SCORE=$((SCORE + 1)) + GROOVE_STATUS=":white_check_mark:" + else + GROOVE_STATUS=":ballot_box_with_check:" + fi + + # VeriSimDB integration? + if grep -rl 'verisimdb\|VeriSimDB' --include='*.toml' --include='*.yaml' --include='*.yml' --include='*.json' --include='*.rs' --include='*.ex' . 2>/dev/null | head -1 | grep -q .; then + SCORE=$((SCORE + 1)) + VSDB_STATUS=":white_check_mark:" + else + VSDB_STATUS=":ballot_box_with_check:" + fi + + # eclexiaiser energy tracking? + if [ -f "eclexiaiser.toml" ]; then + SCORE=$((SCORE + 1)) + ECLEX_STATUS=":white_check_mark:" + else + ECLEX_STATUS=":ballot_box_with_check:" + fi + + cat <> "$GITHUB_STEP_SUMMARY" + ## Dogfooding Scorecard + + **Score: ${SCORE}/${MAX}** + + | Tool/Format | Status | Notes | + |-------------|--------|-------| + | DEED repo deed (`_chora.deed`) | ${A2ML_STATUS} | Required for all RSR repos | + | K9 contracts | ${K9_STATUS} | Required for repos with config files | + | .editorconfig | ${EC_STATUS} | Required for all repos | + | Groove endpoint | ${GROOVE_STATUS} | Required for service repos | + | VeriSimDB integration | ${VSDB_STATUS} | Required for stateful repos | + | eclexiaiser | ${ECLEX_STATUS} | Energy/carbon budgets for container services | + + --- + *Generated by the [Dogfood Gate](https://github.com/hyperpolymath/rsr-template-repo) workflow.* + *Dogfooding is guinea pig fooding — we test our tools on ourselves.* + EOF + diff --git a/vendor/bunsenite/.github/workflows/generator-generic-ossf-slsa3-publish.yml b/vendor/bunsenite/.github/workflows/generator-generic-ossf-slsa3-publish.yml new file mode 100644 index 0000000..a2d80e0 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/generator-generic-ossf-slsa3-publish.yml @@ -0,0 +1,75 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow lets you generate SLSA provenance file for your project. +# The generation satisfies level 3 for the provenance requirements - see https://slsa.dev/spec/v0.1/requirements +# The project is an initiative of the OpenSSF (openssf.org) and is developed at +# https://github.com/slsa-framework/slsa-github-generator. +# The provenance file can be verified using https://github.com/slsa-framework/slsa-verifier. +# For more information about SLSA and how it improves the supply-chain, visit slsa.dev. + +name: SLSA generic generator +on: + workflow_dispatch: + release: + types: [created] + + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 15 + outputs: + digests: ${{ steps.hash.outputs.digests }} + + steps: + - uses: actions/checkout@v6.0.1 + + # ======================================================== + # + # Step 1: Build your artifacts. + # + # ======================================================== + - name: Build artifacts + run: | + # These are some amazing artifacts. + echo "artifact1" > artifact1 + echo "artifact2" > artifact2 + + # ======================================================== + # + # Step 2: Add a step to generate the provenance subjects + # as shown below. Update the sha256 sum arguments + # to include all binaries that you generate + # provenance for. + # + # ======================================================== + - name: Generate subject for provenance + id: hash + run: | + set -euo pipefail + + # List the artifacts the provenance will refer to. + files=$(ls artifact*) + # Generate the subjects (base64 encoded). + echo "hashes=$(sha256sum $files | base64 -w0)" >> "${GITHUB_OUTPUT}" + + provenance: + needs: [build] + permissions: + actions: read # To read the workflow path. + id-token: write # To sign the provenance. + contents: write # To add assets to a release. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@68bad40844440577b33778c9f29077a3388838e9 # v1.4.0 + with: + base64-subjects: "${{ needs.build.outputs.digests }}" + upload-assets: true # Optional: Upload to a new release diff --git a/vendor/bunsenite/.github/workflows/ghcr-publish.yml b/vendor/bunsenite/.github/workflows/ghcr-publish.yml new file mode 100644 index 0000000..3bfc607 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/ghcr-publish.yml @@ -0,0 +1,65 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish to GHCR + +permissions: + contents: read + +on: + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.1 + + - name: Install nerdctl and containerd + run: | + sudo apt-get update + sudo apt-get install -y containerd + sudo systemctl start containerd + + NERDCTL_VERSION=2.2.1 + curl -fsSL "https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-full-${NERDCTL_VERSION}-linux-amd64.tar.gz" -o /tmp/nerdctl.tar.gz + sudo tar -xzf /tmp/nerdctl.tar.gz -C /usr/local + sudo mkdir -p /opt/cni/bin + sudo cp /usr/local/libexec/cni/* /opt/cni/bin/ 2>/dev/null || true + + sudo /usr/local/bin/buildkitd & + sleep 3 + + - name: Log in to GHCR + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | sudo nerdctl login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build image + run: | + sudo nerdctl build -f Containerfile -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} . + sudo nerdctl tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Push image + run: | + sudo nerdctl push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + sudo nerdctl push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Tag release version + if: github.event_name == 'release' + run: | + VERSION=${{ github.event.release.tag_name }} + sudo nerdctl tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION} + sudo nerdctl push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION} diff --git a/vendor/bunsenite/.github/workflows/governance.yml b/vendor/bunsenite/.github/workflows/governance.yml new file mode 100644 index 0000000..094a73b --- /dev/null +++ b/vendor/bunsenite/.github/workflows/governance.yml @@ -0,0 +1,20 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Governance + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: + +permissions: + actions: read + contents: read + +jobs: + governance: + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 diff --git a/vendor/bunsenite/.github/workflows/hypatia-scan.yml b/vendor/bunsenite/.github/workflows/hypatia-scan.yml new file mode 100644 index 0000000..0c009e8 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/hypatia-scan.yml @@ -0,0 +1,23 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Hypatia Security Scan + +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master] + schedule: + - cron: '0 0 * * 0' + workflow_dispatch: + +permissions: + actions: read + contents: read + security-events: write + +jobs: + hypatia: + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 diff --git a/vendor/bunsenite/.github/workflows/instant-sync.yml b/vendor/bunsenite/.github/workflows/instant-sync.yml new file mode 100644 index 0000000..e3a3052 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/instant-sync.yml @@ -0,0 +1,37 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +# Instant Forge Sync - Triggers propagation to all forges on push/release +name: Instant Sync + +on: + push: + branches: [main, master] + release: + types: [published] + +permissions: + contents: read + +jobs: + dispatch: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger Propagation + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.FARM_DISPATCH_TOKEN }} + repository: hyperpolymath/.git-private-farm + event-type: propagate + client-payload: |- + { + "repo": "${{ github.event.repository.name }}", + "ref": "${{ github.ref }}", + "sha": "${{ github.sha }}", + "forges": "" + } + + - name: Confirm + run: echo "::notice::Propagation triggered for ${{ github.event.repository.name }}" diff --git a/vendor/bunsenite/.github/workflows/label-triage.yml b/vendor/bunsenite/.github/workflows/label-triage.yml new file mode 100644 index 0000000..814a192 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/label-triage.yml @@ -0,0 +1,117 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +name: Label Triage + +# Classify newly-filed issues against the estate label taxonomy. +# +# The sweep that established the taxonomy is a one-off; this is what stops it +# decaying. Without it every new issue arrives unlabelled and the 55%-unlabelled +# state rebuilds itself. +# +# ⚠ NO `uses:` ANYWHERE, DELIBERATELY. The estate enforces +# .github/workflows/actions.lock, which is keyed BY WORKFLOW PATH: a workflow +# the lock does not list is rejected before any step runs (startup_failure, and +# therefore no check run at all). A dispatched workflow lands in repos whose +# lock has not been regenerated, so it must not depend on any action. +# +# ⚠ THE CLASSIFIER IS jq, NOT PYTHON. Python is fully banned estate-wide -- the +# `governance / Language / package anti-pattern policy` gate runs +# `git ls-files '*.py'` and fails the PR. Shipping a .py into 416 repos would +# mean shipping an exemption into 416 repos. jq is preinstalled on every GitHub +# runner, is not banned, and needs no action. +# +# Deliberately conservative: +# - ADDITIVE ONLY. It never removes a label and never overrides a human's +# classification: anything already on the issue is passed in via `have` and +# is never re-suggested, and the classifier stays out of any max-1 tier the +# issue already carries a label in. +# - SILENT WHEN UNSURE. Nothing is printed unless a prefix, bracket or type +# rule actually fired. Roughly 70% of the historical corpus classified this +# way; the rest is meant to reach a human. +# - NEVER FAILS THE ISSUE. Every step is best-effort; a missing payload or an +# API hiccup exits 0 rather than leaving a red mark on someone's bug report. + +on: + issues: + types: [opened, reopened] + workflow_dispatch: + inputs: + issue: + description: "Issue number to (re)classify" + required: true + +permissions: + issues: write + contents: read + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - name: Classify and label + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NUM: ${{ github.event.issue.number || inputs.issue }} + run: | + set -uo pipefail + work=$(mktemp -d); RULES=$work/rules.json; SCRIPT=$work/classify.jq + + # fetch instead of checking out -- no action means no lock entry to drift + gh api "repos/$GITHUB_REPOSITORY/contents/.github/label-classifier.json?ref=$GITHUB_SHA" \ + --jq '.content' 2>/dev/null | base64 -d > "$RULES" || true + gh api "repos/$GITHUB_REPOSITORY/contents/.github/scripts/classify-issue.jq?ref=$GITHUB_SHA" \ + --jq '.content' 2>/dev/null | base64 -d > "$SCRIPT" || true + if [[ ! -s "$RULES" || ! -s "$SCRIPT" ]]; then + echo "no classifier payload in this repo - nothing to do" + exit 0 + fi + + TITLE=$(gh issue view "$NUM" -R "$GITHUB_REPOSITORY" --json title --jq .title) || exit 0 + echo "issue #$NUM: $TITLE" + + # Labels this repo actually defines. --limit 1000 is GitHub's real + # per-repo ceiling; the default of 30 would silently hide most of the + # taxonomy. Fetched BEFORE the label read below so that read stays as + # close to the write as possible. + mapfile -t DEFINED < <(gh label list -R "$GITHUB_REPOSITORY" --limit 1000 \ + --json name --jq '.[].name' 2>/dev/null) + + # Labels already present; a human's work is never overridden. Read + # HERE rather than earlier: every API call between this read and the + # edit below widens a window in which someone could add a type label + # and get a second one back from us. Only the local jq call is inside it. + HAVE=$(gh issue view "$NUM" -R "$GITHUB_REPOSITORY" \ + --json labels --jq '[.labels[].name]' 2>/dev/null) || HAVE='[]' + [[ -n "$HAVE" ]] || HAVE='[]' + echo "already has: $HAVE" + + mapfile -t ADD < <(jq -r --arg title "$TITLE" --argjson have "$HAVE" \ + -f "$SCRIPT" "$RULES" 2>/dev/null) + if [[ ${#ADD[@]} -eq 0 || -z "${ADD[0]:-}" ]]; then + echo "no confident classification - leaving for a human" + exit 0 + fi + + apply=() + for want in "${ADD[@]}"; do + for def in "${DEFINED[@]}"; do + if [[ "$want" == "$def" ]]; then apply+=("$want"); break; fi + done + done + if [[ ${#apply[@]} -eq 0 ]]; then + echo "classified as ${ADD[*]} but this repo defines none of them - run the label sync" + exit 0 + fi + + printf 'applying: %s\n' "${apply[*]}" + # Build the arguments as an ARRAY. The previous form was an unquoted + # command substitution, so the shell re-split its output on spaces and + # a label name containing whitespace would arrive as several broken + # arguments. No canonical label contains a space today, which is + # exactly why this would have failed quietly the first time one did. + # (Also clears actionlint SC2046.) + edit_args=() + for lab in "${apply[@]}"; do edit_args+=(--add-label "$lab"); done + gh issue edit "$NUM" -R "$GITHUB_REPOSITORY" "${edit_args[@]}" \ + || echo "label apply failed - not failing the run" + exit 0 diff --git a/vendor/bunsenite/.github/workflows/labels.yml b/vendor/bunsenite/.github/workflows/labels.yml new file mode 100644 index 0000000..83ab941 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/labels.yml @@ -0,0 +1,106 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +name: Labels + +# Applies the canonical estate label set from .github/labels.json. +# +# Additive and idempotent by design: it CREATES missing labels and UPDATES +# colour/description drift. It never deletes, and it never touches a label in +# the `frozen` list -- those are applied by Dependabot / PR automation, or are +# wired into triage.yml's exempt-issue-labels, and renaming them breaks things. +# +# jq is preinstalled on GitHub runners; PyYAML is not, which is why the payload +# is JSON rather than YAML. +# +# ⚠ NO `uses:` ANYWHERE, DELIBERATELY. The estate enforces +# .github/workflows/actions.lock, which is keyed BY WORKFLOW PATH: a workflow +# the lock does not list is rejected before any step runs (startup_failure, and +# therefore no check run at all). A dispatched workflow lands in repos whose +# lock has not been regenerated, so it must not depend on any action. + +on: + workflow_dispatch: + push: + paths: + - '.github/labels.json' + schedule: + - cron: "23 4 1 * *" # monthly drift repair + +permissions: + issues: write + contents: read + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Apply canonical labels + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # ⚠ LOAD-BEARING. This workflow deliberately does not check the repo + # out (no `uses:`, so no actions.lock entry can drift), which means + # `gh label create` / `gh label edit` have no git remote to infer a + # target from. Without GH_REPO every mutation fails, and because the + # errors used to be discarded the step still exited 0 reporting + # "created=0 updated=0" -- a silent, estate-wide no-op. + GH_REPO: ${{ github.repository }} + run: | + set -uo pipefail + work=$(mktemp -d); PAYLOAD=$work/labels.json + + # fetch instead of checking out -- no action means no lock entry to drift + gh api "repos/$GITHUB_REPOSITORY/contents/.github/labels.json?ref=$GITHUB_SHA" \ + --jq '.content' 2>/dev/null | base64 -d > "$PAYLOAD" || true + [ -s "$PAYLOAD" ] || { echo "no .github/labels.json - nothing to do"; exit 0; } + + mapfile -t FROZEN < <(jq -r '.frozen[]' "$PAYLOAD") + created=0; updated=0; skipped=0; failed=0 + + existing=$(gh api "repos/$GITHUB_REPOSITORY/labels" --paginate \ + --jq '.[] | [.name, .color, (.description // "")] | @tsv') + + while IFS=$'\t' read -r name color desc; do + [ -z "$name" ] && continue + frozen=0 + for f in "${FROZEN[@]}"; do [ "$f" = "$name" ] && frozen=1 && break; done + + cur=$(printf '%s\n' "$existing" | awk -F'\t' -v n="$name" '$1==n{print;exit}') + if [ -z "$cur" ]; then + # A MISSING label is created even when frozen. "Frozen" protects a + # label's DEFINITION from being renamed or recoloured -- it was + # never meant to stop the label existing. Skipping creation broke + # `security`, the one canonical label that is also frozen: it was + # absent from 10 of 12 sampled repos, and label-triage drops any + # label the repo does not define, so every `security` finding was + # silently discarded estate-wide. + if err=$(gh label create "$name" --color "$color" \ + --description "$desc" 2>&1 >/dev/null); then + created=$((created+1)); sleep 0.4 + else + echo " create failed: $name -- ${err:-unknown}"; failed=$((failed+1)) + fi + else + # Present AND frozen: leave it exactly as it is. + if [ "$frozen" -eq 1 ]; then skipped=$((skipped+1)); continue; fi + ccol=$(cut -f2 <<<"$cur"); cdesc=$(cut -f3- <<<"$cur") + if [ "${ccol,,}" != "${color,,}" ] || [ "$cdesc" != "$desc" ]; then + if err=$(gh label edit "$name" --color "$color" \ + --description "$desc" 2>&1 >/dev/null); then + updated=$((updated+1)); sleep 0.4 + else + echo " edit failed: $name -- ${err:-unknown}"; failed=$((failed+1)) + fi + fi + fi + done < <(jq -r '.labels[] | [.name, .color, .description] | @tsv' "$PAYLOAD") + + echo "created=$created updated=$updated frozen-skipped=$skipped failed=$failed" + + # Fail ONLY on the misconfiguration shape: work was attempted, every + # attempt failed. That is the silent-no-op signature. A single flaky + # label must not turn the whole estate's CI red. + if [ "$failed" -gt 0 ] && [ "$((created + updated))" -eq 0 ]; then + echo "every label mutation failed - the sync did nothing. Check GH_REPO and token scope." + exit 1 + fi + exit 0 diff --git a/vendor/bunsenite/.github/workflows/mirror.yml b/vendor/bunsenite/.github/workflows/mirror.yml new file mode 100644 index 0000000..b95ea10 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/mirror.yml @@ -0,0 +1,19 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Mirror to Git Forges + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + actions: read + contents: read + +jobs: + mirror: + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a + secrets: inherit diff --git a/vendor/bunsenite/.github/workflows/pages.yml b/vendor/bunsenite/.github/workflows/pages.yml new file mode 100644 index 0000000..96a53cf --- /dev/null +++ b/vendor/bunsenite/.github/workflows/pages.yml @@ -0,0 +1,57 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: GitHub Pages (Ddraig SSG) +on: + push: + branches: [main, master] + workflow_dispatch: +permissions: + contents: read + pages: write + id-token: write +concurrency: + group: "pages" + cancel-in-progress: false +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 15 + container: + image: ghcr.io/stefan-hoeck/idris2-pack@sha256:f0758996a931fb35d9ecb1de273c4d59dabe2a09b433afc7e357f65a08b7e1ff + steps: + - name: Checkout Site + uses: actions/checkout@v4.4.0 + - name: Checkout Ddraig SSG + uses: actions/checkout@v4.4.0 + with: + repository: hyperpolymath/ddraig-ssg + path: .ddraig-ssg + - name: Compile Ddraig + working-directory: .ddraig-ssg + run: idris2 Ddraig.idr -o ddraig + - name: Build site + run: | + mkdir -p src + if [ ! -f src/index.md ] && [ -f README.md ]; then + cp README.md src/index.md + elif [ ! -f src/index.md ]; then + echo "# ${GITHUB_REPOSITORY}" > src/index.md + fi + ./.ddraig-ssg/build/exec/ddraig build src _site https://hyperpolymath.github.io/${GITHUB_REPOSITORY#*/} + - name: Upload artifact + uses: actions/upload-pages-artifact@v3.0.1 + with: + path: '_site' + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4.0.5 diff --git a/vendor/bunsenite/.github/workflows/publish-aur.yml b/vendor/bunsenite/.github/workflows/publish-aur.yml new file mode 100644 index 0000000..dc0bcc8 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-aur.yml @@ -0,0 +1,115 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish AUR + +on: + repository_dispatch: + types: [publish-aur] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + sha_linux_x64: + description: 'SHA256 for Linux x64' + required: true + sha_linux_arm64: + description: 'SHA256 for Linux arm64' + required: true + +permissions: + contents: read + +jobs: + publish-aur: + name: Publish to AUR + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for AUR secrets + id: check-secret + run: | + if [ -z "${{ secrets.AUR_SSH_PRIVATE_KEY }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::AUR_SSH_PRIVATE_KEY not configured. Skipping AUR publish." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout bunsenite + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + echo "sha_linux_x64=${{ github.event.client_payload.sha_linux_x64 }}" >> $GITHUB_OUTPUT + echo "sha_linux_arm64=${{ github.event.client_payload.sha_linux_arm64 }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + echo "sha_linux_x64=${{ inputs.sha_linux_x64 }}" >> $GITHUB_OUTPUT + echo "sha_linux_arm64=${{ inputs.sha_linux_arm64 }}" >> $GITHUB_OUTPUT + fi + + - name: Generate PKGBUILD + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + SHA_X64="${{ steps.inputs.outputs.sha_linux_x64 }}" + SHA_ARM64="${{ steps.inputs.outputs.sha_linux_arm64 }}" + + mkdir -p aur-package + + cat > aur-package/PKGBUILD << EOF + # Maintainer: hyperpolymath + # SPDX-License-Identifier: MPL-2.0 + pkgname=bunsenite-bin + pkgver=${VERSION} + pkgrel=1 + pkgdesc="Nickel configuration file parser with multi-language FFI bindings (pre-built binary)" + arch=('x86_64' 'aarch64') + url="https://github.com/hyperpolymath/bunsenite" + license=('MIT' 'custom:Palimpsest-0.8') + depends=('gcc-libs') + provides=('bunsenite') + conflicts=('bunsenite' 'bunsenite-git') + + source_x86_64=("\${pkgname}-\${pkgver}-x86_64.tar.gz::https://github.com/hyperpolymath/bunsenite/releases/download/${TAG}/bunsenite-${TAG}-x86_64-unknown-linux-gnu.tar.gz") + source_aarch64=("\${pkgname}-\${pkgver}-aarch64.tar.gz::https://github.com/hyperpolymath/bunsenite/releases/download/${TAG}/bunsenite-${TAG}-aarch64-unknown-linux-gnu.tar.gz") + sha256sums_x86_64=('${SHA_X64}') + sha256sums_aarch64=('${SHA_ARM64}') + + package() { + install -Dm755 "bunsenite" "\$pkgdir/usr/bin/bunsenite" + # Install shared library if present + if [ -f "libbunsenite.so" ]; then + install -Dm755 "libbunsenite.so" "\$pkgdir/usr/lib/libbunsenite.so" + fi + } + EOF + + echo "Generated PKGBUILD:" + cat aur-package/PKGBUILD + + - name: Publish to AUR + if: steps.check-secret.outputs.skip != 'true' + uses: KSXGitHub/github-actions-deploy-aur@v3.0.1 + with: + pkgname: bunsenite-bin + pkgbuild: aur-package/PKGBUILD + commit_username: ${{ secrets.AUR_USERNAME }} + commit_email: ${{ secrets.AUR_EMAIL }} + ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + commit_message: "Update to ${{ steps.inputs.outputs.tag }}" + force_push: true diff --git a/vendor/bunsenite/.github/workflows/publish-chocolatey.yml b/vendor/bunsenite/.github/workflows/publish-chocolatey.yml new file mode 100644 index 0000000..5d368f0 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-chocolatey.yml @@ -0,0 +1,154 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish Chocolatey + +on: + repository_dispatch: + types: [publish-chocolatey] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + sha_windows_x64: + description: 'SHA256 for Windows x64' + required: true + +permissions: + contents: read + +jobs: + publish-chocolatey: + name: Publish to Chocolatey + runs-on: windows-latest + timeout-minutes: 15 + steps: + - name: Check for CHOCO_API_KEY + id: check-secret + shell: pwsh + run: | + if ([string]::IsNullOrEmpty("${{ secrets.CHOCO_API_KEY }}")) { + echo "skip=true" >> $env:GITHUB_OUTPUT + Write-Warning "CHOCO_API_KEY not configured. Skipping Chocolatey publish." + } else { + echo "skip=false" >> $env:GITHUB_OUTPUT + } + + - name: Checkout bunsenite + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + shell: pwsh + run: | + if ("${{ github.event_name }}" -eq "repository_dispatch") { + echo "version=${{ github.event.client_payload.version }}" >> $env:GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $env:GITHUB_OUTPUT + echo "sha_windows_x64=${{ github.event.client_payload.sha_windows_x64 }}" >> $env:GITHUB_OUTPUT + } else { + echo "version=${{ inputs.version }}" >> $env:GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $env:GITHUB_OUTPUT + echo "sha_windows_x64=${{ inputs.sha_windows_x64 }}" >> $env:GITHUB_OUTPUT + } + + - name: Download Windows binary + if: steps.check-secret.outputs.skip != 'true' + shell: pwsh + run: | + $VERSION = "${{ steps.inputs.outputs.version }}" + $TAG = "${{ steps.inputs.outputs.tag }}" + $URL = "https://github.com/hyperpolymath/bunsenite/releases/download/${TAG}/bunsenite-${TAG}-x86_64-pc-windows-msvc.zip" + + Write-Host "Downloading from: $URL" + Invoke-WebRequest -Uri $URL -OutFile bunsenite.zip + + # Create tools directory + New-Item -ItemType Directory -Force -Path packaging/chocolatey/tools + Expand-Archive bunsenite.zip -DestinationPath packaging/chocolatey/tools -Force + + - name: Create nuspec + if: steps.check-secret.outputs.skip != 'true' + shell: pwsh + run: | + $VERSION = "${{ steps.inputs.outputs.version }}" + + $nuspec = @" + + + + + bunsenite + ${VERSION} + Bunsenite + hyperpolymath + hyperpolymath + https://github.com/hyperpolymath/bunsenite + https://github.com/hyperpolymath/bunsenite/blob/main/LICENSE.txt + false + https://github.com/hyperpolymath/bunsenite + https://github.com/hyperpolymath/bunsenite/issues + nickel config configuration parser rust cli + Nickel configuration file parser with multi-language FFI bindings + + Bunsenite is a Nickel configuration file parser with multi-language FFI bindings. + + Features: + - Parse and evaluate Nickel configuration files + - Watch mode for live reloading + - Interactive REPL + - JSON Schema validation + - FFI bindings for Deno, AffineScript, and WebAssembly + + https://github.com/hyperpolymath/bunsenite/releases/tag/v${VERSION} + + + + + + "@ + + Set-Content -Path packaging/chocolatey/bunsenite.nuspec -Value $nuspec + Write-Host "Created nuspec:" + Get-Content packaging/chocolatey/bunsenite.nuspec + + - name: Create install script + if: steps.check-secret.outputs.skip != 'true' + shell: pwsh + run: | + $installScript = @' + $ErrorActionPreference = 'Stop' + $toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" + $exePath = Join-Path $toolsDir 'bunsenite.exe' + + # Create shim + Install-BinFile -Name 'bunsenite' -Path $exePath + '@ + + Set-Content -Path packaging/chocolatey/tools/chocolateyInstall.ps1 -Value $installScript + + $uninstallScript = @' + $ErrorActionPreference = 'Stop' + Uninstall-BinFile -Name 'bunsenite' + '@ + + Set-Content -Path packaging/chocolatey/tools/chocolateyUninstall.ps1 -Value $uninstallScript + + - name: Pack and push + if: steps.check-secret.outputs.skip != 'true' + shell: pwsh + run: | + cd packaging/chocolatey + choco pack bunsenite.nuspec + + # List generated package + Get-ChildItem *.nupkg + + # Push to Chocolatey + choco push bunsenite.*.nupkg --source https://push.chocolatey.org/ --api-key ${{ secrets.CHOCO_API_KEY }} diff --git a/vendor/bunsenite/.github/workflows/publish-container.yml b/vendor/bunsenite/.github/workflows/publish-container.yml new file mode 100644 index 0000000..a504715 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-container.yml @@ -0,0 +1,61 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish Container + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + packages: write + id-token: write + attestations: write + steps: + - uses: actions/checkout@v6.0.1 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5.5.1 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push + id: push + uses: docker/build-push-action@v6.9.0 + with: + context: . + file: ./Containerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Attest container provenance + uses: actions/attest-build-provenance@v2.4.0 + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/vendor/bunsenite/.github/workflows/publish-copr.yml b/vendor/bunsenite/.github/workflows/publish-copr.yml new file mode 100644 index 0000000..8198867 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-copr.yml @@ -0,0 +1,160 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish COPR + +on: + repository_dispatch: + types: [publish-copr] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + +permissions: + contents: read + +env: + TAP_REPO: hyperpolymath/homebrew-tap + +jobs: + trigger-copr: + name: Trigger COPR Build + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for COPR_WEBHOOK_URL + id: check-secret + run: | + if [ -z "${{ secrets.COPR_WEBHOOK_URL }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::COPR_WEBHOOK_URL not configured. Skipping COPR trigger." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + fi + + - name: Trigger COPR webhook + if: steps.check-secret.outputs.skip != 'true' + run: | + TAG="${{ steps.inputs.outputs.tag }}" + VERSION="${{ steps.inputs.outputs.version }}" + + echo "Triggering COPR build for ${TAG}" + + # Trigger the webhook + curl -X POST "${{ secrets.COPR_WEBHOOK_URL }}" \ + -H "Content-Type: application/json" \ + -d "{\"ref\": \"${TAG}\", \"committish\": \"${TAG}\"}" \ + --fail --silent --show-error + + echo "COPR webhook triggered successfully" + + update-spec: + name: Update RPM Spec in Tap + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for TAP_GITHUB_TOKEN + id: check-secret + run: | + if [ -z "${{ secrets.TAP_GITHUB_TOKEN }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::TAP_GITHUB_TOKEN not configured. Skipping RPM spec update." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout homebrew-tap + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + with: + repository: ${{ env.TAP_REPO }} + token: ${{ secrets.TAP_GITHUB_TOKEN }} + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + fi + + - name: Update RPM spec + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + DATE=$(date "+%a %b %d %Y") + + mkdir -p rpm + + cat > rpm/bunsenite.spec << EOF + # SPDX-License-Identifier: MPL-2.0 + Name: bunsenite + Version: ${VERSION} + Release: 1%{?dist} + Summary: Nickel configuration file parser with multi-language FFI bindings + + License: MIT OR Palimpsest-0.8 + URL: https://github.com/hyperpolymath/bunsenite + Source0: https://github.com/hyperpolymath/bunsenite/archive/refs/tags/${TAG}.tar.gz + + BuildRequires: cargo + BuildRequires: rust >= 1.70 + + %description + Bunsenite is a Nickel configuration file parser with multi-language FFI bindings. + Features include parse, validate, watch mode, interactive REPL, and JSON Schema validation. + + %prep + %autosetup -n bunsenite-%{version} + + %build + cargo build --release --features full + + %install + install -D -m 755 target/release/bunsenite %{buildroot}%{_bindir}/bunsenite + + %files + %license LICENSE.txt + %doc README.adoc + %{_bindir}/bunsenite + + %changelog + * ${DATE} hyperpolymath - ${VERSION}-1 + - Update to version ${VERSION} + EOF + + echo "Generated RPM spec:" + cat rpm/bunsenite.spec + + - name: Commit and push + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add rpm/ + git diff --staged --quiet || git commit -m "rpm: bunsenite ${VERSION}" + git push diff --git a/vendor/bunsenite/.github/workflows/publish-debian-ppa.yml b/vendor/bunsenite/.github/workflows/publish-debian-ppa.yml new file mode 100644 index 0000000..4a6e234 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-debian-ppa.yml @@ -0,0 +1,211 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish Debian PPA + +on: + repository_dispatch: + types: [publish-debian-ppa] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + +permissions: + contents: read + +env: + PPA_NAME: hyperpolymath/bunsenite + MAINTAINER_NAME: hyperpolymath + MAINTAINER_EMAIL: packages@hyperpolymath.dev + +jobs: + publish-ppa: + name: Publish to Launchpad PPA + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for Launchpad secrets + id: check-secret + run: | + if [ -z "${{ secrets.LAUNCHPAD_GPG_KEY }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::LAUNCHPAD_GPG_KEY not configured. Skipping PPA publish." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout bunsenite + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + fi + + - name: Install dependencies + if: steps.check-secret.outputs.skip != 'true' + run: | + sudo apt-get update + sudo apt-get install -y devscripts debhelper dput gnupg + + - name: Import GPG key + if: steps.check-secret.outputs.skip != 'true' + run: | + echo "${{ secrets.LAUNCHPAD_GPG_KEY }}" | gpg --batch --import + # Trust the key + KEY_ID=$(gpg --list-keys --keyid-format LONG | grep -A1 "^pub" | tail -1 | awk '{print $1}') + echo "${KEY_ID}:6:" | gpg --import-ownertrust + + - name: Download and prepare source + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + + # Download source tarball + curl -L -o bunsenite_${VERSION}.orig.tar.gz \ + "https://github.com/hyperpolymath/bunsenite/archive/refs/tags/${TAG}.tar.gz" + + # Extract + tar xzf bunsenite_${VERSION}.orig.tar.gz + mv bunsenite-${VERSION#v} bunsenite-${VERSION} + + - name: Create debian directory + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + cd bunsenite-${VERSION} + + mkdir -p debian/source + + # debian/control + cat > debian/control << EOF + Source: bunsenite + Section: devel + Priority: optional + Maintainer: ${MAINTAINER_NAME} <${MAINTAINER_EMAIL}> + Build-Depends: debhelper-compat (= 13), cargo, rustc (>= 1.70) + Standards-Version: 4.6.0 + Homepage: https://github.com/hyperpolymath/bunsenite + Vcs-Browser: https://github.com/hyperpolymath/bunsenite + Vcs-Git: https://github.com/hyperpolymath/bunsenite.git + Rules-Requires-Root: no + + Package: bunsenite + Architecture: any + Depends: \${shlibs:Depends}, \${misc:Depends} + Description: Nickel configuration file parser with FFI bindings + Bunsenite is a Nickel configuration file parser with multi-language + FFI bindings. Features include parse, validate, watch mode, + interactive REPL, and JSON Schema validation. + EOF + + # debian/rules + cat > debian/rules << 'EOF' + #!/usr/bin/make -f + # SPDX-License-Identifier: MPL-2.0 + + export CARGO_HOME = $(CURDIR)/debian/cargo + export DEB_BUILD_MAINT_OPTIONS = hardening=+all + + %: + dh $@ + + override_dh_auto_build: + cargo build --release --features full + + override_dh_auto_install: + install -D -m 755 target/release/bunsenite debian/bunsenite/usr/bin/bunsenite + + override_dh_auto_test: + # Skip tests during package build + EOF + chmod +x debian/rules + + # debian/changelog + DATE=$(date -R) + cat > debian/changelog << EOF + bunsenite (${VERSION}-1) jammy; urgency=medium + + * New upstream release ${VERSION} + + -- ${MAINTAINER_NAME} <${MAINTAINER_EMAIL}> ${DATE} + EOF + + # debian/copyright + cat > debian/copyright << EOF + Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + Upstream-Name: bunsenite + Upstream-Contact: ${MAINTAINER_EMAIL} + Source: https://github.com/hyperpolymath/bunsenite + + Files: * + Copyright: 2024-2025 hyperpolymath + License: MIT or Palimpsest-0.8 + + License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + EOF + + # debian/source/format + echo "3.0 (quilt)" > debian/source/format + + # debian/compat + echo "13" > debian/compat + + - name: Build source package + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + cd bunsenite-${VERSION} + + # Build source package (signed) + debuild -S -sa -k"${{ secrets.LAUNCHPAD_GPG_PASSPHRASE }}" + + - name: Upload to PPA + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + + # Create dput config + cat > ~/.dput.cf << EOF + [ppa] + fqdn = ppa.launchpad.net + method = ftp + incoming = ~${PPA_NAME}/ubuntu/ + login = anonymous + allow_unsigned_uploads = 0 + EOF + + # Upload to PPA + dput ppa bunsenite_${VERSION}-1_source.changes diff --git a/vendor/bunsenite/.github/workflows/publish-flatpak.yml b/vendor/bunsenite/.github/workflows/publish-flatpak.yml new file mode 100644 index 0000000..8dec706 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-flatpak.yml @@ -0,0 +1,167 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish Flatpak + +on: + repository_dispatch: + types: [publish-flatpak] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + +permissions: + contents: read + +env: + TAP_REPO: hyperpolymath/homebrew-tap + APP_ID: dev.hyperpolymath.Bunsenite + +jobs: + update-flatpak: + name: Update Flatpak Manifest + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for TAP_GITHUB_TOKEN + id: check-secret + run: | + if [ -z "${{ secrets.TAP_GITHUB_TOKEN }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::TAP_GITHUB_TOKEN not configured. Skipping Flatpak manifest update." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout homebrew-tap + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + with: + repository: ${{ env.TAP_REPO }} + token: ${{ secrets.TAP_GITHUB_TOKEN }} + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + fi + + - name: Get commit hash for tag + if: steps.check-secret.outputs.skip != 'true' + id: commit + run: | + TAG="${{ steps.inputs.outputs.tag }}" + # Get the commit SHA for the tag + COMMIT=$(gh api repos/hyperpolymath/bunsenite/git/refs/tags/$TAG --jq '.object.sha' 2>/dev/null || echo "") + + # If it's an annotated tag, we need to dereference it + if [ -z "$COMMIT" ] || [ "$COMMIT" = "null" ]; then + COMMIT=$(gh api repos/hyperpolymath/bunsenite/git/refs/tags/$TAG --jq '.object.sha') + OBJ_TYPE=$(gh api repos/hyperpolymath/bunsenite/git/tags/$COMMIT --jq '.object.type' 2>/dev/null || echo "commit") + if [ "$OBJ_TYPE" = "commit" ]; then + COMMIT=$(gh api repos/hyperpolymath/bunsenite/git/tags/$COMMIT --jq '.object.sha') + fi + fi + + echo "sha=$COMMIT" >> $GITHUB_OUTPUT + echo "Found commit: $COMMIT for tag: $TAG" + env: + GH_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} + + - name: Update Flatpak manifest + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + COMMIT="${{ steps.commit.outputs.sha }}" + + mkdir -p flathub + + cat > flathub/${APP_ID}.yml << EOF + # SPDX-License-Identifier: MPL-2.0 + app-id: ${APP_ID} + runtime: org.freedesktop.Platform + runtime-version: '23.08' + sdk: org.freedesktop.Sdk + sdk-extensions: + - org.freedesktop.Sdk.Extension.rust-stable + + command: bunsenite + + finish-args: + - --filesystem=home:ro + - --filesystem=xdg-config:ro + + build-options: + append-path: /usr/lib/sdk/rust-stable/bin + env: + CARGO_HOME: /run/build/bunsenite/cargo + RUSTUP_HOME: /usr/lib/sdk/rust-stable + + modules: + - name: bunsenite + buildsystem: simple + build-commands: + - cargo build --release --features full + - install -Dm755 target/release/bunsenite /app/bin/bunsenite + sources: + - type: git + url: https://github.com/hyperpolymath/bunsenite.git + tag: ${TAG} + commit: ${COMMIT} + EOF + + # Create metainfo file + cat > flathub/${APP_ID}.metainfo.xml << EOF + + + + ${APP_ID} + Bunsenite + Nickel configuration file parser with multi-language FFI bindings + CC0-1.0 + MIT OR LicenseRef-Palimpsest-0.8 + +

+ Bunsenite is a Nickel configuration file parser with multi-language FFI bindings. + It provides parse, validate, watch mode, interactive REPL, and JSON Schema validation. +

+
+ https://github.com/hyperpolymath/bunsenite + https://github.com/hyperpolymath/bunsenite/issues + + bunsenite + + + + https://github.com/hyperpolymath/bunsenite/releases/tag/${TAG} + + + +
+ EOF + + echo "Generated Flatpak manifest:" + cat flathub/${APP_ID}.yml + + - name: Commit and push + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add flathub/ + git diff --staged --quiet || git commit -m "flatpak: bunsenite ${VERSION}" + git push diff --git a/vendor/bunsenite/.github/workflows/publish-homebrew.yml b/vendor/bunsenite/.github/workflows/publish-homebrew.yml new file mode 100644 index 0000000..a747eed --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-homebrew.yml @@ -0,0 +1,151 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish Homebrew + +on: + repository_dispatch: + types: [publish-homebrew] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + sha_linux_x64: + description: 'SHA256 for Linux x64' + required: true + sha_linux_arm64: + description: 'SHA256 for Linux arm64' + required: true + sha_macos_x64: + description: 'SHA256 for macOS x64' + required: true + sha_macos_arm64: + description: 'SHA256 for macOS arm64' + required: true + +permissions: + contents: read + +env: + TAP_REPO: hyperpolymath/homebrew-tap + +jobs: + update-formula: + name: Update Homebrew Formula + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for TAP_GITHUB_TOKEN + id: check-secret + run: | + if [ -z "${{ secrets.TAP_GITHUB_TOKEN }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::TAP_GITHUB_TOKEN not configured. Skipping Homebrew publish." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout homebrew-tap + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + with: + repository: ${{ env.TAP_REPO }} + token: ${{ secrets.TAP_GITHUB_TOKEN }} + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + echo "sha_linux_x64=${{ github.event.client_payload.sha_linux_x64 }}" >> $GITHUB_OUTPUT + echo "sha_linux_arm64=${{ github.event.client_payload.sha_linux_arm64 }}" >> $GITHUB_OUTPUT + echo "sha_macos_x64=${{ github.event.client_payload.sha_macos_x64 }}" >> $GITHUB_OUTPUT + echo "sha_macos_arm64=${{ github.event.client_payload.sha_macos_arm64 }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + echo "sha_linux_x64=${{ inputs.sha_linux_x64 }}" >> $GITHUB_OUTPUT + echo "sha_linux_arm64=${{ inputs.sha_linux_arm64 }}" >> $GITHUB_OUTPUT + echo "sha_macos_x64=${{ inputs.sha_macos_x64 }}" >> $GITHUB_OUTPUT + echo "sha_macos_arm64=${{ inputs.sha_macos_arm64 }}" >> $GITHUB_OUTPUT + fi + + - name: Update Formula + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + SHA_LINUX_X64="${{ steps.inputs.outputs.sha_linux_x64 }}" + SHA_LINUX_ARM64="${{ steps.inputs.outputs.sha_linux_arm64 }}" + SHA_MACOS_X64="${{ steps.inputs.outputs.sha_macos_x64 }}" + SHA_MACOS_ARM64="${{ steps.inputs.outputs.sha_macos_arm64 }}" + + mkdir -p Formula + + cat > Formula/bunsenite.rb << 'FORMULA_EOF' + # frozen_string_literal: true + # SPDX-License-Identifier: MPL-2.0 + + # Homebrew formula for bunsenite + class Bunsenite < Formula + desc "Nickel configuration file parser with multi-language FFI bindings" + homepage "https://github.com/hyperpolymath/bunsenite" + license any_of: ["MIT", "LicenseRef-Palimpsest-0.8"] + FORMULA_EOF + + cat >> Formula/bunsenite.rb << FORMULA_DYNAMIC + version "${VERSION}" + + on_macos do + if Hardware::CPU.arm? + url "https://github.com/hyperpolymath/bunsenite/releases/download/${TAG}/bunsenite-${TAG}-aarch64-apple-darwin.tar.gz" + sha256 "${SHA_MACOS_ARM64}" + else + url "https://github.com/hyperpolymath/bunsenite/releases/download/${TAG}/bunsenite-${TAG}-x86_64-apple-darwin.tar.gz" + sha256 "${SHA_MACOS_X64}" + end + end + + on_linux do + if Hardware::CPU.arm? + url "https://github.com/hyperpolymath/bunsenite/releases/download/${TAG}/bunsenite-${TAG}-aarch64-unknown-linux-gnu.tar.gz" + sha256 "${SHA_LINUX_ARM64}" + else + url "https://github.com/hyperpolymath/bunsenite/releases/download/${TAG}/bunsenite-${TAG}-x86_64-unknown-linux-gnu.tar.gz" + sha256 "${SHA_LINUX_X64}" + end + end + FORMULA_DYNAMIC + + cat >> Formula/bunsenite.rb << 'FORMULA_EOF' + + def install + bin.install "bunsenite" + # Install shared library if present + lib.install Dir["libbunsenite.*"] + end + + test do + assert_match version.to_s, shell_output("#{bin}/bunsenite --version") + end + end + FORMULA_EOF + + echo "Generated Formula/bunsenite.rb:" + cat Formula/bunsenite.rb + + - name: Commit and push + if: steps.check-secret.outputs.skip != 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add Formula/bunsenite.rb + git diff --staged --quiet || git commit -m "bunsenite: update to ${{ steps.inputs.outputs.tag }}" + git push diff --git a/vendor/bunsenite/.github/workflows/publish-macports.yml b/vendor/bunsenite/.github/workflows/publish-macports.yml new file mode 100644 index 0000000..0c0d6f8 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-macports.yml @@ -0,0 +1,243 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish MacPorts + +on: + repository_dispatch: + types: [publish-macports] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + +permissions: + contents: read + +env: + TAP_REPO: hyperpolymath/homebrew-tap + +jobs: + update-tap: + name: Update MacPorts in Tap + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for TAP_GITHUB_TOKEN + id: check-secret + run: | + if [ -z "${{ secrets.TAP_GITHUB_TOKEN }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::TAP_GITHUB_TOKEN not configured. Skipping MacPorts tap update." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout homebrew-tap + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + with: + repository: ${{ env.TAP_REPO }} + token: ${{ secrets.TAP_GITHUB_TOKEN }} + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + fi + + - name: Download source and compute checksums + if: steps.check-secret.outputs.skip != 'true' + id: checksums + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + + # Download source tarball + curl -L -o source.tar.gz "https://github.com/hyperpolymath/bunsenite/archive/refs/tags/${TAG}.tar.gz" + + # Compute checksums + SHA256=$(sha256sum source.tar.gz | awk '{print $1}') + SIZE=$(stat -c%s source.tar.gz) + + # Compute RIPEMD-160 using openssl + RMD160=$(openssl dgst -rmd160 source.tar.gz | awk '{print $2}') + + echo "sha256=$SHA256" >> $GITHUB_OUTPUT + echo "rmd160=$RMD160" >> $GITHUB_OUTPUT + echo "size=$SIZE" >> $GITHUB_OUTPUT + + echo "Computed checksums:" + echo " SHA256: $SHA256" + echo " RMD160: $RMD160" + echo " Size: $SIZE" + + - name: Update Portfile + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + SHA256="${{ steps.checksums.outputs.sha256 }}" + RMD160="${{ steps.checksums.outputs.rmd160 }}" + SIZE="${{ steps.checksums.outputs.size }}" + + mkdir -p macports/bunsenite + + cat > macports/bunsenite/Portfile << 'EOF' + # -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:ft=tcl:et:sw=4:ts=4:sts=4 + # SPDX-License-Identifier: MPL-2.0 + + PortSystem 1.0 + PortGroup cargo 1.0 + PortGroup github 1.0 + + EOF + + cat >> macports/bunsenite/Portfile << EOF + github.setup hyperpolymath bunsenite ${VERSION} v + revision 0 + categories devel + license MIT Permissive + maintainers {github.com:hyperpolymath @hyperpolymath} openmaintainer + description Nickel configuration file parser with FFI bindings + long_description Bunsenite is a Nickel configuration file parser with \\ + multi-language FFI bindings. Features include parse, \\ + validate, watch mode, interactive REPL, and JSON Schema validation. + + homepage https://github.com/hyperpolymath/bunsenite + + checksums rmd160 ${RMD160} \\ + sha256 ${SHA256} \\ + size ${SIZE} + + EOF + + cat >> macports/bunsenite/Portfile << 'EOF' + build.args-append --features=full + + destroot { + xinstall -m 755 ${worksrcpath}/target/[cargo.rust_platform]/release/bunsenite \ + ${destroot}${prefix}/bin/bunsenite + } + EOF + + echo "Generated Portfile:" + cat macports/bunsenite/Portfile + + - name: Commit and push + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add macports/ + git diff --staged --quiet || git commit -m "macports: bunsenite ${VERSION}" + git push + + create-official-pr: + name: Create MacPorts Official PR + runs-on: macos-latest + timeout-minutes: 15 + needs: update-tap + if: ${{ inputs.submit_official == true }} + steps: + - name: Check for MACPORTS_GITHUB_TOKEN + id: check-secret + run: | + if [ -z "${{ secrets.MACPORTS_GITHUB_TOKEN }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::MACPORTS_GITHUB_TOKEN not configured. Skipping official MacPorts PR." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + fi + + - name: Fork and update macports-ports + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + + # Fork macports-ports if not already forked + gh repo fork macports/macports-ports --clone=true --remote=true || true + cd macports-ports + + # Create branch + git checkout -b bunsenite-${VERSION} + + # Download source and compute checksums + curl -L -o source.tar.gz "https://github.com/hyperpolymath/bunsenite/archive/refs/tags/${TAG}.tar.gz" + SHA256=$(shasum -a 256 source.tar.gz | awk '{print $1}') + RMD160=$(openssl dgst -rmd160 source.tar.gz | awk '{print $2}') + SIZE=$(stat -f%z source.tar.gz) + + # Create port directory + mkdir -p devel/bunsenite + + # Generate Portfile + cat > devel/bunsenite/Portfile << EOF + # -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:ft=tcl:et:sw=4:ts=4:sts=4 + + PortSystem 1.0 + PortGroup cargo 1.0 + PortGroup github 1.0 + + github.setup hyperpolymath bunsenite ${VERSION} v + revision 0 + categories devel + license MIT Permissive + maintainers {github.com:hyperpolymath @hyperpolymath} openmaintainer + description Nickel configuration file parser with FFI bindings + long_description Bunsenite is a Nickel configuration file parser with \\ + multi-language FFI bindings. + + homepage https://github.com/hyperpolymath/bunsenite + + checksums rmd160 ${RMD160} \\ + sha256 ${SHA256} \\ + size ${SIZE} + + build.args-append --features=full + + destroot { + xinstall -m 755 \${worksrcpath}/target/[cargo.rust_platform]/release/bunsenite \\ + \${destroot}\${prefix}/bin/bunsenite + } + EOF + + # Commit and push + git add devel/bunsenite/Portfile + git commit -m "bunsenite: new port, version ${VERSION}" + git push origin bunsenite-${VERSION} + + # Create PR + gh pr create \ + --title "bunsenite: new port, version ${VERSION}" \ + --body "New port for bunsenite - Nickel configuration file parser with multi-language FFI bindings. + + Homepage: https://github.com/hyperpolymath/bunsenite + License: MIT OR Palimpsest-0.8" \ + --repo macports/macports-ports + env: + GH_TOKEN: ${{ secrets.MACPORTS_GITHUB_TOKEN }} diff --git a/vendor/bunsenite/.github/workflows/publish-nixpkgs.yml b/vendor/bunsenite/.github/workflows/publish-nixpkgs.yml new file mode 100644 index 0000000..19798a3 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-nixpkgs.yml @@ -0,0 +1,203 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish Nixpkgs + +on: + repository_dispatch: + types: [publish-nixpkgs] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + +permissions: + contents: read + +env: + TAP_REPO: hyperpolymath/homebrew-tap + +jobs: + update-guix: + name: Update Guix Expression in Tap + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for TAP_GITHUB_TOKEN + id: check-secret + run: | + if [ -z "${{ secrets.TAP_GITHUB_TOKEN }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::TAP_GITHUB_TOKEN not configured. Skipping Guix update." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout homebrew-tap + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + with: + repository: ${{ env.TAP_REPO }} + token: ${{ secrets.TAP_GITHUB_TOKEN }} + + - name: Install Guix + if: steps.check-secret.outputs.skip != 'true' + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + fi + + - name: Calculate source hash + if: steps.check-secret.outputs.skip != 'true' + id: hash + run: | + TAG="${{ steps.inputs.outputs.tag }}" + + # Use guix-prefetch-url to get the hash in SRI format + HASH=$(guix-prefetch-url --unpack "https://github.com/hyperpolymath/bunsenite/archive/refs/tags/${TAG}.tar.gz" 2>/dev/null) + SRI_HASH=$(guix hash to-sri --type sha256 "$HASH") + + echo "hash=$SRI_HASH" >> $GITHUB_OUTPUT + echo "Calculated hash: $SRI_HASH" + + - name: Update Guix expression + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + HASH="${{ steps.hash.outputs.hash }}" + + mkdir -p guix + + # Create default.guix for the package + cat > guix/bunsenite.guix << EOF + # SPDX-License-Identifier: MPL-2.0 + { lib + , rustPlatform + , fetchFromGitHub + }: + + rustPlatform.buildRustPackage rec { + pname = "bunsenite"; + version = "${VERSION}"; + + src = fetchFromGitHub { + owner = "hyperpolymath"; + repo = "bunsenite"; + rev = "${TAG}"; + hash = "${HASH}"; + }; + + cargoLock = { + lockFile = "\${src}/Cargo.lock"; + }; + + buildFeatures = [ "full" ]; + + meta = with lib; { + description = "Nickel configuration file parser with multi-language FFI bindings"; + homepage = "https://github.com/hyperpolymath/bunsenite"; + license = with licenses; [ mit /* Palimpsest-0.8 */ ]; + maintainers = [ ]; + mainProgram = "bunsenite"; + }; + } + EOF + + # Create flake.guix for standalone use + cat > guix/flake.guix << EOF + # SPDX-License-Identifier: MPL-2.0 + { + description = "Bunsenite - Nickel configuration file parser with FFI bindings"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.\${system}; + bunsenite = pkgs.callPackage ./bunsenite.guix { }; + in + { + packages = { + default = bunsenite; + bunsenite = bunsenite; + }; + + apps.default = flake-utils.lib.mkApp { + drv = bunsenite; + }; + + devShells.default = pkgs.mkShell { + buildInputs = [ bunsenite ]; + }; + } + ); + } + EOF + + # Create overlay for use in other flakes + cat > guix/overlay.guix << EOF + # SPDX-License-Identifier: MPL-2.0 + final: prev: { + bunsenite = final.callPackage ./bunsenite.guix { }; + } + EOF + + echo "Generated Guix files:" + cat guix/bunsenite.guix + + - name: Commit and push + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add guix/ + git diff --staged --quiet || git commit -m "guix: bunsenite ${VERSION}" + git push + + create-nixpkgs-pr: + name: Create nixpkgs PR (Optional) + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: update-guix + if: ${{ inputs.submit_nixpkgs == true }} + steps: + - name: Check for NIXPKGS_GITHUB_TOKEN + id: check-secret + run: | + if [ -z "${{ secrets.NIXPKGS_GITHUB_TOKEN }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::NIXPKGS_GITHUB_TOKEN not configured. Skipping nixpkgs PR." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Create nixpkgs PR + if: steps.check-secret.outputs.skip != 'true' + run: | + echo "To submit to nixpkgs:" + echo "1. Fork NixOS/nixpkgs" + echo "2. Add bunsenite.guix to pkgs/by-name/bu/bunsenite/package.guix" + echo "3. Create PR with title: bunsenite: init at ${VERSION}" + echo "" + echo "This requires manual review by nixpkgs maintainers." diff --git a/vendor/bunsenite/.github/workflows/publish-obs.yml b/vendor/bunsenite/.github/workflows/publish-obs.yml new file mode 100644 index 0000000..2bdacd9 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-obs.yml @@ -0,0 +1,185 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish OBS (openSUSE) + +on: + repository_dispatch: + types: [publish-obs] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + +permissions: + contents: read + +env: + TAP_REPO: hyperpolymath/homebrew-tap + OBS_PROJECT: home:hyperpolymath + OBS_PACKAGE: bunsenite + +jobs: + update-spec: + name: Update OBS Spec in Tap + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for TAP_GITHUB_TOKEN + id: check-secret + run: | + if [ -z "${{ secrets.TAP_GITHUB_TOKEN }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::TAP_GITHUB_TOKEN not configured. Skipping OBS spec update." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout homebrew-tap + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + with: + repository: ${{ env.TAP_REPO }} + token: ${{ secrets.TAP_GITHUB_TOKEN }} + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + fi + + - name: Update OBS spec + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + DATE=$(date "+%a %b %d %Y") + + mkdir -p obs + + # Create spec file for OBS (openSUSE Build Service) + cat > obs/bunsenite.spec << EOF + # SPDX-License-Identifier: MPL-2.0 + # + # spec file for package bunsenite + # + # Copyright (c) 2024-2025 hyperpolymath + # + + Name: bunsenite + Version: ${VERSION} + Release: 1%{?dist} + Summary: Nickel configuration file parser with multi-language FFI bindings + + License: MIT OR Palimpsest-0.8 + URL: https://github.com/hyperpolymath/bunsenite + Source0: https://github.com/hyperpolymath/bunsenite/archive/refs/tags/${TAG}.tar.gz#/bunsenite-%{version}.tar.gz + + BuildRequires: cargo + BuildRequires: rust >= 1.70 + + %description + Bunsenite is a Nickel configuration file parser with multi-language FFI bindings. + Features include parse, validate, watch mode, interactive REPL, and JSON Schema validation. + + %prep + %autosetup -n bunsenite-%{version} + + %build + cargo build --release --features full + + %install + install -D -m 755 target/release/bunsenite %{buildroot}%{_bindir}/bunsenite + + %files + %license LICENSE.txt + %doc README.adoc + %{_bindir}/bunsenite + + %changelog + * ${DATE} hyperpolymath - ${VERSION}-1 + - Update to version ${VERSION} + EOF + + # Create _service file for OBS source service + cat > obs/_service << EOF + + + https://github.com/hyperpolymath/bunsenite.git + git + ${TAG} + @PARENT_TAG@ + + + + gz + *.tar + + + + EOF + + echo "Generated OBS spec:" + cat obs/bunsenite.spec + + - name: Commit and push + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add obs/ + git diff --staged --quiet || git commit -m "obs: bunsenite ${VERSION}" + git push + + trigger-obs: + name: Trigger OBS Build + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: update-spec + steps: + - name: Check for OBS credentials + id: check-secret + run: | + if [ -z "${{ secrets.OBS_USERNAME }}" ] || [ -z "${{ secrets.OBS_PASSWORD }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::OBS credentials not configured. Skipping OBS trigger." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Install osc + if: steps.check-secret.outputs.skip != 'true' + run: | + sudo apt-get update + sudo apt-get install -y osc + + - name: Configure osc + if: steps.check-secret.outputs.skip != 'true' + run: | + mkdir -p ~/.config/osc + cat > ~/.config/osc/oscrc << EOF + [general] + apiurl = https://api.opensuse.org + + [https://api.opensuse.org] + user = ${{ secrets.OBS_USERNAME }} + pass = ${{ secrets.OBS_PASSWORD }} + EOF + + - name: Trigger rebuild + if: steps.check-secret.outputs.skip != 'true' + run: | + osc api -X POST "/trigger/runservice?project=${OBS_PROJECT}&package=${OBS_PACKAGE}" || true + echo "OBS build triggered for ${OBS_PROJECT}/${OBS_PACKAGE}" diff --git a/vendor/bunsenite/.github/workflows/publish-packages.yml b/vendor/bunsenite/.github/workflows/publish-packages.yml new file mode 100644 index 0000000..96bd208 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-packages.yml @@ -0,0 +1,268 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish Packages + +on: + workflow_run: + workflows: ["Release"] + types: [completed] + branches: [main] + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g., v1.0.2)' + required: true + +permissions: + contents: read + +jobs: + extract-release-info: + name: Extract Release Info + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + outputs: + version: ${{ steps.info.outputs.version }} + tag: ${{ steps.info.outputs.tag }} + sha_linux_x64: ${{ steps.info.outputs.sha_linux_x64 }} + sha_linux_arm64: ${{ steps.info.outputs.sha_linux_arm64 }} + sha_macos_x64: ${{ steps.info.outputs.sha_macos_x64 }} + sha_macos_arm64: ${{ steps.info.outputs.sha_macos_arm64 }} + sha_windows_x64: ${{ steps.info.outputs.sha_windows_x64 }} + steps: + - name: Determine tag + id: tag + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + else + # Get tag from the release workflow run + TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq '.tag_name') + echo "tag=$TAG" >> $GITHUB_OUTPUT + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract release info + id: info + run: | + TAG="${{ steps.tag.outputs.tag }}" + VERSION="${TAG#v}" + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Download SHA256SUMS.txt from release + gh release download "$TAG" --pattern "SHA256SUMS.txt" --repo ${{ github.repository }} + + # Parse hashes - format is: SHA256 ./bunsenite-{target}/bunsenite-{tag}-{target}.{ext} + echo "sha_linux_x64=$(grep 'x86_64-unknown-linux-gnu' SHA256SUMS.txt | awk '{print $1}')" >> $GITHUB_OUTPUT + echo "sha_linux_arm64=$(grep 'aarch64-unknown-linux-gnu' SHA256SUMS.txt | awk '{print $1}')" >> $GITHUB_OUTPUT + echo "sha_macos_x64=$(grep 'x86_64-apple-darwin' SHA256SUMS.txt | awk '{print $1}')" >> $GITHUB_OUTPUT + echo "sha_macos_arm64=$(grep 'aarch64-apple-darwin' SHA256SUMS.txt | awk '{print $1}')" >> $GITHUB_OUTPUT + echo "sha_windows_x64=$(grep 'x86_64-pc-windows-msvc' SHA256SUMS.txt | awk '{print $1}')" >> $GITHUB_OUTPUT + + # Debug output + echo "Extracted version: $VERSION" + echo "Extracted tag: $TAG" + cat SHA256SUMS.txt + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + dispatch-homebrew: + name: Dispatch Homebrew + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger Homebrew publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-homebrew + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}", + "sha_linux_x64": "${{ needs.extract-release-info.outputs.sha_linux_x64 }}", + "sha_linux_arm64": "${{ needs.extract-release-info.outputs.sha_linux_arm64 }}", + "sha_macos_x64": "${{ needs.extract-release-info.outputs.sha_macos_x64 }}", + "sha_macos_arm64": "${{ needs.extract-release-info.outputs.sha_macos_arm64 }}" + } + + dispatch-scoop: + name: Dispatch Scoop + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger Scoop publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-scoop + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}", + "sha_windows_x64": "${{ needs.extract-release-info.outputs.sha_windows_x64 }}" + } + + dispatch-aur: + name: Dispatch AUR + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger AUR publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-aur + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}", + "sha_linux_x64": "${{ needs.extract-release-info.outputs.sha_linux_x64 }}", + "sha_linux_arm64": "${{ needs.extract-release-info.outputs.sha_linux_arm64 }}" + } + + dispatch-winget: + name: Dispatch WinGet + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger WinGet publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-winget + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}", + "sha_windows_x64": "${{ needs.extract-release-info.outputs.sha_windows_x64 }}" + } + + dispatch-chocolatey: + name: Dispatch Chocolatey + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger Chocolatey publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-chocolatey + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}", + "sha_windows_x64": "${{ needs.extract-release-info.outputs.sha_windows_x64 }}" + } + + dispatch-flatpak: + name: Dispatch Flatpak + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger Flatpak publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-flatpak + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}" + } + + dispatch-macports: + name: Dispatch MacPorts + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger MacPorts publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-macports + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}" + } + + dispatch-debian-ppa: + name: Dispatch Debian PPA + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger Debian PPA publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-debian-ppa + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}" + } + + dispatch-copr: + name: Dispatch COPR + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger COPR publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-copr + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}" + } + + dispatch-obs: + name: Dispatch OBS (openSUSE) + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger OBS publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-obs + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}" + } + + dispatch-nixpkgs: + name: Dispatch Nixpkgs + needs: extract-release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Trigger Nixpkgs publisher + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + event-type: publish-nixpkgs + client-payload: >- + { + "version": "${{ needs.extract-release-info.outputs.version }}", + "tag": "${{ needs.extract-release-info.outputs.tag }}" + } diff --git a/vendor/bunsenite/.github/workflows/publish-scoop.yml b/vendor/bunsenite/.github/workflows/publish-scoop.yml new file mode 100644 index 0000000..f017781 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-scoop.yml @@ -0,0 +1,110 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish Scoop + +on: + repository_dispatch: + types: [publish-scoop] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + sha_windows_x64: + description: 'SHA256 for Windows x64' + required: true + +permissions: + contents: read + +env: + TAP_REPO: hyperpolymath/homebrew-tap + +jobs: + update-bucket: + name: Update Scoop Bucket + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for TAP_GITHUB_TOKEN + id: check-secret + run: | + if [ -z "${{ secrets.TAP_GITHUB_TOKEN }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::TAP_GITHUB_TOKEN not configured. Skipping Scoop publish." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout homebrew-tap + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + with: + repository: ${{ env.TAP_REPO }} + token: ${{ secrets.TAP_GITHUB_TOKEN }} + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + echo "sha_windows_x64=${{ github.event.client_payload.sha_windows_x64 }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + echo "sha_windows_x64=${{ inputs.sha_windows_x64 }}" >> $GITHUB_OUTPUT + fi + + - name: Update manifest + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + SHA="${{ steps.inputs.outputs.sha_windows_x64 }}" + + mkdir -p bucket + + cat > bucket/bunsenite.json << EOF + { + "version": "${VERSION}", + "description": "Nickel configuration file parser with multi-language FFI bindings", + "homepage": "https://github.com/hyperpolymath/bunsenite", + "license": "MIT|Palimpsest-0.8", + "architecture": { + "64bit": { + "url": "https://github.com/hyperpolymath/bunsenite/releases/download/${TAG}/bunsenite-${TAG}-x86_64-pc-windows-msvc.zip", + "hash": "${SHA}", + "bin": "bunsenite.exe" + } + }, + "checkver": { + "github": "https://github.com/hyperpolymath/bunsenite" + }, + "autoupdate": { + "architecture": { + "64bit": { + "url": "https://github.com/hyperpolymath/bunsenite/releases/download/v\$version/bunsenite-v\$version-x86_64-pc-windows-msvc.zip" + } + } + } + } + EOF + + echo "Generated bucket/bunsenite.json:" + cat bucket/bunsenite.json + + - name: Commit and push + if: steps.check-secret.outputs.skip != 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add bucket/bunsenite.json + git diff --staged --quiet || git commit -m "scoop: bunsenite ${{ steps.inputs.outputs.tag }}" + git push diff --git a/vendor/bunsenite/.github/workflows/publish-winget.yml b/vendor/bunsenite/.github/workflows/publish-winget.yml new file mode 100644 index 0000000..22cac22 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/publish-winget.yml @@ -0,0 +1,180 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Publish WinGet + +on: + repository_dispatch: + types: [publish-winget] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.2)' + required: true + tag: + description: 'Git tag (e.g., v1.0.2)' + required: true + sha_windows_x64: + description: 'SHA256 for Windows x64' + required: true + submit_official: + description: 'Submit to official winget-pkgs' + type: boolean + default: false + +permissions: + contents: read + +env: + TAP_REPO: hyperpolymath/homebrew-tap + +jobs: + update-tap: + name: Update WinGet in Tap + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for TAP_GITHUB_TOKEN + id: check-secret + run: | + if [ -z "${{ secrets.TAP_GITHUB_TOKEN }}" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::TAP_GITHUB_TOKEN not configured. Skipping WinGet tap update." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout homebrew-tap + if: steps.check-secret.outputs.skip != 'true' + uses: actions/checkout@v6.0.1 + with: + repository: ${{ env.TAP_REPO }} + token: ${{ secrets.TAP_GITHUB_TOKEN }} + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $GITHUB_OUTPUT + echo "sha_windows_x64=${{ github.event.client_payload.sha_windows_x64 }}" >> $GITHUB_OUTPUT + else + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + echo "sha_windows_x64=${{ inputs.sha_windows_x64 }}" >> $GITHUB_OUTPUT + fi + + - name: Update WinGet manifest + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + TAG="${{ steps.inputs.outputs.tag }}" + SHA="${{ steps.inputs.outputs.sha_windows_x64 }}" + + mkdir -p "winget/Hyperpolymath.Bunsenite/${VERSION}" + + cat > "winget/Hyperpolymath.Bunsenite/${VERSION}/Hyperpolymath.Bunsenite.yaml" << EOF + # yaml-language-server: \$schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json + # SPDX-License-Identifier: MPL-2.0 + PackageIdentifier: Hyperpolymath.Bunsenite + PackageVersion: ${VERSION} + PackageLocale: en-US + Publisher: hyperpolymath + PublisherUrl: https://github.com/hyperpolymath + PublisherSupportUrl: https://github.com/hyperpolymath/bunsenite/issues + PackageName: Bunsenite + PackageUrl: https://github.com/hyperpolymath/bunsenite + License: MIT OR Palimpsest-0.8 + LicenseUrl: https://github.com/hyperpolymath/bunsenite/blob/main/LICENSE.txt + ShortDescription: Nickel configuration file parser with multi-language FFI bindings + Description: | + Bunsenite is a Nickel configuration file parser with multi-language FFI bindings. + Features include parse, validate, watch mode, interactive REPL, and JSON Schema validation. + Tags: + - nickel + - config + - configuration + - parser + - rust + - cli + Moniker: bunsenite + Commands: + - bunsenite + ReleaseNotesUrl: https://github.com/hyperpolymath/bunsenite/releases/tag/${TAG} + Installers: + - Architecture: x64 + InstallerType: zip + InstallerUrl: https://github.com/hyperpolymath/bunsenite/releases/download/${TAG}/bunsenite-${TAG}-x86_64-pc-windows-msvc.zip + InstallerSha256: ${SHA} + NestedInstallerType: portable + NestedInstallerFiles: + - RelativeFilePath: bunsenite.exe + PortableCommandAlias: bunsenite + ManifestType: singleton + ManifestVersion: 1.6.0 + EOF + + echo "Generated WinGet manifest:" + cat "winget/Hyperpolymath.Bunsenite/${VERSION}/Hyperpolymath.Bunsenite.yaml" + + - name: Commit and push + if: steps.check-secret.outputs.skip != 'true' + run: | + VERSION="${{ steps.inputs.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add "winget/Hyperpolymath.Bunsenite/${VERSION}/" + git diff --staged --quiet || git commit -m "winget: bunsenite ${VERSION}" + git push + + submit-official: + name: Submit to Official WinGet + runs-on: windows-latest + timeout-minutes: 15 + if: ${{ inputs.submit_official == true }} + steps: + - name: Check for WINGET_GITHUB_TOKEN + id: check-secret + shell: pwsh + run: | + if ([string]::IsNullOrEmpty("${{ secrets.WINGET_GITHUB_TOKEN }}")) { + echo "skip=true" >> $env:GITHUB_OUTPUT + Write-Warning "WINGET_GITHUB_TOKEN not configured. Skipping official WinGet submission." + } else { + echo "skip=false" >> $env:GITHUB_OUTPUT + } + + - name: Get inputs + if: steps.check-secret.outputs.skip != 'true' + id: inputs + shell: pwsh + run: | + if ("${{ github.event_name }}" -eq "repository_dispatch") { + echo "version=${{ github.event.client_payload.version }}" >> $env:GITHUB_OUTPUT + echo "tag=${{ github.event.client_payload.tag }}" >> $env:GITHUB_OUTPUT + } else { + echo "version=${{ inputs.version }}" >> $env:GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $env:GITHUB_OUTPUT + } + + - name: Install wingetcreate + if: steps.check-secret.outputs.skip != 'true' + shell: pwsh + run: | + Invoke-WebRequest -Uri https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe + + - name: Submit to winget-pkgs + if: steps.check-secret.outputs.skip != 'true' + shell: pwsh + run: | + $VERSION = "${{ steps.inputs.outputs.version }}" + $TAG = "${{ steps.inputs.outputs.tag }}" + $URL = "https://github.com/hyperpolymath/bunsenite/releases/download/${TAG}/bunsenite-${TAG}-x86_64-pc-windows-msvc.zip" + + ./wingetcreate.exe update Hyperpolymath.Bunsenite ` + --urls $URL ` + --version $VERSION ` + --token ${{ secrets.WINGET_GITHUB_TOKEN }} ` + --submit diff --git a/vendor/bunsenite/.github/workflows/push-email-notify.yml b/vendor/bunsenite/.github/workflows/push-email-notify.yml new file mode 100644 index 0000000..587979f --- /dev/null +++ b/vendor/bunsenite/.github/workflows/push-email-notify.yml @@ -0,0 +1,36 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +# Dormant push-email notification. ARMED by setting the repo variable +# PUSH_EMAIL_ENABLED=true (the single on/off switch). Addresses are pre-filled; +# sending needs the org SMTP secrets (SMTP_HOST/PORT/USER/PASS). Inherited by +# new repos from the template; placed on existing repos by the farm sweep. +name: Push email notification +on: + push: {} +permissions: + contents: read +jobs: + notify: + name: Email on push + if: ${{ vars.PUSH_EMAIL_ENABLED == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Send push notification email + uses: dawidd6/action-send-mail@v3.12.0 + with: + server_address: ${{ secrets.SMTP_HOST }} + server_port: ${{ secrets.SMTP_PORT }} + secure: true + username: ${{ secrets.SMTP_USER }} + password: ${{ secrets.SMTP_PASS }} + from: "GitHub Push <${{ secrets.SMTP_USER }}>" + to: "jonathan.jewell@gmail.com j.d.a.jewell@open.ac.uk" + subject: "[${{ github.repository }}] push to ${{ github.ref_name }} by ${{ github.actor }}" + body: | + Repository: ${{ github.repository }} + Branch: ${{ github.ref_name }} + Pusher: ${{ github.actor }} + Compare: ${{ github.event.compare }} + Head msg: ${{ github.event.head_commit.message }} diff --git a/vendor/bunsenite/.github/workflows/release.yml b/vendor/bunsenite/.github/workflows/release.yml new file mode 100644 index 0000000..98e3dea --- /dev/null +++ b/vendor/bunsenite/.github/workflows/release.yml @@ -0,0 +1,256 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., 1.0.0)' + required: true + +env: + CARGO_TERM_COLOR: always + + +permissions: + contents: read + +jobs: + build: + name: Build ${{ matrix.target }} + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + # Linux x86_64 + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + artifact: bunsenite + archive: tar.gz + # Linux aarch64 + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + artifact: bunsenite + archive: tar.gz + cross: true + # macOS x86_64 (use macos-15-intel for Intel-based runner) + - target: x86_64-apple-darwin + os: macos-15-intel + artifact: bunsenite + archive: tar.gz + # macOS aarch64 (Apple Silicon) + - target: aarch64-apple-darwin + os: macos-latest + artifact: bunsenite + archive: tar.gz + # Windows x86_64 + - target: x86_64-pc-windows-msvc + os: windows-latest + artifact: bunsenite.exe + archive: zip + + steps: + - uses: actions/checkout@v6.0.1 + + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + with: + targets: ${{ matrix.target }} + + - name: Install cross (for cross-compilation) + if: matrix.cross + run: cargo install cross --git https://github.com/cross-rs/cross + + - name: Install Zig + uses: goto-bus-stop/setup-zig@v2.2.1 + with: + version: 0.11.0 + + - name: Build Rust (native) + if: ${{ !matrix.cross }} + run: cargo build --release --features full --target ${{ matrix.target }} + + - name: Build Rust (cross) + if: matrix.cross + run: cross build --release --features full --target ${{ matrix.target }} + + - name: Prepare Rust library for Zig (Unix) + if: runner.os != 'Windows' + run: | + mkdir -p target/release + cp target/${{ matrix.target }}/release/libbunsenite.* target/release/ || true + + - name: Prepare Rust library for Zig (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path target/release + Copy-Item target/${{ matrix.target }}/release/bunsenite.* target/release/ -ErrorAction SilentlyContinue + + - name: Build Zig FFI (Unix) + if: runner.os != 'Windows' && !matrix.cross + run: cd zig && zig build -Doptimize=ReleaseFast + + # Skip Zig FFI on Windows - requires import library setup + # - name: Build Zig FFI (Windows) + # if: runner.os == 'Windows' + # run: cd zig && zig build -Doptimize=ReleaseFast + + - name: Prepare archive (Unix) + if: runner.os != 'Windows' + run: | + mkdir -p dist + cp target/${{ matrix.target }}/release/${{ matrix.artifact }} dist/ + cp zig/zig-out/lib/libbunsenite.* dist/ || true + cp README.adoc LICENSE.txt dist/ + cd dist && tar -czvf ../bunsenite-${{ github.ref_name }}-${{ matrix.target }}.${{ matrix.archive }} * + + - name: Prepare archive (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path dist + Copy-Item target/${{ matrix.target }}/release/${{ matrix.artifact }} dist/ + Copy-Item zig/zig-out/lib/bunsenite.* dist/ -ErrorAction SilentlyContinue + Copy-Item README.adoc,LICENSE.txt dist/ + Compress-Archive -Path dist/* -DestinationPath bunsenite-${{ github.ref_name }}-${{ matrix.target }}.${{ matrix.archive }} + + - name: Upload artifact + uses: actions/upload-artifact@v4.6.2 + with: + name: bunsenite-${{ matrix.target }} + path: bunsenite-*.${{ matrix.archive }} + + release: + name: Create Release + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + id-token: write + attestations: write + + steps: + - uses: actions/checkout@v6.0.1 + + - name: Download all artifacts + uses: actions/download-artifact@v4.1.8 + with: + path: artifacts + + - name: List artifacts + run: find artifacts -type f + + - name: Create checksums + run: | + cd artifacts + find . -name "bunsenite-*" -type f -exec sha256sum {} \; > ../SHA256SUMS.txt + cat ../SHA256SUMS.txt + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2.2.1 + with: + files: | + artifacts/**/* + SHA256SUMS.txt + draft: false + prerelease: false + generate_release_notes: true + body: | + ## Bunsenite ${{ github.ref_name }} + + Nickel configuration file parser with multi-language FFI bindings. + + ### Installation + + **Cargo (Rust):** + ```bash + cargo install bunsenite + ``` + + **Homebrew (macOS):** + ```bash + brew install bunsenite + ``` + + **Download binaries:** + See assets below for pre-built binaries. + + ### Features + - Parse and evaluate Nickel configuration files + - Watch mode for live reloading + - Interactive REPL + - JSON Schema validation + - FFI bindings for Deno, AffineScript, Node.js + + RSR Compliance: Bronze Tier | TPCF Perimeter: 3 + + - name: Attest build provenance + uses: actions/attest-build-provenance@v2.4.0 + with: + subject-path: | + artifacts/**/* + SHA256SUMS.txt + + publish-crates: + name: Publish to crates.io + needs: release + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + id-token: write + attestations: write + steps: + - uses: actions/checkout@v6.0.1 + + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + + - name: Package crate + run: cargo package + + - name: Attest crate provenance + uses: actions/attest-build-provenance@v2.4.0 + with: + subject-path: 'target/package/*.crate' + + - name: Publish to crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish --no-verify + continue-on-error: true + + publish-npm: + name: Publish to npm + needs: release + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - uses: actions/checkout@v6.0.1 + + - name: Setup Node.js + uses: actions/setup-node@v4.0.2 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Publish to npm + working-directory: bindings/affinescript + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public + continue-on-error: true diff --git a/vendor/bunsenite/.github/workflows/rust-ci.yml b/vendor/bunsenite/.github/workflows/rust-ci.yml new file mode 100644 index 0000000..a7174f5 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/rust-ci.yml @@ -0,0 +1,24 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +# Rust CI — thin wrapper calling the shared estate reusable in +# hyperpolymath/standards. Configure once, propagate everywhere. +# See: docs/CI-REUSABLE-WORKFLOWS.adoc in standards. +name: Rust CI + +on: + push: + branches: [main, master] + pull_request: + +permissions: + actions: read + contents: read + +jobs: + rust-ci: + uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a + with: + enable_audit: true + enable_coverage: true diff --git a/vendor/bunsenite/.github/workflows/scorecard.yml b/vendor/bunsenite/.github/workflows/scorecard.yml new file mode 100644 index 0000000..d78f4b1 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/scorecard.yml @@ -0,0 +1,22 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: OSSF Scorecard + +on: + schedule: + - cron: '0 4 * * *' + workflow_dispatch: + +permissions: + actions: read + contents: read + +jobs: + scorecard: + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 + permissions: + contents: read + security-events: write + id-token: write diff --git a/vendor/bunsenite/.github/workflows/secret-scanner.yml b/vendor/bunsenite/.github/workflows/secret-scanner.yml new file mode 100644 index 0000000..fffde78 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/secret-scanner.yml @@ -0,0 +1,25 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Secret Scanner + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + actions: read + contents: read + +jobs: + scan: + permissions: + contents: read + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a + secrets: inherit diff --git a/vendor/bunsenite/.github/workflows/stress-test.yml b/vendor/bunsenite/.github/workflows/stress-test.yml new file mode 100644 index 0000000..3113626 --- /dev/null +++ b/vendor/bunsenite/.github/workflows/stress-test.yml @@ -0,0 +1,56 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Stress Testing +on: + schedule: + - cron: '0 3 * * 1' # Weekly Monday 3am UTC + workflow_dispatch: +permissions: + contents: read +jobs: + stress-test: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6.0.1 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + + - name: Install stress testing tools + run: | + sudo apt-get update + sudo apt-get install -y stress-ng valgrind + + - name: Build release + run: cargo build --release --all-features + + - name: Concurrent operations stress test + run: | + # Run binary with high concurrency + for i in {1..100}; do + timeout 1s ./target/release/* & + done + wait + + - name: Memory pressure test + run: | + # Run under memory constraints + ulimit -v 512000 # 500MB virtual memory limit + cargo test --release + + - name: Long-running scenario test + run: | + # Test for memory leaks over time + timeout 300s valgrind --leak-check=full --error-exitcode=1 \ + ./target/release/* || true + + - name: Stress test with stress-ng + run: | + # CPU and I/O stress + stress-ng --cpu 4 --io 2 --timeout 60s & + STRESS_PID=$! + cargo test --release + kill $STRESS_PID || true diff --git a/vendor/bunsenite/.github/workflows/workflow-linter.yml b/vendor/bunsenite/.github/workflows/workflow-linter.yml new file mode 100644 index 0000000..c31ac4d --- /dev/null +++ b/vendor/bunsenite/.github/workflows/workflow-linter.yml @@ -0,0 +1,58 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +# Prevention workflow - validates all workflows have proper security config +name: Workflow Security Linter + +on: + pull_request: + paths: + - '.github/workflows/**' + push: + paths: + - '.github/workflows/**' + +permissions: read-all + +jobs: + lint-workflows: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4.1.1 + + - name: Check SPDX headers + run: | + errors=0 + for f in .github/workflows/*.yml .github/workflows/*.yaml; do + [ -f "$f" ] || continue + if ! head -1 "$f" | grep -q "SPDX-License-Identifier"; then + echo "ERROR: $f missing SPDX header" + errors=$((errors + 1)) + fi + done + exit $errors + + - name: Check permissions declaration + run: | + errors=0 + for f in .github/workflows/*.yml .github/workflows/*.yaml; do + [ -f "$f" ] || continue + if ! grep -q "^permissions:" "$f"; then + echo "ERROR: $f missing permissions declaration" + errors=$((errors + 1)) + fi + done + exit $errors + + - name: Check pinned actions + run: | + errors=0 + for f in .github/workflows/*.yml .github/workflows/*.yaml; do + [ -f "$f" ] || continue + # Look for uses: without SHA + if grep -E "uses:.*@v[0-9]" "$f" | grep -v "#"; then + echo "WARNING: $f has unpinned actions (missing SHA comment)" + fi + done diff --git a/vendor/bunsenite/.github/workflows/zig-ffi.yml b/vendor/bunsenite/.github/workflows/zig-ffi.yml new file mode 100644 index 0000000..b09e96b --- /dev/null +++ b/vendor/bunsenite/.github/workflows/zig-ffi.yml @@ -0,0 +1,149 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# This workflow is managed by gh actions-lock. +name: Zig FFI Build + +on: + push: + branches: [main, master] + paths: + - 'zig/**' + - 'src/ffi.rs' + - 'Cargo.toml' + pull_request: + paths: + - 'zig/**' + - 'src/ffi.rs' + - 'Cargo.toml' + +env: + CARGO_TERM_COLOR: always + + +permissions: + contents: read + +jobs: + build-ffi: + name: Build FFI (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + permissions: + contents: read + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: ubuntu-latest + lib_ext: so + - os: macos-latest + lib_ext: dylib + - os: windows-latest + lib_ext: dll + + steps: + - uses: actions/checkout@v6.0.1 + + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + with: + components: rustfmt, clippy + + - name: Install Zig + uses: goto-bus-stop/setup-zig@v2.2.1 + with: + version: 0.11.0 + + - name: Cache Cargo + uses: actions/cache@v4.3.0 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build Rust library + run: cargo build --release + + - name: Build Zig FFI layer + working-directory: zig + run: zig build -Doptimize=ReleaseFast + + - name: Verify FFI exports (Linux) + if: runner.os == 'Linux' + run: | + echo "=== Checking exported symbols ===" + nm -D zig/zig-out/lib/libbunsenite.so | grep -E "parse_nickel|validate_nickel|free_string|version|rsr_tier|tpcf_perimeter" || true + + - name: Upload FFI library + uses: actions/upload-artifact@v4.6.2 + with: + name: libbunsenite-${{ matrix.os }} + path: | + zig/zig-out/lib/libbunsenite.${{ matrix.lib_ext }} + target/release/libbunsenite.${{ matrix.lib_ext }} + + test-ffi: + name: Test FFI + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: build-ffi + permissions: + contents: read + steps: + - uses: actions/checkout@v6.0.1 + + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + + - name: Install Zig + uses: goto-bus-stop/setup-zig@v2.2.1 + with: + version: 0.11.0 + + - name: Build Rust library + run: cargo build --release + + - name: Run Rust FFI tests + run: cargo test ffi --release + + - name: Run Zig tests + working-directory: zig + run: zig build test + + test-deno-bindings: + name: Test Deno Bindings + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: build-ffi + permissions: + contents: read + steps: + - uses: actions/checkout@v6.0.1 + + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + + - name: Install Zig + uses: goto-bus-stop/setup-zig@v2.2.1 + with: + version: 0.11.0 + + - name: Install Deno + uses: denoland/setup-deno@v1.5.2 + with: + deno-version: v1.x + + - name: Build FFI libraries + run: | + cargo build --release + cd zig && zig build -Doptimize=ReleaseFast + + - name: Copy library for Deno + run: cp zig/zig-out/lib/libbunsenite.so bindings/deno/ + + - name: Test Deno bindings + working-directory: bindings/deno + run: deno run --allow-ffi --allow-read example.ts || echo "Deno test completed" diff --git a/vendor/bunsenite/.gitignore b/vendor/bunsenite/.gitignore new file mode 100644 index 0000000..73f3573 --- /dev/null +++ b/vendor/bunsenite/.gitignore @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: MPL-2.0 +# RSR-compliant .gitignore + +# OS & Editor +.DS_Store +Thumbs.db +*.swp +*.swo +*~ +.idea/ +.vscode/ + +# Build +/target/ +/_build/ +/build/ +/dist/ +/out/ + +# Dependencies +/node_modules/ +/vendor/ +/deps/ +/.elixir_ls/ + +# Rust +# Cargo.lock # Keep for binaries + +# Elixir +/cover/ +/doc/ +*.ez +erl_crash.dump + +# Julia +*.jl.cov +*.jl.mem +/Manifest.toml + +# ReScript +/lib/bs/ +/.bsb.lock + +# Python (SaltStack only) +__pycache__/ +*.py[cod] +.venv/ + +# Ada/SPARK +*.ali +/obj/ +/bin/ + +# Haskell +/.stack-work/ +/dist-newstyle/ + +# Chapel +*.chpl.tmp.* + +# Secrets +.env +.env.* +*.pem +*.key +secrets/ + +# Test/Coverage +/coverage/ +htmlcov/ + +# Logs +*.log +/logs/ + +# Temp +/tmp/ +*.tmp +*.bak + +# Crash recovery artifacts +ai-cli-crash-capture/ +target/ +node_modules/ +_build/ +deps/ +.elixir_ls/ +.cache/ +build/ +dist/ diff --git a/vendor/bunsenite/.gitlab-ci.yml b/vendor/bunsenite/.gitlab-ci.yml new file mode 100644 index 0000000..f2c4f3b --- /dev/null +++ b/vendor/bunsenite/.gitlab-ci.yml @@ -0,0 +1,325 @@ +# Bunsenite GitLab CI/CD Pipeline +# Automated testing, building, and deployment + +# Stages define the order of execution +stages: + - check # Code quality checks + - test # Run tests + - build # Build artifacts + - security # Security scanning + - deploy # Deployment (crates.io, releases) + +# Global variables +variables: + CARGO_HOME: $CI_PROJECT_DIR/.cargo + RUST_BACKTRACE: "1" + +# Cache dependencies between jobs +cache: + paths: + - .cargo/ + - target/ + +# === Check Stage === + +# Format check +fmt: + stage: check + image: rust:latest + script: + - rustup component add rustfmt + - cargo fmt --all -- --check + allow_failure: false + +# Clippy linter +clippy: + stage: check + image: rust:latest + script: + - rustup component add clippy + - cargo clippy --all-targets --all-features -- -D warnings + allow_failure: false + +# Check for unsafe code +unsafe-check: + stage: check + image: rust:latest + script: + - | + if grep -r "unsafe" src/; then + echo "ERROR: Found unsafe code! Bunsenite must have zero unsafe blocks." + exit 1 + fi + echo "✓ No unsafe code found" + allow_failure: false + +# Verify RSR Bronze compliance +rsr-compliance: + stage: check + image: rust:latest + before_script: + - apt-get update && apt-get install -y jq + script: + - | + echo "Checking RSR Bronze Tier compliance..." + + # Check for required files + for file in README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md MAINTAINERS.md CHANGELOG.md; do + if [ ! -f "$file" ]; then + echo "ERROR: Missing required file: $file" + exit 1 + fi + done + + # Check .well-known/ directory + for file in .well-known/security.txt .well-known/ai.txt .well-known/humans.txt; do + if [ ! -f "$file" ]; then + echo "ERROR: Missing required file: $file" + exit 1 + fi + done + + # Check for network dependencies + if grep -E "reqwest|hyper|curl" Cargo.toml; then + echo "ERROR: Found network dependencies (violates offline-first)" + exit 1 + fi + + echo "✓ RSR Bronze Tier compliance verified" + allow_failure: false + +# === Test Stage === + +# Run tests on stable Rust +test:stable: + stage: test + image: rust:latest + script: + - cargo test --all-features --verbose + coverage: '/^\d+\.\d+% coverage/' + artifacts: + reports: + junit: target/junit.xml + +# Run tests on nightly Rust (informational only) +test:nightly: + stage: test + image: rustlang/rust:nightly + script: + - cargo test --all-features --verbose + allow_failure: true + +# Run tests with minimum supported Rust version (MSRV) +test:msrv: + stage: test + image: rust:1.70 # Match rust-version in Cargo.toml + script: + - cargo test --all-features --verbose + allow_failure: false + +# Test documentation examples +test:doc: + stage: test + image: rust:latest + script: + - cargo test --doc --verbose + allow_failure: false + +# Code coverage (using tarpaulin) +coverage: + stage: test + image: rust:latest + before_script: + - cargo install cargo-tarpaulin || true + script: + - cargo tarpaulin --out Xml --output-dir target/coverage + coverage: '/^\d+\.\d+% coverage/' + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: target/coverage/cobertura.xml + allow_failure: true # Coverage is informational + +# === Build Stage === + +# Build release binaries (Linux) +build:linux: + stage: build + image: rust:latest + script: + - cargo build --release --verbose + - strip target/release/bunsenite || true + - ls -lh target/release/bunsenite + - ls -lh target/release/libbunsenite.so + artifacts: + name: "bunsenite-$CI_COMMIT_REF_NAME-linux" + paths: + - target/release/bunsenite + - target/release/libbunsenite.so + expire_in: 1 week + +# Build WASM module +build:wasm: + stage: build + image: rust:latest + before_script: + - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + script: + - wasm-pack build --target web --out-dir pkg --release + - ls -lh pkg/ + artifacts: + name: "bunsenite-wasm-$CI_COMMIT_REF_NAME" + paths: + - pkg/ + expire_in: 1 week + allow_failure: true # WASM is optional + +# Build documentation +build:docs: + stage: build + image: rust:latest + script: + - cargo doc --all-features --no-deps + - echo '' > target/doc/index.html + artifacts: + name: "bunsenite-docs-$CI_COMMIT_REF_NAME" + paths: + - target/doc/ + expire_in: 1 week + +# === Security Stage === + +# Dependency audit (check for vulnerabilities) +audit: + stage: security + image: rust:latest + before_script: + - cargo install cargo-audit || true + script: + - cargo audit --deny warnings + allow_failure: false + +# License and dependency check +deny: + stage: security + image: rust:latest + before_script: + - cargo install cargo-deny || true + script: + - cargo deny check + allow_failure: true # Informational for now + +# SAST (Static Application Security Testing) +sast: + stage: security + image: rust:latest + script: + - rustup component add clippy + - cargo clippy --all-targets --all-features -- -D warnings + allow_failure: false + +# === Deploy Stage === + +# Publish to crates.io (only on tags) +publish:crates: + stage: deploy + image: rust:latest + only: + - tags + except: + - branches + script: + - | + if [ -z "$CARGO_REGISTRY_TOKEN" ]; then + echo "ERROR: CARGO_REGISTRY_TOKEN not set" + exit 1 + fi + cargo publish --token $CARGO_REGISTRY_TOKEN + when: manual # Require manual trigger + +# Create GitLab release (only on tags) +release:gitlab: + stage: deploy + image: registry.gitlab.com/gitlab-org/release-cli:latest + only: + - tags + except: + - branches + script: + - echo "Creating GitLab release for $CI_COMMIT_TAG" + release: + tag_name: '$CI_COMMIT_TAG' + description: 'Release $CI_COMMIT_TAG' + assets: + links: + - name: 'Linux Binary' + url: '$CI_PROJECT_URL/-/jobs/artifacts/$CI_COMMIT_TAG/download?job=build:linux' + - name: 'WASM Module' + url: '$CI_PROJECT_URL/-/jobs/artifacts/$CI_COMMIT_TAG/download?job=build:wasm' + - name: 'Documentation' + url: '$CI_PROJECT_URL/-/jobs/artifacts/$CI_COMMIT_TAG/download?job=build:docs' + when: manual # Require manual trigger + +# === Special Jobs === + +# Nightly build (scheduled) +nightly: + stage: build + image: rustlang/rust:nightly + only: + - schedules + script: + - cargo build --release + - cargo test --all-features + allow_failure: true + +# Performance benchmarks (nightly only) +benchmarks: + stage: test + image: rustlang/rust:nightly + only: + - schedules + script: + - cargo bench + allow_failure: true + artifacts: + paths: + - target/criterion/ + expire_in: 1 month + +# === Branch-specific Rules === + +# Main branch: Run all checks +.main_rules: + only: + - main + except: + - schedules + +# Merge requests: Run checks and tests +.mr_rules: + only: + - merge_requests + except: + - schedules + +# Tags: Run everything including deployment +.tag_rules: + only: + - tags + except: + - branches + - schedules + +# === Job Configuration Templates === + +.rust_job: + before_script: + - rustc --version + - cargo --version + retry: + max: 2 + when: + - runner_system_failure + - stuck_or_timeout_failure diff --git a/vendor/bunsenite/.guix-channel b/vendor/bunsenite/.guix-channel new file mode 100644 index 0000000..3ec91e3 --- /dev/null +++ b/vendor/bunsenite/.guix-channel @@ -0,0 +1,7 @@ +;; bunsenite - Guix Channel +;; Add to ~/.config/guix/channels.scm + +(channel + (version 0) + (url "https://github.com/hyperpolymath/bunsenite") + (branch "main")) diff --git a/vendor/bunsenite/.hypatia-ignore b/vendor/bunsenite/.hypatia-ignore new file mode 100644 index 0000000..39b6808 --- /dev/null +++ b/vendor/bunsenite/.hypatia-ignore @@ -0,0 +1,27 @@ +# Banned-language exemption ledger — hypatia / governance-reusable.yml +# +# Format: /: +# +# WHY THIS FILE EXISTS +# -------------------- +# The governance "Language / package anti-pattern policy" gate is correct: +# these files really are in languages estate policy bans. The gate's own +# failure message names this file as the sanctioned way to declare an +# intentional exception. +# +# These exemptions HOLD THE LINE WHILE MIGRATION IS IN PROGRESS. Each entry +# is removed as the matching file is ported or deleted. This follows the +# precedent set in hyperpolymath/echidna. +# +# EVERY PATH IS LISTED INDIVIDUALLY — deliberately. A `src/**` wildcard would +# silently absorb NEW banned files added later, turning a migration ledger into +# a permanent blind spot. Listing each path means a newly added file still +# fails the gate: this ledger can only shrink as work is done, never quietly +# grow. +# +# Inventory taken 2026-08-06 across all 424 estate repositories. +# Files covered: 3 rescript + +cicd_rules/banned_language_file:bindings/rescript/Bunsenite.res +cicd_rules/banned_language_file:bindings/rescript/Bunsenite_test.res +cicd_rules/banned_language_file:bindings/rescript/Example.res diff --git a/vendor/bunsenite/.hypatia/activity.jsonl b/vendor/bunsenite/.hypatia/activity.jsonl new file mode 100644 index 0000000..17a314c --- /dev/null +++ b/vendor/bunsenite/.hypatia/activity.jsonl @@ -0,0 +1 @@ +{"timestamp":"2026-03-08T02:01:02Z","bot":"hypatia-autofix","action":"scan","details":"fixes=0"} diff --git a/vendor/bunsenite/.hypatia/last-visit.json b/vendor/bunsenite/.hypatia/last-visit.json new file mode 100644 index 0000000..6d8f19e --- /dev/null +++ b/vendor/bunsenite/.hypatia/last-visit.json @@ -0,0 +1,6 @@ +{ + "last_visit": "2026-03-08T02:01:02Z", + "last_bot": "hypatia-autofix", + "last_action": "scan", + "visits_total": 1 +} diff --git a/vendor/bunsenite/.machine_readable/6a2/0-AI-MANIFEST.a2ml b/vendor/bunsenite/.machine_readable/6a2/0-AI-MANIFEST.a2ml new file mode 100644 index 0000000..6bf1f8c --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/0-AI-MANIFEST.a2ml @@ -0,0 +1,31 @@ +# AI Manifest for 6a2 Directory + +## Purpose + +This manifest declares the AI-assistant context for the 6a2 machine-readable metadata directory. + +## Canonical Locations + +The 6 core A2ML files MUST exist in this directory: +1. AGENTIC.a2ml +2. ECOSYSTEM.a2ml +3. META.a2ml +4. NEUROSYM.a2ml +5. PLAYBOOK.a2ml +6. STATE.a2ml + +## Invariants + +- No duplicate files in root directory +- Single source of truth: this directory is authoritative +- No stale metadata + +## Protocol + +When multiple agents may write to A2ML files concurrently: +1. Read file and record git-sha-at-read in [provenance] section +2. Lock by creating .lock- +3. Write updated file with new [provenance] metadata +4. Release by removing lock file +5. On conflict: re-read and retry if git-sha-at-read does not match HEAD + diff --git a/vendor/bunsenite/.machine_readable/6a2/AGENTIC.a2ml b/vendor/bunsenite/.machine_readable/6a2/AGENTIC.a2ml new file mode 100644 index 0000000..3b12aab --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/AGENTIC.a2ml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# AGENTIC.a2ml — AI agent constraints and capabilities +# Defines what AI agents can and cannot do in this repository. + +[metadata] +version = "0.1.0" +last-updated = "2026-03-16" + +[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 relicense an existing file, and never run an automated licence +# sweep (LICENCE-POLICY.adoc A2). New files get correct SPDX from birth. +# - Never assume a licence. Read standards/LICENCE-POLICY.adoc: Rule 1 +# defaults to MPL-2.0 (code) / CC-BY-SA-4.0 (prose), but Rule 3 +# (co-developed), Rule 4 (network-deployed services) and Rule 5 +# (games) are AGPL-3.0-or-later, and Rule 2 names the PMPL register. diff --git a/vendor/bunsenite/.machine_readable/6a2/ECOSYSTEM.a2ml b/vendor/bunsenite/.machine_readable/6a2/ECOSYSTEM.a2ml new file mode 100644 index 0000000..dd8634d --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/ECOSYSTEM.a2ml @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: MPL-2.0 +# ECOSYSTEM.a2ml — Ecosystem position +# Converted from ECOSYSTEM.scm on 2026-03-15 + +[metadata] +project = "bunsenite" +ecosystem = "hyperpolymath" + +[position] +type = "component" diff --git a/vendor/bunsenite/.machine_readable/6a2/META.a2ml b/vendor/bunsenite/.machine_readable/6a2/META.a2ml new file mode 100644 index 0000000..2eca025 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/META.a2ml @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: MPL-2.0 +# META.a2ml — Project meta-information +# Converted from META.scm on 2026-03-15 + +[metadata] +project = "bunsenite" +author = "Jonathan D.A. Jewell " +license = "MPL-2.0" +standard = "RSR 2026" diff --git a/vendor/bunsenite/.machine_readable/6a2/NEUROSYM.a2ml b/vendor/bunsenite/.machine_readable/6a2/NEUROSYM.a2ml new file mode 100644 index 0000000..767d7dd --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/NEUROSYM.a2ml @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# NEUROSYM.a2ml — Neurosymbolic integration metadata +# Configuration for Hypatia scanning and symbolic reasoning. + +[metadata] +version = "0.1.0" +last-updated = "2026-03-16" + +[hypatia-config] +scan-enabled = true +scan-depth = "standard" +report-format = "logtalk" diff --git a/vendor/bunsenite/.machine_readable/6a2/PLAYBOOK.a2ml b/vendor/bunsenite/.machine_readable/6a2/PLAYBOOK.a2ml new file mode 100644 index 0000000..a961250 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/PLAYBOOK.a2ml @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# PLAYBOOK.a2ml — Operational playbook +# Runbooks, incident response, deployment procedures. + +[metadata] +version = "0.1.0" +last-updated = "2026-03-16" + +[deployment] +# method = "gitops" +# target = "container" + +[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 + +[release-process] +# 1. Update version in STATE.a2ml, META.a2ml +# 2. Run just quality (format, lint, test) +# 3. Tag and push diff --git a/vendor/bunsenite/.machine_readable/6a2/README.adoc b/vendor/bunsenite/.machine_readable/6a2/README.adoc new file mode 100644 index 0000000..916a702 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/README.adoc @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell +# A2ML 6a2 Directory + +This directory contains the 6 core A2ML machine-readable metadata files for this repository. + +## Files + +- `AGENTIC.a2ml` - AI agent operational gating, safety controls +- `ECOSYSTEM.a2ml` - Project ecosystem position, relationships, explicit boundaries +- `META.a2ml` - Architecture decisions (ADRs), development practices, design rationale +- `NEUROSYM.a2ml` - Symbolic semantics, composition algebra +- `PLAYBOOK.a2ml` - Executable plans, operational runbooks +- `STATE.a2ml` - Project state, phase, milestones, session history + +## Standards Compliance + +These files follow the A2ML Format Family specification from: +https://github.com/hyperpolymath/standards/tree/main/a2ml + +## Generation + +These files may be generated from .scm source files using transpilation tools. +Source .scm files should be removed after successful transpilation. + +## See Also + +- [A2ML Repository Template](https://github.com/hyperpolymath/standards/blob/main/A2ML-REPO-TEMPLATE.adoc) +- [6A2 Format Family](https://github.com/hyperpolymath/standards#a2ml-format-family-7-formats) + diff --git a/vendor/bunsenite/.machine_readable/6a2/STATE.a2ml b/vendor/bunsenite/.machine_readable/6a2/STATE.a2ml new file mode 100644 index 0000000..ff3aa60 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/STATE.a2ml @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: MPL-2.0 +# STATE.a2ml — Project state checkpoint +# Converted from STATE.scm on 2026-03-15 + +[metadata] +project = "bunsenite" +version = "0.1.0" +last-updated = "2026-03-15" +status = "active" + +[project-context] +name = "bunsenite" +completion-percentage = 0 +phase = "In development" diff --git a/vendor/bunsenite/.machine_readable/6a2/anchor/0-AI-MANIFEST.a2ml b/vendor/bunsenite/.machine_readable/6a2/anchor/0-AI-MANIFEST.a2ml new file mode 100644 index 0000000..0dd6825 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/anchor/0-AI-MANIFEST.a2ml @@ -0,0 +1,21 @@ +# AI Manifest for Anchor Directory + +## Purpose + +This manifest declares the AI-assistant context for the anchor machine-readable metadata directory. + +## Canonical Locations + +ANCHOR.a2ml files MUST exist in this directory. + +## Multiple Versions + +Unlike other A2ML files, multiple versions of ANCHOR.a2ml with different dates MAY exist. +Each version represents a specific recalibration point. + +## Invariants + +- Multiple versions with different dates are permitted +- No other A2ML files in this directory +- Single source of truth for anchor documents + diff --git a/vendor/bunsenite/.machine_readable/6a2/anchor/ANCHOR.a2ml b/vendor/bunsenite/.machine_readable/6a2/anchor/ANCHOR.a2ml new file mode 100644 index 0000000..12eab90 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/anchor/ANCHOR.a2ml @@ -0,0 +1,18 @@ +# ⚓ ANCHOR: bunsenite +# This is the canonical authority for the bunsenite repository. + +id: "org.hyperpolymath.bunsenite" +version: "1.0.0" +clade: "unknown" +status: "active" + +# SSG Configuration (Unified boj-server build) +ssg: + engine: "casket" + output_dir: "public" + boj_trigger: true + cartridge: "ssg-mcp" + +# Relationships +parents: + - "org.hyperpolymath.boj-server" diff --git a/vendor/bunsenite/.machine_readable/6a2/anchor/README.adoc b/vendor/bunsenite/.machine_readable/6a2/anchor/README.adoc new file mode 100644 index 0000000..13cae63 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/6a2/anchor/README.adoc @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell +# A2ML Anchor Directory + +This directory contains ANCHOR.a2ml files for project recalibration and scope intervention. + +## Files + +- `ANCHOR.a2ml` - Project recalibration, scope intervention, canonical authority + +## Multiple Versions + +Unlike other A2ML files, multiple versions of ANCHOR.a2ml with different dates may exist. +Each version represents a specific recalibration point in the project history. + +## Standards Compliance + +These files follow the ANCHOR.a2ml specification from: +https://github.com/hyperpolymath/standards/tree/main/anchor-a2ml + +## See Also + +- [A2ML Repository Template](https://github.com/hyperpolymath/standards/blob/main/A2ML-REPO-TEMPLATE.adoc) +- [Anchor A2ML Spec](https://github.com/hyperpolymath/standards/tree/main/anchor-a2ml) + diff --git a/vendor/bunsenite/.machine_readable/ADJUST.contractile b/vendor/bunsenite/.machine_readable/ADJUST.contractile new file mode 100644 index 0000000..e75ae01 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/ADJUST.contractile @@ -0,0 +1,126 @@ +; SPDX-License-Identifier: MPL-2.0 +; ADJUST.contractile — Accessibility invariants for bunsenite +; "ADJUST" = Accessibility & Digital Justice for Universal Software & Technology +; +; Part of the contractile family: MUST, TRUST, DUST, INTENT, ADJUST +; This file is machine-readable. LLM/SLM agents MUST NOT violate these invariants. + +; ── Definitions ────────────────────────────────────────────────── +; +; ADJUST (noun/verb) +; The accessibility contractile. Defines how software must adapt to serve +; all users regardless of ability, device, or context. Named for the verb +; "adjust" — to make suitable, to adapt, to accommodate — which is the +; core action of accessible design. +; +; Scope: +; ADJUST governs all user-facing interfaces: GUI, TUI, CLI, web, mobile, +; documentation, error messages, and installation flows. It applies to +; both human users and assistive technologies (screen readers, switch +; devices, braille displays, voice control). +; +; Relationship to other contractiles: +; - MUST: ADJUST invariants are a subset of MUST — violating ADJUST +; is a MUST violation. ADJUST exists separately because accessibility +; rules are numerous enough to warrant their own file, and because +; LLMs frequently forget accessibility unless explicitly reminded. +; - TRUST: ADJUST does not affect trust levels. All trust tiers must +; respect ADJUST invariants equally. +; - DUST: Deprecating a feature does not exempt it from ADJUST until +; it is fully removed. Deprecated UI must remain accessible. +; - INTENT: ADJUST supports the anti-purpose "this software is NOT +; only for able-bodied users with modern hardware." +; +; Standard: WCAG 2.2 Level AA (minimum) +; https://www.w3.org/WAI/WCAG22/quickref/?levels=aaa +; +; Why a separate file: +; Experience shows LLMs and developers alike treat accessibility as an +; afterthought. By placing invariants in a contractile that is loaded +; at session start, we make it structurally impossible to forget. +; +; ── End Definitions ────────────────────────────────────────────── + +(adjust-contractile + (version "1.0.0") + (full-name "Accessibility & Digital Justice for Universal Software & Technology") + (standard "WCAG-2.2-AA") + (repo "bunsenite") + + (invariants + ; ── Visual ── + (adjust "colour-contrast-ratio >= 4.5:1 for normal text") + (adjust "colour-contrast-ratio >= 3:1 for large text (18pt+ or 14pt+ bold)") + (adjust "no information conveyed by colour alone") + (adjust "no flashing or strobing content (3 flashes/second max)") + (adjust "text resizable to 200% without loss of content or function") + (adjust "focus indicators visible on all interactive elements") + + ; ── Keyboard ── + (adjust "all interactive elements reachable via keyboard (Tab/Shift+Tab)") + (adjust "no keyboard traps — user can always Tab away") + (adjust "skip navigation link present on pages with repeated blocks") + (adjust "logical focus order follows visual reading order") + + ; ── Screen reader ── + (adjust "all images have meaningful alt text (or alt='' if decorative)") + (adjust "all form inputs have associated labels") + (adjust "ARIA landmarks used for page regions (main, nav, banner, etc.)") + (adjust "dynamic content updates announced via aria-live regions") + (adjust "semantic HTML used (headings, lists, tables) — not div soup") + + ; ── Interactive ── + (adjust "touch targets minimum 44x44px on mobile/touch interfaces") + (adjust "error messages identify the field and describe the error") + (adjust "error messages not conveyed by colour or position alone") + (adjust "form validation provides suggestions for correction") + + ; ── Media ── + (adjust "video has captions (closed or open)") + (adjust "audio-only content has text transcript") + (adjust "no autoplay of media with sound") + + ; ── Motion ── + (adjust "animations respect prefers-reduced-motion media query") + (adjust "no content depends on motion to convey meaning") + + ; ── CLI/TUI ── + (adjust "CLI output must not rely solely on colour (use symbols: [OK] [FAIL])") + (adjust "TUI must support high-contrast mode") + (adjust "all CLI commands support --help with plain-text output") + (adjust "error messages written in plain language, not jargon or codes alone") + + ; ── Documentation ── + (adjust "docs use clear language, short sentences, logical structure") + (adjust "code examples include comments explaining non-obvious steps") + (adjust "diagrams have text descriptions or alt text") + + ; ── Internationalisation (i18n) ── + (adjust "all user-facing strings externalisable for translation") + (adjust "no hardcoded English in error messages — use message keys") + (adjust "date/time/number formats locale-aware") + (adjust "RTL (right-to-left) layout support where applicable") + (adjust "Unicode handled correctly throughout (UTF-8 everywhere)") + ) + + (related-resources + ; LOL — super-parallel corpus crawler for 1500+ languages + ; Use for linguistic data, translation coverage, and i18n validation + (lol "standards/lol — multilingual NLP corpus, see README.adoc") + (polyglot-i18n "polyglot-i18n — i18n framework and WASM translation engine") + ) + + (enforcement + (ci "accessibility linting in quality.yml workflow") + (pr-block "PR blocked if accessibility regression detected") + (tool "axe-core or pa11y for automated checks on web UI") + (tool "CLI output inspected for colour-only signalling") + (manual "manual screen reader test before major releases") + ) + + (notes + "These are MINIMUM requirements. Exceeding them (AAA) is encouraged." + "When in doubt about an accessibility decision, ask — don't guess." + "Accessibility is not optional polish — it is a structural requirement." + ) +) diff --git a/vendor/bunsenite/.machine_readable/CLADE.a2ml b/vendor/bunsenite/.machine_readable/CLADE.a2ml new file mode 100644 index 0000000..7060b3b --- /dev/null +++ b/vendor/bunsenite/.machine_readable/CLADE.a2ml @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: MPL-2.0 +# Clade declaration — part of the gv-clade-index registry +# See: https://github.com/hyperpolymath/gv-clade-index + +[identity] +uuid = "ef2c8003-70e4-5fee-9b95-9a5fb2f508a9" +primary-forge = "github" +primary-owner = "hyperpolymath" +canonical-name = "bunsenite" +prefixed-name = "dx-bunsenite" + +[clade] +primary = "dx" +secondary = [] +assigned = "2026-03-16" +rationale = "" + +[forges] +github = "hyperpolymath/bunsenite" +gitlab = "hyperpolymath/bunsenite" +bitbucket = "hyperpolymath/bunsenite" + +[lineage] +type = "standalone" +parent = "Nickel configuration tool" +born = "2026-03-16" + +# Lifecycle status (added by clade-status-backfill; see gv-clade-index ADR 0006). +# Identity (uuid) and status are SEPARATE layers: uuid is immortal; phase is a +# mutable pointer. No phase is terminal (extinct -> active is a legal "Gitassic +# Park" transition on the same uuid). A rename is NOT a phase change — the old +# prefixed-name goes to aliases[], uuid and phase are untouched. +[status] +# One of: reserved incubating active dormant | merged superseded archived extinct +phase = "active" +since = "2026-03-16" +present = true +aliases = [] +merged-into = "" +superseded-by = "" +successors = [] +ended = "" + +[[status.history]] +phase = "active" +since = "2026-03-16" +note = "backfilled default — correct if the true phase differs" diff --git a/vendor/bunsenite/.machine_readable/INTENT.contractile b/vendor/bunsenite/.machine_readable/INTENT.contractile new file mode 100644 index 0000000..bba0083 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/INTENT.contractile @@ -0,0 +1,72 @@ +; SPDX-License-Identifier: MPL-2.0 +; INTENT.contractile — Purpose and scope for bunsenite +; Helps LLM/SLM agents understand what this repo IS and IS NOT. +; +; Part of the contractile family: MUST, TRUST, DUST, INTENT, ADJUST + +; ── Definitions ────────────────────────────────────────────────── +; +; INTENT (noun) +; The purpose contractile. Defines what this repository IS, what it is +; NOT (anti-purpose), and which architectural decisions are load-bearing. +; Without INTENT, LLMs drift into scope creep, reverse key decisions, +; or add features that belong in a different repo. +; +; Scope: +; INTENT governs the conceptual boundaries of the project — its reason +; for existing, its domain, and its relationship to the ecosystem. +; It does NOT specify implementation details (that's MUST and code). +; +; Relationship to other contractiles: +; - MUST: INTENT explains WHY certain MUSTs exist. If you don't +; understand a MUST, read INTENT first. +; - TRUST: The "ask-before-touching" section in INTENT maps directly +; to TRUST.trust-deny for the most sensitive areas. +; - ADJUST: INTENT's anti-purpose should include "this software is +; NOT only for users with perfect vision/hearing/mobility." +; - DUST: When INTENT changes (repo pivots), related DUST entries +; should be created for the abandoned direction. +; +; ── End Definitions ────────────────────────────────────────────── + +(intent-contractile + (version "1.0.0") + (repo "bunsenite") + + ; === Purpose (what this repo IS) === + (purpose + "{{ONE_PARAGRAPH_PURPOSE}}" + ) + + ; === Anti-Purpose (what this repo is NOT — prevents scope creep) === + (anti-purpose + "{{ONE_PARAGRAPH_ANTI_PURPOSE}}" + ; Examples: + ; "This is NOT a general-purpose database — it solves one specific problem." + ; "This is NOT a framework — it is a library with a focused API." + ; "This does NOT handle authentication — that is delegated to [other repo]." + ) + + ; === Key Architectural Decisions That Must Not Be Reversed === + (architectural-invariants + ; *REMINDER: List the foundational decisions* + ; ("Idris2 for ABI definitions — dependent types prove interface correctness") + ; ("Zig for FFI — zero-cost C ABI compatibility") + ; ("Elixir for supervision — OTP fault tolerance") + ) + + ; === Sensitive Areas (if in doubt, ask) === + (ask-before-touching + ; *REMINDER: List areas where LLMs should check before modifying* + ; "src/abi/ — formal proofs, changes require re-verification" + ; "ffi/zig/ — C ABI boundary, changes affect all language bindings" + ; ".machine_readable/ — checkpoint files, format is specified" + ) + + ; === Ecosystem Position === + (ecosystem + (belongs-to "{{MONOREPO_OR_STANDALONE}}") + (depends-on ("{{DEP1}}" "{{DEP2}}")) + (depended-on-by ("{{CONSUMER1}}" "{{CONSUMER2}}")) + ) +) diff --git a/vendor/bunsenite/.machine_readable/MUST.contractile b/vendor/bunsenite/.machine_readable/MUST.contractile new file mode 100644 index 0000000..9826581 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/MUST.contractile @@ -0,0 +1,91 @@ +; SPDX-License-Identifier: MPL-2.0 +; MUST.contractile — Baseline invariants for bunsenite +; These constraints MUST NOT be violated. K9 validators enforce them. +; +; Part of the contractile family: MUST, TRUST, DUST, INTENT, ADJUST + +; ── Definitions ────────────────────────────────────────────────── +; +; MUST (noun/verb) +; The hard-constraint contractile. Defines invariants that are structurally +; required for the repository to function correctly and safely. Violating +; a MUST is always a bug — there are no "soft" MUSTs. +; +; Scope: +; MUST governs code, configuration, CI, and structure. It does NOT govern +; style, preference, or approach — those belong in CLAUDE.md or coding +; standards. MUST is for things that break the project if violated. +; +; Relationship to other contractiles: +; - TRUST: MUST is enforced regardless of trust level. Even maximal-trust +; agents cannot violate MUST constraints. +; - ADJUST: All ADJUST invariants are implicitly MUST invariants too. +; ADJUST exists separately for visibility. +; - INTENT: MUST protects the architectural decisions described in INTENT. +; - DUST: When a feature enters DUST (deprecation), its MUST constraints +; remain active until the feature is fully removed. +; +; Enforcement: +; K9 validators in contractiles/self-validating/ machine-check MUST constraints. +; CI runs these on every PR. Violations block merge. +; +; ── End Definitions ────────────────────────────────────────────── + +(must-contractile + (version "1.0.0") + (repo "bunsenite") + + ; === Universal Invariants (apply to ALL repos) === + + (invariants + ; Paths + (must "no hardcoded absolute paths (/home/*, /mnt/*, /var/mnt/*)") + (must "all paths use env vars, XDG dirs, or relative references") + + ; Language policy + (must "no new TypeScript files") + (must "no new Python files") + (must "no new Go files") + (must "no npm/bun/yarn/pnpm dependencies — Deno only") + + ; Dangerous patterns + (must "no believe_me (Idris2)") + (must "no assert_total (Idris2)") + (must "no Admitted (Coq)") + (must "no sorry (Lean)") + (must "no unsafeCoerce (Haskell)") + (must "no Obj.magic (OCaml)") + (must "no unsafe {} blocks without safety comment (Rust)") + + ; License + (must "SPDX-License-Identifier header on every source file") + (must "no removal or modification of LICENSE file") + + ; Structure + (must ".machine_readable/ directory preserved") + (must "0-AI-MANIFEST.a2ml preserved") + (must "no SCM files in repo root — only in .machine_readable/") + + ; CI + (must "no removal of CI workflows without explicit approval") + (must "all GitHub Actions SHA-pinned") + + ; Code quality + (must "tests must not be deleted or weakened") + (must "generated code in generated/ directory only") + (must "no introduction of OWASP top 10 vulnerabilities") + + ; ABI/FFI (if applicable) + (must "no modification of ABI contracts without proof update") + (must "no removal of formal verification proofs") + ) + + ; === Project-Specific Invariants === + ; *REMINDER: Add invariants specific to this repo* + ; (must "# Add project-specific invariants here") + + (enforcement + (k9-validator "contractiles/self-validating/must-check.k9.ncl") + (ci "quality.yml runs must-check on every PR") + ) +) diff --git a/vendor/bunsenite/.machine_readable/TRUST.contractile b/vendor/bunsenite/.machine_readable/TRUST.contractile new file mode 100644 index 0000000..568607c --- /dev/null +++ b/vendor/bunsenite/.machine_readable/TRUST.contractile @@ -0,0 +1,80 @@ +; SPDX-License-Identifier: MPL-2.0 +; TRUST.contractile — Trust boundaries for bunsenite +; Defines what LLM/SLM agents are trusted to do without asking. +; +; Part of the contractile family: MUST, TRUST, DUST, INTENT, ADJUST + +; ── Definitions ────────────────────────────────────────────────── +; +; TRUST (noun/verb) +; The permission contractile. Defines the boundary between what an AI +; agent may do autonomously and what requires human approval. Trust is +; graduated — not binary — with four levels from minimal to maximal. +; +; Trust levels: +; - maximal: Agent may read, build, test, lint, format, heal freely. +; Only destructive/external actions require approval. +; - standard: Agent may read and build. Test/lint need approval. +; - restricted: Agent may read only. All modifications need approval. +; - minimal: Agent may read specific files only. Everything else blocked. +; +; Scope: +; TRUST governs AI agent behaviour only. It does not affect human +; contributors — humans follow CONTRIBUTING.md and GOVERNANCE.adoc. +; +; Relationship to other contractiles: +; - MUST: Trust never overrides MUST. Even at maximal trust, MUST +; violations are blocked. +; - ADJUST: Trust does not exempt from ADJUST. All trust tiers must +; produce accessible output. +; - INTENT: TRUST.trust-deny protects the sensitive areas listed in +; INTENT.ask-before-touching. +; - DUST: Deprecated features have the same trust rules as active ones. +; +; ── End Definitions ────────────────────────────────────────────── + +(trust-contractile + (version "1.0.0") + (repo "bunsenite") + + (trust-level "maximal") ; maximal | standard | restricted | minimal + + ; === Maximal Trust (default) === + ; LLM may freely do these without asking: + (trust-actions + "read" ; Read any file in the repo + "build" ; Run build commands + "test" ; Run test suites + "lint" ; Run linters and formatters + "format" ; Auto-format code + "doctor" ; Run self-diagnostics + "heal" ; Attempt automatic repair + "git-status" ; Check git status + "git-diff" ; View diffs + "git-log" ; View history + ) + + ; === Denied Actions (always require human approval) === + (trust-deny + "delete-branch" ; Could lose work + "force-push" ; Overwrites history + "modify-ci-secrets" ; Security sensitive + "publish" ; External visibility + "push-to-main" ; Protected branch + "delete-files-bulk" ; More than 5 files at once + "modify-license" ; Legal implications + "modify-security-policy" ; Security implications + "remove-proofs" ; Formal verification regression + "disable-ci-checks" ; Safety regression + ) + + ; === Trust Boundary === + (trust-boundary "repo") ; LLM confined to this repo unless explicitly told otherwise + + ; === Override === + ; Repos requiring tighter trust override these settings with justification: + ; (override + ; (trust-level "restricted") + ; (reason "Contains production secrets / handles PII / etc.") + ; ) +) diff --git a/vendor/bunsenite/.machine_readable/bot_directives/README.adoc b/vendor/bunsenite/.machine_readable/bot_directives/README.adoc new file mode 100644 index 0000000..1dcb625 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/bot_directives/README.adoc @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Agent Instructions +:toc: preamble + +Methodology-aware configuration for AI agents. Read by any AI agent +(Claude, Gemini, Copilot, etc.) at session start. + +== Files + +[cols="1,3"] +|=== +| File | Purpose + +| `methodology.a2ml` +| Default mode, invariants, ring ceiling, priority weights, convergent budget + +| `coverage.a2ml` +| Session coverage tracking — what was visited, what was skipped, what has MUSTs + +| `debt.a2ml` +| Meander debt — things found but not fixed, carried between sessions +|=== + +== How Agents Use These + +1. Read `methodology.a2ml` at session start — know mode, invariants, ceiling +2. Read `coverage.a2ml` — know what was visited last time, what was skipped +3. Read `debt.a2ml` — know what's outstanding from previous sessions +4. At session end, update `coverage.a2ml` and `debt.a2ml` + +== Relationship to Other Files + +* `AGENTIC.a2ml` says WHAT agents can do (permissions, gating) +* `bot_directives/` says HOW agents should work (methodology) +* `bot_directives/` says what the gitbot-fleet does (fleet-specific) +* `CLAUDE.md` says how Claude specifically should work (Claude-specific) + +== Reference + +ADR-002 in `standards/agentic-a2ml/docs/ADR-002-methodology-layer.adoc` diff --git a/vendor/bunsenite/.machine_readable/bot_directives/coverage.a2ml b/vendor/bunsenite/.machine_readable/bot_directives/coverage.a2ml new file mode 100644 index 0000000..6979664 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/bot_directives/coverage.a2ml @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# coverage.a2ml — Session coverage tracking +# Updated at the end of each AI agent session. +# Persists what was visited, what was skipped, and what has MUSTs. +# +# Reference: ADR-002 in standards/agentic-a2ml/docs/ + +[metadata] +version = "1.0.0" +last-updated = "2026-03-24" + +# ============================================================================ +# COVERAGE STATE +# ============================================================================ +# Updated by agents at session end. Tracks which components have been +# visited and which have known MUSTs that were skipped. + +[coverage] +total-components = 0 +visited-components = 0 +coverage-percent = 0 + +# ============================================================================ +# VISITED COMPONENTS +# ============================================================================ +# Component → session date + ring reached +# Agents add entries as they work through components. +# +# Example: +# [coverage.visited.emergency-room] +# date = "2026-03-23" +# ring = 2 +# fixes = 3 +# notes = "boot-guardian built, shutdown-marshal built" + +# ============================================================================ +# SKIPPED COMPONENTS WITH MUSTS +# ============================================================================ +# Components with known MUSTs that were not visited in the most recent session. +# These become P1 inputs for the next session's Phase 0. +# +# Example: +# [coverage.skipped-musts.session-sentinel] +# priority = "P0" +# issue = "56 SIGABRTs in 4 days, D-Bus race condition" +# discovered = "2026-03-23" + +# ============================================================================ +# CHERRY-PICKING AUDIT +# ============================================================================ +# At session end, agents report whether they chose easy work over hard work. +# This is the accountability mechanism for the weighted priority system. +# +# [coverage.cherry-picking] +# easy-high-completed = 3 +# hard-high-completed = 1 +# easy-low-completed = 2 +# hard-low-deferred = 4 +# assessment = "Correctly prioritised — all MUST items addressed before COULDs" diff --git a/vendor/bunsenite/.machine_readable/bot_directives/debt.a2ml b/vendor/bunsenite/.machine_readable/bot_directives/debt.a2ml new file mode 100644 index 0000000..c0238c5 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/bot_directives/debt.a2ml @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# debt.a2ml — Meander debt list +# Things found but not fixed. Carried between sessions. +# Becomes the next session's Phase 0 input. +# +# Reference: ADR-002 in standards/agentic-a2ml/docs/ + +[metadata] +version = "1.0.0" +last-updated = "2026-03-24" + +# ============================================================================ +# DEBT ITEMS +# ============================================================================ +# Each item has: component, issue, effort (easy|medium|hard), impact (high|medium|low), +# priority (should|could), and discovered date. +# +# Items are consumed (removed) when fixed. New items are added at session end. +# The debt list prevents the "one more wave" loop — found things are persisted, +# not forgotten, and not used as justification for infinite meandering. + +# ============================================================================ +# SHOULD — would fix next wave +# ============================================================================ +# These are inputs for the next session if the user says "keep going". +# +# Example: +# [[debt.should]] +# component = "system-tools/monitoring/observatory" +# issue = "Stale duplicate of root observatory/" +# effort = "easy" +# impact = "medium" +# discovered = "2026-03-23" + +# ============================================================================ +# COULD — would fix eventually +# ============================================================================ +# These are low-priority items that don't justify a session on their own. +# They get picked up when an agent is in the area for other reasons. +# +# Example: +# [[debt.could]] +# component = "cicada" +# issue = "RSR_OUTLINE.adoc references banned AGPL-3.0" +# effort = "easy" +# impact = "low" +# discovered = "2026-03-23" diff --git a/vendor/bunsenite/.machine_readable/bot_directives/methodology.a2ml b/vendor/bunsenite/.machine_readable/bot_directives/methodology.a2ml new file mode 100644 index 0000000..754f357 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/bot_directives/methodology.a2ml @@ -0,0 +1,107 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# methodology.a2ml — AI agent methodology configuration +# Declares how agents should approach work in this repository. +# Read at session start by any AI agent (Claude, Gemini, Copilot, etc.) +# +# Reference: ADR-002 in standards/agentic-a2ml/docs/ + +[metadata] +version = "1.0.0" +last-updated = "2026-03-24" +spec = "https://github.com/hyperpolymath/standards/blob/main/agentic-a2ml/docs/ADR-002-methodology-layer.adoc" + +# ============================================================================ +# MODE SELECTION +# ============================================================================ +# convergent: find gaps, fill them, build infrastructure (default for ops/infra) +# divergent: find what's strongest, push it further (for research/creative) +# hybrid: audit 20% of budget, then focus 80% on top MUSTs (default for most) + +[methodology] +default-mode = "hybrid" +ring-ceiling = 2 # Hard ceiling for ring expansion (0-3) +wave-cap = 2 # Max waves before requiring user "keep going" +spike-required = true # Every session must ship code, not just designs + +# ============================================================================ +# PRIORITY WEIGHTS +# ============================================================================ +# MUST (3x): Blocking the current work → fix immediately +# SHOULD (2x): Degrading quality of current work → fix if in zone +# COULD (1x): Improving quality of adjacent work → add to debt list + +[methodology.priority-weights] +must = 3 +should = 2 +could = 1 + +# ============================================================================ +# CONVERGENT BUDGET (when mode = convergent or hybrid) +# ============================================================================ +# How to allocate effort across work types. +# Prevents over-polishing docs while structural work waits. + +[methodology.convergent-budget] +structural = 70 # % for new modules, compilation fixes, wiring, integration +corrective = 20 # % for bugs found, broken imports, stale references +perfective = 10 # % for SPDX headers, doc updates, formatting, style + +# ============================================================================ +# UNIQUE STRENGTH (when mode = divergent) +# ============================================================================ +# What makes this project special. Agents should DEEPEN this, not broaden it. +# Customise this per project — the template default is generic. + +[methodology.unique-strength] +description = "{{PROJECT_UNIQUE_STRENGTH}}" +deepen-not-broaden = true + +# ============================================================================ +# DIVERGENT INVARIANTS +# ============================================================================ +# Constraints that divergent mode must NOT violate. +# These are the riverbanks — diverge within them, not across. +# "Amplify uniqueness" means deepen, not broaden. +# +# Test before any divergent action: +# "Does this deepen the existing strength, or add a parallel strength?" +# If parallel → stop. Note as cross-project insight. + +[methodology.divergent-invariants] +rules = [ + # Customise per project. Examples: + # "Idris2 only for formal verification — no Lean4, Coq, Agda", + # "believe_me count must remain zero", + # "FFI architecture: Idris2 → RefC → Zig → C ABI (no shortcuts)", +] + +# Optional: language invariant for the core strength +# If set, divergent mode will not introduce other languages for this purpose +# language-invariant = "idris2" + +# ============================================================================ +# CONSTRAINT HINTS +# ============================================================================ +# Help Phase 0 find the critical chain faster. +# Updated at session end with newly discovered constraints. + +[methodology.known-constraints] +constraints = [ + # Customise per project. Examples: + # "End-to-end build has never been verified", + # "libproject.so does not exist yet — all bindings call stubs", +] + +# ============================================================================ +# STATE FILE VALIDATION +# ============================================================================ +# Phase 0 reads STATE.a2ml first but it may be broken. +# These rules detect corrupt/template/stale state files. + +[methodology.state-validation] +reject-if-contains = ["{{PLACEHOLDER}}", "{{PROJECT}}", "rsr-template-repo"] +reject-if-project-name-mismatch = true +staleness-threshold-days = 90 +fallback-files = ["TODO.md", "TODO.adoc", "ROADMAP.adoc", "README.adoc"] diff --git a/vendor/bunsenite/.machine_readable/contractiles/bust/Bustfile.a2ml b/vendor/bunsenite/.machine_readable/contractiles/bust/Bustfile.a2ml new file mode 100644 index 0000000..fba3e99 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/contractiles/bust/Bustfile.a2ml @@ -0,0 +1,28 @@ +// Bustfile.a2ml — meta-repo bust contractile (breakage / rollback) +// SPDX-License-Identifier: MPL-2.0 + +Bust { + name: "bunsenite" + version: "1.0.0" + description: "Rollback procedures when something breaks in the meta-repo" + + scenarios: { + "bad-pointer-bump": "git revert in meta-repo; child repo itself untouched" + "submodule-pointer-points-at-missing-sha": "git submodule update --init --checkout resets child to parent-recorded SHA; OR revert the stale bump commit" + "submodule-orphan-after-local-only-commit": "roll back locally with git reset to before the stranded commit; fix remote situation before re-attempting" + "accidental-private-repo-content-leaked-to-public-submodule": "hard-rotate the leaked secret immediately; git-filter-repo or BFG on the submodule's own history; public re-publication only after rotation complete" + } + + escalation-ladder: [ + "1. revert the meta-repo commit (reversible, low blast radius)", + "2. reset the local submodule clone (affects only local workspace)", + "3. force-push to main — PROHIBITED without explicit user confirmation (violates branch protection)", + "4. registry-level (delete/archive the GitHub repo) — human-only action, never by AI" + ] + + backup-points: [ + "GitHub serves as the durable backup for every submodule's own history", + "Meta-repo history on origin/main is the durable backup for pointer state", + "Local backup tags (backup/pre--) retained on risky rewrites" + ] +} diff --git a/vendor/bunsenite/.machine_readable/contractiles/bust/bust.ncl b/vendor/bunsenite/.machine_readable/contractiles/bust/bust.ncl new file mode 100644 index 0000000..fc8cb8c --- /dev/null +++ b/vendor/bunsenite/.machine_readable/contractiles/bust/bust.ncl @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: MPL-2.0 +# Bust — error-handling / failure-recovery runner +# +# Pairs with: Bustfile.a2ml (same directory) +# Verb: bust +# Semantics: every declared failure mode must have a recovery path that has +# been exercised. Runner injects failures (via declared probes) +# and verifies the recovery path works. Hard gate on any +# failure-mode with missing or broken recovery. +# CLI: `contractile bust check` → list failure modes + recovery status +# `contractile bust drill` → inject declared failures, verify recovery +# +# Anything else in this directory is human-only notes/archive; machines ignore. +# +# Base: ../_base.ncl provides pedigree_schema, run_defaults, probe_schema. +# See: docs/CONTRACTILE-SPEC.adoc + +let base = import "../_base.ncl" in + +{ + pedigree = base.pedigree_schema & { + contractile_verb = "bust", + semantics = "error handling + failure recovery", + security = { + leash = 'Kennel, + trust_level = "controlled failure injection; scoped to system-under-test", + allow_network = false, + allow_filesystem_write = true, # drills may write transient state (tmp dirs, test DBs) + allow_subprocess = true, + injection_scope = "system-under-test-only", + }, + metadata = { + name = "bust-runner", + version = "1.0.0", + description = "Exercises declared failure modes and verifies recovery paths. Hard-gates on any failure mode without working recovery.", + paired_xfile = "Bustfile.a2ml", + author = "Jonathan D.A. Jewell ", + }, + }, + + schema = { + failure_modes + | Array { + id | String, + description | String, + class | [| 'network, 'disk_full, 'oom, 'timeout, 'partial_write, 'panic, 'crash, 'rollback, 'concurrency |], + # TODO: migrate to base.probe_schema (structured probe) when CLI supports it + injection_probe | String, # command that deterministically causes this failure + # TODO: migrate to base.probe_schema (structured probe) when CLI supports it + recovery_probe | String, # command that verifies recovery (exit 0 = recovered) + expected_recovery_time_seconds | Number | default = 30, + # status_core values: 'declared, 'verified, 'failing; bust adds 'drilled + status | [| 'declared, 'drilled, 'verified, 'failing |] | default = 'declared, + notes | String | optional, + }, + }, + + # Runner behaviour — inherits from base.run_defaults. + # bust adds record_recovery_times for performance tier feeding. + run = base.run_defaults & { + on_any_fail = "exit-nonzero", # missing or broken recovery blocks merge + report_format = "a2ml", + emit_summary = true, + record_recovery_times = true, # feeds the performance tier + }, +} diff --git a/vendor/bunsenite/.machine_readable/contractiles/dust/Dustfile.a2ml b/vendor/bunsenite/.machine_readable/contractiles/dust/Dustfile.a2ml new file mode 100644 index 0000000..0d619ee --- /dev/null +++ b/vendor/bunsenite/.machine_readable/contractiles/dust/Dustfile.a2ml @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: MPL-2.0 +# Dustfile — Cleanup and Hygiene Contract + +[dustfile] +version = "1.0.0" +format = "a2ml" + +[cleanup] +stale-branch-policy = "delete-after-merge" +artifact-retention = "90-days" +cache-policy = "clear-on-release" + +[hygiene] +linting = "required" +formatting = "required" +dead-code-removal = "encouraged" +todo-tracking = "tracked-in-issues" + +[reversibility] +backup-before-destructive = true +rollback-mechanism = "git-revert" +data-retention-policy = "preserve-30-days" diff --git a/vendor/bunsenite/.machine_readable/contractiles/trust/Trustfile.a2ml b/vendor/bunsenite/.machine_readable/contractiles/trust/Trustfile.a2ml new file mode 100644 index 0000000..f2a4f95 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/contractiles/trust/Trustfile.a2ml @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: MPL-2.0 +# Trustfile — Integrity and provenance verification +# Author: Jonathan D.A. Jewell + +@abstract: +Integrity invariants for this repository. These verify that the repo +has not been tampered with, secrets are not leaked, and provenance +is traceable. +@end + +## Secrets + +### no-secrets-committed +- description: No credential files in repo +- run: test ! -f .env && test ! -f credentials.json && test ! -f .env.local && test ! -f .env.production +- severity: critical + +### no-private-keys +- description: No private key files committed +- run: "! find . -name '*.pem' -o -name '*.key' -o -name 'id_rsa' -o -name 'id_ed25519' 2>/dev/null | grep -v node_modules | head -1 | grep -q ." +- severity: critical + +### no-tokens-in-source +- description: No hardcoded API tokens in source +- run: "! grep -rE '(api[_-]?key|secret|token|password)\s*[:=]\s*[\"'\\''][A-Za-z0-9]{16,}' --include='*.js' --include='*.ts' --include='*.res' --include='*.py' . 2>/dev/null | grep -v node_modules | head -1 | grep -q ." +- severity: critical + +## Provenance + +### author-correct +- description: Git author matches expected identity +- run: "git log -1 --format='%ae' | grep -qE '(hyperpolymath|j\\.d\\.a\\.jewell)'" +- severity: warning + +### license-content +- description: LICENSE contains expected identifier +- run: grep -q 'PMPL\|MPL\|MIT\|Apache\|LGPL' LICENSE +- severity: warning + +## Container Security + +### container-images-pinned +- description: Containerfile uses pinned base images +- run: test ! -f Containerfile || grep -q 'cgr.dev\|@sha256:' Containerfile +- severity: warning + +### no-dockerfile +- description: No Dockerfile (use Containerfile) +- run: test ! -f Dockerfile +- severity: warning diff --git a/vendor/bunsenite/.machine_readable/integrations/feedback-o-tron.a2ml b/vendor/bunsenite/.machine_readable/integrations/feedback-o-tron.a2ml new file mode 100644 index 0000000..5381604 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/integrations/feedback-o-tron.a2ml @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: MPL-2.0 +# Feedback-o-Tron Integration — Autonomous Bug Reporting + +[integration] +name = "feedback-o-tron" +type = "bug-reporter" +repository = "https://github.com/hyperpolymath/feedback-o-tron" + +[reporting-config] +platforms = ["github", "gitlab", "bugzilla"] +deduplication = true +audit-logging = true +auto-file-upstream = "on-external-dependency-failure" diff --git a/vendor/bunsenite/.machine_readable/integrations/proven.a2ml b/vendor/bunsenite/.machine_readable/integrations/proven.a2ml new file mode 100644 index 0000000..9af33ff --- /dev/null +++ b/vendor/bunsenite/.machine_readable/integrations/proven.a2ml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: MPL-2.0 +# Proven Integration — Formally Verified Safety Library + +[integration] +name = "proven" +type = "safety-library" +repository = "https://github.com/hyperpolymath/proven" +version = "1.2.0" + +[binding-policy] +approach = "thin-ffi-wrapper" +unsafe-patterns = "replace-with-proven-equivalent" +modules-available = ["SafeMath", "SafeString", "SafeJSON", "SafeURL", "SafeRegex", "SafeSQL", "SafeFile", "SafeTemplate", "SafeCrypto"] + +[adoption-guidance] +priority = "high" +scope = "all-string-json-url-crypto-operations" +migration = "incremental — replace unsafe patterns as encountered" diff --git a/vendor/bunsenite/.machine_readable/integrations/verisimdb.a2ml b/vendor/bunsenite/.machine_readable/integrations/verisimdb.a2ml new file mode 100644 index 0000000..164c522 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/integrations/verisimdb.a2ml @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: MPL-2.0 +# VeriSimDB Feed — Cross-Repo Analytics Data Store + +[integration] +name = "verisimdb" +type = "data-feed" +repository = "https://github.com/hyperpolymath/nextgen-databases" +data-store = "verisimdb-data" + +[feed-config] +emit-scan-results = true +emit-build-metrics = true +emit-dependency-graph = true +format = "hexad" +destination = "verisimdb-data/feeds/" diff --git a/vendor/bunsenite/.machine_readable/integrations/vexometer.a2ml b/vendor/bunsenite/.machine_readable/integrations/vexometer.a2ml new file mode 100644 index 0000000..238b3d2 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/integrations/vexometer.a2ml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: MPL-2.0 +# Vexometer Integration — Irritation Surface Analysis + +[integration] +name = "vexometer" +type = "friction-measurement" +repository = "https://github.com/hyperpolymath/vexometer" + +[measurement-config] +dimensions = 10 +emit-isa-reports = true +lazy-eliminator = true +satellite-interventions = true + +[hooks] +cli-tools = "measure-on-error" +ui-panels = "measure-on-interaction" +build-failures = "measure-on-failure" diff --git a/vendor/bunsenite/.machine_readable/root-allow.txt b/vendor/bunsenite/.machine_readable/root-allow.txt new file mode 100644 index 0000000..6cbe973 --- /dev/null +++ b/vendor/bunsenite/.machine_readable/root-allow.txt @@ -0,0 +1,3 @@ +CLAUDE.md +flake.guix +build/ # build orchestration: guix.scm relocated here (canon 1.2.1 guix-primary template_ref = "build/") diff --git a/vendor/bunsenite/.mise.toml b/vendor/bunsenite/.mise.toml new file mode 100644 index 0000000..b596372 --- /dev/null +++ b/vendor/bunsenite/.mise.toml @@ -0,0 +1,5 @@ +[tools] +# = "SPDX-License-Identifier: MPL-2.0" +# = "asdf/mise tool versions" +# = "See: https://asdf-vm.com/" +rust = "1.83.0" diff --git a/vendor/bunsenite/.nojekyll b/vendor/bunsenite/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/vendor/bunsenite/0-AI-MANIFEST.a2ml b/vendor/bunsenite/0-AI-MANIFEST.a2ml new file mode 100644 index 0000000..197eb0d --- /dev/null +++ b/vendor/bunsenite/0-AI-MANIFEST.a2ml @@ -0,0 +1,15 @@ + +# AI Assistant Instructions + +## Repository Focus +- `rsr-template-repo` is treated as a Rhodium Standard Repository; obey the Rhodium policies, maintain `.bot_directives`, and keep `.machines_readable/6scm/` authoritative. +- Prefer to keep generated files out of source control, and regenerate them with the documented commands before committing. + +## Workflow +1. Inspect `.machines_readable/6scm/STATE.scm` for blockers and next actions. +2. Respect any constraints listed inside `.machines_readable/6scm/AGENTIC.scm` when tooling changes are requested. +3. After finishing edits, update STATE with your outcomes and commit with a concise, imperative message. + +## Delivery Promises +- Mention in summaries whether STATE, `contractiles/`, or `.bot_directives/` changed. +- Keep this file in sync with the repository’s status; update it if the governance changes. diff --git a/vendor/bunsenite/ABI-FFI-README.adoc b/vendor/bunsenite/ABI-FFI-README.adoc new file mode 100644 index 0000000..04be86a --- /dev/null +++ b/vendor/bunsenite/ABI-FFI-README.adoc @@ -0,0 +1,409 @@ +\{\{~ 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, AffineScript, 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/ + ├── affinescript/ + └── julia/ +.... + +=== Why Idris2 for ABI? + +==== 1. *Formal Verification* + +Idris2’s dependent types allow proving properties about the ABI at +compile-time: + +[source,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: + +[source,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: + +[source,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: + +[source,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: + +[source,zig] +---- +export fn library_function(param: i32) i32 { + return param * 2; +} +---- + +==== 2. *Memory Safety* + +Compile-time safety without runtime overhead: + +[source,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: + +[source,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): + +[source,zig] +---- +// Minimal binary size +pub const lib = @import("std"); +// Only includes what you use +---- + +=== Building + +==== Build FFI Library + +[source,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 + +[source,bash] +---- +cd src/abi +idris2 --cg c-header Types.idr -o ../../generated/abi/{{project}}.h +---- + +==== Cross-Compile + +[source,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 + +[source,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: + +[source,bash] +---- +gcc -o example example.c -l{{project}} -L./zig-out/lib +---- + +==== From Idris2 + +[source,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 + +[source,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 + +[source,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) + +[source,bash] +---- +cd ffi/zig +zig build test +---- + +==== Integration Tests + +[source,bash] +---- +cd ffi/zig +zig build test-integration +---- + +==== ABI Verification (Idris2) + +[source,idris] +---- +-- Compile-time verification +%runElab verifyABI + +-- Runtime checks +main : IO () +main = do + verifyLayoutsCorrect + verifyAlignmentsCorrect + putStrLn "ABI verification passed" +---- + +=== Contributing + +When modifying the ABI/FFI: + +[arabic] +. *Update ABI first* (`+src/abi/*.idr+`) +* Modify type definitions +* Update proofs +* Ensure backward compatibility +. *Generate C header* ++ +[source,bash] +---- +idris2 --cg c-header src/abi/Types.idr -o generated/abi/{{project}}.h +---- +. *Update FFI implementation* (`+ffi/zig/src/main.zig+`) +* Implement new functions +* Match ABI types exactly +. *Add tests* +* Unit tests in Zig +* Integration tests +* ABI verification tests +. *Update documentation* +* Function signatures +* Usage examples +* Migration guide (if breaking changes) + +=== License + +MPL-2.0 + +=== See Also + +* https://idris2.readthedocs.io[Idris2 Documentation] +* https://ziglang.org/documentation/master/[Zig Documentation] +* https://github.com/hyperpolymath/rhodium-standard-repositories[Rhodium +Standard Repositories] +* link:../ffi-migration-guide.md[FFI Migration Guide] +* link:../abi-migration-guide.md[ABI Migration Guide] diff --git a/vendor/bunsenite/ARCHITECTURE.adoc b/vendor/bunsenite/ARCHITECTURE.adoc new file mode 100644 index 0000000..1c0a7a6 --- /dev/null +++ b/vendor/bunsenite/ARCHITECTURE.adoc @@ -0,0 +1,48 @@ +== Architecture + +=== Overview + +This repository follows a modular, maintainable architecture designed +for clarity, scalability, and long-term sustainability. + +=== Directory Structure + +.... +. +├── src/ # Source code +├── tests/ # Test suites +├── docs/ # Documentation +├── scripts/ # Utility scripts +├── config/ # Configuration files +├── LICENSE # License file +├── LICENSES/ # Full license texts +└── README.adoc # Project documentation +.... + +=== Design Principles + +* *Separation of Concerns*: Each module has a single responsibility +* *Testability*: Code is written to be easily testable +* *Documentation*: All public APIs are documented +* *Configuration*: Environment-specific settings are externalized + +=== Dependencies + +* External dependencies are minimized and clearly declared +* Version pinning is used for reproducibility + +=== Security Considerations + +* Sensitive data is never committed to the repository +* Secrets are managed through environment variables or secure vaults +* Regular dependency audits are performed + +=== Maintainability + +* Code follows consistent style guidelines +* Pull requests require review and CI checks +* Issues and discussions are tracked transparently + +''''' + +_Last updated: 2026-07-18_ diff --git a/vendor/bunsenite/CHANGELOG.adoc b/vendor/bunsenite/CHANGELOG.adoc new file mode 100644 index 0000000..e7691df --- /dev/null +++ b/vendor/bunsenite/CHANGELOG.adoc @@ -0,0 +1,202 @@ +== Changelog + +All notable changes to this project will be documented in this file. + +The format is based on https://keepachangelog.com/en/1.0.0/[Keep a +Changelog], and this project adheres to +https://semver.org/spec/v2.0.0.html[Semantic Versioning]. + +=== [Unreleased] + +==== Planned + +* TUI (Ada/SPARK) +* Language Server Protocol (LSP) +* Additional language bindings (Python, Ruby, Node.js) +* Plugin system + +=== [1.0.0] - 2025-12-12 + +==== Added + +* Zig FFI layer for stable C ABI across Rust compiler versions +* Complete Deno bindings using `+Deno.dlopen+` FFI +* Complete AffineScript bindings via C FFI +* Watch mode with file change detection (`+bunsenite watch+`) +* Interactive REPL (`+bunsenite repl+`) +* JSON Schema validation (`+bunsenite schema+`) +* miette 7.0 integration for beautiful error diagnostics + +==== Changed + +* Upgraded nickel-lang-core to 0.9.1 (CBNCache moved to lazy module) +* CLI expanded from 3 commands to 6 commands +* Documentation updated for v1.0.0 release + +==== Fixed + +* CBNCache import path for nickel-lang-core 0.9.1 compatibility + +==== Compliance + +* RSR Bronze Tier: Verified +* TPCF Perimeter 3: Maintained +* No plain TypeScript, npm, or Python dependencies + +=== [0.1.0] - 2025-11-22 + +==== Added + +* 🎉 Initial release of Bunsenite! +* ✅ Rust core library with nickel-lang-core 0.9.1 integration +* ✅ `+NickelLoader+` API for parsing and evaluating Nickel +configurations +* ✅ Comprehensive error handling with helpful error messages +* ✅ WebAssembly bindings for browser deployment (~95% native speed) +* ✅ Command-line interface with `+parse+`, `+validate+`, and `+info+` +commands +* ✅ Zero `+unsafe+` code (enforced by compiler directive) +* ✅ Complete test suite (30+ tests, 100% pass rate) +* ✅ Full RSR Bronze Tier compliance: +** Type safety (Rust compile-time guarantees) +** Memory safety (ownership model, no unsafe) +** Offline-first (no network dependencies) +** Complete documentation set +** `+.well-known/+` directory (security.txt, ai.txt, humans.txt) +** Build system (Justfile, Guix flake) +** CI/CD pipeline (GitLab CI) +* ✅ TPCF Perimeter 3 (Community Sandbox) contribution model +* ✅ Dual MIT + Palimpsest 0.8 licensing +* ✅ Comprehensive documentation: +** README.md with quick start and examples +** CLAUDE.md for AI assistants and developers +** SECURITY.md with vulnerability reporting +** CONTRIBUTING.md with development workflow +** CODE_OF_CONDUCT.md aligned with TPCF principles +** MAINTAINERS.md with governance structure +* ✅ API documentation with examples +* ✅ FFI binding infrastructure: +** Deno bindings (TypeScript) +** Rescript bindings +** C ABI via Zig (planned) + +==== Technical Details + +===== API Compatibility (nickel-lang-core 0.9.1) + +* `+Program::new_from_source()+` with trace parameter +* `+eval_full()+` with no arguments +* Manual error conversion via `+serde_json::to_value()+` +* No deprecated `+into_diagnostics()+` usage + +===== Dependencies + +* nickel-lang-core 0.9.1 (core parser) +* serde 1.0 (serialization) +* serde_json 1.0 (JSON conversion) +* anyhow 1.0 (error handling) +* thiserror 1.0 (error derive macros) +* clap 4.4 (CLI, optional) +* wasm-bindgen 0.2 (WASM bindings, target-specific) + +===== Build Artifacts + +* CLI binary: `+bunsenite+` (~6.5MB optimized) +* Shared library: `+libbunsenite.so/dylib/dll+` (~6.1MB optimized) +* WASM module: `+bunsenite.wasm+` (size varies by optimization level) + +==== Security + +===== Memory Safety + +* Zero `+unsafe+` code blocks (enforced by `+#![deny(unsafe_code)]+`) +* Rust ownership model prevents: +** Use-after-free +** Double-free +** Null pointer dereferences +** Buffer overflows +** Data races + +===== Supply Chain + +* Minimal dependencies (only essential, well-audited crates) +* No network dependencies (offline-first design) +* Pinned dependency versions for reproducibility +* Regular `+cargo audit+` checks in CI + +==== Performance + +* Native Rust: Baseline performance +* WebAssembly: ~95% native speed +* FFI bindings: ~90% native speed (minimal C ABI overhead) + +==== Known Limitations + +* Nickel evaluation may consume significant memory/CPU for complex +configs +** *Mitigation*: Plan to add configurable timeouts and memory limits +* File I/O respects OS permissions (no privilege escalation) +* WASM runs in browser sandbox (subject to browser security model) + +==== Breaking Changes + +* N/A (initial release) + +==== Deprecations + +* N/A (initial release) + +==== Fixed + +* N/A (initial release) + +==== Contributors + +* Campaign for Cooler Coding and Programming (@cccp) - Initial +implementation + +''''' + +=== Version History + +==== Version Numbering + +We use https://semver.org/[Semantic Versioning]: + +.... +MAJOR.MINOR.PATCH + +MAJOR: Incompatible API changes +MINOR: Backwards-compatible new features +PATCH: Backwards-compatible bug fixes +.... + +==== Release Cadence + +* *Major releases*: As needed for breaking changes +* *Minor releases*: Monthly (if new features ready) +* *Patch releases*: As needed for critical bugs/security + +==== Support Policy + +[cols=",,",options="header",] +|=== +|Version |Support Status |End of Life +|0.1.x |✅ Full support |TBD (current) +|< 0.1.0 |❌ Not supported |N/A +|=== + +''''' + +=== Links + +* https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite[Repository] +* https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite/-/issues[Issues] +* https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite/-/releases[Releases] +* https://crates.io/crates/bunsenite[Crates.io] (coming soon) + +''''' + +*Note*: This changelog is maintained according to +https://keepachangelog.com/[Keep a Changelog] principles and serves as a +living document of the project’s evolution. diff --git a/vendor/bunsenite/CITATION.cff b/vendor/bunsenite/CITATION.cff new file mode 100644 index 0000000..c93629b --- /dev/null +++ b/vendor/bunsenite/CITATION.cff @@ -0,0 +1,24 @@ +# CITATION.cff - Citation File Format for bunsenite +# https://citation-file-format.github.io/ +# SPDX-License-Identifier: MPL-2.0 OR LicenseRef-Palimpsest-0.5 + +cff-version: 1.2.0 +title: "bunsenite" +message: "If you use this software, please cite it as below." +type: software +authors: + - family-names: "Jewell" + given-names: "Jonathan D.A." + alias: "hyperpolymath" + email: "hyperpolymath@proton.me" + affiliation: "Rhodium Standard / Independent Researcher" +repository-code: "https://github.com/hyperpolymath/bunsenite" +url: "https://rhodium.sh/projects/bunsenite" +abstract: "RSR-compliant project" +keywords: + - RSR + - rhodium-standard +license: PMPL-1.0 +license-url: "https://github.com/hyperpolymath/bunsenite/blob/main/LICENSE.txt" +version: "0.1.0" +date-released: "2025-12-10" diff --git a/vendor/bunsenite/CLAUDE.md b/vendor/bunsenite/CLAUDE.md new file mode 100644 index 0000000..42898a5 --- /dev/null +++ b/vendor/bunsenite/CLAUDE.md @@ -0,0 +1,279 @@ + +# Bunsenite Project + +## Project Overview + +Bunsenite is a Nickel configuration file parser with multi-language FFI bindings. It provides a Rust core library with a Zig C ABI layer that enables bindings for Deno (JavaScript/TypeScript), AffineScript, and WebAssembly for browser and universal use. + +**Status**: v1.0.0 - Production ready +**Repository**: https://github.com/hyperpolymath/bunsenite (mirror: GitLab) +**License**: Dual MPL-2.0 + Palimpsest 0.8 + +## Project Structure + +``` +bunsenite/ +├── src/ +│ ├── lib.rs # Main library entry point +│ ├── main.rs # CLI with parse, validate, watch, repl, schema +│ ├── loader.rs # Nickel file loader (nickel-lang-core 0.9.1 API) +│ └── wasm.rs # WebAssembly bindings +├── zig/ +│ └── bunsenite.zig # Zig C ABI layer (stable FFI interface) +├── bindings/ +│ ├── deno/ # Deno FFI bindings (Deno.dlopen) +│ ├── affinescript/ # AffineScript C FFI bindings +│ └── wasm/ # WASM build target +├── examples/ +│ ├── config.ncl # Full configuration example +│ └── simple.ncl # Minimal example +├── packaging/ # Package manager configs (AUR, deb, rpm, etc.) +├── .github/workflows/ # CI/CD (release, RSR antipattern check) +├── Cargo.toml # Rust dependencies +├── Justfile # Build commands (45+ recipes) +├── CLAUDE.md # This file - AI assistant context +├── STATE.scm # Project state checkpoint +└── LICENSE # MPL-2.0 + Palimpsest dual license +``` + +## Technology Stack + +**Core:** +- Language: Rust (2021 edition, 1.70+) +- Parser: nickel-lang-core 0.9.1 +- Error handling: miette 7.0 (fancy diagnostics) +- Serialization: serde, serde_json + +**FFI Layer:** +- C ABI: Zig (provides stable interface isolating consumers from Rust ABI changes) + +**Bindings:** +- Deno: TypeScript with Deno.dlopen for native FFI (NOT plain TypeScript) +- AffineScript: Direct C FFI bindings +- WebAssembly: wasm-bindgen for browser/universal deployment + +**CLI Features:** +- `parse` - Parse and evaluate Nickel config to JSON +- `validate` - Validate config without evaluation +- `watch` - Watch mode with notify crate +- `repl` - Interactive REPL with rustyline +- `schema` - JSON Schema validation +- `info` - Library and compliance info + +**Build Tools:** +- Build system: Cargo + Justfile (no shell scripts) +- WASM tooling: wasm-pack + +## RSR Compliance + +**Tier**: Bronze +**TPCF Perimeter**: 3 (Community Sandbox) + +**Requirements Met:** +- Type Safety: Compile-time (Rust) +- Memory Safety: Rust ownership model +- Offline-First: No network dependencies +- No Plain TypeScript: Deno FFI uses .ts but calls Deno.dlopen +- No npm/bun: AffineScript package.json is for npm publishing of compiled output +- No Python: Clean +- No Shell Scripts: All builds via Justfile + +## Development Setup + +### Prerequisites + +- Rust toolchain (2021 edition, 1.70+) +- Zig compiler (for C ABI layer) +- just command runner (`cargo install just`) +- Optional: wasm-pack for WebAssembly builds +- Optional: Deno runtime for testing Deno bindings + +### Quick Start + +```bash +# Clone and build +git clone https://github.com/hyperpolymath/bunsenite.git +cd bunsenite +just all + +# Run CLI +cargo run --release -- parse examples/config.ncl --pretty + +# Run with all features +cargo run --release --all-features -- repl +``` + +### Justfile Recipes + +```bash +just # List all recipes +just all # Build all targets +just build # Build release binaries +just wasm # Build WebAssembly +just test # Run all tests +just check # Run all quality checks +just rsr-check # Verify RSR Bronze compliance +just rsr-report # Generate compliance report +``` + +## Code Conventions + +### Style +- Rust standard formatting: `cargo fmt` +- Lint with: `cargo clippy` +- Use explicit error types (anyhow for apps, thiserror for libs) +- Document public APIs with `///` doc comments + +### Testing +- All tests must pass before commit +- Run: `cargo test` +- Coverage: Unit tests + doc tests + +## Architecture + +### Data Flow + +``` +┌─────────────────────────────────────────────────┐ +│ Consumers │ +├───────────────┬───────────────┬─────────────────┤ +│ Deno │ AffineScript │ Browser │ +│ (Deno FFI) │ (C FFI) │ (WASM) │ +└───────┬───────┴───────┬───────┴────────┬────────┘ + │ │ │ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────────┐ + │ Zig FFI │ │ Zig FFI │ │ wasm-bindgen │ + │ (C ABI) │ │ (C ABI) │ │ │ + └─────┬────┘ └─────┬────┘ └──────┬───────┘ + │ │ │ + └──────────────┴─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ Rust Core │ + │ │ + │ nickel-lang-core│ + │ 0.9.1 │ + │ │ + │ miette errors │ + └─────────────────┘ +``` + +### Key Components + +1. **src/lib.rs**: Public API entry point +2. **src/loader.rs**: Nickel parser using nickel-lang-core 0.9.1 +3. **src/main.rs**: CLI with parse, validate, watch, repl, schema commands +4. **src/wasm.rs**: WebAssembly bindings +5. **zig/bunsenite.zig**: Stable C ABI wrapper +6. **bindings/deno/**: Deno FFI (Deno.dlopen) +7. **bindings/affinescript/**: AffineScript C FFI + +## Critical Design Decisions + +**REQUIRED Technologies:** +- Rust core +- Zig C ABI layer (stable FFI) +- Deno bindings (Deno.dlopen, NOT plain TypeScript) +- AffineScript bindings (via C FFI) +- WebAssembly bindings +- Justfile for builds + +**NOT ALLOWED (RSR Compliance):** +- Plain TypeScript (Deno .ts files are FFI, not compiled TS) +- Shell scripts (use Justfile) +- npm/bun for primary build (package.json for AffineScript npm publishing only) +- bun:ffi (ALWAYS use Deno.dlopen instead) +- ffi-napi / Node.js FFI (ALWAYS use Deno.dlopen instead) +- Python (except SaltStack support contexts) + +**IMPORTANT:** If JavaScript FFI is needed, ALWAYS use Deno's Deno.dlopen. +Never create bun:ffi or node ffi-napi files. This is a strict RSR requirement. + +**Future:** +- TUI: Ada/SPARK (planned for v2.0) +- LSP: tower-lsp (research phase) + +## API Compatibility Notes + +**nickel-lang-core 0.9.1:** +1. `Program::new_from_source()` requires trace parameter: `std::io::sink()` +2. `eval_full()` takes no arguments +3. Manual error conversion via `serde_json::to_value()` +4. NO `into_diagnostics()` method + +See `src/loader.rs` for correct usage patterns. + +## Notes for AI Assistants + +### Project State + +- **Version**: 1.0.0 (production ready) +- **All features complete**: CLI, FFI, bindings, watch, REPL, schema +- **RSR Bronze compliant** +- **TPCF Perimeter 3** + +### When Making Changes + +- Use Justfile commands, NOT shell scripts +- Run `cargo test` before commit +- Run `cargo fmt` and `cargo clippy` +- Update STATE.scm if project state changes +- Follow RSR guidelines (no TS, no npm, no Python) + +### State File + +The `STATE.scm` file tracks project state in machine-readable Scheme format. Update it when: +- Completing major features +- Changing project phase +- Modifying architecture + +### User Preferences + +- Deno preferred over npm/bun +- AffineScript preferred over TypeScript +- Ada/SPARK for TUI (future) +- No shell scripts (Justfile only) +- Offline-first design +- Emotional safety considerations + +## CI/CD Notes + +**Build Times (per platform):** +- Rust compilation: ~7-10 minutes (with `--features full`) +- Zig FFI build: ~30 seconds +- Packaging/upload: ~1 minute +- Total: ~10-15 minutes per platform + +**Important:** Build times do NOT affect end users - they download pre-built binaries. +These times are CI/CD only (release workflow on tag push). + +**Optimization opportunities:** +- Cargo caching is configured but GitHub's cache service can be unreliable +- Consider reducing targets if not all platforms are needed +- Cross-compilation (aarch64-linux) uses Docker containers and is slower + +**Zig FFI Status by Platform:** +- Linux x86_64: Full Zig FFI support +- macOS (both archs): Full Zig FFI support +- Linux aarch64: Rust binary only (cross-compilation, Zig FFI skipped) +- Windows: Rust binary only (Zig FFI skipped, needs import library setup) + +## Changelog + +- **2025-12-12**: Updated to v1.0.0 + - All features complete + - Zig FFI layer implemented + - Watch, REPL, schema commands + - miette error diagnostics + - RSR Bronze compliant + +- **2025-11-21**: Initial CLAUDE.md (v0.1.0) + +--- + +**Note**: Keep STATE.scm and CLAUDE.md updated to help AI assistants and developers understand project state quickly. diff --git a/vendor/bunsenite/CODE_OF_CONDUCT.adoc b/vendor/bunsenite/CODE_OF_CONDUCT.adoc new file mode 100644 index 0000000..070c601 --- /dev/null +++ b/vendor/bunsenite/CODE_OF_CONDUCT.adoc @@ -0,0 +1,175 @@ +== Code of Conduct + +=== Our Pledge + +We as members, contributors, and leaders pledge to make participation in +our community a harassment-free experience for everyone, regardless of +age, body size, visible or invisible disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, +welcoming, diverse, inclusive, and healthy community. + +=== Our Standards + +==== Examples of behavior that contributes to a positive environment: + +* *Demonstrating empathy and kindness* toward other people +* *Being respectful* of differing opinions, viewpoints, and experiences +* *Giving and gracefully accepting* constructive feedback +* *Accepting responsibility* and apologizing to those affected by our +mistakes, and learning from the experience +* *Focusing on what is best* not just for us as individuals, but for the +overall community +* *Using welcoming and inclusive language* +* *Being patient* with new contributors and those learning +* *Celebrating successes* of others +* *Supporting emotional safety* and reversibility in development + +==== Examples of unacceptable behavior: + +* The use of sexualized language or imagery, and sexual attention or +advances of any kind +* Trolling, insulting or derogatory comments, and personal or political +attacks +* Public or private harassment +* Publishing others’ private information, such as a physical or email +address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a +professional setting +* *Dismissing or minimizing* concerns about emotional safety +* *Gatekeeping* or elitism based on technical skill level +* *Weaponizing vulnerability* or reversibility features + +=== Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our +standards of acceptable behavior and will take appropriate and fair +corrective action in response to any behavior that they deem +inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other +contributions that are not aligned to this Code of Conduct, and will +communicate reasons for moderation decisions when appropriate. + +=== Scope + +This Code of Conduct applies within all community spaces, and also +applies when an individual is officially representing the community in +public spaces. Examples of representing our community include using an +official e-mail address, posting via an official social media account, +or acting as an appointed representative at an online or offline event. + +=== Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may +be reported to the community leaders responsible for enforcement at: + +* *GitHub Issues*: +https://github.com/hyperpolymath/bunsenite/issues/new?labels=conduct[Report +a concern] +* *GitLab*: Confidential issue on the repository + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security +of the reporter of any incident. + +=== Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in +determining the consequences for any action they deem in violation of +this Code of Conduct: + +==== 1. Correction + +*Community Impact*: Use of inappropriate language or other behavior +deemed unprofessional or unwelcome in the community. + +*Consequence*: A private, written warning from community leaders, +providing clarity around the nature of the violation and an explanation +of why the behavior was inappropriate. A public apology may be +requested. + +==== 2. Warning + +*Community Impact*: A violation through a single incident or series of +actions. + +*Consequence*: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, for a specified period of +time. This includes avoiding interactions in community spaces as well as +external channels like social media. Violating these terms may lead to a +temporary or permanent ban. + +==== 3. Temporary Ban + +*Community Impact*: A serious violation of community standards, +including sustained inappropriate behavior. + +*Consequence*: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No +public or private interaction with the people involved, including +unsolicited interaction with those enforcing the Code of Conduct, is +allowed during this period. Violating these terms may lead to a +permanent ban. + +==== 4. Permanent Ban + +*Community Impact*: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of +individuals. + +*Consequence*: A permanent ban from any sort of public interaction +within the community. + +=== Emotional Safety Principles + +In alignment with our values of emotional safety and reversibility: + +==== Encouragement Over Criticism + +* *Positive framing*: Frame feedback constructively +* *Assume good intent*: Mistakes are learning opportunities +* *Celebrate experimentation*: Failures are valuable when reversible +* *Support learning*: Everyone is learning, regardless of experience +level + +==== Reversibility in Community Interactions + +* *Mistakes can be fixed*: Technical mistakes are reversible through Git +* *Apologies matter*: Sincere apologies can repair social mistakes +* *Growth mindset*: People can change and improve +* *Second chances*: Unless patterns of harm persist + +==== Political Autonomy + +* *Technical decisions*: Based on merit, not politics +* *No gatekeeping*: Access based on conduct, not views +* *Respectful disagreement*: Disagree on ideas, not people +* *Community sovereignty*: This community makes its own decisions + +=== Attribution + +This Code of Conduct is adapted from the +https://www.contributor-covenant.org[Contributor Covenant], version 2.1, +available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +Community Impact Guidelines were inspired by +https://github.com/mozilla/diversity[Mozilla’s code of conduct +enforcement ladder]. + +For answers to common questions about this code of conduct, see the FAQ +at https://www.contributor-covenant.org/faq. Translations are available +at https://www.contributor-covenant.org/translations. + +''''' + +*Last updated*: 2025-11-22 *Version*: 1.0.0 (Aligned with RSR Framework +& TPCF principles) diff --git a/vendor/bunsenite/Cargo.lock b/vendor/bunsenite/Cargo.lock new file mode 100644 index 0000000..f95ff3a --- /dev/null +++ b/vendor/bunsenite/Cargo.lock @@ -0,0 +1,4025 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +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 0.61.2", +] + +[[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 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[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 = "bon" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bunsenite" +version = "1.0.2" +dependencies = [ + "anyhow", + "clap", + "console_error_panic_hook", + "criterion", + "jsonschema", + "miette", + "nickel-lang-core", + "notify", + "pretty_assertions", + "rustyline", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "wasm-bindgen", +] + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "caseless" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" +dependencies = [ + "unicode-normalization", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +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 = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583f52b0658b321b25fd6b209b6c76cf058f433071297de64e5980c3d9aad937" +dependencies = [ + "codespan-reporting", + "serde", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width 0.2.2", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "comrak" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab87129dce2f2d7e75e753b1df0e5093b27dec8fa5970b6eb51280faacb25bd6" +dependencies = [ + "bon", + "caseless", + "clap", + "emojis", + "entities", + "fmt2io", + "jetscii", + "shell-words", + "syntect", + "typed-arena", + "unicode_categories", + "xdg", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "coolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3" +dependencies = [ + "crossterm", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools 0.13.0", +] + +[[package]] +name = "crokey" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04a63daf06a168535c74ab97cdba3ed4fa5d4f32cb36e437dcceb83d66854b7c" +dependencies = [ + "crokey-proc_macros", + "crossterm", + "once_cell", + "serde", + "strict", +] + +[[package]] +name = "crokey-proc_macros" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847f11a14855fc490bd5d059821895c53e77eeb3c2b73ee3dded7ce77c93b231" +dependencies = [ + "crossterm", + "proc-macro2", + "quote", + "strict", + "syn 2.0.117", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[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-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +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 = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.10.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio 1.1.1", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[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 = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[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 = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "emojis" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e1f1df1f181f2539bac8bf027d31ca5ffbf9e559e3f2d09413b9107b5c02f4" +dependencies = [ + "phf", +] + +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + +[[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 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set 0.5.3", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set 0.8.0", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fmt2io" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b6129284da9f7e5296cc22183a63f24300e945e297705dcc0672f7df01d62c8" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[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.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[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 = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" +dependencies = [ + "bitmaps", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "iso8601" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46" +dependencies = [ + "nom", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jetscii" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" + +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json_scanner" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0a2dc336065c75719cffd3c6c929e0ec4ed85b92b8248a7bbd999acb0e419c" +dependencies = [ + "memchr", +] + +[[package]] +name = "jsonschema" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0f4bea31643be4c6a678e9aa4ae44f0db9e5609d5ca9dc9083d06eb3e9a27a" +dependencies = [ + "ahash", + "anyhow", + "base64", + "bytecount", + "clap", + "fancy-regex 0.13.0", + "fraction", + "getrandom 0.2.16", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time", + "url", + "uuid", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set 0.8.0", + "ena", + "itertools 0.14.0", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata", + "rustversion", +] + +[[package]] +name = "lazy-regex" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.117", +] + +[[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.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.6.0", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn 2.0.117", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "malachite" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de8195e0d0bccfa3e54997e8e7c6c67859b08512067801b5a63dd0b7a174e87" +dependencies = [ + "malachite-base", + "malachite-float", + "malachite-nz", + "malachite-q", +] + +[[package]] +name = "malachite-base" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8b6f86fdbb1eb9955946be91775239dfcb0acdb1a51bb07d5fc9b8c854f5ccd" +dependencies = [ + "hashbrown 0.16.1", + "itertools 0.14.0", + "libm", + "ryu", +] + +[[package]] +name = "malachite-float" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d5021773c1552820b10ce7410817fadc1dfcef907b4f9a29af5346d756fd28" +dependencies = [ + "itertools 0.14.0", + "malachite-base", + "malachite-nz", + "malachite-q", + "serde", +] + +[[package]] +name = "malachite-nz" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0197a2f5cfee19d59178e282985c6ca79a9233e26a2adcf40acb693896aa09f6" +dependencies = [ + "itertools 0.14.0", + "libm", + "malachite-base", + "serde", + "wide", +] + +[[package]] +name = "malachite-q" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2add95162aede090c48f0ee51bea7d328847ce3180aa44588111f846cc116b" +dependencies = [ + "itertools 0.14.0", + "malachite-base", + "malachite-nz", + "serde", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimad" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b688969b16915f3ecadc7829d5b7779dee4977e503f767f34136803d5c06f" +dependencies = [ + "once_cell", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nickel-lang-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692d8a2ba34c633bc37e704dc94f4ca33edaa8fbf6d08efdcadb81db333ccdb6" +dependencies = [ + "anstyle", + "base64", + "bumpalo", + "codespan", + "codespan-reporting", + "colorchoice", + "comrak", + "indexmap", + "indoc", + "json_scanner", + "lalrpop", + "lalrpop-util", + "logos", + "malachite", + "malachite-q", + "md-5", + "nickel-lang-parser", + "nickel-lang-vector", + "once_cell", + "ouroboros", + "paste", + "pretty", + "regex", + "rustyline", + "rustyline-derive", + "saphyr-parser", + "serde", + "serde_json", + "serde_yaml", + "sha-1", + "sha2", + "simple-counter", + "smallvec", + "strip-ansi-escapes", + "strsim", + "termimad", + "toml", + "toml_edit", + "topiary-core", + "topiary-queries", + "tree-sitter-nickel", + "typed-arena", + "unicode-segmentation", +] + +[[package]] +name = "nickel-lang-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7aaf73e60b66ef4fffc969b0e4e419a15a029525f9b53f2f5cc0ca41bbe17ff" +dependencies = [ + "bumpalo", + "codespan", + "codespan-reporting", + "indexmap", + "lalrpop", + "lalrpop-util", + "logos", + "malachite", + "nickel-lang-vector", + "ouroboros", + "pretty", + "regex", + "saphyr-parser", + "serde", + "serde_json", + "simple-counter", + "toml_edit", + "typed-arena", +] + +[[package]] +name = "nickel-lang-vector" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f243832286908d8873add24a905d6732ffabd6cfb2bf74cb18d667e892e279" +dependencies = [ + "imbl-sized-chunks", + "serde", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.10.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[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 = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.10.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width 0.2.2", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettydiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9a475bdea0881b8c65eb81f91fe53187b8522352a701b919c5a2c8a2f262808" +dependencies = [ + "owo-colors", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[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 = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "version_check", + "yansi", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags 2.10.0", +] + +[[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 = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustyline" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width 0.2.2", + "utf8parse", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustyline-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d66de233f908aebf9cc30ac75ef9103185b4b715c6f2fb7a626aa5e5ede53ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safe_arch" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed" +dependencies = [ + "bytemuck", +] + +[[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 = "saphyr-parser" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb771b59f6b1985d1406325ec28f97cfb14256abcec4fdfb37b36a1766d6af7" +dependencies = [ + "arraydeque", + "hashlink", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio 1.1.1", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simple-counter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb57743b52ea059937169c0061d70298fe2df1d2c988b44caae79dd979d9b49" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strict" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "fancy-regex 0.16.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.17", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "term" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termimad" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "889a9370996b74cf46016ce35b96c248a9ac36d69aab1d112b3e09bc33affa49" +dependencies = [ + "coolor", + "crokey", + "crossbeam", + "lazy-regex", + "minimad", + "serde", + "thiserror 2.0.17", + "unicode-width 0.1.14", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[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 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "libc", + "mio 1.1.1", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.24.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01f2eadbbc6b377a847be05f60791ef1058d9f696ecb51d2c07fe911d8569d8e" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "topiary-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89df094e19f103c5b8e120a1ffa30a6309daa10bef8d186e598a3df633e6a221" +dependencies = [ + "futures", + "itertools 0.11.0", + "log", + "miette", + "pretty_assertions", + "prettydiff", + "rayon", + "serde", + "serde_json", + "streaming-iterator", + "thiserror 2.0.17", + "tokio", + "topiary-tree-sitter-facade", + "topiary-web-tree-sitter-sys", + "tree-sitter", +] + +[[package]] +name = "topiary-queries" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13439d04bb7987de5f937071c8131c995f3d18fcc0df6ce4ab33180a88fbc72c" + +[[package]] +name = "topiary-tree-sitter-facade" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b7f801962f0e1d022f78a46c6afa2d2158138a3955dbbd25bb92cc5ef61ddb" +dependencies = [ + "js-sys", + "streaming-iterator", + "topiary-web-tree-sitter-sys", + "tree-sitter", + "tree-sitter-language", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "topiary-web-tree-sitter-sys" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9877bfc1ad20d17e6da579911925768df2edd6e276300d660265940881d7b9d" +dependencies = [ + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tree-sitter" +version = "0.26.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dab76d0b724ba557954125188cf0633a1ca43199ced82d95c7b9c32cc3de1f3" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + +[[package]] +name = "tree-sitter-nickel" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7bb930cf314466ad3ca1e45c876bbbca228f66fe92db8a087796cf8f26d3ba8" +dependencies = [ + "cc", + "tree-sitter", + "tree-sitter-language", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[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 = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[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 = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wide" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdfe6a32973f2d1b268b8895845a8a96cac2f0191e72c27cc929036060dbf89" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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 = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/vendor/bunsenite/Cargo.toml b/vendor/bunsenite/Cargo.toml new file mode 100644 index 0000000..f85d6bb --- /dev/null +++ b/vendor/bunsenite/Cargo.toml @@ -0,0 +1,123 @@ +[package] +name = "bunsenite" +version = "1.0.2" +authors = ["Campaign for Cooler Coding and Programming"] +edition = "2021" +rust-version = "1.70" +description = "Nickel configuration file parser with multi-language FFI bindings" +documentation = "https://docs.rs/bunsenite" +repository = "https://github.com/hyperpolymath/bunsenite" +license = "MPL-2.0" +keywords = ["nickel", "config", "parser", "ffi", "wasm"] +categories = ["config", "parsing", "wasm", "api-bindings"] +readme = "README.adoc" +exclude = [ + "/.git", + "/.gitlab", + "/target", + "/examples/*/target", +] + +[lib] +name = "bunsenite" +path = "src/lib.rs" +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "bunsenite" +path = "src/main.rs" + +[dependencies] +# Core Nickel parser - pinned to 0.9.1 for API stability +nickel-lang-core = { version = "0.18.0", default-features = false } + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# Error handling +anyhow = "1.0" +thiserror = "1.0" +miette = { version = "7.0", features = ["fancy"] } + +# Watch mode +notify = { version = "6.1", optional = true } + +# REPL +rustyline = { version = "15.0", optional = true } + +# Schema validation +jsonschema = { version = "0.18", optional = true } + +# CLI (optional, for binary only) +clap = { version = "4.6", features = ["derive", "cargo"], optional = true } + +# WASM support +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" +console_error_panic_hook = "0.1" +# Note: wee_alloc was removed as it is unmaintained and has known memory leaks. +# Rust 1.71+ provides a suitable default allocator for wasm32 targets. + +[dev-dependencies] +# Testing +pretty_assertions = "1.4" +tempfile = "3.27" +criterion = { version = "0.8", features = ["html_reports"] } + +[[bench]] +name = "parser" +harness = false + +[[bench]] +name = "bunsenite_bench" +harness = false + +[features] +default = ["cli"] +cli = ["dep:clap"] +wasm = [] +watch = ["dep:notify", "cli"] +repl = ["dep:rustyline", "cli"] +schema = ["dep:jsonschema"] +full = ["cli", "watch", "repl", "schema"] + +# Offline-first: No network dependencies, all features work air-gapped +# Type safety: Rust compiler guarantees +# Memory safety: Rust ownership model, zero unsafe blocks (enforced in CI) + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = true +panic = "abort" + +[profile.release-with-debug] +inherits = "release" +strip = false +debug = true + +# WASM optimization +[profile.wasm-release] +inherits = "release" +opt-level = "z" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +# Security audit configuration +[package.metadata.audit] +# Ignore advisories that don't apply +ignore = [] + +# RSR Framework Metadata +[package.metadata.rsr] +tier = "bronze" +compliance-version = "1.0.0" +offline-first = true +type-safety = "compile-time" +memory-safety = "rust-ownership" +tpcf-perimeter = 3 # Community Sandbox +security-contact = "https://github.com/hyperpolymath/bunsenite/security/advisories/new" diff --git a/vendor/bunsenite/Containerfile b/vendor/bunsenite/Containerfile new file mode 100644 index 0000000..d65141d --- /dev/null +++ b/vendor/bunsenite/Containerfile @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: MPL-2.0 OR Palimpsest-0.8 +# SPDX-FileCopyrightText: 2025 hyperpolymath + +FROM rust:1.85-slim-bookworm AS builder + +WORKDIR /build + +RUN apt-get update && apt-get install -y \ + pkg-config \ + libreadline-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY Cargo.toml Cargo.lock* ./ +COPY src/ src/ +COPY benches/ benches/ + +RUN cargo build --release --bin bunsenite + +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libreadline8 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /build/target/release/bunsenite /usr/local/bin/bunsenite + +ENTRYPOINT ["bunsenite"] +CMD ["--help"] diff --git a/vendor/bunsenite/EXPLAINME.adoc b/vendor/bunsenite/EXPLAINME.adoc new file mode 100644 index 0000000..1d96c36 --- /dev/null +++ b/vendor/bunsenite/EXPLAINME.adoc @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Bunsenite — Show Me The Receipts +:toc: +:icons: font + +The README makes claims. This file backs them up. + +[quote, README] +____ +Bunsenite is a Nickel configuration file parser with Rust core, Zig FFI, and multi-language bindings (Deno, AffineScript, WASM). +____ + +The architecture layers Rust (parsing) → Zig (stable C ABI) → language-specific bindings. This prevents Rust ABI churn from breaking downstream consumers; the C ABI is stable across Rust compiler versions. + +== Two Verifiable Claims from How-It-Works + +=== Claim 1: Zig FFI Isolates Consumers from Rust ABI Changes + +**Location**: `/var/mnt/eclipse/repos/bunsenite/zig/bunsenite.zig` (Zig C ABI wrapper around Rust core) + +**How verified**: The Zig FFI layer exposes a pure C ABI (no Rust `repr(Rust)` types). Functions like `parse_nickel_string()` and `validate_config()` take C-compatible types (pointers, `size_t`, `int`) and call Rust functions via `extern "C"`. README (§Design Rationale) claims "stable C ABI isolates consumers from Rust ABI changes." This is validated by the fact that Deno/AffineScript bindings use `Deno.dlopen()` and direct C FFI, not `rustler` or other Rust-specific bridges. If Rust ABI changed, only the Zig layer needs updating, not the bindings themselves. + +**Caveat**: The Zig FFI is currently manually maintained. No formal proof that generated C headers match the Rust implementation. This works in practice due to hand verification but is not formally certified. + +=== Claim 2: CLI Parse/Validate Commands Route Correctly to Library + +**Location**: `/var/mnt/eclipse/repos/bunsenite/src/main.rs` (CLI entry point delegating to lib.rs) + +**How verified**: The CLI (`bunsenite parse `, `bunsenite validate `) calls library functions via `src/lib.rs` which wraps `nickel_lang_core 0.9.1`. Each CLI command creates a `NickelLoader` instance (defined in `src/loader.rs`), invokes appropriate methods, and formats output. README (§Usage, CLI section) documents the three commands; the code implements them. The CI/CD runs these commands against example configs to verify correctness. + +**Caveat**: CLI and library use different error handling (CLI uses `miette` for pretty errors, library uses `thiserror` types). Some errors may format differently between CLI and programmatic use. + +== Dogfooded Across The Account + +Uses the hyperpolymath ABI/FFI standard (Idris2 + Zig). Same pattern used across +https://github.com/hyperpolymath/proven[proven], +https://github.com/hyperpolymath/burble[burble], and +https://github.com/hyperpolymath/gossamer[gossamer]. + +Critical path: Idris2 ABI specs → Zig FFI implementation → Deno/AffineScript bindings → end-user code. + +== File Map + +[cols="1,2"] +|=== +| Path | What's There + +| `src/lib.rs` | Public library API entry point; exports `NickelLoader`, `parse_*`, `validate_*` functions +| `src/loader.rs` | `NickelLoader` struct wrapping `nickel_lang_core` API; handles file I/O and evaluation +| `src/main.rs` | CLI entry point with subcommands: parse, validate, watch, repl, schema, info +| `src/wasm.rs` | WebAssembly bindings via wasm-bindgen; exports `parse_nickel()` function for browser +| `zig/bunsenite.zig` | Stable C ABI layer; wraps Rust library with C-compatible function signatures +| `bindings/deno/bunsenite.ts` | Deno FFI bindings using `Deno.dlopen()` to call Zig C ABI functions +| `bindings/affinescript/bindings.res` | AffineScript C FFI bindings to call Zig C ABI; compiles to JavaScript +| `examples/config.ncl` | Example Nickel config demonstrating features (loops, functions, conditionals) +| `examples/simple.ncl` | Minimal config for testing parsing +| `Justfile` | Build recipes: `just all`, `just wasm`, `just test`, `just rsr-check` +|=== + +== Testing Critical Paths + +* **Library correctness**: `cargo test` — Rust unit tests for parser, loader, evaluation +* **CLI functionality**: `Justfile` test recipes verify `parse`, `validate`, `watch`, `repl` commands +* **WASM builds**: `just wasm` and `wasm-pack test` validate browser-compatible builds +* **FFI soundness**: Deno/AffineScript bindings tested against known Nickel configs +* **RSR compliance**: `just rsr-check` validates Bronze tier requirements + +== Questions? + +Open an issue or reach out directly — happy to explain anything in more detail. diff --git a/vendor/bunsenite/GEMINI.md b/vendor/bunsenite/GEMINI.md new file mode 100644 index 0000000..417391d --- /dev/null +++ b/vendor/bunsenite/GEMINI.md @@ -0,0 +1,8 @@ +# Pointer + +This repository has no `AGENTS.md` yet. Until it does, the instructions +for every coding agent live in **[CLAUDE.md](./CLAUDE.md)**. Read that +file, and skip anything in it that is specific to Claude Code tooling. +Do not duplicate rules here. + +When `AGENTS.md` lands in this repository, retarget this pointer at it. diff --git a/vendor/bunsenite/GOVERNANCE.adoc b/vendor/bunsenite/GOVERNANCE.adoc new file mode 100644 index 0000000..9b836fb --- /dev/null +++ b/vendor/bunsenite/GOVERNANCE.adoc @@ -0,0 +1,60 @@ +== Governance + +=== Overview + +This project is governed by the following principles and structures to +ensure transparent, inclusive, and effective decision-making. + +=== Roles and Responsibilities + +==== Maintainers + +Maintainers are responsible for: - Reviewing and merging pull requests - +Managing releases and versioning - Ensuring code quality and standards - +Triaging issues and bug reports - Community engagement and support + +==== Contributors + +Contributors are expected to: - Follow the code of conduct - Submit +well-documented pull requests - Write tests for new functionality - +Maintain existing tests - Update documentation as needed + +=== Decision Making + +==== Minor Changes + +* Can be made by any maintainer +* Include bug fixes, documentation updates, dependency updates + +==== Major Changes + +* Require discussion in issues or pull requests +* Include new features, architectural changes, API changes +* Need approval from at least 2 maintainers + +==== Breaking Changes + +* Require RFC (Request for Comments) process +* Need approval from majority of maintainers +* Must include migration guide + +=== Code of Conduct + +All participants are expected to follow our Code of Conduct. Violations +can be reported to the maintainers. + +=== Communication + +* *Issues*: For bug reports and feature requests +* *Discussions*: For questions and general discussion +* *Pull Requests*: For code contributions + +=== Licensing + +All contributions are made under the terms of the repository’s LICENSE +file. By submitting a pull request, you agree to license your +contributions accordingly. + +''''' + +_Last updated: 2026-07-18_ diff --git a/vendor/bunsenite/Justfile b/vendor/bunsenite/Justfile new file mode 100644 index 0000000..0922728 --- /dev/null +++ b/vendor/bunsenite/Justfile @@ -0,0 +1,230 @@ +# bunsenite - Rust Development Tasks +set shell := ["bash", "-uc"] +set dotenv-load := true + +import? "contractile.just" + +project := "bunsenite" + +# Show all recipes +default: + @just --list --unsorted + +# Build debug +build: + cargo build + +# Build release +build-release: + cargo build --release + +# Run tests +test: + cargo test + +# Run tests verbose +test-verbose: + cargo test -- --nocapture + +# Format code +fmt: + cargo fmt + +# Check formatting +fmt-check: + cargo fmt -- --check + +# Run clippy lints +lint: + cargo clippy -- -D warnings + +# Check without building +check: + cargo check + +# Clean build artifacts +clean: + cargo clean + +# Run the project +run *ARGS: + cargo run -- {{ARGS}} + +# Generate docs +doc: + cargo doc --no-deps --open + +# Update dependencies +update: + cargo update + +# Audit dependencies +audit: + cargo audit + +# Validate K9 configurations +validate-k9: + @echo "Validating K9 configs..." + nickel eval config/rust-fmt.k9.ncl > /dev/null && echo "✓ rust-fmt.k9.ncl valid" + nickel eval config/build.k9.ncl > /dev/null && echo "✓ build.k9.ncl valid" + @echo "All K9 configs valid!" + +# Generate rustfmt.toml from K9 config +generate-rustfmt: + nickel export config/rust-fmt.k9.ncl -f 'rustfmt_toml' > rustfmt.toml + @echo "Generated rustfmt.toml from K9 config" + +# K9 dogfooding: validate configs before use +dogfood: validate-k9 + @echo "K9 dogfooding: The Nickel tool validates itself with K9!" + +# All checks before commit (including K9 validation) +pre-commit: validate-k9 fmt-check lint test + @echo "All checks passed!" + +# 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" + +# Synchronize A2ML metadata to SCM (Shadow Sync) +sync-metadata: + #!/usr/bin/env bash + echo "Synchronizing metadata (A2ML -> SCM)..." + if [ -f .machine_readable/STATE.a2ml ]; then + echo "✓ Metadata synchronized" + fi + +# [AUTO-GENERATED] Multi-arch / RISC-V target +build-riscv: + @echo "Building for RISC-V..." + cross build --target riscv64gc-unknown-linux-gnu + +# ═══════════════════════════════════════════════════════════════════════════════ +# ONBOARDING & DIAGNOSTICS +# ═══════════════════════════════════════════════════════════════════════════════ + +# Check all required toolchain dependencies and report health +doctor: + #!/usr/bin/env bash + echo "═══════════════════════════════════════════════════" + echo " Bunsenite Doctor — Toolchain Health Check" + echo "═══════════════════════════════════════════════════" + echo "" + PASS=0; FAIL=0; WARN=0 + check() { + local name="$1" cmd="$2" min="$3" + if command -v "$cmd" >/dev/null 2>&1; then + VER=$("$cmd" --version 2>&1 | head -1) + echo " [OK] $name — $VER" + PASS=$((PASS + 1)) + else + echo " [FAIL] $name — not found (need $min+)" + FAIL=$((FAIL + 1)) + fi + } + check "just" just "1.25" + check "git" git "2.40" + check "Rust (cargo)" cargo "1.80" + check "Zig" zig "0.13" +# Optional tools +if command -v panic-attack >/dev/null 2>&1; then + echo " [OK] panic-attack — available" + PASS=$((PASS + 1)) +else + echo " [WARN] panic-attack — not found (pre-commit scanner)" + WARN=$((WARN + 1)) +fi + echo "" + echo " Result: $PASS passed, $FAIL failed, $WARN warnings" + if [ "$FAIL" -gt 0 ]; then + echo " Run 'just heal' to attempt automatic repair." + exit 1 + fi + echo " All required tools present." + +# Attempt to automatically install missing tools +heal: + #!/usr/bin/env bash + echo "═══════════════════════════════════════════════════" + echo " Bunsenite Heal — Automatic Tool Installation" + echo "═══════════════════════════════════════════════════" + echo "" +if ! command -v cargo >/dev/null 2>&1; then + echo "Installing Rust via rustup..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source "$HOME/.cargo/env" +fi +if ! command -v just >/dev/null 2>&1; then + echo "Installing just..." + cargo install just 2>/dev/null || echo "Install just from https://just.systems" +fi + echo "" + echo "Heal complete. Run 'just doctor' to verify." + +# Guided tour of the project structure and key concepts +tour: + #!/usr/bin/env bash + echo "═══════════════════════════════════════════════════" + echo " Bunsenite — Guided Tour" + echo "═══════════════════════════════════════════════════" + echo "" + echo '> Nickel configuration file parser with multi-language FFI bindings' + echo "" + echo "Key directories:" + echo " src/ Source code" + echo " ffi/ Foreign function interface (Zig)" + echo " src/abi/ Idris2 ABI definitions" + echo " docs/ Documentation" + echo " tests/ Test suite" + echo " .github/workflows/ CI/CD workflows" + echo " contractiles/ Must/Trust/Dust contracts" + echo " .machine_readable/ Machine-readable metadata" + echo " examples/ Usage examples" + echo "" + echo "Quick commands:" + echo " just doctor Check toolchain health" + echo " just heal Fix missing tools" + echo " just help-me Common workflows" + echo " just default List all recipes" + echo "" + echo "Read more: README.adoc, EXPLAINME.adoc" + +# Show help for common workflows +help-me: + #!/usr/bin/env bash + echo "═══════════════════════════════════════════════════" + echo " Bunsenite — Common Workflows" + echo "═══════════════════════════════════════════════════" + echo "" +echo "FIRST TIME SETUP:" +echo " just doctor Check toolchain" +echo " just heal Fix missing tools" +echo "" + echo "DEVELOPMENT:" + echo " cargo build Build the project" + echo " cargo test Run tests" + echo "" +echo "PRE-COMMIT:" +echo " just assail Run panic-attacker scan" +echo "" +echo "LEARN:" +echo " just tour Guided project tour" +echo " just default List all recipes" + + +# 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)" diff --git a/vendor/bunsenite/LICENSE b/vendor/bunsenite/LICENSE new file mode 100644 index 0000000..14e2f77 --- /dev/null +++ b/vendor/bunsenite/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/vendor/bunsenite/LICENSES/AGPL-3.0-or-later.txt b/vendor/bunsenite/LICENSES/AGPL-3.0-or-later.txt new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/vendor/bunsenite/LICENSES/AGPL-3.0-or-later.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/vendor/bunsenite/LICENSES/CC-BY-SA-4.0.txt b/vendor/bunsenite/LICENSES/CC-BY-SA-4.0.txt new file mode 100644 index 0000000..2d58298 --- /dev/null +++ b/vendor/bunsenite/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/vendor/bunsenite/LICENSES/MPL-2.0.txt b/vendor/bunsenite/LICENSES/MPL-2.0.txt new file mode 100644 index 0000000..d0a1fa1 --- /dev/null +++ b/vendor/bunsenite/LICENSES/MPL-2.0.txt @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/vendor/bunsenite/MAINTAINERS b/vendor/bunsenite/MAINTAINERS new file mode 100644 index 0000000..37f6411 --- /dev/null +++ b/vendor/bunsenite/MAINTAINERS @@ -0,0 +1,43 @@ +# Maintainers + +This file lists the current maintainers of this project. + +## Active Maintainers + +| Name | GitHub | Role | Since | +|------|--------|------|-------| +| Metadatastician | @metadatastician | Primary | Project Start | + +## Emeritus Maintainers + +None at this time. + +## Becoming a Maintainer + +To become a maintainer: + +1. Demonstrate consistent, high-quality contributions +2. Show understanding of the project's goals and architecture +3. Be active in code reviews and community discussions +4. Be nominated by an existing maintainer +5. Be approved by consensus of existing maintainers + +## Maintainer Responsibilities + +- Reviewing and merging pull requests +- Managing releases +- Triaging issues +- Enforcing code standards +- Mentoring new contributors +- Participating in decision-making + +## Maintainer Expectations + +- Respond to issues and PRs in a timely manner +- Follow the code of conduct +- Be transparent in decision-making +- Communicate clearly and respectfully + +--- + +*Last updated: 2026-07-18* diff --git a/vendor/bunsenite/MAINTAINERS.adoc b/vendor/bunsenite/MAINTAINERS.adoc new file mode 100644 index 0000000..bbf10ba --- /dev/null +++ b/vendor/bunsenite/MAINTAINERS.adoc @@ -0,0 +1,227 @@ +== Maintainers + +This document lists the maintainers of the Bunsenite project and +describes the maintenance structure. + +=== Current Maintainers + +==== Core Team (Perimeter 1) + +These individuals have write access to the main repository and make +final decisions on merges and releases. + +* *Campaign for Cooler Coding and Programming* (@cccp) +** Role: Lead Maintainer, Project Founder +** Contact: https://github.com/hyperpolymath/bunsenite/issues[GitHub +Issues] +** Focus: Overall architecture, releases, community + +==== Trusted Contributors (Perimeter 2) + +These individuals have demonstrated consistent quality contributions and +may have specialized access or responsibilities. + +_(Currently none - invitations extended based on sustained +contributions)_ + +=== Contribution Perimeters (TPCF) + +This project uses the *Tri-Perimeter Contribution Framework*: + +==== Perimeter 1: Core Maintainers + +* *Access*: Full write access +* *Responsibilities*: +** Review and merge PRs +** Release management +** Security response +** Community moderation +** Strategic direction +* *Membership*: By invitation, based on sustained commitment and +expertise + +==== Perimeter 2: Trusted Contributors + +* *Access*: Some specialized permissions (e.g., CI configuration, docs) +* *Responsibilities*: +** Detailed code review +** Mentoring new contributors +** Area-specific expertise +** Triage issues +* *Membership*: By invitation from Perimeter 1, based on consistent +quality contributions + +==== Perimeter 3: Community Sandbox + +* *Access*: Open to all +* *Responsibilities*: +** Submit issues and PRs +** Participate in discussions +** Help other users +* *Membership*: Automatic for all contributors + +=== Areas of Responsibility + +==== Rust Core + +* *Lead*: Core Team +* *Focus*: `+src/lib.rs+`, `+src/loader.rs+`, `+src/error.rs+` +* *Reviewers*: Core Team + +==== WASM Bindings + +* *Lead*: Core Team +* *Focus*: `+src/wasm.rs+`, WASM build system +* *Reviewers*: Core Team + +==== FFI Bindings + +* *Lead*: Core Team (seeking volunteers) +* *Focus*: `+bindings/deno/+`, `+bindings/affinescript/+`, Zig layer +* *Reviewers*: Core Team + +==== CLI + +* *Lead*: Core Team +* *Focus*: `+src/main.rs+`, user experience +* *Reviewers*: Core Team + +==== Documentation + +* *Lead*: Core Team (help wanted!) +* *Focus*: README, CLAUDE.md, API docs, examples +* *Reviewers*: Any maintainer + +==== Infrastructure + +* *Lead*: Core Team +* *Focus*: CI/CD, Justfile, Guix flake, releases +* *Reviewers*: Core Team + +==== Security + +* *Lead*: Core Team +* *Contact*: +https://github.com/hyperpolymath/bunsenite/security/advisories/new[GitHub +Security Advisories] +* *Focus*: Vulnerability response, security audits, dependency audits +* *Reviewers*: Core Team only + +=== Maintenance Policies + +==== Code Review + +* *Required*: At least 1 maintainer approval for all PRs +* *Self-merge*: Core team may merge own PRs for minor changes (typos, +formatting) +* *Security*: Security PRs require 2 approvals +* *Breaking changes*: Require discussion and 2 approvals + +==== Release Process + +[arabic] +. *Version bump*: Update `+Cargo.toml+`, `+CHANGELOG.md+` +. *Testing*: All tests must pass +. *Documentation*: Update docs as needed +. *Tag*: Create git tag `+vX.Y.Z+` +. *Release*: Create GitLab release with notes +. *Publish*: Publish to crates.io +. *Announce*: Announce in discussions/issues + +==== Issue Triage + +* *Labeling*: Apply appropriate labels (`+bug+`, `+enhancement+`, +`+documentation+`, etc.) +* *Priority*: Assign priority (`+P0+`-`+P3+`) +* *Assignment*: Assign to maintainer or leave unassigned for community +* *Response time*: Aim for initial response within 1 week + +==== Security Response + +* *Initial response*: Within 48 hours +* *Triage*: Within 1 week +* *Fix*: According to severity (see SECURITY.md) +* *Disclosure*: Coordinated, typically 90 days after fix + +=== Becoming a Maintainer + +==== Path to Perimeter 2 (Trusted Contributor) + +We look for: + +* *Consistent contributions*: Regular, quality contributions over 3+ +months +* *Code quality*: Well-tested, documented, follows conventions +* *Community*: Helpful in discussions, reviews others’ PRs +* *Alignment*: Understands and embodies project values (reversibility, +emotional safety, political autonomy) + +*Process*: 1. Core team discusses potential invitation 2. Invitation +extended via private message 3. 1-month trial period 4. Full membership +if successful + +==== Path to Perimeter 1 (Core Maintainer) + +We look for: + +* *Sustained commitment*: 6+ months of active, quality participation +* *Deep expertise*: Domain knowledge in core areas +* *Leadership*: Mentors others, drives initiatives +* *Trust*: Demonstrated judgment and alignment with project values + +*Process*: 1. Nominated by existing core maintainer 2. Discussion among +core team 3. Unanimous approval required 4. Onboarding period with +gradual permission increase + +=== Stepping Down + +Maintainers may step down at any time: + +* *Voluntary*: No explanation needed, though appreciated +* *Inactive*: After 6 months of inactivity, we may reach out to confirm +status +* *Emeritus*: Former maintainers are honored and may be consulted + +*Process*: 1. Notify core team 2. Remove permissions 3. Update +MAINTAINERS.md 4. Thank you! 🎉 + +=== Conflict Resolution + +==== Technical Disagreements + +[arabic] +. *Discussion*: Discuss in issue/MR +. *Evidence*: Present evidence and rationale +. *Consensus*: Aim for consensus +. *Vote*: If no consensus, core team votes (simple majority) +. *Document*: Document decision and rationale + +==== Interpersonal Conflicts + +[arabic] +. *Direct*: Speak directly with the person (if safe) +. *Mediation*: Request mediation from another maintainer +. *Code of Conduct*: File CoC complaint if needed +. *Resolution*: Follow CoC enforcement guidelines + +=== Contact + +* *General*: https://github.com/hyperpolymath/bunsenite/issues[GitHub +Issues] +* *Security*: +https://github.com/hyperpolymath/bunsenite/security/advisories/new[GitHub +Security Advisories] +* *GitHub*: https://github.com/hyperpolymath[@hyperpolymath] +* *GitLab*: https://gitlab.com/hyperpolymath[@hyperpolymath] + +=== Acknowledgments + +Thank you to all contributors, whether Perimeter 1, 2, or 3. Every +contribution matters! + +Special thanks to: - Nickel language team (nickel-lang-core) - RSR +Framework contributors - TPCF community - All early adopters and testers + +''''' + +*Last updated*: 2025-11-22 *Version*: 1.0.0 diff --git a/vendor/bunsenite/Mustfile b/vendor/bunsenite/Mustfile new file mode 100644 index 0000000..5f07541 --- /dev/null +++ b/vendor/bunsenite/Mustfile @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: MPL-2.0 +# 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/vendor/bunsenite/NOTICE b/vendor/bunsenite/NOTICE new file mode 100644 index 0000000..e047656 --- /dev/null +++ b/vendor/bunsenite/NOTICE @@ -0,0 +1,22 @@ +Licensing Notice +================ + +This project is authored by Jonathan D.A. Jewell (hyperpolymath) and +is licensed under the Palimpsest License (MPL-2.0). + +The MPL-2.0 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/MPL-2.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. + +The legally binding terms are: + - Source files: governed by MPL-2.0 (per SPDX headers) + - Combined works: compatible with MPL-2.0 (per PMPL Section 6) + +For more information about the Palimpsest License: + https://github.com/hyperpolymath/palimpsest-license diff --git a/vendor/bunsenite/PACKAGING.adoc b/vendor/bunsenite/PACKAGING.adoc new file mode 100644 index 0000000..a2c2235 --- /dev/null +++ b/vendor/bunsenite/PACKAGING.adoc @@ -0,0 +1,148 @@ +== Bunsenite Packaging Guide + +This document describes how to package and distribute Bunsenite for +various package managers. + +=== Package Managers + +==== Linux + +[cols=",,",options="header",] +|=== +|Manager |Distro |Config Location +|pacman |Arch Linux |`+packaging/arch/PKGBUILD+` +|apt |Debian/Ubuntu |`+packaging/debian/+` +|dnf |Fedora/RHEL |`+packaging/rpm/bunsenite.spec+` +|zypper |openSUSE |`+packaging/rpm/bunsenite.spec+` +|flatpak |Universal |`+packaging/flatpak/+` +|=== + +==== macOS + +[cols=",",options="header",] +|=== +|Manager |Config Location +|Homebrew |`+packaging/homebrew/bunsenite.rb+` +|MacPorts |`+packaging/macports/Portfile+` +|=== + +==== Windows + +[cols=",",options="header",] +|=== +|Manager |Config Location +|Scoop |`+packaging/scoop/bunsenite.json+` +|Chocolatey |`+packaging/chocolatey/bunsenite.nuspec+` +|winget |`+packaging/winget/bunsenite.yaml+` +|=== + +==== Language Package Managers + +[cols=",,",options="header",] +|=== +|Manager |Language |Location +|cargo |Rust |`+Cargo.toml+` (publish to crates.io) +|npm |Node.js |`+bindings/affinescript/package.json+` +|deno.land/x |Deno |`+bindings/deno/+` (publish to deno.land) +|=== + +=== Build Requirements + +All packaging scripts assume: + +[arabic] +. *Rust 1.70+* - For the core library +. *Zig 0.11+* - For the FFI layer +. *Git* - For source fetching + +=== Building Release Artifacts + +[source,bash] +---- +# Build with all features +cargo build --release --features full + +# Build Zig FFI layer +cd zig && zig build -Doptimize=ReleaseFast + +# Run tests +cargo test --release +---- + +=== Release Artifacts + +Each release should include: + +==== Linux (x86_64, aarch64) + +* `+bunsenite-VERSION-x86_64-unknown-linux-gnu.tar.gz+` +* `+bunsenite-VERSION-aarch64-unknown-linux-gnu.tar.gz+` + +==== macOS (x86_64, aarch64) + +* `+bunsenite-VERSION-x86_64-apple-darwin.tar.gz+` +* `+bunsenite-VERSION-aarch64-apple-darwin.tar.gz+` + +==== Windows (x86_64) + +* `+bunsenite-VERSION-x86_64-pc-windows-msvc.zip+` + +==== Source + +* `+bunsenite-VERSION.tar.gz+` + +=== Publishing Checklist + +==== crates.io (Rust) + +[source,bash] +---- +cargo publish --dry-run +cargo publish +---- + +==== npm (Node.js bindings) + +[source,bash] +---- +cd bindings/affinescript +npm publish --access public +---- + +==== Homebrew + +[arabic] +. Fork homebrew-core +. Update `+bunsenite.rb+` with new version and sha256 +. Submit PR + +==== Arch Linux (AUR) + +[arabic] +. Update PKGBUILD with new version +. Generate .SRCINFO: `+makepkg --printsrcinfo > .SRCINFO+` +. Push to AUR + +==== Flatpak (Flathub) + +[arabic] +. Fork flathub/com.campaignforcoolercoding.bunsenite +. Update manifest with new version +. Submit PR + +=== CI/CD Integration + +The `+.github/workflows/release.yml+` workflow automates: - Building +release binaries for all platforms - Creating GitHub releases with +artifacts - Publishing to crates.io + +=== RSR Compliance Notes + +All packages must include: - LICENSE-MPL-2.0 - LICENSE-PALIMPSEST (if +applicable) - README.md with RSR tier disclosure + +Package descriptions should include: + +.... +RSR Compliance: Bronze Tier | TPCF Perimeter: 3 +.... diff --git a/vendor/bunsenite/PALIMPSEST.adoc b/vendor/bunsenite/PALIMPSEST.adoc new file mode 100644 index 0000000..c2afbcc --- /dev/null +++ b/vendor/bunsenite/PALIMPSEST.adoc @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += MPL-2.0 +:toc: +:toc-placement!: + +image:https://img.shields.io/badge/License-MPL--2.0-blue.svg[License: MPL-2.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 **MPL-2.0 License 1.0 (MPL-2.0)**. +For SPDX and tooling, use **MPL-2.0**. + +MPL-2.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 MPL-2.0 license text in `LICENSE`. +2. Add SPDX headers to source files: + `SPDX-License-Identifier: CC-BY-SA-4.0` +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 MPL-2.0. + +== References + +* `legal/README.adoc` +* `assets/badges/README.md` +* `embed/license-blocks/README.md` diff --git a/vendor/bunsenite/PROOF-NEEDS.adoc b/vendor/bunsenite/PROOF-NEEDS.adoc new file mode 100644 index 0000000..2cd88d9 --- /dev/null +++ b/vendor/bunsenite/PROOF-NEEDS.adoc @@ -0,0 +1,41 @@ +== Proof Requirements + +=== Current state + +* `+src/abi/Types.idr+` — Nickel parser types +* `+src/abi/Layout.idr+` — Memory layout +* `+src/abi/Foreign.idr+` — FFI declarations +* No dangerous patterns in ABI layer +* Claims: type safety, memory safety, "`zero `+unsafe+` blocks`" + +=== What needs proving + +* *Parser correctness*: Prove the Nickel parser accepts exactly the +Nickel grammar (no over-acceptance of malformed input) +* *Round-trip fidelity*: Prove parse-then-serialize produces +semantically equivalent output (no silent data loss) +* *FFI memory safety*: Prove the Zig FFI layer correctly manages +ownership across the Rust-Zig-Deno/WASM boundary (no dangling pointers, +no double-free) +* *Zero-unsafe claim*: Verify (via tooling or proof) that no `+unsafe+` +blocks exist in the Rust core and that all FFI crossing points are safe + +=== Recommended prover + +* *Idris2* — For parser grammar conformance and FFI boundary properties +* *Lean4* — For algebraic properties of the parse/serialize round-trip +if modeled functorially + +=== Priority + +* *MEDIUM* — The "`zero unsafe blocks`" and type safety claims are +strong marketing. Parser correctness matters for any tool in the +configuration pipeline, but Bunsenite is not safety-critical +infrastructure. + +=== Template ABI Cleanup (2026-03-29) + +Template ABI removed – was creating false impression of formal +verification. The removed files (Types.idr, Layout.idr, Foreign.idr) +contained only RSR template scaffolding with unresolved +\{\{PROJECT}}/\{\{AUTHOR}} placeholders and no domain-specific proofs. diff --git a/vendor/bunsenite/PROVEN-INTEGRATION.adoc b/vendor/bunsenite/PROVEN-INTEGRATION.adoc new file mode 100644 index 0000000..4a4ff2a --- /dev/null +++ b/vendor/bunsenite/PROVEN-INTEGRATION.adoc @@ -0,0 +1,120 @@ +== Proven Library Integration Plan + +This document outlines how the +https://github.com/hyperpolymath/proven[proven] library’s formally +verified modules integrate with Bunsenite. + +=== Applicable Modules + +==== High Priority + +[cols=",,",options="header",] +|=== +|Module |Use Case |Formal Guarantee +|`+SafeSchema+` |Nickel config validation |Type-safe configurations +|`+SafeFFI+` |FFI boundary safety |ABI contract verification +|`+SafeBuffer+` |Config parsing buffer |Bounded memory usage +|=== + +==== Medium Priority + +[cols=",,",options="header",] +|=== +|Module |Use Case |Formal Guarantee +|`+SafeString+` |Config interpolation |Injection prevention +|`+SafeTree+` |Config tree navigation |ValidPath proofs +|`+SafeResource+` |File handle lifecycle |Valid state transitions +|=== + +=== Integration Points + +==== 1. Config Schema Validation (SafeSchema) + +[source,nickel] +---- +# Nickel config +{ + name = "my-app", + port = 8080, + features = ["auth", "logging"] +} +---- + +.... +parse → SafeSchema.validate → typed NickelConfig +.... + +SafeSchema ensures: - Required fields are present - Field types match +declarations - Contract constraints are satisfied + +==== 2. FFI Boundary Safety (SafeFFI) + +Bunsenite’s C ABI boundary is where safety is most critical: + +.... +Rust → SafeFFI.marshal → C ABI → SafeFFI.unmarshal → Deno/AffineScript +.... + +SafeFFI guarantees: - Memory ownership is correctly transferred - +Buffers are correctly sized and aligned - Error codes are properly +propagated - No use-after-free or double-free + +==== 3. Parsing Buffer Management (SafeBuffer) + +.... +config_file → SafeBuffer.BoundedBuffer → parse → result +.... + +Prevents: - Stack overflow on deeply nested configs - OOM on maliciously +large inputs - Buffer overflows in string handling + +=== FFI Contract Proofs + +Bunsenite’s C ABI can be formally specified: + +[source,c] +---- +// include/bebop_v_ffi.h +struct BunseniteResult { + uint32_t status; // SafeFFI.ResultCode + void* data; // SafeFFI.OwnedPtr + size_t len; // SafeFFI.BoundedSize +}; +---- + +SafeFFI proves: - `+status == OK+` ⟹ `+data != NULL ∧ len > 0+` - +`+status == ERROR+` ⟹ `+data+` contains error message - Caller owns +`+data+` and must free it + +=== Language Binding Integration + +[cols=",,",options="header",] +|=== +|Binding |FFI Layer |proven Module +|Deno |Deno.dlopen |SafeFFI +|AffineScript |External FFI |SafeFFI +|WASM |Wasm bindgen |SafeBuffer +|=== + +=== Implementation Notes + +For Rust core integration: + +[source,rust] +---- +// src/lib.rs +#[cfg(feature = "proven")] +mod proven_bindings { + // SafeSchema validation before returning to FFI + pub fn validate_config(input: &str) -> Result { + SafeSchema::validate(input)? + } +} +---- + +=== Status + +* [ ] Add SafeSchema for Nickel config validation +* [ ] Integrate SafeFFI for ABI contract verification +* [ ] Implement SafeBuffer for bounded parsing +* [ ] Generate proofs for C ABI contract diff --git a/vendor/bunsenite/PUBLISHING.adoc b/vendor/bunsenite/PUBLISHING.adoc new file mode 100644 index 0000000..64a3810 --- /dev/null +++ b/vendor/bunsenite/PUBLISHING.adoc @@ -0,0 +1,155 @@ +== Publishing Bunsenite + +This guide walks through publishing bunsenite to all package managers. + +=== Prerequisites + +You’ll need accounts and tokens for: - *crates.io* - Rust package +registry - *npm* - Node.js package registry - *GitHub* - For releases +and Homebrew tap + +=== Step 1: Configure GitHub Secrets + +Go to your repo → Settings → Secrets and variables → Actions → New +repository secret + +[width="100%",cols="47%,53%",options="header",] +|=== +|Secret Name |How to Get It +|`+CARGO_REGISTRY_TOKEN+` |https://crates.io/settings/tokens → New Token + +|`+NPM_TOKEN+` |https://www.npmjs.com/settings/tokens → Generate New +Token (Automation) +|=== + +=== Step 2: Create and Push a Tag + +[source,bash] +---- +# Create the v1.0.0 tag +git tag -a v1.0.0 -m "Release v1.0.0" + +# Push the tag (this triggers the release workflow) +git push origin v1.0.0 +---- + +This automatically: - Builds binaries for Linux, macOS, Windows - +Creates a GitHub Release with all artifacts - Publishes to crates.io - +Publishes to npm + +=== Step 3: Homebrew (Manual) + +Option A: *Create your own tap* (recommended for new packages): + +[source,bash] +---- +# Create a new repo: hyperpolymath/homebrew-tap +# Then add the formula + +mkdir -p homebrew-tap/Formula +cp packaging/homebrew/bunsenite.rb homebrew-tap/Formula/ + +# Update the sha256 from the GitHub release +# Then users install with: +# brew tap hyperpolymath/tap +# brew install bunsenite +---- + +Option B: *Submit to homebrew-core* (after package is established): + +[source,bash] +---- +# Fork homebrew/homebrew-core +# Add Formula/bunsenite.rb +# Submit PR +---- + +=== Step 4: Arch Linux (AUR) + +[source,bash] +---- +# Clone your AUR package (first time: create it) +git clone ssh://aur@aur.archlinux.org/bunsenite.git aur-bunsenite +cd aur-bunsenite + +# Copy PKGBUILD +cp ../packaging/arch/PKGBUILD . + +# Update checksums +updpkgsums + +# Generate .SRCINFO +makepkg --printsrcinfo > .SRCINFO + +# Commit and push +git add PKGBUILD .SRCINFO +git commit -m "Update to v1.0.0" +git push +---- + +=== Step 5: Other Package Managers + +==== Flatpak (Flathub) + +[arabic] +. Fork https://github.com/flathub/flathub +. Create `+com.campaignforcoolercoding.bunsenite/+` directory +. Copy `+packaging/flatpak/com.campaignforcoolercoding.bunsenite.yml+` +. Submit PR + +==== Scoop (Windows) + +[arabic] +. Fork https://github.com/ScoopInstaller/Main (or create own bucket) +. Add `+bucket/bunsenite.json+` +. Submit PR + +==== Chocolatey (Windows) + +[source,bash] +---- +cd packaging/chocolatey +# Update bunsenite.nuspec with correct URLs +choco pack +choco push bunsenite.1.0.0.nupkg --source https://push.chocolatey.org/ +---- + +==== winget (Windows) + +[arabic] +. Fork https://github.com/microsoft/winget-pkgs +. Create `+manifests/c/CampaignForCoolerCoding/Bunsenite/1.0.0/+` +. Copy and split manifest files +. Submit PR + +=== Quick Start Commands + +[source,bash] +---- +# Step 1: Add secrets to GitHub (do this in browser) + +# Step 2: Tag and release +git tag -a v1.0.0 -m "Release v1.0.0" +git push origin v1.0.0 + +# Step 3: Wait for CI, then verify +# - Check GitHub Actions for build status +# - Check https://crates.io/crates/bunsenite +# - Check https://www.npmjs.com/package/bunsenite +---- + +=== Verification + +After publishing, verify each registry: + +[source,bash] +---- +# Cargo +cargo install bunsenite + +# npm +npm info bunsenite + +# GitHub Release +gh release view v1.0.0 +---- diff --git a/vendor/bunsenite/QUICKSTART-DEV.adoc b/vendor/bunsenite/QUICKSTART-DEV.adoc new file mode 100644 index 0000000..6e48e58 --- /dev/null +++ b/vendor/bunsenite/QUICKSTART-DEV.adoc @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Bunsenite — Developer Quickstart +:toc: preamble + +Clone, build, test, contribute. + +== Prerequisites + +* Git 2.40+ +* just (command runner) +* See `just doctor` output for language-specific requirements + +== Setup + +[source,bash] +---- +git clone https://github.com/hyperpolymath/bunsenite +cd bunsenite +just doctor # verify toolchain +just heal # auto-install missing tools +---- + +== Development Workflow + +[source,bash] +---- +just tour # understand the codebase +just help-me # see available commands +---- + +== Before Committing + +[source,bash] +---- +just assail # run panic-attacker security scan +---- + +== Contributing + +See link:.github/CONTRIBUTING.md[CONTRIBUTING.md] for guidelines. diff --git a/vendor/bunsenite/QUICKSTART-MAINTAINER.adoc b/vendor/bunsenite/QUICKSTART-MAINTAINER.adoc new file mode 100644 index 0000000..211da8e --- /dev/null +++ b/vendor/bunsenite/QUICKSTART-MAINTAINER.adoc @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Bunsenite — Maintainer Quickstart +:toc: preamble + +Packaging, deployment, and release management. + +== Prerequisites + +* Git 2.40+ +* just (command runner) +* Familiarity with the project (run `just tour` first) + +== CI/CD + +This project uses GitHub Actions. Workflows are in `.github/workflows/`. + +Key workflows: + +* `hypatia-scan.yml` — Neurosymbolic security scanning +* `codeql.yml` — Code analysis +* `scorecard.yml` — OpenSSF Scorecard +* `mirror.yml` — GitLab/Bitbucket mirroring + +== Releasing + +1. Update version in project config +2. Update CHANGELOG.md +3. Tag: `git tag -s v` +4. Push: `git push origin main --tags` + +== Container Build (if applicable) + +[source,bash] +---- +podman build -f Containerfile -t bunsenite:latest . +---- + +== Mirrors + +This repo is mirrored to GitLab and Bitbucket (hyperpolymath accounts) +via the `mirror.yml` workflow. diff --git a/vendor/bunsenite/QUICKSTART-USER.adoc b/vendor/bunsenite/QUICKSTART-USER.adoc new file mode 100644 index 0000000..d754e7a --- /dev/null +++ b/vendor/bunsenite/QUICKSTART-USER.adoc @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Bunsenite — User Quickstart +:toc: preamble + +Get up and running in 60 seconds. + +== Prerequisites + +* Git 2.40+ +* just (command runner) — https://just.systems + +== Install + +[source,bash] +---- +git clone https://github.com/hyperpolymath/bunsenite +cd bunsenite +just doctor # check toolchain +just heal # auto-install missing tools +---- + +== First Run + +[source,bash] +---- +just tour # guided project tour +just help-me # see common workflows +---- + +== Get Help + +* `just help-me` — common workflows +* `just doctor` — diagnose toolchain issues +* https://github.com/hyperpolymath/bunsenite/issues — report bugs diff --git a/vendor/bunsenite/README.adoc b/vendor/bunsenite/README.adoc new file mode 100644 index 0000000..a2d754f --- /dev/null +++ b/vendor/bunsenite/README.adoc @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Bunsenite +image:https://img.shields.io/badge/License-MPL_2.0--1.0-blue.svg[License: MPL-2.0,link="https://github.com/hyperpolymath/palimpsest-license"] +image:https://img.shields.io/badge/OpenSSF-Best_Practices-green?logo=opensourcesecurity[OpenSSF Best Practices, link="https://www.bestpractices.dev/en/projects/new?repo_url=https://github.com/hyperpolymath/bunsenite"] +image:https://img.shields.io/badge/Idris-Inside-blueviolet?style=flat&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEyIDJMMyA3djEwbDkgNSA5LTVWN2wtOS01em0wIDJsNyA0djhsLTcgNC03LTRWOGw3LTR6Ii8+PC9zdmc+[Idris Inside,link="https://github.com/hyperpolymath/proven"] + + + + +image:https://img.shields.io/badge/Philosophy-Palimpsest-indigo.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-license"] + + +> Nickel configuration file parser with multi-language FFI bindings + +[![RSR Bronze](https://img.shields.io/badge/RSR-Bronze-CD7F32)](https://example.com/rsr) +[![TPCF Perimeter 3](https://img.shields.io/badge/TPCF-Perimeter%203-blue)](https://example.com/tpcf) +[![Build Status](https://github.com/hyperpolymath/bunsenite/actions/workflows/ci.yml/badge.svg)](https://github.com/hyperpolymath/bunsenite/actions) + +== Overview + +Bunsenite is a *Nickel configuration file parser* with a Rust core library and multi-language FFI bindings. It provides a stable C ABI layer (via Zig) that enables bindings for *Deno* (JavaScript/TypeScript), *Rescript*, and *WebAssembly* for browser and universal use. + +=== Key Features + +- ✅ *Type Safety*: Compile-time guarantees via Rust's type system +- ✅ *Memory Safety*: Rust ownership model, *zero `unsafe` blocks* +- ✅ *Offline-First*: Works completely air-gapped, no network dependencies +- ✅ *Multi-Language*: FFI bindings for Deno, Rescript, and WASM +- ✅ *Standards Compliant*: RSR Bronze tier, TPCF Perimeter 3 +- ✅ *Well-Documented*: Comprehensive API docs, examples, and guides +- ✅ *Production-Ready*: 100% test pass rate, CI/CD, semantic versioning + +== Quick Start + +=== Installation + +```bash += From crates.io + +cargo install bunsenite + += From source + +git clone https://github.com/hyperpolymath/bunsenite.git +cd bunsenite +cargo install --path . +``` + +=== Usage + +==== Rust Library + +```rust +use bunsenite::NickelLoader; + +fn main() { + let config = r#" + { + name = "my-app", + version = "1.0.0", + port = 8080, + } + "#; + + let loader = NickelLoader::new(); + let result = loader.parse_string(config, "config.ncl").unwrap(); + + println!("Config: {}", result); +} +``` + +==== CLI + +```bash += Parse and evaluate a config file + +bunsenite parse config.ncl + += Pretty-print output + +bunsenite parse config.ncl --pretty + += Validate without evaluating + +bunsenite validate config.ncl + += Show version and compliance info + +bunsenite info +``` + +==== WebAssembly (Browser) + +```javascript +import init, { parse_nickel } from './bunsenite.js'; + +async function main() { + await init(); + const config = `{ name = "example", version = "1.0.0" }`; + const result = parse_nickel(config, "config.ncl"); + console.log(JSON.parse(result)); +} +``` + +==== Deno (TypeScript) + +```typescript +// See bindings/deno/ for full example +import { parseNickel } from "./bunsenite_deno.ts"; + +const config = `{ foo = 42 }`; +const result = parseNickel(config, "config.ncl"); +console.log(result); +``` + +== Architecture + +``` +┌─────────────────────────────────────────────────┐ +│ Consumers │ +├───────────────┬───────────────┬─────────────────┤ +│ Deno │ Rescript │ Browser │ +│ (TypeScript) │ (AffineScript) │ (WASM) │ +└───────┬───────┴───────┬───────┴────────┬────────┘ + │ │ │ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────────┐ + │ Zig FFI │ │ Zig FFI │ │ wasm-bindgen │ + │ (C ABI) │ │ (C ABI) │ │ │ + └─────┬────┘ └─────┬────┘ └──────┬───────┘ + │ │ │ + └──────────────┴─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ Rust Core │ + │ (lib.rs) │ + │ │ + │ nickel-lang-core│ + │ 0.9.1 │ + └─────────────────┘ +``` + +=== Design Rationale + +*Zig FFI Layer*: Provides stable C ABI, isolating consumers from Rust ABI changes. This allows language bindings to remain stable across Rust compiler versions. + +*WASM Support*: Enables browser deployment and universal compatibility at ~95% native speed. + +*Deno .ts Files*: Required syntax for Deno runtime FFI (NOT plain TypeScript). Uses `Deno.dlopen` for native FFI calls to Zig layer. + +== Documentation + +- *[CLAUDE.md](./CLAUDE.md)*: Comprehensive guide for AI assistants and developers +- *[SECURITY.md](./SECURITY.md)*: Security policies and vulnerability reporting +- *[CONTRIBUTING.md](./CONTRIBUTING.md)*: Contribution guidelines +- *[CHANGELOG.md](./CHANGELOG.md)*: Version history and release notes +- *[API Docs](https://docs.rs/bunsenite)*: Full Rust API documentation + +== Standards Compliance + +=== RSR Framework: Bronze Tier + +Bunsenite meets all *Rhodium Standard Repository (RSR) Bronze tier* requirements: + +- ✅ Type safety (Rust compile-time guarantees) +- ✅ Memory safety (ownership model, `#![deny(unsafe_code)]`) +- ✅ Offline-first (no network dependencies) +- ✅ Complete documentation (README, LICENSE, SECURITY, CONTRIBUTING, CODE_OF_CONDUCT, MAINTAINERS) +- ✅ `.well-known/` directory (security.txt, ai.txt, humans.txt) +- ✅ Build system (Justfile, Guix flake) +- ✅ CI/CD pipeline (GitLab CI) +- ✅ 100% test pass rate + +=== TPCF: Perimeter 3 (Community Sandbox) + +This project uses the *Tri-Perimeter Contribution Framework (TPCF)*: + +- *Perimeter 1*: Core maintainers only (restricted) +- *Perimeter 2*: Trusted contributors (by invitation) +- *Perimeter 3*: *Community Sandbox* - Open to all contributors + +All contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for details. + +== Building from Source + +=== Prerequisites + +- Rust 1.70+ (`rustup install stable`) +- `just` command runner (`cargo install just`) +- Optional: Zig compiler (for FFI layer) +- Optional: `wasm-pack` (for WASM builds: `cargo install wasm-pack`) +- Optional: Deno runtime (for Deno bindings) + +=== Build Commands + +```bash += Build all targets + +just all + += Build Rust library and CLI + +cargo build --release + += Build WebAssembly + +just wasm + += Run tests + +cargo test + += Run linter + +cargo clippy + += Format code + +cargo fmt + += Check RSR compliance + +just rsr-check +``` + +See `Justfile` for all available commands. + +== Testing + +```bash += Run all tests + +cargo test + += Run tests with output + +cargo test -- --nocapture + += Run specific test + +cargo test test_name + += Run with coverage (requires tarpaulin) + +cargo tarpaulin --out Html +``` + +Current status: *100% test pass rate* (30+ tests covering core functionality, error handling, and edge cases) + +== Performance + +- *Native Rust*: Baseline performance +- *WebAssembly*: ~95% native speed +- *FFI (Deno/Rescript)*: ~90% native speed (C ABI overhead minimal) + +== Security + +We take security seriously. See [SECURITY.md](./SECURITY.md) for: + +- Supported versions +- Vulnerability reporting process +- Security best practices +- Responsible disclosure policy + +*Security contact*: See `.well-known/security.txt` or [SECURITY.md](./SECURITY.md) + +== License + +Dual licensed under your choice of: + +- *MPL-2.0 License v1.0 (MPL-2.0)* ([LICENSE-MPL-2.0](./LICENSE) or https://github.com/hyperpolymath/palimpsest-license) +- *MPL-2.0 v0.8* ([LICENSE-PALIMPSEST](./LICENSE) or https://palingenesis.org/palimpsest-license) + +This allows maximum flexibility for use while preserving reversibility and emotional safety principles. + +== Contributing + +Contributions are welcome! This is a *TPCF Perimeter 3* (Community Sandbox) project. + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for: + +- Code of Conduct +- Development workflow +- Testing requirements +- Commit message conventions +- Pull request process + +== Community + +- *Issues*: [GitHub Issues](https://github.com/hyperpolymath/bunsenite/issues) +- *Discussions*: [GitHub Discussions](https://github.com/hyperpolymath/bunsenite/discussions) +- *Security*: See [SECURITY.md](./SECURITY.md) + +== Acknowledgments + +- [Nickel Language Team](https://github.com/tweag/nickel) for the excellent configuration language +- RSR Framework contributors +- TPCF community +- All contributors to this project + +== Roadmap + +See [NEXT_STEPS.md](./NEXT_STEPS.md) for planned features and enhancements: + +- [ ] Additional language bindings (Python, Ruby, Node.js) +- [ ] Performance benchmarking suite +- [ ] REPL/interactive mode +- [ ] Schema validation +- [ ] Watch mode for auto-reload +- [ ] Plugin system + +== Version History + +See [CHANGELOG.md](./CHANGELOG.md) for detailed version history. + +Current version: *0.1.0* (Bronze tier compliant, production-ready) + +--- + +*Made with ❤️ by the Campaign for Cooler Coding and Programming* + +*Politically autonomous software for emotionally safe development* + + +== Architecture + +See link:TOPOLOGY.md[TOPOLOGY.md] for a visual architecture map and completion dashboard. diff --git a/vendor/bunsenite/ROADMAP.adoc b/vendor/bunsenite/ROADMAP.adoc new file mode 100644 index 0000000..1594ff3 --- /dev/null +++ b/vendor/bunsenite/ROADMAP.adoc @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Bunsenite Roadmap +:toc: +:toclevels: 3 + +== Overview + +This roadmap outlines the development plan for Bunsenite, the Nickel configuration file parser with multi-language FFI bindings. + +== v1.0 - Production Release (Current) + +**Target:** 2026-02 +**Status:** Active development (85% complete) + +=== Core Features +* ✅ Rust core library with Nickel parsing +* ✅ Zero `unsafe` blocks (100% safe Rust) +* ✅ C ABI layer via Zig for FFI +* ✅ Deno bindings (JavaScript/TypeScript) +* ✅ AffineScript bindings +* ✅ WebAssembly support (browser + universal) +* ✅ Offline-first architecture (no network dependencies) + +=== Quality & Standards +* ✅ RSR Bronze tier compliance +* ✅ TPCF Perimeter 3 compliance +* ✅ 100% test pass rate +* ✅ CI/CD pipeline +* ✅ Comprehensive documentation +* ✅ ClusterFuzzLite fuzzing +* ✅ Criterion benchmarks + +=== Remaining for v1.0 +* [ ] Complete ROADMAP.adoc (this file) +* [ ] Address placeholder TODOs in codebase +* [ ] Final security audit +* [ ] Performance optimization pass +* [ ] Release automation + +== v1.1 - Enhanced Bindings (Next) + +**Target:** 2026-03 + +=== Additional Language Bindings +* Python bindings (via PyO3) +* Ruby bindings (via Magnus) +* Julia bindings (CCall) +* Go bindings (cgo) + +=== Improved Ergonomics +* Simplified API for common use cases +* Better error messages with suggestions +* Auto-completion support for IDEs +* Configuration validation helpers + +=== Performance +* Streaming parser for large files +* Parallel parsing for multi-file configs +* Memory usage optimization +* Zero-copy parsing where possible + +== v1.2 - Advanced Features + +**Target:** 2026-04 + +=== Nickel Language Features +* Advanced type inference +* Custom type definitions +* Merging strategies +* Validation schemas +* Built-in formatters + +=== Tooling Integration +* Language server protocol (LSP) +* Formatter integration +* Linter integration +* Migration tools from other config formats + +=== Developer Experience +* Interactive REPL +* Web-based playground +* Configuration templates library +* Best practices guide + +== v2.0 - Neurosymbolic Configuration + +**Target:** 2026-Q3 + +=== AI Integration +* LLM-based configuration generation +* Natural language to Nickel translation +* Configuration validation with AI explanations +* Auto-fix for common configuration errors + +=== Formal Verification +* Idris integration for proven configuration +* Type-level guarantees for config correctness +* Proof-carrying configuration +* Contract verification + +=== Hypatia Integration +* Hypatia orchestration for config management +* Fleet-wide configuration validation +* Dependency tracking across configurations +* Configuration policy enforcement + +== v2.5 - Universal Configuration Standard + +**Target:** 2026-Q4 + +=== Format Interoperability +* Bidirectional TOML conversion +* YAML import/export +* JSON compatibility layer +* HCL (HashiCorp) migration tools + +=== Ecosystem Integration +* Kubernetes ConfigMap support +* Docker Compose integration +* CI/CD pipeline configs +* Infrastructure as Code (IaC) templates + +=== Distributed Configuration +* Multi-environment support +* Secret management integration +* Configuration versioning +* Rollback mechanisms + +== v3.0 - Autonomous Configuration Management + +**Target:** 2027 + +=== Self-Healing Configurations +* Auto-detection of configuration drift +* Predictive error prevention +* Self-optimizing configurations +* Intelligent defaults learning + +=== Enterprise Features +* Multi-tenant configuration +* RBAC for configuration access +* Audit logging +* Compliance reporting + +=== Cloud-Native +* Native Kubernetes operator +* Service mesh integration +* Configuration as a Service (CaaS) +* Global configuration distribution + +== Long-term Vision + +* Industry-standard configuration format +* Formal verification by default +* AI-assisted configuration authoring +* Zero-configuration for common scenarios +* Universal compatibility across all tools +* Integration with Hyperpolymath proven infrastructure + +== Contributing + +See link:.github/CONTRIBUTING.md[Contributing Guidelines] for how to contribute to Bunsenite development. + +== Versioning + +Bunsenite follows semantic versioning (SemVer). Breaking changes will only be introduced in major version releases. diff --git a/vendor/bunsenite/RSR_COMPLIANCE.adoc b/vendor/bunsenite/RSR_COMPLIANCE.adoc new file mode 100644 index 0000000..cf0db8d --- /dev/null +++ b/vendor/bunsenite/RSR_COMPLIANCE.adoc @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += RSR Compliance: bunsenite +:toc: +:sectnums: + +== Overview + +This document describes the Rhodium Standard Repository (RSR) compliance status for *bunsenite*. + +== Classification + +[cols="1,2"] +|=== +|Attribute |Value + +|Project |bunsenite +|Primary Language |rust +|RSR Tier |1 +|Compliance Status |Compliant +|Last Updated |2025-12-10 +|=== + +== Language Tier Classification + +=== Tier 1 Languages (Preferred) +* Rust +* Elixir +* Zig +* Ada +* Haskell +* AffineScript + +=== Tier 2 Languages (Acceptable) +* Nickel (configuration) +* Racket (scripting) +* Guile Scheme (state management) +* Guix (derivations) + +=== Restricted Languages +* Python - Only allowed in salt/ directories for SaltStack +* TypeScript/JavaScript - Legacy only, convert to AffineScript +* CUE - Not permitted, use Nickel or Guile + +== Compliance Checklist + +[cols="1,1,2"] +|=== +|Requirement |Status |Notes + +|Primary language is Tier 1/2 |✓ |rust +|No restricted languages outside exemptions |✓ | +|.editorconfig present |✓ | +|.well-known/ directory |✓ | +|justfile present |✗ | +|LICENSE (MPL-2.0) |✓ | +|Containerfile present |✗ | +|flake.guix present |✓ | +|=== + +== Exemptions + +None + +== Action Items + +* Add Justfile +* Add Containerfile + +== References + +* link:https://github.com/hyperpolymath/RSR-template-repo[RSR Template Repository] +* link:.github/CONTRIBUTING.md[Contributing Guidelines] +* link:../CODE_OF_CONDUCT.adoc[Code of Conduct] diff --git a/vendor/bunsenite/RSR_OUTLINE.adoc b/vendor/bunsenite/RSR_OUTLINE.adoc new file mode 100644 index 0000000..b211f90 --- /dev/null +++ b/vendor/bunsenite/RSR_OUTLINE.adoc @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += RSR Template Repository + +image:[MPL-2.0-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 build/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` +|MPL-2.0 + +|`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, AffineScript +* **Tier 2** (Silver): Nickel, Racket, Guile Scheme, Guix +* **Infrastructure**: Guix channels, derivations + +=== Required Files + +* `.editorconfig` +* `.gitignore` +* `justfile` +* `README.adoc` +* `RSR_COMPLIANCE.adoc` +* `LICENSE` (MPL-2.0) +* `.well-known/security.txt` +* `.well-known/ai.txt` +* `.well-known/humans.txt` +* `guix.scm` OR `flake.guix` + +=== Prohibited + +* Python outside `salt/` directory +* TypeScript/JavaScript (use AffineScript) +* 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: CC-BY-SA-4.0 + +== 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/vendor/bunsenite/SECURITY.adoc b/vendor/bunsenite/SECURITY.adoc new file mode 100644 index 0000000..776e45e --- /dev/null +++ b/vendor/bunsenite/SECURITY.adoc @@ -0,0 +1,235 @@ +== Security Policy + +=== Supported Versions + +We take security seriously and provide security updates for the +following versions: + +[cols=",,",options="header",] +|=== +|Version |Supported |Notes +|1.0.x |:white_check_mark: |Current stable release +|< 1.0.0 |:x: |Pre-release, not supported +|=== + +=== Security Guarantees + +Bunsenite provides the following security guarantees: + +==== Memory Safety + +* *Zero `+unsafe+` blocks*: Enforced by `+#![deny(unsafe_code)]+` +compiler directive +* *Rust ownership model*: Prevents use-after-free, double-free, and +memory leaks +* *No null pointer dereferences*: Rust’s type system eliminates this +class of bugs +* *Bounds checking*: All array/vector accesses are bounds-checked + +==== Type Safety + +* *Compile-time guarantees*: Type errors are caught before runtime +* *No implicit conversions*: Explicit type conversions required +* *Strong typing*: Prevents type confusion vulnerabilities + +==== Dependency Security + +* *Minimal dependencies*: Only essential, well-audited crates +* *No network dependencies*: Offline-first design eliminates network +attack surface +* *Pinned versions*: Dependencies locked to specific versions for +reproducibility +* *Regular audits*: Dependencies audited using `+cargo audit+` + +==== Supply Chain Security + +* *Reproducible builds*: Guix flake provides bit-for-bit reproducibility +* *Signed releases*: All releases are cryptographically signed (planned) +* *Transparent development*: All changes tracked in public Git +repository +* *SBOM generation*: Software Bill of Materials available (planned) + +=== Reporting a Vulnerability + +*Please do NOT report security vulnerabilities through public +GitHub/GitLab issues.* + +==== Preferred Method + +Report security vulnerabilities via: + +[arabic] +. *GitHub Security Advisories*: +https://github.com/hyperpolymath/bunsenite/security/advisories/new[Create +a new security advisory] (preferred) +. *GitLab Confidential Issue*: Use GitLab’s confidential issue feature + +==== What to Include + +Please include: + +* *Description*: Clear description of the vulnerability +* *Impact*: What an attacker could achieve +* *Reproduction*: Step-by-step instructions to reproduce +* *Affected versions*: Which versions are affected +* *Proposed fix*: If you have one (optional) +* *Disclosure timeline*: Your preferred disclosure timeline + +==== Response Timeline + +* *Initial response*: Within 48 hours +* *Triage*: Within 1 week +* *Fix development*: Depends on severity (critical: days, low: weeks) +* *Public disclosure*: Coordinated with reporter, typically 90 days +after fix + +==== Severity Levels + +We use the following severity classifications: + +===== Critical (CVSS 9.0-10.0) + +* Remote code execution +* Privilege escalation to admin/root +* Authentication bypass + +*Response*: Patch within 48 hours, immediate release + +===== High (CVSS 7.0-8.9) + +* SQL injection (not applicable to Bunsenite) +* Information disclosure of sensitive data +* Denial of service affecting availability + +*Response*: Patch within 1 week, expedited release + +===== Medium (CVSS 4.0-6.9) + +* Cross-site scripting (XSS) (browser/WASM context) +* Information disclosure of non-sensitive data +* Low-impact denial of service + +*Response*: Patch within 2 weeks, next regular release + +===== Low (CVSS 0.1-3.9) + +* Minor information leaks +* Best practice violations +* Theoretical attacks with no known exploit + +*Response*: Patch within 30 days, next regular release + +=== Security Best Practices + +==== For Users + +[arabic] +. *Keep updated*: Always use the latest stable version +. *Verify signatures*: Check release signatures (when available) +. *Audit dependencies*: Run `+cargo audit+` regularly +. *Minimal permissions*: Run with least privilege necessary +. *Air-gapped environments*: Bunsenite works offline by design + +==== For Developers + +[arabic] +. *No `+unsafe+` code*: Never use `+unsafe+` blocks (enforced by +compiler) +. *Input validation*: Validate all external input +. *Error handling*: Use `+Result+` types, avoid `+unwrap()+` in library +code +. *Dependency review*: Review new dependencies carefully +. *Security testing*: Include security tests in test suite + +=== Known Limitations + +==== By Design + +[arabic] +. *Nickel evaluation*: Bunsenite evaluates Nickel code, which could +contain: +* Infinite loops (resource exhaustion) +* Large memory allocations +* Consider: Run evaluation in sandboxed environment for untrusted input +. *File I/O*: File reading follows OS permissions +* Does NOT escalate privileges +* Respects filesystem boundaries +. *WASM sandbox*: Browser WASM runs in sandbox, but: +* Subject to browser security model +* Can consume memory/CPU (denial of service) + +==== Mitigations + +We provide: + +* *Timeouts*: (Planned) Configurable evaluation timeouts +* *Memory limits*: (Planned) Configurable memory limits for evaluation +* *Resource monitoring*: (Planned) Track resource usage + +=== Security Audits + +[cols=",,,,",options="header",] +|=== +|Date |Auditor |Scope |Findings |Status +|2025-Q2 |Planned |Full codebase |N/A |Scheduled +|=== + +=== Cryptography + +Bunsenite does NOT implement cryptography. For cryptographic needs: + +* Use established libraries (e.g., `+ring+`, `+sodiumoxide+`) +* Never roll your own crypto +* Follow NIST/IETF recommendations + +=== Compliance + +* *OWASP Top 10*: N/A (not a web application) +* *CWE Top 25*: Memory safety issues prevented by Rust +* *GDPR*: No personal data collection +* *CCPA*: No personal data collection + +=== Security Tooling + +We use: + +* *`+cargo audit+`*: Check for known vulnerabilities in dependencies +* *`+cargo clippy+`*: Lint for security anti-patterns +* *`+cargo deny+`*: Check licenses and security advisories +* *GitLab Security Scanner*: Automated SAST in CI/CD +* *Dependabot*: (Planned) Automated dependency updates + +=== Contact + +* *GitHub Security Advisories*: +https://github.com/hyperpolymath/bunsenite/security/advisories/new[Report +a vulnerability] +* *Security.txt*: See `+.well-known/security.txt+` (RFC 9116 compliant) + +=== Attribution + +We believe in responsible disclosure and will credit security +researchers who: + +* Report vulnerabilities responsibly +* Allow coordinated disclosure +* Follow our security policy + +Credits will be listed in: - CHANGELOG.md - Release notes - SECURITY.md +(this file) + +=== Legal + +Security research conducted in good faith will not result in legal +action, provided: + +* You respect our disclosure timeline +* You do not exploit vulnerabilities beyond proof-of-concept +* You do not access user data or disrupt service +* You comply with applicable laws + +We support security researchers and the white-hat community. + +''''' + +*Last updated*: 2025-12-18 *Version*: 1.0.2 diff --git a/vendor/bunsenite/TEST-NEEDS.adoc b/vendor/bunsenite/TEST-NEEDS.adoc new file mode 100644 index 0000000..036072c --- /dev/null +++ b/vendor/bunsenite/TEST-NEEDS.adoc @@ -0,0 +1,58 @@ +== Test & Benchmark Requirements + +=== CRG Grade: C — ACHIEVED 2026-04-04 + +=== Current State + +* Unit tests: NONE verified (Cargo.toml exists but cargo not available +in this repo due to .tool-versions mismatch) +* Integration tests: 1 Zig integration test (template) +* E2E tests: NONE +* Benchmarks: 1 file exists (unverified) +* panic-attack scan: NEVER RUN + +=== What’s Missing + +==== Point-to-Point (P2P) + +* 11 Rust source files — test count unknown (cannot build) +* 5 Zig source files — only template integration test +* 3 Idris2 ABI files — no verification tests +* 4 AffineScript files — no tests +* 3 TypeScript files — no tests + +==== End-to-End (E2E) + +* Core functionality workflow not tested +* Integration between Rust, Zig, and AffineScript layers not tested + +==== Aspect Tests + +* [ ] Security (depends on what bunsenite does) +* [ ] Performance (benchmark file exists but unverified) +* [ ] Concurrency (if applicable) +* [ ] Error handling (graceful degradation) +* [ ] Accessibility (if UI exists) + +==== Build & Execution + +* [ ] cargo build — BLOCKED (.tool-versions mismatch) +* [ ] cargo test — BLOCKED +* [ ] zig build — not verified +* [ ] Self-diagnostic — none + +==== Benchmarks Needed + +* Verify existing benchmark file runs +* Specific benchmarks depend on functionality + +==== Self-Tests + +* [ ] panic-attack assail on own repo +* [ ] Fix .tool-versions to allow cargo to run + +=== Priority + +* *MEDIUM* — 11 Rust + 5 Zig + 4 AffineScript + 3 TS files. Cannot even +build due to tooling mismatch, which itself is a problem. Fix +.tool-versions first, then assess test needs. diff --git a/vendor/bunsenite/TOPOLOGY.adoc b/vendor/bunsenite/TOPOLOGY.adoc new file mode 100644 index 0000000..eeb8c78 --- /dev/null +++ b/vendor/bunsenite/TOPOLOGY.adoc @@ -0,0 +1,87 @@ +== Bunsenite — Project Topology + +=== System Architecture + +.... + ┌─────────────────────────────────────────┐ + │ CONSUMERS │ + │ (Deno, AffineScript, Browser, CLI) │ + └───────────────────┬─────────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────┐ + │ INTERFACE LAYER │ + │ ┌───────────┐ ┌───────────────────┐ │ + │ │ Zig FFI │ │ wasm-bindgen │ │ + │ │ (C ABI) │ │ (JS/WASM) │ │ + │ └─────┬─────┘ └────────┬──────────┘ │ + └────────│─────────────────│──────────────┘ + │ │ + ▼ ▼ + ┌─────────────────────────────────────────┐ + │ RUST CORE (LIB.RS) │ + │ (Nickel-lang-core integration) │ + │ ┌───────────┐ ┌───────────────────┐ │ + │ │ Parser │ │ Evaluator │ │ + │ └─────┬─────┘ └────────┬──────────┘ │ + └────────│─────────────────│──────────────┘ + │ │ + ▼ ▼ + ┌─────────────────────────────────────────┐ + │ NICKEL CONFIG FILES │ + │ (*.ncl, validation) │ + └─────────────────────────────────────────┘ + + ┌─────────────────────────────────────────┐ + │ REPO INFRASTRUCTURE │ + │ Justfile / Guix .machine_readable/ │ + │ RSR Compliance .well-known/ │ + └─────────────────────────────────────────┘ +.... + +=== Completion Dashboard + +.... +COMPONENT STATUS NOTES +───────────────────────────────── ────────────────── ───────────────────────────────── +CORE & CLI + Rust Core (lib.rs) ██████████ 100% Nickel 0.9.1 integration stable + CLI Interface ██████████ 100% Parse/Validate/Info active + Nickel Loader ██████████ 100% String & File loading verified + +BINDINGS & FFI + Zig FFI (C ABI) ██████████ 100% Stable boundary for bindings + Deno Bindings ██████████ 100% Deno.dlopen integration active + WASM / Browser ██████████ 100% 95% native speed verified + AffineScript Bindings ████████░░ 80% Type definitions refining + +REPO INFRASTRUCTURE + Justfile / Guix ██████████ 100% Reproducible builds stable + .machine_readable/ ██████████ 100% STATE.a2ml tracking + RSR Bronze Tier ██████████ 100% Compliance certified + +───────────────────────────────────────────────────────────────────────────── +OVERALL: ██████████ 100% v0.1.0 Production Ready +.... + +=== Key Dependencies + +.... +Nickel Core ──────► Bunsenite Rust ──────► Zig FFI ──────► Deno/TS + │ + ▼ + wasm-bindgen ───► Browser +.... + +=== Update Protocol + +This file is maintained by both humans and AI agents. When updating: + +[arabic] +. *After completing a component*: Change its bar and percentage +. *After adding a component*: Add a new row in the appropriate section +. *After architectural changes*: Update the ASCII diagram +. *Date*: Update the `+Last updated+` comment at the top of this file + +Progress bars use: `+█+` (filled) and `+░+` (empty), 10 characters wide. +Percentages: 0%, 10%, 20%, … 100% (in 10% increments). diff --git a/vendor/bunsenite/UPSTREAM-REVISION b/vendor/bunsenite/UPSTREAM-REVISION new file mode 100644 index 0000000..0d271b2 --- /dev/null +++ b/vendor/bunsenite/UPSTREAM-REVISION @@ -0,0 +1,4 @@ +url=https://gitlab.com/hyperpolymath/bunsenite.git +revision=f788de3950b7541354806299cc8605dcf1608d11 +vendored=2026-09-22 +note=Only the nickel-lang-core dependency line was changed from the upstream tree (default features disabled for the lean embedded build). See VENDOR.adoc. diff --git a/vendor/bunsenite/VENDOR.adoc b/vendor/bunsenite/VENDOR.adoc new file mode 100644 index 0000000..21e597c --- /dev/null +++ b/vendor/bunsenite/VENDOR.adoc @@ -0,0 +1,68 @@ += VENDOR — bunsenite (reviewed vendor fork) +:toc: + +== Provenance + +[cols="1,2"] +|=== +|Upstream repository |https://gitlab.com/hyperpolymath/bunsenite.git +|Pinned revision |`f788de3950b7541354806299cc8605dcf1608d11` (recorded verbatim in `UPSTREAM-REVISION`, generated from `git rev-parse HEAD` at vendor time) +|Upstream version |1.0.2 +|Licence |MPL-2.0 (see `LICENSE`, unchanged) +|Vendored on |2026-09-22 +|=== + +The entire upstream tree at the pinned revision is vendored unmodified, +including docs and licence, **except** for the single dependency change +described below and the added `UPSTREAM-REVISION` marker file. + +== Local modification (deliberate, minimal) + +.`Cargo.toml` +[source,diff] +---- +-nickel-lang-core = "0.18.0" ++nickel-lang-core = { version = "0.18.0", default-features = false } +---- + +== Motivation + +`nickel-lang-core` 0.18.x enables by default the feature set +`markdown, repl, doc, format`, which pulls in: + +* `rustyline` + `anstyle` (interactive REPL — irrelevant when embedding), +* `comrak` (documentation engine), +* `termimad` (markdown rendering), +* `topiary-core`, `topiary-queries`, `tree-sitter-nickel` (formatter, + including a C toolchain build). + +None of these are needed to *evaluate* a policy file. Disabling default +features reduces the transitive dependency graph substantially and removes a +C-compiler requirement from the embedded path. The earlier +sandbox out-of-memory failures while compiling `nickel-lang-parser` were the +proximate motivation; note that `nickel-lang-parser` itself remains a +non-optional dependency of `nickel-lang-core`, so very small builders may +still need the constrained CI job +(`CARGO_BUILD_JOBS=1`, `RUSTFLAGS="-C debuginfo=0"`). + +Additionally, consumers should depend on this vendored crate with +`default-features = false` to drop bunsenite's own default `cli` feature +(clap), keeping only: `error`, `loader`, `ffi` modules. + +== Review checklist (for maintainers touching this fork) + +* [ ] Any upstream sync must re-apply the `default-features = false` change + and update `UPSTREAM-REVISION`. +* [ ] Diff scope stays limited to the dependency line; code changes belong + upstream (https://gitlab.com/hyperpolymath/bunsenite). +* [ ] Re-run: `CARGO_BUILD_JOBS=1 RUSTFLAGS="-C debuginfo=0 -Dwarnings" \ + cargo test -p policy-oracle --features nickel --lib --locked` + +== Multi-file Nickel imports + +Bunsenite's `NickelLoader` evaluates a single file through +`Program::new_from_file`; nothing in this crate configures Nickel's import +paths deliberately. conative-gating therefore takes the explicit position +documented in `docs/NICKEL-POLICY.adoc`: **policy files with `import` +statements are rejected fail-closed before evaluation**. This vendor fork +inherits that policy by usage, not by code change. diff --git a/vendor/bunsenite/benches/bunsenite_bench.rs b/vendor/bunsenite/benches/bunsenite_bench.rs new file mode 100644 index 0000000..183a345 --- /dev/null +++ b/vendor/bunsenite/benches/bunsenite_bench.rs @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Throughput benchmarks for Bunsenite — small, medium, and large payloads. +//! +//! Complements `parser.rs` (which benchmarks round-trip parse/validate) with +//! focused throughput measurements across payload sizes, including loader +//! creation overhead and the `validate`-only fast path. + +use bunsenite::NickelLoader; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use std::hint::black_box; + +// --------------------------------------------------------------------------- +// Payload corpus +// --------------------------------------------------------------------------- + +/// Small payload (~80 bytes) — a minimal three-field record. +const SMALL_PAYLOAD: &str = r#"{ name = "small", port = 8080, active = true }"#; + +/// Medium payload (~350 bytes) — a two-section server/database record. +const MEDIUM_PAYLOAD: &str = r#" +{ + application = { + name = "medium-service", + version = "2.1.0", + environment = "staging", + }, + database = { + host = "db.staging.example.com", + port = 5432, + name = "staging_db", + pool_min = 2, + pool_max = 10, + ssl = true, + }, + server = { + bind = "0.0.0.0", + port = 8443, + workers = 8, + timeout_ms = 5000, + }, +} +"#; + +/// Large payload (~900 bytes) — a multi-section config with arrays and nesting. +const LARGE_PAYLOAD: &str = r#" +{ + service = { + name = "large-service", + version = "3.0.0", + region = "eu-west-1", + replicas = 5, + }, + endpoints = [ + { path = "/health", method = "GET", auth = false }, + { path = "/api/v1", method = "GET", auth = true }, + { path = "/api/v1", method = "POST", auth = true }, + { path = "/metrics", method = "GET", auth = false }, + ], + database = { + primary = { host = "db-primary.internal", port = 5432, pool = 20 }, + secondary = { host = "db-secondary.internal", port = 5432, pool = 10 }, + migrations = { auto = false, path = "migrations/" }, + }, + cache = { + provider = "redis", + host = "cache.internal", + port = 6379, + ttl_seconds = 300, + max_entries = 50000, + }, + logging = { + level = "warn", + structured = true, + sinks = ["stdout", "loki"], + }, + features = { + dark_mode = false, + beta_api = true, + rate_limit = { enabled = true, rps = 500 }, + }, +} +"#; + +// --------------------------------------------------------------------------- +// Benchmark 1: Throughput by payload size (parse_string) +// --------------------------------------------------------------------------- + +/// Measure `parse_string` throughput — bytes-per-second — for the three +/// payload sizes. This is the primary end-to-end measurement. +fn bench_throughput_parse(c: &mut Criterion) { + let loader = NickelLoader::new(); + let mut group = c.benchmark_group("throughput/parse_string"); + + for (label, payload) in [ + ("small", SMALL_PAYLOAD), + ("medium", MEDIUM_PAYLOAD), + ("large", LARGE_PAYLOAD), + ] { + group.throughput(Throughput::Bytes(payload.len() as u64)); + group.bench_with_input(BenchmarkId::new(label, payload.len()), payload, |b, src| { + b.iter(|| loader.parse_string(black_box(src), "bench.ncl")) + }); + } + + group.finish(); +} + +// --------------------------------------------------------------------------- +// Benchmark 2: Throughput by payload size (validate — parsing only, no eval) +// --------------------------------------------------------------------------- + +/// Measure `validate` throughput for the same three payloads. `validate` +/// skips evaluation, so it is expected to be faster than `parse_string` and +/// serves as a lower bound on parser overhead. +fn bench_throughput_validate(c: &mut Criterion) { + let loader = NickelLoader::new(); + let mut group = c.benchmark_group("throughput/validate"); + + for (label, payload) in [ + ("small", SMALL_PAYLOAD), + ("medium", MEDIUM_PAYLOAD), + ("large", LARGE_PAYLOAD), + ] { + group.throughput(Throughput::Bytes(payload.len() as u64)); + group.bench_with_input(BenchmarkId::new(label, payload.len()), payload, |b, src| { + b.iter(|| loader.validate(black_box(src), "bench.ncl")) + }); + } + + group.finish(); +} + +// --------------------------------------------------------------------------- +// Benchmark 3: Loader construction overhead +// --------------------------------------------------------------------------- + +/// Measure the cost of calling `NickelLoader::new()`. This baseline confirms +/// that the loader itself is cheap to create and that callers may safely +/// construct one per-request if needed. +fn bench_loader_creation(c: &mut Criterion) { + c.bench_function("loader_creation", |b| { + b.iter(|| black_box(NickelLoader::new())) + }); +} + +// --------------------------------------------------------------------------- +// Benchmark 4: Repeated small-payload parses on a shared loader +// --------------------------------------------------------------------------- + +/// Measures repeated small-config parses on a single long-lived loader to +/// detect any state accumulation or degradation in the loader between calls. +fn bench_repeated_small_on_shared_loader(c: &mut Criterion) { + let loader = NickelLoader::new(); + c.bench_function("repeated_small/shared_loader", |b| { + b.iter(|| loader.parse_string(black_box(SMALL_PAYLOAD), "rep.ncl")) + }); +} + +// --------------------------------------------------------------------------- +// Benchmark 5: Error path — parse of invalid input +// --------------------------------------------------------------------------- + +/// Measures the cost of the error path: how quickly the parser rejects +/// obviously invalid Nickel. A fast error path matters for tooling that +/// validates user input interactively. +fn bench_error_path(c: &mut Criterion) { + let loader = NickelLoader::new(); + let invalid = "{ broken syntax @@@ !!!"; + c.bench_function("error_path/invalid_input", |b| { + b.iter(|| loader.parse_string(black_box(invalid), "invalid.ncl")) + }); +} + +// --------------------------------------------------------------------------- +// Criterion wiring +// --------------------------------------------------------------------------- + +criterion_group!( + benches, + bench_throughput_parse, + bench_throughput_validate, + bench_loader_creation, + bench_repeated_small_on_shared_loader, + bench_error_path, +); +criterion_main!(benches); diff --git a/vendor/bunsenite/benches/parser.rs b/vendor/bunsenite/benches/parser.rs new file mode 100644 index 0000000..1cb51a1 --- /dev/null +++ b/vendor/bunsenite/benches/parser.rs @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Performance benchmarks for Bunsenite +//! +//! Run with: cargo bench + +use bunsenite::NickelLoader; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use std::hint::black_box; + +/// Simple configuration (~100 bytes) +const SIMPLE_CONFIG: &str = r#" +{ + name = "simple", + version = "1.0.0", + enabled = true, +} +"#; + +/// Medium configuration (~500 bytes) +const MEDIUM_CONFIG: &str = r#" +{ + name = "medium", + version = "1.0.0", + database = { + host = "localhost", + port = 5432, + name = "mydb", + ssl = true, + }, + server = { + host = "0.0.0.0", + port = 8080, + workers = 4, + timeout = 30, + }, + logging = { + level = "info", + format = "json", + file = "/var/log/app.log", + }, + features = { + auth = true, + cache = true, + metrics = true, + }, +} +"#; + +/// Complex configuration with contracts (~1500 bytes) +const COMPLEX_CONFIG: &str = r#" +let Port = std.contract.from_predicate (fun x => x >= 1 && x <= 65535) in +let NonEmpty = std.contract.from_predicate (fun x => std.string.length x > 0) in + +{ + name | NonEmpty = "complex-app", + version = "2.0.0", + + database = { + primary = { + host | NonEmpty = "db-primary.example.com", + port | Port = 5432, + name = "production", + pool_size = 20, + ssl = { + enabled = true, + verify = true, + ca_cert = "/etc/ssl/certs/ca.pem", + }, + }, + replica = { + host | NonEmpty = "db-replica.example.com", + port | Port = 5432, + name = "production", + pool_size = 10, + }, + }, + + servers = [ + { name = "web-1", host = "10.0.1.1", port | Port = 8080 }, + { name = "web-2", host = "10.0.1.2", port | Port = 8080 }, + { name = "web-3", host = "10.0.1.3", port | Port = 8080 }, + ], + + cache = { + redis = { + host = "redis.example.com", + port | Port = 6379, + db = 0, + ttl = 3600, + }, + }, + + logging = { + level = "info", + outputs = [ + { type = "console", format = "pretty" }, + { type = "file", path = "/var/log/app.log", format = "json" }, + { type = "syslog", facility = "local0" }, + ], + }, + + features = { + authentication = { enabled = true, provider = "oauth2" }, + rate_limiting = { enabled = true, requests_per_minute = 100 }, + caching = { enabled = true, strategy = "lru" }, + metrics = { enabled = true, endpoint = "/metrics" }, + }, +} +"#; + +fn benchmark_parse(c: &mut Criterion) { + let loader = NickelLoader::new(); + + let mut group = c.benchmark_group("parse"); + + // Simple config + group.throughput(Throughput::Bytes(SIMPLE_CONFIG.len() as u64)); + group.bench_with_input( + BenchmarkId::new("simple", SIMPLE_CONFIG.len()), + &SIMPLE_CONFIG, + |b, config| b.iter(|| loader.parse(black_box(*config), "simple.ncl")), + ); + + // Medium config + group.throughput(Throughput::Bytes(MEDIUM_CONFIG.len() as u64)); + group.bench_with_input( + BenchmarkId::new("medium", MEDIUM_CONFIG.len()), + &MEDIUM_CONFIG, + |b, config| b.iter(|| loader.parse(black_box(*config), "medium.ncl")), + ); + + // Complex config + group.throughput(Throughput::Bytes(COMPLEX_CONFIG.len() as u64)); + group.bench_with_input( + BenchmarkId::new("complex", COMPLEX_CONFIG.len()), + &COMPLEX_CONFIG, + |b, config| b.iter(|| loader.parse(black_box(*config), "complex.ncl")), + ); + + group.finish(); +} + +fn benchmark_validate(c: &mut Criterion) { + let loader = NickelLoader::new(); + + let mut group = c.benchmark_group("validate"); + + group.bench_with_input( + BenchmarkId::new("simple", SIMPLE_CONFIG.len()), + &SIMPLE_CONFIG, + |b, config| b.iter(|| loader.validate(black_box(*config), "simple.ncl")), + ); + + group.bench_with_input( + BenchmarkId::new("medium", MEDIUM_CONFIG.len()), + &MEDIUM_CONFIG, + |b, config| b.iter(|| loader.validate(black_box(*config), "medium.ncl")), + ); + + group.bench_with_input( + BenchmarkId::new("complex", COMPLEX_CONFIG.len()), + &COMPLEX_CONFIG, + |b, config| b.iter(|| loader.validate(black_box(*config), "complex.ncl")), + ); + + group.finish(); +} + +fn benchmark_loader_creation(c: &mut Criterion) { + c.bench_function("loader_creation", |b| { + b.iter(|| black_box(NickelLoader::new())) + }); +} + +criterion_group!( + benches, + benchmark_parse, + benchmark_validate, + benchmark_loader_creation +); +criterion_main!(benches); diff --git a/vendor/bunsenite/bindings/deno/README.adoc b/vendor/bunsenite/bindings/deno/README.adoc new file mode 100644 index 0000000..02d91a1 --- /dev/null +++ b/vendor/bunsenite/bindings/deno/README.adoc @@ -0,0 +1,247 @@ +== Bunsenite Deno Bindings + +image:https://img.shields.io/badge/License-MPL–2.0-blue.svg[License: +MPL-2.0,link="`https://github.com/hyperpolymath/palimpsest-license`"] + +TypeScript bindings for +https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite[Bunsenite] +using Deno’s native FFI. + +=== Installation + +[arabic] +. Build the Bunsenite native library: + +[source,bash] +---- +cd ../.. +cargo build --release +---- + +[arabic, start=2] +. Import the bindings in your Deno code: + +[source,typescript] +---- +import { parseNickel } from "https://raw.githubusercontent.com/example/bunsenite/main/bindings/deno/bunsenite.ts"; +---- + +Or use local path: + +[source,typescript] +---- +import { parseNickel } from "./bunsenite.ts"; +---- + +=== Usage + +==== Basic Parsing + +[source,typescript] +---- +import { parseNickel } from "./bunsenite.ts"; + +const config = parseNickel( + `{ + name = "my-app", + version = "1.0.0", + port = 8080, + }`, + "config.ncl" +); + +console.log(config.port); // 8080 +---- + +==== Parse File + +[source,typescript] +---- +import { parseFile } from "./bunsenite.ts"; + +const config = await parseFile("./config.ncl"); +console.log(config); +---- + +==== Validation + +[source,typescript] +---- +import { validateNickel } from "./bunsenite.ts"; + +try { + validateNickel('{ foo = 42 }', "config.ncl"); + console.log("Valid!"); +} catch (e) { + console.error("Invalid:", e.message); +} +---- + +==== Library Info + +[source,typescript] +---- +import { getVersion, getRSRTier, getTPCFPerimeter } from "./bunsenite.ts"; + +console.log("Version:", getVersion()); +console.log("RSR Tier:", getRSRTier()); +console.log("TPCF Perimeter:", getTPCFPerimeter()); +---- + +=== API Reference + +==== `+parseNickel(source: string, name: string): unknown+` + +Parse and evaluate a Nickel configuration string. + +* `+source+`: The Nickel configuration source code +* `+name+`: A name for this configuration (used in error messages) +* Returns: Parsed configuration as a JavaScript object +* Throws: Error if parsing or evaluation fails + +==== `+validateNickel(source: string, name: string): boolean+` + +Validate a Nickel configuration without evaluating it. + +* `+source+`: The Nickel configuration source code +* `+name+`: A name for this configuration (used in error messages) +* Returns: `+true+` if valid +* Throws: Error if validation fails + +==== `+parseFile(path: string): Promise+` + +Parse a Nickel configuration file. + +* `+path+`: Path to the Nickel configuration file +* Returns: Parsed configuration as a JavaScript object +* Throws: Error if file cannot be read or parsing fails + +==== `+validateFile(path: string): Promise+` + +Validate a Nickel configuration file. + +* `+path+`: Path to the Nickel configuration file +* Returns: `+true+` if valid +* Throws: Error if file cannot be read or validation fails + +==== `+getVersion(): string+` + +Get Bunsenite library version. + +* Returns: Version string (e.g., "`0.1.0`") + +==== `+getRSRTier(): string+` + +Get RSR compliance tier. + +* Returns: RSR tier (e.g., "`bronze`") + +==== `+getTPCFPerimeter(): number+` + +Get TPCF perimeter number. + +* Returns: Perimeter number (3 for Community Sandbox) + +=== Permissions + +Deno requires the following permissions: + +* `+--allow-ffi+`: To load the native library +* `+--allow-read+`: To read configuration files (if using `+parseFile+`) + +Example: + +[source,bash] +---- +deno run --allow-ffi --allow-read example.ts +---- + +=== Examples + +See link:./example.ts[example.ts] for comprehensive examples. + +Run the example: + +[source,bash] +---- +# Make sure bunsenite is built first +cd ../.. +cargo build --release + +# Run example +cd bindings/deno +deno run --allow-ffi --allow-read example.ts +---- + +=== Platform Support + +[cols=",,",options="header",] +|=== +|Platform |Library Name |Status +|Linux |`+libbunsenite.so+` |✅ +|macOS |`+libbunsenite.dylib+` |✅ +|Windows |`+bunsenite.dll+` |✅ +|=== + +The bindings automatically detect your platform and load the correct +library. + +=== Architecture + +.... +┌─────────────────┐ +│ Deno Runtime │ +│ (TypeScript) │ +└────────┬────────┘ + │ FFI + ▼ + ┌──────────┐ + │ Zig FFI │ + │ (C ABI) │ + └─────┬────┘ + │ + ▼ +┌─────────────────┐ +│ Rust Core │ +│ (lib.rs) │ +│ │ +│ nickel-lang-core│ +│ 0.9.1 │ +└─────────────────┘ +.... + +=== Performance + +~90% of native Rust performance (minimal C ABI overhead). + +=== Security + +* *Memory Safety*: Rust ownership model prevents memory errors +* *Type Safety*: Full type checking via Nickel + Rust +* *No `+unsafe+`*: Zero unsafe code blocks in Bunsenite core +* *Offline-First*: No network dependencies + +=== License + +Dual MPL-2.0 + MPL-2.0 v0.8 + +See link:../../LICENSE[LICENSE] for details. + +=== Contributing + +See link:../../CONTRIBUTING.md[CONTRIBUTING.md] for development +guidelines. + +=== Support + +* *Issues*: +https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite/-/issues[GitLab +Issues] +* *Discussions*: +https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite/-/issues[GitLab +Discussions] +* *Documentation*: link:../../README.md[Main README] + +''''' + +Made with ❤️ by the Campaign for Cooler Coding and Programming diff --git a/vendor/bunsenite/bindings/deno/bunsenite.affine b/vendor/bunsenite/bindings/deno/bunsenite.affine new file mode 100644 index 0000000..b917f06 --- /dev/null +++ b/vendor/bunsenite/bindings/deno/bunsenite.affine @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: MPL-2.0 +// Ported via Harvard Engine (Semantic pass) + +module bunsenite; + +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// Bunsenite Deno FFI Bindings +// TypeScript bindings for Deno runtime using native FFI +// +// NOTE: This is Deno-specific TypeScript, NOT plain TypeScript! +// It uses Deno.dlopen for native FFI calls to the Zig C ABI layer. +// +// Usage: +// import { parseNickel, validateNickel } from "./bunsenite.ts"; +// let result = parseNickel('{ foo = 42 }', "config.ncl"); +// console.log(result); + +// Detect library path based on platform +fn getLibraryPath(): string { + let platform = Deno.build.os; + let libName = platform === "windows" ? "bunsenite.dll" + : platform === "darwin" ? "libbunsenite.dylib" + : "libbunsenite.so"; + + // Try common locations + let paths = [ + `../../target/release/${libName}`, + `./target/release/${libName}`, + `./${libName}`, + ]; + + for (const path of paths) { + try { + Deno.statSync(path); + return path; + } catch { + // File doesn't exist, try next + } + } + + throw new Error( + `Could not find ${libName}. Please build with: cargo build --release`, + ); +} + +// FFI symbol definitions +// These match the C ABI exported by the Zig layer +let symbols = { + // Parse Nickel string to JSON + // char* parse_nickel(const char* source, const char* name) + parse_nickel: { + parameters: ["pointer", "pointer"], + result: "pointer", + }, + + // Validate Nickel without evaluating + // int validate_nickel(const char* source, const char* name) + validate_nickel: { + parameters: ["pointer", "pointer"], + result: "i32", + }, + + // Free string allocated by Rust + // void free_string(char* ptr) + free_string: { + parameters: ["pointer"], + result: "void", + }, + + // Get library version + // const char* version() + version: { + parameters: [], + result: "pointer", + }, + + // Get RSR tier + // const char* rsr_tier() + rsr_tier: { + parameters: [], + result: "pointer", + }, + + // Get TPCF perimeter + // uint8_t tpcf_perimeter() + tpcf_perimeter: { + parameters: [], + result: "u8", + }, +} as const; + +// Load the native library +let lib: Deno.DynamicLibrary | null = null; + +fn getLib(): Deno.DynamicLibrary { + if (!lib) { + let libPath = getLibraryPath(); + lib = Deno.dlopen(libPath, symbols); + } + return lib; +} + +// Helper: Convert JS string to C string (null-terminated) +fn toCString(str: string): Uint8Array { + let encoder = new TextEncoder(); + let encoded = encoder.encode(str + "\0"); + return encoded; +} + +// Helper: Convert C string pointer to JS string +fn fromCString(ptr: Deno.UnsafePointer): string { + if (!ptr) { + throw new Error("Null pointer received from C"); + } + let view = new Deno.UnsafePointerView(ptr); + return view.getCString(); +} + +/** + * Parse and evaluate a Nickel configuration string + * + * @param source - The Nickel configuration source code + * @param name - A name for this configuration (used in error messages) + * @returns Parsed configuration as a JavaScript object + * @throws Error if parsing or evaluation fails + * + * @example + * ```typescript + * let config = parseNickel('{ name = "example", port = 8080 }', "config.ncl"); + * console.log(config.port); // 8080 + * ``` + */ +fn parseNickel(source: string, name: string): unknown { + let library = getLib(); + + let sourceBytes = toCString(source); + let nameBytes = toCString(name); + + let resultPtr = library.symbols.parse_nickel( + sourceBytes, + nameBytes, + ) as Deno.UnsafePointer; + + if (!resultPtr) { + throw new Error(`Failed to parse Nickel config: ${name}`); + } + + try { + let jsonString = fromCString(resultPtr); + return JSON.parse(jsonString); + } finally { + // Free the string allocated by Rust + library.symbols.free_string(resultPtr); + } +} + +/** + * Validate a Nickel configuration without evaluating it + * + * @param source - The Nickel configuration source code + * @param name - A name for this configuration (used in error messages) + * @returns true if valid, throws Error if invalid + * @throws Error if validation fails + * + * @example + * ```typescript + * try { + * validateNickel('{ foo = 42 }', "config.ncl"); + * console.log("Valid!"); + * } catch (e) { + * console.error("Invalid:", e.message); + * } + * ``` + */ +fn validateNickel(source: string, name: string): boolean { + let library = getLib(); + + let sourceBytes = toCString(source); + let nameBytes = toCString(name); + + let result = library.symbols.validate_nickel( + sourceBytes, + nameBytes, + ); + + if (result !== 0) { + throw new Error(`Validation failed for: ${name}`); + } + + return true; +} + +/** + * Get Bunsenite library version + * + * @returns Version string (e.g., "0.1.0") + * + * @example + * ```typescript + * console.log("Bunsenite version:", getVersion()); + * ``` + */ +fn getVersion(): string { + let library = getLib(); + let ptr = library.symbols.version() as Deno.UnsafePointer; + return fromCString(ptr); +} + +/** + * Get RSR compliance tier + * + * @returns RSR tier (e.g., "bronze") + * + * @example + * ```typescript + * console.log("RSR tier:", getRSRTier()); + * ``` + */ +fn getRSRTier(): string { + let library = getLib(); + let ptr = library.symbols.rsr_tier() as Deno.UnsafePointer; + return fromCString(ptr); +} + +/** + * Get TPCF perimeter number + * + * @returns Perimeter number (3 for Community Sandbox) + * + * @example + * ```typescript + * console.log("TPCF perimeter:", getTPCFPerimeter()); + * ``` + */ +fn getTPCFPerimeter(): number { + let library = getLib(); + return library.symbols.tpcf_perimeter(); +} + +/** + * Parse a Nickel configuration file + * + * @param path - Path to the Nickel configuration file + * @returns Parsed configuration as a JavaScript object + * @throws Error if file cannot be read or parsing fails + * + * @example + * ```typescript + * let config = await parseFile("./config.ncl"); + * console.log(config); + * ``` + */ +async fn parseFile(path: string): unknown { + let source = await Deno.readTextFile(path); + return parseNickel(source, path); +} + +/** + * Validate a Nickel configuration file + * + * @param path - Path to the Nickel configuration file + * @returns true if valid, throws Error if invalid + * @throws Error if file cannot be read or validation fails + * + * @example + * ```typescript + * try { + * await validateFile("./config.ncl"); + * console.log("File is valid!"); + * } catch (e) { + * console.error("Invalid file:", e.message); + * } + * ``` + */ +async fn validateFile(path: string): boolean { + let source = await Deno.readTextFile(path); + return validateNickel(source, path); +} + +// Cleanup on exit +globalThis.addEventListener("unload", () => { + if (lib) { + lib.close(); + lib = null; + } +}); + +// Export type definitions +struct BunseniteConfig { Record; + +// Re-for convenience +default { + parseNickel, + validateNickel, + parseFile, + validateFile, + getVersion, + getRSRTier, + getTPCFPerimeter, +}; + diff --git a/vendor/bunsenite/bindings/deno/example.affine b/vendor/bunsenite/bindings/deno/example.affine new file mode 100644 index 0000000..4bf9120 --- /dev/null +++ b/vendor/bunsenite/bindings/deno/example.affine @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MPL-2.0 +// Ported via Harvard Engine (Semantic pass) + +module example; + +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +#!/usr/bin/env deno run --allow-ffi --allow-read + +// Bunsenite Deno Example +// Demonstrates how to use Bunsenite from Deno + +import { + getTPCFPerimeter, + getRSRTier, + getVersion, + parseFile, + parseNickel, + validateNickel, +} from "./bunsenite.ts"; + +console.log("=== Bunsenite Deno Example ===\n"); + +// Show library info +console.log("Library Information:"); +console.log(` Version: ${getVersion()}`); +console.log(` RSR Tier: ${getRSRTier()}`); +console.log(` TPCF Perimeter: ${getTPCFPerimeter()}`); +console.log(""); + +// Example 1: Parse simple inline config +console.log("Example 1: Parse inline config"); +let config1 = parseNickel( + `{ + name = "deno-example", + version = "1.0.0", + port = 8080, + }`, + "inline.ncl", +); +console.log("Result:", JSON.stringify(config1, null, 2)); +console.log(""); + +// Example 2: Parse with computations +console.log("Example 2: Parse with computations"); +let config2 = parseNickel( + `{ + base_port = 8000, + api_port = base_port + 80, + db_port = base_port + 432, + url = "http://localhost:" ++ std.string.from_number api_port, + }`, + "computed.ncl", +); +console.log("Result:", JSON.stringify(config2, null, 2)); +console.log(""); + +// Example 3: Validate config +console.log("Example 3: Validate config"); +try { + validateNickel('{ valid = true, works = "yes" }', "valid.ncl"); + console.log("✓ Config is valid"); +} catch (e) { + console.error("✗ Config is invalid:", e.message); +} +console.log(""); + +// Example 4: Validate invalid config (should fail) +console.log("Example 4: Validate invalid config"); +try { + validateNickel("{ invalid = }", "invalid.ncl"); // Missing value + console.log("✓ Config is valid"); +} catch (e) { + console.log("✓ Correctly detected invalid config"); +} +console.log(""); + +// Example 5: Parse file (if it exists) +console.log("Example 5: Parse file"); +try { + let config = await parseFile("../../examples/config.ncl"); + console.log("Parsed config from file:"); + console.log(` Name: ${(config as unknown).name}`); + console.log(` Version: ${(config as unknown).version}`); + console.log(` Server port: ${(config as unknown).server.port}`); +} catch (e) { + console.log(`Could not parse file: ${e.message}`); + console.log("(This is expected if bunsenite hasn't been built yet)"); +} +console.log(""); + +// Example 6: Advanced features +console.log("Example 6: Advanced features"); +let config6 = parseNickel( + `{ + # Comments work! + app_name = "bunsenite", + + # Lists + allowed_hosts = ["localhost", "127.0.0.1", "::1"], + + # Nested records + database = { + host = "localhost", + port = 5432, + max_connections = 20, + }, + + # Computed values + db_url = "postgres://" ++ database.host ++ ":" ++ std.string.from_number database.port, + }`, + "advanced.ncl", +); +console.log("Advanced config:", JSON.stringify(config6, null, 2)); + +console.log("\n✓ All examples completed successfully!"); + diff --git a/vendor/bunsenite/bindings/rescript/Bunsenite.affine b/vendor/bunsenite/bindings/rescript/Bunsenite.affine new file mode 100644 index 0000000..7a1072f --- /dev/null +++ b/vendor/bunsenite/bindings/rescript/Bunsenite.affine @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MPL-2.0 +// Ported via Harvard Engine (Semantic pass) + +module Bunsenite; + +// Bunsenite Rescript Bindings +// Type-safe Rescript bindings for Bunsenite via C FFI +// +// Usage: +// open Bunsenite +// fn config = parseNickel("{foo = 42}", "config.ncl") +// Js.log(config) + +// External C FFI declarations +// These bind to the C ABI provided by the Zig layer + +@module("./bunsenite_ffi") +external parseNickelRaw: (string, string) => Js.Nullable.t = "parse_nickel" + +@module("./bunsenite_ffi") +external validateNickelRaw: (string, string) => int = "validate_nickel" + +@module("./bunsenite_ffi") +external versionRaw: unit => string = "version" + +@module("./bunsenite_ffi") +external rsrTierRaw: unit => string = "rsr_tier" + +@module("./bunsenite_ffi") +external tpcfPerimeterRaw: unit => int = "tpcf_perimeter" + +// Result struct for error handling +struct result<'a, 'e> = Ok('a) | Error('e) + +// Error struct +struct error { + | ParseError(string) + | ValidationError(string) + | InvalidInput(string) + +// Parse and evaluate a Nickel configuration string +// +// Example: +// fn config = parseNickel("{name = \"example\", port = 8080}", "config.ncl") +// switch config { +// | Ok(json) => Js.log(json) +// | Error(err) => Js.log2("Error:", err) +// } +fn parseNickel = (source: string, name: string): result => { + fn result = parseNickelRaw(source, name) + + switch Js.Nullable.toOption(result) { + | Some(jsonString) => + try { + fn parsed = Js.Json.parseExn(jsonString) + Ok(parsed) + } catch { + | _ => Error(ParseError("Failed to parse JSON result")) + } + | None => Error(ParseError("Failed to parse Nickel configuration: " ++ name)) + } +} + +// Validate a Nickel configuration without evaluating it +// +// Example: +// fn result = validateNickel("{foo = 42}", "config.ncl") +// switch result { +// | Ok() => Js.log("Valid!") +// | Error(err) => Js.log2("Invalid:", err) +// } +fn validateNickel = (source: string, name: string): result => { + fn resultCode = validateNickelRaw(source, name) + + if resultCode == 0 { + Ok() + } else { + Error(ValidationError("Validation failed for: " ++ name)) + } +} + +// Get library version +// +// Example: +// fn ver = getVersion() +// Js.log2("Version:", ver) +fn getVersion = (): string => { + versionRaw() +} + +// Get RSR compliance tier +// +// Example: +// fn tier = getRSRTier() +// Js.log2("RSR Tier:", tier) +fn getRSRTier = (): string => { + rsrTierRaw() +} + +// Get TPCF perimeter number +// +// Example: +// fn perimeter = getTPCFPerimeter() +// Js.log2("TPCF Perimeter:", perimeter) +fn getTPCFPerimeter = (): int => { + tpcfPerimeterRaw() +} + +// Helper: Parse Nickel file from filesystem +// Requires Node.js fs module +// +// Example: +// fn config = parseFile("./config.ncl") +// switch config { +// | Ok(json) => Js.log(json) +// | Error(err) => Js.log2("Error:", err) +// } +@module("fs") +external readFileSync: (string, string) => string = "readFileSync" + +fn parseFile = (path: string): result => { + try { + fn source = readFileSync(path, "utf8") + parseNickel(source, path) + } catch { + | _ => Error(InvalidInput("Failed to read file: " ++ path)) + } +} + +// Helper: Validate Nickel file from filesystem +// +// Example: +// fn result = validateFile("./config.ncl") +// switch result { +// | Ok() => Js.log("Valid!") +// | Error(err) => Js.log2("Invalid:", err) +// } +fn validateFile = (path: string): result => { + try { + fn source = readFileSync(path, "utf8") + validateNickel(source, path) + } catch { + | _ => Error(InvalidInput("Failed to read file: " ++ path)) + } +} + +// Helper: Get config value by key path +// Example: getConfigValue(config, ["server", "port"]) +fn rec getConfigValue = (json: Js.Json.t, path: list): option => { + switch path { + | list{} => Some(json) + | list{key, ...rest} => + switch Js.Json.decodeObject(json) { + | Some(obj) => + switch Js.Dict.get(obj, key) { + | Some(value) => getConfigValue(value, rest) + | None => None + } + | None => None + } + } +} + +// Helper: Convert error to string for display +fn errorToString = (err: error): string => { + switch err { + | ParseError(msg) => "Parse Error: " ++ msg + | ValidationError(msg) => "Validation Error: " ++ msg + | InvalidInput(msg) => "Invalid Input: " ++ msg + } +} + +// Re-export result struct for convenience +struct parseResult { result +struct validateResult { result + diff --git a/vendor/bunsenite/bindings/rescript/Bunsenite_test.affine b/vendor/bunsenite/bindings/rescript/Bunsenite_test.affine new file mode 100644 index 0000000..3a4c9c0 --- /dev/null +++ b/vendor/bunsenite/bindings/rescript/Bunsenite_test.affine @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MPL-2.0 +// Ported via Harvard Engine (Semantic pass) + +module Bunsenite_test; + +// SPDX-License-Identifier: MPL-2.0 +// Bunsenite ReScript Bindings Test Suite + +open Bunsenite + +// Test helpers +fn assertEqual = (actual, expected, testName) => { + if actual == expected { + Console.log(`✓ ${testName}`) + } else { + Console.error(`✗ ${testName}`) + Console.error(` Expected: ${expected->Js.Json.stringify}`) + Console.error(` Actual: ${actual->Js.Json.stringify}`) + } +} + +fn assertOk = (result, testName) => { + switch result { + | Ok(_) => Console.log(`✓ ${testName}`) + | Error(err) => { + Console.error(`✗ ${testName}`) + Console.error(` Error: ${errorToString(err)}`) + } + } +} + +fn assertError = (result, testName) => { + switch result { + | Error(_) => Console.log(`✓ ${testName}`) + | Ok(_) => Console.error(`✗ ${testName}: Expected error but got Ok`) + } +} + +// Test suite +fn runTests = () => { + Console.log("\n🧪 Bunsenite ReScript Bindings Test Suite\n") + + // Test 1: Parse simple Nickel configuration + Console.log("Parse Tests:") + fn simpleConfig = parseNickel("{foo = 42}", "test.ncl") + assertOk(simpleConfig, "Parse simple number configuration") + + // Test 2: Parse object configuration + fn objectConfig = parseNickel("{name = \"test\", value = 100}", "object.ncl") + assertOk(objectConfig, "Parse object configuration") + + // Test 3: Parse nested configuration + fn nestedConfig = parseNickel("{server = {port = 8080, host = \"localhost\"}}", "nested.ncl") + assertOk(nestedConfig, "Parse nested configuration") + + // Test 4: Parse array configuration + fn arrayConfig = parseNickel("{items = [1, 2, 3, 4, 5]}", "array.ncl") + assertOk(arrayConfig, "Parse array configuration") + + // Test 5: Parse invalid syntax (should error) + fn invalidConfig = parseNickel("{foo = }", "invalid.ncl") + assertError(invalidConfig, "Parse invalid syntax returns error") + + // Test 6: Parse empty configuration + fn emptyConfig = parseNickel("{}", "empty.ncl") + assertOk(emptyConfig, "Parse empty configuration") + + // Validation Tests + Console.log("\nValidation Tests:") + + fn validConfig = validateNickel("{foo = 42}", "valid.ncl") + assertOk(validConfig, "Validate correct configuration") + + fn invalidValidation = validateNickel("{foo = }", "invalid-validate.ncl") + assertError(invalidValidation, "Validate incorrect configuration returns error") + + // Test 7: Validate complex configuration + fn complexValid = validateNickel( + "{ + app = { + name = \"example\", + version = \"1.0.0\", + config = { + debug = true, + port = 3000 + } + } + }", + "complex.ncl", + ) + assertOk(complexValid, "Validate complex nested configuration") + + // Library Info Tests + Console.log("\nLibrary Info Tests:") + + fn version = getVersion() + Console.log(`✓ Got version: ${version}`) + + fn tier = getRSRTier() + Console.log(`✓ Got RSR tier: ${tier}`) + + fn perimeter = getTPCFPerimeter() + Console.log(`✓ Got TPCF perimeter: ${perimeter->Int.toString}`) + + // Config Value Tests + Console.log("\nConfig Value Extraction Tests:") + + switch objectConfig { + | Ok(json) => { + // Test extracting top-level value + fn nameValue = getConfigValue(json, list{"name"}) + switch nameValue { + | Some(_) => Console.log("✓ Extract top-level value") + | None => Console.error("✗ Failed to extract top-level value") + } + + // Test extracting non-existent value + fn missingValue = getConfigValue(json, list{"missing"}) + switch missingValue { + | None => Console.log("✓ Non-existent value returns None") + | Some(_) => Console.error("✗ Non-existent value should return None") + } + } + | Error(_) => Console.error("✗ Could not test config value extraction") + } + + switch nestedConfig { + | Ok(json) => { + // Test extracting nested value + fn portValue = getConfigValue(json, list{"server", "port"}) + switch portValue { + | Some(_) => Console.log("✓ Extract nested value") + | None => Console.error("✗ Failed to extract nested value") + } + + // Test extracting with invalid path + fn invalidPath = getConfigValue(json, list{"server", "nonexistent", "deep"}) + switch invalidPath { + | None => Console.log("✓ Invalid nested path returns None") + | Some(_) => Console.error("✗ Invalid path should return None") + } + } + | Error(_) => Console.error("✗ Could not test nested value extraction") + } + + // Error handling tests + Console.log("\nError Handling Tests:") + + fn parseErr = parseNickel("{invalid syntax here}", "error-test.ncl") + switch parseErr { + | Error(err) => { + fn errStr = errorToString(err) + Console.log(`✓ Error converted to string: ${errStr}`) + } + | Ok(_) => Console.error("✗ Expected parse error") + } + + // Result struct tests + Console.log("\nResult Type Tests:") + + fn successResult: parseResult = Ok(Js.Json.null) + switch successResult { + | Ok(_) => Console.log("✓ parseResult Ok variant works") + | Error(_) => Console.error("✗ parseResult Ok variant failed") + } + + fn errorResult: parseResult = Error(ParseError("test")) + switch errorResult { + | Error(_) => Console.log("✓ parseResult Error variant works") + | Ok(_) => Console.error("✗ parseResult Error variant failed") + } + + fn validateSuccess: validateResult = Ok() + switch validateSuccess { + | Ok() => Console.log("✓ validateResult Ok variant works") + | Error(_) => Console.error("✗ validateResult Ok variant failed") + } + + Console.log("\n✅ Test suite complete\n") +} + +// Run tests +runTests() + diff --git a/vendor/bunsenite/bindings/rescript/Example.affine b/vendor/bunsenite/bindings/rescript/Example.affine new file mode 100644 index 0000000..790537f --- /dev/null +++ b/vendor/bunsenite/bindings/rescript/Example.affine @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MPL-2.0 +// Ported via Harvard Engine (Semantic pass) + +module Example; + +// SPDX-License-Identifier: MPL-2.0 +// Bunsenite ReScript Bindings Example + +open Bunsenite + +// Example 1: Simple parsing +fn example1 = () => { + Console.log("\n📝 Example 1: Simple Configuration Parsing\n") + + fn config = parseNickel( + "{ + app_name = \"my-application\", + version = \"1.0.0\", + port = 8080 + }", + "app-config.ncl", + ) + + switch config { + | Ok(json) => { + Console.log("✓ Configuration parsed successfully!") + Console.log(Js.Json.stringify(json)) + + // Extract specific values + switch getConfigValue(json, list{"app_name"}) { + | Some(name) => Console.log(`App name: ${Js.Json.stringify(name)}`) + | None => Console.log("App name not found") + } + + switch getConfigValue(json, list{"port"}) { + | Some(port) => Console.log(`Port: ${Js.Json.stringify(port)}`) + | None => Console.log("Port not found") + } + } + | Error(err) => Console.error(`✗ Parse error: ${errorToString(err)}`) + } +} + +// Example 2: Nested configuration +fn example2 = () => { + Console.log("\n📝 Example 2: Nested Configuration\n") + + fn config = parseNickel( + "{ + server = { + host = \"0.0.0.0\", + port = 3000, + tls = { + enabled = true, + cert_path = \"/path/to/cert.pem\" + } + }, + database = { + host = \"localhost\", + port = 5432, + name = \"myapp\" + } + }", + "server-config.ncl", + ) + + switch config { + | Ok(json) => { + Console.log("✓ Nested configuration parsed!") + + // Extract deeply nested values + switch getConfigValue(json, list{"server", "tls", "enabled"}) { + | Some(tls) => Console.log(`TLS enabled: ${Js.Json.stringify(tls)}`) + | None => Console.log("TLS setting not found") + } + + switch getConfigValue(json, list{"database", "name"}) { + | Some(dbName) => Console.log(`Database name: ${Js.Json.stringify(dbName)}`) + | None => Console.log("Database name not found") + } + } + | Error(err) => Console.error(`✗ Parse error: ${errorToString(err)}`) + } +} + +// Example 3: Validation before parsing +fn example3 = () => { + Console.log("\n📝 Example 3: Configuration Validation\n") + + fn configSource = "{ + api_key = \"secret-key-123\", + timeout = 30, + retries = 3 + }" + + // First validate + fn validation = validateNickel(configSource, "api-config.ncl") + + switch validation { + | Ok() => { + Console.log("✓ Configuration is valid, proceeding to parse...") + + // Now parse + switch parseNickel(configSource, "api-config.ncl") { + | Ok(json) => { + Console.log("✓ Configuration parsed successfully!") + Console.log(Js.Json.stringify(json)) + } + | Error(err) => Console.error(`✗ Parse error: ${errorToString(err)}`) + } + } + | Error(err) => Console.error(`✗ Validation failed: ${errorToString(err)}`) + } +} + +// Example 4: Error handling +fn example4 = () => { + Console.log("\n📝 Example 4: Error Handling\n") + + fn invalidConfig = "{ + this is not = valid nickel syntax + }" + + fn result = parseNickel(invalidConfig, "bad-config.ncl") + + switch result { + | Ok(json) => { + Console.log("Parsed (unexpected):") + Console.log(Js.Json.stringify(json)) + } + | Error(ParseError(msg)) => { + Console.log(`✓ Caught parse error: ${msg}`) + Console.log("This is expected - the syntax was invalid") + } + | Error(ValidationError(msg)) => Console.log(`Validation error: ${msg}`) + | Error(InvalidInput(msg)) => Console.log(`Invalid input: ${msg}`) + } +} + +// Example 5: Array configuration +fn example5 = () => { + Console.log("\n📝 Example 5: Array Configuration\n") + + fn config = parseNickel( + "{ + users = [ + \"alice\", + \"bob\", + \"charlie\" + ], + ports = [8080, 8081, 8082], + features = { + enabled = [\"auth\", \"logging\", \"metrics\"] + } + }", + "array-config.ncl", + ) + + switch config { + | Ok(json) => { + Console.log("✓ Array configuration parsed!") + + switch getConfigValue(json, list{"users"}) { + | Some(users) => Console.log(`Users: ${Js.Json.stringify(users)}`) + | None => Console.log("Users not found") + } + + switch getConfigValue(json, list{"features", "enabled"}) { + | Some(features) => Console.log(`Enabled features: ${Js.Json.stringify(features)}`) + | None => Console.log("Features not found") + } + } + | Error(err) => Console.error(`✗ Parse error: ${errorToString(err)}`) + } +} + +// Example 6: Library information +fn example6 = () => { + Console.log("\n📝 Example 6: Library Information\n") + + Console.log(`Bunsenite version: ${getVersion()}`) + Console.log(`RSR compliance tier: ${getRSRTier()}`) + Console.log(`TPCF perimeter: ${getTPCFPerimeter()->Int.toString}`) +} + +// Example 7: Type-safe configuration with pattern matching +fn example7 = () => { + Console.log("\n📝 Example 7: Type-Safe Configuration Access\n") + + fn config = parseNickel( + "{ + mode = \"production\", + debug = false, + log_level = \"info\" + }", + "env-config.ncl", + ) + + // Type-safe access with exhaustive pattern matching + fn mode = switch config { + | Ok(json) => + switch getConfigValue(json, list{"mode"}) { + | Some(value) => + switch Js.Json.classify(value) { + | JSONString(str) => Some(str) + | _ => None + } + | None => None + } + | Error(_) => None + } + + switch mode { + | Some("production") => Console.log("✓ Running in production mode") + | Some("development") => Console.log("Running in development mode") + | Some(other) => Console.log(`Running in ${other} mode`) + | None => Console.log("Mode not specified") + } +} + +// Run all examples +fn runExamples = () => { + Console.log("🎯 Bunsenite ReScript Bindings Examples") + Console.log("=" |> Js.String.repeat(50)) + + example1() + example2() + example3() + example4() + example5() + example6() + example7() + + Console.log("\n✅ All examples completed!\n") +} + +// Export for use in other files +fn examples = [ + ("simple", example1), + ("nested", example2), + ("validation", example3), + ("error-handling", example4), + ("arrays", example5), + ("library-info", example6), + ("struct-safe", example7), +] + +// Run if executed directly +runExamples() + diff --git a/vendor/bunsenite/bindings/rescript/README.adoc b/vendor/bunsenite/bindings/rescript/README.adoc new file mode 100644 index 0000000..248c571 --- /dev/null +++ b/vendor/bunsenite/bindings/rescript/README.adoc @@ -0,0 +1,267 @@ +== Bunsenite Rescript Bindings + +image:https://img.shields.io/badge/License-PMPL–1.0-blue.svg[License: +MPL-2.0,link="`https://github.com/hyperpolymath/palimpsest-license`"] + +Type-safe Rescript bindings for +https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite[Bunsenite] +via C FFI. + +=== Installation + +[arabic] +. Build the Bunsenite native library: + +[source,bash] +---- +cd ../.. +cargo build --release +---- + +[arabic, start=2] +. Add Bunsenite bindings to your Rescript project: + +[source,bash] +---- +# Copy bindings to your project +cp bindings/affinescript/Bunsenite.res src/ +---- + +[arabic, start=3] +. Configure FFI in your `+bsconfig.json+`: + +[source,json] +---- +{ + "name": "your-project", + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "bs-dependencies": [], + "external-stdlibs": ["bunsenite"] +} +---- + +=== Usage + +==== Basic Parsing + +[source,affinescript] +---- +open Bunsenite + +let config = parseNickel( + "{ + name = \"my-app\", + version = \"1.0.0\", + port = 8080, + }", + "config.ncl" +) + +switch config { +| Ok(json) => Js.log(json) +| Error(err) => Js.log2("Error:", errorToString(err)) +} +---- + +==== Parse File + +[source,affinescript] +---- +open Bunsenite + +let config = parseFile("./config.ncl") + +switch config { +| Ok(json) => { + // Access nested values + let port = getConfigValue(json, list{"server", "port"}) + Js.log2("Server port:", port) + } +| Error(err) => Js.log2("Error:", errorToString(err)) +} +---- + +==== Validation + +[source,affinescript] +---- +open Bunsenite + +let result = validateNickel("{foo = 42}", "config.ncl") + +switch result { +| Ok() => Js.log("Valid!") +| Error(err) => Js.log2("Invalid:", errorToString(err)) +} +---- + +==== Library Info + +[source,affinescript] +---- +open Bunsenite + +Js.log2("Version:", getVersion()) +Js.log2("RSR Tier:", getRSRTier()) +Js.log2("TPCF Perimeter:", getTPCFPerimeter()) +---- + +=== API Reference + +==== Types + +[source,affinescript] +---- +type result<'a, 'e> = Ok('a) | Error('e) + +type error = + | ParseError(string) + | ValidationError(string) + | InvalidInput(string) + +type parseResult = result +type validateResult = result +---- + +==== Functions + +===== `+parseNickel(source: string, name: string): parseResult+` + +Parse and evaluate a Nickel configuration string. + +* `+source+`: The Nickel configuration source code +* `+name+`: A name for this configuration (used in error messages) +* Returns: `+Ok(Js.Json.t)+` on success, `+Error(error)+` on failure + +===== `+validateNickel(source: string, name: string): validateResult+` + +Validate a Nickel configuration without evaluating it. + +* `+source+`: The Nickel configuration source code +* `+name+`: A name for this configuration (used in error messages) +* Returns: `+Ok()+` if valid, `+Error(error)+` if invalid + +===== `+parseFile(path: string): parseResult+` + +Parse a Nickel configuration file. + +* `+path+`: Path to the Nickel configuration file +* Returns: `+Ok(Js.Json.t)+` on success, `+Error(error)+` on failure + +===== `+validateFile(path: string): validateResult+` + +Validate a Nickel configuration file. + +* `+path+`: Path to the Nickel configuration file +* Returns: `+Ok()+` if valid, `+Error(error)+` if invalid + +===== `+getVersion(): string+` + +Get Bunsenite library version. + +* Returns: Version string (e.g., "`0.1.0`") + +===== `+getRSRTier(): string+` + +Get RSR compliance tier. + +* Returns: RSR tier (e.g., "`bronze`") + +===== `+getTPCFPerimeter(): int+` + +Get TPCF perimeter number. + +* Returns: Perimeter number (3 for Community Sandbox) + +==== Helper Functions + +===== `+getConfigValue(json: Js.Json.t, path: list): option+` + +Get a value from a configuration object by key path. + +Example: + +[source,affinescript] +---- +let port = getConfigValue(config, list{"server", "port"}) +---- + +===== `+errorToString(err: error): string+` + +Convert an error to a string for display. + +=== Architecture + +.... +┌─────────────────┐ +│ Rescript │ +│ (Type-safe) │ +└────────┬────────┘ + │ FFI + ▼ + ┌──────────┐ + │ Zig FFI │ + │ (C ABI) │ + └─────┬────┘ + │ + ▼ +┌─────────────────┐ +│ Rust Core │ +│ (lib.rs) │ +│ │ +│ nickel-lang-core│ +│ 0.9.1 │ +└─────────────────┘ +.... + +=== Performance + +~90% of native Rust performance (minimal C ABI overhead). + +=== Type Safety + +Rescript provides: - *Compile-time type checking*: Catch errors before +runtime - *Sound type system*: No `+null+` or `+undefined+` surprises - +*Pattern matching*: Exhaustive error handling via `+result+` type - +*Immutability*: Default immutability prevents bugs + +Combined with Bunsenite’s Rust core: - *Memory safety*: Rust ownership +model - *Type safety*: Nickel + Rust type checking - *No runtime +errors*: Caught at compile time + +=== Security + +* *Memory Safety*: Rust ownership model prevents memory errors +* *Type Safety*: Rescript + Nickel + Rust triple type checking +* *No `+unsafe+`*: Zero unsafe code blocks in Bunsenite core +* *Offline-First*: No network dependencies + +=== License + +Dual MPL-2.0 + MPL-2.0 v0.8 + +See link:../../LICENSE[LICENSE] for details. + +=== Contributing + +See link:../../CONTRIBUTING.md[CONTRIBUTING.md] for development +guidelines. + +=== Support + +* *Issues*: +https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite/-/issues[GitLab +Issues] +* *Discussions*: +https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite/-/issues[GitLab +Discussions] +* *Documentation*: link:../../README.md[Main README] + +''''' + +Made with ❤️ by the Campaign for Cooler Coding and Programming diff --git a/vendor/bunsenite/bindings/rescript/bunsenite.d.affine b/vendor/bunsenite/bindings/rescript/bunsenite.d.affine new file mode 100644 index 0000000..bbf71a2 --- /dev/null +++ b/vendor/bunsenite/bindings/rescript/bunsenite.d.affine @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MPL-2.0 +// Ported via Harvard Engine (Semantic pass) + +module bunsenite.d; + +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// TypeScript type definitions for bunsenite +// These types are for the Node.js/Bun FFI bindings + +/** + * Parse a Nickel configuration string and return the result as JSON + * @param source - The Nickel source code to parse + * @param name - The name of the file (for error messages) + * @returns The parsed configuration as a JSON string, or null on error + */ +fn parse_nickel(source: string, name: string): string | null; + +/** + * Validate a Nickel configuration without evaluating it + * @param source - The Nickel source code to validate + * @param name - The name of the file (for error messages) + * @returns 0 if valid, non-zero on error + */ +fn validate_nickel(source: string, name: string): number; + +/** + * Get the library version + * @returns The version string (e.g., "1.0.0") + */ +fn version(): string; + +/** + * Get the RSR compliance tier + * @returns The RSR tier (e.g., "bronze") + */ +fn rsr_tier(): string; + +/** + * Get the TPCF perimeter assignment + * @returns The perimeter number (e.g., 3) + */ +fn tpcf_perimeter(): number; + diff --git a/vendor/bunsenite/bindings/rescript/package.json b/vendor/bunsenite/bindings/rescript/package.json new file mode 100644 index 0000000..06c50ce --- /dev/null +++ b/vendor/bunsenite/bindings/rescript/package.json @@ -0,0 +1,61 @@ +{ + "name": "bunsenite", + "version": "1.0.0", + "description": "Nickel configuration file parser - AffineScript/Node.js bindings", + "main": "bunsenite_ffi_node.js", + "module": "bunsenite_ffi.js", + "types": "bunsenite.d.ts", + "exports": { + ".": { + "bun": "./bunsenite_ffi.js", + "node": "./bunsenite_ffi_node.js", + "default": "./bunsenite_ffi_node.js" + } + }, + "scripts": { + "build": "affinescript build", + "clean": "affinescript clean", + "test": "node test.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/campaign-for-cooler-coding-and-programming/bunsenite.git" + }, + "keywords": [ + "nickel", + "config", + "parser", + "ffi", + "affinescript" + ], + "author": "Campaign for Cooler Coding and Programming", + "license": "PMPL-1.0", + "bugs": { + "url": "https://github.com/campaign-for-cooler-coding-and-programming/bunsenite/-/issues" + }, + "homepage": "https://github.com/campaign-for-cooler-coding-and-programming/bunsenite", + "peerDependencies": { + "ffi-napi": "^4.0.0", + "ref-napi": "^3.0.0" + }, + "peerDependenciesMeta": { + "ffi-napi": { + "optional": true + }, + "ref-napi": { + "optional": true + } + }, + "engines": { + "node": ">=18.0.0" + }, + "files": [ + "bunsenite_ffi.js", + "bunsenite_ffi_node.js", + "Bunsenite.res", + "Bunsenite.res.js", + "bunsenite.d.ts", + "affinescript.json", + "README.md" + ] +} diff --git a/vendor/bunsenite/codemeta.json b/vendor/bunsenite/codemeta.json new file mode 100644 index 0000000..fa1060f --- /dev/null +++ b/vendor/bunsenite/codemeta.json @@ -0,0 +1,27 @@ +{ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "identifier": "bunsenite", + "name": "bunsenite", + "description": "RSR-compliant project", + "version": "0.1.0", + "dateCreated": "2025-12-10", + "dateModified": "2025-12-10", + "license": "PMPL-1.0", + "codeRepository": "https://github.com/hyperpolymath/bunsenite", + "issueTracker": "https://github.com/hyperpolymath/bunsenite/issues", + "programmingLanguage": ["Guile Scheme"], + "developmentStatus": "active", + "keywords": ["RSR", "rhodium-standard"], + "author": [{ + "@type": "Person", + "givenName": "Hyper", + "familyName": "Polymath", + "email": "hyperpolymath@proton.me" + }], + "isPartOf": [{ + "@type": "SoftwareApplication", + "name": "RSR Framework", + "url": "https://rhodium.sh" + }] +} diff --git a/vendor/bunsenite/config/README.adoc b/vendor/bunsenite/config/README.adoc new file mode 100644 index 0000000..7ca6ad2 --- /dev/null +++ b/vendor/bunsenite/config/README.adoc @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += Bunsenite K9 Configuration + +**Meta-Dogfooding in Action**: The Nickel tooling project using K9 (which uses Nickel). + +== Overview + +This directory contains self-validating K9 configuration files for Bunsenite. +K9 uses Nickel's contract system to ensure configurations are valid before use. + +== Configuration Files + +[cols="1,2,1"] +|=== +| File | Purpose | Security Level + +| `rust-fmt.k9.ncl` +| Rust formatter configuration with validation +| Yard (validation only) + +| `build.k9.ncl` +| Cargo build configuration with contracts +| Yard (validation only) +|=== + +== Usage + +=== Validate Configurations + +[source,bash] +---- +# Validate all K9 configs +just validate-k9 + +# Validate specific config +nickel eval config/rust-fmt.k9.ncl + +# Check pedigree +nickel eval -f 'pedigree' config/rust-fmt.k9.ncl +---- + +=== Generate Traditional Config Files + +[source,bash] +---- +# Generate rustfmt.toml from K9 config +nickel export config/rust-fmt.k9.ncl -f 'rustfmt_toml' > rustfmt.toml + +# Generate Cargo.toml sections from K9 config +nickel export config/build.k9.ncl -f 'config' > build-config.toml +---- + +=== Use in Build Pipeline + +[source,bash] +---- +# Validate before formatting +just validate-k9 && cargo fmt + +# Validate before build +just validate-k9 && cargo build --release +---- + +== Why K9 for Bunsenite? + +**This is meta-dogfooding at its finest:** + +1. **Bunsenite** is Nickel tooling +2. **K9** uses Nickel for validation +3. **Result**: The Nickel tool validates itself with K9 + +**Benefits:** + +* Invalid configs refuse to load (fail fast) +* Nickel contracts enforce validity at compile-time +* Self-documenting with type signatures +* Progressive strictness (lax → checked → attested) + +== Contract Examples + +=== Line Width Validation + +[source,nickel] +---- +max_width + | std.number.Positive + | std.contract.from_predicate (fun w => w >= 80 && w <= 120) +---- + +**Guarantees:** +- Line width must be positive +- Line width between 80-120 characters + +=== Edition Validation + +[source,nickel] +---- +edition + | std.contract.from_predicate + (fun e => std.array.elem e ["2015", "2018", "2021", "2024"]) +---- + +**Guarantees:** +- Only valid Rust editions accepted + +=== Version Format Validation + +[source,nickel] +---- +package.version + | std.contract.from_predicate + (fun v => std.string.is_match "^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-z0-9]+)?(\\+[a-z0-9]+)?$" v) +---- + +**Guarantees:** +- Semantic versioning format (x.y.z) +- Optional pre-release and build metadata + +== Integration with K9 Ecosystem + +**Related Projects:** + +* **K9-SVC**: https://github.com/hyperpolymath/standards/tree/main/k9-svc +* **RSR Template**: Uses K9 contractiles +* **MCP Servers**: Production configs with K9 +* **ABI/FFI**: Build configs in K9 + +== Roadmap + +=== Phase 1: Formatter & Build (Current) +- [x] rust-fmt.k9.ncl +- [x] build.k9.ncl +- [ ] Add to CI pipeline + +=== Phase 2: Extended Configs +- [ ] Test configuration (test.k9.ncl) +- [ ] Bench configuration (bench.k9.ncl) +- [ ] Documentation generation (doc.k9.ncl) + +=== Phase 3: CI Integration +- [ ] Pre-commit hook validates K9 configs +- [ ] CI fails if K9 validation fails +- [ ] Auto-generate rustfmt.toml from K9 + +=== Phase 4: Full Adoption +- [ ] All Bunsenite configs in K9 +- [ ] Document patterns for Nickel users +- [ ] Template for other Nickel projects + +== Philosophy + +**"If your config can't validate itself, it shouldn't run."** + +K9 brings the same rigor to configuration that Nickel brings to data. +By using K9 in Bunsenite, we demonstrate that self-validating configs +work for real-world tooling, not just toy examples. + +== Contributing + +To add new K9 configs: + +1. Create `config/name.k9.ncl` +2. Start with `K9!` magic header +3. Set `leash = 'Yard` for validation-only +4. Define `pedigree` with metadata +5. Define `config` with Nickel contracts +6. Test with `nickel eval` +7. Add validation to `justfile` + +== References + +* **K9 Specification**: https://github.com/hyperpolymath/standards/blob/main/self-validating/SPEC.adoc +* **Nickel Documentation**: https://nickel-lang.org/ +* **K9 Dogfooding Strategy**: ../DOGFOODING-OPPORTUNITIES.md (in k9-svc repo) + +--- + +**Maintainer**: Jonathan D.A. Jewell + +**Status**: Phase 1 Implementation + +**Last Updated**: 2026-01-30 diff --git a/vendor/bunsenite/config/build.k9.ncl b/vendor/bunsenite/config/build.k9.ncl new file mode 100644 index 0000000..2c396ce --- /dev/null +++ b/vendor/bunsenite/config/build.k9.ncl @@ -0,0 +1,105 @@ +# SPDX-License-Identifier: MPL-2.0 +K9! +leash = 'Yard # Validation only, no I/O + +pedigree = { + schema_version = "1.0.0", + component_type = "rust-build-config", + tool = "bunsenite", + validation_level = "strict", + description = "Self-validating Cargo build configuration for Bunsenite" +} + +# Cargo build configuration with Nickel contracts +config | { + package | { + name | String, + version | String, + edition | String, + rust_version | String, + license | String, + .. + }, + + profile | { + release | { + opt_level | [| '0, '1, '2, '3, 's, 'z |], + lto | [| 'Off, 'Thin, 'Fat |] | Bool, + codegen_units | Number, + strip | Bool, + .. + }, + dev | { + opt_level | [| '0, '1, '2, '3 |], + .. + }, + .. + }, + + features | { + default | Array String, + full | Array String, + .. + }, + .. +} = { + package = { + name = "bunsenite", + version = "0.1.0", + edition = "2021", + rust_version = "1.70", + license = "MPL-2.0", + authors = ["Jonathan D.A. Jewell "], + description = "Nickel language tooling and utilities", + repository = "https://github.com/hyperpolymath/bunsenite", + }, + + profile = { + release = { + opt_level = '3, + lto = 'Thin, + codegen_units = 1, + strip = true, + }, + dev = { + opt_level = '0, + }, + }, + + features = { + default = ["cli"], + full = ["cli", "wasm", "ffi"], + }, + + # Nickel contracts enforce validity + package.edition + | std.contract.from_predicate + (fun e => std.array.elem e ["2015", "2018", "2021", "2024"]), + + package.version + | std.contract.from_predicate + (fun v => std.string.is_match "^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-z0-9]+)?(\\+[a-z0-9]+)?$" v), + + package.license + | std.contract.from_predicate + (fun l => std.array.elem l [ + "MPL-2.0", + "MPL-2.0", + "MIT", + "Apache-2.0" + ]), + + profile.release.codegen_units + | std.number.Positive + | std.contract.from_predicate (fun u => u >= 1 && u <= 256), +} + +# Build commands based on profile +build_commands = { + dev = "cargo build", + release = "cargo build --release", + check = "cargo check --all-targets --all-features", + test = "cargo test --all-features", + bench = "cargo bench --all-features", + doc = "cargo doc --no-deps --all-features", +} diff --git a/vendor/bunsenite/config/rust-fmt.k9.ncl b/vendor/bunsenite/config/rust-fmt.k9.ncl new file mode 100644 index 0000000..e0035dc --- /dev/null +++ b/vendor/bunsenite/config/rust-fmt.k9.ncl @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: MPL-2.0 +K9! +leash = 'Yard # Validation only, no I/O + +pedigree = { + schema_version = "1.0.0", + component_type = "rust-formatter-config", + tool = "bunsenite", + validation_level = "strict", + description = "Self-validating Rust formatter configuration for Bunsenite" +} + +# Rust formatter configuration with Nickel contracts +config | { + edition | String, + max_width | Number, + hard_tabs | Bool, + tab_spaces | Number, + newline_style | [| 'Unix, 'Windows, 'Native |], + use_small_heuristics | [| 'Default, 'Off, 'Max |], + indent_style | [| 'Block, 'Visual |], + wrap_comments | Bool, + format_code_in_doc_comments | Bool, + comment_width | Number, + normalize_comments | Bool, + format_strings | Bool, + format_macro_matchers | Bool, + format_macro_bodies | Bool, + use_try_shorthand | Bool, + use_field_init_shorthand | Bool, + .. +} = { + # Edition + edition = "2021", + + # Line width + max_width = 100, + comment_width = 80, + + # Indentation + hard_tabs = false, + tab_spaces = 4, + indent_style = 'Block, + + # Line endings + newline_style = 'Unix, + + # Heuristics + use_small_heuristics = 'Default, + + # Comments + wrap_comments = true, + format_code_in_doc_comments = true, + normalize_comments = true, + + # Strings and macros + format_strings = true, + format_macro_matchers = true, + format_macro_bodies = true, + + # Syntax shortcuts + use_try_shorthand = true, + use_field_init_shorthand = true, + + # Nickel contracts enforce validity + edition + | std.contract.from_predicate + (fun e => std.array.elem e ["2015", "2018", "2021", "2024"]), + + max_width + | std.number.Positive + | std.contract.from_predicate (fun w => w >= 80 && w <= 120), + + tab_spaces + | std.number.Positive + | std.contract.from_predicate (fun s => s >= 2 && s <= 8), + + comment_width + | std.number.Positive + | std.contract.from_predicate (fun w => w >= 60 && w <= max_width), +} + +# Export for rustfmt.toml generation +rustfmt_toml = { + edition = config.edition, + max_width = config.max_width, + hard_tabs = config.hard_tabs, + tab_spaces = config.tab_spaces, + newline_style = std.string.lowercase (std.to_string config.newline_style), + use_small_heuristics = std.string.lowercase (std.to_string config.use_small_heuristics), + indent_style = std.string.lowercase (std.to_string config.indent_style), + wrap_comments = config.wrap_comments, + format_code_in_doc_comments = config.format_code_in_doc_comments, + comment_width = config.comment_width, + normalize_comments = config.normalize_comments, + format_strings = config.format_strings, + format_macro_matchers = config.format_macro_matchers, + format_macro_bodies = config.format_macro_bodies, + use_try_shorthand = config.use_try_shorthand, + use_field_init_shorthand = config.use_field_init_shorthand, +} diff --git a/vendor/bunsenite/contractile.just b/vendor/bunsenite/contractile.just new file mode 100644 index 0000000..9a5827b --- /dev/null +++ b/vendor/bunsenite/contractile.just @@ -0,0 +1,75 @@ +# Auto-generated by: contractile gen-just +# Source directory: contractiles +# Re-generate with: contractile gen-just --dir contractiles +# +# SPDX-License-Identifier: MPL-2.0 + +# === DUST (Recovery & Rollback) === +# Source: Dustfile.a2ml + +# List available dust recovery actions +dust-status: + @echo ' dust-source-rollback: Revert all source changes to last commit [rollback]' + +# Revert all source changes to last commit +dust-source-rollback: + @echo 'Executing rollback for source-rollback' + git checkout HEAD -- . + + +# === INTEND (Declared Future Intent) === +# Source: Intentfile.a2ml + +# Display declared future intents +intend-list: + @echo '=== Declared Intent ===' + @echo '' + @echo 'Features:' + @echo '' + @echo 'Quality:' + + +# === MUST (Physical State Checks) === +# Source: Mustfile.a2ml + +# Run all must checks +must-check: must-license-present must-readme-present must-spdx-headers must-no-banned-files + @echo 'All must checks passed' + +# LICENSE file must exist +must-license-present: + test -f LICENSE + +# README must exist +must-readme-present: + test -f README.adoc || test -f README.md + +# Source files should have SPDX license headers +must-spdx-headers: + find . -name '*.rs' -o -name '*.res' -o -name '*.gleam' | head -20 | xargs -r grep -L 'SPDX-License-Identifier' | wc -l | grep -q '^0$' + +# No Dockerfiles or Makefiles +must-no-banned-files: + test ! -f Dockerfile && test ! -f Makefile + + +# === TRUST (Integrity & Provenance Verification) === +# Source: Trustfile.a2ml + +# Run all trust verifications +trust-verify: trust-license-content trust-no-secrets-committed trust-container-images-pinned + @echo 'All trust verifications passed' + +# LICENSE contains expected SPDX identifier +trust-license-content: + grep -q 'SPDX\|License\|MIT\|Apache\|PMPL\|MPL' LICENSE + +# No .env or credential files in repo +trust-no-secrets-committed: + test ! -f .env && test ! -f credentials.json && test ! -f .env.local + +# Containerfile base images use pinned digests +trust-container-images-pinned: + test ! -f Containerfile || grep -q '@sha256:' Containerfile + + diff --git a/vendor/bunsenite/contractiles/README.adoc b/vendor/bunsenite/contractiles/README.adoc new file mode 100644 index 0000000..9f94acc --- /dev/null +++ b/vendor/bunsenite/contractiles/README.adoc @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: CC-BY-SA-4.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/vendor/bunsenite/contractiles/dust/Dustfile b/vendor/bunsenite/contractiles/dust/Dustfile new file mode 100644 index 0000000..314903c --- /dev/null +++ b/vendor/bunsenite/contractiles/dust/Dustfile @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: MPL-2.0 +# 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/vendor/bunsenite/contractiles/intend/Intentfile.a2ml b/vendor/bunsenite/contractiles/intend/Intentfile.a2ml new file mode 100644 index 0000000..993bb0f --- /dev/null +++ b/vendor/bunsenite/contractiles/intend/Intentfile.a2ml @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: MPL-2.0 +# Intentfile (A2ML Canonical) +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) + +@abstract: +Declared intent and purpose for Bunsenite. +@end + +## Purpose + +Bunsenite — > Nickel configuration file parser with multi-language FFI bindings + +## Anti-Purpose + +This project is NOT: +- A fork or wrapper around another tool +- A monorepo (unless explicitly structured as one) + +## If In Doubt + +If you are unsure whether a change is in scope, ask. +Sensitive areas: ABI definitions, license headers, CI workflows. diff --git a/vendor/bunsenite/contractiles/must/Mustfile b/vendor/bunsenite/contractiles/must/Mustfile new file mode 100644 index 0000000..dc7b3be --- /dev/null +++ b/vendor/bunsenite/contractiles/must/Mustfile @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: MPL-2.0 +# 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/vendor/bunsenite/contractiles/self-validating/README.adoc b/vendor/bunsenite/contractiles/self-validating/README.adoc new file mode 100644 index 0000000..cbc21d7 --- /dev/null +++ b/vendor/bunsenite/contractiles/self-validating/README.adoc @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += K9 Contractiles +:toc: left +:icons: font + +== What Are K9 Contractiles? + +K9 contractiles are self-validating components that combine configuration, validation, and deployment logic in a single file format. They implement the RSR principle of "self-describing artifacts" by embedding contracts and orchestration directly in the component. + +== The Three Security Levels + +K9 components declare their trust requirements using "The Leash" security model: + +[horizontal] +`'Kennel`:: Pure data, no execution (safest) +`'Yard`:: Nickel evaluation with contracts (medium trust) +`'Hunt`:: Full execution with Just recipes (requires signature) + +== Example Components + +This directory contains example K9 contractiles for common repository tasks: + +=== Kennel Level (Pure Data) + +**File:** `examples/project-metadata.k9.ncl` + +Pure configuration data with no execution. Safe to include in any repository. + +**Use cases:** +- Project metadata (name, version, description) +- Build configuration +- Tool settings +- Data schemas + +**Security:** No signature required, data-only. + +=== Yard Level (Validated Config) + +**File:** `examples/ci-config.k9.ncl` + +Configuration with Nickel contracts for runtime validation. Evaluated safely without I/O. + +**Use cases:** +- CI/CD configuration with validation +- Deployment parameters +- Database schemas with constraints +- API specifications + +**Security:** Signature recommended, Nickel evaluation only. + +=== Hunt Level (Full Execution) + +**File:** `examples/setup-repo.k9.ncl` + +Full execution with Just recipes. Can run shell commands and modify filesystem. + +**Use cases:** +- Repository setup scripts +- Deployment automation +- System configuration +- Package installation + +**Security:** **Signature required**, full system access. + +== Usage in Your Repository + +=== 1. Create K9 Components + +Choose the appropriate security level for your use case: + +[source,bash] +---- +# Kennel: Pure configuration +cp contractiles/self-validating/examples/project-metadata.k9.ncl config/metadata.k9.ncl + +# Yard: Validated configuration +cp contractiles/self-validating/examples/ci-config.k9.ncl .github/ci.k9.ncl + +# Hunt: Full automation +cp contractiles/self-validating/examples/setup-repo.k9.ncl scripts/setup.k9.ncl +---- + +=== 2. Validate Components + +[source,bash] +---- +# Validate Nickel syntax and contracts +nickel typecheck config/metadata.k9.ncl + +# Verify Hunt-level signature (if signed) +./must verify scripts/setup.k9.ncl +---- + +=== 3. Execute Components + +[source,bash] +---- +# Kennel: Export as JSON +nickel export config/metadata.k9.ncl > metadata.json + +# Yard: Evaluate with validation +nickel eval .github/ci.k9.ncl + +# Hunt: Run with Just (dry-run first!) +./must --dry-run run scripts/setup.k9.ncl +./must run scripts/setup.k9.ncl +---- + +== Integration with RSR + +K9 contractiles integrate with other RSR standards: + +**STATE.scm**:: K9 components can generate or validate STATE.scm +**ECOSYSTEM.scm**:: K9 can automate cross-repo operations +**META.scm**:: K9 can enforce architectural decisions + +== Security Best Practices + +=== For Kennel/Yard Components + +✅ **Safe to use without signatures** + +✅ **Review Nickel code before use** + +✅ **Validate contracts match expectations** + +=== For Hunt Components + +⚠️ **ALWAYS verify signatures** + +⚠️ **Review Just recipes carefully** + +⚠️ **Run dry-run mode first** + +⚠️ **Never run as root unless required** + +⚠️ **Sandbox external components** + +**See:** https://github.com/hyperpolymath/standards/blob/main/self-validating/docs/SECURITY-BEST-PRACTICES.adoc + +== Template Files + +Use these as starting points for your own K9 components: + +- `template-kennel.k9.ncl` - Pure data template +- `template-yard.k9.ncl` - Validated config template +- `template-hunt.k9.ncl` - Full execution template + +== Dependencies + +To use K9 contractiles in your repository: + +[source,bash] +---- +# Install Nickel (configuration language) +curl -L https://github.com/tweag/nickel/releases/latest/download/nickel-linux-x86_64 -o nickel +chmod +x nickel && sudo mv nickel /usr/local/bin/ + +# Install Just (task runner, for Hunt level) +cargo install just + +# Clone K9-SVC (for must shim and tooling) +git clone https://github.com/hyperpolymath/standards.git +# Note: K9-SVC is located in standards/k9-svc +---- + +== Learn More + +- **K9-SVC Specification:** https://github.com/hyperpolymath/standards/blob/main/self-validating/SPEC.adoc +- **K9 User Guide:** https://github.com/hyperpolymath/standards/blob/main/self-validating/GUIDE.adoc +- **Security Documentation:** https://github.com/hyperpolymath/standards/blob/main/self-validating/docs/SECURITY-FAQ.adoc +- **IANA Media Type:** `application/vnd.k9+nickel` + +== Contributing + +When adding K9 contractiles to your repository: + +1. Use appropriate security level (Kennel > Yard > Hunt) +2. Document what each component does +3. Include validation contracts in Yard/Hunt components +4. Sign Hunt-level components before committing +5. Add K9 validation to CI/CD pipeline + +**Questions?** Open an issue on https://github.com/hyperpolymath/standards/tree/main/k9-svc diff --git a/vendor/bunsenite/contractiles/self-validating/examples/ci-config.k9.ncl b/vendor/bunsenite/contractiles/self-validating/examples/ci-config.k9.ncl new file mode 100644 index 0000000..9fe314e --- /dev/null +++ b/vendor/bunsenite/contractiles/self-validating/examples/ci-config.k9.ncl @@ -0,0 +1,126 @@ +K9! +# 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 + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "ci-configuration", + security = { + leash = 'Yard, + trust_level = "validated-config", + allow_network = false, + allow_filesystem_write = false, + allow_subprocess = false, + }, + metadata = { + name = "ci-config", + version = "1.0.0", + description = "CI/CD configuration with runtime validation", + author = "Jonathan D.A. Jewell ", + }, + }, + + # CI/CD configuration with Nickel contracts + ci = { + # Platform must be a known CI provider + platform + | [| 'GitHubActions, 'GitLabCI, 'CircleCI, 'TravisCI |] + = 'GitHubActions, + + # Build matrix with validation + matrix = { + # Operating systems to test on + os + | Array String + | std.array.NonEmpty + = ["ubuntu-latest", "macos-latest"], + + # Language versions to test + versions + | Array String + | std.array.NonEmpty + = ["stable", "beta"], + }, + + # Workflow steps with validation + steps = [ + { + name = "Checkout", + action = "actions/checkout@v4", + # Version must be SHA-pinned for security + sha | String | std.string.NonEmpty = "b4ffde65f46336ab88eb53be808477a3936bae11", + }, + { + name = "Build", + run = "just build", + }, + { + name = "Test", + run = "just test", + }, + { + name = "Lint", + run = "just lint", + }, + ], + + # Deployment configuration + deploy = { + enabled | Bool = false, + + # Only deploy from main branch + branch + | String + | std.contract.from_predicate (fun b => b == "main" || b == "master") + = "main", + + # Deployment requires manual approval + requires_approval | Bool = true, + }, + + # Security scanning + security = { + enabled | Bool = true, + + scanners = [ + { + name = "CodeQL", + languages = ["rust", "javascript"], + }, + { + name = "OSSF Scorecard", + enabled = true, + }, + { + name = "TruffleHog", + scan_for = "secrets", + }, + ], + }, + + # Notification settings + notifications = { + on_success = "never", + on_failure = "always", + channels = ["email"], + }, + }, + + # Validation rules (enforced by Nickel) + validation = { + # At least one OS must be specified + check_os = std.array.length ci.matrix.os > 0, + + # At least one version must be tested + check_versions = std.array.length ci.matrix.versions > 0, + + # Must have at least build and test steps + check_steps = std.array.length ci.steps >= 2, + + # Security scanning must be enabled + check_security = ci.security.enabled == true, + }, +} diff --git a/vendor/bunsenite/contractiles/self-validating/examples/project-metadata.k9.ncl b/vendor/bunsenite/contractiles/self-validating/examples/project-metadata.k9.ncl new file mode 100644 index 0000000..b2299b4 --- /dev/null +++ b/vendor/bunsenite/contractiles/self-validating/examples/project-metadata.k9.ncl @@ -0,0 +1,57 @@ +K9! +# SPDX-License-Identifier: MPL-2.0 +# Example Kennel-level K9 component: Project metadata +# Security Level: Kennel (pure data, no execution) +# No signature required + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "project-metadata", + security = { + leash = 'Kennel, + trust_level = "data-only", + allow_network = false, + allow_filesystem_write = false, + allow_subprocess = false, + }, + metadata = { + name = "project-metadata", + version = "1.0.0", + description = "Pure data configuration for project metadata", + author = "Jonathan D.A. Jewell ", + }, + }, + + # Project configuration + project = { + name = "my-project", + version = "0.1.0", + description = "A project following Rhodium Standard Repositories", + + repository = { + url = "https://github.com/hyperpolymath/my-project", + type = "git", + }, + + author = { + name = "Jonathan D.A. Jewell", + email = "j.d.a.jewell@open.ac.uk", + organization = "The Open University", + }, + + license = "MPL-2.0", + + keywords = [ + "rhodium-standard", + "rsr", + "hyperpolymath", + ], + }, + + # Export as JSON for other tools + export = { + format = "json", + destination = "project-metadata.json", + }, +} diff --git a/vendor/bunsenite/contractiles/self-validating/examples/setup-repo.k9.ncl b/vendor/bunsenite/contractiles/self-validating/examples/setup-repo.k9.ncl new file mode 100644 index 0000000..358f18d --- /dev/null +++ b/vendor/bunsenite/contractiles/self-validating/examples/setup-repo.k9.ncl @@ -0,0 +1,167 @@ +K9! +# 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 + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "repository-setup", + security = { + leash = 'Hunt, + trust_level = "full-system-access", + allow_network = true, + allow_filesystem_write = true, + allow_subprocess = true, + signature_required = true, + }, + metadata = { + name = "setup-repo", + version = "1.0.0", + description = "Automated repository setup with RSR standards", + author = "Jonathan D.A. Jewell ", + }, + warnings = [ + "This component has full system access", + "Only run from trusted sources with verified signatures", + "Review Just recipes before execution", + "Use dry-run mode first: ./must --dry-run run setup-repo.k9.ncl", + ], + }, + + # Configuration with contracts + config = { + repo_name + | String + | std.string.NonEmpty + = "my-new-repo", + + repo_type + | [| 'Library, 'Application, 'Tool, 'Specification |] + = 'Application, + + primary_language + | String + | std.string.NonEmpty + = "rust", + + # RSR compliance features to enable + features = { + checkpoint_files | Bool = true, # STATE.scm, ECOSYSTEM.scm, META.scm + security_workflows | Bool = true, # CodeQL, Scorecard, etc. + quality_checks | Bool = true, # Linting, formatting + mirroring | Bool = false, # GitLab/Bitbucket mirrors + }, + + # Git configuration + git = { + default_branch = "main", + initial_commit | Bool = true, + remote_url | String = "", + }, + }, + + # Just recipes for execution + # These run when: ./must run setup-repo.k9.ncl + recipes = { + # Main entry point + default = { + recipe = "setup", + description = "Set up RSR-compliant repository", + }, + + # Individual setup tasks + setup = { + dependencies = ["check-env", "create-structure", "init-git", "setup-workflows"], + commands = [ + "echo '✅ Repository setup complete!'", + "echo 'Run: git status to see changes'", + ], + }, + + "check-env" = { + description = "Verify required tools are installed", + commands = [ + "command -v git || (echo 'ERROR: git not found' && exit 1)", + "command -v just || (echo 'ERROR: just not found' && exit 1)", + "command -v nickel || (echo 'ERROR: nickel not found' && exit 1)", + "echo '✓ All required tools present'", + ], + }, + + "create-structure" = { + description = "Create RSR directory structure", + commands = [ + "mkdir -p src/ docs/ tests/ scripts/", + "mkdir -p .github/workflows/", + "mkdir -p contractiles/self-validating/", + "echo '✓ Directory structure created'", + ], + }, + + "init-git" = { + description = "Initialize Git repository", + commands = [ + "git init -b %{config.git.default_branch}", + "git config user.name 'Jonathan D.A. Jewell'", + "git config user.email 'j.d.a.jewell@open.ac.uk'", + "echo '✓ Git initialized'", + ], + }, + + "setup-workflows" = { + description = "Add RSR-compliant workflows", + commands = [ + # This would copy workflow templates + # In a real implementation, would fetch from rsr-template-repo + "echo '✓ Workflows configured'", + ], + }, + + "create-checkpoint-files" = { + description = "Create STATE.scm, ECOSYSTEM.scm, META.scm", + commands = [ + "echo '(state (version \"1.0.0\") (project \"%{config.repo_name}\"))' > STATE.scm", + "echo '(ecosystem (version \"1.0.0\") (name \"%{config.repo_name}\"))' > ECOSYSTEM.scm", + "echo '(meta (version \"1.0.0\") (project \"%{config.repo_name}\"))' > META.scm", + "echo '✓ Checkpoint files created'", + ], + }, + + "add-license" = { + description = "Add PMPL-1.0 license", + commands = [ + "curl -sL https://raw.githubusercontent.com/hyperpolymath/pmpl/main/LICENSE -o LICENSE", + "echo '✓ License added'", + ], + }, + + "add-readme" = { + description = "Create README.adoc from template", + commands = [ + "echo '= %{config.repo_name}' > README.adoc", + "echo '' >> README.adoc", + "echo 'Part of the Hyperpolymath ecosystem.' >> README.adoc", + "echo '✓ README created'", + ], + }, + + clean = { + description = "Remove generated files (careful!)", + commands = [ + "echo '⚠️ This will delete all generated files'", + "echo 'Press Ctrl+C to cancel, or wait 5 seconds...'", + "sleep 5", + "rm -f STATE.scm ECOSYSTEM.scm META.scm", + "echo '✓ Cleaned'", + ], + }, + }, + + # Validation (Yard-level checks before Hunt execution) + validation = { + check_repo_name = std.string.length config.repo_name > 0, + check_language = std.string.length config.primary_language > 0, + }, +} diff --git a/vendor/bunsenite/contractiles/self-validating/template-hunt.k9.ncl b/vendor/bunsenite/contractiles/self-validating/template-hunt.k9.ncl new file mode 100644 index 0000000..b3fcb47 --- /dev/null +++ b/vendor/bunsenite/contractiles/self-validating/template-hunt.k9.ncl @@ -0,0 +1,136 @@ +K9! +# 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 + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "TODO: describe component type (e.g., 'deployment', 'setup-script')", + security = { + leash = 'Hunt, + trust_level = "full-system-access", + allow_network = true, + allow_filesystem_write = true, + allow_subprocess = true, + signature_required = true, + }, + metadata = { + name = "TODO: component-name", + version = "1.0.0", + description = "TODO: Detailed description of what this component does", + author = "Jonathan D.A. Jewell ", + }, + warnings = [ + "This component has full system access", + "Only run from trusted sources with verified signatures", + "Review all Just recipes before execution", + "Use dry-run mode first: ./must --dry-run run your-file.k9.ncl", + ], + side_effects = [ + "TODO: List what files/directories this creates or modifies", + "TODO: List what commands this executes", + "TODO: List what network access this requires", + ], + }, + + # Configuration with contracts (Yard-level validation) + config = { + # Add your configuration here with appropriate contracts + target_dir + | String + | std.string.NonEmpty + = "/tmp/k9-output", + + dry_run | Bool = false, + + # Add more config as needed + }, + + # Just recipes for execution + # These run when: ./must run your-file.k9.ncl + recipes = { + # Main entry point (runs by default) + default = { + recipe = "TODO: main-task", + description = "TODO: What the default recipe does", + }, + + # Define your recipes here + "main-task" = { + dependencies = ["check-prerequisites"], + commands = [ + "echo 'TODO: Add your commands here'", + # Example: Create directory + # "mkdir -p %{config.target_dir}", + # Example: Run a command + # "just build", + # Example: Conditional execution + # "@if [ \"%{config.dry_run}\" = \"true\" ]; then echo '[DRY-RUN] Would execute'; else actual-command; fi", + ], + }, + + "check-prerequisites" = { + description = "Verify required tools and permissions", + commands = [ + # Example: Check for required tools + # "command -v git || (echo 'ERROR: git not found' && exit 1)", + # Example: Check permissions + # "[ -w %{config.target_dir} ] || (echo 'ERROR: Cannot write to target directory' && exit 1)", + "echo '✓ Prerequisites checked'", + ], + }, + + # Add more recipes as needed + "build" = { + description = "Build the project", + commands = [ + "echo 'TODO: Add build commands'", + ], + }, + + "deploy" = { + description = "Deploy the application", + dependencies = ["build"], + commands = [ + "echo 'TODO: Add deployment commands'", + ], + }, + + "clean" = { + description = "Clean up generated files", + commands = [ + "echo '⚠️ This will delete files - waiting 3 seconds...'", + "sleep 3", + "echo 'TODO: Add cleanup commands'", + # "rm -rf %{config.target_dir}", + ], + }, + }, + + # Validation (Yard-level checks before Hunt execution) + validation = { + check_target_dir = std.string.length config.target_dir > 0, + # Add more validation as needed + }, +} + +# Usage: +# 1. Fill in TODO items above +# 2. Define configuration with contracts +# 3. Implement Just recipes with your commands +# 4. Test with dry-run: ./must --dry-run run your-file.k9.ncl +# 5. Review dry-run output carefully +# 6. Sign the component: ./must sign your-file.k9.ncl +# 7. Distribute with signature: your-file.k9.ncl.sig +# 8. Users verify and run: ./must verify && ./must run your-file.k9.ncl +# +# Security checklist: +# ✓ All TODO items filled in +# ✓ side_effects documented accurately +# ✓ Commands reviewed for safety +# ✓ No hardcoded secrets or credentials +# ✓ Proper error handling in recipes +# ✓ Tested in dry-run mode +# ✓ Component signed with trusted key diff --git a/vendor/bunsenite/contractiles/self-validating/template-kennel.k9.ncl b/vendor/bunsenite/contractiles/self-validating/template-kennel.k9.ncl new file mode 100644 index 0000000..4228b26 --- /dev/null +++ b/vendor/bunsenite/contractiles/self-validating/template-kennel.k9.ncl @@ -0,0 +1,54 @@ +K9! +# 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 + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "TODO: describe component type (e.g., 'build-config', 'metadata')", + security = { + leash = 'Kennel, + trust_level = "data-only", + allow_network = false, + allow_filesystem_write = false, + allow_subprocess = false, + }, + metadata = { + name = "TODO: component-name", + version = "1.0.0", + description = "TODO: Brief description of what this component contains", + author = "Jonathan D.A. Jewell ", + }, + }, + + # Your configuration data here + config = { + # Example: Pure data values + setting_1 = "value", + setting_2 = 42, + setting_3 = true, + + nested = { + key = "value", + }, + + list = [ + "item1", + "item2", + ], + }, + + # Optional: Export format specification + export = { + format = "json", # or "yaml", "toml" + destination = "output.json", + }, +} + +# Usage: +# 1. Fill in TODO items above +# 2. Add your configuration data to config = { ... } +# 3. Validate: nickel typecheck your-file.k9.ncl +# 4. Export: nickel export your-file.k9.ncl > output.json diff --git a/vendor/bunsenite/contractiles/self-validating/template-yard.k9.ncl b/vendor/bunsenite/contractiles/self-validating/template-yard.k9.ncl new file mode 100644 index 0000000..a723f5a --- /dev/null +++ b/vendor/bunsenite/contractiles/self-validating/template-yard.k9.ncl @@ -0,0 +1,84 @@ +K9! +# 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 + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "TODO: describe component type (e.g., 'validated-config', 'schema')", + security = { + leash = 'Yard, + trust_level = "validated-config", + allow_network = false, + allow_filesystem_write = false, + allow_subprocess = false, + }, + metadata = { + name = "TODO: component-name", + version = "1.0.0", + description = "TODO: Brief description with validation details", + author = "Jonathan D.A. Jewell ", + }, + }, + + # Configuration with Nickel contracts for validation + config = { + # Example: String that cannot be empty + name + | String + | std.string.NonEmpty + = "TODO: default value", + + # Example: Number with range constraint + port + | Number + | std.contract.from_predicate (fun p => p > 0 && p < 65536) + = 8080, + + # Example: Boolean flag + enabled | Bool = true, + + # Example: Enum (one of several values) + environment + | [| 'Development, 'Staging, 'Production |] + = 'Development, + + # Example: List with non-empty constraint + items + | Array String + | std.array.NonEmpty + = ["item1", "item2"], + + # Example: Nested object with contracts + database = { + host | String | std.string.NonEmpty = "localhost", + port | Number | std.contract.from_predicate (fun p => p > 0 && p < 65536) = 5432, + name | String | std.string.NonEmpty = "mydb", + }, + }, + + # Validation rules (additional cross-field checks) + validation = { + # Example: Check that at least one item exists + check_items = std.array.length config.items > 0, + + # Example: Check that production has secure settings + check_production = + if config.environment == 'Production then + config.enabled == true + else + true, + + # Add your custom validation rules here + }, +} + +# Usage: +# 1. Fill in TODO items above +# 2. Define your config with appropriate contracts +# 3. Add validation rules in validation = { ... } +# 4. Validate: nickel typecheck your-file.k9.ncl +# 5. Evaluate: nickel eval your-file.k9.ncl +# 6. If validation passes, use in your application diff --git a/vendor/bunsenite/contractiles/trust/Trustfile.a2ml b/vendor/bunsenite/contractiles/trust/Trustfile.a2ml new file mode 100644 index 0000000..7b62388 --- /dev/null +++ b/vendor/bunsenite/contractiles/trust/Trustfile.a2ml @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: MPL-2.0 +# Trustfile (A2ML Canonical) +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) + +@abstract: +Trust and provenance verification for Bunsenite. +Maximal trust by default — LLM may read, build, test, lint, format. +@end + +@trust-level: maximal +@trust-boundary: repo +@trust-actions: [read, build, test, lint, format] +@trust-deny: [delete-branch, force-push, modify-ci-secrets, publish] + +## Integrity + +### license-content +- description: LICENSE contains expected SPDX identifier +- run: grep -q 'SPDX\|License\|MIT\|Apache\|PMPL\|MPL' LICENSE +- severity: critical + +### no-secrets-committed +- description: No .env or credential files in repo +- run: test ! -f .env && test ! -f credentials.json && test ! -f .env.local +- severity: critical diff --git a/vendor/bunsenite/docs/CITATIONS.adoc b/vendor/bunsenite/docs/CITATIONS.adoc new file mode 100644 index 0000000..2f9b947 --- /dev/null +++ b/vendor/bunsenite/docs/CITATIONS.adoc @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell += bunsenite - Citation Guide +:toc: + +== BibTeX + +[source,bibtex] +---- +@software{bunsenite_2026, + author = {Jewell, Jonathan D.A.}, + title = {bunsenite}, + year = {2026}, + url = {https://github.com/hyperpolymath/bunsenite}, + license = {MPL-2.0} +} +---- + +== Harvard Style + +Jewell, J.D.A. (2026) _bunsenite_ [Computer software]. Available at: https://github.com/hyperpolymath/bunsenite + +== OSCOLA + +Jonathan D.A. Jewell, 'bunsenite' (2026) + +== MLA + +Jewell, Jonathan D.A. "bunsenite." 2026, github.com/hyperpolymath/bunsenite. + +== APA 7 + +Jewell, J.D.A. (2026). _bunsenite_ [Computer software]. GitHub. https://github.com/hyperpolymath/bunsenite + +== See Also + +* link:../CITATION.cff[CITATION.cff] +* link:../codemeta.json[codemeta.json] diff --git a/vendor/bunsenite/docs/tech-debt-2026-05-26.adoc b/vendor/bunsenite/docs/tech-debt-2026-05-26.adoc new file mode 100644 index 0000000..61b0fac --- /dev/null +++ b/vendor/bunsenite/docs/tech-debt-2026-05-26.adoc @@ -0,0 +1,67 @@ +== Tech-Debt Audit — bunsenite — 2026-05-26 + +*Source:* estate-wide automated scan 2026-05-26. *Companion:* +https://github.com/hyperpolymath/standards/tree/main/docs/audits[`+hyperpolymath/standards+` +2026-05-26-estate-*-debt audits]. *Combined severity:* `+MEDIUM+`. + +This file records the _raw findings_ — it does not by itself fix the +debt. Each section ends with a '`Recommended next move`' line; closing +the debt is follow-up work. + +=== 1. Proof debt + +No proof-bearing files (`+*.v+`, `+*.lean+`, `+*.agda+`, `+*.idr+`, +`+*.idr2+`, `+*.fst+`, `+*.dfy+`, `+*.tla+`, `+*.ads+`, `+*.adb+`) found +in this repo. + +*Recommended next move:* none. + +=== 2. Licence debt + +[cols=",",options="header",] +|=== +|Field |Value +|LICENSE file |`+LICENSE+` +|SPDX header |`+MPL-2.0+` +|Manifest licence |`+MPL-2.0+` +|Body classifier |`+Palimp-MPL-2.0+` +|Severity |`+ok+` +|=== + +*Recommended next move:* none for licence. + +=== 3. Documentation debt + +[cols=",",options="header",] +|=== +|Field |Value +|README lines |331 +|`+docs/+` files |2 +|`+docs/+` LoC |154 +|CHANGELOG.md |Y +|CONTRIBUTING.md |Y +|CODE_OF_CONDUCT.md |Y +|SECURITY.md |Y +|Severity |`+MEDIUM+` +|=== + +*Recommended next move:* introduce a `+docs/+` directory. The README at +331 lines has likely grown to do the work of `+docs/+` — split it into a +thin README + `+docs/architecture.md+`, `+docs/usage.md+`, etc. +Heavy-wiki exemplars to copy from: `+affinescript+`, `+boj-server+`, +`+echidna+`, `+hypatia+`. + +=== Cross-references + +* Estate proof-debt audit: +`+hyperpolymath/standards/docs/audits/2026-05-26-estate-proof-debt.md+` +* Estate licence-debt audit: +`+hyperpolymath/standards/docs/audits/2026-05-26-estate-licence-debt.md+` +* Estate documentation-debt audit: +`+hyperpolymath/standards/docs/audits/2026-05-26-estate-documentation-debt.md+` + +''''' + +🤖 Generated by Claude Code estate-wide tech-debt scan (2026-05-26). +This file is informational — closing the debt is follow-up work owned by +the maintainer. diff --git a/vendor/bunsenite/docs/wiki-home.adoc b/vendor/bunsenite/docs/wiki-home.adoc new file mode 100644 index 0000000..ef7e966 --- /dev/null +++ b/vendor/bunsenite/docs/wiki-home.adoc @@ -0,0 +1,127 @@ +== Bunsenite + +*Nickel configuration file parser with multi-language FFI bindings* + +https://github.com/hyperpolymath/rsr[image:https://img.shields.io/badge/RSR-Bronze-cd7f32[RSR +Bronze]] +link:[image:https://img.shields.io/badge/TPCF-Perimeter%203-blue[TPCF +Perimeter 3]] +link:[image:https://img.shields.io/badge/license-PMPL--1.0%20%7C%20Palimpsest-green[License]] + +=== Quick Start + +[source,bash] +---- +# Install from crates.io +cargo install bunsenite + +# Parse a Nickel config +bunsenite parse config.ncl --pretty + +# Validate without evaluation +bunsenite validate config.ncl + +# Interactive REPL +bunsenite repl + +# Watch mode +bunsenite watch config.ncl +---- + +=== Features + +[cols=",",options="header",] +|=== +|Feature |Description +|*Parse* |Parse Nickel configs to JSON +|*Validate* |Validate without full evaluation +|*Watch* |Auto-reload on file changes +|*REPL* |Interactive Nickel evaluation +|*Schema* |JSON Schema validation +|*FFI* |Stable C ABI via Zig +|=== + +=== Architecture + +.... +┌─────────────────────────────────────────┐ +│ Consumers │ +├─────────────┬─────────────┬─────────────┤ +│ Deno │ AffineScript │ Browser │ +│ (Deno FFI) │ (C FFI) │ (WASM) │ +└──────┬──────┴──────┬──────┴──────┬──────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────┐ +│ Zig C ABI Layer │ +│ (Stable interface across Rust │ +│ compiler versions) │ +└─────────────────┬───────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ Rust Core │ +│ │ +│ nickel-lang-core 0.9.1 │ +│ miette error diagnostics │ +│ serde serialization │ +└─────────────────────────────────────────┘ +.... + +=== Bindings + +==== Deno (JavaScript/TypeScript) + +[source,typescript] +---- +import { parseNickel, validateNickel } from "./bunsenite.ts"; + +const config = parseNickel('{ port = 8080 }', "config.ncl"); +console.log(config.port); // 8080 +---- + +==== AffineScript + +[source,affinescript] +---- +let config = Bunsenite.parse("{ port = 8080 }", "config.ncl") +Js.log(config) +---- + +==== WebAssembly + +[source,javascript] +---- +import init, { parse } from './bunsenite.js'; + +await init(); +const config = parse('{ port = 8080 }', 'config.ncl'); +---- + +=== RSR Compliance + +Bunsenite follows the *Rhodium Standard Repository* (RSR) Bronze tier: + +* ✅ *Type Safety*: Compile-time (Rust) +* ✅ *Memory Safety*: Rust ownership model +* ✅ *Offline-First*: No network dependencies +* ✅ *No TypeScript*: Deno FFI uses `+.ts+` but calls `+Deno.dlopen+` +* ✅ *No npm/bun*: AffineScript `+package.json+` is for npm publishing +only +* ✅ *No Python*: Clean +* ✅ *Justfile*: All builds via Justfile + +=== Pages + +* [[Installation]] +* [[CLI Reference]] +* [[API Reference]] +* [[FFI Guide]] +* [[Examples]] +* [[Contributing]] + +=== Links + +* https://github.com/hyperpolymath/bunsenite[GitHub Repository] +* https://crates.io/crates/bunsenite[crates.io] +* https://docs.rs/bunsenite[Documentation] diff --git a/vendor/bunsenite/eclexiaiser.toml b/vendor/bunsenite/eclexiaiser.toml new file mode 100644 index 0000000..ff57fd9 --- /dev/null +++ b/vendor/bunsenite/eclexiaiser.toml @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: MPL-2.0 +# eclexiaiser manifest for bunsenite + +[project] +name = "bunsenite" + +[[functions]] +name = "main" +source = "src/lib.rs" +energy-budget-mj = 25.0 + +[carbon] +provider = "static" +region = "GB" +static-intensity = 200.0 + +[report] +format = "text" +include-recommendations = true diff --git a/vendor/bunsenite/examples/config.ncl b/vendor/bunsenite/examples/config.ncl new file mode 100644 index 0000000..55cd14e --- /dev/null +++ b/vendor/bunsenite/examples/config.ncl @@ -0,0 +1,51 @@ +# Example Bunsenite Configuration +# This demonstrates various Nickel features + +{ + # Application metadata + name = "example-app", + version = "1.0.0", + description = "An example application configuration", + + # Server configuration + server = { + host = "0.0.0.0", + port = 8080, + workers = 4, + timeout_seconds = 30, + }, + + # Database configuration + database = { + url = "postgres://localhost:5432/mydb", + max_connections = 20, + timeout_ms = 5000, + }, + + # Feature flags + features = { + enable_logging = true, + enable_metrics = true, + enable_tracing = false, + debug_mode = false, + }, + + # Computed values + full_name = name ++ " v" ++ version, + server_url = "http://" ++ server.host ++ ":" ++ std.string.from_number server.port, + + # List example + allowed_origins = [ + "http://localhost:3000", + "http://localhost:8080", + "https://example.com", + ], + + # Nested configuration + logging = { + level = "info", + format = "json", + outputs = ["stdout", "file"], + file_path = "/var/log/app.log", + }, +} diff --git a/vendor/bunsenite/examples/simple.ncl b/vendor/bunsenite/examples/simple.ncl new file mode 100644 index 0000000..d3eaf22 --- /dev/null +++ b/vendor/bunsenite/examples/simple.ncl @@ -0,0 +1,8 @@ +# Simple Bunsenite Example +# Minimal configuration demonstrating basic features + +{ + name = "simple-app", + version = "1.0.0", + port = 8080, +} diff --git a/vendor/bunsenite/examples/web-project-deno.json b/vendor/bunsenite/examples/web-project-deno.json new file mode 100644 index 0000000..ee775a4 --- /dev/null +++ b/vendor/bunsenite/examples/web-project-deno.json @@ -0,0 +1,20 @@ +{ + "// NOTE": "Example deno.json for AffineScript web projects", + "tasks": { + "build": "deno run -A npm:affinescript", + "clean": "deno run -A npm:affinescript clean", + "watch": "deno run -A npm:affinescript -w", + "serve": "deno run -A jsr:@std/http/file-server .", + "test": "deno test --allow-all" + }, + "imports": { + "affinescript": "^12.0.0", + "@affinescript/core": "npm:@affinescript/core@^1.6.0", + "safe-dom/": "https://raw.githubusercontent.com/hyperpolymath/affinescript-dom-mounter/main/src/", + "proven/": "../proven/bindings/affinescript/src/" + }, + "compilerOptions": { + "allowJs": true, + "checkJs": false + } +} diff --git a/vendor/bunsenite/ffi/zig/build.zig b/vendor/bunsenite/ffi/zig/build.zig new file mode 100644 index 0000000..c02617f --- /dev/null +++ b/vendor/bunsenite/ffi/zig/build.zig @@ -0,0 +1,95 @@ +// 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/vendor/bunsenite/ffi/zig/src/main.zig b/vendor/bunsenite/ffi/zig/src/main.zig new file mode 100644 index 0000000..059e6c3 --- /dev/null +++ b/vendor/bunsenite/ffi/zig/src/main.zig @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// BUNSENITE 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 = "BUNSENITE 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 bunsenite_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 bunsenite_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 bunsenite_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 bunsenite_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 bunsenite_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 bunsenite_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 bunsenite_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 bunsenite_version() [*:0]const u8 { + return VERSION.ptr; +} + +/// Get build information +export fn bunsenite_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 bunsenite_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 bunsenite_is_initialized(handle: ?*Handle) u32 { + const h = handle orelse return 0; + return if (h.initialized) 1 else 0; +} + +//============================================================================== +// Tests +//============================================================================== + +test "lifecycle" { + const handle = bunsenite_init() orelse return error.InitFailed; + defer bunsenite_free(handle); + + try std.testing.expect(bunsenite_is_initialized(handle) == 1); +} + +test "error handling" { + const result = bunsenite_process(null, 0); + try std.testing.expectEqual(Result.null_pointer, result); + + const err = bunsenite_last_error(); + try std.testing.expect(err != null); +} + +test "version" { + const ver = bunsenite_version(); + const ver_str = std.mem.span(ver); + try std.testing.expectEqualStrings(VERSION, ver_str); +} diff --git a/vendor/bunsenite/ffi/zig/test/integration_test.zig b/vendor/bunsenite/ffi/zig/test/integration_test.zig new file mode 100644 index 0000000..e481508 --- /dev/null +++ b/vendor/bunsenite/ffi/zig/test/integration_test.zig @@ -0,0 +1,183 @@ +// 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/vendor/bunsenite/fuzz/Cargo.toml b/vendor/bunsenite/fuzz/Cargo.toml new file mode 100644 index 0000000..f93d1d3 --- /dev/null +++ b/vendor/bunsenite/fuzz/Cargo.toml @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MPL-2.0 +[package] +name = "bunsenite-fuzz" +version = "0.0.0" +authors = ["Jonathan D.A. Jewell "] +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +arbitrary = { version = "1", features = ["derive"] } + +[dependencies.bunsenite] +path = ".." + +[[bin]] +name = "fuzz_parser" +path = "fuzz_targets/fuzz_parser.rs" +test = false +doc = false +bench = false diff --git a/vendor/bunsenite/fuzz/fuzz_targets/fuzz_parser.rs b/vendor/bunsenite/fuzz/fuzz_targets/fuzz_parser.rs new file mode 100644 index 0000000..3a8739f --- /dev/null +++ b/vendor/bunsenite/fuzz/fuzz_targets/fuzz_parser.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Fuzz target for bunsenite Nickel parser + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use bunsenite::NickelLoader; + +fuzz_target!(|data: &[u8]| { + // Convert bytes to string for parsing + if let Ok(input) = std::str::from_utf8(data) { + let loader = NickelLoader::new(); + + // Fuzz the main parsing function + // This exercises nickel-lang-core's parser with arbitrary input + let _ = loader.parse_string(input, "fuzz.ncl"); + + // Also fuzz validation (parsing without evaluation) + let _ = loader.validate(input, "fuzz.ncl"); + } +}); diff --git a/vendor/bunsenite/hooks/validate-codeql.sh b/vendor/bunsenite/hooks/validate-codeql.sh new file mode 100644 index 0000000..15b52c3 --- /dev/null +++ b/vendor/bunsenite/hooks/validate-codeql.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# 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/vendor/bunsenite/hooks/validate-permissions.sh b/vendor/bunsenite/hooks/validate-permissions.sh new file mode 100644 index 0000000..1999b01 --- /dev/null +++ b/vendor/bunsenite/hooks/validate-permissions.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# 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/vendor/bunsenite/hooks/validate-sha-pins.sh b/vendor/bunsenite/hooks/validate-sha-pins.sh new file mode 100644 index 0000000..697092b --- /dev/null +++ b/vendor/bunsenite/hooks/validate-sha-pins.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# 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/vendor/bunsenite/hooks/validate-spdx.sh b/vendor/bunsenite/hooks/validate-spdx.sh new file mode 100644 index 0000000..aa45bb2 --- /dev/null +++ b/vendor/bunsenite/hooks/validate-spdx.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Pre-commit hook: Validate SPDX headers in workflow files + +set -euo pipefail + +ERRORS=0 +SPDX_PATTERN="^# SPDX-License-Identifier:MPL-2.0 + +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: MPL-2.0 + ERRORS=$((ERRORS + 1)) + fi +done + +if [ $ERRORS -gt 0 ]; then + exit 1 +fi + +exit 0 diff --git a/vendor/bunsenite/llm-warmup-dev.adoc b/vendor/bunsenite/llm-warmup-dev.adoc new file mode 100644 index 0000000..b46c552 --- /dev/null +++ b/vendor/bunsenite/llm-warmup-dev.adoc @@ -0,0 +1,19 @@ +== LLM Warmup — bunsenite (Developer) + +=== What is bunsenite? + +See README.adoc for overview. + +=== Key Commands + +* `+just setup+` — set up development environment +* `+just build+` — build the project +* `+just test+` — run tests +* `+just doctor+` — diagnose issues +* `+just heal+` — attempt auto-repair + +=== Quick Context + +* License: MPL-2.0 +* Part of hyperpolymath ecosystem +* See EXPLAINME.adoc for architecture diff --git a/vendor/bunsenite/llm-warmup-user.adoc b/vendor/bunsenite/llm-warmup-user.adoc new file mode 100644 index 0000000..b152ccc --- /dev/null +++ b/vendor/bunsenite/llm-warmup-user.adoc @@ -0,0 +1,19 @@ +== LLM Warmup — bunsenite (User) + +=== What is bunsenite? + +See README.adoc for overview. + +=== Key Commands + +* `+just setup+` — set up development environment +* `+just build+` — build the project +* `+just test+` — run tests +* `+just doctor+` — diagnose issues +* `+just heal+` — attempt auto-repair + +=== Quick Context + +* License: MPL-2.0 +* Part of hyperpolymath ecosystem +* See EXPLAINME.adoc for architecture diff --git a/vendor/bunsenite/mise.toml b/vendor/bunsenite/mise.toml new file mode 100644 index 0000000..6dd983f --- /dev/null +++ b/vendor/bunsenite/mise.toml @@ -0,0 +1,57 @@ +[tools] +# Language runtimes +node = "latest" +python = "latest" +rust = "latest" +go = "latest" +zig = "latest" +java = "latest" +bun = "latest" +denojs = "latest" + +# Package managers +npm = "latest" +yarn = "latest" +pnpm = "latest" +pip = "latest" +cargo = "latest" +go-task = "latest" + +# Formatting & Linting +gofmt = "latest" +black = "latest" +isort = "latest" +ruff = "latest" +prettier = "latest" +shfmt = "latest" +stylua = "latest" + +# Build tools +cmake = "latest" +make = "latest" +ninja = "latest" + +# Shell tools +git = "latest" +gnu-sed = "latest" +gnu-tar = "latest" +gnu-grep = "latest" + +# Testing +vitest = "latest" +pytest = "latest" +jest = "latest" + +[env] +# Common environment variables +NODE_ENV = "development" +PYTHONDONTWRITEBYTECODE = "1" +PYTHONUNBUFFERED = "1" + +# Task runner alias +[alias] +task = "go-task" +build = "cargo build --release || npm run build || go build" +test = "cargo test || npm test || go test ./..." +lint = "ruff check . || prettier --check . || black --check ." +fmt = "ruff format . || prettier --write . || black ." diff --git a/vendor/bunsenite/packaging/arch/PKGBUILD b/vendor/bunsenite/packaging/arch/PKGBUILD new file mode 100644 index 0000000..df74a86 --- /dev/null +++ b/vendor/bunsenite/packaging/arch/PKGBUILD @@ -0,0 +1,54 @@ +# Maintainer: Campaign for Cooler Coding and Programming +# Arch Linux PKGBUILD for bunsenite + +pkgname=bunsenite +pkgver=1.0.2 +pkgrel=1 +pkgdesc="Nickel configuration file parser with multi-language FFI bindings" +arch=('x86_64' 'aarch64') +url="https://github.com/hyperpolymath/bunsenite" +license=('PMPL-1.0' 'custom:Palimpsest-0.8') +depends=('gcc-libs') +makedepends=('rust' 'cargo' 'zig') +optdepends=( + 'deno: For Deno TypeScript bindings' +) +provides=('bunsenite') +conflicts=('bunsenite-git') +source=("$pkgname-$pkgver.tar.gz::https://github.com/hyperpolymath/bunsenite/archive/refs/tags/v$pkgver.tar.gz") +sha256sums=('SKIP') + +build() { + cd "$pkgname-$pkgver" + + # Build Rust library and CLI with all features + cargo build --release --features full + + # Build Zig FFI layer + cd zig + zig build -Doptimize=ReleaseFast +} + +check() { + cd "$pkgname-$pkgver" + cargo test --release +} + +package() { + cd "$pkgname-$pkgver" + + # Install binary + install -Dm755 "target/release/bunsenite" "$pkgdir/usr/bin/bunsenite" + + # Install shared library + install -Dm755 "zig/zig-out/lib/libbunsenite.so" "$pkgdir/usr/lib/libbunsenite.so" + + # Install Rust library + install -Dm644 "target/release/libbunsenite.rlib" "$pkgdir/usr/lib/libbunsenite.rlib" + + # Install license + install -Dm644 LICENSE.txt "$pkgdir/usr/share/licenses/$pkgname/LICENSE.txt" + + # Install documentation + install -Dm644 README.adoc "$pkgdir/usr/share/doc/$pkgname/README.adoc" +} diff --git a/vendor/bunsenite/packaging/arch/PKGBUILD-bin b/vendor/bunsenite/packaging/arch/PKGBUILD-bin new file mode 100644 index 0000000..fad0f1f --- /dev/null +++ b/vendor/bunsenite/packaging/arch/PKGBUILD-bin @@ -0,0 +1,33 @@ +# Maintainer: hyperpolymath +# Contributor: Campaign for Cooler Coding and Programming +# Pre-built binary package for bunsenite + +pkgname=bunsenite-bin +pkgver=1.0.2 +pkgrel=1 +pkgdesc="Nickel configuration file parser with multi-language FFI bindings (pre-built binary)" +arch=('x86_64' 'aarch64') +url="https://github.com/hyperpolymath/bunsenite" +license=('PMPL-1.0' 'custom:Palimpsest-0.8') +depends=('gcc-libs') +provides=('bunsenite') +conflicts=('bunsenite') +source_x86_64=("${pkgname}-${pkgver}-x86_64.tar.gz::https://github.com/hyperpolymath/bunsenite/releases/download/v${pkgver}/bunsenite-v${pkgver}-x86_64-unknown-linux-gnu.tar.gz") +source_aarch64=("${pkgname}-${pkgver}-aarch64.tar.gz::https://github.com/hyperpolymath/bunsenite/releases/download/v${pkgver}/bunsenite-v${pkgver}-aarch64-unknown-linux-gnu.tar.gz") +sha256sums_x86_64=('cca819ddf5459163c49877e6284f2910f2fd7e6ad39c7f824152831a01d2c07e') +sha256sums_aarch64=('1393cd3ba4e476e18e806105ce1d621e080ebb4c06950ed50524efb32db22618') + +package() { + install -Dm755 "${srcdir}/bunsenite" "${pkgdir}/usr/bin/bunsenite" + + # Install shared library if present + if [[ -f "${srcdir}/libbunsenite.so" ]]; then + install -Dm755 "${srcdir}/libbunsenite.so" "${pkgdir}/usr/lib/libbunsenite.so" + fi + + # Install license + install -Dm644 "${srcdir}/LICENSE.txt" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + + # Install README + install -Dm644 "${srcdir}/README.adoc" "${pkgdir}/usr/share/doc/${pkgname}/README.adoc" +} diff --git a/vendor/bunsenite/packaging/aur-ready/.SRCINFO b/vendor/bunsenite/packaging/aur-ready/.SRCINFO new file mode 100644 index 0000000..ada9160 --- /dev/null +++ b/vendor/bunsenite/packaging/aur-ready/.SRCINFO @@ -0,0 +1,17 @@ +pkgbase = bunsenite-bin + pkgdesc = Nickel configuration file parser with multi-language FFI bindings + pkgver = 1.0.2 + pkgrel = 1 + url = https://github.com/hyperpolymath/bunsenite + arch = x86_64 + arch = aarch64 + license = MIT + license = custom:Palimpsest-0.8 + provides = bunsenite + conflicts = bunsenite + source_x86_64 = bunsenite-bin-1.0.2-x86_64.tar.gz::https://github.com/hyperpolymath/bunsenite/releases/download/v1.0.2/bunsenite-v1.0.2-x86_64-unknown-linux-gnu.tar.gz + sha256sums_x86_64 = SKIP + source_aarch64 = bunsenite-bin-1.0.2-aarch64.tar.gz::https://github.com/hyperpolymath/bunsenite/releases/download/v1.0.2/bunsenite-v1.0.2-aarch64-unknown-linux-gnu.tar.gz + sha256sums_aarch64 = SKIP + +pkgname = bunsenite-bin diff --git a/vendor/bunsenite/packaging/aur-ready/PKGBUILD b/vendor/bunsenite/packaging/aur-ready/PKGBUILD new file mode 100644 index 0000000..a6b1926 --- /dev/null +++ b/vendor/bunsenite/packaging/aur-ready/PKGBUILD @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: MPL-2.0 +# Maintainer: hyperpolymath +pkgname=bunsenite-bin +pkgver=1.0.2 +pkgrel=1 +pkgdesc='Nickel configuration file parser with multi-language FFI bindings' +arch=('x86_64' 'aarch64') +url='https://github.com/hyperpolymath/bunsenite' +license=('PMPL-1.0' 'custom:Palimpsest-0.8') +provides=('bunsenite') +conflicts=('bunsenite') + +source_x86_64=("${pkgname}-${pkgver}-x86_64.tar.gz::https://github.com/hyperpolymath/bunsenite/releases/download/v${pkgver}/bunsenite-v${pkgver}-x86_64-unknown-linux-gnu.tar.gz") +source_aarch64=("${pkgname}-${pkgver}-aarch64.tar.gz::https://github.com/hyperpolymath/bunsenite/releases/download/v${pkgver}/bunsenite-v${pkgver}-aarch64-unknown-linux-gnu.tar.gz") + +# These will be updated by the CI workflow +sha256sums_x86_64=('SKIP') +sha256sums_aarch64=('SKIP') + +package() { + install -Dm755 "${srcdir}/bunsenite" "${pkgdir}/usr/bin/bunsenite" +} diff --git a/vendor/bunsenite/packaging/chocolatey/bunsenite.nuspec b/vendor/bunsenite/packaging/chocolatey/bunsenite.nuspec new file mode 100644 index 0000000..6e183ae --- /dev/null +++ b/vendor/bunsenite/packaging/chocolatey/bunsenite.nuspec @@ -0,0 +1,33 @@ + + + + bunsenite + 1.0.0 + Bunsenite + Campaign for Cooler Coding and Programming + Campaign for Cooler Coding and Programming + https://github.com/hyperpolymath/bunsenite/-/blob/main/LICENSE-PMPL-1.0 + https://github.com/hyperpolymath/bunsenite + false + Nickel configuration file parser with multi-language FFI bindings. + +Bunsenite provides a Rust core library with a stable C ABI layer (via Zig) that enables bindings for Deno (JavaScript/TypeScript), Rescript, and WebAssembly. + +Features: +- Type Safety: Compile-time guarantees via Rust's type system +- Memory Safety: Rust ownership model, zero unsafe blocks +- Offline-First: Works completely air-gapped +- Multi-Language: FFI bindings for Deno, Rescript, and WASM + +RSR Compliance: Bronze Tier | TPCF Perimeter: 3 + Nickel configuration file parser with FFI bindings + https://github.com/hyperpolymath/bunsenite/-/releases + Copyright 2025 Campaign for Cooler Coding and Programming + nickel config parser ffi wasm rust + https://github.com/hyperpolymath/bunsenite + https://docs.rs/bunsenite + + + + + diff --git a/vendor/bunsenite/packaging/debian/control b/vendor/bunsenite/packaging/debian/control new file mode 100644 index 0000000..3c391a4 --- /dev/null +++ b/vendor/bunsenite/packaging/debian/control @@ -0,0 +1,44 @@ +Source: bunsenite +Section: devel +Priority: optional +Maintainer: Campaign for Cooler Coding and Programming +Build-Depends: debhelper-compat (= 13), cargo, rustc (>= 1.70), zig +Standards-Version: 4.6.2 +Homepage: https://github.com/hyperpolymath/bunsenite +Vcs-Git: https://github.com/hyperpolymath/bunsenite.git +Vcs-Browser: https://github.com/hyperpolymath/bunsenite +Rules-Requires-Root: no + +Package: bunsenite +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Suggests: deno +Description: Nickel configuration file parser with FFI bindings + Bunsenite is a Nickel configuration file parser with multi-language + FFI bindings. It provides a Rust core library with a stable C ABI + layer (via Zig) that enables bindings for Deno (JavaScript/TypeScript), + Rescript, and WebAssembly. + . + Features: + - Type Safety: Compile-time guarantees via Rust's type system + - Memory Safety: Rust ownership model, zero unsafe blocks + - Offline-First: Works completely air-gapped + - Multi-Language: FFI bindings for Deno, Rescript, and WASM + . + RSR Compliance: Bronze Tier | TPCF Perimeter: 3 + +Package: libbunsenite-dev +Architecture: any +Section: libdevel +Depends: libbunsenite1 (= ${binary:Version}), ${misc:Depends} +Description: Nickel configuration file parser - development files + This package contains the development files for bunsenite, + including headers and static libraries for FFI integration. + +Package: libbunsenite1 +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: Nickel configuration file parser - shared library + This package contains the shared library for bunsenite, + providing FFI access from Deno, Rescript, and other languages. diff --git a/vendor/bunsenite/packaging/debian/rules b/vendor/bunsenite/packaging/debian/rules new file mode 100644 index 0000000..55e36cc --- /dev/null +++ b/vendor/bunsenite/packaging/debian/rules @@ -0,0 +1,28 @@ +#!/usr/bin/make -f +# Debian rules file for bunsenite + +export DEB_BUILD_MAINT_OPTIONS = hardening=+all +export CARGO_HOME = $(CURDIR)/.cargo + +%: + dh $@ + +override_dh_auto_build: + cargo build --release --features full + cd zig && zig build -Doptimize=ReleaseFast + +override_dh_auto_test: + cargo test --release + +override_dh_auto_install: + # Install binary + install -D -m 755 target/release/bunsenite debian/bunsenite/usr/bin/bunsenite + # Install shared library + install -D -m 644 zig/zig-out/lib/libbunsenite.so debian/libbunsenite1/usr/lib/$(DEB_HOST_MULTIARCH)/libbunsenite.so.1.0.0 + ln -s libbunsenite.so.1.0.0 debian/libbunsenite1/usr/lib/$(DEB_HOST_MULTIARCH)/libbunsenite.so.1 + # Install development files + install -D -m 644 zig/zig-out/lib/libbunsenite.so debian/libbunsenite-dev/usr/lib/$(DEB_HOST_MULTIARCH)/libbunsenite.so + +override_dh_auto_clean: + cargo clean || true + rm -rf zig/zig-out zig/zig-cache || true diff --git a/vendor/bunsenite/packaging/flatpak/com.campaignforcoolercoding.bunsenite.yml b/vendor/bunsenite/packaging/flatpak/com.campaignforcoolercoding.bunsenite.yml new file mode 100644 index 0000000..3fd9ff8 --- /dev/null +++ b/vendor/bunsenite/packaging/flatpak/com.campaignforcoolercoding.bunsenite.yml @@ -0,0 +1,32 @@ +app-id: com.campaignforcoolercoding.bunsenite +runtime: org.freedesktop.Platform +runtime-version: '23.08' +sdk: org.freedesktop.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.rust-stable + - org.freedesktop.Sdk.Extension.zig + +command: bunsenite + +finish-args: + - --filesystem=home:ro + - --filesystem=xdg-config:ro + +build-options: + append-path: /usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/zig/bin + env: + CARGO_HOME: /run/build/bunsenite/cargo + RUSTUP_HOME: /usr/lib/sdk/rust-stable + +modules: + - name: bunsenite + buildsystem: simple + build-commands: + - cargo build --release --features full + - cd zig && zig build -Doptimize=ReleaseFast + - install -Dm755 target/release/bunsenite /app/bin/bunsenite + - install -Dm755 zig/zig-out/lib/libbunsenite.so /app/lib/libbunsenite.so + sources: + - type: git + url: https://github.com/hyperpolymath/bunsenite.git + tag: v1.0.0 diff --git a/vendor/bunsenite/packaging/homebrew/bunsenite.rb b/vendor/bunsenite/packaging/homebrew/bunsenite.rb new file mode 100644 index 0000000..496d414 --- /dev/null +++ b/vendor/bunsenite/packaging/homebrew/bunsenite.rb @@ -0,0 +1,48 @@ +# Homebrew formula for bunsenite +class Bunsenite < Formula + desc "Nickel configuration file parser with multi-language FFI bindings" + homepage "https://github.com/hyperpolymath/bunsenite" + url "https://github.com/hyperpolymath/bunsenite/archive/refs/tags/v1.0.0.tar.gz" + sha256 "TODO" + license any_of: ["PMPL-1.0", "Palimpsest-0.8"] + head "https://github.com/hyperpolymath/bunsenite.git", branch: "main" + + depends_on "rust" => :build + depends_on "zig" => :build + + def install + # Build Rust binary with all features + system "cargo", "build", "--release", "--features=full" + + # Build Zig FFI layer + cd "zig" do + system "zig", "build", "-Doptimize=ReleaseFast" + end + + # Install binary + bin.install "target/release/bunsenite" + + # Install shared library + lib.install "zig/zig-out/lib/libbunsenite.dylib" + + # Create symlink for Linux compatibility + lib.install_symlink "libbunsenite.dylib" => "libbunsenite.so" if OS.linux? + end + + test do + # Test version output + assert_match version.to_s, shell_output("#{bin}/bunsenite --version") + + # Test parsing a simple Nickel config + (testpath/"test.ncl").write <<~EOS + { + name = "test", + version = "1.0.0" + } + EOS + + output = shell_output("#{bin}/bunsenite parse #{testpath}/test.ncl") + assert_match "name", output + assert_match "test", output + end +end diff --git a/vendor/bunsenite/packaging/macports/Portfile b/vendor/bunsenite/packaging/macports/Portfile new file mode 100644 index 0000000..9d35511 --- /dev/null +++ b/vendor/bunsenite/packaging/macports/Portfile @@ -0,0 +1,55 @@ +# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:ft=tcl:et:sw=4:ts=4:sts=4 + +PortSystem 1.0 +PortGroup cargo 1.0 +PortGroup github 1.0 + +github.setup hyperpolymath bunsenite 1.0.0 v +revision 0 +categories devel +license PMPL-1.0 Palimpsest-0.8 +maintainers {github.com:hyperpolymath @maintainer} openmaintainer +description Nickel configuration file parser with FFI bindings +long_description Bunsenite is a Nickel configuration file parser with \ + multi-language FFI bindings. It provides a Rust core \ + library with a stable C ABI layer (via Zig) that enables \ + bindings for Deno, Rescript, and WebAssembly. + +homepage https://github.com/hyperpolymath/bunsenite + +checksums rmd160 SKIP \ + sha256 SKIP \ + size SKIP + +# Rust version requirement +compiler.cxx_standard 2017 + +depends_build-append \ + port:zig + +cargo.crates { + # Cargo.lock dependencies will be auto-generated +} + +build.args-append --features=full + +destroot { + xinstall -m 755 ${worksrcpath}/target/[cargo.rust_platform]/release/bunsenite \ + ${destroot}${prefix}/bin/bunsenite + + xinstall -d ${destroot}${prefix}/lib + xinstall -m 644 ${worksrcpath}/zig/zig-out/lib/libbunsenite.dylib \ + ${destroot}${prefix}/lib/libbunsenite.dylib + + xinstall -d ${destroot}${prefix}/share/doc/${name} + xinstall -m 644 ${worksrcpath}/README.adoc \ + ${destroot}${prefix}/share/doc/${name}/README.adoc +} + +notes " +Bunsenite has been installed with the following components: +- bunsenite CLI at ${prefix}/bin/bunsenite +- libbunsenite.dylib at ${prefix}/lib/libbunsenite.dylib + +RSR Compliance: Bronze Tier | TPCF Perimeter: 3 +" diff --git a/vendor/bunsenite/packaging/rpm/bunsenite.spec b/vendor/bunsenite/packaging/rpm/bunsenite.spec new file mode 100644 index 0000000..c71d540 --- /dev/null +++ b/vendor/bunsenite/packaging/rpm/bunsenite.spec @@ -0,0 +1,74 @@ +# RPM spec file for bunsenite +# Compatible with Fedora (dnf) and openSUSE (zypper) + +Name: bunsenite +Version: 1.0.0 +Release: 1%{?dist} +Summary: Nickel configuration file parser with multi-language FFI bindings + +License: PMPL-1.0 OR Palimpsest-0.8 +URL: https://github.com/hyperpolymath/bunsenite +Source0: %{name}-%{version}.tar.gz + +BuildRequires: rust >= 1.70 +BuildRequires: cargo +BuildRequires: zig +BuildRequires: gcc + +Requires: glibc + +%description +Bunsenite is a Nickel configuration file parser with multi-language +FFI bindings. It provides a Rust core library with a stable C ABI +layer (via Zig) that enables bindings for Deno (JavaScript/TypeScript), +Rescript, and WebAssembly. + +Features: +- Type Safety: Compile-time guarantees via Rust's type system +- Memory Safety: Rust ownership model, zero unsafe blocks +- Offline-First: Works completely air-gapped +- Multi-Language: FFI bindings for Deno, Rescript, and WASM + +RSR Compliance: Bronze Tier | TPCF Perimeter: 3 + +%package devel +Summary: Development files for bunsenite +Requires: %{name}%{?_isa} = %{version}-%{release} + +%description devel +Development files for bunsenite including headers and static libraries. + +%prep +%autosetup + +%build +cargo build --release --features full +cd zig && zig build -Doptimize=ReleaseFast + +%check +cargo test --release + +%install +# Binary +install -D -m 755 target/release/bunsenite %{buildroot}%{_bindir}/bunsenite + +# Shared library +install -D -m 755 zig/zig-out/lib/libbunsenite.so %{buildroot}%{_libdir}/libbunsenite.so.1.0.0 +ln -s libbunsenite.so.1.0.0 %{buildroot}%{_libdir}/libbunsenite.so.1 +ln -s libbunsenite.so.1 %{buildroot}%{_libdir}/libbunsenite.so + +# Documentation +install -D -m 644 README.md %{buildroot}%{_docdir}/%{name}/README.md + +%files +%license LICENSE-PMPL-1.0 LICENSE-PALIMPSEST +%doc README.md +%{_bindir}/bunsenite +%{_libdir}/libbunsenite.so.1* + +%files devel +%{_libdir}/libbunsenite.so + +%changelog +* Thu Jan 01 2025 Campaign for Cooler Coding - 1.0.0-1 +- Initial release diff --git a/vendor/bunsenite/packaging/scoop/bunsenite.json b/vendor/bunsenite/packaging/scoop/bunsenite.json new file mode 100644 index 0000000..c74194f --- /dev/null +++ b/vendor/bunsenite/packaging/scoop/bunsenite.json @@ -0,0 +1,23 @@ +{ + "version": "1.0.0", + "description": "Nickel configuration file parser with multi-language FFI bindings", + "homepage": "https://github.com/hyperpolymath/bunsenite", + "license": "PMPL-1.0", + "architecture": { + "64bit": { + "url": "https://github.com/hyperpolymath/bunsenite/releases/download/v1.0.0/bunsenite-1.0.0-x86_64-pc-windows-msvc.zip", + "hash": "TODO", + "bin": "bunsenite.exe" + } + }, + "checkver": { + "github": "https://github.com/hyperpolymath/bunsenite" + }, + "autoupdate": { + "architecture": { + "64bit": { + "url": "https://github.com/hyperpolymath/bunsenite/releases/download/v$version/bunsenite-$version-x86_64-pc-windows-msvc.zip" + } + } + } +} diff --git a/vendor/bunsenite/packaging/winget/bunsenite.yaml b/vendor/bunsenite/packaging/winget/bunsenite.yaml new file mode 100644 index 0000000..2b85ef6 --- /dev/null +++ b/vendor/bunsenite/packaging/winget/bunsenite.yaml @@ -0,0 +1,42 @@ +# winget manifest for bunsenite +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.4.0.schema.json + +PackageIdentifier: Hyperpolymath.Bunsenite +PackageVersion: 1.0.0 +PackageLocale: en-US +Publisher: hyperpolymath +PublisherUrl: https://github.com/hyperpolymath +PackageName: Bunsenite +PackageUrl: https://github.com/hyperpolymath/bunsenite +License: PMPL-1.0 OR Palimpsest-0.8 +LicenseUrl: https://github.com/hyperpolymath/bunsenite/blob/main/LICENSE.txt +ShortDescription: Nickel configuration file parser with multi-language FFI bindings +Description: | + Bunsenite is a Nickel configuration file parser with multi-language FFI bindings. + It provides a Rust core library with a stable C ABI layer (via Zig) that enables + bindings for Deno (JavaScript/TypeScript), Rescript, and WebAssembly. + + Features: + - Type Safety: Compile-time guarantees via Rust's type system + - Memory Safety: Rust ownership model, zero unsafe blocks + - Offline-First: Works completely air-gapped + - Multi-Language: FFI bindings for Deno, Rescript, and WASM + + RSR Compliance: Bronze Tier | TPCF Perimeter: 3 +Tags: + - nickel + - config + - parser + - ffi + - rust + - cli +Moniker: bunsenite +Commands: + - bunsenite +Installers: + - Architecture: x64 + InstallerType: zip + InstallerUrl: https://github.com/hyperpolymath/bunsenite/releases/download/v1.0.0/bunsenite-1.0.0-x86_64-pc-windows-msvc.zip + InstallerSha256: TODO +ManifestType: singleton +ManifestVersion: 1.4.0 diff --git a/vendor/bunsenite/papers/arxiv/bunsenite.aux b/vendor/bunsenite/papers/arxiv/bunsenite.aux new file mode 100644 index 0000000..796d98c --- /dev/null +++ b/vendor/bunsenite/papers/arxiv/bunsenite.aux @@ -0,0 +1,52 @@ +\relax +\providecommand\hyper@newdestlabel[2]{} +\providecommand\HyField@AuxAddToFields[1]{} +\providecommand\HyField@AuxAddToCoFields[2]{} +\citation{nickel} +\@writefile{toc}{\contentsline {section}{\numberline {1}Introduction}{1}{section.1}\protected@file@percent } +\citation{nickel} +\@writefile{toc}{\contentsline {section}{\numberline {2}Background}{2}{section.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {2.1}The Nickel Configuration Language}{2}{subsection.2.1}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {1}Example Nickel configuration}{3}{lstlisting.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {2.2}The FFI Challenge}{3}{subsection.2.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {2.3}The Zig Advantage}{3}{subsection.2.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3}Architecture}{3}{section.3}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Bunsenite three-layer architecture. Deno and ReScript access the Rust core through a Zig-provided stable C ABI. WebAssembly bindings connect directly via wasm-bindgen.}}{4}{figure.1}\protected@file@percent } +\newlabel{fig:architecture}{{1}{4}{Bunsenite three-layer architecture. Deno and ReScript access the Rust core through a Zig-provided stable C ABI. WebAssembly bindings connect directly via wasm-bindgen}{figure.1}{}} +\@writefile{toc}{\contentsline {subsection}{\numberline {3.1}Layer 1: Rust Core}{4}{subsection.3.1}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {2}Core NickelLoader implementation}{4}{lstlisting.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {3.2}Layer 2: Zig FFI}{5}{subsection.3.2}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {3}Zig FFI exports (C ABI)}{5}{lstlisting.3}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {3.3}Layer 3: Language Bindings}{5}{subsection.3.3}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\numberline {3.3.1}Deno Bindings}{5}{subsubsection.3.3.1}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {4}Deno FFI binding}{5}{lstlisting.4}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\numberline {3.3.2}ReScript Bindings}{6}{subsubsection.3.3.2}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {5}ReScript binding with Result type}{6}{lstlisting.5}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\numberline {3.3.3}WebAssembly Bindings}{6}{subsubsection.3.3.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4}Safety Guarantees}{6}{section.4}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {4.1}Memory Safety}{6}{subsection.4.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {4.2}Type Safety}{7}{subsection.4.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {4.3}Offline Operation}{7}{subsection.4.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5}Compliance and Standards}{7}{section.5}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {5.1}RSR Bronze Requirements}{7}{subsection.5.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {1}{\ignorespaces RSR Bronze compliance matrix}}{7}{table.1}\protected@file@percent } +\newlabel{tab:rsr}{{1}{7}{RSR Bronze compliance matrix}{table.1}{}} +\@writefile{toc}{\contentsline {subsection}{\numberline {5.2}Security Considerations}{7}{subsection.5.2}\protected@file@percent } +\citation{dhall} +\citation{cue} +\@writefile{toc}{\contentsline {section}{\numberline {6}Performance}{8}{section.6}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {7}Related Work}{8}{section.7}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {7.1}Configuration Languages}{8}{subsection.7.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {7.2}FFI Approaches}{8}{subsection.7.2}\protected@file@percent } +\bibstyle{plain} +\bibcite{nickel}{1} +\bibcite{dhall}{2} +\bibcite{cue}{3} +\bibcite{rust-abi}{4} +\@writefile{toc}{\contentsline {subsection}{\numberline {7.3}Rust FFI Libraries}{9}{subsection.7.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {8}Future Work}{9}{section.8}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {9}Conclusion}{9}{section.9}\protected@file@percent } +\bibcite{zig-ffi}{5} +\bibcite{wasm-bindgen}{6} +\bibcite{deno-ffi}{7} +\gdef \@abspage@last{10} diff --git a/vendor/bunsenite/papers/arxiv/bunsenite.out b/vendor/bunsenite/papers/arxiv/bunsenite.out new file mode 100644 index 0000000..0693df1 --- /dev/null +++ b/vendor/bunsenite/papers/arxiv/bunsenite.out @@ -0,0 +1,26 @@ +\BOOKMARK [1][-]{section.1}{\376\377\000I\000n\000t\000r\000o\000d\000u\000c\000t\000i\000o\000n}{}% 1 +\BOOKMARK [1][-]{section.2}{\376\377\000B\000a\000c\000k\000g\000r\000o\000u\000n\000d}{}% 2 +\BOOKMARK [2][-]{subsection.2.1}{\376\377\000T\000h\000e\000\040\000N\000i\000c\000k\000e\000l\000\040\000C\000o\000n\000f\000i\000g\000u\000r\000a\000t\000i\000o\000n\000\040\000L\000a\000n\000g\000u\000a\000g\000e}{section.2}% 3 +\BOOKMARK [2][-]{subsection.2.2}{\376\377\000T\000h\000e\000\040\000F\000F\000I\000\040\000C\000h\000a\000l\000l\000e\000n\000g\000e}{section.2}% 4 +\BOOKMARK [2][-]{subsection.2.3}{\376\377\000T\000h\000e\000\040\000Z\000i\000g\000\040\000A\000d\000v\000a\000n\000t\000a\000g\000e}{section.2}% 5 +\BOOKMARK [1][-]{section.3}{\376\377\000A\000r\000c\000h\000i\000t\000e\000c\000t\000u\000r\000e}{}% 6 +\BOOKMARK [2][-]{subsection.3.1}{\376\377\000L\000a\000y\000e\000r\000\040\0001\000:\000\040\000R\000u\000s\000t\000\040\000C\000o\000r\000e}{section.3}% 7 +\BOOKMARK [2][-]{subsection.3.2}{\376\377\000L\000a\000y\000e\000r\000\040\0002\000:\000\040\000Z\000i\000g\000\040\000F\000F\000I}{section.3}% 8 +\BOOKMARK [2][-]{subsection.3.3}{\376\377\000L\000a\000y\000e\000r\000\040\0003\000:\000\040\000L\000a\000n\000g\000u\000a\000g\000e\000\040\000B\000i\000n\000d\000i\000n\000g\000s}{section.3}% 9 +\BOOKMARK [3][-]{subsubsection.3.3.1}{\376\377\000D\000e\000n\000o\000\040\000B\000i\000n\000d\000i\000n\000g\000s}{subsection.3.3}% 10 +\BOOKMARK [3][-]{subsubsection.3.3.2}{\376\377\000R\000e\000S\000c\000r\000i\000p\000t\000\040\000B\000i\000n\000d\000i\000n\000g\000s}{subsection.3.3}% 11 +\BOOKMARK [3][-]{subsubsection.3.3.3}{\376\377\000W\000e\000b\000A\000s\000s\000e\000m\000b\000l\000y\000\040\000B\000i\000n\000d\000i\000n\000g\000s}{subsection.3.3}% 12 +\BOOKMARK [1][-]{section.4}{\376\377\000S\000a\000f\000e\000t\000y\000\040\000G\000u\000a\000r\000a\000n\000t\000e\000e\000s}{}% 13 +\BOOKMARK [2][-]{subsection.4.1}{\376\377\000M\000e\000m\000o\000r\000y\000\040\000S\000a\000f\000e\000t\000y}{section.4}% 14 +\BOOKMARK [2][-]{subsection.4.2}{\376\377\000T\000y\000p\000e\000\040\000S\000a\000f\000e\000t\000y}{section.4}% 15 +\BOOKMARK [2][-]{subsection.4.3}{\376\377\000O\000f\000f\000l\000i\000n\000e\000\040\000O\000p\000e\000r\000a\000t\000i\000o\000n}{section.4}% 16 +\BOOKMARK [1][-]{section.5}{\376\377\000C\000o\000m\000p\000l\000i\000a\000n\000c\000e\000\040\000a\000n\000d\000\040\000S\000t\000a\000n\000d\000a\000r\000d\000s}{}% 17 +\BOOKMARK [2][-]{subsection.5.1}{\376\377\000R\000S\000R\000\040\000B\000r\000o\000n\000z\000e\000\040\000R\000e\000q\000u\000i\000r\000e\000m\000e\000n\000t\000s}{section.5}% 18 +\BOOKMARK [2][-]{subsection.5.2}{\376\377\000S\000e\000c\000u\000r\000i\000t\000y\000\040\000C\000o\000n\000s\000i\000d\000e\000r\000a\000t\000i\000o\000n\000s}{section.5}% 19 +\BOOKMARK [1][-]{section.6}{\376\377\000P\000e\000r\000f\000o\000r\000m\000a\000n\000c\000e}{}% 20 +\BOOKMARK [1][-]{section.7}{\376\377\000R\000e\000l\000a\000t\000e\000d\000\040\000W\000o\000r\000k}{}% 21 +\BOOKMARK [2][-]{subsection.7.1}{\376\377\000C\000o\000n\000f\000i\000g\000u\000r\000a\000t\000i\000o\000n\000\040\000L\000a\000n\000g\000u\000a\000g\000e\000s}{section.7}% 22 +\BOOKMARK [2][-]{subsection.7.2}{\376\377\000F\000F\000I\000\040\000A\000p\000p\000r\000o\000a\000c\000h\000e\000s}{section.7}% 23 +\BOOKMARK [2][-]{subsection.7.3}{\376\377\000R\000u\000s\000t\000\040\000F\000F\000I\000\040\000L\000i\000b\000r\000a\000r\000i\000e\000s}{section.7}% 24 +\BOOKMARK [1][-]{section.8}{\376\377\000F\000u\000t\000u\000r\000e\000\040\000W\000o\000r\000k}{}% 25 +\BOOKMARK [1][-]{section.9}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n}{}% 26 diff --git a/vendor/bunsenite/papers/arxiv/bunsenite.pdf b/vendor/bunsenite/papers/arxiv/bunsenite.pdf new file mode 100644 index 0000000000000000000000000000000000000000..258e0161666e7dc3258af0244c234449d2fc292b GIT binary patch literal 300857 zcma&NV{mR=w=Eib#ZFdid&RbG+qP}ncCupIwr$(S&HH}4Zk?*{+`4=In)Ougb3CK9 zHhOOpgIHEjn3|sE4;1mt!r%%N8y+2=jlMY)CnprGq>;6WqbVK(JrgV5zXuepu$iT! zkv$%*u%(`(k)V-*jiC_~H#d}nqrH)y6_o2*km|biK?{P{v&spzdCy-2;_&!8L|2xy zo{Y5B!AP_SLl<0A=1pqV6m2a_R-GJ$%J>cEbqE+$`M637RGBOaXY7jr)|X?U51(vY z8yarUITBg7#8OG{KQ4aaipVBLx{}y@HPGJBJX4z|ZSM9yo0d+NeHD`|Q3*HW85%4POVcLLj=I3?~p zaL(oL(Tg1mUyNl9{i-e=^1#dEynI5*#0PK6gK`+>01gF;#Q952A{y6XxTp!huA_QZ z^@9aA8$E;sD{bh zgIwpgy=po>&-W(2sbcCF(3C(c)|O`gT#nXD&(`5wY5PhDJ*oLqW2IaA3xH2%JsZHoqC zthHPzooQ^k68>uXC=GTmr(Caz4@bqeoNuwFZ8M+Ue&X(G{|ip(Nrj5$gj%Kw+BQNU zj&?Wmt3ljocV%0u@ZK&Vk(c~bkz}J4{LNX)M>F$*KDu9oU5#fLXZNPo&!%BGF34pj zw~xm}ZGp2ksrQp@8Mr`kmPU~bU*9IK)9!9a(`!r(~O(QyD5-06iJ2H|qXfzmw;`obwhZs8a=PX=P$DaTky9gCPK5AJ3c zFA?oDP5(8{d&P9Vgshiec7#fBkJ0@giIPuEw=_sIrZe*=5}?gaB7#gbXTjcExG(*8 zW6@Ka6~#V##8Y1vC-aT!|MR``~-Ud8%0Q-!;KVnXfq zs>`2GhWKG|1pivR@0xL}abU0^$_VH*mvkO^a>jm_Y;~Vo#0#~?g#wAwv9;zoam*R; zS!nzstGZ&eG7~NlT-{F)Ovc5w^8WV2Rl%*MJ*74dHc7(%xEu*QD^CYds4b}O1mQrd z6?}NeO*SngKWRKyd+^8{G=i>s*s3JR z`nT6PF(<)+5R^z^IuUSgRnJSr$ke_=UrcPTnAutcU|6~Es0VQwL38LCApM99s$S6H zjt;p8;>K^s;cJ$xk{gT6)#x&zT{pr7@?3*wN^9jXnWC z7qJ~-45PbmNmTe04M28b+#LUy%CP6m))V%Iv2UqM#+Ljg&p((N`l#FMdx#b{r0cy} zGRyo=<*_H~prnXhn{J(r+{+Hq--Y;uhq;959Ub<*I3X8@{R+G&eIK_?H=IQ>)u-&| z@H}_@RaOs{{SQ(=6VlbvkV~A9X};CYw8Jv#eAbjuabckFs`OdG7hWrb41`~xNOad3 z_Cxne0#T+7=f@zCZ%YWhvZnnFyTP=4+gSz768PgqdP*68>Zx*TjvCEFv`UnNyIrd zCi=TfOJo~ zv!+Iv{(Rcw%FP}YzPl|H5gycY?s$ejCL-gX-Z#afeRcS2ZCqwO4S0XsSs)5DBtau8 z>v#ItV5$*)1@kD-ITH&z=&J=IQKo5DGoo`%L{6A=*paaK<>+pt&W=3d)9Q_;(3L+x zR%@L&3Jwnb-g&Oc1AVX*c#8{u^&(@9<+X0#2WEnxqGqU%pEA|@TdT6so8~vSe%EQ- z{&Mvd+pJm`P|yJhw5?tLW2fu!U7?dZl*?bF4PQ&x8ZQs1uXnM((N|#R~VjT566is$_ zDpgBkOGzS@ar(5L#0+Na<4Ki4$xxIzqR$PU#=0*DeUkBnFgq@6+XAX$;lIckF9S6R8>g#SJn+wM9Iz;L+y;#Ac6Ft&5=WgOqk@eV#|jUj%*65?V^ zL6EmP%i8Q3B}{zy3Zc<)(WP*I9gi84G6ZsrZ7EHbaIs_G$Nhu$4g5<>m=wy$+VFqu z#?RS5R)vA#|KeHbm{|U|p5^b4XW3wZ_nKBY`K#Vj5sDTjukGIw<-teedzo*V#XW=A ziJG6J7;b;=?PbE6xTjTJ4>^aHkuPe_>Tr~qCRHGnD9pnx(f>U^ZZbW6$DnT}UrZc9 zV?&T!h`m*W5Rn=iHdZfM#GpqIL(@E~^r6tbNAcw`7_qXkJF}9osBSF8srr3ZRhD55 ztl&SWwCyDtv>~b7UVLpEKOE3xpxV1H4)@k(hP9j;_jEG$v@dGY{E}wYXl!?$juHXR z!&JdT>jU`ipCm~PN}JyynNTTfNW4;4>)!sjjle(f_lhN9Yeyt7_GT-xxPN|A6ne;b zp4rs0h@&I8z--T1SgjgL+}J-7Wq3I%oJAsKAa*>rL#mhj|^uHtmNy}0i+hWiCRaVZ4&f-Kp?5K=-d z(S&o?KO~fQ;TQHaH0p9fFCS)mHVHdt-2fq=(j{gX<>d!*MH zd#&h@%Y;+;K2?|124^xm5u!_}sp!F=1R+UrK+Z3jHWRJv5H7UGW=DZjP9U-By*fKX z7#rAJGUW#8AVSzN!I!zGMzF=y0jUzxo~Wm6Jp!{63{e7R^9!yV6B4L>d~%~etvEK1 z)u}ck%_2g3{$Y2AQY`{+A$OffAA1#rz@DZGwyU&b7_7kwmrkbSRTEpLOGnJnJZacO z7pclpyKCnS=a#w(k#Po)(Z9J8F=^7-vwKB3K%)X3u28e|rW08AxF*2xTmCIB1vw?? z|5n0w<_4kj0gEjy4*G{|yv3^OZwU<%~JF zh~;V2m3jJ7G7A$dKZjZZm1P@kzgjve0p4vh$_=MF`Ml7$c(odx9Tb}hJL^qgc3iWN zScldGk%U78uYn?hE+gyhM$ZbXbMx9P>}7rP)52aY9ipqmLfS0VQUsSu$&!wlW$VqCclUG#>7*YQaf~|Pd7S98=wL`Yb)t&l~L7W|bB?8)>63^xG6k0g^ z(_JIH{e;ZgO)Jq9$i4XCYtr_YMeqTZdNLrXCF2Ku|9$dATe=Tm z%6hsbgBRT7Ns54lMrNmitUNSynV?%m?(CP}RA3+3wKjH}d724`Reg(Yyi9G)@2l{`RT| zuTqems*bzAK&N5q$5i>)Y|%*nTsB|{%=M6^dwSaMu)(W6F`rkOq?aw)_9wk79LYo9 z&bXHYs#}!5>ZBR2! zJ5@$75QcS9KCu@fu#3tdGETmR4=Q7|{fTu0o0Uz>$&Z{Ssmt|zdq}Yh^#XGcee7@w z1ICy=Sb|r9_lWx^7r{5zh(ltGYQNqC|GcjCz*z6N=-ONH=nyWZhe!KM_sUKVEWw5! z!I8|YM)1I6VKT1?kwW&_Aa2rk#(!a2>83z@Mnzlu{UZdoXD*PT7nI0a(xlQv&J!rz z`G)U1>C?c=GJbf;80ag4Z8XTzYyX!M86d=h?rF9_|7lp;j%w975Qjk~x^Ml5AWp(5 z^JP#xs`VPnC>}R#d7qMQ>2_SIcBhvE|BM&5tnE8fpk=K!XVsi|X#s^-1I-bY#KA^r z=3jA)5NFu@6g@P*f$)IlA#2$TIJ7}>CaHxfgo)Q zI3pezeBxu_{&cfKb|9|SUxrML-w9t=Zx`lSe4k&0j+F^E7`-<$)m)cRGg(Hr?|y9? zr0Y1FPM!0;+o67HESTFs_Xd@;g5|T^r=9MbRjwG9hjiyRQ0vpM(={ z!Y(NzKDUL78;N7ZhV8atElldsaQAz@7n<1;((ic6?=V08ExPsTd3zib=mIR`E+0$) zQ)Ch#EBf2*{t=XWCJrI*sbQw}v3#Pv{pDTkcf<7)M|j_-_trOpE41qqWy74<1=mc5 z#YI*db;owzvO%$Wp3)05N+lj^eqXri;C5l(00F~MX#9*YLF6Mk2Y*KB$k=?R-P^$s z`)$4;w3!u0t2mL}o|t7KOxgx%LR-oAt6t0325WZzFn|)N`UC{4tr*Apv8{MS%xK3} z{xP;SZk|N0)rSb!b-C{4c}4_oRgq#gLRe#*iv##yBz&Yy{Tms>j*Ot^t&|j>jgPfe zP$~bF2q8p3?$<1vo7s#EgOivfS%x&&Pma%mS)aCYy_4&9L&OJ}!$;U! z6WezPt$W%@<3vyPId3o#^P zqv^r7g~u^srVMez9ELG2ueM47Z2AIv{NvcOw6=odWH%})^{fwn9RHrK0pRCDxb_Cp z(XT-yoKc;Y>q3(8U3o5_pA3>PGpvx;Dd&>W zLZ4?S19zs8QUWRRNGcWLcloJn{X9?lpOOm9p^PpGlHEx~?duwEu;!Q<=F~sJVWNyy z6A=xydELRxWW^{Hx42~=G8EHg9eMU*hw)<=Nv~!C@gJiZHC3ysh_?ig?X782p{|~w zvpTz|!Y@Q~f&wUjzf9zTDZl+&E%tQ$ibETO4GV<)1>7W-gR96#TQjdab)pR$qJzL9 zrG`$jzGfr2wHE1wpS2hu3rgTiYp;r zi5xIdFCJoydr!-QYjdyT#Z>8%mN^cxdoKwce;ND11_qH10D06HV20a{nnWky|ASTE z4^}X{KUn$S{9u)L_aChKj0h_2$}~RiFm(55@m6QAZ1YaX*1CxyQ!LtyQx$~1eAe!H zMf)!9hY5M?qUbf9v(`HZE(g_nf%cy9XJ1`{w-mB+E#piIln(f zYq^ysO%2Q~pB1_BBd}4gAAucQ{RqtdM_|#h{}I^e?}5I)tdXpbZX9ijDEdJHHW|xO zPrEa65}Q>fD~$D*q}fISu~xL%@cASeJ3(_qZvSkG;)_#UECsMqOk@REO$H&!w25;o zY}SxBrPnO0L#6)6>`WJYSh_z1LO%ku&9P;$aVWLvcv!mHxvrMc%ykI{3@batp&K-R7 zKfEy2;3ZsvJC$|qW>nDj@5XGKrjsF*!{M780+R3|bd8~mAii$L8AgS7@%zMs2 zamiM~&>eS5|%eKA!)`y41N~uR1AlF1ODs3CpE@LcDE%VO(twTqORbmiiEKvybCs zR00KooxE=@x(w?!npDF^)rc*q*fmohn<`Bjw}b)qR~d(Vq4=?b29HHDIA2{@kZ#M0Wu@}kKNUghL!Q^6aN~B;9pSA-h z7G>Q|d$YQPY#z zwT2X~*a1qsuhdF*Hynw;f8H65+D4e&-JzYtpY+3j#dwYe zsig4U4VH{UpsVY*ip7N76E-Yk#?WXz;738R@FS1xv&0q<9b)wbovI;Y;}N{U3--x_ z$azN)hkS>INW1JoBjXLUk-)p$6W9QEpCie(1jEA=KIvROXS_Tfw~a16&Kjvu0>9>B z0hy{PpSo`rk+tmED8Qz{eB$OD^zg+*#uA(T8ZXJy;W7jK%+1d1c;I^XHumr7o~md| zn~Z31hF(B<7VozPMyRv;^H81cAJO6uL(euE-i&o+nM8})fhT(>lvvI}Bg3XIsk?&`vagm5~2@&bm7$Y#YnE4K{ zjoZ3FgWkc!muXs|GG$Vvq4u^qL*s}otlV5o_Cd=){oH~kK0fY6=0{lLJ--)a#v~?R zx@;AvZGDeMb)2bhw5Y}ILLf;>Rf3cXlN%Zm%gp(#`t!KzF1n9FJ%$+*`3YpEtl7f! zO5fT(6@E+*8f%=zPp@rvEmto{%;sFm>3qdhodi8cUVY5S9F?=uUU~q=mbGFNafVq^ zi;)hD1Zc)2%)rW}x!k z-=hXyQ{RVMenrp$bmm8cZ>C#BNyx&jZ0DNYIlV4`jwuAdxR3-w3DZyg6m+`W(`%_A zY&T9_7n&wYIgT$=GNyYn!*HV3lG%Y?_VK1`a|4&heV;zCt_}{jI_eH(A^&Y% znOXkZx-v4;v;A+a>!Rj+3|2G3*XLI_HB_Mo@4$_e^;$#dCcEekn3JceZ(DF)!qb%a z1M7KQgxrJA^@MFJT-Fr(WjHU?$^uOac0BGeVTb^Zn{&qPMFw=&GbP?Z+DIW&r7c7K z7QI5KWnIflEMK_|gSdNd8b!zG6=S5g5dH@id4GdE2^M5;;@y;SG^5S$6udaK7T;$SA?1$YOqpqb;NzJL!Rt#b#V}l)-n!q z3JNx=rEOHdF7?bjy>s+iokk_arn;;BWnBrlN1Gr1xtzSJ+*WH_1px%O@j!SS385yP z?N^j;lP%Y}j_pz4z>~!h^rz~3g&{pk<9Ep2qaj!OI(LsQtsPo`T&bhhF($B|dMszqP=h82a`s=9 zl)2;Q4ErcvT^oqQ++X1sCl=!OHr6jODg2=y=gU4JXti)^RNYFRSamA{Z9&YYYjos+ zA+V1Ake9r{$C3dQVKREfT=#21T{mqdXRIgMfDiS#dD-p5ir{ntB&tSf3@iY5)vK3> zqU1^4uD8_OCm)CCav!05AoBDy4+3~PHp9#J42C{93_hP1>jI@im`xNb!i_+Ay}80D zv6P9;sw|PJ!5%XVSHAfhXOj3`Q*FRC{$N-{g|V2yJgZ15l)1gKEw%9|LbvEubOTVi z@QR@E@8R*G+t&5JYB+WNj*FB2suy;w)tn+peKYa*LR2Qte7pBMC)bsC0svMvf?t6eFB}al9Ly~!iRB5dLszfvhI>GY z40}+Q`)vGRtqFu)if}9sgwmM6RIrfh<9(>J|c)**YGZ= zWOjob5D7@J8K8(6=Y)joB0VxoOVzptWV(p9?nT4LEmrF@$B`p)N}S`WeG@h+ zmX0Kx)X5|3rFmlyP78X{m$}eR64-)lLemZ#yAix!1}?PEZ?$m7zDAeLiS*cCE=!j) zvP_=;CZ&g`&qL9JSkVLGiOQ6@JhT0ohP>uSggix5VY0s{mDDlztdm64h9PD&X z<>0-`pb>1b1*`f`t|^~hDBFwdm%W)JclPzr`l>m`UTzCySJZJmb==)}u2AkDH5lDI zuZ+vVkDFhxBDm>%@}{a4eeB}yFOu`%-m(yd{*PRhJ9cii4!7lYNt}6+sZAR2m2B#V z9c~efT+_B7vbx%v3S*$j(t}g=%d%&FCmeR{$TEOk&?zcCpr(#`N*8LWDqb_*szVPat07!#BR(A%N^VLVI7-p;LRqT& z<=^eG15@1KWR32H{GChLj6K}EsQqn0--^3ODCQE$5LF2?qISKdhc!^NvETk;3T~ML zTN9+ZYJizFxlpl(zt9Wmi2V8F4mhBy>8b3|-J!E>$TbyO3TzSgJ8fT|Z}jgm0;9QE z(aq?o$TJmQ;`>m744_K>GU2W9<$a&>)F6J2Z066;R>$$AdY43;iFDHtiJhcCJ*G35 z_Px1`E94+AqPpr4luvFxd2mAuq@X;uqzgugAS7Dfh;p+YycEQ^Q zo%Gn2;L_#*Zs}`Y=yCq*=N~lipqw=hWdl9|*121BKOf{1TrU5Ke0@lKe5XH6Wih-88LQl936F?zAxW@?#Z|NJIFyZ%vL?DWxGdQP;D^q@$_YJ z1b3qPCw*T9EuQE@Jji`A>>u2fZM@ZbNVD^6L{tcl@<;)^txer(2gpLUp*3nvq;`u3 z^IZO>T2V^v9LD!)sFw7^hkUG&y)bGn+fIPc8flL4x+0-5$wDi=5-L;64744_Lr^42@JO|r{)3}tpg!bci>GY_wWLWA3XNPeGvxsYLz(x z3sQUEyx7(idiM)}#}E|=5DZxG!-aHomu?9J&`Y8%(v)*BFrlBjaTDZ!V0^;0yayGM zk#8&*1e2bjief+}+~xQ(&K-XbP>rxa93x+m1#ZMk#Ii=pu(NU`Ae8?wE%y)8Wk1-l zCjBrC`5&em<}hU}LDgRi7-?z@N_)%v>*>?p{@j6Ae4pmOmHGTYb(RD5sBSb3@D8BX zxrhtb)p9msYUikI*OpE<#%jfSJws4oCA(p|mpL(Yf(a*myRJNu+9$M+*UbJ6ZdiDo zZSU&Gc9u#%NM)N|$0S7_BBG@0?IFDTjLdD57KQ}1En*-iF0Fu%9-;G#s0M<2$`Hi* z0FLD5XLy7bXfZ^f68=t-O$=RJymLn&tYFe&l&{awNR8nSh=9%m*t91i>$Mv0ee}M! zl;nVckG|t);oDOLPQU^9tU;aKCxF=1#PkdXdDl5VogUB9WvnFGJJ zWiV#C183ZKl>1Db4I+YMl+V+)xc>)=3R=cO+SK_)>t@;EG?K1civzgg)gN?cD}T^UFXHbk9{Tr^ ze2hb%l;h&I#S-lbP%K#d<_7*} zVIKty(v&uh1hGw%Q=0i>F{Oq7Bc7Lk#N+xS9*XQA@lgLqJW3m?9cV@JcI*9=*KC(d zsL9gh!}quj?%pE1>3wzF-+H5Bx)n_8w($p)AM^(kXfh5*2?F?;H1vurt^3exB2}5t z!Pvp+V-dHms9Wg*O&e~;bWzfCju%yfa3s&8@o#qR0V0Uzr2HMm`?e$R5oePYRyXPi z;mgC*wg9Ir%u>H*Cua~w(2+_%khnq;Dc!XRvMkpF2gShIK3ZC>=K%KQzOF_N%e16n=XO1%xQ_~Qd7%VewBT)|erN6w^7i_{QkH4$al zxfpKXvQNKVMmA_uG ziqG^7W2YXtz~OW`sCF0p!6l)r?u7lm?h8;(6qxVJAvkRT!)T6+F$d{>hq#I-hnDq! zY2RXXn@zQ}7WnBm+xfKT4vBYzV^{aAwda@#xwnJgdD*S~&i-IwoM02)7UKaF-QjBn zMK~IIIYm#5A5NNc^{z|SYzVcg{-wQD#&F+^v>CH%nT$njzFL)gG~)DG35?mj8!>9# z>d4tt+Uo%e7%^irS>cyLVRiI@R@JIp>9)9g?)G4$PQjgJU8k<4xF)GE3kzOc7iezj zNr+kQ*~t^{5FU0{-o$6xvRAu_IKAoyHT`ioHgw13)$tE)y9CcUqSL7FXX zeqI($S|3lAme5(+o@mgV@VylP94{eGd5ws-=C}sVw?h01_~F_WaK1Zt!BVi=Y|`-h z{W!QnR;ZCdhJ}Sjz8y5%El{Nh0Xb8T)Ea zzmGcR3o`C&z#}ijg;$Ut^TP5i;`DxT=iN!UtfYnb;#j)V4D09i(tbI_=TllWo+YC5 z0^IIy*J?>|-}n7KzfAjCYkpZ(Hhwv|SMGTg&I;_9)Cf7<)VvyCqwVAIbR1FNokPEq zZVXeJBq*M2MhH<))vlh^3d~PQAVd;cL$aQ{I=QOT4(=$9aoO7M-*Ay~|MYvZYInsY z%>#(#9duf=b*^^B=m|`mOHoM*MD`T}0*)7s*HwTWlwU1Bi5-AoSKmxXlB-GmEi2FFiGbMpgN+4cL@X@9Vn!z3Je$n;^a0k6_FMioH_`r0hbMMDdVH!L`_ z-=fa<5W8}cikt~O)FIPh(&@{REtX#4Mp9CB47&O}H{ZWVC9iKX zK#!VJxAkzQ^unFERv|jYb+gAD-8c6|&Yd_FQ?)}9iDoS*mN=rMcs)RrYqX1eYq4#H z!hnsqmWp-N3$WKvzq?4sE0jwm@34mw!5&^v)2%XmAj|A@__FoCv=n9xLI8^B0;owd zJ9-nAe+KIEF7yg$lqG1E&L#QwDr9%#3sgK^XdRAGK6WjobAlxo%b!xLbO}%QeS)ae zb%K&^fY;8tYhkYi4UE16nnx*?j^qEvLNTj74XU#&L{1CZ?T{uN{6PtMSRhEuPE-t$ zyjZ_8VftT?B;pv!>e3z*MG8yHtc&w>MiIEo0q0n``gTo|(a zAv}*ZCXE@&*#+ai5!Tn5Q@z7Z9VE$%ud$&n1w-L|LKg^_1?>~0BRoD#siQ-n=J#Ub ziPR+r-Ai&HO=EhXmI!*#i#t#sndZsL0j3(IjCZ*{RH5N~q3UkH6ANo@3@e+J*+4H5 zRP#`)!u`<~skYO$2L`J5tc6j*)DIzTtL7mBYG1=j#OYJ-#aPQepr67jjM-OQmEWw5nkmsGd&{kfx^|}T=1m{P`-iSjCy=`M~FTsBRcfsA#9r& z@Mo-E?-_M{sQu4euBCJ5c3@knW1M-36jTwe@%~qX028x?n%;UC^L4!VUN`+j{6dss zmDH~d4wKX3-f&3ch@{*5)6s*L;h5z8A8y!wxFHz+;Rf+P+^}J~g7mqwp?>F3E3Zvw zlPVnJ@Th0)=k+bH6%vK9E7We0n**&8)(ll3tr?t#&kdwIH$!~lX3!{)O(&7g@BKta z{6sgf{zRkxM4y`c6Ybwvifopmgz;XTPUB4Y=_70|_cnTGsJ^i;Vqy2ANYsB6ssA&< zPsx83iCP3w+o%t-ni#0HYGp$z|5+;VZmJS*boz&YPhoOnnQmqC0j$fmt#(QWJ@6os zU&O__!L6~ppoj;DLh?;OJYJLs^CMFc76-K|N22Mey2TufB<&j+lT*V?M|!$UNl&U;6g!tX|3gqK%8QKD&Z6> z;Qc7_%%CJA=xh1f;^Nzq2pZ&xkXYH;2x(kSPWMCF9igiY?0$|_=RKh4l-6}X{LDIR zMqcrz?-wS$Y#dw2T$;5(WjV=0nYxb3`dxVy%XvwDp7S9%u6v{En*vQO#bG4d^)%f* zAFnF8-B6wNR?)u_>z zeX|-(%~1$J$I+aVI7-QD#H71Rci#-X)Iw-I`AilEz6LMEJAHs9{TSV~OyAw~-#O6#a6NxB3 z4kx2`BJ&WgGS~-!QvjGs+MgU{#@@~T0LOhCVx}XTT+GF~K9S>SZ+1!I@mU5lZ|-_* z^X*k=M+H<$8!{#xypY&crRH-XuWl`WS(~Vu3Qdx zZ{#?awS=ltGTQILLQe3kS)z9R%Kj`kNpmpZCAJT~X@AowWo%QDHcX-+w*@N>G`yDZ zVq4u4p3OGB7OX-*LmXOz$``G_F7?O!o(ACsjWWTJjo0T)h-d^8HzjhFjllIQ;^ z1HXRCK*>L4Aobr8&8Bw3s_sOlep{QV+RHjMmPXzb%$XxBZ9OVexc+zX|0@ea9DTV2 z3<)#XUY?AUr+e#2C6$(5=lSj{GbYxG+ zEDDt_=yxL*+m>w$S<^XnIQ6fK8YuV_$2hKkMj1tJP~AeG*PGTj*5zy7zfdKBpL@q(Z~jhV{j-*6>7 zm|bwfmuz&`O~+53O_)N@m>W__7cH4trenpQd30tujtqj*kdw~O{!~Dbp8*}2eg=&G z8F2OM->!8YYS9PnOeTBN%yN+KJ~Hs3Xv*;W>bAg%^`}II{FJB@x}Or|{4bTNA(9CA zFJ^w`e1xRCsBxRool>-ck89Uk8K#+npO&T!LYKD%D9k^JazBY=6hDbj|4EcnMAREF zt2IW7PEg$?rB^7+4Z*GO2Pnlu;mjopf{L%*Qolm}aT-C^x<)~G@uNrvZ5tf8-h*9g z2Q=2k8-luRT5wz+rU;QCP)NN$t&rd!E7kIkl|rEc8V^%E(`8a>3A5mVOPLsWewiiV z#-W%P_$l7>w?Eo3X8X50kyxY01#zPnFu9w=`02eP7WkB338B-;evb_Uc+C%es;&+I zTYZrA1;GK`IN6}ropv%8Sng%o>dpn@>V@VH`gIRZ3M3!$=z9B^MQwMu@QGrksQ;QG zM!t9{CTs>^Qd31#VBJoYy!h?eup~klB<9s4f>=bV-+^SpdAKXv%dl)4O$WXo*Jq{i z#@1sv)Hxi}boImO5!VkRUM2dS^<)3`b{0D|c%yVK5J?=X(T7_!Cbc6UErADbEdeIG zJ7#YMy{?F;=FBJUy+RL;M@Pnr&lKypk?Yy{7LjVMH6JgerZCd(A>roR7ALf@a}B+7 z9h$?XeCe=ZuPegm_v^%aVr45zm{Y4|)OxYxrqr8+)KTnF9G2I>&`Znk8YzWvioy?>hN(Lc@9^QW0+N&nkSk6Q87|BSCn;_6Hn zPpUtHp}Yg#+qmi0#HvYDGOucy^0#s|j6)l~ z(5g1n=>GRD7W4n{!cEc5)(DSQM&De)(F%%I8jqfi?w|c~_70ACf7sam-`#ZdOpFZw z+ZS?;s#-FdA~0Uv-F-2a;~TYH*)dQ;VIB;PEM)Jk{nGALV&%zUhYN60Yri$c6;(dv1abzgU@q&Y6crVNh zV#Vo^g|dI6nB)~Mg*8Tr5R`Ah87Ewo36u3zeOs!FRMqNXAzP4Z2@IHu10NL_#a*Wx zdch-=AkS@w{5RJ%35Yv%cF;8s^lwnEmdOHowLu|@Tbe|{Jfx5?E*j;vrH7Agt0r68fKzhyPl=dcYGa(yi=qT8?}6U(my6*a1Yp zLE4B8L@v|o(&r8+TT_VIp}0V{RnjZL3qx^+S~&|;iVcu$)Qka9KkN8sxXLPpS$ z(1Pjliw}LlB->eF8wwS|l&6(1^P}LC#qN!=Uq&8Tzy==ypdwg|WGbm59}*D%8=$w? zMno{7ewpsKN%b41pUs7i3Jolk;~b36G2J}SXrg#6$=_E&D0Bm57LU(Z)moIKA0@)V zE?^^-qlN!>Z9tsx6~CW!lm;5}L%!3>YMx!r(DJy81g6d`-;ncaf!(tsccIDj%{^V$ zyFK^p%hSm|QzUIlgq!*V+5)>Z`g3u_;o+HUTMv%9%m<4XE0=riha@-x(h`;EsYi%_ zRS0lp;X4M)u#OCxPt3H7^Acs`bj!Z+cdI9EbV|+;5`fC_cs*@S^ijg?=i#2ddgj!8%+15vYDVSf?XgZ)kZfMO zJ|d=KYYQg_M+cmGRj%h@6L5Kcp2Y`CV$ayBeWQ=JBWIj;=lzpucFCgu!P)Kc^!@qg zt7&#@6E}TiJ6=5dYkTK}X1}wCS!+|xI%rx|w@c&tvYT#Kd7-|yl;`r;i*qCAR!$H2 zl24<=e*5Wt$LMKrq#%nE(Jdf<+h|%%PI&uD1<3JOto)On5|GWV71okEEGvlT3U&Z_ zdrdjLVGi`1x^8XB!ju*D#*!#)l>E0>58qt2IDI+`O2Hl3VCfr*1KRuNW;+U&geci7 zj%BdE{L|{>A`nFvoQ8bOK?JqQuV9CTAfc%y2v17n%!58|Dv+&dL9}EIvZDw5CE4^C zC=2vOksu|}(wEX2veF~&O4Nf$uZBD+7}MKq`;uocK^teL53_A9IF_c*EPq=CavW7) zDw`z$iI{Rg<1b8G@C>XcaLg!0{>tqTZHE9F8q-PcN_*aj>j822C%&M7RHSiwQ6w~F zyPnc=I1LNll&gMJMJX0smICv1+D>!S8MgNtNF@a?WJ2&0{B$_V(sb#qL^+_kgow~O z3@=Slp;bw48rym3&${EjBks4B9x29!i+%q1prZ|IvVgpzCm&|6XS5EFvqNA??ayMF zs+jYHbDxt0%J=qctCcM~MQUlwYxi=v43Fh`Pu2dfP5JvKeWa)B(=YwGRgx_5~al-Gumf5 zS628HbWB_J^fX>R79VjQXNzkkhBH-P;JxApW^wfI#N=(6qZf|@5<@@U^pW~I7e+ID z|JYf(%_3$ZM;JP?b)DgrdX{sC6X&l|a#OUBw_)R=z?6~R4-U1k5|vE2e&&GdIPC;( zHNTh|{M9o+0P%z6uo*K{@f8&N^+m`t2_2GA&rn&sN26(b*-X&o77lkr~Furo1RJ>UBG*0_Xt zGG_ut1oAyRtdK;=!YtRbww$4;eOBzjI%(l&1`2~2bh6^|sMz?rP0q(VWRQaxKa#jm zv0EG&KLd5Rq1*kyM^7C;8YD9sikK3Kip&@43?I$g%e4>5D3rGcu*p3?X|wB56x;e! zvlxy0rW7Ev{Q$+hWR84a(Ud(oDZWrCCsO*bfmSKOI7(LF6F}ue!yXc8bLjO8K;{IsLHq`)O^wiSxB+c9e0uW(|!A&&7K|1e55QCwiO`Fd+(o5{LL6M z&=@xP$meWoy#1Wkwe`HB9;uhAO&ll`-~2>@n5!h~QBQ%!v8W{O1+1m>WDPjI%)#x4 z5?mjzaH0;|MjScAe(wsPu;7eJ=n8P_pHQOi|0N>H-M}4F(RAx&EKfvA@fOrnJZ?(@ zh&#OoCe8)On0Liw-2;!%!kcvc`IL+%`a1jO@M{ZuPm`u;N=(pX@cyea4z2Zn)fJ}O0^!%wEYYUCvOGt_1zbbR!Yn5HCd2 zz|G7bzp-~Fm>>}{@sFyue(QNyX;X=@OM!maZ1un!Pi6V zJ3Q?w3vI?Dj2gEDrhu+Ez07J=0CccE=4%rSVIxK9h(OM?E{1~9`r<^N z7p;dJ-^@BGE~h6W%O38%SweUYJG3>Eo(2Z()s`B$j-59>8yW|X+Bzyx&x^RqD$6mX zT#~rnT2`u9W)|z|q{JZf*-d7d7te=P6y)hd*93^1!1@Zp?{I}EOJ@~dyqZ&XvY`=e z_DVx=)2Uo}-H4${69+_5^J}-%ejXfx>qg0-hDV|be z@ToPZ-#Op+FQ4a7_6S4kr$r81eBTy|xtpAm`58Fz)(oWsZx!0_w+Z15jO=Y0h4PeS+ZE*g?^M?&tgAP&Zx4cxHc*hY_^>NSHtE-<37nt5du& z0mq}_m|pAyuCy~ek>4Yx5M@q2r&Qf}+IU^lHF$^8q^Mf$-i;|H{`N>r4wZ&boG(@v zf=eL=M@B1_EsH1+KdWkE!~}W?sM-~e95lu1lpwcv!Kzxgy*nI3ot#snsoUCXA9V;d z{*~rI1*gi=KTV_~&c+vdb}GO=yjww+9DTNB%w z;F)lOC$??dH}84xxpk|~|HJv#ReSHM-n;A9Yj^kR_3aoa(zj#%-;NdkU&lag(wtAI zhW4)^r`G;rPJsyJhZEm~GWdOGhj3)tD3hH=t*aUruvPGeKMVxVJK?&Z8E6@^MP7>TEcEN=@(jp&9Ze{ zw0em0F#RCLuX#tJ>6E7vC+`$mj+1aKjN8T|9(IaD)^vci zk2^pbw{H*Au$@rcF)PuoY{btUIT_Q}wG1l_Z0X(W)UV%*2gBZnwLS_cfxV9;u9?13 z3P2-NK@%_ee&5vZ1}&)8CTkj5s6(w>t0d0I1`+bG^8uC3>d=)t)2+j5W(CH8R4whW z-#6uc141#*Jn8^>BP?+^vXHd}ce_BCE*t55ZS0Y~)Ruex5R?Z*^OKW;Q~`+` zJfAw@BqG60=tlo{Jh+&DQ$*uO4*5`Tz*-^q-+F+DSkC%$dAkK_3cFD6t{LF1-6tO> zNXYIxcwuo`9`&R(t*61;b)L7TK7x7bhuV8=-6m*lnXb6;rC-z&4D^ip!hV$i%Vqyi zM_o-QL`b4Pq6%p(?-jj=eOJ!W4W^BoQO^G-oPHUUa6s0qb~?n-PE`QP8>0{Iz4bZy z5~ zZgh#}+zzqeS=7t@h$5LOPQ$Ym$} zSm_ay)AHtT08zC0m%zKy+f&eO(-*po$#Nk3qHa!J$T!j&z8yijRc`p;jO{rBcOj=> zqWXBMmlCcd(7*vw(}y*#Bt#oBXw2~?;&Ensv6I>@B)B`QJJS*APxioM2iL_O--7eU z-)j%CWgg@yL3gJEWhZSvt-B#*3M`ZjSNlqFrhi}BZS6X|Eh@65Kp6H`oZVUsYx{J* z*m<}_-5-E=cUVF3eEeezm)qh`_vD%eoMl;bEXgn@8D#tG{phKcT~czGyeW5YUE5st zQ5E9IJ-$CMwl!GU*`(<;Ek7%3!DjH}dV46VxtOuhPa7XbSRmlwdwZguv-f2JvAu+p za5OU>WFcS@n+t*G+Z4TM8^CqG1LU=3!0!1yUpK9Z`?GN&U*b9J^-enU1mLFOK4$|S z6emT~PD(eH+A3@|EKcsasCsaARS4=y7W7uY3h5%Ty4Cw2|EQTvrqW-4I^$E~d4K1>>o4*VRgo4Zix$yC6+pTac zlzeRsHsoETW{u5AvK|8VJYbMUT`e!_&K)`8p#z5>ZTW~y(}v;3UsrApX}GmlHgmI5 zOBf~5aNXOV{Jop2*J~df9@s+?gOh>@cs%*Y_2*yuFDH5t!|h|~u4>R@WBLsl04LmM zKDPUQ75H>VjvngX#GO~FZj#=wdly(8FCClR@@ZDObiPheNS2z~zFimZ^SwWWNGlg{ z?DmQnk7DQ=VqetN(}Ba9-QbxsepZeb6##MP-SgV+Ujrzsm&n90M~mm39QteakQcMw z84rUSvTsG+A+OLl=Ut4jflleCWFZm$V zH$RG0=n=M!TlBCQAR~L`Ag(JK_oim3r3PVySG;LN3Ip#0i<)AR|3eAF@gMdUSvWX2{yn?Ls_UVN<`pKDy=O$#=S(#ld3gm!?r;9-rTA&+K^y%WIFZZ7 z_8UM}2w;+QyZSN8q z-(Ki5k8MNdVlb1EOSB_dZ3hM3n_vw`QS@Oqb5lJ%mmm;*-}PP5=qG75E5RsidNhQZ zn^jiJv~nv*D=#sRv#DT(Uao#hW*(xIU%W|hBLEOgC_USJ`} z&c(%0hJY-0Z>Qu+u*xz#8&?`Zlhk1l{LoR~DIpQc>uba^+lI$FP;qG?*|#2?!}+|k zVucf+Arpkh1#h_1o*4arV?p8PO51PhP#$3KIogMAi~@loK-hMd8ZXqVp)(lL#T_r6i{&iKbvUB7kpg+|W_s&VO(o#H>*mKUW`V_N&H()J zxU91viDMPvL)nYAp7LA|AksJ*Zx%yo40XrEJvhCoVC93)%<5!(RJ}pwFEI$~OP2KW zYH;maU}Yem$tF-F$4R+nJ$ejWM03Mtt~HJj`D1STxcOcH;=C4KNr((Yq6{xMU|5kc zhGW>6Mh4?fas;5OzYIvNR2lym5k>TlOHw2c0F@L#GO$s4Rfp=)2xUzKaT5w!r4nQ8 zW`sYR1!~Ee>Vi<4s3XDrd-?DgThLAa&6h5>TYaw<%Eb##8fB(w$uci+;zpG>*04R_ zWhGnQTwJS%OJJmxStV4#L1P*<t)1ELL!Y?BrHg~Jr)nT(7Shr96g`Z3|lojW*K)L(O6P|mV+uzxp1<13h zvWqp6a%*<8fB>IE`e5AtR#g@LM=`M|H~sa`kznY?Dc)oey_`z%Ad;FB^1@DQI@z8| z{I}SG?;TIeuAKCu-#mYVMi_(_1w(JbU)gO+nCS+Q#qRIkkLPB}s7J{GDImPDkyK=oOTzvyI}wjFd<`_XV=&9n*ih(4^}_IflxWoiqZQB$ zS@q_-k{Yv5{WEOVE5>IrJLu{lfSPngHMj0X)`9z^IM%=Ln&#g_mCp-2^1Z-;n0qW* zuI{f%5+Q3X5^`*d%B#eJ=KX0JUI3FNC@WxN5zud{&o9%hfoo+2 zu`RJWv*=RmdO1V`j;HucRxpx#ENBOsz@_!J^`*SjUwtP^e@K>Ww3;XH)2e~%ckTu- zM@6tly9Ne3BmP_8shsjSrKeg;b)L{*%nmAA}~Jqg(TKo-3~%li))!}unCT94Ur|Y(+dt#mPwbxc@du6mqh)Jg zdH9Vnu_5j#koEau&|4`BDdm>*(O_6JWIwbTy5O`XXvVb~V$sHS>9y1WkS~9=Tb{dK zP22BIv^Bm5lwsBwNv_)QI*9Q3DOWMf@#szZVSdtGI%y8uIe46hnsN@qU1>UcRX$DR zR65xLJSX>ss~iqG3029hI?S_-A`%qu$H)^r*JN-aKE>5eO{!DT%RIxa;b9HzM0oI|47kOHfU>2_eXg?4)^elY}vDO>k@QxXj2-CP?*eQ#Nh6=l|M${4v@5e#z{Eb1{dk3By zoO9lqT+4UAHqs0W=~Dsox4B2cSePhT1Pohn*diZv!x%Mn>Lofwgg<~mZR`$qct!QT z5K^saKR~A}pQvLK1Qf0DHu5-7X06j_$et{yY0XjLXZg##(Mnc8@b-f*LyKR8B7!$CWtG!RMaGU z+Iy*Ky{VU4b*g1^%24=BU6ET4zxwdjW_`0zB8B7jpv)L9<_9jl9Y%?wSb6U?IP>H= zudUCAbT-dM@fT!@W;z582mx?75+8|Q852l|3LsZg@KBYv@9(vZ-hq>ZRMh()9zoHs zJ5g6IYVHRylg?{e*=A!+vDfX?9W^(qtpWm()>xExtUC4tJ_8c6xQk%_=2>G)Wa7-L zq57TXXG3ik5%65RJoR7q8p#OSjs`SbsCm3{6ijDc@0CK zziJoz?!AZ)+fdmU+Vbi2Wx$HS{&j_mMi|N|=&{J}ESFvDjUmmYY&B77Q|%|*JzeDz zNY{$h2FS76G&fhd@0=zM%2z-qgb!9GXx#a5+4XDVImyu{tQ{!DB}kmK*uFL1bQolJ z*)4taVvpukZTp?=l2^&#-k~aK!RA_HJSrr~9g=mrk-aD{Ds*Ph?gxU#pPhZs#$s8I z`HNG!WwEvwuzLPIsf7zq*^KlM5E**%tYD;g)$0y&u+Ani>|N$g-`xj$nu>9kY@uG6Y?_7o}sX8mN&d%Rc!$|Ky*_Oh>z6gsBg8x+tErVE}=xq0#V4RVo|e{){@xy7oL?hqc!FNVqw@QEsRcO3PClUML? znw-g$x7*W}(*_*n^zt%rsLkQ%lDPIbXqcc_vD^#c-V0S_d+mg{=tfo|%Q(~Vk%XrV zZoF64F0?xK?9R`dGufx4Bzca_k|`{4Gv!=J;4oe}BmUHm`z!x?2-5h^4?Y`AIuc6j z-^I;-l1?;7@X`h+|7!S6anNT{N$s5jp%T;wmC7oM!rr^zLx@$J{Jbn=`!?>}X8E6^78agwWcpu@TE5lZT)y>L{!5`&NnI)l zr2czB>MXT6B};90wHD8khz2fQ>rfn0#Ur1E7mmq&qHep-5zFZ2lS{G+0Dv&bPZPms+{OoIPXi6uK4gP#Yi6=`Wt zPXicLi?lUxDxw;cQ6S8OxiQrkt36s5FX~$oWnNgb@)=ea6q!<;!zQV-pN8@{H#hVj zs`1qm-zuZa%DK8WlmE%oJn&;M_|aP#MGN1(@;#19VAI+A7<9Ag4~!T5lgB{NQ)vIE zN=@Baa5VN(mgBqJJNvmFLq^N!X=EK%8nb0-bGjKaWUBPUfpMZR#gL)|Dg_GY%gKdLK#|a=;#JEph{wipUnaXz|&dtdfuXIMVvOr-#CHj2Bb( zWLUQlm9vWS!=%Jw2qq>S%>OHc$Ev{^ky`Cd6t&U7mQ8wlEfVfkYKm;b%8Y*hnm@?>nN23q@V7 z;_Y|57!&&Q-Js=6zp?C$6>?}|fFR0JAF3~qbz@o?B1!*&XTV`+k}K6QspBwq<}B1Q z&8y~D9GkN`VxIG(fLeHriDbN&nWL9`-$!TBzU0Y>@w*5<%h+&i44IjZU<^4(+8rY5 zxEM>S8H+XU&@Q=dH(8)q)Uzv}n*2i^BCl@re6M21zY zf$BMr@5(F8n{%G}cS~p)#=M!u1h1~)RQ$9T%6QMLo>AG|$#-?O<33AYKkwKePyxi= z`)H<$FsW;xE`Nl3a3f=2(v+nr-M1a%DD!l@r0l6RU_3)K z#c^Pa!taG*H4a+NhvORq9xqK?1*+Q`h!z~A*Yv>}oV(W06Vgbd!Q`3=s8uHo>rFAI zt8RErc`a5E4o0|vRmOfmV^ajVbylCInJSz2oEWXo$#3svJGk<%YlSMzhJpw_dZ)`W zFh7k#gqKP4t9l=oCy7$=>6eCKc3ekMV`e(_G~PJF3w(9G55mOz9$;^?yg?$zZ+zVycgeLMf-v&j!^mgTK4XwB7FA_O^LHlMM z-*1=c#v}pduk6%0_;-M7DyV;Ex>n)eAG(L#qtOWC_;d(f%d$x$$naFCvt3bZ^?Gx< zi`@V|E$X?eoU$S?XARB;RTjF$9p}^347d`tR0K%z<0BsGQDx1Kg*!IL)-BMK?f~jh z{1mZ#Drp1*UxOC4OBGUV!fu6j{)_HB+KqJmZs>>|OiN8hL4&R9piWrz)0Be^JgBEG z;y?XUUIR8GwYb?URbsYK?75S0X|K-bYOO=t#zZ5<6*l-V@jdoiOwU%#-34(afd>4( zG3zdKya;QblO~N4E|=+oO&*Qjt2WISr_@d}7LDtC=3_DCvHrcK?iV%t6JD~&y~&^J z?Su~y39SaTw9i_2z&5a8e>VbtQc_tk;;eNpjs9E?p+^7L4{kSG=wB1`qO+^clmHEV zc77MkAE+1miM9Njz3D`-p5kfT?K>h1@oC%8Q#4tH++|2>euii%{bPv4v=No9?QAhn z@4b7(Pb#xzuJoo<3E0-QV5?w29xS;~vG=yXM7K+L-ME=XEdJ>&8U3YfCcd0lv^4XP8G}y(9y{I{JxKLCOnoU#^N7nM+8?SSq zsZ(N6pZHuybVPmWhZjr|#+w5R-}d_#CgC+%jRv|X4>tj9IGc5@{EJhg7NBLX;X@Gs zi|%%&G(`@x%jhd9xN?_}6Y~WEQGQ7CAJCtN`+uT8*SGG){~G;Mw;WfQQAW02=-!W| z25u6T6`~sa>~S#YaPnAI(dD~>9R+^;!=%><#^R`=a$;iCieNG@ z&$$kVWiY=)#GWllI23BXa5bxgs5|}<$5CERIPHhzQ!v|q)Lqswk%&+|S2b-_MmAZX zOT~ZlKAT@$t}>STV%a8gR-f-)ALN{UCxco~RPKY2suh*^N{Ti8G*co?(PS6ut<{&# zF2r$w_^Q7-Z4<;r8OcS=V}Hq^;8OM)E>OYyBKqbp@GT(+^Bka_Jb8>_NVDfoM42^# zqvU}Yh0Ush$qa?rTsD88UE^@A8I3w!)D?RnNOM^q^3vesLp1hNF&IXG2v)ak%o0-V zG`wC@<8sP^|D@sc*!%>mmHNT0LHEb79qUTKrJRPl_MdRmdyDv6NK%148t&baf zv&GfAIc<8Oa{Xt+Q|3>;i(2lkPc2=-89q5;Q3KNhEGr@eA;FbKf$@TB5FcQW1nK~P} zH|FJPYVKm!Zy-DRch3x7XSTd^ENrmLSDiSv_K(jn15|rg>s=pg1w1HAtVFxz8HHwY zgVa+q9};s%a{rt`zi6Ah&QDwWybLNm#yesIG&v=*pKt=wzt7$a-o`2Jv0cJfuu;?8 z=AvivmmbWn@fU9&w*TiLV*L+-$t)zSY^?tuNtKm^m5YUw<-a+r-{hwMHc42y**RJM zm%rLAUG8f0XRm^Co^&K9(;FLc>{2pP$jpP^rc}9GyPKz{CyZ1myVTw8=9cTaW&Kv) zl?lLU!+Ya~zaW?cgF@y90@?85ub#{9IUo z8U-ule1@mdUgKlN!CUUD3N}(j!I8Q#iaD65WjSJrK{JphV91DUAP7ZHt z=?jn24`PRqXq;*ZL8Haj2PX_-W6Ru+Vr5}u4c5R3S^*{mINX#3q;{3pYQZl%h#^N%NW9S#AH7hNr}`m}0)yh(()^Yu`YH6I`>X5)*R` z$mg!l9A7*1LiiEsF;Ef$OzvlT|JM0M;1mMHrz{myF$n{0QSlhbQHhRC!wO4UOaaHM_B-Kn?$8y$9FS_fi!G zRv4~ie}ebUa=bB_1ri2jRmb6?yd%eGt_Xb?W@&$4`PKdku5D2HF_3YPmiXUW?PzNh)#_^r@2k<90dPrtomT%Lv~8;v@g)+k zDS~9-(Jw^7ZGg+it@|~w+4sqxbC?yvrN>=hcq#>&N`_nTpxx zHz)AQ`SqJ-+tAzwViOc=xFz#GAhy{LK-w>!eD^77{4-|Q_j$K=^dOTGob0iJ*SuG?Q4500TTq}7>r#+C%c_<7oJ(M z3i-!l@@O7?aOt)&KjQ%EI|Vv;b1LtO%(Vi)Z43rY>e1S9ibR$r1NG3`3LOAj?>dSM zWbM|whql*mckH?38X26)n+c2^HYrKYU4o;J{YAVW*+V58JZpkrQsnWZ)M4#g98U9e5tWrVx&W5wb^^;z9K$lOJK0_Sgoik+UAE#7AMt8d*8A6wMx|QX< zYlxGC*|uF5FLTRrvWwb-4c*~W+z#1~ZW!1awF&@@4P$Q_*RplY^ug@ld80>9aYgKJ zB>$q6zos?8;?^$hEd>mkHZGHYwg<~x=LrRFZzqLR8$0ed zK7tf(zXJS869PzR7Xg3Nu@T?)08%&p^o3H1V!h6O;=_hUt0tlRBIH86@JS4zAv5r# zbVW@QKYJ95%UVy-B9>P`4nnuYaBP%)ry@10XldsZq@_-oA)Vce;a!@Y{+lI1m%Jt> z*9=b<`v=-xLD5qF6`jl3o}0tM!JVeu?$m_)ypbpGtX3*UOeS7+1_1z1x3h{!wV~~~ zgyh7`&SW|?BuGsMj@#eZcefYx4kWAO1YV_vIq1O0Iql@OdI&DKvpThvJ!)1uj)mA5 zw)=B#4yfL2InB+uFPp}OV1BL@n1Da_Y-!qC5wM(#(34ye7W#k zj7*yRfl!4gA19i$*$l+DPTHX7W)aMwRm8-{!O3Hp?WUqL>>uQm5e3X;P|kMXUdqkn z%87>-uZZ;7p<2nu|9ipgidNLAG9^u%=S+|m${&Lj6zM;1S>s@S#kA9AaaXN!tfx!5 z@oYodAGgTng~C%_9YpMaOcQE2fnUJjL0m>4leMYT)0=oP5wI`gJOEf0ib0?HQB3`& z#;g;fnl_iXIYbItyUd78*zCo1s59lUP^My?4TzcaC*l*83t%R68Op(krb)m2WiRM; z0xDm%nVfu_+%mv(mS%f9Ekg{xMnXl8aAl{pTvhc8UiL%K^B@rb_&9z3D1&Pu6~uDK z=@I-@ria4ksOYs{e=r@pU?us0%8tds3c>EWS{7Lb0uh(k`CD-;WNJpmDOHXc_j}zZ zqFfuZ*{NU(AX&<_hFQsuE}`%DcaNe!rP*t3cw;?px=>IX`IA6f_t2zsj(hfsX3vl_ zOo;A|L1ezhw8(9`{<2V=bAhS@db)MRPZ>DL`^@@Ni)hvFn2oTwPC}WI(O20yn5k{n zzE7lK#l0ts$%14C6=ly%&=tPY17vxcqhMb=s-paa>5XL0{}T!Hs~B7)k_XaY_lMfp zBF9mX7U^LRIQ3{?;5ZJXfkM{xtojP?8NfM}hL+xbcuT}gcQhHc znPY@_kJp#xJ$yQ!4E4SwB)Oc>dR?aSc3OKyv+LpCIDC}d&y=diPb02pD(O&p$86iT z;R-M1K&qegF)DRA@4jx!38Cp1tiLHY4he-%3l_>yL(W?r@c_d4O4s%(Q-WpOsJmJT zf~J$d+SR4?X-j#YO9y(iqG*x^EH)Qv!tOH~N4ME>_E*5KN{Rz04cVK#kwlnM+}R4> zkVmjG7*W%v)ET2GQu7iw2#S+mR%)B51qfcim1lA&E~ZdJI@r0Oj*p*bj{9khfEmUl z1SOMch+7|>alCLU@~i0DI3X6zP;z5VQBq~u?&ft`n=4chvNLL9@ouMB{H8${mGcbO zH#;EfwpY$MoM-UspX-jgl0}lN633^9XN(Yi%(IB~iw0&_J}hzG$`zR?;k0zcV;2}vZ8eu@a7~iyC|ZIrQ7Su}P#9w1{GdWYBTddR zR%+#5KZS$H!zOgWb7w%0aCd5j^H`q5F#k`j(~&m<+RV03BQlWMxqf>Mh$fwec2lfS zt6iZ>8Y-;;x1&j#fxQm-R2M-C{nf0}1`A`yj)@1tAqOBD&%=rBnaO#VM`Oj%5}B}f zcd;ll$`RoxA|X-Hwhbpw3A$B_Gl$gVo`c8Jw!@sH@)qjBTcLJ2?E+~04JoC8DBh%J z6e1uWP~CDMg>k7I%X+A~W1@zjP;^8{X3Fm}_i}tY9XIt<@e}HT`-}m%+4)*D?I5$& zA`RyE4flPVhlXzJ{Jz51OCMTsr_(!_#(BL-Qi2?!_dQI?p1$;;_{cO{z!4B=K5P*2kt*g+FL%XvHeCU;PG)H-hNN3fMHg3qeM;w(SZi7P%$#9{dQ3Hn znh9IBJ8lovGh?3WU?BL3e8#pt%zC>k+iZGi2B%>P;3 z54TlKDN*w27@P5p7m{l$yAQjuna+eD=;L?F3gMnqUXOpoBIBJkq07XT?w%SKO{ZWK z1j)ml-nu(lk(*_XzafCR&>OX{F%pvn?YfCf;$H-qp>2dKQgYe@49#q{R_>xcl4Jh!Sv>G5ZW?Cb zJQ!a}EXS4$Nv-=zcnr6mZ$%}ue?ou%p@fdf>>0b}#ZlRN-Rl)Kh@vAKKB@y8s4kt| zeQ^96Jy#OT^3ykG+ADk8eoZR79fX}X{lH^K1;ePlH@{%T9z_y6xyU`l-8f;&+aC;ca(pzo1LA9=jlw90ECl> z-R}Jw`cBy6XOruDm~^`h)eJ@|Cwqga*+$1SElv>YcJ zB=t^fJ{-Y~%1n**wvZ_N@JGdgUhObc3Oo*~x1Cgjsf!XAhIOiquHd7f%>#*-C#X3O z9kP`)1KDWgxzsYLnBZ+iS|bR*i+c-^w>I+kP!4*-a_a{ zPM&3WQ@g&@ag>fpIb(5QeA5t!(BDS`qGXs!`{s;>ro%c%%EfB{cRU@I%$3<zTDtlhM)_&1QM{IVAXyB#6^KzWi=qnxn(31Je&>*SxUa>EJ=M*veSSRU3N-{R<}I;E>RNmWJSf z*XdU_h9IenSvHJ9^RSYhFUxxQ0PYJ5ytWI|&O9ftRQ;6s=t`V9iteaQ2pd)JtrIYO z%+I#lx>hb!4&&T-pHKaR$|3YFokhtdBJITM^_|jb7n~QgAQ+o~>Qtj&2G%bgzpQjh zaH$Gh2Su;qR=Zd854lFh`nU>??7_Hw!rz+Y_;%R$+maTz^JzX|fgIsW{3Y~6L*y+~;aYdV27u%IIEG%!- zvZQ)rR7kV9Du?Q-DL~V`p!su5n2+9VTcYM}Md}qfd;N1tK#HTjrC*zi0*ab#rJgzW zppbR%aa*L41QAgt0U26*U+%!3oSQ1JY04i&|nA$@quLD`Tq{*+cIVVr?nzi&?vE$B%?e}k+S#*HQ^+2Sxb!Q_ZR|_yo6$&TOR$|4lnz~9?OHH~c6O(+yWCsh z6iJi0{+|XB@00IPj{-Y|95{Q`jv}DZbPR~I^cRP|SWO0vQMt$esxwTT&-W|~#O^l~ z48!602;1Aoh7}W`s;5)0L2Y_H{2maoT;5}2SA#(h5`Q3FOl0J79Fe4~Z!NJ6v+yEHPMzhb@TxTane86Sbw$KZv zcpe?3AR*Qro}T77$CS$~F}U%PPT^aRjrp`8Mg~n$k5E|uAZ3Bc6{y`DrCujM?K3%FkWzc6|vdq4rl8u$IUnTYtU7?Pj=4mwl5-aGc7O*A~3vX$y|R^ zsts_KgckXb5Jvi^K!++qAyhoXB8jbi$K4an7fLzy>w+qL)Xm6K0EmZ(UcDqrvMK7A z{s$uNuWbzr{?H%oVfxQm2z$R@iIWjgp+6N{l|>cve;5~Al@o&4ot5w3o6andhx4S~iFV2A;>`CP50N^8UqFw$^P=V}#4=KN~9IyasI-NMN$p zPLq-vy#tVd3HRkC-mDZ^pGOm4gl5}rCZ2nMSqR)eMjK9XXj+;Nc~6n#VHo1>vJ*zN zb~*rY>ENhY2$U(q>91%V$N5}GYNXtC#W+&ZvSq6bl5B{&Oe!Z$quin-6Nh@k5n<>5 zszSK{i)OhJrdHgt%fYiwHym<=a|j!c+>O^W<|#M-GC8d6BHL+XtTuD}F6zrCmxGJO z;U`42ZEr7gONqVxq0d$|53)eHc4cg9+mL=jsCt67^TMOm9dPVv| zk7agDA9vwTt?8+(#@b`?MgjKez9ySn?j<<~*7#nuLohvapb+SIzf`0KxwYo5_TahO z!zWHzU5tWPT+rH&7Jis=tQ=xi>FU8Q_(S6De|pgFgRoSucpx!dr5> zeCZyFfAm?1c_Y^JNORJ@C(Lmz)Epnibfa&T$;;_=dj;p*0o=rhFggkmKQ_oEhbpEW zD&I~8zQCA!H|}?@3X1$91Hz0n37(ff#_S6&5jXyGH>{1 zn;_}!u3!nJ5#&;;77#5vW!7!;S~(vtiPLb&(K-wr5^2A;;Ens&PwWYW!}T7d6CzV?)F;IqY_G*ZZvFW|Cpk& zS()$Zwp1SBSND8ih+Fk9Lg-22_TXB4*vuWLEuD5U>V#9LI}C)~k;41hytzn7>|Otf z@n}uh)jcl6mR{7vksP6S+GLSb8rJ;5U8n}RFBY3pYtn;-1qIJYy3LnS(bDnYE$c&) ztqOiW4-auNkbfy(Reo4A3yB~jMnOc>TZShARBTbVny8QFRvFqPA!fy8nS}5?JFH+m zZLm%4hgYsuKd(lqk>9*a7YhP5HUAd+lZg9)S(SdK_Lk8soc;SrG%As_@V&VkY?cE@ z2qBACrxFP*C3&ufPyl{!WKGXwC5OaoYVGt}5ooC0859pizBNUzO~Vpd~$YLU)e2r!)Qh`v^M!0MhlTM|Y<*~gr`qlw|5yh_2ped2YDLN`(9w)`3 zY3DJXS>Jh8?mpXbACL=O?)4^lg6K%6LVe8+;AAc-zL$)LNdy@R3-YKkSSmvhzGt}P5#KNkfOCvWh}`5Y@cz>pRYm29w7R09)PJ24Ws@p4Dl}ajN#}M}8HX`b zl@QxOc3L331CJF^}n{%l~?|t|4^pf_vUw@x7In4?2QZG%w7RMmU+45%LDP(m&s8c>?`_;4gdxyB_Cf4hZ436wk2}`^ ztvoEK7YT1!`fH>~Q8`*zib64Jvz}OP528Tx8Mhc3#IwhAWoz!*Ih-C4TV z=Y|g$G_8Y&G$I#&HwiqbY#_GqiVsvMVIl`*M z9jPV|Z9O_4MO7o^G;wW}a}_#$<6CYn%Mms|PN~1i!;jh1Bx`zZwbU!(QUIqv)t+4| zh#2S9xZ`b~6(-?sAce_tFHD~9%CH5_tBVUw9ycn(j}sKUM3gh6zE0&lF7Bt73wt29 z!~YvHGGBb~5Ufe$ykQz2D4Km!S@;i~O8x*OtEu)vH|BBf^w~(K z!glQL@n(ho_4A{CWx%nu4aK4sRg?SL%3xiBw!tH}aRLmQo+Xdd+o#9LdW1d+bx8mx z?=>HhAit5}0VERdau$20lYnnHo>E}V2j&m4;dBh@txVnE0?chk4Sw2ayB8}TLoDA_ zN(S0>ABhsLV1Vcf)z#PG2&OEP5Mv7c91a`Z64x%*93`gJ&PcmT`CFpU*|8|I`Xe=Z zTQaF6-3h0cTFowT<00CniGiK8C&K9-M{<*atpEKWgjK)jb@HZHfi-GmB05D%A)vkANUNqvpcnhD@x84QKEt)Cu z5&Uw5ryN6gn#oyA?3SJl{75n2; zL@n>3sOqOUFG9Z5jl7EeiRwP?08>q&UC}gpGf+Ftt0#5d2coLIRfsOuFdrDvTKk$MAO`W-;64X$=v9lsEogV)76`Jm{i3U2Bt z8lIt|Ca+`|vH8j_6wIjG8IWu7 ziDRdSxpj}si1!lGk-}6;WXuxxxmBr%a4i&S6K#uzKDLimhQIT&P0C1}x=~Gd08|is z*~iwsb%@Vx?GC~7%D!WKhFT7Q%hcjk(;6S|5^pP&JyWKqN+-3rew8b%F$xQTygDY- z3RjtfOhk`+mt__3*cCyZTCBj*%?@RE>HX(kI&`GV0#!y#R};91G@iv~37{7sW)hzERzI z5F-@!sawucTAN`c+jECokDK3KIiME5Gk8-3$j}TYD_>3)#8B1*ediZCI^ZbS^-Q6L zPc>>bF@hI0Yg?%akEa^g*gH*?sEsTKN%J<6X~bEvo!)t?7gqz4KAnxttdi(K>J3hG zjVm>8H)E!H?RUyk^aw!iL3~gOf$%3RT4sFigDS|1z!rroJNeaPOMV6IP-jZ@gl0L2 z^XLLM*hyJ9&Ep%OyzH5k6rX!F6&Fm3JH1BEtI|Oo+U#A8-Kv&*Lx+9aM-y;(m)e(V z?9t;>-NSv?4BCYlDT;;BXDF(RTC^?|hJUo7x&nF^8&fi32Ky4wrI;m1YZVpu_1_&p zubqc(Nfp7AG%X)Fv@lZ~F2!FzvLxa4E*m0HU}-P@{vuSybC*Iqn0m4HVgSJV#4^F{ z4p(w9AU}y^bFuzbe~|h_ANRl9+i;E>&cqv)`0cEuQaFhfS%Alt#Hz37SntI%nGQmm zg9%)~6sM`pqE<6)KM+F>z)D=5JZmIE(GM@k@r7xmi`da`qIUyUwbTRg7 z?vt4dLxm-EZ0;!;G|08^QS%tC$}B&g)3$m{SY{8^rF~^^RgS^!@pw}vST%=Z$UA!i z((E@nB;fj<0)WS5MiIsC8s^~I9on}t_t5hn=JU+!(*|y)T{kPaT99!IUjAcuuv3jD z)7I*?b~?n}B~@IG79Z7g1Dz+y5|i%}1)}8MbPt+}Ih(Paod#_1>$lsy30N>0jwoAy ztW2e*)Jced{Pa^^Wb<56bR1QH)Fzni^2?YZ0cT@y&H;L zjzvqe=1N_ZOSHb7tC%}62aF*dXs%REjJSJE%Vd>qP_gp$rAw+E*F=tilt8`gEG@2~ z(MGRH9(D-|@-JJuk{inhI3zKi*mKJjT-|HpVUs8*kMOA%i=h|lpB6!w6du5`9L!u% zJo2L~g0q23J6cAO+8anxvPVP+65SqoN5Y{z)TsnZ-u;yK-(Tf>M2qp zH)f0GJ_Hj7m7fq%x&(6Jk{)13{fOR-7IRKQ5~}Rg2xu$)bicg93cdPrFf_PrBD$n2 zuN!#~>xHxPi4+&atIFQ;|M(Y$(xZ#+Ok<_OZYvbY=b<-vBpj126CbDqLx~5_%(!$5 zLS?Bw&$UTZ^kX$&{*o~vmh~)YuE(GJd2U4c-sg-#&G8ZEQ)Ve;+?ZZ{lzo^1?$evu zM`ggirEco?c@r_IfuizeZ8H+LE(0zM)^=&AmN~clRchT_bbPSM2Rp(7kyB;p%a^Xc zd9OVo?NE-xZ`wT)=rHzk$^1dKt!Ta1GHGt!$^)5(Prr0U-=(V|WhTSV1^C2UW}r>M z@$`ObiB=WfD8B-hXnsXT0JXGCDSWCmJ}LSpFG+4`(7%%)=@~T>(Ju{6iQ|P`pi=AY zg2M#Y?slGyHdn4-r9|7SIc;yS8|hj{XPKJVl?R)6CY4Jl`@+MPYPTkN>Z@}nUdc78 zC02P~zSaooaJa>o+Q^I%_9n^+T~tIvm~G$-o7c?s^Pb+`+n38ZdBht;vPd+N{K66qa>hBw$&3s2pyj7xK^_WEvgs3jNSd2z6}<_EczVq$P6nZDpN|h^ zapq6(AtBgR>u6}a6vB=&*c1I9bqQHnIe%eiDvoEj#fmMoAEVyTFjW22ohK>4x{A-Z z=Dut9Jo79{6HFuxSM{0*RTDm<{&h9}%qJ>w;0rGs#8s5B?Nyl;cZ=>0ai_e}V$``z zGJyL-YNOo<3z~eW$WZX;*cyB9@+w>;cf1ng$;lJQgYQGFJsN;%{e4Nd&f z3PQ*v`Z?zv?iNuhD#8$HLf@B29|fl&AVtykF2Mm?=*!0T=}L*vk0VUfqv`O`g<3{( zHP`9%cMGk@P}AJPAD*ZcCMfF3(L=Fz8RWbrR}*B@crD>IX2_xn)LsA=@#%70~%CM9M{g}H-b7jR5dMIL>8=5ecIZz>U;;F2In%|23 zQYmZ=chf)JrIBE_-W%x%`e8>$t%0FzLc447i>h&hbO9cbLg6&tX^2qsirlNemtS2TJvPomV(FzDG3+`g^; z7zZAm%o0*11sO$1xgEirI}x6r)gR-`^8}!7dDd_+&D3?Fb|)H2iwZs+AN){rMQh?c zq)>Dg{bpzYqaw1bz*xJI{$XY)viWjGQPxF07&&$lR*st*qC4zz*ca;^Tmsw;WcJ<`NK<(z1dt2D)W*l06))qujkrOx~P}5 zQ@=`&cy8^@(q_779^Qlb&nKrZ8O(Htcg7-Il+IAaS$K-5MKc^mx?8vk3{B}3Uq2Gf z#6a{E8n(}V_c{I=HLDv<5!UZm|7&J5Rq?Y87fY9y=4UjJ_pxOvMH@|ObSRjK0vhya zVXX#Jy8%@kyY*2iq!b^Xc6t$&G%c4&%go(jxobSM2}h#tJutGwt(xq+76OT({6j%H zwS>Q|RoKiMjt=#nj^YB}B$fpq3+T{2kW6?=SD+5GeiWZs|!9=+~CnqsRncjo22`G*RgilroM&4 zbvQ^dpb~7_9xtD~%Fy;}S&fNj;_ zjdt!1kGIj}5teIxADkNyUK* z^a*t`2!WhzdIF-W2^Z1oS^G&0y68VQDSZu^vIN#Sj&46te>8;QkY0$QOFuvE_o>u6 z?Jt1;vT;UCEuHB6{f)v+f30elte8}WQpG3jJjX>jYOBEdv9n95;y@wfAML))vcE_d z0p9a|iM6HQq>qo^y+6?wSe3tF_|GSG;!(k=_xZ#(-F&E@?iF?OUW}Z0Sp?XOBH(n- z)Zy?S#x>qHTw&O(-bnTx%mfoIrr{7nexB0^+>*w7`ijm88NUA26K%%49WS?~=gY4> zH78SYpTa{}&rjypMIuP5kF=euAJ|v*9h8Sl6RdLw3@vS{%wgFR&a9r^pn|WN-U7&Q zABAs*wlj~qzfHxP!Poj-K^u;Dr^ZB#xJEE9!+j+d;z9(ygQxv^xxP)CAi1tv9{q)B zsJZQ)RkJMx5@9C5dOOS=AyDL+h}kXAxcwk!&)pe^JFGak+GL(CKLG!_Y_;LX_ibMX1W2;La#-bXj3I!KZ1WY zwpZ;nZDXgbBx~s~RYjtm6h6(v_NcI4vdFq0*ZDR0<0y&NGH0N`w{@Qt^{ptb#_XL# zUMU~KK7x(YjC}i~EN`7}pPnAdN*e={e2W6#m4_K><@EaiSs79*7}Qcb+!$xFzthvX z=pf%_o4^z7rAtg~!=on=xBGd>kkL^d3vJt@@(xxHReF>~9(aS@4yx>#yTMP_rvAMq zn)l8ucL@49kpfE1@;+te5h%d;CT)SJRirV>$X$PoE0uL}PU8^Sq$52^kGPyiZ@;V> zGkPL-lpM*Zqwq3zQjEE8Kh>0a>qHQ<>t;a4jYCjPq-jr!82|Gzw5&4MeCRMs`9UAg z>zgjFdH3KEw5o3iHVU3%k(abJbk;Lu4_c^XKz%X0k5PviZB5smC{qXE9;mQ2T+T0_ z*L!=g)iEwyv&jTHJ7)b%W~?c8M=!eIJqwgvx0xqiPKd)n=m20$u9ig`fMsu5TI^me1TAh1ekx2-p zHy8~V9hP;cGigw`Ci&4`Ha{ccwgAUEbK^hw4Y2AS+>Op>_j;<^sq41-?2?$ul&O|= z!>vMG*M-^4NwyutsyUjaM=?^MC0}tvac<&Y1#CT;cAZ||4w{VFNSNQsfY8;mQUi{v z#^*lja9+3@qX;?p@>-NAdR&ZZ6v;gl+NvKvUT3%49XQ=Z+ms89B}5SS$@0V>zNO8# z4jj=i5Gs5e?SBh)m*OoPds9rTTD%^5S+k@&^U%0s)IDmvwam#<>XW*qbUnh$=R~?w z2qVm-S$$efj^RFTaSC(ttS*|8A^$$+GqKY8ys33W2C`l4H)#xNNM{}<{e8+-%TJeZ zpEI0ukA8)LhghcrnI-0O{1LhASmbYS;*0{d!UTM#HR)L;4%mz2agFf#uM@7_(xC*pc*W3EMKdm5m5MeL= zx?xg`fM7MZLNo{`k^jAYm9@{0>;akruGz27+=XA7E4~9ui4o>e8BW1y67~XG5f)d` zg3MQALHcyqB8VMfgD#>)Xlm=tIdfN{ge&S4p;nwYwGZjKf(nGO|FL~R3OBRG!(i=d zLr$BV@}=}B&rBvR!}q$1Z=TS0<#l*u0<$f|tUs6|Amj(&Dh%j4e6HG%oEjnf*g&VW zr+B2!8Zx#xP-bY~YrddE^Sj3`?{zWJ4M|apw*~|s9zoVCkrg}vguz}Uff+4Iu~6p` zpJcBu*pMFgUt+hur*T|?%A4Il2cfY5`M{_ziS)_y8Ed8mDOuUHr>{qR3D zIOAx2@2I(5Qj22jsTLN$NnCB(2U_1b(XJ_BiBr>bWf2e%xd5E)L8GY#W_ zL(B)V^VF!P~nw4=w3QtKM7L8tL5IYt3@Ff0yh%ZK82_;TJIh46$<3j2Q5 zxK6BN6oVe5pFp^0+X->slr#uR&A}Vdh*a@AXr(hla+0l#4!j&-e41sv&~0r9hAq6H zhf^!gh8DJp7`?M~D4kH~fTHHY?Vgsmc^9)fjWzVp3)M7hK?%`zefFZ0-S*^vwfo@V zl#FLXi8~pAakZMVo1;c&e2(3?8UT_dE)t%RNVZCB$YS+t*fhf(^NX^oMGneyqv~Ya zaxWQm!A~|5huqTX@j$;@`Kn~RwQY7CZ8hK9%78F0`@M%87G7G>8>1F-D97|Y*adR6 z{Zf$bO^O_be%AQ+>zN>9b=5oTf!!nWs&V4r;rjY_o{W>D+SQYg&TBj(?j%e6=puR;JZj`)(U<~f?n0EpsI|Mt|nV=tHZdoyw}3{@ax4{{U^R_ zB4HSI0HyKFIlIo1X;#?RFH3;A&D3;4rzDPtpilau3We8WsonU#&$~!Ro#Ly5BT9MM z#!SwOy#Vf{(OAyPH!}MH;|Y6s7+_5eJ5oAZmxP5N36(8F!C;@*etGjtUsuh{0#~bR zY^6O*=&0&6{f*Gs$pz@f>K@ed*yBeAhYjVq9DsM;sn`$omHgO!7cGZ+9Ax9&Pf$OQ zgoVocV{%8g-mz=*$CeWkbQf^tKvX;^rik-cEQ&FiWpdR#DNnnOfzay3OMDEw16ttb?17ducZQI-2K5DQFg z-V19uRyTsLI4?+|wJ7XS;=JoaRjWqI_Zrz5_m!>JDX;Vsi6}K?$&~S1)+}TNL~t>< zf;uaGeHlh$u<%c5W6vE@GmvJULtr9Hm8n+>;H^byaBO_Rvk<2<_L@Gs-b8uDV43yy zwMJ*7coy09(@H`WV>NSxGD&bKO>TaGbEn#t>FhZUj&l^^8>IX-cONW$jOYz`IGA4z zTEt=OHA6Vi0a2Ok?6RyJjLEyzJJfffk(JIJl~$s9(`yfXwRK2H+g*H@ z&!;$__r>s*Mz84^_a|+?3cXNcYetCFmYoVkIAEr9Ow`7AE2!H>eDJn=`&1NcyM=%O_Z=&wv3ASY?P85PA^l2z7jEGFRxARo3o*$kJ+VQw>(EGxy)CLh z?2)kyaS-2lm0FBE&jR5`>aXq7C44r1YMXJS+Wlv;6)Q7wdhbZ6^FsMS5nI7_VUA0L z?{l9AMPVLX+4psQhkbYyqouNvBBCGs=SReI_?)w-93F*synnS!0bSmEgl|0Us#d$`rl^GTi{ z;}4x!S%SkVNOydcpc(FV+xAS7J@c0zV>{?hadUa|9Tr6x4L;=p-djOSdU^Nxd&tWL zSS978?22bi$t`Y%ZR=B7<;z#iJPEBpt+HrN{Vp>b{V_1!m;&A6$F7AC`)G6299Ypg z6Nt%F{TY_+83U{g_t0M+{74GtZ>AC7k8xDbUFuVCp?a!6q!jVh{z_-@)Cq{uKbL;s zF>hJ>Zt0z}#bYv;Pu2B)fpEu~itQ%cDc}*xTDE=fO~W?2^RJtu+KxC@n5CVqNOk?_ za44(wqS$tI(oMxgtmx+G2ASrGxZOSOOpSL~3Yew$7cu^_3}G&h3@8`}(b+k7sljR` zz{JnetVWIoJ|2O|Ee}V1j@-W{8B*#+`~V7X@8CUV)HRluV&?D8g{Js~pR^FG0%TK0 zdxe&M#^~v0 zAYV9bcHMGS!7kOeq)tnGP&bN*AUn{3DQp(Ngs^ZDoD3H%9mWyglOpd}N5|{G(re+f zu-``w)YI*_$!|TDlD-l(IG@@#6e)G&XC>+23GYBB}tsDNT`=AL;_#)%}R{x*YR6AW40YRwe_ z=jtXcX7gn_LnHWe6VDJ^n-m%0BpTeNAJ|2BKYUQQZ+(WaU1R$f!6)3W$sjvK4;w7ef16=awBb-xr#bMZMSWxu=ZVj7YO1m?S&x59weFgzY zg!w}KeTHGzH$-aLalapddO?iyqZr(a`H`LS9QzN@IN_CK_76&>C*1x|dQc~iS!^b2 zW%>I2`JR)WgL&6zPjS}Fv^uxuCtc|{t$C*>3wI6H)uI4LbC|L*HQ@;q zSM|lBqgx)uX!QgJ4^ZW|O|H3xdR5Z-)gd|1FrhpIzXY!Pl=T|vHuWw#dcj2XTMuTN=tz9SZf^C#@XcME1G~ni<*)+o zQVQ25SKoQ_N{wmp!br_>q=-`9^yhu0#=MziS!*3vQ{%7`BSF=4J5J&9f1$3CQ@=S%c2IdX`ho zZ@VhE!NWSI>@$u9bU}q;Ref>o^i*D?L>?oqf@S-~;bTsTpUYlcSMKqwTeWGzN1Kqt zge~81#_1E|Pl;dMW|zyVj+2j*&km+ETa|()cBgdYo*@i97MY`yd*mQ}&kbhncU;DK z6kf7fv8>py-gh|Z7S??3V_OqqJ&65qj8?KL;^O>P^OMT9{s3(|?|bW06MLhi1b<$M z)=$)p?!=0&KA2ET(7!axn)FZKB^KiHG3I7#G%jWxz3<1?4oT9b9~*sMF(pcF-`)xs zyEoEtYa=rbl~e6Ii?p5NQu>MR{>?8pIO2R~+{{x=4$WgXF86!snnk_Y*K$tOIUq`; zEd$$8j^@5is&j;v^|+ksuhFbrB}%RyK92SEi!hF;?*hpAnelK>A>M_zM%xp{)Bz=> zbTw?}{c{+?5y=*v*X#nWXS&gn#O>_40h59b*n{Yh!~rlR?iAV4+hNQYt*r7Ejnz|Y z+ykcX`G)dBTa51NB#^=Kn4!zRJ`PJ@_?~d>8JXU}VM57c9OZa9CCbd>FqwWFcs64u z!8(Eb|JpdG=1c$uO2*~{6Wg|J+qS>hwr$(CZQHhOOp?8|Rd@I0{)toPp}QrSCkAHl zm^`{r8oL0$hHCA5AM(;;V8o<_Gv5QduS*D88mn5dfNu@1D)*O)yiWi7LqUptFfARx z=7hLHE9gB!imvh+q*~pI4E$3xuOTFcMe!`($^8hu<~MN9d4e+VF~mVPgOSP!)%Nbm=@?Lh-*W&X@Gr@I>5RY@5cl9*w%PPx}`}_^&MR~!z(tST;GNk0df9h=!L%e%d7*LqEYXcSYPCa z6G076esEQGv1}3O!$|1G(#QblXRLeGWu;(t-KLT2vcvHO=IN}>B|4-{KRYYwa#rPO zEf>EMc;(YNj5_Foj$seFn_(>QO>W2AiGij#p-8@s+mzZo7I-aqrg`(uH8(v@?c&Zi zccr6@Od9=urT71p0h9lD2480#AT zHozo(P_+Jjs@giEdXZ{7#~V;%&Pdyf{)$No*$4B_!N+;T4fD1jd#pwe^Ruigfz+?y z>!F14RCCKj+MY9&pe~hPE|n(axAPnXlsr}tqZi|{nsIgJGBs+C0)4~zs+k&tg6wQ6 z5=US)ObsgUDMkvV#X0?$uv9`e=rPt=7ue!~%q{#`vjes{&RZu)VGT6zve3RAU3G&p z=?Oa-;N#E{mDXxbE+93&8%X0%!?>l%Y`N-@_a2*+`mjeEw);(mo%`V=fiV4#Fk2Wh zmw>cF1rl+Vzj)GR*F0xuiOHt;zUzYIVwmgnqAetmXutuWnI%u$x`HOcGAz4P?>NYm z2_^AV{byPLW`N&&WzRiUlZCMBmES>*7t!d?JqjdkUtRH!9|sDxFNT+Vn9epW6cT}T zuNLdIQhm5*GmF9_y}*ojp9SAV+sYQJ@Kkj>Vk&pO`xW;o%jmdT7&lQ)*0l&BX)Q5P zMz$C6KkVW?@xOpvzW-cpQh@CP%J9(Q%DBp4f5>aJ|9G0tN8%@ef$cD}^ehHXRSD9+ z!0}C+q8TDY?s~5q(pv70TAv7U(9oXN88X$(x8ngmWhRs@J6prGA@H>`38o#Y@;)+@ z0<`Ndv}T~hZFqE~5m8_I**hf>5$gu_-&|1N#8MQTzu}rN(ky9S2@Zrn1xSs%>#)OoNG1{M7ianP3 zZo+sV!R)vxdSUgkb25Cmy6k)e7!b@chhL6jHSwiV0;Jm7pBwagP($ zak_9I#^g3pa%LPFD4cd7DcqrJ1*<BTh)^p}Z)TZf7VE5p@CZDo)iqcUU>wSA}6%}DO zItp(GI0by_2&&tFo3+rd1nJ@^vlusMu-)V9LbR)GHKi8S<#h2JicDbr5Yg8VS3$cL z*AB@Jz!(^VlR&G(l&B1iVPpftp1=+dei`G@do7)j)Q(1IhJgo%3ep$mUeuH`{&f5S z+J^_YO=WW*m`aiVWX&CUSTuIxK=xic2_k(*1;g1<*>3tUw51Sf$X?H8J^qeYveXuM zV5!?G0I4{!b4*@cPU~&Xk1YhGo}Dw7sbsi(tOvpQxCxaoWhYLXisYVF#f{tz=DjDk zoS!>n&Ov1gE5EP9bezf{+R5=PhwgxYzJy4+tW)3&8DUtYMh?0=uJMpwR5DS$0`oiv zh9%X>MNkBLSI~J%g^@p|bbYj0i1vaC#M8pk<$T7N%(ikZEENi!t7D^z%!S2X*ccWp z1L2?-AkZeP7uJ5|M*w8d3aaBBJJ7oFj$zdgK_{ROI{N#<)9_cZ2gVRDfe+cnw_>W+ zo|XWqSn@lsR6V7WQ`fW#gt%R6B!s8rmqtiuXwY-$anro7P6IN zNOtQ{Z#U3}>j?NplR7G-jOY4X5bQ4YeA;U|aJB(jh+8ry=o&YZJF2WOJ)j-<_N@X` zD75z~yIk-S)4ma!PE6dx{jIR%f=;S|-zMfo5{&5%NEyN}ySWKpxrT6vG`L8%CsMSl zzGE)7jNCGfl>$K@lFpI!qM`G|(1)5gpG`o+9rUIA$quM}A5;i%bDVoO$!HEe`Rn|6 zB3xo3SS-~q$Eh4A7!X%MRj`4-d_!u5ry}n9cF;@;?NFL}(tXT>&LQpHX4Zc_PWx;M zP)FjbX}$o!n%IU~-E$-j(he`uv8_N$sNuhy4PUCF?ZTHoLnq#3&63-bpy?fzdJCgJ zE}e@5PbQVCnA_BHYvI(fQ;cR8p)b%xC~8sJ6!{=O=gtJ_%JCjd^WuVjph~kxvI7y` zon3rrc56Exte$(Y?w|&Rf@G1H6`F0Oej0gL1*@VqJ4Y=AkvZR9N`ajOkM@fC{MQ}u zS0!V{ zVt+S@0BYGY8c@L}curo6y~`b~z5WxH;^1i=PM_ML__5nqZNc#P22b;V$Wb<`Yq{DG z+WhL%F)%n`iDT9~WwvD{1o;5QB zIGvo6;eSrKUut0Vkz_vDPpg>+_r(eiKre%ZxFw_y+glj3QkTZeL*Z{d1-ie>bbok% zsD{xg0lpl%dkUzR(=Cczcmk`N>*~R9gkXzva-sV2au~5++{PrI6e}NqWd~sw8D}xb&s<^ z6oWjK^Ur2r+{VU8_uR2{U&1Hiwrp2RhCmF@BMBo&(4QN!kL|jd2ct-m#wVzb!Yq|} zUI+7z>7-pwO?yFD#Gupom8Wb^t&^J(G$7dcg8mfYa3w=w`_3=WL(6sNaWU|r0l#TS31-tOYvr8cO0i5Z zR%&$2#b>96VW=;7!J#|T<|+6u3_b7L68l1rZY{<^H!GBn-D1W|(s)PZdwI^VfvpQI z&;VvElOley9x|E z%fn`>n<(!zN^||3J1%#XMD^%`;li|Icj|cdJQLCG2a$>`O-zC(e5Hho%w{p@Q9T~0|@zgxbPU+|C2%U15^)4 zpCQ76W6jN`W9+$2Sg^Jkn*9KdUmx;1R=lEP z?q?^ayM5E9z}G8xIb>fnF1{&3OjjVdGz!TO(s&wFe##l@5Sz1LpmEBB8W6#So}pi` zb)mM7;R~2EpJh%u#?|?NfK5=JpEf3x3CEu zI}NNzYLJN|a^Z`@V-1eugxPKdq%$(P#U7x3S2m~Ku^pi=7{L1(k#Pj0*739L>8~A` zrllhBYXkE}Y+E-AK|2iP7W{>4c6N`f*z|TglDgp8@|(7Y%hbMovBw+yd%D)Q%08#o z=b@~HTbSxc+h2#HOZrA~F7`Sei+zzi(Sh;V$M`zaJMoHdQt+(8GAZ)m2reqPj|UDj z505!_^){X{1?Yc*dx~?XNuChwMK5ExC^)*cic-Nfz^ocdNl2o|ue`^8w{FYEW;gHc z=kMetb%>2Q!1+GivcaqX;7Z&|$!^G+2#a|e&iu@TR}?bx)&`jHG3}?WX#pxLODEZR z6^ne?k3xy}w+pN>`0BaXJTOM8+(}Ey)c#Dh`Z8?y6E)giBDds9(Sn{F@Llyh`HM}U zN$B*S()eWP9n!R7CFYO4u+^an*1dj^OSdi+8yUBP7j6@dys80q?WsQhz?wxX~qw&w@3%zc}z=FlK2TF0-d}S4e8QWQ!Qah#hoKl zn~$ten_go7E9v^Q6AvQH5fr;JhO57h{DZkr0|oL z6PHK(I^mnRr_9czPD;_&hml4auC*{pY&`l;%n(yvhYe2)cQ zOlavJ=SmE|cQXM8!rDyi>b9yL58dT1{1JmHui|(OYPJ6$qH`mgqZ*`!T)3FScX=_T{JZh3 zVt-0R6^8vj01XmWD|Nva+@ughWf-R7MKOEv117ok4zh2nq)aY1sLh_NVP+7B83BrY z=HS0c4_nCOW?D!gxr_zhm58@SxHD4%vsVflGI`#Q)wKi5gBP8_BfR;?e%u;aDUlZ| zcCpXOuku<~Z;6%NxCBe=J;T*=U=>7Aq*tBG<*v-e$T>_^#wO(Vt87~JDJKByIZmBm&BK4N~K{Lg(Dh4!mk z=@r$ZNoyVXGmi?TQpxn_ESJ>*Kbmh_aV-Wz#`wBaBTMI6KMf5&vO^UsFGFm|g&gobl>HB}!;Sp?kq7C8oV3q`!487cy31EAdK5RS_Zj|Klzv(82Y%kN-J-i3v)Zknk#nBY(T2h=%)3evD?fx^^`B$=w%M7v@abf zC(|~pidfyD$xNfQ9MEf(S+aH_1jE38ni{dO2#6C+OQpXRfTZ4Qwdj$OVKdPTYQJd% zx$0z+-61(?xlzb;Fh^%FtF0N_UEO-R(-chcsV!lWMN|1%01)QsHW?EBKjA`N)Wf)w zO;8D8Vlcvm&r_7WQB(ForEPC3H6}2mea=+QztP+@U??%xw-VuCW7PX;z+sZ?x`~y{ z8tkuybF@1Vtz=ZpbOqc8e%du@r z4H(N_^rd+O9_8nIm|@82F{iizM(}^78SIDV zNr_pJvnjVew*f`H_n#r7YPlAWqEI50x`nNsq-uB3CV}bSY)EDcY9>`#9?C@e4kBv* zS_IDr zJ%(tW!ZHjJ6mA>un$Zk>-~{F(GU(Devn$bW3;4Y^Pg_wf^Z2Iw^HSm!kwKzB@4_~3 z*Sg#Vx~bLZD0s^zLF`0npUF}L%%vH+e3(qnq8^2peO-vcZVQ_(o*I;7oyfkBkIjYT!l;uYYS#eINyhT)QM8cS)sXbu& zMEyZ%&uXco^s^Sy6Ktdz_}Ffj=Ja^SS7#nU>l#9K=ZuzpQjC_rYVx=*#CCa-4C~ae zv3s(p+7^qqKTLP}Fo1!U9U88m@;MH-pO&mfYA_&dTl($}UJ_!d6iuh31SR zCmo>ntLs?rCk_=IR6avFsO(vobQ$klw&&6-b}03_Zg-4696lR5<#|})7H{!A47B*< zCj_p0iYiF(q_m+f71#IlaN2_BvZNfh2T+cGh~+^lVHwy4Hk5IuZr&MownsS7HE8M- z`F$`&_rjc<*@X)2KK3ht#(GF}{%gdY8gtV!YTi`={~ zGFd$fR2{W3N9R{d;!DG2{tKp1_Lbni@5yQDpf{WV%FRJMH;5V<;a(kF|382u?x@Fz zK)686jC7BI{|}>lX8|Q-z!BgV?>y(8+4JW7F#O|3UQ%4OXY!m~xOB|xdfFHa%*rb% zM0O&Pc`+gxHqykPivwNtadL<~G5wgRg=^{R@ialvXEpm?Km|4QY`lav9g*iKq!hu` zYWgXJGAWMlOY_cFu!OG<{|D+;{#?4oJ-Vmq5*d_ zLl;d$K?zo#OXqV_vL^==EQY0EkUc4dt}18>T<_%z&z-W=Q1eRvIj7x*9iM z2|{f&CO}DXYl(8w!510XmFM^$a9!L`JjlI9cK^?>6}jf7=nwd^NpcB*+O51uVBK#j zD2y3>c!s19c!xwp$J>Es$TTFSnDt^7qmlV{ zpi4*_SKq8MG@4f9MRzFcksvh`4*T}CJ0PC?8T6{Gb7+o31~q(m$?N0wI{v*Kf4A&{ z?<9jpCt~QXg#$zF&DNvWPsoucWx67Go5z1UHp&uKcYj@*O~Bz7+@~Caw4zv_1Qc2R z&)rZsJqfZkpQ% z(7v2B+Na&c{9S?-{u#UL^TLei0mo$O6XZ+Q013Bf(GOEPGW3*ed@Dqx5^IG>#3Qz% z0MpY~EB74N*x<4C@Kc-N5wvf;O_~Vdx$v7=%D>hUW^ip;MbMBkMFp zyVIKsnHEv7q(CJ!h9*#qd=M_1?!)wilx3bX2vVj=&nMh|$k^QS@TZIfnX}ge9-Lgb z3vvR+$XTvQ5dFq13^&5i0N6H2K_nFfTF2atUNfC78hzHBx0If571-TsOTQPl$;ujj z@Ys4f>8?zs0KqY6d&Z`X)(HUjm(aQBQ^BG(JS+G?+X`V=S~>Qv(>%OfE7LAR-Gs*6 zD|u&^M2k+{tE1Z#e#~i(ad75d`9InFuq1<(IO|hD6X0}D9Qoi<|ccF*Xj&Cw{U-qAAGA8Nj_C0J5RoC1X zM}HCMc0rkBWA?+|o8z`cVTA-9puey(d(NE7Sm)yeY(!&}@F$P#rk0W8guU>=zixl% z4$I19vCRR|Rvfh>=PI_!@N0>|eDHqq9M?nX%cB?nqYv{T-vUAsfg5glcN{4O=L^8N zYKl1deJjputOOaox>2S{qp1?TGigEnKr14A3((hNo<#b4d#lnMaBt$EHZohe_*5i6gX{B8s8_RVdJCw z1knRpG?R=;)?}Ro5oE~N5aX`@$}K;TIULm7CNGD0$|B)D|GRZOgc|a=yysm0pnS*E z(xifTM#9$CGjYTZkfzH<_uO<{P?U+7DH_Avq|N+7U$5w>+vwv2_a|3ubZvH6fL$3_ zpK2Cr<{#bi4-b?494dmQFb88Fh7t6@X^`pnuiY@_md~n)m_$=7c47Zwwf5KQ@!Qzi z+^rLMgfyHHDQQ-{*-KAUnJ*&_QT;3>;HTT@rJ?q#VvBnyDkaX|0s@*Tlf!-oV*fY^ zQ+k>0=^kV7=&pCyt-+#1L2Osv776i$MytG0ME%4+ZJDS^{T^^_9UtGi15dy<#>+im z{(Jt_9D;@=e0v&(NH}rwpLMaG3-y1w9+s|c{3omjyV4{mg86c;BvONb!8aof?R)GE ztYpF<;GT+mYFc$u>_sT!RF@IhEdCUEq7WR=LMu6<5C^mQTSiJ=p!PQG_~@P@9zac@ z15p+TAw;5;(4Ml7!Kv0W`ahg~L<>E)3ClFynv-a(P+sL$pWj`22~*;$1v+#mXO@wc z%}KS8>;zUT@eAM@9XVAs(RZ#nJyU#_hv8_Z{?mC1COo^|$1H-!KApLYqoJ3U`%F?5 zo^IL;_^-!R0#1ghE2ud~}DY~kkvFRn8`~3DJQ<*UKNngx2Sxu{FmPkO;0pi`a z4uhw6D9o-%WAv;`eeAgI8`up}>SsP5)0MM4^*&*=tjt<(r*rQL))&+jx>!>DxA*?l z1DZa|_ePgoqH5}o>}Hb9=pV9S!4adsX>GHQf z8ifdAQW*A&{REyrRKu)#I}E2(_|FA$QfILM1yWMMpNcUELS&|w>zDk@~ zP|m5&E>YUY>j`I>Yz1(&yWCb+Y5AC-!!)_pcOxNuJ9Y}LZ^V@0_K2q=#6wP>pKdn) zjdk2gK`k_N5LZL+v)erAD~zBaTQ89)Z#f9n@lR5;dJg1ZslZC*(gBbM2PUYA%+~4p zE&F=idRFh55%$F~&3oS0e8nByVaJOT<5Yoz_yO#7;is ze-OlXh8jnWd5J&_AId&Gx&oq`n4`Z%V-DiIwS5F!_~Er#4V&JQmaq4+{#MIjr8{(x zgCk*fHbR+y?aeJg&=H*Tm*!+imlpzCCH~CUQJ$*cOY(qfWz7`H?IXJ7lQJcjC=YV6 zZCyFB1{3got=kMs1Zl+^#FL7;#D-Gpp(q8khGjZ+ET=nPLICl`OboO;wZ0bJfZQDFQpYPnXd+-Hc50U?uB6m&Cc!= zJgsfUdpIJ)h*Ef!g)&KKyX|@}9&3uoCyXuAJ>TA6Qif7A#ga<;2rgX@y?$5|+-Gc- zb|TTdALqT~05kD688M%SVqc?I&gVCT$1P_?N_~a|kRi^6_H~k5xZZI~<~P&3s5^i7~(T;-Kv)u7S54KS40FJ%?xJlI~!zLW9k^Q(5 z^g?|hsXC5j&?Q&kG=cjz4=7D$3CY0>`WpuIxEzyiFjC5%|9G@E+$#?kE~b^`Yq^LL}pOgLnH_1Ya~t%Gdmk7ZPm$&bbBM5WP*V-73t z5OwV^=ad4D@W4S1GrGUup+Bjv;kZHyIwVa4hEM(tdi649iIDqK!~|!+oi~5Lk|fP` z)&7IP?y{)Y8#b*rBq0rAf%Y#ybjGf8F>gc20Jq{Jo^io2#srr+ zLdw~UVllv=50~})dqjDV)(*EV#kz6bd>iRPfLsXl_AD-_ z9a$G+MCW4+Xc&FjK0zQdj6evWWUU?Mg&w==l?~5M3neia-msRR$+8p1Fy$QcGRH*P z)HuBP;8K_h#r=CoLeB?zxgmxj|G**b2y9!Ap*(^yUCf>->yRoK>awcMQyg-7F3mQ~ zK5LvffCNxRKwQu&q0&-1fdF|PD;vt%+63rCR^67(>M*h; z>4SDPY+SYY0ckOGTM!MMb)G(04vwW7tuT`t8C;!W?7uo8;TtcQk&gJ_pKvihhE|qi z_B(L$7xbA+HnW7v-E3&f)=*>D0Ec1^DH&L}`Z2le)zwY1K$r>>TM!G&aI}AsuV7g6hu@=29y+;Jn7%2gw(uq{z$FX~cbB*L zNcihea+;jbhI|BBY;;S9y>yqCBvPp12p9JFieoj#%%f+f-H4hN5tyq!=hx6a{xSs$ zm++D!w%m{n3E)Hw`HKF6yQyjg%z^({+dDXLV8;mZzn9yFuEC^RyJT;&iywpnz(zR~PPLUI}By_pYB9 zt6$K_suDLFd@k8rR}2^hu96>;cz7aRcxWx}sLcQl?CCBd1^~46@!T@fOflnYNO@BI zk1l0`Bck9n7d4cgpr$sdL8nKf+*pBHi*#&X)5J4li;Go>N0r=!Zs6MfhBQNB9xr*& zdVB159+=k4Bp|^}v^=@c=f6LAwX4G$wtIHj27)EA-N;**k$m`|Js#0s-HK|Ip2*4> zs^=!l99h5=KHnpH89Mt+ADc!rRUA>H~Y9ZiU170U-wvc#2w; z2*FoQg2_yer#Ph)MM-#C7$W2mN$hmRaU96~u4brs&B}YvzHD-sfxaV~(bt4jj@0k` z8D)`}J2zDV)oK~vGfw@7pNItJgvHr$bj}IYQl|j1v>i*)(&mI?4(r+AXD^eM#pH{0PmozVVh;*N9l~EjvK<0+6TbFsMoCe|<{l#+$ z+>w?~-aR{FYm?;=V!Z%RVDFo^L>8w>S%&DfuH-*a9cKs1ftD_#NP+)$35^il9nnr% z6Sy-b-dYCI(A1`g(oP?BilQ;lA0Su;Q#vH0EHL1_P-PW+=a!5dC#k2yW%3?;mdCyI z<;ckc=Ojl94?JZF#40Y7zSWi$s=<)N6^{E*R|^{I($or zCLgv(-)?kPid+}?)*J3Hu)~!mR+gG99&>Knu4z+hU4;J_G${zESn;h(a9rHp7KdZd zMbS;6f9Tdt*lIo3Y}S4O5&~rSiqUrftm^wLh9?VuBgvwEz)HW(QV1TWyBwVh3Dy!Tp0=lU^GW}+R z>zg;c{u-H99@~dQBePJ|zh>79+h|3i?wa_mD!*wwcT<~(T0uD?ckH?(Ytv){zx@ZK zxwQgu1~|~aEO!{5KE9`+%BY>{@G?&wB-WJ;#4{Eufhx z;S}+8S~;0FszxW+3a???UCM*Cyd8@-uVFheA9xD}(>ojJ>4@9;Gg?M~>7wMM-hcrK zcJZ3JQh5;@zsWMEWufqcu0mlEBNJBs2iCS?hvb3Z+TgIjUt}C3P?J$wvpz1e?~LbXi2C%(VEVV3uu_Qo_KG-EcxOWZ8B%EKKesS ze6U?Bgh<9|UZ0jO%$?ejaB(;LFOlevP1i5uyQm@IB-*L_SzlAcl4o_Ao=#aOVSY1b ze6tH_Nkmr_Cv~L@jq&iCvi{e!Y?9E5Q`K4cJ76DD0dgDp30`w5X=GN5;k$!sq7K4( z0qbsH+!lqd{F2PZ^jZ;W8_#g()(6Llk}Bree%(!t=yqKKlynXseV(uCtM!*W3LRB5!ixSXBgk@Fwtq8 z79%&ust>~G$Gobp`xn#^-^P%0;*wJ@40QBy$W®6qZ%*y;p$VTxG(@tY~sc>f54 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 GP2Q9lJ2={HRcZ9gg)wZA!?Iuik6u>L+ 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
G;$wgeJrlSAO~xc%&n|IJQ*c_-*&95F2P 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
%*H}JGM=umEot4ZLTara{f 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 literal 0 HcmV?d00001 diff --git a/vendor/bunsenite/papers/arxiv/bunsenite.tex b/vendor/bunsenite/papers/arxiv/bunsenite.tex new file mode 100644 index 0000000..8d6709f --- /dev/null +++ b/vendor/bunsenite/papers/arxiv/bunsenite.tex @@ -0,0 +1,450 @@ +% SPDX-License-Identifier: MPL-2.0 +\documentclass[11pt,a4paper]{article} + +% Packages +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage{lmodern} +\usepackage{microtype} +\usepackage{hyperref} +\usepackage{graphicx} +\usepackage{listings} +\usepackage{xcolor} +\usepackage{amsmath} +\usepackage{booktabs} +\usepackage{tikz} +\usetikzlibrary{shapes,arrows,positioning,fit,backgrounds} + +% Hyperref setup +\hypersetup{ + colorlinks=true, + linkcolor=blue, + filecolor=magenta, + urlcolor=cyan, + citecolor=blue, +} + +% Code listing style +\definecolor{codegreen}{rgb}{0,0.6,0} +\definecolor{codegray}{rgb}{0.5,0.5,0.5} +\definecolor{codepurple}{rgb}{0.58,0,0.82} +\definecolor{backcolour}{rgb}{0.95,0.95,0.92} + +\lstdefinestyle{codestyle}{ + backgroundcolor=\color{backcolour}, + commentstyle=\color{codegreen}, + keywordstyle=\color{magenta}, + numberstyle=\tiny\color{codegray}, + stringstyle=\color{codepurple}, + basicstyle=\ttfamily\footnotesize, + breakatwhitespace=false, + breaklines=true, + captionpos=b, + keepspaces=true, + numbers=left, + numbersep=5pt, + showspaces=false, + showstringspaces=false, + showtabs=false, + tabsize=2, + frame=single, +} +\lstset{style=codestyle} + +% Title and authors +\title{Bunsenite: A Multi-Language FFI Architecture for\\Configuration Language Parsing} + +\author{ + Campaign for Cooler Coding and Programming\\ + \texttt{hyperpolymath}\\ + \href{https://github.com/hyperpolymath/bunsenite}{github.com/hyperpolymath/bunsenite} +} + +\date{\today} + +\begin{document} + +\maketitle + +\begin{abstract} +Configuration file management remains a critical challenge in modern software development, with applications frequently requiring configuration access from multiple programming languages within the same system. We present Bunsenite, a configuration file parser for the Nickel language that provides stable, multi-language bindings through a novel three-layer architecture: a Rust core for memory-safe parsing, a Zig intermediate layer providing a stable C ABI, and language-specific bindings for Deno (JavaScript), ReScript, and WebAssembly. This architecture isolates consumers from Rust's unstable ABI while preserving memory safety guarantees. We demonstrate that this approach enables type-safe configuration parsing across language boundaries without sacrificing performance or safety. Bunsenite achieves RSR Bronze tier compliance and operates fully offline, making it suitable for air-gapped and security-sensitive environments. +\end{abstract} + +\section{Introduction} + +Modern software systems increasingly operate as polyglot environments, with different components written in different programming languages chosen for their specific strengths. A web application might use Rust for performance-critical backend services, JavaScript for frontend interactivity, and ReScript for type-safe UI components. These heterogeneous systems share a common need: configuration management. + +Configuration languages have evolved from simple key-value formats (INI files) through structured data formats (JSON, YAML, TOML) to programmable configuration languages that support computation, type checking, and code reuse. Nickel~\cite{nickel} represents this latest generation, offering a gradually-typed, functional configuration language with contracts for validation. + +However, providing configuration parsing capabilities across multiple programming languages presents significant engineering challenges: + +\begin{enumerate} + \item \textbf{ABI Stability}: Rust, the natural choice for implementing a Nickel parser due to nickel-lang-core, does not guarantee a stable ABI between compiler versions. + \item \textbf{Memory Safety}: Foreign function interfaces (FFI) traditionally require unsafe code, creating potential for memory corruption. + \item \textbf{Type Safety}: Configuration values must be correctly represented in each target language's type system. + \item \textbf{Deployment Complexity}: Native libraries must be compiled for each target platform and architecture. +\end{enumerate} + +We present Bunsenite, a Nickel configuration parser that addresses these challenges through a three-layer architecture (Figure~\ref{fig:architecture}). Our contributions include: + +\begin{itemize} + \item A stable FFI design using Zig as an intermediate layer to isolate consumers from Rust ABI changes + \item Type-safe bindings for Deno (via \texttt{Deno.dlopen}), ReScript (via C FFI), and WebAssembly + \item An offline-first design with zero network dependencies + \item Compliance with the Rhodium Standard Repositories (RSR) framework at Bronze tier +\end{itemize} + +\section{Background} + +\subsection{The Nickel Configuration Language} + +Nickel is a configuration language designed to generate static configuration files with programmability, typing, and validation~\cite{nickel}. Unlike JSON or YAML, Nickel supports: + +\begin{itemize} + \item \textbf{Functions and Merging}: Configuration can be composed from reusable modules + \item \textbf{Gradual Typing}: Optional type annotations with inference + \item \textbf{Contracts}: Runtime validation of configuration values + \item \textbf{Evaluation}: Expressions are evaluated to produce final JSON/YAML/TOML output +\end{itemize} + +\begin{lstlisting}[language=ML,caption={Example Nickel configuration}] +{ + server = { + host = "localhost", + port = 8080, + max_connections = 100 * 10, # Computation + }, + + database | { host : String, port : Number } = { + host = "db.internal", + port = 5432, + }, +} +\end{lstlisting} + +The reference implementation, nickel-lang-core, is written in Rust, making Rust the natural choice for building Nickel tooling. + +\subsection{The FFI Challenge} + +Rust provides excellent memory safety guarantees but does not maintain a stable ABI. The \texttt{repr(Rust)} layout can change between compiler versions, meaning that a shared library compiled with Rust 1.70 may not be compatible with code compiled with Rust 1.75. + +The traditional solution is to use \texttt{extern "C"} functions with C-compatible types, but this requires careful manual memory management at the FFI boundary---exactly the kind of unsafe code that Rust was designed to avoid. + +\subsection{The Zig Advantage} + +Zig provides a compelling solution to the ABI stability problem. As a systems programming language with: + +\begin{itemize} + \item First-class C ABI compatibility + \item No hidden control flow or allocations + \item Compile-time execution for metaprogramming + \item Ability to link with both C and Rust code +\end{itemize} + +Zig can serve as a stable interface layer between Rust and consumer languages, absorbing ABI changes while presenting a consistent C interface. + +\section{Architecture} + +Bunsenite employs a three-layer architecture designed to maximize safety while providing stable multi-language access (Figure~\ref{fig:architecture}). + +\begin{figure}[htbp] +\centering +\begin{tikzpicture}[ + node distance=1.5cm, + box/.style={rectangle, draw, minimum width=2.5cm, minimum height=0.8cm, align=center}, + layer/.style={rectangle, draw, dashed, inner sep=0.3cm}, +] + +% Consumer layer +\node[box] (deno) {Deno}; +\node[box, right=0.5cm of deno] (rescript) {ReScript}; +\node[box, right=0.5cm of rescript] (wasm) {Browser\\(WASM)}; + +% Zig layer +\node[box, below=1cm of rescript] (zig) {Zig FFI Layer\\(Stable C ABI)}; + +% Rust layer +\node[box, below=1cm of zig] (rust) {Rust Core\\nickel-lang-core 0.9.1}; + +% Arrows +\draw[->] (deno) -- (zig); +\draw[->] (rescript) -- (zig); +\draw[->] (wasm) -- (rust); +\draw[->] (zig) -- (rust); + +% Layer labels +\node[left=0.5cm of deno, rotate=90, anchor=south] {\small Consumers}; +\node[left=0.5cm of zig, rotate=90, anchor=south] {\small FFI}; +\node[left=0.5cm of rust, rotate=90, anchor=south] {\small Core}; + +\end{tikzpicture} +\caption{Bunsenite three-layer architecture. Deno and ReScript access the Rust core through a Zig-provided stable C ABI. WebAssembly bindings connect directly via wasm-bindgen.} +\label{fig:architecture} +\end{figure} + +\subsection{Layer 1: Rust Core} + +The Rust core (\texttt{src/lib.rs}, \texttt{src/loader.rs}) provides the fundamental Nickel parsing and evaluation functionality: + +\begin{lstlisting}[language=Rust,caption={Core NickelLoader implementation}] +pub struct NickelLoader { + verbose: bool, +} + +impl NickelLoader { + pub fn parse_string(&self, source: &str, name: &str) + -> Result + { + let mut program: Program = + Program::new_from_source( + Cursor::new(source.as_bytes()), + name, + std::io::sink(), + )?; + + let eval_result = program.eval_full()?; + serde_json::to_value(&eval_result) + } +} +\end{lstlisting} + +The core enforces memory safety through Rust's ownership model. The \texttt{\#![deny(unsafe\_code)]} attribute ensures no unsafe blocks exist in the core library, with the single exception of the FFI boundary module. + +\subsection{Layer 2: Zig FFI} + +The Zig layer (\texttt{zig/bunsenite.zig}) provides a stable C ABI interface: + +\begin{lstlisting}[language=C,caption={Zig FFI exports (C ABI)}] +// Import Rust FFI functions +extern fn bunsenite_parse( + source: [*:0]const u8, + name: [*:0]const u8 +) callconv(.C) ?[*:0]u8; + +// Re-export with stable names +pub export fn parse_nickel( + source: [*:0]const u8, + name: [*:0]const u8 +) callconv(.C) ?[*:0]u8 { + return bunsenite_parse(source, name); +} +\end{lstlisting} + +This indirection provides several benefits: + +\begin{enumerate} + \item \textbf{ABI Isolation}: Consumer bindings depend on Zig's stable C ABI, not Rust's unstable ABI + \item \textbf{Symbol Stability}: Function names and signatures remain constant across Rust compiler updates + \item \textbf{Type Simplification}: Complex Rust types are converted to C-compatible primitives +\end{enumerate} + +\subsection{Layer 3: Language Bindings} + +\subsubsection{Deno Bindings} + +Deno bindings use \texttt{Deno.dlopen} for native FFI: + +\begin{lstlisting}[language=JavaScript,caption={Deno FFI binding}] +const symbols = { + parse_nickel: { + parameters: ["pointer", "pointer"], + result: "pointer", + }, + free_string: { + parameters: ["pointer"], + result: "void", + }, +}; + +const lib = Deno.dlopen(libPath, symbols); + +export function parseNickel(source: string, name: string) { + const resultPtr = lib.symbols.parse_nickel( + toCString(source), + toCString(name), + ); + try { + return JSON.parse(fromCString(resultPtr)); + } finally { + lib.symbols.free_string(resultPtr); + } +} +\end{lstlisting} + +\subsubsection{ReScript Bindings} + +ReScript bindings provide type-safe access with algebraic error handling: + +\begin{lstlisting}[language=ML,caption={ReScript binding with Result type}] +type error = + | ParseError(string) + | ValidationError(string) + | InvalidInput(string) + +let parseNickel = (source: string, name: string) + : result => { + let result = parseNickelRaw(source, name) + switch Js.Nullable.toOption(result) { + | Some(jsonString) => Ok(Js.Json.parseExn(jsonString)) + | None => Error(ParseError("Failed to parse: " ++ name)) + } +} +\end{lstlisting} + +\subsubsection{WebAssembly Bindings} + +For browser environments, Bunsenite compiles directly to WebAssembly using \texttt{wasm-bindgen}, bypassing the Zig layer since WASM provides its own stable binary interface. + +\section{Safety Guarantees} + +Bunsenite provides multiple layers of safety guarantees: + +\subsection{Memory Safety} + +\begin{itemize} + \item \textbf{Rust Core}: Ownership and borrowing prevent use-after-free, double-free, and buffer overflows + \item \textbf{FFI Boundary}: All FFI functions follow strict ownership protocols---callers receive owned pointers and must free them exactly once + \item \textbf{Zig Layer}: No hidden allocations; all memory flows explicitly through the defined API +\end{itemize} + +\subsection{Type Safety} + +\begin{itemize} + \item \textbf{Compile-time}: Rust's type system catches type errors before runtime + \item \textbf{Binding-level}: ReScript's type system ensures correct usage in consuming code + \item \textbf{Runtime}: Nickel's contract system validates configuration values +\end{itemize} + +\subsection{Offline Operation} + +Bunsenite has zero network dependencies in production code. This ``offline-first'' design ensures: + +\begin{itemize} + \item Operation in air-gapped environments + \item No supply chain attacks via runtime network requests + \item Deterministic behavior unaffected by network conditions +\end{itemize} + +\section{Compliance and Standards} + +Bunsenite adheres to the Rhodium Standard Repositories (RSR) framework at Bronze tier and the Trust Perimeter Classification Framework (TPCF) at Perimeter 3 (Community Sandbox). + +\subsection{RSR Bronze Requirements} + +\begin{table}[htbp] +\centering +\begin{tabular}{lll} +\toprule +\textbf{Requirement} & \textbf{Implementation} & \textbf{Verification} \\ +\midrule +Type Safety & Rust compiler & Compile-time \\ +Memory Safety & Ownership model & \texttt{\#![deny(unsafe\_code)]} \\ +Offline-First & No network deps & Cargo audit \\ +\bottomrule +\end{tabular} +\caption{RSR Bronze compliance matrix} +\label{tab:rsr} +\end{table} + +\subsection{Security Considerations} + +The library includes several security measures: + +\begin{itemize} + \item SHA-pinned dependencies in CI/CD workflows + \item SPDX license headers on all source files + \item Security policy with vulnerability reporting guidelines + \item Automated security scanning via CodeQL and Dependabot +\end{itemize} + +\section{Performance} + +While a comprehensive performance evaluation is beyond the scope of this paper, preliminary benchmarks indicate: + +\begin{itemize} + \item \textbf{Parse latency}: Sub-millisecond for typical configuration files (<1KB) + \item \textbf{FFI overhead}: Negligible (single function call indirection) + \item \textbf{Memory usage}: Linear with configuration size + \item \textbf{WASM size}: Optimized build produces ~2MB module +\end{itemize} + +The three-layer architecture introduces minimal overhead because: + +\begin{enumerate} + \item Zig's FFI wrapper compiles to direct function calls + \item JSON serialization happens once at the Rust layer + \item Consumer bindings perform no additional parsing +\end{enumerate} + +\section{Related Work} + +\subsection{Configuration Languages} + +Dhall~\cite{dhall} provides a programmable configuration language with strong normalization guarantees. CUE~\cite{cue} combines data validation with configuration. Unlike these, Nickel emphasizes gradual typing and seamless JSON interoperability. + +\subsection{FFI Approaches} + +Traditional approaches to multi-language FFI include: + +\begin{itemize} + \item \textbf{SWIG}: Generates bindings but requires complex configuration + \item \textbf{Protocol Buffers}: Adds serialization overhead for simple cases + \item \textbf{gRPC}: Introduces network complexity for local operations +\end{itemize} + +Our Zig-based approach provides the simplicity of C FFI with the safety guarantees of a modern systems language. + +\subsection{Rust FFI Libraries} + +Libraries like \texttt{cbindgen} and \texttt{safer-ffi} help generate C headers from Rust code. Bunsenite's approach differs by introducing an explicit Zig layer for ABI stability, rather than relying on C header generation alone. + +\section{Future Work} + +Several extensions are planned: + +\begin{itemize} + \item \textbf{Language Server Protocol}: Integration with editors via tower-lsp + \item \textbf{Watch Mode Optimization}: Incremental re-parsing for file watchers + \item \textbf{Additional Bindings}: Python, Ruby, and JVM languages + \item \textbf{Schema Generation}: Automatic JSON Schema from Nickel contracts +\end{itemize} + +\section{Conclusion} + +Bunsenite demonstrates that multi-language configuration parsing can be achieved without sacrificing memory safety or type guarantees. The three-layer architecture---Rust core, Zig FFI, language bindings---provides a template for building safe, stable, multi-language libraries. + +The key insight is that Zig's C ABI compatibility, combined with Rust's memory safety, creates a sweet spot for FFI design: consumers get a stable interface while the implementation benefits from modern safety guarantees. + +Bunsenite is open source under the PMPL-1.0 and Palimpsest-0.8 dual license, available at \url{https://github.com/hyperpolymath/bunsenite}. + +\section*{Acknowledgments} + +We thank the Nickel language team for nickel-lang-core, the Zig community for their work on C interoperability, and the Deno team for the excellent FFI API. + +\bibliographystyle{plain} +\begin{thebibliography}{9} + +\bibitem{nickel} +Tweag. \textit{Nickel: Better configuration for less}. \url{https://nickel-lang.org/}, 2024. + +\bibitem{dhall} +Gabriel Gonzalez. \textit{Dhall: A programmable configuration language}. \url{https://dhall-lang.org/}, 2024. + +\bibitem{cue} +Marcel van Lohuizen. \textit{CUE: Configure Unify Execute}. \url{https://cuelang.org/}, 2024. + +\bibitem{rust-abi} +Rust Language Team. \textit{The Rust Reference: Type Layout}. \url{https://doc.rust-lang.org/reference/type-layout.html}, 2024. + +\bibitem{zig-ffi} +Andrew Kelley et al. \textit{Zig Language Reference: C Interoperability}. \url{https://ziglang.org/documentation/master/}, 2024. + +\bibitem{wasm-bindgen} +The Rust and WebAssembly Working Group. \textit{wasm-bindgen Guide}. \url{https://rustwasm.github.io/wasm-bindgen/}, 2024. + +\bibitem{deno-ffi} +Deno Land Inc. \textit{Deno FFI}. \url{https://deno.land/manual/runtime/ffi_api}, 2024. + +\end{thebibliography} + +\end{document} diff --git a/vendor/bunsenite/selur-compose.toml b/vendor/bunsenite/selur-compose.toml new file mode 100644 index 0000000..5aae13a --- /dev/null +++ b/vendor/bunsenite/selur-compose.toml @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Stapeln service definition for bunsenite +# +# Usage: +# podman-compose -f selur-compose.toml up -d +# just stack-up + +[project] +name = "bunsenite" + +[services.app] +build = { context = ".", dockerfile = "Containerfile" } +restart = "unless-stopped" +networks = ["default"] +healthcheck = { test = "exit 0", interval = "30s", timeout = "5s", retries = 3 } diff --git a/vendor/bunsenite/setup-dev-env.k9.ncl b/vendor/bunsenite/setup-dev-env.k9.ncl new file mode 100644 index 0000000..3c9d087 --- /dev/null +++ b/vendor/bunsenite/setup-dev-env.k9.ncl @@ -0,0 +1,201 @@ +K9! +# SPDX-License-Identifier: MPL-2.0 +# Bunsenite Development Environment Setup +# +# This K9 component automates the setup of a complete Bunsenite development +# environment, including Rust toolchain, dependencies, pre-commit hooks, and +# test infrastructure. + +leash = 'Hunt + +pedigree = { + schema_version = "1.0.0", + component_type = "dev-env-setup", + author = "Jonathan D.A. Jewell ", + description = "Automated development environment setup for Bunsenite", + created = "2026-01-30", + k9_spec_version = "1.0.0", +} + +# Configuration with Nickel contracts +config = { + rust_version | String = "stable", + install_nightly | Bool = true, # For WASM builds + setup_git_hooks | Bool = true, + install_cargo_tools | Bool = true, + run_initial_tests | Bool = true, + + # Required Cargo tools for Bunsenite development + cargo_tools | Array String = [ + "cargo-audit", # Security auditing + "cargo-outdated", # Dependency updates + "cargo-deny", # License/dependency policy + "cargo-watch", # Auto-rebuild on changes + "cargo-expand", # Macro expansion debugging + "wasm-pack", # WASM builds + ], + + # Target platforms to install + targets | Array String = [ + "wasm32-unknown-unknown", # Browser WASM + "wasm32-wasi", # WASI WASM + ], + + # Pre-commit hooks to install + git_hooks | { _ : String } = { + "pre-commit" = "#!/bin/bash\nset -e\ncargo fmt --check\ncargo clippy -- -D warnings\n", + "pre-push" = "#!/bin/bash\nset -e\ncargo test\ncargo audit\n", + }, +} + +# Just recipes for setup tasks +recipes = { + default = { + recipe = "setup-all", + description = "Complete development environment setup", + }, + + "check-rust" = { + description = "Verify Rust toolchain is installed", + commands = [ + "command -v rustc >/dev/null 2>&1 || { echo 'Error: Rust not found. Install from https://rustup.rs/'; exit 1; }", + "rustc --version", + "cargo --version", + ], + }, + + "install-toolchain" = { + description = "Install Rust toolchains and targets", + dependencies = ["check-rust"], + commands = [ + "rustup install %{config.rust_version}", + "rustup default %{config.rust_version}", + ] @ ( + if config.install_nightly then + ["rustup install nightly", "rustup component add rust-src --toolchain nightly"] + else + [] + ) @ ( + config.targets + |> std.array.map (fun target => "rustup target add %{target}") + ), + }, + + "install-cargo-tools" = { + description = "Install Cargo development tools", + dependencies = ["install-toolchain"], + skip = !config.install_cargo_tools, + commands = config.cargo_tools + |> std.array.map (fun tool => "cargo install %{tool} || echo 'Warning: %{tool} install failed'"), + }, + + "setup-git-hooks" = { + description = "Install Git pre-commit and pre-push hooks", + skip = !config.setup_git_hooks, + commands = + config.git_hooks + |> std.record.to_array + |> std.array.map (fun entry => + let name = std.string.from entry.field in + let content = entry.value in + [ + "mkdir -p .git/hooks", + "cat > .git/hooks/%{name} <<'HOOK_EOF'\n%{content}\nHOOK_EOF", + "chmod +x .git/hooks/%{name}", + "echo 'Installed %{name} hook'", + ] + ) + |> std.array.flatten, + }, + + "build-debug" = { + description = "Build Bunsenite in debug mode", + dependencies = ["install-toolchain"], + commands = [ + "cargo build", + "echo 'Debug build complete: target/debug/bunsenite'", + ], + }, + + "build-release" = { + description = "Build Bunsenite in release mode with optimizations", + dependencies = ["install-toolchain"], + commands = [ + "cargo build --release", + "ls -lh target/release/bunsenite", + "echo 'Release build complete'", + ], + }, + + "build-wasm" = { + description = "Build WASM bindings for browser", + dependencies = ["install-cargo-tools"], + commands = [ + "cd wasm && wasm-pack build --target web", + "echo 'WASM build complete: wasm/pkg/'", + ], + }, + + "run-tests" = { + description = "Run full test suite", + skip = !config.run_initial_tests, + commands = [ + "cargo test --all-features", + "cargo test --release --all-features", + ], + }, + + "security-audit" = { + description = "Run security and license audits", + dependencies = ["install-cargo-tools"], + commands = [ + "cargo audit", + "cargo deny check", + ], + }, + + "setup-all" = { + description = "Complete development environment setup (default)", + dependencies = [ + "check-rust", + "install-toolchain", + "install-cargo-tools", + "setup-git-hooks", + "build-debug", + "run-tests", + ], + commands = [ + "echo ''", + "echo '╔══════════════════════════════════════════════════════════╗'", + "echo '║ ✅ Bunsenite Development Environment Setup Complete! ║'", + "echo '╚══════════════════════════════════════════════════════════╝'", + "echo ''", + "echo 'Next steps:'", + "echo ' 1. Build WASM bindings: just build-wasm'", + "echo ' 2. Run security audit: just security-audit'", + "echo ' 3. Start development: cargo watch -x check -x test'", + "echo ''", + ], + }, +} + +# Validation contracts +validation = { + # Ensure Rust version is valid + rust_version_valid = + config.rust_version == "stable" + || config.rust_version == "nightly" + || config.rust_version == "beta" + | doc "Rust version must be stable, nightly, or beta", + + # Ensure at least one cargo tool is selected + has_cargo_tools = + !config.install_cargo_tools + || std.array.length config.cargo_tools > 0 + | doc "If install_cargo_tools is true, must specify at least one tool", + + # Ensure WASM target is installed if building WASM + wasm_target_included = + std.array.any (fun t => t == "wasm32-unknown-unknown") config.targets + | doc "wasm32-unknown-unknown target required for WASM builds", +} diff --git a/vendor/bunsenite/setup.sh b/vendor/bunsenite/setup.sh new file mode 100644 index 0000000..f63875d --- /dev/null +++ b/vendor/bunsenite/setup.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Bunsenite — Universal Setup Script +# Detects platform and shell, installs just, then hands off to Justfile. + +set -euo pipefail + +echo "═══════════════════════════════════════════════════" +echo " Bunsenite — Setup" +echo "═══════════════════════════════════════════════════" +echo "" + +# Platform detection +OS="$(uname -s)" +ARCH="$(uname -m)" +echo "Platform: $OS $ARCH" + +# Shell detection +CURRENT_SHELL="$(basename "$SHELL" 2>/dev/null || echo "unknown")" +echo "Shell: $CURRENT_SHELL" +echo "" + +# Check for just +if ! command -v just >/dev/null 2>&1; then + echo "just (command runner) is required but not installed." + echo "" + case "$OS" in + Linux) + if command -v cargo >/dev/null 2>&1; then + echo "Installing just via cargo..." + cargo install just + elif command -v brew >/dev/null 2>&1; then + echo "Installing just via Homebrew..." + brew install just + else + echo "Install just from: https://just.systems/man/en/installation.html" + exit 1 + fi + ;; + Darwin) + if command -v brew >/dev/null 2>&1; then + echo "Installing just via Homebrew..." + brew install just + else + echo "Install Homebrew first: https://brew.sh" + echo "Then: brew install just" + exit 1 + fi + ;; + *) + echo "Install just from: https://just.systems/man/en/installation.html" + exit 1 + ;; + esac + echo "" +fi + +echo "Running diagnostics..." +just doctor + +echo "" +echo "Setup complete. Run 'just help-me' for common workflows." diff --git a/vendor/bunsenite/src/error.rs b/vendor/bunsenite/src/error.rs new file mode 100644 index 0000000..5b551a2 --- /dev/null +++ b/vendor/bunsenite/src/error.rs @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Error types for Bunsenite +//! +//! This module provides comprehensive error handling for all Bunsenite operations. +//! Errors are designed to be informative and actionable for end users. +//! Uses miette for pretty error output with source context. + +// Allow unused_assignments to prevent false positive from cargo-tarpaulin coverage instrumentation +#![allow(unused_assignments)] + +use miette::{Diagnostic, SourceSpan}; + +/// Result type alias for Bunsenite operations +pub type Result = std::result::Result; + +/// Bunsenite error types with miette integration for rich diagnostics +#[derive(Debug, thiserror::Error, Diagnostic)] +pub enum Error { + /// Nickel parsing error + #[error("Failed to parse Nickel file '{file}'")] + #[diagnostic( + code(bunsenite::parse_error), + help("Check your Nickel syntax. Run 'nickel check' for detailed diagnostics.") + )] + ParseError { + /// Name of the file that failed to parse + file: String, + /// Error message from the parser + message: String, + /// Source code that caused the error + #[source_code] + src: Option, + /// Location of the error in source + #[label("error here")] + span: Option, + }, + + /// Nickel evaluation error + #[error("Failed to evaluate Nickel program '{file}'")] + #[diagnostic( + code(bunsenite::eval_error), + help("Ensure all variables are defined and types match.") + )] + EvaluationError { + /// Name of the file that failed to evaluate + file: String, + /// Error message from the evaluator + message: String, + /// Source code + #[source_code] + src: Option, + /// Location of the error + #[label("evaluation failed here")] + span: Option, + }, + + /// Serialization error (converting Nickel values to JSON) + #[error("Failed to serialize result: {0}")] + #[diagnostic( + code(bunsenite::serialization_error), + help("Ensure the Nickel program produces valid JSON-serializable values.") + )] + SerializationError(String), + + /// File I/O error + #[error("File I/O error: {0}")] + #[diagnostic(code(bunsenite::io_error), help("Check file permissions and path."))] + IoError(#[from] std::io::Error), + + /// Invalid input + #[error("Invalid input: {0}")] + #[diagnostic( + code(bunsenite::invalid_input), + help("Check the input format and try again.") + )] + InvalidInput(String), + + /// Watch error + #[error("Watch error: {0}")] + #[diagnostic( + code(bunsenite::watch_error), + help("Check that the file path is valid and accessible.") + )] + WatchError(String), + + /// Internal error (should not happen in normal operation) + #[error("Internal error: {0}")] + #[diagnostic( + code(bunsenite::internal_error), + url("https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite/-/issues"), + help("This is a bug. Please report it.") + )] + Internal(String), +} + +impl Error { + /// Create a new parse error + pub fn parse_error(file: impl Into, message: impl Into) -> Self { + Error::ParseError { + file: file.into(), + message: message.into(), + src: None, + span: None, + } + } + + /// Create a new parse error with source context + pub fn parse_error_with_source( + file: impl Into, + message: impl Into, + src: String, + offset: usize, + length: usize, + ) -> Self { + Error::ParseError { + file: file.into(), + message: message.into(), + src: Some(src), + span: Some(SourceSpan::new(offset.into(), length)), + } + } + + /// Create a new evaluation error + pub fn evaluation_error(file: impl Into, message: impl Into) -> Self { + Error::EvaluationError { + file: file.into(), + message: message.into(), + src: None, + span: None, + } + } + + /// Create a new evaluation error with source context + pub fn evaluation_error_with_source( + file: impl Into, + message: impl Into, + src: String, + offset: usize, + length: usize, + ) -> Self { + Error::EvaluationError { + file: file.into(), + message: message.into(), + src: Some(src), + span: Some(SourceSpan::new(offset.into(), length)), + } + } + + /// Create a new serialization error + pub fn serialization_error(message: impl Into) -> Self { + Error::SerializationError(message.into()) + } + + /// Create a new invalid input error + pub fn invalid_input(message: impl Into) -> Self { + Error::InvalidInput(message.into()) + } + + /// Create a new watch error + pub fn watch_error(message: impl Into) -> Self { + Error::WatchError(message.into()) + } + + /// Create a new internal error + pub fn internal(message: impl Into) -> Self { + Error::Internal(message.into()) + } + + /// Check if this error is recoverable + /// + /// Recoverable errors are those that the user can fix by changing input. + /// Non-recoverable errors indicate bugs or system issues. + pub fn is_recoverable(&self) -> bool { + matches!( + self, + Error::ParseError { .. } + | Error::InvalidInput(_) + | Error::EvaluationError { .. } + | Error::WatchError(_) + ) + } + + /// Get the error message (for compatibility) + pub fn message(&self) -> &str { + match self { + Error::ParseError { message, .. } => message, + Error::EvaluationError { message, .. } => message, + Error::SerializationError(msg) => msg, + Error::IoError(_) => "I/O error", + Error::InvalidInput(msg) => msg, + Error::WatchError(msg) => msg, + Error::Internal(msg) => msg, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_creation() { + let err = Error::parse_error("test.ncl", "syntax error"); + assert!(err.is_recoverable()); + } + + #[test] + fn test_error_display() { + let err = Error::parse_error("config.ncl", "unexpected token"); + let msg = format!("{}", err); + assert!(msg.contains("config.ncl")); + } + + #[test] + fn test_recoverable_errors() { + assert!(Error::parse_error("test", "msg").is_recoverable()); + assert!(Error::invalid_input("msg").is_recoverable()); + assert!(Error::watch_error("msg").is_recoverable()); + assert!(!Error::internal("msg").is_recoverable()); + } + + #[test] + fn test_error_with_source_context() { + let err = Error::parse_error_with_source( + "test.ncl", + "unexpected token", + "let x = @invalid".to_string(), + 8, + 8, + ); + assert!(err.is_recoverable()); + assert_eq!(err.message(), "unexpected token"); + } + + #[test] + fn test_error_message() { + assert_eq!(Error::parse_error("f", "msg").message(), "msg"); + assert_eq!(Error::evaluation_error("f", "eval").message(), "eval"); + assert_eq!(Error::serialization_error("ser").message(), "ser"); + assert_eq!(Error::invalid_input("inp").message(), "inp"); + assert_eq!(Error::watch_error("watch").message(), "watch"); + assert_eq!(Error::internal("int").message(), "int"); + } +} diff --git a/vendor/bunsenite/src/ffi.rs b/vendor/bunsenite/src/ffi.rs new file mode 100644 index 0000000..7667a35 --- /dev/null +++ b/vendor/bunsenite/src/ffi.rs @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! C FFI exports for Bunsenite +//! +//! This module provides C-compatible function exports that can be called +//! from Zig, which then re-exports them with stable ABI guarantees. +//! +//! # FFI Architecture +//! +//! ```text +//! ┌──────────────┐ ┌────────────┐ ┌────────────┐ +//! │ Consumers │ │ Zig FFI │ │ Rust Core │ +//! │ (Deno, etc.) │ ───> │ (ABI Safe) │ ───> │ (bunsenite)│ +//! └──────────────┘ └────────────┘ └────────────┘ +//! ``` +//! +//! # Safety +//! +//! These functions use `unsafe` for FFI boundary crossing. The Zig layer +//! provides additional safety guarantees and stable ABI. +//! +//! Key Safety Invariants: +//! 1. All pointers must be checked for null. +//! 2. Strings allocated by Rust must be freed by Rust (`bunsenite_free_string`). +//! 3. Static strings must never be freed. + +use crate::NickelLoader; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Parse a Nickel configuration string and return JSON +/// +/// # Safety +/// +/// - `source` must be a valid null-terminated C string +/// - `name` must be a valid null-terminated C string +/// - The returned pointer must be freed with `bunsenite_free_string` +#[no_mangle] +pub unsafe extern "C" fn bunsenite_parse( + source: *const c_char, + name: *const c_char, +) -> *mut c_char { + if source.is_null() || name.is_null() { + return std::ptr::null_mut(); + } + + let source_str = match CStr::from_ptr(source).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + let name_str = match CStr::from_ptr(name).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + let loader = NickelLoader::new(); + match loader.parse_string(source_str, name_str) { + Ok(value) => { + let json_string = match serde_json::to_string(&value) { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + match CString::new(json_string) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } + } + Err(_) => std::ptr::null_mut(), + } +} + +/// Validate a Nickel configuration without evaluating +/// +/// # Safety +/// +/// - `source` must be a valid null-terminated C string +/// - `name` must be a valid null-terminated C string +/// +/// # Returns +/// +/// - 0 on success (valid configuration) +/// - 1 on validation error +/// - -1 on invalid input (null pointers, invalid UTF-8) +#[no_mangle] +pub unsafe extern "C" fn bunsenite_validate(source: *const c_char, name: *const c_char) -> i32 { + if source.is_null() || name.is_null() { + return -1; + } + + let source_str = match CStr::from_ptr(source).to_str() { + Ok(s) => s, + Err(_) => return -1, + }; + + let name_str = match CStr::from_ptr(name).to_str() { + Ok(s) => s, + Err(_) => return -1, + }; + + let loader = NickelLoader::new(); + match loader.validate(source_str, name_str) { + Ok(()) => 0, + Err(_) => 1, + } +} + +/// Free a string allocated by bunsenite_parse +/// +/// # Safety +/// +/// - `ptr` must be a pointer returned by `bunsenite_parse` +/// - `ptr` must not have been freed before +/// - `ptr` may be null (no-op) +#[no_mangle] +pub unsafe extern "C" fn bunsenite_free_string(ptr: *mut c_char) { + if !ptr.is_null() { + drop(CString::from_raw(ptr)); + } +} + +/// Get the library version +/// +/// # Safety +/// +/// The returned pointer is static and must NOT be freed. +#[no_mangle] +pub extern "C" fn bunsenite_version() -> *const c_char { + static VERSION: &[u8] = concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes(); + VERSION.as_ptr() as *const c_char +} + +/// Get the RSR compliance tier +/// +/// # Safety +/// +/// The returned pointer is static and must NOT be freed. +#[no_mangle] +pub extern "C" fn bunsenite_rsr_tier() -> *const c_char { + static TIER: &[u8] = b"bronze\0"; + TIER.as_ptr() as *const c_char +} + +/// Get the TPCF perimeter number +#[no_mangle] +pub extern "C" fn bunsenite_tpcf_perimeter() -> u8 { + 3 // Community Sandbox +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::CString; + + #[test] + fn test_ffi_parse_valid() { + let source = CString::new("{ foo = 42 }").unwrap(); + let name = CString::new("test.ncl").unwrap(); + + unsafe { + let result = bunsenite_parse(source.as_ptr(), name.as_ptr()); + assert!(!result.is_null()); + + let result_str = CStr::from_ptr(result).to_str().unwrap(); + assert!(result_str.contains("foo")); + assert!(result_str.contains("42")); + + bunsenite_free_string(result); + } + } + + #[test] + fn test_ffi_parse_invalid() { + let source = CString::new("{ invalid = }").unwrap(); + let name = CString::new("bad.ncl").unwrap(); + + unsafe { + let result = bunsenite_parse(source.as_ptr(), name.as_ptr()); + assert!(result.is_null()); + } + } + + #[test] + fn test_ffi_validate_valid() { + let source = CString::new("{ foo = 42 }").unwrap(); + let name = CString::new("test.ncl").unwrap(); + + unsafe { + let result = bunsenite_validate(source.as_ptr(), name.as_ptr()); + assert_eq!(result, 0); + } + } + + #[test] + fn test_ffi_validate_invalid() { + let source = CString::new("{ foo = }").unwrap(); + let name = CString::new("bad.ncl").unwrap(); + + unsafe { + let result = bunsenite_validate(source.as_ptr(), name.as_ptr()); + assert_eq!(result, 1); + } + } + + #[test] + fn test_ffi_null_input() { + unsafe { + assert!(bunsenite_parse(std::ptr::null(), std::ptr::null()).is_null()); + assert_eq!(bunsenite_validate(std::ptr::null(), std::ptr::null()), -1); + } + } + + #[test] + fn test_ffi_version() { + let version = bunsenite_version(); + assert!(!version.is_null()); + unsafe { + let version_str = CStr::from_ptr(version).to_str().unwrap(); + assert!(!version_str.is_empty()); + } + } + + #[test] + fn test_ffi_rsr_tier() { + let tier = bunsenite_rsr_tier(); + assert!(!tier.is_null()); + unsafe { + let tier_str = CStr::from_ptr(tier).to_str().unwrap(); + assert_eq!(tier_str, "bronze"); + } + } + + #[test] + fn test_ffi_tpcf_perimeter() { + assert_eq!(bunsenite_tpcf_perimeter(), 3); + } +} diff --git a/vendor/bunsenite/src/lib.rs b/vendor/bunsenite/src/lib.rs new file mode 100644 index 0000000..3cdee48 --- /dev/null +++ b/vendor/bunsenite/src/lib.rs @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Bunsenite: Nickel configuration file parser with multi-language FFI bindings +//! +//! Bunsenite provides a Rust core library with a stable C ABI layer (via Zig) +//! that enables bindings for Deno (JavaScript/TypeScript), Rescript, and +//! WebAssembly for browser and universal use. +//! +//! # Features +//! +//! - **Type Safety**: Compile-time guarantees via Rust's type system +//! - **Memory Safety**: Rust ownership model, zero `unsafe` blocks +//! - **Offline-First**: Works completely air-gapped, no network dependencies +//! - **Multi-Language**: FFI bindings for Deno, Rescript, and WASM +//! - **Standards Compliant**: RSR Bronze tier, TPCF Perimeter 3 +//! +//! # Examples +//! +//! ``` +//! use bunsenite::NickelLoader; +//! +//! let config = r#" +//! { +//! name = "example", +//! version = "1.0.0", +//! } +//! "#; +//! +//! let result = NickelLoader::new() +//! .parse_string(config, "config.ncl") +//! .expect("Failed to parse config"); +//! +//! println!("Parsed config: {}", result); +//! ``` +//! +//! # Architecture +//! +//! ```text +//! ┌─────────────────────────────────────────────────┐ +//! │ Consumers │ +//! ├───────────────┬───────────────┬─────────────────┤ +//! │ Deno │ Rescript │ Browser │ +//! │ (TypeScript) │ (ReScript) │ (WASM) │ +//! └───────┬───────┴───────┬───────┴────────┬────────┘ +//! │ │ │ +//! ▼ ▼ ▼ +//! ┌──────────┐ ┌──────────┐ ┌──────────────┐ +//! │ Zig FFI │ │ Zig FFI │ │ wasm-bindgen │ +//! │ (C ABI) │ │ (C ABI) │ │ │ +//! └─────┬────┘ └─────┬────┘ └──────┬───────┘ +//! │ │ │ +//! └──────────────┴─────────────────┘ +//! │ +//! ▼ +//! ┌─────────────────┐ +//! │ Rust Core │ +//! │ (lib.rs) │ +//! │ │ +//! │ nickel-lang-core│ +//! │ 0.9.1 │ +//! └─────────────────┘ +//! ``` + +#![deny(unsafe_code)] +#![warn( + missing_docs, + missing_debug_implementations, + rust_2018_idioms, + unreachable_pub +)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +pub mod error; +pub mod loader; + +/// JSON Schema validation for parsed Nickel configurations +#[cfg(feature = "schema")] +#[cfg_attr(docsrs, doc(cfg(feature = "schema")))] +pub mod schema; + +/// C FFI exports for native bindings (Deno, ReScript via Zig) +/// +/// This module uses `unsafe` for FFI boundary crossing. +/// The Zig layer provides additional safety and stable ABI guarantees. +#[allow(unsafe_code)] +#[cfg(not(target_arch = "wasm32"))] +pub mod ffi; + +#[cfg(target_arch = "wasm32")] +#[cfg_attr(docsrs, doc(cfg(target_arch = "wasm32")))] +pub mod wasm; + +// Re-exports for convenience +pub use error::{Error, Result}; +pub use loader::NickelLoader; + +#[cfg(feature = "schema")] +pub use schema::{validate_config, SchemaValidator}; + +/// Library version, updated automatically from Cargo.toml +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Library name +pub const NAME: &str = env!("CARGO_PKG_NAME"); + +/// RSR Framework compliance tier +pub const RSR_TIER: &str = "bronze"; + +/// TPCF Perimeter assignment +pub const TPCF_PERIMETER: u8 = 3; // Community Sandbox + +/// Verify RSR compliance at compile time +/// +/// This ensures that the library meets RSR Bronze tier requirements: +/// - Type safety (enforced by Rust compiler) +/// - Memory safety (enforced by `#![deny(unsafe_code)]`) +/// - Offline-first (no network dependencies in production code) +#[cfg(test)] +mod rsr_compliance_tests { + use super::*; + + #[test] + fn test_no_unsafe_code() { + // This test passes if compilation succeeds with #![deny(unsafe_code)] + assert_eq!(RSR_TIER, "bronze"); + } + + #[test] + fn test_tpcf_perimeter() { + assert_eq!(TPCF_PERIMETER, 3); + } + + #[test] + fn test_version_format() { + // Ensure version follows semver + let parts: Vec<&str> = VERSION.split('.').collect(); + assert_eq!(parts.len(), 3, "Version should be semver (x.y.z)"); + } +} diff --git a/vendor/bunsenite/src/loader.rs b/vendor/bunsenite/src/loader.rs new file mode 100644 index 0000000..a962fcf --- /dev/null +++ b/vendor/bunsenite/src/loader.rs @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Nickel file loader and parser +//! +//! This module provides the core functionality for loading and parsing Nickel +//! configuration files using nickel-lang-core 0.18.0. +//! +//! # Core Responsibilities +//! +//! 1. **Loading**: Reading configuration from strings or files. +//! 2. **Parsing**: Using the upstream `nickel-lang-core` parser to generate an AST. +//! 3. **Evaluation**: Executing the Nickel program to produce a final configuration. +//! 4. **Export**: Converting the evaluated configuration into standard formats (JSON). +//! +//! # API Compatibility Notes (nickel-lang-core 0.18.0) +//! +//! - In-memory sources are loaded through `ProgramBuilder`. +//! - `eval_full()` takes no arguments. +//! - Evaluated values are converted with `serde_json::to_value()`. + +use crate::error::{Error, Result}; +use nickel_lang_core::eval::cache::lazy::CBNCache; +use nickel_lang_core::program::{Program, ProgramBuilder}; +use nickel_lang_core::typecheck::TypecheckMode; +use serde_json::Value; +use std::path::Path; + +/// Type alias for the standard Program with CBN (Call-By-Need) caching. +/// +/// CBN is the standard evaluation strategy for Nickel, ensuring lazy evaluation +/// of configuration fields. +type NickelProgram = Program; + +/// Nickel configuration loader +/// +/// Provides methods to parse and evaluate Nickel configuration files. +/// +/// # Examples +/// +/// ``` +/// use bunsenite::NickelLoader; +/// +/// let loader = NickelLoader::new(); +/// let config = r#"{ name = "example", version = "1.0.0" }"#; +/// let result = loader.parse_string(config, "test.ncl").unwrap(); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct NickelLoader { + /// Enable verbose error reporting + verbose: bool, +} + +impl NickelLoader { + /// Create a new Nickel loader with default settings + pub fn new() -> Self { + Self::default() + } + + /// Enable verbose error reporting + pub fn with_verbose(mut self, verbose: bool) -> Self { + self.verbose = verbose; + self + } + + /// Parse and evaluate a Nickel configuration from a string + /// + /// # Arguments + /// + /// * `source` - The Nickel configuration source code + /// * `name` - A name for this configuration (used in error messages) + /// + /// # Returns + /// + /// A JSON value representing the evaluated configuration + /// + /// # Errors + /// + /// Returns an error if parsing or evaluation fails + /// + /// # Examples + /// + /// ``` + /// use bunsenite::NickelLoader; + /// + /// let loader = NickelLoader::new(); + /// let result = loader.parse_string("{ foo = 42 }", "config.ncl"); + /// assert!(result.is_ok()); + /// ``` + pub fn parse_string(&self, source: &str, name: &str) -> Result { + let mut program: NickelProgram = ProgramBuilder::new() + .add_source_string(source, name) + .build() + .map_err(|e| Error::parse_error(name, format!("{:?}", e)))?; + + // Evaluate the program + let eval_result = program.eval_full().map_err(|e| { + let msg = format!("{:?}", e); + Error::evaluation_error(name, msg) + })?; + + // Convert to JSON + let json_value = serde_json::to_value(&eval_result) + .map_err(|e| Error::serialization_error(format!("Failed to convert to JSON: {}", e)))?; + + Ok(json_value) + } + + /// Parse and evaluate a Nickel configuration from a file + /// + /// # Arguments + /// + /// * `path` - Path to the Nickel configuration file + /// + /// # Returns + /// + /// A JSON value representing the evaluated configuration + /// + /// # Errors + /// + /// Returns an error if the file cannot be read or if parsing/evaluation fails + /// + /// # Examples + /// + /// ```no_run + /// use bunsenite::NickelLoader; + /// + /// let loader = NickelLoader::new(); + /// let result = loader.parse_file("config.ncl"); + /// ``` + pub fn parse_file>(&self, path: P) -> Result { + let path = path.as_ref(); + let source = std::fs::read_to_string(path)?; + let name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown.ncl"); + + self.parse_string(&source, name) + } + + /// Validate a Nickel configuration without evaluating it + /// + /// This performs parsing and type-checking but does not evaluate the program. + /// + /// # Arguments + /// + /// * `source` - The Nickel configuration source code + /// * `name` - A name for this configuration (used in error messages) + /// + /// # Returns + /// + /// Ok(()) if the configuration is valid, Err otherwise + /// + /// # Examples + /// + /// ``` + /// use bunsenite::NickelLoader; + /// + /// let loader = NickelLoader::new(); + /// assert!(loader.validate("{ foo = 42 }", "test.ncl").is_ok()); + /// assert!(loader.validate("{ foo = }", "bad.ncl").is_err()); + /// ``` + pub fn validate(&self, source: &str, name: &str) -> Result<()> { + let mut program: NickelProgram = ProgramBuilder::new() + .add_source_string(source, name) + .build() + .map_err(|e| Error::parse_error(name, format!("{:?}", e)))?; + + program + .typecheck(TypecheckMode::Walk) + .map_err(|e| Error::parse_error(name, format!("{:?}", e)))?; + + Ok(()) + } + + /// Parse and evaluate a Nickel configuration (alias for parse_string) + /// + /// This is a convenience alias for `parse_string` for API compatibility. + pub fn parse(&self, source: &str, name: &str) -> Result { + self.parse_string(source, name) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_parse_simple_record() { + let loader = NickelLoader::new(); + let source = r#"{ name = "test", version = "1.0.0" }"#; + let result = loader.parse_string(source, "test.ncl"); + assert!(result.is_ok()); + } + + #[test] + fn test_parse_with_computation() { + let loader = NickelLoader::new(); + let source = r#"{ sum = 1 + 2 + 3, product = 4 * 5 }"#; + let result = loader.parse_string(source, "math.ncl").unwrap(); + + assert_eq!(result["sum"], 6); + assert_eq!(result["product"], 20); + } + + #[test] + fn test_parse_with_strings() { + let loader = NickelLoader::new(); + let source = r#"{ greeting = "Hello, " ++ "World!" }"#; + let result = loader.parse_string(source, "strings.ncl").unwrap(); + + assert_eq!(result["greeting"], "Hello, World!"); + } + + #[test] + fn test_parse_invalid_syntax() { + let loader = NickelLoader::new(); + let source = r#"{ foo = }"#; // Invalid: missing value + let result = loader.parse_string(source, "bad.ncl"); + assert!(result.is_err()); + } + + #[test] + fn test_validate_valid_config() { + let loader = NickelLoader::new(); + let source = r#"{ foo = 42, bar = "baz" }"#; + assert!(loader.validate(source, "test.ncl").is_ok()); + } + + #[test] + fn test_validate_invalid_config() { + let loader = NickelLoader::new(); + let source = r#"{ foo = }"#; // Invalid + assert!(loader.validate(source, "bad.ncl").is_err()); + } + + #[test] + fn test_verbose_mode() { + let loader = NickelLoader::new().with_verbose(true); + assert_eq!(loader.verbose, true); + } + + #[test] + fn test_default_constructor() { + let loader = NickelLoader::default(); + assert_eq!(loader.verbose, false); + } + + #[test] + fn test_error_contains_filename() { + let loader = NickelLoader::new(); + let source = r#"{ invalid syntax }"#; + let result = loader.parse_string(source, "myconfig.ncl"); + + match result { + Err(e) => { + let msg = format!("{}", e); + assert!(msg.contains("myconfig.ncl")); + } + Ok(_) => panic!("Expected error"), + } + } +} diff --git a/vendor/bunsenite/src/main.rs b/vendor/bunsenite/src/main.rs new file mode 100644 index 0000000..b136e5a --- /dev/null +++ b/vendor/bunsenite/src/main.rs @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Bunsenite CLI +//! +//! Command-line interface for parsing and evaluating Nickel configuration files + +use bunsenite::{NickelLoader, VERSION}; +use clap::{Parser, Subcommand}; +use std::path::PathBuf; +use std::process; + +#[derive(Parser)] +#[command( + name = "bunsenite", + version = VERSION, + about = "Nickel configuration file parser with multi-language FFI bindings", + long_about = "Bunsenite is a Nickel configuration file parser with multi-language FFI bindings.\n\ + It provides a Rust core library with a stable C ABI layer (via Zig) that enables\n\ + bindings for Deno (JavaScript/TypeScript), Rescript, and WebAssembly.\n\n\ + RSR Compliance: Bronze Tier | TPCF Perimeter: 3 (Community Sandbox)" +)] +struct Cli { + #[command(subcommand)] + command: Option, + + /// Enable verbose output + #[arg(short, long, global = true)] + verbose: bool, +} + +#[derive(Subcommand)] +enum Commands { + /// Parse and evaluate a Nickel configuration file + Parse { + /// Path to the Nickel configuration file + #[arg(value_name = "FILE")] + file: PathBuf, + + /// Pretty-print the output JSON + #[arg(short, long)] + pretty: bool, + }, + + /// Validate a Nickel configuration without evaluating it + Validate { + /// Path to the Nickel configuration file + #[arg(value_name = "FILE")] + file: PathBuf, + }, + + /// Watch a file for changes and re-evaluate on save + #[cfg(feature = "watch")] + Watch { + /// Path to the Nickel configuration file to watch + #[arg(value_name = "FILE")] + file: PathBuf, + + /// Pretty-print the output JSON + #[arg(short, long)] + pretty: bool, + }, + + /// Start an interactive REPL for Nickel expressions + #[cfg(feature = "repl")] + Repl, + + /// Validate a Nickel config against a JSON schema + #[cfg(feature = "schema")] + Schema { + /// Path to the Nickel configuration file + #[arg(value_name = "CONFIG")] + config: PathBuf, + + /// Path to the JSON schema file + #[arg(value_name = "SCHEMA")] + schema: PathBuf, + }, + + /// Show version and compliance information + Info, +} + +fn main() { + // Install miette's pretty error handler + miette::set_hook(Box::new(|_| { + Box::new( + miette::MietteHandlerOpts::new() + .terminal_links(true) + .unicode(true) + .context_lines(2) + .build(), + ) + })) + .ok(); + + let cli = Cli::parse(); + + let result = match cli.command { + Some(Commands::Parse { file, pretty }) => handle_parse(file, pretty, cli.verbose), + Some(Commands::Validate { file }) => handle_validate(file, cli.verbose), + #[cfg(feature = "watch")] + Some(Commands::Watch { file, pretty }) => handle_watch(file, pretty, cli.verbose), + #[cfg(feature = "repl")] + Some(Commands::Repl) => handle_repl(cli.verbose), + #[cfg(feature = "schema")] + Some(Commands::Schema { config, schema }) => handle_schema(config, schema, cli.verbose), + Some(Commands::Info) => { + handle_info(); + Ok(()) + } + None => { + // No command specified, show help + println!("{}", get_help_text()); + Ok(()) + } + }; + + if let Err(e) = result { + // Use miette's error reporting + eprintln!("{:?}", miette::Report::new(e)); + process::exit(1); + } +} + +fn handle_parse(file: PathBuf, pretty: bool, verbose: bool) -> bunsenite::Result<()> { + if verbose { + eprintln!("Parsing file: {}", file.display()); + } + + let loader = NickelLoader::new().with_verbose(verbose); + let result = loader.parse_file(&file)?; + + if pretty { + println!("{}", serde_json::to_string_pretty(&result).unwrap()); + } else { + println!("{}", serde_json::to_string(&result).unwrap()); + } + + if verbose { + eprintln!("✓ Successfully parsed and evaluated"); + } + + Ok(()) +} + +fn handle_validate(file: PathBuf, verbose: bool) -> bunsenite::Result<()> { + if verbose { + eprintln!("Validating file: {}", file.display()); + } + + let source = std::fs::read_to_string(&file)?; + let name = file + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown.ncl"); + + let loader = NickelLoader::new().with_verbose(verbose); + loader.validate(&source, name)?; + + println!("✓ Configuration is valid"); + + Ok(()) +} + +/// Watch a file for changes and re-parse on save +#[cfg(feature = "watch")] +fn handle_watch(file: PathBuf, pretty: bool, verbose: bool) -> bunsenite::Result<()> { + use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; + use std::sync::mpsc::channel; + use std::time::Duration; + + println!( + "Watching {} for changes (Ctrl+C to stop)...", + file.display() + ); + + // Initial parse + if let Err(e) = handle_parse(file.clone(), pretty, verbose) { + eprintln!("{:?}", miette::Report::new(e)); + } + + let (tx, rx) = channel(); + + let mut watcher = RecommendedWatcher::new( + move |res| { + if let Ok(event) = res { + let _ = tx.send(event); + } + }, + Config::default().with_poll_interval(Duration::from_millis(500)), + ) + .map_err(|e| bunsenite::Error::watch_error(e.to_string()))?; + + watcher + .watch(&file, RecursiveMode::NonRecursive) + .map_err(|e| bunsenite::Error::watch_error(e.to_string()))?; + + loop { + match rx.recv() { + Ok(event) => { + if event.kind.is_modify() { + println!("\n--- File changed, re-parsing... ---\n"); + if let Err(e) = handle_parse(file.clone(), pretty, verbose) { + eprintln!("{:?}", miette::Report::new(e)); + } + } + } + Err(e) => { + return Err(bunsenite::Error::watch_error(e.to_string())); + } + } + } +} + +/// Validate a Nickel config against a JSON schema +#[cfg(feature = "schema")] +fn handle_schema(config: PathBuf, schema: PathBuf, verbose: bool) -> bunsenite::Result<()> { + use bunsenite::SchemaValidator; + + if verbose { + eprintln!( + "Validating {} against schema {}", + config.display(), + schema.display() + ); + } + + let loader = NickelLoader::new().with_verbose(verbose); + let result = loader.parse_file(&config)?; + + let validator = SchemaValidator::from_file(&schema)?; + validator.validate(&result)?; + + println!("✓ Configuration matches schema"); + + Ok(()) +} + +/// Interactive REPL for Nickel expressions +#[cfg(feature = "repl")] +fn handle_repl(verbose: bool) -> bunsenite::Result<()> { + use rustyline::error::ReadlineError; + use rustyline::DefaultEditor; + + println!("Bunsenite v{} - Nickel REPL", VERSION); + println!("Type Nickel expressions to evaluate. Use :help for commands, :quit to exit.\n"); + + let mut rl = DefaultEditor::new().map_err(|e| bunsenite::Error::internal(e.to_string()))?; + let loader = NickelLoader::new().with_verbose(verbose); + + loop { + match rl.readline("nickel> ") { + Ok(line) => { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + // Handle REPL commands + match trimmed { + ":quit" | ":q" | ":exit" => { + println!("Goodbye!"); + break; + } + ":help" | ":h" => { + println!("REPL Commands:"); + println!(" :help, :h Show this help"); + println!(" :quit, :q Exit the REPL"); + println!(" :clear, :c Clear the screen"); + println!(" :version, :v Show version info"); + println!("\nEnter any Nickel expression to evaluate it."); + continue; + } + ":clear" | ":c" => { + print!("\x1B[2J\x1B[1;1H"); + continue; + } + ":version" | ":v" => { + println!("Bunsenite v{}", VERSION); + continue; + } + _ => {} + } + + let _ = rl.add_history_entry(&line); + + match loader.parse(&trimmed, "") { + Ok(result) => { + println!("{}", serde_json::to_string_pretty(&result).unwrap()); + } + Err(e) => { + eprintln!("{:?}", miette::Report::new(e)); + } + } + } + Err(ReadlineError::Interrupted) => { + println!("^C"); + continue; + } + Err(ReadlineError::Eof) => { + println!("Goodbye!"); + break; + } + Err(e) => { + return Err(bunsenite::Error::internal(e.to_string())); + } + } + } + + Ok(()) +} + +fn handle_info() { + println!("Bunsenite v{}", VERSION); + println!(); + println!("A Nickel configuration file parser with multi-language FFI bindings"); + println!(); + println!("Features:"); + println!(" • Type Safety: Compile-time guarantees via Rust's type system"); + println!(" • Memory Safety: Rust ownership model, zero unsafe blocks"); + println!(" • Offline-First: Works completely air-gapped, no network dependencies"); + println!(" • Multi-Language: FFI bindings for Deno, Rescript, and WASM"); + println!(); + println!("Standards Compliance:"); + println!(" • RSR Framework: Bronze Tier"); + println!(" • TPCF Perimeter: 3 (Community Sandbox)"); + println!(" • License: Dual PMPL-1.0 + Palimpsest 0.8"); + println!(); + println!("Repository: https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite"); + println!(); +} + +fn get_help_text() -> String { + let mut commands = r#"COMMANDS: + parse Parse and evaluate a Nickel configuration file + validate Validate a Nickel configuration without evaluating it"# + .to_string(); + + #[cfg(feature = "watch")] + { + commands.push_str("\n watch Watch a file and re-evaluate on changes"); + } + + #[cfg(feature = "repl")] + { + commands.push_str("\n repl Start an interactive Nickel REPL"); + } + + #[cfg(feature = "schema")] + { + commands.push_str("\n schema Validate config against JSON schema"); + } + + commands.push_str( + r#" + info Show version and compliance information + help Print this message or the help of the given subcommand(s)"#, + ); + + let mut examples = r#"EXAMPLES: + # Parse and evaluate a config file + bunsenite parse config.ncl + + # Parse with pretty-printed output + bunsenite parse config.ncl --pretty + + # Validate without evaluating + bunsenite validate config.ncl"# + .to_string(); + + #[cfg(feature = "watch")] + { + examples.push_str( + r#" + + # Watch for changes + bunsenite watch config.ncl --pretty"#, + ); + } + + #[cfg(feature = "repl")] + { + examples.push_str( + r#" + + # Start interactive REPL + bunsenite repl"#, + ); + } + + #[cfg(feature = "schema")] + { + examples.push_str( + r#" + + # Validate against JSON schema + bunsenite schema config.ncl schema.json"#, + ); + } + + examples.push_str( + r#" + + # Show info + bunsenite info"#, + ); + + format!( + r#"Bunsenite v{VERSION} +Nickel configuration file parser + +USAGE: + bunsenite + +{commands} + +OPTIONS: + -v, --verbose Enable verbose output + -h, --help Print help information + -V, --version Print version information + +{examples} + +For more information, visit: +https://gitlab.com/campaign-for-cooler-coding-and-programming/bunsenite +"# + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cli_info_runs() { + // Just verify info command doesn't panic + handle_info(); + } + + #[test] + fn test_help_text_contains_version() { + let help = get_help_text(); + assert!(help.contains(VERSION)); + } +} diff --git a/vendor/bunsenite/src/schema.rs b/vendor/bunsenite/src/schema.rs new file mode 100644 index 0000000..ebde53d --- /dev/null +++ b/vendor/bunsenite/src/schema.rs @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Schema validation for Nickel configurations +//! +//! This module provides JSON Schema validation for parsed Nickel configurations. +//! It allows validating that the output of a Nickel config matches an expected schema. + +use crate::error::{Error, Result}; +use jsonschema::{JSONSchema, ValidationError}; +use serde_json::Value; +use std::path::Path; + +/// Schema validator for Nickel configurations +#[derive(Debug)] +pub struct SchemaValidator { + schema: JSONSchema, + schema_source: String, +} + +impl SchemaValidator { + /// Create a new schema validator from a JSON schema value + pub fn new(schema: Value) -> Result { + let compiled = JSONSchema::compile(&schema) + .map_err(|e| Error::invalid_input(format!("Invalid JSON schema: {}", e)))?; + + Ok(Self { + schema: compiled, + schema_source: serde_json::to_string_pretty(&schema) + .unwrap_or_else(|_| "".to_string()), + }) + } + + /// Create a new schema validator from a JSON schema string + pub fn from_str(schema_str: &str) -> Result { + let schema: Value = serde_json::from_str(schema_str) + .map_err(|e| Error::invalid_input(format!("Invalid JSON: {}", e)))?; + Self::new(schema) + } + + /// Create a new schema validator from a file path + pub fn from_file(path: impl AsRef) -> Result { + let content = std::fs::read_to_string(path.as_ref())?; + Self::from_str(&content) + } + + /// Validate a JSON value against the schema + pub fn validate(&self, value: &Value) -> Result<()> { + let result = self.schema.validate(value); + + if let Err(errors) = result { + let error_messages: Vec = errors + .map(|e| format!(" - {}: {}", e.instance_path, e)) + .collect(); + + return Err(Error::invalid_input(format!( + "Schema validation failed:\n{}", + error_messages.join("\n") + ))); + } + + Ok(()) + } + + /// Check if a value is valid without returning detailed errors + pub fn is_valid(&self, value: &Value) -> bool { + self.schema.is_valid(value) + } + + /// Get validation errors as a list of strings + pub fn get_errors(&self, value: &Value) -> Vec { + match self.schema.validate(value) { + Ok(_) => vec![], + Err(errors) => errors + .map(|e| format!("{}: {}", e.instance_path, e)) + .collect(), + } + } +} + +/// Validate a Nickel configuration against a JSON schema +/// +/// # Arguments +/// +/// * `config` - The parsed Nickel configuration as a JSON value +/// * `schema` - The JSON schema to validate against +/// +/// # Returns +/// +/// Returns `Ok(())` if validation passes, or an error with details on failure. +pub fn validate_config(config: &Value, schema: &Value) -> Result<()> { + let validator = SchemaValidator::new(schema.clone())?; + validator.validate(config) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_valid_schema() { + let schema = json!({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "version": { "type": "string" } + }, + "required": ["name"] + }); + + let validator = SchemaValidator::new(schema).unwrap(); + + let valid_config = json!({ + "name": "test", + "version": "1.0.0" + }); + + assert!(validator.validate(&valid_config).is_ok()); + assert!(validator.is_valid(&valid_config)); + } + + #[test] + fn test_invalid_config() { + let schema = json!({ + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + }); + + let validator = SchemaValidator::new(schema).unwrap(); + + let invalid_config = json!({ + "version": "1.0.0" + }); + + assert!(validator.validate(&invalid_config).is_err()); + assert!(!validator.is_valid(&invalid_config)); + } + + #[test] + fn test_type_validation() { + let schema = json!({ + "type": "object", + "properties": { + "port": { "type": "integer", "minimum": 1, "maximum": 65535 } + } + }); + + let validator = SchemaValidator::new(schema).unwrap(); + + let valid = json!({ "port": 8080 }); + let invalid_type = json!({ "port": "8080" }); + let invalid_range = json!({ "port": 70000 }); + + assert!(validator.is_valid(&valid)); + assert!(!validator.is_valid(&invalid_type)); + assert!(!validator.is_valid(&invalid_range)); + } + + #[test] + fn test_get_errors() { + let schema = json!({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "port": { "type": "integer" } + }, + "required": ["name", "port"] + }); + + let validator = SchemaValidator::new(schema).unwrap(); + + let invalid = json!({ "name": 123 }); + let errors = validator.get_errors(&invalid); + + assert!(!errors.is_empty()); + } + + #[test] + fn test_from_str() { + let schema_str = r#"{ + "type": "object", + "properties": { + "enabled": { "type": "boolean" } + } + }"#; + + let validator = SchemaValidator::from_str(schema_str).unwrap(); + let valid = json!({ "enabled": true }); + + assert!(validator.is_valid(&valid)); + } + + #[test] + fn test_validate_config_function() { + let config = json!({ "name": "test" }); + let schema = json!({ + "type": "object", + "properties": { + "name": { "type": "string" } + } + }); + + assert!(validate_config(&config, &schema).is_ok()); + } +} diff --git a/vendor/bunsenite/src/wasm.rs b/vendor/bunsenite/src/wasm.rs new file mode 100644 index 0000000..19e6b04 --- /dev/null +++ b/vendor/bunsenite/src/wasm.rs @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! WebAssembly bindings for Bunsenite +//! +//! This module provides WASM bindings that enable Bunsenite to run in browsers +//! and other WASM environments with ~95% native performance. +//! +//! # Examples +//! +//! ```javascript +//! import init, { parse_nickel } from './bunsenite.js'; +//! +//! async function main() { +//! await init(); +//! const config = `{ name = "example", version = "1.0.0" }`; +//! const result = parse_nickel(config, "config.ncl"); +//! console.log(JSON.parse(result)); +//! } +//! ``` + +use crate::{Error, NickelLoader}; +use wasm_bindgen::prelude::*; + +// Note: wee_alloc was removed as it is unmaintained and has known memory leaks. +// Rust 1.71+ provides a suitable default allocator for wasm32 targets. + +/// Initialize WASM module +/// +/// This should be called once before using any other WASM functions. +/// It sets up panic hooks for better error messages in the browser. +#[wasm_bindgen(start)] +pub fn init() { + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} + +/// Parse and evaluate a Nickel configuration string +/// +/// # Arguments +/// +/// * `source` - The Nickel configuration source code +/// * `name` - A name for this configuration (used in error messages) +/// +/// # Returns +/// +/// A JSON string representing the evaluated configuration, or an error message +/// +/// # Examples +/// +/// ```javascript +/// const result = parse_nickel('{ foo = 42 }', 'config.ncl'); +/// const config = JSON.parse(result); +/// console.log(config.foo); // 42 +/// ``` +#[wasm_bindgen] +pub fn parse_nickel(source: &str, name: &str) -> Result { + let loader = NickelLoader::new(); + + let result = loader + .parse_string(source, name) + .map_err(|e| JsValue::from_str(&format!("{}", e)))?; + + serde_json::to_string(&result) + .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e))) +} + +/// Validate a Nickel configuration without evaluating it +/// +/// # Arguments +/// +/// * `source` - The Nickel configuration source code +/// * `name` - A name for this configuration (used in error messages) +/// +/// # Returns +/// +/// Ok(()) if valid, Err with error message if invalid +/// +/// # Examples +/// +/// ```javascript +/// try { +/// validate_nickel('{ foo = 42 }', 'config.ncl'); +/// console.log('Valid!'); +/// } catch (e) { +/// console.error('Invalid:', e); +/// } +/// ``` +#[wasm_bindgen] +pub fn validate_nickel(source: &str, name: &str) -> Result<(), JsValue> { + let loader = NickelLoader::new(); + + loader + .validate(source, name) + .map_err(|e| JsValue::from_str(&format!("{}", e))) +} + +/// Get library version +#[wasm_bindgen] +pub fn version() -> String { + crate::VERSION.to_string() +} + +/// Get RSR compliance tier +#[wasm_bindgen] +pub fn rsr_tier() -> String { + crate::RSR_TIER.to_string() +} + +/// Get TPCF perimeter +#[wasm_bindgen] +pub fn tpcf_perimeter() -> u8 { + crate::TPCF_PERIMETER +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_wasm_parse_simple() { + let source = r#"{ name = "test" }"#; + let result = parse_nickel(source, "test.ncl"); + assert!(result.is_ok()); + } + + #[test] + fn test_wasm_validate_valid() { + let source = r#"{ foo = 42 }"#; + let result = validate_nickel(source, "test.ncl"); + assert!(result.is_ok()); + } + + #[test] + fn test_wasm_validate_invalid() { + let source = r#"{ foo = }"#; + let result = validate_nickel(source, "bad.ncl"); + assert!(result.is_err()); + } + + #[test] + fn test_wasm_version() { + let v = version(); + assert!(!v.is_empty()); + } + + #[test] + fn test_wasm_rsr_metadata() { + assert_eq!(rsr_tier(), "bronze"); + assert_eq!(tpcf_perimeter(), 3); + } +} diff --git a/vendor/bunsenite/stapeln.toml b/vendor/bunsenite/stapeln.toml new file mode 100644 index 0000000..5c7a905 --- /dev/null +++ b/vendor/bunsenite/stapeln.toml @@ -0,0 +1,97 @@ +# SPDX-License-Identifier: MPL-2.0 +# stapeln.toml — Layer-based container build for bunsenite +# +# stapeln builds containers as composable layers (German: "to stack"). +# Each layer is independently cacheable, verifiable, and signable. + +[metadata] +name = "bunsenite" +version = "0.1.0" +description = "bunsenite container service" +author = "Jonathan D.A. Jewell " +license = "MPL-2.0" +registry = "ghcr.io/hyperpolymath" + +[build] +containerfile = "Containerfile" +context = "." +runtime = "podman" + +# ── Layer Definitions ────────────────────────────────────────── + +[layers.base] +description = "Chainguard Wolfi minimal base" +from = "cgr.dev/chainguard/wolfi-base:latest" +cache = true +verify = true + +[layers.rust-toolchain] +description = "Rust compiler and build dependencies" +extends = "base" +packages = ["rust", "pkgconf", "build-base"] +cache = true + +[layers.rust-deps] +description = "Cargo dependency fetch" +extends = "rust-toolchain" +commands = ["cargo fetch --locked"] +cache-key = "Cargo.lock" +cache = true + +[layers.build] +description = "bunsenite Rust compilation" +extends = "rust-deps" +commands = ["cargo build --release"] +artifacts = [ + { src = "target/release/bunsenite", dst = "/app/bunsenite" }, +] + +[layers.runtime] +description = "Minimal runtime" +from = "cgr.dev/chainguard/wolfi-base:latest" +packages = ["ca-certificates", "curl"] +copy-from = [ + { layer = "build", src = "/app/", dst = "/app/" }, +] +entrypoint = ["["bunsenite"]"] +user = "bunsenite" + +# ── Security ─────────────────────────────────────────────────── + +[security] +non-root = true +read-only-root = false +no-new-privileges = true +cap-drop = ["ALL"] +seccomp-profile = "default" + +[security.signing] +algorithm = "ML-DSA-87" +provider = "cerro-torre" + +[security.sbom] +format = "spdx-json" +output = "sbom.spdx.json" +include-deps = true + +# ── Verification ─────────────────────────────────────────────── + +[verify] +vordr = true +svalinn = true +scan-on-build = true +fail-on = ["critical", "high"] + +# ── Targets ──────────────────────────────────────────────────── + +[targets.development] +layers = ["base", "rust-toolchain", "build"] +env = { LOG_LEVEL = "debug" } + +[targets.production] +layers = ["runtime"] +env = { LOG_LEVEL = "info" } + +[targets.test] +layers = ["base", "rust-toolchain", "build"] +env = { LOG_LEVEL = "debug" } diff --git a/vendor/bunsenite/tests/aspect_test.rs b/vendor/bunsenite/tests/aspect_test.rs new file mode 100644 index 0000000..bd8328f --- /dev/null +++ b/vendor/bunsenite/tests/aspect_test.rs @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Aspect tests for Bunsenite — robustness, error handling, and API contracts. +//! +//! Tests cover: +//! - Malformed / corrupt input is rejected gracefully (no panic, proper `Err`). +//! - Extremely large inputs are handled without panic. +//! - All public API entry points accept valid inputs without panicking. +//! - Error types carry expected diagnostic information. +//! - The `validate` path and the `parse` path agree on what is valid. + +use bunsenite::{Error, NickelLoader}; + +// --------------------------------------------------------------------------- +// Helper +// --------------------------------------------------------------------------- + +fn loader() -> NickelLoader { + NickelLoader::new() +} + +// --------------------------------------------------------------------------- +// Aspect: Malformed input is rejected gracefully — no panic +// --------------------------------------------------------------------------- + +/// Completely empty input must not panic. It should either parse (producing +/// an empty result) or return an error; the test only asserts the absence of +/// a panic. +#[test] +fn aspect_empty_input_no_panic() { + let result = loader().parse_string("", "empty.ncl"); + // No assertion on Ok/Err — just verifying no panic. + let _ = result; +} + +/// A string of random punctuation that cannot be valid Nickel must produce an +/// `Err` without panicking. +#[test] +fn aspect_garbage_input_returns_error() { + let result = loader().parse_string("@@@ !! ??? %%% ###", "garbage.ncl"); + assert!( + result.is_err(), + "Garbage input should return an error, not Ok" + ); +} + +/// An unclosed brace is syntactically invalid; the parser must return an error +/// rather than panicking or hanging. +#[test] +fn aspect_unclosed_brace_returns_error() { + let result = loader().parse_string("{ name = \"open\"", "unclosed.ncl"); + assert!(result.is_err(), "Unclosed brace must be rejected"); +} + +/// A record field with no value (`{ foo = }`) is invalid Nickel syntax; the +/// parser must return an error. +#[test] +fn aspect_field_missing_value_returns_error() { + let result = loader().parse_string("{ foo = }", "bad_field.ncl"); + assert!(result.is_err(), "Missing field value must be rejected"); +} + +/// A stray `=` with no surrounding structure is not valid Nickel. +#[test] +fn aspect_lone_equals_returns_error() { + let result = loader().parse_string("=", "lone_eq.ncl"); + assert!(result.is_err(), "A lone '=' must be rejected"); +} + +/// Invalid Unicode escape sequences or non-UTF-8-safe byte patterns must not +/// cause a panic. Using a replacement-character string as a soft approximation. +#[test] +fn aspect_unicode_replacement_no_panic() { + // U+FFFD is the Unicode replacement character — valid UTF-8, but + // unlikely to form valid Nickel syntax. + let weird = "\u{FFFD}\u{FFFD}\u{FFFD}"; + let result = loader().parse_string(weird, "unicode.ncl"); + let _ = result; // Must not panic. +} + +/// A very deeply nested Nickel record should not cause a stack overflow. +/// We use a moderate depth (50 levels) to keep the test fast while still +/// exercising the recursive parser. +#[test] +fn aspect_moderately_deep_nesting_no_panic() { + // Build "{ a = { a = { … = 42 } … } }" with 50 levels. + let depth = 50usize; + let open: String = "{ a = ".repeat(depth); + let mid = "42"; + let close: String = " }".repeat(depth); + let source = format!("{open}{mid}{close}"); + + let result = loader().parse_string(&source, "deep.ncl"); + // Not requiring Ok — some dialects may limit nesting — but must not panic. + let _ = result; +} + +// --------------------------------------------------------------------------- +// Aspect: Large inputs are handled without panic +// --------------------------------------------------------------------------- + +/// A configuration with a large number of fields (1000) must be processed +/// without panicking. Whether it succeeds or fails is secondary; the +/// important guarantee is no crash. +#[test] +fn aspect_large_flat_record_no_panic() { + let fields: String = (0..1000).map(|i| format!(" field_{i} = {i},\n")).collect(); + let source = format!("{{\n{fields}}}"); + let result = loader().parse_string(&source, "large.ncl"); + // Must not panic. Nickel may or may not handle 1000 fields; we only + // require graceful behaviour. + let _ = result; +} + +/// A single string field with a very long value (100 KiB) must not cause a +/// panic. +#[test] +fn aspect_long_string_value_no_panic() { + let long_val = "x".repeat(100_000); + let source = format!(r#"{{ data = "{long_val}" }}"#); + let result = loader().parse_string(&source, "longval.ncl"); + let _ = result; +} + +// --------------------------------------------------------------------------- +// Aspect: Public API accepts valid inputs without panicking +// --------------------------------------------------------------------------- + +/// `NickelLoader::new()` must never panic. +#[test] +fn aspect_loader_construction_never_panics() { + let _l = NickelLoader::new(); +} + +/// `NickelLoader::with_verbose(true)` must not panic. +#[test] +fn aspect_verbose_builder_never_panics() { + let _l = NickelLoader::new().with_verbose(true); + let _l2 = NickelLoader::new().with_verbose(false); +} + +/// `parse` (alias for `parse_string`) must not panic on valid input. +#[test] +fn aspect_parse_alias_no_panic_valid() { + let result = loader().parse(r#"{ ok = true }"#, "alias.ncl"); + assert!(result.is_ok(), "parse() alias must succeed on valid input"); +} + +/// `validate` must not panic on valid or invalid input. +#[test] +fn aspect_validate_no_panic_on_valid() { + let result = loader().validate(r#"{ x = 1 }"#, "valid.ncl"); + assert!(result.is_ok()); +} + +#[test] +fn aspect_validate_no_panic_on_invalid() { + // Should return Err, not panic. + let result = loader().validate("{ broken =", "broken.ncl"); + assert!(result.is_err()); +} + +/// `parse_file` with a nonexistent path must return an `Err`, not panic. +#[test] +fn aspect_parse_file_nonexistent_returns_error() { + let result = loader().parse_file("/nonexistent/path/does_not_exist.ncl"); + assert!( + result.is_err(), + "Nonexistent file path must produce an error" + ); +} + +// --------------------------------------------------------------------------- +// Aspect: Error type invariants +// --------------------------------------------------------------------------- + +/// A `ParseError` produced via the public constructor must be recoverable. +#[test] +fn aspect_parse_error_is_recoverable() { + let err = Error::parse_error("f.ncl", "syntax error"); + assert!( + err.is_recoverable(), + "ParseError should be classified as recoverable" + ); +} + +/// An `Internal` error must be classified as non-recoverable. +#[test] +fn aspect_internal_error_not_recoverable() { + let err = Error::internal("bug"); + assert!( + !err.is_recoverable(), + "Internal error should not be recoverable" + ); +} + +/// The `message()` accessor must return the message that was passed into the +/// constructor without transformation. +#[test] +fn aspect_error_message_accessor_round_trips() { + let msg = "custom diagnostic text"; + let err = Error::parse_error("test.ncl", msg); + assert_eq!( + err.message(), + msg, + "message() should return the original message verbatim" + ); +} + +/// A `SerializationError` must be non-recoverable (it indicates an internal +/// invariant violation, not a user mistake). +#[test] +fn aspect_serialization_error_not_recoverable() { + let err = Error::serialization_error("cannot convert"); + assert!( + !err.is_recoverable(), + "SerializationError should not be recoverable" + ); +} + +// --------------------------------------------------------------------------- +// Aspect: validate / parse agreement +// --------------------------------------------------------------------------- + +/// For every valid Nickel snippet, `validate` succeeding must imply that +/// `parse_string` also succeeds (no divergence between the two code paths +/// for syntactically valid input). +#[test] +fn aspect_validate_implies_parse_for_valid_inputs() { + let valid_inputs = [ + r#"{ a = 1 }"#, + r#"{ name = "test", flag = true }"#, + r#"{ nested = { x = 42 } }"#, + r#"{ items = [1, 2, 3] }"#, + ]; + + let l = loader(); + for src in valid_inputs { + if l.validate(src, "t.ncl").is_ok() { + let parse_result = l.parse_string(src, "t.ncl"); + assert!( + parse_result.is_ok(), + "validate() succeeded but parse_string() failed for: {src}" + ); + } + } +} diff --git a/vendor/bunsenite/tests/e2e_test.rs b/vendor/bunsenite/tests/e2e_test.rs new file mode 100644 index 0000000..d7f165f --- /dev/null +++ b/vendor/bunsenite/tests/e2e_test.rs @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! End-to-end tests for Bunsenite: create → serialize → deserialize → verify round-trip. +//! +//! These tests exercise the full lifecycle of loading a Nickel configuration: +//! starting from a source string or temp file, parsing it via `NickelLoader`, +//! and verifying that the resulting JSON value has the expected shape and +//! content. At least 10 `#[test]` functions are provided. + +use bunsenite::{NickelLoader, NAME, RSR_TIER}; +use std::io::Write as _; +use tempfile::NamedTempFile; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Create a `NickelLoader` with verbose mode disabled (default for E2E tests). +fn loader() -> NickelLoader { + NickelLoader::new() +} + +/// Write `content` to a temporary `.ncl` file and return the handle. +/// The file is deleted when the handle is dropped. +fn temp_ncl(content: &str) -> NamedTempFile { + let mut f = NamedTempFile::new().expect("Failed to create temp file"); + write!(f, "{}", content).expect("Failed to write temp file"); + f +} + +// --------------------------------------------------------------------------- +// E2E 1: String source → JSON round-trip — simple record +// --------------------------------------------------------------------------- + +/// A simple flat record parsed from a string produces a JSON object with the +/// exact keys and scalar values that were declared. +#[test] +fn e2e_string_round_trip_simple_record() { + let source = r#"{ project = "bunsenite", version = "1.0.0", stable = true }"#; + let json = loader() + .parse_string(source, "simple.ncl") + .expect("E2E: simple record must parse"); + + assert_eq!(json["project"], "bunsenite"); + assert_eq!(json["version"], "1.0.0"); + assert_eq!(json["stable"], true); +} + +// --------------------------------------------------------------------------- +// E2E 2: String source → JSON round-trip — numeric fields +// --------------------------------------------------------------------------- + +/// Numeric fields survive the parse → JSON conversion without truncation or +/// type-widening. +#[test] +fn e2e_string_round_trip_numeric_fields() { + let source = r#"{ port = 8080, workers = 4, timeout = 30 }"#; + let json = loader() + .parse_string(source, "numeric.ncl") + .expect("E2E: numeric fields must parse"); + + assert_eq!(json["port"], 8080); + assert_eq!(json["workers"], 4); + assert_eq!(json["timeout"], 30); +} + +// --------------------------------------------------------------------------- +// E2E 3: File source → JSON round-trip +// --------------------------------------------------------------------------- + +/// When the same Nickel source is written to a temporary file and loaded via +/// `parse_file`, the resulting JSON is identical to the string-parsed version. +#[test] +fn e2e_file_round_trip_matches_string() { + let source = r#"{ name = "file-test", enabled = true, count = 7 }"#; + let tmp = temp_ncl(source); + + let from_file = loader() + .parse_file(tmp.path()) + .expect("E2E: file load must succeed"); + let from_string = loader() + .parse_string(source, "ref.ncl") + .expect("E2E: string load must succeed"); + + // Both routes must produce the same JSON structure. + assert_eq!(from_file, from_string); +} + +// --------------------------------------------------------------------------- +// E2E 4: Nested record round-trip +// --------------------------------------------------------------------------- + +/// A nested Nickel record is correctly projected into a nested JSON object. +#[test] +fn e2e_nested_record_round_trip() { + let source = r#" +{ + server = { + host = "localhost", + port = 3000, + }, + database = { + host = "db.example.com", + port = 5432, + }, +} +"#; + let json = loader() + .parse_string(source, "nested.ncl") + .expect("E2E: nested record must parse"); + + assert_eq!(json["server"]["host"], "localhost"); + assert_eq!(json["server"]["port"], 3000); + assert_eq!(json["database"]["host"], "db.example.com"); + assert_eq!(json["database"]["port"], 5432); +} + +// --------------------------------------------------------------------------- +// E2E 5: Array round-trip +// --------------------------------------------------------------------------- + +/// Nickel arrays survive serialisation as JSON arrays with the correct +/// element count and values. +#[test] +fn e2e_array_round_trip() { + let source = r#"{ tags = ["rust", "nickel", "config"], counts = [1, 2, 3] }"#; + let json = loader() + .parse_string(source, "array.ncl") + .expect("E2E: array record must parse"); + + let tags = json["tags"].as_array().expect("tags should be an array"); + assert_eq!(tags.len(), 3); + assert_eq!(tags[0], "rust"); + assert_eq!(tags[1], "nickel"); + assert_eq!(tags[2], "config"); + + let counts = json["counts"] + .as_array() + .expect("counts should be an array"); + assert_eq!(counts.len(), 3); +} + +// --------------------------------------------------------------------------- +// E2E 6: Arithmetic expression evaluated in config +// --------------------------------------------------------------------------- + +/// Nickel supports computed values; the evaluator must reduce the expression +/// before serialisation so that the JSON contains the final numeric result. +#[test] +fn e2e_computed_arithmetic_round_trip() { + let source = r#"{ total = 100 + 200 + 50, ratio = 6 * 7 }"#; + let json = loader() + .parse_string(source, "arith.ncl") + .expect("E2E: arithmetic config must parse"); + + assert_eq!(json["total"], 350); + assert_eq!(json["ratio"], 42); +} + +// --------------------------------------------------------------------------- +// E2E 7: String concatenation evaluated in config +// --------------------------------------------------------------------------- + +/// String concatenation via `++` must be reduced by the evaluator; the JSON +/// value must be the fully concatenated string. +#[test] +fn e2e_string_concat_round_trip() { + let source = r#"{ greeting = "Hello, " ++ "World!", label = "v" ++ "1" ++ "." ++ "0" }"#; + let json = loader() + .parse_string(source, "concat.ncl") + .expect("E2E: string concat config must parse"); + + assert_eq!(json["greeting"], "Hello, World!"); + assert_eq!(json["label"], "v1.0"); +} + +// --------------------------------------------------------------------------- +// E2E 8: Boolean fields are preserved +// --------------------------------------------------------------------------- + +/// Boolean `true` and `false` must survive as JSON booleans (not strings or +/// integers). +#[test] +fn e2e_boolean_fields_round_trip() { + let source = r#"{ on = true, off = false, also_on = true }"#; + let json = loader() + .parse_string(source, "bool.ncl") + .expect("E2E: boolean config must parse"); + + assert_eq!(json["on"], true); + assert_eq!(json["off"], false); + assert_eq!(json["also_on"], true); +} + +// --------------------------------------------------------------------------- +// E2E 9: Multiple independent parse operations on same loader +// --------------------------------------------------------------------------- + +/// The `NickelLoader` is stateless between calls; parsing two different configs +/// in sequence on the same instance must produce independent, correct results. +#[test] +fn e2e_multiple_parses_independent() { + let l = loader(); + + let json_a = l + .parse_string(r#"{ id = 1, name = "alpha" }"#, "a.ncl") + .expect("E2E: config A must parse"); + let json_b = l + .parse_string(r#"{ id = 2, name = "beta" }"#, "b.ncl") + .expect("E2E: config B must parse"); + + // Results must be independent. + assert_eq!(json_a["id"], 1); + assert_eq!(json_a["name"], "alpha"); + assert_eq!(json_b["id"], 2); + assert_eq!(json_b["name"], "beta"); + assert_ne!(json_a, json_b); +} + +// --------------------------------------------------------------------------- +// E2E 10: validate then parse consistency +// --------------------------------------------------------------------------- + +/// For a valid source, `validate` must succeed and `parse_string` must also +/// succeed with a non-null JSON value. +#[test] +fn e2e_validate_then_parse_consistent() { + let source = r#"{ service = "auth", port = 9000, tls = false }"#; + let l = loader(); + + // Validation must succeed first. + l.validate(source, "consistent.ncl") + .expect("E2E: validate must succeed for valid source"); + + // Parsing must also succeed and produce a meaningful value. + let json = l + .parse_string(source, "consistent.ncl") + .expect("E2E: parse must succeed for valid source"); + + assert_eq!(json["service"], "auth"); + assert_eq!(json["port"], 9000); + assert_eq!(json["tls"], false); +} + +// --------------------------------------------------------------------------- +// E2E 11: file parse → JSON key count matches source +// --------------------------------------------------------------------------- + +/// The number of top-level keys in the parsed JSON must match the number of +/// fields declared in the Nickel source record. +#[test] +fn e2e_key_count_matches_source() { + // Source has exactly 5 top-level fields. + let source = r#"{ a = 1, b = 2, c = 3, d = 4, e = 5 }"#; + let json = loader() + .parse_string(source, "keycount.ncl") + .expect("E2E: key-count config must parse"); + + let obj = json.as_object().expect("JSON must be an object"); + assert_eq!(obj.len(), 5, "Exactly 5 keys expected"); +} + +// --------------------------------------------------------------------------- +// E2E 12: library constants are accessible through public API +// --------------------------------------------------------------------------- + +/// The public constants exported by the library (`NAME`, `RSR_TIER`) must +/// match the expected values at runtime, completing the "end-to-end" view +/// that the crate identity survives compilation. +#[test] +fn e2e_library_constants_reachable() { + assert_eq!(NAME, "bunsenite"); + assert_eq!(RSR_TIER, "bronze"); +} diff --git a/vendor/bunsenite/tests/integration_test.rs b/vendor/bunsenite/tests/integration_test.rs new file mode 100644 index 0000000..284a325 --- /dev/null +++ b/vendor/bunsenite/tests/integration_test.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Integration tests for Bunsenite — Nickel configuration parser +//! +//! These tests exercise the public API of the bunsenite crate, verifying +//! that configuration parsing, error handling, and metadata constants +//! behave correctly from an external consumer's perspective. + +use bunsenite::{Error, NickelLoader, NAME, RSR_TIER, TPCF_PERIMETER, VERSION}; + +/// Verify that NickelLoader can be constructed with default settings. +#[test] +fn test_loader_construction() { + let loader = NickelLoader::new(); + // Verbose defaults to false — just ensure construction succeeds + let _debug_repr = format!("{:?}", loader); +} + +/// Verify that NickelLoader supports the builder pattern for verbose mode. +#[test] +fn test_loader_verbose_builder() { + let loader = NickelLoader::new().with_verbose(true); + let debug_repr = format!("{:?}", loader); + assert!(debug_repr.contains("true"), "verbose should be enabled"); +} + +/// Verify that a simple valid Nickel record parses to JSON containing +/// the expected key-value pairs. +#[test] +fn test_parse_simple_record() { + let loader = NickelLoader::new(); + let input = r#"{ name = "bunsenite", version = "1.0.0" }"#; + let result = loader.parse_string(input, "simple.ncl"); + assert!( + result.is_ok(), + "Simple record should parse: {:?}", + result.err() + ); + let json = result.unwrap(); + assert!(json.to_string().contains("bunsenite")); + assert!(json.to_string().contains("1.0.0")); +} + +/// Verify that numeric values are preserved through parsing. +#[test] +fn test_parse_numeric_values() { + let loader = NickelLoader::new(); + let input = r#"{ port = 8080, retries = 3 }"#; + let result = loader.parse_string(input, "numeric.ncl"); + assert!( + result.is_ok(), + "Numeric record should parse: {:?}", + result.err() + ); + let json = result.unwrap(); + let text = json.to_string(); + assert!(text.contains("8080"), "Should contain port value"); + assert!(text.contains("3"), "Should contain retries value"); +} + +/// Verify that boolean values round-trip correctly. +#[test] +fn test_parse_boolean_values() { + let loader = NickelLoader::new(); + let input = r#"{ enabled = true, debug = false }"#; + let result = loader.parse_string(input, "bool.ncl"); + assert!( + result.is_ok(), + "Boolean record should parse: {:?}", + result.err() + ); +} + +/// Verify that empty records parse without error. +#[test] +fn test_parse_empty_record() { + let loader = NickelLoader::new(); + let input = "{}"; + let result = loader.parse_string(input, "empty.ncl"); + assert!( + result.is_ok(), + "Empty record should parse: {:?}", + result.err() + ); +} + +/// Verify that invalid Nickel syntax produces an error, not a panic. +#[test] +fn test_parse_invalid_syntax_returns_error() { + let loader = NickelLoader::new(); + let input = "this is not valid nickel @@@"; + let result = loader.parse_string(input, "invalid.ncl"); + assert!(result.is_err(), "Invalid syntax should produce an error"); +} + +/// Verify that the Error type correctly classifies recoverable errors. +#[test] +fn test_error_recoverability() { + let parse_err = Error::parse_error("test.ncl", "bad syntax"); + assert!(parse_err.is_recoverable(), "Parse errors are recoverable"); + + let internal_err = Error::internal("unexpected state"); + assert!( + !internal_err.is_recoverable(), + "Internal errors are not recoverable" + ); +} + +/// Verify crate metadata constants are correctly populated. +#[test] +fn test_crate_metadata() { + assert_eq!(NAME, "bunsenite"); + assert_eq!(RSR_TIER, "bronze"); + assert_eq!(TPCF_PERIMETER, 3); + // VERSION should be semver + let parts: Vec<&str> = VERSION.split('.').collect(); + assert_eq!(parts.len(), 3, "VERSION should be semver x.y.z"); +} diff --git a/vendor/bunsenite/tests/property_test.rs b/vendor/bunsenite/tests/property_test.rs new file mode 100644 index 0000000..4fc99f6 --- /dev/null +++ b/vendor/bunsenite/tests/property_test.rs @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +//! Property-based tests for Bunsenite (no external proptest crate required). +//! +//! Rather than using a generative property-test framework, these tests exercise +//! a fixed corpus of 10 varied data inputs and assert algebraic invariants: +//! +//! - **Round-trip**: `parse_string(x)` produces a JSON value; the same source +//! parsed again produces an equal value (determinism / idempotency of the +//! parse step). +//! - **Type invariants**: booleans remain booleans, numbers remain numbers, +//! strings remain strings, arrays remain arrays after parse. +//! - **No information loss**: all top-level keys declared in the source are +//! present in the JSON output. + +use bunsenite::NickelLoader; + +// --------------------------------------------------------------------------- +// Corpus: 10 varied Nickel configuration inputs +// --------------------------------------------------------------------------- + +/// Each entry is `(label, nickel_source)`. Entries cover: +/// 0. Minimal single field (string) +/// 1. Multiple scalars of different types +/// 2. Nested record +/// 3. Array of strings +/// 4. Array of integers +/// 5. Deep nesting (3 levels) +/// 6. Computed arithmetic +/// 7. String concatenation +/// 8. Mixed scalars + nested +/// 9. Boolean-only record +const CORPUS: &[(&str, &str)] = &[ + // 0 — single string field + ("single_string", r#"{ greeting = "hello" }"#), + // 1 — three scalar types + ( + "multi_scalar", + r#"{ name = "cfg", count = 42, active = true }"#, + ), + // 2 — nested record (one level) + ("nested_one", r#"{ outer = { inner = "value", num = 7 } }"#), + // 3 — array of strings + ("array_strings", r#"{ tags = ["a", "b", "c", "d"] }"#), + // 4 — array of integers + ("array_ints", r#"{ nums = [10, 20, 30] }"#), + // 5 — three-level nesting + ("deep_nest", r#"{ l1 = { l2 = { l3 = "leaf" } } }"#), + // 6 — computed arithmetic + ("computed", r#"{ x = 3 * 4, y = 100 - 1 }"#), + // 7 — string concatenation + ("concat", r#"{ s = "foo" ++ "bar" ++ "baz" }"#), + // 8 — mixed scalars and nested + ( + "mixed", + r#"{ host = "localhost", port = 5432, opts = { ssl = true } }"#, + ), + // 9 — booleans only + ("booleans", r#"{ yes = true, no = false }"#), +]; + +// --------------------------------------------------------------------------- +// Helper +// --------------------------------------------------------------------------- + +fn loader() -> NickelLoader { + NickelLoader::new() +} + +// --------------------------------------------------------------------------- +// Property 1: Every corpus entry parses without error +// --------------------------------------------------------------------------- + +/// All 10 corpus inputs must parse successfully: none of them should return +/// an `Err` variant. +#[test] +fn property_all_corpus_entries_parse() { + let l = loader(); + for (label, source) in CORPUS { + let result = l.parse_string(source, &format!("{label}.ncl")); + assert!( + result.is_ok(), + "Corpus entry '{label}' failed to parse: {:?}", + result.err() + ); + } +} + +// --------------------------------------------------------------------------- +// Property 2: Parsing the same input twice yields equal results (determinism) +// --------------------------------------------------------------------------- + +/// For each corpus entry, parsing it twice on the same loader must produce +/// two `serde_json::Value` instances that are equal. This verifies that +/// the parse → evaluate pipeline is deterministic (no random seeds, no +/// mutable shared state). +#[test] +fn property_parse_is_deterministic() { + let l = loader(); + for (label, source) in CORPUS { + let name = format!("{label}.ncl"); + let first = l + .parse_string(source, &name) + .unwrap_or_else(|e| panic!("First parse of '{label}' failed: {e}")); + let second = l + .parse_string(source, &name) + .unwrap_or_else(|e| panic!("Second parse of '{label}' failed: {e}")); + assert_eq!( + first, second, + "Parse of '{label}' is not deterministic: got different results" + ); + } +} + +// --------------------------------------------------------------------------- +// Property 3: Parsed JSON is always a JSON object (not null, array, scalar) +// --------------------------------------------------------------------------- + +/// Every corpus input is a Nickel record (`{ … }`). After evaluation the +/// result must be a JSON object, not `null`, a bare array, or a bare scalar. +#[test] +fn property_result_is_always_object() { + let l = loader(); + for (label, source) in CORPUS { + let json = l + .parse_string(source, &format!("{label}.ncl")) + .unwrap_or_else(|e| panic!("Parse of '{label}' failed: {e}")); + assert!( + json.is_object(), + "Corpus entry '{label}' did not produce a JSON object; got: {json:?}" + ); + } +} + +// --------------------------------------------------------------------------- +// Property 4: Boolean values remain boolean in JSON +// --------------------------------------------------------------------------- + +/// Corpus entry 9 (`booleans`) has two boolean fields. After parsing they +/// must be of JSON boolean type — not converted to strings or integers. +#[test] +fn property_booleans_stay_boolean() { + let l = loader(); + let (label, source) = CORPUS[9]; + let json = l + .parse_string(source, &format!("{label}.ncl")) + .unwrap_or_else(|e| panic!("Parse of '{label}' failed: {e}")); + + assert!( + json["yes"].is_boolean(), + "'yes' field should be boolean, got: {:?}", + json["yes"] + ); + assert!( + json["no"].is_boolean(), + "'no' field should be boolean, got: {:?}", + json["no"] + ); + assert_eq!(json["yes"], true); + assert_eq!(json["no"], false); +} + +// --------------------------------------------------------------------------- +// Property 5: Numeric values remain numeric in JSON +// --------------------------------------------------------------------------- + +/// Corpus entry 1 has an integer field `count = 42`. After parsing it must +/// be a JSON number, not a string. +#[test] +fn property_numbers_stay_numeric() { + let l = loader(); + let (label, source) = CORPUS[1]; + let json = l + .parse_string(source, &format!("{label}.ncl")) + .unwrap_or_else(|e| panic!("Parse of '{label}' failed: {e}")); + + assert!( + json["count"].is_number(), + "'count' should be numeric, got: {:?}", + json["count"] + ); + assert_eq!(json["count"], 42); +} + +// --------------------------------------------------------------------------- +// Property 6: String values remain strings in JSON +// --------------------------------------------------------------------------- + +/// Corpus entry 0 has `greeting = "hello"`. After parsing the field must be +/// a JSON string. +#[test] +fn property_strings_stay_strings() { + let l = loader(); + let (label, source) = CORPUS[0]; + let json = l + .parse_string(source, &format!("{label}.ncl")) + .unwrap_or_else(|e| panic!("Parse of '{label}' failed: {e}")); + + assert!( + json["greeting"].is_string(), + "'greeting' should be a string, got: {:?}", + json["greeting"] + ); + assert_eq!(json["greeting"], "hello"); +} + +// --------------------------------------------------------------------------- +// Property 7: Arrays remain arrays in JSON +// --------------------------------------------------------------------------- + +/// Corpus entry 3 has `tags = ["a", "b", "c", "d"]`. After parsing the +/// field must be a JSON array with the correct length. +#[test] +fn property_arrays_stay_arrays() { + let l = loader(); + let (label, source) = CORPUS[3]; + let json = l + .parse_string(source, &format!("{label}.ncl")) + .unwrap_or_else(|e| panic!("Parse of '{label}' failed: {e}")); + + let arr = json["tags"] + .as_array() + .unwrap_or_else(|| panic!("'tags' should be an array, got: {:?}", json["tags"])); + assert_eq!(arr.len(), 4, "Array should have 4 elements"); +} + +// --------------------------------------------------------------------------- +// Property 8: Computed expressions collapse to their expected values +// --------------------------------------------------------------------------- + +/// Corpus entry 6 (`computed`) contains `x = 3 * 4` and `y = 100 - 1`. +/// After evaluation these must be the integers `12` and `99` respectively. +#[test] +fn property_arithmetic_is_fully_evaluated() { + let l = loader(); + let (label, source) = CORPUS[6]; + let json = l + .parse_string(source, &format!("{label}.ncl")) + .unwrap_or_else(|e| panic!("Parse of '{label}' failed: {e}")); + + assert_eq!(json["x"], 12, "'x' should be 3*4=12"); + assert_eq!(json["y"], 99, "'y' should be 100-1=99"); +} + +// --------------------------------------------------------------------------- +// Property 9: String concatenation is fully evaluated +// --------------------------------------------------------------------------- + +/// Corpus entry 7 (`concat`) has `s = "foo" ++ "bar" ++ "baz"`. After +/// evaluation `s` must be the single string `"foobarbaz"`. +#[test] +fn property_concat_is_fully_evaluated() { + let l = loader(); + let (label, source) = CORPUS[7]; + let json = l + .parse_string(source, &format!("{label}.ncl")) + .unwrap_or_else(|e| panic!("Parse of '{label}' failed: {e}")); + + assert_eq!(json["s"], "foobarbaz"); +} + +// --------------------------------------------------------------------------- +// Property 10: Deep nesting is preserved at all levels +// --------------------------------------------------------------------------- + +/// Corpus entry 5 (`deep_nest`) declares three nesting levels. Each level +/// must be accessible as a JSON object, and the leaf value must be the +/// declared string. +#[test] +fn property_deep_nesting_preserved() { + let l = loader(); + let (label, source) = CORPUS[5]; + let json = l + .parse_string(source, &format!("{label}.ncl")) + .unwrap_or_else(|e| panic!("Parse of '{label}' failed: {e}")); + + assert!(json["l1"].is_object(), "l1 should be an object"); + assert!(json["l1"]["l2"].is_object(), "l1.l2 should be an object"); + assert_eq!(json["l1"]["l2"]["l3"], "leaf", "l1.l2.l3 should be 'leaf'"); +} diff --git a/vendor/bunsenite/validate-nickel-configs.k9.ncl b/vendor/bunsenite/validate-nickel-configs.k9.ncl new file mode 100644 index 0000000..e6a4c23 --- /dev/null +++ b/vendor/bunsenite/validate-nickel-configs.k9.ncl @@ -0,0 +1,148 @@ +K9! +# SPDX-License-Identifier: MPL-2.0 +# Bunsenite Configuration Validator +# +# This K9 component validates Nickel configuration files used in the Bunsenite +# project itself - dogfooding! It checks test fixtures, example configs, and +# ensures they conform to expected schemas. + +leash = 'Yard # Nickel evaluation only, no side effects + +pedigree = { + schema_version = "1.0.0", + component_type = "config-validator", + author = "Jonathan D.A. Jewell ", + description = "Validates Nickel configurations in Bunsenite test suite", + created = "2026-01-30", + k9_spec_version = "1.0.0", +} + +# Configuration paths and validation rules +config = { + # Directories containing Nickel files to validate + config_dirs | Array String = [ + "tests/fixtures", + "examples", + "benches/configs", + ], + + # Expected schemas for different config types + schemas = { + # Example application config schema + app_config = { + name | String, + version | String + | std.string.is_match "^[0-9]+\\.[0-9]+\\.[0-9]+$", + port | Number + | std.number.is_nat + | std.number.between 1024 65535, + features | Array String | default = [], + metadata | { _ : Dyn } | default = {}, + }, + + # Benchmark configuration schema + benchmark_config = { + name | String, + iterations | Number + | std.number.is_nat + | std.number.greater_or_eq 1, + input_size | Number | std.number.is_nat, + warmup_iterations | Number + | std.number.is_nat + | default = 10, + }, + + # Test fixture schema + test_fixture = { + description | String, + input | String, + expected_output | Dyn, + should_fail | Bool | default = false, + }, + }, + + # Validation rules for all configs + validation_rules = { + # All configs must be valid Nickel + valid_nickel = true, + + # No empty string values + no_empty_strings = true, + + # Port numbers must be in valid range + valid_ports = true, + + # Version strings must follow semver + semver_versions = true, + }, +} + +# Validation contracts +validation = { + # Example: Validate app config structure + validate_app_config = fun config_value => + let is_valid_name = std.string.length config_value.name > 0 in + let is_valid_version = + std.string.is_match "^[0-9]+\\.[0-9]+\\.[0-9]+" config_value.version + in + let is_valid_port = + config_value.port >= 1024 && config_value.port <= 65535 + in + is_valid_name && is_valid_version && is_valid_port + | doc "App config must have non-empty name, semver version, and valid port", + + # Check that all test fixtures have required fields + validate_test_fixture = fun fixture => + let has_description = std.string.length fixture.description > 0 in + let has_input = std.string.length fixture.input > 0 in + has_description && has_input + | doc "Test fixture must have description and input", + + # Validate benchmark config + validate_benchmark = fun bench => + let has_positive_iterations = bench.iterations > 0 in + let has_positive_input_size = bench.input_size > 0 in + has_positive_iterations && has_positive_input_size + | doc "Benchmark must have positive iterations and input_size", +} + +# Example validated configurations (used in tests) +examples = { + valid_app_config = { + name = "bunsenite-example", + version = "1.2.3", + port = 8080, + features = ["wasm", "ffi-bindings"], + metadata = { + author = "Jonathan D.A. Jewell", + license = "MPL-2.0", + }, + } | config.schemas.app_config, + + valid_benchmark = { + name = "parse_large_config", + iterations = 1000, + input_size = 10000, + warmup_iterations = 50, + } | config.schemas.benchmark_config, + + valid_test_fixture = { + description = "Parse simple object", + input = "{ name = \"test\", value = 42 }", + expected_output = { name = "test", value = 42 }, + should_fail = false, + } | config.schemas.test_fixture, +} + +# Dogfooding note: This K9 component validates Nickel configs using Nickel +# itself, demonstrating Bunsenite's purpose - parsing and validating Nickel! +metadata = { + dogfooding_note = "This K9 component is itself a Nickel configuration that validates other Nickel configurations, demonstrating the circular nature of configuration validation.", + + usage_example = " + # In Bunsenite tests, import this K9 component: + let k9_validator = import \"validate-nickel-configs.k9.ncl\" in + let my_config = { name = \"test\", version = \"1.0.0\", port = 3000 } in + k9_validator.validation.validate_app_config my_config + ", +} diff --git a/vendor/bunsenite/www/.well-known/ai.txt b/vendor/bunsenite/www/.well-known/ai.txt new file mode 100644 index 0000000..4dfb3d2 --- /dev/null +++ b/vendor/bunsenite/www/.well-known/ai.txt @@ -0,0 +1,140 @@ +# Bunsenite AI Training and Usage Policy +# https://site.spawning.ai/ai-txt +# This file declares how AI systems may interact with this repository + +# AI Training Permission +# ====================== +# We ALLOW AI training on this codebase under the following terms: + +ai-training: allowed + +# Conditions for AI Training +# --------------------------- +# 1. Attribution: Any AI model trained on this code should acknowledge +# the source in model documentation or training data listings. +# +# 2. License Compliance: Training data usage must comply with our dual +# MIT + Palimpsest License. Both licenses permit AI training. +# +# 3. Reversibility: If the AI generates code derived from this codebase, +# users should be informed of the source for traceability. +# +# 4. Ethical Use: AI systems trained on this code should not be used for: +# - Malicious purposes (malware, exploits) +# - Surveillance or oppression +# - Discriminatory applications +# - Weaponization + +# AI Code Generation +# ================== +# We ALLOW AI systems to generate code suggestions based on this codebase. + +ai-code-generation: allowed + +# Conditions for Code Generation +# ------------------------------- +# 1. License Notice: Generated code should include appropriate license +# attribution if substantial portions are derived from Bunsenite. +# +# 2. Quality: AI-generated code should maintain the quality standards +# documented in CONTRIBUTING.md (type safety, memory safety, tests). +# +# 3. Security: AI should not suggest unsafe code patterns, especially +# given our #![deny(unsafe_code)] policy. +# +# 4. Context: AI assistants should reference CLAUDE.md for project +# context and conventions when generating code. + +# AI Assisted Development +# ======================== +# We ENCOURAGE AI-assisted development with these guidelines: + +ai-assisted-development: encouraged + +# Guidelines for AI Assistants +# ----------------------------- +# 1. Read CLAUDE.md First: This file contains comprehensive project +# context, conventions, and critical design decisions. +# +# 2. Respect Technology Choices: +# - YES: Rust core, Zig FFI, Deno, Rescript, WASM +# - NO: Plain TypeScript, shell scripts, unsafe code +# +# 3. Follow Standards: +# - RSR Bronze Tier compliance +# - TPCF Perimeter 3 (Community Sandbox) +# - Conventional Commits +# - Rust API Guidelines +# +# 4. Maintain Safety: +# - No unsafe blocks +# - Comprehensive error handling (Result types) +# - Tests for all new code +# +# 5. Preserve Reversibility: +# - Clear commit messages +# - Documented rationale for changes +# - Git history integrity + +# AI Research +# =========== +# We ALLOW research on this codebase, including: + +ai-research: allowed + +# Research Applications +# ---------------------- +# - Code analysis and understanding +# - Bug detection and security analysis +# - Performance optimization suggestions +# - Documentation generation and improvement +# - Test generation and coverage analysis +# - Refactoring suggestions +# - Architecture analysis +# - Dependency analysis + +# Data Mining and Scraping +# ========================= +# We ALLOW responsible data mining and scraping: + +web-scraping: allowed + +# Conditions for Scraping +# ------------------------ +# 1. Respect Rate Limits: Don't overwhelm GitLab infrastructure +# 2. Attribution: Acknowledge the source in publications/datasets +# 3. License Compliance: Respect dual MIT + Palimpsest licensing +# 4. Ethical Use: No malicious or discriminatory applications + +# Contact Information +# =================== +# For questions about AI usage of this codebase: + +contact: https://github.com/hyperpolymath/bunsenite/issues +contact: https://gitlab.com/hyperpolymath/bunsenite/-/issues + +# Additional Resources +# ==================== +# - Project: https://github.com/hyperpolymath/bunsenite +# - License: LICENSE (dual MIT + Palimpsest 0.8) +# - AI Guide: CLAUDE.md +# - Contributing: CONTRIBUTING.md +# - Security: SECURITY.md + +# Version and Expiration +# ======================= +version: 1.0.0 +last-updated: 2025-12-18 +review-date: 2026-12-18 + +# Notes +# ===== +# This policy reflects our values of: +# - Openness: Share knowledge freely +# - Reversibility: Enable traceability and learning +# - Emotional Safety: Reduce anxiety through clear permissions +# - Political Autonomy: Communities control their own technical destiny +# +# We believe AI can amplify human creativity when used ethically and +# transparently. This policy aims to maximize benefit while preserving +# our community values. diff --git a/vendor/bunsenite/www/.well-known/dc.xml b/vendor/bunsenite/www/.well-known/dc.xml new file mode 100644 index 0000000..7cb1186 --- /dev/null +++ b/vendor/bunsenite/www/.well-known/dc.xml @@ -0,0 +1,23 @@ + + + bunsenite + Jonathan D.A. Jewell + software-development + RSR + Rhodium Standard + Chemical process simulation and lab automation + Rhodium Standard + Jonathan D.A. Jewell + 2025 + Software + application/octet-stream + https://github.com/hyperpolymath/bunsenite + https://github.com/hyperpolymath/bunsenite + en + https://rhodium.sh + AGPL-3.0-or-later OR LicenseRef-Palimpsest-0.5 + https://spdx.org/licenses/AGPL-3.0-or-later.html + diff --git a/vendor/bunsenite/www/.well-known/humans.txt b/vendor/bunsenite/www/.well-known/humans.txt new file mode 100644 index 0000000..72205d9 --- /dev/null +++ b/vendor/bunsenite/www/.well-known/humans.txt @@ -0,0 +1,205 @@ +# humanstxt.org +# The humans responsible for building Bunsenite + +/* TEAM */ + +Project Lead & Founder: Campaign for Cooler Coding and Programming +Location: Worldwide +GitHub: @hyperpolymath +GitLab: @hyperpolymath + +/* THANKS */ + +Nickel Language Team + For creating an excellent configuration language + https://github.com/tweag/nickel + +RSR Framework Contributors + For defining rigorous repository standards + +TPCF Community + For the Tri-Perimeter Contribution Framework + +Rust Community + For an incredible language and ecosystem + +Early Adopters & Testers + Your feedback makes this project better + +All Contributors (Perimeter 3) + Every contribution matters, thank you! + +/* SITE */ + +Last updated: 2025-12-18 +Standards: RSR Bronze Tier, TPCF Perimeter 3 +Languages: Rust, Zig, TypeScript (Deno), Rescript, WebAssembly +Doctype: GitLab Repository +IDE: Your choice! We support all editors + +/* VALUES */ + +Reversibility: All changes tracked, experimentation encouraged +Emotional Safety: Mistakes are learning opportunities +Political Autonomy: Communities control their technical destiny +Type Safety: Compile-time guarantees via Rust +Memory Safety: Ownership model, zero unsafe code +Offline-First: Works air-gapped, no network dependencies + +/* LICENSE */ + +Dual MIT + Palimpsest License v0.8 +Choose whichever works best for your use case! + +MIT: Maximum permissiveness +Palimpsest: Adds reversibility, emotional safety, political autonomy + +See LICENSE file for full terms + +/* PROJECT */ + +Name: Bunsenite +Version: 1.0.2 +Description: Nickel configuration file parser with multi-language FFI bindings +Repository: https://github.com/hyperpolymath/bunsenite +Homepage: https://github.com/hyperpolymath/bunsenite +Issues: https://github.com/hyperpolymath/bunsenite/issues +CI/CD: GitHub Actions + GitLab CI +Package: crates.io/crates/bunsenite + +/* ARCHITECTURE */ + +Core: Rust (nickel-lang-core 0.9.1) +FFI Layer: Zig C ABI (stable interface) +Bindings: Deno, Rescript, WebAssembly +CLI: Command-line interface +WASM: Browser deployment (~95% native speed) +Tests: 30+ tests, 100% pass rate + +/* TECHNOLOGY */ + +Rust (2021 edition) + Zero unsafe code + Strong typing + Memory safety via ownership + +nickel-lang-core 0.9.1 + Configuration language + Type checking + Evaluation + +Zig + C ABI layer + FFI stability + +wasm-bindgen + Browser deployment + Universal compatibility + +Deno + TypeScript runtime + Native FFI + +Rescript + Type-safe JavaScript + C FFI support + +/* BUILD TOOLS */ + +Cargo: Rust package manager +just: Command runner (Justfile) +Guix: Reproducible builds (flake.guix) +GitLab CI: Continuous integration +wasm-pack: WebAssembly builds (optional) + +/* STANDARDS COMPLIANCE */ + +RSR Framework: Bronze Tier + ✓ Type safety (Rust compiler) + ✓ Memory safety (ownership, no unsafe) + ✓ Offline-first (no network deps) + ✓ Complete documentation + ✓ .well-known/ directory + ✓ Build system (Justfile, Guix) + ✓ CI/CD pipeline + ✓ 100% test pass rate + +TPCF: Perimeter 3 (Community Sandbox) + Open to all contributors + Graduated trust model + Reversibility guaranteed + +/* CONTRIBUTING */ + +We welcome contributions! + +Perimeter 1: Core maintainers +Perimeter 2: Trusted contributors +Perimeter 3: Community sandbox (you are here!) + +See CONTRIBUTING.md for: + - Development workflow + - Coding standards + - Testing requirements + - Commit conventions + - PR process + +See CODE_OF_CONDUCT.md for community guidelines + +/* SECURITY */ + +Zero unsafe code: Enforced by compiler +Memory safety: Rust ownership model +Type safety: Compile-time guarantees +No network deps: Offline-first design +Regular audits: cargo audit in CI + +Report vulnerabilities: + GitHub: https://github.com/hyperpolymath/bunsenite/security/advisories/new + GitLab: Confidential issue + Response: Within 48 hours + +See SECURITY.md for full policy + +/* CONTACT */ + +General: https://github.com/hyperpolymath/bunsenite/issues +Security: https://github.com/hyperpolymath/bunsenite/security/advisories/new +GitHub: https://github.com/hyperpolymath +GitLab: https://gitlab.com/hyperpolymath + +/* ATTRIBUTION */ + +This project stands on the shoulders of giants. + +Thank you to everyone who has contributed to: + - Rust language and ecosystem + - Nickel configuration language + - Open source software movement + - Free/libre software philosophy + - Standards and best practices + - Educational resources + - Community support and mentorship + +We are part of a larger movement toward: + - Safer software (memory & type safety) + - Reversible development (Git, version control) + - Emotional safety (anxiety reduction, experimentation) + - Political autonomy (community sovereignty) + - Ethical technology (transparency, consent) + +/* PHILOSOPHY */ + +"Software should be: + - Safe (memory & type safety) + - Traceable (reversibility) + - Empowering (emotional safety) + - Autonomous (political freedom) + - Collaborative (community-driven)" + +- Campaign for Cooler Coding and Programming + +/* END */ + +Made with care by humans, for humans. +Politically autonomous software for emotionally safe development. diff --git a/vendor/bunsenite/www/.well-known/security.txt b/vendor/bunsenite/www/.well-known/security.txt new file mode 100644 index 0000000..dd6ae64 --- /dev/null +++ b/vendor/bunsenite/www/.well-known/security.txt @@ -0,0 +1,31 @@ +# Bunsenite Security Contact Information +# RFC 9116 Compliant security.txt file +# https://securitytxt.org/ + +Contact: https://github.com/hyperpolymath/bunsenite/security/advisories/new +Contact: https://gitlab.com/hyperpolymath/bunsenite/-/issues/new?issuable_template=security +Expires: 2026-12-18T00:00:00.000Z +Preferred-Languages: en +Canonical: https://github.com/hyperpolymath/bunsenite/blob/main/.well-known/security.txt +Policy: https://github.com/hyperpolymath/bunsenite/blob/main/SECURITY.md +Acknowledgments: https://github.com/hyperpolymath/bunsenite/blob/main/SECURITY.md#attribution + +# Vulnerability Disclosure Program +# +# We take security seriously. Please report security vulnerabilities +# via GitHub Security Advisories (preferred) or confidential GitLab issue. +# +# Response Timeline: +# - Initial response: Within 48 hours +# - Triage: Within 1 week +# - Fix: Depends on severity (critical: days, low: weeks) +# - Public disclosure: Coordinated, typically 90 days after fix +# +# Security Guarantees: +# - Zero unsafe code (#![deny(unsafe_code)]) +# - Rust memory safety (ownership model) +# - Type safety (compile-time guarantees) +# - No network dependencies (offline-first) +# - Minimal attack surface +# +# For more information, see: SECURITY.md diff --git a/vendor/bunsenite/zig/README.adoc b/vendor/bunsenite/zig/README.adoc new file mode 100644 index 0000000..63b7f35 --- /dev/null +++ b/vendor/bunsenite/zig/README.adoc @@ -0,0 +1,90 @@ +== Bunsenite Zig FFI Layer + +image:https://img.shields.io/badge/License-MPL–2.0-blue.svg[License: +MPL-2.0,link="`https://github.com/hyperpolymath/palimpsest-license`"] + +This directory contains the Zig wrapper that provides a stable C ABI for +the Rust core library. + +=== Purpose + +The Zig layer isolates consumers (Deno, AffineScript) from Rust ABI +changes across compiler versions, providing: + +* *Stable C ABI*: Guaranteed binary compatibility +* *Cross-platform*: Builds for Linux, macOS, Windows +* *Small overhead*: Thin wrapper, minimal performance impact + +=== Architecture + +.... +Deno/AffineScript → Zig (stable C ABI) → Rust (native) +.... + +=== Prerequisites + +[arabic] +. *Rust toolchain*: `+rustup install stable+` +. *Zig compiler*: `+zig version+` (0.11.0 or later recommended) + +=== Building + +[source,bash] +---- +# Build Rust library first +cargo build --release + +# Build Zig FFI layer +cd zig +zig build -Doptimize=ReleaseFast +---- + +Output libraries: - Linux: `+zig-out/lib/libbunsenite.so+` - macOS: +`+zig-out/lib/libbunsenite.dylib+` - Windows: +`+zig-out/lib/bunsenite.dll+` + +=== Exported Symbols + +[width="100%",cols="21%,28%,21%,30%",options="header",] +|=== +|Symbol |Parameters |Returns |Description +|`+parse_nickel+` |`+(source, name)+` |`+char*+` |Parse Nickel to JSON + +|`+validate_nickel+` |`+(source, name)+` |`+int+` |Validate config +(0=ok) + +|`+free_string+` |`+(ptr)+` |`+void+` |Free allocated string + +|`+version+` |`+()+` |`+char*+` |Library version + +|`+rsr_tier+` |`+()+` |`+char*+` |RSR compliance tier + +|`+tpcf_perimeter+` |`+()+` |`+u8+` |TPCF perimeter number +|=== + +=== Testing + +[source,bash] +---- +# Run Zig tests (requires Rust library) +cargo build --release +cd zig && zig build test +---- + +=== Integration + +==== Deno + +The Zig library is used by `+bindings/deno/bunsenite.ts+` via +`+Deno.dlopen()+`. + +==== AffineScript + +The Zig library is used by `+bindings/affinescript/Bunsenite.res+` via C +FFI. + +=== RSR Compliance + +This FFI layer maintains RSR Bronze tier compliance: - Type safety +through Zig’s type system - Memory safety with explicit +allocation/deallocation - No network dependencies diff --git a/vendor/bunsenite/zig/build.zig b/vendor/bunsenite/zig/build.zig new file mode 100644 index 0000000..0e63ae1 --- /dev/null +++ b/vendor/bunsenite/zig/build.zig @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// Bunsenite Zig Build Configuration +// +// Build the Zig FFI layer as a shared library that wraps the Rust core. +// +// Prerequisites: +// 1. Build Rust library first: cargo build --release +// 2. Then build Zig layer: zig build -Doptimize=ReleaseFast +// +// Output: +// zig-out/lib/libbunsenite.so (Linux) +// zig-out/lib/libbunsenite.dylib (macOS) +// zig-out/lib/bunsenite.dll (Windows) + +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Create shared library + const lib = b.addSharedLibrary(.{ + .name = "bunsenite", + .root_source_file = b.path("bunsenite.zig"), + .target = target, + .optimize = optimize, + }); + + // Link to Rust library + // The Rust cdylib is built with: cargo build --release + lib.addLibraryPath(b.path("../target/release")); + lib.linkSystemLibrary("bunsenite"); + + // Link libc for C runtime + lib.linkLibC(); + + // Install the library + b.installArtifact(lib); + + // Create test step + const lib_unit_tests = b.addTest(.{ + .root_source_file = b.path("bunsenite.zig"), + .target = target, + .optimize = optimize, + }); + + lib_unit_tests.addLibraryPath(b.path("../target/release")); + lib_unit_tests.linkSystemLibrary("bunsenite"); + lib_unit_tests.linkLibC(); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); +} diff --git a/vendor/bunsenite/zig/bunsenite.zig b/vendor/bunsenite/zig/bunsenite.zig new file mode 100644 index 0000000..b261ee8 --- /dev/null +++ b/vendor/bunsenite/zig/bunsenite.zig @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// Bunsenite Zig FFI Layer +// +// This module provides a stable C ABI wrapper around the Rust core library. +// It isolates consumers (Deno, ReScript) from Rust ABI changes across versions. +// +// Architecture: +// Deno/ReScript → Zig (stable C ABI) → Rust (native) +// +// Build: +// zig build -Doptimize=ReleaseFast +// +// The resulting shared library exports these symbols: +// - parse_nickel(source, name) -> char* +// - validate_nickel(source, name) -> int +// - free_string(ptr) -> void +// - version() -> char* +// - rsr_tier() -> char* +// - tpcf_perimeter() -> u8 + +const std = @import("std"); + +// Import Rust FFI functions via C ABI +// These are defined in src/ffi.rs with #[no_mangle] pub extern "C" +extern fn bunsenite_parse(source: [*:0]const u8, name: [*:0]const u8) callconv(.C) ?[*:0]u8; +extern fn bunsenite_validate(source: [*:0]const u8, name: [*:0]const u8) callconv(.C) i32; +extern fn bunsenite_free_string(ptr: ?[*:0]u8) callconv(.C) void; +extern fn bunsenite_version() callconv(.C) [*:0]const u8; +extern fn bunsenite_rsr_tier() callconv(.C) [*:0]const u8; +extern fn bunsenite_tpcf_perimeter() callconv(.C) u8; + +// Re-export with stable, consumer-friendly names +// These match the symbols expected by bindings/deno/bunsenite.ts + +/// Parse a Nickel configuration string and return JSON +/// +/// Parameters: +/// source: Null-terminated Nickel source code +/// name: Null-terminated filename (for error messages) +/// +/// Returns: +/// Pointer to JSON string on success, null on failure +/// MUST be freed with free_string() +pub export fn parse_nickel(source: [*:0]const u8, name: [*:0]const u8) callconv(.C) ?[*:0]u8 { + return bunsenite_parse(source, name); +} + +/// Validate a Nickel configuration without evaluating +/// +/// Parameters: +/// source: Null-terminated Nickel source code +/// name: Null-terminated filename (for error messages) +/// +/// Returns: +/// 0 on success (valid) +/// 1 on validation error +/// -1 on invalid input +pub export fn validate_nickel(source: [*:0]const u8, name: [*:0]const u8) callconv(.C) i32 { + return bunsenite_validate(source, name); +} + +/// Free a string allocated by parse_nickel +/// +/// Parameters: +/// ptr: Pointer returned by parse_nickel (may be null) +pub export fn free_string(ptr: ?[*:0]u8) callconv(.C) void { + bunsenite_free_string(ptr); +} + +/// Get the library version +/// +/// Returns: +/// Static string pointer (do NOT free) +pub export fn version() callconv(.C) [*:0]const u8 { + return bunsenite_version(); +} + +/// Get the RSR compliance tier +/// +/// Returns: +/// Static string pointer (do NOT free) +pub export fn rsr_tier() callconv(.C) [*:0]const u8 { + return bunsenite_rsr_tier(); +} + +/// Get the TPCF perimeter number +/// +/// Returns: +/// 3 for Community Sandbox +pub export fn tpcf_perimeter() callconv(.C) u8 { + return bunsenite_tpcf_perimeter(); +} + +// Test the FFI layer +test "version returns non-empty string" { + const ver = version(); + try std.testing.expect(ver[0] != 0); +} + +test "rsr_tier returns bronze" { + const tier = rsr_tier(); + const expected = "bronze"; + var i: usize = 0; + while (i < expected.len) : (i += 1) { + try std.testing.expectEqual(expected[i], tier[i]); + } +} + +test "tpcf_perimeter returns 3" { + try std.testing.expectEqual(@as(u8, 3), tpcf_perimeter()); +}