Commit Graph

58 Commits

Author SHA1 Message Date
kashorin 68f1693645 refactor: publish video-domain packages independently for 0.3.1 2026-10-09 18:13:50 +08:00
kashorin d7b38b7094 refactor: make caption cues authored content 2026-10-09 00:34:14 +08:00
kashorin a4c25ea76b refactor: make narrative projection an explicit author relation 2026-10-08 15:42:44 +08:00
kashorin 9559574f74 refactor: make caption timing a complete unit table 2026-10-08 14:13:14 +08:00
kashorin b8577391a9 build: assemble the 0.3 distribution release closure 2026-10-07 20:45:22 +08:00
kashorin 190c54c435 refactor: close temporal domain ownership 2026-10-07 20:44:28 +08:00
kashorin 205bfeb98d refactor: establish the adaptive composition model 2026-10-07 20:44:28 +08:00
kashorin ce22da565d test(runtime): separate scale workloads from routine regressions 2026-09-14 19:48:20 +08:00
kashorin b679e32192 refactor: unify adaptive timeline and presentation composition
Place prepared Takes on an independent Timeline with gaps and overlaps. Present picture, sound and captions through explicit Use rules while preserving source timing and project-authored styles.

Unify Companion bands, parameter ownership and source writeback in Studio. Migrate components, examples and authoring guidance, remove retired Speech/Semantic Track interfaces, and retain protocol identifiers at @1.

Validation: repository suite (786 tests; six sandbox-blocked tests passed on permitted rerun), 43 Studio tests after audit fixes, TypeScript, public declarations, four example-package builds, chat Source check and npm file inventory.
2026-09-13 01:01:47 +08:00
rpone a7a7e8fe95 ci: fail a stalled test instead of holding the runner
A Windows job on f80a562 sat for 97 minutes after its last output. The suite had
reached "one local Worker admits later Builds while preserving shared Endpoint
capacity", whose poll for two terminal Build outcomes carried no deadline; one
Build did not complete on that run, so the loop polled every 5 ms until the job
was cancelled. Re-running the same commit passed, so the stall is intermittent
and its cause in the Worker launch path is not yet identified.

Three bounds now apply. The poll fails after 30 s and prints each Build's last
outcome. `node --test` receives `--test-timeout=120000`, well above the 5.75 s
slowest test measured on that run. The job takes `timeout-minutes: 20`.
2026-09-10 07:36:49 +08:00
kashorin e90b070c33 feat(composition): support authored clocks alongside semantic timelines 2026-09-09 23:38:46 +08:00
kashorin 14b6146bf9 test: keep checks tied to behavior 2026-09-08 17:20:20 +08:00
kashorin 0dfee16c2e test: respect host paths and media tools 2026-09-07 05:22:43 +08:00
kashorin 6d5a56c043 feat: complete public extension and transparent media paths 2026-09-07 04:34:42 +08:00
kashorin 4c6af5860f refactor: remove hidden validation side paths 2026-09-07 04:25:42 +08:00
kashorin 02f21dfd92 merge: align OpenAgents skill distribution 2026-09-03 18:56:33 +08:00
QuanCheng 1bea08fd30 refactor(skill): move the skill to skills/hypit and link it per agent
The skill was the real directory at .agents/skills/hypit, with .claude/skills and
.codex/skills as container symlinks pointing at it. That kept one copy, but it made
.agents the de-facto main location and the two agent directories its dependents, and
because the whole skills directory was the link, neither agent could hold a skill of
its own without it landing in the shared directory.

The single copy now lives at skills/hypit, an ordinary repository source alongside
docs, packages and services. Each agent gets a leaf link to it:

  .claude/skills/hypit -> ../../skills/hypit
  .codex/skills/hypit  -> ../../skills/hypit

Claude and Codex are peers, both agent directories stay real directories that can
still take agent-specific skills, and the links are relative so a clone works from
any path. Both Claude Code and the Codex CLI resolve a leaf link of this shape.

A neutral, dot-free location also lets skill catalogs install this skill: several
reject a source path whose segments begin with a dot, so .agents/skills/hypit could
not be addressed at all.

The move drops one directory level, so the fourteen repository-root references in
references/ lose one segment (../../../../docs -> ../../../docs). Paths naming the
skill are updated in reference-video-tools, references/host-setup.md and the
reachability test's skillRoot.

