Initial module scaffold

This commit is contained in:
2026-04-20 20:07:44 +00:00
commit c3e590e782
11 changed files with 1474 additions and 0 deletions

60
scripts/core/analyzer.js Normal file
View File

@@ -0,0 +1,60 @@
import { buildFindings, createFileRecord } from "./finding-engine.js";
import { extractReferencesFromValue } from "./reference-extractor.js";
import { createFileLocator } from "./path-utils.js";
export async function analyzeStorage({ listFiles, listSources, onProgress }={}) {
const files = [];
let fileCount = 0;
let sourceCount = 0;
let referenceCount = 0;
onProgress?.({ phase: "files", label: "Scanne Dateien", files: 0, sources: 0, references: 0 });
for await (const entry of listFiles()) {
files.push(createFileRecord(createFileLocator(entry.storage, entry.path), entry.size ?? null));
fileCount += 1;
if ((fileCount % 100) === 0) {
onProgress?.({ phase: "files", label: "Scanne Dateien", files: fileCount, sources: sourceCount, references: referenceCount });
await yieldToUI();
}
}
const references = [];
onProgress?.({ phase: "sources", label: "Lese Referenzen", files: fileCount, sources: 0, references: 0 });
for await (const source of listSources()) {
sourceCount += 1;
const extracted = extractReferencesFromValue(source.value, {
sourceType: source.sourceType,
sourceScope: source.sourceScope,
sourceLabel: source.sourceLabel
});
references.push(...extracted);
referenceCount += extracted.length;
if ((sourceCount % 50) === 0) {
onProgress?.({
phase: "sources",
label: "Lese Referenzen",
files: fileCount,
sources: sourceCount,
references: referenceCount,
currentSource: source.sourceLabel
});
await yieldToUI();
}
}
onProgress?.({ phase: "findings", label: "Klassifiziere Findings", files: fileCount, sources: sourceCount, references: referenceCount });
const findings = buildFindings({ files, references });
onProgress?.({
phase: "done",
label: "Analyse abgeschlossen",
files: fileCount,
sources: sourceCount,
references: referenceCount,
findings: findings.length
});
return { files, references, findings };
}
async function yieldToUI() {
await new Promise(resolve => setTimeout(resolve, 0));
}

View File

