diff --git a/diagnostics/claims_api.py b/diagnostics/claims_api.py index f18c5d5..8808e45 100644 --- a/diagnostics/claims_api.py +++ b/diagnostics/claims_api.py @@ -423,12 +423,43 @@ async def handle_claim_detail(request): One round-trip, all data resolved server-side. Wikilinks pre-resolved. """ - slug = request.match_info["slug"] + requested_slug = request.match_info["slug"] by_title, by_stem = _build_indexes() + # Resolution order: exact stem → title-normalized (handles description-derived + # slugs from /api/activity-feed that are longer than on-disk file stems) → + # stem-as-prefix (handles description-derived slugs that are shorter than the + # file stem because the description was truncated upstream). + slug = requested_slug rel_path = by_stem.get(slug) if not rel_path: - return web.json_response({"error": "claim not found", "slug": slug}, + # Title fallback: requested slug = slugified frontmatter title + norm = _normalize_for_match(requested_slug) + resolved_stem = by_title.get(norm) + if resolved_stem: + slug = resolved_stem + rel_path = by_stem.get(resolved_stem) + if not rel_path: + # Prefix fallback: walk stems sharing a common prefix with the request, + # pick longest match. Anchored at 32 chars to avoid spurious hits. + norm_req = _normalize_for_match(requested_slug) + best_stem = None + best_len = 0 + for stem in by_stem: + norm_stem = _normalize_for_match(stem) + common = 0 + for a, b in zip(norm_req, norm_stem): + if a != b: + break + common += 1 + if common >= 32 and common > best_len: + best_stem = stem + best_len = common + if best_stem: + slug = best_stem + rel_path = by_stem.get(best_stem) + if not rel_path: + return web.json_response({"error": "claim not found", "slug": requested_slug}, status=404, headers=CORS_HEADERS) filepath = CODEX_BASE / rel_path