OpenFairyGUI Architecture Overview
This page describes ownership, sources of truth, data flow and safety boundaries. See development for setup, terminology, references and checks, and task recipes for change entrypoints. Canonical documents own field lists, defaults and operation catalogs; the overview does not duplicate them.
Summary
UAM is the public declarative authoring contract. Document + Property Graph is Core's internal materialization, protocol-adaptation and execution representation. Existing project files remain authoritative on import; manually lifting them and importing UAM must not bypass source-fidelity checks.
Core owns transaction semantics, Functions composes workflows, Backend manages session state and persistence, and CLI/MCP adapt entrypoints. Reading, materialization, editing, saving and publishing are separate capabilities. A successful query or preview neither authorizes subsequent writes nor guarantees save/publish success.
Module boundaries and authority
| Module | Ownership and change entrypoints | Not owned |
|---|---|---|
| Core | packages/core/src/uam/model.ts, transaction-contracts.ts, transaction.ts: UAM and transactions; properties/: formal properties; io/: XML, binary and platform I/O | Sessions, transport protocols, high-level publish/restore policy |
| Functions | packages/functions/src/uam-transaction.ts: structured stateless transaction results; validate.ts, publish.ts, restore.ts, atlas.ts: workflows | Another selector/operation grammar, implicit publishing/restoring from authoring |
| Backend | packages/backend/src/runtime.ts: assembly; runtime/contracts.ts: method signatures; runtime/capabilities.ts: capabilities; services/: read / authoring / artifact / runtime; storage.ts: storage adapters | Core semantics, MCP transport, publish/restore execution in browser-safe sessions |
| MCP | packages/mcp/src/tool-metadata.ts, tool-handler.ts: method mapping, transport annotations, host-field exclusions and budgets; resources / prompts / stdio | Transaction kernel, path authorization, automatic repair, artifact execution authority |
| CLI | packages/cli/src/cli.ts, commands/: arguments and call assembly; contracts.ts, utils/json-output.ts: process JSON envelope | Domain protocols; workflow results still reuse Core/Functions/Backend types |
| test-utils | packages/test-utils/: test helpers and commit-pinned fixtures | Production protocols or runtime workflows |
scripts/generate-contracts.mjs uses the existing TypeScript compiler to generate structural schemas, operation catalogs, version-bound content and documentation tables from Core/Backend/CLI types. MCP uses existing Zod structural validation; Core then validates semantics. The independent @openfairygui/backend/docs entry distributes generated data without a runtime Backend → CLI dependency or Zod in Core.
The generator entry owns contract assembly and command options. scripts/contracts/schema.mjs owns the type program and schema inference, transport.mjs owns input budgets and byte paths, and output.mjs owns bilingual tables, installed documentation and generated-file drift checks.
Core's formal UAM retains component-instance controller overrides and absent Gear defaults. Functions loads bitmap fonts from their resource branches before publish selection and builds image dependencies; Core encodes font names, glyphs, and published IDs. Code-generation name allocation belongs to Functions; MCP does not fill protocol gaps.
Backend preview materializes a potentially invalid existing snapshot only in memory to compare file changes for repair transactions. Projected state and persistence entrypoints remain strictly validated. MCP applies an aggregate 8 MiB binary input budget only at generated contract byte paths; other JSON retains general structural limits.
Backend's typed diagnostic catalog covers formal error codes, recording every owner of shared codes, documentation URIs and recovery guidance. Responses retain the actual origin and original error fields. CLI/MCP share offline content and a thin Skill shipped with the installed version. Exact fields, versions and digests belong in contracts, diagnostics and installed docs.
The SDK owns MCP tool discovery and dispatch; Hosts can add tools to the same server through public registerTool(). toolPolicies run Host checks after input validation and before Backend execution. A declared Host failure stops the call; allowing it invokes Backend once with the original input. Authorization and grant consumption belong to the Host; revision, path and disk guards remain in Backend. Discovery retains bounded $ref schemas. Host output extensions do not alter the installed Backend contracts or documentation.
Primary data flow
bridge.ts remains the lift/materialize facade, with implementations in bridge-lift.ts, bridge-materialize.ts and bridge-shared.ts; controlled source-file enumeration belongs to project-source-files.ts. Binary content uses Uint8Array, preserved in conversions and transaction working copies without JSON cloning.
Gear string parsing belongs to bridge-lift.ts; Document setter mappings for specific properties belong to bridge-materialize.ts and are reused by creation and transaction updates. Document imports its logger leaf directly. XML readers and writers invoke the common-state handler once using the concrete tag protocol, leaving other tag-specific fields in their branches. Reading common state precedes Gear default capture and does not widen formal property ownership.
display-object-xml-reader.ts keeps tag dispatch and common-state reading. Its sibling display-object-xml-text.ts, display-object-xml-list.ts, display-object-xml-behaviors.ts and display-object-xml-instance.ts own text, lists, Gear/relations and instance overlays respectively. The order is specific properties → common state → Gear → relations → property overrides → extension overlays. Shared XML shapes and property-override parsing live in display-object-xml-shared.ts.
ProjectReader retains the global order of settings, main/branch packages, branch linking and second-pass component parsing. project-reader-discovery.ts separates directory probe results from optional-directory/file-probe diagnostic policy and creates no resources. project-package-reader.ts reads package descriptors and folder metadata and registers resources; project-resource-hydration.ts owns image dimensions, source bytes and derived MovieClip data; project-component-xml-validation.ts checks component XML attribute values. Shared XML node extraction and syntax checks live in utils/xml-utils.ts. The entrypoint still owns read-error classification and completeness, and components are parsed only after all resources are registered.
Shared XML writing formatters, protocol helpers and property-override node serialization live in project-xml-writer-utils.ts. display-object-xml-text-writer.ts writes text and input attributes using GTextField and GTextInput; display-object-xml-list-writer.ts writes list attributes and items using GList and GTree; display-object-xml-instance-writer.ts writes instance references, property overrides and extension data using GComponent. Items and property overrides reuse formal model types. display-object-xml-behaviors-writer.ts owns Gear value formatting, allowed-child filtering and grouped relation serialization; project pre-write checks and display-list output share its Gear validation. display-object-xml-writer.ts retains tag dispatch, concrete serializers for images, graphs and loaders, common state and node ordering. Its common-state interface makes only state getters absent from some tags optional and carries no control-specific attributes. List items and instance property overrides precede Gear/relations; the entry point appends instance extension nodes last.
Before writing, ProjectWriter builds one package/branch output plan containing descriptor, resource and folder targets and descriptor resource ordering. Target checks and writes share that plan. Components and source bytes retain sequential writes without a serialized byte snapshot of the whole project. After all packages are written successfully, cleanup checks actual path identities again to protect old paths still occupied by current outputs.
Project reading, UAM checks and source validation are layered: readProjectDetailed reports read completeness, validateUamProject checks the model, and Functions composes the formal validation report. invalid means a definite error; incomplete means missing capability or data. See project validation.
Transactions and support preflight
The stable entry is packages/core/src/uam/transaction.ts. validateTransactionSupport(project) checks full-project support; with operations it checks the touch set, batch order and final references. Support checking is not full execution preview.
transaction-preflight.ts retains per-operation dispatch and phase ordering. Domain implementations live in packages/core/src/uam/preflight/:
| File | Invariants |
|---|---|
support.ts, values.ts | Support scope, selectors, diagnostic construction, shared value and safe-name checks |
settings.ts | Project/package settings snapshots, JSON-safe values and canonical comparison |
display.ts | Node-kind-specific property snapshots and unchanged-result rejection |
behaviors.ts | Controller, transition and gear pages, targets and same-batch bindings |
resources.ts, resource-folders.ts | Source bytes, PNG/JPEG/JTA, folders and atlas constraints |
lifecycle.ts | Ordered branch/package/component/resource/folder/display-list projection on one working copy |
projected-state.ts | Final group/resource references and boundaries for untouched pre-existing issues |
Lifecycle projection reuses actual UAM apply helpers, not another executor. Domain functions must not independently traverse and reorder the whole batch. Preserve diagnostic codes, paths, ordering and input immutability on failure. uam-transaction-support.test.ts, uam-transaction-apply.test.ts and uam-transaction-lifecycle.test.ts cover these responsibilities and cross-domain batches.
Execution follows existing operation capabilities into transaction-uam-apply.ts or transaction-document-apply.ts, discarding private working copies on failure and returning new normalized UAM on success. Materialization support does not imply arbitrary field mutation, and atomic lifecycle batches are not unrestricted operation combinations. See contracts for exact grammar, scope and discovery.
uam/property-rules/ groups shared rules by text, image/MovieClip and component instance, checking complete snapshot shapes, numeric ranges and local consistency. Whole-project validation and display transaction preflight use these rules directly. validate.ts retains project traversal, global references and diagnostic ordering; preflight retains selectors, current state and operation support. Transaction-specific list and Loader constraints remain in preflight and do not become restrictions on reading existing projects.
property-updates.ts owns display-property update rules shared by preflight projections and both execution paths. The Document path lifts the target node's properties, applies the update and writes through the bridge onto the existing object, preserving Gear and Controller object bindings. Ordered preflight owns Controller payload validation; the executor resolves live references and reuses bridge Controller creation and Gear type mapping. uam-transaction-parity.test.ts triggers the Document path with Controller operations whose net effect is empty, then compares shared-operation results, diagnostics and input immutability.
Backend sessions and persistence
Use openSession for an existing file project: it acquires a session-lifetime lock, hydrates source bytes and compares complete ProjectWriter output before and after the original Document's UAM round-trip. Unmodeled write-back differences mark uamFidelity: unsupported; actual writes are rejected. Use openProjectSession and materializeSession to bootstrap a new workspace only when caller-provided UAM itself is authoritative.
| Operation | State and effects |
|---|---|
queryEntity | Seven fixed projections: project, package, resource, component, displayNode, controller and transition; no selector for the project, exact selectors otherwise; actual revision, detached and bounded values, no source bytes |
preflightTransaction | Checks revision in the same session queue, copies project/bytes, executes and discards; no project/dirty/revision/cache/business-event changes or disk writes |
applyTransaction | Rechecks expectedRevision; success replaces the project, increments revision and marks dirty; failure retains project/revision and may emit rejection events |
saveSession | Saves through the session-bound filesystem; only success updates lastSavedRevision and clears dirty/pending cleanup; does not advance edit revision |
materializeSession | Explicit target/adapter for complete first write; retains path, fidelity and validation gates rather than bypassing dirty saves |
closeSession | Releases the lock after earlier transactions/writes; does not automatically save pending work |
Project and package settings queries reuse ReadService fixed projections, JSON budget checks and deep copying, returning identity and complete settings. Callers change requested fields and pass the complete settings with the queried revision to existing updateProjectSettings / updatePackageSettings transactions. MCP directly maps this query and transaction flow.
Preview compares two formal UAM snapshots for entity/field impacts and reuses the capture filesystem plus ProjectWriter for project-relative file/folder differences. It represents current revision → preview result, not cumulative changes since the last save, a disk-write list or deletion authority. Over-budget summaries fail completely rather than truncate into success; persistence hints always report writeVerified: false, and projected revision is not reserved. See transaction previews.
SessionOperationQueue serializes preview, apply, save, materialize and close for one session without blocking other sessions. SessionRegistry exclusively owns session and path indexes: opening and materializing into new storage reserve the target before asynchronous I/O, commit the binding after success, and release only their own reservation on failure. Failed rebinding retains the original binding. Host locks across runtimes and storage transactions retain their respective responsibilities.
Save, materialize and close capture request values before queuing, preserving the storage adapter identity. UAM normalization owns its gear state values, resource metadata and source bytes. Directory enumeration failures produce incomplete reads that file sessions cannot write back as complete UAM. A session holding a file lock rejects storage rebinding. Failed lock release returns session_close_failed and retains the session and lock record so closing can be retried after correcting the failure. Node treats only an absent lock file as already released; metadata read errors, invalid metadata and token mismatches fail without deleting the lock file.
ReadService receives only session views with deeply read-only UAM and detaches responses after checking their budgets. AuthoringService receives transaction session lookup, cache/event commands and the queue. RuntimeService opens and closes sessions; PersistenceService saves and materializes through the existing session-project-writer.ts. EventService and CacheService exclusively own event sequences/logs and cache entries, querying only the session fields they need.
Events are bounded polling logs. Cache entries are keyed by sessionId and contain revision-bound derived data, not source truth. refreshCache computes counts synchronously, emits one cache.updated event and directly returns BackendCacheSnapshot, without changing edit or saved revisions. The Backend contract version is 3.0.0 and capability schema is 12. The artifact plane declares host capabilities without executing publish/restore.
Save and materialize share an internal PersistenceService completion step: update the saved revision, clear dirty, refresh cache, then emit save.completed followed by cache.updated. Each path retains its own validation, storage binding and error results; both remain serialized by the same session queue.
Pure in-memory session canonicalProjectPath / canonicalPathKey values identify a session only. Save and materialize use session-bound storage or an adapter explicitly provided by the host for that call; they do not automatically acquire the runtime filesystem. Preview persistence hints reflect the actual session binding.
Node / Web and path boundaries
- Core and Backend root entries remain browser-safe. Platform I/O comes from
@openfairygui/core/nodeor/web; use/project-iofor adapter types alone.@openfairygui/functions/uamis the narrow transaction workflow used by Backend's browser entry. - Node assembly lives in
packages/backend/src/node.ts. Opening rejects symlinks within the project tree, and each path operation also checks the nearest existing ancestor's realpath. Backend enforces allowed roots; MCP roots do not grant authority. - Node persistent locks automatically recover only valid same-host records whose owner is confirmed dead or whose PID was reused. Corrupt, cross-host and active locks still conflict. Saves use sibling staging, backup and directory switching, attempting to restore the original tree after commit failure.
ProjectWriteTransactionErrorreports the disk outcome explicitly:diskMayBePartiallyUpdated: falserequires confirmation that the original tree is unchanged or restored. If rollback also fails, the backup and staging directories remain available through the save error'srecoveryPaths. - Browsers inject async storage through
createBackendStorageFileSystem, providingunlinkand non-recursivermdir. Web Locks atomically exclude active peer tabs and are released on refresh/termination; hosts without them must inject an equivalent lease. Persistent files are not browser lock truth. - Generic browser adapters do not automatically inherit Node atomic saves. Without
runProjectWriteTransaction, they do not advertiseatomicSave. Controlled old source files and empty folders are removed only after all new project writes complete. - Browser image replacement uses async transactions and the public
@openfairygui/core/image-validation-workerentry for strict validation. The host must bundle the worker and its dependencies into an adjacent standalone ESM file. Synchronous browser image replacement is rejected; MovieClips use the shared JTA parser.
@openfairygui/core/web provides project-tree I/O only, not binary I/O, sessions, publishing or restoration. Browser hosts handle OPFS/user-folder permissions. See package entries and the browser example for working adapters and worker packaging.
Publish / Restore host boundaries
packages/functions/src/publish.ts orchestrates settings, resource closure, atlas, binary output and code generation. Option/resource domains live in publish/; packing and JTA/FNT codecs live in atlas/. Node/Web reuse the main workflow without implicit execution from Backend sessions.
Each call holds an independent PackagePublishContext in the existing package publish plan. publish/package-context.ts computes resource selection, effective IDs, external filenames and branch policy. External-resource writers consume that context; Atlas receives a selection/ID map keyed by resource identity. Core owns the narrow BinaryPackageEncodingContext, passed through BinaryWriterOptions.packageContext; component encoding receives only the package ID and effective resource ID map. These derived values are not written to package or resource extras, and the Document is not copied. High-resolution links, pixel-hit data and Atlas/Sprite nodes still update the formal model in their publish stages.
Standalone BinaryWriter calls without a context encode all encodable resources, formal IDs and branches in the current model, retaining the existing external-font exclusion. BinaryReader's raw binary slices, sprite data and filename metadata remain available for binary round trips. Explicit context filenames override only the current encoding and do not replace source metadata.
The codegen.ts entrypoint coordinates plugins and packages. codegen-settings.ts resolves settings and output plans; codegen-model.ts builds names and members; codegen-render.ts renders filenames and text without filesystem access; codegen-output.ts shares package cleanup and sequential writes across languages.
publishNode()injects Node filesystem, Sharp and project plugins. Explicit output uses sibling staging before commit and rejects symlinks in existing output. Its returned file list comes from actual writes and atlas completion records, not enumeration of old output.publishBrowser()injects caller filesystems, a Canvas raster adapter and empty hooks, rejecting unsupported settings before writing. Output atomicity belongs to the host; failure lists contain only completed writes.restoreNode()restores only trusted local published directories into separate project directories, reusing path checks, reconstruction and output transactions inrestore.tsandrestore-internals/. It does not guarantee original XML, editor settings, unpublished content or local state, and does not decide whether unknown inputs are trustworthy.
restore.ts keeps preparation, writing and commit order. restore-internals/resource-paths.ts owns controlled source lookup and output paths, reusing resource-path validation from path-utils.ts. skeleton.ts owns skeleton type repair, sidecars and dependency links; asset-output.ts owns atlas cropping, generated files and loose-file output. Font and MovieClip reconstruction reuse font.ts and movie-clip.ts, with concrete Core resource types.
Successful atlas generation replaces that package's previous Atlas/Sprite nodes; failure removes the new nodes and retains the previous complete atlas model. Publication selects resources from formal export flags and dependencies, so prior generated sprites cannot expand the next selection. ProjectWriter validates image ordering hints across all packages and branches before the first write, and uses adapter-provided path identities when cleaning stale files and directories to protect current outputs addressed by case aliases.
Core owns image serialization hints through ProjectWriter.setImageWriteHints(), keyed by image identity without entering properties or extras. omitPackageSize omits inferred dimensions; packageOrder: { afterId, weight } controls output order. An anchor must be an unhinted resource in the same package and branch; an empty ID appends, groups sort by finite weight then resource ID, and invalid anchors reject writing. Restore uses this contract for font textures and glyphs; Functions tracks placeholder glyph images by identity without Writer recognizing font-recovery markers. Setting hints copies and replaces them; a new Writer still honors hints on the returned Document, and empty hints restore normal writing. Hints do not propagate across UAM conversion, reloading or resource object replacement.
CLI only parses arguments, invokes formal Node entrypoints and wraps results. Product MCP exposes no publish/restore execution tools. Evaluation-only artifact hosts use constrained tools with fixed inputs/directories without widening product permissions.
Protocol and behavior references
| Fact to verify | Canonical documentation |
|---|---|
| XML attributes, structural nodes and displayList variants | Attribute protocol, DisplayList tags; metadata lives in packages/core/src/io/project-xml-protocol.ts |
| Sidecars, resources/folders, branch directories, images/JTA, publish settings and write-back | Editor publish settings |
| Binary blocks, resource encoding, auxiliary naming, high-resolution and branch publishing | Binary package protocol, publish settings |
| Completeness, decoding capabilities and safe failure | Project validation, diagnostics |
| Publish plugins and limited restoration | Plugin boundaries, restore limits |
Contracts and consumer verification
agent/impact-map.json drives test selection and the AGENTS guidance table. check:ci combines full tests, contract/documentation checks, documentation builds and external five-package installed consumers. Release checks the exact tarballs about to be published, not workspace links.
scripts/consumer/helpers.mjs owns shared consumer checks for file containment, public exports, bins, CLI envelopes and directory snapshots. Acceptance scenarios and repository self-tests depend directly on this leaf. Both the isolated-consumer copy inventory and Agent evaluation harness digest include it; shared helpers are not imported from the runtime scenario entry.
Consumers run public Node/MCP stdio examples and real Chromium OPFS → Core adapter → Backend session → preview/edit/save → hydrated WebIO reread. Checks cover source bytes, Web Locks, refresh recovery and path rejection, not interactive user-folder authorization, renderers or every browser.
Ten real consumer evaluations cover inspection, precise edits, concurrent recovery, safe stopping with pending work preserved and separate publish/restore tasks. Reference runs are deterministic gates; real-model runs are manual observations, recorded separately and never interchangeable. See agent evaluations for tasks, historical evidence and limits.
Repository doctor checks development versions, dependencies/exports, references, native PNG/JPEG codecs and the temporary directory. Product doctor checks the installed environment, native codecs, temporary/explicit output directories and optionally an explicit project. Neither installs, creates sessions/locks/probes, or runs plugins/publish/restore. Access checks do not prove later writes or rollback. See development verification and installed-version diagnostics.