feat: Implement ScenarioLoader pipeline

This commit is contained in:
2026-07-09 01:59:27 +05:30
parent fa698619b3
commit d96fc04542
11 changed files with 449 additions and 4 deletions

View File

@@ -0,0 +1,15 @@
{
"name": "@omnia/scenario-core",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": "./dist/index.js"
},
"dependencies": {
"@omnia/core": "workspace:*",
"@omnia/spatial": "workspace:*",
"@omnia/memory": "workspace:*",
"zod": "^4.4.3"
}
}

View File

@@ -0,0 +1,2 @@
export * from "./schema.js";
export * from "./loader.js";

View File

@@ -0,0 +1,125 @@
import { WorldState, Entity, SQLiteRepository, AttributeVisibility } from "@omnia/core";
import { Location } from "@omnia/spatial";
import { BufferRepository } from "@omnia/memory";
import { ScenarioSchema, Scenario } from "./schema.js";
export class ScenarioLoader {
constructor(
private coreRepo: SQLiteRepository,
private bufferRepo: BufferRepository,
) {}
/**
* Instantiates a live world from a static JSON scenario template.
* Creates a new world instance in the database using a generated unique World ID.
*
* @param scenarioJson The raw JSON scenario template contents.
* @param targetWorldId The unique ID for the running instance to create (e.g. UUID).
* Allows launching multiple active runs from one scenario.
*/
async initializeWorld(scenarioJson: unknown, targetWorldId: string): Promise<string> {
// 1. Validate scenario template schema
const scenario: Scenario = ScenarioSchema.parse(scenarioJson);
// 2. Instantiate running WorldState using the target instance ID
const world = new WorldState(targetWorldId, new Date(scenario.startTime));
// Seed world-level attributes
world.addAttribute("name", scenario.name, AttributeVisibility.PUBLIC);
world.addAttribute("description", scenario.description, AttributeVisibility.PUBLIC);
if (scenario.world?.attributes) {
for (const attr of scenario.world.attributes) {
const vis = attr.visibility === "PUBLIC" ? AttributeVisibility.PUBLIC : AttributeVisibility.PRIVATE;
world.addAttribute(
attr.name,
attr.value,
vis,
attr.allowedEntities ? new Set(attr.allowedEntities) : null,
);
}
}
// 3. Save World State core row
this.coreRepo.saveWorldState(world);
// 4. Instantiate and Persist Locations
if (scenario.locations) {
for (const locData of scenario.locations) {
const location = new Location(locData.id, locData.parentId ?? null);
if (locData.attributes) {
for (const attr of locData.attributes) {
const vis = attr.visibility === "PUBLIC" ? AttributeVisibility.PUBLIC : AttributeVisibility.PRIVATE;
location.addAttribute(
attr.name,
attr.value,
vis,
attr.allowedEntities ? new Set(attr.allowedEntities) : null,
);
}
}
if (locData.connections) {
location.connections = locData.connections.map((c) => ({
targetId: c.targetId,
portalName: c.portalName,
portalStateDescriptor: c.portalStateDescriptor,
visionProp: c.visionProp,
soundProp: c.soundProp,
bidirectional: c.bidirectional,
}));
}
// Save location record linked to the world instance
this.coreRepo.saveLocation(location, world.id);
}
}
// 5. Instantiate and Persist Entities (with Aliases & Memory Buffers)
if (scenario.entities) {
for (const entData of scenario.entities) {
const entity = new Entity(entData.id, entData.locationId ?? null);
// Load attributes
if (entData.attributes) {
for (const attr of entData.attributes) {
const vis = attr.visibility === "PUBLIC" ? AttributeVisibility.PUBLIC : AttributeVisibility.PRIVATE;
entity.addAttribute(
attr.name,
attr.value,
vis,
attr.allowedEntities ? new Set(attr.allowedEntities) : null,
);
}
}
// Load aliases
if (entData.aliases) {
for (const [targetId, alias] of Object.entries(entData.aliases)) {
entity.aliases.set(targetId, alias);
}
}
// Save entity record linked to the world instance
this.coreRepo.saveEntity(entity, world.id);
// Seed initial memory buffer history
if (entData.initialMemories) {
for (const mem of entData.initialMemories) {
this.bufferRepo.save({
id: mem.id,
ownerId: entData.id,
timestamp: mem.timestamp,
locationId: mem.locationId,
intent: mem.intent,
outcome: mem.outcome,
});
}
}
}
}
return world.id;
}
}

View File

@@ -0,0 +1,65 @@
import { z } from "zod";
export const AttributeVisibilitySchema = z.enum(["PUBLIC", "PRIVATE"]);
export const ScenarioAttributeSchema = z.object({
name: z.string(),
value: z.string(),
visibility: AttributeVisibilitySchema,
allowedEntities: z.array(z.string()).optional(),
});
export const ScenarioPortalConnectionSchema = z.object({
targetId: z.string(),
portalName: z.string().optional(),
portalStateDescriptor: z.string().optional(),
visionProp: z.number().min(0).max(10),
soundProp: z.number().min(0).max(10),
bidirectional: z.boolean(),
});
export const ScenarioLocationSchema = z.object({
id: z.string(),
parentId: z.string().nullable().optional(),
attributes: z.array(ScenarioAttributeSchema).optional(),
connections: z.array(ScenarioPortalConnectionSchema).optional(),
});
export const ScenarioMemoryEntrySchema = z.object({
id: z.string(),
timestamp: z.string(), // ISO string
locationId: z.string().nullable(),
intent: z.object({
type: z.enum(["dialogue", "action", "monologue"]),
originalText: z.string(),
description: z.string(),
actorId: z.string(),
targetIds: z.array(z.string()),
}),
outcome: z.object({
isValid: z.boolean(),
reason: z.string(),
}).optional(),
});
export const ScenarioEntitySchema = z.object({
id: z.string(),
locationId: z.string().nullable().optional(),
attributes: z.array(ScenarioAttributeSchema).optional(),
aliases: z.record(z.string(), z.string()).optional(), // targetId -> subjective descriptor
initialMemories: z.array(ScenarioMemoryEntrySchema).optional(),
});
export const ScenarioSchema = z.object({
id: z.string(), // Template identifier
name: z.string(),
description: z.string(),
startTime: z.string(), // ISO string
world: z.object({
attributes: z.array(ScenarioAttributeSchema).optional(),
}).optional(),
locations: z.array(ScenarioLocationSchema).optional(),
entities: z.array(ScenarioEntitySchema).optional(),
});
export type Scenario = z.infer<typeof ScenarioSchema>;