Verified against main: --links reports the same six pre-existing breakages and no
new ones, --orphans is 0 on both, and both --route sentence sets are identical once
the path prefix and the relative-depth rewrite are normalized — the move is a no-op
for a reader, which is what test/hypit-skill-reachability.mjs exists to check.
2026-09-03 06:54:16 +00:00
kashorin fd2387b738 refactor(build): finalize result-oriented execution model 2026-09-03 13:28:06 +08:00
kashorin e8dc1d07b6 refactor(runtime): replace content-addressed artifacts with resources 2026-09-03 13:25:57 +08:00
rpone 1825839ece Include SVS manifest in spatial test closures 2026-08-29 20:34:48 +08:00
kashorin 75c4f61be9 merge: integrate main reconstruction tooling 2026-08-26 19:06:02 +08:00
kashorin a4c290b032 refactor: preserve semantic identity through video time 2026-08-26 03:05:55 +08:00
rponeawa 232e552217 fix: a batch round is fanned out in one place, and every task says how it is answered (#85)
`hypit-skill-reachability.mjs` started two child processes without `windowsHide`, which the
repository-hygiene suite refuses. That suite is `pnpm test:release`, and `node test/run.mjs` globs
`*.test.ts`, so it never ran it — CI did, on both runners, and main went red on `9a40834f`.

Four commands take a `--batch` and each carried its own copy of the same twenty lines: attempt every
entry, catch per entry, partition into done and failed, shape a result. `runBatch` now owns the
attempting and the isolating; the result's own nouns stay at the call site, because
`compared`/`comparisons` and `reviewed`/`reviews` are what a reader of the output looks for and a
helper that also invented them would need an argument per noun to say nothing extra. Line count is
about flat — the saving is that there is one place where an entry's failure is decided to take only
itself down, not four.

`observe_reference --batch` reported a failed entry under `asked`; it is `input` now, as the other
three already were.

A sweep hands out four tasks per shot, and none of them said which command closed it — the two
commands that hand out a single task each did. `ObservationTaskRequest` now carries `record_with`,
so the caller never picks a recording verb: it copies the line out of the task. That is what makes
two record commands cost nothing, which is the argument for keeping them separate.

`make-placeholder` was the one command spelled with a hyphen while the other fifteen used an
underscore, and the method behind it was already `make_placeholder`. Nothing recorded a reason.

Verified: `pnpm check`, `pnpm test` (501, 0 failures) and `pnpm test:release` (9, 0 failures) — all
three CI gates this time, not two. A batch with one entry naming a shot that does not exist returns
`asked: 2, failed: 1` with the bad input beside its error and the other two answered. A sweep over
four shots hands out 15 tasks, 15 of which carry `record_with`, and the command one of them prints
records against the key it names. `make_placeholder` writes its PNG.
2026-08-26 02:19:35 +08:00
rponeawa 3e855607c7 fix: both routes look at every element once, and the reader is never the builder (#84)
A program authored from a description reached its Build with nothing having looked at it. The
reconstruction route renders each element, has an observer read it, repairs what was named and stops
at a ceiling; the description route went from `preview_check` straight to spending money. The two
routes also disagreed in five places about how the `agent` observer works, none of them following
from the one difference anybody had written down.

`references/element-review.md` owns the round both routes run: one element at a time, render what
the Source configures, mock the layers a Build has not made, one round, two attempt ceilings, measure
rather than guess. `reconstruction/comparison-round.md` and `original-authoring/conformance-round.md`
hold only the delta — a reconstruction has a second picture and asks what differs, a description has
none and asks whether the picture is what it was asked to be. `references/script-time.md` takes the
Segment seam and the `estimate:Speech` duration out of `reconstruction/`, where two shared craft
files were already linking backwards to reach them.

Neither round is a loop: each is capped at one pass, so both files are named for what they are.

`review_element` prepares one picture against an `--intent-file` and hands out the task;
`record_review` closes it; `authoring_check` refuses to pass while any drawing element has never been
looked at. `reconstructionCheck` becomes `authoringCheck` with a mode, so a description project also
reaches the checks that never needed a reference: the `playback` refusal, frame coverage, and Frames
past the Canvas were unreachable to that route only because the function opened by resolving one.

One subagent per picture is now required where the harness has them, and the main agent may not open
the pictures itself. That is what makes the `agent` observer equal to the `gemini` one rather than
merely cheaper: on the paid path the reader is a separate request and cannot be the builder.

Five observer-path defects, none derived from the agent having no sound:

- `NO_SOUND` told the agent to answer from the measured transcript, which no task carried. Tasks now
  carry `transcript_words` for their own stretch and `transcript_ref` for the whole file.
- `record_observation` rejected every narrow-question key it had just handed out: a shot id is
  `shot-001` and the pattern's character class had no hyphen.
- A narrow question bypassed the cache, so gemini paid again for each repeat and an answer recorded
  on the agent path was written where nothing read it. It now shares the sweep's cache, keyed by the
  question as well as the shots, and reports `reused`.
- `--batch` held the agent path to the vendor launch gap: six questions at concurrency 1 took 20s of
  `setTimeout` to build a list. Now 3ms.
- The shot/render duration assert sat inside the agent branch, so gemini compared a 3s shot against a
  1s render without complaint.

Two defects found while exercising this. `render_element` repointed `source=` and `from=` when it
wrote the cut Source three directories down but not `src=`, so any Source with a relative
`media:Image` failed to render once a window was named. `preview.md` still described rendering one
element by hand through a browser test; it now holds `render_element`.

`test/hypit-skill-reachability.mjs` compares the sentences each route can reach, not the links. The
prose is hard-wrapped at 100 columns, so a line-based check passes while a clause goes missing —
`grep -rn "never inspected is a system"` finds three files and there are four.

Verified: `render_element` with no `--reference-id` returns `basis: "estimate"`; a subagent handed
the caption render reported it clipped at both frame edges; `authoring_check` went false → true
across two recorded reviews and refuses a blanked `playback`; re-submitting an unchanged picture
returns `reused` rather than a new task. `reconstruction_check`'s output against a real reconstruction
project is byte-identical before and after the split but for one deliberately changed line. Links 0
broken, orphans 0, description route 2500 → 2727 reachable sentences, every reconstruction-route loss
paired against its replacement by hand. `pnpm check` clean, 501 tests, 0 failures.
2026-08-26 02:02:04 +08:00
rpone b6a8f7bb0f Merge branch 'feat/hypit-studio-semantic-take'
Every conflict resolved to the branch side, except CI, which stays as main has
it: the check job runs on ubuntu-latest and windows-latest. Linux is the 1x
billing rate against macOS's 10x, this repository is private, and the suite is
POSIX either way. Nothing else in .github/workflows/ci.yml differed.

The ranking package is taken from the branch whole, not only in its conflicted
files. main's 1b177713 removed the TierBoard variant from render.ts, types.ts,
style.ts and activation.ts; those files merge without a conflict, so keeping
them would leave the branch's component.ts, manifest.ts, surface.ts, schedule.ts
and index.ts referencing symbols that no longer exist. TierBoard is back.

packages/hyperframes/test/browser-visual.test.ts is deleted, as the branch has
it, and main's GPU-default edit to that file goes with it.

Two of main's additions had to follow the branch's model to compile:

- reference-video-tools reads a Selection through its single open/close anchor
  pair. The branch's NarrativeSelection carries startAnchorId and endAnchorId
  rather than a list of occurrences.
- reference-video-tools builds a StudioAdapterRegistry and passes workspaceRoot,
  matching the signatures loadStudioAdapterRegistry introduced, and depends on
  @hypit/video-cli for the Distribution package root.

@hypit/studio exports ./src/* so reference-video-tools can reach the modules it
imported before the branch declared an exports field.
2026-08-24 16:20:26 +08:00
kashorin 4a733d8efd refactor: separate projects from on-demand machine runtimes 2026-08-23 22:36:00 +08:00
rpone a29b60cc3f fix: refuse a lockfile carrying importers git does not track
`pnpm-workspace.yaml` globs `projects/*/packages/*` so a project-local author package
is one pnpm links, and `.gitignore` excludes `projects/` because a project is working
material. Both are right. Together they mean every local install writes importers a
clone will not have into the file that clone installs from.

The damage was not the importers. It was that the lockfile is dirty after every
install, so its diff stops being read, and seven real dependencies went in without
it — CI refused every run from that commit until it was noticed.

`repository-hygiene.test.mjs` now reads .gitignore and refuses a lockfile naming an
importer under any path it excludes, in two milliseconds, beside the eight invariants
already there. `pnpm lockfile:refresh` writes one it accepts, by setting that
directory aside for the install and putting it back.

The hygiene suite caught the refresh script starting pnpm without windowsHide on its
first run, which is the kind of thing it is for.
2026-08-23 04:24:20 +08:00
kashorin 490fe1125a refactor: externalize temporal windows and simplify studio adapters 2026-08-23 04:11:52 +08:00
rpone 7485fbf24b Merge main, taking main's caption outline fix
Four conflicts. Two were mechanical: `align-transcript.ts` and
`svml-playground/src/pipeline/placeholder.ts` are modify/delete, and the
deletion is this branch's intent — local alignment removes the need to
fabricate a program-audio digest, and Studio has no stand-ins to draw.

The other two are the same bug fixed twice. Both sides found that CSS
`-webkit-text-stroke` is centred on the glyph edge, so half the stroke eats
the letterform, and both moved to `VisualTextPaintLayer` with
`placement: "outside"` — which already existed at the merge base on
`VisualTextFlowElement`. That shared starting point is why they converged an
hour apart.

`packages/caption-fine/src/render.ts` and its test take main's version. Main
extends `VisualTextElement` and the HyperFrames renderer rather than moving
Caption Fine onto text-flow, and it carries two fixes this branch does not:
CSS blur is a radius covering about two deviations while the filter takes a
deviation, so passing the authored number through doubles every shadow and
glow in the repository, and its filter-definition dedup takes 80 defs to 16.
Neither file is touched by `a4f2b31f`, so taking main's reverts `d5ee32c7`
exactly and nothing else. `renderFineCaption` works on `ProgramSpace`, which
survives here as a projection of `SemanticTrack`, so the semantic API change
does not reach it.

The Pill-as-paint-layer and structured-typography work in `d5ee32c7` is a
real gain and is not preserved here; it belongs on top of main's
implementation as its own change.

`pnpm check` clean; `pnpm test` 510 pass, 0 fail, 15 skipped.
2026-08-21 00:40:21 +08:00
kashorin f3d319db22 refactor: make SemanticTrack the program skeleton 2026-08-21 00:11:28 +08:00
rpone a413b3c5c6 fix: the environment-gated test scripts stop depending on sh
Three scripts were written in shell that only one shell speaks. pnpm hands a
script body to /bin/sh on POSIX and to cmd.exe on Windows, and what sh reads as
syntax cmd.exe reads as text.

`VAR=1 command` is the loud one: cmd.exe has no such form and looks for a
program named `HYPIT_BROWSER_TESTS=1`. It fails where you can see it.

The quoted discovery pattern is the quiet one, and it is the same defect this
repository already found in its main suite. sh strips the quotes and unittest
receives `test_*.py`; cmd.exe does not, and unittest receives six characters
that match no file. Both exit 0:

    -p 'test_*.py'    stripped   Ran 11 tests   OK             exit 0
    -p 'test_*.py'    literal    Ran 0 tests    NO TESTS RAN   exit 0

Double quotes are stripped by both shells, so the pattern needs no runner —
only the right quote. The two environment gates move into the runner that
already exists for this, as named suites, where they are assignments rather
than syntax. That also settles the third defect: the OpenCV script named the
POSIX interpreter path outright, though `deployment.ts` has branched on the
platform for that venv all along.

An environment already set by the caller still wins. These are defaults for
running a suite, not a policy about it.
2026-08-20 23:43:04 +08:00
rpone c59d549571 fix: a test run that matched no files is a failure, not a pass
The patterns were written for sh and quoted for it, but pnpm hands a script
body to cmd.exe on Windows, which neither strips the quotes nor expands the
pattern. They would have arrived at `node --test` as literal text — and
`node --test` reports "tests 0" and exits 0 on a pattern that matches nothing,
so a suite that ran not one test reads exactly like a suite that passed.

Expanding the patterns in Node takes the shell out of the question everywhere,
passes explicit paths rather than patterns, and gives the count somewhere to be
checked. The unquoted spelling would have fixed the first half and left the
second: sh has no globstar, so today's patterns survive to Node's glob by
accident, and dropping the quotes would silently narrow the suite to whatever a
future nested test directory happened to match.
2026-08-20 20:57:45 +08:00
rpone 2bca6728c5 fix: the hygiene scan spells its paths one way on every platform
Each rule filters the scanned path with a forward-slash pattern, and the path
came from `relative()` over two `URL.pathname` values. On Windows that yields
backslashes and a leading slash before the drive letter, so four of the six
rules matched nothing and passed — a scan that inspected the repository and a
scan that never reached it report the same thing.

The guard is the point of the commit: it names a path the scan must reach,
refuses a separator that is not "/", and holds the source and manifest counts
to a floor. Without it the same silence returns wearing a different cause.
2026-08-20 20:57:31 +08:00
rpone 4eb55590d7 fix: stop committing the scratch project, and read the preview call from syntax
Two of mine. `git add -A` swept a reverse-engineering scratch project into
33ebbebb — twenty-one files of real but disposable output that were deliberately
supposed to stay on disk. Removed and ignored by shape.

And the preview check read raw text, so the scaffold's own template matched: it
reported `preview/${tag}.png` as a missing picture. Same mistake I had just
fixed in the import check and left standing one function away. Both now read the
syntax tree. Verified still sharp by deleting a real preview and watching it
fail.

The suite is green for the first time since the preview gate landed.
2026-08-20 00:20:01 +08:00
rpone bd8498be98 fix: require a preview from anything that draws, and read imports from syntax
The preview check only fired once a preview was declared, so a package that
skipped the declaration escaped it — and a component that cannot draw itself is
exactly the one whose author never got a picture out of it. Publishing a
VisualTrack is the structural signal, independent of whether anyone remembered
to promise one. Every existing drawing Surface already passes.

The import check read raw text, so the scaffold templates — which contain the
imports the *generated* package will have — counted as the generator's own, and
would have demanded packages/cli depend on everything it can generate. It now
reads the syntax tree, where a string is a string. Verified still sharp by
adding a real undeclared import and watching it fail.

I pushed the scaffold with that break in it: I verified with a narrower command
than the repository's own suite.
2026-08-20 00:17:59 +08:00
rpone 964563dcc8 perf: ask one question per cut, and stop making shots too short to observe
The shot detector accepted any boundary more than 0.3 seconds after the last
one, so a 45-second reference produced four shots under a second and two of
0.33s that the model rejected outright with INVALID_ARGUMENT — a request, a
retry loop and no evidence. A shot is now at least a second, and a trailing
fragment joins the shot before it instead of becoming one: 23 shots became 20,
and none are too short to observe.

The same-take and overlay questions sent an identical part list — two clips and
a tail frame — to learn two things about one cut. They are now one observation
under two headings, which halves the boundary requests and the bytes they
carry. Together with the clip encoding now being reused within an invocation
rather than repeated for each of the five observations that mention a shot,
this takes the sweep from 113 requests to 79.

Pacing moves to HYPIT_REFERENCE_CONCURRENCY and HYPIT_REFERENCE_LAUNCH_GAP_MS.
It depends on the quota behind the credentials, which the calling agent cannot
know, so it stays out of the CLI surface.

A declared Surface preview must now name a picture that exists and is not
empty. A component that cannot draw itself cannot produce one, so this is the
cheapest available check for a component demanding something the graph has to
supply — the failure it was written for was already sitting in the tree.
2026-08-19 23:01:55 +08:00
rpone df23c73d97 refactor: rebrand Narratage as Hypit 2026-08-19 15:51:58 +08:00
rpone bd0900f5c3 refactor: keep the repository boundary tests in test
`tools/` held one file once the Playgrounds moved to `packages/`, and that file
is a test. A directory named for tools that contains no tool only invites the
question of what belongs there; `test/` already owns repository-level material,
holding the shared fixtures the boundary tests read.

The scan roots drop `tools` along with the directory. Leaving the name behind
would have failed every one of the four tests with ENOENT rather than a
boundary report, since the scan reads each root directly.

The layout guide named `tools/` but never named `test/`, so the entry is
replaced rather than removed.
2026-08-17 18:48:11 +08:00
kashorin 7683e38c0e refactor: remove defensive execution scaffolding 2026-08-17 14:03:53 +08:00
kashorin 9e7aa9f4d7 refactor: remove redundant repository scaffolding 2026-08-13 00:17:36 +08:00
kashorin 80bcd3d8e1 feat: isolate caption authoring and close package dependencies
Replace the generic playground with a file-backed Caption Playground, separate source workspaces from installed package roots, and derive exact transitive Module providers in Node package locks.
2026-08-10 22:33:02 +08:00
kashorin 929396fe63 refactor: forbid playground registries and production influence 2026-08-10 21:02:57 +08:00
kashorin 1da9c14a09 refactor: isolate playground from production contracts 2026-08-10 20:24:37 +08:00
kashorin 0066ab51c5 feat: normalize generated media at consumer surfaces 2026-08-10 20:16:14 +08:00
kashorin eb8fb8d632 feat: add author surfaces for generation models 2026-08-10 20:15:46 +08:00
kashorin e366167a63 merge: integrate v2 architecture with main tooling 2026-08-10 15:19:34 +08:00
rpone 191cfa1967 feat: let modules say what their values mean and look like
ValueSchema gains `format` and TypeDeclaration gains `default`. A schema
could say a value is a string but not whether it is a colour or a font
family, and nothing could say what an unspecified one should be -- so every
consumer decided both for itself, and the answers drifted. A colour and a
digest have an exact shape, so validateStoredValue now checks them: a
format nothing enforces goes stale the same way a copied constant does.

Each module now states its own Recipe once, in a recipe.ts its Surface
reads too, with the required key set taken off the default rather than
restated beside it. Four literals that had to be kept in step by hand are
gone.

The playground lists nothing any more. It asks the manifests which modules
declare a Producer outputting a VisualTrack -- exactly the four that do --
builds a form from the declared input schemas, seeds it from the declared
defaults, and calls the module's own bound handler. Its five adapter files
are deleted. A module that gains a visual Producer now appears there on its
own; one that loses it disappears.

Where a module has no honest default it says nothing: a value carrying
content-addressed media cannot have one, because the module has no bytes to
point at, and the form asks for a file instead.
2026-08-09 06:26:27 +08:00
rpone acec2d84f3 feat: preview B-roll and speaker footage from picked media
Media is addressed the way the Runtime addresses it: the picker hashes the
file with Web Crypto and registers a real SHA-256 digest, because
assertHyperframesDocument cross-checks every placeholder against the
document's Artifacts and runs isDigest on each. The form value is the
digest and the bytes stay in the registry, so a component's parameters
remain a plain canonical value a test can build without a browser.

A component with no media yet contributes no Track rather than a stub
digest the compiler would reject -- an empty picker is a state to pass
through, not an error to report. A digest with no bytes draws a placeholder
and a warning instead of blanking the frame.

B-roll playback is derived, not offered: the compiler requires freeze for a
still and forbids it for a moving one, so a control could only offer a way
to be wrong.

speech-basis takes no appearance at all -- projectSpeechVisual always emits
a full-frame cover element -- so the component has clips and nothing else.
That is why speech.full stays in the unmatched list: it is declared in the
golden stylesheet and no Surface reads it.
2026-08-09 05:25:23 +08:00
rpone 44683bd8a2 feat: read a stylesheet as a gallery of previews
Selecting an .svs imports every Recipe in it and previews each one
separately. A sheet is a discovery source, never a dependency: what it
produces is form values, and from there the form is the only source of
truth -- nothing in the render path touches parseSvs unless you chose a
sheet.

Recipes are matched by exact property key set. A Film Recipe is not a
preview subject; it is where the frame size comes from, so opening the
golden sheet fills the canvas with 1080x1920 at 30. A Recipe that matches
nothing is listed with its keys rather than dropped -- font.*, speech.full
and broll.product are real Recipes with no preview yet, and naming them is
the difference between a gap and a silent omission.

Browsing is served by the dev server rather than the File System Access
API, which is Chromium-only and cannot follow a stylesheet's relative
asset path without a second grant. Containment is checked on the resolved
path, so traversal is refused however it is spelled.

Also pins source files as text: a raw NUL in a separator literal works at
runtime and reads as a space, but makes git treat the file as binary and
stop diffing it.
2026-08-09 05:14:12 +08:00
rpone f7c16897bf feat: preview captions and text overlays from a generated form
Each adapter converts form values into its package's own Program type and
then calls that package's real renderer -- renderCaptionTrack and
renderTextTrack -- so the preview is the production lowering rather than a
lookalike. Both are pure and free of node imports, which is what lets them
run in the browser at all.

The form is generated from each component's ValueSchema. Nothing in the
playground validates: the packages' seal and assert functions are the
judge, because they are what a real Build runs and their messages are
already good. A failure shows that message beside the form and leaves the
last good frame on the stage.

Recipes are matched by exact property key set, never by path prefix --
caption.dialogue and caption.short-cues share a prefix and describe
unrelated things, and the compiler itself matches on the key set. The two
key sets are copied from their Surface handlers, so a test reads the
literal back out of the source and fails if they drift.

planCaptionPresentation turned out to read only display text and timing,
so synthetic cues with no alignment tokens preview all three modes
faithfully rather than needing a reduced one.
2026-08-09 05:03:13 +08:00