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-stances-blocking', runId); const childOutputDirectory = path.join(outputDirectory, 'campaign'); const runtimeHarnessPath = path.join(__dirname, `.playtest-stockade-stances-blocking-${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 replaceFirstOccurrence(source, marker, replacement, label) { if (!source.includes(marker)) throw new Error(`${label} marker was not found in the source harness.`); 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'); transformed = replaceExactlyOnce(transformed, " combatAuras: (combatState.auras ?? []).filter((aura) => aura.definition?.id === 'rom-priest-vow-of-silence')\n .map((aura) => ({ instanceId: aura.instanceId, definitionId: aura.definition.id,\n sourceId: aura.sourceId, targetId: aura.targetId, appliedAt: aura.appliedAt,\n expiresAt: aura.expiresAt })),", " combatAuras: (combatState.auras ?? []).filter((aura) => aura.definition?.id === 'rom-priest-vow-of-silence')\n .map((aura) => ({ instanceId: aura.instanceId, definitionId: aura.definition.id,\n sourceId: aura.sourceId, targetId: aura.targetId, appliedAt: aura.appliedAt,\n expiresAt: aura.expiresAt })),\n stanceBlockingAuraTelemetry: (combatState.auras ?? []).filter((aura) => /^enemy-source-spell-(?:7164|7165|3419)$/.test(aura.definition?.id ?? '')).map((aura) => ({\n instanceId: aura.instanceId ?? null, definitionId: aura.definition?.id ?? null,\n definitionName: aura.definition?.name ?? null, sourceId: aura.sourceId ?? null,\n targetId: aura.targetId ?? null, appliedAt: aura.appliedAt ?? null,\n expiresAt: aura.expiresAt ?? null, stacks: aura.stacks ?? null,\n })),", 'stance and Improved Blocking aura telemetry'); transformed = replaceExactlyOnce(transformed, " partySelection: {\n selectedMemberId: partyState.selectedMemberId ?? null,\n selfSelected: Boolean(partyState.selfSelected),\n },", " partySelection: {\n selectedMemberId: partyState.selectedMemberId ?? null,\n selfSelected: Boolean(partyState.selfSelected),\n },\n partyCommand: partyState.command ?? null,", 'read-only party command sampling'); transformed = replaceExactlyOnce(transformed, " classId: combatState.classId,\n secondaryClassId: combatState.secondaryClassId,\n level: combatState.level,", " classId: combatState.classId,\n secondaryClassId: combatState.secondaryClassId,\n specializationId: combatState.specializationId ?? null,\n level: combatState.level,", 'player specialization telemetry'); transformed = replaceExactlyOnce(transformed, " activeCast: combatState.activeCast?.abilityId ?? null,", " activeCast: combatState.activeCast?.abilityId ?? null,\n lastCombatResult: combatState.lastCombatResult ? { ...combatState.lastCombatResult } : null,", 'read-only last combat result telemetry'); transformed = replaceExactlyOnce(transformed, " classId: member.classId,\n secondaryClassId: member.secondaryClassId,\n role: member.role,", " classId: member.classId,\n secondaryClassId: member.secondaryClassId,\n specializationId: member.specializationId ?? null,\n role: member.role,", 'party specialization telemetry'); 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, " await page.locator('.creator-category-tabs button').filter({ hasText: 'RoM' }).click();\n await page.locator('.class-option').filter({ hasText: /^Priest/ }).click();", " await page.locator('.creator-category-tabs button').filter({ hasText: 'WoW' }).click();\n await page.locator('.class-option').filter({ hasText: /^Priest/ }).click();\n await page.locator('.creator-specializations button').filter({ hasText: 'Discipline' }).click();", 'normal WoW Discipline Priest character creation'); transformed = replaceExactlyOnce(transformed, " if (!character || character.classId !== 'rom-priest') throw new Error('UI-created RoM Priest profile is missing.');\n shell.setState({ activeCharacter: character });\n shell.getState().saveActiveProgression(30, 0, character.talentRanks);\n shell.setState({ activeCharacter: null });\n if (!shell.getState().chooseRomSecondaryClass('rom-knight')) throw new Error('Priest/Knight pairing was not accepted.');\n const updated = shell.getState().characters.find((entry) => entry.id === character.id);\n return { id: updated.id, name: updated.name, classId: updated.classId,\n secondaryClassId: updated.secondaryClassId, level: updated.level, categoryId: updated.categoryId };", " if (!character || character.classId !== 'priest' || character.specializationId !== 'priest:discipline') {\n throw new Error('UI-created WoW Discipline Priest profile is missing.');\n }\n shell.setState({ activeCharacter: character });\n shell.getState().saveActiveProgression(30, 0, character.talentRanks);\n shell.setState({ activeCharacter: null });\n const updated = shell.getState().characters.find((entry) => entry.id === character.id);\n return { id: updated.id, name: updated.name, classId: updated.classId, specializationId: updated.specializationId,\n secondaryClassId: updated.secondaryClassId, level: updated.level, categoryId: updated.categoryId };", 'profile creation and level setup'); transformed = replaceExactlyOnce(transformed, " assert.equal(profile.classId, 'rom-priest');\n assert.equal(profile.secondaryClassId, 'rom-knight');", " assert.equal(profile.classId, 'priest');\n assert.equal(profile.specializationId, 'priest:discipline');\n assert.equal(profile.level, 30);", 'Discipline profile assertions'); transformed = transformed.replaceAll('roster-priest-knight-level-30.png', 'roster-discipline-priest-level-30.png') .replaceAll('roster-compact-priest-knight-level-30.png', 'roster-compact-discipline-priest-level-30.png'); transformed = replaceExactlyOnce(transformed, " assert.equal(state.player.classId, 'rom-priest');\n assert.equal(state.player.secondaryClassId, 'rom-knight');\n assert(state.player.level >= minimumLevel,\n `${route.title} entry level remains at least the UI-created profile's level ${minimumLevel}`);\n assert.equal(state.party.length, 4, 'the UI-created Priest has four NPC companions');\n assert(state.party.every((member) => member.classId.startsWith('rom-') && member.statRuleset === 'rom'),\n 'all four companions use native RoM identities and rules');", " assert.equal(state.player.classId, 'priest');\n assert.equal(state.player.specializationId, 'priest:discipline');\n assert(state.player.level >= minimumLevel,\n `${route.title} entry level remains at least the UI-created profile's level ${minimumLevel}`);\n assert.equal(state.party.length, 4, 'the UI-created Discipline Priest has four NPC companions');\n const readySpecializationIds = new Set(['warrior:protection', 'priest:discipline',\n 'hunter:marksmanship', 'mage:fire', 'rogue:assassination']);\n assert(state.party.every((member) => readySpecializationIds.has(member.specializationId)),\n `all four companions use Ready WoW specializations: ${JSON.stringify(state.party.map((member) => member.specializationId))}`);", 'Ready WoW companion assertions'); 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, " activeCast: mob.activeCast, attacks: mob.attacks ?? [], auras: mob.auras ?? [],\n combatAuraTelemetry: mob.combatAuraTelemetry ?? [], abilityCastCounts: mob.abilityCastCounts ?? {},\n statuses: mob.statuses ?? [], 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 })),\n stanceBlockingAuraTelemetry: current.stanceBlockingAuraTelemetry ?? [] });", 'compact route sample stance and blocking telemetry'); transformed = replaceExactlyOnce(transformed, ' movement: [], targeting: [], actions: [], samples: [], observedCombatEvents: [],', ' movement: [], targeting: [], actions: [], samples: [], observedCombatEvents: [],\n observedStanceBlockingAuraObservations: [], observedBlockResultObservations: [],', 'stance and block result observations'); transformed = replaceExactlyOnce(transformed, ' const seenEventKeys = initialEventKeys;\n const observedDeadMobs = new Map();', ' const seenEventKeys = initialEventKeys;\n const observedStanceBlockingAurasById = new Map();\n const seenBlockResultKeys = new Set();\n const observedDeadMobs = new Map();', 'read-only observation deduplication state'); transformed = replaceExactlyOnce(transformed, ' const collect = (snapshot) => {', ` let lastProgressSnapshotAt = 0; let progressSnapshotCount = 0; const recordSnapshot = (snapshot) => { if (lastProgressSnapshotAt && snapshot.now - lastProgressSnapshotAt < 30_000) return; lastProgressSnapshotAt = snapshot.now; progressSnapshotCount += 1; const checkpointIndex = result.activeNavigationCheckpointIndex ?? null; const checkpoint = Number.isInteger(checkpointIndex) ? result.navigationGuidance.checkpoints[checkpointIndex] ?? null : null; const killedBossIds = snapshot.dungeonRun?.killedBossIds ?? []; const killedBossIdSet = new Set(killedBossIds); const objectiveCount = result.objectives.length; const clearedObjectiveIds = result.objectives.filter((objective) => killedBossIdSet.has(objective.id)) .map((objective) => objective.id); const mobs = snapshot.mobs.map((mob) => ({ id: mob.id, name: mob.name, health: mob.health, maxHealth: mob.maxHealth, dead: mob.dead, engaged: mob.engaged, boss: mob.boss ?? false, activeCast: mob.activeCast, position: mob.position })); const progress = { snapshotNumber: progressSnapshotCount, updatedAt: new Date(snapshot.now).toISOString(), elapsedMs: snapshot.now - result.segmentStartedAt, checkpoint: checkpoint ? { index: checkpointIndex, total: result.navigationGuidance.checkpoints.length, routePercent: Math.floor((checkpointIndex + 1) / result.navigationGuidance.checkpoints.length * 100), objectiveId: checkpoint.objectiveId ?? null, name: checkpoint.name ?? null, position: checkpoint.position ?? null, distance3d: distance3(snapshot.player.position, checkpoint.position) } : null, position: snapshot.player.position, player: { classId: snapshot.player.classId, specializationId: snapshot.player.specializationId, health: snapshot.player.health, maxHealth: snapshot.player.maxHealth, mana: snapshot.player.resource, maxMana: snapshot.player.maxResource, resourceType: snapshot.player.resourceType, resourceName: snapshot.player.resourceName, activeCast: snapshot.player.activeCast, selectedTargetId: snapshot.player.selectedTargetId, controlMechanic: snapshot.player.controlMechanic, actionBlockedByControl: snapshot.player.actionBlockedByControl }, party: snapshot.party.map((member) => ({ id: member.id, name: member.name, specializationId: member.specializationId ?? null, role: member.role, health: member.health, maxHealth: member.maxHealth, status: member.status, activeCast: member.activeCast, position: member.position })), deaths: { player: result.playerDeathTransitions.length, party: partyCasualties.length, partyMembers: partyCasualties.map((casualty) => ({ id: casualty.id, name: casualty.name, observedAt: casualty.observedAt, recoveredAt: casualty.recoveredAt })) }, partyCommand: { current: snapshot.partyCommand ?? null, recordedInputs: [result.partyCommand, result.partyCommandAfterOpeningTarget, ...(result.partyCommands ?? [])].filter(Boolean) }, mobs: { liveCount: mobs.filter((mob) => !mob.dead && mob.health > 0).length, deadCount: mobs.filter((mob) => mob.dead || mob.health <= 0).length, engagedCount: mobs.filter((mob) => mob.engaged && !mob.dead && mob.health > 0).length, entries: mobs }, objectives: { count: objectiveCount, clearedCount: clearedObjectiveIds.length, clearedIds: clearedObjectiveIds, killedBossIds }, recentEvents: (snapshot.events ?? []).slice(-20), recentMovement: result.movement.slice(-4).map((step) => ({ at: step.at, from: step.from, to: step.to, planarDisplacement: step.planarDisplacement, requestedDurationMs: step.requestedDurationMs, durationMs: step.durationMs })), lastRecovery: result.playerRecoveryActions.at(-1) ?? null, }; const progressPath = process.env.HEALER_MAN_STOCKADE_PROGRESS_PATH ?? path.join(outputDirectory, 'progress.json'); fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2)); }; const collect = (snapshot) => {`, 'periodic read-only live progress snapshots'); transformed = replaceFirstOccurrence(transformed, ' result.observedCombatEvents.push(event);\n }\n };', ` result.observedCombatEvents.push(event);\n }\n for (const aura of snapshot.stanceBlockingAuraTelemetry ?? []) {\n const auraKey = [aura.instanceId ?? '', aura.appliedAt ?? '', aura.definitionId ?? '',\n aura.sourceId ?? '', aura.targetId ?? ''].join('|');\n const existing = observedStanceBlockingAurasById.get(auraKey);\n if (existing) existing.lastObservedAt = snapshot.now;\n else {\n const observation = { firstObservedAt: snapshot.now, lastObservedAt: snapshot.now, ...aura };\n observedStanceBlockingAurasById.set(auraKey, observation);\n result.observedStanceBlockingAuraObservations.push(observation);\n }\n }\n const lastCombatResult = snapshot.player.lastCombatResult;\n if (lastCombatResult?.outcome === 'block') {\n const latestDamageEvent = [...snapshot.events].reverse().find((event) => event.kind === 'damage');\n const matchedLatestBlockEvent = latestDamageEvent?.outcome === 'block'\n && Number(latestDamageEvent.rawAmount) === Number(lastCombatResult.rawDamage)\n ? latestDamageEvent : null;\n const resultKey = matchedLatestBlockEvent?.id != null ? 'event:' + matchedLatestBlockEvent.id\n : 'unmatched:' + JSON.stringify(lastCombatResult);\n if (!seenBlockResultKeys.has(resultKey)) {\n seenBlockResultKeys.add(resultKey);\n result.observedBlockResultObservations.push({ observedAt: snapshot.now,\n correlation: matchedLatestBlockEvent ? 'latest-damage-event-outcome-and-raw-amount-match' : 'no-matching-latest-damage-event-sampled',\n event: matchedLatestBlockEvent, combatResult: { ...lastCombatResult } });\n }\n }\n };`, 'read-only block result collection'); transformed = replaceExactlyOnce(transformed, '\n };\n const deadline = Date.now() + maxCampaignDurationMs;', '\n recordSnapshot(snapshot);\n };\n const deadline = Date.now() + maxCampaignDurationMs;', 'write periodic route observations'); transformed = replaceExactlyOnce(transformed, ' const firstTarget = await selectHostileWithTab(page, result, 8);', ' collect(entryState);\n const firstTarget = await selectHostileWithTab(page, result, 8);', 'entry aura and combat-state snapshot'); 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 summarizeStanceBlockingEvidence(route) { const partyActors = new Map(); const hostileActors = new Map(); const partyName = route.profile?.name ?? route.playerAtEntry?.name ?? 'Player'; partyActors.set('__player__', { id: '__player__', name: partyName, role: 'healer', actorKind: 'player' }); function addPartyActor(actor) { if (!actor?.id) return; partyActors.set(actor.id, { id: actor.id, name: actor.name ?? null, role: actor.role ?? null, classId: actor.classId ?? null, specializationId: actor.specializationId ?? null, actorKind: 'companion' }); } function addHostileActor(actor) { if (!actor?.id) return; hostileActors.set(actor.id, { id: actor.id, name: actor.name ?? null, serverEntry: actor.serverEntry ?? null, actorKind: 'hostile-mob' }); } function visitActorCollections(value) { if (!value || typeof value !== 'object') return; if (Array.isArray(value)) { value.forEach(visitActorCollections); return; } if (Array.isArray(value.party)) value.party.forEach(addPartyActor); if (Array.isArray(value.mobs)) value.mobs.forEach(addHostileActor); for (const child of Object.values(value)) visitActorCollections(child); } route.partyAtEntry?.forEach(addPartyActor); visitActorCollections(route.samples ?? []); visitActorCollections(route.packEncounters ?? []); visitActorCollections(route.bossEncounters ?? []); const actorName = (id) => partyActors.get(id)?.name ?? hostileActors.get(id)?.name ?? null; const actorSide = (id) => partyActors.has(id) ? 'party' : hostileActors.has(id) ? 'hostile' : 'unresolved'; const sampledAuraObservations = (route.samples ?? []).flatMap((sample) => (sample.stanceBlockingAuraTelemetry ?? []).map((aura) => ({ ...aura, firstObservedAt: sample.at, lastObservedAt: sample.at, }))); const auraApplicationsById = new Map(); for (const aura of [...(route.observedStanceBlockingAuraObservations ?? []), ...sampledAuraObservations]) { const spellIdMatch = /(?:^|-)(7164|7165|3419)$/.exec(String(aura.definitionId ?? '')); const spellId = spellIdMatch ? Number(spellIdMatch[1]) : null; if (![7164, 7165, 3419].includes(spellId)) continue; const applicationId = [aura.instanceId ?? '', aura.appliedAt ?? '', aura.definitionId ?? '', aura.sourceId ?? '', aura.targetId ?? ''].join('|'); const current = auraApplicationsById.get(applicationId); if (!current || Number(aura.lastObservedAt ?? 0) > Number(current.lastObservedAt ?? 0)) { auraApplicationsById.set(applicationId, { ...aura, spellId }); } } const auraApplications = [...auraApplicationsById.values()].map((aura) => ({ instanceId: aura.instanceId ?? null, spellId: aura.spellId, definitionId: aura.definitionId ?? null, name: aura.definitionName ?? (aura.spellId === 7164 ? 'Defensive Stance' : aura.spellId === 7165 ? 'Battle Stance' : 'Improved Blocking'), sourceActorId: aura.sourceId ?? null, sourceActorName: actorName(aura.sourceId), sourceActorSide: actorSide(aura.sourceId), targetActorId: aura.targetId ?? null, targetActorName: actorName(aura.targetId), targetActorSide: actorSide(aura.targetId), appliedAt: aura.appliedAt ?? null, expiresAt: aura.expiresAt ?? null, stacks: aura.stacks ?? null, firstObservedAt: aura.firstObservedAt ?? null, lastObservedAt: aura.lastObservedAt ?? null, telemetry: 'active combatStore aura state from collector observations and route samples, deduplicated by instanceId + appliedAt', })); const stanceAuraApplications = auraApplications.filter((aura) => aura.spellId === 7164 || aura.spellId === 7165); const improvedBlockingAuraApplications = auraApplications.filter((aura) => aura.spellId === 3419); const blockResultByEventId = new Map(); const unmatchedBlockResultObservations = []; for (const observation of route.observedBlockResultObservations ?? []) { const eventId = observation.event?.id; if (eventId != null) blockResultByEventId.set(eventId, observation); else unmatchedBlockResultObservations.push(observation); } const rawBlockEvents = (route.observedCombatEvents ?? []).filter((event) => event.kind === 'damage' && event.outcome === 'block'); const blockEvents = rawBlockEvents.map((event) => { const sourceActorSide = actorSide(event.sourceActorId); const targetActorSide = actorSide(event.targetActorId); const direction = sourceActorSide === 'hostile' && targetActorSide === 'party' ? 'hostile-attack-blocked-by-party' : sourceActorSide === 'party' && targetActorSide === 'hostile' ? 'party-attack-blocked-by-hostile' : 'actor-team-unresolved'; const resultObservation = blockResultByEventId.get(event.id) ?? null; return { eventId: event.id ?? null, occurredAt: event.occurredAt ?? null, direction, sourceActorId: event.sourceActorId ?? null, sourceActorName: actorName(event.sourceActorId), sourceActorSide, targetActorId: event.targetActorId ?? null, targetActorName: actorName(event.targetActorId), targetActorSide, abilityId: event.abilityId ?? null, school: event.school ?? null, outcome: event.outcome ?? null, rawAmount: event.rawAmount ?? null, effectiveAmount: event.effectiveAmount ?? null, absorbed: event.absorbed ?? null, blockedAmount: resultObservation?.combatResult?.blockedAmount ?? null, blockResultCorrelation: resultObservation?.correlation ?? 'event outcome only; no matched lastCombatResult amount sample', lastCombatResult: resultObservation?.combatResult ?? null, }; }); const partyDefenseBlockEvents = blockEvents.filter((event) => event.direction === 'hostile-attack-blocked-by-party'); const partyAttackBlockedEvents = blockEvents.filter((event) => event.direction === 'party-attack-blocked-by-hostile'); return { definitions: { stanceAuraApplications: 'Unique active aura applications for source spells 7164 Defensive Stance and 7165 Battle Stance, keyed by aura instance id; application/source/target timestamps and expiry are preserved.', improvedBlockingAuraApplications: 'Unique active aura applications for source spell 3419 Improved Blocking, keyed by aura instance id plus appliedAt and reconstructed from collector observations plus route samples.', blockEvents: 'Raw read-only combatStore damage events with outcome=block. Direction is classified from party actor IDs and sampled hostile mob IDs.', blockedAmount: 'Copied from sampled lastCombatResult only when its outcome and raw damage amount match the latest damage event in that snapshot; unmatched block outcomes remain recorded with blockedAmount=null.', samplingLimit: 'Aura telemetry and lastCombatResult are sampled when the campaign harness reads live state; short intervals may be missed if they begin and expire between reads.', }, counts: { stanceAuraApplications: stanceAuraApplications.length, defensiveStanceApplications: stanceAuraApplications.filter((aura) => aura.spellId === 7164).length, battleStanceApplications: stanceAuraApplications.filter((aura) => aura.spellId === 7165).length, improvedBlockingAuraApplications: improvedBlockingAuraApplications.length, rawBlockEvents: rawBlockEvents.length, partyDefenseBlockEvents: partyDefenseBlockEvents.length, partyAttackBlockedEvents: partyAttackBlockedEvents.length, actorTeamUnresolvedBlockEvents: blockEvents.filter((event) => event.direction === 'actor-team-unresolved').length, matchedBlockedAmountEvents: blockEvents.filter((event) => event.blockedAmount !== null).length, unmatchedBlockResultObservations: unmatchedBlockResultObservations.length, }, partyActors: [...partyActors.values()], hostileActors: [...hostileActors.values()], auraApplications, stanceAuraApplications, improvedBlockingAuraApplications, blockEvents, unmatchedBlockResultObservations, rawBlockEvents, }; } 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 stance/blocking 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 WoW character-creation UI', classId: 'priest', specializationId: 'priest:discipline', role: 'healer', level: 30, companions: 'four NPC companions, each asserted against the five Ready specialization IDs' }, entryAndPlay: 'normal full Stockade picker and Discipline 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: ['active enemy-source aura applications for spells 7164/7165/3419 with owner, target, apply/expiry, and stack fields', 'raw observed combat events with block outcomes and actor direction', 'read-only lastCombatResult block amount snapshots correlated to matching damage events', 'read-only live-state progress.json snapshots at 30-second intervals'], requestedMaximumCampaignSeconds: 1800, frozenBuildDirectory: requestedDistRealPath, expectedBuildSha256: expectedBuildSha256.toLowerCase(), readOnlyContentSourceDirectory: contentSourceDirectory, contentManifestBeforeSha256: contentManifestBefore.manifestSha256, childOutputDirectory: path.relative(projectRoot, childOutputDirectory), progressFile: path.relative(projectRoot, path.join(outputDirectory, 'progress.json')), progressSnapshotIntervalSeconds: 30, 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, HEALER_MAN_STOCKADE_PROGRESS_PATH: path.join(outputDirectory, 'progress.json') }, 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 ? summarizeStanceBlockingEvidence(route) : null; if (evidence) writeJson(path.join(outputDirectory, 'stances-blocking-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; const expectedBossIds = route?.bossProgress?.orderedBossIds ?? []; const killedBossIds = new Set(route?.bossProgress?.latestKilledBossIds ?? []); const fiveRequiredBossesVerified = expectedBossIds.length === 5 && expectedBossIds.every((bossId) => killedBossIds.has(bossId)); const fullClearRequirementsVerified = Boolean(results?.status === 'verified-full-clear' && results?.campaignFullClearVerified === true && route?.fullClearStatus === 'native-full-dungeon-clear-and-all-required-killed-boss-ids-observed' && route?.dungeonResult?.fullClearVisible === true && route?.dungeonResult?.requiredKilledBossIdsComplete === true && fiveRequiredBossesVerified); runRecord.campaignHarnessFullClearVerified = results?.campaignFullClearVerified ?? false; runRecord.fullClearRequirementsVerified = fullClearRequirementsVerified; runRecord.requiredBossIds = expectedBossIds; runRecord.killedBossIds = [...killedBossIds]; runRecord.fullClearVerified = fullClearRequirementsVerified; runRecord.bossProgress = route?.bossProgress ?? null; runRecord.dungeonResult = route?.dungeonResult ?? null; runRecord.stanceBlockingEvidenceCounts = 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.stanceAuraApplications) { runRecord.statusNotes.push('No live application of Defensive Stance 7164 or Battle Stance 7165 was captured; aura outcomes are not observed in this run.'); } if (route && !evidence?.counts.improvedBlockingAuraApplications) { runRecord.statusNotes.push('No live Improved Blocking 3419 application was captured; aura outcomes are not observed in this run.'); } if (route && !evidence?.counts.rawBlockEvents) { runRecord.statusNotes.push('No damage event with outcome=block was captured; intermittent live sampling limits negative conclusions.'); } else if (route && evidence?.counts.matchedBlockedAmountEvents < evidence?.counts.rawBlockEvents) { runRecord.statusNotes.push(`${evidence.counts.rawBlockEvents - evidence.counts.matchedBlockedAmountEvents} block outcome event(s) had no matching sampled lastCombatResult amount; the outcome is retained without inferring a blocked amount.`); } if (route && evidence?.counts.actorTeamUnresolvedBlockEvents) { runRecord.statusNotes.push(`${evidence.counts.actorTeamUnresolvedBlockEvents} block event(s) could not be assigned to party-versus-hostile direction from sampled actor IDs.`); } if (!runRecord.fullClearVerified && runRecord.executionStatus === 'complete' && route) { runRecord.statusNotes.push('Campaign process exited successfully, but results UI plus all five required boss IDs were not verified; this remains a partial live attempt.'); } 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 ? runRecord.fullClearVerified ? 'complete' : 'partial-full-clear-unverified' : 'failed'; if (!runRecord.buildManifestUnchanged && runRecord.status === 'complete') runRecord.status = 'inconclusive-build-mutated'; writeJson(path.join(outputDirectory, 'run-record.json'), runRecord); console.log(`Stockade stance/blocking 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; }