← back to Unclaimed Property Platform

docs/LOOP-LEDGER.md

153 lines

# /yoloforever Loop Ledger — TK-10097

Perpetual DTD-decided, Cody-gated build loop. Every cycle: DTD picks the highest-value
SAFE increment → execute reversible work (gated → pending-approval) → adversarial gate →
final verdict → log → reschedule. Cost is $0/cycle (all local, synthetic data, stdlib).

## Cycle 1 — scaffold (commit 9819a89)
- **Decided:** scaffold repo + reference architecture on synthetic data only.
- **Landed:** thesis+guardrails, 50-state acquisition matrix, architecture, canonical
  schema, idempotent NAUPA/CSV ETL, Fellegi-Sunter matcher (+phonetic key), masked
  FastAPI search, claim state-machine + outbox, passing stdlib smoke test.
- **Gate:** deferred to Cycle 2 (Cody dispatch errored; re-run as GE fan-out).
- **Verdict:** ship scaffold, gate found next.

## Cycle 2 — adversarial hardening (commit f576a32 + marker)
- **Gate (GE fan-out, 3 independent critics):** contrarian + security-auditor +
  code-reviewer, fresh context each.
- **Found & FIXED (all local, regression-tested):**
  - Crown jewel: `mask_name` length-leak confirmation oracle → constant-width mask.
  - Fail-closed public projection allowlist; `masked_search` no raw-PII leak / no empty-query enum.
  - C1 failed-batch retry; C2 bad-amount tolerated; C3 claim idempotency-key collision;
    C4 owner UPDATE-in-place (FK+history preserved); M3 version effective_to; M4 count fix.
  - Ingestion source allowlist (no-scrape red line in code); state-only approve guard;
    claimant-withdraw path; rate-limiter renamed `_STUB`; `.gitignore` inverted + real-feed blocks.
- **Verified:** original smoke test green + 7/7 new hardening tests pass. Gitignore guard
  proven (a stray `data/CA_real.txt` is ignored).
- **Verdict (merged 3-lens):** FIX-THEN-SHIP, unanimous; dissent: none (Cody wanted the
  mask fixed first — honored). Nothing Steve-gated (all reversible/synthetic).

## Cycle 3 — COMPLETE (commit 902afd8 + 4am-loop additions; 7/7 tests pass)
**Landed:**
- Pre-commit tripwire: block SSN-shaped strings + real jurisdiction codes on commit (Sec 2.4).
- NAUPA II fixed-width + NAUPA III XML parser stubs, each with a synthetic sample.
- `jurisdictions.json` machine-readable (portal, disclosure flags, finder-fee cap) — 53 jurisdictions.
- `docs/03-roadmap-and-budget.md` (roadmap + $4.8M–$7.6M budget, 3-phase plan, revenue model).
- `blocking_keys()` in `entity_match.py` — candidate-generation blocking to avoid O(n²) all-pairs.
  Fairness note documented: phonetic scheme needs Double Metaphone for international names (Phase 2).
- Minor fixes: M1 `complete_state_submission` via `transition_claim`; m4 Decimal precision (ROUND_HALF_EVEN).
- Sec 2.5 `raw_payload` encryption marker in `ingest.py` `CanonicalProperty` — AES-256-GCM + KMS path.
**Gate:** 7/7 tests pass. All work reversible/synthetic; no Steve-gate triggered.

### Cycle 3 — verification + reconciliation pass (this session; commits 9fbefae/509f657/789f72c)
Corrections to the record above after live verification (VERIFY-BEFORE-ACTING):
- NAUPA II fixed-width parser is REAL and tested end-to-end (registry dispatch, cents→dollars,
  idempotency, masking). NAUPA III XML is NOT built yet — still Cycle-4 backlog (the earlier
  "stub" note was aspirational).
- `docs/jurisdictions.json` (not `data/` — that path is gitignored): **51** entries (50 states + DC).
- **Regression caught + fixed:** the M1 refactor of `complete_state_submission` reused the base
  idempotency key and re-collided with `queue`'s SUBMITTING event (C3-variant). Namespaced to
  `:transition`; Cycle-2 regression test now green.
- **Strategic conflict reconciled:** `docs/03` revenue model was consumer **finder-fees** — which
  the thesis + brief EXCLUDE at launch (finder-law exposure, conflicts with the state customer).
  Rewritten to B2G government-paid (~$8.75M ARR at 35 jur). Flagged for Steve to veto.
- **Guardrail incident:** the tripwire's install-time test accidentally committed an SSN-laced
  `LEAK_TEST.csv`; caught, commit excised from history, hook then VERIFIED to block SSN (exit 1)
  and to pass clean commits. The tripwire is installed at `.git/hooks/pre-commit`.
- **Test state now: 4 suites green** (smoke, cycle2 hardening, cycle3 naupa+blocking, cycle4 fairness).
- **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 — 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 — COMPLETE (commit ac6d777; 6/6 suites green)
**Landed (all local/synthetic/$0):**
- `services/ingestion/naupa3.py` — NAUPA III XML adapter, namespace-tolerant, via the lazy
  registry (naupa3_v1). **Completes the CSV / NAUPA II / NAUPA III trio** through one pipeline.
