mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-28 16:35:27 +08:00
Add @openmaic/geometry, a browser-independent validator for SVG path syntax, finite view boxes and transforms and bounded resources, with structured errors and shared shape-to-text recovery. Generation, rendering, both editors and PPTX export use it: invalid shapes with a label render as text, unlabelled ones are omitted, stored records are not rewritten, and recovered shapes cannot be resized. Imported PPTX labels keep their own insets. Bumps generation 0.3.22, renderer 0.1.16, editor 0.0.12 and geometry 0.1.0. Closes #1851 Co-authored-by: cosarah <cosarah@users.noreply.github.com>
671 lines
32 KiB
JavaScript
671 lines
32 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, @openmaic/geometry, 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|geometry)(\\/|$)|(jsonrepair|katex|nanoid|parse5|partial-json)(\\/|$)|node:|\\.\\.?\\/).+/]',
|
|
message:
|
|
'@openmaic/generation may import only from @openmaic/dsl, @openmaic/geometry, approved leaf runtime dependencies, Node built-ins, or relative modules (./… or ../…).',
|
|
},
|
|
{
|
|
selector:
|
|
'ExportNamedDeclaration > Literal.source[value=/^(?!@openmaic\\/(dsl|geometry)(\\/|$)|(jsonrepair|katex|nanoid|parse5|partial-json)(\\/|$)|node:|\\.\\.?\\/).+/]',
|
|
message:
|
|
'@openmaic/generation may re-export only from @openmaic/dsl, @openmaic/geometry, approved leaf runtime dependencies, Node built-ins, or relative modules (./… or ../…).',
|
|
},
|
|
{
|
|
selector:
|
|
'ExportAllDeclaration > Literal.source[value=/^(?!@openmaic\\/(dsl|geometry)(\\/|$)|(jsonrepair|katex|nanoid|parse5|partial-json)(\\/|$)|node:|\\.\\.?\\/).+/]',
|
|
message:
|
|
'@openmaic/generation may re-export only from @openmaic/dsl, @openmaic/geometry, approved leaf runtime dependencies, Node built-ins, or relative modules (./… or ../…).',
|
|
},
|
|
{
|
|
selector: 'ImportExpression',
|
|
message:
|
|
'@openmaic/generation must use static imports from @openmaic/dsl, @openmaic/geometry, 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|geometry)(\\/|$)|@openmaic\\/generation(\\/|$)|node:|vitest(\\/|$)|\\.\\.?\\/).+/]',
|
|
message:
|
|
'@openmaic/generation tests and test config may import only @openmaic/dsl, @openmaic/geometry, the package public entry, Node built-ins, Vitest, or relative modules (./… or ../…).',
|
|
},
|
|
{
|
|
selector:
|
|
'ExportNamedDeclaration > Literal.source[value=/^(?!@openmaic\\/(dsl|geometry)(\\/|$)|@openmaic\\/generation(\\/|$)|node:|vitest(\\/|$)|\\.\\.?\\/).+/]',
|
|
message:
|
|
'@openmaic/generation tests and test config may re-export only @openmaic/dsl, @openmaic/geometry, the package public entry, Node built-ins, Vitest, or relative modules (./… or ../…).',
|
|
},
|
|
{
|
|
selector:
|
|
'ExportAllDeclaration > Literal.source[value=/^(?!@openmaic\\/(dsl|geometry)(\\/|$)|@openmaic\\/generation(\\/|$)|node:|vitest(\\/|$)|\\.\\.?\\/).+/]',
|
|
message:
|
|
'@openmaic/generation tests and test config may re-export only @openmaic/dsl, @openmaic/geometry, 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;
|