Summary
A project file whose legacyEditor envelope holds a non-array where an array is expected loads without complaint, then throws regions.filter is not a function on the first clip edit. Every clip delete / move / duplicate / source-range edit routes through the same unguarded function.
Cause
legacyEditorSchema is z.object({}).passthrough(), so zod validates nothing inside it — any shape survives loadProject.
mapAllRegionCollections (src/lib/ai-edition/document/timeline.ts:243-272) then assumes each envelope is an array and calls .filter() on it.
The migration path already knows better: upgradeV4DocumentToV5 (src/lib/ai-edition/schema/index.ts:577-582) guards these same fields with Array.isArray. The runtime path doesn't.
Reproduction
Hand-edit a project file (or load one written by an older/third-party build) so that:
{ "legacyEditor": { "speedRegions": "oops" } }
The project opens fine. Delete any clip → TypeError: regions.filter is not a function, and the editor is stuck until the file is repaired by hand.
Suggested fix
Mirror the guard the migration already uses:
const speedRegions = Array.isArray(legacy?.speedRegions)
? (legacy.speedRegions as StoredRegion[])
: undefined;
…and likewise for cameraFullscreenRegions and the other envelopes.
Tightening legacyEditorSchema so the arrays are actually validated at load time would be the deeper fix — the passthrough is what lets a malformed document get this far — but the local guard is the cheap correct step and matches what upgradeV4DocumentToV5 already does.
Context
Found while reviewing #307, which changes a line inside this function's callback. Pre-existing and unrelated to that fix, so filing separately.
Summary
A project file whose
legacyEditorenvelope holds a non-array where an array is expected loads without complaint, then throwsregions.filter is not a functionon the first clip edit. Every clip delete / move / duplicate / source-range edit routes through the same unguarded function.Cause
legacyEditorSchemaisz.object({}).passthrough(), so zod validates nothing inside it — any shape survivesloadProject.mapAllRegionCollections(src/lib/ai-edition/document/timeline.ts:243-272) then assumes each envelope is an array and calls.filter()on it.The migration path already knows better:
upgradeV4DocumentToV5(src/lib/ai-edition/schema/index.ts:577-582) guards these same fields withArray.isArray. The runtime path doesn't.Reproduction
Hand-edit a project file (or load one written by an older/third-party build) so that:
{ "legacyEditor": { "speedRegions": "oops" } }The project opens fine. Delete any clip →
TypeError: regions.filter is not a function, and the editor is stuck until the file is repaired by hand.Suggested fix
Mirror the guard the migration already uses:
…and likewise for
cameraFullscreenRegionsand the other envelopes.Tightening
legacyEditorSchemaso the arrays are actually validated at load time would be the deeper fix — the passthrough is what lets a malformed document get this far — but the local guard is the cheap correct step and matches whatupgradeV4DocumentToV5already does.Context
Found while reviewing #307, which changes a line inside this function's callback. Pre-existing and unrelated to that fix, so filing separately.