Building an idempotent, parallel extraction pipeline using map-reduce
techniques for large texts like novels is exactly how production pipelines
operate. We can certainly architect the document sharding and parallel worker
distribution later.
For now, here is the middleware compiler that translates Gemma's standard JSON
output into the compact, executable JSON-Lisp AST format that your MUD and
Canvas engines consume.
This compiler performs a single pass over the extracted nodes. Because the LLM
only outputs dimensions (w, h), the compiler must dynamically resolve the
Euclidean geometry, assigning x and y origins by tracing the graph from a
starting anchor point and calculating offsets based on the exit directions.
The Middleware Compiler Script
* Compiles LLM JSON output into a spatial JSON-Lisp AST.
* @param {Object} extractedJson - The sanitized JSON from Gemma.
* @returns {Array} - The JSON-Lisp "zones" branch.
*/
function compileToAST(extractedJson) {
const roomMap = {};
extractedJson.zones.forEach(z => roomMap[z.id] = z);
const layout = {};
const gap = 3; // The distance (in grid tiles) between connected rooms
// Recursive graph traversal to plot room coordinates
function plotRoom(id, x, y) {
if (layout[id]) return; // Already plotted
const room = roomMap[id];
if (!room) return;
layout[id] = { x, y, w: room.grid.w, h: room.grid.h };
// Calculate neighbor coordinates based on exit directions
if (room.exits) {
room.exits.forEach(exit => {
if (layout[exit.target_id]) return;
const target = roomMap[exit.target_id];
if (!target) return;
let tx = x, ty = y;
// Center the rooms against each other and apply the
directional gap
if (exit.direction === 'north') {
tx = x + Math.floor((room.grid.w - target.grid.w) / 2);
ty = y - target.grid.h - gap;
} else if (exit.direction === 'south') {
tx = x + Math.floor((room.grid.w - target.grid.w) / 2);
ty = y + room.grid.h + gap;
} else if (exit.direction === 'east') {
tx = x + room.grid.w + gap;
ty = y + Math.floor((room.grid.h - target.grid.h) / 2);
} else if (exit.direction === 'west') {
tx = x - target.grid.w - gap;
ty = y + Math.floor((room.grid.h - target.grid.h) / 2);
}
plotRoom(exit.target_id, tx, ty);
});
}
}
// Anchor the first room at an arbitrary center point on the Canvas
if (extractedJson.zones.length > 0) {
plotRoom(extractedJson.zones[0].id, 15, 15);
}
// Fallback: If Gemma hallucinates a disconnected room, plot it off to the
side
extractedJson.zones.forEach((z, index) => {
if (!layout[z.id]) {
plotRoom(z.id, 15 + (index * 10), 15 + (index * 10));
}
});
// Translate the plotted data into the final JSON-Lisp array structure
const astZones = [ "zones" ];
extractedJson.zones.forEach(z => {
const exitsArray = z.exits ? z.exits.map(e => [
"exit",
e.direction,
e.target_id,
{ "desc": `A passage leading ${e.direction}`, "type": "walkable" }
]) : [];
astZones.push([
"room",
z.id,
{
"name": z.name,
"description": z.description,
"grid": layout[z.id]
},
exitsArray
]);
});
return astZones;
}
How to Hook It Up
In your prototype UI, inside the btn-extract click handler (which currently
calls mountWorld), you can wire this directly to your Gemma pipeline:
btnExtract.addEventListener('click', async () => {
btnExtract.textContent = "Extracting... (Contacting Gemma)";
btnExtract.disabled = true;
const textToScan = document.getElementById('book-input').value;
// 1. Run the LLM Extraction
const llmJson = await extractTopologyPass(textToScan);
// 2. Compile to JSON-Lisp AST
const generatedZones = compileToAST(llmJson);
// 3. Mount it into the mock engine state
mockAST[2] = generatedZones; // Overwrite the hardcoded zones with live
data
// 4. Start the Engine
mountWorld();
});
Because this compiler standardizes the output, it guarantees that no matter
what text you feed the pipeline, the resulting AST maps flawlessly to both
your 2D grid logic and your text-based MUD parser. The data can immediately be
serialized using your AST-MD formats and synced via your CRDT relays.
Should we map out the chunking/sharding strategy for large files next, or
would you prefer to draft Pass 2 (Entity & Event Extraction) for this
single-page window first?