View File

@@ -0,0 +1,136 @@
import { describe, test, expect } from "vitest";
import Database from "better-sqlite3";
import { SQLiteRepository } from "@omnia/core";
import { Location } from "@omnia/spatial";
import { BufferRepository } from "@omnia/memory";
import { ScenarioLoader, ScenarioSchema } from "../src/index.js";
describe("Scenario Validation & Schema Tests (Tier 1)", () => {
const validScenario = {
id: "sc-haunted-house",
name: "Haunted House Mystery",
description: "A spooky old manor.",
startTime: "2026-07-09T08:00:00.000Z",
world: {
attributes: [
{ name: "weather", value: "stormy", visibility: "PUBLIC" },
],
},
locations: [
{
id: "lobby",
parentId: null,
attributes: [
{ name: "light", value: "dim", visibility: "PUBLIC" },
],
connections: [
{
targetId: "kitchen",
portalName: "swinging door",
visionProp: 2,
soundProp: 6,
bidirectional: true,
},
],
},
{
id: "kitchen",
parentId: "lobby",
},
],
entities: [
{
id: "investigator",
locationId: "lobby",
attributes: [
{ name: "sanity", value: "100", visibility: "PRIVATE", allowedEntities: ["investigator"] },
],
aliases: {
ghost: "shadowy specter",
},
initialMemories: [
{
id: "mem-seed-1",
timestamp: "2026-07-09T07:55:00.000Z",
locationId: "lobby",
intent: {
type: "action",
originalText: "I entered the foyer.",
description: "entered the house",
actorId: "investigator",
targetIds: [],
},
},
],
},
],
};
test("successfully validates a valid scenario JSON template", () => {
const result = ScenarioSchema.safeParse(validScenario);
expect(result.success).toBe(true);
});
test("fails validation on invalid scenario structure", () => {
const invalidScenario = {
id: "sc-bad",
name: "Missing critical fields",
// description and startTime are missing
};
const result = ScenarioSchema.safeParse(invalidScenario);
expect(result.success).toBe(false);
});
test("loads scenario into SQLite database and reconstitutes all objects correctly", async () => {
const db = new Database(":memory:");
const coreRepo = new SQLiteRepository(db);
const bufferRepo = new BufferRepository(db);
const loader = new ScenarioLoader(coreRepo, bufferRepo);
const targetWorldId = "active-world-run-1";
const worldId = await loader.initializeWorld(validScenario, targetWorldId);
expect(worldId).toBe(targetWorldId);
// 1. Verify WorldState loaded
const world = coreRepo.loadWorldState(targetWorldId);
expect(world).not.toBeNull();
expect(world!.id).toBe(targetWorldId);
expect(world!.clock.get().toISOString()).toBe("2026-07-09T08:00:00.000Z");
expect(world!.attributes.get("name")?.getValue()).toBe("Haunted House Mystery");
expect(world!.attributes.get("weather")?.getValue()).toBe("stormy");
// 2. Verify Locations loaded with connections & hierarchy
const locations = coreRepo.listLocations(targetWorldId, (id, parentId) => new Location(id, parentId));
expect(locations).toHaveLength(2);
const lobby = locations.find((l) => l.id === "lobby");
expect(lobby).toBeDefined();
expect(lobby!.parentId).toBeNull();
expect(lobby!.attributes.get("light")?.getValue()).toBe("dim");
expect(lobby!.connections).toHaveLength(1);
expect(lobby!.connections[0].targetId).toBe("kitchen");
expect(lobby!.connections[0].portalName).toBe("swinging door");
expect(lobby!.connections[0].visionProp).toBe(2);
const kitchen = locations.find((l) => l.id === "kitchen");
expect(kitchen).toBeDefined();
expect(kitchen!.parentId).toBe("lobby");
// 3. Verify Entities loaded with subjective alias map
const loadedInvestigator = world!.getEntity("investigator");
expect(loadedInvestigator).toBeDefined();
expect(loadedInvestigator!.locationId).toBe("lobby");
expect(loadedInvestigator!.attributes.get("sanity")?.getValue()).toBe("100");
expect(loadedInvestigator!.aliases.get("ghost")).toBe("shadowy specter");
// 4. Verify pre-seeded memories loaded in BufferRepository
const memories = bufferRepo.listForOwner("investigator");
expect(memories).toHaveLength(1);
expect(memories[0].id).toBe("mem-seed-1");
expect(memories[0].timestamp).toBe("2026-07-09T07:55:00.000Z");
expect(memories[0].locationId).toBe("lobby");
expect(memories[0].intent.description).toBe("entered the house");
db.close();
});
});

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"],
"references": [
{ "path": "../../packages/core" },
{ "path": "../../packages/spatial" },
{ "path": "../../packages/memory" }
]
}