Skip to content

Exchanging a film

filmopen_exchange holds pure Dart exchange rules over the format’s project index. It never reads a credential store or fetches content. Export, reading and planning leave files unchanged. Applying a reviewed import writes project files through the guarded store writer; library files remain unchanged. Its identities, field paths, projections and dollar-key codec preserve file content independently of authors and versions.

The exchange walks the script from the project file in use through each folder or scene’s pick. It resolves a listed tag for the viewer even where the entry pins another version. An occurrence retains that pin; a differing pin is reported as pinned_differs with both stems. A repeated short stem, including a cycle, is retained as a duplicate occurrence and is not followed again. Entries naming no type, another type or no version are flagged and have no children. Folders and scenes no picked list reaches are unfiled. The walk does not change the application’s own pinned-reference walk.

Only tags passing the format’s tag grammar enter resolution. A malformed typed reference is unresolved, so its text cannot be interpreted as an exact version stem and bypass the viewer’s pick. Occurrences omit empty children arrays; file content arrays keep their original shape.

Selectors name project and entity ids, scenes, their siblings and ancestors, folder descendants, locations used and characters speaking. They share the picked walk and the format’s resolution; a full reference loses author and version before resolving, and an official file never changes exchange references to official-only resolution. Explicit missing epochs remain unresolved; implicit epochs fall back to the project’s default.

Folder descendants follow each named folder’s picked children independently of duplicate occurrences, including a named unfiled folder, and stop cycles. Scanning scenes and catalogue files does not report no_pick; that problem belongs to an identity the selection names whose file has no pick.

A scene’s closure includes cast characters at their entry’s epoch or the scene epoch, block speakers absent from the cast, and their outfits at the resolved character epoch. The cast outfit wins over the character’s default outfit. The closure includes the location with its optional epoch override, props as tags or epoch-bearing objects, the style, explicit outfit and typed resource lists, shots and cues. Explicit shot and cue lists choose membership, including empty lists; absent lists use the scene’s natural shot groups and cue owners. Catalogue references are not scene resources and are reported unresolved. Library creative content follows the same picks as project-local content. Failed closure references are reported as unresolved_reference with the scene, field path and reference; inherited outfit paths identify the character’s default outfit, and speaker paths locate the block. Empty listed tags are unresolved; no prose mention invents a reference.

The recycle bin (Project Specification §7; §5.4). Selection never reads an entity’s deleted: every scope, the pattern assembly and the picked walk select as they would without it — a deleted location’s scenes stay selected for location_usage, a deleted character’s scenes for character_arc. The assembler writes nothing of a deleted version in use: no object, no sources line, no $id/$stem; the picked walk’s occurrence of one gives its place in script to what it holds (PickedOccurrence.output, PickedScript.treeJson), and unfiled leaves it out. Each is reported once as deleted with no detail, and a scene’s reference that reaches one as deleted_reference with {scene, path, reference} — never unresolved. ExportDocument.skippedDeleted counts the entities left out, each once. The project file is never left out. The flag is the viewer’s version in use: another viewer with a live version in use exports it.

A scope has a permanent name, a description, an argument contract and a selector/projection recipe. Arguments are checked before the recipe runs. A tag scope accepts a list of strings; a matching full stem is reduced to its tag, while invalid or missing individual tags report unresolved without discarding valid tags. Named-object arguments contain exactly their declared parts. A named-object argument’s tag parts are read and reported as a tag scope’s. Group selections expand to concrete epochs before their projections union, so a group and a concrete selection share the same file projection.

The registry exposes names, descriptions and argument descriptors through scopes(). It contains nine scopes. all selects the film’s documents whole for handover. project_overview selects the project in brief, characters and locations in summary, and every folder and scene in synopsis. scene_with_context selects named scenes and their closure whole, their sibling scenes and every folder in synopsis. location_usage selects named locations whole at all epochs and scenes set there in synopsis. character_arc selects named characters at epochs used by their scenes, those scenes whole and their ancestor folders in synopsis. folder_whole selects named folders and descendants with scene closures whole, and ancestor folders in synopsis. character_lines selects the named speakers’ dialogue with scene names, synopses and block ids; multiple speakers compose by block identity. scene_dialogue selects named scenes whole, every other scene and every character in synopsis, and the named scenes’ ancestor folders in synopsis. An incompatible recipe requires a new scope name; existing names remain available.

