Skip to content

Commit 59aed10

Browse files
committed
fix(spec): give four packages/spec self-tests a roster, a floor and a verdict handshake
`check-exported-any.ts`, `check-dual-source-exports.ts`, `check-error-code-provenance.ts` and `check-browser-reachable-entries.ts` each dispatched on `--self-test` with no battery roster, no floor and no verdict handshake, so "every case held" and "the cases never ran" printed the same line. Measured on this package rather than inherited: deleting `'InferredFromAnySchema'` from `check-exported-any.ts`'s red-leg fixture name list de-registers one of its two type-half detection pins, and the run still prints its verdict byte-identically and still exits 0. Each of the four now carries all three pieces, COPIED from the landed precedents and never imported, so every self-test keeps running standalone: - a frozen `SELF_TEST_BATTERIES` roster (battery name -> minimum case count), every case registered against the battery most recently opened, registration first in the assertion sink so the floor asserts REACH rather than failure; - `SELF_TEST_BATTERY_FLOOR`, pinned to the roster's own size, so deleting an entry cannot take its own floor with it; - a module-level verdict flag set after the verdict line prints and a dispatch that refuses a self-test which returned without reaching it. In three of the four, a `return` above the verdict did not even stop: control fell through to the real audit, which on a built tree prints its own green line and exits 0. No detector, scan surface, ledger, baseline or audit verdict text moves. Each self-test's verdict line now also reports the registered case count and battery count, because a printed count is evidence and the floor is the proof. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
1 parent d4cb05c commit 59aed10

5 files changed

Lines changed: 668 additions & 29 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Four of this package's own gate checkers can no longer report a self-test that never ran as a self-test that passed.
6+
7+
`check-exported-any.ts`, `check-dual-source-exports.ts`, `check-error-code-provenance.ts` and
8+
`check-browser-reachable-entries.ts` each dispatched on `--self-test` with no battery roster, no floor and no
9+
verdict handshake, so "every case held" and "the cases never ran" printed the same line. Measured on this
10+
package rather than inherited from the sibling that started this programme: deleting one name from
11+
`check-exported-any.ts`'s red-leg fixture list de-registers one of its two type-half detection pins, and the run
12+
still prints its verdict byte-identically and still exits 0.
13+
14+
Each of the four now carries all three pieces, copied from the landed precedents and never imported, so every
15+
self-test still runs standalone:
16+
17+
- a frozen `SELF_TEST_BATTERIES` roster — battery name to minimum case count — with each case registered against
18+
the battery most recently opened, registration first in the assertion sink so the floor asserts REACH;
19+
- `SELF_TEST_BATTERY_FLOOR`, the roster's own size, so deleting an entry cannot take its floor with it;
20+
- a module-level verdict flag set after the verdict line prints, and a dispatch that refuses a self-test which
21+
returned without reaching it — in three of the four, control used to fall through to the real audit, which on a
22+
built tree prints its own green line and exits 0.
23+
24+
What each gate says about the tree is unchanged: no detector, scan surface, ledger, baseline or verdict text of
25+
the audit path moves. The verdict line of each self-test now also reports the registered case count and battery
26+
count, because a printed count is evidence and the floor is the proof.
27+
28+
No published file changes — `packages/spec` ships `dist`, `json-schema`, `liveness`, `prompts`, `llms.txt`,
29+
`README.md`, `src/**/*.zod.ts`, `CHANGELOG.md`, `api-surface`, `api-surface-declarations` and
30+
`spec-changes.json`, and this diff touches none of them; the four files live under `scripts/`, which is not
31+
shipped.

‎packages/spec/scripts/check-browser-reachable-entries.ts‎

