(
group: WorkflowGroup,
row: TableRow,
opts?: { isManualRun?: boolean; mode?: DispatchMode }
)
| 70 | | 'deps-unmet' |
| 71 | |
| 72 | export function classifyEligibility( |
| 73 | group: WorkflowGroup, |
| 74 | row: TableRow, |
| 75 | opts?: { isManualRun?: boolean; mode?: DispatchMode } |
| 76 | ): EligibilityReason { |
| 77 | const isManualRun = opts?.isManualRun ?? false |
| 78 | const mode = opts?.mode ?? 'all' |
| 79 | |
| 80 | if (group.autoRun === false && !isManualRun) return 'autoRun-off' |
| 81 | |
| 82 | const exec = row.executions?.[group.id] |
| 83 | // Dispatcher pre-stamp orphans (`pending` + `executionId: null`) are |
| 84 | // placeholders left behind when a previous dispatcher loop wrote the stamp |
| 85 | // but no cell-task picked up (cascade-lock contention, trigger.dev queue |
| 86 | // failure, etc.). Treat them as claimable so a new dispatcher can re-enqueue |
| 87 | // — without this carve-out the row would render "Queued" forever. Matches |
| 88 | // the `pickNextEligibleGroupForRow` cascade-loop carve-out. |
| 89 | const isOrphanPreStamp = exec?.status === 'pending' && exec.executionId == null |
| 90 | if (!isOrphanPreStamp && isExecInFlight(exec)) return 'in-flight' |
| 91 | const status = exec?.status |
| 92 | |
| 93 | // `mode: 'new'` is the auto-fire scope: only rows that have never been |
| 94 | // attempted on this group run. Any pre-existing exec entry — completed, |
| 95 | // cancelled, or error — keeps the cell sticky until the user manually |
| 96 | // re-runs via "Run column" / "Run all rows" / "Run this row". |
| 97 | // Exception: orphan pre-stamps are claimable (handled above). |
| 98 | if (mode === 'new' && exec && !isOrphanPreStamp) return 'has-prior-attempt' |
| 99 | |
| 100 | const completedAndFilled = status === 'completed' && areOutputsFilled(group, row) |
| 101 | // For an enrichment a `completed` run is terminal even with empty outputs — |
| 102 | // a no-match is a real result, not an unfinished run. Treating it as "done" |
| 103 | // stops the auto cascade from re-invoking billable provider calls on every |
| 104 | // no-match row each dispatch. A genuine input change clears the exec entry |
| 105 | // (see deriveExecClearsForDataPatch), so real re-runs still happen. |
| 106 | const isDone = completedAndFilled || (group.type === 'enrichment' && status === 'completed') |
| 107 | if (!isManualRun && isDone) return 'completed-on-auto' |
| 108 | if (!isManualRun && status === 'error') return 'error-on-auto' |
| 109 | if (!isManualRun && status === 'cancelled') return 'cancelled-on-auto' |
| 110 | // Manual incomplete-mode runs (Run row / Run incomplete) treat a `completed` |
| 111 | // group as done even if an output is blank — only "Run all" re-runs it. The |
| 112 | // auto cascade still re-fills blank workflow outputs (completedAndFilled). |
| 113 | if (mode === 'incomplete') { |
| 114 | if (isManualRun ? status === 'completed' : isDone) { |
| 115 | return 'completed-on-incomplete' |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | if (isManualRun && group.autoRun === false) return 'manual-bypass' |
| 120 | return areGroupDepsSatisfied(group, row) ? 'eligible' : 'deps-unmet' |
| 121 | } |
| 122 | |
| 123 | export function isGroupEligible( |
| 124 | group: WorkflowGroup, |
no test coverage detected