@@ -0,0 +1,154 @@
import { classifyRisk, detectMediaKind, inferOwnerHint, isCorePublicPath, isStorageAreaPath } from "./path-utils.js";
function compareOwner(sourceScope, targetOwner) {
if (!sourceScope) return "unknown";
if ((sourceScope.ownerType === "world") && (targetOwner.ownerType === "system") && (sourceScope.systemId === targetOwner.ownerId)) {
return "same-system";
}
if ((sourceScope.ownerType === "world") && (targetOwner.ownerType === "public") &&
["cards", "icons", "nue", "sounds", "ui"].includes(targetOwner.ownerId)) {
return "core-public";
}
if ((sourceScope.ownerType === "module") && (targetOwner.ownerType === "module")) {
return sourceScope.ownerId === targetOwner.ownerId ? "same-package" : "foreign-module";
}
if ((sourceScope.ownerType === "system") && (targetOwner.ownerType === "system")) {
return sourceScope.ownerId === targetOwner.ownerId ? "same-package" : "cross-package";
}
if ((sourceScope.ownerType === "world") && (targetOwner.ownerType === "world")) {
return sourceScope.ownerId === targetOwner.ownerId ? "same-world" : "cross-world";
}
if ((sourceScope.ownerType === "world") && ((targetOwner.ownerType === "module") || (targetOwner.ownerType === "system"))) {
return "non-package-to-package";
}
if ((sourceScope.ownerType === "world") && (targetOwner.ownerType === "public")) {
return "risky-public";
}
if (((sourceScope.ownerType === "module") || (sourceScope.ownerType === "system")) &&
((targetOwner.ownerType === "module") || (targetOwner.ownerType === "system")) &&
((sourceScope.ownerType !== targetOwner.ownerType) || (sourceScope.ownerId !== targetOwner.ownerId))) {
return "cross-package";
}
return "allowed";
}
export function buildFindings({ files, references }) {
const fileByLocator = new Map(files.map(file => [file.locator, file]));
const refsByLocator = new Map();
const findings = [];
for (const reference of references) {
const normalized = reference.normalized;
if (!normalized) continue;
const bucket = refsByLocator.get(normalized.locator) ?? [];
bucket.push(reference);
refsByLocator.set(normalized.locator, bucket);
}
for (const reference of references) {
const normalized = reference.normalized;
if (!normalized) continue;
const file = fileByLocator.get(normalized.locator);
if (!file) {
if (reference.sourceScope?.ownerType !== "world") continue;
findings.push({
kind: "broken-reference",
severity: reference.sourceScope?.ownerType === "world" ? "high" : "warning",
target: normalized,
source: reference,
reason: `Referenced file ${normalized.locator} does not exist in the scanned roots.`,
recommendation: "Check whether the file was moved, deleted, or should be copied into a stable location.",
confidence: "high"
});
continue;
}
const ownerRelation = compareOwner(reference.sourceScope, file.ownerHint);
if (ownerRelation === "non-package-to-package") {
if (isAnchoredInOwningPackage(file, refsByLocator.get(normalized.locator) ?? [])) continue;
findings.push({
kind: "non-package-to-package-reference",
severity: "high",
target: normalized,
source: reference,
reason: `${reference.sourceScope.ownerType}:${reference.sourceScope.ownerId} references package-owned storage ${normalized.locator}, but the asset is not visibly referenced by its owning package.`,
recommendation: "Review whether the file was manually placed into the package folder. If the package does not use it itself, prefer moving it into user-controlled storage.",
confidence: "high"
});
} else if (ownerRelation === "risky-public") {
if (reference.sourceScope?.ownerType !== "world") continue;
findings.push({
kind: "risky-public-reference",
severity: "high",
target: normalized,
source: reference,
reason: `${reference.sourceScope.ownerType}:${reference.sourceScope.ownerId} references release-public storage ${normalized.locator}.`,
recommendation: "Prefer copying the asset into world or user-controlled storage.",
confidence: "high"
});
}
}
for (const file of files) {
if (detectMediaKind(file.path) === "other") continue;
const refs = refsByLocator.get(file.locator) ?? [];
if (refs.length) continue;
if (!shouldReportOrphan(file, references)) continue;
const severity = (file.riskClass === "package-module") || (file.riskClass === "package-system") || (file.riskClass === "release-public")
? "warning"
: "info";
findings.push({
kind: "orphan-file",
severity,
target: file,
source: null,
reason: `No incoming media reference was found for ${file.locator}.`,
recommendation: severity === "warning"
? "Review whether the file is safe to remove or should be moved into a stable storage location."
: "Review whether the file is intentionally kept as reserve content.",
confidence: "medium"
});
}
return findings;
}
function isAnchoredInOwningPackage(file, references) {
const ownerType = file.ownerHint.ownerType;
const ownerId = file.ownerHint.ownerId;
if (!["module", "system"].includes(ownerType) || !ownerId) return false;
return references.some(reference =>
(reference.sourceScope?.ownerType === ownerType) &&
(reference.sourceScope?.ownerId === ownerId)
);
}
function shouldReportOrphan(file, references) {
if (file.riskClass === "stable-world") {
const worldId = file.ownerHint.ownerId;
return references.some(reference => reference.sourceScope?.ownerType === "world" && reference.sourceScope.ownerId === worldId);
}
if ((file.riskClass === "package-module") || (file.riskClass === "package-system")) {
return isStorageAreaPath(file);
}
if (file.riskClass === "release-public") {
return !isCorePublicPath(file);
}
return false;
}
export function createFileRecord(locator, size = null) {
return {
...locator,
basename: locator.path.split("/").pop() ?? locator.path,
extension: locator.path.includes(".") ? locator.path.split(".").pop().toLowerCase() : "",
size,
mediaKind: detectMediaKind(locator.path),
ownerHint: inferOwnerHint(locator),
riskClass: classifyRisk(locator),
exists: true
};
}

120
scripts/core/path-utils.js Normal file
View File

