diff --git a/packages/core/src/task-store/project-store-ops.ts b/packages/core/src/task-store/project-store-ops.ts index 2a22488069..13c460a9b6 100644 --- a/packages/core/src/task-store/project-store-ops.ts +++ b/packages/core/src/task-store/project-store-ops.ts @@ -504,6 +504,20 @@ export function getRunAuditEventsImpl(store: TaskStore, options: RunAuditEventFi return rows.map((row) => store.rowToRunAuditEvent(row)); } +/* +FNXC:WorkflowLifecycleColumns 2026-07-31-02:45 (audited — DEAD SYNC PATH, do not convert): +The literal below would leak a merge-queue entry on a renamed board — a card leaving review would +never be dequeued — except that this function does not run in production. + +It is the SQLite-mode twin. The live path is `dequeueMergeQueueOnColumnExitInTransaction` +(`async-merge-coordination.ts`), called from `moves.ts`, and it is ALREADY converted: it takes +`moveReviewColumns` and the caller supplies them. This body reaches for `store.db.prepare`, which +throws in PostgreSQL backend mode, so a renamed board never gets far enough to be mis-dequeued. + +Converting it would mean threading a lane set into a function whose first statement cannot execute. +Recorded instead, so the census entry is not mistaken for unconverted debt — and so that whoever +finally deletes the sync SQLite residue can take this with it. +*/ export function dequeueMergeQueueOnColumnExitImpl(store: TaskStore, taskId: string, previousColumn: ColumnId, nextColumn: ColumnId, now: string): void { if (previousColumn !== "in-review" || nextColumn === "in-review") { return; diff --git a/packages/core/src/task-store/task-id-integrity.ts b/packages/core/src/task-store/task-id-integrity.ts index 7c00c71d04..4552c36c68 100644 --- a/packages/core/src/task-store/task-id-integrity.ts +++ b/packages/core/src/task-store/task-id-integrity.ts @@ -422,6 +422,22 @@ export function isTaskArchivedImpl(store: TaskStore, id: string): boolean { Sync isTaskArchived cannot query PostgreSQL. Prefer isTaskArchivedAsyncImpl from async callers. In backend mode use the in-memory task cache when the row is already hydrated; otherwise false (caller should have used async). + */ + /* + FNXC:WorkflowLifecycleColumns 2026-07-31-02:45 (audited — REAL, and narrow): + `cached.column` is a real board lane, so a renamed archived column is not recognised and this + sync check answers false for a card the board shows as archived. + + Narrow because of what it already concedes: the note above says this path exists only for a row + that happens to be hydrated in the cache, and every async caller is told to use + `isTaskArchivedAsyncImpl` instead. The authoritative path (below) reads `getLiveTaskColumn`, whose + own comparison is the one worth converting — fixing it there makes this file's sentinel check + correct without touching it. + + Left counted so the census keeps pointing here, and deliberately NOT converted in isolation: a + sync function with no store-scoped workflow read cannot resolve a lane, and converting this one + while `getLiveTaskColumn` still keys on the literal would leave the two disagreeing about what + archived means. */ const cached = store.taskCache.get(id); return cached?.column === "archived"; diff --git a/packages/engine/src/auto-merge-finalization.ts b/packages/engine/src/auto-merge-finalization.ts index 4b9274acf2..ebf770726e 100644 --- a/packages/engine/src/auto-merge-finalization.ts +++ b/packages/engine/src/auto-merge-finalization.ts @@ -81,6 +81,19 @@ export async function validateWorkflowDoneMergeProof( options: { result?: MergeResult; checkWorkflowSteps?: boolean } = {}, ): Promise { const hasProof = hasDurableMergeProof(task, options.result); + /* + FNXC:WorkflowLifecycleColumns 2026-07-31-02:45 (audited — REAL but DIAGNOSTIC-ONLY): + This literal selects which REASON STRING is reported, not which branch runs. Both arms return + `{ ok: false }`, so on a renamed board a card sitting in the complete lane is refused with the + generic `missing-merge-confirmation` instead of the specific `done-without-merge-confirmation`. + + Worth recording rather than converting from here: the resolver two functions up already computes + `isCompleteColumn` for exactly this workflow, and threading it in is the right fix — but this + function does not receive it, and widening the signature to improve an error string is a change + whose cost outweighs the diagnosis it sharpens. The other two census entries in this file are NOT + defects: the `columnId === "done"` at the top is the resolver's documented degraded fallback (the + live arm calls `columnHasFlag`), and the `step.status` comparison is a STEP status, not a column. + */ if (!hasProof) return { ok: false, reason: task.column === "done" ? "done-without-merge-confirmation" : "missing-merge-confirmation" }; if (options.checkWorkflowSteps !== false && hasIncompleteWorkflowSteps(task)) { return { ok: false, reason: "incomplete-workflow-steps" };