Address Greptile review on aggregation PR

Push the review-stage caller filter into SQL so agent callers in busy
companies can't get a silently truncated review feed, make the routine
reference check a targeted JSONB query instead of a company-wide stage
scan, and expose connections on the UI PipelineListItem type.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-10 20:20:49 +00:00
1 parent 60c3ca6e94
commit 973e8f5e98
3 files changed
+17 -14

No files matched your search

+6 -7
View File
@@ -56,10 +56,10 @@ function caseDisplay(row: { case: CaseRow; stage: StageRow; pipeline: PipelineRo
// Current review-stage approver semantics: reviewerKind "human" awaits any
// human caller, "any" awaits anyone. B1 (stage requireApproval + named
// approver) extends this predicate when it lands — keep the seam here.
function reviewStageAwaitsCaller(config: Record<string, unknown>, caller: AttentionCaller) {
const reviewerKind = typeof config.reviewerKind === "string" ? config.reviewerKind : "human";
if (reviewerKind === "any") return true;
return caller.type === "user";
// SQL-side so busy companies can't truncate an agent's review feed.
function reviewStageAwaitsCallerSql(caller: AttentionCaller) {
if (caller.type === "user") return sql`true`;
return sql`coalesce(${pipelineStages.config}->>'reviewerKind', 'human') = 'any'`;
}
function boundedLimit(limit: number | undefined, fallback: number, max: number) {
@@ -129,13 +129,12 @@ export async function listPipelineAttention(
eq(pipelines.companyId, input.companyId),
eq(pipelineStages.kind, "review"),
isNull(pipelineCases.terminalKind),
reviewStageAwaitsCallerSql(input.caller),
))
.orderBy(asc(pipelineCases.createdAt))
.limit(PIPELINE_ATTENTION_MAX_LIMIT * 2);
.limit(limit);
const reviews = reviewRows
.filter((row) => reviewStageAwaitsCaller((row.stage.config ?? {}) as Record<string, unknown>, input.caller))
.slice(0, limit)
.map((row) => {
const config = (row.stage.config ?? {}) as Record<string, unknown>;
return {
+10 -7
View File
@@ -853,15 +853,18 @@ export function pipelineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeu
dbOrTx: PipelineDb,
input: { companyId: string; routineId: string; exceptStageId?: string | null },
) {
const stages = await dbOrTx
.select({ id: pipelineStages.id, config: pipelineStages.config })
const referencing = await dbOrTx
.select({ id: pipelineStages.id })
.from(pipelineStages)
.innerJoin(pipelines, eq(pipelineStages.pipelineId, pipelines.id))
.where(eq(pipelines.companyId, input.companyId));
return stages.some((stage) =>
stage.id !== input.exceptStageId &&
stageAutomationRoutineIdFromConfig((stage.config ?? {}) as PipelineStageConfig) === input.routineId
);
.where(and(
eq(pipelines.companyId, input.companyId),
sql`${pipelineStages.config}->'onEnter'->>'type' = 'run_routine'`,
sql`${pipelineStages.config}->'onEnter'->>'routineId' = ${input.routineId}`,
input.exceptStageId ? ne(pipelineStages.id, input.exceptStageId) : undefined,
))
.limit(1);
return referencing.length > 0;
}
async function clearPipelineAutomationRoutineIfUnreferenced(
+1
View File
@@ -11,6 +11,7 @@ export interface PipelineListItem {
archivedAt: Date | string | null;
stageCount: number;
openCaseCount: number;
connections: { upstreamPipelineIds: string[]; downstreamPipelineIds: string[] };
createdAt: Date | string;
updatedAt: Date | string;
}