Skip to content

Commit 535c019

Browse files
ci(suite-ceilings): a new package is not red before its first ceiling (#22498)
Fixes #16468 Clause-②: no Implements ruling Q2 B on the suite-duration ratchet (#16468 (comment)): a package absent from the ceiling dataset is a new package. It 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. Q1 A needs no code (the hold is as built). Part 1 of the ratchet landed as PR #22480 (`6abd265320`). ## What changed `scripts/check-test-suite-ceilings.mjs`: - **Grading.** A package absent from the dataset with no ruled entry now gets row status `new` (it was `absent`). `RED_STATUSES` is `over` alone, so the exit code follows only an over-ceiling package or a refusal. - **Rendering.** The row prints `-- no ceiling: new package (not in scripts/test-shard-timings.json; the first refresh after it lands enters it)`, in the same column as the `provisional` and `carried` rows. The red header, the red detail and the error annotation lose their absent-package branch, because no red row can lack a ceiling now. - **"Lost its row" stays red** (see the finding below). `readCeilingTable` already refused a weight with neither a ceiling nor an `uncapped` reason. It now also refuses a ceiling, an `uncapped` reason or a `provisional` mark that names a package with no weight. Without this, that shape would have changed from red (as built, it graded `absent`) to "no ceiling: new package". - **Prose the ruling made false**, prose only. The header bullet now states the ruled rule and cites the ruling URL. Also updated: the NOT MEASURED paragraph, the exit-code table, the `gradeCeilings` status table and the `RULED_CEILING_RAISES` comment ("the only way a package that has never landed can get one"). `.github/workflows/ci.yml`: the comment above `Check the per-package suite-duration ceilings` said "a package absent from the dataset is red (the new-package shape)". It now states the ruled rule. Comment only, no step change. Sweep for other sentences stating the old rule: `git grep -n -i -E "absent from the dataset|new-package shape|new package|ruled raise|no ceiling"` over the tree found only ci.yml lines 1462-1463, and they are fixed here. The ceiling prose in `.github/workflows/shard-timings-refresh.yml` and `scripts/measure-test-shard-timings.mjs` describes the hold and the formula, not the new-package rule, so it is untouched. Neither file is in this diff, and neither is `scripts/test-shard-timings.json` or `scripts/partition-test-shards.mjs`. ## The inverted pin The battery `'a package absent from the dataset is red; an uncapped one is not'` (floor 4) is now `'a new package prints "no ceiling: new package" and is not red; a known one that lost its row is refused'` (floor 10): 1. A new package (9999 s) beside one within its ceiling: row status `new`, exit 0, verdict `OK`. 2. The printed text is `brand-new: 5.00s -- no ceiling: new package`. 3. Beside a package over its ceiling: exit 1, the header counts 1 package, and the single error annotation does not name the new package. 4. A new package alone: `NOT MEASURED`, exit 0. 5. and 6. The `uncapped` half is kept: a `carried` package is not red and its reason is printed. 7. to 10. A known package that lost its row is refused. Covered: a weight with no ceiling or reason; a ceiling with no weight; an `uncapped` reason with no weight; a `provisional` mark with no weight. Each case asserts the refusal text names the package. The ruled-raise battery gains one case (floor 7 to 8), which measures dispatch assumption 6. A ruled entry for a new package still gives it a ceiling that it can run over: 31 s against a ruled 30 s reads `over`, exit 1. The existing case (5 s against a ruled 30 s reads `ok`) is reworded to the new meaning. `node scripts/check-test-suite-ceilings.mjs --self-test`: `self-test OK (42 cases across 7 batteries)`, up from 35. Exit 0 at `791aaf55`. ## Ablation The fix was committed first (`791aaf55`). Each mutation went through `scripts/ablation-replace.mjs`: the anchor had to hit once, the blob had to change on disk, and the restore was proven as blob == `HEAD` (`570a0ceaa046`) with `git diff HEAD` empty. These are root scripts with no `dist/`, so no build leg applies. | leg | mutation | self-test | |:--|:--|:--| | 1 | `RED_STATUSES = new Set(['over', 'new'])` (the PM's ablation) | exit 1. Red, but via a `TypeError` in the red renderer (`r.ceiling` is null), thrown inside the inverted battery's first case. | | 1b | leg 1, plus the red detail and annotation filtered to rows with a ceiling: the as-built semantic restored without the crash | exit 1, by the pin's own assertion: `new: exit 1, verdict OVER, new` | | 2 | the weightless refusal disabled (its `if` short-circuited with `false &&`) | exit 1: `lost row: a ceiling with no weight was graded as new (null)` | | 2b | `provisional` dropped from the weightless set | exit 1: `lost row: a provisional mark with no weight was graded as new (null)` | | 2c | `uncapped` dropped from the weightless set | exit 1: `lost row: an uncapped reason with no weight was graded as new (null)` | | 3 | the printed text reverted to `no ceiling: not in ...` | exit 1: `new: the line is wrong` | Leg 1's direction is red, as expected, but the crash fires before the pin's assertions run. Leg 1b is there so that the inverted assertion itself is what fails. ## Finding: where a package the dataset knows could be graded as new (dispatch assumption 2) Searched: every path in `gradeCeilings` / `readCeilingTable` that yields "not in the dataset", and every path in the generator (`suiteCeilings`, `buildDataset`, `provisionalPackages` in `scripts/measure-test-shard-timings.mjs`) and the refresh lane (`shard-timings-refresh.yml`, `coverageReport` in `scripts/ci/select-shard-timings-run.mjs`) that can remove a package's row. - **A weight with no ceiling and no `uncapped` reason.** Already refused, as the PM expected. It is now pinned in the inverted battery as well. - **A ceiling, an `uncapped` reason or a `provisional` mark with no weight.** This was NOT refused. The grader keys "in the dataset" on `packages` alone, so before this change it graded `absent` (red). A bare Q2 change would have graded it `new` (not red) and ignored the ceiling the table holds. The generator never writes this shape: `suiteCeilings` iterates `packages`, and `provisional` is a subset of executed plus carried, which is `packages`. So only a hand edit reaches it, and it is now refused like its mirror. Probe: a first table built by the generator's own `suiteCeilings` over the live dataset (72 weights, 71 ceilings, 1 `uncapped: provisional`) passes the reader. A new package graded against it prints the new line, exit 0. - **The generator dropping a held ceiling.** `suiteCeilings` drops a prior ceiling whose package left `packages`, and the next executing refresh would set a fresh one. The refresh lane closes this for a package that still exists. `coverageReport` names any package the committed dataset weighs, which the workspace still contains, but which the refresh no longer covers. The workflow then exits 1 with "NOTHING was regenerated and no PR was opened". What can still drop is a package that left the workspace: deleted, or renamed, and a rename's new name is exactly the "new or renamed" case Q2 B rules not red. `@objectstack/dogfood` is excluded from coverage, but ci.yml excludes it from Test Core, so it is never graded. The residual is in Acceptance notes. ## Q1 A, as built (no code, per the ruling) The effective ceiling is `max(generated, ruled entry)`. A `RULED_CEILING_RAISES` entry below the generated ceiling therefore lowers nothing: the verdict lists it under "changing nothing (delete them)". So the ruling's parenthetical "a lowering is a ruled entry too" has no as-built mechanism. Nothing about lowering was changed here. ## Verification at `791aaf55` - `node scripts/check-test-suite-ceilings.mjs --self-test`: exit 0, 42 cases across 7 batteries. On the base (`35ef501e13`) it read 35 across 7. - `node scripts/measure-test-shard-timings.mjs --self-test`: exit 0, unchanged. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, with no paths (it derives the change set from the merge base, 2 paths): 58 commands. 53 exit 0. Four exit 3 (PREREQUISITE NOT MET, they read built `dist/`): `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:sourcemap-no-sources-content`. These four are declared NOT MEASURED. None of them reads `ci.yml` (only doc comments name it), the diff touches no package's `files[]`, and the shared turbo cache is cold. CI runs them after Build Core. - `pnpm check:pm-dispatch-gates` was run detached, as its header prescribes, and read from its log: `✓ dispatch-gates self-test: 2011 cases pass.` It took 1377.4 s on this box, with no lifecycle failure line. - `dispatch-gates --ran`: 58 derived families, 54 run, 4 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN. - Lint, a proven narrowing rather than a repo-wide run. (1) Population: `eslint --print-config` for the touched script resolves 2 rules and no `parserOptions.project` or `projectService`, and the `files` glob does not cover `.yml`. (2) Count: `eslint --no-inline-config --format json` read 1 file, with 0 errors and 0 warnings. (3) Invariance: type-aware linting is not enabled, so this diff cannot move any other file's verdict. ## Acceptance notes - Noted, not filed. A refresh run by hand with the generator's documented command (`provenance.refresh`) has no coverage check. It can therefore drop a still-existing package's held ceiling with its weight. After that, the package would grade as new and the next refresh would set a fresh ceiling. The lane workflow refuses this, and no hand-run refresh has happened. Carrier: whoever next touches `suiteCeilings` or the refresh lane. --- _Generated by [Claude Code](https://claude.ai/code/session_01LYXc6ckoWuZyVZpWYizdMh)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2e10c9a commit 535c019

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)