Tighten Biome lint policy

This commit is contained in:
murashit 2026-06-24 16:09:29 +09:00
parent 79a2c7fcbf
commit ca26a53e1b
15 changed files with 86 additions and 35 deletions

View file

@ -44,9 +44,13 @@
"useHookAtTopLevel": "error"
},
"nursery": {
"noFloatingPromises": "error"
"noFloatingPromises": "error",
"noMisusedPromises": "error",
"noUnsafePlusOperands": "error",
"useExhaustiveSwitchCases": "error"
},
"suspicious": {
"noArrayIndexKey": "error",
"noImportCycles": "error"
}
}

View file

@ -10,9 +10,11 @@ npm run format
npm run check
```
Run `npm run format` after edits and before `npm run check` so Biome formatting and import organization issues are fixed upfront. `npm run check` runs TypeScript type checking, unit tests, lint checks, Biome format and assist checks, CSS verification, and the production esbuild bundle. `npm run check:ci` uses the same checks with CI cache settings for TypeScript, tests, and ESLint. Use `npm run build` when you need to refresh Obsidian-loaded assets without running the full check.
Use this as the normal edit loop. `npm run format` applies Biome formatting and import organization; `npm run check` is the local preflight for type checking, tests, lint, format/assist checks, CSS checks, and the production bundle.
Formatting, import organization, general JavaScript/TypeScript/JSON linting, and JavaScript/TypeScript/CSS/JSON formatting are handled by Biome. The Biome lint configuration explicitly uses the recommended preset, with accessibility diagnostics downgraded to info while the Obsidian-specific UI semantics are reviewed rule by rule. Biome also owns floating Promise, import cycle, and Preact hook dependency/order checks. CSS is formatted by Biome but excluded from Biome linting so CSS lint policy stays centralized in Stylelint and the project CSS usage script. ESLint remains for unsafe-any TypeScript parser-service checks, Obsidian plugin API/text policy checks, and Codex Panel responsibility-boundary checks.
Use focused scripts for tight loops: `npm run typecheck`, `npm run test`, `npm run lint`, or `npm run build`. The `*:ci` variants match CI cache behavior.
Biome owns formatting, import organization, and general JavaScript/TypeScript/JSON linting. Biome warnings fail `npm run lint` and `npm run check`; info diagnostics stay advisory, including accessibility while Obsidian-specific UI semantics are reviewed rule by rule. CSS lint policy stays in Stylelint and the CSS usage script. ESLint remains for typed unsafe-any checks, Obsidian plugin policy, and Codex Panel responsibility-boundary rules.
## Generated and Loaded Files

View file

@ -67,7 +67,7 @@ function readMarkdownTableValues(markdown) {
const header = splitMarkdownTableRow(lines[index]);
if (!header) continue;
const separator = splitMarkdownTableRow(lines[index + 1]);
if (!separator || !separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
if (!separator?.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
const keyColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "key");
const versionColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "version");

View file

@ -47,7 +47,7 @@ function lintCommands({ ciMode, namePrefix = "" }) {
const eslintCacheArgs = ciMode ? "" : " --cache --cache-strategy content --cache-location node_modules/.cache/eslint/.eslintcache";
return [
{ name: `${namePrefix}biome`, command: "biome lint --no-errors-on-unmatched --diagnostic-level=error" },
{ name: `${namePrefix}biome`, command: "biome lint --no-errors-on-unmatched --diagnostic-level=warn --error-on-warnings" },
{
name: `${namePrefix}eslint`,
command: `eslint src tests scripts "*.config.ts" "*.config.mjs" --max-warnings=0${eslintCacheArgs}`,

View file

@ -90,7 +90,7 @@ async function readTexts(files) {
function locationsInTexts(texts, needle) {
const locations = [];
for (const { file, text } of texts) {
let offset = text.indexOf(needle);
const offset = text.indexOf(needle);
if (offset === -1) continue;
const line = text.slice(0, offset).split("\n").length;
locations.push(`${relative(file)}:${line}`);

View file

@ -39,7 +39,7 @@ function finiteTimestamp(value: number): number {
}
function recencyAtPatch(thread: ThreadRecord): { recencyAt: number | null } | Record<string, never> {
if (!Object.prototype.hasOwnProperty.call(thread, "recencyAt")) return {};
if (!Object.hasOwn(thread, "recencyAt")) return {};
const value = thread.recencyAt;
return { recencyAt: typeof value === "number" && Number.isFinite(value) ? value : null };
}

View file

@ -155,7 +155,7 @@ const STREAM_UPDATE_PLANNERS = {
"item/reasoning/summaryPartAdded": (state, notification) =>
appendToolTextPlan(state, notification.params.itemId, notification.params.turnId, "reasoning", "", "reasoning"),
"item/started": (_state, notification) => startedItemPlan(notification.params.item, notification.params.turnId),
"item/completed": (state, notification) => completedItemPlan(state, notification.params.item, notification.params.turnId),
"item/completed": (_state, notification) => completedItemPlan(notification.params.item, notification.params.turnId),
"item/commandExecution/outputDelta": (_state, notification) =>
actionPlan({
type: "message-stream/item-output-appended",
@ -433,7 +433,7 @@ function startedItemPlan(item: TurnItem, turnId: string): ChatNotificationPlan {
return streamItem ? actionPlan({ type: "message-stream/item-upserted", item: streamItem }) : EMPTY_PLAN;
}
function completedItemPlan(state: ChatState, item: TurnItem, turnId: string): ChatNotificationPlan {
function completedItemPlan(item: TurnItem, turnId: string): ChatNotificationPlan {
if (item.type === "userMessage") return EMPTY_PLAN;
const streamItem = messageStreamItemFromTurnItem(item, turnId);
if (!streamItem) return EMPTY_PLAN;

View file

@ -312,11 +312,11 @@ function serverRequestScope(request: ServerRequest): MessageScope {
}
function isServerRequest(message: ServerNotification | ServerRequest): message is ServerRequest {
return Object.prototype.hasOwnProperty.call(SERVER_REQUEST_SCOPE_EXTRACTORS, message.method);
return Object.hasOwn(SERVER_REQUEST_SCOPE_EXTRACTORS, message.method);
}
function isServerNotification(message: ServerNotification | ServerRequest): message is ServerNotification {
return Object.prototype.hasOwnProperty.call(SERVER_NOTIFICATION_SCOPE_EXTRACTORS, message.method);
return Object.hasOwn(SERVER_NOTIFICATION_SCOPE_EXTRACTORS, message.method);
}
function threadTurnRequestScope(request: { params: { threadId: string; turnId: string | null } }): MessageScope {

View file

@ -263,7 +263,7 @@ function threadSettingsValueEqual(left: unknown, right: unknown): boolean {
const rightKeys = Object.keys(right);
return (
leftKeys.length === rightKeys.length &&
leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && threadSettingsValueEqual(left[key], right[key]))
leftKeys.every((key) => Object.hasOwn(right, key) && threadSettingsValueEqual(left[key], right[key]))
);
}

View file

@ -47,6 +47,8 @@ interface ComposerContextMeterViewModel {
percent: string;
}
const COMPOSER_CONTEXT_METER_CELL_IDS = ["context-0", "context-1", "context-2", "context-3"] as const;
export interface ComposerCallbacks {
onInput: (value: string) => void;
onUpdateSuggestions: () => void;
@ -341,14 +343,18 @@ function ComposerContextMeter({ context }: { context: ComposerMetaViewModel["con
return (
<span className="codex-panel__composer-meta-context">
<span className="codex-panel__composer-meta-context-dots">
{context.cells.map((cell, index) => (
<span
key={index}
className={["codex-panel__composer-meta-context-dot", cell.placeholder ? "is-placeholder" : ""].filter(Boolean).join(" ")}
>
{cell.text}
</span>
))}
{COMPOSER_CONTEXT_METER_CELL_IDS.map((id, index) => {
const cell = context.cells[index];
if (!cell) return null;
return (
<span
key={id}
className={["codex-panel__composer-meta-context-dot", cell.placeholder ? "is-placeholder" : ""].filter(Boolean).join(" ")}
>
{cell.text}
</span>
);
})}
</span>
<span className="codex-panel__composer-meta-context-percent">{context.percent}</span>
</span>

View file

@ -2,7 +2,7 @@ import type { ComponentChild as UiNode } from "preact";
import { useLayoutEffect, useRef } from "preact/hooks";
import { renderRawDiffLines } from "../../../../shared/diff/render";
import { type DetailSection, type DetailView } from "../../presentation/message-stream/detail-view";
import type { DetailSection, DetailView } from "../../presentation/message-stream/detail-view";
import type { MessageStreamDisclosureState } from "./context";
export interface DetailRenderContext {

View file

@ -3,12 +3,12 @@ import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import { hasPendingRequests, pendingRequestCountsFromQueues } from "../../../../domain/pending-requests/aggregate";
import { approvalDetailsDisclosureId } from "../../domain/pending-requests/disclosure-ids";
import {
type PendingApprovalViewModel,
type PendingMcpElicitationFieldViewModel,
type PendingMcpElicitationViewModel,
type PendingUserInputQuestionViewModel,
type PendingUserInputViewModel,
import type {
PendingApprovalViewModel,
PendingMcpElicitationFieldViewModel,
PendingMcpElicitationViewModel,
PendingUserInputQuestionViewModel,
PendingUserInputViewModel,
} from "../../presentation/pending-requests/view-model";
import type { PendingRequestBlockActions } from "./context";
import { createStatusMessageClassName } from "./status";

View file

@ -1,9 +1,9 @@
import { Fragment, type ComponentChild as UiNode } from "preact";
import {
type MessageStreamActivityItemView,
type MessageStreamRenderedItemView,
type MessageStreamViewBlock,
import type {
MessageStreamActivityItemView,
MessageStreamRenderedItemView,
MessageStreamViewBlock,
} from "../../presentation/message-stream/view-model";
import type { MessageStreamContext, PendingRequestBlockContext } from "./context";
import { detailNode } from "./detail";

View file

@ -1,4 +1,4 @@
import { type Ref, type ComponentChild as UiNode } from "preact";
import type { Ref, ComponentChild as UiNode } from "preact";
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import type { MessageStreamTextView } from "../../presentation/message-stream/text-view";

View file

@ -27,7 +27,7 @@ describe("development scripts", () => {
[
"#!/usr/bin/env node",
"import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';",
"appendFileSync('codex-args.txt', `${process.argv.slice(2).join('\\n')}\\n`);",
"appendFileSync('codex-args.txt', process.argv.slice(2).join('\\n') + '\\n');",
"mkdirSync('src/generated/app-server/v2', { recursive: true });",
"writeFileSync('src/generated/app-server/v2/Example.ts', '// GENERATED CODE! DO NOT MODIFY BY HAND!\\nexport type Example = string | null | null;\\n');",
].join("\n"),
@ -116,6 +116,28 @@ describe("development scripts", () => {
expect(report.failures).toEqual([]);
});
it("runs Biome lint with warning-level diagnostics", async () => {
const cwd = await tempWorkspace();
const binDir = path.join(cwd, "bin");
await mkdir(binDir, { recursive: true });
await mkdir(path.join(cwd, "scripts", "lint"), { recursive: true });
await writeFile(path.join(cwd, "scripts", "lint", "check-css-usage.mjs"), "");
await writeCaptureBin(path.join(binDir, "biome"), "biome-args.txt");
await writeExitZeroBin(path.join(binDir, "eslint"));
await writeExitZeroBin(path.join(binDir, "stylelint"));
await writeExitZeroBin(path.join(binDir, "knip"));
const result = runNodeScript("scripts/check.mjs", ["--lint"], cwd, {
PATH: `${binDir}${path.delimiter}${process.env["PATH"] ?? ""}`,
});
expect(result.status).toBe(0);
await expect(readFile(path.join(cwd, "biome-args.txt"), "utf8")).resolves.toBe(
"lint\n--no-errors-on-unmatched\n--diagnostic-level=warn\n--error-on-warnings\n",
);
});
it("keeps CSS usage checks quiet when every class is used", async () => {
const cwd = await cssUsageFixture({
"src/styles/10-component.css": ".codex-panel__used { display: block; }\n",
@ -154,7 +176,7 @@ describe("development scripts", () => {
"src/styles/10-component.css": ".codex-panel__used { display: block; }\n",
"src/component.ts": [
'export const used = "codex-panel__used";',
"export const dynamicClassName = `codex-panel__task-step--${status}`;",
"export const dynamicClassName = `codex-panel__task-step--$" + "{status}`;",
].join("\n"),
});
@ -170,7 +192,7 @@ describe("development scripts", () => {
it("does not let dynamic CSS prefixes hide unconfigured class candidates", async () => {
const cwd = await cssUsageFixture({
"src/styles/10-component.css": ".codex-panel__task-step--stale { display: block; }\n",
"src/component.ts": "export const className = `codex-panel__task-step--${status}`;\n",
"src/component.ts": "export const className = `codex-panel__task-step--$" + "{status}`;\n",
});
const result = runNodeScript("scripts/lint/check-css-usage.mjs", [], cwd);
@ -250,3 +272,20 @@ async function writeJson(file: string, value: unknown): Promise<void> {
async function readJson(file: string): Promise<unknown> {
return JSON.parse(await readFile(file, "utf8"));
}
async function writeCaptureBin(file: string, outputFile: string): Promise<void> {
await writeFile(
file,
[
"#!/usr/bin/env node",
"const { appendFileSync } = require('node:fs');",
`appendFileSync(${JSON.stringify(outputFile)}, \`\${process.argv.slice(2).join("\\n")}\\n\`);`,
].join("\n"),
);
await chmod(file, 0o755);
}
async function writeExitZeroBin(file: string): Promise<void> {
await writeFile(file, "#!/usr/bin/env node\n");
await chmod(file, 0o755);
}