| 1 | import { attributeRetention, evidenceIntegrity, retainedCohorts, screeningBlockers } from "./app-memory-evidence.mjs"; |
| 2 | |
| 3 | const COMMON_PROTOCOL = Object.freeze({ version: 4, hydration: "async-task", pointerRest: Object.freeze([0, 0]), viewport: Object.freeze({ width: 1440, height: 1000 }) }); |
| 4 | export const MEMORY_PROTOCOLS = Object.freeze({ |
| 5 | full: Object.freeze({ ...COMMON_PROTOCOL, profile: "full", shards: 3, cycles: 128, mixedCycles: 512 }), |
| 6 | short: Object.freeze({ ...COMMON_PROTOCOL, profile: "short", shards: 1, cycles: 32, mixedCycles: 128 }), |
| 7 | }); |
| 8 | export const MEMORY_PROTOCOL = MEMORY_PROTOCOLS.full; |
| 9 | export const MEMORY_FIXTURES = Object.freeze({ |
| 10 | full: { label: "bench:small-6t", marker: "ASYNC LAYOUT EXPANSION COMPLETE" }, |
| 11 | geometry: { label: "bench:geometry", marker: "Geometry contract fixture complete." }, |
| 12 | windowed: { label: "bench:windowed-1000t", marker: "Windowed turn 1000" }, |
| 13 | }); |
| 14 | const identityFields = ["sourceSHA", "trackedDiffSHA256", "untrackedSourceSHA256", "buildSHA256", "node", "platform", "arch"]; |
| 15 | |
| 16 | export function memoryProtocol(profile = "full") { |
| 17 | const protocol = MEMORY_PROTOCOLS[profile]; |
| 18 | if (!protocol) throw new Error(`unknown App memory profile: ${profile}`); |
| 19 | return protocol; |
| 20 | } |
| 21 | |
| 22 | function registeredProtocol(value) { |
| 23 | return Object.values(MEMORY_PROTOCOLS).find(protocol => JSON.stringify(value) === JSON.stringify(protocol)); |
| 24 | } |
| 25 | |
| 26 | export function verifyIdentity(actual, expected) { |
| 27 | if (!actual || !expected || actual.sourceStatus !== "" || expected.sourceStatus !== "") throw new Error("memory evidence requires a clean source checkout"); |
| 28 | for (const field of identityFields) { |
| 29 | if (typeof actual[field] !== "string" || !actual[field] || actual[field] !== expected[field]) throw new Error(`memory identity mismatch: ${field}`); |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | export function protocolSamples(samples, protocol = MEMORY_PROTOCOL) { |
| 34 | const expected = [["baseline", 0]]; |
| 35 | for (const phase of ["full", "windowed", "safety", "mixed"]) { |
| 36 | const count = phase === "mixed" ? protocol.mixedCycles : protocol.cycles; |
| 37 | for (let round = 32; round <= count; round += 32) expected.push([phase, round]); |
| 38 | } |
| 39 | expected.push(["settled", protocol.mixedCycles]); |
| 40 | return Array.isArray(samples) && samples.length === expected.length |
| 41 | && samples.every((sample, index) => sample.phase === expected[index][0] && sample.roundTrips === expected[index][1]); |
| 42 | } |
| 43 | |
| 44 | export function completeShard(report, expectedProtocol = report.protocol) { |
| 45 | const protocol = registeredProtocol(expectedProtocol); |
| 46 | const run = report.processes?.[0]; |
| 47 | return Boolean(protocol) && JSON.stringify(report.protocol) === JSON.stringify(protocol) |
| 48 | && report.cycles === protocol.cycles && report.mixedCycles === protocol.mixedCycles |
| 49 | && report.processes?.length === 1 && run.process === report.shard?.id |
| 50 | && protocolSamples(run.samples, protocol) |
| 51 | && Array.isArray(run.snapshots) && run.snapshots.length === 5 |
| 52 | && ["baseline", "full", "windowed", "safety", "mixed"].every((phase, index) => |
| 53 | run.snapshots[index].file === `${run.process}-${phase}.heapsnapshot` && run.snapshots[index].summary); |
| 54 | } |
| 55 | |
| 56 | export function aggregateShards(reports, manifest, sourceSHA) { |
| 57 | const protocol = registeredProtocol(manifest.protocol); |
| 58 | if (!protocol || manifest.identity?.sourceSHA !== sourceSHA || !manifest.executionId) throw new Error("invalid memory build manifest"); |
| 59 | if (!Array.isArray(reports) || reports.length !== protocol.shards) throw new Error(`${protocol.shards} complete independent memory shard(s) required for ${protocol.profile} screening`); |
| 60 | const seen = new Set(); |
| 61 | const processes = []; |
| 62 | let browser; |
| 63 | for (const report of reports) { |
| 64 | const id = report.shard?.id; |
| 65 | if (!Number.isInteger(id) || id < 1 || id > protocol.shards || seen.has(id)) throw new Error("duplicate or invalid memory shard"); |
| 66 | seen.add(id); |
| 67 | if (report.shard.executionId !== manifest.executionId || report.shard.total !== protocol.shards) throw new Error("memory shard belongs to another workflow attempt"); |
| 68 | verifyIdentity(report.identity, manifest.identity); |
| 69 | if (JSON.stringify(report.fixtures) !== JSON.stringify(MEMORY_FIXTURES)) throw new Error("memory fixture configuration differs"); |
| 70 | if (report.failure || report.verdict !== "SHARD_PASS" || !report.shardComplete || !completeShard(report, protocol)) throw new Error(`incomplete memory shard ${id}`); |
| 71 | const run = report.processes[0]; |
| 72 | if (!run.browser || (browser && browser !== run.browser)) throw new Error("memory browser versions differ"); |
| 73 | browser = run.browser; |
| 74 | const cohorts = retainedCohorts(run.samples); |
| 75 | const attribution = attributeRetention(run.samples, cohorts); |
| 76 | if (!evidenceIntegrity(run.samples) || !run.samples.every(sample => sample.lifecycle.activeOperations === 0) |
| 77 | || !["evidenceIntegrity", "instrumentedOperationsReleased", "noPageErrors"].every(key => run.checks?.[key] === true) |
| 78 | || !Array.isArray(run.metrics?.pageErrors) || run.metrics.pageErrors.length !== 0 |
| 79 | || screeningBlockers(attribution.reasons).length !== 0) throw new Error(`memory screening failed in shard ${id}`); |
| 80 | processes.push({ ...run, cohorts, attribution }); |
| 81 | } |
| 82 | return { identity: manifest.identity, executionId: manifest.executionId, protocol, |
| 83 | screeningLevel: protocol.profile, protocolComplete: true, verdict: "PASS", attribution: "pending", |
| 84 | processes: processes.sort((a, b) => a.process - b.process) }; |
| 85 | } |
| 86 |