focus takes {folders, scenes}, both required lists of tags and either empty. It selects the project with its epochs and stored children, every character and location whole at every era, every folder and scene in synopsis, and the named folders with everything beneath them and named scenes with what appears in them, whole. With both lists empty it is the project overview plus the project’s children and whole characters and locations. The stored project list participates in an import’s base comparison. An export above the exchange doors’ 8 MiB bound (§6.11) is refused there, as for any scope, rather than paged.

The project file is the explicit selection after applying the viewer, including a choice made only in the current view. When the format’s generic pick names that file, its reason and staleness are retained; otherwise the explicit choice is reported as picked, without inventing staleness. Other identities retain the format’s normal pick resolution.

A cast-only speaker’s scene has blocks: [] under character_lines, meaning cast without lines. On import, [] changes nothing.

The assembler writes the manifest and project first, then creative groups, script files, local catalogue files and batch records in their fixed collection order. It emits only populated arrays, with groups and spec files in natural tag order and epochs in declared order followed by undeclared epochs in natural order. Group names come from the format’s nameOf. Every export includes the selected project with at least its name and epoch frame and the complete picked script alongside the selected content. The manifest, local catalogue and batch records appear only in all; the shared catalogue never appears. The manifest remains whole, including filmopen and unknown keys. Batch content uses the everything projection with notes retained.

A file object’s $id and $stem precede its projected content; dollar-prefixed content keys gain one dollar recursively. The manifest has $id alone. Source rows follow the file objects’ body order and identify the source stem, union projection, SHA-256, pick reason, optional staleness and library origin. Source hashes use canonical projected content before escaping; manifest hashes use its whole JSON. The document reports collected problems without identical duplicates. If no project is selected, export validates argument shape and returns only the envelope with the project’s no_pick and owners.

The envelope carries format version 1, exchange version 1, kind export, its content id, scope name and original arguments, project tag, viewer, UTC export time, sources and problems. The id is e- followed by sixteen hex digits of SHA-256 over canonical document content excluding export_id and exported_at. The wire text is two-space indented JSON with one final newline; bytes measure its UTF-8 encoding. Notes are content like any other key: they travel wherever a projection carries a file’s whole content (whole, everything) or names them, and nothing asks.

The lean export (assemble(..., lean: true), exportScope(..., lean: true), exportEntityDocument(entity, lean: true)) is the targeted document an assistant reads (§6.11a): the same body and the same $ids, without sources, $stem, export_id and exported_at — the envelope is filmopen, exchange, kind, scope, project_tag, viewer, problems — and with the picked script (script, unfiled, and the walk’s problems) only where the selection selects a scene. Its working map, keyed by exchange id, writes an entity from the version a change would be made from instead of the version in use; a full export is never overridden, since an import reads its sources’ hashes, and working without lean is an ArgumentError. A full export is byte for byte what it was without the flag.

The document, group objects and file objects have separate namespaces. Top-level document keys belong to the exchange; a creative group has id, name and epochs, and no file or source row of its own. File objects are recognized by $id. Their identities omit an entity’s author and version, retain a creative epoch, and retain the author for an unversioned batch receipt. The manifest’s identity is manifest.

A full document’s envelope keys occur in this order: filmopen, exchange, kind, export_id, scope, project_tag, viewer, exported_at, sources, problems. The body follows with manifest, project, characters, outfits, locations, props, styles, misc, documents, folders, scenes, shots, cues, models, platforms, plugin_manifests, batches, script, unfiled, omitting unpopulated arrays and all-only content in narrower scopes. File content retains its source key order. Problems use id, problem and optional detail; occurrence flags use child_untyped, child_wrong_type or unresolved, and duplicates use duplicate: true.

Every entity projection omits exactly filmopen, author, v, forked_from, version_label, label, created, updated and by. Names always remain, as do the project’s complete epoch declarations. Everything retains every other field, including unknown content; whole omits media, picks, preview, hooks and x. Summary selects kind, aliases, summary, description, logline and synopsis; synopsis selects kind and synopsis; project overview selects kind, genre, year, logline, synopsis, lang, rating and runtime_min. Frame selects only the retained name and project epochs.

A named projection selects field paths, and its label does not imply whole-file behavior. Identified scene blocks, cast entries, project tracks and character relationships retain respectively id, character, id and character when projecting fields; other lists remain whole values. Speaker filters compose fields on matching blocks rather than spreading one speaker’s fields to another. A named media path uses reference to identify entries, retains usable kept or unreviewed entries, removes archived or deleted entries together with picks or preview that exactly name their references, and drops each retained entry’s kept_at, which is display only. It does not infer alternate references. Everything retains media entries and flags unchanged.

