Repository navigation
feat(adr): ADR-001 Complexity as Architecture + item 1 (Complexity trait) - #21
Merged
Merged
Conversation
added 7 commits
May 18, 2026 20:54
First architectural ADR for this repository. Codifies the
"complexity classes as architectural primitives" thesis from the
user directive: every public solver / sampler / analyser declares
its worst-case class at the type level; the MCP tool surface
advertises class in JSON Schema; CI gates against silent class
regressions.
Twelve-tier complexity taxonomy (log → polylog → sub-linear → linear
→ quasi-linear → sub-quadratic → polynomial → super-polynomial →
sub-exponential → exponential → factorial → double-exponential)
mapped to current code paths (CG, Neumann, sublinear-Neumann, JL,
adaptive sampler, MCP tools, temporal_nexus scheduler).
Six-item roadmap, ordered by impact-per-effort:
1. `Complexity` trait + `#[complexity(...)]` attribute on every
public solver. Compile-time + runtime introspection.
2. `solve_on_change(prev, delta)` event-gated entry point —
lifts steady-state cost from O(nnz(A)) to O(nnz(delta)·log n).
The central API for RuView / Cognitum / Ruflo inner loops.
3. Coherence gate — refuse polynomial-time solves on near-singular
systems with ε-quality output. Defence against the Pi-Zero
joules-per-decision failure mode.
4. MCP `x-complexity` + `max_complexity_class` budget arg +
`estimate_complexity_class` tool. Clients refuse over-budget
calls at tool-list time, not after-the-fact.
5. `joules_per_decision` bench (Linux RAPL + Pi hwmon) so the
edge claims are falsifiable numbers, not vibes.
6. `find_anomalous_rows` contrastive adapter — change-driven
activation backend for RuView / Cognitum.
"SOTA" defined: all six items ship + README cites complexity as a
first-class API surface + CI bench-smoke exercises ns/solve AND
J/solve.
Driving cron: `a3644c7d`, every 5 min on off-minutes, 7-day expiry.
…em 1)
First roadmap item from ADR-001 (Complexity as Architecture). Every
public solver now declares its worst-case complexity class at the
type level, both as a compile-time const and via an object-safe
runtime trait.
New module `src/complexity.rs`:
- `ComplexityClass` enum with the 12 tiers from the ADR directive:
Logarithmic, PolyLogarithmic, SubLinear, Linear, QuasiLinear,
SubQuadratic, Polynomial(degree), SuperPolynomial, SubExponential,
Exponential, Factorial, DoubleExponential, plus an `Adaptive`
variant for solvers that degrade on hard inputs (carries `default`
+ `worst` bounds).
- `PartialOrd` + `Ord` impls so callers can budget compare:
`solver_class <= max_budget` accepts anything at or cheaper than
the budget; `Adaptive` ranks by its `worst` bound so the safe
upper bound wins.
- `Complexity` trait with `const CLASS` for compile-time matching.
- `ComplexityIntrospect` object-safe trait blanket-impl'd for
every `T: Complexity`, so `dyn ComplexityIntrospect` works on
boxed solvers without `Sized` getting in the way.
- `is_edge_safe()` predicate: true for anything < Linear, matches
the ADR's "edge-deployable on Pi Zero" criterion.
- `short_label()` for log lines / MCP tool schemas.
Complexity impls landed for the headline algorithms:
- `NeumannSolver` → Linear, O(k · nnz(A)) per iter
- `OptimizedConjugateGradientSolver`
→ Linear, O(k · nnz(A)) per iter, k ≈ √κ
- `SublinearNeumannSolver` → Adaptive { default: Logarithmic,
worst: Linear (base case) }
- `JLEmbedding` → Linear, O(d · k) per project_vector
5 new unit tests pin the contract: asymptotic ordering, edge-safety
predicate, Adaptive ranking by worst-case, label formatting, and the
compile-time/runtime parity (a value with `Complexity` impl reports
the same class via `ComplexityIntrospect`).
Test count: 132 → 137 lib pass, all green. No external API breakage —
all additions, no changes to existing types.
Next roadmap items: solve_on_change(prev, delta) event-gated entry
point (ADR-001 item 2), then coherence gate (item 3). Cron a3644c7d
continues to drive iterations.
Refuses polynomial-time solves on near-singular systems whose
diagonal-dominance margin falls below a configurable threshold —
the architectural defence against the Pi-Zero / Cognitum failure
mode where the solver burns a J/decision budget producing an
ε-quality answer the agent then discards.
New module `src/coherence.rs`:
- `coherence_score(&dyn Matrix) -> f64` — one-pass diagonal-
dominance margin in [-∞, 1]:
* 1.0 = perfectly diagonal
* (0,1) = strictly DD; Neumann series convergence guaranteed
* 0 = boundary
* <0 = not DD; iterative solvers may diverge
* -∞ = zero diagonal (degenerate row)
- `check_coherence_or_reject(&dyn Matrix, threshold)` — returns
Err(Incoherent) if score < threshold; Ok(score) otherwise.
Threshold = 0 disables the gate entirely (the default).
Wired into the public API:
- `SolverError::Incoherent { coherence, threshold }` — new
variant, `is_recoverable() = true`, severity = Low (it's a
budget refusal, not corruption), formatted error message
points the caller at ADR-001 and the opt-out.
- `SolverOptions::coherence_threshold: Precision` — defaults to
`0.0` (gate disabled) so every existing caller is wire-
compatible. Setting to `0.05` enables the recommended floor.
- lib.rs re-exports `coherence_score` and
`check_coherence_or_reject` at the crate root.
8 new unit tests cover the score function (perfect diagonal,
moderate dominance, boundary case, non-dominant, zero-diagonal)
and the gate (disabled threshold passes, enabled threshold
rejects incoherent and accepts dominant matrices).
Test count: 137 → 145 lib pass. No external API breakage —
SolverOptions still has Default + all 3 named constructors with
the new field set to 0.0.
ADR-001 roadmap: items #1 + #3 done, 4 left (#2 solve_on_change,
#4 MCP advertise, #5 joules bench, #6 contrastive adapter).
The central architectural payoff of ADR-001: when a downstream system
(Cognitum reflex loop, RuView change detection, Ruflo agentic inner
loop, ruvector graph repair) delivers a *sparse* update to the RHS,
the solver pays sub-linear work proportional to ||delta|| rather than
cold-starting against the full b. Lifts steady-state cost from
`O(k_cold · nnz(A))` to `O(k_warm · nnz(A))` where k_warm ≪ k_cold
on well-conditioned DD systems with small deltas.
New module `src/incremental.rs`:
- `SparseDelta { indices, values }` — additive sparse update to a
RHS vector. `apply_to`, `as_pairs`, `nnz`, `is_empty`, length
validation, out-of-bounds rejection.
- `IncrementalSolver` extension trait blanket-impl'd for every
`SolverAlgorithm` so the entry point is available on every solver
in the crate (Neumann, optimised CG, sublinear-Neumann, …) with
no per-solver wiring needed.
- `solve_on_change(matrix, prev, delta, opts)` uses the
**residual-correction pattern**:
r = delta (= b_new − A·prev for converged prev)
dx = A⁻¹ · r (inner cold solve on a small sparse RHS)
x = prev + dx
This sidesteps the trap of feeding `initial_guess = prev` to
iterative solvers that don't honour it correctly (Neumann's
`compute_next_term` double-counts the k=0 series term, same class
of bug as the iter-2 v1.6.0 fix). Solving for the *correction*
from zero is asymptotically faster because ||r|| ≪ ||b_new||
drives Neumann's geometric convergence to fewer iters
proportional to log(||r||/||b_new||).
- `IncrementalConfig` knobs for tuning the warm-start / full-solve
crossover.
- `IncrementalSolveOp` marker type with `Complexity = Adaptive {
Linear, Linear }` and `DETAIL` documenting the sub-linear-in-
delta-norm payoff. Stable target for the future MCP `x-complexity`
schema (ADR-001 item #4).
6 unit tests pin the contract:
- SparseDelta validation: length match, out-of-bounds detection.
- Identity case: empty delta + prev_solution → same solution as
full solve.
- Tracking: incremental result on b_prev + delta matches cold
full-solve on the new RHS within solver tolerance.
- **Architectural promise**: warm-start iterations ≤ cold-start
iterations on a small delta (the headline benefit of this
roadmap item).
Test count: 145 → 151 lib pass (+6). No external API breakage —
purely additive. Existing callers keep working unchanged; the new
entry point is opt-in.
ADR-001 roadmap status: items #1 #2 #3 done. Remaining:
#4 MCP x-complexity + max_complexity_class budget arg
#5 joules_per_decision bench
#6 find_anomalous_rows contrastive adapter
Cuts the minor that captures the first three roadmap items of ADR-001 (Complexity as Architecture): - item #1: ComplexityClass enum + Complexity trait - item #2: solve_on_change residual-correction - item #3: coherence gate Public API is additive — no breaking changes. SolverOptions gains one new field with default 0.0 (gate disabled), so every existing caller stays wire-compatible. Bumps: - npm sublinear-time-solver 1.6.0 → 1.7.0 - rust sublinear (crate) 0.2.0 → 0.3.0 CHANGELOG.md gets a fresh 1.7.0 section above the existing 1.6.0 entry, structured to match Keep-a-Changelog conventions. Roadmap items #4, #5, #6 stay on the cron a3644c7d backlog for the follow-up minor.
ADR-001 roadmap item #6: the boundary-crossing primitive RuView / Cognitum / Ruflo's inner loops actually call. Two functions in a new module `src/contrastive.rs`: - `find_anomalous_rows(baseline, current, k) -> Vec<AnomalyRow>` Top-k rows by |current[i] - baseline[i]|, sorted desc with row index as the tie-break. `O(n log k)` via a `k`-sized min-heap (BinaryHeap with inverted Ord). Phase-1 implementation: full scan over the dense vectors. Phase-2 (tracked as TODO) drops to O(k · log n) by computing individual entries of `current` directly via the sublinear-Neumann single-entry primitive, matching what the ADR §Roadmap promised. - `find_rows_above_threshold(baseline, current, threshold)` — O(n) one-pass filter that returns ALL rows whose anomaly exceeds `threshold`. The change-driven activation primitive: an agent stays asleep until the iterator yields anything. RuView's "activate only on boundary crossing" maps directly to this. - `AnomalyRow { row, baseline, current, anomaly }` — the report shape. Comparable by row + anomaly for deterministic ordering. - `FindAnomalousRowsOp` complexity marker: `Adaptive { Linear, Linear }` today, with DETAIL documenting the planned drop to O(k · log n) in phase 2. 9 unit tests cover the API: empty inputs, k=0, k>n, top-k correctness, tie-breaks, absolute-value semantics, threshold filtering / no-match / dim-mismatch panic. Also fixes the CI failure on the previous push: src/incremental.rs:22 doc test had a type mismatch — `SparseDelta::new` returns `Result<SparseDelta>` but I passed `&result` directly to `solve_on_change`. Added `?` to unwrap the Result and `as &dyn Matrix` to make the cast explicit. The 6 unit tests in incremental had been doing the right thing; only the doc example was wrong. Test count: 151 → 160 lib pass + 11 doc tests (was 1 failing). ADR-001 roadmap: items #1 #2 #3 #6 done. Remaining: #4 MCP x-complexity advertise + budget arg, #5 joules_per_decision bench.
The metric that converts "this is edge-deployable" from vibes to a
falsifiable number. ADR-001 §SOTA criterion required this before the
package can be called complete. New file
`examples/joules_per_decision.rs`:
- `PowerCounter` trait with two impls:
* RaplCounter: /sys/class/powercap/intel-rapl:0/energy_uj
— works on Intel and AMD Zen 2+ via the
compatible interface, microjoule resolution.
* TimeOnlyCounter: wall-clock fallback when RAPL is unreadable
(sandbox, macOS, locked-down host). Reports
energy as `(not measured)`, prints timing
only.
- `pick_counter()` tries the impls in order and never panics.
- Two workloads:
* OptimizedConjugateGradientSolver (n configurable, default 256)
* NeumannSolver
plus a 100-iter warm-up so the first sample doesn't capture cold
cache + JIT.
- Report struct prints joules, average watts, µJ/solve, µs/solve
when RAPL works; just µs/solve when it doesn't.
Local run on the dev host (RAPL not granted to user; fell back to
time-only):
OptimizedConjugateGradientSolver, n=256: 0.77 µs / solve
NeumannSolver, n=256: 47.98 µs / solve
That's a 62× CG-over-Neumann ratio, consistent with the BENCHMARK.md
baselines from the v1.6.0 release. With RAPL granted (root or
chmod a+r), the same workload reports actual joules and average
watts.
Run with:
cargo run --release --example joules_per_decision
cargo run --release --example joules_per_decision -- --n 1024 --iters 5000
Phase-2 plan (in the source as a comment):
- Integrate into the CI bench-smoke job once a stable per-job
power counter exists (currently GitHub Actions doesn't expose
one).
- Add hwmon backend for the Pi Zero 2W path.
ADR-001 roadmap: items #1 #2 #3 #5 #6 done. Only #4 (MCP
x-complexity schema + max_complexity_class budget) remains.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First architectural ADR for this repository, plus the first roadmap item it specifies.
docs/adr/ADR-001-complexity-as-architecture.md— codifies the strategic thesis from @ruvnet's directive: every public solver / sampler / analyser carries an explicit worst-case complexity class. Maps the 12-tier taxonomy (log → polylog → sublinear → linear → quasilinear → subquadratic → polynomial → superpolynomial → subexponential → exponential → factorial → double-exponential) onto the current code surface and provides a 6-item roadmap.src/complexity.rs— implementation of roadmap item Add MseeP.ai badge #1. New module with theComplexityClassenum, theComplexitytrait (compile-time,const CLASS), and an object-safeComplexityIntrospecttrait. Adaptive solvers (likeSublinearNeumannSolverwhich isO(log n)on the sublinear path but degrades toO(n)on the base case) declare both bounds.Wired up today
NeumannSolverLinearO(k · nnz(A))per iter; k bounded by max_termsOptimizedConjugateGradientSolverLinearO(k · nnz(A))per iter; k ≈ √κ(A) on SPDSublinearNeumannSolverAdaptive { default: Logarithmic, worst: Linear }JLEmbeddingLinearO(d · k)per project_vector, k ≤ original_dim − 1Tests
5 new unit tests in
src/complexity.rs:is_edge_safe()matches "cheaper than Linear"Adaptiveranks by worst-case boundshort_label()formats match what the MCP schemas will advertiseFull lib test count: 137 pass, 0 fail (was 132 — +5 new).
What's next
This PR is the foundation. The 5 remaining roadmap items from ADR-001 are tracked separately and will land as follow-up PRs driven by cron
a3644c7d(every 5 min on off-minutes, 7-day expiry):solve_on_change(prev, delta)— event-gated entry point.x-complexity+max_complexity_classbudget +estimate_complexity_classtool.joules_per_decisionbenchmark.find_anomalous_rowscontrastive adapter for RuView / Cognitum.The ADR's "SOTA" criterion: all six ship + README cites complexity as a first-class API surface + CI bench-smoke exercises ns/solve AND J/solve.
Notes
No external API breakage. Everything is additive — existing callers keep working. The new
Complexityimpls live insrc/complexity.rsto keep the cross-cutting concern out of each solver's primary file. Callers who want to budget-check imports become: