Skip to content

Commit b3f4f9d

Browse files
W104 (partial) — Release 0.11 Compatibility Sweep (#447)
* W104 (partial) — compatibility sweep: manifest, replay oracle, pack/smoke Extends the W96.2 regression-evidence manifest (regressionManifest.test.ts) with the two in-repository hosts (MCP server, static ASP.NET host) and the packed public consumer surface (consumer-smoke/), verified to fail for the right reason when a named file goes missing (W104.1, partial — campaign/save enumeration is not yet covered). Adds build/Test-CompatibilitySweep.ps1, composing typecheck/lint/test, a local run of the replay regression oracle against v0.10.0's own fixtures (the same REPLAY_BASELINE_DIR mechanism ci.yml's release-tag-replay job runs automatically at tag time — W104.4, verified: 29 replayed, byte-identical), and the packed-tarball consumer smoke (W104.6, verified: clean tarball, no workspace resolution, ./authoring exports exercised) into one documented, locally runnable command (W104.7). W104.2 (campaign-builder additive-field golden diffs), W104.3 (0.10 save fixture corpus + migration replay), and W104.5 (unified text client/MCP/static host/Adventures integration against one pinned archive) remain unmet: each needs new fixture corpora or cross-repo companion coordination out of proportion for this session, same reasoning W103.7 used for deferring /kit-sync. * verify: record W104 gate report (10 passed, 5 environment-gated did-not-run)
1 parent b0977ba commit b3f4f9d

3 files changed

Lines changed: 214 additions & 19 deletions

File tree