The handover excludes other versions, commentary, official pointers, shared catalogue documents, plugin code and media bytes. It carries the project-local recognized documents and library creative picks, including unknown fields, notes, media references and receipt contents. Narrower scopes are model-facing projections: a named media path retains usable kept or unreviewed renders, omits archived or deleted renders and omits the time a render was kept; content is not anonymized, so names, addresses and URLs written into content remain. Export reads the indexed files and does not read open drafts; a consumer needing the last keystroke settles drafts before obtaining the index.

Export produces exchange version 1. Compatible growth adds selectors, projections, collections or optional keys without changing existing scope names or this encoding. An incompatible scope uses a new permanent name; an incompatible encoding requires new doors while version 1 remains supported by its existing doors.

readImport(document, projectTag: ...) reads a partial body with or without its envelope or kind. It returns decoded file patches in document order, document notes and problems, without resolving a pick or validating a write. File objects under the project, in folders, scenes, shots and cues, and inside creative groups’ epochs retain their unescaped content separately from exchange metadata. The fixed collection table is shared with the assembler. A full-stem id loses author and version for identity but remains a baseline hint when no explicit $stem was supplied.

The reader refuses the whole document for bad_document (not an object, or neither an envelope nor a recognized body), unsupported_version (a present exchange version other than 1), or other_film (a different envelope project tag or project id). An unrelated patch remains usable when another element is malformed. script and unfiled produce structure_ignored; the manifest, catalogue collections and batches produce not_importable; an unknown top-level array produces unknown_collection.

A non-object element, an object with neither identity key, or an identity placed under the wrong type or creative group produces bad_element with its JSON path. An unreadable id produces bad_id. A creative group carrying content without epochs, a concrete file object lacking its epoch, or an epoch object lacking $id produces epoch_required. Group keys other than id, name and epochs produce ignored_key. Two patches with the same normalized id are both refused as duplicate_patch, with both JSON paths; the remaining patches are still read.

$replace and $remove must be lists of grammatical field-path strings. A malformed string produces a patch problem bad_path with that text; a malformed directive list produces the same code with the directive’s name. ImportNote carries kind, optional path and optional structured detail; ImportProblem carries code and the same optional fields. Absent optional fields are omitted. Consumers treat unknown problem codes as refusals and unknown note kinds as information. Codes and kinds grow additively; an id-less element uses bad_element.

A project’s file object may carry $declare, a list of {label, year?, as?} entries. An optional as is a valid short name, including on an existing label. A duplicate alias or one equal to any token after declaration refuses the whole import as shape at $declare.as. Each label must be a nonblank string after trimming, and a year must be an integer, null or absent. Otherwise the planner refuses the whole project change as shape at $declare, with the malformed list or entry as detail; no partial declaration is retained.

patchDocument(before, patch, type) receives the target’s whole document, header included, and a decoded file object. It returns a detached after, changed paths, explicit removed paths, information notes and an optional whole-patch problem. Paths use the field-path grammar, including identified selectors such as blocks[id=bl_3].dialogue. For a free-text id or an arbitrary extension key that cannot be named exactly, a changed path names its nearest grammatical containing value. A root key is reported as itself; a consumer tolerates a root path that FieldPath.tryParse refuses. A removal keeps its original grammatical selector when no stable key exists or its id cannot be printed. Content keys follow the format’s spelling convention and its explicit owner-defined exceptions. It neither resolves a pick nor validates or writes through the writer; those are the planner’s responsibilities. The record (Project Specification §18.3): what a patch supplies and changes is held to its type’s record as the merge reads it — at each object it merges into, a key the record does not name is refused whatever its value, a null included (the exchange stores it) — an empty object, which changes nothing, apart — while a null on a key the record names is the exchange’s stored value; a value supplied whole, an object the patch makes, a new item of an identified list and an item a whole replacement changes are held to their record whole — required members included, a null as a defined key’s value in an object still the stored value, never as an item of a list (cdl.slope: [1, null, 1] is refused) nor inside a union, a rate’s format.fps made or merged —, and a list replacing another to its own bounds and, of its items, only those it adds; a value equal to the one it meets, an item sent back as it stood, $remove, metadata and a project’s or folder’s children (the planner’s own rule, said entry by entry) are not. A breach refuses that patch record, its path the first problem’s and its detail each {path, code, value, expected?, allowed?}; the plan’s other changes stand, the review says it in one sentence naming the paths (importProblemRecord), and import.plan.record logs the id, the type, the problems, the content as supplied and ms. The check holds every import alike, a plug-in’s proposal and a person’s exchange file, since the planner has no origin.

