#!/usr/bin/env node const assert = require("assert"); const fs = require("fs"); const path = require("path"); const repo = path.resolve(__dirname, ".."); const serviceEndpoint = "https://disasmer.michelpaulissen.com"; const serviceHost = "disasmer.michelpaulissen.com"; const forgejoHost = "git.michelpaulissen.com"; function read(relativePath) { return fs.readFileSync(path.join(repo, relativePath), "utf8"); } function maybeRead(relativePath) { const file = path.join(repo, relativePath); return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null; } function expect(source, name, pattern) { assert(source !== null && source !== undefined, `missing source for ${name}`); assert.match(source, pattern, `missing public release dry-run evidence: ${name}`); } const phase2 = maybeRead("acceptance_criteria_phase2.md"); const readme = maybeRead("README.md"); const cliSource = [ read("crates/disasmer-cli/src/main.rs"), read("crates/disasmer-cli/src/config.rs"), read("crates/disasmer-cli/src/run.rs"), read("crates/disasmer-cli/src/client.rs"), read("crates/disasmer-cli/src/tests.rs"), ].join("\n"); const controlSource = read("crates/disasmer-control/src/lib.rs"); const cliLoginSmoke = read("scripts/cli-login-smoke.js"); const vscodePackage = JSON.parse(read("vscode-extension/package.json")); const vscodeExtension = read("vscode-extension/extension.js"); const vscodeSmoke = read("scripts/vscode-extension-smoke.js"); const prepScript = read("scripts/prepare-public-release-dryrun.js"); const publishScript = read("scripts/publish-public-release-dryrun.js"); const preflightScript = read("scripts/public-release-dryrun-preflight.js"); const e2eScript = read("scripts/public-release-dryrun-e2e.js"); const finalEvidenceScript = read("scripts/public-release-dryrun-final-evidence.js"); const workflow = maybeRead(".forgejo/workflows/public-release-dryrun.yml"); const publicAcceptance = read("scripts/acceptance-public.sh"); const privateAcceptance = read("scripts/acceptance-private.sh"); const publicSplit = read("scripts/verify-public-split.sh"); if (readme) { expect(readme, "README", new RegExp(serviceHost.replaceAll(".", "\\."))); } if (phase2) { const name = "phase 2 criteria"; expect(phase2, name, new RegExp(serviceHost.replaceAll(".", "\\."))); expect(phase2, `${name} DNS publication state`, /DNS record|public DNS|dns[-_]publication/i); expect(phase2, `${name} resolver fallback`, /no resolver override|required resolver override|hosts entry|controlled resolution|fallback/i); expect(phase2, `${name} Forgejo host`, new RegExp(forgejoHost.replaceAll(".", "\\."))); expect(phase2, `${name} Forgejo Release`, /Forgejo Release/); expect(phase2, `${name} compiled assets`, /compiled\s+(release\s+)?assets|compiled release assets/); expect(phase2, `${name} filtered tree`, /private\/\*\*[\s\S]*experiments\/\*\*/); expect(phase2, `${name} root Markdown export filter`, /root `\*\.md`|root Markdown/); expect(phase2, `${name} host-neutral workflow filter`, /\.forgejo[\s\S]*--include-forgejo-workflows/); expect(phase2, `${name} GitHub release out of scope`, /GitHub[\s\S]*outside this dry run|public GitHub-release[\s\S]*out of scope/); } expect(cliSource, "CLI default hosted coordinator constant", new RegExp(`DEFAULT_HOSTED_COORDINATOR_ENDPOINT: &str =[\\s\\S]*"${serviceEndpoint.replaceAll(".", "\\.")}"`)); expect(cliSource, "login uses default hosted coordinator", /default_value_t = default_hosted_coordinator_endpoint\(\)/); expect(cliSource, "hosted run records coordinator endpoint", /hosted_coordinator_endpoint[\s\S]*Some\(default_hosted_coordinator_endpoint\(\)\)/); expect(controlSource, "hosted URL uses the shared HTTP control transport", /ControlTransport::Https[\s\S]*agent[\s\S]*\.post\(url\)/); expect(controlSource, "hosted control transport uses the canonical API path", /CONTROL_API_PATH: &str = "\/api\/v1\/control"/); expect(controlSource, "plain HTTP is restricted to loopback", /endpoint\.starts_with\("http:\/\/"\) && !endpoint_is_loopback\(endpoint\)/); expect(cliSource, "default hosted coordinator resolves to the HTTPS control API", /hosted_coordinator_remains_a_real_https_control_endpoint[\s\S]*https:\/\/disasmer\.michelpaulissen\.com\/api\/v1\/control/); assert.doesNotMatch(cliSource, /coord\.disasmer\.invalid/); expect(cliLoginSmoke, "CLI login smoke covers default hosted coordinator", new RegExp(`defaultHostedCoordinatorEndpoint = "${serviceEndpoint.replaceAll(".", "\\.")}"`)); assert.strictEqual( vscodePackage.contributes.debuggers[0].configurationAttributes.launch.properties.coordinatorEndpoint.default, undefined ); expect(vscodeSmoke, "VS Code smoke covers session-inferred coordinator endpoint", /coordinatorEndpoint\.default, undefined/); for (const binary of [ "disasmer", "disasmer-coordinator", "disasmer-node", "disasmer-debug-dap", ]) { expect(prepScript, `release prep includes ${binary}`, new RegExp(`"${binary}"`)); } for (const [name, pattern] of [ ["release prep filters private and experiments", /filteredTopLevel = \["private", "experiments", "\.git", "target"\]/], ["release prep enumerates Git-ignored source paths", /function gitIgnoredSourcePaths\(\)[\s\S]*"ls-files"[\s\S]*"--ignored"[\s\S]*"--exclude-standard"[\s\S]*"--directory"/], ["release prep has archive-safe ignored-path fallbacks without Git metadata", /archiveIgnoredPathFallbacks = \[[\s\S]*"target"[\s\S]*"\.disasmer"[\s\S]*"vscode-extension\/node_modules"[\s\S]*"scripts\/containers-home"[\s\S]*if \(output === null\) \{[\s\S]*return archiveIgnoredPathFallbacks/], ["release prep filters Git-ignored source paths", /isGitIgnored\(childRelative, ignoredSourcePaths\)/], ["release prep verifies Git-ignored paths are absent", /Git-ignored source path leaked into the dry-run public tree/], ["release prep filters generated view state", /filteredDirectoryNames = \["\.disasmer"\]/], ["release prep filters internal root Markdown", /isFilteredRootMarkdown[\s\S]*path\.extname\(entry\.name\)\.toLowerCase\(\) === "\.md"[\s\S]*README\.md[\s\S]*SECURITY\.md/], ["release prep retains product README", /product README\.md is missing from the dry-run public tree/], ["release prep filters .forgejo by default", /topLevel === "\.forgejo" && !includeForgejoWorkflows/], ["release prep has Forgejo workflow opt-in argument", /--include-forgejo-workflows/], ["release prep builds public release bins", /cargo"[\s\S]*"build"[\s\S]*"--workspace"[\s\S]*"--bins"[\s\S]*"--release"/], ["release prep writes binary archive", /disasmer-public-binaries-\$\{releaseName\}-\$\{platformName\(\)\}\.tar\.gz/], ["release prep writes source archive", /disasmer-public-source-\$\{releaseName\}\.tar\.gz/], ["release prep writes extension VSIX", /\$\{packageJson\.name\}-\$\{packageJson\.version\}\.vsix[\s\S]*@vscode\/vsce[\s\S]*package/], ["release prep packages extension non-interactively", /@vscode\/vsce[\s\S]*package[\s\S]*--skip-license/], ["release prep writes selected-user guide", /DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-\$\{releaseName\}\.md/], ["release prep writes selected-user invite", /DISASMER_PUBLIC_DRYRUN_INVITE-\$\{releaseName\}\.md/], ["release prep accepts resolver instructions", /DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS/], ["release prep accepts hosts entry", /DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY/], ["release prep accepts deployment IP", /DISASMER_PUBLIC_RELEASE_DRYRUN_IP/], ["selected-user guide documents DNS or fallback", /public DNS[\s\S]*(fallback|hosts entry)|DNS record[\s\S]*controlled resolution/], ["selected-user guide names hosted coordinator", /default hosted coordinator is the deployment/], ["selected-user guide rejects standalone Core coordinator", /standalone open-source Core coordinator/], ["selected-user invite documents friends dry run", /friends helping test[\s\S]*not broadly advertised/], ["selected-user invite distinguishes Core", /hosted server is not the standalone Core\s+coordinator/], ["selected-user guide documents default login", /disasmer login --browser/], ["selected-user guide documents VSIX install", /code --install-extension disasmer-vscode-\*\.vsix/], ["selected-user guide documents node attach", /disasmer node attach --coordinator https:\/\/disasmer\.michelpaulissen\.com/], ["release prep packages generated evidence", /stageEvidenceAsset[\s\S]*EVIDENCE_BINDING\.json[\s\S]*disasmer-public-evidence-/], ["release prep binds binary digests", /binary_digests: binaryDigests/], ["release prep writes checksums", /SHA256SUMS/], ["release prep writes manifest", /public-release-manifest\.json/], ["release prep writes public tree provenance", /DISASMER_PUBLIC_TREE\.json/], ["release prep records public tree identity", /public_tree_identity: publicTreeIdentity/], ["release prep records DNS state", /dns_publication_state:/], ["release prep records resolver override", /resolver_override:/], ["release prep records Forgejo URLs", /public_repo_url:[\s\S]*public_repo_remote:[\s\S]*forgejo_release_url:/], ["release prep requires publish opt-in", /DISASMER_PUBLISH_PUBLIC_TREE/], ["release prep requires Forgejo remote", /remote\.includes\(forgejoHost\)/], ["release prep can push filtered tree", /git"[\s\S]*"push"[\s\S]*"public"[\s\S]*`HEAD:\$\{publicRepoBranch\}`/], ["release prep records publish result", /public_tree_publish: publicTreePublish/], ["release prep publishes tree before building target", /const publicTreePublish = publishPublicTree[\s\S]*buildPublicBinaries\(\)/], ]) { expect(prepScript, name, pattern); } for (const [name, pattern] of [ ["release publisher requires Forgejo token", /DISASMER_FORGEJO_TOKEN/], ["release publisher infers Forgejo repo identity", /function resolveRepoIdentity\(manifest\)[\s\S]*parseForgejoRepoIdentity/], ["release publisher uses manifest public repo URL", /manifest\.public_repo_url[\s\S]*manifest\.public_repo_remote/], ["release publisher uses token auth", /Authorization: `token \$\{token\}`/], ["release publisher lists releases", /\/repos\/\$\{encodeURIComponent\(owner\)\}\/\$\{encodeURIComponent\(repoName\)\}\/releases\?limit=100/], ["release publisher creates release", /POST[\s\S]*\/repos\/\$\{encodeURIComponent\(owner\)\}\/\$\{encodeURIComponent\(repoName\)\}\/releases/], ["release publisher reloads release details", /function loadRelease\(releaseId\)[\s\S]*\/releases\/\$\{releaseId\}/], ["release publisher uploads assets", /releases\/\$\{release\.id\}\/assets\?name=\$\{encodeURIComponent\(asset\.name\)\}/], ["release publisher uses attachment multipart field", /multipartFile\("attachment", asset\.file\)/], ["release publisher skips already attached assets", /existingAssetByName\(release, asset\.name\)/], ["release publisher rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/], ["release publisher checks manifest kind", /manifest\.kind !== "disasmer-public-release-dryrun"/], ["release publisher requires public tree push", /public tree must be pushed before publishing the Forgejo Release/], ["release publisher accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/], ["release publisher targets external public tree commit", /function publicTreeCommit\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_COMMIT/], ["release publisher writes evidence report", /public-release-dryrun-forgejo-release\.json/], ["release publisher records reused assets", /reused_assets:/], ]) { expect(publishScript, name, pattern); } for (const [name, pattern] of [ ["preflight rejects stale release manifests", /manifest\.source_commit[\s\S]*currentSourceCommit/], ["preflight disables interactive remote prompts", /function nonInteractiveEnv[\s\S]*GIT_TERMINAL_PROMPT[\s\S]*GIT_ASKPASS[\s\S]*SSH_ASKPASS[\s\S]*GIT_SSH_COMMAND[\s\S]*BatchMode=yes[\s\S]*NumberOfPasswordPrompts=0[\s\S]*git", \["ls-remote"[\s\S]*timeout/], ["preflight accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/], ["preflight verifies public branch commit", /remoteMain[\s\S]*publicTreeCommit/], ["preflight verifies local asset checksums", /parseSha256Sums[\s\S]*checksum mismatch/], ["preflight records public tree push source", /public_tree_push_source/], ["preflight records pending external gates", /external_gates:[\s\S]*forgejo_release_publication[\s\S]*public_release_e2e/], ["preflight writes evidence report", /public-release-dryrun-preflight\.json/], ]) { expect(preflightScript, name, pattern); } for (const [name, pattern] of [ ["e2e runner requires explicit opt-in", /DISASMER_PUBLIC_RELEASE_DRYRUN_E2E=1/], ["e2e runner rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/], ["e2e runner disables interactive git prompts", /function nonInteractiveGitEnv[\s\S]*GIT_TERMINAL_PROMPT[\s\S]*GIT_ASKPASS[\s\S]*SSH_ASKPASS[\s\S]*GIT_SSH_COMMAND[\s\S]*BatchMode=yes[\s\S]*NumberOfPasswordPrompts=0[\s\S]*function commandEnv/], ["e2e runner accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/], ["e2e runner downloads Forgejo Release assets", /downloadReleaseAssets/], ["e2e runner verifies SHA256SUMS", /verifyChecksums/], ["e2e runner clones public repo", /"git", \["clone", "--depth", "1"/], ["e2e runner rejects private tree leaks", /private[\s\S]*experiments[\s\S]*target/], ["e2e runner rejects internal root Markdown leaks", /internal root Markdown file leaked into public repo/], ["e2e runner requires product README", /public checkout must include product README\.md/], ["e2e runner rejects .forgejo leaks by default", /\.forgejo\/ leaked into public repo/], ["e2e runner rejects generated view state", /generated \.disasmer view state leaked into public repo/], ["e2e runner checks public tree identity", /hashTree\(checkout\)[\s\S]*manifest\.public_tree_identity/], ["e2e runner extracts public binaries", /tar"[\s\S]*"-xzf"/], ["e2e runner loads public coordinator binary", /executable\(installDir, "disasmer-coordinator"\)/], ["e2e runner checks default hosted coordinator", /defaultLoginPlan\.coordinator[\s\S]*serviceEndpoint/], ["e2e runner requires an external browser driver", /DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND/], ["e2e runner uses the CLI browser opener boundary", /DISASMER_BROWSER_OPEN_COMMAND: browserOpenCommand/], ["e2e runner derives scope from the hosted session", /const tenant = loginSession\.tenant[\s\S]*const project = loginSession\.project[\s\S]*const user = loginSession\.user/], ["e2e runner uses public CLI node attach", /"node"[\s\S]*"attach"[\s\S]*serviceEndpoint/], ["e2e runner starts public worker runtime", /workerArgs[\s\S]*"--worker"[\s\S]*cp\.spawn\(disasmerNode/], ["e2e runner launches the released product through CLI", /"run"[\s\S]*"build"[\s\S]*runReport\.status[\s\S]*main_launched/], ["standalone Core proof launches a real Wasm TaskSpec", /type: "launch_task"[\s\S]*task_spec:[\s\S]*kind: "coordinator_node_wasm"[\s\S]*bundle_digest: manifest\.bundle_digest/], ["e2e runner verifies assignment polling", /worker_assignment_poll_verified/], ["e2e runner validates standalone Core coordinator", /validateStandaloneCoreCoordinator/], ["e2e runner uses canonical standalone Core wire envelopes", /function sendCore\(addr, message\)[\s\S]*coordinatorWireRequest\(message, "public-e2e-core"\)/], ["e2e runner records public coordinator validation", /core_coordinator_validated/], ["e2e runner validates released live DAP", /validateReleasedLiveDap/], ["e2e runner verifies attach-mode Debug Epoch transition", /validateReleasedLiveDap[\s\S]*resumed through coordinator[\s\S]*requested freeze/], ["e2e runner verifies attached coordinator task completion", /validateReleasedLiveDap[\s\S]*variable\.name === "state" && variable\.value === "Completed"[\s\S]*coordinator task event from node/], ["e2e runner attaches live DAP to Client-authorized process", /attached_to_existing_client_process/], ["e2e runner verifies task events", /type: "list_task_events"/], ["e2e runner verifies download link", /type: "create_artifact_download_link"/], ["e2e runner verifies VS Code debugger", /scripts\/vscode-f5-smoke\.js/], ["e2e runner writes e2e report", /public-release-dryrun-e2e\.json/], ]) { expect(e2eScript, name, pattern); } for (const [name, pattern] of [ ["final evidence reads manifest", /public-release-manifest\.json/], ["final evidence rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/], ["final evidence verifies filtered export", /function assertFilteredOut\(manifest\)/], ["final evidence reads Forgejo Release report", /public-release-dryrun-forgejo-release\.json/], ["final evidence reads deployment manifest", /deployment-manifest\.json/], ["final evidence reads service smoke report", /public-release-dryrun-service\.json/], ["final evidence reads hosted Client compatibility report", /hosted-client-compat\.json/], ["final evidence reads Core coordinator compatibility report", /core-coordinator-compat\.json/], ["final evidence requires public e2e report", /public-release-dryrun-e2e\.json/], ["final evidence verifies Forgejo host", /forgejoHost = "git\.michelpaulissen\.com"/], ["final evidence verifies default hosted coordinator", /serviceEndpoint = "https:\/\/disasmer\.michelpaulissen\.com"/], ["final evidence accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/], ["final evidence records resolved public repo", /resolvedPublicRepoRemote[\s\S]*public_repository_url: resolvedPublicRepoRemote/], ["final evidence requires private hosted coordinator", /coordinator_implementation[\s\S]*"hosted-policy-coordinator"/], ["final evidence requires service private coordinator marker", /service\.coordinator_implementation[\s\S]*"hosted-policy-coordinator"/], ["final evidence requires current service smoke source", /service\.source_commit[\s\S]*manifest\.source_commit/], ["final evidence requires current service smoke release", /service\.release_name[\s\S]*manifest\.release_name/], ["final evidence requires current Client compatibility source", /compat\.source_commit[\s\S]*manifest\.source_commit/], ["final evidence requires current Client compatibility release", /compat\.release_name[\s\S]*manifest\.release_name/], ["final evidence requires current public coordinator source", /coreCoordinator\.source_commit[\s\S]*manifest\.source_commit/], ["final evidence requires current public coordinator release", /coreCoordinator\.release_name[\s\S]*manifest\.release_name/], ["final evidence requires server-generated service enrollment", /"server_generated_enrollment_grant"/], ["final evidence requires service probe cleanup", /"process_abort"/], ["final evidence requires process main launch", /e2e\.launch_task_response[\s\S]*"main_launched"/], ["final evidence requires assignment polling", /worker_assignment_poll_verified/], ["final evidence requires public coordinator validation", /core_coordinator_validated/], ["final evidence requires standalone Core coordinator", /standalone-core-coordinator/], ["final evidence requires released live DAP", /released_live_dap_verified/], ["final evidence requires pushed public tree", /publicTreeAlreadyPushed\(manifest\)/], ["final evidence requires release assets", /downloaded_release_assets/], ["final evidence requires VS Code debugger", /vscode_debugger_verified/], ["final evidence writes final report", /public-release-dryrun-final\.json/], ]) { expect(finalEvidenceScript, name, pattern); } if (workflow) { for (const [name, pattern] of [ ["manual Forgejo workflow", /workflow_dispatch:/], ["Linux Forgejo asset job", /linux-assets:[\s\S]*runs-on: docker/], ["Windows Forgejo asset job", /windows-assets:[\s\S]*runs-on: windows/], ["Windows runner caveat", /intermittently online/], ["workflow runs release prep", /node scripts\/prepare-public-release-dryrun\.js/], ["workflow uploads assets", /actions\/upload-artifact@v4[\s\S]*target\/public-release-dryrun\/assets\/\*/], ]) { expect(workflow, name, pattern); } } expect( prepScript, "public tree prep disables interactive git prompts", /function nonInteractiveGitEnv[\s\S]*GIT_TERMINAL_PROMPT[\s\S]*GIT_ASKPASS[\s\S]*SSH_ASKPASS[\s\S]*GIT_SSH_COMMAND[\s\S]*BatchMode=yes[\s\S]*NumberOfPasswordPrompts=0[\s\S]*function commandEnv/ ); for (const [scriptName, script] of [ ["public acceptance", publicAcceptance], ["public split", publicSplit], ]) { assert( script.includes("node scripts/public-release-dryrun-contract-smoke.js"), `${scriptName} must run public-release-dryrun-contract-smoke.js` ); assert( script.includes("node scripts/prepare-public-release-dryrun.js"), `${scriptName} must run prepare-public-release-dryrun.js` ); assert( script.includes("node scripts/publish-public-release-dryrun.js"), `${scriptName} must be able to publish the Forgejo Release` ); assert( script.includes("DISASMER_FORGEJO_TOKEN"), `${scriptName} must gate Forgejo Release publishing on a token` ); assert( script.includes('if [[ -n "${DISASMER_FORGEJO_TOKEN:-}" ]]'), `${scriptName} must not require owner/repo env when the manifest can infer the Forgejo repository` ); } for (const [scriptName, script] of [ ["public acceptance", publicAcceptance], ["private acceptance", privateAcceptance], ]) { if (scriptName === "public acceptance") { assert( script.includes("node scripts/public-release-dryrun-e2e.js"), "public acceptance must be able to run public-release-dryrun-e2e.js" ); assert( script.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_E2E"), "public acceptance must gate public-release-dryrun-e2e.js" ); } assert( script.includes("node scripts/public-release-dryrun-final-evidence.js"), `${scriptName} must be able to run public-release-dryrun-final-evidence.js` ); assert( script.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL"), `${scriptName} must gate final public release dry-run evidence` ); } console.log("Public release dry-run contract smoke passed");