Skip to content
Merged
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
26 changes: 17 additions & 9 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 @@ -260,7 +261,7 @@ async function scoreDisputeWithTriage(

const result = await router.classifyDispute({
entity_id: disputeId,
entity_type: 'dispute',
entity_type: 'event', // @canon: chittycanon://gov/governance#core-types — disputes are Event (E)
title: dispute.title,
dispute_type: dispute.dispute_type,
amount: dispute.amount_at_stake != null ? Number(dispute.amount_at_stake) : undefined,
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
132 changes: 73 additions & 59 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,
}),

/** 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 Expand Up @@ -813,7 +827,7 @@ export function routerClient(env: Env) {
/** Classify a dispute via ChittyRouter TriageAgent */
classifyDispute: (payload: {
entity_id: string;
entity_type: 'dispute';
entity_type: 'event'; // @canon: chittycanon://gov/governance#core-types — disputes are Event (E)
title: string;
dispute_type: string;
amount?: number;
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