A missing content key leaves its value untouched. Objects merge leaf by leaf; an empty object names no leaf. Scalars, including null, are values, and a present null is removed only explicitly. Value lists such as aliases and props replace whole, including an empty list. They have no stable entry identity across replacement; their filters retain literal meaning. A content key its type’s record does not name is refused (below), as is a changed value of another shape. Canonical equality is checked before writes and allocation. Changing between object, list and scalar shapes requires explicit replacement; otherwise the whole patch is refused as shape, with the affected path.

The projection’s identified-list table also governs import: scene blocks by id, scene cast and character relationships by character, and project tracks by id. Existing entries merge in place. New entries follow the nearest preceding entry named by the patch, or append when none precedes them; unmentioned entries retain their order. An empty identified list changes nothing. An exactly unchanged list is accepted before addressability or allocation is examined. A changed merge or a directive into a list with missing or duplicate keys refuses the whole patch as blocks_not_addressable, unless the list is replaced whole. Removing an existing entry’s identifying key is refused by that same rule; removing the whole entry is allowed.

Scene blocks use the format’s allocator. The original floor is nextBlockNumber(before), read once. An id-less new block receives nextBlockId; an explicit new numbered id below that original floor is refused as block_id_below_floor. Existing ids remain usable, and new free-text ids are accepted without changing the counter. Explicit new numbers are reserved before automatic ids are assigned. Whole-list replacement applies the same allocator. Using or removing a number preserves the floor in next_block; a changed derived counter appears in paths. A non-script blocks list is refused as blocks_not_addressable.

The exchange id must agree with the target’s type, tag and filename epoch. Supplied content type, tag and, for a library type, epoch must also agree, or the patch is refused as identity. For an empty creation baseline these values agree with the validated id. A scene’s epoch is editable content; a folder carries none (Project Specification §10.1). A project’s $declare belongs to the planner; on any other type it is stripped with ignored_key. $stem remains a hint; other unknown exchange keys are stripped with ignored_key.

The patch never writes filmopen, author, v, forked_from, version_label, label, created, updated, by, source, media, picks, preview, hooks, provider_bindings, active_reference or voice_binding. It also excludes a project’s contributors, next_epoch and epochs, and a scene’s incoming next_block. Only the planner’s $declare changes a project’s epochs and next_epoch. A differing value or an attempted removal produces ignored_key at the path; an identical echo is silent. These exclusions apply to root fields, including directives, and do not prevent writing an extension’s nested fields.

A project’s and a folder’s children is a value list, replaced whole. Each entry the patch adds must be a typed reference to a folder or a scene, else the change is refused shape at children with the entry as detail; entries already listed are kept as stored. Duplicates, cycles and references not yet present are not refused. A parent whose list is changed is named whole in the export used as base, so a list the person changed meanwhile is a conflict at children.

Directive order is replace, merge, remove. Every $replace path must have a value in the patch, otherwise replace_without_value refuses it. Replacement writes that value whole and deliberately permits a shape change. $remove deletes a key, leaf, identified entry or entry field; an absent target produces remove_absent. A removal equal to or inside a replacement, or supplied beside a value at the same path, refuses the whole patch as conflicting_directives, carrying both paths. Different selectors resolving to the same entry obey the same conflict rule. A refusal returns a copy of the original without partial changes. No absence deletes a key or a file.

An unchanged exported file patch, applied to its whole source document named by the stem hint, yields the same document with no changed path, removal or patch note. Project epoch echoes remain unwritten, and character-line projections merge onto the full script by block id. Manifest and batch data are information and do not become file patches.

planImport(project, viewer, document, base: ..., guards: ..., done: ...) turns the reader’s patches into a pure review on the index after applying forViewer. Each identity uses its current pickAt result; a project identity uses the selected project file. An author or version in an id is only a baseline hint. A creative epoch not declared by the selected project is refused as unknown_epoch; existing ambiguous versions are no_pick. An absent identity named by the base’s source rows, or the document’s where the base has none, is refused as missing_since_export, rather than recreated.

