From 0eea2d94e15c02cd3fe209b89834201e1d521f8e Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Fri, 17 Jul 2026 01:50:19 -0600 Subject: [PATCH] refactor(convey): fold nine date-nav index builders into one helper Nine /api/index routes each reimplemented the same fold from {day: total} to the date-nav wire contract. Extract it into a single pure helper, solstone/convey/date_nav.py::build_date_nav_index, mirroring the day_grid.py precedent, and migrate all nine routes onto it. Deletes the five private _build_date_nav_index copies. The helper takes exactly one parameter and accumulates totals raw: no rounding, no int() coercion. That keeps float cost sources intact and makes callers adapt at the call site rather than the helper growing flags. Sorting inside the helper makes the payload deterministic: sol and tokens enumerate days via glob(), so month insertion order was previously filesystem-dependent. tokens rounds month totals to 2 decimals at the call site, preserving the existing wire contract now that the helper accumulates raw. This is not a behavior change: _aggregate_token_data already rounds each day to 2dp before the fold sees it. Co-Authored-By: Claude Opus 4.8 (1M context) --- solstone/apps/activities/routes.py | 33 +---- solstone/apps/activities/tests/test_routes.py | 15 +++ solstone/apps/body/routes.py | 23 +--- solstone/apps/chat/routes.py | 24 +--- solstone/apps/reflections/routes.py | 20 +-- solstone/apps/sol/routes.py | 32 +---- solstone/apps/speakers/routes.py | 17 +-- solstone/apps/timeline/routes.py | 17 +-- solstone/apps/tokens/routes.py | 31 +---- solstone/apps/tokens/tests/test_routes.py | 17 +++ solstone/apps/transcripts/routes.py | 27 +--- solstone/convey/date_nav.py | 22 ++++ tests/test_convey_date_nav.py | 121 ++++++++++++++++++ 13 files changed, 212 insertions(+), 187 deletions(-) create mode 100644 solstone/convey/date_nav.py create mode 100644 tests/test_convey_date_nav.py diff --git a/solstone/apps/activities/routes.py b/solstone/apps/activities/routes.py index 5ce06ee24..2f2ad8659 100644 --- a/solstone/apps/activities/routes.py +++ b/solstone/apps/activities/routes.py @@ -11,6 +11,7 @@ from typing import Any from flask import Blueprint, current_app, jsonify, redirect, request, url_for from solstone.convey import state +from solstone.convey.date_nav import build_date_nav_index from solstone.convey.reasons import ( ACTIVITIES_BUSY, ACTIVITY_ALREADY_EXISTS, @@ -107,38 +108,18 @@ def _activity_months() -> set[str]: return months -def _build_date_nav_index() -> dict[str, Any]: - months: dict[str, int] = {} - first_day: str | None = None - last_day: str | None = None - +@activities_bp.route("/api/index") +def api_index() -> Any: + """Return read-only whole-journal date navigation coverage.""" + day_counts: dict[str, int] = {} for month in sorted(_activity_months()): try: month_counts = _month_activity_counts(month) except ValueError: continue for day, facet_counts in month_counts.items(): - count = sum(facet_counts.values()) - if count <= 0: - continue - months[month] = months.get(month, 0) + count - if first_day is None or day < first_day: - first_day = day - if last_day is None or day > last_day: - last_day = day - - coverage = ( - {"start": first_day, "end": last_day} - if first_day is not None and last_day is not None - else None - ) - return {"coverage": coverage, "months": months} - - -@activities_bp.route("/api/index") -def api_index() -> Any: - """Return read-only whole-journal date navigation coverage.""" - return jsonify(_build_date_nav_index()) + day_counts[day] = sum(facet_counts.values()) + return jsonify(build_date_nav_index(day_counts)) @activities_bp.route("/api/stats/") diff --git a/solstone/apps/activities/tests/test_routes.py b/solstone/apps/activities/tests/test_routes.py index 842cc7e9d..b576cb0d5 100644 --- a/solstone/apps/activities/tests/test_routes.py +++ b/solstone/apps/activities/tests/test_routes.py @@ -111,6 +111,21 @@ def test_api_index_empty_journal(activities_env): assert response.get_json() == {"coverage": None, "months": {}} +def test_api_index_skips_invalid_activity_month(activities_env): + journal, facet, _day, _day_path = activities_env(None) + stray_file = journal / "facets" / facet / "activities" / "20269901.jsonl" + stray_file.write_text( + '{"id": "stray", "activity": "coding", "title": "Stray"}\n', + encoding="utf-8", + ) + client = create_app(journal=str(journal)).test_client() + + response = client.get("/app/activities/api/index") + + assert response.status_code == 200 + assert "202699" not in response.get_json()["months"] + + def test_api_index_is_read_only(activities_env): journal, _facet, _day, _day_path = activities_env( [{"id": "a1", "activity": "coding", "title": "Coding"}] diff --git a/solstone/apps/body/routes.py b/solstone/apps/body/routes.py index 004782326..dfd2ae67d 100644 --- a/solstone/apps/body/routes.py +++ b/solstone/apps/body/routes.py @@ -36,6 +36,7 @@ from flask import ( ) from solstone.convey import state +from solstone.convey.date_nav import build_date_nav_index from solstone.convey.reasons import INVALID_DAY, INVALID_REQUEST_VALUE from solstone.convey.utils import error_response from solstone.think.importers.health_schema import ( @@ -4469,27 +4470,9 @@ def api_window(): @body_bp.get("/api/index") def api_index() -> Any: - day_counts = _read_health_dedupe_stats(_journal_root())["by_day"] - months: dict[str, int] = {} - first_day: str | None = None - last_day: str | None = None - - for day, count in day_counts.items(): - if count <= 0: - continue - month = day[:6] - months[month] = months.get(month, 0) + count - if first_day is None or day < first_day: - first_day = day - if last_day is None or day > last_day: - last_day = day - - coverage = ( - {"start": first_day, "end": last_day} - if first_day is not None and last_day is not None - else None + return jsonify( + build_date_nav_index(_read_health_dedupe_stats(_journal_root())["by_day"]) ) - return jsonify({"coverage": coverage, "months": months}) @body_bp.get("/api/stats/") diff --git a/solstone/apps/chat/routes.py b/solstone/apps/chat/routes.py index 2927db9d5..eca8f2c42 100644 --- a/solstone/apps/chat/routes.py +++ b/solstone/apps/chat/routes.py @@ -12,6 +12,7 @@ from flask import Blueprint, abort, current_app, jsonify, redirect, request, url from solstone.apps.chat.config import DEFAULT_THINKING_SURFACES, load_chat_config from solstone.convey.chat_stream import read_chat_events +from solstone.convey.date_nav import build_date_nav_index from solstone.convey.reasons import INVALID_DAY, INVALID_MONTH from solstone.convey.sol_initiated.copy import ( KIND_OWNER_CHAT_OPEN, @@ -105,27 +106,8 @@ def _chat_day_count(day: str) -> int: @chat_bp.route("/api/index") def api_index() -> Any: - months: dict[str, int] = {} - first_day: str | None = None - last_day: str | None = None - - for day_name in day_dirs().keys(): - count = _chat_day_count(day_name) - if count <= 0: - continue - month = day_name[:6] - months[month] = months.get(month, 0) + count - if first_day is None or day_name < first_day: - first_day = day_name - if last_day is None or day_name > last_day: - last_day = day_name - - coverage = ( - {"start": first_day, "end": last_day} - if first_day is not None and last_day is not None - else None - ) - return jsonify({"coverage": coverage, "months": months}) + day_counts = {day: _chat_day_count(day) for day in day_dirs()} + return jsonify(build_date_nav_index(day_counts)) @chat_bp.route("/api/stats/") diff --git a/solstone/apps/reflections/routes.py b/solstone/apps/reflections/routes.py index fc8ef529e..571d96b02 100644 --- a/solstone/apps/reflections/routes.py +++ b/solstone/apps/reflections/routes.py @@ -22,6 +22,7 @@ from markdown import Markdown from solstone.apps.reflections import copy as reflections_copy from solstone.apps.reflections.dates import next_reflection_sunday +from solstone.convey.date_nav import build_date_nav_index from solstone.convey.day_grid import build_day_grid_payload from solstone.convey.reasons import FILE_NOT_FOUND, INVALID_MONTH from solstone.convey.utils import DATE_RE, error_response, format_date @@ -203,24 +204,7 @@ def api_state() -> Any: @reflections_bp.route("/api/index") def api_index() -> Any: - months: dict[str, int] = {} - first_day: str | None = None - last_day: str | None = None - - for day in _list_reflection_days(): - month = day[:6] - months[month] = months.get(month, 0) + 1 - if first_day is None or day < first_day: - first_day = day - if last_day is None or day > last_day: - last_day = day - - coverage = ( - {"start": first_day, "end": last_day} - if first_day is not None and last_day is not None - else None - ) - return jsonify({"coverage": coverage, "months": months}) + return jsonify(build_date_nav_index({day: 1 for day in _list_reflection_days()})) @reflections_bp.route("/api/grid") diff --git a/solstone/apps/sol/routes.py b/solstone/apps/sol/routes.py index 41ec8ed68..77588811f 100644 --- a/solstone/apps/sol/routes.py +++ b/solstone/apps/sol/routes.py @@ -24,6 +24,7 @@ from flask import ( ) from solstone.convey import state +from solstone.convey.date_nav import build_date_nav_index from solstone.convey.reasons import ( FILE_NOT_FOUND, FILE_READ_FAILED, @@ -691,35 +692,14 @@ def _talent_use_counts(month: str | None = None) -> dict[str, dict[str, int]]: return stats -def _build_date_nav_index() -> dict[str, Any]: - day_counts = _talent_use_counts() - months: dict[str, int] = {} - first_day: str | None = None - last_day: str | None = None - - for day, facet_counts in day_counts.items(): - count = sum(facet_counts.values()) - if count <= 0: - continue - month = day[:6] - months[month] = months.get(month, 0) + count - if first_day is None or day < first_day: - first_day = day - if last_day is None or day > last_day: - last_day = day - - coverage = ( - {"start": first_day, "end": last_day} - if first_day is not None and last_day is not None - else None - ) - return {"coverage": coverage, "months": months} - - @sol_bp.route("/api/index") def api_index() -> Any: """Return read-only whole-journal date navigation coverage.""" - return jsonify(_build_date_nav_index()) + day_counts = { + day: sum(facet_counts.values()) + for day, facet_counts in _talent_use_counts().items() + } + return jsonify(build_date_nav_index(day_counts)) @sol_bp.route("/api/stats/") diff --git a/solstone/apps/speakers/routes.py b/solstone/apps/speakers/routes.py index ea306ef98..ddf558ca0 100644 --- a/solstone/apps/speakers/routes.py +++ b/solstone/apps/speakers/routes.py @@ -72,6 +72,7 @@ from solstone.apps.speakers.status import get_speakers_status from solstone.apps.speakers.suggest import format_suggestions, suggest_opportunities from solstone.apps.speakers.wipe import wipe_speaker_artifacts from solstone.apps.utils import log_app_action +from solstone.convey.date_nav import build_date_nav_index from solstone.convey.day_grid import build_day_grid_payload from solstone.convey.reasons import ( ENTITY_BLOCKED, @@ -910,24 +911,10 @@ def _speaker_grid_counts() -> tuple[dict[str, int], dict[str, int]]: return days, activity -def _build_date_nav_index() -> dict[str, Any]: - day_counts = _speaker_segment_counts() - months: dict[str, int] = {} - - for day, count in day_counts.items(): - if count <= 0: - continue - month = day[:6] - months[month] = months.get(month, 0) + count - - coverage = _coverage_from_counts(day_counts) - return {"coverage": coverage, "months": months} - - @speakers_bp.route("/api/index") def api_index() -> Any: """Return read-only whole-journal date navigation coverage.""" - return jsonify(_build_date_nav_index()) + return jsonify(build_date_nav_index(_speaker_segment_counts())) @speakers_bp.route("/api/grid") diff --git a/solstone/apps/timeline/routes.py b/solstone/apps/timeline/routes.py index 89824bcfc..c93677cb4 100644 --- a/solstone/apps/timeline/routes.py +++ b/solstone/apps/timeline/routes.py @@ -14,6 +14,7 @@ from typing import Any from flask import Blueprint, current_app, jsonify, redirect, url_for from solstone.convey import state +from solstone.convey.date_nav import build_date_nav_index from solstone.convey.day_grid import build_day_grid_payload from solstone.convey.reasons import ( INVALID_DAY, @@ -474,20 +475,6 @@ def _day_segment_counts(month: str | None = None) -> dict[str, int]: return out -def _build_date_nav_index() -> dict[str, Any]: - day_counts = _day_segment_counts() - if not day_counts: - return {"coverage": None, "months": {}} - - months: dict[str, int] = {} - for day, count in sorted(day_counts.items()): - month = day[:6] - months[month] = months.get(month, 0) + count - - days = sorted(day_counts) - return {"coverage": {"start": days[0], "end": days[-1]}, "months": months} - - @timeline_bp.route("/api/stats/") def timeline_stats(ym: str) -> Any: if not _MONTH_RE.fullmatch(ym): @@ -526,7 +513,7 @@ def timeline_grid() -> Any: @timeline_bp.route("/api/index") def timeline_index() -> Any: - return jsonify(_build_date_nav_index()) + return jsonify(build_date_nav_index(_day_segment_counts())) @timeline_bp.route("/api/month/") diff --git a/solstone/apps/tokens/routes.py b/solstone/apps/tokens/routes.py index 34cc7f48b..953534be0 100644 --- a/solstone/apps/tokens/routes.py +++ b/solstone/apps/tokens/routes.py @@ -13,6 +13,7 @@ from typing import Any, Dict from flask import Blueprint, current_app, jsonify, redirect, request, url_for from solstone.convey import state +from solstone.convey.date_nav import build_date_nav_index from solstone.convey.reasons import INVALID_DAY, INVALID_MONTH, INVALID_REQUEST_VALUE from solstone.convey.utils import DATE_RE, error_response, respond_collection from solstone.think.models import calc_token_cost, get_model_provider, iter_token_log @@ -402,34 +403,14 @@ def _token_cost_stats(month: str | None = None) -> dict[str, float]: return stats -def _build_date_nav_index() -> dict[str, Any]: - day_costs = _token_cost_stats() - months: dict[str, float] = {} - first_day: str | None = None - last_day: str | None = None - - for day, cost in day_costs.items(): - if cost <= 0: - continue - month = day[:6] - months[month] = round(months.get(month, 0.0) + cost, 2) - if first_day is None or day < first_day: - first_day = day - if last_day is None or day > last_day: - last_day = day - - coverage = ( - {"start": first_day, "end": last_day} - if first_day is not None and last_day is not None - else None - ) - return {"coverage": coverage, "months": months} - - @tokens_bp.route("/api/index") def api_index() -> Any: """Return read-only whole-journal date navigation coverage.""" - return jsonify(_build_date_nav_index()) + payload = build_date_nav_index(_token_cost_stats()) + payload["months"] = { + month: round(total, 2) for month, total in payload["months"].items() + } + return jsonify(payload) @tokens_bp.route("/api/stats/") diff --git a/solstone/apps/tokens/tests/test_routes.py b/solstone/apps/tokens/tests/test_routes.py index 98b7ee5f8..4708b21e0 100644 --- a/solstone/apps/tokens/tests/test_routes.py +++ b/solstone/apps/tokens/tests/test_routes.py @@ -175,6 +175,23 @@ def test_api_index_reports_nonzero_coverage_and_months(tokens_env, monkeypatch): assert body["months"]["202604"] == pytest.approx(0.3) +def test_api_index_month_total_keeps_two_decimal_contract(tokens_env, monkeypatch): + env = tokens_env( + { + "20260304": [_entry("gpt-5", 1000)], + "20260305": [_entry("gpt-5", 2000)], + } + ) + _patch_token_cost(monkeypatch) + + response = env.client.get("/app/tokens/api/index") + + assert response.status_code == 200 + # Exact equality, not approx: the shared helper sums raw to + # 0.30000000000000004. The call-site round is what holds the 2dp contract. + assert response.get_json()["months"]["202603"] == 0.3 + + def test_api_index_month_totals_match_api_stats(tokens_env, monkeypatch): env = tokens_env( { diff --git a/solstone/apps/transcripts/routes.py b/solstone/apps/transcripts/routes.py index c1fcad286..e59383405 100644 --- a/solstone/apps/transcripts/routes.py +++ b/solstone/apps/transcripts/routes.py @@ -33,6 +33,7 @@ from flask import ( import solstone.think.deferred_deletes as deferred_deletes from solstone.apps.utils import log_app_action from solstone.convey import emit +from solstone.convey.date_nav import build_date_nav_index from solstone.convey.reasons import ( FILE_NOT_FOUND, FILE_READ_FAILED, @@ -284,27 +285,11 @@ def api_index() -> Any: Reuses ``_day_range_count`` for each day, so month totals match the sum of ``/api/stats/{month}`` for the same month. """ - months: dict[str, int] = {} - first_day: str | None = None - last_day: str | None = None - - for day_name, path in day_dirs().items(): - count = _day_range_count(day_name, Path(path)) - if count <= 0: - continue - month = day_name[:6] - months[month] = months.get(month, 0) + count - if first_day is None or day_name < first_day: - first_day = day_name - if last_day is None or day_name > last_day: - last_day = day_name - - coverage = ( - {"start": first_day, "end": last_day} - if first_day is not None and last_day is not None - else None - ) - return jsonify({"coverage": coverage, "months": months}) + day_counts = { + day_name: _day_range_count(day_name, Path(path)) + for day_name, path in day_dirs().items() + } + return jsonify(build_date_nav_index(day_counts)) @transcripts_bp.route("/") diff --git a/solstone/convey/date_nav.py b/solstone/convey/date_nav.py new file mode 100644 index 000000000..9e11e7215 --- /dev/null +++ b/solstone/convey/date_nav.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + + +def build_date_nav_index(day_counts: Mapping[str, int | float]) -> dict[str, Any]: + months: dict[str, int | float] = {} + days: list[str] = [] + + for day, total in sorted(day_counts.items()): + if total <= 0: + continue + days.append(day) + month = day[:6] + months[month] = months.get(month, 0) + total + + coverage = {"start": days[0], "end": days[-1]} if days else None + return {"coverage": coverage, "months": months} diff --git a/tests/test_convey_date_nav.py b/tests/test_convey_date_nav.py new file mode 100644 index 000000000..ba4d89bc2 --- /dev/null +++ b/tests/test_convey_date_nav.py @@ -0,0 +1,121 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +from solstone.convey.date_nav import build_date_nav_index + + +def test_date_nav_index_accumulates_int_counts(): + payload = build_date_nav_index( + { + "20400101": 2, + "20400103": 5, + "20400201": 7, + } + ) + + assert payload == { + "coverage": {"start": "20400101", "end": "20400201"}, + "months": {"204001": 7, "204002": 7}, + } + + +def test_date_nav_index_preserves_float_counts(): + payload = build_date_nav_index( + { + "20400301": 0.014, + "20400302": 0.014, + "20400401": 1.5, + } + ) + + assert payload == { + "coverage": {"start": "20400301", "end": "20400401"}, + "months": {"204003": 0.028, "204004": 1.5}, + } + + +def test_date_nav_index_skips_zero_totals(): + payload = build_date_nav_index( + { + "20400501": 0, + "20400502": 3, + "20400503": 0, + } + ) + + assert payload == { + "coverage": {"start": "20400502", "end": "20400502"}, + "months": {"204005": 3}, + } + + +def test_date_nav_index_skips_negative_totals(): + payload = build_date_nav_index( + { + "20400601": -5, + "20400602": 4, + "20400603": -1, + } + ) + + assert payload == { + "coverage": {"start": "20400602", "end": "20400602"}, + "months": {"204006": 4}, + } + + +def test_date_nav_index_empty_input_has_no_coverage(): + payload = build_date_nav_index({}) + + assert payload == {"coverage": None, "months": {}} + + +def test_date_nav_index_single_day_input(): + payload = build_date_nav_index({"20400704": 9}) + + assert payload == { + "coverage": {"start": "20400704", "end": "20400704"}, + "months": {"204007": 9}, + } + + +def test_date_nav_index_spans_month_boundary(): + payload = build_date_nav_index( + { + "20400831": 2, + "20400901": 3, + } + ) + + assert payload == { + "coverage": {"start": "20400831", "end": "20400901"}, + "months": {"204008": 2, "204009": 3}, + } + + +def test_date_nav_index_spans_year_boundary(): + payload = build_date_nav_index( + { + "20401231": 6, + "20410101": 8, + } + ) + + assert payload == { + "coverage": {"start": "20401231", "end": "20410101"}, + "months": {"204012": 6, "204101": 8}, + } + + +def test_date_nav_index_coverage_is_independent_of_insertion_order(): + payload = build_date_nav_index( + { + "20420301": 1, + "20420101": 1, + "20420201": 1, + } + ) + + assert payload["coverage"] == {"start": "20420101", "end": "20420301"} -- 2.51.2