Imported from juhinebhnani4/LDIP (
.claude/skills/blast-radius-research/SKILL.md). Install upstream withnpx skills add juhinebhnani4/LDIP --skill blast-radius-research. Copyright stays with the author.
Deep Research Protocol — Two-Phase Investigation
When to invoke: Before implementing ANY proposed change, or before recommending solutions to any operational/architectural problem. This protocol replaces ad-hoc research prompts.
Why this exists: Multiple failure modes observed across sessions (2026-04-21/22):
- Contradiction metadata: Three passes needed because each asked a different question shape. Missed that skip-1-mention already existed, missed two tracker types with similar names, missed a was_escalated bug on the failure path.
- Railway cost: Generic cloud cost advice ("enable auto-sleep, reduce concurrency, combine services") proposed without reading BUGS.md, railway.toml, or understanding how Celery workers connect to Redis. Three of four suggestions would have made ARCH-002 worse.
- Common root: Research looked at the surface (symptom, file list, generic patterns) without understanding the actual system (runtime behavior, prior analysis, interaction boundaries).
Phase 0: Prior Fix Audit (Only when touching previously-fixed code)
Why this exists (2026-05-25): GAP-11 was "FIXED" in 2026-05-06 but the fix was broken —
library_document_idwas passed asdocument_id, violating an FK constraint. The error was swallowed bypersist_cost's catch block. Three weeks of library OCR ran with zero cost tracking. The summary service had the same shape (2026-04-29):self._get_supabase_client()deployed instead ofself.supabase, silently failed on 3 matters. GAP-19:chunk.parent_chunk_indexdoesn't exist — every act upload silently failing at chunking since parent-child chunker was introduced.
Trigger: When BUGS.md shows a bug as FIXED in the subsystem you're changing, OR when you're building on top of a previous fix.
For each FIXED bug in the affected subsystem:
- Read the fix description in BUGS.md. What was the claimed fix?
- Read the actual code. Does it match the description?
- Was the fix verified in production? Look for "production-verified", "live verified", or "live-tested" in the BUGS.md entry. If absent: treat as UNVERIFIED.
- For UNVERIFIED fixes: trace the code path to the DB write / API call / state mutation. Does the value survive to persistence? Check FK constraints, NOT NULL constraints, type mismatches, attribute names.
- Can the fix be exercised with available test data? If the code path requires specific input (scanned PDF, >30 page doc, specific document type, external service failure): say so. If you can't trigger it, mark as "DEPLOYED BUT UNEXERCISED" in BUGS.md, not "FIXED."
Prompt fragment:
PHASE 0: Check BUGS.md for any bug marked FIXED in the subsystem I'm
changing. For each: read the claimed fix, read the actual code, check if
it was production-verified. If not verified: trace the code path to the
DB write and check for FK/type/attribute mismatches. If the fix requires
specific input to trigger: can I provide that input? If not, flag it.
Phase 1: Open Exploration (Inductive — What Don't I Know?)
Purpose: Understand the actual system before proposing anything. This phase prevents recommending solutions that conflict with existing architecture or ignore prior analysis.
This phase is mandatory. Skipping it is how every bad recommendation in this project's history was born.
1.1 — What does the project already know?
Before analyzing any problem, check whether it's already been analyzed.
- Search
BUGS.mdfor the problem domain keywords (worker, cost, concurrency, queue, sleep, memory, etc.) - Search
ARCH-PATTERNS.mdfor the pattern shape (P1-P9) - Search
.claude/projects/*/memory/for prior session findings - Search
CLAUDE.mdfor relevant rules or constraints
If prior analysis exists, START FROM WHERE IT LEFT OFF. Do not restart from scratch. State what the prior analysis found and what's new.
Prompt fragment:
FIRST: Search BUGS.md, ARCH-PATTERNS.md, and memory files for prior
analysis of this problem. Search for: [DOMAIN KEYWORDS]. If prior analysis
exists, summarize what it found and identify what's NEW or CHANGED since
then. Do not re-derive known conclusions.
1.2 — What does the system actually do? (Not what should it do)
Read the actual runtime configuration, not just the application code.
- Deployment configs:
railway.toml,start-worker.sh,Procfile,docker-compose.yml,vercel.json - Startup behavior: What processes launch? What do they import? What memory do they consume?
- Connection patterns: How does the worker connect to Redis? How does the API connect to Supabase? Pull-based or push-based? Persistent or transient?
- Runtime data: If the question is about cost, query
llm_costs. If it's about failures, queryprocessing_jobs. If it's about timing, check logs. Code tells you what SHOULD happen; data tells you what DOES happen.
CRITICAL — Trace the current failure path, not just the happy path:
Before reporting "this behavior doesn't exist," trace what the system does TODAY when the user hits the problem scenario. Walk through the exact code path step by step, including every branch and edge case. The current behavior IS the baseline — you can't evaluate a proposed change without understanding what it replaces.
Specifically:
- What code path runs today when the problem scenario occurs?
- What conditions trigger each branch? What happens when those conditions are
0,None, or empty? - What does the user actually see on screen at the end of this path? Name the exact component that renders and the exact text/state shown. If you can't name the component and the text, you haven't finished Phase 1.
Why this was added (2026-04-29): Q&A processing guard research reported "no handling exists" when in fact partial handling existed in
hybrid_search.py(BM25 fallback +searchNoticerendering). The agents described what the fallback code does in the happy case but never traced what happens whentotal_chunks == 0(the fallback condition is false, user sees generic "no results"). Three gaps were only discovered by manual line-by-line code reading.
Prompt fragment:
Read the actual deployment/runtime configuration, not just the application
code. For this problem, read: [SPECIFIC CONFIG FILES]. Then answer: What
processes actually run? How do they connect to external services? What is
their actual resource consumption? If live data exists that could answer
the question directly, say what query would answer it.
THEN: Trace what happens TODAY when the user hits the problem scenario.
Walk through the exact code path step by step. At every branch condition,
check what happens when the value is 0, None, or empty. Follow the path
all the way to what the user sees on screen — name the component and the
text. If you can't, you haven't finished.
THEN: After tracing the PRIMARY code path, ask: "What OTHER processes
write to the same state this path reads?" Specifically:
- If the problem involves a UI that polls/watches a DB table or API:
enumerate ALL writers to that table/endpoint, not just the one
triggered by the user action under investigation.
- If the problem involves "stuck" or "missing" state: check whether any
BACKGROUND process (beat task, sweep, pre-generation, webhook) writes
to the same state on a different trigger.
- State the effective severity: is the user stuck PERMANENTLY, or does
a background process unstick them within N seconds? Name the process.
Why "all writers" was added (2026-05-25): UX-015 was documented as "stuck at 0% forever." Phase 1 traced the act upload path and correctly found zero
processing_jobs. But the summary pre-generation job (fired bydetect_contradictionsfor ANY document in the matter) creates aprocessing_jobsrow, which unsticks the page in ~5-10s. The severity was wrong because only one writer was traced. Same pattern: E2E-007 (traced finalize but not the sweep that re-triggers it), UX-003 (traced one status source but missedtabProcessingStatusfrom a different fetch).
1.3 — What systems interact that aren't in the codebase?
Map the boundaries between this system and external systems that won't appear in any grep.
- Infrastructure model: How does Railway detect idle? How does it scale? What triggers a restart?
- Broker model: How does Celery discover tasks? Pull from Redis? Push? What happens if the process sleeps?
- API quotas: Gemini RPM, OpenAI rate limits, Supabase connection pool limits, Document AI per-page pricing
- Timing dependencies: What assumes something else is running? (Beat assumes worker is alive. Recovery sweeps assume beat is running. Pipeline completion assumes detect_contradictions will eventually fire.)
Prompt fragment:
What external systems interact with this component? For each: How does
the interaction work (pull/push, persistent/transient, HTTP/broker)?
What happens if one side goes down or sleeps? What assumptions does each
system make about the other's availability?
1.4 — What constraints exist that aren't obvious from the symptom?
The surface problem ("RAM is too high") hides constraints that eliminate most generic solutions.
- Read the ARCH entries referenced by the problem area
- Check if proposed solutions have been explicitly REJECTED before (BUGS.md tracks these with reasons)
- Check if the problem interacts with known architectural debt
Prompt fragment:
Before proposing any solution: check BUGS.md for approaches that have
already been REJECTED for this problem (search for "REJECTED", "DEFERRED",
"UNSAFE"). Check if the problem area intersects with any ARCH-001 through
ARCH-007 entries. List any constraints that would disqualify generic
solutions.
1.5 — System-level synthesis (the zoom-out step)
Purpose: After tracing individual code paths (1.2) and mapping interactions (1.3), step back and evaluate the subsystem as a whole. Individual paths can each look reasonable while the system they form has structural gaps.
This step catches three failure modes that 1.1–1.4 miss:
A. Multiple entry paths, no shared gate: For any subsystem you're investigating, enumerate ALL entry paths and ALL exit paths. If there are multiple ways to reach the same state (e.g., a document becoming a library_document, a job reaching COMPLETED status), map all of them. Gaps between entry paths reveal classification/routing flaws — one path may have validation that another lacks.
B. No recovery from wrong decisions: For any decision point (classification, routing, status transition): what happens if the decision is WRONG? Is there a recovery path? If the user realizes the mistake 10 minutes later, what can they do? If the answer is "nothing" or "delete and start over," that's a structural finding — not a feature request.
C. Design intent vs. actual architecture: Looking at all the paths, tables, and decision points you've mapped — does this subsystem have a coherent design, or are there structural gaps (missing paths, orphaned states, parallel implementations that should be unified, dead ends)? Does the architecture match what the system is supposed to do?
Why this was added (2026-04-29): Library document classification research traced the upload path correctly but stopped there. The library system actually has 4 entry paths, only 1 of which was exposed in the UI (and that one had no selector). The auto-fetch pipeline was sophisticated but backwards-only (detected Acts in case files, couldn't detect that an uploaded file IS an Act). Misclassified documents had no recovery path. Two parallel chunk tables existed for the same logical work. None of these flaws were visible from tracing a single code path — they only emerged when asking "does this subsystem make sense as a whole?" The user had to prompt this zoom-out twice before the structural analysis happened.
Prompt fragment:
SYNTHESIS (after completing 1.1–1.4): Step back from individual code paths.
A. How many ENTRY PATHS lead into this subsystem? Map all of them. Do they
all pass through the same validation/classification gate, or do some
bypass it? If bypassed — what breaks?
B. For the key DECISION POINTS in this subsystem: what happens when the
decision is WRONG? Can the user or system recover, or is it permanent?
If permanent — that's a structural finding.
C. Looking at the full map: does the subsystem's architecture match its
design intent? Are there parallel implementations that should be unified,
missing paths that should exist, or orphaned states with no transitions
out? Name any ARCH-pattern matches (P1–P6 from ARCH-PATTERNS.md).
D. Query the LIVE DATABASE for the subsystem's tables. Check: status
distribution (does it match what the code predicts?), data completeness
(do terminal-status records actually have their data?), stuck/orphaned
records. If data contradicts code, the data wins.
E. For any PARALLEL TABLES serving the same logical purpose: compare
schemas column by column. Every column in table A missing from table B
is a gap where future improvements to A are silently not applied to B.
D. QUERY THE LIVE DATA. Code tells you what SHOULD happen; the database
tells you what DID happen. For the subsystem you've mapped, run:
- Status distribution: SELECT status, COUNT(*) GROUP BY status
- Data completeness: Do records in terminal status actually have
the data they should? (e.g., completed docs should have chunks;
chunks should have embeddings)
- Orphans and stuck records: Any rows stuck in non-terminal state
for >1 hour? Any link table rows pointing to deleted parents?
If the data contradicts the code's claims, the data wins.
E. For PARALLEL TABLES that serve the same logical purpose (e.g.,
chunks vs library_chunks, two embedding columns): compare schemas
COLUMN BY COLUMN. List every column in table A that's missing from
table B. For each missing column, ask: does table B's pipeline
populate an equivalent? If not — that's a gap where every future
improvement to A must be manually remembered for B.
If you can't confidently answer these five questions, you haven't
explored enough — go back to 1.2.
Why D and E were added (2026-04-30): Full library subsystem audit found 3 P0 gaps that were invisible from code reading alone. (D) Querying
library_chunksrevealed 77% had NULL embeddings and 1 "completed" doc had 0 chunks — the code's completion logic was vacuously true on empty sets. (E) Comparingchunksvslibrary_chunkscolumn-by-column revealedfts,embedding_model_version,text_start_offset/end_offsetall missing — meaning every search improvement tochunkswas silently not applied to library search. Both gaps existed for months because prior research read code paths without verifying data reality.
1.6 — "It doesn't exist" is a CLAIM, not an observation — run the absence search
When you are about to assert that some capability, file, path, endpoint, column, caller, or recovery mechanism does not exist — "the frontend is client-only", "there's no delete path", "this is never called", "no rate limiter here", "no server-side auth", "the infra for X is absent" — STOP. A universal-negative is the single easiest claim to get wrong, because you can only reason from the files you happened to open. Inferring absence from the local files in front of you is not evidence of absence — it is absence of evidence.
Before the negative claim leaves your mouth — and ESPECIALLY before it becomes a load-bearing premise that rules a solution in or out — run the cheap codebase-wide search that would disprove it:
- Search the WHOLE repo, not the subtree you're standing in. glob/grep for the thing itself:
middleware.ts,*/server.ts,next/headers,createServerClient,.delete(, the route string, the column name. - Search by CONCEPT, not one spelling. A capability can be implemented many ways — "server-side auth" might be a
middleware.tsOR a server client ORcookies()/headers()usage. One name returning nothing proves nothing. - If the claim is "X never happens", enumerate every writer/caller/dispatcher of X, not just the one path you traced (this is the 1.5A "all entry paths" rule, applied to a negative).
Only after the search comes back empty may you say "does not exist" — and then say how you checked ("grep next/headers|createServerClient across frontend/src = 0 matches"). If it comes back non-empty, you just caught a wrong premise before it cost a decision.
Why this was added (2026-06-11): During FE-ARCH-01
choose-solution, I declared the frontend "client-only — no server-side auth path exists" and used it to rule the L3 server-side gate "infeasible / grandiose, would require building absent infra." I'd inferred that from the API-client files being'use client', without searching the repo. A singlegrep next/headers|createServerClientwould have surfacedmiddleware.ts(already does server-side auth on every/matterrequest) andlib/supabase/server.ts(a server-side DB client). The final pick (L2) happened to survive, but the reason was wrong — and had the app had 5 resource routes instead of 1, the false premise would have driven the wrong pick. Same shape as the "verify against the live system, not a theory" post-mortem, here applied to architectural-capability existence during design, which 1.1–1.5 didn't explicitly cover.
Prompt fragment:
Before asserting that anything DOES NOT EXIST (a capability, file, endpoint,
column, recovery path, caller, infra), run the codebase-wide search that would
DISPROVE it — search by CONCEPT across the WHOLE repo, not the subtree you've
been reading. Report how you checked and the match count. Inference from the
files you happened to open is NOT evidence of absence. This matters most when
the negative is a load-bearing premise for ruling a solution in or out.
Phase 2: Directed Verification (Deductive — Validate What I Think I Know)
Purpose: Once you have a proposed change, verify it won't break anything and find the simplest implementation. This phase prevents implementing the wrong thing correctly.
Only enter Phase 2 after Phase 1 has produced a specific, informed proposal.
2.1 — Does this already exist?
Before building anything, check whether the behavior is already implemented.
- Grep for the behavior, not just the function name
- Example: "skip 1-mention entities" → grep for
< 2,<= 1,total_statements,len(all_statements)
"Does this exist?" has THREE answers, not two:
- YES → report what exists and STOP
- NO → proceed to 2.2
- PARTIALLY → describe what exists, what's missing, and what's broken about the partial implementation. This is the most dangerous answer because it means the proposed change must integrate with existing code, not replace it. List the specific gaps.
Why this was added (2026-04-29): Q&A processing guard research answered "NO, zero handling exists" when the real answer was "PARTIALLY — BM25 fallback exists but has 3 gaps (only fires on zero results, fails when total_chunks=0, no upfront check)." The binary YES/NO framing caused the agents to miss existing infrastructure that the implementation needed to integrate with.
Prompt fragment:
BEFORE tracing any blast radius: search for whether this behavior already
exists in the codebase. Grep for the BEHAVIOR (not just the function name).
[SPECIFIC GREP SUGGESTIONS].
Answer with one of: YES (stop), NO (proceed), or PARTIALLY. If PARTIALLY:
describe what exists, what's missing, and what's broken. Partial
implementations are the most dangerous — the new code must integrate with
what's already there, not ignore it.
2.2 — What types cross function boundaries?
At every function call in the chain, name the exact type going in and coming out.
- If two functions use different types with similar names, that's a critical finding
- Trace: where does the value ORIGINATE, and does that object survive to the PERSISTENCE layer?
For cross-stack changes (backend → frontend or vice versa):
Don't stop at the API boundary. Trace what happens INSIDE the receiving side when the data arrives. Specifically:
- What properties survive serialization → deserialization → object construction?
- If the backend sends a typed response (e.g.,
ErrorDatawithcode), does the frontend preserve that type, or does it construct a simpler object (e.g., plainError) that drops fields? - If the frontend has conditional rendering (e.g., "show retry button if retryable"), what conditions does the received data need to satisfy? Will the new data satisfy them, or will it fall through to a wrong path?
Why this was added (2026-04-29): Backend sent
{code: "DOCUMENTS_PROCESSING", retry_suggested: true}via SSE. FrontenduseSSE.tscreatednew Error(errorData.error)— discarding thecodefield. ThencanRetryError()checkedinstanceof ApiError(not plain Error) → returned false → showed auto-dismissing toast instead of persistent retry alert. The data was correct at the API boundary but lost in transit through frontend internals.
Prompt fragment:
At every function call boundary, name the EXACT TYPE going in and coming
out. If two types have similar names (CostTracker vs LLMCostTracker), flag
this prominently. Trace: where does the value originate, and does that
object instance survive to where it's consumed/persisted?
For cross-stack changes: trace PAST the API boundary into the receiving
side. What properties survive? What type does the receiver construct?
Does it drop any fields the sender included? If the receiver has
conditional rendering or branching, will the new data satisfy the
conditions or fall through to the wrong branch?
2.3 — What happens on every branch?
For any function that returns Optional or can raise, trace the None/exception path. For any boolean flag, trace who sets it in EVERY branch.
- Happy path alone is insufficient
- If a flag stays at its default on a path where it semantically shouldn't, that's a bug
Apply this to EXISTING code too, not just proposed code. When the problem involves a UX bug, trace every branch of the existing handling — including the "no handling" path — all the way to what the user sees on screen. Name the component, the text, and why it's wrong.
Trace the full lifecycle, not just the processing phase. For any component with setup → processing → cleanup/teardown phases (SSE streams, WebSocket connections, async operations with finally blocks, React useEffect cleanups), trace ALL three phases. Cleanup/teardown code often has its own branching logic that can overwrite state set during processing.
Verify enumerations are complete. When writing a list of values that should cover "all X" (all statuses, all event types, all error codes), verify the list against the source of truth (the enum definition, the database column values, the API spec). Don't write from memory — read and cross-reference.
Why this was added (2026-04-29):
- Lifecycle miss:
useSSE.tsstream-end cleanup (line 620-647) had its own error-creation logic that overwrote theDOCUMENTS_PROCESSINGerror set during event processing. Research traced event processing but not stream teardown.- Enumeration miss:
_PROCESSING_STATUSESwas written with 4 of 6 pre-terminal statuses.ocr_completewas missed because the list was written from memory instead of cross-referencing theDocumentStatusenum.
Prompt fragment:
For every function that returns Optional or can raise: trace the
None/exception path. For every boolean flag: trace who sets it in EVERY
branch. Report any flag that stays at its default where it shouldn't.
Apply this to EXISTING code in the problem area too, not just proposed
code. For UX bugs: trace the current code path to the screen. Name the
component that renders and the exact text the user sees. If you can't
name them, you haven't finished.
For components with lifecycle phases (setup → process → cleanup): trace
ALL phases, especially cleanup/teardown. Does cleanup overwrite state
set during processing? Does a finally block create new errors?
When writing a list of values that covers "all X": verify against the
source enum/table/spec. Don't write from memory.
2.4 — What's the simplest plumbing?
Before modifying shared infrastructure, check if the layer above or below already has the capability.
- Adding a field to a dataclass with 46 consumers is the WRONG default
- Always prefer the smallest blast radius that achieves the goal
Prompt fragment:
Before modifying shared infrastructure: check if the layer above or below
already has the capability. What's the smallest change (fewest files,
fewest callers affected) that achieves the goal?
2.5 — For DB permission/schema changes: query the live database, not migration files
Three mandatory checks before writing any migration that touches permissions, grants, function signatures, or schema objects:
-
Get current state from system catalogs, not migration files. Migrations are append-only history — functions get DROPped and recreated across multiple migrations, columns get ALTERed, grants get overridden by default privileges. One query to
pg_proc,pg_default_acl,information_schema.columns, orpg_classgives the exact current answer. Migration files give you a guess that may be wrong. -
Understand the full grant chain. Supabase (and many Postgres setups) use
ALTER DEFAULT PRIVILEGESto auto-grant permissions on new objects. AREVOKE FROM anonis a no-op ifPUBLICstill has the grant via default privileges. Before writing any REVOKE, check:pg_default_acl— what auto-grants exist?proacl/relacl— what's the full ACL on the object? Look for=X/...(the PUBLIC grant entry).- If PUBLIC has a grant, you must
REVOKE FROM PUBLIC— revoking from named roles alone won't help.
-
Verify the EFFECT after applying, not just the exit code.
REVOKEsucceeds silently even if another grant path still provides access. Always checkhas_function_privilege()/has_table_privilege()immediately after. "0 errors" is not proof that the fix worked.
Why this was added (2026-05-13): SEC-002 fix (revoke anon access to 42 SECURITY DEFINER functions) took 40 minutes instead of 10 because of three compounding failures: (a) Research agents read migration SQL files to determine function signatures — got several wrong (e.g.,
find_library_duplicates(text, uuid, integer)vs actual(text, integer, double precision)). (b) Nobody checked Supabase's default privilege model —REVOKE FROM anonexecuted "successfully" on all 43 statements buthas_function_privilege('anon', ...)still returned true becausePUBLIChad EXECUTE via default privileges. (c) "43 succeeded, 0 failed" was treated as proof the fix worked without verifying the actual effect. Three iterations of debugging were needed to discover the=X/postgres(PUBLIC grant) in the ACL. The correct fix wasREVOKE FROM PUBLIC, anon+ALTER DEFAULT PRIVILEGES ... REVOKE FROM PUBLIC.
Prompt fragment:
For DB permission changes: query the LIVE database before writing the
migration. Get function signatures from pg_proc, not migration files.
Check pg_default_acl for auto-grants. After applying, verify with
has_function_privilege() / has_table_privilege() — exit code 0 is not
proof. If PUBLIC has a grant, REVOKE FROM PUBLIC, not just named roles.
2.6 — Enum/constraint parity: the value the app WRITES must be accepted by the column
For every code path that writes a constrained value to a DB column — an
enum string, a status, a type discriminator, anything backed by a CHECK (col = ANY(ARRAY[...])), a Postgres ENUM type, or a FK — verify the LIVE
constraint accepts the EXACT string the application emits. Read both sides and
diff them character-for-character:
- App side: the enum definition (
class XStatus(str, Enum)) — the literal.valuestrings, not the member names. - DB side: query the live catalog, not migration files:
- CHECK constraint:
SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid='public.<table>'::regclass AND contype='c'; - Enum type:
SELECT enumlabel FROM pg_enum e JOIN pg_type t ON t.oid=e.enumtypid WHERE t.typname='<type>';
- CHECK constraint:
- Diff the two sets. Any app value not in the DB set is a write that will be
rejected (SQLSTATE
23514for CHECK,22P02for enum) — and if the caller swallows the error, it is silent, permanent data loss. Any DB value not in the app set is dead/legacy and a clue the two drifted.
This is the column-content analogue of the existing schema-column check (1.5E): columns existing is necessary but not sufficient — the allowed values must also match. One catalog query catches drift that has otherwise survived for months (the constraint and enum get edited in different PRs; nothing re-checks parity).
Why this was added (2026-06-04): RISK-1 verification surfaced that
citations_verification_status_checkallowed'not_found'while the enumVerificationStatusemits'section_not_found'. Every section-not-found write was rejected (23514) and swallowed byupdate_citation_verification's try/except, so the valuesection_not_foundhad NEVER appeared in the table — citations that resolved to it stayedpendingfor months. Onepg_get_constraintdefquery diffed against the enum would have caught it instantly. The batch task reportederrors=0the whole time because it counted the verification verdict, not the write outcome (see hostile-review Section M).
Prompt fragment:
For any column I write an enum/constrained value to: read the app enum's
.value strings AND query the live CHECK constraint (pg_get_constraintdef) or
pg_enum labels. Diff them. Any app value not accepted by the DB is a silent
write-rejection (23514/22P02) — especially dangerous if the writer swallows
the error. Migration files are not proof; query the live catalog.
Composite Prompt Templates
Template A: Investigating a Problem (before recommending solutions)
I need to understand [PROBLEM] before recommending any solutions.
## PHASE 1.1 — What does the project already know?
Search BUGS.md, ARCH-PATTERNS.md, and memory files for prior analysis.
Search for: [DOMAIN KEYWORDS]. If prior analysis exists, summarize what
it found and what's new/changed.
## PHASE 1.2 — What does the system actually do?
Read the actual runtime/deployment configs: [SPECIFIC FILES]. What
processes run? How do they connect to external services? What is their
actual resource consumption? If live data could answer this, say what
query to run.
THEN: Trace what happens TODAY when the user hits the problem scenario.
Walk the exact code path step by step, including every branch. At each
branch, check what happens when the value is 0, None, or empty. Follow
the path to what the user sees on screen — name the component and the
text. If you can't name them, you haven't finished.
## PHASE 1.3 — What external systems interact here?
What systems outside the codebase interact with this component? How does
each interaction work (pull/push, persistent/transient)? What happens if
one side goes down?
## PHASE 1.4 — What constraints eliminate generic solutions?
Check BUGS.md for REJECTED/DEFERRED/UNSAFE approaches. Check if this
intersects ARCH-001 through ARCH-007. List constraints that would
disqualify standard advice.
## PHASE 1.5 — System-level synthesis (the zoom-out)
How many ENTRY PATHS lead into this subsystem? Map all of them. Do they
all pass through the same validation/classification gate? For key
DECISION POINTS: what happens when the decision is WRONG — can the user
recover? Looking at the full map: does the architecture match the design
intent, or are there structural gaps?
Query the LIVE DATABASE for the subsystem's tables. Check: status
distribution (does it match what the code predicts?), data completeness
(do terminal-status records actually have their expected data?),
stuck/orphaned records. If data contradicts code, the data wins.
For any PARALLEL TABLES serving the same logical purpose: compare schemas
column by column. Every column in table A missing from table B is a gap
where future improvements to A won't be applied to B.
Report: what the system actually does, what's already been analyzed,
what constraints exist, and what questions remain unanswered.
DO NOT propose solutions — just report findings.
Template B: Verifying a Proposed Change (before implementing)
I'm verifying the blast radius of [PROPOSED CHANGE] in [FILE/MODULE].
Phase 1 exploration is complete — this is directed verification.
## PHASE 2.1 — Does this already exist?
Search for whether this behavior already exists. Grep for the BEHAVIOR:
[SPECIFIC GREP SUGGESTIONS]. Answer: YES (stop), NO (proceed), or
PARTIALLY (describe what exists, what's missing, what's broken — this
is the most dangerous answer because the new code must integrate with
what's already there).
## PHASE 2.2 — What types cross function boundaries?
Trace the full call chain from [ENTRY POINT] to [EXIT POINT]. At every
boundary, name the EXACT TYPE in and out. Flag similar-named types.
## PHASE 2.3 — What happens on every branch?
For every Optional-returning or raising function: trace the failure path.
For every boolean flag: trace all setters.
## PHASE 2.4 — What's the simplest plumbing?
Check if the layer above/below already has the capability. What's the
smallest change that achieves the goal?
Report: every file and function with line numbers. Be specific.
Failure Modes This Protocol Prevents
| Failure Mode | Example | Which Phase Catches It |
|---|---|---|
| Rediscovering known problems | Railway analysis ignoring ARCH-002 | Phase 1.1 |
| Not understanding the runtime | "Enable auto-sleep" for a Redis-pull worker | Phase 1.2 |
| Describing happy path, missing failure path | BM25 fallback "works" but total_chunks==0 falls through silently |
Phase 1.2 (trace current failure path) |
| Not tracing to user-visible outcome | "Fallback exists" without naming what user sees on screen | Phase 1.2 (trace to screen) |
| Missing system interactions | Beat dying when worker sleeps | Phase 1.3 |
| Proposing rejected solutions | "Combine API + worker" against ARCH-002 | Phase 1.4 |
| Tracing ONE path when MULTIPLE exist | Library has 4 entry paths; research traced only upload path | Phase 1.5A (enumerate all entry paths) |
| No recovery from wrong decisions | Misclassified Act has no reclassification path — permanent garbage | Phase 1.5B (recovery from wrong decisions) |
| Individual paths look fine but system is incoherent | 4 library entry paths with no shared classification gate | Phase 1.5C (design intent vs architecture) |
| Claiming absence from local files without searching | "Frontend is client-only" → missed middleware.ts + server.ts; ruled L3 infeasible on a false premise |
Phase 1.6 (absence search) |
| Reporting "doesn't exist" when partial exists | Q&A guard "NO" when BM25 fallback + searchNotice already built | Phase 2.1 (PARTIALLY answer) |
| Building something that exists | Skip-1-mention already implemented | Phase 2.1 |
| Missing type mismatches | CostTracker vs LLMCostTracker | Phase 2.2 |
| Cross-stack data loss at API boundary | SSE error code field dropped by new Error() |
Phase 2.2 (cross-stack trace) |
| Missing failure path bugs | was_escalated wrong on screening failure | Phase 2.3 |
| Tracing only proposed code, not existing | Existing _check_embedding_status zero-branch never traced |
Phase 2.3 (apply to existing code) |
| Missing cleanup/teardown logic | SSE stream-end overwrites error set during processing | Phase 2.3 (trace full lifecycle) |
| Incomplete enumeration from memory | _PROCESSING_STATUSES missing ocr_complete — 4 of 6 values |
Phase 2.3 (verify enumerations) |
| Over-engineering the change | Modifying dataclass vs fixing plumbing | Phase 2.4 |
When to Use Which Template
- "Why is X happening?" / "How do I fix X?" / "What are my options?" → Template A (explore first, don't jump to solutions)
- "Implement X" / "Change Y to Z" / "Add feature W" → Template A first (understand the system), then Template B (verify the specific change)
- "This is a one-line config change" → Still do Phase 1.1 (check prior analysis) and Phase 1.4 (check constraints). Skip the rest if genuinely trivial.
Honest Limitation
This is a smart sticky note, not a wall. I have to remember to use it. The zoom-out guard hook fires automatically; this skill does not. The closest to a wall: if the zoom-out guard detects research agents being spawned, it should enforce that Phase 1 was completed before Phase 2 begins.