Each ImportChange carries its identity, action, current target, detached before and after content, changed paths, explicit removals, notes, optional refusal and a snapshot of completed stages supplied by the caller. ImportStage names create, content and pick. Content equality produces nothing before write permissions are checked. A changed pick owned by the exact viewer is an update; another author’s pick is a fork. A library-origin pick is always a fork, including in development mode. Under warning guards only another author’s project-local project file becomes an update, with guard_overridden; other foreign entities still fork.

A changed absent identity is a create only for a creative type other than outfit, or a folder or scene, under one plain tag, at a declared creative epoch, with a nonblank name and no existing or earlier planned group, except the character seed route below. not_creatable carries one of six detail tokens: dotted_tag, uuid_tag, type, group_exists, group_planned or no_name. A missing era of a known character, or a later era of a character first created by this import, is a seeded creation. Locations and other creative groups retain the ordinary group refusal. Creation leaves the entity unlisted unless the document lists it in its parent’s children. Planning first declares project eras, then rewrites aliases in typed positions in a detached raw document and decodes it again, retaining the reader’s duplicate_patch rule. Typed positions are creative epoch ids and a scene’s epoch, location_epoch, cast[].epoch and props[].epoch; other text stays unchanged. The project is planned and applied first, creative versions next, then folders and scenes in their original relative order. A scene precedes the folder that lists it when the document puts it first. There is no general dependency engine or rollback. A creation that fails keeps its own outcome while the parent keeps the entry, which the reader drops until the entity exists. Catalogue content remains non-importable information from the reader.

The planner matches declaration labels exactly once trimmed, using the patched project’s declared epochs. An existing label, including one declared earlier in the list, produces epoch_exists {label, epoch} and changes nothing. For the first unknown label, unless the list names the era’s own label, FilmProject.soleUnusedEpoch supplies the sole declared era only when epochUsage has no explicit use of its token: filename epochs, scene epochs and location, cast or prop epoch overrides all count, while an implicit scene does not. That era is relabelled in the patched content with its token and unknown fields preserved, an omitted year cleared, and epoch_relabelled {label, year?, epoch, was: {label, year}} shown in the review. Later unknown labels append. If no such sole unused era exists, a new label receives the importing viewer’s next token from the store’s allocation inventory, including tokens allocated earlier in this list, and produces epoch_declared {label, year?, epoch}. Labels never match by year; a relabel alone leaves next_epoch unchanged. The planner appends new eras in list order, encodes epochs with orders renumbered from one and raises next_epoch in the reviewed after content. Only epochs joins changed paths: the export frame does not carry next_epoch, and epochs detects a declaration made since the base export. An empty or all-known list changes nothing; a new declaration without a viewer refuses no_viewer. Extra entry keys other than as are ignored. Aliases map existing labels to their tokens as well as newly allocated labels; the notes and declaration-outcome log retain as. Epoch checks and age fills read the planned project declarations. import.plan.start, import.aliases, import.alias.rewritten, import.plan.refused and import.plan.ordered retain input, rewritten positions, decisions, final order/content and elapsed time.

A changed scene derives any missing cast character versions before itself, once per type_tag@epoch, using the cast override, scene era or default. Existing versions, including discarded ones, prevent a create. Unchanged scenes derive nothing. A known character uses seedSourceFor; a newcomer uses its first planned create and records that planned id, rather than an existing stem, as from. seedAtEpoch supplies the complete cleared review and its age, clearing and retention decisions (§6.1a). A known character explicitly patched at a missing era is seeded first, then patched. cast_version and seeded_from explain the source; cast_version_refused explains ambiguous, none or no_pick without refusing the scene. The planned/refused events retain source values, scene, character, era, candidates, decision reasons and elapsed time. Consent binds the seed source and epoch as well as content; generated metadata is supplied at write time. The store import adapter uses createAtEpoch(clear: true) for a seed, then saves the complete reviewed content plus the actual writer-owned header, preserving root removals and provenance. No automatic retry or compensation is added.

On a character patch, only a changed birth_date path from the patch itself, excluding inherited seed paths, calls ageFromBirthDate against the declared era. A fill adds appearance.age to the reviewed paths, including the before value and ordinary base-conflict comparison, with no extra note. A malformed imported date stays as written with badBirthDate; a summary-only edit or an unchanged date echo keeps the age. import.age.start and import.age.fill log the identity, birth date verbatim, era year, before/after age, decision/reason and elapsed time. There is no load-time rewrite, year-edit recomputation or new content validation.

