> **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>
78 lines
3.6 KiB
TypeScript
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;
|
|
}
|