Files
healer-man/scripts/runewaker-pipeline/lib/encounter-implementation-report.mjs
T
2026-09-08 10:40:26 -04:00

703 lines
28 KiB
JavaScript

import { createHash } from "node:crypto";
import { readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const recipeDirectory = path.join(projectRoot, "scripts/runewaker-pipeline/recipes");
const reportJsonFile = path.join(
projectRoot,
"scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.json",
);
const reportMarkdownFile = path.join(
projectRoot,
"scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.md",
);
export const IMPLEMENTATION_STATUSES = Object.freeze([
"implemented",
"verified",
"deferred-source-gap",
"deferred-asset-gap",
"deferred-runtime-gap",
"excluded-non-gameplay",
"intentional-empty",
]);
export const FIDELITY_LEVELS = Object.freeze([
"source-exact",
"source-backed-adapted",
"healer-man-authored",
]);
const EXPECTED_FALLBACK_BOSSES = Object.freeze([
"rw-fa-100070",
"rw-fa-100561",
"rw-fa-100562",
"rw-fa-100563",
]);
const EXPECTED_NON_EMPTY_ZERO_BOSS_PACKAGES = Object.freeze([
"aeternal-circle",
"dungeon-of-dalanis",
"grafu-castle",
"kal-turok-hive",
"lair-demon-dragon",
"tomb-seven-heroes",
"warnorken-arena",
"zurhidon-stronghold",
]);
const EXPECTED_INTENTIONAL_EMPTY_PACKAGES = Object.freeze([
"demon-stronghold-125",
"zurhidon-stronghold-124",
]);
const SIGNAL_PATTERNS = Object.freeze([
["summon", /summon|spawn.*npc|create.*npc|create.*monster|add.*npc/i],
["door", /door|gate|seal|barrier|open.*object|close.*object/i],
["timer", /timer|delay|sleep|wait|schedule/i],
["movement", /moveto|move_to|patrol|waypoint|autoplot|path/i],
["spell", /cast|magic|spell|skill/i],
["death", /ondead|on_dead|dead|death|onkill|on_kill/i],
["phase", /phase|stage|state|wave/i],
["interaction", /click|useobject|talk|speak|interact|trigger/i],
["loot", /loot|treasure|drop|reward|chest/i],
]);
const readJson = async (file) => JSON.parse((await readFile(file, "utf8")).replace(/^\uFEFF/, ""));
const projectPath = (file) => path.relative(projectRoot, file).split(path.sep).join("/");
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
const sha256File = async (file) => sha256(await readFile(file));
const compareText = (left, right) => String(left).localeCompare(String(right));
const sameMembers = (left, right) => (
left.length === right.length && left.every((value, index) => value === right[index])
);
const constantCase = (value) => String(value).replaceAll("-", "_").toUpperCase();
function generatedJson(source, name, suffix) {
const marker = "export const " + name + " = ";
const start = source.indexOf(marker);
if (start < 0) throw new Error("Missing generated constant " + name + ".");
const valueStart = start + marker.length;
const valueEnd = source.indexOf(suffix, valueStart);
if (valueEnd < 0) throw new Error("Missing type suffix for " + name + ".");
return JSON.parse(source.slice(valueStart, valueEnd));
}
function runtimeTemplateId(entityId) {
const match = String(entityId).match(/-(\d+)$/);
return match ? Number(match[1]) : null;
}
function categoriesFor(value) {
return SIGNAL_PATTERNS
.filter(([, pattern]) => pattern.test(String(value ?? "")))
.map(([kind]) => kind);
}
function statusCounts(items) {
return Object.fromEntries(IMPLEMENTATION_STATUSES.map((status) => [
status,
items.filter((item) => item.status === status).length,
]));
}
function fidelityCounts(items) {
return Object.fromEntries(FIDELITY_LEVELS.map((fidelity) => [
fidelity,
items.filter((item) => item.fidelity === fidelity).length,
]));
}
function sourceFile(pathName, fileSha256, contentSha256 = null) {
return {
path: pathName,
sha256: fileSha256,
...(contentSha256 ? { contentSha256 } : {}),
};
}
function deferredRowRecord({ dungeonId, zoneId, row, snapshotPath, snapshotFileSha256, snapshot }) {
const evidenceText = [
row.name,
row.sourceName,
row.modelPath,
row.source?.roleName,
row.source?.autoPlot,
row.source?.plotClassName,
...Object.values(row.source?.templateScripts ?? {}),
].filter(Boolean).join(" ");
return {
id: "row:" + dungeonId + ":" + row.spawnId,
kind: "deferred-source-row",
dungeonId,
zoneId,
sourceIds: {
spawnId: row.spawnId,
templateId: row.templateId,
roomId: Number(row.source?.roomId ?? 0),
},
name: row.name,
sourceName: row.sourceName,
sourceClassification: row.classification,
sourcePosition: row.sourcePosition,
direction: row.direction,
modelPath: row.modelPath ?? null,
heuristicCategories: categoriesFor(evidenceText),
status: "deferred-source-gap",
fidelity: "source-exact",
source: {
snapshot: sourceFile(snapshotPath, snapshotFileSha256, snapshot.contentSha256),
},
blocker: "The source row preserves identity and placement, but no reviewed evidence proves its gameplay behavior.",
nextWork: "Identify the authoritative trigger, state transition, reset/persistence rules, and asset requirements; then add an explicit source-spawn mapping.",
clearingTest: "Add a deterministic fixture proving that source spawn " + row.spawnId
+ " executes the reviewed behavior and resets idempotently.",
};
}
function runtimeSpellMatches(entities, templateId, spellId) {
return Object.entries(entities).flatMap(([entityId, entity]) => {
if (runtimeTemplateId(entityId) !== templateId) return [];
return (entity.combat?.attacks ?? [])
.filter((attack) => Number(attack.spellId ?? attack.source?.spellId ?? 0) === spellId)
.map((attack) => ({
entityId,
attackId: attack.id,
telegraph: attack.telegraph ?? null,
delivery: attack.delivery,
}));
});
}
function baseEvidenceRecord({
id,
evidenceKind,
dungeonId,
zoneId,
templateId = null,
categories,
value,
source,
}) {
return {
id,
kind: "mechanics-evidence",
evidenceKind,
dungeonId,
zoneId,
sourceIds: templateId == null ? {} : { templateId },
categories: [...new Set(categories)].sort(compareText),
value,
status: "deferred-source-gap",
fidelity: "source-exact",
source,
blocker: "This is source evidence, but it does not prove a complete executable mechanic by itself.",
nextWork: "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.",
clearingTest: "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior.",
};
}
function templateEvidenceRecords({
dungeonId,
zoneId,
mechanics,
mechanicsPath,
mechanicsFileSha256,
snapshot,
snapshotPath,
snapshotFileSha256,
entities,
}) {
const records = [];
const commonSource = {
mechanicsAudit: sourceFile(mechanicsPath, mechanicsFileSha256),
snapshot: sourceFile(snapshotPath, snapshotFileSha256, snapshot.contentSha256),
};
for (const candidate of mechanics.templateCandidates ?? []) {
const templateId = Number(candidate.templateId);
for (const value of candidate.autoPlots ?? []) {
records.push(baseEvidenceRecord({
id: "signal:" + dungeonId + ":template:" + templateId + ":autoplot:" + sha256(value).slice(0, 12),
evidenceKind: "auto-plot",
dungeonId,
zoneId,
templateId,
categories: categoriesFor(value),
value,
source: commonSource,
}));
}
for (const value of candidate.plotClassNames ?? []) {
records.push(baseEvidenceRecord({
id: "signal:" + dungeonId + ":template:" + templateId + ":plot-class:" + sha256(value).slice(0, 12),
evidenceKind: "plot-class",
dungeonId,
zoneId,
templateId,
categories: categoriesFor(value),
value,
source: commonSource,
}));
}
for (const script of candidate.templateScripts ?? []) {
const value = script.event + ":" + script.script;
records.push(baseEvidenceRecord({
id: "signal:" + dungeonId + ":template:" + templateId + ":script:"
+ script.event + ":" + sha256(script.script).slice(0, 12),
evidenceKind: "template-script",
dungeonId,
zoneId,
templateId,
categories: categoriesFor(value),
value: { event: script.event, script: script.script },
source: commonSource,
}));
}
for (const spell of candidate.spells ?? []) {
const spellId = Number(spell.id);
const matches = runtimeSpellMatches(entities, templateId, spellId);
const record = baseEvidenceRecord({
id: "signal:" + dungeonId + ":template:" + templateId + ":spell:" + spellId,
evidenceKind: "source-spell",
dungeonId,
zoneId,
templateId,
categories: ["spell"],
value: { spellId, level: Number(spell.level ?? 0) },
source: commonSource,
});
if (matches.length) {
record.status = "verified";
record.fidelity = "source-backed-adapted";
record.runtimeMappings = matches;
record.blocker = "The original RuneWaker coefficient and native stat inputs are not available in the portable package.";
record.nextWork = "Retain the exact spell identity and revisit numerical fidelity only if authoritative formula inputs are recovered.";
record.clearingTest = "npm run runewaker:audit:gameplay verifies the source spell ID and runtime attack remain aligned.";
}
records.push(record);
}
}
return records;
}
function luaEvidenceRecords({ dungeonId, zoneId, mechanics, mechanicsPath, mechanicsFileSha256 }) {
const luaHashByFile = new Map(
(mechanics.luaSources ?? []).map((source) => [source.file, source.sha256]),
);
return (mechanics.luaSignals ?? []).map((signal) => baseEvidenceRecord({
id: "signal:" + dungeonId + ":lua:" + signal.file + ":" + signal.line,
evidenceKind: "lua-line",
dungeonId,
zoneId,
categories: signal.kinds ?? [],
value: { line: signal.line, text: signal.text },
source: {
mechanicsAudit: sourceFile(mechanicsPath, mechanicsFileSha256),
lua: sourceFile(signal.file, luaHashByFile.get(signal.file) ?? null),
},
}));
}
function renderMarkdown(report) {
const rows = report.dungeons.map((dungeon) => [
"|", dungeon.id, "|", dungeon.sourceRows, "|", dungeon.deferredSourceRows.length, "|",
dungeon.mechanicsEvidence.length, "|", dungeon.evidenceStatusCounts.verified, "|",
dungeon.evidenceStatusCounts["deferred-source-gap"], "|", dungeon.bossObjectives, "|",
dungeon.packageAssessment.status, "|",
].join(" "));
const categoryRows = Object.entries(report.summary.evidenceCategoryMemberships)
.map(([kind, count]) => "| " + kind + " | " + count + " |");
const blockerRows = Object.entries(report.blockerGroups)
.map(([key, value]) => "| " + key + " | " + value.items + " | " + value.nextWork + " |");
return [
"# RuneWaker encounter implementation and fidelity ledger",
"",
"Structural validation: **" + report.validationStatus + "**",
"Content completion: **" + report.completionStatus + "**",
"",
"This report is deterministic and offline. Its JSON companion inventories every deferred",
"source spawn row and every direct mechanics evidence record. A regex category membership",
"is a review label, not a unique mechanic; one evidence record may belong to several categories.",
"",
"## Coverage",
"",
"- " + report.summary.dungeons + " dungeon packages.",
"- " + report.summary.deferredSourceRows + " deferred source rows with stable spawn IDs.",
"- " + report.summary.uniqueMechanicsEvidence + " unique mechanics evidence records: "
+ report.summary.luaEvidenceSignals + " Lua lines and "
+ report.summary.templateEvidenceSignals + " template references.",
"- " + report.summary.evidenceCategoryMembershipCount
+ " overlapping category memberships; this number is intentionally not reported as mechanics.",
"- " + report.summary.verifiedEvidence + " evidence records have an exact executable source-spell mapping.",
"- " + report.summary.sourceGapEvidence + " evidence records remain source-review gaps.",
"",
"## Status definitions",
"",
"- `verified`: an exact source identifier is present in executable runtime data and checked by the audit.",
"- `implemented`: executable behavior exists, but source fidelity is incomplete or explicitly authored.",
"- `deferred-source-gap`: source context does not prove the full behavior; the generator never guesses it.",
"- `deferred-asset-gap`: behavior is understood but a required asset is unavailable.",
"- `deferred-runtime-gap`: behavior is understood but the runtime primitive is unavailable.",
"- `excluded-non-gameplay`: a reviewed source item is confirmed to have no gameplay behavior.",
"- `intentional-empty`: the authoritative packaged snapshot contains no population rows.",
"",
"## Per-dungeon inventory",
"",
"| Dungeon | Source rows | Deferred rows | Evidence | Verified | Source gaps | Bosses | Package status |",
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |",
...rows,
"",
"## Four explicit fallback bosses",
"",
"| Boss | Runtime ID | Status | Fidelity | Blocker |",
"| --- | --- | --- | --- | --- |",
...report.specialCases.fallbackBosses.map((boss) => "| " + boss.name + " | `" + boss.entityId
+ "` | " + boss.status + " | " + boss.fidelity + " | " + boss.blocker + " |"),
"",
"These attacks are intentionally playable and telegraphed HealerMan behavior. They are not",
"presented as original RuneWaker mechanics until authoritative source behavior is recovered.",
"",
"## Non-empty packages without confirmed boss objectives",
"",
...report.specialCases.nonEmptyZeroBossPackages.map((entry) => "- `" + entry.dungeonId + "`: "
+ entry.sourceRows + " rows; " + entry.blocker + " Next: " + entry.nextWork),
"",
"## Intentional empty variants",
"",
...report.specialCases.intentionalEmptyPackages.map((entry) => "- `" + entry.dungeonId + "` (Zone "
+ entry.zoneId + "): " + entry.blocker),
"",
"## Evidence category memberships",
"",
"| Category | Evidence records carrying the label |",
"| --- | ---: |",
...categoryRows,
"",
"## Remaining work ledger",
"",
"| Blocker group | Items | Clearing work |",
"| --- | ---: | --- |",
...blockerRows,
"",
"The exhaustive per-item blocker, next-work instruction, clearing test, source IDs, source",
"files, and SHA-256 hashes are in `RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.json`.",
"",
"## Validation issues",
"",
...(report.issues.length ? report.issues.map((entry) => "- " + entry) : ["- None."]),
"",
].join("\n");
}
function validateReport(report) {
const issues = [];
const inventory = report.dungeons.flatMap((dungeon) => [
...dungeon.deferredSourceRows,
...dungeon.mechanicsEvidence,
]);
const ids = new Set();
for (const item of inventory) {
if (ids.has(item.id)) issues.push("Duplicate inventory ID: " + item.id + ".");
ids.add(item.id);
if (!IMPLEMENTATION_STATUSES.includes(item.status)) {
issues.push(item.id + " has unsupported status " + item.status + ".");
}
if (!FIDELITY_LEVELS.includes(item.fidelity)) {
issues.push(item.id + " has unsupported fidelity " + item.fidelity + ".");
}
if (!item.blocker || !item.nextWork || !item.clearingTest) {
issues.push(item.id + " does not document blocker, next work, and clearing test.");
}
}
const fallbackIds = report.specialCases.fallbackBosses.map((boss) => boss.entityId).sort(compareText);
if (!sameMembers(fallbackIds, [...EXPECTED_FALLBACK_BOSSES])) {
issues.push("Expected exactly the four documented Forsaken Abbey fallback bosses; found "
+ fallbackIds.join(", ") + ".");
}
const zeroBossIds = report.specialCases.nonEmptyZeroBossPackages
.map((entry) => entry.dungeonId).sort(compareText);
if (!sameMembers(zeroBossIds, [...EXPECTED_NON_EMPTY_ZERO_BOSS_PACKAGES])) {
issues.push("Non-empty zero-boss package set drifted: " + zeroBossIds.join(", ") + ".");
}
const emptyIds = report.specialCases.intentionalEmptyPackages
.map((entry) => entry.dungeonId).sort(compareText);
if (!sameMembers(emptyIds, [...EXPECTED_INTENTIONAL_EMPTY_PACKAGES])) {
issues.push("Intentional-empty package set drifted: " + emptyIds.join(", ") + ".");
}
return issues;
}
export async function buildEncounterImplementationReport() {
const recipeNames = (await readdir(recipeDirectory))
.filter((name) => name.endsWith("-population.json"))
.sort(compareText);
const bossMechanicsPath = path.join(projectRoot, "dungeon-pipeline/boss-mechanics.json");
const [bossMechanics, bossMechanicsFileSha256] = await Promise.all([
readJson(bossMechanicsPath),
sha256File(bossMechanicsPath),
]);
const dungeons = [];
for (const recipeName of recipeNames) {
const recipeFile = path.join(recipeDirectory, recipeName);
const recipe = await readJson(recipeFile);
const dungeonId = recipe.dungeonId;
const zoneId = recipe.zoneId;
const snapshotFile = path.resolve(projectRoot, recipe.files.snapshot);
const mechanicsFile = path.resolve(
projectRoot,
recipe.files.mechanicsReport
?? "src/assets/game/dungeons/" + dungeonId + "/mechanics-audit.json",
);
const generatedSourceFile = path.resolve(projectRoot, recipe.files.generatedSource);
const [
recipeFileSha256,
snapshot,
snapshotFileSha256,
mechanics,
mechanicsFileSha256,
generatedSource,
generatedSourceSha256,
] = await Promise.all([
sha256File(recipeFile),
readJson(snapshotFile),
sha256File(snapshotFile),
readJson(mechanicsFile),
sha256File(mechanicsFile),
readFile(generatedSourceFile, "utf8"),
sha256File(generatedSourceFile),
]);
const prefix = constantCase(dungeonId);
const entities = generatedJson(
generatedSource,
prefix + "_ENTITIES",
" as const satisfies PopulationDefinitionMap;",
);
const bosses = generatedJson(
generatedSource,
prefix + "_BOSSES",
" as const satisfies readonly DungeonBossObjective[];",
);
const snapshotPath = projectPath(snapshotFile);
const mechanicsPath = projectPath(mechanicsFile);
const deferredSourceRows = (snapshot.rows ?? [])
.filter((row) => String(row.classification).startsWith("deferred"))
.map((row) => deferredRowRecord({
dungeonId,
zoneId,
row,
snapshotPath,
snapshotFileSha256,
snapshot,
}))
.sort((left, right) => left.sourceIds.spawnId - right.sourceIds.spawnId);
const mechanicsEvidence = [
...luaEvidenceRecords({
dungeonId,
zoneId,
mechanics,
mechanicsPath,
mechanicsFileSha256,
}),
...templateEvidenceRecords({
dungeonId,
zoneId,
mechanics,
mechanicsPath,
mechanicsFileSha256,
snapshot,
snapshotPath,
snapshotFileSha256,
entities,
}),
].sort((left, right) => compareText(left.id, right.id));
const packageStatus = snapshot.rows.length === 0
? "intentional-empty"
: bosses.length === 0
? "deferred-source-gap"
: "verified";
dungeons.push({
id: dungeonId,
zoneId,
sourceRows: snapshot.rows.length,
bossObjectives: bosses.length,
sourceArtifacts: {
recipe: sourceFile(projectPath(recipeFile), recipeFileSha256),
snapshot: sourceFile(snapshotPath, snapshotFileSha256, snapshot.contentSha256),
mechanicsAudit: sourceFile(mechanicsPath, mechanicsFileSha256),
generatedRuntime: sourceFile(projectPath(generatedSourceFile), generatedSourceSha256),
upstreamHashes: snapshot.sourceHashes ?? {},
},
packageAssessment: {
status: packageStatus,
fidelity: "source-exact",
blocker: packageStatus === "intentional-empty"
? "The authoritative snapshot contains zero population rows; no combat population is fabricated."
: packageStatus === "deferred-source-gap"
? "The package is non-empty, but its preserved source classification confirms no boss objectives."
: "No package-level population blocker is recorded.",
nextWork: packageStatus === "deferred-source-gap"
? "Recover authoritative boss membership and encounter order before creating boss objectives."
: "Re-run the ledger whenever portable source content changes.",
clearingTest: packageStatus === "deferred-source-gap"
? "Add a source-backed boss-membership fixture and verify the generated objective list."
: "npm run runewaker:audit:gameplay",
},
deferredRowStatusCounts: statusCounts(deferredSourceRows),
evidenceStatusCounts: statusCounts(mechanicsEvidence),
evidenceFidelityCounts: fidelityCounts(mechanicsEvidence),
deferredSourceRows,
mechanicsEvidence,
});
}
dungeons.sort((left, right) => compareText(left.id, right.id));
const allRows = dungeons.flatMap((dungeon) => dungeon.deferredSourceRows);
const allEvidence = dungeons.flatMap((dungeon) => dungeon.mechanicsEvidence);
const fallbackBosses = Object.entries(bossMechanics.bosses ?? {})
.filter(([, boss]) => boss.source?.kind === "healer-man-source-gap")
.map(([entityId, boss]) => {
const dungeon = dungeons.find((entry) => entry.id === boss.source.dungeonId);
return {
entityId,
name: boss.name,
dungeonId: boss.source.dungeonId,
templateId: Number(boss.source.templateId),
mechanicIds: (boss.mechanics ?? []).map((mechanic) => mechanic.id).sort(compareText),
telegraphedMechanicIds: (boss.mechanics ?? [])
.filter((mechanic) => mechanic.telegraph)
.map((mechanic) => mechanic.id)
.sort(compareText),
status: "implemented",
fidelity: "healer-man-authored",
source: {
bossMechanicsCatalog: sourceFile(projectPath(bossMechanicsPath), bossMechanicsFileSha256),
snapshot: dungeon?.sourceArtifacts.snapshot ?? null,
roots: boss.source.roots ?? [],
functions: boss.source.functions ?? [],
spellIds: boss.source.spellIds ?? [],
},
blocker: "Preserved source evidence does not expose an executable original combat kit.",
nextWork: "Replace the fallback only after authoritative spell timing, targeting, and effects are recovered; preserve modular IDs and telegraphs.",
clearingTest: "Add source-backed mechanic fixtures and assert the catalog source kind changes from healer-man-source-gap to runewaker.",
};
})
.sort((left, right) => compareText(left.entityId, right.entityId));
const nonEmptyZeroBossPackages = dungeons
.filter((dungeon) => dungeon.sourceRows > 0 && dungeon.bossObjectives === 0)
.map((dungeon) => ({
dungeonId: dungeon.id,
zoneId: dungeon.zoneId,
sourceRows: dungeon.sourceRows,
status: "deferred-source-gap",
fidelity: "source-exact",
source: dungeon.sourceArtifacts.snapshot,
blocker: "The portable source package contains population rows but no source-confirmed boss membership.",
nextWork: "Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.",
clearingTest: "Verify at least one source-backed boss objective, or add reviewed evidence that this package intentionally has none.",
}));
const intentionalEmptyPackages = dungeons
.filter((dungeon) => dungeon.sourceRows === 0)
.map((dungeon) => ({
dungeonId: dungeon.id,
zoneId: dungeon.zoneId,
sourceRows: 0,
status: "intentional-empty",
fidelity: "source-exact",
source: dungeon.sourceArtifacts.snapshot,
blocker: "The authoritative packaged snapshot contains no active population rows.",
nextWork: "No population work is scheduled unless a different authoritative source snapshot is supplied.",
clearingTest: "Keep the zero-row snapshot hash and intentional-empty classification stable in this audit.",
}));
const categoryMemberships = {};
for (const evidence of allEvidence) {
for (const category of evidence.categories) {
categoryMemberships[category] = (categoryMemberships[category] ?? 0) + 1;
}
}
const luaEvidenceSignals = allEvidence.filter((item) => item.evidenceKind === "lua-line").length;
const templateEvidenceSignals = allEvidence.length - luaEvidenceSignals;
const report = {
schemaVersion: 1,
validationStatus: "green",
completionStatus: allRows.length || allEvidence.some((item) => item.status.startsWith("deferred"))
? "review-required"
: "complete",
scope: "All configured offline RuneWaker dungeon packages",
generatedBy: "scripts/runewaker-pipeline/audit-encounter-implementation.mjs",
deterministic: true,
policy: {
statuses: IMPLEMENTATION_STATUSES,
fidelities: FIDELITY_LEVELS,
evidenceRule: "Direct source rows and source references are inventoried once. Regex category memberships may overlap and are never counted as unique mechanics.",
failClosedRule: "When static evidence cannot prove complete behavior, the item remains deferred-source-gap rather than receiving a guessed implementation status.",
sourceExactRule: "source-exact describes preserved evidence identity and provenance, not an assertion that deferred gameplay behavior is known.",
},
summary: {
dungeons: dungeons.length,
deferredSourceRows: allRows.length,
uniqueMechanicsEvidence: allEvidence.length,
luaEvidenceSignals,
templateEvidenceSignals,
evidenceCategoryMembershipCount: Object.values(categoryMemberships)
.reduce((total, count) => total + count, 0),
evidenceCategoryMemberships: Object.fromEntries(
Object.entries(categoryMemberships).sort(([left], [right]) => compareText(left, right)),
),
deferredRowStatuses: statusCounts(allRows),
evidenceStatuses: statusCounts(allEvidence),
evidenceFidelities: fidelityCounts(allEvidence),
verifiedEvidence: allEvidence.filter((item) => item.status === "verified").length,
sourceGapEvidence: allEvidence.filter((item) => item.status === "deferred-source-gap").length,
fallbackBosses: fallbackBosses.length,
nonEmptyZeroBossPackages: nonEmptyZeroBossPackages.length,
intentionalEmptyPackages: intentionalEmptyPackages.length,
},
blockerGroups: {
deferredSourceRows: {
items: allRows.length,
nextWork: "Review source semantics and add explicit source-spawn encounter mappings.",
},
unresolvedMechanicsEvidence: {
items: allEvidence.filter((item) => item.status === "deferred-source-gap").length,
nextWork: "Review surrounding Lua/template context; specify full trigger, effect, and reset behavior.",
},
adaptedSpellFormula: {
items: allEvidence.filter((item) => item.status === "verified"
&& item.fidelity === "source-backed-adapted").length,
nextWork: "Recover native formula inputs before claiming coefficient-level fidelity.",
},
sourceGapFallbackBosses: {
items: fallbackBosses.length,
nextWork: "Recover authoritative combat kits and replace only with source-backed modular mechanics.",
},
nonEmptyZeroBossPackages: {
items: nonEmptyZeroBossPackages.length,
nextWork: "Recover authoritative boss membership and objective order.",
},
},
specialCases: {
fallbackBosses,
nonEmptyZeroBossPackages,
intentionalEmptyPackages,
},
issues: [],
dungeons,
};
report.issues = validateReport(report);
report.validationStatus = report.issues.length ? "blocked" : "green";
return report;
}
export async function generateEncounterImplementationReport() {
const report = await buildEncounterImplementationReport();
await Promise.all([
writeFile(reportJsonFile, JSON.stringify(report, null, 2) + "\n", "utf8"),
writeFile(reportMarkdownFile, renderMarkdown(report), "utf8"),
]);
return {
report,
reportJson: projectPath(reportJsonFile),
reportMarkdown: projectPath(reportMarkdownFile),
};
}