Every planned write is dry-checked for a writable project and a viewer, refusing as read_only or no_viewer. An update calls the store’s checkSave with the same guards; a fork or creation calls validateContent. Invalid content is invalid_content, with the store’s ContentProblem name in snake case as detail, such as name_required, version_label_too_long, epochs_required or children_not_a_list. Identity and epoch-floor refusals retain identity and next_epoch_lowered. Validation never writes, reports a guard override to the log or publishes an exception’s text.

Source hints prefer a matching base row, then the document’s row, then the file’s stem hint. A differing hint produces pick_changed, whose detail is the old stem. A claimed scope is rerun on the current index with its arguments, any other key of the claim ignored; assembly’s mandatory project frame is included. A current source projection whose sourceHash differs produces changed_since_export. An unknown or malformed scope or a scope no longer selecting the id produces not_in_scope_now; a body-only import without a scope claim does not. A base export supplies the scope where present. For each changed or removed path that the projection selects, a current value differing from both the decoded base value and the proposed value produces conflict, with the path alone. Conflict selection follows the scope’s rerun projection by root key: a whole or everything part includes every root; name and project epochs are always frame; otherwise the root must be the first segment of a selected path. A selected field whose base value was absent is still judged; with no projection, comparison is unchanged. An excluded path is not judged and logs import.conflict.unjudged with its id, path, projection and reason excluded_by_projection. Missing values and explicit null remain distinct. A changed root key is reported as itself; literal root keys and parsed paths sharing a spelling are compared separately, so an unchanged route neither hides nor invents a conflict in the changed one.

ImportPlan.sameAs compares ordered identities, actions, target stems, canonical before and after content, changed and removed paths, notes with their payloads and refusal and document diagnostics. Completed-stage transport is not a substitute for this consent comparison. toAnswer() produces typed, bounded ImportAnswer and ImportChangeInfo records, with the reader’s ImportNote and ImportProblem as its diagnostics. It carries no before or after values, stage artifacts or proposal id. Unknown problem codes mean refusal; unknown note kinds mean information.

The plan’s state follows this total table in order. ImportState also includes checked, which a dry-run door substitutes for proposed; a queued proposal’s id belongs to that door.

Condition State
A whole-document other_film, unsupported_version, bad_document or alias shape refused
At least one update, fork or create, including alongside independent refusals proposed
No write and at least one refused change, including alongside nothing refused
No write or refused change, including an unchanged echo with informational collections nothing

ImportWriter exposes update, fork, create and pickOnly, each returning a StageOutcome with completed stages and an optional bounded problem. ImportResult.outcomes holds writable changes’ outcomes in plan order; skipped nothing and refused changes are omitted. applyPlan runs one change at a time, starts with its carried done stages and continues past a failed change. Each outcome merges the change’s carried stages with the stages completed by this apply, including stages retained when a later stage fails. The caller supplies that stage map as the corresponding change’s done on a retry. Already completed writable changes remain in the result without another writer call. Outcome and stage maps are immutable snapshots. Writes are atomic per file; there is no transaction or rollback across changes, and no file is deleted.

An update completes with content alone and does not change the pick. A fork writes the reviewed patched content in one store call and reports content with the made stem. A creation completes create, then content, then pick; a retained create stage resumes its existing stem without creating another file. An ordinary creation overlays the created document with the reviewed after content. A seeded creation saves the complete reviewed content plus actual writer-owned metadata, preserving explicit root removals, creation time and provenance. Filename-owned header fields stay out of a seed’s displayed change paths. A fork already carries the reviewed source header. Carried content is never written again; only a fork or creation whose content is done and pick is missing runs pickOnly.

A project’s declarations are saved by its existing content stage, an update or a fork, together with its other content in one write. Apply re-plans allocation against the folder as it is and redraws the review without writing when the allocated content moved. For each newly attempted epoch_declared entry, completed content logs import.epoch.declared {id, label, year?, epoch, stem}, even when a fork’s later pick fails; otherwise import.epoch.not_declared {id, label, year?, epoch, code, stage} records the refusal. A relabel note uses import.epoch.relabelled on completed content and import.epoch.not_relabelled on refusal, with the same stem or code/stage and the old/new values. Planning logs import.epoch.start, the relabel or append decision with full usage and values, and import.epoch.done or .refused with elapsed time: relabel reason soleUnused, append reason used, notSole or claimed. import.epoch.claimed logs the token, label, supplied year, reason existingLabel and elapsed time when the list confirms the eligible era’s own label. Pick-only recovery emits none of these apply events.

