From fedb31c362af3ea4c33ce1efcf4c8cee5b10c073 Mon Sep 17 00:00:00 2001 From: Sarath Soman Date: Tue, 26 May 2026 01:01:50 +0100 Subject: [PATCH] chore(ship): disable biome useLiteralKeys + clear lint errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Biome's `useLiteralKeys` rule (auto-enabled via "recommended": true) rewrites `process.env["X"]` → `process.env.X`. This directly conflicts with the project's tsconfig flag `noPropertyAccessFromIndexSignature: true`, which requires bracket notation on index-signature types. The conflict was causing `bun scripts/am-i-done.ts` to block on ~150 mechanical lint findings that can't actually be auto-fixed without breaking tsc. Resolution: - Disable `complexity.useLiteralKeys` in biome.json (incompatible with our tsc strict config — the project's choice on tsc side is the source of truth) - Hand-fix the 6 error-level lint findings biome was reporting: - `server-sse.test.ts:203` — replace `!` non-null assertions with `as string` - `topology-evolve.ts:279` — replace `eslint-disable` with `biome-ignore` for the intentional yield-less async generator - `RealiseDagGraph.tsx:66,73` — wrap `forEach((id, i) => Map.set(...))` in braces (Map.set returns the map; forEach callbacks shouldn't return) - `NowBar.tsx:34` — replace `
` with `
` (per a11y/useSemanticElements); update NowBar.test.tsx to query the new shape - `JsonView.tsx:82` — `// biome-ignore lint/suspicious/noArrayIndexKey` (JSON array order is stable; index is the only stable key) - 5 import-sort auto-fixes applied by `biome check --write` (mechanical) - `runs-root.test.ts:41` — escape literal `${HOME}` in test description so biome doesn't read it as an unintended template literal Unblocks the `am-i-done.ts` biome layer. Two remaining baseline layers (no-throw violations in `capability-cleanup.ts`; tsc prototype-reach via `gallery/entries.ts`) are out of scope here — separate concerns to be tracked. Co-Authored-By: Claude Opus 4.7 (1M context) --- ship/biome.json | 3 ++- ship/src/lib/run-dir.ts | 1 + ship/src/lib/runs-root.test.ts | 2 +- ship/src/server-sse.test.ts | 2 +- ship/src/topology-evolve.ts | 3 +-- ship/src/web/lib/JsonView.tsx | 1 + ship/src/web/lib/RealiseDagGraph.tsx | 8 ++++++-- ship/src/web/lib/StageCard.tsx | 2 +- ship/src/web/pages/Home.tsx | 2 +- ship/src/web/pages/RunDetail.tsx | 2 +- ship/src/web/ui/NowBar.test.tsx | 5 ++--- ship/src/web/ui/NowBar.tsx | 5 ++--- ship/src/web/ui/StatePill.tsx | 2 +- 13 files changed, 21 insertions(+), 17 deletions(-) diff --git a/ship/biome.json b/ship/biome.json index b8ec5f6..0f8f2e8 100644 --- a/ship/biome.json +++ b/ship/biome.json @@ -27,7 +27,8 @@ "rules": { "recommended": true, "complexity": { - "noUselessConstructor": "error" + "noUselessConstructor": "error", + "useLiteralKeys": "off" }, "correctness": { "noUnusedVariables": "error", diff --git a/ship/src/lib/run-dir.ts b/ship/src/lib/run-dir.ts index c463d1a..707bce9 100644 --- a/ship/src/lib/run-dir.ts +++ b/ship/src/lib/run-dir.ts @@ -6,6 +6,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { basename, join } from "node:path"; import { isPidAlive, readStateFile } from "../engine/kernel/state.ts"; import { runDirFor, shipRunsRoot } from "./runs-root.ts"; + // Re-export so downstream consumers can pick the threshold up from the same // surface that ships the field they gate on. export { STUCK_THRESHOLD_SECONDS } from "./stuck-threshold.ts"; diff --git a/ship/src/lib/runs-root.test.ts b/ship/src/lib/runs-root.test.ts index 53a1470..c2983e8 100644 --- a/ship/src/lib/runs-root.test.ts +++ b/ship/src/lib/runs-root.test.ts @@ -38,7 +38,7 @@ afterEach(() => { }); describe("shipRunsRoot — precedence", () => { - it("claim 1: default is ${HOME}/.pramana/ship/runs when no env/config override", () => { + it("claim 1: default is /.pramana/ship/runs when no env/config override", () => { // No PRAMANA_SHIP_RUNS_DIR set, no ~/.pramana/config.json present. expect(shipRunsRoot()).toBe(join(fakeHome, ".pramana", "ship", "runs")); }); diff --git a/ship/src/server-sse.test.ts b/ship/src/server-sse.test.ts index 65bea96..9bc0ee4 100644 --- a/ship/src/server-sse.test.ts +++ b/ship/src/server-sse.test.ts @@ -200,7 +200,7 @@ describe("Claim 2 — connect replays every existing events.jsonl row verbatim", expect(frames.length).toBe(seeded.length); for (let i = 0; i < seeded.length; i++) { expect(frames[i]).toBe(seeded[i]); - expect(rowAndParse(frames[i]!)).toEqual(rowAndParse(seeded[i]!)); + expect(rowAndParse(frames[i] as string)).toEqual(rowAndParse(seeded[i] as string)); } }); }); diff --git a/ship/src/topology-evolve.ts b/ship/src/topology-evolve.ts index a2eec3b..6ccd280 100644 --- a/ship/src/topology-evolve.ts +++ b/ship/src/topology-evolve.ts @@ -275,9 +275,8 @@ interface EvolveTopology extends Topology { function makeEvolveTopology(payload: string): EvolveTopology { return { - // eslint-disable-next-line require-yield + // biome-ignore lint/correctness/useYield: single-shot — handler does not use the generator path async *generator(_ctx: EvolveCtx) { - // Single-shot — handler does not use the generator path. void _ctx; return; }, diff --git a/ship/src/web/lib/JsonView.tsx b/ship/src/web/lib/JsonView.tsx index 926e1c3..caab4ee 100644 --- a/ship/src/web/lib/JsonView.tsx +++ b/ship/src/web/lib/JsonView.tsx @@ -79,6 +79,7 @@ function JsonArray({ value }: { value: Json[] }) { {open && (
{value.map((v, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: JSON array order is stable; index is the only key
{i < value.length - 1 ? "," : ""} diff --git a/ship/src/web/lib/RealiseDagGraph.tsx b/ship/src/web/lib/RealiseDagGraph.tsx index 87d42c8..6a81be9 100644 --- a/ship/src/web/lib/RealiseDagGraph.tsx +++ b/ship/src/web/lib/RealiseDagGraph.tsx @@ -63,14 +63,18 @@ interface BuildResult { export function buildGraph(audit: RealiseAudit): BuildResult { const wavePos = new Map(); audit.dagWaves.forEach((wave, waveIdx) => { - wave.forEach((id, rowIdx) => wavePos.set(id, { wave: waveIdx, row: rowIdx })); + wave.forEach((id, rowIdx) => { + wavePos.set(id, { wave: waveIdx, row: rowIdx }); + }); }); // Items present in itemIds but not in any wave fall back to the last wave. // (Cycles drop nodes from order — those still get rendered as orphans.) const fallbackWave = audit.dagWaves.length; const orphans = audit.itemIds.filter((id) => !wavePos.has(id)); - orphans.forEach((id, i) => wavePos.set(id, { wave: fallbackWave, row: i })); + orphans.forEach((id, i) => { + wavePos.set(id, { wave: fallbackWave, row: i }); + }); const nodes: Node[] = audit.itemIds.map((id) => { const pos = wavePos.get(id) ?? { wave: 0, row: 0 }; diff --git a/ship/src/web/lib/StageCard.tsx b/ship/src/web/lib/StageCard.tsx index 7ba5d32..0c39074 100644 --- a/ship/src/web/lib/StageCard.tsx +++ b/ship/src/web/lib/StageCard.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import type { StageEntry, StageTelemetry } from "../types"; -import { ArtifactPreview } from "./ArtifactPreview"; import { StatePill } from "../ui/StatePill"; +import { ArtifactPreview } from "./ArtifactPreview"; function formatDuration(ms?: number): string { if (ms === undefined) return "—"; diff --git a/ship/src/web/pages/Home.tsx b/ship/src/web/pages/Home.tsx index 53d53ca..ac862e2 100644 --- a/ship/src/web/pages/Home.tsx +++ b/ship/src/web/pages/Home.tsx @@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { api, type PostRunOutcome } from "../api"; +import type { RunSummary } from "../types"; import { NowBar, type NowBarRun } from "../ui/NowBar"; import { StatePill } from "../ui/StatePill"; -import type { RunSummary } from "../types"; interface IssueGroup { issue: string; diff --git a/ship/src/web/pages/RunDetail.tsx b/ship/src/web/pages/RunDetail.tsx index 039218e..a01c92a 100644 --- a/ship/src/web/pages/RunDetail.tsx +++ b/ship/src/web/pages/RunDetail.tsx @@ -6,8 +6,8 @@ import { ArtifactPreview } from "../lib/ArtifactPreview"; import { PipelineGraph } from "../lib/PipelineGraph"; import { RealiseDagGraph } from "../lib/RealiseDagGraph"; import { StageCard } from "../lib/StageCard"; -import { StatePill } from "../ui/StatePill"; import { MetaGrid } from "../ui/MetaGrid"; +import { StatePill } from "../ui/StatePill"; function formatElapsed(ms: number): string { const total = Math.max(0, Math.floor(ms / 1000)); diff --git a/ship/src/web/ui/NowBar.test.tsx b/ship/src/web/ui/NowBar.test.tsx index da4e1da..1c5e04b 100644 --- a/ship/src/web/ui/NowBar.test.tsx +++ b/ship/src/web/ui/NowBar.test.tsx @@ -54,7 +54,7 @@ function isoDaysAgo(d: number): string { } describe("NowBar — claim 1 (renders when one active run exists)", () => { - it('produces role=region with aria-label="currently active runs" and "1 running" text', () => { + it('renders as
with aria-label="currently active runs" and "1 running" text', () => { render([ { id: "r1", @@ -64,9 +64,8 @@ describe("NowBar — claim 1 (renders when one active run exists)", () => { secondsSinceLastEvent: 12, }, ]); - const region = container.querySelector('[role="region"]'); + const region = container.querySelector('section[aria-label="currently active runs"]'); expect(region).not.toBeNull(); - expect(region?.getAttribute("aria-label")).toBe("currently active runs"); expect(region?.textContent ?? "").toContain("1 running"); }); diff --git a/ship/src/web/ui/NowBar.tsx b/ship/src/web/ui/NowBar.tsx index 950f10d..92ed32b 100644 --- a/ship/src/web/ui/NowBar.tsx +++ b/ship/src/web/ui/NowBar.tsx @@ -30,8 +30,7 @@ export function NowBar({ runs }: { runs: NowBarRun[] }) { if (runs.length === 0) return null; const anomalies = runs.filter((r) => r.secondsSinceLastEvent > STUCK_THRESHOLD_SECONDS); return ( -
-
+
); } diff --git a/ship/src/web/ui/StatePill.tsx b/ship/src/web/ui/StatePill.tsx index 9396d4a..e7400dc 100644 --- a/ship/src/web/ui/StatePill.tsx +++ b/ship/src/web/ui/StatePill.tsx @@ -14,7 +14,7 @@ // per the discipline documented in ui/StatusNode.ts. import type { CSSProperties } from "react"; -import { tokens, type StateKind } from "../design/tokens"; +import { type StateKind, tokens } from "../design/tokens"; import { STATUS_LABEL } from "./StatusNode"; export interface StatePillProps {