Skip to content

Commit 791aaf5

Browse files
committed
ci(suite-ceilings): a new package is not red before its first ceiling
A package absent from the ceiling dataset now prints "no ceiling: new package" and is not red; the first refresh after it lands enters it in the dataset. A package the dataset knows but that lost its row stays red: besides the existing refusal of a weight with no ceiling or reason, the table reader now refuses a ceiling, `uncapped` reason or `provisional` mark with no weight, which would otherwise have graded as a new package. The self-test battery for the absent case is inverted to the new meaning, and a ruled ceiling for a new package is shown to grade it and to go red when it runs over. Claude-Session: https://claude.ai/code/session_01LYXc6ckoWuZyVZpWYizdMh Co-authored-by: Claude <noreply@anthropic.com>
1 parent 35ef501 commit 791aaf5

2 files changed

Lines changed: 124 additions & 41 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1460,7 +1460,9 @@ jobs:
14601460
# held by every later refresh; it rises only by a ruled entry in the
14611461
# script's RULED_CEILING_RAISES. A `provisional` package prints "no
14621462
# ceiling: provisional" and is not red; a package absent from the dataset
1463-
# is red (the new-package shape). See the script header for the ruling.
1463+
# prints "no ceiling: new package" and is not red until the first refresh
1464+
# after it lands gives it a ceiling, while a package the dataset knows
1465+
# that lost its row is refused, red. See the script header for the ruling.
14641466
#
14651467
# HERE, in the aggregator, rather than in each shard, because the CLI's
14661468
# file-level slices land on three shards: the three windows are SUMMED

‎scripts/check-test-suite-ceilings.mjs‎

Lines changed: 121 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@
2828
* provisional" and is not red. A package `uncapped` as `carried` (a cache HIT
2929
* re-confirmed it, but no run of the window executed it) is not red either:
3030
* it is in the dataset, and the next refresh that executes it gives it one.
31-
* - ONLY a package absent from the dataset is red for having no ceiling -- the
32-
* new-package shape. A package that has never landed is never measured by a
33-
* refresh, so for a new package that red is lifted by a ruled raise below,
34-
* not by waiting.
31+
* - A package absent from the dataset is a NEW package: it prints "no
32+
* ceiling: new package" and is not red, and the first refresh after it lands
33+
* enters it in the dataset under the formula above. A package the dataset
34+
* KNOWS but that lost its row is not new and stays red: a weight with neither
35+
* a ceiling nor an `uncapped` reason, or a ceiling, an `uncapped` reason or a
36+
* `provisional` mark with no weight, is a table the generator did not write,
37+
* and readCeilingTable refuses it. Ruling (Q2 B):
38+
* https://github.com/objectstack-ai/objectstack/issues/16468#issuecomment-6082629070
3539
* - A ceiling rises only by a ruling: RULED_CEILING_RAISES, each entry naming
3640
* the ruling's comment URL. Nothing else in this file moves a ceiling.
3741
*
@@ -44,10 +48,10 @@
4448
* hand-typed one is exactly what the generator exists to prevent. Until the
4549
* first refresh after this check landed writes `ceilings`, a dataset WITHOUT
4650
* the key is read as NOT MEASURED ("no ceiling table yet") and exits 0. That
47-
* is distinct from a package absent from a dataset that HAS the key (red), and
48-
* the reading is printed as a warning annotation on every run, because a
49-
* persistent NOT MEASURED is a defect: it means the refresh stopped writing
50-
* ceilings.
51+
* is distinct from a package absent from a dataset that HAS the key (a new
52+
* package), and the reading is printed as a warning annotation on every run,
53+
* because a persistent NOT MEASURED is a defect: it means the refresh stopped
54+
* writing ceilings.
5155
*
5256
* ## Where it runs, and why the CLI's slices are SUMMED
5357
*
@@ -89,9 +93,11 @@
8993
*
9094
* ## Exit codes
9195
*
92-
* 0 OK, or NOT MEASURED (no table yet; nothing executed; no captures)
93-
* 1 a package over its ceiling, a package absent from the dataset, or a
94-
* refusal (a malformed ceiling table or raise record)
96+
* 0 OK, or NOT MEASURED (no table yet; nothing executed; no captures); a
97+
* new package alone never makes it 1
98+
* 1 a package over its ceiling, or a refusal (a malformed ceiling table --
99+
* a package the dataset knows that lost its row among them -- or raise
100+
* record)
95101
*/
96102

