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), }; }