Files
openmaic/OpenMAIC/eslint.config.mjs
2026-08-16 14:58:47 +08:00

671 lines
31 KiB
JavaScript

import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
import nextTs from 'eslint-config-next/typescript';
// The AI SDK must be reached only through callLLM / streamLLM in lib/ai/llm.ts
// (#1003). Static and namespace imports are handled by
// @typescript-eslint/no-restricted-imports further down; a DYNAMIC import is an
// ImportExpression, which no-restricted-imports cannot see, so it needs
// no-restricted-syntax. That key is configured per-block and flat config replaces
// rule options rather than merging them, so the ban lives here and is spread into
// every block that sets the key, instead of one repo-wide block that would
// silently drop those blocks' own module boundaries.
const AI_SDK_DYNAMIC_IMPORT_BAN = [
{
selector: "ImportExpression > Literal[value='ai']",
message:
"Call the model through callLLM / streamLLM in @/lib/ai/llm instead of importing the AI SDK dynamically. A dynamic import('ai') reaches the same generateText / streamText and skips usage accounting, the LLM_THINKING_DISABLED kill switch and per-provider thinking resolution.",
},
{
selector: "ImportExpression > TemplateLiteral > TemplateElement[value.cooked='ai']",
message:
'Call the model through callLLM / streamLLM in @/lib/ai/llm instead of importing the AI SDK dynamically (template-literal form of the same bypass).',
},
];
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
// Third-party / vendored packages (not our code):
'packages/docs/**',
'packages/mathml2omml/**',
'packages/pptxgenjs/**',
// Our own @openmaic/* packages: lint the source, but skip build output,
// installed deps, and the vendored JS sources under importer/src1.
'packages/@openmaic/*/dist/**',
'packages/@openmaic/*/node_modules/**',
'packages/@openmaic/importer/src1/**',
// Generated importer bundle copied into public/ by the sync script (postinstall):
'public/vendor/**',
// Claude Code local files:
'.claude/**',
'.superpowers/**',
'.worktrees/**',
'.scratch/**',
// Playwright e2e tests (not React code):
'e2e/**',
// Isolated MP4 render service: its own package, tsconfig, and Node-only
// deps (@hyperframes/producer). Linted/typechecked under render-service/.
'render-service/**',
]),
{
rules: {
// Dynamic AI-generated image URLs from various providers are incompatible
// with next/image (requires known dimensions and whitelisted domains).
'@next/next/no-img-element': 'off',
// Allow unused vars/args prefixed with _ (common convention for intentionally
// unused destructured values, callback params, etc.)
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
},
],
},
},
// Package boundary (machine-enforced): @openmaic/renderer is a standalone,
// app-agnostic package. It must never reach back into the host app through
// the `@/…` path alias, so a deadline can't punch a "temporary"
// store/undo/media dependency through the package API. Host concerns
// (document + undo ownership, media resolution, i18n, hotkeys) are injected
// via props/callbacks instead.
//
// Policy: the package must contain NO `@/…` path-alias string at all. `@/` is
// exclusively the host-app import alias, and the package authors zero such
// strings, so we match the string prefix wherever it appears rather than
// chasing individual call shapes. This is complete against every single-literal
// module-reference form — static / `import type` / `export … from`, dynamic
// `import()`, `require()`, `require.resolve()`, `import.meta.resolve()`, their
// computed-property (`require['resolve']`) and template-literal variants, and
// string-concatenation operands (`'@/lib/' + x`) — because all of them contain
// a `@/` literal. One rule, one report per violation.
//
// Out of scope (undecidable by lint, evasion-only): a specifier assembled
// entirely from non-`@/` pieces (`'@' + '/x'`, a variable) and relative parent
// escapes (`../../app`). Those are caught by building/publishing the package in
// isolation (only `@openmaic/dsl` + declared peers external), not by this rule.
{
files: ['packages/@openmaic/renderer/**/*.{ts,tsx,js,jsx,mjs,cjs}'],
rules: {
'no-restricted-syntax': [
'error',
...AI_SDK_DYNAMIC_IMPORT_BAN,
{
selector: 'Literal[value=/^@\\//]',
message:
'@openmaic/renderer must not reference a host-app path (@/…). This package authors no `@/…` strings — depend only on @openmaic/dsl and declared peers, and inject host concerns (stores, undo, media resolution, i18n, hotkeys) via props/callbacks.',
},
{
selector: 'TemplateElement[value.cooked=/^@\\//]',
message:
'@openmaic/renderer must not reference a host-app path (@/…) in a template literal. Depend only on @openmaic/dsl and declared peers; inject host concerns via props/callbacks.',
},
],
},
},
// Package boundary (machine-enforced): @openmaic/storage is a standalone,
// app-agnostic persistence package. Same policy as the @openmaic/renderer
// boundary above — it must contain NO `@/…` host-app path-alias string, so a
// deadline can't punch a "temporary" host dependency through the package API.
// It depends only on @openmaic/dsl; host wiring (which store persists where)
// lives in the app, which imports the package, never the reverse.
{
files: ['packages/@openmaic/storage/**/*.{ts,tsx,js,jsx,mjs,cjs}'],
rules: {
'no-restricted-syntax': [
'error',
...AI_SDK_DYNAMIC_IMPORT_BAN,
{
selector: 'Literal[value=/^@\\//]',
message:
'@openmaic/storage must not reference a host-app path (@/…). This package authors no `@/…` strings — depend only on @openmaic/dsl. The app wires its stores through the package, not the reverse.',
},
{
selector: 'TemplateElement[value.cooked=/^@\\//]',
message:
'@openmaic/storage must not reference a host-app path (@/…) in a template literal. Depend only on @openmaic/dsl.',
},
],
},
},
// Package boundary (machine-enforced): @openmaic/generation is a standalone,
// app-agnostic package. Every package code directory is covered so future
// scripts and tooling cannot bypass the boundary. The leaf dependency list
// mirrors the package's declared dependencies and is widened only together
// with package.json.
{
files: ['packages/@openmaic/generation/**/*.{ts,tsx,js,jsx,mjs,cjs}'],
rules: {
'no-restricted-syntax': [
'error',
...AI_SDK_DYNAMIC_IMPORT_BAN,
{
selector: 'Literal[value=/^@\\//]',
message:
'@openmaic/generation must not reference a host-app path (@/…). Depend only on @openmaic/dsl, Node built-ins, and relative modules.',
},
{
selector: 'TemplateElement[value.cooked=/^@\\//]',
message:
'@openmaic/generation must not reference a host-app path (@/…) in a template literal.',
},
{
selector:
'ImportDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|(jsonrepair|katex|nanoid|partial-json)(\\/|$)|node:|\\.\\.?\\/).+/]',
message:
'@openmaic/generation may import only from @openmaic/dsl, approved leaf runtime dependencies, Node built-ins, or relative modules (./… or ../…).',
},
{
selector:
'ExportNamedDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|(jsonrepair|katex|nanoid|partial-json)(\\/|$)|node:|\\.\\.?\\/).+/]',
message:
'@openmaic/generation may re-export only from @openmaic/dsl, approved leaf runtime dependencies, Node built-ins, or relative modules (./… or ../…).',
},
{
selector:
'ExportAllDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|(jsonrepair|katex|nanoid|partial-json)(\\/|$)|node:|\\.\\.?\\/).+/]',
message:
'@openmaic/generation may re-export only from @openmaic/dsl, approved leaf runtime dependencies, Node built-ins, or relative modules (./… or ../…).',
},
{
selector: 'ImportExpression',
message:
'@openmaic/generation must use static imports from @openmaic/dsl, Node built-ins, or relative modules.',
},
{
selector: "CallExpression[callee.name='require']",
message: '@openmaic/generation must not use require().',
},
],
},
},
// Flat config replaces a rule's complete options in later matching blocks.
// Repeat every restriction for tests (and their Vitest config), changing only
// the static import allowlist to add the package public entry and Vitest.
{
files: [
'packages/@openmaic/generation/test/**/*.{ts,tsx,js,jsx,mjs,cjs}',
'packages/@openmaic/generation/vitest.config.ts',
],
rules: {
'no-restricted-syntax': [
'error',
...AI_SDK_DYNAMIC_IMPORT_BAN,
{
selector: 'Literal[value=/^@\\//]',
message:
'@openmaic/generation tests and test config must not reference a host-app path (@/…). Read app prompt assets as files for parity checks.',
},
{
selector: 'TemplateElement[value.cooked=/^@\\//]',
message:
'@openmaic/generation tests and test config must not reference a host-app path (@/…) in a template literal.',
},
{
selector:
'ImportDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|@openmaic\\/generation(\\/|$)|node:|vitest(\\/|$)|\\.\\.?\\/).+/]',
message:
'@openmaic/generation tests and test config may import only @openmaic/dsl, the package public entry, Node built-ins, Vitest, or relative modules (./… or ../…).',
},
{
selector:
'ExportNamedDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|@openmaic\\/generation(\\/|$)|node:|vitest(\\/|$)|\\.\\.?\\/).+/]',
message:
'@openmaic/generation tests and test config may re-export only @openmaic/dsl, the package public entry, Node built-ins, Vitest, or relative modules (./… or ../…).',
},
{
selector:
'ExportAllDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|@openmaic\\/generation(\\/|$)|node:|vitest(\\/|$)|\\.\\.?\\/).+/]',
message:
'@openmaic/generation tests and test config may re-export only @openmaic/dsl, the package public entry, Node built-ins, Vitest, or relative modules (./… or ../…).',
},
{
selector: 'ImportExpression',
message: '@openmaic/generation tests and test config must use static imports.',
},
{
selector: "CallExpression[callee.name='require']",
message: '@openmaic/generation tests and test config must not use require().',
},
],
},
},
// Module boundary (machine-enforced): lib/choreography is the shared
// orchestration spec (timing + action timeline). It lives in the app (not a
// package) because its semantics co-evolve with the playback engine, but it
// must stay pure so the classroom-video exporter can interpret it in a pure
// Node environment. Two guards, mirroring the package boundaries above:
// 1. NO `@/…` host-app path-alias string — it authors none; it depends only
// on @openmaic/dsl (types + the fire-and-forget partition) and relative
// siblings. The app and exporter import it, never the reverse.
// 2. NO React / DOM / render-backend runtime import (react, react-dom, gsap,
// framer-motion, motion) — these are bare specifiers the `@/` rule can't
// see. A descriptor describes animation; it never renders it.
{
files: ['lib/choreography/**/*.{ts,tsx,js,jsx,mjs,cjs}'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Literal[value=/^@\\//]',
message:
'lib/choreography must not reference a host-app path (@/…). It authors no `@/…` strings — depend only on @openmaic/dsl and relative siblings, so the exporter can interpret it in pure Node. The app and exporter import it, not the reverse.',
},
{
selector: 'TemplateElement[value.cooked=/^@\\//]',
message:
'lib/choreography must not reference a host-app path (@/…) in a template literal. Depend only on @openmaic/dsl and relative siblings.',
},
// Import allowlist (static imports/re-exports): the ONLY permitted
// sources are `@openmaic/dsl`(/subpaths), `zod`, and in-folder relatives
// (`./…`). Anything else — a parent-escape `../…` reaching back into the
// app, or any other bare package — fails. Enforced on Import/Export
// source string nodes via a negative-lookahead so the guard is a true
// allowlist, not a blocklist of known-bad names.
{
selector:
'ImportDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|zod(\\/|$)|\\.\\/).+/]',
message:
'lib/choreography may import only from @openmaic/dsl, zod, or in-folder relatives (./…). No parent-escape (../…) into the app and no other packages — keep it pure so the exporter runs in plain Node.',
},
{
selector:
'ExportNamedDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|zod(\\/|$)|\\.\\/).+/]',
message:
'lib/choreography may re-export only from @openmaic/dsl, zod, or in-folder relatives (./…).',
},
{
selector:
'ExportAllDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|zod(\\/|$)|\\.\\/).+/]',
message:
'lib/choreography may re-export only from @openmaic/dsl, zod, or in-folder relatives (./…).',
},
// No dynamic import() or require() — they bypass the static allowlist and
// can pull in a render backend at runtime.
{
selector: 'ImportExpression',
message:
'lib/choreography must not use dynamic import() — it bypasses the static import allowlist. Use a top-level import from @openmaic/dsl, zod, or a relative sibling.',
},
{
selector: "CallExpression[callee.name='require']",
message:
'lib/choreography must not use require() — it bypasses the static import allowlist.',
},
],
'no-restricted-imports': [
'error',
{
patterns: [
{
group: [
'react',
'react-dom',
'react/*',
'react-dom/*',
'gsap',
'gsap/*',
'framer-motion',
'motion',
'motion/*',
],
message:
'lib/choreography must stay render-backend-agnostic (pure Node): no React / DOM / GSAP / framer-motion. It describes timing and animation as data; the app effect components and the exporter render it.',
},
],
},
],
},
},
// Module boundary (machine-enforced): lib/video-export is the classroom-video
// compiler (issue #864). It produces the VideoTimeline IR with pure passes and
// must stay interpretable in pure Node (no FFmpeg / Chrome / DOM), so runtime
// app state enters only through the injected TimingProbe / AssetSource. Guards,
// mirroring lib/choreography: no `@/…` host path, no React/DOM/render backend,
// no dynamic import()/require(), and an import-source allowlist.
//
// The relative-path allowance is DEPTH-SPECIFIC, because a single `../…` means
// different things at different depths: from a module-root file it escapes the
// module (`lib/video-export/../action` = `lib/action`), but from `passes/` it
// stays inside (`lib/video-export/passes/../ir` = `lib/video-export/ir`). So
// the boundary is split into two disjoint file scopes (root `*` does not match
// `passes/`), each with its own source allowlist; everything else is shared and
// duplicated verbatim.
//
// Root files — lib/video-export/*.ts: relative imports may only be `./…`
// (children) or the declared cross-module dep `../choreography`. Any other
// `../…` (e.g. `../action/engine`) escapes the module and is rejected.
{
files: ['lib/video-export/*.{ts,tsx,js,jsx,mjs,cjs}'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Literal[value=/^@\\//]',
message:
'lib/video-export must not reference a host-app path (@/…). Live app state enters only through the injected TimingProbe / AssetSource — depend only on @openmaic/dsl, zod, ../choreography, and relative siblings, so the compiler runs in pure Node.',
},
{
selector: 'TemplateElement[value.cooked=/^@\\//]',
message:
'lib/video-export must not reference a host-app path (@/…) in a template literal. Depend only on @openmaic/dsl, zod, ../choreography, and relative siblings.',
},
{
selector:
'ImportDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|zod(\\/|$)|\\.\\/|\\.\\.\\/choreography(\\/|$)).+/]',
message:
'lib/video-export root files may import only from @openmaic/dsl, zod, ../choreography, or in-module children (./…). A ../… into anything but ../choreography escapes the module and is rejected.',
},
{
selector:
'ExportNamedDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|zod(\\/|$)|\\.\\/|\\.\\.\\/choreography(\\/|$)).+/]',
message:
'lib/video-export root files may re-export only from @openmaic/dsl, zod, ../choreography, or in-module children (./…).',
},
{
selector:
'ExportAllDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|zod(\\/|$)|\\.\\/|\\.\\.\\/choreography(\\/|$)).+/]',
message:
'lib/video-export root files may re-export only from @openmaic/dsl, zod, ../choreography, or in-module children (./…).',
},
{
selector: 'ImportExpression',
message:
'lib/video-export must not use dynamic import() — it bypasses the static import allowlist. Use a top-level import from @openmaic/dsl, zod, ../choreography, or a relative sibling.',
},
{
selector: "CallExpression[callee.name='require']",
message:
'lib/video-export must not use require() — it bypasses the static import allowlist.',
},
],
'no-restricted-imports': [
'error',
{
patterns: [
{
group: [
'react',
'react-dom',
'react/*',
'react-dom/*',
'gsap',
'gsap/*',
'framer-motion',
'motion',
'motion/*',
],
message:
'lib/video-export must stay render-backend-agnostic (pure Node): no React / DOM / GSAP / framer-motion. The compiler emits the IR as data; the downstream Hyperframes emitter renders it.',
},
],
},
],
},
},
// Nested files — lib/video-export/{passes,legacy}/**: one level deeper, so a single `../…`
// reaches a module-root file and STAYS inside the module; only a two-level
// `../../…` escapes, and that is allowed solely for `../../choreography`.
{
files: [
'lib/video-export/passes/**/*.{ts,tsx,js,jsx,mjs,cjs}',
'lib/video-export/legacy/**/*.{ts,tsx,js,jsx,mjs,cjs}',
],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Literal[value=/^@\\//]',
message:
'lib/video-export must not reference a host-app path (@/…). Live app state enters only through the injected TimingProbe / AssetSource — depend only on @openmaic/dsl, zod, ../../choreography, and relative siblings, so the compiler runs in pure Node.',
},
{
selector: 'TemplateElement[value.cooked=/^@\\//]',
message:
'lib/video-export must not reference a host-app path (@/…) in a template literal. Depend only on @openmaic/dsl, zod, ../../choreography, and relative siblings.',
},
// `./…` (children) and a single `../…` (a pass reaching a module-root
// file, which stays inside lib/video-export) are allowed; a two-level
// `../../…` escape is rejected UNLESS it is exactly `../../choreography`.
{
selector:
'ImportDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|zod(\\/|$)|\\.\\/|\\.\\.\\/\\.\\.\\/choreography(\\/|$)|\\.\\.\\/(?!\\.\\.\\/)).+/]',
message:
'lib/video-export passes may import only from @openmaic/dsl, zod, ../../choreography, or in-module relatives (./… or a single ../… that stays inside the module). A ../../ escape into the rest of the app is rejected.',
},
{
selector:
'ExportNamedDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|zod(\\/|$)|\\.\\/|\\.\\.\\/\\.\\.\\/choreography(\\/|$)|\\.\\.\\/(?!\\.\\.\\/)).+/]',
message:
'lib/video-export passes may re-export only from @openmaic/dsl, zod, ../../choreography, or in-module relatives.',
},
{
selector:
'ExportAllDeclaration > Literal.source[value=/^(?!@openmaic\\/dsl(\\/|$)|zod(\\/|$)|\\.\\/|\\.\\.\\/\\.\\.\\/choreography(\\/|$)|\\.\\.\\/(?!\\.\\.\\/)).+/]',
message:
'lib/video-export passes may re-export only from @openmaic/dsl, zod, ../../choreography, or in-module relatives.',
},
{
selector: 'ImportExpression',
message:
'lib/video-export must not use dynamic import() — it bypasses the static import allowlist. Use a top-level import from @openmaic/dsl, zod, ../../choreography, or a relative sibling.',
},
{
selector: "CallExpression[callee.name='require']",
message:
'lib/video-export must not use require() — it bypasses the static import allowlist.',
},
],
'no-restricted-imports': [
'error',
{
patterns: [
{
group: [
'react',
'react-dom',
'react/*',
'react-dom/*',
'gsap',
'gsap/*',
'framer-motion',
'motion',
'motion/*',
],
message:
'lib/video-export must stay render-backend-agnostic (pure Node): no React / DOM / GSAP / framer-motion. The compiler emits the IR as data; the downstream Hyperframes emitter renders it.',
},
],
},
],
},
},
// Hyperframes emitter boundary. This subtree stays pure and backend-text-only:
// it may reach other video-export modules through one-level relatives, and it
// has one intentional app-module dependency on the existing pure Quiz math
// renderer so classroom and exported formulas cannot drift. Every other
// two-level escape is rejected.
{
files: ['lib/video-export/emit-hyperframes/**/*.{ts,tsx,js,jsx,mjs,cjs}'],
rules: {
'no-restricted-syntax': [
'error',
{
selector:
'ImportDeclaration > Literal.source[value=/^(?!\\.\\/|\\.\\.\\/(?!\\.\\.\\/)|\\.\\.\\/\\.\\.\\/quiz\\/math-text$).+/]',
message:
'The Hyperframes emitter may import only in-module relatives (./… or one ../…) and the shared pure Quiz renderer ../../quiz/math-text.',
},
{
selector:
'ExportNamedDeclaration > Literal.source[value=/^(?!\\.\\/|\\.\\.\\/(?!\\.\\.\\/)|\\.\\.\\/\\.\\.\\/quiz\\/math-text$).+/]',
message:
'The Hyperframes emitter may re-export only in-module relatives (./… or one ../…) and ../../quiz/math-text.',
},
{
selector:
'ExportAllDeclaration > Literal.source[value=/^(?!\\.\\/|\\.\\.\\/(?!\\.\\.\\/)|\\.\\.\\/\\.\\.\\/quiz\\/math-text$).+/]',
message:
'The Hyperframes emitter may re-export only in-module relatives (./… or one ../…) and ../../quiz/math-text.',
},
{
selector: 'ImportExpression',
message:
'The Hyperframes emitter must not use dynamic import() — it bypasses the static import allowlist.',
},
{
selector: "CallExpression[callee.name='require']",
message:
'The Hyperframes emitter must not use require() — it bypasses the static import allowlist.',
},
],
},
},
// PBL v2 operations boundary: leaf/shared kernel operations may be used by
// composite runtime operations, but the kernel must never reach back into
// operations/runtime. Match the module string in every literal form so
// static imports, re-exports, dynamic imports, and require-like calls cannot
// quietly invert the boundary.
{
files: ['lib/pbl/v2/operations/kernel/**/*.{ts,tsx,js,jsx,mjs,cjs}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: [
'../runtime',
'../runtime/*',
'@/lib/pbl/v2/operations/runtime',
'@/lib/pbl/v2/operations/runtime/*',
],
message:
'PBL v2 kernel operations must not import operations/runtime. Runtime operations may depend on the project-definition kernel, never the reverse.',
},
],
},
],
'no-restricted-syntax': [
'error',
...AI_SDK_DYNAMIC_IMPORT_BAN,
{
selector: 'Literal[value=/\\/runtime(?:\\/|$)/]',
message:
'PBL v2 kernel operations must not import operations/runtime. Runtime operations may depend on the project-definition kernel, never the reverse.',
},
{
selector: 'TemplateElement[value.cooked=/\\/runtime(?:\\/|$)/]',
message:
'PBL v2 kernel operations must not import operations/runtime. Runtime operations may depend on the project-definition kernel, never the reverse.',
},
],
},
},
// Single LLM entry point (machine-enforced): server-side model calls go through
// `callLLM` / `streamLLM` in lib/ai/llm.ts. That wrapper is where usage
// accounting (`recordUsage`), the `LLM_THINKING_DISABLED` kill switch, and
// per-provider thinking resolution live — a direct `generateText` / `streamText`
// silently opts out of all three, and the opt-out is invisible at the call site.
// The PBL v2 runtime drifted exactly this way (#1003): five direct calls meant
// zero usage records for the busiest traffic in the product, plus three
// different meanings for one thinking config.
//
// The rule uses the @typescript-eslint variant deliberately: the base
// `no-restricted-imports` is already configured for lib/choreography and
// lib/video-export, and flat config REPLACES a rule's options per key rather
// than merging them, so reusing that key here would silently drop those
// module-boundary bans. Different key, no interference.
//
// Every reachable import form is covered, verified by feeding each one to
// eslint rather than assumed:
// - `import { streamText } from 'ai'` → this rule
// - `import * as ai from 'ai'` → this rule too; ESLint reports a
// namespace import when `importNames` is set, since the namespace would
// carry the restricted name
// - `require('ai')` → the repo-wide
// `@typescript-eslint/no-require-imports` (inherited from
// eslint-config-next/typescript) already forbids require() anywhere
// - `await import('ai')` → the no-restricted-syntax block below
// An `eslint-disable` comment defeats any of them, which is the point: the
// bypass has to be written down where a reviewer sees it.
//
// Scope is every linted source extension, matching the module-boundary blocks
// above — NOT just ts/tsx. An earlier revision guarded only ts/tsx, which left
// `app/api/route.js` and `scripts/*.mjs` free to import the SDK; review caught
// it. tests/lint-llm-entry-guard.test.ts pins the whole matrix so the scope
// cannot quietly narrow again.
{
files: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'],
ignores: [
// The entry point itself.
'lib/ai/llm.ts',
// Offline harnesses and a test whose subject IS the SDK: not server request
// paths, nothing to account for, and the integration test must be able to
// call the raw SDK to assert what the wrapper is built on.
'eval/**',
'tests/**',
],
rules: {
'@typescript-eslint/no-restricted-imports': [
'error',
{
paths: [
{
name: 'ai',
importNames: ['generateText', 'streamText'],
message:
'Call the model through callLLM / streamLLM in @/lib/ai/llm instead of the AI SDK directly — that is where usage accounting, the LLM_THINKING_DISABLED kill switch, and per-provider thinking resolution are applied. Both wrappers pass every SDK option straight through, and take an optional per-call thinking config.',
},
],
},
],
},
},
// Same boundary, dynamic form. This block cannot match files covered by the
// package and module boundary blocks above that also set no-restricted-syntax
// (flat config replaces rule options per key, so it would drop their module
// boundaries), hence the ignores.
// Every ignored directory is nonetheless covered, and covered by a rule rather
// than by an argument:
// - packages/@openmaic/renderer, packages/@openmaic/storage, and
// packages/@openmaic/generation — the same
// AI_SDK_DYNAMIC_IMPORT_BAN is spread into their own blocks above. An earlier
// revision left them out on the reasoning that they are built in isolation
// against @openmaic/dsl; review showed `void import('ai')` under the renderer
// source path passing lint, which is exactly why that reasoning was not good
// enough.
// - lib/choreography, lib/video-export — their blocks ban EVERY
// ImportExpression outright, which subsumes this one.
{
files: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'],
ignores: [
'lib/ai/llm.ts',
'eval/**',
'tests/**',
// Blocks above that configure no-restricted-syntax for their own boundary.
'lib/choreography/**',
'lib/video-export/**',
'lib/pbl/v2/operations/kernel/**',
'packages/@openmaic/renderer/**',
'packages/@openmaic/storage/**',
'packages/@openmaic/generation/**',
],
rules: {
'no-restricted-syntax': ['error', ...AI_SDK_DYNAMIC_IMPORT_BAN],
},
},
]);
export default eslintConfig;