@@ -0,0 +1,120 @@
export const RELEASE_PREFIXES = new Set(["canvas", "cards", "docs", "fonts", "icons", "lang", "nue", "scripts", "sounds", "toolclips", "tours", "ui"]);
const DATA_PREFIXES = new Set(["assets", "modules", "systems", "worlds"]);
const MEDIA_EXTENSIONS = {
image: new Set(["avif", "gif", "jpeg", "jpg", "png", "svg", "webp"]),
video: new Set(["m4v", "mkv", "mov", "mp4", "mpeg", "mpg", "webm"]),
audio: new Set(["flac", "m4a", "mid", "midi", "mp3", "ogg", "opus", "wav", "webm"])
};
export function normalizePath(path) {
const normalized = String(path ?? "")
.replace(/\\/g, "/")
.replace(/^[./]+/, "")
.replace(/^\/+/, "")
.replace(/\/{2,}/g, "/")
.trim();
try {
return decodeURI(normalized);
} catch {
return normalized;
}
}
export function splitResourceSuffix(path) {
const [withoutHash] = String(path ?? "").split("#", 1);
const [cleanPath] = withoutHash.split("?", 1);
return cleanPath;
}
export function parseStoragePath(rawValue) {
if (typeof rawValue !== "string") return null;
const trimmed = rawValue.trim();
if (!trimmed) return null;
if (/^(?:https?:|data:|blob:)/i.test(trimmed)) return null;
const clean = normalizePath(splitResourceSuffix(trimmed));
if (!clean) return null;
const first = clean.split("/", 1)[0];
let storage = null;
if (DATA_PREFIXES.has(first)) storage = "data";
else if (RELEASE_PREFIXES.has(first)) storage = "public";
if (!storage) return null;
return {
storage,
path: clean,
locator: `${storage}:${clean}`
};
}
export function getExtension(path) {
const clean = normalizePath(path);
const index = clean.lastIndexOf(".");
return index === -1 ? "" : clean.slice(index + 1).toLowerCase();
}
export function detectMediaKind(path) {
const ext = getExtension(path);
for (const [kind, extensions] of Object.entries(MEDIA_EXTENSIONS)) {
if (extensions.has(ext)) return kind;
}
return "other";
}
export function isMediaPath(path) {
return detectMediaKind(path) !== "other";
}
export function classifyRisk(locator) {
const parts = locator.path.split("/");
if (locator.storage === "public") return "release-public";
if (parts[0] === "modules") return "package-module";
if (parts[0] === "systems") return "package-system";
if (parts[0] === "assets") return "stable-user";
if (parts[0] === "worlds") return "stable-world";
return locator.storage === "data" ? "stable-user" : "unknown";
}
export function inferOwnerHint(locator) {
const parts = locator.path.split("/");
if (locator.storage === "public") {
return { ownerType: "public", ownerId: parts[0] ?? null };
}
if (parts[0] === "assets") {
return { ownerType: "user", ownerId: null };
}
if ((parts[0] === "worlds") && parts[1]) {
return { ownerType: "world", ownerId: parts[1] };
}
if ((parts[0] === "modules") && parts[1]) {
return { ownerType: "module", ownerId: parts[1] };
}
if ((parts[0] === "systems") && parts[1]) {
return { ownerType: "system", ownerId: parts[1] };
}
if (locator.storage === "data") {
return { ownerType: "user", ownerId: parts[0] ?? null };
}
return { ownerType: "unknown", ownerId: null };
}
export function isStorageAreaPath(locator) {
const parts = locator.path.split("/");
return ((parts[0] === "modules") || (parts[0] === "systems")) && (parts[2] === "storage");
}
export function isCorePublicPath(locator) {
if (locator.storage !== "public") return false;
const parts = locator.path.split("/");
return RELEASE_PREFIXES.has(parts[0] ?? "");
}
export function createFileLocator(storage, path) {
const cleanPath = normalizePath(path);
return {
storage,
path: cleanPath,
locator: `${storage}:${cleanPath}`
};
}

View File

@@ -0,0 +1,102 @@
import { createFileLocator, isMediaPath, normalizePath, parseStoragePath } from "./path-utils.js";
const ATTRIBUTE_PATTERNS = [
/\b(?:src|href|poster)\s*=\s*["']([^"'<>]+)["']/gi,
/url\(\s*["']?([^"')]+)["']?\s*\)/gi
];
export function collectStringCandidates(value, visit) {
if (typeof value === "string") {
visit(value);
return;
}
if (Array.isArray(value)) {
for (const entry of value) collectStringCandidates(entry, visit);
return;
}
if ((value !== null) && (typeof value === "object")) {
for (const entry of Object.values(value)) collectStringCandidates(entry, visit);
}
}
export function extractReferencesFromValue(value, source) {
const references = [];
collectStringCandidates(value, candidate => {
const direct = resolveReference(candidate, source);
if (direct && isMediaPath(direct.path)) {
references.push({
sourceType: source.sourceType,
sourceScope: source.sourceScope,
sourceLabel: source.sourceLabel,
rawValue: candidate,
normalized: {
...direct,
targetKind: "local-file"
}
});
}
for (const pattern of ATTRIBUTE_PATTERNS) {
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(candidate))) {
const nested = resolveReference(match[1], source);
if (!nested || !isMediaPath(nested.path)) continue;
references.push({
sourceType: source.sourceType,
sourceScope: source.sourceScope,
sourceLabel: source.sourceLabel,
rawValue: match[1],
normalized: {
...nested,
targetKind: "local-file"
}
});
}
}
});
return dedupeReferences(references);
}
function resolveReference(rawValue, source) {
const direct = parseStoragePath(rawValue);
if (direct) return direct;
return resolvePackageRelativeReference(rawValue, source);
}
function resolvePackageRelativeReference(rawValue, source) {
if (typeof rawValue !== "string") return null;
if (/^(?:https?:|data:|blob:)/i.test(rawValue.trim())) return null;
const path = normalizePath(rawValue);
if (!path || !path.includes("/")) return null;
const ownerType = source.sourceScope?.ownerType;
const ownerId = source.sourceScope?.ownerId;
if (!ownerType || !ownerId) return null;
if (ownerType === "module") {
return createFileLocator("data", `modules/${ownerId}/${path}`);
}
if (ownerType === "system") {
return createFileLocator("data", `systems/${ownerId}/${path}`);
}
if (ownerType === "world") {
return createFileLocator("data", path);
}
return null;
}
function dedupeReferences(references) {
const seen = new Set();
return references.filter(reference => {
const key = [
reference.sourceType,
reference.sourceLabel,
reference.normalized?.locator ?? "",
reference.rawValue
].join("|");
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}