From 6103004cd498a12954d4f34675157f43ac2b6ce6 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:46:07 +0000 Subject: [PATCH 1/2] fix: harden evidence boundaries and hold premature launch Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .github/CONTRIBUTING.md | 6 + .github/workflows/ci.yml | 22 +- .github/workflows/publish-image.yml | 14 +- .machine_readable/descriptiles/ECOSYSTEM.a2ml | 6 + .machine_readable/descriptiles/META.a2ml | 13 +- .machine_readable/descriptiles/NEUROSYM.a2ml | 16 +- .machine_readable/descriptiles/STATE.a2ml | 23 +- CONTRIBUTING.adoc | 58 ++ Containerfile | 10 +- DEBT.adoc | 14 + DEPLOY.adoc | 60 +- Justfile | 4 + QUICKSTART.adoc | 134 +---- README.adoc | 293 ++-------- action.yml | 26 +- crates/oikosbot-analysis/README.adoc | 86 +-- crates/oikosbot-analysis/src/analyzer.rs | 29 +- crates/oikosbot-analysis/src/calibration.rs | 39 +- crates/oikosbot-analysis/src/dependencies.rs | 1 + crates/oikosbot-analysis/src/security.rs | 186 +----- crates/oikosbot-cli/README.adoc | 72 +-- crates/oikosbot-cli/src/config.rs | 42 +- crates/oikosbot-cli/src/estate.rs | 99 +++- crates/oikosbot-cli/src/main.rs | 259 ++++----- crates/oikosbot-cli/tests/check_gate.rs | 173 +++++- crates/oikosbot-eclexia/src/lib.rs | 24 +- crates/oikosbot-fleet/src/lib.rs | 1 + crates/oikosbot-metrics/README.adoc | 17 +- crates/oikosbot-metrics/src/lib.rs | 63 ++- crates/oikosbot-pareto/README.adoc | 16 +- crates/oikosbot-pareto/src/contract.rs | 305 ++++++++++ crates/oikosbot-pareto/src/lib.rs | 107 +++- .../oikosbot-pareto/tests/trust_boundary.rs | 98 ++++ crates/oikosbot-sarif/README.adoc | 2 +- crates/oikosbot-sarif/src/lib.rs | 40 +- crates/oikosbot-telemetry/README.adoc | 2 +- crates/oikosbot-telemetry/src/collect.rs | 235 ++++++-- crates/oikosbot-telemetry/src/derive.rs | 11 +- docs/HANDOFF.adoc | 105 ++++ docs/LAUNCH-READINESS.adoc | 183 ++++++ docs/README.adoc | 27 +- docs/STATUS.adoc | 234 ++------ docs/architecture.adoc | 23 + docs/ci-runbook.adoc | 23 +- docs/policies.adoc | 28 + docs/research/EVIDENCE-SCOPED-PARETO.adoc | 240 ++++++++ docs/usage.adoc | 529 ++++++------------ policies/README.adoc | 14 +- policies/finding_taxonomy.ecl | 11 +- rust-toolchain.toml | 6 + tests/test-action-arguments.sh | 37 ++ tests/test-container-contract.sh | 34 ++ tools/ci/release-preflight.sh | 16 + 53 files changed, 2526 insertions(+), 1590 deletions(-) create mode 100644 CONTRIBUTING.adoc create mode 100644 crates/oikosbot-pareto/src/contract.rs create mode 100644 crates/oikosbot-pareto/tests/trust_boundary.rs create mode 100644 docs/HANDOFF.adoc create mode 100644 docs/LAUNCH-READINESS.adoc create mode 100644 docs/architecture.adoc create mode 100644 docs/policies.adoc create mode 100644 docs/research/EVIDENCE-SCOPED-PARETO.adoc create mode 100644 rust-toolchain.toml create mode 100755 tests/test-action-arguments.sh create mode 100755 tests/test-container-contract.sh create mode 100755 tools/ci/release-preflight.sh diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 553670b..c79a4d2 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,3 +19,9 @@ This project follows the [Rhodium Standard Repository By contributing, you agree that your contributions will be licensed under MPL-2.0 (with MPL-2.0 as automatic legal fallback). + +## Development guide + +See [CONTRIBUTING.adoc](../CONTRIBUTING.adoc) for the build walkthrough, +code/test conventions, metadata workflow and release/fleet coordination. +See [launch readiness](../docs/LAUNCH-READINESS.adoc) before proposing a release. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 749af93..58c1dd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,15 +59,17 @@ jobs: ~/.cargo/git target key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} - # Uses the runner's preinstalled stable toolchain (no third-party action). + # rustup honors rust-toolchain.toml; no third-party toolchain action. # NB: the default workspace deliberately EXCLUDES crates/oikosbot-fleet so # OikosBot builds with no dependency on hyperpolymath/gitbot-fleet. - name: Format check run: cargo fmt --check - name: Build (standalone workspace) run: cargo build --workspace --locked - - name: Test (standalone workspace) + - name: Test default standalone workspace run: cargo test --workspace --locked + - name: Test all retained features + run: cargo test --workspace --all-features --locked - name: Eclexia adapter feature and rejection checks run: bash tests/test-eclexia-adapter.sh - name: Clippy (informational) @@ -82,6 +84,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7.0.1 + - name: Composite Action argument contract + run: bash tests/test-action-arguments.sh - name: Check shell wrapper syntax run: bash -n bot-integration-affine/check.sh security: @@ -108,3 +112,17 @@ jobs: with: sarif_file: "trivy-results.sarif" continue-on-error: true + container-contract: + name: Container smoke contract + needs: rust + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.1 + - name: Build candidate + run: docker build -f Containerfile -t oikosbot:verify . + - name: Paired clean and firing fixtures, version and non-root execution + run: bash tests/test-container-contract.sh oikosbot:verify diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 59e1107..3e40e2c 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -16,6 +16,8 @@ on: - 'Cargo.toml' - 'Cargo.lock' - 'Containerfile' + - 'rust-toolchain.toml' + - 'tests/test-container-contract.sh' - '.dockerignore' - '.github/workflows/publish-image.yml' workflow_dispatch: {} @@ -53,9 +55,19 @@ jobs: flavor: | latest=false tags: | - type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=development,enable={{is_default_branch}} type=semver,pattern={{version}} type=sha + - name: Build candidate for smoke contract + uses: docker/build-push-action@v7.4.0 + with: + context: . + file: ./Containerfile + load: true + tags: oikosbot:verify + cache-from: type=gha + - name: Test candidate contract + run: bash tests/test-container-contract.sh oikosbot:verify - name: Build and push id: push uses: docker/build-push-action@v7.4.0 diff --git a/.machine_readable/descriptiles/ECOSYSTEM.a2ml b/.machine_readable/descriptiles/ECOSYSTEM.a2ml index 0c905f8..8480074 100644 --- a/.machine_readable/descriptiles/ECOSYSTEM.a2ml +++ b/.machine_readable/descriptiles/ECOSYSTEM.a2ml @@ -31,3 +31,9 @@ projects = [ { name = "eclexia", repo = "hyperpolymath/eclexia", relation = "policy-language + future-host", note = "Turing-complete general-purpose language implementing the Economics-as-Code paradigm; far broader in scope than this bot. Today: the .ecl policy surface (currently evaluated by a filename-matching FAKE backend — see DEBT.adoc). Intended trajectory: OikosBot's dominance/frontier maths moves into the Eclexia stdlib so its currently-inert @optimize becomes semantically real for every Eclexia program; OikosBot's DEA duals become the real solver behind shadow_price(), replacing hand-set constants; and OikosBot is eventually IMPLEMENTED IN Eclexia. OikosBot is its first serious application, not its owner." }, { name = "standards", repo = "hyperpolymath/standards", relation = "reusable-workflows", note = "Supplies the reusable workflows this repo calls (mirror, secret-scanner, scorecard, governance). Pin bumps here are estate-coordinated." } ] + +[pareto-research-audit] +date = "2026-09-26" +reference = "docs/research/EVIDENCE-SCOPED-PARETO.adoc" +related = ["eclexia", "echo-types", "residual-evidence-types", "epistemic-types", "tropical-types", "choreographic-types", "absolute-zero", "januskey", "proven-tests-and-benches"] +relationship = "Pinned source inspection and conceptual comparison only; no imported proof or new runtime dependency." diff --git a/.machine_readable/descriptiles/META.a2ml b/.machine_readable/descriptiles/META.a2ml index b0f5275..fbbaa22 100644 --- a/.machine_readable/descriptiles/META.a2ml +++ b/.machine_readable/descriptiles/META.a2ml @@ -25,6 +25,13 @@ build-tool = "just" [maintenance-axes] scoping-first = true -axis-1 = "must > intend > like" -axis-2 = "corrective > adaptive > perfective" -axis-3 = "systems > compliance > effects" +canonical = "NEUROSYM.a2ml [finding-taxonomy]" +axis-1 = "must > intend > wish" +axis-2 = "corrective | adaptive | perfective | preventive" +axis-3 = "systems | compliance | externalities" + +[decision-contract-2026-09-26] +status = "proposed-implementation-awaiting-verification" +reference = "docs/research/EVIDENCE-SCOPED-PARETO.adoc" +principle = "Separate modeled feasibility, robust comparison, evidence adequacy and policy authorization; do not infer must/auto-fix authority from confidence." +launch = "hold-general-launch-and-regulator-claims" diff --git a/.machine_readable/descriptiles/NEUROSYM.a2ml b/.machine_readable/descriptiles/NEUROSYM.a2ml index a9f1e5d..bb40a96 100644 --- a/.machine_readable/descriptiles/NEUROSYM.a2ml +++ b/.machine_readable/descriptiles/NEUROSYM.a2ml @@ -21,21 +21,23 @@ report-format = "logtalk" # model = "hypatia-v2" [finding-taxonomy] -# Every OikosBot finding is classified on three orthogonal axes, layered -# intent -> action -> outcome. The intent axis is executable Eclexia -# (policies/finding_taxonomy.ecl); see policies/README.adoc for the prose. +# Finding vocabulary (unclassified legacy/synthetic findings may omit tags). +# Pattern findings are classified on three orthogonal axes, layered +# intent -> action -> outcome. The legacy numeric example is Eclexia +# (policies/finding_taxonomy.ecl), not executed by the builtin Rust adapter. axes = ["intent", "maintenance", "locus"] [finding-taxonomy.intent] -# Deontic force. DERIVED from the finding's confidence score (not hand-tagged), -# and identical to the gitbot-fleet Safety Triangle and its thresholds. +# Canonical vocabulary; categories do not grant mutation authority. +# Resource Confidence is not a probability. Current Rust heuristic rules +# emit wish; future promotion requires validated evidence AND policy authority. values = ["must", "intend", "wish"] -must = { triangle = "eliminate", confidence = ">=0.95", disposition = "auto-fix" } +must = { triangle = "eliminate", confidence = ">=0.95", disposition = "policy-review-required" } intend = { triangle = "substitute", confidence = ">=0.85", disposition = "review" } wish = { triangle = "control", confidence = "<0.85", disposition = "human" } [finding-taxonomy.maintenance] -# Why the change is made (Swanson 1976 / ISO/IEC 14764). Hand-tagged per rule. +# Unordered categories. Why the change is made (Swanson 1976 / ISO/IEC 14764). Hand-tagged per rule. values = ["corrective", "adaptive", "perfective", "preventive"] [finding-taxonomy.locus] diff --git a/.machine_readable/descriptiles/STATE.a2ml b/.machine_readable/descriptiles/STATE.a2ml index 3fa3f6b..bd8a80d 100644 --- a/.machine_readable/descriptiles/STATE.a2ml +++ b/.machine_readable/descriptiles/STATE.a2ml @@ -7,7 +7,7 @@ project = "oikosbot" version = "0.1.0-dev" last-updated = "2026-09-26" status = "active" -session = "2026-08-07 documentation + debt audit: DEBT.adoc written (licence/docs/code/proof/CI-CD, every item evidenced); removed an UNEARNED OpenSSF Best Practices badge (API returns empty — never registered); deleted ARCHITECTURE.md and GOVERNANCE.md (generic boilerplate describing separate src/ and tests/ directories this repo does not have, shadowing the real .adoc versions); deleted orphaned LICENSES/AGPL-3.0-or-later.txt (no file declares it); fixed docs/README.adoc's DUPLICATED SPDX header (MPL-2.0 on line 1 shadowing CC-BY-SA-4.0 on line 2 — a doc licensed as code, and a class of defect that passes the presence check while asserting the wrong licence); ARCHITECTURE.adoc banner-flagged as TARGET design with each unbuilt component named; tech-debt-2026-05-26.adoc marked superseded; wiki built out from a one-line stub; repo description and topics set. Prior session 2026-08-03/04 estate economics round one: #60 merged (oikosbot-telemetry/-capability/-dea + estate CLI), oikosbot-estate snapshot repo created, #65 in review (path-keyed gate detection). Prior: 2026-07-28 generation-1 go-live: Pareto engine made executable (#42 — crates/oikosbot-pareto: ε-tolerant dominance, normalized weighted frontier, base-vs-head verdicts with confidence gating, oikosbot compare, SARIF pareto_* properties, EconScore composition per ARCHITECTURE); .oikos.yml --config + auto-discovery (estate configs + governance flag now real); push-email-notify removed (dual-use ruling); publish-image root-caused to GHCR package access (permission_denied: write_package — owner grant, Containerfile verified sound via podman); composite action.yml + docs/COMPARISON-climate-warrior.adoc. Rulings: Action-mode first then App; NO interim listener (upstream AffineScript Http::Server instead); advisor + machine-checked trade-offs; Scallop replaces DeepProbLog. Prior session: 2026-06-21 close-out of the post-extraction work: fleet bridge → BotId::Oikosbot + ReScript-era containers removed (#5); finding taxonomy in NEUROSYM.a2ml [finding-taxonomy] + policies/finding_taxonomy.ecl (#9); robot-repo-automaton build fix + Rust build/test/clippy CI gate (gitbot-fleet); stale-identity sweep across SECURITY/CLAUDE/META (#11); standards reusable-workflow pin refresh (#13); LICENSE dual-SPDX MPL-2.0 + CC-BY-SA-4.0, SECURITY.md finalized (reporting → j.d.a.jewell@open.ac.uk), redundant trufflehog job dropped (#14); added docs/README.adoc documentation map. Open follow-ups: #12 (taxonomy vocab reconciliation), #16 (developer+maintainer docs + README split), #17 (end-user docs), #18 (taxonomy tags through Finding types). Session 2026-09-26 (branch arena/01a0dad6-oikosbot): two long-standing red checks root-caused and fixed — Hypatia's single high/critical was research_extensions RE008 on dependabot-automerge.yml (github.actor identity check; PR-author gate retained), and Publish Image's exit 101 was an MSRV drift (tree-sitter 0.27.0 needs Rust 1.90/edition 2024 vs rust:1.88-slim; builder now rust:1-slim). #48 calibration wiring landed (see calibration-wiring and enforcement-inert). Debt register updated for the resolved items; docs delivered: docs/usage.adoc, docs/ci-runbook.adoc, per-crate READMEs, analyzers/code-haskell/README.adoc. NOT verified in CI by the author (no Rust/Haskell/Elixir toolchain in the sandbox): verification means the PR's own CI run plus, for the image, a successful Publish Image on main. Also this session: DEBT.adoc re-verified against the tree — six entries whose fixes had already landed (orphaned AGPL licence text, unheaded GOVERNANCE.md, the ARCHITECTURE.md/GOVERNANCE.md boilerplate pair, the unannotated ARCHITECTURE.adoc, the wiki stub, the dormant push-email-notify.yml) are now marked resolved with evidence; the wiki is recorded as CLAIMED not verified because wiki pages are not in the repository tree; licence identifier counts re-measured (159 MPL-2.0 / 65 CC-BY-SA-4.0 occurrences). Local verification that WAS possible: every changed .rs file parses clean under the tree-sitter-rust grammar (no ERROR nodes); all 12 AnalysisResult struct literals in the workspace carry resource_range; the check_gate.rs fixtures were simulated through a port of the analyser's own logic (complexity, loop depth, pattern detection, calibration rows, Pareto verdict) and reproduce the asserted verdicts; tools/ci/linter-verify.sh passes all four steps." +session = "2026-09-26 launch hardening: audited all seven open issues and pinned estate sources; withdrew unearned calibration claims; no-loss dominance and invalid-input handling; scoped interval refinement prototype; explicit no-check exits, taxonomy transport, telemetry pagination and partial-failure handling; reserved security adapter now compiles as unavailable; docs split and release hold. See launch-hardening-audit and docs/LAUNCH-READINESS.adoc. Earlier session claims are preserved in git history, not current evidence." [project-context] name = "OikosBot" @@ -36,8 +36,8 @@ milestones = [ issues = [ { id = "github-actions-budget", severity = "operational", summary = "Actions spending limit exhausted intermittently across the estate. Signature: job conclusion 'failure' with ZERO steps and the annotation 'job was not started because recent account payments have failed'. Hit 2 of 15 swept consumer repos (chronicles-of-slavia, canonical-ums — the latter private). NOT a workflow defect; do not debug the workflow when steps==0." }, { id = "actions-lockfile-enforcement", severity = "resolved", summary = "RESOLVED for this repo. GitHub's Actions workflow-lockfile enforcement killed every workflow estate-wide at startup (0s startup_failure, error visible only on the run's HTML page). Cured by shipping .github/workflows/actions.lock in #61; reusable-caller permissions fixed in #63. CI now runs green (Hypatia, Secret Scanner, Governance, Language Policy, CodeQL). Consumers still need their own lockfiles — see consumer-fleet-dark." }, - { id = "enforcement-inert", severity = "resolved-per-file", summary = "RESOLVED for the per-file path on 2026-09-26 (#48): calibration is wired. estimate_resources() maps a detected pattern onto an OperationKind (calibration::operation_for_pattern) and prices it with estimate_operation(), which returns the min/typical/max band AND the confidence that row has earned — Calibrated for HashLookup/Sort/Allocation/MathCompute, Estimated for the host-dependent FileIO/StringOp rows, Estimated on the naive complexity path. So assess().actionable is now true for calibrated drivers and `compare --check` exits 1 on an undocumented calibrated regression; proven by crates/oikosbot-cli/tests/check_gate.rs. Heuristic findings still cannot block, and the refusal stays LOUD (::warning::, #47). STILL OPEN: absolute figures are unvalidated against profiling data, and the eco score's log scale (anchored at 1 J) saturates near 100 under calibrated microjoule-scale estimates, so the eco THRESHOLD discriminates far less than before while the Pareto verdicts (base vs head) are unaffected. ESTATE path remains the genuinely Measured exception: oikosbot-telemetry derive.rs assigns Confidence::Measured to wall_minutes from the GitHub API." }, - { id = "per-file-collinearity", severity = "design", summary = "PARTIALLY SOLVED 2026-09-26 (#48). Units with a recognised pattern are now priced from per-kind calibration rows whose energy/duration/memory formulae are NOT the same function of complexity (Sort is n*log2(n) on energy and duration but linear on memory; Allocation is linear on bytes; MathCompute has no memory term), so those units no longer collapse the frontier. Units with NO recognised pattern still go through the naive path, where energy, duration, carbon and memory all derive from ONE integer (complexity = raw AST node count) and four of five objectives remain scalar multiples of each other. The dominance maths in oikosbot-pareto is correct; the naive inputs make it near-vacuous. SOLVED at estate level (telemetry axes measured independent: wall_minutes~size_kb = -0.049 across 381 repos). See DEBT.adoc." }, + { id = "enforcement-inert", severity = "launch-blocker", summary = "Calibration wiring is not calibration evidence. Every per-file table row is Estimated or Unknown until reproducible receipts exist. Requested blocking checks now return 2 when evidence is missing. No regulator readiness claimed." }, + { id = "per-file-collinearity", severity = "design", summary = "Per-file axes remain heuristic and correlated. Pattern formulas do not establish independence or useful-work equivalence. The old epsilon dominance relation admitted cycles; this branch uses no-loss dominance, with authored finite-grid tests pending execution. Estate correlation is cohort-specific evidence, not a universal theorem." }, { id = "policy-engines-never-execute", severity = "design", summary = "TWO fake gates. (1) policy-engine/datalog/eco_rules.dl has never executed — Souffle is DECLARED in guix/manifest.scm and guix/oikos.scm but invoked nowhere (no match in Justfile, *.just or any workflow); its allocation-waste and debt rules have no Rust counterpart. (2) oikosbot-eclexia's default backend dispatches on the .ecl FILE STEM and never reads file contents; its hardcoded thresholds contradict the files (energy_threshold.ecl says >50 J per function, builtin fires >1000 J total). Mitigated by a loud ::warning:: in #59; still fake." }, { id = "consumer-fleet-dark", severity = "operational", summary = "15 estate repos carry .github/workflows/oikosbot.yml pinned to oikosbot@bb95ab50 (v0.1.0), all merged — but each needs its OWN Actions lockfile before its workflows can start. OikosBot is installed everywhere and running nowhere. Highest-value follow-up." }, { id = "idaptik-ums-repo-wide-startup-failure", severity = "external", summary = "metadatastician/idaptik-ums fails ALL workflows at startup on main (OikosBot, Licence hygiene, CodeQL), with two workflows displayed as PATHS not names — the estate tell for never-parsed. Repo-level, pre-existing, not caused by the OikosBot sweep (the same file succeeded on a branch there)." } @@ -45,7 +45,7 @@ issues = [ [critical-next-actions] actions = [ - { id = "calibration-wiring", priority = "P1", status = "landed-2026-09-26", summary = "Issue #48 DONE (pending CI verification): patterns.rs detections mapped onto OperationKind via calibration::operation_for_pattern; estimate_operation() prices recognised units and returns a ResourceRange carrying that row's confidence; naive path retained for unrecognised code and labelled Estimated; ResourceRange propagated on AnalysisResult and emitted in SARIF properties.resource_range; falsifier tests added (calibration confidence ladder, pattern mapping, analyzer Calibrated-vs-Estimated, and CLI tests/check_gate.rs proving --check can exit 1). BEFORE/AFTER (derived by reasoning, NOT measured — no toolchain in the authoring sandbox): a depth-3 nested-loop unit of ~30 AST nodes went from naive 0.1 J/node (~3 J, eco ~89) to the calibrated Sort row (~0.007 J, eco clamped 100); every downstream figure moves with it. The eco-score saturation is documented in STATUS.adoc/QUICKSTART.adoc/README.adoc and flagged for an owner ruling on the score scale." }, + { id = "calibration-wiring", priority = "P0", status = "wired-evidence-open", summary = "Issue #48 remains open: bands and pattern mapping exist, but no calibration corpus, validation error or model-adequacy receipts. Numeric table values retained; confidence downgraded rather than manufactured. Scope-aware interval prototype remains separate from CLI enforcement." }, { id = "rsr-julia-template-phantom-sha", priority = "P1", summary = "hyperpolymath/rsr-julia-library-template-repo .github/workflows/codeql.yml pins phantom SHA 29b1f65c1f735799893313399435a59f54045865 (no such commit). It is a TEMPLATE, so every minted repo inherits a dead CodeQL gate. Valid replacement: 4187e74d05793876e9989daffde9c3e66b4acd07." }, { id = "bot-affine-webhook-handler", priority = "P2", summary = "Wire the AS-side webhook receiver in bot-integration-affine/ using Http server + Json stdlib externs (ruled: do the upstream AffineScript work, no interim listener)" }, { id = "hpm-json-object-keys", priority = "P3", summary = "Add hpm_json_object_keys export to hpm-json-rsr Zig FFI to close the JObject materialisation gap in stdlib/json.affine to_json" }, @@ -81,3 +81,18 @@ notes = "main green across CI, CodeQL, Governance, Hypatia, Language Policy, Sco policy-adapter = "Malformed binary verdicts now error; native feature explicitly unavailable; placeholder evaluation measurements removed" validation = "Default and all-features adapter regression tests; CI feature check added" remaining = "Real Eclexia input/result protocol, bound execution, source-policy evaluation and measured evaluation cost remain unimplemented" + +[launch-hardening-audit] +date = "2026-09-26" +branch = "arena/01a0df1f-oikosbot" +status = "hold-general-launch-and-regulator-claims" +assurance = "designed-with-limited-local-checks; not workspace-tested or proven" +issue-disposition = "docs/LAUNCH-READINESS.adoc (all seven open issues; none closed)" +research = "docs/research/EVIDENCE-SCOPED-PARETO.adoc (12 pinned estate references, including nextgen-typing coordination)" +implemented = ["withdraw unearned calibration labels", "no-loss dominance and invalid-input refusal", "experimental scoped interval/hard-limit contract", "format-independent no-check exit 2", "taxonomy for pattern findings", "strict parser/config/traversal handling", "pagination and partial-collection errors", "explicit unavailable security/policy adapters", "candidate-image and action-argument controls", "pinned Rust 1.90.0 and Bookworm build/runtime", "audience docs and launch gates"] +local-checks = ["git diff --check", "all changed Rust sources parsed with tree-sitter-rust 0.24.2", "changed Rust formatted and checked with standalone rustfmt 1.8.0 from Rust 1.88.0", "workflow linter: SPDX, permissions, action-lock coverage and lockcheck passed", "bash syntax checks", "tests/test-action-arguments.sh passed with a Docker argument recorder (not E2E)", "release-preflight missing-tool control returned 2", "4 contract unit tests compiled/run in an isolated rustc 1.88.0 harness with enum adapters", "4 numerical Pareto regression tests compiled/run from extracted function bodies with serialization omitted"] +not-verified = ["cargo fmt under pinned 1.90.0", "workspace and all-feature compilation/tests", "CLI and telemetry integration tests", "SARIF full schema", "candidate container execution", "published digest/provenance/Action E2E", "Haskell and AffineScript builds", "upstream or new formal proofs"] +verification-limit = "Pinned toolchain download unavailable; older tools recovered outside repo from rustbin packages with registry integrity checks, not upstream attestations. Cargo test -p oikosbot-pareto with 1.88.0 failed fetching index.crates.io; isolated harness checks do not establish crate linkage, serialization or CLI correctness. Required CI remains outstanding." +release-performed = false +metadata-check = "Six core TOML-shaped descriptiles and rust-toolchain.toml parse with Python 3.11 tomllib; not a full A2ML validator. Local links in the new/replaced entry guides resolve." +publication-channel = "Main-branch candidate publication uses development, not automatic latest promotion; existing consumer digests unchanged. Semver publication still requires owner review and explicit tagging." diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 0000000..870269a --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += Contributing to OikosBot +:toc: + +Start with `0-AI-MANIFEST.a2ml`, `.claude/CLAUDE.md`, the six descriptiles and +link:docs/LAUNCH-READINESS.adoc[launch readiness]. Keep changes scoped; do not +import the similarly named DSL or conflate the fleet identities. + +== Build and verify the implemented surfaces + +Install the Rust toolchain declared in `rust-toolchain.toml`. Native dependencies +include a C/C++ compiler, CMake, Make and libclang for tree-sitter/HiGHS. Then: + +[source,shell] +---- +cargo build --workspace --locked +cargo test --workspace --locked +cargo fmt --check +cargo clippy --workspace --locked +bash tests/test-eclexia-adapter.sh +---- + +Haskell is independent: with GHC 9.4 and Cabal 3.10 run `just haskell-build` and +`just haskell-test`. With a compatible upstream AffineScript compiler run +`AS_BIN=/path/to/main.exe just affine-check`; shell syntax checks are *not* +AffineScript type checks. The optional fleet crate needs a compatible sibling +checkout; do not add it as a mandatory dependency of the standalone workspace. + +`just check` runs Rust and Haskell build/tests, not full release verification. +`just rust-fmt-check` checks formatting; `just shell-check` checks the AS wrapper +syntax. `just release-preflight` collects Rust/container evidence where tooling +exists, but cannot authorize a release. See link:docs/ci-runbook.adoc[CI runbook]. + +== Code, tests and documentation + +Use Rust for the workspace, AffineScript for the App, AsciiDoc for prose, and the +existing repository tool/language policy. Preserve existing licence/SPDX notices; +flag conflicts instead of sweeping them. Run rustfmt. Prefer descriptive commits +(`fix(pareto): ...`, `test(cli): ...`, `docs: ...`) and explain compatibility changes. + +Each gate needs a clean fixture, a firing fixture for the intended reason, and +an unavailable/invalid-input control. Do not substitute line coverage for +observable assertions. Keep benchmark corpora and raw datasets out of source; +version workloads, environment and baselines before gating on timings. + +Update descriptiles in place, especially STATE with exact commands, outcomes and +unavailable tools. Distinguish designed/tested/audited/proven. Keep a loss/residual +account for changed models; do not promote confidence to make a failing release +check green. Link evidence and limits in the PR, request review, and do not close +issues that still have unexecuted or external acceptance requirements. + +== Release and fleet coordination + +Do not change the Action image digest until the artifact at that digest has been +tested. Coordinate JSON taxonomy/exit-status changes with consumers and the optional +fleet bridge. Do not change a shared fleet API here. Release approval belongs to +maintainers after link:docs/LAUNCH-READINESS.adoc[the launch gates], not to a score. diff --git a/Containerfile b/Containerfile index f834c63..c17ee71 100644 --- a/Containerfile +++ b/Containerfile @@ -10,12 +10,10 @@ # Multi-stage, glibc-consistent (rust:slim builder -> debian:slim runtime), # non-root. The oikosbot-fleet bridge is excluded from the default workspace, # so this builds standalone with no gitbot-fleet dependency. -# MSRV floor for this workspace is Rust 1.90 (tree-sitter 0.27.0 declares -# rust-version = "1.90", edition = "2024"). Use the rolling stable builder -# rather than a pinned point release so the floor stays satisfied as -# dependencies move; arrow-rs 59.3.0 declares MSRV 1.88, tree-sitter is the -# binding constraint. -FROM rust:1-slim AS builder +# Toolchain matches rust-toolchain.toml; both stages use Bookworm so the +# binary cannot accidentally require a newer glibc than the runtime. +# Image digest pinning remains a release-preflight obligation. +FROM rust:1.90.0-slim-bookworm AS builder WORKDIR /build RUN apt-get update \ && apt-get install -y --no-install-recommends cmake g++ libclang-dev make \ diff --git a/DEBT.adoc b/DEBT.adoc index a915459..0462888 100644 --- a/DEBT.adoc +++ b/DEBT.adoc @@ -490,3 +490,17 @@ repository tree and is therefore recorded as *claimed* rather than verified. Two entries were corrected rather than closed: the licence-identifier counts were re-measured, and the #16/#17 docs entry now names what the 2026-09-26 documentation pass actually delivered. + +== Superseding launch audit — 2026-09-26 + +link:docs/LAUNCH-READINESS.adoc[The current launch audit] supersedes earlier +resolved/working-regulator claims in this historical register. Wiring the table +was implemented; calibration was *not* demonstrated. All known per-file rows +now stay Estimated. The old epsilon relation could cycle; no-loss dominance +and authored regression controls replace it. Explicit missing-evidence checks +return 2, not false success. None of these source changes has been released. + +The audit maps all seven currently open issues, remaining test/benchmark/proof +obligations, runtime/container boundaries and standards-profile tensions. It also +records unavailable toolchains. Do not count authored tests as executed evidence +or a filename/score as proof. No general launch or regulator approval is given. diff --git a/DEPLOY.adoc b/DEPLOY.adoc index 472436b..a67eb85 100644 --- a/DEPLOY.adoc +++ b/DEPLOY.adoc @@ -1,29 +1,43 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -= Oikos Bot Deployment Guide += OikosBot deployment boundaries and containment -[NOTE] -==== -*Status: in flight.* +*No general-launch or production App approval.* The Action/CLI container is a +batch analyzer, not an HTTP webhook receiver. Existing published artifacts may +still have older exit/confidence behavior. Source changes do not upgrade them. -The previous OikosBot deployment runbook (Podman Quadlet + Caddy + -ReScript/Deno containers under `containers/production/`) was retired on -2026-05-28 alongside the ReScript codebase. See `git log -- containers/` -and `git log -- bot-integration/` for the prior material — it remains a -valid reference for the runtime topology (Caddy reverse-proxy + bot -network + healthchecks) and can be re-used once the AffineScript port -reaches operational parity. +== Experimental advisory Action/CLI rollout -The replacement codebase is the AffineScript port at -link:bot-integration-affine/[`bot-integration-affine/`]. It is currently a -scaffold: cross-module type-checking, JSON payload extraction, and the -HTTP server accept loop are gated on upstream AffineScript stdlib work -(`Json` v0.3 RSR rewire + `http-capability-gateway` extern surfacing). -Production deployment will resume once those gates lift and the AS -port has a stable build target. +. Review link:docs/LAUNCH-READINESS.adoc[launch gates] and select a limited opt-in + pilot with a maintainer responsible for interpreting model limits. +. Build/test a candidate; test the *published digest* independently for version, + anonymous access, non-root execution, SARIF clean/firing fixtures and provenance. +. Pin both Action revision and image digest. Begin with `mode: report`; do not + enable regulator or claim that an estimate-based check passed. +. Record release commit, digest, toolchain, configuration and model assumptions. + Keep logs free of credentials and private source; retain artifacts only under + an agreed access/retention policy. +. If the run fails or returns no-check, stop promotion and inspect stderr. Do + not upload absent SARIF, suppress errors, or fall back to mutable `latest`. -For now, OikosBot is *not* running in production. The previous -`.github/app.yml` manifest still describes the intended permissions -and webhook events and remains valid for re-registering the GitHub -App when the AS port is ready. -==== +Containment: disable the workflow/Action in affected consumers, retain the failed +artifact/logs safely, and restore the previous reviewed revision/digest only if +it remains safe. Revoke/rotate credentials if exposed. Restoring code does not +undo disclosed data, consumed energy or external actions. Follow +link:SECURITY.md[private incident reporting]. + +== App production hold + +`bot-integration-affine/` has signature-verification and orchestration source; +that is not demonstrated end-to-end deployment. Before writing executable +production startup instructions, obtain compiler/typecheck evidence, real HTTP +listener/transport integration, malformed/signature/replay tests, bounded payloads, +least-privilege installation-token handling, idempotent delivery processing, +rate-limit/backoff behavior, and validated comment publication. + +Also require health/readiness probes, secret rotation, redacted logging, +retention policy, resource limits, graceful shutdown and a rehearsed incident +procedure. VeriSimDB remains a design integration, not a tested persistence path. +Until these exist, no service-start command is advertised as production-ready. +Historical ReScript/Quadlet/Caddy materials are in Git history and are not a +supported deployment recipe for the current AffineScript scaffold. diff --git a/Justfile b/Justfile index e022fb6..99d02cf 100644 --- a/Justfile +++ b/Justfile @@ -35,3 +35,7 @@ shell-check: clean: cargo clean cd {{ haskell_dir }} && cabal clean + +# Evidence collection only, never an automatic release authorization. +release-preflight: + bash tools/ci/release-preflight.sh diff --git a/QUICKSTART.adoc b/QUICKSTART.adoc index 2a56ef5..c2f3a5e 100644 --- a/QUICKSTART.adoc +++ b/QUICKSTART.adoc @@ -2,125 +2,41 @@ // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell = OikosBot Quickstart :icons: font -:source-highlighter: rouge -== Build +*Alpha, advisory use only.* See link:docs/LAUNCH-READINESS.adoc[the launch hold] +and link:CONTRIBUTING.adoc[toolchain/native build prerequisites]. [source,bash] ---- -cargo build --release +cargo build --release --locked -p oikosbot-cli +./target/release/oikosbot analyze path/to/file.rs --format json +./target/release/oikosbot report path/to/repo --format sarif --output results.sarif +./target/release/oikosbot compare path/to/base path/to/head --format json ---- -== Usage +Supported source languages are Rust (`.rs`), JavaScript (`.js`) and Python (`.py`). +A directory scan must find analyzable functions; unsupported, empty or fully +excluded input is not a successful scan. Configure mode/excludes/languages with +`.oikos.yml` or `--config`; see link:docs/usage.adoc[the user guide]. -=== Analyze a file +Numbers are *heuristic estimates*, including recognized pattern-table rows. +No calibration receipt currently validates those rows or their envelopes. +`compare --check` and blocking `check` therefore return *2 (no check performed)* +when evidence is insufficient. Use `report` or plain `compare` for advisory use. +A documented trade-off cannot turn an estimate into evidence or approval. -[source,bash] ----- -./target/release/oikosbot analyze path/to/file.rs ----- - -Supports: Rust (`.rs`), JavaScript (`.js`), Python (`.py`). - -=== Pareto verdict on a change (base vs head) - -[source,bash] ----- -./target/release/oikosbot compare path/to/base path/to/head ----- - -Classifies the change in objective space (carbon, energy, time, memory, -maintainability): *pareto-improvement* (better on at least one objective, -worse on none), *pareto-regression* (worse on at least one, better on none), -*trade-off* (mixed — document it with a `Pareto-Trade-off:` trailer in the PR -description), or *neutral*. Advisory by default; `--check --pr-body pr.txt` -fails the run on an undocumented, measured regression or trade-off. - -[IMPORTANT] -==== -*`--check` blocks only on evidence, by design.* Only objectives backed by -`Measured` or `Calibrated` inputs may fail a run. The analyser earns that -evidence per finding: a recognised pattern is priced from the calibration table -(`crates/oikosbot-analysis/src/calibration.rs`) and labelled `Calibrated`, while -unclassified code stays on the naive complexity-derived estimate and is labelled -`Estimated`. - -So an undocumented regression whose drivers are calibrated exits 1; the same -regression on heuristic drivers prints a `::warning::` naming the verdict and -the confidence level, and exits 0. A gate that quietly does nothing is -indistinguishable from one that passed, which is precisely the failure mode -OikosBot exists to find — so the refusal is never silent. -==== - -=== Per-repo configuration - -`check`, `report`, and `compare` honor an `.oikos.yml` in the analyzed -directory (or pass `--config path/to/.oikos.yml`): operating mode -(advisor/regulator), `thresholds.eco_minimum`, `exclude` globs, and -`analysis.languages`. - -=== Analyze oikosbot itself (dogfooding!) - -[source,bash] ----- -./target/release/oikosbot self-analyze ----- - -This proves we practice what we preach — the analyzer itself is efficient. - -=== JSON output +Pareto comparisons report improvement, regression, trade-off, neutral or +indeterminate within the declared numeric model. They do not establish ecological +benefit for equivalent real workloads. Scores are heuristic and weights encode +value choices; do not optimize for the score instead of useful work. [source,bash] ---- -./target/release/oikosbot analyze file.rs --format json ----- - -== What It Does - -OikosBot analyzes code for: - -* *Energy consumption* (Joules) -* *Carbon footprint* (gCO2e) -* *Execution time* (milliseconds) -* *Memory usage* (bytes) - -And provides: - -* *Eco score* (0-100) — environmental impact -* *Econ score* (0-100) — algorithmic efficiency -* *Quality score* (0-100) — code quality -* *Overall health* (weighted combination) - -== Example Output - -[source,text] ----- -📍 Function: nested_loops_function - Resources: - Energy: 7.10 J - Carbon: 0.0009 gCO2e - Time: 35.50 ms - - Health Index: - Eco: 80.4/100 - Overall: 75.1/100 - - Recommendations: - • Consider algorithm optimization to reduce nested iterations +cargo test --workspace --locked +cargo fmt --check +bash tests/test-eclexia-adapter.sh ---- -== The Eclexia Connection - -* *Phase 1* (DONE): Analyzer built with Eclexia principles (explicit resource types) -* *Phase 2* (Next): Policy engine runs Eclexia code from `policies/*.ecl` -* *Phase 3+*: Migrate core to Eclexia as the language matures - -== Philosophy - -[quote] -____ -"The best ecological code analyzer is one that's ecological itself." -____ - -OikosBot demonstrates that efficient, resource-aware code is not just theory — -it works in practice. +The builtin Eclexia adapter does not execute arbitrary `.ecl`; native integration +is explicitly unavailable. The self-analysis command is a demonstration, *not* +a benchmark or proof that OikosBot itself is resource-efficient. diff --git a/README.adoc b/README.adoc index 3bbcb70..4b116ad 100644 --- a/README.adoc +++ b/README.adoc @@ -3,264 +3,95 @@ = OikosBot Jonathan D.A. Jewell v0.1.0-dev -:toc: macro -:toclevels: 2 :icons: font -:source-highlighter: rouge -image:https://img.shields.io/badge/License-MPL_2.0-blue.svg[License: MPL-2.0,link="https://opensource.org/licenses/MPL-2.0"] -image:https://img.shields.io/badge/status-alpha-orange.svg[Status: alpha] - -*Measures what software costs — in money, time, energy and carbon — and which -repositories turn that spend into verified capability.* - -OikosBot analyses code and CI history through an *ecological and economic* lens. -It is unusual in two ways: it treats the *economic* axes as seriously as the -ecological ones, and it starts from *Pareto optimality* rather than a single -score — a change is an improvement only if it dominates, and a trade-off is -named as a trade-off. - -[NOTE] -==== -This repository is only **OikosBot**, and it is easy to confuse with two -similarly-named-but-separate things: - -* **oikos** — the stock-flow consistent economics *DSL*, now in - https://github.com/hyperpolymath/oikos-economics-accounting-dsl[`hyperpolymath/oikos-economics-accounting-dsl`]. -* **sustainabot** — a *reserved member slot* of - https://github.com/hyperpolymath/gitbot-fleet[`hyperpolymath/gitbot-fleet`] - (`bots/sustainabot/`), to be prototyped later. OikosBot is **not** sustainabot; - the implementation that briefly lived there was a misfiled copy of OikosBot and - has been moved into this repo. - -See link:DISAMBIGUATION.adoc[`DISAMBIGUATION.adoc`] for the full breakdown. -==== - -toc::[] - -== Documentation - -For a map of all documentation — grouped by audience (developer / maintainer / -end-user) and by concept — see link:docs/README.adoc[`docs/README.adoc`]. - -== Two units of analysis - -OikosBot works at two scales, and they have very different evidential strength. - -[cols="1,3,3"] -|=== -| | *Per file* (`analyze` / `check` / `report` / `compare`) | *Per estate* (`estate collect` / `analyse` / `report`) - -| Question -| Did this change dominate its base, or trade off? -| Which repositories turn spend into verified capability — and which consume input and produce none? - -| Inputs -| tree-sitter AST metrics -| GitHub CI telemetry: billable minutes, wall-clock, runner class - -| Confidence -| `Estimated` only -| `Measured` money and time; `Calibrated` energy; `Estimated` carbon - -| Can it block? -| No — see the caveat below -| Not yet; round one is read-only by design -|=== - -The estate view exists because *allocative efficiency is not a property of a -single change*. It means scarce resources flow to their highest-valued -competing use; with one diff in front of you there is nothing to allocate -between. Opportunity cost and marginal analysis need a portfolio, so the -portfolio is the unit. - -== Status - -`v0.1.0` — **online in CI/Action mode**; estate pipeline **shipped, read-only**. -Full picture: link:docs/STATUS.adoc[`docs/STATUS.adoc`] (human), -link:.machine_readable/descriptiles/STATE.a2ml[`STATE.a2ml`] (machine), and -link:DEBT.adoc[`DEBT.adoc`] (what is owed). - -Working: the Pareto engine (`crates/oikosbot-pareto`), the estate pipeline -(`oikosbot-telemetry` / `-capability` / `-dea`), the published container -`ghcr.io/hyperpolymath/oikos`, the composite GitHub Action, `.oikos.yml` -configuration, and SARIF output that GitHub code scanning ingests. +Ecological and economic code-analysis tooling: Rust source-pattern analysis, +SARIF reports, advisory Pareto comparisons, and read-only estate telemetry/DEA. +OikosBot is *not* the `oikos` economics DSL or the `sustainabot` fleet slot: +link:DISAMBIGUATION.adoc[disambiguation]. [IMPORTANT] ==== -**Per-file resource figures are still estimates, and only calibrated ones can -block.** The analyser prices a recognised pattern from the calibration table and -labels the finding `Calibrated`; anything it cannot classify stays on the naive -complexity path and is labelled `Estimated`. Only `Measured`/`Calibrated` -objectives may fail a run, so `--check` blocks on a calibrated regression and -warns loudly (exit 0) on a heuristic one. The absolute figures remain static and -unvalidated — trust the direction, not the joules. - -The *estate* path is the exception: `wall_minutes` comes straight from the -GitHub API and is genuinely `Measured`. See link:DEBT.adoc[`DEBT.adoc`] for the -full account of what is and is not earned. +*Alpha; general launch and regulator claims are on hold.* +Per-file energy/carbon/time/memory figures are heuristic estimates, including +recognized patterns. No row currently has a reproducible calibration receipt. +A score, range or frontier membership is not proof of environmental benefit. +See link:docs/LAUNCH-READINESS.adoc[launch gates, issue disposition and evidence]. ==== -The Rust analysis workspace (`crates/oikosbot-*`: CLI, analysis engine, metrics, -Pareto engine, SARIF output, and Eclexia policy evaluation) builds and tests -cleanly and is the most complete surface — it was extracted from the former -`gitbot-fleet/bots/sustainabot/` and renamed. - -The AffineScript webhook receiver (`bot-integration-affine/`) is a scaffold. -HMAC-SHA256 webhook signature verification is implemented in -`bot-integration-affine/src/Webhook.affine` (constant-time, via the `hpm-crypto` -RSR FFI), and the upstream AffineScript standard-library prerequisites have -landed (`json` RSR rewire, affinescript#421; `Http` server RSR rewire, -affinescript#425). The webhook → analyse → comment orchestration is now wired in -source (`src/Payload.affine` extracts the PR fields and `handle_github_webhook` -runs `Oikos.handle_pr_event`); what remains is binding the HTTP listener -(`Sub.http_server`), which is gated on the stdlib `Http::Server` host type. (The -AffineScript changes are unverified — there is no AS toolchain in this -environment.) +== Start here -The Haskell analyzer scaffold (`analyzers/code-haskell/`) builds independently. +* link:QUICKSTART.adoc[Build and first report] +* link:docs/usage.adoc[Use the CLI/Action and interpret results] +* link:docs/architecture.adoc[Implemented architecture versus target design] +* link:docs/policies.adoc[Policy/configuration limits] +* link:docs/research/EVIDENCE-SCOPED-PARETO.adoc[The mathematics, type contract and estate comparisons] +* link:docs/README.adoc[Full documentation map] +* link:CONTRIBUTING.adoc[Development and verification] -== Repository Layout - -[source,text] ----- -oikosbot/ -|-- crates/ # Rust workspace (oikosbot-* analysis engine + CLI) -| |-- oikosbot-cli/ # `oikosbot` binary (analyze/check/report/compare/estate) -| |-- oikosbot-analysis/ # tree-sitter analysis + eco/econ heuristics -| |-- oikosbot-metrics/ # shared metric/result types + confidence ladder -| |-- oikosbot-pareto/ # epsilon-tolerant dominance, frontier, verdicts -| |-- oikosbot-telemetry/ # estate: gh-API collector, parquet snapshots, derived metrics -| |-- oikosbot-capability/ # estate: verified-capability output metrics -| |-- oikosbot-dea/ # estate: Data Envelopment Analysis (CCR/BCC LPs) -| |-- oikosbot-sarif/ # SARIF report generation -| |-- oikosbot-eclexia/ # Eclexia (.ecl) policy evaluation -| `-- oikosbot-fleet/ # OPTIONAL gitbot-fleet bridge (excluded by default) -|-- bot-integration-affine/ # AffineScript webhook receiver (in flight) -|-- analyzers/code-haskell/ # Haskell eco/econ/quality analysis engine -|-- policy-engine/ # Datalog and DeepProbLog policy rules -|-- policies/ # Eclexia (.ecl) policy definitions -|-- config/oikos.yaml # Bot configuration -|-- databases/ontology/ # eco.ttl — VeriSimDB semantic-witness ontology seed -|-- examples/ # Example CI integration -|-- fuzz/ # cargo-fuzz targets (excluded from the workspace) -|-- docs/ # documentation map, status, setup guides, specs -|-- ARCHITECTURE.adoc # Platform architecture (TARGET design — see DEBT.adoc) -|-- DEBT.adoc # Debt register: licence / docs / code / proof / CI-CD -|-- DISAMBIGUATION.adoc # oikos vs OikosBot vs sustainabot -|-- QUICKSTART.adoc # CLI quickstart -`-- DEPLOY.adoc # Deployment status and runbook notes ----- +== What exists -== Data Layer +The standalone Rust workspace supports Rust/JavaScript/Python analysis, +pattern findings, JSON/text/SARIF, `.oikos.yml`, point-vector Pareto comparisons, +and estate `collect|analyse|report`. These are advisory models. The experimental +scoped interval contract is a library prototype, not a production gate. -OikosBot persists analysis results, code-relationship graphs, and the eco/econ -knowledge base in a single identity-consonance store, -https://github.com/hyperpolymath/verisimdb[VeriSimDB]. In VeriSimDB one identity -is an *octad* of modal witnesses — graph, vector, tensor, semantic, document, -temporal, provenance, spatial. The **graph + document** witnesses take over the -role once planned for ArangoDB (code-dependency graphs, analysis history, project -metadata), and the **semantic** witness takes over the role once planned for -Virtuoso/RDF (ontology knowledge, proof blobs). The eco/econ ontology seed for -the semantic witness lives at -link:databases/ontology/eco.ttl[`databases/ontology/eco.ttl`]. +The composite Action and GHCR image have been published previously. The default +image is digest-pinned; source fixes do not change it until a reviewed artifact +is built, tested and explicitly adopted. This branch's hardening changes still +require Rust/container CI verification. -VeriSimDB is `v0.1.0` and OikosBot's runtime client is *deferred*: the data layer -is retargeted in design and configuration now (see -link:config/oikos.yaml[`config/oikos.yaml`] and -link:ARCHITECTURE.adoc[`ARCHITECTURE.adoc`]), with runtime wiring tracked on the -link:ROADMAP.adoc[roadmap]. The legacy ArangoDB + Virtuoso assets were retired -(recoverable in git history). +The AffineScript webhook App remains a scaffold with source-level orchestration; +listener/transport and operational parity are not demonstrated. Haskell is an +independent analyzer surface. VeriSimDB is the target datastore, not a working +runtime client. link:DEPLOY.adoc[Deployment boundaries]. -== Local Checks +== Run an advisory report [source,shell] ---- -cargo build --workspace # or: just rust-build -cargo test --workspace # or: just rust-test -just haskell-build -just haskell-test -just affine-check ----- - -The default `cargo` workspace deliberately excludes `crates/oikosbot-fleet` -(the optional gitbot-fleet bridge) so OikosBot builds standalone. See -link:DISAMBIGUATION.adoc[`DISAMBIGUATION.adoc`]. - -`just affine-check` expects an AffineScript compiler. Override the compiler path -with `AS_BIN=/path/to/main.exe` when needed. - -== GitHub Action - -OikosBot runs in CI as a composite action wrapping the published container -(`ghcr.io/hyperpolymath/oikos`): - -[source,yaml] ----- -- uses: hyperpolymath/oikosbot@main # pin a tag/SHA in production - with: - mode: report # report | check | compare - path: . - output: results.sarif # upload with github/codeql-action/upload-sarif +cargo build --locked -p oikosbot-cli +./target/debug/oikosbot report crates/oikosbot-analysis/src --format sarif --output results.sarif +./target/debug/oikosbot compare /path/to/base /path/to/head --format json ---- -`compare` mode issues a Pareto verdict against a base checkout (see -link:QUICKSTART.adoc[QUICKSTART]). A repo-local `.oikos.yml` is honored -automatically. Full workflow example: -link:examples/oikosbot-ci.yml[`examples/oikosbot-ci.yml`]. - -How OikosBot relates to the nearest Marketplace neighbour: -link:docs/COMPARISON-climate-warrior.adoc[OikosBot vs Climate Warrior]. +`report` does not enforce. Explicit checks without sufficient evidence return +*2 (no check performed)*, not a green pass; 1 is reserved for a supported negative +check. This intentionally changes earlier behavior: heuristic checks used to +warn and exit 0, and text threshold checks could fail while JSON/SARIF did not. +See link:docs/usage.adoc[compatibility and exit semantics]. == Estate analysis -Treats a whole GitHub organisation as a portfolio competing for one budget of -minutes, money, energy and carbon. - [source,shell] ---- -oikosbot estate collect --owner --out [--max-runs 200] -oikosbot estate analyse --staging --snapshot -oikosbot estate report --snapshot [--format md|json] [-o FILE] +oikosbot estate collect --owner ORG --out STAGING --max-runs 200 +oikosbot estate analyse --staging STAGING --snapshot SNAPSHOT +oikosbot estate report --snapshot SNAPSHOT --format json ---- -`collect` reads CI run history through the authenticated `gh` CLI — no -instrumentation in the measured repos, and it works retroactively. It is -resumable: a staging file that already exists is skipped, so an interrupted -sweep can simply be re-run. +This path is read-only. Workflow API intervals are not invoices or CPU time; +energy/carbon/imputed cost remain estimates. DEA is relative to the selected +peers, inputs and output proxies—not causal ecological impact. Partial collection +fails explicitly; capped histories and old staging files still require review. +Snapshots belong outside this source repository, for example in +https://github.com/hyperpolymath/oikosbot-estate[`oikosbot-estate`]. -`analyse` derives per-repo cost and capability, then runs *Data Envelopment -Analysis* — input-oriented CCR and BCC solved as linear programs. DEA compares -units with multiple incommensurable inputs *and* outputs, needs no prices, and -returns three things: an efficiency score, the *peer set* (which frontier repos -this one is measured against — an exemplar, not just a number), and the LP -*duals*, which are shadow prices derived from the estate's own data. - -The output measure is *verified capability*, not activity: does a workflow -parse, can a gate actually fail, do artefacts publish. That reframes a familiar -pathology as an economic one — a repository burning CI minutes on a gate with -hundreds of successes and no failure in its entire history is consuming real -input for no verified output. - -Snapshots are versioned Parquet committed to a separate dataset repository, -https://github.com/hyperpolymath/oikosbot-estate[`hyperpolymath/oikosbot-estate`], -so history is git history and the analysis never silently measures a corpus -containing itself. Two `analyse` runs over the same staging produce -byte-identical output. - -[NOTE] -==== -Round one is deliberately *read-only*: it reports, it does not gate. The -premise was checked before anything was built on it — measured correlation -between compute time and code volume across 381 repositories is −0.049, i.e. -the axes really are independent rather than one variable in disguise. -==== +== Layout and checks -== GitHub App +`crates/`:: Rust workspace; `oikosbot-fleet` is optional and excluded. +`bot-integration-affine/`:: App scaffold, not a deployable substitute for the CLI. +`analyzers/code-haskell/`:: Independent Haskell analyzer. +`policies/`, `policy-engine/`:: Policy examples/target surfaces; see implementation limits. +`docs/`, `.machine_readable/descriptiles/`:: Human guides and six core A2ML records. +`tests/`, `tools/ci/`:: Contract fixtures and verification scripts. -The app manifest lives at link:.github/app.yml[`.github/app.yml`]. The setup -guide is link:docs/GITHUB_APP_SETUP.adoc[`docs/GITHUB_APP_SETUP.adoc`]. +Run `cargo test --workspace --locked`, `cargo fmt --check`, and +`just release-preflight` where their toolchains are installed. Missing tools +are not passes. Follow link:docs/LAUNCH-READINESS.adoc[the launch review] before +promoting any artifact or enabling an enforcement claim. -== Licence +== Licence and security -MPL-2.0. See link:LICENSE[`LICENSE`]. +MPL-2.0 code; per-file notices remain authoritative. See link:LICENSE[LICENSE], +link:LICENSES/[LICENSES], and link:SECURITY.md[private vulnerability reporting]. diff --git a/action.yml b/action.yml index 1f5ffa5..965c27d 100644 --- a/action.yml +++ b/action.yml @@ -50,11 +50,10 @@ inputs: default: '' check: description: >- - compare mode: fail on an undocumented Pareto regression/trade-off whose - driving objectives are Measured or Calibrated. Recognised patterns are - priced from the calibration table and may block; unrecognised code stays - on the naive estimate (Estimated), which may not — it warns loudly and - exits 0 instead (see QUICKSTART and docs/usage.adoc). + Request comparison enforcement. In current source, missing calibration + evidence returns no-check (2), not success. The default image is an older + published artifact and may retain earlier behavior; adopt a newly tested + digest explicitly before relying on changed semantics. Use false for advice. required: false default: 'false' image: @@ -85,20 +84,31 @@ runs: IMAGE: ${{ inputs.image }} run: | set -euo pipefail + case "$DO_CHECK" in + true|false) ;; + *) echo "::error::check must be true or false"; exit 2 ;; + esac args=("$MODE") case "$MODE" in compare) - [ -n "$BASE" ] || { echo "::error::compare mode requires the 'base' input"; exit 1; } + [ -n "$BASE" ] || { echo "::error::compare mode requires the 'base' input"; exit 2; } args+=("$BASE" "$TARGET") [ "$DO_CHECK" = "true" ] && args+=(--check) [ -n "$PR_BODY" ] && args+=(--pr-body "$PR_BODY") - [ "$FORMAT" = "json" ] && args+=(--format json) + # Compare has no SARIF renderer. Map the inherited default to + # JSON rather than silently ignoring --output in text mode. + case "$FORMAT" in + sarif|json) args+=(--format json) ;; + text) args+=(--format text) ;; + *) echo "::error::unknown comparison format '$FORMAT'"; exit 2 ;; + esac + [ -z "$THRESHOLD" ] || { echo "::error::eco-threshold is not a compare input"; exit 2; } ;; report|check) args+=("$TARGET" --format "$FORMAT") ;; *) - echo "::error::unknown mode '$MODE' (report | check | compare)"; exit 1 + echo "::error::unknown mode '$MODE' (report | check | compare)"; exit 2 ;; esac [ -n "$OUTFILE" ] && args+=(--output "$OUTFILE") diff --git a/crates/oikosbot-analysis/README.adoc b/crates/oikosbot-analysis/README.adoc index 292b675..fc804a2 100644 --- a/crates/oikosbot-analysis/README.adoc +++ b/crates/oikosbot-analysis/README.adoc @@ -1,74 +1,34 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell = `oikosbot-analysis` -:toc: macro -Turns source files into `AnalysisResult`s: parse with tree-sitter, walk the -AST, detect inefficiency patterns, and price the unit. +Tree-sitter parsing for Rust, JavaScript and Python, function-level heuristic +resource models, pattern detection and recommendations. Entry points: +`analyze_file`, `analyze_source`, `Analyzer`. Results use `oikosbot-metrics`. +Recovered syntax errors are rejected instead of reporting a partial parse as success. -== Modules +`patterns.rs` owns the syntactic rules; paired fixtures live in +`tests/pattern_fixtures.rs`. Pattern findings carry the authored taxonomy +`wish/perfective/externalities`; generic measurement telemetry has no taxonomy. -[cols="1,4"] -|=== -| `analyzer` | `Analyzer` — parses a file or source string, finds function -nodes, and builds one result per function. +`calibration.rs` is a historical filename for a heuristic operation table. Known +patterns map to operation kinds, returning a min/typical/max envelope. There are +*no reproducible calibration receipts*: all known rows are Estimated; Generic is +Unknown. Unrecognized source uses the naive AST-size heuristic without a band. +The nested-loop/sorting mapping is approximate, not a complexity equivalence. +Neither point estimates nor their envelopes establish physical resource usage. -| `language` | Extension → tree-sitter language. Supports `rs`, `js`/`mjs`/`cjs`, -`ts`/`mts`/`cts` (JavaScript grammar), `py`/`pyw`. +`carbon.rs` supplies an assumed grid coefficient. `dependencies.rs` performs +manifest analysis. `directives.rs` reads integration directives; `migration.rs` +contains language/migration advice. `security.rs` explicitly returns unavailable: +the retained panic-attack feature has no integrated upstream dependency. It now +compiles as a refusal seam rather than referencing an absent crate. -| `patterns` | `detect_patterns` — nested loops (depth ≥ 3), busy-wait, string -concatenation in a loop, `.clone()` in a loop, unbuffered I/O, large -allocation, redundant `to_string`/`to_owned`. Each match carries an -`impact_multiplier` and a suggestion. - -| `calibration` | `estimate_operation(kind, n)` — the priced operation rows, -each returning a `ResourceRange` that carries the min/typical/max band **and** -the confidence that row has earned (`Calibrated` for the measured kinds, -`Estimated` for the host-dependent ones); `operation_for_pattern` — the -documented pattern → `OperationKind` mapping. - -| `carbon` | `estimate_carbon(energy)` — the single carbon model used everywhere. - -| `dependencies` | Manifest scanning for heavy dependencies -(`oikosbot/heavy-dependency` findings). - -| `directives` | In-source directive parsing. - -| `migration` | Migration helpers for the analysis output. - -| `security` | Security–sustainability correlation. **Feature-gated** -(`panic-attack`): not compiled by default, enabled by -`oikosbot-cli --features security`. -|=== - -== How a resource estimate is chosen - -`Analyzer::estimate_resources` has exactly two paths, chosen by evidence: - -. **Calibrated** — a detected pattern maps to a known operation category. The - estimate comes from `calibration::estimate_operation`, the finding carries - that row's confidence (`Calibrated` for `HashLookup`, `Sort`, `Allocation`, - `MathCompute`; `Estimated` for the host-dependent `FileIO`/`StringOp`/ - `NetworkCall` rows), - and the min/typical/max band is propagated on the result. When several - patterns map, the largest `impact_multiplier` wins. -. **Naive** — no recognised pattern. The historical - `complexity × constant` heuristic (0.1 J per AST node), labelled - `Estimated`, with no band claimed. - -`redundant-allocation` is deliberately unmapped: a marginal finding must not -inherit `Calibrated` confidence from the allocation row. - -== What it does not do - -It does not run the policies under `policies/` (that is `oikosbot-eclexia`), -does not rank units against each other (`oikosbot-pareto`), and does not emit -SARIF (`oikosbot-sarif`). The absolute figures are static estimates: the -calibrated path is better anchored than the naive one, but neither has been -validated against profiling data. - -== Tests +Tests (require the pinned Rust/native toolchain): ---- -cargo test -p oikosbot-analysis +cargo test --locked -p oikosbot-analysis --all-features ---- + +See link:../../docs/LAUNCH-READINESS.adoc[launch gates] before treating any model +output as enforcement evidence. New branch tests still require execution. diff --git a/crates/oikosbot-analysis/src/analyzer.rs b/crates/oikosbot-analysis/src/analyzer.rs index df3a7f9..e02a29e 100644 --- a/crates/oikosbot-analysis/src/analyzer.rs +++ b/crates/oikosbot-analysis/src/analyzer.rs @@ -49,6 +49,9 @@ impl Analyzer { .parse(source, None) .context("Failed to parse source")?; + if tree.root_node().has_error() { + anyhow::bail!("no check performed: source contains syntax errors"); + } self.analyze_tree(source, &tree) } @@ -135,6 +138,7 @@ impl Analyzer { let end = node.end_position(); Some(AnalysisResult { + taxonomy: FindingTaxonomy::for_rule(&rule_id), location, resources, health, @@ -206,11 +210,10 @@ impl Analyzer { /// /// Two paths, chosen by evidence rather than by preference: /// - /// * **Calibrated path** — the unit carries a detected pattern that maps to + /// * **Pattern-table path** — the unit carries a detected pattern that maps to /// a known operation category (`calibration::operation_for_pattern`). The /// estimate comes from `calibration::estimate_operation` and the - /// confidence is whatever that row earns (Calibrated for measured rows, - /// Estimated for host-dependent ones). The min/typical/max band is + /// confidence remains Estimated until calibration receipts exist. The band is /// propagated so consumers can see the spread instead of treating the /// point estimate as exact. /// * **Naive path** — no recognised pattern, so there is nothing to price. @@ -255,7 +258,7 @@ impl Analyzer { /// alone. Kept for units with no recognised pattern — but note that all /// four axes are the same linear function of `complexity`, so a Pareto /// frontier computed over them is a one-dimensional sort. That is why the - /// calibrated path exists. + /// pattern-table path exists. fn naive_resources(&self, complexity: usize) -> ResourceProfile { let energy = Energy::joules(complexity as f64 * 0.1); let duration = Duration::milliseconds(complexity as f64 * 0.5); @@ -341,7 +344,7 @@ mod tests { /// `depth` nested `for` loops — the smallest nest flagged as /// `nested-loops` is depth 3. The body avoids every other pattern (no /// `.clone()`, no `File::open`, no `vec![]`, no string `+`, no - /// `to_string`) so exactly one calibrated row prices the unit. + /// `to_string`) so exactly one heuristic row prices the unit. fn nested_loop_work(depth: usize) -> String { let mut body = String::from("fn deep_work(items: &[u32]) -> usize {\n let mut total = 0;\n"); @@ -366,16 +369,14 @@ mod tests { body } - /// Falsifier for the old behaviour: a recognised pattern used to be - /// labelled `Estimated` like everything else, so no finding could ever be - /// `Calibrated` and `--check` could never block. + /// Recognising a pattern must not manufacture calibration evidence. #[test] - fn recognized_pattern_earns_calibrated_confidence_and_a_band() { + fn recognized_pattern_retains_estimated_confidence_and_a_band() { let results = analyze(&nested_loop_work(3)); assert_eq!(results.len(), 1, "one function, one finding"); let result = &results[0]; - assert_eq!(result.confidence, Confidence::Calibrated); + assert_eq!(result.confidence, Confidence::Estimated); assert_eq!(result.rule_id, "oikosbot/nested-loops"); // The band is propagated, not collapsed: min <= typical <= max, and the @@ -383,7 +384,7 @@ mod tests { let range = result .resource_range .as_ref() - .expect("a calibrated estimate must carry its band"); + .expect("a table estimate must carry its band"); assert!(range.min.energy.0 <= range.typical.energy.0); assert!(range.typical.energy.0 <= range.max.energy.0); assert!((range.typical.energy.0 - result.resources.energy.0).abs() < 1e-12); @@ -407,20 +408,20 @@ mod tests { assert!(result.resources.energy.0 > 0.0); } - /// The calibrated path must actually change the numbers, or the wiring + /// The pattern-table path must actually change the numbers, or the wiring /// would be decoration. Same unit, same size, different evidence. #[test] fn calibration_changes_the_estimate() { let plain = analyze(&plain_work(40)); let nested = analyze(&nested_loop_work(3)); - // The naive path charges 0.1 J per node; the calibrated Sort row + // The naive path charges 0.1 J per node; the heuristic Sort row // charges microjoules per comparison-bounded operation. A nested-loop // unit is no longer free just because it is small, and plain code is // no longer expensive just because it is long. assert!( plain[0].resources.energy.0 > nested[0].resources.energy.0, - "naive energy {:.4} J should dwarf calibrated {:.6} J", + "naive energy {:.4} J should dwarf table estimate {:.6} J", plain[0].resources.energy.0, nested[0].resources.energy.0 ); diff --git a/crates/oikosbot-analysis/src/calibration.rs b/crates/oikosbot-analysis/src/calibration.rs index 28ffac1..e65ea4b 100644 --- a/crates/oikosbot-analysis/src/calibration.rs +++ b/crates/oikosbot-analysis/src/calibration.rs @@ -15,7 +15,7 @@ use crate::carbon::estimate_carbon; use oikosbot_metrics::{Confidence, Duration, Energy, Memory, ResourceProfile, ResourceRange}; -/// Operation categories for calibrated estimates +/// Operation categories for heuristic estimates #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OperationKind { /// HashMap/BTreeMap lookup @@ -36,20 +36,18 @@ pub enum OperationKind { Generic, } -/// Calibrated resource profiles for known operation patterns. +/// Heuristic resource profiles for known operation patterns. /// /// These are expert estimates that will be refined with profiling data. /// All values assume a modern x86_64 system at ~50W TDP. /// -/// Returns the min/typical/max envelope, carrying the confidence a finding -/// built on this estimate is entitled to. The ladder is deliberate and -/// per-kind: only `HashLookup`, `Sort`, `Allocation` and `MathCompute` are -/// backed by calibration data, so only those rows are `Calibrated`. `FileIO`, -/// `StringOp` and `NetworkCall` depend on host and workload specifics we have -/// not measured, so they stay `Estimated` even though they use this table. -/// `Generic` is the fallback for code we could not classify and stays -/// `Unknown`. Confidence is therefore earned per finding, never -/// blanket-assigned. +/// Returns a heuristic min/typical/max envelope, NOT a statistical confidence +/// interval or a certified bound. No row currently has a reproducible calibration +/// receipt (workload, hardware, samples, fit and validation error). Consequently +/// every known operation stays `Estimated`; `Generic` stays `Unknown`. +/// Recognising syntax does not calibrate its runtime cost. In particular, AST +/// node count is not operation count and nested loops are not sorting. +/// Retaining the old table values preserves report comparability, not validity. pub fn estimate_operation(kind: OperationKind, n: usize) -> ResourceRange { match kind { OperationKind::HashLookup => { @@ -59,7 +57,7 @@ pub fn estimate_operation(kind: OperationKind, n: usize) -> ResourceRange { min: profile(0.000001 * count, 0.00005 * count, 64 * n.max(1)), typical: profile(0.000005 * count, 0.0001 * count, 128 * n.max(1)), max: profile(0.00005 * count, 0.001 * count, 256 * n.max(1)), - confidence: Confidence::Calibrated, + confidence: Confidence::Estimated, } } OperationKind::Sort => { @@ -70,7 +68,7 @@ pub fn estimate_operation(kind: OperationKind, n: usize) -> ResourceRange { min: profile(0.00001 * nlogn, 0.0001 * nlogn, 8 * n), typical: profile(0.00005 * nlogn, 0.0005 * nlogn, 16 * n), max: profile(0.0005 * nlogn, 0.005 * nlogn, 32 * n), - confidence: Confidence::Calibrated, + confidence: Confidence::Estimated, } } OperationKind::FileIO => { @@ -99,7 +97,7 @@ pub fn estimate_operation(kind: OperationKind, n: usize) -> ResourceRange { min: profile(0.000001 * bytes / 1000.0, 0.00001, n), typical: profile(0.00001 * bytes / 1000.0, 0.0001, n + 64), max: profile(0.0001 * bytes / 1000.0, 0.001, n + 4096), - confidence: Confidence::Calibrated, + confidence: Confidence::Estimated, } } OperationKind::StringOp => { @@ -119,7 +117,7 @@ pub fn estimate_operation(kind: OperationKind, n: usize) -> ResourceRange { min: profile(0.0000005 * ops, 0.000005 * ops, 0), typical: profile(0.000005 * ops, 0.00005 * ops, 0), max: profile(0.00005 * ops, 0.0005 * ops, 0), - confidence: Confidence::Calibrated, + confidence: Confidence::Estimated, } } OperationKind::Generic => { @@ -151,9 +149,8 @@ pub fn estimate_operation(kind: OperationKind, n: usize) -> ResourceRange { /// `Estimated`. pub fn operation_for_pattern(pattern: &str) -> Option { match pattern { - // Repeated comparison-bounded work. A depth-d loop is superlinear in - // its bound the way n·log n is, and comparison-bound is the dominant - // cost of sorting, so Sort is the closest priced category. + // Historical heuristic only: depth-d loops are NOT n log n sorting. + // Retained for report comparability, never promoted to Calibrated. "nested-loops" => Some(OperationKind::Sort), // A spin loop burns CPU without yielding: repeated arithmetic with no // allocation and no I/O, i.e. the MathCompute row. Note the row's @@ -192,7 +189,7 @@ mod tests { let range = estimate_operation(OperationKind::HashLookup, 1000); assert!(range.min.energy.0 < range.typical.energy.0); assert!(range.typical.energy.0 < range.max.energy.0); - assert_eq!(range.confidence, Confidence::Calibrated); + assert_eq!(range.confidence, Confidence::Estimated); } #[test] @@ -216,8 +213,8 @@ mod tests { let range = estimate_operation(kind, 10); assert_eq!( range.confidence, - Confidence::Calibrated, - "{kind:?} is calibrated data and must report Calibrated" + Confidence::Estimated, + "{kind:?} has no calibration receipt and must stay Estimated" ); } for kind in [ diff --git a/crates/oikosbot-analysis/src/dependencies.rs b/crates/oikosbot-analysis/src/dependencies.rs index adf2f5c..fe36d92 100644 --- a/crates/oikosbot-analysis/src/dependencies.rs +++ b/crates/oikosbot-analysis/src/dependencies.rs @@ -224,6 +224,7 @@ fn dep_finding_to_result(finding: DepFinding) -> AnalysisResult { let severity_boost = if finding.uses_all_features { 2.0 } else { 1.0 }; AnalysisResult { + taxonomy: None, location: CodeLocation { file: finding.manifest, line: 0, diff --git a/crates/oikosbot-analysis/src/security.rs b/crates/oikosbot-analysis/src/security.rs index ef3da5a..d9a936c 100644 --- a/crates/oikosbot-analysis/src/security.rs +++ b/crates/oikosbot-analysis/src/security.rs @@ -1,168 +1,38 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -//! Security-sustainability correlation engine. +//! Reserved security-sustainability integration boundary. //! -//! Maps panic-attack weak points to sustainability impact, producing -//! findings that correlate security risk with ecological cost. -//! -//! This module is only available when the `panic-attack` feature is enabled. - -#[cfg(feature = "panic-attack")] -mod inner { - use anyhow::Result; - use oikosbot_metrics::*; - use std::path::Path; - - /// Security-sustainability correlation result - #[derive(Debug, Clone)] - pub struct SecurityCorrelation { - /// The original analysis results for the repo - pub eco_results: Vec, - /// Security-derived sustainability findings - pub security_findings: Vec, - /// Composite score (0-100) combining security and eco scores - pub composite_score: f64, - } - - /// Run panic-attack scan and correlate with sustainability analysis. - pub fn correlate( - repo_path: &Path, - eco_results: &[AnalysisResult], - ) -> Result { - // Run panic-attack scan - let report = panic_attack::xray::analyze(repo_path)?; - - let mut security_findings = Vec::new(); - - for wp in &report.weak_points { - let (impact_desc, energy_multiplier) = match wp.category { - panic_attack::types::WeakPointCategory::UnsafeCode => ( - "Crash risk from unsafe code: all prior computation wasted on crash", - 3.0, - ), - panic_attack::types::WeakPointCategory::PanicPath => ( - "Panic/abort path: energy and carbon invested in computation becomes sunk cost", - 2.5, - ), - panic_attack::types::WeakPointCategory::UnboundedLoop => { - ("Unbounded loop: potential CPU waste and carbon spike", 4.0) - } - panic_attack::types::WeakPointCategory::UncheckedAllocation => ( - "Unchecked allocation: memory waste from potential OOM or oversized alloc", - 2.0, - ), - panic_attack::types::WeakPointCategory::ResourceLeak => ( - "Resource leak: ongoing waste of handles, goroutines, or memory", - 3.5, - ), - panic_attack::types::WeakPointCategory::RaceCondition => ( - "Race condition: unpredictable resource usage, potential for wasted retries", - 2.0, - ), - panic_attack::types::WeakPointCategory::BlockingIO => ( - "Blocking I/O: thread held idle, wasting CPU time and energy", - 1.5, - ), - panic_attack::types::WeakPointCategory::DeadlockPotential => ( - "Deadlock potential: complete CPU waste when threads stall", - 4.0, - ), - }; - - let severity_str = format!("{}", wp.severity); - - // Estimate energy impact based on severity and category - let base_energy = match wp.severity { - panic_attack::types::Severity::Critical => 100.0, - panic_attack::types::Severity::High => 50.0, - panic_attack::types::Severity::Medium => 20.0, - panic_attack::types::Severity::Low => 5.0, - }; - - let energy = Energy::joules(base_energy * energy_multiplier); - let carbon = crate::carbon::estimate_carbon(energy); - - let file = wp - .location - .clone() - .unwrap_or_else(|| "".to_string()); - - // Check if any eco result overlaps this location (boost severity) - let has_eco_overlap = eco_results.iter().any(|r| r.location.file == file); - let boost = if has_eco_overlap { 1.5 } else { 1.0 }; - - let eco_score = EcoScore::new((100.0 - (energy.0 * boost).ln() * 10.0).max(0.0)); - let econ_score = EconScore::new(50.0); // neutral economic impact - - let health = HealthIndex::compute(eco_score, econ_score, 50.0); - - let finding = AnalysisResult { - location: CodeLocation { - file, - line: 0, // panic-attack doesn't provide line numbers in WeakPoint - column: 0, - end_line: None, - end_column: None, - name: Some(format!("{:?}", wp.category)), - }, - resources: ResourceProfile { - energy, - duration: Duration::milliseconds(base_energy * 0.5), - carbon, - memory: Memory::kilobytes((base_energy * 10.0) as usize), - }, - health, - recommendations: vec![ - format!("[{}] {}", severity_str, impact_desc), - wp.description.clone(), - ], - rule_id: "oikosbot/security-sustainability".to_string(), - suggestion: Some(format!( - "Address {:?} weak point to prevent {} energy waste", - wp.category, impact_desc - )), - end_location: None, - confidence: Confidence::Estimated, - pareto: None, - resource_range: None, - }; - - security_findings.push(finding); - } - - // Calculate composite score - let all_eco: Vec = eco_results - .iter() - .chain(security_findings.iter()) - .map(|r| r.health.eco_score.0) - .collect(); - - let composite_score = if all_eco.is_empty() { - 100.0 - } else { - all_eco.iter().sum::() / all_eco.len() as f64 - }; +//! The upstream panic-attacker dependency was removed when this workspace +//! became standalone. Enabling the retained feature must compile, but cannot +//! pretend a scan happened. Restore the adapter only with pinned dependencies, +//! boundary tests, provenance and a reviewed result protocol. + +use anyhow::Result; +use oikosbot_metrics::AnalysisResult; +use std::path::Path; + +#[derive(Debug, Clone)] +pub struct SecurityCorrelation { + pub eco_results: Vec, + pub security_findings: Vec, + pub composite_score: f64, +} - Ok(SecurityCorrelation { - eco_results: eco_results.to_vec(), - security_findings, - composite_score, - }) - } +pub fn correlate( + _repo_path: &Path, + _eco_results: &[AnalysisResult], +) -> Result { + anyhow::bail!("security correlation unavailable: panic-attacker adapter is not integrated") } -#[cfg(feature = "panic-attack")] -pub use inner::*; +#[cfg(test)] +mod tests { + use super::*; -/// Stub types available even without the feature, for API compatibility -#[cfg(not(feature = "panic-attack"))] -pub mod stub { - /// Security correlation is unavailable without the `panic-attack` feature. - pub fn is_available() -> bool { - false + #[test] + fn unintegrated_scanner_never_returns_an_empty_success() { + let error = correlate(Path::new("."), &[]).unwrap_err(); + assert!(error.to_string().contains("unavailable")); } } - -#[cfg(not(feature = "panic-attack"))] -pub use stub::*; diff --git a/crates/oikosbot-cli/README.adoc b/crates/oikosbot-cli/README.adoc index a4a45cd..548dbb9 100644 --- a/crates/oikosbot-cli/README.adoc +++ b/crates/oikosbot-cli/README.adoc @@ -1,64 +1,26 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell = `oikosbot-cli` -:toc: macro -The `oikosbot` binary — the product surface. Every other crate is a library; -this is what the container image runs and what the composite action invokes. +The `oikosbot` binary is the standalone product surface used by the container +and composite Action. `main.rs` dispatches file/directory analysis, reports, +comparisons and optional-adapter requests; `config.rs` resolves `.oikos.yml`; +`estate.rs` stages and analyzes read-only portfolio telemetry. -== Subcommands +See link:../../docs/usage.adoc[usage] for commands, config and the exit contract. +Reports are advisory. Requested blocking checks cannot pass on heuristic evidence: +they return 2 (no check), not 0. Output format does not change enforcement. +Parser/config/traversal errors and unavailable requested integrations are errors, +not silently dropped inputs. Tracing goes to stderr so JSON stdout stays parseable. -[cols="1,4"] -|=== -| `check ` | Analyse a directory and compare each finding's eco score -against the floor. Below-floor findings are listed; the run exits 1 only when -the config makes enforcement blocking (`mode: regulator` or -`enforcement: blocking`). - -| `report ` | Same analysis, emit SARIF/JSON/text. This is the action's -default mode. - -| `compare ` | Pareto verdict for a diff: improvement, regression, -trade-off or neutral, with per-objective deltas and the confidence that backs -them. `--check` enforces the trade-off doctrine; `--pr-body` supplies the PR -description to check for a documented `Pareto-Trade-off:`. - -| `estate collect|analyse|report` | Estate telemetry, capability and DEA -analysis. Read-only by design. - -| `analyze ` | Analyse a single file. - -| `self-analyze` | Dogfooding: run the analyser over this repository. - -| `fleet ` | Prints how to run the optional `oikosbot-fleet` bridge, -which is deliberately *not* a dependency of this binary. -|=== - -== Configuration - -`--config ` or an auto-discovered `.oikos.yml` next to the analysed -directory. Keys that take effect: `mode`, `thresholds.eco_minimum.carbon` / -`.energy`, `enforcement`, `exclude`, `analysis.languages`. Everything else in -`config/oikos.yaml` is parsed and discarded (documented as aspirational there). - -== The enforcement doctrine - -`compare --check` exits 1 on an undocumented regression or trade-off **only -when every driving objective is `Measured` or `Calibrated`**. When it cannot -enforce, it prints a `::warning::` naming the verdict and the confidence, and -exits 0 — never silently. A gate that quietly does nothing is indistinguishable -from one that passed. - -=== Integration tests - -`tests/check_gate.rs` proves the doctrine end to end against the real binary: -an undocumented calibrated regression exits 1, the same regression without -`--check` exits 0, a documented trade-off is accepted, and a heuristic -regression refuses to block *and says so*. - -== Tests +`tests/check_gate.rs` provides end-to-end controls for advisory/no-check behavior, +JSON/text output, clean/firing SARIF and taxonomy, exclusions, malformed input, +unsupported config languages and symlinks. These are authored tests, not a proof +that ecological predictions are correct. No current per-file model is calibrated. ---- -cargo test -p oikosbot-cli -cargo test -p oikosbot-cli --test check_gate +cargo test --locked -p oikosbot-cli --all-features ---- + +The optional fleet bridge is not a dependency. Haskell, the App and VeriSimDB +are not implied operational by a successful CLI build. diff --git a/crates/oikosbot-cli/src/config.rs b/crates/oikosbot-cli/src/config.rs index 2a3cb19..9496aee 100644 --- a/crates/oikosbot-cli/src/config.rs +++ b/crates/oikosbot-cli/src/config.rs @@ -93,16 +93,17 @@ pub fn load(path: &Path) -> Result { Some("consultant") => Mode::Consultant, Some("regulator") => Mode::Regulator, Some("advisor") | None => Mode::Advisor, - Some(other) => { - tracing::warn!( - "unknown mode {other:?} in {}; using advisor", - path.display() - ); - Mode::Advisor - } + Some(other) => anyhow::bail!("unknown mode {other:?} in {}", path.display()), }; let level = raw.thresholds.eco_minimum.unwrap_or_default(); + anyhow::ensure!( + matches!( + level.enforcement.as_deref(), + None | Some("blocking" | "warning" | "advisory") + ), + "unknown threshold enforcement mode" + ); let enforcement_blocking = mode == Mode::Regulator || level.enforcement.as_deref() == Some("blocking"); // The eco floor: carbon threshold, falling back to energy if only that @@ -115,7 +116,7 @@ pub fn load(path: &Path) -> Result { Ok(g) => { globs.add(g); } - Err(e) => tracing::warn!("ignoring invalid exclude glob {pattern:?}: {e}"), + Err(e) => anyhow::bail!("invalid exclude glob {pattern:?}: {e}"), } } let exclude = globs.build().context("cannot build exclude globs")?; @@ -123,24 +124,14 @@ pub fn load(path: &Path) -> Result { let allowed_extensions: Vec<&'static str> = if raw.analysis.languages.is_empty() { DEFAULT_EXTENSIONS.to_vec() } else { - let exts: Vec<&'static str> = raw - .analysis + raw.analysis .languages .iter() - .filter_map(|l| language_extension(l)) - .collect(); - if exts.is_empty() { - // Config names only languages the analyzer cannot parse yet; - // analyzing nothing would be a silent no-scan. Fall back loudly. - tracing::warn!( - "no configured language is supported by the analyzer; \ - falling back to {:?}", - DEFAULT_EXTENSIONS - ); - DEFAULT_EXTENSIONS.to_vec() - } else { - exts - } + .map(|language| { + language_extension(language) + .with_context(|| format!("unsupported configured language: {language}")) + }) + .collect::>>()? }; Ok(ResolvedConfig { @@ -205,7 +196,6 @@ exclude: analysis: languages: - rust - - typescript - python "#, ); @@ -215,7 +205,7 @@ analysis: assert!(!c.enforcement_blocking); assert!(c.exclude.is_match("crates/foo/target/debug/x.rs")); assert!(!c.exclude.is_match("crates/foo/src/lib.rs")); - // typescript is listed but unsupported; rust + python survive. + // Both configured languages are supported. assert_eq!(c.allowed_extensions, vec!["rs", "py"]); } diff --git a/crates/oikosbot-cli/src/estate.rs b/crates/oikosbot-cli/src/estate.rs index c9c2943..fe4fc11 100644 --- a/crates/oikosbot-cli/src/estate.rs +++ b/crates/oikosbot-cli/src/estate.rs @@ -254,9 +254,18 @@ fn stage_json(path: &Path, items: &[T]) -> Result<()> { /// (see `stage_json`'s doc comment): that one is fatal, since nothing /// downstream can proceed for the owner without it. fn collect(gh: &dyn GhRunner, owners: &[String], out: &Path, max_runs: usize) -> Result<()> { + anyhow::ensure!(max_runs > 0, "max-runs must be positive"); fs::create_dir_all(out).with_context(|| format!("create staging dir {}", out.display()))?; + let mut failures = 0; for owner in owners { + anyhow::ensure!( + !owner.is_empty() + && owner + .bytes() + .all(|c| c.is_ascii_alphanumeric() || c == b'-'), + "invalid GitHub owner (must not be a path): {owner}" + ); let owner_dir = out.join(owner); fs::create_dir_all(&owner_dir) .with_context(|| format!("create owner staging dir {}", owner_dir.display()))?; @@ -287,6 +296,7 @@ fn collect(gh: &dyn GhRunner, owners: &[String], out: &Path, max_runs: usize) -> } Err(e) => { eprintln!("error: list_repos({owner}): {e:#}"); + failures += 1; continue; } } @@ -298,6 +308,17 @@ fn collect(gh: &dyn GhRunner, owners: &[String], out: &Path, max_runs: usize) -> continue; } let name = repo_name(&repo.repo); + anyhow::ensure!( + repo.repo == format!("{owner}/{name}") + && !name.is_empty() + && name != "." + && name != ".." + && name + .bytes() + .all(|c| c.is_ascii_alphanumeric() || b"._-".contains(&c)), + "invalid repository identity in staging: {}", + repo.repo + ); let runs_path = owner_dir.join(format!("runs-{name}.json")); if runs_path.exists() { @@ -311,9 +332,15 @@ fn collect(gh: &dyn GhRunner, owners: &[String], out: &Path, max_runs: usize) -> repo.repo, runs_path.display() ), - Err(e) => eprintln!("error: stage runs for {}: {e:#}", repo.repo), + Err(e) => { + failures += 1; + eprintln!("error: stage runs for {}: {e:#}", repo.repo); + } }, - Err(e) => eprintln!("error: collect_runs({}): {e:#}", repo.repo), + Err(e) => { + failures += 1; + eprintln!("error: collect_runs({}): {e:#}", repo.repo); + } } } @@ -329,14 +356,24 @@ fn collect(gh: &dyn GhRunner, owners: &[String], out: &Path, max_runs: usize) -> repo.repo, releases_path.display() ), - Err(e) => eprintln!("error: stage releases for {}: {e:#}", repo.repo), + Err(e) => { + failures += 1; + eprintln!("error: stage releases for {}: {e:#}", repo.repo); + } }, - Err(e) => eprintln!("error: collect_releases({}): {e:#}", repo.repo), + Err(e) => { + failures += 1; + eprintln!("error: collect_releases({}): {e:#}", repo.repo); + } } } } } + anyhow::ensure!( + failures == 0, + "incomplete collection: {failures} operations failed; staged successes retained for retry" + ); Ok(()) } @@ -348,7 +385,8 @@ fn collect(gh: &dyn GhRunner, owners: &[String], out: &Path, max_runs: usize) -> fn load_json_prefixed(dir: &Path, prefix: &str) -> Result> { let mut paths: Vec = WalkDir::new(dir) .into_iter() - .filter_map(|e| e.ok()) + .collect::, _>>()? + .into_iter() .filter(|e| e.file_type().is_file()) .map(|e| e.into_path()) .filter(|p| { @@ -375,6 +413,36 @@ fn analyse(staging: &Path, snapshot_dir: &Path) -> Result<()> { let runs: Vec = load_json_prefixed(staging, "runs-")?; let repos: Vec = load_json_prefixed(staging, "repos-")?; let releases: Vec = load_json_prefixed(staging, "releases-")?; + anyhow::ensure!(!repos.is_empty(), "no repository inventory in staging"); + for repo in repos.iter().filter(|repo| !repo.archived) { + let (owner, name) = repo + .repo + .split_once('/') + .context("invalid staged repository identity")?; + anyhow::ensure!( + !owner.is_empty() + && !name.is_empty() + && owner + .bytes() + .all(|c| c.is_ascii_alphanumeric() || c == b'-') + && name != "." + && name != ".." + && name + .bytes() + .all(|c| c.is_ascii_alphanumeric() || b"._-".contains(&c)), + "unsafe staged repository identity" + ); + for prefix in ["runs", "releases"] { + anyhow::ensure!( + staging + .join(owner) + .join(format!("{prefix}-{name}.json")) + .is_file(), + "incomplete staging: missing {prefix} for {}", + repo.repo + ); + } + } fs::create_dir_all(snapshot_dir) .with_context(|| format!("create snapshot dir {}", snapshot_dir.display()))?; @@ -699,3 +767,24 @@ mod tests { .unwrap(); } } + +#[cfg(test)] +mod collection_boundary_tests { + use super::*; + struct Failed; + impl GhRunner for Failed { + fn api(&self, _path: &str) -> Result { + anyhow::bail!("HTTP 429") + } + } + #[test] + fn partial_collection_is_not_success() { + let dir = tempfile::tempdir().unwrap(); + assert!(collect(&Failed, &["o".into()], dir.path(), 200).is_err()); + } + #[test] + fn owner_is_not_a_filesystem_path() { + let dir = tempfile::tempdir().unwrap(); + assert!(collect(&Failed, &["../escape".into()], dir.path(), 200).is_err()); + } +} diff --git a/crates/oikosbot-cli/src/main.rs b/crates/oikosbot-cli/src/main.rs index a8dbb04..7a54884 100644 --- a/crates/oikosbot-cli/src/main.rs +++ b/crates/oikosbot-cli/src/main.rs @@ -4,7 +4,7 @@ //! # OikosBot CLI //! //! Ecological and economic code analysis tool. -//! Built with Eclexia principles - proving resource-aware design works. +//! Built with Eclexia principles - exploring resource-aware design. #![forbid(unsafe_code)] mod config; @@ -166,12 +166,23 @@ enum Commands { }, } -fn main() -> Result<()> { +fn main() { + if let Err(error) = run() { + eprintln!("no check performed: {error:#}"); + std::process::exit(2); + } +} + +fn run() -> Result<()> { let cli = Cli::parse(); + let threshold_check = matches!(&cli.command, Commands::Check { .. }); // Set up logging let log_level = if cli.verbose { "debug" } else { "info" }; - tracing_subscriber::fmt().with_env_filter(log_level).init(); + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_env_filter(log_level) + .init(); match cli.command { Commands::Analyze { @@ -218,6 +229,10 @@ fn main() -> Result<()> { .or(cfg.as_ref().and_then(|c| c.eco_threshold)) .unwrap_or(50.0); + anyhow::ensure!( + eco_threshold.is_finite() && (0.0..=100.0).contains(&eco_threshold), + "eco threshold must be finite and between 0 and 100" + ); let mut all_results = collect_directory_results(&path, cfg.as_ref())?; // Intra-repo Pareto pass over the organic code units (before @@ -227,12 +242,12 @@ fn main() -> Result<()> { // Security-sustainability correlation if security { - run_security_correlation(&path, &mut all_results); + run_security_correlation(&path, &mut all_results)?; } // Policy evaluation if let Some(ref pdir) = policy_dir { - run_policy_evaluation(pdir, &mut all_results); + run_policy_evaluation(pdir, &mut all_results)?; } // Emit formatted output @@ -270,25 +285,26 @@ fn main() -> Result<()> { println!("\nOutput written to: {}", out_path.display()); } - // Without a config, keep the historical blocking behaviour. - // With one, honor its enforcement: advisor/warning configs - // report without failing the run; blocking/regulator fail. - let blocking = cfg.as_ref().is_none_or(|c| c.enforcement_blocking); - if files_below_threshold > 0 { - if blocking { - std::process::exit(1); - } else { - println!( - "\nAdvisory mode ({}): not failing the run.", - cfg.as_ref() - .map(|c| c.source.display().to_string()) - .unwrap_or_default() - ); - } - } + // Gate evaluation is below rendering, independent of format. } - _ => { - eprintln!("Unsupported format: {}", format); + _ => anyhow::bail!("Unsupported format: {}", format), + } + // `report` is always advisory. `check` must not green-light a gate + // merely because estimates have fallen below/above a score floor. + if threshold_check && cfg.as_ref().is_none_or(|c| c.enforcement_blocking) { + anyhow::ensure!( + all_results.iter().all(|r| matches!( + r.confidence, + oikosbot_metrics::Confidence::Measured + | oikosbot_metrics::Confidence::Calibrated + )), + "threshold check NOT enforced: resource figures are estimates; use report for advisory output" + ); + if all_results + .iter() + .any(|r| r.health.eco_score.0 < eco_threshold) + { + std::process::exit(1); } } } @@ -361,7 +377,14 @@ fn main() -> Result<()> { None => println!("{}", text), } } - _ => print_comparison(&assessment, confidence, documented), + "text" => { + let text = format_comparison(&assessment, confidence, documented); + match output { + Some(ref path) => fs::write(path, text)?, + None => print!("{text}"), + } + } + _ => anyhow::bail!("Unsupported comparison format: {}", format), } // Advisory by default; --check enforces the trade-off doctrine — @@ -371,27 +394,16 @@ fn main() -> Result<()> { oikosbot_pareto::ParetoVerdict::Regression | oikosbot_pareto::ParetoVerdict::TradeOff ); - if check && needs_documentation && documented != Some(true) { - if assessment.actionable { + if check { + anyhow::ensure!( + assessment.verdict != oikosbot_pareto::ParetoVerdict::Indeterminate + && matches!(confidence, oikosbot_metrics::Confidence::Measured + | oikosbot_metrics::Confidence::Calibrated), + "--check requested but NOT enforced: no validated resource evidence; advisory result only (use compare without --check)" + ); + if needs_documentation && documented != Some(true) && assessment.actionable { std::process::exit(1); } - // Enforcement was asked for and cannot be delivered: the - // driving objectives rest on heuristic estimates, which by - // design may not block a merge. Say so unmissably. A gate - // that silently no-ops is indistinguishable from one that - // passed, which is the failure mode this tool exists to - // find — so it must never be silent about its own limits. - eprintln!( - "::warning::--check requested but NOT enforced: the objectives driving \ - this {} verdict are {:?}, and only Measured or Calibrated inputs may \ - block. Reported as advisory. See docs: enforcement is inert while \ - resource figures remain heuristic estimates.", - match assessment.verdict { - oikosbot_pareto::ParetoVerdict::Regression => "pareto-regression", - _ => "trade-off", - }, - confidence, - ); } } @@ -463,10 +475,11 @@ fn collect_directory_results( "target" | "node_modules" | ".git" | "dist" | "build" | ".cache" ) }) - .filter_map(|e| e.ok()) { + let entry = entry?; let entry_path = entry.path(); - if !entry_path.is_file() { + // Do not follow symlink files outside the declared analysis boundary. + if !entry.file_type().is_file() { continue; } @@ -487,16 +500,14 @@ fn collect_directory_results( } } - match analyze_file(entry_path) { - Ok(results) => { - all_results.extend(results); - } - Err(e) => { - info!("Skipping {}: {}", entry_path.display(), e); - } - } + all_results.extend(analyze_file(entry_path)?); } + anyhow::ensure!( + !all_results.is_empty(), + "no analyzable code units under {} (unsupported, empty or excluded input)", + path.display() + ); Ok(all_results) } @@ -506,17 +517,12 @@ fn emit_output( format: &str, output: Option<&std::path::Path>, ) -> Result<()> { + anyhow::ensure!(!results.is_empty(), "no analyzable code units"); let text = match format { "sarif" => oikosbot_sarif::to_sarif_json(results, VERSION)?, "json" => serde_json::to_string_pretty(results)?, - "text" => { - print_results_text(results); - return Ok(()); - } - other => { - eprintln!("Unsupported format: {}", other); - return Ok(()); - } + "text" => format_results_text(results), + other => anyhow::bail!("Unsupported format: {}", other), }; match output { @@ -532,64 +538,37 @@ fn emit_output( Ok(()) } -fn print_comparison( +fn format_comparison( assessment: &oikosbot_pareto::Comparison, confidence: oikosbot_metrics::Confidence, documented: Option, -) { - let verdict_label = match assessment.verdict { +) -> String { + let label = match assessment.verdict { oikosbot_pareto::ParetoVerdict::Improvement => "PARETO IMPROVEMENT", oikosbot_pareto::ParetoVerdict::Regression => "PARETO REGRESSION", oikosbot_pareto::ParetoVerdict::TradeOff => "TRADE-OFF", oikosbot_pareto::ParetoVerdict::Neutral => "NEUTRAL", + oikosbot_pareto::ParetoVerdict::Indeterminate => "NO COMPARISON", }; - println!("Pareto verdict: {}\n", verdict_label); - - println!( - " {:<20} {:>14} {:>14} movement", - "objective", "base", "head" - ); + let mut text = format!("Pareto verdict: {label}\nConfidence: {confidence:?}\n"); for d in &assessment.deltas { - let movement = if d.improvement > oikosbot_pareto::DEFAULT_EPSILON { - "improved" - } else if d.improvement < -oikosbot_pareto::DEFAULT_EPSILON { - "worsened" - } else { - "unchanged" - }; - println!( - " {:<20} {:>14.4} {:>14.4} {}", - d.name, d.base, d.head, movement - ); + text.push_str(&format!( + " {}: base {:.4}, head {:.4}, signed improvement {:.4}\n", + d.name, d.base, d.head, d.improvement + )); } - - if !assessment.drivers.is_empty() { - println!("\nDrivers: {}", assessment.drivers.join(", ")); + text.push_str(&format!("Drivers: {}\n", assessment.drivers.join(", "))); + if !assessment.actionable { + text.push_str("Advisory: heuristic estimates cannot drive blocking decisions.\n"); } - println!( - "Confidence: {:?}{}", - confidence, - if assessment.actionable { - "" + if let Some(present) = documented { + text.push_str(if present { + "Trade-off documentation: present (not approval)\n" } else { - " (advisory - heuristic estimates cannot drive blocking decisions)" - } - ); - - match (assessment.verdict, documented) { - ( - oikosbot_pareto::ParetoVerdict::TradeOff | oikosbot_pareto::ParetoVerdict::Regression, - Some(true), - ) => println!("Trade-off documentation: present"), - ( - oikosbot_pareto::ParetoVerdict::TradeOff | oikosbot_pareto::ParetoVerdict::Regression, - Some(false), - ) => println!( - "Trade-off documentation: MISSING - add a \"Pareto-Trade-off:\" trailer \ - (competing objectives, decision, why it is Pareto-optimal here, rough metric impact)" - ), - _ => {} + "Trade-off documentation: MISSING\n" + }); } + text } fn print_summary( @@ -675,7 +654,7 @@ fn print_results_text(results: &[oikosbot_metrics::AnalysisResult]) { fn run_policy_evaluation( policy_dir: &std::path::Path, results: &mut Vec, -) { +) -> Result<()> { match oikosbot_eclexia::evaluate_policies(policy_dir, results) { Ok(decisions) => { let warns = decisions @@ -697,58 +676,26 @@ fn run_policy_evaluation( results.extend(policy_results); } Err(e) => { - eprintln!("Policy evaluation failed: {}", e); + return Err(e); } } + Ok(()) } /// Run security-sustainability correlation if the feature is available fn run_security_correlation( path: &std::path::Path, - _results: &mut Vec, -) { - // Check for .machine_readable/bot_directives/panic-attack.scm - let directive = oikosbot_analysis::directives::check_directive(path, "panic-attack"); - - match directive { - Some(ref d) if !d.allow => { - eprintln!( - "Security scan denied by .machine_readable/bot_directives/panic-attack.scm: {}", - d.notes.as_deref().unwrap_or("no reason given") - ); - return; - } - None => { - eprintln!( - "Warning: No .machine_readable/bot_directives/panic-attack.scm found in {}. \ - Running security scan anyway.", - path.display() - ); - } - _ => {} - } - - #[cfg(feature = "security")] - { - match oikosbot_analysis::security::correlate(path, results) { - Ok(correlation) => { - eprintln!( - "Security scan: {} findings, composite score: {:.1}", - correlation.security_findings.len(), - correlation.composite_score - ); - results.extend(correlation.security_findings); - } - Err(e) => { - eprintln!("Security scan failed: {}", e); - } - } - } - - #[cfg(not(feature = "security"))] - { - eprintln!("Security correlation unavailable: build with --features security"); + results: &mut Vec, +) -> Result<()> { + if let Some(directive) = oikosbot_analysis::directives::check_directive(path, "panic-attack") { + anyhow::ensure!( + directive.allow, + "security scan denied by repository directive" + ); } + let correlation = oikosbot_analysis::security::correlate(path, results)?; + results.extend(correlation.security_findings); + Ok(()) } fn format_results_text(results: &[oikosbot_metrics::AnalysisResult]) -> String { @@ -772,6 +719,16 @@ fn format_results_text(results: &[oikosbot_metrics::AnalysisResult]) -> String { " Eco: {:.1}/100 Overall: {:.1}/100\n", result.health.eco_score.0, result.health.overall )); + out.push_str(&format!( + " Duration: {:.4} ms Memory: {} bytes\n Confidence: {:?}\n Rule: {}\n", + result.resources.duration.0, + result.resources.memory.0, + result.confidence, + result.rule_id + )); + for recommendation in &result.recommendations { + out.push_str(&format!(" Recommendation: {recommendation}\n")); + } } out diff --git a/crates/oikosbot-cli/tests/check_gate.rs b/crates/oikosbot-cli/tests/check_gate.rs index d1410d7..27b8114 100644 --- a/crates/oikosbot-cli/tests/check_gate.rs +++ b/crates/oikosbot-cli/tests/check_gate.rs @@ -1,22 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -//! End-to-end proof that `oikosbot compare --check` can actually block. -//! -//! Before the calibration wiring every finding was `Estimated`, so `--check` -//! could only ever print the inert-enforcement warning: the gate could not -//! fail. These tests pin the behaviour that makes the trade-off doctrine real — -//! -//! * a regression whose driving objectives are calibrated **blocks**; -//! * the same regression without `--check` stays advisory; -//! * a documented trade-off is accepted; -//! * a heuristic (unrecognised) regression still refuses to block, and says so -//! loudly rather than passing silently. -//! -//! Fixtures are sized so both sides land on a calibrated row: base is a -//! depth-3 loop nest, head the same nest one level deeper. Both are therefore -//! `Calibrated`, and the deeper nest is worse on every objective, so the -//! verdict is a genuine `Regression` with real drivers behind it. +//! End-to-end controls for advisory output and explicit no-check status. +//! Pattern-table estimates have no calibration receipts and must not gate. use std::fs; use std::path::Path; @@ -35,9 +21,9 @@ fn tree(files: &[(&str, &str)]) -> TempDir { /// A Rust function whose body is `depth` nested `for` loops. /// /// Depth 3 is the smallest nest `detect_patterns` flags as `nested-loops`, -/// which maps onto the calibrated `Sort` row. The body avoids every other +/// which maps onto the heuristic `Sort` row. The body avoids every other /// pattern — no `.clone()`, no `File::open`, no `vec![]`, no string `+`, no -/// `to_string` — so exactly one calibrated row prices the unit and nothing +/// `to_string` — so exactly one heuristic row prices the unit and nothing /// drags its confidence back to `Estimated`. fn nested_loop_work(depth: usize) -> String { let mut body = String::from("fn deep_work(items: &[u32]) -> usize {\n let mut total = 0;\n"); @@ -83,7 +69,7 @@ fn stderr(out: &Output) -> String { } #[test] -fn undocumented_calibrated_regression_blocks_under_check() { +fn recognized_regression_cannot_manufacture_a_gate() { let base = tree(&[("base.rs", &nested_loop_work(3))]); let head = tree(&[("head.rs", &nested_loop_work(4))]); @@ -91,8 +77,8 @@ fn undocumented_calibrated_regression_blocks_under_check() { assert_eq!( out.status.code(), - Some(1), - "an undocumented calibrated regression must exit 1; stdout: {}", + Some(2), + "an unvalidated regression must return no-check (2); stdout: {}", stdout(&out) ); let stdout = stdout(&out); @@ -101,8 +87,8 @@ fn undocumented_calibrated_regression_blocks_under_check() { "expected a regression verdict; stdout: {stdout}" ); assert!( - stdout.contains("Confidence: Calibrated"), - "the drivers must be calibrated, not heuristic; stdout: {stdout}" + stdout.contains("Confidence: Estimated"), + "the drivers must retain heuristic confidence; stdout: {stdout}" ); } @@ -123,7 +109,7 @@ fn same_regression_is_advisory_without_check() { } #[test] -fn documented_regression_is_accepted_under_check() { +fn documentation_cannot_manufacture_evidence() { let base = tree(&[("base.rs", &nested_loop_work(3))]); let head = tree(&[ ("head.rs", &nested_loop_work(4)), @@ -144,8 +130,8 @@ fn documented_regression_is_accepted_under_check() { assert_eq!( out.status.code(), - Some(0), - "a documented trade-off must be accepted; stdout: {}", + Some(2), + "documentation cannot make estimates enforceable; stdout: {}", stdout(&out) ); } @@ -159,8 +145,8 @@ fn heuristic_regression_refuses_to_block_and_says_so() { assert_eq!( out.status.code(), - Some(0), - "heuristic estimates advise, they do not block; stdout: {}", + Some(2), + "heuristic estimates return no-check rather than pass; stdout: {}", stdout(&out) ); assert!( @@ -169,3 +155,134 @@ fn heuristic_regression_refuses_to_block_and_says_so() { stderr(&out) ); } + +fn run(args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_oikosbot")) + .args(args) + .output() + .unwrap() +} + +#[test] +fn json_stdout_is_json_and_text_output_is_written() { + let dir = tree(&[("clean.rs", "fn clean() -> u32 { 1 }")]); + let source = dir.path().join("clean.rs"); + let out = run(&["analyze", source.to_str().unwrap(), "--format", "json"]); + assert!(out.status.success(), "{}", stderr(&out)); + let payload: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap(); + assert!(payload.is_array()); + let file = dir.path().join("output.txt"); + let out = run(&[ + "analyze", + source.to_str().unwrap(), + "--output", + file.to_str().unwrap(), + ]); + assert!(out.status.success()); + assert!(!fs::read_to_string(file).unwrap().is_empty()); +} + +#[test] +fn report_is_advisory_but_check_never_passes_on_estimates_in_any_format() { + let dir = tree(&[("clean.rs", "fn clean() -> u32 { 1 }")]); + for format in ["text", "json", "sarif"] { + let out = run(&[ + "report", + dir.path().to_str().unwrap(), + "--format", + format, + "--eco-threshold", + "100", + ]); + assert_eq!(out.status.code(), Some(0), "{}", stderr(&out)); + let out = run(&["check", dir.path().to_str().unwrap(), "--format", format]); + assert_eq!(out.status.code(), Some(2), "{}", stderr(&out)); + } +} + +#[test] +fn empty_invalid_excluded_and_unsupported_are_not_successful_scans() { + for files in [ + vec![], + vec![("broken.rs", "fn broken( {")], + vec![("unsupported.txt", "hello")], + vec![ + ("clean.rs", "fn clean() {}"), + (".oikos.yml", "exclude: ['**/*.rs']"), + ], + vec![ + ("clean.rs", "fn clean() {}"), + (".oikos.yml", "analysis:\n languages: [typescript]"), + ], + vec![ + ("clean.rs", "fn clean() {}"), + (".oikos.yml", "exclude: ['[']"), + ], + vec![("clean.rs", "fn clean() {}"), (".oikos.yml", "analysis: [")], + ] { + let dir = tree(&files); + let out = run(&["report", dir.path().to_str().unwrap()]); + assert_eq!(out.status.code(), Some(2), "{}", stderr(&out)); + } +} + +#[test] +fn silence_firing_and_taxonomy_survive_the_cli_sarif_boundary() { + let clean = tree(&[("clean.rs", "fn clean() -> u32 { 1 }")]); + let firing = tree(&[("loops.rs", &nested_loop_work(3))]); + for (dir, count) in [(&clean, 0), (&firing, 1)] { + let out = run(&["report", dir.path().to_str().unwrap(), "--format", "sarif"]); + assert!(out.status.success(), "{}", stderr(&out)); + let log: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap(); + let findings = log["runs"][0]["results"].as_array().unwrap(); + assert_eq!(findings.len(), count); + if count == 1 { + assert_eq!(findings[0]["ruleId"], "oikosbot/nested-loops"); + assert_eq!(findings[0]["properties"]["taxonomy"]["intent"], "wish"); + assert_eq!( + findings[0]["properties"]["taxonomy"]["locus"], + "externalities" + ); + assert!(findings[0].get("fixes").is_none()); + } + } +} + +#[cfg(unix)] +#[test] +fn directory_scan_does_not_follow_external_file_symlinks() { + let outside = tree(&[("source.rs", &nested_loop_work(3))]); + let root = tree(&[]); + std::os::unix::fs::symlink( + outside.path().join("source.rs"), + root.path().join("link.rs"), + ) + .unwrap(); + assert_eq!( + run(&["report", root.path().to_str().unwrap()]) + .status + .code(), + Some(2) + ); +} + +#[test] +fn requested_unavailable_security_is_not_silent_success() { + let dir = tree(&[("clean.rs", "fn clean() {}")]); + let out = run(&["report", dir.path().to_str().unwrap(), "--security"]); + assert_eq!(out.status.code(), Some(2)); + assert!(stderr(&out).contains("unavailable")); +} + +#[test] +fn requested_missing_policy_directory_is_not_silent_success() { + let dir = tree(&[("clean.rs", "fn clean() {}")]); + let missing = dir.path().join("missing-policies"); + let out = run(&[ + "report", + dir.path().to_str().unwrap(), + "--policy-dir", + missing.to_str().unwrap(), + ]); + assert_eq!(out.status.code(), Some(2)); +} diff --git a/crates/oikosbot-eclexia/src/lib.rs b/crates/oikosbot-eclexia/src/lib.rs index 08cad5d..12651e2 100644 --- a/crates/oikosbot-eclexia/src/lib.rs +++ b/crates/oikosbot-eclexia/src/lib.rs @@ -50,9 +50,14 @@ pub fn evaluate_policies( let mut decisions = Vec::new(); // Find all .ecl files in the policy directory - if !policy_dir.exists() { - return Ok(decisions); - } + anyhow::ensure!( + policy_dir.is_dir(), + "policy directory is missing or not a directory" + ); + anyhow::ensure!( + !results.is_empty(), + "no analysis results for policy evaluation" + ); let entries = std::fs::read_dir(policy_dir) .with_context(|| format!("Failed to read policy dir: {}", policy_dir.display()))?; @@ -71,19 +76,14 @@ pub fn evaluate_policies( match evaluate_single_policy(&path, results) { Ok(decision) => decisions.push(decision), Err(e) => { - tracing::warn!("Policy {} failed: {}", policy_name, e); - decisions.push(PolicyDecision { - outcome: PolicyOutcome::Warn, - message: format!("Policy evaluation error: {}", e), - suggestion: None, - evaluation_cost: None, - policy_name, - }); + return Err(e) + .with_context(|| format!("policy {policy_name} was not evaluated")) } } } } + anyhow::ensure!(!decisions.is_empty(), "no .ecl policies found"); Ok(decisions) } @@ -383,6 +383,7 @@ pub fn decisions_to_results(decisions: &[PolicyDecision]) -> Vec }); AnalysisResult { + taxonomy: None, location: oikosbot_metrics::CodeLocation { file: "policies/".to_string(), line: 0, @@ -507,6 +508,7 @@ mod tests { fn sample_result(energy_j: f64) -> AnalysisResult { AnalysisResult { + taxonomy: None, location: oikosbot_metrics::CodeLocation { file: "test.rs".to_string(), line: 1, diff --git a/crates/oikosbot-fleet/src/lib.rs b/crates/oikosbot-fleet/src/lib.rs index 18c0e85..8076335 100644 --- a/crates/oikosbot-fleet/src/lib.rs +++ b/crates/oikosbot-fleet/src/lib.rs @@ -363,6 +363,7 @@ mod tests { fn sample_result(energy_j: f64) -> AnalysisResult { AnalysisResult { + taxonomy: None, location: CodeLocation { file: "test.rs".to_string(), line: 1, diff --git a/crates/oikosbot-metrics/README.adoc b/crates/oikosbot-metrics/README.adoc index d0fdeea..eea648e 100644 --- a/crates/oikosbot-metrics/README.adoc +++ b/crates/oikosbot-metrics/README.adoc @@ -10,24 +10,23 @@ unit or a confidence, its type is defined in this crate. == What it owns * **Units** — `Energy` (J), `Duration` (ms), `Carbon` (gCO2e), `Memory` - (bytes), each a newtype over the base unit with constructors - (`Energy::joules`, `Carbon::kilograms_co2e`, …). Constructors, not bare - floats, so a unit mix-up cannot compile. + (bytes), each a newtype over a base unit. They distinguish API dimensions, + but public numeric constructors do not validate physical measurements. * **`ResourceProfile`** — the four-axis profile for one code unit, plus `Add` and `cost(&ShadowPrices)` for the Eclexia-style shadow-price valuation. -* **`ResourceRange`** — the min/typical/max envelope behind a calibrated +* **`ResourceRange`** — the min/typical/max envelope behind a pattern-table estimate. * **Scores** — `EcoScore`, `EconScore` (both clamped 0–100) and `HealthIndex::compute(eco, econ, quality)`, whose `overall` is `0.4·eco + 0.3·econ + 0.3·quality`. * **`Confidence`** — the ladder `Measured > Calibrated > Estimated > - `Unknown`, ordered by `PartialOrd`. Only `Measured` and `Calibrated` may - drive a blocking decision anywhere in the system. + `Unknown`. Resource labels alone do not authorize action; no current per-file + row has earned calibration evidence. * **`AnalysisResult`** / **`CodeLocation`** / **`ParetoInfo`** — the finding - record. Every field after `resources` is `#[serde(default)]`, so a JSON - document written by an older version still loads; `resource_range` is - additionally skipped when absent, keeping the wire format stable. + record. New optional `taxonomy` and `resource_range` fields default to None + for older JSON and are skipped when absent; core resource/health fields remain + required. Adding fields is a Rust struct-literal source compatibility change. == What it does not do diff --git a/crates/oikosbot-metrics/src/lib.rs b/crates/oikosbot-metrics/src/lib.rs index 325a5ff..a1ed4bb 100644 --- a/crates/oikosbot-metrics/src/lib.rs +++ b/crates/oikosbot-metrics/src/lib.rs @@ -138,7 +138,7 @@ pub struct ResourceProfile { /// [`AnalysisResult::resources`] collapses an estimate to a single point (the /// `typical` bound). The envelope is kept alongside it so consumers can see the /// uncertainty band instead of treating the point estimate as exact. It is -/// present only when the estimate came from a calibrated operation profile (see +/// present only when the estimate came from a heuristic operation profile (see /// `oikosbot_analysis::calibration::estimate_operation`); the naive /// `complexity`-derived path produces a point estimate with no band and leaves /// this `None`. @@ -280,9 +280,68 @@ pub struct ParetoInfo { pub dominated_by: usize, } +/// Descriptive taxonomy, not permission to modify code. Resource confidence +/// is an evidence category, NOT a calibrated probability of correctness. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FindingTaxonomy { + pub intent: Intent, + pub maintenance: Maintenance, + pub locus: Locus, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Intent { + Must, + Intend, + Wish, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Maintenance { + Corrective, + Adaptive, + Perfective, + Preventive, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Locus { + Systems, + Compliance, + Externalities, +} + +impl FindingTaxonomy { + /// Authored rule classification. No auto-fix authority is implied, even + /// if a future resource estimate is Measured. Unclassified rules retain + /// None rather than fabricated tags. + pub fn for_rule(rule: &str) -> Option { + match rule { + "oikosbot/nested-loops" + | "oikosbot/busy-wait" + | "oikosbot/string-concat-in-loop" + | "oikosbot/clone-in-loop" + | "oikosbot/unbuffered-io" + | "oikosbot/large-allocation" + | "oikosbot/redundant-allocation" => Some(Self { + intent: Intent::Wish, + maintenance: Maintenance::Perfective, + locus: Locus::Externalities, + }), + _ => None, + } + } +} + /// Analysis result for a single code unit (function, file, module) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AnalysisResult { + /// None for telemetry, legacy input or a rule not yet classified. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub taxonomy: Option, pub location: CodeLocation, pub resources: ResourceProfile, pub health: HealthIndex, @@ -303,7 +362,7 @@ pub struct AnalysisResult { /// Pareto pass; None when analyzed in isolation). #[serde(default)] pub pareto: Option, - /// Uncertainty band behind `resources`, when the estimate is calibrated + /// Uncertainty band behind `resources`, when the estimate uses the pattern table /// (see [`ResourceRange`]). `None` on the naive `complexity`-derived path, /// where `resources` is a point estimate with no band. #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/crates/oikosbot-pareto/README.adoc b/crates/oikosbot-pareto/README.adoc index 1f2ebfc..8330db5 100644 --- a/crates/oikosbot-pareto/README.adoc +++ b/crates/oikosbot-pareto/README.adoc @@ -9,16 +9,17 @@ verdict that the `compare` subcommand reports. == Public surface * **Objectives** — `result_objectives()` is the live five-axis set the Rust - analyser actually measures: carbon, energy, duration, memory (minimise) and + analyser actually models: carbon, energy, duration, memory (minimise) and the quality score as the maintainability proxy (maximise). * **Dominance and frontiers** — `dominates`, `frontier_indices`, `dominated_by_counts`, `normalize`, `pareto_scores`, `refactor_candidates`, - all ε-tolerant (`DEFAULT_EPSILON = 1e-6`). + with tolerance suppressing tiny wins but never permitting losses + (`DEFAULT_EPSILON = 1e-6`). * **Verdicts** — `compare` classifies head vs base as `Improvement`, - `Regression`, `TradeOff` or `Neutral`; `assess` adds the per-objective + `Regression`, `TradeOff`, `Neutral` or `Indeterminate`; `assess` adds the per-objective deltas, the list of *drivers* (objectives that actually moved), and `actionable`. -* **Confidence gating** — `actionable` is true only when **every driving** +* **Confidence gating** — `actionable` is true only when **every** objective is backed by `Measured` or `Calibrated` input. Heuristic estimates advise; they do not block. `aggregate_confidence` and `weaker_confidence` combine confidences across a result set. @@ -29,7 +30,12 @@ verdict that the `compare` subcommand reports. == What it does not do It never decides whether a finding is *true* — it compares numbers it is given. -`actionable` is a statement about evidence, not about severity. +`actionable` is a legacy confidence-label eligibility flag, not authenticated +evidence or authority to act. The CLI supplies only estimates today. + +`contract.rs` is an experimental scoped interval refinement: explicit contexts/units, +non-compensatory limits, and unresolved overlapping uncertainty. It is not wired +as production authorization. See link:../../docs/research/EVIDENCE-SCOPED-PARETO.adoc[design and proof obligations]. == Tests diff --git a/crates/oikosbot-pareto/src/contract.rs b/crates/oikosbot-pareto/src/contract.rs new file mode 100644 index 0000000..e520501 --- /dev/null +++ b/crates/oikosbot-pareto/src/contract.rs @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +//! Experimental evidence-scoped, non-compensatory decision contract. +//! +//! This is a runtime refinement prototype, not a new type calculus or proof +//! of environmental benefit. Private constructors prevent unchecked numeric +//! states; they do NOT authenticate a caller's model, receipts or authority. +//! In particular there is no conversion from this module to permission to +//! merge, auto-fix, spend a budget, or claim a real-world Pareto improvement. + +use crate::Direction; +use oikosbot_metrics::Confidence; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Interval { + lower: f64, + upper: f64, +} + +impl Interval { + pub fn new(lower: f64, upper: f64) -> Result { + if !lower.is_finite() || !upper.is_finite() || lower > upper { + return Err("interval must have ordered finite bounds"); + } + Ok(Self { lower, upper }) + } +} + +/// Axes are dimensioned and ordered explicitly. A limit is a hard ceiling +/// for Minimize or hard floor for Maximize, never a compensable weight. +#[derive(Debug, Clone, PartialEq)] +pub struct Axis { + pub name: String, + pub unit: String, + pub direction: Direction, + pub tolerance: f64, + pub limit: Option, +} + +/// Equality is a compatibility requirement, not evidence that the context +/// is faithful to reality. All identifiers should be content-addressed in a +/// future ingestion adapter. No serde deserializer may bypass validation. +#[derive(Debug, Clone, PartialEq)] +pub struct Context { + pub workload: String, + pub functional_unit: String, + pub boundary: String, + pub horizon: String, + pub model_revision: String, + pub policy_revision: String, + pub axes: Vec, +} + +impl Context { + fn validate(&self) -> Result<(), &'static str> { + if [ + &self.workload, + &self.functional_unit, + &self.boundary, + &self.horizon, + &self.model_revision, + &self.policy_revision, + ] + .iter() + .any(|s| s.trim().is_empty()) + || self.axes.is_empty() + { + return Err("comparison requires an explicit nonempty context"); + } + for (i, axis) in self.axes.iter().enumerate() { + if axis.name.trim().is_empty() + || axis.unit.trim().is_empty() + || !axis.tolerance.is_finite() + || axis.tolerance < 0.0 + || axis.limit.is_some_and(|limit| !limit.is_finite()) + || self.axes[..i].iter().any(|a| a.name == axis.name) + { + return Err("invalid or duplicate objective"); + } + } + Ok(()) + } +} + +/// A declared warrant, NOT an authenticated proof. One per objective; a +/// caller cannot turn an energy receipt into evidence for quality or carbon. +#[derive(Debug, Clone)] +pub struct Evidence { + pub confidence: Confidence, + pub source: String, + pub assumptions: Vec, +} + +#[derive(Debug)] +pub struct Observation<'a> { + context: &'a Context, + values: Vec, + evidence: Vec, +} + +impl<'a> Observation<'a> { + pub fn new( + context: &'a Context, + values: Vec, + evidence: Vec, + ) -> Result { + context.validate()?; + if values.len() != context.axes.len() || evidence.len() != values.len() { + return Err("each declared objective requires a value and evidence record"); + } + if evidence.iter().any(|e| e.source.trim().is_empty()) { + return Err("evidence source must be explicit, including heuristic sources"); + } + Ok(Self { + context, + values, + evidence, + }) + } + + pub fn evidence(&self) -> &[Evidence] { + &self.evidence + } + + /// Check every hard limit over the WHOLE interval. No amount of money + /// saved can compensate for an ecological ceiling breach (or vice versa). + /// An overlap is insufficient evidence of feasibility, not proof of breach. + pub fn admit(&self) -> Result, &'static str> { + for (axis, value) in self.context.axes.iter().zip(&self.values) { + if let Some(limit) = axis.limit { + let within = match axis.direction { + Direction::Minimize => value.upper <= limit, + Direction::Maximize => value.lower >= limit, + }; + if !within { + return Err("hard-limit feasibility is not established"); + } + } + } + Ok(Feasible { observation: self }) + } +} + +/// Witness of numeric feasibility under the DECLARED model, not deployment +/// approval. No public constructor, no deserialization escape hatch. +#[derive(Debug)] +pub struct Feasible<'o, 'c> { + observation: &'o Observation<'c>, +} + +impl Feasible<'_, '_> { + pub fn evidence(&self) -> &[Evidence] { + self.observation.evidence() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Relation { + Improvement, + Regression, + TradeOff, + Neutral, + /// Overlapping uncertainty is not neutrality or a proved trade-off. + Unresolved, +} + +/// Universal interval comparison: a claim must hold for every pair in the +/// two boxes. This intentionally ignores correlation and may be conservative. +/// It cannot establish which box contains the actual world. +pub fn compare(base: &Observation<'_>, head: &Observation<'_>) -> Result { + if base.context != head.context { + return Err("incompatible workload, units, boundary, horizon, model or policy"); + } + let mut no_worse = true; + let mut no_better = true; + let mut better = false; + let mut worse = false; + let mut neutral = true; + for ((axis, b), h) in base.context.axes.iter().zip(&base.values).zip(&head.values) { + let (least, most) = match axis.direction { + Direction::Minimize => (b.lower - h.upper, b.upper - h.lower), + Direction::Maximize => (h.lower - b.upper, h.upper - b.lower), + }; + if !least.is_finite() || !most.is_finite() { + return Err("comparison overflow"); + } + no_worse &= least >= 0.0; + no_better &= most <= 0.0; + better |= least > axis.tolerance; + worse |= most < -axis.tolerance; + neutral &= least.abs() <= axis.tolerance && most.abs() <= axis.tolerance; + } + Ok(if no_worse && better { + Relation::Improvement + } else if no_better && worse { + Relation::Regression + } else if better && worse { + Relation::TradeOff + } else if neutral { + Relation::Neutral + } else { + Relation::Unresolved + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn context() -> Context { + Context { + workload: "fixture-v1".into(), + functional_unit: "one completed request".into(), + boundary: "operational electricity only".into(), + horizon: "one invocation".into(), + model_revision: "synthetic-v1".into(), + policy_revision: "test-policy-v1".into(), + axes: vec![ + Axis { + name: "carbon".into(), + unit: "gCO2e".into(), + direction: Direction::Minimize, + tolerance: 0.0, + limit: Some(10.0), + }, + Axis { + name: "cost".into(), + unit: "USD".into(), + direction: Direction::Minimize, + tolerance: 0.0, + limit: Some(20.0), + }, + ], + } + } + + fn observation<'a>(ctx: &'a Context, bounds: &[(f64, f64)]) -> Observation<'a> { + Observation::new( + ctx, + bounds + .iter() + .map(|&(lo, hi)| Interval::new(lo, hi).unwrap()) + .collect(), + bounds + .iter() + .map(|_| Evidence { + confidence: Confidence::Estimated, + source: "synthetic test fixture, not measurement".into(), + assumptions: vec!["box contains modeled possibilities only".into()], + }) + .collect(), + ) + .unwrap() + } + + #[test] + fn numeric_refinements_reject_invalid_values() { + for (lo, hi) in [(2.0, 1.0), (f64::NAN, 1.0), (0.0, f64::INFINITY)] { + assert!(Interval::new(lo, hi).is_err()); + } + let ctx = context(); + assert!(Observation::new(&ctx, vec![], vec![]).is_err()); + let mut invalid = context(); + invalid.axes[1].name = "carbon".into(); + assert!(invalid.validate().is_err()); + } + + #[test] + fn savings_cannot_buy_an_ecological_exception() { + let ctx = context(); + let good = observation(&ctx, &[(8.0, 10.0), (10.0, 15.0)]); + let bad = observation(&ctx, &[(9.0, 11.0), (0.0, 0.0)]); + assert!(good.admit().is_ok()); + assert!(bad.admit().is_err()); + assert_eq!( + good.admit().unwrap().evidence()[0].confidence, + Confidence::Estimated + ); + } + + #[test] + fn interval_overlap_is_not_a_point_estimate_win() { + let ctx = context(); + let base = observation(&ctx, &[(4.0, 8.0), (5.0, 5.0)]); + let head = observation(&ctx, &[(3.0, 7.0), (5.0, 5.0)]); + assert_eq!(compare(&base, &head).unwrap(), Relation::Unresolved); + let separated = observation(&ctx, &[(1.0, 2.0), (5.0, 5.0)]); + assert_eq!(compare(&base, &separated).unwrap(), Relation::Improvement); + assert_eq!(compare(&separated, &base).unwrap(), Relation::Regression); + let trade = observation(&ctx, &[(1.0, 2.0), (6.0, 6.0)]); + assert_eq!(compare(&base, &trade).unwrap(), Relation::TradeOff); + } + + #[test] + fn scope_and_units_cannot_be_silently_transported() { + let ctx = context(); + let mut other = context(); + other.axes[0].unit = "kgCO2e".into(); + let a = observation(&ctx, &[(5.0, 5.0), (5.0, 5.0)]); + let b = observation(&other, &[(1.0, 1.0), (1.0, 1.0)]); + assert!(compare(&a, &b).is_err()); + assert_eq!(compare(&a, &a).unwrap(), Relation::Neutral); + } +} diff --git a/crates/oikosbot-pareto/src/lib.rs b/crates/oikosbot-pareto/src/lib.rs index 303ef67..e6f724c 100644 --- a/crates/oikosbot-pareto/src/lib.rs +++ b/crates/oikosbot-pareto/src/lib.rs @@ -32,6 +32,7 @@ //! candidates (mirrors `needs_refactor` in `eco_rules.dl`). #![forbid(unsafe_code)] +pub mod contract; use oikosbot_metrics::{ AnalysisResult, Confidence, EconScore, HealthIndex, ParetoInfo, ShadowPrices, @@ -96,19 +97,43 @@ fn improvement(direction: Direction, a: f64, b: f64) -> f64 { } } +fn valid_objectives(objectives: &[Objective], eps: f64) -> bool { + !objectives.is_empty() + && eps.is_finite() + && eps >= 0.0 + && objectives.iter().enumerate().all(|(i, o)| { + !o.name.trim().is_empty() + && o.weight.is_finite() + && o.weight >= 0.0 + && !objectives[..i].iter().any(|p| p.name == o.name) + }) +} + +fn valid_point(objectives: &[Objective], point: &[f64]) -> bool { + point.len() == objectives.len() && point.iter().all(|v| v.is_finite()) +} + +fn valid_pair(objectives: &[Objective], a: &[f64], b: &[f64], eps: f64) -> bool { + valid_objectives(objectives, eps) + && valid_point(objectives, a) + && valid_point(objectives, b) + && a.iter().zip(b).all(|(a, b)| (a - b).is_finite()) +} + /// Does point `a` Pareto-dominate point `b`? /// -/// True iff `a` is at least as good as `b` on every objective (within `eps`) -/// and strictly better (by more than `eps`) on at least one. Mismatched -/// vector lengths never dominate (total, no panic). +/// True iff `a` is no worse on EVERY objective and better by more than `eps` +/// on at least one. Tolerance suppresses tiny wins; it never permits losses. +/// Allowing losses up to epsilon creates cycles in three or more dimensions. +/// Invalid, non-finite or mismatched vectors never dominate. pub fn dominates(objectives: &[Objective], a: &[f64], b: &[f64], eps: f64) -> bool { - if a.len() != objectives.len() || b.len() != objectives.len() { + if !valid_pair(objectives, a, b, eps) { return false; } let mut any_strict = false; for (i, obj) in objectives.iter().enumerate() { let imp = improvement(obj.direction, a[i], b[i]); - if imp < -eps { + if imp < 0.0 { return false; // a is worse on this objective } if imp > eps { @@ -124,10 +149,12 @@ pub fn dominates(objectives: &[Objective], a: &[f64], b: &[f64], eps: f64) -> bo pub fn frontier_indices(objectives: &[Objective], points: &[Vec], eps: f64) -> Vec { (0..points.len()) .filter(|&i| { - !points - .iter() - .enumerate() - .any(|(j, p)| j != i && dominates(objectives, p, &points[i], eps)) + valid_objectives(objectives, eps) + && valid_point(objectives, &points[i]) + && !points + .iter() + .enumerate() + .any(|(j, p)| j != i && dominates(objectives, p, &points[i], eps)) }) .collect() } @@ -155,6 +182,9 @@ pub fn normalize(objectives: &[Objective], points: &[Vec]) -> Vec> return Vec::new(); } let n_obj = objectives.len(); + if !valid_objectives(objectives, 0.0) || points.iter().any(|p| !valid_point(objectives, p)) { + return vec![vec![0.0; n_obj]; points.len()]; + } let mut mins = vec![f64::INFINITY; n_obj]; let mut maxs = vec![f64::NEG_INFINITY; n_obj]; for p in points { @@ -173,9 +203,14 @@ pub fn normalize(objectives: &[Objective], points: &[Vec]) -> Vec> if range <= f64::EPSILON { 0.5 } else { + // Half-scale only when subtraction overflowed. + let scale = if range.is_finite() { 1.0 } else { 0.5 }; + let lo = mins[i] * scale; + let hi = maxs[i] * scale; + let value = v * scale; match objectives[i].direction { - Direction::Minimize => (maxs[i] - v) / range, - Direction::Maximize => (v - mins[i]) / range, + Direction::Minimize => (hi - value) / (hi - lo), + Direction::Maximize => (value - lo) / (hi - lo), } } }) @@ -186,12 +221,16 @@ pub fn normalize(objectives: &[Objective], points: &[Vec]) -> Vec> /// Weights normalized to sum 1 (uniform if all weights are zero/invalid). fn normalized_weights(objectives: &[Objective]) -> Vec { - let sum: f64 = objectives.iter().map(|o| o.weight.max(0.0)).sum(); - if sum <= f64::EPSILON { + let largest = objectives.iter().map(|o| o.weight).fold(0.0, f64::max); + if largest <= 0.0 || !largest.is_finite() { let n = objectives.len().max(1); return vec![1.0 / n as f64; objectives.len()]; } - objectives.iter().map(|o| o.weight.max(0.0) / sum).collect() + let sum: f64 = objectives.iter().map(|o| o.weight / largest).sum(); + objectives + .iter() + .map(|o| (o.weight / largest) / sum) + .collect() } /// Weighted Euclidean distance in normalized space. With weights summing to 1 @@ -216,6 +255,9 @@ pub fn pareto_scores(objectives: &[Objective], points: &[Vec], eps: f64) -> if points.is_empty() { return Vec::new(); } + if !valid_objectives(objectives, eps) || points.iter().any(|p| !valid_point(objectives, p)) { + return vec![0.0; points.len()]; + } let frontier = frontier_indices(objectives, points, eps); let normalized = normalize(objectives, points); let weights = normalized_weights(objectives); @@ -231,9 +273,8 @@ pub fn pareto_scores(objectives: &[Objective], points: &[Vec], eps: f64) -> if d.is_finite() { (100.0 * (1.0 - d)).clamp(0.0, 100.0) } else { - // No frontier can only happen on empty input, handled above; - // stay total anyway. - 100.0 + // Never promote an unusable distance to a perfect score. + 0.0 } }) .collect() @@ -305,6 +346,8 @@ pub enum ParetoVerdict { TradeOff, /// No objective moved by more than ε. Neutral, + /// Invalid or incomplete inputs; no comparison was performed. + Indeterminate, } /// Per-objective delta between base and head. @@ -325,7 +368,7 @@ pub struct Comparison { /// Objectives that actually moved (|improvement| > ε) — the verdict's /// drivers. pub drivers: Vec, - /// Whether the verdict may drive a blocking decision: every driving + /// Whether the verdict may drive a blocking decision: every /// objective must be backed by `Measured` or `Calibrated` inputs. /// Heuristic estimates advise; they do not block. pub actionable: bool, @@ -333,6 +376,9 @@ pub struct Comparison { /// Classify head vs base (see [`ParetoVerdict`]). pub fn compare(objectives: &[Objective], base: &[f64], head: &[f64], eps: f64) -> ParetoVerdict { + if !valid_pair(objectives, base, head, eps) { + return ParetoVerdict::Indeterminate; + } if dominates(objectives, head, base, eps) { ParetoVerdict::Improvement } else if dominates(objectives, base, head, eps) { @@ -372,6 +418,14 @@ pub fn assess( eps: f64, ) -> Comparison { let verdict = compare(objectives, base, head, eps); + if verdict == ParetoVerdict::Indeterminate { + return Comparison { + verdict, + deltas: Vec::new(), + drivers: Vec::new(), + actionable: false, + }; + } let deltas: Vec = objectives .iter() .enumerate() @@ -392,14 +446,10 @@ pub fn assess( .map(|d| d.name.clone()) .collect(); let actionable = !drivers.is_empty() - && deltas.iter().enumerate().all(|(i, d)| { - if d.improvement.abs() > eps { - let c = confidences.get(i).copied().unwrap_or(Confidence::Unknown); - confidence_rank(c) >= confidence_rank(Confidence::Calibrated) - } else { - true - } - }); + && confidences.len() == objectives.len() + && confidences + .iter() + .all(|&c| confidence_rank(c) >= confidence_rank(Confidence::Calibrated)); Comparison { verdict, deltas, @@ -673,6 +723,7 @@ mod tests { ParetoVerdict::Regression => assert_eq!(ba, ParetoVerdict::Improvement), ParetoVerdict::TradeOff => assert_eq!(ba, ParetoVerdict::TradeOff), ParetoVerdict::Neutral => assert_eq!(ba, ParetoVerdict::Neutral), + ParetoVerdict::Indeterminate => panic!("finite fixtures must compare"), } } } @@ -712,7 +763,7 @@ mod tests { ); assert!(!guessed.actionable, "heuristic estimates must not block"); - // Confidence on a non-driving objective is irrelevant. + // Unknown evidence on a non-driving objective cannot establish no worsening. let mixed = assess( &o, &base, @@ -720,7 +771,7 @@ mod tests { &[Confidence::Measured, Confidence::Unknown], DEFAULT_EPSILON, ); - assert!(mixed.actionable); + assert!(!mixed.actionable, "no-worse claims also require evidence"); } #[test] diff --git a/crates/oikosbot-pareto/tests/trust_boundary.rs b/crates/oikosbot-pareto/tests/trust_boundary.rs new file mode 100644 index 0000000..1804efe --- /dev/null +++ b/crates/oikosbot-pareto/tests/trust_boundary.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +use oikosbot_metrics::Confidence; +use oikosbot_pareto::{ + assess, compare, dominates, frontier_indices, pareto_scores, Direction, Objective, + ParetoVerdict, +}; + +fn axes() -> Vec { + ["carbon", "cost", "time"] + .iter() + .map(|name| Objective::new(name, Direction::Minimize, 1.0)) + .collect() +} + +#[test] +fn tolerated_losses_must_not_create_a_dominance_cycle() { + // Old epsilon relation: a > b > c > a, empty frontier, all scores 100. + let points = vec![ + vec![0.0, 1.0, 2.0], + vec![2.0, 0.0, 1.0], + vec![1.0, 2.0, 0.0], + ]; + let objectives = axes(); + assert_eq!(frontier_indices(&objectives, &points, 1.0), vec![0, 1, 2]); + for a in &points { + for b in &points { + assert!(!dominates(&objectives, a, b, 1.0)); + } + } + assert!(dominates(&objectives, &[0.0; 3], &[2.0; 3], 1.0)); +} + +#[test] +fn malformed_data_is_not_neutral_actionable_or_optimal() { + let objectives = axes(); + for bad in [vec![], vec![1.0], vec![f64::NAN; 3], vec![f64::INFINITY; 3]] { + let result = assess( + &objectives, + &[1.0; 3], + &bad, + &[Confidence::Measured; 3], + 0.0, + ); + assert_eq!(result.verdict, ParetoVerdict::Indeterminate); + assert!(!result.actionable); + assert!(frontier_indices(&objectives, &[bad.clone()], 0.0).is_empty()); + assert_eq!(pareto_scores(&objectives, &[bad], 0.0), vec![0.0]); + } + for eps in [-1.0, f64::NAN, f64::INFINITY] { + assert_eq!( + compare(&objectives, &[1.0; 3], &[2.0; 3], eps), + ParetoVerdict::Indeterminate + ); + } +} + +#[test] +fn unchanged_axis_is_still_part_of_the_no_worse_claim() { + let result = assess( + &axes(), + &[2.0; 3], + &[1.0, 2.0, 2.0], + &[ + Confidence::Measured, + Confidence::Unknown, + Confidence::Measured, + ], + 0.0, + ); + assert_eq!(result.verdict, ParetoVerdict::Improvement); + assert!(!result.actionable); +} + +#[test] +fn dominance_is_transitive_on_a_finite_grid() { + let objectives = axes(); + let mut points = Vec::new(); + for x in 0..3 { + for y in 0..3 { + for z in 0..3 { + points.push(vec![x as f64, y as f64, z as f64]); + } + } + } + for eps in [0.0, 0.5, 1.0] { + for a in &points { + for b in &points { + for c in &points { + if dominates(&objectives, a, b, eps) && dominates(&objectives, b, c, eps) { + assert!(dominates(&objectives, a, c, eps)); + } + } + } + } + } +} diff --git a/crates/oikosbot-sarif/README.adoc b/crates/oikosbot-sarif/README.adoc index a0a765a..a7f3502 100644 --- a/crates/oikosbot-sarif/README.adoc +++ b/crates/oikosbot-sarif/README.adoc @@ -24,7 +24,7 @@ OikosBot's findings. `quality_score`, `overall_health`, `energy_joules`, `carbon_gco2e`, `duration_ms`, `memory_bytes`, `confidence`, optional `suggestion`, optional `pareto_status` / `pareto_score` / `pareto_dominated_by`, and — for - calibrated estimates — `resource_range` with `min`/`typical`/`max` per axis. + pattern-table estimates — `resource_range` with `min`/`typical`/`max` per axis. * Suggestions ride in the message and in `properties`. There is deliberately no SARIF `fix` object: OikosBot does not rewrite code. diff --git a/crates/oikosbot-sarif/src/lib.rs b/crates/oikosbot-sarif/src/lib.rs index a0e7a33..08dc323 100644 --- a/crates/oikosbot-sarif/src/lib.rs +++ b/crates/oikosbot-sarif/src/lib.rs @@ -349,11 +349,25 @@ fn convert_result(result: &AnalysisResult, rule_ids: &[&str]) -> SarifResult { uri: result.location.file.clone(), uri_base_id: Some("%SRCROOT%".to_string()), }, - region: Some(Region { - start_line: Some(result.location.line), - start_column: Some(result.location.column), - end_line: result.location.end_line, - end_column: result.location.end_column, + // Zero means unknown in legacy synthetic findings. SARIF uses + // one-based source positions; omit unknown regions, never invent + // a location at line 1 or serialize a schema-invalid zero. + region: (result.location.line > 0).then(|| { + let end_line = result + .location + .end_line + .filter(|&line| line >= result.location.line); + Region { + start_line: Some(result.location.line), + start_column: (result.location.column > 0).then_some(result.location.column), + end_line, + end_column: result.location.end_column.filter(|&column| { + column > 0 + && end_line.is_some_and(|line| { + line > result.location.line || column >= result.location.column + }) + }), + } }), }, logical_locations: result.location.name.as_ref().map(|name| { @@ -382,6 +396,7 @@ fn convert_result(result: &AnalysisResult, rule_ids: &[&str]) -> SarifResult { "duration_ms": result.resources.duration.0, "memory_bytes": result.resources.memory.0, "confidence": format!("{:?}", result.confidence), + "taxonomy": result.taxonomy, }); if let Some(ref s) = result.suggestion { properties["suggestion"] = serde_json::json!(s); @@ -391,7 +406,7 @@ fn convert_result(result: &AnalysisResult, rule_ids: &[&str]) -> SarifResult { properties["pareto_score"] = serde_json::json!(pareto.score); properties["pareto_dominated_by"] = serde_json::json!(pareto.dominated_by); } - // Propagate the calibrated uncertainty band so consumers can see the spread + // Propagate the heuristic uncertainty band so consumers can see the spread // instead of treating the point estimate as exact. Absent on the naive // path, where there is no band to report. if let Some(ref range) = result.resource_range { @@ -436,6 +451,7 @@ mod tests { fn sample_result() -> AnalysisResult { AnalysisResult { + taxonomy: None, location: CodeLocation { file: "src/main.rs".to_string(), line: 10, @@ -461,6 +477,18 @@ mod tests { } } + #[test] + fn unknown_source_positions_are_omitted_not_fabricated() { + let mut result = sample_result(); + result.location.line = 0; + result.location.column = 0; + let log = to_sarif(&[result], "0.1.0"); + assert!(log.runs[0].results[0].locations[0] + .physical_location + .region + .is_none()); + } + #[test] fn test_sarif_structure() { let results = vec![sample_result()]; diff --git a/crates/oikosbot-telemetry/README.adoc b/crates/oikosbot-telemetry/README.adoc index 3f8426b..f9a507d 100644 --- a/crates/oikosbot-telemetry/README.adoc +++ b/crates/oikosbot-telemetry/README.adoc @@ -21,7 +21,7 @@ everything downstream consumes. | `derive` | `derive_per_repo(runs, assumptions)` — per-repo aggregates, and `confidence_of(metric)` which states the confidence of each derived figure: `wall_minutes` is `Measured` (it comes straight from the GitHub API), while -energy, carbon and cost are `Calibrated`/`Estimated` from `Assumptions`. +energy, carbon and imputed cost are `Estimated` from `Assumptions`, not calibration receipts. | `snapshot` | `write_runs`/`read_runs`, `write_repos`/`read_repos`, `write_releases`/`read_releases` — Parquet staging. diff --git a/crates/oikosbot-telemetry/src/collect.rs b/crates/oikosbot-telemetry/src/collect.rs index b7da310..2e6aef0 100644 --- a/crates/oikosbot-telemetry/src/collect.rs +++ b/crates/oikosbot-telemetry/src/collect.rs @@ -33,21 +33,42 @@ impl GhRunner for GhCli { /// Parse an RFC3339 UTC timestamp of the exact form `YYYY-MM-DDTHH:MM:SSZ` /// into seconds since the Unix epoch, via the civil-days algorithm. -fn iso_to_epoch(s: &str) -> i64 { +fn iso_to_epoch(s: &str) -> Result { let b = s.as_bytes(); - let num = |a: usize, z: usize| -> i64 { s[a..z].parse().unwrap_or(0) }; - if b.len() < 20 { - return 0; - } - let (y, m, d) = (num(0, 4), num(5, 7), num(8, 10)); - let (hh, mm, ss) = (num(11, 13), num(14, 16), num(17, 19)); + anyhow::ensure!( + b.len() == 20 + && b.iter().enumerate().all(|(i, c)| match i { + 4 | 7 => *c == b'-', + 10 => *c == b'T', + 13 | 16 => *c == b':', + 19 => *c == b'Z', + _ => c.is_ascii_digit(), + }), + "invalid UTC timestamp {s:?}" + ); + // ASCII validation above makes these byte boundaries safe. + let num = |a: usize, z: usize| -> Result { Ok(s[a..z].parse()?) }; + let (y, m, d) = (num(0, 4)?, num(5, 7)?, num(8, 10)?); + let (hh, mm, ss) = (num(11, 13)?, num(14, 16)?, num(17, 19)?); + let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0); + let days_in_month = match m { + 2 if leap => 29, + 2 => 28, + 4 | 6 | 9 | 11 => 30, + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + _ => 0, + }; + anyhow::ensure!( + y >= 1970 && d >= 1 && d <= days_in_month && hh < 24 && mm < 60 && ss < 60, + "out-of-range UTC timestamp {s:?}" + ); let yy = if m <= 2 { y - 1 } else { y }; let era = yy.div_euclid(400); let yoe = yy - era * 400; let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; let days = era * 146_097 + doe - 719_468; - days * 86_400 + hh * 3600 + mm * 60 + ss + Ok(days * 86_400 + hh * 3600 + mm * 60 + ss) } /// Fetch up to `max_runs` workflow runs for `repo`, paginating 100 at a time. @@ -58,11 +79,18 @@ pub fn collect_runs(gh: &dyn GhRunner, repo: &str, max_runs: usize) -> Result 0) + .context("invalid run id")?; + anyhow::ensure!( + !rows.iter().any(|row: &RunRow| row.run_id == id), + "duplicate run {id} across pages: collection is not a stable snapshot" + ); let started = r["run_started_at"].as_str().unwrap_or("").to_string(); let updated = r["updated_at"].as_str().unwrap_or("").to_string(); // `run_started_at` is nullable in GitHub's API (queued/early-state @@ -73,11 +101,11 @@ pub fn collect_runs(gh: &dyn GhRunner, repo: &str, max_runs: usize) -> Result Result= total.min(max_runs as u64), + "truncated run history: short page contradicts total_count" + ); + } + break; + } page += 1; } Ok(rows) @@ -102,40 +140,81 @@ pub fn collect_runs(gh: &dyn GhRunner, repo: &str, max_runs: usize) -> Result Result> { let mut out = Vec::new(); - for page in 1..=10 { + // The extra page distinguishes exactly 1000 repos from truncation. + for page in 1..=11 { let v = gh.api(&format!( "users/{owner}/repos?per_page=100&page={page}&type=owner" ))?; - let arr = v.as_array().cloned().unwrap_or_default(); + let arr = v + .as_array() + .context("invalid repository response: expected array")?; if arr.is_empty() { break; } - for r in &arr { + anyhow::ensure!( + page <= 10, + "estate exceeds 1000 repository cap; refusing truncated inventory" + ); + for r in arr { + let name = r["full_name"] + .as_str() + .filter(|s| s.contains('/')) + .context("repository is missing owner/name identity")?; + anyhow::ensure!( + !out.iter().any(|row: &RepoRow| row.repo == name), + "duplicate repository {name}: unstable pagination" + ); out.push(RepoRow { - repo: r["full_name"].as_str().unwrap_or("").to_string(), + repo: name.to_string(), visibility: r["visibility"].as_str().unwrap_or("").to_string(), archived: r["archived"].as_bool().unwrap_or(false), pushed_at: r["pushed_at"].as_str().unwrap_or("").to_string(), size_kb: r["size"].as_i64().unwrap_or(0), }); } + if arr.len() < 100 { + break; + } } Ok(out) } -/// Fetch up to 100 releases for `repo`. +/// Fetch public release history. A safety cap is an error, never a complete +/// empty or truncated history. Draft releases do not count as publication. pub fn collect_releases(gh: &dyn GhRunner, repo: &str) -> Result> { - let v = gh.api(&format!("repos/{repo}/releases?per_page=100"))?; - Ok(v.as_array() - .cloned() - .unwrap_or_default() - .iter() - .map(|r| ReleaseRow { - repo: repo.to_string(), - tag: r["tag_name"].as_str().unwrap_or("").to_string(), - published_at: r["published_at"].as_str().unwrap_or("").to_string(), - }) - .collect()) + let mut rows = Vec::new(); + for page in 1..=101 { + let v = gh.api(&format!("repos/{repo}/releases?per_page=100&page={page}"))?; + let releases = v + .as_array() + .context("invalid releases response: expected array")?; + anyhow::ensure!( + page <= 100 || releases.is_empty(), + "release history exceeds collection cap" + ); + for r in releases { + if r["draft"].as_bool() == Some(true) { + continue; + } + let tag = r["tag_name"] + .as_str() + .filter(|s| !s.is_empty()) + .context("missing release tag")?; + anyhow::ensure!( + !rows.iter().any(|row: &ReleaseRow| row.tag == tag), + "duplicate release tag {tag}" + ); + rows.push(ReleaseRow { + repo: repo.to_string(), + tag: tag.to_string(), + published_at: r["published_at"].as_str().unwrap_or("").to_string(), + }); + } + if releases.len() < 100 { + return Ok(rows); + } + } + bail!("release pagination did not terminate") } #[cfg(test)] @@ -156,6 +235,7 @@ mod tests { "event": "push", "conclusion": "startup_failure", "run_started_at": "2026-08-01T00:00:00Z", "updated_at": "2026-08-01T00:00:00Z"}]})); let rows = collect_runs(&fake, "o/r", 50).unwrap(); + assert_eq!(rows.len(), 1); assert_eq!(rows[0].conclusion, "startup_failure"); assert_eq!(rows[0].duration_s, 0); assert_eq!(rows[0].workflow_path, ".github/workflows/ci.yml"); @@ -168,6 +248,7 @@ mod tests { "event": "push", "conclusion": null, "run_started_at": null, "updated_at": "2026-08-01T00:05:00Z"}]})); let rows = collect_runs(&fake, "o/r", 50).unwrap(); + assert_eq!(rows.len(), 1); assert_eq!( rows[0].duration_s, 0, "null run_started_at must not be treated as epoch zero" @@ -187,17 +268,103 @@ mod tests { #[test] fn iso_to_epoch_known_pairs() { - assert_eq!(iso_to_epoch("1970-01-01T00:00:00Z"), 0); + assert_eq!(iso_to_epoch("1970-01-01T00:00:00Z").unwrap(), 0); assert_eq!( - iso_to_epoch("2026-08-01T00:05:00Z") - iso_to_epoch("2026-08-01T00:00:00Z"), + iso_to_epoch("2026-08-01T00:05:00Z").unwrap() + - iso_to_epoch("2026-08-01T00:00:00Z").unwrap(), 300 ); // Month-boundary guard: 23:59 on 07-31 -> 00:01 on 08-01 is 120s, not // a huge/negative jump, proving the civil-days arithmetic carries the // month/day rollover correctly. assert_eq!( - iso_to_epoch("2026-08-01T00:01:00Z") - iso_to_epoch("2026-07-31T23:59:00Z"), + iso_to_epoch("2026-08-01T00:01:00Z").unwrap() + - iso_to_epoch("2026-07-31T23:59:00Z").unwrap(), 120 ); } } + +#[cfg(test)] +mod boundary_tests { + use super::*; + use std::cell::RefCell; + use std::collections::VecDeque; + + struct Pages(RefCell>>); + impl GhRunner for Pages { + fn api(&self, _path: &str) -> Result { + self.0 + .borrow_mut() + .pop_front() + .expect("unexpected extra request") + } + } + fn pages(values: Vec>) -> Pages { + Pages(RefCell::new(values.into())) + } + fn runs(start: i64, count: usize) -> serde_json::Value { + serde_json::json!({"workflow_runs": (0..count).map(|i| serde_json::json!({ + "id": start + i as i64, "name": "CI", "conclusion": "success", + "run_started_at": "2026-08-01T00:00:00Z", "updated_at": "2026-08-01T00:01:00Z" + })).collect::>()}) + } + + #[test] + fn two_pages_are_collected_once_and_short_page_terminates() { + let gh = pages(vec![Ok(runs(1, 100)), Ok(runs(101, 1))]); + let rows = collect_runs(&gh, "o/r", 200).unwrap(); + assert_eq!(rows.len(), 101); + assert_eq!(rows[100].run_id, 101); + } + + #[test] + fn malformed_rate_limited_and_duplicate_pages_cannot_become_empty_success() { + let gh = pages(vec![Ok(serde_json::json!({"message":"rate limited"}))]); + assert!(collect_runs(&gh, "o/r", 200).is_err()); + let gh = pages(vec![Ok(runs(1, 100)), Err(anyhow::anyhow!("HTTP 429"))]); + assert!(collect_runs(&gh, "o/r", 200).is_err()); + let gh = pages(vec![Ok(runs(1, 100)), Ok(runs(1, 1))]); + assert!(collect_runs(&gh, "o/r", 200).is_err()); + let gh = pages(vec![Ok(serde_json::json!({}))]); + assert!(list_repos(&gh, "o").is_err()); + let gh = pages(vec![Ok(serde_json::json!({}))]); + assert!(collect_releases(&gh, "o/r").is_err()); + } + + #[test] + fn short_page_cannot_claim_a_larger_declared_history() { + let mut page = runs(1, 1); + page["total_count"] = serde_json::json!(10); + assert!(collect_runs(&pages(vec![Ok(page)]), "o/r", 200).is_err()); + } + + #[test] + fn invalid_timestamps_are_errors_not_epoch_zero_or_utf8_panics() { + for timestamp in [ + "", + "💥💥💥💥💥", + "2026-02-30T00:00:00Z", + "2026-08-01T99:00:00Z", + ] { + assert!(iso_to_epoch(timestamp).is_err()); + } + assert!(iso_to_epoch("2024-02-29T00:00:00Z").is_ok()); + } + + #[test] + fn repository_inventory_cap_is_not_silent_truncation() { + let mut responses = Vec::new(); + for page in 0..11 { + responses.push(Ok(serde_json::json!((0..100) + .map(|i| serde_json::json!({ + "full_name": format!("o/r{}", page * 100 + i) + })) + .collect::>()))); + } + assert!(list_repos(&pages(responses), "o") + .unwrap_err() + .to_string() + .contains("cap")); + } +} diff --git a/crates/oikosbot-telemetry/src/derive.rs b/crates/oikosbot-telemetry/src/derive.rs index f3b363d..2e200c3 100644 --- a/crates/oikosbot-telemetry/src/derive.rs +++ b/crates/oikosbot-telemetry/src/derive.rs @@ -3,8 +3,7 @@ //! //! Transforms flat RunRow telemetry into aggregated, economically meaningful //! metrics with explicit confidence levels. The confidence ladder is the honesty -//! contract: only API-sourced quantities are Measured; coefficients over -//! measurements are Calibrated; anything resting on a declared assumption +//! contract: only API-sourced quantities are Measured; coefficients without calibration receipts are Estimated; anything resting on a declared assumption //! (grid region) is Estimated. use crate::rows::RunRow; @@ -92,12 +91,12 @@ pub fn derive_per_repo(runs: &[RunRow], a: &Assumptions) -> Vec { /// Return the confidence level for a derived metric. /// /// The ladder is the honesty contract: only API-sourced quantities are -/// Measured; coefficients over measurements are Calibrated; anything +/// Measured as API interval proxies; unvalidated coefficients and anything /// resting on a declared assumption (grid region) is Estimated. pub fn confidence_of(metric: &str) -> Confidence { match metric { "wall_minutes" => Confidence::Measured, - "energy_kwh" | "imputed_cost_usd" => Confidence::Calibrated, + "energy_kwh" | "imputed_cost_usd" => Confidence::Estimated, "carbon_g" => Confidence::Estimated, _ => Confidence::Unknown, } @@ -134,8 +133,8 @@ mod tests { #[test] fn confidence_ladder_is_honest() { assert_eq!(confidence_of("wall_minutes"), Confidence::Measured); - assert_eq!(confidence_of("energy_kwh"), Confidence::Calibrated); + assert_eq!(confidence_of("energy_kwh"), Confidence::Estimated); assert_eq!(confidence_of("carbon_g"), Confidence::Estimated); - assert_eq!(confidence_of("imputed_cost_usd"), Confidence::Calibrated); + assert_eq!(confidence_of("imputed_cost_usd"), Confidence::Estimated); } } diff --git a/docs/HANDOFF.adoc b/docs/HANDOFF.adoc new file mode 100644 index 0000000..394d7db --- /dev/null +++ b/docs/HANDOFF.adoc @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += Next-agent baton and owner decisions +:toc: + +== Start here + +Continue the OikosBot launch-hardening work, not a launch. Read, in order: + +. `0-AI-MANIFEST.a2ml` and `.claude/CLAUDE.md`. +. `.machine_readable/descriptiles/STATE.a2ml`, especially the latest dated session. +. `docs/LAUNCH-READINESS.adoc` and the merged/pending hardening PR's checks. +. `docs/research/EVIDENCE-SCOPED-PARETO.adoc` (pinned estate sources and exact proof boundaries). +. `docs/usage.adoc` (breaking exit/configuration semantics). + +Locate the hardening PR and verify its current state rather than assuming a merge: + +[source,shell] +---- +gh pr list --repo hyperpolymath/oikosbot --state all --head arena/01a0df1f-oikosbot +gh run list --repo hyperpolymath/oikosbot --limit 15 +gh issue list --repo hyperpolymath/oikosbot --state open +---- + +Do not close issues merely because a checkbox has source code behind it. Review +current head/merge-SHA CI evidence. Historical local verification limitations stay +in the audit; later CI receipts supersede those limits only for checks actually run. + +== Non-negotiable boundaries + +* Remain advisory/alpha. No semver release, stable image promotion, Marketplace + launch, regulator enablement or auto-fix authority without explicit owner approval + and the launch receipts. A code merge is not a release approval. +* Preserve exit 2 for no requested check performed. Do not make missing evidence, + malformed input, missing tools or unavailable integrations green. +* Expert tables remain Estimated/Unknown. Recognition of a code pattern, a range, + or an enum label cannot manufacture calibration, a probability or policy authority. +* Pareto dominance permits no losses; epsilon suppresses small wins, not losses. + Invalid input is Indeterminate. The interval contract remains a separate prototype. +* `Feasible` witnesses only the declared numeric constraints. No claim of real-world + adequacy, consent, authenticated evidence, budget ownership or ecological benefit. +* Rust workspace is standalone. Do not reintroduce the separate economics DSL or + fleet Sustainabot identity. Security/native Eclexia seams report unavailable until + real adapters are integrated. App and VeriSimDB remain operationally unverified. +* Preserve existing licence notices. Do not bulk-copy template scaffolds, badges, + ABI proofs or metadata formats and claim conformance. Consult the applicability + tensions and exact source revisions recorded in the audit. + +== Next executable tranche (priority order) + +. Resolve any failed head/main CI checks first. Test *both* default and all-feature + builds: all-features alone does not cover feature-disabled behavior. Confirm the + published development artifact if publication occurred; do not change the Action + digest without testing that exact artifact. Inspect PR comments/check annotations, + not just the aggregate status. +. Issue #81: add pinned full SARIF 2.1.0 schema validation plus negative fixtures, + then test the actual composite Action (not its argument recorder) and a released + digest. Test anonymous pull, version, non-root execution, provenance, semver/SHA + consistency and deliberate nonexistent-digest rejection. Ensure the tested + candidate and published digest are the same artifact before claiming that. +. Calibration #48: design one small reproducible workload/calibration experiment + with the owner's functional unit and boundary. Preserve raw observations, + environment/model versions, repetitions, holdouts and error bounds. Validate + actual-world applicability; propagate evidence per axis. Keep benchmarks and + corpora outside the source repo following existing estate storage conventions. + Do not promote a whole result to Calibrated because one operation was measured. +. Collection completeness: version an inventory/coverage/freshness manifest; + distinguish missing time from zero, cap from complete history, elapsed workflow + interval from job CPU/billable time, and resumed files from fresh observations. + Add atomic writes and validation of pre-existing staging identities/content. +. Taxonomy #18: deliberately classify synthetic policy/dependency findings and + preserve backwards JSON behavior; do not derive `must` from a resource enum. + App comment transport remains contingent on a real App runtime. +. Research: start a minimal checked proof of the no-loss order and interval + comparator soundness relative to explicit box membership. Include cycle, + incomparability, overlapping intervals, inconsistent evidence and lost-context + counterexamples. Coordinate vocabulary with nextgen-typing; novelty requires + prior-work/separation evidence, not a new name. This research is not permission + to wire the prototype into enforcement. + +Keep the next PR narrower than this initial cross-cutting audit. Record exact +commands, tool versions, run URLs and remaining limits. Add paired clean/firing/ +no-check fixtures for each new boundary. No fake proof or successful-tool receipts. + +== Owner decisions (not chores delegated back from automatable work) + +. Choose the first useful functional unit and representative workload: for example, + one successful request at specified correctness/latency, or one verified build. + Name the first one or two repositories/pilots and the reference baseline. +. Choose the observation boundary and horizon: operational electricity only versus + embodied/upstream effects; region/grid assumptions; per-operation versus portfolio + budgets. State which omissions must be visible on every report. +. Decide non-compensatory limits and who may accept a genuine trade-off. Financial + savings must not silently override an ecological ceiling. Unknown/unmeasured limits + remain unknown; do not invent numbers to unblock the agent. +. Approve profiling resources/access and an initial advisory-only pilot when its + artifact checks pass. If measured power is unavailable, accept a clearly labeled + proxy experiment—not a false energy calibration. +. Keep general launch/regulator/App deployment on hold. Review a later explicit + release proposal with receipts; no release action is required merely to merge + the hardening work. + +An agent can draft options and run the engineering checks. It cannot decide these +valuation/authority boundaries on the owner's behalf or obtain hardware readings +from an environment that does not expose them. diff --git a/docs/LAUNCH-READINESS.adoc b/docs/LAUNCH-READINESS.adoc new file mode 100644 index 0000000..c5c05da --- /dev/null +++ b/docs/LAUNCH-READINESS.adoc @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += Launch readiness and issue disposition +:toc: + +Assessment date: *2026-09-26*. Baseline: `2f47a61`. +Decision: *HOLD general launch and regulator/auto-fix claims*. +This branch is a proposed hardening tranche, not a release certification. +No release, image promotion, issue closure or deployment was performed. + +== Evidence boundary + +The repository already has published Action/container surfaces; publication is +not evidence of production readiness. The baseline main-branch CI run +`36204445111` and Publish Image run `36204445093` reported success on GitHub. +Those runs do *not* validate this branch. + +Full pinned-toolchain Rust, Haskell, AffineScript, Docker and formal-prover +execution was not available. Rustup/static Rust and Debian downloads failed. +An older standalone Rust 1.88.0 toolset was later recovered for formatting and +isolated numerical checks. Cargo testing still failed on inaccessible crates.io. +New workspace/integration tests are *authored, not locally executed*. The prototype's +assurance label is *designed*, not proven. Passing a syntax check is not a build. +See the verification record below for checks that actually ran. + +== Immediate trust-boundary corrections in this branch + +* Withdraw unearned `Calibrated` labels from the expert resource table and + estate power/price coefficients. Existing numeric table values are retained + for comparison; neither the values nor the direction of change is validated. +* No-loss dominance replaces epsilon-permitted losses, which could form cycles. + Invalid/non-finite comparisons return `Indeterminate`, not `Neutral` or a panic. +* `check` and `compare --check` distinguish missing evidence (exit 2) from + a supported negative judgement (exit 1). Reports remain advisory (exit 0). + Formatting no longer determines whether a threshold gate runs. +* Reject recovered syntax errors, invalid config modes/globs/languages, + empty/fully excluded scans, and unreadable traversal entries. Directory scans + do not follow file symlinks. JSON stdout is no longer mixed with tracing logs. +* The reserved security feature no longer references an absent dependency; it + compiles as an explicit unavailable seam. Policy directory/evaluation errors + propagate instead of becoming warnings or empty successful evaluations. +* Typed taxonomy reaches pattern results, JSON and SARIF. Unclassified rules + remain unclassified. Resource confidence never becomes a numeric probability + or permission to auto-fix. +* Short GitHub pages terminate collection; malformed responses, duplicate pages, + invalid timestamps and inventory caps fail explicitly. Release collection + paginates. Partial collection retains successes but returns failure. Analysis + refuses a staging inventory missing required per-repository files. +* Rust version is declared in `rust-toolchain.toml`; container build and runtime + share Bookworm. Added candidate-image smoke fixtures to PR CI and publication. + These are not full release-contract or provenance tests. + +== Every currently open GitHub issue + +No issue is closed on the strength of authored-but-unexecuted tests. + +[cols="1,2,3",options="header"] +|=== +|Issue |Disposition |Remaining acceptance evidence +|https://github.com/hyperpolymath/oikosbot/issues/12[#12 taxonomy vocabulary] +|Proposal implemented: NEUROSYM canonical; wish / preventive / externalities. +|Owner review of the deontic interpretation. A `must` label is not authority. +|https://github.com/hyperpolymath/oikosbot/issues/16[#16 developer docs] +|Thin README, architecture/policy routing, contributing/build guide and launch checklist. +|Check links/rendering and execute the multi-toolchain walkthrough. No claim that scaffolds build. +|https://github.com/hyperpolymath/oikosbot/issues/17[#17 user docs] +|Usage, exit semantics, score limitations, modes, policies and deployment hold documented. +|A production App runbook requires a real listener, tested orchestration and operations evidence. +|https://github.com/hyperpolymath/oikosbot/issues/18[#18 taxonomy transport] +|Pattern rules carry typed tags through JSON/SARIF; old input defaults to no taxonomy. +|Execute tests; classify synthetic policy/security/dependency rules deliberately and wire App comments. +|https://github.com/hyperpolymath/oikosbot/issues/48[#48 calibration] +|Wiring existed, but evidence promotion was unsound. Retain bands; downgrade unsupported claims. +|Reproducible calibration corpus, sampling, hardware/workloads, holdouts, error analysis, +per-axis propagation and reviewed before/after results. A range alone does not earn calibration. +|https://github.com/hyperpolymath/oikosbot/issues/81[#81 tests/benchmarks] +|Added numeric/cycle, CLI clean/firing/no-check, scoped-contract, pagination and image-smoke controls. +|Full SARIF schema validation, *composite Action* and digest/provenance release tests, +mutation, shrinking properties, real fuzz targets, compatibility and frozen benchmarks remain open. +|https://github.com/hyperpolymath/oikosbot/issues/82[#82 future interfaces] +|Applicability respected: no fabricated Idris2/Zig/Hexadeca proof scaffolds. +|Reassess when an owned foreign interface is introduced. Upstream AffineScript FFI +use is not an OikosBot ABI proof. Canonical Hexadeca authority is still unlocated here. +|=== + +== Release gates (must all have receipts for the proposed release) + +. *Build*: pinned toolchain + locked dependencies, all workspace tests, meaningful + clippy review, optional feature tests, paired clean/firing/no-check controls. +. *Artifact*: build once, test and publish the same digest; anonymous pull by + digest, expected version, non-root execution, semver/SHA tag consistency, + provenance verification and deliberate nonexistent-digest rejection. + Current smoke checks test a candidate build, not the separately pushed digest. +. *Action*: run `action.yml` itself with clean and firing fixtures and validate + SARIF 2.1.0 against a pinned schema and GitHub constraints. Do not substitute + a direct CLI/container test for this test. +. *Evidence*: reviewer accepts the calibration/uncertainty model for any proposed + enforcement. Until then, release only explicitly experimental advisory reports. +. *Governance*: review declared local applicability against the pinned standards + sources, resolve drift explicitly, and record responsible authority and review date. +. *Operations*: App launch additionally requires AffineScript compiler evidence, + real listener/transport, payload/HMAC negative tests, replay/idempotency controls, + least-privilege tokens, rate limits, bounded resource consumption, secret rotation, + redacted logs, incident containment and a tested rollback procedure. +. *Owner approval*: explicit scoped release approval after reviewing these receipts. + A high Pareto score or green CI does not substitute for it. + +Run `just release-preflight` to collect a *subset* of the evidence. Missing tools +return 2. It does not cover Haskell/AS/provers, full SARIF schema, live registry +permissions, attestations or the Action wrapper. Do not advance a tag/digest on +its output alone. Existing `action.yml` remains pinned to the previously published +image: none of these source fixes silently changes that artifact. + +== Known residuals that prevent a wider claim + +* Raw per-function comparison is not a comparison of equivalent useful work. + AST-node count is not executions; deletion or changed scope can look like a win. +* The table maps nested loops to sorting and has speculative envelopes. Neither + extrema nor correlations have a validated measurement interpretation. +* Quality/eco/econ scores and price weights encode choices, not social welfare. + Energy and carbon are correlated; a weighted sum can count the same proxy twice. +* A workflow's `updated_at - run_started_at` is an API interval proxy, not job CPU + time, billable minutes or an invoice. Missing timestamps are still stored as + zero duration in the legacy schema. Runner mix, cancellations and concurrency + require a new sampling/coverage schema before cost claims. +* Collection is capped by `max_runs`, not an entire history; resumability still + trusts pre-existing staging files. No signed completeness manifest, atomic + snapshot transaction, freshness contract or globally stable pagination exists. +* Historical workflow failures do not establish that a quality gate is sound. + DEA scores are relative to selected peers and modeled outputs, not causal impact. +* Scope/units/interval validation is not model adequacy, authenticated evidence, + allocation authority, consent, distributive justice or protection from rebound. +* Bounded-input handling, meaningful fuzzing/mutation, benchmark baselines, + dependency vulnerability review and full policy/config compatibility need work. + +== Standards applicability + +See link:research/EVIDENCE-SCOPED-PARETO.adoc[the pinned estate-source map]. +The standards constitution inspected is explicitly *draft*. Its separation of +policy and evidence is adopted as a local safety discipline, not claimed as +estate certification. The template is starting material, not authority to +invent completed ABI proofs, badges or quantum-safe provenance. + +Keep the six existing A2ML descriptiles in their declared location. The current +template advertises deed/CLADE/ANCHOR surfaces while standards still references +six A2ML files: record this as an upstream/local profile tension, do not rename +metadata wholesale without a versioned migration. Likewise the standards README +runtime table conflicts with its canonical `.claude/CLAUDE.md`; the latter and +this repo use Bun/AffineScript. No new application JS/TS/Python was introduced. +Existing licence files and SPDX notices were not swept or reclassified. + +== Verification record + +The final session summary in `STATE.a2ml [launch-hardening-audit]` records local +checks and unavailable tools. Required Rust commands are: + +[source,shell] +---- +cargo fmt --check +cargo test --workspace --locked +cargo test -p oikosbot-pareto --test trust_boundary --locked +cargo test -p oikosbot-cli --test check_gate --locked +bash tests/test-eclexia-adapter.sh +bash tools/ci/release-preflight.sh +---- + +Actually executed locally: workflow-linter's three checks plus lockcheck; Bash +syntax; Action argument acceptance/rejection with a Docker recorder; missing-tool +preflight refusal; tree-sitter parsing of changed Rust files; formatting/checking +with standalone rustfmt 1.8.0 (Rust 1.88.0). Eight isolated numerical tests passed: +four from the actual experimental contract module with enum-only adapters, and +four Pareto regression controls over extracted function bodies with serialization +removed. These checks are *not* Cargo integration tests, pinned-1.90.0 verification, +SARIF/CLI validation or proof of actual-world resource bounds. + +The issue query returned *seven open issues*, plus closed registry/release +incident #69 in the all-state query. #69 is not reopened or claimed re-verified: +anonymous pullability and the proposed release digest still need fresh checks. + +Main pushes publish candidates under `development` instead of automatically +advancing `latest`. Existing digests are unchanged. This limits accidental stable +promotion; it does not implement GitHub environment approval or replace an owner's +release review. Semver tags must not be created until the outstanding gates pass. diff --git a/docs/README.adoc b/docs/README.adoc index 9fd0e35..5e1ac78 100644 --- a/docs/README.adoc +++ b/docs/README.adoc @@ -46,7 +46,7 @@ fleet slot (see link:../DISAMBIGUATION.adoc[DISAMBIGUATION]). default workspace). * link:../analyzers/code-haskell/README.adoc[Haskell analyzer] — what `analyzers/code-haskell` computes and how it relates to the Rust workspace. -* _Still open (link:https://github.com/hyperpolymath/oikosbot/issues/16[#16]): an end-to-end build walkthrough._ +* link:../CONTRIBUTING.adoc[End-to-end build walkthrough]. == For maintainers @@ -56,7 +56,7 @@ fleet slot (see link:../DISAMBIGUATION.adoc[DISAMBIGUATION]). * link:ci-runbook.adoc[CI runbook] — the gate inventory, `actions.lock` rules, how to diagnose Hypatia and `Publish Image` failures, the `just` target reference, and the pre-release (GitHub Marketplace) checklist. -* _Still open (link:https://github.com/hyperpolymath/oikosbot/issues/16[#16]): the `README` → `docs/` split._ +* link:architecture.adoc[Implemented architecture] and link:policies.adoc[policy boundaries]. == For end users @@ -65,7 +65,7 @@ fleet slot (see link:../DISAMBIGUATION.adoc[DISAMBIGUATION]). * link:usage.adoc[Using OikosBot] — action inputs, the three modes, what each verdict and confidence level means, the SARIF shape, `.oikos.yml` configuration, `BOT_MODE`, and a troubleshooting table. -* _Still open (link:https://github.com/hyperpolymath/oikosbot/issues/17[#17]): a production deploy runbook — link:../DEPLOY.adoc[DEPLOY] is still a stub, gated on AffineScript operational parity._ +* link:../DEPLOY.adoc[Advisory rollout and App production hold]; operational parity remains unverified. == Machine-readable docs (A2ML) @@ -81,8 +81,8 @@ The canonical project state and decisions live in `.machine_readable/descriptile * link:STATUS.adoc[**STATUS**] — what works, what does *not*, and how each claim was verified. Includes the standing caveat that resource figures are static - estimates, that only calibrated findings may block a merge, and that the eco - score's absolute scale saturates under calibrated magnitudes. + estimates, that heuristic checks return no-check, and that no table row has + a calibration receipt. * link:COMPARISON-climate-warrior.adoc[**OikosBot vs Climate Warrior**] — positioning against the nearest GitHub Marketplace neighbour. @@ -112,3 +112,20 @@ For whoever picks this up next — the automation or a new maintainer: * link:handoff/open-issues.txt[**open-issues.txt**] — verbatim issue bodies. * link:handoff/CI-EVIDENCE.md[**CI-EVIDENCE**] — how the CI failures were diagnosed without access to logs or artifacts. + +== Launch hardening and research (2026-09-26) + +* link:LAUNCH-READINESS.adoc[Launch hold, every open issue and acceptance evidence] +* link:research/EVIDENCE-SCOPED-PARETO.adoc[Mathematics, type contract and pinned estate sources] +* link:architecture.adoc[Implemented versus target architecture] +* link:policies.adoc[Actual policy/configuration behavior] +* link:../CONTRIBUTING.adoc[End-to-end build and contribution walkthrough] + +The README split and build walkthrough are now supplied. Production App +operations remain gated, not completed by documentation alone. Earlier calibration +and regulator claims in historical debt/session records are superseded by this audit. + +== Continuation + +link:HANDOFF.adoc[Next-agent baton and the owner's decisions] separates executable +engineering work from the workload, scope and authority choices the owner must make. diff --git a/docs/STATUS.adoc b/docs/STATUS.adoc index 0f3a88c..13d7ff2 100644 --- a/docs/STATUS.adoc +++ b/docs/STATUS.adoc @@ -1,192 +1,48 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell = OikosBot: current state -:toc: macro -:icons: font - -_Point-in-time status, 2026-07-28 (generation 1). Machine-readable counterpart: -link:../.machine_readable/descriptiles/STATE.a2ml[`STATE.a2ml`]. Everything asserted here -was measured, not inferred — see <>._ - -toc::[] - -[#what-works] -== What works today - -OikosBot is **online in CI/Action mode**. A repository can consume it in three ways: - -[cols="1,3",options="header"] -|=== -| Surface | Status - -| GitHub Action -| `uses: hyperpolymath/oikosbot@v0.1.0` — runs the published container, no Rust - toolchain needed. Modes: `report` (SARIF), `check` (threshold), `compare` - (Pareto verdict). - -| Container -| `ghcr.io/hyperpolymath/oikos` — published, digest-pinned by the action. - -| CLI -| `cargo build --release` → `oikosbot analyze\|check\|report\|compare`. - -| Estate CLI -| `oikosbot estate collect\|analyse\|report` — organisation-scale analysis over - CI telemetry. Read-only; reports, does not gate. -|=== - -Findings reach GitHub's Security tab as SARIF 2.1.0, carrying `pareto_status`, -`pareto_score`, `pareto_dominated_by`, `econ_score` and a `confidence` label on -every result. - -=== The estate pipeline (newest, strongest evidence) - -Shipped 2026-08-03 (PR #60). Three crates — `oikosbot-telemetry`, -`oikosbot-capability`, `oikosbot-dea` — plus the `estate` subcommand. - -It exists because *allocative efficiency is not a property of a single diff*: -scarce resources flowing to their highest-valued competing use requires -competing claims on a shared budget. So the unit of analysis is the whole -estate, and the per-PR check becomes one projection of it. - -What makes it the strongest surface in the project: - -* *The inputs are measured, not modelled.* Wall-clock minutes come straight - from the GitHub API — the first genuinely `Measured` quantity anywhere in - OikosBot. No instrumentation is installed in the analysed repositories, and - history already recorded is available retroactively. -* *The premise was falsified first.* If compute time had turned out to be a - proxy for code volume, the DEA inputs would have been one variable in - disguise. Measured across 381 repositories and 69,445 runs: - `pearson(wall_minutes, size_kb) = -0.049`. Independent. -* *Output is verified capability, not activity* — does a workflow parse, can a - gate actually fail, do artefacts publish. A gate with many successes and no - failure in its whole history has never demonstrated it can fail. -* *DEA gives an exemplar, not just a score.* Each off-frontier repository is - returned with its peer reference set — the frontier repositories it is - measured against — plus the LP duals, which are shadow prices derived from - the estate's own data. -* *Analysis is deterministic:* two runs over the same staging produce - byte-identical `analysis.json`. - -Snapshots live in -https://github.com/hyperpolymath/oikosbot-estate[`hyperpolymath/oikosbot-estate`] -— data only, no code, one directory per collection run, so history is git -history and OikosBot never silently measures a corpus containing itself. - -Round one deliberately does **not** gate. See link:../DEBT.adoc[`DEBT.adoc`] -for its known rough edges (zero-run repositories are absent from the analysis; -zero-output repositories score θ = 0 with empty peer sets, an artefact of -flooring rather than a ranking). - -=== The Pareto engine - -The economic core (`crates/oikosbot-pareto`) is real and executable: - -* ε-tolerant dominance, frontier extraction, dominated-by ranking — all total - functions (no panics on empty or degenerate input). -* Objectives are **min-max normalized before any distance is measured** — raw - gCO2e, joules and bytes are not commensurable — with weights folded into the - metric. ParetoScore = `100 × (1 − weighted distance to frontier)`. -* Pull-request verdicts: **pareto-improvement**, **pareto-regression**, - **trade-off**, **neutral**, with per-objective deltas. -* Trade-off documentation (a `Pareto-Trade-off:` trailer or heading) is - machine-checked, per the doctrine "don't improve one axis at another's - expense without saying so". -* `EconScore = 0.5·Pareto + 0.3·allocation (shadow prices) + 0.2·debt`, as - specified in link:../ARCHITECTURE.adoc[ARCHITECTURE.adoc]. - -[#what-does-not] -== What does *not* work yet — read this before trusting a number - -[IMPORTANT] -==== -**Resource figures are estimates, and enforcement is earned per finding.** - -`Analyzer::estimate_resources()` has two paths. A unit with a recognised -pattern is priced from `calibration::estimate_operation()` and carries that -row's confidence — `Calibrated` for the measured operation kinds, `Estimated` -for the host-dependent ones. A unit with no recognised pattern stays on the -naive `complexity × 0.1 J` form and is labelled `Estimated`. - -Since only `Measured` or `Calibrated` inputs may fail a run, `--check` blocks -on an undocumented regression whose drivers are calibrated, and refuses to -block on heuristic drivers — saying so loudly in a `::warning::` rather than -passing silently. A gate that quietly does nothing is indistinguishable from one -that passed, which is exactly the failure mode OikosBot exists to find. - -Wired in issue #48. What is *not* earned yet: the absolute figures have never -been validated against profiling data, and the eco score's log scale is anchored -at 1 J, so calibrated (microjoule-scale) estimates clamp near 100 — the eco -threshold therefore discriminates far less than the Pareto verdicts, which -compare base against head. Treat OikosBot as an advisor with a working -regulator, not the reverse. -==== - -Also incomplete: - -* **Languages**: Rust, JavaScript, Python only. A `.oikos.yml` naming - unsupported languages is warned about loudly, never silently skipped. -* **GitHub App** (webhook, comments without CI): not live. Ruled 2026-07-28 to - wait on upstream AffineScript stdlib work rather than build an interim - listener. -* **Policy engine**: `policy-engine/datalog/eco_rules.dl` has never executed. It - is a specification, kept as the reference the Rust dominance code conforms to. - Ruled port target: Scallop (Rust), not DeepProbLog (Python). -* **VeriSimDB / praxis loop**: design and config only. - -[#how-we-know] -== How we know (verification performed) - -* The published container was pulled and run end-to-end, producing SARIF that - **GitHub accepted: 101 results** ingested via - `metadatastician/enaction-engine` PR #21. -* Workspace tests pass, including property tests for dominance - (irreflexivity, antisymmetry), the frontier invariant, normalization - orientation, and conformance with the Datalog dominance semantics. -* The pinned image digest was verified to resolve, run, and contain the SARIF - fix before being committed. - -=== Two measurement traps worth remembering - -. **Parseable JSON is not schema-valid SARIF.** `serde_json` accepted output - that GitHub rejected, because our `fixes[]` objects lacked the required - `artifactChanges`. No OikosBot SARIF was ingestible before #45. A regression - test now guards it. -. **Timeout expiry is reported as "cancelled", not "failed".** The docs site had - never deployed — 30 consecutive runs showed `cancelled`, which were the cold - GHC build exceeding a 15-minute limit. Raised to 60; the site is now live. - -[#consumers] -== Consumers - -Seventeen repositories carry a `.oikos.yml` and an OikosBot workflow. All of -them previously pinned an image tag that had never existed, and masked the -resulting failure with `|| echo` — so the gate was green while never once -executing. All are now on the composite action, having been proven on one repo -first before the pattern was swept. - -Consumer workflow shape (see link:../examples/oikosbot-ci.yml[`examples/oikosbot-ci.yml`]): - -[source,yaml] ----- -- uses: hyperpolymath/oikosbot@v0.1.0 - with: - mode: report - path: . - output: results.sarif -- uses: github/codeql-action/upload-sarif@ - with: - sarif_file: results.sarif - category: oikosbot ----- - -A `container:` job cannot be used: the image sets a non-root `USER`, which -GitHub rejects. - -== Positioning - -See link:COMPARISON-climate-warrior.adoc[OikosBot vs Climate Warrior] for how -this differs from the nearest GitHub Marketplace neighbour. In short: that -product is storage with carbon telemetry; OikosBot is a code analyzer with an -economics engine. The overlap is the SCI vocabulary and nothing else. +:toc: + +As of 2026-09-26: *alpha; hold general launch and regulator claims*. +This supersedes earlier descriptions of a “working regulator” based on the +pattern table. Calibration wiring existed, but no calibration receipts were +found; labels alone were not earned evidence. + +== Implemented surfaces, not certification + +* Standalone Rust analysis, metrics, SARIF, point-vector Pareto comparisons and + builtin policy adapter. The native Eclexia path reports unavailable. +* Read-only estate telemetry/capability/DEA and snapshot/report commands. +* Previously published Action and container. The pinned digest is an older + artifact; this branch's changes have not been released into it. +* Haskell independent analyzer and AffineScript App scaffold. + +== Proposed hardening on this branch + +Heuristic evidence stays Estimated. Invalid comparisons become Indeterminate; +epsilon no longer permits losses/cycles. CLI no-check exits are explicit and +format-independent; parser/traversal/config failures cannot masquerade as success. +Taxonomy is typed and emitted for pattern findings. Collection validates responses, +pagination and partial failures. Candidate container checks and a pinned Rust +version are added. See link:LAUNCH-READINESS.adoc[the full issue-by-issue audit]. + +The new `pareto::contract` module is an *experimental runtime refinement*, not a +production authority or a proven novel type system. It checks scoped interval +comparisons and non-compensatory hard limits. Its research/proof obligations and +estate relationships are described in +link:research/EVIDENCE-SCOPED-PARETO.adoc[the research note]. + +== Not demonstrated + +Reproducible resource calibration, real workload equivalence, validated physical +budgets, authenticated evidence ingestion, full SARIF schema/Action/release +conformance, mutation/fuzz coverage and benchmark baselines remain outstanding. +The App listener/transport, deployment operations and VeriSimDB runtime client +are not demonstrated. No source-level orchestration claim establishes a deployable App. + +Full pinned-toolchain Rust/Haskell/AS/container execution was unavailable. +An older standalone formatter and eight isolated Rust numerical checks ran; +these do not establish workspace integration or serialization correctness. +New tests are authored but require CI execution; earlier successful main-branch +runs do not test this branch. Check `STATE.a2ml [launch-hardening-audit]` for exact +local verification. No release-readiness grade or formal proof is claimed. diff --git a/docs/architecture.adoc b/docs/architecture.adoc new file mode 100644 index 0000000..8106ba0 --- /dev/null +++ b/docs/architecture.adoc @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += Implemented architecture + +The standalone Rust CLI orchestrates `analysis` (tree-sitter patterns), `metrics` +(result types and heuristics), `pareto` (point comparisons plus an experimental +scoped interval contract), `sarif` (reporting), and `eclexia` (builtin policy +adapter with explicit native-unavailable behavior). + +The estate pipeline is `telemetry -> capability -> dea -> report`. It collects +through the authenticated `gh` CLI, stages JSON, and writes Parquet snapshots. +It is not a calibration system or a mutation engine. See +link:LAUNCH-READINESS.adoc[measurement and completeness limitations]. + +`oikosbot-fleet` is excluded from the default workspace and requires a compatible +sibling gitbot-fleet. Its Oikosbot identity is separate from Sustainabot. Haskell +builds independently. The AffineScript App and VeriSimDB client are incomplete. +Do not infer their operational readiness from the Rust build. + +link:../ARCHITECTURE.adoc[ARCHITECTURE] remains the detailed *target* design, +not evidence of deployed components. Per-crate guides are indexed in +link:README.adoc[the documentation map]. For the research contract and boundaries, +see link:research/EVIDENCE-SCOPED-PARETO.adoc[evidence-scoped Pareto judgements]. diff --git a/docs/ci-runbook.adoc b/docs/ci-runbook.adoc index 3f00e02..3f80062 100644 --- a/docs/ci-runbook.adoc +++ b/docs/ci-runbook.adoc @@ -188,7 +188,7 @@ recurring estate-wide patterns: `rust-version = "1.90"` and `edition = "2024"`, while the `Containerfile` builder was `rust:1.88-slim`. CI on `ubuntu-latest` passed because the runner ships a newer rustc; the container did not. The builder now uses the - rolling `rust:1-slim`. Diagnose this class of failure by checking the + explicit `rust:1.90.0-slim-bookworm` (matching `rust-toolchain.toml`). Diagnose this class of failure by checking the *builder* toolchain against the workspace's highest declared `rust-version`, not by trusting CI's green tick. |=== @@ -225,3 +225,24 @@ The Action is the release unit: `action.yml` plus the container image it pins. 6. The Marketplace listing is published from `main` by a repository owner, and the `DEPLOY.adoc` gate (AffineScript operational parity) is either met or explicitly still open. + +== Additional launch-hardening checks + +`CI / Container smoke contract` builds the candidate on PRs after Rust tests, +then tests version, default non-root user, silent clean SARIF and a known firing +rule. Publish Image now runs the same contract on a locally loaded candidate +before pushing. Neither checks full SARIF schema nor proves that the final pushed +digest is identical to the candidate; the release-digest/provenance contract is +still required. New source/test/toolchain files must trigger publication when +an owner decides to adopt the changes. + +`just release-preflight` collects local evidence and returns 2 if required tooling +is absent. The launch audit, not this script, owns the list of remaining gates. +Do not treat the informational clippy job, AffineScript shell syntax job or +security scanner's continue-on-error as full conformance gates. + +Main-branch publication now updates `development`, not `latest`. It is a candidate +channel, not a launch endorsement. Existing digest consumers are unaffected; +consumers of `latest` do not silently receive these breaking check semantics. +Semver publication remains tied to an explicitly created version tag and must +follow the launch review; no version tag was created in this work. diff --git a/docs/policies.adoc b/docs/policies.adoc new file mode 100644 index 0000000..675d83a --- /dev/null +++ b/docs/policies.adoc @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += Policies: implementation and authority + +The Rust CLI consumes a small subset of `.oikos.yml`: mode, Eco-score floor, +exclude globs and supported-language selection. Most of `config/oikos.yaml` is +a target schema, not live runtime configuration. Unknown top-level target fields +are still accepted; do not assume they enforce anything. See link:usage.adoc[usage]. + +The default `oikosbot-eclexia` implementation is a builtin rule adapter, not an +Eclexia interpreter. `eclexia-native` explicitly reports unavailable until native +integration exists. `.ecl` files and the Datalog/other policy-engine surfaces are +not proof that arbitrary policy source executes. Use `tests/test-eclexia-adapter.sh` +to check the actual builtin/native rejection contract. + +The canonical finding vocabulary is +`NEUROSYM.a2ml [finding-taxonomy]`; META points to it. Maintenance and locus are +categories, not ordered rankings. Current heuristic Rust patterns emit +`wish/perfective/externalities`. No conversion exists from the resource evidence +ladder to the example 0.95/0.85 classifier's probability input. The taxonomy +sample's automatic action function returns false: an obligation requires explicit +policy authority as well as evidence, and consent/exception handling where applicable. + +Policy budgets must not be inferred from score weights. The experimental Pareto +contract supports hard ceilings/floors on interval-valued dimensions, but is not +wired into the App or a production resource gate. Money saved cannot buy an +undeclared exception to a carbon cap. A trade-off heading is a documentation +signal, not an approved exception. diff --git a/docs/research/EVIDENCE-SCOPED-PARETO.adoc b/docs/research/EVIDENCE-SCOPED-PARETO.adoc new file mode 100644 index 0000000..ce1bf7d --- /dev/null +++ b/docs/research/EVIDENCE-SCOPED-PARETO.adoc @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += Evidence-scoped Pareto judgements: type contract and research question +:toc: +:stem: latexmath + +Status: *designed*, with an experimental Rust runtime refinement prototype in +`crates/oikosbot-pareto/src/contract.rs`. No novel calculus or new theorem is +claimed. No upstream proofs were executed in this audit, and none have been +transported into the Rust implementation. + +== The apparent paradox + +Economics and ecology do not need to be collapsed into one ordered scalar. +Resource allocation can take ecological limits as constraints rather than as +prices. Pareto order is a *partial* order: incomparability is legitimate output, +not a type error to hide. A Pareto frontier alone cannot choose among incomparable +points, make a feasible set nonempty, or decide whose welfare matters. + +Three different questions must stay separate: + +. *Feasibility*: are non-negotiable constraints satisfied? +. *Comparison*: does one admissible alternative improve on another under a + declared model, or do they trade off? +. *Authorization*: who may accept the trade-off, with what evidence, safeguards, + expiry and appeal mechanism? + +Ordinary products, sums, refinements, dependent pairs and indexed relations can +represent these today. A new primitive type is not needed merely to store a +vector or prevent an unvalidated value from entering a checked API. The research +opportunity is in *evidence-preserving transport, composition and revision* of +these judgements, including the consequences of characterization choices. + +== A precise bounded claim + +Let a context `C` specify useful functional unit, workload distribution, system +boundary (operational/embodied effects, upstream/downstream), horizon, population, +objective schema/units/directions, model revision and policy revision. Let `E` +be evidence and `W(C,E)` the nonempty set of worlds consistent with it. +A robust no-worse judgement for head `h` versus base `b` requires, after orienting +all axes as costs: + +[stem] +++++ +\forall w \in W(C,E),\ \forall i,\ f_i(h,w) \le f_i(b,w). +++++ + +For the prototype's stronger fixed-driver improvement require: + +[stem] +++++ +\exists j,\ \forall w \in W(C,E),\ f_j(b,w)-f_j(h,w) > \epsilon_j. +++++ + +This differs from `for every world there exists an improved axis`: preserve the +quantifier order. Constraints also hold universally, not just at mean estimates. +If `W` is empty, return inconsistent evidence, not a vacuously certified win. +Applying the claim to reality additionally requires actual-world membership and +adequacy of the observation/valuation model. No type checker can infer those +from syntax or a confidence enum. + +A schematic dependent record (specification, not checked code) is: + +[source,text] +---- +ScopedJudgement C E b h = + admissibleWorldsNonempty + × functionalEquivalenceUnder C b h + × hardConstraintsHold C E h + × (RobustImprovement C E b h + + RobustRegression C E b h + + EstablishedTradeOff C E b h + + Neutral C E b h + + Unresolved C E b h) + × evidenceReceipts + × excludedObservablesAndResidualRisk +---- + +`Authorized C E judgement` must be a *different* type supplied by an actual +policy/consent authority. Never provide `Confidence -> Authorized` or +`tradeoff_heading_present -> Authorized`. Even measured data can be irrelevant +to the claim; calibration does not make an obligation. + +== Runtime prototype and current CLI + +The experimental module implements only finite interval-box numeric checks: + +* `Interval::new` rejects reversed, NaN and infinite bounds. +* `Observation::new` requires one value/evidence record per dimension, nonempty + context, units and source names, distinct axes and finite per-axis tolerances. +* Comparisons require equal contexts, including ordered units and policy/model + versions. No implicit currency, carbon-unit or workload conversion exists. +* For a minimizing axis, head's upper bound must be at most base's lower bound + to establish robust no-worsening. Interval overlap is normally `Unresolved`, + not a point-estimate victory. Box independence is conservative; real joint + evidence can support more precise conclusions. +* `admit()` constructs a private `Feasible` witness only when the whole interval + satisfies every declared ceiling/floor. A cheaper point outside a carbon cap + cannot buy admission. This checks declared numbers, not real-world feasibility. +* Evidence records remain attached. Their source strings are not authenticated, + and their confidence fields do not confer authority. There is deliberately no + deserializer or public constructor for the checked witnesses and no conversion + from these witnesses to an auto-fix/merge capability. + +The existing CLI has no workload-equivalence or evidence-ingestion adapter, so +it does *not* use this module to authorize anything. Its old point-vector scores +remain advisory proxies. Missing evidence in an explicitly requested gate is +exit 2, not success and not an ecological violation. + +== Two concrete mathematical failure modes + +=== Epsilon cycles + +Previously `a` could dominate `b` while being worse by epsilon on some axes. +At epsilon 1, all-minimize vectors `(0,1,2)`, `(2,0,1)`, `(1,2,0)` form a cycle: +each wins by 2 on one axis and loses by 1 on two. The frontier is empty; the old +fallback awarded 100 anyway. The corrected relation allows *no losses* and +requires at least one win greater than epsilon. Its intended real-number +relation is transitive and irreflexive. Exhaustive finite-grid tests are not a +machine-checked proof for the floating-point implementation. + +=== Scalarization forgets what is needed for safety + +Two points with equal weighted score can lie on opposite sides of a hard carbon +cap. For equal weights, `(carbon=12,cost=0)` and `(carbon=6,cost=6)` both score 12; +a cap of 10 separates them. A function seeing only 12 cannot decide both cases +correctly. Retain the original vector, units, constraints and evidence rather +than claiming a scalar is a lossless summary. Positive-weight scalarization may +select supported frontier points; it cannot recover every nonconvex frontier or +supply normative weights. + +== Relation to the estate (source inspection, not proof reuse) + +* *Eclexia*: dimensional resource types, hard `requires` constraints, adaptive + solutions and shadow prices are directly relevant. Prices are marginal values + relative to constraints/model; they do not cancel ecological ceilings. The + OikosBot native adapter remains unavailable and the builtin evaluator does not + execute arbitrary `.ecl`. Do not imply full Eclexia semantics are integrated. +* *Residual Evidence Types*: `world + observation equality + E(world)` is the + closest formulation for claims true across all admissible candidates. Preserve + nonemptiness and actual-world membership rather than counting candidates as + probabilities. The library explicitly describes ordinary dependent constructions. +* *Epistemic Types*: warrant versus sound warrant, receiver-relative claim meaning, + and evidence transport address the central trust-boundary issue. A report or + receipt must not silently become factive knowledge after serialization. +* *Echo Types*: use fibers/residues to expose what a scalar projection forgets. + Retaining the source witness differs from asserting the scalar identifies it. + An aggregate score is not an ecological proof; a resource grade is not an Echo. +* *Tropical Types*: composition laws for resource bounds are relevant, but must + differ by resource and execution model. Sequential energy can add; concurrent + latency may take a max under stated scheduling assumptions; peak memory has + lifetime/overlap constraints. Do not use a single semiring for all axes by fiat. +* *Choreographic Types*: distributed measurement/decision workflows could need + provenance transport across causal cuts. K-CUT is presented as open upstream; + do not use it as an established theorem or a current launch dependency. +* *Absolute Zero*: declare the observation model and excluded observables, and + separate null effect from null disclosure. Identity of program output is not + zero physical energy, and not evidence of no external ecological effect. +* *JanusKey*: inversion metadata can restore modeled file state; it cannot undo + consumed electricity, emissions, disclosure or a decision's social impact. + Restoring code is containment/rollback, not full external-world reversibility. +* *Proven-Tests*: paired silent/firing fixtures and evidence grades are adopted + as testing discipline. Rust examples are not reclassified as Idris2 proofs. + +== Falsifiable research programme (not a launch prerequisite) + +. Mechanize the minimal context-indexed order and feasibility record in one + existing estate prover. Prove irreflexivity/transitivity; exhibit incomparable + alternatives and an empty feasible set. State the numeric domain explicitly. +. Prove soundness of interval comparison relative to box membership, then give + counterexamples showing where correlations make that abstraction incomplete. +. Establish a no-smuggling theorem for evidence transport: changing boundary, + workload, units, observer or policy requires an explicit validated mapping. +. Investigate composition under shared budgets and overlapping effects. Two + locally feasible changes need not be jointly feasible; duplicated evidence + and double-spent budgets need linear/affine accounting or a ledger. +. Define revision/retraction: invalidated calibration must invalidate dependent + judgements, not leave stale green badges. Explore monotonicity under stronger + evidence and its failure when policy/observation boundaries change. +. Compare against refinement/dependent types, robust multiobjective optimization, + multi-criteria decision analysis, resource/effect systems and decision theory. + Claim novelty only after a literature review and a separation result identifying + what this assembly does that those existing constructions do not. + +Do not let the research name imply that economics and ecology are now reconciled +universally. An appropriate working description is *evidence-scoped Pareto +judgements with non-compensatory constraints*, not a proved “sustainability type”. + +== Characterization and adoption + +Publish the model card alongside every report: whose objectives, workload and +functional equivalence, time horizon, geography, lifecycle boundary, uncertainty, +omitted effects, normalization/reference cohort and policy authority. Show the +vector/frontier and limitations before any aggregate ranking. Provide contestability, +manual review and retraction; do not rank people or infer moral worth from repo CI. + +Absolute ecological budgets, distributional effects and rebound (cheaper work +causing more work) can defeat per-unit efficiency claims. “Pareto-optimal” means +nondominated among the compared alternatives under this model, *not* sustainable, +fair, globally optimal or fit to launch. This characterization is part of the +safety contract, not marketing copy added after the mathematics. + +== Pinned inspected sources + +Revisions below identify the downloaded source snapshots. Read their README and +proof-status/contract boundaries, not just their repository descriptions. They +are references, not new runtime dependencies or certification authorities. + +[cols="1,2",options="header"] +|=== +|Repository |Inspected revision +|rsr-template-repo |https://github.com/hyperpolymath/rsr-template-repo/tree/05bf1b97dd647e02e2a66a33e511a8922ca60e61[`05bf1b97dd647e02e2a66a33e511a8922ca60e61`] +|standards |https://github.com/hyperpolymath/standards/tree/2479cf769ed5f0481ccf64860a2ab954514c2b59[`2479cf769ed5f0481ccf64860a2ab954514c2b59`] +|eclexia |https://github.com/hyperpolymath/eclexia/tree/7c67b30620840d628436cf209267e36ae810a1d0[`7c67b30620840d628436cf209267e36ae810a1d0`] +|echo-types |https://github.com/hyperpolymath/echo-types/tree/e6e7ee71e783b932e3cb08ef87495b0289e48d3c[`e6e7ee71e783b932e3cb08ef87495b0289e48d3c`] +|residual-evidence-types |https://github.com/hyperpolymath/residual-evidence-types/tree/eb01e18a384f34489fbb14907b19bc4a7b4675bd[`eb01e18a384f34489fbb14907b19bc4a7b4675bd`] +|tropical-types |https://github.com/hyperpolymath/tropical-types/tree/9a5c34f20aba2589d44e8b1bbf32540551ffa395[`9a5c34f20aba2589d44e8b1bbf32540551ffa395`] +|epistemic-types |https://github.com/hyperpolymath/epistemic-types/tree/dd948fbdddbdd06282cda8425e3e644e013c11ff[`dd948fbdddbdd06282cda8425e3e644e013c11ff`] +|choreographic-types |https://github.com/hyperpolymath/choreographic-types/tree/98193bec8579a2094ee3cd7828f05ebbefdc50ba[`98193bec8579a2094ee3cd7828f05ebbefdc50ba`] +|absolute-zero |https://github.com/hyperpolymath/absolute-zero/tree/3e959cbc5b94c4ab199ef0567b45002043f24d75[`3e959cbc5b94c4ab199ef0567b45002043f24d75`] +|januskey |https://github.com/hyperpolymath/januskey/tree/b9aaca1ec74e0d3a1e9fe8c75a2151cacfaf8e4d[`b9aaca1ec74e0d3a1e9fe8c75a2151cacfaf8e4d`] +|proven-tests-and-benches |https://github.com/hyperpolymath/proven-tests-and-benches/tree/6b19deb7ccbd73836dff8d6c8e65ec9351fa7767[`6b19deb7ccbd73836dff8d6c8e65ec9351fa7767`] +|=== + +Additional coordination reference: +https://github.com/hyperpolymath/nextgen-typing/blob/86cb69e5cf296f6188eaf03775d9b6f3084a36ea/docs/TYPE-CONNECTIONS.adoc[nextgen-typing TYPE-CONNECTIONS at `86cb69e5cf296f6188eaf03775d9b6f3084a36ea`]. +Its shared glossary explicitly treats these families as complementary questions, +not stronger/weaker ranks or necessarily new primitives. Any future named Pareto +research family should be coordinated there after its proof/novelty obligations +are stated; no new estate repository or family registration is proposed as done. + +Concrete Eclexia implementation inspected at the pinned revision: +`runtime/eclexia-runtime/src/adaptive.rs`, especially +`SolutionCandidate::meets_budget` and `AdaptiveDecisionEngine::select_best`. +It already filters candidates by resource budget *before* weighted/shadow-price +selection. Thus feasibility-before-preference is not a new OikosBot invention. +That implementation also uses ordinary floating-point comparisons; those are +not a substitute for finite-value validation or measurement evidence. OikosBot's +experimental boundary rejects non-finite values rather than inheriting a +comparison's fallback-to-equality behavior. No upstream patch or proof is claimed. diff --git a/docs/usage.adoc b/docs/usage.adoc index 7f74a09..9489c1c 100644 --- a/docs/usage.adoc +++ b/docs/usage.adoc @@ -1,394 +1,193 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell = Using OikosBot -:toc: macro -:icons: font +:toc: -Audience: anyone consuming OikosBot — as a GitHub Action in a workflow, as the -published container, or as the CLI in a pipeline. This page is about *running* -it and *reading what it says*. For what the numbers mean and how far they can -be trusted, start with link:../EXPLAINME.adoc[EXPLAINME] and -link:../DEBT.adoc[DEBT]; the short version is repeated in -<> below. +This guide describes *this source branch*. An existing pinned container may +have older semantics. Do not assume changing `action.yml` documentation upgrades +its image. See link:LAUNCH-READINESS.adoc[release gates and remaining work]. -== Two ways to run it +== CLI modes and exit contract -[cols="1,4"] -|=== -| Composite Action | `uses: hyperpolymath/oikosbot@main` (or a pinned SHA). Runs the -published CLI container, so a consumer needs no Rust toolchain. See -`examples/oikosbot-ci.yml` for a complete workflow. - -| Container | `docker run --rm ghcr.io/hyperpolymath/oikos@sha256: `. -Same binary, same flags; useful for local runs and for CI systems that are not -GitHub Actions. -|=== - -The CLI's entrypoint is `oikosbot`, with four subcommands: `report`, `check`, -`compare` and `estate` (plus `self-analyze` for dogfooding). - -== Action inputs - -Every input is optional; the defaults are what `examples/oikosbot-ci.yml` uses. - -[cols="2,1,3,2",options="header"] -|=== -| Input | Default | Meaning | Maps to CLI - -| `mode` -| `report` -| `report` (write SARIF), `check` (threshold gate), `compare` (Pareto verdict -against a base). Anything else is a hard error. -| subcommand - -| `path` -| `.` -| Directory to analyse; in `compare` mode this is the *head* directory. -| positional arg - -| `base` -| _(empty)_ -| Base directory for `compare` mode — e.g. a checkout of the target branch. -Required when `mode: compare`. -| positional arg - -| `format` -| `sarif` -| `sarif` \| `json` \| `text`. -| `--format` - -| `output` -| `results.sarif` -| Output file; empty means stdout. The written path is exposed as the action's -`output-file` output. -| `--output` - -| `config` -| _(empty)_ -| Path to an `.oikos.yml`. Empty means auto-discover one under `path`. -| `--config` - -| `eco-threshold` -| _(empty)_ -| Eco-score floor, 0–100. Empty means "take it from the config, else 50". -| `--eco-threshold` - -| `pr-body` -| _(empty)_ -| File holding the PR description, for the trade-off documentation check in -`compare` mode. -| `--pr-body` - -| `check` -| `false` -| In `compare` mode, fail the run on an undocumented regression or trade-off -_whose drivers are measured or calibrated_. Heuristic findings still cannot -block; see <>. -| `--check` - -| `image` -| pinned digest -| The container image to run. Pinned by digest, never by tag — override it only -to test a candidate build. -| (action-level) -|=== - -[NOTE] -==== -`eco-threshold` is an *eco-score* floor, not an energy or carbon figure. The -config keys `thresholds.eco_minimum.carbon` and `.energy` both express that -same 0–100 floor today; `carbon` wins if both are set. -==== - -== What each mode does - -`report`:: Analyse the directory and emit findings. With `format: sarif` the -output is uploaded to GitHub code scanning by the caller's workflow; `json` is -the same results with the SARIF envelope removed; `text` prints a summary. - -`check`:: Analyse, then compare each finding's eco score against the floor. -Below-floor findings are listed, and the run **fails (exit 1) when the config -says enforcement is blocking** — either `enforcement: blocking` or -`mode: regulator`. With an `advisor`/`consultant` config the run reports and -exits 0, printing which config made it advisory. - -`compare`:: Analyse base and head, project both onto the five Pareto -objectives, and classify the change: - -[cols="2,4"] -|=== -| Verdict | Meaning - -| `Improvement` -| Head is better on at least one objective and worse on none. - -| `Regression` -| Head is worse on at least one objective and better on none. - -| `TradeOff` -| Better on some, worse on others. Must be documented with a -`Pareto-Trade-off:` trailer (or a heading containing "pareto trade-off") in the -PR body passed via `pr-body`. - -| `Neutral` -| No objective moved by more than ε (1e-6). -|=== - -[#enforcement] -=== Enforcement: what can actually fail a run - -`compare --check` fails on an undocumented `Regression` or `TradeOff` **only -when every driving objective is backed by `Measured` or `Calibrated` input**. -That gate exists because a heuristic number must never be able to block a -merge, and it is enforced by the code, not by convention. - -What that means in practice: - -* A function whose detected pattern maps to a known operation category is - priced from the calibration table - (`crates/oikosbot-analysis/src/calibration.rs`) and carries - `Confidence::Calibrated` for the measured rows (`HashLookup`, `Sort`, - `Allocation`, `MathCompute`) — so a regression driven by those findings *can* - block. -* A function with no recognised pattern stays on the naive complexity-derived - estimate and carries `Confidence::Estimated`, which can never block. -* When `--check` is requested but the drivers are heuristic, the run **exits 0 - and prints a `::warning::` naming the verdict and the confidence level**. - A gate that quietly does nothing is indistinguishable from one that passed, - which is the failure mode OikosBot exists to find. - -The estate path (`oikosbot estate`) is the one place with genuinely `Measured` -input: `wall_minutes` comes straight from the GitHub API. - -== Reading a finding - -Every finding is one analysed function (or one synthetic policy/security -record) with: - -* **location** — file, line/column, and the end position used for range - annotations, plus the function name; -* **rule_id** — `oikosbot/` (see the rule table below) or - `oikosbot/general` for a function with no detected pattern; -* **suggestion** — the concrete fix, also printed into the SARIF message; -* **resources** — energy (J), duration (ms), carbon (gCO2e), memory (bytes); -* **health** — eco (0–100), econ (0–100), quality (0–100), and the composite - `overall = 0.4·eco + 0.3·econ + 0.3·quality`; -* **confidence** — the ladder below; -* **resource_range** — when the estimate is calibrated, the min/typical/max - band behind `resources`. `resources` *is* the typical bound. Absent on the - naive path, where no band was computed. - -[cols="1,3"] -|=== -| Confidence | Meaning - -| `Measured` -| Read from an instrument (the GitHub API's wall-clock minutes). The only - level that needs no calibration. - -| `Calibrated` -| Priced from a measured operation profile. Earned per finding, per operation - kind — never assigned to a whole file or run. - -| `Estimated` -| Heuristic: a pattern we recognise but have not measured (host-dependent I/O - and string work), or the naive complexity path. Advises; never blocks. - -| `Unknown` -| No estimate at all (the generic fallback row). -|=== - -[IMPORTANT] -==== -Absolute figures are still small, static and unvalidated against profiling -data. Treat the *relative* signal — which function is worse, which direction a -diff moves — as the product, and the absolute joules as an order-of-magnitude -hint. See link:../DEBT.adoc[DEBT] for the current list of unearned claims. -==== +[source,shell] +---- +oikosbot analyze src/main.rs --format json +oikosbot report . --format sarif --output results.sarif +oikosbot compare BASE HEAD --format json --output comparison.json +---- -=== Rules +* `analyze`: one supported source file, text/JSON/SARIF. +* `report`: a directory, always advisory; default SARIF. +* `check`: directory threshold check. Explicit advisor config makes it advisory; + absent config, regulator mode or blocking config requests enforcement. +* `compare`: advisory base/head model comparison; `--check` requests enforcement. + `--pr-body FILE` detects a documentation marker, *not* policy approval. +* `estate`: read-only portfolio telemetry and relative DEA. It never gates. -[cols="2,1,4",options="header"] +[cols="1,3",options="header"] |=== -| Rule id | Default level | Fires when - -| `oikosbot/nested-loops` -| warning -| Loop nesting depth ≥ 3. - -| `oikosbot/busy-wait` -| warning -| A `loop`/`while` whose body has no sleep/await/yield/recv, no I/O and no - legitimate iteration. - -| `oikosbot/string-concat-in-loop` -| warning -| A `+`/`+=` string concatenation inside a loop body. - -| `oikosbot/clone-in-loop` -| note -| `.clone()` inside a loop body. - -| `oikosbot/unbuffered-io` -| warning -| `File::open`/`File::create` with no `BufReader`/`BufWriter` in scope. - -| `oikosbot/large-allocation` -| note -| An allocation with a numeric literal above 1,000,000. - -| `oikosbot/redundant-allocation` -| note -| Five or more `.to_string()`/`.to_owned()` in one function. - -| `oikosbot/eco-threshold`, `oikosbot/carbon-intensity`, - `oikosbot/security-sustainability` -| warning -| Emitted by the threshold, policy and security-correlation paths rather than - by pattern detection. +|Exit |Meaning +|0 |Requested analysis/report completed, or an explicitly advisory check completed. +Not proof of sustainability or that an enforcement check passed. +|1 |Supported check returned a negative outcome. Not currently earnable from +per-file resource estimates; retained for future validated inputs. +|2 |No requested check performed: insufficient evidence, empty/excluded/unsupported +input, syntax/read/config error, incomplete collection, invalid format or missing infrastructure. |=== -=== SARIF shape - -`format: sarif` emits SARIF 2.1.0 with one run, driver name `oikosbot`, and the -rules above in `tool.driver.rules`. Per result: - -* `ruleId` is the finding's `rule_id`; `level` comes from the eco score - (`error` below 30, `warning` below 60, else `note`). -* `locations[0].physicalLocation.region` carries start/end line and column, so - GitHub can annotate the whole function. -* `properties` carries the machine-readable payload: `eco_score`, - `econ_score`, `quality_score`, `overall_health`, `energy_joules`, - `carbon_gco2e`, `duration_ms`, `memory_bytes`, `confidence`, optional - `suggestion`, optional `pareto_status` / `pareto_score` / - `pareto_dominated_by`, and — for calibrated estimates — `resource_range` - with `min`/`typical`/`max` per axis. - -`oikosbot/general` records are per-function telemetry, not defects: they appear -in `json`/`text` output and are **filtered out of the SARIF results**, so -analysing a repository does not raise a code-scanning alert for every function -in it. - -== Configuring it (`.oikos.yml`) - -Place an `.oikos.yml` in the analysed directory (or pass `--config`). The keys -that take effect today: +For a nonblocking review use `report` or `compare` without `--check`. Do not +mask status 2 with `|| true` and then claim a passed gate. JSON and SARIF go to +stdout or `--output`; logs go to stderr. Text also honors `--output`. + +*Compatibility change*: previous heuristic `compare --check` warned and exited +0. It now emits its advisory report then exits 2, even when a trade-off heading +exists or the point estimate is unchanged. Old text-only threshold failure was +also format-dependent; gate evaluation now follows rendering for every format. + +== Findings, scores and confidence + +A finding identifies a *syntactic pattern*, not measured waste. Review semantics +and profile representative useful work before changing it. For example, +`oikosbot/nested-loops` invites algorithm review; a small fixed loop may be correct +and cheap. `busy-wait` suggests checking blocking/event-driven alternatives. +Memory, latency and complexity trade-offs must be evaluated in context. + +Eco/Econ/Quality/Overall 0–100 values are heuristic model scores, not percentages +of sustainability or a comparison to a validated world baseline. Overall uses +`0.4*Eco + 0.3*Econ + 0.3*Quality`: inherited value choices, not empirically +justified universal weights. The CLI does not implement arbitrary config weight +overrides. The log-scaled Eco formula can saturate near 100 on tiny table values. +Do not select a launch threshold merely because the score looks precise. + +* `Measured`: sourced from observations within a declared boundary, not necessarily + measuring the intended quantity. Estate wall intervals are API-derived proxies. +* `Calibrated`: requires reproducible calibration against relevant baselines. + *No per-file table row currently earns this category.* +* `Estimated`: both recognized-pattern tables and naive AST heuristics; estate + energy, carbon and imputed money also rely on assumptions. +* `Unknown`: no adequate basis supplied. + +`resource_range` retains heuristic min/typical/max values. They are not validated +bounds, quantiles or confidence intervals. A named pattern and a wide range do +not constitute calibration. Even the sign of a real-world change remains unverified. + +== Pareto verdicts + +* *Improvement/regression*: a point vector is no worse on every axis and improves + by more than tolerance on at least one (or the reverse). +* *TradeOff*: opposing movements; inspect the actual dimensions, not just a score. +* *Neutral*: no modeled movement beyond tolerance, not physical equivalence. +* *Indeterminate*: invalid/missing/non-finite comparison, not a neutral pass. + +The CLI aggregates per-function resource proxies and mean quality. These are +not workload-equivalent measurements; adding/removing/splitting functions can +change the comparison. “Optimal” only means nondominated among the supplied +points under this model. It is not global, fair or necessarily feasible. + +A `Pareto-Trade-off:` trailer or `Pareto Trade-off` heading records that text is +present. It does not verify substance, approval, consent or an exception to hard +limits. See link:research/EVIDENCE-SCOPED-PARETO.adoc[the separate interval contract] +for a design that retains scope, units, uncertainty, and non-compensatory constraints. +That prototype does not authorize the CLI. + +== Finding taxonomy + +Pattern findings carry `taxonomy.intent`, `.maintenance`, `.locus` through JSON +and SARIF. The canonical vocabulary is NEUROSYM: `must|intend|wish`, +`corrective|adaptive|perfective|preventive`, `systems|compliance|externalities`. +Current heuristic patterns are `wish/perfective/externalities`. General telemetry +and unclassified synthetic/legacy findings have no taxonomy; do not invent one. + +Resource confidence is *not* a probability of correctness. It cannot be mapped +to 0.95 and then used as permission for automatic action. The legacy numeric +Eclexia taxonomy example is not an active runtime rule; auto-action is disabled. + +== SARIF + +`report --format sarif` emits SARIF 2.1.0 with rule IDs, source locations, messages +and custom properties for scores, resources, confidence, taxonomy, optional bands +and Pareto metadata. Suggestions are text, never invented replacement edits. +`oikosbot/general` is measurement telemetry and is filtered out of alerts; JSON +retains it. Clean analyzed code can therefore yield zero SARIF results without +having zero analyzed units. An empty scan is instead an error. + +Upload only when a report file was successfully produced. Full schema and GitHub +consumer validation remain release gates; Rust structural fixtures are not full +SARIF schema validation. Custom properties are for consumers that understand +these limitations, not standardized measured-carbon assertions. + +== Configuration [source,yaml] ---- -mode: regulator # consultant | advisor (default) | regulator +mode: advisor thresholds: eco_minimum: - carbon: 50 # the eco-score floor, 0-100 (energy: is the fallback) - enforcement: blocking # fail the run when below the floor -exclude: # globs, matched against paths relative to the root + carbon: 50 + enforcement: warning +exclude: - "**/target/**" - - "**/node_modules/**" analysis: languages: [rust, javascript, python] ---- -* `mode: regulator` (or `enforcement: blocking`) is what makes a below-floor - finding fail the run. Without either, `check` reports and exits 0, naming the - config that made it advisory. -* `analysis.languages` is intersected with what the tree-sitter analyser can - actually parse (`rust`, `javascript`, `python`). Naming only unsupported - languages is warned about loudly and falls back to the default set, because - analysing nothing would be a silent no-scan. -* Other keys in `config/oikos.yaml` (`eco_standard`, `eco_excellence`, - `complexity`, and the datastore block) are parsed and **discarded** by the - current loader. They are documented for the target design and take no effect; - the file says so inline. - -Policy files under `policies/` (`*.ecl`, Eclexia) are a separate mechanism — -see link:../policies/README.adoc[policies/README]. They are evaluated by -`oikosbot-eclexia` when `--policy-dir` is passed. - -== `BOT_MODE` (the App) vs `mode` (the CLI) - -`BOT_MODE` configures the AffineScript webhook receiver in -`bot-integration-affine/` — it is read in `src/Config.affine`, lower-cased, and -anything unrecognised becomes `advisor`. - -[cols="1,3"] -|=== -| Value | Behaviour (from `src/Types.affine` and `src/Report.affine`) - -| `consultant` -| Answers questions and offers alternatives. PR comments open with - "Oikos consultant — analysis". +`--config` wins over auto-discovery of `.oikos.yml` / `.oikos.yaml` at the directory +root. Excludes match relative paths. Unsupported configured languages, malformed +YAML, unknown modes/enforcement and invalid globs are errors; they no longer +silently broaden a scan. Directory file symlinks are not followed. Fully excluded +or function-free input returns no-check rather than false success. -| `advisor` (default) -| Proactive suggestions on pull requests. Comments open with - "Oikos advisor — suggestions". +`eco_minimum.carbon` (or `.energy` fallback) is historically an *Eco-score floor*, +not a physical carbon/energy budget. Do not confuse it with joules/grams. Other +sections of `config/oikos.yaml`, including custom weights and physical budgets, +are target configuration and may be ignored by the current CLI. See +link:policies.adoc[policy implementation boundaries]. -| `regulator` -| Enforces policy compliance. Comments open with - "Oikos regulator — policy review". -|=== +`consultant` and `advisor` are advisory config modes. `regulator` requests a +blocking check but does not manufacture evidence. `report` stays advisory in +all modes; `compare --check` is an explicit request independent of these modes. +`BOT_MODE` belongs to the AffineScript App scaffold, not a second CLI mode switch. -The CLI's `.oikos.yml` `mode:` key is the same three values, but it only -decides enforcement (regulator ⇒ blocking); it does not change the report -wording. `GET /health` on the receiver reports the active mode, which is the -quickest way to confirm what a deployment thinks it is. +== Composite Action -[NOTE] -==== -The GitHub App receiver is a scaffold: the webhook handler and HMAC -verification exist, the HTTP listener is gated on upstream AffineScript stdlib -work, and comments are not yet posted from a live deployment. Until then, CI is -the supported path. -==== +The inputs are declared in link:../action.yml[action.yml]. Pin the Action revision +and the image digest after artifact review. Default mode is advisory `report`, +default format/output are SARIF/`results.sarif`. `compare` requires a base checkout; +use `format: json` and a `.json` output path. The wrapper's legacy SARIF default +for compare is mapped to JSON; it does not provide comparison SARIF. -== Troubleshooting +[source,yaml] +---- +- uses: hyperpolymath/oikosbot@REVIEWED_COMMIT_SHA + with: + mode: report + path: . + format: sarif + output: results.sarif +---- -[cols="2,3",options="header"] -|=== -| Symptom | Cause and fix - -| `unknown mode '' (report | check | compare)` -| The action's `mode` input is misspelled. The container exits 1 immediately. - -| `compare mode requires the 'base' input` -| `mode: compare` without `base`. Check out the target branch into a directory - and pass it. - -| `no analyzable files under and/or ` -| The directories contain no file with a supported extension, or everything - was excluded. Check `exclude` globs and `analysis.languages`. - -| `Unsupported file extension: ` -| A file was analysed directly (not via a directory) and its extension is not - `rs`/`js`/`py`. - -| Run reports but never fails -| Expected in `advisor`/`consultant` mode. Set `mode: regulator` or - `enforcement: blocking` to make below-floor findings fail. - -| `--check` warns "NOT enforced" and exits 0 -| The verdict is real but its drivers are heuristic (`Estimated`), and only - measured/calibrated inputs may block. This is the gate refusing to fake a - decision, not a bug. - -| Every function is an alert in code scanning -| It should not be: `oikosbot/general` records are filtered out of SARIF. If - you see per-function alerts, the caller is uploading `format: json` output as - SARIF. - -| Eco scores look uniformly high -| The eco score is a log scale anchored at 1 J; calibrated estimates are - microjoule-scale, so most units clamp at 100. The eco *threshold* therefore - discriminates far less than the Pareto verdicts, which compare base against - head. This is a known, tracked limitation, not a silently passing gate. -|=== +This is a placeholder example, not an executable pinned workflow. Use a reviewed +published revision, not the literal placeholder. Authentication/package visibility +must be tested independently of local container execution. -[#honesty] -== What to trust +== Troubleshooting -* Direction and ranking (which unit is worse, which way a diff moves) are the - product. They are deterministic for a given input tree. -* Absolute joules, grams and milliseconds are static estimates. The calibrated - path is better anchored than the naive one, but neither has been validated - against profiling data on real hardware. -* `Confidence` tells you which of the two you are looking at, per finding. Read - it before acting on a number. +* *No check / exit 2*: inspect stderr; distinguish missing evidence from syntax, + paths, excludes, unsupported languages and tool failures. Use advisory mode + when advice is what you intended—not an unconditional pass wrapper. +* *GHCR denied*: verify digest availability and anonymous access; no credential + should be embedded in config or logs. Do not silently fall back to `latest`. +* *No App comment*: the App is not production-ready. Check listener, payload, + HMAC and transport evidence before assuming the CLI container is a webhook server. +* *Estate partial failure*: retry collection; successful staging is retained. + Check repository inventory and missing per-repo files before analysis. A capped + run sample is not a complete history; zero duration can still mean missing time. +* *Surprising score*: inspect raw axes and assumptions, especially AST size and + quality normalization. Measure equivalent work rather than optimizing the score. + +Deployment and containment: link:../DEPLOY.adoc[DEPLOY]. No production App rollout +is recommended until link:LAUNCH-READINESS.adoc[the explicit operational gates] pass. + +`--security` currently returns unavailable even with the retained security feature: +the upstream scanner is not integrated. Missing/empty policy directories and +policy evaluation failures also return no-check; they are not clean scans. diff --git a/policies/README.adoc b/policies/README.adoc index 43e76cb..26f6f39 100644 --- a/policies/README.adoc +++ b/policies/README.adoc @@ -30,9 +30,9 @@ link:../.machine_readable/descriptiles/NEUROSYM.a2ml[`NEUROSYM.a2ml` `[finding-t | `intent` | `must` / `intend` / `wish` -| Deontic force, *derived from confidence*. Identical to the gitbot-fleet - Safety Triangle: `must` ≡ eliminate (≥ 0.95, auto-fix), `intend` ≡ substitute - (≥ 0.85, review), `wish` ≡ control (< 0.85, human). **This axis is executable.** +| Intended deontic vocabulary, not automatic authorization. Current Rust heuristic + patterns emit `wish`. The legacy numeric example uses probability thresholds, + but resource confidence categories are not probabilities; no conversion is earned. | `maintenance` | `corrective` / `adaptive` / `perfective` / `preventive` @@ -45,7 +45,7 @@ link:../.machine_readable/descriptiles/NEUROSYM.a2ml[`NEUROSYM.a2ml` `[finding-t above. |=== -Because only `intent` is numerically derivable, only it is executable in -Eclexia; `maintenance` and `locus` are categorical tags set when a rule is -authored. A natural next step is to carry all three tags on the Rust `Finding` -types (`crates/oikosbot-metrics`) so emitted SARIF/reports are tagged end to end. +The Rust pattern findings now carry all three typed tags through JSON and SARIF. +Unclassified synthetic/legacy findings retain no taxonomy. The numeric Eclexia +example is not an active Rust policy, and automatic action is disabled without +policy authority and validated evidence. See link:../docs/policies.adoc[policy boundaries]. diff --git a/policies/finding_taxonomy.ecl b/policies/finding_taxonomy.ecl index fc78435..3852427 100644 --- a/policies/finding_taxonomy.ecl +++ b/policies/finding_taxonomy.ecl @@ -16,10 +16,10 @@ // `maintenance` and `locus` are categorical tags attached when a rule is // authored, so they are not computed here. // -// Written in Eclexia (dogfooding): classifying a finding is provably cheaper -// than producing it. +// Example Eclexia policy, not executed by the builtin Rust adapter. +// No cost proof is supplied for the declared resource annotations. -// intent = MUST <=> Safety-Triangle "eliminate" <=> auto-fix, no review. +// Legacy numeric intent classification only; not authorization to auto-fix. def intent_is_must(confidence: Float) -> Bool @requires: energy < 0.01J, carbon < 0.001gCO2e @optimize: minimize energy @@ -41,9 +41,10 @@ def intent_is_wish(confidence: Float) -> Bool confidence < 0.85 } -// A finding may be auto-actioned iff its intent is MUST. +// Confidence alone never authorizes mutation. This sample has no authority +// adapter, so automatic action is disabled for every input. def is_auto_actionable(confidence: Float) -> Bool @requires: energy < 0.01J, latency < 1ms { - intent_is_must(confidence) + false } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..c402fa4 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +[toolchain] +channel = "1.90.0" +profile = "minimal" +components = ["rustfmt", "clippy"] diff --git a/tests/test-action-arguments.sh b/tests/test-action-arguments.sh new file mode 100755 index 0000000..dd9c1f7 --- /dev/null +++ b/tests/test-action-arguments.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +# Exercise the actual action shell block with a recorder, NOT an image/E2E test. +set -euo pipefail +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +# The action has one final, eight-space-indented run block. Refuse drift. +[[ $(grep -c '^ run: |$' action.yml) == 1 ]] || exit 2 +sed -n '/^ run: |$/,$p' action.yml | tail -n +2 | sed 's/^ //' > "$work/action.sh" +bash -n "$work/action.sh" +mkdir "$work/bin" +cat > "$work/bin/docker" <<'SH' +#!/usr/bin/env bash +printf '%s\n' "$@" > "$RECORDED_ARGS" +SH +chmod +x "$work/bin/docker" +export PATH="$work/bin:$PATH" RECORDED_ARGS="$work/args" +export GITHUB_WORKSPACE="$work/workspace" MODE=report TARGET='source with spaces' BASE='' +export FORMAT=sarif OUTFILE=results.sarif CONFIG='' THRESHOLD='' PR_BODY='' DO_CHECK=false IMAGE=fixture-image +bash "$work/action.sh" +grep -Fx 'source with spaces' "$work/args" >/dev/null +MODE=compare BASE=base FORMAT=sarif bash "$work/action.sh" +grep -Fx json "$work/args" >/dev/null +grep -Fx -- --output "$work/args" >/dev/null +expect_rejected() { + rm -f "$work/args" + local status=0 + env "$@" bash "$work/action.sh" > "$work/rejection" 2>&1 || status=$? + [[ $status == 2 && ! -e "$work/args" ]] || { cat "$work/rejection"; exit 1; } +} +expect_rejected MODE=invalid +expect_rejected MODE=compare BASE='' +expect_rejected MODE=compare BASE=base THRESHOLD=50 +expect_rejected DO_CHECK=typo +printf 'action argument controls passed (no container was executed)\n' diff --git a/tests/test-container-contract.sh b/tests/test-container-contract.sh new file mode 100755 index 0000000..6e86860 --- /dev/null +++ b/tests/test-container-contract.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +# Black-box smoke contract. Not SARIF schema validation or provenance verification. +# 0 = contract passed, 1 = wrong observed outcome, 2 = no check/tool failure. +set -Eeuo pipefail +trap 'echo "no check performed: container contract infrastructure failed" >&2; exit 2' ERR +for tool in docker jq; do + command -v "$tool" >/dev/null || { echo "missing $tool" >&2; exit 2; } +done +image=${1:?usage: test-container-contract.sh IMAGE} +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/clean" "$work/firing" +printf 'fn clean() -> u32 { 1 }\n' > "$work/clean/source.rs" +cat > "$work/firing/source.rs" <<'RS' +fn work() { + for i in 0..8 { for j in 0..8 { for k in 0..8 { let _ = i + j + k; } } } +} +RS +fail() { echo "wrong outcome: $*" >&2; exit 1; } +version=$(docker run --rm --network none "$image" --version) +[[ $version == 'oikosbot '* ]] || fail "missing version" +uid=$(docker run --rm --network none --entrypoint /usr/bin/id "$image" -u) +[[ $uid =~ ^[0-9]+$ && $uid != 0 ]] || fail "default user is root or unknown" +for fixture in clean firing; do + docker run --rm --network none --read-only --cap-drop ALL \ + --security-opt no-new-privileges --user "$(id -u):$(id -g)" \ + -v "$work:/work" -w /work "$image" report "/work/$fixture" \ + --format sarif --output "/work/$fixture.sarif" +done +jq -e '.version == "2.1.0" and (.runs[0].results | length == 0)' "$work/clean.sarif" >/dev/null || fail "clean fixture alerted" +jq -e '.runs[0].results | any(.ruleId == "oikosbot/nested-loops")' "$work/firing.sarif" >/dev/null || fail "known rule did not fire" +echo "container smoke contract passed (not a release certification)" diff --git a/tools/ci/release-preflight.sh b/tools/ci/release-preflight.sh new file mode 100755 index 0000000..8a88089 --- /dev/null +++ b/tools/ci/release-preflight.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +# Local evidence collection, not permission to release. See docs/LAUNCH-READINESS.adoc. +set -Eeuo pipefail +for tool in cargo rustc docker jq; do + command -v "$tool" >/dev/null || { echo "no check performed: missing $tool" >&2; exit 2; } +done +cargo fmt --check +cargo test --workspace --locked +cargo test --workspace --all-features --locked +cargo clippy --workspace --locked +bash tests/test-eclexia-adapter.sh +docker build -f Containerfile -t oikosbot:preflight . +bash tests/test-container-contract.sh oikosbot:preflight +echo 'Local evidence collected. Release remains subject to the launch-readiness review.' From ea3156526f6d366418c634d9378709235aca82fd Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:48:16 +0000 Subject: [PATCH 2/2] docs: record PR verification blocker and next-agent baton Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .machine_readable/descriptiles/STATE.a2ml | 12 +++++++ docs/HANDOFF.adoc | 40 +++++++++++++++++++++++ docs/LAUNCH-READINESS.adoc | 9 +++++ 3 files changed, 61 insertions(+) diff --git a/.machine_readable/descriptiles/STATE.a2ml b/.machine_readable/descriptiles/STATE.a2ml index bd8a80d..9d71ba6 100644 --- a/.machine_readable/descriptiles/STATE.a2ml +++ b/.machine_readable/descriptiles/STATE.a2ml @@ -96,3 +96,15 @@ verification-limit = "Pinned toolchain download unavailable; older tools recover release-performed = false metadata-check = "Six core TOML-shaped descriptiles and rust-toolchain.toml parse with Python 3.11 tomllib; not a full A2ML validator. Local links in the new/replaced entry guides resolve." publication-channel = "Main-branch candidate publication uses development, not automatic latest promotion; existing consumer digests unchanged. Semver publication still requires owner review and explicit tagging." + +[pr-handoff-2026-09-26] +pr = "https://github.com/hyperpolymath/oikosbot/pull/110" +status = "open-not-merged; CI startup blocked by actor policy" +initial-head = "6103004cd498a12954d4f34675157f43ac2b6ce6" +ci-run = "https://github.com/hyperpolymath/oikosbot/actions/runs/36267202433" +ci-evidence = "startup_failure; zero jobs; annotation: Actor is not allowed to trigger Actions workflows. Workflow file: '.github/workflows/ci.yml'." +other-blocked-workflows = ["Governance", "Hypatia Security Scan", "Secret Scanner", "Language Policy Enforcement", "Workflow Security Linter"] +review-limit = "Initial CodeRabbit SUCCESS was a skipped review (bot user); external checks do not substitute for workspace/container tests." +owner-action = "Authorized human retrigger or Actions actor-policy resolution; verify jobs actually ran. No credential sharing, admin bypass, auto-merge or release." +handoff = "docs/HANDOFF.adoc" +new-tests = "CI and release-preflight now exercise both default and all-feature Cargo tests; the latter alone omits feature-disabled paths." diff --git a/docs/HANDOFF.adoc b/docs/HANDOFF.adoc index 394d7db..5d31a59 100644 --- a/docs/HANDOFF.adoc +++ b/docs/HANDOFF.adoc @@ -3,6 +3,46 @@ = Next-agent baton and owner decisions :toc: +== Current integration status — 2026-09-26 + +*PR: https://github.com/hyperpolymath/oikosbot/pull/110[#110]. Not merged.* +The owner requested creation and merge, but required verification could not run. +Initial head `6103004cd498a12954d4f34675157f43ac2b6ce6` was pushed successfully. +The CI run +https://github.com/hyperpolymath/oikosbot/actions/runs/36267202433[36267202433] +ended in `startup_failure`, with zero jobs and this annotation: + +[quote] +____ +Actor is not allowed to trigger Actions workflows. Workflow file: '.github/workflows/ci.yml'. +____ + +Governance, Hypatia, Secret Scanner, Language Policy and Workflow Security Linter +also failed to start. This is an actor-policy restriction on the Arena app, not a +Rust/compiler/test failure and not grounds to remove or weaken CI. Repository +Actions-settings and branch-protection reads returned integration-scope HTTP 403; +ordinary git/PR access works. Do not request credentials or impersonate another actor. + +*Do not rely only on `gh pr checks`*: a workflow rejected before creating jobs may +be absent from that rollup. Inspect workflow runs for the exact head SHA too. +CodeRabbit initially reported SUCCESS while its comment said “Review skipped: +Bot user detected.” That is no review receipt. External scan success does not +replace Rust/Haskell/container execution or substantive review. + +The owner needs an authorized human-triggered CI event or an appropriate +Actions actor-policy permission. One normal retrigger route is closing and +reopening this PR from the owner's own GitHub account (the workflows already +subscribe to `pull_request` reopening). Whether that actor is permitted must +then be verified from the new run; do not promise that a retrigger fixes policy. +If policy continues to reject it, inspect repository/account Actions restrictions. +Do not change organization policy from an agent, force-merge or enable auto-merge +while these checks are absent. No release or image-digest promotion occurred. + +The next agent should verify this live status, address real CI failures once +checks can start, record the evidence, then merge #110 only when warranted. +Respect the new session's assigned-branch constraints; do not assume permission +to switch branches or push to this PR's head from a differently bound session. + == Start here Continue the OikosBot launch-hardening work, not a launch. Read, in order: diff --git a/docs/LAUNCH-READINESS.adoc b/docs/LAUNCH-READINESS.adoc index c5c05da..34467bd 100644 --- a/docs/LAUNCH-READINESS.adoc +++ b/docs/LAUNCH-READINESS.adoc @@ -181,3 +181,12 @@ Main pushes publish candidates under `development` instead of automatically advancing `latest`. Existing digests are unchanged. This limits accidental stable promotion; it does not implement GitHub environment approval or replace an owner's release review. Semver tags must not be created until the outstanding gates pass. + +== PR verification attempt — 2026-09-26 + +https://github.com/hyperpolymath/oikosbot/pull/110[PR #110] is open, not merged. +The first GitHub CI attempt +https://github.com/hyperpolymath/oikosbot/actions/runs/36267202433[36267202433] +was rejected before jobs existed: *“Actor is not allowed to trigger Actions +workflows.”* No Rust, Haskell or container test receipt was obtained. See +link:HANDOFF.adoc[the handoff] for the owner action and safe continuation steps.