Files
fusion/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts
gsxdsm ccf562f178 gate: compare mirrored INTERFACES too, and delete the dead prop that found (#3034)
> **Re-landing the second half of #3031.** That PR merged into #3029's
branch and only its first commit reached `main` — the arity rule
shipped, the interface rule and its finding did not. Verified on `main`:
the gate reports *"7 mirrored function(s)"* with no interface count, and
the dead prop below is still there.

## What

The arity rule covers exported functions. The same files also mirror
**interfaces**, which is the larger surface — six copies of
`PluginDashboardViewContext` alone.

**One direction only.** A mirror may declare *fewer* properties, and all
six do (6, 8, 7, 7, 3, 6 against the real nine) because a plugin mirrors
the fields it uses. Demanding equality would fail every plugin for not
using everything — which is how a check gets ignored and then deleted. A
property the real type **doesn't have** is the drift that matters: a
rename nobody propagated, where the plugin keeps compiling and reads a
field the host never sends.

## Its first interface run found a live one

```
dashboard-interop.d.ts:67  TaskCardProps.workflowStepNameLookup is not a property of the real TaskCardProps
```

Git history says it **was** one when FN-2466 and FN-7039 added this
threading. The dashboard removed it later; nothing propagated that to
the plugin's hand-written declaration. So the plugin built a lookup map
from `context.workflowSteps` on every render, threaded it through two
components, and handed it to a `TaskCard` with no such prop.

Deleted rather than exempted — a new gate shouldn't ship with a waiver
for its own first finding. Behaviour-preserving: the value never reached
anything.

## Measured on `main`

| check | result |
|---|---|
| population | **7 functions + 10 interfaces across 6 plugins**, all
matching after the deletion |
| control probe | phantom property **caught**; clean tree exits 0 |
| anti-vacuity | now also requires a non-zero *interface* comparison |
| gate's own suite | **5 → 8** |
| dependency-graph suite | 179 green; `tsc` clean |
| other five gates · census | green |

## Running total for this check

Three real drifts, none of which any other instrument reported:

1. `isTaskStuck` stuck at three parameters through the whole lane
conversion (#3003)
2. `taskStuckTimeoutMs?: number` vs the required `number | undefined` —
in **two independent authors'** declarations
3. `workflowStepNameLookup` outliving its removal from `TaskCard`

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 01:43:58 -07:00

78 lines
3.6 KiB
TypeScript

declare module "@fusion/dashboard/app/utils/taskStuck" {
import type { Task, TraitFlags } from "@fusion/core";
/* FNXC:WorkflowLifecycleColumns 2026-07-31-15:30: the 4th parameter existed upstream and this shim
did not declare it, so the plugin could not pass resolved traits even once it had them — and
`isWipColumnRole` fell back to the literal, meaning NO card in the graph was ever shown stuck on a
renamed board while the same card showed stuck correctly on the main board. */
export function isTaskStuck(
task: Task,
/* FNXC:PluginInteropDrift 2026-07-31-07:50: positionally REQUIRED in the real signature
(`number | undefined`), not optional — a mirror that is merely approximate is the drift this
file already caused once. Found by check-plugin-interop-drift. */
taskStuckTimeoutMs: number | undefined,
lastFetchTimeMs?: number,
columnFlags?: Partial<TraitFlags>,
): boolean;
}
declare module "@fusion/dashboard/app/plugins/types" {
import type { ReactNode } from "react";
import type { Task, TaskDetail, TraitFlags, WorkflowStep } from "@fusion/core";
export type DetailTaskTab = "definition" | "logs" | "changes" | "comments" | "model" | "workflow" | "pr" | "retries";
export type PluginToastType = "success" | "error" | "warning" | "info";
export interface PluginDashboardViewContext {
projectId?: string;
tasks: Task[];
workflowSteps: WorkflowStep[];
openTaskDetail: (task: Task | TaskDetail, initialTab?: DetailTaskTab) => void;
/* FNXC:WorkflowLifecycleColumns 2026-07-31-15:30: mirrors the host's `PluginDashboardViewContext`. */
columnFlagsByTaskId?: ReadonlyMap<string, Partial<TraitFlags>>;
renderTaskCard?: (task: Task | TaskDetail) => ReactNode;
addToast?: (message: string, type?: PluginToastType) => void;
}
export type PluginTaskView = `plugin:${string}:${string}`;
}
declare module "@fusion/dashboard/app/components/TaskCard" {
import type { Column, Task, TaskDetail, TraitFlags } from "@fusion/core";
import type { ReactElement } from "react";
interface TaskCardProps {
task: Task;
projectId?: string;
onOpenDetail: (task: Task | TaskDetail) => void;
addToast: (message: string, type?: "success" | "error" | "info" | "warning") => void;
globalPaused?: boolean;
onUpdateTask?: (
id: string,
updates: { title?: string; description?: string; dependencies?: string[] }
) => Promise<Task>;
onArchiveTask?: (id: string) => Promise<Task>;
onUnarchiveTask?: (id: string) => Promise<Task>;
onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean }) => Promise<Task>;
onRetryTask?: (id: string) => Promise<Task>;
onOpenDetailWithTab?: (task: Task | TaskDetail, initialTab: "changes") => void;
taskStuckTimeoutMs?: number;
onOpenMission?: (missionId: string) => void;
onMoveTask?: (id: string, column: Column, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
lastFetchTimeMs?: number;
/* FNXC:WorkflowLifecycleColumns 2026-07-31-15:30: the prop the host card already accepts; without it
declared here a plugin-drawn card could not be given the board's traits at all. */
taskColumnFlags?: Partial<TraitFlags>;
disableDrag?: boolean;
}
export function TaskCard(props: TaskCardProps): ReactElement;
}
declare module "@fusion/dashboard/app/utils/projectStorage" {
export function getScopedItem(baseKey: string, projectId?: string): string | null;
export function setScopedItem(baseKey: string, value: string, projectId?: string): void;
export function removeScopedItem(baseKey: string, projectId?: string): void;
}