Skip to content

Commit 0a95865

Browse files
committed
compute: a sole-suspect failure explains sibling failures; cells bottom-align the expander
A failure that pins several unproven toolchains no longer blames all of them when one of those versions is already the only suspect of another failure on the same release: the 9.1.1 JDK 25 failures on SDK 36 pinned build-tools 36.1.0, which had no success of its own and was reported as failing. Cells anchor the expander dots to the bottom edge, hide the secondary label when the column is too narrow for it, and the grid's minimum width grows so the label fits on most desktop viewports.
1 parent 9aa934b commit 0a95865

4 files changed

Lines changed: 91 additions & 36 deletions

File tree

‎shared/compute.ts‎

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -201,31 +201,59 @@ export function verifiedPairs(
201201

202202
/**
203203
* A failed build pins several toolchains at once and cannot say which one is
204-
* to blame, so a failure only implicates a toolchain version that no
205-
* successful build of the same release used.
204+
* to blame. A failure therefore implicates only toolchain versions that no
205+
* successful build of the same release used, and once one of its pinned
206+
* versions is the sole suspect of another failure, that version explains the
207+
* failure and the rest stay unjudged.
206208
*/
207209
function verifiedFor(
208210
packageName: string,
209211
version: string,
210212
verified: VerificationResult[],
211213
outcome: ResultClass,
212214
): VersionCompatibility["verified"] {
213-
const forThisVersion = (pairs: ReturnType<typeof verifiedPairs>) =>
214-
pairs.filter((pair) => pair.package === packageName && pair.version === version);
215-
const successes = forThisVersion(verifiedPairs(verified, "success"));
216-
const provenToWork = (pair: (typeof successes)[number]) =>
217-
successes.some((ok) => ok.toolchain === pair.toolchain && sameLine(ok.toolchainVersion, pair.toolchainVersion));
215+
const forThisVersion = (entries: VerificationResult[]) =>
216+
entries.filter((entry) => entry.package === packageName && entry.version === version);
217+
const successes = forThisVersion(verified).filter((entry) => classify(entry) === "success");
218+
const provenToWork = (toolchain: ToolchainKey, toolchainVersion: string) =>
219+
successes.some((ok) => {
220+
const okVersion = ok.toolchains[toolchain as Exclude<ToolchainKey, "node">];
221+
return okVersion !== undefined && sameLine(okVersion, toolchainVersion);
222+
});
218223

219224
const result: VersionCompatibility["verified"] = {};
220-
for (const pair of forThisVersion(verifiedPairs(verified, outcome))) {
221-
if (outcome !== "success" && provenToWork(pair)) {
222-
continue;
225+
const add = (toolchain: ToolchainKey, toolchainVersion: string) => {
226+
const list = (result[toolchain] ??= []);
227+
if (!list.includes(toolchainVersion)) {
228+
list.push(toolchainVersion);
229+
}
230+
};
231+
232+
if (outcome === "success") {
233+
for (const pair of verifiedPairs(verified, "success")) {
234+
if (pair.package === packageName && pair.version === version) {
235+
add(pair.toolchain, pair.toolchainVersion);
236+
}
223237
}
224-
const list = (result[pair.toolchain] ??= []);
225-
if (!list.includes(pair.toolchainVersion)) {
226-
list.push(pair.toolchainVersion);
238+
} else {
239+
const suspects = forThisVersion(verified)
240+
.filter((entry) => classify(entry) === outcome)
241+
.map((entry) =>
242+
(Object.entries(entry.toolchains) as Array<[ToolchainKey, string]>).filter(
243+
([toolchain, toolchainVersion]) => !provenToWork(toolchain, toolchainVersion),
244+
),
245+
);
246+
const culprits = suspects.filter((pins) => pins.length === 1).map(([pin]) => pin);
247+
for (const pins of suspects) {
248+
const explained = culprits.find(([toolchain, toolchainVersion]) =>
249+
pins.some(([key, value]) => key === toolchain && sameLine(value, toolchainVersion)),
250+
);
251+
for (const [toolchain, toolchainVersion] of explained ? [explained] : pins) {
252+
add(toolchain, toolchainVersion);
253+
}
227254
}
228255
}
256+
229257
for (const list of Object.values(result)) {
230258
list.sort((a, b) => semver.rcompare(semver.coerce(a) ?? "0.0.0", semver.coerce(b) ?? "0.0.0"));
231259
}

‎test/compute.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,28 @@ describe("failure attribution", () => {
282282
expect(cellFor(doc, "@nativescript/android", "9.0.5", "buildTools", "37.0.0").state).toBe("unverified");
283283
expect(cellFor(doc, "@nativescript/android", "9.0.5", "jdk", "25").state).toBe("unsupported");
284284
});
285+
286+
it("does not blame an unproven toolchain when a sole suspect explains the failure", () => {
287+
const doc = buildDocument({
288+
generatedAt: "2026-09-09T00:00:00.000Z",
289+
toolchains: { xcode: [], cocoapods: [], compileSdk: [{ version: "37" }, { version: "36" }], buildTools: [{ version: "37.0.0" }, { version: "36.1.0" }], jdk: [{ version: "25" }, { version: "21" }], node: [] },
290+
packages: [
291+
{
292+
spec: { name: "@nativescript/android", toolchains: ["compileSdk", "buildTools", "jdk"], keep: 5 },
293+
document: { distTags: {}, manifests: [{ version: "9.1.1", requirements: { jdk: ">=17" } }] },
294+
},
295+
],
296+
overrides: { requirements: [], advisories: [] },
297+
verified: [
298+
{ package: "@nativescript/android", version: "9.1.1", toolchains: { compileSdk: "37", buildTools: "37.0.0", jdk: "21" }, with: { nativescript: "9.1.1", node: "24" }, recordedAt: "2026-09-09T00:00:00Z" },
299+
{ package: "@nativescript/android", version: "9.1.1", toolchains: { compileSdk: "37", buildTools: "37.0.0", jdk: "25" }, with: { nativescript: "9.1.1", node: "24" }, outcome: "failure", attempts: 2, recordedAt: "2026-09-09T00:00:00Z" },
300+
// build-tools 36.1.0 has no success of its own, but the failure is already explained by JDK 25.
301+
{ package: "@nativescript/android", version: "9.1.1", toolchains: { compileSdk: "36", buildTools: "36.1.0", jdk: "25" }, with: { nativescript: "9.1.1", node: "24" }, outcome: "failure", attempts: 2, recordedAt: "2026-09-09T00:00:00Z" },
302+
],
303+
});
304+
const v911 = doc.packages["@nativescript/android"].versions["9.1.1"];
305+
expect(v911.failed).toEqual({ jdk: ["25"] });
306+
expect(cellFor(doc, "@nativescript/android", "9.1.1", "buildTools", "36.1.0").state).toBe("unverified");
307+
expect(cellFor(doc, "@nativescript/android", "9.1.1", "compileSdk", "36").state).toBe("unverified");
308+
});
285309
});

‎web/src/components/CompatCell.vue‎

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,34 +50,37 @@ const background = computed(() => {
5050

5151
<template>
5252
<td
53-
class="relative cursor-pointer px-3 pt-3.5 pb-2.5 text-center align-top transition-colors outline-none focus-visible:ring-2 focus-visible:ring-declared/50 focus-visible:ring-inset"
53+
class="relative cursor-pointer px-3 pt-3.5 pb-6 text-center align-top transition-colors outline-none focus-visible:ring-2 focus-visible:ring-declared/50 focus-visible:ring-inset"
5454
:class="background"
5555
tabindex="0"
5656
:title="`${STATE_HELP[state]} ${open ? 'Click to collapse.' : 'Click for details.'}`"
5757
@click="emit('toggle')"
5858
@keydown.enter.prevent="emit('toggle')"
5959
@keydown.space.prevent="emit('toggle')"
6060
>
61-
<StateIcon :state="state" :size="22" class="mx-auto" />
62-
<span class="mt-1 block tabular-nums" :class="TEXT[state]">{{ primary?.version ?? "No data" }}</span>
63-
<span
64-
v-if="secondary"
65-
class="mt-0.5 flex items-center justify-center gap-1 text-[11px] tabular-nums"
66-
:title="`${secondary.version}: ${secondary.cell.reason}`"
67-
>
68-
<StateIcon :state="secondary.cell.state" :size="12" />
69-
<span class="font-medium" :class="TEXT[secondary.cell.state]">{{ secondary.version }}</span>
70-
<span class="text-neutral-500 dark:text-neutral-400">{{ SECONDARY_LABEL[secondary.cell.state] }}</span>
71-
</span>
72-
<span
73-
v-if="summary.prerelease"
74-
class="mt-0.5 flex items-center justify-center gap-1 text-[11px] tabular-nums text-neutral-500 dark:text-neutral-400"
75-
:title="`${summary.prerelease.version} beta: ${summary.prerelease.cell.reason}`"
76-
>
77-
<StateIcon :state="summary.prerelease.cell.state" :size="12" />
78-
{{ summary.prerelease.version }}
79-
<BetaChip />
80-
</span>
61+
<!-- The label hides when the column is narrower than the line. -->
62+
<div class="@container">
63+
<StateIcon :state="state" :size="22" class="mx-auto" />
64+
<span class="mt-1 block tabular-nums" :class="TEXT[state]">{{ primary?.version ?? "No data" }}</span>
65+
<span
66+
v-if="secondary"
67+
class="mt-0.5 flex items-center justify-center gap-1 text-[11px] tabular-nums"
68+
:title="`${secondary.version}: ${secondary.cell.reason}`"
69+
>
70+
<StateIcon :state="secondary.cell.state" :size="12" />
71+
<span class="font-medium" :class="TEXT[secondary.cell.state]">{{ secondary.version }}</span>
72+
<span class="text-neutral-500 @max-[7rem]:hidden dark:text-neutral-400">{{ SECONDARY_LABEL[secondary.cell.state] }}</span>
73+
</span>
74+
<span
75+
v-if="summary.prerelease"
76+
class="mt-0.5 flex items-center justify-center gap-1 text-[11px] tabular-nums text-neutral-500 dark:text-neutral-400"
77+
:title="`${summary.prerelease.version} beta: ${summary.prerelease.cell.reason}`"
78+
>
79+
<StateIcon :state="summary.prerelease.cell.state" :size="12" />
80+
{{ summary.prerelease.version }}
81+
<BetaChip />
82+
</span>
83+
</div>
8184
<span
8285
v-if="flagged"
8386
class="absolute top-1.5 right-2 flex items-center gap-0.5 text-xs font-bold text-advisory"
@@ -86,6 +89,6 @@ const background = computed(() => {
8689
<StateIcon state="advisory" :size="16" />
8790
<span v-if="summary.advisories.length > 1">{{ summary.advisories.length }}</span>
8891
</span>
89-
<span class="mt-0.5 block text-xs leading-none tracking-widest text-neutral-400" aria-hidden="true">···</span>
92+
<span class="absolute inset-x-0 bottom-2 block text-xs leading-none tracking-widest text-neutral-400" aria-hidden="true">···</span>
9093
</td>
9194
</template>

‎web/src/components/CompatGrid.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const headCell = `sticky z-10 bg-neutral-50 text-center font-semibold dark:bg-ne
8686
<!-- Below the desktop breakpoint the grid scrolls sideways inside this wrapper; above it, the
8787
table fits the page, the wrapper does not clip, and the header can stay pinned. -->
8888
<div class="overflow-x-auto border-y border-neutral-200 lg:overflow-visible dark:border-neutral-700/80">
89-
<table class="w-full min-w-[60rem] table-fixed border-separate border-spacing-0 text-sm">
89+
<table class="w-full min-w-[66rem] table-fixed border-separate border-spacing-0 text-sm">
9090
<colgroup>
9191
<col class="w-88" />
9292
<col v-for="key in ALL_KEYS" :key="key" />

0 commit comments

Comments
 (0)