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.
224 lines
8.5 KiB
JavaScript
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 }]
|
|
}
|
|
}
|
|
)
|