ProjectWriterImport(project, reload, viewer: ..., guards: ...) captures the same importing viewer and guards used for planning. A null viewer refuses every entry stage as no_viewer. Every write goes through ProjectWriter, and a library target is never passed to saveEntity. The reload callback re-reads the same source and library; each reload applies forViewer for the captured viewer, and later stages use that new index. Updates bind the reviewed target’s raw text; forks bind the reviewed source and use the store’s patched-content fork. Creation remains unlisted unless the document lists it in its parent’s children; the same application-order and failed-creation rules apply. Each failure the four roads catch is logged import.write.failed {id, stage, type, error, stem, code, done} at warning — the change’s identity, the error’s words and the stem worked on (the target, the made or the retained one) — before it becomes the stage’s code, which carries no exception text.

A fork or creation is complete only when a reload’s pickAt resolves to its made stem. Otherwise the outcome is created_not_picked, with the successfully written stages retained: made, not yet in use. Recovery re-reads the stem carried by content, checks that it is the importing viewer’s project-local artifact for this identity, compares its content and only then sets and verifies the pick. A missing or changed artifact is artifact_changed; nothing is picked, rewritten or forked for that change.

The public pure artifactMatches comparison is shared by import writers and preserves both input documents. It compares canonical documents after excluding author, v, forked_from, created, updated, version_label, label, next_epoch and contributors from both. A project’s retained contributors must hold each reviewed entry unchanged, and must include the viewer’s owner only when the source owner differs from that viewer’s owner. Extra contributor entries are allowed. The comparison does not duplicate the store’s allocation arithmetic. A creation’s recovery expectation supplies the generated filmopen: 1 when absent; the retained format header still participates in equality.

Store refusals map to stale_file, read_only, no_viewer, not_your_file, identity, next_epoch_lowered and entity_exists. Any other failure is write_failed. Failure detail names the stage alone, never exception text; successfully completed stages retain their stems even when a later write or reload fails. created_not_picked and artifact_changed similarly identify the pick or content stage without publishing raw errors.

The patch examples below describe content changes independently of those actions.

In the patch Effect
"logline": "…" the leaf written
"logline": null the leaf written as null
"$remove": ["logline"] the key removed
"appearance": { "hair": { "color": "gray" } } one leaf changed, the rest of appearance kept
"appearance": {} nothing
"$replace": ["appearance"], "appearance": {} appearance emptied
"$replace": ["appearance"], "appearance": { … }, "$remove": ["appearance.hair"] refused, conflicting_directives
"$remove": ["logline"], "logline": "…" refused, conflicting_directives
"$replace": ["aliases"] with no aliases refused, replace_without_value
"aliases": ["KIRA"] the list replaced
"blocks": [ { "id": "bl_3", "dialogue": "…" } ] one block’s dialogue changed; the others kept
"blocks": [ { "dialogue": "…" } ] a new block at the end under the next number
"blocks": [ { "id": "bl_2" }, { "dialogue": "…" } ] a new block after bl_2
"$remove": ["blocks[id=bl_3]"] the block removed; next_block kept past it
"$replace": ["blocks"], "blocks": [ … ] the script is exactly the list given, in that order
"cast": [ { "character": "spouse", "epoch": "28yo" } ] the entry for spouse patched or added; the others kept

For a film with one project file named Film and one default epoch, project_overview produces this document at the fixed time shown:

{
"filmopen": 1,
"exchange": 1,
"kind": "export",
"export_id": "e-8663dee4a5a55142",
"scope": {
"name": "project_overview",
"args": null
},
"project_tag": "film",
"viewer": "ann",
"exported_at": "2026-10-10T12:00:00.000Z",
"sources": [
{
"id": "pj_film",
"stem": "pj_film_ann_v1",
"projection": "project_overview",
"sha256": "001de1fd79fd3dbec597b11a229602e85bb65256c65dff2db527c194dfd32cb5",
"reason": "sole_owner"
}
],
"problems": [],
"project": {
"$id": "pj_film",
"$stem": "pj_film_ann_v1",
"name": "Film",
"epochs": {
"default": {}
}
}
}