From 50a0e977520d8db2cac09acad92ae54789a7aad9 Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:58:01 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?SQL=20branching,=20error=20distinguishability,=20try-catch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. timeline.ts: apply branched SQL query pattern (matching mcp.ts fix) 2. mcp.ts get_case_timeline: wrap evidence fetch in try-catch, add warnings/partial fields for degraded responses 3. integrations.ts: searchEntries/getChainOfCustody now return ServiceArrayResult with explicit error field so callers can distinguish "no data" from "service unreachable" Co-Authored-By: Claude Opus 4.6 --- src/lib/integrations.ts | 24 ++++++++++++++++++------ src/routes/mcp.ts | 23 ++++++++++++++--------- src/routes/timeline.ts | 18 ++++++++++-------- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/lib/integrations.ts b/src/lib/integrations.ts index 06fe493..861774f 100644 --- a/src/lib/integrations.ts +++ b/src/lib/integrations.ts @@ -6,6 +6,13 @@ import type { Env } from '../index'; * All calls are fire-and-forget safe — failures are logged but don't break the caller. */ +/** Distinguishes "no data" from "service error" for array-returning methods */ +export interface ServiceArrayResult { + data: T[]; + error?: string; + status?: number; +} + // ── ChittyLedger ───────────────────────────────────────────── // Audit trail: entries, chain verification, custody queries // NOTE: Evidence/case operations live on ChittyEvidence, not ChittyLedger. @@ -54,17 +61,22 @@ export function ledgerClient(env: Env) { addEntry: (entry: LedgerEntryPayload) => post<{ id: string; sequenceNumber: number; hash: string }>('/entries', entry), - /** Search ledger entries */ - searchEntries: async (params: { entityType?: string; entityId?: string; actor?: string; status?: string; limit?: number }): Promise[]> => { + /** Search ledger entries — returns { data, error? } to distinguish empty from failure */ + searchEntries: async (params: { entityType?: string; entityId?: string; actor?: string; status?: string; limit?: number }): Promise>> => { const qs = new URLSearchParams( Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)] as [string, string]) ).toString(); - return await get[]>(`/entries${qs ? `?${qs}` : ''}`) ?? []; + const result = await get[]>(`/entries${qs ? `?${qs}` : ''}`); + if (result === null) return { data: [], error: 'ChittyLedger unreachable or returned error' }; + return { data: result }; }, - /** Get chain of custody for an entity */ - getChainOfCustody: async (entityId: string) => - await get[]>(`/custody/${encodeURIComponent(entityId)}`) ?? [], + /** Get chain of custody for an entity — returns { data, error? } to distinguish empty from failure */ + getChainOfCustody: async (entityId: string): Promise>> => { + const result = await get[]>(`/custody/${encodeURIComponent(entityId)}`); + if (result === null) return { data: [], error: 'ChittyLedger unreachable or returned error' }; + return { data: result }; + }, /** Verify ledger chain integrity */ verifyChain: () => get<{ valid: boolean; errors: string[] }>('/verify'), diff --git a/src/routes/mcp.ts b/src/routes/mcp.ts index 0258e30..31b3bee 100644 --- a/src/routes/mcp.ts +++ b/src/routes/mcp.ts @@ -1225,16 +1225,21 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN // Facts from ChittyEvidence const evidence = evidenceClient(env); if (evidence) { - const facts = startDate && endDate - ? await evidence.getFactsByDateRange(caseId, startDate, endDate) - : await evidence.getEnrichedFacts(caseId); - if (facts) { - for (const f of facts) { - if (!f.fact_date) continue; - events.push({ id: `fact:${f.id}`, date: f.fact_date, type: 'fact', title: f.fact_text.slice(0, 120), factType: f.fact_type, confidence: f.confidence, status: f.verification_status }); + try { + const facts = startDate && endDate + ? await evidence.getFactsByDateRange(caseId, startDate, endDate) + : await evidence.getEnrichedFacts(caseId); + if (facts) { + for (const f of facts) { + if (!f.fact_date) continue; + events.push({ id: `fact:${f.id}`, date: f.fact_date, type: 'fact', title: f.fact_text.slice(0, 120), factType: f.fact_type, confidence: f.confidence, status: f.verification_status }); + } + } else { + warnings.push('Failed to fetch facts from ChittyEvidence'); } - } else { - warnings.push('Failed to fetch facts from ChittyEvidence'); + } catch (err) { + console.error('[mcp/get_case_timeline] evidence facts error:', err); + warnings.push('Evidence facts unavailable: service error'); } } else { warnings.push('ChittyEvidence not configured'); diff --git a/src/routes/timeline.ts b/src/routes/timeline.ts index b4d59c1..c418df2 100644 --- a/src/routes/timeline.ts +++ b/src/routes/timeline.ts @@ -65,14 +65,16 @@ timelineRoutes.get('/cases/:caseId/timeline', async (c) => { // 2. Fetch legal deadlines from ChittyCommand DB const sql = getDb(c.env); try { - const deadlines = await sql` - SELECT id, title, description, deadline_date, deadline_type, status, urgency_score - FROM cc_legal_deadlines - WHERE case_ref = ${caseId} - ${startDate ? sql`AND deadline_date >= ${startDate}` : sql``} - ${endDate ? sql`AND deadline_date <= ${endDate}` : sql``} - ORDER BY deadline_date ASC - `; + let deadlines; + if (startDate && endDate) { + deadlines = await sql`SELECT id, title, description, deadline_date, deadline_type, status, urgency_score FROM cc_legal_deadlines WHERE case_ref = ${caseId} AND deadline_date >= ${startDate} AND deadline_date <= ${endDate} ORDER BY deadline_date ASC`; + } else if (startDate) { + deadlines = await sql`SELECT id, title, description, deadline_date, deadline_type, status, urgency_score FROM cc_legal_deadlines WHERE case_ref = ${caseId} AND deadline_date >= ${startDate} ORDER BY deadline_date ASC`; + } else if (endDate) { + deadlines = await sql`SELECT id, title, description, deadline_date, deadline_type, status, urgency_score FROM cc_legal_deadlines WHERE case_ref = ${caseId} AND deadline_date <= ${endDate} ORDER BY deadline_date ASC`; + } else { + deadlines = await sql`SELECT id, title, description, deadline_date, deadline_type, status, urgency_score FROM cc_legal_deadlines WHERE case_ref = ${caseId} ORDER BY deadline_date ASC`; + } for (const d of deadlines) { events.push({ id: `deadline:${d.id}`, From 72f01b1f8d06a39aced09ebd8b48984c24160dba Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Thu, 26 Mar 2026 01:52:29 +0000 Subject: [PATCH 2/2] fix: log HTTP response body on non-ok status in all integration clients All 16 `!res.ok` branches across ledger, evidence, finance, charge, connect, mercury, books, assets, scrape, and router clients now capture and log the response body (truncated to 500 chars). This surfaces upstream error messages (auth failures, validation errors, rate limits) that were previously silently dropped. Plaid and Notion clients already had this pattern and were not changed. Co-Authored-By: Claude Opus 4.6 --- src/lib/integrations.ts | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/lib/integrations.ts b/src/lib/integrations.ts index 861774f..e4fa537 100644 --- a/src/lib/integrations.ts +++ b/src/lib/integrations.ts @@ -41,7 +41,7 @@ export function ledgerClient(env: Env) { async function get(path: string): Promise { try { const res = await fetch(`${baseUrl}${path}`, { headers }); - if (!res.ok) { console.error(`[ledger] GET ${path} failed: ${res.status}`); return null; } + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[ledger] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[ledger] GET ${path} error:`, err); return null; } } @@ -51,7 +51,7 @@ export function ledgerClient(env: Env) { const res = await fetch(`${baseUrl}${path}`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); - if (!res.ok) { console.error(`[ledger] POST ${path} failed: ${res.status}`); return null; } + if (!res.ok) { const errBody = await res.text().catch(() => ''); console.error(`[ledger] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[ledger] POST ${path} error:`, err); return null; } } @@ -127,7 +127,7 @@ export function evidenceClient(env: Env) { async function get(path: string): Promise { try { const res = await fetch(`${baseUrl}${path}`, { headers }); - if (!res.ok) { console.error(`[evidence] GET ${path} failed: ${res.status}`); return null; } + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[evidence] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[evidence] GET ${path} error:`, err); @@ -142,7 +142,7 @@ export function evidenceClient(env: Env) { headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); - if (!res.ok) { console.error(`[evidence] POST ${path} failed: ${res.status}`); return null; } + if (!res.ok) { const errBody = await res.text().catch(() => ''); console.error(`[evidence] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[evidence] POST ${path} error:`, err); @@ -236,7 +236,7 @@ export function financeClient(env: Env) { const res = await fetch(`${baseUrl}${path}`, { headers: { 'X-Source-Service': 'chittycommand' }, }); - if (!res.ok) return null; + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[finance] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[finance] ${path} error:`, err); @@ -288,7 +288,8 @@ export function chargeClient(env: Env) { body: JSON.stringify(body), }); if (!res.ok) { - console.error(`[charge] ${path} failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[charge] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; @@ -435,7 +436,8 @@ export function connectClient(env: Env) { signal: AbortSignal.timeout(30000), }); if (!res.ok) { - console.error(`[connect] POST ${path} failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[connect] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; @@ -464,7 +466,7 @@ export function connectClient(env: Env) { const res = await fetch(`${baseUrl}/api/discover/${encodeURIComponent(serviceName)}`, { headers: { 'X-Source-Service': 'chittycommand' }, }); - if (!res.ok) { console.error(`[connect/discover] ${serviceName} failed: ${res.status}`); return null; } + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[connect/discover] ${serviceName} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } const data = await res.json() as { url: string }; // Store in KV with TTL try { await env.COMMAND_KV.put(key, JSON.stringify({ url: data.url }), { expirationTtl: 300 }); } catch (err) { console.warn('[connect/discover] KV write failed:', err); } @@ -555,7 +557,8 @@ export function mercuryClient(token: string) { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, }); if (!res.ok) { - console.error(`[mercury] ${path} failed: ${res.status}`); + const body = await res.text().catch(() => ''); + console.error(`[mercury] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; @@ -590,7 +593,7 @@ export function booksClient(env: Env) { const res = await fetch(`${baseUrl}/api/summary`, { headers: { 'X-Source-Service': 'chittycommand' }, }); - if (!res.ok) return null; + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[books] GET /api/summary failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as Record; } catch (err) { console.error('[books] summary error:', err); @@ -606,7 +609,8 @@ export function booksClient(env: Env) { body: JSON.stringify(payload), }); if (!res.ok) { - console.error(`[books] record-transaction failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[books] POST /api/transaction failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as Record; @@ -630,7 +634,7 @@ export function assetsClient(env: Env) { const res = await fetch(`${baseUrl}${path}`, { headers: { 'X-Source-Service': 'chittycommand' }, }); - if (!res.ok) return null; + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[assets] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[assets] ${path} error:`, err); @@ -646,7 +650,8 @@ export function assetsClient(env: Env) { body: JSON.stringify(body), }); if (!res.ok) { - console.error(`[assets] ${path} failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[assets] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; @@ -684,7 +689,8 @@ export function scrapeClient(env: Env) { signal: AbortSignal.timeout(30000), }); if (!res.ok) { - console.error(`[scrape] ${path} failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[scrape] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; @@ -746,7 +752,8 @@ export function routerClient(env: Env) { signal: AbortSignal.timeout(60000), }); if (!res.ok) { - console.error(`[router] ${path} failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[router] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; @@ -764,7 +771,8 @@ export function routerClient(env: Env) { signal: AbortSignal.timeout(10000), }); if (!res.ok) { - console.error(`[router] GET ${path} failed: ${res.status}`); + const body = await res.text().catch(() => ''); + console.error(`[router] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T;