Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions src/lib/dispute-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { NeonQueryFunction } from '@neondatabase/serverless';
import type { Env } from '../index';
import { notionClient, routerClient, ledgerClient } from './integrations';


// ── Types ─────────────────────────────────────────────────────

interface DisputeCore {
Expand Down Expand Up @@ -302,21 +303,28 @@ 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 caseRef = `CC-DISPUTE-${disputeId.slice(0, 8)}`;
const entryResult = await ledger.addEntry({
entityType: 'audit',
entityId: caseRef,
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: caseRef, ledger_entry_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} → case ${caseRef} (entry ${entryResult.id})`);
}
} catch (err) {
console.error(`[dispute-sync:ledger] Failed for dispute ${disputeId}:`, err);
Expand Down
130 changes: 72 additions & 58 deletions src/lib/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
status?: 'pending' | 'confirmed' | 'rejected';
}

export function ledgerClient(env: Env) {
Expand All @@ -38,63 +32,55 @@ export function ledgerClient(env: Env) {
headers['Authorization'] = `Bearer ${env.CHITTYLEDGER_TOKEN}`;
}

async function post<T>(path: string, body: unknown): Promise<T | null> {
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<Record<string, unknown>[]> => {
/** Search ledger entries */
searchEntries: async (params: { entityType?: string; entityId?: string; actor?: string; status?: string; limit?: number }): Promise<Record<string, unknown>[]> => {
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<string, unknown>[];
} catch { return []; }
},

/** Get facts for a case (if supported) */
getFactsForCase: async (caseId: string): Promise<Record<string, unknown>[]> => {
/** Get chain of custody for an entity */
getChainOfCustody: async (entityId: string): Promise<Record<string, unknown>[]> => {
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<string, unknown>[];
} catch { return []; }
},

/** Get contradictions for a case (if supported) */
getContradictionsForCase: async (caseId: string): Promise<Record<string, unknown>[]> => {
/** 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<string, unknown>[];
} 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<Record<string, unknown> | null> => {
try {
const res = await fetch(`${baseUrl}/statistics`, { headers });
if (!res.ok) return null;
return await res.json() as Record<string, unknown>;
} catch { return null; }
},
};
}
Expand Down Expand Up @@ -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<string, string> = { 'X-Source-Service': 'chittycommand' };

async function get<T>(path: string): Promise<T | null> {
try {
Expand All @@ -145,7 +131,35 @@ export function evidenceClient(env: Env) {
}
}

async function post<T>(path: string, body: unknown): Promise<T | null> {
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,
}),
Comment on lines +151 to +157

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how submitDocument is called to see if fileSize/evidenceTier are expected
rg -n "submitDocument" --type ts -A 5

Repository: chittyos/chittycommand

Length of output: 1416


🏁 Script executed:

#!/bin/bash
# Check for any documentation or tests about the ChittyEvidence /collect endpoint
rg -n "/collect" --type ts -B 2 -A 2

Repository: chittyos/chittycommand

Length of output: 556


🏁 Script executed:

#!/bin/bash
# Look for any ChittyEvidence API schema or type definitions
rg -n "ChittyEvidence|collect" --type ts -i | head -30

Repository: chittyos/chittycommand

Length of output: 3277


🏁 Script executed:

#!/bin/bash
# Check if there are other integrations files or clients that might show the expected structure
find . -type f -name "*evidence*" -o -name "*type*" | grep -E "\.(ts|tsx|d\.ts)$"

Repository: chittyos/chittycommand

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Read the full integrations.ts file around the submitDocument method to see documentation
cat -n src/lib/integrations.ts | sed -n '130,170p'

Repository: chittyos/chittycommand

Length of output: 2021


🏁 Script executed:

#!/bin/bash
# Check if there are any constants or configuration about ChittyEvidence API expectations
rg -n "CHITTY.*EVIDENCE|evidence.*url|/collect|submit" --type ts -i -B 2 -A 2 src/lib/integrations.ts

Repository: chittyos/chittycommand

Length of output: 2172


🏁 Script executed:

#!/bin/bash
# Look for any recent commits or history mentioning these fields
git log --oneline --all -S "fileSize\|evidenceTier" -- src/lib/integrations.ts 2>/dev/null | head -10

Repository: chittyos/chittycommand

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Look for any tests related to submitDocument or evidenceClient
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*.test.js" \) | xargs rg -l "submitDocument|evidenceClient" 2>/dev/null

Repository: chittyos/chittycommand

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check the complete integrations.ts to see if there are any comments about field handling
cat -n src/lib/integrations.ts | sed -n '115,125p'

Repository: chittyos/chittycommand

Length of output: 478


Include fileSize and evidenceTier fields in the /collect POST request.

The submitDocument method accepts fileSize and evidenceTier in the payload but drops them from the request body sent to /collect. Both callers in src/routes/documents.ts and src/routes/bridge/ledger.ts explicitly pass evidenceTier, indicating these fields are intentional and should be forwarded to the ChittyEvidence service.

Current code (lines 151–157)
    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,
      }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/integrations.ts` around lines 147 - 153, The submitDocument helper is
dropping payload.fileSize and payload.evidenceTier when calling
post('/collect'); update the request body in submitDocument so it forwards
file_size: payload.fileSize and evidence_tier: payload.evidenceTier along with
the existing file_name, document_type, description, and case_id fields (keep the
function name submitDocument and the POST to '/collect' unchanged).


/** Record a chain-of-custody event on a document */
addCustodyEntry: (documentId: string, entry: { action: string; performedBy: string; location?: string; notes?: string }) =>
post<Record<string, unknown>>(`/legal/documents/${encodeURIComponent(documentId)}/custody`, entry),

/** Get enriched facts for a case (facts + entities + amounts) */
getEnrichedFacts: (caseId: string) =>
get<EvidenceFact[]>(`/facts/cases/${encodeURIComponent(caseId)}/enriched`),
Expand Down
4 changes: 2 additions & 2 deletions src/lib/job-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
}
Expand Down
61 changes: 39 additions & 22 deletions src/routes/bridge/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -25,28 +25,40 @@ 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}
`;
synced++;
}
}

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);
Expand All @@ -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++;
Expand All @@ -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',
Expand Down
10 changes: 5 additions & 5 deletions src/routes/documents.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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),
Expand Down
Loading
Loading