Skip to content

Commit a2b7328

Browse files
fix(ci): the Console Pin Gate never counts text objectui's own source carries as the published spec (#21709) (#21717)
Fixes #21709 Clause-②: no ## What changes The `Console Pin Gate`'s probes no longer treat text that objectui's own source carries as evidence of either spec. A console bundle carries text from objectui's code as well as from the specs. objectui's component registry writes `inputs` descriptions that copy spec `.describe()` text, either verbatim or as a prefix it then extends. When the spec rewords one of those, the old text becomes published-only. objectui's literal still carries it, so a substring search found "the published spec" in a bundle built from this tree's spec. That is what has turned every merge-queue build red since #21699 (`16d241a6af`): the object-gantt `markers` describe and objectui's `packages/plugin-gantt/src/index.tsx:190`. - `scripts/console-spec-probes.mjs`: `readHostSourceBlob(dir)` reads objectui's tracked code files at the pin (`git ls-files`; test files excluded; a quote's backslash normalised). `chooseProbes` now takes that blob as `hostBlob`, required. It is a TypeError without it. - **The rule (both legs):** a candidate that is in the bundle **and** carried by objectui's source is not evidence. It is counted (`counts.hostCarried`), never decides a verdict, and is never returned as a probe, so it is never stamped. A candidate **absent** from the bundle is still evidence, whoever else carries it. The rule excuses presence, never absence. - **One new refusal:** if every published-only candidate is in the bundle and objectui carries every one, the stale leg has no probe left. That is neither "no skew" nor "absent", so the assert exits **2** (inconclusive). The old code answered that same state exit 1, with a false accusation. It never passed. - `scripts/assert-console-spec-injection.mjs`: a new required `--objectui` flag (objectui's build tree, the git checkout at the pin). Its messages report host-carried text separately, so a pass no longer prints "all N published-only descriptions are absent" when one of them is in the bundle. - `scripts/build-console.sh` (**declared, and strictly needed by the route**): the single assert call site passes `--objectui "$BUILD_ROOT"`, the tree it just built. The only other way to find that tree is to reach two levels up from `--vendored`, which couples the assert to a path layout. A required flag means a forgotten call site fails loudly (exit 2). - `scripts/check-console-injection.mjs`: the replay logic is unchanged. It needs no objectui tree, because the build never stamps a host-carried probe. Its self-test gains battery 14, and batteries 12 and 13 now pass `--objectui`. - **Same-class fix on the fresh leg, declared:** the same rule applies to the injected-only witness. A witness that objectui writes itself is no proof of the injection. Before this change, a bundle that held objectui's literal and **no spec at all** passed; now it reads "neither spec appears" (exit 2). The four bounded-fix conditions all hold: same defect class, same function, a file nobody else has claimed, and the same gate family. It is pinned in battery 14. - The stamp shape is unchanged (`stampVersion` 1). The stamp now records the **filtered** choice, so a cache-hit replay agrees with the build. ## Reproduction (base `7e0066af7a`, the head of queue run `37187916146`) - **CI:** queue run `37187916146`, job `111393796807` ("Build the Console SPA at the pinned objectui SHA"). Its triage comment on #21700 quotes `✗ Built console still carries the PUBLISHED @objectstack/spec.` I could not read the raw job log from this container: the log blob host answered 403. So the per-candidate figure comes from the local build below, at the same commit. - **Local, the real door:** `scripts/build-console.sh` at `7e0066af7a`, run under `os-verify-lock.sh`. It used objectui `ab1879721595`, and the vendored `@objectstack/spec` resolved to **17.6.0**. Lock held 559s. Result: exit 1, `(1 of 38 published-only descriptions), the first of them: "Extra vertical reference lines drawn like the Today marker ({ date, label?, color? })"`. - **Measured, not assumed:** over that real bundle (33.3 MB of JS), the published-only candidates present are exactly **1 of 38**, and it is the `markers` text. objectui's tracked non-test source carries it. The injected-only candidates present are 26 of 26, and none of those is host-carried. ## Route: (a), by measurement - **(b), whole-literal matching, would fix today's collision.** The `markers` text is not a whole literal in the real bundle. - **It leaves half the family open.** In the real bundle, **9** spec describes are whole literals inside objectui's own chunks, for example "Action IDs available for related records" in the `plugin-grid` chunk. Rewording any of those 9 would recreate this red under (b). - **At the pin, over the module's own file filter**, objectui's non-test code holds 18 literals equal to a spec describe and 18 more that begin with one. - **(a) covers both shapes.** It drops only text whose presence could not have been evidence anyway, and only when that text is in the bundle. - **Leak detectability (pin 2 on the real bundle):** I added the published 17.6.0 JS as a chunk to the real assets. The head assert exits 1 on `37 of 38`, detector "Actions to execute during transition". Replaying the good build's stamp beside those assets also exits 1. - **Cost:** the host blob is 32.3 MB and reads in about 0.3 s. ## Pins (battery 14, `node scripts/check-console-injection.mjs --self-test`) Base: 44 assertions. Head: 67. Every fixture runs the real assert script and replays its stamp through `evaluate()`. The mirrored texts sort **first**, so an unfiltered pick would choose them. 1. **Pin 1 passes:** the injected spec plus an objectui literal that begins with a published-only describe, plus a second literal equal to one (which exercises the quote-backslash normalisation). The stamp records `staleDetector` = a text objectui does not write. The replay passes. 2. **Pin 2 still fails:** a bundle carrying the published spec's own JS fails with exit 1 and names a detector objectui does not write. No stamp is written. objectui's **test** file quotes that detector, so this case also pins that tests are not read as source. 3. **Pin 3, the replay agrees:** the replay matches the assert on both bundles: 0 on bundle 1, and 1 ("carries the PUBLISHED") on bundle 2 with the good stamp beside it. Battery 14 also covers: - the all-host-carried stale leg (exit 2, "cannot judge"); - host-carried text absent from the bundle, which stays a valid detector (exit 0, stamped, replay 0); - the fresh-leg case (exit 2); - an objectui tree git cannot list (exit 2); - `chooseProbes` without `hostBlob` (TypeError). **Ablation.** Predicted first and saved, then run through `scripts/ablation-replace.mjs` in wrap mode. The mutation was `const hostCarried = new Set();`. On disk the anchor went 1 to 0 and the marker 0 to 1. The restore was proven: blob `903c3e80cce1` equals HEAD, and `git diff HEAD` is empty. - **Self-test:** exit 1, with **13 failures, exactly the predicted list**, all in battery 14 (pin 1 ×6, pin 2 detector wording ×2, pin 3 bundle-2 wording ×1, all-host-carried ×2, fresh leg ×2). No other battery failed. - **Pin 2's exit 1 held with and without the fix.** - **Synthetic bundles under ablation:** exit 1 `1 of 38` with the `markers` detector, and exit 1 `38 of 38`. - **Real bundle under ablation:** exit 1 `1 of 38` with the `markers` detector, and exit 1 `38 of 38` with the published chunk added. ## The real door on this head - **End to end:** `scripts/build-console.sh` on `93ea8e7d80` reused the built objectui tree, rebuilt the console at the pin, and ran the fixed assert. Exit **0**, with `37 of 38 published-only descriptions are absent; 1 ... ARE in the bundle, and objectui's own source at the pin carries each of them too`. Lock held 278s. The next CI steps then ran on that dist: `pnpm check:console-sha` exited 0, and `pnpm check:console-injection --require-stamp` exited 0 (self-test 67, replay passed). - **That build was before the last commit.** `93ea8e7d80` differs from the final head `0bcf6a0a95` only in a self-test fixture line in `check-console-injection.mjs`, and that self-test was re-run on `0bcf6a0a95`. - **PR side:** all four changed paths are in `ci.yml`'s `console` filter, so `Console Pin Gate` runs on this PR. Three of them (`build-console.sh`, the assert, the probes module) are in the dist cache key, so the key moves and the cache misses. The PR head therefore gets a full console build at the pin with this assert, and that is the CI reading of the real door. - **Queue:** once this merges, every queue build's `Console Pin Gate` runs this assert. `release.yml`'s key hashes `build-console.sh`, so its next run rebuilds through the new call site too. ## Gates (on `0bcf6a0a95`) - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 32 commands. All 32 exited 0. - `--ran` reconciliation: 32 derived, 32 run, 0 NOT MEASURED, 0 unrun, every one with a recorded exit code. That includes `pnpm check:console-injection`, `check:console-sha`, `check:ci-filter-parity` (+ self-test), `check:nul-bytes`, `check:entry-guard` and `check:pm-dispatch-gates` (1976 cases, run detached). - **eslint, narrowed:** `eslint.config.mjs` lints all three changed `.mjs` files, so none is ignored. `--format json` reported 3 files, 0 errors and 0 warnings. The config has no type-aware linting, so this diff cannot move any untouched file's verdict. `build-console.sh` is outside eslint's population. The full `pnpm lint` is left to CI. ## Changeset `skip-changeset`: nothing published changes. The diff is root `scripts/` only, and the root package is private and ships no `files`. I built the console dist (the one published package this build feeds) and grepped it: `readHostSourceBlob` / `hostCarried` have 0 hits, while the positive control, the `markers` literal, is found in 3 files. ## Acceptance notes - **Dead branch, not touched:** the assert's "published spec is gone, but nothing unique ... was found" branch is unreachable, both before and after this change. The "neither" branch and the stale exit cover every way into it. Carrier: none. - **Parity tests stay evidence:** objectui's `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` quotes 2 of the 38 published-only describes. Tests are excluded from the host source, so in a real leak those two still count as evidence. - **Same-tree pin:** the reproduction and the real-door runs used the same tree as the failing queue run, `7e0066af7a` (its `head_sha`). `origin/main` has since moved by four commits, and none of them touches this gate's inputs. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c43a8ae commit a2b7328

4 files changed

Lines changed: 354 additions & 24 deletions

File tree

‎scripts/assert-console-spec-injection.mjs‎

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@
3535
// positive evidence that the published spec is still in the bundle. The fresh
3636
// witness alone cannot distinguish "injection worked" from "some other copy".
3737
//
38+
// ## objectui's own text is not evidence of either spec (objectstack#21709)
39+
//
40+
// The bundle also carries objectui's OWN code, and objectui's component
41+
// registry writes `inputs` descriptions that mirror spec `.describe()` text.
42+
// When the spec rewords one, the old text is published-only and objectui's
43+
// literal still carries it, so a pure substring search reads objectui's own
44+
// registry as "the published spec": object-gantt `markers` turned every
45+
// merge-queue build red that way. So the third input, --objectui, is the tree
46+
// the console was built from, and a probe its tracked source carries is never
47+
// counted as present — nor stamped. Absent text stays evidence; see
48+
// chooseProbes and readHostSourceBlob for the rule and why it cannot hide a
49+
// real leak.
50+
//
3851
// ## Probe derivation lives in ./console-spec-probes.mjs
3952
//
4053
// Not here, because objectstack#9667 added a SECOND consumer: this script can
@@ -56,6 +69,7 @@
5669
// node scripts/assert-console-spec-injection.mjs \
5770
// --injected <framework packages/spec> \
5871
// --vendored <objectui build tree node_modules/@objectstack/spec> \
72+
// --objectui <objectui build tree: the git checkout at the pin> \
5973
// --assets <built console dist/assets>
6074
//
6175
// Exit: 0 = injection proven (or no skew to prove) · 1 = injection failed
@@ -67,6 +81,7 @@ import {
6781
ProbeError,
6882
chooseProbes,
6983
readBundle,
84+
readHostSourceBlob,
7085
readSpecBlob,
7186
writeStamp,
7287
} from './console-spec-probes.mjs';
@@ -84,7 +99,7 @@ function parseArgs(argv) {
8499
if (argv[i + 1] === undefined) fail(`\`${key}\` has no value`);
85100
out[key.slice(2)] = argv[i + 1];
86101
}
87-
for (const required of ['injected', 'vendored', 'assets']) {
102+
for (const required of ['injected', 'vendored', 'objectui', 'assets']) {
88103
if (!out[required]) fail(`--${required} is required`);
89104
}
90105
return out;
@@ -100,10 +115,12 @@ const distDir = path.dirname(assetsDir);
100115
let bundle;
101116
let injectedBlob;
102117
let vendoredBlob;
118+
let hostBlob;
103119
try {
104120
bundle = readBundle(assetsDir);
105121
injectedBlob = readSpecBlob(path.resolve(args.injected), 'injected');
106122
vendoredBlob = readSpecBlob(path.resolve(args.vendored), 'vendored');
123+
hostBlob = readHostSourceBlob(path.resolve(args.objectui), 'objectui');
107124
} catch (error) {
108125
if (!(error instanceof ProbeError)) throw error;
109126
fail(error.message);
@@ -112,12 +129,31 @@ try {
112129
// Chosen with the bundle in view (objectstack#20646): the witness is injected-only
113130
// text this bundle carries, and the stale leg is judged over EVERY published-only
114131
// description, not the one that sorts first — see chooseProbes for why the old
115-
// alphabetical pick read text from entries the console never imports.
116-
const { freshWitness, freshPresent, freshCounts, staleDetector, stalePresent, staleCounts } = chooseProbes({
117-
injectedBlob,
118-
vendoredBlob,
119-
bundle,
120-
});
132+
// alphabetical pick read text from entries the console never imports. And with
133+
// objectui's own source in view (objectstack#21709): text that source carries is
134+
// never counted as present, on either leg, and never stamped.
135+
const {
136+
freshWitness,
137+
freshPresent,
138+
freshCounts,
139+
staleDetector,
140+
stalePresent,
141+
staleCounts,
142+
freshHostCarried,
143+
staleHostCarried,
144+
} = chooseProbes({ injectedBlob, vendoredBlob, bundle, hostBlob });
145+
146+
/** One leg's descriptions that this bundle carries only as objectui's own text. */
147+
function hostCarriedNote(indent, kind, counts, carried) {
148+
if (counts.hostCarried === 0) return [];
149+
return [
150+
`${indent}${counts.hostCarried} ${kind} description(s) ARE in the bundle, and objectui's own`,
151+
`${indent}source at the pin carries each of them too, so they are not evidence of either spec:`,
152+
`${indent} "${carried[0]}"`,
153+
];
154+
}
155+
const freshNote = (indent) => hostCarriedNote(indent, 'injected-only', freshCounts, freshHostCarried);
156+
const staleNote = (indent) => hostCarriedNote(indent, 'published-only', staleCounts, staleHostCarried);
121157

122158
/** Record what this build proved, for check:console-injection to replay. */
123159
function stamp(skew) {
@@ -139,20 +175,37 @@ function stamp(skew) {
139175
}
140176
}
141177

142-
if (!freshWitness && !staleDetector) {
178+
if (freshCounts.pool === 0 && staleCounts.pool === 0) {
143179
console.log('✓ Injected and vendored @objectstack/spec declare the same descriptions');
144180
console.log(' — no observable skew, so nothing for this check to assert.');
145181
stamp(false);
146182
process.exit(0);
147183
}
148184

185+
// The stale leg with no probe left: every published-only description is in the
186+
// bundle AND carried by objectui's own source, so not one of them can say which
187+
// side put it there. That is a leg that cannot judge this bundle — not "no skew"
188+
// (the pool is not empty) and not "absent" (the text is right there) — so it is
189+
// inconclusive, never a pass: a bundle carrying the published spec would look
190+
// exactly like this.
191+
if (staleCounts.pool > 0 && staleDetector === null) {
192+
console.error("✗ The staleness leg cannot judge this bundle: every published-only description");
193+
console.error(" it could look for is in the bundle, and objectui's own source at the pin carries");
194+
console.error(' each one too, so none can tell the published spec from objectui\'s own text.');
195+
console.error(' The injection is UNVERIFIED by this check.');
196+
console.error(` published-only descriptions: ${staleCounts.pool}, all of them carried by objectui's source`);
197+
console.error(` first of them: "${staleHostCarried[0]}"`);
198+
process.exit(2);
199+
}
200+
149201
// Neither probe anywhere in the bundle means the spec is not in this build at
150202
// all — the check cannot speak to an injection it cannot see.
151203
if (freshPresent !== true && stalePresent !== true) {
152204
console.error('✗ Neither spec appears in the built console — no @objectstack/spec');
153205
console.error(' content matched. The injection is UNVERIFIED by this check.');
154206
console.error(` injected-only descriptions in the bundle: ${freshCounts.inBundle} of ${freshCounts.pool}`);
155207
console.error(` published-only descriptions in the bundle: ${staleCounts.inBundle} of ${staleCounts.pool}`);
208+
for (const line of [...freshNote(' '), ...staleNote(' ')]) console.error(line);
156209
process.exit(2);
157210
}
158211

@@ -165,6 +218,11 @@ if (stalePresent === true) {
165218
console.error(` Text found in the bundle that ONLY the vendored spec has (${staleCounts.inBundle} of`);
166219
console.error(` ${staleCounts.pool} published-only descriptions), the first of them:`);
167220
console.error(` "${staleDetector}"`);
221+
if (staleCounts.hostCarried > 0) {
222+
console.error('');
223+
console.error(` Not counted: ${staleCounts.hostCarried} more published-only description(s) the bundle`);
224+
console.error(" carries that objectui's own source at the pin carries too.");
225+
}
168226
if (freshPresent === true) {
169227
console.error('');
170228
console.error(' Note: text unique to this tree\'s spec is ALSO in the bundle —');
@@ -179,15 +237,22 @@ if (freshPresent !== true) {
179237
console.error(" this tree's spec was found either — the build is in an unexpected");
180238
console.error(' state and the injection is UNVERIFIED.');
181239
console.error(` expected: "${freshWitness}"`);
240+
for (const line of freshNote(' ')) console.error(line);
182241
process.exit(2);
183242
}
184243

185244
console.log("✓ Console bundle carries THIS tree's @objectstack/spec, and only it.");
186245
console.log(` present (injected only): "${freshWitness}"`);
187246
console.log(` — ${freshCounts.inBundle} of ${freshCounts.pool} injected-only descriptions are in the bundle`);
247+
for (const line of freshNote(' ')) console.log(line);
188248
if (staleDetector) {
189249
console.log(` absent (vendored only): "${staleDetector}"`);
190-
console.log(` — and all ${staleCounts.pool} published-only descriptions are absent`);
250+
if (staleCounts.hostCarried === 0) {
251+
console.log(` — and all ${staleCounts.pool} published-only descriptions are absent`);
252+
} else {
253+
console.log(` — and ${staleCounts.pool - staleCounts.hostCarried} of ${staleCounts.pool} published-only descriptions are absent;`);
254+
for (const line of staleNote(' ')) console.log(line);
255+
}
191256
}
192257
stamp(true);
193258
process.exit(0);

‎scripts/build-console.sh‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,9 +376,12 @@ fi
376376
# BOTH directions: the console bundle also holds a second, transitive copy of
377377
# this tree's spec (pulled in through the injected client above), which makes a
378378
# one-sided "is the new text present" probe pass even with no injection at all.
379+
# --objectui is the checkout just built: objectui's own registry mirrors spec
380+
# descriptions, so text its tracked source carries is not evidence of either spec.
379381
node "${FRAMEWORK_ROOT}/scripts/assert-console-spec-injection.mjs" \
380382
--injected "$SPEC_PKG" \
381383
--vendored "${BUILD_ROOT}/node_modules/@objectstack/spec" \
384+
--objectui "$BUILD_ROOT" \
382385
--assets "${TARGET}/assets"
383386

384387
# ── Ship the TRACKED SDUI manifest (ADR-0080) ────────────────────────

0 commit comments

Comments
 (0)