1616"""
1717from __future__ import annotations
1818
19- import logging
2019import re
2120
2221from fastapi import APIRouter , HTTPException
3130 generate_ruling_from_passages ,
3231)
3332from app .resolve_model import resolve
33+ from app .rule_cache_catalog import RuleCacheCatalog
3434from app .rule_query import (
3535 RuleQueryCompositionError ,
3636 RuleQueryDependencies ,
4040)
4141from app .rules import (
4242 MAX_QUERY_CHARS ,
43- RULING_CACHE_PATH_PREFIX ,
4443 RulesIndex ,
45- _parse_ruling_cache ,
46- coerce_topic ,
4744 keyword_classify_topic ,
4845)
4946
50- logger = logging .getLogger (__name__ )
51-
5247router = APIRouter (prefix = "/rule" , tags = ["rule" ])
5348
5449# Module-level singletons — set by main.py lifespan, patchable in tests.
@@ -204,58 +199,14 @@ async def rule_query(req: RuleQueryRequest) -> JSONResponse:
204199# --- Enumeration endpoints — no LLM, no embedding, pure Obsidian directory walks ---
205200
206201
207- def _build_ruling_index_entry (path : str , parsed : dict , topic : str | None = None ) -> dict :
208- """Extract the summary fields a UI / bot would list from a cached ruling."""
209- # hash is the last path component without .md.
210- hash_part = path .rsplit ("/" , 1 )[- 1 ].removesuffix (".md" )
211- return {
212- "hash" : hash_part ,
213- "topic" : topic or parsed .get ("topic" ),
214- "question" : parsed .get ("question" , "" ),
215- "composed_at" : parsed .get ("composed_at" , "" ),
216- "last_reused_at" : parsed .get ("last_reused_at" , parsed .get ("composed_at" , "" )),
217- "marker" : parsed .get ("marker" , "" ),
218- "source" : parsed .get ("source" ),
219- }
220-
221-
222- async def _collect_rulings_under (prefix : str ) -> list [tuple [str , dict ]]:
223- """Walk a topic-prefix (or root rulings prefix), return list of (path, parsed_frontmatter)."""
224- if obsidian is None :
225- return []
226- try :
227- paths = await obsidian .list_directory (prefix )
228- except Exception as exc :
229- logger .warning ("_collect_rulings_under: list_directory %s failed: %s" , prefix , exc )
230- return []
231- out : list [tuple [str , dict ]] = []
232- for p in paths :
233- if not p .endswith (".md" ):
234- continue
235- text = await obsidian .get_note (p )
236- if not text :
237- continue
238- parsed = _parse_ruling_cache (text )
239- if parsed is None :
240- logger .warning ("_collect_rulings_under: malformed cache at %s — skipping" , p )
241- continue
242- out .append ((p , parsed ))
243- return out
244-
245-
246202@router .post ("/show" )
247203async def rule_show (req : RuleShowRequest ) -> JSONResponse :
248204 """List rulings under a given topic folder. Sorted by last_reused_at desc."""
249205 if obsidian is None :
250206 raise HTTPException (
251207 status_code = 503 , detail = {"error" : "rule subsystem not initialised" }
252208 )
253- topic = coerce_topic (req .topic )
254- prefix = f"{ RULING_CACHE_PATH_PREFIX } /{ topic } /"
255- collected = await _collect_rulings_under (prefix )
256- entries = [_build_ruling_index_entry (p , parsed , topic = topic ) for p , parsed in collected ]
257- entries .sort (key = lambda e : e .get ("last_reused_at" , "" ), reverse = True )
258- return JSONResponse ({"topic" : topic , "count" : len (entries ), "rulings" : entries })
209+ return JSONResponse (await RuleCacheCatalog (obsidian ).show_topic (req .topic ))
259210
260211
261212@router .post ("/history" )
@@ -265,29 +216,7 @@ async def rule_history(req: RuleHistoryRequest) -> JSONResponse:
265216 raise HTTPException (
266217 status_code = 503 , detail = {"error" : "rule subsystem not initialised" }
267218 )
268- n = req .n # already clamped to [1, 100] by field_validator
269- root_prefix = f"{ RULING_CACHE_PATH_PREFIX } /"
270- try :
271- all_paths = await obsidian .list_directory (root_prefix )
272- except Exception as exc :
273- logger .warning ("rule_history: root list_directory failed: %s" , exc )
274- all_paths = []
275- all_entries : list [dict ] = []
276- for p in all_paths :
277- if not p .endswith (".md" ):
278- continue
279- text = await obsidian .get_note (p )
280- if not text :
281- continue
282- parsed = _parse_ruling_cache (text )
283- if parsed is None :
284- continue
285- # topic = path segment between the root prefix and the final filename.
286- stripped = p .removeprefix (root_prefix )
287- topic = stripped .split ("/" , 1 )[0 ] if "/" in stripped else "misc"
288- all_entries .append (_build_ruling_index_entry (p , parsed , topic = topic ))
289- all_entries .sort (key = lambda e : e .get ("last_reused_at" , "" ), reverse = True )
290- return JSONResponse ({"n" : n , "rulings" : all_entries [:n ]})
219+ return JSONResponse (await RuleCacheCatalog (obsidian ).history (req .n ))
291220
292221
293222@router .post ("/list" )
@@ -297,32 +226,4 @@ async def rule_list() -> JSONResponse:
297226 raise HTTPException (
298227 status_code = 503 , detail = {"error" : "rule subsystem not initialised" }
299228 )
300- root_prefix = f"{ RULING_CACHE_PATH_PREFIX } /"
301- try :
302- all_paths = await obsidian .list_directory (root_prefix )
303- except Exception as exc :
304- logger .warning ("rule_list: root list_directory failed: %s" , exc )
305- all_paths = []
306- # Group by topic slug (first segment after root_prefix).
307- per_topic : dict [str , dict ] = {}
308- for p in all_paths :
309- if not p .endswith (".md" ):
310- continue
311- stripped = p .removeprefix (root_prefix )
312- if "/" not in stripped :
313- continue
314- topic = stripped .split ("/" , 1 )[0 ]
315- text = await obsidian .get_note (p )
316- if not text :
317- continue
318- parsed = _parse_ruling_cache (text )
319- if parsed is None :
320- continue
321- bucket = per_topic .setdefault (topic , {"slug" : topic , "count" : 0 , "last_activity" : "" })
322- bucket ["count" ] += 1
323- last_act = parsed .get ("last_reused_at" , parsed .get ("composed_at" , "" ))
324- if last_act > bucket ["last_activity" ]:
325- bucket ["last_activity" ] = last_act
326- topics = list (per_topic .values ())
327- topics .sort (key = lambda t : t .get ("last_activity" , "" ), reverse = True )
328- return JSONResponse ({"topics" : topics })
229+ return JSONResponse (await RuleCacheCatalog (obsidian ).topics ())
0 commit comments