From ab0222d5b68eb85ceed33301d73ccb4ebcee9bc7 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:06:00 +0000 Subject: [PATCH 1/2] test(analysis): paired silent/firing fixtures for every analyzer rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #81, P0: "Give every analyzer rule a paired silent/firing fixture; malformed or unsupported input must report 'no check performed' rather than green." This lands that item. crates/oikosbot-analysis/tests/pattern_fixtures.rs covers all seven rules in patterns::detect_patterns. Each pair differs in exactly the feature that triggers the rule under test, so the silent side proves the silence is for the intended reason rather than because the fixture happens to be clean: nested-loops depth 3 vs depth 2 busy-wait spin vs spin-with-one-sleep-per-iteration string-concat-in-loop s = s + "-" vs push_str clone-in-loop item.clone().len() vs item.len() unbuffered-io File::open vs BufReader::new(File::open(..)) large-allocation with_capacity(2_000_000) vs with_capacity(1024) redundant-allocation five .to_string() calls vs four Several pairs deliberately pin a boundary rather than the mere presence of a pattern: nested-loops at the depth-3 threshold, redundant-allocation at the five-call threshold, large-allocation at the 1 MB threshold. If a threshold moves, these fail. Every fixture is a single function and asserts the published AnalysisResult:: rule_id, not the private detectors, per #81's "tests assert observable behaviour". The single-function constraint is load-bearing: rule_id reports the most significant pattern in detect_patterns order, so a fixture tripping two rules would only prove the first. The pairs therefore double as a check that the rules do not fire spuriously on each other's inputs. The "no check performed" cases assert the acceptance rule that missing tools, unparsed inputs and crashes are not passes: a source with no functions, malformed source and truncated source each yield zero findings rather than a green oikosbot/general, and an unsupported extension is an error naming the reason. Verification (no Rust toolchain in this sandbox): all 17 sources in the test file were run through a faithful port of detect_patterns against the tree-sitter-rust grammar and produce exactly the verdicts asserted — 14 paired fixtures plus the three no-check cases, 0 divergences. tools/ci/linter-verify.sh passes all four steps. CI on the merged #48 work already proved the workspace builds and tests green; this file is additive and needs the same confirmation. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- CHANGELOG.adoc | 14 + DEBT.adoc | 2 +- .../tests/pattern_fixtures.rs | 311 ++++++++++++++++++ 3 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 crates/oikosbot-analysis/tests/pattern_fixtures.rs 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..0507ca2 100644 --- a/DEBT.adoc +++ b/DEBT.adoc @@ -38,7 +38,7 @@ 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`, 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}" + ); +} From d78bbd2b2ec9c9dafc9066db1b1ce4cb6d51d552 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:12:17 +0000 Subject: [PATCH 2/2] docs: owner-actions handoff + debt-register line wrapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/handoff/owner-actions.adoc collects everything that needs a permission a checkout does not have, so the handoff does not have to be reconstructed from a session log: * the Actions actor policy that makes every pull_request workflow on an agent branch fail at startup ("Actor is not allowed to trigger Actions workflows"), with the three ways past it — re-run the workflows as the owner, allow the bot actor in Actions settings, or merge to main; * the four phantom required contexts in the main ruleset, why each can never report, and the reproduction command — including the point that protection which can never be satisfied trains everyone into --admin, which bypasses all 27 checks including the 23 real ones; * the design rulings the code is waiting on: the EcoScore scale (now that calibrated energies saturate calculate_eco_score near 100 and the eco-threshold gate is near-vacuous), the never-executed Datalog engine, the Eclexia fake gate, #12's family-4 mapping, and three minor open questions; * ready-to-post issue comments, since the automation cannot comment on or close issues ("Resource not accessible by integration"); * the other repositories: the consumer fleet that needs its own lockfiles, and the repo-wide startup failure in metadatastician/idaptik-ums. docs/README.adoc indexes it under a new Handoff section. DEBT.adoc's newly resolved entries are re-wrapped to the file's prevailing style — they had been written as single long lines running to 895 characters, against a file whose cells wrap near 80. The Summary table keeps its single-line-row convention, which is what it already used. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- DEBT.adoc | 62 +++++-- docs/README.adoc | 14 ++ docs/handoff/owner-actions.adoc | 296 ++++++++++++++++++++++++++++++++ 3 files changed, 361 insertions(+), 11 deletions(-) create mode 100644 docs/handoff/owner-actions.adoc diff --git a/DEBT.adoc b/DEBT.adoc index 0507ca2..a915459 100644 --- a/DEBT.adoc +++ b/DEBT.adoc @@ -42,14 +42,31 @@ 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/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.