Commit Graph

2 Commits

Author SHA1 Message Date
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
gsxdsm
a460a9bbc0 fix(plugins,dashboard): the dependency graph drew every card with the LEGACY lane vocabulary (#3029)
## The third producer of unflagged cards — the one a host-side fix could
not reach

#3025 fixed the two producers that go through `renderTaskCard`.
`GraphTaskNode` is a third: it imports `TaskCard` **directly** through
the plugin's interop shim, so that fix bypassed it and every role helper
inside a graph card kept reading the legacy ids.

The same component also called the stuck predicate without its flags:

```ts
const isStuck = isTaskStuck(task, taskStuckTimeoutMs, lastFetchTimeMs);   // no columnFlags
```

so `isWipColumnRole` fell back to the literal and **no card in the graph
could ever be stuck on a renamed board**. Because `isStuck` gates
`isActive`, a wedged card rendered with the **active** styling — the
graph reported *"running"* about a task that had not moved in hours,
while the main board showed the same card as stuck.

That asymmetry between two views of one task is the defect, and it is
what the new test pins.

## One cause, so one fix

Both symptoms came from the same gap: `PluginDashboardViewContext`
exposed `tasks` and nothing about the board's vocabulary. It now carries
`columnFlagsByTaskId` — the same per-task map `renderTaskCard` already
uses, **two lines away in the same object literal**.

## I filed this twice as blocked on a public-API change. It was not.

```
packages/dashboard                        @fusion/dashboard                        private: true
packages/plugin-sdk                       @fusion/plugin-sdk                       private: true
plugins/fusion-plugin-dependency-graph    @fusion-plugin-examples/dependency-graph private: true
```

No published surface anywhere in the path — three in-repo private
packages and a hand-written `.d.ts`. **#3026 landed the general form of
that mistake while I was still making it**: a deferral's stated blocker
is a claim, and mine decayed unchecked until I finally measured it.

## Two type decisions worth reviewing

- **`Partial<TraitFlags>`** in the plugin-facing type, not the
dashboard's `ExecutorColumnFlags` — that module's own header restricts
it to `@fusion/core` and `react` imports so external plugin builds can
consume it. Same runtime object either way.
- **`MainContentProps.columnFlagsByTaskId` widened** from `{complete,
archived, intake, hold}` to the flags the map really carries. It is
built from `workflow.columns.find(...).flags`, so the four-flag
declaration was a narrower view than the value — and `countsTowardWip`,
which every wip predicate needs, was invisible through it. That narrow
type is why threading this looked impossible at first.

Absent still means legacy, matching how the host treats remote rows and
off-board columns: the degraded answer is the documented literal, never
*"this board has no wip lane"*.

## Revert proof

Dropping the 4th argument:

```
AssertionError: expected 'graph-task-node graph-task-node--acti…' not to contain 'graph-task-node--active'
      Tests  1 failed | 26 passed (27)
```

The paired case (a fresh legacy `in-progress` card still reads active)
passes both ways by design — it guards against over-detection, so I am
not counting it as coverage.

The gate agrees independently:
`plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx: 1 -> 0`,
baseline re-recorded 16 → 15 in the same commit.

## Verification (measured)

- plugin suite — **185 passed / 20 files**
- dashboard `dashboard/` + `plugins/` suites — **48 passed / 6 files**
- `tsc --noEmit` clean in both packages; `pnpm lint` clean
- `lifecycle-column-census --strict`, `check-lane-wiring` (15, none
added), `check-sql-column-literals`, `check-inert-flag-seams`,
`check-fnxc-future-dates` — green

No changeset: all three packages are `private: true`.

---------

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