671 lines
31 KiB
JavaScript
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;
|