diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index f723f87..604a747 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -17,6 +17,20 @@ https://semver.org/spec/v2.0.0.html[Semantic Versioning]. ==== Added +* test(analysis): `+crates/oikosbot-analysis/tests/pattern_fixtures.rs+` — paired + silent/firing fixtures for **every** analyzer rule (issue #81, P0). Each pair + differs in exactly the feature that triggers the rule under test, so the + silent side proves silence is for the intended reason rather than because the + body happens to be clean: nested-loops at depth 3 vs 2, busy-wait with and + without a sleep, `+s = s + "…"+` vs `+push_str+`, `.clone()` vs a borrow, + `+File::open+` with and without a `+BufReader+`, a 2 MB vs 1 KB capacity, and + five vs four `.to_string()` calls. Each pair also pins the boundary, not just + the presence of the pattern. Three "no check performed" cases assert that + missing tools and unparsed input are not passes: a source with no functions, + malformed source, and truncated source all yield **zero** findings rather than + a green `+oikosbot/general+`; an unsupported extension is an error, not a + silent success. The fixtures assert the published `+rule_id+`, not the private + detectors. * docs: `+DEBT.adoc+` re-verified against the tree. Six entries whose fixes had already landed were still listed as open — the orphaned `+LICENSES/AGPL-3.0-or-later.txt+`, the unheaded `+GOVERNANCE.md+`, the boilerplate `+ARCHITECTURE.md+`/ diff --git a/DEBT.adoc b/DEBT.adoc index 00f1ff2..a915459 100644 --- a/DEBT.adoc +++ b/DEBT.adoc @@ -38,18 +38,35 @@ Severity is about *consequence*, not effort: == Licence debt The scheme itself is sound and consistently applied: **MPL-2.0 for code, -CC-BY-SA-4.0 for documentation**, 159 and 65 identifier occurrences respectively +CC-BY-SA-4.0 for documentation**, 160 and 65 identifier occurrences respectively (`grep -rhoP "SPDX-License-Identifier: \K[A-Za-z0-9.+-]+"`, re-counted 2026-09-26 against the current tree; the raw total includes two headers quoted inside `docs/superpowers/plans/2026-08-03-estate-economics-round-one.adoc`, -which teaches the convention by example). No third licence is declared anywhere -in the tree. +which teaches the convention by example). No third licence is declared +anywhere in the tree. [cols="1,4"] |=== -| — | *Resolved 2026-08-07 (re-verified against the tree 2026-09-26):* *`LICENSES/AGPL-3.0-or-later.txt` was orphaned.* No file declared the AGPL identifier, so a REUSE-conformant tree should not carry its text. The file is absent from both the working tree and the base commit (`git ls-tree -r 7781489 -- LICENSES/` → only `CC-BY-SA-4.0.txt`, `MPL-2.0.txt`). The only remaining "AGPL" strings are a prohibition in `.machine_readable/descriptiles/AGENTIC.a2ml` ("Never use AGPL license (use MPL-2.0)"), this register, and a quotation of another repo's header in `docs/superpowers/specs/`. Note the identifier itself is deliberately *not* restated verbatim in this entry: a quoted `SPDX-License-Identifier:` line inside a document is indistinguishable from a real declaration to `grep`, and this register has already recorded that class of defect once (see the duplicated-header entry above). - -| — | *Resolved 2026-08-07 (re-verified 2026-09-26):* *`GOVERNANCE.md` carried no SPDX header at all.* The file was deleted as boilerplate (see <>); the surviving `GOVERNANCE.adoc` carries `SPDX-License-Identifier: CC-BY-SA-4.0` on line 1, as every other document does. +| — | *Resolved 2026-08-07 (re-verified against the tree 2026-09-26):* +*`LICENSES/AGPL-3.0-or-later.txt` was orphaned.* No file declared the +AGPL identifier, so a REUSE-conformant tree should not carry its text. +The file is absent from both the working tree and the base commit (`git +ls-tree -r 7781489 -- LICENSES/` → only `CC-BY-SA-4.0.txt`, +`MPL-2.0.txt`). The only remaining "AGPL" strings are a prohibition in +`.machine_readable/descriptiles/AGENTIC.a2ml` ("Never use AGPL license +(use MPL-2.0)"), this register, and a quotation of another repo's header +in `docs/superpowers/specs/`. Note the identifier itself is deliberately +*not* restated verbatim in this entry: a quoted +`SPDX-License-Identifier:` line inside a document is indistinguishable +from a real declaration to `grep`, and this register has already +recorded that class of defect once (see the duplicated-header entry +above). + +| — | *Resolved 2026-08-07 (re-verified 2026-09-26):* *`GOVERNANCE.md` carried +no SPDX header at all.* The file was deleted as boilerplate (see +<>); the surviving `GOVERNANCE.adoc` carries +`SPDX-License-Identifier: CC-BY-SA-4.0` on line 1, as every other +document does. | `HYGIENE` | *`docs/README.adoc` was mis-licensed by a duplicated header.* It carried **two** conflicting identifiers — `MPL-2.0` on line 1 and @@ -71,11 +88,29 @@ invisible: a correct header in the wrong position silently fails the gate. [cols="1,4"] |=== -| — | *Resolved 2026-08-07 (re-verified against the tree 2026-09-26):* *`ARCHITECTURE.md` was generic boilerplate contradicting the repository.* It documented a `src/ tests/ scripts/ config/` layout while this repo is a Cargo workspace whose code lives in `crates/`, it had no SPDX header, and it shadowed the real `ARCHITECTURE.adoc`. Deleted; `ARCHITECTURE.adoc` is now the only architecture document and is banner-flagged as target design. - -| — | *Resolved 2026-08-07 (re-verified 2026-09-26):* *`GOVERNANCE.md` was the same defect* — generic prose duplicating the real, SPDX-headed `GOVERNANCE.adoc`. Deleted. - -| — | *Resolved 2026-08-07 (re-verified 2026-09-26):* *`ARCHITECTURE.adoc` specified components that do not exist.* The document now opens with a `[WARNING]` block — "*This is the TARGET design, not the current state*" — that names each unbuilt component inline: the OCaml documentation analyzer (absent; `analyzers/` holds only `code-haskell`), the superseded "Python + Datalog" policy-engine line (Python is banned; the 2026-07-28 ruling retargets to Scallop), the fourth `policy developer` bot role (only three modes exist in `crates/oikosbot-cli/src/config.rs`), and the Praxis Loop / DeepProbLog inference. Readers are pointed at `docs/STATUS.adoc`, `EXPLAINME.adoc` and this register for what is actually built. +| — | *Resolved 2026-08-07 (re-verified against the tree 2026-09-26):* +*`ARCHITECTURE.md` was generic boilerplate contradicting the +repository.* It documented a `src/ tests/ scripts/ config/` layout while +this repo is a Cargo workspace whose code lives in `crates/`, it had no +SPDX header, and it shadowed the real `ARCHITECTURE.adoc`. Deleted; +`ARCHITECTURE.adoc` is now the only architecture document and is +banner-flagged as target design. + +| — | *Resolved 2026-08-07 (re-verified 2026-09-26):* *`GOVERNANCE.md` was the +same defect* — generic prose duplicating the real, SPDX-headed +`GOVERNANCE.adoc`. Deleted. + +| — | *Resolved 2026-08-07 (re-verified 2026-09-26):* *`ARCHITECTURE.adoc` +specified components that do not exist.* The document now opens with a +`[WARNING]` block — "*This is the TARGET design, not the current state*" +— that names each unbuilt component inline: the OCaml documentation +analyzer (absent; `analyzers/` holds only `code-haskell`), the +superseded "Python + Datalog" policy-engine line (Python is banned; the +2026-07-28 ruling retargets to Scallop), the fourth `policy developer` +bot role (only three modes exist in +`crates/oikosbot-cli/src/config.rs`), and the Praxis Loop / DeepProbLog +inference. Readers are pointed at `docs/STATUS.adoc`, `EXPLAINME.adoc` +and this register for what is actually built. | `BLOCKING` | *The README carried an unearned OpenSSF Best Practices badge.* A hardcoded green `shields.io` image asserting "OpenSSF Best Practices", linked @@ -406,7 +441,12 @@ https://github.com/hyperpolymath/oikosbot/pull/64[#64]. settings-level disabled state (which is invisible from the repository contents), why it is kept, and what re-enabling requires. -| — | *Resolved 2026-07-28 (re-verified against the tree 2026-09-26):* *`push-email-notify.yml` was dormant by design* while remaining in the tree, against the estate's dual-use ruling. The workflow is gone from `.github/workflows/` (15 workflows listed, none named `push-email-notify.yml`), and `STATE.a2ml` records its removal under that ruling. +| — | *Resolved 2026-07-28 (re-verified against the tree 2026-09-26):* +*`push-email-notify.yml` was dormant by design* while remaining in the +tree, against the estate's dual-use ruling. The workflow is gone from +`.github/workflows/` (15 workflows listed, none named +`push-email-notify.yml`), and `STATE.a2ml` records its removal under +that ruling. |=== == What is *not* debt diff --git a/crates/oikosbot-analysis/tests/pattern_fixtures.rs b/crates/oikosbot-analysis/tests/pattern_fixtures.rs new file mode 100644 index 0000000..d19672d --- /dev/null +++ b/crates/oikosbot-analysis/tests/pattern_fixtures.rs @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! Paired silent/firing fixtures for every analyzer rule (issue #81, P0). +//! +//! The acceptance rule from #81 is that *every* gate demonstrates both a clean +//! input that stays silent and a broken input that fires **for the intended +//! reason**. Each pair below differs in exactly one feature — the one that +//! triggers the rule under test — so a regression cannot pass by accident: if +//! the silent side starts firing, the rule has become trigger-happy, and if the +//! firing side goes quiet, the rule has stopped working. +//! +//! Every fixture is a single function, so each pair asserts on exactly one +//! finding. That matters because `AnalysisResult::rule_id` reports the *most +//! significant* pattern found, in `patterns::detect_patterns` order; a fixture +//! that tripped two rules would only prove the first one. The pairs are +//! therefore also a check that the rules do not fire spuriously on each +//! other's inputs. +//! +//! These assert observable behaviour — the `rule_id` the analyzer publishes — +//! not the private detectors, and they need no filesystem: `analyze_source` +//! takes the source directly. + +use oikosbot_analysis::{analyze_source, Language}; +use std::path::Path; + +/// Every `rule_id` the analyzer produces for a source, in source order. +fn rules(source: &str) -> Vec { + let results = analyze_source(source, Language::Rust).expect("analysis must not fail"); + results.into_iter().map(|r| r.rule_id).collect() +} + +/// The one finding a single-function fixture must produce. +/// +/// Panics if the fixture does not yield exactly one finding, which keeps a +/// malformed fixture from quietly testing nothing. +fn sole_rule(source: &str) -> String { + let found = rules(source); + assert_eq!( + found.len(), + 1, + "expected exactly one finding from a one-function fixture, got {found:?}" + ); + found.into_iter().next().expect("one finding") +} + +// --------------------------------------------------------------------------- +// nested-loops — fires at loop depth >= 3 +// --------------------------------------------------------------------------- + +/// Three nested `for` loops: the smallest nest the rule flags. +const NESTED_FIRE: &str = r#"fn three_deep(rows: &[Vec]) -> usize { + let mut total = 0; + for row in rows { + for cell in row { + for _shift in 0..4 { + total += cell as usize; + } + } + } + total +} +"#; + +/// The same nest one level shallower. Identical shape, one level short of the +/// threshold — silence must be because of the depth, not because the body is +/// somehow clean. +const NESTED_SILENT: &str = r#"fn two_deep(rows: &[Vec]) -> usize { + let mut total = 0; + for row in rows { + for cell in row { + total += cell as usize; + } + } + total +} +"#; + +#[test] +fn nested_loops_fires_at_depth_three_and_not_at_two() { + assert_eq!(sole_rule(NESTED_FIRE), "oikosbot/nested-loops"); + assert_eq!(sole_rule(NESTED_SILENT), "oikosbot/general"); +} + +// --------------------------------------------------------------------------- +// busy-wait — a `loop`/`while` whose body never blocks +// --------------------------------------------------------------------------- + +/// Spins on an atomic with no sleep, await, yield or blocking call. +const BUSY_WAIT_FIRE: &str = r#"fn spin(flag: &AtomicBool) -> usize { + let mut spins = 0; + loop { + if flag.load(Ordering::Relaxed) { + break; + } + spins += 1; + } + spins +} +"#; + +/// The same loop with one sleep per iteration: it still polls, but it no longer +/// burns the CPU continuously, which is what the rule is about. +const BUSY_WAIT_SILENT: &str = r#"fn paced(flag: &AtomicBool) -> usize { + let mut spins = 0; + loop { + if flag.load(Ordering::Relaxed) { + break; + } + spins += 1; + std::thread::sleep(std::time::Duration::from_millis(1)); + } + spins +} +"#; + +#[test] +fn busy_wait_fires_on_a_loop_that_never_blocks() { + assert_eq!(sole_rule(BUSY_WAIT_FIRE), "oikosbot/busy-wait"); + assert_eq!(sole_rule(BUSY_WAIT_SILENT), "oikosbot/general"); +} + +// --------------------------------------------------------------------------- +// string-concat-in-loop — `s = s + "…"` inside a loop body +// --------------------------------------------------------------------------- + +/// Builds a string with `+` inside the loop: quadratic allocation. +const CONCAT_FIRE: &str = r#"fn joined(parts: &[&str]) -> String { + let mut out = String::new(); + for part in parts { + out = out + "-"; + } + out +} +"#; + +/// The same loop using `push_str`, which amortises the allocation. Note the +/// loop still allocates once up front — the difference is per-iteration +/// reallocation, which is the thing being flagged. +const CONCAT_SILENT: &str = r#"fn pushed(parts: &[&str]) -> String { + let mut out = String::new(); + for part in parts { + out.push_str("-"); + } + out +} +"#; + +#[test] +fn string_concat_fires_only_on_concatenation_not_appending() { + assert_eq!(sole_rule(CONCAT_FIRE), "oikosbot/string-concat-in-loop"); + assert_eq!(sole_rule(CONCAT_SILENT), "oikosbot/general"); +} + +// --------------------------------------------------------------------------- +// clone-in-loop — `.clone()` inside a loop body +// --------------------------------------------------------------------------- + +/// Clones each element on every iteration. +const CLONE_FIRE: &str = r#"fn copied(items: &[String]) -> usize { + let mut total = 0; + for item in items { + total += item.clone().len(); + } + total +} +"#; + +/// The same loop borrowing instead of copying. +const CLONE_SILENT: &str = r#"fn borrowed(items: &[String]) -> usize { + let mut total = 0; + for item in items { + total += item.len(); + } + total +} +"#; + +#[test] +fn clone_in_loop_fires_on_the_clone_and_not_the_borrow() { + assert_eq!(sole_rule(CLONE_FIRE), "oikosbot/clone-in-loop"); + assert_eq!(sole_rule(CLONE_SILENT), "oikosbot/general"); +} + +// --------------------------------------------------------------------------- +// unbuffered-io — `File::open`/`File::create` with no `BufReader`/`BufWriter` +// --------------------------------------------------------------------------- + +/// Reads straight from the file handle. +const IO_FIRE: &str = r#"fn raw_read(path: &Path) -> std::io::Result { + let mut count = 0; + let mut handle = File::open(path)?; + count += 1; + Ok(count) +} +"#; + +/// The same read wrapped in a `BufReader`. The `File::open` is still there — +/// the pair proves the rule looks for buffering, not for file access. +const IO_SILENT: &str = r#"fn buffered_read(path: &Path) -> std::io::Result { + let mut count = 0; + let mut handle = BufReader::new(File::open(path)?); + count += 1; + Ok(count) +} +"#; + +#[test] +fn unbuffered_io_fires_without_a_bufwrapper_and_not_with_one() { + assert_eq!(sole_rule(IO_FIRE), "oikosbot/unbuffered-io"); + assert_eq!(sole_rule(IO_SILENT), "oikosbot/general"); +} + +// --------------------------------------------------------------------------- +// large-allocation — a capacity literal above 1 MB +// --------------------------------------------------------------------------- + +/// Allocates 2 MB up front. +const ALLOC_FIRE: &str = r#"fn big_buffer() -> Vec { + let buffer: Vec = Vec::with_capacity(2_000_000); + buffer +} +"#; + +/// The same allocation at 1 KB: under the threshold, so no finding. +const ALLOC_SILENT: &str = r#"fn small_buffer() -> Vec { + let buffer: Vec = Vec::with_capacity(1024); + buffer +} +"#; + +#[test] +fn large_allocation_fires_above_the_threshold_and_not_below() { + assert_eq!(sole_rule(ALLOC_FIRE), "oikosbot/large-allocation"); + assert_eq!(sole_rule(ALLOC_SILENT), "oikosbot/general"); +} + +// --------------------------------------------------------------------------- +// redundant-allocation — five or more `.to_string()`/`.to_owned()` calls +// --------------------------------------------------------------------------- + +/// Five stringifyings: at the threshold the rule fires. +const REDUNDANT_FIRE: &str = r#"fn quint(a: &str, b: &str, c: u16, d: &str, e: &str) { + let _a = a.to_string(); + let _b = b.to_string(); + let _c = c.to_string(); + let _d = d.to_string(); + let _e = e.to_string(); +} +"#; + +/// Four of the same: below the threshold, deliberately, so the pair pins the +/// boundary rather than the presence of `.to_string()`. +const REDUNDANT_SILENT: &str = r#"fn quad(a: &str, b: &str, c: u16, d: &str) { + let _a = a.to_string(); + let _b = b.to_string(); + let _c = c.to_string(); + let _d = d.to_string(); +} +"#; + +#[test] +fn redundant_allocation_fires_at_five_calls_and_not_four() { + assert_eq!(sole_rule(REDUNDANT_FIRE), "oikosbot/redundant-allocation"); + assert_eq!(sole_rule(REDUNDANT_SILENT), "oikosbot/general"); +} + +// --------------------------------------------------------------------------- +// "No check performed" is not a pass (#81 acceptance rule) +// --------------------------------------------------------------------------- + +/// A source with no functions in it. The analyzer must report *nothing* rather +/// than inventing a clean bill of health. +#[test] +fn source_without_functions_yields_no_findings() { + let source = "// only a comment and a struct\nstruct Point {\n x: u32,\n y: u32,\n}\n"; + assert!( + rules(source).is_empty(), + "a file with no functions must not produce findings" + ); +} + +/// Unparseable source. Tree-sitter is error-tolerant and will happily hand back +/// a partial tree, so this asserts the analyzer does not dress a parse failure +/// up as a finding on code it never understood. +#[test] +fn malformed_source_yields_no_findings() { + let malformed = "fn broken( {\n let x = ;\n"; + assert!( + rules(malformed).is_empty(), + "unparsed input must not be reported as analysed code" + ); + + let truncated = "fn half(a: u32) -> u32 {\n let b = a +\n"; + assert!( + rules(truncated).is_empty(), + "truncated input must not be reported as analysed code" + ); +} + +/// An extension the analyzer does not support. This is the clearest case of +/// "no check performed": it must be an error, never a silent success. +#[test] +fn unsupported_extension_is_an_error_not_a_pass() { + let err = Language::detect(Path::new("notes.txt")) + .expect_err("an unsupported extension must not be silently accepted"); + assert!( + err.to_string().contains("Unsupported file extension"), + "the error should name the reason, got: {err}" + ); +} diff --git a/docs/README.adoc b/docs/README.adoc index 844c1fb..9fd0e35 100644 --- a/docs/README.adoc +++ b/docs/README.adoc @@ -98,3 +98,17 @@ _Remaining audience-depth gaps: the README→`docs/` split and the end-to-end build walkthrough in link:https://github.com/hyperpolymath/oikosbot/issues/16[#16], and the production deploy runbook in link:https://github.com/hyperpolymath/oikosbot/issues/17[#17]._ + +== Handoff + +For whoever picks this up next — the automation or a new maintainer: + +* link:handoff/PROMPT.md[**PROMPT**] — the per-issue plan for the open backlog, + with the hard facts that should not be rediscovered. +* link:handoff/owner-actions.adoc[**Owner actions**] — everything that needs a + permission a checkout does not have: repository settings, other repositories, + and the design rulings the code is waiting on. Includes ready-to-post issue + comments for the tracker. +* link:handoff/open-issues.txt[**open-issues.txt**] — verbatim issue bodies. +* link:handoff/CI-EVIDENCE.md[**CI-EVIDENCE**] — how the CI failures were + diagnosed without access to logs or artifacts. diff --git a/docs/handoff/owner-actions.adoc b/docs/handoff/owner-actions.adoc new file mode 100644 index 0000000..a24a7dd --- /dev/null +++ b/docs/handoff/owner-actions.adoc @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + += Owner actions — what only you can do +Jonathan D.A. Jewell +:toc: preamble +:toclevels: 2 + +Everything in the repository that could be fixed from a checkout has been +fixed. This file lists what is left, and every item needs a permission the +automation does not have: repository settings, another repository, or a design +ruling. It also carries ready-to-post issue comments so the tracker does not +have to be rewritten by hand. + +Gathered 2026-09-26 on branch `arena/01a0dad6-oikosbot`. + +== The one blocker that gates everything else + +`CI` (and every other workflow on a `pull_request` event from this branch) ends +in `startup_failure` at 0 seconds. Fetching the run's HTML page gives the reason +verbatim: + +____ +Actor is not allowed to trigger Actions workflows. +Workflow file: '.github/workflows/ci.yml'. +____ + +That is a repository policy on *which actors* may trigger workflows, not a +defect in the workflows, the lockfile, or the actor's permissions — the API run +object does not even carry the message, it exists only on the HTML page. + +It is also why the sandbox cannot verify any Rust change: with no Rust toolchain +obtainable (rustup and crates.io are both blocked by the proxy allowlist), +`cargo fmt --check`, `cargo build`, `cargo test` and `cargo clippy` can only be +exercised by CI. + +Three ways past it, in order of preference: + +. **Re-run the PR's workflows yourself.** On the PR page, *Re-run all jobs*. + Because you are the actor, the policy does not apply. +. **Allow the bot actor** — Settings → Actions → General → *Actions permissions* + → the allow-list of actors that may trigger workflows. Add + `arena-ai-coding-agent[bot]`. This also unblocks every future session branch. +. **Merge to `main`.** A `push` event on `main` runs normally — that is how + #107 was verified. `Publish Image` only triggers on `main`, `v*` tags or a + manual dispatch, so the container fix needs this route regardless. + +== Verification status + +[cols="1,1,2,3"] +|=== +| PR | Change | Status | What is still unverified + +| #107 | #48 calibration wiring, RE008 fix, MSRV fix | **merged** (`97a3393`) | Nothing. `CI` green on the merge, including the `Rust Workspace` job's format check, build, test, Eclexia adapter and clippy steps. `Hypatia` and `Publish Image` — the two long-standing red checks — are green. +| #109 | #81 P0 analyzer fixtures | open, CI blocked by the actor policy | `cargo test -p oikosbot-analysis` must run the new `pattern_fixtures.rs`. Verified out-of-band by porting `detect_patterns` and running all 17 sources through it (see the PR body); CI is the remaining confirmation. +|=== + +== Repository settings + +=== 1. Four phantom required contexts + +The `main` ruleset requires 27 status checks. All 27 that *can* report do report +and pass — but four of the required contexts never appear, so `mergeStateStatus` +is permanently `BLOCKED` and every merge needs `--admin`. + +[cols="2,3"] +|=== +| Required context | Why it never reports + +| `Build and push oikos image` +| `publish-image.yml` triggers only on `push: branches: [main]`. It cannot run on a pull request, so **no PR can ever satisfy it**. Structurally unsatisfiable, not merely absent. + +| `Dependabot` +| Reports only on Dependabot's own PRs. Unsatisfiable on any human PR. + +| `governance / Guix primary / Nix fallback policy` +| The job was renamed to `governance / Guix packaging policy (Nix retired)`. Renaming a job silently orphans the required context that names it. + +| `lint-workflows` +| Declared as a job in `workflow-linter.yml` but did not report on the PR measured; the governance-scoped `governance / Workflow security linter` did. Looks like a duplicate or stale entry — needs confirmation. +|=== + +*Fix:* drop the two structurally unsatisfiable contexts, rename the Guix one to +match the job, and confirm the fourth. + +The consequence is worse than the inconvenience: branch protection that can never +be satisfied is not protection. It trains everyone to merge with `--admin`, which +bypasses **all** the checks including the 23 real ones, so over-specifying the +ruleset yields *less* enforcement than specifying it correctly. + +*Reproduce:* +[source,shell] +---- +comm -23 <(gh api repos/hyperpolymath/oikosbot/rules/branches/main \ + -q '.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context' \ + | sort -u) <(gh pr checks | cut -f1 | sort -u) +---- + +=== 2. Actions actor policy + +See the blocker above. This is the single change that unblocks CI verification +for every future session. + +=== 3. GHCR package access + +`Publish Image` previously failed with `permission_denied: write_package`; that +was granted. It is green now. Worth re-checking only if the image build fails +again. + +=== 4. Marketplace listing + +Publishing the release and the Marketplace listing from `main` is owner-only. +Before publishing, two things need a decision: + +* `action.yml` pins the image by digest (`sha256:c6a127…`). That digest predates + the `rust:1-slim` builder fix, so it **must be re-pinned** after the next + successful `Publish Image` run on `main`. The new digest cannot be known from + the sandbox (`ghcr.io` is unreachable here). +* Version 0.1.0 in `Cargo.toml` — confirm that is the release you intend to + publish. + +== Design rulings the code is waiting on + +These are not bugs. Each is a decision the automation should not make on the +project's behalf. + +=== 1. EcoScore scale (blocking: the `check` eco-threshold gate) + +The canon says `EcoScore = w1·CarbonScore + w2·EnergyScore + w3·ResourceScore`. +The implementation is a single term: `100 − 10·ln(E)` clamped to 0–100. + +Now that calibration is wired, calibrated energies land around 0.001–1 J, so the +score saturates near 100 for almost everything and the `check` eco-threshold +gate becomes near-vacuous. A ~30-node depth-3 nested loop went from a naive ~3 J +(eco ≈ 89) to a calibrated ~0.007 J (eco clamped to 100). The gate was already +weak — it only fires above roughly 1480 nodes — but the wiring makes it weaker. + +Either rescale `EnergyScore` or accept that the eco-threshold gate is decorative +and say so in the docs. What should not happen is leaving a gate that looks like +it enforces something. + +=== 2. The Datalog policy engine has never executed + +`policy-engine/datalog/eco_rules.dl` is Souffle-dialect Datalog. Nothing invokes +it: no Souffle call in `Justfile`, any `.just` file, or any workflow. Souffle +*is* declared as a dependency in `guix/manifest.scm` and `guix/oikos.scm` — the +toolchain is packaged, the rules are written, and the two are never connected. + +Consequence: the allocation-waste and technical-debt rules declared in the +Datalog have **no Rust counterpart at all**. The `Alloc` and `Debt` terms in +EconScore are unrelated stand-ins, not implementations of those rules. + +Options, in the order the evidence supports: + +. Wire Souffle into `just` and run the rules as a real gate. +. Port to an executable Scallop engine (the 2026-07-28 ruling's target). +. Keep it as a specification and **remove the pretence that it is executed** — + `docs/STATUS.adoc` already says this honestly, but `ROADMAP.adoc` used to + imply otherwise. + +=== 3. The Eclexia policy backend is a fake gate + +`oikosbot-eclexia` has three backends. The default, `evaluate_builtin`, +dispatches on the `.ecl` **file stem** and applies hardcoded Rust thresholds; +the file's contents are never read. The thresholds contradict the files they +claim to implement: `policies/energy_threshold.ecl` declares `> 50.0 J` **per +function**, the builtin fires at `> 1000 J` **total**. + +It is mitigated, not fixed: every builtin evaluation emits a loud +`::warning::… file contents NOT parsed`. The real fix is the upstream `eclexia` +binary or the native parser/interpreter, neither of which this repository can +supply. `eclexia-native` is a reserved, unbuildable seam. + +=== 4. #12 — taxonomy vocabulary reconciliation + +`META.a2ml [maintenance-axes]` and `NEUROSYM.a2ml [finding-taxonomy]` define +overlapping-but-different vocabularies for the same concept. The DEED migration +is the delivery vehicle, and the converter fails closed on the family-4 mapping +that this needs. Either: + +. extend the standards-side family-4 mapping for `[finding-taxonomy]`, **or** +. rule explicitly on the mapping so the converter can proceed. + +Do not hand-roll an unmapped clause — that is the one outcome worse than leaving +it open. + +=== 5. Minor open design questions + +* `aggregate_point` sums resources but averages quality across files. Confirm + that asymmetry is intended. +* Compile-time epsilon and weights versus runtime configuration. +* `push-email-notify.yml` was removed under the dual-use ruling. Confirm no + other repo needs an exemption. + +== Ready-to-post issue comments + +The automation cannot comment on or close issues ("Resource not accessible by +integration"), so the text is kept here instead. + +=== #48 — close as completed + +[source,markdown] +---- +Landed in #107 (squash-merged to main as 97a3393) and verified by CI: CI green, +including the Rust Workspace job's Format check, Build, Test, Eclexia adapter +and Clippy steps. + +Asked vs shipped: + +- Map detected patterns onto OperationKind — calibration::operation_for_pattern + maps nested-loops to Sort, busy-wait to MathCompute, string-concat-in-loop to + StringOp, clone-in-loop and large-allocation to Allocation, unbuffered-io to + FileIO. redundant-allocation is deliberately unmapped so a marginal finding + cannot inherit calibrated confidence. +- Use estimate_operation() for recognised patterns, naive path for the rest — + Analyzer::estimate_resources() takes the calibrated row when a pattern maps, + otherwise naive_resources() labelled Confidence::Estimated with no band. +- Confidence earned per finding — it comes from the row that priced the unit, + not from a constant. +- Propagate the real ResourceRange — oikosbot_metrics::ResourceRange{min, + typical, max, confidence} on AnalysisResult, serde-defaulted, emitted in SARIF + as properties.resource_range. +- The three falsifier tests — analyzer.rs carries them, and + crates/oikosbot-cli/tests/check_gate.rs proves --check can exit 1 end-to-end + against the real binary. + +Two consequences recorded rather than hidden: the eco-threshold gate is now +near-vacuous because calibrated energies are small enough that +calculate_eco_score saturates near 100 (needs an owner ruling on the +EnergyScore scale), and unrecognised code is still collinear on the naive path. +---- + +=== #81 — tick the P0 checkbox that landed + +The item *"Give every analyzer rule a paired silent/firing fixture; malformed or +unsupported input must report 'no check performed' rather than green"* is +delivered in #109. Seven rules, each with a firing and a silent fixture that +differs in exactly the triggering feature, plus three no-check cases. The other +six P0 items and all of P1 remain open. + +=== #17 — end-user guide + +Substantially delivered in #107: `docs/usage.adoc` covers the action inputs, the +three modes, verdicts and the confidence ladder, the SARIF shape, `.oikos.yml`, +`BOT_MODE` (defined from the code, not guessed) and troubleshooting; +`docs/README.adoc` is now a documentation map; `docs/ci-runbook.adoc` covers the +gates. Still missing before this can close: an issue-level walkthrough for a +maintainer triaging a specific finding, and `DEPLOY.adoc`, which stays gated on +AffineScript operational parity. + +=== #16 — split the README into a docs tree + +Delivered in #107: `docs/README.adoc` index, a per-crate `README.adoc` set +(metrics, analysis, pareto, sarif, eclexia, telemetry, capability, dea, cli) and +`analyzers/code-haskell/README.adoc`, each stating what the crate owns, its +public surface, and what it does **not** do. `ROADMAP.adoc` and `README.adoc` +point at the tree. + +=== #18 — taxonomy tags through the Rust `Finding` types + +Blocked on #12's vocabulary ruling. The plan is unchanged: add `intent` (derived +from confidence), `maintenance` and `locus` to the types in +`crates/oikosbot-metrics`, populate them in `crates/oikosbot-analysis`, emit them +in `crates/oikosbot-sarif` `properties` and in PR comments, using enums rather +than strings. Can only be verified in CI. + +=== #82 — close and re-file + +This is a conditional policy with an unresolvable blocker: no canonical Hexadeca +authority exists. It is not actionable as a task. On your confirmation, close it +with a comment linking the re-files: + +. a policy doc in-repo (`docs/policies/interface-gating.adoc`) recording the + Idris2 / Zig / SNIF / Hexadeca trigger conditions, and +. an issue — standards-side if you agree — to locate or publish the Hexadeca + authority. + +Keep #81 cross-referenced. + +== Other repositories + +=== The consumer fleet is still dark + +Fifteen estate repositories carry `.github/workflows/oikosbot.yml` pinned to +`oikosbot@bb95ab50` (v0.1.0), all merged — but each needs its **own** Actions +lockfile before its workflows can start. Until then OikosBot is installed +everywhere and running nowhere. This is the highest-value follow-up, and it +cannot be done from this checkout. + +=== Idaptik-UMS has a repo-wide startup failure + +`metadatastician/idaptik-ums` fails all workflows at startup on `main` +(OikosBot, Licence hygiene, CodeQL), with two workflows displayed as paths +rather than names — the estate tell for never-parsed. Repo-level and +pre-existing; not caused by the OikosBot sweep, since the same file succeeded on +a branch there.