- commit
- d99d0b0d494bf2d1f3c3a47d134d87fdf482c3c3
- parent
- af8582ad2a8ee2788a8337fcfa470842f6d02231
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 3 | +- |
| A | src/ | 2 | + |
| M | src/ | 81 | +----------------------------- |
| A | src/ | 76 | ++++++++++++++++++++++++++++ |
@@ -1,8 +1,9 @@
11 /** Predicates implemented by the MVP engine. */22 import { buildIndexes } from './engine/indexes.js';33 import { parseRules } from './engine/rules.js';4+import { CORE_PROPERTY_TOKENS } from './engine/tokens.js';45 5-export const CORE_PROPERTIES = new Set(['YOU', 'PUSH', 'STOP', 'WIN', 'SINK', 'DEFEAT']);6+export const CORE_PROPERTIES = new Set(CORE_PROPERTY_TOKENS);67 const MAX_AUTOMATA = 32;78 const MAX_AUTOMATON_PATTERN = 32;89 const clone = value => structuredClone(value);@@ -1,4 +1,5 @@
1-import { parseRules } from './engine/rules.js';1+import { CORE_PROPERTY_TOKENS } from './engine/tokens.js';2+export { collectModWarnings, MAX_MOD_WARNINGS } from './mods/diagnostics.js';23 34 /**45 * Browser loader and validator for local CommonJS mods.@@ -15,14 +16,13 @@ export const MAX_LEVEL_OBJECTS = 4096;
1516 export const MAX_LEVEL_AUTOMATA = 32;1617 export const MAX_AUTOMATON_PATTERN = 32;1718 export const SUPPORTED_MOD_FEATURES = Object.freeze(['automata']);18-export const MAX_MOD_WARNINGS = 64;1919 2020 const ID_RE = /^[a-z][a-z0-9._-]{1,63}$/;2121 const LEVEL_ID_RE = /^[a-z][a-z0-9._-]{0,63}$/;2222 const VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;2323 const TOKEN_RE = /^[A-Z][A-Z0-9_-]*$/;2424 const RESERVED = new Set(['IS', 'AND', 'YOU', 'PUSH', 'STOP', 'WIN', 'SINK', 'DEFEAT']);25-const CORE_PROPERTIES = new Set(['YOU', 'PUSH', 'STOP', 'WIN', 'SINK', 'DEFEAT']);25+const CORE_PROPERTIES = new Set(CORE_PROPERTY_TOKENS);2626 const FEATURE_RE = /^[a-z][a-z0-9._-]{0,31}$/;2727 const AUTOMATON_SOUNDS = new Set(['tick', 'move', 'push', 'blockedStop', 'blockedBounds', 'ruleFormed', 'ruleBroken', 'transform', 'sink', 'defeat', 'win', 'undo', 'restart', 'select', 'confirm', 'back', 'scene']);2828 @@ -225,81 +225,6 @@ export function validateMod(mod) {
225225 return errors;226226 }227227 228-/**229- * Return bounded, nonfatal diagnostics for authored rule sentences. The230- * engine parser is reused so stacked words and AND expansion stay aligned231- * with gameplay. Diagnostics are intentionally not attached to normalized232- * packs or serialized share data.233- */234-export function collectModWarnings(normalizedMod) {235- if (!isRecord(normalizedMod)) return [];236- const warnings = [];237- const levels = Array.isArray(normalizedMod.levels) ? normalizedMod.levels : [];238- const allProperties = new Set(CORE_PROPERTIES);239- for (const level of levels) for (const automaton of Array.isArray(level?.automata) ? level.automata : []) {240- if (typeof automaton?.property === 'string') allProperties.add(automaton.property.toUpperCase());241- }242- for (const [levelIndex, level] of levels.entries()) {243- const objects = (Array.isArray(level?.objects) ? level.objects : []).map((object, index) => ({ ...object, id: object.id || `o${index}` }));244- const semantic = new Set(CORE_PROPERTIES);245- for (const automaton of Array.isArray(level?.automata) ? level.automata : []) {246- if (typeof automaton?.property === 'string') semantic.add(automaton.property.toUpperCase());247- }248- const parsed = parseRules(objects, semantic);249- const words = new Map(objects.filter(object => object?.kind === 'word').map(object => [object.id, object]));250- const objectIndexes = new Map(objects.map((object, index) => [object.id, index]));251- const knownNouns = new Set(objects.filter(object => object?.kind !== 'word' && typeof object?.noun === 'string').map(object => object.noun.toUpperCase()));252- for (const automaton of Array.isArray(level?.automata) ? level.automata : []) {253- if (typeof automaton?.subject === 'string') knownNouns.add(automaton.subject.toUpperCase());254- }255- const levelId = typeof level?.id === 'string' ? level.id : undefined;256- const levelTitle = typeof level?.title === 'string' && level.title ? level.title : `Proof ${levelIndex + 1}`;257- const seenWords = new Set();258- const sourceLocation = sourceId => `levels[${levelIndex}].objects[${objectIndexes.get(sourceId)}].text`;259- const formed = new Map();260- for (const rule of parsed.rules) {261- const ids = rule.sourceIds || [];262- const isIndex = ids.findIndex(id => words.get(id)?.text === 'IS');263- const predicateId = ids.slice(isIndex + 1).find(id => words.get(id)?.text === rule.predicate);264- if (predicateId && !formed.has(predicateId)) formed.set(predicateId, { sentence: rule.text, role: 'predicate', subject: rule.subject });265- for (const id of ids.slice(0, isIndex < 0 ? 0 : isIndex)) {266- const word = words.get(id)?.text;267- if (word && word !== 'AND' && !formed.has(id)) formed.set(id, { sentence: rule.text, role: 'subject' });268- }269- }270- const addWarning = ({ word, sourceId, sentence = null, message, detail }) => {271- const upper = String(word).toUpperCase();272- if (seenWords.has(upper)) return;273- seenWords.add(upper);274- warnings.push({ word: upper, ...(levelId ? { levelId } : {}), levelTitle, sentence, message, detail: detail || sourceLocation(sourceId) });275- };276- for (const object of objects) {277- if (object.kind !== 'word' || typeof object.text !== 'string') continue;278- const word = object.text.toUpperCase();279- if (word === 'IS' || word === 'AND' || semantic.has(word) || knownNouns.has(word)) continue;280- const context = formed.get(object.id);281- const otherProperty = allProperties.has(word) && !semantic.has(word);282- const sentence = context?.sentence || null;283- const message = context?.role === 'predicate'284- ? (otherProperty285- ? `${word} is declared as a property in another proof but has no behavior here; ${sentence} will transform ${context.subject} into ${word}.`286- : `${word} is not a declared property here; ${sentence} will transform ${context.subject} into ${word}.`)287- : context?.role === 'subject'288- ? (otherProperty289- ? `${word} is declared as a property in another proof but has no behavior here; ${sentence} uses it as a noun.`290- : `${word} is not a known noun in this proof; ${sentence} uses it as a noun.`)291- : otherProperty292- ? `${word} is declared as a property in another proof but has no behavior here.`293- : `${word} is not a known noun or declared property in this proof.`;294- addWarning({ word, sourceId: object.id, sentence, message, detail: context295- ? `${sourceLocation(object.id)}: ${word} is not an implemented property or known noun in this proof.`296- : `${sourceLocation(object.id)}: word ${word} is not a placed noun, automaton subject, core property, or property implemented in this proof.` });297- if (warnings.length >= MAX_MOD_WARNINGS) return warnings;298- }299- }300- return warnings;301-}302-303228 function normalizeMod(mod) {304229 const normalized = copy(mod);305230 normalized.manifest.engine = normalized.manifest.engine ?? MOD_SCHEMA_VERSION;@@ -0,0 +1,76 @@
1+import { parseRules } from '../engine/rules.js';2+import { CORE_PROPERTY_TOKENS } from '../engine/tokens.js';3+4+export const MAX_MOD_WARNINGS = 64;5+const CORE_PROPERTIES = new Set(CORE_PROPERTY_TOKENS);6+const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value);7+8+/** Return bounded author warnings without mutating or annotating pack data. */9+export function collectModWarnings(normalizedMod) {10+ if (!isRecord(normalizedMod)) return [];11+ const warnings = [];12+ const levels = Array.isArray(normalizedMod.levels) ? normalizedMod.levels : [];13+ const allProperties = new Set(CORE_PROPERTIES);14+ for (const level of levels) for (const automaton of Array.isArray(level?.automata) ? level.automata : []) {15+ if (typeof automaton?.property === 'string') allProperties.add(automaton.property.toUpperCase());16+ }17+ for (const [levelIndex, level] of levels.entries()) {18+ const objects = (Array.isArray(level?.objects) ? level.objects : []).map((object, index) => ({ ...object, id: object.id || `o${index}` }));19+ const semantic = new Set(CORE_PROPERTIES);20+ for (const automaton of Array.isArray(level?.automata) ? level.automata : []) {21+ if (typeof automaton?.property === 'string') semantic.add(automaton.property.toUpperCase());22+ }23+ const parsed = parseRules(objects, semantic);24+ const words = new Map(objects.filter(object => object?.kind === 'word').map(object => [object.id, object]));25+ const objectIndexes = new Map(objects.map((object, index) => [object.id, index]));26+ const knownNouns = new Set(objects.filter(object => object?.kind !== 'word' && typeof object?.noun === 'string').map(object => object.noun.toUpperCase()));27+ for (const automaton of Array.isArray(level?.automata) ? level.automata : []) {28+ if (typeof automaton?.subject === 'string') knownNouns.add(automaton.subject.toUpperCase());29+ }30+ const levelId = typeof level?.id === 'string' ? level.id : undefined;31+ const levelTitle = typeof level?.title === 'string' && level.title ? level.title : `Proof ${levelIndex + 1}`;32+ const seenWords = new Set();33+ const sourceLocation = sourceId => `levels[${levelIndex}].objects[${objectIndexes.get(sourceId)}].text`;34+ const formed = new Map();35+ for (const rule of parsed.rules) {36+ const ids = rule.sourceIds || [];37+ const isIndex = ids.findIndex(id => words.get(id)?.text === 'IS');38+ const predicateId = ids.slice(isIndex + 1).find(id => words.get(id)?.text === rule.predicate);39+ if (predicateId && !formed.has(predicateId)) formed.set(predicateId, { sentence: rule.text, role: 'predicate', subject: rule.subject });40+ for (const id of ids.slice(0, isIndex < 0 ? 0 : isIndex)) {41+ const word = words.get(id)?.text;42+ if (word && word !== 'AND' && !formed.has(id)) formed.set(id, { sentence: rule.text, role: 'subject' });43+ }44+ }45+ const addWarning = ({ word, sourceId, sentence = null, message, detail }) => {46+ const upper = String(word).toUpperCase();47+ if (seenWords.has(upper)) return;48+ seenWords.add(upper);49+ warnings.push({ word: upper, ...(levelId ? { levelId } : {}), levelTitle, sentence, message, detail: detail || sourceLocation(sourceId) });50+ };51+ for (const object of objects) {52+ if (object.kind !== 'word' || typeof object.text !== 'string') continue;53+ const word = object.text.toUpperCase();54+ if (word === 'IS' || word === 'AND' || semantic.has(word) || knownNouns.has(word)) continue;55+ const context = formed.get(object.id);56+ const otherProperty = allProperties.has(word) && !semantic.has(word);57+ const sentence = context?.sentence || null;58+ const message = context?.role === 'predicate'59+ ? (otherProperty60+ ? `${word} is declared as a property in another proof but has no behavior here; ${sentence} will transform ${context.subject} into ${word}.`61+ : `${word} is not a declared property here; ${sentence} will transform ${context.subject} into ${word}.`)62+ : context?.role === 'subject'63+ ? (otherProperty64+ ? `${word} is declared as a property in another proof but has no behavior here; ${sentence} uses it as a noun.`65+ : `${word} is not a known noun in this proof; ${sentence} uses it as a noun.`)66+ : otherProperty67+ ? `${word} is declared as a property in another proof but has no behavior here.`68+ : `${word} is not a known noun or declared property in this proof.`;69+ addWarning({ word, sourceId: object.id, sentence, message, detail: context70+ ? `${sourceLocation(object.id)}: ${word} is not an implemented property or known noun in this proof.`71+ : `${sourceLocation(object.id)}: word ${word} is not a placed noun, automaton subject, core property, or property implemented in this proof.` });72+ if (warnings.length >= MAX_MOD_WARNINGS) return warnings;73+ }74+ }75+ return warnings;76+}