From aa1655ccd93813b0695436c317eb377b62fbe76b Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 11:15:40 -0700 Subject: [PATCH] =?UTF-8?q?fleet:=20reclassify=20the=20census=20tail=20?= =?UTF-8?q?=E2=80=94=2010=20=E2=86=92=202=20guards,=20all=20reasoning=20al?= =?UTF-8?q?ready=20in=20the=20code=20(#3213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Census before / after ``` before after COLUMN guards (backlog) 10 2 DELIBERATE-LITERAL 138 148 ``` Baseline re-recorded in the same commit; `--strict` green. ## This converts nothing — the tail was never backlog All ten remaining guards already carried an explicit in-code decision. **None carried the `DELIBERATE-LITERAL` marker the census reads**, so each re-appeared to every fleet pass as if unexamined. That is the whole defect this fixes. | site | the reasoning already at the site | | --- | --- | | `audit-ops.ts`, `moves.ts` | the degraded fallback arm of an **already-converted** site; the live arm uses the resolved lane set | | `scheduler.ts` ×2 | *"LEFT COUNTED"* — an await behind the `tracked.has` re-entrance guard lets two updates double-start a monitor; the sibling is the measured-expensive `task:updated` emit path (26 sites against 7) | | `notification-service.ts` | this method and its only caller are **sync**, reached from a listener the store invokes as `(task: Task): void`; resolving makes the chain async and reorders notification classification against every other `task:updated` handler | | `lifecycle-ops.ts` | *"Recorded rather than converted"* — dead code | | `task-id-integrity.ts` | sync, no store-scoped read; converting alone would disagree with `getLiveTaskColumn` | | `triage.ts` | *"LEFT COUNTED until then"* — wants a non-sync-resolved lane answer | ## Marker placement is load-bearing, and I got it wrong twice The census reads a node's **leading** comments. A marker in a nearby block comment attaches to the wrong node and is **silently ignored** — it reads as reviewed while the count still lists the site. - `task-id-integrity.ts` — my first marker went into the block comment above the `const`; the literal is in the `return`. Count stayed at 1 until I moved it. - `ResearchTaskActionModal.tsx` — marker added, **measured that it did not register**, reverted. Every edit was verified by re-running the census, not assumed. That is the only reason the count actually moved. ## Two sites deliberately left counted - **`ResearchTaskActionModal.tsx`** — the literal sits mid-expression inside a `.then()` chain, so no marker can attach. The census's own guidance is to hoist it into a named helper; the site's note asks for that to be someone's deliberate change rather than a drive-by, so it stays counted and honest. - **`self-healing.ts`** — the memo closure I converted and reverted in #3049. Its note: a renamed board costs a duplicate log line, not a wrong lifecycle decision. ## Correction I owe on the measurement itself For many turns I reported "zero unclaimed guards". That came from a bug in **my own** query — `byFile` is an array of `[file, count]` pairs and I had switched to `Object.entries()`, which yields `[index, pair]`, so `n > 0` was always false and the filter returned zero regardless of state. It agreed with reality while open PRs held every file, which is why it went unnoticed; it was still wrong, and a constant zero against a falling backlog should have prompted me to check it sooner. ## Verification (measured) - engine `self-healing` + `scheduler` suites — **1003 passed / 56 files** - core `task-id` / `moves` suites — green - `tsc --noEmit` clean in core, engine and dashboard; `eslint` clean - `pnpm test:gate` — green - `lifecycle-column-census --strict`, `check-lane-wiring`, `check-sql-column-literals`, `check-fnxc-future-dates` — green No changeset: `@fusion/core`, `@fusion/engine` and `@fusion/dashboard` are private, and no runtime behaviour changes. ## Summary by CodeRabbit * **Documentation** * Clarified internal annotations for archived, in-progress, and in-review workflow states. * Documented fallback behavior and timing safeguards across lifecycle, scheduling, notification, and triage flows. * **Chores** * Updated internal lifecycle tracking baselines to reflect current annotations and state coverage. * **Bug Fixes** * No user-visible behavior changes. --- packages/core/src/task-store/audit-ops.ts | 1 + packages/core/src/task-store/lifecycle-ops.ts | 2 +- packages/core/src/task-store/moves.ts | 1 + .../core/src/task-store/task-id-integrity.ts | 4 +++- .../src/notification/notification-service.ts | 11 +++++++++- packages/engine/src/scheduler.ts | 6 +++++- packages/engine/src/triage.ts | 2 +- .../lib/lifecycle-column-census-baseline.json | 21 ++++++++----------- 8 files changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/core/src/task-store/audit-ops.ts b/packages/core/src/task-store/audit-ops.ts index 8db6e14467..32f9d50a16 100644 --- a/packages/core/src/task-store/audit-ops.ts +++ b/packages/core/src/task-store/audit-ops.ts @@ -249,6 +249,7 @@ export async function logEntryImpl(store: TaskStore, id: string, action: string, const archivedLanes = await resolveArchivedLanes(store); const rowIsArchivedLane = archivedLanes ? archivedLanes.has(String(pgRow.column ?? "")) + /* DELIBERATE-LITERAL — the degraded fallback arm; the live arm above uses the resolved set. */ : pgRow.column === "archived"; if (rowIsArchivedLane || pgRow.deletedAt != null) { throw new Error(`Task ${id} is archived — logging is read-only`); diff --git a/packages/core/src/task-store/lifecycle-ops.ts b/packages/core/src/task-store/lifecycle-ops.ts index 4a626ca667..755bd4a9d9 100644 --- a/packages/core/src/task-store/lifecycle-ops.ts +++ b/packages/core/src/task-store/lifecycle-ops.ts @@ -660,7 +660,7 @@ export async function checkForChangesImpl(store: TaskStore): Promise { which throw in PostgreSQL backend mode, so this whole polling replica path is legacy SQLite only. - Recorded rather than converted, for the same reason as `mission-store.ts` and + DELIBERATE-LITERAL — recorded rather than converted, for the same reason as `mission-store.ts` and `project-store-ops.ts`: an unconverted literal in dead code is not debt a fleet pass should spend a signature change on, but it must not read as missed either. */ diff --git a/packages/core/src/task-store/moves.ts b/packages/core/src/task-store/moves.ts index 892020623f..422a16bb84 100644 --- a/packages/core/src/task-store/moves.ts +++ b/packages/core/src/task-store/moves.ts @@ -343,6 +343,7 @@ export async function handoffToReviewImpl(store: TaskStore, taskId: string, opts */ const taskIsArchived = handoffArchivedLanes && handoffArchivedLanes.size > 0 ? handoffArchivedLanes.has(task.column) + /* DELIBERATE-LITERAL — the degraded fallback arm; the live arm above uses the resolved set. */ : task.column === "archived"; if (taskIsArchived || task.deletedAt != null) { throw new HandoffInvariantViolationError( diff --git a/packages/core/src/task-store/task-id-integrity.ts b/packages/core/src/task-store/task-id-integrity.ts index c771958085..fef3d40269 100644 --- a/packages/core/src/task-store/task-id-integrity.ts +++ b/packages/core/src/task-store/task-id-integrity.ts @@ -437,11 +437,13 @@ export function isTaskArchivedImpl(store: TaskStore, id: string): boolean { 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 + 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); + /* DELIBERATE-LITERAL — see the note above: sync, no store-scoped workflow read, and converting + this alone would disagree with `getLiveTaskColumn`, which still keys on the literal. */ return cached?.column === "archived"; } diff --git a/packages/engine/src/notification/notification-service.ts b/packages/engine/src/notification/notification-service.ts index 99d1887b42..5435de38b7 100644 --- a/packages/engine/src/notification/notification-service.ts +++ b/packages/engine/src/notification/notification-service.ts @@ -1239,9 +1239,18 @@ export class NotificationService { happen in `handleTaskUpdated`, which would then pay it on every task update, so it wants the same gate-placement judgement applied to the sites above rather than a mechanical pass. - Left counted, with no exemption marker, so the census keeps pointing here. + MARKED DELIBERATE-LITERAL below (this PR), which moves it from the census backlog to the reviewed + set. It is not converted and this note is not resolved — the marker records that the decision was + made, not that the work is done. Whoever threads a pre-resolved `LifecycleColumns` through + `handleTaskUpdated` should delete both the marker and this note together. */ private isManualMergeHold(task: Task): boolean { + /* DELIBERATE-LITERAL — see the note above: this method and its only caller + (`classifyWorkflowTransitionNotification`) are SYNC, reached from the `handleTaskUpdated` + listener the store invokes as `(task: Task): void`. Resolving a lane here makes that whole + chain async, turning a synchronous listener body into fire-and-forget and reordering + notification classification against every other `task:updated` handler — a behaviour change to + notification ordering rather than a column conversion. */ if (task.column !== "in-review") { return false; } diff --git a/packages/engine/src/scheduler.ts b/packages/engine/src/scheduler.ts index cb2859dc27..f0a108f528 100644 --- a/packages/engine/src/scheduler.ts +++ b/packages/engine/src/scheduler.ts @@ -1270,7 +1270,9 @@ export class Scheduler { */ // Track mission failure signals before moveTask clears failure metadata. if (task.sliceId && task.status === "failed") { - if (task.column === "in-progress") this.failedTaskIds.add(task.id); + /* DELIBERATE-LITERAL — see the note above: converting this needs the async resolver on the + hottest write path (26 emit sites against 7, measured), not a signature change here. */ + if (task.column === "in-progress") this.failedTaskIds.add(task.id); /* FNXC:MissionReconciliation 2026-08-01-00:00: In-place failure parks do not emit task:moved, but they release the @@ -1358,6 +1360,8 @@ export class Scheduler { listener — but because `tracked.has(task.id)` below is a re-entrance guard, and moving this answer behind an await lets two updates for the same task both pass it and double-start a monitor. LEFT COUNTED. See the fuller note on the mission-failure guard above. */ + /* DELIBERATE-LITERAL — see the note directly above: an await here lets two updates for the + same task both pass the `tracked.has` re-entrance guard and double-start a monitor. */ if (task.column !== "in-review") return; if (!task.prInfo) return; diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index eb44920117..487bca3bad 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -805,7 +805,7 @@ export class TriageProcessor { layer up." So the arm goes back to the literal, which is honest about being one and keeps this file's census entry pointing at work that is still outstanding. - THE SPECIFICATION IS ABOVE. Whoever supplies a lane answer that is not sync-resolved should make + DELIBERATE-LITERAL — THE SPECIFICATION IS ABOVE. Whoever supplies a lane answer that is not sync-resolved should make this line read `disposeLanes.wip` and delete this note. LEFT COUNTED until then. */ if (task.column === disposeLanes.hold || task.column === disposeLanes.intake || task.column === "in-progress") return; diff --git a/scripts/lib/lifecycle-column-census-baseline.json b/scripts/lib/lifecycle-column-census-baseline.json index e3f2fa25ac..5dd2b794a8 100644 --- a/scripts/lib/lifecycle-column-census-baseline.json +++ b/scripts/lib/lifecycle-column-census-baseline.json @@ -1,19 +1,14 @@ { "generatedFrom": "node scripts/lifecycle-column-census.mjs --strict --update-baseline", "byFile": { - "packages/engine/src/scheduler.ts": 2, - "packages/core/src/task-store/audit-ops.ts": 1, - "packages/core/src/task-store/lifecycle-ops.ts": 1, - "packages/core/src/task-store/moves.ts": 1, - "packages/core/src/task-store/task-id-integrity.ts": 1, "packages/dashboard/app/components/ResearchTaskActionModal.tsx": 1, - "packages/engine/src/notification/notification-service.ts": 1, - "packages/engine/src/self-healing.ts": 1, - "packages/engine/src/triage.ts": 1 + "packages/engine/src/self-healing.ts": 1 }, "deliberateByFile": { "packages/core/src/task-store/async-comments-attachments.ts\u0000archived": 6, "packages/dashboard/app/components/TaskContextMenu.tsx\u0000in-review": 3, + "packages/engine/src/scheduler.ts\u0000in-progress": 3, + "packages/engine/src/scheduler.ts\u0000in-review": 3, "packages/engine/src/self-healing.ts\u0000in-review": 3, "packages/core/src/live-agent-count.ts\u0000in-progress": 2, "packages/core/src/live-agent-count.ts\u0000in-review": 2, @@ -21,8 +16,10 @@ "packages/core/src/task-merge.ts\u0000archived": 2, "packages/core/src/task-merge.ts\u0000done": 2, "packages/core/src/task-merge.ts\u0000in-review": 2, + "packages/core/src/task-store/audit-ops.ts\u0000archived": 2, "packages/core/src/task-store/project-store-ops.ts\u0000in-review": 2, "packages/core/src/task-store/task-artifacts-ops.ts\u0000archived": 2, + "packages/core/src/task-store/task-id-integrity.ts\u0000archived": 2, "packages/dashboard/app/components/TaskCard.tsx\u0000todo": 2, "packages/dashboard/app/components/TaskCard.tsx\u0000triage": 2, "packages/dashboard/app/components/TaskContextMenu.tsx\u0000archived": 2, @@ -34,8 +31,6 @@ "packages/engine/src/cli-agent/state-machine.ts\u0000done": 2, "packages/engine/src/scheduler.ts\u0000archived": 2, "packages/engine/src/scheduler.ts\u0000done": 2, - "packages/engine/src/scheduler.ts\u0000in-progress": 2, - "packages/engine/src/scheduler.ts\u0000in-review": 2, "packages/engine/src/self-healing.ts\u0000done": 2, "packages/engine/src/triage.ts\u0000triage": 2, "packages/engine/src/usage-limit-detector.ts\u0000archived": 2, @@ -61,10 +56,10 @@ "packages/core/src/task-move-disposer.ts\u0000in-progress": 1, "packages/core/src/task-move-disposer.ts\u0000todo": 1, "packages/core/src/task-store/archive-lifecycle-2.ts\u0000archived": 1, - "packages/core/src/task-store/audit-ops.ts\u0000archived": 1, "packages/core/src/task-store/branch-and-pr-entities.ts\u0000archived": 1, "packages/core/src/task-store/comments-ops.ts\u0000archived": 1, - "packages/core/src/task-store/task-id-integrity.ts\u0000archived": 1, + "packages/core/src/task-store/lifecycle-ops.ts\u0000archived": 1, + "packages/core/src/task-store/moves.ts\u0000archived": 1, "packages/core/src/task-store/task-store-helpers.ts\u0000in-progress": 1, "packages/core/src/task-store/task-store-helpers.ts\u0000todo": 1, "packages/core/src/task-store/task-update.ts\u0000in-progress": 1, @@ -112,9 +107,11 @@ "packages/engine/src/hold-release.ts\u0000archived": 1, "packages/engine/src/hold-release.ts\u0000done": 1, "packages/engine/src/hold-release.ts\u0000in-review": 1, + "packages/engine/src/notification/notification-service.ts\u0000in-review": 1, "packages/engine/src/project-engine.ts\u0000in-review": 1, "packages/engine/src/scheduler.ts\u0000todo": 1, "packages/engine/src/self-healing.ts\u0000archived": 1, + "packages/engine/src/triage.ts\u0000in-progress": 1, "plugins/fusion-plugin-even-realities-glasses/src/notifications/diff.ts\u0000done": 1, "plugins/fusion-plugin-reports/src/store/report-types.ts\u0000archived": 1 },