Handle references into inactive modules
This commit is contained in:
@@ -40,6 +40,10 @@
|
|||||||
"Failed": "Kosmos Storage Audit fehlgeschlagen: {message}",
|
"Failed": "Kosmos Storage Audit fehlgeschlagen: {message}",
|
||||||
"OpenSourceFailed": "Die Quelle konnte nicht geöffnet werden: {uuid}"
|
"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": {
|
"Summary": {
|
||||||
"NoAnalysis": "Noch keine Analyse ausgeführt.",
|
"NoAnalysis": "Noch keine Analyse ausgeführt.",
|
||||||
"Files": "Dateien",
|
"Files": "Dateien",
|
||||||
|
|||||||
@@ -40,6 +40,10 @@
|
|||||||
"Failed": "Kosmos Storage Audit failed: {message}",
|
"Failed": "Kosmos Storage Audit failed: {message}",
|
||||||
"OpenSourceFailed": "Could not open source: {uuid}"
|
"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": {
|
"Summary": {
|
||||||
"NoAnalysis": "No analysis has been run yet.",
|
"NoAnalysis": "No analysis has been run yet.",
|
||||||
"Files": "Files",
|
"Files": "Files",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "kosmos-storage-audit",
|
"id": "kosmos-storage-audit",
|
||||||
"title": "Kosmos Storage Audit",
|
"title": "Kosmos Storage Audit",
|
||||||
"description": "Analyzes media references and risky storage locations across Foundry data and public roots.",
|
"description": "Analyzes media references and risky storage locations across Foundry data and public roots.",
|
||||||
"version": "0.0.23",
|
"version": "0.0.24",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "13",
|
"minimum": "13",
|
||||||
"verified": "13"
|
"verified": "13"
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ export async function runRuntimeAnalysis({ onProgress }={}) {
|
|||||||
listFiles: () => listFoundryFiles(onProgress),
|
listFiles: () => listFoundryFiles(onProgress),
|
||||||
listSources: () => listFoundrySources(onProgress),
|
listSources: () => listFoundrySources(onProgress),
|
||||||
onProgress,
|
onProgress,
|
||||||
i18n: { format }
|
i18n: { format },
|
||||||
|
packageActivity: collectPackageActivity()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,3 +222,13 @@ function findEmbeddedIndex(path, segment) {
|
|||||||
const candidate = path[index + 1];
|
const candidate = path[index + 1];
|
||||||
return Number.isInteger(candidate) ? candidate : null;
|
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 };
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export class StorageAuditReportApp extends foundry.applications.api.ApplicationV
|
|||||||
hasAnalysis: !!this.#analysis,
|
hasAnalysis: !!this.#analysis,
|
||||||
showAll: this.#showAll,
|
showAll: this.#showAll,
|
||||||
progress: this.#progress,
|
progress: this.#progress,
|
||||||
|
notices: this.#analysis?.notices ?? [],
|
||||||
summary: this.#summarize(this.#analysis),
|
summary: this.#summarize(this.#analysis),
|
||||||
groupedFindings
|
groupedFindings
|
||||||
};
|
};
|
||||||
@@ -89,6 +90,7 @@ export class StorageAuditReportApp extends foundry.applications.api.ApplicationV
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
${renderProgress(context.progress, context.loading)}
|
${renderProgress(context.progress, context.loading)}
|
||||||
|
${renderNotices(context.notices, context.loading)}
|
||||||
${renderSummary(context.summary, context.loading)}
|
${renderSummary(context.summary, context.loading)}
|
||||||
${renderGroupedFindingList(context.groupedFindings, context.hasAnalysis, context.loading, context.showAll)}
|
${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;
|
if (!this.#analysis) return;
|
||||||
const payload = {
|
const payload = {
|
||||||
exportedAt: new Date().toISOString(),
|
exportedAt: new Date().toISOString(),
|
||||||
|
notices: this.#analysis.notices ?? [],
|
||||||
summary: this.#summarize(this.#analysis),
|
summary: this.#summarize(this.#analysis),
|
||||||
groupedFindings: groupFindings(this.#analysis.findings),
|
groupedFindings: groupFindings(this.#analysis.findings),
|
||||||
findings: this.#analysis.findings.map(serializeFinding)
|
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 => `<li>${escapeHtml(notice.message ?? "")}</li>`).join("");
|
||||||
|
return `
|
||||||
|
<section class="storage-audit__summary storage-audit__summary--notices">
|
||||||
|
<h3>${localize("KSA.Notice.Title")}</h3>
|
||||||
|
<ul class="storage-audit__notice-list">${items}</ul>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderSummary(summary, loading) {
|
function renderSummary(summary, loading) {
|
||||||
if (loading) return "";
|
if (loading) return "";
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { buildFindings, createFileRecord } from "./finding-engine.js";
|
|||||||
import { extractReferencesFromValue } from "./reference-extractor.js";
|
import { extractReferencesFromValue } from "./reference-extractor.js";
|
||||||
import { createFileLocator } from "./path-utils.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 = [];
|
const files = [];
|
||||||
let fileCount = 0;
|
let fileCount = 0;
|
||||||
let sourceCount = 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 });
|
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?.({
|
onProgress?.({
|
||||||
phase: "done",
|
phase: "done",
|
||||||
label: format(i18n, "KSA.Progress.Completed"),
|
label: format(i18n, "KSA.Progress.Completed"),
|
||||||
files: fileCount,
|
files: fileCount,
|
||||||
sources: sourceCount,
|
sources: sourceCount,
|
||||||
references: referenceCount,
|
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() {
|
async function yieldToUI() {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function compareOwner(sourceScope, targetOwner) {
|
|||||||
return "allowed";
|
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 fileByLocator = new Map(files.map(file => [createCanonicalLocator(file.storage, file.path), file]));
|
||||||
const fileLocators = files.map(file => ({
|
const fileLocators = files.map(file => ({
|
||||||
storage: file.storage,
|
storage: file.storage,
|
||||||
@@ -43,6 +43,7 @@ export function buildFindings({ files, references, i18n }={}) {
|
|||||||
const refsByLocator = new Map();
|
const refsByLocator = new Map();
|
||||||
const wildcardReferences = [];
|
const wildcardReferences = [];
|
||||||
const findings = [];
|
const findings = [];
|
||||||
|
const inactiveModuleReferenceIds = new Set();
|
||||||
|
|
||||||
for (const reference of resolvedReferences) {
|
for (const reference of resolvedReferences) {
|
||||||
const normalized = reference.normalized;
|
const normalized = reference.normalized;
|
||||||
@@ -84,6 +85,10 @@ export function buildFindings({ files, references, i18n }={}) {
|
|||||||
|
|
||||||
const ownerRelation = compareOwner(reference.sourceScope, file.ownerHint);
|
const ownerRelation = compareOwner(reference.sourceScope, file.ownerHint);
|
||||||
if (ownerRelation === "non-package-to-package") {
|
if (ownerRelation === "non-package-to-package") {
|
||||||
|
if (isInactiveModuleTarget(file, packageActivity)) {
|
||||||
|
inactiveModuleReferenceIds.add(file.ownerHint.ownerId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (isAnchoredInOwningPackage(file, matchingReferencesForFile(file, refsByLocator, wildcardReferences))) continue;
|
if (isAnchoredInOwningPackage(file, matchingReferencesForFile(file, refsByLocator, wildcardReferences))) continue;
|
||||||
findings.push({
|
findings.push({
|
||||||
kind: "non-package-to-package-reference",
|
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) {
|
function resolveReferenceTarget(reference, fileByLocator, fileLocators) {
|
||||||
@@ -225,6 +233,27 @@ function shouldReportOrphan(file, references) {
|
|||||||
return false;
|
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) {
|
function isDerivedSceneThumbnail(file) {
|
||||||
const path = String(file.path ?? "");
|
const path = String(file.path ?? "");
|
||||||
return /^worlds\/[^/]+\/assets\/scenes\/[^/]+-thumb\.(?:png|webp)$/u.test(path);
|
return /^worlds\/[^/]+\/assets\/scenes\/[^/]+-thumb\.(?:png|webp)$/u.test(path);
|
||||||
|
|||||||
@@ -30,6 +30,11 @@
|
|||||||
padding: 1.1rem 1.2rem;
|
padding: 1.1rem 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.storage-audit__summary--notices {
|
||||||
|
padding-top: 0.95rem;
|
||||||
|
padding-bottom: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
.storage-audit__grouped {
|
.storage-audit__grouped {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -221,6 +226,13 @@
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.storage-audit__notice-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.storage-audit__list {
|
.storage-audit__list {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
40
tests/inactive-module-reference-test.mjs
Normal file
40
tests/inactive-module-reference-test.mjs
Normal file
@@ -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");
|
||||||
@@ -19,7 +19,7 @@ const references = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const findings = buildFindings({ files, references });
|
const { findings } = buildFindings({ files, references });
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
findings.some(finding => finding.kind === "orphan-file" && finding.target.locator === "data:worlds/demo-world/assets/scenes/exampleScene-thumb.webp"),
|
findings.some(finding => finding.kind === "orphan-file" && finding.target.locator === "data:worlds/demo-world/assets/scenes/exampleScene-thumb.webp"),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const references = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const findings = buildFindings({ files, references });
|
const { findings } = buildFindings({ files, references });
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
findings.some(finding => finding.kind === "broken-reference" && finding.target.locator === "public:canvas/background_paper_16x9_4k.webp"),
|
findings.some(finding => finding.kind === "broken-reference" && finding.target.locator === "public:canvas/background_paper_16x9_4k.webp"),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const references = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const findings = buildFindings({ files, references });
|
const { findings } = buildFindings({ files, references });
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
findings.some(finding => finding.kind === "broken-reference" && finding.target.locator === "data:modules/example/icons/token/Ork*_Token.webp"),
|
findings.some(finding => finding.kind === "broken-reference" && finding.target.locator === "data:modules/example/icons/token/Ork*_Token.webp"),
|
||||||
|
|||||||
Reference in New Issue
Block a user