.claude/verify-report.json

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,77 +3,77 @@
33
{
44
"name": "Typecheck",
55
"status": "Passed",
6-
"detail": "npm --prefix src/engine run typechecktsc --noEmit && tsc --noEmit -p tsconfig.scripts.json, exit 0. Covers the engine package and its scripts project."
6+
"detail": "npm --prefix src/engine run typecheck: tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json, exit 0."
77
},
88
{
99
"name": "Lint",
1010
"status": "Passed",
11-
"detail": "npm --prefix src/engine run linteslint src scripts, exit 0. Includes the determinism guard banning Math.random, non-bit-stable Math.*, and Date.now in src/."
11+
"detail": "npm --prefix src/engine run lint: eslint src scripts, exit 0."
1212
},
1313
{
1414
"name": "Test",
1515
"status": "Passed",
16-
"detail": "npm --prefix src/engine test — vitest run: 94 test files passed, 1554 tests passed, 0 failed."
16+
"detail": "npm --prefix src/engine test: Test Files 94 passed (94); Tests 1560 passed (1560); Duration 6.42s."
1717
},
1818
{
1919
"name": "Pack package",
2020
"status": "Passed",
21-
"detail": "cd src/engine; npm pack --silent — exit 0, produced the-running-dev-game-engine-0.10.0.tgz."
21+
"detail": "npm pack --silent (src/engine) produced the-running-dev-game-engine-0.10.0.tgz, exit 0."
2222
},
2323
{
2424
"name": "Inspect tarball",
2525
"status": "Passed",
26-
"detail": "tar -tzf over the packed tarball: 233 entries, 0 violations (no package/src/, no tsconfig*.json, no .test.* artifacts), package/dist/ present."
26+
"detail": "tar -tzf the-running-dev-game-engine-0.10.0.tgz: no src/ files, no tsconfig*.json, no *.test.* build artifacts present, package/dist/ present. All four assertions passed."
2727
},
2828
{
2929
"name": "Consumer smoke",
3030
"status": "Passed",
31-
"detail": "Cleared consumer-smoke/{node_modules,package-lock.json,dist}, then npm run install:engine (added 5 packages), npm run build (tsc -p tsconfig.json), npm run smoke (node dist/smoke.js) — all exit 0."
31+
"detail": "consumer-smoke: npm ci (added 3 packages), npm run install:engine (installed the-running-dev-game-engine-0.10.0.tgz via file:), npm run build (tsc -p tsconfig.json, no errors), npm run smoke (node dist/smoke.js, no assertion failures). All steps exit 0."
3232
},
3333
{
3434
"name": "Parse-check PowerShell scripts",
3535
"status": "Passed",
36-
"detail": "[System.Management.Automation.Language.Parser]::ParseFile over every *.ps1 outside node_modules/ — 37 files checked, 0 parse errors."
36+
"detail": "[System.Management.Automation.Language.Parser]::ParseFile run recursively over every *.ps1 in the repository: zero parse errors reported, PARSE_EXIT=0."
3737
},
3838
{
3939
"name": "Run Pester tests",
4040
"status": "Passed",
41-
"detail": "Invoke-Pester -Path tools — 263 passed, 0 failed, 22 skipped, 285 discovered across 17 files."
41+
"detail": "Invoke-Pester -Path tools -Output Detailed -PassThru: Tests Passed: 324, Failed: 0, Skipped: 35, NotRun: 0. Tests completed in 46.49s."
4242
},
4343
{
4444
"name": "Validate Markdown links, terminology, and generated files",
4545
"status": "Passed",
46-
"detail": "./build/Test-Documentation.ps1 — human-documentation drift check passed across 18 generated engine pages, compatibility pointers, and the guide; documentation checks passed across 195 Markdown files."
46+
"detail": "./build/Test-Documentation.ps1: Human-documentation drift check passed across 18 generated engine page(s), compatibility pointers, and the guide. Documentation checks passed across 197 Markdown file(s)."
4747
},
4848
{
49-
"name": "Build and verify landing page",
50-
"status": "Passed",
51-
"detail": "npm --prefix src/engine run build (exit 0), then npm --prefix site run check — oxlint, tsc -b, vitest (15 passed), vitest browser (11 passed), build (222ms, both HTML entry points carry required static metadata, 3 bundles free of Node-only runtime references), and verify-merge.mjs proved '/', '/roadmap/' and the protected 'docs/' subtree."
49+
"name": "Build documentation",
50+
"status": "DidNotRun",
51+
"reason": "This step runs inside the ghcr.io/the-running-dev/docs-template container via /template/scripts/docs-build.ps1, which is not available outside that published image. The repository's own local equivalent is docs.ps1, generated by Invoke-SetupDocs and deliberately not committed (CLAUDE.md); it is not installed in this checkout, and Docker alone cannot substitute for the container's /template contents. The PR's 'Verify Documentation Build' check is the source of truth for this gate."
5252
},
5353
{
54-
"name": "Build documentation",
54+
"name": "Build and verify landing page",
5555
"status": "DidNotRun",
56-
"reason": "./docs.ps1 is not present in the working tree. It is installed by Invoke-SetupDocs and deliberately not committed (CLAUDE.md, Docs Site), so the production Docusaurus build with onBrokenLinks: 'throw' — the only gate that resolves routes and heading anchors — cannot be run here. The 'Verify Documentation Build' required check on the pull request is where this answer comes from."
56+
"reason": "Runs inside the same docs-template container as 'Build documentation' and depends on its artifacts/docs output (site/package.json's test:merge script reads ../artifacts/docs, which only exists after that containerized step runs). Also relies on the container's Alpine chromium install for the real-browser suite (PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser), not reproducible standalone in this environment. The PR's 'Verify Documentation Build' check is the source of truth."
5757
},
5858
{
5959
"name": "Merge landing page into documentation build",
6060
"status": "DidNotRun",
61-
"reason": "Requires a completed docs build in artifacts/docs produced from this tree, and 'Build documentation' did not run in this session — there is no artifacts/docs directory present locally at all."
61+
"reason": "Depends on the same containerized artifacts/docs output as 'Build and verify landing page', which did not run locally for the reason given there. The PR's 'Verify Documentation Build' check is the source of truth."
6262
},
6363
{
6464
"name": "Test the host",
65-
"status": "DidNotRun",
66-
"reason": "dotnet test src/host/SubZeroDev.GameEngine.Host.Tests requires NUGET_GITHUB_TOKEN for the sibling-repository package feed (src/host/nuget.config), and that environment variable is not set in this session. The host-image workflow on the pull request is where this answer comes from."
65+
"status": "Passed",
66+
"detail": "dotnet test src/host/SubZeroDev.GameEngine.Host.Tests/SubZeroDev.GameEngine.Host.Tests.csproj -c Release: Passed! - Failed: 0, Passed: 12, Skipped: 0, Total: 12, Duration: 422 ms."
6767
},
6868
{
6969
"name": "Positive route and probe smoke",
7070
"status": "DidNotRun",
71-
"reason": "Requires the host Docker image, whose build (Dockerfile line 85-89) mounts nuget_github_actor/nuget_github_token BuildKit secrets to restore the sibling-repository package feed. Those secrets are not available in this session, so the image cannot be built. Docker itself is available locally."
71+
"reason": "Requires building the host Docker image first, which requires a NUGET_GITHUB_TOKEN build secret (secrets.REGISTRY_TOKEN in CI) to restore SubZeroDev.Platform.Hosting from the sibling SubZeroDev.Platform repository's GitHub Packages feed. Attempted the build locally without that secret to confirm: docker buildx build failed at the host-build restore step with 'error : Value cannot be null or empty string. (Parameter '\"'\"'username'\"'\"')' after 'cat: can'\"'\"'t open /run/secrets/nuget_github_token: No such file or directory'. The PR's 'Platform Static Host Image' check is the source of truth."
7272
},
7373
{
7474
"name": "Negative fixture -- corrupted artifact must fail to start",
7575
"status": "DidNotRun",
76-
"reason": "Builds FROM the host image via --build-arg BASE_IMAGE, so it has the same unmet precondition as the positive smoke: that image cannot be built for want of the NuGet BuildKit secrets."
76+
"reason": "This step builds a second image FROM the base image built in 'Positive route and probe smoke' (--build-arg BASE_IMAGE=subzerodev-gameengine-host:smoke), which did not build locally for the reason given there — there is no base image to build the negative fixture from. The PR's 'Platform Static Host Image' check is the source of truth."
7777
}
7878
]
7979
}

