动态 Runtime QPK
动态 Runtime QPK的实现设计、使用约定与验证要求。
This document defines the development standard for AI generated incremental content in QuaEngine. Runtime content is delivered as signed QPK packages produced by Quack, mounted by QuaAssets, and activated by the engine through RuntimeContentManager.
Goals
- Treat every AI generated increment as a package, not as loose resource pushes.
- Keep the renderer stateless and transient.
- Preserve save/load, rewind, backlog, timeline, animation, audio, and store state correctness when packages are loaded, continued, unloaded, or restored later.
- Support multiple QPK packages continuing the same scene, timeline, lane, or visual composition.
- Make production runtime JS loading verifiable through hash/signature trust policy.
Terms
- Runtime Package: A dynamic QPK with
manifest.runtimePackage. It may contain assets, compiled QuaScript modules, scene modules, engine/renderer/compiler plugin modules, story graph deltas, and store migrations. - Bundle: A QuaAssets mounted unit. Runtime packages mount as side-by-side bundles and do not patch an existing bundle unless an explicit force replacement is requested.
- Content provenance:
contentPackageIdrecords the primary package that created a story point or projection.requiredRuntimePackagesrecords all packages that must be present to safely restore or keep that projection. - View projection: Engine-owned render-relevant state such as characters, background, choices, effects, UI overlays, animations, and plugin projections.
Package Flow
- Quack builds a
.qpkin dynamic mode. - QuaAssets loads it with
loadDynamicBundle(source, options)and storesruntimePackageId, priority, version, and loaded time on the bundle and its assets. - Engine calls
loadRuntimePackage(source)throughRuntimeContentManager. - The trust policy verifies required hash/signature metadata before activation.
- Engine activates runtime engine plugins, scene factories, story graph deltas, store migrations, script modules, and renderer plugin manifests.
- Renderer receives only pipeline events and view snapshots. Web runtime code loads renderer plugin JS through a Web-side loader, not through engine core.
Runtime packages are additive side-by-side mounts. Use the old patch flow only to update an already existing bundle. Do not model AI generated scene continuations as individual resource pushes.
Manifest Contract
manifest.runtimePackage is the source of truth for activation:
id,version, optionalsequence,priority, anddependenciesscripts[]for compiled QuaScript modulesscenes[]for scene modules registered byRuntimeContentManagerplugins[]for engine, renderer, or compiler plugin modulesstoryGraphDeltas[]for graph, node, edge, lane, route, and timeline incrementsstoreMigrations[]for idempotent runtime state/schema defaultsintegrityandsignaturefor production trust policy
Dynamic QuaScript modules must use stable IDs. Do not generate random step UUIDs for runtime content. Compile step IDs from module ID, stable seed, and step index or label so save/load and read-progress keys remain deterministic.
Runtime scene modules use { id, assetName, exportName?, version?, metadata? }. Engine core loads them only through the injected runtimeModuleLoader.loadSceneModule and registers the returned Scene or scene factory with registerScene. Scene modules are unloaded with the owning package and must not depend on renderer-owned state.
Asset Resolution
QuaAssets must rank non-explicit lookups by:
- explicit
bundleName, when provided bundlePriority- asset or bundle
version loadedAt- locale match and default locale fallback
Provider records and stored bundle records must follow the same ranking. A dev provider should not accidentally override a higher-priority runtime package for the same asset name.
Dynamic unload removes that bundle's assets and emits asset:changed removals. Renderer object URLs, WebAudio buffers, and DOM plugin resources are renderer implementation details and must refresh or dispose through existing asset revision paths.
Story tree presentation assets are stored as structural refs, not URLs. Engine may resolve { type, name, runtimePackageId? } through resolveStoryAssetRef() to package-aware AssetData; Web/story-map UI creates any object URL transiently outside engine core.
Engine Runtime Ownership
RuntimeContentManager owns runtime package lifecycle in @quajs/engine:
loadRuntimePackage(source, options)activateRuntimePackage(packageId)unloadRuntimePackage(packageId, options)getRuntimePackages()registerScriptModule(record)runScriptModule(moduleId, scope?)runScriptModuleFrom(moduleId, { nodeId?, labelId?, entryId?, stepId?, packageId?, scope? })- runtime scene factory activation from
runtimePackage.scenes[] ensureRuntimePackages(packageIds)
Engine core stays platform neutral. JS byte loading and import() are supplied through injected runtime module loaders. Engine core must not use Blob, object URLs, DOM APIs, or browser globals.
Save, Load, And Jump
Every save slot, checkpoint, story point, backlog entry, and rewind target that touches runtime content must carry dependencies.
- Story points use
contentPackageId,scriptModuleId, andscriptModuleVersion. - Checkpoint and save metadata use
requiredRuntimePackages. - View projections can carry
contentPackageIdandrequiredRuntimePackages. - Before jump/load/rewind, engine must ensure required packages are active.
- If the registry cannot resolve a missing package, restore must fail clearly instead of partially restoring broken state.
Default unload must reject packages referenced by current story point, current checkpoint metadata, or current view projection. { force: true } is reserved for teardown, rollback, tests, or deliberate state eviction after moving the player to safe content.
Same Scene Continuation
Multiple QPKs may continue the same scene, timeline, lane, route, or visual composition. This is expected.
Rules:
- Do not key read progress by
sceneId + stepIdalone. IncludecontentPackageId, script module identity, lane, route, timeline, protagonist, node, and line where applicable. - If package A creates a character/background and package B changes expression, sprite, position, layer, lighting, or animation, the projection must require both A and B.
- A package-scoped update must not steal ownership of existing base layers unless it replaces them deliberately.
- Save/load must restore all required packages, not just the package of the current story point.
This allows a generated package to add "the next moment" in the same scene without forcing scene boundaries or duplicating base assets.
Story Graph And Timeline
@quajs/story-graph owns route, lane, protagonist, timeline, node, and edge metadata. Engine core should not hardcode timeline semantics.
Runtime graph deltas must be package-scoped:
- nodes, edges, lanes, routes, and timeline records carry package provenance
- choice targets may point into dynamic nodes or dynamic script modules
- unloading one package must remove only that package's graph delta and leave other same-scene packages intact
Chapter select is part of the story graph model. A selectable chapter is a story node with chapterSelect metadata, and the public chapter select list is derived from graph nodes plus unlockedNodes. It must not create a second authoritative chapter progression store. Runtime package unload removes removed nodes from chapter select projection and prunes stale unlock entries for package-owned nodes.
Store Migration Policy
Runtime packages may migrate state only through declared, idempotent migrations.
- Each migration is keyed by package ID, migration ID, and version.
- Applied migrations are stored in engine-owned runtime state.
- Migrations may set defaults, register scopes, or upgrade projections.
- Migrations must not blindly overwrite player progress, choices, settings, or current scene state.
- Re-activation must not re-run already applied migrations.
Plugin Development Rules
Feature plugins must treat runtime package activation/unload as part of their state model.
- Add
onRuntimePackageActivate,onRuntimePackageUnload, oronRuntimePackageMigratewhen the plugin owns package-scoped state. - Any engine-owned projection written by runtime content must carry package provenance.
- If a projection can depend on multiple packages, preserve
contentPackageIdand mergerequiredRuntimePackages. - On unload, remove only state that belongs to or requires that package.
- Unknown plugin projections are still engine-owned view state; force unload may clear plugin projections that declare runtime package dependencies.
Plugin-specific expectations:
- backlog: entries and voice replay carry required packages; replay/rewind ensures packages before use.
- settings: dynamic settings scopes carry package ID; unload unregisters scope but preserves player values.
- animation: definitions, active playback, and filled final projections carry package ID; unload removes package-owned definitions and active/final projections.
- audio: tracks and chapter voice defaults carry package ID; unload stops tracks and clears package-owned chapter/current-line references.
- background: background and layers carry package metadata; layer updates during runtime content merge package dependencies.
- character/sprite: character projection owns runtime provenance; sprite renderer remains a stateless asset projection.
- inventory: item/category definitions may be package-owned and are removed on unload when they belong to or depend on the package. Profile item records are long-lived
QuaStoreprofile data and are preserved; projections mark records without active definitions as unavailable. - story graph: graph deltas, chapter select thumbnails, and chapter select metadata preserve package dependencies; jump helpers ensure required packages before entering dynamic targets.
- renderer plugins: engine only emits renderer plugin manifests through the pipeline; Web renderer loads/destroys transient plugin resources. Vue/React/Svelte adapters wrap Web renderer plugin factories rather than duplicating DOM runtime behavior.
Sprite Deltas
Runtime sprite packages may contain expression-only deltas. For example, a package with characters/alice/expressions/happy.png may generate a sprite manifest that references alice/base.png from an earlier bundle and overlays the new expression layer.
Do not require every runtime package to duplicate base character art. Asset ranking and package dependency metadata are what make cross-package sprite composition safe.
Trust Policy
Production runtime JS and plugin modules require integrity and signature verification. Development and tests may opt into unsigned packages explicitly.
Rules:
trustPolicy.requireSignaturerejects unsigned packages.runtimePackage.integrity.hashis the Quack manifestmerkleRoot; it is stable because it is derived from package payload assets instead of the final QPK bytes that contain the manifest itself.trustPolicy.verifyPackagereceives the full bundle hash plus manifest metadata, including the package integrity hash and manifest merkle root.- Failed trust verification must unload the mounted bundle and leave runtime package state clean.
- Engine core verifies policy but does not perform Web module import details.
Required Test Coverage
Any runtime package feature should include targeted coverage for:
- dynamic QPK manifest creation with assets, scripts, plugins, graph deltas, migrations, and trust metadata
- runtime scene factory registration, duplicate rejection, registry loading, rollback cleanup, and unload cleanup
- dynamic bundle load/unload, same asset ranking, locale fallback, and
asset:changed - stable script step IDs and same-scene multi-package continuation
- save/load and jump with missing package failure and registry recovery
- story graph/timeline deltas from multiple packages in the same scene
- store migration idempotency
- unload protection for story point, checkpoint metadata, and view projection
- plugin state cleanup for backlog, settings, animation, audio, background, inventory, sprite/character, story graph/chapter select, and renderer plugins
Review Checklist
- Does every authoritative state mutation go through engine/store APIs?
- Does every runtime-created projection carry
contentPackageIdor mergerequiredRuntimePackages? - Does save/load capture view-level package dependencies, not only story point dependencies?
- Can package B continue package A's scene without corrupting read progress or unload cleanup?
- Does default unload reject active dependencies?
- Does force unload clear runtime projections before assets are removed?
- Are store migrations declared, idempotent, and non-destructive?
- Are dynamic JS/plugin modules loaded through injected loaders and verified by trust policy?
- Is renderer state still transient and replaceable?