diff --git a/docs/design/oura-import.md b/docs/design/oura-import.md index 3591f1779..62ae074a9 100644 --- a/docs/design/oura-import.md +++ b/docs/design/oura-import.md @@ -220,7 +220,7 @@ Removing `email` and `personal` changes only what future authorization requests Landed in the skeleton: -- `SENSITIVE_IMPORTERS` is derived from `HEALTH_IMPORTER_REGISTRY` in `health_schema.py` so health importer registration and gate coverage cannot drift. +- `SENSITIVE_IMPORTERS` is an explicit importer/backend-name gate set: `{"apple_health", "oura"}`. It intentionally does not derive from source-family registries because `oura_api` and `dexcom_clarity` are source families, not approval-artifact importer names. - Same approval artifact (`imports/_approvals/health_import_preflight.json`, same `APPROVAL_SCHEMA`/`CHECKLIST_VERSION`): `approved_importers` must include `"oura"`; all five replication-destination decisions and the raw-retention decision apply unchanged to Oura data. - Same per-run `--confirm-health-save` requirement; `OuraImporter.process()` enforces the gate itself in save mode (defense in depth alongside the CLI's pre-`process` enforcement), **before** any parse or write, then stops at the phase-O1 seam. - Tests prove: missing artifact blocks; artifact without `"oura"` in `approved_importers` blocks (`importer_not_approved`); missing per-run confirmation blocks; a fully approved run still writes nothing (seam); failure payloads leak no fixture paths or values. diff --git a/solstone/think/importers/health_schema.py b/solstone/think/importers/health_schema.py index 3b9ced19d..a3920b408 100644 --- a/solstone/think/importers/health_schema.py +++ b/solstone/think/importers/health_schema.py @@ -35,6 +35,7 @@ HEALTH_CARD_STREAMS: Final = frozenset( stream for stream in HEALTH_CARD_STREAM_BY_FAMILY.values() if stream is not None ) + class HealthCardStreamError(ValueError): """Raised when a health source family cannot write a chronicle card stream.""" diff --git a/solstone/think/importers/pre_save_gate.py b/solstone/think/importers/pre_save_gate.py index 9a4e03a57..0da402b36 100644 --- a/solstone/think/importers/pre_save_gate.py +++ b/solstone/think/importers/pre_save_gate.py @@ -48,10 +48,9 @@ import json from dataclasses import dataclass from enum import StrEnum from pathlib import Path -from typing import Any +from typing import Any, Final from solstone.think.importers.health_schema import ( - HEALTH_IMPORTER_REGISTRY, SOURCE_APPLE_HEALTH, SOURCE_OURA, ) @@ -75,7 +74,10 @@ CHECKLIST_DESTINATIONS = ( "other", ) DESTINATION_DECISIONS = {"approved", "excluded"} -SENSITIVE_IMPORTERS = frozenset(HEALTH_IMPORTER_REGISTRY) +# Approval artifacts are keyed by importer/backend names, not source families. +# Do not derive this from KNOWN_SOURCE_FAMILIES: oura_api and dexcom_clarity +# are source families with no approval-artifact importer name. +SENSITIVE_IMPORTERS: Final = frozenset({"apple_health", "oura"}) class RawRetentionDecision(StrEnum): diff --git a/tests/test_cluster.py b/tests/test_cluster.py index d3c9d5e30..68f1bd18d 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -1427,34 +1427,6 @@ def test_scan_day_marks_markdown_only_health_segment_as_markdown(tmp_path, monke ) == {"markdown": "analyzed"} -def test_health_registry_keys_match_sensitive_importers(): - from solstone.think.importers.health_schema import HEALTH_IMPORTER_REGISTRY - from solstone.think.importers.pre_save_gate import SENSITIVE_IMPORTERS - - assert set(HEALTH_IMPORTER_REGISTRY) == set(SENSITIVE_IMPORTERS) - - -def test_registry_day_summary_streams_are_health_card_streams(tmp_path, monkeypatch): - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - mod = importlib.import_module("solstone.think.cluster") - - from solstone.think.importers.health_schema import HEALTH_IMPORTER_REGISTRY - - for entry in HEALTH_IMPORTER_REGISTRY.values(): - segment = ( - tmp_path - / "chronicle" - / "20240101" - / entry.day_summary_stream - / "090000_300" - ) - segment.mkdir(parents=True) - (segment / "day_summary_transcript.md").write_text("health summary\n") - - assert entry.day_summary_stream in mod.HEALTH_CARD_STREAMS - assert mod._is_markdown_only_health_segment(entry.day_summary_stream, segment) - - @pytest.mark.parametrize( "stream", ["import.kindle", "import.ics", "import.obsidian", "import.document"], diff --git a/tests/test_importer_presave_gate.py b/tests/test_importer_presave_gate.py index db2ef8369..fe138b7cd 100644 --- a/tests/test_importer_presave_gate.py +++ b/tests/test_importer_presave_gate.py @@ -13,12 +13,17 @@ from pathlib import Path import pytest from solstone.think.importers.file_importer import ImportResult +from solstone.think.importers.health_schema import ( + SOURCE_DEXCOM_CLARITY, + SOURCE_OURA_API, +) from solstone.think.importers.pre_save_gate import ( APPROVAL_SCHEMA, CHECKLIST_DESTINATIONS, CHECKLIST_VERSION, OURA_SYNC_APPROVAL_SCHEMA, OURA_SYNC_CHECKLIST_VERSION, + SENSITIVE_IMPORTERS, PreSaveGateError, RawRetentionDecision, approval_path_for_journal, @@ -162,6 +167,12 @@ def _run_gate_then_save( return process() +def test_sensitive_importers_are_importer_names_not_source_families(): + assert SENSITIVE_IMPORTERS == frozenset({"apple_health", "oura"}) + assert SOURCE_OURA_API not in SENSITIVE_IMPORTERS + assert SOURCE_DEXCOM_CLARITY not in SENSITIVE_IMPORTERS + + def test_apple_health_save_missing_artifact_blocks_before_setup( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ):