Skip to content

build: add ESLint flat config and CI lint step - #57

Merged
danielgerlag merged 1 commit into
mainfrom
build/eslint
Sep 26, 2026
Merged

danielgerlag merged 1 commit into
mainfrom
build/eslint

Conversation

@danielgerlag

@danielgerlag danielgerlag commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Part of #33. This covers the lint item only: pnpm lint exists and runs in CI.

Head: e560dda, one commit rebased onto main 5817703 (after #56).

What this adds

  • eslint.config.mjs (flat config) at the repo root. It uses @eslint/js recommended plus typescript-eslint recommended. The rules are syntax-only, not type-checked: no project or projectService. There are no other plugins and no stylistic rules.
  • tooling/eslint-config (@borg/eslint-config, private, no build/test scripts). It is a three-line re-export shim for @eslint/js, typescript-eslint and globals. tooling/* is added to pnpm-workspace.yaml.
  • Root lint script (eslint .) and devDeps eslint 10.11.0 and @borg/eslint-config workspace:*.
  • CI: a Lint step in both jobs (verify and verify (ubuntu-latest)), before Type-check. No other workflow changes.

Pinned versions, all from registry.npmjs.org: eslint 10.11.0, @eslint/js 10.0.1, typescript-eslint 8.70.1, globals 17.12.0, typescript 6.0.3 (lint only). The registry config is untouched.

Docs and example (added after the rebase onto #56)

  • README "Checks": corepack pnpm lint is the first line.
  • CONTRIBUTING: the Verify block lists pnpm lint, the CI paragraph says both jobs run lint, and the pre-PR sentence reads "pnpm lint, pnpm typecheck, and pnpm test".
  • examples/headless/src/main.ts (added by docs: pitch-first README and plugin authoring guide #56) had 2 no-require-imports hits: template-string require() for each plugin's manifest and main.
    • It now uses static ES imports of each plugin's borg.plugin.json and main, with an explicit plugins: [...] array. No new disables.
    • The same four plugins load in the same order. The only difference is that the plugin modules are imported at startup instead of inside loadMain. Because the imports are static, typos now fail at compile time.
    • The README copy of the file is updated to match; examples/headless/test checks that it is verbatim. The example still prints borg.hello: Kernel alive.
    • examples/headless/test/headless.test.ts was already clean.

Decision: TypeScript 7 vs typescript-eslint

The repo is on typescript ^7.0.2, the native port. Its npm package has no JS compiler API; require("typescript") is only version.cjs. typescript-eslint 8.70.1, the latest, requires a JS-API TypeScript >=4.8.4 <6.1.0. It crashes on load when its typescript peer resolves to 7.0.2, which is what happens when it is a root devDep. pnpm overrides don't apply to peer dependencies; I tried.

The fix is to install typescript-eslint through the @borg/eslint-config workspace package, which depends on typescript 6.0.3. pnpm resolves the peer inside that importer, so the lint parser gets 6.0.3. The repo's own TypeScript is unchanged:

  • root tsc -v is 7.0.2
  • pnpm typecheck still runs on 7.0.2
  • the lockfile resolves @typescript-eslint/* with (typescript@6.0.3) and everything else as before

When typescript-eslint supports TS 7, the shim and the 6.0.3 pin can go away.

The config file is .mjs because the root package.json has no "type": "module". With a .js name, Node prints MODULE_TYPELESS_PACKAGE_JSON on every run. I left the root module type alone; changing it is a repo-wide change.

Ignored (generated or build output)

**/dist/**, **/.package/**, **/node_modules/**, **/bundled-plugins.ts, **/bundled-ui-plugins.ts, **/coverage/**, **/.vite/**, playwright-report/**, test-results/**, .research/**. These mirror .gitignore plus the two generated bundle files. No hand-written source is ignored.

Baseline: pure recommended config, 84 errors across 13 rules

Rule Count Handling
no-empty 20 still on, with allowEmptyCatch: true (all 20 were empty catch {})
no-undef 15 still on. Fixed by adding browser globals to scripts/verify-graph-designer.mjs (page.evaluate callbacks)
@typescript-eslint/no-require-imports 12 off for tests/e2e/**/*.ts only
@typescript-eslint/no-unused-vars 12 still on, ignoring ^_ names (all 12 were _-prefixed discards)
preserve-caught-error 10 off
prefer-const 7 off
no-control-regex 2 off for 2 files
@typescript-eslint/no-this-alias 1 off for 1 file
no-unassigned-vars 1 off for 1 file
no-unused-private-class-members 1 off for 1 file
@typescript-eslint/no-empty-object-type 1 fixed in source
no-useless-assignment 1 fixed in source
no-useless-escape 1 fixed in source

Disabled rules, to tighten later

Each one has a comment in eslint.config.mjs.

Rule Violations Scope Why
preserve-caught-error 10 global Provider runtimes (anthropic, openai, ollama, openrouter, chat, mcp catalog) rethrow without Error.cause. Adding cause changes the thrown error objects.
prefer-const 7 global Mostly a let declared before a closure that references it, then assigned (e.g. run in loop-manager, disposable in graphs executor). Converting needs restructuring per site.
@typescript-eslint/no-require-imports 12 tests/e2e/**/*.ts Specs load the Electron path with require(require.resolve("electron")).
no-control-regex 2 packages/contracts/src/index.ts, packages/kernel/src/tls-service.ts Validators reject control characters on purpose.
@typescript-eslint/no-this-alias 1 packages/kernel/src/tls-service.ts The TLS stream adapter uses const self = this inside ReadableStream/WritableStream callbacks.
no-unassigned-vars 1 plugins/mcp-apps/src/ui.tsx Solid's ref={frame} assigns the variable, which the rule can't see.
no-unused-private-class-members 1 plugins/channel-imap/src/imap-codec.ts ImapCodec.#loop is a write-only once-guard.

The file-scoped offs stay enforced everywhere else. I checked this with a planted snippet: it fails in a new kernel file and passes only in the listed file. There are no inline eslint-disable comments.

Source fixes (type-level or no-op; no runtime behavior change)

  • packages/kernel/src/prompt-assembler.ts: export interface PromptSlot extends PromptSlotContribution {} becomes export type PromptSlot = PromptSlotContribution. It is the same type, nothing declaration-merges PromptSlot, and the kernel index re-export is unchanged.
  • packages/kernel/src/model-gateway.ts: existing = await journal.save(...) becomes await journal.save(...). existing is never read after that point; the save is still awaited.
  • packages/kernel/src/tls-service.ts: /[\[\]]/ becomes /[[\]]/. It is the same character class and the regex has no flags.

Lockfile

The diff against main is additions only (+749 / -0), regenerated with pnpm 12.0.0 on top of #56's lockfile: the new tooling/eslint-config importer, the two root devDeps, and the packages and snapshots they pull in. Nothing existing is re-resolved and the settings are unchanged.

Gates (local, Node 24, CI=true, on e560dda)

Gate Result
pnpm install --frozen-lockfile pass (lockfile up to date)
pnpm lint pass, 0 errors, 0 warnings
pnpm check:boundaries pass
pnpm typecheck pass (tsc -v = 7.0.2)
pnpm test 140 files, 935 tests passed
xvfb-run -a pnpm test:e2e 44 passed, 2 skipped, 0 failed

@danielgerlag

Copy link
Copy Markdown
Owner Author

CI note: on the first attempt, the macOS verify job failed one e2e test: slice-1-shell.spec.ts:254 ("uses OS-protected secret storage in production mode"). The error was Electron process did not exit within 8000ms after app.quit(). Lint, type-check and unit tests had passed in that job. That test isn't touched by this PR, so I re-ran the failed job once and it passed. Both jobs are now green.

Baseline @eslint/js + typescript-eslint recommended (not type-checked) via a
@borg/eslint-config shim that pins typescript 6.0.3 for typescript-eslint,
since the repo's typescript 7 has no JS compiler API. Adds pnpm lint, a Lint
step in both CI jobs, and three no-op source fixes.

The headless example now loads its plugins with static imports instead of
require() so it passes no-require-imports; the README copy is updated to
match. README and CONTRIBUTING list pnpm lint with the other checks.

Part of #33
@danielgerlag
danielgerlag merged commit c9ff54b into main Sep 26, 2026
2 checks passed
@danielgerlag
danielgerlag deleted the build/eslint branch September 26, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant