From b322cfbf17386add6974c52f40ff79aaddbc08b9 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 06:31:00 +0000 Subject: [PATCH] fix(devx): refuse a console injection stamp whose `packages` array is empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check:console-injection --require-stamp` passed on a well-formed stamp with an empty `packages` array. readStamp's `Array.isArray` shape check accepts `[]`, evaluate() fell through to "Nothing assertable in this stamp" and exited 0 — so the flag whose entire purpose is to refuse a vacuous pass was satisfied by a stamp that asserts nothing. Measured: all three of the gate's substantive verdicts — the published-only detector present in the bundle, the stamp's own fresh witness missing from it, and probe expiry — are derived per package entry inside the loop over that array. An empty array silences every one of them. A dist literally carrying the PUBLISHED spec, the objectstack#8134 defect this gate exists to end, exited 0 under --require-stamp. That is strictly MORE vacuous than the state objectstack#10428 refused one input over: an unbuilt spec skips only the expiry re-check and leaves the two bundle assertions standing. Refusing the lesser vacuity while tolerating the greater one is incoherent, so this refuses on the same terms — exit 1 under --require-stamp, advisory when bare, matching the no-dist and no-stamp verdicts. Two layers, and they are not redundant: - PRODUCER: writeStamp now refuses to write an empty entries array. It is the one call site every producer passes, and the array shape exists to be GROWN (objectstack#9659), so the day entries are derived from a package list instead of a literal, a filter matching nothing becomes producible. The caller downgrades the throw to a warning and writes no stamp, landing the build in the missing-stamp state the gate already refuses. - CONSUMER: the gate refuses it too, because its input crosses a cache boundary the producer guard cannot reach. The dist is RESTORED FROM CACHE from another run and may be partially restored or modified after it was proved — the gate's own existing failure text says so. Producer and consumer are deliberately not in one trust domain, which is why this gate exists as a separate script at all. Scoped to `packages.length === 0`, NOT to the `asserted === 0` notice: a no-skew entry also leaves `asserted` at 0 but is a positive record that the build looked and found nothing to tell the specs apart. Keying on `asserted` would fail every no-skew run, which objectstack#10428 deliberately kept passing. Reachability re-verified on main: writeStamp has exactly one call site, with a hard-coded single-element array, reached from both stamping paths. No stamp this repo produces can be empty today. The refusal covers hand-assembled, truncated and partially-restored dists, and the derived-entries future the shape invites. Self-test 27 -> 36 assertions. objectstack#10428's four-row table (spec not built / built unchanged / built moved forward / built caught up, both flag modes) is unmoved, as are the no-skew, no-stamp and published-spec controls. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt --- scripts/check-console-injection.mjs | 121 ++++++++++++++++++++++++++++ scripts/console-spec-probes.mjs | 18 +++++ 2 files changed, 139 insertions(+) diff --git a/scripts/check-console-injection.mjs b/scripts/check-console-injection.mjs index 99d127a153..9ba8aadcf7 100644 --- a/scripts/check-console-injection.mjs +++ b/scripts/check-console-injection.mjs @@ -164,6 +164,7 @@ import { readBundle, readSpecBlob, readStamp, + writeStamp, } from './console-spec-probes.mjs'; import { isEntrypoint } from './invoked-as.mjs'; @@ -257,6 +258,46 @@ export function evaluate({ distDir, specDir, requireStamp = false, cacheKey = '' return { code: 1, out, err }; } + // A stamp whose `packages` array is EMPTY (objectstack#10595). Well-formed — + // readStamp's Array.isArray shape check accepts `[]` — and therefore not a + // code-2 "cannot read"; it is readable and says nothing, which is a different + // failure. Measured: all three of this gate's substantive verdicts (the + // published-only detector in the bundle, the stamp's own fresh witness + // missing from it, and probe expiry) live inside the loop below, so an empty + // array silences every one of them. A dist literally carrying the PUBLISHED + // spec — the objectstack#8134 defect this gate exists to end — passes green. + // + // That is strictly MORE vacuous than the state objectstack#10428 refused one + // input over: an unbuilt spec skips only the expiry re-check and leaves the + // two bundle assertions standing. Refusing the lesser vacuity while tolerating + // the greater one is incoherent, so this refuses on the same terms. + // + // Scoped to `stamp.packages.length === 0`, NOT to the `asserted === 0` notice + // below, which a legitimate no-skew stamp also reaches: a no-skew entry is a + // POSITIVE record that the build looked and found nothing to tell the two + // specs apart, whereas an empty array is no record at all. Keying on + // `asserted` would fail the no-skew runs objectstack#10428 deliberately kept + // passing. Advisory when bare, for the same reason every other refusal here + // is: exit 1 under the flag, matching the no-dist and no-stamp verdicts. + if (stamp.packages.length === 0 && requireStamp) { + err.push( + `✗ Console dist at ${rel(distDir)} carries a stamp with an EMPTY \`packages\` array.`, + '', + ' The stamp is well-formed and asserts nothing. Every verdict that actually', + " interrogates the dist — the published-only detector, the stamp's own fresh", + ' witness, probe expiry — is derived per package entry, so with no entries', + ' this run would report green without making a single assertion about the', + ' artifact it is guarding. A dist carrying the PUBLISHED spec would pass.', + '', + ' Nothing this repo builds writes such a stamp: assert-console-spec-injection.mjs', + ' records one hard-coded @objectstack/spec entry on both of its stamping paths.', + ' So this dist was hand-assembled, truncated in transit, or partially restored.', + '', + ...remedy(cacheKey), + ); + return { code: 1, out, err }; + } + // The tree's own spec, for the expiry re-check. Absent when spec is not built // — a real state for a bare checkout, and not a reason to fail on its own: the // bundle assertions below stand without it. Under --require-stamp it IS a @@ -600,6 +641,86 @@ function selfTest() { ); } + // 7c. THE THIRD VACUITY PATH (objectstack#10595): a well-formed stamp whose + // `packages` array is EMPTY. readStamp's Array.isArray check accepts `[]`, + // and every substantive verdict is derived per entry, so the gate asserts + // nothing at all — strictly more vacuous than 7b, which still ran the two + // bundle assertions. + { + const empty = { stampVersion: 1, generatedBy: 'scripts/assert-console-spec-injection.mjs', packages: [] }; + const dist = makeDist(path.join(root, 'empty-stamp'), `console(${JSON.stringify(FRESH)})`, empty); + + expect('empty packages is advisory by default', evaluate({ distDir: dist, specDir }).code, 0); + + const r = evaluate({ distDir: dist, specDir, requireStamp: true, cacheKey: 'Linux-console-dist-cafe01' }); + expect('empty packages is fatal under --require-stamp', r.code, 1); + const text = r.err.join('\n'); + checked += 1; + // Branch-unique wording: every refusal prints remedy(), so keying on that + // cannot tell this branch from the no-stamp one it sits next to. + if (!text.includes('EMPTY `packages` array')) { + failures.push('empty-packages failure must name the empty packages array'); + } + checked += 1; + if (!text.includes('gh cache delete "Linux-console-dist-cafe01"')) { + failures.push('empty-packages failure must name the exact cache key to delete'); + } + + // THE REJECT SIDE, ASSERTED POSITIVELY. The point is not the exit code on a + // benign fixture — it is that an empty stamp silences the verdict this whole + // gate exists for. This dist carries the PUBLISHED spec (objectstack#8134's + // defect); before this fix it exited 0 under --require-stamp. + const poisoned = makeDist( + path.join(root, 'empty-stamp-poisoned'), + `console(${JSON.stringify(FRESH)});console(${JSON.stringify(STALE)})`, + empty, + ); + expect( + 'a PUBLISHED-spec bundle under an empty stamp no longer passes --require-stamp', + evaluate({ distDir: poisoned, specDir, requireStamp: true }).code, + 1, + ); + + // PRECISION, and the reason this keys on `packages.length` and not on the + // `asserted === 0` notice further down. A no-skew entry ALSO leaves + // `asserted` at 0 and reaches that same notice, but it is a positive record + // that the build looked and found nothing to tell the specs apart — an + // assertion nobody was owed, which objectstack#10428 deliberately kept + // passing. Keying on `asserted` would silently fail every no-skew run. + expect( + 'precision: a no-skew stamp still passes --require-stamp on a built spec', + evaluate({ + distDir: makeDist(path.join(root, 'noskew-required'), 'console("anything")', stampFor({ skew: false, freshWitness: null, staleDetector: null })), + specDir, + requireStamp: true, + }).code, + 0, + ); + } + + // 7d. The producer cannot emit that stamp in the first place. writeStamp is + // the one call site every producer passes, and the entries array is meant + // to GROW (objectstack#9659) — the day it is derived rather than literal, + // an empty result becomes producible. Refused at the write. + { + const dir = fs.mkdtempSync(path.join(root, 'writestamp-')); + checked += 1; + let threw = null; + try { + writeStamp(dir, []); + } catch (error) { + threw = error; + } + if (!(threw instanceof ProbeError)) { + failures.push(`writeStamp([]) must throw ProbeError, got ${threw === null ? 'no throw' : threw.constructor.name}`); + } + expect('writeStamp([]) writes no stamp at all', fs.existsSync(path.join(dir, STAMP_BASENAME)), false); + + // Positive control: the shape the real producer writes still goes through. + writeStamp(dir, [{ name: '@objectstack/spec', skew: true, freshWitness: FRESH, staleDetector: STALE }]); + expect('writeStamp writes a populated stamp', fs.existsSync(path.join(dir, STAMP_BASENAME)), true); + } + // 8. A build that found no skew records it, and this gate says so honestly. { const dist = makeDist( diff --git a/scripts/console-spec-probes.mjs b/scripts/console-spec-probes.mjs index a731cb51e8..3105322a10 100644 --- a/scripts/console-spec-probes.mjs +++ b/scripts/console-spec-probes.mjs @@ -146,6 +146,24 @@ export function readBundle(assetsDir) { * same staleness question. Appending an entry must not need a shape change. */ export function writeStamp(distDir, entries) { + // A stamp with no entries asserts NOTHING about the dist it sits beside, and + // check:console-injection's three substantive verdicts — published-only + // detector present, stamped fresh witness missing, probe expired — all live + // inside its loop over this array. Writing an empty one would produce a dist + // that satisfies `--require-stamp` while the gate makes no assertion at all. + // + // Unrepresentable here rather than only detected there: this is the single + // call site every producer must pass, and the array shape above exists to be + // GROWN (objectstack#9659), so the day entries are derived from a package list + // instead of a literal, a filter that matches nothing becomes producible. + // Refusing at the write keeps that from ever reaching a dist. + // + // The caller downgrades this throw to a warning and writes no stamp, which + // lands the build in the missing-stamp state the gate already refuses under + // `--require-stamp` — an unguarded state converted into a guarded one. + if (!Array.isArray(entries) || entries.length === 0) { + bad(`refusing to write an empty ${STAMP_BASENAME}: a stamp with no packages asserts nothing`); + } const stamp = { stampVersion: STAMP_VERSION, generatedBy: 'scripts/assert-console-spec-injection.mjs',