The model layer
How a project folder becomes an index the rest of the application reads: the filename grammar, documents, resolution, epochs, media names and the script’s list.
5. The model layer
Section titled “5. The model layer”5.1 Filename grammar — filmopen_format/stems.dart
Section titled “5.1 Filename grammar — filmopen_format/stems.dart”FileNameParser.parse(path) implements Project Specification §5.4 and returns one of a sealed set:
| Class | Filename shape | Holds |
| VersionStem | <type>_<tag>[_<epoch>]_<author>_v<n> | type, tag, epoch?, Author, v; stem, versionsKey (ch_main-hero_30yo), groupKey (ch_main-hero) |
| OfficialStem | <type>_<tag>[_<epoch>]_official | type, tag, epoch?; versionsKey |
| CommentaryStem | cm_<type>_<tag>_<author> | target type, tag, author |
| BatchStem | <type>_<id>_<author>, the type rd, cp or as | id, author, type (EntityType.renderBatch, comp or session; renderBatch where none is given) |
| TakeName | <deliverable>_<source stem>_r<id>_<renderer>_<n>.<ext> | deliverable, source VersionStem, batch id, renderer, n, extension; batchStem |
| UnknownName | anything else | the path |
Rules enforced by the parser: exact token counts per type (library types carry an epoch, spec types do not); v<n> with n ≥ 1; take number n is a positive integer without leading zeros; the reserved owner official is never accepted as an author; a take’s batch token is r followed by at least one character. parseStem(stem) parses a stem without extension, as found in forked_from and batch source fields.
namingWarnings(parsed) returns warnings (as codes) for any segment outside [a-z0-9][a-z0-9-]{0,31} (§5.2); a valid segment draws no advice on its length. An epoch or tag segment that parses as an allocated id is exempt, so one of up to 43 characters is not outside the grammar; author and batch segments keep the grammar even when their spelling is an allocated id. A canonical lowercase UUID is exempt in a tag position alone, the source tag embedded in a take included (ShortName.isUuid, isValidTag); author, epoch and batch segments keep the ordinary rule. The rule is applied to every parsed name, including takes and commentary.
Author (filmopen_format/author.dart) splits owner[.workspace]; equality is by full handle.
Prose mentions use ReferenceCodec, not the filename parser. Its own
ReferenceKind table names characters (ch), locations (lo), props (pr),
project epochs (ep) and media; an epoch mention does not introduce a filename
entity type. Canonical source is @<prefix>_<id>. Entity ids are dotted
segments and an epoch id is one segment, allocated or ordinary.
A media token names a picture a prompt may send to a model (§9.8) and is written @<id>, with no prefix: on an
entity’s own prompts @inspiration-<n> and @reference-<n>; in a block’s frame prompts @<stem>:reference-<n>,
@<stem>:character_sheet-<n>, @<stem>:location_sheet-<n>, @<stem>:outfit_sheet-<n> and
@<stem>:prop_sheet-<n>, where <stem> is a character’s, location’s, outfit’s or prop’s canonical reference without
its @ (ch_kira, lo_singapore, of_kira.coat, pr_car); an outfit has no mention of its own, @of_kira alone
being plain text. <n> counts from 1 with no leading zero. The word is not paired with the stem’s type:
@ch_kira:prop_sheet-1 parses and names nothing, refused at render. The word names its group through one table,
mediaTokenGroups (media_refs.dart): inspiration → inspirations, reference → reference_images,
character_sheet → character_sheets, location_sheet → location_sheets, outfit_sheet → outfit_sheets,
prop_sheet → prop_sheets.
EntityType (filmopen_format/entity_type.dart) is the enum of the format’s seventeen prefixes — folder (fo, a spec type) which took se, ep and sq out of it with the Project Specification 1.12 (§5.3), so that a file of one of those types is a name the grammar does not describe and sits under Other files, reported and never touched — with category (library / spec / commentary / renderBatch), hasEpoch, isVersioned, and the dotted-tag containment rules parentType / containedType (outfits nest under characters; areas under locations). Deliverable is the enum of take prefixes with isImage, and token, the word a batch item’s deliverable and a picks key spell it with: first_frame and last_frame for the two frames, sheet for the concept sheet, the enum’s name for every other (Project Specification §1.4). Neither enum carries display text.
5.2 Documents — filmopen_format/entity.dart
Section titled “5.2 Documents — filmopen_format/entity.dart”Entity: one versioned file. Holds theVersionStem, the project-relativepath, the completejsonmap (unknown fields preserved) andrawText, the file’s text as read, which a save hands back to the source so a file changed meanwhile is never overwritten. Typed accessors (name,versionLabel— §7, with the earlierlabelspelling still read,forkedFrom,notes,kind,stringList(key),blocks) never throw on a wrong JSON type.headerMismatchesandfieldTypeWarningsreturn warning codes.OfficialPointer: from an_official.json;fromJsonreturns null when theofficialfield is missing, which the loader reports. Keeps its wholejsonso the writer can rewrite it without losing fields.Commentary/CommentaryEntry: onecm_file withpickandpicked_at(preferis the 1.5 spelling, still read) — the author’s pick — and entries (on,at,rating±1,text,block,field); keeps its wholejsonfor the same reason.SetRecord: one pending set and its history (Project Specification §12.7) — acp_oras_file, or anrd_ofkind: queue— byRenderBatch’s pattern:stem(aBatchStem, whosetypeis the record’s),path,json,rawText(what a write passes asexpectedText),items(every object ofitems, read as written),kind,statusandcreatedas written (an unknown word kept),isPending(statusispendingStatusOf(type):openfor a session,pendingotherwise),nextItem(the larger ofnext_itemas a whole number —7or7.0— and one more than the highest wholen, so an absent or lowered mark still numbers after every item), the comp’s members (title,description,visibility,posted,error) and the session’s (started,ended,summary,run,assistantSession,assistant),ownMembersOf(type)naming them, andnewestFirst, the order of a type’s records: bycreatedas an instant, newest first, a record without a readable one last, equal instants by stem.RenderBatch/RenderItem: onerd_file with its items (Project Specification §12.2).RenderBatch.requestis aRenderRequestFacts— what a render was asked:render_id,plugin,model,platform,stage,prompt,params,seed_random,inputs,inputs_by_name,treatment(aRenderTreatment, the leaves that applied and each one’s document),sources,purpose,asked_at,from— null except on arenderrecord that has one;stageis the request’s; the record’s cost is its topcost_usd, null where unknown, andtotalCostUsdthat value or 0. ARenderItemholds the file’s own facts —n,source,deliverable,group,output,duration_ms,status— and nothing of the request.MediaStage { prototype, draft, finalCut }is the stage, writtenprototype,draftandfinal.Take: a media file discovered by name;pathis its file name inmedia/, andmediais its located source value. Never listed inside an entity (§12.1).
Two helpers, stringField(json, key) and numField(json, key), are the only way JSON scalars are read. A third, parseInstant(text), is the only way a timestamp is read as an instant: RFC 3339 with Z, z or an offset (§1.4); a value without a zone would be local time on whichever machine reads it, so it counts as absent, like anything that does not parse.
character_age.dart reads the stored apparent age with ageAt. birthYearOf first reads birth_date, a string YYYY, YYYY-MM or YYYY-MM-DD (isBirthDate: the month 01–12, the day 01–31; the form’s Birth date box refuses inline with the same test), and reads the legacy exact "N (YYYY)" age only when the birth date is absent. A malformed present birth date yields no year. The pure ageFromBirthDate(document, epoch) returns {age, decision, reason}: filled with the numeric difference of calendar years, or kept with the original age and noBirthDate, badBirthDate, noEpochYear, yearAfterEpoch or appearanceShape. It leaves its inputs unchanged and preserves a non-object appearance. Callers choose the trigger and log its values and decision; the helper never rewrites a loaded document. ageFromYear and AgeYearRefusal remain available for the form’s legacy year-entry path.
5.3 The index — filmopen_format/project.dart
Section titled “5.3 The index — filmopen_format/project.dart”FilmProject.isSessionCopy identifies a writable source with no disk root: the sample — extracted to a folder of its own, or the web’s overlay — or a memory project. Rooted sources and read-only bundled sources are not session copies. The tree, page guards, media forms, recorder and automation read this one predicate.
FilmProject is immutable after loading and holds:
| Field | Content |
| entitiesByStem | every versioned entity by full stem |
| groupsByKey | EntityGroup per type+tag; each group holds EntityVersions per epoch (spec types use the single epoch null); versions are sorted by author handle then number. EntityVersions.all is every version the folder holds there, the one list the loader adds to; versions is the retained — every one not carrying discarded (Project Specification §7) — and discarded the set-aside, both read-only views of it. highestBy (the author’s working copy, §13.1), highestByOwner, authors and owners read the retained; highestOfAllBy reads every version, for allocation, so a discarded number is never given again. EntityGroup.allVersions is the retained at every epoch and everyVersion the whole set; name is the first retained version’s name, else the tag |
| officialsByKey | pointers by versions key |
| commentaries, batches, takes | as discovered |
| takesByStem | every take by its stem, the first takes lists where two share one; derived once per project, never stored, and read by takeNamed (a stem, or a stem and one extension), ProjectMedia.of and the epoch’s picture instead of a scan per tile |
| sets | every SetRecord: each cp_ and as_ file and each rd_ of kind: queue, which is never among batches; optional, empty by default, kept by withSelectedProject |
| otherFiles | files outside their defined area, nested in data/ or media/, or documents not named by the grammar; reported, never touched |
| warnings | every ProjectWarning raised while loading |
| projectCandidates | every pj version in the folder, stable order |
| selectedProject | the pj version in use, or null when the user must choose (needsProjectChoice) |
| projectOfRecord | the loader’s own §4.1 choice — the official pj, else the sole author’s — which a viewer’s pick never replaces; directors are read from it |
Queries: group(type, tag), groupsOf(type) in natural tag order — every group, the wholly discarded included —, retainedGroupsOf(type) the groups with a retained version (EntityGroup.hasRetained), which the tree, the lists and the choosers read, childGroups(type, parentTag) for dotted tags one level down, officialPointer / officialEntity / isOfficial, takesFor(group), commentaryFor(group), batch(stem), groupCounts, pendingOf(type, viewer) — the viewer’s newest record of the type in its pending status, null without a viewer (one per author per type is the application’s rule, so a folder with more shows the others in the history) — and historyOf(type), every record of the type that is no author’s pending one, newest first. officialEntity answers null where the pointer names a discarded version: such a pointer names none, at every step of resolution. isOfficial stays exact inspection of the pointer, discarded or not, so a discarded file a pointer names still routes its own references through officialPickAt. withSelectedProject(entity) answers the chooser without reloading. The groups by type and by parent tag, and the commentary by entity and author (the first file commentaries lists where two share one), are derived once per project and never stored, as takesByStem is, so that resolving every row of a long film or listing every scene’s shots scans the project once; groupsOf and childGroups answer a list of the caller’s own.
5.3a Project-level knowledge — filmopen_format/project_kind.dart, filmopen_format/epochs.dart
Section titled “5.3a Project-level knowledge — filmopen_format/project_kind.dart, filmopen_format/epochs.dart”ProjectKind is the enum of §8.1 (short-film, film, mini-series, series) with older spellings read as their nearest value (fromName: 1.4’s franchise, and the short-lived short / feature / miniseries), and nothing else: since the format’s 1.12 the kind is a word for people, the OMC mapping and a website, optional, naming no levels and choosing no list (§8.1), so isEpisodic, defaultRootKey and the label of a story level (unitLabel) went with the levels. recommendedGenres is the vocabulary of Appendix A.9. EpochDeclaration / declaredEpochs(json) read the project’s epochs object in the order §8.2 defines (unnumbered first in file order, then by order); encodeEpochs(list, previous:) writes it back renumbered 1..n, keeping fields the app does not know. FilmProject exposes kind, declaredEpochList, defaultEpoch (the first declared, else default), epochLabel, sortedEpochs(...) (declared order, then natural), isDirector(author) (§8.3: read from projectOfRecord, never from the viewer’s own fork of the project file, which forViewer selects; where the folder has no file of record, the origin of the selected file decides — followed back through forked_from, whoever wrote each link, to the project file that forks nothing — so no fork appoints anybody and the original’s author can write a first pointer; a loop or a source the folder lacks decides nothing. ProjectWriter.saveEntity keeps a project file’s forked_from as it is, so the chain cannot be cut through the app), commentaryBy, ownPickStem, pickOf(group, viewer) and isPickOf.
5.4 Resolution — FilmProject.resolve
Section titled “5.4 Resolution — FilmProject.resolve”Implements §6.1 in a simplified but faithful form. Given a type, a reference (tag or full stem), an optional epoch, whether that epoch was explicit, and an optional viewer:
- A full stem names its file exactly.
- No such tag → unresolved (
noEntity). - Epoch: for library types the wanted epoch is the given one or the project’s default epoch (the first it declares, §8.2;
defaultwhen it declares none). If there is no file for it: an explicit epoch, or the default itself, is unresolved (noEpoch); an implicit epoch falls back to the default and marksfellBack; no file at the default either → unresolved (noEpochOrDefault). - Author — the viewer’s pick at that epoch (
FilmProject.pickAt, §6.1 step 2): the viewer’spickwhen it names a retained file here (a pinned stem, or the official pointer when it is<versions key>_official), then — for a workspace handle — their owner’s; then the official version; then the viewer’s own highest version not discarded, then their owner’s, which is what an author sees while the project has chosen nothing; then, if exactly one owner has versions, that owner’s highest under the plain handle, else under their derived handles (an abandonedaaron.ai_v3does not show overaaron_v2). A pick beats official and nothing else does: an author who has said nothing is on official, and forking records a pick, so an author’s own work is not lost behind the project’s choice (§13.1, §13.4). The project’s ownpjversions skip the own-highest step and count author handles rather than owners in the last one, so the check and the loaded project agree (§4.1).pickAtreturns aPick(entity,PickReason: picked, tracksOfficial, official, own, soleOwner, andstaleSince) and is the one source the tree, the header and the version menu read. Compare is the exception: its columns are the viewer’s own latest and whatever they selected, so it readspickAtonly to draw the check on a heading.Pick.staleSincenames the official pointer that superseded the viewer’s own pick —set_atstrictly afterpicked_at, both read withparseInstantand compared to the second, so a fraction another tool wrote does not make equal seconds later (§1.4, §13.2). It is null whenever either time is missing, unreadable or without a zone, when the seconds are equal, when the pick names the official version, when the pick tracks official, for a pick a workspace inherits from its owner, which the workspace cannot abandon, and when the pointer names a discarded version, which names none. It never changes whatpickAtreturns: no timestamp decides (§7).Pick.showsCheckis what a check is drawn from — false forPickReason.official, since the star already says official is in use (§13.4). - Otherwise unresolved (
ambiguous, listing the owners);hasNoPicknames that state for the UI.
§6.1’s other half — a file named by an official pointer resolves through official only — is resolve(..., referencing:), with inUseFor(versions, viewer, referencing:) behind it: for a file an official pointer names, officialPickAt(versions) — official, else the sole owner’s highest (by handle for the project file, as in pickAt), and nothing an author picked; for anything else, pickAt. nameOf takes the same referencing. It is passed where a version’s own references are rendered (today the Script tab’s speakers) and not where the reader is browsing: the tree, the version row and the pages keep showing each viewer the version in use for them, so a fork is still what its author sees. A proposal sees its author’s world; official sees the official world.
A version set aside (Project Specification §6.1, §7). discarded is the one header key resolution reads (Entity.isDiscarded: the key present, whatever its value), and it divides two contracts. Selection — what is in use, what a short reference resolves to — reads EntityVersions.versions and so never lands on a discarded version: a pick naming one falls through, a pointer naming one names none (officialEntity), the own-highest and sole-owner steps count the retained, and an epoch whose every version is discarded resolves for nobody (noFile). Exact inspection — entitiesByStem, a full stem in a reference, isOfficial, the page a stem opens — still finds the file. Allocation and every existence check read the whole set: highestOfAllBy, takenChildTags (everyVersion), AllocationInventory.fromProject (every project file in entitiesByStem). The format raises no warning for it; the loader reports a pointer or a pick naming one (officialDiscarded, pickDiscarded, §6) and chooses the project file among the retained. discarded is a header key, never a value row, and copy-left never moves it.
The recycle bin (Project Specification §7). Resolution never reads an entity’s deleted: pickAt, resolve and officialPickAt select a deleted version as any other, so that a live pin stays live and the person can still reach the deleted version to restore it. The flag of the file a context resolves governs in that context (isEntityDeleted, entity.dart, true only for true) — the viewer’s pick, an exact pinned stem, the version an official file’s reference reaches — so a scene pinned to a live version is live in the script although the viewer’s pick is deleted, and an official scene’s character is live where its official file is. A chooser asks isGroupDeleted (recycle_bin.dart): the version in use is deleted at every epoch the entity has; deleted at one and live at another, it is still offered. A row of the script says it (ScriptRow.isDeleted, §5.12) and keeps its place. What consumes the film skips such an entity at its own output — the exchange (§6.18), the plug-in doors (§6.11), the entity export (§7.3), the forms’ pickers (§9.3) — and copy-left never moves the key (§9.3). A stated limit: a render reads what it composes as before — an outfit’s deleted parent character (source_composition.dart), and a visual treatment inherited from a deleted folder or a deleted style (visual_style.dart).
5.5 Project selection — ProjectLoader._selectProject
Section titled “5.5 Project selection — ProjectLoader._selectProject”Implements §4.1: a valid official pj pointer wins; else the highest version when exactly one author handle has project files — handles, not owners as §6.1 step 3 counts them, so a workspace’s translated fork of the project file asks rather than being opened as the owner’s latest; else no selection and the UI asks. Several project tags in one folder also ask. An official pointer to a missing file is a warning and falls through as if absent; one naming a version of another entity or epoch is reported (officialWrongTarget) and dropped, so it is never anybody’s official version. The choice is kept twice: as selectedProject and as projectOfRecord, which nothing replaces. FilmProject.forViewer(viewer) then applies the viewer’s own pick for the project entity (their pick in cm_pj_<tag>_<viewer>.json, §4.1): the provider calls it after every load, so a viewer who forked the project file works from their fork while everyone else keeps the official one.
5.6 Diagnostics — filmopen_format/diagnostics.dart
Section titled “5.6 Diagnostics — filmopen_format/diagnostics.dart”ProjectWarning(kind, args, path) with WarningKind (naming, duplicate stem, header missing or mismatched, field type, official pointer problems, unreadable JSON — including duplicateKey, which carries the repeated name so the sentence can be in any language (§18.1) — project selection, the manifest and library kinds: noManifest, manifestTagMismatch, libraryStemShadowed, libraryUnavailable, noEpochsDeclared, §8.2, and pickOtherEntity — a pick naming an existing file of another entity — officialWrongTarget and timestampUnreadable, a picked_at or set_at that is not an instant with a zone, renderWithoutRequest, a render record without its request, still listed, and officialDiscarded and pickDiscarded, an official pointer or a pick naming a discarded version, which counts as none and is ignored). A duplicate commentary stem in the indexed sources is duplicateStem, and the first found is the one kept and written. ResolutionProblem(kind, type, reference, epoch, owners) with ProblemKind (five kinds). Both have a toString for logs only; the UI renders them through the localisation layer. A third set of codes, ChildProblem (childUntyped, childWrongType, childDuplicate), belongs to an entry of a children list and is §5.11’s.
5.7 The manifest — filmopen_format/manifest.dart
Section titled “5.7 The manifest — filmopen_format/manifest.dart”filmopen-project.json (Project Specification §4.4) identifies the project folder. ProjectManifest.fromJson reads tag and note tolerantly and retains the complete JSON. ProjectManifest.rawText, like RenderBatch.rawText, is the file’s text as read: what a change of the manifest is checked against, and what a later publication of the index serves instead of reading the file again (§7.5). ProjectManifest.template is what the creator writes, without a data field; projectManifestFileName is the one constant that names the file. An older data field is kept but neither the loader nor the store uses it or warns about it.
5.8 Short names — filmopen_format/short_name.dart
Section titled “5.8 Short names — filmopen_format/short_name.dart”The format’s segment (§5.2) as a person types it: ShortName.pattern ([a-z0-9][a-z0-9-]{0,31}), maxLength 32, validate (a ShortNameProblem: empty, invalid characters, bad start, too long), validateUsername (a person’s username, §5.6: usernamePattern, [a-z0-9][a-z0-9-]{2,31}, so 3 to 32 characters — shorter is tooShort — and never ShortName.reservedOwner, official, which is reserved; default is valid; the website’s usernames follow the same grammar), validateHandle (an author handle, §5.6: owner[.workspace], the owner a username by validateUsername and the workspace a segment of at most maxHandlePartLength, 20 — more dots is tooManyDots; the filename grammar’s reservedOwner is the same constant), isValidOrAllocated (a segment or an allocated id: the test for an epoch key, a tag segment or a reference segment, which isValidTag applies per dotted part; every other segment is isValid), normalise (free text to the nearest segment, never inventing one). A plug-in’s derived handle takes its workspace part from its tag, whose own bound binds it; Author.parse splits it without applying the bound for a person’s handle. The filename grammar in stems.dart uses the same pattern, so there is exactly one place the rule lives; filmopen_ui/short_name_field.dart is the input box built on it.
AllocatedId (allocated_id.dart) reads <owner>-<n>, where the owner has at most 32 characters (AllocatedId.of requires a username) and the positive decimal suffix has at most ten digits and no leading zeros. The owner is everything before the last hyphen, so john-1-2 belongs to owner john-1, number 2. Its canonical value is one segment of up to 43 characters, past the ordinary 32; AllocatedId.of refuses an owner that is no username. The workspace part never participates in this identity. No epoch token is reserved; the first declared epoch in order is the default, and default is an ordinary segment.
ReferenceCodec (reference_codec.dart) tries a media token first at each
@, so @ch_kira:character_sheet-1 is one media token rather than the character
followed by text; a media token ends where its number ends, and @reference-01,
@reference-0 or @ch_kira:inspiration-1 is none. It parses canonical mentions before
the shorthand that omits the underscore. A malformed canonical id does not
fall back to shorthand. Each ReferenceToken holds its exact target and
inclusive-start, exclusive-end UTF-16 offsets into the original source;
tokens are immutable and returned in order. The parser takes the longest
valid id prefix, with terminal dots and hyphens left as text, and stops at
whitespace or a character outside the grammar. An entity’s segment that is a
canonical UUID is read and encoded whole; an epoch’s never is. A segment’s run longer than 32 characters
that parses as an allocated id is read whole too, its terminal hyphens left as text. Unsupported kinds remain
text. Unknown or deleted targets still parse: an optional knowledge callback
annotates them as unknown without changing their identity, filtering them,
or choosing a substitute. Without that callback their knowledge is
unassessed; a known target is not a claim that contextual version resolution
is unambiguous. Resolution through a project’s picks belongs to the state.
encode validates the kind’s id grammar and writes canonical source without
normalizing it. Because a terminal hyphen remains text at a mention boundary,
a valid id ending in a hyphen can be encoded but does not parse back as that
same id; the codec defines no escape spelling for it. A media target encodes only
where its whole id is a media token.
The picture of an entity at an epoch is filmopen_media’s, pure (epoch_picture.dart): epochPictureFor — the version’s preview, then its picks.sheet, then the first image take at that epoch, never a reference the version holds deleted — which a card draws, and pictureInUse, which a picker’s entry draws: at the epoch asked where the entity has a version there, else where its name is read.
What a prompt may name is filmopen_media‘s, pure (prompt_inputs.dart). promptTokenCandidates(project, document, kind, viewer:) answers, in order, the tokens a prompt of the writers’ kind may name, each with its holder’s stem
and name, its word, its group, n and its entry: a *-reference prompt its file’s inspirations; a *-sheet prompt
its inspirations, then its reference images; first-frame, last-frame and clip every character’s, location’s,
outfit’s and prop’s reference images and sheets, the scene’s context first (its cast’s outfits included) and then the
film’s other entities, each entity as it resolves for the viewer at the
epoch nameOf names it at, none in the viewer’s recycle bin (isGroupDeleted); the voice, the treatment and any
other kind none. n counts only the entries a render may send (isPromptInput, which reads
MediaEntry.usable: not deleted and not archived, including unreviewed entries), in file order, so a token’s number moves when an earlier entry is archived. promptInputsOf(text, candidates) answers the entries the words’ media tokens name, in order of first appearance and each once, and the
tokens that name none.
AllocationInventory (allocation_inventory.dart) copies the loaded index into
an immutable snapshot; it never reserves an identity or edits the index. Epoch
tokens come from every project version’s declared keys, every group’s filename
epochs, content usage, official pointers, takes, and the independent object-form
epoch keys of commentary picks, pick times and visits. Library origins count.
Each project’s next_epoch belongs to that version author’s owner, so workspace
versions contribute to the same owner’s highest floor. Outfit segments are
grouped by the complete character tag, using the last dot, and include groups,
surviving takes and pointers, and commentary targets even when no pick survives,
and the filename of an outfit version any warning names — so one the loader set
aside (unreadable, not an object, a duplicate key) still holds the path an
allocation would write.
nextEpochId(owner, inventory) and nextOutfitId(owner, character, inventory)
(allocation.dart) return the owner part and a positive decimal number, one more
than that exact owner’s highest allocated suffix in the relevant inventory, or
1 when none exists. The epoch counter is already a next number, so it supplies
a floor without another increment; outfit allocation does not use it. Other
owners and nonallocated spellings do not advance the number. Allocation rejects
invalid owners and throws allocatedIdExhausted rather than reusing an id when
the ten-digit suffix is exhausted. The result is a proposal, not a reservation.
readNextEpoch and encodeNextEpoch (epochs.dart) read a finite whole number,
tolerating an integral double, and encode at least 1, the previous counter and
one past every same-owner id in the new and previous declaration keys. Deleting
the highest declaration does not lower that high water. A floor that fits an
allocated id is encoded as a whole number, so 4.0 is written back as 4;
oversized whole floors are preserved in encoding as they are, and allocation
refuses them separately. A project
version’s next_epoch is uncopyable alongside its contributors; a scene’s
next_block remains protected. The writer supplies these values when it writes;
pure rules here change no source. A fork carrying another owner’s counter can
only raise the start, leaving a gap; dropping it belongs to the fork writer.
Different owners have disjoint ids. The same owner on two offline machines can
propose the same id; allocation does not serialize those proposals, and the
entity writer’s create: true refuses a duplicate at sync.
5.9 Origin — Entity.origin, Take.media
Section titled “5.9 Origin — Entity.origin, Take.media”EntityOrigin { project, library } says which root a versioned file came from; EntityGroup.isFromLibrary is true when every version of an entity came from the library. Take.media is the MediaLocation resolved at load time, from the project’s media/ folder: FileMedia(path) for a folder on disk, the native sample’s extracted folder included; on the web build, whose sample stays in memory, AssetMedia(key) for the bundled media and BytesMedia(bytes) for the session copy’s own media.
5.10 Media names, references and paths — filmopen_format/media_names.dart, media_refs.dart, media_paths.dart
Section titled “5.10 Media names, references and paths — filmopen_format/media_names.dart, media_refs.dart, media_paths.dart”What a piece of media is, what an application calls an upload, and how an entity keeps its media — one place, because the importer that writes the file, the page that looks for its thumbnail and the filename grammar that must parse what was written all depend on the answers agreeing.
MediaKind { image, video, audio, other } and mediaKindOf(name) read the lower-cased extension against mediaExtensions (Project Specification §6.3); anything else is other, which the importer refuses. uploadPrefixFor(kind, type) is the one place the rule lives — a prefix names a file’s purpose, never its origin (§5.5): a picture is a cs take, a clip a cl take, a character’s audio its vo voice sample, and any other sound an am take, an outfit included, since an outfit has no voice (§9.0). uploadTakesFor names the files of one upload action, numbering from 1 per purpose and leaving a file that is not media without a name; nextBatchId(at, taken:) is eight lower-case hex digits of the second the import began, and takes the next free one when that author already has it — the clock proposes, the check decides (§5.2, §12.3). thumbnailPathFor(ref) answers thumbnails/tn_<take stem>.jpg, relative to the project folder, for a reference that is a take — by its stem or by its file name — and null for anything else. Only a take’s name is unique in a project by design (§5.4: the batch id and the take number are in it), and it stays unique whatever folders stand before it; front.png and front.wav would claim one tn_front.jpg between them, so a reference whose basename is not a take’s has no thumbnail of its own in this version and the tile draws the kind’s stock picture. The grammar says where a name ends, never the last dot, since a workspace handle (john123.locale-th) and a dotted tag (sh_5.2) carry one. documentsFolder, mediaFolder and thumbnailsFolder name the three folders. documentPath(name) and mediaFilePath(name) form paths into their flat folders; isMediaFileName(name) refuses empty names, directory segments, separators, drive/scheme separators and NUL for local media; projectAreaOf(path) classifies root paths, document paths, media paths and thumbnails, returning the file name and whether it is nested. Files elsewhere, or nested in data/ or media/, are outside those areas and reported. The library keeps its existing layout.
media_refs.dart is §9.0’s container. An entity’s media is one object, media (mediaContainerKey), whose keys are groups and whose values are arrays of entries; entityMediaPlaces is each type’s places — a character’s, an outfit’s, a location’s, a prop’s, the project’s and a folder’s — in the order its card shows them, each with the kinds it takes, isReservedGroup answers preview and media, which never name a group, and isMediaGroupName is the grammar: a lowercase snake_case word that is not reserved. MediaEntry is one entry: reference, a media reference; batch and item, the record’s stem and the item’s n, present where a record exists; favorite, archived, deleted and note, what the version decides; duration_ms, width, height, audio (a clip’s sound as measured at insertion, null where unmeasured), stage (a MediaStage), spans (a MediaSpans, spans.dart), unreviewed (only true) and kept_at, what describes the media; and extra, every other key. It holds the object as the file had it, so toJson writes it back with every key in its order, reference first, a known key of another type kept as it was and read as absent; fromJson answers null for anything that is not an object with a non-empty string reference, and withFlags(favorite:, archived:, deleted:, note:, unreviewed:, keptAt:) changes a flag, the note or kept_at in its place, removing one that is cleared. A reference is read once per document: mediaGroupsOf(json) lists the container’s arrays that have a group’s name, in file order, and entriesIn(json, group) the readable entries of one, each reference answered only at its first place in the document — deleted entries included, for a caller to hide — and entryNaming(json, reference) that first entry. The document’s places are the entity’s groups and then each block’s (§10.3: every object of blocks whose media is an object, its groups the six of blockMediaGroups), in the order of blocks, read by the one walker: a reference a block’s entry names first is held there and found there, and entriesIn and every writer that takes an entity group pass a block’s entry over; isArchived and isDeleted read its flags, and liveReferencesOf(json) lists every reference a live — read, not deleted — entry names, which is what a page shows; entryNaming and the flags it reads find a take by either spelling, its stem or its file name (§6.3); only a take has two spellings, a file placed by hand or an address its one word. Every writer copies the document and adds or changes, never removes an entry: withEntriesInserted(json, group, entries, first:, kindOf:, block:) puts a block of entries into one group — the entity’s, or with block: one of the six groups of the first block of that id, wrongShape where none has it or its media is not an object, and no preview from it — each entry whose reference no group of the document holds — a take in either spelling — before or after what is there, making the array and the container where absent and setting preview to the first picture it adds where the file has none; withEntryAttributes(json, group, reference, …) sets or clears one entry’s flags or note, and deleted: true clears preview, every picks value and a character’s voice.active_reference naming the reference — by the take’s stem or its file name, either spelling, an emptied voice going too — in the same change; withEntriesReordered(json, group, references) puts the readable entries in a new order that must be the same set, the array’s odd items and repeats after them. An odd item — a string, a number, an object without a reference — is skipped by every reader and left in place by every writer, and a container or an array of another shape is wrongShape, the document then as it was. acceptedKindsFor(group) is what a place takes: a block’s six groups their BlockMediaGroup.kinds (spans.dart: clips video, the two frames’ groups pictures, voice lines, music and sound effects sounds), wherever the name is used, then a type’s place what entityMediaPlaces says — sounds in voice_samples, clips in motion_clips, pictures and other files in every other place — and every kind in a group neither names. acceptsFile(kinds, name) is what an insertion takes into such a place — a picture, a clip or a sound of one of those kinds, never a file of no media kind, which an import cannot name (§12.3) — and it is the one rule the service, a place and the upload sheet judge a file by. arrivedAfter(project, entry, since) answers whether the batch an entry names was created after the viewer’s last look (§13.4): an entry that names no batch never did. takeNamed(project, reference) is the take a reference names, by its stem or its file name, and documentsNamingTake(project, take) the documents that keep it — every object of every array of media, the entity’s or a block’s, that is not deleted, a repeat a reader passes over and one under a key that cannot name a group included, since the delete takes no chance on bytes a document names — what the writer’s delete and the place’s delete dialog both read. mediaWarnings(json) is what the loader reports: a repeated reference (duplicateMedia), a group holding an unreadable entry or not an array (mediaEntryUnreadable), a key that cannot name a group (mediaGroupReserved), and a media that is not an object (fieldShouldBeObject); a block’s media is reported with the same kinds, a repeat counted across the whole document, its key or group named after the block — bl_8.clips, or #4.music for a block whose id is not a bl_<n> — and a block whose media is not an object passed over unreported — and a key that is not one of its six groups reported as one that cannot name a group. mediaCopyKeys and copyMediaInto are copy-left’s media row (§9.3): per group, the live entries the left lacks are added after its own, whole, and an entry both hold in one group takes the right’s set flags and note; a character’s voice.active_reference is copied with it only where the left holds that voice sample, not deleted, once the copy is done (§9.1), and is otherwise left as it was; nothing is removed or cleared, a deleted entry never moves, and only the left’s own top-level keys are assigned.
spans.dart is the spans of a block’s media entry (Project Specification §10.3). MediaSpans takes one of two shapes (SpansShape): blocks, the consecutive blocks from the owning block a render covers, a non-empty list of non-empty ids; or until, the extent it plays over — end_of_scene (MediaSpans.endOfScene), { "block": <id> } or { "seconds": <n> }, any finite number greater than zero, written as given. fromJson answers null for anything but exactly one of the two — both keys, neither, an extra key, an empty list, an empty or non-string id, an until of another shape, a seconds that is not a number greater than zero — and reports nothing, as an entry’s other known keys of another type read; the entry keeps the value as the file had it. The constructors refuse what the reader would read as absent, an empty list first, with an ArgumentError. That the owning block is the first covered and the blocks are consecutive is the writer’s promise, which coverageIntact catches when a file broke it. blockMediaGroups is the block’s six groups in order, one BlockMediaGroup each — the shapes it allows and the spans a new entry is written with: clips covers blocks only, so that rendered video is outdated by any change to a block it covers; first_frames, last_frames and voice_lines take none, their owning block alone; music and sound_effects take until or blocks, until: end_of_scene by default, so that editing the dialogue under them does not outdate them. spansAllowed(spans, group) is an entry’s spans where its group allows the shape, else null — an unknown group allowing nothing — and null means the owning block alone, whether the entry had no spans or one its group does not allow; the caller knows the group, since an entry does not.
voice_samples.dart is §9.1’s voice. VoiceBinding is a voice sample’s provenance, its entry’s voice_binding: provider, a platform tag, and voice_id and generated_voice_id, each where given an id of isVoiceId‘s grammar (1 to 128 letters, digits, _ and -); fromJson answers null for any other shape, which a reader skips and a writer keeps, and voiceBindingOf(entry) reads an entry’s. activeVoiceIdFor(json, platform) is the active binding, voice.provider_bindings’ id for a platform; activeVoiceSample(json) the live sample of voice_samples that voice.active_reference names — archived or not — and null where it names none, which a reader ignores, never taking another sample in its place; voicePreviews(json) the samples, live and shown, whose binding names a preview and no voice; isPreviewOnly(json, id) whether an id is one the samples know only as a preview’s, which speech never takes. withVoiceKept(json, reference, voiceId:, provider:, auditionText:) is the one change keeping a voice makes (media_refs.dart): the sample’s existing binding given the voice, or a new binding from provider for an uploaded sample with no preview id; voice.provider_bindings the voice for that platform; voice.active_reference the sample; and known audition text on that entry and on legacy voice.audition_text. Without supplied words, the entry retains only its own text, never another sample’s legacy words. No prompt is written. It refuses, the document as it was, where no live voice sample names the reference or neither its binding nor a provider is supplied (noSample), or voice or its bindings are of another shape (wrongShape). MediaEntry.extra preserves the entry’s transcript and binding through round-trip and a copy-left that brings the sample whole; an entry already held keeps its own text.
prompts.dart is the prompts a document holds (Project Specification §9): one prompts object keyed by kind on every holder, each prompt {positive, negative?, created?, plugin?}. promptPaths(document) lists them as PromptPaths — prompts.<kind> at the top, visual_style.prompts.<kind>, and each block’s prompts.<kind> by the block’s id, of a kind known or not, a value of another shape and a block without an id holding none — the one list the writer, the draft’s adoption and copy-left read; promptAt(document, at) is the prompt at one; documentApartFromPromptTimes sets every prompt’s created aside for a comparison, copying only what it must; isPromptStampPath(path) says whether a path names a prompt’s created or plugin. promptStateOf(prompt, holderUpdated) is a prompt against its holder’s updated — the file’s for one at the top or of the treatment, the block’s for a block’s: empty where positive is blank or absent, current where created carries a zone and is not earlier to the second, else outdated; a holder without a readable updated is earlier than any prompt. The spellings before prompts — prompt, voice.prompt, visual_style.prompt — are read nowhere.
entity_fields.dart holds the fields of the character, location, outfit and prop forms on their dotted paths (characterBoxes, locationBoxes, outfitBoxes, propBoxes, each path with what it holds: text, number, lines), the prop’s kinds, the outfit’s conditions and a garment’s keys; the forms draw their boxes from these maps, and fieldShapes(type) renders them for the assistant (§6.11a: a tool-less opening’s context, else update-entity‘s guide, contentGuide) — the fields with widgets of their own first (name, kind with its vocabulary, aliases, summary, the age, the voice’s pitch and pace, relationships, within, time_of_day, an outfit’s condition and garments, a prop’s hero), then every box, then a metadata line — where an attribute no field holds goes —, then one nested-JSON example of the first dotted path where the form has one (a prop’s has none); prompts and creator notes left out. What each box holds is read from the records (boxesOf, boxValueAt, which throw where a box names a path its record lacks); the order and the subset are the forms’. Every other type a command writes has lines drawn from its record (name first, a closed nested record by its leaves, a dynamic map as map of <key> → <value>, a lang as a BCP 47 code), the header, provenance, media, picks, prompts and extension containers left out; a type no command writes has none.
The records (field_maps.dart, Project Specification §7–§14, Appendix A, §18.3): fieldMapDefs holds, as JSON Schema consts in the keywords JsonShape reads, a record for each of the fifteen types a command creates or updates (fieldMapTypes: character, outfit, location, prop, style, misc, document, folder, scene, project, shot, cue, model, platform, plug-in), the block, and the records they share — the prompt, the media entry and its spans, place, the look and the visual treatment, a shot’s deliverable, a plug-in’s settings. A record is closed; open are metadata, x, a plug-in’s settings, a media entry (the specification’s named exception) and source‘s provenance under an import format’s key; a dynamic map declares its key in its description and its value in additionalProperties. No vocabulary is an enum (Appendix A’s are suggestions), and a member is required only where the specification marks its neighbours optional — a prompt’s positive, a cast entry’s character, a media entry’s reference, a voice binding’s provider, a deliverable’s model and prompt, and a plug-in’s api, a key slot’s platform, an offer’s label, an action’s label and call, a setting’s type and a sharing operation’s function, model and unit (§14.4, as the manifest reader requires them) — besides a scene prop’s prop, a fractional rate’s num and den, and the one member each alternative of a media entry’s spans names (blocks, until, block, seconds), which have no meaning without them; a whole document requires its header and name. epoch is a library entity’s (its filename’s) and a scene’s (its content’s) alone: on a folder, a project, a shot, a cue or a model, platform or plug-in document it is a key the record does not define, which a change may only delete. A CDL’s slope, offset and power hold exactly three numbers (minItems, maxItems). The checker is JsonShape (json_shape.dart, §6.11), kept here beside the records and re-exported by filmopen_plugins. contentProblems(type, content, {patch}) (content_check.dart) answers every path at which new content breaks its type’s record — an unknown key (notAllowed, with ShapeProblem.allowed the keys the record has), a value of another shape, a required member missing — and is a pure function: it reads no file and logs nothing, its callers logging the content, the check and the answer. With patch it reads a change as create-entity and update-entity merge one (§7.3), through fieldMapPatchDefs, the records’ change views derived from them: at a merge boundary — the top of a document and every object reached through objects — every key optional and null deleting it, an old unknown key included, which may be written only as null; a list replacing whole, so it holds to the stored records and a null in it is refused; a scene’s blocks a list of block changes (block_change: a block’s keys each replacing or cleared by null, insert, deleted: true). It decides no reference, epoch, identity, staleness or authority, and leaves whether an id or an anchor exists, and which number a new block takes, to the block merge; what a merged document must hold — its header, a record’s required members — stays the writer’s. The exporter scripts/gen_field_maps.dart writes the records and their change views to plugins/filmopen-field-maps.json (one record to a line, types naming each type’s document and change), and its --check, which writes nothing and prints every differing line, runs in the gate.
reference_analysis.dart is what a description of a character’s picture or recording writes into the character (Project Specification A.1). analysisChanges(kind, fields) reads a description’s fields — the plug-in interface’s CharacterPicture for image, VoiceHeard for audio — as the leaves it writes: a picture’s pictureLeaves (the appearance’s face, hair, eyes, facial hair, skin, teeth, glasses, jewellery, species, build, posture, hands, marks and notes, and prompts.reference.positive and prompts.reference.negative), a recording’s recordingLeaves (voice.description, timbre, accent, lang, pitch, pace, and prompts.voice.positive, beside the character’s other prompts), and nothing else: anything it names beside them — a name, an age, a relationship, an outfit, a binding, a pointer, a key holding a dot — refuses it at its path (unexpectedLeaf), as the plug-ins’ closed schemas require, never dropped (Project Specification §18.3); the caller’s analysis.failed logs that path beside the answer whole. Each is trimmed; a null, an empty text or an empty list tells nothing and leaves the file as it was. A value of another type, a text over 4,000 characters (analysisTextMost) or holding a control character, or a pitch or a pace of no vocabulary refuses the whole description (AnalysisChanges.refused, its path). A picture’s clothing (outfit_description) is no leaf: it joins the appearance notes as a paragraph of its own, unless they hold it already, and never default_outfit, which names an outfit. withAnalysisApplied(json, changes) writes them into a copy all together or none, and says which leaves’ values that changed: a leaf under a text, or a text where the file holds a list or an object, refuses them all with that path, the document as it was. uploadsIn(project, json, group, kind) is what a person uploaded to a place, the most recent first: the live, shown entries of that kind named by an import’s record (kind: import) that says when it was created — of two in one second, the record whose id, the upload’s second counted on, is higher — and within one record the last it took in first, by the item’s number — where the tiles stand never says what came when; a render’s take, a file placed by hand and an entry whose record is gone or says no time are not uploads.
visual_style.dart is a film’s visual treatment (Project Specification §8.7, A.11). visualStyleLeaves are the profile’s leaves under visual_style, by dotted path, the five primary leaves first — camera, camera_mount, lens, depth_of_field_prompt, color_grading, each a person’s words, in the order treatmentPromptLeaves gives with the English words a prompt names each by — with the tokens of each that has a vocabulary — the look’s grain, halation, contrast, saturation, sharpness and vignette, color_temp_bias and depth_of_field — and palette, a list of at most eight colours written #rrggbb (paletteMost, paletteColour); the treatment’s prompt, prompts.treatment.positive and .negative — its created and plugin no leaves, never projected; last, preset, a reference <tag>:<entry id> to the style a prompt builder’s catalogue names, inherited as any text leaf, never shared by a Style and never written by styleChanges. styleLeavesOf(profile) is what a reader takes of a profile: a null, an empty text or an empty list absent (§13.3), a value of another shape or a token of no vocabulary left out, and a palette taken whole, lowercased, or not at all — a list with a colour of another form or more than eight colours is none (isPalette), since a list replaces a list whole. treatmentFor(project, entity, viewer:) is what applies to an entity (ComposedTreatment: its leaves, the stem of the document each came from, and whether its place was ambiguous): to a project file, its own profile; to anything else, the profile of the project file in use, then each folder’s that holds it, from the outermost in by the script as the viewer reads it (scriptPlaceOf), then its own where it is a folder (withOwn), then the leaves a scene’s Style shares with a profile (styleSharedLeaves: description, look, palette, color_temp_bias, lens_character, prompts) — each overriding the one before leaf by leaf, a list replacing a list whole. An entity outside the script takes the project’s; one listed twice, or under a folder listed twice or under itself, takes no folder’s (ambiguous). composePrompt(own, treatment, leaves:) is the one composition: the entity’s words first — the treatment’s preset, which leaves may hold, adds nothing: the chosen style is stored, never composed — then each primary leaf of leaves that holds words, as Camera: … in treatmentPromptLeaves’ order, then the treatment’s words — each part trimmed, an empty one left out, the rest joined by ; ; it never replaces the entity’s words and writes nothing back, so that rendering again names nothing twice. styleChanges(fields) is what a description of a picture’s look writes (the plug-in interface’s VisualStyle), under visual_style, all or none (a leaf beside visualStyleLeaves, a preset included, which a describer never writes, refusing it at its path): each leaf trimmed; a null, an empty text or an empty palette telling nothing and leaving the file as it was; a value of another type, a text over 4,000 characters or holding a control character, a token of no vocabulary, or a palette of more than eight colours or one not #rrggbb refusing the whole description with its path. It is written as a character’s description is (withAnalysisApplied).
input_limits.dart is what a model’s catalogue entry says one of its inputs takes (Project Specification §14.8). inputRefusal(spec, mime:, name:, bytes:) says why the input that one entry of capabilities.inputs describes does not take a piece of the film’s media — formats where the input lists formats and the media is none of them, max_mb where the media’s measured size is more than it names — or nothing where it takes it or cannot tell. The formats a piece of media is (mediaFormatsOf) are what its media type says — the catalogue’s own for a type it names, jpeg and jpg for a JPEG, mp3 for an MPEG sound, else the type’s subtype less any x- (image/jpg, audio/x-m4a) — together with what its name’s extension says, either being enough, so that a type recorded in an older spelling never refuses what the file’s name says it is; a megabyte is read as a mebibyte, the larger of its two readings, so that nothing a model takes is refused, and a size nobody measured is held to none. inputFormatsOf and inputMostMegabytes read the two limits. The host holds a render’s inputs to them before a plug-in sees them (§6.11), and the render dialog offers only the pictures a model’s first frame takes (§9.3).
MediaSlot (media_slot.dart) names where an insertion goes: MediaSlot.group(name), a group of the container, or MediaSlot.path(dotted), an array at a dotted path outside it, which a later page may name and the attachment does not serve yet. Its JSON has an explicit discriminator, group or path; anything else — an earlier refs or voice slot among them — is refused as invalidMediaSlot, so a journal whose target no longer decodes ends as an undecodable payload does. MediaInsertionResult carries a copied document, whether it changed, and a MediaSlotProblem.
Folder path arithmetic is media_paths.dart: splitPath returns PathParts, the root and segments of a path, for folder comparisons and safety checks. It recognizes drive letters, UNC hosts and leading separators, resolves . and .., and never climbs above an absolute root; PathParts.isAbsolute says whether a root is present. The windows parameter makes separators explicit, so tests on either platform can check both path conventions. Project creation does not choose or write a media locator. Old manifest locators do not affect loading.
5.11 The script’s list — filmopen_format/children.dart
Section titled “5.11 The script’s list — filmopen_format/children.dart”Project Specification 1.12 replaces the three story levels with folders (fo), and the four root lists with one list, children, on the project file and on a folder (§8.5, §10.1 of the format). This file is the rule in one place, pure Dart, and everything that reads or edits the script reads it. Nothing of 1.11’s structure is read: storyChildListKeys and the three unit types are gone.
- The entry grammar. Folders and scenes mix in the list, so every entry carries its type.
ChildEntry.parse(text)reads a full reference first (sc_5_maria_v1, through the filename grammar of §5.1), which pins that version (pinned, aVersionStem), and otherwise a short stem<type>_<tag>: what stands before the first_is the type when it is two lower-case letters (the format’s §5.2 and §5.4), and what follows is taken as the tag, whatever it is — one that names nothing is looked up all the same, and dropped (below) or reported unresolved.childTypesis{folder, scene}.shortStem(sc_5) names the folder or scene whatever version an entry pins, so two entries with one short stem list the same thing twice;referenceis whatFilmProject.resolvetakes — the full stem of a pinned entry, else the tag.ChildEntry.of(type, tag)is the entry a writer adds: the short stem, which follows the version in use. - What is reported, never guessed (
ChildProblem):childUntyped, a bare tag ("5") or anything else that names no type, an entry that is not a string among them (kept in its place, shown as the JSON writes it);childWrongType, an entry naming another type (ch_kira, a shot, a type the format does not know such as 1.11’sep_e1), with the prefix as its argument;childDuplicate, a second or later listing of one folder or scene.storyChildren(json)answers a file’s entries in order and reports a second listing within that list; the walk of the whole script reports one in another list the same way, in the order it draws the rows (§5.12). An absentchildren, or a value that is not a list, is an empty script or folder, and 1.11’sseasons,episodes,sequencesandscenesmean nothing. The codes are said infilmopen_l10n, as every code is:Labels.childProblemText(entry)(childProblemUntyped,childProblemWrongTypewith the type the entry names,childProblemDuplicate), the reason a flagged row will carry. The fourth thing the format’s §8.5 has a reader report, a cycle, is not this file’s: one file cannot show that a folder lists itself through another, so it is the walk’s (§5.12). An entry with a type and no name (sc_) needs no code either: it is read as written and is an unresolved reference when it is looked up. - Dangling entries are dropped, not reported (the format’s §8.5).
danglingReason(entry, project)ismissingwhere the project has no file of the folder or scene an entry names anddiscardedwhere every version of it is set aside, else null — a full reference to a version that is not there, of a folder or scene that is, still names something and stays an unresolved row. The walk (§5.12) leaves such an entry out when the project is loaded; the file keeps it until its list is next changed. A save never rewriteschildren; the list’s own edits —addChild,removeChild,moveChild— start from the list as the reader shows it,withoutDanglingChildren(json, project), so the entry leaves the file with the person’s next change of that list and places are counted as the walk counts them. A read logs nothing of it; the list write that leaves an entry out logschildren.dangling {parent, child, reason, list}once for each,listbeing the list the write started from, decided from the view the write reads (§7.5), so a version the app has discarded and not yet published counts; it is logged when the write lands, before the index is published, so a publication that fails still leaves the line. - The three edits —
withChildAdded(json, entry, {index}),withChildRemoved(json, entry, {occurrence}),withChildMoved(json, entry, index, {occurrence})— copy the document (copyDocument) and leave every other key, and every entry they were not asked about, alone. An entry is addressed as it is written, so a pinned entry moves pinned andoccurrencechooses among equal entries — equal as a reader shows them (ChildEntry.text): an entry that is not a string (5,null, an object) is addressed by the JSON it is, so that a row and an edit count in one coordinate, a bad entry can be taken out of a list from the app, and of5and"5"the one that was pointed at goes; an index is clamped to the list, and in a move it is the place the entry has in the result. Adding a folder or scene the list already names — under either spelling — changes nothing, removing or moving an entry that is not there changes nothing, and achildrenof another shape is left for the JSON editor: the caller writes only what differs, and nothing here deletes a file. - The many-entry edits —
withChildrenAdded(json, entries, {index})andwithChildrenRemoved(json, entries)— are the bulk move’s (§7): every entry added fromindex, or at the end, in the order given, and every entry taken out once, its first listing of that exact text. The document is copied once for the whole selection and its copied list edited, never a copy per entry, so fifty entries cost one copy as they cost one write. An entry the list already names — as the list grows, so one given twice is listed once — is skipped by the single add’s rule and the place does not move; an entry that is not there is left alone; achildrenof another shape, and an empty selection, change nothing. - The numbers an application gives what it creates (the format’s §17: a short name says nothing a person may change).
nextSceneTag(project)is one more than the highest number any scene’s short name is, whatever gaps there are, and1where none is a number;nextFolderTag(project)the same over the folders’ short names of the shapef<n>(folderTagPrefix), thefkeeping a folder’s cues (cu_f1.1) from a scene’s (cu_1.1). Both count overtakenChildTags(project, type)— every short name a file of that type has, so a file in no list counts, and every one achildrenlist names, in any version of the project file or of a folder, whether or not a file answers to it: an entry without its file may be a colleague’s that has not arrived yet, and a new folder or scene under that short name would be taken for it. The dialog refuses such a short name as taken, and so doesaddChild. Both rest onnextNumberedTag(tags, prefix:), which counts inBigInt— twenty digits are a valid segment — and proposes the lowest free number where one more would be longer than a segment may be, so the proposal is always a short name. - What a cue belongs to (the format’s §11.2).
cueOwnerOf(json, tag:)answers aCueOwner: the folder itsfolderkey names, else the scene itsscenekey names, else the scene of its own short name (cu_5.1→ scene5),isNamedsaying which; null where nothing names one and the tag has no dot.foldercomes first where a file carries both, since the two say different things:scenewhose block idsblocksare,folderwhere the cue hangs. 1.11’sunitis not read.
folderKinds (vocabulary.dart) is the vocabulary a folder’s kind is offered from (§10.1 of the format); a file may name another token, or none.
5.12 The script as one viewer sees it — filmopen_format/script_outline.dart
Section titled “5.12 The script as one viewer sees it — filmopen_format/script_outline.dart”Every version of a folder has a children of its own, so what the script holds depends on which versions are in use for the viewer (§5.4). scriptOutline(project, viewer:) is the one walk of the script, which the tree, the Script page, a folder’s contents and the move dialog read so that they agree: the project file’s entries (FilmProject.selectedProject) depth-first, each folder’s in its own order, as a ScriptOutline of ScriptRows. A row is an entry as written (entry, a ChildEntry), the file whose list holds it (listedIn), its index there and its occurrence among equal entries — what §5.11’s edits take, counted in the list as the reader shows it — the number of rows that list draws (siblings), its depth, the group the entry names when the project has such a folder or scene, the resolution of looking it up (FilmProject.resolve, so a short stem is the version in use and a pinned entry that version alone), and, for a folder that resolved, its children.
- What one file cannot show is reported here. A second listing is decided in the order the rows are drawn — mark an entry, descend, take the next — so the later row on screen carries
childDuplicate, in the same list or in another, and its first listing is clean. A cycle is an entry naming a folder above it (isCycle); it is the report where it is a second listing too, being the more exact. Neither is followed: a second listing’s contents are under its first. Both compare short stems, so a folder is inside itself whichever version the entry pins. - A browsing read. An entry is looked up for the viewer, whatever the status of the file that lists it, as the tree’s references always were (§5.4): a fork is what its author sees in their script, under an official folder too. §6.1’s other half — a file an official pointer names reads the official world — is for rendering a version’s own references and for an export; such a reader passes
referencing: listedIntoresolve, andScriptRow.listedInis there for it. - Never guessed, and what is gone dropped. An entry that names neither a folder nor a scene is a row with its
ChildProblemand nothing else; one that names a folder or scene the project lacks, or whose every version is discarded, is no row at all (§5.11,ScriptOutline.droppedlisting it with its reason for the reader that logs); any other that resolves to nothing is a row with theResolutionProblem. Where the entry names a folder or scene the project has but no version can be chosen — several authors and nothing official, a pinned version that is not there —groupis still set, so that a reader can reach its versions from the flagged row, and the folder’s lists are not followed. - The lists as the viewer works on them (
scriptOutline(…, workingOf:)). A reader may say which version’s list to read for the project file and for each folder named by a short stem — the state’sworkingScriptOutlineanswers the viewer’s assistant draft (§7.3) where there is one, so that a folder the assistant files with no parent is in the script and a scene it moves is drawn once — and the walk reads that version’schildren(ScriptOutline.root,ScriptRow.contents), else the version in use’s. Only the lists follow it: every row still draws the version in use (entity, its name and content), a pinned entry is read at the version it pins, and Not in the script orders its folders by the same lists it draws them with. WithoutworkingOf— the block-move dialog of a scene’s script (filmopen_forms’script_move.dart; the tree’s Move to… reads the tree’s walk and offers its drafts), the plug-ins’ctx.project.script(), a style’s inheritance — the walk is the versions in use’s, as before. - One folder version’s script (
folderOutline(project, folder, viewer:)): the rows beneath the version given, followed into its subfolders by the same walk — that version’s ownchildren, never the version in use’s, the folder itself an ancestor of its rows so that a list naming it is a cycle. A folder’s Script and Workflow tabs read it only for a version the script does not draw, the rows of the walk otherwise;childRowsOfstays one list deep for the Contents card. - Not in the script (
unfiled; the format’s §8.5). Every folder and scene of the project that no drawn list names — named, not resolved, so an entry that pins a missing version does not put its scene in both places — each once: first the folders no other unfiled folder names, each with what it holds beneath it, then the scenes still not drawn, then whatever is left, which only folders that name each other leave behind (the first is drawn and the walk’s own cycle report does the rest). A project made before folders therefore shows an empty script and its scenes here. While the reader has not chosen a project file (§5.5: several authors, nothing official, no pick) there is no script for anything to be outside of: the outline is empty on both sides until the chooser is answered, rather than every scene being called unfiled. - In the recycle bin (
ScriptRow.isDeleted): the version a row draws is deleted (§5.4). The row is drawn and walked as any other — the tree keeps its place, struck, and it opens that version — and what consumes the script (the plug-ins’project.script, §6.11, and the exchange’s picked walk,PickedScript.treeJson, §6.18) leaves it out, what a deleted folder holds taking its place. childRowsOf(project, parent, viewer:)is one file’s list alone — a version the script does not draw, such as another author’s folder on its own page — with a second listing reported within that list and folders not followed.cueGroupsOf(project, type, tag, viewer:)answers the cues that belong to a folder or a scene (§5.11’scueOwnerOf, read from each cue’s version in use, else its first), in natural order, so a cue that names a folder is never a scene’s, whatever its own short name is. Where no version of a cue is in use none is chosen to speak for it: the cue belongs wherever any of its versions says, so it is shown — flagged, as every ambiguous reference is — in each of those places and hidden in none.cueGroupsByOwner(project, viewer:)answers every owner’s at once, by the owner’s group key (sc_5), each cue resolved once — what the tree reads, so that a long film’s Cues rows do not resolve every cue once per row;cueGroupsOfis its list for one owner.
A folder is, or holds, another. folderIsOrHolds(project, moved, destination, viewer:) answers whether the folder an entry names is the folder destination or holds it at any depth — read as the walk reads, each entry resolved for the viewer and a pinned one at the version it pins, wherever or whether the script draws the folder; a cycle the files already hold ends the search rather than turning for ever; a scene, an entry that names nothing and a folder no file has hold nothing. It is the rule a move asks before it writes (§7): a folder cannot go into itself or into a folder beneath it.
5.13 A scene’s cast — filmopen_format/scene_cast.dart
Section titled “5.13 A scene’s cast — filmopen_format/scene_cast.dart”A scene lists its characters in cast, one object each: { character, epoch?, outfit?, notes? } (Project Specification §10.2, A.5 — one selection per character, the entry’s own epoch being the character’s where it differs from the scene’s). castOf(json) answers the entries as copies all the way down (deepCopy: a nested value is never the file’s own object), an absent key being an empty cast, and null where the value is not that — not a list, a bare short name, an entry that names no character — which the form shows as it is and never writes over (never guess). The edits are pure and return a new list: castWithAdded (the name alone, at the end, once), castWithoutAt, castWithEpochAt (the entry’s own epoch, or no key for as the scene), castWithOutfitAt (the entry’s outfit, or no key for the character’s default_outfit), and castWithCharacters(cast, names), what an agent’s typing means — the names in their order, an entry that stays keeping everything it holds (its epoch, its outfit, its notes, any key this reader does not know), the k-th time a name is given taking the k-th entry of that character so that typing back what a file says changes nothing, a new entry being the name alone and never a second one for a character. writeCast writes the list, or removes the key where it is empty. Pure Dart, tested in test/scene_cast_test.dart.
5.13a A scene’s blocks — filmopen_format/script_block.dart
Section titled “5.13a A scene’s blocks — filmopen_format/script_block.dart”A scene’s script is its blocks, one shape for every block (Project Specification §10.3): id, character — the one speaker — dialogue, direction, action, duration_sec (the seconds the writer asks for; absent is auto), beats (its beats in their order, each {position?, description?, duration_sec?} and the keys a reader does not know — Beat; position one of beatPositions, before_dialogue, during_dialogue, after_dialogue, absent where there is no dialogue when a beat is added, another kept as written; part of its text; an empty list never written), subtitles (its subtitles in their order, each {position?, text?, duration_sec?} and the keys a reader does not know — Subtitle, the beat’s shape with text for description, read, written, added and refused exactly as a beat is; the lines burnt over the picture, timed by the beats’ rule, layTimed §19; part of its text; an empty list never written), output_duration_sec (the seconds the scene’s output gives the block, set by a person or the assistant; absent, the timeline’s rule decides; a value not above zero read as absent and kept in extra until a value is set), lang, prompts (its first_frame and last_frame, part of its text — blockTextKeys — each prompt’s created aside), updated (when its text last changed, as the writer stamps it; null for a block unchanged since it was written), and every key a reader does not know. ScriptBlock is one block: fromJson answers null for what is not a block — anything but an object, an id that is not a string, a known key of another type, a beats that is not a list of beats Beat.fromJson reads (a position or description that is not a string, a duration_sec that is not a number above zero) or a subtitles that is not a list Subtitle.fromJson reads, a text beside a dialogue or an action — and reads a block written before 1.23 by the format’s one rule: its text is the dialogue where its kind was dialogue, narration or lyric, else the action; offscreen, voiceover and dual are (O.S.), (V.O.) and (dual) at the head of direction; a title’s duration_ms is its duration_sec. toJson writes id first, the keys the block holds, then extra in its order, and never kind, text, offscreen, voiceover or dual; the Dart names of the two numbers are duration and outputDuration. The id is never renumbered and a number is never given out twice in a file, by these changes and by copy-left’s block copy, which writes next_block wherever an insert, a replace or a delete would let the next number fall (applyCopy): nextBlockNumber(json) is the larger of the scene’s next_block — a whole number however JSON stored it, 7 or 7.0 — and one more than the highest bl_<n> present (1 with neither), nextBlockId its id, and withNextBlockRaised(json, n) raises next_block past n. The changes are by position and in place, each rebuilding the blocks list rather than editing the index’s own, each refusing (ScriptEditProblem.wrongShape) a blocks that is not a script and answering a BlockEdit: withBlockAdded(json, block:, at:) puts a block — empty, or a copy of one with every key but its id — under the next number and raises next_block; withBlockRemoved(json, at) takes any entry out and keeps the next number where it was, so a deleted block’s number never comes back; withBlockChanged(json, at, apply) writes apply’s answer in 1.23’s shape, nothing where the answer is the block as read (a block of the old shape is rewritten only when it changes), gives an id-less block the next number at its first change, carries apply‘s beats and subtitles so that an ordinary edit keeps them, and never lets apply set the id or the block’s updated, which stays as read for the writer to stamp (§10.3); withBlockNumbered(json, at) gives an id-less block its number before anything in it changes — for dictation, bound to the block while its words are on their way — and leaves the rest as written; withBlocksReordered(json, from, to); withBlockPrompt(json, blockId, kind, prompt) sets the block’s prompt of that kind — or removes it, and a prompts it leaves empty, for null — and nothing else of it, refusing a block id the scene does not hold (noEntry) and a block prompts that is no object (wrongShape), the writer stamping its created and the block’s updated; withBeatAdded(json, at, position:) appends a beat — during_dialogue where none is given and the block has dialogue, none where it has not —, and withBeatChanged(json, at, k, apply), withBeatRemoved(json, at, k) (the last taking beats with it) and withBeatsReordered(json, at, from, to) change one, each through withBlockChanged, a beat out of range being noEntry with nothing written; withSubtitleAdded, withSubtitleChanged, withSubtitleRemoved and withSubtitlesReordered are the same four changes over subtitles; beatsTotal(block) is the beats’ seconds, null where none gives any, and beatsSummaryOf(entry) their count and total for any entry; subtitlesTotal and subtitlesSummaryOf are the same over subtitles. The format logs nothing and answers a refusal code; the person’s change of a beat is logged by the editor that makes it. blockLogName(id, index) is how a usage record and a warning name a block, a stable field: its id where that is bl_<n>, else #<place>; a log line that names a block by it carries the block’s own id beside it as block_id. A scene’s next_block is never copied itself (uncopyableKeys). blockIdsUsedTwice names the ids more than one entry gives, blockIdsOf every block’s id in file order; coverageFrom(ids, owning, through) is a block media’s coverage, the consecutive ids from the owning block through the one chosen, empty where either is missing or the last stands first; coverageIntact(ids, blocks) whether the scene’s ids from the first covered to the last are exactly the coverage — false where an end is gone, the last stands first, or a block was inserted, deleted or moved inside, never an error — so that a render over blocks reads outdated with no copy of their content kept (§10.3); speakersIn the characters the cast and the blocks name, blockSummary the parts of a block’s condensed line (the speaker and the dialogue, else the action, else the direction; the speaker kept beside the direction or alone on an empty block, never beside an action), splitDirection a direction’s Fountain extensions at its head ((O.S.), (V.O.), (dual)) and the note after them (DirectionParts), which a screenplay page puts on the cue’s line and in parentheses under it, and blockLineOf a block as one line of text, which a comparison’s row shows — the speaker with those extensions, the dialogue, the rest of the direction in parentheses and the action. speakerName(project, scene, json, tag, viewer:) is the name a speaker is shown by, as the scene’s own reference to the character (§5.4): at the epoch the scene’s cast entry gives it — read entry by entry, so an odd entry hides no other’s — else the scene’s own epoch, read from the document given (a draft’s), the version the scene sees, through official where the scene is official; else the character’s name for the viewer; else the short name as written. The editor’s lines and pull-down read it; the Script tab still has its own copy of the same rule.
5.13b The script as a block diff — filmopen_format/script_diff.dart
Section titled “5.13b The script as a block diff — filmopen_format/script_diff.dart”scriptDiff(left, right) answers the rows of two documents’ blocks as a git-like diff (§9.3), each row a DiffRow with a DiffKind and the block’s row id on each side that has a cell. A row id is the block’s id as the comparison names its row — the id, <id>#<k> for its k-th repeat in the file, #<place> for an entry without an id — so a repeat pairs by its occurrence and an id-less entry by its place, the copy safeguards for both staying where they are. The rows follow the longest common subsequence of the two sequences: a pair in it is same or replace, told apart by the two blocks’ canonical JSON as blockCase tells them; an id on the left alone is added, on the right alone removed; an id on both sides outside it is two rows, movedOut at its place on the right (the left’s cell blank) and movedIn at its place on the left (the right’s blank). Where two runs are equally long the left’s row comes first, so the answer is the same every time; no move is detected beyond that. Two sequences of the same ids in the same order are paired in place, in one pass, since the common-subsequence table is the two lengths’ product. display is the row id a row is shown under and is shared by a move’s two rows, so rows are told apart by position. Pure, logging nothing. A stated limit: a block whose own id is spelled like a repeat’s row id (1#2 beside two blocks 1) shares that row id, as the comparison’s rows already do.
5.14 The address of a server — filmopen_format/server_address.dart
Section titled “5.14 The address of a server — filmopen_format/server_address.dart”ServerAddress is the one grammar of a server setting’s value (§6.11, Project Specification §14.4) — one implementation the manifest reader, the door and the box all read, so that no two of them can disagree. parse answers an address or null: http or https, a host, an optional port and an optional base path, with no user info, no query and no fragment; the text is trimmed, the scheme and host lower-cased, the scheme’s own port and a trailing / left off, so one server is one value (== and hashCode are over that text). Uri.parse removes dot segments as RFC 3986 §5.2.4 says, so a base path can name nothing above the address. isThisComputer is 127/8, ::1 and localhost with the names under it; isPrivate adds 10/8, 172.16/12, 192.168/16, 169.254/16, 100.64/10 (where Tailscale lives), fc00::/7 and fe80::/10 — and any host name, since what it resolves to cannot be known here, so the server box’s warning for a plain http address (§9.4) is only ever for an address that is certainly a public IP address. An IPv4-mapped IPv6 address (::ffff:10.0.0.5) is read as the IPv4 address it names, and a rooting dot is not part of a name, so localhost. is this computer. resolve(path) joins a route to the base path and keeps the query the plug-in wrote, and answers null for anything that could leave the address — a path that is itself an address, one starting //, any . or .. segment (%2e%2e among them), a percent-escape that is not two hexadecimal digits (a sign or a space is not one, so 100% final.png is refused rather than sent as another route), an escape whose bytes are not UTF-8, a control character, and an empty or trailing-empty segment; a fragment is dropped, never sent. A segment is read as a server that decodes before it removes dot segments would read it — what RFC 3986 §6.2.2.2 warns against and several real stacks do — so ..%2F.. is refused exactly as /../../ is, while a%2Fb, neither of whose parts is a dot segment, is a name and passes. Every segment is escaped exactly once: the address keeps its path decoded and lets Uri write it, and a route’s escapes are read back one at a time — the text between them kept as a run, so a character above the basic plane survives — before it is written again, so a base path of /My%20Comfy is sent as /My%20Comfy and not as /My%2520Comfy, and /ver/año%20x.png keeps both halves. parse answers an address or null and never throws, whatever a person types or a manifest ships. sameOrigin compares scheme, host and port, a scheme’s own port counting as none, and is what a redirect may stay within. origin puts an IPv6 literal’s brackets back, which Uri.host takes off. Pure Dart, tested in test/server_address_test.dart.
5.15 Which film this is — filmopen_format/project_key.dart
Section titled “5.15 Which film this is — filmopen_format/project_key.dart”projectKeyOf(FilmProject) and projectSourceKey(ProjectSource) answer one token per film, so that work begun in one film can never end in another: a run of a plug-in is bound to its film by this key (§6.11), and the jobs’ journal binds each insertion job to it (§6.12–§6.13). Two FilmProjects cannot be compared for the purpose — every load builds a new index, forViewer builds another and a re-read a third, all of one film — so the key is the film’s source. A source with a rootPath answers folder:<canonical path>; one without, or one whose path is empty or only spaces, answers session:<n>, taken once and kept for as long as that source object lives (an Expando, so a source no longer held is forgotten with its token). Two folders holding the same film are two films; one folder opened twice, or re-read, is one, since ProjectController.refresh re-reads the same source and a second Open folder… makes a new DirectoryProjectSource of the same path. A project without a root — the bundled sample’s session copy, extracted to a folder or held in memory, a test’s fixture, a session-only project where there is no disk — is itself and nothing else.
This is the one project key: projectSourceKeyProvider (§7.1), which the browser resets its navigation on, answers projectKeyOf and has no rule of its own. So two session copies of the sample are two sources, though both are a SampleFolderSource (an OverlayProjectSource on the web) of one label, and the browser keeps no selection pointing into the copy that was closed; and one folder whose path is written with other separators is one source, so a re-read under the other spelling keeps the navigation.
A session: key is of this isolate and this run of the application: the counter is library-private and starts again at 0 in every isolate, so such a key may never be written to a file and read back after a restart — two different films would meet under one token. A folder: key is text derived from the path, and may be. canonicalRootPath is text and never a question put to a disk, the format knowing no dart:io: separators become /, a run of them becomes one except a leading // (a Windows share’s root), and a trailing one is dropped from anything longer than a root. That is all — case is kept, and a link, a subst drive and an extended-length prefix are not resolved. Only what is certainly one folder is joined: folding a drive’s or a share’s case would join C:\Films with c:\films, which NTFS does not tell apart, but it would also join two real folders on a share exported from a case-sensitive file system or in a Windows directory marked case sensitive, and the two errors do not cost the same. A false split costs a run begun under one spelling not seeing an index re-read under the other — and, where the film changed on disk meanwhile, a refusal in place of a write, since every version goes out with create: true and a number already taken takes the whole run with it (project_writer.dart). That is the safe direction, and the same thing binding does for a film that was closed. It also counts one folder as two films anywhere something is kept per film — the recents list, the jobs’ journal. A false join makes a run treat a stranger film’s index as its own, which is what the key exists to prevent. isDurableProjectKey says which keys may be written down; folderNameOf is the one rule for a folder’s own name — the last segment, either separator ending one — which DirectoryProjectSource.label (§6.2) and DevelopmentFolder.label (§9.4a) both read rather than each splitting a path of its own. Pure Dart, tested in test/project_key_test.dart, on the real folder source in filmopen_store/test/directory_source_test.dart and on the provider in filmopen_state/test/project_provider_test.dart.