Files
goodbuddy/eslint.config.js
T
mesalogo d80d42b032 feat(storage): move desktop SQLite ownership into a storage utility process
Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.

- Remove the Main readWithFallback path and the supervision worker's
  separate business write connection; manifests are scanned read-only and
  committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
  unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
  ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
  storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
  directories and reclaim confirmed stale ones; stop writing duplicate
  parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
  acceptance fixtures, and maintenance/architecture documentation.

Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
2026-10-06 15:40:18 +08:00

273 lines
11 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).
// ---------------------------------------------------------------------------
// Concrete SQLite implementations, executed by storage owners or the remote Agent.
// Type imports do not grant permission to execute SQLite.
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/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'
]
// Explicit execution contexts, not a src/main or *worker* naming exemption.
const sqliteExecutionContexts = [
'src/agent-daemon/agent-owned-acp-prompt.ts',
'src/agent-daemon/daemon.ts',
'src/agent-daemon/direct-linux-stdio-process-owner.ts',
'src/agent-daemon/index.ts',
'src/agent-daemon/runtime-composition.ts',
'src/main/desktop-storage-owner.ts',
'src/main/desktop-storage-files.ts',
'src/main/desktop-storage-runtime-operations.ts',
'src/main/desktop-storage-entry.ts',
'src/main/desktop-storage-runtime-host-fixture.ts',
'src/main/readonly-query-worker.ts',
'src/main/assistant-storage-worker.ts',
'src/main/agent/private-sqlite-database.ts'
]
// Match known module entry points, including the legacy opener re-export. This
// guards imports rather than trying to infer arbitrary transitive call graphs.
const sqliteValueImports = {
'node:sqlite': ['DatabaseSync'],
'private-sqlite-database': ['openPrivateSqliteDatabase', 'PreparedPrivateSqliteDatabaseFile'],
'assistant-database': ['AssistantDatabase'],
'knowledge-database': ['KnowledgeDatabase'],
'external-knowledge-store': ['ExternalKnowledgeStore'],
'conversation-attachment-storage': ['ConversationAttachmentStorage'],
'runtime-session-binding-store': ['SqliteRuntimeSessionBindingStore'],
'agent-model-gateway': ['AgentModelCallLedger'],
'event-journal': ['EventJournal'],
'runtime-owner-registry': ['RuntimeOwnerRegistry'],
'semantic-prompt-store': ['SemanticPromptStore'],
'assistant-storage-upgrade': ['getPendingAssistantStorageUpgrade', 'upgradeAssistantStorage'],
'desktop-storage-owner': ['DesktopStorageOwner'],
'desktop-storage-files': ['openDesktopStorageFiles'],
'desktop-storage-runtime-operations': ['DesktopStorageRuntimeOwner']
}
const sqliteExecutionRule = {
meta: { type: 'problem', schema: [], messages: {
owner: 'SQLite value access belongs in an explicit storage owner, worker or remote Agent context (SA-07). Main must use the async storage interface; type imports are allowed.'
} },
create(context) {
function check(node, source, specifiers) {
if (node.importKind === 'type' || node.exportKind === 'type') return
const path = source?.type === 'TemplateLiteral' && source.expressions.length === 0
? source.quasis[0].value.cooked : source?.value
if (typeof path !== 'string') return
const name = path.split('/').at(-1).replace(/\.(?:[cm]?[jt]s)$/, '')
const restricted = /(?:^|\/)agent-daemon(?:\/index(?:\.[cm]?[jt]s)?)?$/.test(path)
? ['AgentModelCallLedger', 'EventJournal', 'RuntimeOwnerRegistry', 'SemanticPromptStore']
: sqliteValueImports[name]
if (!restricted) return
if (!specifiers || specifiers.length === 0 || specifiers.some(specifier => {
if (specifier.importKind === 'type' || specifier.exportKind === 'type') return false
const imported = specifier.imported ?? specifier.local
return !['ImportSpecifier', 'ExportSpecifier'].includes(specifier.type)
|| restricted.includes(imported?.name ?? imported?.value)
})) context.report({ node, messageId: 'owner' })
}
return {
ImportDeclaration: node => check(node, node.source, node.specifiers),
ExportNamedDeclaration: node => check(node, node.source, node.specifiers),
ExportAllDeclaration: node => check(node, node.source),
ImportExpression: node => check(node, node.source),
CallExpression: node => {
if (node.callee.type === 'Identifier' && node.callee.name === 'require') check(node, node.arguments[0])
},
TSImportEqualsDeclaration: node => check(node, node.moduleReference.expression)
}
}
}
// 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 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/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; 7,904 after moving the composer, route callbacks and the task
// store out. Ceiling rounded up to the next 100. Only ever lower this number.
const appTsxMaxLines = 8000
export default tseslint.config(
{
ignores: [
'.agent-resources/**',
'.remote-runtime-resources/**',
'.runtime-resources/**',
'coverage/**',
'dist/**',
'docs/**/*-demo.*',
'docs/**/*-demo-*.js',
'docs/**/vendor/**',
'node_modules/**',
'out/**',
'temp/**',
'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. ---------
{
files: ['src/**/*.{ts,tsx,js,mjs,cjs}'],
ignores: ['**/*.test.{ts,tsx,js}', ...databaseSyncAllowlist, ...sqliteExecutionContexts],
plugins: { architecture: { rules: { 'sqlite-execution-context': sqliteExecutionRule } } },
rules: { 'architecture/sqlite-execution-context': 'error' }
},
// Synchronous fs in Main.
{
files: ['src/main/**/*.ts'],
ignores: ['**/*.test.ts', ...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 }]
}
}
)