personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(import): add staged-inventory read route (journal-sources/<name>/staged)

Mirror sol call import list-staged as an HTTP per-area inventory for entities, facets, and config in a respond_collection envelope.

Read-only with no peer-ingest guard; CLI cutover is a separate lode.

Co-Authored-By: Claude <noreply@anthropic.com>

+215 -1
+69 -1
solstone/apps/import/routes.py
··· 25 25 JOURNAL_SOURCE_PROBLEM, 26 26 MISSING_REQUIRED_FIELD, 27 27 ) 28 - from solstone.convey.utils import error_response, respond_collection 28 + from solstone.convey.utils import error_response, load_json, respond_collection 29 29 from solstone.think.detect_created import detect_created 30 30 from solstone.think.importers.utils import ( 31 31 build_import_info, ··· 999 999 "stats": source.get("stats", {}), 1000 1000 } 1001 1001 ) 1002 + 1003 + 1004 + @import_bp.route("/api/journal-sources/<name>/staged") 1005 + def api_journal_source_staged(name: str) -> Any: 1006 + source = find_journal_source_by_name(name) 1007 + if not source: 1008 + return error_response( 1009 + JOURNAL_SOURCE_PROBLEM, 1010 + status=404, 1011 + detail=f"Journal source '{name}' not found", 1012 + ) 1013 + area = request.args.get("area") 1014 + if area is not None and area not in {"entities", "facets", "config"}: 1015 + return error_response( 1016 + INVALID_REQUEST_VALUE, 1017 + status=400, 1018 + detail="Area must be one of: entities, facets, config", 1019 + ) 1020 + # Mirrors api_journal_source_status: a registry-returned record always has a 1021 + # valid prefix, so call journal_source_state_prefix directly (no try/except). 1022 + state_dir = get_state_directory(journal_source_state_prefix(source)) 1023 + items: list[dict[str, Any]] = [] 1024 + 1025 + if area in {None, "entities"}: 1026 + staged_dir = state_dir / "entities" / "staged" 1027 + for staged_path in sorted(staged_dir.glob("*.json")): 1028 + payload = load_json(staged_path) 1029 + if not isinstance(payload, dict): 1030 + continue 1031 + items.append( 1032 + { 1033 + "area": "entities", 1034 + "source_id": staged_path.stem, 1035 + "reason": payload.get("reason"), 1036 + "source_entity": payload.get("source_entity"), 1037 + "match_candidates": payload.get("match_candidates"), 1038 + "staged_at": payload.get("staged_at"), 1039 + } 1040 + ) 1041 + 1042 + if area in {None, "facets"}: 1043 + staged_dir = state_dir / "facets" / "staged" 1044 + for staged_path in sorted(staged_dir.glob("**/*.staged.json")): 1045 + payload = load_json(staged_path) 1046 + if not isinstance(payload, dict): 1047 + continue 1048 + relative_path = staged_path.relative_to(staged_dir) 1049 + parts = relative_path.parts 1050 + if len(parts) < 3: 1051 + continue 1052 + line = { 1053 + "area": "facets", 1054 + "staged_file": relative_path.as_posix(), 1055 + "facet": parts[0], 1056 + "file_type": parts[1], 1057 + } 1058 + line.update(payload) 1059 + items.append(line) 1060 + 1061 + if area in {None, "config"}: 1062 + diff = load_json(state_dir / "config" / "diff.json") 1063 + # Config-parity decision: include the config item iff diff.json exists 1064 + # AND loads as a dict. A missing / unreadable / non-dict diff is omitted 1065 + # (still HTTP 200) — never a 500, never an empty-diff placeholder. 1066 + if isinstance(diff, dict): 1067 + items.append({"area": "config", "diff": diff}) 1068 + 1069 + return respond_collection(items) 1002 1070 1003 1071 1004 1072 @import_bp.route("/journal/<key_prefix>/manifest/<area>")
+146
tests/test_journal_source_dl_surfaces.py
··· 4 4 from __future__ import annotations 5 5 6 6 import argparse 7 + import json 7 8 from importlib import import_module 8 9 9 10 import pytest ··· 16 17 import_routes = import_module("solstone.apps.import.routes") 17 18 journal_source_cli = import_module("solstone.think.importers.journal_source_cli") 18 19 20 + create_state_directory = journal_sources.create_state_directory 19 21 generate_key = journal_sources.generate_key 20 22 journal_source_state_prefix = journal_sources.journal_source_state_prefix 21 23 load_journal_source_by_fingerprint = journal_sources.load_journal_source_by_fingerprint ··· 80 82 return dl_source 81 83 82 84 85 + def _client(): 86 + app = Flask(__name__) 87 + app.register_blueprint(import_routes.import_bp) 88 + return app.test_client() 89 + 90 + 91 + def _write_json(path, payload) -> None: 92 + path.parent.mkdir(parents=True, exist_ok=True) 93 + path.write_text(json.dumps(payload), encoding="utf-8") 94 + 95 + 96 + def _stage_all_areas(journal_root) -> dict: 97 + dl_source = _dl_source() 98 + assert save_journal_source(dl_source) is True 99 + prefix = journal_source_state_prefix(dl_source) 100 + state_dir = create_state_directory(journal_root, prefix) 101 + 102 + entity_item = { 103 + "area": "entities", 104 + "source_id": "ent-1", 105 + "reason": "new", 106 + "source_entity": {"name": "Ada"}, 107 + "match_candidates": [{"entity_id": "ada"}], 108 + "staged_at": "2026-06-07T00:00:00Z", 109 + } 110 + _write_json( 111 + state_dir / "entities" / "staged" / "ent-1.json", 112 + { 113 + "reason": entity_item["reason"], 114 + "source_entity": entity_item["source_entity"], 115 + "match_candidates": entity_item["match_candidates"], 116 + "staged_at": entity_item["staged_at"], 117 + "junk": "ignored", 118 + }, 119 + ) 120 + _write_json(state_dir / "entities" / "staged" / "bad.json", []) 121 + 122 + facet_payload = {"source_id": "foo", "reason": "missing_target"} 123 + facet_item = { 124 + "area": "facets", 125 + "staged_file": "work/entity_observations/foo.staged.json", 126 + "facet": "work", 127 + "file_type": "entity_observations", 128 + **facet_payload, 129 + } 130 + _write_json( 131 + state_dir 132 + / "facets" 133 + / "staged" 134 + / "work" 135 + / "entity_observations" 136 + / "foo.staged.json", 137 + facet_payload, 138 + ) 139 + _write_json(state_dir / "facets" / "staged" / "shallow.staged.json", {}) 140 + 141 + config_item = {"area": "config", "diff": {"field.a": {"category": "x"}}} 142 + _write_json(state_dir / "config" / "diff.json", config_item["diff"]) 143 + 144 + return { 145 + "entities": entity_item, 146 + "facets": facet_item, 147 + "config": config_item, 148 + } 149 + 150 + 83 151 def test_api_journal_source_list_excludes_pl_records(journal_env) -> None: 84 152 dl_source = _save_dl_and_pl() 85 153 app = Flask(__name__) ··· 123 191 pl_record = load_journal_source_by_fingerprint(FINGERPRINT) 124 192 assert pl_record is not None 125 193 assert pl_record["revoked"] is False 194 + 195 + 196 + def test_api_journal_source_staged_returns_all_areas_and_skips_invalid( 197 + journal_env, 198 + ) -> None: 199 + expected = _stage_all_areas(journal_env) 200 + 201 + response = _client().get("/app/import/api/journal-sources/alpha/staged") 202 + 203 + assert response.status_code == 200 204 + body = response.get_json() 205 + assert body["total"] == len(body["items"]) 206 + assert body["items"] == [ 207 + expected["entities"], 208 + expected["facets"], 209 + expected["config"], 210 + ] 211 + assert all(item.get("source_id") != "bad" for item in body["items"]) 212 + assert all( 213 + item.get("staged_file") != "shallow.staged.json" for item in body["items"] 214 + ) 215 + 216 + 217 + def test_api_journal_source_staged_area_filter(journal_env) -> None: 218 + expected = _stage_all_areas(journal_env) 219 + client = _client() 220 + 221 + entities = client.get("/app/import/api/journal-sources/alpha/staged?area=entities") 222 + facets = client.get("/app/import/api/journal-sources/alpha/staged?area=facets") 223 + config = client.get("/app/import/api/journal-sources/alpha/staged?area=config") 224 + 225 + assert entities.status_code == 200 226 + assert entities.get_json() == {"items": [expected["entities"]], "total": 1} 227 + assert facets.status_code == 200 228 + assert facets.get_json() == {"items": [expected["facets"]], "total": 1} 229 + assert config.status_code == 200 230 + assert config.get_json() == {"items": [expected["config"]], "total": 1} 231 + 232 + 233 + def test_api_journal_source_staged_empty_valid_and_unknown(journal_env) -> None: 234 + dl_source = _dl_source() 235 + assert save_journal_source(dl_source) is True 236 + create_state_directory(journal_env, journal_source_state_prefix(dl_source)) 237 + client = _client() 238 + 239 + empty = client.get("/app/import/api/journal-sources/alpha/staged") 240 + unknown = client.get("/app/import/api/journal-sources/ghost/staged") 241 + 242 + assert empty.status_code == 200 243 + assert empty.get_json() == {"items": [], "total": 0} 244 + assert unknown.status_code == 404 245 + assert unknown.get_json()["reason_code"] == "journal_source_problem" 246 + 247 + 248 + def test_api_journal_source_staged_invalid_area_returns_error(journal_env) -> None: 249 + dl_source = _dl_source() 250 + assert save_journal_source(dl_source) is True 251 + 252 + response = _client().get("/app/import/api/journal-sources/alpha/staged?area=bogus") 253 + 254 + assert response.status_code == 400 255 + body = response.get_json() 256 + assert body["reason_code"] == "invalid_request_value" 257 + assert "items" not in body 258 + 259 + 260 + def test_api_journal_source_staged_omits_non_dict_config(journal_env) -> None: 261 + dl_source = _dl_source() 262 + assert save_journal_source(dl_source) is True 263 + state_dir = create_state_directory( 264 + journal_env, journal_source_state_prefix(dl_source) 265 + ) 266 + _write_json(state_dir / "config" / "diff.json", []) 267 + 268 + response = _client().get("/app/import/api/journal-sources/alpha/staged?area=config") 269 + 270 + assert response.status_code == 200 271 + assert response.get_json() == {"items": [], "total": 0}