Lines changed: 157 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -715,21 +715,99 @@ function audit(): never {
715715
// Self-test — the shapes, not the corpus
716716
// ---------------------------------------------------------------------------
717717

718+
// Set by `selfTest()` only after its verdict line prints, and read at the
719+
// dispatch at the foot of this file: a `return` that leaves the function above
720+
// that line prints nothing, and here it does not even stop — control falls
721+
// through to `audit()`, which on a built tree prints its own green line and
722+
// exits 0. A self-test that never finished, reported as one that passed.
723+
// ⛔ AN EXIT CODE IS NOT A HANDSHAKE; the flag is the thing an early return
724+
// cannot carry with it.
725+
let selfTestReachedVerdict = false;
726+
727+
// ── The self-test's own battery roster and floor ───────────────────────────
728+
//
729+
// `failures.length === 0` used to be this self-test's ONLY success condition,
730+
// and the verdict line names three mechanisms rather than a count, so "every
731+
// case held" and "the cases never ran" printed the same sentence. The shrink was
732+
// measured on the sibling `check-exported-any.ts`, whose self-test has the same
733+
// anatomy — deleting one name from a fixture name list de-registers that case
734+
// and the run still prints its verdict byte-identically and still exits 0.
735+
//
736+
// Closed the way `scripts/check-agent-model-declared.mjs` and its TypeScript
737+
// ports (`scripts/check-test-typecheck.mts`, `check-duration-unit-keys.ts` in
738+
// this directory) closed it — COPIED and ⛔ never imported, because every
739+
// self-test has to keep running standalone as
740+
// `tsx scripts/check-browser-reachable-entries.ts --self-test`, and a shared
741+
// assertion module would be one point of failure for every instrument at once.
742+
// What is pinned is the registered NAMES, not a number.
743+
//
744+
// A BATTERY HERE IS A SECTION — the `// ──` groups this self-test was already
745+
// written in. Each opens with `battery('<name>')` and every `check()` after it
746+
// is attributed to that name until the next one opens, so a section that stops
747+
// running names ITSELF at the floor rather than going quiet.
748+
//
749+
// ⛔ A pinned TOTAL is not the repair — the refusal battery falling from 10
750+
// cases to 1 keeps a total "right" the moment a sibling grows — and ⛔ neither
751+
// is a roster DERIVED from the run: a count taken from the cases that ran can
752+
// never notice one that stopped.
753+
//
754+
// The counts are a FLOOR, not an equality: adding cases is ordinary work and
755+
// must not red. A battery BELOW its floor means cases stopped running.
756+
const SELF_TEST_BATTERIES: Readonly<Record<string, number>> = Object.freeze({
757+
'the scanner FINDS real specifiers': 1,
758+
'the scanner does NOT fabricate specifiers out of prose or lookalikes': 1,
759+
'the walk: transitive over relative hops, both verdicts': 4,
760+
'the refusal: an unbuilt or stale tree is NOT MEASURED': 10,
761+
'the reconciliation: both directions': 4,
762+
'target resolution reads BOTH conditions': 1,
763+
"the browser condition's targets, and only when declared (#11072)": 4,
764+
'what counts as Node-only (#11072)': 2,
765+
'the node-only verdict over a walked graph, BOTH directions (#11072)': 2,
766+
});
767+
768+
// DELETING an entry silences that battery's floor exactly as effectively as
769+
// zeroing it, so the roster's own size is pinned too.
770+
const SELF_TEST_BATTERY_FLOOR = 9;
771+
772+
// The key a case is filed under when no battery is open. It is not a declared
773+
// battery, so it reds by the same set difference rather than silently inflating
774+
// whichever battery happened to open last.
775+
const UNATTRIBUTED_BATTERY = '(no battery open)';
776+
718777
/**
719778
* A green run over today's build proves only what today's build contains, and
720779
* two of the things this gate must do cannot be exercised by it at all: the tree
721780
* has ZERO relative hops between bundles (every entry is self-contained), so the
722781
* transitive walk — the part that keeps the gate honest the day `splitting` is
723782
* turned on — would ship never having run. These fixtures are the contract.
724783
*/
725-
function selfTest(): never {
784+
function selfTest(): void {
726785
const failures: string[] = [];
786+
// The battery ledger this self-test's floor is evaluated against.
787+
// `battery()` opens a battery; every `check()` below is attributed to the one
788+
// most recently opened.
789+
//
790+
// Registration is the FIRST statement of `check()`, before the line is
791+
// printed and before `failures` moves, because the floor asserts REACH: a
792+
// case that runs and FAILS still registers, and only a case that never runs
793+
// at all goes missing from the ledger. Routing registration through the
794+
// failure sink instead would register a case only when it failed — a fully
795+
// green run would register 0 and every battery would read DID NOT RUN, the
796+
// floor inverted rather than installed.
797+
const seen = new Map<string, number>();
798+
let openBattery: string | undefined;
799+
const battery = (name: string): void => {
800+
openBattery = name;
801+
};
727802
const check = (name: string, ok: boolean, detail = ''): void => {
803+
const attributedTo = openBattery ?? UNATTRIBUTED_BATTERY;
804+
seen.set(attributedTo, (seen.get(attributedTo) ?? 0) + 1);
728805
if (!ok) failures.push(`${name}${detail ? ` — ${detail}` : ''}`);
729806
console.log(` ${ok ? '✓' : '✗'} ${name}${!ok && detail ? ` — ${detail}` : ''}`);
730807
};
731808

732809
// ── The scanner: it must FIND real specifiers … ──────────────────────────
810+
battery('the scanner FINDS real specifiers');
733811
const real = [
734812
`import { z } from 'zod';`,
735813
`export { a } from "./chunk-A.mjs";`,
@@ -746,6 +824,7 @@ function selfTest(): never {
746824
);
747825

748826
// ── … and it must NOT fabricate them out of prose or lookalikes ──────────
827+
battery('the scanner does NOT fabricate specifiers out of prose or lookalikes');
749828
// The first case is REAL text from dist/index.mjs: a documentation string that
750829
// contains a complete import statement. A naive grep reports it as a link.
751830
const ghost = [
@@ -764,6 +843,7 @@ function selfTest(): never {
764843
);
765844

766845
// ── The walk: transitive over relative hops, both verdicts ───────────────
846+
battery('the walk: transitive over relative hops, both verdicts');
767847
const tmp = mkdtempSync(join(tmpdir(), 'os-browser-reachable-'));
768848
try {
769849
const dist = join(tmp, 'dist');
@@ -813,6 +893,7 @@ function selfTest(): never {
813893
}
814894

815895
// ── The refusal: an unbuilt or stale tree is NOT MEASURED ────────────────
896+
battery('the refusal: an unbuilt or stale tree is NOT MEASURED');
816897
// Driven here rather than against this workspace, because the only way to
817898
// observe the refusal on the real tree is to break the real tree. Both
818899
// verdicts are pinned: a guard only ever seen green cannot be told apart from
@@ -998,6 +1079,7 @@ function selfTest(): never {
9981079
}
9991080

10001081
// ── The reconciliation: both directions ──────────────────────────────────
1082+
battery('the reconciliation: both directions');
10011083
const emptyLedger = (): Ledger => ({ browserReachable: {}, unjudged: [], notAModule: [] });
10021084

10031085
let problems: string[] = [];
@@ -1033,6 +1115,7 @@ function selfTest(): never {
10331115
check('accepts a fully classified exports map', problems.length === 0, JSON.stringify(problems));
10341116

10351117
// ── Target resolution reads BOTH conditions ──────────────────────────────
1118+
battery('target resolution reads BOTH conditions');
10361119
const targets = targetsOf({
10371120
import: { types: './dist/x/index.d.mts', default: './dist/x/index.mjs' },
10381121
require: { types: './dist/x/index.d.ts', default: './dist/x/index.js' },
@@ -1044,6 +1127,7 @@ function selfTest(): never {
10441127
);
10451128

10461129
// ── #11072: the browser condition's targets, and only when declared ──────
1130+
battery("the browser condition's targets, and only when declared (#11072)");
10471131
const conditioned = {
10481132
browser: {
10491133
import: { types: './dist/x/index.d.mts', default: './dist/browser/x/index.mjs' },
@@ -1077,6 +1161,7 @@ function selfTest(): never {
10771161
);
10781162

10791163
// ── #11072: what counts as Node-only ─────────────────────────────────────
1164+
battery('what counts as Node-only (#11072)');
10801165
const nodeOnly = ['fs', 'node:fs', 'fs/promises', 'node:path', 'pg-connection-string',
10811166
'pg-connection-string/index.js'];
10821167
const browserSafe = ['zod', 'zod/v4', './chunk.mjs', 'ai'];
@@ -1092,6 +1177,7 @@ function selfTest(): never {
10921177
);
10931178

10941179
// ── #11072: the node-only verdict over a walked graph, BOTH directions ───
1180+
battery('the node-only verdict over a walked graph, BOTH directions (#11072)');
10951181
const nodeOnlyTmp = mkdtempSync(join(tmpdir(), 'os-browser-reachable-nodeonly-'));
10961182
try {
10971183
const dist = join(nodeOnlyTmp, 'dist');
@@ -1131,13 +1217,78 @@ function selfTest(): never {
11311217
rmSync(nodeOnlyTmp, { recursive: true, force: true });
11321218
}
11331219

1134-
if (failures.length) {
1135-
console.error(`\n✗ self-test: ${failures.length} case(s) failed.`);
1220+
// ── The floor: every declared battery RAN, and ran its cases ─────────────
1221+
//
1222+
// Evaluated after every battery has had its chance and BEFORE the verdict, so
1223+
// the success line below can only be printed by a run in which the set of
1224+
// batteries that registered EQUALS the set declared, each at or above its own
1225+
// count. A set difference names WHICH battery stopped; a count says only that
1226+
// something did — and, before this block existed, not even that.
1227+
const floorProblems: string[] = [];
1228+
const declaredBatteries = Object.keys(SELF_TEST_BATTERIES);
1229+
if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) {
1230+
floorProblems.push(
1231+
`SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` +
1232+
`${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`,
1233+
);
1234+
}
1235+
for (const [name, count] of seen) {
1236+
if (declaredBatteries.includes(name)) continue;
1237+
floorProblems.push(
1238+
`self-test battery "${name}" registered ${count} case(s) but is not declared in ` +
1239+
'SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.',
1240+
);
1241+
}
1242+
for (const name of declaredBatteries) {
1243+
const count = seen.get(name) ?? 0;
1244+
if (count >= SELF_TEST_BATTERIES[name]) continue;
1245+
floorProblems.push(
1246+
count === 0
1247+
? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` +
1248+
'The verdict below would have claimed those cases hold.'
1249+
: `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` +
1250+
`${SELF_TEST_BATTERIES[name]} — ${SELF_TEST_BATTERIES[name] - count} case(s) that used to run no longer do.`,
1251+
);
1252+
}
1253+
if (floorProblems.length > 0) {
1254+
for (const problem of floorProblems) console.error(`✗ self-test floor: ${problem}`);
1255+
console.error(
1256+
'✗ self-test floor: A battery below its floor means cases STOPPED RUNNING — the battery is the ' +
1257+
'bug, not the number. Find what stopped registering (a deleted case, a fixture the cases are ' +
1258+
'built from that no longer exists, a guard that now skips) and restore it.',
1259+
);
1260+
}
1261+
1262+
if (failures.length || floorProblems.length) {
1263+
if (failures.length) console.error(`\n✗ self-test: ${failures.length} case(s) failed.`);
11361264
process.exit(1);
11371265
}
1138-
console.log('✅ self-test: scanner, transitive walk and ledger reconciliation all behave.');
1139-
process.exit(0);
1266+
// The count is printed because a reader had to hand-tally the checkmarks to
1267+
// get one, and it is printed AFTER the floor rather than instead of it: the
1268+
// number is evidence, the floor is the proof.
1269+
const registered = [...seen.values()].reduce((a, b) => a + b, 0);
1270+
console.log(
1271+
'✅ self-test: scanner, transitive walk and ledger reconciliation all behave — ' +
1272+
`${registered} case(s) across ${declaredBatteries.length} batteries, every battery at or above ` +
1273+
'its pinned floor.',
1274+
);
1275+
selfTestReachedVerdict = true;
11401276
}
11411277

1142-
if (SELF_TEST) selfTest();
1278+
if (SELF_TEST) {
1279+
selfTest();
1280+
// The handshake. Without it a `return` above the verdict prints nothing and
1281+
// does not even stop: control reaches `audit()` below, which on a built tree
1282+
// prints its own green line and exits 0 — a self-test that never finished,
1283+
// reported as one that passed.
1284+
if (!selfTestReachedVerdict) {
1285+
console.error(
1286+
'\n✗ check-browser-reachable-entries self-test: selfTest() returned without reaching its verdict,\n' +
1287+
'so no verdict line was printed. Exiting 0 here would report a self-test that never\n' +
1288+
'finished as a self-test that passed.\n',
1289+
);
1290+
process.exit(1);
1291+
}
1292+
process.exit(0);
1293+
}
11431294
audit();

0 commit comments

Comments
 (0)