CI failure causal decision engine — answers 'did this code change cause the failure?'
Project description
RootLens — Causal Decision Engine for CI Failure Triage
This is not a log analyzer. It is a decision system based on causal reasoning.
Most CI systems answer: "What error happened?" RootLens answers: "Can this failure be caused by the code change?"
- If provably NO →
CLOSE(zero blame) - If provably YES →
TRUE_REJECT(100% blame with evidence) - If unknown →
ESCALATE(refuse to guess)
The Problem
30–40% of CI failures are infrastructure issues (disk full, network timeout, node crash) that get misattributed to code changes. This causes:
- Developers waste time investigating failures they didn't cause
- Real bugs get delayed because triage is overwhelmed with false signals
- Trust in the CI system erodes
RootLens eliminates false blame through deterministic causal reasoning.
Impact (Internal Validation)
Validated on an internal production CI system (dataset not publicly available):
| Metric | Value |
|---|---|
| Internal dataset | 159 labeled CI failure cases |
| Decision precision | ~100% (deterministic subset) |
| Infrastructure false-blame | reduced from 30–40% to 0% |
| Auto-resolution coverage | 60–70% of cases |
| Remainder | Safely escalated to human review |
Reproducible benchmark (included): 41 cases, 100% accuracy, 0% false blame. See benchmarks/.
Architecture
┌─────────────┐
│ Build Log │
└──────┬──────┘
│
┌──────▼──────┐
│ Q1: State │──── MERGED/ABANDONED → CLOSE
└──────┬──────┘
│ open
┌──────▼──────┐
│ Q1.5: Infra │──── causal impossibility → CLOSE + proof
│ (10 rules) │
└──────┬──────┘
│ not infra
┌──────▼──────┐
│ Q2: Compile │──── error ∩ changed_files → TRUE_REJECT
│ + file match│
└──────┬──────┘
│ no match
┌──────▼──────┐
│ ESCALATE │──── "I don't know"
└─────────────┘
Three Layers
| Layer | Purpose | Output |
|---|---|---|
| Q1 | Change state filter | CLOSE if merged/abandoned |
| Q1.5 | Causal negation (proof of impossibility) | CLOSE + causal_proof |
| Q2 | Causal confirmation (file intersection) | TRUE_REJECT with evidence |
Why Three Classes, Not Two?
Binary (blame/no-blame) forces guessing when evidence is insufficient:
- False blame → developer wastes time on infra issues
- False acquittal → real bugs escape review
ESCALATE is not failure — it is honest admission that causality cannot be established.
Q1.5: The Core Innovation
Q1.5 is NOT a pattern matcher. Each rule must prove:
"No plausible code change can be on the causal chain for this failure."
Each rule contains:
pattern— what to matchcounterexample— why no code change can trigger thiscausal_proof— formal reasoning for the decision
See docs/q1_5_rules.md for all 10 rules.
Quick Start
pip install rootlens
python examples/demo.py
Or from source:
git clone https://github.com/zhangbin0027/rootlens.git
cd rootlens
pip install -e .
python examples/demo.py
Demo Output
[Scenario 1] Infrastructure Failure — Disk Full
decision: CLOSE
reason: Infrastructure failure: DISK_FULL
confidence: 1.0
proof: Disk capacity is a build node resource constraint...
rule_name: DISK_FULL
matched_signal: No space left on device
[Scenario 2] Compilation Error — File Intersection Match
decision: TRUE_REJECT
reason: Compilation error in changed file: src/auth/login_handler.cpp
confidence: 1.0
rule_name: Q2_SMOKING_GUN
matched_signal: error: cannot find symbol
[Scenario 3] Repo Sync Failure — Pre-Apply
decision: CLOSE
reason: Infrastructure failure: PRE_APPLY_FAILURE
confidence: 1.0
proof: Error in repo initialization phase, before code checkout...
rule_name: PRE_APPLY_FAILURE
matched_signal: MANIFEST_INIT_ERROR
Benchmark
python benchmarks/evaluate.py
Results (41 cases, adversarial included):
| Metric | RootLens | Naive Baseline |
|---|---|---|
| Accuracy | 100% | ~66% |
| False Blame Rate | 0% | ~15% |
Project Structure
rootlens/
├── README.md
├── requirements.txt
├── docs/
│ ├── decision-model.md
│ └── q1_5_rules.md
├── src/
│ └── engine/
│ ├── models.py # BuildContext, DecisionResult, RuleVerdict, ErrorSignal
│ ├── q1_5.py # 10 hardcoded causal rules (Python, NOT YAML)
│ ├── classifier.py # YAML pattern loader → ErrorSignal (no decisions)
│ ├── decision.py # Q1→Q1.5→Q2 enforced order
│ └── pipeline.py # analyze() public API
├── configs/
│ └── error_patterns.yaml
├── examples/
│ ├── logs/
│ │ ├── compile_error.log
│ │ ├── infra_failure.log
│ │ └── repo_sync_failure.log
│ └── demo.py
└── benchmarks/
├── dataset.json # 41 cases with adversarial samples
└── evaluate.py # baseline comparison
API Usage
from rootlens import analyze
result = analyze(
build_log="error: No space left on device",
changed_files=["src/main.cpp"],
)
print(result.decision) # "CLOSE"
print(result.confidence) # 1.0
print(result.proof) # "Disk capacity is a build node resource..."
Parameters
| Parameter | Required | Description |
|---|---|---|
build_log |
Yes | Raw build log text (must be non-empty string) |
changed_files |
No | Files modified by the change |
change_status |
No | "open" (default), "merged", or "abandoned" |
change_count |
No | Number of changes in batch (default 1) |
build_stage |
No | Informational only — not used in decision logic |
Design Principles
- Q1.5 is Python code, not YAML — causal rules require logic, not patterns
- Classifier never decides — it returns signals, decisions are in decision.py
- Gate order is hardcoded — Q1→Q1.5→Q2 cannot be reordered
- Empty input = safe default — missing changed_files → ESCALATE, not silent failure
- Zero false blame — the system will ESCALATE rather than wrongly blame
- Exact path matching — Q2 uses exact path comparison only. If your CI logs report absolute paths but changed_files are relative, Q2 will ESCALATE (safe default). Normalize paths before input for best coverage.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rootlens-0.1.0.tar.gz.
File metadata
- Download URL: rootlens-0.1.0.tar.gz
- Upload date:
- Size: 23.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f1ee2eba900f621e269472b2eddc4d5521f956342a7519cf2dc9a16472bfcfa
|
|
| MD5 |
779bb1d295f26e350041c428a20b0d18
|
|
| BLAKE2b-256 |
29a878a7fcf77f26c8806627b3b2397d3a3fd6a7e81db668ac9c6b9072227013
|
Provenance
The following attestation bundles were made for rootlens-0.1.0.tar.gz:
Publisher:
publish.yml on zhangbin0027/rootlens
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rootlens-0.1.0.tar.gz -
Subject digest:
8f1ee2eba900f621e269472b2eddc4d5521f956342a7519cf2dc9a16472bfcfa - Sigstore transparency entry: 1522522556
- Sigstore integration time:
-
Permalink:
zhangbin0027/rootlens@46e9b310b2ab391578b757728b4ad49cbe686f95 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/zhangbin0027
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@46e9b310b2ab391578b757728b4ad49cbe686f95 -
Trigger Event:
release
-
Statement type:
File details
Details for the file rootlens-0.1.0-py3-none-any.whl.
File metadata
- Download URL: rootlens-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
68801bd02f2c7a530b1f9fc23a89c9e4881ff644734b6ead787fc84a324c2efe
|
|
| MD5 |
50a21390fb78338611ca2aceb38d2beb
|
|
| BLAKE2b-256 |
7b942eb9e5114379562418fd44a3143941625d89c52b61e9872c07b59a6f3318
|
Provenance
The following attestation bundles were made for rootlens-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on zhangbin0027/rootlens
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rootlens-0.1.0-py3-none-any.whl -
Subject digest:
68801bd02f2c7a530b1f9fc23a89c9e4881ff644734b6ead787fc84a324c2efe - Sigstore transparency entry: 1522522605
- Sigstore integration time:
-
Permalink:
zhangbin0027/rootlens@46e9b310b2ab391578b757728b4ad49cbe686f95 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/zhangbin0027
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@46e9b310b2ab391578b757728b4ad49cbe686f95 -
Trigger Event:
release
-
Statement type: