const { spawnSync } = require('node:child_process'); const crypto = require('node:crypto'); const fs = require('node:fs'); const path = require('node:path'); const { FULL_STOCKADE_DUNGEON_ID, FULL_STOCKADE_DUNGEON_TITLE, FULL_STOCKADE_BOSS, } = require('./playtest-defaults.cjs'); const projectRoot = path.resolve(__dirname, '..'); const contentSourceDirectory = path.join(projectRoot, 'content'); const contentManifestPath = path.join(contentSourceDirectory, 'manifest.json'); const sourceHarnessPath = path.join(projectRoot, 'scripts', 'playtest-campaign-boss-pass.cjs'); const sourceHarness = fs.readFileSync(sourceHarnessPath, 'utf8'); const prepareOnly = process.argv.includes('--prepare-only'); const frozenBuildAuthorized = process.argv.includes('--frozen-build-authorized'); const runId = new Date().toISOString().replace(/[-:.]/g, '').replace('T', '-').replace('Z', ''); const outputDirectory = path.join(projectRoot, 'playtest-artifacts', 'stockade-enemy-mechanics', runId); const childOutputDirectory = path.join(outputDirectory, 'campaign'); const runtimeHarnessPath = path.join(__dirname, `.playtest-stockade-enemy-mechanics-${process.pid}.cjs`); const requestedBuildDirectory = process.env.HEALER_MAN_STOCKADE_DIST?.trim() ? path.resolve(process.env.HEALER_MAN_STOCKADE_DIST.trim()) : null; const buildDirectory = requestedBuildDirectory ?? path.join(projectRoot, 'dist'); const expectedBuildSha256Argument = process.argv.find((value) => value.startsWith('--expected-build-sha256=')) ?.slice('--expected-build-sha256='.length); const expectedBuildSha256 = expectedBuildSha256Argument ?? process.env.HEALER_MAN_STOCKADE_DIST_SHA256?.trim() ?? null; function sha256(bytes) { return crypto.createHash('sha256').update(bytes).digest('hex'); } function replaceExactlyOnce(source, marker, replacement, label) { const count = source.split(marker).length - 1; if (count !== 1) throw new Error(`${label} must occur exactly once in the source harness; found ${count}.`); return source.replace(marker, replacement); } function collectBuildManifest() { if (!fs.existsSync(buildDirectory) || !fs.statSync(buildDirectory).isDirectory()) { throw new Error(`Frozen build directory is missing or is not a directory: ${buildDirectory}`); } const files = []; function visit(directory, relativeDirectory = '') { for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { const relativePath = path.posix.join(relativeDirectory, entry.name); const absolutePath = path.join(directory, entry.name); if (entry.isDirectory()) visit(absolutePath, relativePath); else if (entry.isFile()) { const bytes = fs.readFileSync(absolutePath); files.push({ path: relativePath, bytes: bytes.length, sha256: sha256(bytes) }); } } } visit(buildDirectory); const canonicalManifest = files.map((file) => `${file.path}\t${file.bytes}\t${file.sha256}\n`).join(''); const bundleModuleReport = files.find((file) => file.path === 'bundle-module-report.json'); return { capturedAt: new Date().toISOString(), directory: buildDirectory, fileCount: files.length, aggregateSha256: sha256(canonicalManifest), bundleModuleReportSha256: bundleModuleReport?.sha256 ?? null, files, }; } function collectContentManifestSnapshot() { const bytes = fs.readFileSync(contentManifestPath); const manifest = JSON.parse(bytes.toString('utf8')); return { capturedAt: new Date().toISOString(), directory: contentSourceDirectory, manifestPath: contentManifestPath, manifestBytes: bytes.length, manifestSha256: sha256(bytes), manifestVersion: manifest.version ?? null, schemaVersion: manifest.schemaVersion ?? null, packIds: Array.isArray(manifest.packs) ? manifest.packs.map((pack) => pack.id).sort() : [], fileCount: manifest.files && typeof manifest.files === 'object' ? Object.keys(manifest.files).length : null, }; } function collectLoadedContentObjectEvidence(responses) { const objectResponses = responses.filter((response) => response?.pathname?.startsWith('/content/objects/')); const responseCountsByPath = objectResponses.reduce((counts, response) => { counts.set(response.pathname, (counts.get(response.pathname) ?? 0) + 1); return counts; }, new Map()); const uniqueObjects = [...new Map(objectResponses.map((response) => [response.pathname, { ...response, responseCount: responseCountsByPath.get(response.pathname), }])).values()]; return uniqueObjects.map((response) => { const relativeObjectPath = decodeURIComponent(response.pathname.slice('/content/'.length)); const absoluteObjectPath = path.resolve(contentSourceDirectory, relativeObjectPath); const relativeCheck = path.relative(contentSourceDirectory, absoluteObjectPath); if (relativeCheck.startsWith('..') || path.isAbsolute(relativeCheck)) { return { ...response, localObjectPath: null, localBytes: null, localSha256: null, objectIntegrity: 'unsafe-path-refused' }; } if (!fs.existsSync(absoluteObjectPath) || !fs.statSync(absoluteObjectPath).isFile()) { return { ...response, localObjectPath: path.relative(projectRoot, absoluteObjectPath), localBytes: null, localSha256: null, objectIntegrity: 'not-found-on-content-source' }; } const bytes = fs.readFileSync(absoluteObjectPath); const actualSha256 = sha256(bytes); const urlObjectSha256 = /^([a-f0-9]{64})(?:\.|$)/i.exec(path.basename(absoluteObjectPath))?.[1]?.toLowerCase() ?? null; return { ...response, localObjectPath: path.relative(projectRoot, absoluteObjectPath), localBytes: bytes.length, localSha256: actualSha256, urlObjectSha256, objectIntegrity: response.status === 200 && urlObjectSha256 ? actualSha256 === urlObjectSha256 ? 'served-file-matches-hash-address' : 'served-file-hash-mismatch' : response.status === 200 ? 'served-without-hash-address' : 'http-not-successful', }; }); } function createInstrumentedHarness() { if (FULL_STOCKADE_DUNGEON_ID !== 'stormwind-stockade' || FULL_STOCKADE_DUNGEON_TITLE !== 'Stormwind Stockade') { throw new Error(`Full Stockade fixture changed unexpectedly: ${FULL_STOCKADE_DUNGEON_ID} / ${FULL_STOCKADE_DUNGEON_TITLE}`); } const mobMarker = ' schoolLockedUntil: mob.schoolLockedUntil ?? {},'; const outputMarker = "const artifactRoot = path.resolve('playtest-artifacts/campaign-boss-pass');"; const runtimeRelativeOutput = path.relative(projectRoot, childOutputDirectory).replace(/\\/g, '/'); let transformed = sourceHarness; transformed = replaceExactlyOnce(transformed, outputMarker, `const artifactRoot = path.resolve(${JSON.stringify(runtimeRelativeOutput)});`, 'Campaign output directory'); transformed = replaceExactlyOnce(transformed, mobMarker, ` attacks: (mob.attacks ?? []).map((attack) => ({ id: attack.id, name: attack.name, spellId: attack.spellId ?? null, school: attack.school ?? null, castTimeMs: attack.castTimeMs ?? 0, cooldownMs: attack.cooldownMs ?? 0, interruptible: attack.interruptible !== false, })), auras: mob.auras ?? [], combatAuraTelemetry: (combatState.auras ?? []).filter((aura) => aura.targetId === mob.id).map((aura) => ({ instanceId: aura.instanceId ?? null, definitionId: aura.definition?.id ?? null, definitionName: aura.definition?.name ?? null, sourceId: aura.sourceId ?? null, targetId: aura.targetId ?? null, appliedAt: aura.appliedAt ?? null, expiresAt: aura.expiresAt ?? null, stacks: aura.stacks ?? null, definition: aura.definition ?? null, })), schoolLockedUntil: mob.schoolLockedUntil ?? {},`, 'Mob telemetry projection'); const distPathReplacements = [ ["fs.readFileSync('dist/bundle-module-report.json', 'utf8')", "fs.readFileSync(path.join(process.env.HEALER_MAN_STOCKADE_DIST, 'bundle-module-report.json'), 'utf8')", 'bundle module report path'], ["path.join('dist/assets', dungeonRunStoreAssetFile)", "path.join(process.env.HEALER_MAN_STOCKADE_DIST, 'assets', dungeonRunStoreAssetFile)", 'dungeon run module verification path'], ["fs.readdirSync('dist/assets')", "fs.readdirSync(path.join(process.env.HEALER_MAN_STOCKADE_DIST, 'assets'))", 'module asset enumeration path'], ["const runtime = createGameServer({ dataDir: dataDirectory, enableShelvedModes: false });", "const runtime = createGameServer({ dataDir: dataDirectory, staticDir: process.env.HEALER_MAN_STOCKADE_DIST, contentDir: path.resolve('content'), enableShelvedModes: false });\n if (path.resolve(runtime.staticDir) !== path.resolve(process.env.HEALER_MAN_STOCKADE_DIST)) {\n throw new Error(`Playtest server resolved an unexpected static directory: ${runtime.staticDir}`);\n }\n if (path.resolve(runtime.contentDir) !== path.resolve('content')) {\n throw new Error(`Playtest server resolved an unexpected read-only content source: ${runtime.contentDir}`);\n }", 'static server document root'], [" const pageErrors = [];\n const consoleErrors = [];", " const pageErrors = [];\n const consoleErrors = [];\n const contentSourceResponses = [];", 'content source response telemetry'], [" page.on('pageerror', (error) => pageErrors.push(error.message));", " page.on('pageerror', (error) => pageErrors.push(error.message));\n page.on('response', (response) => {\n try {\n const url = new URL(response.url());\n if (url.pathname !== '/content/manifest.json' && !url.pathname.startsWith('/content/objects/')) return;\n const headers = response.headers();\n contentSourceResponses.push({\n observedAt: new Date().toISOString(),\n method: response.request().method(),\n url: response.url(),\n pathname: url.pathname,\n status: response.status(),\n contentType: headers['content-type'] ?? null,\n contentLength: headers['content-length'] ?? null,\n contentEncoding: headers['content-encoding'] ?? null,\n cacheControl: headers['cache-control'] ?? null,\n });\n } catch (error) {\n contentSourceResponses.push({ observedAt: new Date().toISOString(), instrumentationError: String(error) });\n }\n });", 'content source response listener'], ["Expected built game assets were not found in dist/assets: ${moduleFiles.join(', ')}", "Expected built game assets were not found in the frozen Stockade distribution: ${moduleFiles.join(', ')}", 'asset failure message'], ]; for (const [marker, replacement, label] of distPathReplacements) { transformed = replaceExactlyOnce(transformed, marker, replacement, label); } transformed = replaceExactlyOnce(transformed, ' const routeResult = await runRouteSegment(page, route, entry, profile);', ' const routeResult = await runRouteSegment(page, route, entry, profile);\n routeResult.contentSourceResponses = contentSourceResponses.slice();', 'route content source evidence'); transformed = replaceExactlyOnce(transformed, ' activeCast: mob.activeCast, position: mob.position })) });', ' activeCast: mob.activeCast, attacks: mob.attacks ?? [], auras: mob.auras ?? [],\n combatAuraTelemetry: mob.combatAuraTelemetry ?? [], abilityCastCounts: mob.abilityCastCounts ?? {},\n statuses: mob.statuses ?? [], position: mob.position })) });', 'compact route sample enemy mechanics telemetry'); transformed = replaceExactlyOnce(transformed, ' const beforePosition = current.player.position;\n const beforeDistance = distance3(beforePosition, checkpoint.position);\n const planarRemaining = distancePlanar(beforePosition, checkpoint.position);\n const speedEstimate = ordinaryMovementSpeedEstimate(result.movement);\n const durationMs = Math.max(45, Math.min(350, Math.floor(planarRemaining * 0.58 / speedEstimate * 1_000)));\n const moved = await walkToward(page, result, checkpoint.position, durationMs);\n if (!moved) {\n routeBlocked = { kind: \'no-directional-input\', checkpointIndex, checkpoint, position: current.player.position };', ' const beforePosition = current.player.position;\n const beforeDistance = distance3(beforePosition, checkpoint.position);\n const planarRemaining = distancePlanar(beforePosition, checkpoint.position);\n const speedEstimate = ordinaryMovementSpeedEstimate(result.movement);\n const recentResponseScales = result.movement.slice(-8)\n .filter((step) => Number(step.requestedDurationMs) > 0 && Number(step.durationMs) >= Number(step.requestedDurationMs)\n && Number(step.planarDisplacement) >= 0.08)\n .map((step) => Number(step.durationMs) / Number(step.requestedDurationMs)).sort((left, right) => left - right);\n const movementResponseScale = recentResponseScales.length\n ? recentResponseScales[Math.floor(recentResponseScales.length / 2)] : 1;\n const plannedDurationMs = planarRemaining * 0.58 / speedEstimate * 1_000 / movementResponseScale;\n const nearCheckpointLimitMs = planarRemaining < 1.5 ? 55 : 350;\n const durationMs = Math.max(25, Math.min(nearCheckpointLimitMs, 350, Math.floor(plannedDurationMs)));\n const moved = await walkToward(page, result, checkpoint.position, durationMs);\n if (!moved) {\n routeBlocked = { kind: \'no-directional-input\', checkpointIndex, checkpoint, position: current.player.position };', 'adaptive ordinary movement timing from measured response'); transformed = replaceExactlyOnce(transformed, ' buildAssets: moduleFiles, error: error.stack ?? error.message, pageErrors, consoleErrors,', ' buildAssets: moduleFiles, error: error.stack ?? error.message, pageErrors, consoleErrors,\n contentSourceResponses,', 'failure content source evidence'); if (/['"`]dist\/(?:assets|bundle-module-report)/.test(transformed)) { throw new Error('A hard-coded campaign harness distribution path survived frozen-build instrumentation.'); } return transformed; } function summarizeEnemyEvidence(route) { const mobSamples = []; const rawCombatEvents = route.observedCombatEvents ?? []; function visit(value, sourcePath = '$') { if (!value || typeof value !== 'object') return; if (Array.isArray(value.mobs)) { const at = value.at ?? value.now ?? value.observedAt ?? null; for (const mob of value.mobs) { if (!mob || typeof mob !== 'object') continue; mobSamples.push({ sampledAt: at, sourcePath, id: mob.id ?? null, name: mob.name ?? null, serverEntry: mob.serverEntry ?? null, health: mob.health ?? null, maxHealth: mob.maxHealth ?? null, dead: mob.dead ?? null, engaged: mob.engaged ?? null, activeCast: mob.activeCast ?? null, abilityCastCounts: mob.abilityCastCounts ?? null, attacks: mob.attacks ?? [], auras: mob.auras ?? [], combatAuraTelemetry: mob.combatAuraTelemetry ?? [], statuses: mob.statuses ?? [], }); } } if (Array.isArray(value)) value.forEach((child, index) => visit(child, `${sourcePath}[${index}]`)); else for (const [key, child] of Object.entries(value)) visit(child, `${sourcePath}.${key}`); } visit(route); const enemyActorIds = new Set(mobSamples.map((sample) => sample.id).filter(Boolean)); const enemyNameById = new Map(mobSamples.filter((sample) => sample.id && sample.name) .map((sample) => [sample.id, sample.name])); const combatAuraSamples = mobSamples.filter((sample) => sample.combatAuraTelemetry.length > 0) .map((sample) => ({ sampledAt: sample.sampledAt, sourcePath: sample.sourcePath, enemyId: sample.id, enemyName: sample.name, auras: sample.combatAuraTelemetry })); const enemyBuffAuraApplications = [...new Map(combatAuraSamples.flatMap((sample) => sample.auras .filter((aura) => enemyActorIds.has(aura.sourceId) && enemyActorIds.has(aura.targetId) && aura.definition?.disposition === 'buff') .map((aura) => [aura.instanceId, { instanceId: aura.instanceId, definitionId: aura.definitionId, definitionName: aura.definitionName, sourceId: aura.sourceId, sourceName: enemyNameById.get(aura.sourceId) ?? null, targetId: aura.targetId, targetName: enemyNameById.get(aura.targetId) ?? null, appliedAt: aura.appliedAt, expiresAt: aura.expiresAt, stacks: aura.stacks, }]))).filter(([instanceId]) => Boolean(instanceId))].map(([, aura]) => aura); const enemyDamageEvents = rawCombatEvents.filter((event) => enemyActorIds.has(event.sourceActorId) && event.kind === 'damage').map((event) => ({ ...event, sourceEnemyName: enemyNameById.get(event.sourceActorId) ?? null })); const rendDamageEvents = enemyDamageEvents.filter((event) => /(?:^|\D)(?:6547|16509)(?:\D|$)/ .test(String(event.abilityId ?? event.spellId ?? event.attackId ?? '')) || /rend/i.test(String(event.abilityName ?? event.name ?? ''))); const chainLightningCastSamples = mobSamples.filter((sample) => sample.name === FULL_STOCKADE_BOSS && sample.activeCast && (Number(sample.activeCast.spellId) === 421 || /(?:^|\D)421(?:\D|$)/.test(String(sample.activeCast.attackId ?? '')) || /chain\s*lightning/i.test(String(sample.activeCast.name ?? '')))); const rendCastSamples = mobSamples.filter((sample) => sample.activeCast && (Number(sample.activeCast.spellId) === 6547 || Number(sample.activeCast.spellId) === 16509 || /rend/i.test(String(sample.activeCast.name ?? '')) || /(?:^|\D)(?:6547|16509)(?:\D|$)/.test(String(sample.activeCast.attackId ?? '')))); const demoralizingShoutCastWindows = [...new Map(mobSamples.filter((sample) => sample.activeCast && (Number(sample.activeCast.spellId) === 13730 || Number(sample.activeCast.spellId) === 16244 || /demoralizing\s*shout/i.test(String(sample.activeCast.name ?? ''))) ).map((sample) => [`${sample.id}|${sample.activeCast.attackId}|${sample.activeCast.startedAt}`, { enemyId: sample.id, enemyName: sample.name, attackId: sample.activeCast.attackId ?? null, spellId: sample.activeCast.spellId ?? null, startedAt: sample.activeCast.startedAt ?? null, completesAt: sample.activeCast.completesAt ?? null, interruptible: sample.activeCast.interruptible ?? null, }])).values()]; const rendAttacks = mobSamples.flatMap((sample) => (sample.attacks ?? []).filter((attack) => ( Number(attack.spellId) === 6547 || Number(attack.spellId) === 16509 || /rend/i.test(String(attack.name ?? '')) || /(?:^|\D)(?:6547|16509)(?:\D|$)/.test(String(attack.id ?? '')) )).map((attack) => ({ enemyId: sample.id, enemyName: sample.name, ...attack }))); const enemyAuraObservations = mobSamples.filter((sample) => Array.isArray(sample.auras) && sample.auras.length > 0) .map((sample) => ({ sampledAt: sample.sampledAt, sourcePath: sample.sourcePath, enemyId: sample.id, enemyName: sample.name, auras: sample.auras })); return { definitions: { rawCombatEvents: 'Unmodified combatStore event objects collected by the campaign harness; timestamps and amount fields retain source values.', enemyDamageEvents: 'Raw damage events whose source actor id appeared in observed mob snapshots.', rendDamageEvents: 'Raw enemy damage events whose source ability id/name matches Rend 6547 or 16509.', mobSamples: 'Read-only snapshots extended with each mob attack definition and mob aura state.', combatAuraSamples: 'Read-only combatStore auras filtered to the sampled mob id, retaining definition, owner, source, apply, expiry, and stack fields.', enemyBuffAuraApplications: 'Unique enemy-source buff auras whose source and target actor ids both appeared in sampled mobs; deduplicated by aura instance id.', chainLightningMatch: 'Hamhock active cast matches spellId 421, attackId containing 421, or source cast name Chain Lightning.', rendCastMatch: 'Mob active cast matches spellId 6547/16509, attackId containing either ID, or source cast name Rend.', demoralizingShoutCastWindows: 'Unique active cast windows for enemy spell 13730/16244 or a Demoralizing Shout-named cast, keyed by enemy id, attack id, and startedAt.', rendMatch: 'Mob attack definition matches spellId 6547/16509, attack name Rend, or attack id containing one of those IDs.', }, counts: { mobSamples: mobSamples.length, rawCombatEvents: rawCombatEvents.length, enemyDamageEvents: enemyDamageEvents.length, rendDamageEvents: rendDamageEvents.length, chainLightningCastSamples: chainLightningCastSamples.length, rendCastSamples: rendCastSamples.length, demoralizingShoutCastWindows: demoralizingShoutCastWindows.length, rendAttackDefinitions: rendAttacks.length, enemyAuraObservations: enemyAuraObservations.length, combatAuraSamples: combatAuraSamples.length, enemyBuffAuraApplications: enemyBuffAuraApplications.length, }, enemyDamageEvents, rendDamageEvents, chainLightningCastSamples, rendCastSamples, demoralizingShoutCastWindows, rendAttackDefinitions: [...new Map(rendAttacks.map((attack) => [ `${attack.enemyId}|${attack.id}|${attack.spellId}`, attack, ])).values()], enemyAuraObservations, combatAuraSamples, enemyBuffAuraApplications, mobSamples, rawCombatEvents, }; } function writeJson(filePath, value) { fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); } function main() { const instrumentedHarness = createInstrumentedHarness(); fs.writeFileSync(runtimeHarnessPath, instrumentedHarness, 'utf8'); try { const syntax = spawnSync(process.execPath, ['--check', runtimeHarnessPath], { cwd: projectRoot, encoding: 'utf8', windowsHide: true, }); if (syntax.status !== 0) throw new Error(`Instrumented campaign harness syntax check failed: ${syntax.stderr || syntax.error}`); if (prepareOnly) { console.log('Prepared scoped Stockade enemy-mechanics harness; source and transformed syntax are valid; no distribution scan, server, or browser was started.'); return; } if (!process.argv.includes('--full-stockade')) { throw new Error('This source-specific full-campaign harness requires explicit --full-stockade opt-in.'); } if (!frozenBuildAuthorized) { throw new Error('Live mode is locked until the owner authorizes the frozen build. Use --prepare-only for static preparation.'); } if (!requestedBuildDirectory) { throw new Error('Live mode requires HEALER_MAN_STOCKADE_DIST to point to the immutable task-specific distribution.'); } if (!expectedBuildSha256 || !/^[a-f0-9]{64}$/i.test(expectedBuildSha256)) { throw new Error('Live mode requires --expected-build-sha256=<64 hex chars> or HEALER_MAN_STOCKADE_DIST_SHA256.'); } if (!fs.existsSync(requestedBuildDirectory) || !fs.statSync(requestedBuildDirectory).isDirectory()) { throw new Error(`Task-specific frozen distribution is missing: ${requestedBuildDirectory}`); } const projectDistRealPath = fs.realpathSync(path.join(projectRoot, 'dist')); const requestedDistRealPath = fs.realpathSync(requestedBuildDirectory); if (requestedDistRealPath === projectDistRealPath) { throw new Error('Live mode requires the isolated frozen distribution copy, not the mutable project dist directory.'); } fs.mkdirSync(outputDirectory, { recursive: true }); const beforeManifest = collectBuildManifest(); const contentManifestBefore = collectContentManifestSnapshot(); writeJson(path.join(outputDirectory, 'build-manifest-before.json'), beforeManifest); writeJson(path.join(outputDirectory, 'content-manifest-before.json'), contentManifestBefore); const runRecord = { status: 'running', runId, startedAt: new Date().toISOString(), dungeon: { id: FULL_STOCKADE_DUNGEON_ID, title: FULL_STOCKADE_DUNGEON_TITLE }, profilePlan: { creation: 'normal offline character-creation UI', classId: 'rom-priest', secondaryClassId: 'rom-knight', isolatedLevel: 30, pairingSetup: 'existing campaign harness shell profile setup before dungeon entry' }, entryAndPlay: 'normal full Stockade picker and healer-role entry; campaign harness ordinary WASD, target selection, attack/support healing/party commands; no Boss Testing, teleport, collision bypass, enemy/damage/aura/health/cast seeding, or combat store writes', campaignHarness: path.relative(projectRoot, sourceHarnessPath), instrumentation: ['mob attack definitions', 'mob auras', 'combat auras filtered to each enemy id with definitions/owners/timestamps/stacks', 'raw observed combat events'], requestedMaximumCampaignSeconds: 1800, frozenBuildDirectory: requestedDistRealPath, expectedBuildSha256: expectedBuildSha256.toLowerCase(), readOnlyContentSourceDirectory: contentSourceDirectory, contentManifestBeforeSha256: contentManifestBefore.manifestSha256, childOutputDirectory: path.relative(projectRoot, childOutputDirectory), buildManifestBeforeSha256: beforeManifest.aggregateSha256, loadedBuildAssets: [], statusNotes: [], }; writeJson(path.join(outputDirectory, 'run-record.json'), runRecord); if (beforeManifest.aggregateSha256.toLowerCase() !== expectedBuildSha256.toLowerCase()) { runRecord.status = 'not-started-frozen-build-fingerprint-mismatch'; runRecord.executionStatus = 'browser-not-started'; runRecord.statusNotes.push('Expected aggregate SHA-256 did not match the supplied frozen distribution; no game server or browser was started.'); writeJson(path.join(outputDirectory, 'run-record.json'), runRecord); console.error(`Frozen-build fingerprint mismatch; preserved preflight evidence at ${path.relative(projectRoot, outputDirectory)}`); process.exitCode = 1; return; } const child = spawnSync(process.execPath, [runtimeHarnessPath, '--campaign-boss-route', `--route=${FULL_STOCKADE_DUNGEON_ID}`, '--max-campaign-seconds=1800'], { cwd: projectRoot, env: { ...process.env, HEALER_MAN_STOCKADE_DIST: requestedDistRealPath }, stdio: 'inherit', timeout: 2_100_000, windowsHide: true, }); const afterManifest = collectBuildManifest(); const contentManifestAfter = collectContentManifestSnapshot(); writeJson(path.join(outputDirectory, 'build-manifest-after.json'), afterManifest); writeJson(path.join(outputDirectory, 'content-manifest-after.json'), contentManifestAfter); const candidateDirectories = fs.existsSync(childOutputDirectory) ? fs.readdirSync(childOutputDirectory, { withFileTypes: true }) .filter((entry) => entry.isDirectory()).map((entry) => path.join(childOutputDirectory, entry.name)).sort() : []; const campaignDirectory = candidateDirectories.at(-1) ?? null; const routePath = campaignDirectory ? path.join(campaignDirectory, `${FULL_STOCKADE_DUNGEON_ID}-campaign-boss-route.json`) : null; const routeResultPath = campaignDirectory ? path.join(campaignDirectory, `${FULL_STOCKADE_DUNGEON_ID}-route.json`) : null; const resultsPath = campaignDirectory ? path.join(campaignDirectory, 'results.json') : null; const route = routePath && fs.existsSync(routePath) ? JSON.parse(fs.readFileSync(routePath, 'utf8')) : null; const routeResult = routeResultPath && fs.existsSync(routeResultPath) ? JSON.parse(fs.readFileSync(routeResultPath, 'utf8')) : null; const results = resultsPath && fs.existsSync(resultsPath) ? JSON.parse(fs.readFileSync(resultsPath, 'utf8')) : null; const evidence = route ? summarizeEnemyEvidence(route) : null; if (evidence) writeJson(path.join(outputDirectory, 'enemy-mechanics-evidence.json'), evidence); const failurePath = campaignDirectory ? path.join(campaignDirectory, 'failure.json') : null; const failure = failurePath && fs.existsSync(failurePath) ? JSON.parse(fs.readFileSync(failurePath, 'utf8')) : null; const contentSourceResponses = route?.contentSourceResponses ?? routeResult?.contentSourceResponses ?? failure?.contentSourceResponses ?? []; const loadedContentObjects = collectLoadedContentObjectEvidence(contentSourceResponses); const contentSourceEvidence = { sourceDirectory: contentSourceDirectory, manifestBefore: contentManifestBefore, manifestAfter: contentManifestAfter, manifestUnchanged: contentManifestBefore.manifestSha256 === contentManifestAfter.manifestSha256, responses: contentSourceResponses, loadedObjects: loadedContentObjects, counts: { manifestResponses: contentSourceResponses.filter((response) => response?.pathname === '/content/manifest.json').length, successfulManifestResponses: contentSourceResponses.filter((response) => response?.pathname === '/content/manifest.json' && response.status === 200).length, objectResponses: contentSourceResponses.filter((response) => response?.pathname?.startsWith('/content/objects/')).length, successfulObjectResponses: contentSourceResponses.filter((response) => response?.pathname?.startsWith('/content/objects/') && response.status === 200).length, verifiedHashAddressedObjects: loadedContentObjects.filter((object) => object.objectIntegrity === 'served-file-matches-hash-address').length, objectIntegrityFailures: loadedContentObjects.filter((object) => object.objectIntegrity === 'served-file-hash-mismatch' || object.objectIntegrity === 'not-found-on-content-source' || object.objectIntegrity === 'unsafe-path-refused').length, }, }; writeJson(path.join(outputDirectory, 'content-source-evidence.json'), contentSourceEvidence); runRecord.endedAt = new Date().toISOString(); runRecord.executionStatus = child.error ? 'failed-to-complete' : child.status === 0 ? 'complete' : 'failed'; runRecord.childExitCode = child.status; runRecord.childSignal = child.signal ?? null; runRecord.childError = child.error ? { message: child.error.message, code: child.error.code ?? null } : null; runRecord.campaignDirectory = campaignDirectory ? path.relative(projectRoot, campaignDirectory) : null; runRecord.campaignRoutePath = routePath && fs.existsSync(routePath) ? path.relative(projectRoot, routePath) : null; runRecord.loadedBuildAssets = results?.buildAssets ?? []; runRecord.actualAssetUrls = route?.actualAssetUrls ?? []; runRecord.campaignResultStatus = results?.status ?? null; runRecord.fullClearVerified = results?.campaignFullClearVerified ?? false; runRecord.bossProgress = route?.bossProgress ?? null; runRecord.dungeonResult = route?.dungeonResult ?? null; runRecord.enemyMechanicsEvidenceCounts = evidence?.counts ?? null; runRecord.contentManifestAfterSha256 = contentManifestAfter.manifestSha256; runRecord.contentManifestUnchanged = contentSourceEvidence.manifestUnchanged; runRecord.contentSourceResponseCounts = contentSourceEvidence.counts; runRecord.contentSourceEvidencePath = path.relative(projectRoot, path.join(outputDirectory, 'content-source-evidence.json')); runRecord.buildManifestAfterSha256 = afterManifest.aggregateSha256; runRecord.buildManifestUnchanged = beforeManifest.aggregateSha256 === afterManifest.aggregateSha256 && afterManifest.aggregateSha256.toLowerCase() === expectedBuildSha256.toLowerCase(); runRecord.pageErrors = results?.pageErrors ?? null; runRecord.consoleErrors = results?.consoleErrors ?? null; runRecord.screenshots = campaignDirectory ? fs.readdirSync(campaignDirectory).filter((file) => file.toLowerCase().endsWith('.png')) .map((file) => path.relative(projectRoot, path.join(campaignDirectory, file))).sort() : []; if (route && !evidence?.counts.chainLightningCastSamples) { runRecord.statusNotes.push('Hamhock Chain Lightning live cast was not captured; inspect target cast coverage and label inconclusive if the boss was not observed or no matching cast entered sampled state.'); } if (route && !evidence?.counts.rendAttackDefinitions && !evidence?.counts.rendDamageEvents) { runRecord.statusNotes.push('No sampled mob exposed Rend 6547/16509 or a Rend-named attack definition; label source mechanics coverage inconclusive.'); } if (route && !evidence?.counts.rendCastSamples) { runRecord.statusNotes.push(evidence?.counts.rendDamageEvents ? 'Enemy Rend 6547/16509 damage was observed in the raw combat stream; no corresponding active-cast state was sampled.' : 'No live mob Rend 6547/16509 cast entered a sampled active-cast state; attack definitions alone do not establish a cast.'); } if (route && !evidence?.counts.enemyAuraObservations && evidence?.counts.enemyBuffAuraApplications) { runRecord.statusNotes.push(`Mob aura snapshots remained empty, but ${evidence.counts.enemyBuffAuraApplications} unique enemy-source buff applications were captured from combat aura telemetry.`); } else if (route && !evidence?.counts.enemyAuraObservations && !evidence?.counts.enemyBuffAuraApplications) { runRecord.statusNotes.push('No non-empty mob aura snapshot or enemy-source buff application was observed; report enemy-buff outcome as not observed.'); } if (!runRecord.buildManifestUnchanged) { runRecord.statusNotes.push('The frozen distribution fingerprint changed during the run; preserve the evidence but do not attribute it to one immutable build.'); } if (!runRecord.contentManifestUnchanged) { runRecord.statusNotes.push('The read-only content manifest changed during the run; content source provenance is inconclusive.'); } runRecord.status = runRecord.executionStatus === 'complete' && route ? 'complete' : 'failed'; if (!runRecord.buildManifestUnchanged && runRecord.status === 'complete') runRecord.status = 'inconclusive-build-mutated'; writeJson(path.join(outputDirectory, 'run-record.json'), runRecord); console.log(`Stockade enemy-mechanics QA ${runRecord.status}: ${path.relative(projectRoot, outputDirectory)}`); if (runRecord.status !== 'complete') process.exitCode = child.status || 1; } finally { fs.rmSync(runtimeHarnessPath, { force: true }); } } try { main(); } catch (error) { fs.rmSync(runtimeHarnessPath, { force: true }); console.error(error.stack ?? error.message); process.exitCode = 1; }