import { classifyRisk, createCanonicalLocator, 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, i18n }={}) { const fileByLocator = new Map(files.map(file => [createCanonicalLocator(file.storage, file.path), file])); const fileLocators = files.map(file => ({ storage: file.storage, path: file.path, locator: createCanonicalLocator(file.storage, file.path) })); const resolvedReferences = references.map(reference => resolveReferenceTarget(reference, fileByLocator, fileLocators)); const refsByLocator = new Map(); const wildcardReferences = []; const findings = []; for (const reference of resolvedReferences) { const normalized = reference.normalized; if (!normalized) continue; if (normalized.targetKind === "wildcard") { wildcardReferences.push(reference); continue; } const normalizedLocator = createCanonicalLocator(normalized.storage, normalized.path); const bucket = refsByLocator.get(normalizedLocator) ?? []; bucket.push(reference); refsByLocator.set(normalizedLocator, bucket); } for (const reference of resolvedReferences) { const normalized = reference.normalized; if (!normalized) continue; const normalizedLocator = createCanonicalLocator(normalized.storage, normalized.path); const file = fileByLocator.get(normalizedLocator); if (!file) { if ((normalized.targetKind === "wildcard") && wildcardMatchesAny(normalized, fileLocators)) continue; if (reference.sourceScope?.ownerType !== "world") continue; findings.push({ kind: "broken-reference", severity: reference.sourceScope?.ownerType === "world" ? "high" : "warning", target: { ...normalized, locator: normalizedLocator }, source: reference, reason: normalized.targetKind === "wildcard" ? format(i18n, "KSA.FindingReason.BrokenWildcard", { locator: normalized.locator }) : format(i18n, "KSA.FindingReason.BrokenReference", { locator: normalized.locator }), recommendation: normalized.targetKind === "wildcard" ? format(i18n, "KSA.FindingRecommendation.CheckWildcard") : format(i18n, "KSA.FindingRecommendation.CheckMissingFile"), confidence: "high" }); continue; } const ownerRelation = compareOwner(reference.sourceScope, file.ownerHint); if (ownerRelation === "non-package-to-package") { if (isAnchoredInOwningPackage(file, matchingReferencesForFile(file, refsByLocator, wildcardReferences))) continue; findings.push({ kind: "non-package-to-package-reference", severity: "high", target: { ...normalized, locator: normalizedLocator }, source: reference, reason: format(i18n, "KSA.FindingReason.UnanchoredPackageTarget", { sourceOwner: `${reference.sourceScope.ownerType}:${reference.sourceScope.ownerId}`, locator: normalized.locator }), recommendation: format(i18n, "KSA.FindingRecommendation.MoveToStableStorage"), confidence: "high" }); } else if (ownerRelation === "risky-public") { if (reference.sourceScope?.ownerType !== "world") continue; findings.push({ kind: "risky-public-reference", severity: "high", target: { ...normalized, locator: normalizedLocator }, source: reference, reason: format(i18n, "KSA.FindingReason.RiskyPublicTarget", { sourceOwner: `${reference.sourceScope.ownerType}:${reference.sourceScope.ownerId}`, locator: normalized.locator }), recommendation: format(i18n, "KSA.FindingRecommendation.CopyToStableStorage"), confidence: "high" }); } } for (const file of files) { if (detectMediaKind(file.path) === "other") continue; const refs = matchingReferencesForFile(file, refsByLocator, wildcardReferences); if (refs.length) continue; if (!shouldReportOrphan(file, resolvedReferences)) 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: format(i18n, "KSA.FindingReason.OrphanFile", { locator: file.locator }), recommendation: severity === "warning" ? format(i18n, "KSA.FindingRecommendation.ReviewOrMoveOrphan") : format(i18n, "KSA.FindingRecommendation.KeepAsReserve"), confidence: "medium" }); } return findings; } function resolveReferenceTarget(reference, fileByLocator, fileLocators) { const normalized = reference.normalized; if (!normalized) return reference; const currentLocator = createCanonicalLocator(normalized.storage, normalized.path); if (fileByLocator.has(currentLocator)) return reference; const alternateStorage = normalized.storage === "data" ? "public" : normalized.storage === "public" ? "data" : null; if (!alternateStorage) return reference; const alternateLocator = createCanonicalLocator(alternateStorage, normalized.path); if (normalized.targetKind === "wildcard") { const alternateTarget = { ...normalized, storage: alternateStorage, locator: alternateLocator }; if (!wildcardMatchesAny(alternateTarget, fileLocators)) return reference; return { ...reference, normalized: alternateTarget }; } if (!fileByLocator.has(alternateLocator)) return reference; return { ...reference, normalized: { ...normalized, storage: alternateStorage, locator: alternateLocator } }; } 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 matchingReferencesForFile(file, refsByLocator, wildcardReferences) { const exactRefs = refsByLocator.get(createCanonicalLocator(file.storage, file.path)) ?? []; const matchingWildcardRefs = wildcardReferences.filter(reference => wildcardMatchesFile(reference.normalized, file)); return [...exactRefs, ...matchingWildcardRefs]; } function wildcardMatchesAny(target, fileLocators) { const matcher = wildcardToRegExp(target.path); return fileLocators.some(file => file.storage === target.storage && matcher.test(file.path)); } function wildcardMatchesFile(target, file) { if (!target || (target.targetKind !== "wildcard")) return false; if (target.storage !== file.storage) return false; return wildcardToRegExp(target.path).test(file.path); } function wildcardToRegExp(pattern) { const escaped = String(pattern) .replace(/[.+^${}()|\\]/g, "\\$&") .replace(/\*/g, ".*") .replace(/\?/g, "."); return new RegExp(`^${escaped}$`, "u"); } function shouldReportOrphan(file, references) { if (isDerivedSceneThumbnail(file)) return false; 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; } function isDerivedSceneThumbnail(file) { const path = String(file.path ?? ""); return /^worlds\/[^/]+\/assets\/scenes\/[^/]+-thumb\.(?:png|webp)$/u.test(path); } 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 }; } function format(i18n, key, data = {}) { return i18n?.format?.(key, data) ?? key; }