FN-126: fix inert sync-lane validation

Prevent inert synchronization lanes from bypassing merge-gate validation while preserving task-lane cache and archive lifecycle behavior.

- Reject sync-resolved lane conversions in the static validator.
- Preserve task-lane cache emissions and active-session cleanup across task mutations and archival.
- Add regression coverage, baseline updates, documentation, and a release changeset.

Files changed:
 .changeset/fn-126-inert-sync-lane.md               |   7 +
 .../a-falling-count-is-not-evidence.md             |  12 +
 .../task-lane-cache-emitter-preservation.test.ts   | 269 +++++++++++++++++++++
 .../core/src/task-store/archive-lifecycle-2.ts     |   3 +-
 packages/core/src/task-store/moves.ts              |   3 +-
 packages/core/src/task-store/task-artifacts-ops.ts |   3 +-
 packages/core/src/task-store/task-update.ts       |   3 +-
 packages/core/src/task-store/update-task-deps.ts   |   3 +-
 ...xecutor-archive-releases-active-session.test.ts |  80 ++++++
 packages/engine/src/executor.ts                    |   4 +-
 .../src/executor/executor-side-effect-hosts.ts     |   2 +-
 .../executor/is-backward-move-out-of-planning.ts   |   7 +-
 .../src/executor/task-executor-graph-facades.ts    |   2 +-
 .../engine/src/executor/task-executor-imports.ts   |   1 -
 .../engine/src/executor/wire-executor-lifecycle.ts |  18 +-
 .../check-inert-sync-lane-conversions.test.mjs     |  83 +++++--
 scripts/check-inert-sync-lane-conversions.mjs      |  19 +-
 scripts/lib/inert-sync-lane-baseline.json          |   6 +-
 18 files changed, 479 insertions(+), 46 deletions(-)

Fusion-Task-Id: FN-126

Fusion-Task-Lineage: 0c7a1a3a-9446-44f8-955d-df6c402dfd31

Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
Fusion Agent
2026-08-22 01:39:23 +00:00
parent 8b681775ae
commit 33f4797ceb
18 changed files with 479 additions and 46 deletions

View File

@@ -2,7 +2,7 @@
* FNXC:CodeOrganization 2026-08-04-07:15:
* Single side-effect import for TaskExecutor FNXC/doc hosts (U4) so executor.ts
* does not spend a line per host module. isBackwardMoveOutOfPlanning body stays
* on TaskExecutor for inert-sync-lane (2 guards).
* on TaskExecutor for payload/cache/legacy lane tiering; no sync lane resolver is permitted.
*/
import "./is-backward-move-out-of-planning.js";
import "./task-executor-fields.js";

View File

@@ -1,8 +1,11 @@
/**
* FNXC:CodeOrganization 2026-08-04-06:20:
* Host for isBackwardMoveOutOfPlanning requirement history (U4). The method body stays on
* TaskExecutor so `check-inert-sync-lanes` keeps counting the two resolvePlannerLanes guards
* in executor.ts — do not free-peel that body without re-proving the inert-sync baseline.
* TaskExecutor and consumes only task:moved payload/cache lanes; it must never resolve lanes synchronously.
*
* FNXC:WorkflowResolvedColumns 2026-08-22-00:13:
* Measurement showed the PostgreSQL sync reader returns exactly the legacy defaults, so its fallback
* bought no renamed-board correctness. The listener now tiers payload, TaskLaneCache, then literals.
*
* FNXC:WorkflowLifecycleColumns 2026-07-30-16:55 (PR #2628 review, greptile P1):
* THE FORWARD EXCLUSIONS MUST RESOLVE TOO, and leaving them literal made this branch WORSE

View File

@@ -1,7 +1,7 @@
/**
* FNXC:CodeOrganization 2026-08-04-09:20:
* Workflow graph / merge-boundary / graph-failure routing facades peeled from TaskExecutor (U4).
* isBackwardMoveOutOfPlanning stays on TaskExecutor for inert-sync-lane (2 guards).
* isBackwardMoveOutOfPlanning stays on TaskExecutor for payload/cache/legacy lane tiering; no sync lane resolver is permitted.
*/
import type { Task, TaskDetail, Settings, Agent, ResolvedTaskOutputLanguage, WorkflowIr, WorkflowColumnAgent } from "@fusion/core";
import * as impl from "./impl-bindings.js";

View File

@@ -12,7 +12,6 @@ export type {
Agent, MergeResult, WorkflowIrNode, WorkflowIr, WorkflowColumnAgent, TaskMoveLanes,
ApprovalRequestStore,
} from "@fusion/core";
export { resolvePlannerLanes } from "../execution/replan-target.js";
export type { WorkflowGraphTaskRunResult } from "../workflows/workflow-graph-task-runner.js";
export type { WorkflowLegacySeams } from "../workflows/workflow-node-handlers.js";
export type { WorkflowRuntimePrimitives } from "../execution/runtime-primitives.js";

View File

@@ -269,6 +269,7 @@ export function wireExecutorLifecycle(deps: WireExecutorLifecycleDeps): WireExec
asserts every `task:moved` emit site supplies it. Until one of those lands, treat the fallback
as a live inertness path rather than defensive dead code.
*/
/* FNXC:WorkflowResolvedColumns 2026-08-22-00:13: This supersedes the prior residual-risk note: optional emitter payloads now consult TaskLaneCache before legacy ids; making lanes required and bridge forwarding remain separate follow-ups. */
deps.store.on("task:moved", ({ task, from, to, source, lanes }) => {
/*
FNXC:Diagnostics 2026-08-10-18:32:
@@ -292,9 +293,18 @@ export function wireExecutorLifecycle(deps: WireExecutorLifecycleDeps): WireExec
of this payload. `wipLane`/`archivedLane`/`holdLane` are read as SINGLE ids rather than sets
because each branch below is a lane-identity test on one column, which is what the literals were.
*/
const wipLane = lanes?.wip ?? "in-progress";
const archivedLane = lanes?.archived ?? "archived";
const holdLane = lanes?.hold ?? "todo";
/*
FNXC:WorkflowResolvedColumns 2026-08-22-00:13:
Optional payload lanes win, then the store's synchronous TTL cache preserves the last real
answer after an emitter resolution miss; literals are only the cold-cache compatibility tier.
Runtime/project bridges re-emit on their own EventEmitters, not TaskStore, so they cannot feed
this listener and intentionally remain outside this contract.
*/
const effectiveLanes = lanes ?? deps.store.laneCache?.get(task.id);
/* FNXC:WorkflowResolvedColumns 2026-08-22-00:28: fall back only when no lane answer exists; an answer with an absent role must not invent a legacy role-named column. */
const wipLane = effectiveLanes ? effectiveLanes.wip : "in-progress";
const archivedLane = effectiveLanes ? effectiveLanes.archived : "archived";
const holdLane = effectiveLanes ? effectiveLanes.hold : "todo";
if (to === wipLane) {
deps.userCanceledTaskIds.delete(task.id);
if (deps.recoveringCompleted.has(task.id)) {
@@ -354,7 +364,7 @@ export function wireExecutorLifecycle(deps: WireExecutorLifecycleDeps): WireExec
}
}),
);
} else if (deps.isBackwardMoveOutOfPlanning(task.id, from, to, lanes)) {
} else if (deps.isBackwardMoveOutOfPlanning(task.id, from, to, effectiveLanes)) {
/*
FNXC:PlanningEvacuation 2026-07-25-23:00:
A card pulled BACKWARD out of a planner lane (the reported case: todo → Ideas) must stop all