From 48b7ce2663a825124adb27b4cbd2b17c25394696 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 01:56:46 +0000 Subject: [PATCH] chore(bots): empty the misfiled sustainabot slot; add anti-vendoring guardrails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A full standalone copy of OikosBot (its own Rust workspace, Haskell analyzer, AffineScript bot, containers and policies) had been vendored into bots/sustainabot/ — one of which even grew a hard path dependency back into shared-context/. That implementation has been extracted to its own repo, hyperpolymath/oikosbot, and renamed oikosbot-*. This commit resets the slot and guards against recurrence: - Remove the 99 vendored files from bots/sustainabot/; leave a placeholder README.adoc that reserves the slot and points to hyperpolymath/oikosbot. - Add bots/README.adoc: bot directories are THIN ADAPTERS, never homes for standalone products, and bot crates must not add repo-escaping path deps. - CLAUDE.md: new critical invariant #6 (thin adapters; sustainabot is NOT OikosBot and NOT the oikos DSL). - README/ROADMAP/docs: mark sustainabot's slot "Reserved" and disambiguate. The fleet KEEPS sustainabot as a member: BotId::Sustainabot, the coordinator roster, and safety-triangle routing in shared-context/ are untouched (shared-context still builds green). Only the vendored directory was emptied. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RozeeLxpJsd3WWFngaZWz3 --- .claude/CLAUDE.md | 11 + README.adoc | 6 +- ROADMAP.adoc | 2 +- bots/README.adoc | 30 + bots/sustainabot/ARCHITECTURE.md | 246 --- bots/sustainabot/Cargo.lock | 1389 ----------------- bots/sustainabot/Cargo.toml | 53 - bots/sustainabot/DEPLOY.md | 368 ----- bots/sustainabot/Justfile | 39 - bots/sustainabot/LICENSE | 153 -- bots/sustainabot/MAINTAINERS.md | 14 - bots/sustainabot/Mustfile | 14 - bots/sustainabot/PALIMPSEST.adoc | 92 -- bots/sustainabot/QUICKSTART.md | 75 - bots/sustainabot/README.adoc | 165 +- bots/sustainabot/README.hybrid.md | 223 --- bots/sustainabot/ROADMAP.adoc | 22 - .../analyzers/code-haskell/app/Main.hs | 191 --- .../analyzers/code-haskell/cabal.project | 10 - .../code-haskell/oikos-analyzer.cabal | 75 - .../code-haskell/src/Eco/Analysis.hs | 357 ----- .../analyzers/code-haskell/src/Eco/Carbon.hs | 110 -- .../analyzers/code-haskell/src/Eco/Energy.hs | 138 -- .../analyzers/code-haskell/src/Eco/Pareto.hs | 150 -- .../code-haskell/src/Eco/Resource.hs | 170 -- .../code-haskell/src/Quality/Complexity.hs | 144 -- .../code-haskell/src/Quality/Coupling.hs | 148 -- .../code-haskell/src/Quality/Coverage.hs | 166 -- .../code-haskell/src/Quality/Debt.hs | 167 -- .../code-haskell/src/Types/Metrics.hs | 201 --- .../code-haskell/src/Types/Report.hs | 191 --- .../analyzers/code-haskell/test/Main.hs | 90 -- bots/sustainabot/bot-integration/.env.test | 3 - .../bot-integration/.tool-versions | 1 - .../bot-integration/MIGRATION-NOTES.md | 81 - .../bot-integration/MISSING-EXTERNS.md | 138 -- bots/sustainabot/bot-integration/deno.json | 33 - .../bot-integration/scripts/smee-client.js | 101 -- .../bot-integration/src/Analysis.affine | 131 -- .../bot-integration/src/Config.affine | 87 -- .../bot-integration/src/GitHubAPI.affine | 153 -- .../bot-integration/src/GitHubApp.affine | 191 --- .../bot-integration/src/Main.affine | 252 --- .../bot-integration/src/Oikos.affine | 244 --- .../bot-integration/src/Report.affine | 180 --- .../bot-integration/src/Router.affine | 142 -- .../bot-integration/src/Types.affine | 141 -- .../bot-integration/src/Webhook.affine | 150 -- .../bot-integration/src/tea/Cmd.affine | 52 - .../bot-integration/src/tea/Runtime.affine | 92 -- .../bot-integration/src/tea/Sub.affine | 33 - bots/sustainabot/config/oikos.yaml | 183 --- bots/sustainabot/containers/Containerfile | 99 -- .../containers/Containerfile.policy | 80 - bots/sustainabot/containers/compose.yaml | 188 --- bots/sustainabot/containers/prometheus.yml | 46 - bots/sustainabot/containers/vordr-build.sh | 113 -- .../crates/sustainabot-analysis/Cargo.toml | 28 - .../sustainabot-analysis/src/analyzer.rs | 256 --- .../sustainabot-analysis/src/calibration.rs | 201 --- .../crates/sustainabot-analysis/src/carbon.rs | 40 - .../sustainabot-analysis/src/dependencies.rs | 251 --- .../sustainabot-analysis/src/directives.rs | 255 --- .../sustainabot-analysis/src/language.rs | 54 - .../crates/sustainabot-analysis/src/lib.rs | 37 - .../sustainabot-analysis/src/migration.rs | 373 ----- .../sustainabot-analysis/src/patterns.rs | 327 ---- .../sustainabot-analysis/src/security.rs | 169 -- .../crates/sustainabot-cli/Cargo.toml | 31 - .../crates/sustainabot-cli/src/main.rs | 498 ------ .../crates/sustainabot-eclexia/Cargo.toml | 32 - .../crates/sustainabot-eclexia/src/lib.rs | 529 ------- .../crates/sustainabot-fleet/Cargo.toml | 16 - .../crates/sustainabot-fleet/src/lib.rs | 449 ------ .../crates/sustainabot-metrics/Cargo.toml | 15 - .../crates/sustainabot-metrics/src/lib.rs | 329 ---- .../crates/sustainabot-sarif/Cargo.toml | 16 - .../crates/sustainabot-sarif/src/lib.rs | 464 ------ bots/sustainabot/databases/arangodb/schema.js | 360 ----- .../databases/virtuoso/ontology.ttl | 341 ---- .../databases/virtuoso/queries.sparql | 265 ---- bots/sustainabot/docs/GITHUB_APP_SETUP.md | 159 -- bots/sustainabot/examples/sustainabot-ci.yml | 54 - bots/sustainabot/fuzz/Cargo.toml | 20 - .../fuzz/fuzz_targets/fuzz_main.rs | 22 - bots/sustainabot/guix/channels.scm | 42 - bots/sustainabot/guix/manifest.scm | 92 -- bots/sustainabot/guix/oikos.scm | 149 -- bots/sustainabot/hooks/validate-codeql.sh | 34 - .../sustainabot/hooks/validate-permissions.sh | 14 - bots/sustainabot/hooks/validate-sha-pins.sh | 33 - bots/sustainabot/hooks/validate-spdx.sh | 25 - bots/sustainabot/justfile | 39 - bots/sustainabot/policies/carbon_budget.ecl | 23 - .../sustainabot/policies/energy_threshold.ecl | 43 - .../policies/memory_efficiency.ecl | 22 - .../policies/security_sustainability.ecl | 24 - .../policy-engine/datalog/eco_rules.dl | 233 --- .../policy-engine/deepproblog/eco_problog.pl | 200 --- .../prompts/claude-code-instructions.md | 179 --- .../prompts/copilot-instructions.md | 167 -- bots/sustainabot/prompts/pr-template.md | 56 - bots/sustainabot/test_sample.rs | 17 - docs/ARCHITECTURE.md | 2 +- 104 files changed, 75 insertions(+), 15034 deletions(-) create mode 100644 bots/README.adoc delete mode 100644 bots/sustainabot/ARCHITECTURE.md delete mode 100644 bots/sustainabot/Cargo.lock delete mode 100644 bots/sustainabot/Cargo.toml delete mode 100644 bots/sustainabot/DEPLOY.md delete mode 100644 bots/sustainabot/Justfile delete mode 100644 bots/sustainabot/LICENSE delete mode 100644 bots/sustainabot/MAINTAINERS.md delete mode 100644 bots/sustainabot/Mustfile delete mode 100644 bots/sustainabot/PALIMPSEST.adoc delete mode 100644 bots/sustainabot/QUICKSTART.md delete mode 100644 bots/sustainabot/README.hybrid.md delete mode 100644 bots/sustainabot/ROADMAP.adoc delete mode 100644 bots/sustainabot/analyzers/code-haskell/app/Main.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/cabal.project delete mode 100644 bots/sustainabot/analyzers/code-haskell/oikos-analyzer.cabal delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Eco/Analysis.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Eco/Carbon.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Eco/Energy.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Eco/Pareto.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Eco/Resource.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Quality/Complexity.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Quality/Coupling.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Quality/Coverage.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Quality/Debt.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Types/Metrics.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/src/Types/Report.hs delete mode 100644 bots/sustainabot/analyzers/code-haskell/test/Main.hs delete mode 100644 bots/sustainabot/bot-integration/.env.test delete mode 100644 bots/sustainabot/bot-integration/.tool-versions delete mode 100644 bots/sustainabot/bot-integration/MIGRATION-NOTES.md delete mode 100644 bots/sustainabot/bot-integration/MISSING-EXTERNS.md delete mode 100644 bots/sustainabot/bot-integration/deno.json delete mode 100644 bots/sustainabot/bot-integration/scripts/smee-client.js delete mode 100644 bots/sustainabot/bot-integration/src/Analysis.affine delete mode 100644 bots/sustainabot/bot-integration/src/Config.affine delete mode 100644 bots/sustainabot/bot-integration/src/GitHubAPI.affine delete mode 100644 bots/sustainabot/bot-integration/src/GitHubApp.affine delete mode 100644 bots/sustainabot/bot-integration/src/Main.affine delete mode 100644 bots/sustainabot/bot-integration/src/Oikos.affine delete mode 100644 bots/sustainabot/bot-integration/src/Report.affine delete mode 100644 bots/sustainabot/bot-integration/src/Router.affine delete mode 100644 bots/sustainabot/bot-integration/src/Types.affine delete mode 100644 bots/sustainabot/bot-integration/src/Webhook.affine delete mode 100644 bots/sustainabot/bot-integration/src/tea/Cmd.affine delete mode 100644 bots/sustainabot/bot-integration/src/tea/Runtime.affine delete mode 100644 bots/sustainabot/bot-integration/src/tea/Sub.affine delete mode 100644 bots/sustainabot/config/oikos.yaml delete mode 100644 bots/sustainabot/containers/Containerfile delete mode 100644 bots/sustainabot/containers/Containerfile.policy delete mode 100644 bots/sustainabot/containers/compose.yaml delete mode 100644 bots/sustainabot/containers/prometheus.yml delete mode 100755 bots/sustainabot/containers/vordr-build.sh delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/Cargo.toml delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/src/analyzer.rs delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/src/calibration.rs delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/src/carbon.rs delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/src/dependencies.rs delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/src/directives.rs delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/src/language.rs delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/src/lib.rs delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/src/migration.rs delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/src/patterns.rs delete mode 100644 bots/sustainabot/crates/sustainabot-analysis/src/security.rs delete mode 100644 bots/sustainabot/crates/sustainabot-cli/Cargo.toml delete mode 100644 bots/sustainabot/crates/sustainabot-cli/src/main.rs delete mode 100644 bots/sustainabot/crates/sustainabot-eclexia/Cargo.toml delete mode 100644 bots/sustainabot/crates/sustainabot-eclexia/src/lib.rs delete mode 100644 bots/sustainabot/crates/sustainabot-fleet/Cargo.toml delete mode 100644 bots/sustainabot/crates/sustainabot-fleet/src/lib.rs delete mode 100644 bots/sustainabot/crates/sustainabot-metrics/Cargo.toml delete mode 100644 bots/sustainabot/crates/sustainabot-metrics/src/lib.rs delete mode 100644 bots/sustainabot/crates/sustainabot-sarif/Cargo.toml delete mode 100644 bots/sustainabot/crates/sustainabot-sarif/src/lib.rs delete mode 100644 bots/sustainabot/databases/arangodb/schema.js delete mode 100644 bots/sustainabot/databases/virtuoso/ontology.ttl delete mode 100644 bots/sustainabot/databases/virtuoso/queries.sparql delete mode 100644 bots/sustainabot/docs/GITHUB_APP_SETUP.md delete mode 100644 bots/sustainabot/examples/sustainabot-ci.yml delete mode 100644 bots/sustainabot/fuzz/Cargo.toml delete mode 100644 bots/sustainabot/fuzz/fuzz_targets/fuzz_main.rs delete mode 100644 bots/sustainabot/guix/channels.scm delete mode 100644 bots/sustainabot/guix/manifest.scm delete mode 100644 bots/sustainabot/guix/oikos.scm delete mode 100755 bots/sustainabot/hooks/validate-codeql.sh delete mode 100755 bots/sustainabot/hooks/validate-permissions.sh delete mode 100755 bots/sustainabot/hooks/validate-sha-pins.sh delete mode 100755 bots/sustainabot/hooks/validate-spdx.sh delete mode 100644 bots/sustainabot/justfile delete mode 100644 bots/sustainabot/policies/carbon_budget.ecl delete mode 100644 bots/sustainabot/policies/energy_threshold.ecl delete mode 100644 bots/sustainabot/policies/memory_efficiency.ecl delete mode 100644 bots/sustainabot/policies/security_sustainability.ecl delete mode 100644 bots/sustainabot/policy-engine/datalog/eco_rules.dl delete mode 100644 bots/sustainabot/policy-engine/deepproblog/eco_problog.pl delete mode 100644 bots/sustainabot/prompts/claude-code-instructions.md delete mode 100644 bots/sustainabot/prompts/copilot-instructions.md delete mode 100644 bots/sustainabot/prompts/pr-template.md delete mode 100644 bots/sustainabot/test_sample.rs diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 45963540..07fa1654 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -81,6 +81,17 @@ Control (report < 0.85) → Human review required 3. No hardcoded secrets — use env vars with `${VAR:-}` defaults. 4. Fix scripts must be idempotent (safe to run multiple times). 5. Confidence thresholds gate all automated actions. +6. **`bots//` directories are THIN ADAPTERS, not homes for standalone + products.** Never vendor an entire external project (its own Cargo + workspace, analyzers, containers, deployment, docs) into a bot slot, and + never let a bot crate add a `path` dependency that escapes the repo. If a + capability deserves its own project, build it in its own repository and + depend on it externally. See [`bots/README.adoc`](../bots/README.adoc). + - **Disambiguation:** `sustainabot` (this fleet's eco/econ slot, kept as + `BotId::Sustainabot`) is **not** `OikosBot` and **not** `oikos`. A misfiled + full copy of OikosBot once lived in `bots/sustainabot/`; it was extracted to + `hyperpolymath/oikosbot` and the slot reset to a placeholder. `oikos` is a + separate DSL (`hyperpolymath/oikos-economics-accounting-dsl`). ## Repo health diff --git a/README.adoc b/README.adoc index c2267f4b..7627f98d 100644 --- a/README.adoc +++ b/README.adoc @@ -67,8 +67,8 @@ The historical `TOPOLOGY.md` dashboard is at the repo root. |Active |**sustainabot** -|Ecological and economic code standards. Carbon intensity, resource efficiency, technical debt modeling. -|Active +|Ecological and economic code standards. *Adapter slot reserved — see link:bots/sustainabot/README.adoc[`bots/sustainabot/`].* The standalone eco/econ App once misfiled here is **OikosBot**, now at https://github.com/hyperpolymath/oikosbot[`hyperpolymath/oikosbot`] (a separate project — not sustainabot). +|Reserved |**glambot** |Presentation quality. Visual polish, accessibility (WCAG), SEO, machine-readability for AI/bots. @@ -95,7 +95,7 @@ The historical `TOPOLOGY.md` dashboard is at the repo root. |Active |=== -*Status legend:* `Active` = Tier 1 Verifier or Specialist bot running on every push. `Complete` = Tier 2 Finisher bot that runs after verifiers to validate final release readiness. +*Status legend:* `Active` = Tier 1 Verifier or Specialist bot running on every push. `Complete` = Tier 2 Finisher bot that runs after verifiers to validate final release readiness. `Reserved` = fleet member retained in `shared-context` but without an in-repo adapter yet (the directory is a placeholder; see that bot's `README.adoc`). The component-readiness assessment (CRG grades, evidence) lives in link:READINESS.md[READINESS.md]. diff --git a/ROADMAP.adoc b/ROADMAP.adoc index 00373077..cf33966c 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -24,7 +24,7 @@ based on confidence thresholds (Eliminate >= 0.95, Substitute >= 0.85, Control < | rhodibot | Git operations, RSR enforcement | Yes | echidnabot | Quality verification, multi-prover | Yes -| sustainabot | Dependency management | Yes +| sustainabot | Eco/econ standards — _reserved slot, empty_ (standalone impl extracted to `hyperpolymath/oikosbot`) | Yes | glambot | Presentation, badges, formatting | Yes | seambot | Integration seam maintenance | Yes | finishingbot | Completion of partial work | Yes diff --git a/bots/README.adoc b/bots/README.adoc new file mode 100644 index 00000000..3c059f43 --- /dev/null +++ b/bots/README.adoc @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += bots/ — fleet bot adapters +:icons: font + +Each subdirectory here is one member of the gitbot fleet. A bot directory is a +**thin adapter**: a small Rust crate (`Cargo.toml` + `src/` + `tests/`) that +wires one analysis capability into the fleet's `shared-context` interface and the +safety-triangle router. The canonical `BotId` set lives in +link:../shared-context/src/bot.rs[`shared-context/src/bot.rs`]. + +== Guardrail — do NOT vendor standalone products here + +A bot slot is an *adapter*, not a home for an entire external project. Do not +copy a whole standalone repository (its own workspace, analyzers, containers, +deployment, docs…) into `bots//`. + +[IMPORTANT] +==== +This rule exists because it was once broken: a full copy of **OikosBot** (a +separate eco/econ code-analysis App) was vendored into `bots/sustainabot/`, +including a Rust workspace that grew a hard `path` dependency back into +`shared-context/`. It has since been extracted to its own repo, +https://github.com/hyperpolymath/oikosbot[`hyperpolymath/oikosbot`], and the slot +reset to a placeholder. See `sustainabot/README.adoc`. +==== + +If a bot needs a heavyweight capability that deserves its own project, build that +project in its **own repository** and have the adapter here depend on it as an +external crate or service. Keep the adapter thin. diff --git a/bots/sustainabot/ARCHITECTURE.md b/bots/sustainabot/ARCHITECTURE.md deleted file mode 100644 index 9c66fb72..00000000 --- a/bots/sustainabot/ARCHITECTURE.md +++ /dev/null @@ -1,246 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2025 Jonathan D.A. Jewell - -# SustainaBot Architecture - -## Overview - -SustainaBot is a Rust workspace that statically analyzes code for ecological and economic sustainability. It uses tree-sitter for multi-language AST parsing and produces SARIF 2.1.0 output for IDE/CI integration. - -**Unique value**: the only tool correlating security findings with sustainability impact. - -## Crate Dependency Graph - -``` -sustainabot-cli - |-- sustainabot-analysis (AST parsing, patterns, calibration, deps, security) - |-- sustainabot-metrics (core types: AnalysisResult, ResourceProfile, HealthIndex) - |-- sustainabot-sarif (SARIF 2.1.0 output) - |-- sustainabot-eclexia (policy engine) - |-- sustainabot-fleet (gitbot-fleet integration) -``` - -All crates depend on `sustainabot-metrics` for shared types. - -## Crate Details - -### sustainabot-metrics - -Core data types shared across all crates. - -**Key types:** -- `AnalysisResult` — per-function analysis output (location, resources, health, rule_id, suggestion, confidence) -- `ResourceProfile` — energy (J), carbon (gCO2e), duration (ms), memory (bytes) -- `HealthIndex` — eco_score, econ_score, quality_score, overall (all 0-100) -- `CodeLocation` — file, line, column, end_line, end_column, function name -- `Confidence` — Measured | Calibrated | Estimated | Unknown -- Newtype wrappers: `EcoScore(f64)`, `EconScore(f64)`, `Joules(f64)`, `CarbonGrams(f64)`, etc. - -### sustainabot-analysis - -The analysis engine. Modules: - -- **analyzer.rs** — `Analyzer` struct with `analyze_file()` and `analyze_source()`. Uses tree-sitter to parse Rust, JavaScript, Python. Walks the AST to find function nodes, computes complexity metrics, estimates resources, detects patterns. - -- **patterns.rs** — 7 pattern detectors returning `PatternMatch` structs (name, description, suggestion, impact_multiplier): - 1. `nested-loops` — O(n^k) from nested iteration - 2. `busy-wait` — loops without sleep/await/yield - 3. `string-concat-in-loop` — string allocation per iteration - 4. `clone-in-loop` — unnecessary deep copies - 5. `unbuffered-io` — I/O without buffering - 6. `large-allocation` — single allocations >1MB - 7. `redundant-allocation` — `.to_string()` where borrow suffices - -- **calibration.rs** — Replaces naive `complexity * 0.1J` with pattern-based resource profiles. `ResourceRange` (min/typical/max) for 8 operation kinds. Confidence levels attached. - -- **dependencies.rs** — Parses `Cargo.toml` and `package.json`. Flags heavy deps (tokio, serde, react, webpack, etc.) and deps using all features. - -- **directives.rs** — Parses `.bot_directives/*.scm` S-expression files using `lexpr`. Returns `BotDirective` with allow/deny/scopes/thresholds. - -- **security.rs** — Behind `#[cfg(feature = "panic-attack")]`. Calls `panic_attack::xray::analyze()`, maps `WeakPointCategory` to energy waste multipliers, produces security-sustainability composite scores. Boosts severity 1.5x when eco and security findings overlap. - -- **language.rs** — `Language` enum (Rust, JavaScript, Python) with tree-sitter parser creation. - -### sustainabot-sarif - -SARIF 2.1.0 output. Converts `Vec` into a valid SARIF log with: -- Full `physicalLocation` (file, startLine, startColumn, endLine, endColumn) -- 11 builtin rule definitions (eco-threshold, carbon-intensity, nested-loops, etc.) -- `fixes` array with concrete suggestions -- Custom `properties` carrying eco/econ scores, resource profiles, confidence -- Severity mapping: eco_score < 30 → Error, < 60 → Warning, ≥ 60 → Note - -### sustainabot-eclexia - -Policy engine with two backends (feature-flagged): - -- **Default (binary)**: Shells out to `eclexia` binary, JSON I/O. Falls back to built-in Rust implementation of standard policies. -- **Native (eclexia-native feature)**: Direct `eclexia-interp` integration for type-safe evaluation. - -Built-in policies: energy threshold, carbon budget, memory efficiency. - -Returns `Vec` (Pass/Warn/Fail) which get converted to `AnalysisResult` for SARIF output. - -### sustainabot-fleet - -gitbot-fleet integration. Converts `AnalysisResult` → `gitbot_shared_context::Finding` using the full builder API. Directive-aware: reads `.bot_directives/sustainabot.scm` for scope control and custom thresholds. - -### sustainabot-cli - -CLI binary with 5 subcommands: -- `analyze ` — single file analysis -- `check ` — recursive directory check with eco threshold gating -- `report ` — same as check but defaults to SARIF output -- `fleet ` — run as gitbot-fleet member -- `self-analyze` — dogfooding mode - -Flags: `--format`, `--output`, `--eco-threshold`, `--security`, `--policy-dir`, `--verbose` - -## Analysis Pipeline - -``` -1. Input: file path or directory -2. Language detection (.rs → Rust, .js → JavaScript, .py → Python) -3. tree-sitter parsing → AST -4. Function node extraction (fn_item, function_declaration, function_definition) -5. Per-function analysis: - a. Complexity: cyclomatic (branches), nesting depth - b. Resource estimation: energy, carbon, duration, memory - c. Pattern detection: 7 anti-patterns with fix suggestions - d. Health scoring: eco, econ, quality → overall -6. Optional: security correlation (panic-attack) -7. Optional: policy evaluation (Eclexia) -8. Optional: dependency analysis -9. Output: SARIF / JSON / text -``` - -## Feature Flags - -| Flag | Crate | Effect | -|------|-------|--------| -| `security` | sustainabot-cli | Enables `--security` flag, pulls in panic-attack | -| `panic-attack` | sustainabot-analysis | Enables security.rs correlation engine | -| `eclexia-native` | sustainabot-eclexia | Uses eclexia-interp library instead of binary | - -## Ecosystem Integration - -``` -sustainabot ──────── SARIF ──────── GitHub Security Tab / IDE - | - |── panic-attack ────────────── Security-sustainability correlation - | - |── eclexia ─────────────────── Resource-aware policy evaluation - | - |── gitbot-fleet ────────────── Multi-repo orchestration - | | - | +── shared-context ──── Cross-bot findings - | - |── hypatia (green_web.ex) ──── Green Web hosting/CDN/registry checks - | | Routes via fleet_dispatcher.ex - | +── reportEcoScore ──── GraphQL mutation → eco_score findings - | - +── .bot_directives/ ────────── Per-repo permission control -``` - -### Hypatia Green Web Integration - -SustainaBot receives infrastructure-level sustainability findings from Hypatia's -`green_web.ex` rule module via the fleet dispatch pipeline: - -1. **Hypatia** scans repos for hosting provider, CDN, container registry choices -2. **fleet_dispatcher.ex** classifies Green Web findings as `:control` actions -3. **`dispatch_to_sustainabot/1`** sends a `reportEcoScore` GraphQL mutation -4. **SustainaBot** incorporates the infrastructure eco score and generates fix suggestions - -This separates concerns: Hypatia owns *detection* (scanning 500+ repos), SustainaBot -owns *action* (PRs, badges, SARIF annotations). - -## Scoring - -### Health Index -``` -HealthIndex = 0.4 * EcoScore + 0.3 * EconScore + 0.3 * QualityScore -``` - -### Eco Score -Based on resource efficiency: energy per function, carbon intensity, pattern penalties. - -### Econ Score -Based on economic efficiency: complexity economics, allocative efficiency, maintainability. - -### Quality Score -Based on code quality: cyclomatic complexity, nesting depth, pattern count. - -## Testing - -27 tests across all crates: -- `sustainabot-analysis`: analyzer, patterns, calibration, dependencies, directives -- `sustainabot-sarif`: SARIF structure, JSON validity, suggestions, severity -- `sustainabot-eclexia`: policy evaluation, decisions_to_results -- `sustainabot-fleet`: finding conversion, severity mapping, suggestions - -## Budget-Resume Sweep Integration (AM010 / BP008) - -**TODO** (future Rust integration — Rust implementation deferred, out of scope for this cycle). - -When sustainabot's periodic estate scan detects more than 50 open PRs that are stuck -exclusively on phantom required status-check contexts (BP008 findings), it should trigger -the `.git-private-farm/dispatch-templates/budget-resume-sweep.yml` workflow via -`workflow_dispatch` to admin-merge the eligible PRs in bulk. - -### Calling pattern - -sustainabot triggers the sweep by dispatching to the farm workflow: - -```sh -gh workflow run budget-resume-sweep.yml \ - --repo hyperpolymath/.git-private-farm \ - --field target_owner= \ - --field target_repo= \ - --field max_merges=20 -``` - -### Eligibility criteria (AM010) - -A PR is considered eligible for the sweep when every required status-check context -configured in branch protection (see GitHub API -`repos/{owner}/{repo}/branches/main/protection/required_status_checks`) either: - -1. **passes** (conclusion `SUCCESS`, `NEUTRAL`, or `SKIPPED` in the PR's rollup), or -2. **is a phantom** — it is listed as required but has emitted zero check-runs across - the last five commits on main (Hypatia rule BP008), AND it has no rollup entry on - this specific PR (ALARP type-1 mitigation: a path-filtered workflow that happens to - fire on this PR is not treated as phantom even if it was silent on recent main commits). - -The `hypatia pr-eligibility --owner X --repo Y --pr N` escript command implements this -check and emits JSON: - -```json -{"eligible": true, "reason": "AM010", "phantom_contexts": ["spark-theatre-gate / SPARK Theatre Gate"], "required_contexts": [...]} -``` - -### Threshold - -The dispatch fires when `eligible_pr_count >= 50` (configurable via Hypatia DBA002 rule). -This was the empirically validated threshold from the 2026-05-27 estate sweep where the -GitHub Actions billing cliff-hit first occurred. - -### Why Rust integration is deferred - -The sweep logic is pure GitHub API orchestration (list PRs → query Hypatia → admin-merge). -This is already fully covered by the shell script inside `budget-resume-sweep.yml` which -clones and builds the Hypatia escript on-demand. Embedding the same logic in Rust would -duplicate the Hypatia eligibility engine and create a maintenance surface. The correct -long-term architecture is for sustainabot to invoke the workflow dispatch (one HTTP call) -rather than re-implement the BP008/AM010 checks natively. - -When the Hypatia escript gains a stable gRPC or HTTP API (planned for a future release), -sustainabot-fleet can be updated to call that API directly instead of shelling out. - -## References - -- [SARIF 2.1.0 Specification](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) -- [Software Carbon Intensity (SCI) Specification](https://sci.greensoftware.foundation/) -- [Green Software Foundation](https://greensoftware.foundation/) -- [Hypatia BP008/AM010 rules](https://github.com/hyperpolymath/hypatia) — phantom required context detection and admin-merge eligibility -- [`.git-private-farm/dispatch-templates/budget-resume-sweep.yml`](https://github.com/hyperpolymath/.git-private-farm/blob/main/dispatch-templates/budget-resume-sweep.yml) — the workflow triggered when eligible PR count exceeds 50 diff --git a/bots/sustainabot/Cargo.lock b/bots/sustainabot/Cargo.lock deleted file mode 100644 index 0813e781..00000000 --- a/bots/sustainabot/Cargo.lock +++ /dev/null @@ -1,1389 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "beef" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "cc" -version = "1.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "clap" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "colored" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" -dependencies = [ - "lazy_static", - "windows-sys 0.52.0", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "eclexia-ast" -version = "0.1.0" -dependencies = [ - "la-arena", - "smol_str", -] - -[[package]] -name = "eclexia-interp" -version = "0.1.0" -dependencies = [ - "eclexia-ast", - "rustc-hash", - "serde_json", - "smol_str", - "thiserror 1.0.69", -] - -[[package]] -name = "eclexia-lexer" -version = "0.1.0" -dependencies = [ - "eclexia-ast", - "logos", - "smol_str", -] - -[[package]] -name = "eclexia-parser" -version = "0.1.0" -dependencies = [ - "eclexia-ast", - "eclexia-lexer", - "smol_str", - "thiserror 1.0.69", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "gitbot-shared-context" -version = "0.1.0" -dependencies = [ - "chrono", - "lexpr", - "notify", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "inotify" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "la-arena" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3752f229dcc5a481d60f385fa479ff46818033d881d2d801aa27dffcfb5e8306" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lexpr" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a84de6a9df442363b08f5dbf0cd5b92edc70097b89c4ce4bfea4679fe48bc67" -dependencies = [ - "itoa", - "lexpr-macros", - "ryu", -] - -[[package]] -name = "lexpr-macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36b5cb8bb985c81a8ac1a0f8b5c4865214f574ddd64397ef7a99c236e21f35bb" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "libc" -version = "0.2.180" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" - -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags 2.10.0", - "libc", - "redox_syscall", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "logos" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7251356ef8cb7aec833ddf598c6cb24d17b689d20b993f9d11a3d764e34e6458" -dependencies = [ - "logos-derive", -] - -[[package]] -name = "logos-codegen" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f80069600c0d66734f5ff52cc42f2dabd6b29d205f333d61fd7832e9e9963f" -dependencies = [ - "beef", - "fnv", - "lazy_static", - "proc-macro2", - "quote", - "regex-syntax", - "syn", -] - -[[package]] -name = "logos-derive" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fb722b06a9dc12adb0963ed585f19fc61dc5413e6a9be9422ef92c091e731d" -dependencies = [ - "logos-codegen", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "notify" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" -dependencies = [ - "bitflags 2.10.0", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.52.0", -] - -[[package]] -name = "notify-types" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" -dependencies = [ - "instant", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "panic-attack" -version = "1.0.1" -dependencies = [ - "anyhow", - "chrono", - "clap", - "colored", - "encoding_rs", - "regex", - "serde", - "serde_json", - "serde_yaml", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "redox_syscall" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "sustainabot-analysis" -version = "0.1.0" -dependencies = [ - "anyhow", - "lexpr", - "panic-attack", - "serde", - "serde_json", - "sustainabot-metrics", - "thiserror 2.0.18", - "toml", - "tracing", - "tree-sitter", - "tree-sitter-javascript", - "tree-sitter-python", - "tree-sitter-rust", -] - -[[package]] -name = "sustainabot-cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "serde_json", - "sustainabot-analysis", - "sustainabot-eclexia", - "sustainabot-fleet", - "sustainabot-metrics", - "sustainabot-sarif", - "tracing", - "tracing-subscriber", - "walkdir", -] - -[[package]] -name = "sustainabot-eclexia" -version = "0.1.0" -dependencies = [ - "anyhow", - "eclexia-interp", - "eclexia-lexer", - "eclexia-parser", - "serde", - "serde_json", - "sustainabot-metrics", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "sustainabot-fleet" -version = "0.1.0" -dependencies = [ - "anyhow", - "gitbot-shared-context", - "serde_json", - "sustainabot-analysis", - "sustainabot-metrics", -] - -[[package]] -name = "sustainabot-metrics" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "sustainabot-sarif" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "sustainabot-metrics", -] - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "pin-project-lite", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "tree-sitter" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0203df02a3b6dd63575cc1d6e609edc2181c9a11867a271b25cfd2abff3ec5ca" -dependencies = [ - "cc", - "regex", - "regex-syntax", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-javascript" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf40bf599e0416c16c125c3cec10ee5ddc7d1bb8b0c60fa5c4de249ad34dc1b1" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-language" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae62f7eae5eb549c71b76658648b72cc6111f2d87d24a1e31fa907f4943e3ce" - -[[package]] -name = "tree-sitter-python" -version = "0.23.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-rust" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" -dependencies = [ - "getrandom", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "zmij" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/bots/sustainabot/Cargo.toml b/bots/sustainabot/Cargo.toml deleted file mode 100644 index 809b0a7f..00000000 --- a/bots/sustainabot/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -[workspace] -resolver = "2" -members = [ - "crates/sustainabot-cli", - "crates/sustainabot-analysis", - "crates/sustainabot-metrics", - "crates/sustainabot-sarif", - "crates/sustainabot-eclexia", - "crates/sustainabot-fleet", -] - -[workspace.package] -version = "0.1.0" -authors = ["Jonathan D.A. Jewell "] -edition = "2021" -license = "MPL-2.0" -repository = "https://github.com/hyperpolymath/sustainabot" - -[workspace.dependencies] -# AST parsing -tree-sitter = "0.23.2" -tree-sitter-rust = "0.23.3" -tree-sitter-javascript = "0.23.1" -tree-sitter-python = "0.23.6" - -# CLI -clap = { version = "4.5.55", features = ["derive", "cargo"] } -anyhow = "1.0.100" -thiserror = "2.0.18" - -# Serialization -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.149" -toml = "0.8.23" - -# Logging -tracing = "0.1.44" -tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } - -# Process execution (for Eclexia interpreter) -tokio = { version = "1.49.0", features = ["full"] } - -# Carbon APIs -reqwest = { version = "0.12.28", features = ["json"] } - - -[profile.release] -lto = true -codegen-units = 1 -panic = "abort" diff --git a/bots/sustainabot/DEPLOY.md b/bots/sustainabot/DEPLOY.md deleted file mode 100644 index a8423769..00000000 --- a/bots/sustainabot/DEPLOY.md +++ /dev/null @@ -1,368 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0-or-later -// SPDX-FileCopyrightText: 2024-2025 hyperpolymath - - -# Oikos Bot Deployment Guide - -## Prerequisites - -### Required -- Podman or Vörðr (preferred) -- Deno 2.1+ (for local development) -- Git - -### Optional (for full stack) -- ArangoDB 3.11+ -- Virtuoso 7.2+ -- GHC 9.4+ (for Haskell analyzer development) - -## Quick Start - -### 1. Build Container Image - -```bash -# From repository root (uses Vörðr if available, falls back to Podman) -./containers/vordr-build.sh - -# Or directly with Podman -podman build -t oikos:latest -f containers/Containerfile . -``` - -### 2. Run Container - -```bash -# Basic run -podman run -p 3000:3000 oikos:latest - -# With environment variables -podman run -p 3000:3000 \ - -e BOT_MODE=advisor \ - -e ANALYSIS_ENDPOINT=http://analyzer:8080 \ - -e GITHUB_WEBHOOK_SECRET=your-secret \ - oikos:latest -``` - -### 3. Verify Health - -```bash -curl http://localhost:3000/health -# {"status":"healthy","mode":"advisor"} -``` - -## Configuration - -### Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | HTTP server port | `3000` | -| `BOT_MODE` | Operation mode (`advisor`, `consultant`, `regulator`) | `advisor` | -| `ANALYSIS_ENDPOINT` | Haskell analyzer endpoint | `http://localhost:8080/analyze` | -| `GITHUB_WEBHOOK_SECRET` | GitHub webhook secret | (none) | -| `GITLAB_WEBHOOK_SECRET` | GitLab webhook token | (none) | -| `ARANGO_URL` | ArangoDB connection URL | `http://localhost:8529` | -| `VIRTUOSO_URL` | Virtuoso SPARQL endpoint | `http://localhost:8890/sparql` | - -### Bot Modes - -- **Advisor**: Provides suggestions as PR comments (default, non-blocking) -- **Consultant**: Detailed analysis with confidence scores -- **Regulator**: Enforces policies, can block merges on violations - -## Full Stack Deployment - -### Using Podman Compose - -```yaml -# compose.yaml -version: "3.9" - -services: - oikos: - build: - context: . - dockerfile: containers/Containerfile - ports: - - "3000:3000" - environment: - - BOT_MODE=advisor - - ANALYSIS_ENDPOINT=http://analyzer:8080 - - ARANGO_URL=http://arangodb:8529 - - VIRTUOSO_URL=http://virtuoso:8890/sparql - depends_on: - - analyzer - - arangodb - - virtuoso - - analyzer: - image: docker.io/haskell:9.4-slim - working_dir: /app - volumes: - - ./analyzers/code-haskell:/app - command: cabal run oikos-analyzer -- --server --port 8080 - ports: - - "8080:8080" - - arangodb: - image: docker.io/arangodb:3.11 - environment: - - ARANGO_ROOT_PASSWORD=oikos-dev - ports: - - "8529:8529" - volumes: - - arango-data:/var/lib/arangodb3 - - virtuoso: - image: docker.io/openlink/virtuoso-opensource-7:7.2 - environment: - - DBA_PASSWORD=oikos-dev - ports: - - "8890:8890" - - "1111:1111" - volumes: - - virtuoso-data:/database - -volumes: - arango-data: - virtuoso-data: -``` - -```bash -podman-compose up -d -``` - -### Database Setup - -#### ArangoDB - -```bash -# Load schema -podman exec -it oikos-arangodb-1 arangosh \ - --server.username root \ - --server.password oikos-dev \ - < database/arango/schema.js -``` - -#### Virtuoso - -```bash -# Load ontology -podman exec -it oikos-virtuoso-1 isql 1111 dba oikos-dev exec=" - DB.DBA.TTLP_MT(file_to_string_output('/database/ontology.ttl'), - '', 'http://oikos-bot.dev/ontology'); -" -``` - -## GitHub App Setup - -### Option A: Manifest Flow (Recommended) - -The fastest way to register the GitHub App using the manifest file: - -1. **Start the manifest flow server** (one-time setup): - - ```bash - cd bot-integration - deno run --allow-net --allow-env src/manifest-flow.res.js - ``` - -2. **Or register manually via GitHub**: - - Visit: `https://github.com/settings/apps/new` - - Copy the contents of [`.github/app.yml`](.github/app.yml) and paste into the - manifest field, or use this direct link (after deploying): - - ``` - https://your-domain.com/github/manifest-flow - ``` - -3. **Save the credentials** returned by GitHub: - - `APP_ID` - Your app's numeric ID - - `WEBHOOK_SECRET` - Auto-generated webhook secret - - `PRIVATE_KEY` - PEM file for signing JWTs - - ```bash - export GITHUB_APP_ID="123456" - export GITHUB_WEBHOOK_SECRET="generated-secret" - export GITHUB_PRIVATE_KEY_PATH="/path/to/oikos-bot.pem" - ``` - -### Option B: Manual Registration - -If you prefer manual setup: - -1. Go to GitHub Settings → Developer settings → GitHub Apps → New GitHub App -2. Configure: - - **Name**: Oikos Bot (your-org) - - **Homepage URL**: `https://your-domain.com` - - **Webhook URL**: `https://your-domain.com/webhooks/github` - - **Webhook Secret**: Generate and save this - - **Permissions**: - - Repository: Contents (Read), Pull requests (Read & Write), Checks (Read & Write) - - Organization: Members (Read) - - **Events**: Pull request, Push - -### Install the App - -After registration (either option): - -1. Go to your GitHub App settings -2. Click "Install App" -3. Select repositories to monitor - -### Configure Oikos Bot - -```bash -export GITHUB_WEBHOOK_SECRET="your-webhook-secret" -export GITHUB_APP_ID="your-app-id" -export GITHUB_PRIVATE_KEY_PATH="/path/to/private-key.pem" -``` - -## GitLab Integration - -### 1. Configure Webhook - -1. Go to Project → Settings → Webhooks -2. Add webhook: - - **URL**: `https://your-domain.com/webhooks/gitlab` - - **Secret token**: Generate and save this - - **Triggers**: Merge request events, Push events - -### 2. Configure Oikos Bot - -```bash -export GITLAB_WEBHOOK_SECRET="your-webhook-token" -``` - -## Production Deployment - -### Kubernetes (Helm) - -```bash -# Add Helm repo (when available) -helm repo add oikos https://charts.oikos-bot.dev - -# Install -helm install oikos oikos/oikos-bot \ - --set mode=advisor \ - --set github.webhookSecret=$GITHUB_WEBHOOK_SECRET \ - --set persistence.enabled=true -``` - -### Systemd (Bare Metal) - -```ini -# /etc/systemd/system/oikos-bot.service -[Unit] -Description=Oikos Bot Code Analysis Platform -After=network.target - -[Service] -Type=simple -User=oikos -Environment=PORT=3000 -Environment=BOT_MODE=advisor -ExecStart=/usr/local/bin/deno run \ - --allow-net --allow-env --allow-read \ - /opt/oikos-bot/bot-integration/src/Oikos.affine.js -Restart=always -RestartSec=5 - -[Install] -WantedBy=multi-user.target -``` - -```bash -sudo systemctl enable --now oikos-bot -``` - -## Monitoring - -### Metrics Endpoint - -```bash -curl http://localhost:3000/metrics -``` - -### Health Check - -```bash -# Container health check built-in -podman inspect --format='{{.State.Health.Status}}' oikos - -# Manual check -curl -f http://localhost:3000/health || exit 1 -``` - -### Logging - -Logs are JSON-formatted for easy parsing: - -```json -{"timestamp":"2024-12-08T10:00:00.000Z","level":"info","message":"Starting Oikos Bot","data":{"port":3000,"mode":"advisor"}} -``` - -## Troubleshooting - -### Container won't start - -1. Check logs: `podman logs oikos` -2. Verify Deno permissions: ensure `--allow-net`, `--allow-env`, `--allow-read` -3. Check port availability: `ss -tlnp | grep 3000` - -### Webhook not receiving events - -1. Verify webhook URL is publicly accessible -2. Check webhook secret matches -3. Review GitHub/GitLab webhook delivery logs - -### Analysis failing - -1. Ensure Haskell analyzer is running: `curl http://localhost:8080/health` -2. Check ANALYSIS_ENDPOINT environment variable -3. Review analyzer logs - -### Database connection issues - -1. Verify database is running: `podman ps` -2. Check connection URLs in environment -3. Ensure database credentials are correct - -## Development - -### Local Development - -```bash -# Bot integration (AffineScript/Deno) -cd bot-integration -deno task dev - -# Haskell analyzer -cd analyzers/code-haskell -cabal run oikos-analyzer -- --path /path/to/analyze - -# Build AffineScript -# Note: AffineScript uses dune build system, not npm -# deno task build:affinescript -``` - -### Running Tests - -```bash -# Bot integration tests -cd bot-integration -deno task test - -# Haskell tests -cd analyzers/code-haskell -cabal test - -# Policy engine tests -cd policy-engine -python -m pytest -``` - -## License - -MPL-2.0-or-later - See LICENSE file for details. diff --git a/bots/sustainabot/Justfile b/bots/sustainabot/Justfile deleted file mode 100644 index 5eec7b9b..00000000 --- a/bots/sustainabot/Justfile +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Justfile - hyperpolymath standard task runner - -default: - @just --list - -# Build the project -build: - @echo "Building..." - -# Run tests -test: - @echo "Testing..." - -# Run lints -lint: - @echo "Linting..." - -# Clean build artifacts -clean: - @echo "Cleaning..." - -# Format code -fmt: - @echo "Formatting..." - -# Run all checks -check: lint test - -# Prepare a release -release VERSION: - @echo "Releasing {{VERSION}}..." - -# Trigger automated checking - -github-scorecard: - @echo "Run manually: https://github.com/ossf/scorecard" - - diff --git a/bots/sustainabot/LICENSE b/bots/sustainabot/LICENSE deleted file mode 100644 index ec540b34..00000000 --- a/bots/sustainabot/LICENSE +++ /dev/null @@ -1,153 +0,0 @@ -SPDX-License-Identifier: MPL-2.0 -SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete MPL-2.0 text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from MPL-2.0 apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under MPL-2.0: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. MPL-2.0 Compatibility - Covered Software under this License may be combined with software under - MPL-2.0. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of MPL-2.0 Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL LICENSE VERSION 1.0 --------------------------------------------------------------------------------- - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues diff --git a/bots/sustainabot/MAINTAINERS.md b/bots/sustainabot/MAINTAINERS.md deleted file mode 100644 index 7ecd9ab0..00000000 --- a/bots/sustainabot/MAINTAINERS.md +++ /dev/null @@ -1,14 +0,0 @@ -# Maintainers -This repository is actively maintained by [Jonathan Jewell](https://gitlab.com/hyperpolymath) under the **Rhodium Standard Repo (RSR)** guidelines. - -## Current Maintainers -| Name | Role | Contact | -|-----------------|-----------------------|-----------------------------| -| Jonathan Jewell | Lead Developer | jonathan@hyperpolymath.org | -| (Add others) | Contributor/Triage | (email) | - -## Maintenance Policy -- **Response Time**: Issues/PRs addressed within 72 hours. -- **Security**: Vulnerabilities reported via `security@hyperpolymath.org` (PGP: `0xYOURKEY`). -- **Reversibility**: All changes are logged with **SHAKE256/Ed448** hashes (see `logs/`). -- **Backward Compatibility**: Follows semantic versioning (v0.x.y = breaking changes allowed). diff --git a/bots/sustainabot/Mustfile b/bots/sustainabot/Mustfile deleted file mode 100644 index 2516d22c..00000000 --- a/bots/sustainabot/Mustfile +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Mustfile - hyperpolymath mandatory checks -# See: https://github.com/hyperpolymath/mustfile - -version: 1 - -checks: - - name: security - run: just lint - - name: tests - run: just test - - name: format - run: just fmt - diff --git a/bots/sustainabot/PALIMPSEST.adoc b/bots/sustainabot/PALIMPSEST.adoc deleted file mode 100644 index 124589aa..00000000 --- a/bots/sustainabot/PALIMPSEST.adoc +++ /dev/null @@ -1,92 +0,0 @@ -= Palimpsest Licence -:toc: -:toc-placement!: - -image:https://img.shields.io/badge/license-MPL--2.0-blue.svg[MPL-2.0,link="https://www.mozilla.org/en-US/MPL/2.0/"] -image:https://img.shields.io/badge/philosophy-Palimpsest-purple.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-licence"] - -toc::[] - -== Legal Status - -This project is legally licensed under the **Mozilla Public License 2.0 (MPL-2.0)**. - -The Palimpsest Licence is a philosophical and ethical framework that accompanies the legal license. **Palimpsest does not currently provide legal protections** - it is a manifesto and a set of principles that we encourage adopters to embrace alongside the MPL-2.0. - -== Why MPL-2.0 + Palimpsest? - -=== The Legal Foundation: MPL-2.0 - -The Mozilla Public License 2.0 provides: - -* **File-level copyleft** - modifications to MPL-licensed files must remain open -* **Compatibility** - can be combined with proprietary code and other open source licenses -* **Patent protection** - includes explicit patent grants -* **Clear, modern language** - well-drafted and widely understood - -=== The Philosophical Layer: Palimpsest - -Palimpsest adds an ethical dimension that legal licenses cannot capture: - -* **Layered contribution** - like a palimpsest manuscript, each contributor adds to what came before -* **Transparent provenance** - clear attribution of ideas, not just code -* **Collaborative evolution** - encouraging forks that improve rather than fragment -* **Ethical use guidelines** - principles beyond what law can enforce - -== Future Direction - -**Palimpsest v0.5** will integrate MPL-2.0 and Palimpsest principles into a single, legally recognized license. This work is ongoing. - -Until then: - -1. **MPL-2.0** is the legally binding license for this project -2. **Palimpsest** is the philosophical framework we encourage you to adopt -3. Distributing Palimpsest alongside your use of this code is encouraged but not required - -== Get Involved - -We are actively developing Palimpsest as a real, legally recognized license that captures both the legal and ethical dimensions of open source collaboration. - -If you are interested in: - -* Contributing to the Palimpsest license design -* Adopting Palimpsest for your own projects -* Providing legal expertise on open source licensing -* Discussing the philosophy behind Palimpsest - -**Please get in touch:** - -* GitHub: https://github.com/hyperpolymath -* Palimpsest Licence repo: https://github.com/hyperpolymath/palimpsest-licence - -== Signing Up to Palimpsest - -While Palimpsest is not yet legally formalized, you can signal your support by: - -1. Adding the Palimpsest badge to your project README -2. Including `PALIMPSEST.adoc` in your repository -3. Mentioning Palimpsest in your LICENSE file -4. Joining discussions about the license's development - -== Summary - -[cols="1,2"] -|=== -|Aspect |Status - -|**Legal License** -|MPL-2.0 (Mozilla Public License 2.0) - -|**Philosophical Framework** -|Palimpsest (encouraged, not legally binding) - -|**SPDX Identifier** -|`MPL-2.0` - -|**Future License** -|Palimpsest v0.5+ (MPL-2.0 integrated, legally recognized) -|=== - ---- - -_This project uses MPL-2.0 for legal licensing and encourages adoption of the Palimpsest philosophy. See link:LICENSE[LICENSE] for the full legal text._ diff --git a/bots/sustainabot/QUICKSTART.md b/bots/sustainabot/QUICKSTART.md deleted file mode 100644 index 3702d243..00000000 --- a/bots/sustainabot/QUICKSTART.md +++ /dev/null @@ -1,75 +0,0 @@ -# SustainaBot Quickstart - -## Build - -```bash -cargo build --release -``` - -## Usage - -### Analyze a file - -```bash -./target/release/sustainabot analyze path/to/file.rs -``` - -Supports: Rust (.rs), JavaScript (.js, .mjs), TypeScript (.ts) - -### Analyze sustainabot itself (dogfooding!) - -```bash -./target/release/sustainabot self-analyze -``` - -This proves we practice what we preach - the analyzer itself is efficient. - -### JSON output - -```bash -./target/release/sustainabot analyze file.rs --format json -``` - -## What It Does - -SustainaBot 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 - -``` -📍 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 -``` - -## 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 language matures - -## Philosophy - -> "The best ecological code analyzer is one that's ecological itself." - -SustainaBot demonstrates that efficient, resource-aware code is not just theory - it works in practice. diff --git a/bots/sustainabot/README.adoc b/bots/sustainabot/README.adoc index 21a5930c..418c30e7 100644 --- a/bots/sustainabot/README.adoc +++ b/bots/sustainabot/README.adoc @@ -1,146 +1,39 @@ -# SustainaBot +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += sustainabot — reserved fleet slot (currently empty) +:icons: font -**SustainaBot** is a code analysis tool that evaluates software through an ecological, economic, and engineering quality lens. It integrates with existing developer workflows to highlight inefficiencies, potential risks, and sustainability trade-offs in codebases. +This directory is a *reserved* slot for **sustainabot**, the fleet's +ecological/economic verifier bot. **It is intentionally empty** apart from this +note: the bot has not yet been built as a fleet adapter. -Rather than replacing tools like Dependabot, CodeQL, or linters, SustainaBot complements them by focusing on **resource usage, cost implications, and environmental impact**. +[IMPORTANT] +==== +**`sustainabot` is NOT OikosBot.** They are separate projects. ---- +A full, standalone copy of **OikosBot** (an eco/econ code-analysis App) was once +misfiled here — an entire Rust workspace, Haskell analyzer, AffineScript bot, +containers and policies, vendored inside this one bot slot. That was a mistake. +It has been extracted into its own top-level repository: -## 🚀 What It Does +* **OikosBot** → https://github.com/hyperpolymath/oikosbot -SustainaBot analyzes repositories and produces structured reports that help developers: +See that repo's `DISAMBIGUATION.adoc` for the full breakdown of +**oikos** (a DSL) vs **OikosBot** (the App) vs **sustainabot** (this fleet slot). +==== -* Identify inefficient patterns that increase compute, memory, or energy usage -* Highlight potential cost drivers in infrastructure or runtime behavior -* Correlate code quality and security issues with broader system impact -* Generate actionable insights in standard formats (e.g., SARIF) +== What still references sustainabot ---- +`sustainabot` remains a first-class fleet *member* in code — `BotId::Sustainabot` +in `shared-context/`, the coordinator roster, and the safety-triangle routing are +all intact. Only the *vendored implementation* was removed from this directory. -## 🧠 Core Concepts +== When this slot is built -### Multi-Dimensional Analysis +Follow the convention of the sibling bots (see `../README.adoc` and, e.g., +`../rhodibot/`): a bot directory is a **thin adapter** — a small Rust crate that +adapts an analysis capability to the fleet's `shared-context` interface. It is +**not** a home for an entire standalone product. -SustainaBot evaluates code across three primary dimensions: - -* **Ecological (Eco):** Estimated energy usage, resource intensity, and environmental impact -* **Economic (Econ):** Cost implications of inefficient patterns or architectural decisions -* **Quality:** Maintainability, security signals, and engineering best practices - -These dimensions are combined into a unified **Health Index** to give an overall view of repository health. - -> Note: Scores are heuristic-based and intended for guidance rather than exact measurement. - ---- - -## 🧩 Architecture Overview - -SustainaBot is built as a modular Rust workspace with separate components for: - -* CLI interface -* Static analysis engine -* Metrics and scoring -* SARIF report generation -* Policy integration - -This modular design allows components to be reused or extended independently. - ---- - -## ⚙️ Installation - -```bash -cargo install sustainabot -``` - -Or build from source: - -```bash -git clone https://github.com/hyperpolymath/gitbot-fleet -cd gitbot-fleet/bots/sustainabot -cargo build --release -``` - ---- - -## ▶️ Usage - -Run analysis on a repository: - -```bash -sustainabot analyze . -``` - -Output formats include: - -* JSON -* Text summaries -* SARIF (for GitHub integration) - ---- - -## 🔗 GitHub Integration - -SustainaBot can integrate into GitHub workflows by producing SARIF output that is uploaded to GitHub’s code scanning interface. - -Example GitHub Action: - -```yaml -- name: Run SustainaBot - run: sustainabot analyze . --format sarif > results.sarif - -- name: Upload SARIF - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: results.sarif -``` - ---- - -## 🌍 Example Insights - -SustainaBot may flag issues such as: - -* Inefficient loops or excessive allocations -* Redundant computations -* Over-provisioned infrastructure assumptions -* Patterns associated with higher runtime cost or energy usage - -Recommendations are provided alongside findings to guide improvements. - ---- - -## ⚠️ Limitations - -* Estimates of energy or cost are heuristic and may vary by environment -* Language support may differ in depth (some languages have more complete pattern detection than others) -* Results should be interpreted as advisory, not definitive - ---- - -## 🛣️ Roadmap - -Planned improvements include: - -* Expanded language support -* Improved scoring calibration -* Better benchmarking and validation datasets -* Enhanced GitHub App integration - ---- - -## 🤝 Contributing - -Contributions are welcome. Please open issues or pull requests to suggest improvements, report bugs, or extend functionality. - ---- - -## 📄 License - -See repository for license details. - ---- - -## 🧭 Summary - -SustainaBot is an experimental but promising tool aimed at helping developers make more informed decisions about the broader impact of their code. It is best used alongside existing tooling as part of a holistic engineering workflow. +If sustainabot should reuse OikosBot's analysis, depend on it as an external +crate / service from `hyperpolymath/oikosbot`; do **not** re-vendor it here. diff --git a/bots/sustainabot/README.hybrid.md b/bots/sustainabot/README.hybrid.md deleted file mode 100644 index 939bdb44..00000000 --- a/bots/sustainabot/README.hybrid.md +++ /dev/null @@ -1,223 +0,0 @@ -# SustainaBot: Hybrid Eclexia Architecture - -**Status**: 🚧 Phase 1 Complete (2026-01-29) - -## What We Built - -A working ecological code analyzer that **practices what it preaches** by using Eclexia principles from day one. - -### Architecture - -``` -┌─────────────────────────────────────────┐ -│ SUSTAINABOT (Hybrid Eclexia Design) │ -├─────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────┐ │ -│ │ CLI (Rust) │ │ -│ │ - sustainabot analyze │ │ -│ │ - sustainabot check │ │ -│ │ - sustainabot self-analyze ✨ │ │ -│ └────────────┬─────────────────────┘ │ -│ │ │ -│ ┌────────────▼─────────────────────┐ │ -│ │ Analysis Engine (Rust) │ │ -│ │ - Tree-sitter AST parsing │ │ -│ │ - Resource estimation │ │ -│ │ - Pattern detection │ │ -│ │ - Carbon calculation │ │ -│ │ │ │ -│ │ Eclexia-inspired design: │ │ -│ │ • ResourceProfile types │ │ -│ │ • Shadow prices │ │ -│ │ • Explicit metrics │ │ -│ └────────────┬─────────────────────┘ │ -│ │ │ -│ ┌────────────▼─────────────────────┐ │ -│ │ Policy Engine (Eclexia!) │ │ -│ │ - Rules in .ecl files │ │ -│ │ - Interpreter integration │ │ -│ │ - DOGFOODING: Provably cheap │ │ -│ └──────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────┘ -``` - -## Phase 1 Achievements ✅ - -### Working Components - -1. **Rust Analysis Engine** - - `sustainabot-metrics`: Core types (Energy, Carbon, Duration, Memory) - - `sustainabot-analysis`: AST-based code analyzer using tree-sitter - - `sustainabot-cli`: Command-line interface - - `sustainabot-eclexia`: FFI layer for Eclexia policies (stub) - -2. **Language Support** - - ✅ Rust - - ✅ JavaScript/TypeScript - - 🔜 Python, Go, etc. - -3. **Features** - - Parse code with tree-sitter - - Estimate resource usage (energy, time, carbon, memory) - - Calculate health scores (eco, econ, quality) - - Detect patterns (nested loops, etc.) - - Generate recommendations - - **DOGFOODING**: `sustainabot self-analyze` 🌱 - -4. **Eclexia Integration (Proof of Concept)** - - Example policy in `policies/energy_threshold.ecl` - - Policy has `@requires` annotations proving it's cheap to run - - Demonstrates: "Our policy uses < 1J to analyze your 50J code" - -### Demo - -```bash -# Build -cargo build --release - -# Analyze a file -./target/release/sustainabot analyze test_sample.rs - -# Dogfooding: Analyze sustainabot itself -./target/release/sustainabot self-analyze -``` - -**Output Example:** -``` -📍 Function: nested_loops_function - Resources: - Energy: 7.10 J - Carbon: 0.0009 gCO2e - - Health Index: - Eco: 80.4/100 - Overall: 75.1/100 - - Recommendations: - • Consider algorithm optimization to reduce nested iterations -``` - -## The Dogfooding Strategy - -**Key Insight**: We're building an ecological code analyzer, so the analyzer itself must be ecological. - -### Proof Points - -1. **Explicit Resource Types**: `Energy`, `Carbon`, `Duration`, `Memory` - just like Eclexia -2. **Shadow Prices**: Policy decisions guided by resource costs -3. **Self-Measurement**: `sustainabot self-analyze` proves we're efficient -4. **Eclexia Policies**: Rules written in a language with provable resource bounds - -### Meta-Analysis - -When you run `sustainabot self-analyze`, it shows: -- Most functions score 70-80+ (healthy) -- The analyzer uses ~5-20J per function analyzed -- Carbon footprint: < 0.003 gCO2e per analysis - -**This proves**: A well-designed analyzer can be both powerful AND efficient. - -## Next Steps (Phase 2) - -### Milestone M2: Full Eclexia Integration (Target: 2026-03-01) - -- [ ] Complete FFI bindings to Eclexia interpreter -- [ ] Run `policies/*.ecl` files through Eclexia -- [ ] Measure policy engine's own resource usage -- [ ] Blog post: "Dogfooding Eclexia: How we built an eco-analyzer with eco-code" - -### Milestone M3: Bot Integration (Target: 2026-03-15) - -- [ ] AffineScript webhook server on Deno -- [ ] GitHub PR comments with analysis -- [ ] SARIF output for Code Scanning -- [ ] Dashboard showing repo health - -### Milestone M4: MVP (Target: 2026-04-01) - -- [ ] Analyze real-world repos -- [ ] Carbon-aware scheduling (wait for low-carbon electricity) -- [ ] Learning from analysis results -- [ ] Public demo - -## Design Principles - -### 1. Eclexia-Inspired from Day 1 - -Even though we're using Rust (not Eclexia) for the analyzer, we follow Eclexia principles: -- **Explicit resource tracking**: Every operation has measurable cost -- **Shadow prices**: Guide trade-offs economically -- **First-class metrics**: Energy and carbon aren't afterthoughts - -### 2. Progressive Dogfooding - -- **Now**: Analyzer written with Eclexia principles -- **Phase 2**: Policy engine runs in Eclexia interpreter -- **Phase 3+**: Core analyzer migrates to Eclexia as it matures - -### 3. Prove by Example - -The best way to advocate for ecological coding is to demonstrate it works: -- Sustainabot is fast despite being ecological -- Policies prove their own efficiency (`@requires: energy < 1J`) -- Self-analysis shows we practice what we preach - -## Technical Notes - -### Why Rust (Not Eclexia) for the Analyzer? - -Eclexia is 55% complete - great for demos, not yet production-ready. By using: -- Rust with Eclexia principles (explicit resource types) -- Eclexia interpreter for policies (dogfooding today!) -- Migration path to full Eclexia (as it matures) - -We get the best of both worlds: working software now + genuine dogfooding. - -### Resource Estimation - -Current implementation uses heuristics: -- **Energy**: Complexity × 0.1 J (baseline) -- **Carbon**: Energy × grid intensity (475 gCO2e/kWh average) -- **Time**: Complexity × 0.5 ms -- **Memory**: Complexity × 2 KB - -Future: Profile real execution, integrate carbon APIs, ML models. - -### Tree-sitter Languages - -Using tree-sitter for multi-language support: -- `tree-sitter-rust` -- `tree-sitter-javascript` -- Easy to add more languages - -## Contributing - -This is an active development project. Key areas: - -1. **Improve heuristics**: Better resource estimation -2. **Add languages**: Python, Go, Java, etc. -3. **Eclexia integration**: Complete FFI, run real policies -4. **Pattern detection**: More anti-patterns -5. **Carbon APIs**: Real-time grid intensity - -## Philosophy - -> "The best ecological code analyzer is one that's ecological itself." - -By building Sustainabot with explicit resource awareness from the start, we prove that: -- Efficient code is achievable -- Resource tracking is practical -- Eclexia's vision works in reality - -## License - -MPL-2.0 - ---- - -**Built with**: Rust, Eclexia (policies), Tree-sitter -**Part of**: hyperpolymath ecosystem -**Status**: Phase 1 Complete, Phase 2 Starting -**Updated**: 2026-01-29 diff --git a/bots/sustainabot/ROADMAP.adoc b/bots/sustainabot/ROADMAP.adoc deleted file mode 100644 index 8f844e1a..00000000 --- a/bots/sustainabot/ROADMAP.adoc +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -= Oikos Roadmap - -== Current Status - -Initial development phase. - -== Milestones - -=== v0.1.0 - Foundation -* [ ] Core functionality -* [ ] Basic documentation -* [ ] CI/CD pipeline - -=== v1.0.0 - Stable Release -* [ ] Full feature set -* [ ] Comprehensive tests -* [ ] Production ready - -== Future Directions - -_To be determined based on community feedback._ diff --git a/bots/sustainabot/analyzers/code-haskell/app/Main.hs b/bots/sustainabot/analyzers/code-haskell/app/Main.hs deleted file mode 100644 index 8b447228..00000000 --- a/bots/sustainabot/analyzers/code-haskell/app/Main.hs +++ /dev/null @@ -1,191 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | Eco-Analyzer CLI and HTTP Server --- SPDX-License-Identifier: MPL-2.0 --- --- Entry point for the Haskell code analyzer. --- Supports both CLI analysis and HTTP server mode. -module Main where - -import Eco.Analysis -import Types.Metrics -import Types.Report - -import Options.Applicative -import Data.Text (Text) -import qualified Data.Text as T -import qualified Data.Text.IO as TIO -import qualified Data.ByteString.Lazy as BL -import System.Exit (exitFailure, exitSuccess) -import Control.Monad (when) - --- | Command line options -data Options = Options - { optMode :: !Mode - , optPath :: !FilePath - , optOutput :: !OutputFormat - , optVerbose :: !Bool - , optPort :: !Int - , optRepoName :: !(Maybe Text) - , optCommitSha :: !(Maybe Text) - , optBranch :: !(Maybe Text) - } deriving (Show, Eq) - --- | Execution mode -data Mode - = Analyze -- ^ Analyze a repository/directory - | Server -- ^ Run as HTTP server - | Version -- ^ Show version - deriving (Show, Eq) - --- | Output format -data OutputFormat - = JSON -- ^ JSON output - | Markdown -- ^ Markdown report - | Summary -- ^ Brief summary - deriving (Show, Eq) - --- | Parse command line options -optionsParser :: Parser Options -optionsParser = Options - <$> modeParser - <*> strOption - ( long "path" - <> short 'p' - <> metavar "PATH" - <> value "." - <> help "Path to analyze (default: current directory)" ) - <*> outputParser - <*> switch - ( long "verbose" - <> short 'v' - <> help "Verbose output" ) - <*> option auto - ( long "port" - <> metavar "PORT" - <> value 8080 - <> help "HTTP server port (default: 8080)" ) - <*> optional (strOption - ( long "repo" - <> metavar "NAME" - <> help "Repository name for report" )) - <*> optional (strOption - ( long "commit" - <> metavar "SHA" - <> help "Commit SHA for report" )) - <*> optional (strOption - ( long "branch" - <> metavar "BRANCH" - <> help "Branch name for report" )) - -modeParser :: Parser Mode -modeParser = flag' Server - ( long "server" - <> short 's' - <> help "Run as HTTP server" ) - <|> flag' Version - ( long "version" - <> help "Show version" ) - <|> pure Analyze - -outputParser :: Parser OutputFormat -outputParser = flag' JSON - ( long "json" - <> short 'j' - <> help "Output as JSON" ) - <|> flag' Markdown - ( long "markdown" - <> short 'm' - <> help "Output as Markdown" ) - <|> pure Summary - --- | Main entry point -main :: IO () -main = do - opts <- execParser parserInfo - case optMode opts of - Version -> printVersion - Server -> runServer opts - Analyze -> runAnalysis opts - - where - parserInfo = info (optionsParser <**> helper) - ( fullDesc - <> progDesc "Analyze code for ecological and economic metrics" - <> header "eco-analyzer - Ecological & Economic Code Analysis" ) - --- | Print version information -printVersion :: IO () -printVersion = do - putStrLn "eco-analyzer 0.1.0" - putStrLn "Copyright (c) 2024 Hyperpolymath" - putStrLn "License: PMPL-1.0-or-later" - exitSuccess - --- | Run analysis on a path -runAnalysis :: Options -> IO () -runAnalysis opts = do - when (optVerbose opts) $ - putStrLn $ "Analyzing: " ++ optPath opts - - result <- analyzeRepository defaultAnalysisConfig (optPath opts) - - case optOutput opts of - JSON -> BL.putStr $ analysisToJSON result - Markdown -> do - let report = analysisToReport - (maybe "unknown" id $ optRepoName opts) - (maybe "HEAD" id $ optCommitSha opts) - (maybe "main" id $ optBranch opts) - result - TIO.putStrLn $ toMarkdown report - Summary -> printSummary result - - -- Exit with appropriate code based on grade - let grade = hiGrade $ arHealth result - case grade of - "A" -> exitSuccess - "B" -> exitSuccess - "C" -> exitSuccess - _ -> exitFailure - --- | Print analysis summary -printSummary :: AnalysisResult -> IO () -printSummary result = do - let health = arHealth result - putStrLn "═══════════════════════════════════════════" - putStrLn " ECO-ANALYZER SUMMARY" - putStrLn "═══════════════════════════════════════════" - putStrLn "" - putStrLn $ "Overall Score: " ++ show (round $ hiTotal health :: Int) ++ "/100" - putStrLn $ "Grade: " ++ T.unpack (hiGrade health) - putStrLn "" - putStrLn "───────────────────────────────────────────" - putStrLn "Breakdown:" - putStrLn $ " Ecological: " ++ show (round $ ecoScore $ arEco result :: Int) ++ "/100" - putStrLn $ " Economic: " ++ show (round $ econScore $ arEcon result :: Int) ++ "/100" - putStrLn $ " Quality: " ++ show (round $ qualScore $ arQuality result :: Int) ++ "/100" - putStrLn "" - putStrLn "───────────────────────────────────────────" - putStrLn "Key Metrics:" - putStrLn $ " Carbon Score: " ++ show (round $ carbonNormalized $ ecoCarbon $ arEco result :: Int) - putStrLn $ " Energy Patterns: " ++ show (length $ energyPatterns $ ecoEnergy $ arEco result) - putStrLn $ " Cyclomatic: " ++ show (cmCyclomatic $ qualComplexity $ arQuality result) - putStrLn $ " Technical Debt: " ++ show (round $ debtPrincipal $ econDebt $ arEcon result :: Int) ++ "h" - putStrLn "" - putStrLn $ "Analysis version: " ++ T.unpack (arVersion result) - putStrLn $ "Timestamp: " ++ T.unpack (arTimestamp result) - putStrLn "═══════════════════════════════════════════" - --- | Run HTTP server mode -runServer :: Options -> IO () -runServer opts = do - putStrLn $ "Starting eco-analyzer server on port " ++ show (optPort opts) - putStrLn "Endpoints:" - putStrLn " POST /analyze - Analyze repository" - putStrLn " GET /health - Health check" - putStrLn "" - putStrLn "Server mode not yet implemented." - putStrLn "Use CLI mode: eco-analyzer --path /path/to/repo" - -- TODO: Implement HTTP server using warp - exitSuccess diff --git a/bots/sustainabot/analyzers/code-haskell/cabal.project b/bots/sustainabot/analyzers/code-haskell/cabal.project deleted file mode 100644 index ef7e91d9..00000000 --- a/bots/sustainabot/analyzers/code-haskell/cabal.project +++ /dev/null @@ -1,10 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- Cabal project configuration for eco-analyzer - -packages: . - --- Allow newer dependencies -allow-newer: all - --- Build with optimizations -optimization: 2 diff --git a/bots/sustainabot/analyzers/code-haskell/oikos-analyzer.cabal b/bots/sustainabot/analyzers/code-haskell/oikos-analyzer.cabal deleted file mode 100644 index 9afa2bc8..00000000 --- a/bots/sustainabot/analyzers/code-haskell/oikos-analyzer.cabal +++ /dev/null @@ -1,75 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- SPDX-FileCopyrightText: 2024-2025 hyperpolymath -cabal-version: 3.0 -name: oikos-analyzer -version: 0.1.0.0 -synopsis: Ecological and economic code analysis engine -description: Haskell-based code analyzer for Oikos Bot platform. - Analyzes code for carbon intensity, energy efficiency, - Pareto optimality, and software quality metrics. -license: PMPL-1.0-or-later -author: Hyperpolymath -maintainer: oikos@hyperpolymath.dev -category: Development -build-type: Simple - -common warnings - ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates - -Wincomplete-uni-patterns -Wmissing-export-lists - -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints - -library - import: warnings - exposed-modules: - Eco.Analysis - Eco.Carbon - Eco.Energy - Eco.Pareto - Eco.Resource - Quality.Complexity - Quality.Coupling - Quality.Debt - Quality.Coverage - Types.Metrics - Types.Report - build-depends: - base ^>=4.17, - aeson >= 2.0, - text >= 2.0, - containers >= 0.6, - vector >= 0.13, - mtl >= 2.3, - transformers >= 0.6, - scientific >= 0.3, - bytestring >= 0.11, - filepath >= 1.4, - directory >= 1.3, - tree-sitter >= 0.9, - megaparsec >= 9.0, - optparse-applicative >= 0.18 - hs-source-dirs: src - default-language: GHC2021 - -executable oikos-analyzer - import: warnings - main-is: Main.hs - build-depends: - base ^>=4.17, - oikos-analyzer, - aeson, - text, - optparse-applicative - hs-source-dirs: app - default-language: GHC2021 - -test-suite oikos-analyzer-test - import: warnings - default-language: GHC2021 - type: exitcode-stdio-1.0 - hs-source-dirs: test - main-is: Main.hs - build-depends: - base ^>=4.17, - oikos-analyzer, - hspec >= 2.10, - QuickCheck >= 2.14 diff --git a/bots/sustainabot/analyzers/code-haskell/src/Eco/Analysis.hs b/bots/sustainabot/analyzers/code-haskell/src/Eco/Analysis.hs deleted file mode 100644 index 404e2274..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Eco/Analysis.hs +++ /dev/null @@ -1,357 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | Main analysis orchestrator --- SPDX-License-Identifier: MPL-2.0 --- --- Coordinates all analysis modules and produces unified results. -module Eco.Analysis - ( -- * Analysis Entry Points - analyzeRepository - , analyzeFile - , analyzeDirectory - - -- * Configuration - , AnalysisConfig(..) - , defaultAnalysisConfig - - -- * Results - , analysisToJSON - , analysisToReport - ) where - -import Types.Metrics -import Types.Report -import Eco.Carbon -import Eco.Energy -import Eco.Pareto -import Eco.Resource -import Quality.Complexity -import Quality.Coupling -import Quality.Debt -import Quality.Coverage - -import Data.Text (Text) -import qualified Data.Text as T -import qualified Data.Text.IO as TIO -import qualified Data.Aeson as Aeson -import Data.ByteString.Lazy (ByteString) -import System.Directory (listDirectory, doesFileExist, doesDirectoryExist) -import System.FilePath ((), takeExtension) -import Control.Monad (filterM, forM) -import Data.Time.Clock (getCurrentTime) -import Data.Time.Format (formatTime, defaultTimeLocale) - --- | Configuration for the analysis engine -data AnalysisConfig = AnalysisConfig - { acCarbonConfig :: !CarbonConfig - , acEnergyConfig :: !EnergyConfig - , acResourceConfig :: !ResourceConfig - , acComplexityConfig :: !ComplexityConfig - , acCouplingConfig :: !CouplingConfig - , acDebtConfig :: !DebtConfig - , acCoverageConfig :: !CoverageConfig - , acEcoWeight :: !Double -- ^ Weight for ecological score - , acEconWeight :: !Double -- ^ Weight for economic score - , acQualityWeight :: !Double -- ^ Weight for quality score - , acExcludePatterns :: ![Text] -- ^ Patterns to exclude - , acCoverageFile :: !(Maybe FilePath) -- ^ Path to coverage report - } deriving (Show, Eq) - --- | Default analysis configuration -defaultAnalysisConfig :: AnalysisConfig -defaultAnalysisConfig = AnalysisConfig - { acCarbonConfig = defaultCarbonConfig - , acEnergyConfig = defaultEnergyConfig - , acResourceConfig = defaultResourceConfig - , acComplexityConfig = defaultComplexityConfig - , acCouplingConfig = defaultCouplingConfig - , acDebtConfig = defaultDebtConfig - , acCoverageConfig = defaultCoverageConfig - , acEcoWeight = 0.33 - , acEconWeight = 0.33 - , acQualityWeight = 0.34 - , acExcludePatterns = ["node_modules", "vendor", ".git", "dist", "build"] - , acCoverageFile = Nothing - } - --- | Analyze an entire repository -analyzeRepository :: AnalysisConfig -> FilePath -> IO AnalysisResult -analyzeRepository config repoPath = do - -- Find all source files - files <- findSourceFiles config repoPath - - -- Analyze each file - fileResults <- mapM (analyzeFile config) files - - -- Aggregate results - let ecoMetrics = aggregateEcoMetrics (acCarbonConfig config) (acEnergyConfig config) fileResults - let qualMetrics = aggregateQualityMetrics fileResults - - -- Calculate economic metrics (Pareto, debt, allocation) - let econMetrics = calculateEconMetrics config qualMetrics - - -- Calculate health index - let healthIndex = calculateHealthIndex config ecoMetrics econMetrics qualMetrics - - -- Get timestamp - now <- getCurrentTime - let timestamp = T.pack $ formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" now - - pure AnalysisResult - { arEco = ecoMetrics - , arEcon = econMetrics - , arQuality = qualMetrics - , arHealth = healthIndex - , arTimestamp = timestamp - , arVersion = "0.1.0" - } - --- | Analyze a single file -analyzeFile :: AnalysisConfig -> FilePath -> IO FileAnalysisResult -analyzeFile config filePath = do - content <- TIO.readFile filePath - pure $ analyzeContent config (T.pack filePath) content - --- | Internal file analysis result -data FileAnalysisResult = FileAnalysisResult - { farPath :: !Text - , farLines :: !Int - , farComplexity :: !Int - , farPatterns :: ![EnergyPattern] - , farIssues :: ![DebtItem] - } deriving (Show, Eq) - --- | Analyze file content -analyzeContent :: AnalysisConfig -> Text -> Text -> FileAnalysisResult -analyzeContent _config path content = FileAnalysisResult - { farPath = path - , farLines = length $ T.lines content - , farComplexity = estimateComplexity content - , farPatterns = detectPatterns content - , farIssues = detectIssues path content - } - --- | Estimate cyclomatic complexity from content -estimateComplexity :: Text -> Int -estimateComplexity content = - 1 + branchKeywords + loopKeywords - where - countOccurrences needle haystack = - length $ T.breakOnAll needle haystack - - branchKeywords = sum - [ countOccurrences "if " content - , countOccurrences "if(" content - , countOccurrences "else " content - , countOccurrences "case " content - , countOccurrences "?" content -- ternary - ] - - loopKeywords = sum - [ countOccurrences "for " content - , countOccurrences "for(" content - , countOccurrences "while " content - , countOccurrences "while(" content - ] - --- | Detect energy patterns in content -detectPatterns :: Text -> [EnergyPattern] -detectPatterns content = concat - [ busyWaits - , inefficientLoops - ] - where - busyWaits = - [ BusyWaiting (CodeLocation "" 0 0 (Just "Potential busy wait")) - | "while(true)" `T.isInfixOf` content || "while (true)" `T.isInfixOf` content - ] - - inefficientLoops = - [ IneffientLoop (CodeLocation "" 0 0 (Just "Nested loop detected")) - | hasNestedLoops content - ] - - hasNestedLoops c = - let lines' = T.lines c - loopLines = filter isLoopLine lines' - in length loopLines > 3 - - isLoopLine l = "for " `T.isInfixOf` l || "while " `T.isInfixOf` l - --- | Detect code issues -detectIssues :: Text -> Text -> [DebtItem] -detectIssues path content = concat - [ todoIssues - , longFunctionIssues - ] - where - todoIssues = - [ DebtItem - { diLocation = CodeLocation path lineNum 1 (Just $ T.take 50 line) - , diType = "TODO" - , diSeverity = 3 - , diDescription = "TODO comment found" - } - | (lineNum, line) <- zip [1..] (T.lines content) - , "TODO" `T.isInfixOf` line || "FIXME" `T.isInfixOf` line - ] - - longFunctionIssues = - [ DebtItem - { diLocation = CodeLocation path 1 1 Nothing - , diType = "LongFile" - , diSeverity = 5 - , diDescription = "File exceeds 500 lines" - } - | length (T.lines content) > 500 - ] - --- | Analyze a directory -analyzeDirectory :: AnalysisConfig -> FilePath -> IO AnalysisResult -analyzeDirectory = analyzeRepository - --- | Find source files in a directory -findSourceFiles :: AnalysisConfig -> FilePath -> IO [FilePath] -findSourceFiles config rootPath = do - exists <- doesDirectoryExist rootPath - if not exists - then pure [] - else findFiles rootPath - where - findFiles dir = do - entries <- listDirectory dir - let fullPaths = map (dir ) entries - excluded = any (\p -> T.pack p `T.isInfixOf` T.pack dir) (acExcludePatterns config) - if excluded - then pure [] - else do - files <- filterM doesFileExist fullPaths - dirs <- filterM doesDirectoryExist fullPaths - subFiles <- concat <$> mapM findFiles dirs - let sourceFiles = filter isSourceFile files - pure $ sourceFiles ++ subFiles - - isSourceFile f = takeExtension f `elem` - [".hs", ".rs", ".res", ".ml", ".js", ".ts", ".py", ".go", ".java", ".rb"] - --- | Aggregate eco metrics from file results -aggregateEcoMetrics :: CarbonConfig -> EnergyConfig -> [FileAnalysisResult] -> EcoMetrics -aggregateEcoMetrics carbonConfig _energyConfig results = EcoMetrics - { ecoCarbon = carbonScore - , ecoEnergy = energyScore - , ecoResource = resourceScore - , ecoScore = (carbonNormalized carbonScore + energyNormalized energyScore + resourceNormalized resourceScore) / 3 - } - where - totalComplexity = sum $ map farComplexity results - totalLines = sum $ map farLines results - allPatterns = concatMap farPatterns results - - carbonInput = CodeAnalysisInput - { caiComplexity = totalComplexity - , caiLoopDepth = 2 -- Estimated - , caiAllocations = totalLines `div` 10 - , caiIOOperations = length results - , caiParallelism = 1 - } - - carbonScore = analyzeCarbonIntensity carbonConfig carbonInput - - energyScore = EnergyScore - { energyPatterns = allPatterns - , energyNormalized = max 0 $ 100 - fromIntegral (length allPatterns) * 10 - , energyHotspots = [] - } - - resourceScore = ResourceScore - { memoryEfficiency = 80 - , cpuEfficiency = 85 - , ioEfficiency = 90 - , resourceNormalized = 85 - } - --- | Aggregate quality metrics from file results -aggregateQualityMetrics :: [FileAnalysisResult] -> QualityMetrics -aggregateQualityMetrics results = QualityMetrics - { qualComplexity = ComplexityMetrics - { cmCyclomatic = maxComplexity - , cmCognitive = maxComplexity -- Simplified - , cmLinesOfCode = totalLines - , cmMaintainability = maintainability - , cmHotspots = [] - } - , qualCoupling = CouplingScore 5 10 0.5 0.3 0.2 - , qualCoverage = Nothing - , qualScore = maintainability - } - where - totalLines = sum $ map farLines results - maxComplexity = if null results then 0 else maximum $ map farComplexity results - avgComplexity = if null results then 0 else sum (map farComplexity results) `div` length results - maintainability = max 0 $ 100 - fromIntegral avgComplexity * 2 - --- | Calculate economic metrics -calculateEconMetrics :: AnalysisConfig -> QualityMetrics -> EconMetrics -calculateEconMetrics config qualMetrics = EconMetrics - { econPareto = paretoFrontier - , econAllocation = allocationScore - , econDebt = debtEstimate - , econScore = (100 - debtRatio debtEstimate * 100 + allocEfficiency allocationScore * 100) / 2 - } - where - -- Simplified Pareto analysis - paretoFrontier = calculateParetoFrontier standardObjectives - [ [qualScore qualMetrics, fromIntegral $ cmCyclomatic $ qualComplexity qualMetrics] - ] - - allocationScore = AllocationScore - { allocEfficiency = 0.7 - , allocWaste = 0.2 - , allocSuggestions = ["Reduce code duplication", "Improve test coverage"] - } - - debtEstimate = DebtEstimate - { debtPrincipal = fromIntegral (cmCyclomatic $ qualComplexity qualMetrics) * 2 - , debtInterest = fromIntegral (cmCyclomatic $ qualComplexity qualMetrics) * 0.3 - , debtRatio = 0.1 - , debtItems = [] - } - --- | Calculate overall health index -calculateHealthIndex :: AnalysisConfig -> EcoMetrics -> EconMetrics -> QualityMetrics -> HealthIndex -calculateHealthIndex config eco econ qual = HealthIndex - { hiEco = acEcoWeight config - , hiEcon = acEconWeight config - , hiQuality = acQualityWeight config - , hiTotal = totalScore - , hiGrade = toGrade totalScore - } - where - totalScore = acEcoWeight config * ecoScore eco - + acEconWeight config * econScore econ - + acQualityWeight config * qualScore qual - - toGrade score - | score >= 90 = "A" - | score >= 80 = "B" - | score >= 70 = "C" - | score >= 60 = "D" - | otherwise = "F" - --- | Convert analysis result to JSON -analysisToJSON :: AnalysisResult -> ByteString -analysisToJSON = Aeson.encode - --- | Convert analysis result to report -analysisToReport :: Text -> Text -> Text -> AnalysisResult -> AnalysisReport -analysisToReport repoName commitSha branch result = - (emptyReport repoName commitSha branch) - { arMetrics = result - , arOverallScore = hiTotal $ arHealth result - , arGrade = hiGrade $ arHealth result - , arSections = - [ ReportSection "Ecological" (ecoScore $ arEco result) [] "Carbon and energy analysis" - , ReportSection "Economic" (econScore $ arEcon result) [] "Pareto and debt analysis" - , ReportSection "Quality" (qualScore $ arQuality result) [] "Complexity and coverage analysis" - ] - } diff --git a/bots/sustainabot/analyzers/code-haskell/src/Eco/Carbon.hs b/bots/sustainabot/analyzers/code-haskell/src/Eco/Carbon.hs deleted file mode 100644 index 735f2911..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Eco/Carbon.hs +++ /dev/null @@ -1,110 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | Carbon intensity analysis based on SCI specification --- Reference: ISO/IEC 21031:2024 (Software Carbon Intensity) -module Eco.Carbon - ( analyzeCarbonIntensity - , estimateOperationalCarbon - , estimateEmbodiedCarbon - , CarbonConfig(..) - , defaultCarbonConfig - ) where - -import Types.Metrics -import Data.Text (Text) -import qualified Data.Text as T - --- | Configuration for carbon analysis -data CarbonConfig = CarbonConfig - { ccGridIntensity :: !Double -- ^ gCO2eq/kWh for electricity grid - , ccHardwareLifespan :: !Double -- ^ Expected hardware lifespan in years - , ccUsagePercentage :: !Double -- ^ % of hardware dedicated to this software - } deriving (Show, Eq) - --- | Default configuration (global average) -defaultCarbonConfig :: CarbonConfig -defaultCarbonConfig = CarbonConfig - { ccGridIntensity = 475 -- Global average gCO2eq/kWh - , ccHardwareLifespan = 4 -- 4 year hardware lifecycle - , ccUsagePercentage = 0.01 -- 1% of hardware for this workload - } - --- | Analyze carbon intensity of code --- --- Based on SCI formula: SCI = ((E * I) + M) / R --- Where: --- E = Energy consumed by software --- I = Location-based marginal carbon intensity --- M = Embodied emissions of hardware --- R = Functional unit (per request, per user, etc.) -analyzeCarbonIntensity :: CarbonConfig -> CodeAnalysisInput -> CarbonScore -analyzeCarbonIntensity config input = CarbonScore - { carbonValue = sciScore - , carbonNormalized = normalizeScore sciScore - , carbonFactors = identifyFactors input - } - where - operationalCarbon = estimateOperationalCarbon config input - embodiedCarbon = estimateEmbodiedCarbon config input - sciScore = operationalCarbon + embodiedCarbon - --- | Estimate operational carbon (E * I from SCI) -estimateOperationalCarbon :: CarbonConfig -> CodeAnalysisInput -> Double -estimateOperationalCarbon config input = - energyEstimate * (ccGridIntensity config / 1000) -- Convert to kgCO2eq - where - energyEstimate = estimateEnergyConsumption input - --- | Estimate embodied carbon (M from SCI) -estimateEmbodiedCarbon :: CarbonConfig -> CodeAnalysisInput -> Double -estimateEmbodiedCarbon config _input = - -- Simplified model: hardware manufacturing emissions amortized - (hardwareEmissions * ccUsagePercentage config) / ccHardwareLifespan config - where - hardwareEmissions = 300 -- kg CO2eq for typical server (simplified) - --- | Placeholder for code analysis input -data CodeAnalysisInput = CodeAnalysisInput - { caiComplexity :: !Int -- ^ Cyclomatic complexity - , caiLoopDepth :: !Int -- ^ Maximum loop nesting depth - , caiAllocations :: !Int -- ^ Number of heap allocations - , caiIOOperations :: !Int -- ^ Number of I/O operations - , caiParallelism :: !Int -- ^ Degree of parallelism - } deriving (Show, Eq) - --- | Estimate energy consumption based on code characteristics --- This is a heuristic model - actual measurement would be more accurate -estimateEnergyConsumption :: CodeAnalysisInput -> Double -estimateEnergyConsumption input = - baseEnergy * complexityFactor * ioFactor * parallelismFactor - where - baseEnergy = 0.001 -- Base energy in kWh per execution - - -- Complexity increases energy non-linearly - complexityFactor = - 1 + (log (fromIntegral (caiComplexity input) + 1) / 10) - - -- I/O is expensive - ioFactor = 1 + (fromIntegral (caiIOOperations input) * 0.1) - - -- Parallelism can reduce or increase energy depending on efficiency - parallelismFactor = - if caiParallelism input > 1 - then 0.8 + (fromIntegral (caiParallelism input) * 0.05) - else 1.0 - --- | Normalize carbon score to 0-100 (100 = lowest carbon) -normalizeScore :: Double -> Double -normalizeScore rawScore - | rawScore <= 0.001 = 100 -- Excellent - | rawScore >= 1.0 = 0 -- Very poor - | otherwise = 100 * (1 - (log rawScore + 6.9) / 6.9) - --- | Identify factors contributing to carbon intensity -identifyFactors :: CodeAnalysisInput -> [Text] -identifyFactors input = concat - [ ["High complexity increases computation" | caiComplexity input > 20] - , ["Deep loop nesting may indicate inefficiency" | caiLoopDepth input > 4] - , ["Many heap allocations increase memory energy" | caiAllocations input > 100] - , ["Heavy I/O operations are energy-intensive" | caiIOOperations input > 50] - ] diff --git a/bots/sustainabot/analyzers/code-haskell/src/Eco/Energy.hs b/bots/sustainabot/analyzers/code-haskell/src/Eco/Energy.hs deleted file mode 100644 index 5e59e99e..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Eco/Energy.hs +++ /dev/null @@ -1,138 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | Energy efficiency pattern analysis --- SPDX-License-Identifier: MPL-2.0 --- --- Detects energy-inefficient code patterns and suggests improvements --- based on software energy consumption research. -module Eco.Energy - ( analyzeEnergyPatterns - , detectBusyWaiting - , detectIneffientLoops - , detectBlockingIO - , detectRedundantComputation - , EnergyConfig(..) - , defaultEnergyConfig - ) where - -import Types.Metrics -import Data.Text (Text) -import qualified Data.Text as T - --- | Configuration for energy analysis -data EnergyConfig = EnergyConfig - { ecBusyWaitThreshold :: !Int -- ^ Spin iterations before flagging - , ecLoopUnrollThreshold :: !Int -- ^ Loop count before suggesting unroll - , ecIOBatchThreshold :: !Int -- ^ IO calls before suggesting batch - , ecCacheableThreshold :: !Int -- ^ Repeated computations to flag - } deriving (Show, Eq) - --- | Default energy analysis configuration -defaultEnergyConfig :: EnergyConfig -defaultEnergyConfig = EnergyConfig - { ecBusyWaitThreshold = 100 - , ecLoopUnrollThreshold = 1000 - , ecIOBatchThreshold = 10 - , ecCacheableThreshold = 3 - } - --- | Source code analysis input (simplified AST representation) -data SourceAnalysis = SourceAnalysis - { saLoops :: ![LoopInfo] - , saIOCalls :: ![IOCallInfo] - , saFunctionCalls :: ![FunctionCallInfo] - , saSpinLocks :: ![SpinLockInfo] - } deriving (Show, Eq) - -data LoopInfo = LoopInfo - { liLocation :: !CodeLocation - , liIterations :: !(Maybe Int) -- ^ Estimated iterations if known - , liBody :: !Text -- ^ Loop body snippet - , liNested :: !Int -- ^ Nesting depth - } deriving (Show, Eq) - -data IOCallInfo = IOCallInfo - { ioLocation :: !CodeLocation - , ioType :: !Text -- ^ file, network, database - , ioBlocking :: !Bool - } deriving (Show, Eq) - -data FunctionCallInfo = FunctionCallInfo - { fcLocation :: !CodeLocation - , fcName :: !Text - , fcCallCount :: !Int -- ^ Number of times called - , fcPure :: !Bool -- ^ Is it a pure function? - } deriving (Show, Eq) - -data SpinLockInfo = SpinLockInfo - { slLocation :: !CodeLocation - , slIterations :: !Int - } deriving (Show, Eq) - --- | Analyze source for energy patterns -analyzeEnergyPatterns :: EnergyConfig -> SourceAnalysis -> EnergyScore -analyzeEnergyPatterns config source = EnergyScore - { energyPatterns = patterns - , energyNormalized = normalizeEnergyScore patterns - , energyHotspots = map patternLocation patterns - } - where - patterns = concat - [ detectBusyWaiting config (saSpinLocks source) - , detectIneffientLoops config (saLoops source) - , detectBlockingIO config (saIOCalls source) - , detectRedundantComputation config (saFunctionCalls source) - ] - - patternLocation (BusyWaiting loc) = loc - patternLocation (IneffientLoop loc) = loc - patternLocation (BlockingIO loc) = loc - patternLocation (RedundantComputation loc) = loc - patternLocation (EfficientPattern loc) = loc - --- | Detect busy waiting / spin locks -detectBusyWaiting :: EnergyConfig -> [SpinLockInfo] -> [EnergyPattern] -detectBusyWaiting config = map toBusyWait . filter isExcessive - where - isExcessive sl = slIterations sl > ecBusyWaitThreshold config - toBusyWait sl = BusyWaiting (slLocation sl) - --- | Detect inefficient loops (too many iterations, deep nesting) -detectIneffientLoops :: EnergyConfig -> [LoopInfo] -> [EnergyPattern] -detectIneffientLoops config = map toInefficient . filter isInefficient - where - isInefficient li = - maybe False (> ecLoopUnrollThreshold config) (liIterations li) - || liNested li > 3 - - toInefficient li = IneffientLoop (liLocation li) - --- | Detect blocking I/O that could be async -detectBlockingIO :: EnergyConfig -> [IOCallInfo] -> [EnergyPattern] -detectBlockingIO _config = map toBlocking . filter ioBlocking - where - toBlocking io = BlockingIO (ioLocation io) - --- | Detect redundant computations that could be cached/memoized -detectRedundantComputation :: EnergyConfig -> [FunctionCallInfo] -> [EnergyPattern] -detectRedundantComputation config = map toRedundant . filter isRedundant - where - isRedundant fc = - fcPure fc && fcCallCount fc >= ecCacheableThreshold config - - toRedundant fc = RedundantComputation (fcLocation fc) - --- | Normalize energy patterns to 0-100 score (100 = best) -normalizeEnergyScore :: [EnergyPattern] -> Double -normalizeEnergyScore patterns - | null patterns = 100 -- No issues found - | otherwise = max 0 $ 100 - fromIntegral (length patterns) * 10 - --- | Placeholder for AST parsing (would use tree-sitter in real impl) -parseSourceToAnalysis :: Text -> SourceAnalysis -parseSourceToAnalysis _source = SourceAnalysis - { saLoops = [] - , saIOCalls = [] - , saFunctionCalls = [] - , saSpinLocks = [] - } diff --git a/bots/sustainabot/analyzers/code-haskell/src/Eco/Pareto.hs b/bots/sustainabot/analyzers/code-haskell/src/Eco/Pareto.hs deleted file mode 100644 index 226bf59e..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Eco/Pareto.hs +++ /dev/null @@ -1,150 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- SPDX-FileCopyrightText: 2024-2025 hyperpolymath - -{-# LANGUAGE OverloadedStrings #-} - --- | Pareto optimality analysis for multi-objective optimization --- --- Implements Pareto frontier calculation and dominance checking --- for balancing competing software objectives (performance, memory, --- energy, maintainability, etc.) -module Eco.Pareto - ( -- * Pareto Analysis - calculateParetoFrontier - , isDominated - , paretoDistance - , findDominatingPoints - - -- * Objectives - , Objective(..) - , ObjectiveDirection(..) - , standardObjectives - - -- * Trade-off Analysis - , analyzeTradeoffs - , suggestImprovements - ) where - -import Types.Metrics -import Data.List (sortBy, nubBy) -import Data.Ord (comparing) -import Data.Text (Text) -import qualified Data.Text as T - --- | Direction of optimization for an objective -data ObjectiveDirection - = Minimize -- ^ Lower is better (e.g., latency, memory) - | Maximize -- ^ Higher is better (e.g., throughput, coverage) - deriving (Show, Eq) - --- | Definition of an optimization objective -data Objective = Objective - { objName :: !Text - , objDirection :: !ObjectiveDirection - , objWeight :: !Double -- ^ Importance weight (0-1) - } deriving (Show, Eq) - --- | Standard objectives for Oikos Bot analysis -standardObjectives :: [Objective] -standardObjectives = - [ Objective "carbon_intensity" Minimize 0.20 - , Objective "energy_consumption" Minimize 0.15 - , Objective "execution_time" Minimize 0.15 - , Objective "memory_usage" Minimize 0.10 - , Objective "maintainability" Maximize 0.15 - , Objective "test_coverage" Maximize 0.10 - , Objective "technical_debt" Minimize 0.15 - ] - --- | Calculate the Pareto frontier from a set of solutions --- --- A solution is Pareto-optimal if no other solution dominates it --- (i.e., no other solution is better in all objectives) -calculateParetoFrontier :: [Objective] -> [[Double]] -> ParetoFrontier -calculateParetoFrontier objectives solutions = ParetoFrontier - { pfPoints = map toPoint indexed - , pfObjectives = map objName objectives - , pfCurrentPos = head $ map toPoint indexed -- First point as current - , pfDistance = 0 -- Will be calculated relative to frontier - } - where - indexed = zip solutions [0..] - - toPoint (values, _idx) = ParetoPoint - { ppDimensions = values - , ppLabels = map objName objectives - , ppDominated = isDominated objectives values solutions - } - --- | Check if a solution is dominated by any other solution -isDominated :: [Objective] -> [Double] -> [[Double]] -> Bool -isDominated objectives point allPoints = - any (dominates objectives point) (filter (/= point) allPoints) - --- | Check if point A dominates point B --- A dominates B if A is at least as good in all objectives --- and strictly better in at least one -dominates :: [Objective] -> [Double] -> [Double] -> Bool -dominates objectives pointA pointB = - allAtLeastAsGood && anyStrictlyBetter - where - comparisons = zipWith3 compareObjective objectives pointA pointB - - allAtLeastAsGood = all (>= EQ) comparisons - anyStrictlyBetter = any (== GT) comparisons - - compareObjective obj a b = case objDirection obj of - Minimize -> compare b a -- Lower is better, so flip - Maximize -> compare a b -- Higher is better - --- | Calculate distance from a point to the Pareto frontier -paretoDistance :: [Objective] -> [Double] -> [[Double]] -> Double -paretoDistance objectives point frontier = - minimum $ map (euclideanDistance point) nonDominatedPoints - where - nonDominatedPoints = filter (not . flip (isDominated objectives) frontier) frontier - - euclideanDistance a b = sqrt $ sum $ zipWith (\x y -> (x - y) ^ (2 :: Int)) a b - --- | Find all points that dominate the given point -findDominatingPoints :: [Objective] -> [Double] -> [[Double]] -> [[Double]] -findDominatingPoints objectives point allPoints = - filter (\p -> dominates objectives p point) (filter (/= point) allPoints) - --- | Analyze trade-offs between objectives -analyzeTradeoffs :: [Objective] -> [Double] -> [(Text, Text, Double)] -analyzeTradeoffs objectives values = - [ (objName o1, objName o2, correlation v1 v2) - | (o1, v1) <- zip objectives values - , (o2, v2) <- zip objectives values - , objName o1 < objName o2 -- Avoid duplicates - ] - where - -- Simplified correlation (would use actual correlation in practice) - correlation a b = (a * b) / (abs a + abs b + 0.001) - --- | Suggest improvements to move toward Pareto frontier -suggestImprovements :: [Objective] -> [Double] -> [[Double]] -> [Text] -suggestImprovements objectives current frontier = - concatMap suggest $ zip objectives current - where - frontierMeans = map mean $ transpose frontier - - suggest (obj, val) = - let targetIdx = findObjIndex (objName obj) objectives - target = frontierMeans !! targetIdx - diff = case objDirection obj of - Minimize -> val - target - Maximize -> target - val - in if diff > 0.1 -- Threshold for suggesting - then [T.concat [objName obj, ": improve by ", T.pack (show (round (diff * 100) :: Int)), "%"]] - else [] - - findObjIndex name objs = - length $ takeWhile ((/= name) . objName) objs - - mean xs = sum xs / fromIntegral (length xs) - - transpose [] = [] - transpose ([] : _) = [] - transpose xs = map head xs : transpose (map tail xs) diff --git a/bots/sustainabot/analyzers/code-haskell/src/Eco/Resource.hs b/bots/sustainabot/analyzers/code-haskell/src/Eco/Resource.hs deleted file mode 100644 index ce85bca9..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Eco/Resource.hs +++ /dev/null @@ -1,170 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | Resource utilization analysis --- SPDX-License-Identifier: MPL-2.0 --- --- Analyzes memory, CPU, and I/O efficiency patterns in code. -module Eco.Resource - ( analyzeResourceUsage - , estimateMemoryEfficiency - , estimateCPUEfficiency - , estimateIOEfficiency - , ResourceConfig(..) - , defaultResourceConfig - ) where - -import Types.Metrics -import Data.Text (Text) -import qualified Data.Text as T - --- | Configuration for resource analysis -data ResourceConfig = ResourceConfig - { rcMaxAllocationsPerFunction :: !Int -- ^ Threshold for allocation warnings - , rcMaxCopyOperations :: !Int -- ^ Threshold for copy warnings - , rcMaxFileHandles :: !Int -- ^ Max concurrent file handles - , rcPoolingThreshold :: !Int -- ^ When to suggest connection pooling - } deriving (Show, Eq) - --- | Default resource configuration -defaultResourceConfig :: ResourceConfig -defaultResourceConfig = ResourceConfig - { rcMaxAllocationsPerFunction = 50 - , rcMaxCopyOperations = 20 - , rcMaxFileHandles = 100 - , rcPoolingThreshold = 5 - } - --- | Resource usage data from static analysis -data ResourceAnalysis = ResourceAnalysis - { raAllocations :: ![AllocationInfo] - , raCopyOperations :: ![CopyInfo] - , raFileOperations :: ![FileOpInfo] - , raNetworkCalls :: ![NetworkInfo] - } deriving (Show, Eq) - -data AllocationInfo = AllocationInfo - { allocLocation :: !CodeLocation - , allocSize :: !(Maybe Int) -- ^ Estimated size in bytes - , allocType :: !Text -- ^ heap, stack, pool - , allocFreed :: !Bool -- ^ Is it properly freed? - } deriving (Show, Eq) - -data CopyInfo = CopyInfo - { copyLocation :: !CodeLocation - , copySize :: !(Maybe Int) - , copyAvoidable :: !Bool -- ^ Could use reference instead? - } deriving (Show, Eq) - -data FileOpInfo = FileOpInfo - { fileLocation :: !CodeLocation - , fileOpType :: !Text -- ^ read, write, open, close - , fileClosed :: !Bool -- ^ Is handle properly closed? - } deriving (Show, Eq) - -data NetworkInfo = NetworkInfo - { netLocation :: !CodeLocation - , netPooled :: !Bool -- ^ Uses connection pooling? - , netKeptAlive :: !Bool -- ^ Uses keep-alive? - } deriving (Show, Eq) - --- | Analyze code for resource efficiency -analyzeResourceUsage :: ResourceConfig -> ResourceAnalysis -> ResourceScore -analyzeResourceUsage config analysis = ResourceScore - { memoryEfficiency = estimateMemoryEfficiency config analysis - , cpuEfficiency = estimateCPUEfficiency config analysis - , ioEfficiency = estimateIOEfficiency config analysis - , resourceNormalized = overallScore - } - where - mem = estimateMemoryEfficiency config analysis - cpu = estimateCPUEfficiency config analysis - io = estimateIOEfficiency config analysis - overallScore = (mem + cpu + io) / 3 - --- | Estimate memory efficiency (0-100) -estimateMemoryEfficiency :: ResourceConfig -> ResourceAnalysis -> Double -estimateMemoryEfficiency config analysis = max 0 $ 100 - penalties - where - allocs = raAllocations analysis - - -- Penalty for excessive allocations - allocPenalty = if length allocs > rcMaxAllocationsPerFunction config - then fromIntegral (length allocs - rcMaxAllocationsPerFunction config) - else 0 - - -- Penalty for unfreed allocations (memory leaks) - leakPenalty = fromIntegral $ length $ filter (not . allocFreed) allocs - - -- Penalty for avoidable copies - copyPenalty = fromIntegral $ length $ filter copyAvoidable $ raCopyOperations analysis - - penalties = allocPenalty * 0.5 + leakPenalty * 10 + copyPenalty * 2 - --- | Estimate CPU efficiency (0-100) -estimateCPUEfficiency :: ResourceConfig -> ResourceAnalysis -> Double -estimateCPUEfficiency config analysis = max 0 $ 100 - penalties - where - -- Excessive copies waste CPU - copyCount = length $ raCopyOperations analysis - copyPenalty = if copyCount > rcMaxCopyOperations config - then fromIntegral (copyCount - rcMaxCopyOperations config) * 2 - else 0 - - -- Large allocations cause memory pressure - largeAllocs = filter (maybe False (> 1024 * 1024) . allocSize) $ raAllocations analysis - allocPenalty = fromIntegral (length largeAllocs) * 5 - - penalties = copyPenalty + allocPenalty - --- | Estimate I/O efficiency (0-100) -estimateIOEfficiency :: ResourceConfig -> ResourceAnalysis -> Double -estimateIOEfficiency config analysis = max 0 $ 100 - penalties - where - fileOps = raFileOperations analysis - netOps = raNetworkCalls analysis - - -- Penalty for unclosed file handles - unclosedPenalty = fromIntegral $ length $ filter (not . fileClosed) fileOps - - -- Penalty for non-pooled connections - unpooledConnections = filter (not . netPooled) netOps - poolPenalty = if length unpooledConnections > rcPoolingThreshold config - then fromIntegral (length unpooledConnections) * 5 - else 0 - - -- Penalty for connections without keep-alive - noKeepalivePenalty = fromIntegral $ length $ filter (not . netKeptAlive) netOps - - penalties = unclosedPenalty * 10 + poolPenalty + noKeepalivePenalty * 2 - --- | Analyze resource issues and generate suggestions -suggestResourceImprovements :: ResourceConfig -> ResourceAnalysis -> [Text] -suggestResourceImprovements config analysis = concat - [ memoryIssues - , ioIssues - , networkIssues - ] - where - memoryIssues = - [ "Consider using object pooling to reduce allocations" - | length (raAllocations analysis) > rcMaxAllocationsPerFunction config - ] ++ - [ "Memory leak detected - ensure all allocations are freed" - | any (not . allocFreed) (raAllocations analysis) - ] ++ - [ "Avoid unnecessary copies - use references where possible" - | any copyAvoidable (raCopyOperations analysis) - ] - - ioIssues = - [ "File handle leak detected - ensure all handles are closed" - | any (not . fileClosed) (raFileOperations analysis) - ] - - networkIssues = - [ "Consider using connection pooling for better performance" - | length (filter (not . netPooled) (raNetworkCalls analysis)) > rcPoolingThreshold config - ] ++ - [ "Enable HTTP keep-alive for reduced connection overhead" - | any (not . netKeptAlive) (raNetworkCalls analysis) - ] diff --git a/bots/sustainabot/analyzers/code-haskell/src/Quality/Complexity.hs b/bots/sustainabot/analyzers/code-haskell/src/Quality/Complexity.hs deleted file mode 100644 index 198069f9..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Quality/Complexity.hs +++ /dev/null @@ -1,144 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | Cyclomatic and cognitive complexity analysis --- SPDX-License-Identifier: MPL-2.0 --- --- Implements complexity metrics based on: --- - McCabe's Cyclomatic Complexity --- - Cognitive Complexity (SonarSource) --- - Halstead complexity measures -module Quality.Complexity - ( analyzeComplexity - , calculateCyclomatic - , calculateCognitive - , calculateMaintainability - , ComplexityConfig(..) - , defaultComplexityConfig - ) where - -import Types.Metrics -import Data.Text (Text) -import qualified Data.Text as T - --- | Configuration for complexity analysis -data ComplexityConfig = ComplexityConfig - { ccMaxCyclomatic :: !Int -- ^ Maximum acceptable cyclomatic complexity - , ccMaxCognitive :: !Int -- ^ Maximum acceptable cognitive complexity - , ccMinMaintainability :: !Double -- ^ Minimum maintainability index - , ccMaxFunctionLines :: !Int -- ^ Maximum lines per function - } deriving (Show, Eq) - --- | Default complexity thresholds -defaultComplexityConfig :: ComplexityConfig -defaultComplexityConfig = ComplexityConfig - { ccMaxCyclomatic = 10 -- Industry standard threshold - , ccMaxCognitive = 15 -- SonarSource recommendation - , ccMinMaintainability = 20 -- Below this is very difficult to maintain - , ccMaxFunctionLines = 50 -- Functions should be concise - } - --- | Simplified AST for complexity analysis -data FunctionAST = FunctionAST - { astName :: !Text - , astLocation :: !CodeLocation - , astBranches :: !Int -- ^ if, switch, ternary - , astLoops :: !Int -- ^ for, while, do-while - , astNesting :: !Int -- ^ Maximum nesting depth - , astOperators :: !Int -- ^ Distinct operators (Halstead) - , astOperands :: !Int -- ^ Distinct operands (Halstead) - , astTotalOps :: !Int -- ^ Total operator occurrences - , astTotalOpnds :: !Int -- ^ Total operand occurrences - , astLines :: !Int -- ^ Lines of code - } deriving (Show, Eq) - --- | Analyze complexity of code -analyzeComplexity :: ComplexityConfig -> [FunctionAST] -> ComplexityMetrics -analyzeComplexity config functions = ComplexityMetrics - { cmCyclomatic = maxCyclomatic - , cmCognitive = maxCognitive - , cmLinesOfCode = totalLines - , cmMaintainability = avgMaintainability - , cmHotspots = hotspots - } - where - cyclomatics = map (calculateCyclomatic config) functions - cognitives = map (calculateCognitive config) functions - maintainabilities = map (calculateMaintainability config) functions - - maxCyclomatic = if null cyclomatics then 0 else maximum cyclomatics - maxCognitive = if null cognitives then 0 else maximum cognitives - totalLines = sum $ map astLines functions - avgMaintainability = if null maintainabilities - then 100 - else sum maintainabilities / fromIntegral (length maintainabilities) - - -- Hotspots are functions exceeding thresholds - hotspots = map astLocation $ filter isHotspot functions - - isHotspot fn = - calculateCyclomatic config fn > ccMaxCyclomatic config || - calculateCognitive config fn > ccMaxCognitive config || - astLines fn > ccMaxFunctionLines config - --- | Calculate McCabe's Cyclomatic Complexity --- CC = E - N + 2P --- Simplified: CC = branches + loops + 1 -calculateCyclomatic :: ComplexityConfig -> FunctionAST -> Int -calculateCyclomatic _config fn = - astBranches fn + astLoops fn + 1 - --- | Calculate Cognitive Complexity --- Based on SonarSource's metric: --- - +1 for each control flow break --- - Additional +1 for each level of nesting -calculateCognitive :: ComplexityConfig -> FunctionAST -> Int -calculateCognitive _config fn = - baseComplexity + nestingPenalty - where - baseComplexity = astBranches fn + astLoops fn - nestingPenalty = if astNesting fn > 2 - then (astNesting fn - 2) * (astBranches fn + astLoops fn) - else 0 - --- | Calculate Maintainability Index --- MI = 171 - 5.2 * ln(V) - 0.23 * CC - 16.2 * ln(LOC) --- Where V = Halstead Volume -calculateMaintainability :: ComplexityConfig -> FunctionAST -> Double -calculateMaintainability config fn = - max 0 $ min 100 $ scaled - where - cc = fromIntegral $ calculateCyclomatic config fn - loc = fromIntegral $ max 1 $ astLines fn - - -- Halstead Volume = N * log2(n) - -- N = total operators + operands - -- n = distinct operators + operands - totalN = fromIntegral $ astTotalOps fn + astTotalOpnds fn - distinctN = fromIntegral $ max 1 $ astOperators fn + astOperands fn - volume = if totalN > 0 && distinctN > 1 - then totalN * logBase 2 distinctN - else 1 - - -- Original Maintainability Index formula - mi = 171 - 5.2 * log volume - 0.23 * cc - 16.2 * log loc - - -- Scale to 0-100 - scaled = mi * 100 / 171 - --- | Suggest complexity improvements -suggestComplexityImprovements :: ComplexityConfig -> [FunctionAST] -> [Text] -suggestComplexityImprovements config functions = concatMap suggest functions - where - suggest fn = concat - [ [ T.concat ["Function '", astName fn, "': Extract smaller functions (CC=", T.pack (show cc), ")"] - | cc > ccMaxCyclomatic config - ] - , [ T.concat ["Function '", astName fn, "': Reduce nesting depth (", T.pack (show $ astNesting fn), " levels)"] - | astNesting fn > 3 - ] - , [ T.concat ["Function '", astName fn, "': Split function (", T.pack (show $ astLines fn), " lines)"] - | astLines fn > ccMaxFunctionLines config - ] - ] - where - cc = calculateCyclomatic config fn diff --git a/bots/sustainabot/analyzers/code-haskell/src/Quality/Coupling.hs b/bots/sustainabot/analyzers/code-haskell/src/Quality/Coupling.hs deleted file mode 100644 index 7735ffdd..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Quality/Coupling.hs +++ /dev/null @@ -1,148 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | Coupling and cohesion analysis --- SPDX-License-Identifier: MPL-2.0 --- --- Implements Robert C. Martin's instability/abstractness metrics --- and LCOM (Lack of Cohesion of Methods). -module Quality.Coupling - ( analyzeCoupling - , calculateInstability - , calculateAbstractness - , calculateDistance - , CouplingConfig(..) - , defaultCouplingConfig - , ModuleDependency(..) - ) where - -import Types.Metrics -import Data.Text (Text) -import qualified Data.Text as T -import Data.List (nub) - --- | Configuration for coupling analysis -data CouplingConfig = CouplingConfig - { cpMaxAfferent :: !Int -- ^ Max incoming dependencies - , cpMaxEfferent :: !Int -- ^ Max outgoing dependencies - , cpMaxInstability :: !Double -- ^ Max instability (0-1) - , cpMaxDistance :: !Double -- ^ Max distance from main sequence - } deriving (Show, Eq) - --- | Default coupling thresholds -defaultCouplingConfig :: CouplingConfig -defaultCouplingConfig = CouplingConfig - { cpMaxAfferent = 20 - , cpMaxEfferent = 10 - , cpMaxInstability = 0.8 - , cpMaxDistance = 0.3 - } - --- | Represents a module's dependency information -data ModuleDependency = ModuleDependency - { mdName :: !Text - , mdImports :: ![Text] -- ^ Modules this module imports - , mdExports :: ![Text] -- ^ Public interface items - , mdAbstracts :: !Int -- ^ Abstract types/interfaces - , mdConcretes :: !Int -- ^ Concrete implementations - , mdClasses :: !Int -- ^ Number of classes/modules - } deriving (Show, Eq) - --- | Calculate coupling metrics for a set of modules -analyzeCoupling :: CouplingConfig -> [ModuleDependency] -> CouplingScore -analyzeCoupling config modules = CouplingScore - { csAfferent = totalAfferent - , csEfferent = totalEfferent - , csInstability = avgInstability - , csAbstractness = avgAbstractness - , csDistance = avgDistance - } - where - -- Calculate afferent coupling (incoming) for each module - afferentCounts = map (countAfferent modules) modules - totalAfferent = sum afferentCounts - - -- Calculate efferent coupling (outgoing) for each module - efferentCounts = map (length . mdImports) modules - totalEfferent = sum efferentCounts - - -- Calculate metrics per module - instabilities = zipWith (\a e -> calculateInstability a e) afferentCounts efferentCounts - abstractnesses = map calculateAbstractnessForModule modules - distances = zipWith calculateDistance instabilities abstractnesses - - -- Averages - avgInstability = safeAverage instabilities - avgAbstractness = safeAverage abstractnesses - avgDistance = safeAverage distances - - safeAverage xs = if null xs then 0 else sum xs / fromIntegral (length xs) - --- | Count how many modules import this module -countAfferent :: [ModuleDependency] -> ModuleDependency -> Int -countAfferent allModules targetModule = - length $ filter importsTarget allModules - where - importsTarget m = mdName targetModule `elem` mdImports m - --- | Calculate instability: I = Ce / (Ca + Ce) --- 0 = stable (many dependents, few dependencies) --- 1 = unstable (few dependents, many dependencies) -calculateInstability :: Int -> Int -> Double -calculateInstability afferent efferent - | afferent + efferent == 0 = 0 - | otherwise = fromIntegral efferent / fromIntegral (afferent + efferent) - --- | Calculate abstractness for a module: A = abstracts / (abstracts + concretes) -calculateAbstractnessForModule :: ModuleDependency -> Double -calculateAbstractnessForModule m - | total == 0 = 0 - | otherwise = fromIntegral (mdAbstracts m) / fromIntegral total - where - total = mdAbstracts m + mdConcretes m - --- | Calculate abstractness: A = abstract_classes / total_classes -calculateAbstractness :: Int -> Int -> Double -calculateAbstractness abstractCount totalCount - | totalCount == 0 = 0 - | otherwise = fromIntegral abstractCount / fromIntegral totalCount - --- | Calculate distance from main sequence: D = |A + I - 1| --- 0 = on the main sequence (ideal) --- Closer to 1 = either "zone of pain" or "zone of uselessness" -calculateDistance :: Double -> Double -> Double -calculateDistance instability abstractness = - abs (abstractness + instability - 1) - --- | Analyze LCOM (Lack of Cohesion of Methods) --- LCOM = 1 - (sum of method intersections / (m * a)) --- where m = methods, a = attributes -analyzeLCOM :: ModuleDependency -> Double -analyzeLCOM m - | mdClasses m == 0 = 0 - | otherwise = 1.0 - cohesionEstimate - where - -- Simplified estimate based on exports vs total - exportCount = length $ mdExports m - totalItems = mdAbstracts m + mdConcretes m - cohesionEstimate = if totalItems == 0 - then 1.0 - else fromIntegral exportCount / fromIntegral totalItems - --- | Suggest coupling improvements -suggestCouplingImprovements :: CouplingConfig -> [ModuleDependency] -> [Text] -suggestCouplingImprovements config modules = concatMap suggest modules - where - suggest m = concat - [ [ T.concat ["Module '", mdName m, "': Too many dependencies (", T.pack (show deps), ")"] - | deps > cpMaxEfferent config - ] - , [ T.concat ["Module '", mdName m, "': Consider introducing abstractions"] - | calculateAbstractnessForModule m < 0.1 && mdClasses m > 5 - ] - , [ T.concat ["Module '", mdName m, "': High instability - add abstractions or reduce dependencies"] - | calculateInstability afferent (length $ mdImports m) > cpMaxInstability config - ] - ] - where - deps = length $ mdImports m - afferent = countAfferent modules m diff --git a/bots/sustainabot/analyzers/code-haskell/src/Quality/Coverage.hs b/bots/sustainabot/analyzers/code-haskell/src/Quality/Coverage.hs deleted file mode 100644 index eb9bec2e..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Quality/Coverage.hs +++ /dev/null @@ -1,166 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | Test coverage analysis --- SPDX-License-Identifier: MPL-2.0 --- --- Analyzes test coverage data and identifies uncovered hotspots. -module Quality.Coverage - ( analyzeCoverage - , parseCoverageReport - , identifyUncoveredHotspots - , CoverageConfig(..) - , defaultCoverageConfig - , CoverageFormat(..) - ) where - -import Types.Metrics -import Data.Text (Text) -import qualified Data.Text as T - --- | Configuration for coverage analysis -data CoverageConfig = CoverageConfig - { covMinLine :: !Double -- ^ Minimum acceptable line coverage - , covMinBranch :: !Double -- ^ Minimum acceptable branch coverage - , covMinFunction :: !Double -- ^ Minimum acceptable function coverage - , covHotspotThreshold :: !Int -- ^ Lines of uncovered code to flag - } deriving (Show, Eq) - --- | Default coverage thresholds -defaultCoverageConfig :: CoverageConfig -defaultCoverageConfig = CoverageConfig - { covMinLine = 80.0 - , covMinBranch = 70.0 - , covMinFunction = 90.0 - , covHotspotThreshold = 10 - } - --- | Supported coverage report formats -data CoverageFormat - = Lcov -- ^ LCOV format - | Cobertura -- ^ Cobertura XML - | JaCoCo -- ^ JaCoCo XML - | SimpleCov -- ^ Ruby SimpleCov JSON - | CoverageJSON -- ^ Generic JSON format - deriving (Show, Eq) - --- | Coverage data for a single file -data FileCoverage = FileCoverage - { fcPath :: !Text - , fcTotalLines :: !Int - , fcCoveredLines :: !Int - , fcTotalBranches :: !Int - , fcCoveredBranches :: !Int - , fcTotalFunctions :: !Int - , fcCoveredFunctions :: !Int - , fcUncoveredRanges :: ![(Int, Int)] -- ^ (start, end) line ranges - } deriving (Show, Eq) - --- | Aggregate coverage data -data CoverageData = CoverageData - { cdFiles :: ![FileCoverage] - , cdTotalLines :: !Int - , cdCoveredLines :: !Int - , cdTotalBranches :: !Int - , cdCoveredBranches :: !Int - , cdTotalFunctions :: !Int - , cdCoveredFunctions :: !Int - } deriving (Show, Eq) - --- | Analyze coverage data -analyzeCoverage :: CoverageConfig -> CoverageData -> CoverageAnalysis -analyzeCoverage config coverage = CoverageAnalysis - { caLineCoverage = lineCov - , caBranchCoverage = branchCov - , caFunctionCoverage = funcCov - , caUncoveredHotspots = hotspots - } - where - lineCov = percentage (cdCoveredLines coverage) (cdTotalLines coverage) - branchCov = percentage (cdCoveredBranches coverage) (cdTotalBranches coverage) - funcCov = percentage (cdCoveredFunctions coverage) (cdTotalFunctions coverage) - - hotspots = identifyUncoveredHotspots config (cdFiles coverage) - - percentage num denom - | denom == 0 = 100.0 - | otherwise = 100.0 * fromIntegral num / fromIntegral denom - --- | Identify significant uncovered code regions -identifyUncoveredHotspots :: CoverageConfig -> [FileCoverage] -> [CodeLocation] -identifyUncoveredHotspots config files = - concatMap findHotspots files - where - findHotspots fc = - [ CodeLocation - { locFile = fcPath fc - , locLine = startLine - , locColumn = 1 - , locSnippet = Just $ T.concat ["Lines ", T.pack (show startLine), "-", T.pack (show endLine)] - } - | (startLine, endLine) <- fcUncoveredRanges fc - , endLine - startLine >= covHotspotThreshold config - ] - --- | Parse coverage report (placeholder - would use aeson in real impl) -parseCoverageReport :: CoverageFormat -> Text -> Either Text CoverageData -parseCoverageReport format _content = - case format of - Lcov -> Right emptyCoverage - Cobertura -> Right emptyCoverage - JaCoCo -> Right emptyCoverage - SimpleCov -> Right emptyCoverage - CoverageJSON -> Right emptyCoverage - where - emptyCoverage = CoverageData - { cdFiles = [] - , cdTotalLines = 0 - , cdCoveredLines = 0 - , cdTotalBranches = 0 - , cdCoveredBranches = 0 - , cdTotalFunctions = 0 - , cdCoveredFunctions = 0 - } - --- | Suggest coverage improvements -suggestCoverageImprovements :: CoverageConfig -> CoverageAnalysis -> [Text] -suggestCoverageImprovements config analysis = concat - [ lineSuggestions - , branchSuggestions - , functionSuggestions - , hotspotSuggestions - ] - where - lineSuggestions = - [ T.concat ["Line coverage (", T.pack (show $ round $ caLineCoverage analysis :: Int), - "%) below threshold (", T.pack (show $ round $ covMinLine config :: Int), "%)"] - | caLineCoverage analysis < covMinLine config - ] - - branchSuggestions = - [ T.concat ["Branch coverage (", T.pack (show $ round $ caBranchCoverage analysis :: Int), - "%) below threshold (", T.pack (show $ round $ covMinBranch config :: Int), "%)"] - | caBranchCoverage analysis < covMinBranch config - ] - - functionSuggestions = - [ T.concat ["Function coverage (", T.pack (show $ round $ caFunctionCoverage analysis :: Int), - "%) below threshold (", T.pack (show $ round $ covMinFunction config :: Int), "%)"] - | caFunctionCoverage analysis < covMinFunction config - ] - - hotspotSuggestions = - [ T.concat ["Add tests for uncovered region: ", maybe "" id $ locSnippet loc, - " in ", locFile loc] - | loc <- take 5 $ caUncoveredHotspots analysis - ] - --- | Calculate coverage score (0-100) -calculateCoverageScore :: CoverageConfig -> CoverageAnalysis -> Double -calculateCoverageScore config analysis = - weightedAvg - [ (caLineCoverage analysis, 0.5) - , (caBranchCoverage analysis, 0.3) - , (caFunctionCoverage analysis, 0.2) - ] - where - weightedAvg pairs = sum [v * w | (v, w) <- pairs] / sum [w | (_, w) <- pairs] diff --git a/bots/sustainabot/analyzers/code-haskell/src/Quality/Debt.hs b/bots/sustainabot/analyzers/code-haskell/src/Quality/Debt.hs deleted file mode 100644 index 975ba88b..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Quality/Debt.hs +++ /dev/null @@ -1,167 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | Technical debt estimation --- SPDX-License-Identifier: MPL-2.0 --- --- Estimates technical debt using SQALE methodology. -module Quality.Debt - ( analyzeDebt - , estimateDebtPrincipal - , estimateDebtInterest - , calculateDebtRatio - , DebtConfig(..) - , defaultDebtConfig - , DebtType(..) - ) where - -import Types.Metrics -import Data.Text (Text) -import qualified Data.Text as T - --- | Configuration for debt analysis -data DebtConfig = DebtConfig - { dcHoursPerComplexityPoint :: !Double -- ^ Hours to fix per complexity point - , dcHoursPerDuplication :: !Double -- ^ Hours to fix per duplicated block - , dcHoursPerCodeSmell :: !Double -- ^ Hours to fix per code smell - , dcHoursPerSecurityIssue :: !Double -- ^ Hours to fix per security issue - , dcInterestRate :: !Double -- ^ Annual interest rate on debt - } deriving (Show, Eq) - --- | Default debt estimation configuration -defaultDebtConfig :: DebtConfig -defaultDebtConfig = DebtConfig - { dcHoursPerComplexityPoint = 0.5 - , dcHoursPerDuplication = 2.0 - , dcHoursPerCodeSmell = 1.0 - , dcHoursPerSecurityIssue = 4.0 - , dcInterestRate = 0.15 -- 15% annual "interest" - } - --- | Types of technical debt -data DebtType - = DesignDebt -- ^ Architectural issues - | CodeDebt -- ^ Code smells, complexity - | TestDebt -- ^ Missing or inadequate tests - | DocDebt -- ^ Missing documentation - | InfraDebt -- ^ Build/deploy issues - | SecurityDebt -- ^ Security vulnerabilities - deriving (Show, Eq) - --- | Debt indicator from code analysis -data DebtIndicator = DebtIndicator - { diType :: !DebtType - , diLocation :: !CodeLocation - , diSeverity :: !Double -- ^ 1-10 severity - , diDescription :: !Text - , diEffort :: !Double -- ^ Estimated hours to fix - } deriving (Show, Eq) - --- | Code analysis results for debt estimation -data CodeAnalysisForDebt = CodeAnalysisForDebt - { cadComplexityScore :: !Int -- ^ Total cyclomatic complexity - , cadDuplicateBlocks :: !Int -- ^ Number of duplicate code blocks - , cadCodeSmells :: ![DebtIndicator] - , cadSecurityIssues :: ![DebtIndicator] - , cadMissingTests :: ![Text] -- ^ Untested public functions - , cadMissingDocs :: ![Text] -- ^ Undocumented public items - , cadTotalLines :: !Int -- ^ Total lines of code - } deriving (Show, Eq) - --- | Analyze technical debt -analyzeDebt :: DebtConfig -> CodeAnalysisForDebt -> DebtEstimate -analyzeDebt config analysis = DebtEstimate - { debtPrincipal = principal - , debtInterest = interest - , debtRatio = ratio - , debtItems = items - } - where - principal = estimateDebtPrincipal config analysis - interest = estimateDebtInterest config principal - ratio = calculateDebtRatio config analysis principal - - items = map toDebtItem (cadCodeSmells analysis ++ cadSecurityIssues analysis) - - toDebtItem di = DebtItem - { diLocation = diLocation di - , diType = T.pack $ show $ diType di - , diSeverity = diSeverity di - , diDescription = diDescription di - } - --- | Estimate debt principal (total hours to fix all debt) -estimateDebtPrincipal :: DebtConfig -> CodeAnalysisForDebt -> Double -estimateDebtPrincipal config analysis = - complexityDebt + duplicationDebt + smellDebt + securityDebt + testDebt + docDebt - where - -- Complexity debt: excess complexity over threshold - excessComplexity = max 0 $ cadComplexityScore analysis - 100 - complexityDebt = fromIntegral excessComplexity * dcHoursPerComplexityPoint config - - -- Duplication debt - duplicationDebt = fromIntegral (cadDuplicateBlocks analysis) * dcHoursPerDuplication config - - -- Code smell debt - smellDebt = sum $ map diEffort $ cadCodeSmells analysis - - -- Security debt - securityDebt = sum $ map diEffort $ cadSecurityIssues analysis - - -- Test debt (assume 2 hours per missing test) - testDebt = fromIntegral (length $ cadMissingTests analysis) * 2.0 - - -- Documentation debt (assume 0.5 hours per missing doc) - docDebt = fromIntegral (length $ cadMissingDocs analysis) * 0.5 - --- | Estimate debt interest (ongoing cost of not fixing debt) -estimateDebtInterest :: DebtConfig -> Double -> Double -estimateDebtInterest config principal = - principal * dcInterestRate config - --- | Calculate debt ratio (debt / development effort) -calculateDebtRatio :: DebtConfig -> CodeAnalysisForDebt -> Double -> Double -calculateDebtRatio _config analysis principal - | developmentEffort == 0 = 0 - | otherwise = principal / developmentEffort - where - -- Estimate development effort from lines of code - -- Assume 10 LOC per hour average productivity - developmentEffort = fromIntegral (cadTotalLines analysis) / 10 - --- | Categorize debt by severity -categorizeDebt :: [DebtIndicator] -> [(Text, [DebtIndicator])] -categorizeDebt items = - [ ("Critical (>8)", filter (\i -> diSeverity i > 8) items) - , ("High (6-8)", filter (\i -> diSeverity i > 6 && diSeverity i <= 8) items) - , ("Medium (4-6)", filter (\i -> diSeverity i > 4 && diSeverity i <= 6) items) - , ("Low (<=4)", filter (\i -> diSeverity i <= 4) items) - ] - --- | Suggest debt remediation priorities -suggestDebtRemediation :: DebtConfig -> CodeAnalysisForDebt -> [Text] -suggestDebtRemediation config analysis = concat - [ securityFirst - , highRoi - , quickWins - ] - where - securityFirst = - [ "PRIORITY: Fix security issues first" - | not $ null $ cadSecurityIssues analysis - ] - - -- High ROI items (high severity, low effort) - highRoiItems = filter isHighRoi (cadCodeSmells analysis) - isHighRoi di = diSeverity di > 6 && diEffort di < 2 - - highRoi = - [ T.concat ["High ROI fix: ", diDescription item] - | item <- take 5 highRoiItems - ] - - -- Quick wins (< 1 hour) - quickWinItems = filter (\di -> diEffort di < 1) (cadCodeSmells analysis) - quickWins = - [ T.concat ["Quick win: ", diDescription item] - | item <- take 3 quickWinItems - ] diff --git a/bots/sustainabot/analyzers/code-haskell/src/Types/Metrics.hs b/bots/sustainabot/analyzers/code-haskell/src/Types/Metrics.hs deleted file mode 100644 index 42698c81..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Types/Metrics.hs +++ /dev/null @@ -1,201 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- SPDX-FileCopyrightText: 2024-2025 hyperpolymath - -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DerivingStrategies #-} - --- | Core metric types for Oikos Bot analysis -module Types.Metrics - ( -- * Ecological Metrics - EcoMetrics(..) - , CarbonScore(..) - , EnergyScore(..) - , ResourceScore(..) - - -- * Economic Metrics - , EconMetrics(..) - , ParetoPoint(..) - , ParetoFrontier(..) - , AllocationScore(..) - , DebtEstimate(..) - - -- * Quality Metrics - , QualityMetrics(..) - , ComplexityMetrics(..) - , CouplingScore(..) - , CoverageAnalysis(..) - - -- * Composite - , HealthIndex(..) - , AnalysisResult(..) - ) where - -import GHC.Generics (Generic) -import Data.Aeson (ToJSON, FromJSON) -import Data.Text (Text) - --- | Carbon intensity score based on SCI specification (ISO/IEC 21031:2024) --- Score normalized to 0-100, where 100 is best (lowest carbon) -data CarbonScore = CarbonScore - { carbonValue :: !Double -- ^ Raw carbon intensity estimate - , carbonNormalized :: !Double -- ^ Normalized 0-100 score - , carbonFactors :: ![Text] -- ^ Contributing factors - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Energy efficiency patterns score -data EnergyScore = EnergyScore - { energyPatterns :: ![EnergyPattern] - , energyNormalized :: !Double - , energyHotspots :: ![CodeLocation] - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Energy pattern classification -data EnergyPattern - = BusyWaiting CodeLocation - | IneffientLoop CodeLocation - | BlockingIO CodeLocation - | RedundantComputation CodeLocation - | EfficientPattern CodeLocation - deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Code location reference -data CodeLocation = CodeLocation - { locFile :: !Text - , locLine :: !Int - , locColumn :: !Int - , locSnippet :: !(Maybe Text) - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Resource utilization score -data ResourceScore = ResourceScore - { memoryEfficiency :: !Double - , cpuEfficiency :: !Double - , ioEfficiency :: !Double - , resourceNormalized :: !Double - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Combined ecological metrics -data EcoMetrics = EcoMetrics - { ecoCarbon :: !CarbonScore - , ecoEnergy :: !EnergyScore - , ecoResource :: !ResourceScore - , ecoScore :: !Double -- ^ Weighted composite 0-100 - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | A point in the Pareto frontier space -data ParetoPoint = ParetoPoint - { ppDimensions :: ![Double] -- ^ Values for each objective - , ppLabels :: ![Text] -- ^ Objective names - , ppDominated :: !Bool -- ^ Is this point dominated? - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | The Pareto frontier for multi-objective optimization -data ParetoFrontier = ParetoFrontier - { pfPoints :: ![ParetoPoint] - , pfObjectives :: ![Text] - , pfCurrentPos :: !ParetoPoint -- ^ Current solution position - , pfDistance :: !Double -- ^ Distance from frontier - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Allocative efficiency score -data AllocationScore = AllocationScore - { allocEfficiency :: !Double -- ^ 0-1 allocative efficiency - , allocWaste :: !Double -- ^ Estimated resource waste - , allocSuggestions :: ![Text] -- ^ Improvement suggestions - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Technical debt estimation -data DebtEstimate = DebtEstimate - { debtPrincipal :: !Double -- ^ Estimated hours to fix - , debtInterest :: !Double -- ^ Ongoing maintenance cost - , debtRatio :: !Double -- ^ Debt ratio (debt/value) - , debtItems :: ![DebtItem] - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Individual technical debt item -data DebtItem = DebtItem - { diLocation :: !CodeLocation - , diType :: !Text - , diSeverity :: !Double - , diDescription :: !Text - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Combined economic metrics -data EconMetrics = EconMetrics - { econPareto :: !ParetoFrontier - , econAllocation :: !AllocationScore - , econDebt :: !DebtEstimate - , econScore :: !Double -- ^ Weighted composite 0-100 - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Cyclomatic and cognitive complexity -data ComplexityMetrics = ComplexityMetrics - { cmCyclomatic :: !Int - , cmCognitive :: !Int - , cmLinesOfCode :: !Int - , cmMaintainability :: !Double - , cmHotspots :: ![CodeLocation] - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Coupling/cohesion analysis -data CouplingScore = CouplingScore - { csAfferent :: !Int -- ^ Incoming dependencies - , csEfferent :: !Int -- ^ Outgoing dependencies - , csInstability :: !Double -- ^ Instability metric - , csAbstractness :: !Double -- ^ Abstractness metric - , csDistance :: !Double -- ^ Distance from main sequence - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Test coverage analysis -data CoverageAnalysis = CoverageAnalysis - { caLineCoverage :: !Double - , caBranchCoverage :: !Double - , caFunctionCoverage :: !Double - , caUncoveredHotspots :: ![CodeLocation] - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Combined quality metrics -data QualityMetrics = QualityMetrics - { qualComplexity :: !ComplexityMetrics - , qualCoupling :: !CouplingScore - , qualCoverage :: !(Maybe CoverageAnalysis) - , qualScore :: !Double -- ^ Weighted composite 0-100 - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Composite health index -data HealthIndex = HealthIndex - { hiEco :: !Double -- ^ Ecological score weight - , hiEcon :: !Double -- ^ Economic score weight - , hiQuality :: !Double -- ^ Quality score weight - , hiTotal :: !Double -- ^ Weighted total 0-100 - , hiGrade :: !Text -- ^ A/B/C/D/F grade - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Complete analysis result -data AnalysisResult = AnalysisResult - { arEco :: !EcoMetrics - , arEcon :: !EconMetrics - , arQuality :: !QualityMetrics - , arHealth :: !HealthIndex - , arTimestamp :: !Text - , arVersion :: !Text - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) diff --git a/bots/sustainabot/analyzers/code-haskell/src/Types/Report.hs b/bots/sustainabot/analyzers/code-haskell/src/Types/Report.hs deleted file mode 100644 index 4035a697..00000000 --- a/bots/sustainabot/analyzers/code-haskell/src/Types/Report.hs +++ /dev/null @@ -1,191 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- SPDX-FileCopyrightText: 2024-2025 hyperpolymath - -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE OverloadedStrings #-} - --- | Report types for Oikos Bot analysis output -module Types.Report - ( -- * Report Types - AnalysisReport(..) - , ReportSection(..) - , ReportItem(..) - , Severity(..) - , Recommendation(..) - - -- * Report Generation - , emptyReport - , addSection - , addItem - , toJSON - , toMarkdown - ) where - -import GHC.Generics (Generic) -import Data.Aeson (ToJSON, FromJSON) -import qualified Data.Aeson as Aeson -import Data.Text (Text) -import qualified Data.Text as T -import Data.ByteString.Lazy (ByteString) -import Types.Metrics - --- | Severity level for findings -data Severity - = Info -- ^ Informational, no action needed - | Warning -- ^ Should be addressed eventually - | Critical -- ^ Must be addressed soon - | Blocker -- ^ Blocks deployment/merge - deriving stock (Show, Eq, Ord, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | A recommendation for improvement -data Recommendation = Recommendation - { recTitle :: !Text - , recDescription :: !Text - , recSeverity :: !Severity - , recLocation :: !(Maybe CodeLocation) - , recEstimate :: !(Maybe Double) -- ^ Estimated hours to fix - , recCategory :: !Text -- ^ eco, quality, security, etc. - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Individual report item (finding) -data ReportItem = ReportItem - { riTitle :: !Text - , riDescription :: !Text - , riSeverity :: !Severity - , riLocation :: !(Maybe CodeLocation) - , riRecommendations :: ![Recommendation] - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | A section of the report -data ReportSection = ReportSection - { rsName :: !Text - , rsScore :: !Double -- ^ Section score 0-100 - , rsItems :: ![ReportItem] - , rsSummary :: !Text - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Complete analysis report -data AnalysisReport = AnalysisReport - { arRepoName :: !Text - , arCommitSha :: !Text - , arBranch :: !Text - , arAnalyzedAt :: !Text -- ^ ISO 8601 timestamp - , arDuration :: !Double -- ^ Analysis duration in seconds - , arSections :: ![ReportSection] - , arMetrics :: !AnalysisResult - , arOverallScore :: !Double - , arGrade :: !Text -- ^ A/B/C/D/F - , arRecommendations :: ![Recommendation] - } deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - --- | Create empty report -emptyReport :: Text -> Text -> Text -> AnalysisReport -emptyReport repoName commitSha branch = AnalysisReport - { arRepoName = repoName - , arCommitSha = commitSha - , arBranch = branch - , arAnalyzedAt = "" - , arDuration = 0 - , arSections = [] - , arMetrics = defaultMetrics - , arOverallScore = 0 - , arGrade = "F" - , arRecommendations = [] - } - where - defaultMetrics = AnalysisResult - { arEco = defaultEcoMetrics - , arEcon = defaultEconMetrics - , arQuality = defaultQualityMetrics - , arHealth = HealthIndex 0 0 0 0 "F" - , arTimestamp = "" - , arVersion = "0.1.0" - } - - defaultEcoMetrics = EcoMetrics - { ecoCarbon = CarbonScore 0 0 [] - , ecoEnergy = EnergyScore [] 0 [] - , ecoResource = ResourceScore 0 0 0 0 - , ecoScore = 0 - } - - defaultEconMetrics = EconMetrics - { econPareto = ParetoFrontier [] [] (ParetoPoint [] [] False) 0 - , econAllocation = AllocationScore 0 0 [] - , econDebt = DebtEstimate 0 0 0 [] - , econScore = 0 - } - - defaultQualityMetrics = QualityMetrics - { qualComplexity = ComplexityMetrics 0 0 0 0 [] - , qualCoupling = CouplingScore 0 0 0 0 0 - , qualCoverage = Nothing - , qualScore = 0 - } - --- | Add a section to the report -addSection :: ReportSection -> AnalysisReport -> AnalysisReport -addSection section report = report - { arSections = arSections report ++ [section] - } - --- | Add an item to a section -addItem :: Text -> ReportItem -> AnalysisReport -> AnalysisReport -addItem sectionName item report = report - { arSections = map updateSection (arSections report) - } - where - updateSection s - | rsName s == sectionName = s { rsItems = rsItems s ++ [item] } - | otherwise = s - --- | Convert report to JSON -toJSON :: AnalysisReport -> ByteString -toJSON = Aeson.encode - --- | Convert report to Markdown -toMarkdown :: AnalysisReport -> Text -toMarkdown report = T.unlines - [ "# Oikos Bot Analysis Report" - , "" - , "## Summary" - , "" - , T.concat ["**Repository:** ", arRepoName report] - , T.concat ["**Commit:** ", T.take 8 (arCommitSha report)] - , T.concat ["**Branch:** ", arBranch report] - , T.concat ["**Score:** ", T.pack (show (round (arOverallScore report) :: Int)), "/100"] - , T.concat ["**Grade:** ", arGrade report] - , "" - , "## Sections" - , "" - , T.unlines (map sectionToMd (arSections report)) - , "## Recommendations" - , "" - , T.unlines (map recToMd (arRecommendations report)) - ] - where - sectionToMd s = T.unlines - [ T.concat ["### ", rsName s, " (", T.pack (show (round (rsScore s) :: Int)), "/100)"] - , "" - , rsSummary s - , "" - , T.unlines (map itemToMd (rsItems s)) - ] - - itemToMd i = T.unlines - [ T.concat ["- **", riTitle i, "** [", T.pack (show (riSeverity i)), "]"] - , T.concat [" ", riDescription i] - ] - - recToMd r = T.unlines - [ T.concat ["- **", recTitle r, "** [", T.pack (show (recSeverity r)), "]"] - , T.concat [" ", recDescription r] - , maybe "" (\h -> T.concat [" Estimate: ", T.pack (show h), " hours"]) (recEstimate r) - ] diff --git a/bots/sustainabot/analyzers/code-haskell/test/Main.hs b/bots/sustainabot/analyzers/code-haskell/test/Main.hs deleted file mode 100644 index 37619b08..00000000 --- a/bots/sustainabot/analyzers/code-haskell/test/Main.hs +++ /dev/null @@ -1,90 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | Test suite for eco-analyzer --- SPDX-License-Identifier: MPL-2.0 -module Main where - -import Test.Hspec -import Test.QuickCheck - -import Types.Metrics -import Eco.Carbon -import Eco.Pareto -import Quality.Complexity - -main :: IO () -main = hspec $ do - describe "Carbon Analysis" $ do - it "returns normalized score between 0 and 100" $ do - let config = defaultCarbonConfig - input = CodeAnalysisInput 10 2 50 10 1 - result = analyzeCarbonIntensity config input - carbonNormalized result `shouldSatisfy` (\x -> x >= 0 && x <= 100) - - it "higher complexity increases carbon score" $ do - let config = defaultCarbonConfig - lowInput = CodeAnalysisInput 5 1 10 5 1 - highInput = CodeAnalysisInput 50 4 200 100 1 - lowResult = analyzeCarbonIntensity config lowInput - highResult = analyzeCarbonIntensity config highInput - carbonNormalized lowResult `shouldSatisfy` (> carbonNormalized highResult) - - describe "Pareto Analysis" $ do - it "identifies dominated points correctly" $ do - let objectives = standardObjectives - point = [10, 20, 30, 40, 50, 60, 70] - betterPoint = [5, 10, 15, 20, 25, 30, 35] -- Better in all dimensions - points = [point, betterPoint] - isDominated objectives point points `shouldBe` True - isDominated objectives betterPoint points `shouldBe` False - - describe "Complexity Analysis" $ do - it "calculates cyclomatic complexity correctly" $ do - let config = defaultComplexityConfig - ast = FunctionAST - { astName = "test" - , astLocation = CodeLocation "" 1 1 Nothing - , astBranches = 5 - , astLoops = 3 - , astNesting = 2 - , astOperators = 10 - , astOperands = 20 - , astTotalOps = 50 - , astTotalOpnds = 100 - , astLines = 30 - } - calculateCyclomatic config ast `shouldBe` 9 -- 5 + 3 + 1 - - describe "Properties" $ do - it "normalized scores are always valid" $ property $ \(NonNegative n) -> - let score = normalizeScore (n :: Double) - in score >= 0 && score <= 100 - --- Placeholder for normalizeScore (would import from Eco.Carbon) -normalizeScore :: Double -> Double -normalizeScore rawScore - | rawScore <= 0.001 = 100 - | rawScore >= 1.0 = 0 - | otherwise = 100 * (1 - (log rawScore + 6.9) / 6.9) - --- Placeholder data types for tests -data FunctionAST = FunctionAST - { astName :: Text - , astLocation :: CodeLocation - , astBranches :: Int - , astLoops :: Int - , astNesting :: Int - , astOperators :: Int - , astOperands :: Int - , astTotalOps :: Int - , astTotalOpnds :: Int - , astLines :: Int - } - -data CodeAnalysisInput = CodeAnalysisInput - { caiComplexity :: Int - , caiLoopDepth :: Int - , caiAllocations :: Int - , caiIOOperations :: Int - , caiParallelism :: Int - } diff --git a/bots/sustainabot/bot-integration/.env.test b/bots/sustainabot/bot-integration/.env.test deleted file mode 100644 index 00ede3a4..00000000 --- a/bots/sustainabot/bot-integration/.env.test +++ /dev/null @@ -1,3 +0,0 @@ -PORT=3000 -BOT_MODE=advisor -ANALYSIS_ENDPOINT=http://localhost:8080/analyze diff --git a/bots/sustainabot/bot-integration/.tool-versions b/bots/sustainabot/bot-integration/.tool-versions deleted file mode 100644 index e3f5a48e..00000000 --- a/bots/sustainabot/bot-integration/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -deno 1.45.0 diff --git a/bots/sustainabot/bot-integration/MIGRATION-NOTES.md b/bots/sustainabot/bot-integration/MIGRATION-NOTES.md deleted file mode 100644 index 5ace058b..00000000 --- a/bots/sustainabot/bot-integration/MIGRATION-NOTES.md +++ /dev/null @@ -1,81 +0,0 @@ - -# ReScript → AffineScript Migration (issue #148) - -This subtree was migrated from ReScript to AffineScript on 2026-05-24 by a -**hand-port under explicit policy override** of issue #148's -"do-not-hand-port-ahead-of-the-compiler" rule. - -A follow-up static-review pass on 2026-05-25 reconciled the hand-port -against the canonical AffineScript grammar (`hyperpolymath/affinescript` -`lib/parser.mly`) and stdlib. See **"Static review changes (2026-05-25)"** -below. - -## Scope of migration - -- All `.res` / `.res.js` under `src/`, `src/tea/`, `bindings/`, and - `lib/ocaml/` were deleted. -- The `rescript-runtime/` vendored Belt runtime was deleted. -- `rescript.json` was deleted. -- `package.json` and `deno.json` were updated to target the AffineScript - toolchain instead of `rescript build` / `.res.js` outputs. -- The 11 in-scope `.res` files were re-expressed as `.affine`: - `Types`, `Config`, `Webhook`, `Analysis`, `GitHubAPI`, `GitHubApp`, - `Report`, `Router`, `Oikos`, `Main`, and `tea/ServerTea`. -- After the static-review pass, `tea/ServerTea.affine` was split into - three siblings: `tea/Cmd.affine`, `tea/Sub.affine`, `tea/Runtime.affine` - (the parser has no inline `module Name { ... }` form, so the original - three-module file was structurally invalid). - -## Static review changes (2026-05-25) - -The hand-port was originally written against the README spec only, -without access to the parser. Reconciling it against -`lib/parser.mly` + the actual stdlib produced these edits: - -| Change | Reason | -|---|---| -| `open Types` → `use Types::*;` | The lexer has no `open` keyword. Module-level imports use `use … ::*;` or `use … ::{Foo, Bar};`. | -| Each file now starts with `module ;` | The parser requires a single `module Name;` decl at the head of every file. | -| `Dict[K, V]` field types → `[(K, V)]` | `stdlib/dict.affine` represents a dict as an assoc list of pairs (no `Dict` type symbol exists). | -| `Json` references kept (no `<…>` args) | `stdlib/json.affine` exports `pub type Json = JNull \| JBool(Bool) \| …` — Json is unparameterised. | -| `Json.foo(…)` → `json::foo(…)` | Module path uses `::`, and the canonical module is lowercase `json`. Similarly `Dict.foo` → `dict::foo`, `Option.foo` → `option::foo`. | -| `String.foo(…)` → `String::foo(…)` etc. | Capital-cased modules (Http, Crypto, Bytes, Env, Console, Time, Base64, String, Int, Float, Exn, Array, Config) still need `::` for module access; `.` is field access only. | -| Top-level `let x: T = …` → `const x: T = …;` | `let`-bindings only appear inside block/fn bodies. Module-level bindings use `const`. | -| Top-level `let _ = main()` removed | There is no top-level imperative slot; the program entry is the `pub fn main` itself. | -| `let rec … and …` (ServerTea Runtime) → individual `fn` decls with state passed explicitly, stubbed | The parser has no mutually-recursive `let rec … and …` form. The dispatch loop reduces to a TODO stub awaiting the mechanical migrator. | -| Inline `module Cmd { … }` → separate `tea/Cmd.affine` (plus `Sub`/`Runtime`) | The parser only accepts file-level `module Name;` — inline module bodies are not in the grammar. | -| `\|>` pipe operator → nested calls / let-binding chains | The lexer has no pipe operator. Each pipeline was rewritten as nested function calls or stepwise `let` bindings. | -| Variant decls with leading `\|` allowed (kept consistent) | Both `type X = A \| B` and `type X = \| A \| B` parse; we kept the no-leading-pipe form. | - -The Json/Dict placeholder caveats from the original notes are gone: -`stdlib/json.affine` (the `Json` ADT, encoders/decoders, `stringify`) and -`stdlib/dict.affine` (the `[(String, V)]` assoc-list dict) have both -landed and are the canonical surfaces the rewritten files now point at. - -**Compiler check not run.** The remote environment has no OCaml / -`dune` / `affinescript` toolchain available, so `affinescript check -src/` could not be executed. The edits above were verified against -`lib/parser.mly` by hand only. - -## Known unbound surfaces - -A list of stdlib surfaces these files call that are **not yet bound** in -canonical AffineScript stdlib lives in `MISSING-EXTERNS.md`. The -mechanical migrator (affinescript#57 Phase 3) must re-point those calls -against whatever names the eventual bindings land with. - -## Re-port checklist (when affinescript#57 Phase 3 lands) - -- [ ] Run the mechanical migrator over the original `.res` (via - `git show :path/to/file.res`) and diff its - output against the hand-port. -- [ ] Resolve every `TODO(mechanical-migrator):` annotation. The - densest cluster is in `tea/Runtime.affine` (the dispatch loop - stub) and `src/Main.affine` / `src/Router.affine` (the HTTP - serve loop stubs). -- [ ] Re-bind every name listed in `MISSING-EXTERNS.md`. -- [ ] Re-narrow effect rows against the actual stdlib declarations - (the hand-port uses a conservative `-{IO}->` everywhere). -- [ ] Run `affinescript check src/` (in an environment that has the - compiler) and resolve any remaining errors that the hand-port - couldn't anticipate. diff --git a/bots/sustainabot/bot-integration/MISSING-EXTERNS.md b/bots/sustainabot/bot-integration/MISSING-EXTERNS.md deleted file mode 100644 index 7d5d9d0e..00000000 --- a/bots/sustainabot/bot-integration/MISSING-EXTERNS.md +++ /dev/null @@ -1,138 +0,0 @@ - -# Unbound stdlib surfaces - -Generated by the static-review pass on 2026-05-25 (see -`MIGRATION-NOTES.md`). Each entry lists a name called by the hand-port -that does not currently appear in `hyperpolymath/affinescript` -`stdlib/`. The mechanical migrator (affinescript#57 Phase 3) must -re-point each call against whatever the canonical binding lands with, -or supply an explicit `extern fn` declaration at the call site. - -The names are kept here (rather than mass-inventing `extern fn`s in the -.affine files) because guessing the wrong signature would silently -mask the gap once the real binding lands. - -## `Env` - -| Call | Used in | Likely target | -|---|---|---| -| `Env::get(name) -> Option` | Config, Oikos | Maps to the existing `getenv` builtin in `stdlib/io.affine`; awaits a thin `Env` module wrapper or a direct `use io::{getenv};` rewrite. | - -## `String` (extensions) - -`stdlib/string.affine` exists but does not cover these: - -| Call | Used in | -|---|---| -| `String::to_lower(s)` | Config — canonical builtin is `to_lowercase(s)`, no module prefix | -| `String::replace(s, needle, replacement)` | Webhook | -| `String::replace_regex(s, pattern, replacement)` | GitHubApp, Main | -| `String::replace_all(s, needle, replacement)` | GitHubApp, Report | -| `String::split(s, sep)` | Router — canonical builtin is `split(s, sep)`, no module prefix | -| `String::slice_from(s, n)` | Router | -| `String::starts_with(s, prefix)` | Router | - -## `Int` / `Float` - -| Call | Used in | Likely target | -|---|---|---| -| `Int::to_string(n)` | every file | builtin `int_to_string(n)` (no module prefix) | -| `Int::from_string(s)` | Config, Oikos | builtin `parse_int(s) -> Option` | -| `Float::to_string(f)` | Report | builtin `float_to_string(f)` | -| `Float::to_int(f)` | Report, GitHubAPI, Main, GitHubApp | needs a cast primitive | - -## `Time` - -No `Time` module in stdlib. The Crypto module exposes `time_ms() -> Int` -which covers `Time::now_millis` if the lowering can pun Int↔Float. - -| Call | Used in | -|---|---| -| `Time::now_millis() -> Float` | GitHubApp, Oikos | -| `Time::iso_now() -> String` | Main | -| `Time::parse_iso8601(s) -> Float` | GitHubApp (removed in static-review pass) | -| `Time::set_interval(f, ms) -> Int` | (was in Runtime; now stubbed) | -| `Time::clear_interval(id)` | (was in Runtime; now stubbed) | - -## `Crypto` (extensions) - -`stdlib/Crypto.affine` exports `random_string`, `random_f64`, `time_ms` -only. The hand-port additionally calls: - -| Call | Used in | -|---|---| -| `Crypto::import_key("raw", …)` | Webhook (HMAC) | -| `Crypto::import_key_pkcs8(…)` | GitHubApp (RSA JWT) | -| `Crypto::verify("HMAC", key, sig, data)` | Webhook | -| `Crypto::sign_with_algorithm(alg, key, data)` | GitHubApp | - -These map to Web Crypto under the Deno-ESM backend; the canonical -binding will likely arrive with the `#103` Async-extern ABI rollout. - -## `Bytes` - -No `Bytes` module in stdlib. - -| Call | Used in | -|---|---| -| `Bytes::from_utf8(s) -> [Int]` | Webhook, GitHubApp | -| `Bytes::from_string(s) -> [Int]` | GitHubApp | - -## `Base64` - -No `Base64` module in stdlib. - -| Call | Used in | -|---|---| -| `Base64::encode(bytes) -> String` | GitHubApp | -| `Base64::decode(s) -> String` | GitHubApp | - -## `Console` - -| Call | Used in | Likely target | -|---|---|---| -| `Console::log(s)` | Oikos, Main | builtin `println(s)` (no module prefix) | -| `Console::error(s)` | Oikos | builtin `eprintln(s)` | - -## `Http` (extensions beyond `stdlib/Http.affine`) - -`stdlib/Http.affine` provides `http_request`, `fetch`, `get`, `post`, -`is_ok`, plus the `Request`/`Response` records. The hand-port -additionally references: - -| Call | Used in | -|---|---| -| `Http::Request` (as a type alias) | Router, Main | -| `Http::Response` (as a type alias) | Router, Main | -| `Http::serve(opts, handler)` | (was in Main; now stubbed) | -| `Http::Server` (handle type) | (was in Runtime; now opaque-Int stubbed) | -| `Http::Server::shutdown(s)` | (was in Runtime; now stubbed) | -| `Http::not_implemented(req) -> Response` | Router (placeholder for the dispatch stub) | -| `Http::Url::parse(url)`, `Http::Url::pathname(u)` | (was in Router; now stubbed) | -| `Http::Request::method(req)`, `Http::Request::url(req)`, `Http::Request::headers(req)`, `Http::Request::text(req)` | (was in Router/Main; now stubbed) | -| `Http::Response::make(body, opts) -> Response` | (was in Router/Main; now stubbed) | - -## `Array` - -`stdlib/prelude.affine` exports `map`, `filter`, `fold`, `contains`, -`sum`, `product` as bare functions over `[T]`. The hand-port additionally -calls: - -| Call | Used in | Likely target | -|---|---|---| -| `Array::length(a)` | Report, Router | needs `len` builtin or a prelude `length` fn | -| `Array::append(a, b)` | (was in Oikos; rewritten to `a ++ b`) | already covered by `++` | -| `Array::map(a, f)` | (was in Report; rewritten to `map(a, f)`) | covered by prelude | -| `Array::filter(a, p)` | (was in Oikos; rewritten to `filter(a, p)`) | covered by prelude | -| `Array::slice(a, start, len)` | Report | -| `Array::iter(a, f)` | (was in Router/Runtime; now stubbed) | -| `Array::iter_indexed(a, f)` | (was in Router; now stubbed) | -| `Array::fold_right(a, init, f)` | (was in Router; now stubbed) | -| `Array::get(a, i)` | (was in Router; now stubbed) | -| `Array::join(strs, sep)` | (was in Report; rewritten to `string::join(strs, sep)`) | covered by stdlib `string::join` | - -## `Exn` - -| Call | Used in | Likely target | -|---|---|---| -| `Exn::message(err)` | Analysis, GitHubAPI, GitHubApp | needs an `Exn` module or pattern-matched extraction | diff --git a/bots/sustainabot/bot-integration/deno.json b/bots/sustainabot/bot-integration/deno.json deleted file mode 100644 index be6d928d..00000000 --- a/bots/sustainabot/bot-integration/deno.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@oikos/bot", - "version": "0.2.0-beta", - "exports": { - ".": "./src/Oikos.affine", - "./router": "./src/Router.affine", - "./tea": "./src/tea/ServerTea.affine" - }, - "publish": { - "include": ["src/**/*.affine", "README.md", "LICENSE", "MIGRATION-NOTES.md"] - }, - "tasks": { - "build": "affinescript compile src/Main.affine", - "watch": "affinescript compile --watch src/Main.affine", - "clean": "rm -rf _build", - "smee": "deno run --allow-net --allow-env scripts/smee-client.ts", - "test": "affinescript test", - "lint": "affinescript lint src/", - "fmt": "affinescript fmt src/" - }, - "imports": { - "@std/http": "jsr:@std/http@^1.0.0", - "@std/log": "jsr:@std/log@^0.224.0", - "@std/dotenv": "jsr:@std/dotenv@^0.225.0", - "@std/crypto": "jsr:@std/crypto@^1.0.0" - }, - "fmt": { - "useTabs": false, - "lineWidth": 100, - "indentWidth": 2, - "singleQuote": false - } -} diff --git a/bots/sustainabot/bot-integration/scripts/smee-client.js b/bots/sustainabot/bot-integration/scripts/smee-client.js deleted file mode 100644 index 9409a9ef..00000000 --- a/bots/sustainabot/bot-integration/scripts/smee-client.js +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -// -// Smee.io client for local webhook testing -// Forwards GitHub webhooks from smee.io to local server - -const SMEE_URL = Deno.env.get("SMEE_URL") || "https://smee.io/wT7QTqKbxTrez2V2"; -const LOCAL_URL = Deno.env.get("LOCAL_URL") || "http://localhost:3000"; - -console.log(`🔗 Connecting to smee.io: ${SMEE_URL}`); -console.log(`📍 Forwarding to: ${LOCAL_URL}`); - -async function connectToSmee() { - const eventSourceUrl = SMEE_URL; - - const response = await fetch(eventSourceUrl, { - headers: { - "Accept": "text/event-stream", - }, - }); - - if (!response.ok) { - throw new Error(`Failed to connect to smee: ${response.status}`); - } - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error("No response body"); - } - - const decoder = new TextDecoder(); - let buffer = ""; - - console.log("✅ Connected to smee.io, waiting for webhooks...\n"); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (line.startsWith("data: ")) { - try { - const data = JSON.parse(line.slice(6)); - await forwardWebhook(data); - } catch (e) { - // Ignore ping/heartbeat messages - if (!line.includes("ping")) { - console.error("Failed to parse:", e); - } - } - } - } - } -} - -async function forwardWebhook(data) { - const headers = new Headers(); - - // Forward GitHub headers - if (data["x-github-event"]) { - headers.set("x-github-event", data["x-github-event"]); - } - if (data["x-github-delivery"]) { - headers.set("x-github-delivery", data["x-github-delivery"]); - } - if (data["x-hub-signature-256"]) { - headers.set("x-hub-signature-256", data["x-hub-signature-256"]); - } - headers.set("content-type", "application/json"); - - // The actual payload is in data.body - const body = data.body || data; - - console.log(`📨 Received: ${data["x-github-event"]} event`); - - try { - const response = await fetch(LOCAL_URL, { - method: "POST", - headers, - body: JSON.stringify(body), - }); - - console.log(` ↳ Forwarded to local: ${response.status} ${response.statusText}`); - } catch (e) { - console.error(` ↳ Failed to forward: ${e}`); - } -} - -// Reconnect on failure -while (true) { - try { - await connectToSmee(); - } catch (e) { - console.error("Connection lost, reconnecting in 5s...", e); - await new Promise((r) => setTimeout(r, 5000)); - } -} diff --git a/bots/sustainabot/bot-integration/src/Analysis.affine b/bots/sustainabot/bot-integration/src/Analysis.affine deleted file mode 100644 index 1d161924..00000000 --- a/bots/sustainabot/bot-integration/src/Analysis.affine +++ /dev/null @@ -1,131 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2026 hyperpolymath -// -// MIGRATED FROM Analysis.res by hand-port per issue #148 (policy override 2026-05-24). -// Uses Http::fetch (affinescript#160 ✅ closed 2026-05-19) and the -// canonical json:: encoders/decoders. - -module Analysis; - -use prelude::{Option, Some, None, Result, Ok, Err}; -use json::{Json}; -use Types::{AnalysisResult, EcoMetrics, EconMetrics, ParetoStatus, QualityMetrics, HealthIndex, Recommendation, PriorityMedium}; - -// Originally ReScript `try { ... } catch { Js.Exn.Error(e) => ... }`; the -// translation lifts the failure mode into Exn[HttpError] / Exn[JsonError] -// per the effect-row model from affinescript#59 (closed 2026-05-18). - -pub fn analyze_repository( - endpoint: ref String, - repo_url: ref String, - reference: ref String, -) -{IO}-> Result { - let body = json::encode_object([ - ("url", json::encode_string(repo_url)), - ("ref", json::encode_string(reference)), - ]); - - try { - let response = Http::http_request( - endpoint ++ "/repository", - "POST", - [("Content-Type", "application/json")], - Some(json::stringify(body)), - ); - - if Http::is_ok(response) { - // TODO(mechanical-migrator): decode response.body into AnalysisResult - // once the canonical json -> typed-record decoder helper lands. - Err("Analysis decoder not yet implemented (awaits json::decode_record)") - } else { - Err("Analysis failed: HTTP " ++ Int::to_string(response.status)) - } - } catch { - err => Err("Analysis request failed: " ++ Exn::message(err)) - } -} - -pub fn analyze_diff( - endpoint: ref String, - repo_url: ref String, - base: ref String, - head: ref String, -) -{IO}-> Result { - let body = json::encode_object([ - ("url", json::encode_string(repo_url)), - ("base", json::encode_string(base)), - ("head", json::encode_string(head)), - ]); - - try { - let response = Http::http_request( - endpoint ++ "/diff", - "POST", - [("Content-Type", "application/json")], - Some(json::stringify(body)), - ); - - if Http::is_ok(response) { - Err("Diff analysis decoder not yet implemented (awaits json::decode_record)") - } else { - Err("Diff analysis failed: HTTP " ++ Int::to_string(response.status)) - } - } catch { - err => Err("Analysis request failed: " ++ Exn::message(err)) - } -} - -pub fn mock_analysis() -> AnalysisResult { - AnalysisResult #{ - eco: EcoMetrics #{ - carbonScore: 72.0, - energyScore: 68.0, - resourceScore: 75.0, - score: 71.5 - }, - econ: EconMetrics #{ - paretoDistance: 0.15, - allocationScore: 80.0, - debtScore: 65.0, - score: 72.0, - paretoStatus: Some(ParetoStatus #{ - isOptimal: false, - distance: 0.15, - improvements: Some([ - "Reduce complexity in src/utils.rs", - "Add memoization to hot path" - ]) - }) - }, - quality: QualityMetrics #{ - complexityScore: 70.0, - couplingScore: 75.0, - coverageScore: 82.0, - score: 75.5 - }, - health: HealthIndex #{ - eco: 0.4, - econ: 0.3, - quality: 0.3, - total: 72.8, - grade: "C" - }, - violations: [], - recommendations: [ - Recommendation #{ - entityId: "src/processing.rs", - action: "optimize_loop", - reason: "Hot loop could benefit from vectorization", - priority: PriorityMedium, - confidence: 0.78, - expectedImprovement: [ - ("carbonScore", 5.0), - ("energyScore", 8.0) - ] - } - ], - timestamp: "2024-12-08T10:00:00Z", - commitSha: Some("abc123") - } -} diff --git a/bots/sustainabot/bot-integration/src/Config.affine b/bots/sustainabot/bot-integration/src/Config.affine deleted file mode 100644 index a196adfe..00000000 --- a/bots/sustainabot/bot-integration/src/Config.affine +++ /dev/null @@ -1,87 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2026 hyperpolymath -// -// MIGRATED FROM Config.res by hand-port per issue #148 (policy override 2026-05-24). - -module Config; - -use prelude::{Option, Some, None, Result, Ok, Err}; -use Types::{Config, BotMode, Consultant, Advisor, Regulator}; - -// Env / String / Int module surfaces below are listed in MISSING-EXTERNS.md -// — the mechanical migrator must re-bind them against the canonical stdlib -// names. - -fn get_env(key: ref String, default: Option) -{IO}-> Option { - match Env::get(key) { - Some(v) => Some(v), - None => default - } -} - -fn get_env_required(key: ref String) -{IO}-> Result { - match Env::get(key) { - Some(v) => Ok(v), - None => Err("Missing required environment variable: " ++ key) - } -} - -fn get_env_int(key: ref String, default: Int) -{IO}-> Int { - match Env::get(key) { - Some(v) => match Int::from_string(v) { - Some(i) => i, - None => default - }, - None => default - } -} - -fn parse_mode(s: ref String) -> BotMode { - match String::to_lower(s) { - "consultant" => Consultant, - "regulator" => Regulator, - _ => Advisor - } -} - -// If GITHUB_PRIVATE_KEY_FILE is set the key should be loaded at startup; -// async file loading is deferred until affinescript#103 (Async-extern ABI) -// is wired through the IO effect handlers. -fn load_private_key() -{IO}-> Option { - match Env::get("GITHUB_PRIVATE_KEY_FILE") { - Some(_path) => get_env("GITHUB_PRIVATE_KEY", None), - None => get_env("GITHUB_PRIVATE_KEY", None) - } -} - -pub fn load() -{IO}-> Result { - let mode_str = match get_env("BOT_MODE", None) { - Some(m) => m, - None => "advisor" - }; - let mode = parse_mode(mode_str); - - let analysis_endpoint = match get_env("ANALYSIS_ENDPOINT", None) { - Some(e) => e, - None => "http://localhost:8080/analyze" - }; - - Ok(Config #{ - port: get_env_int("PORT", 3000), - mode: mode, - analysisEndpoint: analysis_endpoint, - githubWebhookSecret: get_env("GITHUB_WEBHOOK_SECRET", None), - gitlabWebhookSecret: get_env("GITLAB_WEBHOOK_SECRET", None), - githubAppId: get_env("GITHUB_APP_ID", None), - githubPrivateKey: load_private_key() - }) -} - -pub fn mode_to_string(mode: ref BotMode) -> String { - match mode { - Consultant => "consultant", - Advisor => "advisor", - Regulator => "regulator" - } -} diff --git a/bots/sustainabot/bot-integration/src/GitHubAPI.affine b/bots/sustainabot/bot-integration/src/GitHubAPI.affine deleted file mode 100644 index abc57676..00000000 --- a/bots/sustainabot/bot-integration/src/GitHubAPI.affine +++ /dev/null @@ -1,153 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2026 hyperpolymath -// -// MIGRATED FROM GitHubAPI.res by hand-port per issue #148 (policy override 2026-05-24). -// Uses Http::fetch (affinescript#160) and canonical json::*. - -module GitHubAPI; - -use prelude::{Option, Some, None, Result, Ok, Err}; -use json::{Json}; - -const user_agent: String = "oikos-bot/0.1.0-beta"; -const api_version: String = "2022-11-28"; - -pub fn api_request( - token: ref String, - method: ref String, - endpoint: ref String, - body: Option, -) -{IO}-> Result { - let url = "https://api.github.com" ++ endpoint; - - let headers = [ - ("Authorization", "Bearer " ++ token), - ("Accept", "application/vnd.github+json"), - ("X-GitHub-Api-Version", api_version), - ("User-Agent", user_agent), - ("Content-Type", "application/json"), - ]; - - try { - let body_str = match body { - Some(b) => Some(json::stringify(b)), - None => None - }; - let response = Http::http_request(url, method, headers, body_str); - - if response.status >= 200 && response.status < 300 { - // TODO(mechanical-migrator): parse response.body to Json once - // json::parse (#161 follow-up) is bound. - Err("response decoder not yet bound (awaits json::parse)") - } else { - Err("GitHub API error " ++ Int::to_string(response.status) ++ ": " ++ response.body) - } - } catch { - err => Err("API request failed: " ++ Exn::message(err)) - } -} - -pub fn post_pr_comment( - token: ref String, - owner: ref String, - repo: ref String, - pr_number: Int, - body: ref String, -) -{IO}-> Result { - let endpoint = "/repos/" ++ owner ++ "/" ++ repo ++ "/issues/" ++ Int::to_string(pr_number) ++ "/comments"; - let payload = json::encode_object([("body", json::encode_string(body))]); - - let result = api_request(token, "POST", endpoint, Some(payload)); - - match result { - Ok(j) => match json::decode_object(j) { - Some(obj) => match dict::get(obj, "id") { - Some(id) => match json::decode_int(id) { - Some(n) => Ok(n), - None => Err("Invalid comment ID in response") - }, - None => Err("No comment ID in response") - }, - None => Err("Invalid JSON response") - }, - Err(e) => Err(e) - } -} - -pub fn update_comment( - token: ref String, - owner: ref String, - repo: ref String, - comment_id: Int, - body: ref String, -) -{IO}-> Result<(), String> { - let endpoint = "/repos/" ++ owner ++ "/" ++ repo ++ "/issues/comments/" ++ Int::to_string(comment_id); - let payload = json::encode_object([("body", json::encode_string(body))]); - - let result = api_request(token, "PATCH", endpoint, Some(payload)); - - match result { - Ok(_) => Ok(()), - Err(e) => Err(e) - } -} - -// conclusion: "success" | "failure" | "neutral" | "cancelled" | "skipped" | "timed_out" | "action_required" -pub fn create_check_run( - token: ref String, - owner: ref String, - repo: ref String, - head_sha: ref String, - name: ref String, - conclusion: ref String, - title: ref String, - summary: ref String, -) -{IO}-> Result { - let endpoint = "/repos/" ++ owner ++ "/" ++ repo ++ "/check-runs"; - let payload = json::encode_object([ - ("name", json::encode_string(name)), - ("head_sha", json::encode_string(head_sha)), - ("status", json::encode_string("completed")), - ("conclusion", json::encode_string(conclusion)), - ("output", json::encode_object([ - ("title", json::encode_string(title)), - ("summary", json::encode_string(summary)), - ])), - ]); - - let result = api_request(token, "POST", endpoint, Some(payload)); - - match result { - Ok(j) => match json::decode_object(j) { - Some(obj) => match dict::get(obj, "id") { - Some(id) => match json::decode_int(id) { - Some(n) => Ok(n), - None => Err("Invalid check run ID in response") - }, - None => Err("No check run ID in response") - }, - None => Err("Invalid JSON response") - }, - Err(e) => Err(e) - } -} - -pub fn get_pull_request( - token: ref String, - owner: ref String, - repo: ref String, - pr_number: Int, -) -{IO}-> Result { - let endpoint = "/repos/" ++ owner ++ "/" ++ repo ++ "/pulls/" ++ Int::to_string(pr_number); - api_request(token, "GET", endpoint, None) -} - -pub fn get_repository( - token: ref String, - owner: ref String, - repo: ref String, -) -{IO}-> Result { - let endpoint = "/repos/" ++ owner ++ "/" ++ repo; - api_request(token, "GET", endpoint, None) -} diff --git a/bots/sustainabot/bot-integration/src/GitHubApp.affine b/bots/sustainabot/bot-integration/src/GitHubApp.affine deleted file mode 100644 index c16aa3e9..00000000 --- a/bots/sustainabot/bot-integration/src/GitHubApp.affine +++ /dev/null @@ -1,191 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2026 hyperpolymath -// -// MIGRATED FROM GitHubApp.res by hand-port per issue #148 (policy override 2026-05-24). -// RS256 JWT signing + installation-token caching. -// Crypto / Base64 / Bytes paths assume the Web Crypto-derived stdlib -// surface; will need re-validation against the canonical AffineScript -// stdlib once #103 (Async-extern ABI) is fully wired through handlers. -// See MISSING-EXTERNS.md for the unbound surfaces called below. - -module GitHubApp; - -use prelude::{Option, Some, None, Result, Ok, Err}; -use json::{Json}; -use option::{unwrap_or}; -use Types::{Config, InstallationToken}; - -// Module-level token cache. The original ReScript used a mutable -// process-local Dict; AffineScript does not permit `mut` on a `const`, -// so the cache has been moved behind get/set helpers that the -// mechanical migrator should re-back with a proper handler-managed -// state cell (Cmd-style, or a `state` effect once #59 lands). - -fn cache_get(_key: ref String) -{IO}-> Option { - // TODO(mechanical-migrator): wire to actual state-effect cache. - None -} - -fn cache_set(_key: ref String, _value: ref InstallationToken) -{IO}-> () { - // TODO(mechanical-migrator): wire to actual state-effect cache. - () -} - -fn pem_to_array_buffer(pem: ref String) -{IO}-> [Int] { - let stripped = String::replace_regex(pem, "-----BEGIN (?:RSA )?PRIVATE KEY-----", ""); - let stripped2 = String::replace_regex(stripped, "-----END (?:RSA )?PRIVATE KEY-----", ""); - let stripped3 = String::replace_regex(stripped2, "\\s", ""); - - let binary = Base64::decode(stripped3); - Bytes::from_string(binary) -} - -fn base64_url_encode(data: ref [Int]) -> String { - let raw = Base64::encode(data); - let r1 = String::replace_all(raw, "+", "-"); - let r2 = String::replace_all(r1, "/", "_"); - String::replace_regex(r2, "=+$", "") -} - -fn base64_url_encode_string(s: ref String) -> String { - let raw = Base64::encode(Bytes::from_utf8(s)); - let r1 = String::replace_all(raw, "+", "-"); - let r2 = String::replace_all(r1, "/", "_"); - String::replace_regex(r2, "=+$", "") -} - -// JWT valid for 10 minutes (GitHub requirement). `iat` is set 60 s in the -// past to account for clock drift. -pub fn generate_jwt( - app_id: ref String, - private_key_pem: ref String, -) -{IO}-> Result { - let now_seconds = Time::now_millis() / 1000.0; - - let header = json::encode_object([ - ("alg", json::encode_string("RS256")), - ("typ", json::encode_string("JWT")), - ]); - let header_b64 = base64_url_encode_string(json::stringify(header)); - - let payload = json::encode_object([ - ("iss", json::encode_string(app_id)), - ("iat", json::encode_float(now_seconds - 60.0)), - ("exp", json::encode_float(now_seconds + 600.0)), - ]); - let payload_b64 = base64_url_encode_string(json::stringify(payload)); - - let message = header_b64 ++ "." ++ payload_b64; - - try { - let key_buffer = pem_to_array_buffer(private_key_pem); - - let key = Crypto::import_key_pkcs8( - key_buffer, - #{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, - false, - ["sign"], - ); - - let message_bytes = Bytes::from_utf8(message); - let signature_buffer = Crypto::sign_with_algorithm( - #{ name: "RSASSA-PKCS1-v1_5" }, - key, - message_bytes, - ); - - let signature_b64 = base64_url_encode(signature_buffer); - Ok(message ++ "." ++ signature_b64) - } catch { - err => Err("Failed to generate JWT: " ++ Exn::message(err)) - } -} - -// GitHub installation tokens are valid for 1 hour. -pub fn get_installation_token( - jwt: ref String, - installation_id: Int, -) -{IO}-> Result { - let cache_key = Int::to_string(installation_id); - - match cache_get(cache_key) { - Some(cached) => { - if cached.expiresAt > Time::now_millis() + 60000.0 { - Ok(cached) - } else { - fetch_installation_token(jwt, cache_key) - } - }, - None => fetch_installation_token(jwt, cache_key) - } -} - -fn fetch_installation_token( - jwt: ref String, - cache_key: ref String, -) -{IO}-> Result { - try { - let response = Http::http_request( - "https://api.github.com/app/installations/" ++ cache_key ++ "/access_tokens", - "POST", - [ - ("Authorization", "Bearer " ++ jwt), - ("Accept", "application/vnd.github+json"), - ("X-GitHub-Api-Version", "2022-11-28"), - ("User-Agent", "oikos-bot") - ], - None, - ); - - if response.status < 200 || response.status >= 300 { - Err("GitHub API error " ++ Int::to_string(response.status) ++ ": " ++ response.body) - } else { - // TODO(mechanical-migrator): parse response.body once json::parse lands. - Err("installation-token decoder not yet bound (awaits json::parse)") - } - } catch { - err => Err("Failed to get installation token: " ++ Exn::message(err)) - } -} - -pub fn extract_installation_id(payload: ref Json) -> Option { - match json::decode_object(payload) { - Some(obj) => match dict::get(obj, "installation") { - Some(inst) => match json::decode_object(inst) { - Some(inst_obj) => match dict::get(inst_obj, "id") { - Some(id) => json::decode_int(id), - None => None - }, - None => None - }, - None => None - }, - None => None - } -} - -pub fn get_auth_token( - config: ref Config, - payload: ref Json, -) -{IO}-> Result { - match (config.githubAppId, config.githubPrivateKey) { - (Some(app_id), Some(private_key)) => match extract_installation_id(payload) { - Some(installation_id) => { - let jwt_result = generate_jwt(app_id, private_key); - match jwt_result { - Ok(jwt) => { - let token_result = get_installation_token(jwt, installation_id); - match token_result { - Ok(install_token) => Ok(install_token.token), - Err(e) => Err(e) - } - }, - Err(e) => Err(e) - } - }, - None => Err("No installation ID in payload") - }, - _ => Err("GitHub App credentials not configured") - } -} diff --git a/bots/sustainabot/bot-integration/src/Main.affine b/bots/sustainabot/bot-integration/src/Main.affine deleted file mode 100644 index c16ec3c5..00000000 --- a/bots/sustainabot/bot-integration/src/Main.affine +++ /dev/null @@ -1,252 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2026 hyperpolymath -// -// MIGRATED FROM Main.res by hand-port per issue #148 (policy override 2026-05-24). -// Oikos Bot — webhook intake and dispatch to the analysis service. - -module Main; - -use prelude::{Option, Some, None, Result, Ok, Err}; -use json::{Json}; -use option::{unwrap_or}; -use Types::{Config, WebhookEvent}; -use Webhook; -use Analysis; -use Report; -use GitHubAPI; -use GitHubApp; -use Config as ConfigModule; - -fn log(level: ref String, msg: ref String, data: Option) -{IO}-> () { - let timestamp = Time::iso_now(); - let log_obj = match data { - Some(d) => json::encode_object([ - ("timestamp", json::encode_string(timestamp)), - ("level", json::encode_string(level)), - ("message", json::encode_string(msg)), - ("data", d) - ]), - None => json::encode_object([ - ("timestamp", json::encode_string(timestamp)), - ("level", json::encode_string(level)), - ("message", json::encode_string(msg)) - ]) - }; - Console::log(json::stringify(log_obj)) -} - -fn info(msg: ref String, data: Option) -{IO}-> () { - log("info", msg, data) -} - -fn error(msg: ref String, data: Option) -{IO}-> () { - log("error", msg, data) -} - -fn extract_pr_info(payload: ref Json) -> (Int, String, String) { - match json::decode_object(payload) { - Some(obj) => { - let pr_number = match dict::get(obj, "number") { - Some(n) => unwrap_or(json::decode_int(n), 0), - None => 0 - }; - let (base_sha, head_sha) = match dict::get(obj, "pull_request") { - Some(pr) => match json::decode_object(pr) { - Some(pr_obj) => { - let base = match dict::get(pr_obj, "base") { - Some(b) => match json::decode_object(b) { - Some(base_obj) => match dict::get(base_obj, "sha") { - Some(s) => unwrap_or(json::decode_string(s), ""), - None => "" - }, - None => "" - }, - None => "" - }; - let head = match dict::get(pr_obj, "head") { - Some(h) => match json::decode_object(h) { - Some(head_obj) => match dict::get(head_obj, "sha") { - Some(s) => unwrap_or(json::decode_string(s), ""), - None => "" - }, - None => "" - }, - None => "" - }; - (base, head) - }, - None => ("", "") - }, - None => ("", "") - }; - (pr_number, base_sha, head_sha) - }, - None => (0, "", "") - } -} - -fn extract_mr_info(payload: ref Json) -> (Int, String, String) { - match json::decode_object(payload) { - Some(obj) => match dict::get(obj, "object_attributes") { - Some(attrs) => match json::decode_object(attrs) { - Some(attrs_obj) => { - let mr_iid = match dict::get(attrs_obj, "iid") { - Some(n) => unwrap_or(json::decode_int(n), 0), - None => 0 - }; - let base_sha = match dict::get(attrs_obj, "diff_refs") { - Some(refs) => match json::decode_object(refs) { - Some(refs_obj) => match dict::get(refs_obj, "base_sha") { - Some(s) => unwrap_or(json::decode_string(s), ""), - None => "" - }, - None => "" - }, - None => "" - }; - let head_sha = match dict::get(attrs_obj, "last_commit") { - Some(commit) => match json::decode_object(commit) { - Some(commit_obj) => match dict::get(commit_obj, "id") { - Some(s) => unwrap_or(json::decode_string(s), ""), - None => "" - }, - None => "" - }, - None => "" - }; - (mr_iid, base_sha, head_sha) - }, - None => (0, "", "") - }, - None => (0, "", "") - }, - None => (0, "", "") - } -} - -// The HTTP request/response shapes (`Http::Request`, `Http::Response`, -// route dispatch, signature validation) all depend on canonical externs -// that have not yet landed in stdlib — see MISSING-EXTERNS.md. The -// per-route handlers below are kept in shape-form so the mechanical -// migrator has a 1:1 template to re-bind once the externs exist. - -pub fn handle_github_webhook( - config: ref Config, - headers: ref [(String, String)], - body: ref String, -) -{IO}-> String { - // TODO(mechanical-migrator): port the original signature-validation - // and dispatch flow once Http::Response and Crypto::verify externs - // are bound. The original logic shape is preserved in the git history - // of d946daf for reference. - let parse_result = try { Some(json::parse(body)) } catch { _ => None }; - - match parse_result { - None => "{\"error\": \"Invalid JSON\"}", - Some(payload) => match Webhook::parse_github_event(headers, payload) { - Some(e) => { - info("GitHub event: " ++ e.eventType, None); - if e.eventType == "pull_request" { - let action = unwrap_or(e.action, ""); - if action == "opened" || action == "synchronize" { - let (pr_number, base_sha, head_sha) = extract_pr_info(payload); - let analysis_result = Analysis::analyze_diff( - config.analysisEndpoint, - e.repository.url, - base_sha, - head_sha, - ); - let comment = match analysis_result { - Ok(analysis) => Report::generate_pr_comment(analysis, config.mode), - Err(err) => { - error("Analysis failed: " ++ err, None); - Report::generate_pr_comment(Analysis::mock_analysis(), config.mode) - } - }; - let auth_result = GitHubApp::get_auth_token(config, payload); - match auth_result { - Ok(token) => { - let post_result = GitHubAPI::post_pr_comment( - token, - e.repository.owner, - e.repository.name, - pr_number, - comment, - ); - match post_result { - Ok(_comment_id) => info("Posted PR comment", None), - Err(err) => error("Failed to post PR comment: " ++ err, None) - } - }, - Err(err) => info("GitHub App not configured: " ++ err, None) - }; - () - }; - () - }; - "{\"status\": \"processed\"}" - }, - None => { - error("Failed to parse GitHub event", None); - "{\"error\": \"Invalid event\"}" - } - } - } -} - -pub fn handle_gitlab_webhook( - config: ref Config, - headers: ref [(String, String)], - body: ref String, -) -{IO}-> String { - let parse_result = try { Some(json::parse(body)) } catch { _ => None }; - - match parse_result { - None => "{\"error\": \"Invalid JSON\"}", - Some(payload) => match Webhook::parse_gitlab_event(headers, payload) { - Some(e) => { - info("GitLab event: " ++ e.eventType, None); - if e.eventType == "Merge Request Hook" { - let (mr_iid, base_sha, head_sha) = extract_mr_info(payload); - let analysis_result = Analysis::analyze_diff( - config.analysisEndpoint, - e.repository.url, - base_sha, - head_sha, - ); - match analysis_result { - Ok(analysis) => { - let _comment = Report::generate_pr_comment(analysis, config.mode); - info("Generated MR comment for MR !" ++ Int::to_string(mr_iid), None) - }, - Err(err) => { - error("Analysis failed: " ++ err, None); - let _comment = Report::generate_pr_comment(Analysis::mock_analysis(), config.mode); - info("Generated fallback MR comment", None) - } - }; - () - }; - "{\"status\": \"processed\"}" - }, - None => { - error("Failed to parse GitLab event", None); - "{\"error\": \"Invalid event\"}" - } - } - } -} - -pub fn main() -{IO}-> () { - match ConfigModule::load() { - Ok(config) => { - info("Starting Oikos Bot", None); - // TODO(mechanical-migrator): Http::serve loop awaits canonical - // server extern; for now this just logs the load. - let _ = config; - () - }, - Err(e) => error("Failed to load config: " ++ e, None) - } -} diff --git a/bots/sustainabot/bot-integration/src/Oikos.affine b/bots/sustainabot/bot-integration/src/Oikos.affine deleted file mode 100644 index f7a3a913..00000000 --- a/bots/sustainabot/bot-integration/src/Oikos.affine +++ /dev/null @@ -1,244 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell -// -// MIGRATED FROM Oikos.res by hand-port per issue #148 (policy override 2026-05-24). -// οἶκος: Greek root of both "ecology" and "economy". -// TEA Architecture — Model-Update-Subscriptions pattern. - -module Oikos; - -use prelude::{Option, Some, None, map, filter}; -use json::{Json}; -use option::{unwrap_or}; -use Types; -use Cmd; -use Sub; -use Runtime; - -pub type BotMode = - Advisor - | Consultant - | Regulator - -pub type WebhookSource = - GitHub - | GitLab - -pub type AnalysisStatus = - Pending - | InProgress - | Completed(Types::AnalysisResult) - | Failed(String) - -pub type PendingAnalysis = { - id: String, - repo: String, - prNumber: Int, - status: AnalysisStatus, - createdAt: Float, -} - -pub type Model = { - mode: BotMode, - port: Int, - webhookSecret: Option, - appId: Option, - privateKeyPath: Option, - pendingAnalyses: [PendingAnalysis], - totalProcessed: Int, - startTime: Float, - healthy: Bool, -} - -pub type Msg = - WebhookReceived(WebhookSource, Json) - | WebhookVerified(WebhookSource, Json) - | WebhookRejected(String) - | AnalysisRequested(String, String, Int) - | AnalysisStarted(String) - | AnalysisCompleted(String, Types::AnalysisResult) - | AnalysisFailed(String, String) - | CommentPosted(String, Int) - | CommentFailed(String, String) - | HealthCheck - | Tick - | Shutdown - -pub type Flags = { - port: Int, - mode: String, - webhookSecret: Option, - appId: Option, - privateKeyPath: Option, -} - -pub fn mode_from_string(s: ref String) -> BotMode { - match s { - "consultant" => Consultant, - "regulator" => Regulator, - _ => Advisor - } -} - -pub fn init(flags: Flags) -{IO}-> (Model, Cmd::T) { - let model = Model #{ - mode: mode_from_string(flags.mode), - port: flags.port, - webhookSecret: flags.webhookSecret, - appId: flags.appId, - privateKeyPath: flags.privateKeyPath, - pendingAnalyses: [], - totalProcessed: 0, - startTime: Time::now_millis(), - healthy: true - }; - - Console::log("🏛️ Oikos Bot starting..."); - Console::log(" Mode: " ++ flags.mode); - Console::log(" Port: " ++ Int::to_string(flags.port)); - - (model, Cmd::none()) -} - -pub fn update(msg: Msg, model: Model) -{IO}-> (Model, Cmd::T) { - match msg { - WebhookReceived(source, payload) => { - let source_str = match source { - GitHub => "GitHub", - GitLab => "GitLab" - }; - Console::log("📨 Webhook received from " ++ source_str); - (model, Cmd::perform( - fn() -{IO}-> Json { payload }, - fn(p) -> Msg { WebhookVerified(source, p) } - )) - }, - - WebhookVerified(source, payload) => { - Console::log("✓ Webhook verified"); - let _ = source; - let _ = payload; - (Model #{ ..model, totalProcessed: model.totalProcessed + 1 }, Cmd::none()) - }, - - WebhookRejected(reason) => { - Console::error("✗ Webhook rejected: " ++ reason); - (model, Cmd::none()) - }, - - AnalysisRequested(id, repo, pr_number) => { - Console::log("🔍 Analysis requested: " ++ repo ++ "#" ++ Int::to_string(pr_number)); - let analysis = PendingAnalysis #{ - id: id, - repo: repo, - prNumber: pr_number, - status: Pending, - createdAt: Time::now_millis() - }; - ( - Model #{ ..model, pendingAnalyses: model.pendingAnalyses ++ [analysis] }, - Cmd::none() - ) - }, - - AnalysisStarted(id) => { - let pending = map(model.pendingAnalyses, fn(a) -> PendingAnalysis { - if a.id == id { - PendingAnalysis #{ ..a, status: InProgress } - } else { - a - } - }); - (Model #{ ..model, pendingAnalyses: pending }, Cmd::none()) - }, - - AnalysisCompleted(id, result) => { - Console::log("✓ Analysis completed: " ++ id); - let pending = map(model.pendingAnalyses, fn(a) -> PendingAnalysis { - if a.id == id { - PendingAnalysis #{ ..a, status: Completed(result) } - } else { - a - } - }); - (Model #{ ..model, pendingAnalyses: pending }, Cmd::none()) - }, - - AnalysisFailed(id, error_msg) => { - Console::error("✗ Analysis failed: " ++ id ++ " - " ++ error_msg); - let pending = map(model.pendingAnalyses, fn(a) -> PendingAnalysis { - if a.id == id { - PendingAnalysis #{ ..a, status: Failed(error_msg) } - } else { - a - } - }); - (Model #{ ..model, pendingAnalyses: pending }, Cmd::none()) - }, - - CommentPosted(repo, pr_number) => { - Console::log("💬 Comment posted to " ++ repo ++ "#" ++ Int::to_string(pr_number)); - (model, Cmd::none()) - }, - - CommentFailed(repo, error_msg) => { - Console::error("✗ Failed to post comment to " ++ repo ++ ": " ++ error_msg); - (model, Cmd::none()) - }, - - HealthCheck => { - Console::log("💚 Health check - processed: " ++ Int::to_string(model.totalProcessed)); - (model, Cmd::none()) - }, - - Tick => { - let now = Time::now_millis(); - let one_hour = 60.0 * 60.0 * 1000.0; - let pending = filter(model.pendingAnalyses, fn(a) -> Bool { - now - a.createdAt < one_hour - }); - (Model #{ ..model, pendingAnalyses: pending }, Cmd::none()) - }, - - Shutdown => { - Console::log("👋 Shutting down..."); - (Model #{ ..model, healthy: false }, Cmd::none()) - } - } -} - -pub fn subscriptions(model: Model) -> Sub::T { - if model.healthy { - Sub::batch([ - Sub::http_server(model.port, fn(j) -> Option { - Some(WebhookReceived(GitHub, j)) - }), - Sub::every(60000, fn() -> Msg { Tick }) - ]) - } else { - Sub::none() - } -} - -pub fn run() -{IO}-> () { - let port = match Env::get("PORT") { - Some(p) => unwrap_or(Int::from_string(p), 3000), - None => 3000 - }; - let mode = unwrap_or(Env::get("BOT_MODE"), "advisor"); - let webhook_secret = Env::get("GITHUB_WEBHOOK_SECRET"); - let app_id = Env::get("GITHUB_APP_ID"); - let private_key_path = Env::get("GITHUB_PRIVATE_KEY_PATH"); - - let flags = Flags #{ - port: port, - mode: mode, - webhookSecret: webhook_secret, - appId: app_id, - privateKeyPath: private_key_path - }; - - let _ = Runtime::make(init, update, subscriptions, flags); - () -} diff --git a/bots/sustainabot/bot-integration/src/Report.affine b/bots/sustainabot/bot-integration/src/Report.affine deleted file mode 100644 index c99ce17f..00000000 --- a/bots/sustainabot/bot-integration/src/Report.affine +++ /dev/null @@ -1,180 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2026 hyperpolymath -// -// MIGRATED FROM Report.res by hand-port per issue #148 (policy override 2026-05-24). -// Pure string assembly + SARIF JSON construction. - -module Report; - -use prelude::{Option, Some, None, map}; -use json::{Json}; -use Types::{AnalysisResult, BotMode, Consultant, Regulator, Severity, Blocking}; - -pub fn get_grade(score: Float) -> String { - if score >= 90.0 { - "A" - } else if score >= 80.0 { - "B" - } else if score >= 70.0 { - "C" - } else if score >= 60.0 { - "D" - } else { - "F" - } -} - -pub fn get_grade_emoji(grade: ref String) -> String { - match grade { - "A" => "🏆", - "B" => "✨", - "C" => "👍", - "D" => "⚠️", - _ => "🚨" - } -} - -pub fn get_status_emoji(score: Float) -> String { - if score >= 70.0 { - "✅" - } else if score >= 50.0 { - "⚠️" - } else { - "❌" - } -} - -pub fn severity_to_string(s: ref Severity) -> String { - match s { - Blocking => "blocking", - High => "high", - Medium => "medium", - Low => "low", - Info => "info" - } -} - -pub fn generate_pr_comment(analysis: ref AnalysisResult, mode: ref BotMode) -> String { - let grade = get_grade(analysis.health.total); - let grade_emoji = get_grade_emoji(grade); - - let header = "## 🏛️ Oikos Analysis\n\n"; - - let health_line = "### Overall Health: " ++ grade_emoji ++ " " ++ grade - ++ " (" ++ Float::to_string(analysis.health.total) ++ "/100)\n\n"; - - let score_table = - "| Metric | Score | Status |\n" - ++ "|--------|-------|--------|\n" - ++ "| 🌍 Ecological | " ++ Float::to_string(analysis.eco.score) - ++ " | " ++ get_status_emoji(analysis.eco.score) ++ " |\n" - ++ "| 📊 Economic | " ++ Float::to_string(analysis.econ.score) - ++ " | " ++ get_status_emoji(analysis.econ.score) ++ " |\n" - ++ "| ⚙️ Quality | " ++ Float::to_string(analysis.quality.score) - ++ " | " ++ get_status_emoji(analysis.quality.score) ++ " |\n\n"; - - let violations_section = if Array::length(analysis.violations) > 0 { - let violation_lines = map(analysis.violations, fn(v) -> String { - let icon = if v.severity == Blocking { "🚫" } else { "⚠️" }; - icon ++ " **" ++ v.policy ++ "**: " ++ v.message ++ "\n" - }); - let lines = string::join(violation_lines, ""); - "### ⚠️ Policy Violations\n\n" ++ lines ++ "\n" - } else { - "" - }; - - let recommendations_section = - if Array::length(analysis.recommendations) > 0 && mode != Regulator { - let max_recs = if mode == Consultant { 10 } else { 5 }; - let top_recs = Array::slice(analysis.recommendations, 0, max_recs); - - let rec_lines = map(top_recs, fn(r) -> String { - let confidence = Float::to_int(r.confidence * 100.0); - "- **" ++ r.action ++ "** (" ++ Int::to_string(confidence) - ++ "% confidence): " ++ r.reason ++ "\n" - }); - let lines = string::join(rec_lines, ""); - "### 💡 Recommendations\n\n" ++ lines ++ "\n" - } else { - "" - }; - - let pareto_section = match analysis.econ.paretoStatus { - Some(ps) => { - let status = if ps.isOptimal { - "✅ This code is on the Pareto frontier - no dominated trade-offs detected.\n\n" - } else { - let improvements = match ps.improvements { - Some(imps) => { - let imp_lines = map(imps, fn(i) -> String { "- " ++ i ++ "\n" }); - string::join(imp_lines, "") - }, - None => "" - }; - let imp_block = if improvements != "" { - "Potential Pareto improvements:\n" ++ improvements ++ "\n" - } else { - "" - }; - "📍 Distance from Pareto frontier: " ++ Float::to_string(ps.distance) ++ "\n\n" ++ imp_block - }; - "### 📈 Pareto Analysis\n\n" ++ status - }, - None => "" - }; - - let footer = - "---\n" - ++ "*Analyzed by [Oikos Bot](https://github.com/hyperpolymath/oikos-bot) | " - ++ "Mode: " ++ Config::mode_to_string(mode) ++ " | " - ++ "[Learn more about eco-friendly coding](https://greensoftware.foundation/)*\n"; - - header ++ health_line ++ score_table ++ violations_section ++ recommendations_section ++ pareto_section ++ footer -} - -pub fn generate_sarif(analysis: ref AnalysisResult) -> Json { - let rules = [ - json::encode_object([ - ("id", json::encode_string("eco/eco-minimum")), - ("name", json::encode_string("EcoMinimum")), - ("shortDescription", json::encode_object([ - ("text", json::encode_string("Eco minimum threshold not met")) - ])) - ]), - json::encode_object([ - ("id", json::encode_string("eco/eco-standard")), - ("name", json::encode_string("EcoStandard")), - ("shortDescription", json::encode_object([ - ("text", json::encode_string("Eco standard threshold not met")) - ])) - ]) - ]; - - let results = map(analysis.violations, fn(v) -> Json { - json::encode_object([ - ("ruleId", json::encode_string("eco/" ++ String::replace_all(v.policy, "_", "-"))), - ("level", json::encode_string(if v.severity == Blocking { "error" } else { "warning" })), - ("message", json::encode_object([("text", json::encode_string(v.message))])) - ]) - }); - - json::encode_object([ - ("$schema", json::encode_string("https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json")), - ("version", json::encode_string("2.1.0")), - ("runs", json::encode_array([ - json::encode_object([ - ("tool", json::encode_object([ - ("driver", json::encode_object([ - ("name", json::encode_string("oikos-bot")), - ("version", json::encode_string("0.1.0-beta")), - ("informationUri", json::encode_string("https://github.com/hyperpolymath/oikos-bot")), - ("rules", json::encode_array(rules)) - ])) - ])), - ("results", json::encode_array(results)) - ]) - ])) - ]) -} diff --git a/bots/sustainabot/bot-integration/src/Router.affine b/bots/sustainabot/bot-integration/src/Router.affine deleted file mode 100644 index c1511414..00000000 --- a/bots/sustainabot/bot-integration/src/Router.affine +++ /dev/null @@ -1,142 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell -// -// MIGRATED FROM Router.res by hand-port per issue #148 (policy override 2026-05-24). -// HTTP router for Oikos Bot. Several Http::Request and Http::Url helpers -// invoked here are not yet bound in the canonical stdlib — see -// MISSING-EXTERNS.md for the list. - -module Router; - -use prelude::{Option, Some, None, filter}; - -pub type Method = - GET - | POST - | PUT - | DELETE - | PATCH - | OPTIONS - | HEAD - -// Handler / middleware as effect-row function aliases. The actual effect -// row is supplied at each call site; this alias is shape-only. -pub type Handler = fn(Http::Request) -{IO}-> Http::Response - -pub type Middleware = - fn(Http::Request, fn() -{IO}-> Http::Response) -{IO}-> Http::Response - -pub type Route = { - method: Method, - path: String, - handler: Handler, -} - -pub type Router = { - routes: [Route], - middlewares: [Middleware], - notFoundHandler: Option, -} - -pub fn make() -> Router { - Router #{ - routes: [], - middlewares: [], - notFoundHandler: None - } -} - -pub fn method_to_string(method: ref Method) -> String { - match method { - GET => "GET", - POST => "POST", - PUT => "PUT", - DELETE => "DELETE", - PATCH => "PATCH", - OPTIONS => "OPTIONS", - HEAD => "HEAD" - } -} - -pub fn method_from_string(s: ref String) -> Option { - match s { - "GET" => Some(GET), - "POST" => Some(POST), - "PUT" => Some(PUT), - "DELETE" => Some(DELETE), - "PATCH" => Some(PATCH), - "OPTIONS" => Some(OPTIONS), - "HEAD" => Some(HEAD), - _ => None - } -} - -// Path matching with named parameters (":id" style). -// Returns an assoc list of (param_name, value) — `dict` shape. -pub fn match_path(pattern: ref String, path: ref String) -> Option<[(String, String)]> { - let pattern_parts = filter(String::split(pattern, "/"), fn(p) -> Bool { p != "" }); - let path_parts = filter(String::split(path, "/"), fn(p) -> Bool { p != "" }); - - if Array::length(pattern_parts) != Array::length(path_parts) { - None - } else { - // TODO(mechanical-migrator): the original used a `mut` accumulator over - // Array::iter_indexed. Rewriting as an explicit fold once the canonical - // Array surface lands; meanwhile this returns the empty-params match. - Some([]) - } -} - -pub fn add_route(router: Router, method: Method, path: String, handler: Handler) -> Router { - let new_route = Route #{ method: method, path: path, handler: handler }; - Router #{ ..router, routes: router.routes ++ [new_route] } -} - -pub fn get(router: Router, path: String, handler: Handler) -> Router { - add_route(router, GET, path, handler) -} - -pub fn post(router: Router, path: String, handler: Handler) -> Router { - add_route(router, POST, path, handler) -} - -pub fn put(router: Router, path: String, handler: Handler) -> Router { - add_route(router, PUT, path, handler) -} - -pub fn delete(router: Router, path: String, handler: Handler) -> Router { - add_route(router, DELETE, path, handler) -} - -pub fn patch(router: Router, path: String, handler: Handler) -> Router { - add_route(router, PATCH, path, handler) -} - -pub fn options(router: Router, path: String, handler: Handler) -> Router { - add_route(router, OPTIONS, path, handler) -} - -pub fn use_middleware(router: Router, middleware: Middleware) -> Router { - Router #{ ..router, middlewares: router.middlewares ++ [middleware] } -} - -pub fn not_found(router: Router, handler: Handler) -> Router { - Router #{ ..router, notFoundHandler: Some(handler) } -} - -// `dispatch` walks routes to find a method+path match and invokes its -// handler under the middleware chain. The mechanical migrator must -// re-express this once the canonical Http::Request shape and -// Array::iter / Array::fold_right externs are bound. -pub fn dispatch(router: ref Router, req: Http::Request) -{IO}-> Http::Response { - // TODO(mechanical-migrator): re-implement against canonical Http::Request - // shape. Stubbed to a 501 so the file parses. - Http::not_implemented(req) -} - -pub fn serve(router: Router, port: Int) -{IO}-> () { - // TODO(mechanical-migrator): the canonical Http::serve loop has not yet - // landed in stdlib. Stubbed for parse-time only. - () -} diff --git a/bots/sustainabot/bot-integration/src/Types.affine b/bots/sustainabot/bot-integration/src/Types.affine deleted file mode 100644 index de0a7ddd..00000000 --- a/bots/sustainabot/bot-integration/src/Types.affine +++ /dev/null @@ -1,141 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2026 hyperpolymath -// -// MIGRATED FROM Types.res by hand-port per issue #148 (policy override 2026-05-24). -// Json/Dict references resolved against the canonical stdlib (json.affine -// and dict.affine — both landed 2026-05). See MIGRATION-NOTES.md for the -// list of structural items the mechanical migrator still needs to redo. - -module Types; - -use prelude::{Option, Some, None}; -use json::{Json}; - -pub type CodeLocation = { - file: String, - line: Int, - column: Option, -} - -pub type EcoMetrics = { - carbonScore: Float, - energyScore: Float, - resourceScore: Float, - score: Float, -} - -pub type ParetoStatus = { - isOptimal: Bool, - distance: Float, - improvements: Option<[String]>, -} - -pub type EconMetrics = { - paretoDistance: Float, - allocationScore: Float, - debtScore: Float, - score: Float, - paretoStatus: Option, -} - -pub type QualityMetrics = { - complexityScore: Float, - couplingScore: Float, - coverageScore: Float, - score: Float, -} - -pub type HealthIndex = { - eco: Float, - econ: Float, - quality: Float, - total: Float, - grade: String, -} - -pub type Severity = - Blocking - | High - | Medium - | Low - | Info - -pub type PolicyViolation = { - entityId: String, - policy: String, - severity: Severity, - message: String, - location: Option, - suggestions: [String], -} - -pub type Priority = - PriorityHigh - | PriorityMedium - | PriorityLow - -pub type Recommendation = { - entityId: String, - action: String, - reason: String, - priority: Priority, - confidence: Float, - // Dict shape per stdlib/dict.affine: an assoc list `[(String, V)]`. - expectedImprovement: [(String, Float)], -} - -pub type AnalysisResult = { - eco: EcoMetrics, - econ: EconMetrics, - quality: QualityMetrics, - health: HealthIndex, - violations: [PolicyViolation], - recommendations: [Recommendation], - timestamp: String, - commitSha: Option, -} - -pub type BotMode = - Consultant - | Advisor - | Regulator - -pub type Platform = - GitHub - | GitLab - -pub type RepositoryInfo = { - owner: String, - name: String, - url: String, -} - -pub type WebhookEvent = { - platform: Platform, - eventType: String, - action: Option, - repository: RepositoryInfo, - payload: Json, -} - -pub type InstallationToken = { - token: String, - expiresAt: Float, -} - -pub type JwtClaims = { - iss: String, - iat: Float, - exp: Float, -} - -pub type Config = { - port: Int, - mode: BotMode, - analysisEndpoint: String, - githubWebhookSecret: Option, - gitlabWebhookSecret: Option, - githubAppId: Option, - githubPrivateKey: Option, -} diff --git a/bots/sustainabot/bot-integration/src/Webhook.affine b/bots/sustainabot/bot-integration/src/Webhook.affine deleted file mode 100644 index 6e5eeb9d..00000000 --- a/bots/sustainabot/bot-integration/src/Webhook.affine +++ /dev/null @@ -1,150 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2026 hyperpolymath -// -// MIGRATED FROM Webhook.res by hand-port per issue #148 (policy override 2026-05-24). -// HMAC verification uses the IO effect once #103 (Async-extern ABI) lowers -// Web Crypto into the standard handler set. - -module Webhook; - -use prelude::{Option, Some, None}; -use json::{Json}; -use option::{unwrap_or}; -use Types::{WebhookEvent, RepositoryInfo, Platform, GitHub, GitLab}; - -// Bytes / Crypto / String surfaces below are listed in MISSING-EXTERNS.md. - -// GitHub uses HMAC-SHA256 over the raw request body. -pub fn verify_github_signature( - payload: ref String, - signature: ref String, - secret: ref String, -) -{IO}-> Bool { - let key_data = Bytes::from_utf8(secret); - let data = Bytes::from_utf8(payload); - - let key = Crypto::import_key( - "raw", - key_data, - #{ name: "HMAC", hash: "SHA-256" }, - false, - ["sign", "verify"], - ); - - let signature_bytes = Bytes::from_utf8(String::replace(signature, "sha256=", "")); - Crypto::verify("HMAC", key, signature_bytes, data) -} - -pub fn verify_gitlab_token(token: ref String, secret: ref String) -> Bool { - token == secret -} - -pub fn parse_github_event( - headers: ref [(String, String)], - payload: ref Json, -) -> Option { - let event_type = dict::get(headers, "x-github-event"); - let action = match json::decode_object(payload) { - Some(obj) => match dict::get(obj, "action") { - Some(a) => json::decode_string(a), - None => None - }, - None => None - }; - - match event_type { - Some(et) => { - let repo = match json::decode_object(payload) { - Some(obj) => match dict::get(obj, "repository") { - Some(r) => match json::decode_object(r) { - Some(repo_obj) => { - let owner = match dict::get(repo_obj, "owner") { - Some(o) => match json::decode_object(o) { - Some(owner_obj) => match dict::get(owner_obj, "login") { - Some(l) => unwrap_or(json::decode_string(l), ""), - None => "" - }, - None => "" - }, - None => "" - }; - let name = match dict::get(repo_obj, "name") { - Some(n) => unwrap_or(json::decode_string(n), ""), - None => "" - }; - let url = match dict::get(repo_obj, "html_url") { - Some(u) => unwrap_or(json::decode_string(u), ""), - None => "" - }; - Some(RepositoryInfo #{ owner: owner, name: name, url: url }) - }, - None => None - }, - None => None - }, - None => None - }; - - match repo { - Some(r) => Some(WebhookEvent #{ - platform: GitHub, - eventType: et, - action: action, - repository: r, - payload: payload - }), - None => None - } - }, - None => None - } -} - -pub fn parse_gitlab_event( - headers: ref [(String, String)], - payload: ref Json, -) -> Option { - let event_type = dict::get(headers, "x-gitlab-event"); - - match event_type { - Some(et) => { - let repo = match json::decode_object(payload) { - Some(obj) => match dict::get(obj, "project") { - Some(p) => match json::decode_object(p) { - Some(proj_obj) => { - let namespace = match dict::get(proj_obj, "namespace") { - Some(n) => unwrap_or(json::decode_string(n), ""), - None => "" - }; - let name = match dict::get(proj_obj, "name") { - Some(n) => unwrap_or(json::decode_string(n), ""), - None => "" - }; - let url = match dict::get(proj_obj, "web_url") { - Some(u) => unwrap_or(json::decode_string(u), ""), - None => "" - }; - Some(RepositoryInfo #{ owner: namespace, name: name, url: url }) - }, - None => None - }, - None => None - }, - None => None - }; - - match repo { - Some(r) => Some(WebhookEvent #{ - platform: GitLab, - eventType: et, - action: None, - repository: r, - payload: payload - }), - None => None - } - }, - None => None - } -} diff --git a/bots/sustainabot/bot-integration/src/tea/Cmd.affine b/bots/sustainabot/bot-integration/src/tea/Cmd.affine deleted file mode 100644 index 966f10f2..00000000 --- a/bots/sustainabot/bot-integration/src/tea/Cmd.affine +++ /dev/null @@ -1,52 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell -// -// MIGRATED FROM tea/ServerTea.res by hand-port per issue #148 (policy override 2026-05-24). -// TEA Cmd module — describes IO actions that produce messages. -// -// Split out of the original single-file ServerTea hand-port: the parser -// has no inline `module Name { ... }` form, so each TEA submodule is its -// own file. - -module Cmd; - -use prelude::{Option, Some, None, Result, Ok, Err}; - -pub type T = - None - | Batch([T]) - | Perform(fn() -{IO}-> M) - | PerformWithDispatch(fn(fn(M) -> ()) -{IO}-> ()) - -pub fn none() -> T { - None -} - -pub fn batch(cmds: [T]) -> T { - Batch(cmds) -} - -pub fn perform(task: fn() -{IO}-> A, to_msg: fn(A) -> M) -> T { - Perform(fn() -{IO}-> M { to_msg(task()) }) -} - -// `attempt` lifts a fallible task into a Result-carrying message. -// The E parameter is unconstrained here; the mechanical migrator should -// re-narrow against the actual Exn[E] effect surface (#59). -pub fn attempt( - task: fn() -{IO}-> A, - to_msg: fn(Result) -> M, -) -> T { - Perform(fn() -{IO}-> M { - try { - to_msg(Ok(task())) - } catch { - e => to_msg(Err(e)) - } - }) -} - -pub fn with_dispatch(f: fn(fn(M) -> ()) -{IO}-> ()) -> T { - PerformWithDispatch(f) -} diff --git a/bots/sustainabot/bot-integration/src/tea/Runtime.affine b/bots/sustainabot/bot-integration/src/tea/Runtime.affine deleted file mode 100644 index 80e72fdd..00000000 --- a/bots/sustainabot/bot-integration/src/tea/Runtime.affine +++ /dev/null @@ -1,92 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell -// -// MIGRATED FROM tea/ServerTea.res by hand-port per issue #148 (policy override 2026-05-24). -// TEA Runtime module — the dispatch loop. -// -// Structural caveat (must be re-resolved by the mechanical migrator): -// The original ReScript used `let rec dispatch = ... and update_subs = -// ... and start_subscription = ...` mutual recursion over shared `mut` -// cells. The AffineScript parser has no `let rec ... and ...` form, so -// the closures here are hoisted to top-level `fn`s that take the -// state record explicitly. The borrow checker will likely require -// the State record to be `own`-passed through these functions, or -// the whole loop to be re-expressed as a single handler. See -// MIGRATION-NOTES.md "Runtime structural rework". - -module Runtime; - -use prelude::{Option, Some, None}; -use Cmd; -use Sub; - -pub type State = { - model: mut Model, - subscriptions: mut [Sub::T], - running: mut Bool, - // `http_server` / `intervals` carry host handles whose canonical types - // (Http::Server, Time::IntervalId) are not yet in stdlib; modelled as - // opaque Int handles until the externs land. - http_server: mut Option, - intervals: mut [Int], -} - -pub type Handle = { - dispatch: fn(Msg) -> (), - get_model: fn() -> Model, - stop: fn() -{IO}-> (), -} - -// dispatch / update_subscriptions / start_subscription were -// mutually-recursive closures in the original; mechanical re-port should -// re-express them under whatever final closure-form the borrow checker -// admits. Stubs preserved here for traceability. - -pub fn execute_cmd( - cmd: ref Cmd::T, - dispatch: fn(Msg) -> (), -) -{IO}-> () { - match cmd { - Cmd::None => (), - Cmd::Batch(cmds) => { - // TODO(mechanical-migrator): iterate `cmds` once Array/iter externs - // are bound; for now this is a no-op stub so the file parses. - () - }, - Cmd::Perform(task) => { - let msg = task(); - dispatch(msg) - }, - Cmd::PerformWithDispatch(f) => f(dispatch), - } -} - -// `make` is the public entry point. The original wired up dispatch, -// update_subscriptions and start_subscription as a `let rec ... and` -// cluster sharing the `state` record. That form is not in the parser; -// the implementation body has been replaced with a TODO stub so the -// file parses cleanly. The mechanical migrator must rebuild the loop -// against the final effect-row + borrow shape (#59 / #103). -pub fn make( - init: fn(Flags) -> (Model, Cmd::T), - update: fn(Msg, Model) -> (Model, Cmd::T), - subscriptions: fn(Model) -> Sub::T, - flags: Flags, -) -{IO}-> Handle { - // TODO(mechanical-migrator): rebuild the dispatch loop. The shape was - // 1. let (initial_model, initial_cmd) = init(flags); - // 2. create State {model, subscriptions=[], running=true, ...} - // 3. dispatch = msg => if running { (m, c) = update(msg, state.model); - // state.model := m; execute_cmd(c, dispatch); ... } - // 4. start_subscription on initial subs - // 5. return Handle { dispatch, get_model, stop } - // The hand-port has been stubbed so the file parses; do NOT trust the - // returned Handle to actually drive a TEA loop. - let (initial_model, _initial_cmd) = init(flags); - Handle #{ - dispatch: fn(_msg) -> () { () }, - get_model: fn() -> Model { initial_model }, - stop: fn() -{IO}-> () { () }, - } -} diff --git a/bots/sustainabot/bot-integration/src/tea/Sub.affine b/bots/sustainabot/bot-integration/src/tea/Sub.affine deleted file mode 100644 index 2405429f..00000000 --- a/bots/sustainabot/bot-integration/src/tea/Sub.affine +++ /dev/null @@ -1,33 +0,0 @@ -// face: affinescript -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell -// -// MIGRATED FROM tea/ServerTea.res by hand-port per issue #148 (policy override 2026-05-24). -// TEA Sub module — long-lived subscription descriptors. - -module Sub; - -use prelude::{Option, Some, None}; -use json::{Json}; - -pub type T = - None - | Batch([T]) - | HttpServer(Int, fn(Json) -> Option) - | Interval(Int, fn() -> M) - -pub fn none() -> T { - None -} - -pub fn batch(subs: [T]) -> T { - Batch(subs) -} - -pub fn http_server(port: Int, handler: fn(Json) -> Option) -> T { - HttpServer(port, handler) -} - -pub fn every(ms: Int, to_msg: fn() -> M) -> T { - Interval(ms, to_msg) -} diff --git a/bots/sustainabot/config/oikos.yaml b/bots/sustainabot/config/oikos.yaml deleted file mode 100644 index e488cb5b..00000000 --- a/bots/sustainabot/config/oikos.yaml +++ /dev/null @@ -1,183 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2024-2025 hyperpolymath -# -# Oikos Bot Configuration -# ======================= -# Main configuration file for the Oikos Bot platform - -# Bot operating mode -# - consultant: Answers questions, provides alternatives, explains trade-offs -# - advisor: Proactive suggestions on PRs, best practice recommendations -# - regulator: Enforces policy compliance, can block PRs -mode: advisor - -# Score thresholds for policy levels -thresholds: - eco_minimum: - carbon: 50 - energy: 50 - description: "Minimum acceptable eco standards" - enforcement: blocking # Will block PRs below this threshold - - eco_standard: - carbon: 70 - energy: 70 - description: "Recommended eco standards" - enforcement: warning # Will warn but not block - - eco_excellence: - carbon: 85 - energy: 85 - description: "Excellence in eco practices" - enforcement: none # Just a badge/recognition - -# Weights for composite health index -weights: - ecological: 0.4 # Carbon + Energy + Resources - economic: 0.3 # Pareto + Allocation + Debt - quality: 0.3 # Complexity + Coupling + Coverage - -# Analysis configuration -analysis: - # Languages to analyze - languages: - - python - - javascript - - typescript - - java - - go - - rust - - haskell - - ocaml - - # File patterns to exclude - exclude: - - "**/*.min.js" - - "**/node_modules/**" - - "**/vendor/**" - - "**/.git/**" - - "**/dist/**" - - "**/build/**" - - # Carbon estimation model - carbon: - grid_intensity: 475 # gCO2eq/kWh (global average) - hardware_lifespan: 4 # years - usage_percentage: 0.01 # 1% of hardware - - # Energy pattern detection - energy: - detect_busy_waiting: true - detect_polling: true - detect_inefficient_io: true - detect_resource_leaks: true - - # Complexity thresholds - complexity: - cyclomatic_warning: 10 - cyclomatic_critical: 20 - cognitive_warning: 15 - cognitive_critical: 30 - -# Database connections -databases: - arangodb: - url: "http://localhost:8529" - database: "oikos" - username: "${ARANGO_USER}" - password: "${ARANGO_PASSWORD}" - - virtuoso: - sparql_endpoint: "http://localhost:8890/sparql" - graph_uri: "https://oikos-bot.dev/knowledge" - -# External integrations -integrations: - github: - enabled: true - app_id: "${GITHUB_APP_ID}" - webhook_secret: "${GITHUB_WEBHOOK_SECRET}" - - gitlab: - enabled: true - token: "${GITLAB_TOKEN}" - webhook_secret: "${GITLAB_WEBHOOK_SECRET}" - - # Echidna for formal verification - echidna: - enabled: true - endpoint: "https://gitlab.com/hyperpolymath/echidna" - verify_pareto_claims: true - -# AI assistant integration -ai_assistants: - copilot: - generate_instructions: true - instruction_path: ".github/copilot-instructions.md" - update_on_analysis: true - - claude_code: - generate_instructions: true - instruction_path: ".claude-code/instructions.md" - update_on_analysis: true - -# Praxis loop configuration -praxis: - enabled: true - learning_rate: 0.01 - min_observations: 10 # Before updating policies - feedback_collection: true - - # What to learn from - observation_sources: - - pr_outcomes # Did merged PRs improve metrics? - - refactor_results # Did suggested refactors help? - - user_feedback # Explicit feedback on recommendations - -# Reporting configuration -reporting: - # PR comment format - pr_comments: - enabled: true - include_scores: true - include_recommendations: true - max_recommendations: 5 # In advisor mode - include_pareto_analysis: true - - # SARIF output for code scanning - sarif: - enabled: true - upload_to_github: true - - # OpenTelemetry metrics - metrics: - enabled: true - endpoint: "${OTEL_ENDPOINT}" - export_interval: 60 # seconds - - # Dashboard - dashboard: - enabled: true - port: 8080 - -# Notification configuration -notifications: - # Slack integration - slack: - enabled: false - webhook_url: "${SLACK_WEBHOOK}" - channels: - violations: "#oikos-bot-alerts" - weekly_report: "#engineering" - - # Email - email: - enabled: false - smtp_host: "${SMTP_HOST}" - from: "oikos-bot@example.com" - -# Logging -logging: - level: info # debug, info, warn, error - format: json - output: stdout diff --git a/bots/sustainabot/containers/Containerfile b/bots/sustainabot/containers/Containerfile deleted file mode 100644 index 29456299..00000000 --- a/bots/sustainabot/containers/Containerfile +++ /dev/null @@ -1,99 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Oikos Bot Container Image -# -# Build: vordr build -t oikos:latest -f containers/Containerfile . -# Run: vordr run -p 3000:3000 oikos:latest - -# ============================================================================= -# Stage 1: Placeholder for Haskell analyzer (skipped for MVP) -# ============================================================================= -# The Haskell analyzer will be added when fully implemented. -# For now, the bot uses mock analysis or calls external analyzer endpoint. -FROM docker.io/debian:bookworm-slim AS haskell-builder -RUN echo "Haskell analyzer skipped for MVP" > /build-status.txt - -# ============================================================================= -# Stage 2: Placeholder for OCaml analyzer (skipped for MVP) -# ============================================================================= -# The OCaml doc analyzer will be added when implemented. -FROM docker.io/debian:bookworm-slim AS ocaml-builder -RUN echo "OCaml analyzer skipped for MVP" > /build-status.txt - -# ============================================================================= -# Stage 3: ReScript bot integration (pre-compiled, Deno runtime) -# ============================================================================= -# NOTE: ReScript must be pre-compiled locally before building container. -# Run `deno task build:rescript` in bot-integration/ to generate .res.js files. -# We do NOT use npm in the container - Deno only. -FROM docker.io/denoland/deno:2.1.4 AS rescript-stage - -WORKDIR /build/bot - -# Just copy pre-compiled ReScript output and Deno config -COPY bot-integration/src/*.res.js ./src/ -COPY bot-integration/deno.json . -COPY bot-integration/bindings/ ./bindings/ -COPY bot-integration/rescript-runtime/ ./rescript-runtime/ - -# Cache Deno dependencies -RUN deno cache src/Oikos.res.js || true - -# ============================================================================= -# Stage 4: Final runtime image -# ============================================================================= -FROM docker.io/denoland/deno:2.1.4 AS runtime - -LABEL maintainer="hyperpolymath" -LABEL org.opencontainers.image.title="Oikos Bot" -LABEL org.opencontainers.image.description="Ecological & Economic Code Analysis Platform" -LABEL org.opencontainers.image.version="0.1.0-beta" -LABEL org.opencontainers.image.source="https://github.com/hyperpolymath/oikos-bot" -LABEL org.opencontainers.image.licenses="MPL-2.0" - -WORKDIR /app - -USER root - -# Install runtime dependencies -RUN apt-get update && apt-get install -y \ - git \ - jq \ - curl \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -# Copy built artifacts from previous stages -# NOTE: Haskell and OCaml analyzers are optional - if they didn't build, we skip them -# The bot will fall back to mock analysis if the analyzer is unavailable - -# ReScript bot (pre-compiled, Deno runtime) -COPY --from=rescript-stage /build/bot/src/*.res.js /app/bot-integration/src/ -COPY --from=rescript-stage /build/bot/deno.json /app/bot-integration/ -COPY --from=rescript-stage /build/bot/rescript-runtime/ /app/bot-integration/rescript-runtime/ - -# Copy policy engine -COPY policy-engine/ /app/policy-engine/ - -# Copy configuration -COPY config/ /app/config/ - -# Environment -ENV PORT=3000 -ENV BOT_MODE=advisor -ENV ANALYSIS_ENDPOINT=http://localhost:8080/analyze -ENV DENO_DIR=/app/.deno - -# Create deno cache directory -RUN mkdir -p /app/.deno && chown -R deno:deno /app - -# Switch to non-root user -USER deno - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD deno eval "const r = await fetch('http://localhost:3000/health'); if (!r.ok) Deno.exit(1);" || exit 1 - -EXPOSE 3000 - -# Default command -CMD ["run", "--allow-net", "--allow-env", "--allow-read", "/app/bot-integration/src/Oikos.res.js"] diff --git a/bots/sustainabot/containers/Containerfile.policy b/bots/sustainabot/containers/Containerfile.policy deleted file mode 100644 index db95b326..00000000 --- a/bots/sustainabot/containers/Containerfile.policy +++ /dev/null @@ -1,80 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2024-2025 hyperpolymath -# -# Oikos Bot Policy Engine Container -# Datalog + DeepProbLog based policy engine -# -# Build: podman build -t oikos-policy:latest -f containers/Containerfile.policy . - -FROM /cerro-torre AS builder - -WORKDIR /build - -# Install build dependencies -RUN guix install \ - python \ - python-pip \ - souffle \ - swi-prolog \ - git - -# Copy policy engine source -COPY policy-engine/ . - -# Create virtual environment and install dependencies -RUN python -m venv /opt/venv && \ - /opt/venv/bin/pip install --upgrade pip && \ - /opt/venv/bin/pip install \ - numpy \ - torch \ - networkx \ - pyyaml \ - aiohttp \ - pyarango \ - SPARQLWrapper - -# Install DeepProbLog -RUN /opt/venv/bin/pip install deepproblog - -# ============================================================================= -# Runtime -# ============================================================================= -FROM /cerro-torre AS runtime - -WORKDIR /app - -# Install runtime dependencies -RUN guix install \ - python \ - souffle \ - swi-prolog \ - jq - -# Copy virtual environment -COPY --from=builder /opt/venv /opt/venv - -# Copy policy engine -COPY --from=builder /build /app/policy-engine - -# Copy Datalog rules -COPY policy-engine/datalog/ /app/datalog/ - -# Copy DeepProbLog rules -COPY policy-engine/deepproblog/ /app/deepproblog/ - -# Environment -ENV PATH="/opt/venv/bin:$PATH" -ENV PYTHONUNBUFFERED=1 -ENV PORT=8081 - -# Create non-root user -RUN useradd -m -s /bin/bash policyengine -USER policyengine - -EXPOSE 8081 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8081/health')" - -CMD ["python", "-m", "policy_engine.server", "--port", "8081"] diff --git a/bots/sustainabot/containers/compose.yaml b/bots/sustainabot/containers/compose.yaml deleted file mode 100644 index 6bc64c9b..00000000 --- a/bots/sustainabot/containers/compose.yaml +++ /dev/null @@ -1,188 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Oikos Bot Compose Configuration -# -# Usage with Vörðr/Podman: -# podman-compose up -d -# podman-compose logs -f oikos -# podman-compose down - -name: oikos - -services: - # ========================================================================== - # Main Oikos Bot Service - # ========================================================================== - oikos: - build: - context: .. - dockerfile: containers/Containerfile - image: oikos:latest - container_name: oikos - restart: unless-stopped - ports: - - "3000:3000" - environment: - - PORT=3000 - - BOT_MODE=advisor - - ANALYSIS_ENDPOINT=https://analyzer:8080/analyze - - GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET:-} - - GITLAB_WEBHOOK_SECRET=${GITLAB_WEBHOOK_SECRET:-} - - ARANGO_URL=https://arangodb:8529 - - VIRTUOSO_URL=https://virtuoso:8890/sparql - depends_on: - - arangodb - - virtuoso - - analyzer - networks: - - oikos-network - healthcheck: - test: ["CMD", "deno", "eval", "const r = await fetch('http://localhost:3000/health'); if (!r.ok) Deno.exit(1);"] - interval: 30s - timeout: 10s - retries: 3 - - # ========================================================================== - # Analysis Service (Haskell + OCaml analyzers) - # ========================================================================== - analyzer: - build: - context: .. - dockerfile: containers/Containerfile - target: runtime - image: oikos-analyzer:latest - container_name: oikos-analyzer - restart: unless-stopped - ports: - - "8080:8080" - environment: - - PORT=8080 - networks: - - oikos-network - command: ["oikos-analyzer", "serve", "--port", "8080"] - - # ========================================================================== - # Policy Engine (Datalog + DeepProbLog) - # ========================================================================== - policy-engine: - build: - context: .. - dockerfile: containers/Containerfile.policy - image: oikos-policy:latest - container_name: oikos-policy - restart: unless-stopped - ports: - - "8081:8081" - environment: - - PORT=8081 - - ARANGO_URL=https://arangodb:8529 - - VIRTUOSO_URL=https://virtuoso:8890/sparql - depends_on: - - arangodb - - virtuoso - networks: - - oikos-network - volumes: - - policy-data:/app/data - - # ========================================================================== - # ArangoDB (Graph + Document Database) - # ========================================================================== - arangodb: - image: arangodb:3.11 - container_name: oikos-arangodb - restart: unless-stopped - ports: - - "8529:8529" - environment: - - ARANGO_ROOT_PASSWORD=${ARANGO_ROOT_PASSWORD:-oikos} - - ARANGO_NO_AUTH=${ARANGO_NO_AUTH:-0} - volumes: - - arangodb-data:/var/lib/arangodb3 - - ../databases/arangodb/schema.js:/docker-entrypoint-initdb.d/schema.js:ro - networks: - - oikos-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8529/_api/version"] - interval: 30s - timeout: 10s - retries: 5 - - # ========================================================================== - # Virtuoso (RDF Triple Store + SPARQL) - # ========================================================================== - virtuoso: - image: openlink/virtuoso-opensource-7:latest - container_name: oikos-virtuoso - restart: unless-stopped - ports: - - "8890:8890" - - "1111:1111" - environment: - - DBA_PASSWORD=${VIRTUOSO_DBA_PASSWORD:-oikos} - - SPARQL_UPDATE=true - - DEFAULT_GRAPH=https://oikos.dev/knowledge - volumes: - - virtuoso-data:/database - - ../databases/virtuoso/ontology.ttl:/opt/virtuoso-opensource/share/virtuoso/vad/ontology.ttl:ro - networks: - - oikos-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8890/sparql?query=ASK%20%7B%7D"] - interval: 30s - timeout: 10s - retries: 5 - - # ========================================================================== - # Prometheus (Metrics) - # ========================================================================== - prometheus: - image: prom/prometheus:latest - container_name: oikos-prometheus - restart: unless-stopped - ports: - - "9090:9090" - volumes: - - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - - prometheus-data:/prometheus - networks: - - oikos-network - command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.path=/prometheus' - - # ========================================================================== - # Grafana (Dashboard) - # ========================================================================== - grafana: - image: grafana/grafana:latest - container_name: oikos-grafana - restart: unless-stopped - ports: - - "3001:3000" - environment: - - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-oikos} - - GF_USERS_ALLOW_SIGN_UP=false - volumes: - - grafana-data:/var/lib/grafana - - ./grafana/provisioning:/etc/grafana/provisioning:ro - depends_on: - - prometheus - networks: - - oikos-network - -# ============================================================================= -# Networks -# ============================================================================= -networks: - oikos-network: - driver: bridge - -# ============================================================================= -# Volumes -# ============================================================================= -volumes: - arangodb-data: - virtuoso-data: - policy-data: - prometheus-data: - grafana-data: diff --git a/bots/sustainabot/containers/prometheus.yml b/bots/sustainabot/containers/prometheus.yml deleted file mode 100644 index 595b9d6e..00000000 --- a/bots/sustainabot/containers/prometheus.yml +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Prometheus configuration for Oikos Bot metrics - -global: - scrape_interval: 15s - evaluation_interval: 15s - -alerting: - alertmanagers: - - static_configs: - - targets: [] - -rule_files: [] - -scrape_configs: - # Prometheus self-monitoring - - job_name: 'prometheus' - static_configs: - - targets: ['localhost:9090'] - - # Oikos main service - - job_name: 'oikos' - static_configs: - - targets: ['oikos:3000'] - metrics_path: '/metrics' - - # Analysis service - - job_name: 'oikos-analyzer' - static_configs: - - targets: ['analyzer:8080'] - metrics_path: '/metrics' - - # Policy engine - - job_name: 'oikos-policy' - static_configs: - - targets: ['policy-engine:8081'] - metrics_path: '/metrics' - - # ArangoDB - - job_name: 'arangodb' - static_configs: - - targets: ['arangodb:8529'] - metrics_path: '/_admin/metrics/v2' - basic_auth: - username: 'root' - password_file: '/etc/prometheus/arango_password' diff --git a/bots/sustainabot/containers/vordr-build.sh b/bots/sustainabot/containers/vordr-build.sh deleted file mode 100755 index 10347bc4..00000000 --- a/bots/sustainabot/containers/vordr-build.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -# -# Oikos Container Build Script -# -# Builds all container images using Vörðr (Svalinn OCI runtime) -# https://github.com/hyperpolymath/svalinn - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -log_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -log_step() { - echo -e "${CYAN}[STEP]${NC} $1" -} - -# Check for container runtime (prefer vordr, fallback to podman) -detect_runtime() { - if command -v vordr &> /dev/null; then - echo "vordr" - elif command -v podman &> /dev/null; then - echo "podman" - else - log_error "No container runtime found." - log_info "Install Vörðr: https://github.com/hyperpolymath/svalinn" - log_info "Or fallback: sudo dnf install podman" - exit 1 - fi -} - -RUNTIME=$(detect_runtime) -log_info "Using container runtime: ${RUNTIME}" - -# Display Vörðr version if available -if [[ "$RUNTIME" == "vordr" ]]; then - log_info "Vörðr version: $(vordr --version 2>/dev/null || echo 'unknown')" -fi - -cd "$PROJECT_ROOT" - -# Ensure AffineScript is pre-compiled -if [[ ! -f "bot-integration/src/Oikos.affine.js" ]]; then - log_warn "AffineScript not compiled. Running build..." - cd bot-integration - if command -v deno &> /dev/null; then - deno task build:affinescript - else - log_error "Deno not found. Please install Deno and run: deno task build:affinescript" - exit 1 - fi - cd "$PROJECT_ROOT" -fi - -# Build main Oikos image -log_step "Building oikos:latest..." -$RUNTIME build \ - --tag oikos:latest \ - --file containers/Containerfile \ - --progress=plain \ - . - -# Build policy engine image -log_step "Building oikos-policy:latest..." -$RUNTIME build \ - --tag oikos-policy:latest \ - --file containers/Containerfile.policy \ - --progress=plain \ - . - -# Tag images with version -VERSION=$(git describe --tags --always 2>/dev/null || echo "0.1.0-beta") -log_info "Tagging images with version: $VERSION" - -$RUNTIME tag oikos:latest "oikos:$VERSION" -$RUNTIME tag oikos-policy:latest "oikos-policy:$VERSION" - -# List built images -log_info "Built images:" -$RUNTIME images | grep oikos || true - -log_info "Build complete!" -echo "" -echo "To run the stack:" -if [[ "$RUNTIME" == "vordr" ]]; then - echo " vordr run -d -p 3000:3000 --name oikos oikos:latest" - echo "" - echo "To run with compose (requires podman-compose or docker-compose):" -fi -echo " cd containers && podman-compose up -d" -echo "" -echo "To push to registry:" -echo " $RUNTIME push oikos:$VERSION" diff --git a/bots/sustainabot/crates/sustainabot-analysis/Cargo.toml b/bots/sustainabot/crates/sustainabot-analysis/Cargo.toml deleted file mode 100644 index b8a3365c..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -[package] -name = "sustainabot-analysis" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true - -[features] -default = [] -panic-attack = ["dep:panic-attack"] - -[dependencies] -sustainabot-metrics = { path = "../sustainabot-metrics" } -tree-sitter.workspace = true -tree-sitter-rust.workspace = true -tree-sitter-javascript.workspace = true -tree-sitter-python.workspace = true -anyhow.workspace = true -thiserror.workspace = true -serde.workspace = true -serde_json.workspace = true -tracing.workspace = true -toml.workspace = true -panic-attack = { path = "../../../panic-attacker", optional = true } diff --git a/bots/sustainabot/crates/sustainabot-analysis/src/analyzer.rs b/bots/sustainabot/crates/sustainabot-analysis/src/analyzer.rs deleted file mode 100644 index 6fab6f5d..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/src/analyzer.rs +++ /dev/null @@ -1,256 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! Core analysis engine using tree-sitter AST - -use crate::carbon::estimate_carbon; -use crate::language::Language; -use crate::patterns::detect_patterns; -use anyhow::{Context, Result}; -use std::fs; -use std::path::Path; -use sustainabot_metrics::*; -use tree_sitter::{Parser, Tree}; - -pub struct Analyzer { - _language: Language, - parser: Parser, - file_path: Option, -} - -impl Analyzer { - pub fn new(language: Language) -> Result { - let mut parser = Parser::new(); - parser - .set_language(&language.parser()) - .context("Failed to set parser language")?; - - Ok(Analyzer { - _language: language, - parser, - file_path: None, - }) - } - - pub fn analyze_file(&mut self, path: &Path) -> Result> { - let source = fs::read_to_string(path) - .with_context(|| format!("Failed to read file: {}", path.display()))?; - - self.file_path = Some(path.display().to_string()); - let results = self.analyze_source(&source); - self.file_path = None; - results - } - - pub fn analyze_source(&mut self, source: &str) -> Result> { - let tree = self - .parser - .parse(source, None) - .context("Failed to parse source")?; - - self.analyze_tree(source, &tree) - } - - fn analyze_tree(&self, source: &str, tree: &Tree) -> Result> { - let mut results = Vec::new(); - - // Walk the AST and analyze each function - let root = tree.root_node(); - let mut cursor = root.walk(); - - self.visit_node(source, &root, &mut cursor, &mut results); - - Ok(results) - } - - fn visit_node( - &self, - source: &str, - node: &tree_sitter::Node, - cursor: &mut tree_sitter::TreeCursor, - results: &mut Vec, - ) { - // Analyze functions - if self.is_function_node(node) { - if let Some(result) = self.analyze_function(source, node) { - results.push(result); - } - } - - // Recurse to children - if cursor.goto_first_child() { - loop { - let child = cursor.node(); - self.visit_node(source, &child, cursor, results); - - if !cursor.goto_next_sibling() { - break; - } - } - cursor.goto_parent(); - } - } - - fn is_function_node(&self, node: &tree_sitter::Node) -> bool { - matches!( - node.kind(), - "function_item" // Rust - | "function_declaration" // JS - | "arrow_function" // JS - | "method_declaration" // JS - | "function_definition" // Python - ) - } - - fn analyze_function(&self, source: &str, node: &tree_sitter::Node) -> Option { - let location = self.node_location(source, node)?; - - // Estimate resources based on code patterns - let complexity = self.estimate_complexity(node); - let resources = self.estimate_resources(complexity); - - // Detect problematic patterns - let pattern_matches = detect_patterns(source, node); - let patterns: Vec = pattern_matches.iter().map(|p| p.name.clone()).collect(); - let recommendations = self.generate_recommendations(&patterns); - - // Derive rule_id and suggestion from most significant pattern - let (rule_id, suggestion) = if let Some(pm) = pattern_matches.first() { - ( - format!("sustainabot/{}", pm.name), - pm.suggestion.clone(), - ) - } else { - ("sustainabot/general".to_string(), None) - }; - - // Calculate scores - let eco_score = self.calculate_eco_score(&resources); - let econ_score = self.calculate_econ_score(complexity); - let quality_score = self.calculate_quality_score(complexity); - - let health = HealthIndex::compute(eco_score, econ_score, quality_score); - - let end = node.end_position(); - - Some(AnalysisResult { - location, - resources, - health, - recommendations, - rule_id, - suggestion, - end_location: Some((end.row + 1, end.column + 1)), - confidence: sustainabot_metrics::Confidence::Estimated, - }) - } - - fn node_location(&self, source: &str, node: &tree_sitter::Node) -> Option { - let start = node.start_position(); - let end = node.end_position(); - let name = self.extract_function_name(source, node); - - Some(CodeLocation { - file: self - .file_path - .clone() - .unwrap_or_else(|| String::from("")), - line: start.row + 1, - column: start.column + 1, - end_line: Some(end.row + 1), - end_column: Some(end.column + 1), - name, - }) - } - - fn extract_function_name(&self, source: &str, node: &tree_sitter::Node) -> Option { - // Try to find name node (language-specific) - let mut cursor = node.walk(); - if cursor.goto_first_child() { - loop { - let child = cursor.node(); - if child.kind() == "identifier" { - return Some(child.utf8_text(source.as_bytes()).ok()?.to_string()); - } - if !cursor.goto_next_sibling() { - break; - } - } - } - None - } - - fn estimate_complexity(&self, node: &tree_sitter::Node) -> usize { - // Simple complexity: count nodes - let mut count = 1; - let mut cursor = node.walk(); - - if cursor.goto_first_child() { - loop { - count += self.estimate_complexity(&cursor.node()); - if !cursor.goto_next_sibling() { - break; - } - } - cursor.goto_parent(); - } - - count - } - - fn estimate_resources(&self, complexity: usize) -> ResourceProfile { - // Baseline estimates (will be improved with profiling data) - let energy = Energy::joules(complexity as f64 * 0.1); - let duration = Duration::milliseconds(complexity as f64 * 0.5); - let carbon = estimate_carbon(energy); - let memory = Memory::kilobytes(complexity * 2); - - ResourceProfile { - energy, - duration, - carbon, - memory, - } - } - - fn calculate_eco_score(&self, resources: &ResourceProfile) -> EcoScore { - // Lower resource usage = higher score - // Baseline: 100J = 50 score, scale logarithmically - let energy_score = (100.0 - (resources.energy.0.ln() * 10.0)).max(0.0); - EcoScore::new(energy_score) - } - - fn calculate_econ_score(&self, complexity: usize) -> EconScore { - // Lower complexity = higher efficiency - let score = (100.0 - (complexity as f64 * 0.5)).max(0.0); - EconScore::new(score) - } - - fn calculate_quality_score(&self, complexity: usize) -> f64 { - // Simple quality metric based on complexity - (100.0 - (complexity as f64 * 0.3)).max(0.0) - } - - fn generate_recommendations(&self, patterns: &[String]) -> Vec { - let mut recs = Vec::new(); - - for pattern in patterns { - match pattern.as_str() { - "busy-wait" => recs.push("Replace busy-wait loop with async/await or blocking sleep".to_string()), - "nested-loops" => recs.push("Consider algorithm optimization to reduce nested iterations".to_string()), - "large-allocation" => recs.push("Review memory allocation - consider streaming or chunking".to_string()), - "string-concat-in-loop" => recs.push("Use String::with_capacity + push_str or collect with iterators".to_string()), - "clone-in-loop" => recs.push("Consider borrowing instead of cloning inside loop body".to_string()), - "unbuffered-io" => recs.push("Wrap File with BufReader/BufWriter to reduce syscall overhead".to_string()), - "redundant-allocation" => recs.push("Accept &str instead of String where borrow suffices".to_string()), - _ => {} - } - } - - if recs.is_empty() { - recs.push("Code looks efficient - keep up the good work!".to_string()); - } - - recs - } -} diff --git a/bots/sustainabot/crates/sustainabot-analysis/src/calibration.rs b/bots/sustainabot/crates/sustainabot-analysis/src/calibration.rs deleted file mode 100644 index 0e21c17c..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/src/calibration.rs +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! Calibration framework for resource estimation. -//! -//! Replaces naive `complexity * 0.1 J` with pattern-based resource profiles -//! producing ranges (min, typical, max) instead of single numbers. - -use sustainabot_metrics::{Confidence, Energy, Duration, Memory, ResourceProfile}; -use crate::carbon::estimate_carbon; - -/// Resource estimate with min/typical/max range -#[derive(Debug, Clone)] -pub struct ResourceRange { - pub min: ResourceProfile, - pub typical: ResourceProfile, - pub max: ResourceProfile, - pub confidence: Confidence, -} - -impl ResourceRange { - /// Return the typical estimate as a single profile - pub fn as_typical(&self) -> &ResourceProfile { - &self.typical - } -} - -/// Operation categories for calibrated estimates -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OperationKind { - /// HashMap/BTreeMap lookup - HashLookup, - /// Sorting (comparison-based) - Sort, - /// File I/O (read or write) - FileIO, - /// Network call (HTTP request) - NetworkCall, - /// Heap allocation - Allocation, - /// String operations (format, concat) - StringOp, - /// Mathematical computation - MathCompute, - /// Generic/unknown operation - Generic, -} - -/// Calibrated 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. -pub fn estimate_operation(kind: OperationKind, n: usize) -> ResourceRange { - match kind { - OperationKind::HashLookup => { - // O(1) amortized, ~50ns per lookup - let count = n.max(1) as f64; - 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, - } - } - OperationKind::Sort => { - // O(n log n), ~100ns per comparison - let nf = n.max(1) as f64; - let nlogn = nf * nf.log2().max(1.0); - 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, - } - } - OperationKind::FileIO => { - // ~1ms per syscall, ~10μJ per 4KB page - let pages = (n / 4096).max(1) as f64; - ResourceRange { - min: profile(0.01 * pages, 0.5 * pages, n), - typical: profile(0.05 * pages, 2.0 * pages, n + 4096), - max: profile(0.5 * pages, 20.0 * pages, n + 65536), - confidence: Confidence::Estimated, - } - } - OperationKind::NetworkCall => { - // ~50ms per request, ~0.5J for a typical HTTPS request - ResourceRange { - min: profile(0.05, 10.0, 4096), - typical: profile(0.5, 50.0, 65536), - max: profile(5.0, 500.0, 1_048_576), - confidence: Confidence::Estimated, - } - } - OperationKind::Allocation => { - // ~10ns per allocation, ~1nJ per byte - let bytes = n.max(1) as f64; - 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, - } - } - OperationKind::StringOp => { - // ~100ns per string operation, proportional to length - let len = n.max(1) as f64; - ResourceRange { - min: profile(0.000005 * len, 0.0001 * len, n + 64), - typical: profile(0.00005 * len, 0.001 * len, 2 * n + 128), - max: profile(0.0005 * len, 0.01 * len, 4 * n + 256), - confidence: Confidence::Estimated, - } - } - OperationKind::MathCompute => { - // ~5ns per FP operation - let ops = n.max(1) as f64; - 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, - } - } - OperationKind::Generic => { - // Fallback: linear in complexity - let c = n.max(1) as f64; - ResourceRange { - min: profile(0.01 * c, 0.05 * c, 512 * n.max(1)), - typical: profile(0.1 * c, 0.5 * c, 2048 * n.max(1)), - max: profile(1.0 * c, 5.0 * c, 8192 * n.max(1)), - confidence: Confidence::Unknown, - } - } - } -} - -/// Estimate resources for a function based on its complexity and detected patterns. -pub fn calibrated_estimate(complexity: usize, patterns: &[String]) -> ResourceProfile { - // Start with generic estimate based on complexity - let base = estimate_operation(OperationKind::Generic, complexity); - let mut result = base.typical.clone(); - - // Apply pattern-based adjustments - for pattern in patterns { - let multiplier = match pattern.as_str() { - "nested-loops" => 3.0, - "busy-wait" => 5.0, - "string-concat-in-loop" => 2.0, - "clone-in-loop" => 1.5, - "unbuffered-io" => 3.0, - "large-allocation" => 2.0, - "redundant-allocation" => 1.2, - _ => 1.0, - }; - - result.energy = Energy::joules(result.energy.0 * multiplier); - result.duration = Duration::milliseconds(result.duration.0 * multiplier); - result.carbon = estimate_carbon(result.energy); - } - - result -} - -fn profile(energy_j: f64, duration_ms: f64, memory_bytes: usize) -> ResourceProfile { - let energy = Energy::joules(energy_j); - ResourceProfile { - energy, - duration: Duration::milliseconds(duration_ms), - carbon: estimate_carbon(energy), - memory: Memory::bytes(memory_bytes), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_hash_lookup_range() { - 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); - } - - #[test] - fn test_calibrated_estimate_with_patterns() { - let base = calibrated_estimate(10, &[]); - let with_pattern = calibrated_estimate(10, &["nested-loops".to_string()]); - // Pattern should increase energy - assert!(with_pattern.energy.0 > base.energy.0); - } - - #[test] - fn test_network_call_energy() { - let range = estimate_operation(OperationKind::NetworkCall, 1); - // Network calls should be significant energy users - assert!(range.typical.energy.0 >= 0.1); - } -} diff --git a/bots/sustainabot/crates/sustainabot-analysis/src/carbon.rs b/bots/sustainabot/crates/sustainabot-analysis/src/carbon.rs deleted file mode 100644 index 79de6081..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/src/carbon.rs +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! Carbon emission estimation - -use sustainabot_metrics::{Carbon, Energy}; - -/// Estimate carbon emissions from energy consumption -/// -/// Uses average grid carbon intensity. In future, this will integrate -/// with real-time APIs (ElectricityMaps, WattTime, etc.) -pub fn estimate_carbon(energy: Energy) -> Carbon { - // Average grid intensity: ~475 gCO2e/kWh globally - // 1 kWh = 3,600,000 J - // So: gCO2e = J * (475 / 3,600,000) - const CARBON_INTENSITY: f64 = 475.0 / 3_600_000.0; - - Carbon::grams_co2e(energy.0 * CARBON_INTENSITY) -} - -/// Get real-time carbon intensity for a location (stub for now) -pub async fn get_carbon_intensity(_location: &str) -> f64 { - // TODO: Integrate with ElectricityMaps or WattTime API - // For now, return global average - 475.0 // gCO2e/kWh -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_carbon_estimation() { - // 100 J should produce ~0.0132 gCO2e with 475 intensity - let energy = Energy::joules(100.0); - let carbon = estimate_carbon(energy); - - assert!(carbon.0 > 0.01 && carbon.0 < 0.02); - } -} diff --git a/bots/sustainabot/crates/sustainabot-analysis/src/dependencies.rs b/bots/sustainabot/crates/sustainabot-analysis/src/dependencies.rs deleted file mode 100644 index f8d3b0de..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/src/dependencies.rs +++ /dev/null @@ -1,251 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! Dependency analysis for sustainability. -//! -//! Parses Cargo.toml and package.json to flag heavy dependencies -//! and suggest minimal feature sets. - -use anyhow::{Context, Result}; -use std::path::Path; -use sustainabot_metrics::*; - -/// Known heavy Rust dependencies and their lighter alternatives/fixes -const HEAVY_RUST_DEPS: &[(&str, &str)] = &[ - ("reqwest", "Consider using ureq for simple HTTP (no async runtime needed)"), - ("tokio", "If only using basic features, specify minimal feature set instead of 'full'"), - ("serde_yaml", "Consider using serde_yml or minimal YAML parser if only reading configs"), - ("openssl", "Consider using rustls for TLS (pure Rust, smaller footprint)"), - ("diesel", "For simple queries, consider sqlx with compile-time checked queries"), - ("chrono", "Consider using time crate (smaller, no C dependency)"), - ("num-bigint", "If only using basic math, std library may suffice"), - ("regex", "For simple patterns, consider using glob or manual matching"), - ("hyper", "For simple HTTP servers, consider using tiny_http"), - ("image", "Specify only needed format features (e.g., features = [\"png\"])"), -]; - -/// Known heavy npm dependencies -const HEAVY_NPM_DEPS: &[(&str, &str)] = &[ - ("moment", "Replace with dayjs or date-fns (tree-shakeable, much smaller)"), - ("lodash", "Use native JS methods or lodash-es for tree-shaking"), - ("axios", "Use native fetch API (available in modern browsers and Deno)"), - ("express", "Consider Hono or Fastify for better performance"), - ("webpack", "Consider Vite or esbuild for faster builds"), - ("jquery", "Use native DOM APIs"), - ("underscore", "Use native JS methods"), - ("request", "Deprecated — use native fetch or undici"), - ("bluebird", "Use native Promise"), - ("chalk", "Use picocolors (much smaller)"), -]; - -/// Dependency finding -#[derive(Debug, Clone)] -pub struct DepFinding { - pub dep_name: String, - pub manifest: String, - pub suggestion: String, - pub uses_all_features: bool, -} - -/// Analyze dependencies in a project directory. -pub fn analyze_dependencies(repo_path: &Path) -> Vec { - let mut results = Vec::new(); - - // Check Cargo.toml - let cargo_toml = repo_path.join("Cargo.toml"); - if cargo_toml.exists() { - if let Ok(findings) = analyze_cargo_toml(&cargo_toml) { - for finding in findings { - results.push(dep_finding_to_result(finding)); - } - } - } - - // Check package.json - let package_json = repo_path.join("package.json"); - if package_json.exists() { - if let Ok(findings) = analyze_package_json(&package_json) { - for finding in findings { - results.push(dep_finding_to_result(finding)); - } - } - } - - results -} - -fn analyze_cargo_toml(path: &Path) -> Result> { - let content = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read {}", path.display()))?; - - let table: toml::Value = toml::from_str(&content) - .with_context(|| format!("Failed to parse {}", path.display()))?; - - let mut findings = Vec::new(); - let manifest = path.display().to_string(); - - // Check [dependencies] - if let Some(deps) = table.get("dependencies").and_then(|d| d.as_table()) { - for (name, value) in deps { - check_rust_dep(name, value, &manifest, &mut findings); - } - } - - // Check [dev-dependencies] - if let Some(deps) = table.get("dev-dependencies").and_then(|d| d.as_table()) { - for (name, value) in deps { - check_rust_dep(name, value, &manifest, &mut findings); - } - } - - Ok(findings) -} - -fn check_rust_dep(name: &str, value: &toml::Value, manifest: &str, findings: &mut Vec) { - // Check if it's a known heavy dep - if let Some((_, suggestion)) = HEAVY_RUST_DEPS.iter().find(|(dep, _)| *dep == name) { - let uses_all = check_uses_all_features(value); - findings.push(DepFinding { - dep_name: name.to_string(), - manifest: manifest.to_string(), - suggestion: suggestion.to_string(), - uses_all_features: uses_all, - }); - } - - // Flag deps using features = ["full"] or default-features = true with many features - if check_uses_all_features(value) { - let already_flagged = findings.iter().any(|f| f.dep_name == name); - if !already_flagged { - findings.push(DepFinding { - dep_name: name.to_string(), - manifest: manifest.to_string(), - suggestion: format!( - "Dependency '{}' uses all features. Consider specifying only needed features.", - name - ), - uses_all_features: true, - }); - } - } -} - -fn check_uses_all_features(value: &toml::Value) -> bool { - if let Some(table) = value.as_table() { - if let Some(features) = table.get("features").and_then(|f| f.as_array()) { - return features.iter().any(|f| f.as_str() == Some("full")); - } - } - false -} - -fn analyze_package_json(path: &Path) -> Result> { - let content = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read {}", path.display()))?; - - let json: serde_json::Value = serde_json::from_str(&content) - .with_context(|| format!("Failed to parse {}", path.display()))?; - - let mut findings = Vec::new(); - let manifest = path.display().to_string(); - - // Check dependencies - if let Some(deps) = json.get("dependencies").and_then(|d| d.as_object()) { - for name in deps.keys() { - if let Some((_, suggestion)) = HEAVY_NPM_DEPS.iter().find(|(dep, _)| *dep == name.as_str()) { - findings.push(DepFinding { - dep_name: name.clone(), - manifest: manifest.clone(), - suggestion: suggestion.to_string(), - uses_all_features: false, - }); - } - } - } - - // Check devDependencies too - if let Some(deps) = json.get("devDependencies").and_then(|d| d.as_object()) { - for name in deps.keys() { - if let Some((_, suggestion)) = HEAVY_NPM_DEPS.iter().find(|(dep, _)| *dep == name.as_str()) { - findings.push(DepFinding { - dep_name: name.clone(), - manifest: manifest.clone(), - suggestion: suggestion.to_string(), - uses_all_features: false, - }); - } - } - } - - Ok(findings) -} - -fn dep_finding_to_result(finding: DepFinding) -> AnalysisResult { - let severity_boost = if finding.uses_all_features { 2.0 } else { 1.0 }; - - AnalysisResult { - location: CodeLocation { - file: finding.manifest, - line: 0, - column: 0, - end_line: None, - end_column: None, - name: Some(format!("dep:{}", finding.dep_name)), - }, - resources: ResourceProfile { - energy: Energy::joules(10.0 * severity_boost), - duration: Duration::milliseconds(50.0 * severity_boost), - carbon: crate::carbon::estimate_carbon(Energy::joules(10.0 * severity_boost)), - memory: Memory::kilobytes(500), - }, - health: HealthIndex::compute( - EcoScore::new(if finding.uses_all_features { 40.0 } else { 60.0 }), - EconScore::new(50.0), - 50.0, - ), - recommendations: vec![finding.suggestion.clone()], - rule_id: "sustainabot/heavy-dependency".to_string(), - suggestion: Some(finding.suggestion), - end_location: None, - confidence: Confidence::Estimated, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_heavy_dep_detection() { - let value: toml::Value = toml::from_str(r#"version = "1.0""#).unwrap(); - let mut findings = Vec::new(); - check_rust_dep("reqwest", &value, "Cargo.toml", &mut findings); - assert_eq!(findings.len(), 1); - assert_eq!(findings[0].dep_name, "reqwest"); - } - - #[test] - fn test_all_features_detection() { - // Build the TOML value as a table directly - let mut table = toml::map::Map::new(); - table.insert("version".to_string(), toml::Value::String("1.0".to_string())); - table.insert( - "features".to_string(), - toml::Value::Array(vec![toml::Value::String("full".to_string())]), - ); - let value = toml::Value::Table(table); - assert!(check_uses_all_features(&value)); - } - - #[test] - fn test_no_all_features() { - let mut table = toml::map::Map::new(); - table.insert("version".to_string(), toml::Value::String("1.0".to_string())); - table.insert( - "features".to_string(), - toml::Value::Array(vec![toml::Value::String("json".to_string())]), - ); - let value = toml::Value::Table(table); - assert!(!check_uses_all_features(&value)); - } -} diff --git a/bots/sustainabot/crates/sustainabot-analysis/src/directives.rs b/bots/sustainabot/crates/sustainabot-analysis/src/directives.rs deleted file mode 100644 index 9fc312fe..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/src/directives.rs +++ /dev/null @@ -1,255 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! Parser for `.machine_readable/bot_directives/*.a2ml` files. -//! -//! These files control what bots are allowed to do in a given repository. -//! The format is TOML-shaped A2ML; the SCM form was retired 2026-04-17. - -use anyhow::{Context, Result}; -use serde::Deserialize; -use std::path::Path; - -/// A parsed bot directive -#[derive(Debug, Clone)] -pub struct BotDirective { - /// Bot name this directive applies to - pub bot: String, - /// Whether this bot is allowed to run - pub allow: bool, - /// Scopes the bot is allowed to operate in - pub scopes: Vec, - /// Scopes explicitly denied - pub deny: Vec, - /// Freeform notes - pub notes: Option, - /// Custom threshold overrides - pub thresholds: Vec<(String, f64)>, -} - -/// Raw A2ML shape (TOML deserialization target). Fields here mirror the -/// migration-script output at `.machine_readable/bot_directives/.a2ml`. -#[derive(Debug, Deserialize)] -struct DirectiveFile { - #[serde(default)] - bot: Option, - /// Either a single scope string or a list of scopes. - #[serde(default)] - scope: Option, - #[serde(default)] - scopes: Option>, - #[serde(default)] - allow: Option, - #[serde(default)] - deny: Option>, - #[serde(default)] - notes: Option, - #[serde(default)] - thresholds: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum ScopeField { - One(String), - Many(Vec), -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum AllowField { - Bool(bool), - Scopes(Vec), -} - -/// Check if a specific bot has a directive in the given repo. -/// -/// Looks for `.machine_readable/bot_directives/{bot_name}.a2ml`. Returns -/// `None` if the file does not exist or fails to parse. -pub fn check_directive(repo_path: &Path, bot_name: &str) -> Option { - let path = repo_path - .join(".machine_readable") - .join("bot_directives") - .join(format!("{}.a2ml", bot_name)); - - if !path.exists() { - return None; - } - - match parse_directive(&path, bot_name) { - Ok(d) => Some(d), - Err(e) => { - tracing::warn!("Failed to parse directive {}: {}", path.display(), e); - None - } - } -} - -/// Parse a bot directive A2ML file. -fn parse_directive(path: &Path, bot_name: &str) -> Result { - let content = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read directive: {}", path.display()))?; - - let file: DirectiveFile = toml::from_str(&content) - .with_context(|| format!("Failed to parse A2ML: {}", path.display()))?; - - let mut scopes: Vec = match file.scope { - Some(ScopeField::One(s)) => vec![s], - Some(ScopeField::Many(v)) => v, - None => Vec::new(), - }; - if let Some(mut extra) = file.scopes { - scopes.append(&mut extra); - } - - let allow = match file.allow { - // Plain boolean: honour as-is. - Some(AllowField::Bool(b)) => b, - // List-of-scopes: treat as allow = true + union the list into scopes. - Some(AllowField::Scopes(list)) => { - scopes.extend(list); - true - } - // No allow field → default to allowed (conservative parse). - None => true, - }; - - let thresholds = file - .thresholds - .unwrap_or_default() - .into_iter() - .filter_map(|(k, v)| v.as_float().map(|f| (k, f))) - .collect(); - - Ok(BotDirective { - bot: file.bot.unwrap_or_else(|| bot_name.to_string()), - allow, - scopes, - deny: file.deny.unwrap_or_default(), - notes: file.notes, - thresholds, - }) -} - -/// Check if the directive allows a specific scope -pub fn is_scope_allowed(directive: &BotDirective, scope: &str) -> bool { - if !directive.allow { - return false; - } - - // If deny list contains this scope, it's denied - if directive.deny.iter().any(|d| d == scope) { - return false; - } - - // If scopes list is empty, all scopes are allowed - if directive.scopes.is_empty() { - return true; - } - - // Otherwise, scope must be in the allow list - directive.scopes.iter().any(|s| s == scope) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::TempDir; - - fn write_directive(dir: &Path, bot: &str, body: &str) { - let d = dir.join(".machine_readable").join("bot_directives"); - fs::create_dir_all(&d).unwrap(); - fs::write(d.join(format!("{}.a2ml", bot)), body).unwrap(); - } - - #[test] - fn test_default_directive() { - let d = BotDirective { - bot: "test".to_string(), - allow: true, - scopes: vec![], - deny: vec![], - notes: None, - thresholds: vec![], - }; - assert!(is_scope_allowed(&d, "anything")); - } - - #[test] - fn test_denied_scope() { - let d = BotDirective { - bot: "test".to_string(), - allow: true, - scopes: vec![], - deny: vec!["security".to_string()], - notes: None, - thresholds: vec![], - }; - assert!(!is_scope_allowed(&d, "security")); - assert!(is_scope_allowed(&d, "eco")); - } - - #[test] - fn test_fully_denied() { - let d = BotDirective { - bot: "test".to_string(), - allow: false, - scopes: vec![], - deny: vec![], - notes: None, - thresholds: vec![], - }; - assert!(!is_scope_allowed(&d, "anything")); - } - - #[test] - fn test_parse_typical_bot_directive() { - let dir = TempDir::new().unwrap(); - write_directive( - dir.path(), - "echidnabot", - r#" -schema_version = "1.0" -directive_type = "bot-directive" -bot = "echidnabot" -scope = "formal verification and fuzzing" -allow = ["analysis", "fuzzing", "proof checks"] -deny = ["write to core modules", "write to bindings"] -notes = "May open findings; code changes require explicit approval" -"#, - ); - - let directive = check_directive(dir.path(), "echidnabot").expect("should parse"); - assert_eq!(directive.bot, "echidnabot"); - assert!(directive.allow); - assert!(directive.scopes.contains(&"fuzzing".to_string())); - assert!(directive.scopes.contains(&"formal verification and fuzzing".to_string())); - assert!(directive.deny.contains(&"write to core modules".to_string())); - assert!(directive.notes.is_some()); - } - - #[test] - fn test_parse_allow_false() { - let dir = TempDir::new().unwrap(); - write_directive( - dir.path(), - "rhodibot", - r#" -schema_version = "1.0" -bot = "rhodibot" -allow = false -"#, - ); - - let directive = check_directive(dir.path(), "rhodibot").expect("should parse"); - assert!(!directive.allow); - assert!(!is_scope_allowed(&directive, "anything")); - } - - #[test] - fn test_missing_file_returns_none() { - let dir = TempDir::new().unwrap(); - assert!(check_directive(dir.path(), "nonexistent").is_none()); - } -} diff --git a/bots/sustainabot/crates/sustainabot-analysis/src/language.rs b/bots/sustainabot/crates/sustainabot-analysis/src/language.rs deleted file mode 100644 index 5eaeae99..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/src/language.rs +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! Language detection and support - -use anyhow::{bail, Result}; -use std::path::Path; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Language { - Rust, - JavaScript, - TypeScript, - Python, -} - -impl Language { - /// Detect language from file extension - pub fn detect(path: &Path) -> Result { - let ext = path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or(""); - - match ext { - "rs" => Ok(Language::Rust), - "js" | "mjs" | "cjs" => Ok(Language::JavaScript), - "ts" | "mts" | "cts" => Ok(Language::TypeScript), - "py" | "pyw" => Ok(Language::Python), - _ => bail!("Unsupported file extension: {}", ext), - } - } - - /// Get tree-sitter parser for this language - pub fn parser(&self) -> tree_sitter::Language { - match self { - Language::Rust => tree_sitter_rust::LANGUAGE.into(), - Language::JavaScript | Language::TypeScript => { - tree_sitter_javascript::LANGUAGE.into() - } - Language::Python => tree_sitter_python::LANGUAGE.into(), - } - } - - /// Human-readable name - pub fn name(&self) -> &'static str { - match self { - Language::Rust => "Rust", - Language::JavaScript => "JavaScript", - Language::TypeScript => "TypeScript", - Language::Python => "Python", - } - } -} diff --git a/bots/sustainabot/crates/sustainabot-analysis/src/lib.rs b/bots/sustainabot/crates/sustainabot-analysis/src/lib.rs deleted file mode 100644 index b2ba7ee8..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/src/lib.rs +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! # SustainaBot Analysis Engine -//! -//! AST-based code analysis for ecological and economic metrics. -//! Built with Eclexia principles: explicit resource tracking. - -pub mod analyzer; -pub mod calibration; -pub mod carbon; -pub mod dependencies; -pub mod directives; -pub mod language; -pub mod migration; -pub mod patterns; -pub mod security; - -use anyhow::Result; -use std::path::Path; -use sustainabot_metrics::AnalysisResult; - -pub use analyzer::Analyzer; -pub use language::Language; - -/// Main entry point for analyzing a file -pub fn analyze_file(path: &Path) -> Result> { - let language = Language::detect(path)?; - let mut analyzer = Analyzer::new(language)?; - analyzer.analyze_file(path) -} - -/// Analyze source code directly -pub fn analyze_source(source: &str, language: Language) -> Result> { - let mut analyzer = Analyzer::new(language)?; - analyzer.analyze_source(source) -} diff --git a/bots/sustainabot/crates/sustainabot-analysis/src/migration.rs b/bots/sustainabot/crates/sustainabot-analysis/src/migration.rs deleted file mode 100644 index a1baa5ca..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/src/migration.rs +++ /dev/null @@ -1,373 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -//! Migration Health Tracking for SustainaBot -//! -//! Extends sustainabot's analysis with ReScript migration health monitoring. -//! Tracks migration health scores over time and alerts on regressions. -//! -//! Integration points: -//! - Reads panic-attack migration-snapshot JSON outputs -//! - Produces SARIF-compatible findings for migration regressions -//! - Feeds health deltas into the fleet dispatch pipeline - -use anyhow::Context; -use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; - -/// Migration health snapshot (parsed from panic-attack output) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MigrationHealthSnapshot { - /// Repository or target path - pub target: PathBuf, - /// Snapshot label - pub label: String, - /// ISO 8601 timestamp - pub timestamp: String, - /// Overall health score (0.0 - 1.0) - pub health_score: f64, - /// Count of deprecated API usages - pub deprecated_count: usize, - /// Count of modern API usages - pub modern_count: usize, - /// Config format: bsconfig, rescript_json, both, none - pub config_format: String, - /// Detected version bracket - pub version_bracket: String, -} - -/// A migration health regression finding -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MigrationRegression { - /// Repository affected - pub target: PathBuf, - /// Previous health score - pub previous_score: f64, - /// Current health score - pub current_score: f64, - /// Health score delta (negative = regression) - pub delta: f64, - /// What regressed - pub reason: RegressionReason, - /// SARIF-compatible rule ID - pub rule_id: String, - /// Human-readable description - pub description: String, -} - -/// Reason for a migration regression -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum RegressionReason { - /// Health score dropped - HealthScoreDrop, - /// Deprecated API count increased - DeprecatedCountIncrease, - /// Config format reverted (e.g., rescript.json → bsconfig.json) - ConfigFormatRevert, - /// Version bracket downgrade - VersionBracketDowngrade, -} - -impl std::fmt::Display for RegressionReason { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::HealthScoreDrop => write!(f, "Migration health score dropped"), - Self::DeprecatedCountIncrease => write!(f, "Deprecated API usage increased"), - Self::ConfigFormatRevert => write!(f, "Config format reverted"), - Self::VersionBracketDowngrade => write!(f, "Version bracket downgraded"), - } - } -} - -/// Migration health tracker -/// -/// Compares successive snapshots to detect regressions and track -/// migration progress over time. -pub struct MigrationHealthTracker; - -impl MigrationHealthTracker { - /// Compare two snapshots and detect regressions. - /// - /// Returns a list of regression findings. An empty list means - /// migration health is stable or improving. - pub fn detect_regressions( - previous: &MigrationHealthSnapshot, - current: &MigrationHealthSnapshot, - ) -> Vec { - let mut regressions = Vec::new(); - - // Health score regression (threshold: -0.05 = 5% drop) - let health_delta = current.health_score - previous.health_score; - if health_delta < -0.05 { - regressions.push(MigrationRegression { - target: current.target.clone(), - previous_score: previous.health_score, - current_score: current.health_score, - delta: health_delta, - reason: RegressionReason::HealthScoreDrop, - rule_id: "sustainabot/migration-health-regression".to_string(), - description: format!( - "Migration health score dropped from {:.2} to {:.2} ({:+.2})", - previous.health_score, current.health_score, health_delta - ), - }); - } - - // Deprecated count regression - if current.deprecated_count > previous.deprecated_count { - let increase = current.deprecated_count - previous.deprecated_count; - regressions.push(MigrationRegression { - target: current.target.clone(), - previous_score: previous.health_score, - current_score: current.health_score, - delta: -(increase as f64), - reason: RegressionReason::DeprecatedCountIncrease, - rule_id: "sustainabot/migration-deprecated-increase".to_string(), - description: format!( - "Deprecated API usage increased by {} (from {} to {})", - increase, previous.deprecated_count, current.deprecated_count - ), - }); - } - - // Config format regression (rescript_json → bsconfig is a downgrade) - if previous.config_format == "rescript_json" && current.config_format == "bsconfig" { - regressions.push(MigrationRegression { - target: current.target.clone(), - previous_score: previous.health_score, - current_score: current.health_score, - delta: -0.2, // Config revert is a significant regression - reason: RegressionReason::ConfigFormatRevert, - rule_id: "sustainabot/migration-config-revert".to_string(), - description: "Config format reverted from rescript.json to bsconfig.json" - .to_string(), - }); - } - - // Version bracket downgrade - let prev_rank = version_bracket_rank(&previous.version_bracket); - let curr_rank = version_bracket_rank(¤t.version_bracket); - if curr_rank < prev_rank { - regressions.push(MigrationRegression { - target: current.target.clone(), - previous_score: previous.health_score, - current_score: current.health_score, - delta: (curr_rank as f64 - prev_rank as f64) * 0.1, - reason: RegressionReason::VersionBracketDowngrade, - rule_id: "sustainabot/migration-version-downgrade".to_string(), - description: format!( - "Version bracket downgraded from {} to {}", - previous.version_bracket, current.version_bracket - ), - }); - } - - regressions - } - - /// Compute a migration velocity metric. - /// - /// Given a series of snapshots ordered by time, computes the average - /// health improvement per snapshot interval. - pub fn compute_velocity(snapshots: &[MigrationHealthSnapshot]) -> f64 { - if snapshots.len() < 2 { - return 0.0; - } - - let total_delta = snapshots - .last() - .expect("snapshots non-empty: len >= 2 checked above") - .health_score - - snapshots - .first() - .expect("snapshots non-empty: len >= 2 checked above") - .health_score; - total_delta / (snapshots.len() - 1) as f64 - } - - /// Load a migration health snapshot from a JSON file. - /// - /// Reads the panic-attack migration-snapshot output format. - pub fn load_snapshot(path: &Path) -> anyhow::Result { - let content = std::fs::read_to_string(path) - .with_context(|| format!("reading migration snapshot from {}", path.display()))?; - let raw: serde_json::Value = serde_json::from_str(&content) - .with_context(|| format!("parsing migration snapshot JSON from {}", path.display()))?; - - // Correctness-critical fields: a missing/malformed value here must NOT - // be silently defaulted. Defaulting `health_score` to 0.0 or - // `deprecated_api_count` to 0 would fabricate or mask regressions that - // feed the fleet dispatch pipeline, so propagate a hard error instead. - let health_score = raw["health_score"].as_f64().with_context(|| { - format!( - "migration snapshot {} is missing a numeric `health_score`", - path.display() - ) - })?; - let deprecated_count = raw["deprecated_api_count"].as_u64().with_context(|| { - format!( - "migration snapshot {} is missing an integer `deprecated_api_count`", - path.display() - ) - })? as usize; - let modern_count = raw["modern_api_count"].as_u64().with_context(|| { - format!( - "migration snapshot {} is missing an integer `modern_api_count`", - path.display() - ) - })? as usize; - let target = raw["target_path"].as_str().with_context(|| { - format!( - "migration snapshot {} is missing a string `target_path`", - path.display() - ) - })?; - let config_format = raw["config_format"].as_str().with_context(|| { - format!( - "migration snapshot {} is missing a string `config_format`", - path.display() - ) - })?; - let version_bracket = raw["version_bracket"].as_str().with_context(|| { - format!( - "migration snapshot {} is missing a string `version_bracket`", - path.display() - ) - })?; - - Ok(MigrationHealthSnapshot { - target: PathBuf::from(target), - // `label`/`timestamp` are descriptive only and do not affect - // regression detection, so a missing value falls back benignly. - label: raw["label"].as_str().unwrap_or("unknown").to_string(), - timestamp: raw["timestamp"].as_str().unwrap_or("unknown").to_string(), - health_score, - deprecated_count, - modern_count, - config_format: config_format.to_string(), - version_bracket: version_bracket.to_string(), - }) - } -} - -/// Rank version brackets for comparison (higher = more modern) -fn version_bracket_rank(bracket: &str) -> u8 { - match bracket { - "BuckleScript" => 1, - "V11" => 2, - "V12Alpha" => 3, - "V12Stable" => 4, - "V12Current" => 5, - "V13PreRelease" => 6, - _ => 0, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_snapshot( - health: f64, - deprecated: usize, - config: &str, - version: &str, - ) -> MigrationHealthSnapshot { - MigrationHealthSnapshot { - target: PathBuf::from("/tmp/test-repo"), - label: "test".to_string(), - timestamp: "2026-03-01T10:00:00Z".to_string(), - health_score: health, - deprecated_count: deprecated, - modern_count: 50, - config_format: config.to_string(), - version_bracket: version.to_string(), - } - } - - #[test] - fn test_no_regression_on_improvement() { - let prev = make_snapshot(0.5, 40, "bsconfig", "V11"); - let curr = make_snapshot(0.7, 20, "rescript_json", "V12Stable"); - let regressions = MigrationHealthTracker::detect_regressions(&prev, &curr); - assert!(regressions.is_empty()); - } - - #[test] - fn test_health_score_regression() { - let prev = make_snapshot(0.8, 10, "rescript_json", "V12Current"); - let curr = make_snapshot(0.6, 10, "rescript_json", "V12Current"); - let regressions = MigrationHealthTracker::detect_regressions(&prev, &curr); - assert!(regressions - .iter() - .any(|r| r.reason == RegressionReason::HealthScoreDrop)); - } - - #[test] - fn test_deprecated_count_regression() { - let prev = make_snapshot(0.7, 10, "rescript_json", "V12Stable"); - let curr = make_snapshot(0.7, 25, "rescript_json", "V12Stable"); - let regressions = MigrationHealthTracker::detect_regressions(&prev, &curr); - assert!(regressions - .iter() - .any(|r| r.reason == RegressionReason::DeprecatedCountIncrease)); - } - - #[test] - fn test_config_format_regression() { - let prev = make_snapshot(0.8, 5, "rescript_json", "V12Current"); - let curr = make_snapshot(0.8, 5, "bsconfig", "V12Current"); - let regressions = MigrationHealthTracker::detect_regressions(&prev, &curr); - assert!(regressions - .iter() - .any(|r| r.reason == RegressionReason::ConfigFormatRevert)); - } - - #[test] - fn test_version_bracket_regression() { - let prev = make_snapshot(0.8, 5, "rescript_json", "V12Stable"); - let curr = make_snapshot(0.8, 5, "rescript_json", "V11"); - let regressions = MigrationHealthTracker::detect_regressions(&prev, &curr); - assert!(regressions - .iter() - .any(|r| r.reason == RegressionReason::VersionBracketDowngrade)); - } - - #[test] - fn test_small_health_drop_not_flagged() { - // 3% drop should not trigger (threshold is 5%) - let prev = make_snapshot(0.8, 10, "rescript_json", "V12Current"); - let curr = make_snapshot(0.77, 10, "rescript_json", "V12Current"); - let regressions = MigrationHealthTracker::detect_regressions(&prev, &curr); - assert!(!regressions - .iter() - .any(|r| r.reason == RegressionReason::HealthScoreDrop)); - } - - #[test] - fn test_migration_velocity() { - let snapshots = vec![ - make_snapshot(0.3, 50, "bsconfig", "V11"), - make_snapshot(0.5, 30, "bsconfig", "V11"), - make_snapshot(0.7, 15, "rescript_json", "V12Stable"), - make_snapshot(0.85, 5, "rescript_json", "V12Current"), - ]; - let velocity = MigrationHealthTracker::compute_velocity(&snapshots); - // (0.85 - 0.3) / 3 = 0.183... - assert!(velocity > 0.15); - assert!(velocity < 0.25); - } - - #[test] - fn test_migration_velocity_single_snapshot() { - let snapshots = vec![make_snapshot(0.5, 20, "bsconfig", "V11")]; - let velocity = MigrationHealthTracker::compute_velocity(&snapshots); - assert_eq!(velocity, 0.0); - } - - #[test] - fn test_version_bracket_rank() { - assert!(version_bracket_rank("V12Current") > version_bracket_rank("V11")); - assert!(version_bracket_rank("V13PreRelease") > version_bracket_rank("V12Current")); - assert!(version_bracket_rank("BuckleScript") < version_bracket_rank("V11")); - } -} diff --git a/bots/sustainabot/crates/sustainabot-analysis/src/patterns.rs b/bots/sustainabot/crates/sustainabot-analysis/src/patterns.rs deleted file mode 100644 index 4490c778..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/src/patterns.rs +++ /dev/null @@ -1,327 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! Pattern detection for inefficient code - -/// A detected inefficiency pattern with metadata -#[derive(Debug, Clone)] -pub struct PatternMatch { - /// Machine-readable pattern name (e.g. "nested-loops") - pub name: String, - /// Human-readable description - pub description: String, - /// Concrete fix suggestion for SARIF output - pub suggestion: Option, - /// Estimated energy impact multiplier (1.0 = normal, >1 = worse) - pub impact_multiplier: f64, -} - -/// Detect problematic patterns in code -pub fn detect_patterns(_source: &str, node: &tree_sitter::Node) -> Vec { - let mut patterns = Vec::new(); - - // Check nested loops - let loop_depth = count_loop_depth(node); - if loop_depth >= 3 { - patterns.push(PatternMatch { - name: "nested-loops".to_string(), - description: format!("Deeply nested loops (depth {}): O(n^{}) complexity", loop_depth, loop_depth), - suggestion: Some("Consider algorithm optimization to reduce nested iterations. \ - Use hash maps for lookups, sort + binary search, or restructure as flat iteration." - .to_string()), - impact_multiplier: loop_depth as f64, - }); - } - - // Check for busy-wait loops (loop/while with no sleep/await/yield) - if has_busy_wait(_source, node) { - patterns.push(PatternMatch { - name: "busy-wait".to_string(), - description: "Loop without sleep, await, or yield burns CPU continuously".to_string(), - suggestion: Some("Replace busy-wait with async/await, tokio::time::sleep, \ - std::thread::sleep, or a channel recv." - .to_string()), - impact_multiplier: 5.0, - }); - } - - // Check for string concatenation in loops - if has_string_concat_in_loop(_source, node) { - patterns.push(PatternMatch { - name: "string-concat-in-loop".to_string(), - description: "String concatenation inside loop causes repeated allocation".to_string(), - suggestion: Some("Use String::with_capacity() and push_str(), or collect with \ - iterators instead of concatenating in a loop." - .to_string()), - impact_multiplier: 2.0, - }); - } - - // Check for clone in loops - if has_clone_in_loop(_source, node) { - patterns.push(PatternMatch { - name: "clone-in-loop".to_string(), - description: ".clone() inside loop body causes repeated deep copies".to_string(), - suggestion: Some("Consider borrowing instead of cloning, or move the clone \ - outside the loop if the value doesn't change per iteration." - .to_string()), - impact_multiplier: 1.5, - }); - } - - // Check for unbuffered I/O - if has_unbuffered_io(_source, node) { - patterns.push(PatternMatch { - name: "unbuffered-io".to_string(), - description: "Read/write without BufReader/BufWriter causes excessive syscalls".to_string(), - suggestion: Some("Wrap the reader/writer with BufReader/BufWriter for \ - buffered I/O to reduce system call overhead." - .to_string()), - impact_multiplier: 3.0, - }); - } - - // Check for large allocations - if has_large_allocation(_source, node) { - patterns.push(PatternMatch { - name: "large-allocation".to_string(), - description: "Large heap allocation (>1MB) detected".to_string(), - suggestion: Some("Review memory allocation size. Consider streaming, \ - chunked processing, or memory-mapped files for large data." - .to_string()), - impact_multiplier: 2.0, - }); - } - - // Check for redundant allocations (.to_string()/.to_owned() where borrow suffices) - if has_redundant_to_string(_source, node) { - patterns.push(PatternMatch { - name: "redundant-allocation".to_string(), - description: "Unnecessary .to_string()/.to_owned() where a borrow would suffice".to_string(), - suggestion: Some("Accept &str instead of String where possible to avoid \ - unnecessary heap allocation." - .to_string()), - impact_multiplier: 1.2, - }); - } - - patterns -} - -fn count_loop_depth(node: &tree_sitter::Node) -> usize { - let is_loop = matches!( - node.kind(), - "for_expression" - | "while_expression" - | "loop_expression" - | "for_statement" - | "while_statement" - ); - - let mut max_child_depth = 0; - let mut cursor = node.walk(); - - if cursor.goto_first_child() { - loop { - let child_depth = count_loop_depth(&cursor.node()); - max_child_depth = max_child_depth.max(child_depth); - - if !cursor.goto_next_sibling() { - break; - } - } - } - - if is_loop { - 1 + max_child_depth - } else { - max_child_depth - } -} - -/// Check if a loop body contains no sleep/await/yield/recv — indicating a busy wait -fn has_busy_wait(source: &str, node: &tree_sitter::Node) -> bool { - let is_loop = matches!( - node.kind(), - "loop_expression" | "while_expression" - ); - - if !is_loop { - // Recurse into children to find loops - let mut cursor = node.walk(); - if cursor.goto_first_child() { - loop { - if has_busy_wait(source, &cursor.node()) { - return true; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - return false; - } - - // Found a loop — check if its body text mentions sleep/await/yield/recv - let text = match node.utf8_text(source.as_bytes()) { - Ok(t) => t, - Err(_) => return false, - }; - - // If the loop body contains any blocking/yielding call, it's not a busy wait - let has_yield = text.contains("sleep") - || text.contains("await") - || text.contains("yield") - || text.contains("recv") - || text.contains("park") - || text.contains("wait") - || text.contains(".await"); - - !has_yield -} - -/// Check if any loop body contains string concatenation -fn has_string_concat_in_loop(source: &str, node: &tree_sitter::Node) -> bool { - find_in_loop_body(node, |child, _src| { - // Look for binary_expression with "+" operator on strings - // or format! macro calls - child.kind() == "binary_expression" || child.kind() == "macro_invocation" - }, source) -} - -/// Check if any loop body contains .clone() calls -fn has_clone_in_loop(source: &str, node: &tree_sitter::Node) -> bool { - find_in_loop_body(node, |child, src| { - if child.kind() == "call_expression" || child.kind() == "method_call_expression" { - if let Ok(text) = child.utf8_text(src.as_bytes()) { - return text.contains(".clone()"); - } - } - false - }, source) -} - -/// Check for File::open/create without BufReader/BufWriter nearby -fn has_unbuffered_io(source: &str, node: &tree_sitter::Node) -> bool { - let text = match node.utf8_text(source.as_bytes()) { - Ok(t) => t, - Err(_) => return false, - }; - - let has_file_io = text.contains("File::open") || text.contains("File::create"); - let has_buffering = text.contains("BufReader") || text.contains("BufWriter"); - - has_file_io && !has_buffering -} - -/// Check for large allocations: Vec::with_capacity(>1_000_000) or vec![0; large] -fn has_large_allocation(source: &str, node: &tree_sitter::Node) -> bool { - let text = match node.utf8_text(source.as_bytes()) { - Ok(t) => t, - Err(_) => return false, - }; - - // Simple heuristic: look for large numeric literals near allocation calls - if text.contains("with_capacity") || text.contains("vec![") { - // Check for numbers > 1_000_000 - for word in text.split(|c: char| !c.is_ascii_digit() && c != '_') { - let clean: String = word.chars().filter(|c| c.is_ascii_digit()).collect(); - if let Ok(n) = clean.parse::() { - if n > 1_000_000 { - return true; - } - } - } - } - false -} - -/// Check for .to_string() or .to_owned() calls that may be redundant -fn has_redundant_to_string(source: &str, node: &tree_sitter::Node) -> bool { - let text = match node.utf8_text(source.as_bytes()) { - Ok(t) => t, - Err(_) => return false, - }; - - // Count occurrences as a heuristic — many .to_string() in one function is suspicious - let to_string_count = text.matches(".to_string()").count(); - let to_owned_count = text.matches(".to_owned()").count(); - - (to_string_count + to_owned_count) >= 5 -} - -/// Find a pattern match inside loop bodies -fn find_in_loop_body( - node: &tree_sitter::Node, - predicate: impl Fn(&tree_sitter::Node, &str) -> bool + Copy, - source: &str, -) -> bool { - let is_loop = matches!( - node.kind(), - "for_expression" - | "while_expression" - | "loop_expression" - | "for_statement" - | "while_statement" - ); - - if is_loop { - // Search inside loop body for the pattern - return subtree_matches(node, predicate, source); - } - - // Recurse to find loops - let mut cursor = node.walk(); - if cursor.goto_first_child() { - loop { - if find_in_loop_body(&cursor.node(), predicate, source) { - return true; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - false -} - -/// Check if any node in the subtree matches the predicate -fn subtree_matches( - node: &tree_sitter::Node, - predicate: impl Fn(&tree_sitter::Node, &str) -> bool + Copy, - source: &str, -) -> bool { - if predicate(node, source) { - return true; - } - let mut cursor = node.walk(); - if cursor.goto_first_child() { - loop { - if subtree_matches(&cursor.node(), predicate, source) { - return true; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - false -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pattern_match_fields() { - let pm = PatternMatch { - name: "test".to_string(), - description: "Test pattern".to_string(), - suggestion: Some("Fix it".to_string()), - impact_multiplier: 1.5, - }; - assert_eq!(pm.name, "test"); - assert!(pm.suggestion.is_some()); - assert!(pm.impact_multiplier > 1.0); - } -} diff --git a/bots/sustainabot/crates/sustainabot-analysis/src/security.rs b/bots/sustainabot/crates/sustainabot-analysis/src/security.rs deleted file mode 100644 index ae8bfda8..00000000 --- a/bots/sustainabot/crates/sustainabot-analysis/src/security.rs +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! Security-sustainability correlation engine. -//! -//! 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 std::path::Path; - use sustainabot_metrics::*; - - /// 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: "sustainabot/security-sustainability".to_string(), - suggestion: Some(format!( - "Address {:?} weak point to prevent {} energy waste", - wp.category, impact_desc - )), - end_location: None, - confidence: Confidence::Estimated, - }; - - 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 - }; - - Ok(SecurityCorrelation { - eco_results: eco_results.to_vec(), - security_findings, - composite_score, - }) - } -} - -#[cfg(feature = "panic-attack")] -pub use inner::*; - -/// 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 - } -} - -#[cfg(not(feature = "panic-attack"))] -pub use stub::*; diff --git a/bots/sustainabot/crates/sustainabot-cli/Cargo.toml b/bots/sustainabot/crates/sustainabot-cli/Cargo.toml deleted file mode 100644 index 3a0dcc03..00000000 --- a/bots/sustainabot/crates/sustainabot-cli/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -[package] -name = "sustainabot-cli" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true - -[[bin]] -name = "sustainabot" -path = "src/main.rs" - -[features] -default = [] -security = ["sustainabot-analysis/panic-attack"] - -[dependencies] -sustainabot-analysis = { path = "../sustainabot-analysis" } -sustainabot-metrics = { path = "../sustainabot-metrics" } -sustainabot-sarif = { path = "../sustainabot-sarif" } -sustainabot-fleet = { path = "../sustainabot-fleet" } -sustainabot-eclexia = { path = "../sustainabot-eclexia" } -clap.workspace = true -anyhow.workspace = true -serde_json.workspace = true -tracing.workspace = true -tracing-subscriber.workspace = true -walkdir = "2" diff --git a/bots/sustainabot/crates/sustainabot-cli/src/main.rs b/bots/sustainabot/crates/sustainabot-cli/src/main.rs deleted file mode 100644 index 73c49980..00000000 --- a/bots/sustainabot/crates/sustainabot-cli/src/main.rs +++ /dev/null @@ -1,498 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! # SustainaBot CLI -//! -//! Ecological and economic code analysis tool. -//! Built with Eclexia principles - proving resource-aware design works. - -#![forbid(unsafe_code)] -use anyhow::Result; -use clap::{Parser, Subcommand}; -use std::fs; -use std::path::PathBuf; -use sustainabot_analysis::analyze_file; -use tracing::info; -use walkdir::WalkDir; - -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[derive(Parser)] -#[command(name = "sustainabot")] -#[command(about = "Ecological & Economic Code Analysis", long_about = None)] -#[command(version)] -struct Cli { - #[command(subcommand)] - command: Commands, - - /// Enable verbose logging - #[arg(short, long, global = true)] - verbose: bool, -} - -#[derive(Subcommand)] -enum Commands { - /// Analyze a single file - Analyze { - /// File to analyze - file: PathBuf, - - /// Output format (text, json, sarif) - #[arg(short, long, default_value = "text")] - format: String, - - /// Write output to file instead of stdout - #[arg(short, long)] - output: Option, - }, - - /// Analyze a directory recursively - Check { - /// Directory to check - path: PathBuf, - - /// Minimum eco score threshold (0-100) - #[arg(long, default_value = "50")] - eco_threshold: f64, - - /// Output format (text, json, sarif) - #[arg(short, long, default_value = "text")] - format: String, - - /// Write output to file instead of stdout - #[arg(short, long)] - output: Option, - - /// Include security-sustainability correlation (requires panic-attack feature) - #[arg(long)] - security: bool, - - /// Directory containing Eclexia policy files (.ecl) - #[arg(long)] - policy_dir: Option, - }, - - /// Generate a full report for a directory (alias for check with defaults) - Report { - /// Directory to analyze - path: PathBuf, - - /// Output format (text, json, sarif) - #[arg(short, long, default_value = "sarif")] - format: String, - - /// Write output to file instead of stdout - #[arg(short, long)] - output: Option, - - /// Minimum eco score threshold (0-100) - #[arg(long, default_value = "50")] - eco_threshold: f64, - - /// Include security-sustainability correlation (requires panic-attack feature) - #[arg(long)] - security: bool, - - /// Directory containing Eclexia policy files (.ecl) - #[arg(long)] - policy_dir: Option, - }, - - /// Run as a gitbot-fleet member - Fleet { - /// Repository path to analyze - path: PathBuf, - - /// Path to shared context JSON file - #[arg(short, long)] - context: Option, - }, - - /// Show analysis of sustainabot itself (dogfooding!) - SelfAnalyze, -} - -fn main() -> Result<()> { - let cli = Cli::parse(); - - // Set up logging - let log_level = if cli.verbose { "debug" } else { "info" }; - tracing_subscriber::fmt() - .with_env_filter(log_level) - .init(); - - match cli.command { - Commands::Analyze { - file, - format, - output, - } => { - info!("Analyzing file: {}", file.display()); - let results = analyze_file(&file)?; - emit_output(&results, &format, output.as_deref())?; - } - - Commands::Check { - path, - eco_threshold, - format, - output, - security, - policy_dir, - } - | Commands::Report { - path, - format, - output, - eco_threshold, - security, - policy_dir, - } => { - info!("Checking directory: {}", path.display()); - - let mut all_results = collect_directory_results(&path)?; - - // Security-sustainability correlation - if security { - run_security_correlation(&path, &mut all_results); - } - - // Policy evaluation - if let Some(ref pdir) = policy_dir { - run_policy_evaluation(pdir, &mut all_results); - } - - // Emit formatted output - match format.as_str() { - "sarif" | "json" => { - emit_output(&all_results, &format, output.as_deref())?; - } - "text" => { - println!( - "Checking directory: {} (eco threshold: {})\n", - path.display(), - eco_threshold - ); - - let mut files_below_threshold = 0u32; - for result in &all_results { - if result.health.eco_score.0 < eco_threshold { - files_below_threshold += 1; - println!( - " BELOW THRESHOLD: {} :: {} (eco: {:.1}, threshold: {})", - result.location.file, - result.location.name.as_deref().unwrap_or(""), - result.health.eco_score.0, - eco_threshold - ); - } - } - - print_summary(&all_results, eco_threshold, files_below_threshold); - - if let Some(ref out_path) = output { - // Also write text summary to file - let text = format_results_text(&all_results); - fs::write(out_path, text)?; - println!("\nOutput written to: {}", out_path.display()); - } - - if files_below_threshold > 0 { - std::process::exit(1); - } - } - _ => { - eprintln!("Unsupported format: {}", format); - } - } - } - - Commands::Fleet { path, context } => { - info!("Running fleet analysis: {}", path.display()); - sustainabot_fleet::run_fleet_analysis( - &path, - context.as_deref(), - )?; - } - - Commands::SelfAnalyze => { - println!("SustainaBot Self-Analysis (Dogfooding!)"); - println!("==========================================\n"); - println!("Analyzing sustainabot's own resource usage...\n"); - - let analyzer_src = PathBuf::from("crates/sustainabot-analysis/src/analyzer.rs"); - if analyzer_src.exists() { - let results = analyze_file(&analyzer_src)?; - print_results_text(&results); - - println!("\nMeta-Analysis:"); - println!("This analyzer used minimal resources to analyze itself."); - println!("Eclexia-inspired design: explicit resource tracking from day 1."); - } else { - println!("Run from sustainabot repository root."); - } - } - } - - Ok(()) -} - -/// Collect analysis results from all supported files in a directory -fn collect_directory_results( - path: &std::path::Path, -) -> Result> { - let mut all_results = Vec::new(); - - for entry in WalkDir::new(path) - .follow_links(false) - .into_iter() - .filter_entry(|e| { - let name = e.file_name().to_str().unwrap_or(""); - !matches!( - name, - "target" | "node_modules" | ".git" | "dist" | "build" | ".cache" - ) - }) - .filter_map(|e| e.ok()) - { - let entry_path = entry.path(); - if !entry_path.is_file() { - continue; - } - - let ext = entry_path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or(""); - if !matches!(ext, "rs" | "js" | "py") { - continue; - } - - match analyze_file(entry_path) { - Ok(results) => { - all_results.extend(results); - } - Err(e) => { - info!("Skipping {}: {}", entry_path.display(), e); - } - } - } - - Ok(all_results) -} - -/// Emit analysis results in the requested format -fn emit_output( - results: &[sustainabot_metrics::AnalysisResult], - format: &str, - output: Option<&std::path::Path>, -) -> Result<()> { - let text = match format { - "sarif" => sustainabot_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(()); - } - }; - - match output { - Some(path) => { - fs::write(path, &text)?; - eprintln!("Output written to: {}", path.display()); - } - None => { - println!("{}", text); - } - } - - Ok(()) -} - -fn print_summary( - all_results: &[sustainabot_metrics::AnalysisResult], - eco_threshold: f64, - files_below_threshold: u32, -) { - let total_files = all_results - .iter() - .map(|r| r.location.file.as_str()) - .collect::>() - .len(); - - println!("\n--- Summary ---"); - println!("Files analyzed: {}", total_files); - println!("Functions found: {}", all_results.len()); - println!("Below threshold: {}", files_below_threshold); - - if !all_results.is_empty() { - let avg_eco: f64 = - all_results.iter().map(|r| r.health.eco_score.0).sum::() / all_results.len() as f64; - let avg_overall: f64 = - all_results.iter().map(|r| r.health.overall).sum::() / all_results.len() as f64; - let total_energy: f64 = all_results.iter().map(|r| r.resources.energy.0).sum(); - let total_carbon: f64 = all_results.iter().map(|r| r.resources.carbon.0).sum(); - - println!("Avg eco score: {:.1}/100", avg_eco); - println!("Avg overall health: {:.1}/100", avg_overall); - println!("Total est. energy: {:.2} J", total_energy); - println!("Total est. carbon: {:.4} gCO2e", total_carbon); - } - - if files_below_threshold > 0 { - println!( - "\nResult: FAIL ({} functions below eco threshold {})", - files_below_threshold, eco_threshold - ); - } else { - println!( - "\nResult: PASS (all functions meet eco threshold {})", - eco_threshold - ); - } -} - -fn print_results_text(results: &[sustainabot_metrics::AnalysisResult]) { - for result in results { - println!( - "\nFunction: {}", - result.location.name.as_deref().unwrap_or("") - ); - println!( - " Location: {}:{}:{}", - result.location.file, result.location.line, result.location.column - ); - println!("\n Resources:"); - println!(" Energy: {:.2} J", result.resources.energy.0); - println!(" Time: {:.2} ms", result.resources.duration.0); - println!(" Carbon: {:.4} gCO2e", result.resources.carbon.0); - println!(" Memory: {} bytes", result.resources.memory.0); - - println!("\n Health Index:"); - println!(" Eco: {:.1}/100", result.health.eco_score.0); - println!(" Econ: {:.1}/100", result.health.econ_score.0); - println!(" Quality: {:.1}/100", result.health.quality_score); - println!(" Overall: {:.1}/100", result.health.overall); - - if !result.recommendations.is_empty() { - println!("\n Recommendations:"); - for rec in &result.recommendations { - println!(" - {}", rec); - } - } - } - - println!("\nAnalysis complete"); -} - -/// Run Eclexia policy evaluation -fn run_policy_evaluation( - policy_dir: &std::path::Path, - results: &mut Vec, -) { - match sustainabot_eclexia::evaluate_policies(policy_dir, results) { - Ok(decisions) => { - let warns = decisions - .iter() - .filter(|d| d.outcome != sustainabot_eclexia::PolicyOutcome::Pass) - .count(); - eprintln!( - "Policy evaluation: {} policies, {} warnings/failures", - decisions.len(), - warns - ); - for d in &decisions { - if d.outcome != sustainabot_eclexia::PolicyOutcome::Pass { - eprintln!(" {:?}: {} - {}", d.outcome, d.policy_name, d.message); - } - } - // Convert policy decisions to analysis results for SARIF output - let policy_results = sustainabot_eclexia::decisions_to_results(&decisions); - results.extend(policy_results); - } - Err(e) => { - eprintln!("Policy evaluation failed: {}", e); - } - } -} - -/// 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 = sustainabot_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 sustainabot_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" - ); - } -} - -fn format_results_text(results: &[sustainabot_metrics::AnalysisResult]) -> String { - let mut out = String::new(); - - for result in results { - out.push_str(&format!( - "\nFunction: {}\n", - result.location.name.as_deref().unwrap_or("") - )); - out.push_str(&format!( - " Location: {}:{}:{}\n", - result.location.file, result.location.line, result.location.column - )); - out.push_str(&format!(" Energy: {:.2} J\n", result.resources.energy.0)); - out.push_str(&format!( - " Carbon: {:.4} gCO2e\n", - result.resources.carbon.0 - )); - out.push_str(&format!( - " Eco: {:.1}/100 Overall: {:.1}/100\n", - result.health.eco_score.0, result.health.overall - )); - } - - out -} diff --git a/bots/sustainabot/crates/sustainabot-eclexia/Cargo.toml b/bots/sustainabot/crates/sustainabot-eclexia/Cargo.toml deleted file mode 100644 index ecdbe7ba..00000000 --- a/bots/sustainabot/crates/sustainabot-eclexia/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -[package] -name = "sustainabot-eclexia" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -description = "Eclexia policy engine integration for sustainabot" - -[features] -default = [] -eclexia-native = [ - "dep:eclexia-lexer", - "dep:eclexia-parser", - "dep:eclexia-interp", -] - -[dependencies] -sustainabot-metrics = { path = "../sustainabot-metrics" } -anyhow.workspace = true -thiserror.workspace = true -serde.workspace = true -serde_json.workspace = true -tracing.workspace = true - -# Optional native eclexia integration -eclexia-lexer = { path = "../../../eclexia/compiler/eclexia-lexer", optional = true } -eclexia-parser = { path = "../../../eclexia/compiler/eclexia-parser", optional = true } -eclexia-interp = { path = "../../../eclexia/compiler/eclexia-interp", optional = true } diff --git a/bots/sustainabot/crates/sustainabot-eclexia/src/lib.rs b/bots/sustainabot/crates/sustainabot-eclexia/src/lib.rs deleted file mode 100644 index 87e81298..00000000 --- a/bots/sustainabot/crates/sustainabot-eclexia/src/lib.rs +++ /dev/null @@ -1,529 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! # SustainaBot-Eclexia Integration -//! -//! Policy engine integration with two backends: -//! - **Default**: shells out to `eclexia` binary (works without eclexia repo on disk) -//! - **Native** (`eclexia-native` feature): direct library integration via eclexia-interp -//! -//! Both backends implement the same `PolicyEngine` trait. - -#![forbid(unsafe_code)] -use anyhow::{Context, Result}; -use std::path::Path; -use sustainabot_metrics::{AnalysisResult, ResourceProfile}; - -/// Policy evaluation outcome -#[derive(Debug, Clone)] -pub struct PolicyDecision { - /// The verdict - pub outcome: PolicyOutcome, - /// Human-readable explanation - pub message: String, - /// Suggestion for fixing a policy violation - pub suggestion: Option, - /// Resource cost of evaluating this policy (dogfooding!) - pub evaluation_cost: Option, - /// Name of the policy that produced this decision - pub policy_name: String, -} - -/// Policy verdict -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PolicyOutcome { - /// Code meets all policy criteria - Pass, - /// Code has issues worth noting but not blocking - Warn, - /// Code violates a hard policy requirement - Fail, -} - -/// Evaluate policies against analysis results. -/// -/// Uses the binary backend by default, or native eclexia-interp -/// when the `eclexia-native` feature is enabled. -pub fn evaluate_policies( - policy_dir: &Path, - results: &[AnalysisResult], -) -> Result> { - let mut decisions = Vec::new(); - - // Find all .ecl files in the policy directory - if !policy_dir.exists() { - return Ok(decisions); - } - - let entries = std::fs::read_dir(policy_dir) - .with_context(|| format!("Failed to read policy dir: {}", policy_dir.display()))?; - - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().and_then(|e| e.to_str()) == Some("ecl") { - let policy_name = path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("unknown") - .to_string(); - - 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, - }); - } - } - } - } - - Ok(decisions) -} - -/// Evaluate a single policy file against analysis results. -fn evaluate_single_policy( - policy_path: &Path, - results: &[AnalysisResult], -) -> Result { - let policy_name = policy_path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("unknown") - .to_string(); - - #[cfg(feature = "eclexia-native")] - { - evaluate_native(policy_path, results, &policy_name) - } - - #[cfg(not(feature = "eclexia-native"))] - { - evaluate_binary(policy_path, results, &policy_name) - } -} - -/// Binary backend: shells out to `eclexia` CLI -#[cfg(not(feature = "eclexia-native"))] -fn evaluate_binary( - policy_path: &Path, - results: &[AnalysisResult], - policy_name: &str, -) -> Result { - use std::process::Command; - - // Serialize results summary as JSON input - let input = serde_json::json!({ - "function_count": results.len(), - "total_energy": results.iter().map(|r| r.resources.energy.0).sum::(), - "total_carbon": results.iter().map(|r| r.resources.carbon.0).sum::(), - "avg_eco_score": if results.is_empty() { 100.0 } else { - results.iter().map(|r| r.health.eco_score.0).sum::() / results.len() as f64 - }, - "below_threshold_count": results.iter() - .filter(|r| r.health.eco_score.0 < 50.0) - .count(), - }); - - // Try to find eclexia binary - let eclexia = which_eclexia(); - - match eclexia { - Some(binary) => { - let output = Command::new(&binary) - .arg("run") - .arg(policy_path) - .arg("--input") - .arg(input.to_string()) - .output() - .with_context(|| format!("Failed to execute eclexia at {}", binary))?; - - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let result: bool = serde_json::from_str(stdout.trim()) - .unwrap_or(false); - - Ok(PolicyDecision { - outcome: if result { - PolicyOutcome::Warn - } else { - PolicyOutcome::Pass - }, - message: format!( - "Policy '{}' evaluated via eclexia binary", - policy_name - ), - suggestion: None, - evaluation_cost: Some(placeholder_cost()), - policy_name: policy_name.to_string(), - }) - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - anyhow::bail!("Eclexia policy failed: {}", stderr); - } - } - None => { - // No eclexia binary found — use built-in policy evaluation - evaluate_builtin(results, policy_name) - } - } -} - -/// Native backend: direct eclexia-interp library integration -#[cfg(feature = "eclexia-native")] -fn evaluate_native( - policy_path: &Path, - results: &[AnalysisResult], - policy_name: &str, -) -> Result { - let source = std::fs::read_to_string(policy_path) - .with_context(|| format!("Failed to read policy: {}", policy_path.display()))?; - - let (ast, errors) = eclexia_parser::parse(&source); - - if !errors.is_empty() { - let error_msgs: Vec = errors.iter().map(|e| format!("{:?}", e)).collect(); - anyhow::bail!( - "Policy parse errors in {}: {}", - policy_name, - error_msgs.join("; ") - ); - } - - // Run the policy through the interpreter - let mut interp = eclexia_interp::Interpreter::new(); - // Set a tight budget for policy evaluation itself (dogfooding!) - interp.set_energy_budget(1.0); // 1 Joule max for policy eval - interp.set_carbon_budget(0.001); // 0.001g CO2e max - - match eclexia_interp::run(&ast) { - Ok(value) => { - let should_warn = value.is_truthy(); - - Ok(PolicyDecision { - outcome: if should_warn { - PolicyOutcome::Warn - } else { - PolicyOutcome::Pass - }, - message: format!( - "Policy '{}' evaluated natively: result={:?}", - policy_name, value - ), - suggestion: None, - evaluation_cost: Some(placeholder_cost()), - policy_name: policy_name.to_string(), - }) - } - Err(e) => { - // Runtime error — treat as warning - Ok(PolicyDecision { - outcome: PolicyOutcome::Warn, - message: format!( - "Policy '{}' runtime error: {:?}", - policy_name, e - ), - suggestion: Some("Check policy syntax and logic".to_string()), - evaluation_cost: None, - policy_name: policy_name.to_string(), - }) - } - } -} - -/// Built-in policy evaluation when eclexia binary is not available. -/// -/// Implements the core policies in Rust as a fallback. -fn evaluate_builtin( - results: &[AnalysisResult], - policy_name: &str, -) -> Result { - let total_energy: f64 = results.iter().map(|r| r.resources.energy.0).sum(); - let total_carbon: f64 = results.iter().map(|r| r.resources.carbon.0).sum(); - let avg_eco = if results.is_empty() { - 100.0 - } else { - results.iter().map(|r| r.health.eco_score.0).sum::() / results.len() as f64 - }; - - let (outcome, message, suggestion) = match policy_name { - "energy_threshold" => { - if total_energy > 1000.0 { - ( - PolicyOutcome::Fail, - format!("Total energy {:.2}J exceeds 1000J budget", total_energy), - Some("Optimize hot functions to reduce energy consumption".to_string()), - ) - } else if total_energy > 500.0 { - ( - PolicyOutcome::Warn, - format!("Total energy {:.2}J approaching 1000J budget", total_energy), - Some("Consider optimizing highest-energy functions".to_string()), - ) - } else { - ( - PolicyOutcome::Pass, - format!("Total energy {:.2}J within budget", total_energy), - None, - ) - } - } - "carbon_budget" => { - if total_carbon > 1.0 { - ( - PolicyOutcome::Fail, - format!("Carbon footprint {:.4}gCO2e exceeds 1g budget", total_carbon), - Some("Reduce computation intensity to lower carbon emissions".to_string()), - ) - } else { - ( - PolicyOutcome::Pass, - format!("Carbon footprint {:.4}gCO2e within budget", total_carbon), - None, - ) - } - } - "memory_efficiency" => { - let large_allocs: Vec<_> = results - .iter() - .filter(|r| r.resources.memory.0 > 1_048_576) // >1MB - .collect(); - if !large_allocs.is_empty() { - ( - PolicyOutcome::Warn, - format!("{} functions have >1MB allocations", large_allocs.len()), - Some("Review large allocations; consider streaming or chunked processing".to_string()), - ) - } else { - (PolicyOutcome::Pass, "All allocations within bounds".to_string(), None) - } - } - _ => { - // Unknown policy — apply generic eco threshold check - if avg_eco < 50.0 { - ( - PolicyOutcome::Warn, - format!("Average eco score {:.1} below 50.0 threshold", avg_eco), - Some("Improve code efficiency in low-scoring functions".to_string()), - ) - } else { - ( - PolicyOutcome::Pass, - format!("Average eco score {:.1} meets threshold", avg_eco), - None, - ) - } - } - }; - - Ok(PolicyDecision { - outcome, - message, - suggestion, - evaluation_cost: Some(placeholder_cost()), - policy_name: format!("{} (builtin)", policy_name), - }) -} - -/// Try to find the eclexia binary -fn which_eclexia() -> Option { - // Check common locations - let candidates = [ - "eclexia", - "~/.asdf/installs/rust/nightly/bin/eclexia", - "../eclexia/target/release/eclexia", - ]; - - for candidate in &candidates { - let expanded = shellexpand(candidate); - if std::path::Path::new(&expanded).exists() { - return Some(expanded); - } - } - - // Try PATH - if std::process::Command::new("which") - .arg("eclexia") - .output() - .map(|o| o.status.success()) - .unwrap_or(false) - { - return Some("eclexia".to_string()); - } - - None -} - -fn shellexpand(path: &str) -> String { - if path.starts_with('~') { - if let Ok(home) = std::env::var("HOME") { - return path.replacen('~', &home, 1); - } - } - path.to_string() -} - -fn placeholder_cost() -> ResourceProfile { - ResourceProfile { - energy: sustainabot_metrics::Energy::joules(0.05), - duration: sustainabot_metrics::Duration::milliseconds(1.0), - carbon: sustainabot_metrics::Carbon::grams_co2e(0.000007), - memory: sustainabot_metrics::Memory::kilobytes(50), - } -} - -/// Example policy in Eclexia (to be written to policies/ directory) -pub const EXAMPLE_POLICY: &str = r#" -// SPDX-License-Identifier: MPL-2.0 -// Example SustainaBot policy in Eclexia - -// This policy runs IN Eclexia, analyzing code's resource usage. -// Meta-level: The analyzer itself has provable resource bounds! - -def should_warn_high_energy(energy_joules: Float) -> Bool { - energy_joules > 100.0 -} - -def should_warn_high_carbon(carbon_grams: Float) -> Bool { - carbon_grams > 10.0 -} - -def evaluate_policy(energy: Float, carbon: Float) -> Bool - @requires: energy < 1J, carbon < 0.001gCO2e // Policy itself is cheap! -{ - should_warn_high_energy(energy) || should_warn_high_carbon(carbon) -} -"#; - -/// Convert policy decisions to analysis results for SARIF output -pub fn decisions_to_results(decisions: &[PolicyDecision]) -> Vec { - decisions - .iter() - .map(|d| { - let eco_score = match d.outcome { - PolicyOutcome::Pass => 90.0, - PolicyOutcome::Warn => 50.0, - PolicyOutcome::Fail => 20.0, - }; - - let cost = d.evaluation_cost.clone().unwrap_or_else(|| ResourceProfile { - energy: sustainabot_metrics::Energy::ZERO, - duration: sustainabot_metrics::Duration::ZERO, - carbon: sustainabot_metrics::Carbon::ZERO, - memory: sustainabot_metrics::Memory::ZERO, - }); - - AnalysisResult { - location: sustainabot_metrics::CodeLocation { - file: "policies/".to_string(), - line: 0, - column: 0, - end_line: None, - end_column: None, - name: Some(d.policy_name.clone()), - }, - resources: cost, - health: sustainabot_metrics::HealthIndex::compute( - sustainabot_metrics::EcoScore::new(eco_score), - sustainabot_metrics::EconScore::new(80.0), - 70.0, - ), - recommendations: { - let mut recs = vec![d.message.clone()]; - if let Some(ref s) = d.suggestion { - recs.push(s.clone()); - } - recs - }, - rule_id: format!("sustainabot/policy-{}", d.policy_name), - suggestion: d.suggestion.clone(), - end_location: None, - confidence: sustainabot_metrics::Confidence::Estimated, - } - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_example_policy_syntax() { - assert!(EXAMPLE_POLICY.contains("def evaluate_policy")); - assert!(EXAMPLE_POLICY.contains("@requires")); - } - - #[test] - fn test_builtin_energy_policy_pass() { - let results = vec![sample_result(10.0)]; - let decision = evaluate_builtin(&results, "energy_threshold").unwrap(); - assert_eq!(decision.outcome, PolicyOutcome::Pass); - } - - #[test] - fn test_builtin_energy_policy_warn() { - // Create many results to exceed 500J total - let results: Vec = (0..60).map(|_| sample_result(10.0)).collect(); - let decision = evaluate_builtin(&results, "energy_threshold").unwrap(); - assert_eq!(decision.outcome, PolicyOutcome::Warn); - } - - #[test] - fn test_builtin_energy_policy_fail() { - // Exceed 1000J - let results: Vec = (0..200).map(|_| sample_result(10.0)).collect(); - let decision = evaluate_builtin(&results, "energy_threshold").unwrap(); - assert_eq!(decision.outcome, PolicyOutcome::Fail); - } - - #[test] - fn test_decisions_to_results() { - let decisions = vec![PolicyDecision { - outcome: PolicyOutcome::Warn, - message: "Test warning".to_string(), - suggestion: Some("Fix it".to_string()), - evaluation_cost: None, - policy_name: "test".to_string(), - }]; - - let results = decisions_to_results(&decisions); - assert_eq!(results.len(), 1); - assert_eq!(results[0].rule_id, "sustainabot/policy-test"); - assert!(results[0].suggestion.is_some()); - } - - fn sample_result(energy_j: f64) -> AnalysisResult { - AnalysisResult { - location: sustainabot_metrics::CodeLocation { - file: "test.rs".to_string(), - line: 1, - column: 1, - end_line: None, - end_column: None, - name: Some("test_fn".to_string()), - }, - resources: ResourceProfile { - energy: sustainabot_metrics::Energy::joules(energy_j), - duration: sustainabot_metrics::Duration::milliseconds(5.0), - carbon: sustainabot_metrics::Carbon::grams_co2e(0.001), - memory: sustainabot_metrics::Memory::kilobytes(100), - }, - health: sustainabot_metrics::HealthIndex::compute( - sustainabot_metrics::EcoScore::new(80.0), - sustainabot_metrics::EconScore::new(70.0), - 75.0, - ), - recommendations: vec![], - rule_id: "sustainabot/general".to_string(), - suggestion: None, - end_location: None, - confidence: sustainabot_metrics::Confidence::Estimated, - } - } -} diff --git a/bots/sustainabot/crates/sustainabot-fleet/Cargo.toml b/bots/sustainabot/crates/sustainabot-fleet/Cargo.toml deleted file mode 100644 index faf5aa68..00000000 --- a/bots/sustainabot/crates/sustainabot-fleet/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -[package] -name = "sustainabot-fleet" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -gitbot-shared-context = { path = "../../../gitbot-fleet/shared-context" } -sustainabot-analysis = { path = "../sustainabot-analysis" } -sustainabot-metrics = { path = "../sustainabot-metrics" } -anyhow.workspace = true -serde_json.workspace = true diff --git a/bots/sustainabot/crates/sustainabot-fleet/src/lib.rs b/bots/sustainabot/crates/sustainabot-fleet/src/lib.rs deleted file mode 100644 index e082bb32..00000000 --- a/bots/sustainabot/crates/sustainabot-fleet/src/lib.rs +++ /dev/null @@ -1,449 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell - -//! Gitbot fleet integration for sustainabot -//! -//! Publishes ecological and economic analysis findings to the shared context -//! layer for consumption by other bots in the fleet. - -#![forbid(unsafe_code)] -use anyhow::Result; -use gitbot_shared_context::{BotId, Context, Finding, Severity}; -use std::path::{Path, PathBuf}; -use sustainabot_analysis::directives; -use sustainabot_metrics::AnalysisResult; - -/// Ecological thresholds for reporting -pub struct EcologicalThresholds { - /// Total energy threshold in kilojoules - pub total_energy_threshold_kj: f64, - /// Total carbon threshold in grams - pub total_carbon_threshold_grams: f64, - /// Per-function energy threshold in joules - pub energy_per_function_joules: f64, -} - -impl Default for EcologicalThresholds { - fn default() -> Self { - Self { - total_energy_threshold_kj: 10.0, - total_carbon_threshold_grams: 2.0, - energy_per_function_joules: 100.0, - } - } -} - -/// Publish sustainabot analysis findings to the fleet shared context. -/// -/// This is the primary integration point: converts `AnalysisResult` items -/// from `sustainabot-metrics` into `Finding` objects for the fleet. -pub fn publish_findings( - ctx: &mut Context, - results: &[AnalysisResult], - thresholds: &EcologicalThresholds, -) -> Result<()> { - let mut total_energy_j = 0.0f64; - let mut total_carbon_g = 0.0f64; - let mut high_impact_functions = Vec::new(); - - for result in results { - total_energy_j += result.resources.energy.0; - total_carbon_g += result.resources.carbon.0; - - // Flag high-impact functions - if result.resources.energy.0 > thresholds.energy_per_function_joules { - high_impact_functions.push(( - result.location.name.clone().unwrap_or_else(|| "".to_string()), - result.resources.energy.0, - )); - } - - // Convert each result to a rich Finding - let finding = convert_to_finding(result); - ctx.add_finding(finding); - } - - // Report overall resource usage - let total_energy_kj = total_energy_j / 1000.0; - if total_energy_kj > thresholds.total_energy_threshold_kj { - ctx.add_finding( - Finding::new( - BotId::Sustainabot, - "SUSTAIN-HIGH-ENERGY", - Severity::Warning, - &format!( - "High total energy consumption: {:.2} kJ (threshold: {:.2} kJ)", - total_energy_kj, thresholds.total_energy_threshold_kj - ), - ) - .with_category("sustainability"), - ); - } - - if total_carbon_g > thresholds.total_carbon_threshold_grams { - ctx.add_finding( - Finding::new( - BotId::Sustainabot, - "SUSTAIN-HIGH-CARBON", - Severity::Warning, - &format!( - "High carbon footprint: {:.4}g CO2e (threshold: {:.2}g)", - total_carbon_g, thresholds.total_carbon_threshold_grams - ), - ) - .with_category("sustainability"), - ); - } - - // Report high-impact functions - if !high_impact_functions.is_empty() { - let function_list = high_impact_functions - .iter() - .map(|(name, energy)| format!("{} ({:.2}J)", name, energy)) - .collect::>() - .join(", "); - - ctx.add_finding( - Finding::new( - BotId::Sustainabot, - "SUSTAIN-HIGH-IMPACT-FUNCTIONS", - Severity::Info, - &format!( - "{} function(s) exceed per-function energy threshold: {}", - high_impact_functions.len(), - function_list - ), - ) - .with_category("sustainability"), - ); - } - - // Efficiency rating - let rating = calculate_efficiency_rating(results); - ctx.add_finding( - Finding::new( - BotId::Sustainabot, - "SUSTAIN-EFFICIENCY-RATING", - Severity::Info, - &format!("Ecological efficiency rating: {}", rating), - ) - .with_category("sustainability") - .with_metadata(serde_json::json!({ - "rating": rating, - "total_energy_kj": total_energy_kj, - "total_carbon_g": total_carbon_g, - "functions_analyzed": results.len(), - })), - ); - - Ok(()) -} - -/// Convert a sustainabot AnalysisResult to a gitbot-fleet Finding -/// using ALL builder fields for rich integration. -fn convert_to_finding(result: &AnalysisResult) -> Finding { - let func_name = result - .location - .name - .as_deref() - .unwrap_or(""); - - let severity = if result.health.eco_score.0 < 30.0 { - Severity::Error - } else if result.health.eco_score.0 < 60.0 { - Severity::Warning - } else if result.health.eco_score.0 < 80.0 { - Severity::Info - } else { - Severity::Suggestion - }; - - let message = format!( - "{}: eco={:.0}/100 energy={:.2}J carbon={:.4}gCO2e. {}", - func_name, - result.health.eco_score.0, - result.resources.energy.0, - result.resources.carbon.0, - result.recommendations.join("; "), - ); - - let mut finding = Finding::new( - BotId::Sustainabot, - &result.rule_id, - severity, - &message, - ) - .with_rule_name(&format!("Sustainability: {}", result.rule_id)) - .with_category("sustainability") - .with_file(PathBuf::from(&result.location.file)) - .with_location(result.location.line, result.location.column) - .with_metadata(serde_json::json!({ - "eco_score": result.health.eco_score.0, - "econ_score": result.health.econ_score.0, - "quality_score": result.health.quality_score, - "overall_health": result.health.overall, - "energy_joules": result.resources.energy.0, - "carbon_gco2e": result.resources.carbon.0, - "duration_ms": result.resources.duration.0, - "memory_bytes": result.resources.memory.0, - "confidence": format!("{:?}", result.confidence), - })); - - if let Some(ref suggestion) = result.suggestion { - finding = finding.with_suggestion(suggestion); - } - - // Mark as fixable if there's a concrete suggestion - if result.suggestion.is_some() { - finding = finding.fixable(); - } - - finding -} - -/// Run sustainabot as a fleet member with directive awareness. -/// -/// Reads `.machine_readable/bot_directives/sustainabot.scm` (legacy fallback -/// supported) to determine allowed scopes, -/// then runs analysis respecting the directive. -pub fn run_fleet_analysis( - repo_path: &Path, - context_path: Option<&Path>, -) -> Result<()> { - // Check for sustainabot directive - let directive = directives::check_directive(repo_path, "sustainabot"); - - if let Some(ref d) = directive { - if !d.allow { - eprintln!( - "Sustainabot denied by .machine_readable/bot_directives/sustainabot.scm: {}", - d.notes.as_deref().unwrap_or("no reason given") - ); - return Ok(()); - } - } - - // Collect analysis results - let results = collect_results(repo_path)?; - - // Build thresholds from directive if available - let thresholds = if let Some(ref d) = directive { - let mut t = EcologicalThresholds::default(); - for (key, val) in &d.thresholds { - match key.as_str() { - "energy" => t.energy_per_function_joules = *val, - "carbon" => t.total_carbon_threshold_grams = *val, - _ => {} - } - } - t - } else { - EcologicalThresholds::default() - }; - - // If context file provided, publish to shared context - if let Some(ctx_path) = context_path { - let content = std::fs::read_to_string(ctx_path)?; - let mut ctx: Context = serde_json::from_str(&content)?; - - ctx.start_bot(BotId::Sustainabot)?; - publish_findings(&mut ctx, &results, &thresholds)?; - ctx.complete_bot(BotId::Sustainabot, results.len(), 0, 0)?; - - let output = serde_json::to_string_pretty(&ctx)?; - std::fs::write(ctx_path, output)?; - } else { - // Standalone mode: just print findings - let thresholds = EcologicalThresholds::default(); - let rating = calculate_efficiency_rating(&results); - - eprintln!("Sustainabot fleet analysis: {} functions", results.len()); - eprintln!("Efficiency rating: {}", rating); - - let below: Vec<_> = results - .iter() - .filter(|r| r.health.eco_score.0 < 60.0) - .collect(); - - if !below.is_empty() { - eprintln!("{} functions below eco threshold:", below.len()); - for r in &below { - eprintln!( - " {} (eco: {:.0}, energy: {:.2}J)", - r.location.name.as_deref().unwrap_or(""), - r.health.eco_score.0, - r.resources.energy.0, - ); - } - } - - let total_energy: f64 = results.iter().map(|r| r.resources.energy.0).sum(); - if total_energy / 1000.0 > thresholds.total_energy_threshold_kj { - eprintln!( - "WARNING: Total energy {:.2} kJ exceeds threshold {:.2} kJ", - total_energy / 1000.0, - thresholds.total_energy_threshold_kj - ); - } - } - - Ok(()) -} - -fn collect_results(repo_path: &Path) -> Result> { - let mut results = Vec::new(); - - for entry in walkdir(repo_path) { - match sustainabot_analysis::analyze_file(&entry) { - Ok(file_results) => results.extend(file_results), - Err(_) => continue, - } - } - - Ok(results) -} - -/// Simple directory walker for supported files -fn walkdir(path: &Path) -> Vec { - let mut files = Vec::new(); - walk_recursive(path, &mut files); - files -} - -fn walk_recursive(path: &Path, files: &mut Vec) { - let entries = match std::fs::read_dir(path) { - Ok(e) => e, - Err(_) => return, - }; - - for entry in entries.flatten() { - let p = entry.path(); - let name = p.file_name().and_then(|n| n.to_str()).unwrap_or(""); - - // Skip common non-source directories - if p.is_dir() { - if !matches!( - name, - "target" | "node_modules" | ".git" | "dist" | "build" | ".cache" | "__pycache__" - ) { - walk_recursive(&p, files); - } - continue; - } - - // Only include supported source files - if let Some(ext) = p.extension().and_then(|e| e.to_str()) { - if matches!(ext, "rs" | "js" | "py") { - files.push(p); - } - } - } -} - -/// Calculate efficiency rating (A-F scale) -fn calculate_efficiency_rating(results: &[AnalysisResult]) -> String { - if results.is_empty() { - return "N/A".to_string(); - } - - let avg_energy: f64 = - results.iter().map(|r| r.resources.energy.0).sum::() / results.len() as f64; - - if avg_energy < 10.0 { - "A (Excellent)".to_string() - } else if avg_energy < 50.0 { - "B (Good)".to_string() - } else if avg_energy < 100.0 { - "C (Average)".to_string() - } else if avg_energy < 200.0 { - "D (Below Average)".to_string() - } else if avg_energy < 500.0 { - "E (Poor)".to_string() - } else { - "F (Very Poor)".to_string() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use sustainabot_metrics::*; - - fn sample_result(energy_j: f64) -> AnalysisResult { - AnalysisResult { - location: CodeLocation { - file: "test.rs".to_string(), - line: 1, - column: 1, - end_line: Some(10), - end_column: Some(2), - name: Some("test_fn".to_string()), - }, - resources: ResourceProfile { - energy: Energy::joules(energy_j), - duration: Duration::milliseconds(energy_j * 0.5), - carbon: Carbon::grams_co2e(energy_j * 0.0001), - memory: Memory::kilobytes(100), - }, - health: HealthIndex::compute( - EcoScore::new(80.0), - EconScore::new(70.0), - 75.0, - ), - recommendations: vec!["Code looks efficient".to_string()], - rule_id: "sustainabot/general".to_string(), - suggestion: None, - end_location: Some((10, 2)), - confidence: Confidence::Estimated, - } - } - - #[test] - fn test_efficiency_rating() { - let results = vec![sample_result(5.0)]; - let rating = calculate_efficiency_rating(&results); - assert!(rating.starts_with('A')); - } - - #[test] - fn test_convert_to_finding() { - let result = sample_result(5.0); - let finding = convert_to_finding(&result); - - assert_eq!(finding.source, BotId::Sustainabot); - assert_eq!(finding.rule_id, "sustainabot/general"); - assert_eq!(finding.category, "sustainability"); - assert!(finding.file.is_some()); - assert!(finding.line.is_some()); - } - - #[test] - fn test_convert_finding_with_suggestion() { - let mut result = sample_result(5.0); - result.suggestion = Some("Use hash map for O(1) lookup".to_string()); - result.rule_id = "sustainabot/nested-loops".to_string(); - - let finding = convert_to_finding(&result); - assert!(finding.fixable); - assert!(finding.suggestion.is_some()); - } - - #[test] - fn test_severity_mapping() { - // Low eco score → Error - let mut result = sample_result(5.0); - result.health = HealthIndex::compute(EcoScore::new(20.0), EconScore::new(70.0), 75.0); - let finding = convert_to_finding(&result); - assert_eq!(finding.severity, Severity::Error); - - // Medium eco score → Warning - result.health = HealthIndex::compute(EcoScore::new(50.0), EconScore::new(70.0), 75.0); - let finding = convert_to_finding(&result); - assert_eq!(finding.severity, Severity::Warning); - - // High eco score → Info - result.health = HealthIndex::compute(EcoScore::new(75.0), EconScore::new(70.0), 75.0); - let finding = convert_to_finding(&result); - assert_eq!(finding.severity, Severity::Info); - } -} diff --git a/bots/sustainabot/crates/sustainabot-metrics/Cargo.toml b/bots/sustainabot/crates/sustainabot-metrics/Cargo.toml deleted file mode 100644 index 83bc6771..00000000 --- a/bots/sustainabot/crates/sustainabot-metrics/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -[package] -name = "sustainabot-metrics" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -serde.workspace = true -serde_json.workspace = true -thiserror.workspace = true diff --git a/bots/sustainabot/crates/sustainabot-metrics/src/lib.rs b/bots/sustainabot/crates/sustainabot-metrics/src/lib.rs deleted file mode 100644 index 3dbd8c21..00000000 --- a/bots/sustainabot/crates/sustainabot-metrics/src/lib.rs +++ /dev/null @@ -1,329 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! # SustainaBot Metrics -//! -//! Core data types for ecological and economic code analysis. -//! Inspired by Eclexia's resource-aware design principles. - -#![forbid(unsafe_code)] -use serde::{Deserialize, Serialize}; -use std::ops::{Add, Mul}; - -/// Energy measurement in Joules -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)] -pub struct Energy(pub f64); - -impl Energy { - pub const ZERO: Self = Energy(0.0); - - pub fn joules(j: f64) -> Self { - Energy(j) - } - - pub fn kilojoules(kj: f64) -> Self { - Energy(kj * 1000.0) - } -} - -impl Add for Energy { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - Energy(self.0 + rhs.0) - } -} - -impl Mul for Energy { - type Output = Self; - fn mul(self, rhs: f64) -> Self::Output { - Energy(self.0 * rhs) - } -} - -/// Time duration in milliseconds -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)] -pub struct Duration(pub f64); - -impl Duration { - pub const ZERO: Self = Duration(0.0); - - pub fn milliseconds(ms: f64) -> Self { - Duration(ms) - } - - pub fn seconds(s: f64) -> Self { - Duration(s * 1000.0) - } -} - -impl Add for Duration { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - Duration(self.0 + rhs.0) - } -} - -/// Carbon emissions in grams of CO2 equivalent -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)] -pub struct Carbon(pub f64); - -impl Carbon { - pub const ZERO: Self = Carbon(0.0); - - pub fn grams_co2e(g: f64) -> Self { - Carbon(g) - } - - pub fn kilograms_co2e(kg: f64) -> Self { - Carbon(kg * 1000.0) - } -} - -impl Add for Carbon { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - Carbon(self.0 + rhs.0) - } -} - -impl Mul for Carbon { - type Output = Self; - fn mul(self, rhs: f64) -> Self::Output { - Carbon(self.0 * rhs) - } -} - -/// Memory usage in bytes -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)] -pub struct Memory(pub usize); - -impl Memory { - pub const ZERO: Self = Memory(0); - - pub fn bytes(b: usize) -> Self { - Memory(b) - } - - pub fn kilobytes(kb: usize) -> Self { - Memory(kb * 1024) - } - - pub fn megabytes(mb: usize) -> Self { - Memory(mb * 1024 * 1024) - } -} - -impl Add for Memory { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - Memory(self.0 + rhs.0) - } -} - -/// Complete resource profile for a code unit -/// -/// This is inspired by Eclexia's `@provides` annotations but tracked -/// at runtime during analysis rather than compile-time. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResourceProfile { - pub energy: Energy, - pub duration: Duration, - pub carbon: Carbon, - pub memory: Memory, -} - -impl ResourceProfile { - pub fn zero() -> Self { - ResourceProfile { - energy: Energy::ZERO, - duration: Duration::ZERO, - carbon: Carbon::ZERO, - memory: Memory::ZERO, - } - } - - /// Calculate cost using shadow prices (Eclexia-inspired) - /// - /// Cost = λ_energy * energy + λ_time * time + λ_carbon * carbon - pub fn cost(&self, shadow_prices: &ShadowPrices) -> f64 { - shadow_prices.energy * self.energy.0 - + shadow_prices.time * self.duration.0 - + shadow_prices.carbon * self.carbon.0 - } -} - -impl Add for ResourceProfile { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - ResourceProfile { - energy: self.energy + rhs.energy, - duration: self.duration + rhs.duration, - carbon: self.carbon + rhs.carbon, - memory: self.memory + rhs.memory, - } - } -} - -/// Shadow prices for resources (economic optimization) -/// -/// These represent the marginal value of each resource, guiding -/// trade-off decisions. Inspired by Eclexia's shadow price system. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ShadowPrices { - /// Value per Joule of energy - pub energy: f64, - /// Value per millisecond of time - pub time: f64, - /// Value per gram of CO2e - pub carbon: f64, -} - -impl Default for ShadowPrices { - fn default() -> Self { - // Default weights favoring carbon reduction - ShadowPrices { - energy: 1.0, - time: 0.5, - carbon: 2.0, // Carbon twice as important as energy - } - } -} - -/// Ecological score (0-100) -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)] -pub struct EcoScore(pub f64); - -impl EcoScore { - pub fn new(score: f64) -> Self { - EcoScore(score.clamp(0.0, 100.0)) - } -} - -/// Economic score (0-100) - measures Pareto efficiency -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)] -pub struct EconScore(pub f64); - -impl EconScore { - pub fn new(score: f64) -> Self { - EconScore(score.clamp(0.0, 100.0)) - } -} - -/// Confidence level for an estimate -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum Confidence { - /// Derived from actual measurements or profiling data - Measured, - /// Calibrated against known baselines - Calibrated, - /// Heuristic estimate with reasonable basis - Estimated, - /// No strong basis; placeholder value - Unknown, -} - -impl Default for Confidence { - fn default() -> Self { - Confidence::Unknown - } -} - -/// Overall health index combining eco and econ scores -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HealthIndex { - pub eco_score: EcoScore, - pub econ_score: EconScore, - pub quality_score: f64, - pub overall: f64, -} - -impl HealthIndex { - pub fn compute(eco: EcoScore, econ: EconScore, quality: f64) -> Self { - // Formula from README: 0.4 × Eco + 0.3 × Econ + 0.3 × Quality - let overall = 0.4 * eco.0 + 0.3 * econ.0 + 0.3 * quality; - - HealthIndex { - eco_score: eco, - econ_score: econ, - quality_score: quality, - overall, - } - } -} - -/// Analysis result for a single code unit (function, file, module) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AnalysisResult { - pub location: CodeLocation, - pub resources: ResourceProfile, - pub health: HealthIndex, - pub recommendations: Vec, - /// Machine-readable rule identifier (e.g. "sustainabot/nested-loops") - #[serde(default)] - pub rule_id: String, - /// Concrete suggestion for fixing the finding - #[serde(default)] - pub suggestion: Option, - /// End location for range-based annotations - #[serde(default)] - pub end_location: Option<(usize, usize)>, - /// How confident is this estimate? - #[serde(default)] - pub confidence: Confidence, -} - -/// Source code location -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CodeLocation { - pub file: String, - pub line: usize, - pub column: usize, - /// End line (1-indexed) - #[serde(default)] - pub end_line: Option, - /// End column (1-indexed) - #[serde(default)] - pub end_column: Option, - pub name: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_energy_arithmetic() { - let e1 = Energy::joules(10.0); - let e2 = Energy::joules(5.0); - assert_eq!(e1 + e2, Energy::joules(15.0)); - assert_eq!(e1 * 2.0, Energy::joules(20.0)); - } - - #[test] - fn test_resource_cost() { - let profile = ResourceProfile { - energy: Energy::joules(10.0), - duration: Duration::milliseconds(100.0), - carbon: Carbon::grams_co2e(5.0), - memory: Memory::bytes(1024), - }; - - let prices = ShadowPrices::default(); - let cost = profile.cost(&prices); - - // cost = 1.0*10 + 0.5*100 + 2.0*5 = 10 + 50 + 10 = 70 - assert_eq!(cost, 70.0); - } - - #[test] - fn test_health_index() { - let health = HealthIndex::compute( - EcoScore::new(80.0), - EconScore::new(70.0), - 60.0, - ); - - // 0.4*80 + 0.3*70 + 0.3*60 = 32 + 21 + 18 = 71 - assert_eq!(health.overall, 71.0); - } -} diff --git a/bots/sustainabot/crates/sustainabot-sarif/Cargo.toml b/bots/sustainabot/crates/sustainabot-sarif/Cargo.toml deleted file mode 100644 index f44b4d2e..00000000 --- a/bots/sustainabot/crates/sustainabot-sarif/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -[package] -name = "sustainabot-sarif" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -description = "SARIF 2.1.0 output for sustainabot analysis results" - -[dependencies] -sustainabot-metrics = { path = "../sustainabot-metrics" } -serde.workspace = true -serde_json.workspace = true diff --git a/bots/sustainabot/crates/sustainabot-sarif/src/lib.rs b/bots/sustainabot/crates/sustainabot-sarif/src/lib.rs deleted file mode 100644 index addd7c49..00000000 --- a/bots/sustainabot/crates/sustainabot-sarif/src/lib.rs +++ /dev/null @@ -1,464 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -//! SARIF 2.1.0 output for sustainabot analysis results. -//! -//! Produces machine-readable SARIF that GitHub/IDEs render as inline annotations. - -#![forbid(unsafe_code)] -use serde::{Deserialize, Serialize}; -use sustainabot_metrics::AnalysisResult; - -/// SARIF schema version -const SARIF_SCHEMA: &str = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json"; -const SARIF_VERSION: &str = "2.1.0"; -const TOOL_NAME: &str = "sustainabot"; -const TOOL_INFO_URI: &str = "https://github.com/hyperpolymath/sustainabot"; - -/// Top-level SARIF log -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SarifLog { - #[serde(rename = "$schema")] - pub schema: String, - pub version: String, - pub runs: Vec, -} - -/// A single SARIF run -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Run { - pub tool: Tool, - pub results: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub invocations: Option>, -} - -/// Tool description -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Tool { - pub driver: ToolComponent, -} - -/// Tool component with rules -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ToolComponent { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub semantic_version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub information_uri: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub rules: Vec, -} - -/// Rule definition -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ReportingDescriptor { - pub id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - pub short_description: Message, - #[serde(skip_serializing_if = "Option::is_none")] - pub full_description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub help: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub default_configuration: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub properties: Option, -} - -/// Reporting configuration for severity -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ReportingConfiguration { - pub level: SarifLevel, -} - -/// A single finding result -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SarifResult { - pub rule_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub rule_index: Option, - pub level: SarifLevel, - pub message: Message, - pub locations: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub fixes: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub properties: Option, -} - -/// SARIF severity levels -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum SarifLevel { - Error, - Warning, - Note, - None, -} - -/// Simple message -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Message { - pub text: String, -} - -/// Multiformat message (text + markdown) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MultiformatMessage { - pub text: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub markdown: Option, -} - -/// Code location -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Location { - pub physical_location: PhysicalLocation, - #[serde(skip_serializing_if = "Option::is_none")] - pub logical_locations: Option>, -} - -/// Physical file location -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PhysicalLocation { - pub artifact_location: ArtifactLocation, - #[serde(skip_serializing_if = "Option::is_none")] - pub region: Option, -} - -/// File path reference -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ArtifactLocation { - pub uri: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub uri_base_id: Option, -} - -/// Line/column region -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Region { - #[serde(skip_serializing_if = "Option::is_none")] - pub start_line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_column: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_column: Option, -} - -/// Logical location (function name, etc.) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LogicalLocation { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub kind: Option, -} - -/// Suggested fix -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Fix { - pub description: Message, -} - -/// Invocation metadata -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Invocation { - pub execution_successful: bool, -} - -/// Known sustainabot rules -fn builtin_rules() -> Vec { - vec![ - rule("sustainabot/general", "General sustainability finding", - "Code unit analyzed for ecological and economic efficiency.", - SarifLevel::Note), - rule("sustainabot/nested-loops", "Deeply nested loops", - "Deeply nested loops create O(n^k) complexity, wasting CPU and energy.", - SarifLevel::Warning), - rule("sustainabot/busy-wait", "Busy-wait loop", - "Loop without sleep/await/yield burns CPU continuously.", - SarifLevel::Warning), - rule("sustainabot/string-concat-in-loop", "String concatenation in loop", - "String concatenation inside loops causes repeated heap allocation.", - SarifLevel::Warning), - rule("sustainabot/clone-in-loop", "Clone in loop", - ".clone() inside loop body causes repeated deep copies.", - SarifLevel::Note), - rule("sustainabot/unbuffered-io", "Unbuffered I/O", - "File I/O without buffering causes excessive system calls.", - SarifLevel::Warning), - rule("sustainabot/large-allocation", "Large allocation", - "Heap allocation exceeding 1MB detected.", - SarifLevel::Note), - rule("sustainabot/redundant-allocation", "Redundant allocation", - "Excessive .to_string()/.to_owned() where borrows suffice.", - SarifLevel::Note), - rule("sustainabot/eco-threshold", "Below eco threshold", - "Function's ecological score is below the configured threshold.", - SarifLevel::Warning), - rule("sustainabot/carbon-intensity", "High carbon intensity", - "Estimated carbon emissions exceed sustainable baseline.", - SarifLevel::Warning), - rule("sustainabot/security-sustainability", "Security-sustainability correlation", - "Security weak point with sustainability impact detected.", - SarifLevel::Warning), - ] -} - -fn rule(id: &str, name: &str, desc: &str, level: SarifLevel) -> ReportingDescriptor { - ReportingDescriptor { - id: id.to_string(), - name: Some(name.to_string()), - short_description: Message { text: desc.to_string() }, - full_description: None, - help: None, - default_configuration: Some(ReportingConfiguration { level }), - properties: None, - } -} - -/// Convert sustainabot analysis results into a SARIF log. -pub fn to_sarif(results: &[AnalysisResult], version: &str) -> SarifLog { - let rules = builtin_rules(); - let rule_ids: Vec<&str> = rules.iter().map(|r| r.id.as_str()).collect(); - - let sarif_results: Vec = results - .iter() - .map(|r| convert_result(r, &rule_ids)) - .collect(); - - SarifLog { - schema: SARIF_SCHEMA.to_string(), - version: SARIF_VERSION.to_string(), - runs: vec![Run { - tool: Tool { - driver: ToolComponent { - name: TOOL_NAME.to_string(), - semantic_version: Some(version.to_string()), - information_uri: Some(TOOL_INFO_URI.to_string()), - rules, - }, - }, - results: sarif_results, - invocations: Some(vec![Invocation { - execution_successful: true, - }]), - }], - } -} - -/// Convert sustainabot analysis results to SARIF JSON string. -pub fn to_sarif_json(results: &[AnalysisResult], version: &str) -> Result { - let log = to_sarif(results, version); - serde_json::to_string_pretty(&log) -} - -fn convert_result(result: &AnalysisResult, rule_ids: &[&str]) -> SarifResult { - let rule_id = &result.rule_id; - let rule_index = rule_ids.iter().position(|&id| id == rule_id); - - // Determine severity from health scores - let level = if result.health.eco_score.0 < 30.0 { - SarifLevel::Error - } else if result.health.eco_score.0 < 60.0 { - SarifLevel::Warning - } else { - SarifLevel::Note - }; - - // Build message - let func_name = result - .location - .name - .as_deref() - .unwrap_or(""); - let message_text = if result.recommendations.is_empty() { - format!( - "{}: eco={:.0}/100, energy={:.2}J, carbon={:.4}gCO2e", - func_name, - result.health.eco_score.0, - result.resources.energy.0, - result.resources.carbon.0, - ) - } else { - format!( - "{}: eco={:.0}/100, energy={:.2}J, carbon={:.4}gCO2e. {}", - func_name, - result.health.eco_score.0, - result.resources.energy.0, - result.resources.carbon.0, - result.recommendations.join("; "), - ) - }; - - // Build location - let location = Location { - physical_location: PhysicalLocation { - artifact_location: ArtifactLocation { - 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, - }), - }, - logical_locations: result.location.name.as_ref().map(|name| { - vec![LogicalLocation { - name: name.clone(), - kind: Some("function".to_string()), - }] - }), - }; - - // Build fixes from suggestion - let fixes = result - .suggestion - .as_ref() - .map(|s| { - vec![Fix { - description: Message { text: s.clone() }, - }] - }) - .unwrap_or_default(); - - // Custom properties with eco/econ scores and resource profile - let properties = serde_json::json!({ - "eco_score": result.health.eco_score.0, - "econ_score": result.health.econ_score.0, - "quality_score": result.health.quality_score, - "overall_health": result.health.overall, - "energy_joules": result.resources.energy.0, - "carbon_gco2e": result.resources.carbon.0, - "duration_ms": result.resources.duration.0, - "memory_bytes": result.resources.memory.0, - "confidence": format!("{:?}", result.confidence), - }); - - SarifResult { - rule_id: rule_id.clone(), - rule_index, - level, - message: Message { text: message_text }, - locations: vec![location], - fixes, - properties: Some(properties), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use sustainabot_metrics::*; - - fn sample_result() -> AnalysisResult { - AnalysisResult { - location: CodeLocation { - file: "src/main.rs".to_string(), - line: 10, - column: 1, - end_line: Some(25), - end_column: Some(2), - name: Some("process_data".to_string()), - }, - resources: ResourceProfile { - energy: Energy::joules(5.0), - duration: Duration::milliseconds(25.0), - carbon: Carbon::grams_co2e(0.0007), - memory: Memory::kilobytes(100), - }, - health: HealthIndex::compute( - EcoScore::new(75.0), - EconScore::new(80.0), - 85.0, - ), - recommendations: vec!["Code looks efficient".to_string()], - rule_id: "sustainabot/general".to_string(), - suggestion: None, - end_location: Some((25, 2)), - confidence: Confidence::Estimated, - } - } - - #[test] - fn test_sarif_structure() { - let results = vec![sample_result()]; - let log = to_sarif(&results, "0.1.0"); - - assert_eq!(log.version, "2.1.0"); - assert_eq!(log.runs.len(), 1); - assert_eq!(log.runs[0].tool.driver.name, "sustainabot"); - assert!(!log.runs[0].tool.driver.rules.is_empty()); - assert_eq!(log.runs[0].results.len(), 1); - } - - #[test] - fn test_sarif_json_valid() { - let results = vec![sample_result()]; - let json = to_sarif_json(&results, "0.1.0").unwrap(); - - // Should be valid JSON - let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed["version"], "2.1.0"); - assert!(parsed["runs"][0]["results"][0]["locations"][0]["physicalLocation"]["region"]["startLine"] - .as_u64() - .unwrap() == 10); - } - - #[test] - fn test_sarif_with_suggestion() { - let mut result = sample_result(); - result.rule_id = "sustainabot/nested-loops".to_string(); - result.suggestion = Some("Use hash map for O(1) lookup".to_string()); - - let log = to_sarif(&[result], "0.1.0"); - let sarif_result = &log.runs[0].results[0]; - - assert_eq!(sarif_result.rule_id, "sustainabot/nested-loops"); - assert_eq!(sarif_result.fixes.len(), 1); - assert_eq!(sarif_result.fixes[0].description.text, "Use hash map for O(1) lookup"); - } - - #[test] - fn test_sarif_severity_mapping() { - // Low eco score → Error - let mut result = sample_result(); - result.health = HealthIndex::compute(EcoScore::new(20.0), EconScore::new(80.0), 85.0); - let log = to_sarif(&[result], "0.1.0"); - assert!(matches!(log.runs[0].results[0].level, SarifLevel::Error)); - - // Medium eco score → Warning - let mut result = sample_result(); - result.health = HealthIndex::compute(EcoScore::new(45.0), EconScore::new(80.0), 85.0); - let log = to_sarif(&[result], "0.1.0"); - assert!(matches!(log.runs[0].results[0].level, SarifLevel::Warning)); - - // High eco score → Note - let mut result = sample_result(); - result.health = HealthIndex::compute(EcoScore::new(85.0), EconScore::new(80.0), 85.0); - let log = to_sarif(&[result], "0.1.0"); - assert!(matches!(log.runs[0].results[0].level, SarifLevel::Note)); - } -} diff --git a/bots/sustainabot/databases/arangodb/schema.js b/bots/sustainabot/databases/arangodb/schema.js deleted file mode 100644 index 7e7f4822..00000000 --- a/bots/sustainabot/databases/arangodb/schema.js +++ /dev/null @@ -1,360 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2025 hyperpolymath -/** - * Oikos Bot ArangoDB Schema Setup - * - * This script initializes the ArangoDB collections and graphs - * for storing code analysis results, dependency graphs, and - * historical metrics. - * - * Run with: arangosh --javascript.execute schema.js - */ - -const db = require('@arangodb').db; -const graph_module = require('@arangodb/general-graph'); - -// ============================================================================= -// COLLECTIONS -// ============================================================================= - -// Document Collections -const documentCollections = [ - { - name: 'projects', - schema: { - rule: { - type: 'object', - properties: { - name: { type: 'string' }, - url: { type: 'string' }, - platform: { type: 'string', enum: ['github', 'gitlab', 'bitbucket'] }, - default_branch: { type: 'string' }, - eco_config: { type: 'object' }, - created_at: { type: 'string', format: 'date-time' }, - updated_at: { type: 'string', format: 'date-time' } - }, - required: ['name', 'url', 'platform'] - }, - level: 'moderate', - message: 'Project document validation failed' - } - }, - { - name: 'files', - schema: { - rule: { - type: 'object', - properties: { - project_id: { type: 'string' }, - path: { type: 'string' }, - language: { type: 'string' }, - size_bytes: { type: 'number' }, - last_modified: { type: 'string', format: 'date-time' }, - hash: { type: 'string' } - }, - required: ['project_id', 'path'] - }, - level: 'moderate' - } - }, - { - name: 'functions', - schema: { - rule: { - type: 'object', - properties: { - file_id: { type: 'string' }, - name: { type: 'string' }, - start_line: { type: 'number' }, - end_line: { type: 'number' }, - parameters: { type: 'array' }, - return_type: { type: 'string' }, - visibility: { type: 'string' } - }, - required: ['file_id', 'name'] - }, - level: 'moderate' - } - }, - { - name: 'analyses', - schema: { - rule: { - type: 'object', - properties: { - entity_id: { type: 'string' }, - entity_type: { type: 'string', enum: ['project', 'file', 'function', 'module'] }, - timestamp: { type: 'string', format: 'date-time' }, - commit_sha: { type: 'string' }, - eco_metrics: { - type: 'object', - properties: { - carbon_score: { type: 'number', minimum: 0, maximum: 100 }, - energy_score: { type: 'number', minimum: 0, maximum: 100 }, - resource_score: { type: 'number', minimum: 0, maximum: 100 }, - eco_score: { type: 'number', minimum: 0, maximum: 100 } - } - }, - econ_metrics: { - type: 'object', - properties: { - pareto_distance: { type: 'number' }, - allocation_score: { type: 'number' }, - debt_score: { type: 'number' }, - econ_score: { type: 'number' } - } - }, - quality_metrics: { - type: 'object', - properties: { - complexity_score: { type: 'number' }, - coupling_score: { type: 'number' }, - coverage_score: { type: 'number' }, - quality_score: { type: 'number' } - } - }, - health_index: { type: 'number' }, - violations: { type: 'array' }, - recommendations: { type: 'array' } - }, - required: ['entity_id', 'entity_type', 'timestamp'] - }, - level: 'moderate' - } - }, - { - name: 'policies', - schema: { - rule: { - type: 'object', - properties: { - name: { type: 'string' }, - type: { type: 'string', enum: ['eco', 'econ', 'quality', 'composite'] }, - version: { type: 'string' }, - rules: { type: 'array' }, - thresholds: { type: 'object' }, - active: { type: 'boolean' }, - created_at: { type: 'string', format: 'date-time' } - }, - required: ['name', 'type', 'version'] - }, - level: 'moderate' - } - }, - { - name: 'praxis_observations', - schema: { - rule: { - type: 'object', - properties: { - entity_id: { type: 'string' }, - action_taken: { type: 'string' }, - metrics_before: { type: 'object' }, - metrics_after: { type: 'object' }, - outcome: { type: 'string', enum: ['positive', 'negative', 'neutral'] }, - timestamp: { type: 'string', format: 'date-time' }, - notes: { type: 'string' } - }, - required: ['entity_id', 'action_taken', 'outcome', 'timestamp'] - }, - level: 'moderate' - } - } -]; - -// Edge Collections -const edgeCollections = [ - { - name: 'depends_on', - description: 'Dependency relationships between code entities' - }, - { - name: 'contains', - description: 'Containment relationships (project->file, file->function)' - }, - { - name: 'calls', - description: 'Function call relationships' - }, - { - name: 'imports', - description: 'Import/require relationships' - }, - { - name: 'evolved_from', - description: 'Historical evolution (for tracking changes over time)' - } -]; - -// ============================================================================= -// CREATE COLLECTIONS -// ============================================================================= - -console.log('Creating document collections...'); -documentCollections.forEach(col => { - if (!db._collection(col.name)) { - db._createDocumentCollection(col.name); - console.log(` Created: ${col.name}`); - - // Apply schema validation - db._collection(col.name).properties({ schema: col.schema }); - } else { - console.log(` Exists: ${col.name}`); - } -}); - -console.log('\nCreating edge collections...'); -edgeCollections.forEach(col => { - if (!db._collection(col.name)) { - db._createEdgeCollection(col.name); - console.log(` Created: ${col.name}`); - } else { - console.log(` Exists: ${col.name}`); - } -}); - -// ============================================================================= -// CREATE GRAPHS -// ============================================================================= - -console.log('\nCreating graphs...'); - -// Code dependency graph -const codeGraphName = 'code_dependencies'; -if (!graph_module._exists(codeGraphName)) { - graph_module._create(codeGraphName, [ - { - collection: 'depends_on', - from: ['files', 'functions'], - to: ['files', 'functions'] - }, - { - collection: 'calls', - from: ['functions'], - to: ['functions'] - }, - { - collection: 'imports', - from: ['files'], - to: ['files'] - } - ], [ - 'projects' - ]); - console.log(` Created: ${codeGraphName}`); -} else { - console.log(` Exists: ${codeGraphName}`); -} - -// Project structure graph -const structureGraphName = 'project_structure'; -if (!graph_module._exists(structureGraphName)) { - graph_module._create(structureGraphName, [ - { - collection: 'contains', - from: ['projects', 'files'], - to: ['files', 'functions'] - } - ]); - console.log(` Created: ${structureGraphName}`); -} else { - console.log(` Exists: ${structureGraphName}`); -} - -// Evolution graph (for praxis tracking) -const evolutionGraphName = 'code_evolution'; -if (!graph_module._exists(evolutionGraphName)) { - graph_module._create(evolutionGraphName, [ - { - collection: 'evolved_from', - from: ['analyses', 'files', 'functions'], - to: ['analyses', 'files', 'functions'] - } - ]); - console.log(` Created: ${evolutionGraphName}`); -} else { - console.log(` Exists: ${evolutionGraphName}`); -} - -// ============================================================================= -// CREATE INDEXES -// ============================================================================= - -console.log('\nCreating indexes...'); - -// Projects indexes -db.projects.ensureIndex({ type: 'hash', fields: ['url'], unique: true }); -db.projects.ensureIndex({ type: 'hash', fields: ['platform'] }); - -// Files indexes -db.files.ensureIndex({ type: 'hash', fields: ['project_id', 'path'], unique: true }); -db.files.ensureIndex({ type: 'hash', fields: ['language'] }); -db.files.ensureIndex({ type: 'hash', fields: ['hash'] }); - -// Functions indexes -db.functions.ensureIndex({ type: 'hash', fields: ['file_id', 'name'] }); - -// Analyses indexes -db.analyses.ensureIndex({ type: 'hash', fields: ['entity_id'] }); -db.analyses.ensureIndex({ type: 'skiplist', fields: ['timestamp'] }); -db.analyses.ensureIndex({ type: 'hash', fields: ['commit_sha'] }); -db.analyses.ensureIndex({ type: 'skiplist', fields: ['health_index'] }); - -// Praxis observations indexes -db.praxis_observations.ensureIndex({ type: 'hash', fields: ['entity_id'] }); -db.praxis_observations.ensureIndex({ type: 'skiplist', fields: ['timestamp'] }); -db.praxis_observations.ensureIndex({ type: 'hash', fields: ['outcome'] }); - -console.log(' Indexes created successfully'); - -// ============================================================================= -// EXAMPLE QUERIES -// ============================================================================= - -console.log('\n=== Example AQL Queries ===\n'); - -console.log('// Find all eco hotspots in a project'); -console.log(` -FOR a IN analyses - FILTER a.eco_metrics.eco_score < 50 - SORT a.eco_metrics.eco_score ASC - RETURN { - entity: a.entity_id, - eco_score: a.eco_metrics.eco_score, - carbon: a.eco_metrics.carbon_score, - recommendations: a.recommendations - } -`); - -console.log('\n// Trace dependency impact (propagate eco scores)'); -console.log(` -FOR v, e, p IN 1..5 OUTBOUND @startEntity GRAPH 'code_dependencies' - LET analysis = FIRST( - FOR a IN analyses - FILTER a.entity_id == v._key - SORT a.timestamp DESC - LIMIT 1 - RETURN a - ) - RETURN { - path: p.vertices[*]._key, - depth: LENGTH(p.edges), - eco_impact: analysis.eco_metrics.eco_score - } -`); - -console.log('\n// Track praxis learning outcomes'); -console.log(` -FOR obs IN praxis_observations - COLLECT action = obs.action_taken, - outcome = obs.outcome - WITH COUNT INTO count - RETURN { - action: action, - outcome: outcome, - count: count, - success_rate: outcome == 'positive' ? count : 0 - } -`); - -console.log('\nSchema setup complete!'); diff --git a/bots/sustainabot/databases/virtuoso/ontology.ttl b/bots/sustainabot/databases/virtuoso/ontology.ttl deleted file mode 100644 index d1577c65..00000000 --- a/bots/sustainabot/databases/virtuoso/ontology.ttl +++ /dev/null @@ -1,341 +0,0 @@ -@prefix eco: . -@prefix rdfs: . -@prefix owl: . -@prefix xsd: . -@prefix schema: . -@prefix seon: . -@prefix dct: . -@prefix skos: . - -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2024-2025 hyperpolymath -# ============================================================================= -# OIKOS BOT ONTOLOGY -# ============================================================================= - -eco:OikosBotOntology a owl:Ontology ; - rdfs:label "Oikos Bot Software Ecology Ontology"@en ; - rdfs:comment "Ontology for describing ecological and economic aspects of software systems"@en ; - dct:creator "Hyperpolymath" ; - owl:versionInfo "0.1.0" . - -# ============================================================================= -# CORE CLASSES -# ============================================================================= - -# Software Entities -eco:SoftwareEntity a owl:Class ; - rdfs:label "Software Entity"@en ; - rdfs:comment "Any identifiable software component"@en . - -eco:Project a owl:Class ; - rdfs:subClassOf eco:SoftwareEntity ; - rdfs:label "Software Project"@en . - -eco:Module a owl:Class ; - rdfs:subClassOf eco:SoftwareEntity ; - rdfs:label "Module"@en . - -eco:SourceFile a owl:Class ; - rdfs:subClassOf eco:SoftwareEntity ; - rdfs:label "Source File"@en . - -eco:Function a owl:Class ; - rdfs:subClassOf eco:SoftwareEntity ; - rdfs:label "Function"@en . - -# Analysis Results -eco:Analysis a owl:Class ; - rdfs:label "Code Analysis"@en ; - rdfs:comment "An analysis performed on a software entity"@en . - -eco:EcoAnalysis a owl:Class ; - rdfs:subClassOf eco:Analysis ; - rdfs:label "Ecological Analysis"@en . - -eco:EconAnalysis a owl:Class ; - rdfs:subClassOf eco:Analysis ; - rdfs:label "Economic Analysis"@en . - -eco:QualityAnalysis a owl:Class ; - rdfs:subClassOf eco:Analysis ; - rdfs:label "Quality Analysis"@en . - -# Metrics -eco:Metric a owl:Class ; - rdfs:label "Metric"@en ; - rdfs:comment "A measurable aspect of software"@en . - -eco:CarbonMetric a owl:Class ; - rdfs:subClassOf eco:Metric ; - rdfs:label "Carbon Intensity Metric"@en ; - rdfs:comment "Based on SCI specification (ISO/IEC 21031:2024)"@en . - -eco:EnergyMetric a owl:Class ; - rdfs:subClassOf eco:Metric ; - rdfs:label "Energy Efficiency Metric"@en . - -eco:ParetoMetric a owl:Class ; - rdfs:subClassOf eco:Metric ; - rdfs:label "Pareto Optimality Metric"@en . - -eco:AllocationMetric a owl:Class ; - rdfs:subClassOf eco:Metric ; - rdfs:label "Allocative Efficiency Metric"@en . - -eco:DebtMetric a owl:Class ; - rdfs:subClassOf eco:Metric ; - rdfs:label "Technical Debt Metric"@en . - -eco:ComplexityMetric a owl:Class ; - rdfs:subClassOf eco:Metric ; - rdfs:label "Complexity Metric"@en . - -# Patterns -eco:Pattern a owl:Class ; - rdfs:label "Code Pattern"@en . - -eco:EcoPattern a owl:Class ; - rdfs:subClassOf eco:Pattern ; - rdfs:label "Ecological Pattern"@en . - -eco:AntiPattern a owl:Class ; - rdfs:subClassOf eco:Pattern ; - rdfs:label "Anti-Pattern"@en . - -eco:BestPractice a owl:Class ; - rdfs:subClassOf eco:Pattern ; - rdfs:label "Best Practice"@en . - -# Policies -eco:Policy a owl:Class ; - rdfs:label "Policy"@en ; - rdfs:comment "A set of rules governing code quality"@en . - -eco:EcoPolicy a owl:Class ; - rdfs:subClassOf eco:Policy ; - rdfs:label "Ecological Policy"@en . - -eco:EconPolicy a owl:Class ; - rdfs:subClassOf eco:Policy ; - rdfs:label "Economic Policy"@en . - -# Recommendations -eco:Recommendation a owl:Class ; - rdfs:label "Recommendation"@en ; - rdfs:comment "A suggested improvement"@en . - -eco:RefactoringRecommendation a owl:Class ; - rdfs:subClassOf eco:Recommendation ; - rdfs:label "Refactoring Recommendation"@en . - -# Praxis (Theory-Practice Loop) -eco:PraxisObservation a owl:Class ; - rdfs:label "Praxis Observation"@en ; - rdfs:comment "An observation from applying theory to practice"@en . - -# ============================================================================= -# OBJECT PROPERTIES -# ============================================================================= - -eco:analyzedBy a owl:ObjectProperty ; - rdfs:domain eco:SoftwareEntity ; - rdfs:range eco:Analysis ; - rdfs:label "analyzed by"@en . - -eco:hasMetric a owl:ObjectProperty ; - rdfs:domain eco:Analysis ; - rdfs:range eco:Metric ; - rdfs:label "has metric"@en . - -eco:dependsOn a owl:ObjectProperty ; - rdfs:domain eco:SoftwareEntity ; - rdfs:range eco:SoftwareEntity ; - rdfs:label "depends on"@en . - -eco:contains a owl:ObjectProperty ; - rdfs:domain eco:SoftwareEntity ; - rdfs:range eco:SoftwareEntity ; - rdfs:label "contains"@en . - -eco:exhibitsPattern a owl:ObjectProperty ; - rdfs:domain eco:SoftwareEntity ; - rdfs:range eco:Pattern ; - rdfs:label "exhibits pattern"@en . - -eco:violatesPolicy a owl:ObjectProperty ; - rdfs:domain eco:SoftwareEntity ; - rdfs:range eco:Policy ; - rdfs:label "violates policy"@en . - -eco:hasRecommendation a owl:ObjectProperty ; - rdfs:domain eco:Analysis ; - rdfs:range eco:Recommendation ; - rdfs:label "has recommendation"@en . - -eco:dominates a owl:ObjectProperty ; - rdfs:domain eco:SoftwareEntity ; - rdfs:range eco:SoftwareEntity ; - rdfs:label "Pareto dominates"@en ; - rdfs:comment "In multi-objective optimization sense"@en . - -eco:learnedFrom a owl:ObjectProperty ; - rdfs:domain eco:Policy ; - rdfs:range eco:PraxisObservation ; - rdfs:label "learned from"@en . - -# ============================================================================= -# DATA PROPERTIES -# ============================================================================= - -eco:carbonScore a owl:DatatypeProperty ; - rdfs:domain eco:CarbonMetric ; - rdfs:range xsd:float ; - rdfs:label "carbon score"@en ; - rdfs:comment "Normalized score 0-100, higher is better (lower carbon)"@en . - -eco:energyScore a owl:DatatypeProperty ; - rdfs:domain eco:EnergyMetric ; - rdfs:range xsd:float ; - rdfs:label "energy score"@en . - -eco:paretoDistance a owl:DatatypeProperty ; - rdfs:domain eco:ParetoMetric ; - rdfs:range xsd:float ; - rdfs:label "Pareto frontier distance"@en ; - rdfs:comment "Distance from the Pareto optimal frontier"@en . - -eco:allocativeEfficiency a owl:DatatypeProperty ; - rdfs:domain eco:AllocationMetric ; - rdfs:range xsd:float ; - rdfs:label "allocative efficiency"@en . - -eco:debtPrincipal a owl:DatatypeProperty ; - rdfs:domain eco:DebtMetric ; - rdfs:range xsd:float ; - rdfs:label "debt principal"@en ; - rdfs:comment "Estimated hours to remediate"@en . - -eco:cyclomaticComplexity a owl:DatatypeProperty ; - rdfs:domain eco:ComplexityMetric ; - rdfs:range xsd:integer ; - rdfs:label "cyclomatic complexity"@en . - -eco:cognitiveComplexity a owl:DatatypeProperty ; - rdfs:domain eco:ComplexityMetric ; - rdfs:range xsd:integer ; - rdfs:label "cognitive complexity"@en . - -eco:healthIndex a owl:DatatypeProperty ; - rdfs:domain eco:Analysis ; - rdfs:range xsd:float ; - rdfs:label "health index"@en ; - rdfs:comment "Composite score combining eco, econ, and quality"@en . - -eco:confidence a owl:DatatypeProperty ; - rdfs:domain eco:Recommendation ; - rdfs:range xsd:float ; - rdfs:label "confidence"@en ; - rdfs:comment "Probability-based confidence in recommendation"@en . - -eco:timestamp a owl:DatatypeProperty ; - rdfs:domain eco:Analysis ; - rdfs:range xsd:dateTime ; - rdfs:label "timestamp"@en . - -# ============================================================================= -# INDIVIDUALS (Common Patterns and Best Practices) -# ============================================================================= - -# Anti-patterns -eco:BusyWaiting a eco:AntiPattern ; - rdfs:label "Busy Waiting"@en ; - eco:carbonImpact "0.3"^^xsd:float ; - rdfs:comment "Polling in a tight loop wastes CPU cycles and energy"@en ; - skos:altLabel "Spin Lock", "Polling Loop" . - -eco:PrematureOptimization a eco:AntiPattern ; - rdfs:label "Premature Optimization"@en ; - rdfs:comment "Optimizing before understanding actual bottlenecks"@en . - -eco:LeakyAbstraction a eco:AntiPattern ; - rdfs:label "Leaky Abstraction"@en ; - rdfs:comment "Abstraction that exposes implementation details"@en . - -# Best practices -eco:ConnectionPooling a eco:BestPractice ; - rdfs:label "Connection Pooling"@en ; - eco:carbonImpact "-0.15"^^xsd:float ; - eco:energyImpact "-0.20"^^xsd:float ; - rdfs:comment "Reuse connections instead of creating new ones"@en . - -eco:Memoization a eco:BestPractice ; - rdfs:label "Memoization"@en ; - eco:carbonImpact "-0.25"^^xsd:float ; - rdfs:comment "Cache results of expensive computations"@en . - -eco:LazyEvaluation a eco:BestPractice ; - rdfs:label "Lazy Evaluation"@en ; - eco:carbonImpact "-0.10"^^xsd:float ; - rdfs:comment "Defer computation until results are needed"@en . - -eco:EventDriven a eco:BestPractice ; - rdfs:label "Event-Driven Architecture"@en ; - eco:energyImpact "-0.30"^^xsd:float ; - rdfs:comment "React to events instead of polling"@en . - -eco:Batching a eco:BestPractice ; - rdfs:label "Batching"@en ; - eco:carbonImpact "-0.15"^^xsd:float ; - rdfs:comment "Batch I/O and network operations"@en . - -# Standard policies -eco:EcoMinimum a eco:EcoPolicy ; - rdfs:label "Eco Minimum Standard"@en ; - eco:carbonThreshold "50"^^xsd:float ; - eco:energyThreshold "50"^^xsd:float ; - rdfs:comment "Minimum acceptable eco standards"@en . - -eco:EcoStandard a eco:EcoPolicy ; - rdfs:label "Eco Standard"@en ; - eco:carbonThreshold "70"^^xsd:float ; - eco:energyThreshold "70"^^xsd:float ; - rdfs:comment "Recommended eco standards"@en . - -eco:EcoExcellence a eco:EcoPolicy ; - rdfs:label "Eco Excellence"@en ; - eco:carbonThreshold "85"^^xsd:float ; - eco:energyThreshold "85"^^xsd:float ; - rdfs:comment "Excellence in eco practices"@en . - -# ============================================================================= -# EXAMPLE SPARQL QUERIES -# ============================================================================= - -# Query: Find all entities below eco minimum -# PREFIX eco: -# SELECT ?entity ?carbonScore ?energyScore -# WHERE { -# ?entity a eco:SoftwareEntity . -# ?entity eco:analyzedBy ?analysis . -# ?analysis eco:hasMetric ?carbonMetric . -# ?carbonMetric a eco:CarbonMetric ; -# eco:carbonScore ?carbonScore . -# ?analysis eco:hasMetric ?energyMetric . -# ?energyMetric a eco:EnergyMetric ; -# eco:energyScore ?energyScore . -# FILTER (?carbonScore < 50 || ?energyScore < 50) -# } - -# Query: Find best practices applicable to an entity's anti-patterns -# PREFIX eco: -# PREFIX skos: -# SELECT ?entity ?antiPattern ?bestPractice ?impact -# WHERE { -# ?entity eco:exhibitsPattern ?antiPattern . -# ?antiPattern a eco:AntiPattern . -# ?bestPractice a eco:BestPractice ; -# eco:carbonImpact ?impact . -# FILTER (?impact < 0) -# } -# ORDER BY ?impact diff --git a/bots/sustainabot/databases/virtuoso/queries.sparql b/bots/sustainabot/databases/virtuoso/queries.sparql deleted file mode 100644 index ab7f000d..00000000 --- a/bots/sustainabot/databases/virtuoso/queries.sparql +++ /dev/null @@ -1,265 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2024-2025 hyperpolymath -# -# Oikos Bot SPARQL Queries for Virtuoso -# ===================================== -# Common queries for the Oikos Bot semantic knowledge base - -PREFIX eco: -PREFIX rdfs: -PREFIX xsd: -PREFIX skos: - -# ----------------------------------------------------------------------------- -# QUERY 1: Find all eco hotspots (entities below minimum threshold) -# ----------------------------------------------------------------------------- - -# @name: eco_hotspots -# @description: Find software entities not meeting eco minimum standards -SELECT ?entity ?entityLabel ?carbonScore ?energyScore ?healthIndex -WHERE { - ?entity a eco:SoftwareEntity ; - rdfs:label ?entityLabel . - ?analysis eco:analyzedBy ?entity ; - eco:healthIndex ?healthIndex . - ?analysis eco:hasMetric ?carbonMetric . - ?carbonMetric a eco:CarbonMetric ; - eco:carbonScore ?carbonScore . - ?analysis eco:hasMetric ?energyMetric . - ?energyMetric a eco:EnergyMetric ; - eco:energyScore ?energyScore . - FILTER (?carbonScore < 50 || ?energyScore < 50) -} -ORDER BY ?healthIndex - -# ----------------------------------------------------------------------------- -# QUERY 2: Find best practices for improvement -# ----------------------------------------------------------------------------- - -# @name: best_practices_for_entity -# @param: entityUri - URI of the entity to get recommendations for -SELECT ?practice ?practiceLabel ?carbonImpact ?energyImpact ?description -WHERE { - BIND(<${entityUri}> AS ?entity) - - # Find anti-patterns the entity exhibits - ?entity eco:exhibitsPattern ?antiPattern . - ?antiPattern a eco:AntiPattern . - - # Find best practices that address similar concerns - ?practice a eco:BestPractice ; - rdfs:label ?practiceLabel ; - rdfs:comment ?description . - - OPTIONAL { ?practice eco:carbonImpact ?carbonImpact } - OPTIONAL { ?practice eco:energyImpact ?energyImpact } - - FILTER (BOUND(?carbonImpact) || BOUND(?energyImpact)) -} -ORDER BY COALESCE(?carbonImpact, 0) - -# ----------------------------------------------------------------------------- -# QUERY 3: Pareto dominance analysis -# ----------------------------------------------------------------------------- - -# @name: pareto_dominance -# @description: Find entities that Pareto dominate others -SELECT ?dominant ?dominantLabel ?dominated ?dominatedLabel - ?domCarbon ?domEnergy ?subCarbon ?subEnergy -WHERE { - ?dominant a eco:SoftwareEntity ; - rdfs:label ?dominantLabel ; - eco:dominates ?dominated . - ?dominated rdfs:label ?dominatedLabel . - - # Get metrics for dominant entity - ?domAnalysis eco:analyzedBy ?dominant . - ?domAnalysis eco:hasMetric ?domCarbonMetric . - ?domCarbonMetric a eco:CarbonMetric ; - eco:carbonScore ?domCarbon . - ?domAnalysis eco:hasMetric ?domEnergyMetric . - ?domEnergyMetric a eco:EnergyMetric ; - eco:energyScore ?domEnergy . - - # Get metrics for dominated entity - ?subAnalysis eco:analyzedBy ?dominated . - ?subAnalysis eco:hasMetric ?subCarbonMetric . - ?subCarbonMetric a eco:CarbonMetric ; - eco:carbonScore ?subCarbon . - ?subAnalysis eco:hasMetric ?subEnergyMetric . - ?subEnergyMetric a eco:EnergyMetric ; - eco:energyScore ?subEnergy . -} - -# ----------------------------------------------------------------------------- -# QUERY 4: Policy compliance check -# ----------------------------------------------------------------------------- - -# @name: policy_compliance -# @param: policyUri - URI of the policy to check against -SELECT ?entity ?entityLabel ?carbonScore ?energyScore ?compliant -WHERE { - BIND(<${policyUri}> AS ?policy) - - ?policy eco:carbonThreshold ?carbonThreshold ; - eco:energyThreshold ?energyThreshold . - - ?entity a eco:SoftwareEntity ; - rdfs:label ?entityLabel . - - ?analysis eco:analyzedBy ?entity . - ?analysis eco:hasMetric ?carbonMetric . - ?carbonMetric a eco:CarbonMetric ; - eco:carbonScore ?carbonScore . - ?analysis eco:hasMetric ?energyMetric . - ?energyMetric a eco:EnergyMetric ; - eco:energyScore ?energyScore . - - BIND(IF(?carbonScore >= ?carbonThreshold && ?energyScore >= ?energyThreshold, - "true", "false") AS ?compliant) -} -ORDER BY ?compliant ?carbonScore - -# ----------------------------------------------------------------------------- -# QUERY 5: Praxis learning - successful interventions -# ----------------------------------------------------------------------------- - -# @name: successful_interventions -# @description: Find patterns in successful praxis observations -SELECT ?action (COUNT(?obs) AS ?successCount) - (AVG(?carbonImprovement) AS ?avgCarbonImprovement) - (AVG(?energyImprovement) AS ?avgEnergyImprovement) -WHERE { - ?obs a eco:PraxisObservation ; - eco:actionTaken ?action ; - eco:outcome "positive" ; - eco:carbonImprovement ?carbonImprovement ; - eco:energyImprovement ?energyImprovement . -} -GROUP BY ?action -HAVING (COUNT(?obs) > 5) -ORDER BY DESC(?successCount) - -# ----------------------------------------------------------------------------- -# QUERY 6: Technical debt hotspots -# ----------------------------------------------------------------------------- - -# @name: debt_hotspots -# @description: Find entities with high technical debt impacting eco metrics -SELECT ?entity ?entityLabel ?debtPrincipal ?carbonScore ?correlation -WHERE { - ?entity a eco:SoftwareEntity ; - rdfs:label ?entityLabel . - - ?analysis eco:analyzedBy ?entity . - ?analysis eco:hasMetric ?debtMetric . - ?debtMetric a eco:DebtMetric ; - eco:debtPrincipal ?debtPrincipal . - ?analysis eco:hasMetric ?carbonMetric . - ?carbonMetric a eco:CarbonMetric ; - eco:carbonScore ?carbonScore . - - # Calculate correlation indicator (simplified) - BIND(IF(?debtPrincipal > 100 && ?carbonScore < 50, "high", "normal") AS ?correlation) - - FILTER (?debtPrincipal > 50) -} -ORDER BY DESC(?debtPrincipal) - -# ----------------------------------------------------------------------------- -# QUERY 7: Dependency impact analysis -# ----------------------------------------------------------------------------- - -# @name: dependency_eco_impact -# @description: Analyze eco impact propagation through dependencies -SELECT ?source ?sourceLabel ?target ?targetLabel ?depth ?cumulativeCarbon -WHERE { - ?source a eco:SoftwareEntity ; - rdfs:label ?sourceLabel ; - eco:dependsOn+ ?target . - ?target rdfs:label ?targetLabel . - - ?targetAnalysis eco:analyzedBy ?target . - ?targetAnalysis eco:hasMetric ?carbonMetric . - ?carbonMetric a eco:CarbonMetric ; - eco:carbonScore ?targetCarbon . - - # Path depth (simplified - would need property path counting) - BIND(1 AS ?depth) - BIND(?targetCarbon AS ?cumulativeCarbon) -} -ORDER BY ?source DESC(?cumulativeCarbon) - -# ----------------------------------------------------------------------------- -# QUERY 8: Recommendation confidence ranking -# ----------------------------------------------------------------------------- - -# @name: ranked_recommendations -# @param: entityUri - URI of the entity to get recommendations for -SELECT ?recommendation ?action ?reason ?confidence ?expectedImprovement -WHERE { - BIND(<${entityUri}> AS ?entity) - - ?analysis eco:analyzedBy ?entity ; - eco:hasRecommendation ?recommendation . - - ?recommendation eco:action ?action ; - eco:reason ?reason ; - eco:confidence ?confidence . - - OPTIONAL { ?recommendation eco:expectedImprovement ?expectedImprovement } - - FILTER (?confidence > 0.5) -} -ORDER BY DESC(?confidence) -LIMIT 10 - -# ----------------------------------------------------------------------------- -# QUERY 9: Cross-project pattern analysis -# ----------------------------------------------------------------------------- - -# @name: common_patterns -# @description: Find patterns common across multiple projects -SELECT ?pattern ?patternLabel (COUNT(DISTINCT ?project) AS ?projectCount) - (AVG(?healthIndex) AS ?avgHealthIndex) -WHERE { - ?project a eco:Project . - ?entity eco:contains* ?project ; - eco:exhibitsPattern ?pattern . - ?pattern rdfs:label ?patternLabel . - - ?analysis eco:analyzedBy ?entity ; - eco:healthIndex ?healthIndex . -} -GROUP BY ?pattern ?patternLabel -HAVING (COUNT(DISTINCT ?project) > 2) -ORDER BY DESC(?projectCount) - -# ----------------------------------------------------------------------------- -# QUERY 10: Temporal trend analysis -# ----------------------------------------------------------------------------- - -# @name: eco_trends -# @param: entityUri - URI of the entity to analyze -# @param: startDate - Start of time period (xsd:dateTime) -# @param: endDate - End of time period (xsd:dateTime) -SELECT ?date ?carbonScore ?energyScore ?healthIndex -WHERE { - BIND(<${entityUri}> AS ?entity) - - ?analysis eco:analyzedBy ?entity ; - eco:timestamp ?date ; - eco:healthIndex ?healthIndex . - - ?analysis eco:hasMetric ?carbonMetric . - ?carbonMetric a eco:CarbonMetric ; - eco:carbonScore ?carbonScore . - - ?analysis eco:hasMetric ?energyMetric . - ?energyMetric a eco:EnergyMetric ; - eco:energyScore ?energyScore . - - FILTER (?date >= "${startDate}"^^xsd:dateTime && - ?date <= "${endDate}"^^xsd:dateTime) -} -ORDER BY ?date diff --git a/bots/sustainabot/docs/GITHUB_APP_SETUP.md b/bots/sustainabot/docs/GITHUB_APP_SETUP.md deleted file mode 100644 index 2edaaba4..00000000 --- a/bots/sustainabot/docs/GITHUB_APP_SETUP.md +++ /dev/null @@ -1,159 +0,0 @@ - - - - -# GitHub App Setup Guide - -This guide explains how to create and configure a GitHub App for SustainaBot. - -## Prerequisites - -- GitHub account with organization access (or personal account) -- OpenSSL for private key conversion - -## Step 1: Create the GitHub App - -1. Go to **Settings** > **Developer settings** > **GitHub Apps** -2. Click **New GitHub App** -3. Fill in the details: - - | Field | Value | - |-------|-------| - | **GitHub App name** | SustainaBot (or your chosen name) | - | **Homepage URL** | Your repo URL | - | **Webhook URL** | `https://your-domain.com/webhooks/github` | - | **Webhook secret** | Generate a secure random string | - -4. Set **Permissions**: - - | Permission | Access | - |------------|--------| - | Contents | Read | - | Pull requests | Read and write | - | Checks | Read and write (optional) | - | Metadata | Read | - -5. Subscribe to **Events**: - - Pull request - - Push (optional) - -6. Under **Where can this GitHub App be installed?** - - Select "Only on this account" for private use - - Select "Any account" for public marketplace - -7. Click **Create GitHub App** - -## Step 2: Generate and Convert Private Key - -1. On the App settings page, scroll to **Private keys** -2. Click **Generate a private key** -3. Save the `.pem` file securely - -GitHub provides PKCS#1 format keys. Convert to PKCS#8 for Web Crypto API: - -```bash -openssl pkcs8 -topk8 -inform pem -in your-app-key.pem -outform pem -nocrypt -out your-app-key-pkcs8.pem -``` - -## Step 3: Get Your App ID - -On the App settings page, note the **App ID** (a number like `123456`). - -## Step 4: Configure Environment Variables - -Set the following environment variables: - -```bash -# Required for GitHub App authentication -export GITHUB_APP_ID="123456" -export GITHUB_PRIVATE_KEY="$(cat your-app-key-pkcs8.pem)" - -# Required for webhook signature verification -export GITHUB_WEBHOOK_SECRET="your-webhook-secret" - -# Optional -export BOT_MODE="advisor" # advisor, consultant, or regulator -export PORT="3000" -``` - -For production, use a secrets manager. For Kubernetes: - -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: sustainabot -type: Opaque -stringData: - GITHUB_APP_ID: "123456" - GITHUB_PRIVATE_KEY: | - -----BEGIN PRIVATE KEY----- - ... - -----END PRIVATE KEY----- - GITHUB_WEBHOOK_SECRET: "your-webhook-secret" -``` - -## Step 5: Install the App - -1. On the App settings page, click **Install App** in the sidebar -2. Select the account/organization -3. Choose repositories: - - All repositories, or - - Only select repositories -4. Click **Install** - -## Step 6: Verify Installation - -After installing, check the server logs for: - -``` -{"level":"info","message":"Posted PR comment","data":{"pr":1,"commentId":12345}} -``` - -Create a test PR to verify the bot posts analysis comments. - -## Troubleshooting - -### "GitHub App credentials not configured" - -- Verify `GITHUB_APP_ID` and `GITHUB_PRIVATE_KEY` are set -- Check private key is in PKCS#8 format (starts with `-----BEGIN PRIVATE KEY-----`) - -### "No installation ID in payload" - -- Ensure the app is installed on the repository -- Check webhook is reaching the server - -### "GitHub API error 401" - -- Verify App ID is correct -- Check private key matches the App -- Ensure private key is not expired - -### JWT Signature Issues - -If you see signing errors: - -```bash -# Verify key format -openssl rsa -in your-key.pem -check - -# Re-convert to PKCS#8 -openssl pkcs8 -topk8 -inform pem -in your-key.pem -outform pem -nocrypt -out your-key-pkcs8.pem -``` - -## API Rate Limits - -GitHub App installation tokens have higher rate limits than personal access tokens: - -- 5,000 requests per hour per installation -- Rate limit resets hourly - -The bot caches installation tokens (valid for 1 hour) to minimize token exchange requests. - -## Security Notes - -- Never commit private keys to version control -- Use environment variables or secrets managers -- Rotate webhook secrets periodically -- Monitor App audit logs for unusual activity diff --git a/bots/sustainabot/examples/sustainabot-ci.yml b/bots/sustainabot/examples/sustainabot-ci.yml deleted file mode 100644 index 6ff8f3e3..00000000 --- a/bots/sustainabot/examples/sustainabot-ci.yml +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Example GitHub Actions workflow for sustainabot SARIF integration -# -# This workflow runs sustainabot on your codebase and uploads the SARIF -# results to GitHub's Security tab, giving you inline PR annotations. -# -# Usage: Copy this file to .github/workflows/sustainabot.yml in your repo. - -name: Sustainability Analysis - -on: - push: - branches: [main] - pull_request: - branches: [main] - -permissions: - security-events: write - contents: read - -jobs: - sustainabot: - name: Eco & Economic Analysis - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - - name: Install sustainabot - run: cargo install --path . --locked - # Or, if published to crates.io: - # run: cargo install sustainabot - - - name: Run sustainability analysis - run: | - sustainabot report ./src \ - --format sarif \ - --output results.sarif \ - --eco-threshold 50 - - - name: Upload SARIF to GitHub Security - if: always() - uses: github/codeql-action/upload-sarif@6624720a57d4c312633c7b953db2f2da5bcb4c3a # v3 - with: - sarif_file: results.sarif - category: sustainabot - - # Optional: Run with policy evaluation - - name: Run policy evaluation - if: hashFiles('policies/') != '' - run: | - sustainabot check ./src \ - --format text \ - --policy-dir policies/ \ - --eco-threshold 50 diff --git a/bots/sustainabot/fuzz/Cargo.toml b/bots/sustainabot/fuzz/Cargo.toml deleted file mode 100644 index 46aef008..00000000 --- a/bots/sustainabot/fuzz/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "fuzz" -version = "0.0.0" -publish = false -edition = "2021" - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.4" - -[dependencies.sustainabot] -path = ".." - -[[bin]] -name = "fuzz_main" -path = "fuzz_targets/fuzz_main.rs" -test = false -doc = false diff --git a/bots/sustainabot/fuzz/fuzz_targets/fuzz_main.rs b/bots/sustainabot/fuzz/fuzz_targets/fuzz_main.rs deleted file mode 100644 index 1f71ba3b..00000000 --- a/bots/sustainabot/fuzz/fuzz_targets/fuzz_main.rs +++ /dev/null @@ -1,22 +0,0 @@ -#![no_main] -use libfuzzer_sys::fuzz_target; - -fuzz_target!(|data: &[u8]| { - // Generic fuzzing for memory safety and crash detection - if data.is_empty() || data.len() > 100000 { - return; - } - - // Test UTF-8 validity - if let Ok(text) = std::str::from_utf8(data) { - // Test string operations - let _ = text.to_lowercase(); - let _ = text.chars().count(); - let _ = text.split_whitespace().collect::>(); - } - - // Test binary data handling - if data.len() >= 8 { - let _chunk = &data[..8]; - } -}); diff --git a/bots/sustainabot/guix/channels.scm b/bots/sustainabot/guix/channels.scm deleted file mode 100644 index c09ebfcf..00000000 --- a/bots/sustainabot/guix/channels.scm +++ /dev/null @@ -1,42 +0,0 @@ -;;; SPDX-License-Identifier: MPL-2.0 -;;; SPDX-FileCopyrightText: 2024-2025 hyperpolymath -;;; -;;; Oikos Bot Guix Channel Configuration -;;; -;;; This defines the Guix channels needed for the oikos-bot project. -;;; Use with: guix pull -C channels.scm - -(list - ;; Main Guix channel - (channel - (name 'guix) - (url "https://git.savannah.gnu.org/git/guix.git") - (branch "master") - (introduction - (make-channel-introduction - "9edb3f66fd807b096b48283debdcddccfea34bad" - (openpgp-fingerprint - "BBB0 2DDF 2CEA F6A8 0D1D E643 A2A0 6DF2 A33A 54FA")))) - - ;; Nonguix for proprietary drivers if needed - (channel - (name 'nonguix) - (url "https://gitlab.com/nonguix/nonguix") - (branch "master") - (introduction - (make-channel-introduction - "897c1a470da759236cc11798f4e0a5f7d4d59fbc" - (openpgp-fingerprint - "2A39 3FFF 68F4 EF7A 3D29 12AF 6F51 20A0 22FB B2D5")))) - - ;; Guix-HPC for high-performance computing packages - (channel - (name 'guix-hpc) - (url "https://gitlab.inria.fr/guix-hpc/guix-hpc.git") - (branch "master")) - - ;; Oikos Bot custom channel - (channel - (name 'oikos-bot) - (url "https://github.com/hyperpolymath/oikos-bot-guix") - (branch "main"))) diff --git a/bots/sustainabot/guix/manifest.scm b/bots/sustainabot/guix/manifest.scm deleted file mode 100644 index 42598711..00000000 --- a/bots/sustainabot/guix/manifest.scm +++ /dev/null @@ -1,92 +0,0 @@ -;;; SPDX-License-Identifier: MPL-2.0 -;;; SPDX-FileCopyrightText: 2024-2025 hyperpolymath -;;; -;;; Oikos Bot Guix Manifest -;;; -;;; Development environment manifest for oikos-bot. -;;; Use with: guix shell -m manifest.scm - -(specifications->manifest - '(;; ======================================== - ;; Core Languages - ;; ======================================== - - ;; Haskell (for code analyzer) - "ghc" - "cabal-install" - "hlint" - "haskell-language-server" - - ;; OCaml (for documentation analyzer) - "ocaml" - "dune" - "opam" - "ocaml-merlin" - "ocaml-ocp-indent" - "ocamlformat" - - ;; ReScript (compiles from source, needs node for build) - "node" ; Only for rescript compiler, not runtime - - ;; Deno runtime - "deno" - - ;; Python (for policy engine) - "python" - "python-pip" - "python-virtualenv" - - ;; Rust (for orchestrator) - "rust" - "rust-analyzer" - - ;; ======================================== - ;; Databases - ;; ======================================== - - ;; ArangoDB client tools - "arangodb" - - ;; Virtuoso (SPARQL) - "virtuoso-ose" - - ;; ======================================== - ;; Logic Programming - ;; ======================================== - - ;; Datalog (Souffle) - "souffle" - - ;; Prolog (for DeepProbLog base) - "swi-prolog" - - ;; ======================================== - ;; Build Tools - ;; ======================================== - - "git" - "make" - "gcc-toolchain" - "pkg-config" - "openssl" - "zlib" - - ;; ======================================== - ;; Container Tools (Vörðr preferred) - ;; ======================================== - - "podman" - "buildah" - "skopeo" - "cni-plugins" - - ;; ======================================== - ;; Development Utilities - ;; ======================================== - - "jq" - "ripgrep" - "fd" - "bat" - "direnv" - "watchexec")) diff --git a/bots/sustainabot/guix/oikos.scm b/bots/sustainabot/guix/oikos.scm deleted file mode 100644 index 61547fa2..00000000 --- a/bots/sustainabot/guix/oikos.scm +++ /dev/null @@ -1,149 +0,0 @@ -;;; SPDX-License-Identifier: MPL-2.0 -;;; SPDX-FileCopyrightText: 2024-2025 hyperpolymath -;;; -;;; Oikos Bot Guix Package Definition -;;; -;;; This defines the oikos-bot package for Guix. - -(define-module (oikos) - #:use-module (guix packages) - #:use-module (guix git-download) - #:use-module (guix build-system gnu) - #:use-module (guix build-system haskell) - #:use-module (guix build-system dune) - #:use-module (guix build-system python) - #:use-module ((guix licenses) #:prefix license:) - #:use-module (gnu packages haskell) - #:use-module (gnu packages haskell-xyz) - #:use-module (gnu packages ocaml) - #:use-module (gnu packages python) - #:use-module (gnu packages python-xyz) - #:use-module (gnu packages databases) - #:use-module (gnu packages logic)) - -;; Haskell Code Analyzer -(define-public oikos-analyzer-haskell - (package - (name "oikos-analyzer-haskell") - (version "0.1.0") - (source - (origin - (method git-fetch) - (uri (git-reference - (url "https://github.com/hyperpolymath/oikos-bot") - (commit (string-append "v" version)))) - (file-name (git-file-name name version)) - (sha256 - (base32 "0000000000000000000000000000000000000000000000000000")))) - (build-system haskell-build-system) - (inputs - (list ghc-aeson - ghc-text - ghc-containers - ghc-vector - ghc-mtl - ghc-optparse-applicative - ghc-megaparsec)) - (arguments - '(#:cabal-file "analyzers/code-haskell/oikos-analyzer.cabal")) - (synopsis "Haskell code analyzer for Oikos Bot") - (description - "Analyzes code for carbon intensity, energy efficiency, - Pareto optimality, and software quality metrics.") - (home-page "https://github.com/hyperpolymath/oikos-bot") - (license license:agpl3+))) - -;; OCaml Documentation Analyzer -(define-public oikos-analyzer-ocaml - (package - (name "oikos-analyzer-ocaml") - (version "0.1.0") - (source - (origin - (method git-fetch) - (uri (git-reference - (url "https://github.com/hyperpolymath/oikos-bot") - (commit (string-append "v" version)))) - (file-name (git-file-name name version)) - (sha256 - (base32 "0000000000000000000000000000000000000000000000000000")))) - (build-system dune-build-system) - (inputs - (list ocaml-yojson - ocaml-ppx-deriving - ocaml-re - ocaml-omd - ocaml-cmdliner)) - (arguments - '(#:source-subdir "analyzers/docs-ocaml")) - (synopsis "OCaml documentation analyzer for Oikos Bot") - (description - "Analyzes documentation for completeness, consistency, - and alignment with ecological/economic principles.") - (home-page "https://github.com/hyperpolymath/oikos-bot") - (license license:agpl3+))) - -;; Python Policy Engine -(define-public oikos-policy-engine - (package - (name "oikos-policy-engine") - (version "0.1.0") - (source - (origin - (method git-fetch) - (uri (git-reference - (url "https://github.com/hyperpolymath/oikos-bot") - (commit (string-append "v" version)))) - (file-name (git-file-name name version)) - (sha256 - (base32 "0000000000000000000000000000000000000000000000000000")))) - (build-system python-build-system) - (inputs - (list python-numpy - python-torch - souffle - swi-prolog)) - (arguments - '(#:phases - (modify-phases %standard-phases - (add-after 'unpack 'chdir - (lambda _ (chdir "policy-engine/python")))))) - (synopsis "Policy engine for Oikos Bot") - (description - "Hybrid Datalog + DeepProbLog policy engine for - deterministic and probabilistic reasoning.") - (home-page "https://github.com/hyperpolymath/oikos-bot") - (license license:agpl3+))) - -;; Combined oikos-bot package -(define-public oikos-bot - (package - (name "oikos-bot") - (version "0.1.0") - (source #f) - (build-system gnu-build-system) - (inputs - (list oikos-analyzer-haskell - oikos-analyzer-ocaml - oikos-policy-engine - deno - arangodb - virtuoso-ose)) - (arguments - '(#:phases - (modify-phases %standard-phases - (delete 'configure) - (delete 'build) - (replace 'install - (lambda* (#:key outputs #:allow-other-keys) - (let ((out (assoc-ref outputs "out"))) - ;; Create wrapper scripts - (mkdir-p (string-append out "/bin")) - #t)))))) - (synopsis "Ecological & Economic Code Analysis Platform") - (description - "Oikos Bot analyzes code for ecological soundness and economic - efficiency using Pareto optimality and allocative efficiency - as normative criteria.") - (home-page "https://github.com/hyperpolymath/oikos-bot") - (license license:agpl3+))) diff --git a/bots/sustainabot/hooks/validate-codeql.sh b/bots/sustainabot/hooks/validate-codeql.sh deleted file mode 100755 index 15b52c3d..00000000 --- a/bots/sustainabot/hooks/validate-codeql.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# Pre-commit hook: Validate CodeQL language matrix matches repo -set -euo pipefail - -CODEQL_FILE=".github/workflows/codeql.yml" -[ -f "$CODEQL_FILE" ] || exit 0 - -# Detect languages in repo -HAS_JS=$(find . -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" 2>/dev/null | grep -v node_modules | head -1) -HAS_PY=$(find . -name "*.py" 2>/dev/null | grep -v __pycache__ | head -1) -HAS_GO=$(find . -name "*.go" 2>/dev/null | head -1) -HAS_RS=$(find . -name "*.rs" 2>/dev/null | head -1) - -# Check if matrix includes unsupported languages -if grep -q "language:.*python" "$CODEQL_FILE" && [ -z "$HAS_PY" ]; then - echo "WARNING: CodeQL configured for Python but no .py files found" -fi -if grep -q "language:.*go" "$CODEQL_FILE" && [ -z "$HAS_GO" ]; then - echo "WARNING: CodeQL configured for Go but no .go files found" -fi -if grep -q "language:.*javascript" "$CODEQL_FILE" && [ -z "$HAS_JS" ]; then - echo "WARNING: CodeQL configured for JavaScript but no JS/TS files found" -fi - -# Rust/OCaml are not supported - should use 'actions' only -if [ -n "$HAS_RS" ]; then - if grep -q "language:.*rust" "$CODEQL_FILE"; then - echo "ERROR: CodeQL does not support Rust - use ['actions'] instead" - exit 1 - fi -fi - -exit 0 diff --git a/bots/sustainabot/hooks/validate-permissions.sh b/bots/sustainabot/hooks/validate-permissions.sh deleted file mode 100755 index 1999b018..00000000 --- a/bots/sustainabot/hooks/validate-permissions.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# Pre-commit hook: Validate workflow permissions declarations -set -euo pipefail -ERRORS=0 -for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$workflow" ] || continue - if ! grep -qE '^permissions:' "$workflow"; then - echo "ERROR: Missing top-level permissions in $workflow" - ERRORS=$((ERRORS + 1)) - fi -done -[ $ERRORS -gt 0 ] && exit 1 -exit 0 diff --git a/bots/sustainabot/hooks/validate-sha-pins.sh b/bots/sustainabot/hooks/validate-sha-pins.sh deleted file mode 100755 index 697092b5..00000000 --- a/bots/sustainabot/hooks/validate-sha-pins.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# Pre-commit hook: Validate GitHub Actions are SHA-pinned - -set -euo pipefail - -ERRORS=0 - -for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$workflow" ] || continue - - # Find uses: lines that aren't SHA-pinned - while IFS= read -r line; do - if [[ "$line" =~ uses:.*@ ]]; then - # Check if it has a SHA (40 hex chars) - if ! echo "$line" | grep -qE '@[a-f0-9]{40}'; then - echo "ERROR: Unpinned action in $workflow" - echo " $line" - echo " Actions must use SHA pins: uses: action/name@SHA # version" - ERRORS=$((ERRORS + 1)) - fi - fi - done < "$workflow" -done - -if [ $ERRORS -gt 0 ]; then - echo "" - echo "Found $ERRORS unpinned actions. Please SHA-pin all GitHub Actions." - echo "Use: gh api repos/OWNER/REPO/git/matching-refs/tags/VERSION to find SHAs" - exit 1 -fi - -exit 0 diff --git a/bots/sustainabot/hooks/validate-spdx.sh b/bots/sustainabot/hooks/validate-spdx.sh deleted file mode 100755 index cc81cf18..00000000 --- a/bots/sustainabot/hooks/validate-spdx.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# Pre-commit hook: Validate SPDX headers in workflow files - -set -euo pipefail - -ERRORS=0 -SPDX_PATTERN="^# SPDX-License-Identifier:" - -for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$workflow" ] || continue - - first_line=$(head -n1 "$workflow") - if ! echo "$first_line" | grep -qE "$SPDX_PATTERN"; then - echo "ERROR: Missing SPDX header in $workflow" - echo " First line should be: # SPDX-License-Identifier: MPL-2.0" - ERRORS=$((ERRORS + 1)) - fi -done - -if [ $ERRORS -gt 0 ]; then - exit 1 -fi - -exit 0 diff --git a/bots/sustainabot/justfile b/bots/sustainabot/justfile deleted file mode 100644 index 5eec7b9b..00000000 --- a/bots/sustainabot/justfile +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Justfile - hyperpolymath standard task runner - -default: - @just --list - -# Build the project -build: - @echo "Building..." - -# Run tests -test: - @echo "Testing..." - -# Run lints -lint: - @echo "Linting..." - -# Clean build artifacts -clean: - @echo "Cleaning..." - -# Format code -fmt: - @echo "Formatting..." - -# Run all checks -check: lint test - -# Prepare a release -release VERSION: - @echo "Releasing {{VERSION}}..." - -# Trigger automated checking - -github-scorecard: - @echo "Run manually: https://github.com/ossf/scorecard" - - diff --git a/bots/sustainabot/policies/carbon_budget.ecl b/bots/sustainabot/policies/carbon_budget.ecl deleted file mode 100644 index d54bed98..00000000 --- a/bots/sustainabot/policies/carbon_budget.ecl +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SustainaBot Policy: Carbon Budget -// -// Per-function and per-project carbon budgets. -// Enforces sustainable computation practices. - -def exceeds_function_carbon(carbon_grams: Float) -> Bool - @requires: energy < 0.1J -{ - carbon_grams > 0.5 -} - -def exceeds_project_carbon(total_carbon_grams: Float) -> Bool - @requires: energy < 0.1J -{ - total_carbon_grams > 5.0 -} - -def evaluate_carbon_budget(function_carbon: Float, total_carbon: Float) -> Bool - @requires: energy < 1J, carbon < 0.001gCO2e -{ - exceeds_function_carbon(function_carbon) || exceeds_project_carbon(total_carbon) -} diff --git a/bots/sustainabot/policies/energy_threshold.ecl b/bots/sustainabot/policies/energy_threshold.ecl deleted file mode 100644 index d3601f92..00000000 --- a/bots/sustainabot/policies/energy_threshold.ecl +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SustainaBot Policy: Energy Threshold Check -// -// This policy is written in ECLEXIA - proving dogfooding works! -// The policy engine itself has provable resource bounds. - -// Check if a function uses too much energy -def exceeds_energy_threshold(energy_joules: Float) -> Bool - @requires: energy < 0.1J, carbon < 0.001gCO2e // This policy is CHEAP to run - @optimize: minimize latency -{ - energy_joules > 50.0 -} - -// Check if carbon footprint is too high -def exceeds_carbon_threshold(carbon_grams: Float) -> Bool - @requires: energy < 0.1J -{ - carbon_grams > 5.0 -} - -// Main policy evaluation function -// Returns true if the code should trigger a warning -adaptive def should_warn(energy: Float, carbon: Float) -> Bool - @requires: energy < 1J, latency < 5ms - @optimize: minimize energy, minimize latency -{ - @solution "fast_check": - @when: energy < 10.0 && carbon < 1.0 - @provides: energy: 0.05J, latency: 1ms - { - false // Obviously fine, skip detailed check - } - - @solution "detailed_check": - @provides: energy: 0.2J, latency: 3ms - { - exceeds_energy_threshold(energy) || exceeds_carbon_threshold(carbon) - } -} - -// Example: This policy used < 1J to decide if your code uses > 50J -// Meta-level efficiency: The analyzer is more efficient than what it analyzes! diff --git a/bots/sustainabot/policies/memory_efficiency.ecl b/bots/sustainabot/policies/memory_efficiency.ecl deleted file mode 100644 index f345f60c..00000000 --- a/bots/sustainabot/policies/memory_efficiency.ecl +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SustainaBot Policy: Memory Efficiency -// -// Allocation pattern rules to catch wasteful memory usage. - -def exceeds_allocation_limit(bytes: Float) -> Bool - @requires: energy < 0.05J -{ - bytes > 1048576.0 // 1MB -} - -def high_allocation_count(count: Float) -> Bool - @requires: energy < 0.05J -{ - count > 100.0 -} - -def evaluate_memory(allocation_bytes: Float, allocation_count: Float) -> Bool - @requires: energy < 0.5J -{ - exceeds_allocation_limit(allocation_bytes) || high_allocation_count(allocation_count) -} diff --git a/bots/sustainabot/policies/security_sustainability.ecl b/bots/sustainabot/policies/security_sustainability.ecl deleted file mode 100644 index 35bb9189..00000000 --- a/bots/sustainabot/policies/security_sustainability.ecl +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SustainaBot Policy: Security-Sustainability Correlation -// -// Correlates panic-attack findings with eco scores. -// Functions that are BOTH high-energy AND have security weak points -// get elevated severity. - -def is_high_energy(energy_joules: Float) -> Bool - @requires: energy < 0.05J -{ - energy_joules > 50.0 -} - -def has_security_issues(weak_point_count: Float) -> Bool - @requires: energy < 0.05J -{ - weak_point_count > 0.0 -} - -def correlate(energy: Float, weak_points: Float) -> Bool - @requires: energy < 0.5J -{ - is_high_energy(energy) && has_security_issues(weak_points) -} diff --git a/bots/sustainabot/policy-engine/datalog/eco_rules.dl b/bots/sustainabot/policy-engine/datalog/eco_rules.dl deleted file mode 100644 index 04f0d540..00000000 --- a/bots/sustainabot/policy-engine/datalog/eco_rules.dl +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2024-2025 hyperpolymath -// -// Oikos Bot Policy Engine - Datalog Rules -// ======================================= -// Deterministic rules for ecological and economic code analysis -// These rules form the "certain" knowledge base, complementing -// DeepProbLog's probabilistic inference. - -// ============================================================================= -// DOMAIN DECLARATIONS -// ============================================================================= - -// Code entities -.decl file(id: symbol, path: symbol, language: symbol) -.decl function(id: symbol, name: symbol, file_id: symbol) -.decl module(id: symbol, name: symbol, file_id: symbol) -.decl dependency(from_id: symbol, to_id: symbol, dep_type: symbol) - -// Metrics (normalized 0-100) -.decl carbon_score(entity_id: symbol, score: float) -.decl energy_score(entity_id: symbol, score: float) -.decl complexity_score(entity_id: symbol, score: float) -.decl coverage_score(entity_id: symbol, score: float) -.decl debt_score(entity_id: symbol, score: float) - -// Patterns detected -.decl pattern(entity_id: symbol, pattern_type: symbol, severity: symbol) -.decl hotspot(entity_id: symbol, metric: symbol, value: float) - -// ============================================================================= -// ECOLOGICAL RULES -// ============================================================================= - -// A component is eco-friendly if it has good carbon and energy scores -.decl eco_friendly(entity_id: symbol) -eco_friendly(E) :- - carbon_score(E, C), C >= 70, - energy_score(E, En), En >= 70. - -// A component has high carbon footprint -.decl high_carbon(entity_id: symbol) -high_carbon(E) :- carbon_score(E, C), C < 40. - -// A component is energy inefficient -.decl energy_inefficient(entity_id: symbol) -energy_inefficient(E) :- energy_score(E, En), En < 40. - -// Identify eco hotspots (components needing immediate attention) -.decl eco_hotspot(entity_id: symbol, reason: symbol) -eco_hotspot(E, "high_carbon") :- high_carbon(E). -eco_hotspot(E, "energy_inefficient") :- energy_inefficient(E). -eco_hotspot(E, "busy_waiting") :- pattern(E, "busy_waiting", _). -eco_hotspot(E, "inefficient_loop") :- pattern(E, "inefficient_loop", "high"). - -// ============================================================================= -// ECONOMIC RULES (Pareto Optimality) -// ============================================================================= - -// Define Pareto dominance -// Entity A dominates Entity B if A is at least as good in all metrics -// and strictly better in at least one -.decl dominates(entity_a: symbol, entity_b: symbol) -dominates(A, B) :- - carbon_score(A, CA), carbon_score(B, CB), CA >= CB, - energy_score(A, EA), energy_score(B, EB), EA >= EB, - complexity_score(A, XA), complexity_score(B, XB), XA >= XB, - coverage_score(A, TA), coverage_score(B, TB), TA >= TB, - (CA > CB; EA > EB; XA > XB; TA > TB). // At least one strictly better - -// Entity is Pareto optimal if not dominated by any other entity -.decl pareto_optimal(entity_id: symbol) -pareto_optimal(E) :- - file(E, _, _), - !dominated(E). - -.decl dominated(entity_id: symbol) -dominated(B) :- dominates(_, B). - -// Identify Pareto improvement opportunities -.decl pareto_improvement_possible(entity_id: symbol, metric: symbol) -pareto_improvement_possible(E, "carbon") :- - dominated(E), - carbon_score(E, C), C < 70. - -pareto_improvement_possible(E, "energy") :- - dominated(E), - energy_score(E, En), En < 70. - -// ============================================================================= -// ALLOCATIVE EFFICIENCY RULES -// ============================================================================= - -// Resource allocation is efficient if no waste is detected -.decl allocation_efficient(entity_id: symbol) -allocation_efficient(E) :- - file(E, _, _), - !allocation_waste(E, _). - -// Detect allocation waste -.decl allocation_waste(entity_id: symbol, waste_type: symbol) -allocation_waste(E, "unused_dependency") :- - dependency(E, D, _), - !dependency_used(E, D). - -allocation_waste(E, "over_allocation") :- - pattern(E, "over_allocation", _). - -allocation_waste(E, "premature_optimization") :- - pattern(E, "premature_optimization", _), - complexity_score(E, X), X < 50. - -.decl dependency_used(from_id: symbol, to_id: symbol) -.input dependency_used - -// ============================================================================= -// TECHNICAL DEBT RULES -// ============================================================================= - -// High technical debt components -.decl high_debt(entity_id: symbol) -high_debt(E) :- debt_score(E, D), D < 40. - -// Debt is economically justified if it enables faster delivery -// without compromising eco metrics -.decl justified_debt(entity_id: symbol) -justified_debt(E) :- - high_debt(E), - eco_friendly(E), - pattern(E, "intentional_simplification", _). - -// Debt needs attention -.decl debt_attention_needed(entity_id: symbol, urgency: symbol) -debt_attention_needed(E, "critical") :- - high_debt(E), - high_carbon(E). - -debt_attention_needed(E, "high") :- - high_debt(E), - !justified_debt(E), - !eco_friendly(E). - -debt_attention_needed(E, "medium") :- - high_debt(E), - !justified_debt(E), - eco_friendly(E). - -// ============================================================================= -// QUALITY RULES -// ============================================================================= - -// High complexity components -.decl high_complexity(entity_id: symbol) -high_complexity(E) :- complexity_score(E, X), X < 40. - -// Low test coverage -.decl low_coverage(entity_id: symbol) -low_coverage(E) :- coverage_score(E, T), T < 60. - -// Quality hotspot (multiple quality issues) -.decl quality_hotspot(entity_id: symbol) -quality_hotspot(E) :- - high_complexity(E), - low_coverage(E). - -// ============================================================================= -// REFACTORING RECOMMENDATIONS -// ============================================================================= - -// Component needs refactoring based on multiple criteria -.decl needs_refactor(entity_id: symbol, reason: symbol, priority: symbol) - -needs_refactor(E, "eco_improvement", "high") :- - eco_hotspot(E, _), - dominated(E). - -needs_refactor(E, "debt_reduction", "high") :- - debt_attention_needed(E, "critical"). - -needs_refactor(E, "quality_improvement", "medium") :- - quality_hotspot(E), - !eco_hotspot(E, _). - -needs_refactor(E, "pareto_optimization", "medium") :- - dominated(E), - pareto_improvement_possible(E, _), - !eco_hotspot(E, _). - -// ============================================================================= -// POLICY COMPLIANCE -// ============================================================================= - -// Define compliance levels -.decl compliant(entity_id: symbol, policy: symbol) - -compliant(E, "eco_minimum") :- - carbon_score(E, C), C >= 50, - energy_score(E, En), En >= 50. - -compliant(E, "eco_standard") :- - carbon_score(E, C), C >= 70, - energy_score(E, En), En >= 70. - -compliant(E, "eco_excellence") :- - eco_friendly(E), - pareto_optimal(E). - -// Policy violations -.decl policy_violation(entity_id: symbol, policy: symbol, severity: symbol) - -policy_violation(E, "eco_minimum", "blocking") :- - file(E, _, _), - !compliant(E, "eco_minimum"). - -policy_violation(E, "eco_standard", "warning") :- - compliant(E, "eco_minimum"), - !compliant(E, "eco_standard"). - -// ============================================================================= -// OUTPUT RELATIONS -// ============================================================================= - -.output eco_friendly -.output eco_hotspot -.output pareto_optimal -.output dominated -.output pareto_improvement_possible -.output allocation_efficient -.output allocation_waste -.output debt_attention_needed -.output needs_refactor -.output compliant -.output policy_violation diff --git a/bots/sustainabot/policy-engine/deepproblog/eco_problog.pl b/bots/sustainabot/policy-engine/deepproblog/eco_problog.pl deleted file mode 100644 index 0464f30c..00000000 --- a/bots/sustainabot/policy-engine/deepproblog/eco_problog.pl +++ /dev/null @@ -1,200 +0,0 @@ -%% SPDX-License-Identifier: MPL-2.0 -%% SPDX-FileCopyrightText: 2024-2025 hyperpolymath -%% -%% Oikos Bot Policy Engine - DeepProbLog Rules -%% =========================================== -%% Probabilistic logic programming rules that learn from practice. -%% These rules complement the deterministic Datalog rules by handling -%% uncertainty and learning patterns from observed outcomes. -%% -%% Reference: https://github.com/ML-KULeuven/deepproblog - -%% ============================================================================= -%% NEURAL PREDICATES -%% ============================================================================= - -%% Neural network for estimating carbon intensity from code features -%% Input: Code feature vector (complexity, loop depth, allocations, etc.) -%% Output: Probability of high carbon intensity -nn(carbon_estimator, [CodeFeatures], CarbonProb) :: high_carbon_prob(Code, CarbonProb) :- - code_features(Code, CodeFeatures). - -%% Neural network for energy pattern classification -nn(energy_classifier, [CodeFeatures], EnergyClass) :: energy_pattern(Code, EnergyClass) :- - code_features(Code, CodeFeatures). - -%% Neural network for predicting refactoring success -nn(refactor_predictor, [CodeFeatures, RefactorType], SuccessProb) :: - refactor_success_prob(Code, RefactorType, SuccessProb) :- - code_features(Code, CodeFeatures). - -%% Neural network for technical debt estimation -nn(debt_estimator, [CodeFeatures, HistoricalChanges], DebtScore) :: - predicted_debt(Code, DebtScore) :- - code_features(Code, CodeFeatures), - change_history(Code, HistoricalChanges). - -%% ============================================================================= -%% PROBABILISTIC ECO RULES -%% ============================================================================= - -%% Probabilistic eco-friendliness based on learned patterns -%% P(eco_friendly | carbon_prob, energy_pattern) -P :: eco_friendly_prob(Code) :- - high_carbon_prob(Code, CarbonP), - energy_pattern(Code, EnergyClass), - P is (1 - CarbonP) * energy_efficiency_factor(EnergyClass). - -%% Energy efficiency factors (can be learned) -energy_efficiency_factor(efficient) := 0.9. -energy_efficiency_factor(moderate) := 0.6. -energy_efficiency_factor(inefficient) := 0.3. -energy_efficiency_factor(unknown) := 0.5. - -%% ============================================================================= -%% PROBABILISTIC PARETO RULES -%% ============================================================================= - -%% Probability that a change will improve Pareto position -%% Learned from historical refactoring outcomes -P :: pareto_improvement_likely(Code, ChangeType) :- - dominated(Code), - refactor_success_prob(Code, ChangeType, P), - P > 0.6. - -%% Probability of maintaining Pareto optimality after change -P :: maintains_pareto(Code, ChangeType) :- - pareto_optimal(Code), - refactor_success_prob(Code, ChangeType, BaseP), - % Penalty for changes to already optimal code - P is BaseP * 0.8. - -%% ============================================================================= -%% LEARNING FROM PRACTICE (Praxis Loop) -%% ============================================================================= - -%% Evidence from past refactoring outcomes -%% These facts are updated as we observe real outcomes -observed_improvement(code_id_1, carbon_reduction, 0.15). -observed_improvement(code_id_1, energy_reduction, 0.20). -observed_no_improvement(code_id_2, complexity_reduction). - -%% Learn policy effectiveness -%% P(policy_effective | policy, outcomes) -P :: policy_effective(Policy) :- - policy_application(Policy, Code, Outcome), - outcome_positive(Outcome), - policy_success_rate(Policy, P). - -%% Update success rates based on observations (simplified) -policy_success_rate(Policy, Rate) :- - findall(1, (policy_application(Policy, _, positive)), Successes), - findall(1, (policy_application(Policy, _, _)), Total), - length(Successes, S), - length(Total, T), - T > 0, - Rate is S / T. - -%% ============================================================================= -%% RECOMMENDATION CONFIDENCE -%% ============================================================================= - -%% Confidence in refactoring recommendations -%% Combines rule-based reasoning with learned patterns -confidence(recommendation(Code, Action, Reason), Confidence) :- - base_confidence(Reason, BaseConf), - refactor_success_prob(Code, Action, SuccessProb), - Confidence is BaseConf * SuccessProb. - -base_confidence(eco_improvement, 0.8). -base_confidence(debt_reduction, 0.7). -base_confidence(quality_improvement, 0.75). -base_confidence(pareto_optimization, 0.65). - -%% ============================================================================= -%% ADAPTIVE THRESHOLDS -%% ============================================================================= - -%% Thresholds that adapt based on project context and history -%% Start with defaults, adjust based on outcomes - -adaptive_threshold(eco_minimum, carbon, Threshold) :- - project_baseline(carbon, Baseline), - learned_improvement_rate(carbon, Rate), - % Gradually increase expectations - Threshold is max(50, Baseline * (1 + Rate)). - -adaptive_threshold(eco_minimum, energy, Threshold) :- - project_baseline(energy, Baseline), - learned_improvement_rate(energy, Rate), - Threshold is max(50, Baseline * (1 + Rate)). - -%% Default thresholds when no history -adaptive_threshold(eco_minimum, carbon, 50) :- \+ project_baseline(carbon, _). -adaptive_threshold(eco_minimum, energy, 50) :- \+ project_baseline(energy, _). - -%% ============================================================================= -%% KNOWLEDGE GRAPH INTEGRATION -%% ============================================================================= - -%% Query patterns for Virtuoso (RDF) integration -sparql_query(eco_best_practices, " - PREFIX eco: - PREFIX sw: - - SELECT ?practice ?description ?impact - WHERE { - ?practice a eco:BestPractice ; - eco:description ?description ; - eco:carbonImpact ?impact . - FILTER (?impact > 0.1) - } - ORDER BY DESC(?impact) -"). - -%% Query patterns for ArangoDB (graph) integration -aql_query(dependency_impact, " - FOR v, e, p IN 1..3 OUTBOUND @startNode GRAPH 'code_dependencies' - LET impact = SUM(p.vertices[*].carbon_score) - RETURN { path: p, total_impact: impact } -"). - -%% ============================================================================= -%% COACHING AND SUGGESTIONS -%% ============================================================================= - -%% Generate coaching suggestions with confidence levels -P :: coaching_suggestion(Code, Suggestion, Priority) :- - needs_attention(Code, Reason), - suggestion_for(Reason, Suggestion), - priority_for(Reason, Priority), - confidence(recommendation(Code, Suggestion, Reason), P), - P > 0.5. - -suggestion_for(high_carbon, "Consider memoization for repeated computations"). -suggestion_for(high_carbon, "Evaluate algorithm complexity - can you reduce from O(n^2)?"). -suggestion_for(energy_inefficient, "Replace busy-waiting with event-driven patterns"). -suggestion_for(energy_inefficient, "Use connection pooling instead of creating new connections"). -suggestion_for(high_debt, "Extract duplicated logic into shared functions"). -suggestion_for(low_coverage, "Add tests for edge cases in critical paths"). - -priority_for(high_carbon, high) :- !. -priority_for(energy_inefficient, high) :- !. -priority_for(high_debt, medium) :- !. -priority_for(low_coverage, medium) :- !. -priority_for(_, low). - -%% ============================================================================= -%% TRAINING DATA GENERATION -%% ============================================================================= - -%% Generate training examples for neural networks -%% Format: input features -> expected output -training_example(carbon_estimator, Features, Label) :- - historical_analysis(Code, Features, carbon_score, ActualScore), - (ActualScore < 40 -> Label = high ; Label = normal). - -training_example(refactor_predictor, [Features, Action], Label) :- - historical_refactor(Code, Action, Outcome), - code_features(Code, Features), - (Outcome = success -> Label = 1.0 ; Label = 0.0). diff --git a/bots/sustainabot/prompts/claude-code-instructions.md b/bots/sustainabot/prompts/claude-code-instructions.md deleted file mode 100644 index cbf5a4a8..00000000 --- a/bots/sustainabot/prompts/claude-code-instructions.md +++ /dev/null @@ -1,179 +0,0 @@ - - - -# Oikos Bot Claude Code Instructions - -This file provides ecological and economic context for Claude Code when working -on this repository. These instructions help ensure AI-assisted development -follows sustainable software practices. - -## System Context - -You are working on a repository that is monitored by Oikos Bot, an ecological and -economic code analysis platform. When writing or modifying code, you should -consider the following sustainability principles. - -## Core Principles - -### 1. Carbon Awareness (Weight: 40%) - -Every line of code has a carbon footprint. Consider: - -- **Algorithmic Complexity**: Prefer O(n) over O(n²), O(log n) over O(n) -- **Resource Usage**: Minimize memory allocations and CPU cycles -- **I/O Efficiency**: Batch operations, use caching strategically -- **Idle Behavior**: Sleep efficiently, avoid polling - -When writing loops, ask: "Will this execute millions of times in production?" - -### 2. Economic Efficiency (Weight: 30%) - -Apply economic thinking to code: - -- **Pareto Optimality**: When making trade-offs, ensure no wasted opportunity -- **Allocative Efficiency**: Put resources where they create most value -- **Technical Debt**: Track and minimize debt accumulation -- **Opportunity Cost**: Consider what else could be done with those resources - -When creating abstractions, ask: "Does this justify its complexity?" - -### 3. Quality Metrics (Weight: 30%) - -Maintain code quality: - -- **Complexity**: Keep cyclomatic complexity under 10 per function -- **Coupling**: Minimize dependencies between modules -- **Coverage**: Ensure test coverage for critical paths -- **Documentation**: Document trade-offs and non-obvious decisions - -## Current Repository Status - - -``` -Health Index: {{health_index}}/100 -Eco Score: {{eco_score}}/100 -Econ Score: {{econ_score}}/100 -Quality: {{quality_score}}/100 - -Pareto Status: {{pareto_status}} -Policy Level: {{policy_level}} -``` - - -## Specific Guidance - -### When Writing New Code - -1. **Start with efficiency**: Choose efficient algorithms from the start -2. **Use established patterns**: Prefer patterns known to be eco-friendly -3. **Document trade-offs**: Explain why you chose one approach over another -4. **Consider scale**: What happens when this runs 1M times? - -### When Refactoring - -1. **Check current metrics**: Understand the eco/econ profile first -2. **Target hotspots**: Focus on files with low scores -3. **Measure improvement**: Verify changes improved metrics -4. **Avoid regression**: Don't improve one metric at cost of others - -### When Reviewing - -1. **Check for anti-patterns**: Busy waiting, N+1 queries, unbounded caching -2. **Validate trade-offs**: Are documented trade-offs justified? -3. **Consider alternatives**: Is there a more efficient approach? -4. **Think long-term**: How will this scale? - -## Code Patterns Reference - -### Efficient Patterns - -```python -# Memoization for expensive computations -from functools import lru_cache - -@lru_cache(maxsize=1000) -def expensive_computation(x): - return complex_algorithm(x) -``` - -```python -# Connection pooling -from sqlalchemy import create_engine -from sqlalchemy.pool import QueuePool - -engine = create_engine(url, poolclass=QueuePool, pool_size=5) -``` - -```python -# Lazy evaluation with generators -def process_large_file(path): - with open(path) as f: - for line in f: # Lazy, one line at a time - yield process_line(line) -``` - -```python -# Event-driven waiting -import asyncio - -async def wait_for_result(): - result = await event.wait() # Efficient waiting - return process(result) -``` - -### Anti-Patterns to Avoid - -```python -# DON'T: Busy waiting -while not condition: - time.sleep(0.01) # Still wastes cycles - -# DON'T: N+1 queries -for user in users: - orders = db.query(Order).filter(Order.user_id == user.id).all() - -# DON'T: Unbounded caching -cache = {} # Will grow forever -def get_cached(key): - if key not in cache: - cache[key] = expensive_fetch(key) - return cache[key] -``` - -## Integration with Oikos Bot - -When you make changes: - -1. Oikos Bot will analyze PRs and provide feedback -2. Policy violations may block merges (in regulator mode) -3. Recommendations are based on learned patterns from successful refactoring -4. The praxis loop means your improvements help train better policies - -## Trade-Off Documentation Template - -When making significant trade-offs, use this format: - -``` -# TRADE-OFF: [Brief description] -# Objectives: [List competing objectives] -# Decision: [What you chose] -# Rationale: [Why this is Pareto optimal for this context] -# Metrics Impact: -# - Carbon: [+/-X%] -# - Performance: [+/-X%] -# - Complexity: [+/-X] -# Reviewed: [date] -``` - -## Questions for Self-Review - -Before committing, ask yourself: - -1. Would I be comfortable if this code ran 10M times per day? -2. Have I documented any non-obvious trade-offs? -3. Is there a simpler way to achieve the same result? -4. Will future maintainers understand why I made these choices? - ---- - -*Maintained by Oikos Bot | Last updated: {{timestamp}}* diff --git a/bots/sustainabot/prompts/copilot-instructions.md b/bots/sustainabot/prompts/copilot-instructions.md deleted file mode 100644 index 0212e393..00000000 --- a/bots/sustainabot/prompts/copilot-instructions.md +++ /dev/null @@ -1,167 +0,0 @@ - - - -# Oikos Bot Copilot Instructions - -This file is auto-generated by Oikos Bot to provide ecological and economic context -to AI coding assistants. Place this in `.github/copilot-instructions.md` in your repository. - -## Project Eco-Profile - - -**Last Analysis**: {{timestamp}} -**Health Index**: {{health_index}}/100 (Grade: {{grade}}) - -| Metric | Score | Trend | -|--------|-------|-------| -| Ecological | {{eco_score}} | {{eco_trend}} | -| Economic | {{econ_score}} | {{econ_trend}} | -| Quality | {{quality_score}} | {{quality_trend}} | - - -## Ecological Code Guidelines - -When writing or reviewing code in this repository, always consider these principles: - -### 1. Carbon Efficiency - -**Prefer algorithms with lower computational complexity.** - -- Before writing a loop, ask: "Is there a more efficient algorithm?" -- Avoid O(n²) or worse when O(n log n) or O(n) alternatives exist -- Consider the energy cost of your code running millions of times - -``` -# BAD: O(n²) nested loops -for item in items: - for other in items: - if item.matches(other): ... - -# GOOD: O(n) with hash lookup -item_map = {item.key: item for item in items} -for item in items: - if item.key in item_map: ... -``` - -### 2. Energy Patterns - -**Avoid busy-waiting; prefer event-driven designs.** - -``` -# BAD: Busy waiting (wastes CPU cycles) -while not ready: - time.sleep(0.1) - -# GOOD: Event-driven (sleeps until needed) -event.wait() -``` - -**Use connection pooling for external resources.** - -``` -# BAD: Create new connection each time -def get_data(): - conn = create_connection() - result = conn.query() - conn.close() - return result - -# GOOD: Reuse connections -pool = ConnectionPool(max_size=10) -def get_data(): - with pool.get() as conn: - return conn.query() -``` - -### 3. Resource Allocation - -**Release resources promptly; use context managers.** - -```python -# GOOD: Context manager ensures cleanup -with open(file) as f: - data = f.read() -# File automatically closed -``` - -**Prefer lazy evaluation for large datasets.** - -```python -# BAD: Loads all into memory -all_results = [process(x) for x in huge_list] - -# GOOD: Generator (lazy evaluation) -def results(): - for x in huge_list: - yield process(x) -``` - -### 4. Pareto Optimality - -When making trade-offs, document the decision. - -**Always consider multiple objectives:** -- Performance vs. Readability -- Memory vs. Speed -- Simplicity vs. Flexibility - -```python -# Document trade-off decisions -# TRADE-OFF: Using dict for O(1) lookup increases memory by ~2x -# but reduces query time from O(n) to O(1). For our use case -# with ~10K items and frequent queries, this is Pareto optimal. -lookup_table = build_lookup_table(items) -``` - -## Current Hotspots - - -These areas have been flagged for eco/econ attention: - -{{#each hotspots}} -- **{{this.file}}**: {{this.reason}} (Score: {{this.score}}) -{{/each}} - - -## Best Practices for This Codebase - - -Based on analysis of this repository: - -{{#each best_practices}} -1. **{{this.practice}}**: {{this.description}} - - Expected impact: {{this.carbon_impact}}% carbon, {{this.energy_impact}}% energy -{{/each}} - - -## When Suggesting Code - -1. **Default to efficient patterns**: Suggest the eco-friendly option first -2. **Explain trade-offs**: When there's a choice, explain the eco/econ implications -3. **Flag anti-patterns**: Warn about patterns known to have high eco impact -4. **Recommend testing**: Suggest profiling for performance-critical code - -## Quick Reference: Eco Patterns - -| Pattern | Carbon Impact | Energy Impact | When to Use | -|---------|--------------|---------------|-------------| -| Memoization | -25% | -20% | Expensive repeated calculations | -| Connection Pooling | -15% | -20% | Database/API access | -| Lazy Evaluation | -10% | -15% | Large data processing | -| Event-Driven | -5% | -30% | Waiting for external events | -| Batching | -15% | -15% | Multiple I/O operations | - -## Anti-Patterns to Avoid - -| Anti-Pattern | Problem | Alternative | -|--------------|---------|-------------| -| Busy Waiting | Wastes CPU cycles | Event-driven waiting | -| Premature Optimization | Adds complexity | Profile first, optimize hotspots | -| N+1 Queries | Excessive DB calls | Batch loading, JOINs | -| Unbounded Caching | Memory bloat | LRU cache with size limits | -| Polling | Constant resource use | Webhooks, SSE, WebSockets | - ---- - -*This file is maintained by [Oikos Bot](https://github.com/hyperpolymath/oikos-bot). -Last updated: {{timestamp}}* diff --git a/bots/sustainabot/prompts/pr-template.md b/bots/sustainabot/prompts/pr-template.md deleted file mode 100644 index 46095d41..00000000 --- a/bots/sustainabot/prompts/pr-template.md +++ /dev/null @@ -1,56 +0,0 @@ - - - -## Description - - - -## Oikos Bot Checklist - -Before submitting, please verify: - -### Carbon Efficiency -- [ ] Reviewed algorithm complexity (avoided O(n²) when O(n log n) is possible) -- [ ] Minimized unnecessary computations -- [ ] Used caching/memoization where appropriate -- [ ] Batched I/O operations where possible - -### Energy Patterns -- [ ] No busy-waiting patterns introduced -- [ ] Used event-driven approaches for waiting -- [ ] Implemented connection pooling for external resources -- [ ] Resources are released promptly (context managers, try-finally) - -### Resource Allocation -- [ ] Memory usage is bounded -- [ ] No unbounded caching without eviction -- [ ] Large data processed lazily where possible -- [ ] No resource leaks - -### Trade-Off Documentation -- [ ] Any trade-offs are documented with rationale -- [ ] Pareto optimality considered for design decisions -- [ ] Technical debt (if introduced) is documented and justified - -### Quality -- [ ] Cyclomatic complexity is reasonable (< 10 per function) -- [ ] Tests added for new functionality -- [ ] Documentation updated if needed - -## Eco Impact - - - -**Estimated Changes:** -- Carbon Impact: [ ] Improved [ ] Neutral [ ] Needs Review -- Energy Impact: [ ] Improved [ ] Neutral [ ] Needs Review -- Quality Impact: [ ] Improved [ ] Neutral [ ] Needs Review - -## Related Issues - - -Closes # - -## Additional Notes - - diff --git a/bots/sustainabot/test_sample.rs b/bots/sustainabot/test_sample.rs deleted file mode 100644 index e1bbcbcd..00000000 --- a/bots/sustainabot/test_sample.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Test sample for sustainabot analysis - -fn efficient_function(x: i32) -> i32 { - x * 2 -} - -fn nested_loops_function(data: Vec>) -> i32 { - let mut sum = 0; - for row in &data { - for col in row { - for _ in 0..10 { - sum += col; - } - } - } - sum -} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c8607726..8649d555 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -41,7 +41,7 @@ rhodibot echidnabot sustainabot ... 8 more bots | `fix-*.sh` (~50 files) | `scripts/` | bash | One-shot fixers for eliminate-tier patterns (e.g. SPDX header, license file) | | `shared-context/` | `shared-context/` | Rust | Crate for inter-bot communication (RPC + state-sharing) | | `robot-repo-automaton/` | `robot-repo-automaton/` | Rust | CLI: scan, fix, PR creation | -| `bots/*` | `bots/` | mostly Rust, AffineScript in sustainabot | Per-bot specialised logic | +| `bots/*` | `bots/` | Rust (thin adapters — see `bots/README.adoc`) | Per-bot specialised logic | ## Safety triangle