fix(journal): surface variable eval errors
Collect and report failed declarations, failed tag modifier expressions, and circular dependencies instead of swallowing them. Diagnostics are deduped, logged with key/expression context, cleared on re-init, and exposed through useJournalCompletions and the VariableView warning banner. A bad declaration no longer aborts the rest of a cascade. Reject malformed numeric literals in the tokenizer so inputs like "1.2.3" throw instead of being truncated by parseFloat.
This commit is contained in:
@@ -56,6 +56,26 @@ export interface ActiveMod {
|
||||
kind: "numeric" | "tagmap";
|
||||
}
|
||||
|
||||
/**
|
||||
* A problem detected while evaluating declarations or tag modifiers at
|
||||
* runtime. These are benign to the engine (the offending entry is skipped) but
|
||||
* signal an authoring mistake that would otherwise silently produce a wrong
|
||||
* value, so they are collected for surfacing in the UI.
|
||||
*/
|
||||
export interface ReactivityDiagnostic {
|
||||
kind: "init" | "declaration" | "modifier";
|
||||
/** Human-readable description, already including the offending context. */
|
||||
message: string;
|
||||
/** The variable the diagnostic concerns (e.g. "$hp"). */
|
||||
key?: string;
|
||||
/** The expression that failed to evaluate. */
|
||||
expression?: string;
|
||||
/** For modifiers: the tag whose activation triggered the modifier ("#warrior"). */
|
||||
tag?: string;
|
||||
/** For modifiers: the variable whose tagmap activated the tag ("$class"). */
|
||||
source?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tagmap helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -133,6 +153,61 @@ const sourceActivations = new Map<string, Array<{
|
||||
/** Set of $vars currently being re-evaluated (cycle guard) */
|
||||
const inFlight = new Set<string>();
|
||||
|
||||
/** Diagnostics collected since the last init, keyed by a stable id to dedupe. */
|
||||
const diagnostics = new Map<string, ReactivityDiagnostic>();
|
||||
|
||||
/** Listeners notified whenever the diagnostic set changes. */
|
||||
const diagnosticListeners = new Set<(diagnostics: ReactivityDiagnostic[]) => void>();
|
||||
|
||||
function errorText(e: unknown): string {
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
function diagnosticId(d: ReactivityDiagnostic): string {
|
||||
return [d.kind, d.key ?? "", d.expression ?? "", d.source ?? "", d.tag ?? "", d.message].join("\u0000");
|
||||
}
|
||||
|
||||
function notifyDiagnostics(): void {
|
||||
const list = [...diagnostics.values()];
|
||||
for (const listener of diagnosticListeners) listener(list);
|
||||
}
|
||||
|
||||
/** Record a diagnostic, dedupe by identity, and notify listeners. */
|
||||
function reportDiagnostic(d: ReactivityDiagnostic): void {
|
||||
const id = diagnosticId(d);
|
||||
if (diagnostics.has(id)) return;
|
||||
diagnostics.set(id, d);
|
||||
notifyDiagnostics();
|
||||
console.warn(`[var-reactivity] ${d.message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostics collected by the engine (bad declarations, modifier expressions,
|
||||
* circular dependencies). Cleared by `initReactivity` so a re-init reports a
|
||||
* fresh set. Repeated failures of the same expression are reported once.
|
||||
*/
|
||||
export function getReactivityDiagnostics(): ReactivityDiagnostic[] {
|
||||
return [...diagnostics.values()];
|
||||
}
|
||||
|
||||
/** Remove all collected diagnostics. */
|
||||
export function clearReactivityDiagnostics(): void {
|
||||
if (diagnostics.size === 0) return;
|
||||
diagnostics.clear();
|
||||
notifyDiagnostics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to diagnostic changes. The listener receives the current list on
|
||||
* every change. Returns an unsubscribe function.
|
||||
*/
|
||||
export function onReactivityDiagnostics(
|
||||
listener: (diagnostics: ReactivityDiagnostic[]) => void,
|
||||
): () => void {
|
||||
diagnosticListeners.add(listener);
|
||||
return () => diagnosticListeners.delete(listener);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -151,6 +226,7 @@ export function initReactivity(state: VarReactivityState): void {
|
||||
numericMods.clear();
|
||||
tagMapMods.clear();
|
||||
sourceActivations.clear();
|
||||
clearReactivityDiagnostics();
|
||||
|
||||
// Index tag modifiers
|
||||
for (const tm of state.tagModifiers) {
|
||||
@@ -176,7 +252,15 @@ export function initReactivity(state: VarReactivityState): void {
|
||||
}
|
||||
}
|
||||
|
||||
checkCircular();
|
||||
try {
|
||||
checkCircular();
|
||||
} catch (e) {
|
||||
reportDiagnostic({
|
||||
kind: "init",
|
||||
message: `Declarations contain an unresolvable dependency: ${errorText(e)}`,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract $var names from an expression string. */
|
||||
@@ -400,8 +484,13 @@ export function computeInitialValues(
|
||||
if (!results.some((x) => x.key === key)) {
|
||||
results.push({ key, value: combined, base: rawValue });
|
||||
}
|
||||
} catch {
|
||||
// skip failed evaluations at init time
|
||||
} catch (e) {
|
||||
reportDiagnostic({
|
||||
kind: "declaration",
|
||||
key,
|
||||
expression: expr,
|
||||
message: `Declaration ${key} failed to evaluate "${expr}": ${errorText(e)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,8 +595,17 @@ function applyTagMapActivations(
|
||||
} else {
|
||||
results.push({ key: mod.target, value: combined, base });
|
||||
}
|
||||
} catch {
|
||||
// skip failed modifier
|
||||
} catch (e) {
|
||||
reportDiagnostic({
|
||||
kind: "modifier",
|
||||
key: mod.target,
|
||||
expression: mod.expression,
|
||||
tag,
|
||||
source,
|
||||
message:
|
||||
`Modifier ${tag} → ${mod.target} failed to evaluate ` +
|
||||
`"${mod.expression}" (activated by ${source}): ${errorText(e)}`,
|
||||
});
|
||||
}
|
||||
} else if (wasActive && !isActive) {
|
||||
// Deactivate this modifier
|
||||
@@ -635,6 +733,14 @@ function reevaluateDependents(
|
||||
if (!results.some((x) => x.key === key)) {
|
||||
results.push({ key, value: combined, base: rawValue });
|
||||
}
|
||||
} catch (e) {
|
||||
// Report the bad declaration but keep evaluating unrelated dependents.
|
||||
reportDiagnostic({
|
||||
kind: "declaration",
|
||||
key,
|
||||
expression: declExprs.get(key),
|
||||
message: `Declaration ${key} failed to re-evaluate "${declExprs.get(key) ?? ""}": ${errorText(e)}`,
|
||||
});
|
||||
} finally {
|
||||
inFlight.delete(key);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user