hyapp-h5/scripts/check-activity-room-reward-fami-layer-assets.mjs
2026-07-17 11:54:43 +08:00

130 lines
4.0 KiB
JavaScript

import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..'
);
const pageFile = path.join(repoRoot, 'activity/room-reward/fami.html');
const cssFile = path.join(repoRoot, 'activity/room-reward/fami/fami.css');
const scriptFile = path.join(repoRoot, 'activity/room-reward/fami/fami.js');
const assetRoot = path.join(
repoRoot,
'activity/room-reward/fami/assets/layers'
);
const manifestFile = path.join(
repoRoot,
'activity/room-reward/fami/assets/manifest.json'
);
function fail(message) {
throw new Error(`[fami-room-reward-layer-audit] ${message}`);
}
function sha256(file) {
return crypto
.createHash('sha256')
.update(fs.readFileSync(file))
.digest('hex');
}
for (const file of [pageFile, cssFile, scriptFile, manifestFile]) {
if (!fs.existsSync(file))
fail(`missing required file: ${path.relative(repoRoot, file)}`);
}
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
const assets = Array.isArray(manifest.assets) ? manifest.assets : [];
if (!assets.length) fail('manifest has no independently traced assets');
const forbiddenNodes = new Set(manifest.policy?.forbiddenNodes || []);
const manifestFiles = new Set();
for (const asset of assets) {
if (
!asset.file ||
!asset.sha256 ||
!Array.isArray(asset.nodes) ||
!asset.nodes.length
) {
fail(`incomplete manifest entry: ${JSON.stringify(asset)}`);
}
if (asset.nodes.some((node) => forbiddenNodes.has(node))) {
fail(
`aggregate/root Figma node exported as runtime asset: ${asset.file}`
);
}
if (!String(asset.kind || '').startsWith('single-')) {
fail(`asset is not classified as a single visual layer: ${asset.file}`);
}
const file = path.join(assetRoot, asset.file);
if (!fs.existsSync(file)) fail(`manifest asset missing: ${asset.file}`);
if (sha256(file) !== asset.sha256) fail(`hash mismatch: ${asset.file}`);
if (asset.pixelSize?.[0] === 1080 && asset.pixelSize?.[1] === 1985) {
fail(`whole-page sized asset is forbidden: ${asset.file}`);
}
manifestFiles.add(asset.file);
}
for (const file of fs.readdirSync(assetRoot)) {
if (!manifestFiles.has(file))
fail(`untracked runtime layer asset: ${file}`);
}
const html = fs.readFileSync(pageFile, 'utf8');
const css = fs.readFileSync(cssFile, 'utf8');
const js = fs.readFileSync(scriptFile, 'utf8');
const runtimeSource = `${html}\n${css}\n${js}`;
for (const forbidden of [
'figma-default.png',
'/.audit/',
'822:2775',
'822:2783',
]) {
if (runtimeSource.includes(forbidden))
fail(`forbidden aggregate reference: ${forbidden}`);
}
const referencedAssets = new Set();
for (const match of runtimeSource.matchAll(
/(?:fami\/)?assets\/layers\/([\w.-]+)/g
)) {
referencedAssets.add(match[1]);
}
for (const file of referencedAssets) {
if (!manifestFiles.has(file))
fail(`runtime asset is not in manifest: ${file}`);
}
for (const requiredDOM of [
'id="rrTargetValue"',
'id="rrTierTable"',
'id="rrUserName"',
'id="rrContributionValue"',
'id="rrRewardValue"',
]) {
if (!html.includes(requiredDOM))
fail(`dynamic DOM layer missing: ${requiredDOM}`);
}
if (html.includes('<button')) {
fail(
'automatic-settlement values must not be implemented as claim buttons'
);
}
if (!js.includes("'/api/v1/activities/room-turnover-reward'")) {
fail('real room reward API is not connected');
}
if (!js.includes("params.get('mock') === '1'")) {
fail('Figma fixture must be gated behind explicit ?mock=1');
}
if (/catch[\s\S]{0,500}mockData/.test(js)) {
fail('API errors must not silently fall back to Figma business data');
}
console.log(
`[fami-room-reward-layer-audit] passed: ${assets.length} traced single-layer assets, DOM/API business content verified`
);