Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion fixos/diagnostics/disk_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ def analyze_disk_usage(self, path: str = None) -> Dict[str, Any]:
total_gb = stat.total / (1024**3)
used_gb = stat.used / (1024**3)
free_gb = stat.free / (1024**3)
usage_percent = (used_gb / total_gb) * 100
if stat.total <= 0:
raise ValueError("Filesystem reports no capacity")
# Reserved blocks are unavailable to ordinary processes and must
# not make their remaining capacity appear healthier.
user_capacity = stat.used + stat.free
usage_percent = 100.0 * stat.used / user_capacity if user_capacity else 100.0

large_files = self.get_large_files(path)
cache_dirs = self.get_cache_dirs(path)
Expand Down
47 changes: 47 additions & 0 deletions project/ticket-114/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Ticket 114: Classify disk pressure using capacity available to services

- **ID**: ticket-114
- **Owner**: agent:codex
- **Status**: IN_PROGRESS
- **Workflow state**: PUBLICATION
- **Created**: 2026-10-31

## Goal and scope

The live root filesystem has about 2.5% available capacity, but DiskAnalyzer
reports 94% usage and only warning severity because its denominator includes
capacity reserved from ordinary processes. Calculate utilization using used
bytes plus available bytes while preserving raw size fields and thresholds.

SESSION_EXECUTION_AUTHORIZATION: the user requested continuing FixOS repairs
and merges on 2026-10-31. This bounded correction includes independent protected
publication and deployment into the two existing host installations. It grants
no filesystem-reserve change, deletion of unknown data or approval bypass.

## Acceptance criteria

- [x] AC-01: Reserved capacity cannot mask critical or exhausted application
capacity; unchanged ordinary and empty-filesystem cases remain explicit.
- [ ] AC-02: Affected tests, full required checks and managed governance pass.
- [ ] AC-03: Independent exact-head publication and deployed verification pass.

## Validation

Eight capacity cases cover reserved-block pressure, exhausted available space,
ordinary utilization, an unused filesystem, and an invalid zero-capacity
observation. Four failed against the baseline with warning/healthy severity
instead of critical. All 15 affected tests now pass, including Testless coverage
collection. Changed-file Ruff and whitespace checks passed. The complete suite,
required Python matrix, protected review and deployment are publication work.

The initial XS contract mistakenly estimated 20 active implementation minutes;
the adopted limit is ten. The remaining two-file implementation was bounded to
ten minutes without widening its class, scope or budgets. The owner released
the earlier editing reservation and reacquired a real fenced lease bound to
the corrected intent. External controller receipts retain both plan hashes.

The first full-suite run exposed a pre-existing live BuildKit dependency in a
proposal unit fixture. Independently reviewed ticket-115 / PR #147 fixed that
fixture. This ticket was reconciled onto its merged base with an updated intent
and a fresh controller lease; capacity implementation and regression tests
remain byte-for-byte unchanged. The resulting head requires fresh validation.
76 changes: 76 additions & 0 deletions project/ticket-114/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-114",
"summary": "Classify disk pressure using capacity available to services",
"workstream": "application",
"classification": {
"kind": "BUG",
"priority": "P1",
"origin": "requested"
},
"allowedPaths": [
"project/ticket-114/**",
"fixos/diagnostics/disk_analyzer.py",
"tests/unit/test_disk_analyzer_capacity.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [
"python"
],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "1a03305fb891c0e643a57018676af1364c821ce9",
"targetBranch": "main",
"outcome": "Independently publish and deploy disk-pressure classification based on capacity available to ordinary processes, so reserved blocks cannot hide critical pressure.",
"nonGoals": [
"No filesystem reserve changes or host data deletion",
"No cleanup scope, threshold or service-policy changes",
"No scan traversal or MCP interface changes"
],
"complexity": "XS",
"estimatedMinutes": 10,
"budgets": {
"maxImplementationFiles": 2,
"maxAffectedComponents": 1,
"maxPublicInterfaceChanges": 0,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Compute utilization from used bytes plus bytes available to ordinary users, retaining raw capacity fields and existing severity thresholds.",
"components": [
{
"name": "disk-analyzer",
"paths": [
"fixos/diagnostics/disk_analyzer.py",
"tests/unit/test_disk_analyzer_capacity.py"
]
}
],
"responsibilityChanges": false,
"interfaceChanges": [],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "Revert through protected delivery and reinstall the prior reviewed wheel."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"python -m pytest tests/unit/test_disk_analyzer_capacity.py tests/unit/test_disk_analyzer.py -q",
"./project/governance-check.sh"
],
"evidence": "project/ticket-114/README.md"
}
]
}
}
66 changes: 66 additions & 0 deletions tests/unit/test_disk_analyzer_capacity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Disk severity must reflect capacity available to ordinary processes."""

from types import SimpleNamespace

import pytest

from fixos.diagnostics.disk_analyzer import DiskAnalyzer

GIB = 1024**3


@pytest.fixture
def analyzer(tmp_path, monkeypatch):
instance = DiskAnalyzer(str(tmp_path))
for method in (
"get_large_files",
"get_cache_dirs",
"get_log_dirs",
"get_temp_dirs",
"suggest_cleanup_actions",
):
monkeypatch.setattr(instance, method, lambda *args, **kwargs: [])
return instance


@pytest.mark.parametrize(
("used", "available", "expected_percent", "expected_status"),
[
(90, 1, 98.90, "critical"),
(94, 2, 97.92, "critical"),
(90, 0, 100.0, "critical"),
(0, 0, 100.0, "critical"),
(90, 10, 90.0, "warning"),
(50, 50, 50.0, "healthy"),
(0, 100, 0.0, "healthy"),
],
)
def test_capacity_available_to_processes_controls_severity(
analyzer, monkeypatch, used, available, expected_percent, expected_status
):
monkeypatch.setattr(
"shutil.disk_usage",
lambda path: SimpleNamespace(
total=100 * GIB, used=used * GIB, free=available * GIB
),
)

report = analyzer.analyze_disk_usage()

assert report["status"] == expected_status
assert report["usage_percent"] == expected_percent
assert report["total_gb"] == 100
assert report["used_gb"] == used
assert report["free_gb"] == available


def test_zero_capacity_is_reported_as_an_error(analyzer, monkeypatch):
monkeypatch.setattr(
"shutil.disk_usage",
lambda path: SimpleNamespace(total=0, used=0, free=0),
)

report = analyzer.analyze_disk_usage()

assert "error" in report
assert "status" not in report
Loading