import type { AIState, Engine, EntityState, ZoneState } from '@ai-rpg-engine/core'; import type { AuthoredFactionMembership, ContentPack, EntityAiState, PackFactionRecord } from './refs.js'; import type { EntityBlueprint, ZoneDefinition } from './schemas.js'; import type { ValidationError } from './validate.js'; import { type GateContext, type GateResult } from './gate.js'; /** * One field the converter did not carry into runtime state, named at its source * path with the reason. * * `reason` is a closed vocabulary rather than free text so a consumer can react * to a class of drop without string-matching: * * - `no-runtime-field` — the target state type has no counterpart. The field is * authorable and simply has nowhere to land. * - `needs-module-vocabulary` — a runtime counterpart exists but constructing it * requires a module's own vocabulary (a status id resolved against a * registry, an AI intent profile that is a pack closure). C3's work. * - `inert-without-pack-code` — carried faithfully, and provably does nothing * unless pack code gives it meaning. See {@link ZONE_HAZARD_NOTE}. * - `session-scoped` — real, consumable content that is read at pack-construction * or session-setup time, BEFORE a world exists. Writing it into a booted world * would do nothing. See {@link extractSessionContent}. * - `evaluated-not-mapped` — examined, with a recorded rationale, and deliberately * NOT carried. Distinct from the three above because it is a DECISION rather than * a gap: mapping it would add fields with no consumer, or stand a second system * beside one that already exists. See {@link EVALUATED_NOT_MAPPED_KEYS}. */ export type DropReason = 'no-runtime-field' | 'needs-module-vocabulary' | 'inert-without-pack-code' | 'session-scoped' | 'evaluated-not-mapped'; export type DroppedField = { /** Source path, e.g. `entities[2](guard).aiProfile`. */ path: string; reason: DropReason; /** Why, in one sentence a content author can act on. */ detail: string; }; /** What one injected channel did with its slice of the pack. */ export type ChannelReport = { /** How many records the channel ingested. */ applied: number; errors?: ValidationError[]; dropped?: DroppedField[]; }; /** * A pack key routed into a booted engine by a consumer that CAN depend on the * owning module. See `createStandardChannels` in @ai-rpg-engine/modules. */ export type IntakeChannel = { /** The top-level pack key this channel consumes. */ key: string; apply(engine: Engine, data: unknown): ChannelReport; }; /** * Named combat brain. Structural subset of modules' IntentProfile — content-schema * sits below @ai-rpg-engine/modules, so only `id` is required here. A real * IntentProfile is assignable. Closures stay with the cognition module; this * seam only needs the name to write EntityState.ai.profileId. */ export type IntentProfileRef = { id: string; }; /** How applyContentPack turns an authored aiProfile name into EntityState.ai. */ export type AiProfileLookup = { profiles?: IntentProfileRef[] | Record; entityAi?: Record; }; export type ApplyContentPackOptions = { /** * Handlers for module-owned pack keys (`districts`, `buildCatalog`, * `progressionTrees`). A key present in the pack with no handler is REPORTED * as dropped, never silently skipped. */ channels?: IntakeChannel[]; /** * Skip re-validating elements this seam is about to convert. Only set this * when the pack came straight out of `loadContentFromFile` (which already * validated it) — it is a duplicate-work switch, not a strictness switch. */ prevalidated?: boolean; /** * Run the four-check load gate before applying anything (C1/P2). When the gate * refuses, NOTHING is applied and the result carries the diff report. * * Opt-in by argument rather than always-on, because this is the boundary where * strictness belongs: `loadContent` keeps its permissive structural validation * for callers that only want to check a file, and a pack claiming it can be * loaded INTO A WORLD is held to the version/module/hash/key contract. That * boundary did not exist before this cycle. */ gate?: GateContext; /** * Named intent profiles (IntentProfile[] or id→AIState). When an entity's * `aiProfile` matches, {@link EntityState.ai} is written instead of dropped. * Unresolved names are a structured error — an unresolved brain stands still. */ profiles?: IntentProfileRef[] | Record; }; export type ApplyContentPackResult = { /** False if anything was refused. Dropped fields do NOT flip this. */ ok: boolean; /** Present when `options.gate` was supplied. Carries the diff report. */ gate?: GateResult; /** Records ingested, per channel key. */ applied: Record; /** Every field the converter did not carry, named. */ dropped: DroppedField[]; errors: ValidationError[]; /** Non-fatal observations (unhandled channels, inert-by-construction data). */ advisories: ValidationError[]; }; /** * C0's sharpest measurement, and the reason `hazards` is carried but not * counted as a win: hazard STRINGS carry no engine semantics. Their meaning is * JavaScript the pack ships, invoked at `environment-core.ts:295`. * * Same field, two mutations, all twelve shipped worlds: adding `'unstable * floor'` — which starter-fantasy's closure matches at `setup.ts:137` — moves * the simulation in one world. Adding `'loose cobbles'` — which no closure * anywhere references — moves nothing, anywhere. * * So a data-only export ships no closures, and the one rule-bearing zone field * the lane transports faithfully still arrives inert. Typed hazards * (`HazardDefinition`'s effect union) are the C3 repair; this seam's duty is to * carry the strings and SAY SO. */ export declare const ZONE_HAZARD_NOTE: string; /** * `ZoneDefinition` → `ZoneState`. * * The `roomId` decision (CONTRACT.md §2.2): DERIVED as `zone.id`. The store * requires the field, the definition has no counterpart, and C0 measured it * `stored-inert` — 0 of 12 worlds moved, zero readers. Removing it from * `ZoneState` is an engine type change touching every starter for a field no * rule reads, filed under engine-hygiene in REPORT §9. Deriving costs one line * and keeps the store's invariant true. * * `stability` is deliberately NOT set. It is alive (4 of 12 worlds move on it, * four readers) and unauthorable — `ZoneDefinition` has no such field. Making it * authorable is a schema change, not a wire change (REPORT §6.1). */ export declare function zoneDefinitionToState(def: ZoneDefinition, dropped?: DroppedField[], path?: string): ZoneState; /** * Resolve an authored `aiProfile` / `entityAi` overlay into runtime AIState. * Returns `undefined` when the entity has no AI to apply. */ export declare function resolveEntityAi(bp: EntityBlueprint, lookup: AiProfileLookup | undefined): { ok: true; ai: AIState; } | { ok: false; profileId: string; } | undefined; /** * `EntityBlueprint` → `EntityState`. * * `blueprintId` is derived from `id` — a blueprint is its own template until an * instancing vocabulary exists. Note what CANNOT be carried: the blueprint has * no `zoneId`, so an exported pack knows every NPC and where none of them stand * (REPORT §2). That is reported per entity, not assumed known. * * `aiProfile` is resolved against {@link AiProfileLookup} (options.profiles and/or * ContentPack.entityAi). A hit writes EntityState.ai; an unresolved name is * dropped AND reported as a structured error when `errors` is supplied. */ export declare function entityBlueprintToState(bp: EntityBlueprint, dropped?: DroppedField[], path?: string, lookup?: AiProfileLookup, errors?: ValidationError[]): EntityState; /** * Pack keys this seam routes directly, using core state APIs only. * * `placements` joins them at C3/P1: it writes `EntityState.zoneId`, a core field, * so it needs no module vocabulary and stays on this side of the layering. */ export declare const CORE_INTAKE_KEYS: readonly ["zones", "entities", "placements", "items", "itemPlacements", "entityAi", "ruleProfiles", "factions"]; /** * Pack keys that need a module's own vocabulary and can still be routed into an * ALREADY-BOOTED world, and therefore arrive as injected {@link IntakeChannel}s. * * ⚠ CORRECTION TO THE COMMISSIONING BRIEF, measured in this cycle. C0 filed * `districts`, `buildCatalog` and `progressionTrees` together as "three cheap * wire gaps ... the cheapest thing on this whole list to close" (REPORT §3.1), * and the C1 brief carried that forward as three channels to route. The claim is * exactly right about SHAPE and wrong about INGESTION, and C1's own definition * of "real" — reaches a runtime — is what exposes the difference: * * - `districts` reads its definitions out of world state * (`district-core.ts:212`, `world.modules['district-core']`), so a post-boot * write lands and the readers see it. **Routable. It is here.** * - `progressionTrees` is closure-captured at module construction * (`progression-core.ts:70-72` builds a `Map` the `unlock` verb closes over at * :109) and is NEVER read from world state. A post-boot write cannot reach it. * - `buildCatalog` is consumed by character creation before a session runs * (PackInfo → `cli/src/character-builder.ts`), not by any world reader. * * The last two are not dead and not dropped — they are SESSION-SCOPED, and the * seam that serves them is {@link extractSessionContent}, which a pack or host * reads BEFORE constructing modules. All three still join the declared key list * (that half of C0's finding was correct and is closed by the gate); only one of * the three can be handed to a world that is already running. * * C3/P1 adds `encounterAnchors` here for the same reason `districts` qualifies: * `encounter-spawn` keeps its content in a module-side registry keyed by * `world.meta.gameId` and reads its per-zone tables from there at tick time, so * a registration after boot is seen by every later roll. The channel REGISTERS * into that existing system rather than standing a second spawn system beside * it — see `encounterAnchorsChannel` in @ai-rpg-engine/modules. */ export declare const MODULE_INTAKE_KEYS: readonly ["districts", "encounterAnchors", "hazardDefinitions"]; /** * Pack keys carrying real content that is consumed at construction/session-setup * time rather than by a world reader. See {@link MODULE_INTAKE_KEYS} for the * measurement behind the split. */ export declare const SESSION_SCOPED_KEYS: readonly ["buildCatalog", "archetypes", "backgrounds", "progressionTrees", "ruleset"]; /** * Pack keys the engine KNOWS about and deliberately does not carry, each with the * reason a content author needs to hear. * * The distinction this table exists to draw: a key here is not a gap and not a typo. It * was evaluated (C3 REPORT §8) and mapping it was refused on the merits. Without the * distinction, the load gate has only two verdicts — carried, or fatal — and an * ordinary forge export is fatal. */ export declare const EVALUATED_NOT_MAPPED_KEYS: Record; /** * Route a validated {@link ContentPack} into a booted engine's world. * * Pre-condition: `engine` was built by pack code (`createGame`). This function * adds content to the world that code produced; it does not build one. * * Post-condition: every pack field is either applied, or named in * `dropped`/`advisories`. Nothing is silently eaten — that is the whole point. */ export declare function applyContentPack(engine: Engine, pack: ContentPack, options?: ApplyContentPackOptions): ApplyContentPackResult; /** * Content a pack or host consumes BEFORE a world exists — the honest home for * the two keys {@link MODULE_INTAKE_KEYS} could not take. * * `applyContentPack` writes into a booted world. These two are read at * construction time and handed to the things that close over them: * * JSON-pack boot recipe (F-82b17cb3 / F-5b62643f / F-c9309691): extractSessionContent → * construct modules from the bag → applyContentPack({ profiles, channels }). * applyContentPack stays UNROUTED for dialogues/quests/abilities/statuses * (those modules freeze their registries at construction). items also live * here so a host can hand them to createEquipmentCore; apply still resolves * entity inventory/equipment against pack.items. districts / encounterAnchors * / hazardDefinitions are construction-time slices AND module-intake keys — * inject createStandardChannels() so apply still routes encounterAnchors / * hazardDefinitions via channels (do not auto-inject; do not import * @ai-rpg-engine/modules into this package). `manifest` (F-df51e0bf) is * lifted here too and belongs in the `new Engine()` call below — * `EngineOptions.manifest` is REQUIRED with no default, so a recipe that * omits it is missing a required constructor argument, not just skipping an * optional one. * * ⚠ F-c9309691 — the recipe below does NOT call `buildWorldStack()`. The * documented recipe used to (`...buildWorldStack({ quests: session.quests ?? * [], districts: session.districts ?? [] }).modules`) and could not * construct, two independent ways: (1) `WorldStackConfig.quests` is a * {@link QuestCoreConfig}-shaped object (`{ gameId, quests }`), never a bare * array — `session.quests ?? []` is truthy even when empty (`[]` is truthy in * JS), so `buildWorldStack`'s own `if (config.quests)` guard always fired and * handed a bare array straight to `createQuestCore`, which reads * `config.quests` off it (`undefined`) and throws iterating it, quest content * or not; (2) `buildWorldStack`'s faction-cognition / rumor-propagation / * belief-provenance modules require `cognition-core` and `perception-filter` * registered BEFORE the stack (world-stack.ts's own file-header contract), * and this seam has no host UI to wire perception through. The list below * mirrors `@ai-rpg-engine/ollama`'s `loadPlayableModules` instead — the * engine's own tested minimal playable stack — proven end-to-end in * packages/starter-fantasy/src/json-boot-recipe.test.ts: * * ```ts * import { * traversalCore, statusCore, combatCore, inventoryCore, * createCognitionCore, createEnvironmentCore, createDistrictCore, * createEncounterSpawn, createQuestCore, createDialogueCore, * createAbilityCore, createProgressionCore, createWorldTick, * createStandardChannels, registerStatusDefinitions, applyStatus, removeStatus, * } from '@ai-rpg-engine/modules'; * import { createEquipmentCore } from '@ai-rpg-engine/equipment'; * * const session = extractSessionContent(pack); * const gameId = (session.manifest as { id?: string } | undefined)?.id ?? hostManifest.id; * registerStatusDefinitions(session.statuses ?? []); * const engine = new Engine({ * manifest: session.manifest ?? hostManifest, * ruleset: session.ruleset ?? hostRuleset, * modules: [ * traversalCore, * statusCore, * // Presence-gated, NOT length-gated (contrast quests below): an * // authored `abilities: []` still constructs the module — matches * // loadPlayableModules's own Array.isArray guard. * ...(session.abilities ? [createAbilityCore({ abilities: session.abilities })] : []), * combatCore, * inventoryCore, * createCognitionCore(), * createEnvironmentCore(), * createDistrictCore({ districts: session.districts ?? [] }), * createEncounterSpawn({ gameId, encounters: [], entityTemplates: [], zoneTables: {} }), * // Length-gated, NOT presence-gated: QuestCoreConfig is { gameId, * // quests }, never a bare array, and an empty array must not construct * // the module (see the ⚠ note above — this guard IS the fix). * ...(session.quests && session.quests.length > 0 * ? [createQuestCore({ gameId, quests: session.quests })] * : []), * ...(session.dialogues && session.dialogues.length > 0 * ? [createDialogueCore(session.dialogues)] * : []), * createProgressionCore({ trees: session.progressionTrees ?? [] }), * createWorldTick(), * // Optional: only when the pack authors an item catalog. * // EquipmentCoreConfig REQUIRES both `catalog` AND `statuses` (neither * // is `?` — equipment-core.ts:746-751, EquipmentStatusOps); `statuses` * // is the same { registerDefinitions, apply, remove } triple every * // starter wires from this engine build's own module ops (see e.g. * // starter-fantasy/src/setup.ts). * ...(session.items * ? [createEquipmentCore({ * catalog: { items: session.items }, * statuses: { registerDefinitions: registerStatusDefinitions, apply: applyStatus, remove: removeStatus }, * })] * : []), * ], * }); * applyContentPack(engine, pack, { profiles, channels: createStandardChannels() }); * ``` * * `applyContentPack` in that last line also stamps `world.playerId` (and * `world.locationId`, when placed) the moment the pack's own `entities[]` * contains EXACTLY one `type: 'player'` record and the store still carries * WorldStore's untouched default (F-67786a6c — see the identity-stamp block * right after the placements pass, above). This recipe needs no separate * `engine.store.state.playerId = ...` line for the common one-player pack; a * host that already set its own playerId, or a pack declaring zero or several * `type: 'player'` entities, is left untouched (the ambiguous case is an * advisory, never a guess). * * Deliberately untyped beyond `unknown[]`/`unknown`: `BuildCatalog` lives in * @ai-rpg-engine/character-creation and `ProgressionTreeDefinition` in this * package's own schema surface, but validating either here would drag * construction-time policy into a wire-shaped module. The caller owns the cast * and the validation (`validateBuildCatalog` is exported alongside this). */ export type SessionContent = { /** Present only if the pack carried the key. */ buildCatalog?: unknown; /** Present only if the pack carried the key. */ progressionTrees?: unknown[]; /** Chargen kits — same session as buildCatalog. Present only if the pack carried the key. */ archetypes?: unknown[]; /** Chargen kits — same session as buildCatalog. Present only if the pack carried the key. */ backgrounds?: unknown[]; /** * Pack-authored GameManifest (F-df51e0bf). Present only if the pack carried * the key. `EngineOptions.manifest` is REQUIRED with no default — bind this * at Engine construction (`manifest: session.manifest ?? hostManifest`), the * same way `ruleset` binds below. */ manifest?: unknown; /** * Pack-authored RulesetDefinition. Present only if the pack carried the key. * Bind it at Engine construction; loadContent already validated it. */ ruleset?: unknown; /** Dialogue trees for createDialogueCore. Present only if the pack carried the key. */ dialogues?: unknown[]; /** Quest definitions for buildWorldStack({ quests }). Present only if the pack carried the key. */ quests?: unknown[]; /** Ability definitions for createAbilityCore. Present only if the pack carried the key. */ abilities?: unknown[]; /** Status definitions for registerStatusDefinitions. Present only if the pack carried the key. */ statuses?: unknown[]; /** Item catalog for createEquipmentCore({ catalog: { items } }). Present only if the pack carried the key. */ items?: unknown[]; /** * District topology for buildWorldStack({ districts }). Present only if the * pack carried the key. applyContentPack still routes this via an injected * channel — extract here so a JSON host can construct district-core without * importing the pack's named `districts` export. */ districts?: unknown[]; /** * Per-zone spawn sets. Present only if the pack carried the key. Construction * bag for hosts that seed encounter-spawn; apply still needs a channel. */ encounterAnchors?: unknown[]; /** * Typed hazard records. Present only if the pack carried the key. Construction * bag for hosts that seed the interpreter; apply still needs a channel. */ hazardDefinitions?: unknown[]; /** Keys found but unusable, with the reason — never silently omitted. */ advisories: ValidationError[]; }; export declare function extractSessionContent(pack: ContentPack): SessionContent; /** * `ContentPack` keys that are genuinely declared and validated, carry real * content, and still have no route into a world at this rung. Each is named in * the result so an author is never told "applied" about a pack half of which * went nowhere. */ export declare const UNROUTED_DECLARED_KEYS: ReadonlyArray; /** * Content-owned seed of WorldState.factions from an authored FactionMembership * roster (F-749aba8e). Same list that feeds faction-cognition / defeat-fallout * — not a third list. First-wins: a host-registered or pack.factions record * is left alone. Defaults when the row omits the optional fields: name= * factionId, reputation=0, disposition='neutral'. * * Call this from createGame after Engine construction (and after * applyContentPack when the pack also authors pack.factions) so the registry * fills even if modules has not yet hydrated membership onto WorldState. */ export declare function seedWorldFactionsFromMembership(world: { factions: Record; }, rows: readonly Pick[]): void; //# sourceMappingURL=intake.d.ts.map