diff --git a/.claude/skills/release-checklist/SKILL.md b/.claude/skills/release-checklist/SKILL.md index ecda427..7a5d8d5 100644 --- a/.claude/skills/release-checklist/SKILL.md +++ b/.claude/skills/release-checklist/SKILL.md @@ -19,7 +19,7 @@ npm run review # advisory local code review of the working-tree diff npm run review:docs # advisory local doc-consistency review ``` -- `build`/`lint`/`test` are release blockers if they fail. +- `build`/`lint`/`test` are release blockers if they fail. `npm run lint` is configured to mirror the Obsidian community-review bot for the classes that previously slipped through to submission: the type-checked `@typescript-eslint` rules (`no-deprecated` / `no-unsafe-*` / `no-unnecessary-type-assertion`), `no-unsupported-api` (an Obsidian API newer than `manifest.json`'s `minAppVersion`), and `noInlineConfig` (an `eslint-disable` can no longer silence a rule). If lint fails on one of these, it is doing its job; do not work around it with a disable comment. See RELEASING.md's guideline-conflict checklist. - The two `review` runs are **advisory** (always exit 0). Read the reports in `docs/claude-scratch/local-review--report.md` and surface anything real. They will not block, and they are imperfect (local, quantized model), so treat findings as a first-pass filter, not a gate. - **If a review report says "(no findings returned)" or looks empty/garbled**, it is almost always a model/tooling issue, not a clean bill of health — see `docs/DEV_TOOLING.md` Gotchas (reasoning models need thinking disabled; the context must fit the prompt; a huge diff truncates). Fix the tooling or note it; do not read empty output as "no problems." diff --git a/CLAUDE.md b/CLAUDE.md index 0c3cc2f..466c682 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ Two things make `src/` runnable under Vitest even though it targets the Obsidian - Bundler: [esbuild.config.mjs](esbuild.config.mjs). `obsidian`, `electron`, all `@codemirror/*`, all `@lezer/*`, and Node built-ins are marked `external`. Never import other runtime deps without bundling them in. - Release artifacts are `main.js`, `manifest.json`, and `styles.css` at the repo root. Do not commit the generated `main.js`. - TypeScript config ([tsconfig.json](tsconfig.json)) is strict: `noImplicitAny`, `strictNullChecks`, `noImplicitReturns`, `noUncheckedIndexedAccess`, `useUnknownInCatchVariables`, `baseUrl: "src"`. Target ES6, module ESNext, lib DOM + ES5/6/7 only. No Node lib, so don't reach for Node APIs in plugin code. -- ESLint ([eslint.config.mts](eslint.config.mts)) layers `eslint-plugin-obsidianmd`'s recommended rules on top of `typescript-eslint`. These rules encode Obsidian-specific correctness checks; respect them rather than disabling. +- ESLint ([eslint.config.mts](eslint.config.mts)) layers `eslint-plugin-obsidianmd`'s recommended rules on top of `typescript-eslint`. These rules encode Obsidian-specific correctness checks; respect them rather than disabling. The config is deliberately tuned to **mirror the Obsidian community-review bot** for the classes that once slipped through to submission (see [docs/RELEASING.md](docs/RELEASING.md)): it enables the type-checked `@typescript-eslint` rules (`no-deprecated`, `no-unsafe-*`, `no-unnecessary-type-assertion`) scoped to `src/`; cherry-picks `no-unsupported-api` from a `0.4.1` alias of the plugin (`obsidianmd-latest`, since the pinned `0.1.9` base predates that rule) so an Obsidian API newer than `manifest.json`'s `minAppVersion` fails lint locally; and sets `noInlineConfig` so an `eslint-disable` comment can never silence a rule (the bot forbids disables). The base plugin stays pinned at `0.1.9` because `0.4.x`'s recommended `ui/sentence-case` is over-aggressive and diverges from what the bot enforces; only the single `no-unsupported-api` rule is taken from the alias. To reach an API outside the typed/deprecated surface, use a local structural type-alias (e.g. `ScriptProcessorNodeLike` in [src/realtime/pcm.ts](src/realtime/pcm.ts)), never a disable comment. ## Source layout diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 260d01a..059e860 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -48,9 +48,11 @@ The tag name must equal `manifest.json`'s `version` exactly. `.npmrc` already pi These are the recurring findings; clear them before tagging. Most are also why the items above exist. +**Local lint now mirrors the bot for the classes that previously slipped through** (the 1.2.0 submission failed on three the local lint did not catch). [eslint.config.mts](../eslint.config.mts) enables: the type-checked `@typescript-eslint` rules (`no-deprecated`, the `no-unsafe-*` family, `no-unnecessary-type-assertion`); `no-unsupported-api` (cherry-picked from a `0.4.1` alias of `eslint-plugin-obsidianmd` since our pinned `0.1.9` base lacks it) so a direct Obsidian API newer than `minAppVersion` is caught locally; and `noInlineConfig` so an `eslint-disable` can never silence a rule. So `npm run lint` failing on these now is the point. When the review bot adds a new rule class we do not catch, add it here the same way (prefer cherry-picking one rule from the alias over adopting `0.4.x`'s recommended config wholesale, whose `ui/sentence-case` is over-aggressive and diverges from the bot). + - **Plugin `id`**: lowercase letters and hyphens only, must not end in `plugin`, must not contain `obsidian`. Locked once published; do not change it. (Ours is `rewrite-voice-notes`.) -- **No newer-than-minAppVersion APIs**: see the `minAppVersion` rule above. -- **No `eslint-disable` directives.** The bot rejects disabling its rules. If a string trips `ui/sentence-case` (e.g. a random example), pass it through a variable instead of a string literal; the rule only inspects literals. +- **No newer-than-minAppVersion APIs**: see the `minAppVersion` rule above. Enforced locally by `obsidianmd-latest/no-unsupported-api`. +- **No `eslint-disable` directives.** The bot rejects disabling its rules, and `noInlineConfig` makes them inert locally (an attempt to suppress an error just leaves the error). Reach APIs outside the typed/deprecated surface through local type-aliases instead (see [src/realtime/pcm.ts](../src/realtime/pcm.ts)'s `ScriptProcessorNodeLike`). If a string trips `ui/sentence-case` (e.g. a random example), pass it through a variable instead of a string literal; the rule only inspects literals. - **Popout-window safety**: use `activeDocument` / `activeWindow` instead of `document` / `window`-as-globalThis where a popout could differ; use `window.setTimeout` / `window.clearTimeout` (not bare `setTimeout`); avoid `globalThis` (use `window`). For paired `addEventListener` / `removeEventListener`, capture one document reference so removal targets the same object. - **No `!important` in [styles.css](../styles.css).** Raise specificity, use CSS variables, or toggle via Obsidian's `el.toggle()` / `hide()` / `show()` (which set inline display) instead. - **Manifest `description`**: action-focused, <= 250 chars, ends with a period, no emoji. diff --git a/eslint.config.mts b/eslint.config.mts index c256437..62b6297 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -1,5 +1,12 @@ import tseslint from 'typescript-eslint'; import obsidianmd from "eslint-plugin-obsidianmd"; +// The Obsidian community-review bot runs a newer eslint-plugin-obsidianmd than our +// pinned 0.1.9 base, whose `no-unsupported-api` rule (flags a direct Obsidian API call +// newer than manifest `minAppVersion`) is what caught `FileManager.trashFile` at +// submission. We keep 0.1.9 as the base config (its `ui/sentence-case` is the one the bot +// actually enforces; 0.4.x's is over-aggressive and produces wrong fixes), and cherry-pick +// ONLY that one rule from a 0.4.1 alias so the minAppVersion check runs locally too. +import obsidianmdLatest from "obsidianmd-latest"; import globals from "globals"; import { globalIgnores } from "eslint/config"; @@ -55,6 +62,15 @@ const sentenceCaseOptions = { export default tseslint.config( { + // The community-review bot rejects `eslint-disable` directives (it flags both the + // undescribed comment and disabling a rule at all). `noInlineConfig` makes every inline + // eslint directive inert, so a rule (ours or the bot's) cannot be silenced from a + // comment: an attempt to suppress a real error just leaves the error in place and fails + // `npm run lint`. Reach APIs outside the typed/deprecated surface through local + // type-aliases instead (see src/realtime/pcm.ts), never a disable comment. + linterOptions: { + noInlineConfig: true, + }, languageOptions: { globals: { ...globals.browser, @@ -101,6 +117,18 @@ export default tseslint.config( '@typescript-eslint/no-unnecessary-type-assertion': 'error', }, }, + { + // Cherry-picked from the 0.4.1 alias (see the import comment): the single rule that + // flags a direct Obsidian API newer than manifest `minAppVersion`. Registering the + // plugin only makes its rules available; nothing else from 0.4.1 runs unless enabled + // here, so its divergent sentence-case rule stays off. This is the check that would + // have caught `trashFile` locally before the 1.2.0 submission. + files: ['src/**/*.ts'], + plugins: { 'obsidianmd-latest': obsidianmdLatest }, + rules: { + 'obsidianmd-latest/no-unsupported-api': 'error', + }, + }, { // test/ runs under Node via Vitest, not inside Obsidian, so the Node-builtin ban that // keeps src/ portable to the Electron/mobile plugin runtime doesn't apply here. diff --git a/package-lock.json b/package-lock.json index 5be062e..bce0d69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "eslint-plugin-obsidianmd": "0.1.9", "globals": "14.0.0", "jiti": "2.6.1", + "obsidianmd-latest": "npm:eslint-plugin-obsidianmd@0.4.1", "tslib": "2.4.0", "typescript": "^5.8.3", "typescript-eslint": "8.35.1", @@ -478,6 +479,34 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-plugin-eslint-comments": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.7.2.tgz", + "integrity": "sha512-LF03qURSwEWm2dz5wtdDCzNk+7Opl0X7q6I3undsaIuNsEiNvRV3BCtqu14Q/6Pzg1tBj44LcxpW2EpSLZStZw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^4.0.0", + "ignore": "^7.0.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@eslint-community/eslint-plugin-eslint-comments/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -626,7 +655,6 @@ "integrity": "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@eslint/core": "^0.17.0", "@eslint/plugin-kit": "^0.4.1", @@ -705,7 +733,6 @@ "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18" } @@ -1188,7 +1215,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2882,12 +2908,20 @@ "node": ">=10" } }, + "node_modules/eslint-plugin-no-unsanitized": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.5.tgz", + "integrity": "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg==", + "dev": true, + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, "node_modules/eslint-plugin-obsidianmd": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/eslint-plugin-obsidianmd/-/eslint-plugin-obsidianmd-0.1.9.tgz", "integrity": "sha512-/gyo5vky3Y7re4BtT/8MQbHU5Wes4o6VRqas3YmXE7aTCnMsdV0kfzV1GDXJN9Hrsc9UQPoeKUMiapKL0aGE4g==", "dev": true, - "license": "MIT", "dependencies": { "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "8.56.2", @@ -2930,7 +2964,6 @@ "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz", "integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==", "dev": true, - "license": "MIT", "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" @@ -4560,10 +4593,9 @@ } }, "node_modules/obsidian": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.10.3.tgz", - "integrity": "sha512-VP+ZSxNMG7y6Z+sU9WqLvJAskCfkFrTz2kFHWmmzis+C+4+ELjk/sazwcTHrHXNZlgCeo8YOlM6SOrAFCynNew==", - "license": "MIT", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", + "integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==", "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" @@ -4573,6 +4605,92 @@ "@codemirror/view": "6.38.6" } }, + "node_modules/obsidianmd-latest": { + "name": "eslint-plugin-obsidianmd", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-obsidianmd/-/eslint-plugin-obsidianmd-0.4.1.tgz", + "integrity": "sha512-Nv3593kVsFOS8kir/HWaJ5CR3xcyiPWEPyXst5Pib8mxRYYuLYPpJS2ALMoZXHulHyiGwoYW8AaxvLRc4rhrRg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.7.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/js": "^9.30.1", + "@eslint/json": "0.14.0", + "@microsoft/eslint-plugin-sdl": "^1.1.0", + "@types/eslint": "9.6.1", + "@types/node": "20.12.12", + "@typescript-eslint/types": "^8.33.1", + "@typescript-eslint/utils": "^8.33.1", + "eslint": ">=9.19.0", + "eslint-plugin-depend": "1.3.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-json-schema-validator": "5.1.0", + "eslint-plugin-no-unsanitized": "^4.1.5", + "eslint-plugin-security": "2.1.1", + "globals": "14.0.0", + "obsidian": "1.12.3", + "semver": "^7.7.4", + "typescript": "5.4.5", + "typescript-eslint": "^8.35.1" + }, + "bin": { + "eslint-plugin-obsidian": "dist/lib/index.js" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@eslint/js": "^9.30.1", + "@eslint/json": "0.14.0", + "eslint": ">=9.19.0", + "obsidian": "1.8.7", + "typescript-eslint": "^8.35.1" + } + }, + "node_modules/obsidianmd-latest/node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/obsidianmd-latest/node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/obsidianmd-latest/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/obsidianmd-latest/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", diff --git a/package.json b/package.json index 8d7a9ee..ebd08cf 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "eslint-plugin-obsidianmd": "0.1.9", "globals": "14.0.0", "jiti": "2.6.1", + "obsidianmd-latest": "npm:eslint-plugin-obsidianmd@0.4.1", "tslib": "2.4.0", "typescript": "^5.8.3", "typescript-eslint": "8.35.1",