- commit
- 46272da00ecf1e5249732f3d64b21ebaf2c11f3c
- parent
- a7f17259823610ef61c60bea0526344d79202dc0
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 79 | +++++++++++++++++++----------- |
| M | tests/ | 28 | +++++++++- |
@@ -233,19 +233,13 @@ export function validateMod(mod) {
233233 */234234 export function collectModWarnings(normalizedMod) {235235 if (!isRecord(normalizedMod)) return [];236- const knownNouns = new Set();237- for (const noun of Array.isArray(normalizedMod.vocabulary) ? normalizedMod.vocabulary : []) {238- if (typeof noun === 'string') knownNouns.add(noun.toUpperCase());239- }240- for (const noun of Object.keys(isRecord(normalizedMod.palette) ? normalizedMod.palette : {})) knownNouns.add(noun.toUpperCase());241236 const warnings = [];242- const seen = new Set();243- for (const level of Array.isArray(normalizedMod.levels) ? normalizedMod.levels : []) {244- for (const object of Array.isArray(level?.objects) ? level.objects : []) {245- if (object?.kind !== 'word' && typeof object?.noun === 'string') knownNouns.add(object.noun.toUpperCase());246- }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());247241 }248- for (const [levelIndex, level] of (Array.isArray(normalizedMod.levels) ? normalizedMod.levels : []).entries()) {242+ for (const [levelIndex, level] of levels.entries()) {249243 const objects = (Array.isArray(level?.objects) ? level.objects : []).map((object, index) => ({ ...object, id: object.id || `o${index}` }));250244 const semantic = new Set(CORE_PROPERTIES);251245 for (const automaton of Array.isArray(level?.automata) ? level.automata : []) {@@ -254,25 +248,54 @@ export function collectModWarnings(normalizedMod) {
254248 const parsed = parseRules(objects, semantic);255249 const words = new Map(objects.filter(object => object?.kind === 'word').map(object => [object.id, object]));256250 const objectIndexes = new Map(objects.map((object, index) => [object.id, index]));257- for (const sentence of parsed.sentences) {258- const connector = sentence.ids.findIndex(id => words.get(id)?.text === 'IS');259- for (const sourceId of sentence.ids.slice(connector + 1)) {260- const predicate = words.get(sourceId)?.text;261- if (!predicate || predicate === 'AND' || semantic.has(predicate) || knownNouns.has(predicate)) continue;262- const key = `${level?.id || levelIndex}:${sourceId}`;263- if (seen.has(key)) continue;264- seen.add(key);265- const levelId = typeof level?.id === 'string' ? level.id : undefined;266- const location = `levels[${levelIndex}].objects[${objectIndexes.get(sourceId)}].text`;267- const proof = `Proof ${levelIndex + 1}${level?.title ? ` (${level.title})` : ''}`;268- warnings.push({269- message: `${proof}: ${predicate} is not a declared property or known noun in this pack.`,270- detail: `${location}: predicate ${predicate} is not a core property, declared property, or pack noun.`,271- ...(levelId ? { levelId } : {}),272- });273- if (warnings.length >= MAX_MOD_WARNINGS) return warnings;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' });274268 }275269 }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+ }276299 }277300 return warnings;278301 }@@ -83,6 +83,8 @@ test('warns on an unknown authored predicate while allowing known noun transform
8383 { kind: 'word', text: 'DOOR', x: 0, y: 0 }, { kind: 'word', text: 'IS', x: 1, y: 0 }, { kind: 'word', text: 'SHUT', x: 2, y: 0 },8484 { kind: 'word', text: 'ROCK', x: 4, y: 0 }, { kind: 'word', text: 'IS', x: 5, y: 0 }, { kind: 'word', text: 'MIAU', x: 6, y: 0 },8585 { kind: 'object', noun: 'DOOR', x: 0, y: 1 },86+ { kind: 'object', noun: 'MIAU', x: 1, y: 1 },87+ { kind: 'object', noun: 'ROCK', x: 2, y: 1 },8688 ] }],8789 };`);8890 const warnings = collectModWarnings(mod);@@ -98,6 +100,7 @@ test('warning paths identify each unknown predicate in an AND sentence', () => {
98100 levels: [{ id: 'intro', title: 'An open question', width: 5, height: 1, objects: [99101 { text: 'DOOR', x: 0, y: 0 }, { text: 'IS', x: 1, y: 0 },100102 { text: 'SHUT', x: 2, y: 0 }, { text: 'AND', x: 3, y: 0 }, { text: 'LOCK', x: 4, y: 0 },103+ { kind: 'object', noun: 'DOOR', x: 0, y: 0 },101104 ] }],102105 };`);103106 assert.deepEqual(collectModWarnings(mod).map(warning => warning.detail.split(':')[0]), [@@ -111,11 +114,30 @@ test('duplicate physical sentences report each unknown predicate word once', ()
111114 levels: [{ id: 'intro', width: 3, height: 2, objects: [112115 { text: 'DOOR', x: 0, y: 0 }, { text: 'IS', x: 1, y: 0 }, { text: 'SHUT', x: 2, y: 0 },113116 { text: 'DOOR', x: 0, y: 1 }, { text: 'IS', x: 1, y: 1 }, { text: 'SHUT', x: 2, y: 1 },117+ { kind: 'object', noun: 'DOOR', x: 1, y: 1 },114118 ] }],115119 };`);116- assert.deepEqual(collectModWarnings(mod).map(warning => warning.detail.split(':')[0]), [117- 'levels[0].objects[2].text', 'levels[0].objects[5].text',118- ]);120+ const warnings = collectModWarnings(mod);121+ assert.equal(warnings.length, 1);122+ assert.equal(warnings[0].word, 'SHUT');123+});124+125+test('warns on a declared property word left loose or unsupported in another proof', () => {126+ const mod = loadMod(`module.exports = {127+ manifest: { id: 'level-local-properties', name: 'Level Local Properties', version: '1.0.0' },128+ levels: [129+ { id: 'clock', width: 3, height: 1, objects: [130+ { kind: 'word', text: 'MOUSE', x: 0, y: 0 }, { kind: 'word', text: 'IS', x: 1, y: 0 }, { kind: 'word', text: 'TICK', x: 2, y: 0 },131+ { kind: 'object', noun: 'MOUSE', x: 0, y: 0 },132+ ], automata: [{ id: 'clock', property: 'TICK', pattern: [{ action: 'wait' }] }] },133+ { id: 'plain', width: 1, height: 1, objects: [{ kind: 'word', text: 'TICK', x: 0, y: 0 }] },134+ ],135+ };`);136+ const warnings = collectModWarnings(mod).filter(warning => warning.levelId === 'plain');137+ assert.equal(warnings.length, 1);138+ assert.equal(warnings[0].word, 'TICK');139+ assert.equal(warnings[0].sentence, null);140+ assert.match(warnings[0].message, /another proof/);119141 });120142 121143 test('declared automaton properties cannot also be object nouns', () => {