build/Test-CompatibilitySweep.ps1

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<#
2+
.SYNOPSIS
3+
Runs the 0.11 release-candidate compatibility sweep (W104) from a clean checkout.
4+
5+
.DESCRIPTION
6+
Composes, into one documented command, the checks that together decide whether
7+
the current tree can call itself a compatibility candidate against a prior
8+
release tag (W104.7):
9+
10+
1. typecheck, lint, and the full test suite from src/engine -- includes the
11+
W96.2/W104.1 regression-evidence manifest (regressionManifest.test.ts),
12+
which fails the moment a named suite, fixture, host, or packed-consumer
13+
file goes missing.
14+
2. The replay regression oracle (07-replay.md), run locally against the
15+
baseline tag's own committed fixtures and outcomes -- the same
16+
REPLAY_BASELINE_DIR mechanism .github/workflows/ci.yml's
17+
release-tag-replay job runs automatically on every release tag push.
18+
Running it here proves W104.4 (byte-identical replay) without waiting
19+
for a tag.
20+
3. npm pack, with the same tarball-content assertions ci.yml's engine job
21+
makes (no src/, no tsconfig, no test artifacts, dist/ present) --
22+
W104.6's clean-pack half.
23+
4. The packed-tarball consumer smoke (consumer-smoke/), installed with no
24+
workspace resolution back into src/engine -- W104.6's install/compile/run
25+
half, exercising the public and ./authoring entry points together.
26+
27+
This script does not replace ci.yml's two jobs; it reproduces their combined
28+
result on demand, from any machine, without needing a release tag pushed
29+
first -- exactly the gap W104 exists to close before 0.11 is cut.
30+
31+
.PARAMETER BaselineTag
32+
The prior release tag to compare replay fixtures against. Defaults to the
33+
most recent 'v*' tag reachable from HEAD, matching ci.yml's own
34+
"previous tag" resolution.
35+
36+
.PARAMETER SkipConsumerSmoke
37+
Skip the packed-tarball consumer-smoke step. For a fast local re-run of the
38+
engine-only checks; CI always runs the full sweep.
39+
#>
40+
[CmdletBinding()]
41+
param (
42+
[Parameter()]
43+
[ValidateNotNullOrEmpty()]
44+
[string] $BaselineTag,
45+
46+
[Parameter()]
47+
[switch] $SkipConsumerSmoke
48+
)
49+
50+
Set-StrictMode -Version 3.0
51+
$ErrorActionPreference = 'Stop'
52+
53+
function Find-RepositoryRoot {
54+
param ([Parameter(Mandatory)] [string] $StartPath)
55+
$current = Resolve-Path -LiteralPath $StartPath | Select-Object -ExpandProperty Path
56+
while ($true) {
57+
if (Test-Path -LiteralPath (Join-Path $current '.git')) { return $current }
58+
$parent = Split-Path -Path $current -Parent
59+
if ([string]::IsNullOrEmpty($parent) -or $parent -eq $current) {
60+
throw "Could not find a repository root (.git) above '$StartPath'."
61+
}
62+
$current = $parent
63+
}
64+
}
65+
66+
$repoRoot = Find-RepositoryRoot -StartPath $PSScriptRoot
67+
$enginePath = Join-Path $repoRoot 'src/engine'
68+
$consumerSmokePath = Join-Path $repoRoot 'consumer-smoke'
69+
70+
function Invoke-Step {
71+
param (
72+
[Parameter(Mandatory)] [string] $Name,
73+
[Parameter(Mandatory)] [scriptblock] $Script
74+
)
75+
Write-Host "==> $Name" -ForegroundColor Cyan
76+
& $Script
77+
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) {
78+
throw "Compatibility sweep failed at step: $Name (exit $LASTEXITCODE)"
79+
}
80+
}
81+
82+
if (-not $BaselineTag) {
83+
$tags = git -C $repoRoot tag --list 'v*' --sort=-v:refname
84+
if (-not $tags) {
85+
throw "No 'v*' tags found — nothing to compare the replay corpus against yet."
86+
}
87+
$BaselineTag = ($tags -split "`n")[0]
88+
}
89+
Write-Host "Compatibility sweep baseline tag: $BaselineTag" -ForegroundColor Yellow
90+
91+
Push-Location $enginePath
92+
try {
93+
Invoke-Step 'Install dependencies' { npm ci }
94+
Invoke-Step 'Typecheck' { npm run typecheck }
95+
Invoke-Step 'Lint' { npm run lint }
96+
Invoke-Step 'Test (includes W104.1 regression-evidence manifest)' { npm test }
97+
98+
Invoke-Step "Replay oracle vs $BaselineTag (W104.4)" {
99+
$baselineDir = Join-Path ([System.IO.Path]::GetTempPath()) "replay-baseline-$BaselineTag"
100+
if (Test-Path $baselineDir) { Remove-Item $baselineDir -Recurse -Force }
101+
New-Item -ItemType Directory -Path $baselineDir | Out-Null
102+
103+
$paths = git -C $repoRoot ls-tree -r --name-only $BaselineTag -- src/engine/fixtures/replay |
104+
Where-Object { $_ -match '\.(fixture|outcome)\.json$' }
105+
foreach ($path in $paths) {
106+
$name = Split-Path -Path $path -Leaf
107+
git -C $repoRoot show "${BaselineTag}:${path}" | Out-File -FilePath (Join-Path $baselineDir $name) -Encoding utf8
108+
}
109+
if (-not (Get-ChildItem -Path $baselineDir -Filter '*.fixture.json' -ErrorAction SilentlyContinue)) {
110+
Write-Host "$BaselineTag predates the replay corpus — nothing to compare yet." -ForegroundColor Yellow
111+
return
112+
}
113+
114+
$env:REPLAY_BASELINE_DIR = $baselineDir
115+
try {
116+
npx vitest run `
117+
src/campaigns/bulgaria-bureaucracy.replay.test.ts `
118+
src/campaigns/stable-life.replay.test.ts `
119+
src/campaigns/world-graph-mvp.replay.test.ts
120+
} finally {
121+
Remove-Item Env:\REPLAY_BASELINE_DIR
122+
}
123+
}
124+
125+
$tarballPath = $null
126+
Invoke-Step 'Pack package (W104.6)' {
127+
$tarballName = (npm pack --silent | Select-Object -Last 1).Trim()
128+
$script:tarballPath = Join-Path $enginePath $tarballName
129+
}
130+
131+
Invoke-Step 'Inspect tarball contents (W104.6)' {
132+
$entries = tar -tzf $tarballPath
133+
if ($entries | Select-String -Pattern '(^|/)src/') { throw 'tarball contains source files under src/' }
134+
if ($entries | Select-String -Pattern '(^|/)tsconfig[^/]*\.json$') { throw 'tarball contains tsconfig JSON files' }
135+
if ($entries | Select-String -Pattern '\.test\.(js|mjs|ts|d\.ts|js\.map|mjs\.map|ts\.map)$') { throw 'tarball contains test build artifacts' }
136+
if (-not ($entries | Select-String -Pattern '^package/dist/')) { throw 'tarball does not contain dist output' }
137+
}
138+
139+
if (-not $SkipConsumerSmoke) {
140+
Push-Location $consumerSmokePath
141+
try {
142+
if (Test-Path 'node_modules') { Remove-Item 'node_modules' -Recurse -Force }
143+
if (Test-Path 'dist') { Remove-Item 'dist' -Recurse -Force }
144+
$env:ENGINE_TARBALL = $tarballPath
145+
try {
146+
Invoke-Step 'Consumer smoke: install (W104.6)' { npm ci }
147+
Invoke-Step 'Consumer smoke: install engine tarball (W104.6)' { npm run install:engine }
148+
Invoke-Step 'Consumer smoke: build + run (W104.6)' { npm run build; npm run smoke }
149+
} finally {
150+
Remove-Item Env:\ENGINE_TARBALL
151+
}
152+
} finally {
153+
Pop-Location
154+
}
155+
} else {
156+
Write-Host "Skipping consumer smoke (-SkipConsumerSmoke)." -ForegroundColor Yellow
157+
}
158+
} finally {
159+
Pop-Location
160+
}
161+
162+
Write-Host "Compatibility sweep passed against baseline $BaselineTag." -ForegroundColor Green

src/engine/src/core/validation/regressionManifest.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { describe, expect, it } from "vitest";
1717
import { FIXTURES_DIR } from "../../campaigns/replay-corpus.js";
1818

1919
const SRC_ROOT = fileURLToPath(new URL("../../", import.meta.url));
20+
const REPO_ROOT = fileURLToPath(new URL("../../../../../", import.meta.url));
2021

2122
interface KindManifest {
2223
readonly kind: string;
@@ -102,3 +103,35 @@ describe("W96.2 regression evidence manifest", () => {
102103
});
103104
}
104105
});
106+
107+
/**
108+
* W104.1 — the two in-repository hosts and the packed public consumer surface named
109+
* in the compatibility-sweep manifest (30-slices.md § W104). Neither is a kind's own
110+
* regression evidence, so it does not belong in `MANIFEST` above; both are still
111+
* artifacts the sweep must notice going missing rather than silently losing coverage.
112+
*/
113+
const HOSTS: readonly string[] = [
114+
// The MCP server — one of the two composition roots this repository ships.
115+
"src/engine/src/mcp/server.ts",
116+
// The static ASP.NET host (design 15) — the other composition root.
117+
"src/host/SubZeroDev.GameEngine.Host/Program.cs",
118+
"src/host/SubZeroDev.GameEngine.Host/SubZeroDev.GameEngine.Host.csproj",
119+
];
120+
121+
const PACKED_CONSUMER_SURFACE: readonly string[] = [
122+
// The packed-tarball consumer smoke (W104.6) — the proof the public surface installs
123+
// and compiles with no workspace resolution back into src/engine.
124+
"consumer-smoke/package.json",
125+
"consumer-smoke/install-engine.mjs",
126+
"consumer-smoke/smoke.ts",
127+
];
128+
129+
describe("W104.1 compatibility-sweep manifest — hosts and packed consumer surface", () => {
130+
it.each(HOSTS)("names host file %s, and it exists", (relativePath) => {
131+
expect(existsSync(`${REPO_ROOT}${relativePath}`), relativePath).toBe(true);
132+
});
133+
134+
it.each(PACKED_CONSUMER_SURFACE)("names packed-consumer-surface file %s, and it exists", (relativePath) => {
135+
expect(existsSync(`${REPO_ROOT}${relativePath}`), relativePath).toBe(true);
136+
});
137+
});

0 commit comments

Comments
 (0)