Files
goodbuddy/eslint.config.js
T
mesalogo b6c7de5f91 perf: move conversation persistence, refresh and agent events out of App
Persistence and history loading, refresh scheduling and the agent event handler work against the conversation store outside React; App.tsx drops to 9,566 lines and the lint ceiling to 9,600.
2026-10-03 05:53:33 +08:00

224 lines
8.5 KiB
JavaScript

import js from '@eslint/js'
import reactHooks from 'eslint-plugin-react-hooks'
import globals from 'globals'
import tseslint from 'typescript-eslint'
// ---------------------------------------------------------------------------
// Architecture ratchets (docs/roadmap/product-performance-experience-improvement-plan.md,
// batch 3C, PERF-17). Existing code still violates these rules, so each rule
// carries an allowlist of the files that violated it when the rule was added.
// The allowlists must ONLY SHRINK: remove a file once it is fixed, never add
// one. New code that needs the forbidden API belongs in the place the plan
// assigns it (data process, async fs, domain store, layout shell).
// ---------------------------------------------------------------------------
// PERF-16: SQLite belongs to a dedicated data process (utilityProcess); Main
// only routes messages. Files that imported DatabaseSync from 'node:sqlite'
// (including `import type`) on 2026-10-02. Shrink only.
const databaseSyncAllowlist = [
'src/agent-daemon/agent-model-gateway.ts',
'src/agent-daemon/event-journal.ts',
'src/agent-daemon/installation-cleanup.ts',
'src/agent-daemon/runtime-owner-registry.ts',
'src/agent-daemon/semantic-prompt-store.ts',
'src/main/agent/runtime-session-binding-store.ts',
'src/main/assistant/assistant-database.ts',
'src/main/assistant/assistant-storage-upgrade.ts',
'src/main/assistant/story-graph-reader.ts',
'src/main/assistant/subagent-progress-storage.ts',
'src/main/assistant/supervision-experiences.ts',
'src/main/assistant/supervision-review-store.ts',
'src/main/assistant/supervision-stories.ts',
'src/main/assistant/supervision-suggestions.ts',
'src/main/assistant/supervision-timeline.ts',
'src/main/conversation-attachment-storage.ts',
'src/main/knowledge/external/external-knowledge-store.ts',
'src/main/knowledge/knowledge-database.ts',
'src/shared/node/private-sqlite-database.ts'
]
// PERF-15/PERF-16 rule 1: Main must not block its event loop on file IO.
// Synchronous fs functions; also caught as `fs.xxxSync` member access.
const syncFsFunctions = [
'accessSync', 'appendFileSync', 'chmodSync', 'chownSync', 'closeSync', 'copyFileSync', 'cpSync',
'existsSync', 'fchmodSync', 'fchownSync', 'fdatasyncSync', 'fstatSync', 'fsyncSync', 'ftruncateSync',
'futimesSync', 'globSync', 'lchmodSync', 'lchownSync', 'linkSync', 'lstatSync', 'lutimesSync',
'mkdirSync', 'mkdtempSync', 'opendirSync', 'openSync', 'readdirSync', 'readFileSync', 'readlinkSync',
'readSync', 'readvSync', 'realpathSync', 'renameSync', 'rmdirSync', 'rmSync', 'statfsSync', 'statSync',
'symlinkSync', 'truncateSync', 'unlinkSync', 'utimesSync', 'writeFileSync', 'writeSync', 'writevSync'
]
const syncFsMessage = 'Synchronous fs APIs block the Main event loop (PERF-17). Use node:fs/promises, or move the work to the data process (PERF-16).'
const databaseSyncRestriction = [{
name: 'node:sqlite',
importNames: ['DatabaseSync'],
message: 'DatabaseSync may only be used by the data process (PERF-16); other code must go through its async interface (PERF-15). The allowlist in eslint.config.js must only shrink.'
}]
const syncFsRestriction = ['node:fs', 'fs', 'original-fs'].map(name => ({ name, importNames: syncFsFunctions, message: syncFsMessage }))
// Files under src/main (non-test) that used synchronous fs APIs on 2026-10-02. Shrink only.
const mainSyncFsAllowlist = [
'src/main/agent/paged-output-store.ts',
'src/main/assistant/assistant-database.ts',
'src/main/assistant/assistant-storage-upgrade.ts',
'src/main/conversation-attachment-storage.ts',
'src/main/magic-notes/magic-note-storage.ts',
'src/main/portable-user-data.ts',
'src/main/remote-agent/control-plane-package-installer.ts',
'src/main/windows-notification-identity.ts'
]
// PERF-13 rule 2: IPC subscriptions live in domain stores (*.ts), components
// subscribe to stores through selectors. React component files (*.tsx) that
// subscribed directly to window.goodbuddy.*.on* on 2026-10-02. Shrink only.
const rendererIpcSubscriptionAllowlist = [
'src/renderer/src/App.tsx',
'src/renderer/src/DocumentResultPreview.tsx',
'src/renderer/src/MagicNotesPanel.tsx',
'src/renderer/src/MagicNotesWorkspace.tsx',
'src/renderer/src/RightAssistantSidebar.tsx',
'src/renderer/src/StaticHtmlPreview.tsx'
]
const rendererIpcSubscriptionMessage = 'Components must not subscribe to window.goodbuddy.*.on* directly (PERF-17). Move the subscription into a domain store and read it with a selector (PERF-13).'
// PERF-13 goal: App.tsx shrinks to a layout shell. 11,635 lines on
// 2026-10-02; 9,567 on 2026-10-03 after moving persistence, refresh and agent
// events out. Ceiling rounded up to the next 100. Only ever lower this number.
const appTsxMaxLines = 9600
export default tseslint.config(
{
ignores: [
'.agent-resources/**',
'.remote-runtime-resources/**',
'.runtime-resources/**',
'coverage/**',
'dist/**',
'docs/**/*-demo.*',
'docs/**/*-demo-*.js',
'docs/**/vendor/**',
'node_modules/**',
'out/**',
'shareserver/**'
]
},
js.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname
}
}
},
{
files: ['src/main/**/*.ts', 'src/preload/**/*.ts', 'src/shared/**/*.ts'],
languageOptions: {
globals: {
...globals.node
}
}
},
{
files: ['build/**/*.cjs'],
languageOptions: {
globals: {
...globals.node
}
},
rules: {
'@typescript-eslint/no-require-imports': 'off'
}
},
{
files: ['src/renderer/src/**/*.{ts,tsx}'],
languageOptions: {
globals: {
...globals.browser
}
},
plugins: {
'react-hooks': reactHooks
},
rules: {
...reactHooks.configs.recommended.rules
}
},
{
files: ['sites/**/*.js'],
languageOptions: {
globals: {
...globals.browser
}
}
},
{
files: ['sites/**/*.mjs'],
languageOptions: {
globals: {
...globals.node
}
}
},
{
files: ['**/*.test.{ts,tsx}', 'vitest.config.ts'],
languageOptions: {
globals: {
...globals.node,
...globals.browser,
...globals.vitest
}
}
},
// --- Architecture ratchets (PERF-17), see allowlists at the top. ---------
// Rules a (DatabaseSync) and b (sync fs) share `no-restricted-imports`, and a
// later flat-config block replaces an earlier block's options for the same
// rule. The blocks below therefore partition the files so that each file
// gets exactly the restrictions it is subject to.
// DatabaseSync outside the data process (all of src, both value and type imports).
{
files: ['src/**/*.{ts,tsx}'],
ignores: ['**/*.test.{ts,tsx}', ...databaseSyncAllowlist],
rules: { 'no-restricted-imports': ['error', { paths: databaseSyncRestriction }] }
},
// Synchronous fs in Main: files not allowlisted for either rule get both restrictions.
{
files: ['src/main/**/*.ts'],
ignores: ['**/*.test.ts', ...mainSyncFsAllowlist, ...databaseSyncAllowlist],
rules: { 'no-restricted-imports': ['error', { paths: [...databaseSyncRestriction, ...syncFsRestriction] }] }
},
// Main files allowlisted for DatabaseSync but not for sync fs.
{
files: databaseSyncAllowlist.filter(file => file.startsWith('src/main/')),
ignores: mainSyncFsAllowlist,
rules: { 'no-restricted-imports': ['error', { paths: syncFsRestriction }] }
},
// `fs.readFileSync(...)` style access through a namespace/default import.
{
files: ['src/main/**/*.ts'],
ignores: ['**/*.test.ts', ...mainSyncFsAllowlist],
rules: {
'no-restricted-syntax': ['error', {
selector: `MemberExpression[object.name=/^(fs|nodeFs|fsSync|originalFs)$/][property.name=/^(${syncFsFunctions.join('|')})$/]`,
message: syncFsMessage
}]
}
},
// Direct IPC subscriptions in renderer components.
{
files: ['src/renderer/src/**/*.tsx'],
ignores: ['**/*.test.tsx', ...rendererIpcSubscriptionAllowlist],
rules: {
'no-restricted-syntax': ['error', {
selector: "CallExpression[callee.property.name=/^on[A-Z]/] > MemberExpression.callee MemberExpression[object.name='window'][property.name='goodbuddy']",
message: rendererIpcSubscriptionMessage
}]
}
},
// App.tsx size ceiling.
{
files: ['src/renderer/src/App.tsx'],
rules: {
'max-lines': ['error', { max: appTsxMaxLines, skipBlankLines: false, skipComments: false }]
}
}
)