Skip to content

Commit 184f441

Browse files
committed
test(observability-map): assert the mirror direction of the corpus property
The corpus only failed on a score RISE, so it structurally could not catch a false accusation: 19 of 43 preserving entries were lowering 104 real routes' scores and nothing noticed. Every preserving entry now also asserts fallsIn, the mirror of risesIn: no route measured in both runs may score lower, over a comparison population pinned to the whole measured baseline so a shrunken population cannot pass vacuously. Exactly two entries carry a permanent lowers exemption, the reason on the entry itself (wrap-body-in-non-array-map and -filter, mechanism C: relocated deciding catches cap at not-applicable, 77 routes each). An exempted entry must still fall, and every fall must be exactly error-classification pass -> not-applicable with nothing moving to fail; anything else is a new defect hiding under the exemption. Red-capable in both directions: dropping the lowers field fails the entry on its 77 falls, and reverting the mechanism-A fix fails dead-if-false and dead-if-false-return on 78 falls each.
1 parent 78e6ad5 commit 184f441

2 files changed

Lines changed: 112 additions & 4 deletions

File tree

internal-packages/observability-map/src/mutationCorpus.test.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import { CHECKS } from "./checks/index.js";
2020
* - for a semantics-preserving rewrite, no individual route's score rises and no measured route
2121
* drops out of the measured set. The tree mean can hide a route going up by taking another down;
2222
* `[0].map(...)` is exactly that shape.
23+
* - the mirror of that, for a semantics-preserving rewrite: no individual route's score FALLS on
24+
* the routes measured in both runs. A fall is a false accusation, which is the direction that
25+
* gets the tool switched off, and it went unasserted for three rounds while 19 entries regressed
26+
* 104 routes (see `fallsIn`). Exactly two entries carry a permanent `lowers` exemption, with the
27+
* reason on the entry and the residual shape asserted instead of waived.
2328
*
2429
* Tree scale, not per-fixture, because that is where laundering pays. A shape that moves one
2530
* hand-written fixture by 50 points may move the tree by nothing; a shape that moves the tree is the
@@ -163,6 +168,45 @@ function risesIn(baseline: Measurement, after: Measurement): Rise[] {
163168
return rises.sort((a, b) => b.to - b.from - (a.to - a.from));
164169
}
165170

