← back to Unclaimed Property Platform
docs: ledger Cycle-4 record (search service + real limiter); TK-10097
9a964260330801cdb949769e60abad1aafbe73c0 · 2026-08-01 19:32:10 -0700 · Steve Abrams
Files touched
Diff
commit 9a964260330801cdb949769e60abad1aafbe73c0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 19:32:10 2026 -0700
docs: ledger Cycle-4 record (search service + real limiter); TK-10097
---
docs/LOOP-LEDGER.md | 30 ++++++++++++++++++++++--------
1 file changed, 22 insertions(+), 8 deletions(-)
diff --git a/docs/LOOP-LEDGER.md b/docs/LOOP-LEDGER.md
index 4e01f53..4d05254 100644
--- a/docs/LOOP-LEDGER.md
+++ b/docs/LOOP-LEDGER.md
@@ -58,15 +58,29 @@ Corrections to the record above after live verification (VERIFY-BEFORE-ACTING):
- **Fairness fix:** `blocking_keys` no longer early-returns on empty-norm (CJK/Arabic) names —
they fall through to geo keys so no owner is un-indexable.
-## Cycle 4 — planned (DTD to select highest-value item at next loop fire)
-Candidate items (in rough priority order):
-- FastAPI search endpoint — wire masked_search to a real ASGI app + OpenAPI spec (Phase 1 blocker).
-- SQLite → Postgres migration scaffold (production persistence layer).
-- Rate-limiter stub → real sliding-window (brute-force guard; Sec 1.2).
+## Cycle 4 — COMPLETE (commit bdd1a4b; 5/5 suites green)
+**Landed (all local/synthetic/$0):**
+- `services/common/rate_limit.py` — real SlidingWindowRateLimiter (injectable clock, per-key
+ budgets, retry_after). **Closes Security 1.3** — the rate-limit `_STUB` masquerade is gone.
+- `services/search/service.py` — SearchService: validate → rate-limit → masked_search →
+ assert_public_safe (fail-closed), one enforcement point for every transport.
+- `services/search/run_local.py` — dependency-free stdlib HTTP endpoint. **SMOKE-TESTED over
+ real HTTP**: 200 masked JSON, 400 empty-query, 429 after budget, /healthz. Single-threaded
+ (SQLite thread-bound; prod = Postgres pool — bug caught + fixed during verification).
+- `tests/test_cycle5_search.py` — deterministic (injected clock): masked results, real 429 +
+ window refill, empty-query rejection, per-client-key isolation.
+**Verify note:** ThreadingHTTPServer + a thread-bound SQLite conn raised ProgrammingError on
+the search path (healthz/400/429 masked it since they don't hit the DB). Caught via smoke
+test, switched to single-threaded HTTPServer, re-verified. Classic "the happy path that
+doesn't touch the resource hides the bug."
+
+## Cycle 5 — planned (DTD to select at next fire)
+- NAUPA III XML adapter → the last format (namespace-tolerant, streaming, reject-log).
- Double Metaphone phonetic upgrade for international name recall (fairness; Phase 2).
-- NAUPA III XML → production: namespace-tolerant, streaming, reject-log.
-- Fairness test suite: recall across English/Spanish/Vietnamese/Chinese/Arabic corpora.
-- Batch-retry for partial C1 failure (idempotent batch re-run from last-good offset).
+- SQLite → Postgres persistence scaffold (thread-safe pool — enables the threaded server).
+- Batch-retry for partial C1 failure (idempotent re-run from last-good offset).
+- FastAPI reference: wire search_api.py to SearchService + OpenAPI (needs deps; verify in venv).
+- Suppression/correction feed path (state instructs remove-from-search) + latency metric.
## Standing Steve-gates (loop NEVER auto-does — would draft to pending-approval)
Real state/NAUPA data acquisition · scraping · LOIs/contracts/counsel · spend · deploy ·
← bdd1a4b Cycle 4: runnable masked-search service + REAL rate limiter
·
back to Unclaimed Property Platform
·
Cycle 5: NAUPA III XML adapter (completes CSV/NAUPA II/III t ac6d777 →