97103
import { readFileSync, readdirSync, statSync } from 'node:fs';
@@ -120,9 +126,9 @@ export const SLOWEST_FILES_NAMED = 5;
120126
// raise is reviewable only if the words that authorised it can be read. The
121127
// effective ceiling is the larger of the generated one and the raise, so a raise
122128
// below the generated ceiling changes nothing and is reported as stale. An entry
123-
// for a package absent from the dataset gives a new package its first ceiling --
124-
// the only way a package that has never landed can get one, since a refresh only
125-
// measures what runs on `main`.
129+
// for a package absent from the dataset gives that new package a ceiling, graded
130+
// from then on, before the first refresh after it lands could; without one it
131+
// prints "no ceiling: new package" and is not red.
126132
//
127133
// ⛔ Never raise a ceiling by editing `ceilings` in scripts/test-shard-timings.json:
128134
// that file is generated, and the next refresh holds whatever it finds.
@@ -198,6 +204,21 @@ export function readCeilingTable(dataset) {
198204
'the dataset rather than editing it.'
199205
);
200206
}
207+
// And the other direction: the generator writes a ceiling, an `uncapped`
208+
// reason or a `provisional` mark only beside a weight, so a package named by
209+
// one of them without a weight is a package the dataset KNOWS that lost its
210+
// row. Graded, it would read as a new package and pass; refused, it stays red.
211+
const weightless = [...new Set([...Object.keys(ceilings), ...Object.keys(uncapped), ...provisional])]
212+
.filter((n) => !Object.hasOwn(packages, n))
213+
.sort((a, b) => String(a).localeCompare(String(b), 'en'));
214+
if (weightless.length > 0) {
215+
throw new Error(
216+
`${DATASET_REL}: ${weightless.length} package(s) carry a ceiling, an \`uncapped\` reason or a ` +
217+
`\`provisional\` mark but no weight (${weightless.join(', ')}). The generator writes those only beside a ` +
218+
'weight, so each is a package the dataset knows that lost its row, not a new package; regenerate the ' +
219+
'dataset rather than editing it.'
220+
);
221+
}
201222
return {
202223
table: {
203224
packages,
@@ -221,8 +242,8 @@ export function readCeilingTable(dataset) {
221242
* `status`:
222243
*
223244
* over graded, over its effective ceiling RED
224-
* absent not in the dataset, and no ruled raise RED
225245
* ok graded, within its effective ceiling
246+
* new not in the dataset, and no ruled raise no ceiling, not red
226247
* provisional in the dataset's `provisional` list no ceiling, not red
227248
* uncapped `uncapped` with a reason (`carried`) no ceiling, not red
228249
* partial a sliced package missing a part this run NOT MEASURED
@@ -268,7 +289,7 @@ export function gradeCeilings({ merged, dataset, raises = RULED_CEILING_RAISES }
268289
row.raise = raise;
269290
if (generated === null && raise === null) {
270291
if (!inDataset) {
271-
row.status = 'absent';
292+
row.status = 'new';
272293
} else {
273294
row.status = 'uncapped';
274295
row.reason = table.uncapped[name];
@@ -285,7 +306,9 @@ export function gradeCeilings({ merged, dataset, raises = RULED_CEILING_RAISES }
285306

286307
// ── Rendering ─────────────────────────────────────────────────────────────
287308

288-
const RED_STATUSES = new Set(['over', 'absent']);
309+
// A new package is not among them (ruling Q2 B): it is red only once a ceiling
310+
// exists for it and it runs over.
311+
const RED_STATUSES = new Set(['over']);
289312

290313
function sliceNote(row) {
291314
if (!row.slices) return '';
@@ -357,8 +380,11 @@ export function renderCeilingVerdict(report, label) {
357380
return ` -- ${r.name}: ${r.seconds.toFixed(2)}s -- no ceiling: ${r.reason}`;
358381
case 'partial':
359382
return ` -- ${r.name}: NOT MEASURED -- a part of its suite is missing from this run${sliceNote(r)}; a part is never graded as the whole`;
360-
case 'absent':
361-
return ` NEW ${r.name}: ${r.seconds.toFixed(2)}s -- no ceiling: not in ${DATASET_REL} (a new package)`;
383+
case 'new':
384+
return (
385+
` -- ${r.name}: ${r.seconds.toFixed(2)}s -- no ceiling: new package (not in ${DATASET_REL}; ` +
386+
'the first refresh after it lands enters it)'
387+
);
362388
default:
363389
throw new Error(`unknown row status ${r.status}`);
364390
}
@@ -385,13 +411,10 @@ export function renderCeilingVerdict(report, label) {
385411
}
386412

387413
const detail = red
388-
.map((r) =>
389-
r.status === 'over'
390-
? ` ${r.name}: ${r.seconds.toFixed(2)}s measured against its ${r.ceiling.toFixed(2)}s ceiling ` +
391-
`(${(r.seconds / r.ceiling).toFixed(2)}x; ${ceilingText(r)})${sliceNote(r)}. Its slowest test files:\n${filesText(r)}`
392-
: ` ${r.name}: ${r.seconds.toFixed(2)}s and no ceiling -- it is not in ${DATASET_REL}, so it is a new package. ` +
393-
'It stays red until a ruled raise gives it a first ceiling (a refresh only measures what runs on main). ' +
394-
`Its slowest test files:\n${filesText(r)}`
414+
.map(
415+
(r) =>
416+
` ${r.name}: ${r.seconds.toFixed(2)}s measured against its ${r.ceiling.toFixed(2)}s ceiling ` +
417+
`(${(r.seconds / r.ceiling).toFixed(2)}x; ${ceilingText(r)})${sliceNote(r)}. Its slowest test files:\n${filesText(r)}`
395418
)
396419
.join('\n');
397420
return {
@@ -400,14 +423,12 @@ export function renderCeilingVerdict(report, label) {
400423
out: red.map(
401424
(r) =>
402425
`::error title=Suite-duration ceiling::${escapeWorkflowCommandMessage(
403-
r.status === 'over'
404-
? `${r.name}: ${r.seconds.toFixed(2)}s, over its ${r.ceiling.toFixed(2)}s ceiling. Slowest file: ` +
405-
`${r.files[0] ? `${r.files[0].file} ${(r.files[0].ms / 1000).toFixed(2)}s` : '(none captured)'}.`
406-
: `${r.name}: ${r.seconds.toFixed(2)}s and no ceiling -- a package absent from ${DATASET_REL}.`
426+
`${r.name}: ${r.seconds.toFixed(2)}s, over its ${r.ceiling.toFixed(2)}s ceiling. Slowest file: ` +
427+
`${r.files[0] ? `${r.files[0].file} ${(r.files[0].ms / 1000).toFixed(2)}s` : '(none captured)'}.`
407428
)}`
408429
),
409430
err: [
410-
`${head}: OVER -- ${label}: ${red.length} package(s) over their suite-duration ceiling or without one.\n` +
431+
`${head}: OVER -- ${label}: ${red.length} package(s) over their suite-duration ceiling.\n` +
411432
`${detail}\n` +
412433
` ${basis}.\n` +
413434
' Make the suite faster, or take the raise to a ruling: a ceiling rises only by an entry in\n' +
@@ -515,10 +536,10 @@ let selfTestReachedVerdict = false;
515536
const SELF_TEST_BATTERIES = Object.freeze({
516537
'over the ceiling is red, naming the package and its slowest files': 6,
517538
'provisional prints "no ceiling: provisional" and is not red': 3,
518-
'a package absent from the dataset is red; an uncapped one is not': 4,
539+
'a new package prints "no ceiling: new package" and is not red; a known one that lost its row is refused': 10,
519540
'no ceiling table yet is NOT MEASURED, never red': 5,
520541
'file-sliced packages are summed across shards': 6,
521-
'a ruled raise lifts a ceiling, and only a well-formed one does': 7,
542+
'a ruled raise lifts a ceiling, and only a well-formed one does': 8,
522543
'a malformed ceiling table is refused, never read as a pass': 4,
523544
});
524545

@@ -630,14 +651,35 @@ function selfTest() {
630651
if (held.exitCode !== 0 || status(held, 'prov') !== 'provisional') throw new Error(`provisional: a held ceiling overrode the provisional exemption (${held.verdict})`);
631652
});
632653

633-
battery('a package absent from the dataset is red; an uncapped one is not');
634-
const fresh = run([capture('2/6', [['brand-new', 5], ['a', 10]], [file('brand-new', 'n.test.ts', 4_000)])]);
654+
battery('a new package prints "no ceiling: new package" and is not red; a known one that lost its row is refused');
635655
check(() => {
636-
if (fresh.exitCode !== 1 || status(fresh, 'brand-new') !== 'absent') throw new Error(`absent: exit ${fresh.exitCode}, ${status(fresh, 'brand-new')}`);
656+
// Absent from the dataset, beside a package within its ceiling: the run is OK.
657+
const fresh = run([capture('2/6', [['brand-new', 9999], ['a', 10]], [file('brand-new', 'n.test.ts', 4_000)])]);
658+
if (fresh.exitCode !== 0 || fresh.verdict !== 'OK' || status(fresh, 'brand-new') !== 'new') {
659+
throw new Error(`new: exit ${fresh.exitCode}, verdict ${fresh.verdict}, ${status(fresh, 'brand-new')}`);
660+
}
637661
});
638662
check(() => {
639-
if (!fresh.err[0].includes('brand-new: 5.00s and no ceiling') || !fresh.err[0].includes('n.test.ts')) {
640-
throw new Error(`absent: the package or its files are not named:\n${fresh.err[0]}`);
663+
const fresh = run([capture('2/6', [['brand-new', 5], ['a', 10]])]);
664+
if (!fresh.err[0].includes('brand-new: 5.00s -- no ceiling: new package')) throw new Error(`new: the line is wrong:\n${fresh.err[0]}`);
665+
});
666+
check(() => {
667+
// Beside a package over its ceiling, only that one is red and named.
668+
const both = run([capture('2/6', [['brand-new', 9999], ['a', 151]])]);
669+
if (
670+
both.exitCode !== 1 ||
671+
!both.err[0].includes('1 package(s) over their suite-duration ceiling') ||
672+
both.out.length !== 1 ||
673+
both.out.some((l) => l.includes('brand-new'))
674+
) {
675+
throw new Error(`new: a new package was counted red beside an over one:\n${both.err[0]}\n${JSON.stringify(both.out)}`);
676+
}
677+
});
678+
check(() => {
679+
// Alone, it grades nothing: NOT MEASURED, exit 0.
680+
const alone = run([capture('2/6', [['brand-new', 9999]])]);
681+
if (alone.exitCode !== 0 || alone.verdict !== 'NOT MEASURED' || status(alone, 'brand-new') !== 'new') {
682+
throw new Error(`new: a new package alone read ${alone.verdict}, exit ${alone.exitCode}`);
641683
}
642684
});
643685
check(() => {
@@ -648,6 +690,38 @@ function selfTest() {
648690
const carried = run([capture('2/6', [['carried', 9999], ['a', 1]])]);
649691
if (!carried.err[0].includes('carried: 9999.00s -- no ceiling: carried')) throw new Error(`uncapped: the reason is not named:\n${carried.err[0]}`);
650692
});
693+
// A package the dataset KNOWS that lost its row is not new: it is refused, so
694+
// it stays red rather than passing as "no ceiling: new package".
695+
const refusal = (fn) => {
696+
try {
697+
fn();
698+
return null;
699+
} catch (err) {
700+
return err?.message ?? String(err);
701+
}
702+
};
703+
check(() => {
704+
// A weight with neither a ceiling nor an `uncapped` reason (`b` lost its ceiling row).
705+
const why = refusal(() => run([capture('1/6', [['b', 1]])], dataset({ ceilings: { a: 150, cli: 2000 } })));
706+
if (why === null || !why.includes('neither a ceiling nor an `uncapped`') || !why.includes('(b)')) {
707+
throw new Error(`lost row: a weighed package without a ceiling row was graded (${why})`);
708+
}
709+
});
710+
check(() => {
711+
// A held ceiling with no weight (`b` lost its weight row).
712+
const why = refusal(() => run([capture('1/6', [['b', 1]])], dataset({ packages: { a: 100, prov: 10, carried: 20, cli: 1500 } })));
713+
if (why === null || !why.includes('but no weight (b)')) throw new Error(`lost row: a ceiling with no weight was graded as new (${why})`);
714+
});
715+
check(() => {
716+
// An `uncapped` reason with no weight.
717+
const why = refusal(() => run([capture('1/6', [['carried', 1]])], dataset({ packages: { a: 100, b: 50, prov: 10, cli: 1500 } })));
718+
if (why === null || !why.includes('but no weight (carried)')) throw new Error(`lost row: an uncapped reason with no weight was graded as new (${why})`);
719+
});
720+
check(() => {
721+
// A `provisional` mark with no weight.
722+
const why = refusal(() => run([capture('1/6', [['ghost', 1]])], dataset({ provisional: ['prov', 'ghost'] })));
723+
if (why === null || !why.includes('but no weight (ghost)')) throw new Error(`lost row: a provisional mark with no weight was graded as new (${why})`);
724+
});
651725

652726
battery('no ceiling table yet is NOT MEASURED, never red');
653727
const { ceilings: _c, uncapped: _u, ...preCeilings } = dataset();
@@ -727,9 +801,16 @@ function selfTest() {
727801
if (r.exitCode !== 1 || !r.err[0].includes(raiseUrl)) throw new Error(`raise: over the raise was not red, or the ruling not named:\n${r.err[0]}`);
728802
});
729803
check(() => {
730-
// A new package's first ceiling, before any refresh could give it one.
804+
// A ruled ceiling for a new package grades it before the first refresh after it lands could.
731805
const r = run([capture('1/6', [['brand-new', 5]])], dataset(), { 'brand-new': { seconds: 30, ruling: raiseUrl } });
732-
if (r.exitCode !== 0 || status(r, 'brand-new') !== 'ok') throw new Error(`raise: a ruled first ceiling did not lift the new-package red (${status(r, 'brand-new')})`);
806+
if (r.exitCode !== 0 || status(r, 'brand-new') !== 'ok') throw new Error(`raise: a ruled ceiling did not grade a new package (${status(r, 'brand-new')})`);
807+
});
808+
check(() => {
809+
// ...and one it can run over: a ruled ceiling is a ceiling, new package or not.
810+
const r = run([capture('1/6', [['brand-new', 31]])], dataset(), { 'brand-new': { seconds: 30, ruling: raiseUrl } });
811+
if (r.exitCode !== 1 || status(r, 'brand-new') !== 'over' || !r.err[0].includes('brand-new: 31.00s measured against its 30.00s ceiling')) {
812+
throw new Error(`raise: a new package over its ruled ceiling was not red (${status(r, 'brand-new')}):\n${r.err[0]}`);
813+
}
733814
});
734815
check(() => {
735816
// A raise at or under the generated ceiling changes nothing, and says so.

0 commit comments

Comments
 (0)