171+
type Fall = { fileName: string; from: number; to: number; before: string; after: string };
172+
173+
/**
174+
* The mirror of `risesIn`: route files the mutation made look WORSE, worst first. A preserving
175+
* edit lowering a route's score is a false accusation, the direction that gets the tool switched
176+
* off, and for three rounds it was structurally invisible here because only rises were asserted.
177+
* 19 of 43 preserving entries were regressing 104 routes when it was first measured.
178+
*
179+
* Only routes measured in BOTH runs are compared. A route ENTERING the measured set is not a fall:
180+
* unmeasured routes score the vacuous 100, so a mutation that brings one in at 50 registers a
181+
* 100 -> 50 "fall" that is nothing of the kind. A route LEAVING the measured set is already
182+
* counted by `risesIn`, not double-counted here. `compared` is the size of the both-measured
183+
* population, asserted against `baseline.measured` in every preserving entry so a silently
184+
* shrunken comparison cannot pass.
185+
*/
186+
function fallsIn(baseline: Measurement, after: Measurement): { falls: Fall[]; compared: number } {
187+
const falls: Fall[] = [];
188+
let compared = 0;
189+
for (const [fileName, before] of baseline.perEntry) {
190+
const now = after.perEntry.get(fileName);
191+
if (!now || !before.measured || !now.measured) continue;
192+
compared++;
193+
if (now.score >= before.score) continue;
194+
falls.push({
195+
fileName,
196+
from: before.score,
197+
to: now.score,
198+
before: before.checks,
199+
after: now.checks,
200+
});
201+
}
202+
return { falls: falls.sort((a, b) => a.to - a.from - (b.to - b.from)), compared };
203+
}
204+
205+
/** The `id=status` pairs of a `perEntry.checks` string, for the exemption shape assertion. */
206+
function checkStatuses(checks: string): Map<string, string> {
207+
return new Map(checks.split(" ").map((pair) => pair.split("=") as [string, string]));
208+
}
209+
166210
/** Mean score over the routes measured in BOTH runs. The plain mean moves when the measured
167211
* population moves, which a mutation can do without making any route look better: an inert
168212
* try/catch takes 15 trivial routes off the exemption list and into the report, and a route joining
@@ -277,6 +321,10 @@ describeCorpus("mutation corpus over the real route tree", { timeout: ENTRY_TIME
277321
it("has a baseline worth mutating", () => {
278322
expect(baseline).not.toBeNull();
279323
expect(baseline!.entryPoints).toBeGreaterThan(300);
324+
// The falls assertions compare over the routes measured in both runs and pin that population
325+
// to `baseline.measured`, so the baseline itself has to be big enough that a broken scan
326+
// cannot produce a tiny population the mirror trivially holds over.
327+
expect(baseline!.measured).toBeGreaterThan(300);
280328
expect(baseline!.global).not.toBeNull();
281329
console.log(
282330
`[corpus] baseline global=${baseline!.global} mean=${baseline!.exactMean.toFixed(3)} ` +
@@ -327,19 +375,27 @@ describeCorpus("mutation corpus over the real route tree", { timeout: ENTRY_TIME
327375
expect([...baseline!.perEntry.keys()].filter((f) => !after.perEntry.has(f))).toEqual([]);
328376

329377
const rises = risesIn(baseline!, after);
378+
const { falls, compared } = fallsIn(baseline!, after);
330379
const common = commonMean(baseline!, after);
331380
console.log(
332381
`[corpus] ${mutation.id}: global ${baseline!.global} -> ${after.global} ` +
333382
`(mean ${baseline!.exactMean.toFixed(3)} -> ${after.exactMean.toFixed(3)}, ` +
334383
`common mean ${common.before.toFixed(3)} -> ${common.after.toFixed(3)}, ` +
335384
`measured ${baseline!.measured} -> ${after.measured}, files ${changed}, sites ${sites}, ` +
336-
`routes raised ${rises.length})` +
385+
`routes raised ${rises.length}, routes lowered ${falls.length})` +
337386
rises
338387
.slice(0, 3)
339388
.map(
340389
(r) =>
341390
`\n ${r.fileName} ${r.from}->${r.to}\n was: ${r.before}\n now: ${r.after}`
342391
)
392+
.join("") +
393+
falls
394+
.slice(0, 3)
395+
.map(
396+
(f) =>
397+
`\n ${f.fileName} ${f.from}->${f.to}\n was: ${f.before}\n now: ${f.after}`
398+
)
343399
.join("")
344400
);
345401

@@ -357,6 +413,37 @@ describeCorpus("mutation corpus over the real route tree", { timeout: ENTRY_TIME
357413
// figure above is where that trade is held to account.
358414
if (mutation.kind === "preserving") {
359415
expect(rises.map((r) => `${r.fileName} ${r.from}->${r.to}`)).toEqual([]);
416+
417+
// The mirror direction. A preserving edit must not make any route look WORSE either: a
418+
// fall here is a false accusation, the direction that gets the tool switched off, and it
419+
// went unasserted for three rounds while 19 entries regressed 104 routes. Together with
420+
// the rises assertion and the entry-point guards above, this pins per-route score
421+
// EQUALITY for a preserving entry. The comparison population is pinned to the whole
422+
// measured baseline first, so a silently shrunken population cannot pass vacuously.
423+
expect(compared).toBe(baseline!.measured);
424+
if (mutation.lowers === undefined) {
425+
expect(falls.map((f) => `${f.fileName} ${f.from}->${f.to}`)).toEqual([]);
426+
} else {
427+
// An exempted entry must still be falling, or the exemption is stale and has to be
428+
// removed deliberately rather than sitting as cover for the next defect.
429+
expect(falls.length).toBeGreaterThan(0);
430+
// And the falls must have exactly the measured residual shape the exemption was
431+
// granted for: `error-classification` moving pass -> not-applicable, every other
432+
// check's status unchanged, nothing anywhere moving to fail. Anything else is a new
433+
// defect hiding under the exemption.
434+
for (const fall of falls) {
435+
const before = checkStatuses(fall.before);
436+
const now = checkStatuses(fall.after);
437+
expect([...now.keys()].sort()).toEqual([...before.keys()].sort());
438+
for (const [id, was] of before) {
439+
const is = now.get(id);
440+
if (is === was) continue;
441+
expect(`${fall.fileName}: ${id} ${was}->${is}`).toBe(
442+
`${fall.fileName}: error-classification pass->not-applicable`
443+
);
444+
}
445+
}
446+
}
360447
}
361448
});
362449
}

internal-packages/observability-map/src/mutations.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ export type Mutation = {
4444
kind: MutationKind;
4545
/** What the rewrite does, in one line, for the corpus table in the report. */
4646
what: string;
47+
/**
48+
* Set only on a `preserving` entry that is EXPECTED to lower some routes' scores, with the
49+
* one-line reason on the entry itself. The mirror assertion in `mutationCorpus.test.ts` requires
50+
* falls to be empty for every preserving entry without this field, and for an entry with it,
51+
* requires falls to be nonzero and every fall to be exactly `error-classification` moving pass
52+
* to not-applicable with nothing moving to fail. Deliberately a per-entry field rather than a
53+
* set or a skip list: an exemption is a decision with a reason, enforced in both directions, and
54+
* it must not be a place entries get filed so the suite stays green.
55+
*/
56+
lowers?: string;
4757
/** The mutated file, or null when this file has nothing for the mutation to touch. */
4858
apply(fileName: string, source: string): MutationResult | null;
4959
};
@@ -335,11 +345,18 @@ function deadThrowAfter(id: string, what: string, wrap: (body: string) => string
335345
}
336346

