mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-31 07:43:18 +08:00
* fix(video-export): bridge play_video element id → media ref Generated-media records are keyed by the element's media ref (gen_vid_…), but a play_video action targets the slide element by its .id. The app-side compiler deps looked media/durations up by the raw element id, so every generated video missed → present:false → no <video> emitted, no bytes collected → the clip vanished from the export (only its static first frame, baked into the base PNG, remained). Build an elementId → mediaRef bridge across all slide scenes in createVideoTimelineDeps (mirroring the live engine's resolveMediaPlaceholderId) and resolve both media() and videoDurationMs() through it. Refs #867. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): re-measure KaTeX fit-scale after fonts settle KaTeX sizes large delimiters (\left\{, \begin{cases}) from its KaTeX_Size faces, which load asynchronously. KatexContent measured its shrink-to-fit scale once on mount, against the fallback font, and never recomputed — so a cold slide snapshot baked a stale scale and the big brace desynced from the piecewise body ("大括号后面的分段函数与大括号错位"). - Re-run the fit-scale measurement on document.fonts.ready and on each `loadingdone`, keeping the shrink-only (cap-at-1) behavior. - slideToPng waits two extra frames before capture so the font-triggered React re-measure commits into the DOM html2canvas reads. Refs #867. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): place spotlight/laser/video on the rendered content box The pure geometry helper resolves an element's authored outer box against a fixed 1000×562.5 base, but the live overlays — and the exported frame PNG — measure the element's `.element-content` box, which for horizontal text is auto-height plus 10px content padding. So a spotlight/laser sat offset from where the text actually renders ("spotlight 与对应元素的位置有偏差", #867 item 5). Add an optional GeometryProbe to the compiler's DI boundary: when supplied, the geometry pass prefers each element's measured content-box geometry (video keeps its rotation from the authored element), degrading to the pure authored-box calc on a miss so the compiler stays deterministic in unit contexts. App side: measureSlideElementGeometry (new, in @openmaic/renderer/snapshot) mounts the slide off-screen and reads the same `.element-content`-against- container box the live SpotlightOverlay uses; createVideoTimelineDeps pre-measures every spotlight/laser/play_video target per scene and serves the probe as a synchronous table lookup. Tests: geometry.test.ts quantifies the outer-box vs content-box delta and proves probe-wins / fallback / rotation-preservation; timeline-deps.test.ts covers the pre-measure wiring. Refs #867. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): decode a first-frame poster for posterless videos A play_video clip only shows during its play window; the rest of the time the static base-frame PNG backs that spot. But html2canvas can't draw a <video>, so the snapshot swaps it for its poster <img> — and generated videos usually carry no poster, leaving the video area blank whenever the clip isn't playing. When a video record has no stored poster bytes, decode the video blob's first frame (seek slightly off 0, draw to a canvas) and set element.poster before the base snapshot, so that spot shows the first frame — the "paused on frame one" look — instead of blank. Falls back to no poster on decode failure / timeout / CORS taint, never failing the export. Refs #867. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(video-export): split subtitles, make burn-in optional, centered modal Burned-in captions used one cue per whole narration paragraph, so a long cue became a 4–6 line block covering the lower half of the slide, and there was no way to turn it off. - Split cues (pure, compiler layer): lib/video-export/split-cue.ts splits each cue on sentence → clause → hard-wrap to ≤~40 CJK-equiv units, distributing the parent window by character weight (last piece pinned to the parent end, sub-1.2s slivers merged). Wired into the timeline pass, so the burned-in overlay and the SRT/VTT sidecar share one split track and can't drift. - Burn-in optional (default off, #867 item 2): emitHyperframes gains burnInSubtitles; off → clean video + sidecar SRT/VTT only. Caption layout hard- clamped to 2 lines so an outlier can't grow tall again. - Download subtitles without rendering: compileSubtitles() runs only the compiler (no assets/zip/service); new useDownloadSubtitles saves an .srt. - Centered modal: video-export-dialog.tsx replaces the cramped dropdown section, grouping Output / Subtitles / Actions / Progress with a stable lifecycle for multi-minute renders. header-controls opens it from an "Export Video" entry; old video-export-menu.tsx removed. i18n keys added across all 8 locales. Refs #867. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: prettier format video-export fidelity changes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: drop unused playVideo helper in timeline-deps test The media-bridge tests assert via deps.assets.media(...) directly, so the playVideo helper (and its PlayVideoAction import) were dead. Flagged by the code-quality bot on #952. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): resolve cross-review findings + lower subtitle band - emit-hyperframes: drop duplicate `display:-webkit-box` on subtitle cues so they start truly `display:none` (a second declaration overrode the `none`, showing every cue stacked at t=0 when burn-in was enabled). - geometry: don't feed the measured content-box AABB to a rotated video clip — it already encloses the rotation, so re-applying `rotate` doubled it. Rotated elements fall back to the authored box (one, un-doubled source of truth). - build-export-zip: extract shared `compileStageIr` so buildExportZip and compileSubtitles can't drift on the timing/assets/geometry wiring. - split-cue: join merged CJK cue pieces without a spurious ASCII space. - subtitles: lower the caption band (bottomRatio 0.055 → 0.01) and hoist the band's magic numbers into a named SUBTITLE constants block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): stop the snapshot onclone override from reshaping KaTeX The export snapshot injects a neutral-kerning / geometricPrecision style over `.slide-renderer-prose *` to keep CJK prose+table glyphs from mis-advancing under html2canvas-pro. KaTeX formulas (output:'html') also live inside `.slide-renderer-prose`, so the override was reshaping their math glyph advances — a burned-in formula rendered subtly differently from the live canvas, which applies no such override. Empirical check (Playwright + system Chrome, cases+fraction formula, native paint vs html2canvas): the override raised the formula's mean per-pixel delta vs the live paint from 120.1 to 139.2 (~16%) and moved ~75% of inked pixels. Restoring native font-kerning/text-rendering for `.katex` subtrees brings the formula's delta back to 120.1 (bit-identical to the no-override capture) while prose and table text keep the override. The residual 120.1 is the deeper html2canvas rasterization gap (vlist/frac-line/delimiter), a separate track. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): render slides via native paint, bundle KaTeX font embed Root-fix the exported-vs-live formula distortion (and the filter/mask/video gaps) by rasterizing slides with native Chrome paint instead of html2canvas's reimplementation. - slideToPng now captures via html-to-image (foreignObject → the same Chrome engine that paints the live classroom rasterizes the same DOM). KaTeX formulas, CSS filter, soft-edge masks, and mixed CJK/Latin text come out as the classroom shows them. html2canvas-pro stays as a fallback (with the filter/mask bakes + KaTeX text-rendering reset) for when native paint can't run — e.g. a cross-origin image taints the canvas. - A foreignObject SVG can't reach the document font registry, so fonts must be inlined. KaTeX math faces are prepended from the bundled woff2 (KATEX_FONT_EMBED_CSS, generated by scripts/generate-katex-fonts.mjs), NOT left to runtime getFontEmbedCSS — which reads cssRules and silently drops a cross-origin KaTeX stylesheet, collapsing a large brace to a fallback glyph (the observed "formula render failure" on e.g. a \begin{cases} with \text{}). A per-face guard verifies every KaTeX face the formula references is embedded, falling back to html2canvas otherwise. Verified in a Playwright spike (native vs html2canvas vs html-to-image): formula inked-delta 143→53; filter/mask/video match native; and with getFontEmbedCSS forced empty, the bundled embed alone still renders the failing formula correctly. Renderer package tests: 226 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): prettier-format the generated KaTeX font-embed module The generator emitted an unformatted TS file (double quotes, long unwrapped CSS literal) that failed CI `prettier --check`. Run the generated output through the repo's Prettier config before writing, so every rebuild produces CI-clean, stable output. Regenerated the committed file to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lint): rename reserved `module` var in katex-fonts generator; ignore .scratch - generate-katex-fonts.mjs assigned to a variable named `module`, tripping @next/next/no-assign-module-variable (the one hard eslint error). Renamed to `fileContents`; generated output is byte-identical (deterministic). - Add `.scratch/**` to the eslint globalIgnores (alongside .claude/.worktrees) so local throwaway verification scaffolding doesn't pollute `pnpm lint`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): address PR #952 review — scene-scoped media bridge, safer cue splitting Resolves the two blocking correctness issues and three follow-ups from the cross-review on #952. - Scope the elementId→mediaRef bridge by scene (was deck-wide/last-writer-wins): slide-local ids like `video_001` now resolve within their own scene. `assets.media` uses its scene arg; `timing.videoDurationMs` resolves via action object identity. Adds a cross-scene collision test. - Stop treating every ASCII period as a sentence boundary: a lone `.` splits only when not between word chars (3.14, v1.2) and not an abbreviation dot (e.g.), collapsing repeated-punctuation runs. Adds decimal/version/abbrev/ellipsis tests. - Share the module-level in-flight guard across ZIP export and subtitle download (extracted to export-in-flight.ts); dialog `busy` folds in `downloading`. - Skip the off-screen geometry render for the subtitles-only path (skipGeometry); duration probes are kept so sidecar cue timing stays in sync with the video. - Align measure.ts font settling with slideToPng: force-load each used face before fonts.ready so measured content-boxes don't bake fallback advance widths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(video-export): prettier-format split-cue test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(video-export): enable beginFrame capture via PRODUCER_BROWSER_GPU_MODE=hardware The producer defaults to `software` GPU mode, which silently force-enables the CPU-bound `Page.captureScreenshot` fallback: SwiftShader pins every core on rasterization and caps frame capture at ~10 fps. Selecting `hardware` keeps Chromium's `HeadlessExperimental.beginFrame` capture active instead. Measured on a 900-frame (30s) clip, no real GPU on the host: - capture ~10 -> ~19 fps, total 100s -> 62s (~38% faster) - CPU peak ~810% -> ~98%; peak mem 2.2 GiB -> 0.9 GiB - static frames pixel-identical; animation frames differ only in sub-pixel edge antialiasing (same-path re-render is deterministic/inf PSNR) No real GPU is required — the win is the faster capture API, not hardware rasterization; beginFrame falls back gracefully if the host lacks a GPU. Also eases the RENDER_JOB_DEADLINE_MS timeout risk on long videos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(video-export): sentence splitting via Intl.Segmenter + abbreviation merge Addresses the PR #952 follow-up: separate sentence-boundary detection from cue shaping and stop growing a custom punctuation heuristic. - splitSentences now delegates boundary detection to Intl.Segmenter (locale- agnostic ICU), the multilingual/CJK baseline. It already preserves 3.14, v1.2, e.g., U.S., ellipses, and CJK 。!?… without any hand-rolled period scan. - ICU still ends a sentence after a titlecase abbreviation before a capitalized word (Dr. Smith, Fig. 3) — the edge case the reviewer flagged. A small abbreviation-aware post-merge re-joins those, driven by an ABBREVIATIONS set. - Semicolons are no longer sentence ends (they join clauses); moved ;; into the clause-level SECONDARY splitter so over-budget sentences still break there. - Cue shaping (budget / clause / hard-wrap / weighted timing) is unchanged, so detection and shaping are now distinct stages. Not using `sbd` as suggested: lib/video-export/*.ts sits behind the machine- enforced eslint purity boundary (imports limited to @openmaic/dsl, zod, ../choreography, siblings), so a new npm dep is rejected. Intl.Segmenter is a pure Node >=20.9 builtin, matching the reviewer's "small abbreviation-aware post-merge layer" alternative. Tests: add Dr. Smith / Fig. 3 / U.S. coverage; the rhetorical ellipsis now stays one under-budget cue (was split); add a semicolon over-budget case. 22/22 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(video-export): prettier-format split-cue sentence refactor Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n(es-MX): add 7 subtitle/video export keys to align with en-US main added the Spanish (Mexico) locale (#942) after this branch's subtitle keys landed, so es-MX was the only locale missing export.subtitles* and export.videoBurnSubtitles* — which failed the i18n key-alignment CI check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): restrict title post-merge so it never deletes real boundaries Addresses the blocking re-review on the Intl.Segmenter change: the abbreviation post-merge unconditionally rejoined the next ICU segment whenever the previous one ended in a listed abbreviation, which erased genuine sentence boundaries — `It was made by Acme Inc. Next point.` collapsed into one cue, and even an explicit newline (`Acme Inc.\nNext topic`) was rejoined. The merge now fires only for the case ICU actually mis-splits — a title (Mr/Mrs/Ms/Dr/Prof/Sr/Jr) before a capitalized proper noun — and only when: - the previous segment does not end at a newline (hard boundary), and - the next segment starts with a capital letter. Sentence-trailing abbreviations (Inc./Ltd./etc./vs./No.) are dropped from the set entirely: they legitimately end sentences, and ICU already keeps a title before a number (No. 5, Fig. 3) or a lowercase continuation (etc. and…) whole without any merge, so nothing there needs rejoining. Regression tests: Inc./etc. boundary preserved, newline-after-title preserved, lowercase continuation kept whole, plus the retained Dr. Smith / Fig. 3 / U.S. cases. 26 split-cue tests pass; full video-export suite 123 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
368 lines
17 KiB
JavaScript
368 lines
17 KiB
JavaScript
import { defineConfig, globalIgnores } from 'eslint/config';
|
|
import nextVitals from 'eslint-config-next/core-web-vitals';
|
|
import nextTs from 'eslint-config-next/typescript';
|
|
|
|
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',
|
|
{
|
|
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',
|
|
{
|
|
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.',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
// 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.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
// Pass files — lib/video-export/passes/**: 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}'],
|
|
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.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
|
|
export default eslintConfig;
|