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:
@@ -5,6 +5,7 @@
|
||||
* See CHUNK #4 step 2.4.
|
||||
*/
|
||||
import type { Character, Entity, ProseMirrorDoc } from "./types.js";
|
||||
import { collectMentionsInDoc } from "./backlinks.js";
|
||||
|
||||
export type RelationshipSymmetry = "symmetric" | "directed";
|
||||
|
||||
@@ -32,7 +33,16 @@ export interface RelationshipIssue {
|
||||
relationship: string;
|
||||
}
|
||||
|
||||
export type ConsistencyIssue = MentionIssue | RelationshipIssue;
|
||||
export interface DeadCharacterIssue {
|
||||
kind: "dead-character-mentioned";
|
||||
chapterId: string;
|
||||
chapterTitle: string;
|
||||
entityId: string;
|
||||
/** The mention text, if any. */
|
||||
name?: string | undefined;
|
||||
}
|
||||
|
||||
export type ConsistencyIssue = MentionIssue | RelationshipIssue | DeadCharacterIssue;
|
||||
|
||||
/**
|
||||
* Find mention nodes that reference an entity id that no longer exists (e.g.
|
||||
@@ -44,25 +54,50 @@ export function findOrphanedMentions(
|
||||
): MentionIssue[] {
|
||||
const issues: MentionIssue[] = [];
|
||||
for (const ch of chapters) {
|
||||
const visit = (content: unknown[], _path: number[]): void => {
|
||||
for (const node of content as Record<string, unknown>[]) {
|
||||
if (!node || typeof node !== "object") continue;
|
||||
if (node.type === "CharacterMention") {
|
||||
const attrs = node.attrs as { entityId?: string; name?: string } | undefined;
|
||||
if (attrs?.entityId && !entityIds.has(attrs.entityId)) {
|
||||
issues.push({
|
||||
kind: "orphan-mention",
|
||||
chapterId: ch.id,
|
||||
chapterTitle: ch.title,
|
||||
entityId: attrs.entityId,
|
||||
name: attrs.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (Array.isArray(node.content)) visit(node.content as unknown[], _path);
|
||||
const mentioned = collectMentionsInDoc(ch.doc);
|
||||
for (const entityId of mentioned) {
|
||||
if (!entityIds.has(entityId)) {
|
||||
issues.push({
|
||||
kind: "orphan-mention",
|
||||
chapterId: ch.id,
|
||||
chapterTitle: ch.title,
|
||||
entityId,
|
||||
});
|
||||
}
|
||||
};
|
||||
visit(ch.doc.content ?? [], []);
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find characters who are marked `dead` but are still mentioned (by entity id)
|
||||
* in a chapter body. A dead character should not appear alive in the story.
|
||||
*
|
||||
* Bare-name mentions (no `entityId`) are ignored — a reference to a name that
|
||||
* is not yet an entity is an orphan-mention concern, not a dead-character one.
|
||||
*/
|
||||
export function findDeadCharacterMentions(
|
||||
chapters: Array<{ id: string; title: string; doc: ProseMirrorDoc }>,
|
||||
entities: Entity[],
|
||||
): DeadCharacterIssue[] {
|
||||
const dead = new Set(
|
||||
entities
|
||||
.filter((e): e is Character => !("fields" in e) && !("body" in e) && e.status === "dead")
|
||||
.map((c) => c.id),
|
||||
);
|
||||
const issues: DeadCharacterIssue[] = [];
|
||||
for (const chapter of chapters) {
|
||||
const mentioned = collectMentionsInDoc(chapter.doc);
|
||||
for (const entityId of mentioned) {
|
||||
if (dead.has(entityId)) {
|
||||
issues.push({
|
||||
kind: "dead-character-mentioned",
|
||||
chapterId: chapter.id,
|
||||
chapterTitle: chapter.title,
|
||||
entityId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
@@ -125,6 +160,7 @@ export function findInconsistencies(
|
||||
const issues: ConsistencyIssue[] = [
|
||||
...findOrphanedMentions(chapters, entityIds),
|
||||
...findRelationshipAsymmetry(entities),
|
||||
...findDeadCharacterMentions(chapters, entities),
|
||||
];
|
||||
return issues;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user