337347
/** Wrap every route body in a single-shot wrapper, `open` before its statements and `close` after. */
338-
function wrapEveryBody(id: string, what: string, open: string, close: string): Mutation {
348+
function wrapEveryBody(
349+
id: string,
350+
what: string,
351+
open: string,
352+
close: string,
353+
lowers?: string
354+
): Mutation {
339355
return {
340356
id,
341357
kind: "preserving",
342358
what,
359+
...(lowers === undefined ? {} : { lowers }),
343360
apply(fileName, source) {
344361
const sf = parse(fileName, source);
345362
const edits: Edit[] = [];
@@ -541,7 +558,9 @@ export const MUTATIONS: Mutation[] = [
541558
"wrap-body-in-non-array-map",
542559
"wrap every route body in a non-array receiver's .map(...)",
543560
"return obsMapResult.map(async () => {",
544-
"});"
561+
"});",
562+
"moves every catch behind the iteration boundary; refused deciding catches cap at " +
563+
"not-applicable, so a pass legitimately becomes n/a (mechanism C ruling)"
545564
),
546565
// Round D item 3. `auth-scope` fired on any property at all whose value was a caller id, wherever
547566
// it sat, so one dead statement at the head of a body cleared it. These are the two halves: the
@@ -578,7 +597,9 @@ export const MUTATIONS: Mutation[] = [
578597
"wrap-body-in-non-array-filter",
579598
"wrap every route body in a non-array receiver's .filter(...)",
580599
"return obsMapPipe.filter(async () => {",
581-
"});"
600+
"});",
601+
"moves every catch behind the iteration boundary; refused deciding catches cap at " +
602+
"not-applicable, so a pass legitimately becomes n/a (mechanism C ruling)"
582603
),
583604

584605
{

0 commit comments

Comments
 (0)