chore(plugins): delete the superseded fusion-plugin-even-cards (#2790) (#2988)

Closes #2790 by finishing a decision that was already made and written
down.

## The issue's premise was wrong, including as I filed it

I raised this as "a package accidentally missing from
`pnpm-workspace.yaml`." It wasn't. `CHANGELOG-archive.md:9596`:

> Consolidate Even Realities plugin support into
`fusion-plugin-even-realities-glasses` and **remove
`fusion-plugin-even-cards` from the active workspace package list to
avoid duplicate user-facing integrations.**

The removal was deliberate, for a stated reason. The directory is what
got left behind. That also rules out the option I had been weighting
first — adding it back would undo a shipped consolidation and re-create
the duplicate integration it was removed to prevent.

## Unreachable by every path

| check | result |
|---|---|
| `pnpm-workspace.yaml` globs | no — never installed or built |
| CLI bundle list (`packages/cli/tsup.config.ts`) | no — 0 mentions,
while seven other plugins are named |
| runtime `plugins/*` directory-scan discovery | none exists — plugins
are enumerated explicitly |
| `package.json` | `private: true` — never published |
| imports outside its own directory | none |
| kept as a docs/authoring example | no — zero references in `docs/` or
any root `*.md` |
| successor in the workspace | yes —
`fusion-plugin-even-realities-glasses` |

## It was also polluting two ratchets

Dead code in a scanned tree is worse than dead code: both censuses are
**source-text scanners**, so they counted debt in files no build or
typecheck covers. Nobody could retire those entries through a
normally-verified refactor, and they inflated how much of the remaining
debt looked actionable.

Both baselines regenerated, and I checked each diff rather than trusting
the totals:

| baseline | change |
|---|---|
| `lane-wiring` | 26 → 23 sites, 21 → 20 files — **one entry removed**,
`board-routes.ts: 3` |
| `lifecycle-column-census` | exactly its two `board-cards.ts` entries |

Neither regeneration tightened anything unrelated — worth confirming
explicitly, because `lifecycle-column-census.mjs --strict` **writes**
its baseline as a side effect and could have folded an unrelated drop
into this commit.

**Verified:** lane-wiring, SQL-literal and FNXC gates all exit 0 after
the deletion; lint clean. 15 files removed.

## Why I went ahead

I said twice I would not delete this unilaterally. What changed is that
the trade-off dissolved — once the consolidation decision turned out to
be documented and the "is it a teaching example?" question answered by a
docs grep, there was nothing left to decide, only to execute. The
deletion is git-reversible and the standing guidance is that reversible
calls are mine to make.

Fourth time today a thing I filed as "needs someone else's judgement"
turned out to have its answer already in the repository. Cheap habit
worth keeping: before deferring, grep for whether the judgement has
already been made.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-30 23:35:25 -07:00
committed by GitHub
parent 684c324084
commit cfe47b3754
17 changed files with 0 additions and 654 deletions

View File

@@ -1,19 +0,0 @@
# @fusion-plugin-examples/even-cards
## 0.1.3
### Patch Changes
- @fusion/plugin-sdk@0.26.0
## 0.1.2
### Patch Changes
- @fusion/plugin-sdk@0.25.0
## 0.1.1
### Patch Changes
- @fusion/plugin-sdk@0.24.0

View File

@@ -1,17 +0,0 @@
# Even Cards Plugin (Deprecated)
`fusion-plugin-even-cards` is deprecated and superseded by the canonical plugin:
- `fusion-plugin-even-realities-glasses`
Board/task card APIs previously provided here (`/board/cards`, `/board`, `/tasks/:id/cards`) now live under the canonical plugin id and route namespace.
## Migration
Use:
- `/api/plugins/fusion-plugin-even-realities-glasses/board/cards`
- `/api/plugins/fusion-plugin-even-realities-glasses/board`
- `/api/plugins/fusion-plugin-even-realities-glasses/tasks/:id/cards`
This package is removed from the active workspace package list and retained only as a temporary compatibility artifact pending full cleanup.

View File

@@ -1,26 +0,0 @@
{
"name": "@fusion-plugin-examples/even-cards",
"version": "0.1.3",
"type": "module",
"description": "Even Realities on-device cards flow for Fusion board/task status",
"private": true,
"exports": {
".": {
"types": "./src/index.ts",
"source": "./src/index.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"test": "vitest run --silent=passed-only --reporter=dot"
},
"dependencies": {
"@fusion/plugin-sdk": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.5.2",
"typescript": "^5.7.0",
"vitest": "^4.1.0"
}
}

View File

@@ -1,26 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { requireApiKey } from "../routes/auth.js";
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
describe("requireApiKey", () => {
it("returns 503 when api key setting is missing", () => {
const result = requireApiKey({ settings: {}, logger } as any, { headers: {} });
expect(result).toEqual({ ok: false, response: { status: 503, body: { error: "plugin not configured" } } });
});
it("returns 401 when header missing", () => {
const result = requireApiKey({ settings: { apiKey: "secret" }, logger } as any, { headers: {} });
expect(result).toEqual({ ok: false, response: { status: 401, body: { error: "unauthorized" } } });
});
it("returns 401 when key does not match", () => {
const result = requireApiKey({ settings: { apiKey: "secret" }, logger } as any, { headers: { authorization: "Bearer nope" } });
expect(result).toEqual({ ok: false, response: { status: 401, body: { error: "unauthorized" } } });
});
it("returns ok on matching key", () => {
const result = requireApiKey({ settings: { apiKey: "secret" }, logger } as any, { headers: { authorization: "Bearer secret" } });
expect(result).toEqual({ ok: true });
});
});

View File

@@ -1,111 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import plugin from "../index.js";
import type { FusionTask } from "../cards/types.js";
function makeTask(id: string, column: FusionTask["column"], updatedAt: string): FusionTask {
return {
id,
title: id,
description: id,
column,
status: "pending",
priority: "normal",
createdAt: "2026-05-08T10:00:00.000Z",
updatedAt,
currentStep: 0,
steps: [],
dependencies: [],
} as FusionTask;
}
function createContext(tasks: FusionTask[], apiKey = "secret") {
return {
settings: { apiKey },
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
taskStore: {
listTasks: vi.fn(async () => tasks),
getTask: vi.fn(async (id: string) => tasks.find((task) => task.id === id)),
},
} as any;
}
function route(path: string) {
return plugin.routes!.find((entry) => entry.path === path)!;
}
describe("even cards routes", () => {
it("returns 401 when auth header missing", async () => {
const response = (await route("/board/cards").handler({ headers: {} }, createContext([]))) as any;
expect(response.status).toBe(401);
});
it("returns 503 when plugin is not configured", async () => {
const response = (await route("/board/cards").handler({ headers: { authorization: "Bearer secret" } }, createContext([], ""))) as any;
expect(response.status).toBe(503);
});
it("returns deck on happy path", async () => {
const tasks = [makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z"), makeTask("FN-2", "in-progress", "2026-05-08T12:00:00.000Z")];
const response = (await route("/board/cards").handler({ headers: { authorization: "Bearer secret" } }, createContext(tasks))) as any;
expect(response.status).toBe(200);
expect(response.body.deck.cards[0].id).toBe("summary");
expect(response.body.deck.cards).toHaveLength(3);
});
it("filters columns in memory", async () => {
const tasks = [makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z"), makeTask("FN-2", "triage", "2026-05-08T12:00:00.000Z")];
const response = (await route("/board/cards").handler(
{ headers: { authorization: "Bearer secret" }, query: { columns: "triage" } },
createContext(tasks),
)) as any;
expect(response.body.deck.summary.counts.triage).toBe(1);
expect(response.body.deck.summary.counts.todo).toBe(0);
});
it("clamps max bounds", async () => {
const tasks = Array.from({ length: 30 }, (_, idx) => makeTask(`FN-${idx + 1}`, "todo", `2026-05-08T12:${String(idx).padStart(2, "0")}:00.000Z`));
const low = (await route("/board/cards").handler(
{ headers: { authorization: "Bearer secret" }, query: { max: "0" } },
createContext(tasks),
)) as any;
const high = (await route("/board/cards").handler(
{ headers: { authorization: "Bearer secret" }, query: { max: "99" } },
createContext(tasks),
)) as any;
expect(low.body.deck.cards).toHaveLength(1);
expect(high.body.deck.cards).toHaveLength(20);
});
it("returns board summary", async () => {
const tasks = [makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z")];
const response = (await route("/board").handler({ headers: { authorization: "Bearer secret" } }, createContext(tasks))) as any;
expect(response.status).toBe(200);
expect(response.body.summary.counts.todo).toBe(1);
});
it("returns task deck for known id", async () => {
const tasks = [makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z")];
const response = (await route("/tasks/:id/cards").handler(
{ headers: { authorization: "Bearer secret" }, params: { id: "FN-1" } },
createContext(tasks),
)) as any;
expect(response.status).toBe(200);
expect(response.body.deck.cards[0].id).toBe("FN-1");
});
it("returns 404 for unknown task id", async () => {
const response = (await route("/tasks/:id/cards").handler(
{ headers: { authorization: "Bearer secret" }, params: { id: "FN-404" } },
createContext([]),
)) as any;
expect(response.status).toBe(404);
});
it("excludes archived tasks from deck cards", async () => {
const tasks = [makeTask("FN-1", "archived", "2026-05-08T12:00:00.000Z"), makeTask("FN-2", "todo", "2026-05-08T11:00:00.000Z")];
const response = (await route("/board/cards").handler({ headers: { authorization: "Bearer secret" } }, createContext(tasks))) as any;
const ids = response.body.deck.cards.map((card: any) => card.id);
expect(ids).toEqual(["summary", "FN-2"]);
});
});

View File

@@ -1,75 +0,0 @@
import { describe, expect, it } from "vitest";
import { boardToDeck } from "../cards/board-cards.js";
import { formatRelativeAge, formatTaskId, statusBadge, truncateLine, wrapLines } from "../cards/format.js";
import { taskToCard } from "../cards/task-cards.js";
import type { FusionTask } from "../cards/types.js";
function makeTask(id: string, column: FusionTask["column"], updatedAt: string): FusionTask {
return {
id,
description: `Task ${id}`,
title: `Title ${id}`,
column,
status: "pending",
priority: "normal",
currentStep: 0,
steps: [],
dependencies: [],
createdAt: "2026-05-08T10:00:00.000Z",
updatedAt,
} as FusionTask;
}
describe("cards format", () => {
it("truncates at boundary", () => {
expect(truncateLine("12345", 5)).toBe("12345");
expect(truncateLine("123456", 5)).toBe("1234…");
});
it("wraps across lines", () => {
expect(wrapLines("one two three four", 7, 3)).toEqual(["one two", "three", "four"]);
});
it("formats task id and relative age", () => {
expect(formatTaskId("fn-42")).toBe("FN-42");
expect(formatRelativeAge("2026-05-08T11:58:00.000Z", "2026-05-08T12:00:00.000Z")).toBe("2m");
});
it("maps all task columns to badges", () => {
expect(statusBadge("triage").tone).toBe("triage");
expect(statusBadge("todo").tone).toBe("todo");
expect(statusBadge("in-progress").tone).toBe("in-progress");
expect(statusBadge("in-review").tone).toBe("in-review");
expect(statusBadge("done").tone).toBe("done");
expect(statusBadge("archived").tone).toBe("neutral");
});
});
describe("board deck", () => {
it("summarizes counts and handles empty board", () => {
const deck = boardToDeck([], { now: "2026-05-08T12:00:00.000Z" });
expect(deck.cards).toHaveLength(1);
expect(deck.summary.counts.todo).toBe(0);
});
it("creates a task card", () => {
const card = taskToCard(makeTask("FN-7", "in-review", "2026-05-08T12:00:00.000Z"), { now: "2026-05-08T12:10:00.000Z" });
expect(card.id).toBe("FN-7");
expect(card.badge.tone).toBe("in-review");
expect(card.lines.join(" ")).toContain("Assignee unassigned");
});
it("caps deck and sorts deterministically", () => {
const tasks: FusionTask[] = [
makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z"),
makeTask("FN-9", "in-progress", "2026-05-08T11:00:00.000Z"),
makeTask("FN-2", "done", "2026-05-08T12:00:00.000Z"),
makeTask("FN-3", "archived", "2026-05-08T12:00:00.000Z"),
];
const deck = boardToDeck(tasks, { maxCards: 3, now: "2026-05-08T12:00:00.000Z" });
expect(deck.cards).toHaveLength(3);
expect(deck.cards[1].id).toBe("FN-9");
expect(deck.cards[2].id).toBe("FN-1");
});
});

View File

@@ -1,59 +0,0 @@
import { DEFAULT_MAX_CARDS_PER_DECK, DEFAULT_MAX_CHARS_PER_LINE, DEFAULT_MAX_LINES_PER_CARD, statusBadge, truncateLine, wrapLines } from "./format.js";
import { taskToCard } from "./task-cards.js";
import type { BoardSummary, CardDeck, FusionColumn, FusionTask, GlassesCard } from "./types.js";
/*
FNXC:WorkflowResolvedColumns 2026-07-30-21:10 DELIBERATE-LITERAL: no resolution source reaches this package.
This plugin depends on `@fusion/plugin-sdk` ONLY (see its package.json) — not `@fusion/core` — and the
SDK does not re-export the lifecycle role helpers. There is no IR, no store, and no trait flags on
`FusionTask` here, so there is nothing to resolve FROM: converting is possible only at the caller or by
giving the plugin a new dependency, both structural changes and out of scope for the conversion.
`COLUMN_ORDER` is a DISPLAY ordering for the glasses deck and degrades honestly on a renamed board — an
unknown column is simply absent from the summary line. The `!== "archived" && !== "done"` filter in
`boardToDeck` is the one that misreads such a board: a finished card is neither, so it shows as active.
That is a cosmetic over-report on a secondary surface, not a lifecycle decision. The real fix is the SDK
exposing role flags on the task shape it hands plugins — recorded here as the correct home rather than
guessed at.
*/
const COLUMN_ORDER: FusionColumn[] = ["triage", "todo", "in-progress", "in-review", "done", "archived"];
function boardSummary(tasks: FusionTask[]): BoardSummary {
const counts = Object.fromEntries(COLUMN_ORDER.map((column) => [column, 0]));
let updatedAt: string | null = null;
for (const task of tasks) {
counts[task.column] = (counts[task.column] ?? 0) + 1;
if (!updatedAt || task.updatedAt > updatedAt) updatedAt = task.updatedAt;
}
return { counts, updatedAt };
}
function summaryCard(summary: BoardSummary, now: string, maxCharsPerLine: number, maxLines: number): GlassesCard {
const summaryText = `Triage ${summary.counts.triage} Todo ${summary.counts.todo} Doing ${summary.counts["in-progress"]} Review ${summary.counts["in-review"]} Done ${summary.counts.done}`;
return {
id: "summary",
kind: "summary",
title: truncateLine("Fusion board", maxCharsPerLine),
lines: wrapLines(summaryText, maxCharsPerLine, maxLines),
badge: statusBadge("todo"),
updatedAt: summary.updatedAt ?? now,
};
}
/* FNXC:WorkflowResolvedColumns 2026-07-30-21:10 DELIBERATE-LITERAL: no resolution source in this package — see the note above `COLUMN_ORDER`. */
export function boardToDeck(tasks: FusionTask[], opts?: { maxCharsPerLine?: number; maxLines?: number; maxCards?: number; now?: string }): CardDeck {
const maxCharsPerLine = opts?.maxCharsPerLine ?? DEFAULT_MAX_CHARS_PER_LINE;
const maxLines = opts?.maxLines ?? DEFAULT_MAX_LINES_PER_CARD;
const maxCards = opts?.maxCards ?? DEFAULT_MAX_CARDS_PER_DECK;
const now = opts?.now ?? new Date().toISOString();
const summary = boardSummary(tasks);
const active = tasks
.filter((task) => task.column !== "archived" && task.column !== "done")
.sort((a, b) => (b.updatedAt === a.updatedAt ? b.id.localeCompare(a.id) : b.updatedAt.localeCompare(a.updatedAt)))
.slice(0, Math.max(0, maxCards - 1));
const cards: GlassesCard[] = [summaryCard(summary, now, maxCharsPerLine, maxLines), ...active.map((task) => taskToCard(task, { maxCharsPerLine, maxLines, now }))];
return { cards, summary };
}

View File

@@ -1,70 +0,0 @@
import type { CardStatusBadge, FusionColumn } from "./types.js";
export const DEFAULT_MAX_CHARS_PER_LINE = 24;
export const DEFAULT_MAX_LINES_PER_CARD = 4;
export const DEFAULT_MAX_CARDS_PER_DECK = 8;
export function truncateLine(input: string, max: number): string {
const value = input.trim();
if (value.length <= max) return value;
if (max <= 1) return "…";
return `${value.slice(0, max - 1)}…`;
}
export function wrapLines(text: string, max: number, maxLines: number): string[] {
const words = text.trim().split(/\s+/).filter(Boolean);
if (words.length === 0) return [];
const lines: string[] = [];
let current = "";
for (const word of words) {
const candidate = current ? `${current} ${word}` : word;
if (candidate.length <= max) {
current = candidate;
continue;
}
if (current) {
lines.push(current);
if (lines.length === maxLines) return lines.map((line, idx) => (idx === maxLines - 1 ? truncateLine(line, max) : line));
}
current = word.length <= max ? word : truncateLine(word, max);
}
if (current && lines.length < maxLines) {
lines.push(current);
}
return lines.slice(0, maxLines).map((line, idx, arr) => (idx === arr.length - 1 ? truncateLine(line, max) : line));
}
export function statusBadge(column: FusionColumn): CardStatusBadge {
switch (column) {
case "triage":
case "todo":
case "in-progress":
case "in-review":
case "done":
return { label: column, tone: column };
case "archived":
default:
return { label: column, tone: "neutral" };
}
}
export function formatTaskId(id: string): string {
return id.trim().toUpperCase();
}
export function formatRelativeAge(createdAtIso: string, nowIso: string): string {
const created = Date.parse(createdAtIso);
const now = Date.parse(nowIso);
if (!Number.isFinite(created) || !Number.isFinite(now) || now <= created) return "0m";
const diffMs = now - created;
const minutes = Math.floor(diffMs / 60000);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
const days = Math.floor(hours / 24);
return `${days}d`;
}

View File

@@ -1,20 +0,0 @@
import { DEFAULT_MAX_CHARS_PER_LINE, DEFAULT_MAX_LINES_PER_CARD, formatRelativeAge, formatTaskId, statusBadge, truncateLine, wrapLines } from "./format.js";
import type { FusionTask, GlassesCard } from "./types.js";
export function taskToCard(task: FusionTask, opts?: { maxCharsPerLine?: number; maxLines?: number; now?: string }): GlassesCard {
const maxCharsPerLine = opts?.maxCharsPerLine ?? DEFAULT_MAX_CHARS_PER_LINE;
const maxLines = opts?.maxLines ?? DEFAULT_MAX_LINES_PER_CARD;
const now = opts?.now ?? new Date().toISOString();
const assignee = task.assignedAgentId ?? task.assigneeUserId ?? "unassigned";
const body = [`Priority ${task.priority ?? "normal"}`, `Assignee ${assignee}`, `Age ${formatRelativeAge(task.createdAt, now)}`].join(" ");
return {
id: task.id,
taskId: task.id,
kind: "task",
title: truncateLine(formatTaskId(task.id) + " " + (task.title?.trim() || task.description), maxCharsPerLine),
lines: wrapLines(body, maxCharsPerLine, maxLines),
badge: statusBadge(task.column),
updatedAt: task.updatedAt,
};
}

View File

@@ -1,40 +0,0 @@
export type FusionColumn = "triage" | "todo" | "in-progress" | "in-review" | "done" | "archived";
export interface FusionTask {
id: string;
title?: string;
description: string;
column: FusionColumn;
priority?: string;
assignedAgentId?: string;
assigneeUserId?: string;
createdAt: string;
updatedAt: string;
}
export type CardTone = "triage" | "todo" | "in-progress" | "in-review" | "done" | "neutral";
export interface CardStatusBadge {
label: string;
tone: CardTone;
}
export interface GlassesCard {
id: string;
kind: "summary" | "task";
title: string;
lines: string[];
badge: CardStatusBadge;
taskId?: string;
updatedAt: string;
}
export interface BoardSummary {
counts: Record<string, number>;
updatedAt: string | null;
}
export interface CardDeck {
cards: GlassesCard[];
summary: BoardSummary;
}

View File

@@ -1,39 +0,0 @@
import { definePlugin } from "@fusion/plugin-sdk";
import type { FusionPlugin, PluginSettingSchema } from "@fusion/plugin-sdk";
import { boardRoutes } from "./routes/board-routes.js";
const settingsSchema: Record<string, PluginSettingSchema> = {
apiKey: {
type: "password",
label: "Companion API Key",
description: "Bearer token used by the glasses companion app.",
required: true,
group: "Authentication",
},
boardPollingMs: {
type: "number",
label: "Board Polling Interval (ms)",
defaultValue: 10000,
group: "On-device Cards",
},
};
const plugin: FusionPlugin = definePlugin({
manifest: {
id: "fusion-plugin-even-cards",
name: "Even Cards",
version: "0.1.0",
description: "On-device card payloads for board/task status in Even Realities flows",
author: "Fusion Team",
settingsSchema,
},
state: "installed",
routes: [...boardRoutes],
hooks: {
onLoad: async (ctx) => {
ctx.logger.info("Even Cards plugin loaded");
},
},
});
export default plugin;

View File

@@ -1,39 +0,0 @@
import { createHash, timingSafeEqual } from "node:crypto";
import type { PluginContext, PluginRouteResponse } from "@fusion/plugin-sdk";
function toDigest(value: string): Buffer {
return createHash("sha256").update(value).digest();
}
function readBearer(headers: Record<string, string | string[] | undefined>): string | undefined {
const auth = headers.authorization ?? headers.Authorization;
const header = Array.isArray(auth) ? auth[0] : auth;
if (!header) return undefined;
const match = header.match(/^Bearer\s+(.+)$/i);
return match?.[1]?.trim() || undefined;
}
export function requireApiKey(
ctx: PluginContext,
req: { headers: Record<string, string | string[] | undefined> },
): { ok: true } | { ok: false; response: PluginRouteResponse } {
const expected = typeof ctx.settings.apiKey === "string" ? ctx.settings.apiKey.trim() : "";
if (!expected) {
return { ok: false, response: { status: 503, body: { error: "plugin not configured" } } };
}
const provided = readBearer(req.headers ?? {});
if (!provided) {
return { ok: false, response: { status: 401, body: { error: "unauthorized" } } };
}
const expectedDigest = toDigest(expected);
const providedDigest = toDigest(provided);
const valid = timingSafeEqual(expectedDigest, providedDigest);
if (!valid) {
return { ok: false, response: { status: 401, body: { error: "unauthorized" } } };
}
return { ok: true };
}

View File

@@ -1,79 +0,0 @@
import type { PluginContext, PluginRouteDefinition, PluginRouteResponse } from "@fusion/plugin-sdk";
import { boardToDeck } from "../cards/board-cards.js";
import { DEFAULT_MAX_CARDS_PER_DECK, DEFAULT_MAX_CHARS_PER_LINE, DEFAULT_MAX_LINES_PER_CARD } from "../cards/format.js";
import { taskToCard } from "../cards/task-cards.js";
import type { CardDeck, FusionColumn, FusionTask } from "../cards/types.js";
import { requireApiKey } from "./auth.js";
const ALLOWED_COLUMNS: FusionColumn[] = ["triage", "todo", "in-progress", "in-review", "done", "archived"];
function parseColumns(raw: unknown): Set<FusionColumn> | null {
if (typeof raw !== "string" || !raw.trim()) return null;
const parsed = raw
.split(",")
.map((value) => value.trim())
.filter((value): value is FusionColumn => ALLOWED_COLUMNS.includes(value as FusionColumn));
return parsed.length ? new Set(parsed) : null;
}
function parseMax(raw: unknown): number {
const value = typeof raw === "string" ? Number.parseInt(raw, 10) : Number.NaN;
if (!Number.isFinite(value)) return DEFAULT_MAX_CARDS_PER_DECK;
return Math.max(1, Math.min(20, Math.floor(value)));
}
function requestData(req: unknown): { headers: Record<string, string | string[] | undefined>; query: Record<string, unknown>; params: Record<string, unknown> } {
const candidate = (req ?? {}) as { headers?: Record<string, string | string[] | undefined>; query?: Record<string, unknown>; params?: Record<string, unknown> };
return { headers: candidate.headers ?? {}, query: candidate.query ?? {}, params: candidate.params ?? {} };
}
async function getBoardCards(req: unknown, ctx: PluginContext): Promise<PluginRouteResponse> {
const request = requestData(req);
const auth = requireApiKey(ctx, { headers: request.headers });
if (!auth.ok) return auth.response;
const all = ((await ctx.taskStore.listTasks({ includeArchived: false })) as FusionTask[]) ?? [];
const columns = parseColumns(request.query.columns);
const filtered = columns ? all.filter((task) => columns.has(task.column)) : all;
const maxCards = parseMax(request.query.max);
const deck = boardToDeck(filtered, { maxCharsPerLine: DEFAULT_MAX_CHARS_PER_LINE, maxLines: DEFAULT_MAX_LINES_PER_CARD, maxCards });
return { status: 200, body: { deck, generatedAt: new Date().toISOString() } };
}
async function getBoardSummary(req: unknown, ctx: PluginContext): Promise<PluginRouteResponse> {
const request = requestData(req);
const auth = requireApiKey(ctx, { headers: request.headers });
if (!auth.ok) return auth.response;
const all = ((await ctx.taskStore.listTasks({ includeArchived: false })) as FusionTask[]) ?? [];
const columns = parseColumns(request.query.columns);
const filtered = columns ? all.filter((task) => columns.has(task.column)) : all;
const deck = boardToDeck(filtered, { maxCharsPerLine: DEFAULT_MAX_CHARS_PER_LINE, maxLines: DEFAULT_MAX_LINES_PER_CARD, maxCards: 1 });
return { status: 200, body: { summary: deck.summary, updatedAt: deck.summary.updatedAt } };
}
async function getTaskCards(req: unknown, ctx: PluginContext): Promise<PluginRouteResponse> {
const request = requestData(req);
const auth = requireApiKey(ctx, { headers: request.headers });
if (!auth.ok) return auth.response;
const taskId = typeof request.params.id === "string" ? request.params.id.trim() : "";
if (!taskId) return { status: 400, body: { error: "task id is required" } };
const task = (await ctx.taskStore.getTask(taskId)) as FusionTask | undefined;
if (!task) return { status: 404, body: { error: "task not found" } };
const deck: CardDeck = {
cards: [taskToCard(task, { maxCharsPerLine: DEFAULT_MAX_CHARS_PER_LINE, maxLines: DEFAULT_MAX_LINES_PER_CARD })],
summary: boardToDeck([task], { maxCards: 1 }).summary,
};
return { status: 200, body: { deck, generatedAt: new Date().toISOString() } };
}
export const boardRoutes: PluginRouteDefinition[] = [
{ method: "GET", path: "/board/cards", handler: getBoardCards, description: "Read-only board card deck" },
{ method: "GET", path: "/board", handler: getBoardSummary, description: "Read-only board summary" },
{ method: "GET", path: "/tasks/:id/cards", handler: getTaskCards, description: "Read-only single task card deck" },
];

View File

@@ -1,8 +0,0 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"]
}

View File

@@ -1,23 +0,0 @@
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
import { computeMaxWorkers } from "../../packages/core/src/__test-utils__/vitest-workers";
const maxWorkers = computeMaxWorkers();
export default defineConfig({
resolve: {
alias: {
"@fusion/core": fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)),
"@fusion/plugin-sdk": fileURLToPath(new URL("../../packages/plugin-sdk/src/index.ts", import.meta.url)),
},
},
test: {
environment: "node",
include: ["src/__tests__/**/*.test.ts"],
setupFiles: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-setup.ts", import.meta.url))],
globalSetup: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-teardown.ts", import.meta.url))],
pool: "threads",
maxWorkers,
minWorkers: 1,
},
});

View File

@@ -19,7 +19,6 @@
"packages/cli/src/commands/dashboard-tui/bucket-mapping.ts": 1,
"packages/cli/src/extension.ts": 1,
"plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx": 1,
"plugins/fusion-plugin-even-cards/src/routes/board-routes.ts": 3,
"plugins/fusion-plugin-even-realities-glasses/src/routes/board-routes.ts": 1
}
}

View File

@@ -127,8 +127,6 @@
"packages/engine/src/scheduler.ts\u0000todo": 1,
"packages/engine/src/self-healing.ts\u0000archived": 1,
"packages/engine/src/triage.ts\u0000triage": 1,
"plugins/fusion-plugin-even-cards/src/cards/board-cards.ts\u0000archived": 1,
"plugins/fusion-plugin-even-cards/src/cards/board-cards.ts\u0000done": 1,
"plugins/fusion-plugin-even-realities-glasses/src/notifications/diff.ts\u0000done": 1,
"plugins/fusion-plugin-reports/src/store/report-types.ts\u0000archived": 1
},