Files
healer-man/scripts/playtest-stockade-stances-blocking.cjs
T
2026-10-06 22:25:34 -04:00

833 lines
58 KiB
JavaScript

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