Build the agent-native compute CRM platform
CI / verify (push) Successful in 3m6s

This commit is contained in:
2026-08-13 01:39:01 -07:00
parent bfd2f8d95a
commit 853bde2265
160 changed files with 61812 additions and 483 deletions
+143
View File
@@ -0,0 +1,143 @@
import { FACT_STATUSES } from '@pig/core';
import type { Fact, Database } from '@pig/db';
import { accounts, contacts, facts } from '@pig/db';
import { and, desc, eq } from 'drizzle-orm';
import { Hono } from 'hono';
import { z } from 'zod';
import {
apiError,
MutationError,
mutation,
type ApiEnv,
type MutationDefinition,
} from '../lib/mutation';
const factDecisionSchema = z
.object({
status: z.enum(FACT_STATUSES).refine(
(status): status is 'approved' | 'dismissed' =>
status === 'approved' || status === 'dismissed',
'A review may only approve or dismiss a proposed fact.',
),
})
.strict();
export interface FactDecisionResult {
fact: Fact;
/** Approval records a judgement; applying arbitrary field names is a separate safe system. */
recordUpdated: false;
}
function carriesEvidence(fact: Fact): boolean {
return Boolean(
fact.sourceUrl?.trim() ||
(fact.evidence && Object.keys(fact.evidence).length > 0),
);
}
/**
* Human approval accepts the evidence, not an untyped write to an arbitrary
* CRM field. Keeping those operations separate prevents a plausible claim
* such as `email` from silently overwriting a real person's record.
*/
export const factDecisionDefinition: MutationDefinition<
typeof factDecisionSchema,
FactDecisionResult
> = {
schema: factDecisionSchema,
permission: { capability: 'data:import', team: 'research' },
invalidMessage: 'Invalid fact review decision.',
async mutate({ input, params, principal, tx, now }) {
const id = params.id;
if (!id) throw MutationError.notFound('Fact');
const [before] = await tx.select().from(facts).where(eq(facts.id, id)).limit(1);
if (!before) throw MutationError.notFound('Fact');
if (before.status !== 'proposed') {
throw new MutationError(
'fact_already_decided',
'Only a proposed fact can be reviewed.',
409,
);
}
if (input.status === 'approved' && !carriesEvidence(before)) {
throw new MutationError(
'missing_evidence',
'A fact needs a source or evidence before it can be approved.',
409,
);
}
const [updated] = await tx
.update(facts)
.set({
status: input.status,
decidedByUserId: principal.userId,
decidedAt: now,
})
.where(and(eq(facts.id, id), eq(facts.status, 'proposed')))
.returning();
if (!updated) {
throw new MutationError(
'fact_already_decided',
'This fact was reviewed by someone else.',
409,
);
}
const approved = input.status === 'approved';
return {
data: { fact: updated, recordUpdated: false },
activity: {
type: 'agent_action',
subject: `${approved ? 'Approved' : 'Dismissed'} proposed ${before.field}`,
body: approved
? 'Evidence approved; the CRM record remains unchanged until field-aware application is available.'
: 'Proposal dismissed; the CRM record was not changed.',
accountId: before.accountId ?? undefined,
contactId: before.contactId ?? undefined,
meta: {
factId: before.id,
decision: input.status,
field: before.field,
recordUpdated: false,
},
},
};
},
};
export function createFactsRoute(db: Database) {
const route = new Hono<ApiEnv>();
route.get('/api/facts', async (c) => {
const parsedStatus = z
.enum(FACT_STATUSES)
.safeParse(c.req.query('status') ?? 'proposed');
if (!parsedStatus.success) {
return c.json(
apiError('invalid_request', 'Unknown fact status.', parsedStatus.error.issues),
400,
);
}
const rows = await db
.select({
fact: facts,
accountName: accounts.name,
contactName: contacts.fullName,
})
.from(facts)
.leftJoin(accounts, eq(facts.accountId, accounts.id))
.leftJoin(contacts, eq(facts.contactId, contacts.id))
.where(eq(facts.status, parsedStatus.data))
.orderBy(desc(facts.observedAt))
.limit(200);
return c.json({ facts: rows });
});
route.patch('/api/facts/:id/decision', mutation(db, factDecisionDefinition));
return route;
}