feat: scaffold outreach engine monorepo on the novelpad-desktop stack
Workspaces: config (copied), outreach-core (schema + actions/queries + hard gates), outreach-ai (Gemini client + embeddings copies, profiler and mission-fit-judge agent stubs), outreach-worker (DBOS executor with nightly ingest + hourly expiry workflows), outreach-review (RR7 review queue v0). Initial drizzle migration incl. pgvector extension. Stack contract: Yarn 4.5.0 + Turbo, Node 22.16, Drizzle 0.44.6 + pgvector, DBOS 4.17.6, @google/genai on Vertex, gemini-embedding-001 @1536, React Router v7. Files copied from novelpad-desktop carry provenance headers @ 62c56b87. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export * from './insert-match.server.js';
|
||||
export * from './set-match-review.server.js';
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { NpOutreachDatabase, NpOutreachTransaction } from '#~/db/db.js';
|
||||
import { schema } from '#~/db/db.js';
|
||||
|
||||
export type NewMatchInput = Omit<
|
||||
typeof schema.matches.$inferInsert,
|
||||
'id' | 'createdAt' | 'updatedAt' | 'reviewStatus'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Records a scored (org, grant) match. Always lands in `reviewStatus:
|
||||
* 'pending'` — the review decision is a separate step via
|
||||
* `serverSetMatchReview`.
|
||||
*/
|
||||
export async function serverInsertMatch(
|
||||
db: NpOutreachDatabase | NpOutreachTransaction,
|
||||
match: NewMatchInput,
|
||||
): Promise<string> {
|
||||
const [row] = await db
|
||||
.insert(schema.matches)
|
||||
.values({ ...match, reviewStatus: 'pending' })
|
||||
.returning({ id: schema.matches.id });
|
||||
|
||||
if (row == null) {
|
||||
throw new Error('serverInsertMatch: insert returned no row');
|
||||
}
|
||||
|
||||
return row.id;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
|
||||
import type { NpOutreachDatabase, NpOutreachTransaction } from '#~/db/db.js';
|
||||
import { schema } from '#~/db/db.js';
|
||||
|
||||
type ReviewStatus = (typeof schema.matches.$inferSelect)['reviewStatus'];
|
||||
type RejectReason = (typeof schema.matches.$inferSelect)['rejectReason'];
|
||||
|
||||
/**
|
||||
* Records a human reviewer's decision on a match. `rejectReason` is only
|
||||
* meaningful (and only ever set) when `reviewStatus` is `'rejected'` — any
|
||||
* other status clears it, so a previously-rejected match doesn't carry a
|
||||
* stale reason if it's later re-approved after editing.
|
||||
*/
|
||||
export async function serverSetMatchReview(
|
||||
db: NpOutreachDatabase | NpOutreachTransaction,
|
||||
{
|
||||
matchId,
|
||||
reviewStatus,
|
||||
rejectReason,
|
||||
}: {
|
||||
matchId: string;
|
||||
reviewStatus: ReviewStatus;
|
||||
rejectReason?: RejectReason;
|
||||
},
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(schema.matches)
|
||||
.set({
|
||||
reviewStatus,
|
||||
rejectReason: reviewStatus === 'rejected' ? (rejectReason ?? null) : null,
|
||||
updatedAt: sql`now()`,
|
||||
})
|
||||
.where(eq(schema.matches.id, matchId));
|
||||
}
|
||||
Reference in New Issue
Block a user