clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / src / mods / diagnostics.js
diagnostics.js 8.4K
1import { parseRules, RuleParseLimitError } from '../engine/rules.js';2import { CORE_PROPERTY_TOKENS } from '../engine/tokens.js';3import { CURRENT_RULES_VERSION } from '../mods.js';4import { resolveWorldMetadata } from './world.js';56export const MAX_MOD_WARNINGS = 64;7const CORE_PROPERTIES = new Set(CORE_PROPERTY_TOKENS);8const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value);910/** Return bounded author warnings without mutating or annotating pack data. */11export function collectModWarnings(normalizedMod) {12 if (!isRecord(normalizedMod)) return [];13 const warnings = [];14 warnings.push(...resolveWorldMetadata(normalizedMod).warnings);15 const manifest = normalizedMod.manifest;16 if (isRecord(manifest) && Object.prototype.hasOwnProperty.call(manifest, 'rules')) {17 const rules = Number.isInteger(manifest.rules) ? manifest.rules : 1;18 if (rules < CURRENT_RULES_VERSION) warnings.push({19 word: 'RULES',20 levelId: null,21 levelTitle: null,22 sentence: null,23 message: 'This pack was made for older rules; some proofs may play differently.',24 detail: `manifest.rules: effective revision ${rules}; current host revision ${CURRENT_RULES_VERSION}.`,25 });26 }27 if (warnings.length >= MAX_MOD_WARNINGS) return warnings;28 const levels = Array.isArray(normalizedMod.levels) ? normalizedMod.levels : [];29 const allProperties = new Set(CORE_PROPERTIES);30 const packNouns = new Set(Array.isArray(normalizedMod.vocabulary) ? normalizedMod.vocabulary.filter(value => typeof value === 'string').map(value => value.toUpperCase()) : []);31 for (const noun of Object.keys(isRecord(normalizedMod.palette) ? normalizedMod.palette : {})) packNouns.add(noun.toUpperCase());32 const allObjectNouns = new Set();33 for (const level of levels) for (const automaton of Array.isArray(level?.automata) ? level.automata : []) {34 if (typeof automaton?.property === 'string') allProperties.add(automaton.property.toUpperCase());35 }36 for (const level of levels) for (const object of Array.isArray(level?.objects) ? level.objects : []) {37 if (object?.kind !== 'word' && typeof object?.noun === 'string') allObjectNouns.add(object.noun.toUpperCase());38 }39 for (const [levelIndex, level] of levels.entries()) {40 const levelId = typeof level?.id === 'string' ? level.id : undefined;41 const levelTitle = typeof level?.title === 'string' && level.title ? level.title : `Proof ${levelIndex + 1}`;42 if (typeof level?.title === 'string' && level.title.length > 32) {43 warnings.push({44 word: 'TITLE',45 ...(levelId ? { levelId } : {}),46 levelTitle,47 sentence: null,48 message: 'Proof titles should use 24 characters or fewer for easier reading.',49 detail: `levels[${levelIndex}].title: proof title is ${level.title.length} characters; recommended maximum is 24.`,50 });51 if (warnings.length >= MAX_MOD_WARNINGS) return warnings;52 }53 const objects = (Array.isArray(level?.objects) ? level.objects : []).map((object, index) => ({ ...object, id: object.id || `o${index}` }));54 const semantic = new Set(CORE_PROPERTIES);55 for (const automaton of Array.isArray(level?.automata) ? level.automata : []) {56 if (typeof automaton?.property === 'string') semantic.add(automaton.property.toUpperCase());57 }58 let parsed;59 try {60 parsed = parseRules(objects, semantic);61 } catch (error) {62 if (!(error instanceof RuleParseLimitError)) throw error;63 const limit = Number.isInteger(error.limit) ? ` (${error.limit})` : '';64 warnings.push({65 word: 'RULES',66 ...(levelId ? { levelId } : {}),67 levelTitle,68 sentence: null,69 message: `Rule text is too complex to analyze safely; reduce stacked rule alternatives.`,70 detail: `levels[${levelIndex}].objects: rule parse ${error.kind || 'complexity'} budget exceeded${limit}.`,71 });72 if (warnings.length >= MAX_MOD_WARNINGS) return warnings;73 continue;74 }75 const words = new Map(objects.filter(object => object?.kind === 'word').map(object => [object.id, object]));76 const objectIndexes = new Map(objects.map((object, index) => [object.id, index]));77 const knownNouns = new Set(objects.filter(object => object?.kind !== 'word' && typeof object?.noun === 'string').map(object => object.noun.toUpperCase()));78 for (const automaton of Array.isArray(level?.automata) ? level.automata : []) {79 if (typeof automaton?.subject === 'string') knownNouns.add(automaton.subject.toUpperCase());80 }81 const seenWords = new Set();82 const sourceLocation = sourceId => `levels[${levelIndex}].objects[${objectIndexes.get(sourceId)}].text`;83 const formed = new Map();84 for (const rule of parsed.rules) {85 const ids = rule.sourceIds || [];86 const isIndex = ids.findIndex(id => words.get(id)?.text === 'IS');87 const predicateId = ids.slice(isIndex + 1).find(id => words.get(id)?.text === rule.predicate);88 if (predicateId && !formed.has(predicateId)) formed.set(predicateId, { sentence: rule.text, role: 'predicate', subject: rule.subject });89 for (const id of ids.slice(0, isIndex < 0 ? 0 : isIndex)) {90 const word = words.get(id)?.text;91 if (word && word !== 'AND' && !formed.has(id)) formed.set(id, { sentence: rule.text, role: 'subject' });92 }93 }94 const addWarning = ({ word, sourceId, sentence = null, message, detail }) => {95 const upper = String(word).toUpperCase();96 if (seenWords.has(upper)) return;97 seenWords.add(upper);98 warnings.push({ word: upper, ...(levelId ? { levelId } : {}), levelTitle, sentence, message, detail: detail || sourceLocation(sourceId) });99 };100 for (const object of objects) {101 if (object.kind !== 'word' || typeof object.text !== 'string') continue;102 const word = object.text.toUpperCase();103 const crossPropertyThing = allProperties.has(word) && allObjectNouns.has(word);104 if (word === 'IS' || word === 'AND' || semantic.has(word) || (knownNouns.has(word) && !crossPropertyThing) || (packNouns.has(word) && !crossPropertyThing)) continue;105 const context = formed.get(object.id);106 const otherProperty = allProperties.has(word) && !semantic.has(word);107 const crossPackThing = allObjectNouns.has(word) && !knownNouns.has(word) && !crossPropertyThing;108 const sentence = context?.sentence || null;109 const message = context?.role === 'predicate'110 ? (crossPropertyThing111 ? `${word} is declared as a property and used as a thing in another proof; ${sentence} will transform ${context.subject} into ${word}.`112 : crossPackThing113 ? `${word} is used as a thing in another proof and has no property behavior here; ${sentence} will transform ${context.subject} into ${word}.`114 : otherProperty115 ? `${word} is declared as a property in another proof but has no behavior here; ${sentence} will transform ${context.subject} into ${word}.`116 : `${word} is not a declared property here; ${sentence} will transform ${context.subject} into ${word}.`)117 : context?.role === 'subject'118 ? (crossPropertyThing119 ? `${word} is declared as a property and used as a thing in another proof; ${sentence} uses it as a noun.`120 : crossPackThing121 ? `${word} is used as a thing in another proof but is not a known noun here; ${sentence} uses it as a noun.`122 : otherProperty123 ? `${word} is declared as a property in another proof but has no behavior here; ${sentence} uses it as a noun.`124 : `${word} is not a known noun in this proof; ${sentence} uses it as a noun.`)125 : crossPropertyThing126 ? `${word} is declared as a property and used as a thing in another proof, so this proof needs an explicit choice.`127 : crossPackThing128 ? `${word} is used as a thing in another proof but is not a known noun here.`129 : otherProperty130 ? `${word} is declared as a property in another proof but has no behavior here.`131 : `${word} is not a known noun or declared property in this proof.`;132 addWarning({ word, sourceId: object.id, sentence, message, detail: context133 ? `${sourceLocation(object.id)}: ${word} is not an implemented property or known noun in this proof.`134 : `${sourceLocation(object.id)}: word ${word} is not a placed noun, automaton subject, core property, or property implemented in this proof.` });135 if (warnings.length >= MAX_MOD_WARNINGS) return warnings;136 }137 }138 return warnings;139}