diff --git a/.recursive/architecture/MODULE_MAP.md b/.recursive/architecture/MODULE_MAP.md index f1793b1..56f1a26 100644 --- a/.recursive/architecture/MODULE_MAP.md +++ b/.recursive/architecture/MODULE_MAP.md @@ -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 @@ -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) | @@ -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 | diff --git a/.recursive/tasks/0273.md b/.recursive/tasks/0273.md index abc9eb9..ad614ef 100644 --- a/.recursive/tasks/0273.md +++ b/.recursive/tasks/0273.md @@ -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 diff --git a/nightshift/core/state.py b/nightshift/core/state.py index c6b8b23..4e8b2dd 100644 --- a/nightshift/core/state.py +++ b/nightshift/core/state.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import math from pathlib import Path from typing import Any @@ -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", {}) @@ -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): @@ -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( diff --git a/nightshift/tests/test_nightshift.py b/nightshift/tests/test_nightshift.py index 0925a6b..5876fbf 100644 --- a/nightshift/tests/test_nightshift.py +++ b/nightshift/tests/test_nightshift.py @@ -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" @@ -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": [], @@ -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. @@ -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):