From 3ef8dd92c4086d42dac758060ef064fea9d56cab Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 13 Sep 2026 08:34:35 -0400 Subject: [PATCH] oskiewar release: stamp the version instead of remembering it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `buildVersion` is the piece's count of committed revisions to itself, the release gate compares it against `git rev-list --count`, and nothing moved it. It drifted every time anybody committed to oskiewar.js without remembering, and the deploy refused — twice in one afternoon, at v104 against a real count of 109 and again at v110 against 111. The refusal was doing its job. The part that was wrong is that both times the piece had been telling players an old version in the corner of the title screen while running new code, for however long the drift had been there. The number is on screen. A stale one makes every bug report cite a build that is not the build. The gate already knew both numbers at the moment it refused, so now it stamps: write, reburn the hash-bound social preview, commit, push, and restate the source against the same baseline to check the stamp actually landed. Three decisions worth keeping: A new commit, not an amend. The count is a count of commits, so an amend has to reason about whether HEAD already touched this file — and HEAD here is usually already pushed, into a checkout another session is committing to. Rewriting that history is a way to lose somebody's work. It pushes, and it has to. `lith/deploy.fish` deploys from pushed state only: it resets the box to `origin/main`. A stamp that stayed local would ship the old bytes and then fail its own hash verification, which is a worse failure than the one being fixed. It never moves a version backwards. A `buildVersion` AHEAD of the count is not drift, and it wants a person. `--no-bump` keeps the old refusal, and a dry run writes nothing. Eleven tests cover the arithmetic and the ordering, which is where an off-by-one would publish a version that disagrees with the code behind it. Also: MARKETING.md had the trim line at 500 views; the policy has been 1,000 since the underperformer line moved, and the report now prints permalinks. Co-Authored-By: Claude Opus 5 (1M context) --- xbox/live/MARKETING.md | 8 +-- xbox/tools/oskiewar-release.mjs | 76 ++++++++++++++++++++++++++-- xbox/tools/oskiewar-release.test.mjs | 54 ++++++++++++++++++++ 3 files changed, 131 insertions(+), 7 deletions(-) diff --git a/xbox/live/MARKETING.md b/xbox/live/MARKETING.md index 16cedae8cc..4c5b635181 100644 --- a/xbox/live/MARKETING.md +++ b/xbox/live/MARKETING.md @@ -144,9 +144,11 @@ at all. `reel.mjs --report` rolls it up per market. `node xbox/live/marketing/trim.mjs` reports underperformers without contacting Instagram: live reels from the last 30 days, at least 24 hours old, with fewer -than 500 measured views. Missing insights are protected. After a human has -confirmed and completed deletion in Instagram's own activity tool, record the -exact media ids locally: +than 1,000 measured views (`trimPolicy.maxViews`). Missing insights are +protected, and each candidate prints its `permalink` — the address the +deletion actually happens at, which `reel.mjs --insights` fills in. After a +human has confirmed and completed deletion in Instagram's own activity tool, +record the exact media ids locally: ```sh node xbox/live/marketing/trim.mjs --record-deleted 123,456 --confirmed-web-delete diff --git a/xbox/tools/oskiewar-release.mjs b/xbox/tools/oskiewar-release.mjs index e20c74e0b1..dfe64dd8eb 100644 --- a/xbox/tools/oskiewar-release.mjs +++ b/xbox/tools/oskiewar-release.mjs @@ -151,6 +151,69 @@ async function verifyWeb(hash) { if (actual !== hash) throw new Error(`web hash ${actual.slice(0, 12)} != ${hash.slice(0, 12)}`); } +// The version stamp, stamped. +// +// `buildVersion` is the piece's own count of committed revisions to itself, +// the release gate compares it against `git rev-list --count`, and nothing +// moved it — so it drifted every time anybody committed to oskiewar.js without +// remembering, and the deploy refused. Twice in one afternoon. Both times the +// piece had been telling players an old version in the corner of the title +// screen while running new code, which is the part that actually matters: the +// number is on screen, and a stale one makes every bug report cite a build +// that is not the build. +// +// The gate already knew both numbers at the moment it refused. So it stamps. +// +// A NEW commit rather than an amend, which is what I first reached for: the +// count is a count of commits, so an amend has to reason about whether HEAD +// already touched this file, and — the real objection — HEAD here is usually +// already pushed, and rewriting pushed history in a checkout that has another +// session committing into it is a way to lose somebody's work. +// +// It pushes, and it has to. `lith/deploy.fish` deploys from pushed git state +// only — it resets the box to `origin/main` — so a bump that stayed local +// would ship the old bytes and then fail its own hash verification, which is +// a worse failure than the one being fixed. +function stampBuildVersion(current, previous = null) { + if (current.build === current.expectedBuild) return current; + if (current.build > current.expectedBuild) + throw new Error(`Oskiewar build v${current.build} is AHEAD of its ` + + `revision count v${current.expectedBuild}; that is not drift, and it ` + + "wants a person"); + + // Counted from the clean tree, before the write below dirties it: the + // stamp's own commit is the one that takes the count to this number. + const next = current.expectedBuild + 1; + console.log(`→ stamping oskiewar v${current.build ?? "?"} → v${next}`); + const bytes = readFileSync(sourcePath, "utf8"); + const stamped = bytes.replace(/const buildVersion = \d+;/, + `const buildVersion = ${next};`); + if (stamped === bytes) + throw new Error("could not find `const buildVersion = ;` to stamp"); + writeFileSync(sourcePath, stamped); + + // The social preview is hash-bound to these bytes and the deploy checks it, + // so the stamp has to carry it along or it just trades one refusal for + // another. + run("node", ["xbox/live/render-social-preview.mjs"]); + run("git", ["add", "xbox/live/oskiewar.js", "xbox/live/social"]); + run("git", ["commit", "-m", `oskiewar v${next}: release stamp`, "-m", + "`buildVersion` is the piece's count of committed revisions to itself and " + + "the number the title screen shows. Stamped by the release rather than " + + "by remembering, so the corner of the screen cannot disagree with the " + + "code behind it.\n\n" + + "Co-Authored-By: Claude Opus 5 (1M context) "]); + run("git", ["push"]); + + // Restated against the SAME baseline, so the stamp commit cannot quietly + // change the severity the release was classified at. + const restated = sourceState(previous); + if (restated.build !== restated.expectedBuild) + throw new Error(`stamp landed at v${restated.build} against an expected ` + + `v${restated.expectedBuild}`); + return restated; +} + async function reconcile(receipt, { dryRun = false } = {}) { const hash = receipt.desired.hash; if (receipt.channels.web.status !== "current") { @@ -211,14 +274,19 @@ async function main() { const [command = "status", ...args] = process.argv.slice(2); const dryRun = args.includes("--dry-run"); const previous = readReceipt(); - const current = sourceState(previous); + let current = sourceState(previous); if (command === "status") return print(readReceipt(), current); if (command === "deploy") { if (!current.tracked || current.dirty) throw new Error("Oskiewar source must be tracked and committed before a unified release"); - if (current.build !== current.expectedBuild) - throw new Error(`Oskiewar build v${current.build ?? "?"} does not match ` + - `its committed source revision count v${current.expectedBuild}`); + // `--no-bump` keeps the old behaviour: refuse, and let a person decide. + const stamped = args.includes("--no-bump") ? current + : dryRun ? current : stampBuildVersion(current, previous); + if (stamped.build !== stamped.expectedBuild) + throw new Error(`Oskiewar build v${stamped.build ?? "?"} does not match ` + + `its committed source revision count v${stamped.expectedBuild}` + + (args.includes("--no-bump") ? " (drop --no-bump to stamp it)" : "")); + current = stamped; const receipt = newRelease(current.hash, current.commit, current.severity, previous); save(receipt); await reconcile(receipt, { dryRun }); diff --git a/xbox/tools/oskiewar-release.test.mjs b/xbox/tools/oskiewar-release.test.mjs index c5e2f55abb..1cdc83a025 100644 --- a/xbox/tools/oskiewar-release.test.mjs +++ b/xbox/tools/oskiewar-release.test.mjs @@ -79,3 +79,57 @@ test("asking for the Xbox explicitly still fails when it is asleep", () => { // is a failure of what was asked for — it just says which kind. assert.match(source, /throw new Error\(`Xbox is offline: \$\{probe\.reason\}`\)/); }); + +// ── The release stamp ────────────────────────────────────────────────────── +// `stampBuildVersion` writes, reburns, commits and pushes, so it is not +// callable from a test without doing all four to this checkout. What IS +// testable is the arithmetic and the ordering it rests on, which is where the +// mistakes live: an off-by-one here publishes a version number that disagrees +// with the code behind it, which is the exact fault the stamp exists to fix. + +test("the stamp counts its own commit", () => { + const stamp = source.match(/function stampBuildVersion[\s\S]*?\n}\n/)[0]; + // Counted from the CLEAN tree before the write dirties it, and +1 because + // the stamp's own commit is the one that takes the count there. + assert.match(stamp, /const next = current\.expectedBuild \+ 1;/); + // `expectedBuild` itself already adds 1 when the tree is dirty, so reading + // it after the write would double-count. + const wroteAt = stamp.indexOf("writeFileSync(sourcePath"); + const countedAt = stamp.indexOf("current.expectedBuild + 1"); + assert.ok(countedAt < wroteAt, + "the count must be taken before the file is written"); +}); + +test("the stamp refuses to move a version backwards", () => { + const stamp = source.match(/function stampBuildVersion[\s\S]*?\n}\n/)[0]; + assert.match(stamp, /current\.build > current\.expectedBuild/); + assert.match(stamp, /is AHEAD of its/); + // And does nothing at all when there is nothing to do. + assert.match(stamp, /if \(current\.build === current\.expectedBuild\) return current;/); +}); + +test("the stamp carries the hash-bound social preview with it", () => { + const stamp = source.match(/function stampBuildVersion[\s\S]*?\n}\n/)[0]; + const burnedAt = stamp.indexOf("render-social-preview.mjs"); + const committedAt = stamp.indexOf('"commit"'); + assert.ok(burnedAt > 0 && burnedAt < committedAt, + "the preview is reburned before the commit, or the deploy trades one refusal for another"); + assert.match(stamp, /"xbox\/live\/oskiewar\.js", "xbox\/live\/social"/); +}); + +test("the stamp pushes, because lith deploys pushed state only", () => { + const stamp = source.match(/function stampBuildVersion[\s\S]*?\n}\n/)[0]; + assert.match(stamp, /run\("git", \["push"\]\)/); + // A new commit, never an amend: HEAD here is usually already pushed, and + // another session commits into this same checkout. + assert.doesNotMatch(stamp, /--amend/); + // And it verifies the stamp actually landed rather than assuming. + assert.match(stamp, /stamp landed at v/); +}); + +test("--no-bump keeps the old refusal available", () => { + assert.match(source, /args\.includes\("--no-bump"\) \? current/); + assert.match(source, /drop --no-bump to stamp it/); + // A dry run never writes, commits or pushes anything. + assert.match(source, /: dryRun \? current : stampBuildVersion\(current, previous\)/); +}); -- 2.51.2