/** * Shared "is this sub-composition file usable?" check. * * `data-composition-src` files are authored by AI agents far more often than * by humans clicking a UI. The dominant real-world failure is a scene worker * that dies mid-write (or a step that references a scene before writing it), * leaving an empty or partial `compositions/scene-*.html` on disk. Historically * this surfaced in three different ways depending on which code path touched * the file first: * * 1. A raw crash inside linkedom's `Document.head` getter — destructuring * `firstElementChild` off a `null` `documentElement` — when the file is * empty or contains no parseable markup. * 2. An actionable-but-late `Error` thrown deep inside the render compiler * (see git history: #1364), which aborted the whole render. * 3. A silent skip (see git history: #1678) that drops the scene from the * output with only a `console.warn`, producing a materially broken * video (missing scene, no error surfaced anywhere) with no clear * signal to the caller. * * This module gives every consumer (lint, render pre-flight, the tolerant * inliner) a single, shared definition of "usable" so they can never * disagree about whether a given file would render something. It lives in * `@hyperframes/parsers` (rather than `@hyperframes/core`, where it * originated) because `@hyperframes/lint` needs it too, and `lint` cannot * depend on `core` — `core` already depends on `lint` — so this shared, * dependency-free check lives in the common ancestor package both `core` * and `lint` already depend on. * * `inlineSubCompositions.ts` (in `@hyperframes/core`) intentionally stays * tolerant (skip + continue) for the preview/studio bundling path, where * partial content while iterating is expected. `lint` and the render * pre-flight check (`packages/producer/src/services/htmlCompiler.ts`) use * this helper to fail loudly and name the exact offending file, because a * render that silently drops a scene is strictly worse than a render that * refuses to start. */ type SubCompositionValidityReason = "empty" | "unparsable" | "no-content" | "no-composition-root"; interface SubCompositionValidity { ok: boolean; /** Present when `ok` is false. */ reason?: SubCompositionValidityReason; /** Human-readable detail suitable for direct inclusion in an error message. */ detail?: string; } /** Minimal shape both linkedom's `Document` and `happy-dom`'s satisfy. */ interface ParsableDocumentLike { documentElement: { outerHTML?: string; } | null; body?: { innerHTML?: string | null; } | null; querySelector(selector: string): { innerHTML?: string | null; } | null; } /** * Check whether `html` (the raw file contents resolved for a * `data-composition-src` reference) is non-empty and parses to a document * that actually contains renderable content. * * Mirrors the content-detection steps in `inlineSubCompositions` exactly * (resolve → parse → find `