chore: establish learning module baseline
This commit is contained in:
74
OpenMAIC/lib/edit/content-validation.ts
Normal file
74
OpenMAIC/lib/edit/content-validation.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { Action } from '@/lib/types/action';
|
||||
import type { Scene } from '@/lib/types/stage';
|
||||
import type { SceneOutline } from '@/lib/types/generation';
|
||||
import { ELEMENT_BOUND } from '@/components/edit/ActionsBar/cue-meta';
|
||||
|
||||
/**
|
||||
* Pure content-validity checks for the editor. These surface *meaningless* edit
|
||||
* states — an empty speech line, a cue pointing at nothing, a scene with no
|
||||
* actions, a titleless outline — so the UI can mark them (scenes, lint-style)
|
||||
* or gate a forward step (outline → generate). No UI or store dependencies.
|
||||
*
|
||||
* Empties are tolerated while editing; this module only *describes* them. Whom
|
||||
* to block and where is the caller's policy.
|
||||
*/
|
||||
|
||||
/** A meaningless state tied to a specific action in a scene's timeline. */
|
||||
export type ActionIssue =
|
||||
| { kind: 'emptySpeech'; actionId: string }
|
||||
| { kind: 'unboundCue'; actionId: string }
|
||||
| { kind: 'emptyDiscussion'; actionId: string };
|
||||
|
||||
/** A meaningless state found on a single scene (action-level issues + empty list). */
|
||||
export type SceneIssue = { kind: 'emptyActions' } | ActionIssue;
|
||||
|
||||
/** A meaningless state found on a single outline. */
|
||||
export type OutlineIssue = { kind: 'emptyTitle' };
|
||||
|
||||
const isBlank = (s: string | undefined): boolean => !s || s.trim() === '';
|
||||
|
||||
/**
|
||||
* Per-action content issues, in timeline order: a blank speech line, a cue that
|
||||
* points at no element, a discussion with no topic. Drives the inline marks on
|
||||
* the ActionsBar clips.
|
||||
*/
|
||||
export function validateActions(actions: Action[]): ActionIssue[] {
|
||||
const issues: ActionIssue[] = [];
|
||||
for (const a of actions) {
|
||||
if (a.type === 'speech' && isBlank((a as { text?: string }).text)) {
|
||||
issues.push({ kind: 'emptySpeech', actionId: a.id });
|
||||
} else if (ELEMENT_BOUND.has(a.type) && isBlank((a as { elementId?: string }).elementId)) {
|
||||
issues.push({ kind: 'unboundCue', actionId: a.id });
|
||||
} else if (a.type === 'discussion' && isBlank((a as { topic?: string }).topic)) {
|
||||
issues.push({ kind: 'emptyDiscussion', actionId: a.id });
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
/** All content-validity issues on a scene, in action order (empty list first). */
|
||||
export function validateScene(scene: Scene): SceneIssue[] {
|
||||
const actions = (scene.actions ?? []) as Action[];
|
||||
if (actions.length === 0) return [{ kind: 'emptyActions' }];
|
||||
return validateActions(actions);
|
||||
}
|
||||
|
||||
/** Whether a scene has any content-validity issue. */
|
||||
export function sceneHasIssues(scene: Scene): boolean {
|
||||
return validateScene(scene).length > 0;
|
||||
}
|
||||
|
||||
/** Content-validity issues on an outline (blank title is the only blocking one). */
|
||||
export function validateOutline(outline: SceneOutline): OutlineIssue[] {
|
||||
return isBlank(outline.title) ? [{ kind: 'emptyTitle' }] : [];
|
||||
}
|
||||
|
||||
/** Whether any outline carries a blocking issue (used to gate generation). */
|
||||
export function outlinesHaveBlockingIssues(outlines: SceneOutline[]): boolean {
|
||||
return outlines.some((o) => validateOutline(o).length > 0);
|
||||
}
|
||||
|
||||
/** Count of outlines with a blocking issue (drives the gate's reason text). */
|
||||
export function countBlockingOutlines(outlines: SceneOutline[]): number {
|
||||
return outlines.reduce((n, o) => n + (validateOutline(o).length > 0 ? 1 : 0), 0);
|
||||
}
|
||||
Reference in New Issue
Block a user