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
6 changes: 3 additions & 3 deletions .recursive/architecture/MODULE_MAP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Module Map

Last updated: 2026-04-09 by session #0089
Last updated: 2026-04-09 by session #0092
Generated via: `python3 -m nightshift module-map --write`
Stale after: 5 newer sessions without a refresh

Expand All @@ -17,7 +17,7 @@ Read it before opening modules one by one when you need fast orientation.
| `core/constants.py` | 847 | Module-level constants and tiny utilities used across the package. | `now_local`, `print_status`, `DATA_VERSION`, `SUPPORTED_AGENTS` | PR #269 (2e91d5f) |
| `raven/summary.py` | 141 | Feature summary generation for Loop 2 build output. | `generate_feature_summary`, `_API_DIR_SEGMENTS`, `_CLI_DIR_SEGMENTS`, `_CONFIG_DIR_SEGMENTS` | 1636b72 |
| `core/shell.py` | 256 | Subprocess execution: streaming runner, git helper, shell utilities. | `run_command`, `run_capture`, `git`, `command_exists` | PR #269 (2e91d5f) |
| `core/state.py` | 229 | Shift state: read, write, mutate counters, JSON I/O. | `load_json`, `write_json`, `read_state`, `top_path` | PR #267 (65f423d) |
| `core/state.py` | 237 | Shift state: read, write, mutate counters, JSON I/O. | `load_json`, `write_json`, `read_state`, `top_path` | session #0092 |
| `owl/readiness.py` | 234 | Production-readiness checks for Loop 2 feature builds. | `collect_changed_files`, `check_secrets`, `check_debug_prints`, `check_test_coverage` | PR #204 (df36eff) |
| `raven/coordination.py` | 196 | Sub-agent coordination for Loop 2 -- detects file overlaps and generates hints. | `extract_file_references`, `detect_overlaps`, `generate_coordination_hints`, `inject_hints` | PR #229 (c2acba2) |
| `infra/module_map.py` | 473 | Generate a persistent module map for fast cross-session orientation. | `module_map_path`, `generate_module_map`, `render_module_map`, `write_module_map` | PR #251 (c32e527) |
Expand All @@ -29,7 +29,7 @@ Read it before opening modules one by one when you need fast orientation.
| `owl/eval_runner.py` | 698 | Evaluation runner: score nightshift against a target repo (or dry-run with synthetic data). | `score_artifacts`, `format_eval_table`, `run_eval_dry_run`, `run_eval_full` | PR #269 (2e91d5f) |
| `raven/e2e.py` | 113 | End-to-end test runner for Loop 2 feature builds. | `infer_test_command`, `detect_smoke_test`, `run_e2e_tests`, `_MAKEFILE_TEST_TARGET` | 1636b72 |
| `raven/profiler.py` | 547 | Repo profiling for Loop 2 -- detects language, framework, dependencies, structure. | `profile_repo` | PR #220 (d9e4320) |
| `owl/cycle.py` | 987 | Per-cycle logic: prompt building, agent dispatch, verification, evaluation. | `extract_json`, `read_repo_instructions`, `wrap_repo_instructions`, `command_for_agent` | session #0089 |
| `owl/cycle.py` | 974 | Per-cycle logic: prompt building, agent dispatch, verification, evaluation. | `extract_json`, `read_repo_instructions`, `wrap_repo_instructions`, `command_for_agent` | PR #267 (65f423d) |
| `raven/planner.py` | 483 | Feature planner for Loop 2 -- builds structured plans from repo profiles. | `build_plan_prompt`, `validate_plan`, `parse_plan`, `execution_order` | 1636b72 |
| `raven/subagent.py` | 281 | Sub-agent spawner for Loop 2 -- executes work orders via codex or claude CLI. | `spawn_task`, `spawn_wave`, `format_wave_result`, `_TASK_COMPLETION_REQUIRED_KEYS` | 1636b72 |
| `raven/decomposer.py` | 175 | Task decomposer for Loop 2 -- converts FeaturePlans into sub-agent work orders. | `build_work_order_prompt`, `decompose_plan`, `format_work_orders` | 1636b72 |
Expand Down
4 changes: 2 additions & 2 deletions .recursive/tasks/0273.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
status: pending
status: done
priority: low
target: v0.0.8
vision_section: loop1
source: code-review
created: 2026-04-09
completed:
completed: 2026-04-09
---

# Guard int() calls in _build_state counters block
Expand Down
26 changes: 17 additions & 9 deletions nightshift/core/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import json
import math
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -37,6 +38,13 @@ def write_json(path: Path, payload: dict[str, Any] | ShiftState | FeatureState)
_REQUIRED_STATE_KEYS = {"version", "date", "branch", "agent", "baseline", "counters", "cycles"}


def _load_counter_value(value: object) -> int:
"""Convert a counter value to int, falling back to zero for bad data."""
if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value):
return int(value)
return 0


