Application status findings — 2026-08-21 snapshot¶
Probed: 2026-08-21, main @ 89950f8 · Corrected: 2026-09-01 against
main @ 34 commits past prod-v1.4.0
This is a dated findings document, not a live status page. It records what probing the code, the database and production turned up on 2026-08-21: what is implemented and working, what is built but hidden, and what is broken. Read every claim as "true on the stated date". A correction pass on 2026-09-01 removed the findings that had since been fixed or were wrong, and marked what changed; anything not re-verified on that date still carries the 2026-08-21 date only.
The original document ended with a one-week ship plan and a go/no-go checklist. Both were removed on 2026-09-01: the plan was largely executed (see §7), and a merged document that describes finished work as pending is worse than no plan at all.
Companion references, both narrower, both maintained separately:
guest-access.md— what a signed-out visitor can reach after #510, and the metering that is supposed to apply to it.sidebar-map.md— nominally the canonical sidebar map. It describes a 6-item sidebar;frontend/components/app-sidebar.tsxrenders 17 distinct routes. On that disagreement,sidebar-map.mdis the stale document, not §5b below.
It supersedes ROUTES_AUDIT.md (2026-08-05). That audit's
largest findings — the missing /judge-fingerprint and /reasoning-lines
proxies, the mock-data blog, the tiptap-editor type error — have all been
fixed since. Its per-route table is still useful for detail; treat its verdicts
as stale.
1. Verdict¶
The codebase is in better shape than the product is.
Every automated gate is green. The infrastructure is live (health degraded
since 2026-09-01 — Meilisearch sync is stale). What is missing is not
engineering quality — it is that a stranger cannot get to the one thing this
app does well.
Two facts blocked handing it to users, and neither was a code-quality problem. Both have moved since 2026-08-21, and both still bite:
- Production is running a build from 2026-08-25. The newest tag is
prod-v1.4.0;mainis 34 commits ahead of it. Smaller than the three-month, 532-commit drift this document originally recorded — that gap was closed by theprod-v1.4.0release — but the shape of the problem is unchanged: deploys are manual, and every fix listed below as "already done" is done onmain, not in front of users. - A visitor could not see a single judgment without an account and a
confirmed email.
/search,/chat,/documents/[id]all 307'd to/auth/login. This was #510,priority: critical.
Closed 2026-09-01 by PR
#561. On main, /search
and the judgment detail page are anonymous:
frontend/lib/supabase/public-route-policy.ts:17 ('/search' in
EXACT_PUBLIC_PAGES) and :71 (PUBLIC_JUDGMENT_PAGE_PATTERN).
/search/extractions deliberately stays gated.
Production still returns 307 on /search (probed 2026-09-01), because
it has not been redeployed since. The wall this document argues against is
gone from the code and still standing in front of users. That is fact 1
again, with a concrete cost attached.
And the wall was cosmetic anyway. The anon key that production ships inside
its own landing-page HTML reads all 12,307 judgments and all 329,237 chunks
directly from Supabase REST — the database carries policies explicitly named
"Public read access". The gate blocked the user, not the data (§4).
Beyond those, the product has one strong, complete spine — search and read over 12,307 judgments, with 40+ structured fields already extracted from every one of them — wrapped in 63 pages, most of which are furnished rooms with nothing in them.
The job is not to build. It is to deploy, unlock, and de-clutter.
2. What was measured, and how¶
Every claim below is from a command run on 2026-08-21, not from documentation.
| Check | Command | Result |
|---|---|---|
| Backend unit tests | poetry run pytest -q -m unit |
2367 collected · 2345 passed · 22 skipped · exit 0 |
| Frontend unit tests | npm test |
2416 tests / 181 suites, all pass · 29 snapshots |
| Frontend typecheck | npm run typecheck |
exit 0 (the tiptap-editor error from the Aug-05 audit is gone) |
CI on main |
gh run list |
green |
| Required checks | gh api .../branches/main/protection |
7: Backend Lint · Backend Unit Tests · Frontend Lint · Frontend Unit Tests · Frontend E2E Smoke (UI-only) · Database Contract · Frontend Route Contract (Chromium) |
| Production reachability | curl https://juddges.augustyniak.ai/ |
200 |
| Production dependencies | GET /api/health/status |
status: "degraded" — redis · postgresql · supabase · celery · langfuse healthy; meilisearch degraded, "Meilisearch is accessible and healthy (sync stale)", last_completed_at: null, last_error: "Event loop is closed" (re-probed 2026-09-01) |
| Data volumes | Supabase REST, Prefer: count=exact |
see §4 |
| pgvector search RPCs | POST /rest/v1/rpc/... |
count_judgments_filtered → 12307 · search_judgments_hybrid → responds |
| Access posture | pg_class / pg_policies over DATABASE_URL, read-only |
43 public tables, RLS on for all 43 · see §4 |
| Anonymous reachability | production's own browser-shipped anon key vs Supabase REST |
corpus fully readable · see §4 |
| First-page search cost | select count_judgments_filtered(...) |
388 ms |
3. Delivery and infrastructure¶
| Item | State |
|---|---|
| Production URL | https://juddges.augustyniak.ai — live |
| Overall prod health | degraded — five of six dependencies healthy, Meilisearch reports sync stale |
| Newest production tag | prod-v1.4.0, 2026-08-25 |
Commits on main since that tag |
34 |
| Version reported by prod health | "unknown" — the running image does not know its own version |
Anonymous GET /search in production |
307 — main opened it (#561), production has not been redeployed |
| Image build | manual, scripts/build_and_push_prod.sh (not in CI, by design) |
Read those rows twice: the deploy pipeline works and has been used —
prod-v1.4.0 closed a three-month, 532-commit gap. It then immediately started
opening a new one, and the 34 commits sitting in it include the single change
this document spent the most words arguing for. The highest-value action
available still costs one command.
4. Data — the real story¶
Counts pulled live from Supabase:
| Table | Rows | Consequence |
|---|---|---|
judgments |
12,307 | The corpus. Complete. |
document_chunks |
329,237 | RAG substrate. Complete. |
judgments with embedding |
12,307 / 12,307 | Semantic search covers 100% of the corpus |
judgments with full_text |
12,307 / 12,307 | median ~20,000 chars — real documents, not stubs |
chunks with embedding |
329,237 / 329,237 | |
search_analytics |
1,620 | People have actually searched |
profiles |
8 | Eight accounts exist |
collections |
8 | |
extraction_schemas |
0 | |
extraction_jobs |
0 | |
chats |
0 | |
saved_searches |
0 | |
app_events |
0 | |
blog_posts |
0 | |
publications |
0 | |
reasoning_lines |
0 |
Corpus composition (n=1,000 sample, and per-jurisdiction n=300–1,000 checks):
- 59% Polish —
JuDDGES/pl-court-raw, all Sąd Apelacyjny (Court of Appeal), all criminal - 41% UK —
JuDDGES/en-court-raw, EWCA Criminal Division / Crown Court - Decision dates span 2003–2024, clustered on 2013 and 2023
base_extraction_status = completedfor 100% of sampled rowsstructure_extraction_status = completedfor 99.4%deep_analysis_status = pendingfor 100% — the deep-analysis pipeline has never runumap_xis NULL for 100% — the topic map has no coordinates
The asset nobody is showing the user¶
Every judgment already carries a completed extraction against
universal_legal_document_base_schema — 40+ fields, including offender and
victim attributes, offences, sentences, appeal grounds and outcomes. Fill rates
sampled at n=200 per jurisdiction:
| Field | PL | UK |
|---|---|---|
base_appeal_outcome |
200/200 | 200/200 |
base_offender_gender |
189/200 | 199/200 |
base_convict_offences |
153/200 | 198/200 |
base_sentences_received |
153/200 | 179/200 |
This is the differentiated product. /search/extractions already filters and
facets on it. It is not in the primary call to action anywhere.
Data defects a legal user will find in the first minute¶
titleandsummaryare both the raw first ~200 characters of the document. Not a title, not a summary. Search results currently read:
Sygn. akt : II AKa 145/13 WYROK W IMIENIU RZECZYPOSPOLITEJ POLSKIEJ Dnia 6 czerwca…
Neutral Citation Number: [2022] EWCA Crim 463 Case No: 202102226 B2 and 202103504 B2 IN THE…
Meanwhile base_case_name is populated corpus-wide and reads:
The card renders doc.title at
frontend/lib/styles/components/search-document-card.tsx:28. base_case_name
is used only as a labelled field inside the extractions view.
-
Every UK judgment is labelled
case_type = 'Civil'(1,000/1,000 sampled), while the same rows havebase_convict_offencesfilled 198/200 and case names of the formR v …. These are criminal appeals. The facet is exposed on/searchand actively misleads. -
court_levelfor UK rows readsCrown Court(887/1,000) for what are Court of Appeal (Criminal Division) decisions. Crown Court is the court below. -
Two exposed facets are entirely empty:
outcome(NULL for 100%) andcited_legislation(0/500). -
legal_topicsandkeywordsare Polish-only — ~80% filled on PL rows, 0/300 on UK rows. The topic facet silently halves the corpus.
Access posture — verified against the live database¶
Read-only introspection of pg_class and pg_policies over DATABASE_URL,
plus a reachability test using production's own browser-shipped key.
The good news, and it is substantial:
- RLS is enabled on all 43 tables in
public. Zero exceptions. judgmentsanddocument_chunkseach carry an explicitly named policy —"Public read access for judgments"/"…for document_chunks",USING (true), rolepublic. The corpus is world-readable by design, not by accident.collections,chats,profilesare owner-scoped toauthenticated(auth.uid() = user_id). Verified by observation too: theanonkey returns 0 rows where the service-role key returns 8, 0 and 8 respectively.- Of the 16 write policies reachable by role
public, 13 are gated onauth.role() = 'service_role'— anon cannot use them.
The finding that reframes issue #510:
The anon-role JWT is embedded in the production landing page's HTML, as it must
be for any browser Supabase client. Using that key, pulled from
https://juddges.augustyniak.ai/ and nothing else:
The login wall on /search protects nothing. Anyone who opens devtools
already has full read access to all 12,307 judgments and all 329,237 chunks,
straight from Supabase REST, bypassing the app entirely. The gate does not
secure the corpus — it only stops legitimate users from reaching the UI built
to read it.
Given the named "Public read access" policies, this is almost certainly the
intended posture for public court rulings. It should still be a conscious,
recorded decision rather than an inherited one.
Two real weaknesses, both low severity, neither blocking — plus one item the 2026-08-21 pass read backwards:
contact_submissions—INSERTfor{anon, authenticated}withWITH CHECK: true. Unbounded anonymous insert. Because the anon key reaches PostgREST directly, any rate limiting or validation in the app's/api/contacthandler is bypassable. Same shape onfeature_requestsandsearch_feedback(both additionally constrained touser_id IS NULL OR user_id = auth.uid(), which does not bound volume).anonholdsINSERT/UPDATE/DELETEgrants on 33 of 43 tables, includingjudgmentsandprofiles. No policy currently lets anon use them, so nothing is exploitable today — but the grants are the wrong default, and a future permissive policy would silently become a write hole. This is the pattern already recorded insupabase-grants-are-not-a-boundary: revoke at the grant layer, do not rely on policy discipline alone.- 11 tables have RLS on with zero policies — deny-all. For eight of them the 2026-08-21 pass judged that correct. For the three blog tables it called it a trap, and was wrong.
Corrected 2026-09-01. The original text called the three blog tables a trap, claimed
anonholds aSELECTgrant on them, and recommended adding aSELECTpolicy before seeding. All three parts were wrong, and the recommendation would have punched a hole in a deliberate boundary.
supabase/migrations/20260804000001_create_blog_tables.sql:190-193says so verbatim: "blog_posts / blog_tags / blog_categories: intentionally no anon or authenticated policy or grant. Public reads are served by the backend through the service-role client (which bypasses RLS), matching the service_role-only EXECUTE grants onlist_public_blog_postsandget_public_blog_post." The grants at:222-224areGRANT ALL … TO service_roleand nothing else — anon holds noSELECTgrant.The read path is
backend/app/api/blog.py:279and:331, both callingget_admin_supabase_client().rpc(...). Seeding rows intoblog_postsis sufficient on its own; no policy change is needed, and adding an anonSELECTpolicy would expose unpublished drafts that the RPCs filter out.
publications and its three satellite tables are in the same deny-all state
and return a hard 401 permission denied to anon. Whether their read path is
equally service-role-mediated was not re-verified on 2026-09-01 — check
before acting on /publications.
Performance¶
count_judgments_filtered — invoked on the first page of every search to
populate estimated_total — takes 388 ms against the production database.
That sits on the first-page latency budget. It is best-effort and its failures
are swallowed, so it is a candidate for moving off the hot path rather than a
correctness problem.
(A 12-second ILIKE scan also showed up while probing, but that was an ad-hoc
query of mine, not a path the application takes — search goes through
Meilisearch and the tsvector/HNSW indexes. Recorded only so the number is not
mistaken for a product measurement.)
5. Feature inventory¶
63 page routes, 101 Next.js API route handlers, 30 FastAPI routers plus 3
LangServe chains (/qa, /chat, /enhance_query).
5a. Working — verified wiring plus real data behind it¶
| Feature | Route | Evidence |
|---|---|---|
| Keyword search | /search (Text mode) |
Meilisearch, 12,307 docs indexed. As of 2026-09-01 prod reports meilisearch: degraded — sync stale, so the index may be behind the corpus. |
| Semantic search | /search (Vector mode) |
pgvector; search_judgments_hybrid RPC responds; 100% embedding coverage |
| Structured-field search | /search/extractions |
base-schema facets/histograms/NL-filter; 100% extraction coverage |
| Document reader | /documents/[id] |
metadata + HTML + similar + versions endpoints all present |
| Auth | /auth/* |
Supabase; 8 accounts; login/signup/reset/update all wired |
| Chat / RAG | /chat, /chat/[id] |
LangServe /chat + pgvector chunks; fork/export/history routes present — but see the LLM billing note below |
| Collections | /collections, /collections/[id] |
8 rows; full CRUD |
| Admin panel | /admin/* |
require_admin on every backend endpoint; stats/users/system/content |
| Health & status | /status |
public, polls /api/health/status; reports degraded as of 2026-09-01 (§2) |
| Judge fingerprint | /judge-fingerprint |
reads judgments; BFF proxy at /api/judge-fingerprint/* (fixed since Aug-05) |
| Reasoning lines | /reasoning-lines |
BFF proxy at /api/reasoning-lines/[...path] (fixed since Aug-05) |
| Blog | /blog |
now fetches /api/blog/posts (mock data removed since Aug-05) |
| Precedents | /precedents |
LLM-on-demand, no table dependency |
| Argumentation analysis | /argumentation-analysis |
LLM-on-demand, no table dependency |
| i18n EN/PL | app-wide | 577 keys each, symmetric; 180 of 285 components call t() |
Wiring is not billing — added 2026-08-25. Every "working" grade above was established by tracing code paths and data, not by making a live LLM call. On 2026-08-25 the
prod-v1.4.0release build failed generating release notes withopenai.RateLimitError: 429 … You have no credits remaining. TheOPENAI_API_KEYin the repo.envis exhausted.If production shares that key, then chat/RAG, the precedent finder, argumentation analysis, schema generation and the extraction pipeline all fail at the first request — and nothing would have shown it, because
/api/health/statusdoes not probe the LLM. Every dependency it does probe can report healthy while this is true.Search is unaffected either way: judgment and chunk embeddings are BGE-M3 served by TEI, and keyword search needs no LLM. Both
/searchmodes keep working.Tracked in #546. Treat the four LLM-backed rows in this table as wired and untested end-to-end until that issue is closed.
5b. Hidden — built, reachable, invisible¶
The sidebar links 17 distinct routes, two of which (/auth/login,
/auth/sign-up) show only when signed out — so a signed-in user is offered
15 destinations. There are 63 pages. Forty-six have no navigation entry
at all; they are reachable only by typing a URL or following an in-page link.
The ones that matter, because they represent finished work nobody can find:
| Route | What it is |
|---|---|
/extract |
The extraction workbench — pick collection + schema, run a job |
/extractions, /extractions/[id] |
Job list and result detail |
/schemas, /schemas/[id], /schema-chat |
Schema library and the LLM schema builder |
/reasoning-lines, /reasoning-lines/[id] |
Reasoning-line explorer (in the API layer, not the nav) |
/statistics |
Corpus statistics |
/settings |
Embedding provider, email alerts |
/documents/[id] |
The document reader itself — no nav entry |
/help, /changelog, /about, /team, /ecosystem, /use-cases |
Everything a new user would read first |
/publications, /blog |
Public-facing content surfaces |
/schemas/base is in the nav, but /schemas (the library) is not — so the
one schema route a user can reach is the read-only base definition.
5c. Empty — the feature works, there is nothing in it¶
| Surface | Backing table | Rows |
|---|---|---|
/schemas, /schema-chat, and the schema picker in /extract |
extraction_schemas |
0 |
/extractions |
extraction_jobs |
0 |
/chat history |
chats |
0 |
/saved-searches |
saved_searches |
0 |
/blog |
blog_posts |
0 |
/publications |
publications |
0 (page falls back to a static list, masking this) |
/reasoning-lines |
reasoning_lines |
0 |
/topic-modeling map |
judgments.umap_x |
NULL for all 12,307 |
| Analytics funnel | app_events |
0 |
/blog deserves a note of its own, because the 2026-08-21 pass got it wrong.
The table is empty and that is the only reason the page is empty. Reads go
through service-role RPCs (backend/app/api/blog.py:279, :331) that bypass
RLS by design, so seeding rows is sufficient — no policy or grant change is
needed, and adding one would expose drafts the RPCs filter out. See §4.
/extract deserves a specific note: with extraction_schemas empty, a user who
finds the extraction workbench has nothing to select. The feature is complete and
unusable at the same time. Issue
#507 already asks whether the
pipeline still runs end-to-end; the answer from the data is that it has never run
in this environment.
5d. Broken or degraded¶
State on main as of 2026-09-01. Rows fixed between the 2026-08-21 probe and
that date are marked inline and kept, because the failure they represent is
still worth recognising; rows with no such marker are still broken.
| Problem | Location | Note |
|---|---|---|
| Anonymous search is unmetered | backend/app/server.py, backend/app/api/search.py:331 |
PR #561 opened /search to guests; the rate limit that was supposed to bound it never landed. SlowAPIMiddleware is never registered in server.py — only app.state.limiter and the exception handler are — so DEFAULT_RATE_LIMITS applies to nothing, and documents_search carries no @limiter.limit decorator (41 other call sites in backend/app/ do). Tracked as #565. This is the successor to the old "guest search is dead code" finding: frontend/lib/guestMode.ts was deleted in 0eb91b6c and replaced by frontend/lib/guest/session.ts, a server-authoritative Redis counter on an HttpOnly cookie — the right shape, with the backstop missing. |
/search/extractions links every row to a route that does not exist |
frontend/app/search/extractions/page.tsx:185 |
The row <Link> targets /judgments/<row.id> and there is no frontend/app/judgments/ directory. Every result row 404s. The intended target is /documents/[id]. This is the surface §4 calls "the differentiated product": 100% populated, and unclickable. |
| The public landing page invented its own trending searches | /, dashboard trending-topics |
#558 — the home page rendered hardcoded query counts as if they were real search activity. Fixed by PR #564, 2026-09-01, which removed the fabricated data rather than backfilling it. Kept here because it is the same failure mode as the facets below: production telling the first user something untrue. |
| Meilisearch hybrid disabled | docs/how-to/re-enable-hybrid-search.md |
The bge-m3 embedder is not registered on the live index, so semantic_ratio > 0 returns 502. The UI option is hidden and persisted hybrid is coerced back to text to avoid a 502 loop (frontend/lib/store/searchStore.ts:1143-1146). Vector mode via pgvector is unaffected and works. |
| Search results show boilerplate | search-document-card.tsx:28 |
See §4 defect 1 |
base_case_name ranked last |
meilisearch_config.py searchableAttributes |
title (boilerplate) is weighted highest; the clean case name is weighted lowest. Searching a party name ranks badly. |
Prod reports version: "unknown" |
running image | No way to tell what is deployed from the outside |
| Docs drift | CLAUDE.md |
States search "uses Supabase pgvector". The /search page is Meilisearch-first; pgvector serves Vector mode and RAG. Both are true; the doc reads as if only one exists. |
| 6 backend test files skipped as obsolete | test_schemas_integration.py, test_schemas_validation.py |
"mock app.schemas.InformationExtractor which no longer exists" — schema extraction has no active integration coverage |
| Unbounded anonymous insert | contact_submissions policy WITH CHECK: true |
Reachable via PostgREST with the browser-shipped key, bypassing /api/contact entirely. Same shape on feature_requests, search_feedback. See §4. |
anon holds write grants on 33 of 43 tables |
Supabase grants | Not exploitable today (no permissive policy), but the wrong default — one careless future policy turns it into a write hole. See §4. |
/publications masks an empty table |
publications — RLS on, zero policies, no anon grant |
Anon gets 401 permission denied, and the page falls back to a static list rather than showing an honest empty state. Whether the intended read path is a service-role RPC (as it demonstrably is for blog_*) was not verified. See §4. |
5e. Known, filed, and correctly deferred¶
The issues that shaped this snapshot, with their state as of 2026-09-01:
- ~~#510
priority: critical~~ — landing page promises search, delivers a login form. Closed, implemented by PR #561. Not deployed. - #565 — open, and the direct consequence of the above: anonymous search is unmetered because the documented rate-limit backstop was never wired up (§5d).
- #527 — extraction detail page paints without schema/collection metadata
- #507 — verify the extraction pipeline still runs
- #506 — GDPR deletion requests are recorded and never processed, while the API promises erasure within 30 days. (The original text described this as "no deletion route", which understated it: the route accepted requests and dropped them.) Partly addressed by PR #562, which wired the deletion processor to an admin queue. Still open for the subject-facing route and the admin UI.
- #546 — open; the OpenAI key has no credits and no health check would show it (see §5a).
- #516 — landing runs 28 animations with no
prefers-reduced-motionguard (a global guard landed inc9f0d48; verify and close) - #518, #195 — activation-funnel analytics
- #61, #60 — Research Assistant, Schema Marketplace (large, deferred, correctly so)
6. The gap between "code complete" and "usable by a stranger"¶
Four gaps, in order of how fast a first user hits them.
- They hit a login wall before they see anything. ~~Nothing else matters
until this changes.~~ Fixed on
mainby PR #561 (#510 closed 2026-09-01) — and still true in production, which has not been redeployed and answers307on/search. The gap moved from a product decision to a deploy. - If they get in, the results are unreadable. Ten rows of
Sygn. akt : II AKa 145/13 WYROK W IMIENIU…— when the clean case name is already sitting in the same row of the same table. - The nav points at empty rooms and hides the full ones.
/topicsand/topic-modelinghave no coordinates. Meanwhile/search/extractions— 100% populated, genuinely differentiated — is one unlabelled entry whose every result row 404s (§5d), and/extractis not in the nav at all. (The/history404 that stood here was fixed by PR #528, merged 2026-08-25.) - Two facets lie and two are empty.
case_type: Civilon criminal appeals,court_level: Crown Courton Court of Appeal decisions,outcomeandcited_legislationempty.
None of these is a rewrite.
7. Open follow-ups¶
The original document ended here with a five-day plan. It is deleted, because most of it shipped and the rest has an issue number:
| Original plan item | State on 2026-09-01 |
|---|---|
Day 1 — merge #528, deploy, fix version: "unknown" |
#528 merged 2026-08-25; prod-v1.4.0 cut 2026-08-25. version still reports "unknown". |
Day 2 — open /search and /documents/[id] to guests, meter them |
Shipped by PR #561. The metering did not ship — #565. |
Day 3 — display base_case_name, re-weight Meilisearch, fix the lying facets |
Not started. |
Day 4 — rebuild the sidebar, seed extraction_schemas, run one extraction job |
Not started. |
| Day 5 — cold-path walkthrough, GDPR deletion path, activation events | GDPR deletion partly shipped by PR #562. The rest not started. |
What remains from this snapshot with no issue tracking it. Listed as findings, not as a schedule — each is a one-line description of work someone will have to file before doing:
version: "unknown".VERSIONis not plumbed into the image, so there is no way to confirm from outside which build is deployed. This is what made the 532-commit drift invisible for three months.- Display title resolution. One resolver —
base_case_name→case_number→title— used bysearch-document-card.tsx:28, the document page header and the extractions table. The data is already in the row (§4). - Meilisearch field weighting.
searchableAttributesinbackend/app/services/meilisearch_config.py:75putstitle(boilerplate) first andbase_neutral_citation_numberin the tie-break tail. Searching a party name ranks badly. - The two lying facets.
case_type: Civilandcourt_level: Crown Courton UK criminal appeals — a migration plus a reindex, and aDatabase Contracttest asserting nojurisdiction = 'UK'row with non-emptybase_convict_offencesis labelledCivil. - The two empty facets.
outcomeandcited_legislationrender empty dropdowns;legal_topics/keywordsare Polish-only and silently halve the corpus. /search/extractionsresult rows 404 (§5d) — a one-word href fix on the surface this document calls the differentiated product.extraction_schemasis empty, which makes/extract,/schemasand/schema-chatcomplete and unusable at the same time.contact_submissionsaccepts unbounded anonymous inserts via PostgREST, bypassing/api/contact(§4).anonholds write grants on 33 of 43 tables — not exploitable today, the wrong default tomorrow (§4).- Sidebar composition. 17 sidebar routes against 63 pages, with the
populated surfaces buried and the empty ones promoted (§5b, §5c) — and
sidebar-map.mddocumenting a sidebar that has not existed for some time. - Meilisearch sync is stale in production (§2) —
last_completed_at: null,last_error: "Event loop is closed". - 6 backend test files skipped as obsolete, leaving schema extraction with no active integration coverage (§5d).
8. Explicitly deferred at the time of the snapshot¶
Naming these so their absence is a decision rather than an oversight.
| Deferred | Why |
|---|---|
| Re-enabling Meilisearch hybrid | Needs a BGE-M3 TEI server, an embedder registration and a full reindex. Vector mode via pgvector already gives users semantic search. |
| Deep analysis pipeline | deep_analysis_status = pending for all 12,307. A large batch LLM spend with no user-facing surface ready to consume it. |
| UMAP coordinates / topic map | Same shape of cost. Hide the surface instead. |
| Research Assistant (#61), Schema Marketplace (#60) | Multi-week features. Correctly deferred. |
| Rewriting the 6 obsolete schema test files | Real debt, no immediate user impact. File it. |
| Splitting the 10+ files over 500 lines (#59) | Refactoring while shipping trades certainty for tidiness. |
| Grafana dashboards (#196) | Sentry and Langfuse are live and sufficient for a first cohort. |
| Major-version Dependabot bumps | Not while shipping. |
9. Standing risks¶
- Manual deploys drift, and they start drifting again immediately. Three
months and 532 commits is what "manual, when I remember" produced the first
time.
prod-v1.4.0closed that gap on 2026-08-25; by 2026-09-01 the gap was 34 commits and already contained the #510 fix. Whatever else changes, put a calendar trigger or a CI release job behindbuild_and_push_prod.sh. - Fixing something on
mainis not fixing it. Every claim in this document about what a user experiences has to be read against the deployed build, not the branch.version: "unknown"in/api/health/statusmeans there is no cheap way to tell the two apart from outside. - Green tests do not mean working pages. PR #528's own description makes the
point precisely: the
/historyunit tests mockedfetch, so a missing route was invisible. The route-contract suite is the antidote — extend it whenever a page gains a BFF call. - The corpus is narrower than the landing page implies. Criminal appellate
decisions from two jurisdictions, 2003–2024. Say so on
/helpand on the landing page. A user who expects Polish civil case law and finds only criminal appeals will not come back, and will be right not to. - Eight accounts and zero events means there is no behavioural baseline. The first real cohort is the only chance to capture one.
- An empty page has more than one possible cause and they look identical.
Zero rows, a deny-all RLS policy, and a read path that deliberately runs
through a service-role RPC all render as "nothing here". Whenever a surface is
empty, read the migration before you read the data — and prefer an explicit
REVOKEover relying on every future policy being written correctly. - A deny-all table is not automatically a bug. The 2026-08-21 pass read the
three blog tables' zero-policy state as a trap and recommended adding an anon
SELECTpolicy; the migration that created them says in a comment that the absence is deliberate (§4). An audit that only queriespg_policieswill recommend widening a boundary someone closed on purpose. Read the migration.