From fccaf402ae7da88d7bfb39fabf9ea2de48d3d676 Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:21:00 +0000 Subject: [PATCH 1/4] fix: redirect phantom ledger API calls to correct services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ledgerClient was calling /api/evidence, /api/cases/*, and /api/evidence/*/custody — none of which exist on ChittyLedger. These endpoints belong to ChittyEvidence. - Rewrite ledgerClient to only expose real ChittyLedger endpoints: POST /entries, GET /entries, GET /custody/:id, GET /verify, GET /statistics - Add submitDocument and addCustodyEntry to evidenceClient - Update all callers (bridge, mcp, documents, timeline, dispute-sync) to use evidenceClient for evidence/case operations - Remove duplicate evidence fetch in timeline route - All operations include Bearer auth via CHITTYLEDGER_TOKEN Co-Authored-By: Claude Opus 4.6 --- src/lib/dispute-sync.ts | 23 ++++--- src/lib/integrations.ts | 130 ++++++++++++++++++++---------------- src/routes/bridge/ledger.ts | 61 +++++++++++------ src/routes/documents.ts | 10 +-- src/routes/ledger.ts | 17 +++-- src/routes/mcp.ts | 76 ++++++++------------- src/routes/timeline.ts | 28 +------- 7 files changed, 170 insertions(+), 175 deletions(-) diff --git a/src/lib/dispute-sync.ts b/src/lib/dispute-sync.ts index b00dab1..3c235fd 100644 --- a/src/lib/dispute-sync.ts +++ b/src/lib/dispute-sync.ts @@ -16,6 +16,7 @@ import type { NeonQueryFunction } from '@neondatabase/serverless'; import type { Env } from '../index'; import { notionClient, routerClient, ledgerClient } from './integrations'; + // ── Types ───────────────────────────────────────────────────── interface DisputeCore { @@ -302,21 +303,27 @@ async function linkDisputeToLedger( const ledger = ledgerClient(env); if (!ledger) return; - const caseResult = await ledger.createCase({ - caseNumber: `CC-DISPUTE-${disputeId.slice(0, 8)}`, - title: dispute.title, - caseType: 'CIVIL', - description: dispute.description || undefined, + const entryResult = await ledger.addEntry({ + entityType: 'audit', + entityId: `CC-DISPUTE-${disputeId.slice(0, 8)}`, + action: 'dispute:created', + actor: 'chittycommand', + actorType: 'service', + metadata: { + title: dispute.title, + description: dispute.description, + caseType: 'CIVIL', + }, }); - if (caseResult?.id) { + if (entryResult?.id) { await sql` UPDATE cc_disputes - SET metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_case_id: caseResult.id })}::jsonb, + SET metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_case_id: entryResult.id })}::jsonb, updated_at = NOW() WHERE id = ${disputeId} `; - console.log(`[dispute-sync:ledger] Linked dispute ${disputeId} → case ${caseResult.id}`); + console.log(`[dispute-sync:ledger] Linked dispute ${disputeId} → ledger entry ${entryResult.id}`); } } catch (err) { console.error(`[dispute-sync:ledger] Failed for dispute ${disputeId}:`, err); diff --git a/src/lib/integrations.ts b/src/lib/integrations.ts index b2c7ed3..38aa1e2 100644 --- a/src/lib/integrations.ts +++ b/src/lib/integrations.ts @@ -7,23 +7,17 @@ import type { Env } from '../index'; */ // ── ChittyLedger ───────────────────────────────────────────── -// Evidence pipeline: documents → evidence, disputes → cases, actions → custody log +// Audit trail: entries, chain verification, custody queries +// NOTE: Evidence/case operations live on ChittyEvidence, not ChittyLedger. -export interface LedgerEvidencePayload { - filename: string; - fileType: string; - fileSize?: string; - description?: string; - evidenceTier: string; - caseId?: string; -} - -export interface LedgerCustodyEntry { - evidenceId: string; +export interface LedgerEntryPayload { + entityType: 'transaction' | 'evidence' | 'custody' | 'audit'; + entityId?: string; action: string; - performedBy: string; - location?: string; - notes?: string; + actor?: string; + actorType?: 'user' | 'service' | 'system'; + metadata?: Record; + status?: 'pending' | 'confirmed' | 'rejected'; } export function ledgerClient(env: Env) { @@ -38,63 +32,55 @@ export function ledgerClient(env: Env) { headers['Authorization'] = `Bearer ${env.CHITTYLEDGER_TOKEN}`; } - async function post(path: string, body: unknown): Promise { - try { - const res = await fetch(`${baseUrl}${path}`, { - method: 'POST', - headers, - body: JSON.stringify(body), - }); - if (!res.ok) { - console.error(`[ledger] ${path} failed: ${res.status}`); - return null; - } - return await res.json() as T; - } catch (err) { - console.error(`[ledger] ${path} error:`, err); - return null; - } - } - return { - /** Push a document into ChittyLedger as evidence */ - createEvidence: (payload: LedgerEvidencePayload) => - post<{ id: string }>('/api/evidence', payload), - - /** Record a chain-of-custody event */ - addCustodyEntry: (entry: LedgerCustodyEntry) => - post('/api/evidence/' + entry.evidenceId + '/custody', entry), - - /** Create or link a case in ChittyLedger */ - createCase: (payload: { caseNumber: string; title: string; caseType: string; description?: string }) => - post<{ id: string }>('/api/cases', payload), + /** Add an audit/custody/evidence entry to the ledger */ + addEntry: async (entry: LedgerEntryPayload): Promise<{ id: string; sequenceNumber: number; hash: string } | null> => { + try { + const res = await fetch(`${baseUrl}/entries`, { + method: 'POST', headers, body: JSON.stringify(entry), + }); + if (!res.ok) { console.error(`[ledger] POST /entries failed: ${res.status}`); return null; } + return await res.json() as { id: string; sequenceNumber: number; hash: string }; + } catch (err) { console.error('[ledger] POST /entries error:', err); return null; } + }, - /** Get evidence by case */ - getEvidenceByCase: async (caseId: string): Promise[]> => { + /** Search ledger entries */ + searchEntries: async (params: { entityType?: string; entityId?: string; actor?: string; status?: string; limit?: number }): Promise[]> => { try { - const qs = new URLSearchParams({ caseId }).toString(); - const res = await fetch(`${baseUrl}/api/evidence?${qs}`, { headers }); + const qs = new URLSearchParams( + Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)] as [string, string]) + ).toString(); + const res = await fetch(`${baseUrl}/entries${qs ? `?${qs}` : ''}`, { headers }); if (!res.ok) return []; return await res.json() as Record[]; } catch { return []; } }, - /** Get facts for a case (if supported) */ - getFactsForCase: async (caseId: string): Promise[]> => { + /** Get chain of custody for an entity */ + getChainOfCustody: async (entityId: string): Promise[]> => { try { - const res = await fetch(`${baseUrl}/api/cases/${encodeURIComponent(caseId)}/facts`, { headers }); + const res = await fetch(`${baseUrl}/custody/${encodeURIComponent(entityId)}`, { headers }); if (!res.ok) return []; return await res.json() as Record[]; } catch { return []; } }, - /** Get contradictions for a case (if supported) */ - getContradictionsForCase: async (caseId: string): Promise[]> => { + /** Verify ledger chain integrity */ + verifyChain: async (): Promise<{ valid: boolean; errors: string[] } | null> => { try { - const res = await fetch(`${baseUrl}/api/cases/${encodeURIComponent(caseId)}/contradictions`, { headers }); - if (!res.ok) return []; - return await res.json() as Record[]; - } catch { return []; } + const res = await fetch(`${baseUrl}/verify`, { headers }); + if (!res.ok) return null; + return await res.json() as { valid: boolean; errors: string[] }; + } catch { return null; } + }, + + /** Get ledger statistics */ + getStatistics: async (): Promise | null> => { + try { + const res = await fetch(`${baseUrl}/statistics`, { headers }); + if (!res.ok) return null; + return await res.json() as Record; + } catch { return null; } }, }; } @@ -132,7 +118,7 @@ export function evidenceClient(env: Env) { const baseUrl = env.CHITTYEVIDENCE_URL; if (!baseUrl) return null; - const headers = { 'X-Source-Service': 'chittycommand' }; + const headers: Record = { 'X-Source-Service': 'chittycommand' }; async function get(path: string): Promise { try { @@ -145,7 +131,35 @@ export function evidenceClient(env: Env) { } } + async function post(path: string, body: unknown): Promise { + try { + const res = await fetch(`${baseUrl}${path}`, { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!res.ok) { console.error(`[evidence] POST ${path} failed: ${res.status}`); return null; } + return await res.json() as T; + } catch (err) { + console.error(`[evidence] POST ${path} error:`, err); + return null; + } + } + return { + /** Submit a document to the evidence pipeline */ + submitDocument: (payload: { filename: string; fileType: string; fileSize?: string; description?: string; evidenceTier?: string; caseId?: string }) => + post<{ id: string; submission_id?: string }>('/collect', { + file_name: payload.filename, + document_type: payload.fileType, + description: payload.description, + case_id: payload.caseId, + }), + + /** Record a chain-of-custody event on a document */ + addCustodyEntry: (documentId: string, entry: { action: string; performedBy: string; location?: string; notes?: string }) => + post>(`/legal/documents/${encodeURIComponent(documentId)}/custody`, entry), + /** Get enriched facts for a case (facts + entities + amounts) */ getEnrichedFacts: (caseId: string) => get(`/facts/cases/${encodeURIComponent(caseId)}/enriched`), diff --git a/src/routes/bridge/ledger.ts b/src/routes/bridge/ledger.ts index 44c3c13..ad16fda 100644 --- a/src/routes/bridge/ledger.ts +++ b/src/routes/bridge/ledger.ts @@ -2,17 +2,17 @@ import { Hono } from 'hono'; import type { Env } from '../../index'; import type { AuthVariables } from '../../middleware/auth'; import { getDb } from '../../lib/db'; -import { ledgerClient } from '../../lib/integrations'; +import { evidenceClient, ledgerClient } from '../../lib/integrations'; import { recordActionSchema } from '../../lib/validators'; export const ledgerBridgeRoutes = new Hono<{ Bindings: Env; Variables: AuthVariables }>(); -// ── ChittyLedger Sync ──────────────────────────────────────── +// ── Evidence Sync (via ChittyEvidence) ──────────────────────── -/** Push all unsynced documents to ChittyLedger as evidence */ +/** Push all unsynced documents to ChittyEvidence pipeline */ ledgerBridgeRoutes.post('/sync-documents', async (c) => { - const ledger = ledgerClient(c.env); - if (!ledger) return c.json({ error: 'ChittyLedger not configured' }, 503); + const evidence = evidenceClient(c.env); + if (!evidence) return c.json({ error: 'ChittyEvidence not configured' }, 503); const sql = getDb(c.env); const unsynced = await sql` @@ -25,17 +25,17 @@ ledgerBridgeRoutes.post('/sync-documents', async (c) => { let synced = 0; for (const doc of unsynced) { - const evidence = await ledger.createEvidence({ + const result = await evidence.submitDocument({ filename: doc.filename || 'unknown', fileType: doc.doc_type || 'upload', description: `Uploaded via ChittyCommand: ${doc.filename}`, evidenceTier: 'BUSINESS_RECORDS', }); - if (evidence?.id) { + if (result?.id) { await sql` UPDATE cc_documents SET - metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_evidence_id: evidence.id })}::jsonb, + metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_evidence_id: result.id })}::jsonb, processing_status = 'synced' WHERE id = ${doc.id} `; @@ -43,10 +43,22 @@ ledgerBridgeRoutes.post('/sync-documents', async (c) => { } } - return c.json({ total: unsynced.length, synced, message: `Synced ${synced} documents to ChittyLedger` }); + // Also log the sync event to ChittyLedger audit trail + const ledger = ledgerClient(c.env); + if (ledger && synced > 0) { + ledger.addEntry({ + entityType: 'audit', + action: 'evidence:sync-documents', + actor: 'chittycommand', + actorType: 'service', + metadata: { total: unsynced.length, synced }, + }).catch(() => {}); + } + + return c.json({ total: unsynced.length, synced, message: `Synced ${synced} documents to ChittyEvidence` }); }); -/** Push disputes to ChittyLedger as cases */ +/** Push disputes to ChittyLedger as audit entries */ ledgerBridgeRoutes.post('/sync-disputes', async (c) => { const ledger = ledgerClient(c.env); if (!ledger) return c.json({ error: 'ChittyLedger not configured' }, 503); @@ -60,17 +72,23 @@ ledgerBridgeRoutes.post('/sync-disputes', async (c) => { let synced = 0; for (const dispute of unsynced) { - const caseResult = await ledger.createCase({ - caseNumber: `CC-DISPUTE-${(dispute.id as string).slice(0, 8)}`, - title: dispute.title as string, - caseType: 'CIVIL', - description: dispute.description as string || undefined, + const entryResult = await ledger.addEntry({ + entityType: 'audit', + entityId: `CC-DISPUTE-${(dispute.id as string).slice(0, 8)}`, + action: 'dispute:created', + actor: 'chittycommand', + actorType: 'service', + metadata: { + title: dispute.title, + description: dispute.description, + caseType: 'CIVIL', + }, }); - if (caseResult?.id) { + if (entryResult?.id) { await sql` UPDATE cc_disputes SET - metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_case_id: caseResult.id })}::jsonb + metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_case_id: entryResult.id })}::jsonb WHERE id = ${dispute.id} `; synced++; @@ -80,17 +98,16 @@ ledgerBridgeRoutes.post('/sync-disputes', async (c) => { return c.json({ total: unsynced.length, synced, message: `Synced ${synced} disputes to ChittyLedger` }); }); -/** Record an action in ChittyLedger chain of custody */ +/** Record an action in ChittyEvidence chain of custody */ ledgerBridgeRoutes.post('/record-action', async (c) => { - const ledger = ledgerClient(c.env); - if (!ledger) return c.json({ error: 'ChittyLedger not configured' }, 503); + const evidence = evidenceClient(c.env); + if (!evidence) return c.json({ error: 'ChittyEvidence not configured' }, 503); const parsed = recordActionSchema.safeParse(await c.req.json()); if (!parsed.success) return c.json({ error: 'Invalid request', details: parsed.error.issues }, 400); const body = parsed.data; - const result = await ledger.addCustodyEntry({ - evidenceId: body.evidence_id, + const result = await evidence.addCustodyEntry(body.evidence_id, { action: body.action, performedBy: 'chittycommand', location: 'ChittyCommand Dashboard', diff --git a/src/routes/documents.ts b/src/routes/documents.ts index 61578db..aa84eb5 100644 --- a/src/routes/documents.ts +++ b/src/routes/documents.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono'; import type { Env } from '../index'; import { getDb } from '../lib/db'; -import { ledgerClient } from '../lib/integrations'; +import { evidenceClient } from '../lib/integrations'; export const documentRoutes = new Hono<{ Bindings: Env }>(); const UUID_V4ISH = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; @@ -56,10 +56,10 @@ documentRoutes.post('/upload', async (c) => { RETURNING * `; - // Fire-and-forget: push to ChittyLedger evidence pipeline - const ledger = ledgerClient(c.env); - if (ledger) { - ledger.createEvidence({ + // Fire-and-forget: push to ChittyEvidence pipeline + const evidence = evidenceClient(c.env); + if (evidence) { + evidence.submitDocument({ filename: safeName, fileType: file.type, fileSize: String(file.size), diff --git a/src/routes/ledger.ts b/src/routes/ledger.ts index 80e6fe9..93c0038 100644 --- a/src/routes/ledger.ts +++ b/src/routes/ledger.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono'; import type { Env } from '../index'; -import { ledgerClient } from '../lib/integrations'; +import { evidenceClient } from '../lib/integrations'; export const ledgerRoutes = new Hono<{ Bindings: Env }>(); @@ -8,10 +8,10 @@ export const ledgerRoutes = new Hono<{ Bindings: Env }>(); ledgerRoutes.get('/ledger/evidence', async (c) => { const caseId = c.req.query('case_id'); if (!caseId) return c.json({ error: 'Missing query param: case_id' }, 400); - const ledger = ledgerClient(c.env); - if (!ledger) return c.json({ error: 'ChittyLedger not configured' }, 503); - const items = await ledger.getEvidenceByCase(caseId); - return c.json({ case_id: caseId, evidence: items }); + const evidence = evidenceClient(c.env); + if (!evidence) return c.json({ error: 'ChittyEvidence not configured' }, 503); + const facts = await evidence.getEnrichedFacts(caseId); + return c.json({ case_id: caseId, evidence: facts || [] }); }); // POST /api/v1/ledger/record-custody { evidence_id, action, notes? } @@ -23,9 +23,8 @@ ledgerRoutes.post('/ledger/record-custody', async (c) => { if (!evidenceId || !action) return c.json({ error: 'Missing fields: evidence_id, action' }, 400); // @ts-expect-error app-level vars const userId = (c.get('userId') as string | undefined) || 'api-client'; - const ledger = ledgerClient(c.env); - if (!ledger) return c.json({ error: 'ChittyLedger not configured' }, 503); - const result = await ledger.addCustodyEntry({ evidenceId, action, performedBy: userId, notes }); + const evidence = evidenceClient(c.env); + if (!evidence) return c.json({ error: 'ChittyEvidence not configured' }, 503); + const result = await evidence.addCustodyEntry(evidenceId, { action, performedBy: userId, notes }); return c.json({ ok: !!result, result }); }); - diff --git a/src/routes/mcp.ts b/src/routes/mcp.ts index 7cc7c86..673e650 100644 --- a/src/routes/mcp.ts +++ b/src/routes/mcp.ts @@ -600,16 +600,10 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN case 'ledger_get_evidence': { const caseId = String(args.case_id || '').trim(); if (!caseId) throw new Error('Missing argument: case_id'); - if (!env.CHITTYLEDGER_URL) return { error: 'ChittyLedger not configured' }; - try { - const qs = new URLSearchParams({ caseId }).toString(); - const res = await fetch(`${env.CHITTYLEDGER_URL}/api/evidence?${qs}`, { headers: { 'X-Source-Service': 'chittycommand' } }); - if (!res.ok) return { error: 'Failed to fetch evidence', code: res.status }; - const items = await res.json(); - return { case_id: caseId, evidence: items }; - } catch (err) { - return { error: String(err) }; - } + const ev = evidenceClient(env); + if (!ev) return { error: 'ChittyEvidence not configured' }; + const facts = await ev.getEnrichedFacts(caseId); + return { case_id: caseId, evidence: facts || [] }; } case 'ledger_record_custody': { @@ -617,63 +611,51 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN const action = String(args.action || '').trim(); const notes = args.notes ? String(args.notes) : undefined; if (!evidenceId || !action) throw new Error('Missing arguments: evidence_id, action'); - if (!env.CHITTYLEDGER_URL) return { error: 'ChittyLedger not configured' }; - try { - const payload = { evidenceId, action, performedBy: 'mcp-client', ...(notes ? { notes } : {}) }; - const res = await fetch(`${env.CHITTYLEDGER_URL}/api/evidence/${encodeURIComponent(evidenceId)}/custody`, { - method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Source-Service': 'chittycommand' }, body: JSON.stringify(payload) - }); - if (!res.ok) return { error: 'Failed to record custody', code: res.status }; - const data = await res.json(); - return { ok: true, result: data }; - } catch (err) { - return { error: String(err) }; - } + const ev = evidenceClient(env); + if (!ev) return { error: 'ChittyEvidence not configured' }; + const result = await ev.addCustodyEntry(evidenceId, { action, performedBy: 'mcp-client', notes }); + return { ok: !!result, result }; } case 'ledger_facts': { const caseId = String(args.case_id || '').trim(); if (!caseId) throw new Error('Missing argument: case_id'); - if (!env.CHITTYLEDGER_URL) return { facts: [] }; - try { - const res = await fetch(`${env.CHITTYLEDGER_URL}/api/cases/${encodeURIComponent(caseId)}/facts`, { headers: { 'X-Source-Service': 'chittycommand' } }); - if (!res.ok) return { facts: [] }; - return { case_id: caseId, facts: await res.json() }; - } catch { return { facts: [] }; } + const ev = evidenceClient(env); + if (!ev) return { facts: [] }; + const facts = await ev.getStatementOfFacts(caseId); + return { case_id: caseId, facts: facts || [] }; } case 'ledger_contradictions': { const caseId = String(args.case_id || '').trim(); if (!caseId) throw new Error('Missing argument: case_id'); - if (!env.CHITTYLEDGER_URL) return { contradictions: [] }; - try { - const res = await fetch(`${env.CHITTYLEDGER_URL}/api/cases/${encodeURIComponent(caseId)}/contradictions`, { headers: { 'X-Source-Service': 'chittycommand' } }); - if (!res.ok) return { contradictions: [] }; - return { case_id: caseId, contradictions: await res.json() }; - } catch { return { contradictions: [] }; } + const ev = evidenceClient(env); + if (!ev) return { contradictions: [] }; + const contradictions = await ev.getContradictions(caseId); + return { case_id: caseId, contradictions: contradictions || [] }; } case 'ledger_create_case_for_dispute': { const disputeId = String(args.dispute_id || '').trim(); if (!disputeId) throw new Error('Missing argument: dispute_id'); - if (!env.CHITTYLEDGER_URL) return { error: 'ChittyLedger not configured' }; + const ledger = ledgerClient(env); + if (!ledger) return { error: 'ChittyLedger not configured' }; const rows = await sql`SELECT id, title, dispute_type, description, metadata FROM cc_disputes WHERE id = ${disputeId}`; if (rows.length === 0) throw new Error('Dispute not found'); const d = rows[0] as any; const metadata = (d.metadata as any) || {}; if (metadata.ledger_case_id) return { dispute_id: disputeId, case_id: metadata.ledger_case_id, linked: true }; - try { - const payload = { caseNumber: `CC-DISPUTE-${String(d.id).slice(0,8)}`, title: String(d.title), caseType: 'CIVIL', description: d.description || undefined }; - const res = await fetch(`${env.CHITTYLEDGER_URL}/api/cases`, { - method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Source-Service': 'chittycommand' }, body: JSON.stringify(payload) - }); - if (!res.ok) return { error: 'Failed to create case', code: res.status }; - const data = await res.json() as { id: string }; - await sql`UPDATE cc_disputes SET metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_case_id: data.id })}::jsonb WHERE id = ${disputeId}`; - return { dispute_id: disputeId, case_id: data.id, linked: true }; - } catch (err) { - return { error: String(err) }; - } + const entryResult = await ledger.addEntry({ + entityType: 'audit', + entityId: `CC-DISPUTE-${String(d.id).slice(0, 8)}`, + action: 'dispute:created', + actor: 'chittycommand', + actorType: 'service', + metadata: { title: String(d.title), caseType: 'CIVIL', description: d.description || undefined }, + }); + if (!entryResult) return { error: 'Failed to create ledger entry' }; + await sql`UPDATE cc_disputes SET metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_case_id: entryResult.id })}::jsonb WHERE id = ${disputeId}`; + return { dispute_id: disputeId, case_id: entryResult.id, linked: true }; } case 'ledger_link_case_for_dispute': { diff --git a/src/routes/timeline.ts b/src/routes/timeline.ts index 22843ff..321cdcc 100644 --- a/src/routes/timeline.ts +++ b/src/routes/timeline.ts @@ -2,7 +2,7 @@ import { Hono } from 'hono'; import type { Env } from '../index'; import type { AuthVariables } from '../middleware/auth'; import { getDb } from '../lib/db'; -import { evidenceClient, ledgerClient } from '../lib/integrations'; +import { evidenceClient } from '../lib/integrations'; export const timelineRoutes = new Hono<{ Bindings: Env; Variables: AuthVariables }>(); @@ -116,31 +116,7 @@ timelineRoutes.get('/cases/:caseId/timeline', async (c) => { console.error('[timeline] disputes error:', err); } - // 4. Fetch evidence documents from ChittyLedger - const ledger = ledgerClient(c.env); - if (ledger) { - try { - const docs = await ledger.getEvidenceByCase(caseId); - for (const doc of docs) { - const uploadDate = (doc.created_at || doc.uploaded_at || '') as string; - if (!uploadDate) continue; - events.push({ - id: `doc:${doc.id}`, - date: uploadDate, - type: 'document', - title: `Document: ${doc.filename || doc.title || 'Untitled'}`, - description: (doc.description as string) || undefined, - source: 'chittyledger', - metadata: { - fileType: doc.file_type, - evidenceTier: doc.evidence_tier, - }, - }); - } - } catch (err) { - console.error('[timeline] ledger docs error:', err); - } - } + // 4. Evidence documents already fetched in section 1 via evidenceClient // Sort by date ascending events.sort((a, b) => a.date.localeCompare(b.date)); From db09ebd9772d08a8a0ca457f802daa396d476dd6 Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:24:26 +0000 Subject: [PATCH 2/4] fix: remove remaining phantom ledger call in get_case_timeline MCP tool The get_case_timeline handler still called ledger.getEvidenceByCase() which was removed from the rewritten ledgerClient. Evidence documents are already fetched via evidenceClient in the facts section above. Co-Authored-By: Claude Opus 4.6 --- src/routes/mcp.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/routes/mcp.ts b/src/routes/mcp.ts index 673e650..e6bd1a6 100644 --- a/src/routes/mcp.ts +++ b/src/routes/mcp.ts @@ -1238,20 +1238,7 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN events.push({ id: `dispute:${d.id}`, date: d.created_at, type: 'dispute', title: d.title, status: d.status, domain: d.domain }); } - // Documents from ChittyLedger - const ledger = ledgerClient(env); - if (ledger) { - try { - const docs = await ledger.getEvidenceByCase(caseId); - for (const doc of docs) { - const uploadDate = (doc.created_at || doc.uploaded_at || '') as string; - if (!uploadDate) continue; - events.push({ id: `doc:${doc.id}`, date: uploadDate, type: 'document', title: `Document: ${doc.filename || doc.title || 'Untitled'}`, source: 'chittyledger' }); - } - } catch (err) { - console.error('[mcp/timeline] ledger docs error:', err); - } - } + // Documents already covered by ChittyEvidence facts above events.sort((a, b) => String(a.date).localeCompare(String(b.date))); return { caseId, eventCount: events.length, events }; From 691dc0434b08f6dc6c469bc228489be3504e1a56 Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Wed, 25 Mar 2026 05:03:42 +0000 Subject: [PATCH 3/4] fix: use deterministic case ref for ledger_case_id, not entry UUID ledger_case_id is used for timeline/deadline joins and must be a stable case reference (CC-DISPUTE-{prefix}), not a ledger audit entry UUID. Store the entry UUID separately as ledger_entry_id. Fixes CodeRabbit review findings on both mcp.ts and dispute-sync.ts. Co-Authored-By: Claude Opus 4.6 --- src/lib/dispute-sync.ts | 7 ++++--- src/routes/ledger.ts | 5 +++-- src/routes/mcp.ts | 7 ++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/lib/dispute-sync.ts b/src/lib/dispute-sync.ts index 3c235fd..c654ee9 100644 --- a/src/lib/dispute-sync.ts +++ b/src/lib/dispute-sync.ts @@ -303,9 +303,10 @@ async function linkDisputeToLedger( const ledger = ledgerClient(env); if (!ledger) return; + const caseRef = `CC-DISPUTE-${disputeId.slice(0, 8)}`; const entryResult = await ledger.addEntry({ entityType: 'audit', - entityId: `CC-DISPUTE-${disputeId.slice(0, 8)}`, + entityId: caseRef, action: 'dispute:created', actor: 'chittycommand', actorType: 'service', @@ -319,11 +320,11 @@ async function linkDisputeToLedger( if (entryResult?.id) { await sql` UPDATE cc_disputes - SET metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_case_id: entryResult.id })}::jsonb, + SET metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_case_id: caseRef, ledger_entry_id: entryResult.id })}::jsonb, updated_at = NOW() WHERE id = ${disputeId} `; - console.log(`[dispute-sync:ledger] Linked dispute ${disputeId} → ledger entry ${entryResult.id}`); + console.log(`[dispute-sync:ledger] Linked dispute ${disputeId} → case ${caseRef} (entry ${entryResult.id})`); } } catch (err) { console.error(`[dispute-sync:ledger] Failed for dispute ${disputeId}:`, err); diff --git a/src/routes/ledger.ts b/src/routes/ledger.ts index 93c0038..9c2fbf4 100644 --- a/src/routes/ledger.ts +++ b/src/routes/ledger.ts @@ -10,8 +10,9 @@ ledgerRoutes.get('/ledger/evidence', async (c) => { if (!caseId) return c.json({ error: 'Missing query param: case_id' }, 400); const evidence = evidenceClient(c.env); if (!evidence) return c.json({ error: 'ChittyEvidence not configured' }, 503); - const facts = await evidence.getEnrichedFacts(caseId); - return c.json({ case_id: caseId, evidence: facts || [] }); + // Search for documents associated with this case + const docs = await evidence.searchDocuments(caseId); + return c.json({ case_id: caseId, evidence: docs || [] }); }); // POST /api/v1/ledger/record-custody { evidence_id, action, notes? } diff --git a/src/routes/mcp.ts b/src/routes/mcp.ts index e6bd1a6..6272cb2 100644 --- a/src/routes/mcp.ts +++ b/src/routes/mcp.ts @@ -645,17 +645,18 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN const d = rows[0] as any; const metadata = (d.metadata as any) || {}; if (metadata.ledger_case_id) return { dispute_id: disputeId, case_id: metadata.ledger_case_id, linked: true }; + const caseRef = `CC-DISPUTE-${String(d.id).slice(0, 8)}`; const entryResult = await ledger.addEntry({ entityType: 'audit', - entityId: `CC-DISPUTE-${String(d.id).slice(0, 8)}`, + entityId: caseRef, action: 'dispute:created', actor: 'chittycommand', actorType: 'service', metadata: { title: String(d.title), caseType: 'CIVIL', description: d.description || undefined }, }); if (!entryResult) return { error: 'Failed to create ledger entry' }; - await sql`UPDATE cc_disputes SET metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_case_id: entryResult.id })}::jsonb WHERE id = ${disputeId}`; - return { dispute_id: disputeId, case_id: entryResult.id, linked: true }; + await sql`UPDATE cc_disputes SET metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ ledger_case_id: caseRef, ledger_entry_id: entryResult.id })}::jsonb WHERE id = ${disputeId}`; + return { dispute_id: disputeId, case_id: caseRef, ledger_entry_id: entryResult.id, linked: true }; } case 'ledger_link_case_for_dispute': { From 86ceb47c6e9cf81b3d2b19f855204593b9faea21 Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Wed, 25 Mar 2026 05:17:42 +0000 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20SQL=20errors=20in=20timeline/jobs=20?= =?UTF-8?q?=E2=80=94=20conditional=20query,=20missing=20column,=20null=20g?= =?UTF-8?q?uard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Neon SQL template literal: conditional WHERE clauses don't work inline, use separate query branches for date filtering in get_case_timeline - Fix cc_disputes query: 'domain' column doesn't exist, use 'dispute_type' - Fix listJobs null guard: check Array.isArray(result.jobs) before .map() Co-Authored-By: Claude Opus 4.6 --- src/lib/job-dispatcher.ts | 4 ++-- src/routes/mcp.ts | 15 ++++++++++++--- src/routes/timeline.ts | 6 +++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/lib/job-dispatcher.ts b/src/lib/job-dispatcher.ts index 8efbc18..375c5a5 100644 --- a/src/lib/job-dispatcher.ts +++ b/src/lib/job-dispatcher.ts @@ -147,10 +147,10 @@ export async function listJobs( jobType: filters.jobType, limit: filters.limit, }); - if (result) { + if (result && Array.isArray(result.jobs)) { return { jobs: result.jobs.map(mapRouterJob), - total: result.total, + total: result.total ?? result.jobs.length, }; } } diff --git a/src/routes/mcp.ts b/src/routes/mcp.ts index 6272cb2..3b3f9cb 100644 --- a/src/routes/mcp.ts +++ b/src/routes/mcp.ts @@ -1228,15 +1228,24 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN } // Deadlines from DB (with date filtering) - const deadlines = await sql`SELECT id, title, deadline_date, deadline_type, status 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, deadline_date, deadline_type, status 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, deadline_date, deadline_type, status 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, deadline_date, deadline_type, status FROM cc_legal_deadlines WHERE case_ref = ${caseId} AND deadline_date <= ${endDate} ORDER BY deadline_date ASC`; + } else { + deadlines = await sql`SELECT id, title, deadline_date, deadline_type, status FROM cc_legal_deadlines WHERE case_ref = ${caseId} ORDER BY deadline_date ASC`; + } for (const d of deadlines) { events.push({ id: `deadline:${d.id}`, date: d.deadline_date, type: 'deadline', title: d.title, deadlineType: d.deadline_type, status: d.status }); } // Disputes from DB - const disputes = await sql`SELECT id, title, status, domain, created_at FROM cc_disputes WHERE metadata->>'case_ref' = ${caseId} OR metadata->>'ledger_case_id' = ${caseId}`; + const disputes = await sql`SELECT id, title, status, dispute_type, created_at FROM cc_disputes WHERE metadata->>'case_ref' = ${caseId} OR metadata->>'ledger_case_id' = ${caseId}`; for (const d of disputes) { - events.push({ id: `dispute:${d.id}`, date: d.created_at, type: 'dispute', title: d.title, status: d.status, domain: d.domain }); + events.push({ id: `dispute:${d.id}`, date: d.created_at, type: 'dispute', title: d.title, status: d.status, disputeType: d.dispute_type }); } // Documents already covered by ChittyEvidence facts above diff --git a/src/routes/timeline.ts b/src/routes/timeline.ts index 321cdcc..de206c7 100644 --- a/src/routes/timeline.ts +++ b/src/routes/timeline.ts @@ -91,7 +91,7 @@ timelineRoutes.get('/cases/:caseId/timeline', async (c) => { // 3. Fetch dispute milestones try { const disputes = await sql` - SELECT id, title, status, priority, domain, created_at, updated_at, metadata + SELECT id, title, status, priority, dispute_type, created_at, updated_at, metadata FROM cc_disputes WHERE metadata->>'case_ref' = ${caseId} OR metadata->>'ledger_case_id' = ${caseId} @@ -102,13 +102,13 @@ timelineRoutes.get('/cases/:caseId/timeline', async (c) => { id: `dispute:${d.id}`, date: d.created_at, type: 'dispute', - title: `[${d.domain || 'general'}] ${d.title}`, + title: `[${d.dispute_type || 'general'}] ${d.title}`, description: `Status: ${d.status}, Priority: ${d.priority}`, source: 'chittycommand', metadata: { status: d.status, priority: d.priority, - domain: d.domain, + disputeType: d.dispute_type, }, }); }