Files
PubScript/apps/web/src/store/seed.ts
T
Schwitzz 301e842c5c feat(entity): Chunk #4 remaining items — consistency UI, context rail, palette search, expanded seed & DATA_MODEL
- Consistency tab in EntitiesView surfacing findInconsistencies() results
- ContextRail + MentionPopover wired into WritingView for entity context/hover
- CommandPalette indexes entities and chapters for cross-entity search
- link field type for entity references in character fields
- findDeadCharacterMentions consistency check + tests
- Expand seed with a location, note, and a natural Elias Vell mention so the
  dead-character-after-death case is actually exercised
- Author docs/DATA_MODEL.md documenting core schemas, types, storage contract
- Rename CharacterMention id attr -> entityId to match core backlinks/consistency
2026-10-26 12:02:03 -05:00

265 lines
13 KiB
TypeScript

/**
* Client-side seed for the offline-first local adapter.
*
* The server `seed.ts` populates SQLite; this mirrors it for IndexedDB so the
* browser app has the same demo project ("The Glass Harbor") to explore the
* editor against on first run. Idempotent: the store only seeds when no
* projects exist yet.
*/
import {
createUlid,
type Board,
type Character,
type Note,
type ProseMirrorDoc,
type WorldItem,
type StorageAdapter,
} from "@pubscript/core";
/** A single ProseMirror paragraph node. */
function para(text: string): ProseMirrorDoc["content"][0] {
return { type: "paragraph", content: [{ type: "text", text }] };
}
/** A paragraph containing a character mention atom (used to exercise backlinks & consistency). */
function paraMention(
before: string,
entityId: string,
label: string,
after: string,
): ProseMirrorDoc["content"][0] {
return {
type: "paragraph",
content: [
{ type: "text", text: before },
{ type: "CharacterMention", attrs: { entityId, label } },
{ type: "text", text: after },
],
};
}
/** Build a ProseMirror doc from an array of paragraph texts or nodes. */
function doc(paras: Array<string | ProseMirrorDoc["content"][0]>): ProseMirrorDoc {
return { type: "doc", content: paras.map((p) => (typeof p === "string" ? para(p) : p)) };
}
const CHAPTERS: { title: string; paras: Array<string | ProseMirrorDoc["content"][0]> }[] = [
{
title: "Chapter One — The Arrival",
paras: [
"The ferry that carried Mara Vell to the Glass Harbor left the mainland before dawn, and by the time it came to rest the sea had turned the color of a bruised plum.",
"She pressed her palm against the cold window and watched the lighthouse appear out of the mist — a thin white line standing where the land gave up to the water.",
"Three years since she had last seen it, and yet it seemed to have shrunk, or perhaps she had simply grown too large for the memory of it.",
"The harbor master waited for her on the quay, wrapped in oilskin against a wind that did not so much blow as insist. He did not wave. He never waved.",
'"You came," he said, as if to say he had not expected her, and had been wrong about it all morning.',
'"The light is failing," he added. "We have had three nights without it. The fish are turning away."',
],
},
{
title: "Chapter Two — The Storm",
paras: [
"The storm arrived on the second night, and it came the way everything at the Glass Harbor came — without apology and without warning.",
"Mara climbed the one hundred and fourteen steps to the lamp, each one slick with salt spray, and found that the mechanism had seized with the same stubborn rust that had seized everything else on this rock.",
"She worked through the small hours with a wrench and her own two hands, coaxing the gears until at last the great beam swung free and threw its light out across the black water.",
"Below her, in the harbor, the boats threw their lines and danced on them, and she thought of all the sailors who had been lost here when the light was dead.",
'"Four ships in ten years," said a voice behind her. It was Old Fen, the hermit, half-frozen and grinning as if the storm were a joke only he understood.',
'"They did not die because the light failed," Fen said. "They died because they forgot that someone had to keep it."',
],
},
{
title: "Chapter Three — The Glass in the Dark",
paras: [
"In the morning the sea lay flat and exhausted, strewn with the kelp and driftwood the storm had disgorged.",
"Mara walked the shoreline and found, half-buried in the wrack, a shard of glass the size of her palm — but the glass was warm, and it held inside it a light that did not belong to the rising sun.",
"This was the secret the Glass Harbor had kept for a hundred years: the light of the lighthouse was not made, it was mined — drawn up from the deep in shards of living glass that only a keeper knew how to kindle.",
"She understood then why the old keeper had never waved, and why he had never left. Some duties are not chosen. They are inherited, like the tide.",
paraMention(
"Her father, ",
"char-elias-vell",
"Elias Vell",
" had drowned keeping the light when she was fifteen — and now the duty he left behind felt heavier than the whole sea.",
),
"When Tomas Reed returned from his rounds that evening, he found Mara at the lamp, her hands on the warm glass, and he stopped in the doorway with the old grief on his face.",
'"I can teach you," he said. "If you want it. No one has ever wanted it before."',
"Mara looked out at the water, at the fragile line of the quay, at the little lights of the town that had welcomed her without asking why she had come.",
'"I want it," she said, and the light answered for her.',
],
},
];
const CHARACTERS: Character[] = [
{
id: "char-mara-vell",
projectId: "",
name: "Mara Vell",
role: "The New Keeper",
bio: "A cartographer who returned to the Glass Harbor she fled three years ago, carrying a grief she has never named. Precise, stubborn, and quietly afraid of being needed.",
appearance: "Late thirties, salt-short hair, salt-calloused hands, a scar through one eyebrow from a rigging line she did not remember cutting herself on.",
voice: "Dry, understated, speaks like someone counting the cost of every word.",
relationships: [
{ targetId: "char-tomas-reed", kind: "family", note: "Uncle; the keeper before her." },
{ targetId: "char-old-fen", kind: "mentor", note: "Taught her to swim in waters too cold for children." },
],
arc: "From someone running from duty to someone who chooses to keep the light.",
status: "active",
tags: ["protagonist", "keeper"],
},
{
id: "char-tomas-reed",
projectId: "",
name: "Tomas Reed",
role: "The Harbor Master",
bio: "The keeper of the quay and the keeper of the light before Mara. Carries the old grief of a storm he could not save. Does not wave because he believes grief is a full-time job.",
appearance: "Sixties, weathered like driftwood, one leg that aches before a storm.",
voice: "Few words. Speaks in the short sentences of a man who has learned words are expensive.",
relationships: [
{ targetId: "char-mara-vell", kind: "family", note: "Niece; the keeper he is training." },
{ targetId: "char-old-fen", kind: "ally", note: "Oldest friend and only confidant." },
],
arc: "From guarding a secret alone to trusting someone to inherit it.",
status: "active",
tags: ["mentor", "keeper"],
},
{
id: "char-iris-kade",
projectId: "",
name: "Iris Kade",
role: "The Smuggler",
bio: "Runs the supply boat between the harbor and the mainland, carrying glass as carefully as she carries secrets. Witty, generous, and impossible to trust with a straight face.",
appearance: "Forties, sun-leathered, a silver ring through each ear, always barefoot even in winter.",
voice: "Fast, warm, teasing. Lies beautifully and enjoys it.",
relationships: [
{ targetId: "char-mara-vell", kind: "ally", note: "Funds the lighthouse quietly; becomes Mara's ally on the mainland." },
{ targetId: "char-tomas-reed", kind: "rival", note: "Clashes over how to keep the harbor alive." },
],
arc: "From a self-interested survivor to someone with a place to belong.",
status: "active",
tags: ["ally", "rival"],
},
{
id: "char-old-fen",
projectId: "",
name: "Old Fen",
role: "The Hermit",
bio: "Lives in a cottage where the land gives up to the water. Has seen four ships lost and never once said how he knew it was coming. Everyone fears him and loves him in the same breath.",
appearance: "Ageless, thin, eyes the pale gray of the sea before a storm.",
voice: "Speaks in riddles that turn out to be warnings. Grins at the wrong moments.",
relationships: [
{ targetId: "char-mara-vell", kind: "mentor", note: "The child he taught to swim; the keeper he is guiding." },
{ targetId: "char-tomas-reed", kind: "ally", note: "Oldest friend; the only one who understands him." },
],
arc: "From keeper of impossible knowledge to the one who finally tells the truth.",
status: "active",
tags: ["mysterious", "hermit"],
},
{
id: "char-elias-vell",
projectId: "",
name: "Elias Vell",
role: "The Keeper Who Died",
bio: "Mara's father and the lighthouse keeper before Tomas. Drowned keeping the light through the Great Storm of Mara's fifteenth year. He is gone, but his absence shapes everyone who loved him — especially the daughter who left and came back.",
appearance: "Salt of Mara's own hair, a keeper's sun-leathered face, hands gone blue from the deep glass.",
voice: "Never heard. Remembered for the songs he hummed while turning the lamp.",
relationships: [
{ targetId: "char-mara-vell", kind: "family", note: "Daughter; the one he could not save from the harbor, or from himself." },
],
arc: "A grief inherited, not chosen — the weight Mara carries and must finally set down.",
status: "dead",
tags: ["ghost", "father", "keeper"],
},
];
const WORLD: WorldItem = {
id: "world-glass-harbor",
projectId: "",
kind: "location",
name: "The Glass Harbor",
fields: {
kind: "location",
description:
"A fishing village on a wind-battered rock at the edge of the mainland shelf, lit by a lighthouse whose beam is fed by shards of living glass drawn from the deep.",
population: "Under two hundred",
economy: "Fishing, and the quiet, illegal trade in harbor-glass that keeps the light alive.",
notable:
"The Lighthouse (one hundred and fourteen steps to the lamp) · The Quay (where Tomas waits and Iris unloads her cargo) · The Wrack Line (where the sea deposits what the storm has taken).",
},
};
const LIGHTHOUSE: WorldItem = {
id: "world-lighthouse",
projectId: "",
kind: "location",
name: "The Lighthouse",
fields: {
kind: "location",
description:
"A tall white tower on the northern point of the rock, reached by one hundred and fourteen slick steps. Its lamp is fed by shards of living glass, and the mechanism that turns the beam has seized more than once.",
population: "—",
economy: "—",
notable: "The Lamp (where the beam is kindled) · The Gears (that seize with salt and rust) · The Stairs (one hundred and fourteen, and counting).",
},
};
const ARRIVAL_NOTE: Note = {
id: "note-arrival",
projectId: "",
body: "Mara Vell returns to the Glass Harbor three years after fleeing it. She came for a reason she has never named — and the harbor, its failing light, and the people who keep it are not the same as the memory she carried away. What is the grief she runs from, and why does the glass answer when she says she wants the light?",
pinned: true,
tags: ["plot", "act-one"],
linkedTo: "char-mara-vell",
};
const BOARD: Board = {
id: "board-story",
projectId: "",
nodes: [
{ id: "n1", type: "note", position: { x: 80, y: 120 }, data: { text: "Mara arrives at the harbor" } },
{ id: "n2", type: "note", position: { x: 320, y: 120 }, data: { text: "The light is failing" } },
{ id: "n3", type: "note", position: { x: 560, y: 120 }, data: { text: "The storm arrives" } },
{ id: "n4", type: "note", position: { x: 800, y: 120 }, data: { text: "Mara saves the light" } },
{ id: "n5", type: "note", position: { x: 320, y: 320 }, data: { text: "The secret of the glass" } },
{ id: "n6", type: "note", position: { x: 560, y: 320 }, data: { text: "Mara chooses the duty" } },
],
edges: [
{ id: "e1", source: "n1", target: "n2", type: "leads-to" },
{ id: "e2", source: "n2", target: "n3", type: "leads-to" },
{ id: "e3", source: "n3", target: "n4", type: "leads-to" },
{ id: "e4", source: "n4", target: "n5", type: "causes" },
{ id: "e5", source: "n5", target: "n6", type: "leads-to" },
],
};
/**
* Seed the adapter with the demo project if it has no projects yet.
* Returns the created project id.
*/
export async function seedDemoProject(adapter: StorageAdapter): Promise<string> {
const project = await adapter.createProject({
title: "The Glass Harbor",
cover: { color: "#0f3d3e" },
});
const book = await adapter.createBook({ projectId: project.id, title: "Book One" });
for (const chapter of CHAPTERS) {
await adapter.saveDocument({
id: createUlid(),
projectId: project.id,
bookId: book.id,
title: chapter.title,
doc: doc(chapter.paras),
rev: 0,
});
}
for (const character of CHARACTERS) {
await adapter.saveEntity({ ...character, projectId: project.id });
}
await adapter.saveEntity({ ...WORLD, projectId: project.id });
await adapter.saveEntity({ ...LIGHTHOUSE, projectId: project.id });
await adapter.saveEntity({ ...ARRIVAL_NOTE, projectId: project.id });
await adapter.saveBoard({ ...BOARD, projectId: project.id });
return project.id;
}