Files
fool-cardgame/scripts/check-core-purity.mjs
T
ftcandClaude Opus 5 51117a698c M3: модуль сохранения
По ARCHITECTURE.md: реестр независимых секций, JSON с ключом на секцию.
Ключи — явные строковые константы, а не имена классов: релизная сборка
минифицирует имена и молча потеряла бы все сохранения.

- ISaveSection, SaveManager, IStorage + MemoryStorage
- CocosStorage: единственный файл модуля, знающий про движок;
  при отказе хранилища уходит в память, а не роняет игру
- StatsSection: сыграно/побед/поражений/ничьих, текущая и лучшая серия
- терпимая загрузка: битый JSON, чужая версия, сломанная секция
  или отвалившееся хранилище не ломают остальное
- проверка чистоты расширена на modules/ со списком адаптеров

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 10:45:52 +03:00

60 lines
1.9 KiB
JavaScript

// Guards the one architectural rule the type system cannot express: the game
// logic must not depend on the engine. See docs/TECHNICAL.md.
//
// core/ never imports `cc`, at all.
// modules/ is engine-aware, but only through the adapters listed below —
// everything else in it stays testable headlessly.
//
// A new file that needs `cc` under modules/ is a deliberate decision, so it has
// to be added here on purpose rather than slipping in unnoticed.
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative, sep } from 'node:path';
const PURE_ROOT = 'assets/scripts/core';
const MODULES_ROOT = 'assets/scripts/modules';
const ENGINE_ADAPTERS = new Set([
'save/CocosStorage.ts',
]);
const ENGINE_IMPORT = /\bfrom\s+['"](cc|cc\/env)['"]/;
const offenders = [];
function walk(dir, onFile) {
for (const entry of readdirSync(dir)) {
const path = join(dir, entry);
if (statSync(path).isDirectory()) walk(path, onFile);
else if (path.endsWith('.ts')) onFile(path);
}
}
function engineImports(path) {
return readFileSync(path, 'utf8')
.split('\n')
.map((line, i) => ({ line: line.trim(), number: i + 1 }))
.filter((entry) => ENGINE_IMPORT.test(entry.line));
}
walk(PURE_ROOT, (path) => {
for (const hit of engineImports(path)) {
offenders.push(`${path}:${hit.number} ${hit.line} (core/ must never import the engine)`);
}
});
walk(MODULES_ROOT, (path) => {
const key = relative(MODULES_ROOT, path).split(sep).join('/');
if (ENGINE_ADAPTERS.has(key)) return;
for (const hit of engineImports(path)) {
offenders.push(
`${path}:${hit.number} ${hit.line} (add '${key}' to ENGINE_ADAPTERS if this is intended)`,
);
}
});
if (offenders.length > 0) {
console.error(`Engine dependency in code that must stay pure:\n${offenders.join('\n')}`);
process.exit(1);
}
console.log('core purity OK');