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
This commit is contained in:
Schwitzz
2026-10-26 12:02:03 -05:00
parent 4efa71515e
commit 301e842c5c
17 changed files with 1265 additions and 65 deletions
+69 -4
View File
@@ -10,6 +10,7 @@ import {
createUlid,
type Board,
type Character,
type Note,
type ProseMirrorDoc,
type WorldItem,
type StorageAdapter,
@@ -20,12 +21,29 @@ function para(text: string): ProseMirrorDoc["content"][0] {
return { type: "paragraph", content: [{ type: "text", text }] };
}
/** Build a ProseMirror doc from an array of paragraph texts. */
function doc(paras: string[]): ProseMirrorDoc {
return { type: "doc", content: paras.map(para) };
/** 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 },
],
};
}
const CHAPTERS: { title: string; paras: string[] }[] = [
/** 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: [
@@ -55,6 +73,12 @@ const CHAPTERS: { title: string; paras: string[] }[] = [
"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.",
@@ -128,6 +152,21 @@ const CHARACTERS: Character[] = [
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 = {
@@ -146,6 +185,30 @@ const WORLD: WorldItem = {
},
};
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: "",
@@ -193,6 +256,8 @@ export async function seedDemoProject(adapter: StorageAdapter): Promise<string>
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;