def _build_state(raw: dict[str, Any], state_path: Path) -> ShiftState:
"""Construct a ShiftState from a validated raw dict, field by field."""
baseline_raw = raw.get("baseline", {})
Expand All @@ -52,14 +60,14 @@ def _build_state(raw: dict[str, Any], state_path: Path) -> ShiftState:
message=str(baseline_raw.get("message", "")),
)
counters = Counters(
fixes=int(counters_raw.get("fixes", 0)),
issues_logged=int(counters_raw.get("issues_logged", 0)),
files_touched=int(counters_raw.get("files_touched", 0)),
low_impact_fixes=int(counters_raw.get("low_impact_fixes", 0)),
failed_verifications=int(counters_raw.get("failed_verifications", 0)),
empty_cycles=int(counters_raw.get("empty_cycles", 0)),
agent_failures=int(counters_raw.get("agent_failures", 0)),
tests_written=int(counters_raw.get("tests_written", 0)),
fixes=_load_counter_value(counters_raw.get("fixes", 0)),
issues_logged=_load_counter_value(counters_raw.get("issues_logged", 0)),
files_touched=_load_counter_value(counters_raw.get("files_touched", 0)),
low_impact_fixes=_load_counter_value(counters_raw.get("low_impact_fixes", 0)),
failed_verifications=_load_counter_value(counters_raw.get("failed_verifications", 0)),
empty_cycles=_load_counter_value(counters_raw.get("empty_cycles", 0)),
agent_failures=_load_counter_value(counters_raw.get("agent_failures", 0)),
tests_written=_load_counter_value(counters_raw.get("tests_written", 0)),
)
cycles_raw = raw.get("cycles", [])
if not isinstance(cycles_raw, list):
Expand All @@ -71,7 +79,7 @@ def _build_state(raw: dict[str, Any], state_path: Path) -> ShiftState:
category_counts: dict[str, int] = {
k: int(v)
for k, v in raw_category_counts.items()
if isinstance(k, str) and k in _VALID_CATEGORIES and isinstance(v, (int, float))
if isinstance(k, str) and k in _VALID_CATEGORIES and isinstance(v, (int, float)) and math.isfinite(v)
}

return ShiftState(
Expand Down
94 changes: 92 additions & 2 deletions nightshift/tests/test_nightshift.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,8 +688,8 @@ def test_corrupt_category_counts_values_dropped_silently(self, tmp_path):

Task #0266: int(v) in the category_counts comprehension has no guard.
A state file with {"Security": "not-a-number"} or {"Security": null}
would previously raise ValueError/TypeError. The isinstance check
ensures corrupt values are silently skipped, consistent with how
would previously raise ValueError/TypeError. Non-finite floats are also
skipped, so corrupt values are silently dropped, consistent with how
unknown keys are already silently dropped.
"""
state_path = tmp_path / "state.json"
Expand All @@ -715,6 +715,8 @@ def test_corrupt_category_counts_values_dropped_silently(self, tmp_path):
"Error Handling": None,
"A11y": [],
"Performance": "not-a-number",
"Docs": float("nan"),
"Polish": float("inf"),
},
"recent_cycle_paths": [],
"cycles": [],
Expand All @@ -736,6 +738,8 @@ def test_corrupt_category_counts_values_dropped_silently(self, tmp_path):
assert "Error Handling" not in state["category_counts"]
assert "A11y" not in state["category_counts"]
assert "Performance" not in state["category_counts"]
assert "Docs" not in state["category_counts"]
assert "Polish" not in state["category_counts"]

def test_valid_category_counts_load_correctly(self, tmp_path):
"""Valid numeric category_counts survive the sanitization pass intact.
Expand Down Expand Up @@ -780,6 +784,92 @@ def test_valid_category_counts_load_correctly(self, tmp_path):
)
assert state["category_counts"] == {"Security": 3, "Tests": 2, "Code Quality": 1}

def test_corrupt_counter_values_fall_back_to_zero(self, tmp_path):
"""Corrupt counter values must not crash read_state and fall back to zero."""
state_path = tmp_path / "state.json"
existing = {
"version": 1,
"date": "2026-04-09",
"branch": "nightshift/2026-04-09",
"agent": "claude",
"baseline": {"status": "passed", "command": None, "message": ""},
"counters": {
"fixes": "bad",
"issues_logged": None,
"files_touched": ["oops"],
"low_impact_fixes": {"count": 1},
"failed_verifications": "7",
"empty_cycles": float("inf"),
"agent_failures": [],
"tests_written": "many",
},
"recent_cycle_paths": [],
"cycles": [],
"halt_reason": None,
"log_only_mode": False,
}
state_path.write_text(json.dumps(existing))
state = nightshift.read_state(
state_path,
today="2026-04-09",
branch="nightshift/2026-04-09",
agent="claude",
verify_command=None,
)
assert state["counters"] == {
"fixes": 0,
"issues_logged": 0,
"files_touched": 0,
"low_impact_fixes": 0,
"failed_verifications": 0,
"empty_cycles": 0,
"agent_failures": 0,
"tests_written": 0,
}

def test_valid_counter_values_load_correctly(self, tmp_path):
"""Valid numeric counter values must still load and normalize."""
state_path = tmp_path / "state.json"
existing = {
"version": 1,
"date": "2026-04-09",
"branch": "nightshift/2026-04-09",
"agent": "claude",
"baseline": {"status": "passed", "command": None, "message": ""},
"counters": {
"fixes": 5.0,
"issues_logged": 2,
"files_touched": 7,
"low_impact_fixes": 1,
"failed_verifications": 3,
"empty_cycles": 4,
"agent_failures": 6,
"tests_written": 8,
},
"recent_cycle_paths": [],
"cycles": [],
"halt_reason": None,
"log_only_mode": False,
}
state_path.write_text(json.dumps(existing))
state = nightshift.read_state(
state_path,
today="2026-04-09",
branch="nightshift/2026-04-09",
agent="claude",
verify_command=None,
)
assert state["counters"] == {
"fixes": 5,
"issues_logged": 2,
"files_touched": 7,
"low_impact_fixes": 1,
"failed_verifications": 3,
"empty_cycles": 4,
"agent_failures": 6,
"tests_written": 8,
}


class TestTopPath:
def test_basic(self):
Expand Down
Loading