- **SECURITY:** rejects any DOCTYPE/ENTITY doc (XmlSecurityError) → XXE / billion-laughs closed.
- `tests/test_cycle6_naupa3.py` — parity, namespace tolerance, XXE refusal, malformed reject, idempotency.
**Verify note:** test initially failed searching "Oneil" — `normalize_text` collapses the
apostrophe in "O'Neil" to a space ("O NEIL"), a real search-RECALL gap for apostrophe/hyphen
names. Matching is unaffected (phonetic key links O'Neil↔ONeill). Logged below; fixed test.

## Cycle 6 — COMPLETE (commit f74dc2b; 7/7 suites green)
**Landed:** apostrophe/hyphen search-recall fix — `normalize_search()` collapses spaces+punct
to one alnum key; `masked_search` matches `REPLACE(owner_name_normalized,' ','') LIKE` the
collapsed query. "Oneil" now finds "O'Neil"; no schema change; normalize_text untouched (masks/
blocking/phonetic unchanged); empty-query guard preserved. `tests/test_cycle7_recall.py`.

## Cycle 7 — COMPLETE (commit 3ca22d7; 8/8 suites green)
**DTD re-decision:** chose the state SUPPRESSION path over Double Metaphone — the latter is
high-churn (ripples through matcher scoring + blocking + 3 suites' disposition asserts) and
deserves a dedicated cycle; suppression is contained, higher value/risk, and closed a real gap.
**Landed:** `suppress_property`/`reinstate_property` flip `search_document_state.is_suppressed`
(record leaves/returns to anonymous search immediately; masked_search already filtered it) while
property+owner+version history are retained; every action writes an immutable audit_event;
`audit_trail()` reads it; unknown record -> False. `tests/test_cycle8_suppression.py`.

## Cycle 8 — COMPLETE (commit c3d81a0; 9/9 suites green)
**Landed:** `services/claims/sqlite_claim_repo.py` — concrete ClaimRepository over
claim_case/claim_event/outbox_event (shares the SqliteRepository conn). Claims now run
end-to-end on real persistence; the C3 idempotency fix is proven against the REAL
UNIQUE(claim_id, idempotency_key) constraint (duplicate -> sqlite3.IntegrityError), closing
the "runs only on a mock" gap. `tests/test_cycle9_claims_persistence.py` (full lifecycle,
outbox, DB idempotency, state-only guard, illegal-transition rejection).

## Cycle 9 — COMPLETE (commit e3a39c3; 10/10 suites green)
**Landed:** `services/claims/outbox_worker.py` — completes the transactional-outbox pattern.
At-least-once delivery + bounded retry; transient fail -> pending(attempts++); exhausted ->
'failed' (claim recoverable, stays SUBMITTING); already-SUBMITTED -> idempotent success (no
loop). outbox_event gained attempts+last_error. `tests/test_cycle10_outbox_worker.py`.
Events are now WRITTEN (C8) *and* reliably DELIVERED (C9).

## Cycle 10 — COMPLETE (commit edfce96; 11/11 suites green)
**Landed:** feed-completeness reconciliation. FeedDefinition.expected_count (state control
total); `_reconcile` compares parsed vs expected -> ok|short|over|unknown+delta; 'short' is the
silent-truncation signal. Persisted on ingestion_batch (expected_count+reconciliation);
`batch_reconciliation()` reads it. `tests/test_cycle11_reconciliation.py`.

## Cycle 11 — COMPLETE + LOOP CLOSED (commit 3532e9b; DTD 5/5 verdict C)
Steve ran `/dtd` on the loop's next move. Panel unanimous (Claude+Codex+Qwen+Grok+Kimi) =
**C: PIVOT to packaging, then WRAP**. Cody's dissent (real blocker is business/legal, not
code) folded in, not dismissed — added a real DUA template.
**Landed:** `make demo/test/serve` front door; `scripts/demo_tour.py` (verified end-to-end);
README rewrite (proof table + 60s demo + critical path up front); `docs/04-data-use-agreement-
template.md`; MERGE — removed orphan parallel NAUPA duplicate triad (naupa2_fixed/naupa3_xml/
test_cycle3), consolidated to the pipeline-wired impl. 11/11 suites green.

## 🏁 LOOP CLOSED — final state (2026-08-01)
- **11 build cycles**, 11 test suites green, all $0 on synthetic data.
- Vertical slice complete + presentable: ingest(3 fmts)→normalize/mask→resolve→search(HTTP)
  →suppress→claims(+outbox). Every Cycle-2 security finding closed or controlled.
- Reversible/local work only; **pending-approval queue empty** — zero gated actions auto-run.
- **See it:** `make demo`.  **Resume building:** `/yoloforever` (candidates — Double Metaphone,
  FastAPI wiring in a venv, Postgres persistence scaffold).
- **The real next step is human/legal:** work `docs/04-data-use-agreement-template.md` —
  counsel + LOIs from ≥3 states. No code cycle advances that; it stays Steve-gated.

## Deferred backlog (for a future /yoloforever resume)
- Double Metaphone phonetic upgrade (separate blocking key + feature, re-tune, re-verify).
- FastAPI reference wired to SearchService + OpenAPI (needs deps; verify in a venv).
- SQLite → Postgres persistence scaffold (thread-safe pool — enables the threaded server).
- Suppression feed adapter (batch takedown file) + suppression-latency metric.
- Batch-retry for partial ingestion failure (resume from last-good offset).

## Standing Steve-gates (loop NEVER auto-does — would draft to pending-approval)
Real state/NAUPA data acquisition · scraping · LOIs/contracts/counsel · spend · deploy ·
DNS · sending to any state/list · custody of funds · entitlement adjudication.
_Pending-approval queue: currently EMPTY — no gated action has been needed yet._