From 7d574f7c1be2729d63852bfdea5d9fe7d6ded043 Mon Sep 17 00:00:00 2001 From: Kosmos Date: Tue, 21 Apr 2026 14:00:37 +0000 Subject: [PATCH] Handle references into inactive modules --- lang/de.json | 4 +++ lang/en.json | 4 +++ module.json | 2 +- scripts/adapters/foundry-runtime.js | 13 +++++++- scripts/apps/audit-report-app.js | 14 +++++++++ scripts/core/analyzer.js | 8 ++--- scripts/core/finding-engine.js | 33 +++++++++++++++++-- styles/audit.css | 12 +++++++ tests/inactive-module-reference-test.mjs | 40 ++++++++++++++++++++++++ tests/scene-thumbnail-orphan-test.mjs | 2 +- tests/storage-root-resolution-test.mjs | 2 +- tests/wildcard-reference-test.mjs | 2 +- 12 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 tests/inactive-module-reference-test.mjs diff --git a/lang/de.json b/lang/de.json index 63b6000..7a02f8a 100644 --- a/lang/de.json +++ b/lang/de.json @@ -40,6 +40,10 @@ "Failed": "Kosmos Storage Audit fehlgeschlagen: {message}", "OpenSourceFailed": "Die Quelle konnte nicht geöffnet werden: {uuid}" }, + "Notice": { + "Title": "Hinweise", + "InactiveModuleReferences": "Die aktive Welt referenziert Dateien aus inaktiven Modulen. Diese Modulziele wurden nicht als unverankert gewertet: {modules}" + }, "Summary": { "NoAnalysis": "Noch keine Analyse ausgeführt.", "Files": "Dateien", diff --git a/lang/en.json b/lang/en.json index 83e5285..0f9566a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -40,6 +40,10 @@ "Failed": "Kosmos Storage Audit failed: {message}", "OpenSourceFailed": "Could not open source: {uuid}" }, + "Notice": { + "Title": "Notes", + "InactiveModuleReferences": "The active world references files from inactive modules. These module targets were not treated as unanchored: {modules}" + }, "Summary": { "NoAnalysis": "No analysis has been run yet.", "Files": "Files", diff --git a/module.json b/module.json index d455ada..c17420b 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "kosmos-storage-audit", "title": "Kosmos Storage Audit", "description": "Analyzes media references and risky storage locations across Foundry data and public roots.", - "version": "0.0.23", + "version": "0.0.24", "compatibility": { "minimum": "13", "verified": "13" diff --git a/scripts/adapters/foundry-runtime.js b/scripts/adapters/foundry-runtime.js index 79e0b3b..ebcadd1 100644 --- a/scripts/adapters/foundry-runtime.js +++ b/scripts/adapters/foundry-runtime.js @@ -122,7 +122,8 @@ export async function runRuntimeAnalysis({ onProgress }={}) { listFiles: () => listFoundryFiles(onProgress), listSources: () => listFoundrySources(onProgress), onProgress, - i18n: { format } + i18n: { format }, + packageActivity: collectPackageActivity() }); } @@ -221,3 +222,13 @@ function findEmbeddedIndex(path, segment) { const candidate = path[index + 1]; return Number.isInteger(candidate) ? candidate : null; } + +function collectPackageActivity() { + const modules = new Map(); + const moduleLabels = new Map(); + for (const module of game.modules.values()) { + modules.set(module.id, !!module.active); + moduleLabels.set(module.id, module.title ?? module.id); + } + return { modules, moduleLabels }; +} diff --git a/scripts/apps/audit-report-app.js b/scripts/apps/audit-report-app.js index 9e751e8..7415797 100644 --- a/scripts/apps/audit-report-app.js +++ b/scripts/apps/audit-report-app.js @@ -38,6 +38,7 @@ export class StorageAuditReportApp extends foundry.applications.api.ApplicationV hasAnalysis: !!this.#analysis, showAll: this.#showAll, progress: this.#progress, + notices: this.#analysis?.notices ?? [], summary: this.#summarize(this.#analysis), groupedFindings }; @@ -89,6 +90,7 @@ export class StorageAuditReportApp extends foundry.applications.api.ApplicationV ${renderProgress(context.progress, context.loading)} + ${renderNotices(context.notices, context.loading)} ${renderSummary(context.summary, context.loading)} ${renderGroupedFindingList(context.groupedFindings, context.hasAnalysis, context.loading, context.showAll)} `; @@ -141,6 +143,7 @@ export class StorageAuditReportApp extends foundry.applications.api.ApplicationV if (!this.#analysis) return; const payload = { exportedAt: new Date().toISOString(), + notices: this.#analysis.notices ?? [], summary: this.#summarize(this.#analysis), groupedFindings: groupFindings(this.#analysis.findings), findings: this.#analysis.findings.map(serializeFinding) @@ -232,6 +235,17 @@ function renderProgress(progress, loading) { `; } +function renderNotices(notices, loading) { + if (loading || !notices?.length) return ""; + const items = notices.map(notice => `
  • ${escapeHtml(notice.message ?? "")}
  • `).join(""); + return ` +
    +

    ${localize("KSA.Notice.Title")}

    + +
    + `; +} + function renderSummary(summary, loading) { if (loading) return ""; if (!summary) { diff --git a/scripts/core/analyzer.js b/scripts/core/analyzer.js index 180ba87..09c752f 100644 --- a/scripts/core/analyzer.js +++ b/scripts/core/analyzer.js @@ -2,7 +2,7 @@ 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, i18n }={}) { +export async function analyzeStorage({ listFiles, listSources, onProgress, i18n, packageActivity }={}) { const files = []; let fileCount = 0; let sourceCount = 0; @@ -46,16 +46,16 @@ export async function analyzeStorage({ listFiles, listSources, onProgress, i18n } onProgress?.({ phase: "findings", label: format(i18n, "KSA.Progress.ClassifyFindings"), files: fileCount, sources: sourceCount, references: referenceCount }); - const findings = buildFindings({ files, references, i18n }); + const result = buildFindings({ files, references, i18n, packageActivity }); onProgress?.({ phase: "done", label: format(i18n, "KSA.Progress.Completed"), files: fileCount, sources: sourceCount, references: referenceCount, - findings: findings.length + findings: result.findings.length }); - return { files, references, findings }; + return { files, references, findings: result.findings, notices: result.notices ?? [] }; } async function yieldToUI() { diff --git a/scripts/core/finding-engine.js b/scripts/core/finding-engine.js index a7fdae7..bff930c 100644 --- a/scripts/core/finding-engine.js +++ b/scripts/core/finding-engine.js @@ -32,7 +32,7 @@ function compareOwner(sourceScope, targetOwner) { return "allowed"; } -export function buildFindings({ files, references, i18n }={}) { +export function buildFindings({ files, references, i18n, packageActivity }={}) { const fileByLocator = new Map(files.map(file => [createCanonicalLocator(file.storage, file.path), file])); const fileLocators = files.map(file => ({ storage: file.storage, @@ -43,6 +43,7 @@ export function buildFindings({ files, references, i18n }={}) { const refsByLocator = new Map(); const wildcardReferences = []; const findings = []; + const inactiveModuleReferenceIds = new Set(); for (const reference of resolvedReferences) { const normalized = reference.normalized; @@ -84,6 +85,10 @@ export function buildFindings({ files, references, i18n }={}) { const ownerRelation = compareOwner(reference.sourceScope, file.ownerHint); if (ownerRelation === "non-package-to-package") { + if (isInactiveModuleTarget(file, packageActivity)) { + inactiveModuleReferenceIds.add(file.ownerHint.ownerId); + continue; + } if (isAnchoredInOwningPackage(file, matchingReferencesForFile(file, refsByLocator, wildcardReferences))) continue; findings.push({ kind: "non-package-to-package-reference", @@ -137,7 +142,10 @@ export function buildFindings({ files, references, i18n }={}) { }); } - return findings; + return { + findings, + notices: createNotices({ inactiveModuleReferenceIds, packageActivity, i18n }) + }; } function resolveReferenceTarget(reference, fileByLocator, fileLocators) { @@ -225,6 +233,27 @@ function shouldReportOrphan(file, references) { return false; } +function isInactiveModuleTarget(file, packageActivity) { + if (file.ownerHint?.ownerType !== "module") return false; + const active = packageActivity?.modules?.get?.(file.ownerHint.ownerId); + return active === false; +} + +function createNotices({ inactiveModuleReferenceIds, packageActivity, i18n }) { + const moduleIds = [...inactiveModuleReferenceIds].sort((a, b) => a.localeCompare(b)); + if (!moduleIds.length) return []; + const moduleLabels = moduleIds.map(id => packageActivity?.moduleLabels?.get?.(id) ?? id); + return [{ + kind: "inactive-module-references", + severity: "info", + moduleIds, + moduleLabels, + message: format(i18n, "KSA.Notice.InactiveModuleReferences", { + modules: moduleLabels.join(", ") + }) + }]; +} + function isDerivedSceneThumbnail(file) { const path = String(file.path ?? ""); return /^worlds\/[^/]+\/assets\/scenes\/[^/]+-thumb\.(?:png|webp)$/u.test(path); diff --git a/styles/audit.css b/styles/audit.css index 97bcc84..1b99e5e 100644 --- a/styles/audit.css +++ b/styles/audit.css @@ -30,6 +30,11 @@ padding: 1.1rem 1.2rem; } +.storage-audit__summary--notices { + padding-top: 0.95rem; + padding-bottom: 0.95rem; +} + .storage-audit__grouped { display: grid; gap: 1rem; @@ -221,6 +226,13 @@ border-bottom: 0; } +.storage-audit__notice-list { + margin: 0; + padding-left: 1.2rem; + display: grid; + gap: 0.3rem; +} + .storage-audit__list { padding: 1rem; } diff --git a/tests/inactive-module-reference-test.mjs b/tests/inactive-module-reference-test.mjs new file mode 100644 index 0000000..18edc09 --- /dev/null +++ b/tests/inactive-module-reference-test.mjs @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import { buildFindings, createFileRecord } from "../scripts/core/finding-engine.js"; +import { createFileLocator } from "../scripts/core/path-utils.js"; + +const files = [ + createFileRecord(createFileLocator("data", "modules/example-module/icons/token.webp"), 1234) +]; + +const references = [ + { + sourceType: "world-document", + sourceScope: { ownerType: "world", ownerId: "demo-world", systemId: "demo-system", subtype: "actors" }, + sourceLabel: "Actor demo", + normalized: { + ...createFileLocator("data", "modules/example-module/icons/token.webp"), + targetKind: "local-file" + } + } +]; + +const packageActivity = { + modules: new Map([["example-module", false]]), + moduleLabels: new Map([["example-module", "Example Module"]]) +}; + +const result = buildFindings({ files, references, packageActivity, i18n: { format: (key, data={}) => `${key}:${data.modules ?? ""}` } }); + +assert.equal( + result.findings.some(finding => finding.kind === "non-package-to-package-reference"), + false, + "world references into inactive modules should not be reported as unanchored package targets" +); + +assert.equal( + result.notices.some(notice => notice.kind === "inactive-module-references" && notice.moduleIds.includes("example-module")), + true, + "inactive module references should create a report notice" +); + +console.log("inactive-module-reference-test: ok"); diff --git a/tests/scene-thumbnail-orphan-test.mjs b/tests/scene-thumbnail-orphan-test.mjs index cfc6c67..bc2789a 100644 --- a/tests/scene-thumbnail-orphan-test.mjs +++ b/tests/scene-thumbnail-orphan-test.mjs @@ -19,7 +19,7 @@ const references = [ } ]; -const findings = buildFindings({ files, references }); +const { findings } = buildFindings({ files, references }); assert.equal( findings.some(finding => finding.kind === "orphan-file" && finding.target.locator === "data:worlds/demo-world/assets/scenes/exampleScene-thumb.webp"), diff --git a/tests/storage-root-resolution-test.mjs b/tests/storage-root-resolution-test.mjs index 3fcd86f..9c57d9c 100644 --- a/tests/storage-root-resolution-test.mjs +++ b/tests/storage-root-resolution-test.mjs @@ -19,7 +19,7 @@ const references = [ } ]; -const findings = buildFindings({ files, references }); +const { findings } = buildFindings({ files, references }); assert.equal( findings.some(finding => finding.kind === "broken-reference" && finding.target.locator === "public:canvas/background_paper_16x9_4k.webp"), diff --git a/tests/wildcard-reference-test.mjs b/tests/wildcard-reference-test.mjs index 6828a93..9f0e556 100644 --- a/tests/wildcard-reference-test.mjs +++ b/tests/wildcard-reference-test.mjs @@ -20,7 +20,7 @@ const references = [ } ]; -const findings = buildFindings({ files, references }); +const { findings } = buildFindings({ files, references }); assert.equal( findings.some(finding => finding.kind === "broken-reference" && finding.target.locator === "data:modules/example/icons/token/Ork*_Token.webp"),